Files
test-repo/scripts/generate-builtin-plugins.js

256 lines
7.0 KiB
JavaScript

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";
const projectRoot = new URL("../", import.meta.url);
const sourceDirectory = new URL("./src/", projectRoot);
const buildScript = `node ${import.meta.url.slice(projectRoot.href.length)}`;
const pluginFiles = [
{
kind: "development",
pattern: "language-*/index.js",
file: new URL(
"main/plugins/builtin-plugins/development-plugins.js",
sourceDirectory,
),
getPluginName: (file) => path.dirname(file).slice("language-".length),
},
{
kind: "production",
pattern: "plugins/*.js",
file: new URL(
"main/plugins/builtin-plugins/production-plugins.js",
sourceDirectory,
),
getPluginName: (file) => path.basename(file, path.extname(file)),
},
];
function getImportSource(from, to) {
from = toPath(from);
to = toPath(to);
const relation = path.relative(path.dirname(from), to);
return relation.replaceAll("\\", "/");
}
async function getPluginData(file, pluginFile, getPluginName) {
const pluginName = getPluginName(pluginFile);
assert.ok(
isValidIdentifier(pluginName),
`Invalid plugin name '${pluginName}'.`,
);
const url = new URL(pluginFile, sourceDirectory);
const implementation = await import(url);
const pluginData = {
name: pluginName,
url,
parsers: Object.hasOwn(implementation, "parsers")
? Object.keys(implementation.parsers)
: undefined,
printers: Object.hasOwn(implementation, "printers")
? Object.keys(implementation.printers)
: undefined,
};
for (const property of ["languages", "options"]) {
if (!Object.hasOwn(implementation, property)) {
continue;
}
const value = implementation[property];
pluginData[property] = {
value,
// 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),
};
}
return pluginData;
}
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");
}
function getExportStatements(plugins, file) {
const codes = sortByUrl(plugins).map((plugin) => {
const properties = {
name: JSON.stringify(plugin.name),
load: `() => import("${getImportSource(file, plugin.url)}")`,
};
for (const property of ["options", "languages"]) {
if (!plugin[property]) {
continue;
}
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.parsers) {
properties.parsers = JSON.stringify(plugin.parsers);
}
if (plugin.printers) {
properties.printers = JSON.stringify(plugin.printers);
}
return outdent`
{
${Object.entries(properties)
.map(([property, code]) => `${property}: ${code},`)
.join("\n")}
},
`;
});
return outdent`
export const plugins = /* @__PURE__ */ toLazyLoadPlugins(
${codes.join("\n")}
);
`;
}
async function buildPlugins({ kind, file, pattern, getPluginName }) {
const plugins = await Array.fromAsync(
fs.glob(pattern, { cwd: sourceDirectory }),
(pluginFile) => getPluginData(file, pluginFile, getPluginName, kind),
);
// Split js plugin for https://github.com/prettier/prettier/pull/18481
if (kind === "development") {
const jsPlugin = plugins.find(({ name }) => name === "js");
const estreePlugin = { ...jsPlugin, name: "estree" };
delete estreePlugin.languages;
delete estreePlugin.options;
delete estreePlugin.parsers;
delete jsPlugin.printers;
plugins.push(estreePlugin);
}
const code = outdent`
/*
Generated file, do NOT edit
Run \`${buildScript}\` to regenerate
*/
${getImportStatements(plugins, file)}
import {toLazyLoadPlugins} from "./utilities.js";
${getExportStatements(plugins, file)}
`;
const formatted = await format(code, {
parser: "meriyah",
objectWrap: "collapse",
});
await fs.writeFile(file, formatted);
}
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}`),
};
},
);
return data;
}
let languagesAndOptions;
async function locateLanguageOrOptions(pluginData, value, kind) {
languagesAndOptions ??= await collectLanguagesAndOptions();
const directImport = languagesAndOptions.find(
(data) => data.kind === kind && data.implementation === value,
);
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 '${kind}' entry for plugin '${pluginData.name}'.`,
);
}
function sortByUrl(array) {
return array.toSorted((elementA, elementB) =>
elementA.url.href.localeCompare(elementB.url.href),
);
}
await Promise.all(pluginFiles.map((plugins) => buildPlugins(plugins)));