From 7dfa8167f0246725a8a7f6952e3d41d7421fc34a Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 19 Sep 2024 12:34:08 +0200 Subject: [PATCH 1/2] feat: improve `jiti/native` compatibility with node and deno --- .github/workflows/ci.yml | 20 +++--- .gitignore | 1 + README.md | 4 ++ lib/jiti-native.mjs | 72 +++++++++++++------ lib/types.d.ts | 5 ++ package.json | 14 ++-- test/bun-native.test.ts | 26 ------- test/native/bun.test.ts | 21 ++++++ test/native/deno.test.ts | 36 ++++++++++ test/native/node.test.ts | 35 +++++++++ ...gister-test.mjs => node-register.test.mjs} | 0 vitest.config.ts | 6 +- 12 files changed, 176 insertions(+), 64 deletions(-) delete mode 100644 test/bun-native.test.ts create mode 100644 test/native/bun.test.ts create mode 100644 test/native/deno.test.ts create mode 100644 test/native/node.test.ts rename test/{register-test.mjs => node-register.test.mjs} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68deeb30..43f0f53a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,11 +4,9 @@ on: push: branches: - main - - v1 pull_request: branches: - main - - v1 permissions: id-token: write @@ -20,7 +18,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - node: [18, 20] + node: [18, 20, 22] fail-fast: false steps: @@ -33,18 +31,24 @@ jobs: node-version: ${{ matrix.node }} cache: "pnpm" - uses: oven-sh/setup-bun@v2 - if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 20 }} + if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 22 }} with: bun-version: latest + - uses: denoland/setup-deno@v1 + if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 22 }} + with: + deno-version: v1.x - run: pnpm install - run: pnpm lint - if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 20 }} + if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 22 }} - run: pnpm build - run: pnpm vitest run --coverage - - run: pnpm test:register - if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 20 }} + - run: pnpm test:node-register + if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 22 }} - run: pnpm test:bun --coverage - if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 20 }} + if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 22 }} + - run: pnpm test:native + if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 22 }} - name: nightly release if: | github.event_name == 'push' && diff --git a/.gitignore b/.gitignore index 1fdc881e..a7993e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist *.log* coverage .tmp +deno.lock diff --git a/README.md b/README.md index a7c7273e..b8955810 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,10 @@ List of modules (within `node_modules`) to always use native require for them. List of modules (within `node_modules`) to transform them regardless of syntax. +### `importMeta` + +Parent module's [`import.meta`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta) context to use for ESM resolution. + ### `experimentalBun` - Type: Boolean diff --git a/lib/jiti-native.mjs b/lib/jiti-native.mjs index eaaf20eb..6113be24 100644 --- a/lib/jiti-native.mjs +++ b/lib/jiti-native.mjs @@ -3,15 +3,15 @@ * @typedef {import('./types').JitiOptions} JitiOptions */ +const isDeno = "Deno" in globalThis; + /** - * @param {string} parentURL - * @param {JitiOptions} [_opts] + * @param {string|URL} [parentURL] + * @param {JitiOptions} [jitiOptions] * @returns {Jiti} */ -export function createJiti(parentURL = "./_.js", _opts) { - if (isDir(parentURL)) { - parentURL += "/_.js"; - } +export function createJiti(parentURL, jitiOptions) { + parentURL = normalizeParentURL(parentURL); /** @type {Jiti} */ function jiti() { @@ -26,7 +26,13 @@ export function createJiti(parentURL = "./_.js", _opts) { jiti.esmResolve = (id, opts) => { try { - return import.meta.resolve(id, opts?.parentURL || parentURL); + const importMeta = jitiOptions?.importMeta || import.meta; + if (isDeno) { + // Deno throws TypeError: Invalid arguments when passing parentURL + return importMeta.resolve(id); + } + const parent = normalizeParentURL(opts?.parentURL || parentURL); + return importMeta.resolve(id, parent); } catch (error) { if (opts?.try) { return undefined; @@ -37,15 +43,38 @@ export function createJiti(parentURL = "./_.js", _opts) { }; jiti.import = async function (id, opts) { - const resolved = await this.esmResolve(id, opts); - if (!resolved) { - if (opts?.try) { - return undefined; - } else { - throw new Error(`Cannot resolve module '${id}'`); + for (const suffix of ["", "/index"]) { + // prettier-ignore + for (const ext of ["", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"]) { + try { + const resolved = this.esmResolve(id + suffix + ext, opts); + if (!resolved) { + continue; + } + let importAttrs = undefined + if (resolved.endsWith('.json')) { + importAttrs = { with: { type: 'json'}} + } + return await import(resolved, importAttrs); + } catch (error) { + if (error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') { + continue + } + if (opts?.try) { + return undefined; + } + throw error; + } } } - return import(resolved); + if (!opts?.try) { + const parent = normalizeParentURL(opts?.parentURL || parentURL); + const error = new Error( + `[jiti] [ERR_MODULE_NOT_FOUND] Cannot import '${id}' from '${parent}'.`, + ); + error.code = "ERR_MODULE_NOT_FOUND"; + throw error; + } }; jiti.transform = () => { @@ -78,12 +107,15 @@ function unsupportedError(message) { ); } -function isDir(filename) { - if (filename instanceof URL || filename.startsWith("file://")) { - return false; +function normalizeParentURL(input) { + if (!input) { + return "file:///"; + } + if (input instanceof URL || input.startsWith("file://")) { + return input; } - if (filename.endsWith("/")) { - return true; + if (input.endsWith("/")) { + input += "_"; // append a dummy filename } - return !filename.split("/").pop().includes("."); + return `file://${input}`; } diff --git a/lib/types.d.ts b/lib/types.d.ts index c3df73d6..4a4ac0af 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -131,6 +131,11 @@ export interface JitiOptions { */ transformModules?: string[]; + /** + * Parent module's import.meta context to use for ESM resolution. + */ + importMeta?: ImportMeta; + /** * Enable experimental native Bun support for transformations. */ diff --git a/package.json b/package.json index 3e254eb6..0aed8d91 100644 --- a/package.json +++ b/package.json @@ -41,15 +41,19 @@ "build": "pnpm clean && NODE_ENV=production pnpm webpack", "clean": "rm -rf dist", "dev": "pnpm clean && pnpm webpack --watch", - "jiti": "JITI_DEBUG=1 ./lib/jiti-cli.mjs", + "jiti": "JITI_DEBUG=1 lib/jiti-cli.mjs", "lint": "eslint . && prettier -c src lib test stubs", "lint:fix": "eslint --fix . && prettier -w src lib test stubs", "prepack": "pnpm build", "release": "pnpm build && pnpm test && changelogen --release --prerelease --push --publish --publishTag 2x", - "test": "pnpm lint && pnpm test:types && vitest run --coverage && pnpm test:register && pnpm test:bun", - "test:types": "tsc --noEmit", - "test:register": "node ./test/register-test.mjs", - "test:bun": "bun --bun test test/bun" + "test": "pnpm lint && pnpm test:types && vitest run --coverage && pnpm test:node-register && pnpm test:bun && pnpm test:native", + "test:bun": "bun --bun test test/bun", + "test:native": "pnpm test:native:bun && pnpm test:native:node && pnpm test:native:deno", + "test:native:bun": "bun --bun test test/native/bun.test.ts", + "test:native:deno": "deno test -A test/native/deno.test.ts", + "test:native:node": "node --test --experimental-strip-types test/native/node.test.ts", + "test:node-register": "node --test test/node-register.test.mjs", + "test:types": "tsc --noEmit" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/test/bun-native.test.ts b/test/bun-native.test.ts deleted file mode 100644 index a33f61c3..00000000 --- a/test/bun-native.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { fileURLToPath } from "node:url"; -import { readdir } from "node:fs/promises"; -// @ts-ignore -import { test } from "bun:test"; - -import { createJiti } from "../lib/jiti-native.mjs"; - -const fixturesDir = fileURLToPath(new URL("fixtures", import.meta.url)); - -const fixtures = await readdir(fixturesDir); - -const _jiti = createJiti(fixturesDir); - -for (const fixture of fixtures) { - if ( - fixture === "error-runtime" || - fixture === "error-parse" || - fixture === "typescript" - ) { - continue; - } - - test("fixtures/" + fixture + " (ESM) (Native)", async () => { - await _jiti.import("./" + fixture); - }); -} diff --git a/test/native/bun.test.ts b/test/native/bun.test.ts new file mode 100644 index 00000000..fcb6d7f1 --- /dev/null +++ b/test/native/bun.test.ts @@ -0,0 +1,21 @@ +import { readdir } from "node:fs/promises"; +// @ts-ignore +import { test } from "bun:test"; + +import { createJiti } from "../../lib/jiti-native.mjs"; + +const fixtures = await readdir(new URL("../fixtures", import.meta.url)); + +const jiti = createJiti(import.meta.url, { importMeta: import.meta }); + +const ignore = new Set(["error-runtime", "error-parse", "typescript"]); + +for (const fixture of fixtures) { + if (ignore.has(fixture)) { + continue; + } + + test("fixtures/" + fixture + " (ESM) (Native)", async () => { + await jiti.import("../fixtures/" + fixture); + }); +} diff --git a/test/native/deno.test.ts b/test/native/deno.test.ts new file mode 100644 index 00000000..226091a8 --- /dev/null +++ b/test/native/deno.test.ts @@ -0,0 +1,36 @@ +import { readdir } from "node:fs/promises"; +import { test } from "node:test"; + +import { createJiti } from "../../lib/jiti-native.mjs"; + +const fixtures = await readdir(new URL("../fixtures", import.meta.url)); + +const jiti = createJiti(import.meta.url, { importMeta: import.meta }); + +// Mostly broken because default type is set to commonjs in fixtures root +const ignore = new Set( + [ + "error-runtime", + "error-parse", + "pure-esm-dep", + "proto", + "json", + "esm", + "env", + "typescript", + "top-level-await", + "exotic", + "circular", + "import-map", + ].filter(Boolean), +); + +for (const fixture of fixtures) { + if (ignore.has(fixture)) { + continue; + } + + test("fixtures/" + fixture + " (ESM) (Native)", async () => { + await jiti.import("../fixtures/" + fixture); + }); +} diff --git a/test/native/node.test.ts b/test/native/node.test.ts new file mode 100644 index 00000000..9633f573 --- /dev/null +++ b/test/native/node.test.ts @@ -0,0 +1,35 @@ +import { readdir } from "node:fs/promises"; +import { test } from "node:test"; + +import { createJiti } from "../../lib/jiti-native.mjs"; + +const fixtures = await readdir(new URL("../fixtures", import.meta.url)); + +const jiti = createJiti(import.meta.url, { importMeta: import.meta }); + +// Mostly broken because default type is set to commonjs in fixtures root +const ignore = new Set( + [ + "error-runtime", + "error-parse", + "pure-esm-dep", + "proto", + "json", + "esm", + "env", + "typescript", + "top-level-await", + "exotic", + "circular", + ].filter(Boolean), +); + +for (const fixture of fixtures) { + if (ignore.has(fixture)) { + continue; + } + + test("fixtures/" + fixture + " (ESM) (Native)", async () => { + await jiti.import("../fixtures/" + fixture); + }); +} diff --git a/test/register-test.mjs b/test/node-register.test.mjs similarity index 100% rename from test/register-test.mjs rename to test/node-register.test.mjs diff --git a/vitest.config.ts b/vitest.config.ts index cc612de2..b433e306 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,10 +2,6 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - exclude: [ - "**/test.{ts,mjs,cjs,js}", - "node_modules/**/*", - "test/bun*.test.ts", - ], + include: ["test/fixturtes.test.ts", "test/utils.test.ts"], }, }); From c238261a7240717ccb6f119060fa280a0217ae5b Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 19 Sep 2024 12:37:22 +0200 Subject: [PATCH 2/2] add note --- README.md | 2 +- lib/types.d.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b8955810..ab0529e5 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ List of modules (within `node_modules`) to transform them regardless of syntax. ### `importMeta` -Parent module's [`import.meta`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta) context to use for ESM resolution. +Parent module's [`import.meta`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta) context to use for ESM resolution. (only used for `jiti/native` import). ### `experimentalBun` diff --git a/lib/types.d.ts b/lib/types.d.ts index 4a4ac0af..53455cb9 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -133,6 +133,8 @@ export interface JitiOptions { /** * Parent module's import.meta context to use for ESM resolution. + * + * (Only used for `jiti/native` import) */ importMeta?: ImportMeta;