@nifrajs/web 5.4.0 → 5.5.0

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 (100) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/build-plan.d.ts +6 -0
  3. package/dist/build-plan.d.ts.map +1 -1
  4. package/dist/build-plan.js +25 -0
  5. package/dist/build-plan.js.map +1 -1
  6. package/dist/build-vite.d.ts +2 -0
  7. package/dist/build-vite.d.ts.map +1 -1
  8. package/dist/build-vite.js +2 -0
  9. package/dist/build-vite.js.map +1 -1
  10. package/dist/build.d.ts +17 -3
  11. package/dist/build.d.ts.map +1 -1
  12. package/dist/build.js +81 -40
  13. package/dist/build.js.map +1 -1
  14. package/dist/deferred.d.ts.map +1 -1
  15. package/dist/deferred.js +20 -6
  16. package/dist/deferred.js.map +1 -1
  17. package/dist/dev.d.ts +5 -0
  18. package/dist/dev.d.ts.map +1 -1
  19. package/dist/dev.js +7 -6
  20. package/dist/dev.js.map +1 -1
  21. package/dist/diagnostic.d.ts +6 -0
  22. package/dist/diagnostic.d.ts.map +1 -1
  23. package/dist/diagnostic.js +41 -0
  24. package/dist/diagnostic.js.map +1 -1
  25. package/dist/host-policy.d.ts +72 -0
  26. package/dist/host-policy.d.ts.map +1 -0
  27. package/dist/host-policy.js +149 -0
  28. package/dist/host-policy.js.map +1 -0
  29. package/dist/host-reach.d.ts +49 -0
  30. package/dist/host-reach.d.ts.map +1 -0
  31. package/dist/host-reach.js +144 -0
  32. package/dist/host-reach.js.map +1 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/internal/certified-components.d.ts +8 -0
  38. package/dist/internal/certified-components.d.ts.map +1 -0
  39. package/dist/internal/certified-components.js +8 -0
  40. package/dist/internal/certified-components.js.map +1 -0
  41. package/dist/internal/codegen.d.ts +8 -0
  42. package/dist/internal/codegen.d.ts.map +1 -1
  43. package/dist/internal/codegen.js +15 -4
  44. package/dist/internal/codegen.js.map +1 -1
  45. package/dist/internal/output-guard.d.ts +6 -0
  46. package/dist/internal/output-guard.d.ts.map +1 -1
  47. package/dist/internal/output-guard.js +118 -16
  48. package/dist/internal/output-guard.js.map +1 -1
  49. package/dist/internal/page-execution.d.ts +2 -0
  50. package/dist/internal/page-execution.d.ts.map +1 -1
  51. package/dist/internal/page-execution.js +16 -11
  52. package/dist/internal/page-execution.js.map +1 -1
  53. package/dist/internal/render-document.d.ts +3 -1
  54. package/dist/internal/render-document.d.ts.map +1 -1
  55. package/dist/internal/render-document.js +10 -4
  56. package/dist/internal/render-document.js.map +1 -1
  57. package/dist/internal/web-app.d.ts +15 -1
  58. package/dist/internal/web-app.d.ts.map +1 -1
  59. package/dist/internal/web-app.js +8 -0
  60. package/dist/internal/web-app.js.map +1 -1
  61. package/dist/public-dir.d.ts +4 -3
  62. package/dist/public-dir.d.ts.map +1 -1
  63. package/dist/public-dir.js +24 -18
  64. package/dist/public-dir.js.map +1 -1
  65. package/dist/router.d.ts +2 -1
  66. package/dist/router.d.ts.map +1 -1
  67. package/dist/router.js +9 -8
  68. package/dist/router.js.map +1 -1
  69. package/dist/server-components/vite.d.ts.map +1 -1
  70. package/dist/server-components/vite.js +8 -6
  71. package/dist/server-components/vite.js.map +1 -1
  72. package/dist/service-worker.d.ts +3 -1
  73. package/dist/service-worker.d.ts.map +1 -1
  74. package/dist/service-worker.js +29 -8
  75. package/dist/service-worker.js.map +1 -1
  76. package/dist/zones.d.ts +5 -1
  77. package/dist/zones.d.ts.map +1 -1
  78. package/dist/zones.js +17 -4
  79. package/dist/zones.js.map +1 -1
  80. package/package.json +4 -4
  81. package/src/build-plan.ts +26 -0
  82. package/src/build-vite.ts +4 -0
  83. package/src/build.ts +114 -44
  84. package/src/deferred.ts +20 -6
  85. package/src/dev.ts +7 -6
  86. package/src/diagnostic.ts +43 -0
  87. package/src/host-policy.ts +219 -0
  88. package/src/host-reach.ts +177 -0
  89. package/src/index.ts +10 -2
  90. package/src/internal/certified-components.ts +7 -0
  91. package/src/internal/codegen.ts +24 -5
  92. package/src/internal/output-guard.ts +166 -15
  93. package/src/internal/page-execution.ts +23 -9
  94. package/src/internal/render-document.ts +11 -4
  95. package/src/internal/web-app.ts +27 -1
  96. package/src/public-dir.ts +31 -24
  97. package/src/router.ts +9 -8
  98. package/src/server-components/vite.ts +11 -6
  99. package/src/service-worker.ts +32 -9
  100. package/src/zones.ts +27 -5
