mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
feat: support Svelte in bundler and dev server (#17735)
This commit is contained in:
55
.github/workflows/packages-ci.yml
vendored
Normal file
55
.github/workflows/packages-ci.yml
vendored
Normal 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
34
packages/bun-plugin-svelte/.gitignore
vendored
Normal 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
|
||||
86
packages/bun-plugin-svelte/README.md
Normal file
86
packages/bun-plugin-svelte/README.md
Normal 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.
|
||||
65
packages/bun-plugin-svelte/bun.lock
Normal file
65
packages/bun-plugin-svelte/bun.lock
Normal 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=="],
|
||||
}
|
||||
}
|
||||
32
packages/bun-plugin-svelte/package.json
Normal file
32
packages/bun-plugin-svelte/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
17
packages/bun-plugin-svelte/src/index.spec.ts
Normal file
17
packages/bun-plugin-svelte/src/index.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
98
packages/bun-plugin-svelte/src/index.ts
Normal file
98
packages/bun-plugin-svelte/src/index.ts
Normal 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 };
|
||||
4
packages/bun-plugin-svelte/src/modules.d.ts
vendored
Normal file
4
packages/bun-plugin-svelte/src/modules.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "*.svelte" {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
45
packages/bun-plugin-svelte/src/options.spec.ts
Normal file
45
packages/bun-plugin-svelte/src/options.spec.ts
Normal 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",
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
91
packages/bun-plugin-svelte/src/options.ts
Normal file
91
packages/bun-plugin-svelte/src/options.ts
Normal 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);
|
||||
@@ -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
|
||||
};
|
||||
"
|
||||
`;
|
||||
19
packages/bun-plugin-svelte/test/fixtures/foo.svelte
vendored
Normal file
19
packages/bun-plugin-svelte/test/fixtures/foo.svelte
vendored
Normal 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>
|
||||
88
packages/bun-plugin-svelte/test/index.test.ts
Normal file
88
packages/bun-plugin-svelte/test/index.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
34
packages/bun-plugin-svelte/tsconfig.json
Normal file
34
packages/bun-plugin-svelte/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
40
packages/bun-types/bun.d.ts
vendored
40
packages/bun-types/bun.d.ts
vendored
@@ -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(
|
||||
/**
|
||||
|
||||
@@ -578,6 +578,7 @@ pub const JSBundler = struct {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Bun.build(config)`
|
||||
pub fn buildFn(
|
||||
globalThis: *JSC.JSGlobalObject,
|
||||
callframe: *JSC.CallFrame,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!--]-->"
|
||||
,
|
||||
}
|
||||
`;
|
||||
66
test/integration/svelte/client-side.test.ts
Normal file
66
test/integration/svelte/client-side.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
15
test/integration/svelte/fixtures/app/App.svelte
Normal file
15
test/integration/svelte/fixtures/app/App.svelte
Normal 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>
|
||||
2
test/integration/svelte/fixtures/app/bunfig.toml
Normal file
2
test/integration/svelte/fixtures/app/bunfig.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[serve.static]
|
||||
plugins = ["bun-plugin-svelte"]
|
||||
10
test/integration/svelte/fixtures/app/index.html
Normal file
10
test/integration/svelte/fixtures/app/index.html
Normal 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>
|
||||
6
test/integration/svelte/fixtures/app/index.ts
Normal file
6
test/integration/svelte/fixtures/app/index.ts
Normal 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 });
|
||||
27
test/integration/svelte/fixtures/client-code-on-server.ts
Normal file
27
test/integration/svelte/fixtures/client-code-on-server.ts
Normal 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();
|
||||
@@ -0,0 +1,2 @@
|
||||
import { SveltePlugin } from "bun-plugin-svelte";
|
||||
Bun.plugin(SveltePlugin({ development: process.env.NODE_ENV === "development" }));
|
||||
9
test/integration/svelte/fixtures/server-imports.ts
Normal file
9
test/integration/svelte/fixtures/server-imports.ts
Normal 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();
|
||||
44
test/integration/svelte/fixtures/todo-list.svelte
Normal file
44
test/integration/svelte/fixtures/todo-list.svelte
Normal 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>
|
||||
74
test/integration/svelte/server-side.test.ts
Normal file
74
test/integration/svelte/server-side.test.ts
Normal 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()", () => {
|
||||
|
||||
|
||||
// // });
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user