feat: support Svelte in bundler and dev server (#17735)

This commit is contained in:
Don Isaac
2025-03-04 14:16:18 -08:00
committed by GitHub
parent 4ef7a43939
commit a41d773aaa
33 changed files with 1123 additions and 16 deletions

55
.github/workflows/packages-ci.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Packages CI
on:
push:
branches:
- main
paths:
- "packages/**"
- .prettierrc
- .prettierignore
- tsconfig.json
- oxlint.json
- "!**/*.md"
pull_request:
branches:
- main
paths:
- "packages/**"
- .prettierrc
- .prettierignore
- tsconfig.json
- oxlint.json
- "!**/*.md"
env:
BUN_VERSION: "canary"
jobs:
bun-plugin-svelte:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/actions/setup-bun
with:
bun-version: ${{ env.BUN_VERSION }}
- name: Install dependencies
run: |
bun install
pushd ./packages/bun-plugin-svelte && bun install
- name: Lint
run: |
bunx oxlint@0.15 --format github --deny-warnings
bunx prettier --config ../../.prettierrc --check .
working-directory: ./packages/bun-plugin-svelte
- name: Check types
run: bun check:types
working-directory: ./packages/bun-plugin-svelte
- name: Test
run: bun test
working-directory: ./packages/bun-plugin-svelte

34
packages/bun-plugin-svelte/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@@ -0,0 +1,86 @@
<p align="center">
<a href="https://bun.sh"><img src="https://github.com/user-attachments/assets/50282090-adfd-4ddb-9e27-c30753c6b161" alt="Logo" height=170></a>
</p>
<h1 align="center"><code>bun-plugin-svelte</code></h1>
The official [Svelte](https://svelte.dev/) plugin for [Bun](https://bun.sh/).
## Installation
```sh
bun add -D bun-plugin-svelte
```
## Dev Server Usage
`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
<!-- index.html -->
<html>
<head>
<script type="module" src="./index.ts"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
```
```ts
// index.ts
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 });
```
```svelte
<!-- App.svelte -->
<script lang="ts">
// out-of-the-box typescript support
let name: string = "Bun";
</script>
<main class="app">
<h1>Cookin up apps with {name}</h1>
</main>
<style>
h1 {
color: #ff3e00;
text-align: center;
font-size: 2em;
}
</style>
```
## Bundler Usage
```ts
// build.ts
// to use: bun run build.ts
import { SveltePlugin } from "bun-plugin-svelte"; // NOTE: not published to npm yet
Bun.build({
entrypoints: ["src/index.ts"],
outdir: "dist",
target: "browser", // use "bun" or "node" to use Svelte components server-side
sourcemap: true, // sourcemaps not yet supported
plugins: [
SveltePlugin({
development: true, // turn off for prod builds. Defaults to false
}),
],
});
```
## Server-Side Usage
`bun-plugin-svelte` does not yet support server-side imports (e.g. for SSR).
This will be added in the near future.

View File

@@ -0,0 +1,65 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "bun-plugin-svelte",
"devDependencies": {
"bun-types": "canary",
"svelte": "^5.20.4",
},
"peerDependencies": {
"svelte": "^5",
"typescript": "^5",
},
},
},
"packages": {
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
"@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"acorn-typescript": ["acorn-typescript@1.4.13", "", { "peerDependencies": { "acorn": ">=8.9.0" } }, "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"bun-types": ["bun-types@1.2.4-canary.20250226T140704", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P8b2CGLtbvi/kQ4dPHBhU5qkguIjHMYCjNqjWDTKSnodWDTbcv9reBdktZJ7m5SF4m15JLthfFq2PtwKpA9a+w=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"esrap": ["esrap@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"svelte": ["svelte@5.20.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "acorn-typescript": "^1.4.13", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.3", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2Mo/AfObaw9zuD0u1JJ7sOVzRCGcpETEyDkLbtkcctWpCMCIyT0iz83xD8JT29SR7O4SgswuPRIDYReYF/607A=="],
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
}
}

View File

@@ -0,0 +1,32 @@
{
"name": "bun-plugin-svelte",
"version": "0.0.1",
"type": "module",
"module": "src/index.ts",
"index": "src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"lint": "oxlint .",
"check:types": "tsc --noEmit",
"build:types": "tsc --emitDeclarationOnly --declaration --declarationDir ./dist"
},
"devDependencies": {
"bun-types": "canary",
"svelte": "^5.20.4"
},
"peerDependencies": {
"typescript": "^5",
"svelte": "^5"
},
"files": [
"README.md",
"bunfig.toml",
"tsconfig.json",
"modules.d.ts",
"dist",
"src",
"!src/**/*.spec.ts"
]
}

