sveltekit_build¶
Wraps a vite build driven by SvelteKit's own Vite plugin as a single Bazel
action, and returns both halves of a SvelteKit application: the browser bundle
under client/ and the request handler under server/.
The action stages a SvelteKit project directory from declared inputs alone — the
src/ tree, a node_modules tree, svelte.config.js, the Vite config carrying
the plugin — makes that directory the process working directory, runs the build
in it, and moves .svelte-kit/output into one declared output artifact. The
working directory is the only thing SvelteKit looks at; see
Why this is a rule and not a ts_bundle.
Usage¶
load("@rules_typescript//npm:defs.bzl", "node_modules")
load("@rules_typescript//ts:defs.bzl", "sveltekit_build")
node_modules(
name = "node_modules",
deps = [
"@npm//:svelte",
"@npm//:sveltejs_kit",
"@npm//:sveltejs_vite-plugin-svelte",
"@npm//:vite",
],
)
sveltekit_build(
name = "app",
srcs = glob(["src/**"]),
config = "vite.config.mjs",
node_modules = ":node_modules",
svelte_config = "svelte.config.js",
)
Gazelle generates both targets when it sees @sveltejs/kit in package.json.
The two configs are hand-authored and carry no Bazel-specific lines:
// vite.config.mjs
import { sveltekit } from "@sveltejs/kit/vite";
export default { plugins: [await sveltekit()] };
Pin kit.version.name. See Reproducibility.
Gazelle-generated srcs¶
Gazelle writes srcs as a glob() over src/ plus the assets tree, and
recomputes it on every run. The assets tree is kit.files.assets, which
defaults to static/ but relocates; Gazelle reads the name out of
svelte.config.js and reports the forms it cannot read:
typescript: SvelteKit detected: svelte.config.js sets kit.files.assets in a form
Gazelle cannot read, so srcs globs static/ and the assets tree may be missing
from it -- an app whose assets are not staged builds green and 404s on them. Name
the tree in srcs with a "# keep" comment on its pattern.
staging_srcs is generated from the other side of the glob: every target
outside src/ and the assets tree, so a shared package the glob cannot cover
still reaches the build.
A pattern or a label Gazelle does not derive needs a # keep on its line, and
so do hand-set config and svelte_config values. Full contract:
Attributes Gazelle owns on the framework rules.
Attributes¶
| Attribute | Type | Default | Description |
|---|---|---|---|
srcs |
label_list |
required | Every project file SvelteKit reads: src/app.html, the route tree under src/routes/, src/lib/**, and anything they import from inside this package |
svelte_config |
label |
required | svelte.config.js. That extension only |
config |
label |
required | The Vite config whose default export carries SvelteKit's plugin |
node_modules |
label |
required | A node_modules target holding @sveltejs/kit, @sveltejs/vite-plugin-svelte, svelte, vite and the app's dependencies |
config_srcs |
label_list |
[] |
Local modules config imports, staged beside it |
staging_srcs |
label_list |
[] |
Files from other packages, staged at their workspace-relative paths |
tsconfig |
label |
None |
A tsconfig.json staged at the project root |
env |
string_dict |
{} |
Extra environment variables for the build |
allow_subpackages |
list |
[] |
Directories under the globbed tree that are Bazel packages of their own, accepted as holes in srcs |
allow_network |
bool |
False |
Let the build reach the network. The action otherwise runs with block-network |
Each srcs file lands at its path relative to the target's package.
src/app.html and the route tree are read off disk, not through imports, so
they must be listed even though nothing imports them. svelte_config takes a
.js file only, for the reasons under
Checks the rule performs. The rule stages config
in a subdirectory of the project root, so its relative imports resolve against
config_srcs and not against the app sources. staging_srcs has the same
contract as next_build's. allow_subpackages is described
under
Subpackages under the globbed tree.
Output¶
One directory artifact, <name>_sveltekit_out, holding what SvelteKit wrote to
.svelte-kit/output:
client/_app/immutable/entry/{start,app}.<hash>.js
client/_app/immutable/nodes/<n>.<hash>.js one per route node
client/_app/immutable/{chunks,assets}/**
client/_app/version.json kit.version.name
client/.vite/manifest.json
server/index.js the request handler
server/manifest.js route ids, patterns, nodes
server/entries/pages/_page.svelte.js one per route file
server/entries/pages/_page.server.ts.js
server/entries/endpoints/**/_server.ts.js one per +server route
server/entries/fallbacks/{layout,error}.svelte.js
server/chunks/**
server/.vite/manifest.json
Route entries are named after the route files, with + rewritten to _
because + is not legal in an output filename.
No adapter is run: the output is SvelteKit's own build, not a platform-specific deployment bundle.
Why this is a rule and not a ts_bundle¶
SvelteKit reads process.cwd(), and only process.cwd(). load_config() globs
<cwd>/svelte.config.{js,ts}, then resolves every kit.files.* entry —
src/app.html, src/routes, src/lib, static — against that same directory,
from Vite's config hook, before a single module is transformed. It scans the
route tree off disk and writes <cwd>/.svelte-kit.
The plugin's own config hook returns root: cwd and only warns when the host
config set root to something else, so ts_bundle's
staging-root redirection is inert: an app staged anywhere but the working
directory fails with src/app.html does not exist whatever root says. Hosting
SvelteKit inside ts_bundle would mean changing where the shared Vite wrapper
cds, for TanStack, Remix and every future Vite framework. The plugin also
replaces build.outDir, build.rollupOptions.input, the output filename
patterns, base, publicDir, build.manifest and build.ssr with its own
values, so a ts_bundle output directory would sit empty while the real bundle
landed in an undeclared .svelte-kit/.
One vite build is two builds: build.ssr starts true, and the SSR half's
writeBundle flips it and calls vite.build() again for the client. Neither
half is independently cacheable, so both come out of one action.
Reproducibility¶
kit.version.name defaults to Date.now().toString(), evaluated when
SvelteKit's options module loads. It lands in client/_app/version.json and is
hashed into the __sveltekit_<hash> global, so it changes chunk content and
therefore chunk filenames. Unpinned, no two builds agree and nothing downstream
of the target ever gets a cache hit.
The rule cannot see the config's contents at analysis time, so it checks the
emitted version.json afterwards and warns when the version reads as
epoch-milliseconds, which is what the default produces:
//tests/integration:sveltekit_test builds twice from clean and byte-compares
the full set of hashed client filenames.
Subpackages Under the Globbed Tree¶
glob() does not descend into a subpackage. A BUILD file anywhere under the
globbed tree removes its whole directory from srcs, and SvelteKit then
compiles the route tree minus that directory and reports success for the routes
that survived. With no check in front of it, a BUILD file under
src/routes/blog/[slug] leaves server/manifest.js carrying the rest and
nothing else:
sveltekit_build is a macro so that never reaches a build. Before the rule is
instantiated it asks native.subpackages() which directories under the globbed
tree are packages of their own, and fails naming the first:
sveltekit_build(name = "app"): src/routes/blog/[slug]/BUILD.bazel makes
src/routes/blog/[slug] a Bazel package, and the srcs glob does not descend into
one.
Delete the BUILD file. Gazelle will not create one under src/ and reports any
it finds there; emptying a file — all it can do to one it did not write — leaves
the package behind, so the file itself has to go. If a subpackage is deliberate
and its contents reach the app through staging_srcs, list it:
Checks the Rule Performs¶
Three failure modes SvelteKit itself does not report. The rule fails on each:
- A route file SvelteKit never compiled. The keys of
server/.vite/manifest.jsonare the staged paths verbatim, so the rule requires every staged+-prefixed file to appear there and lists the ones that do not. A route tree staged outsidekit.files.routesis the usual cause. - A build that staged no routes at all. SvelteKit emits
entries/fallbacks/either way and the summary looks normal, so the rule fails at analysis time when no+page*or+server*file is insrcs. - A
svelte.config.mjsthat only Bazel would read. The rule stages the config assvelte.config.jswhatever it is called, so a.mjsbuilds here and nowhere else: SvelteKit's ownload_config()globssvelte.config.{js,ts}at its cwd and would not find it. Rejected at analysis time. A.tsis in that glob, butload_config()imports what it finds through Node, and the toolchain Node cannot load a.tsfile at all (ERR_UNKNOWN_FILE_EXTENSION) — rejected too.
//tests/integration:sveltekit_test covers all of these, the subpackage hole
above, a missing src/app.html and the unpinned-version warning.
Hermeticity¶
The action runs with block-network. SvelteKit itself needs no network: it has
no telemetry and fetches nothing. A config plugin is arbitrary code, though,
and a Google-Fonts or remote-schema plugin would make the output depend on a
host. allow_network = True opts a target out.
src/ gets no TypeScript targets¶
Gazelle generates no ts_compile or ts_test anywhere under a SvelteKit app's
src/, and prints that once per run. Two reasons:
- A BUILD file under
src/makes a subpackage, andglob()does not descend into a subpackage, so the staged tree would silently lose exactly the modules the app imports. See Subpackages under the globbed tree. - A route module conventionally imports
./$types, which exists only as.svelte-kit/types/**/$types.d.tsand is reachable only through therootDirsscheme in thetsconfig.jsonSvelteKit generates. A plaints_compilehas neither.
TypeScript you want compiled and unit-tested by Bazel goes in a package outside
src/, which Gazelle names in staging_srcs.
Not Covered Yet¶
- Adapters. The build stops at
.svelte-kit/output. An adapter runs incloseBundleand conventionally writes tobuild/at the project root, which the rule does not relocate. - Type-checking
<script lang="ts">. That needssvelte-check, which pullssvelte2tsxandtypescript. It consumes the generated.svelte-kit/tsconfig.jsonand$types, so it needs asvelte-kit syncoutput as an input, or must run sync itself.svelte_libraryhas the same gap for the same reason. - A dev server.
ts_dev_server'sDevServerInfoseam is the right place, but SvelteKit's dev mode writes.svelte-kitcontinuously and would need a writable project root outside the source tree.