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:
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user