View File

@@ -0,0 +1,17 @@
import { describe, it, expect } from "bun:test";
import { SveltePlugin } from "./index";
describe("SveltePlugin", () => {
it.each([true, false, 0, 1, "hi"])("throws if passed a non-object (%p)", (badOptions: any) => {
expect(() => SveltePlugin(badOptions)).toThrow(TypeError);
});
it("may be nullish or not provided", () => {
expect(() => SveltePlugin()).not.toThrow();
expect(() => SveltePlugin(null as any)).not.toThrow();
expect(() => SveltePlugin(undefined)).not.toThrow();
});
it.each([null, 1, "hi", {}, "Client"])("throws if forceSide is not 'client' or 'server' (%p)", (forceSide: any) => {
expect(() => SveltePlugin({ forceSide })).toThrow(TypeError);
});
});

View File

@@ -0,0 +1,98 @@
import type { BunPlugin, BuildConfig, OnLoadResult } from "bun";
import { basename } from "node:path";
import { compile, compileModule } from "svelte/compiler";
import { getBaseCompileOptions, validateOptions, type SvelteOptions, hash } from "./options";
const kEmptyObject = Object.create(null);
const virtualNamespace = "bun-svelte";
function SveltePlugin(options: SvelteOptions = kEmptyObject as SvelteOptions): BunPlugin {
if (options != null) validateOptions(options);
/**
* import specifier -> CSS source code
*/
const virtualCssModules = new Map<string, VirtualCSSModule>();
type VirtualCSSModule = {
/** Path to the svelte file whose css this is for */
sourcePath: string;
/** Source code */
source: string;
};
return {
name: "bun-plugin-svelte",
setup(builder) {
const { config = kEmptyObject as Partial<BuildConfig> } = builder;
const baseCompileOptions = getBaseCompileOptions(options ?? (kEmptyObject as Partial<SvelteOptions>), config);
builder
.onLoad({ filter: /\.svelte(?:\.[tj]s)?$/ }, async args => {
const { path } = args;
var isModule = false;
switch (path.substring(path.length - 2)) {
case "js":
case "ts":
isModule = true;
break;
}
const sourceText = await Bun.file(path).text();
const side =
args && "side" in args // "side" only passed when run from dev server
? (args as { side: "client" | "server" }).side
: "server";
const hmr = Boolean((args as { hmr?: boolean })["hmr"] ?? process.env.NODE_ENV !== "production");
const generate = baseCompileOptions.generate ?? side;
const compileFn = isModule ? compileModule : compile;
const result = compileFn(sourceText, {
...baseCompileOptions,
generate,
filename: args.path,
hmr,
});
var { js, css } = result;
if (css?.code && generate != "server") {
const uid = `${basename(path)}-${hash(path)}-style`.replaceAll(`"`, `'`);
const virtualName = virtualNamespace + ":" + uid + ".css";
virtualCssModules.set(virtualName, { sourcePath: path, source: css.code });
js.code += `\nimport "${virtualName}";`;
}
return {
contents: result.js.code,
loader: "js",
} satisfies OnLoadResult;
// TODO: allow plugins to return multiple results.
// TODO: support layered sourcemaps
})
.onResolve({ filter: /^bun-svelte:/ }, args => {
return {
path: args.path,
namespace: "bun-svelte",
};
})
.onLoad({ filter: /\.css$/, namespace: virtualNamespace }, args => {
const { path } = args;
const mod = virtualCssModules.get(path);
if (!mod) throw new Error("Virtual CSS module not found: " + path);
const { sourcePath, source } = mod;
virtualCssModules.delete(path);
return {
contents: source,
loader: "css",
watchFiles: [sourcePath],
};
});
},
};
}
export default SveltePlugin({ development: true }) as BunPlugin;
export { SveltePlugin, type SvelteOptions };

View File

@@ -0,0 +1,4 @@
declare module "*.svelte" {
const content: any;
export default content;
}

View File

