From d3eb2b2d081f4b5b1008e64655ce7cc8fefd731e Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Mon, 1 Dec 2025 19:33:56 +0800 Subject: [PATCH] Reuse arrays in visitor keys (#18386) --- scripts/build/config.js | 5 +- scripts/build/esbuild-plugins/evaluate.js | 92 ++++++++++++--- src/language-css/get-visitor-keys.js | 2 +- ...sitor-keys.js => visitor-keys.evaluate.js} | 6 +- src/language-handlebars/get-visitor-keys.js | 2 +- .../html-void-elements.evaluate.js | 5 - src/language-handlebars/utilities.js | 5 +- ...sitor-keys.evaluate.js => visitor-keys.js} | 0 src/language-html/get-visitor-keys.js | 2 +- ...sitor-keys.js => visitor-keys.evaluate.js} | 6 +- .../traverse/visitor-keys.evaluate.js | 6 +- src/language-json/get-visitor-keys.js | 2 +- ...sitor-keys.js => visitor-keys.evaluate.js} | 6 +- src/language-markdown/get-visitor-keys.js | 2 +- ...sitor-keys.js => visitor-keys.evaluate.js} | 6 +- src/language-yaml/get-visitor-keys.js | 2 +- ...sitor-keys.js => visitor-keys.evaluate.js} | 30 +++-- .../visitor-keys.js} | 32 +++++- tests/integration/__tests__/bundle.js | 106 ++++++++++++++++++ tests/unit/__snapshots__/visitor-keys.js.snap | 4 - tests/unit/visitor-keys.js | 12 +- 21 files changed, 273 insertions(+), 60 deletions(-) rename src/language-css/{visitor-keys.js => visitor-keys.evaluate.js} (89%) delete mode 100644 src/language-handlebars/html-void-elements.evaluate.js rename src/language-handlebars/{visitor-keys.evaluate.js => visitor-keys.js} (100%) rename src/language-html/{visitor-keys.js => visitor-keys.evaluate.js} (80%) rename src/language-json/{visitor-keys.js => visitor-keys.evaluate.js} (70%) rename src/language-markdown/{visitor-keys.js => visitor-keys.evaluate.js} (84%) rename src/language-yaml/{visitor-keys.js => visitor-keys.evaluate.js} (61%) rename src/{language-js/traverse/utilities.js => utilities/visitor-keys.js} (69%) diff --git a/scripts/build/config.js b/scripts/build/config.js index 28aa80abf..f26525238 100644 --- a/scripts/build/config.js +++ b/scripts/build/config.js @@ -598,7 +598,10 @@ const pluginFiles = [ "const LOCAL_DEBUG = false &&", ); - text = text.replace(/(?<=\n)export .*?;/u, "export { preprocess };"); + text = text.replace( + /(?<=\n)export .*?;/u, + "export { preprocess, getVoidTags, visitorKeys };", + ); return text; }, diff --git a/scripts/build/esbuild-plugins/evaluate.js b/scripts/build/esbuild-plugins/evaluate.js index 009bdd090..7a007aeac 100644 --- a/scripts/build/esbuild-plugins/evaluate.js +++ b/scripts/build/esbuild-plugins/evaluate.js @@ -1,33 +1,89 @@ +import assert from "node:assert"; +import path from "node:path"; import url from "node:url"; import { isValidIdentifier } from "@babel/types"; +import { outdent } from "outdent"; import serialize from "serialize-javascript"; +function serializeModule(module) { + return Object.entries(module) + .map(([specifier, value]) => { + const code = + value instanceof RegExp + ? `/${value.source}/${value.flags}` + : serialize(value, { space: 2, unsafe: true }); + if (specifier === "default") { + return `export default ${code};`; + } + + if (!isValidIdentifier(specifier)) { + throw new Error(`${specifier} is not a valid specifier`); + } + + return `export const ${specifier} = ${code};`; + }) + .join("\n"); +} + +function serializeVisitorKeys(module) { + const specifiers = Object.keys(module); + assert.deepEqual(specifiers, ["default"]); + + const references = new Map(); + const properties = Object.entries(module.default); + for (const [, keys] of properties) { + if (!references.has(keys)) { + references.set(keys, 1); + } else { + references.set(keys, references.get(keys) + 1); + } + } + + const variables = []; + const propertiesText = []; + for (const [type, keys] of properties) { + const isSingleReference = references.get(keys) === 1; + const key = isValidIdentifier(type) ? type : JSON.stringify(type); + if (isSingleReference) { + propertiesText.push(` ${key}: ${JSON.stringify(keys)},`); + } else { + if (!variables.includes(keys)) { + variables.push(keys); + } + const index = variables.indexOf(keys); + propertiesText.push(` ${key}: vk[${index}],`); + } + } + + const code = outdent` + const vk = [ + ${variables.map((keys) => ` ${JSON.stringify(keys)},`).join("\n")} + ]; + + export default { + ${propertiesText.join("\n")} + }; + `; + + return code; +} + export default function esbuildPluginEvaluate() { return { name: "evaluate", setup(build) { build.onLoad( { filter: /\.evaluate\.[cm]?js$/, namespace: "file" }, - async ({ path }) => { - const module = await import(url.pathToFileURL(path)); - const text = Object.entries(module) - .map(([specifier, value]) => { - const code = - value instanceof RegExp - ? `/${value.source}/${value.flags}` - : serialize(value, { space: 2 }); + async ({ path: filePath }) => { + const module = await import(url.pathToFileURL(filePath)); + const isVisitorKeys = + path.basename(filePath, path.extname(filePath)) === + "visitor-keys.evaluate"; - if (specifier === "default") { - return `export default ${code};`; - } + const text = isVisitorKeys + ? serializeVisitorKeys(module) + : serializeModule(module); - if (!isValidIdentifier(specifier)) { - throw new Error(`${specifier} is not a valid specifier`); - } - - return `export const ${specifier} = ${code};`; - }) - .join("\n"); return { contents: text }; }, ); diff --git a/src/language-css/get-visitor-keys.js b/src/language-css/get-visitor-keys.js index 558c57e6b..f4d75eb17 100644 --- a/src/language-css/get-visitor-keys.js +++ b/src/language-css/get-visitor-keys.js @@ -1,5 +1,5 @@ import createGetVisitorKeys from "../utilities/create-get-visitor-keys.js"; -import visitorKeys from "./visitor-keys.js"; +import visitorKeys from "./visitor-keys.evaluate.js"; const getVisitorKeys = createGetVisitorKeys(visitorKeys); diff --git a/src/language-css/visitor-keys.js b/src/language-css/visitor-keys.evaluate.js similarity index 89% rename from src/language-css/visitor-keys.js rename to src/language-css/visitor-keys.evaluate.js index 5ba1fe98d..3cfdc9661 100644 --- a/src/language-css/visitor-keys.js +++ b/src/language-css/visitor-keys.evaluate.js @@ -1,4 +1,6 @@ -const visitorKeys = { +import { generateReferenceSharedVisitorKeys } from "../utilities/visitor-keys.js"; + +const visitorKeys = generateReferenceSharedVisitorKeys({ "css-root": ["frontMatter", "nodes"], "css-comment": [], "css-rule": ["selector", "nodes"], @@ -43,6 +45,6 @@ const visitorKeys = { "value-atword": [], "value-unicode-range": [], "value-unknown": [], -}; +}); export default visitorKeys; diff --git a/src/language-handlebars/get-visitor-keys.js b/src/language-handlebars/get-visitor-keys.js index f4d75eb17..558c57e6b 100644 --- a/src/language-handlebars/get-visitor-keys.js +++ b/src/language-handlebars/get-visitor-keys.js @@ -1,5 +1,5 @@ import createGetVisitorKeys from "../utilities/create-get-visitor-keys.js"; -import visitorKeys from "./visitor-keys.evaluate.js"; +import visitorKeys from "./visitor-keys.js"; const getVisitorKeys = createGetVisitorKeys(visitorKeys); diff --git a/src/language-handlebars/html-void-elements.evaluate.js b/src/language-handlebars/html-void-elements.evaluate.js deleted file mode 100644 index c8116fef9..000000000 --- a/src/language-handlebars/html-void-elements.evaluate.js +++ /dev/null @@ -1,5 +0,0 @@ -import { getVoidTags } from "@glimmer/syntax"; - -const htmlVoidElements = new Set(getVoidTags()); - -export default htmlVoidElements; diff --git a/src/language-handlebars/utilities.js b/src/language-handlebars/utilities.js index 1bd614516..fe10c2831 100644 --- a/src/language-handlebars/utilities.js +++ b/src/language-handlebars/utilities.js @@ -1,4 +1,7 @@ -import htmlVoidElements from "./html-void-elements.evaluate.js"; +// Do not use void tags from other package, won't match +import { getVoidTags } from "@glimmer/syntax"; + +const htmlVoidElements = new Set(getVoidTags()); function isUppercase(string) { return string.toUpperCase() === string; diff --git a/src/language-handlebars/visitor-keys.evaluate.js b/src/language-handlebars/visitor-keys.js similarity index 100% rename from src/language-handlebars/visitor-keys.evaluate.js rename to src/language-handlebars/visitor-keys.js diff --git a/src/language-html/get-visitor-keys.js b/src/language-html/get-visitor-keys.js index 01f48a9d7..87d21dee0 100644 --- a/src/language-html/get-visitor-keys.js +++ b/src/language-html/get-visitor-keys.js @@ -1,5 +1,5 @@ import createGetVisitorKeys from "../utilities/create-get-visitor-keys.js"; -import visitorKeys from "./visitor-keys.js"; +import visitorKeys from "./visitor-keys.evaluate.js"; const getVisitorKeys = createGetVisitorKeys(visitorKeys, "kind"); diff --git a/src/language-html/visitor-keys.js b/src/language-html/visitor-keys.evaluate.js similarity index 80% rename from src/language-html/visitor-keys.js rename to src/language-html/visitor-keys.evaluate.js index 534bce879..cf036e223 100644 --- a/src/language-html/visitor-keys.js +++ b/src/language-html/visitor-keys.evaluate.js @@ -1,4 +1,6 @@ -const visitorKeys = { +import { generateReferenceSharedVisitorKeys } from "../utilities/visitor-keys.js"; + +const visitorKeys = generateReferenceSharedVisitorKeys({ root: ["children"], element: ["attrs", "children"], ieConditionalComment: ["children"], @@ -17,6 +19,6 @@ const visitorKeys = { angularLetDeclarationInitializer: [], angularIcuExpression: ["cases"], angularIcuCase: ["expression"], -}; +}); export default visitorKeys; diff --git a/src/language-js/traverse/visitor-keys.evaluate.js b/src/language-js/traverse/visitor-keys.evaluate.js index 754d23cba..e2c40be3a 100644 --- a/src/language-js/traverse/visitor-keys.evaluate.js +++ b/src/language-js/traverse/visitor-keys.evaluate.js @@ -3,10 +3,11 @@ import { visitorKeys as tsVisitorKeys } from "@typescript-eslint/visitor-keys"; import { visitorKeys as angularVisitorKeys } from "angular-estree-parser"; import flowVisitorKeys from "hermes-parser/dist/generated/ESTreeVisitorKeys.js"; import { + generateReferenceSharedVisitorKeys, removeNodeTypes, removeVisitorKeys, unionVisitorKeys, -} from "./utilities.js"; +} from "../../utilities/visitor-keys.js"; const additionalVisitorKeys = { // Prettier @@ -70,4 +71,7 @@ let visitorKeys = unionVisitorKeys( visitorKeys = removeNodeTypes(visitorKeys, excludeNodeTypes); visitorKeys = removeVisitorKeys(visitorKeys, excludeVisitorKeys); +// This should be the last step +visitorKeys = generateReferenceSharedVisitorKeys(visitorKeys); + export default visitorKeys; diff --git a/src/language-json/get-visitor-keys.js b/src/language-json/get-visitor-keys.js index 558c57e6b..f4d75eb17 100644 --- a/src/language-json/get-visitor-keys.js +++ b/src/language-json/get-visitor-keys.js @@ -1,5 +1,5 @@ import createGetVisitorKeys from "../utilities/create-get-visitor-keys.js"; -import visitorKeys from "./visitor-keys.js"; +import visitorKeys from "./visitor-keys.evaluate.js"; const getVisitorKeys = createGetVisitorKeys(visitorKeys); diff --git a/src/language-json/visitor-keys.js b/src/language-json/visitor-keys.evaluate.js similarity index 70% rename from src/language-json/visitor-keys.js rename to src/language-json/visitor-keys.evaluate.js index bb13cbc10..cd913da24 100644 --- a/src/language-json/visitor-keys.js +++ b/src/language-json/visitor-keys.evaluate.js @@ -1,4 +1,6 @@ -const visitorKeys = { +import { generateReferenceSharedVisitorKeys } from "../utilities/visitor-keys.js"; + +const visitorKeys = generateReferenceSharedVisitorKeys({ JsonRoot: ["node"], ArrayExpression: ["elements"], ObjectExpression: ["properties"], @@ -11,6 +13,6 @@ const visitorKeys = { Identifier: [], TemplateLiteral: ["quasis"], TemplateElement: [], -}; +}); export default visitorKeys; diff --git a/src/language-markdown/get-visitor-keys.js b/src/language-markdown/get-visitor-keys.js index 558c57e6b..f4d75eb17 100644 --- a/src/language-markdown/get-visitor-keys.js +++ b/src/language-markdown/get-visitor-keys.js @@ -1,5 +1,5 @@ import createGetVisitorKeys from "../utilities/create-get-visitor-keys.js"; -import visitorKeys from "./visitor-keys.js"; +import visitorKeys from "./visitor-keys.evaluate.js"; const getVisitorKeys = createGetVisitorKeys(visitorKeys); diff --git a/src/language-markdown/visitor-keys.js b/src/language-markdown/visitor-keys.evaluate.js similarity index 84% rename from src/language-markdown/visitor-keys.js rename to src/language-markdown/visitor-keys.evaluate.js index f906d15b6..ae4ab2e0b 100644 --- a/src/language-markdown/visitor-keys.js +++ b/src/language-markdown/visitor-keys.evaluate.js @@ -1,4 +1,6 @@ -const visitorKeys = { +import { generateReferenceSharedVisitorKeys } from "../utilities/visitor-keys.js"; + +const visitorKeys = generateReferenceSharedVisitorKeys({ root: ["children"], paragraph: ["children"], sentence: ["children"], @@ -36,6 +38,6 @@ const visitorKeys = { tableRow: ["children"], listItem: ["children"], text: [], -}; +}); export default visitorKeys; diff --git a/src/language-yaml/get-visitor-keys.js b/src/language-yaml/get-visitor-keys.js index 558c57e6b..f4d75eb17 100644 --- a/src/language-yaml/get-visitor-keys.js +++ b/src/language-yaml/get-visitor-keys.js @@ -1,5 +1,5 @@ import createGetVisitorKeys from "../utilities/create-get-visitor-keys.js"; -import visitorKeys from "./visitor-keys.js"; +import visitorKeys from "./visitor-keys.evaluate.js"; const getVisitorKeys = createGetVisitorKeys(visitorKeys); diff --git a/src/language-yaml/visitor-keys.js b/src/language-yaml/visitor-keys.evaluate.js similarity index 61% rename from src/language-yaml/visitor-keys.js rename to src/language-yaml/visitor-keys.evaluate.js index d30a060e3..a50ef8ae4 100644 --- a/src/language-yaml/visitor-keys.js +++ b/src/language-yaml/visitor-keys.evaluate.js @@ -1,4 +1,21 @@ -const visitorKeys = Object.fromEntries( +import { generateReferenceSharedVisitorKeys } from "../utilities/visitor-keys.js"; + +/** +@import {VisitorKeys} from "../utilities/visitor-keys.js"; +*/ + +const commentsKeys = [ + "indicatorComment", + "leadingComments", + "middleComments", + "trailingComment", + "endComments", +]; + +const tagAndAnchor = ["anchor", "tag"]; + +/** @type {VisitorKeys} */ +let visitorKeys = Object.fromEntries( Object.entries({ root: ["children"], document: ["head", "body", "children"], @@ -28,15 +45,12 @@ const visitorKeys = Object.fromEntries( type, [ ...keys, - "anchor", - "tag", - "indicatorComment", - "leadingComments", - "middleComments", - "trailingComment", - "endComments", + ...(type !== "tag" && type !== "anchor" ? tagAndAnchor : []), + ...commentsKeys, ], ]), ); +visitorKeys = generateReferenceSharedVisitorKeys(visitorKeys); + export default visitorKeys; diff --git a/src/language-js/traverse/utilities.js b/src/utilities/visitor-keys.js similarity index 69% rename from src/language-js/traverse/utilities.js rename to src/utilities/visitor-keys.js index de4c1584a..4ce303eec 100644 --- a/src/language-js/traverse/utilities.js +++ b/src/utilities/visitor-keys.js @@ -1,4 +1,6 @@ -/** @typedef {Record} VisitorKeys */ +/** +@typedef {readonly string[]} Keys +@typedef {Record} VisitorKeys */ const unique = (values) => [...new Set(values)]; @@ -64,4 +66,30 @@ function removeNodeTypes(visitorKeys, nodeTypesToRemove) { return result; } -export { removeNodeTypes, removeVisitorKeys, unionVisitorKeys }; +/** +@param {VisitorKeys} visitorKeys +@returns {VisitorKeys} +*/ +function generateReferenceSharedVisitorKeys(visitorKeys) { + /** @type {Map} */ + const cache = new Map(); + /** @type {VisitorKeys} */ + const result = {}; + + for (const [type, keys] of Object.entries(visitorKeys)) { + const cacheKey = keys.toSorted().join("\0"); + if (!cache.has(cacheKey)) { + cache.set(cacheKey, keys); + } + result[type] = cache.get(cacheKey); + } + + return result; +} + +export { + generateReferenceSharedVisitorKeys, + removeNodeTypes, + removeVisitorKeys, + unionVisitorKeys, +}; diff --git a/tests/integration/__tests__/bundle.js b/tests/integration/__tests__/bundle.js index 2c05526ea..cbba5d3fa 100644 --- a/tests/integration/__tests__/bundle.js +++ b/tests/integration/__tests__/bundle.js @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { inspect } from "node:util"; import createEsmUtils from "esm-utils"; import fastGlob from "fast-glob"; import coreOptions from "../../../src/main/core-options.evaluate.js"; @@ -70,6 +71,111 @@ describe("standalone", () => { expect(esmOutput).toBe(umdOutput); }); } + + const printerVisitorKeysSettings = new Map([ + [ + "estree", + { + sharedVisitorKeys: true, + nodes: [ + { type: "FunctionDeclaration" }, + { type: "FunctionExpression" }, + ], + }, + ], + [ + "estree-json", + { + sharedVisitorKeys: true, + nodes: [{ type: "StringLiteral" }, { type: "Identifier" }], + }, + ], + [ + "glimmer", + { + sharedVisitorKeys: false, + nodes: [{ type: "Template" }, { type: "Block" }], + }, + ], + [ + "graphql", + { + sharedVisitorKeys: false, + nodes: [{ kind: "StringValue" }, { kind: "BooleanValue" }], + }, + ], + [ + "html", + { + sharedVisitorKeys: true, + nodes: [{ kind: "comment" }, { kind: "cdata" }], + }, + ], + [ + "mdast", + { + sharedVisitorKeys: true, + nodes: [{ type: "code" }, { type: "image" }], + }, + ], + [ + "postcss", + { + sharedVisitorKeys: true, + nodes: [{ type: "media-query-list" }, { type: "selector-pseudo" }], + }, + ], + [ + "yaml", + { + sharedVisitorKeys: true, + nodes: [{ type: "blockLiteral" }, { type: "quoteSingle" }], + }, + ], + ]); + + test("visitor keys with shared reference", () => { + for (const [name, printer] of esmPlugins.flatMap((plugin) => + Object.entries(plugin.printers ?? {}), + )) { + try { + expect(printerVisitorKeysSettings.has(name)).toBe(true); + } catch { + throw new Error(`Missing settings for printer '${name}'.`); + } + const { getVisitorKeys } = printer; + expect(typeof getVisitorKeys).toBe("function"); + const { sharedVisitorKeys, nodes } = printerVisitorKeysSettings.get(name); + expect( + typeof sharedVisitorKeys === "boolean" && + Array.isArray(nodes) && + nodes.length > 1, + ).toBe(true); + const keys = nodes.map((node) => getVisitorKeys(node)); + + try { + expect(keys.every((keys) => Array.isArray(keys))).toBe(true); + } catch { + throw new Error( + `Missing visitor keys for '${name}' nodes: ${inspect(nodes)}.`, + ); + } + const [firstNodeKeys, ...restNodeKeys] = keys; + if (sharedVisitorKeys) { + // Should be same reference + expect(restNodeKeys.every((keys) => keys === firstNodeKeys)).toBe(true); + } else { + // Should be same, but not same reference + expect( + restNodeKeys.every( + (keys) => + keys !== firstNodeKeys && + JSON.stringify(keys) === JSON.stringify(firstNodeKeys), + ), + ).toBe(true); + } + } + }); }); test("global objects", async () => { diff --git a/tests/unit/__snapshots__/visitor-keys.js.snap b/tests/unit/__snapshots__/visitor-keys.js.snap index 0d816d2eb..75c96533b 100644 --- a/tests/unit/__snapshots__/visitor-keys.js.snap +++ b/tests/unit/__snapshots__/visitor-keys.js.snap @@ -1668,12 +1668,10 @@ exports[`visitor keys yaml 1`] = ` "trailingComment", ], "anchor": [ - "anchor", "endComments", "indicatorComment", "leadingComments", "middleComments", - "tag", "trailingComment", ], "blockFolded": [ @@ -1892,12 +1890,10 @@ exports[`visitor keys yaml 1`] = ` "trailingComment", ], "tag": [ - "anchor", "endComments", "indicatorComment", "leadingComments", "middleComments", - "tag", "trailingComment", ], } diff --git a/tests/unit/visitor-keys.js b/tests/unit/visitor-keys.js index 5411b42b7..cce4eb192 100644 --- a/tests/unit/visitor-keys.js +++ b/tests/unit/visitor-keys.js @@ -1,11 +1,11 @@ -import postcssVisitorKeys from "../../src/language-css/visitor-keys.js"; +import postcssVisitorKeys from "../../src/language-css/visitor-keys.evaluate.js"; import graphqlVisitorKeys from "../../src/language-graphql/visitor-keys.js"; -import glimmerVisitorKeys from "../../src/language-handlebars/visitor-keys.evaluate.js"; -import htmlVisitorKeys from "../../src/language-html/visitor-keys.js"; +import glimmerVisitorKeys from "../../src/language-handlebars/visitor-keys.js"; +import htmlVisitorKeys from "../../src/language-html/visitor-keys.evaluate.js"; import estreeVisitorKeys from "../../src/language-js/traverse/visitor-keys.evaluate.js"; -import jsonVisitorKeys from "../../src/language-json/visitor-keys.js"; -import remarkVisitorKeys from "../../src/language-markdown/visitor-keys.js"; -import yamlVisitorKeys from "../../src/language-yaml/visitor-keys.js"; +import jsonVisitorKeys from "../../src/language-json/visitor-keys.evaluate.js"; +import remarkVisitorKeys from "../../src/language-markdown/visitor-keys.evaluate.js"; +import yamlVisitorKeys from "../../src/language-yaml/visitor-keys.evaluate.js"; // Keep eye on package change describe("visitor keys", () => {