mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Compare commits
1 Commits
bun-v1.3.4
...
jarred/htm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a1905255e |
@@ -118,7 +118,91 @@ pub const AnyRoute = union(enum) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn htmlRouteFromJS(argument: JSC.JSValue, init_ctx: *ServerInitContext) ?AnyRoute {
|
||||
fn htmlManifestEntryFromJS(entry: JSC.JSValue, global: *JSC.JSGlobalObject, index_route: *?AnyRoute, index_route_path: []const u8, user_routes_to_build: *std.ArrayList(ServerConfig.StaticRouteEntry)) bun.JSError!void {
|
||||
var path: JSC.Node.PathOrFileDescriptor = .{
|
||||
.path = .{ .encoded_slice = try entry.getOptional(global, "path", ZigString.Slice) orelse return },
|
||||
};
|
||||
defer path.deinit();
|
||||
const headers: ?*JSC.WebCore.FetchHeaders = if (try entry.getOptional(global, "headers", JSValue)) |headers_value|
|
||||
JSC.WebCore.FetchHeaders.createFromJS(global, headers_value)
|
||||
else
|
||||
null;
|
||||
|
||||
defer {
|
||||
if (headers) |h| h.deref();
|
||||
}
|
||||
|
||||
if (global.hasException()) return error.JSError;
|
||||
|
||||
var blob = Blob.findOrCreateFileFromPath(
|
||||
&path,
|
||||
global,
|
||||
true,
|
||||
);
|
||||
|
||||
var path_segment = std.ArrayList(u8).init(bun.default_allocator);
|
||||
errdefer path_segment.deinit();
|
||||
|
||||
var path_relative_to_cwd = bun.path.relativeNormalized(bun.fs.FileSystem.instance.top_level_dir, path.slice(), .posix, true);
|
||||
if (strings.hasPrefix(path_relative_to_cwd, "./")) {
|
||||
path_relative_to_cwd = path_relative_to_cwd[1..];
|
||||
}
|
||||
|
||||
if (!strings.hasPrefix(path_relative_to_cwd, "/")) {
|
||||
try path_segment.append('/');
|
||||
}
|
||||
|
||||
try path_segment.appendSlice(path_relative_to_cwd);
|
||||
|
||||
const any_route: AnyRoute = if (blob.needsToReadFile())
|
||||
.{
|
||||
.file = FileRoute.initFromBlob(blob, .{
|
||||
.headers = headers,
|
||||
.server = null,
|
||||
}),
|
||||
}
|
||||
else
|
||||
.{
|
||||
.static = StaticRoute.initFromAnyBlob(&.{ .Blob = blob }, .{
|
||||
.headers = headers,
|
||||
.server = null,
|
||||
}),
|
||||
};
|
||||
|
||||
if (index_route.* == null and strings.eql(path.slice(), index_route_path)) {
|
||||
index_route.* = any_route;
|
||||
} else {
|
||||
var methods = HTTP.Method.Optional{
|
||||
.method = .{},
|
||||
};
|
||||
methods.insert(.GET);
|
||||
methods.insert(.HEAD);
|
||||
|
||||
try user_routes_to_build.append(.{
|
||||
.path = path_segment.items,
|
||||
.route = any_route,
|
||||
.method = methods,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn htmlManifestObjectFromJS(argument: JSC.JSValue, init_ctx: *ServerInitContext, global: *JSC.JSGlobalObject) bun.JSError!?AnyRoute {
|
||||
const index: ZigString.Slice = try (argument.getOptional(global, "index", ZigString.Slice)) orelse return null;
|
||||
defer index.deinit();
|
||||
|
||||
const files: JSValue = (try argument.getOwnArray(global, "files")) orelse return null;
|
||||
var array_iter = files.arrayIterator(global);
|
||||
var index_route: ?AnyRoute = null;
|
||||
const index_route_ptr: *?AnyRoute = &index_route;
|
||||
|
||||
while (array_iter.next()) |entry| {
|
||||
try htmlManifestEntryFromJS(entry, global, index_route_ptr, index.slice(), init_ctx.static_routes);
|
||||
}
|
||||
|
||||
return index_route;
|
||||
}
|
||||
|
||||
pub fn htmlRouteFromJS(argument: JSC.JSValue, init_ctx: *ServerInitContext, global: *JSC.JSGlobalObject) bun.JSError!?AnyRoute {
|
||||
if (argument.as(HTMLBundle)) |html_bundle| {
|
||||
const entry = init_ctx.dedupe_html_bundle_map.getOrPut(html_bundle) catch bun.outOfMemory();
|
||||
if (!entry.found_existing) {
|
||||
@@ -129,6 +213,10 @@ pub const AnyRoute = union(enum) {
|
||||
}
|
||||
}
|
||||
|
||||
if (argument.isObject()) {
|
||||
return htmlManifestObjectFromJS(argument, init_ctx, global);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -137,6 +225,7 @@ pub const AnyRoute = union(enum) {
|
||||
dedupe_html_bundle_map: std.AutoHashMap(*HTMLBundle, bun.ptr.RefPtr(HTMLBundle.Route)),
|
||||
js_string_allocations: bun.bake.StringRefList,
|
||||
framework_router_list: std.ArrayList(bun.bake.Framework.FileSystemRouterType),
|
||||
static_routes: *std.ArrayList(ServerConfig.StaticRouteEntry),
|
||||
};
|
||||
|
||||
pub fn fromJS(
|
||||
@@ -145,7 +234,7 @@ pub const AnyRoute = union(enum) {
|
||||
argument: JSC.JSValue,
|
||||
init_ctx: *ServerInitContext,
|
||||
) bun.JSError!?AnyRoute {
|
||||
if (AnyRoute.htmlRouteFromJS(argument, init_ctx)) |html_route| {
|
||||
if (try AnyRoute.htmlRouteFromJS(argument, init_ctx, global)) |html_route| {
|
||||
return html_route;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ has_content_length_header: bool,
|
||||
pub const InitOptions = struct {
|
||||
server: ?AnyServer,
|
||||
status_code: u16 = 200,
|
||||
headers: ?*JSC.WebCore.FetchHeaders = null,
|
||||
};
|
||||
|
||||
pub fn lastModifiedDate(this: *const FileRoute) ?u64 {
|
||||
@@ -34,13 +35,15 @@ pub fn lastModifiedDate(this: *const FileRoute) ?u64 {
|
||||
}
|
||||
|
||||
pub fn initFromBlob(blob: Blob, opts: InitOptions) *FileRoute {
|
||||
const headers = Headers.from(null, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory();
|
||||
const headers = Headers.from(opts.headers, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory();
|
||||
return bun.new(FileRoute, .{
|
||||
.ref_count = .init(),
|
||||
.server = opts.server,
|
||||
.blob = blob,
|
||||
.headers = headers,
|
||||
.status_code = opts.status_code,
|
||||
.has_last_modified_header = headers.contains("last-modified"),
|
||||
.has_content_length_header = headers.contains("content-length"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -511,6 +511,7 @@ pub fn fromJS(
|
||||
.dedupe_html_bundle_map = .init(bun.default_allocator),
|
||||
.framework_router_list = .init(bun.default_allocator),
|
||||
.js_string_allocations = .empty,
|
||||
.static_routes = &args.static_routes,
|
||||
};
|
||||
errdefer {
|
||||
init_ctx.arena.deinit();
|
||||
@@ -1067,7 +1068,7 @@ pub fn fromJS(
|
||||
return;
|
||||
}
|
||||
|
||||
const UserRouteBuilder = struct {
|
||||
pub const UserRouteBuilder = struct {
|
||||
route: ServerConfig.RouteDeclaration,
|
||||
callback: JSC.Strong.Optional = .empty,
|
||||
|
||||
|
||||
@@ -22,11 +22,12 @@ pub const InitFromBytesOptions = struct {
|
||||
server: ?AnyServer,
|
||||
mime_type: ?*const bun.http.MimeType = null,
|
||||
status_code: u16 = 200,
|
||||
headers: ?*JSC.WebCore.FetchHeaders = null,
|
||||
};
|
||||
|
||||
/// Ownership of `blob` is transferred to this function.
|
||||
pub fn initFromAnyBlob(blob: *const AnyBlob, options: InitFromBytesOptions) *StaticRoute {
|
||||
var headers = Headers.from(null, bun.default_allocator, .{ .body = blob }) catch bun.outOfMemory();
|
||||
var headers = Headers.from(options.headers, bun.default_allocator, .{ .body = blob }) catch bun.outOfMemory();
|
||||
if (options.mime_type) |mime_type| {
|
||||
if (headers.getContentType() == null) {
|
||||
headers.append("Content-Type", mime_type.value) catch bun.outOfMemory();
|
||||
|
||||
@@ -871,6 +871,13 @@ pub const PathOrFileDescriptor = union(Tag) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slice(this: *const PathOrFileDescriptor) []const u8 {
|
||||
return switch (this.*) {
|
||||
.path => this.path.slice(),
|
||||
.fd => @panic("slice not supported for file descriptors"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn estimatedSize(this: *const PathOrFileDescriptor) usize {
|
||||
return switch (this.*) {
|
||||
.path => this.path.estimatedSize(),
|
||||
|
||||
@@ -4806,6 +4806,9 @@ pub const Headers = struct {
|
||||
|
||||
return null;
|
||||
}
|
||||
pub inline fn contains(this: *const Headers, name: []const u8) bool {
|
||||
return this.get(name) != null;
|
||||
}
|
||||
|
||||
pub fn append(this: *Headers, name: []const u8, value: []const u8) !void {
|
||||
var offset: u32 = @truncate(this.buf.items.len);
|
||||
|
||||
484
test/js/bun/http/bun-serve-html-manifest.test.ts
Normal file
484
test/js/bun/http/bun-serve-html-manifest.test.ts
Normal file
@@ -0,0 +1,484 @@
|
||||
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
||||
import { join } from "path";
|
||||
import { tempDirWithFiles } from "harness";
|
||||
import type { Server } from "bun";
|
||||
|
||||
describe("serve html manifest", () => {
|
||||
test.only("basic manifest object with index and files", async () => {
|
||||
const dir = tempDirWithFiles("html-manifest-basic", {
|
||||
"index.html": `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Manifest Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from Manifest</h1>
|
||||
</body>
|
||||
</html>`,
|
||||
"about.html": `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>About Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>About Us</h1>
|
||||
</body>
|
||||
</html>`,
|
||||
"styles.css": `.container { color: blue; }`,
|
||||
"script.js": `console.log("Hello from script");`,
|
||||
});
|
||||
|
||||
// Create a manifest object that mimics what would be generated by the bundler
|
||||
const manifest = {
|
||||
index: join(dir, "index.html"),
|
||||
files: [{ path: join(dir, "about.html") }, { path: join(dir, "styles.css") }, { path: join(dir, "script.js") }],
|
||||
};
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/": manifest,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
// Test index route
|
||||
const indexResponse = await fetch(server.url);
|
||||
expect(indexResponse.status).toBe(200);
|
||||
expect(indexResponse.headers.get("content-type")).toBe("text/html;charset=utf-8");
|
||||
const indexHtml = await indexResponse.text();
|
||||
expect(indexHtml).toContain("<h1>Hello from Manifest</h1>");
|
||||
|
||||
// Test file routes
|
||||
const aboutResponse = await fetch(`${server.url}about.html`);
|
||||
expect(aboutResponse.status).toBe(200);
|
||||
expect(aboutResponse.headers.get("content-type")).toBe("text/html;charset=utf-8");
|
||||
const aboutHtml = await aboutResponse.text();
|
||||
expect(aboutHtml).toContain("<h1>About Us</h1>");
|
||||
|
||||
const cssResponse = await fetch(`${server.url}styles.css`);
|
||||
expect(cssResponse.status).toBe(200);
|
||||
expect(cssResponse.headers.get("content-type")).toBe("text/css;charset=utf-8");
|
||||
const css = await cssResponse.text();
|
||||
expect(css).toContain(".container { color: blue; }");
|
||||
|
||||
const jsResponse = await fetch(`${server.url}script.js`);
|
||||
expect(jsResponse.status).toBe(200);
|
||||
expect(jsResponse.headers.get("content-type")).toBe("text/javascript;charset=utf-8");
|
||||
const js = await jsResponse.text();
|
||||
expect(js).toContain('console.log("Hello from script");');
|
||||
});
|
||||
|
||||
test("manifest with custom headers", async () => {
|
||||
const dir = tempDirWithFiles("html-manifest-headers", {
|
||||
"index.html": `<!DOCTYPE html><html><body>Index</body></html>`,
|
||||
"cached.js": `console.log("cached");`,
|
||||
});
|
||||
|
||||
const manifest = {
|
||||
index: join(dir, "index.html"),
|
||||
files: [
|
||||
{
|
||||
path: join(dir, "cached.js"),
|
||||
headers: {
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
"X-Custom-Header": "custom-value",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
static: {
|
||||
"/": manifest,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
const jsResponse = await fetch(`${server.url}cached.js`);
|
||||
expect(jsResponse.status).toBe(200);
|
||||
expect(jsResponse.headers.get("cache-control")).toBe("public, max-age=3600");
|
||||
expect(jsResponse.headers.get("x-custom-header")).toBe("custom-value");
|
||||
});
|
||||
|
||||
test("manifest with nested paths", async () => {
|
||||
const dir = tempDirWithFiles("html-manifest-nested", {
|
||||
"index.html": `<!DOCTYPE html><html><body>Root</body></html>`,
|
||||
"assets/styles.css": `.nested { color: red; }`,
|
||||
"assets/images/logo.png": Buffer.from("fake png data"),
|
||||
"pages/about.html": `<!DOCTYPE html><html><body>About</body></html>`,
|
||||
});
|
||||
|
||||
const manifest = {
|
||||
index: join(dir, "index.html"),
|
||||
files: [
|
||||
{ path: join(dir, "assets/styles.css") },
|
||||
{ path: join(dir, "assets/images/logo.png") },
|
||||
{ path: join(dir, "pages/about.html") },
|
||||
],
|
||||
};
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
static: {
|
||||
"/": manifest,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
// Test nested paths are served correctly
|
||||
const cssResponse = await fetch(`${server.url}assets/styles.css`);
|
||||
expect(cssResponse.status).toBe(200);
|
||||
const css = await cssResponse.text();
|
||||
expect(css).toContain(".nested { color: red; }");
|
||||
|
||||
const pngResponse = await fetch(`${server.url}assets/images/logo.png`);
|
||||
expect(pngResponse.status).toBe(200);
|
||||
expect(pngResponse.headers.get("content-type")).toBe("image/png");
|
||||
|
||||
const aboutResponse = await fetch(`${server.url}pages/about.html`);
|
||||
expect(aboutResponse.status).toBe(200);
|
||||
const aboutHtml = await aboutResponse.text();
|
||||
expect(aboutHtml).toContain("About");
|
||||
});
|
||||
|
||||
test("manifest with multiple routes", async () => {
|
||||
const dir = tempDirWithFiles("html-manifest-multiple", {
|
||||
"home/index.html": `<!DOCTYPE html><html><body>Home</body></html>`,
|
||||
"home/home.js": `console.log("home");`,
|
||||
"admin/index.html": `<!DOCTYPE html><html><body>Admin</body></html>`,
|
||||
"admin/admin.js": `console.log("admin");`,
|
||||
});
|
||||
|
||||
const homeManifest = {
|
||||
index: join(dir, "home/index.html"),
|
||||
files: [{ path: join(dir, "home/home.js") }],
|
||||
};
|
||||
|
||||
const adminManifest = {
|
||||
index: join(dir, "admin/index.html"),
|
||||
files: [{ path: join(dir, "admin/admin.js") }],
|
||||
};
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
static: {
|
||||
"/": homeManifest,
|
||||
"/admin": adminManifest,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
// Test home route
|
||||
const homeResponse = await fetch(server.url);
|
||||
expect(homeResponse.status).toBe(200);
|
||||
const homeHtml = await homeResponse.text();
|
||||
expect(homeHtml).toContain("Home");
|
||||
|
||||
const homeJsResponse = await fetch(`${server.url}home/home.js`);
|
||||
expect(homeJsResponse.status).toBe(200);
|
||||
const homeJs = await homeJsResponse.text();
|
||||
expect(homeJs).toContain('console.log("home");');
|
||||
|
||||
// Test admin route
|
||||
const adminResponse = await fetch(`${server.url}admin`);
|
||||
expect(adminResponse.status).toBe(200);
|
||||
const adminHtml = await adminResponse.text();
|
||||
expect(adminHtml).toContain("Admin");
|
||||
|
||||
const adminJsResponse = await fetch(`${server.url}admin/admin.js`);
|
||||
expect(adminJsResponse.status).toBe(200);
|
||||
const adminJs = await adminJsResponse.text();
|
||||
expect(adminJs).toContain('console.log("admin");');
|
||||
});
|
||||
|
||||
test("manifest with large files", async () => {
|
||||
const largeContent = "x".repeat(1024 * 1024); // 1MB
|
||||
const dir = tempDirWithFiles("html-manifest-large", {
|
||||
"index.html": `<!DOCTYPE html><html><body>Index</body></html>`,
|
||||
"large.txt": largeContent,
|
||||
});
|
||||
|
||||
const manifest = {
|
||||
index: join(dir, "index.html"),
|
||||
files: [{ path: join(dir, "large.txt") }],
|
||||
};
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
static: {
|
||||
"/": manifest,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
const response = await fetch(`${server.url}large.txt`);
|
||||
expect(response.status).toBe(200);
|
||||
const text = await response.text();
|
||||
expect(text.length).toBe(1024 * 1024);
|
||||
expect(text).toBe(largeContent);
|
||||
});
|
||||
|
||||
test("manifest with binary files", async () => {
|
||||
const binaryData = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]); // JPEG header
|
||||
const dir = tempDirWithFiles("html-manifest-binary", {
|
||||
"index.html": `<!DOCTYPE html><html><body>Index</body></html>`,
|
||||
"image.jpg": binaryData,
|
||||
});
|
||||
|
||||
const manifest = {
|
||||
index: join(dir, "index.html"),
|
||||
files: [{ path: join(dir, "image.jpg") }],
|
||||
};
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
static: {
|
||||
"/": manifest,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
const response = await fetch(`${server.url}image.jpg`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toBe("image/jpeg");
|
||||
const buffer = await response.arrayBuffer();
|
||||
expect(new Uint8Array(buffer)).toEqual(new Uint8Array(binaryData));
|
||||
});
|
||||
|
||||
test("manifest handles HEAD requests", async () => {
|
||||
const dir = tempDirWithFiles("html-manifest-head", {
|
||||
"index.html": `<!DOCTYPE html><html><body>Index</body></html>`,
|
||||
"file.txt": "Hello World",
|
||||
});
|
||||
|
||||
const manifest = {
|
||||
index: join(dir, "index.html"),
|
||||
files: [{ path: join(dir, "file.txt") }],
|
||||
};
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
static: {
|
||||
"/": manifest,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
// HEAD request for index
|
||||
const indexHead = await fetch(server.url, { method: "HEAD" });
|
||||
expect(indexHead.status).toBe(200);
|
||||
expect(indexHead.headers.get("content-type")).toBe("text/html;charset=utf-8");
|
||||
expect(await indexHead.text()).toBe("");
|
||||
|
||||
// HEAD request for file
|
||||
const fileHead = await fetch(`${server.url}file.txt`, { method: "HEAD" });
|
||||
expect(fileHead.status).toBe(200);
|
||||
expect(fileHead.headers.get("content-type")).toBe("text/plain;charset=utf-8");
|
||||
expect(await fileHead.text()).toBe("");
|
||||
});
|
||||
|
||||
test("manifest with empty files array", async () => {
|
||||
const dir = tempDirWithFiles("html-manifest-empty", {
|
||||
"index.html": `<!DOCTYPE html><html><body>Index Only</body></html>`,
|
||||
});
|
||||
|
||||
const manifest = {
|
||||
index: join(dir, "index.html"),
|
||||
files: [],
|
||||
};
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
static: {
|
||||
"/": manifest,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
// Index should work
|
||||
const indexResponse = await fetch(server.url);
|
||||
expect(indexResponse.status).toBe(200);
|
||||
const html = await indexResponse.text();
|
||||
expect(html).toContain("Index Only");
|
||||
|
||||
// Other paths should return 404
|
||||
const notFound = await fetch(`${server.url}nonexistent.js`);
|
||||
expect(notFound.status).toBe(404);
|
||||
});
|
||||
|
||||
test("manifest with wildcards and API routes", async () => {
|
||||
const dir = tempDirWithFiles("html-manifest-wildcard", {
|
||||
"index.html": `<!DOCTYPE html><html><body>App</body></html>`,
|
||||
"app.js": `console.log("app");`,
|
||||
});
|
||||
|
||||
const manifest = {
|
||||
index: join(dir, "index.html"),
|
||||
files: [{ path: join(dir, "app.js") }],
|
||||
};
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
static: {
|
||||
"/*": manifest,
|
||||
"/api/*": false,
|
||||
},
|
||||
fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
return Response.json({ api: true, path: url.pathname });
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
// Test HTML routes
|
||||
for (const path of ["/", "/about", "/contact"]) {
|
||||
const response = await fetch(`${server.url}${path}`);
|
||||
expect(response.status).toBe(200);
|
||||
const html = await response.text();
|
||||
expect(html).toContain("App");
|
||||
}
|
||||
|
||||
// Test static file
|
||||
const jsResponse = await fetch(`${server.url}app.js`);
|
||||
expect(jsResponse.status).toBe(200);
|
||||
const js = await jsResponse.text();
|
||||
expect(js).toContain('console.log("app");');
|
||||
|
||||
// Test API routes
|
||||
const apiResponse = await fetch(`${server.url}api/users`);
|
||||
expect(apiResponse.status).toBe(200);
|
||||
const json = await apiResponse.json();
|
||||
expect(json).toEqual({ api: true, path: "/api/users" });
|
||||
});
|
||||
|
||||
test("manifest with development mode", async () => {
|
||||
const dir = tempDirWithFiles("html-manifest-dev", {
|
||||
"index.html": `<!DOCTYPE html><html><body>Dev Mode</body></html>`,
|
||||
"app.js": `console.log("development");`,
|
||||
});
|
||||
|
||||
const manifest = {
|
||||
index: join(dir, "index.html"),
|
||||
files: [{ path: join(dir, "app.js") }],
|
||||
};
|
||||
|
||||
for (const development of [true, false]) {
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
development,
|
||||
static: {
|
||||
"/": manifest,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
const response = await fetch(server.url);
|
||||
expect(response.status).toBe(200);
|
||||
const html = await response.text();
|
||||
expect(html).toContain("Dev Mode");
|
||||
}
|
||||
});
|
||||
|
||||
test("manifest with relative paths converted to absolute", async () => {
|
||||
const dir = tempDirWithFiles("html-manifest-relative", {
|
||||
"public/index.html": `<!DOCTYPE html><html><body>Public</body></html>`,
|
||||
"public/assets/style.css": `body { margin: 0; }`,
|
||||
});
|
||||
|
||||
// Test that relative paths are handled correctly
|
||||
const manifest = {
|
||||
index: join(dir, "public/index.html"),
|
||||
files: [{ path: join(dir, "public/assets/style.css") }],
|
||||
};
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
static: {
|
||||
"/": manifest,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
const cssResponse = await fetch(`${server.url}public/assets/style.css`);
|
||||
expect(cssResponse.status).toBe(200);
|
||||
const css = await cssResponse.text();
|
||||
expect(css).toContain("body { margin: 0; }");
|
||||
});
|
||||
|
||||
test("manifest reload", async () => {
|
||||
const dir = tempDirWithFiles("html-manifest-reload", {
|
||||
"v1/index.html": `<!DOCTYPE html><html><body>Version 1</body></html>`,
|
||||
"v2/index.html": `<!DOCTYPE html><html><body>Version 2</body></html>`,
|
||||
});
|
||||
|
||||
const manifest1 = {
|
||||
index: join(dir, "v1/index.html"),
|
||||
files: [],
|
||||
};
|
||||
|
||||
const manifest2 = {
|
||||
index: join(dir, "v2/index.html"),
|
||||
files: [],
|
||||
};
|
||||
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
static: {
|
||||
"/": manifest1,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Test initial version
|
||||
const response1 = await fetch(server.url);
|
||||
expect(response1.status).toBe(200);
|
||||
const html1 = await response1.text();
|
||||
expect(html1).toContain("Version 1");
|
||||
|
||||
// Reload with new manifest
|
||||
server.reload({
|
||||
static: {
|
||||
"/": manifest2,
|
||||
},
|
||||
fetch(req) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
// Test updated version
|
||||
const response2 = await fetch(server.url);
|
||||
expect(response2.status).toBe(200);
|
||||
const html2 = await response2.text();
|
||||
expect(html2).toContain("Version 2");
|
||||
} finally {
|
||||
server.stop(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user