@webjsdev/server 0.9.0 → 0.10.1

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.
Files changed (102) hide show
  1. package/README.md +4 -1
  2. package/index.d.ts +793 -125
  3. package/index.js +8 -4
  4. package/package.json +3 -3
  5. package/src/action-flash.js +185 -0
  6. package/src/action-params.js +156 -0
  7. package/src/action-route.js +8 -9
  8. package/src/action-seed.js +44 -5
  9. package/src/actions.js +97 -15
  10. package/src/api.js +67 -10
  11. package/src/app-layout.js +133 -0
  12. package/src/asset-hash.js +82 -49
  13. package/src/auth.js +612 -108
  14. package/src/browser-entries.js +2 -17
  15. package/src/build-epoch.js +1 -1
  16. package/src/cache-tags.js +5 -1
  17. package/src/check/rules-actions.js +37 -1
  18. package/src/check/rules-components.js +114 -40
  19. package/src/check/rules-imports.js +175 -4
  20. package/src/check/rules-route-modules.js +200 -0
  21. package/src/check/rules-routes.js +55 -0
  22. package/src/check/rules-routing.js +143 -95
  23. package/src/check/rules-typescript.js +24 -2
  24. package/src/check/rules.js +104 -9
  25. package/src/check/runner-support.js +53 -171
  26. package/src/check/runner.js +22 -6
  27. package/src/client-signals.js +171 -0
  28. package/src/component-scanner.js +22 -16
  29. package/src/context.js +152 -16
  30. package/src/cookie-store.js +269 -0
  31. package/src/core-exports.js +190 -0
  32. package/src/csrf.js +21 -3
  33. package/src/dev/bun-app-source.js +95 -5
  34. package/src/dev/config.js +7 -51
  35. package/src/dev/dev-service-worker.js +48 -0
  36. package/src/dev/esm-hot-hooks.js +373 -0
  37. package/src/dev/handler.js +502 -155
  38. package/src/dev/helpers.js +127 -96
  39. package/src/dev/hot-host.js +33 -6
  40. package/src/dev/poke-state.js +110 -0
  41. package/src/dev/serve.js +177 -129
  42. package/src/dev/server.js +92 -33
  43. package/src/dev/specifier-versions.js +125 -0
  44. package/src/dev/watch-ignore.js +49 -4
  45. package/src/dev/watch-poll.js +78 -0
  46. package/src/dev/watch-tree.js +59 -22
  47. package/src/dev-classify.js +59 -26
  48. package/src/dev-error.js +104 -3
  49. package/src/dev-hot-classify.js +291 -0
  50. package/src/dev-hot-client.js +245 -0
  51. package/src/dev-import.js +22 -5
  52. package/src/dev-overlay.js +26 -0
  53. package/src/dev-ready-probe.js +69 -0
  54. package/src/dev-regenerate.js +2 -2
  55. package/src/dev-reload-hold.js +49 -20
  56. package/src/dev-reload-reason.js +61 -0
  57. package/src/dev-reload-worker.js +62 -13
  58. package/src/dev-secret.js +73 -0
  59. package/src/dev-source-locations-hook.js +4 -1
  60. package/src/dev-source-locations.js +33 -2
  61. package/src/dev.js +0 -1
  62. package/src/env-files.js +124 -0
  63. package/src/file-storage.js +2 -2
  64. package/src/foreign-imports.js +228 -0
  65. package/src/form-action-identity.js +52 -25
  66. package/src/form-dispatch.js +142 -66
  67. package/src/html-cache.js +35 -4
  68. package/src/importmap.js +2 -2
  69. package/src/instrumentation.js +59 -3
  70. package/src/js-scan.js +5 -4
  71. package/src/jwe.js +155 -0
  72. package/src/lazy-components.js +166 -0
  73. package/src/listener-core.js +4 -2
  74. package/src/lit-hints.js +172 -0
  75. package/src/middleware-load.js +248 -0
  76. package/src/module-graph.js +73 -47
  77. package/src/module-side-effect.js +284 -0
  78. package/src/public-files.js +73 -0
  79. package/src/redirect-location.js +19 -0
  80. package/src/redirects.js +9 -1
  81. package/src/route-frontier.js +151 -0
  82. package/src/route-report.js +130 -0
  83. package/src/router.js +120 -13
  84. package/src/session.js +32 -10
  85. package/src/ssr/client-router-flag.js +1 -1
  86. package/src/ssr/document.js +37 -13
  87. package/src/ssr/head.js +78 -23
  88. package/src/ssr/preloads.js +31 -45
  89. package/src/ssr/render.js +241 -55
  90. package/src/ssr/responses.js +51 -4
  91. package/src/ssr/shipped-modules.js +22 -16
  92. package/src/testing.d.ts +71 -2
  93. package/src/testing.js +103 -21
  94. package/src/thenable-params.js +26 -0
  95. package/src/vendor/jspm.js +12 -1
  96. package/src/vendor/pins.js +9 -8
  97. package/src/vendor/scanner.js +14 -9
  98. package/src/vendor.js +5 -3
  99. package/webjs-config.schema.json +1 -6
  100. package/src/component-elision.js +0 -1828
  101. package/src/elision-differential.js +0 -101
  102. package/src/elision-report.js +0 -193