@@ -37,7 +37,9 @@ export interface ServiceWorkerOptions {
37
37
  /**
38
38
  * URL of a page to serve when a navigation fails and the network is unreachable. It must be a
39
39
  * static, user-independent document (a prerendered `/offline`), because every visitor gets the same
40
- * bytes. Omit it and failed navigations simply fail, which is the honest default.
40
+ * bytes. Install fails rather than store it, or any precached URL, when the response is an error, a
41
+ * redirect, or marked `private` or `no-store`. Omit it and failed navigations simply fail, which is
42
+ * the honest default.
41
43
  */
42
44
  readonly offlineUrl?: string
43
45
  /**
@@ -115,10 +117,35 @@ const OWN_URLS = new Set(PRECACHE)
115
117
  const OWN = { cacheName: CACHE }
116
118
  const OFFLINE = ${offline === undefined ? "null" : JSON.stringify(offline)}
117
119
 
120
+ // Whether every visitor may be served these bytes from this cache. CacheStorage itself ignores
121
+ // Cache-Control, and a redirect can land one visitor's page under a shared URL.
122
+ const shareable = (response) =>
123
+ response.ok &&
124
+ response.type === "basic" &&
125
+ !response.redirected &&
126
+ !(response.headers.get("cache-control") ?? "")
127
+ .split(",")
128
+ .some((directive) => /^\\s*(?:no-store|private)\\s*(?:=|$)/i.test(directive))
129
+
118
130
  self.addEventListener("install", (event) => {
119
- // Take over as soon as the new assets are stored; the activate handler drops the old cache, so
120
- // waiting would only serve stale bytes for longer.
121
- event.waitUntil(caches.open(CACHE).then((c) => c.addAll(PRECACHE)).then(() => self.skipWaiting()))
131
+ // Every response is checked before any is stored, so a refused one leaves this build's cache
132
+ // empty and the install failed, as addAll would on an error status. Take over as soon as the new
133
+ // assets are stored; the activate handler drops the old cache, so waiting would only serve stale
134
+ // bytes for longer.
135
+ event.waitUntil(
136
+ Promise.all(
137
+ PRECACHE.map((url) =>
138
+ fetch(url).then((response) => {
139
+ if (shareable(response)) return response
140
+ throw new TypeError("[nifra/web] not precached, the response is not shareable: " + url)
141
+ }),
142
+ ),
143
+ )
144
+ .then((responses) =>
145
+ caches.open(CACHE).then((c) => Promise.all(responses.map((r, i) => c.put(PRECACHE[i], r)))),
146
+ )
147
+ .then(() => self.skipWaiting()),
148
+ )
122
149
  })
123
150
 
124
151
  self.addEventListener("activate", (event) => {
@@ -156,11 +183,7 @@ self.addEventListener("fetch", (event) => {
156
183
  caches.match(request, OWN).then((hit) => {
157
184
  if (hit !== undefined) return hit
158
185
  return fetch(request).then((response) => {
159
- const cacheable =
160
- response.ok &&
161
- response.type === "basic" &&
162
- !(response.headers.get("cache-control") ?? "").includes("no-store")
163
- if (cacheable) {
186
+ if (shareable(response)) {
164
187
  const copy = response.clone()
165
188
  void caches.open(CACHE).then((c) => c.put(request, copy))
166
189
  }
package/src/zones.ts CHANGED
@@ -170,7 +170,11 @@ export interface ZoneConfig {
170
170
  readonly backend?: readonly string[]
171
171
  /** App-relative folders or files holding frontend code, beside `frontend/`. */
172
172
  readonly frontend?: readonly string[]
173
- /** App-relative folders or files holding code both sides import, beside `shared/`. */
173
+ /**
174
+ * App-relative folders or files holding code both sides import, beside `shared/`. Unlike the other
175
+ * sides, an entry may sit inside `frontend/` or `backend/`: a module there that both sides import
176
+ * stays where it is, held to the shared rules (it may import only shared code and packages).
177
+ */
174
178
  readonly shared?: readonly string[]
175
179
  }
