Skip to content

Commit a1cac7d

Browse files
committed
presets: update winterjs
1 parent 23c93b0 commit a1cac7d

6 files changed

Lines changed: 135 additions & 165 deletions

File tree

‎src/build/rolldown/config.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ export const getRolldownConfig = async (nitro: Nitro): Promise<RolldownOptions>
7272

7373
const outputConfig = config.output as OutputOptions;
7474
if (outputConfig.inlineDynamicImports || outputConfig.format === "iife") {
75-
delete outputConfig.inlineDynamicImports;
7675
outputConfig.codeSplitting = false;
7776
}
77+
// `inlineDynamicImports` is deprecated in favor of `codeSplitting: false`
78+
delete outputConfig.inlineDynamicImports;
7879

7980
return config as RolldownOptions;
8081
};

‎src/presets/winterjs/preset.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
1+
import { builtinModules } from "node:module";
12
import { defineNitroPreset } from "../_utils/preset.ts";
3+
import type { Nitro, RollupConfig } from "nitro/types";
4+
5+
const NODE_BUILTINS = new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)]);
26

37
const winterjs = defineNitroPreset(
48
{
59
extends: "base-worker",
610
entry: "./winterjs/runtime/winterjs",
7-
minify: false,
811
serveStatic: "inline",
912
wasm: {
1013
lazy: true,
1114
},
15+
hooks: {
16+
"rollup:before": (_nitro: Nitro, config: RollupConfig) => {
17+
// WinterJS cannot parse `import.meta`. The entry banner injected by
18+
// `nitro:server-main` is only read back by the Node.js public asset
19+
// reader, which this preset never uses (`serveStatic: "inline"`).
20+
config.plugins = (config.plugins as { name?: string }[]).filter(
21+
(plugin) => plugin?.name !== "nitro:server-main"
22+
) as RollupConfig["plugins"];
23+
24+
// WinterJS has no `node:*` modules, so they must resolve to their unenv
25+
// aliases. Left external, an IIFE bundle turns them into undefined globals.
26+
if (Array.isArray(config.external)) {
27+
config.external = config.external.filter(
28+
(id) => typeof id !== "string" || !NODE_BUILTINS.has(id)
29+
);
30+
}
31+
},
32+
},
1233
commands: {
1334
preview:
1435
"wasmer run wasmer/winterjs --forward-host-env --net --mapdir app:./ app/server/index.mjs",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// WinterJS (v1.1.5) implements the WinterCG API surface, except that its `Response`
2+
// constructor rejects a `null` body and a nullish `status`, both valid per spec.
3+
// https://github.com/wasmerio/winterjs
4+
const NativeResponse = globalThis.Response;
5+
6+
globalThis.Response = class Response extends NativeResponse {
7+
constructor(body?: BodyInit | null, init?: ResponseInit) {
8+
super(body === null ? undefined : body, init?.status == null ? { ...init, status: 200 } : init);
9+
}
10+
} as typeof globalThis.Response;
Lines changed: 9 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,15 @@
1-
// @ts-nocheck TODO: Remove after removing polyfills
21
import "#nitro/virtual/polyfills";
3-
import { useNitroApp } from "nitro/app";
4-
import { hasProtocol, joinURL } from "ufo";
5-
6-
// Types are reverse engineered from runtime
2+
import "./_polyfill.ts";
73

8-
interface ExecuteRequest {
9-
url: URL;
10-
body?: ReadableStream;
11-
bodyUsed: boolean;
12-
headers: Headers;
13-
getMethod: () => string;
14-
}
15-
16-
interface FetchEvent extends Event {
17-
request: ExecuteRequest;
18-
respondWith(response: Promise<Response> | Response): Promise<Response>;
19-
waitUntil: (promise: Promise<any>) => void;
20-
}
4+
import type { ServerRequest, ServiceWorkerFetchEvent } from "srvx";
5+
import { useNitroApp } from "nitro/app";
216

227
const nitroApp = useNitroApp();
238

24-
// Use plain handler as winterjs Web API is incomplete
25-
// TODO: Migrate to toWebHandler
26-
const _handler = toPlainHandler(nitroApp.h3App);
27-
28-
async function _handleEvent(event: FetchEvent) {
29-
try {
30-
const res = await _handler({
31-
path:
32-
event.request.url.pathname +
33-
(event.request.url.search ? `?${event.request.url.search}` : ""),
34-
method: event.request.getMethod() || "GET",
35-
body: event.request.body,
36-
headers: event.request.headers,
37-
context: {
38-
waitUntil: (promise: Promise<any>) => event.waitUntil(promise),
39-
_platform: {
40-
winterjs: {
41-
event,
42-
},
43-
},
44-
},
45-
});
46-
const body = typeof res.body === "string" ? res.body : await toBuffer(res.body as any);
47-
return new Response(body, {
48-
status: res.status,
49-
statusText: res.statusText,
50-
headers: res.headers,
51-
});
52-
} catch (error: unknown) {
53-
const errString = (error as Error)?.message + "\n" + (error as Error)?.stack;
54-
console.error(errString);
55-
return new Response(errString, { status: 500 });
56-
}
57-
}
58-
59-
addEventListener("fetch" as any, async (event: FetchEvent) => {
60-
event.respondWith(await _handleEvent(event));
9+
// WinterJS runs service worker scripts: https://github.com/wasmerio/winterjs
10+
addEventListener("fetch" as any, (event: ServiceWorkerFetchEvent) => {
11+
const request = event.request as ServerRequest;
12+
request.runtime ??= { name: "winterjs", serviceWorker: { event } };
13+
request.waitUntil = (promise) => event.waitUntil(promise as Promise<unknown>);
14+
event.respondWith(nitroApp.fetch(request));
6115
});
62-
63-
// ------------------------------
64-
// Polyfills for missing APIs
65-
// ------------------------------
66-
67-
function toBuffer(data: ReadableStream): Promise<Buffer> {
68-
return new Promise<Buffer>((resolve, reject) => {
69-
const chunks: Buffer[] = [];
70-
data
71-
.pipeTo(
72-
new WritableStream({
73-
write(chunk) {
74-
chunks.push(chunk);
75-
},
76-
close() {
77-
resolve(Buffer.concat(chunks));
78-
},
79-
abort(reason) {
80-
reject(reason);
81-
},
82-
})
83-
)
84-
.catch(reject);
85-
});
86-
}
87-
88-
// Headers.entries
89-
if (!Headers.prototype.entries) {
90-
// @ts-ignore
91-
Headers.prototype.entries = function () {
92-
return [...this];
93-
};
94-
}
95-
96-
// URL.pathname
97-
if (!URL.prototype.pathname) {
98-
Object.defineProperty(URL.prototype, "pathname", {
99-
get() {
100-
return this.path || "/";
101-
},
102-
});
103-
}
104-
105-
// URL constructor (relative support)
106-
const _URL = globalThis.URL;
107-
globalThis.URL = class URL extends _URL {
108-
constructor(url: string | URL, base: string | URL) {
109-
if (!base || hasProtocol(url)) {
110-
super(url);
111-
return;
112-
}
113-
super(joinURL(base, url));
114-
}
115-
};
116-
117-
// Response (avoid Promise body)
118-
const _Response = globalThis.Response;
119-
globalThis.Response = class Response extends _Response {
120-
_body: BodyInit;
121-
122-
constructor(body, init) {
123-
super(body, init);
124-
this._body = body;
125-
}
126-
127-
get body() {
128-
// TODO: Return ReadableStream (should be iterable)
129-
return this._body as any;
130-
}
131-
};

‎test/presets/winterjs.test.ts‎

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import { execa } from "execa";
1+
import { existsSync } from "node:fs";
2+
import { mkdir, rm, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { execa, execaSync } from "execa";
25
import { getRandomPort, waitForPort } from "get-port-please";
6+
import { join } from "pathe";
37
import { describe } from "vitest";
48
import { setupTest, testNitro } from "../tests.ts";
59

6-
const hasWasmer = false;
7-
// execaCommandSync("wasmer --version", { stdio: "ignore", reject: false })
8-
// .exitCode === 0;
10+
const wasmer = await resolveWasmer();
911

10-
describe.runIf(hasWasmer)("nitro:preset:winterjs", async () => {
12+
describe.runIf(wasmer)("nitro:preset:winterjs", async () => {
1113
const ctx = await setupTest("winterjs");
1214
testNitro(ctx, async () => {
1315
const port = await getRandomPort();
1416
const p = execa(
15-
"wasmer",
17+
wasmer!,
1618
[
1719
"run",
1820
"wasmer/winterjs",
@@ -23,10 +25,10 @@ describe.runIf(hasWasmer)("nitro:preset:winterjs", async () => {
2325
"app/server/index.mjs",
2426
],
2527
{
26-
stdio: "inherit",
28+
stdio: process.env.TEST_DEBUG ? "inherit" : "ignore",
29+
reject: false,
2730
env: {
28-
// ...ctx.env,
29-
RUST_BACKTRACE: "full",
31+
...ctx.env,
3032
LISTEN_IP: "127.0.0.1",
3133
PORT: String(port),
3234
},
@@ -36,10 +38,51 @@ describe.runIf(hasWasmer)("nitro:preset:winterjs", async () => {
3638
url: `http://127.0.0.1:${port}`,
3739
close: () => p.kill(),
3840
} as any;
39-
await waitForPort(port, { delay: 1000, retries: 20, host: "127.0.0.1" });
41+
await waitForPort(port, { delay: 1000, retries: 30, host: "127.0.0.1" });
4042
return async ({ url, ...opts }) => {
4143
const res = await ctx.fetch(url, opts);
4244
return res;
4345
};
4446
});
4547
});
48+
49+
// Resolves the `wasmer` runner used to run WinterJS. Falls back to downloading a
50+
// release into a cached temporary dir when `DOWNLOAD_WASMER` is set (opt-in, as
51+
// it needs network access), otherwise the suite is skipped.
52+
async function resolveWasmer(): Promise<string | undefined> {
53+
if (execaSync("wasmer", ["--version"], { stdio: "ignore", reject: false }).exitCode === 0) {
54+
return "wasmer";
55+
}
56+
if (!process.env.DOWNLOAD_WASMER) {
57+
return undefined;
58+
}
59+
60+
const dir = join(tmpdir(), "nitro-tests-wasmer");
61+
const bin = join(dir, "bin/wasmer");
62+
if (existsSync(bin)) {
63+
return bin;
64+
}
65+
66+
const os = { linux: "linux", darwin: "darwin" }[process.platform as string];
67+
const arch = { x64: "amd64", arm64: process.platform === "darwin" ? "arm64" : "aarch64" }[
68+
process.arch as string
69+
];
70+
if (!os || !arch) {
71+
throw new Error(`DOWNLOAD_WASMER: unsupported platform (${process.platform}-${process.arch})`);
72+
}
73+
74+
const url = `https://github.com/wasmerio/wasmer/releases/latest/download/wasmer-${os}-${arch}.tar.gz`;
75+
console.log(`[winterjs] downloading ${url}`);
76+
const res = await fetch(url);
77+
if (!res.ok) {
78+
throw new Error(`DOWNLOAD_WASMER: ${url} responded with ${res.status}`);
79+
}
80+
81+
const archive = join(dir, "wasmer.tar.gz");
82+
await mkdir(dir, { recursive: true });
83+
await writeFile(archive, new Uint8Array(await res.arrayBuffer()));
84+
await execa("tar", ["-xzf", archive, "-C", dir]);
85+
await rm(archive);
86+
87+
return bin;
88+
}

0 commit comments

Comments
 (0)