Skip to content

Commit 4092b27

Browse files
Add test for Bun.serve static with html (#16413)
1 parent c1218b2 commit 4092b27

File tree

5 files changed

+333
-2
lines changed

5 files changed

+333
-2
lines changed

packages/bun-types/ambient.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ declare module "*/bun.lock" {
1717
var contents: import("bun").BunLockFile;
1818
export = contents;
1919
}
20+
21+
declare module "*.html" {
22+
// In Bun v1.2, we might change this to Bun.HTMLBundle
23+
var contents: any;
24+
export = contents;
25+
}

packages/bun-types/bun.d.ts

+15
Original file line numberDiff line numberDiff line change
@@ -5189,6 +5189,21 @@ declare module "bun" {
51895189
*/
51905190
const isMainThread: boolean;
51915191

5192+
/**
5193+
* Used when importing an HTML file at runtime.
5194+
*
5195+
* @example
5196+
*
5197+
* ```ts
5198+
* import app from "./index.html";
5199+
* ```
5200+
*
5201+
* Bun.build support for this isn't imlpemented yet.
5202+
*/
5203+
interface HTMLBundle {
5204+
index: string;
5205+
}
5206+
51925207
interface Socket<Data = undefined> extends Disposable {
51935208
/**
51945209
* Write `data` to the socket

src/js/node/domain.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
// Import Events
2-
var EventEmitter = require("node:events");
1+
let EventEmitter;
32
const { ERR_UNHANDLED_ERROR } = require("internal/errors");
43

54
const ObjectDefineProperty = Object.defineProperty;
65

76
// Export Domain
87
var domain: any = {};
98
domain.createDomain = domain.create = function () {
9+
if (!EventEmitter) {
10+
EventEmitter = require("node:events");
11+
}
1012
var d = new EventEmitter();
1113

1214
function emitError(e) {
+274
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { Subprocess } from "bun";
2+
import { test, expect } from "bun:test";
3+
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
4+
import { join } from "path";
5+
test("serve html", async () => {
6+
const dir = tempDirWithFiles("html-css-js", {
7+
"dashboard.html": /*html*/ `
8+
<!DOCTYPE html>
9+
<html>
10+
<head>
11+
<title>Dashboard</title>
12+
<link rel="stylesheet" href="styles.css">
13+
<script type="module" src="script.js"></script>
14+
<script type="module" src="dashboard.js"></script>
15+
</head>
16+
<body>
17+
<div class="container">
18+
<h1>Dashboard</h1>
19+
<p>This is a separate route to test multiple pages work</p>
20+
<button id="counter">Click me: 0</button>
21+
<br><br>
22+
<a href="/">Back to Home</a>
23+
</div>
24+
</body>
25+
</html>
26+
`,
27+
"dashboard.js": /*js*/ `
28+
import './script.js';
29+
// Additional dashboard-specific code could go here
30+
console.log("How...dashing?")
31+
`,
32+
"index.html": /*html*/ `
33+
<!DOCTYPE html>
34+
<html>
35+
<head>
36+
<title>Bun HTML Import Test</title>
37+
<link rel="stylesheet" href="styles.css">
38+
<script type="module" src="script.js"></script>
39+
</head>
40+
<body>
41+
<div class="container">
42+
<h1>Hello from Bun!</h1>
43+
<button id="counter">Click me: 0</button>
44+
</div>
45+
</body>
46+
</html>
47+
`,
48+
"script.js": /*js*/ `
49+
let count = 0;
50+
const button = document.getElementById('counter');
51+
button.addEventListener('click', () => {
52+
count++;
53+
button.textContent = \`Click me: \${count}\`;
54+
});
55+
`,
56+
"styles.css": /*css*/ `
57+
.container {
58+
max-width: 800px;
59+
margin: 2rem auto;
60+
text-align: center;
61+
font-family: system-ui, sans-serif;
62+
}
63+
64+
button {
65+
padding: 0.5rem 1rem;
66+
font-size: 1.25rem;
67+
border-radius: 0.25rem;
68+
border: 2px solid #000;
69+
background: #fff;
70+
cursor: pointer;
71+
transition: all 0.2s;
72+
}
73+
74+
button:hover {
75+
background: #000;
76+
color: #fff;
77+
}
78+
`,
79+
});
80+
81+
const { subprocess, port, hostname } = await waitForServer(dir, {
82+
"/": join(dir, "index.html"),
83+
"/dashboard": join(dir, "dashboard.html"),
84+
});
85+
86+
{
87+
const html = await (await fetch(`http://${hostname}:${port}/`)).text();
88+
const trimmed = html
89+
.trim()
90+
.split("\n")
91+
.map(a => a.trim())
92+
.filter(a => a.length > 0)
93+
.join("\n")
94+
.trim()
95+
.replace(/chunk-[a-z0-9]+\.css/g, "chunk-HASH.css")
96+
.replace(/chunk-[a-z0-9]+\.js/g, "chunk-HASH.js");
97+
98+
expect(trimmed).toMatchInlineSnapshot(`
99+
"<!DOCTYPE html>
100+
<html>
101+
<head>
102+
<title>Bun HTML Import Test</title>
103+
<link rel="stylesheet" crossorigin href="/chunk-HASH.css"><script type="module" crossorigin src="/chunk-HASH.js"></script></head>
104+
<body>
105+
<div class="container">
106+
<h1>Hello from Bun!</h1>
107+
<button id="counter">Click me: 0</button>
108+
</div>
109+
</body>
110+
</html>"
111+
`);
112+
}
113+
114+
{
115+
const html = await (await fetch(`http://${hostname}:${port}/dashboard`)).text();
116+
const jsSrc = new URL(
117+
html.match(/<script type="module" crossorigin src="([^"]+)"/)?.[1]!,
118+
"http://" + hostname + ":" + port,
119+
);
120+
var cssSrc = new URL(
121+
html.match(/<link rel="stylesheet" crossorigin href="([^"]+)"/)?.[1]!,
122+
"http://" + hostname + ":" + port,
123+
);
124+
const trimmed = html
125+
.trim()
126+
.split("\n")
127+
.map(a => a.trim())
128+
.filter(a => a.length > 0)
129+
.join("\n")
130+
.trim()
131+
.replace(/chunk-[a-z0-9]+\.css/g, "chunk-HASH.css")
132+
.replace(/chunk-[a-z0-9]+\.js/g, "chunk-HASH.js");
133+
134+
expect(trimmed).toMatchInlineSnapshot(`
135+
"<!DOCTYPE html>
136+
<html>
137+
<head>
138+
<title>Dashboard</title>
139+
<link rel="stylesheet" crossorigin href="/chunk-HASH.css"><script type="module" crossorigin src="/chunk-HASH.js"></script></head>
140+
<body>
141+
<div class="container">
142+
<h1>Dashboard</h1>
143+
<p>This is a separate route to test multiple pages work</p>
144+
<button id="counter">Click me: 0</button>
145+
<br><br>
146+
<a href="/">Back to Home</a>
147+
</div>
148+
</body>
149+
</html>"
150+
`);
151+
const response = await fetch(jsSrc!);
152+
const js = await response.text();
153+
expect(
154+
js
155+
.replace(/# debugId=[a-z0-9A-Z]+/g, "# debugId=<debug-id>")
156+
.replace(/# sourceMappingURL=[^"]+/g, "# sourceMappingURL=<source-mapping-url>"),
157+
).toMatchInlineSnapshot(`
158+
"// script.js
159+
var count = 0;
160+
var button = document.getElementById("counter");
161+
button.addEventListener("click", () => {
162+
count++;
163+
button.textContent = \`Click me: \${count}\`;
164+
});
165+
166+
// dashboard.js
167+
console.log("How...dashing?");
168+
169+
//# debugId=<debug-id>
170+
//# sourceMappingURL=<source-mapping-url>"
171+
`);
172+
const sourceMapURL = js.match(/# sourceMappingURL=([^"]+)/)?.[1];
173+
if (!sourceMapURL) {
174+
throw new Error("No source map URL found");
175+
}
176+
const sourceMap = await (await fetch(new URL(sourceMapURL, "http://" + hostname + ":" + port))).json();
177+
sourceMap.sourcesContent = sourceMap.sourcesContent.map(a => a.trim());
178+
expect(JSON.stringify(sourceMap, null, 2)).toMatchInlineSnapshot(`
179+
"{
180+
"version": 3,
181+
"sources": [
182+
"script.js",
183+
"dashboard.js"
184+
],
185+
"sourcesContent": [
186+
"let count = 0;\\n const button = document.getElementById('counter');\\n button.addEventListener('click', () => {\\n count++;\\n button.textContent = \`Click me: \${count}\`;\\n });",
187+
"import './script.js';\\n // Additional dashboard-specific code could go here\\n console.log(\\"How...dashing?\\")"
188+
],
189+
"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;",
190+
"debugId": "0B3DD451DC3D66B564756E2164756E21",
191+
"names": []
192+
}"
193+
`);
194+
const headers = response.headers.toJSON();
195+
headers.date = "<date>";
196+
headers.sourcemap = headers.sourcemap.replace(/chunk-[a-z0-9]+\.js.map/g, "chunk-HASH.js.map");
197+
expect(headers).toMatchInlineSnapshot(`
198+
{
199+
"content-length": "316",
200+
"content-type": "text/javascript;charset=utf-8",
201+
"date": "<date>",
202+
"etag": "42b631804ef51c7e",
203+
"sourcemap": "/chunk-HASH.js.map",
204+
}
205+
`);
206+
}
207+
208+
{
209+
const css = await (await fetch(cssSrc!)).text();
210+
expect(css).toMatchInlineSnapshot(`
211+
"/* styles.css */
212+
.container {
213+
text-align: center;
214+
font-family: system-ui, sans-serif;
215+
max-width: 800px;
216+
margin: 2rem auto;
217+
}
218+
219+
button {
220+
font-size: 1.25rem;
221+
border-radius: .25rem;
222+
border: 2px solid #000;
223+
cursor: pointer;
224+
transition: all .2s;
225+
background: #fff;
226+
padding: .5rem 1rem;
227+
}
228+
229+
button:hover {
230+
color: #fff;
231+
background: #000;
232+
}
233+
"
234+
`);
235+
}
236+
237+
expect(await (await fetch(`http://${hostname}:${port}/a-different-url`)).text()).toMatchInlineSnapshot(`"Hello World"`);
238+
239+
subprocess.kill();
240+
});
241+
242+
async function waitForServer(
243+
dir: string,
244+
entryPoints: Record<string, string>,
245+
): Promise<{
246+
subprocess: Subprocess;
247+
port: number;
248+
hostname: string;
249+
}> {
250+
let defer = Promise.withResolvers<{
251+
subprocess: Subprocess;
252+
port: number;
253+
hostname: string;
254+
}>();
255+
const process = Bun.spawn({
256+
cmd: [bunExe(), "--experimental-html", join(import.meta.dir, "bun-serve-static-fixture.js")],
257+
env: {
258+
...bunEnv,
259+
NODE_ENV: undefined,
260+
},
261+
cwd: dir,
262+
ipc(message, subprocess) {
263+
subprocess.send({
264+
files: entryPoints,
265+
});
266+
defer.resolve({
267+
subprocess,
268+
port: message.port,
269+
hostname: message.hostname,
270+
});
271+
},
272+
});
273+
return defer.promise;
274+
}

test/js/bun/http/bun-serve-static-fixture.js

+34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)