From 5cc3c26c46f939110cd2a477ac14aa6dad5909ce Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Mon, 15 Dec 2025 01:39:44 +0800 Subject: [PATCH] Remove static import of plugins from bundled Prettier entry (#18483) --- package.json | 2 +- scripts/generate-builtin-plugins.js | 232 +++++++++--------- ...-config.js => production-eslint-config.js} | 20 +- .../builtin-plugins/production-plugins.js | 49 ++-- src/plugins/estree.js | 4 +- 5 files changed, 143 insertions(+), 164 deletions(-) rename scripts/{bundle-eslint-config.js => production-eslint-config.js} (89%) diff --git a/package.json b/package.json index 8d573b7a3..e25963163 100644 --- a/package.json +++ b/package.json @@ -189,7 +189,7 @@ "test:production-node14": "cross-env NODE_ENV=production yarn test:node14", "test:production-standalone": "cross-env TEST_STANDALONE=1 yarn test:production", "test:production-standalone-node14": "cross-env TEST_STANDALONE=1 yarn test:production-node14", - "test:production-lint": "eslint dist/prettier --config=./scripts/bundle-eslint-config.js --quiet --format friendly", + "test:production-lint": "eslint dist --config=./scripts/production-eslint-config.js --quiet --format friendly", "perf": "yarn && yarn build && cross-env NODE_ENV=production node ./dist/prettier/bin/prettier.cjs", "perf:inspect": "yarn && yarn build && cross-env NODE_ENV=production node --inspect-brk ./dist/prettier/bin/prettier.cjs", "perf:benchmark": "yarn perf --debug-benchmark", diff --git a/scripts/generate-builtin-plugins.js b/scripts/generate-builtin-plugins.js index dba9398e5..993402aeb 100644 --- a/scripts/generate-builtin-plugins.js +++ b/scripts/generate-builtin-plugins.js @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import fs from "node:fs/promises"; import path from "node:path"; import { isValidIdentifier } from "@babel/types"; +import camelcase from "camelcase"; import { outdent } from "outdent"; import { format } from "prettier"; import { toPath } from "url-or-path"; @@ -39,22 +40,20 @@ function getImportSource(from, to) { return relation.replaceAll("\\", "/"); } -async function getPluginData(file, pluginFile, getPluginName, kind) { +async function getPluginData(file, pluginFile, getPluginName) { const pluginName = getPluginName(pluginFile); assert.ok( isValidIdentifier(pluginName), `Invalid plugin name '${pluginName}'.`, ); - pluginFile = new URL(pluginFile, sourceDirectory); + const url = new URL(pluginFile, sourceDirectory); - const implementation = await import(pluginFile); - const importSource = getImportSource(file, pluginFile); + const implementation = await import(url); const pluginData = { name: pluginName, - importSource, - file: pluginFile, + url, parserNames: Object.hasOwn(implementation, "parsers") ? Object.keys(implementation.parsers) : undefined, @@ -71,101 +70,48 @@ async function getPluginData(file, pluginFile, getPluginName, kind) { const value = implementation[property]; pluginData[property] = { value, - variableName: `${pluginName}${property[0].toUpperCase() + property.slice(1)}`, + // If we import `languages` and `options` from the plugin entry + // It will be much slower to load the Prettier entry + entries: await locateLanguageOrOptions(pluginData, value, property), }; - - const entry = - kind === "development" - ? // If we import `languages` and `options` from the `index.js` - // It will be much slower to load builtin plugins - await locateLanguageOrOptions(pluginData, property) - : { - url: pluginFile, - specifier: property, - }; - - pluginData[property].entry = entry; } return pluginData; } -function getImportStatements(file, plugin) { - const statements = new Map(); +function getImportStatements(plugins, file) { + const entries = plugins.flatMap((plugin) => + ["languages", "options"].flatMap( + (property) => plugin[property]?.entries ?? [], + ), + ); + return sortByUrl(entries) + .map( + (entry) => outdent` + import ${entry.specifier === "default" ? "" : "* as "}${entry.variableName} from "${getImportSource(file, entry.url)}"; + `, + ) + .join("\n"); +} - for (const property of ["languages", "options"]) { +function getPluginExportStatement(plugin, file) { + const properties = { + name: JSON.stringify(plugin.name), + importPlugin: `() => import("${getImportSource(file, plugin.url)}")`, + }; + + for (const property of ["options", "languages"]) { if (!plugin[property]) { continue; } - - const { entry, variableName } = plugin[property]; - const { url } = entry; - const specifier = { - imported: entry.specifier, - local: variableName, - }; - - if (!statements.has(url)) { - statements.set(url, []); - } - - statements.get(url).push(specifier); - } - - if (statements.size === 0) { - return; - } - - const statementsText = []; - for (const [url, specifiers] of statements) { - let defaultSpecifier = ""; - const namedSpecifiers = []; - for (const [index, { local, imported }] of specifiers.entries()) { - if (imported === "default") { - assert.ok( - index === 0, - `Unexpected specifier '${imported}' at index '${index}'.`, - ); - } - if (imported === "default") { - defaultSpecifier = local; - } else { - namedSpecifiers.push(`${imported} as ${local}`); - } - } - let statementText = "import"; - if (defaultSpecifier) { - statementText += ` ${defaultSpecifier}`; - } - - if (namedSpecifiers.length > 0) { - if (defaultSpecifier) { - statementText += ","; - } - - statementText += ` {${namedSpecifiers.join(",")}}`; - } - - const source = getImportSource(file, url); - statementText += ` from "${source}";`; - statementsText.push(statementText); - } - - return statementsText.join("\n"); -} - -function getPluginExportStatement(plugin) { - const properties = { - name: JSON.stringify(plugin.name), - importPlugin: `() => import("${plugin.importSource}")`, - }; - - if (plugin.options) { - properties.options = plugin.options.variableName; - } - - if (plugin.languages) { - properties.languages = plugin.languages.variableName; + const variableNames = plugin[property].entries.map( + ({ variableName }) => variableName, + ); + const isArray = property === "languages"; + properties[property] = + variableNames.length === 1 + ? variableNames + : `${isArray ? "[" : "{"}${variableNames.map((variableName) => `...${variableName},`).join("")}${isArray ? "]" : "}"}`; } if (plugin.parserNames) { @@ -202,23 +148,19 @@ async function buildPlugins({ kind, file, pattern, getPluginName }) { plugins.push(estreePlugin); } - plugins.sort((pluginA, pluginB) => - pluginA.importSource.localeCompare(pluginB.importSource), - ); - const code = outdent` /* Generated file, do NOT edit Run \`${buildScript}\` to regenerate */ - ${plugins - .map((plugin) => getImportStatements(file, plugin)) - .filter(Boolean) - .join("\n")} + ${getImportStatements(plugins, file)} import {toLazyLoadPlugin} from "./utilities.js"; - ${plugins.map((plugin) => getPluginExportStatement(plugin, kind)).join("\n")} + + ${sortByUrl(plugins) + .map((plugin) => getPluginExportStatement(plugin, file)) + .join("\n")} `; const formatted = await format(code, { parser: "meriyah" }); @@ -226,33 +168,79 @@ async function buildPlugins({ kind, file, pattern, getPluginName }) { await fs.writeFile(file, formatted); } -async function locateLanguageOrOptions(plugin, property) { - const directory = new URL("./", plugin.file); - const possibleEntries = await Array.fromAsync( - fs.glob( - property === "languages" - ? ["languages.js", "languages.evaluate.js"] - : ["options.js"], - { cwd: directory }, - ), +async function collectLanguagesAndOptions() { + const data = await Array.fromAsync( + fs.glob(["language-*/{languages,languages.evaluate,options}.js"], { + cwd: sourceDirectory, + }), + async (file) => { + const url = new URL(file, sourceDirectory); + let implementation = await import(url); + + let specifier = "*"; + if (Object.hasOwn(implementation, "default")) { + specifier = "default"; + implementation = implementation.default; + } + + const kind = Array.isArray(implementation) ? "languages" : "options"; + const languageName = path.dirname(file).slice("language-".length); + + return { + kind, + url, + specifier, + implementation, + variableName: camelcase(`${languageName}-${kind}`), + }; + }, ); - assert.equal( - possibleEntries.length, - 1, - `Can not locate '${property}' entry for plugin '${plugin.name}'.`, + + return data; +} + +let languagesAndOptions; +async function locateLanguageOrOptions(pluginData, value, kind) { + languagesAndOptions ??= await collectLanguagesAndOptions(); + const directImport = languagesAndOptions.find( + (data) => data.kind === kind && data.implementation === value, ); - const [file] = possibleEntries; - const url = new URL(file, directory); - const implementation = await import(url); - if (implementation.default === plugin[property].value) { - return { - url, - specifier: "default", - }; + if (directImport) { + return [directImport]; + } + + if (pluginData.name === "estree" && kind === "languages") { + const entries = languagesAndOptions.filter( + (data) => + data.kind === kind && + (data.variableName === camelcase(`js-${kind}`) || + data.variableName === camelcase(`json-${kind}`)), + ); + + const languages = entries + .flatMap((entry) => entry.implementation) + .toSorted((languageA, languageB) => + languageA.name.localeCompare(languageB.name), + ); + assert.equal(languages.length, value.length); + const sorted = value.toSorted((languageA, languageB) => + languageA.name.localeCompare(languageB.name), + ); + for (const [index, language] of languages.entries()) { + assert.equal(language, sorted[index]); + } + + return entries; } throw new Error( - `Can not locate '${property}' entry for plugin '${plugin.name}'.`, + `Can not locate '${kind}' entry for plugin '${pluginData.name}'.`, + ); +} + +function sortByUrl(array) { + return array.toSorted((elementA, elementB) => + elementA.url.href.localeCompare(elementB.url.href), ); } diff --git a/scripts/bundle-eslint-config.js b/scripts/production-eslint-config.js similarity index 89% rename from scripts/bundle-eslint-config.js rename to scripts/production-eslint-config.js index 83c25cba1..f74fbfa0e 100644 --- a/scripts/bundle-eslint-config.js +++ b/scripts/production-eslint-config.js @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import eslintPluginCompat from "eslint-plugin-compat"; import packageBuildConfigs from "./build/packages/index.js"; -import { DIST_DIR } from "./utilities/index.js"; +import { PROJECT_ROOT } from "./utilities/index.js"; const { browserslist: targets } = JSON.parse( await fs.readFile(new URL("../package.json", import.meta.url)), @@ -17,17 +17,17 @@ const jsFiles = packageBuildConfigs .map((file) => path.join(packageConfig.distDirectory, file.output)), ), ) - .map((file) => path.relative(DIST_DIR, file).replaceAll("\\", "/")); + .map((file) => path.relative(PROJECT_ROOT, file).replaceAll("\\", "/")); const nodejsFiles = jsFiles.filter( (file) => file.endsWith(".cjs") || [ - "prettier/index.mjs", - "prettier/internal/legacy-cli.mjs", - "prettier/internal/experimental-cli.mjs", - "prettier/internal/experimental-cli-worker.mjs", - "plugin-oxc/index.mjs", + "dist/prettier/index.mjs", + "dist/prettier/internal/legacy-cli.mjs", + "dist/prettier/internal/experimental-cli.mjs", + "dist/prettier/internal/experimental-cli-worker.mjs", + "dist/plugin-oxc/index.mjs", ].includes(file), ); @@ -98,10 +98,9 @@ const nodejsRestrictedSyntaxes = [ }, ]; -/* TODO[@fisker]: Fix `no-restricted-syntax` */ - export default [ { + name: "All files", plugins: { compat: eslintPluginCompat, }, @@ -125,6 +124,7 @@ export default [ }, { + name: "Browser files", files: browserFiles, rules: { "no-restricted-syntax": [ @@ -136,6 +136,7 @@ export default [ }, { + name: "Node.js files", files: nodejsFiles, rules: { "no-restricted-syntax": [ @@ -147,6 +148,7 @@ export default [ }, { + name: "Prettier bin file", files: ["bin/prettier.cjs"], languageOptions: { ecmaVersion: 5, diff --git a/src/main/plugins/builtin-plugins/production-plugins.js b/src/main/plugins/builtin-plugins/production-plugins.js index 9f50ee9df..f4dfc73a1 100644 --- a/src/main/plugins/builtin-plugins/production-plugins.js +++ b/src/main/plugins/builtin-plugins/production-plugins.js @@ -3,31 +3,20 @@ Generated file, do NOT edit Run `node scripts/generate-builtin-plugins.js` to regenerate */ -import { - languages as estreeLanguages, - options as estreeOptions, -} from "../../../plugins/estree.js"; -import { languages as glimmerLanguages } from "../../../plugins/glimmer.js"; -import { - languages as graphqlLanguages, - options as graphqlOptions, -} from "../../../plugins/graphql.js"; -import { - languages as htmlLanguages, - options as htmlOptions, -} from "../../../plugins/html.js"; -import { - languages as markdownLanguages, - options as markdownOptions, -} from "../../../plugins/markdown.js"; -import { - languages as postcssLanguages, - options as postcssOptions, -} from "../../../plugins/postcss.js"; -import { - languages as yamlLanguages, - options as yamlOptions, -} from "../../../plugins/yaml.js"; +import cssLanguages from "../../../language-css/languages.evaluate.js"; +import cssOptions from "../../../language-css/options.js"; +import graphqlLanguages from "../../../language-graphql/languages.evaluate.js"; +import graphqlOptions from "../../../language-graphql/options.js"; +import handlebarsLanguages from "../../../language-handlebars/languages.evaluate.js"; +import htmlLanguages from "../../../language-html/languages.evaluate.js"; +import htmlOptions from "../../../language-html/options.js"; +import jsLanguages from "../../../language-js/languages.evaluate.js"; +import jsOptions from "../../../language-js/options.js"; +import jsonLanguages from "../../../language-json/languages.evaluate.js"; +import markdownLanguages from "../../../language-markdown/languages.evaluate.js"; +import markdownOptions from "../../../language-markdown/options.js"; +import yamlLanguages from "../../../language-yaml/languages.evaluate.js"; +import yamlOptions from "../../../language-yaml/options.js"; import { toLazyLoadPlugin } from "./utilities.js"; export const acorn = /* @__PURE__ */ toLazyLoadPlugin({ @@ -68,8 +57,8 @@ export const babel = /* @__PURE__ */ toLazyLoadPlugin({ export const estree = /* @__PURE__ */ toLazyLoadPlugin({ name: "estree", importPlugin: () => import("../../../plugins/estree.js"), - options: estreeOptions, - languages: estreeLanguages, + options: jsOptions, + languages: [...jsonLanguages, ...jsLanguages], printerNames: ["estree", "estree-json"], }); export const flow = /* @__PURE__ */ toLazyLoadPlugin({ @@ -80,7 +69,7 @@ export const flow = /* @__PURE__ */ toLazyLoadPlugin({ export const glimmer = /* @__PURE__ */ toLazyLoadPlugin({ name: "glimmer", importPlugin: () => import("../../../plugins/glimmer.js"), - languages: glimmerLanguages, + languages: handlebarsLanguages, parserNames: ["glimmer"], printerNames: ["glimmer"], }); @@ -116,8 +105,8 @@ export const meriyah = /* @__PURE__ */ toLazyLoadPlugin({ export const postcss = /* @__PURE__ */ toLazyLoadPlugin({ name: "postcss", importPlugin: () => import("../../../plugins/postcss.js"), - options: postcssOptions, - languages: postcssLanguages, + options: cssOptions, + languages: cssLanguages, parserNames: ["css", "less", "scss"], printerNames: ["postcss"], }); diff --git a/src/plugins/estree.js b/src/plugins/estree.js index a738f2ead..1887ddf90 100644 --- a/src/plugins/estree.js +++ b/src/plugins/estree.js @@ -5,7 +5,6 @@ // printers as jsPrinters, // } from "../language-js/index.js"; import jsLanguages from "../language-js/languages.evaluate.js"; -import jsOptions from "../language-js/options.js"; import * as jsPrinters from "../language-js/printers.js"; import { languages as jsonLanguages, @@ -17,4 +16,5 @@ export const printers = { ...jsonPrinters, }; export const languages = [...jsLanguages, ...jsonLanguages]; -export const options = { ...jsOptions }; + +export { default as options } from "../language-js/options.js";