mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 04:18:58 +00:00
hi
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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/<name>
|
||||
* This directory must contain `bun.app.ts` to allow hacking on fixtures manually via `bun run .`
|
||||
*/
|
||||
fixture: string;
|
||||
}) & {
|
||||
test: (dev: Dev) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -327,31 +334,47 @@ export function devTest<T extends DevServerTest>(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<T extends DevServerTest>(description: string, options: T
|
||||
dedent`
|
||||
import appConfig from "./bun.app.ts";
|
||||
export default {
|
||||
...appConfig,
|
||||
port: 0,
|
||||
...appConfig
|
||||
};
|
||||
`,
|
||||
);
|
||||
@@ -373,6 +396,7 @@ export function devTest<T extends DevServerTest>(description: string, options: T
|
||||
{
|
||||
FORCE_COLOR: "1",
|
||||
BUN_DEV_SERVER_TEST_RUNNER: "1",
|
||||
BUN_DUMP_STATE_ON_CRASH: "1",
|
||||
},
|
||||
]),
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
|
||||
@@ -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
|
||||
// - 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(`<p>This is my svelte server component (non-interactive)</p> <p>Bun v${Bun.version}</p>`);
|
||||
expect(html).toContain(`>This is a client component (interactive island)</p>`);
|
||||
// TODO: puppeteer test for client-side interactivity, hmr.
|
||||
// care must be taken to implement this in a way that is not flaky.
|
||||
},
|
||||
});
|
||||
|
||||
8
test/bake/fixtures/svelte-component-islands/bun.app.ts
Normal file
8
test/bake/fixtures/svelte-component-islands/bun.app.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import svelte from "./framework";
|
||||
|
||||
export default {
|
||||
port: 3000,
|
||||
app: {
|
||||
framework: svelte(),
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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<string, string>();
|
||||
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*<script.*?>\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" };
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/// <reference path="../../../../../src/bake/bake.d.ts" />
|
||||
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) => `<link rel="stylesheet" href="${style}">`).join("")
|
||||
+ meta.modulepreload.map((style) => `<link rel="modulepreload" href="${style}">`).join("");
|
||||
// Script tags
|
||||
const scripts = nextIslandId > 0
|
||||
? `<script>self.$islands=${uneval(islands)}</script>` +
|
||||
meta.modules.map((module) => `<script type="module" src="${module}"></script>`).join("")
|
||||
: ""; // If no islands, no JavaScript
|
||||
|
||||
return new Response(
|
||||
"<!DOCTYPE html><html><head>" + head + extraHead + "</head><body>"
|
||||
+ body + "</body>" + scripts + "</html>",
|
||||
{ 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<string, Island[]>;
|
||||
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 += `<bake-island id="I:${islandId}">`;
|
||||
const file = (islands[clientModuleId] ??= []);
|
||||
file.push([islandId, clientExportId, props]);
|
||||
component(...args);
|
||||
payload.out += `</bake-island>`;
|
||||
isInsideIsland = false;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<script>
|
||||
"use client";
|
||||
let { initial } = $props();
|
||||
let count = $state(initial);
|
||||
function increment() {
|
||||
count += 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p>This is a client component (interactive island)</p>
|
||||
<button onclick={increment}>
|
||||
Clicked {count} {count === 1 ? 'time' : 'times'}
|
||||
</button>
|
||||
</div>
|
||||
<style>
|
||||
div {
|
||||
border: 1px solid red;
|
||||
padding: 1rem;
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import Counter from "./_Counter.svelte";
|
||||
</script>
|
||||
<main>
|
||||
<h1>hello</h1>
|
||||
<p>This is my svelte server component (non-interactive)</p>
|
||||
<p>Bun v{Bun.version}</p>
|
||||
<Counter initial={5} />
|
||||
</main>
|
||||
<style>
|
||||
main {
|
||||
border: 1px solid blue;
|
||||
padding: 1rem;
|
||||
h1 {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
BIN
test/bun.lockb
BIN
test/bun.lockb
Binary file not shown.
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user