Reuse arrays in visitor keys (#18386)

This commit is contained in:
fisker Cheung authored and GitHub committed 2025-12-01 19:33:56 +08:00
1 parent c45fef106e
commit d3eb2b2d08
21 files changed
+266 -53

No files matched your search

+4 -1
View File
@@ -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;
},
+67 -11
View File
@@ -1,22 +1,17 @@
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";
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)
function serializeModule(module) {
return Object.entries(module)
.map(([specifier, value]) => {
const code =
value instanceof RegExp
? `/${value.source}/${value.flags}`
: serialize(value, { space: 2 });
: serialize(value, { space: 2, unsafe: true });
if (specifier === "default") {
return `export default ${code};`;
}
@@ -28,6 +23,67 @@ export default function esbuildPluginEvaluate() {
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: filePath }) => {
const module = await import(url.pathToFileURL(filePath));
const isVisitorKeys =
path.basename(filePath, path.extname(filePath)) ===
"visitor-keys.evaluate";
const text = isVisitorKeys
? serializeVisitorKeys(module)
: serializeModule(module);
return { contents: text };
},
);
+1 -1
View File
@@ -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);
@@ -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;
+1 -1
View File
@@ -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);
@@ -1,5 +0,0 @@
import { getVoidTags } from "@glimmer/syntax";
const htmlVoidElements = new Set(getVoidTags());
export default htmlVoidElements;
+4 -1
View File
@@ -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;
+1 -1
View File
@@ -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");
@@ -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;
@@ -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;
+1 -1
View File
@@ -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);
@@ -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;
+1 -1
View File
@@ -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);
@@ -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;
+1 -1
View File
@@ -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);
@@ -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;
@@ -1,4 +1,6 @@
/** @typedef {Record<string, readonly string[]>} VisitorKeys */
/**
@typedef {readonly string[]} Keys
@typedef {Record<string, Keys>} 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<string, Keys>} */
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,
};
+106
View File
@@ -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 () => {
@@ -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",
],
}
+6 -6
View File
@@ -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", () => {