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

+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,