diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 459727f7..bbc98f3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,7 @@ jobs: - run: pnpm lint - run: pnpm test:types - run: pnpm test:node + - run: pnpm test:workerd - run: pnpm build - name: nightly release if: | diff --git a/package.json b/package.json index 6e1878b0..72c543a8 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,12 @@ "test": "pnpm lint && pnpm test:types && pnpm test:node", "test:cf": "pnpm jiti test/cloudflare.ts", "test:deno": "NODE_NO_WARNINGS=1 pnpm jiti test/deno.ts", - "test:node-coverage": "node test/node-coverage.mjs", "test:node": "node --test --import jiti/register ./test/node/test-*", + "test:node-coverage": "node test/node-coverage.mjs", "test:node:watch": "node --test --watch --import jiti/register ./test/node/test-*", "test:types": "tsc --noEmit", - "test:vc": "pnpm jiti test/vercel.ts" + "test:vc": "pnpm jiti test/vercel.ts", + "test:workerd": "node test/workerd/main.mjs" }, "dependencies": { "defu": "^6.1.4", @@ -47,16 +48,19 @@ "ufo": "^1.5.4" }, "devDependencies": { + "@parcel/watcher": "^2.4.1", "@types/node": "^22.7.4", "automd": "^0.3.10", "changelogen": "^0.5.7", "consola": "^3.2.3", + "esbuild": "^0.24.0", "eslint": "^9.12.0", "eslint-config-unjs": "^0.4.1", "jiti": "^2.3.3", "prettier": "^3.3.3", "typescript": "^5.6.2", "unbuild": "^2.0.0", + "workerd": "^1.20241004.0", "wrangler": "^3.80.0" }, "packageManager": "pnpm@9.12.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9e727c4..c34ec573 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: specifier: ^1.5.4 version: 1.5.4 devDependencies: + '@parcel/watcher': + specifier: ^2.4.1 + version: 2.4.1 '@types/node': specifier: ^22.7.4 version: 22.7.4 @@ -33,6 +36,9 @@ importers: consola: specifier: ^3.2.3 version: 3.2.3 + esbuild: + specifier: ^0.24.0 + version: 0.24.0 eslint: specifier: ^9.12.0 version: 9.12.0(jiti@2.3.3) @@ -51,6 +57,9 @@ importers: unbuild: specifier: ^2.0.0 version: 2.0.0(typescript@5.6.2) + workerd: + specifier: ^1.20241004.0 + version: 1.20241004.0 wrangler: specifier: ^3.80.0 version: 3.80.0 @@ -150,30 +159,60 @@ packages: cpu: [x64] os: [darwin] + '@cloudflare/workerd-darwin-64@1.20241004.0': + resolution: {integrity: sha512-c2afR486NXDRcPm7RaTSRDnffFklPCXde/IeNVhEhBJ8O+pQhBOdDcGIy8zXPwMu0CYga0iHNZmpbsl+ZcHttA==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20240925.0': resolution: {integrity: sha512-MiQ6uUmCXjsXgWNV+Ock2tp2/tYqNJGzjuaH6jFioeRF+//mz7Tv7J7EczOL4zq+TH8QFOh0/PUsLyazIWVGng==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20241004.0': + resolution: {integrity: sha512-siD9fexv5lr2IpBczWV7OPgJvHj8/fJUrRAYCMcBURkfiwssK91coQeZlN1NdQ85aYELVgxDFoG+p86OS+ZzLw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + '@cloudflare/workerd-linux-64@1.20240925.0': resolution: {integrity: sha512-Rjix8jsJMfsInmq3Hm3fmiRQ+rwzuWRPV1pg/OWhMSfNP7Qp2RCU+RGkhgeR9Z5eNAje0Sn2BMrFq4RvF9/yRA==} engines: {node: '>=16'} cpu: [x64] os: [linux] + '@cloudflare/workerd-linux-64@1.20241004.0': + resolution: {integrity: sha512-EtKGXO5fzRgX6UhDDLhjjEsB1QtliHb12zavZ/S0C8hKPz76II7MQ3Lls9kfB62fbdMP8L6vcqWPObEUcw6GSw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + '@cloudflare/workerd-linux-arm64@1.20240925.0': resolution: {integrity: sha512-VYIPeMHQRtbwQoIjUwS/zULlywPxyDvo46XkTpIW5MScEChfqHvAYviQ7TzYGx6Q+gmZmN+DUB2KOMx+MEpCxA==} engines: {node: '>=16'} cpu: [arm64] os: [linux] + '@cloudflare/workerd-linux-arm64@1.20241004.0': + resolution: {integrity: sha512-XO7VBE1YaFf/o9tKO1PqDqaxkU2eAR2DLX7R0+R8p+q92sUDXyoxo48T3yJDfxWndnKJ6hSJfvKanw3Mq9Tisw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + '@cloudflare/workerd-windows-64@1.20240925.0': resolution: {integrity: sha512-C8peGvaU5R51bIySi1VbyfRgwNSSRknqoFSnSbSBI3uTN3THTB3UnmRKy7GXJDmyjgXuT9Pcs1IgaWNubLtNtw==} engines: {node: '>=16'} cpu: [x64] os: [win32] + '@cloudflare/workerd-windows-64@1.20241004.0': + resolution: {integrity: sha512-o+TmCYGq58jNUDbG73xOvd648XvJ2TicI++2BBoySklJXG6f4But5AwA8TxQgmeujR3vpBjPZKexEzcZSUOTtA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@cloudflare/workers-shared@0.5.4': resolution: {integrity: sha512-PNL/0TjKRdUHa1kwgVdqUNJVZ9ez4kacsi8omz+gv859EvJmsVuGiMAClY2YfJnC9LVKhKCcjqmFgKNXG9/IXA==} engines: {node: '>=16.7.0'} @@ -2423,6 +2462,11 @@ packages: engines: {node: '>=16'} hasBin: true + workerd@1.20241004.0: + resolution: {integrity: sha512-TCFJ7Zw7svR3adg1fnlPWj/yXhjBnQloLEIJqdu57hli/GsgwlbomwrbM3mdMgbS+K9zYeaYqknXiBN0EXk3QQ==} + engines: {node: '>=16'} + hasBin: true + wrangler@3.80.0: resolution: {integrity: sha512-c9aN7Buf7XgTPpQkw1d0VjNRI4qg3m35PTg70Tg4mOeHwHGjsd74PAsP1G8BjpdqDjfWtsua7tj1g4M3/5dYNQ==} engines: {node: '>=16.17.0'} @@ -2604,18 +2648,33 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20240925.0': optional: true + '@cloudflare/workerd-darwin-64@1.20241004.0': + optional: true + '@cloudflare/workerd-darwin-arm64@1.20240925.0': optional: true + '@cloudflare/workerd-darwin-arm64@1.20241004.0': + optional: true + '@cloudflare/workerd-linux-64@1.20240925.0': optional: true + '@cloudflare/workerd-linux-64@1.20241004.0': + optional: true + '@cloudflare/workerd-linux-arm64@1.20240925.0': optional: true + '@cloudflare/workerd-linux-arm64@1.20241004.0': + optional: true + '@cloudflare/workerd-windows-64@1.20240925.0': optional: true + '@cloudflare/workerd-windows-64@1.20241004.0': + optional: true + '@cloudflare/workers-shared@0.5.4': dependencies: mime: 3.0.0 @@ -4768,6 +4827,14 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20240925.0 '@cloudflare/workerd-windows-64': 1.20240925.0 + workerd@1.20241004.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20241004.0 + '@cloudflare/workerd-darwin-arm64': 1.20241004.0 + '@cloudflare/workerd-linux-64': 1.20241004.0 + '@cloudflare/workerd-linux-arm64': 1.20241004.0 + '@cloudflare/workerd-windows-64': 1.20241004.0 + wrangler@3.80.0: dependencies: '@cloudflare/kv-asset-handler': 0.3.4 diff --git a/test/workerd/config.capnp b/test/workerd/config.capnp new file mode 100644 index 00000000..f6030bd9 --- /dev/null +++ b/test/workerd/config.capnp @@ -0,0 +1,16 @@ +using Workerd = import "../../node_modules/workerd/workerd.capnp"; + +const unitTests :Workerd.Config = ( + services = [ + (name = "tests", worker = .testsWorker), + ], +); + +const testsWorker :Workerd.Worker = ( + modules = [ + (name = "tests", esModule = embed "./tests.mjs") + ], + compatibilityDate = "2024-09-01", + compatibilityFlags = ["nodejs_compat"], + moduleFallback = "localhost:8888", +); diff --git a/test/workerd/main.mjs b/test/workerd/main.mjs new file mode 100644 index 00000000..3ca86848 --- /dev/null +++ b/test/workerd/main.mjs @@ -0,0 +1,195 @@ +import { dirname, join } from "node:path"; +import { existsSync } from "node:fs"; +import { readFile, mkdir, writeFile } from "node:fs/promises"; +import { spawn } from "node:child_process"; +import { createServer } from "node:http"; +import { fileURLToPath } from "node:url"; +import workerd from "workerd"; + +// CLI args +const watchMode = process.argv.includes("--watch"); + +// Dirs +export const testsDir = fileURLToPath(new URL(".", import.meta.url)); +export const rootDir = fileURLToPath(new URL("../..", import.meta.url)); +export const srcDir = join(rootDir, "src"); + +/** + * Test runner main + */ +async function main() { + // Print info + console.log( + `Workerd: ${workerd.version} (compatibility date: ${workerd.compatibilityDate})`, + ); + + // Start module server + const server = await createModuleServer(8888); + server.unref(); + + // Run tests once + if (runTests() === false) { + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); + } + + // Start watcher + if (watchMode) { + const watcher = await import("@parcel/watcher").then((r) => r.default); + const watchDirs = [srcDir, testsDir]; + console.log( + `Watching for changes:\n${watchDirs.map((d) => ` - ${d}`).join("\n")}`, + ); + for (const dir of watchDirs) { + watcher.subscribe( + dir, + () => { + console.clear(); + runTests(); + }, + { ignore: [".tmp"] }, + ); + } + } +} + +/** + * Spawn workerd to run tests + */ +function runTests() { + try { + if (runTests.proc) { + runTests.proc.kill(); + runTests.proc = undefined; + } + console.log(`Running tests...`); + const workerdBin = workerd.default; + runTests.proc = spawn( + workerdBin, + ["test", "--experimental", "config.capnp"], + { + cwd: testsDir, + stdio: "inherit", + env: { + ...process.env, + LLVM_SYMBOLIZER: findLLVMsymbolizer(), + }, + }, + ); + } catch (error) { + if (error) { + console.error(error.stdout || error); + } + return false; + } +} + +/** + * Try to llvm-symbolizer binary in common locations + */ +function findLLVMsymbolizer() { + if (process.env.LLVM_SYMBOLIZER) { + return process.env.LLVM_SYMBOLIZER; + } + const paths = [ + "/opt/homebrew/opt/llvm/bin/llvm-symbolizer", + "/usr/bin/llvm-symbolizer", + ]; + for (const path of paths) { + if (existsSync(path)) { + return path; + } + } + return "llvm-symbolizer"; +} + +/** + * Create fallback module server + * + * Reference: + * https://github.com/cloudflare/workerd/pull/1423 + * https://github.com/cloudflare/workerd/tree/main/samples/module_fallback + */ +async function createModuleServer(port = 8888) { + // Unenv preset + const { createJiti } = await import("jiti"); + const jiti = createJiti(import.meta.url); + const unenv = await jiti.import("../../src/index.ts"); + const preset = unenv.env(unenv.nodeless, unenv.cloudflare); + const alias = Object.fromEntries( + Object.entries(preset.alias).map(([k, v]) => [ + k, + v.replace("unenv/runtime", join(srcDir, "runtime")), + ]), + ); + + // Use esbuild to bundle + const { build } = await import("esbuild"); + + const server = createServer(async (req, res) => { + try { + const resolveMethod = req.headers["x-resolve-method"]; + const url = new URL(req.url, "http://localhost"); + const referrer = url.searchParams.get("referrer"); + const specifier = url.searchParams.get("specifier"); + const rawSpecifier = url.searchParams.get("rawSpecifier"); + + console.log( + `[server] ${rawSpecifier} ${referrer ? `from ${referrer}` : ""}`, + ); + + // unenv/runtime/* + const unenvPath = /^unenv\/runtime\/(.*)$/.exec(rawSpecifier)?.[1]; + if (!unenvPath) { + res.writeHead(404); + return res.end(); + } + + // Load node module + // prettier-ignore + const entryDir = join(srcDir, "runtime", unenvPath); + const entryFile = existsSync(join(entryDir, "$cloudflare.ts")) + ? join(entryDir, "$cloudflare.ts") + : join(entryDir, "index.ts"); + + const transpiled = await build({ + entryPoints: [entryFile], + banner: { + js: `/*\n * Raw specifier: ${rawSpecifier}\n * Source: ${entryFile}\n */\n`, + }, + bundle: true, + write: false, + format: "esm", + target: "esnext", + platform: "node", + sourcemap: "inline", + alias, + }); + + const esModule = transpiled.outputFiles[0].text; + + if (process.env.DUMP_MODULES) { + const dumpPath = join(testsDir, ".tmp", rawSpecifier + ".mjs"); + await mkdir(dirname(dumpPath), { recursive: true }); + await writeFile(dumpPath, esModule, "utf8"); + } + + res.end(JSON.stringify({ esModule })); + } catch (error) { + console.error("[server]", error); + res.writeHead(500); + res.end(); + } + }); + + return new Promise((resolve) => { + server.listen({ port, host: "localhost" }, () => { + console.log( + `Module fallback server listening on http://localhost:${port}`, + ); + resolve(server); + }); + }); +} + +await main(); diff --git a/test/workerd/tests.mjs b/test/workerd/tests.mjs new file mode 100644 index 00000000..61aaf110 --- /dev/null +++ b/test/workerd/tests.mjs @@ -0,0 +1,32 @@ +import assert from "node:assert"; +import process from "node:process"; + +globalThis.process = process; + +// ---- node:crypto ---- + +export const crypto_getRandomValues = { + async test() { + const crypto = await import("unenv/runtime/node/crypto"); + + const array = new Uint32Array(10); + crypto.getRandomValues(array); + assert.strictEqual(array.length, 10); + assert(array.every((v) => v >= 0 && v <= 0xff_ff_ff_ff)); + }, +}; + +// ---- node:url ---- + +export const url_parse = { + async test() { + const url = await import("unenv/runtime/node/url"); + + assert.throws( + () => { + url.parse("http://[127.0.0.1\u0000c8763]:8000/"); + }, + { code: "ERR_INVALID_URL", input: "http://[127.0.0.1\u0000c8763]:8000/" }, + ); + }, +};