Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/pack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@hono/node-server": "^1.19.11",
"@hono/node-ws": "^1.3.0",
"@jridgewell/trace-mapping": "^0.3.31",
"@swc/helpers": "0.5.15",
"@swc/helpers": "0.5.23",
"@utoo/pack-shared": "*",
"browserslist": "^4.28.5",
"domparser-rs": "^0.0.7",
Expand Down
89 changes: 89 additions & 0 deletions packages/pack/src/__test__/swcHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { spawn } from "child_process";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { describe, expect, it } from "vitest";

const testDir = path.dirname(fileURLToPath(import.meta.url));
const packageRoot = path.resolve(testDir, "../..");
const repoRoot = path.resolve(packageRoot, "../..");
const resultPrefix = "__SWC_HELPERS__";

// Keep native worker threads and shutdown isolated from the Vitest process.
function runFixture(): Promise<unknown> {
const targetDir = path.join(repoRoot, "target");
fs.mkdirSync(targetDir, { recursive: true });
const projectPath = fs.mkdtempSync(path.join(targetDir, "swc-helpers-"));

return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[
path.join(repoRoot, "node_modules/vite-node/vite-node.mjs"),
path.join(testDir, "swcHelpersChild.ts"),
projectPath,
],
{
cwd: packageRoot,
env: {
...process.env,
NODE_PATH: [
path.join(packageRoot, "node_modules"),
path.join(repoRoot, "node_modules"),
process.env.NODE_PATH,
]
.filter(Boolean)
.join(path.delimiter),
},
stdio: ["ignore", "pipe", "pipe"],
},
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
const timeout = setTimeout(() => child.kill("SIGKILL"), 50_000);
const cleanup = () => {
clearTimeout(timeout);
fs.rmSync(projectPath, { recursive: true, force: true });
};
child.on("error", (error) => {
cleanup();
reject(error);
});
child.on("close", (code, signal) => {
try {
if (code !== 0) {
throw new Error(
`SWC helpers fixture failed (${code}, ${signal})\n${stderr}\n${stdout}`,
);
}
const line = stdout
.split(/\r?\n/)
.find((item) => item.startsWith(resultPrefix));
if (!line)
throw new Error(`Missing fixture result\n${stderr}\n${stdout}`);
resolve(JSON.parse(line.slice(resultPrefix.length)));
} catch (error) {
reject(error);
} finally {
cleanup();
}
});
});
}

describe("SWC runtime helpers", () => {
it("preserves named RegExp groups when compiling for older browsers", async () => {
await expect(runFixture()).resolves.toEqual({
word: "hello",
matched: "hello-hello",
rejectsDifferentWord: true,
replacement: "[hello]",
});
}, 60_000);
});
97 changes: 97 additions & 0 deletions packages/pack/src/__test__/swcHelpersChild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";

const projectPath = process.argv[2];
if (!projectPath || !path.isAbsolute(projectPath)) {
throw new Error("An absolute fixture directory is required");
}
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../../..",
);

type RegExpResult = {
word: string;
matched: string;
rejectsDifferentWord: boolean;
replacement: string;
};

async function main() {
// The override belongs to pack's addon only. HtmlPlugin's domparser-rs
// dependency must load its own native library.
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
await import("../binding");
delete process.env.NAPI_RS_NATIVE_LIBRARY_PATH;
}
const { build } = await import("../commands/build");
const sourceDir = path.join(projectPath, "src");
const outputDir = path.join(projectPath, "dist");
fs.mkdirSync(sourceDir, { recursive: true });
fs.writeFileSync(
path.join(sourceDir, "regexp-dependency.js"),
`export function inspectRegExp(value) {
const pattern = /^(?<word>[a-z]+)-\\k<word>$/;
const match = pattern.exec(value);
return {
word: match.groups.word,
matched: match[0],
rejectsDifferentWord: !pattern.test("hello-world"),
replacement: value.replace(pattern, "[$<word>]"),
};
}
`,
);
fs.writeFileSync(
path.join(sourceDir, "index.js"),
'export { inspectRegExp } from "./regexp-dependency.js";\n',
);

await build(
{
buildId: "swc-helpers-test",
tracing: false,
config: {
mode: "production",
target: "chrome 49",
entry: [
{
name: "index",
import: "./src/index.js",
library: { name: "SwcHelpersRegression" },
},
],
output: { path: outputDir, filename: "index.js", clean: true },
sourceMaps: false,
persistentCaching: false,
optimization: { minify: false },
},
},
projectPath,
repoRoot,
);

const filename = path.join(outputDir, "index.js");
const body = fs.readFileSync(filename, "utf8");
// Modern Node supports the original syntax. Require an emitted helper so
// native RegExp support cannot hide a missing compatibility transform.
assert.ok(body.includes("_wrap_reg_exp"), "Missing emitted RegExp helper");
const context: {
URL: typeof URL;
self?: unknown;
SwcHelpersRegression?: { inspectRegExp: (value: string) => RegExpResult };
} = { URL };
context.self = context;
vm.runInNewContext(body, context, { filename, timeout: 1000 });
assert.ok(context.SwcHelpersRegression, "Missing library exports");
const result = context.SwcHelpersRegression.inspectRegExp("hello-hello");
console.log(`__SWC_HELPERS__${JSON.stringify(result)}`);
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
Loading