Remove static import of plugins from bundled Prettier entry (#18483)

This commit is contained in:
fisker Cheung authored and GitHub committed 2025-12-15 01:39:44 +08:00
1 parent 01363e7310
commit 5cc3c26c46
5 files changed
+143 -164

No files matched your search

+1 -1
View File
@@ -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",
+110 -122
View File
@@ -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),
);
}
@@ -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,
@@ -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"],
});
+2 -2
View File
@@ -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";