Repository navigation
Releases: shadowjs-dev/dusk
Release list
1.0.3
Fixed
- The metadata errors (
headersof a route,metadatawithgenerateMetadata,viewportwithgenerateViewport, a header a route cannot set,metadataBase,jsonLd) carry production codesDUSK_META_1toDUSK_META_7instead of their development text, so the message text no longer reaches the client bundle (about 150 bytes gzip on every page of a Dusk app with head metadata). - The ISR cache key keeps the case of param and splat values (
/p/AbCand/p/abcare two entries) and folds only the base and the static segments, andrevalidatePathreaches the entry of the exact spelling. - The shared OIDC discovery and key set fetches run with their own timeout only: a caller that disconnects no longer fails the others waiting or starts the retry delay; each caller stops waiting on its own signal.
- A redirect thrown from an API route or global middleware puts the mount
basein front of a path target, as a page redirect does (absolute URLs are unchanged). - The loader data a single-flight server function sends back is computed with the cookies after the mutation (the ones it set and deleted), not the
Cookieheader the browser sent. - A POST (or other allowed method) from an origin listed in
security.cors.originsto an API route is trusted by the CSRF check, so a request that passed its preflight is no longer refused with 403. - The static file server answers a 304 to the
ETagof the gzip copy it compresses on the fly (and still to the identity one). - Vercel and Netlify route a handler-mode prerendered page's own file URLs (
/admin/index.html,/admin.html,/index.html) to the function, so its middleware and rules run. <Script>with inline children and noidis marked withdata-dusk-scripton the server, and the browser adopts it instead of running it a second time.
1.0.2
Fixed
useServiceWorkerUpdate().update()can run again when the new worker never takes control (it was discarded or ignored the message): the lock now ends after ten seconds, when the waiting worker disappears, or whenpostMessagethrows. The update state belongs to the page'sServiceWorkerContainerinstead of the module.createOidcClienthas areportoption; a refresh token that could not be revoked at sign-out goes to it (defaultconsole.error) instead of being dropped. A refreshed ID token is compared with the subject of the stored user, or of the ID token stored at sign-in when the user is missing from the session; with neither it is refused (OIDC_ID_TOKEN_INVALID).createSseClientstops on a 204, retries a 408, a 429 and every 5xx (a 5xx used to end the client), and waits at least theRetry-Afterof the answer (at most five minutes).- Static files (the Node server, the Bun and Deno layer, and
file()) readRangeandIf-Rangeas RFC 9110 says:bytes=5-2is invalid and ignored (200, not 416), a weak entity tag never satisfiesIf-Range, and a date only does when it equals anLast-Modifiedthat is at least a second old. The Bun and Deno servers now answer byte ranges. - The prerender step of
dusk buildwrites a non-HTML response for a path with an extension (/sitemap.xml,/robots.txt) as that file instead ofsitemap.xml/index.html, and fails the build when two prerendered paths differ only in case.prerenderFiletakes an optional content type. dusk buildrecords its output folder in.dusk/build.json, anddusk startserves it (dusk({ outDir })is no longer ignored);dusk start --dir <folder>names a folder, anddiststays the default.- The image variants step reports a folder it cannot read (permissions, I/O) instead of treating it as empty. The server function scan warns through the build logger when it stops at 20 000 files or skips a file over 2 MB that declares server functions.
cacheDataandcachedFetchtreat a data cache store that fails (read or write) as a miss: the error goes toreportand the loader's value is returned, as the page cache does.- A signed flash cookie that does not decode is reported (phase
render).redisStoretakes areportoption for a stored string that is not JSON.safeImageFetchtakes areportoption for a response header the Fetch API refuses, andserve({ workers })logs a failed call to a worker that is not just gone. <Image unoptimized>checkswidthandheight(finite, greater than 0) and refuses ajavascript:,vbscript:or other script-bearing source;data:image/*,blob:, http(s) and relative sources are served as given.- A WebSocket upgrade on the Node server behind
trustProxyis judged on the forwarded host and protocol, as a plain request is; before, the origin check compared the browser'sOriginwith the internal address and refused every browser. <Image alt="">writesalt=""again: an emptyaltmarks a decorative image, and dropping it made screen readers announce the file name.islandAttributeschecks that the props are plain data iteratively: props nested 100,000 levels deep are refused with the size-limitRangeErrorinstead of overflowing the stack (cycles and bad values are still named by path).dusk buildprints the real build folder (dusk({ outDir })) in the summary, the phase rows and thenode <adapter outDir>/server.mjshint instead of alwaysdist; the JSON summary carries it asoutDir.- A server function call whose input cannot be serialized, made in the same tick as other calls, now rejects alone; before, it left every call batched with it pending forever.
- The submission store's revalidator stays
loadinguntil every overlapping revalidation has finished. <Form>submitted by JavaScript now sends the name and value of the button that was pressed, as a plain browser submit does.- Islands with the
idlestrategy cancel their idle callback withcancelIdleCallbackwhen disposed, instead ofclearTimeouton an id from another id space. requireUser's login redirect appendsreturnTowith&when the login path already has a query.fontFacerefuses aweightorstylethat could end the declaration or the style element it is written into.scriptTagandimagePropsrefuse a source with a tab or line break inside (/<TAB>/hostreads as//hostin a browser).- The file KV store no longer deletes a value written by
setwhile it was removing the expired entry it had just read, andlistskips a file that is not an entry instead of throwing. - A refused WebSocket upgrade on the Node server no longer writes a second
Content-Length,Transfer-EncodingorConnectionheader when the response brought its own. - The Bun and Deno servers report a missing
staticfolder once at start-up and send requests to the handler, instead of an unhandled rejection and a 500 for every request. dusk buildwith anoutDiroutside the app no longer writes a marker thatdusk startrefuses and thatdusk buildcould never fix: it says the folder could not be recorded and to pass--dirtodusk start.- A single-flight server function runs the middleware of the page named in
x-dusk-locationbefore its loaders, so a visitor can no longer read the loader data of a page whose_middlewareormiddlewarerefuses them; the loaders also see thelocalsthe app-wide middleware set. - A redirect whose target holds an emoji or a lone surrogate answers a 302 with the text percent-encoded instead of throwing, and a form post that redirects to a non-Latin-1 path answers a 303 instead of a 500.
security.corsaddsVary: Originto every API answer (also to an unlisted or missing origin) and keeps it when the route sets aVaryof its own.- A page served from the cache with a CSP nonce no longer answers 304 to a matching
If-None-Match: the stored body would carry the old nonce under the new policy. - A WebSocket message is limited in its number of fragments, so endless empty continuation frames cannot grow the reader's memory without bound (close code 1009).
cachedFetchin draft mode keeps the checks, the error on a non-OK answer and the text or JSON parsing of a normal call.- The image endpoint answers
HEAD, and the global error document escapes the language it wraps in<html lang>. dusk({ outDir })refuses the project root, a folder that holds the source folder, and a Windows drive path (the build emptiesoutDir); an adapter whose folder is, or holds, the build output or the sources fails with a message before it empties anything.dusk buildreads the manifest from the folderdusk({ outDir })names, not always fromdist.- A source file named like an
Object.prototypemember (src/constructor.ts,src/toString.png) or a dangling link at the top ofsrcno longer fails the convention scan. - An env variable named
constructorortoStringin an.envfile no longer crashes the build; a relativeenvDiris read from the project root. - The deployment id fails when the source folder cannot be listed instead of hashing a partial listing.
- The dev server serves page routes whose URL starts with
/@or/__(such as/@me) and builds a request URL from the Host header even for a target that starts with//. - The dev watcher registers server functions only from the files the build scan reads (not tests, declarations or markdown).
- Import protection accepts a built-in name that resolves to an installed polyfill or an alias (
events,buffer);node:names and unresolved built-ins are still refused. dusk devbounds the memory ofwarnOncemessages.imageVariants()warns through the Vite logger when it stops reading the project: more than 20,000 source files undersrc/, a source file over 1 MiB (named), or more than 100,000 images inpublic/. Before, it skipped them silently and the images they referenced were served unprepared.
1.0.1
Added
dev.allowedHostsindusk.config.ts(string[]ortrue): host namesdusk devanswers besideslocalhost. Vite's host check answered 403 to any otherHost, for examplejoin.example.commapped to127.0.0.1while testing subdomain routing. The hosts the app declares insecurity.allowedHostsare allowed in development too (a port is dropped,*.example.combecomes.example.com). New:DuskDevConfig,devAllowedHostsOf.prerender.skipMissing(dusk.config.tsanddusk({ prerender })): a listed path that matches no route is skipped with a warning instead of failing the build.onErrorof the prerender options getsfrom, the page a crawled path was linked from.- Guide: Serve a subdomain from a route (a middleware
rewrite()servesjoin.example.com/<code>from/join/<code>, unknown paths go to the apex, with how to try it locally and on Vercel).
Fixed
- A redirect in
dusk.config.tsmay end in a fragment (/old-downloadto/#download), for a path, for an absolute URL of a trusted origin and forrouteRulesredirects. The fragment is sent unchanged inLocation, the request's query goes before it, and the static export's meta-refresh page keeps the query and the fragment of the target. A rewrite destination still may not have one. - A prerender that fails for a path that matches no route names the path, where it was listed (
prerender.routes,prerender.entries,routeRules, theprerenderexport or function of a route file, or the page the link was found on) and the closest route.
1.0.0
Added
-
Multi-core serving:
serve({ workers })(a number, or"auto":os.availableParallelism(), at most 16),dusk start --workers <n|auto>andDUSK_WORKERSfor the generatedserver.mjs. The process becomes the primary of anode:clusterpool that serves nothing, forks the workers, waits until each listens (a worker that dies before the pool is ready fails the start), replaces a crashed worker with an exponential delay (500 ms doubling to 30 s), stops with exit code 1 and a clear message when 5 workers crash within 30 s (supervise.maxCrashes,crashWindowMs), kills a worker that does not listen within 30 s, replaces every worker one by one onSIGUSR2orpool.restart()(the new worker listens before the old one drains, which first stops accepting and answers on its open connections withConnection: close), and onSIGTERM/SIGINTdrains the workers for up togracefulShutdownMs.ServerHandlegainsrole("single","worker","primary") andpool(PoolControl:workers,pids(),restart(),failed);workerInfo()andmarkProcessLocalStore(label)are exported from@shadow-js/dusk/node; outside production a worker sendsx-dusk-worker. The primary warns once that in-memory stores are per worker (dusk start --workerswhen a page setsrevalidate;servefor every store that callsmarkProcessLocalStore). Withoutworkers, or with 1, nothing of the pool is loaded and the server behaves as before.tools/loadgains--workers. -
Production observability, opt-in and loaded only when used: the
serverTiminghandler option (W3CServer-Timingwithtotal,route,loaders,renderand your own metrics; on in development, off in production;{ header, onTimings }; never replaces a header the response already has; for a streamed page the head carries the time to the shell andonTimingsgets the full render time),event.timing.measure/event.timing.addandtiming()(@shadow-js/dusk/server) with token-checked names (at most 64 characters, at most 20 per request) and descriptions reduced to safe ASCII, child spans for the loader chain (dusk.loaders,dusk.loader), the endpoint (dusk.page,dusk.api,dusk.rpc, ...), the render and the stream (dusk.render,dusk.page.stream) with OpenTelemetry attribute names (http.response.status_code,http.route,error.type) andrecordException, each ended exactly once also on abort and timeout, and themetricshandler option (createCounter().add,createHistogram().record;dusk.server.requestsandhttp.server.request.duration). Without a tracer the request path allocates no span. New types:ServerTimingOptions,RequestTimings,EventTiming,MetricsLike,CounterLike,HistogramLike;RequestEventgainstiming. Guide: Configuring a Dusk app, Observability. -
pwa()(@shadow-js/dusk/vite, opt-in): after the client build it writesmanifest.webmanifestfrom a typed manifest, an offline page (no script, no inline style) andsw.js, a service worker generated as TypeScript and compiled by the build with no runtime library. The worker precaches the build's hashed assets with a content-hash revision per file, reuses unchanged files from the previous build's cache, answers a failed navigation with an app shell or the offline page, and applies runtime rules per path glob (network-first,cache-first,stale-while-revalidate, with entry and age limits and a network timeout). Caches are named by build id and the other builds' caches are deleted on activate. It handles only same-originGETrequests inside the base, never a request withAuthorizationor a range, never/_dusk/**, and never keeps a response that is not a plain200, isno-storeorprivate, varies on cookies, or carries a nonce policy. A new version waits until the page asks. -
@shadow-js/dusk/features/pwa(opt-in subpath):registerServiceWorker(SSR-safe, scoped, waits forload, not in development unlessdev: true),useServiceWorkerUpdate()returning{ waiting, update }, andunregisterServiceWorker. Guide: Dusk: installable apps and offline. -
imageVariants()(@shadow-js/dusk/vite, opt-in): at build time it prepares content-hashed AVIF and WebP files at a width ladder (640, 960, 1280, 1920 by default) for the images ofpublic/that the source mentions, caches the encoded files between builds, warns once whensharpis missing and serves the originals, and warns about originals over a size budget.<Image>writes a<picture>whose sources point at the prepared files, preloads the best prepared format for apriorityimage, and keeps the optimizer URLs on the<img>as the fallback. -
defineFontsgainsroutes(preload the files of some families on some routes only;fontPreloadsForpicks the tags for a path),report(andFontsOutput.report,formatFontReport): every font file with its size and whether every page preloads it. -
@shadow-js/dusk/auth(opt-in subpath, no runtime dependency): provider-agnostic sign-in for Auth0, Okta, Keycloak, Google, Microsoft Entra, GitHub, Discord and any OpenID Connect issuer.createOidcClientruns the OAuth 2.1 / OpenID Connect authorization code flow with PKCE (S256),stateandnoncekept in the session for ten minutes and one use, discovery with caching, ID token verification with WebCrypto (RS256, PS256, ES256, EdDSA; HS256 only for a confidential client that lists it) through a JWKS cache with key rotation and a refetch limit, checks ofiss,aud,azp,exp,nbf,iat,nonceandat_hash, theissresponse parameter (mix-up defense), userinfo, refresh token rotation with single-flight, and RP-initiated logout with token revocation. Every call to the provider has a timeout, no redirects and a 1 MiB cap; issuers and endpoints must be https (localhost in development).signIngives the session a new id;requireUseris a Dusk guard. Presets:google,microsoft,auth0,okta,keycloak,github,discord. AlsoverifyJwt,createJwksCache,OidcError,isOidcError. Guide: Dusk: sign-in with OAuth and OpenID Connect. -
Scalable caching: request coalescing (single flight) for the page cache and the data cache with a cap on requests per key (
cache.maxWaiters, 1,000) and on keys in flight (cache.maxInFlight, 10,000;maxInFlightof the data cache is now 10,000 too), every request getting its own response or copy of the value, failures shared and not cached, a leaving request not stopping the shared work unless every request left (499for a page request whose client left;signalofcacheData;cachedFetchfetches with the signal of the shared run), and a render timeout (cache.renderTimeoutMs, 30 s;timeoutMsof the data cache). -
Stale-while-revalidate: the background refresh of a stale page or value goes to the platform's
waitUntilwhen the request has one, a failed refresh keeps the stale copy, is reported once and backs off (cache.backoffBaseMs1 s,cache.backoffMaxMs60 s), pages answer withAgeands-maxageequal to their lifetime, and a render or run that began before a purge of one of its tags does not store its result after it. -
redisStore(client, options)(@shadow-js/dusk/stores/redis): a page cache store over a five-command Redis client interface (no Redis package), with a version tag, an entry size cap, key prefixes, tag indexes kept as sets (noSCAN), a tag purge that does not lose the index of a concurrent write, and an optional tag index expiry.CacheStoregained an optionalpurgeTag. -
runStoreContract(store)andSTORE_CONTRACT(@shadow-js/dusk/stores/contract): the scenarios every page cache store passes; the memory store, the KV adapters andredisStorerun them.
Changed
-
The page cache key is
hashKey([host, path, query, cookies])(the query key format); the KV page cache adapter stores a key or tag longer than 512 characters under its digest.createKeyValueCacheStore().tags()lists only keys whose entry still exists and carries the tag. -
--jsonon every command of theduskbin, in one documented contract (schema 1:schema,command,ok,data,warningsand, on failure,error: { code, message }with a stableDUSK_code).build,doctor,completion,--helpand--versionprint one document;devandstartprint JSON Lines (readywith the address,request,stopped). Stdout carries only JSON, logs go to stderr, nothing is asked, a refused command line and an uncaught exception are documents too, and the exit codes are unchanged. The build summary is thedataofdusk build --jsonand the doctor checks aredata.checks; the shell completion scripts offer the flag. -
File routes, server functions, API routes, middleware and request security.
-
Server rendering, streaming, prerendering, client hydration, metadata, assets and deployment adapters.
-
Server HTML applies matched route stylesheets; client navigation inherits the server URL policy.
-
Native Bun, Deno and Cloudflare WebSocket bridges share guarded upgrades, message limits and untracked callbacks.
-
WebSocket cleanup releases connection slots on failed closes and bounds heartbeat timers; Deno graceful shutdown waits for native close handshakes up to its configured deadline.
-
Node layouts declare their WebSocket and static-file middleware support; Vercel and Netlify edge targets declare explicit-event request context. The deploy guide checks its generated capability table against adapter factories.
-
serve({ compress })takeslevel,types(replaces the default compressible list) andfilter(request, response), which skips compression unless it returnstrue, so responses that reflect a secret can stay out of compression (BREACH). Options are validated when the server starts: an unknown name, a number out of range or atypesorfilterof the wrong kind throws with the option's name. zstd is negotiated by q-value after brotli an...