@@ -0,0 +1,45 @@
import { describe, beforeAll, it, expect } from "bun:test";
import type { BuildConfig } from "bun";
import type { CompileOptions } from "svelte/compiler";
import { getBaseCompileOptions, type SvelteOptions } from "./options";
describe("getBaseCompileOptions", () => {
describe("when no options are provided", () => {
const pluginOptions: SvelteOptions = {};
let fullDefault: Readonly<CompileOptions>;
beforeAll(() => {
fullDefault = Object.freeze(getBaseCompileOptions(pluginOptions, {}));
});
it("when minification is disabled, whitespace and comments are preserved", () => {
expect(getBaseCompileOptions(pluginOptions, { minify: false })).toEqual(
expect.objectContaining({
preserveWhitespace: true,
preserveComments: true,
}),
);
});
it("defaults to production mode", () => {
expect(fullDefault.dev).toBeFalse();
});
});
it.each([{}, { side: "server" }, { side: "client" }, { side: undefined }] as Partial<BuildConfig>[])(
"when present, forceSide takes precedence over config (%o)",
buildConfig => {
expect(getBaseCompileOptions({ forceSide: "client" }, buildConfig)).toEqual(
expect.objectContaining({
generate: "client",
}),
);
expect(getBaseCompileOptions({ forceSide: "server" }, buildConfig)).toEqual(
expect.objectContaining({
generate: "server",
}),
);
},
);
});

View File

@@ -0,0 +1,91 @@
import { strict as assert } from "node:assert";
import type { BuildConfig } from "bun";
import type { CompileOptions } from "svelte/compiler";
export interface SvelteOptions {
/**
* Force client-side or server-side generation.
*
* By default, this plugin will detect the side of the build based on how
* it's used. For example, `"client"` code will be generated when used with {@link Bun.build}.
*/
forceSide?: "client" | "server";
/**
* When `true`, this plugin will generate development-only checks and other
* niceties.
*
* When `false`, this plugin will generate production-ready code
*
* Defaults to `true` when run via Bun's dev server, `false` otherwise.
*/
development?: boolean;
}
/**
* @internal
*/
export function validateOptions(options: unknown): asserts options is SvelteOptions {
assert(options && typeof options === "object", new TypeError("bun-svelte-plugin: options must be an object"));
if ("forceSide" in options) {
switch (options.forceSide) {
case "client":
case "server":
break;
default:
throw new TypeError(
`bun-svelte-plugin: forceSide must be either 'client' or 'server', got ${options.forceSide}`,
);
}
}
}
/**
* @internal
*/
export function getBaseCompileOptions(pluginOptions: SvelteOptions, config: Partial<BuildConfig>): CompileOptions {
let { forceSide, development = false } = pluginOptions;
const { minify = false, target } = config;
const shouldMinify = Boolean(minify);
const {
whitespace: minifyWhitespace,
syntax: _minifySyntax,
identifiers: _minifyIdentifiers,
} = typeof minify === "object"
? minify
: {
whitespace: shouldMinify,
syntax: shouldMinify,
identifiers: shouldMinify,
};
if (forceSide == null && typeof target === "string") {
switch (target) {
case "browser":
forceSide = "client";
break;
case "node":
case "bun":
forceSide = "server";
break;
default:
// warn? throw?
}
}
return {
css: "external",
generate: forceSide,
preserveWhitespace: !minifyWhitespace,
preserveComments: !shouldMinify,
dev: development,
cssHash({ css }) {
// same prime number seed used by svelte/compiler.
// TODO: ensure this provides enough entropy
return `svelte-${hash(css)}`;
},
};
}
export const hash = (content: string): string => Bun.hash(content, 5381).toString(36);

View File

@@ -0,0 +1,91 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`Bun.plugin using { forceSide: 'server' } allows for imported components to be SSR'd: foo.svelte - head 1`] = `""`;
exports[`Bun.plugin using { forceSide: 'server' } allows for imported components to be SSR'd: foo.svelte - body 1`] = `
"<!--[--><!---->
<main class="app svelte-30r5b3lexyb64">
<h1 class="svelte-30r5b3lexyb64">Hello World!</h1>
</main>
<!--]-->"
`;
exports[`Bun.plugin Generates server-side code: foo.svelte - head 1`] = `""`;
exports[`Bun.plugin Generates server-side code: foo.svelte - body 1`] = `
"<!--[--><!---->
<main class="app svelte-30r5b3lexyb64">
<h1 class="svelte-30r5b3lexyb64">Hello World!</h1>
</main>
<!--]-->"
`;
exports[`Bun.build Generates server-side code when targeting "node" or "bun": foo.svelte - server-side (node) 1`] = `
{
"body":
"<!--[--><!---->
<main class="app svelte-30r5b3lexyb64">
<h1 class="svelte-30r5b3lexyb64">Hello World!</h1>
</main>
<!--]-->"
,
"head": "",
"html":
"<!--[--><!---->
<main class="app svelte-30r5b3lexyb64">
<h1 class="svelte-30r5b3lexyb64">Hello World!</h1>
</main>
<!--]-->"
,
}
`;
exports[`Bun.build Generates server-side code when targeting "node" or "bun": foo.svelte - server-side (bun) 1`] = `
{
"body":
"<!--[--><!---->
<main class="app svelte-30r5b3lexyb64">
<h1 class="svelte-30r5b3lexyb64">Hello World!</h1>
</main>
<!--]-->"
,
"head": "",
"html":
"<!--[--><!---->
<main class="app svelte-30r5b3lexyb64">
<h1 class="svelte-30r5b3lexyb64">Hello World!</h1>
</main>
<!--]-->"
,
}
`;
exports[`Bun.build Generates client-side code when targeting 'browser': foo.svelte - client-side 1`] = `
"// test/fixtures/foo.svelte
var foo_default = "./foo-y5ajevk1.svelte";
export {
foo_default as default
};
"
`;
exports[`Bun.build Generates client-side code when targeting 'browser': foo.svelte - client-side index 1`] = `
"// test/fixtures/foo.svelte
var foo_default = "./foo-y5ajevk1.svelte";
export {
foo_default as default
};
"
`;

