diff --git a/packages/bun-plugin-svelte/README.md b/packages/bun-plugin-svelte/README.md index b599c9ecb0..217d1d8cd3 100644 --- a/packages/bun-plugin-svelte/README.md +++ b/packages/bun-plugin-svelte/README.md @@ -8,7 +8,7 @@ The official [Svelte](https://svelte.dev/) plugin for [Bun](https://bun.sh/). ## Installation ```sh -bun add -D bun-plugin-svelte +$ bun add -D bun-plugin-svelte ``` ## Dev Server Usage @@ -16,52 +16,25 @@ bun add -D bun-plugin-svelte `bun-plugin-svelte` integrates with Bun's [Fullstack Dev Server](https://bun.sh/docs/bundler/fullstack), giving you HMR when developing your Svelte app. -```html - - - - - - -
- - +Start by registering it in your [bunfig.toml](https://bun.sh/docs/runtime/bunfig): + +```toml +[serve.static] +plugins = ["bun-plugin-svelte"] ``` -```ts -// index.ts +Then start your dev server: -import { mount, unmount } from "svelte"; -import App from "./App.svelte"; - -// mount the application entrypoint to the DOM -const root = document.getElementById("root")!; -const app = mount(App, { target: root }); +``` +$ bun index.html ``` -```svelte - - - - -
-

Cookin up apps with {name}

-
- - -``` +See the [example](https://github.com/oven-sh/bun/tree/main/packages/bun-plugin-svelte/example) for a complete example. ## Bundler Usage +`bun-plugin-svelte` lets you bundle Svelte components with [`Bun.build`](https://bun.sh/docs/bundler). + ```ts // build.ts // to use: bun run build.ts @@ -70,7 +43,7 @@ import { SveltePlugin } from "bun-plugin-svelte"; // NOTE: not published to npm Bun.build({ entrypoints: ["src/index.ts"], outdir: "dist", - target: "browser", // use "bun" or "node" to use Svelte components server-side + target: "browser", sourcemap: true, // sourcemaps not yet supported plugins: [ SveltePlugin({ diff --git a/packages/bun-plugin-svelte/example/App.svelte b/packages/bun-plugin-svelte/example/App.svelte new file mode 100644 index 0000000000..a73279986b --- /dev/null +++ b/packages/bun-plugin-svelte/example/App.svelte @@ -0,0 +1,311 @@ + + +
+
+
+ +
+

bun-plugin-svelte

+

The official Svelte plugin for Bun

+ + +
+ +
+

🏃‍➡️ Quick Start

+ +
+
+

1. Install from NPM

+
bun add -D bun-plugin-svelte
+
+
+

2. Add it to your bunfig.toml

+

+[serve.static]
+plugins = ["bun-plugin-svelte"];
+        
+
+
+
+ +
+

✨ Features

+
+ + Integrates with Bun's Fullstack Dev Server for hot module replacement + + + Bundle Svelte components with Bun.build + +
+ +
+

📖 Resources

+ +
+ + +
+
+ + diff --git a/packages/bun-plugin-svelte/example/FeatureCard.svelte b/packages/bun-plugin-svelte/example/FeatureCard.svelte new file mode 100644 index 0000000000..b3578018e8 --- /dev/null +++ b/packages/bun-plugin-svelte/example/FeatureCard.svelte @@ -0,0 +1,28 @@ + + +
+

+ + {title} + +

+

+ {@render children()} +

+
+ + diff --git a/packages/bun-plugin-svelte/example/bunfig.toml b/packages/bun-plugin-svelte/example/bunfig.toml new file mode 100644 index 0000000000..af8251def1 --- /dev/null +++ b/packages/bun-plugin-svelte/example/bunfig.toml @@ -0,0 +1,2 @@ +[serve.static] +plugins = ["bun-plugin-svelte"] diff --git a/packages/bun-plugin-svelte/example/index.html b/packages/bun-plugin-svelte/example/index.html new file mode 100644 index 0000000000..7933124600 --- /dev/null +++ b/packages/bun-plugin-svelte/example/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + +
+ + diff --git a/packages/bun-plugin-svelte/example/index.ts b/packages/bun-plugin-svelte/example/index.ts new file mode 100644 index 0000000000..4c97b23f31 --- /dev/null +++ b/packages/bun-plugin-svelte/example/index.ts @@ -0,0 +1,29 @@ +import { mount, unmount } from "svelte"; +import App from "./App.svelte"; + +declare global { + var didMount: boolean | undefined; + var hljs: any; +} + +let app: Record | undefined; + +// mount the application entrypoint to the DOM on first load. On subsequent hot +// updates, the app will be unmounted and re-mounted via the accept handler. + +const root = document.getElementById("root")!; +if (!globalThis.didMount) { + app = mount(App, { target: root }); +} +globalThis.didMount = true; + +if (import.meta.hot) { + import.meta.hot.accept(async () => { + // avoid unmounting twice when another update gets accepted while outros are playing + if (!app) return; + const prevApp = app; + app = undefined; + await unmount(prevApp, { outro: true }); + app = mount(App, { target: root }); + }); +} diff --git a/packages/bun-plugin-svelte/package.json b/packages/bun-plugin-svelte/package.json index 90cbccfd53..12fcfcd4df 100644 --- a/packages/bun-plugin-svelte/package.json +++ b/packages/bun-plugin-svelte/package.json @@ -1,6 +1,6 @@ { "name": "bun-plugin-svelte", - "version": "0.0.1", + "version": "0.0.5", "description": "Official Svelte plugin for Bun", "repository": { "type": "git", @@ -16,7 +16,9 @@ ".": "./src/index.ts" }, "scripts": { + "example": "bun --config=./example/bunfig.toml example/index.html", "lint": "oxlint .", + "fmt": "prettier --write .", "check:types": "tsc --noEmit", "build:types": "tsc --emitDeclarationOnly --declaration --declarationDir ./dist" }, diff --git a/packages/bun-plugin-svelte/src/plugin.ts b/packages/bun-plugin-svelte/src/plugin.ts new file mode 100644 index 0000000000..0f8ce75221 --- /dev/null +++ b/packages/bun-plugin-svelte/src/plugin.ts @@ -0,0 +1,72 @@ +import { basename } from "node:path"; +import type { BuildConfig, Transpiler } from "bun"; +import { getBaseCompileOptions, getBaseModuleCompileOptions, hash, type SvelteOptions } from "./options"; +import { compile, type CompileOptions, type CompileResult, type ModuleCompileOptions } from "svelte/compiler"; + +const virtualNamespace = "bun-svelte"; + +export class SveltePluginService { + private compileOptions: CompileOptions; + private compileModuleOptions: ModuleCompileOptions; + + private ts: Transpiler; + /** Virtual CSS modules created by Svelte components. Keys are import specifiers. */ + private css: Map; + + constructor(options: SvelteOptions, config: Partial) { + this.compileOptions = getBaseCompileOptions(options, config); + this.compileModuleOptions = getBaseModuleCompileOptions(options, config); + + this.ts = new Bun.Transpiler({ loader: "ts" }); + this.css = new Map(); + } + + public compileComponent(source: string, filename: string, hmr: boolean, side?: "client" | "server"): CompileResult { + const generate = this.compileOptions.generate ?? side; + const result = compile(source, { + ...this.compileOptions, + generate, + filename, + hmr, + }); + + var { js, css } = result; + if (css?.code && generate != "server") { + const uid = `${basename(filename)}-${hash(filename)}-style`.replaceAll(`"`, `'`); + const virtualName = virtualNamespace + ":" + uid + ".css"; + this.css.set(virtualName, { sourcePath: filename, source: css.code }); + js.code += `\nimport "${virtualName}";`; + } + + return result; + } + + public async compileModule(source: string, filename: string, side?: "client" | "server"): Promise { + const generate = this.compileModuleOptions.generate ?? side; + if (filename.endsWith("ts")) { + source = await this.ts.transform(source); + } + + // NOTE: we assume js/ts modules won't have CSS blocks in them, so no + // virtual modules get created. + return compile(source, { + ...this.compileModuleOptions, + generate, + filename, + }); + } + + public takeVirtualCSSModule(path: string): VirtualCSSModule { + const mod = this.css.get(path); + if (!mod) throw new Error("Virtual CSS module not found: " + path); + this.css.delete(path); + return mod; + } +} + +type VirtualCSSModule = { + /** Path to the svelte file whose css this is for */ + sourcePath: string; + /** Source code */ + source: string; +};