Skip to content

Commit c5904e3

Browse files
Use real http server for browser testing (#506)
1 parent 4f7be4f commit c5904e3

File tree

5 files changed

+64
-23
lines changed

5 files changed

+64
-23
lines changed

Sources/CartonHelpers/StaticArchive.swift

Lines changed: 3 additions & 3 deletions
Large diffs are not rendered by default.

Sources/carton-frontend-slim/BundleLayout.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ struct BundleLayout {
8181
<body>
8282
<script type="module">
8383
import { testBrowser } from "./index.js";
84-
testBrowser([], true);
84+
testBrowser([], {}, true);
8585
</script>
8686
</body>
8787
@@ -209,8 +209,8 @@ struct BundleLayout {
209209
return internalInstantiate(options, imports);
210210
}
211211
212-
export async function testBrowser(args, inPage = false) {
213-
await internalTestBrowser(instantiate, wasmFileName, args, import.meta.url, inPage);
212+
export async function testBrowser(args, options, inPage = false) {
213+
await internalTestBrowser(instantiate, wasmFileName, args, import.meta.url, options, inPage);
214214
}
215215
216216
export async function testNode(args) {

Sources/carton-release/HashArchive.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ struct HashArchive: AsyncParsableCommand {
5050
let arguments = [
5151
"esbuild", "--bundle", "entrypoint/\(tsFilename)", "--outfile=static/\(filename)",
5252
"--external:node:url", "--external:node:path",
53-
"--external:node:module", "--external:node:fs/promises",
53+
"--external:node:module", "--external:node:http",
54+
"--external:node:fs/promises", "--external:node:fs",
5455
"--external:playwright",
5556
"--format=esm",
5657
"--external:./JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs",

entrypoint/JavaScriptKit_JavaScriptKit.resources/Runtime/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export class SwiftRuntime {
2+
constructor({ sharedMemory: booleam });
23
setInstance(instance: WebAssembly.Instance): void;
34
main?(): void;
45
readonly wasmImports: ImportedFunctions;

entrypoint/intrinsics.ts

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory, WASIProcExit, In
1616
import type { SwiftRuntime, SwiftRuntimeConstructor } from "./JavaScriptKit_JavaScriptKit.resources/Runtime/index";
1717
import { polyfill as polyfillWebAssemblyTypeReflection } from "wasm-imports-parser/polyfill";
1818
import type { ImportEntry } from "wasm-imports-parser";
19+
import { AddressInfo } from "node:net";
1920

2021
// Apply polyfill for WebAssembly Type Reflection JS API to inspect imported memory info.
2122
// https://github.com/WebAssembly/js-types/blob/main/proposals/js-types/Overview.md
@@ -65,7 +66,14 @@ export async function instantiate(rawOptions: InstantiationOptions, extraWasmImp
6566

6667
let swift: SwiftRuntime | undefined = options.swift;
6768
if (!swift && options.SwiftRuntime) {
68-
swift = new options.SwiftRuntime();
69+
let sharedMemory = false;
70+
for (const importEntry of WebAssembly.Module.imports(options.module)) {
71+
if (importEntry.module === "env" && importEntry.name === "memory" && importEntry.kind === "memory") {
72+
sharedMemory = true;
73+
break;
74+
}
75+
}
76+
swift = new options.SwiftRuntime({ sharedMemory });
6977
}
7078

7179
let stdoutLine: LineDecoder | undefined = undefined;
@@ -242,10 +250,54 @@ async function extractAndSaveFile(rootFs: Map<string, Inode>, path: string): Pro
242250
return false;
243251
}
244252

245-
export async function testBrowser(instantiate: Instantiate, wasmFileName: string, args: string[], indexJsUrl: string, inPage: boolean) {
253+
export async function testBrowser(
254+
instantiate: Instantiate,
255+
wasmFileName: string,
256+
args: string[],
257+
indexJsUrl: string,
258+
options: { contentTypes?: (fileName: string) => string } = {},
259+
inPage: boolean = false
260+
) {
246261
if (inPage) {
247262
return await testBrowserInPage(instantiate, wasmFileName, args);
248263
}
264+
265+
const { fileURLToPath } = await import("node:url");
266+
const path = await import("node:path");
267+
const fs = await import("node:fs/promises");
268+
const { existsSync } = await import("node:fs");
269+
const indexJsPath = fileURLToPath(indexJsUrl);
270+
const webRoot = path.dirname(indexJsPath);
271+
272+
const http = await import("node:http");
273+
const defaultContentTypes: Record<string, string> = {
274+
".html": "text/html",
275+
".js": "text/javascript",
276+
".mjs": "text/javascript",
277+
".wasm": "application/wasm",
278+
};
279+
const server = http.createServer(async (req, res) => {
280+
const url = new URL(req.url!, `http://${req.headers.host}`);
281+
const pathname = url.pathname;
282+
const filePath = path.join(webRoot, pathname);
283+
if (existsSync(filePath) && (await fs.stat(filePath)).isFile()) {
284+
const data = await fs.readFile(filePath);
285+
const ext = pathname.slice(pathname.lastIndexOf("."));
286+
const contentType = options.contentTypes?.(pathname) || defaultContentTypes[ext] || "text/plain";
287+
res.writeHead(200, { "Content-Type": contentType });
288+
res.end(data);
289+
} else if (pathname === "/process-info.json") {
290+
res.writeHead(200, { "Content-Type": "application/json" });
291+
res.end(JSON.stringify({ env: process.env }));
292+
} else {
293+
res.writeHead(404);
294+
res.end();
295+
}
296+
});
297+
298+
await new Promise<void>((resolve) => server.listen({ host: "localhost", port: 0 }, () => resolve()));
299+
const address = server.address() as AddressInfo;
300+
249301
const playwright = await (async () => {
250302
try {
251303
// @ts-ignore
@@ -263,29 +315,16 @@ Please run the following command to install it:
263315
const browser = await playwright.chromium.launch();
264316
const context = await browser.newContext();
265317
const page = await context.newPage();
266-
const { fileURLToPath } = await import("node:url");
267-
const path = await import("node:path");
268-
const indexJsPath = fileURLToPath(indexJsUrl);
269-
const webRoot = path.dirname(indexJsPath);
270318

271319
// Forward console messages in the page to the Node.js console
272320
page.on("console", (message: any) => {
273321
console.log(message.text());
274322
});
275323

276-
await page.route("http://example.com/**/*", async (route: any) => {
277-
const url = route.request().url();
278-
const urlPath = new URL(url).pathname;
279-
if (urlPath === "/process-info.json") {
280-
route.fulfill({ body: JSON.stringify({ env: process.env }) });
281-
return;
282-
}
283-
route.fulfill({ path: path.join(webRoot, urlPath.slice(1)) });
284-
});
285324
const onExit = new Promise<number>((resolve) => {
286325
page.exposeFunction("exitTest", resolve);
287326
});
288-
await page.goto("http://example.com/test.browser.html");
327+
await page.goto(`http://localhost:${address.port}/test.browser.html`);
289328
const exitCode = await onExit;
290329
await browser.close();
291330
process.exit(exitCode);

0 commit comments

Comments
 (0)