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

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
}
}