diff --git a/jest.config.js b/jest.config.js index b0b1c5b23..03d125ac4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -40,8 +40,8 @@ if (isProduction) { ); } -if (nodejsMajorVersion <= 16) { - // Uses import attributes +if (nodejsMajorVersion <= 18) { + // Uses import attributes and `Array#toSorted()` testPathIgnorePatterns.push( "/tests/integration/__tests__/help-options.js", ); diff --git a/package.json b/package.json index c32d14b24..8d573b7a3 100644 --- a/package.json +++ b/package.json @@ -206,13 +206,15 @@ "fix": "run-s --continue-on-error fix:eslint fix:prettier", "fix:eslint": "yarn lint:eslint --fix", "fix:prettier": "yarn lint:prettier --write", + "fix:builtin-plugins": "yarn generate-builtin-plugins", "build": "node ./scripts/build/build.js", "build:website": "node ./scripts/build-website.js", "build:website:pr": "cross-env PULL_REQUEST=true yarn build:website", "generate-changelog": "node ./scripts/generate-changelog.js", - "debug": "node bin/prettier.js --ignore-path=.prettierignore --plugin=./packages/plugin-oxc/index.js --plugin=./packages/plugin-hermes/index.js", - "debug:watch": "node --watch bin/prettier.js --ignore-path=.prettierignore --plugin=./packages/plugin-oxc/index.js --plugin=./packages/plugin-hermes/index.js", - "debug:inspect": "node --inspect-brk bin/prettier.js --ignore-path=.prettierignore --plugin=./packages/plugin-oxc/index.js --plugin=./packages/plugin-hermes/index.js" + "generate-builtin-plugins": "node scripts/generate-builtin-plugins.js", + "debug": "node bin/prettier.js --ignore-path=.prettierignore", + "debug:watch": "node --watch bin/prettier.js --ignore-path=.prettierignore", + "debug:inspect": "node --inspect-brk bin/prettier.js --ignore-path=.prettierignore" }, "c8": { "reporter": [ diff --git a/packages/plugin-hermes/index.js b/packages/plugin-hermes/index.js index 4da52b513..17f7e3bcb 100644 --- a/packages/plugin-hermes/index.js +++ b/packages/plugin-hermes/index.js @@ -1,5 +1,5 @@ import * as hermesParsers from "../../src/language-js/parse/hermes.js"; -import * as estreePrinter from "../../src/language-js/printer.js"; +import { estree as estreePrinter } from "../../src/language-js/printers.js"; const AST_FORMAT = "estree-hermes"; diff --git a/packages/plugin-oxc/index.js b/packages/plugin-oxc/index.js index 732f89bf2..b994e8bc7 100644 --- a/packages/plugin-oxc/index.js +++ b/packages/plugin-oxc/index.js @@ -1,5 +1,5 @@ import * as oxcParsers from "../../src/language-js/parse/oxc.js"; -import * as estreePrinter from "../../src/language-js/printer.js"; +import { estree as estreePrinter } from "../../src/language-js/printers.js"; const AST_FORMAT = "estree-oxc"; diff --git a/scripts/generate-builtin-plugins.js b/scripts/generate-builtin-plugins.js new file mode 100644 index 000000000..dba9398e5 --- /dev/null +++ b/scripts/generate-builtin-plugins.js @@ -0,0 +1,259 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { isValidIdentifier } from "@babel/types"; +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, kind) { + const pluginName = getPluginName(pluginFile); + assert.ok( + isValidIdentifier(pluginName), + `Invalid plugin name '${pluginName}'.`, + ); + + pluginFile = new URL(pluginFile, sourceDirectory); + + const implementation = await import(pluginFile); + const importSource = getImportSource(file, pluginFile); + + const pluginData = { + name: pluginName, + importSource, + file: pluginFile, + parserNames: Object.hasOwn(implementation, "parsers") + ? Object.keys(implementation.parsers) + : undefined, + printerNames: 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, + variableName: `${pluginName}${property[0].toUpperCase() + property.slice(1)}`, + }; + + 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(); + + for (const property of ["languages", "options"]) { + 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; + } + + if (plugin.parserNames) { + properties.parserNames = JSON.stringify(plugin.parserNames); + } + + if (plugin.printerNames) { + properties.printerNames = JSON.stringify(plugin.printerNames); + } + + return outdent` + export const ${plugin.name} = /* @__PURE__ */ toLazyLoadPlugin({ + ${Object.entries(properties) + .map(([property, code]) => `${property}: ${code},`) + .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.parserNames; + delete jsPlugin.printerNames; + 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")} + import {toLazyLoadPlugin} from "./utilities.js"; + + ${plugins.map((plugin) => getPluginExportStatement(plugin, kind)).join("\n")} + `; + + const formatted = await format(code, { parser: "meriyah" }); + + 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 }, + ), + ); + assert.equal( + possibleEntries.length, + 1, + `Can not locate '${property}' entry for plugin '${plugin.name}'.`, + ); + const [file] = possibleEntries; + const url = new URL(file, directory); + const implementation = await import(url); + if (implementation.default === plugin[property].value) { + return { + url, + specifier: "default", + }; + } + + throw new Error( + `Can not locate '${property}' entry for plugin '${plugin.name}'.`, + ); +} + +await Promise.all(pluginFiles.map((plugins) => buildPlugins(plugins))); diff --git a/src/language-js/index.js b/src/language-js/index.js new file mode 100644 index 000000000..ff5bfd75e --- /dev/null +++ b/src/language-js/index.js @@ -0,0 +1,4 @@ +export { default as languages } from "./languages.evaluate.js"; +export { default as options } from "./options.js"; +export * as parsers from "./parse/index.js"; +export * as printers from "./printers.js"; diff --git a/src/language-js/parse/acorn.js b/src/language-js/parse/acorn.js index 539b90233..11efa0406 100644 --- a/src/language-js/parse/acorn.js +++ b/src/language-js/parse/acorn.js @@ -99,4 +99,4 @@ function parse(text, options) { return postprocess(ast, { text }); } -export const acorn = createParser(parse); +export const acorn = /* @__PURE__ */ createParser(parse); diff --git a/src/language-js/parse/angular.js b/src/language-js/parse/angular.js index 9638864d4..abc4c9139 100644 --- a/src/language-js/parse/angular.js +++ b/src/language-js/parse/angular.js @@ -35,7 +35,11 @@ function createParser(parseMethod) { }; } -export const __ng_action = createParser(parseAction); -export const __ng_binding = createParser(parseBinding); -export const __ng_interpolation = createParser(parseInterpolationExpression); -export const __ng_directive = createParser(parseTemplateBindings); +export const __ng_action = /* @__PURE__ */ createParser(parseAction); +export const __ng_binding = /* @__PURE__ */ createParser(parseBinding); +export const __ng_interpolation = /* @__PURE__ */ createParser( + parseInterpolationExpression, +); +export const __ng_directive = /* @__PURE__ */ createParser( + parseTemplateBindings, +); diff --git a/src/language-js/parse/babel.js b/src/language-js/parse/babel.js index 23a6924b1..4fe61bceb 100644 --- a/src/language-js/parse/babel.js +++ b/src/language-js/parse/babel.js @@ -230,39 +230,41 @@ const allowedReasonCodes = new Set([ ]); const babelParserOptionsCombinations = [appendPlugins(["jsx"])]; -const babel = createBabelParser({ +const babel = /* @__PURE__ */ createBabelParser({ optionsCombinations: babelParserOptionsCombinations, }); -const babelTs = createBabelParser({ +const babelTs = /* @__PURE__ */ createBabelParser({ optionsCombinations: [ appendPlugins(["jsx", "typescript"]), appendPlugins(["typescript"]), ], }); -const babelExpression = createBabelParser({ +const babelExpression = /* @__PURE__ */ createBabelParser({ isExpression: true, optionsCombinations: [appendPlugins(["jsx"])], }); -const babelTSExpression = createBabelParser({ +const babelTSExpression = /* @__PURE__ */ createBabelParser({ isExpression: true, optionsCombinations: [appendPlugins(["typescript"])], }); -const babelFlow = createBabelParser({ +const babelFlow = /* @__PURE__ */ createBabelParser({ optionsCombinations: [ appendPlugins(["jsx", ["flow", { all: true }], "flowComments"]), ], }); -const babelEstree = createBabelParser({ +const babelEstree = /* @__PURE__ */ createBabelParser({ optionsCombinations: babelParserOptionsCombinations.map((options) => appendPlugins(["estree"], options), ), }); -export { babel, babelFlow as "babel-flow", babelTs as "babel-ts" }; - -/** @internal */ // eslint-disable-next-line simple-import-sort/exports export { + babel, + babelFlow as "babel-flow", + babelTs as "babel-ts", + + /** @internal */ babelExpression as __js_expression, babelTSExpression as __ts_expression, /** for vue filter */ diff --git a/src/language-js/parse/espree.js b/src/language-js/parse/espree.js index 79d04f028..8061ae73e 100644 --- a/src/language-js/parse/espree.js +++ b/src/language-js/parse/espree.js @@ -55,4 +55,4 @@ function parse(text, options) { return postprocess(ast, { parser: "espree", text }); } -export const espree = createParser(parse); +export const espree = /* @__PURE__ */ createParser(parse); diff --git a/src/language-js/parse/flow.js b/src/language-js/parse/flow.js index c6d0f97b9..3f0b0ac43 100644 --- a/src/language-js/parse/flow.js +++ b/src/language-js/parse/flow.js @@ -57,4 +57,4 @@ function parse(text) { return postprocess(ast, { parser: "flow", text }); } -export const flow = createParser(parse); +export const flow = /* @__PURE__ */ createParser(parse); diff --git a/src/language-js/parse/hermes.js b/src/language-js/parse/hermes.js index cc81c9047..42e13f49b 100644 --- a/src/language-js/parse/hermes.js +++ b/src/language-js/parse/hermes.js @@ -48,4 +48,4 @@ async function parse(text /* , options*/) { return postprocess(ast, { text, parser: "hermes" }); } -export const hermes = createParser(parse); +export const hermes = /* @__PURE__ */ createParser(parse); diff --git a/src/language-js/parse/index.js b/src/language-js/parse/index.js new file mode 100644 index 000000000..e2b938d1e --- /dev/null +++ b/src/language-js/parse/index.js @@ -0,0 +1,9 @@ +export * from "./acorn.js"; +export * from "./angular.js"; +export * from "./babel.js"; +export * from "./espree.js"; +export * from "./flow.js"; +export * from "./hermes.js"; +export * from "./meriyah.js"; +export * from "./oxc.js"; +export * from "./typescript.js"; diff --git a/src/language-js/parse/meriyah.js b/src/language-js/parse/meriyah.js index 75f6455a2..e2764dc43 100644 --- a/src/language-js/parse/meriyah.js +++ b/src/language-js/parse/meriyah.js @@ -102,4 +102,4 @@ function parse(text, options) { return postprocess(ast, { parser: "meriyah", text }); } -export const meriyah = createParser(parse); +export const meriyah = /* @__PURE__ */ createParser(parse); diff --git a/src/language-js/parse/oxc.js b/src/language-js/parse/oxc.js index b6093d505..ead89e169 100644 --- a/src/language-js/parse/oxc.js +++ b/src/language-js/parse/oxc.js @@ -151,7 +151,7 @@ async function parseTs(text, options) { return postprocess(ast, { text, parser: "oxc", oxcAstType: "ts" }); } -const oxc = createParser(parseJs); -const oxcTs = createParser(parseTs); +const oxc = /* @__PURE__ */ createParser(parseJs); +const oxcTs = /* @__PURE__ */ createParser(parseTs); export { oxc, oxcTs as "oxc-ts" }; diff --git a/src/language-js/parse/typescript.js b/src/language-js/parse/typescript.js index 87c1567b5..16d3ea6c8 100644 --- a/src/language-js/parse/typescript.js +++ b/src/language-js/parse/typescript.js @@ -106,4 +106,4 @@ function parse(text, options) { return postprocess(ast, { parser: "typescript", text }); } -export const typescript = createParser(parse); +export const typescript = /* @__PURE__ */ createParser(parse); diff --git a/src/language-js/print/index.js b/src/language-js/print/index.js index f2d53b9d8..adea1d26d 100644 --- a/src/language-js/print/index.js +++ b/src/language-js/print/index.js @@ -98,4 +98,4 @@ function print(path, options, print, args) { ]); } -export default print; +export { print as printEstree }; diff --git a/src/language-js/printer.js b/src/language-js/printer.js deleted file mode 100644 index 27cd50d80..000000000 --- a/src/language-js/printer.js +++ /dev/null @@ -1,19 +0,0 @@ -export const features = { - // TODO: Make this default behavior - experimental_avoidAstMutation: true, -}; -export { default as massageAstNode } from "./clean.js"; -export { default as canAttachComment } from "./comments/can-attach-comment.js"; -export { default as handleComments } from "./comments/handle-comments.js"; -export { default as isGap } from "./comments/is-gap.js"; -export { default as willPrintOwnComments } from "./comments/will-print-own-comments.js"; -export { default as embed } from "./embed/index.js"; -export { insertPragma } from "./pragma.js"; -export { printComment } from "./print/comment.js"; -export { - default as print, - default as printPrettierIgnored, -} from "./print/index.js"; -export { default as getVisitorKeys } from "./traverse/get-visitor-keys.js"; -export { default as isBlockComment } from "./utilities/is-block-comment.js"; -export { default as hasPrettierIgnore } from "./utilities/is-ignored.js"; diff --git a/src/language-js/printers.js b/src/language-js/printers.js new file mode 100644 index 000000000..b8fbc5ec1 --- /dev/null +++ b/src/language-js/printers.js @@ -0,0 +1,35 @@ +const features = { + // TODO: Make this default behavior + experimental_avoidAstMutation: true, +}; +import massageAstNode from "./clean.js"; +import canAttachComment from "./comments/can-attach-comment.js"; +import handleComments from "./comments/handle-comments.js"; +import isGap from "./comments/is-gap.js"; +import willPrintOwnComments from "./comments/will-print-own-comments.js"; +import embed from "./embed/index.js"; +import { insertPragma } from "./pragma.js"; +import { printComment } from "./print/comment.js"; +import { printEstree } from "./print/index.js"; +import getVisitorKeys from "./traverse/get-visitor-keys.js"; +import isBlockComment from "./utilities/is-block-comment.js"; +import hasPrettierIgnore from "./utilities/is-ignored.js"; + +const estree = { + features, + massageAstNode, + canAttachComment, + handleComments, + isGap, + willPrintOwnComments, + embed, + insertPragma, + printComment, + printPrettierIgnored: printEstree, + print: printEstree, + getVisitorKeys, + isBlockComment, + hasPrettierIgnore, +}; + +export { estree }; diff --git a/src/language-json/index.js b/src/language-json/index.js new file mode 100644 index 000000000..c75251feb --- /dev/null +++ b/src/language-json/index.js @@ -0,0 +1,3 @@ +export { default as languages } from "./languages.evaluate.js"; +export * as parsers from "./parsers.js"; +export * as printers from "./printers.js"; diff --git a/src/language-json/parser-json.js b/src/language-json/parse/json.js similarity index 81% rename from src/language-json/parser-json.js rename to src/language-json/parse/json.js index 9f3247b14..c8a362fdf 100644 --- a/src/language-json/parser-json.js +++ b/src/language-json/parse/json.js @@ -1,9 +1,8 @@ import { parse, parseExpression } from "@babel/parser"; -import createError from "../common/parser-create-error.js"; -import createBabelParseError from "../language-js/parse/utilities/create-babel-parse-error.js"; -import createParser from "../language-js/parse/utilities/create-parser.js"; -import wrapBabelExpression from "../language-js/parse/utilities/wrap-babel-expression.js"; -import isNonEmptyArray from "../utilities/is-non-empty-array.js"; +import createError from "../../common/parser-create-error.js"; +import createBabelParseError from "../../language-js/parse/utilities/create-babel-parse-error.js"; +import wrapBabelExpression from "../../language-js/parse/utilities/wrap-babel-expression.js"; +import isNonEmptyArray from "../../utilities/is-non-empty-array.js"; const babelParseOptions = { tokens: false, @@ -172,16 +171,4 @@ function assertJsonNode(node) { } } -const json = createParser({ - parse: (text) => parseJson(text), - hasPragma: () => true, - hasIgnorePragma: () => false, -}); -const json5 = createParser((text) => parseJson(text)); -const jsonc = createParser((text) => parseJson(text, { allowEmpty: true })); -const jsonStringify = createParser({ - parse: (text) => parseJson(text, { allowComments: false }), - astFormat: "estree-json", -}); - -export { json, json5, jsonc, jsonStringify as "json-stringify" }; +export { parseJson }; diff --git a/src/language-json/parsers.js b/src/language-json/parsers.js new file mode 100644 index 000000000..ed27323bd --- /dev/null +++ b/src/language-json/parsers.js @@ -0,0 +1,18 @@ +import createParser from "../language-js/parse/utilities/create-parser.js"; +import { parseJson } from "./parse/json.js"; + +const json = /* @__PURE__ */ createParser({ + parse: (text) => parseJson(text), + hasPragma: () => true, + hasIgnorePragma: () => false, +}); +const json5 = /* @__PURE__ */ createParser((text) => parseJson(text)); +const jsonc = /* @__PURE__ */ createParser((text) => + parseJson(text, { allowEmpty: true }), +); +const jsonStringify = /* @__PURE__ */ createParser({ + parse: (text) => parseJson(text, { allowComments: false }), + astFormat: "estree-json", +}); + +export { json, json5, jsonc, jsonStringify as "json-stringify" }; diff --git a/src/language-json/print/index.js b/src/language-json/print/index.js new file mode 100644 index 000000000..ece5bc330 --- /dev/null +++ b/src/language-json/print/index.js @@ -0,0 +1 @@ +export { printJson } from "./json.js"; diff --git a/src/language-json/printer-estree-json.js b/src/language-json/print/json.js similarity index 89% rename from src/language-json/printer-estree-json.js rename to src/language-json/print/json.js index 69cb1526a..e4c8126b0 100644 --- a/src/language-json/printer-estree-json.js +++ b/src/language-json/print/json.js @@ -1,8 +1,9 @@ -import { hardline, indent, join } from "../document/index.js"; -import printString from "../utilities/print-string.js"; -import UnexpectedNodeError from "../utilities/unexpected-node-error.js"; +import { hardline, indent, join } from "../../document/index.js"; +import printString from "../../utilities/print-string.js"; +import UnexpectedNodeError from "../../utilities/unexpected-node-error.js"; +import { getRaw } from "../utilities.js"; -function genericPrint(path, options, print) { +function printJson(path, options, print) { const { node } = path; switch (node.type) { case "JsonRoot": @@ -69,10 +70,6 @@ function genericPrint(path, options, print) { } } -function getRaw(node) { - return node.extra.raw; -} - function isObjectKey(path) { return path.key === "key" && path.parent.type === "ObjectProperty"; } @@ -153,5 +150,4 @@ function clean(original, cloned /* , parent*/) { clean.ignoredProperties = ignoredProperties; -export { clean as massageAstNode, genericPrint as print }; -export { default as getVisitorKeys } from "./get-visitor-keys.js"; +export { clean, printJson }; diff --git a/src/language-json/printers.js b/src/language-json/printers.js new file mode 100644 index 000000000..e82139fef --- /dev/null +++ b/src/language-json/printers.js @@ -0,0 +1,12 @@ +import getVisitorKeys from "./get-visitor-keys.js"; +import { printJson } from "./print/index.js"; +import { clean as massageAstNode } from "./print/json.js"; + +const estreeJsonPrinter = { + massageAstNode, + print: printJson, + getVisitorKeys, +}; + +export { estreeJsonPrinter as "estree-json" }; +export { estree } from "../language-js/printers.js"; diff --git a/src/language-json/utilities.js b/src/language-json/utilities.js new file mode 100644 index 000000000..020c8bd5a --- /dev/null +++ b/src/language-json/utilities.js @@ -0,0 +1,5 @@ +function getRaw(node) { + return node.extra.raw; +} + +export { getRaw }; diff --git a/src/main/plugins/builtin-plugins/development-plugins.js b/src/main/plugins/builtin-plugins/development-plugins.js new file mode 100644 index 000000000..c7b6f6e53 --- /dev/null +++ b/src/main/plugins/builtin-plugins/development-plugins.js @@ -0,0 +1,110 @@ +/* +Generated file, do NOT edit +Run `node scripts/generate-builtin-plugins.js` to regenerate +*/ + +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 css = /* @__PURE__ */ toLazyLoadPlugin({ + name: "css", + importPlugin: () => import("../../../language-css/index.js"), + options: cssOptions, + languages: cssLanguages, + parserNames: ["css", "less", "scss"], + printerNames: ["postcss"], +}); +export const graphql = /* @__PURE__ */ toLazyLoadPlugin({ + name: "graphql", + importPlugin: () => import("../../../language-graphql/index.js"), + options: graphqlOptions, + languages: graphqlLanguages, + parserNames: ["graphql"], + printerNames: ["graphql"], +}); +export const handlebars = /* @__PURE__ */ toLazyLoadPlugin({ + name: "handlebars", + importPlugin: () => import("../../../language-handlebars/index.js"), + languages: handlebarsLanguages, + parserNames: ["glimmer"], + printerNames: ["glimmer"], +}); +export const html = /* @__PURE__ */ toLazyLoadPlugin({ + name: "html", + importPlugin: () => import("../../../language-html/index.js"), + options: htmlOptions, + languages: htmlLanguages, + parserNames: ["angular", "html", "lwc", "mjml", "vue"], + printerNames: ["html"], +}); +export const js = /* @__PURE__ */ toLazyLoadPlugin({ + name: "js", + importPlugin: () => import("../../../language-js/index.js"), + options: jsOptions, + languages: jsLanguages, + parserNames: [ + "__babel_estree", + "__js_expression", + "__ng_action", + "__ng_binding", + "__ng_directive", + "__ng_interpolation", + "__ts_expression", + "__vue_event_binding", + "__vue_expression", + "__vue_ts_event_binding", + "__vue_ts_expression", + "acorn", + "babel", + "babel-flow", + "babel-ts", + "espree", + "flow", + "hermes", + "meriyah", + "oxc", + "oxc-ts", + "typescript", + ], +}); +export const estree = /* @__PURE__ */ toLazyLoadPlugin({ + name: "estree", + importPlugin: () => import("../../../language-js/index.js"), + printerNames: ["estree"], +}); +export const json = /* @__PURE__ */ toLazyLoadPlugin({ + name: "json", + importPlugin: () => import("../../../language-json/index.js"), + languages: jsonLanguages, + parserNames: ["json", "json-stringify", "json5", "jsonc"], + printerNames: ["estree", "estree-json"], +}); +export const markdown = /* @__PURE__ */ toLazyLoadPlugin({ + name: "markdown", + importPlugin: () => import("../../../language-markdown/index.js"), + options: markdownOptions, + languages: markdownLanguages, + parserNames: ["markdown", "mdx", "remark"], + printerNames: ["mdast"], +}); +export const yaml = /* @__PURE__ */ toLazyLoadPlugin({ + name: "yaml", + importPlugin: () => import("../../../language-yaml/index.js"), + options: yamlOptions, + languages: yamlLanguages, + parserNames: ["yaml"], + printerNames: ["yaml"], +}); diff --git a/src/main/plugins/builtin-plugins/production-plugins.js b/src/main/plugins/builtin-plugins/production-plugins.js new file mode 100644 index 000000000..9f50ee9df --- /dev/null +++ b/src/main/plugins/builtin-plugins/production-plugins.js @@ -0,0 +1,136 @@ +/* +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 { toLazyLoadPlugin } from "./utilities.js"; + +export const acorn = /* @__PURE__ */ toLazyLoadPlugin({ + name: "acorn", + importPlugin: () => import("../../../plugins/acorn.js"), + parserNames: ["acorn", "espree"], +}); +export const angular = /* @__PURE__ */ toLazyLoadPlugin({ + name: "angular", + importPlugin: () => import("../../../plugins/angular.js"), + parserNames: [ + "__ng_action", + "__ng_binding", + "__ng_directive", + "__ng_interpolation", + ], +}); +export const babel = /* @__PURE__ */ toLazyLoadPlugin({ + name: "babel", + importPlugin: () => import("../../../plugins/babel.js"), + parserNames: [ + "__babel_estree", + "__js_expression", + "__ts_expression", + "__vue_event_binding", + "__vue_expression", + "__vue_ts_event_binding", + "__vue_ts_expression", + "babel", + "babel-flow", + "babel-ts", + "json", + "json-stringify", + "json5", + "jsonc", + ], +}); +export const estree = /* @__PURE__ */ toLazyLoadPlugin({ + name: "estree", + importPlugin: () => import("../../../plugins/estree.js"), + options: estreeOptions, + languages: estreeLanguages, + printerNames: ["estree", "estree-json"], +}); +export const flow = /* @__PURE__ */ toLazyLoadPlugin({ + name: "flow", + importPlugin: () => import("../../../plugins/flow.js"), + parserNames: ["flow"], +}); +export const glimmer = /* @__PURE__ */ toLazyLoadPlugin({ + name: "glimmer", + importPlugin: () => import("../../../plugins/glimmer.js"), + languages: glimmerLanguages, + parserNames: ["glimmer"], + printerNames: ["glimmer"], +}); +export const graphql = /* @__PURE__ */ toLazyLoadPlugin({ + name: "graphql", + importPlugin: () => import("../../../plugins/graphql.js"), + options: graphqlOptions, + languages: graphqlLanguages, + parserNames: ["graphql"], + printerNames: ["graphql"], +}); +export const html = /* @__PURE__ */ toLazyLoadPlugin({ + name: "html", + importPlugin: () => import("../../../plugins/html.js"), + options: htmlOptions, + languages: htmlLanguages, + parserNames: ["angular", "html", "lwc", "mjml", "vue"], + printerNames: ["html"], +}); +export const markdown = /* @__PURE__ */ toLazyLoadPlugin({ + name: "markdown", + importPlugin: () => import("../../../plugins/markdown.js"), + options: markdownOptions, + languages: markdownLanguages, + parserNames: ["markdown", "mdx", "remark"], + printerNames: ["mdast"], +}); +export const meriyah = /* @__PURE__ */ toLazyLoadPlugin({ + name: "meriyah", + importPlugin: () => import("../../../plugins/meriyah.js"), + parserNames: ["meriyah"], +}); +export const postcss = /* @__PURE__ */ toLazyLoadPlugin({ + name: "postcss", + importPlugin: () => import("../../../plugins/postcss.js"), + options: postcssOptions, + languages: postcssLanguages, + parserNames: ["css", "less", "scss"], + printerNames: ["postcss"], +}); +export const typescript = /* @__PURE__ */ toLazyLoadPlugin({ + name: "typescript", + importPlugin: () => import("../../../plugins/typescript.js"), + parserNames: ["typescript"], +}); +export const yaml = /* @__PURE__ */ toLazyLoadPlugin({ + name: "yaml", + importPlugin: () => import("../../../plugins/yaml.js"), + options: yamlOptions, + languages: yamlLanguages, + parserNames: ["yaml"], + printerNames: ["yaml"], +}); diff --git a/src/main/plugins/builtin-plugins/utilities.js b/src/main/plugins/builtin-plugins/utilities.js new file mode 100644 index 000000000..b43beb267 --- /dev/null +++ b/src/main/plugins/builtin-plugins/utilities.js @@ -0,0 +1,58 @@ +/** +@param {{ + name: string, + importPlugin: () => Promise, + options?: any, + languages?: any[], + parserNames?: string[], + printerNames?: string[], +}} param0 +@returns +*/ + +function toLazyLoadPlugin({ + name, + importPlugin, + options, + languages, + parserNames, + printerNames, +}) { + const plugin = { name }; + if (options) { + plugin.options = options; + } + + if (languages) { + plugin.languages = languages; + } + + for (const { property, names } of [ + { property: "parsers", names: parserNames }, + { property: "printers", names: printerNames }, + ]) { + if (names) { + plugin[property] = Object.fromEntries( + names.map((name) => [ + name, + process.env.NODE_ENV === "production" + ? async () => { + const loaded = await importPlugin(); + Object.assign(plugin, loaded); + return loaded[property][name]; + } + : async () => { + const loaded = await importPlugin(); + // Hide `estree` printer in js plugin + plugin[property] = loaded[property]; + return loaded[property][name]; + }, + ]), + ); + } + } + + return plugin; +} + +export { toLazyLoadPlugin }; diff --git a/src/main/plugins/load-builtin-plugins.js b/src/main/plugins/load-builtin-plugins.js index 6abe376fd..021865fdc 100644 --- a/src/main/plugins/load-builtin-plugins.js +++ b/src/main/plugins/load-builtin-plugins.js @@ -1,7 +1,19 @@ -import builtinPluginsProxy from "../../plugins/builtin-plugins-proxy.js"; +const loadBuiltinPluginsWithoutCache = + process.env.NODE_ENV === "production" + ? async function () { + return Object.values( + await import("./builtin-plugins/production-plugins.js"), + ); + } + : async function () { + return Object.values( + await import("./builtin-plugins/development-plugins.js"), + ); + }; +let cache; function loadBuiltinPlugins() { - return builtinPluginsProxy; + return (cache ??= loadBuiltinPluginsWithoutCache()); } export default loadBuiltinPlugins; diff --git a/src/plugins/acorn.js b/src/plugins/acorn.js index 48f00945a..22bfa4424 100644 --- a/src/plugins/acorn.js +++ b/src/plugins/acorn.js @@ -1,4 +1,4 @@ -import { acorn } from "../language-js/parse/acorn.js"; -import { espree } from "../language-js/parse/espree.js"; +import * as acornParsers from "../language-js/parse/acorn.js"; +import * as espreeParsers from "../language-js/parse/espree.js"; -export const parsers = { acorn, espree }; +export const parsers = { ...acornParsers, ...espreeParsers }; diff --git a/src/plugins/babel.js b/src/plugins/babel.js index 128f6212a..1d7afb476 100644 --- a/src/plugins/babel.js +++ b/src/plugins/babel.js @@ -1,5 +1,5 @@ import * as babelParsers from "../language-js/parse/babel.js"; // JSON parsers are based on babel, bundle together to reduce package size -import * as jsonParsers from "../language-json/parser-json.js"; +import * as jsonParsers from "../language-json/parsers.js"; export const parsers = { ...babelParsers, ...jsonParsers }; diff --git a/src/plugins/builtin-plugins-proxy.js b/src/plugins/builtin-plugins-proxy.js deleted file mode 100644 index e72e0d360..000000000 --- a/src/plugins/builtin-plugins-proxy.js +++ /dev/null @@ -1,153 +0,0 @@ -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"; - -// Lazy load the plugins -function createParsersAndPrinters(modules) { - const parsers = Object.create(null); - const printers = Object.create(null); - - for (const { - importPlugin, - parsers: parserNames = [], - printers: printerNames = [], - } of modules) { - const loadPlugin = async () => { - const plugin = await importPlugin(); - Object.assign(parsers, plugin.parsers); - Object.assign(printers, plugin.printers); - return plugin; - }; - - for (const parserName of parserNames) { - parsers[parserName] = async () => - (await loadPlugin()).parsers[parserName]; - } - - for (const printerName of printerNames) { - printers[printerName] = async () => - (await loadPlugin()).printers[printerName]; - } - } - - return { parsers, printers }; -} - -// We need create estree as a separate plugin -// For details see tests/integration/__tests__/plugin-override-builtin-printers.js -const estreePlugin = createParsersAndPrinters([ - { - importPlugin: () => import("./estree.js"), - printers: ["estree", "estree-json"], - }, -]); - -const options = { - ...cssOptions, - ...graphqlOptions, - ...htmlOptions, - ...jsOptions, - ...markdownOptions, - ...yamlOptions, -}; - -const languages = [ - ...cssLanguages, - ...graphqlLanguages, - ...handlebarsLanguages, - ...htmlLanguages, - ...jsLanguages, - ...jsonLanguages, - ...markdownLanguages, - ...yamlLanguages, -]; - -const { parsers, printers } = createParsersAndPrinters([ - { - importPlugin: () => import("./acorn.js"), - parsers: ["acorn", "espree"], - }, - { - importPlugin: () => import("./angular.js"), - parsers: [ - "__ng_action", - "__ng_binding", - "__ng_interpolation", - "__ng_directive", - ], - }, - { - importPlugin: () => import("./babel.js"), - parsers: [ - "babel", - "babel-flow", - "babel-ts", - "__js_expression", - "__ts_expression", - "__vue_expression", - "__vue_ts_expression", - "__vue_event_binding", - "__vue_ts_event_binding", - "__babel_estree", - "json", - "json5", - "jsonc", - "json-stringify", - ], - }, - { - importPlugin: () => import("./flow.js"), - parsers: ["flow"], - }, - { - importPlugin: () => import("./glimmer.js"), - parsers: ["glimmer"], - printers: ["glimmer"], - }, - { - importPlugin: () => import("./graphql.js"), - parsers: ["graphql"], - printers: ["graphql"], - }, - { - importPlugin: () => import("./html.js"), - parsers: ["html", "angular", "vue", "lwc", "mjml"], - printers: ["html"], - }, - { - importPlugin: () => import("./markdown.js"), - parsers: ["markdown", "mdx", "remark"], - printers: ["mdast"], - }, - { - importPlugin: () => import("./meriyah.js"), - parsers: ["meriyah"], - }, - { - importPlugin: () => import("./postcss.js"), - parsers: ["css", "less", "scss"], - printers: ["postcss"], - }, - { - importPlugin: () => import("./typescript.js"), - parsers: ["typescript"], - }, - { - importPlugin: () => import("./yaml.js"), - parsers: ["yaml"], - printers: ["yaml"], - }, -]); - -export default [estreePlugin, { options, languages, parsers, printers }]; diff --git a/src/plugins/estree.js b/src/plugins/estree.js index 27f657af7..a738f2ead 100644 --- a/src/plugins/estree.js +++ b/src/plugins/estree.js @@ -1,11 +1,20 @@ +// Can't bundle due to side effects +// import { +// languages as jsLanguages, +// options as jsOptions, +// printers as jsPrinters, +// } from "../language-js/index.js"; import jsLanguages from "../language-js/languages.evaluate.js"; -import * as estreePrinter from "../language-js/printer.js"; -import jsonLanguages from "../language-json/languages.evaluate.js"; -import * as estreeJsonPrinter from "../language-json/printer-estree-json.js"; +import jsOptions from "../language-js/options.js"; +import * as jsPrinters from "../language-js/printers.js"; +import { + languages as jsonLanguages, + printers as jsonPrinters, +} from "../language-json/index.js"; export const printers = { - estree: estreePrinter, - "estree-json": estreeJsonPrinter, + ...jsPrinters, + ...jsonPrinters, }; export const languages = [...jsLanguages, ...jsonLanguages]; -export { default as options } from "../language-js/options.js"; +export const options = { ...jsOptions }; diff --git a/tests/config/run-format-test.js b/tests/config/run-format-test.js index d50a2b9dd..f40127d5f 100644 --- a/tests/config/run-format-test.js +++ b/tests/config/run-format-test.js @@ -570,27 +570,54 @@ async function format(originalText, originalOptions) { }; } -const externalPlugins = new Map([ - ["oxc", "plugin-oxc"], - ["oxc-ts", "plugin-oxc"], - ["hermes", "plugin-hermes"], -]); +async function getExternalPlugins() { + const distDirectory = new URL("../../dist/", import.meta.url); + const directories = fs.readdirSync(distDirectory); + const plugins = await Promise.all( + directories + .filter((directory) => directory.startsWith("plugin-")) + .map(async (directory) => { + const url = new URL(`./${directory}/index.mjs`, distDirectory); + const implementation = await import(url); + return { + url, + implementation, + }; + }), + ); + const externalParsers = new Map(); + for (const plugin of plugins) { + const { parsers } = plugin.implementation; + if (parsers) { + for (const parser of Object.keys(parsers)) { + externalParsers.set(parser, plugin); + } + } + } + return externalParsers; +} + +let externalParsers; async function loadPlugins(options) { - const { parser } = options; - if (externalPlugins.has(options.parser)) { - const plugins = options.plugins ?? []; - const pluginName = externalPlugins.get(parser); - const url = new URL( - isProduction - ? `../../dist/${pluginName}/index.mjs` - : `../../packages/${pluginName}/index.js`, - import.meta.url, - ); - plugins.push(TEST_STANDALONE ? await import(url) : url); - return { ...options, plugins }; + if (!isProduction || !options.parser) { + return options; } - return options; + const { parser } = options; + + if (!externalParsers) { + externalParsers = getExternalPlugins(); + externalParsers = await externalParsers; + } + if (!externalParsers.has(parser)) { + return options; + } + const plugin = externalParsers.get(parser); + + const plugins = options.plugins ?? []; + plugins.push(TEST_STANDALONE ? plugin.implementation : plugin.url); + + return { ...options, plugins }; } export default runFormatTest; diff --git a/tests/unit/builtin-plugins.js b/tests/unit/builtin-plugins.js index 31a11629d..1752cb5b7 100644 --- a/tests/unit/builtin-plugins.js +++ b/tests/unit/builtin-plugins.js @@ -29,6 +29,7 @@ test("builtin parsers", async () => { "flow", "glimmer", "graphql", + "hermes", "html", "json", "json-stringify", @@ -40,6 +41,8 @@ test("builtin parsers", async () => { "mdx", "meriyah", "mjml", + "oxc", + "oxc-ts", "remark", "scss", "typescript",