@nifrajs/web 4.0.2 → 5.1.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 +19 -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 +6 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +46 -18
- 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-port.d.ts +3 -2
- package/dist/dev-port.d.ts.map +1 -1
- package/dist/dev-port.js +2 -6
- package/dist/dev-port.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 +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- 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/package-entries.d.ts +34 -0
- package/dist/internal/package-entries.d.ts.map +1 -0
- package/dist/internal/package-entries.js +324 -0
- package/dist/internal/package-entries.js.map +1 -0
- 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 +437 -114
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +15 -2
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +8 -0
- package/dist/internal/private-env.d.ts.map +1 -1
- package/dist/internal/private-env.js +25 -10
- package/dist/internal/private-env.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 +5 -44
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +51 -63
- 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 +14 -0
- package/dist/internal/route-path.d.ts.map +1 -0
- package/dist/internal/route-path.js +19 -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 +190 -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 +25 -18
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +81 -24
- 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/without-query.d.ts +4 -0
- package/dist/internal/without-query.d.ts.map +1 -0
- package/dist/internal/without-query.js +9 -0
- package/dist/internal/without-query.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 +71 -20
- package/dist/internal/zone-graph.js.map +1 -1
- package/dist/isr.d.ts +59 -3
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +311 -41
- 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 +37 -4
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +4 -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 +9 -1
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +84 -7
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +17 -21
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/plugins/zone-guard.d.ts.map +1 -1
- package/dist/plugins/zone-guard.js +44 -3
- package/dist/plugins/zone-guard.js.map +1 -1
- package/dist/prerender.d.ts +5 -1
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +41 -19
- package/dist/prerender.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 +25 -2
- 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 +47 -50
- package/dist/router.js.map +1 -1
- package/dist/seo.d.ts +49 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +48 -0
- package/dist/seo.js.map +1 -0
- 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 +64 -2
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +331 -28
- package/dist/zones.js.map +1 -1
- package/package.json +102 -4
- package/src/build-vite.ts +31 -9
- package/src/build.ts +109 -31
- package/src/cdn.ts +18 -12
- package/src/client.ts +48 -17
- package/src/deferred.ts +27 -9
- package/src/dev-feed.ts +24 -5
- package/src/dev-port.ts +4 -10
- 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 +5 -6
- 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/package-entries.ts +331 -0
- package/src/internal/page-execution.ts +573 -114
- package/src/internal/parity.ts +15 -2
- package/src/internal/private-env.ts +27 -13
- package/src/internal/progressive-cookies.ts +60 -0
- package/src/internal/public-env.ts +14 -0
- package/src/internal/render-document.ts +66 -89
- package/src/internal/response-controls.ts +31 -14
- package/src/internal/revive-dates.ts +35 -0
- package/src/internal/route-path.ts +24 -0
- package/src/internal/secret-scan.ts +190 -18
- package/src/internal/server-boundary.ts +29 -5
- package/src/internal/web-app.ts +126 -42
- package/src/internal/wire-date.ts +16 -0
- package/src/internal/without-query.ts +8 -0
- package/src/internal/zone-graph.ts +78 -25
- package/src/isr.ts +364 -45
- package/src/llms-txt.ts +32 -3
- package/src/manifest.ts +38 -4
- package/src/open-resolved.ts +64 -0
- package/src/plugins/kit.ts +31 -11
- package/src/plugins/vite-leak-guard.ts +104 -8
- package/src/plugins/vite-zone-guard.ts +21 -19
- package/src/plugins/zone-guard.ts +41 -2
- package/src/prerender.ts +39 -18
- package/src/public-dir.ts +27 -8
- package/src/query.ts +107 -26
- package/src/render-seam.ts +30 -2
- package/src/route-types.ts +34 -7
- package/src/router.ts +66 -59
- package/src/seo.ts +68 -0
- 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 +418 -32
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import type { CapabilityDefinition } from "@nifrajs/core/capabilities"
|
|
2
|
+
import { isResponseResult, isStatusSignal, STATUS_SIGNAL } from "../internal/render-document.ts"
|
|
3
|
+
import {
|
|
4
|
+
authorizeServerComponent,
|
|
5
|
+
isServerComponent,
|
|
6
|
+
prepareAuthorizedServerComponent,
|
|
7
|
+
serverComponentCapabilities,
|
|
8
|
+
validateServerComponentInput,
|
|
9
|
+
} from "./definition.ts"
|
|
10
|
+
import { ServerComponentError } from "./error.ts"
|
|
11
|
+
import { componentFrames } from "./frames.ts"
|
|
12
|
+
import { componentRangeHTML } from "./html.ts"
|
|
13
|
+
import {
|
|
14
|
+
COMPONENT_LIMITS,
|
|
15
|
+
COMPONENT_RESPONSE_HEADERS,
|
|
16
|
+
componentFailure,
|
|
17
|
+
componentKey,
|
|
18
|
+
readComponentRequest,
|
|
19
|
+
} from "./protocol.ts"
|
|
20
|
+
import { BoundedComponentTasks, componentAbort } from "./scheduler.ts"
|
|
21
|
+
import { componentValue } from "./schema.ts"
|
|
22
|
+
import type {
|
|
23
|
+
ComponentInstance,
|
|
24
|
+
ComponentOwner,
|
|
25
|
+
ComponentRecord,
|
|
26
|
+
ServerComponent,
|
|
27
|
+
ServerComponentContext,
|
|
28
|
+
ServerComponentHandle,
|
|
29
|
+
ServerComponentRenderAdapter,
|
|
30
|
+
ServerComponentRuntime,
|
|
31
|
+
} from "./types.ts"
|
|
32
|
+
import { componentInputKey } from "./value.ts"
|
|
33
|
+
|
|
34
|
+
const admission = new BoundedComponentTasks(
|
|
35
|
+
COMPONENT_LIMITS.activeLoads,
|
|
36
|
+
COMPONENT_LIMITS.queuedLoads,
|
|
37
|
+
)
|
|
38
|
+
const encoder = new TextEncoder()
|
|
39
|
+
const token = /^[A-Za-z0-9_-]{1,128}$/
|
|
40
|
+
|
|
41
|
+
export interface ServerComponentsOptions {
|
|
42
|
+
readonly build: string
|
|
43
|
+
readonly adapter: ServerComponentRenderAdapter
|
|
44
|
+
readonly owners: Readonly<Record<string, ComponentOwner>>
|
|
45
|
+
readonly capabilities?: readonly CapabilityDefinition[]
|
|
46
|
+
readonly assets?: readonly string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function serverComponentInstance<Input, Props, View>(
|
|
50
|
+
component: ServerComponent<Input, Props, View>,
|
|
51
|
+
key: string | number,
|
|
52
|
+
input: Input,
|
|
53
|
+
parent?: string,
|
|
54
|
+
): ComponentInstance {
|
|
55
|
+
const instance: ComponentInstance = { component, key, input }
|
|
56
|
+
if (parent !== undefined) Object.assign(instance, { parent })
|
|
57
|
+
return instance
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function defineComponentOwner<const Owner extends ComponentOwner>(owner: Owner): Owner {
|
|
61
|
+
return owner
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function identity(value: unknown): Promise<string> {
|
|
65
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(JSON.stringify(value)))
|
|
66
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")
|
|
67
|
+
}
|
|
68
|
+
export function serverComponentIdentity(
|
|
69
|
+
owner: string,
|
|
70
|
+
component: ServerComponentHandle,
|
|
71
|
+
key: string | number,
|
|
72
|
+
parent: string | null = null,
|
|
73
|
+
): Promise<string> {
|
|
74
|
+
if (
|
|
75
|
+
component.reference === undefined ||
|
|
76
|
+
!componentKey(key) ||
|
|
77
|
+
(parent !== null && !token.test(parent))
|
|
78
|
+
)
|
|
79
|
+
throw new ServerComponentError("component_contract")
|
|
80
|
+
return identity([owner, component.reference, parent, key])
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createServerComponents(options: ServerComponentsOptions): ServerComponentRuntime {
|
|
84
|
+
if (
|
|
85
|
+
!token.test(options.build) ||
|
|
86
|
+
options.adapter.version !== 1 ||
|
|
87
|
+
!["react", "vue", "svelte", "solid", "preact"].includes(options.adapter.name)
|
|
88
|
+
)
|
|
89
|
+
throw new ServerComponentError("component_contract")
|
|
90
|
+
const owners = new Map<
|
|
91
|
+
string,
|
|
92
|
+
{ owner: ComponentOwner; components: ReadonlyMap<string, ServerComponentHandle> }
|
|
93
|
+
>()
|
|
94
|
+
const assets = new Set(options.assets ?? [])
|
|
95
|
+
if (
|
|
96
|
+
[...assets].some(
|
|
97
|
+
(path) => !/^\/assets\/[A-Za-z0-9_./-]+$/.test(path) || path.split("/").includes(".."),
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
throw new ServerComponentError("component_contract")
|
|
101
|
+
const capabilities = new Map(
|
|
102
|
+
(options.capabilities ?? []).map((capability) => [capability.id, capability]),
|
|
103
|
+
)
|
|
104
|
+
if (capabilities.size !== (options.capabilities?.length ?? 0))
|
|
105
|
+
throw new ServerComponentError("component_contract")
|
|
106
|
+
for (const [routeId, owner] of Object.entries(options.owners)) {
|
|
107
|
+
if (
|
|
108
|
+
typeof owner.instances !== "function" ||
|
|
109
|
+
(owner.scope !== "anonymous" && typeof owner.scope !== "function")
|
|
110
|
+
)
|
|
111
|
+
throw new ServerComponentError("component_contract")
|
|
112
|
+
const components = new Map<string, ServerComponentHandle>()
|
|
113
|
+
const allowed = new Set(owner.capabilities ?? [])
|
|
114
|
+
for (const capability of allowed) {
|
|
115
|
+
const declared = capabilities.get(capability)
|
|
116
|
+
if (declared === undefined || (declared.zone === "domain" && declared.access !== "read"))
|
|
117
|
+
throw new ServerComponentError("component_contract")
|
|
118
|
+
}
|
|
119
|
+
for (const component of owner.components) {
|
|
120
|
+
if (
|
|
121
|
+
!isServerComponent(component) ||
|
|
122
|
+
component.reference === undefined ||
|
|
123
|
+
!token.test(component.reference) ||
|
|
124
|
+
components.has(component.reference)
|
|
125
|
+
)
|
|
126
|
+
throw new ServerComponentError("component_contract")
|
|
127
|
+
components.set(component.reference, component)
|
|
128
|
+
if (serverComponentCapabilities(component).some((capability) => !allowed.has(capability)))
|
|
129
|
+
throw new ServerComponentError("component_contract")
|
|
130
|
+
}
|
|
131
|
+
owners.set(routeId, { owner, components })
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
owns: (routeId) => owners.has(routeId),
|
|
135
|
+
capabilities: (routeId) => owners.get(routeId)?.owner.capabilities ?? [],
|
|
136
|
+
classify(request, routeId) {
|
|
137
|
+
const selection = readComponentRequest(request)
|
|
138
|
+
if (selection === undefined) return undefined
|
|
139
|
+
if (selection.build !== options.build)
|
|
140
|
+
throw new ServerComponentError("component_version", 409)
|
|
141
|
+
if (!owners.get(routeId)?.components.has(selection.reference))
|
|
142
|
+
throw new ServerComponentError("component_unavailable", 404)
|
|
143
|
+
return selection
|
|
144
|
+
},
|
|
145
|
+
async execute(routeId, context, selection, nonce) {
|
|
146
|
+
const entry = owners.get(routeId)
|
|
147
|
+
if (entry === undefined) throw new ServerComponentError("component_unavailable", 404)
|
|
148
|
+
const signal = AbortSignal.any([
|
|
149
|
+
context.signal,
|
|
150
|
+
AbortSignal.timeout(COMPONENT_LIMITS.executionMs),
|
|
151
|
+
])
|
|
152
|
+
const ctx: ServerComponentContext = Object.create(context, {
|
|
153
|
+
signal: { value: signal, enumerable: true },
|
|
154
|
+
})
|
|
155
|
+
const { scope, instances } = await admission.run(signal, async () => ({
|
|
156
|
+
scope: entry.owner.scope === "anonymous" ? "anonymous" : await entry.owner.scope(ctx),
|
|
157
|
+
instances: await entry.owner.instances(ctx),
|
|
158
|
+
}))
|
|
159
|
+
if (typeof scope !== "string" || !token.test(scope))
|
|
160
|
+
throw new ServerComponentError("component_contract")
|
|
161
|
+
if (!Array.isArray(instances) || instances.length > COMPONENT_LIMITS.instances)
|
|
162
|
+
throw new ServerComponentError("component_limit", 413)
|
|
163
|
+
const candidates = await Promise.all(
|
|
164
|
+
instances.map(async (instance) => {
|
|
165
|
+
const reference = instance.component.reference
|
|
166
|
+
if (
|
|
167
|
+
reference === undefined ||
|
|
168
|
+
entry.components.get(reference) !== instance.component ||
|
|
169
|
+
!componentKey(instance.key) ||
|
|
170
|
+
(instance.parent !== undefined && !token.test(instance.parent))
|
|
171
|
+
)
|
|
172
|
+
throw new ServerComponentError("component_contract")
|
|
173
|
+
return {
|
|
174
|
+
...instance,
|
|
175
|
+
reference,
|
|
176
|
+
id: await serverComponentIdentity(
|
|
177
|
+
routeId,
|
|
178
|
+
instance.component,
|
|
179
|
+
instance.key,
|
|
180
|
+
instance.parent ?? null,
|
|
181
|
+
),
|
|
182
|
+
input: componentValue(instance.input, COMPONENT_LIMITS.inputBytes),
|
|
183
|
+
}
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
const byId = new Map(candidates.map((instance) => [instance.id, instance]))
|
|
187
|
+
if (byId.size !== candidates.length) throw new ServerComponentError("component_contract")
|
|
188
|
+
await admission.run(signal, () =>
|
|
189
|
+
Promise.all(
|
|
190
|
+
candidates.map((instance) =>
|
|
191
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The owner allowlist and private definition registry identify this handle; input validation precedes callback execution.
|
|
192
|
+
validateServerComponentInput(instance.component as ServerComponent, instance.input),
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
for (const instance of candidates) {
|
|
197
|
+
const chain = new Set([instance.id])
|
|
198
|
+
let parent = instance.parent
|
|
199
|
+
while (parent !== undefined) {
|
|
200
|
+
const ancestor = byId.get(parent)
|
|
201
|
+
if (ancestor === undefined || chain.has(parent) || chain.size >= 16)
|
|
202
|
+
throw new ServerComponentError("component_contract")
|
|
203
|
+
chain.add(parent)
|
|
204
|
+
parent = ancestor.parent
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const roots =
|
|
208
|
+
selection === undefined
|
|
209
|
+
? candidates
|
|
210
|
+
: candidates.filter(
|
|
211
|
+
(item) =>
|
|
212
|
+
item.reference === selection.reference &&
|
|
213
|
+
item.key === selection.key &&
|
|
214
|
+
(item.parent ?? null) === selection.parent &&
|
|
215
|
+
componentInputKey(item.input) === componentInputKey(selection.input),
|
|
216
|
+
)
|
|
217
|
+
if (selection !== undefined && roots.length !== 1)
|
|
218
|
+
throw new ServerComponentError("component_unavailable", 404)
|
|
219
|
+
const selected = new Set(roots.map((item) => item.id))
|
|
220
|
+
if (selection !== undefined)
|
|
221
|
+
for (let depth = 0; depth < 16; depth++)
|
|
222
|
+
for (const item of candidates)
|
|
223
|
+
if (item.parent !== undefined && selected.has(item.parent)) selected.add(item.id)
|
|
224
|
+
const picked =
|
|
225
|
+
selection === undefined
|
|
226
|
+
? candidates
|
|
227
|
+
: [
|
|
228
|
+
...roots,
|
|
229
|
+
...candidates.filter((item) => selected.has(item.id) && item.id !== roots[0]?.id),
|
|
230
|
+
]
|
|
231
|
+
const authorized = new Map<string, object>()
|
|
232
|
+
await admission.run(signal, async () => {
|
|
233
|
+
for (const instance of picked)
|
|
234
|
+
authorized.set(
|
|
235
|
+
instance.id,
|
|
236
|
+
await authorizeServerComponent(
|
|
237
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The owner allowlist and private definition registry identify this handle; input validation precedes callback execution.
|
|
238
|
+
instance.component as ServerComponent,
|
|
239
|
+
instance.input,
|
|
240
|
+
ctx,
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
})
|
|
244
|
+
const requestTasks = new BoundedComponentTasks(
|
|
245
|
+
COMPONENT_LIMITS.requestLoads,
|
|
246
|
+
COMPONENT_LIMITS.instances,
|
|
247
|
+
)
|
|
248
|
+
let bytes = 0
|
|
249
|
+
let count = candidates.length
|
|
250
|
+
const pending = new Map<string, Promise<ComponentRecord>>()
|
|
251
|
+
const render = (instance: (typeof candidates)[number]): Promise<ComponentRecord> => {
|
|
252
|
+
const prior = pending.get(instance.id)
|
|
253
|
+
if (prior !== undefined) return prior
|
|
254
|
+
const task = (async () => {
|
|
255
|
+
const children = await Promise.all(
|
|
256
|
+
picked.filter((child) => child.parent === instance.id).map(render),
|
|
257
|
+
)
|
|
258
|
+
return requestTasks.run(signal, () =>
|
|
259
|
+
admission.run(signal, async () => {
|
|
260
|
+
const id = instance.id
|
|
261
|
+
// The runtime allowlist verified the handle; prepare still validates the schema-derived input and output.
|
|
262
|
+
const prepared = await prepareAuthorizedServerComponent(authorized.get(id)!)
|
|
263
|
+
const renderContext: import("./types.ts").ComponentRenderContext = {
|
|
264
|
+
reference: instance.reference,
|
|
265
|
+
instance: id,
|
|
266
|
+
signal,
|
|
267
|
+
children,
|
|
268
|
+
}
|
|
269
|
+
if (nonce !== undefined) Object.assign(renderContext, { nonce })
|
|
270
|
+
const rendered = await options.adapter.render(
|
|
271
|
+
prepared.view,
|
|
272
|
+
prepared.props,
|
|
273
|
+
renderContext,
|
|
274
|
+
)
|
|
275
|
+
if (signal.aborted) throw componentAbort(signal)
|
|
276
|
+
if (typeof rendered.html !== "string")
|
|
277
|
+
throw new ServerComponentError("component_contract")
|
|
278
|
+
const html = componentRangeHTML(rendered.html, assets)
|
|
279
|
+
count += rendered.islands?.length ?? 0
|
|
280
|
+
if (count > COMPONENT_LIMITS.instances)
|
|
281
|
+
throw new ServerComponentError("component_limit", 413)
|
|
282
|
+
const record: ComponentRecord = {
|
|
283
|
+
instance: id,
|
|
284
|
+
reference: instance.reference,
|
|
285
|
+
key: instance.key,
|
|
286
|
+
parent: instance.parent ?? null,
|
|
287
|
+
input: instance.input,
|
|
288
|
+
html,
|
|
289
|
+
}
|
|
290
|
+
if (rendered.payload !== undefined)
|
|
291
|
+
Object.assign(record, {
|
|
292
|
+
payload: componentValue(rendered.payload, COMPONENT_LIMITS.outputBytes),
|
|
293
|
+
})
|
|
294
|
+
if (rendered.islands !== undefined)
|
|
295
|
+
Object.assign(record, {
|
|
296
|
+
islands: rendered.islands.map((island) => ({
|
|
297
|
+
...island,
|
|
298
|
+
props: componentValue(island.props),
|
|
299
|
+
})),
|
|
300
|
+
})
|
|
301
|
+
const size = encoder.encode(JSON.stringify(record)).length
|
|
302
|
+
bytes += size
|
|
303
|
+
if (
|
|
304
|
+
size > COMPONENT_LIMITS.outputBytes ||
|
|
305
|
+
bytes >
|
|
306
|
+
(selection === undefined
|
|
307
|
+
? COMPONENT_LIMITS.documentBytes
|
|
308
|
+
: COMPONENT_LIMITS.outputBytes)
|
|
309
|
+
)
|
|
310
|
+
throw new ServerComponentError("component_limit", 413)
|
|
311
|
+
return record
|
|
312
|
+
}),
|
|
313
|
+
)
|
|
314
|
+
})()
|
|
315
|
+
pending.set(instance.id, task)
|
|
316
|
+
return task
|
|
317
|
+
}
|
|
318
|
+
const records = await Promise.all(picked.map(render))
|
|
319
|
+
if (signal.aborted) throw componentAbort(signal)
|
|
320
|
+
return {
|
|
321
|
+
v: 1,
|
|
322
|
+
build: options.build,
|
|
323
|
+
adapter: options.adapter.name,
|
|
324
|
+
owner: await identity(routeId),
|
|
325
|
+
scope,
|
|
326
|
+
records,
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
response(state, selection) {
|
|
330
|
+
return new Response(
|
|
331
|
+
componentFrames(
|
|
332
|
+
{
|
|
333
|
+
...selection,
|
|
334
|
+
input: undefined,
|
|
335
|
+
adapter: state.adapter,
|
|
336
|
+
owner: state.owner,
|
|
337
|
+
scope: state.scope,
|
|
338
|
+
instance: state.records[0]?.instance,
|
|
339
|
+
},
|
|
340
|
+
state,
|
|
341
|
+
),
|
|
342
|
+
{ headers: COMPONENT_RESPONSE_HEADERS },
|
|
343
|
+
)
|
|
344
|
+
},
|
|
345
|
+
failure(error) {
|
|
346
|
+
if (isResponseResult(error)) return this.failure(error.toResponse())
|
|
347
|
+
if (isStatusSignal(error)) {
|
|
348
|
+
const status = error[STATUS_SIGNAL]
|
|
349
|
+
const init: ResponseInit = { status: status.status }
|
|
350
|
+
if (status.headers !== undefined) init.headers = status.headers
|
|
351
|
+
return this.failure(new Response(null, init))
|
|
352
|
+
}
|
|
353
|
+
if (error instanceof Response) {
|
|
354
|
+
const headers = new Headers(error.headers)
|
|
355
|
+
headers.set("cache-control", "private, no-store")
|
|
356
|
+
headers.delete("content-length")
|
|
357
|
+
if (error.status >= 300 && error.status < 400)
|
|
358
|
+
return new Response(null, { status: error.status, headers })
|
|
359
|
+
if (error.status < 300) {
|
|
360
|
+
headers.set("x-nifra-component-control", String(error.status))
|
|
361
|
+
return Response.json({ v: 1, control: { status: error.status } }, { headers })
|
|
362
|
+
}
|
|
363
|
+
const code = [401, 403, 404].includes(error.status)
|
|
364
|
+
? "component_denied"
|
|
365
|
+
: "component_failed"
|
|
366
|
+
const response = componentFailure(
|
|
367
|
+
new ServerComponentError(code, error.status >= 400 ? error.status : 500),
|
|
368
|
+
)
|
|
369
|
+
headers.set("content-type", "application/json")
|
|
370
|
+
headers.set("x-content-type-options", "nosniff")
|
|
371
|
+
return new Response(response.body, { status: response.status, headers })
|
|
372
|
+
}
|
|
373
|
+
return componentFailure(error)
|
|
374
|
+
},
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ServerComponentError } from "./error.ts"
|
|
2
|
+
|
|
3
|
+
interface PendingTask {
|
|
4
|
+
readonly signal: AbortSignal
|
|
5
|
+
readonly start: () => void
|
|
6
|
+
readonly reject: (reason: unknown) => void
|
|
7
|
+
readonly cancel: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function componentAbort(signal: AbortSignal): ServerComponentError {
|
|
11
|
+
return new ServerComponentError(
|
|
12
|
+
signal.reason instanceof DOMException && signal.reason.name === "TimeoutError"
|
|
13
|
+
? "component_timeout"
|
|
14
|
+
: "component_cancelled",
|
|
15
|
+
504,
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class BoundedComponentTasks {
|
|
20
|
+
#active = 0
|
|
21
|
+
readonly #queue: PendingTask[] = []
|
|
22
|
+
constructor(
|
|
23
|
+
readonly concurrency: number,
|
|
24
|
+
readonly capacity: number,
|
|
25
|
+
) {
|
|
26
|
+
if (
|
|
27
|
+
!Number.isSafeInteger(concurrency) ||
|
|
28
|
+
concurrency < 1 ||
|
|
29
|
+
!Number.isSafeInteger(capacity) ||
|
|
30
|
+
capacity < 0
|
|
31
|
+
)
|
|
32
|
+
throw new ServerComponentError("component_contract")
|
|
33
|
+
}
|
|
34
|
+
get counts(): { active: number; queued: number } {
|
|
35
|
+
return { active: this.#active, queued: this.#queue.length }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
run<T>(signal: AbortSignal, work: () => T | Promise<T>): Promise<T> {
|
|
39
|
+
if (signal.aborted) return Promise.reject(componentAbort(signal))
|
|
40
|
+
if (this.#active >= this.concurrency && this.#queue.length >= this.capacity)
|
|
41
|
+
return Promise.reject(new ServerComponentError("component_busy", 429))
|
|
42
|
+
return new Promise<T>((resolve, reject) => {
|
|
43
|
+
let began = false
|
|
44
|
+
const cancel = (): void => {
|
|
45
|
+
if (!began) {
|
|
46
|
+
const index = this.#queue.indexOf(pending)
|
|
47
|
+
if (index !== -1) this.#queue.splice(index, 1)
|
|
48
|
+
signal.removeEventListener("abort", cancel)
|
|
49
|
+
}
|
|
50
|
+
reject(componentAbort(signal))
|
|
51
|
+
}
|
|
52
|
+
const start = (): void => {
|
|
53
|
+
began = true
|
|
54
|
+
this.#active++
|
|
55
|
+
const complete = (): void => {
|
|
56
|
+
signal.removeEventListener("abort", cancel)
|
|
57
|
+
this.#active--
|
|
58
|
+
const next = this.#queue.shift()
|
|
59
|
+
if (next !== undefined) next.start()
|
|
60
|
+
}
|
|
61
|
+
// Admission is released by actual completion, never by the public promise's abort.
|
|
62
|
+
void Promise.resolve()
|
|
63
|
+
.then(() => {
|
|
64
|
+
if (signal.aborted) throw componentAbort(signal)
|
|
65
|
+
return work()
|
|
66
|
+
})
|
|
67
|
+
.then(
|
|
68
|
+
(value) => {
|
|
69
|
+
complete()
|
|
70
|
+
resolve(value)
|
|
71
|
+
},
|
|
72
|
+
(error: unknown) => {
|
|
73
|
+
complete()
|
|
74
|
+
reject(error)
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
const pending: PendingTask = { signal, start, reject, cancel }
|
|
79
|
+
signal.addEventListener("abort", cancel, { once: true })
|
|
80
|
+
if (this.#active < this.concurrency) start()
|
|
81
|
+
else this.#queue.push(pending)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@nifrajs/core/server"
|
|
2
|
+
import { jsonSchemaOf } from "../internal/output-guard.ts"
|
|
3
|
+
import { ServerComponentError } from "./error.ts"
|
|
4
|
+
|
|
5
|
+
export { componentValue } from "./value.ts"
|
|
6
|
+
|
|
7
|
+
const forbidden = new Set(["__proto__", "prototype", "constructor"])
|
|
8
|
+
const scalar = new Set(["string", "number", "integer", "boolean", "null"])
|
|
9
|
+
|
|
10
|
+
export function assertComponentSchema(schema: StandardSchemaV1): void {
|
|
11
|
+
const root = jsonSchemaOf(schema)
|
|
12
|
+
if (root === undefined) throw new ServerComponentError("component_contract")
|
|
13
|
+
const visiting = new Set<object>()
|
|
14
|
+
const checked = new Set<object>()
|
|
15
|
+
const visit = (value: unknown, depth: number): void => {
|
|
16
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || depth > 32) {
|
|
17
|
+
throw new ServerComponentError("component_contract")
|
|
18
|
+
}
|
|
19
|
+
if (visiting.has(value)) throw new ServerComponentError("component_contract")
|
|
20
|
+
if (checked.has(value)) return
|
|
21
|
+
visiting.add(value)
|
|
22
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Schema traversal verifies object shape and local references before inspecting schema members.
|
|
23
|
+
const node = value as Record<string, unknown>
|
|
24
|
+
if (node.$ref !== undefined) {
|
|
25
|
+
if (typeof node.$ref !== "string" || !node.$ref.startsWith("#/"))
|
|
26
|
+
throw new ServerComponentError("component_contract")
|
|
27
|
+
let target: unknown = root
|
|
28
|
+
for (const part of node.$ref.slice(2).split("/")) {
|
|
29
|
+
const name = part.replace(/~1/g, "/").replace(/~0/g, "~")
|
|
30
|
+
if (
|
|
31
|
+
forbidden.has(name) ||
|
|
32
|
+
typeof target !== "object" ||
|
|
33
|
+
target === null ||
|
|
34
|
+
!Object.hasOwn(target, name)
|
|
35
|
+
) {
|
|
36
|
+
throw new ServerComponentError("component_contract")
|
|
37
|
+
}
|
|
38
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Schema traversal verifies object shape and local references before inspecting schema members.
|
|
39
|
+
target = (target as Record<string, unknown>)[name]
|
|
40
|
+
}
|
|
41
|
+
visit(target, depth + 1)
|
|
42
|
+
} else if (node.anyOf !== undefined || node.oneOf !== undefined || node.allOf !== undefined) {
|
|
43
|
+
for (const name of ["anyOf", "oneOf", "allOf"]) {
|
|
44
|
+
if (node[name] === undefined) continue
|
|
45
|
+
if (!Array.isArray(node[name]) || node[name].length === 0)
|
|
46
|
+
throw new ServerComponentError("component_contract")
|
|
47
|
+
for (const child of node[name]) visit(child, depth + 1)
|
|
48
|
+
}
|
|
49
|
+
} else if (node.type === "object") {
|
|
50
|
+
if (node.additionalProperties !== false || node.patternProperties !== undefined)
|
|
51
|
+
throw new ServerComponentError("component_contract")
|
|
52
|
+
const properties = node.properties
|
|
53
|
+
if (typeof properties !== "object" || properties === null || Array.isArray(properties))
|
|
54
|
+
throw new ServerComponentError("component_contract")
|
|
55
|
+
for (const [name, child] of Object.entries(properties)) {
|
|
56
|
+
if (forbidden.has(name)) throw new ServerComponentError("component_contract")
|
|
57
|
+
visit(child, depth + 1)
|
|
58
|
+
}
|
|
59
|
+
} else if (node.type === "array") {
|
|
60
|
+
if (Array.isArray(node.items)) for (const child of node.items) visit(child, depth + 1)
|
|
61
|
+
else visit(node.items, depth + 1)
|
|
62
|
+
} else if (typeof node.type !== "string" || !scalar.has(node.type)) {
|
|
63
|
+
throw new ServerComponentError("component_contract")
|
|
64
|
+
}
|
|
65
|
+
visiting.delete(value)
|
|
66
|
+
checked.add(value)
|
|
67
|
+
}
|
|
68
|
+
visit(root, 0)
|
|
69
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface ComponentEntryOptions {
|
|
2
|
+
readonly build: string
|
|
3
|
+
readonly entry: string
|
|
4
|
+
readonly styles: readonly string[]
|
|
5
|
+
readonly assets: readonly string[]
|
|
6
|
+
readonly serverEntry: string
|
|
7
|
+
readonly platform: "bun" | "node" | "deno" | "cloudflare" | "vercel"
|
|
8
|
+
readonly manifestDigest?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Only validated, build-owned paths and tokens become generated source. */
|
|
12
|
+
export function componentServerEntry(options: ComponentEntryOptions): string {
|
|
13
|
+
const local = options.platform !== "cloudflare"
|
|
14
|
+
const setup = local
|
|
15
|
+
? [
|
|
16
|
+
'import { readFile } from "node:fs/promises"',
|
|
17
|
+
'import { createHash } from "node:crypto"',
|
|
18
|
+
'const manifestBytes = await readFile(new URL("./component-manifest.json", import.meta.url))',
|
|
19
|
+
`if (createHash("sha256").update(manifestBytes).digest("hex") !== ${JSON.stringify(options.manifestDigest)}) throw new Error("component_version")`,
|
|
20
|
+
'const manifest = JSON.parse(manifestBytes.toString("utf8"))',
|
|
21
|
+
"for (const [file, hash] of Object.entries(manifest.files)) {",
|
|
22
|
+
' if (!["server", "ssr", "rsc", "assets"].includes(file.split("/")[0]) || file.split("/").some((part) => !/^[A-Za-z0-9_.-]+$/.test(part) || part === "..") || createHash("sha256").update(await readFile(new URL(file, import.meta.url))).digest("hex") !== hash) throw new Error("component_version")',
|
|
23
|
+
"}",
|
|
24
|
+
`const module = await import(${JSON.stringify(`./${options.serverEntry}`)})`,
|
|
25
|
+
]
|
|
26
|
+
: [`import * as module from ${JSON.stringify(`./${options.serverEntry}`)}`]
|
|
27
|
+
const factory = [
|
|
28
|
+
`if (module.__nifraComponentBuild !== ${JSON.stringify(options.build)} || typeof module.createApp !== "function") throw new Error("component_version")`,
|
|
29
|
+
`const app = await module.createApp(${JSON.stringify(options.entry)}, ${JSON.stringify({ styles: options.styles, assets: options.assets, build: options.build })})`,
|
|
30
|
+
'if (typeof app?.fetch !== "function") throw new Error("component_contract")',
|
|
31
|
+
"export const fetch = (request, env, context) => app.fetch(request, { env, ...(context?.waitUntil ? { waitUntil: context.waitUntil.bind(context) } : {}) })",
|
|
32
|
+
]
|
|
33
|
+
if (!local) return `${[...setup, ...factory, "export default { fetch }"].join("\n")}\n`
|
|
34
|
+
const assets = options.platform === "vercel" ? [] : options.assets
|
|
35
|
+
const serving = [
|
|
36
|
+
`const assets = new Set(${JSON.stringify(assets)})`,
|
|
37
|
+
"const serve = async (request) => {",
|
|
38
|
+
" const path = new URL(request.url).pathname",
|
|
39
|
+
' if (assets.has(path) && ["GET", "HEAD"].includes(request.method)) {',
|
|
40
|
+
' const bytes = await readFile(new URL("." + path, import.meta.url))',
|
|
41
|
+
' const type = path.endsWith(".css") ? "text/css" : path.endsWith(".js") ? "text/javascript" : path.endsWith(".svg") ? "image/svg+xml" : "application/octet-stream"',
|
|
42
|
+
' return new Response(request.method === "HEAD" ? null : bytes, { headers: { "content-type": type, "cache-control": "public, max-age=31536000, immutable", "x-content-type-options": "nosniff" } })',
|
|
43
|
+
" }",
|
|
44
|
+
" return fetch(request)",
|
|
45
|
+
"}",
|
|
46
|
+
]
|
|
47
|
+
if (options.platform === "bun")
|
|
48
|
+
return `${[
|
|
49
|
+
...setup,
|
|
50
|
+
...factory,
|
|
51
|
+
...serving,
|
|
52
|
+
"Bun.serve({ port: Number(process.env.PORT ?? 3000), fetch: serve })",
|
|
53
|
+
].join("\n")}\n`
|
|
54
|
+
if (options.platform === "deno")
|
|
55
|
+
return `${[
|
|
56
|
+
...setup,
|
|
57
|
+
...factory,
|
|
58
|
+
...serving,
|
|
59
|
+
'Deno.serve({ port: Number(Deno.env.get("PORT") ?? 3000) }, serve)',
|
|
60
|
+
].join("\n")}\n`
|
|
61
|
+
const http = [
|
|
62
|
+
'import { createServer } from "node:http"',
|
|
63
|
+
'import { Readable } from "node:stream"',
|
|
64
|
+
'import { pipeline } from "node:stream/promises"',
|
|
65
|
+
"const handle = async (req, res) => {",
|
|
66
|
+
" const abort = new AbortController()",
|
|
67
|
+
" const close = () => { if (!res.writableEnded) abort.abort() }",
|
|
68
|
+
' res.on("close", close)',
|
|
69
|
+
" try {",
|
|
70
|
+
" const headers = new Headers()",
|
|
71
|
+
" for (let i = 0; i < req.rawHeaders.length; i += 2) headers.append(req.rawHeaders[i], req.rawHeaders[i + 1])",
|
|
72
|
+
// Resolving the target against a base reads `//evil.example/admin` as a host; the origin comes
|
|
73
|
+
// from a validated Host header, and an absolute-form target is routed by its path.
|
|
74
|
+
' const host = req.headers.host ?? "localhost"',
|
|
75
|
+
" if (!/^[\\w.:[\\]-]+$/.test(host)) { res.writeHead(400); res.end(); return }",
|
|
76
|
+
' const target = req.url.startsWith("/") ? req.url : ((url) => url.pathname + url.search)(new URL(req.url))',
|
|
77
|
+
' const request = new Request("http://" + host + target, { method: req.method, headers, signal: abort.signal, ...(["GET", "HEAD"].includes(req.method) ? {} : { body: Readable.toWeb(req), duplex: "half" }) })',
|
|
78
|
+
" const response = await serve(request)",
|
|
79
|
+
" res.statusCode = response.status",
|
|
80
|
+
' for (const [name, value] of response.headers) if (name !== "set-cookie") res.setHeader(name, value)',
|
|
81
|
+
" const cookies = response.headers.getSetCookie()",
|
|
82
|
+
' if (cookies.length) res.setHeader("set-cookie", cookies)',
|
|
83
|
+
' if (response.body && req.method !== "HEAD") await pipeline(Readable.fromWeb(response.body), res)',
|
|
84
|
+
" else res.end()",
|
|
85
|
+
' } catch { if (!res.headersSent) res.writeHead(500); res.end("component_failed") }',
|
|
86
|
+
' finally { res.off("close", close) }',
|
|
87
|
+
"}",
|
|
88
|
+
]
|
|
89
|
+
return `${[
|
|
90
|
+
...setup,
|
|
91
|
+
...factory,
|
|
92
|
+
...serving,
|
|
93
|
+
...http,
|
|
94
|
+
options.platform === "vercel"
|
|
95
|
+
? "export default handle"
|
|
96
|
+
: 'createServer(handle).listen(Number(process.env.PORT ?? 3000), "0.0.0.0")',
|
|
97
|
+
].join("\n")}\n`
|
|
98
|
+
}
|