From df8604b518d5199cd6224a64cc3f8891094704bf Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Fri, 14 Jun 2024 14:26:30 +0800 Subject: [PATCH] Transform `@typescript-eslint/*` to ESM (#16402) --- cspell.json | 1 + scripts/build/build-javascript-module.js | 14 -- scripts/build/config.js | 218 +++++++++--------- .../build/esbuild-plugins/replace-module.js | 41 ++-- scripts/build/esmify-typescript-eslint.js | 146 ++++++++++++ scripts/build/modify-typescript-module.js | 83 ++++--- src/language-js/parse/typescript.js | 11 +- 7 files changed, 322 insertions(+), 192 deletions(-) create mode 100644 scripts/build/esmify-typescript-eslint.js diff --git a/cspell.json b/cspell.json index e008d732b..c88313091 100644 --- a/cspell.json +++ b/cspell.json @@ -78,6 +78,7 @@ "Ericsburgh", "Esben", "eslump", + "esmify", "esproposal", "execa", "Fabio", diff --git a/scripts/build/build-javascript-module.js b/scripts/build/build-javascript-module.js index 4f7f34f56..ef1e26846 100644 --- a/scripts/build/build-javascript-module.js +++ b/scripts/build/build-javascript-module.js @@ -54,20 +54,6 @@ function getEsbuildOptions({ file, files, shouldCollectLicenses, cliOptions }) { module: "*", process: transform, }, - // Transform `require("typescript")` to `import` - { - module: "*", - process(text, file) { - text = text.replaceAll( - /(?:const|let|var) (\w+) = (?:__importStar\(require\("typescript"\)\)|require\("typescript"\));/g, - 'import * as $1 from "typescript";', - ); - if (/require\(["']typescript["']\)/.test(text)) { - throw new Error(`Unexpected \`require("typescript")\` in ${file}.`); - } - return text; - }, - }, // #12493, not sure what the problem is, but replace the cjs version with esm version seems fix it { module: require.resolve("tslib"), diff --git a/scripts/build/config.js b/scripts/build/config.js index cf06fcf18..6f22b1ac9 100644 --- a/scripts/build/config.js +++ b/scripts/build/config.js @@ -3,12 +3,14 @@ import path from "node:path"; import url from "node:url"; import createEsmUtils from "esm-utils"; +import { outdent } from "outdent"; import { copyFile, DIST_DIR, PROJECT_ROOT } from "../utils/index.js"; import buildJavascriptModule from "./build-javascript-module.js"; import buildLicense from "./build-license.js"; import buildPackageJson from "./build-package-json.js"; import buildTypes from "./build-types.js"; +import esmifyTypescriptEslint from "./esmify-typescript-eslint.js"; import modifyTypescriptModule from "./modify-typescript-module.js"; import { getPackageFile } from "./utils.js"; @@ -124,68 +126,6 @@ const pluginFiles = [ "@typescript-eslint/typescript-estree/dist/parser.js", ), }, - { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/parser.js", - ), - process(text) { - text = text - .replace('require("./create-program/createDefaultProgram")', "{}") - .replace('require("./create-program/createIsolatedProgram")', "{}") - .replace('require("./create-program/createProjectProgram")', "{}") - .replace('require("./create-program/useProvidedPrograms")', "{}") - .replace( - 'require("./semantic-or-syntactic-errors")', - "{getFirstSemanticOrSyntacticError(){}}", - ); - return text; - }, - }, - { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/parseSettings/createParseSettings.js", - ), - process(text) { - return text - .replace('require("./resolveProjectList")', "{}") - .replace( - 'require("../create-program/shared")', - "{ensureAbsolutePath: path => path}", - ) - .replace( - "process.cwd()", - JSON.stringify("/prettier-security-dirname-placeholder"), - ) - .replace( - "parseSettings.projects = ", - "parseSettings.projects = [] || ", - ); - }, - }, - { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/parseSettings/inferSingleRun.js", - ), - text: "exports.inferSingleRun = () => false;", - }, - { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/parseSettings/ExpiringCache.js", - ), - text: "exports.ExpiringCache = class {};", - }, - { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/parseSettings/getProjectConfigFiles.js", - ), - text: "exports.resolveProjectList = () => [];", - }, - { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/parseSettings/warnAboutTSVersion.js", - ), - text: "exports.warnAboutTSVersion = () => {};", - }, { module: getPackageFile( "@typescript-eslint/typescript-estree/dist/create-program/getScriptKind.js", @@ -198,54 +138,115 @@ const pluginFiles = [ }, { module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/version-check.js", + "@typescript-eslint/typescript-estree/dist/parseSettings/createParseSettings.js", ), - text: "exports.typescriptVersionIsAtLeast = new Proxy({}, {get: () => true})", + process(text) { + return text + .replace( + "process.cwd()", + JSON.stringify("/prettier-security-dirname-placeholder"), + ) + .replace( + "parseSettings.projects = ", + "parseSettings.projects = true ? new Map() : ", + ); + }, }, { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/jsx/xhtml-entities.js", - ), - text: "exports.xhtmlEntities = {};", + module: "*", + process: esmifyTypescriptEslint, }, { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/create-program/createProjectService.js", - ), - text: "", - }, - { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/create-program/getWatchProgramsForProjects.js", - ), - text: "", - }, - { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/create-program/describeFilePath.js", - ), - text: "", - }, - { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/create-program/createProjectProgram.js", - ), - text: "", - }, - { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/useProgramFromProjectService.js", - ), - text: "", + module: "*", + process(text, file) { + if (/require\(["'](?:typescript|ts-api-utils)["']\)/.test(text)) { + throw new Error(`Unexpected \`require("typescript")\` in ${file}.`); + } + + return text; + }, }, + ...[ + { + file: "@typescript-eslint/typescript-estree/dist/parseSettings/inferSingleRun.js", + text: "export const inferSingleRun = () => false;", + }, + { + file: "@typescript-eslint/typescript-estree/dist/parseSettings/ExpiringCache.js", + text: outdent` + export const DEFAULT_TSCONFIG_CACHE_DURATION_SECONDS = undefined; + export const ExpiringCache = class {}; + `, + }, + { + file: "@typescript-eslint/typescript-estree/dist/parseSettings/getProjectConfigFiles.js", + text: "export const resolveProjectList = () => [];", + }, + { + file: "@typescript-eslint/typescript-estree/dist/parseSettings/resolveProjectList.js", + text: "export const resolveProjectList = () => [];", + }, + { + file: "@typescript-eslint/typescript-estree/dist/parseSettings/warnAboutTSVersion.js", + text: "export const warnAboutTSVersion = () => {};", + }, + { + file: "@typescript-eslint/typescript-estree/dist/version-check.js", + text: "export const typescriptVersionIsAtLeast = new Proxy({}, {get: () => true})", + }, + { + file: "@typescript-eslint/typescript-estree/dist/jsx/xhtml-entities.js", + text: "export const xhtmlEntities = {};", + }, + { + file: "@typescript-eslint/typescript-estree/dist/simple-traverse.js", + text: "export const simpleTraverse = () => {};", + }, + { + file: "@typescript-eslint/typescript-estree/dist/create-program/shared.js", + text: "export const ensureAbsolutePath = path => path;", + }, + { + file: "@typescript-eslint/typescript-estree/dist/create-program/createIsolatedProgram.js", + text: "export const createIsolatedProgram = () => {};", + }, + { + file: "@typescript-eslint/typescript-estree/dist/create-program/useProvidedPrograms.js", + text: outdent` + export const useProvidedPrograms = () => {}; + export const createProgramFromConfigFile = () => {}; + `, + }, + { + file: "@typescript-eslint/typescript-estree/dist/create-program/createProjectService.js", + text: "export const createProjectService = () => {};", + }, + { + file: "@typescript-eslint/typescript-estree/dist/create-program/getWatchProgramsForProjects.js", + text: "export const getWatchProgramsForProjects = () => {};", + }, + "@typescript-eslint/typescript-estree/dist/create-program/describeFilePath.js", + { + file: "@typescript-eslint/typescript-estree/dist/create-program/createProjectProgram.js", + text: "export const createProjectProgram = () => {};", + }, + { + file: "@typescript-eslint/typescript-estree/dist/useProgramFromProjectService.js", + text: "export const useProgramFromProjectService = () => {};", + }, + { + file: "@typescript-eslint/typescript-estree/dist/semantic-or-syntactic-errors.js", + text: "export const getFirstSemanticOrSyntacticError = () => {};", + }, + ].map((options) => { + options = typeof options === "string" ? { file: options } : options; + return { + module: getPackageFile(options.file), + text: options.text || "export {};", + }; + }), // Only needed if `range`/`loc` in parse options is `false` - { - module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/ast-converter.js", - ), - process: (text) => text.replace('require("./simple-traverse")', "{}"), - }, { module: getPackageFile("debug/src/browser.js"), path: path.join(dirname, "./shims/debug.js"), @@ -260,15 +261,12 @@ const pluginFiles = [ }, { module: getPackageFile( - "@typescript-eslint/typescript-estree/dist/convert-comments.js", + "@typescript-eslint/types/dist/generated/ast-spec.js", ), - process(text) { - text = text.replace( - 'const tsutils = __importStar(require("ts-api-utils"));', - 'import * as tsutils from "ts-api-utils";', - ); - return text; - }, + text: outdent` + const TYPE_STORE = new Proxy({}, {get: (_, type) => type}); + export { TYPE_STORE as AST_TOKEN_TYPES, TYPE_STORE as AST_NODE_TYPES}; + `, }, // Use named import from `typescript` { diff --git a/scripts/build/esbuild-plugins/replace-module.js b/scripts/build/esbuild-plugins/replace-module.js index fa6da5997..69b6326a8 100644 --- a/scripts/build/esbuild-plugins/replace-module.js +++ b/scripts/build/esbuild-plugins/replace-module.js @@ -9,7 +9,7 @@ const DEFAULT_ON_LOAD_CONCEPTS = { function processReplacements(replacements) { const onResolveReplacements = new Map(); const onLoadReplacements = new Map(); - const onLoadProcessors = new Map(); + const onLoadProcessors = []; const checkPathReplaced = (module) => { if (!onResolveReplacements.has(module)) { @@ -75,34 +75,24 @@ function processReplacements(replacements) { checkPathReplaced(module); checkTextReplaced(module); - if (!onLoadProcessors.has(module)) { - onLoadProcessors.set(module, []); - } - - const processFunctions = onLoadProcessors.get(module); + let { process } = replacement; if (Object.hasOwn(replacement, "process")) { - const { process } = replacement; if (typeof process !== "function") { throw new TypeError("'process' option should be a function."); } - - processFunctions.push(process); - continue; - } - - if ( + } else if ( Object.hasOwn(replacement, "find") && Object.hasOwn(replacement, "replacement") ) { - processFunctions.push((text) => - text.replaceAll(replacement.find, replacement.replacement), - ); - continue; + process = (text) => + text.replaceAll(replacement.find, replacement.replacement); + } else { + console.log(replacement); + throw new Error("Unexpected replacement option."); } - console.log(replacement); - throw new Error("Unexpected replacement option."); + onLoadProcessors.push({ module, process }); } return { onResolveReplacements, onLoadReplacements, onLoadProcessors }; @@ -148,23 +138,18 @@ function setupOnResolveListener(build, { concepts, replacements }) { } function setupOnLoadListener(build, { concepts, replacements, processors }) { - if (replacements.size === 0 && processors.size === 0) { + if (replacements.size === 0 && processors.length === 0) { return; } - const processFunctionsForAllModules = processors.has("*") - ? processors.get("*") - : []; - build.onLoad(concepts, async ({ path: file }) => { if (replacements.has(file)) { return replacements.get(file); } - const processFunctions = [ - ...processFunctionsForAllModules, - ...(processors.has(file) ? processors.get(file) : []), - ]; + const processFunctions = processors + .filter(({ module }) => module === "*" || module === file) + .map((processor) => processor.process); if (processFunctions.length === 0) { return; diff --git a/scripts/build/esmify-typescript-eslint.js b/scripts/build/esmify-typescript-eslint.js new file mode 100644 index 000000000..7d65e0480 --- /dev/null +++ b/scripts/build/esmify-typescript-eslint.js @@ -0,0 +1,146 @@ +/* Transform `@typescript-eslint/*` module to ESM */ + +import * as path from "node:path"; + +import { outdent } from "outdent"; + +import { PROJECT_ROOT, writeFile } from "../utils/index.js"; + +function esmifyTypescriptEslint(text) { + /* + ```js + const foo = __importStar(require("foo")); + const foo = require("foo"); + ``` + -> + ```js + import * as foo from "foo"; + ```` + */ + text = text.replaceAll( + // TODO: Use duplicate capture group name when eslint supports + /(?<=\n)(?:const|let|var) (?\w+) = (?:__importStar\(require\(["'](?.*?)["']\)\)|require\(["'](?.*?)["']\));/g, + (...args) => { + const groups = args.at(-1); + return `import * as ${groups.variable} from "${groups.moduleName1 || groups.moduleName2}";`; + }, + ); + + /* + ```js + const foo = __importDefault(require("foo")); + ``` + -> + ```js + import foo_default_export from "foo"; + const foo = {default: foo_default_export}; + ```` + */ + text = text.replaceAll( + /(?<=\n)(?:const|let|var) (?\w+) = __importDefault\(require\(["'](?.*?)["']\)\);/g, + outdent` + import $_default_export from "$"; + const $ = {default: $_default_export}; + `, + ); + + text = text.replaceAll('"use strict";', ""); + text = text.replaceAll( + 'Object.defineProperty(exports, "__esModule", { value: true });', + "", + ); + text = text.replaceAll(/(?<=\n)(?:exports\.\w+ = )+void 0;/g, ""); + text = text.replaceAll( + /(?<=\n)exports\.(?\w+) = (?\w+);/g, + (...args) => { + const { variable, specifier } = args.at(-1); + if (specifier === variable) { + return `export {${specifier}};`; + } + + return `export const ${specifier} = ${variable};`; + }, + ); + + text = text.replaceAll( + /(?<=\n)exports\.(?\w+)(?= = `)/g, + "export const $", + ); + + /* + ```js + __exportStar(require("foo"), exports); + ``` + -> + ```js + export * from "foo"; + ```` + */ + text = text.replaceAll( + /(?<=\n)__exportStar\(require\(["'](?.*?)["']\), exports\);/g, + 'export * from "$";', + ); + + /* + ```js + Object.defineProperty(exports, "foo", { enumerable: true, get: function () { return foo; } }); + ``` + -> + ```js + export * from "foo"; + ```` + */ + text = text.replaceAll( + /(?<=\n)Object\.defineProperty\(exports, "(?\w+)", { enumerable: true, get: function \(\) { return (?.*?); } }\);/g, + "export const $ = $;", + ); + + /* + ```js + exports.foo = __importStar(require("foo")); + ``` + -> + ```js + import * as foo_namespace_export from "foo"; + export {foo_namespace_export as foo}; + ```` + */ + text = text.replaceAll( + /(?<=\n)exports\.(?\w+) = __importStar\(require\(["'](?.*?)["']\)\);/g, + outdent` + import * as $_namespace_export from "$"; + export {$_namespace_export as $}; + `, + ); + + // Make sure ESBuild treat it as Module + text += "\nexport {};"; + + return text; +} + +const NODE_MODULES_DIRECTORY = path.join( + PROJECT_ROOT, + "node_modules/@typescript-eslint/", +); +// Save modified code to `{PROJECT_ROOT}/.tmp/@typescript-eslint/` for debug +const saveToDisk = (process) => (text, file) => { + if (!file.startsWith(NODE_MODULES_DIRECTORY)) { + return text; + } + + const code = process(text); + + writeFile( + path.join( + PROJECT_ROOT, + ".tmp/@typescript-eslint-esm/", + path.relative(NODE_MODULES_DIRECTORY, file), + ), + code, + ); + + return code; +}; + +export default saveToDisk(esmifyTypescriptEslint); diff --git a/scripts/build/modify-typescript-module.js b/scripts/build/modify-typescript-module.js index f2fe2a6a0..278a5d29f 100644 --- a/scripts/build/modify-typescript-module.js +++ b/scripts/build/modify-typescript-module.js @@ -149,17 +149,23 @@ function getExports(entry) { lines = lines.slice(2, -2); - const exports = lines.map((line) => { - const match = line.match( - /^\s*(?.*?): \(\) => (?.*?),?$/, - ); + const exports = lines + .map((line) => { + const match = line.match( + /^\s*(?.*?): \(\) => (?.*?),?$/, + ); - if (!match) { - throw new Error("Unexpected source"); - } + if (!match) { + throw new Error("Unexpected source"); + } - return match.groups; - }); + if (UNUSED_SPECIFIERS.has(match.groups.specifier)) { + return; + } + + return match.groups; + }) + .filter(Boolean); return exports; } @@ -170,7 +176,7 @@ function modifyTypescriptModule(text) { const source = new TypeScriptModuleSource(text); const entry = source.modules.find((module) => module.isEntry); - let exports = getExports(entry); + const exports = getExports(entry); source.removeModule(entry); // Deprecated @@ -317,38 +323,45 @@ function modifyTypescriptModule(text) { }); /* spell-checker: enable */ - let code = source.toString(); - exports = exports.filter( - ({ specifier }) => !UNUSED_SPECIFIERS.has(specifier), - ); + source.append(createExports(exports)); - code = addExports(code, exports); - - code += - "\n" + + // Used in `ts-api-utils` + source.append( outdent` export const isUnparsedPrepend = () => false; export const isUnparsedTextLike = () => false; - `; + `, + ); - return { code, exports }; + // Used in `@typescript-eslint/typescript-estree`, but we manually removed them + // source.append( + // outdent` + // export const createProgram = () => {}; + // export const flattenDiagnosticMessageText = () => {}; + // export const FileWatcherEventKind = () => {}; + // export const createWatchCompilerHost = () => {}; + // export const createAbstractBuilder = () => {}; + // export const createWatchProgram = () => {}; + // export const getParsedCommandLineOfConfigFile = () => {}; + // export const createCompilerHost = () => {}; + // export const formatDiagnostics = () => {}; + // `, + // ); + + return { code: source.toString(), exports }; } -function addExports(code, exports) { - return ( - code + - "\n\n" + - outdent` - export { - ${exports - .map(({ specifier, variable }) => - variable === specifier ? specifier : `${variable} as ${specifier}`, - ) - .map((line) => ` ${line},`) - .join("\n")} - }; - ` - ); +function createExports(exports) { + return outdent` + export { + ${exports + .map(({ specifier, variable }) => + variable === specifier ? specifier : `${variable} as ${specifier}`, + ) + .map((line) => ` ${line},`) + .join("\n")} + }; + `; } // Save modified code to `{PROJECT_ROOT}/.tmp/modified-typescript.js` for debug diff --git a/src/language-js/parse/typescript.js b/src/language-js/parse/typescript.js index 7150b0414..3ecbb3268 100644 --- a/src/language-js/parse/typescript.js +++ b/src/language-js/parse/typescript.js @@ -24,14 +24,15 @@ const baseParseOptions = { }; function createParseError(error) { - const { message, location } = error; - - /* c8 ignore next 3 */ - if (!location) { + /* c8 ignore next 3 -- not a parse error */ + if (!error?.location) { return error; } - const { start, end } = location; + const { + message, + location: { start, end }, + } = error; return createError(message, { loc: {