From 0f05bc5871dd64fd103c902c259ed00d165a3f5a Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Mon, 3 Nov 2025 04:54:10 +0800 Subject: [PATCH] Build: Reuse `doc.mjs` in `index.mjs` (#18193) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- package.json | 1 + scripts/build/build-javascript-module.js | 2 +- scripts/build/config.js | 1 + scripts/build/transform/index.js | 64 ++++---- .../get-public-doc-functionality.js | 42 ++++++ scripts/build/transform/transforms/index.js | 3 + .../transform-doc-module-imports.js | 138 ++++++++++++++++++ tests/unit/syntax-transform.js | 65 ++++++--- yarn.lock | 8 + 9 files changed, 275 insertions(+), 49 deletions(-) create mode 100644 scripts/build/transform/transforms/get-public-doc-functionality.js create mode 100644 scripts/build/transform/transforms/transform-doc-module-imports.js diff --git a/package.json b/package.json index dae1252cd..169776602 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,7 @@ "jsesc": "3.1.0", "knip": "5.66.2", "magic-string": "0.30.21", + "make-synchronized": "0.8.0", "nano-spawn": "2.0.0", "node-style-text": "2.1.2", "npm-run-all2": "8.0.4", diff --git a/scripts/build/build-javascript-module.js b/scripts/build/build-javascript-module.js index 838432047..25287e8a3 100644 --- a/scripts/build/build-javascript-module.js +++ b/scripts/build/build-javascript-module.js @@ -96,7 +96,7 @@ function getEsbuildOptions({ packageConfig, file, cliOptions }) { // Transform `.at`, `Object.hasOwn`, and `String#replaceAll` { module: "*", - process: transform, + process: (text, file) => transform(text, file, buildOptions), }, // #12493, not sure what the problem is, but replace the cjs version with esm version seems fix it { diff --git a/scripts/build/config.js b/scripts/build/config.js index 754b4d0ff..862a85903 100644 --- a/scripts/build/config.js +++ b/scripts/build/config.js @@ -786,6 +786,7 @@ const nodejsFiles = [ }, ], addDefaultExport: true, + reuseDocModule: true, }, { input: "src/index.cjs", diff --git a/scripts/build/transform/index.js b/scripts/build/transform/index.js index a5ce775a3..3a8d9475c 100644 --- a/scripts/build/transform/index.js +++ b/scripts/build/transform/index.js @@ -8,36 +8,42 @@ import allTransforms from "./transforms/index.js"; /* Doesn't work for dependencies */ -function transform(original, file) { - if ( - ![ - SOURCE_DIR, - ...[ - /* spell-checker: disable */ - "camelcase", - "angular-estree-parser", - "jest-docblock", - "espree", - "@babel/parser", - "@typescript-eslint/typescript-estree", - "meriyah", - "@glimmer", - "@prettier/cli", - "hermes-parser", - "kasi", - "fast-string-truncated-width", - "fast-ignore", - /* spell-checker: enable */ - ].map((directory) => - path.join(PROJECT_ROOT, `node_modules/${directory}/`), - ), - ].some((directory) => file.startsWith(directory)) - ) { +function shouldTransform(file, buildOptions) { + const allowedDirectories = buildOptions.reuseDocModule + ? [SOURCE_DIR] + : [ + SOURCE_DIR, + ...[ + /* spell-checker: disable */ + "camelcase", + "angular-estree-parser", + "jest-docblock", + "espree", + "@babel/parser", + "@typescript-eslint/typescript-estree", + "meriyah", + "@glimmer", + "@prettier/cli", + "hermes-parser", + "kasi", + "fast-string-truncated-width", + "fast-ignore", + /* spell-checker: enable */ + ].map((directory) => + path.join(PROJECT_ROOT, `node_modules/${directory}/`), + ), + ]; + + return allowedDirectories.some((directory) => file.startsWith(directory)); +} + +function transform(original, file, buildOptions) { + if (!shouldTransform(file, buildOptions)) { return original; } const transforms = allTransforms.filter( - (transform) => !transform.shouldSkip(original, file), + (transform) => !transform.shouldSkip(original, file, buildOptions), ); if (transforms.length === 0) { @@ -55,7 +61,7 @@ function transform(original, file) { }); traverse(ast, (node) => { for (const transform of transforms) { - if (!transform.test(node)) { + if (!transform.test(node, file)) { continue; } @@ -65,7 +71,7 @@ function transform(original, file) { injected.add(transform.inject); } - const replacement = transform.transform(node); + const replacement = transform.transform(node, file); if (replacement && replacement !== node) { replaceNode(node, replacement); } @@ -80,8 +86,6 @@ function transform(original, file) { ast, { sourceFileName: file, - experimental_preserveFormat: true, - retainLines: true, comments: true, jsescOption: null, minified: false, diff --git a/scripts/build/transform/transforms/get-public-doc-functionality.js b/scripts/build/transform/transforms/get-public-doc-functionality.js new file mode 100644 index 000000000..796416156 --- /dev/null +++ b/scripts/build/transform/transforms/get-public-doc-functionality.js @@ -0,0 +1,42 @@ +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import makeSynchronized from "make-synchronized"; +import { SOURCE_DIR } from "../../../utils/index.js"; + +const PUBLIC_MODULE_URL = pathToFileURL( + path.join(SOURCE_DIR, "document/public.js"), +); + +let publicDocFunctionalities; +async function getPublicDocFunctionalities() { + if (!publicDocFunctionalities) { + const publicDocModule = await import(PUBLIC_MODULE_URL); + + publicDocFunctionalities = new WeakMap(); + for (const [namespace, functionalities] of Object.entries( + publicDocModule, + )) { + for (const [name, functionality] of Object.entries(functionalities)) { + if (publicDocFunctionalities.has(functionality)) { + continue; + } + + publicDocFunctionalities.set(functionality, { namespace, name }); + } + } + } + + return publicDocFunctionalities; +} + +async function isPublicDocFunctionality(name, file) { + const [publicDocFunctionalities, { [name]: functionality }] = + await Promise.all([ + getPublicDocFunctionalities(), + import(pathToFileURL(file)), + ]); + + return publicDocFunctionalities.get(functionality); +} + +export default makeSynchronized(import.meta, isPublicDocFunctionality); diff --git a/scripts/build/transform/transforms/index.js b/scripts/build/transform/transforms/index.js index 6b7a64e71..4b00a7e87 100644 --- a/scripts/build/transform/transforms/index.js +++ b/scripts/build/transform/transforms/index.js @@ -1,3 +1,4 @@ +import transformDocModuleImports from "./transform-doc-module-imports.js"; import transformMethodAt from "./transform-method-at.js"; import transformMethodFindLast from "./transform-method-find-last.js"; import transformMethodFindLastIndex from "./transform-method-find-last-index.js"; @@ -20,4 +21,6 @@ export default [ transformMethodToReversed, // For performance transformStringRaw, + // Replace imports with public doc module if possible + transformDocModuleImports, ]; diff --git a/scripts/build/transform/transforms/transform-doc-module-imports.js b/scripts/build/transform/transforms/transform-doc-module-imports.js new file mode 100644 index 000000000..9a85fa5d2 --- /dev/null +++ b/scripts/build/transform/transforms/transform-doc-module-imports.js @@ -0,0 +1,138 @@ +import path from "node:path"; +import { SOURCE_DIR } from "../../../utils/index.js"; +import getPublicDocFunctionality from "./get-public-doc-functionality.js"; +import { + createIdentifier, + createMemberExpression, + createStringLiteral, +} from "./utilities.js"; + +const DOC_MODULE_DIRECTORY = path.join(SOURCE_DIR, "document/"); +const PUBLIC_DOC_MODULE_PATH = path.join(SOURCE_DIR, "document/public.js"); + +/** + * @param {import("@babel/types").Node} node + * @returns {boolean} + */ +function getImportDeclarationReplacement(node, file) { + if (node.type !== "ImportDeclaration") { + return; + } + + const source = node.source.value; + + if (!source.startsWith(".")) { + return; + } + + const resolved = path.join(file, `../${source}`); + + if ( + !resolved.startsWith(DOC_MODULE_DIRECTORY) || + resolved === PUBLIC_DOC_MODULE_PATH + ) { + return; + } + + const removed = []; + const specifiers = node.specifiers + .map((specifier) => { + const { imported, local } = specifier; + if (imported.type !== "Identifier" || local.type !== "Identifier") { + return specifier; + } + + const { name } = imported; + + const replacement = getPublicDocFunctionality(name, resolved); + + if (!replacement) { + return specifier; + } + + removed.push({ variable: local.name, ...replacement }); + }) + .filter(Boolean); + + if (removed.length === 0) { + return; + } + + return { + node: + removed.length === node.specifiers.length + ? undefined + : { ...node, specifiers }, + removed, + }; +} + +/** + * @param {import("@babel/types").Program} node + * @returns {import("@babel/types").Program} + */ +function transformProgramWithoutCache(program, file) { + const replaced = []; + const body = program.body + .map((node) => { + const replacement = getImportDeclarationReplacement(node, file); + + if (!replacement) { + return node; + } + + replaced.push(...replacement.removed); + + return replacement.node; + }) + .filter(Boolean); + + if (replaced.length === 0) { + return; + } + + const identityPrefix = "__doc_"; + const namespaces = [...new Set(replaced.map(({ namespace }) => namespace))]; + + body.unshift( + { + type: "ImportDeclaration", + source: createStringLiteral(PUBLIC_DOC_MODULE_PATH), + specifiers: namespaces.map((namespace) => ({ + type: "ImportSpecifier", + imported: createIdentifier(namespace), + local: createIdentifier(`${identityPrefix}${namespace}`), + })), + }, + { + type: "VariableDeclaration", + kind: "const", + declarations: replaced.map(({ variable, namespace, name }) => ({ + type: "VariableDeclarator", + id: createIdentifier(variable), + init: createMemberExpression(`${identityPrefix}${namespace}.${name}`), + })), + }, + ); + + return { ...program, body }; +} + +const cache = new WeakMap(); +function transformProgram(program, file) { + if (program.type !== "Program") { + return program; + } + if (!cache.has(program)) { + cache.set(program, transformProgramWithoutCache(program, file) ?? program); + } + + return cache.get(program); +} + +export default { + shouldSkip: (text, file, buildOptions) => + !buildOptions.reuseDocModule || file.startsWith(DOC_MODULE_DIRECTORY), + test: (program, file) => program !== transformProgram(program, file), + transform: transformProgram, +}; diff --git a/tests/unit/syntax-transform.js b/tests/unit/syntax-transform.js index 525df39f3..318d130bf 100644 --- a/tests/unit/syntax-transform.js +++ b/tests/unit/syntax-transform.js @@ -8,15 +8,25 @@ const file = url.fileURLToPath( const shimsDirectory = url.fileURLToPath( new URL("../../scripts/build/shims", import.meta.url), ); +const publicDocPath = url.fileURLToPath( + new URL("../../src/document/public.js", import.meta.url), +); const transform = (code) => - transformCode(code, file).replaceAll( - JSON.stringify(shimsDirectory + path.sep).slice(1, -1), - "/", - ); + transformCode(code, file, { + reuseDocModule: true, + }) + .replaceAll( + JSON.stringify(shimsDirectory + path.sep).slice(1, -1), + "/", + ) + .replaceAll( + JSON.stringify(publicDocPath).slice(1, -1), + "", + ); test("Object.hasOwn", () => { expect(transform("Object.hasOwn(foo, bar)")).toMatchInlineSnapshot( - `"Object.prototype.hasOwnProperty.call(foo,bar)"`, + `"Object.prototype.hasOwnProperty.call(foo, bar);"`, ); }); @@ -24,25 +34,25 @@ test(".at", () => { expect(transform("foo.at(-1)")).toMatchInlineSnapshot(` "import __at from "/method-at.js"; - __at (/* OPTIONAL_OBJECT: false */0,foo,-1)" + __at(/* OPTIONAL_OBJECT: false */0, foo, -1);" `); expect(transform("foo?.at(-1)")).toMatchInlineSnapshot(` "import __at from "/method-at.js"; - __at (/* OPTIONAL_OBJECT: true */1,foo,-1)" + __at(/* OPTIONAL_OBJECT: true */1, foo, -1);" `); expect(transform("foo?.bar.baz.at(-1)")).toMatchInlineSnapshot(` "import __at from "/method-at.js"; - __at (/* OPTIONAL_OBJECT: true */1,foo?.bar.baz,-1)" + __at(/* OPTIONAL_OBJECT: true */1, foo?.bar.baz, -1);" `); expect(transform("foo.at(-1)?.bar")).toMatchInlineSnapshot(` "import __at from "/method-at.js"; - __at (/* OPTIONAL_OBJECT: false */0,foo,-1)?.bar" + __at(/* OPTIONAL_OBJECT: false */0, foo, -1)?.bar;" `); // Optional call not supported @@ -53,7 +63,7 @@ test("String#replaceAll", () => { expect(transform("foo.replaceAll('a', 'b')")).toMatchInlineSnapshot(` "import __replaceAll from "/method-replace-all.js"; - __replaceAll (/* OPTIONAL_OBJECT: false */0,foo,'a','b')" + __replaceAll(/* OPTIONAL_OBJECT: false */0, foo, 'a', 'b');" `); }); @@ -61,12 +71,12 @@ test("Array#findLast", () => { expect(transform("foo.findLast(callback)")).toMatchInlineSnapshot(` "import __findLast from "/method-find-last.js"; - __findLast (/* OPTIONAL_OBJECT: false */0,foo,callback)" + __findLast(/* OPTIONAL_OBJECT: false */0, foo, callback);" `); expect(transform("foo?.findLast(callback)")).toMatchInlineSnapshot(` "import __findLast from "/method-find-last.js"; - __findLast (/* OPTIONAL_OBJECT: true */1,foo,callback)" + __findLast(/* OPTIONAL_OBJECT: true */1, foo, callback);" `); // Not supported @@ -79,12 +89,12 @@ test("Array#findLastIndex", () => { expect(transform("foo.findLastIndex(callback)")).toMatchInlineSnapshot(` "import __findLastIndex from "/method-find-last-index.js"; - __findLastIndex (/* OPTIONAL_OBJECT: false */0,foo,callback)" + __findLastIndex(/* OPTIONAL_OBJECT: false */0, foo, callback);" `); expect(transform("foo?.findLastIndex(callback)")).toMatchInlineSnapshot(` "import __findLastIndex from "/method-find-last-index.js"; - __findLastIndex (/* OPTIONAL_OBJECT: true */1,foo,callback)" + __findLastIndex(/* OPTIONAL_OBJECT: true */1, foo, callback);" `); // Not supported @@ -97,12 +107,12 @@ test("Array#toReversed", () => { expect(transform("foo.toReversed()")).toMatchInlineSnapshot(` "import __toReversed from "/method-to-reversed.js"; - __toReversed (/* OPTIONAL_OBJECT: false */0,foo)" + __toReversed(/* OPTIONAL_OBJECT: false */0, foo);" `); expect(transform("foo?.toReversed()")).toMatchInlineSnapshot(` "import __toReversed from "/method-to-reversed.js"; - __toReversed (/* OPTIONAL_OBJECT: true */1,foo)" + __toReversed(/* OPTIONAL_OBJECT: true */1, foo);" `); expect(transform("foo.toReversed(extraArgument)")).toMatchInlineSnapshot( @@ -112,9 +122,28 @@ test("Array#toReversed", () => { test("String.raw", () => { expect(transform("String.raw`\\\\\\uINVALID`")).toMatchInlineSnapshot( - String.raw`""\\\\\\uINVALID""`, + String.raw`""\\\\\\uINVALID";"`, ); expect(transform("String.raw`\\uINVALID${'world'}`")).toMatchInlineSnapshot( - `" \`\\\\uINVALID\${'world'}\`"`, + `"\`\\\\uINVALID\${'world'}\`;"`, ); }); + +test("public doc functionality", () => { + expect(transform('import {align, line} from "./document/index.js"')) + .toMatchInlineSnapshot(` + "import { builders as __doc_builders } from ""; + const align = __doc_builders.align, + line = __doc_builders.line;" + `); + expect( + transform( + 'import {align as renamedAlign, line, notExists} from "./document/index.js"', + ), + ).toMatchInlineSnapshot(` + "import { builders as __doc_builders } from ""; + const renamedAlign = __doc_builders.align, + line = __doc_builders.line; + import { notExists } from "./document/index.js";" + `); +}); diff --git a/yarn.lock b/yarn.lock index 9a80e2e2b..6e02cbd97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6385,6 +6385,13 @@ __metadata: languageName: node linkType: hard +"make-synchronized@npm:0.8.0": + version: 0.8.0 + resolution: "make-synchronized@npm:0.8.0" + checksum: 10/e744bafcd61ee1ecabe6fb2c295ecb4b06a7bfe4e844222b80b7a5ae80a4d27ba657abc4892d1c702fa2f6ae568d8505e801c1498fe1379dd824ded5483d978c + languageName: node + linkType: hard + "makeerror@npm:1.0.12": version: 1.0.12 resolution: "makeerror@npm:1.0.12" @@ -7330,6 +7337,7 @@ __metadata: leven: "npm:4.1.0" linguist-languages: "npm:9.0.0" magic-string: "npm:0.30.21" + make-synchronized: "npm:0.8.0" meriyah: "npm:6.1.4" micromatch: "npm:4.0.8" minimist: "npm:1.2.8"