@webjsdev/server 0.8.45 → 0.8.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/server",
3
- "version": "0.8.45",
3
+ "version": "0.8.47",
4
4
  "type": "module",
5
5
  "description": "webjs dev/prod server: SSR, router, API, server actions, live reload",
6
6
  "main": "index.js",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The optional `route(action, opts?)` convenience adapter (#488).
3
3
  *
4
- * REST endpoints in webjs go through `route.ts` (the framework's first-class
4
+ * REST endpoints in WebJs go through `route.ts` (the framework's first-class
5
5
  * HTTP handler). The ALWAYS-WORKS baseline is a hand-written `route.ts` that
6
6
  * imports a `'use server'` action and calls it:
7
7
  *
@@ -28,6 +28,22 @@
28
28
  * export const POST = route(createPost, { validate: createPostSchema });
29
29
  * ```
30
30
  *
31
+ * Passing the action's MODULE NAMESPACE instead of the function auto-applies the
32
+ * action's declared `export const middleware` and `export const validate`, so a
33
+ * guard declared once next to the action protects the REST boundary the same way
34
+ * it protects the RPC boundary (#876). This is the way to keep "declare once,
35
+ * protected on every entry point" true without re-listing the config:
36
+ *
37
+ * ```js
38
+ * // app/api/posts/route.ts
39
+ * import { route } from '@webjsdev/server';
40
+ * import * as postActions from '../../../modules/posts/actions/create-post.server.ts';
41
+ * export const POST = route(postActions); // its declared middleware + validate apply
42
+ * ```
43
+ *
44
+ * The function form still takes explicit `opts`, and an explicit `opts.middleware`
45
+ * / `opts.validate` overrides the module's declared config when both are given.
46
+ *
31
47
  * Adapter rules when invoked over HTTP:
32
48
  * - URL query params, route params (`ctx.params`), and the parsed JSON body
33
49
  * (for a method with a body) are merged into a single object argument
@@ -51,6 +67,7 @@
51
67
  import { runValidate } from './actions.js';
52
68
  import { runWithActionSignal } from './action-signal.js';
53
69
  import { runActionChain } from './action-middleware.js';
70
+ import { actionFunctionNames, actionMiddleware } from './action-config.js';
54
71
  import { readTextBounded, payloadTooLarge, DEFAULT_MAX_BODY_BYTES } from './body-limit.js';
55
72
  import { getBodyLimits } from './context.js';
56
73
 
@@ -69,16 +86,48 @@ function jsonBodyLimit() {
69
86
  * Expose a plain `'use server'` action over REST as a `route.ts`-style handler.
70
87
  *
71
88
  * @template A, R
72
- * @param {(input: A, ctx: { req: Request, params: Record<string,string> }) => R | Promise<R>} action
73
- * the server action to call (its source is unchanged; this only wraps the
74
- * HTTP-to-action adapter around it).
89
+ * @param {((input: A, ctx: { req: Request, params: Record<string,string> }) => R | Promise<R>) | Record<string, unknown>} actionOrModule
90
+ * the server action to call, OR the action's module namespace (`import * as m`)
91
+ * so its declared `middleware` / `validate` config is auto-applied (#876). Its
92
+ * source is unchanged; this only wraps the HTTP-to-action adapter around it.
75
93
  * @param {{
76
94
  * validate?: (input: any) => any,
77
95
  * middleware?: Function[],
78
96
  * }} [opts]
79
97
  * @returns {(req: Request, ctx?: { params?: Record<string,string> }) => Promise<Response>}
80
98
  */
81
- export function route(action, opts = {}) {
99
+ export function route(actionOrModule, opts = {}) {
100
+ // Resolve the callable action plus any config the caller did not pass
101
+ // explicitly. A function is used verbatim (the original form). A module
102
+ // namespace exposes the single action function and its declared config, so
103
+ // `route(mod)` inherits the same `middleware` / `validate` the RPC boundary
104
+ // reads (#876). An explicit `opts` value still wins over the declared one.
105
+ let action;
106
+ /** @type {{ middleware?: Function[], validate?: Function }} */
107
+ let declared = {};
108
+ if (typeof actionOrModule === 'function') {
109
+ action = actionOrModule;
110
+ } else if (actionOrModule && typeof actionOrModule === 'object') {
111
+ const names = actionFunctionNames(actionOrModule);
112
+ if (names.length !== 1) {
113
+ throw new Error(
114
+ `route(module) expects exactly one action function in the module, found ${names.length}` +
115
+ (names.length ? ` (${names.join(', ')})` : ''),
116
+ );
117
+ }
118
+ action = /** @type {Function} */ (actionOrModule[names[0]]);
119
+ declared = {
120
+ middleware: actionMiddleware(actionOrModule),
121
+ validate: typeof actionOrModule.validate === 'function'
122
+ ? /** @type {Function} */ (actionOrModule.validate)
123
+ : undefined,
124
+ };
125
+ } else {
126
+ throw new Error('route() expects an action function or a module namespace');
127
+ }
128
+ const validate = opts.validate ?? declared.validate;
129
+ const middlewareChain = opts.middleware ?? declared.middleware ?? [];
130
+
82
131
  return async function handler(req, ctx) {
83
132
  const params = (ctx && ctx.params) || {};
84
133
  const url = new URL(req.url);
@@ -102,13 +151,13 @@ export function route(action, opts = {}) {
102
151
  }
103
152
 
104
153
  let arg = { ...query, ...params, ...body };
105
- if (typeof opts.validate === 'function') {
154
+ if (typeof validate === 'function') {
106
155
  // Run the validator through the SHARED contract (#245) so the REST adapter
107
156
  // and the RPC path interpret a `{ success, fieldErrors }` envelope, a
108
157
  // throw, and a transform-return identically. A structured failure becomes
109
158
  // a 422 JSON; a throw stays a 400 (keeping a schema lib's `issues`); a
110
159
  // transform-return replaces the input.
111
- const v = runValidate(opts.validate, arg);
160
+ const v = runValidate(validate, arg);
112
161
  if (!v.ok) {
113
162
  if (v.thrown !== undefined) {
114
163
  const msg = v.result.error || 'Invalid input';
@@ -126,10 +175,9 @@ export function route(action, opts = {}) {
126
175
  // The action runs inside the request signal scope (#492) and the per-action
127
176
  // middleware chain (#490). `ranAction` distinguishes a real completion from
128
177
  // a middleware short-circuit (the action never ran).
129
- const middleware = opts.middleware || [];
130
178
  let ranAction = false;
131
179
  const result = await runWithActionSignal(req.signal, () =>
132
- runActionChain(middleware, { request: req, args: [arg], signal: req.signal }, () => {
180
+ runActionChain(middlewareChain, { request: req, args: [arg], signal: req.signal }, () => {
133
181
  ranAction = true;
134
182
  return action(/** @type any */ (arg), { req, params });
135
183
  }));
package/src/actions.js CHANGED
@@ -33,12 +33,12 @@ function jsonBodyLimit() {
33
33
  }
34
34
 
35
35
  /**
36
- * Internal RPC wire-format content type. Distinguishes webjs action
36
+ * Internal RPC wire-format content type. Distinguishes WebJs action
37
37
  * responses from plain `application/json` so the stub can pick the right
38
38
  * parser and external JSON consumers aren't confused.
39
39
  *
40
40
  * Uses the content type from the active serializer (defaults to
41
- * `application/vnd.webjs+json` with the built-in webjs serializer).
41
+ * `application/vnd.webjs+json` with the built-in WebJs serializer).
42
42
  */
43
43
  export const RPC_CONTENT_TYPE = 'application/vnd.webjs+json';
44
44
 
package/src/auth.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * NextAuth/Auth.js-style authentication for webjs.
2
+ * NextAuth/Auth.js-style authentication for WebJs.
3
3
  *
4
4
  * JWT or database sessions, Credentials + OAuth (Google, GitHub) providers.
5
5
  * Uses Web Crypto HMAC-SHA256: no external dependencies.
package/src/body-limit.js CHANGED
@@ -252,7 +252,7 @@ export async function readBytesBounded(req, limit) {
252
252
  /**
253
253
  * Read a request body as text, bounded by `limit`. Used by the RPC endpoint,
254
254
  * `readBody`, and the exposed-action REST path, all of which then parse the
255
- * text (webjs wire or JSON).
255
+ * text (WebJs wire or JSON).
256
256
  *
257
257
  * @param {Request} req
258
258
  * @param {number} limit max bytes (0 / negative = unlimited)
package/src/check.js CHANGED
@@ -14,7 +14,7 @@ import { analyzeElision } from './component-elision.js';
14
14
  import { RESERVED_CONFIG } from './action-config.js';
15
15
 
16
16
  /**
17
- * Convention validator for webjs apps.
17
+ * Convention validator for WebJs apps.
18
18
  *
19
19
  * Scans an app directory and reports correctness violations: things that
20
20
  * crash the app, leak a secret, or fail the build / type-strip. Designed to be
@@ -380,7 +380,7 @@ function findBrowserMemberUses(code) {
380
380
  }
381
381
 
382
382
  /**
383
- * Scan a webjs app directory and report convention violations.
383
+ * Scan a WebJs app directory and report convention violations.
384
384
  *
385
385
  * Every rule is a correctness check (a crash, a security leak, or a
386
386
  * build/type-strip failure), so they all run unconditionally. There is no
@@ -436,7 +436,7 @@ export async function checkConventions(appDir) {
436
436
  // redactor leaves them alone.
437
437
  if (!/class\s+\w+\s+extends\s+WebComponent/.test(scan)) continue;
438
438
  // Accept either registration pattern:
439
- // Counter.register('tag') (webjs idiom)
439
+ // Counter.register('tag') (WebJs idiom)
440
440
  // customElements.define('tag', Counter) (native)
441
441
  if (/\b[A-Z][A-Za-z0-9_$]*\.register\s*\(\s*['"`]/.test(scan)) continue;
