@nifrajs/web 3.5.0 → 4.0.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.
- package/dist/backend-only.d.ts +15 -0
- package/dist/backend-only.d.ts.map +1 -0
- package/dist/backend-only.js +16 -0
- package/dist/backend-only.js.map +1 -0
- package/dist/boundary.d.ts +5 -0
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +11 -2
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +18 -6
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +82 -34
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +4 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +68 -13
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +57 -23
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +356 -85
- package/dist/build.js.map +1 -1
- package/dist/bun-dev-entry.d.ts +1 -1
- package/dist/bun-dev-entry.js +1 -1
- package/dist/cdn.d.ts +74 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.js +132 -0
- package/dist/cdn.js.map +1 -0
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +111 -15
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +4 -3
- package/dist/conformance.d.ts.map +1 -1
- package/dist/conformance.js +12 -3
- package/dist/conformance.js.map +1 -1
- package/dist/csp.d.ts +93 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +122 -0
- package/dist/csp.js.map +1 -0
- package/dist/dev-client.d.ts +76 -0
- package/dist/dev-client.d.ts.map +1 -0
- package/dist/dev-client.js +280 -0
- package/dist/dev-client.js.map +1 -0
- package/dist/dev-diagnostics.d.ts +26 -2
- package/dist/dev-diagnostics.d.ts.map +1 -1
- package/dist/dev-diagnostics.js +38 -7
- package/dist/dev-diagnostics.js.map +1 -1
- package/dist/dev-error.d.ts +4 -13
- package/dist/dev-error.d.ts.map +1 -1
- package/dist/dev-error.js +46 -4
- package/dist/dev-error.js.map +1 -1
- package/dist/dev-feed.d.ts +298 -0
- package/dist/dev-feed.d.ts.map +1 -0
- package/dist/dev-feed.js +797 -0
- package/dist/dev-feed.js.map +1 -0
- package/dist/dev-indicator.d.ts +38 -0
- package/dist/dev-indicator.d.ts.map +1 -0
- package/dist/dev-indicator.js +93 -0
- package/dist/dev-indicator.js.map +1 -0
- package/dist/dev-session.d.ts +77 -0
- package/dist/dev-session.d.ts.map +1 -0
- package/dist/dev-session.js +580 -0
- package/dist/dev-session.js.map +1 -0
- package/dist/dev-sourcemap.d.ts +27 -0
- package/dist/dev-sourcemap.d.ts.map +1 -0
- package/dist/dev-sourcemap.js +235 -0
- package/dist/dev-sourcemap.js.map +1 -0
- package/dist/dev-ssr-graph.d.ts.map +1 -1
- package/dist/dev-ssr-graph.js +13 -5
- package/dist/dev-ssr-graph.js.map +1 -1
- package/dist/dev.d.ts +15 -2
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +69 -44
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +35 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -0
- package/dist/diagnostic-catalog.js +203 -0
- package/dist/diagnostic-catalog.js.map +1 -0
- package/dist/diagnostic-prompt.d.ts +49 -0
- package/dist/diagnostic-prompt.d.ts.map +1 -0
- package/dist/diagnostic-prompt.js +303 -0
- package/dist/diagnostic-prompt.js.map +1 -0
- package/dist/diagnostic.d.ts +17 -23
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +31 -39
- package/dist/diagnostic.js.map +1 -1
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +4 -1
- package/dist/draft.js.map +1 -1
- package/dist/fn.d.ts +5 -0
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +13 -3
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +4 -2
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +13 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +48 -0
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
- package/dist/internal/cdn/cloudflare.js +94 -0
- package/dist/internal/cdn/cloudflare.js.map +1 -0
- package/dist/internal/cdn/core.d.ts +91 -0
- package/dist/internal/cdn/core.d.ts.map +1 -0
- package/dist/internal/cdn/core.js +220 -0
- package/dist/internal/cdn/core.js.map +1 -0
- package/dist/internal/cdn/fastly.d.ts +17 -0
- package/dist/internal/cdn/fastly.d.ts.map +1 -0
- package/dist/internal/cdn/fastly.js +43 -0
- package/dist/internal/cdn/fastly.js.map +1 -0
- package/dist/internal/cdn/vercel.d.ts +31 -0
- package/dist/internal/cdn/vercel.d.ts.map +1 -0
- package/dist/internal/cdn/vercel.js +56 -0
- package/dist/internal/cdn/vercel.js.map +1 -0
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +141 -25
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-csp.d.ts +24 -0
- package/dist/internal/dev-csp.d.ts.map +1 -0
- package/dist/internal/dev-csp.js +160 -0
- package/dist/internal/dev-csp.js.map +1 -0
- package/dist/internal/dev-reserved.d.ts +25 -0
- package/dist/internal/dev-reserved.d.ts.map +1 -0
- package/dist/internal/dev-reserved.js +39 -0
- package/dist/internal/dev-reserved.js.map +1 -0
- package/dist/internal/html-spans.d.ts +10 -0
- package/dist/internal/html-spans.d.ts.map +1 -0
- package/dist/internal/html-spans.js +46 -0
- package/dist/internal/html-spans.js.map +1 -0
- package/dist/internal/loading-runtime.d.ts +44 -0
- package/dist/internal/loading-runtime.d.ts.map +1 -0
- package/dist/internal/loading-runtime.js +206 -0
- package/dist/internal/loading-runtime.js.map +1 -0
- package/dist/internal/matches-runtime.d.ts +8 -0
- package/dist/internal/matches-runtime.d.ts.map +1 -0
- package/dist/internal/matches-runtime.js +61 -0
- package/dist/internal/matches-runtime.js.map +1 -0
- package/dist/internal/mount-shadow.d.ts +36 -0
- package/dist/internal/mount-shadow.d.ts.map +1 -0
- package/dist/internal/mount-shadow.js +65 -0
- package/dist/internal/mount-shadow.js.map +1 -0
- package/dist/internal/node-builtins.d.ts +3 -0
- package/dist/internal/node-builtins.d.ts.map +1 -0
- package/dist/internal/node-builtins.js +79 -0
- package/dist/internal/node-builtins.js.map +1 -0
- package/dist/internal/output-guard.d.ts +65 -0
- package/dist/internal/output-guard.d.ts.map +1 -0
- package/dist/internal/output-guard.js +630 -0
- package/dist/internal/output-guard.js.map +1 -0
- package/dist/internal/page-execution.d.ts +20 -8
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +571 -105
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts +21 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +104 -22
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +12 -0
- package/dist/internal/private-env.d.ts.map +1 -0
- package/dist/internal/private-env.js +335 -0
- package/dist/internal/private-env.js.map +1 -0
- package/dist/internal/public-url.d.ts +3 -0
- package/dist/internal/public-url.d.ts.map +1 -0
- package/dist/internal/public-url.js +18 -0
- package/dist/internal/public-url.js.map +1 -0
- package/dist/internal/render-document.d.ts +25 -19
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +77 -61
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/response-controls.d.ts +75 -0
- package/dist/internal/response-controls.d.ts.map +1 -0
- package/dist/internal/response-controls.js +256 -0
- package/dist/internal/response-controls.js.map +1 -0
- package/dist/internal/runtime-contract.d.ts +11 -16
- package/dist/internal/runtime-contract.d.ts.map +1 -1
- package/dist/internal/runtime-contract.js.map +1 -1
- package/dist/internal/secret-scan.d.ts +104 -0
- package/dist/internal/secret-scan.d.ts.map +1 -0
- package/dist/internal/secret-scan.js +485 -0
- package/dist/internal/secret-scan.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +6 -29
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +6 -66
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/source-map.d.ts +47 -0
- package/dist/internal/source-map.d.ts.map +1 -0
- package/dist/internal/source-map.js +225 -0
- package/dist/internal/source-map.js.map +1 -0
- package/dist/internal/target-compat.d.ts +27 -0
- package/dist/internal/target-compat.d.ts.map +1 -0
- package/dist/internal/target-compat.js +77 -0
- package/dist/internal/target-compat.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +6 -0
- package/dist/internal/timing-safe-equal.d.ts.map +1 -1
- package/dist/internal/timing-safe-equal.js +10 -0
- package/dist/internal/timing-safe-equal.js.map +1 -1
- package/dist/internal/vitals-runtime.d.ts +18 -0
- package/dist/internal/vitals-runtime.d.ts.map +1 -0
- package/dist/internal/vitals-runtime.js +38 -0
- package/dist/internal/vitals-runtime.js.map +1 -0
- package/dist/internal/web-app.d.ts +38 -9
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +199 -59
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/zone-graph.d.ts +89 -0
- package/dist/internal/zone-graph.d.ts.map +1 -0
- package/dist/internal/zone-graph.js +492 -0
- package/dist/internal/zone-graph.js.map +1 -0
- package/dist/isr.d.ts +83 -6
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +263 -36
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +31 -6
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +220 -13
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +386 -9
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts +26 -5
- package/dist/module-graph.d.ts.map +1 -1
- package/dist/module-graph.js +17 -7
- package/dist/module-graph.js.map +1 -1
- package/dist/navigation.d.ts +17 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +11 -5
- package/dist/navigation.js.map +1 -1
- package/dist/nonce.d.ts +47 -0
- package/dist/nonce.d.ts.map +1 -0
- package/dist/nonce.js +63 -0
- package/dist/nonce.js.map +1 -0
- package/dist/plugins/css-modules.d.ts.map +1 -1
- package/dist/plugins/css-modules.js +4 -3
- package/dist/plugins/css-modules.js.map +1 -1
- package/dist/plugins/kit.d.ts +9 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +18 -0
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/svg.d.ts +22 -3
- package/dist/plugins/svg.d.ts.map +1 -1
- package/dist/plugins/svg.js +112 -1
- package/dist/plugins/svg.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +107 -3
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +378 -17
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +61 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
- package/dist/plugins/vite-zone-guard.js +208 -0
- package/dist/plugins/vite-zone-guard.js.map +1 -0
- package/dist/plugins/zone-guard.d.ts +27 -0
- package/dist/plugins/zone-guard.d.ts.map +1 -0
- package/dist/plugins/zone-guard.js +72 -0
- package/dist/plugins/zone-guard.js.map +1 -0
- package/dist/prerender.d.ts +10 -0
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +35 -3
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +9 -0
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +67 -9
- package/dist/public-dir.js.map +1 -1
- package/dist/pwa-manifest.d.ts +68 -0
- package/dist/pwa-manifest.d.ts.map +1 -0
- package/dist/pwa-manifest.js +178 -0
- package/dist/pwa-manifest.js.map +1 -0
- package/dist/query.js +4 -4
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +42 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +6 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-manifest.d.ts +3 -0
- package/dist/route-manifest.d.ts.map +1 -1
- package/dist/route-manifest.js +26 -3
- package/dist/route-manifest.js.map +1 -1
- package/dist/route-types.d.ts +24 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +195 -0
- package/dist/route-types.js.map +1 -0
- package/dist/router.d.ts +28 -6
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +129 -40
- package/dist/router.js.map +1 -1
- package/dist/vitals.d.ts +33 -0
- package/dist/vitals.d.ts.map +1 -0
- package/dist/vitals.js +16 -0
- package/dist/vitals.js.map +1 -0
- package/dist/vite.d.ts +16 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +187 -36
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +70 -0
- package/dist/zones.d.ts.map +1 -0
- package/dist/zones.js +365 -0
- package/dist/zones.js.map +1 -0
- package/package.json +67 -14
- package/src/backend-only.ts +16 -0
- package/src/boundary.ts +18 -5
- package/src/build-plan.ts +90 -35
- package/src/build-vite.ts +77 -11
- package/src/build.ts +466 -96
- package/src/bun-dev-entry.ts +1 -1
- package/src/cdn.ts +216 -0
- package/src/client.ts +111 -18
- package/src/conformance.ts +16 -3
- package/src/csp.ts +194 -0
- package/src/dev-client.ts +349 -0
- package/src/dev-diagnostics.ts +60 -7
- package/src/dev-error.ts +49 -5
- package/src/dev-feed.ts +1140 -0
- package/src/dev-indicator.ts +113 -0
- package/src/dev-session.ts +729 -0
- package/src/dev-sourcemap.ts +281 -0
- package/src/dev-ssr-graph.ts +16 -5
- package/src/dev.ts +86 -43
- package/src/diagnostic-catalog.ts +243 -0
- package/src/diagnostic-prompt.ts +355 -0
- package/src/diagnostic.ts +63 -66
- package/src/draft.ts +4 -1
- package/src/fn.ts +19 -3
- package/src/fs.ts +13 -2
- package/src/index.ts +42 -10
- package/src/internal/cdn/cloudflare.ts +146 -0
- package/src/internal/cdn/core.ts +316 -0
- package/src/internal/cdn/fastly.ts +62 -0
- package/src/internal/cdn/vercel.ts +95 -0
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/codegen.ts +161 -24
- package/src/internal/dev-csp.ts +176 -0
- package/src/internal/dev-reserved.ts +52 -0
- package/src/internal/html-spans.ts +50 -0
- package/src/internal/loading-runtime.ts +244 -0
- package/src/internal/matches-runtime.ts +62 -0
- package/src/internal/mount-shadow.ts +88 -0
- package/src/internal/node-builtins.ts +79 -0
- package/src/internal/output-guard.ts +701 -0
- package/src/internal/page-execution.ts +795 -185
- package/src/internal/parity.ts +150 -36
- package/src/internal/private-env.ts +334 -0
- package/src/internal/public-url.ts +22 -0
- package/src/internal/render-document.ts +114 -67
- package/src/internal/response-controls.ts +284 -0
- package/src/internal/runtime-contract.ts +10 -15
- package/src/internal/secret-scan.ts +623 -0
- package/src/internal/server-boundary.ts +6 -69
- package/src/internal/source-map.ts +256 -0
- package/src/internal/target-compat.ts +92 -0
- package/src/internal/timing-safe-equal.ts +13 -0
- package/src/internal/vitals-runtime.ts +53 -0
- package/src/internal/web-app.ts +262 -84
- package/src/internal/zone-graph.ts +593 -0
- package/src/isr.ts +325 -37
- package/src/llms-txt.ts +28 -4
- package/src/manifest.ts +670 -16
- package/src/module-graph.ts +53 -9
- package/src/navigation.ts +29 -6
- package/src/nonce.ts +104 -0
- package/src/plugins/css-modules.ts +5 -3
- package/src/plugins/kit.ts +24 -0
- package/src/plugins/svg.ts +135 -4
- package/src/plugins/vite-leak-guard.ts +529 -16
- package/src/plugins/vite-zone-guard.ts +263 -0
- package/src/plugins/zone-guard.ts +110 -0
- package/src/prerender.ts +49 -3
- package/src/public-dir.ts +69 -8
- package/src/pwa-manifest.ts +226 -0
- package/src/query.ts +4 -4
- package/src/render-seam.ts +46 -1
- package/src/route-manifest.ts +15 -3
- package/src/route-types.ts +264 -0
- package/src/router.ts +163 -63
- package/src/vitals.ts +44 -0
- package/src/vite.ts +207 -36
- package/src/zones.ts +430 -0
- package/dist/plugins/vite-server-only.d.ts +0 -49
- package/dist/plugins/vite-server-only.d.ts.map +0 -1
- package/dist/plugins/vite-server-only.js +0 -57
- package/dist/plugins/vite-server-only.js.map +0 -1
- package/dist/server-only.d.ts +0 -21
- package/dist/server-only.d.ts.map +0 -1
- package/dist/server-only.js +0 -25
- package/dist/server-only.js.map +0 -1
- package/src/plugins/vite-server-only.ts +0 -75
- package/src/server-only.ts +0 -25
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/pwa-manifest` - a Web App Manifest builder, the declarative half of PWA next to
|
|
3
|
+
* `@nifrajs/web/service-worker`'s generated worker. Pure + runtime-agnostic: describe the app and
|
|
4
|
+
* get the `manifest.json` bytes to serve (typically `app.get("/manifest.webmanifest", ...)` with
|
|
5
|
+
* `content-type: application/manifest+json`), plus the `<link rel="manifest">` tag for the
|
|
6
|
+
* document head. Follows the [Web App Manifest spec](https://www.w3.org/TR/appmanifest/) field
|
|
7
|
+
* names exactly, so MDN is the reference for anything not documented here.
|
|
8
|
+
*
|
|
9
|
+
* Invalid input throws loudly at build/startup time (a missing install name, a malformed icon
|
|
10
|
+
* size, an unknown display mode) - a manifest the browser silently ignores is worse than no
|
|
11
|
+
* manifest, and these are all developer mistakes, never request input.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type ManifestDisplay = "fullscreen" | "standalone" | "minimal-ui" | "browser"
|
|
15
|
+
export type ManifestDir = "ltr" | "rtl" | "auto"
|
|
16
|
+
export type ManifestPurpose = "any" | "maskable" | "monochrome"
|
|
17
|
+
|
|
18
|
+
export interface ManifestIcon {
|
|
19
|
+
/** Icon URL, resolved relative to `start_url` by the browser when relative. */
|
|
20
|
+
readonly src: string
|
|
21
|
+
/** Space-separated sizes, e.g. `"192x192 512x512"`, or `"any"`. Default `"any"`. */
|
|
22
|
+
readonly sizes?: string
|
|
23
|
+
/** MIME type, e.g. `"image/png"`. */
|
|
24
|
+
readonly type?: string
|
|
25
|
+
/** Space-separated purposes. Default `"any"`. */
|
|
26
|
+
readonly purpose?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ManifestScreenshot {
|
|
30
|
+
readonly src: string
|
|
31
|
+
readonly sizes?: string
|
|
32
|
+
readonly type?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ManifestShortcut {
|
|
36
|
+
readonly name: string
|
|
37
|
+
readonly url: string
|
|
38
|
+
readonly short_name?: string
|
|
39
|
+
readonly description?: string
|
|
40
|
+
readonly icons?: readonly ManifestIcon[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PwaManifestOptions {
|
|
44
|
+
/** Full install name (`"Nifra News"`). One of `name` / `short_name` is required. */
|
|
45
|
+
readonly name?: string
|
|
46
|
+
/** Short install name for launchers (`"News"`). One of `name` / `short_name` is required. */
|
|
47
|
+
readonly short_name?: string
|
|
48
|
+
readonly description?: string
|
|
49
|
+
/** Navigation scope root. Default `"/"`. */
|
|
50
|
+
readonly start_url?: string
|
|
51
|
+
/** URL scope the manifest (and its service worker) controls. Default `start_url`'s directory. */
|
|
52
|
+
readonly scope?: string
|
|
53
|
+
/** Preferred display mode. Default `"standalone"` (the installable PWA shape). */
|
|
54
|
+
readonly display?: ManifestDisplay
|
|
55
|
+
readonly orientation?: string
|
|
56
|
+
readonly background_color?: string
|
|
57
|
+
readonly theme_color?: string
|
|
58
|
+
readonly lang?: string
|
|
59
|
+
readonly dir?: ManifestDir
|
|
60
|
+
/** Opaque id distinguishing this install (defaults to `start_url` per spec). */
|
|
61
|
+
readonly id?: string
|
|
62
|
+
readonly categories?: readonly string[]
|
|
63
|
+
readonly icons?: readonly ManifestIcon[]
|
|
64
|
+
readonly screenshots?: readonly ManifestScreenshot[]
|
|
65
|
+
readonly shortcuts?: readonly ManifestShortcut[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const SIZE_LIST = /^(?:any|\d+x\d+)(?:\s+(?:any|\d+x\d+))*$/
|
|
69
|
+
const PURPOSE_TOKENS: ReadonlySet<string> = new Set(["any", "maskable", "monochrome"])
|
|
70
|
+
|
|
71
|
+
/** The start URL's directory, origin-preserving for absolute URLs. Query strings and hashes
|
|
72
|
+
* never belong to a scope - it is a URL prefix the browser matches against. */
|
|
73
|
+
function defaultScope(start_url: string): string {
|
|
74
|
+
if (start_url.startsWith("/")) {
|
|
75
|
+
const bare = start_url.split(/[?#]/)[0] as string
|
|
76
|
+
return bare.endsWith("/") ? bare : bare.slice(0, bare.lastIndexOf("/") + 1)
|
|
77
|
+
}
|
|
78
|
+
let url: URL
|
|
79
|
+
try {
|
|
80
|
+
url = new URL(start_url)
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error(`pwaManifest: start_url ${JSON.stringify(start_url)} is not a path or URL`)
|
|
83
|
+
}
|
|
84
|
+
if (url.origin === "null" || (url.protocol !== "http:" && url.protocol !== "https:")) {
|
|
85
|
+
throw new Error(`pwaManifest: start_url ${JSON.stringify(start_url)} is not a path or URL`)
|
|
86
|
+
}
|
|
87
|
+
const dir = url.pathname.endsWith("/")
|
|
88
|
+
? url.pathname
|
|
89
|
+
: url.pathname.slice(0, url.pathname.lastIndexOf("/") + 1)
|
|
90
|
+
return `${url.origin}${dir}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function purposeOf(purpose: string | undefined, what: string): string | undefined {
|
|
94
|
+
if (purpose === undefined) return undefined
|
|
95
|
+
const tokens = purpose.split(/\s+/).filter((t) => t !== "")
|
|
96
|
+
if (tokens.length === 0) throw new Error(`pwaManifest: ${what} purpose must not be empty`)
|
|
97
|
+
for (const token of tokens) {
|
|
98
|
+
if (!PURPOSE_TOKENS.has(token)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`pwaManifest: ${what} purpose ${JSON.stringify(token)} must be any/maskable/monochrome`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return tokens.join(" ")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Build the manifest document object (pass through `JSON.stringify` to serve it). */
|
|
108
|
+
export function pwaManifest(options: PwaManifestOptions): Record<string, unknown> {
|
|
109
|
+
const { name, short_name } = options
|
|
110
|
+
if (name === undefined && short_name === undefined) {
|
|
111
|
+
throw new Error("pwaManifest: one of name / short_name is required")
|
|
112
|
+
}
|
|
113
|
+
if (name !== undefined && name.trim() === "") {
|
|
114
|
+
throw new Error("pwaManifest: name must not be empty")
|
|
115
|
+
}
|
|
116
|
+
if (short_name !== undefined && short_name.trim() === "") {
|
|
117
|
+
throw new Error("pwaManifest: short_name must not be empty")
|
|
118
|
+
}
|
|
119
|
+
const display = options.display ?? "standalone"
|
|
120
|
+
if (
|
|
121
|
+
display !== "fullscreen" &&
|
|
122
|
+
display !== "minimal-ui" &&
|
|
123
|
+
display !== "browser" &&
|
|
124
|
+
display !== "standalone"
|
|
125
|
+
) {
|
|
126
|
+
throw new Error(`pwaManifest: unknown display ${JSON.stringify(display)}`)
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
options.dir !== undefined &&
|
|
130
|
+
options.dir !== "ltr" &&
|
|
131
|
+
options.dir !== "rtl" &&
|
|
132
|
+
options.dir !== "auto"
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(`pwaManifest: unknown dir ${JSON.stringify(options.dir)}`)
|
|
135
|
+
}
|
|
136
|
+
const start_url = options.start_url ?? "/"
|
|
137
|
+
if (start_url === "" || start_url.startsWith("//")) {
|
|
138
|
+
throw new Error("pwaManifest: start_url must be a relative path or absolute URL")
|
|
139
|
+
}
|
|
140
|
+
const manifest: Record<string, unknown> = {}
|
|
141
|
+
if (name !== undefined) manifest.name = name
|
|
142
|
+
if (short_name !== undefined) manifest.short_name = short_name
|
|
143
|
+
if (options.description !== undefined) manifest.description = options.description
|
|
144
|
+
manifest.start_url = start_url
|
|
145
|
+
// The default scope is the start URL's directory ("https://x.com/app/home" → "…/app/"),
|
|
146
|
+
// query/hash excluded; a same-origin absolute URL keeps its origin, a relative one stays
|
|
147
|
+
// relative. Anything unparseable is a developer mistake, and fails loud.
|
|
148
|
+
manifest.scope = options.scope ?? defaultScope(start_url)
|
|
149
|
+
manifest.display = display
|
|
150
|
+
if (options.orientation !== undefined) manifest.orientation = options.orientation
|
|
151
|
+
if (options.background_color !== undefined) manifest.background_color = options.background_color
|
|
152
|
+
if (options.theme_color !== undefined) manifest.theme_color = options.theme_color
|
|
153
|
+
if (options.lang !== undefined) manifest.lang = options.lang
|
|
154
|
+
if (options.dir !== undefined) manifest.dir = options.dir
|
|
155
|
+
if (options.id !== undefined) manifest.id = options.id
|
|
156
|
+
if (options.categories !== undefined) manifest.categories = [...options.categories]
|
|
157
|
+
if (options.icons !== undefined) {
|
|
158
|
+
manifest.icons = options.icons.map((icon, i) => {
|
|
159
|
+
if (icon.src === "") throw new Error(`pwaManifest: icons[${i}].src must not be empty`)
|
|
160
|
+
const sizes = icon.sizes ?? "any"
|
|
161
|
+
if (!SIZE_LIST.test(sizes)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`pwaManifest: icons[${i}].sizes ${JSON.stringify(icon.sizes)} must be "any" or space-separated WxH`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
const entry: Record<string, unknown> = { src: icon.src, sizes }
|
|
167
|
+
if (icon.type !== undefined) entry.type = icon.type
|
|
168
|
+
const purpose = purposeOf(icon.purpose, `icons[${i}]`)
|
|
169
|
+
if (purpose !== undefined) entry.purpose = purpose
|
|
170
|
+
return entry
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
if (options.screenshots !== undefined) {
|
|
174
|
+
manifest.screenshots = options.screenshots.map((shot, i) => {
|
|
175
|
+
if (shot.src === "") throw new Error(`pwaManifest: screenshots[${i}].src must not be empty`)
|
|
176
|
+
const entry: Record<string, unknown> = { src: shot.src }
|
|
177
|
+
if (shot.sizes !== undefined) {
|
|
178
|
+
if (!SIZE_LIST.test(shot.sizes)) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`pwaManifest: screenshots[${i}].sizes ${JSON.stringify(shot.sizes)} must be "any" or space-separated WxH`,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
entry.sizes = shot.sizes
|
|
184
|
+
}
|
|
185
|
+
if (shot.type !== undefined) entry.type = shot.type
|
|
186
|
+
return entry
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
if (options.shortcuts !== undefined) {
|
|
190
|
+
manifest.shortcuts = options.shortcuts.map((cut, i) => {
|
|
191
|
+
if (cut.name === "") throw new Error(`pwaManifest: shortcuts[${i}].name must not be empty`)
|
|
192
|
+
if (cut.url === "") throw new Error(`pwaManifest: shortcuts[${i}].url must not be empty`)
|
|
193
|
+
const entry: Record<string, unknown> = { name: cut.name, url: cut.url }
|
|
194
|
+
if (cut.short_name !== undefined) entry.short_name = cut.short_name
|
|
195
|
+
if (cut.description !== undefined) entry.description = cut.description
|
|
196
|
+
if (cut.icons !== undefined) {
|
|
197
|
+
entry.icons = cut.icons.map((icon, j) => {
|
|
198
|
+
if (icon.src === "") {
|
|
199
|
+
throw new Error(`pwaManifest: shortcuts[${i}].icons[${j}].src must not be empty`)
|
|
200
|
+
}
|
|
201
|
+
const sizes = icon.sizes ?? "any"
|
|
202
|
+
if (!SIZE_LIST.test(sizes)) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`pwaManifest: shortcuts[${i}].icons[${j}].sizes must be "any" or space-separated WxH`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
return { src: icon.src, sizes }
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
return entry
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
return manifest
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Serialize a manifest document to servable JSON (stable key order, no whitespace). */
|
|
217
|
+
export function serializeManifest(manifest: Record<string, unknown>): string {
|
|
218
|
+
return JSON.stringify(manifest)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The `<link rel="manifest">` tag for the document head. */
|
|
222
|
+
export function manifestLink(href = "/manifest.webmanifest"): string {
|
|
223
|
+
if (href === "") throw new Error("manifestLink: href must not be empty")
|
|
224
|
+
const quoted = href.replace(/"/g, """)
|
|
225
|
+
return `<link rel="manifest" href="${quoted}">`
|
|
226
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -642,7 +642,7 @@ export function createMutation<TData, TVariables>(
|
|
|
642
642
|
let mutationFn = fn
|
|
643
643
|
let cbs = callbacks
|
|
644
644
|
let state = IDLE_MUTATION as MutationState<TData, TVariables>
|
|
645
|
-
let
|
|
645
|
+
let latest = 0
|
|
646
646
|
const listeners = new Set<() => void>()
|
|
647
647
|
const emit = (): void => {
|
|
648
648
|
for (const l of listeners) l()
|
|
@@ -665,19 +665,19 @@ export function createMutation<TData, TVariables>(
|
|
|
665
665
|
},
|
|
666
666
|
reset: () => setState(IDLE_MUTATION as MutationState<TData, TVariables>),
|
|
667
667
|
mutate: async (variables) => {
|
|
668
|
-
const mine = ++
|
|
668
|
+
const mine = ++latest
|
|
669
669
|
setState({ status: "pending", data: undefined, error: undefined, variables })
|
|
670
670
|
// Only await a callback when one is actually supplied - an `await undefined` would insert a
|
|
671
671
|
// needless microtask before the mutationFn runs (and complicates single-flight timing).
|
|
672
672
|
if (cbs.onMutate !== undefined) await cbs.onMutate(variables)
|
|
673
673
|
try {
|
|
674
674
|
const data = await mutationFn(variables)
|
|
675
|
-
if (mine ===
|
|
675
|
+
if (mine === latest) setState({ status: "success", data, error: undefined, variables })
|
|
676
676
|
if (cbs.onSuccess !== undefined) await cbs.onSuccess(data, variables)
|
|
677
677
|
if (cbs.onSettled !== undefined) await cbs.onSettled(data, undefined, variables)
|
|
678
678
|
return data
|
|
679
679
|
} catch (error) {
|
|
680
|
-
if (mine ===
|
|
680
|
+
if (mine === latest) setState({ status: "error", data: undefined, error, variables })
|
|
681
681
|
if (cbs.onError !== undefined) await cbs.onError(error, variables)
|
|
682
682
|
if (cbs.onSettled !== undefined) await cbs.onSettled(undefined, error, variables)
|
|
683
683
|
throw error
|
package/src/render-seam.ts
CHANGED
|
@@ -30,6 +30,41 @@ export interface RenderProps {
|
|
|
30
30
|
readonly search?: Record<string, unknown>
|
|
31
31
|
/** Neutral named-boundary states; adapters choose how each boundary's `render` UI is mounted. */
|
|
32
32
|
readonly boundaries?: BoundaryStates
|
|
33
|
+
/** The rendered chain's module ids and `handle` exports, for the adapter's `useMatches`. */
|
|
34
|
+
readonly matchChain?: MatchChain
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The modules a render stacks, as `useMatches` needs them: each one's id and `handle` export,
|
|
39
|
+
* outermost layout first and the page (or status page) last. Built by the server for SSR and by the
|
|
40
|
+
* generated client entry per route, from the same manifest, so both sides report the same matches.
|
|
41
|
+
*/
|
|
42
|
+
export interface MatchChain {
|
|
43
|
+
/** Layout ids (`orgs/[org]/_layout`), then the page's route id. */
|
|
44
|
+
readonly ids: readonly string[]
|
|
45
|
+
/** Each module's `handle` export, index-aligned with {@link ids}. */
|
|
46
|
+
readonly handles: readonly unknown[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One module of the rendered chain, as `useMatches` returns it. */
|
|
50
|
+
export interface UIMatch {
|
|
51
|
+
/** The module's id: a layout's (`orgs/[org]/_layout`), or the page's route id last. */
|
|
52
|
+
readonly id: string
|
|
53
|
+
/** The part of the URL path this module wraps: a layout's directory prefix, the whole path for the
|
|
54
|
+
* page. Raw, as it appears in the URL. */
|
|
55
|
+
readonly pathname: string
|
|
56
|
+
/** The params this module's loader receives: a layout's own, every route param for the page. */
|
|
57
|
+
readonly params: Readonly<Record<string, string>>
|
|
58
|
+
/** This module's loader data: `null` for a layout without a loader. */
|
|
59
|
+
readonly data: unknown
|
|
60
|
+
/** The module's `handle` export - any value, commonly a breadcrumb label or a render function. */
|
|
61
|
+
readonly handle: unknown
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Per-document options for {@link RenderAdapter.renderToStream}. */
|
|
65
|
+
export interface RenderStreamOptions {
|
|
66
|
+
/** The document's CSP nonce, for every executable `<script>` the framework streams. */
|
|
67
|
+
readonly nonce?: string
|
|
33
68
|
}
|
|
34
69
|
|
|
35
70
|
/**
|
|
@@ -39,11 +74,14 @@ export interface RenderProps {
|
|
|
39
74
|
export interface RenderAdapter {
|
|
40
75
|
/**
|
|
41
76
|
* Render a route's layout `chain` (outermost layout → page) to a Web stream of HTML bytes,
|
|
42
|
-
* including the framework's hydration markers.
|
|
77
|
+
* including the framework's hydration markers. When `options.nonce` is set, every executable
|
|
78
|
+
* `<script>` the framework streams (out-of-order Suspense runtimes, serialized resources) must
|
|
79
|
+
* carry it, or a nonce-based Content-Security-Policy blocks it and the boundary never resolves.
|
|
43
80
|
*/
|
|
44
81
|
renderToStream(
|
|
45
82
|
chain: readonly unknown[],
|
|
46
83
|
props: RenderProps,
|
|
84
|
+
options?: RenderStreamOptions,
|
|
47
85
|
): ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>
|
|
48
86
|
/** Render the chain to a complete HTML string when no content is deferred. */
|
|
49
87
|
renderToString?(chain: readonly unknown[], props: RenderProps): string | Promise<string>
|
|
@@ -67,6 +105,13 @@ export function ssrModuleLoader(): SsrModuleLoader | undefined {
|
|
|
67
105
|
return loaderSlot[SSR_MODULE_LOADER_SLOT]
|
|
68
106
|
}
|
|
69
107
|
|
|
108
|
+
/**
|
|
109
|
+
* `id` of the inert `<script type="application/json">` a hydrating document hands its page state over
|
|
110
|
+
* in: one JSON object keyed by the `*_GLOBAL` names below, which the client entry assigns onto
|
|
111
|
+
* `window` before anything reads them. Data the browser never executes needs no CSP nonce or hash.
|
|
112
|
+
*/
|
|
113
|
+
export const HANDOVER_ID = "__nifra-handover"
|
|
114
|
+
|
|
70
115
|
/** Global the server serializes loader data into; the client reads it to hydrate. */
|
|
71
116
|
export const DATA_GLOBAL = "__NIFRA_DATA__"
|
|
72
117
|
/** Per-layout loader data for hydration. */
|
package/src/route-manifest.ts
CHANGED
|
@@ -16,8 +16,19 @@
|
|
|
16
16
|
* target, and reports what the target cannot honour. Pure and fs-free: it takes an already-built
|
|
17
17
|
* `Manifest` so it runs at build time, in `nifra routes`, and in a test with a hand-built manifest.
|
|
18
18
|
*/
|
|
19
|
+
|
|
20
|
+
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
19
21
|
import type { Manifest, RouteModule } from "./manifest.ts"
|
|
20
22
|
|
|
23
|
+
export {
|
|
24
|
+
formatShadowedPages,
|
|
25
|
+
normalizeMountPath,
|
|
26
|
+
type ShadowedPage,
|
|
27
|
+
shadowedPages,
|
|
28
|
+
} from "./internal/mount-shadow.ts"
|
|
29
|
+
/** Which half of a route each export belongs in: the frontend file or its `x.backend.ts`. */
|
|
30
|
+
export { BACKEND_ROUTE_EXPORTS, FRONTEND_ROUTE_EXPORTS } from "./manifest.ts"
|
|
31
|
+
|
|
21
32
|
/**
|
|
22
33
|
* How a route produces its HTML.
|
|
23
34
|
*
|
|
@@ -115,7 +126,8 @@ export function deriveRouteEntry(
|
|
|
115
126
|
requires: readonly RouteCapability[]
|
|
116
127
|
} = { id, pattern, mode, hydrate: module.hydrate !== false, requires }
|
|
117
128
|
if (mode === "isr" && module.revalidate !== undefined) entry.revalidate = module.revalidate
|
|
118
|
-
|
|
129
|
+
// A function's tags exist per request; the manifest records only a fixed list.
|
|
130
|
+
if (mode === "isr" && Array.isArray(module.revalidateTags)) {
|
|
119
131
|
entry.revalidateTags = module.revalidateTags
|
|
120
132
|
}
|
|
121
133
|
if (prerenderedPaths !== undefined) entry.prerenderedPaths = prerenderedPaths
|
|
@@ -134,7 +146,7 @@ const TARGET_CAPABILITIES: Readonly<Record<string, readonly RouteCapability[]>>
|
|
|
134
146
|
bun: ["server"],
|
|
135
147
|
node: ["server"],
|
|
136
148
|
deno: ["server"],
|
|
137
|
-
|
|
149
|
+
cloudflare: ["server"],
|
|
138
150
|
vercel: ["server"],
|
|
139
151
|
static: [],
|
|
140
152
|
}
|
|
@@ -169,7 +181,7 @@ export async function buildRouteManifest(
|
|
|
169
181
|
const module = (await route.load()) as RouteModule
|
|
170
182
|
entries.push(deriveRouteEntry(route.id, route.pattern, module, options.prerendered?.[route.id]))
|
|
171
183
|
}
|
|
172
|
-
entries.sort((a, b) => a.pattern
|
|
184
|
+
entries.sort((a, b) => codeUnitOrder(a.pattern, b.pattern))
|
|
173
185
|
|
|
174
186
|
const conflicts: RouteManifestConflict[] = []
|
|
175
187
|
const capabilities =
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/route-types` - the generated `./+types/<route>` modules.
|
|
3
|
+
*
|
|
4
|
+
* Every route file gets `.nifra/types/<routes>/<dir>/+types/<name>.d.ts`, declaring a `Route`
|
|
5
|
+
* namespace: the route's `Params`, the `LoaderData` and `ActionData` its output schemas let through,
|
|
6
|
+
* the `LoaderArgs` its backend half receives, and its component's props. With
|
|
7
|
+
* `"rootDirs": [".", "./.nifra/types"]` in tsconfig, both halves of a route import it as
|
|
8
|
+
* `./+types/<name>`:
|
|
9
|
+
*
|
|
10
|
+
* // routes/blog/[slug].tsx
|
|
11
|
+
* import type { Route } from "./+types/[slug]"
|
|
12
|
+
* export default function Post({ data }: Route.ComponentProps) { ... }
|
|
13
|
+
*
|
|
14
|
+
* // routes/blog/[slug].backend.ts
|
|
15
|
+
* export async function loader({ api, params }: Route.LoaderArgs) { ... }
|
|
16
|
+
*
|
|
17
|
+
* `LoaderData` is the output type of `loaderOutput`, not the loader's return type: the data guard
|
|
18
|
+
* projects the loader's value through that schema, so the schema is what the component receives.
|
|
19
|
+
* `.nifra/types/register.d.ts` registers `backend/app.ts`'s `backend` with `@nifrajs/client`, so `api`
|
|
20
|
+
* is typed without a route importing the backend.
|
|
21
|
+
*/
|
|
22
|
+
import {
|
|
23
|
+
type Dirent,
|
|
24
|
+
existsSync,
|
|
25
|
+
mkdirSync,
|
|
26
|
+
readdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
rmSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
} from "node:fs"
|
|
31
|
+
import { dirname, join, relative } from "node:path"
|
|
32
|
+
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
33
|
+
import {
|
|
34
|
+
buildManifest,
|
|
35
|
+
isMiddlewareFile,
|
|
36
|
+
isRouteBackendFile,
|
|
37
|
+
type LayoutEntry,
|
|
38
|
+
type Manifest,
|
|
39
|
+
} from "./manifest.ts"
|
|
40
|
+
|
|
41
|
+
/** Where the generated types live, relative to the app root. */
|
|
42
|
+
export const ROUTE_TYPES_DIR = ".nifra/types"
|
|
43
|
+
|
|
44
|
+
const BACKEND_APP = "backend/app"
|
|
45
|
+
const SCRIPT_EXTS = [".ts", ".tsx", ".js", ".jsx"] as const
|
|
46
|
+
|
|
47
|
+
export interface RouteTypesOptions {
|
|
48
|
+
/** The app root: where `.nifra/` and `backend/app.ts` live. */
|
|
49
|
+
readonly appRoot: string
|
|
50
|
+
/** The routes directory. Defaults to `<appRoot>/routes`. */
|
|
51
|
+
readonly routesDir?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Kind = "page" | "layout"
|
|
55
|
+
|
|
56
|
+
interface RouteFile {
|
|
57
|
+
readonly file: string
|
|
58
|
+
readonly backend: string | undefined
|
|
59
|
+
readonly kind: Kind
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const posix = (path: string): string => path.replaceAll("\\", "/")
|
|
63
|
+
|
|
64
|
+
/** Every route file the router serves, once each: pages, layouts and the special pages. */
|
|
65
|
+
function routeFiles(manifest: Manifest): readonly RouteFile[] {
|
|
66
|
+
const files = new Map<string, RouteFile>()
|
|
67
|
+
const add = (entry: Pick<LayoutEntry, "file" | "backend">, kind: Kind): void => {
|
|
68
|
+
if (!files.has(entry.file))
|
|
69
|
+
files.set(entry.file, { file: entry.file, backend: entry.backend, kind })
|
|
70
|
+
}
|
|
71
|
+
for (const route of manifest.routes) add(route, "page")
|
|
72
|
+
for (const layout of Object.values(manifest.layouts)) add(layout, "layout")
|
|
73
|
+
for (const group of [
|
|
74
|
+
manifest.errors,
|
|
75
|
+
manifest.notFounds,
|
|
76
|
+
manifest.statusPages,
|
|
77
|
+
manifest.loadings,
|
|
78
|
+
]) {
|
|
79
|
+
for (const entry of Object.values(group ?? {})) add(entry, "page")
|
|
80
|
+
}
|
|
81
|
+
if (manifest.notFound !== undefined) add(manifest.notFound, "page")
|
|
82
|
+
return [...files.values()].sort((a, b) => codeUnitOrder(a.file, b.file))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const PARAM = /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/
|
|
86
|
+
const OPTIONAL = /^\[\[([A-Za-z_][A-Za-z0-9_]*)\]\]$/
|
|
87
|
+
const CATCH_ALL = /^\[\.\.\.([A-Za-z_][A-Za-z0-9_]*)\]$/
|
|
88
|
+
|
|
89
|
+
/** The params a route file's URL carries: its directories', plus its own name's for a page. */
|
|
90
|
+
function paramsType(file: string, kind: Kind): string {
|
|
91
|
+
const segments = file.replace(/\.[^./]+$/, "").split("/")
|
|
92
|
+
const own =
|
|
93
|
+
kind === "page" && !segments.at(-1)?.startsWith("_") ? segments : segments.slice(0, -1)
|
|
94
|
+
const fields: string[] = []
|
|
95
|
+
for (const segment of own) {
|
|
96
|
+
const optional = OPTIONAL.exec(segment)
|
|
97
|
+
const name = optional?.[1] ?? PARAM.exec(segment)?.[1] ?? CATCH_ALL.exec(segment)?.[1]
|
|
98
|
+
if (name !== undefined) fields.push(`readonly ${name}${optional === null ? "" : "?"}: string`)
|
|
99
|
+
}
|
|
100
|
+
return fields.length === 0 ? "Record<string, never>" : `{ ${fields.join("; ")} }`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A module specifier for `target` from a file in `fromDir`, without a script extension. */
|
|
104
|
+
function specifier(fromDir: string, target: string): string {
|
|
105
|
+
let path = posix(relative(fromDir, target))
|
|
106
|
+
for (const ext of SCRIPT_EXTS) if (path.endsWith(ext)) path = path.slice(0, -ext.length)
|
|
107
|
+
return path.startsWith(".") ? path : `./${path}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const backendAppOf = (appRoot: string): string | undefined =>
|
|
111
|
+
SCRIPT_EXTS.map((ext) => join(appRoot, `${BACKEND_APP}${ext}`)).find((path) => existsSync(path))
|
|
112
|
+
|
|
113
|
+
function routeTypesModule(
|
|
114
|
+
route: RouteFile,
|
|
115
|
+
out: string,
|
|
116
|
+
routesDir: string,
|
|
117
|
+
label: string,
|
|
118
|
+
register: string | undefined,
|
|
119
|
+
): string {
|
|
120
|
+
const dir = dirname(out)
|
|
121
|
+
const backend = route.backend === undefined ? undefined : join(routesDir, route.backend)
|
|
122
|
+
// Only a script module's exports are visible to TypeScript; a .svelte/.vue/.mdx one is an ambient shim.
|
|
123
|
+
const frontend = /\.[jt]sx?$/.test(route.file) ? join(routesDir, route.file) : undefined
|
|
124
|
+
const lines = [`// Generated by nifra from ${label}. Do not edit: \`nifra types\` rewrites it.`]
|
|
125
|
+
if (register !== undefined)
|
|
126
|
+
lines.push(`/// <reference path="${posix(relative(dir, register))}" />`)
|
|
127
|
+
lines.push('import type { OutputOf, RouteLoaderArgs, SearchSchemaOf } from "@nifrajs/client"')
|
|
128
|
+
if (route.kind === "page") lines.push('import type { RenderProps } from "@nifrajs/web"')
|
|
129
|
+
lines.push("")
|
|
130
|
+
if (backend !== undefined)
|
|
131
|
+
lines.push(`type Backend = typeof import("${specifier(dir, backend)}")`)
|
|
132
|
+
if (frontend !== undefined)
|
|
133
|
+
lines.push(`type Frontend = typeof import("${specifier(dir, frontend)}")`)
|
|
134
|
+
if (backend !== undefined || frontend !== undefined) lines.push("")
|
|
135
|
+
const output = (name: string) => (backend === undefined ? "null" : `OutputOf<Backend, "${name}">`)
|
|
136
|
+
lines.push(
|
|
137
|
+
"export namespace Route {",
|
|
138
|
+
" /** The URL params this route's path carries. */",
|
|
139
|
+
` export type Params = ${paramsType(route.file, route.kind)}`,
|
|
140
|
+
" /** What the loader sends to the browser: the output type of `loaderOutput`. */",
|
|
141
|
+
` export type LoaderData = ${output("loaderOutput")}`,
|
|
142
|
+
" /** What the action sends back: the output type of `actionOutput`. */",
|
|
143
|
+
` export type ActionData = ${output("actionOutput")}`,
|
|
144
|
+
" /** The context the backend half's loader receives. */",
|
|
145
|
+
` export type LoaderArgs = RouteLoaderArgs<Params${frontend === undefined ? "" : ", SearchSchemaOf<Frontend>"}>`,
|
|
146
|
+
" /** The context the backend half's action receives. */",
|
|
147
|
+
" export type ActionArgs = LoaderArgs",
|
|
148
|
+
)
|
|
149
|
+
if (route.kind === "page") {
|
|
150
|
+
lines.push(
|
|
151
|
+
" /** The page component's props. */",
|
|
152
|
+
' export type ComponentProps = Omit<RenderProps, "data" | "actionData" | "params"> & {',
|
|
153
|
+
" readonly data: LoaderData",
|
|
154
|
+
" readonly actionData?: ActionData",
|
|
155
|
+
" readonly params: Params",
|
|
156
|
+
" }",
|
|
157
|
+
)
|
|
158
|
+
} else {
|
|
159
|
+
lines.push(
|
|
160
|
+
" /** The layout component's props; `Children` is the adapter's children type. */",
|
|
161
|
+
" export type ComponentProps<Children = unknown> = {",
|
|
162
|
+
" readonly data: LoaderData",
|
|
163
|
+
" readonly children?: Children",
|
|
164
|
+
" }",
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
lines.push("}", "")
|
|
168
|
+
return lines.join("\n")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function registerModule(out: string, backendApp: string, label: string): string {
|
|
172
|
+
return [
|
|
173
|
+
`// Generated by nifra from ${label}. Do not edit: \`nifra types\` rewrites it.`,
|
|
174
|
+
`import type { backend } from "${specifier(dirname(out), backendApp)}"`,
|
|
175
|
+
"",
|
|
176
|
+
'declare module "@nifrajs/client" {',
|
|
177
|
+
" interface Register {",
|
|
178
|
+
" readonly backend: typeof backend",
|
|
179
|
+
" }",
|
|
180
|
+
"}",
|
|
181
|
+
"",
|
|
182
|
+
].join("\n")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Every generated types file for the app, by absolute path. */
|
|
186
|
+
export function routeTypeFiles(options: RouteTypesOptions): ReadonlyMap<string, string> {
|
|
187
|
+
const appRoot = options.appRoot
|
|
188
|
+
const routesDir = options.routesDir ?? join(appRoot, "routes")
|
|
189
|
+
const typesRoot = join(appRoot, ROUTE_TYPES_DIR)
|
|
190
|
+
const files = new Map<string, string>()
|
|
191
|
+
const backendApp = backendAppOf(appRoot)
|
|
192
|
+
const register = backendApp === undefined ? undefined : join(typesRoot, "register.d.ts")
|
|
193
|
+
if (backendApp !== undefined && register !== undefined) {
|
|
194
|
+
files.set(register, registerModule(register, backendApp, posix(relative(appRoot, backendApp))))
|
|
195
|
+
}
|
|
196
|
+
if (!existsSync(routesDir)) return files
|
|
197
|
+
const listed = (readdirSync(routesDir, { recursive: true }) as string[])
|
|
198
|
+
.map(posix)
|
|
199
|
+
.filter(
|
|
200
|
+
(file) =>
|
|
201
|
+
/\.(?:tsx|jsx|svelte|vue|mdx)$/.test(file) ||
|
|
202
|
+
isRouteBackendFile(file) ||
|
|
203
|
+
isMiddlewareFile(file),
|
|
204
|
+
)
|
|
205
|
+
// Discovery only: the importer is never called.
|
|
206
|
+
const manifest = buildManifest(listed, () => () => Promise.reject(new Error("not loaded")))
|
|
207
|
+
const routesRel = posix(relative(appRoot, routesDir))
|
|
208
|
+
for (const route of routeFiles(manifest)) {
|
|
209
|
+
const dir = dirname(route.file)
|
|
210
|
+
const name = route.file.slice(dir === "." ? 0 : dir.length + 1).replace(/\.[^.]+$/, "")
|
|
211
|
+
const out = join(typesRoot, routesRel, dir === "." ? "" : dir, "+types", `${name}.d.ts`)
|
|
212
|
+
files.set(out, routeTypesModule(route, out, routesDir, `${routesRel}/${route.file}`, register))
|
|
213
|
+
}
|
|
214
|
+
return files
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Generated files missing or different on disk, and files on disk the app no longer generates. */
|
|
218
|
+
export function staleRouteTypes(options: RouteTypesOptions): readonly string[] {
|
|
219
|
+
const files = routeTypeFiles(options)
|
|
220
|
+
const stale: string[] = []
|
|
221
|
+
for (const [path, content] of files) {
|
|
222
|
+
if (!existsSync(path) || readFileSync(path, "utf8") !== content) stale.push(path)
|
|
223
|
+
}
|
|
224
|
+
for (const path of onDisk(join(options.appRoot, ROUTE_TYPES_DIR))) {
|
|
225
|
+
if (!files.has(path)) stale.push(path)
|
|
226
|
+
}
|
|
227
|
+
return stale.sort()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function onDisk(dir: string): readonly string[] {
|
|
231
|
+
if (!existsSync(dir)) return []
|
|
232
|
+
return (readdirSync(dir, { recursive: true, withFileTypes: true }) as Dirent[])
|
|
233
|
+
.filter((entry) => entry.isFile())
|
|
234
|
+
.map((entry) => join(entry.parentPath, entry.name))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface RouteTypesResult {
|
|
238
|
+
/** Files written because they were missing or out of date. */
|
|
239
|
+
readonly written: readonly string[]
|
|
240
|
+
/** Files removed because no route generates them any more. */
|
|
241
|
+
readonly removed: readonly string[]
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Bring `.nifra/types` up to date with the app's routes. Unchanged files are left alone, so an
|
|
246
|
+
* editor watching them sees only real changes. Owns the directory: anything else in it is removed.
|
|
247
|
+
*/
|
|
248
|
+
export function writeRouteTypes(options: RouteTypesOptions): RouteTypesResult {
|
|
249
|
+
const files = routeTypeFiles(options)
|
|
250
|
+
const written: string[] = []
|
|
251
|
+
const removed: string[] = []
|
|
252
|
+
for (const [path, content] of files) {
|
|
253
|
+
if (existsSync(path) && readFileSync(path, "utf8") === content) continue
|
|
254
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
255
|
+
writeFileSync(path, content)
|
|
256
|
+
written.push(path)
|
|
257
|
+
}
|
|
258
|
+
for (const path of onDisk(join(options.appRoot, ROUTE_TYPES_DIR))) {
|
|
259
|
+
if (files.has(path)) continue
|
|
260
|
+
rmSync(path)
|
|
261
|
+
removed.push(path)
|
|
262
|
+
}
|
|
263
|
+
return { written: written.sort(), removed: removed.sort() }
|
|
264
|
+
}
|