176
180
 
@@ -258,8 +262,15 @@ export function parseZoneConfig(value: unknown, source: string): ZoneConfig {
258
262
  if (folder.split("/").includes("..")) fail(`"${side}" entry "${entry}" may not contain ".."`)
259
263
  if (/[*?[\]{}!]/.test(folder))
260
264
  fail(`"${side}" entry "${entry}" must be a plain path (a trailing "/**" is allowed)`)
261
- if (RESERVED_FOLDERS.has(top) || top.startsWith(".nifra"))
262
- fail(`"${side}" entry "${entry}" is inside ${top}/, which already has its own rules`)
265
+ const sharedInSide =
266
+ side === "shared" && (top === "frontend" || top === "backend") && folder !== top
267
+ if ((RESERVED_FOLDERS.has(top) && !sharedInSide) || top.startsWith(".nifra"))
268
+ fail(
269
+ `"${side}" entry "${entry}" is inside ${top}/, which already has its own rules` +
270
+ (top === "frontend" || top === "backend"
271
+ ? `; only a "shared" entry may name a path inside ${top}/`
272
+ : ""),
273
+ )
263
274
  const other = owner.get(folder)
264
275
  if (other !== undefined && other !== side)
265
276
  fail(`"${folder}" is listed as both ${other} and ${side}; a path has one side`)
@@ -587,11 +598,22 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
587
598
  if (suffix === "shared") return { zone: "shared" }
588
599
  return { zone: "route-frontend" }
589
600
  }
590
- const configured = ZONE_FOLDERS[top] === undefined ? configuredPath(rel) : undefined
591
- const folder = ZONE_FOLDERS[top] ?? configured?.[1]
601
+ // Inside a reserved folder only a "shared" entry can match: the parser refuses the others there.
602
+ const configured = configuredPath(rel)
603
+ const folder = configured?.[1] ?? ZONE_FOLDERS[top]
592
604
  if (folder !== undefined) {
593
605
  if (suffix === undefined || suffix === folder) return { zone: folder }
594
606
  if (suffix === "fn" && folder === "backend") return { zone: "fn" }
607
+ // Code both sides import may live beside the side that wrote it. Shared code may import only
608
+ // shared code and packages, so the declaration narrows what the file may reach, never widens it.
609
+ // A file "nifra.zones" lists by name has that side; its suffix cannot contradict it.
610
+ if (
611
+ suffix === "shared" &&
612
+ (folder === "frontend" || folder === "backend") &&
613
+ top !== "public" &&
614
+ configured?.[0] !== rel
615
+ )
616
+ return { zone: "shared" }
595
617
  const where =
596
618
  configured === undefined
597
619
  ? `sits in ${top}/`