diff --git a/packages/jco/test/fixtures/componentize/node-url/source.js b/packages/jco/test/fixtures/componentize/node-url/source.js index dd4d59de5..23cefc5a2 100644 --- a/packages/jco/test/fixtures/componentize/node-url/source.js +++ b/packages/jco/test/fixtures/componentize/node-url/source.js @@ -175,15 +175,23 @@ export function run() { "", "a/b", ].map((d) => [domainToASCII(d), domainToUnicode(d)]); - report.posixFiles = ["/tmp/a b#c?d%é", "/tmp/back\\slash", "/tmp/a\nb\tc\rd", "/a/../b/", "/"].map((p) => { - const parsed = pathToFileURL(p); - return [ - parsed.href, - fileURLToPath(parsed), - Buffer.isBuffer(fileURLToPathBuffer(parsed)), - fileURLToPathBuffer(parsed).toString("hex"), - ]; - }); + report.posixFiles = Object.fromEntries( + ["/tmp/a b#c?d%é", "/tmp/back\\slash", "/tmp/a\nb\tc\rd", "/a/../b/", "/"].map((p) => { + // Pin the native oracle's path semantics too: its default is Windows + // on Windows hosts, while components always default to POSIX. + const options = { windows: false }; + const parsed = pathToFileURL(p, options); + return [ + p, + [ + parsed.href, + fileURLToPath(parsed, options), + Buffer.isBuffer(fileURLToPathBuffer(parsed, options)), + fileURLToPathBuffer(parsed, options).toString("hex"), + ], + ]; + }), + ); report.windowsFiles = [ "C:\\Program Files\\a#b?c.txt", "C:/a/../b/", @@ -301,10 +309,10 @@ export function run() { () => domainToASCII(), () => domainToUnicode(), () => fileURLToPath("https://example.com"), - () => fileURLToPath("file:///a%2Fb"), + () => fileURLToPath("file:///a%2Fb", { windows: false }), () => fileURLToPath("file:///C:/a%5Cb", { windows: true }), () => fileURLToPath("file:///relative", { windows: true }), - () => fileURLToPath("file:///a/%FF"), + () => fileURLToPath("file:///a/%FF", { windows: false }), () => pathToFileURL(42), () => format(new URL("https://example.com"), []), () => urlToHttpOptions(null), diff --git a/packages/jco/test/node/path.js b/packages/jco/test/node/path.js index 88b0aba5c..8f09e1c2b 100644 --- a/packages/jco/test/node/path.js +++ b/packages/jco/test/node/path.js @@ -2,9 +2,9 @@ // // The adapter unit tests (`builtins.js`) call the plugin's hooks directly, which cannot // tell whether `jco componentize` uses it at all. These build a real component and run it. -import { cwd } from "node:process"; -import { join, posix, win32 } from "node:path"; +import { posix, win32 } from "node:path"; +import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation"; import { assert, expect, suite, test } from "vitest"; import { componentizeFixture, setupAsyncTest } from "../helpers.js"; @@ -14,8 +14,13 @@ suite("node:path in a component", () => { const { componentPath, stderr } = await componentizeFixture({ fixture: "node-path", bundle: true, copy: true }); assert.strictEqual(stderr, ""); + // The guest defaults to POSIX, independently of the host OS. Give it a + // WASI cwd instead of implicitly using the native process's cwd. + const guestCwd = "/workspace/project"; + const imports = new WASIShim().getImportObject(); + imports["wasi:cli/environment"] = { ...imports["wasi:cli/environment"], initialCwd: () => guestCwd }; const { instance, cleanup } = await setupAsyncTest({ - component: { name: "node-path", path: componentPath }, + component: { name: "node-path", path: componentPath, imports }, }); try { @@ -24,7 +29,7 @@ suite("node:path in a component", () => { assert.strictEqual(instance.lexical(), "a/c|C:\\a\\b|\\|true"); // resolve() of a relative path goes through wasi:cli/environment#initial-cwd. - assert.strictEqual(instance.fromCwd(), join(cwd(), "relative")); + assert.strictEqual(instance.fromCwd(), posix.resolve(guestCwd, "relative")); // Exercise the lazily loaded matcher, including its brace-expansion dependency. // Repeated calls also cover reuse after the first initialization. diff --git a/packages/jco/test/node/url.js b/packages/jco/test/node/url.js index adbbd8a4c..3373708ba 100644 --- a/packages/jco/test/node/url.js +++ b/packages/jco/test/node/url.js @@ -1,9 +1,10 @@ import assert from "node:assert/strict"; import { pathToFileURL } from "node:url"; -import { resolve } from "node:path"; +import { posix } from "node:path"; import { createRequire } from "node:module"; +import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation"; import { suite, test } from "vitest"; -import { componentizeFixture, transpileComponent } from "../helpers.js"; +import { componentizeFixture, setupAsyncTest, transpileComponent } from "../helpers.js"; import { hasJspi } from "../common.js"; const isNode24 = process.versions.node.split(".")[0] === "24"; @@ -24,6 +25,12 @@ suite("node:url in components", () => { // Avoid importing Node 24-only named exports during collection on other hosts. const { run: runInNode } = await import("../fixtures/componentize/node-url/source.js"); const expected = JSON.parse(runInNode()); + // Node 24's pathToFileURL still uses the host path.sep when restoring + // a trailing slash, even with { windows: false }. On Windows, "/" + // becomes "file:////". Use the POSIX root invariant for this case; + // retain the native differential oracle for every other path. + // https://github.com/nodejs/node/blob/v24.21.0/lib/internal/url.js#L1594-L1600 + expected.posixFiles["/"] = ["file:///", "/", true, "2f"]; assert.strictEqual(expected.stableSort, "a=2&a=1&z=1&z=0"); assert.deepEqual(expected.identity, Array(10).fill(true)); const { componentPath } = await componentizeFixture({ @@ -73,13 +80,24 @@ suite("node:url in components", () => { copy: true, extraArgs: ["--backend", backend], }); - const { modulePath } = await transpileComponent({ componentPath, name: "node-url-cwd" }); - const component = await import(modulePath); - for (const path of ["relative", "a/../two words#?%", "", "/absolute"]) { - assert.deepEqual(JSON.parse(component.fromCwd(path)), { - href: pathToFileURL(path).href, - path: resolve(path), - }); + // WASI supplies a guest cwd. Native Windows cwd and URL defaults + // are not an oracle for the component's POSIX-default node:url. + const guestCwd = "/workspace/project"; + const imports = new WASIShim().getImportObject(); + imports["wasi:cli/environment"] = { ...imports["wasi:cli/environment"], initialCwd: () => guestCwd }; + const { instance, cleanup } = await setupAsyncTest({ + component: { name: "node-url-cwd", path: componentPath, imports }, + }); + try { + for (const path of ["relative", "a/../two words#?%", "", "/absolute"]) { + const expectedPath = posix.resolve(guestCwd, path); + assert.deepEqual(JSON.parse(instance.fromCwd(path)), { + href: pathToFileURL(expectedPath, { windows: false }).href, + path: expectedPath, + }); + } + } finally { + await cleanup(); } }, ); diff --git a/packages/jco/test/node/wasi.js b/packages/jco/test/node/wasi.js index 929fee716..814acf414 100644 --- a/packages/jco/test/node/wasi.js +++ b/packages/jco/test/node/wasi.js @@ -4,6 +4,7 @@ import { spawn } from "node:child_process"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import { assert, expect, suite, test } from "vitest"; import { worldMetadataFor } from "../../src/cmd/componentize.js"; import { WASI_WIT_REQUIREMENT, injectNodeWitImports } from "../../src/node-wit.js"; @@ -79,7 +80,7 @@ suite("node:wasi", () => { const { esModuleOutputPath, cleanup } = await setupAsyncTest({ component: { name: `node-wasi-${backend}`, path: componentPath, skipInstantiation: true }, }); - const runner = join(FIXTURE.pathname, "run.js"); + const runner = fileURLToPath(new URL("run.js", FIXTURE)); try { const denied = await run(process.execPath, [runner, esModuleOutputPath, "denied"]); expect(denied.status, denied.stderr).toBe(0);