@nifrajs/web 4.0.2 → 5.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/README.md +11 -0
- package/dist/build-vite.d.ts +19 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +31 -10
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +8 -10
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +90 -31
- package/dist/build.js.map +1 -1
- package/dist/cdn.d.ts +8 -0
- package/dist/cdn.d.ts.map +1 -1
- package/dist/cdn.js +14 -15
- package/dist/cdn.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +25 -7
- package/dist/client.js.map +1 -1
- package/dist/deferred.d.ts +2 -2
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +28 -7
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +24 -5
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +4 -14
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +7 -17
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +13 -1
- package/dist/dev.js.map +1 -1
- package/dist/draft.d.ts +8 -5
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +30 -13
- package/dist/draft.js.map +1 -1
- package/dist/fonts-google.d.ts.map +1 -1
- package/dist/fonts-google.js +7 -0
- package/dist/fonts-google.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/codegen.d.ts +1 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +56 -29
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/disposable-dir.d.ts +8 -0
- package/dist/internal/disposable-dir.d.ts.map +1 -0
- package/dist/internal/disposable-dir.js +25 -0
- package/dist/internal/disposable-dir.js.map +1 -0
- package/dist/internal/entry-chunks.d.ts +9 -0
- package/dist/internal/entry-chunks.d.ts.map +1 -0
- package/dist/internal/entry-chunks.js +15 -0
- package/dist/internal/entry-chunks.js.map +1 -0
- package/dist/internal/evidence-traversal.d.ts +12 -0
- package/dist/internal/evidence-traversal.d.ts.map +1 -0
- package/dist/internal/evidence-traversal.js +48 -0
- package/dist/internal/evidence-traversal.js.map +1 -0
- package/dist/internal/head-merge.d.ts +6 -0
- package/dist/internal/head-merge.d.ts.map +1 -1
- package/dist/internal/head-merge.js +9 -0
- package/dist/internal/head-merge.js.map +1 -1
- package/dist/internal/js-string.d.ts +2 -0
- package/dist/internal/js-string.d.ts.map +1 -1
- package/dist/internal/js-string.js +5 -1
- package/dist/internal/js-string.js.map +1 -1
- package/dist/internal/mount-shadow.js +1 -1
- package/dist/internal/mount-shadow.js.map +1 -1
- package/dist/internal/output-guard.d.ts +7 -0
- package/dist/internal/output-guard.d.ts.map +1 -1
- package/dist/internal/output-guard.js +80 -8
- package/dist/internal/output-guard.js.map +1 -1
- package/dist/internal/page-execution.d.ts +6 -0
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +377 -108
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/progressive-cookies.d.ts +14 -0
- package/dist/internal/progressive-cookies.d.ts.map +1 -0
- package/dist/internal/progressive-cookies.js +43 -0
- package/dist/internal/progressive-cookies.js.map +1 -0
- package/dist/internal/public-env.d.ts +2 -0
- package/dist/internal/public-env.d.ts.map +1 -0
- package/dist/internal/public-env.js +13 -0
- package/dist/internal/public-env.js.map +1 -0
- package/dist/internal/render-document.d.ts +1 -0
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +28 -5
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/response-controls.d.ts +2 -0
- package/dist/internal/response-controls.d.ts.map +1 -1
- package/dist/internal/response-controls.js +26 -16
- package/dist/internal/response-controls.js.map +1 -1
- package/dist/internal/revive-dates.d.ts +16 -0
- package/dist/internal/revive-dates.d.ts.map +1 -0
- package/dist/internal/revive-dates.js +32 -0
- package/dist/internal/revive-dates.js.map +1 -0
- package/dist/internal/route-path.d.ts +8 -0
- package/dist/internal/route-path.d.ts.map +1 -0
- package/dist/internal/route-path.js +8 -0
- package/dist/internal/route-path.js.map +1 -0
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +88 -20
- package/dist/internal/secret-scan.js.map +1 -1
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +33 -5
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/web-app.d.ts +21 -16
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +73 -21
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/wire-date.d.ts +3 -0
- package/dist/internal/wire-date.d.ts.map +1 -0
- package/dist/internal/wire-date.js +14 -0
- package/dist/internal/wire-date.js.map +1 -0
- package/dist/internal/zone-graph.d.ts +4 -0
- package/dist/internal/zone-graph.d.ts.map +1 -1
- package/dist/internal/zone-graph.js +54 -14
- package/dist/internal/zone-graph.js.map +1 -1
- package/dist/isr.d.ts +58 -2
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +296 -39
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts +2 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +29 -3
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +28 -4
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +3 -0
- package/dist/manifest.js.map +1 -1
- package/dist/open-resolved.d.ts +18 -0
- package/dist/open-resolved.d.ts.map +1 -0
- package/dist/open-resolved.js +62 -0
- package/dist/open-resolved.js.map +1 -0
- package/dist/plugins/kit.d.ts +1 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +26 -11
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +3 -1
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +4 -0
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/public-dir.d.ts +8 -1
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +17 -6
- package/dist/public-dir.js.map +1 -1
- package/dist/query.d.ts +13 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +89 -26
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +2 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +1 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +20 -9
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +9 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +39 -46
- package/dist/router.js.map +1 -1
- package/dist/server-components/build-info.d.ts +3 -0
- package/dist/server-components/build-info.d.ts.map +1 -0
- package/dist/server-components/build-info.js +7 -0
- package/dist/server-components/build-info.js.map +1 -0
- package/dist/server-components/build-target.d.ts +12 -0
- package/dist/server-components/build-target.d.ts.map +1 -0
- package/dist/server-components/build-target.js +153 -0
- package/dist/server-components/build-target.js.map +1 -0
- package/dist/server-components/build.d.ts +24 -0
- package/dist/server-components/build.d.ts.map +1 -0
- package/dist/server-components/build.js +366 -0
- package/dist/server-components/build.js.map +1 -0
- package/dist/server-components/bundle-proof.d.ts +3 -0
- package/dist/server-components/bundle-proof.d.ts.map +1 -0
- package/dist/server-components/bundle-proof.js +23 -0
- package/dist/server-components/bundle-proof.js.map +1 -0
- package/dist/server-components/client.d.ts +46 -0
- package/dist/server-components/client.d.ts.map +1 -0
- package/dist/server-components/client.js +594 -0
- package/dist/server-components/client.js.map +1 -0
- package/dist/server-components/definition.d.ts +11 -0
- package/dist/server-components/definition.d.ts.map +1 -0
- package/dist/server-components/definition.js +94 -0
- package/dist/server-components/definition.js.map +1 -0
- package/dist/server-components/dev-http.d.ts +5 -0
- package/dist/server-components/dev-http.d.ts.map +1 -0
- package/dist/server-components/dev-http.js +71 -0
- package/dist/server-components/dev-http.js.map +1 -0
- package/dist/server-components/error.d.ts +9 -0
- package/dist/server-components/error.d.ts.map +1 -0
- package/dist/server-components/error.js +24 -0
- package/dist/server-components/error.js.map +1 -0
- package/dist/server-components/frames.d.ts +7 -0
- package/dist/server-components/frames.d.ts.map +1 -0
- package/dist/server-components/frames.js +169 -0
- package/dist/server-components/frames.js.map +1 -0
- package/dist/server-components/html-policy.d.ts +6 -0
- package/dist/server-components/html-policy.d.ts.map +1 -0
- package/dist/server-components/html-policy.js +75 -0
- package/dist/server-components/html-policy.js.map +1 -0
- package/dist/server-components/html.d.ts +4 -0
- package/dist/server-components/html.d.ts.map +1 -0
- package/dist/server-components/html.js +67 -0
- package/dist/server-components/html.js.map +1 -0
- package/dist/server-components/islands.d.ts +44 -0
- package/dist/server-components/islands.d.ts.map +1 -0
- package/dist/server-components/islands.js +42 -0
- package/dist/server-components/islands.js.map +1 -0
- package/dist/server-components/native.d.ts +4 -0
- package/dist/server-components/native.d.ts.map +1 -0
- package/dist/server-components/native.js +81 -0
- package/dist/server-components/native.js.map +1 -0
- package/dist/server-components/portal.d.ts +6 -0
- package/dist/server-components/portal.d.ts.map +1 -0
- package/dist/server-components/portal.js +35 -0
- package/dist/server-components/portal.js.map +1 -0
- package/dist/server-components/props.d.ts +3 -0
- package/dist/server-components/props.d.ts.map +1 -0
- package/dist/server-components/props.js +14 -0
- package/dist/server-components/props.js.map +1 -0
- package/dist/server-components/protocol.d.ts +44 -0
- package/dist/server-components/protocol.d.ts.map +1 -0
- package/dist/server-components/protocol.js +139 -0
- package/dist/server-components/protocol.js.map +1 -0
- package/dist/server-components/range.d.ts +24 -0
- package/dist/server-components/range.d.ts.map +1 -0
- package/dist/server-components/range.js +263 -0
- package/dist/server-components/range.js.map +1 -0
- package/dist/server-components/registry.d.ts +6 -0
- package/dist/server-components/registry.d.ts.map +1 -0
- package/dist/server-components/registry.js +30 -0
- package/dist/server-components/registry.js.map +1 -0
- package/dist/server-components/response.d.ts +4 -0
- package/dist/server-components/response.d.ts.map +1 -0
- package/dist/server-components/response.js +32 -0
- package/dist/server-components/response.js.map +1 -0
- package/dist/server-components/router-integration.d.ts +9 -0
- package/dist/server-components/router-integration.d.ts.map +1 -0
- package/dist/server-components/router-integration.js +29 -0
- package/dist/server-components/router-integration.js.map +1 -0
- package/dist/server-components/runtime.d.ts +14 -0
- package/dist/server-components/runtime.d.ts.map +1 -0
- package/dist/server-components/runtime.js +276 -0
- package/dist/server-components/runtime.js.map +1 -0
- package/dist/server-components/scheduler.d.ts +14 -0
- package/dist/server-components/scheduler.d.ts.map +1 -0
- package/dist/server-components/scheduler.js +74 -0
- package/dist/server-components/scheduler.js.map +1 -0
- package/dist/server-components/schema.d.ts +4 -0
- package/dist/server-components/schema.d.ts.map +1 -0
- package/dist/server-components/schema.js +77 -0
- package/dist/server-components/schema.js.map +1 -0
- package/dist/server-components/server-entry.d.ts +12 -0
- package/dist/server-components/server-entry.d.ts.map +1 -0
- package/dist/server-components/server-entry.js +90 -0
- package/dist/server-components/server-entry.js.map +1 -0
- package/dist/server-components/source.d.ts +15 -0
- package/dist/server-components/source.d.ts.map +1 -0
- package/dist/server-components/source.js +269 -0
- package/dist/server-components/source.js.map +1 -0
- package/dist/server-components/state.d.ts +4 -0
- package/dist/server-components/state.d.ts.map +1 -0
- package/dist/server-components/state.js +123 -0
- package/dist/server-components/state.js.map +1 -0
- package/dist/server-components/stream.d.ts +6 -0
- package/dist/server-components/stream.d.ts.map +1 -0
- package/dist/server-components/stream.js +70 -0
- package/dist/server-components/stream.js.map +1 -0
- package/dist/server-components/trusted-html.d.ts +3 -0
- package/dist/server-components/trusted-html.d.ts.map +1 -0
- package/dist/server-components/trusted-html.js +14 -0
- package/dist/server-components/trusted-html.js.map +1 -0
- package/dist/server-components/types.d.ts +107 -0
- package/dist/server-components/types.d.ts.map +1 -0
- package/dist/server-components/types.js +2 -0
- package/dist/server-components/types.js.map +1 -0
- package/dist/server-components/typescript.d.ts +11 -0
- package/dist/server-components/typescript.d.ts.map +1 -0
- package/dist/server-components/typescript.js +28 -0
- package/dist/server-components/typescript.js.map +1 -0
- package/dist/server-components/value.d.ts +5 -0
- package/dist/server-components/value.d.ts.map +1 -0
- package/dist/server-components/value.js +77 -0
- package/dist/server-components/value.js.map +1 -0
- package/dist/server-components/vite.d.ts +12 -0
- package/dist/server-components/vite.d.ts.map +1 -0
- package/dist/server-components/vite.js +197 -0
- package/dist/server-components/vite.js.map +1 -0
- package/dist/server-components.d.ts +5 -0
- package/dist/server-components.d.ts.map +1 -0
- package/dist/server-components.js +4 -0
- package/dist/server-components.js.map +1 -0
- package/dist/service-worker.d.ts +8 -1
- package/dist/service-worker.d.ts.map +1 -1
- package/dist/service-worker.js +20 -6
- package/dist/service-worker.js.map +1 -1
- package/dist/vite.d.ts +12 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +7 -1
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +40 -0
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +170 -6
- package/dist/zones.js.map +1 -1
- package/package.json +97 -4
- package/src/build-vite.ts +31 -9
- package/src/build.ts +109 -31
- package/src/cdn.ts +18 -12
- package/src/client.ts +25 -6
- package/src/deferred.ts +27 -9
- package/src/dev-feed.ts +24 -5
- package/src/dev-session.ts +4 -13
- package/src/dev.ts +29 -1
- package/src/draft.ts +35 -14
- package/src/fonts-google.ts +10 -0
- package/src/index.ts +3 -2
- package/src/internal/codegen.ts +60 -29
- package/src/internal/disposable-dir.ts +24 -0
- package/src/internal/entry-chunks.ts +18 -0
- package/src/internal/evidence-traversal.ts +57 -0
- package/src/internal/head-merge.ts +16 -0
- package/src/internal/js-string.ts +6 -1
- package/src/internal/mount-shadow.ts +1 -1
- package/src/internal/output-guard.ts +88 -7
- package/src/internal/page-execution.ts +506 -108
- package/src/internal/progressive-cookies.ts +60 -0
- package/src/internal/public-env.ts +14 -0
- package/src/internal/render-document.ts +26 -11
- package/src/internal/response-controls.ts +31 -14
- package/src/internal/revive-dates.ts +35 -0
- package/src/internal/route-path.ts +8 -0
- package/src/internal/secret-scan.ts +90 -18
- package/src/internal/server-boundary.ts +29 -5
- package/src/internal/web-app.ts +116 -37
- package/src/internal/wire-date.ts +16 -0
- package/src/internal/zone-graph.ts +59 -19
- package/src/isr.ts +351 -43
- package/src/llms-txt.ts +32 -3
- package/src/manifest.ts +28 -4
- package/src/open-resolved.ts +64 -0
- package/src/plugins/kit.ts +31 -11
- package/src/plugins/vite-leak-guard.ts +5 -1
- package/src/public-dir.ts +27 -8
- package/src/query.ts +107 -26
- package/src/render-seam.ts +2 -0
- package/src/route-types.ts +34 -7
- package/src/router.ts +58 -55
- package/src/server-components/build-info.ts +11 -0
- package/src/server-components/build-target.ts +194 -0
- package/src/server-components/build.ts +419 -0
- package/src/server-components/bundle-proof.ts +33 -0
- package/src/server-components/client.ts +663 -0
- package/src/server-components/definition.ts +125 -0
- package/src/server-components/dev-http.ts +75 -0
- package/src/server-components/error.ts +24 -0
- package/src/server-components/frames.ts +181 -0
- package/src/server-components/html-policy.ts +94 -0
- package/src/server-components/html.ts +81 -0
- package/src/server-components/islands.ts +111 -0
- package/src/server-components/native.ts +101 -0
- package/src/server-components/portal.ts +42 -0
- package/src/server-components/props.ts +19 -0
- package/src/server-components/protocol.ts +167 -0
- package/src/server-components/range.ts +313 -0
- package/src/server-components/registry.ts +34 -0
- package/src/server-components/response.ts +44 -0
- package/src/server-components/router-integration.ts +40 -0
- package/src/server-components/runtime.ts +376 -0
- package/src/server-components/scheduler.ts +84 -0
- package/src/server-components/schema.ts +69 -0
- package/src/server-components/server-entry.ts +98 -0
- package/src/server-components/source.ts +324 -0
- package/src/server-components/state.ts +143 -0
- package/src/server-components/stream.ts +71 -0
- package/src/server-components/trusted-html.ts +21 -0
- package/src/server-components/types.ts +138 -0
- package/src/server-components/typescript.ts +39 -0
- package/src/server-components/value.ts +82 -0
- package/src/server-components/vite.ts +242 -0
- package/src/server-components.ts +32 -0
- package/src/service-worker.ts +24 -7
- package/src/vite.ts +8 -1
- package/src/zones.ts +208 -7
package/src/internal/codegen.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { LayoutEntry, Manifest } from "../manifest.ts"
|
|
|
4
4
|
import {
|
|
5
5
|
ACTION_GLOBAL,
|
|
6
6
|
BOUNDARY_GLOBAL,
|
|
7
|
+
COMPONENT_GLOBAL,
|
|
7
8
|
DATA_GLOBAL,
|
|
8
9
|
HANDOVER_ID,
|
|
9
10
|
LAYOUT_DATA_GLOBAL,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
ROUTE_GLOBAL,
|
|
12
13
|
} from "../render-seam.ts"
|
|
13
14
|
import { PRERENDERED_GLOBAL } from "../router.ts"
|
|
14
|
-
import { jsStringLiteral } from "./js-string.ts"
|
|
15
|
+
import { jsStringLiteral, jsValueLiteral } from "./js-string.ts"
|
|
15
16
|
|
|
16
17
|
/** The globals the page-state handover may set - nothing else in it reaches `window`. */
|
|
17
18
|
const HANDOVER_GLOBALS = [
|
|
@@ -20,6 +21,7 @@ const HANDOVER_GLOBALS = [
|
|
|
20
21
|
ROUTE_GLOBAL,
|
|
21
22
|
ACTION_GLOBAL,
|
|
22
23
|
BOUNDARY_GLOBAL,
|
|
24
|
+
COMPONENT_GLOBAL,
|
|
23
25
|
PRERENDERED_GLOBAL,
|
|
24
26
|
]
|
|
25
27
|
export interface GenerateClientEntryOptions {
|
|
@@ -36,6 +38,7 @@ export interface GenerateClientEntryOptions {
|
|
|
36
38
|
readonly clientModule: string
|
|
37
39
|
/** Turn a route/layout source file (relative to the routes dir) into an import specifier. */
|
|
38
40
|
readonly resolve: (file: string) => string
|
|
41
|
+
readonly serverComponents?: boolean
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
@@ -81,21 +84,43 @@ export function generateClientEntry(
|
|
|
81
84
|
files.push(errorFile)
|
|
82
85
|
errorRouteIds.push(route.id)
|
|
83
86
|
}
|
|
84
|
-
loaderRows.push(` ${
|
|
87
|
+
loaderRows.push(` ${jsStringLiteral(route.id)}: ${lazyLoader(files)},`)
|
|
85
88
|
if (route.layoutIds.length > 0) layoutIdsOf[route.id] = route.layoutIds
|
|
86
89
|
patternRows.push(
|
|
87
|
-
` { routeId: ${
|
|
90
|
+
` { routeId: ${jsStringLiteral(route.id)}, pattern: ${jsStringLiteral(route.pattern)} },`,
|
|
88
91
|
)
|
|
89
92
|
}
|
|
93
|
+
// A root status page renders inside the root layout unless it exports `inLayouts = false`, which
|
|
94
|
+
// only its module says - so the layout loads after the page, and only for a page that does not opt out.
|
|
95
|
+
const rootLayout = manifest.layouts._layout
|
|
96
|
+
const statusLoader = (routeId: string, file: string): string =>
|
|
97
|
+
rootLayout === undefined
|
|
98
|
+
? lazyLoader([file])
|
|
99
|
+
: `() => import(${jsStringLiteral(resolve(file))}).then((page) => withRootLayout(${jsStringLiteral(routeId)}, page))`
|
|
90
100
|
if (manifest.notFound !== undefined) {
|
|
91
|
-
loaderRows.push(` "_404": ${
|
|
101
|
+
loaderRows.push(` "_404": ${statusLoader("_404", manifest.notFound.file)},`)
|
|
92
102
|
statusRoutes[404] = "_404"
|
|
93
103
|
}
|
|
94
104
|
for (const [status, entry] of Object.entries(manifest.statusPages ?? {})) {
|
|
95
105
|
const routeId = `_${status}`
|
|
96
|
-
loaderRows.push(` ${
|
|
106
|
+
loaderRows.push(` ${jsStringLiteral(routeId)}: ${statusLoader(routeId, entry.file)},`)
|
|
97
107
|
statusRoutes[Number(status)] = routeId
|
|
98
108
|
}
|
|
109
|
+
const layoutStatusLines =
|
|
110
|
+
rootLayout === undefined || Object.keys(statusRoutes).length === 0
|
|
111
|
+
? []
|
|
112
|
+
: [
|
|
113
|
+
"const layoutStatusRoutes = {}",
|
|
114
|
+
"const withRootLayout = (id, page) => {",
|
|
115
|
+
" if (page.inLayouts === false) return [page]",
|
|
116
|
+
` return import(${jsStringLiteral(resolve(rootLayout.file))}).then((layout) => {`,
|
|
117
|
+
" layoutStatusRoutes[id] = true",
|
|
118
|
+
' layoutIdsOf[id] = ["_layout"]',
|
|
119
|
+
" return [layout, page]",
|
|
120
|
+
" })",
|
|
121
|
+
"}",
|
|
122
|
+
]
|
|
123
|
+
const routerStatusOption = layoutStatusLines.length === 0 ? "" : ", layoutStatusRoutes"
|
|
99
124
|
// `_loading` pages. Emitted only when the app has one, so an app without them imports nothing extra.
|
|
100
125
|
// Each is its own lazy chunk; a route row carries its layout ids and the loading pages above it with
|
|
101
126
|
// the number of those layouts each sits under - all the runtime needs to choose one.
|
|
@@ -110,14 +135,14 @@ export function generateClientEntry(
|
|
|
110
135
|
])
|
|
111
136
|
routeRows.set(
|
|
112
137
|
route.id,
|
|
113
|
-
` ${
|
|
138
|
+
` ${jsStringLiteral(route.id)}: ${jsValueLiteral([route.layoutIds, above])},`,
|
|
114
139
|
)
|
|
115
140
|
}
|
|
116
141
|
loadingLines.push(
|
|
117
142
|
"const loadingModules = {",
|
|
118
143
|
...loadings.map(
|
|
119
144
|
([id, entry]) =>
|
|
120
|
-
` ${
|
|
145
|
+
` ${jsStringLiteral(id)}: () => import(${jsStringLiteral(resolve(entry.file))}),`,
|
|
121
146
|
),
|
|
122
147
|
"}",
|
|
123
148
|
"const loadingRoutes = {",
|
|
@@ -129,14 +154,17 @@ export function generateClientEntry(
|
|
|
129
154
|
return `${[
|
|
130
155
|
// `/client`, never the root: the root's graph carries the server (renderPage, the static-file
|
|
131
156
|
// server), and Vite's dev server evaluates what it is given instead of tree-shaking it.
|
|
132
|
-
'import { createClientRouter, createMatcher, mergeHeads,
|
|
157
|
+
'import { createClientRouter, createMatcher, mergeHeads, resolveChainMeta } from "@nifrajs/web/client"',
|
|
133
158
|
'import { applyHead, currentDocumentNonce, installForms, installHistory, signalHydrated, waitForStyles } from "@nifrajs/web/client"',
|
|
159
|
+
...(options.serverComponents
|
|
160
|
+
? ['import { bindComponentRouter } from "@nifrajs/web/server-components/client"']
|
|
161
|
+
: []),
|
|
134
162
|
// Namespace import: `errorBoundary` is optional (an adapter may not export it). A namespace member
|
|
135
163
|
// access yields `undefined` if absent - unlike a named import, which would be a link error.
|
|
136
164
|
...(loadings.length > 0
|
|
137
165
|
? ['import { withLoading } from "@nifrajs/web/internal/loading-runtime"']
|
|
138
166
|
: []),
|
|
139
|
-
`import * as __adapter from ${
|
|
167
|
+
`import * as __adapter from ${jsStringLiteral(clientModule)}`,
|
|
140
168
|
"const { mountRouter } = __adapter",
|
|
141
169
|
// The assurance hook is optional: ordinary client entries pay only for the namespace lookup, while
|
|
142
170
|
// hydration runners get a runtime identity from the exact adapter module that mounted the page. The
|
|
@@ -152,8 +180,8 @@ export function generateClientEntry(
|
|
|
152
180
|
// names actually exports `mountRouter`. Without this the miss surfaces as
|
|
153
181
|
// "mountRouter is not a function" from inside a bundled chunk, at first paint, naming neither the
|
|
154
182
|
// module nor the requirement - a self-executing entry passes every build and fails only here.
|
|
155
|
-
`if (typeof mountRouter !== "function") throw new Error(${
|
|
156
|
-
`[nifra/web] clientModule ${
|
|
183
|
+
`if (typeof mountRouter !== "function") throw new Error(${jsValueLiteral(
|
|
184
|
+
`[nifra/web] clientModule ${jsStringLiteral(clientModule)} does not export \`mountRouter\`. ` +
|
|
157
185
|
"A client module must export `mountRouter({ router, routes, container })` - it is called by " +
|
|
158
186
|
"the generated bootstrap, so a self-executing entry will not work. Use your adapter's " +
|
|
159
187
|
'`/client` entry (e.g. "@nifrajs/web-react/client"), or re-export mountRouter from it.',
|
|
@@ -169,10 +197,11 @@ export function generateClientEntry(
|
|
|
169
197
|
// to act on. An adapter that knows its framework is in that position says so, and the entry turns
|
|
170
198
|
// the swallowed update into an honest full reload.
|
|
171
199
|
'if (import.meta.hot && __adapter.hotUpdateNeedsReload) import.meta.hot.on("bun:afterUpdate", () => location.reload())',
|
|
172
|
-
`const errorRouteIds = new Set(${
|
|
200
|
+
`const errorRouteIds = new Set(${jsValueLiteral(errorRouteIds)})`,
|
|
173
201
|
// Each route is a lazy loader: dynamic imports → Bun.build (splitting) emits one chunk per
|
|
174
202
|
// route, shared layouts/deps deduped into shared chunks, so a route's code loads only when
|
|
175
203
|
// visited. loadModule caches the [layouts…, page] component chain + the chain's meta list per id.
|
|
204
|
+
...layoutStatusLines,
|
|
176
205
|
"const loaders = {",
|
|
177
206
|
...loaderRows,
|
|
178
207
|
"}",
|
|
@@ -191,7 +220,7 @@ export function generateClientEntry(
|
|
|
191
220
|
// boundary), with the page's `HydrateFallback` - or an empty leaf - where the component goes.
|
|
192
221
|
"const holds = {}",
|
|
193
222
|
// routeId → the ids and `handle` exports `useMatches` reports (layouts, then the page).
|
|
194
|
-
`const layoutIdsOf = ${
|
|
223
|
+
`const layoutIdsOf = ${jsValueLiteral(layoutIdsOf)}`,
|
|
195
224
|
"const matchChains = {}",
|
|
196
225
|
"const loadModule = async (id) => {",
|
|
197
226
|
" if (chains[id]) return",
|
|
@@ -235,11 +264,11 @@ export function generateClientEntry(
|
|
|
235
264
|
// router, the deferred mapper and the adapters read, before any of them runs. Only the LAST
|
|
236
265
|
// matching <script> counts, and only known keys: page HTML (sanitized user content) can carry an
|
|
237
266
|
// element with the same id, and arbitrary keys copied onto `window` include `location`.
|
|
238
|
-
`const handovers = document.querySelectorAll(${
|
|
267
|
+
`const handovers = document.querySelectorAll(${jsValueLiteral(`script[type="application/json"][id="${HANDOVER_ID}"]`)})`,
|
|
239
268
|
"const handover = handovers[handovers.length - 1]",
|
|
240
269
|
"if (handover !== undefined) {",
|
|
241
270
|
' const state = JSON.parse(handover.textContent || "{}")',
|
|
242
|
-
` for (const key of ${
|
|
271
|
+
` for (const key of ${jsValueLiteral(HANDOVER_GLOBALS)}) {`,
|
|
243
272
|
" if (Object.prototype.hasOwnProperty.call(state, key)) Reflect.set(window, key, state[key])",
|
|
244
273
|
" }",
|
|
245
274
|
"}",
|
|
@@ -249,7 +278,7 @@ export function generateClientEntry(
|
|
|
249
278
|
// Derive the initial route from the URL (correct on refresh/deep-link); fall back to the
|
|
250
279
|
// server-injected route id for non-pattern routes (e.g. _404, which matches nothing).
|
|
251
280
|
"const matched = createMatcher(patterns)(location.pathname)",
|
|
252
|
-
`const statusRoutes = ${
|
|
281
|
+
`const statusRoutes = ${jsValueLiteral(statusRoutes)}`,
|
|
253
282
|
`const serverRoute = window.${ROUTE_GLOBAL} ?? ""`,
|
|
254
283
|
// A terminal status page the server rendered (`_404`, `_410`, ...) outranks the URL match: a
|
|
255
284
|
// loader that throws notFound() on `/learn/bad-slug` leaves a URL that still matches
|
|
@@ -270,6 +299,7 @@ export function generateClientEntry(
|
|
|
270
299
|
// Undefined on a page-only app, which is exactly what the router treats as "no layout data".
|
|
271
300
|
` layoutData: window.${LAYOUT_DATA_GLOBAL} && window.${LAYOUT_DATA_GLOBAL}.map(mapDeferred),`,
|
|
272
301
|
` boundaries: mapDeferred(window.${BOUNDARY_GLOBAL}),`,
|
|
302
|
+
` serverComponents: window.${COMPONENT_GLOBAL},`,
|
|
273
303
|
// actionData (only set after a form POST) is in the initial state so the binding hydrates
|
|
274
304
|
// consistently with the server-rendered markup; mapped through `mapDeferred` too so a deferred
|
|
275
305
|
// action's placeholders become registry markers (a no-op when the action didn't defer).
|
|
@@ -280,8 +310,9 @@ export function generateClientEntry(
|
|
|
280
310
|
// With `_loading` pages the store is wrapped: a slow navigation then names a loading chain (the
|
|
281
311
|
// shared layouts + the `_loading` component) for the mounted view to render in the meantime.
|
|
282
312
|
loadings.length > 0
|
|
283
|
-
?
|
|
284
|
-
:
|
|
313
|
+
? `const router = withLoading(createClientRouter({ patterns, initial, loadModule, statusRoutes${routerStatusOption}, searchClientKeys, routeHooks }), { routes: loadingRoutes, modules: loadingModules, chains, searchSchemas, matchChains })`
|
|
314
|
+
: `const router = createClientRouter({ patterns, initial, loadModule, statusRoutes${routerStatusOption}, searchClientKeys, routeHooks })`,
|
|
315
|
+
...(options.serverComponents ? ["bindComponentRouter(router)"] : []),
|
|
285
316
|
"installHistory(router)",
|
|
286
317
|
"installForms(router)",
|
|
287
318
|
// The container is found in the DOM, not baked in. `rootId` is a per-render option and this entry
|
|
@@ -292,7 +323,7 @@ export function generateClientEntry(
|
|
|
292
323
|
`const root = document.querySelector("body > div[${ROOT_ATTRIBUTE}]") ?? document.getElementById("root")`,
|
|
293
324
|
// Loud, because there is no recoverable case: this entry ships only in a document `renderPage`
|
|
294
325
|
// opened a container in, so reaching here means the document is not the one it was built for.
|
|
295
|
-
`if (!root) throw new Error(${
|
|
326
|
+
`if (!root) throw new Error(${jsValueLiteral(
|
|
296
327
|
"[nifra/web] no hydration container found. The generated bootstrap mounts into the element " +
|
|
297
328
|
`renderPage opened the app in - the one marked \`${ROOT_ATTRIBUTE}\` when a custom \`rootId\` ` +
|
|
298
329
|
"is set, `#root` otherwise - and this document has neither. If you emit the document yourself, " +
|
|
@@ -351,8 +382,8 @@ export function generateClientEntry(
|
|
|
351
382
|
// `origin: location.origin` matches the SSR `originOf(req)` (both are `URL.origin` for the same
|
|
352
383
|
// page URL), so a soft-nav re-resolves the SAME absolute canonical/og:url - no head drift.
|
|
353
384
|
" const nonce = currentDocumentNonce()",
|
|
354
|
-
" const args = { data: s.data, params: s.params, origin: location.origin, ...(nonce === undefined ? {} : { nonce }) }",
|
|
355
|
-
" applyHead(mergeHeads((metas[s.routeId] ?? [undefined]
|
|
385
|
+
" const args = { data: s.data, pageData: s.data, params: s.params, origin: location.origin, ...(nonce === undefined ? {} : { nonce }) }",
|
|
386
|
+
" applyHead(mergeHeads(resolveChainMeta(metas[s.routeId] ?? [undefined], s.layoutData, args)))",
|
|
356
387
|
" }",
|
|
357
388
|
" })",
|
|
358
389
|
"}).catch((error) => console.error('[nifra/web] hydration failed:', error))",
|
|
@@ -391,13 +422,13 @@ export function generateRouteSearchTypes(
|
|
|
391
422
|
.sort((a, b) => (a[0] < b[0] ? -1 : 1))
|
|
392
423
|
.map(
|
|
393
424
|
([pattern, spec]) =>
|
|
394
|
-
` ${
|
|
425
|
+
` ${jsStringLiteral(pattern)}: SearchOf<typeof import(${jsStringLiteral(spec)})>`,
|
|
395
426
|
)
|
|
396
427
|
return `${[
|
|
397
428
|
"// GENERATED by @nifrajs/web generateRouteSearchTypes - typed cross-route navigate({ to, search }).",
|
|
398
429
|
"// Regenerated from the route files; a stale navigate search shape becomes a tsc error. Do not edit.",
|
|
399
430
|
'import type { SearchOf } from "@nifrajs/web"',
|
|
400
|
-
`declare module ${
|
|
431
|
+
`declare module ${jsStringLiteral(module)} {`,
|
|
401
432
|
" interface RouteSearch {",
|
|
402
433
|
...entries,
|
|
403
434
|
" }",
|
|
@@ -504,12 +535,12 @@ export function generateServerManifest(
|
|
|
504
535
|
"// the same pure logic discoverRoutes feeds, so patterns + layout chains match exactly.",
|
|
505
536
|
'import { buildManifest, type RouteModule } from "@nifrajs/web"',
|
|
506
537
|
]
|
|
507
|
-
const clientEntryLine = `export const clientEntry = ${
|
|
538
|
+
const clientEntryLine = `export const clientEntry = ${jsStringLiteral(clientEntry)}`
|
|
508
539
|
// Baked stylesheet URLs so the server entry can pass them to createWebApp → <link rel="stylesheet">.
|
|
509
540
|
// Always emitted (default empty) so consumers can `import { styles, routeStyles }` unconditionally.
|
|
510
|
-
const stylesLine = `export const styles = ${
|
|
511
|
-
const routeStylesLine = `export const routeStyles = ${
|
|
512
|
-
const cssLoadingLine = `export const cssLoading = ${
|
|
541
|
+
const stylesLine = `export const styles = ${jsValueLiteral(styles ?? [])}`
|
|
542
|
+
const routeStylesLine = `export const routeStyles = ${jsValueLiteral(routeStyles ?? {})}`
|
|
543
|
+
const cssLoadingLine = `export const cssLoading = ${jsStringLiteral(cssLoading)}`
|
|
513
544
|
if (lazy) {
|
|
514
545
|
// Lazy: `() => import("./routes/x")` per route (static specifier → one chunk per route under a
|
|
515
546
|
// code-splitting bundler, loaded on first request). The map keys are the route-relative paths
|
|
@@ -533,9 +564,9 @@ export function generateServerManifest(
|
|
|
533
564
|
// Eager: `import * as` per route (all bundled into the entry, parsed at boot). Index-based
|
|
534
565
|
// identifiers are collision-proof regardless of filename.
|
|
535
566
|
const imports = files.map(
|
|
536
|
-
(file, i) => `import * as m${i} from ${
|
|
567
|
+
(file, i) => `import * as m${i} from ${jsStringLiteral(moduleSpecifier(resolve(file)))}`,
|
|
537
568
|
)
|
|
538
|
-
const entries = files.map((file, i) => ` ${
|
|
569
|
+
const entries = files.map((file, i) => ` ${jsStringLiteral(file)}: m${i},`)
|
|
539
570
|
return `${[
|
|
540
571
|
...header,
|
|
541
572
|
...imports,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs"
|
|
2
|
+
import { isAbsolute, parse, relative, sep } from "node:path"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Throws unless `dir` is safe to delete recursively: not a filesystem root, and neither equal to nor
|
|
6
|
+
* an ancestor of any path in `keep` (the sources a build reads, the working directory). Both sides are
|
|
7
|
+
* compared after resolving symlinks, so a link pointing at the project is refused too. A `dir` that
|
|
8
|
+
* does not exist yet is always safe.
|
|
9
|
+
*/
|
|
10
|
+
export function assertDisposableDir(option: string, dir: string, keep: readonly string[]): void {
|
|
11
|
+
if (!existsSync(dir)) return
|
|
12
|
+
const real = realpathSync.native(dir)
|
|
13
|
+
if (parse(real).root === real)
|
|
14
|
+
throw new Error(`[nifra/web] ${option} (${dir}) is a filesystem root; refusing to clear it.`)
|
|
15
|
+
for (const path of keep) {
|
|
16
|
+
if (!existsSync(path)) continue
|
|
17
|
+
const rel = relative(real, realpathSync.native(path))
|
|
18
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) continue
|
|
19
|
+
throw new Error(
|
|
20
|
+
`[nifra/web] ${option} (${dir}) ${rel === "" ? "is" : "contains"} ${path}; the build clears ` +
|
|
21
|
+
`${option} first, which would delete it. Point ${option} at a directory of its own.`,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Each entry's emitted JS chunk, keyed by the absolute source path the build metafile records for it.
|
|
5
|
+
* Bun's API does not promise that `outputs` follow `entrypoints` order, so a route's preload is read
|
|
6
|
+
* from the output that names the route's own source as its entry, never from a position.
|
|
7
|
+
*/
|
|
8
|
+
export function entryChunks(
|
|
9
|
+
outputs: Readonly<Record<string, { readonly entryPoint?: string }>>,
|
|
10
|
+
cwd: string,
|
|
11
|
+
): ReadonlyMap<string, string> {
|
|
12
|
+
const chunks = new Map<string, string>()
|
|
13
|
+
for (const [path, output] of Object.entries(outputs)) {
|
|
14
|
+
if (output.entryPoint !== undefined && /\.[cm]?js$/.test(path))
|
|
15
|
+
chunks.set(resolve(cwd, output.entryPoint), path)
|
|
16
|
+
}
|
|
17
|
+
return chunks
|
|
18
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AsyncLocalStorage } from "node:async_hooks"
|
|
2
|
+
import type { BackendEvidenceProvider } from "@nifrajs/core/mount"
|
|
3
|
+
|
|
4
|
+
/** The providers composing the current evidence read, outermost first. */
|
|
5
|
+
export type EvidenceAncestry = ReadonlySet<BackendEvidenceProvider>
|
|
6
|
+
|
|
7
|
+
// Looked up, never imported: a self-contained Worker bundle hoists an import of node:async_hooks,
|
|
8
|
+
// and a Worker on an older compatibility date without nodejs_compat then fails to start. Created
|
|
9
|
+
// on the first evidence read, so serving an app never constructs an AsyncLocalStorage.
|
|
10
|
+
let storage: AsyncLocalStorage<EvidenceAncestry> | undefined
|
|
11
|
+
|
|
12
|
+
// Without async context, a cycle through a provider that drops the chain reads like concurrent
|
|
13
|
+
// reads. A nested read never settles before its children, so such a cycle piles up in-flight reads
|
|
14
|
+
// of one provider without bound, while real concurrency stays at a handful (llms.txt memoizes).
|
|
15
|
+
const MAX_UNTRACKED_READS = 64
|
|
16
|
+
const untrackedReads = new WeakMap<BackendEvidenceProvider, number>()
|
|
17
|
+
|
|
18
|
+
function ancestryStorage(): AsyncLocalStorage<EvidenceAncestry> | undefined {
|
|
19
|
+
const hooks = globalThis.process?.getBuiltinModule?.("node:async_hooks")
|
|
20
|
+
if (hooks?.AsyncLocalStorage === undefined) return undefined
|
|
21
|
+
storage ??= new hooks.AsyncLocalStorage()
|
|
22
|
+
return storage
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run `read` with `provider` joined to the providers already composing this read, refusing a
|
|
27
|
+
* provider that is its own ancestor. The async context follows a cycle through any provider; the
|
|
28
|
+
* `passed` chain, which framework providers hand to the providers they compose, is the only record
|
|
29
|
+
* on a runtime without AsyncLocalStorage, where a cap on in-flight reads of one provider stops a
|
|
30
|
+
* cycle the chain cannot see.
|
|
31
|
+
*/
|
|
32
|
+
export async function traverseEvidence<T>(
|
|
33
|
+
provider: BackendEvidenceProvider,
|
|
34
|
+
passed: unknown,
|
|
35
|
+
read: (ancestry: EvidenceAncestry) => Promise<T>,
|
|
36
|
+
): Promise<T> {
|
|
37
|
+
const context = ancestryStorage()
|
|
38
|
+
const parents = new Set(context?.getStore())
|
|
39
|
+
if (passed instanceof Set) for (const parent of passed) parents.add(parent)
|
|
40
|
+
if (parents.has(provider)) {
|
|
41
|
+
throw new Error("[nifra/web] cyclic composed assurance evidence mount")
|
|
42
|
+
}
|
|
43
|
+
const ancestry: EvidenceAncestry = parents.add(provider)
|
|
44
|
+
if (context !== undefined) return context.run(ancestry, read, ancestry)
|
|
45
|
+
const reads = untrackedReads.get(provider) ?? 0
|
|
46
|
+
if (reads >= MAX_UNTRACKED_READS) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`[nifra/web] cyclic composed assurance evidence mount: more than ${MAX_UNTRACKED_READS} reads of one app in flight. This runtime has no AsyncLocalStorage (on Cloudflare, enable nodejs_compat), so a cycle through a custom provider is caught only by this cap.`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
untrackedReads.set(provider, reads + 1)
|
|
52
|
+
try {
|
|
53
|
+
return await read(ancestry)
|
|
54
|
+
} finally {
|
|
55
|
+
untrackedReads.set(provider, (untrackedReads.get(provider) ?? 1) - 1)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -45,6 +45,22 @@ export function resolveMeta(meta: MetaInput | undefined, args: MetaArgs): Meta {
|
|
|
45
45
|
return meta
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a route's `meta` chain, outermost layout first and the page last. The page's `meta` gets
|
|
50
|
+
* `args`; a layout's gets its own loader data as `data` (`null` when it has no loader), the value its
|
|
51
|
+
* component renders. The SSR head and a soft navigation both resolve through this, so they match.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveChainMeta(
|
|
54
|
+
metas: readonly (MetaInput | undefined)[],
|
|
55
|
+
layoutData: readonly unknown[] | undefined,
|
|
56
|
+
args: MetaArgs,
|
|
57
|
+
): Meta[] {
|
|
58
|
+
const last = metas.length - 1
|
|
59
|
+
return metas.map((meta, i) =>
|
|
60
|
+
resolveMeta(meta, i < last ? { ...args, data: layoutData?.[i] ?? null } : args),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
/**
|
|
49
65
|
* Merge a layout chain's heads, outermost first: `title`/`lang`/`dir` are nearest-wins, and
|
|
50
66
|
* `meta`/`link`/`script` concatenate in chain order.
|
|
@@ -12,7 +12,12 @@ const LINE_SEPARATOR = String.fromCharCode(0x2028)
|
|
|
12
12
|
const PARAGRAPH_SEPARATOR = String.fromCharCode(0x2029)
|
|
13
13
|
|
|
14
14
|
export function jsStringLiteral(value: string): string {
|
|
15
|
-
return
|
|
15
|
+
return jsValueLiteral(value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A JSON value embedded in generated source, with the same script-boundary protection. */
|
|
19
|
+
export function jsValueLiteral(value: unknown): string {
|
|
20
|
+
return (JSON.stringify(value) ?? "undefined")
|
|
16
21
|
.replaceAll("<", "\\u003C")
|
|
17
22
|
.replaceAll(LINE_SEPARATOR, "\\u2028")
|
|
18
23
|
.replaceAll(PARAGRAPH_SEPARATOR, "\\u2029")
|
|
@@ -83,6 +83,6 @@ export function formatShadowedPages(pages: readonly ShadowedPage[]): string {
|
|
|
83
83
|
return (
|
|
84
84
|
`${pages.length === 1 ? "1 page route" : `${pages.length} page routes`} can never render: a mount answers every request under its path before page routing, and its 404 is final.\n` +
|
|
85
85
|
`${lines.join("\n")}\n` +
|
|
86
|
-
|
|
86
|
+
"Move each file out of the mounted path, or serve that URL from the mounted app. The backend's mount path is the `apiPrefix` exported from framework.ts; without one the backend is not mounted."
|
|
87
87
|
)
|
|
88
88
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* its own `validate` output, as zod and valibot objects do by default.
|
|
15
15
|
*/
|
|
16
16
|
import type { StandardResult, StandardSchemaV1 } from "@nifrajs/core/server"
|
|
17
|
+
import { wireDate } from "./wire-date.ts"
|
|
17
18
|
|
|
18
19
|
// Registry symbols shared with core and `@nifrajs/schema`, so two copies of either still agree.
|
|
19
20
|
const RESPONSE_RESULT = Symbol.for("nifra.response.result")
|
|
@@ -23,6 +24,7 @@ const DEFERRED_VALIDATE = Symbol.for("nifra.schema.deferredValidate")
|
|
|
23
24
|
|
|
24
25
|
export const DECLASSIFIED_KEY = "x-nifra-declassified"
|
|
25
26
|
const DEFERRED_KEY = "x-nifra-deferred"
|
|
27
|
+
const DATE_KEY = "x-nifra-date"
|
|
26
28
|
|
|
27
29
|
/** A field name that usually holds a credential or personal identifier. */
|
|
28
30
|
export function isSensitiveFieldName(name: string): boolean {
|
|
@@ -80,6 +82,8 @@ interface CompiledGuard {
|
|
|
80
82
|
/** Undefined when the schema exposes no JSON Schema to project by. */
|
|
81
83
|
readonly project: Project | undefined
|
|
82
84
|
readonly validate: StandardSchemaV1["~standard"]["validate"]
|
|
85
|
+
/** Dev only: name the keys the guard dropped, so a schema missing a field is not a silent blank. */
|
|
86
|
+
readonly warnDropped: boolean
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
interface Deferred {
|
|
@@ -101,6 +105,10 @@ const isPlain = (value: object): boolean => {
|
|
|
101
105
|
return proto === Object.prototype || proto === null
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
// Read once per schema at route load, never per request.
|
|
109
|
+
const inDevelopment = (): boolean =>
|
|
110
|
+
typeof process !== "undefined" && process.env?.NODE_ENV === "development"
|
|
111
|
+
|
|
104
112
|
/** Re-root the issues children pushed since `from` under `key`. Runs only on failure. */
|
|
105
113
|
function prefix(state: RunState, from: number, key: string | number): void {
|
|
106
114
|
for (let i = from; i < state.issues.length; i++) state.issues[i] = `/${key}${state.issues[i]}`
|
|
@@ -142,6 +150,20 @@ const UNKNOWN: Project = (value, state) => {
|
|
|
142
150
|
*/
|
|
143
151
|
const LEAF: Project = (value) => (value instanceof Date ? value.toJSON() : value)
|
|
144
152
|
|
|
153
|
+
const ISO_DATE =
|
|
154
|
+
/^\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:[Zz]|[+-]\d{2}:?\d{2})?)?$/
|
|
155
|
+
// An invalid Date stays as it is, so the schema's own check refuses it by path.
|
|
156
|
+
const toWireDate = (value: Date): unknown =>
|
|
157
|
+
Number.isNaN(value.getTime()) ? value : wireDate(value.getTime())
|
|
158
|
+
|
|
159
|
+
/** A `t.date()` field: a `Date` or an ISO 8601 string becomes a tagged `Date` the browser rebuilds. */
|
|
160
|
+
const DATE: Project = (value) =>
|
|
161
|
+
value instanceof Date
|
|
162
|
+
? toWireDate(value)
|
|
163
|
+
: typeof value === "string" && ISO_DATE.test(value)
|
|
164
|
+
? toWireDate(new Date(value))
|
|
165
|
+
: value
|
|
166
|
+
|
|
145
167
|
/** An array whose items are all scalars, passed through whole. */
|
|
146
168
|
const LEAF_ARRAY: Project = (value) => value
|
|
147
169
|
|
|
@@ -151,8 +173,11 @@ const typesOf = (node: Node): readonly unknown[] | undefined =>
|
|
|
151
173
|
const COMBINATORS = ["anyOf", "oneOf", "allOf"] as const
|
|
152
174
|
|
|
153
175
|
/** What a schema node (with no `$ref` or combinator) projects as. */
|
|
154
|
-
function shapeOf(
|
|
176
|
+
function shapeOf(
|
|
177
|
+
node: Node,
|
|
178
|
+
): "deferred" | "date" | "object" | "array" | "both" | "leaf" | "unknown" {
|
|
155
179
|
if (node[DEFERRED_KEY] === true) return "deferred"
|
|
180
|
+
if (node[DATE_KEY] === true) return "date"
|
|
156
181
|
const types = typesOf(node)
|
|
157
182
|
const object =
|
|
158
183
|
types?.includes("object") === true ||
|
|
@@ -229,6 +254,8 @@ class Compiler {
|
|
|
229
254
|
switch (shapeOf(node)) {
|
|
230
255
|
case "deferred":
|
|
231
256
|
return this.deferred(node, where)
|
|
257
|
+
case "date":
|
|
258
|
+
return DATE
|
|
232
259
|
case "both":
|
|
233
260
|
return this.dispatch(this.object(node, where), this.array(node, where), undefined)
|
|
234
261
|
case "object":
|
|
@@ -411,6 +438,8 @@ class Compiler {
|
|
|
411
438
|
objects.length === 0 ? undefined : this.object(mergeObjects(objects), where),
|
|
412
439
|
arrays.length === 0 ? undefined : this.array(mergeArrays(arrays), where),
|
|
413
440
|
deferred === undefined ? undefined : this.compile(deferred, where),
|
|
441
|
+
// A union's string branch makes an ISO string ambiguous, so only a Date becomes a date here.
|
|
442
|
+
kinds.includes("date"),
|
|
414
443
|
)
|
|
415
444
|
}
|
|
416
445
|
|
|
@@ -418,10 +447,11 @@ class Compiler {
|
|
|
418
447
|
object: Project | undefined,
|
|
419
448
|
array: Project | undefined,
|
|
420
449
|
deferred: Project | undefined,
|
|
450
|
+
date = false,
|
|
421
451
|
): Project {
|
|
422
452
|
return (value, state) => {
|
|
423
453
|
if (typeof value !== "object" || value === null) return value
|
|
424
|
-
if (value instanceof Date) return value.toJSON()
|
|
454
|
+
if (value instanceof Date) return date ? toWireDate(value) : value.toJSON()
|
|
425
455
|
if (Array.isArray(value)) return array === undefined ? value : array(value, state)
|
|
426
456
|
if (isDeferred(value)) {
|
|
427
457
|
if (deferred !== undefined) return deferred(value, state)
|
|
@@ -442,6 +472,7 @@ class Compiler {
|
|
|
442
472
|
const inner: CompiledGuard = {
|
|
443
473
|
project: this.compile(node.inner, where),
|
|
444
474
|
validate: validate as CompiledGuard["validate"],
|
|
475
|
+
warnDropped: inDevelopment(),
|
|
445
476
|
}
|
|
446
477
|
return (value, state) => {
|
|
447
478
|
if (!isDeferred(value)) return value
|
|
@@ -526,7 +557,7 @@ function mergeArrays(branches: readonly Node[]): Node {
|
|
|
526
557
|
}
|
|
527
558
|
|
|
528
559
|
/** The JSON Schema a schema exposes to project by, if any. */
|
|
529
|
-
function jsonSchemaOf(schema: StandardSchemaV1): Node | undefined {
|
|
560
|
+
export function jsonSchemaOf(schema: StandardSchemaV1): Node | undefined {
|
|
530
561
|
const own = (schema as { readonly jsonSchema?: unknown }).jsonSchema
|
|
531
562
|
if (isNode(own)) return own
|
|
532
563
|
const standard = schema["~standard"] as {
|
|
@@ -570,11 +601,52 @@ export function outputGuard(schema: unknown, owner: string, exportName: string):
|
|
|
570
601
|
)
|
|
571
602
|
}
|
|
572
603
|
}
|
|
573
|
-
const guard: CompiledGuard = {
|
|
604
|
+
const guard: CompiledGuard = {
|
|
605
|
+
project,
|
|
606
|
+
validate: standard["~standard"].validate,
|
|
607
|
+
warnDropped: inDevelopment(),
|
|
608
|
+
}
|
|
574
609
|
compiled.set(schema, guard)
|
|
575
610
|
return guard
|
|
576
611
|
}
|
|
577
612
|
|
|
613
|
+
/** Where `before` held a key `after` lacks. Array indices collapse to `*`, so one field reads once. */
|
|
614
|
+
function droppedPaths(before: unknown, after: unknown, path: string, out: Set<string>): void {
|
|
615
|
+
if (before === after || typeof before !== "object" || before === null) return
|
|
616
|
+
if (typeof after !== "object" || after === null || before instanceof Date) return
|
|
617
|
+
if (Array.isArray(before)) {
|
|
618
|
+
if (!Array.isArray(after)) return
|
|
619
|
+
const length = Math.min(before.length, after.length)
|
|
620
|
+
for (let i = 0; i < length; i++) droppedPaths(before[i], after[i], `${path}/*`, out)
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
const kept: Readonly<Record<string, unknown>> = Object.fromEntries(Object.entries(after))
|
|
624
|
+
for (const [key, value] of Object.entries(before)) {
|
|
625
|
+
// JSON drops an undefined value anyway; the guard dropping it changes nothing the page sees.
|
|
626
|
+
if (value === undefined) continue
|
|
627
|
+
if (!Object.hasOwn(kept, key)) out.add(`${path}/${key}`)
|
|
628
|
+
else droppedPaths(value, kept[key], `${path}/${key}`, out)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const warnedDrops = new Set<string>()
|
|
633
|
+
|
|
634
|
+
function warnDropped(label: string, before: unknown, after: unknown): void {
|
|
635
|
+
const found = new Set<string>()
|
|
636
|
+
droppedPaths(before, after, "", found)
|
|
637
|
+
if (found.size === 0) return
|
|
638
|
+
const paths = [...found]
|
|
639
|
+
const key = `${label}\n${paths.join("\n")}`
|
|
640
|
+
if (warnedDrops.has(key)) return
|
|
641
|
+
if (warnedDrops.size >= 1000) warnedDrops.clear()
|
|
642
|
+
warnedDrops.add(key)
|
|
643
|
+
const shown = paths.slice(0, 10).join(", ")
|
|
644
|
+
const more = paths.length > 10 ? ` and ${paths.length - 10} more` : ""
|
|
645
|
+
console.warn(
|
|
646
|
+
`[nifra/web] ${label} returned field(s) its output schema does not declare, so they were dropped before the browser: ${shown}${more}. Declare a field in the schema if the page needs it. (Shown in development only.)`,
|
|
647
|
+
)
|
|
648
|
+
}
|
|
649
|
+
|
|
578
650
|
const pathOf = (issue: { readonly path?: ReadonlyArray<unknown> | undefined }): string =>
|
|
579
651
|
(issue.path ?? [])
|
|
580
652
|
.map(
|
|
@@ -583,7 +655,12 @@ const pathOf = (issue: { readonly path?: ReadonlyArray<unknown> | undefined }):
|
|
|
583
655
|
)
|
|
584
656
|
.join("") || "/"
|
|
585
657
|
|
|
586
|
-
function settle(
|
|
658
|
+
function settle(
|
|
659
|
+
guard: CompiledGuard,
|
|
660
|
+
result: StandardResult<unknown>,
|
|
661
|
+
label: string,
|
|
662
|
+
input: unknown,
|
|
663
|
+
): unknown {
|
|
587
664
|
if (result.issues !== undefined) {
|
|
588
665
|
const paths = [...new Set(result.issues.map(pathOf))]
|
|
589
666
|
throw new OutputGuardError(
|
|
@@ -595,6 +672,8 @@ function settle(guard: CompiledGuard, result: StandardResult<unknown>, label: st
|
|
|
595
672
|
const state: RunState = { label, issues: [] }
|
|
596
673
|
UNKNOWN(result.value, state)
|
|
597
674
|
if (state.issues.length > 0) throw undeclaredDeferred(label, state.issues)
|
|
675
|
+
// No JSON Schema to project by: the schema's own validate stripped what it does not declare.
|
|
676
|
+
if (guard.warnDropped) warnDropped(label, input, result.value)
|
|
598
677
|
}
|
|
599
678
|
return result.value
|
|
600
679
|
}
|
|
@@ -612,11 +691,13 @@ export function runGuard(guard: CompiledGuard, value: unknown, label: string): u
|
|
|
612
691
|
const state: RunState = { label, issues: [] }
|
|
613
692
|
projected = guard.project(value, state)
|
|
614
693
|
if (state.issues.length > 0) throw undeclaredDeferred(label, state.issues)
|
|
694
|
+
// Copy-on-write: the same reference back means nothing was dropped anywhere below.
|
|
695
|
+
if (guard.warnDropped && projected !== value) warnDropped(label, value, projected)
|
|
615
696
|
}
|
|
616
697
|
const result = guard.validate(projected)
|
|
617
698
|
return result instanceof Promise
|
|
618
|
-
? result.then((settled) => settle(guard, settled, label))
|
|
619
|
-
: settle(guard, result, label)
|
|
699
|
+
? result.then((settled) => settle(guard, settled, label, projected))
|
|
700
|
+
: settle(guard, result, label, projected)
|
|
620
701
|
}
|
|
621
702
|
|
|
622
703
|
/** What a browser-bound channel sends, and how it is named in errors. */
|