diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 66% rename from .eslintrc.js rename to .eslintrc.cjs index a51ab03..a385c67 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -1,7 +1,6 @@ module.exports = { env: { browser: true, - commonjs: true, es2021: true, }, extends: ['standard', 'plugin:prettier/recommended', 'plugin:n/recommended'], @@ -10,24 +9,16 @@ module.exports = { env: { node: true, }, - files: ['.eslintrc.{js,cjs}'], + files: ['.eslintrc.cjs', '.prettierrc.cjs'], parserOptions: { sourceType: 'script', }, }, - { - env: { - node: true, - }, - files: ['tests/**/*.js'], - parserOptions: { - sourceType: 'module', - }, - }, ], ignorePatterns: ['tests/fixtures/**/*.js'], parserOptions: { ecmaVersion: 'latest', + sourceType: 'module', }, rules: {}, }; diff --git a/.prettierrc.js b/.prettierrc.cjs similarity index 100% rename from .prettierrc.js rename to .prettierrc.cjs diff --git a/package.json b/package.json index 61e454a..19d051c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "license": "ISC", "author": "", + "type": "module", "exports": { ".": "./src/parser/gjs-gts-parser.js", "./hbs": "./src/parser/hbs-parser.js", @@ -23,7 +24,7 @@ "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"", "lint:js": "eslint . --max-warnings=0", "lint:js:fix": "eslint . --fix --max-warnings=0", - "lint:package": "pnpm publint", + "lint:package": "publint", "bench": "node --expose-gc tests/parser.bench.mjs", "bench:compare": "node scripts/bench-compare.mjs", "bench:summary": "./scripts/local-bench-summary.sh", diff --git a/src/parser/gjs-gts-parser.js b/src/parser/gjs-gts-parser.js index cdc16e2..8c753f5 100644 --- a/src/parser/gjs-gts-parser.js +++ b/src/parser/gjs-gts-parser.js @@ -1,13 +1,11 @@ -const tsconfigUtils = require('@typescript-eslint/tsconfig-utils'); -const babelParser = require('@babel/eslint-parser/experimental-worker'); -const { registerParsedFile } = require('../preprocessor/noop'); -const { - patchTs, - replaceExtensions, - syncMtsGtsSourceFiles, - typescriptParser, -} = require('./ts-patch'); -const { transformForLint, preprocessGlimmerTemplates, convertAst } = require('./transforms'); +import { createRequire } from 'node:module'; +import tsconfigUtils from '@typescript-eslint/tsconfig-utils'; +import babelParser from '@babel/eslint-parser/experimental-worker'; +import { registerParsedFile } from '../preprocessor/noop.js'; +import { patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser } from './ts-patch.js'; +import { transformForLint, preprocessGlimmerTemplates, convertAst } from './transforms.js'; + +const require = createRequire(import.meta.url); /** * implements https://eslint.org/docs/latest/extend/custom-parsers @@ -131,86 +129,86 @@ function getAllowJs(options) { /** * @type {import('eslint').ParserModule} */ -module.exports = { - meta: { - name: 'ember-eslint-parser', - version: '*', - }, - - parseForESLint(code, options) { - const allowGjsWasSet = options.allowGjs !== undefined; - const allowGjs = allowGjsWasSet ? options.allowGjs : getAllowJs(options); - let actualAllowGjs; - // Only patch TypeScript if we actually need it. - if (options.programs || options.projectService || options.project) { - ({ allowGjs: actualAllowGjs } = patchTs({ allowGjs })); - } - registerParsedFile(options.filePath); - let jsCode = code; - const info = transformForLint(code, options.filePath); - jsCode = info.output; +export const meta = { + name: 'ember-eslint-parser', + version: '*', +}; - const isTypescript = options.filePath.endsWith('.gts') || options.filePath.endsWith('.ts'); - let useTypescript = true; +export function parseForESLint(code, options) { + const allowGjsWasSet = options.allowGjs !== undefined; + const allowGjs = allowGjsWasSet ? options.allowGjs : getAllowJs(options); + let actualAllowGjs; + // Only patch TypeScript if we actually need it. + if (options.programs || options.projectService || options.project) { + ({ allowGjs: actualAllowGjs } = patchTs({ allowGjs })); + } + registerParsedFile(options.filePath); + let jsCode = code; + const info = transformForLint(code, options.filePath); + jsCode = info.output; - if (options.useBabel || !typescriptParser) { - useTypescript = false; - } + const isTypescript = options.filePath.endsWith('.gts') || options.filePath.endsWith('.ts'); + let useTypescript = true; - let result = null; - const filePath = options.filePath; - if (options.project || options.projectService) { - jsCode = replaceExtensions(jsCode); - } + if (options.useBabel || !typescriptParser) { + useTypescript = false; + } - if (isTypescript && !typescriptParser) { - throw new Error('Please install typescript to process gts'); - } + let result = null; + const filePath = options.filePath; + if (options.project || options.projectService) { + jsCode = replaceExtensions(jsCode); + } - try { - result = - isTypescript || useTypescript - ? typescriptParser.parseForESLint(jsCode, { - ...options, - ranges: true, - extraFileExtensions: ['.gts', '.gjs'], - filePath, - }) - : babelParser.parseForESLint(jsCode, { - ...options, - requireConfigFile: false, - ranges: true, - }); - if (!info.templateInfos?.length) { - return result; - } - const preprocessedResult = preprocessGlimmerTemplates(info, code); - preprocessedResult.code = code; - const { templateVisitorKeys } = preprocessedResult; - const visitorKeys = { ...result.visitorKeys, ...templateVisitorKeys }; - result.isTypescript = isTypescript || useTypescript; - convertAst(result, preprocessedResult, visitorKeys); - if (result.services?.program) { - // Compare allowJs with the actual program's compiler options - const programAllowJs = result.services.program.getCompilerOptions?.()?.allowJs; - if ( - !allowGjsWasSet && - programAllowJs !== undefined && - actualAllowGjs !== undefined && - actualAllowGjs !== programAllowJs - ) { - // eslint-disable-next-line no-console - console.warn( - '[ember-eslint-parser] allowJs does not match the actual program. Consider setting allowGjs explicitly.\n' + - ` Current: ${allowGjs}, Program: ${programAllowJs}` - ); - } - syncMtsGtsSourceFiles(result.services.program); + if (isTypescript && !typescriptParser) { + throw new Error('Please install typescript to process gts'); + } + + try { + result = + isTypescript || useTypescript + ? typescriptParser.parseForESLint(jsCode, { + ...options, + ranges: true, + extraFileExtensions: ['.gts', '.gjs'], + filePath, + }) + : babelParser.parseForESLint(jsCode, { + ...options, + requireConfigFile: false, + ranges: true, + }); + if (!info.templateInfos?.length) { + return result; + } + const preprocessedResult = preprocessGlimmerTemplates(info, code); + preprocessedResult.code = code; + const { templateVisitorKeys } = preprocessedResult; + const visitorKeys = { ...result.visitorKeys, ...templateVisitorKeys }; + result.isTypescript = isTypescript || useTypescript; + convertAst(result, preprocessedResult, visitorKeys); + if (result.services?.program) { + // Compare allowJs with the actual program's compiler options + const programAllowJs = result.services.program.getCompilerOptions?.()?.allowJs; + if ( + !allowGjsWasSet && + programAllowJs !== undefined && + actualAllowGjs !== undefined && + actualAllowGjs !== programAllowJs + ) { + // eslint-disable-next-line no-console + console.warn( + '[ember-eslint-parser] allowJs does not match the actual program. Consider setting allowGjs explicitly.\n' + + ` Current: ${allowGjs}, Program: ${programAllowJs}` + ); } - return { ...result, visitorKeys }; - } catch (e) { - console.error(e); - throw e; + syncMtsGtsSourceFiles(result.services.program); } - }, -}; + return { ...result, visitorKeys }; + } catch (e) { + console.error(e); + throw e; + } +} + +export default { meta, parseForESLint }; diff --git a/src/parser/hbs-parser.js b/src/parser/hbs-parser.js index 8bd2b39..9722a74 100644 --- a/src/parser/hbs-parser.js +++ b/src/parser/hbs-parser.js @@ -1,6 +1,6 @@ -const eslintScope = require('eslint-scope'); -const DocumentLines = require('../utils/document'); -const { processGlimmerTemplate, buildGlimmerVisitorKeys } = require('./transforms'); +import * as eslintScope from 'eslint-scope'; +import DocumentLines from '../utils/document.js'; +import { processGlimmerTemplate, buildGlimmerVisitorKeys } from './transforms.js'; // Constant: Program + all Glimmer node types. Computed once at module load. const hbsVisitorKeys = { Program: ['body'], ...buildGlimmerVisitorKeys() }; @@ -17,76 +17,76 @@ const hbsVisitorKeys = { Program: ['body'], ...buildGlimmerVisitorKeys() }; /** * @type {import('eslint').ParserModule} */ -module.exports = { - meta: { - name: 'ember-eslint-parser/hbs', - version: '*', - }, +export const meta = { + name: 'ember-eslint-parser/hbs', + version: '*', +}; - parseForESLint(code, options) { - const filePath = (options && options.filePath) || ''; - const codeLines = new DocumentLines(code); +export function parseForESLint(code, options) { + const filePath = (options && options.filePath) || ''; + const codeLines = new DocumentLines(code); - let result; - try { - result = processGlimmerTemplate({ - templateContent: code, - codeLines, - templateRange: [0, code.length], + let result; + try { + result = processGlimmerTemplate({ + templateContent: code, + codeLines, + templateRange: [0, code.length], + }); + } catch (e) { + // Transform glimmer parse error to ESLint-compatible error + const loc = e.location || (e.hash && e.hash.loc); + if (loc && loc.start) { + const err = Object.assign(new SyntaxError(e.message), { + lineNumber: loc.start.line, + column: loc.start.column, + index: codeLines.positionToOffset(loc.start), + fileName: filePath, }); - } catch (e) { - // Transform glimmer parse error to ESLint-compatible error - const loc = e.location || (e.hash && e.hash.loc); - if (loc && loc.start) { - const err = Object.assign(new SyntaxError(e.message), { - lineNumber: loc.start.line, - column: loc.start.column, - index: codeLines.positionToOffset(loc.start), - fileName: filePath, - }); - throw err; - } - throw e; + throw err; } + throw e; + } + + const { ast: templateNode, comments } = result; + + // Wrap in a synthetic Program node (required by ESLint) + const program = { + type: 'Program', + body: [templateNode], + tokens: templateNode.tokens, + comments, + range: [0, code.length], + start: 0, + end: code.length, + loc: { + start: { line: 1, column: 0 }, + end: codeLines.offsetToPosition(code.length), + }, + }; - const { ast: templateNode, comments } = result; + // Build visitor keys: Program + all Glimmer node types + const visitorKeys = hbsVisitorKeys; - // Wrap in a synthetic Program node (required by ESLint) - const program = { + // Create an empty scope manager. + // For HBS, all locals are assumed to be defined at runtime, + // so we don't track variable references (no no-undef errors). + const scopeManager = eslintScope.analyze( + { type: 'Program', - body: [templateNode], - tokens: templateNode.tokens, - comments, + body: [], range: [0, code.length], - start: 0, - end: code.length, - loc: { - start: { line: 1, column: 0 }, - end: codeLines.offsetToPosition(code.length), - }, - }; + loc: program.loc, + }, + { range: true } + ); - // Build visitor keys: Program + all Glimmer node types - const visitorKeys = hbsVisitorKeys; + return { + ast: program, + scopeManager, + visitorKeys, + services: {}, + }; +} - // Create an empty scope manager. - // For HBS, all locals are assumed to be defined at runtime, - // so we don't track variable references (no no-undef errors). - const scopeManager = eslintScope.analyze( - { - type: 'Program', - body: [], - range: [0, code.length], - loc: program.loc, - }, - { range: true } - ); - - return { - ast: program, - scopeManager, - visitorKeys, - services: {}, - }; - }, -}; +export default { meta, parseForESLint }; diff --git a/src/parser/transforms.js b/src/parser/transforms.js index d20f0d9..b2b3509 100644 --- a/src/parser/transforms.js +++ b/src/parser/transforms.js @@ -1,11 +1,22 @@ -const ContentTag = require('content-tag'); -const glimmer = require('@glimmer/syntax'); -const { visitorKeys: glimmerVisitorKeys } = glimmer; -const DocumentLines = require('../utils/document'); -const { Reference, Scope, Variable, Definition } = require('eslint-scope'); -const htmlTagsSet = new Set(require('html-tags').default); -const svgTagsSet = new Set(require('svg-tags')); -const mathMLTagsSet = new Set(require('mathml-tag-names').mathmlTagNames); +import { createRequire } from 'node:module'; +import ContentTag from 'content-tag'; +import { + visitorKeys as glimmerVisitorKeys, + traverse as glimmerTraverse, + preprocess as glimmerPreprocess, + isKeyword as glimmerIsKeyword, +} from '@glimmer/syntax'; +import DocumentLines from '../utils/document.js'; +import { Reference, Scope, Variable, Definition } from 'eslint-scope'; +import htmlTags from 'html-tags'; +import svgTags from 'svg-tags'; +import { mathmlTagNames } from 'mathml-tag-names'; + +const htmlTagsSet = new Set(htmlTags); +const svgTagsSet = new Set(svgTags); +const mathMLTagsSet = new Set(mathmlTagNames); + +const require = createRequire(import.meta.url); let TypescriptScope = null; try { @@ -255,7 +266,7 @@ function collectNodes(ast) { const textNodes = []; const emptyTextNodes = []; - glimmer.traverse(ast, { + glimmerTraverse(ast, { All(node, path) { node.parent = path.parentNode; allNodes.push(node); @@ -377,7 +388,7 @@ function processGlimmerTemplate({ templateContent, codeLines, templateRange, tok end: codeLines.offsetToPosition(range[1]), }); - const ast = glimmer.preprocess(templateContent, { mode: 'codemod' }); + const ast = glimmerPreprocess(templateContent, { mode: 'codemod' }); const { allNodes, comments, textNodes, emptyTextNodes } = collectNodes(ast); // Fix ranges, locs, and prefix types with "Glimmer" @@ -463,7 +474,7 @@ function processGlimmerTemplate({ templateContent, codeLines, templateRange, tok * @param code * @return {{templateVisitorKeys: {}, comments: *[], templateInfos: {templateRange: *, range: *, replacedRange: *}[]}} */ -module.exports.preprocessGlimmerTemplates = function preprocessGlimmerTemplates(info, code) { +export function preprocessGlimmerTemplates(info, code) { const templateInfos = info.templateInfos.map((r) => ({ utf16Range: [r.range.startUtf16Codepoint, r.range.endUtf16Codepoint], })); @@ -489,7 +500,7 @@ module.exports.preprocessGlimmerTemplates = function preprocessGlimmerTemplates( templateInfos, comments: allComments, }; -}; +} /** * traverses the AST and replaces the transformed template parts with the Glimmer @@ -502,7 +513,7 @@ module.exports.preprocessGlimmerTemplates = function preprocessGlimmerTemplates( * @param preprocessedResult * @param visitorKeys */ -module.exports.convertAst = function convertAst(result, preprocessedResult, visitorKeys) { +export function convertAst(result, preprocessedResult, visitorKeys) { const templateInfos = preprocessedResult.templateInfos; let counter = 0; result.ast.comments.push(...preprocessedResult.comments); @@ -546,7 +557,7 @@ module.exports.convertAst = function convertAst(result, preprocessedResult, visi if (node.type === 'GlimmerPathExpression' && node.head.type === 'VarHead') { const name = node.head.name; - if (glimmer.isKeyword(name)) { + if (glimmerIsKeyword(name)) { return null; } const { scope, variable } = findVarInParentScopes(result.scopeManager, path, name) || {}; @@ -635,12 +646,11 @@ module.exports.convertAst = function convertAst(result, preprocessedResult, visi if (counter !== templateInfos.length) { throw new Error('failed to process all templates'); } -}; +} -const replaceRange = function replaceRange(s, start, end, substitute) { +export const replaceRange = function replaceRange(s, start, end, substitute) { return s.slice(0, start) + substitute + s.slice(end); }; -module.exports.replaceRange = replaceRange; const processor = new ContentTag.Preprocessor(); @@ -676,7 +686,7 @@ function createError(code, message, fileName, start, end = start) { return new EmberParserError(message, fileName, { end, start }); } -module.exports.transformForLint = function transformForLint(code, fileName) { +export function transformForLint(code, fileName) { let jsCode = code; /** * @@ -772,9 +782,6 @@ module.exports.transformForLint = function transformForLint(code, fileName) { templateInfos: result, output: jsCode, }; -}; +} -module.exports.traverse = traverse; -module.exports.tokenize = tokenize; -module.exports.processGlimmerTemplate = processGlimmerTemplate; -module.exports.buildGlimmerVisitorKeys = buildGlimmerVisitorKeys; +export { traverse, tokenize, processGlimmerTemplate, buildGlimmerVisitorKeys }; diff --git a/src/parser/ts-patch.js b/src/parser/ts-patch.js index 4076ff3..ebbec98 100644 --- a/src/parser/ts-patch.js +++ b/src/parser/ts-patch.js @@ -1,6 +1,8 @@ -const fs = require('node:fs'); -const { transformForLint } = require('./transforms'); -const { replaceRange } = require('./transforms'); +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import { transformForLint, replaceRange } from './transforms.js'; + +const require = createRequire(import.meta.url); let patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser, isPatched, allowGjs; @@ -162,9 +164,4 @@ try { syncMtsGtsSourceFiles = () => null; } -module.exports = { - patchTs, - replaceExtensions, - syncMtsGtsSourceFiles, - typescriptParser, -}; +export { patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser }; diff --git a/src/preprocessor/noop.js b/src/preprocessor/noop.js index b173262..94d188a 100644 --- a/src/preprocessor/noop.js +++ b/src/preprocessor/noop.js @@ -9,24 +9,27 @@ const parsedFiles = new Set(); -module.exports = { - registerParsedFile(f) { - parsedFiles.add(f); - }, - preprocess: undefined, - postprocess: (messages, fileName) => { - const msgs = messages.flat(); - if (!parsedFiles.has(fileName)) { - msgs[0] = msgs[0] || { - message: '', - }; - msgs[0].message += '\n'; - msgs[0].message += - 'To lint Gjs/Gts files please follow the setup guide at https://github.com/ember-cli/eslint-plugin-ember#gtsgjs' + - '\nNote that this error can also happen if you have multiple versions of eslint-plugin-ember in your node_modules'; - } - parsedFiles.delete(fileName); // required for tests - return msgs; - }, - supportsAutofix: true, +export function registerParsedFile(f) { + parsedFiles.add(f); +} + +export const preprocess = undefined; + +export const postprocess = (messages, fileName) => { + const msgs = messages.flat(); + if (!parsedFiles.has(fileName)) { + msgs[0] = msgs[0] || { + message: '', + }; + msgs[0].message += '\n'; + msgs[0].message += + 'To lint Gjs/Gts files please follow the setup guide at https://github.com/ember-cli/eslint-plugin-ember#gtsgjs' + + '\nNote that this error can also happen if you have multiple versions of eslint-plugin-ember in your node_modules'; + } + parsedFiles.delete(fileName); // required for tests + return msgs; }; + +export const supportsAutofix = true; + +export default { registerParsedFile, preprocess, postprocess, supportsAutofix }; diff --git a/src/utils/document.js b/src/utils/document.js index fb97990..3b2f1d3 100644 --- a/src/utils/document.js +++ b/src/utils/document.js @@ -98,5 +98,5 @@ function isLineBreak(ch) { ); } -module.exports = DocumentLines; -module.exports.isLineBreak = isLineBreak; +export default DocumentLines; +export { isLineBreak };