442
442
  if (/\bcustomElements\.define\s*\(/.test(scan)) continue;
@@ -1208,7 +1208,7 @@ function findImportChain(graph, from, to) {
1208
1208
  * @param {Violation[]} violations appended to in place
1209
1209
  */
1210
1210
  async function checkServerImportInBrowserModule(appDir, violations) {
1211
- // No `app/` directory means this is not a routable webjs app (e.g. a bare
1211
+ // No `app/` directory means this is not a routable WebJs app (e.g. a bare
1212
1212
  // component library, or a fixture with only `lib/`); nothing ships, so the
1213
1213
  // rule has nothing to police. Skip rather than do the heavy analysis.
1214
1214
  if (!(await pathExists(join(appDir, 'app')))) return;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Static analyser deciding whether a webjs component module can be
2
+ * Static analyser deciding whether a WebJs component module can be
3
3
  * ELIDED from the browser, that is, never downloaded as JS because its
4
4
  * SSR'd HTML is the complete, final output and the component does no
5
5
  * client-side work.
@@ -34,7 +34,7 @@
34
34
  * packages/core/AGENTS.md and the MAINTAINER NOTE on the
35
35
  * WebComponent lifecycle in packages/core/src/component.js both
36
36
  * direct anyone adding an interactivity surface to update these
37
- * lists, since webjs development is largely AI-agent driven.
37
+ * lists, since WebJs development is largely AI-agent driven.
38
38
  */
39
39
 
40
40
  import {
@@ -101,7 +101,7 @@ export const CLIENT_LIFECYCLE_HOOKS = [
101
101
  'connectedCallback',
102
102
  'disconnectedCallback',
103
103
  'attributeChangedCallback',
104
- // Standard custom-element callback webjs does not itself define (so it
104
+ // Standard custom-element callback WebJs does not itself define (so it
105
105
  // is absent from the prototype guard's CLASSIFICATION), but an author
106
106
  // can still override it to do client work. Kept for conservatism.
107
107
  'adoptedCallback',
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Server-side scanner that walks the app tree and records the
3
- * browser-visible URL for every webjs component module.
3
+ * browser-visible URL for every WebJs component module.
4
4
  *
5
5
  * Called once on the first request (lazily, via `ensureReady`), then memoized. Results are used to prime the core
6
6
  * registry (`primeModuleUrl`) BEFORE any SSR render: so when a page
7
7
  * renders a component tag, `lookupModuleUrl(tag)` already has the URL
8
8
  * ready for `<link rel="modulepreload">` hints.
9
9
  *
10
- * The convention webjs uses is the web-standard one:
10
+ * The convention WebJs uses is the web-standard one:
11
11
  *
12
12
  * class Counter extends WebComponent { … }
13
13
  * customElements.define('my-counter', Counter);
package/src/cors.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Reusable CORS primitive for webjs.
2
+ * Reusable CORS primitive for WebJs.
3
3
  *
4
- * `cors(options)` returns a webjs MIDDLEWARE `(req, next) => Response`,
4
+ * `cors(options)` returns a WebJs MIDDLEWARE `(req, next) => Response`,
5
5
  * usable in `middleware.js` (root or per-segment) or wrapped around a
6
6
  * `route.js` handler. This is the public app-facing API. The shared
7
7
  * origin-resolution + header-building core (`resolveOrigin` /
package/src/csp.js CHANGED
@@ -181,7 +181,7 @@ export function readCspConfig(pkg) {
181
181
  export function mintNonce() {
182
182
  const bytes = new Uint8Array(16);
183
183
  crypto.getRandomValues(bytes);
184
- // Buffer is available in every webjs server runtime (Node 24+). Avoids a
184
+ // Buffer is available in every WebJs server runtime (Node 24+). Avoids a
185
185
  // hand-rolled base64 of a binary string.
186
186
  return Buffer.from(bytes).toString('base64');
187
187
  }
package/src/csrf.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * `cop-middleware` and Go 1.25's `http.CrossOriginProtection` use, and the
5
5
  * spiritual sibling of Next.js / Astro's Origin-vs-Host check.
6
6
  *
7
- * Why not a token cookie: webjs previously issued a per-request `webjs_csrf`
7
+ * Why not a token cookie: WebJs previously issued a per-request `webjs_csrf`
8
8
  * double-submit cookie on every SSR response. That made SSR HTML
9
9
  * un-cacheable at a CDN (a CDN skips a response with `Set-Cookie`, and a
10
10
  * cached one would share / poison the token across visitors). A header check
package/src/dev-error.js CHANGED
@@ -19,7 +19,7 @@ import { readFileSync } from 'node:fs';
19
19
  /**
20
20
  * Pull the first useful `file:line:column` out of an error stack. Prefers a
21
21
  * frame under `appDir` (the user's own code) over a framework / node_modules
22
- * frame, so the overlay points at the edit that broke, not webjs internals.
22
+ * frame, so the overlay points at the edit that broke, not WebJs internals.
23
23
  *
24
24
  * Handles the V8 stack forms `at fn (/abs/file.ts:12:5)`, `at /abs/file.ts:12:5`,
25
25
  * and `at file:///abs/file.ts:12:5`.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The dev live-reload SharedWorker relay (#887), the BROWSER half. Kept as a
3
+ * standalone browser-safe module (no node imports) so the served worker inlines
4
+ * the EXACT source a browser test drives, with no drift, the same pattern as
5
+ * `dev-overlay.js` (#264). `reloadWorkerJs` in dev.js reads this file, strips
6
+ * the `export` keyword, and appends a
7
+ * `startReloadWorker(self, EventSource, '<eventsUrl>')` call.
8
+ *
9
+ * One SharedWorker is shared across every tab of the origin (a SharedWorker is
10
+ * keyed by its script URL), so it holds the ONE `EventSource` to
11
+ * `/__webjs/events` and fans each `reload` / `webjs-error` out to every tab over
12
+ * its `MessagePort`. Tab count never touches the browser's per-host HTTP/1.1
13
+ * connection cap, which the per-tab `EventSource` it replaces used to exhaust.
14
+ *
15
+ * @param {{ onconnect: any }} scope the worker global (`self`)
16
+ * @param {new (url: string) => any} EventSourceCtor the `EventSource` constructor
17
+ * @param {string} eventsUrl the base-path-aware `/__webjs/events` URL
18
+ */
19
+ export function startReloadWorker(scope, EventSourceCtor, eventsUrl) {
20
+ /** @type {Set<any>} */
21
+ const ports = new Set();
22
+ /** @type {string | null} the last error frame, cached for late-joining tabs */
23
+ let lastError = null;
24
+
25
+ // A MessagePort has no reliable close event, so prune a port when a post to it
26
+ // throws (a closed tab). Some browsers silently no-op instead of throwing,
27
+ // leaving a dead port in the set, but that is a harmless dev-only no-op and
28
+ // the set is bounded by the tabs opened in one session.
29
+ function fanout(msg) {
30
+ for (const p of ports) {
31
+ try { p.postMessage(msg); } catch (_) { ports.delete(p); }
32
+ }
33
+ }
34
+
35
+ const es = new EventSourceCtor(eventsUrl);
36
+ es.addEventListener('reload', () => { lastError = null; fanout({ type: 'reload' }); });
37
+ es.addEventListener('webjs-error', (e) => { lastError = e.data; fanout({ type: 'webjs-error', data: e.data }); });
38
+
39
+ scope.onconnect = (e) => {
40
+ const port = e.ports[0];
41
+ ports.add(port);
42
+ port.start();
43
+ // A tab that connects AFTER a breaking edit still needs the current overlay.
44
+ // The single shared EventSource already consumed the server's replay (#264),
45
+ // so the worker caches the last error and hands it to each new tab itself.
46
+ if (lastError != null) {
47
+ try { port.postMessage({ type: 'webjs-error', data: lastError }); } catch (_) { ports.delete(port); }
48
+ }
49
+ };
50
+
51
+ // Returned for tests; the served worker ignores it.
52
+ return { ports, es };
53
+ }
package/src/dev.js CHANGED
@@ -136,7 +136,7 @@ const MIME = {
136
136
  * `namespace` with values, parameter properties, legacy decorators
137
137
  * with `emitDecoratorMetadata`, `import = require`) throws at strip
138
138
  * time. The `erasable-typescript-only` and `no-non-erasable-typescript`
139
- * lint rules catch these at edit time. webjs is buildless end-to-end:
139
+ * lint rules catch these at edit time. WebJs is buildless end-to-end:
140
140
  * there is no bundler fallback.
141
141
  *
142
142
  * The transformed bytes are cached per request handler in `state.tsCache`
@@ -441,7 +441,7 @@ export async function readServerTimeoutsFromApp(appDir) {
441
441
  }
442
442
 
443
443
  /**
444
- * Create a reusable, framework-agnostic request handler for a webjs app.
444
+ * Create a reusable, framework-agnostic request handler for a WebJs app.
445
445
  * The returned `handle(req)` takes a standard `Request` and resolves to a
446
446
  * standard `Response`: suitable for Node http, Deno, Bun, Cloudflare Workers,
447
447
  * or embedding inside an Express/Fastify app.
@@ -456,7 +456,7 @@ export async function readServerTimeoutsFromApp(appDir) {
456
456
  * }} opts
457
457
  */
458
458
  export async function createRequestHandler(opts) {
459
- // Preflight: webjs needs Node 24+ (built-in TS strip + recursive fs.watch).
459
+ // Preflight: WebJs needs Node 24+ (built-in TS strip + recursive fs.watch).
460
460
  // Throw a clear Error here so an embedded host (Express/Fastify/Bun/Deno)
461
461
  // gets the actionable message at boot, not a cryptic API failure mid-request.
462
462
  assertNodeVersion({ onFail: 'throw' });
@@ -1464,7 +1464,7 @@ export async function createRequestHandler(opts) {
1464
1464
  }
1465
1465
 
1466
1466
  /**
1467
- * Start a webjs HTTP server. Thin wrapper around `createRequestHandler`.
1467
+ * Start a WebJs HTTP server. Thin wrapper around `createRequestHandler`.
1468
1468
  *
1469
1469
  * Speaks plain HTTP/1.1. TLS termination + HTTP/2 to the browser is
1470
1470
  * expected to be handled by a reverse proxy (PaaS edge, nginx, Caddy,
@@ -1745,6 +1745,14 @@ async function tryServeFrameworkStatic(path, method, ctx) {
1745
1745
  });
1746
1746
  }
1747
1747
 
1748
+ // Dev live-reload SharedWorker: one shared connection for all tabs (#887).
1749
+ if (path === '/__webjs/reload-worker.js') {
1750
+ if (!dev) return new Response('Not found', { status: 404 });
1751
+ return new Response(reloadWorkerJs(basePath()), {
1752
+ headers: { 'content-type': 'application/javascript; charset=utf-8' },
1753
+ });
1754
+ }
1755
+
1748
1756
  // Core module: /__webjs/core/*
1749
1757
  //
1750
1758
  // ETag + ~1h max-age, NOT immutable. The URL path is un-versioned
@@ -1822,7 +1830,8 @@ async function handleCore(req, ctx) {
1822
1830
 
1823
1831
  // Health / readiness probes (`/__webjs/health`, `/__webjs/ready`) and the
1824
1832
  // framework-internal static assets (`/__webjs/core/*`, `/__webjs/reload.js`,
1825
- // downloaded `/__webjs/vendor/*`) are served in `handle()` BEFORE ensureReady,
1833
+ // `/__webjs/reload-worker.js`, downloaded `/__webjs/vendor/*`) are served in
1834
+ // `handle()` BEFORE ensureReady,
1826
1835
  // so they are not repeated here. This fallback covers the (currently
1827
1836
  // unreachable) case of handleCore being entered for one of those assets, so
1828
1837
  // the routing stays correct if a future caller bypasses the early path.
@@ -2138,7 +2147,7 @@ async function loadMiddleware(appDir, dev, logger) {
2138
2147
  * @param {import('./logger.js').Logger} logger
2139
2148
  */
2140
2149
  /**
2141
- * Create a plain HTTP/1.1 server. webjs deploys are expected to sit
2150
+ * Create a plain HTTP/1.1 server. WebJs deploys are expected to sit
2142
2151
  * behind a reverse proxy (PaaS edge, nginx, Caddy, etc.) that handles
2143
2152
  * TLS termination and speaks HTTP/2 to clients: Node's http2 module
2144
2153
  * doesn't need to be involved on the framework side.
@@ -2259,7 +2268,25 @@ async function sendWebResponse(res, webRes, req, opts) {
2259
2268
  */
2260
2269
  async function fileResponse(abs, opts) {
2261
2270
  try {
2262
- const data = await readFile(abs);
2271
+ let data = await readFile(abs);
2272
+ // In dev an external watcher (tailwindcss --watch, esbuild, ...) rewrites a
2273
+ // public asset with truncate-then-write, so the file is 0 bytes for a short
2274
+ // window during a rebuild. A hot reload that lands in that window would
2275
+ // serve empty CSS / JS and the page paints unstyled (#891). Close that
2276
+ // 0-byte window: when an empty read comes from a file that was JUST modified
2277
+ // (the mid-rewrite signal), re-read a few times so the truncated read does
2278
+ // not reach the browser. A genuinely empty, untouched asset is served
2279
+ // immediately (no delay), and a non-zero-but-partial write is a smaller
2280
+ // residual gap this does not cover. Prod has no such watcher and is
2281
+ // left untouched.
2282
+ if (opts.dev && data.length === 0) {
2283
+ let midRewrite = false;
2284
+ try { midRewrite = Date.now() - (await stat(abs)).mtimeMs < 500; } catch { /* gone */ }
2285
+ for (let i = 0; midRewrite && i < 12 && data.length === 0; i++) {
2286
+ await new Promise((r) => setTimeout(r, 20));
2287
+ try { data = await readFile(abs); } catch { break; }
2288
+ }
2289
+ }
2263
2290
  const type = MIME[extname(abs).toLowerCase()] || 'application/octet-stream';
2264
2291
  // The body is fully buffered (read into `data`), so opt it into the
2265
2292
  // conditional-GET funnel, which is the single place that hashes the bytes
@@ -2327,7 +2354,7 @@ async function exists(p) {
2327
2354
  * dev server returns the error to the caller. The
2328
2355
  * `erasable-typescript-only` and `no-non-erasable-typescript` lint
2329
2356
  * rules catch these at edit time. There is no bundler fallback;
2330
- * webjs is buildless end-to-end.
2357
+ * WebJs is buildless end-to-end.
2331
2358
  *
2332
2359
  * The backend is the runtime-appropriate stripper (#508): Node 24+'s built-in
2333
2360
  * `module.stripTypeScriptTypes`, or `amaro` on Bun (byte-identical, equally
@@ -2609,18 +2636,74 @@ function locatePackageDir(appDir, pkgName) {
2609
2636
  const DEV_OVERLAY_SRC = readFileSync(new URL('./dev-overlay.js', import.meta.url), 'utf8')
2610
2637
  .replace(/^export /gm, '');
2611
2638
 
2639
+ // The reload SharedWorker relay source (#887), read once + `export`-stripped so
2640
+ // it inlines into the served worker script. Sharing the one source
2641
+ // (`dev-reload-worker.js`, which the browser test imports directly) means the
2642
+ // test drives the EXACT relay code that ships, with no drift (same as #264).
2643
+ const RELOAD_WORKER_SRC = readFileSync(new URL('./dev-reload-worker.js', import.meta.url), 'utf8')
2644
+ .replace(/^export /gm, '');
2645
+
2612
2646
  function reloadClientJs(bp) {
2613
2647
  // The overlay renderer uses textContent throughout (never innerHTML), so the
2614
2648
  // error message / code frame can never inject markup (#264). Served only in
2615
2649
  // dev (the /__webjs/reload.js branch 404s in prod), so it never reaches a
2616
2650
  // production page.
2651
+ //
2652
+ // Every tab shares ONE live-reload connection through a SharedWorker (#887).
2653
+ // The old client opened an `EventSource` per tab, and because the dev server
2654
+ // is HTTP/1.1 and browsers cap concurrent connections per host at ~6, a
2655
+ // handful of open tabs would hold every connection slot with idle SSE streams
2656
+ // and later tabs could not even fetch their HTML. The SharedWorker holds the
2657
+ // single `EventSource` (see reloadWorkerJs) and relays each `reload` /
2658
+ // `webjs-error` to every tab. The overlay still renders on the main thread
2659
+ // (a worker has no DOM), so the worker forwards only the raw frame data.
2660
+ //
2661
+ // Fallback: where `SharedWorker` is missing (some mobile browsers) or its
2662
+ // construction throws (a strict dev CSP without `worker-src`), each tab opens
2663
+ // its own `EventSource`, the original behaviour. Correct, just not shared.
2664
+ const eventsUrl = JSON.stringify(withBasePath('/__webjs/events', bp));
2665
+ const workerUrl = JSON.stringify(withBasePath('/__webjs/reload-worker.js', bp));
2617
2666
  return `// webjs dev reload client
2618
2667
  ${DEV_OVERLAY_SRC}
2619
- const es = new EventSource(${JSON.stringify(withBasePath('/__webjs/events', bp))});
2620
- es.addEventListener('reload', () => location.reload());
2621
- es.addEventListener('webjs-error', (e) => {
2622
- let f; try { f = JSON.parse(e.data); } catch (_) { return; }
2668
+ function __webjsApplyError(data) {
2669
+ let f; try { f = JSON.parse(data); } catch (_) { return; }
2623
2670
  renderDevOverlay(f);
2624
- });
2671
+ }
2672
+ function __webjsDirectEvents() {
2673
+ const es = new EventSource(${eventsUrl});
2674
+ es.addEventListener('reload', () => location.reload());
2675
+ es.addEventListener('webjs-error', (e) => __webjsApplyError(e.data));
2676
+ }
2677
+ try {
2678
+ if (typeof SharedWorker !== 'undefined') {
2679
+ const w = new SharedWorker(${workerUrl});
2680
+ w.port.onmessage = (e) => {
2681
+ const m = e.data || {};
2682
+ if (m.type === 'reload') location.reload();
2683
+ else if (m.type === 'webjs-error') __webjsApplyError(m.data);
2684
+ };
2685
+ w.port.start();
2686
+ } else {
2687
+ __webjsDirectEvents();
2688
+ }
2689
+ } catch (_) {
2690
+ __webjsDirectEvents();
2691
+ }
2692
+ `;
2693
+ }
2694
+
2695
+ /**
2696
+ * The dev live-reload SharedWorker. One instance is shared across every tab of
2697
+ * the same origin (a SharedWorker is keyed by its script URL), so it holds the
2698
+ * ONE `EventSource` to `/__webjs/events` and fans each event out to all tabs
2699
+ * over their `MessagePort`s (#887). The `EventSource` URL carries the base path
2700
+ * the same way the client's does (#256). Served only in dev.
2701
+ * @param {string} bp the normalized base path (`''` = no-op)
2702
+ * @returns {string}
2703
+ */
2704
+ function reloadWorkerJs(bp) {
2705
+ return `// webjs dev reload worker (one shared connection for all tabs)
2706
+ ${RELOAD_WORKER_SRC}
2707
+ startReloadWorker(self, EventSource, ${JSON.stringify(withBasePath('/__webjs/events', bp))});
2625
2708
  `;
2626
2709
  }
@@ -2,7 +2,7 @@
2
2
  * App-level elision report (#646).
3
3
  *
4
4
  * A reporting layer over the `analyzeElision` verdict, NOT a second analysis
5
- * and NOT a build (webjs is no-build; elision is the server's analysis pass,
5
+ * and NOT a build (WebJs is no-build; elision is the server's analysis pass,
6
6
  * run at dev-server start and re-derived after each fs.watch rebuild). It
7
7
  * builds the module graph, runs `analyzeElision`, and returns the page / layout
8
8
  * route modules that SHIP WHOLE to the browser, each with the first
package/src/env-schema.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Startup env-var validation with a typed schema hook (issue #236).
3
3
  *
4
- * webjs auto-loads `<appDir>/.env` into `process.env` at boot, but does no
4
+ * WebJs auto-loads `<appDir>/.env` into `process.env` at boot, but does no
5
5
  * validation, so a missing or misconfigured required var (DATABASE_URL,
6
6
  * AUTH_SECRET, ...) fails late and cryptically (a database connect error
7
7
  * mid-request, an undefined secret signing a token). This module adds an
@@ -2,7 +2,7 @@
2
2
  * File storage primitive: a pluggable `FileStore` with a streaming local-disk
3
3
  * default adapter (issue #247).
4
4
  *
5
- * webjs already round-trips a native `File` / `Blob` / `FormData` over the RPC
5
+ * WebJs already round-trips a native `File` / `Blob` / `FormData` over the RPC
6
6
  * wire (the serializer), but nothing decided WHERE the bytes land. This module
7
7
  * is that answer. The model mirrors `cache.js` exactly: a documented interface,
8
8
  * a default adapter (`diskStore`), and a module singleton (`setFileStore` /
@@ -35,7 +35,7 @@
35
35
  * (`File` / `Blob` / `ReadableStream` / `Uint8Array` in, a `{ body, size,
36
36
  * contentType }` handle out), so an S3 / R2 / GCS adapter is a drop-in: it
37
37
  * implements the same `put` / `get` / `delete` / `url` and the call sites do
38
- * not change. webjs ships no S3 SDK; see the JSDoc on `FileStore` below.
38
+ * not change. WebJs ships no S3 SDK; see the JSDoc on `FileStore` below.
39
39
  *
40
40
  * @module file-storage
41
41
  */
package/src/forwarded.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Build a full URL from a Node IncomingMessage, respecting standard
3
3
  * reverse-proxy headers (`X-Forwarded-Proto`, `X-Forwarded-Host`).
4
4
  *
5
- * Why: webjs apps are almost always deployed behind a reverse proxy
5
+ * Why: WebJs apps are almost always deployed behind a reverse proxy
6
6
  * (Railway, Fly, Render, Vercel, Cloudflare, nginx, Caddy, Traefik -
7
7
  * see the no-build architecture docs). The proxy terminates TLS and
8
8
  * speaks plain HTTP/1.1 to the container, so `req.url` inside the
package/src/headers.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Secure-by-default response headers plus a small declarative per-path
3
3
  * header config (issue #232).
4
4
  *
5
- * webjs sets a baseline of standard security headers on every document
5
+ * WebJs sets a baseline of standard security headers on every document
6
6
  * and asset response, so a scaffolded app is not clickjackable or
7
7
  * MIME-sniffable out of the box (no reverse proxy required for the
8
8
  * baseline). The defaults are LITERAL HTTP headers, no abstraction.
package/src/json.js CHANGED
@@ -15,7 +15,7 @@ import { readTextBounded, BodyLimitError, DEFAULT_MAX_BODY_BYTES } from './body-
15
15
  * The helper reads the in-flight Request from the AsyncLocalStorage
16
16
  * request context. If the caller sent `Accept: application/vnd.webjs+json`
17
17
  * (e.g. via the `richFetch` client helper), the response body is
18
- * encoded with the webjs serializer so rich types (Date, Map, Set,
18
+ * encoded with the WebJs serializer so rich types (Date, Map, Set,
19
19
  * BigInt, TypedArrays, Blob, File, FormData, cycles) survive. Otherwise
20
20
  * the response is plain `application/json`, unchanged behaviour for
21
21
  * curl / external consumers.
@@ -47,7 +47,7 @@ export async function json(data, init = {}) {
47
47
  }
48
48
 
49
49
  /**
50
- * Parse a request body using the webjs serializer when the client sent
50
+ * Parse a request body using the WebJs serializer when the client sent
51
51
  * our content type, otherwise as plain JSON. Handy for route handlers
52
52
  * that accept rich bodies from the `richFetch` helper but plain JSON
53
53
  * from everyone else.
@@ -73,7 +73,7 @@ export function startBunListener(ctx) {
73
73
  const server = Bun.serve({
74
74
  port,
75
75
  idleTimeout: bunIdleTimeout(timeouts),
76
- // webjs owns its dev error overlay (over SSE) and handles thrown errors in
76
+ // WebJs owns its dev error overlay (over SSE) and handles thrown errors in
77
77
  // `fetch`, so Bun's own development error page must never interfere; keep it
78
78
  // deterministic (off) rather than tied to NODE_ENV.
79
79
  development: false,
@@ -172,7 +172,7 @@ export function startBunListener(ctx) {
172
172
  * the `open` handler; messages/close are forwarded to its emitter from the
173
173
  * `websocket.message` / `websocket.close` handlers.
174
174
  *
175
- * Only `'message'` and `'close'` are forwarded (the events webjs WS handlers and
175
+ * Only `'message'` and `'close'` are forwarded (the events WebJs WS handlers and
176
176
  * `broadcast()` use). Bun exposes no per-socket `'error'` / `'ping'` / `'pong'`
177
177
  * handler to bridge; a socket failure on Bun surfaces as a `'close'`, so
178
178
  * `'close'`-keyed cleanup still runs. Documented in the WebSockets doc.
package/src/logger.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Minimal pluggable logger.
3
3
  *
4
- * webjs doesn't pick pino/winston/bunyan for you. The default in prod emits
4
+ * WebJs doesn't pick pino/winston/bunyan for you. The default in prod emits
5
5
  * one JSON object per line to stdout: trivially ingestable by any log
6
6
  * aggregator. In dev, lines are plain text for readability.
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Node-version preflight guard (issue #238).
3
3
  *
4
- * webjs depends on Node 24+ built-ins: `module.stripTypeScriptTypes` (the
4
+ * WebJs depends on Node 24+ built-ins: `module.stripTypeScriptTypes` (the
5
5
  * no-build TypeScript strip) and recursive `fs.watch` (dev live-reload). On an
6
6
  * older Node the failure surfaces late and cryptically (a strip error or a
7
7
  * missing API deep inside a request), not as a clear "you need Node 24+". This
package/src/redirects.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Declarative permanent / temporary redirects (issue #254).
3
3
  *
4
- * webjs already ships `redirect(url)` (a per-request throw sentinel) for
4
+ * WebJs already ships `redirect(url)` (a per-request throw sentinel) for
5
5
  * imperative, request-time redirects. This module adds the missing
6
6
  * DECLARATIVE surface: a config of old-path -> new-path rules an app
7
7
  * declares once and the framework applies at the very start of request
package/src/serializer.js CHANGED
@@ -2,7 +2,7 @@ import { stringify, parse } from '@webjsdev/core';
2
2
 
3
3
  /**
4
4
  * @typedef {Object} Serializer
5
- * A pluggable serializer that controls how webjs server actions encode and
5
+ * A pluggable serializer that controls how WebJs server actions encode and
6
6
  * decode values on the RPC wire.
7
7
  *
8
8
  * **AI hint:** The default serializer uses webjs's built-in
@@ -50,7 +50,7 @@ let current = defaultSerializer;
50
50
  *
51
51
  * **AI hint:** Use this in server-side code that needs to encode or decode
52
52
  * RPC payloads. It returns whatever serializer was set via `setSerializer`,
53
- * or the default webjs serializer if none was set.
53
+ * or the default WebJs serializer if none was set.
54
54
  *
55
55
  * @returns {Serializer}
56
56
  */
package/src/testing.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Server-side test harness helpers (issue #267).
3
3
  *
4
- * `createRequestHandler(...).handle(request)` drives the FULL webjs pipeline
4
+ * `createRequestHandler(...).handle(request)` drives the FULL WebJs pipeline
5
5
  * (middleware, routing, SSR, page actions, server-action RPC, auth + CSRF), and
6
6
  * the framework's own suite already drives it. These helpers are THIN builders
7
7
  * over the native `Request` / `Response` around that `handle()`, so an app test
@@ -350,7 +350,7 @@ export async function rawActionRequest(app, serverFilePath, fnName, args = [], o
350
350
  }
351
351
 
352
352
  /**
353
- * Browser-test harness (#806): build a webjs handler that a `web-test-runner`
353
+ * Browser-test harness (#806): build a WebJs handler that a `web-test-runner`
354
354
  * config can proxy module requests to, so a `test/**​/browser/*.test.js` file
355
355
  * can import a real `.ts` component that imports a `'use server'` action and it
356
356
  * loads + hydrates in a real browser. Plain WTR serves raw TS with no webjs
@@ -369,7 +369,7 @@ export async function rawActionRequest(app, serverFilePath, fnName, args = [], o
369
369
  * of `@webjsdev/server/testing` stays light (they pull the full server + the
370
370
  * `ws` WebSocket subsystem).
371
371
  *
372
- * @param {string} appDir the webjs app root (usually `process.cwd()`)
372
+ * @param {string} appDir the WebJs app root (usually `process.cwd()`)
373
373
  * @returns {Promise<{ handle: Handle, warmup: () => Promise<void>, importmapHtml: () => string }>}
374
374
  */
375
375
  export async function createBrowserTestHandler(appDir) {
package/src/ts-strip.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Pluggable TypeScript stripper (#508), the one seam that lets webjs run on both
2
+ * Pluggable TypeScript stripper (#508), the one seam that lets WebJs run on both
3
3
  * Node and Bun.
4
4
  *
5
- * webjs serves `.ts` / `.mts` source to the browser as JavaScript by ERASING the
5
+ * WebJs serves `.ts` / `.mts` source to the browser as JavaScript by ERASING the
6
6
  * type syntax in place (position-preserving whitespace replacement, so no
7
7
  * sourcemap is shipped and stack traces stay byte-exact). On Node 24+ that is the
8
8
  * built-in `module.stripTypeScriptTypes`. Bun has no such built-in, so this
package/src/vendor.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Resolve bare npm imports to browser-loadable URLs via jspm.io.
3
3
  *
4
- * webjs follows the Rails 7 + importmap-rails posture exactly. When user
4
+ * WebJs follows the Rails 7 + importmap-rails posture exactly. When user
5
5
  * code imports a bare specifier (e.g. `import dayjs from 'dayjs'`), the
6
6
  * browser can't resolve it natively. The framework's job is to emit an
7
7
  * importmap that translates each bare specifier to a real URL.
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * importmap: { "dayjs": "https://ga.jspm.io/npm:dayjs@1.11.13/index.js" }
12
12
  *
13
- * The browser fetches the bundle directly from jspm.io. The webjs server
13
+ * The browser fetches the bundle directly from jspm.io. The WebJs server
14
14
  * does not proxy, cache, or bundle anything. jspm.io has done the work
15
15
  * server-side (CJS-to-ESM conversion, transitive bundling, browser
16
16
  * polyfills).
@@ -85,7 +85,7 @@ const FRAMEWORK_SERVER_ONLY = new Set(['@webjsdev/cli', '@webjsdev/server', '@we
85
85
  * Excludes:
86
86
  * - `node_modules`, `.webjs`, `public` directories
87
87
  * - Any directory starting with `_` (webjs `_private/` convention)
88
- * - `test/` and `tests/` (server-only by webjs convention)
88
+ * - `test/` and `tests/` (server-only by WebJs convention)
89
89
  * - Files whose name marks them as server-only:
90
90
  * * `*.server.{js,ts,mjs,mts}` (path-level boundary)
91
91
  * * `route.{js,ts,mjs,mts}` (file-router HTTP handler)
@@ -1030,7 +1030,7 @@ export async function readPinFile(appDir) {
1030
1030
  * When `integrity` is provided and non-empty, it's included alongside
1031
1031
  * `imports` as a sibling key (matching the browser importmap-integrity
1032
1032
  * spec: a flat `{url: 'sha384-...'}` map). Omitted entirely when empty
1033
- * so older webjs versions read the file as before.
1033
+ * so older WebJs versions read the file as before.
1034
1034
  *
1035
1035
  * `provider` is persisted alongside imports when non-default. It lets
1036
1036
  * `webjs vendor update` know which CDN to re-resolve against, and
@@ -1343,7 +1343,7 @@ export async function pinAll(appDir, opts = {}) {
1343
1343
  }
1344
1344
 
1345
1345
  // The app legitimately has zero bare-specifier imports (or the
1346
- // scanner is running outside a webjs project). Don't create an
1346
+ // scanner is running outside a WebJs project). Don't create an
1347
1347
  // empty `.webjs/vendor/importmap.json`. Without this guard the file
1348
1348
  // gets written as `{ imports: {} }` in whatever cwd the CLI was
1349
1349
  // invoked from, then immediately rejected by readPinFile's empty