From 628e83e2000052b1a283639944f26bfb44cb5186 Mon Sep 17 00:00:00 2001 From: kezhaofeng Date: Mon, 23 Sep 2024 11:47:45 +0800 Subject: [PATCH] fix: new import.meta implement by ponyfill in commonjs, import.meta.resolve use nodejs buildin require.resolve in esmodule, import.meta.resolve use nodejs buildin import.meta.resolve or createRequest(filename).resolve add import.meta.filename / import.meta.dirname support --- deno.jsonc | 1 + lib/compiler_transforms.test.ts | 48 ++-- lib/compiler_transforms.ts | 157 +++--------- rs-lib/src/polyfills/import_meta.rs | 8 +- .../src/polyfills/scripts/deno.import-meta.ts | 229 +++++++++++++++++- tests/integration.test.ts | 17 ++ .../polyfill_import_meta_project/mod.test.ts | 33 +++ tests/polyfill_import_meta_project/mod.ts | 1 + 8 files changed, 335 insertions(+), 159 deletions(-) create mode 100644 tests/polyfill_import_meta_project/mod.test.ts create mode 100644 tests/polyfill_import_meta_project/mod.ts diff --git a/deno.jsonc b/deno.jsonc index 7dbea8c..86002be 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -40,6 +40,7 @@ "rs-lib/src/polyfills/scripts/", "tests/declaration_import_project/npm", "tests/import_map_project/npm", + "tests/import_meta_project/npm", "tests/json_module_project/npm", "tests/module_mappings_project/npm", "tests/node_types_project/npm", diff --git a/lib/compiler_transforms.test.ts b/lib/compiler_transforms.test.ts index 0c59624..5106cf8 100644 --- a/lib/compiler_transforms.test.ts +++ b/lib/compiler_transforms.test.ts @@ -4,14 +4,18 @@ import { assertEquals } from "@std/assert"; import { ts } from "@ts-morph/bootstrap"; import { transformImportMeta } from "./compiler_transforms.ts"; -function testImportReplacements(input: string, output: string, cjs = true) { +function testImportReplacements( + input: string, + output: string, + module: ts.ModuleKind, +) { const sourceFile = ts.createSourceFile( "file.ts", input, ts.ScriptTarget.Latest, ); const newSourceFile = ts.transform(sourceFile, [transformImportMeta], { - module: cjs ? ts.ModuleKind.CommonJS : ts.ModuleKind.ES2015, + module, }).transformed[0]; const text = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, @@ -19,44 +23,50 @@ function testImportReplacements(input: string, output: string, cjs = true) { assertEquals(text, output); } +const testImportReplacementsEsm = (input: string, output: string) => + testImportReplacements(input, output, ts.ModuleKind.ES2015); +const testImportReplacementsCjs = (input: string, output: string) => + testImportReplacements(input, output, ts.ModuleKind.CommonJS); -Deno.test("transform import.meta.url expressions", () => { - testImportReplacements( +Deno.test("transform import.meta.url expressions in commonjs", () => { + testImportReplacementsCjs( "function test() { new URL(import.meta.url); }", - `function test() { new URL(require("url").pathToFileURL(__filename).href); }\n`, + `function test() { new URL(globalThis[Symbol.for("import-meta-ponyfill-commonjs")](require, module).url); }\n`, + ); +}); +Deno.test("transform import.meta.url expressions in esModule", () => { + testImportReplacementsEsm( + "function test() { new URL(import.meta.url); }", + `function test() { new URL(globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url); }\n`, ); }); -Deno.test("transform import.meta.main expressions", () => { - testImportReplacements( +Deno.test("transform import.meta.main expressions in commonjs", () => { + testImportReplacementsCjs( "if (import.meta.main) { console.log('main'); }", - `if ((require.main === module)) { + `if (globalThis[Symbol.for("import-meta-ponyfill-commonjs")](require, module).main) { console.log("main"); }\n`, ); }); Deno.test("transform import.meta.main expressions in esModule", () => { - testImportReplacements( - "if (import.meta.main) { console.log('main'); }", - `if ((import.meta.url === ("file:///" + process.argv[1].replace(/\\\\/g, "/")).replace(/\\/{3,}/, "///"))) { - console.log("main"); -}\n`, - false, + testImportReplacementsEsm( + "export const isMain = import.meta.main;", + `export const isMain = globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).main;\n`, ); }); Deno.test("transform import.meta.resolve expressions", () => { - testImportReplacements( + testImportReplacementsCjs( "function test(specifier) { import.meta.resolve(specifier); }", - `function test(specifier) { new URL(specifier, require("url").pathToFileURL(__filename).href).href; }\n`, + `function test(specifier) { globalThis[Symbol.for("import-meta-ponyfill-commonjs")](require, module).resolve(specifier); }\n`, ); }); Deno.test("transform import.meta.resolve expressions in esModule", () => { - testImportReplacements( + testImportReplacementsEsm( "function test(specifier) { import.meta.resolve(specifier); }", - `function test(specifier) { new URL(specifier, import.meta.url).href; }\n`, - false, + `function test(specifier) { globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).resolve(specifier); }\n`, ); }); diff --git a/lib/compiler_transforms.ts b/lib/compiler_transforms.ts index d54dc36..6ccbcc7 100644 --- a/lib/compiler_transforms.ts +++ b/lib/compiler_transforms.ts @@ -14,147 +14,52 @@ export const transformImportMeta: ts.TransformerFactory = ( return (sourceFile) => ts.visitEachChild(sourceFile, visitNode, context); function visitNode(node: ts.Node): ts.Node { - // find `import.meta.resolve` - if ( - ts.isCallExpression(node) && - node.arguments.length === 1 && - isImportMetaProp(node.expression) && - node.expression.name.escapedText === "resolve" - ) { - return ts.visitEachChild( - getReplacementImportMetaResolve(node.arguments), - visitNode, - context, - ); - } else if (isImportMetaProp(node)) { - // find `import.meta.url` or `import.meta.main` - if (node.name.escapedText === "url" && isScriptModule) { - return getReplacementImportMetaUrl(); - } else if (node.name.escapedText === "main") { - if (isScriptModule) { - return getReplacementImportMetaMainScript(); - } else { - return getReplacementImportMetaMainEsm(); - } + // find `import.meta` + if (ts.isMetaProperty(node)) { + if (isScriptModule) { + return getReplacementImportMetaScript(); + } else { + return getReplacementImportMetaEsm(); } } return ts.visitEachChild(node, visitNode, context); } - function isImportMetaProp( - node: ts.Node, - ): node is ts.PropertyAccessExpression & { name: ts.Identifier } { - return ts.isPropertyAccessExpression(node) && - ts.isMetaProperty(node.expression) && - node.expression.keywordToken === ts.SyntaxKind.ImportKeyword && - ts.isIdentifier(node.name); - } - - function getReplacementImportMetaUrl() { - // Copy and pasted from ts-ast-viewer.com - // require("url").pathToFileURL(__filename).href - return factory.createPropertyAccessExpression( - factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createCallExpression( - factory.createIdentifier("require"), - undefined, - [factory.createStringLiteral("url")], - ), - factory.createIdentifier("pathToFileURL"), - ), - undefined, - [factory.createIdentifier("__filename")], - ), - factory.createIdentifier("href"), - ); - } - - function getReplacementImportMetaMainScript() { + function getReplacementImportMeta( + symbolFor: string, + argumentsArray: readonly ts.Expression[], + ) { // Copy and pasted from ts-ast-viewer.com - // (require.main === module) - return factory.createParenthesizedExpression(factory.createBinaryExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier("require"), - factory.createIdentifier("main"), - ), - factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), - factory.createIdentifier("module"), - )); - } - - function getReplacementImportMetaMainEsm() { - // Copy and pasted from ts-ast-viewer.com - // (import.meta.url === ('file:///'+process.argv[1].replace(/\\/g,'/')).replace(/\/{3,}/,'///')); - // 1. `process.argv[1]` is fullpath; - // 2. Win's path is `E:\path\to\main.mjs`, replace to `E:/path/to/main.mjs` - return factory.createParenthesizedExpression( - factory.createBinaryExpression( - factory.createPropertyAccessExpression( - factory.createMetaProperty( - ts.SyntaxKind.ImportKeyword, - factory.createIdentifier("meta"), - ), - factory.createIdentifier("url"), - ), - factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + // globalThis[Symbol.for('import-meta-ponyfill')](...args) + return factory.createCallExpression( + factory.createElementAccessExpression( + factory.createIdentifier("globalThis"), factory.createCallExpression( factory.createPropertyAccessExpression( - factory.createParenthesizedExpression( - factory.createBinaryExpression( - factory.createStringLiteral("file:///"), - factory.createToken(ts.SyntaxKind.PlusToken), - factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createElementAccessExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier("process"), - factory.createIdentifier("argv"), - ), - factory.createNumericLiteral("1"), - ), - factory.createIdentifier("replace"), - ), - undefined, - [ - factory.createRegularExpressionLiteral("/\\\\/g"), - factory.createStringLiteral("/"), - ], - ), - ), - ), - factory.createIdentifier("replace"), + factory.createIdentifier("Symbol"), + factory.createIdentifier("for"), ), undefined, - [ - factory.createRegularExpressionLiteral("/\\/{3,}/"), - factory.createStringLiteral("///"), - ], + [factory.createStringLiteral(symbolFor)], ), ), + undefined, + argumentsArray, ); } - - function getReplacementImportMetaResolve(args: ts.NodeArray) { - // Copy and pasted from ts-ast-viewer.com - // new URL(specifier, import.meta.url).href - return factory.createPropertyAccessExpression( - factory.createNewExpression( - factory.createIdentifier("URL"), - undefined, - [ - ...args, - factory.createPropertyAccessExpression( - factory.createMetaProperty( - ts.SyntaxKind.ImportKeyword, - factory.createIdentifier("meta"), - ), - factory.createIdentifier("url"), - ), - ], + function getReplacementImportMetaScript() { + return getReplacementImportMeta("import-meta-ponyfill-commonjs", [ + factory.createIdentifier("require"), + factory.createIdentifier("module"), + ]); + } + function getReplacementImportMetaEsm() { + return getReplacementImportMeta("import-meta-ponyfill-esmodule", [ + factory.createMetaProperty( + ts.SyntaxKind.ImportKeyword, + factory.createIdentifier("meta"), ), - factory.createIdentifier("href"), - ); + ]); } }; diff --git a/rs-lib/src/polyfills/import_meta.rs b/rs-lib/src/polyfills/import_meta.rs index 82d3ea0..bf8f72d 100644 --- a/rs-lib/src/polyfills/import_meta.rs +++ b/rs-lib/src/polyfills/import_meta.rs @@ -2,7 +2,6 @@ use deno_ast::view::Expr; use deno_ast::view::Node; -use deno_ast::SourceRanged; use super::Polyfill; use super::PolyfillVisitContext; @@ -15,13 +14,10 @@ impl Polyfill for ImportMetaPolyfill { true } - fn visit_node(&self, node: Node, context: &PolyfillVisitContext) -> bool { + fn visit_node(&self, node: Node, _context: &PolyfillVisitContext) -> bool { if let Node::MemberExpr(expr) = node { if let Expr::MetaProp(_meta) = expr.obj { - let text = expr.prop.text_fast(context.program); - if text == "main" || text == "resolve" { - return true; - } + return true; } } false diff --git a/rs-lib/src/polyfills/scripts/deno.import-meta.ts b/rs-lib/src/polyfills/scripts/deno.import-meta.ts index 07b6207..82cd889 100644 --- a/rs-lib/src/polyfills/scripts/deno.import-meta.ts +++ b/rs-lib/src/polyfills/scripts/deno.import-meta.ts @@ -1,5 +1,53 @@ +/** + * Based on [import-meta-ponyfill](https://github.com/gaubee/import-meta-ponyfill), + * but instead of using npm to install additional dependencies, + * this approach manually consolidates cjs/mjs/d.ts into a single file. + * + * Note that this code might be imported multiple times + * (for example, both dnt.test.polyfills.ts and dnt.polyfills.ts contain this code; + * or Node.js might dynamically clear the cache and then force a require). + * Therefore, it's important to avoid redundant writes to global objects. + * Additionally, consider that commonjs is used alongside esm, + * so the two ponyfill functions are stored independently in two separate global objects. + */ +//@ts-ignore +import { createRequire } from "node:module"; +//@ts-ignore +import { fileURLToPath, pathToFileURL, type URL } from "node:url"; +//@ts-ignore +import { dirname } from "node:path"; declare global { interface ImportMeta { + /** A string representation of the fully qualified module URL. When the + * module is loaded locally, the value will be a file URL (e.g. + * `file:///path/module.ts`). + * + * You can also parse the string as a URL to determine more information about + * how the current module was loaded. For example to determine if a module was + * local or not: + * + * ```ts + * const url = new URL(import.meta.url); + * if (url.protocol === "file:") { + * console.log("this module was loaded locally"); + * } + * ``` + */ + url: string; + /** + * A function that returns resolved specifier as if it would be imported + * using `import(specifier)`. + * + * ```ts + * console.log(import.meta.resolve("./foo.js")); + * // file:///dev/foo.js + * ``` + * + * @param specifier The module specifier to resolve relative to `parent`. + * @param parent The absolute parent module URL to resolve from. + * @returns The absolute (`file:`) URL string for the resolved module. + */ + resolve(specifier: string, parent?: string | URL | undefined): string; /** A flag that indicates if the current module is the main module that was * called when starting the program under Deno. * @@ -11,17 +59,182 @@ declare global { */ main: boolean; - /** A function that returns resolved specifier as if it would be imported - * using `import(specifier)`. + /** The absolute path of the current module. * - * ```ts - * console.log(import.meta.resolve("./foo.js")); - * // file:///dev/foo.js + * This property is only provided for local modules (ie. using `file://` URLs). + * + * Example: + * ``` + * // Unix + * console.log(import.meta.filename); // /home/alice/my_module.ts + * + * // Windows + * console.log(import.meta.filename); // C:\alice\my_module.ts + * ``` + */ + filename: string; + + /** The absolute path of the directory containing the current module. + * + * This property is only provided for local modules (ie. using `file://` URLs). + * + * * Example: + * ``` + * // Unix + * console.log(import.meta.dirname); // /home/alice + * + * // Windows + * console.log(import.meta.dirname); // C:\alice * ``` */ - // @ts-ignore override - resolve(specifier: string): string; + dirname: string; } } -export {} +type NodeRequest = ReturnType; +type NodeModule = NonNullable; +interface ImportMetaPonyfillCommonjs { + (require: NodeRequest, module: NodeModule): ImportMeta; +} +interface ImportMetaPonyfillEsmodule { + (importMeta: ImportMeta): ImportMeta; +} +interface ImportMetaPonyfill + extends ImportMetaPonyfillCommonjs, ImportMetaPonyfillEsmodule { +} + +const defineGlobalPonyfill = (symbolFor: string, fn: Function) => { + if (!Reflect.has(globalThis, Symbol.for(symbolFor))) { + Object.defineProperty( + globalThis, + Symbol.for(symbolFor), + { + configurable: true, + get() { + return fn; + }, + }, + ); + } +}; + +export let import_meta_ponyfill_commonjs = ( + Reflect.get(globalThis, Symbol.for("import-meta-ponyfill-commonjs")) ?? + (() => { + const moduleImportMetaWM = new WeakMap(); + return (require, module) => { + let importMetaCache = moduleImportMetaWM.get(module); + if (importMetaCache == null) { + const importMeta = Object.assign(Object.create(null), { + url: pathToFileURL(module.filename).href, + main: require.main == module, + resolve: (specifier: string, parentURL = importMeta.url) => { + return pathToFileURL( + (importMeta.url === parentURL + ? require + : createRequire(parentURL)) + .resolve(specifier), + ).href; + }, + filename: module.filename, + dirname: module.path, + }); + moduleImportMetaWM.set(module, importMeta); + importMetaCache = importMeta; + } + return importMetaCache; + }; + })() +) as ImportMetaPonyfillCommonjs; +defineGlobalPonyfill( + "import-meta-ponyfill-commonjs", + import_meta_ponyfill_commonjs, +); + +export let import_meta_ponyfill_esmodule = ( + Reflect.get(globalThis, Symbol.for("import-meta-ponyfill-esmodule")) ?? + ((importMeta: ImportMeta) => { + const resolveFunStr = String(importMeta.resolve); + const shimWs = new WeakSet(); + //@ts-ignore + const mainUrl = ("file:///" + process.argv[1].replace(/\\/g, "/")) + .replace( + /\/{3,}/, + "///", + ); + const commonShim = (importMeta: ImportMeta) => { + if (typeof importMeta.main !== "boolean") { + importMeta.main = importMeta.url === mainUrl; + } + if (typeof importMeta.filename !== "string") { + importMeta.filename = fileURLToPath(importMeta.url); + importMeta.dirname = dirname(importMeta.filename); + } + }; + if ( + // v16.2.0+, v14.18.0+: Add support for WHATWG URL object to parentURL parameter. + resolveFunStr === "undefined" || + // v20.0.0+, v18.19.0+"" This API now returns a string synchronously instead of a Promise. + resolveFunStr.startsWith("async") + // enable by --experimental-import-meta-resolve flag + ) { + import_meta_ponyfill_esmodule = (importMeta: ImportMeta) => { + if (!shimWs.has(importMeta)) { + shimWs.add(importMeta); + const importMetaUrlRequire = { + url: importMeta.url, + require: createRequire(importMeta.url), + }; + importMeta.resolve = function resolve( + specifier: string, + parentURL = importMeta.url, + ) { + return pathToFileURL( + (importMetaUrlRequire.url === parentURL + ? importMetaUrlRequire.require + : createRequire(parentURL)).resolve(specifier), + ).href; + }; + commonShim(importMeta); + } + return importMeta; + }; + } else { + /// native support + import_meta_ponyfill_esmodule = (importMeta: ImportMeta) => { + if (!shimWs.has(importMeta)) { + shimWs.add(importMeta); + commonShim(importMeta); + } + return importMeta; + }; + } + return import_meta_ponyfill_esmodule(importMeta); + }) +) as ImportMetaPonyfillEsmodule; +defineGlobalPonyfill( + "import-meta-ponyfill-esmodule", + import_meta_ponyfill_esmodule, +); + +export let import_meta_ponyfill = ( + (...args: any[]) => { + const _MODULE = (() => { + if (typeof require === "function" && typeof module === "object") { + return "commonjs"; + } else { + // eval("typeof import.meta"); + return "esmodule"; + } + })(); + if (_MODULE === "commonjs") { + //@ts-ignore + import_meta_ponyfill = (r, m) => import_meta_ponyfill_commonjs(r, m); + } else { + //@ts-ignore + import_meta_ponyfill = (im) => import_meta_ponyfill_esmodule(im); + } + //@ts-ignore + return import_meta_ponyfill(...args); + } +) as ImportMetaPonyfill; diff --git a/tests/integration.test.ts b/tests/integration.test.ts index ba9ad13..0187e9f 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -857,6 +857,22 @@ Deno.test("should build and test the array find last polyfill project", async () }); }); +Deno.test("should build and test the import meta polyfill project", async () => { + await runTest("polyfill_import_meta_project", { + test: true, + entryPoints: ["mod.ts"], + outDir: "./npm", + shims: { deno: "dev" }, + package: { + name: "polyfill-import-meta-project", + version: "0.0.0", + }, + packageManager: "pnpm", + }, (output) => { + output.assertExists("esm/_dnt.polyfills.js"); + }); +}); + Deno.test("should build and test the array.fromAsync polyfill project", async () => { await runTest("polyfill_array_from_async_project", { entryPoints: ["mod.ts"], @@ -1149,6 +1165,7 @@ async function runTest( | "polyfill_project" | "polyfill_array_from_async_project" | "polyfill_array_find_last_project" + | "polyfill_import_meta_project" | "module_mappings_project" | "node_types_project" | "undici_project" diff --git a/tests/polyfill_import_meta_project/mod.test.ts b/tests/polyfill_import_meta_project/mod.test.ts new file mode 100644 index 0000000..12f7684 --- /dev/null +++ b/tests/polyfill_import_meta_project/mod.test.ts @@ -0,0 +1,33 @@ +const assert = (ok: boolean) => { + if (!ok) { + throw new Error("no ok"); + } +}; + +Deno.test("import.meta expression", () => { + assert( + eval("typeof Deno") === "object" ? true : function () { + return import.meta.main; + }.toString().includes("import-meta-ponyfill"), + ); +}); + +Deno.test("import.meta.main", () => { + assert(typeof import.meta.main === "boolean"); +}); + +Deno.test("import.meta.url", () => { + assert(typeof import.meta.url === "string"); +}); + +Deno.test("import.meta.resolve", () => { + assert(typeof import.meta.resolve === "function"); +}); + +Deno.test("import.meta.filename", () => { + assert(typeof import.meta.filename === "string"); +}); + +Deno.test("import.meta.dirname", () => { + assert(typeof import.meta.dirname === "string"); +}); diff --git a/tests/polyfill_import_meta_project/mod.ts b/tests/polyfill_import_meta_project/mod.ts new file mode 100644 index 0000000..bee2c8e --- /dev/null +++ b/tests/polyfill_import_meta_project/mod.ts @@ -0,0 +1 @@ +export const isMain = import.meta.main;