Skip to content

Commit 37148fb

Browse files
authored
fix(core): resolve new URL() and wasm from on-disk source (#1455) (#1464)
1 parent f5b5bf7 commit 37148fb

28 files changed

Lines changed: 438 additions & 129 deletions

e2e/new-url/index.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { readFileSync } from 'node:fs';
2+
import { fileURLToPath } from 'node:url';
3+
import { expect, it } from '@rstest/core';
4+
5+
// Regression test for #1455: `new URL(<relative literal>, import.meta.url)`
6+
// must resolve at runtime relative to the source module, instead of being
7+
// rewritten by Rspack into a hashed bundled asset path.
8+
it('resolves a sibling file via new URL(..., import.meta.url)', () => {
9+
const siblingPath = fileURLToPath(new URL('./sibling.txt', import.meta.url));
10+
expect(siblingPath).toMatch(/[\\/]new-url[\\/]sibling\.txt$/);
11+
expect(readFileSync(siblingPath, 'utf8')).toBe('hello');
12+
});

e2e/new-url/sibling.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello

e2e/rstest.config.mts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { fileURLToPath } from 'node:url';
12
import { defineConfig } from '@rstest/core';
23

34
/**
@@ -42,6 +43,16 @@ export default defineConfig({
4243
react: 'commonjs react',
4344
},
4445
},
46+
resolve: {
47+
alias: {
48+
// Fixture-only: lets wasm-imports/src/aliasmod.wasm reach its glue through
49+
// a non-relative specifier, exercising the wasm loader's resolver-based
50+
// import wiring (bare/alias, not just `./` paths).
51+
'@e2e/wasm-glue': fileURLToPath(
52+
new URL('./wasm-imports/src/alias-glue.js', import.meta.url),
53+
),
54+
},
55+
},
4556
pool: {
4657
// Limit to 80% of available workers to reduce "worker exited unexpectedly"
4758
// errors in resource-constrained environments (e.g., certain CI runners).

e2e/wasm-imports/index.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect, test } from '@rstest/core';
2+
import { callExp, getImpCalls } from './src/glue.js';
3+
4+
// Import-bearing wasm (the wasm declares `(import "<module>" "imp" ...)`):
5+
// - mod.wasm imports a RESOLVABLE JS module ('./glue.js') — the wasm-bindgen
6+
// `--target bundler` shape, including the circular glue<->wasm dependency.
7+
// wasmLoader.mjs links the glue statically and instantiates with an
8+
// importObject, so a plain `import` works transparently.
9+
// - envmod.wasm imports the synthetic, non-resolvable 'env' module: the build
10+
// must still succeed (no `import 'env'` emitted) and only fail at runtime.
11+
12+
test('resolvable glue import: circular wasm-bindgen-bundler shape links transparently', () => {
13+
expect(callExp()).toBe(42); // glue.callExp -> wasm.exp -> imports ./glue.js imp() -> 42
14+
});
15+
16+
test('direct import of an import-bearing wasm resolves its importObject', async () => {
17+
const ns = await import('./src/mod.wasm');
18+
expect(ns.exp()).toBe(42);
19+
});
20+
21+
test('non-resolvable env import builds, and only LinkErrors at runtime', async () => {
22+
await expect(import('./src/envmod.wasm')).rejects.toThrow();
23+
});
24+
25+
// The wasm loader wires an import-module by actually resolving it, not by
26+
// checking for a `./ ../ /` prefix. aliasmod.wasm imports `@e2e/wasm-glue`
27+
// (a non-relative specifier mapped via resolve.alias); a prefix-only heuristic
28+
// would drop it and the wasm would LinkError. Actual resolution wires the glue.
29+
test('alias import module: non-relative specifier resolves its glue via resolve.alias', async () => {
30+
const ns = await import('./src/aliasmod.wasm');
31+
expect(ns.exp()).toBe(42); // wasm.exp -> imports @e2e/wasm-glue imp() -> 42
32+
});
33+
34+
// Regression guard for the load-bearing property that distinguishes the
35+
// build-time wasmLoader from any native auto-link approach: the glue the test
36+
// imports and the glue the wasm links against must be the SAME instance. The
37+
// loader keeps glue in rspack's module graph (one shared instance), so a state
38+
// mutation the wasm triggers through `imp()` is observable here. A native
39+
// `import('x.wasm')` would link the wasm against a separate Node-loaded glue
40+
// copy, leaving `getImpCalls()` at its prior value (split-brain).
41+
test('import-bearing wasm links the same glue instance the test holds (no split-brain)', () => {
42+
const before = getImpCalls();
43+
callExp(); // -> wasm.exp() -> calls glue.imp() exactly once
44+
expect(getImpCalls()).toBe(before + 1);
45+
});

e2e/wasm-imports/src/alias-glue.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Glue for aliasmod.wasm, reached through a `resolve.alias` (see the e2e root
2+
// config's `@e2e/wasm-glue` entry). It exercises the loader wiring an
3+
// import-module whose specifier is NOT relative (`./ ../ /`): the old
4+
// prefix heuristic dropped these, actual resolution wires them.
5+
export function imp() {
6+
return 42;
7+
}

e2e/wasm-imports/src/aliasmod.wasm

60 Bytes
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export function exp(): number;

e2e/wasm-imports/src/envmod.wasm

49 Bytes
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export function exp(): number;

e2e/wasm-imports/src/generate.mjs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Regenerates the two minimal wasm fixtures used by ../index.test.ts.
2+
// Run: `node src/generate.mjs`. Each module imports `<mod>.imp: () => i32`
3+
// and exports `exp: () => i32` whose body is `call imp`.
4+
import { writeFileSync } from 'node:fs';
5+
6+
const str = (s) => [s.length, ...[...s].map((c) => c.charCodeAt(0))];
7+
const wasmImporting = (mod, name) => {
8+
const imp = [0x01, ...str(mod), ...str(name), 0x00, 0x00]; // import mod.name func t0
9+
return Buffer.from([
10+
0x00,
11+
0x61,
12+
0x73,
13+
0x6d,
14+
0x01,
15+
0x00,
16+
0x00,
17+
0x00, // magic + version
18+
0x01,
19+
0x05,
20+
0x01,
21+
0x60,
22+
0x00,
23+
0x01,
24+
0x7f, // type: () -> i32
25+
0x02,
26+
imp.length,
27+
...imp, // import section
28+
0x03,
29+
0x02,
30+
0x01,
31+
0x00, // func: exp uses t0
32+
0x07,
33+
0x07,
34+
0x01,
35+
0x03,
36+
0x65,
37+
0x78,
38+
0x70,
39+
0x00,
40+
0x01, // export "exp" func#1
41+
0x0a,
42+
0x06,
43+
0x01,
44+
0x04,
45+
0x00,
46+
0x10,
47+
0x00,
48+
0x0b, // code: call 0; end
49+
]);
50+
};
51+
52+
// Resolvable glue import (wasm-bindgen --target bundler shape).
53+
writeFileSync(
54+
new URL('./mod.wasm', import.meta.url),
55+
wasmImporting('./glue.js', 'imp'),
56+
);
57+
// Synthetic, non-resolvable import (emscripten/wasi shape).
58+
writeFileSync(
59+
new URL('./envmod.wasm', import.meta.url),
60+
wasmImporting('env', 'imp'),
61+
);
62+
// Non-relative but resolvable import module: reaches its glue through a
63+
// `resolve.alias` entry rather than a `./` path (see ../index.test.ts).
64+
writeFileSync(
65+
new URL('./aliasmod.wasm', import.meta.url),
66+
wasmImporting('@e2e/wasm-glue', 'imp'),
67+
);

0 commit comments

Comments
 (0)