feat: serve static assets from the build manifest - #16908
Conversation
|
Install the latest version of pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/9e67c1fa7c711f322043ed7d47430782850ca8e2Open in |
🦋 Changeset detectedLatest commit: 9e67c1f The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
81ea893 to
1192a76
Compare
53a5aae to
b0a2627
Compare
050fcd5 to
59ff3e6
Compare
|
I'm going to merge the downstack PR of this one because it's straightforward and gets rid of a dep, but this one needs some additional discussion. It makes some tradeoffs that might be worth it, but I don't think are straightforward enough to just yolo into without the broader team's input. We can talk about this at the maintainer's meeting on Friday. Stuff I see while going through (some of these are existing Generated static output is now immutableCurrently, If a deployment adds a file to the build output, the new file is never served. Deleting or replacing a file has similar unpredictable consequences. This is relevant to (off the top of my head):
This isn't necessarily an unacceptable model, but it's a major compatibility break from what we currently have. A compromise would be to manifest the known path set but obtain size and validator metadata at startup... but I'm not sure how much better that would really be.
|
|
Thanks! On the HTTP semantics, everything you listed reproduces on this build and I'll push fixes: q-value parsing for I took a look at sirv's source code. sirv sends I measured on this branch vs the sirv build, N × 3 KB compressible files plus 3 × 5 MB binaries, Node 22, loopback, five cold starts each:
At N=1000 the build difference is 0.8 s and everything else is within noise except the manifest (246 KB vs 18 KB). Startup and resident memory go the other way from what you expected because sirv's With On the freeze itself, sirv already freezes at boot: |
68a047e to
f6397a4
Compare
|
With the aliases built at boot and the variants storing only a size, the manifest is ~150 bytes per file (1.5 MB at 10k files, down from 2.4 MB). Time to listening at 10k files is 510 to 652 ms against sirv's 536 to 689, with the walk being the same A file overwritten after the build is served with its new content, uncompressed, because the |
f6397a4 to
b76f6b9
Compare
Right, but freezing at boot and freezing at buildtime are very different things -- freezing at boot still allows you to, for example, build the same app once but swap out a JSON configuration file and deploy it multiple places. |
|
Exactly. The implementation should not treat the build manifest as an immutable allowlist: it needs to validate recorded files and discover additions at startup, so deployment-time replacement or addition of static configuration files remains supported. |
Since d554932, recorded files are stat-ed at startup and rehashed on a size or mtime change, added files under the client dir are discovered, deleted ones dropped. |
|
I haven't looked too closely at the code because unfortunately it needs a rebase — the recent adapter API changes (dropping Having said that: are we sure we want to do boot time rather than build time? When would you 'swap out a JSON configuration file' in your |
|
The mime table is manifest.mimeTypes, which kit already ships and fetch.js already uses, so neither option adds a lookup. I lean build-time too, every other adapter already serves exactly what the build produced. Dropping ccb4bc9 gets the build-time version back. |
b76f6b9 to
ccb4bc9
Compare
ccb4bc9 to
adb5140
Compare
📝 WalkthroughWalkthrough
Assessment against linked issues
Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: 🔵 Low · up to Clients that reject all available encodings may receive an unacceptable response instead of 406. The issue is bounded and straightforward to fix. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Full details: Linked Issues checkExplanation The changes address both linked issues. The adapter uses manifest asset tables with hashes, aliases, compressed variants, and MIME types. It removes the listed runtime dependencies and adds coverage for filenames ending in '+'. Full details: Docstring CoverageExplanation Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 7 files. (7 skipped: 7 unsupported.) Warning Some tools did not complete. Review the errors below. 🔧 ESLint
packages/adapter-node/index.jsESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. packages/adapter-node/internal.d.tsESLint skipped: the matched ESLint configuration already failed (missing-dependency). packages/adapter-node/package.jsonESLint skipped: the matched ESLint configuration already failed (missing-dependency).
Comment |
|
I removed the boot-time commit. Additionally, hashing no longer goes through read streams.
|
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: CHILL
Plan: Advanced
Run ID: ddacbebd-082e-406f-a292-607c695e39b3
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (13)
.changeset/static-assets-build-time.md.changeset/static-assets-etag.md.changeset/static-assets-methods.mddocumentation/docs/25-build-and-deploy/40-adapter-node.mddocumentation/docs/60-appendix/35-migrating-to-sveltekit-3.mdpackages/adapter-node/index.jspackages/adapter-node/internal.d.tspackages/adapter-node/package.jsonpackages/adapter-node/src/handler.jspackages/adapter-node/src/static.jspackages/adapter-node/src/static.spec.tspackages/adapter-node/test/apps/basic/test/test.jspackages/kit/src/core/adapt/builder.js
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
sveltejs/svelte(manual)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
| } | ||
|
|
||
| /** @param {string} coding */ | ||
| const weight = (coding) => weights.get(coding) ?? weights.get('*') ?? 0; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject requests that disallow every available representation.
For a normal GET, when identity;q=0 or *;q=0 rejects identity and all available compressed variants have weight zero, negotiate returns undefined. serve_static then selects the identity file and can send it with status 200. Return a distinct “not acceptable” result and send status 406 instead.
7b933e3 to
28d211a
Compare
An unhandled read stream error (a file deleted from the build output, EMFILE) crashed the process. Headers are already sent by then, so drop the connection.
…from the original hash
28d211a to
3b68018
Compare
|
|
||
| Development dependencies will be bundled into your app using [Rolldown](https://rolldown.rs/). To control whether a given package is bundled or externalised, place it in `devDependencies` or `dependencies` respectively in your `package.json`. | ||
|
|
||
| Client assets and prerendered output are served from a list of files recorded during the build. Only `GET` and `HEAD` requests are served from it; other methods continue to SvelteKit. Every asset carries an ETag computed during the build, so conditional requests revalidate with an empty `304` response. Byte ranges are supported. Files below SvelteKit's `immutable` directory receive `Cache-Control: public,max-age=31536000,immutable`. |
There was a problem hiding this comment.
I think we should probably 405 for non GET/HEAD requests instead of continuing to the server — this is what generally happens on other platforms. If the request continues to the server then the likely best case is a 404, which would be incorrect
| - the `ORIGIN` environment variable is removed (set `paths.origin` in your Vite config instead) | ||
| - static assets are served from a list recorded at build time; files added to the output directory afterwards are not served, and replaced ones keep their old size and `ETag` (use environment variables for runtime configuration) | ||
| - `ETag`s for static assets are content hashes, and `Last-Modified` is no longer sent | ||
| - only `GET` and `HEAD` requests are served static assets; other methods reach SvelteKit |
There was a problem hiding this comment.
Closes #16565, fixes #11766.
adapter-nodeserves client and prerendered files through sirv, which re-derives at request time what the build already computed: it walks the output directory at boot and stats every file, takesContent-Typefrom its own bundledmrmime(so types kit adds to the manifest never reach it), setsVaryfrom its options rather than from the file it resolved (the over-send #16566 had to work around), and gets byte ranges wrong (bytes=0-0, the probe HTML5 video and PDF.js use, returned the whole file;bytes=-3was off by one).The adapter now records two tables at adapt time, one per mount, exported from
build/adapter-node.jsalongside the other hand-off values. Each maps a servable pathname (including thefoo.html/foo/index.htmlaliases in sirv's resolution order) to its file, size, content-hash ETag and compressed-variant sizes.src/static.jsresolves both tables into one map at boot, so a request is a lookup, header negotiation and a stream.sirv,@polka/urland their transitivemrmime/totalistdrop out of the adapter, along with the 27kB sirv chunk in the build output.builder.mimeTypesis now also seeded from client output extensions, the mechanism #16564 added for prerendered output.The breaking changes (build-time asset list, content-hash ETags without
Last-Modified,GET/HEADonly) each have a changeset. Assets are frozen at build time rather than boot time because every other adapter serves exactly what the build produced.Percent-decoding follows kit's router (
decodeURIwith%25preserved),Accept-Encodingis parsed with q-values,If-None-Matchaccepts lists,*and weak tags, andIf-Rangeis honoured.static.spec.tscovers each behaviour on its own. No adapter-node test app exercises a base path, a pre-existing coverage hole.Adapt-time measurements: #16908 (comment)