Skip to content

Releases: shadowjs-dev/dusk

1.0.3

Choose a tag to compare

@github-actions github-actions released this 29 Oct 10:32

Fixed

  • The metadata errors (headers of a route, metadata with generateMetadata, viewport with generateViewport, a header a route cannot set, metadataBase, jsonLd) carry production codes DUSK_META_1 to DUSK_META_7 instead 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/AbC and /p/abc are two entries) and folds only the base and the static segments, and revalidatePath reaches 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 base in 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 Cookie header the browser sent.
  • A POST (or other allowed method) from an origin listed in security.cors.origins to 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 ETag of 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 no id is marked with data-dusk-script on the server, and the browser adopts it instead of running it a second time.

1.0.2

Choose a tag to compare

@github-actions github-actions released this 29 Oct 01:10

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 when postMessage throws. The update state belongs to the page's ServiceWorkerContainer instead of the module.
  • createOidcClient has a report option; a refresh token that could not be revoked at sign-out goes to it (default console.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).
  • createSseClient stops on a 204, retries a 408, a 429 and every 5xx (a 5xx used to end the client), and waits at least the Retry-After of the answer (at most five minutes).
  • Static files (the Node server, the Bun and Deno layer, and file()) read Range and If-Range as RFC 9110 says: bytes=5-2 is invalid and ignored (200, not 416), a weak entity tag never satisfies If-Range, and a date only does when it equals an Last-Modified that is at least a second old. The Bun and Deno servers now answer byte ranges.
  • The prerender step of dusk build writes a non-HTML response for a path with an extension (/sitemap.xml, /robots.txt) as that file instead of sitemap.xml/index.html, and fails the build when two prerendered paths differ only in case. prerenderFile takes an optional content type.
  • dusk build records its output folder in .dusk/build.json, and dusk start serves it (dusk({ outDir }) is no longer ignored); dusk start --dir <folder> names a folder, and dist stays 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.
  • cacheData and cachedFetch treat a data cache store that fails (read or write) as a miss: the error goes to report and the loader's value is returned, as the page cache does.
  • A signed flash cookie that does not decode is reported (phase render). redisStore takes a report option for a stored string that is not JSON. safeImageFetch takes a report option for a response header the Fetch API refuses, and serve({ workers }) logs a failed call to a worker that is not just gone.
  • <Image unoptimized> checks width and height (finite, greater than 0) and refuses a javascript:, 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 trustProxy is judged on the forwarded host and protocol, as a plain request is; before, the origin check compared the browser's Origin with the internal address and refused every browser.
  • <Image alt=""> writes alt="" again: an empty alt marks a decorative image, and dropping it made screen readers announce the file name.
  • islandAttributes checks that the props are plain data iteratively: props nested 100,000 levels deep are refused with the size-limit RangeError instead of overflowing the stack (cycles and bad values are still named by path).
  • dusk build prints the real build folder (dusk({ outDir })) in the summary, the phase rows and the node <adapter outDir>/server.mjs hint instead of always dist; the JSON summary carries it as outDir.
  • 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 loading until 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 idle strategy cancel their idle callback with cancelIdleCallback when disposed, instead of clearTimeout on an id from another id space.
  • requireUser's login redirect appends returnTo with & when the login path already has a query.
  • fontFace refuses a weight or style that could end the declaration or the style element it is written into.
  • scriptTag and imageProps refuse a source with a tab or line break inside (/<TAB>/host reads as //host in a browser).
  • The file KV store no longer deletes a value written by set while it was removing the expired entry it had just read, and list skips 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-Encoding or Connection header when the response brought its own.
  • The Bun and Deno servers report a missing static folder once at start-up and send requests to the handler, instead of an unhandled rejection and a 500 for every request.
  • dusk build with an outDir outside the app no longer writes a marker that dusk start refuses and that dusk build could never fix: it says the folder could not be recorded and to pass --dir to dusk start.
  • A single-flight server function runs the middleware of the page named in x-dusk-location before its loaders, so a visitor can no longer read the loader data of a page whose _middleware or middleware refuses them; the loaders also see the locals the 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.cors adds Vary: Origin to every API answer (also to an unlisted or missing origin) and keeps it when the route sets a Vary of 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).
  • cachedFetch in 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 empties outDir); an adapter whose folder is, or holds, the build output or the sources fails with a message before it empties anything.
  • dusk build reads the manifest from the folder dusk({ outDir }) names, not always from dist.
  • A source file named like an Object.prototype member (src/constructor.ts, src/toString.png) or a dangling link at the top of src no longer fails the convention scan.
  • An env variable named constructor or toString in an .env file no longer crashes the build; a relative envDir is 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 dev bounds the memory of warnOnce messages.
  • imageVariants() warns through the Vite logger when it stops reading the project: more than 20,000 source files under src/, a source file over 1 MiB (named), or more than 100,000 images in public/. Before, it skipped them silently and the images they referenced were served unprepared.

1.0.1

Choose a tag to compare

@github-actions github-actions released this 28 Oct 05:41