package/src/asset-hash.js CHANGED
@@ -71,25 +71,46 @@ let _appDir = '';
71
71
  let _coreDir = '';
72
72
 
73
73
  /**
74
- * A fingerprint of the current elision verdict (the elidable-component +
75
- * inert-route set), folded into an APP MODULE's hash. An app module's SERVED
76
- * body is not its raw source: `dev.js` runs `elideImportsFromSource` over it,
77
- * stripping a side-effect import of a display-only component. That strip is a
78
- * property of the IMPORTED component's verdict, not of the importer's own
79
- * bytes, so when a component flips display-only <-> interactive the importer's
80
- * served body changes while its source is byte-identical. Hashing the source
81
- * alone would keep the same `?v`, and a returning client would hold the stale
82
- * immutable copy (the now-interactive component never imported, so never
83
- * hydrated) for the full immutable TTL. Folding the verdict fingerprint into
84
- * the hash busts every app module's url on a verdict change, so the stale-copy
85
- * window cannot open. Empty when no module is elidable (then an app module's
86
- * hash is exactly `sha256(bytes)`), and only mixed for files under `_appDir`
87
- * (core / public assets are never elision-transformed). Set by `dev.js`'s
88
- * `ensureReady` after `analyzeElision`, re-set on every rebuild.
74
+ * A fingerprint of the current `static lazy` component set, folded into an
75
+ * APP MODULE's hash. An app module's SERVED body is not its raw source:
76
+ * `dev.js` runs `deferLazyImportsFromSource` over it, rewriting a side-effect
77
+ * import of a lazy component into a lazy-loader registration. That rewrite is
78
+ * a property of the IMPORTED component, not of the importer's own bytes, so
79
+ * when a component gains or loses `static lazy` the importer's served body
80
+ * changes while its source is byte-identical. Hashing the source alone would
81
+ * keep the same `?v`, and a returning client would hold the stale immutable
82
+ * copy for the full immutable TTL. Folding the fingerprint into the hash busts
83
+ * every app module's url on a change, so the stale-copy window cannot open.
84
+ * Empty when no component is lazy (then an app module's hash is exactly
85
+ * `sha256(bytes)`), and only mixed for files under `_appDir` (core / public
86
+ * assets are never rewritten at serve time). Set by `dev.js`'s `ensureReady`
87
+ * after the boot analysis, re-set on every rebuild.
89
88
  *
90
89
  * @type {string}
91
90
  */
92
- let _elisionFp = '';
91
+ let _serveFp = '';
92
+
93
+ /**
94
+ * The dev browser module versions (#1689), or null outside dev. When set,
95
+ * `withAssetHash` appends `?t=<n>` to a same-origin url whose file has a
96
+ * version above 0 instead of returning it unchanged, so the boot script, the
97
+ * modulepreload hints and the lazy-loader map name a bumped module by the same
98
+ * url its importers' rewritten specifiers do (`dev/specifier-versions.js`).
99
+ * One module, one url per document: two urls would be two module instances.
100
+ * @type {((abs: string) => number) | null}
101
+ */
102
+ let _devVersionOf = null;
103
+
104
+ /**
105
+ * Install (or clear, with null) the dev browser module version lookup.
106
+ * `dev/handler.js` passes `state.browserVersions.versionOf` in dev only.
107
+ * @param {((abs: string) => number) | null} fn
108
+ * @returns {void}
109
+ */
110
+ export function setDevModuleVersions(fn) {
111
+ _devVersionOf = typeof fn === 'function' ? fn : null;
112
+ bumpBuildEpoch();
113
+ }
93
114
 
