feat(bun-plugin-svelte): custom elements (#18336)

This commit is contained in:
Don Isaac
2025-03-24 23:24:56 -07:00
committed by GitHub
parent 1656bca9ab
commit a07844ea13
5 changed files with 77 additions and 18 deletions

View File

@@ -4,12 +4,12 @@
"": {
"name": "bun-plugin-svelte",
"devDependencies": {
"@threlte/core": "8.0.1",
"bun-types": "canary",
"svelte": "^5.20.4",
},
"peerDependencies": {
"svelte": "^5",
"typescript": "^5",
},
},
},
@@ -26,6 +26,8 @@
"@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=="],
"@threlte/core": ["@threlte/core@8.0.1", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "svelte": ">=5", "three": ">=0.155" } }, "sha512-vy1xRQppJFNmfPTeiRQue+KmYFsbPgVhwuYXRTvVrwPeD2oYz43gxUeOpe1FACeGKxrxZykeKJF5ebVvl7gBxw=="],
"@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=="],
@@ -54,9 +56,11 @@
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"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=="],
"three": ["three@0.174.0", "", {}, "sha512-p+WG3W6Ov74alh3geCMkGK9NWuT62ee21cV3jEnun201zodVF4tCE5aZa2U122/mkLRmhJJUQmLLW1BH00uQJQ=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],

View File

@@ -11,7 +11,11 @@ describe("SveltePlugin", () => {
expect(() => SveltePlugin(undefined)).not.toThrow();
});
it.each([null, 1, "hi", {}, "Client"])("throws if forceSide is not 'client' or 'server' (%p)", (forceSide: any) => {
it.each([1, "hi", {}, "Client"])("throws if forceSide is not 'client' or 'server' (%p)", (forceSide: any) => {
expect(() => SveltePlugin({ forceSide })).toThrow(TypeError);
});
it.each([null, undefined])("forceSide may be nullish", (forceSide: any) => {
expect(() => SveltePlugin({ forceSide })).not.toThrow();
});
});

View File

@@ -2,7 +2,7 @@ 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";
import { getBaseCompileOptions, validateOptions, type SvelteOptions } from "./options";
describe("getBaseCompileOptions", () => {
describe("when no options are provided", () => {
@@ -42,4 +42,13 @@ describe("getBaseCompileOptions", () => {
);
},
);
});
}); // getBaseCompileOptions
describe("validateOptions(options)", () => {
it.each(["", 1, null, undefined, true, false, Symbol("hi")])(
"throws if options is not an object (%p)",
(badOptions: any) => {
expect(() => validateOptions(badOptions)).toThrow();
},
);
}); // validateOptions

View File

@@ -2,7 +2,8 @@ import { strict as assert } from "node:assert";
import { type BuildConfig } from "bun";
import type { CompileOptions, ModuleCompileOptions } from "svelte/compiler";
export interface SvelteOptions {
type OverrideCompileOptions = Pick<CompileOptions, "customElement" | "runes" | "modernAst" | "namespace">;
export interface SvelteOptions extends Pick<CompileOptions, "runes"> {
/**
* Force client-side or server-side generation.
*
@@ -20,6 +21,11 @@ export interface SvelteOptions {
* Defaults to `true` when run via Bun's dev server, `false` otherwise.
*/
development?: boolean;
/**
* Options to forward to the Svelte compiler.
*/
compilerOptions?: OverrideCompileOptions;
}
/**
@@ -27,15 +33,24 @@ export interface SvelteOptions {
*/
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) {
const opts = options as Record<keyof SvelteOptions, unknown>;
if (opts.forceSide != null) {
if (typeof opts.forceSide !== "string") {
throw new TypeError("bun-svelte-plugin: forceSide must be a string, got " + typeof opts.forceSide);
}
switch (opts.forceSide) {
case "client":
case "server":
break;
default:
throw new TypeError(
`bun-svelte-plugin: forceSide must be either 'client' or 'server', got ${options.forceSide}`,
);
throw new TypeError(`bun-svelte-plugin: forceSide must be either 'client' or 'server', got ${opts.forceSide}`);
}
}
if (opts.compilerOptions) {
if (typeof opts.compilerOptions !== "object") {
throw new TypeError("bun-svelte-plugin: compilerOptions must be an object");
}
}
}
@@ -44,7 +59,10 @@ export function validateOptions(options: unknown): asserts options is SvelteOpti
* @internal
*/
export function getBaseCompileOptions(pluginOptions: SvelteOptions, config: Partial<BuildConfig>): CompileOptions {
let { development = false } = pluginOptions;
let {
development = false,
compilerOptions: { customElement, runes, modernAst, namespace } = kEmptyObject as OverrideCompileOptions,
} = pluginOptions;
const { minify = false } = config;
const shouldMinify = Boolean(minify);
@@ -68,6 +86,10 @@ export function getBaseCompileOptions(pluginOptions: SvelteOptions, config: Part
preserveWhitespace: !minifyWhitespace,
preserveComments: !shouldMinify,
dev: development,
customElement,
runes,
modernAst,
namespace,
cssHash({ css }) {
// same prime number seed used by svelte/compiler.
// TODO: ensure this provides enough entropy
@@ -109,3 +131,4 @@ function generateSide(pluginOptions: SvelteOptions, config: Partial<BuildConfig>
}
export const hash = (content: string): string => Bun.hash(content, 5381).toString(36);
const kEmptyObject = Object.create(null);

View File

@@ -24,13 +24,32 @@ afterAll(() => {
}
});
it("hello world component", async () => {
const res = await Bun.build({
entrypoints: [fixturePath("foo.svelte")],
outdir,
plugins: [SveltePlugin()],
describe("given a hello world component", () => {
const entrypoints = [fixturePath("foo.svelte")];
it("when no options are provided, builds successfully", async () => {
const res = await Bun.build({
entrypoints,
outdir,
plugins: [SveltePlugin()],
});
expect(res.success).toBeTrue();
});
describe("when a custom element is provided", () => {
let res: BuildOutput;
beforeAll(async () => {
res = await Bun.build({
entrypoints,
outdir,
plugins: [SveltePlugin({ compilerOptions: { customElement: true } })],
});
});
it("builds successfully", () => {
expect(res.success).toBeTrue();
});
});
expect(res.success).toBeTrue();
});
describe("when importing `.svelte.ts` files with ESM", () => {