View File

@@ -0,0 +1,19 @@
<script>
let name = "World";
</script>
<main class="app">
<h1>Hello {name}!</h1>
</main>
<style>
h1 {
color: #ff3e00;
text-align: center;
font-size: 2em;
font-weight: 100;
}
.app {
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,88 @@
import { describe, beforeAll, it, expect, afterEach, afterAll } from "bun:test";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
import { render } from "svelte/server";
import { SveltePlugin } from "../src";
const fixturePath = (...segs: string[]) => path.join(import.meta.dirname, "fixtures", ...segs);
// temp dir that gets deleted after all tests
let outdir: string;
beforeAll(() => {
const prefix = `svelte-test-${Math.random().toString(36).substring(2, 15)}`;
outdir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
});
afterAll(() => {
try {
fs.rmSync(outdir, { recursive: true, force: true });
} catch {
// suppress
}
});
it("hello world component", async () => {
const res = await Bun.build({
entrypoints: [fixturePath("foo.svelte")],
outdir,
plugins: [SveltePlugin()],
});
expect(res.success).toBeTrue();
});
describe("Bun.build", () => {
it.each(["node", "bun"] as const)('Generates server-side code when targeting "node" or "bun"', async target => {
const res = await Bun.build({
entrypoints: [fixturePath("foo.svelte")],
outdir,
target,
plugins: [SveltePlugin({ forceSide: "server" })],
});
expect(res.success).toBeTrue();
const componentPath = res.outputs[0].path;
const component = await import(componentPath);
expect(component.default).toBeTypeOf("function");
expect(render(component.default)).toMatchSnapshot(`foo.svelte - server-side (${target})`);
});
it("Generates client-side code when targeting 'browser'", async () => {
const res = await Bun.build({
entrypoints: [fixturePath("foo.svelte")],
outdir,
target: "browser",
});
expect(res.success).toBeTrue();
const componentPath = path.resolve(res.outputs[0].path);
const entrypoint = await res.outputs[0].text();
expect(entrypoint).toMatchSnapshot(`foo.svelte - client-side index`);
expect(await Bun.file(componentPath).text()).toMatchSnapshot(`foo.svelte - client-side`);
});
});
describe("Bun.plugin", () => {
afterEach(() => {
Bun.plugin.clearAll();
});
// test.only("using { forceSide: 'server' } allows for imported components to be SSR'd", async () => {
it("Generates server-side code", async () => {
Bun.plugin(SveltePlugin());
const foo = await import(fixturePath("foo.svelte"));
expect(foo).toBeTypeOf("object");
expect(foo).toHaveProperty("default");
const actual = render(foo.default);
expect(actual).toEqual(
expect.objectContaining({
head: expect.any(String),
body: expect.any(String),
}),
);
expect(actual.head).toMatchSnapshot("foo.svelte - head");
expect(actual.body).toMatchSnapshot("foo.svelte - body");
});
});

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"emitDeclarationOnly": true,
// Best practices
"strict": true,
"strictNullChecks": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"stripInternal": true,
// thank you Titian
"isolatedDeclarations": true,
"declaration": true,
"declarationMap": true,
// Some stricter flags (disabled by default)
"noImplicitAny": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@@ -2603,9 +2603,15 @@ declare module "bun" {
kind: ImportKind;
}
/**
* @see [Bun.build API docs](https://bun.sh/docs/bundler#api)
*/
interface BuildConfig {
entrypoints: string[]; // list of file path
outdir?: string; // output directory
/**
* @default "browser"
*/
target?: Target; // default: "browser"
/**
* Output module format. Top-level await is only supported for `"esm"`.
@@ -2649,7 +2655,25 @@ declare module "bun" {
define?: Record<string, string>;
// origin?: string; // e.g. http://mydomain.com
loader?: { [k in string]: Loader };
sourcemap?: "none" | "linked" | "inline" | "external" | "linked" | boolean; // default: "none", true -> "inline"
/**
* Specifies if and how to generate source maps.
*
* - `"none"` - No source maps are generated
* - `"linked"` - A separate `*.ext.map` file is generated alongside each
* `*.ext` file. A `//# sourceMappingURL` comment is added to the output
* file to link the two. Requires `outdir` to be set.
* - `"inline"` - an inline source map is appended to the output file.
* - `"external"` - Generate a separate source map file for each input file.
* No `//# sourceMappingURL` comment is added to the output file.
*
* `true` and `false` are aliasees for `"inline"` and `"none"`, respectively.
*
* @default "none"
*
* @see {@link outdir} required for `"linked"` maps
* @see {@link publicPath} to customize the base url of linked source maps
*/
sourcemap?: "none" | "linked" | "inline" | "external" | "linked" | boolean;
/**
* package.json `exports` conditions used when resolving imports
*
@@ -2678,6 +2702,14 @@ declare module "bun" {
* ```
*/
env?: "inline" | "disable" | `${string}*`;
/**
* Whether to enable minification.
*
* Use `true`/`false` to enable/disable all minification options. Alternatively,
* you can pass an object for granular control over certain minifications.
*
* @default false
*/
minify?:
| boolean
| {
@@ -5764,13 +5796,15 @@ declare module "bun" {
*
* If unspecified, it is assumed that the plugin is compatible with all targets.
*
* This field is not read by Bun.plugin
* This field is not read by {@link Bun.plugin}
*/
target?: Target;
/**
* A function that will be called when the plugin is loaded.
*
* This function may be called in the same tick that it is registered, or it may be called later. It could potentially be called multiple times for different targets.
* This function may be called in the same tick that it is registered, or it
* may be called later. It could potentially be called multiple times for
* different targets.
*/
setup(
/**

View File

@@ -578,6 +578,7 @@ pub const JSBundler = struct {
);
}
/// `Bun.build(config)`
pub fn buildFn(
globalThis: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,

View File

@@ -69,7 +69,7 @@ public:
{
}
VirtualModuleMap* virtualModules = nullptr;
VirtualModuleMap* _Nullable virtualModules = nullptr;
bool mustDoExpensiveRelativeLookup = false;
JSC::EncodedJSValue run(JSC::JSGlobalObject* globalObject, BunString* namespaceString, BunString* path);

View File

@@ -61,6 +61,14 @@ export function loadAndResolvePluginsForServe(
root: bunfig_folder,
};
class InvalidBundlerPluginError extends TypeError {
pluginName: string;
constructor(pluginName: string, reason: string) {
super(`"${pluginName}" is not a valid bundler plugin: ${reason}`);
this.pluginName = pluginName;
}
}
let bundlerPlugin = this;
let promiseResult = (async (
plugins: string[],
@@ -77,9 +85,9 @@ export function loadAndResolvePluginsForServe(
throw new TypeError(`Expected "${plugins[i]}" to be a module which default exports a bundler plugin.`);
}
let pluginModule = pluginModuleRaw.default;
if (!pluginModule || pluginModule.name === undefined || pluginModule.setup === undefined) {
throw new TypeError(`"${plugins[i]}" is not a valid bundler plugin.`);
}
if (!pluginModule) throw new InvalidBundlerPluginError(plugins[i], "default export is missing");
if (pluginModule.name === undefined) throw new InvalidBundlerPluginError(plugins[i], "name is missing");
if (pluginModule.setup === undefined) throw new InvalidBundlerPluginError(plugins[i], "setup() is missing");
onstart_promises_array = await runSetupFn.$apply(bundlerPlugin, [
pluginModule.setup,
config,

View File

@@ -23,6 +23,7 @@
"aws-cdk-lib": "2.148.0",
"axios": "1.6.8",
"body-parser": "1.20.2",
"bun-plugin-svelte": "file:../packages/bun-plugin-svelte",
"bun-plugin-yaml": "0.0.1",
"comlink": "4.4.1",
"commander": "12.1.0",
@@ -72,7 +73,7 @@
"string-width": "7.0.0",
"stripe": "15.4.0",
"supertest": "6.3.3",
"svelte": "5.4.0",
"svelte": "5.20.4",
"type-graphql": "2.0.0-rc.2",
"typeorm": "0.3.20",
"typescript": "5.0.2",
@@ -587,7 +588,7 @@
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
"@types/estree": ["@types/estree@1.0.5", "", {}, "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="],
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
"@types/is-buffer": ["@types/is-buffer@2.0.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-G6OXy83Va+xEo8XgqAJYOuvOMxeey9xM5XKkvwJNmN8rVdcB+r15HvHsG86hl86JvU0y1aa7Z2ERkNFYWw9ySg=="],
@@ -867,6 +868,9 @@
"buffer-writer": ["buffer-writer@2.0.0", "", {}, "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="],
"bun-plugin-svelte": ["bun-plugin-svelte@file:../packages/bun-plugin-svelte", { "dependencies": { "svelte": "^5.20.4", "svelte-hmr": "^0.16.0" }, "devDependencies": { "bun-types": "canary" }, "peerDependencies": { "typescript": "^5" } }],
"bun-types": ["bun-types@1.2.4-canary.20250226T140704", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P8b2CGLtbvi/kQ4dPHBhU5qkguIjHMYCjNqjWDTKSnodWDTbcv9reBdktZJ7m5SF4m15JLthfFq2PtwKpA9a+w=="],
"bun-plugin-yaml": ["bun-plugin-yaml@0.0.1", "", { "dependencies": { "js-yaml": "^4.1.0" } }, "sha512-dAqe0eJu+SGtrwp75hrtDHWRnmMUdBJK365PPeM0skwFqWu6FYouVzaluG2FVVgs0NsKd+sXiWyGqvs0cilrkA=="],
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
@@ -915,6 +919,8 @@
"clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -1089,7 +1095,7 @@
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"esrap": ["esrap@1.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1" } }, "sha512-ZlQmCCK+n7SGoqo7DnfKaP1sJZa49P01/dXzmjCASSo04p72w8EksT2NMK8CEX8DhKsfJXANioIw8VyHNsBfvQ=="],
"esrap": ["esrap@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
@@ -1987,7 +1993,9 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"svelte": ["svelte@5.4.0", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "acorn-typescript": "^1.4.13", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "esm-env": "^1.2.1", "esrap": "^1.2.3", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2I/mjD8cXDpKfdfUK+T6yo/OzugMXIm8lhyJUFM5F/gICMYnkl3C/+4cOSpia8TqpDsi6Qfm5+fdmBNMNmaf2g=="],
"svelte": ["svelte@5.20.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "acorn-typescript": "^1.4.13", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.3", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2Mo/AfObaw9zuD0u1JJ7sOVzRCGcpETEyDkLbtkcctWpCMCIyT0iz83xD8JT29SR7O4SgswuPRIDYReYF/607A=="],
"svelte-hmr": ["svelte-hmr@0.16.0", "", { "peerDependencies": { "svelte": "^3.19.0 || ^4.0.0" } }, "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA=="],
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
@@ -2291,6 +2299,10 @@
"@testing-library/react/react": ["react@file:../node_modules/react", {}],
"@types/eslint/@types/estree": ["@types/estree@1.0.5", "", {}, "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="],
"@types/eslint-scope/@types/estree": ["@types/estree@1.0.5", "", {}, "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="],
"@verdaccio/auth/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"@verdaccio/commons-api/http-status-codes": ["http-status-codes@2.2.0", "", {}, "sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng=="],
@@ -2453,8 +2465,6 @@
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"is-reference/@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
"jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"jest-diff/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
@@ -2651,6 +2661,8 @@
"vitest/magic-string": ["magic-string@0.30.10", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ=="],
"webpack/@types/estree": ["@types/estree@1.0.5", "", {}, "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="],
"webpack/acorn": ["acorn@8.12.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw=="],
"webpack-cli/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],

View File

@@ -0,0 +1,38 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`When bun-plugin-svelte is enabled via Bun.plugin() can be render()-ed 1`] = `
{
"body": Any<String>,
"head": Any<String>,
"html":
"<!--[--><!-- source: https://svelte.dev/playground/7eb8c1dd6cac414792b0edb53521ab49?version=5.20.4 -->
<main>
<h1 class="title svelte-y13uzakzmn8w">Todo List</h1>
<input value="" type="text" placeholder="new todo item..">
<button>Add</button>
<br>
<!--[--><!---->
<input checked type="checkbox">
<span class="svelte-y13uzakzmn8w checked">Write my first post</span>
<span role="button">❌</span>
<br>
<!---->
<input type="checkbox">
<span class="svelte-y13uzakzmn8w">Upload the post to the blog</span>
<span role="button">❌</span>
<br>
<!---->
<input type="checkbox">
<span class="svelte-y13uzakzmn8w">Publish the post at Facebook</span>
<span role="button">❌</span>
<br>
<!--]-->
</main>
<!--]-->"
,
}
`;

View File

@@ -0,0 +1,66 @@
import path from "node:path";
import { bunEnv, bunExe, tmpdirSync } from "harness";
import { promises as fs, statSync } from "node:fs";
import { Subprocess } from "bun";
const fixturePath = (...segs: string[]): string => path.join(import.meta.dirname, "fixtures", ...segs);
beforeAll(async () => {
const pluginDir = path.resolve(import.meta.dirname, "..", "..", "..", "packages", "bun-plugin-svelte");
expect(statSync(pluginDir).isDirectory()).toBeTrue();
Bun.spawnSync([bunExe(), "install"], {
cwd: pluginDir,
stdio: ["ignore", "ignore", "ignore"],
env: bunEnv,
});
});
describe("generating client-side code", () => {
test("Bundling Svelte components", async () => {
const outdir = tmpdirSync("bun-svelte-client-side");
const { SveltePlugin } = await import("bun-plugin-svelte");
try {
const result = await Bun.build({
entrypoints: [fixturePath("app/index.ts")],
outdir,
sourcemap: "inline",
minify: true,
target: "browser",
plugins: [SveltePlugin({ development: true })],
});
expect(result.success).toBeTrue();
const entrypoint = result.outputs.find(o => o.kind === "entry-point");
expect(entrypoint).toBeDefined();
} finally {
await fs.rm(outdir, { force: true, recursive: true });
}
});
describe("Using Svelte components in Bun's dev server", () => {
let server: Subprocess;
beforeAll(async () => {
server = Bun.spawn([bunExe(), "./index.html"], {
env: {
...bunEnv,
NODE_ENV: "development",
},
cwd: fixturePath("app"),
stdio: ["ignore", "inherit", "inherit"],
});
await Bun.sleep(500);
});
afterAll(() => {
server?.kill();
});
it("serves the app", async () => {
const response = await fetch("http://localhost:3000");
await console.log(await response.text());
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toMatch("text/html");
});
});
});

View File

@@ -0,0 +1,15 @@
<script lang="ts">
let count: number = 0;
</script>
<main>
<h1 class="title">Svelte App</h1>
<button on:click={() => count++}>Click me</button>
<p>Count: {count}</p>
</main>
<style>
.title {
color: red;
}
</style>

View File

@@ -0,0 +1,2 @@
[serve.static]
plugins = ["bun-plugin-svelte"]

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>Svelte App</title>
<script type="module" src="./index.ts"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,6 @@
import { mount } from "svelte";
import App from "./App.svelte";
// import App from "../todo-list.svelte";
const root = document.body.appendChild(document.createElement("div"));
mount(App, { target: root });

View File

@@ -0,0 +1,27 @@
/// <reference lib="dom" />
import { SveltePlugin } from "bun-plugin-svelte";
import { Window } from "happy-dom";
import { expect } from "bun:test";
Bun.plugin(SveltePlugin({ forceSide: "client", development: true }));
const { mount } = await import("svelte");
// @ts-ignore
const window = globalThis.window = new Window({
width: 1024,
height: 768,
url: "http://localhost:3000",
});
const document = globalThis.document = window.document as unknown as Document;
const body = document.body;
const root = document.body.appendChild(document.createElement("div"));
const { default: TodoApp } = await import("./todo-list.svelte");
mount(TodoApp, { target: root });
expect(root.innerHTML).not.toBeEmpty();

View File

@@ -0,0 +1,2 @@
import { SveltePlugin } from "bun-plugin-svelte";
Bun.plugin(SveltePlugin({ development: process.env.NODE_ENV === "development" }));

View File

@@ -0,0 +1,9 @@
import { render } from "svelte/server";
import { expect } from "bun:test";
import TodoApp from "./todo-list.svelte";
expect(TodoApp).toBeTypeOf("function");
const result = render(TodoApp);
expect(result).toMatchObject({ head: expect.any(String), body: expect.any(String) });
expect(result.body).not.toBeEmpty();

View File

@@ -0,0 +1,44 @@
<!-- source: https://svelte.dev/playground/7eb8c1dd6cac414792b0edb53521ab49?version=5.20.4 -->
<script>
let newItem = "";
let todoList = [
{ text: "Write my first post", status: true },
{ text: "Upload the post to the blog", status: false },
{ text: "Publish the post at Facebook", status: false },
];
function addToList() {
todoList = [...todoList, { text: newItem, status: false }];
newItem = "";
}
function removeFromList(index) {
todoList.splice(index, 1);
todoList = todoList;
}
</script>
<main>
<h1 class="title">Todo List</h1>
<input bind:value={newItem} type="text" placeholder="new todo item.." />
<button on:click={addToList}>Add</button>
<br />
{#each todoList as item, index}
<input bind:checked={item.status} type="checkbox" />
<span class:checked={item.status}>{item.text}</span>
<span role="button" on:click={() => removeFromList(index)}>❌</span>
<br />
{/each}
</main>
<style>
.title {
font-weight: bold;
}
.checked {
text-decoration: line-through;
}
</style>

View File

@@ -0,0 +1,74 @@
// TODO: full server-side support
// import { SveltePlugin } from "bun-plugin-svelte";
// import { render } from "svelte/server";
// import { bunRun, bunEnv, bunExe } from "harness";
// import path from "path";
// // import { describe, beforeEach, afterEach, it, expect } from "bun:test";
// const fixturePath = (...segs: string[]) => path.join(__dirname, "fixtures", ...segs);
// // await Bun.plugin(SveltePlugin({ development: true }));
// // import TodoApp from "./fixtures/todo-list.svelte";
// // afterAll(() => {
// // Bun.plugin.clearAll();
// // })
// describe("When bun-plugin-svelte is enabled via Bun.plugin()", () => {
// // beforeEach(async () => {
// // await Bun.plugin(SveltePlugin({ development: true }));
// // });
// // afterEach(() => {
// // Bun.plugin.clearAll();
// // });
// it("can render() production builds", async () => {
// const result = Bun.spawnSync([bunExe(), "--preload=./server-imports.preload.ts", "server-imports.ts"], {
// cwd: fixturePath(),
// env: bunEnv,
// });
// if (result.exitCode !== 0) {
// console.error(result.stderr.toString("utf8"));
// throw new Error("rendering failed");
// }
// expect(result.exitCode).toBe(0);
// // const { default: TodoApp } = await import("./fixtures/todo-list.svelte");
// // expect(TodoApp).toBeTypeOf("function");
// // const result = render(TodoApp);
// // expect(result).toMatchObject({ head: expect.any(String), body: expect.any(String) });
// // expect(result).toMatchSnapshot();
// });
// it("can render() development builds", async () => {
// const result = Bun.spawnSync([bunExe(), "--preload=./server-imports.preload.ts", "server-imports.ts"], {
// cwd: fixturePath(),
// env: {
// ...bunEnv,
// NODE_ENV: "development",
// }
// });
// if (result.exitCode !== 0) {
// console.error(result.stderr.toString("utf8"));
// throw new Error("rendering failed");
// }
// expect(result.exitCode).toBe(0);
// // // const { default: TodoApp } = await import("./fixtures/todo-list.svelte");
// // const result = render(TodoApp);
// // expect(result).toMatchObject({ head: expect.any(String), body: expect.any(String) });
// // expect(result).toMatchSnapshot();
// });
// // FIXME: onResolve is not called for CSS imports on server-side
// it.skip("if forced to use client-side generation, could be used with happy-dom in Bun", () => {
// expect(() => bunRun(fixturePath("client-code-on-server.ts"), { NODE_ENV: "development" })).not.toThrow();
// })
// });
// // describe("When using Bun.build()", () => {
// // });

View File

@@ -25,7 +25,7 @@ describe("package.json dependencies must be exact versions", async () => {
// Hyphen is necessary to accept prerelease versions like "1.1.3-alpha.7"
// This regex still forbids semver ranges like "1.0.0 - 1.2.0", as those must have spaces
// around the hyphen.
const okRegex = /^([a-zA-Z0-9\.\-])+$/;
const okRegex = /^(([a-zA-Z0-9\.\-]|)+$|file:)/;
for (const [name, dep] of Object.entries(dependencies)) {
expect(dep, `dependency ${name} specifies non-exact version "${dep}"`).toMatch(okRegex);

View File

@@ -28,6 +28,7 @@
"aws-cdk-lib": "2.148.0",
"axios": "1.6.8",
"body-parser": "1.20.2",
"bun-plugin-svelte": "file:../packages/bun-plugin-svelte",
"bun-plugin-yaml": "0.0.1",
"comlink": "4.4.1",
"commander": "12.1.0",
@@ -77,7 +78,7 @@
"string-width": "7.0.0",
"stripe": "15.4.0",
"supertest": "6.3.3",
"svelte": "5.4.0",
"svelte": "5.20.4",
"type-graphql": "2.0.0-rc.2",
"typeorm": "0.3.20",
"typescript": "5.0.2",

View File

@@ -10,7 +10,6 @@ test.skipIf(isWindows)("verify that we can call sigint 4096 times", () => {
const handler = () => {
count++;
console.count("SIGINT");
if (count === 1024 * 4) {
process.off("SIGINT", handler);
process.exitCode = 0;