94
115
  /**
95
116
  * Memoized per-file content hash. Keyed by absolute path. Cleared on the
@@ -118,20 +139,20 @@ export function setAssetRoots(opts) {
118
139
  }
119
140
 
120
141
  /**
121
- * Set the elision-verdict fingerprint folded into app-module hashes (see
122
- * `_elisionFp`). A no-op when the value is unchanged; on a change it clears the
123
- * hash cache so every app module re-hashes against the new verdict. `dev.js`
124
- * calls this from `ensureReady` after `analyzeElision` (and again on each
125
- * rebuild), passing a stable digest of the elidable-component + inert-route set
126
- * (or `''` when nothing is elidable).
142
+ * Set the lazy-component fingerprint folded into app-module hashes (see
143
+ * `_serveFp`). A no-op when the value is unchanged; on a change it clears the
144
+ * hash cache so every app module re-hashes against the new set. `dev.js`
145
+ * calls this from `ensureReady` after the boot analysis (and again on each
146
+ * rebuild), passing a stable digest of the lazy-component set (or `''` when
147
+ * no component is lazy).
127
148
  *
128
149
  * @param {string} fp
129
150
  * @returns {void}
130
151
  */
131
- export function setElisionFingerprint(fp) {
152
+ export function setServeFingerprint(fp) {
132
153
  const next = fp || '';
133
- if (next === _elisionFp) return;
134
- _elisionFp = next;
154
+ if (next === _serveFp) return;
155
+ _serveFp = next;
135
156
  _hashCache.clear();
136
157
  // Past the early return on purpose: an unchanged fingerprint moves no hash,
137
158
  // so the epoch-keyed memos over those hashes stay valid (#1644).
@@ -183,8 +204,8 @@ export function assetHashFor(absPath) {
183
204
  try {
184
205
  const bytes = readFileSync(absPath);
185
206
  const h = createHash('sha256').update(bytes);
186
- // An app module's served body is elision-transformed, so fold the verdict
187
- // fingerprint in (see `_elisionFp`). Core and `public/` files are never
207
+ // An app module's served body is rewritten at serve time, so fold the
208
+ // fingerprint in (see `_serveFp`). Core and `public/` files are never
188
209
  // transformed, so they hash over their bytes alone, which the two
189
210
  // exclusions below enforce rather than merely assume. Empty fp leaves an
190
211
  // app module's hash at exactly `sha256(bytes)`.
@@ -200,18 +221,18 @@ export function assetHashFor(absPath) {
200
221
  // in this monorepo, where the workspace symlink lands outside, which
201
222
  // is exactly why no in-repo test ever caught it.
202
223
  //
203
- // Neither file set is elision-transformed (core is served verbatim by
204
- // `fileResponse`), so folding the verdict in buys nothing and costs
205
- // correctness: any deploy that merely flips an UNRELATED component between
206
- // elidable and interactive would move their urls, re-downloading the whole
224
+ // Neither file set is rewritten (core is served verbatim by
225
+ // `fileResponse`), so folding the fingerprint in buys nothing and costs
226
+ // correctness: any deploy that merely flips an UNRELATED component's
227
+ // `static lazy` would move their urls, re-downloading the whole
207
228
  // core runtime plus every marked stylesheet, image, and script even though
208
229
  // not one of those bytes changed. That is the precise cost a content hash
209
230
  // exists to avoid.
210
- if (_elisionFp && _appDir && absPath.startsWith(_appDir + sep)
231
+ if (_serveFp && _appDir && absPath.startsWith(_appDir + sep)
211
232
  && !absPath.startsWith(join(_appDir, 'public') + sep)
212
233
  && !(_coreDir && absPath.startsWith(_coreDir + sep))) {
213
234
  h.update('\0');
214
- h.update(_elisionFp);
235
+ h.update(_serveFp);
215
236
  }
216
237
  const hash = h.digest('hex').slice(0, HASH_LEN);
217
238
  _hashCache.set(absPath, hash);
@@ -310,7 +331,7 @@ function resolveUrlToFile(url) {
310
331
  * @returns {string}
311
332
  */
312
333
  export function withAssetHash(url, basePath = '') {
313
- if (!_enabled) return url;
334
+ if (!_enabled && !_devVersionOf) return url;
314
335
  if (typeof url !== 'string' || url[0] !== '/' || url[1] === '/') return url;
315
336
  // Resolve against the file system using the base-path-stripped url, so a
316
337
  // sub-path-deployed `<basePath>/app/foo.js` maps to `<appDir>/app/foo.js`.
@@ -322,6 +343,11 @@ export function withAssetHash(url, basePath = '') {
322
343
  }
323
344
  const abs = resolveUrlToFile(forResolve);
324
345
  if (!abs) return url;
346
+ if (!_enabled) {
347
+ // Dev: no content hash, only the module's browser version when it moved.
348
+ const n = /** @type {(abs: string) => number} */ (_devVersionOf)(abs);
349
+ return n > 0 ? `${url}${url.includes('?') ? '&' : '?'}t=${n}` : url;
350
+ }
325
351
  const hash = assetHashFor(abs);
326
352
  if (!hash) return url;
327
353
  // The freshly-emitted url never already carries a query, but stay robust:
@@ -335,14 +361,12 @@ export function withAssetHash(url, basePath = '') {
335
361
  * body behind the isomorphic `asset()` helper (#1194); `dev.js` installs it
336
362
  * via `setAssetUrlProvider` at boot.
337
363
  *
338
- * The `public/` gate is defensive, not decorative. `resolveUrlToFile` maps any
339
- * same-origin path under the PROJECT ROOT, so an app that passed request- or
340
- * user-derived data in (`asset(user.avatarPath)`) would otherwise read and
341
- * publish a content hash for `/.env`, `/db/app.db`, or a `.server.ts`, leaking
342
- * both that the file exists and a fingerprint of its exact bytes for files the
343
- * serve path deliberately 404s. Mirror the static-asset route instead: under
344
- * `public/`, no traversal. Anything else returns unchanged WITHOUT touching
345
- * the disk, so a rejected path costs nothing and reveals nothing.
364
+ * The path is resolved under `<appDir>/public` ONLY (public files are served at
365
+ * the url root, so `/app.css` is `public/app.css`), never the project root, so
366
+ * an app that passed request- or user-derived data in (`asset(user.avatarPath)`)
367
+ * cannot get a content hash published for `/.env`, `/db/app.db`, or a
368
+ * `.server.ts`. Mirror the static-asset route: inside `public/`, no traversal.
369
+ * A path naming no public file returns unchanged.
346
370
  *
347
371
  * A fragment is split before hashing. `withAssetHash` appends the query, so
348
372
  * passing the whole thing would emit `/x.svg#icon?v=H`: the browser then
@@ -360,14 +384,23 @@ export function resolveAssetUrl(p, bp = '') {
360
384
  const cuts = [probe.indexOf('?'), probe.indexOf('#')].filter((i) => i !== -1);
361
385
  let decoded = probe.slice(0, cuts.length ? Math.min(...cuts) : probe.length);
362
386
  try { decoded = decodeURIComponent(decoded); } catch { /* keep raw */ }
363
- if (decoded.includes('..') || !decoded.startsWith('/public/')) return p;
387
+ // Public files are served at the url ROOT (`public/app.css` is `/app.css`,
388
+ // Next.js parity), so the path names a file under `<appDir>/public`. The
389
+ // gate keeps the lookup inside that directory: no traversal, no other file.
390
+ if (decoded.includes('..') || decoded.startsWith('/__webjs/')) return p;
364
391
  const h = p.indexOf('#');
365
392
  const path = h === -1 ? p : p.slice(0, h);
366
393
  const frag = h === -1 ? '' : p.slice(h);
367
394
  // An author-supplied query is left alone: it may carry meaning we do not
368
395
  // own, and appending would merge with `&`.
369
396
  if (path.includes('?')) return p;
370
- return withAssetHash(path, bp) + frag;
397
+ if (!_enabled || !_appDir) return p;
398
+ const root = join(_appDir, 'public') + sep;
399
+ const abs = join(root, decoded);
400
+ if (!abs.startsWith(root)) return p;
401
+ const hash = assetHashFor(abs);
402
+ if (!hash) return p;
403
+ return `${path}?v=${hash}` + frag;
371
404
  }
372
405
 
373
406
  /**
@@ -458,7 +491,7 @@ function normalizeSpecToResolved(spec, abs) {
458
491
  *
459
492
  * The appended hash is `assetHashFor(resolvedTarget)`, the exact value
460
493
  * `withAssetHash` computes for the modulepreload href of the same file (same
461
- * absolute path -> same hash, elision-fingerprint fold included), so the two
494
+ * absolute path -> same hash, serve-fingerprint fold included), so the two
462
495
  * URLs are byte-identical by construction.
463
496
  *
464
497
  * The fetched URL is made to equal the preload href even when the specifier's
@@ -488,11 +521,11 @@ function normalizeSpecToResolved(spec, abs) {
488
521
  * splicing `?v` into the served string's value).
489
522
  *
490
523
  * A pure pass-through when fingerprinting is disabled (dev, or roots unset), so
491
- * dev output stays byte-identical. Runs AFTER `elideImportsFromSource` in the
492
- * serve path, so an elided side-effect import (already replaced by a comment)
493
- * is not matched.
524
+ * dev output stays byte-identical. Runs AFTER `deferLazyImportsFromSource` in
525
+ * the serve path, so a deferred side-effect import (already replaced by a
526
+ * lazy-loader registration that carries its own versioned url) is not matched.
494
527
  *
495
- * @param {string} source the served module source (post-elision, type-stripped if TS)
528
+ * @param {string} source the served module source (lazy imports deferred, type-stripped if TS)
496
529
  * @param {string} importerAbs absolute path of the module being served
497
530
  * @returns {string}
498
531
  */