Added

  • dev.allowedHosts in dusk.config.ts (string[] or true): host names dusk dev answers besides localhost. Vite's host check answered 403 to any other Host, for example join.example.com mapped to 127.0.0.1 while testing subdomain routing. The hosts the app declares in security.allowedHosts are allowed in development too (a port is dropped, *.example.com becomes .example.com). New: DuskDevConfig, devAllowedHostsOf.
  • prerender.skipMissing (dusk.config.ts and dusk({ prerender })): a listed path that matches no route is skipped with a warning instead of failing the build. onError of the prerender options gets from, the page a crawled path was linked from.
  • Guide: Serve a subdomain from a route (a middleware rewrite() serves join.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.ts may end in a fragment (/old-download to /#download), for a path, for an absolute URL of a trusted origin and for routeRules redirects. The fragment is sent unchanged in Location, 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, the prerender export or function of a route file, or the page the link was found on) and the closest route.

1.0.0

Choose a tag to compare

@github-actions github-actions released this 22 Oct 09:55

Added

  • Multi-core serving: serve({ workers }) (a number, or "auto": os.availableParallelism(), at most 16), dusk start --workers <n|auto> and DUSK_WORKERS for the generated server.mjs. The process becomes the primary of a node:cluster pool 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 on SIGUSR2 or pool.restart() (the new worker listens before the old one drains, which first stops accepting and answers on its open connections with Connection: close), and on SIGTERM/SIGINT drains the workers for up to gracefulShutdownMs. ServerHandle gains role ("single", "worker", "primary") and pool (PoolControl: workers, pids(), restart(), failed); workerInfo() and markProcessLocalStore(label) are exported from @shadow-js/dusk/node; outside production a worker sends x-dusk-worker. The primary warns once that in-memory stores are per worker (dusk start --workers when a page sets revalidate; serve for every store that calls markProcessLocalStore). Without workers, or with 1, nothing of the pool is loaded and the server behaves as before. tools/load gains --workers.

  • Production observability, opt-in and loaded only when used: the serverTiming handler option (W3C Server-Timing with total, route, loaders, render and 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 and onTimings gets the full render time), event.timing.measure / event.timing.add and timing() (@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) and recordException, each ended exactly once also on abort and timeout, and the metrics handler option (createCounter().add, createHistogram().record; dusk.server.requests and http.server.request.duration). Without a tracer the request path allocates no span. New types: ServerTimingOptions, RequestTimings, EventTiming, MetricsLike, CounterLike, HistogramLike; RequestEvent gains timing. Guide: Configuring a Dusk app, Observability.

  • pwa() (@shadow-js/dusk/vite, opt-in): after the client build it writes manifest.webmanifest from a typed manifest, an offline page (no script, no inline style) and sw.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-origin GET requests inside the base, never a request with Authorization or a range, never /_dusk/**, and never keeps a response that is not a plain 200, is no-store or private, 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 for load, not in development unless dev: true), useServiceWorkerUpdate() returning { waiting, update }, and unregisterServiceWorker. 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 of public/ that the source mentions, caches the encoded files between builds, warns once when sharp is 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 a priority image, and keeps the optimizer URLs on the <img> as the fallback.

  • defineFonts gains routes (preload the files of some families on some routes only; fontPreloadsFor picks the tags for a path), report (and FontsOutput.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. createOidcClient runs the OAuth 2.1 / OpenID Connect authorization code flow with PKCE (S256), state and nonce kept 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 of iss, aud, azp, exp, nbf, iat, nonce and at_hash, the iss response 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). signIn gives the session a new id; requireUser is a Dusk guard. Presets: google, microsoft, auth0, okta, keycloak, github, discord. Also verifyJwt, 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; maxInFlight of 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 (499 for a page request whose client left; signal of cacheData; cachedFetch fetches with the signal of the shared run), and a render timeout (cache.renderTimeoutMs, 30 s; timeoutMs of the data cache).

  • Stale-while-revalidate: the background refresh of a stale page or value goes to the platform's waitUntil when the request has one, a failed refresh keeps the stale copy, is reported once and backs off (cache.backoffBaseMs 1 s, cache.backoffMaxMs 60 s), pages answer with Age and s-maxage equal 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 (no SCAN), a tag purge that does not lose the index of a concurrent write, and an optional tag index expiry. CacheStore gained an optional purgeTag.

  • runStoreContract(store) and STORE_CONTRACT (@shadow-js/dusk/stores/contract): the scenarios every page cache store passes; the memory store, the KV adapters and redisStore run 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.

  • --json on every command of the dusk bin, in one documented contract (schema 1: schema, command, ok, data, warnings and, on failure, error: { code, message } with a stable DUSK_ code). build, doctor, completion, --help and --version print one document; dev and start print JSON Lines (ready with 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 the data of dusk build --json and the doctor checks are data.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 }) takes level, types (replaces the default compressible list) and filter(request, response), which skips compression unless it returns true, 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 a types or filter of the wrong kind throws with the option's name. zstd is negotiated by q-value after brotli an...

Read more