@nifrajs/web 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend-only.d.ts +15 -0
- package/dist/backend-only.d.ts.map +1 -0
- package/dist/backend-only.js +16 -0
- package/dist/backend-only.js.map +1 -0
- package/dist/boundary.d.ts +5 -0
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +11 -2
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +35 -6
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +89 -35
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +18 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +97 -17
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +72 -23
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +411 -98
- package/dist/build.js.map +1 -1
- package/dist/bun-dev-entry.d.ts +1 -1
- package/dist/bun-dev-entry.js +1 -1
- package/dist/cdn.d.ts +74 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.js +132 -0
- package/dist/cdn.js.map +1 -0
- package/dist/client.d.ts +21 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +226 -15
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +4 -3
- package/dist/conformance.d.ts.map +1 -1
- package/dist/conformance.js +12 -3
- package/dist/conformance.js.map +1 -1
- package/dist/csp.d.ts +93 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +122 -0
- package/dist/csp.js.map +1 -0
- package/dist/css-contract.d.ts +21 -0
- package/dist/css-contract.d.ts.map +1 -0
- package/dist/css-contract.js +28 -0
- package/dist/css-contract.js.map +1 -0
- package/dist/dev-client.d.ts +76 -0
- package/dist/dev-client.d.ts.map +1 -0
- package/dist/dev-client.js +280 -0
- package/dist/dev-client.js.map +1 -0
- package/dist/dev-diagnostics.d.ts +26 -2
- package/dist/dev-diagnostics.d.ts.map +1 -1
- package/dist/dev-diagnostics.js +38 -7
- package/dist/dev-diagnostics.js.map +1 -1
- package/dist/dev-error.d.ts +4 -13
- package/dist/dev-error.d.ts.map +1 -1
- package/dist/dev-error.js +46 -4
- package/dist/dev-error.js.map +1 -1
- package/dist/dev-feed.d.ts +298 -0
- package/dist/dev-feed.d.ts.map +1 -0
- package/dist/dev-feed.js +797 -0
- package/dist/dev-feed.js.map +1 -0
- package/dist/dev-indicator.d.ts +38 -0
- package/dist/dev-indicator.d.ts.map +1 -0
- package/dist/dev-indicator.js +93 -0
- package/dist/dev-indicator.js.map +1 -0
- package/dist/dev-session.d.ts +77 -0
- package/dist/dev-session.d.ts.map +1 -0
- package/dist/dev-session.js +580 -0
- package/dist/dev-session.js.map +1 -0
- package/dist/dev-sourcemap.d.ts +27 -0
- package/dist/dev-sourcemap.d.ts.map +1 -0
- package/dist/dev-sourcemap.js +235 -0
- package/dist/dev-sourcemap.js.map +1 -0
- package/dist/dev-ssr-graph.d.ts.map +1 -1
- package/dist/dev-ssr-graph.js +13 -5
- package/dist/dev-ssr-graph.js.map +1 -1
- package/dist/dev.d.ts +15 -2
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +69 -44
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +35 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -0
- package/dist/diagnostic-catalog.js +203 -0
- package/dist/diagnostic-catalog.js.map +1 -0
- package/dist/diagnostic-prompt.d.ts +49 -0
- package/dist/diagnostic-prompt.d.ts.map +1 -0
- package/dist/diagnostic-prompt.js +303 -0
- package/dist/diagnostic-prompt.js.map +1 -0
- package/dist/diagnostic.d.ts +17 -23
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +31 -39
- package/dist/diagnostic.js.map +1 -1
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +4 -1
- package/dist/draft.js.map +1 -1
- package/dist/fn.d.ts +5 -0
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +13 -3
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +4 -2
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +15 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +48 -0
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
- package/dist/internal/cdn/cloudflare.js +94 -0
- package/dist/internal/cdn/cloudflare.js.map +1 -0
- package/dist/internal/cdn/core.d.ts +91 -0
- package/dist/internal/cdn/core.d.ts.map +1 -0
- package/dist/internal/cdn/core.js +220 -0
- package/dist/internal/cdn/core.js.map +1 -0
- package/dist/internal/cdn/fastly.d.ts +17 -0
- package/dist/internal/cdn/fastly.d.ts.map +1 -0
- package/dist/internal/cdn/fastly.js +43 -0
- package/dist/internal/cdn/fastly.js.map +1 -0
- package/dist/internal/cdn/vercel.d.ts +31 -0
- package/dist/internal/cdn/vercel.d.ts.map +1 -0
- package/dist/internal/cdn/vercel.js +56 -0
- package/dist/internal/cdn/vercel.js.map +1 -0
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/codegen.d.ts +3 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +151 -30
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-csp.d.ts +24 -0
- package/dist/internal/dev-csp.d.ts.map +1 -0
- package/dist/internal/dev-csp.js +160 -0
- package/dist/internal/dev-csp.js.map +1 -0
- package/dist/internal/dev-reserved.d.ts +25 -0
- package/dist/internal/dev-reserved.d.ts.map +1 -0
- package/dist/internal/dev-reserved.js +39 -0
- package/dist/internal/dev-reserved.js.map +1 -0
- package/dist/internal/html-spans.d.ts +10 -0
- package/dist/internal/html-spans.d.ts.map +1 -0
- package/dist/internal/html-spans.js +46 -0
- package/dist/internal/html-spans.js.map +1 -0
- package/dist/internal/loading-runtime.d.ts +44 -0
- package/dist/internal/loading-runtime.d.ts.map +1 -0
- package/dist/internal/loading-runtime.js +206 -0
- package/dist/internal/loading-runtime.js.map +1 -0
- package/dist/internal/matches-runtime.d.ts +8 -0
- package/dist/internal/matches-runtime.d.ts.map +1 -0
- package/dist/internal/matches-runtime.js +61 -0
- package/dist/internal/matches-runtime.js.map +1 -0
- package/dist/internal/mount-shadow.d.ts +36 -0
- package/dist/internal/mount-shadow.d.ts.map +1 -0
- package/dist/internal/mount-shadow.js +65 -0
- package/dist/internal/mount-shadow.js.map +1 -0
- package/dist/internal/node-builtins.d.ts +3 -0
- package/dist/internal/node-builtins.d.ts.map +1 -0
- package/dist/internal/node-builtins.js +79 -0
- package/dist/internal/node-builtins.js.map +1 -0
- package/dist/internal/output-guard.d.ts +65 -0
- package/dist/internal/output-guard.d.ts.map +1 -0
- package/dist/internal/output-guard.js +630 -0
- package/dist/internal/output-guard.js.map +1 -0
- package/dist/internal/page-execution.d.ts +69 -0
- package/dist/internal/page-execution.d.ts.map +1 -0
- package/dist/internal/page-execution.js +1021 -0
- package/dist/internal/page-execution.js.map +1 -0
- package/dist/internal/parity.d.ts +21 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +104 -22
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +12 -0
- package/dist/internal/private-env.d.ts.map +1 -0
- package/dist/internal/private-env.js +335 -0
- package/dist/internal/private-env.js.map +1 -0
- package/dist/internal/public-url.d.ts +3 -0
- package/dist/internal/public-url.d.ts.map +1 -0
- package/dist/internal/public-url.js +18 -0
- package/dist/internal/public-url.js.map +1 -0
- package/dist/internal/render-document.d.ts +37 -21
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +104 -65
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-url.d.ts +3 -0
- package/dist/internal/request-url.d.ts.map +1 -0
- package/dist/internal/request-url.js +14 -0
- package/dist/internal/request-url.js.map +1 -0
- package/dist/internal/response-controls.d.ts +75 -0
- package/dist/internal/response-controls.d.ts.map +1 -0
- package/dist/internal/response-controls.js +256 -0
- package/dist/internal/response-controls.js.map +1 -0
- package/dist/internal/runtime-contract.d.ts +11 -16
- package/dist/internal/runtime-contract.d.ts.map +1 -1
- package/dist/internal/runtime-contract.js.map +1 -1
- package/dist/internal/secret-scan.d.ts +104 -0
- package/dist/internal/secret-scan.d.ts.map +1 -0
- package/dist/internal/secret-scan.js +485 -0
- package/dist/internal/secret-scan.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +6 -29
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +6 -66
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/source-map.d.ts +47 -0
- package/dist/internal/source-map.d.ts.map +1 -0
- package/dist/internal/source-map.js +225 -0
- package/dist/internal/source-map.js.map +1 -0
- package/dist/internal/target-compat.d.ts +27 -0
- package/dist/internal/target-compat.d.ts.map +1 -0
- package/dist/internal/target-compat.js +77 -0
- package/dist/internal/target-compat.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +6 -0
- package/dist/internal/timing-safe-equal.d.ts.map +1 -1
- package/dist/internal/timing-safe-equal.js +10 -0
- package/dist/internal/timing-safe-equal.js.map +1 -1
- package/dist/internal/vitals-runtime.d.ts +18 -0
- package/dist/internal/vitals-runtime.d.ts.map +1 -0
- package/dist/internal/vitals-runtime.js +38 -0
- package/dist/internal/vitals-runtime.js.map +1 -0
- package/dist/internal/web-app.d.ts +56 -8
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +215 -772
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/zone-graph.d.ts +89 -0
- package/dist/internal/zone-graph.d.ts.map +1 -0
- package/dist/internal/zone-graph.js +492 -0
- package/dist/internal/zone-graph.js.map +1 -0
- package/dist/isr.d.ts +83 -6
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +301 -39
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts +3 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +37 -9
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +221 -14
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +386 -9
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts +26 -5
- package/dist/module-graph.d.ts.map +1 -1
- package/dist/module-graph.js +17 -7
- package/dist/module-graph.js.map +1 -1
- package/dist/navigation.d.ts +17 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +11 -5
- package/dist/navigation.js.map +1 -1
- package/dist/nonce.d.ts +47 -0
- package/dist/nonce.d.ts.map +1 -0
- package/dist/nonce.js +63 -0
- package/dist/nonce.js.map +1 -0
- package/dist/plugins/css-modules.d.ts.map +1 -1
- package/dist/plugins/css-modules.js +4 -3
- package/dist/plugins/css-modules.js.map +1 -1
- package/dist/plugins/kit.d.ts +9 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +18 -0
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/svg.d.ts +22 -3
- package/dist/plugins/svg.d.ts.map +1 -1
- package/dist/plugins/svg.js +112 -1
- package/dist/plugins/svg.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +107 -3
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +378 -17
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +61 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
- package/dist/plugins/vite-zone-guard.js +208 -0
- package/dist/plugins/vite-zone-guard.js.map +1 -0
- package/dist/plugins/zone-guard.d.ts +27 -0
- package/dist/plugins/zone-guard.d.ts.map +1 -0
- package/dist/plugins/zone-guard.js +72 -0
- package/dist/plugins/zone-guard.js.map +1 -0
- package/dist/prerender.d.ts +10 -0
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +35 -3
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +9 -0
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +131 -48
- package/dist/public-dir.js.map +1 -1
- package/dist/pwa-manifest.d.ts +68 -0
- package/dist/pwa-manifest.d.ts.map +1 -0
- package/dist/pwa-manifest.js +178 -0
- package/dist/pwa-manifest.js.map +1 -0
- package/dist/query.js +4 -4
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +42 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +6 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-manifest.d.ts +3 -0
- package/dist/route-manifest.d.ts.map +1 -1
- package/dist/route-manifest.js +26 -3
- package/dist/route-manifest.js.map +1 -1
- package/dist/route-types.d.ts +24 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +195 -0
- package/dist/route-types.js.map +1 -0
- package/dist/router.d.ts +28 -6
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +129 -40
- package/dist/router.js.map +1 -1
- package/dist/trusted-html.d.ts +34 -0
- package/dist/trusted-html.d.ts.map +1 -0
- package/dist/trusted-html.js +35 -0
- package/dist/trusted-html.js.map +1 -0
- package/dist/vitals.d.ts +33 -0
- package/dist/vitals.d.ts.map +1 -0
- package/dist/vitals.js +16 -0
- package/dist/vitals.js.map +1 -0
- package/dist/vite.d.ts +16 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +187 -36
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +70 -0
- package/dist/zones.d.ts.map +1 -0
- package/dist/zones.js +365 -0
- package/dist/zones.js.map +1 -0
- package/package.json +67 -14
- package/src/backend-only.ts +16 -0
- package/src/boundary.ts +18 -5
- package/src/build-plan.ts +113 -36
- package/src/build-vite.ts +124 -15
- package/src/build.ts +535 -102
- package/src/bun-dev-entry.ts +1 -1
- package/src/cdn.ts +216 -0
- package/src/client.ts +251 -18
- package/src/conformance.ts +16 -3
- package/src/csp.ts +194 -0
- package/src/css-contract.ts +44 -0
- package/src/dev-client.ts +349 -0
- package/src/dev-diagnostics.ts +60 -7
- package/src/dev-error.ts +49 -5
- package/src/dev-feed.ts +1140 -0
- package/src/dev-indicator.ts +113 -0
- package/src/dev-session.ts +729 -0
- package/src/dev-sourcemap.ts +281 -0
- package/src/dev-ssr-graph.ts +16 -5
- package/src/dev.ts +86 -43
- package/src/diagnostic-catalog.ts +243 -0
- package/src/diagnostic-prompt.ts +355 -0
- package/src/diagnostic.ts +63 -66
- package/src/draft.ts +4 -1
- package/src/fn.ts +19 -3
- package/src/fs.ts +13 -2
- package/src/index.ts +57 -10
- package/src/internal/cdn/cloudflare.ts +146 -0
- package/src/internal/cdn/core.ts +316 -0
- package/src/internal/cdn/fastly.ts +62 -0
- package/src/internal/cdn/vercel.ts +95 -0
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/codegen.ts +180 -29
- package/src/internal/dev-csp.ts +176 -0
- package/src/internal/dev-reserved.ts +52 -0
- package/src/internal/html-spans.ts +50 -0
- package/src/internal/loading-runtime.ts +244 -0
- package/src/internal/matches-runtime.ts +62 -0
- package/src/internal/mount-shadow.ts +88 -0
- package/src/internal/node-builtins.ts +79 -0
- package/src/internal/output-guard.ts +701 -0
- package/src/internal/page-execution.ts +1371 -0
- package/src/internal/parity.ts +150 -36
- package/src/internal/private-env.ts +334 -0
- package/src/internal/public-url.ts +22 -0
- package/src/internal/render-document.ts +153 -73
- package/src/internal/request-url.ts +14 -0
- package/src/internal/response-controls.ts +284 -0
- package/src/internal/runtime-contract.ts +10 -15
- package/src/internal/secret-scan.ts +623 -0
- package/src/internal/server-boundary.ts +6 -69
- package/src/internal/source-map.ts +256 -0
- package/src/internal/target-compat.ts +92 -0
- package/src/internal/timing-safe-equal.ts +13 -0
- package/src/internal/vitals-runtime.ts +53 -0
- package/src/internal/web-app.ts +290 -879
- package/src/internal/zone-graph.ts +593 -0
- package/src/isr.ts +366 -39
- package/src/llms-txt.ts +44 -8
- package/src/manifest.ts +671 -17
- package/src/module-graph.ts +53 -9
- package/src/navigation.ts +29 -6
- package/src/nonce.ts +104 -0
- package/src/plugins/css-modules.ts +5 -3
- package/src/plugins/kit.ts +24 -0
- package/src/plugins/svg.ts +135 -4
- package/src/plugins/vite-leak-guard.ts +529 -16
- package/src/plugins/vite-zone-guard.ts +263 -0
- package/src/plugins/zone-guard.ts +110 -0
- package/src/prerender.ts +49 -3
- package/src/public-dir.ts +135 -49
- package/src/pwa-manifest.ts +226 -0
- package/src/query.ts +4 -4
- package/src/render-seam.ts +46 -1
- package/src/route-manifest.ts +15 -3
- package/src/route-types.ts +264 -0
- package/src/router.ts +163 -63
- package/src/trusted-html.ts +44 -0
- package/src/vitals.ts +44 -0
- package/src/vite.ts +207 -36
- package/src/zones.ts +430 -0
- package/dist/plugins/vite-server-only.d.ts +0 -49
- package/dist/plugins/vite-server-only.d.ts.map +0 -1
- package/dist/plugins/vite-server-only.js +0 -57
- package/dist/plugins/vite-server-only.js.map +0 -1
- package/dist/server-only.d.ts +0 -21
- package/dist/server-only.d.ts.map +0 -1
- package/dist/server-only.js +0 -25
- package/dist/server-only.js.map +0 -1
- package/src/plugins/vite-server-only.ts +0 -75
- package/src/server-only.ts +0 -25
package/src/build.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* are Bun-specific and never on the request path (own subpath, like `@nifrajs/web/fs`); the *output*
|
|
6
6
|
* runs on any runtime.
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
8
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
9
9
|
import { cp, lstat, mkdir, realpath } from "node:fs/promises"
|
|
10
10
|
import {
|
|
11
11
|
dirname,
|
|
12
12
|
join,
|
|
13
13
|
basename as pathBasename,
|
|
14
|
+
posix,
|
|
14
15
|
relative,
|
|
15
16
|
resolve as resolvePath,
|
|
16
17
|
sep,
|
|
@@ -30,7 +31,9 @@ import {
|
|
|
30
31
|
detectServerOnlyInClient,
|
|
31
32
|
formatNodeBuiltinLeak,
|
|
32
33
|
formatServerOnlyLeak,
|
|
34
|
+
parseBuildTarget,
|
|
33
35
|
parseManifestClientEntry,
|
|
36
|
+
parseManifestCssLoading,
|
|
34
37
|
parseManifestRouteStyles,
|
|
35
38
|
parseManifestStyles,
|
|
36
39
|
planBuildTarget,
|
|
@@ -38,6 +41,7 @@ import {
|
|
|
38
41
|
type SizeReport,
|
|
39
42
|
} from "./build-plan.ts"
|
|
40
43
|
import { sanitizeOutputNames } from "./chunk-names.ts"
|
|
44
|
+
import { type CssLoadingMode, normalizeCssCodeSplit, normalizeCssLoading } from "./css-contract.ts"
|
|
41
45
|
import { discoverRoutes } from "./fs.ts"
|
|
42
46
|
import { generateClientEntry, generateServerManifest } from "./index.ts"
|
|
43
47
|
import { dedupePolicyFor } from "./internal/identity-policy.ts"
|
|
@@ -46,16 +50,42 @@ import {
|
|
|
46
50
|
assertIdentityParity,
|
|
47
51
|
collectDevelopmentParityInput,
|
|
48
52
|
} from "./internal/parity.ts"
|
|
53
|
+
import { privateEnvCheck } from "./internal/private-env.ts"
|
|
54
|
+
import { publicUrlPath } from "./internal/public-url.ts"
|
|
55
|
+
import {
|
|
56
|
+
assertNoSecrets,
|
|
57
|
+
buildEnvironment,
|
|
58
|
+
emittedScanFiles,
|
|
59
|
+
formatSecretFindings,
|
|
60
|
+
graphScanInput,
|
|
61
|
+
originName,
|
|
62
|
+
publicScanFiles,
|
|
63
|
+
type SecretExemption,
|
|
64
|
+
scanForSecrets,
|
|
65
|
+
textOf,
|
|
66
|
+
} from "./internal/secret-scan.ts"
|
|
49
67
|
import {
|
|
50
68
|
generateServerFnStub,
|
|
51
69
|
SERVER_FN_MODULE,
|
|
52
|
-
SERVER_ONLY_MODULE,
|
|
53
|
-
SERVER_ONLY_REPLACEMENT,
|
|
54
70
|
serverFnNamespace,
|
|
55
71
|
} from "./internal/server-boundary.ts"
|
|
72
|
+
import { formatUnsupportedBuiltins, unsupportedBuiltins } from "./internal/target-compat.ts"
|
|
73
|
+
import {
|
|
74
|
+
accountEmittedFiles,
|
|
75
|
+
bunModuleSource,
|
|
76
|
+
type EmittedFile,
|
|
77
|
+
formatClientGraphVerdict,
|
|
78
|
+
formatServerGraphVerdict,
|
|
79
|
+
formatUnaccountedOutput,
|
|
80
|
+
verifyClientGraph,
|
|
81
|
+
verifyServerGraph,
|
|
82
|
+
} from "./internal/zone-graph.ts"
|
|
83
|
+
import { type ClientModuleGraph, fromBunMetafile } from "./module-graph.ts"
|
|
84
|
+
import { zoneGuardPlugin } from "./plugins/zone-guard.ts"
|
|
56
85
|
// `buildTarget(static)` drives the SSG prerender engine directly (it's also re-exported below).
|
|
57
|
-
import { fromBunMetafile } from "./module-graph.ts"
|
|
58
86
|
import { prerenderRoutes } from "./prerender.ts"
|
|
87
|
+
import { CONTENT_TYPES, IMMUTABLE, ONE_DAY } from "./public-dir.ts"
|
|
88
|
+
import { createZoneClassifier } from "./zones.ts"
|
|
59
89
|
|
|
60
90
|
export * from "./build-plan.ts"
|
|
61
91
|
|
|
@@ -80,6 +110,8 @@ interface BunMetafile {
|
|
|
80
110
|
>
|
|
81
111
|
}
|
|
82
112
|
|
|
113
|
+
// The secret scan every client build, `public/` copy and prerender runs.
|
|
114
|
+
export type { SecretExemption, SecretRule } from "./internal/secret-scan.ts"
|
|
83
115
|
// Build-time SSG: prerender opted-in static + dynamic routes to `index.html` (+ static `_data.json`),
|
|
84
116
|
// run after `buildClient`.
|
|
85
117
|
export {
|
|
@@ -133,6 +165,12 @@ export interface BuildClientOptions {
|
|
|
133
165
|
* wins over an auto-exposed var (it's layered last). Sourced from `Bun.env` (falls back to
|
|
134
166
|
* `process.env`) at build time. */
|
|
135
167
|
readonly publicEnvPrefix?: string
|
|
168
|
+
/**
|
|
169
|
+
* Reviewed false positives of the secret scan, which fails the build when a bundle, source map or
|
|
170
|
+
* `public/` file carries what looks like a credential or the value of a non-public environment
|
|
171
|
+
* variable. Each names its rule, its file (or, for an environment value, its variable) and a reason.
|
|
172
|
+
*/
|
|
173
|
+
readonly secretExemptions?: readonly SecretExemption[]
|
|
136
174
|
}
|
|
137
175
|
|
|
138
176
|
/**
|
|
@@ -157,22 +195,6 @@ export function publicEnvDefines(
|
|
|
157
195
|
return defines
|
|
158
196
|
}
|
|
159
197
|
|
|
160
|
-
/**
|
|
161
|
-
* Percent-encode one path segment exactly the way a browser encodes it into a request URL.
|
|
162
|
-
*
|
|
163
|
-
* NOT `encodeURIComponent`. That escapes the sub-delimiters `, @ + = & ; $`, which a browser sends raw -
|
|
164
|
-
* so a file named `report,2026.csv` would be recorded as `/report%2C2026.csv` while the request arrives
|
|
165
|
-
* as `/report,2026.csv`, the allowlist lookup misses, and the file 404s in production only. `encodeURI`
|
|
166
|
-
* agrees with `URL.pathname` on every character except `?` and `#`, which terminate a path and so must
|
|
167
|
-
* be escaped explicitly here.
|
|
168
|
-
*/
|
|
169
|
-
function encodePathSegment(segment: string): string {
|
|
170
|
-
return encodeURI(segment).replace(
|
|
171
|
-
/[?#]/g,
|
|
172
|
-
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
173
|
-
)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
198
|
/**
|
|
177
199
|
* Cloudflare Pages rejects a `_routes.json` with more than 100 include+exclude rules combined, or any
|
|
178
200
|
* single rule longer than 100 characters.
|
|
@@ -245,7 +267,7 @@ function collapsibleDirs(
|
|
|
245
267
|
}
|
|
246
268
|
|
|
247
269
|
/**
|
|
248
|
-
* Build the
|
|
270
|
+
* Build the cloudflare `_routes.json` rules for a set of copied public files, within Cloudflare's budget.
|
|
249
271
|
*
|
|
250
272
|
* `exclude` is what Pages serves straight from the CDN instead of invoking the worker, so naming every
|
|
251
273
|
* public file is ideal - and impossible past ~99 of them. A `public/` of icons, fonts and share images
|
|
@@ -332,7 +354,7 @@ export async function copyPublicDir(from: string, to: string): Promise<string[]>
|
|
|
332
354
|
const target = join(to, rel)
|
|
333
355
|
await mkdir(join(target, ".."), { recursive: true })
|
|
334
356
|
await cp(candidateReal, target)
|
|
335
|
-
copied.push(
|
|
357
|
+
copied.push(publicUrlPath(rel))
|
|
336
358
|
}
|
|
337
359
|
return copied.sort()
|
|
338
360
|
}
|
|
@@ -350,11 +372,13 @@ export function resyncServerManifestSource(
|
|
|
350
372
|
manifest: Parameters<typeof generateServerManifest>[0],
|
|
351
373
|
routesPrefix: string,
|
|
352
374
|
): string {
|
|
375
|
+
const cssLoading = parseManifestCssLoading(source)
|
|
353
376
|
return generateServerManifest(manifest, {
|
|
354
377
|
resolve: (file) => `${routesPrefix}${file}`,
|
|
355
378
|
clientEntry: parseManifestClientEntry(source) ?? "",
|
|
356
379
|
styles: parseManifestStyles(source),
|
|
357
380
|
routeStyles: parseManifestRouteStyles(source),
|
|
381
|
+
...(cssLoading !== undefined ? { cssLoading } : {}),
|
|
358
382
|
lazy: MANIFEST_IS_LAZY.test(source),
|
|
359
383
|
})
|
|
360
384
|
}
|
|
@@ -407,6 +431,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
407
431
|
// layered after these in the `define` object). `Bun` may be absent under non-Bun typecheck - guard it.
|
|
408
432
|
const buildEnv = (typeof Bun !== "undefined" ? Bun.env : undefined) ?? process.env
|
|
409
433
|
const publicDefines = publicEnvDefines(options.publicEnvPrefix ?? "PUBLIC_", buildEnv)
|
|
434
|
+
const privateEnv = privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_")
|
|
410
435
|
// Keep the generated source beside the project, not inside `outDir`: module resolution starts at the
|
|
411
436
|
// importing file, so an absolute `--out /tmp/deploy` must still resolve the app's dependencies.
|
|
412
437
|
// A unique directory also avoids clobbering a user file or colliding with parallel builds.
|
|
@@ -428,6 +453,10 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
428
453
|
...routeManifest.routes.map((r) => r.file),
|
|
429
454
|
...Object.values(routeManifest.layouts).map((l) => l.file),
|
|
430
455
|
...(routeManifest.notFound ? [routeManifest.notFound.file] : []),
|
|
456
|
+
// A nested `_404` is an entry only for its stylesheet: it renders on the server, unhydrated.
|
|
457
|
+
...Object.values(routeManifest.notFounds ?? {}).map((page) => page.file),
|
|
458
|
+
// A `_loading` page is an entry for its stylesheet too: the bootstrap imports it lazily.
|
|
459
|
+
...Object.values(routeManifest.loadings ?? {}).map((page) => page.file),
|
|
431
460
|
]),
|
|
432
461
|
].sort()
|
|
433
462
|
|
|
@@ -436,12 +465,14 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
436
465
|
// entry→CSS link for per-route splitting: keyed by the unique source path, so it survives
|
|
437
466
|
// same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. Not yet in
|
|
438
467
|
// `@types/bun`'s `BuildConfig`, so spread it in (spread props skip the excess-property check).
|
|
439
|
-
const buildExtras = { metafile: true }
|
|
468
|
+
const buildExtras = { metafile: true, throw: false }
|
|
469
|
+
const classifier = createZoneClassifier({ appRoot: root, routesDir, generatedFiles: [entryFile] })
|
|
470
|
+
const refused = new Map<string, string>()
|
|
471
|
+
// No `outdir`: the bundle stays in memory until the graph proves it holds browser code only.
|
|
440
472
|
const result = await (async () => {
|
|
441
473
|
try {
|
|
442
474
|
return await Bun.build({
|
|
443
475
|
entrypoints: [entryFile, ...routeFiles.map(resolve)],
|
|
444
|
-
outdir: outDir,
|
|
445
476
|
target: "browser",
|
|
446
477
|
naming: "[name]-[hash].[ext]",
|
|
447
478
|
publicPath,
|
|
@@ -452,11 +483,16 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
452
483
|
...buildExtras,
|
|
453
484
|
minify: options.minify ?? true,
|
|
454
485
|
plugins: [
|
|
486
|
+
// First, so it sees every file before another plugin's `onLoad` claims it.
|
|
487
|
+
zoneGuardPlugin({
|
|
488
|
+
appRoot: root,
|
|
489
|
+
classifier,
|
|
490
|
+
onDenied: (file, reason) => refused.set(file, reason),
|
|
491
|
+
}),
|
|
455
492
|
...declaredSingleCopyPlugins(root),
|
|
456
493
|
reactDedupePlugin(routesDir),
|
|
457
494
|
preactDedupePlugin(routesDir),
|
|
458
495
|
svelteDedupePlugin(routesDir),
|
|
459
|
-
serverOnlyEmptyPlugin(),
|
|
460
496
|
serverFnStubPlugin(),
|
|
461
497
|
...(options.plugins ?? []),
|
|
462
498
|
],
|
|
@@ -464,10 +500,11 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
464
500
|
// Replace `process.env.*` at compile time so an app module reading config off `process.env` doesn't
|
|
465
501
|
// hit a `process is not defined` crash in the browser. Bun does longest-match: NODE_ENV resolves to
|
|
466
502
|
// the build mode (React's prod/dev branch); each PUBLIC_* var resolves to its baked VALUE; every
|
|
467
|
-
// other `process.env.X` becomes undefined (the bare `process.env` → `
|
|
468
|
-
// never leak). Callers can override any of these via `options.define` (layered last).
|
|
503
|
+
// other `process.env.X` becomes undefined (the bare `process.env` → `{}` fallback - so secrets
|
|
504
|
+
// never leak). Callers can override any of these via `options.define` (layered last). The value
|
|
505
|
+
// must be JSON: Bun inlines anything else (`({})`) as a string literal.
|
|
469
506
|
define: {
|
|
470
|
-
"process.env": "
|
|
507
|
+
"process.env": "{}",
|
|
471
508
|
"process.env.NODE_ENV": JSON.stringify(mode),
|
|
472
509
|
...publicDefines,
|
|
473
510
|
...options.define,
|
|
@@ -477,40 +514,125 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
477
514
|
rmSync(entryDir, { recursive: true, force: true })
|
|
478
515
|
}
|
|
479
516
|
})()
|
|
517
|
+
const cwd = process.cwd()
|
|
518
|
+
const clientMeta = (result as unknown as { metafile?: BunMetafile }).metafile
|
|
480
519
|
if (!result.success) {
|
|
520
|
+
// A refused file can be what broke the build (a backend module importing a server-only built-in),
|
|
521
|
+
// so the refusal is the error to show, not the bundler's symptom.
|
|
522
|
+
const verdict =
|
|
523
|
+
refused.size > 0 && clientMeta !== undefined
|
|
524
|
+
? formatClientGraphVerdict(
|
|
525
|
+
verifyClientGraph(fromBunMetafile(clientMeta), {
|
|
526
|
+
classifier,
|
|
527
|
+
sourceOf: bunModuleSource(cwd),
|
|
528
|
+
privateEnv,
|
|
529
|
+
refused: new Set(refused.keys()),
|
|
530
|
+
}),
|
|
531
|
+
)
|
|
532
|
+
: undefined
|
|
533
|
+
if (verdict !== undefined) throw new Error(verdict)
|
|
534
|
+
if (refused.size > 0) throw new Error(formatRefusedFiles(refused, root))
|
|
481
535
|
throw new Error(
|
|
482
536
|
`[nifra/web] client build failed:\n${result.logs.map((l) => String(l)).join("\n")}`,
|
|
483
537
|
)
|
|
484
538
|
}
|
|
485
539
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
540
|
+
const emitted: EmittedFile[] = await Promise.all(
|
|
541
|
+
result.outputs.map(async (out): Promise<EmittedFile> => {
|
|
542
|
+
if (out.kind === "sourcemap") return { name: out.path, kind: "map", text: await out.text() }
|
|
543
|
+
if (out.kind === "asset") {
|
|
544
|
+
if (out.path.endsWith(".css"))
|
|
545
|
+
return { name: out.path, kind: "css", text: await out.text() }
|
|
546
|
+
const text = textOf(new Uint8Array(await out.arrayBuffer()))
|
|
547
|
+
return { name: out.path, kind: "asset", ...(text === undefined ? {} : { text }) }
|
|
548
|
+
}
|
|
549
|
+
return { name: out.path, kind: "code", text: await out.text() }
|
|
550
|
+
}),
|
|
551
|
+
)
|
|
552
|
+
const clientGraph = withEmittedImports(fromBunMetafile(clientMeta), emitted, publicPath)
|
|
553
|
+
const verdict = verifyClientGraph(clientGraph, {
|
|
554
|
+
classifier,
|
|
555
|
+
sourceOf: bunModuleSource(cwd),
|
|
556
|
+
privateEnv,
|
|
557
|
+
refused: new Set(refused.keys()),
|
|
558
|
+
})
|
|
559
|
+
// Two independent records of one fact: a file the plugin refused that the graph never shows means the
|
|
560
|
+
// graph is missing evidence, and the build cannot prove anything else about it either.
|
|
561
|
+
const reported = new Set(verdict.leaks.map((leak) => resolvePath(root, leak.module)))
|
|
562
|
+
const unseen = [...refused.keys()].filter((file) => !reported.has(file))
|
|
563
|
+
const graphMessage = formatClientGraphVerdict(
|
|
564
|
+
unseen.length === 0
|
|
565
|
+
? verdict
|
|
566
|
+
: {
|
|
567
|
+
...verdict,
|
|
568
|
+
gaps: [
|
|
569
|
+
...verdict.gaps,
|
|
570
|
+
...unseen.map(
|
|
571
|
+
(file) => `${file} was refused while loading but is missing from the graph`,
|
|
572
|
+
),
|
|
573
|
+
],
|
|
574
|
+
},
|
|
575
|
+
)
|
|
576
|
+
if (graphMessage !== undefined) throw new Error(graphMessage)
|
|
493
577
|
// #4: a `node:` builtin (e.g. `node:crypto`) pulled into a CLIENT chunk builds fine (Bun substitutes a
|
|
494
578
|
// browser polyfill) but breaks/leaks at runtime. Fail with the chain (entry → … → builtin), through the
|
|
495
579
|
// SHARED formatter so the Vite pipeline's identical guard reads byte-for-byte the same.
|
|
496
580
|
const nodeBuiltinLeak = formatNodeBuiltinLeak(detectNodeBuiltinsInClient(clientGraph))
|
|
497
581
|
if (nodeBuiltinLeak !== undefined) throw new Error(nodeBuiltinLeak)
|
|
498
582
|
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
// `*.server`, so neither other guard fires. Same shared formatter as above.
|
|
583
|
+
// A module that imports the `backend-only` marker yet reached a client chunk: library code the zones
|
|
584
|
+
// cannot place on a side by path alone. Same shared formatter as the Vite pipeline.
|
|
502
585
|
const serverOnlyLeak = formatServerOnlyLeak(detectServerOnlyInClient(clientGraph))
|
|
503
586
|
if (serverOnlyLeak !== undefined) throw new Error(serverOnlyLeak)
|
|
504
587
|
|
|
588
|
+
const unaccounted = formatUnaccountedOutput(
|
|
589
|
+
accountEmittedFiles(emitted, clientGraph, {
|
|
590
|
+
classifier,
|
|
591
|
+
sourceOf: bunModuleSource(cwd),
|
|
592
|
+
outDir: resolvePath(outDir),
|
|
593
|
+
}),
|
|
594
|
+
)
|
|
595
|
+
if (unaccounted !== undefined) throw new Error(unaccounted)
|
|
596
|
+
|
|
597
|
+
const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public")
|
|
598
|
+
const { sources, originsOf } = graphScanInput(clientGraph, bunModuleSource(cwd), classifier)
|
|
599
|
+
const secrets = formatSecretFindings(
|
|
600
|
+
scanForSecrets({
|
|
601
|
+
sources,
|
|
602
|
+
artifacts: [
|
|
603
|
+
...emitted.flatMap((file) => {
|
|
604
|
+
if (file.text === undefined) return []
|
|
605
|
+
const name = file.name.replace(/^\.\//, "")
|
|
606
|
+
return emittedScanFiles(name, file.text, originsOf(name))
|
|
607
|
+
}),
|
|
608
|
+
...(publicDir !== undefined && existsSync(publicDir)
|
|
609
|
+
? publicScanFiles(publicDir, originName(root, resolvePath(publicDir)))
|
|
610
|
+
: []),
|
|
611
|
+
],
|
|
612
|
+
env: buildEnv,
|
|
613
|
+
publicEnvPrefix: options.publicEnvPrefix ?? "PUBLIC_",
|
|
614
|
+
...(options.secretExemptions ? { exemptions: options.secretExemptions } : {}),
|
|
615
|
+
}),
|
|
616
|
+
)
|
|
617
|
+
if (secrets !== undefined) throw new Error(secrets)
|
|
618
|
+
|
|
619
|
+
const outputs = await Promise.all(
|
|
620
|
+
result.outputs.map(async (out) => {
|
|
621
|
+
const path = join(outDir, out.path)
|
|
622
|
+
await Bun.write(path, out)
|
|
623
|
+
return { kind: out.kind, path }
|
|
624
|
+
}),
|
|
625
|
+
)
|
|
626
|
+
|
|
505
627
|
// Rename any chunk whose basename isn't URL-safe (dynamic-route files become `[slug]-hash.js`) and
|
|
506
628
|
// rewrite the references - otherwise the lazy import 404s and the route silently never hydrates.
|
|
507
|
-
const renamed = sanitizeOutputNames(
|
|
629
|
+
const renamed = sanitizeOutputNames(outputs)
|
|
508
630
|
const toUrl = (path: string): string =>
|
|
509
631
|
`${publicPath}${renamed.get(basename(path)) ?? basename(path)}`
|
|
510
632
|
// Entry-point outputs come back in entrypoint order: [bootstrap, ...routeFiles]. Map each route file
|
|
511
633
|
// to its chunk URL by that order (guarded against drift), then a route's chunks = its layout chain +
|
|
512
634
|
// own file.
|
|
513
|
-
const entryPoints =
|
|
635
|
+
const entryPoints = outputs.filter((o) => o.kind === "entry-point")
|
|
514
636
|
const bootstrap = entryPoints[0]
|
|
515
637
|
if (bootstrap === undefined) throw new Error("[nifra/web] build produced no entry-point output")
|
|
516
638
|
if (entryPoints.length !== routeFiles.length + 1) {
|
|
@@ -549,7 +671,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
549
671
|
// per-route `cssBundle` outputs follow. Dropping those per-route outputs (the old `aggregate`-only
|
|
550
672
|
// shape) left them in `manifest.assets` but not `css`, so a route-scoped stylesheet normalized to
|
|
551
673
|
// `asset:css` and tripped the module-graph parity contract - a bundler-dependent divergence.
|
|
552
|
-
const cssAssets =
|
|
674
|
+
const cssAssets = outputs.filter((o) => o.kind === "asset" && o.path.endsWith(".css"))
|
|
553
675
|
const css: readonly string[] = [
|
|
554
676
|
...cssAssets.filter((o) => cssNameOf(o.path) === bootstrapName),
|
|
555
677
|
...cssAssets.filter((o) => cssNameOf(o.path) !== bootstrapName),
|
|
@@ -561,7 +683,6 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
561
683
|
// same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. A page then
|
|
562
684
|
// links only its layout chain + own CSS (deduped); an empty array means the page needs no CSS at all.
|
|
563
685
|
// Absent (→ aggregate fallback) only if Bun emits no metafile/cssBundle - never silently incomplete.
|
|
564
|
-
const cwd = process.cwd()
|
|
565
686
|
const cssByEntry = new Map<string, string>()
|
|
566
687
|
for (const out of Object.values(clientMeta?.outputs ?? {})) {
|
|
567
688
|
if (out.entryPoint !== undefined && out.cssBundle !== undefined) {
|
|
@@ -581,24 +702,35 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
581
702
|
// SSR pages unstyled. Keep routeStyles absent until at least one authored route entry owns CSS.
|
|
582
703
|
const routeCssEntries = [...cssByEntry.keys()].filter((entry) => entry !== resolvePath(entryFile))
|
|
583
704
|
if (css.length > 0 && routeCssEntries.length > 0) {
|
|
705
|
+
// A `_loading` page can show on a navigation from ANY page, and a document links only its own
|
|
706
|
+
// route's stylesheets - so every page that can start a navigation carries the loading pages' too.
|
|
707
|
+
const loadingFiles = Object.values(routeManifest.loadings ?? {}).map((page) => page.file)
|
|
584
708
|
for (const route of routeManifest.routes) {
|
|
585
709
|
routeStyles[route.id] = stylesFor([
|
|
586
710
|
...route.layoutIds.map((id) => routeManifest.layouts[id]?.file ?? ""),
|
|
587
711
|
route.file,
|
|
712
|
+
...loadingFiles,
|
|
713
|
+
])
|
|
714
|
+
}
|
|
715
|
+
if (routeManifest.notFound) {
|
|
716
|
+
routeStyles._404 = stylesFor([routeManifest.notFound.file, ...loadingFiles])
|
|
717
|
+
}
|
|
718
|
+
for (const [id, page] of Object.entries(routeManifest.notFounds ?? {})) {
|
|
719
|
+
routeStyles[id] = stylesFor([
|
|
720
|
+
...page.layoutIds.map((layoutId) => routeManifest.layouts[layoutId]?.file ?? ""),
|
|
721
|
+
page.file,
|
|
588
722
|
])
|
|
589
723
|
}
|
|
590
|
-
if (routeManifest.notFound) routeStyles._404 = stylesFor([routeManifest.notFound.file])
|
|
591
724
|
}
|
|
592
725
|
|
|
593
726
|
// Copy `public/` into the output next to the hashed assets. A missing directory is normal (most
|
|
594
727
|
// apps have none) and must not fail the build.
|
|
595
|
-
const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public")
|
|
596
728
|
const publicFiles =
|
|
597
729
|
publicDir !== undefined && existsSync(publicDir) ? await copyPublicDir(publicDir, outDir) : []
|
|
598
730
|
|
|
599
731
|
const manifest: BuildManifest = {
|
|
600
732
|
entry: toUrl(bootstrap.path),
|
|
601
|
-
assets:
|
|
733
|
+
assets: outputs.map((o) => toUrl(o.path)),
|
|
602
734
|
routes,
|
|
603
735
|
...(publicFiles.length > 0 ? { publicFiles } : {}),
|
|
604
736
|
...(css.length > 0 ? { css } : {}),
|
|
@@ -626,6 +758,8 @@ export interface BuildServerOptions {
|
|
|
626
758
|
readonly styles?: readonly string[] | undefined
|
|
627
759
|
/** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`) - baked alongside `styles`. */
|
|
628
760
|
readonly routeStyles?: Readonly<Record<string, readonly string[]>> | undefined
|
|
761
|
+
/** How framework-owned stylesheet links should activate before hydration (default `"blocking"`). */
|
|
762
|
+
readonly cssLoading?: CssLoadingMode
|
|
629
763
|
/** Route/layout file → import specifier in the generated manifest (default: a relative path from the
|
|
630
764
|
* manifest's location - written next to `serverEntry` - to `routesDir`). */
|
|
631
765
|
readonly resolve?: (file: string) => string
|
|
@@ -655,6 +789,14 @@ export interface BuildServerOptions {
|
|
|
655
789
|
* - on Cloudflare, ship them with wrangler's `no_bundle` + `find_additional_modules` + an ESModule
|
|
656
790
|
* `rule` (Node/Deno import the chunks natively). Eager (one self-contained file) stays the default. */
|
|
657
791
|
readonly lazy?: boolean
|
|
792
|
+
/** The app root the zone rules classify against (default: the directory holding `routesDir`). */
|
|
793
|
+
readonly root?: string
|
|
794
|
+
/** Modules a build tool wrote or names as an entry (the adapter module a generated entry imports).
|
|
795
|
+
* Like `serverEntry` and the generated manifest, they may import both halves of a route. */
|
|
796
|
+
readonly generatedFiles?: readonly string[]
|
|
797
|
+
/** The public-env prefix the app's browser code may read (default `"PUBLIC_"`). Shared code in
|
|
798
|
+
* the server bundle is held to it too. */
|
|
799
|
+
readonly publicEnvPrefix?: string
|
|
658
800
|
}
|
|
659
801
|
|
|
660
802
|
/** The built worker bundle - point your `wrangler.toml`'s `main` at `worker`. */
|
|
@@ -745,12 +887,55 @@ const SVELTE_DEDUPE_PATTERN = dedupePolicyFor("svelte").bunPattern ?? /^svelte($
|
|
|
745
887
|
* fixed subpath list), Svelte has many internal subpaths, so each matched import is resolved dynamically.
|
|
746
888
|
* `svelte/compiler` (build-time only, not in the bundle) doesn't match the filter and is left alone. No-op
|
|
747
889
|
* when Svelte isn't used / isn't resolvable from `from`.
|
|
890
|
+
*
|
|
891
|
+
* Svelte is NOT condition-agnostic, unlike React and Preact: its bare entry maps `browser` to the client
|
|
892
|
+
* runtime and `default` to the server one. `Bun.resolveSync` answers for the process running the build -
|
|
893
|
+
* a server runtime - so pinning its answer put the SERVER entry into a browser bundle, where `hydrate`
|
|
894
|
+
* throws and the page never becomes interactive. For a browser bundle the package is therefore pinned by
|
|
895
|
+
* DIRECTORY and its export map is read with the bundle's own conditions.
|
|
748
896
|
*/
|
|
749
897
|
export const svelteDedupePlugin = (from: string): BunPlugin => ({
|
|
750
898
|
name: "nifra-svelte-dedupe",
|
|
751
899
|
setup(build) {
|
|
900
|
+
// Absent when the plugin runs as a runtime plugin rather than inside `Bun.build`; a build with no
|
|
901
|
+
// `target` is a browser build (Bun's default).
|
|
902
|
+
const config = (
|
|
903
|
+
build as { config?: { target?: string; conditions?: string | readonly string[] } }
|
|
904
|
+
).config
|
|
905
|
+
const browser = config !== undefined && (config.target ?? "browser") === "browser"
|
|
906
|
+
const conditions = new Set<string>([
|
|
907
|
+
"browser",
|
|
908
|
+
"import",
|
|
909
|
+
...(typeof config?.conditions === "string"
|
|
910
|
+
? [config.conditions]
|
|
911
|
+
: (config?.conditions ?? [])),
|
|
912
|
+
])
|
|
913
|
+
// The pinned copy's export map, read once. `null` when Svelte is not resolvable from the app root.
|
|
914
|
+
let pinned: { readonly root: string; readonly map: Record<string, unknown> } | null | undefined
|
|
915
|
+
const pin = (): { readonly root: string; readonly map: Record<string, unknown> } | null => {
|
|
916
|
+
if (pinned !== undefined) return pinned
|
|
917
|
+
try {
|
|
918
|
+
const manifest = Bun.resolveSync("svelte/package.json", from)
|
|
919
|
+
const map = (JSON.parse(readFileSync(manifest, "utf8")) as { exports?: unknown }).exports
|
|
920
|
+
pinned =
|
|
921
|
+
typeof map === "object" && map !== null
|
|
922
|
+
? { root: dirname(manifest), map: map as Record<string, unknown> }
|
|
923
|
+
: null
|
|
924
|
+
} catch {
|
|
925
|
+
pinned = null
|
|
926
|
+
}
|
|
927
|
+
return pinned
|
|
928
|
+
}
|
|
752
929
|
build.onResolve({ filter: SVELTE_DEDUPE_PATTERN }, (args) => {
|
|
753
930
|
try {
|
|
931
|
+
if (browser) {
|
|
932
|
+
const copy = pin()
|
|
933
|
+
const target =
|
|
934
|
+
copy === null
|
|
935
|
+
? undefined
|
|
936
|
+
: exportTarget(copy.map[`.${args.path.slice("svelte".length)}`], conditions)
|
|
937
|
+
if (copy !== null && target !== undefined) return { path: join(copy.root, target) }
|
|
938
|
+
}
|
|
754
939
|
return { path: Bun.resolveSync(args.path, from) }
|
|
755
940
|
} catch {
|
|
756
941
|
return undefined // not resolvable from the app root - leave Bun's default resolution
|
|
@@ -759,6 +944,19 @@ export const svelteDedupePlugin = (from: string): BunPlugin => ({
|
|
|
759
944
|
},
|
|
760
945
|
})
|
|
761
946
|
|
|
947
|
+
/** Walk one `exports` entry the way a resolver does: the first key, in the package's own order, that is
|
|
948
|
+
* an active condition (or `default`) wins. */
|
|
949
|
+
function exportTarget(entry: unknown, conditions: ReadonlySet<string>): string | undefined {
|
|
950
|
+
if (typeof entry === "string") return entry
|
|
951
|
+
if (typeof entry !== "object" || entry === null) return undefined
|
|
952
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
953
|
+
if (key !== "default" && !conditions.has(key)) continue
|
|
954
|
+
const target = exportTarget(value, conditions)
|
|
955
|
+
if (target !== undefined) return target
|
|
956
|
+
}
|
|
957
|
+
return undefined
|
|
958
|
+
}
|
|
959
|
+
|
|
762
960
|
/**
|
|
763
961
|
* The app's declared single-copy rule, applied to the bundle.
|
|
764
962
|
*
|
|
@@ -775,33 +973,60 @@ const declaredSingleCopyPlugins = (root: string): readonly BunPlugin[] =>
|
|
|
775
973
|
readSingleCopyDeclaration(root) === undefined ? [] : [declaredSingleCopyPlugin({ cwd: root })]
|
|
776
974
|
|
|
777
975
|
/**
|
|
778
|
-
*
|
|
779
|
-
* `
|
|
780
|
-
*
|
|
781
|
-
* import resolves to `undefined` rather than a "missing export" bundle error (verified), and the real
|
|
782
|
-
* import subtree is gone. The complement to the node-builtin guard: when a server-only import is co-located
|
|
783
|
-
* in a route file (so it can't be tree-shaken out and the guard fails loud), moving it into a `*.server`
|
|
784
|
-
* module is the fix. CLIENT-only - buildServer keeps the real module, which runs server-side.
|
|
976
|
+
* Give each output chunk the specifiers its emitted code imports. Bun's metafile does not record them
|
|
977
|
+
* (its output `imports` stay empty even for an external the code keeps), so they are read from the code.
|
|
978
|
+
* A specifier under `publicPath` or relative to the chunk is mapped back to the output it names.
|
|
785
979
|
*/
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
980
|
+
function withEmittedImports(
|
|
981
|
+
graph: ClientModuleGraph,
|
|
982
|
+
emitted: readonly EmittedFile[],
|
|
983
|
+
publicPath?: string,
|
|
984
|
+
): ClientModuleGraph {
|
|
985
|
+
const scanner = new Bun.Transpiler({ loader: "js" })
|
|
986
|
+
const imports = new Map<string, string[]>()
|
|
987
|
+
for (const file of emitted) {
|
|
988
|
+
if (file.kind !== "code" || file.text === undefined) {
|
|
989
|
+
imports.set(posix.normalize(file.name), [])
|
|
990
|
+
continue
|
|
991
|
+
}
|
|
992
|
+
imports.set(
|
|
993
|
+
posix.normalize(file.name),
|
|
994
|
+
scanner.scanImports(file.text).map(({ path }) => {
|
|
995
|
+
if (publicPath !== undefined && path.startsWith(publicPath))
|
|
996
|
+
return path.slice(publicPath.length)
|
|
997
|
+
if (path.startsWith("./") || path.startsWith("../"))
|
|
998
|
+
return posix.join(posix.dirname(posix.normalize(file.name)), path)
|
|
999
|
+
return path
|
|
1000
|
+
}),
|
|
1001
|
+
)
|
|
1002
|
+
}
|
|
1003
|
+
const chunks: Record<string, ClientModuleGraph["chunks"][string]> = {}
|
|
1004
|
+
// A chunk with no emitted file keeps no `imports`, which the verifiers treat as missing evidence.
|
|
1005
|
+
for (const [path, chunk] of Object.entries(graph.chunks)) {
|
|
1006
|
+
const kept = imports.get(posix.normalize(path))
|
|
1007
|
+
chunks[path] = kept === undefined ? chunk : { ...chunk, imports: kept }
|
|
1008
|
+
}
|
|
1009
|
+
return { ...graph, chunks }
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/** Refusals the bundler stopped on before it produced a graph, so without import chains. */
|
|
1013
|
+
function formatRefusedFiles(refused: ReadonlyMap<string, string>, root: string): string {
|
|
1014
|
+
const lines = [...refused].map(([file, reason]) => {
|
|
1015
|
+
const rel = relative(root, file).replaceAll("\\", "/")
|
|
1016
|
+
return ` - ${rel.startsWith("..") ? file : rel}: ${reason}`
|
|
1017
|
+
})
|
|
1018
|
+
return `[nifra/web] the browser build reached code that may not ship to a browser:\n${lines.join("\n")}`
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
export { zoneGuardPlugin } from "./plugins/zone-guard.ts"
|
|
795
1022
|
|
|
796
1023
|
/**
|
|
797
1024
|
* Server functions in the CLIENT build: replace each `*.fn.ts` module with stubs that call the routes
|
|
798
1025
|
* the server mounted, so the function bodies - and everything they import - never reach a browser.
|
|
799
1026
|
*
|
|
800
|
-
* The
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
* `internal/server-boundary.ts` so the Vite pipeline emits identical stubs from the same code; two
|
|
804
|
-
* hand-written copies would be a client that works in dev and 404s in production.
|
|
1027
|
+
* The generation itself is in `internal/server-boundary.ts` so the Vite pipeline emits identical stubs
|
|
1028
|
+
* from the same code; two hand-written copies would be a client that works in dev and 404s in
|
|
1029
|
+
* production.
|
|
805
1030
|
*
|
|
806
1031
|
* CLIENT-only. The server build keeps the real module, which is what `serverFunctions()` mounts.
|
|
807
1032
|
*/
|
|
@@ -847,7 +1072,7 @@ const solidWebServerPlugin = (from: string): BunPlugin => ({
|
|
|
847
1072
|
* silently ships a broken worker.
|
|
848
1073
|
*/
|
|
849
1074
|
export async function buildServer(options: BuildServerOptions): Promise<ServerBuild> {
|
|
850
|
-
const { routesDir, serverEntry, outDir, clientEntry, styles, routeStyles } = options
|
|
1075
|
+
const { routesDir, serverEntry, outDir, clientEntry, styles, routeStyles, cssLoading } = options
|
|
851
1076
|
const entryDir = dirname(serverEntry)
|
|
852
1077
|
const manifestFile = options.manifestFile ?? "server-manifest.ts"
|
|
853
1078
|
// Default: import routes relative from the generated manifest (next to serverEntry) to routesDir.
|
|
@@ -866,13 +1091,21 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
866
1091
|
const manifest = discoverRoutes(routesDir)
|
|
867
1092
|
writeFileSync(
|
|
868
1093
|
`${entryDir}/${manifestFile}`,
|
|
869
|
-
generateServerManifest(manifest, {
|
|
1094
|
+
generateServerManifest(manifest, {
|
|
1095
|
+
resolve,
|
|
1096
|
+
clientEntry,
|
|
1097
|
+
styles,
|
|
1098
|
+
routeStyles,
|
|
1099
|
+
...(cssLoading !== undefined ? { cssLoading } : {}),
|
|
1100
|
+
lazy,
|
|
1101
|
+
}),
|
|
870
1102
|
)
|
|
871
1103
|
|
|
872
1104
|
const result = await Bun.build({
|
|
873
1105
|
entrypoints: [serverEntry],
|
|
874
1106
|
outdir: outDir,
|
|
875
1107
|
target,
|
|
1108
|
+
metafile: true,
|
|
876
1109
|
conditions: [...conditions],
|
|
877
1110
|
define: {
|
|
878
1111
|
...(options.define ?? { "process.env.NODE_ENV": '"production"' }),
|
|
@@ -902,6 +1135,48 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
902
1135
|
`[nifra/web] server build failed:\n${result.logs.map((l) => String(l)).join("\n")}`,
|
|
903
1136
|
)
|
|
904
1137
|
}
|
|
1138
|
+
// Named as the metafile names outputs: relative to `outdir`.
|
|
1139
|
+
const nameOf = (path: string): string => relative(outDir, path).replaceAll("\\", "/")
|
|
1140
|
+
const emitted = await Promise.all(
|
|
1141
|
+
result.outputs.map(
|
|
1142
|
+
async (out): Promise<EmittedFile> =>
|
|
1143
|
+
out.kind === "sourcemap" || out.kind === "asset"
|
|
1144
|
+
? { name: nameOf(out.path), kind: out.kind === "sourcemap" ? "map" : "asset" }
|
|
1145
|
+
: { name: nameOf(out.path), kind: "code", text: await out.text() },
|
|
1146
|
+
),
|
|
1147
|
+
)
|
|
1148
|
+
const serverGraph = withEmittedImports(
|
|
1149
|
+
fromBunMetafile((result as unknown as { metafile?: BunMetafile }).metafile),
|
|
1150
|
+
emitted,
|
|
1151
|
+
)
|
|
1152
|
+
const appRoot = resolvePath(options.root ?? dirname(routesDir))
|
|
1153
|
+
const classifier = createZoneClassifier({
|
|
1154
|
+
appRoot,
|
|
1155
|
+
routesDir,
|
|
1156
|
+
generatedFiles: [
|
|
1157
|
+
serverEntry,
|
|
1158
|
+
join(entryDir, manifestFile),
|
|
1159
|
+
...(options.generatedFiles ?? []),
|
|
1160
|
+
].map((file) => resolvePath(file)),
|
|
1161
|
+
})
|
|
1162
|
+
const sourceOf = bunModuleSource(process.cwd())
|
|
1163
|
+
const labelOf = (id: string): string => {
|
|
1164
|
+
const source = sourceOf(id)
|
|
1165
|
+
return source.kind === "file" ? relative(appRoot, source.file).replaceAll("\\", "/") : id
|
|
1166
|
+
}
|
|
1167
|
+
const refusal =
|
|
1168
|
+
formatServerGraphVerdict(
|
|
1169
|
+
verifyServerGraph(serverGraph, {
|
|
1170
|
+
classifier,
|
|
1171
|
+
sourceOf,
|
|
1172
|
+
privateEnv: privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_"),
|
|
1173
|
+
}),
|
|
1174
|
+
) ?? formatUnsupportedBuiltins(unsupportedBuiltins(serverGraph, target, labelOf), target)
|
|
1175
|
+
if (refusal !== undefined) {
|
|
1176
|
+
// Bun wrote the bundle already; a refused build leaves nothing a deploy step could pick up.
|
|
1177
|
+
for (const output of result.outputs) rmSync(output.path, { force: true })
|
|
1178
|
+
throw new Error(refusal)
|
|
1179
|
+
}
|
|
905
1180
|
const entryOutput = result.outputs.find((o) => o.kind === "entry-point")
|
|
906
1181
|
if (entryOutput === undefined) {
|
|
907
1182
|
throw new Error("[nifra/web] server build produced no entry-point output")
|
|
@@ -918,12 +1193,37 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
918
1193
|
// …) - so we GENERATE it here (per target) instead of asking each app to ship five near-identical files.
|
|
919
1194
|
// ===================================================================================================
|
|
920
1195
|
|
|
1196
|
+
/** The `createWebApp` options a generated server entry can import from the app's framework module. */
|
|
1197
|
+
export const SERVER_ENTRY_OPTIONS = ["apiPrefix", "apiStrip", "mounts", "csp", "nonce"] as const
|
|
1198
|
+
export type ServerEntryOption = (typeof SERVER_ENTRY_OPTIONS)[number]
|
|
1199
|
+
/** Each importable option mapped to the specifier of the module that exports it. */
|
|
1200
|
+
export type ServerEntryOptionImports = Readonly<Partial<Record<ServerEntryOption, string>>>
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Where `c.clientIp` comes from in a generated server entry. Left out (the default), an edge target has
|
|
1204
|
+
* no caller address at all and a self-hosting target uses the socket peer. `"platform"` also trusts the
|
|
1205
|
+
* header an edge target's platform overwrites at its edge (see {@link PLATFORM_CLIENT_IP_HEADERS}); it
|
|
1206
|
+
* changes nothing on a self-hosting target, whose socket peer is already the platform's answer.
|
|
1207
|
+
*/
|
|
1208
|
+
export type ServerEntryClientIp = "platform"
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* The header each edge platform sets to the caller's address, replacing any value the client sent.
|
|
1212
|
+
* That replacement is the whole trust: the same bundle served without the platform's edge in front
|
|
1213
|
+
* (`wrangler pages dev`, a self-hosted workerd) hands the header straight from the client, which is
|
|
1214
|
+
* why it is only believed on an explicit `clientIp: "platform"`.
|
|
1215
|
+
*/
|
|
1216
|
+
const PLATFORM_CLIENT_IP_HEADERS: Readonly<Partial<Record<BuildTarget, string>>> = {
|
|
1217
|
+
cloudflare: "cf-connecting-ip",
|
|
1218
|
+
vercel: "x-real-ip",
|
|
1219
|
+
}
|
|
1220
|
+
|
|
921
1221
|
/**
|
|
922
1222
|
* Codegen the per-target **server entry** module (source text) for `buildServer` to bundle. It imports
|
|
923
1223
|
* the app's `adapter` (from `framework.ts`), the optional `backend` (from `backend.ts`), and the
|
|
924
1224
|
* generated `{ manifest, clientEntry }` (from `./server-manifest`), builds `createWebApp`, then wires
|
|
925
1225
|
* the right host:
|
|
926
|
-
* - `
|
|
1226
|
+
* - `cloudflare` / `vercel`: `export default` the fetch handler (the platform serves /assets/* itself).
|
|
927
1227
|
* - `deno`: same fetch-handler default, plus `Deno.serve` self-host when run directly.
|
|
928
1228
|
* - `bun` / `node`: a self-hosting server that ALSO serves the client bundle from disk (those
|
|
929
1229
|
* runtimes have a filesystem; the static `/assets/*` sit next to the entry).
|
|
@@ -940,19 +1240,42 @@ export function generateServerEntry(options: {
|
|
|
940
1240
|
/** Import specifier for the module exporting `use`, or `undefined`. Must resolve to the
|
|
941
1241
|
* same edge-safe module as `adapterImport`. */
|
|
942
1242
|
readonly useImport?: string
|
|
1243
|
+
/** `createWebApp` options to import by name, each mapped to the specifier of the edge-safe module
|
|
1244
|
+
* that exports it. */
|
|
1245
|
+
readonly optionImports?: ServerEntryOptionImports
|
|
943
1246
|
/** Document `<title>` passed to `createWebApp`. */
|
|
944
1247
|
readonly title?: string
|
|
945
1248
|
/** Encoded root-relative public file paths copied into the deploy directory. */
|
|
946
1249
|
readonly publicFiles?: readonly string[]
|
|
1250
|
+
/** Trust the edge platform's client-address header for `c.clientIp`. See {@link ServerEntryClientIp}. */
|
|
1251
|
+
readonly clientIp?: ServerEntryClientIp
|
|
947
1252
|
}): string {
|
|
948
1253
|
const {
|
|
949
1254
|
target,
|
|
950
1255
|
adapterImport,
|
|
951
1256
|
backendImport,
|
|
952
1257
|
useImport,
|
|
1258
|
+
optionImports = {},
|
|
953
1259
|
title = "nifra",
|
|
954
1260
|
publicFiles = [],
|
|
1261
|
+
clientIp,
|
|
955
1262
|
} = options
|
|
1263
|
+
if (clientIp !== undefined && clientIp !== "platform") {
|
|
1264
|
+
throw new Error(
|
|
1265
|
+
`[nifra/web] generateServerEntry: clientIp must be "platform" or left out, got ${JSON.stringify(clientIp)}`,
|
|
1266
|
+
)
|
|
1267
|
+
}
|
|
1268
|
+
const clientIpHeader = clientIp === "platform" ? PLATFORM_CLIENT_IP_HEADERS[target] : undefined
|
|
1269
|
+
// Identifiers come only from the fixed list, never from the caller's keys, so generated code names
|
|
1270
|
+
// nothing the list does not.
|
|
1271
|
+
const importedOptions = SERVER_ENTRY_OPTIONS.filter((name) => optionImports[name] !== undefined)
|
|
1272
|
+
const optionModules = new Map<string, string[]>()
|
|
1273
|
+
for (const name of importedOptions) {
|
|
1274
|
+
const from = optionImports[name] as string
|
|
1275
|
+
const names = optionModules.get(from)
|
|
1276
|
+
if (names === undefined) optionModules.set(from, [name])
|
|
1277
|
+
else names.push(name)
|
|
1278
|
+
}
|
|
956
1279
|
if (target === "static") {
|
|
957
1280
|
throw new Error("[nifra/web] generateServerEntry: `static` has no server entry (SSG only)")
|
|
958
1281
|
}
|
|
@@ -960,30 +1283,40 @@ export function generateServerEntry(options: {
|
|
|
960
1283
|
if (backendImport !== undefined) lines.push('import { inProcessClient } from "@nifrajs/client"')
|
|
961
1284
|
lines.push(`import { adapter } from ${JSON.stringify(adapterImport)}`)
|
|
962
1285
|
if (useImport !== undefined) lines.push(`import { use } from ${JSON.stringify(useImport)}`)
|
|
1286
|
+
for (const [from, names] of optionModules) {
|
|
1287
|
+
lines.push(`import { ${names.join(", ")} } from ${JSON.stringify(from)}`)
|
|
1288
|
+
}
|
|
963
1289
|
if (backendImport !== undefined) {
|
|
964
1290
|
lines.push(`import { backend } from ${JSON.stringify(backendImport)}`)
|
|
965
1291
|
}
|
|
966
|
-
lines.push(
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1292
|
+
lines.push(
|
|
1293
|
+
'import { clientEntry, cssLoading, manifest, styles, routeStyles } from "./server-manifest"',
|
|
1294
|
+
)
|
|
1295
|
+
// Cloudflare needs the fetch-handler shape; the others call app.fetch directly.
|
|
1296
|
+
if (target === "cloudflare") lines.push('import { toFetchHandler } from "@nifrajs/core/server"')
|
|
970
1297
|
if (target === "node") lines.push('import { serve } from "@nifrajs/node"')
|
|
971
1298
|
lines.push(
|
|
972
1299
|
"",
|
|
973
1300
|
"const app = createWebApp({",
|
|
974
1301
|
" adapter,",
|
|
975
1302
|
...(useImport !== undefined ? [" use,"] : []),
|
|
1303
|
+
...importedOptions.map((name) => ` ${name},`),
|
|
976
1304
|
" manifest,",
|
|
977
1305
|
" clientEntry,",
|
|
978
1306
|
" styles,",
|
|
979
1307
|
" routeStyles,",
|
|
1308
|
+
" cssLoading,",
|
|
980
1309
|
...(backendImport !== undefined ? [" api: inProcessClient(backend),"] : []),
|
|
981
1310
|
` title: ${JSON.stringify(title)},`,
|
|
1311
|
+
// Core's own `{ header }` trust, so the mounted backend and every loader see the derived caller.
|
|
1312
|
+
...(clientIpHeader !== undefined
|
|
1313
|
+
? [` server: { clientIp: { header: ${JSON.stringify(clientIpHeader)} } },`]
|
|
1314
|
+
: []),
|
|
982
1315
|
"})",
|
|
983
1316
|
"",
|
|
984
1317
|
)
|
|
985
1318
|
|
|
986
|
-
if (target === "
|
|
1319
|
+
if (target === "cloudflare") {
|
|
987
1320
|
// Cloudflare Pages advanced mode: `_routes.json` keeps static paths off the worker entirely, and
|
|
988
1321
|
// everything else falls through to this handler (SSR).
|
|
989
1322
|
//
|
|
@@ -1033,22 +1366,34 @@ export function generateServerEntry(options: {
|
|
|
1033
1366
|
' segment !== "." && segment !== ".." && /^[A-Za-z0-9._-]+$/.test(segment)',
|
|
1034
1367
|
" return segments.every(safe) ? '.' + pathname : undefined",
|
|
1035
1368
|
"}",
|
|
1036
|
-
|
|
1369
|
+
// The type table and cache policy `servePublicDir` uses. Without a known type, Bun answers a file
|
|
1370
|
+
// as an `application/octet-stream` attachment, which a browser downloads instead of showing.
|
|
1371
|
+
`const TYPES: Record<string, string> = ${JSON.stringify(CONTENT_TYPES)}`,
|
|
1372
|
+
"const staticHeaders = (pathname: string): Record<string, string> => ({",
|
|
1373
|
+
' "content-type": TYPES[pathname.slice(pathname.lastIndexOf(".")).toLowerCase()] ?? "application/octet-stream",',
|
|
1374
|
+
` "cache-control": pathname.startsWith("/assets/") ? ${JSON.stringify(IMMUTABLE)} : ${JSON.stringify(ONE_DAY)},`,
|
|
1375
|
+
' "x-content-type-options": "nosniff",',
|
|
1376
|
+
"})",
|
|
1037
1377
|
)
|
|
1378
|
+
// Bun.serve and Deno.serve delimit the body themselves, so its declared length is the transport
|
|
1379
|
+
// frame - the guarantee core's own listen() and the Deno adapter mark (see markTrustedBodyFraming).
|
|
1380
|
+
const markFramed =
|
|
1381
|
+
'(req as unknown as Record<symbol, unknown>)[Symbol.for("nifra.body.trustedFraming")] = true'
|
|
1038
1382
|
if (target === "bun") {
|
|
1039
1383
|
lines.push(
|
|
1040
1384
|
"const server = Bun.serve({",
|
|
1041
1385
|
" port: Number(Bun.env.PORT ?? 3000),",
|
|
1042
|
-
" async fetch(req) {",
|
|
1386
|
+
" async fetch(req, server) {",
|
|
1043
1387
|
" const { pathname } = new URL(req.url)",
|
|
1044
1388
|
" const filePath = staticPath(pathname)",
|
|
1045
1389
|
" if (filePath !== undefined) {",
|
|
1046
1390
|
" const file = Bun.file(new URL(filePath, STATIC_ROOT))",
|
|
1047
1391
|
' if (!(await file.exists())) return new Response("not found", { status: 404 })',
|
|
1048
|
-
|
|
1049
|
-
' return new Response(file, { headers: { "content-type": TYPES[ext] ?? "application/octet-stream" } })',
|
|
1392
|
+
" return new Response(file, { headers: staticHeaders(pathname) })",
|
|
1050
1393
|
" }",
|
|
1051
|
-
|
|
1394
|
+
` ${markFramed}`,
|
|
1395
|
+
" // The socket peer, looked up only when something reads it (as core's own listen() does).",
|
|
1396
|
+
" return app.fetch(req, { get clientIp() { return server.requestIP(req)?.address } })",
|
|
1052
1397
|
" },",
|
|
1053
1398
|
"})",
|
|
1054
1399
|
// The `${...}` here is literal OUTPUT (a template in the GENERATED file), not a template in this
|
|
@@ -1062,19 +1407,18 @@ export function generateServerEntry(options: {
|
|
|
1062
1407
|
'import { readFile } from "node:fs/promises"',
|
|
1063
1408
|
"await serve(",
|
|
1064
1409
|
" {",
|
|
1065
|
-
" async fetch(req) {",
|
|
1410
|
+
" async fetch(req, platform) {",
|
|
1066
1411
|
" const { pathname } = new URL(req.url)",
|
|
1067
1412
|
" const filePath = staticPath(pathname)",
|
|
1068
1413
|
" if (filePath !== undefined) {",
|
|
1069
1414
|
" try {",
|
|
1070
1415
|
" const body = await readFile(new URL(filePath, STATIC_ROOT))",
|
|
1071
|
-
|
|
1072
|
-
' return new Response(body, { headers: { "content-type": TYPES[ext] ?? "application/octet-stream" } })',
|
|
1416
|
+
" return new Response(body, { headers: staticHeaders(pathname) })",
|
|
1073
1417
|
" } catch {",
|
|
1074
1418
|
' return new Response("not found", { status: 404 })',
|
|
1075
1419
|
" }",
|
|
1076
1420
|
" }",
|
|
1077
|
-
" return app.fetch(req)",
|
|
1421
|
+
" return app.fetch(req, platform)",
|
|
1078
1422
|
" },",
|
|
1079
1423
|
" },",
|
|
1080
1424
|
" { port: Number(process.env.PORT ?? 3000) },",
|
|
@@ -1084,22 +1428,21 @@ export function generateServerEntry(options: {
|
|
|
1084
1428
|
}
|
|
1085
1429
|
// deno
|
|
1086
1430
|
lines.push(
|
|
1087
|
-
"const handler = toFetchHandler(app)",
|
|
1088
1431
|
"// @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1089
|
-
'Deno.serve({ port: Number(Deno.env.get("PORT") ?? "3000") }, async (req) => {',
|
|
1432
|
+
'Deno.serve({ port: Number(Deno.env.get("PORT") ?? "3000") }, async (req, info) => {',
|
|
1090
1433
|
" const { pathname } = new URL(req.url)",
|
|
1091
1434
|
" const filePath = staticPath(pathname)",
|
|
1092
1435
|
" if (filePath !== undefined) {",
|
|
1093
1436
|
" try {",
|
|
1094
1437
|
" // @ts-ignore - Deno.readFile is present on the Deno runtime.",
|
|
1095
1438
|
" const body = await Deno.readFile(new URL(filePath, STATIC_ROOT))",
|
|
1096
|
-
|
|
1097
|
-
' return new Response(body, { headers: { "content-type": TYPES[ext] ?? "application/octet-stream" } })',
|
|
1439
|
+
" return new Response(body, { headers: staticHeaders(pathname) })",
|
|
1098
1440
|
" } catch {",
|
|
1099
1441
|
' return new Response("not found", { status: 404 })',
|
|
1100
1442
|
" }",
|
|
1101
1443
|
" }",
|
|
1102
|
-
|
|
1444
|
+
` ${markFramed}`,
|
|
1445
|
+
" return app.fetch(req, { clientIp: info.remoteAddr.hostname })",
|
|
1103
1446
|
"})",
|
|
1104
1447
|
)
|
|
1105
1448
|
return `${lines.join("\n")}\n`
|
|
@@ -1139,6 +1482,9 @@ export interface BuildTargetOptions {
|
|
|
1139
1482
|
* applied before page routes are declared), or `undefined`. Must resolve to the same edge-safe
|
|
1140
1483
|
* module as `adapterImport`. */
|
|
1141
1484
|
readonly useImport?: string
|
|
1485
|
+
/** `createWebApp` options the server entry imports by name (`apiPrefix`, `mounts`, `csp`, ...), each
|
|
1486
|
+
* mapped to the specifier (resolvable from `workDir`) of the same edge-safe module as `adapterImport`. */
|
|
1487
|
+
readonly optionImports?: ServerEntryOptionImports
|
|
1142
1488
|
/** Factory that builds the app for `static` prerendering, GIVEN the client build's manifest - so the
|
|
1143
1489
|
* emitted hydration `<script src>` uses the REAL content-hashed entry (`client.entry`) plus the same
|
|
1144
1490
|
* styles/route-preload the server targets use. A pre-built instance can't work here: the hash isn't known
|
|
@@ -1157,8 +1503,25 @@ export interface BuildTargetOptions {
|
|
|
1157
1503
|
readonly publicDir?: string | false
|
|
1158
1504
|
/** Prefix of environment variables allowed into the client bundle (default `"PUBLIC_"`). */
|
|
1159
1505
|
readonly publicEnvPrefix?: string
|
|
1506
|
+
/** Reviewed false positives of the secret scan over the bundle, `public/` and prerendered pages. */
|
|
1507
|
+
readonly secretExemptions?: readonly SecretExemption[]
|
|
1508
|
+
/**
|
|
1509
|
+
* Vite-only CSS output policy. `false` emits one aggregate stylesheet. The native Bun build does not
|
|
1510
|
+
* support this switch and fails closed if it is supplied instead of silently ignoring it.
|
|
1511
|
+
*/
|
|
1512
|
+
readonly cssCodeSplit?: boolean
|
|
1513
|
+
/**
|
|
1514
|
+
* Activation policy for framework-owned SSR stylesheet links. Defaults to `"blocking"`; `"deferred"`
|
|
1515
|
+
* fetches with `media="print"` and promotes before hydration. With Vite, pair it with
|
|
1516
|
+
* `cssCodeSplit: false`; the native Bun build supports the activation policy but keeps its existing
|
|
1517
|
+
* CSS output strategy.
|
|
1518
|
+
*/
|
|
1519
|
+
readonly cssLoading?: CssLoadingMode
|
|
1160
1520
|
/** Document `<title>` for the generated server entry. */
|
|
1161
1521
|
readonly title?: string
|
|
1522
|
+
/** Trust the edge platform's client-address header for `c.clientIp` (`cloudflare`, `vercel`). Off by
|
|
1523
|
+
* default, leaving an edge app with no caller address. See {@link ServerEntryClientIp}. */
|
|
1524
|
+
readonly clientIp?: ServerEntryClientIp
|
|
1162
1525
|
}
|
|
1163
1526
|
|
|
1164
1527
|
/** Minimal app surface `buildTarget`'s static path needs - a fetch handler (a built `createWebApp`). */
|
|
@@ -1186,29 +1549,36 @@ export interface BuildTargetResult {
|
|
|
1186
1549
|
* `<outDir>/assets/*`, then per target:
|
|
1187
1550
|
* - `static`: prerenders opted-in routes (`prerenderRoutes`) to `<outDir>/<path>/index.html` (+
|
|
1188
1551
|
* `_data.json`); needs `prerenderApp`. No server.
|
|
1189
|
-
* - `
|
|
1190
|
-
* - `vercel`:
|
|
1191
|
-
*
|
|
1552
|
+
* - `cloudflare`: a `_worker.js` (edge bundle) + a `_routes.json` excluding /assets/* from the worker.
|
|
1553
|
+
* - `vercel`: Vercel's Build Output API v3 - `config.json`, the client bundle and public files under
|
|
1554
|
+
* `static/`, and the edge function at `functions/index.func/index.js`; `vercel deploy --prebuilt`
|
|
1555
|
+
* uploads it as it is when `outDir` is `.vercel/output`.
|
|
1192
1556
|
* - `deno`/`node`/`bun`: the self-hosting server bundle (`server.js`) next to the assets.
|
|
1193
1557
|
* The server entry is GENERATED (`generateServerEntry`) and bundled (`buildServer`); the app supplies
|
|
1194
1558
|
* only adapter/backend/routes. Returns the manifest + a size report. Throws on any build failure.
|
|
1195
|
-
*
|
|
1196
|
-
* Note: the heavier platform wrappers (`.vercel/output` v3 layout, wrangler ISR `find_additional_modules`)
|
|
1197
|
-
* remain app-owned scripts; this command targets the common single-bundle deploys. See the CLI docs.
|
|
1198
1559
|
*/
|
|
1199
1560
|
/** The default (Bun) strategy - `buildClient`/`buildServer` from this module. */
|
|
1200
1561
|
export const bunBundler: Bundler = {
|
|
1201
|
-
buildClient: (input) =>
|
|
1202
|
-
|
|
1562
|
+
buildClient: (input) => {
|
|
1563
|
+
const cssCodeSplit = normalizeCssCodeSplit(input.cssCodeSplit)
|
|
1564
|
+
if (cssCodeSplit === false) {
|
|
1565
|
+
throw new Error(
|
|
1566
|
+
"[nifra/web] cssCodeSplit: false is a Vite-only production option. Use buildTargetVite or remove it for the native Bun build.",
|
|
1567
|
+
)
|
|
1568
|
+
}
|
|
1569
|
+
return buildClient({
|
|
1203
1570
|
routesDir: input.routesDir,
|
|
1204
1571
|
outDir: input.outDir,
|
|
1205
1572
|
clientModule: input.clientModule,
|
|
1206
1573
|
...(input.plugins ? { plugins: input.plugins as BunPlugin[] } : {}),
|
|
1207
1574
|
...(input.conditions ? { conditions: input.conditions } : {}),
|
|
1208
1575
|
...(input.define ? { define: input.define } : {}),
|
|
1576
|
+
...(input.cssCodeSplit !== undefined ? { cssCodeSplit } : {}),
|
|
1209
1577
|
...(input.publicDir !== undefined ? { publicDir: input.publicDir } : {}),
|
|
1210
1578
|
...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
|
|
1211
|
-
|
|
1579
|
+
...(input.secretExemptions !== undefined ? { secretExemptions: input.secretExemptions } : {}),
|
|
1580
|
+
})
|
|
1581
|
+
},
|
|
1212
1582
|
buildServer: (input) =>
|
|
1213
1583
|
buildServer({
|
|
1214
1584
|
routesDir: input.routesDir,
|
|
@@ -1216,8 +1586,14 @@ export const bunBundler: Bundler = {
|
|
|
1216
1586
|
outDir: input.outDir,
|
|
1217
1587
|
clientEntry: input.clientEntry,
|
|
1218
1588
|
target: input.target,
|
|
1589
|
+
...(input.styles !== undefined ? { styles: input.styles } : {}),
|
|
1590
|
+
...(input.routeStyles !== undefined ? { routeStyles: input.routeStyles } : {}),
|
|
1219
1591
|
...(input.plugins ? { plugins: input.plugins as BunPlugin[] } : {}),
|
|
1220
1592
|
...(input.define ? { define: input.define } : {}),
|
|
1593
|
+
...(input.cssLoading !== undefined ? { cssLoading: input.cssLoading } : {}),
|
|
1594
|
+
...(input.root !== undefined ? { root: input.root } : {}),
|
|
1595
|
+
...(input.generatedFiles !== undefined ? { generatedFiles: input.generatedFiles } : {}),
|
|
1596
|
+
...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
|
|
1221
1597
|
}),
|
|
1222
1598
|
}
|
|
1223
1599
|
|
|
@@ -1241,15 +1617,20 @@ export async function buildTargetWith(
|
|
|
1241
1617
|
bundler: Bundler,
|
|
1242
1618
|
): Promise<BuildTargetResult> {
|
|
1243
1619
|
const { routesDir, outDir, workDir } = options
|
|
1244
|
-
const
|
|
1620
|
+
const requestedCssCodeSplit =
|
|
1621
|
+
options.cssCodeSplit === undefined ? undefined : normalizeCssCodeSplit(options.cssCodeSplit)
|
|
1622
|
+
const requestedCssLoading =
|
|
1623
|
+
options.cssLoading === undefined ? undefined : normalizeCssLoading(options.cssLoading)
|
|
1624
|
+
const targetPlan = planBuildTarget(parseBuildTarget(target), outDir)
|
|
1245
1625
|
const { rmSync } = await import("node:fs")
|
|
1246
1626
|
rmSync(outDir, { recursive: true, force: true })
|
|
1247
1627
|
rmSync(workDir, { recursive: true, force: true })
|
|
1248
|
-
const
|
|
1628
|
+
const staticRoot = targetPlan.staticDir === "" ? outDir : `${outDir}/${targetPlan.staticDir}`
|
|
1629
|
+
const assetsDir = `${staticRoot}/assets`
|
|
1249
1630
|
mkdirSync(assetsDir, { recursive: true })
|
|
1250
1631
|
mkdirSync(workDir, { recursive: true })
|
|
1251
1632
|
|
|
1252
|
-
// (1) Client bundle → <
|
|
1633
|
+
// (1) Client bundle → <static root>/assets/* (every target ships the same hashed client bundle).
|
|
1253
1634
|
let client = await bundler.buildClient({
|
|
1254
1635
|
routesDir,
|
|
1255
1636
|
outDir: assetsDir,
|
|
@@ -1259,16 +1640,45 @@ export async function buildTargetWith(
|
|
|
1259
1640
|
define: { "process.env.NODE_ENV": '"production"', ...(options.define ?? {}) },
|
|
1260
1641
|
publicDir: false,
|
|
1261
1642
|
...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
|
|
1643
|
+
...(options.secretExemptions !== undefined
|
|
1644
|
+
? { secretExemptions: options.secretExemptions }
|
|
1645
|
+
: {}),
|
|
1646
|
+
...(requestedCssCodeSplit !== undefined ? { cssCodeSplit: requestedCssCodeSplit } : {}),
|
|
1647
|
+
...(requestedCssLoading !== undefined ? { cssLoading: requestedCssLoading } : {}),
|
|
1262
1648
|
root: resolvePath(dirname(routesDir)),
|
|
1263
1649
|
})
|
|
1650
|
+
// Bun's client builder has no CSS-loading field, but the server-side document policy is bundler
|
|
1651
|
+
// independent. Carry the requested policy into the result so static prerender callbacks and the
|
|
1652
|
+
// generated server entry see the same choice on both build strategies.
|
|
1653
|
+
if (requestedCssLoading !== undefined && client.cssLoading !== requestedCssLoading) {
|
|
1654
|
+
client = { ...client, cssLoading: requestedCssLoading }
|
|
1655
|
+
}
|
|
1264
1656
|
const publicDir =
|
|
1265
1657
|
options.publicDir === false
|
|
1266
1658
|
? undefined
|
|
1267
1659
|
: resolvePath(options.publicDir ?? join(dirname(routesDir), "public"))
|
|
1660
|
+
const secretScan = {
|
|
1661
|
+
env: buildEnvironment(),
|
|
1662
|
+
publicEnvPrefix: options.publicEnvPrefix ?? "PUBLIC_",
|
|
1663
|
+
...(options.secretExemptions !== undefined ? { exemptions: options.secretExemptions } : {}),
|
|
1664
|
+
}
|
|
1665
|
+
if (publicDir !== undefined && existsSync(publicDir)) {
|
|
1666
|
+
assertNoSecrets({
|
|
1667
|
+
...secretScan,
|
|
1668
|
+
artifacts: publicScanFiles(publicDir, originName(dirname(routesDir), publicDir)),
|
|
1669
|
+
})
|
|
1670
|
+
}
|
|
1268
1671
|
const publicFiles =
|
|
1269
|
-
publicDir !== undefined && existsSync(publicDir)
|
|
1672
|
+
publicDir !== undefined && existsSync(publicDir)
|
|
1673
|
+
? await copyPublicDir(publicDir, staticRoot)
|
|
1674
|
+
: []
|
|
1270
1675
|
if (publicFiles.length > 0) {
|
|
1271
1676
|
client = { ...client, publicFiles }
|
|
1677
|
+
}
|
|
1678
|
+
// `buildClient` writes its first manifest before the target orchestration knows the activation policy
|
|
1679
|
+
// (the native Bun client builder has no CSS-loading option). Persist the target-level additions even
|
|
1680
|
+
// when the app has no public/ files; otherwise the returned manifest and the deploy artifact disagree.
|
|
1681
|
+
if (requestedCssLoading !== undefined || publicFiles.length > 0) {
|
|
1272
1682
|
writeFileSync(`${assetsDir}/manifest.json`, JSON.stringify(client, null, 2))
|
|
1273
1683
|
}
|
|
1274
1684
|
|
|
@@ -1287,6 +1697,7 @@ export async function buildTargetWith(
|
|
|
1287
1697
|
app,
|
|
1288
1698
|
routes: manifest.routes,
|
|
1289
1699
|
outDir,
|
|
1700
|
+
secrets: secretScan,
|
|
1290
1701
|
})
|
|
1291
1702
|
if (result.prerendered.length === 0) {
|
|
1292
1703
|
// A static build that renders nothing is almost always a misconfig (no `prerender = true` / no
|
|
@@ -1319,9 +1730,11 @@ export async function buildTargetWith(
|
|
|
1319
1730
|
target,
|
|
1320
1731
|
adapterImport: options.adapterImport,
|
|
1321
1732
|
...(options.useImport !== undefined ? { useImport: options.useImport } : {}),
|
|
1733
|
+
...(options.optionImports !== undefined ? { optionImports: options.optionImports } : {}),
|
|
1322
1734
|
...(options.backendImport !== undefined ? { backendImport: options.backendImport } : {}),
|
|
1323
1735
|
...(options.title !== undefined ? { title: options.title } : {}),
|
|
1324
1736
|
...(publicFiles.length > 0 ? { publicFiles } : {}),
|
|
1737
|
+
...(options.clientIp !== undefined ? { clientIp: options.clientIp } : {}),
|
|
1325
1738
|
}),
|
|
1326
1739
|
)
|
|
1327
1740
|
const { worker } = await bundler.buildServer({
|
|
@@ -1330,14 +1743,23 @@ export async function buildTargetWith(
|
|
|
1330
1743
|
outDir: `${workDir}/server`,
|
|
1331
1744
|
clientEntry: client.entry,
|
|
1332
1745
|
target: targetPlan.serverTarget,
|
|
1746
|
+
...(client.css !== undefined ? { styles: client.css } : {}),
|
|
1747
|
+
...(client.routeStyles !== undefined ? { routeStyles: client.routeStyles } : {}),
|
|
1333
1748
|
...(options.serverPlugins ? { plugins: options.serverPlugins } : {}),
|
|
1334
1749
|
define: { "process.env.NODE_ENV": '"production"', ...(options.define ?? {}) },
|
|
1750
|
+
...(client.cssLoading !== undefined
|
|
1751
|
+
? { cssLoading: client.cssLoading }
|
|
1752
|
+
: requestedCssLoading !== undefined
|
|
1753
|
+
? { cssLoading: requestedCssLoading }
|
|
1754
|
+
: {}),
|
|
1335
1755
|
root: resolvePath(dirname(routesDir)),
|
|
1756
|
+
generatedFiles: [resolvePath(workDir, options.adapterImport)],
|
|
1757
|
+
...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
|
|
1336
1758
|
})
|
|
1337
1759
|
|
|
1338
1760
|
// (3) Assemble the deploy dir for the target.
|
|
1339
1761
|
const { cpSync } = await import("node:fs")
|
|
1340
|
-
if (targetPlan.target === "
|
|
1762
|
+
if (targetPlan.target === "cloudflare") {
|
|
1341
1763
|
cpSync(worker, `${outDir}/${targetPlan.outputFile}`)
|
|
1342
1764
|
// The app's real patterns, so a directory is only collapsed into a glob once the route table
|
|
1343
1765
|
// proves nothing can be served beneath it.
|
|
@@ -1359,7 +1781,18 @@ export async function buildTargetWith(
|
|
|
1359
1781
|
)
|
|
1360
1782
|
}
|
|
1361
1783
|
} else if (targetPlan.target === "vercel") {
|
|
1784
|
+
const fn = dirname(`${outDir}/${targetPlan.outputFile}`)
|
|
1785
|
+
mkdirSync(fn, { recursive: true })
|
|
1362
1786
|
cpSync(worker, `${outDir}/${targetPlan.outputFile}`)
|
|
1787
|
+
writeFileSync(
|
|
1788
|
+
`${fn}/.vc-config.json`,
|
|
1789
|
+
`${JSON.stringify({ runtime: "edge", entrypoint: "index.js" }, null, 2)}\n`,
|
|
1790
|
+
)
|
|
1791
|
+
// Files the build wrote are served first; every other path is the SSR function's.
|
|
1792
|
+
writeFileSync(
|
|
1793
|
+
`${outDir}/config.json`,
|
|
1794
|
+
`${JSON.stringify({ version: 3, routes: [{ handle: "filesystem" }, { src: "/(.*)", dest: "/index" }] }, null, 2)}\n`,
|
|
1795
|
+
)
|
|
1363
1796
|
} else {
|
|
1364
1797
|
cpSync(worker, `${outDir}/${targetPlan.outputFile}`)
|
|
1365
1798
|
}
|