@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/dist/build.js
CHANGED
|
@@ -5,21 +5,30 @@
|
|
|
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
|
-
import { dirname, join, basename as pathBasename, relative, resolve as resolvePath, sep, } from "node:path";
|
|
10
|
+
import { dirname, join, basename as pathBasename, posix, relative, resolve as resolvePath, sep, } from "node:path";
|
|
11
11
|
import { singleCopyPlugin as declaredSingleCopyPlugin, readSingleCopyDeclaration, } from "@nifrajs/core/single-copy";
|
|
12
12
|
import { Glob } from "bun";
|
|
13
|
-
import { aggregateSizeReport, detectNodeBuiltinsInClient, detectServerOnlyInClient, formatNodeBuiltinLeak, formatServerOnlyLeak, parseManifestClientEntry, parseManifestRouteStyles, parseManifestStyles, planBuildTarget, } from "./build-plan.js";
|
|
13
|
+
import { aggregateSizeReport, detectNodeBuiltinsInClient, detectServerOnlyInClient, formatNodeBuiltinLeak, formatServerOnlyLeak, parseBuildTarget, parseManifestClientEntry, parseManifestCssLoading, parseManifestRouteStyles, parseManifestStyles, planBuildTarget, } from "./build-plan.js";
|
|
14
14
|
import { sanitizeOutputNames } from "./chunk-names.js";
|
|
15
|
+
import { normalizeCssCodeSplit, normalizeCssLoading } from "./css-contract.js";
|
|
15
16
|
import { discoverRoutes } from "./fs.js";
|
|
16
17
|
import { generateClientEntry, generateServerManifest } from "./index.js";
|
|
17
18
|
import { dedupePolicyFor } from "./internal/identity-policy.js";
|
|
18
19
|
import { assertDevelopmentProductionParity, assertIdentityParity, collectDevelopmentParityInput, } from "./internal/parity.js";
|
|
19
|
-
import {
|
|
20
|
-
|
|
20
|
+
import { privateEnvCheck } from "./internal/private-env.js";
|
|
21
|
+
import { publicUrlPath } from "./internal/public-url.js";
|
|
22
|
+
import { assertNoSecrets, buildEnvironment, emittedScanFiles, formatSecretFindings, graphScanInput, originName, publicScanFiles, scanForSecrets, textOf, } from "./internal/secret-scan.js";
|
|
23
|
+
import { generateServerFnStub, SERVER_FN_MODULE, serverFnNamespace, } from "./internal/server-boundary.js";
|
|
24
|
+
import { formatUnsupportedBuiltins, unsupportedBuiltins } from "./internal/target-compat.js";
|
|
25
|
+
import { accountEmittedFiles, bunModuleSource, formatClientGraphVerdict, formatServerGraphVerdict, formatUnaccountedOutput, verifyClientGraph, verifyServerGraph, } from "./internal/zone-graph.js";
|
|
21
26
|
import { fromBunMetafile } from "./module-graph.js";
|
|
27
|
+
import { zoneGuardPlugin } from "./plugins/zone-guard.js";
|
|
28
|
+
// `buildTarget(static)` drives the SSG prerender engine directly (it's also re-exported below).
|
|
22
29
|
import { prerenderRoutes } from "./prerender.js";
|
|
30
|
+
import { CONTENT_TYPES, IMMUTABLE, ONE_DAY } from "./public-dir.js";
|
|
31
|
+
import { createZoneClassifier } from "./zones.js";
|
|
23
32
|
export * from "./build-plan.js";
|
|
24
33
|
const basename = (path) => pathBasename(path);
|
|
25
34
|
// The lazy shape declares `const loaders`, the eager shape `const modules`. Anchored at a line start,
|
|
@@ -54,18 +63,6 @@ export function publicEnvDefines(prefix, env) {
|
|
|
54
63
|
}
|
|
55
64
|
return defines;
|
|
56
65
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Percent-encode one path segment exactly the way a browser encodes it into a request URL.
|
|
59
|
-
*
|
|
60
|
-
* NOT `encodeURIComponent`. That escapes the sub-delimiters `, @ + = & ; $`, which a browser sends raw -
|
|
61
|
-
* so a file named `report,2026.csv` would be recorded as `/report%2C2026.csv` while the request arrives
|
|
62
|
-
* as `/report,2026.csv`, the allowlist lookup misses, and the file 404s in production only. `encodeURI`
|
|
63
|
-
* agrees with `URL.pathname` on every character except `?` and `#`, which terminate a path and so must
|
|
64
|
-
* be escaped explicitly here.
|
|
65
|
-
*/
|
|
66
|
-
function encodePathSegment(segment) {
|
|
67
|
-
return encodeURI(segment).replace(/[?#]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
68
|
-
}
|
|
69
66
|
/**
|
|
70
67
|
* Cloudflare Pages rejects a `_routes.json` with more than 100 include+exclude rules combined, or any
|
|
71
68
|
* single rule longer than 100 characters.
|
|
@@ -116,7 +113,7 @@ function collapsibleDirs(publicFiles, routePatterns) {
|
|
|
116
113
|
return dirs;
|
|
117
114
|
}
|
|
118
115
|
/**
|
|
119
|
-
* Build the
|
|
116
|
+
* Build the cloudflare `_routes.json` rules for a set of copied public files, within Cloudflare's budget.
|
|
120
117
|
*
|
|
121
118
|
* `exclude` is what Pages serves straight from the CDN instead of invoking the worker, so naming every
|
|
122
119
|
* public file is ideal - and impossible past ~99 of them. A `public/` of icons, fonts and share images
|
|
@@ -200,7 +197,7 @@ export async function copyPublicDir(from, to) {
|
|
|
200
197
|
const target = join(to, rel);
|
|
201
198
|
await mkdir(join(target, ".."), { recursive: true });
|
|
202
199
|
await cp(candidateReal, target);
|
|
203
|
-
copied.push(
|
|
200
|
+
copied.push(publicUrlPath(rel));
|
|
204
201
|
}
|
|
205
202
|
return copied.sort();
|
|
206
203
|
}
|
|
@@ -213,11 +210,13 @@ export async function copyPublicDir(from, to) {
|
|
|
213
210
|
* Pure: `source` + the discovered `manifest` in, new source out.
|
|
214
211
|
*/
|
|
215
212
|
export function resyncServerManifestSource(source, manifest, routesPrefix) {
|
|
213
|
+
const cssLoading = parseManifestCssLoading(source);
|
|
216
214
|
return generateServerManifest(manifest, {
|
|
217
215
|
resolve: (file) => `${routesPrefix}${file}`,
|
|
218
216
|
clientEntry: parseManifestClientEntry(source) ?? "",
|
|
219
217
|
styles: parseManifestStyles(source),
|
|
220
218
|
routeStyles: parseManifestRouteStyles(source),
|
|
219
|
+
...(cssLoading !== undefined ? { cssLoading } : {}),
|
|
221
220
|
lazy: MANIFEST_IS_LAZY.test(source),
|
|
222
221
|
});
|
|
223
222
|
}
|
|
@@ -264,6 +263,7 @@ export async function buildClient(options) {
|
|
|
264
263
|
// layered after these in the `define` object). `Bun` may be absent under non-Bun typecheck - guard it.
|
|
265
264
|
const buildEnv = (typeof Bun !== "undefined" ? Bun.env : undefined) ?? process.env;
|
|
266
265
|
const publicDefines = publicEnvDefines(options.publicEnvPrefix ?? "PUBLIC_", buildEnv);
|
|
266
|
+
const privateEnv = privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_");
|
|
267
267
|
// Keep the generated source beside the project, not inside `outDir`: module resolution starts at the
|
|
268
268
|
// importing file, so an absolute `--out /tmp/deploy` must still resolve the app's dependencies.
|
|
269
269
|
// A unique directory also avoids clobbering a user file or colliding with parallel builds.
|
|
@@ -281,6 +281,10 @@ export async function buildClient(options) {
|
|
|
281
281
|
...routeManifest.routes.map((r) => r.file),
|
|
282
282
|
...Object.values(routeManifest.layouts).map((l) => l.file),
|
|
283
283
|
...(routeManifest.notFound ? [routeManifest.notFound.file] : []),
|
|
284
|
+
// A nested `_404` is an entry only for its stylesheet: it renders on the server, unhydrated.
|
|
285
|
+
...Object.values(routeManifest.notFounds ?? {}).map((page) => page.file),
|
|
286
|
+
// A `_loading` page is an entry for its stylesheet too: the bootstrap imports it lazily.
|
|
287
|
+
...Object.values(routeManifest.loadings ?? {}).map((page) => page.file),
|
|
284
288
|
]),
|
|
285
289
|
].sort();
|
|
286
290
|
// `metafile: true` asks Bun for the input/output graph - specifically `outputs[js].entryPoint`
|
|
@@ -288,12 +292,14 @@ export async function buildClient(options) {
|
|
|
288
292
|
// entry→CSS link for per-route splitting: keyed by the unique source path, so it survives
|
|
289
293
|
// same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. Not yet in
|
|
290
294
|
// `@types/bun`'s `BuildConfig`, so spread it in (spread props skip the excess-property check).
|
|
291
|
-
const buildExtras = { metafile: true };
|
|
295
|
+
const buildExtras = { metafile: true, throw: false };
|
|
296
|
+
const classifier = createZoneClassifier({ appRoot: root, routesDir, generatedFiles: [entryFile] });
|
|
297
|
+
const refused = new Map();
|
|
298
|
+
// No `outdir`: the bundle stays in memory until the graph proves it holds browser code only.
|
|
292
299
|
const result = await (async () => {
|
|
293
300
|
try {
|
|
294
301
|
return await Bun.build({
|
|
295
302
|
entrypoints: [entryFile, ...routeFiles.map(resolve)],
|
|
296
|
-
outdir: outDir,
|
|
297
303
|
target: "browser",
|
|
298
304
|
naming: "[name]-[hash].[ext]",
|
|
299
305
|
publicPath,
|
|
@@ -304,11 +310,16 @@ export async function buildClient(options) {
|
|
|
304
310
|
...buildExtras,
|
|
305
311
|
minify: options.minify ?? true,
|
|
306
312
|
plugins: [
|
|
313
|
+
// First, so it sees every file before another plugin's `onLoad` claims it.
|
|
314
|
+
zoneGuardPlugin({
|
|
315
|
+
appRoot: root,
|
|
316
|
+
classifier,
|
|
317
|
+
onDenied: (file, reason) => refused.set(file, reason),
|
|
318
|
+
}),
|
|
307
319
|
...declaredSingleCopyPlugins(root),
|
|
308
320
|
reactDedupePlugin(routesDir),
|
|
309
321
|
preactDedupePlugin(routesDir),
|
|
310
322
|
svelteDedupePlugin(routesDir),
|
|
311
|
-
serverOnlyEmptyPlugin(),
|
|
312
323
|
serverFnStubPlugin(),
|
|
313
324
|
...(options.plugins ?? []),
|
|
314
325
|
],
|
|
@@ -316,10 +327,11 @@ export async function buildClient(options) {
|
|
|
316
327
|
// Replace `process.env.*` at compile time so an app module reading config off `process.env` doesn't
|
|
317
328
|
// hit a `process is not defined` crash in the browser. Bun does longest-match: NODE_ENV resolves to
|
|
318
329
|
// the build mode (React's prod/dev branch); each PUBLIC_* var resolves to its baked VALUE; every
|
|
319
|
-
// other `process.env.X` becomes undefined (the bare `process.env` → `
|
|
320
|
-
// never leak). Callers can override any of these via `options.define` (layered last).
|
|
330
|
+
// other `process.env.X` becomes undefined (the bare `process.env` → `{}` fallback - so secrets
|
|
331
|
+
// never leak). Callers can override any of these via `options.define` (layered last). The value
|
|
332
|
+
// must be JSON: Bun inlines anything else (`({})`) as a string literal.
|
|
321
333
|
define: {
|
|
322
|
-
"process.env": "
|
|
334
|
+
"process.env": "{}",
|
|
323
335
|
"process.env.NODE_ENV": JSON.stringify(mode),
|
|
324
336
|
...publicDefines,
|
|
325
337
|
...options.define,
|
|
@@ -330,36 +342,110 @@ export async function buildClient(options) {
|
|
|
330
342
|
rmSync(entryDir, { recursive: true, force: true });
|
|
331
343
|
}
|
|
332
344
|
})();
|
|
345
|
+
const cwd = process.cwd();
|
|
346
|
+
const clientMeta = result.metafile;
|
|
333
347
|
if (!result.success) {
|
|
348
|
+
// A refused file can be what broke the build (a backend module importing a server-only built-in),
|
|
349
|
+
// so the refusal is the error to show, not the bundler's symptom.
|
|
350
|
+
const verdict = refused.size > 0 && clientMeta !== undefined
|
|
351
|
+
? formatClientGraphVerdict(verifyClientGraph(fromBunMetafile(clientMeta), {
|
|
352
|
+
classifier,
|
|
353
|
+
sourceOf: bunModuleSource(cwd),
|
|
354
|
+
privateEnv,
|
|
355
|
+
refused: new Set(refused.keys()),
|
|
356
|
+
}))
|
|
357
|
+
: undefined;
|
|
358
|
+
if (verdict !== undefined)
|
|
359
|
+
throw new Error(verdict);
|
|
360
|
+
if (refused.size > 0)
|
|
361
|
+
throw new Error(formatRefusedFiles(refused, root));
|
|
334
362
|
throw new Error(`[nifra/web] client build failed:\n${result.logs.map((l) => String(l)).join("\n")}`);
|
|
335
363
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
364
|
+
const emitted = await Promise.all(result.outputs.map(async (out) => {
|
|
365
|
+
if (out.kind === "sourcemap")
|
|
366
|
+
return { name: out.path, kind: "map", text: await out.text() };
|
|
367
|
+
if (out.kind === "asset") {
|
|
368
|
+
if (out.path.endsWith(".css"))
|
|
369
|
+
return { name: out.path, kind: "css", text: await out.text() };
|
|
370
|
+
const text = textOf(new Uint8Array(await out.arrayBuffer()));
|
|
371
|
+
return { name: out.path, kind: "asset", ...(text === undefined ? {} : { text }) };
|
|
372
|
+
}
|
|
373
|
+
return { name: out.path, kind: "code", text: await out.text() };
|
|
374
|
+
}));
|
|
375
|
+
const clientGraph = withEmittedImports(fromBunMetafile(clientMeta), emitted, publicPath);
|
|
376
|
+
const verdict = verifyClientGraph(clientGraph, {
|
|
377
|
+
classifier,
|
|
378
|
+
sourceOf: bunModuleSource(cwd),
|
|
379
|
+
privateEnv,
|
|
380
|
+
refused: new Set(refused.keys()),
|
|
381
|
+
});
|
|
382
|
+
// Two independent records of one fact: a file the plugin refused that the graph never shows means the
|
|
383
|
+
// graph is missing evidence, and the build cannot prove anything else about it either.
|
|
384
|
+
const reported = new Set(verdict.leaks.map((leak) => resolvePath(root, leak.module)));
|
|
385
|
+
const unseen = [...refused.keys()].filter((file) => !reported.has(file));
|
|
386
|
+
const graphMessage = formatClientGraphVerdict(unseen.length === 0
|
|
387
|
+
? verdict
|
|
388
|
+
: {
|
|
389
|
+
...verdict,
|
|
390
|
+
gaps: [
|
|
391
|
+
...verdict.gaps,
|
|
392
|
+
...unseen.map((file) => `${file} was refused while loading but is missing from the graph`),
|
|
393
|
+
],
|
|
394
|
+
});
|
|
395
|
+
if (graphMessage !== undefined)
|
|
396
|
+
throw new Error(graphMessage);
|
|
343
397
|
// #4: a `node:` builtin (e.g. `node:crypto`) pulled into a CLIENT chunk builds fine (Bun substitutes a
|
|
344
398
|
// browser polyfill) but breaks/leaks at runtime. Fail with the chain (entry → … → builtin), through the
|
|
345
399
|
// SHARED formatter so the Vite pipeline's identical guard reads byte-for-byte the same.
|
|
346
400
|
const nodeBuiltinLeak = formatNodeBuiltinLeak(detectNodeBuiltinsInClient(clientGraph));
|
|
347
401
|
if (nodeBuiltinLeak !== undefined)
|
|
348
402
|
throw new Error(nodeBuiltinLeak);
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
// `*.server`, so neither other guard fires. Same shared formatter as above.
|
|
403
|
+
// A module that imports the `backend-only` marker yet reached a client chunk: library code the zones
|
|
404
|
+
// cannot place on a side by path alone. Same shared formatter as the Vite pipeline.
|
|
352
405
|
const serverOnlyLeak = formatServerOnlyLeak(detectServerOnlyInClient(clientGraph));
|
|
353
406
|
if (serverOnlyLeak !== undefined)
|
|
354
407
|
throw new Error(serverOnlyLeak);
|
|
408
|
+
const unaccounted = formatUnaccountedOutput(accountEmittedFiles(emitted, clientGraph, {
|
|
409
|
+
classifier,
|
|
410
|
+
sourceOf: bunModuleSource(cwd),
|
|
411
|
+
outDir: resolvePath(outDir),
|
|
412
|
+
}));
|
|
413
|
+
if (unaccounted !== undefined)
|
|
414
|
+
throw new Error(unaccounted);
|
|
415
|
+
const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public");
|
|
416
|
+
const { sources, originsOf } = graphScanInput(clientGraph, bunModuleSource(cwd), classifier);
|
|
417
|
+
const secrets = formatSecretFindings(scanForSecrets({
|
|
418
|
+
sources,
|
|
419
|
+
artifacts: [
|
|
420
|
+
...emitted.flatMap((file) => {
|
|
421
|
+
if (file.text === undefined)
|
|
422
|
+
return [];
|
|
423
|
+
const name = file.name.replace(/^\.\//, "");
|
|
424
|
+
return emittedScanFiles(name, file.text, originsOf(name));
|
|
425
|
+
}),
|
|
426
|
+
...(publicDir !== undefined && existsSync(publicDir)
|
|
427
|
+
? publicScanFiles(publicDir, originName(root, resolvePath(publicDir)))
|
|
428
|
+
: []),
|
|
429
|
+
],
|
|
430
|
+
env: buildEnv,
|
|
431
|
+
publicEnvPrefix: options.publicEnvPrefix ?? "PUBLIC_",
|
|
432
|
+
...(options.secretExemptions ? { exemptions: options.secretExemptions } : {}),
|
|
433
|
+
}));
|
|
434
|
+
if (secrets !== undefined)
|
|
435
|
+
throw new Error(secrets);
|
|
436
|
+
const outputs = await Promise.all(result.outputs.map(async (out) => {
|
|
437
|
+
const path = join(outDir, out.path);
|
|
438
|
+
await Bun.write(path, out);
|
|
439
|
+
return { kind: out.kind, path };
|
|
440
|
+
}));
|
|
355
441
|
// Rename any chunk whose basename isn't URL-safe (dynamic-route files become `[slug]-hash.js`) and
|
|
356
442
|
// rewrite the references - otherwise the lazy import 404s and the route silently never hydrates.
|
|
357
|
-
const renamed = sanitizeOutputNames(
|
|
443
|
+
const renamed = sanitizeOutputNames(outputs);
|
|
358
444
|
const toUrl = (path) => `${publicPath}${renamed.get(basename(path)) ?? basename(path)}`;
|
|
359
445
|
// Entry-point outputs come back in entrypoint order: [bootstrap, ...routeFiles]. Map each route file
|
|
360
446
|
// to its chunk URL by that order (guarded against drift), then a route's chunks = its layout chain +
|
|
361
447
|
// own file.
|
|
362
|
-
const entryPoints =
|
|
448
|
+
const entryPoints = outputs.filter((o) => o.kind === "entry-point");
|
|
363
449
|
const bootstrap = entryPoints[0];
|
|
364
450
|
if (bootstrap === undefined)
|
|
365
451
|
throw new Error("[nifra/web] build produced no entry-point output");
|
|
@@ -397,7 +483,7 @@ export async function buildClient(options) {
|
|
|
397
483
|
// per-route `cssBundle` outputs follow. Dropping those per-route outputs (the old `aggregate`-only
|
|
398
484
|
// shape) left them in `manifest.assets` but not `css`, so a route-scoped stylesheet normalized to
|
|
399
485
|
// `asset:css` and tripped the module-graph parity contract - a bundler-dependent divergence.
|
|
400
|
-
const cssAssets =
|
|
486
|
+
const cssAssets = outputs.filter((o) => o.kind === "asset" && o.path.endsWith(".css"));
|
|
401
487
|
const css = [
|
|
402
488
|
...cssAssets.filter((o) => cssNameOf(o.path) === bootstrapName),
|
|
403
489
|
...cssAssets.filter((o) => cssNameOf(o.path) !== bootstrapName),
|
|
@@ -408,7 +494,6 @@ export async function buildClient(options) {
|
|
|
408
494
|
// same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. A page then
|
|
409
495
|
// links only its layout chain + own CSS (deduped); an empty array means the page needs no CSS at all.
|
|
410
496
|
// Absent (→ aggregate fallback) only if Bun emits no metafile/cssBundle - never silently incomplete.
|
|
411
|
-
const cwd = process.cwd();
|
|
412
497
|
const cssByEntry = new Map();
|
|
413
498
|
for (const out of Object.values(clientMeta?.outputs ?? {})) {
|
|
414
499
|
if (out.entryPoint !== undefined && out.cssBundle !== undefined) {
|
|
@@ -428,22 +513,32 @@ export async function buildClient(options) {
|
|
|
428
513
|
// SSR pages unstyled. Keep routeStyles absent until at least one authored route entry owns CSS.
|
|
429
514
|
const routeCssEntries = [...cssByEntry.keys()].filter((entry) => entry !== resolvePath(entryFile));
|
|
430
515
|
if (css.length > 0 && routeCssEntries.length > 0) {
|
|
516
|
+
// A `_loading` page can show on a navigation from ANY page, and a document links only its own
|
|
517
|
+
// route's stylesheets - so every page that can start a navigation carries the loading pages' too.
|
|
518
|
+
const loadingFiles = Object.values(routeManifest.loadings ?? {}).map((page) => page.file);
|
|
431
519
|
for (const route of routeManifest.routes) {
|
|
432
520
|
routeStyles[route.id] = stylesFor([
|
|
433
521
|
...route.layoutIds.map((id) => routeManifest.layouts[id]?.file ?? ""),
|
|
434
522
|
route.file,
|
|
523
|
+
...loadingFiles,
|
|
524
|
+
]);
|
|
525
|
+
}
|
|
526
|
+
if (routeManifest.notFound) {
|
|
527
|
+
routeStyles._404 = stylesFor([routeManifest.notFound.file, ...loadingFiles]);
|
|
528
|
+
}
|
|
529
|
+
for (const [id, page] of Object.entries(routeManifest.notFounds ?? {})) {
|
|
530
|
+
routeStyles[id] = stylesFor([
|
|
531
|
+
...page.layoutIds.map((layoutId) => routeManifest.layouts[layoutId]?.file ?? ""),
|
|
532
|
+
page.file,
|
|
435
533
|
]);
|
|
436
534
|
}
|
|
437
|
-
if (routeManifest.notFound)
|
|
438
|
-
routeStyles._404 = stylesFor([routeManifest.notFound.file]);
|
|
439
535
|
}
|
|
440
536
|
// Copy `public/` into the output next to the hashed assets. A missing directory is normal (most
|
|
441
537
|
// apps have none) and must not fail the build.
|
|
442
|
-
const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public");
|
|
443
538
|
const publicFiles = publicDir !== undefined && existsSync(publicDir) ? await copyPublicDir(publicDir, outDir) : [];
|
|
444
539
|
const manifest = {
|
|
445
540
|
entry: toUrl(bootstrap.path),
|
|
446
|
-
assets:
|
|
541
|
+
assets: outputs.map((o) => toUrl(o.path)),
|
|
447
542
|
routes,
|
|
448
543
|
...(publicFiles.length > 0 ? { publicFiles } : {}),
|
|
449
544
|
...(css.length > 0 ? { css } : {}),
|
|
@@ -540,12 +635,55 @@ const SVELTE_DEDUPE_PATTERN = dedupePolicyFor("svelte").bunPattern ?? /^svelte($
|
|
|
540
635
|
* fixed subpath list), Svelte has many internal subpaths, so each matched import is resolved dynamically.
|
|
541
636
|
* `svelte/compiler` (build-time only, not in the bundle) doesn't match the filter and is left alone. No-op
|
|
542
637
|
* when Svelte isn't used / isn't resolvable from `from`.
|
|
638
|
+
*
|
|
639
|
+
* Svelte is NOT condition-agnostic, unlike React and Preact: its bare entry maps `browser` to the client
|
|
640
|
+
* runtime and `default` to the server one. `Bun.resolveSync` answers for the process running the build -
|
|
641
|
+
* a server runtime - so pinning its answer put the SERVER entry into a browser bundle, where `hydrate`
|
|
642
|
+
* throws and the page never becomes interactive. For a browser bundle the package is therefore pinned by
|
|
643
|
+
* DIRECTORY and its export map is read with the bundle's own conditions.
|
|
543
644
|
*/
|
|
544
645
|
export const svelteDedupePlugin = (from) => ({
|
|
545
646
|
name: "nifra-svelte-dedupe",
|
|
546
647
|
setup(build) {
|
|
648
|
+
// Absent when the plugin runs as a runtime plugin rather than inside `Bun.build`; a build with no
|
|
649
|
+
// `target` is a browser build (Bun's default).
|
|
650
|
+
const config = build.config;
|
|
651
|
+
const browser = config !== undefined && (config.target ?? "browser") === "browser";
|
|
652
|
+
const conditions = new Set([
|
|
653
|
+
"browser",
|
|
654
|
+
"import",
|
|
655
|
+
...(typeof config?.conditions === "string"
|
|
656
|
+
? [config.conditions]
|
|
657
|
+
: (config?.conditions ?? [])),
|
|
658
|
+
]);
|
|
659
|
+
// The pinned copy's export map, read once. `null` when Svelte is not resolvable from the app root.
|
|
660
|
+
let pinned;
|
|
661
|
+
const pin = () => {
|
|
662
|
+
if (pinned !== undefined)
|
|
663
|
+
return pinned;
|
|
664
|
+
try {
|
|
665
|
+
const manifest = Bun.resolveSync("svelte/package.json", from);
|
|
666
|
+
const map = JSON.parse(readFileSync(manifest, "utf8")).exports;
|
|
667
|
+
pinned =
|
|
668
|
+
typeof map === "object" && map !== null
|
|
669
|
+
? { root: dirname(manifest), map: map }
|
|
670
|
+
: null;
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
pinned = null;
|
|
674
|
+
}
|
|
675
|
+
return pinned;
|
|
676
|
+
};
|
|
547
677
|
build.onResolve({ filter: SVELTE_DEDUPE_PATTERN }, (args) => {
|
|
548
678
|
try {
|
|
679
|
+
if (browser) {
|
|
680
|
+
const copy = pin();
|
|
681
|
+
const target = copy === null
|
|
682
|
+
? undefined
|
|
683
|
+
: exportTarget(copy.map[`.${args.path.slice("svelte".length)}`], conditions);
|
|
684
|
+
if (copy !== null && target !== undefined)
|
|
685
|
+
return { path: join(copy.root, target) };
|
|
686
|
+
}
|
|
549
687
|
return { path: Bun.resolveSync(args.path, from) };
|
|
550
688
|
}
|
|
551
689
|
catch {
|
|
@@ -554,6 +692,22 @@ export const svelteDedupePlugin = (from) => ({
|
|
|
554
692
|
});
|
|
555
693
|
},
|
|
556
694
|
});
|
|
695
|
+
/** Walk one `exports` entry the way a resolver does: the first key, in the package's own order, that is
|
|
696
|
+
* an active condition (or `default`) wins. */
|
|
697
|
+
function exportTarget(entry, conditions) {
|
|
698
|
+
if (typeof entry === "string")
|
|
699
|
+
return entry;
|
|
700
|
+
if (typeof entry !== "object" || entry === null)
|
|
701
|
+
return undefined;
|
|
702
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
703
|
+
if (key !== "default" && !conditions.has(key))
|
|
704
|
+
continue;
|
|
705
|
+
const target = exportTarget(value, conditions);
|
|
706
|
+
if (target !== undefined)
|
|
707
|
+
return target;
|
|
708
|
+
}
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
557
711
|
/**
|
|
558
712
|
* The app's declared single-copy rule, applied to the bundle.
|
|
559
713
|
*
|
|
@@ -568,32 +722,50 @@ export const svelteDedupePlugin = (from) => ({
|
|
|
568
722
|
*/
|
|
569
723
|
const declaredSingleCopyPlugins = (root) => readSingleCopyDeclaration(root) === undefined ? [] : [declaredSingleCopyPlugin({ cwd: root })];
|
|
570
724
|
/**
|
|
571
|
-
*
|
|
572
|
-
* `
|
|
573
|
-
*
|
|
574
|
-
* import resolves to `undefined` rather than a "missing export" bundle error (verified), and the real
|
|
575
|
-
* import subtree is gone. The complement to the node-builtin guard: when a server-only import is co-located
|
|
576
|
-
* in a route file (so it can't be tree-shaken out and the guard fails loud), moving it into a `*.server`
|
|
577
|
-
* module is the fix. CLIENT-only - buildServer keeps the real module, which runs server-side.
|
|
725
|
+
* Give each output chunk the specifiers its emitted code imports. Bun's metafile does not record them
|
|
726
|
+
* (its output `imports` stay empty even for an external the code keeps), so they are read from the code.
|
|
727
|
+
* A specifier under `publicPath` or relative to the chunk is mapped back to the output it names.
|
|
578
728
|
*/
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
729
|
+
function withEmittedImports(graph, emitted, publicPath) {
|
|
730
|
+
const scanner = new Bun.Transpiler({ loader: "js" });
|
|
731
|
+
const imports = new Map();
|
|
732
|
+
for (const file of emitted) {
|
|
733
|
+
if (file.kind !== "code" || file.text === undefined) {
|
|
734
|
+
imports.set(posix.normalize(file.name), []);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
imports.set(posix.normalize(file.name), scanner.scanImports(file.text).map(({ path }) => {
|
|
738
|
+
if (publicPath !== undefined && path.startsWith(publicPath))
|
|
739
|
+
return path.slice(publicPath.length);
|
|
740
|
+
if (path.startsWith("./") || path.startsWith("../"))
|
|
741
|
+
return posix.join(posix.dirname(posix.normalize(file.name)), path);
|
|
742
|
+
return path;
|
|
585
743
|
}));
|
|
586
|
-
}
|
|
587
|
-
}
|
|
744
|
+
}
|
|
745
|
+
const chunks = {};
|
|
746
|
+
// A chunk with no emitted file keeps no `imports`, which the verifiers treat as missing evidence.
|
|
747
|
+
for (const [path, chunk] of Object.entries(graph.chunks)) {
|
|
748
|
+
const kept = imports.get(posix.normalize(path));
|
|
749
|
+
chunks[path] = kept === undefined ? chunk : { ...chunk, imports: kept };
|
|
750
|
+
}
|
|
751
|
+
return { ...graph, chunks };
|
|
752
|
+
}
|
|
753
|
+
/** Refusals the bundler stopped on before it produced a graph, so without import chains. */
|
|
754
|
+
function formatRefusedFiles(refused, root) {
|
|
755
|
+
const lines = [...refused].map(([file, reason]) => {
|
|
756
|
+
const rel = relative(root, file).replaceAll("\\", "/");
|
|
757
|
+
return ` - ${rel.startsWith("..") ? file : rel}: ${reason}`;
|
|
758
|
+
});
|
|
759
|
+
return `[nifra/web] the browser build reached code that may not ship to a browser:\n${lines.join("\n")}`;
|
|
760
|
+
}
|
|
761
|
+
export { zoneGuardPlugin } from "./plugins/zone-guard.js";
|
|
588
762
|
/**
|
|
589
763
|
* Server functions in the CLIENT build: replace each `*.fn.ts` module with stubs that call the routes
|
|
590
764
|
* the server mounted, so the function bodies - and everything they import - never reach a browser.
|
|
591
765
|
*
|
|
592
|
-
* The
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
* `internal/server-boundary.ts` so the Vite pipeline emits identical stubs from the same code; two
|
|
596
|
-
* hand-written copies would be a client that works in dev and 404s in production.
|
|
766
|
+
* The generation itself is in `internal/server-boundary.ts` so the Vite pipeline emits identical stubs
|
|
767
|
+
* from the same code; two hand-written copies would be a client that works in dev and 404s in
|
|
768
|
+
* production.
|
|
597
769
|
*
|
|
598
770
|
* CLIENT-only. The server build keeps the real module, which is what `serverFunctions()` mounts.
|
|
599
771
|
*/
|
|
@@ -634,7 +806,7 @@ const solidWebServerPlugin = (from) => ({
|
|
|
634
806
|
* silently ships a broken worker.
|
|
635
807
|
*/
|
|
636
808
|
export async function buildServer(options) {
|
|
637
|
-
const { routesDir, serverEntry, outDir, clientEntry, styles, routeStyles } = options;
|
|
809
|
+
const { routesDir, serverEntry, outDir, clientEntry, styles, routeStyles, cssLoading } = options;
|
|
638
810
|
const entryDir = dirname(serverEntry);
|
|
639
811
|
const manifestFile = options.manifestFile ?? "server-manifest.ts";
|
|
640
812
|
// Default: import routes relative from the generated manifest (next to serverEntry) to routesDir.
|
|
@@ -650,11 +822,19 @@ export async function buildServer(options) {
|
|
|
650
822
|
const edge = target === "browser";
|
|
651
823
|
const conditions = options.conditions ?? (edge ? ["workerd", "edge-light", "browser"] : [target]);
|
|
652
824
|
const manifest = discoverRoutes(routesDir);
|
|
653
|
-
writeFileSync(`${entryDir}/${manifestFile}`, generateServerManifest(manifest, {
|
|
825
|
+
writeFileSync(`${entryDir}/${manifestFile}`, generateServerManifest(manifest, {
|
|
826
|
+
resolve,
|
|
827
|
+
clientEntry,
|
|
828
|
+
styles,
|
|
829
|
+
routeStyles,
|
|
830
|
+
...(cssLoading !== undefined ? { cssLoading } : {}),
|
|
831
|
+
lazy,
|
|
832
|
+
}));
|
|
654
833
|
const result = await Bun.build({
|
|
655
834
|
entrypoints: [serverEntry],
|
|
656
835
|
outdir: outDir,
|
|
657
836
|
target,
|
|
837
|
+
metafile: true,
|
|
658
838
|
conditions: [...conditions],
|
|
659
839
|
define: {
|
|
660
840
|
...(options.define ?? { "process.env.NODE_ENV": '"production"' }),
|
|
@@ -682,6 +862,38 @@ export async function buildServer(options) {
|
|
|
682
862
|
if (!result.success) {
|
|
683
863
|
throw new Error(`[nifra/web] server build failed:\n${result.logs.map((l) => String(l)).join("\n")}`);
|
|
684
864
|
}
|
|
865
|
+
// Named as the metafile names outputs: relative to `outdir`.
|
|
866
|
+
const nameOf = (path) => relative(outDir, path).replaceAll("\\", "/");
|
|
867
|
+
const emitted = await Promise.all(result.outputs.map(async (out) => out.kind === "sourcemap" || out.kind === "asset"
|
|
868
|
+
? { name: nameOf(out.path), kind: out.kind === "sourcemap" ? "map" : "asset" }
|
|
869
|
+
: { name: nameOf(out.path), kind: "code", text: await out.text() }));
|
|
870
|
+
const serverGraph = withEmittedImports(fromBunMetafile(result.metafile), emitted);
|
|
871
|
+
const appRoot = resolvePath(options.root ?? dirname(routesDir));
|
|
872
|
+
const classifier = createZoneClassifier({
|
|
873
|
+
appRoot,
|
|
874
|
+
routesDir,
|
|
875
|
+
generatedFiles: [
|
|
876
|
+
serverEntry,
|
|
877
|
+
join(entryDir, manifestFile),
|
|
878
|
+
...(options.generatedFiles ?? []),
|
|
879
|
+
].map((file) => resolvePath(file)),
|
|
880
|
+
});
|
|
881
|
+
const sourceOf = bunModuleSource(process.cwd());
|
|
882
|
+
const labelOf = (id) => {
|
|
883
|
+
const source = sourceOf(id);
|
|
884
|
+
return source.kind === "file" ? relative(appRoot, source.file).replaceAll("\\", "/") : id;
|
|
885
|
+
};
|
|
886
|
+
const refusal = formatServerGraphVerdict(verifyServerGraph(serverGraph, {
|
|
887
|
+
classifier,
|
|
888
|
+
sourceOf,
|
|
889
|
+
privateEnv: privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_"),
|
|
890
|
+
})) ?? formatUnsupportedBuiltins(unsupportedBuiltins(serverGraph, target, labelOf), target);
|
|
891
|
+
if (refusal !== undefined) {
|
|
892
|
+
// Bun wrote the bundle already; a refused build leaves nothing a deploy step could pick up.
|
|
893
|
+
for (const output of result.outputs)
|
|
894
|
+
rmSync(output.path, { force: true });
|
|
895
|
+
throw new Error(refusal);
|
|
896
|
+
}
|
|
685
897
|
const entryOutput = result.outputs.find((o) => o.kind === "entry-point");
|
|
686
898
|
if (entryOutput === undefined) {
|
|
687
899
|
throw new Error("[nifra/web] server build produced no entry-point output");
|
|
@@ -696,12 +908,24 @@ export async function buildServer(options) {
|
|
|
696
908
|
// The ONLY thing apps used to hand-write per target was the server entry (`_worker.ts`, `server-bun.ts`,
|
|
697
909
|
// …) - so we GENERATE it here (per target) instead of asking each app to ship five near-identical files.
|
|
698
910
|
// ===================================================================================================
|
|
911
|
+
/** The `createWebApp` options a generated server entry can import from the app's framework module. */
|
|
912
|
+
export const SERVER_ENTRY_OPTIONS = ["apiPrefix", "apiStrip", "mounts", "csp", "nonce"];
|
|
913
|
+
/**
|
|
914
|
+
* The header each edge platform sets to the caller's address, replacing any value the client sent.
|
|
915
|
+
* That replacement is the whole trust: the same bundle served without the platform's edge in front
|
|
916
|
+
* (`wrangler pages dev`, a self-hosted workerd) hands the header straight from the client, which is
|
|
917
|
+
* why it is only believed on an explicit `clientIp: "platform"`.
|
|
918
|
+
*/
|
|
919
|
+
const PLATFORM_CLIENT_IP_HEADERS = {
|
|
920
|
+
cloudflare: "cf-connecting-ip",
|
|
921
|
+
vercel: "x-real-ip",
|
|
922
|
+
};
|
|
699
923
|
/**
|
|
700
924
|
* Codegen the per-target **server entry** module (source text) for `buildServer` to bundle. It imports
|
|
701
925
|
* the app's `adapter` (from `framework.ts`), the optional `backend` (from `backend.ts`), and the
|
|
702
926
|
* generated `{ manifest, clientEntry }` (from `./server-manifest`), builds `createWebApp`, then wires
|
|
703
927
|
* the right host:
|
|
704
|
-
* - `
|
|
928
|
+
* - `cloudflare` / `vercel`: `export default` the fetch handler (the platform serves /assets/* itself).
|
|
705
929
|
* - `deno`: same fetch-handler default, plus `Deno.serve` self-host when run directly.
|
|
706
930
|
* - `bun` / `node`: a self-hosting server that ALSO serves the client bundle from disk (those
|
|
707
931
|
* runtimes have a filesystem; the static `/assets/*` sit next to the entry).
|
|
@@ -710,7 +934,23 @@ export async function buildServer(options) {
|
|
|
710
934
|
* string out) so the generation is unit-testable without a real build.
|
|
711
935
|
*/
|
|
712
936
|
export function generateServerEntry(options) {
|
|
713
|
-
const { target, adapterImport, backendImport, useImport, title = "nifra", publicFiles = [], } = options;
|
|
937
|
+
const { target, adapterImport, backendImport, useImport, optionImports = {}, title = "nifra", publicFiles = [], clientIp, } = options;
|
|
938
|
+
if (clientIp !== undefined && clientIp !== "platform") {
|
|
939
|
+
throw new Error(`[nifra/web] generateServerEntry: clientIp must be "platform" or left out, got ${JSON.stringify(clientIp)}`);
|
|
940
|
+
}
|
|
941
|
+
const clientIpHeader = clientIp === "platform" ? PLATFORM_CLIENT_IP_HEADERS[target] : undefined;
|
|
942
|
+
// Identifiers come only from the fixed list, never from the caller's keys, so generated code names
|
|
943
|
+
// nothing the list does not.
|
|
944
|
+
const importedOptions = SERVER_ENTRY_OPTIONS.filter((name) => optionImports[name] !== undefined);
|
|
945
|
+
const optionModules = new Map();
|
|
946
|
+
for (const name of importedOptions) {
|
|
947
|
+
const from = optionImports[name];
|
|
948
|
+
const names = optionModules.get(from);
|
|
949
|
+
if (names === undefined)
|
|
950
|
+
optionModules.set(from, [name]);
|
|
951
|
+
else
|
|
952
|
+
names.push(name);
|
|
953
|
+
}
|
|
714
954
|
if (target === "static") {
|
|
715
955
|
throw new Error("[nifra/web] generateServerEntry: `static` has no server entry (SSG only)");
|
|
716
956
|
}
|
|
@@ -720,18 +960,24 @@ export function generateServerEntry(options) {
|
|
|
720
960
|
lines.push(`import { adapter } from ${JSON.stringify(adapterImport)}`);
|
|
721
961
|
if (useImport !== undefined)
|
|
722
962
|
lines.push(`import { use } from ${JSON.stringify(useImport)}`);
|
|
963
|
+
for (const [from, names] of optionModules) {
|
|
964
|
+
lines.push(`import { ${names.join(", ")} } from ${JSON.stringify(from)}`);
|
|
965
|
+
}
|
|
723
966
|
if (backendImport !== undefined) {
|
|
724
967
|
lines.push(`import { backend } from ${JSON.stringify(backendImport)}`);
|
|
725
968
|
}
|
|
726
|
-
lines.push('import { clientEntry, manifest, styles, routeStyles } from "./server-manifest"');
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
if (usesToFetch)
|
|
969
|
+
lines.push('import { clientEntry, cssLoading, manifest, styles, routeStyles } from "./server-manifest"');
|
|
970
|
+
// Cloudflare needs the fetch-handler shape; the others call app.fetch directly.
|
|
971
|
+
if (target === "cloudflare")
|
|
730
972
|
lines.push('import { toFetchHandler } from "@nifrajs/core/server"');
|
|
731
973
|
if (target === "node")
|
|
732
974
|
lines.push('import { serve } from "@nifrajs/node"');
|
|
733
|
-
lines.push("", "const app = createWebApp({", " adapter,", ...(useImport !== undefined ? [" use,"] : []), " manifest,", " clientEntry,", " styles,", " routeStyles,", ...(backendImport !== undefined ? [" api: inProcessClient(backend),"] : []), ` title: ${JSON.stringify(title)},`,
|
|
734
|
-
|
|
975
|
+
lines.push("", "const app = createWebApp({", " adapter,", ...(useImport !== undefined ? [" use,"] : []), ...importedOptions.map((name) => ` ${name},`), " manifest,", " clientEntry,", " styles,", " routeStyles,", " cssLoading,", ...(backendImport !== undefined ? [" api: inProcessClient(backend),"] : []), ` title: ${JSON.stringify(title)},`,
|
|
976
|
+
// Core's own `{ header }` trust, so the mounted backend and every loader see the derived caller.
|
|
977
|
+
...(clientIpHeader !== undefined
|
|
978
|
+
? [` server: { clientIp: { header: ${JSON.stringify(clientIpHeader)} } },`]
|
|
979
|
+
: []), "})", "");
|
|
980
|
+
if (target === "cloudflare") {
|
|
735
981
|
// Cloudflare Pages advanced mode: `_routes.json` keeps static paths off the worker entirely, and
|
|
736
982
|
// everything else falls through to this handler (SSR).
|
|
737
983
|
//
|
|
@@ -747,20 +993,26 @@ export function generateServerEntry(options) {
|
|
|
747
993
|
return `${lines.join("\n")}\n`;
|
|
748
994
|
}
|
|
749
995
|
// bun / node / deno self-host AND serve the client bundle from disk (it sits next to this entry).
|
|
750
|
-
lines.push("// The client bundle and public files live next to this entry; serve only manifest-approved paths.", 'const STATIC_ROOT = new URL("./", import.meta.url)', `const PUBLIC_FILES = new Set(${JSON.stringify(publicFiles)})`, "const staticPath = (pathname: string): string | undefined => {", " if (PUBLIC_FILES.has(pathname)) return '.' + pathname", ' if (!pathname.startsWith("/assets/")) return undefined', ' const segments = pathname.slice(1).split("/")', " // `.` and `..` match the character class, and the path is joined onto a base URL that resolves", " // dot segments - so they are rejected explicitly rather than relying on the request URL having", " // been normalized upstream.", " const safe = (segment: string): boolean =>", ' segment !== "." && segment !== ".." && /^[A-Za-z0-9._-]+$/.test(segment)', " return segments.every(safe) ? '.' + pathname : undefined", "}",
|
|
996
|
+
lines.push("// The client bundle and public files live next to this entry; serve only manifest-approved paths.", 'const STATIC_ROOT = new URL("./", import.meta.url)', `const PUBLIC_FILES = new Set(${JSON.stringify(publicFiles)})`, "const staticPath = (pathname: string): string | undefined => {", " if (PUBLIC_FILES.has(pathname)) return '.' + pathname", ' if (!pathname.startsWith("/assets/")) return undefined', ' const segments = pathname.slice(1).split("/")', " // `.` and `..` match the character class, and the path is joined onto a base URL that resolves", " // dot segments - so they are rejected explicitly rather than relying on the request URL having", " // been normalized upstream.", " const safe = (segment: string): boolean =>", ' segment !== "." && segment !== ".." && /^[A-Za-z0-9._-]+$/.test(segment)', " return segments.every(safe) ? '.' + pathname : undefined", "}",
|
|
997
|
+
// The type table and cache policy `servePublicDir` uses. Without a known type, Bun answers a file
|
|
998
|
+
// as an `application/octet-stream` attachment, which a browser downloads instead of showing.
|
|
999
|
+
`const TYPES: Record<string, string> = ${JSON.stringify(CONTENT_TYPES)}`, "const staticHeaders = (pathname: string): Record<string, string> => ({", ' "content-type": TYPES[pathname.slice(pathname.lastIndexOf(".")).toLowerCase()] ?? "application/octet-stream",', ` "cache-control": pathname.startsWith("/assets/") ? ${JSON.stringify(IMMUTABLE)} : ${JSON.stringify(ONE_DAY)},`, ' "x-content-type-options": "nosniff",', "})");
|
|
1000
|
+
// Bun.serve and Deno.serve delimit the body themselves, so its declared length is the transport
|
|
1001
|
+
// frame - the guarantee core's own listen() and the Deno adapter mark (see markTrustedBodyFraming).
|
|
1002
|
+
const markFramed = '(req as unknown as Record<symbol, unknown>)[Symbol.for("nifra.body.trustedFraming")] = true';
|
|
751
1003
|
if (target === "bun") {
|
|
752
|
-
lines.push("const server = Bun.serve({", " port: Number(Bun.env.PORT ?? 3000),", " async fetch(req) {", " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " const file = Bun.file(new URL(filePath, STATIC_ROOT))", ' if (!(await file.exists())) return new Response("not found", { status: 404 })',
|
|
1004
|
+
lines.push("const server = Bun.serve({", " port: Number(Bun.env.PORT ?? 3000),", " async fetch(req, server) {", " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " const file = Bun.file(new URL(filePath, STATIC_ROOT))", ' if (!(await file.exists())) return new Response("not found", { status: 404 })', " return new Response(file, { headers: staticHeaders(pathname) })", " }", ` ${markFramed}`, " // The socket peer, looked up only when something reads it (as core's own listen() does).", " return app.fetch(req, { get clientIp() { return server.requestIP(req)?.address } })", " },", "})",
|
|
753
1005
|
// The `${...}` here is literal OUTPUT (a template in the GENERATED file), not a template in this
|
|
754
1006
|
// source - split so biome's noTemplateCurlyInString doesn't flag it; the emitted line is unchanged.
|
|
755
1007
|
`console.log(\`nifra (Bun) → http://localhost:$${"{server.port}"}\`)`);
|
|
756
1008
|
return `${lines.join("\n")}\n`;
|
|
757
1009
|
}
|
|
758
1010
|
if (target === "node") {
|
|
759
|
-
lines.push('import { readFile } from "node:fs/promises"', "await serve(", " {", " async fetch(req) {", " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " try {", " const body = await readFile(new URL(filePath, STATIC_ROOT))",
|
|
1011
|
+
lines.push('import { readFile } from "node:fs/promises"', "await serve(", " {", " async fetch(req, platform) {", " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " try {", " const body = await readFile(new URL(filePath, STATIC_ROOT))", " return new Response(body, { headers: staticHeaders(pathname) })", " } catch {", ' return new Response("not found", { status: 404 })', " }", " }", " return app.fetch(req, platform)", " },", " },", " { port: Number(process.env.PORT ?? 3000) },", ")");
|
|
760
1012
|
return `${lines.join("\n")}\n`;
|
|
761
1013
|
}
|
|
762
1014
|
// deno
|
|
763
|
-
lines.push("
|
|
1015
|
+
lines.push("// @ts-ignore - Deno global is present on the Deno runtime this output targets.", 'Deno.serve({ port: Number(Deno.env.get("PORT") ?? "3000") }, async (req, info) => {', " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " try {", " // @ts-ignore - Deno.readFile is present on the Deno runtime.", " const body = await Deno.readFile(new URL(filePath, STATIC_ROOT))", " return new Response(body, { headers: staticHeaders(pathname) })", " } catch {", ' return new Response("not found", { status: 404 })', " }", " }", ` ${markFramed}`, " return app.fetch(req, { clientIp: info.remoteAddr.hostname })", "})");
|
|
764
1016
|
return `${lines.join("\n")}\n`;
|
|
765
1017
|
}
|
|
766
1018
|
/** Measure each emitted output file's raw + gzip size. Reads the file off disk and gzips it with
|
|
@@ -783,36 +1035,48 @@ async function measureOutputs(paths) {
|
|
|
783
1035
|
* `<outDir>/assets/*`, then per target:
|
|
784
1036
|
* - `static`: prerenders opted-in routes (`prerenderRoutes`) to `<outDir>/<path>/index.html` (+
|
|
785
1037
|
* `_data.json`); needs `prerenderApp`. No server.
|
|
786
|
-
* - `
|
|
787
|
-
* - `vercel`:
|
|
788
|
-
*
|
|
1038
|
+
* - `cloudflare`: a `_worker.js` (edge bundle) + a `_routes.json` excluding /assets/* from the worker.
|
|
1039
|
+
* - `vercel`: Vercel's Build Output API v3 - `config.json`, the client bundle and public files under
|
|
1040
|
+
* `static/`, and the edge function at `functions/index.func/index.js`; `vercel deploy --prebuilt`
|
|
1041
|
+
* uploads it as it is when `outDir` is `.vercel/output`.
|
|
789
1042
|
* - `deno`/`node`/`bun`: the self-hosting server bundle (`server.js`) next to the assets.
|
|
790
1043
|
* The server entry is GENERATED (`generateServerEntry`) and bundled (`buildServer`); the app supplies
|
|
791
1044
|
* only adapter/backend/routes. Returns the manifest + a size report. Throws on any build failure.
|
|
792
|
-
*
|
|
793
|
-
* Note: the heavier platform wrappers (`.vercel/output` v3 layout, wrangler ISR `find_additional_modules`)
|
|
794
|
-
* remain app-owned scripts; this command targets the common single-bundle deploys. See the CLI docs.
|
|
795
1045
|
*/
|
|
796
1046
|
/** The default (Bun) strategy - `buildClient`/`buildServer` from this module. */
|
|
797
1047
|
export const bunBundler = {
|
|
798
|
-
buildClient: (input) =>
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1048
|
+
buildClient: (input) => {
|
|
1049
|
+
const cssCodeSplit = normalizeCssCodeSplit(input.cssCodeSplit);
|
|
1050
|
+
if (cssCodeSplit === false) {
|
|
1051
|
+
throw new Error("[nifra/web] cssCodeSplit: false is a Vite-only production option. Use buildTargetVite or remove it for the native Bun build.");
|
|
1052
|
+
}
|
|
1053
|
+
return buildClient({
|
|
1054
|
+
routesDir: input.routesDir,
|
|
1055
|
+
outDir: input.outDir,
|
|
1056
|
+
clientModule: input.clientModule,
|
|
1057
|
+
...(input.plugins ? { plugins: input.plugins } : {}),
|
|
1058
|
+
...(input.conditions ? { conditions: input.conditions } : {}),
|
|
1059
|
+
...(input.define ? { define: input.define } : {}),
|
|
1060
|
+
...(input.cssCodeSplit !== undefined ? { cssCodeSplit } : {}),
|
|
1061
|
+
...(input.publicDir !== undefined ? { publicDir: input.publicDir } : {}),
|
|
1062
|
+
...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
|
|
1063
|
+
...(input.secretExemptions !== undefined ? { secretExemptions: input.secretExemptions } : {}),
|
|
1064
|
+
});
|
|
1065
|
+
},
|
|
808
1066
|
buildServer: (input) => buildServer({
|
|
809
1067
|
routesDir: input.routesDir,
|
|
810
1068
|
serverEntry: input.serverEntry,
|
|
811
1069
|
outDir: input.outDir,
|
|
812
1070
|
clientEntry: input.clientEntry,
|
|
813
1071
|
target: input.target,
|
|
1072
|
+
...(input.styles !== undefined ? { styles: input.styles } : {}),
|
|
1073
|
+
...(input.routeStyles !== undefined ? { routeStyles: input.routeStyles } : {}),
|
|
814
1074
|
...(input.plugins ? { plugins: input.plugins } : {}),
|
|
815
1075
|
...(input.define ? { define: input.define } : {}),
|
|
1076
|
+
...(input.cssLoading !== undefined ? { cssLoading: input.cssLoading } : {}),
|
|
1077
|
+
...(input.root !== undefined ? { root: input.root } : {}),
|
|
1078
|
+
...(input.generatedFiles !== undefined ? { generatedFiles: input.generatedFiles } : {}),
|
|
1079
|
+
...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
|
|
816
1080
|
}),
|
|
817
1081
|
};
|
|
818
1082
|
/** Build a full deploy dir for `target` using the default Bun bundler. See {@link buildTargetWith}. */
|
|
@@ -827,14 +1091,17 @@ export async function buildTarget(target, options) {
|
|
|
827
1091
|
*/
|
|
828
1092
|
export async function buildTargetWith(target, options, bundler) {
|
|
829
1093
|
const { routesDir, outDir, workDir } = options;
|
|
830
|
-
const
|
|
1094
|
+
const requestedCssCodeSplit = options.cssCodeSplit === undefined ? undefined : normalizeCssCodeSplit(options.cssCodeSplit);
|
|
1095
|
+
const requestedCssLoading = options.cssLoading === undefined ? undefined : normalizeCssLoading(options.cssLoading);
|
|
1096
|
+
const targetPlan = planBuildTarget(parseBuildTarget(target), outDir);
|
|
831
1097
|
const { rmSync } = await import("node:fs");
|
|
832
1098
|
rmSync(outDir, { recursive: true, force: true });
|
|
833
1099
|
rmSync(workDir, { recursive: true, force: true });
|
|
834
|
-
const
|
|
1100
|
+
const staticRoot = targetPlan.staticDir === "" ? outDir : `${outDir}/${targetPlan.staticDir}`;
|
|
1101
|
+
const assetsDir = `${staticRoot}/assets`;
|
|
835
1102
|
mkdirSync(assetsDir, { recursive: true });
|
|
836
1103
|
mkdirSync(workDir, { recursive: true });
|
|
837
|
-
// (1) Client bundle → <
|
|
1104
|
+
// (1) Client bundle → <static root>/assets/* (every target ships the same hashed client bundle).
|
|
838
1105
|
let client = await bundler.buildClient({
|
|
839
1106
|
routesDir,
|
|
840
1107
|
outDir: assetsDir,
|
|
@@ -844,14 +1111,43 @@ export async function buildTargetWith(target, options, bundler) {
|
|
|
844
1111
|
define: { "process.env.NODE_ENV": '"production"', ...(options.define ?? {}) },
|
|
845
1112
|
publicDir: false,
|
|
846
1113
|
...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
|
|
1114
|
+
...(options.secretExemptions !== undefined
|
|
1115
|
+
? { secretExemptions: options.secretExemptions }
|
|
1116
|
+
: {}),
|
|
1117
|
+
...(requestedCssCodeSplit !== undefined ? { cssCodeSplit: requestedCssCodeSplit } : {}),
|
|
1118
|
+
...(requestedCssLoading !== undefined ? { cssLoading: requestedCssLoading } : {}),
|
|
847
1119
|
root: resolvePath(dirname(routesDir)),
|
|
848
1120
|
});
|
|
1121
|
+
// Bun's client builder has no CSS-loading field, but the server-side document policy is bundler
|
|
1122
|
+
// independent. Carry the requested policy into the result so static prerender callbacks and the
|
|
1123
|
+
// generated server entry see the same choice on both build strategies.
|
|
1124
|
+
if (requestedCssLoading !== undefined && client.cssLoading !== requestedCssLoading) {
|
|
1125
|
+
client = { ...client, cssLoading: requestedCssLoading };
|
|
1126
|
+
}
|
|
849
1127
|
const publicDir = options.publicDir === false
|
|
850
1128
|
? undefined
|
|
851
1129
|
: resolvePath(options.publicDir ?? join(dirname(routesDir), "public"));
|
|
852
|
-
const
|
|
1130
|
+
const secretScan = {
|
|
1131
|
+
env: buildEnvironment(),
|
|
1132
|
+
publicEnvPrefix: options.publicEnvPrefix ?? "PUBLIC_",
|
|
1133
|
+
...(options.secretExemptions !== undefined ? { exemptions: options.secretExemptions } : {}),
|
|
1134
|
+
};
|
|
1135
|
+
if (publicDir !== undefined && existsSync(publicDir)) {
|
|
1136
|
+
assertNoSecrets({
|
|
1137
|
+
...secretScan,
|
|
1138
|
+
artifacts: publicScanFiles(publicDir, originName(dirname(routesDir), publicDir)),
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
const publicFiles = publicDir !== undefined && existsSync(publicDir)
|
|
1142
|
+
? await copyPublicDir(publicDir, staticRoot)
|
|
1143
|
+
: [];
|
|
853
1144
|
if (publicFiles.length > 0) {
|
|
854
1145
|
client = { ...client, publicFiles };
|
|
1146
|
+
}
|
|
1147
|
+
// `buildClient` writes its first manifest before the target orchestration knows the activation policy
|
|
1148
|
+
// (the native Bun client builder has no CSS-loading option). Persist the target-level additions even
|
|
1149
|
+
// when the app has no public/ files; otherwise the returned manifest and the deploy artifact disagree.
|
|
1150
|
+
if (requestedCssLoading !== undefined || publicFiles.length > 0) {
|
|
855
1151
|
writeFileSync(`${assetsDir}/manifest.json`, JSON.stringify(client, null, 2));
|
|
856
1152
|
}
|
|
857
1153
|
if (targetPlan.kind === "static") {
|
|
@@ -867,6 +1163,7 @@ export async function buildTargetWith(target, options, bundler) {
|
|
|
867
1163
|
app,
|
|
868
1164
|
routes: manifest.routes,
|
|
869
1165
|
outDir,
|
|
1166
|
+
secrets: secretScan,
|
|
870
1167
|
});
|
|
871
1168
|
if (result.prerendered.length === 0) {
|
|
872
1169
|
// A static build that renders nothing is almost always a misconfig (no `prerender = true` / no
|
|
@@ -892,9 +1189,11 @@ export async function buildTargetWith(target, options, bundler) {
|
|
|
892
1189
|
target,
|
|
893
1190
|
adapterImport: options.adapterImport,
|
|
894
1191
|
...(options.useImport !== undefined ? { useImport: options.useImport } : {}),
|
|
1192
|
+
...(options.optionImports !== undefined ? { optionImports: options.optionImports } : {}),
|
|
895
1193
|
...(options.backendImport !== undefined ? { backendImport: options.backendImport } : {}),
|
|
896
1194
|
...(options.title !== undefined ? { title: options.title } : {}),
|
|
897
1195
|
...(publicFiles.length > 0 ? { publicFiles } : {}),
|
|
1196
|
+
...(options.clientIp !== undefined ? { clientIp: options.clientIp } : {}),
|
|
898
1197
|
}));
|
|
899
1198
|
const { worker } = await bundler.buildServer({
|
|
900
1199
|
routesDir,
|
|
@@ -902,13 +1201,22 @@ export async function buildTargetWith(target, options, bundler) {
|
|
|
902
1201
|
outDir: `${workDir}/server`,
|
|
903
1202
|
clientEntry: client.entry,
|
|
904
1203
|
target: targetPlan.serverTarget,
|
|
1204
|
+
...(client.css !== undefined ? { styles: client.css } : {}),
|
|
1205
|
+
...(client.routeStyles !== undefined ? { routeStyles: client.routeStyles } : {}),
|
|
905
1206
|
...(options.serverPlugins ? { plugins: options.serverPlugins } : {}),
|
|
906
1207
|
define: { "process.env.NODE_ENV": '"production"', ...(options.define ?? {}) },
|
|
1208
|
+
...(client.cssLoading !== undefined
|
|
1209
|
+
? { cssLoading: client.cssLoading }
|
|
1210
|
+
: requestedCssLoading !== undefined
|
|
1211
|
+
? { cssLoading: requestedCssLoading }
|
|
1212
|
+
: {}),
|
|
907
1213
|
root: resolvePath(dirname(routesDir)),
|
|
1214
|
+
generatedFiles: [resolvePath(workDir, options.adapterImport)],
|
|
1215
|
+
...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
|
|
908
1216
|
});
|
|
909
1217
|
// (3) Assemble the deploy dir for the target.
|
|
910
1218
|
const { cpSync } = await import("node:fs");
|
|
911
|
-
if (targetPlan.target === "
|
|
1219
|
+
if (targetPlan.target === "cloudflare") {
|
|
912
1220
|
cpSync(worker, `${outDir}/${targetPlan.outputFile}`);
|
|
913
1221
|
// The app's real patterns, so a directory is only collapsed into a glob once the route table
|
|
914
1222
|
// proves nothing can be served beneath it.
|
|
@@ -923,7 +1231,12 @@ export async function buildTargetWith(target, options, bundler) {
|
|
|
923
1231
|
}
|
|
924
1232
|
}
|
|
925
1233
|
else if (targetPlan.target === "vercel") {
|
|
1234
|
+
const fn = dirname(`${outDir}/${targetPlan.outputFile}`);
|
|
1235
|
+
mkdirSync(fn, { recursive: true });
|
|
926
1236
|
cpSync(worker, `${outDir}/${targetPlan.outputFile}`);
|
|
1237
|
+
writeFileSync(`${fn}/.vc-config.json`, `${JSON.stringify({ runtime: "edge", entrypoint: "index.js" }, null, 2)}\n`);
|
|
1238
|
+
// Files the build wrote are served first; every other path is the SSR function's.
|
|
1239
|
+
writeFileSync(`${outDir}/config.json`, `${JSON.stringify({ version: 3, routes: [{ handle: "filesystem" }, { src: "/(.*)", dest: "/index" }] }, null, 2)}\n`);
|
|
927
1240
|
}
|
|
928
1241
|
else {
|
|
929
1242
|
cpSync(worker, `${outDir}/${targetPlan.outputFile}`);
|