From 4092b271cdd6fabd888a1ee4dd9e1c008c4331aa Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 15 Jan 2025 04:08:04 -0800 Subject: [PATCH] Add test for Bun.serve static with html (#16413) --- packages/bun-types/ambient.d.ts | 6 + packages/bun-types/bun.d.ts | 15 + src/js/node/domain.ts | 6 +- test/js/bun/http/bun-serve-html.test.ts | 274 +++++++++++++++++++ test/js/bun/http/bun-serve-static-fixture.js | 34 +++ 5 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 test/js/bun/http/bun-serve-html.test.ts create mode 100644 test/js/bun/http/bun-serve-static-fixture.js diff --git a/packages/bun-types/ambient.d.ts b/packages/bun-types/ambient.d.ts index bd10bfb0d35e66..31d13cebe851c3 100644 --- a/packages/bun-types/ambient.d.ts +++ b/packages/bun-types/ambient.d.ts @@ -17,3 +17,9 @@ declare module "*/bun.lock" { var contents: import("bun").BunLockFile; export = contents; } + +declare module "*.html" { + // In Bun v1.2, we might change this to Bun.HTMLBundle + var contents: any; + export = contents; +} diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 5b2d4776468748..2fb43f806aa5c3 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -5189,6 +5189,21 @@ declare module "bun" { */ const isMainThread: boolean; + /** + * Used when importing an HTML file at runtime. + * + * @example + * + * ```ts + * import app from "./index.html"; + * ``` + * + * Bun.build support for this isn't imlpemented yet. + */ + interface HTMLBundle { + index: string; + } + interface Socket extends Disposable { /** * Write `data` to the socket diff --git a/src/js/node/domain.ts b/src/js/node/domain.ts index 6a712a0a3f50a3..2789b87792c5c0 100644 --- a/src/js/node/domain.ts +++ b/src/js/node/domain.ts @@ -1,5 +1,4 @@ -// Import Events -var EventEmitter = require("node:events"); +let EventEmitter; const { ERR_UNHANDLED_ERROR } = require("internal/errors"); const ObjectDefineProperty = Object.defineProperty; @@ -7,6 +6,9 @@ const ObjectDefineProperty = Object.defineProperty; // Export Domain var domain: any = {}; domain.createDomain = domain.create = function () { + if (!EventEmitter) { + EventEmitter = require("node:events"); + } var d = new EventEmitter(); function emitError(e) { diff --git a/test/js/bun/http/bun-serve-html.test.ts b/test/js/bun/http/bun-serve-html.test.ts new file mode 100644 index 00000000000000..87bc43095bda20 --- /dev/null +++ b/test/js/bun/http/bun-serve-html.test.ts @@ -0,0 +1,274 @@ +import { Subprocess } from "bun"; +import { test, expect } from "bun:test"; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; +import { join } from "path"; +test("serve html", async () => { + const dir = tempDirWithFiles("html-css-js", { + "dashboard.html": /*html*/ ` + + + + Dashboard + + + + + +
+

Dashboard

+

This is a separate route to test multiple pages work

+ +

+ Back to Home +
+ + + `, + "dashboard.js": /*js*/ ` + import './script.js'; + // Additional dashboard-specific code could go here + console.log("How...dashing?") + `, + "index.html": /*html*/ ` + + + + Bun HTML Import Test + + + + +
+

Hello from Bun!

+ +
+ + + `, + "script.js": /*js*/ ` + let count = 0; + const button = document.getElementById('counter'); + button.addEventListener('click', () => { + count++; + button.textContent = \`Click me: \${count}\`; + }); + `, + "styles.css": /*css*/ ` + .container { + max-width: 800px; + margin: 2rem auto; + text-align: center; + font-family: system-ui, sans-serif; + } + + button { + padding: 0.5rem 1rem; + font-size: 1.25rem; + border-radius: 0.25rem; + border: 2px solid #000; + background: #fff; + cursor: pointer; + transition: all 0.2s; + } + + button:hover { + background: #000; + color: #fff; + } + `, + }); + + const { subprocess, port, hostname } = await waitForServer(dir, { + "/": join(dir, "index.html"), + "/dashboard": join(dir, "dashboard.html"), + }); + + { + const html = await (await fetch(`http://${hostname}:${port}/`)).text(); + const trimmed = html + .trim() + .split("\n") + .map(a => a.trim()) + .filter(a => a.length > 0) + .join("\n") + .trim() + .replace(/chunk-[a-z0-9]+\.css/g, "chunk-HASH.css") + .replace(/chunk-[a-z0-9]+\.js/g, "chunk-HASH.js"); + + expect(trimmed).toMatchInlineSnapshot(` +" + + +Bun HTML Import Test + + +
+

Hello from Bun!

+ +
+ +" +`); + } + + { + const html = await (await fetch(`http://${hostname}:${port}/dashboard`)).text(); + const jsSrc = new URL( + html.match(/ + +
+

Dashboard

+

This is a separate route to test multiple pages work

+ +

+Back to Home +
+ +" +`); + const response = await fetch(jsSrc!); + const js = await response.text(); + expect( + js + .replace(/# debugId=[a-z0-9A-Z]+/g, "# debugId=") + .replace(/# sourceMappingURL=[^"]+/g, "# sourceMappingURL="), + ).toMatchInlineSnapshot(` +"// script.js +var count = 0; +var button = document.getElementById("counter"); +button.addEventListener("click", () => { + count++; + button.textContent = \`Click me: \${count}\`; +}); + +// dashboard.js +console.log("How...dashing?"); + +//# debugId= +//# sourceMappingURL=" +`); + const sourceMapURL = js.match(/# sourceMappingURL=([^"]+)/)?.[1]; + if (!sourceMapURL) { + throw new Error("No source map URL found"); + } + const sourceMap = await (await fetch(new URL(sourceMapURL, "http://" + hostname + ":" + port))).json(); + sourceMap.sourcesContent = sourceMap.sourcesContent.map(a => a.trim()); + expect(JSON.stringify(sourceMap, null, 2)).toMatchInlineSnapshot(` +"{ + "version": 3, + "sources": [ + "script.js", + "dashboard.js" + ], + "sourcesContent": [ + "let count = 0;\\n const button = document.getElementById('counter');\\n button.addEventListener('click', () => {\\n count++;\\n button.textContent = \`Click me: \${count}\`;\\n });", + "import './script.js';\\n // Additional dashboard-specific code could go here\\n console.log(\\"How...dashing?\\")" + ], + "mappings": ";AACM,IAAI,QAAQ;AACZ,IAAM,SAAS,SAAS,eAAe,SAAS;AAChD,OAAO,iBAAiB,SAAS,MAAM;AACrC;AACA,SAAO,cAAc,aAAa;AAAA,CACnC;;;ACHD,QAAQ,IAAI,gBAAgB;", + "debugId": "0B3DD451DC3D66B564756E2164756E21", + "names": [] +}" +`); + const headers = response.headers.toJSON(); + headers.date = ""; + headers.sourcemap = headers.sourcemap.replace(/chunk-[a-z0-9]+\.js.map/g, "chunk-HASH.js.map"); + expect(headers).toMatchInlineSnapshot(` +{ + "content-length": "316", + "content-type": "text/javascript;charset=utf-8", + "date": "", + "etag": "42b631804ef51c7e", + "sourcemap": "/chunk-HASH.js.map", +} +`); + } + + { + const css = await (await fetch(cssSrc!)).text(); + expect(css).toMatchInlineSnapshot(` +"/* styles.css */ +.container { + text-align: center; + font-family: system-ui, sans-serif; + max-width: 800px; + margin: 2rem auto; +} + +button { + font-size: 1.25rem; + border-radius: .25rem; + border: 2px solid #000; + cursor: pointer; + transition: all .2s; + background: #fff; + padding: .5rem 1rem; +} + +button:hover { + color: #fff; + background: #000; +} +" +`); + } + + expect(await (await fetch(`http://${hostname}:${port}/a-different-url`)).text()).toMatchInlineSnapshot(`"Hello World"`); + + subprocess.kill(); +}); + +async function waitForServer( + dir: string, + entryPoints: Record, +): Promise<{ + subprocess: Subprocess; + port: number; + hostname: string; +}> { + let defer = Promise.withResolvers<{ + subprocess: Subprocess; + port: number; + hostname: string; + }>(); + const process = Bun.spawn({ + cmd: [bunExe(), "--experimental-html", join(import.meta.dir, "bun-serve-static-fixture.js")], + env: { + ...bunEnv, + NODE_ENV: undefined, + }, + cwd: dir, + ipc(message, subprocess) { + subprocess.send({ + files: entryPoints, + }); + defer.resolve({ + subprocess, + port: message.port, + hostname: message.hostname, + }); + }, + }); + return defer.promise; +} diff --git a/test/js/bun/http/bun-serve-static-fixture.js b/test/js/bun/http/bun-serve-static-fixture.js new file mode 100644 index 00000000000000..3f76cc3c892927 --- /dev/null +++ b/test/js/bun/http/bun-serve-static-fixture.js @@ -0,0 +1,34 @@ +import { serve } from "bun"; + +let server = Bun.serve({ + port: 0, + development: true, + async fetch(req) { + return new Response("Hello World", { + status: 404, + }); + }, +}); + +process.on("message", async message => { + const files = message.files || {}; + const routes = {}; + for (const [key, value] of Object.entries(files)) { + routes[key] = (await import(value)).default; + } + + server.reload({ + static: routes, + development: true, + fetch(req) { + return new Response("Hello World", { + status: 404, + }); + }, + }); +}); + +process.send({ + port: server.port, + hostname: server.hostname, +});