You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In a generated scaffold, the opt-in service worker (#271) cannot register at the path webjs documents. Static assets under public/ serve ONLY at /public/*, but every service-worker surface registers /sw.js at the site root:
agent-docs/service-worker.md (the registration snippet + "Registered from /sw.js, the worker's scope is the site root /").
The shipped packages/cli/templates/public/sw.js self-registration comment (/sw.js?v=<build>).
Root AGENTS.md app layout documents public/* as "static assets, served at /<name>" (i.e. root), which itself contradicts the actual /public/* serving.
Empirically, in a freshly generated full-stack app: GET /sw.js -> 404, GET /public/sw.js -> 200 (same for /offline.html vs /public/offline.html). So a user who follows the documented recipe registers a 404 and gets no worker. Worse, the only path that DOES serve (/public/sw.js) gives the worker a /public/* scope by default, so it would not control the site root even if registered, unless the response carries Service-Worker-Allowed: /.
Found while reviewing the scaffold feature gallery (#826): the new app/features/service-worker demo had to choose a path, and this inconsistency surfaced. The demo now uses /sw.js (matching the canonical docs + correct scope), which will 404 until this is fixed.
Design / approach
Serve the small set of "root assets" a webjs app expects at the site root, from public/. The framework already special-cases /favicon.ico -> public/favicon.ico in the same branch, so extend that to the service-worker + PWA root files: /sw.js, /offline.html, /robots.txt, /manifest.webmanifest (the exact set can match what the metadata routes + service-worker feature assume). For /sw.js, also send Service-Worker-Allowed: / so the worker controls the whole origin regardless of the served path. Decide and document ONE canonical model, then make AGENTS.md, agent-docs/service-worker.md, the shipped public/sw.js, and the scaffold layout all agree with it. (The scaffold layout currently uses /public/tailwind-browser.js, which matches the actual /public/* serving, so the two conventions coexist inconsistently today.)
Implementation notes (for the implementing agent)
Where to edit: packages/server/src/dev.js around L1828-1839 (the path.startsWith('/public/') || path === '/favicon.ico' static branch) and the equivalent path in the prod listener(s) (packages/server/src/listener-*.js / the serve path) so dev and prod agree. Mirror the existing /favicon.ico -> /public/favicon.ico remap for the root-asset set, and add the Service-Worker-Allowed: / header on the /sw.js response.
Landmines: the existing branch has a traversal guard (decoded .. containment under appDir/public/); keep any new remap inside that guard. Service-worker SCOPE is the real trap: serving the bytes at root is necessary but the browser also scopes by the request path, so the Service-Worker-Allowed header (or serving the file physically at root) is required for site-wide scope. Bun listener (listener-bun.js) has its own static path; fix both (runtime parity, Support both Bun and Node runtimes (first-class create + run) #508) and add a test/bun/* assertion.
Invariants: keep it no-build; do not move public/ files at scaffold time (they stay under public/, only the SERVED URL changes).
Tests + docs: add a handle: /sw.js serves public/sw.js at root with Service-Worker-Allowed unit test (packages/server/test/dev/dev-handler.test.js), a Bun parity assertion, and reconcile AGENTS.md + agent-docs/service-worker.md + packages/cli/templates/public/sw.js + the scaffold gallery app/features/service-worker demo to the chosen canonical path.
Acceptance criteria
GET /sw.js serves public/sw.js (200) in a generated scaffold, in dev AND prod, on Node AND Bun.
The /sw.js response carries Service-Worker-Allowed: / (or the file is physically root-scoped) so the worker controls the whole origin.
agent-docs/service-worker.md, root AGENTS.md, the shipped public/sw.js, and the app/features/service-worker demo all describe the SAME, working path.
A counterfactual proves the test fires (revert the remap -> /sw.js 404s -> test red).
Docs / AGENTS.md updated to state the canonical public-root-asset serving model.
Problem
In a generated scaffold, the opt-in service worker (#271) cannot register at the path webjs documents. Static assets under
public/serve ONLY at/public/*, but every service-worker surface registers/sw.jsat the site root:agent-docs/service-worker.md(the registration snippet + "Registered from/sw.js, the worker's scope is the site root/").packages/cli/templates/public/sw.jsself-registration comment (/sw.js?v=<build>).AGENTS.mdapp layout documentspublic/*as "static assets, served at/<name>" (i.e. root), which itself contradicts the actual/public/*serving.Empirically, in a freshly generated full-stack app:
GET /sw.js-> 404,GET /public/sw.js-> 200 (same for/offline.htmlvs/public/offline.html). So a user who follows the documented recipe registers a 404 and gets no worker. Worse, the only path that DOES serve (/public/sw.js) gives the worker a/public/*scope by default, so it would not control the site root even if registered, unless the response carriesService-Worker-Allowed: /.Found while reviewing the scaffold feature gallery (#826): the new
app/features/service-workerdemo had to choose a path, and this inconsistency surfaced. The demo now uses/sw.js(matching the canonical docs + correct scope), which will 404 until this is fixed.Design / approach
Serve the small set of "root assets" a webjs app expects at the site root, from
public/. The framework already special-cases/favicon.ico->public/favicon.icoin the same branch, so extend that to the service-worker + PWA root files:/sw.js,/offline.html,/robots.txt,/manifest.webmanifest(the exact set can match what the metadata routes + service-worker feature assume). For/sw.js, also sendService-Worker-Allowed: /so the worker controls the whole origin regardless of the served path. Decide and document ONE canonical model, then make AGENTS.md,agent-docs/service-worker.md, the shippedpublic/sw.js, and the scaffold layout all agree with it. (The scaffold layout currently uses/public/tailwind-browser.js, which matches the actual/public/*serving, so the two conventions coexist inconsistently today.)Implementation notes (for the implementing agent)
packages/server/src/dev.jsaround L1828-1839 (thepath.startsWith('/public/') || path === '/favicon.ico'static branch) and the equivalent path in the prod listener(s) (packages/server/src/listener-*.js/ the serve path) so dev and prod agree. Mirror the existing/favicon.ico->/public/favicon.icoremap for the root-asset set, and add theService-Worker-Allowed: /header on the/sw.jsresponse...containment underappDir/public/); keep any new remap inside that guard. Service-worker SCOPE is the real trap: serving the bytes at root is necessary but the browser also scopes by the request path, so theService-Worker-Allowedheader (or serving the file physically at root) is required for site-wide scope. Bun listener (listener-bun.js) has its own static path; fix both (runtime parity, Support both Bun and Node runtimes (first-class create + run) #508) and add atest/bun/*assertion.public/files at scaffold time (they stay underpublic/, only the SERVED URL changes).handle: /sw.js serves public/sw.js at root with Service-Worker-Allowedunit test (packages/server/test/dev/dev-handler.test.js), a Bun parity assertion, and reconcileAGENTS.md+agent-docs/service-worker.md+packages/cli/templates/public/sw.js+ the scaffold galleryapp/features/service-workerdemo to the chosen canonical path.Acceptance criteria
GET /sw.jsservespublic/sw.js(200) in a generated scaffold, in dev AND prod, on Node AND Bun./sw.jsresponse carriesService-Worker-Allowed: /(or the file is physically root-scoped) so the worker controls the whole origin.agent-docs/service-worker.md, rootAGENTS.md, the shippedpublic/sw.js, and theapp/features/service-workerdemo all describe the SAME, working path./sw.js404s -> test red).