diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig
index c7cb78b426..71a10e9393 100644
--- a/src/bake/DevServer.zig
+++ b/src/bake/DevServer.zig
@@ -4353,7 +4353,7 @@ fn dumpStateDueToCrash(dev: *DevServer) !void {
const filepath = std.fmt.bufPrintZ(&filepath_buf, "incremental-graph-crash-dump.{d}.html", .{std.time.timestamp()}) catch "incremental-graph-crash-dump.html";
const file = std.fs.cwd().createFileZ(filepath, .{}) catch |err| {
bun.handleErrorReturnTrace(err, @errorReturnTrace());
- Output.warn("Could not open directory for dumping sources: {}", .{err});
+ Output.warn("Could not open file for dumping incremental graph: {}", .{err});
return;
};
defer file.close();
diff --git a/src/bake/production.zig b/src/bake/production.zig
index 4e796c155b..58c53279f1 100644
--- a/src/bake/production.zig
+++ b/src/bake/production.zig
@@ -155,7 +155,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa
break :config try bake.UserOptions.fromJS(app, vm.global);
},
.rejected => |err| {
- return global.throwValue2(err.toError() orelse err);
+ return global.throwValue(err.toError() orelse err);
},
};
diff --git a/test/bake/dev-server-harness.ts b/test/bake/dev-server-harness.ts
index 947b37d132..49cf8f572e 100644
--- a/test/bake/dev-server-harness.ts
+++ b/test/bake/dev-server-harness.ts
@@ -27,7 +27,9 @@ export const minimalFramework: Bake.Framework = {
},
};
-export interface DevServerTest {
+export type DevServerTest = ({
+ /** Starting files */
+ files: FileObject;
/**
* Framework to use. Consider `minimalFramework` if possible.
* Provide this object or `files['bun.app.ts']` for a dynamic one.
@@ -38,8 +40,13 @@ export interface DevServerTest {
* combined with the `framework` option.
*/
pluginFile?: string;
- /** Starting files */
- files: FileObject;
+} | {
+ /**
+ * Copy all files from test/bake/fixtures/
+ * This directory must contain `bun.app.ts` to allow hacking on fixtures manually via `bun run .`
+ */
+ fixture: string;
+}) & {
test: (dev: Dev) => Promise;
}
@@ -327,31 +334,47 @@ export function devTest(description: string, options: T
jest.test(`DevServer > ${basename}.${count}: ${description}`, async () => {
const root = path.join(tempDir, basename + count);
- writeAll(root, options.files);
- if (options.files["bun.app.ts"] == undefined) {
- if (!options.framework) {
- throw new Error("Must specify a options.framework or provide a bun.app.ts file");
+ if ('files' in options) {
+ writeAll(root, options.files);
+ if (options.files["bun.app.ts"] == undefined) {
+ if (!options.framework) {
+ throw new Error("Must specify a options.framework or provide a bun.app.ts file");
+ }
+ if (options.pluginFile) {
+ fs.writeFileSync(path.join(root, "pluginFile.ts"), dedent(options.pluginFile));
+ }
+ fs.writeFileSync(
+ path.join(root, "bun.app.ts"),
+ dedent`
+ ${options.pluginFile ?
+ `import plugins from './pluginFile.ts';` : "let plugins = undefined;"
+ }
+ export default {
+ app: {
+ framework: ${JSON.stringify(options.framework)},
+ plugins,
+ },
+ };
+ `,
+ );
+ } else {
+ if (options.pluginFile) {
+ throw new Error("Cannot provide both bun.app.ts and pluginFile");
+ }
}
- if (options.pluginFile) {
- fs.writeFileSync(path.join(root, "pluginFile.ts"), dedent(options.pluginFile));
- }
- fs.writeFileSync(
- path.join(root, "bun.app.ts"),
- dedent`
- ${options.pluginFile ?
- `import plugins from './pluginFile.ts';` : "let plugins = undefined;"
- }
- export default {
- app: {
- framework: ${JSON.stringify(options.framework)},
- plugins,
- },
- };
- `,
- );
} else {
- if (options.pluginFile) {
- throw new Error("Cannot provide both bun.app.ts and pluginFile");
+ if (!options.fixture) {
+ throw new Error("Must provide either `fixture` or `files`");
+ }
+ const fixture = path.join(devTestRoot, "../fixtures", options.fixture);
+ fs.cpSync(fixture, root, { recursive: true });
+
+ if(!fs.existsSync(path.join(root, "bun.app.ts"))) {
+ throw new Error(`Fixture ${fixture} must contain a bun.app.ts file.`);
+ }
+ if (!fs.existsSync(path.join(root, "node_modules"))) {
+ // link the node_modules directory from test/node_modules to the temp directory
+ fs.symlinkSync(path.join(devTestRoot, "../../node_modules"), path.join(root, "node_modules"), "junction");
}
}
fs.writeFileSync(
@@ -359,8 +382,8 @@ export function devTest(description: string, options: T
dedent`
import appConfig from "./bun.app.ts";
export default {
+ ...appConfig,
port: 0,
- ...appConfig
};
`,
);
@@ -373,6 +396,7 @@ export function devTest(description: string, options: T
{
FORCE_COLOR: "1",
BUN_DEV_SERVER_TEST_RUNNER: "1",
+ BUN_DUMP_STATE_ON_CRASH: "1",
},
]),
stdio: ["pipe", "pipe", "pipe"],
diff --git a/test/bake/dev/ecosystem.test.ts b/test/bake/dev/ecosystem.test.ts
index 068795a875..5a3a6f6c78 100644
--- a/test/bake/dev/ecosystem.test.ts
+++ b/test/bake/dev/ecosystem.test.ts
@@ -2,10 +2,21 @@
// should be preferred to write specific tests for the bugs that these libraries
// discovered, but it easy and still a reasonable idea to just test the library
// entirely.
+import { expect } from "bun:test";
import { devTest } from "../dev-server-harness";
-// TODO: svelte server component example project
// Bugs discovered thanks to Svelte:
-// - Valid circular import use.
-// - Re-export `.e_import_identifier`, including live bindings.
-// TODO: - something related to the wrong push function being called
\ No newline at end of file
+// - Circular import situations
+// - export { live_binding }
+// - export { x as y }
+devTest('svelte component islands example', {
+ fixture: 'svelte-component-islands',
+ async test(dev) {
+ const html = await dev.fetch('/').text()
+ expect(html).toContain('self.$islands={\"pages/_Counter.svelte\":[[0,\"default\",{initial:5}]]}');
+ expect(html).toContain(`This is my svelte server component (non-interactive)
Bun v${Bun.version}
`);
+ expect(html).toContain(`>This is a client component (interactive island)
`);
+ // TODO: puppeteer test for client-side interactivity, hmr.
+ // care must be taken to implement this in a way that is not flaky.
+ },
+});
diff --git a/test/bake/fixtures/svelte-component-islands/bun.app.ts b/test/bake/fixtures/svelte-component-islands/bun.app.ts
new file mode 100644
index 0000000000..9813768a22
--- /dev/null
+++ b/test/bake/fixtures/svelte-component-islands/bun.app.ts
@@ -0,0 +1,8 @@
+import svelte from "./framework";
+
+export default {
+ port: 3000,
+ app: {
+ framework: svelte(),
+ },
+};
diff --git a/test/bake/fixtures/svelte-component-islands/framework/client.ts b/test/bake/fixtures/svelte-component-islands/framework/client.ts
new file mode 100644
index 0000000000..aca37275e0
--- /dev/null
+++ b/test/bake/fixtures/svelte-component-islands/framework/client.ts
@@ -0,0 +1,14 @@
+import type { IslandMap } from "./server";
+import { hydrate } from 'svelte';
+
+declare var $islands: IslandMap;
+Object.entries($islands).forEach(async([moduleId, islands]) => {
+ const mod = await import(moduleId);
+ for(const [islandId, exportId, props] of islands) {
+ const elem = document.getElementById(`I:${islandId}`)!;
+ hydrate(mod[exportId], {
+ target: elem,
+ props,
+ });
+ }
+});
diff --git a/test/bake/fixtures/svelte-component-islands/framework/index.ts b/test/bake/fixtures/svelte-component-islands/framework/index.ts
new file mode 100644
index 0000000000..7cecf75358
--- /dev/null
+++ b/test/bake/fixtures/svelte-component-islands/framework/index.ts
@@ -0,0 +1,68 @@
+import type { Bake } from "bun";
+import * as path from "node:path";
+import * as fs from "node:fs/promises";
+import * as svelte from "svelte/compiler";
+
+export default function (): Bake.Framework {
+ return {
+ serverComponents: {
+ separateSSRGraph: false,
+ serverRuntimeImportSource: "./framework/server.ts",
+ },
+ fileSystemRouterTypes: [
+ {
+ root: "pages",
+ serverEntryPoint: "./framework/server.ts",
+ clientEntryPoint: "./framework/client.ts",
+ style: "nextjs-pages", // later, this will be fully programmable
+ extensions: [".svelte"],
+ },
+ ],
+ plugins: [
+ {
+ // This is missing a lot of code that a plugin like `esbuild-svelte`
+ // handles, but this is only an examplea of how such a plugin could
+ // have server-components at a minimal level.
+ name: "svelte-server-components",
+ setup(b) {
+ const cssMap = new Map();
+ b.onLoad({ filter: /.svelte$/ }, async (args) => {
+ const contents = await fs.readFile(args.path, "utf-8");
+ const result = svelte.compile(contents, {
+ filename: args.path,
+ css: "external",
+ cssOutputFilename: path.basename(args.path, ".svelte") + ".css",
+ hmr: true,
+ dev: true,
+ generate: args.side,
+ });
+ // If CSS is specified, add a CSS import
+ let jsCode = result.js.code;
+ if (result.css) {
+ cssMap.set(args.path, result.css.code);
+ jsCode = `import ${JSON.stringify("svelte-css:" + args.path)};` + jsCode;
+ }
+ // Extract a "use client" directive from the file.
+ const header = contents.match(/^\s*\s*("[^"\n]*"|'[^'\n]*')/)?.[1];
+ if (header) {
+ jsCode = header + ';' + jsCode;
+ }
+ return {
+ contents: jsCode,
+ loader: "js",
+ watchFiles: [args.path],
+ };
+ });
+
+ // Resolve CSS files
+ b.onResolve({ filter: /^svelte-css:/ }, async (args) => {
+ return { path: args.path.replace(/^svelte-css:/, ""), namespace: "svelte-css" };
+ });
+ b.onLoad({ filter: /./, namespace: "svelte-css" }, async (args) => {
+ return { contents: cssMap.get(args.path) ?? "", loader: "css" };
+ });
+ },
+ },
+ ],
+ };
+}
diff --git a/test/bake/fixtures/svelte-component-islands/framework/server.ts b/test/bake/fixtures/svelte-component-islands/framework/server.ts
new file mode 100644
index 0000000000..29253245f8
--- /dev/null
+++ b/test/bake/fixtures/svelte-component-islands/framework/server.ts
@@ -0,0 +1,71 @@
+///
+import type { Bake } from "bun";
+import * as svelte from "svelte/server";
+import { uneval } from "devalue";
+
+export function render(req: Request, meta: Bake.RouteMetadata) {
+ isInsideIsland = false;
+ islands = {};
+ const { body, head } = svelte.render(meta.pageModule.default, {
+ props: {
+ params: meta.params,
+ },
+ });
+
+ // Add stylesheets and preloaded modules to the head
+ const extraHead = meta.styles.map((style) => ``).join("")
+ + meta.modulepreload.map((style) => ``).join("");
+ // Script tags
+ const scripts = nextIslandId > 0
+ ? `` +
+ meta.modules.map((module) => ``).join("")
+ : ""; // If no islands, no JavaScript
+
+ return new Response(
+ "" + head + extraHead + ""
+ + body + "" + scripts + "",
+ { headers: { "content-type": "text/html" } },
+ );
+}
+
+// To allow static site generation, frameworks can specify a prerender function
+export function prerender(meta: Bake.RouteMetadata) {
+ return {
+ files: {
+ '/index.html': render(null!, meta),
+ },
+ };
+}
+
+let isInsideIsland = false;
+let nextIslandId = 0;
+let islands: IslandMap;
+export type IslandMap = Record;
+export type Island = [islandId: number, exportId: string, props: any];
+
+/**
+ * @param component The original export value, as is.
+ * @param clientModuleId A string that the browser will pass to `import()`.
+ * @param clientExportId The export ID from the imported module.
+ * @returns A wrapped value for the export.
+ */
+export function registerClientReference(
+ component: Function,
+ clientModuleId: string,
+ clientExportId: string,
+) {
+ return function Island(...args: any[]) {
+ if (isInsideIsland) {
+ return component(...args);
+ }
+ isInsideIsland = true;
+ const [payload, props] = args;
+ const islandId = nextIslandId++;
+ payload.out += ``;
+ const file = (islands[clientModuleId] ??= []);
+ file.push([islandId, clientExportId, props]);
+ component(...args);
+ payload.out += ``;
+ isInsideIsland = false;
+ };
+}
diff --git a/test/bake/fixtures/svelte-component-islands/pages/_Counter.svelte b/test/bake/fixtures/svelte-component-islands/pages/_Counter.svelte
new file mode 100644
index 0000000000..7c4a618edb
--- /dev/null
+++ b/test/bake/fixtures/svelte-component-islands/pages/_Counter.svelte
@@ -0,0 +1,24 @@
+
+
+
+
This is a client component (interactive island)
+
+
+
diff --git a/test/bake/fixtures/svelte-component-islands/pages/index.svelte b/test/bake/fixtures/svelte-component-islands/pages/index.svelte
new file mode 100644
index 0000000000..f5b8c0728c
--- /dev/null
+++ b/test/bake/fixtures/svelte-component-islands/pages/index.svelte
@@ -0,0 +1,18 @@
+
+
+ hello
+ This is my svelte server component (non-interactive)
+ Bun v{Bun.version}
+
+
+
\ No newline at end of file
diff --git a/test/bun.lockb b/test/bun.lockb
index 6a1061e911..699279fcb2 100755
Binary files a/test/bun.lockb and b/test/bun.lockb differ
diff --git a/test/package.json b/test/package.json
index 03a8717ce3..f643ef682d 100644
--- a/test/package.json
+++ b/test/package.json
@@ -21,6 +21,7 @@
"axios": "1.6.8",
"body-parser": "1.20.2",
"comlink": "4.4.1",
+ "devalue": "5.1.1",
"es-module-lexer": "1.3.0",
"esbuild": "0.18.6",
"express": "4.18.2",
@@ -59,7 +60,7 @@
"string-width": "7.0.0",
"stripe": "15.4.0",
"supertest": "6.3.3",
- "svelte": "3.55.1",
+ "svelte": "5.4.0",
"typescript": "5.0.2",
"undici": "5.20.0",
"verdaccio": "6.0.0",