@nifrajs/web 3.5.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend-only.d.ts +15 -0
- package/dist/backend-only.d.ts.map +1 -0
- package/dist/backend-only.js +16 -0
- package/dist/backend-only.js.map +1 -0
- package/dist/boundary.d.ts +5 -0
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +11 -2
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +18 -6
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +82 -34
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +4 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +68 -13
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +57 -23
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +356 -85
- package/dist/build.js.map +1 -1
- package/dist/bun-dev-entry.d.ts +1 -1
- package/dist/bun-dev-entry.js +1 -1
- package/dist/cdn.d.ts +74 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.js +132 -0
- package/dist/cdn.js.map +1 -0
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +136 -35
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +4 -3
- package/dist/conformance.d.ts.map +1 -1
- package/dist/conformance.js +12 -3
- package/dist/conformance.js.map +1 -1
- package/dist/csp.d.ts +93 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +122 -0
- package/dist/csp.js.map +1 -0
- package/dist/dev-client.d.ts +76 -0
- package/dist/dev-client.d.ts.map +1 -0
- package/dist/dev-client.js +280 -0
- package/dist/dev-client.js.map +1 -0
- package/dist/dev-diagnostics.d.ts +26 -2
- package/dist/dev-diagnostics.d.ts.map +1 -1
- package/dist/dev-diagnostics.js +38 -7
- package/dist/dev-diagnostics.js.map +1 -1
- package/dist/dev-error.d.ts +4 -13
- package/dist/dev-error.d.ts.map +1 -1
- package/dist/dev-error.js +46 -4
- package/dist/dev-error.js.map +1 -1
- package/dist/dev-feed.d.ts +298 -0
- package/dist/dev-feed.d.ts.map +1 -0
- package/dist/dev-feed.js +797 -0
- package/dist/dev-feed.js.map +1 -0
- package/dist/dev-indicator.d.ts +38 -0
- package/dist/dev-indicator.d.ts.map +1 -0
- package/dist/dev-indicator.js +93 -0
- package/dist/dev-indicator.js.map +1 -0
- package/dist/dev-session.d.ts +77 -0
- package/dist/dev-session.d.ts.map +1 -0
- package/dist/dev-session.js +580 -0
- package/dist/dev-session.js.map +1 -0
- package/dist/dev-sourcemap.d.ts +27 -0
- package/dist/dev-sourcemap.d.ts.map +1 -0
- package/dist/dev-sourcemap.js +235 -0
- package/dist/dev-sourcemap.js.map +1 -0
- package/dist/dev-ssr-graph.d.ts.map +1 -1
- package/dist/dev-ssr-graph.js +13 -5
- package/dist/dev-ssr-graph.js.map +1 -1
- package/dist/dev.d.ts +15 -2
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +69 -44
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +35 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -0
- package/dist/diagnostic-catalog.js +203 -0
- package/dist/diagnostic-catalog.js.map +1 -0
- package/dist/diagnostic-prompt.d.ts +49 -0
- package/dist/diagnostic-prompt.d.ts.map +1 -0
- package/dist/diagnostic-prompt.js +303 -0
- package/dist/diagnostic-prompt.js.map +1 -0
- package/dist/diagnostic.d.ts +17 -23
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +31 -39
- package/dist/diagnostic.js.map +1 -1
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +4 -1
- package/dist/draft.js.map +1 -1
- package/dist/fn.d.ts +5 -0
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +13 -3
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +4 -2
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +13 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +48 -0
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
- package/dist/internal/cdn/cloudflare.js +94 -0
- package/dist/internal/cdn/cloudflare.js.map +1 -0
- package/dist/internal/cdn/core.d.ts +91 -0
- package/dist/internal/cdn/core.d.ts.map +1 -0
- package/dist/internal/cdn/core.js +220 -0
- package/dist/internal/cdn/core.js.map +1 -0
- package/dist/internal/cdn/fastly.d.ts +17 -0
- package/dist/internal/cdn/fastly.d.ts.map +1 -0
- package/dist/internal/cdn/fastly.js +43 -0
- package/dist/internal/cdn/fastly.js.map +1 -0
- package/dist/internal/cdn/vercel.d.ts +31 -0
- package/dist/internal/cdn/vercel.d.ts.map +1 -0
- package/dist/internal/cdn/vercel.js +56 -0
- package/dist/internal/cdn/vercel.js.map +1 -0
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +141 -25
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-csp.d.ts +24 -0
- package/dist/internal/dev-csp.d.ts.map +1 -0
- package/dist/internal/dev-csp.js +160 -0
- package/dist/internal/dev-csp.js.map +1 -0
- package/dist/internal/dev-reserved.d.ts +25 -0
- package/dist/internal/dev-reserved.d.ts.map +1 -0
- package/dist/internal/dev-reserved.js +39 -0
- package/dist/internal/dev-reserved.js.map +1 -0
- package/dist/internal/html-spans.d.ts +10 -0
- package/dist/internal/html-spans.d.ts.map +1 -0
- package/dist/internal/html-spans.js +46 -0
- package/dist/internal/html-spans.js.map +1 -0
- package/dist/internal/loading-runtime.d.ts +44 -0
- package/dist/internal/loading-runtime.d.ts.map +1 -0
- package/dist/internal/loading-runtime.js +206 -0
- package/dist/internal/loading-runtime.js.map +1 -0
- package/dist/internal/matches-runtime.d.ts +8 -0
- package/dist/internal/matches-runtime.d.ts.map +1 -0
- package/dist/internal/matches-runtime.js +61 -0
- package/dist/internal/matches-runtime.js.map +1 -0
- package/dist/internal/mount-shadow.d.ts +36 -0
- package/dist/internal/mount-shadow.d.ts.map +1 -0
- package/dist/internal/mount-shadow.js +65 -0
- package/dist/internal/mount-shadow.js.map +1 -0
- package/dist/internal/node-builtins.d.ts +3 -0
- package/dist/internal/node-builtins.d.ts.map +1 -0
- package/dist/internal/node-builtins.js +79 -0
- package/dist/internal/node-builtins.js.map +1 -0
- package/dist/internal/output-guard.d.ts +65 -0
- package/dist/internal/output-guard.d.ts.map +1 -0
- package/dist/internal/output-guard.js +630 -0
- package/dist/internal/output-guard.js.map +1 -0
- package/dist/internal/page-execution.d.ts +20 -8
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +571 -105
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts +21 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +104 -22
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +12 -0
- package/dist/internal/private-env.d.ts.map +1 -0
- package/dist/internal/private-env.js +335 -0
- package/dist/internal/private-env.js.map +1 -0
- package/dist/internal/public-url.d.ts +3 -0
- package/dist/internal/public-url.d.ts.map +1 -0
- package/dist/internal/public-url.js +18 -0
- package/dist/internal/public-url.js.map +1 -0
- package/dist/internal/render-document.d.ts +25 -19
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +77 -61
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/response-controls.d.ts +75 -0
- package/dist/internal/response-controls.d.ts.map +1 -0
- package/dist/internal/response-controls.js +256 -0
- package/dist/internal/response-controls.js.map +1 -0
- package/dist/internal/runtime-contract.d.ts +11 -16
- package/dist/internal/runtime-contract.d.ts.map +1 -1
- package/dist/internal/runtime-contract.js.map +1 -1
- package/dist/internal/secret-scan.d.ts +104 -0
- package/dist/internal/secret-scan.d.ts.map +1 -0
- package/dist/internal/secret-scan.js +503 -0
- package/dist/internal/secret-scan.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +6 -29
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +6 -66
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/source-map.d.ts +47 -0
- package/dist/internal/source-map.d.ts.map +1 -0
- package/dist/internal/source-map.js +225 -0
- package/dist/internal/source-map.js.map +1 -0
- package/dist/internal/target-compat.d.ts +27 -0
- package/dist/internal/target-compat.d.ts.map +1 -0
- package/dist/internal/target-compat.js +77 -0
- package/dist/internal/target-compat.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +6 -0
- package/dist/internal/timing-safe-equal.d.ts.map +1 -1
- package/dist/internal/timing-safe-equal.js +10 -0
- package/dist/internal/timing-safe-equal.js.map +1 -1
- package/dist/internal/vitals-runtime.d.ts +18 -0
- package/dist/internal/vitals-runtime.d.ts.map +1 -0
- package/dist/internal/vitals-runtime.js +38 -0
- package/dist/internal/vitals-runtime.js.map +1 -0
- package/dist/internal/web-app.d.ts +38 -9
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +199 -59
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/zone-graph.d.ts +89 -0
- package/dist/internal/zone-graph.d.ts.map +1 -0
- package/dist/internal/zone-graph.js +492 -0
- package/dist/internal/zone-graph.js.map +1 -0
- package/dist/isr.d.ts +83 -6
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +263 -36
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +31 -6
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +220 -13
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +394 -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 +80 -0
- package/dist/plugins/zone-guard.js.map +1 -0
- package/dist/prerender.d.ts +10 -0
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +35 -3
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +9 -0
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +67 -9
- package/dist/public-dir.js.map +1 -1
- package/dist/pwa-manifest.d.ts +68 -0
- package/dist/pwa-manifest.d.ts.map +1 -0
- package/dist/pwa-manifest.js +178 -0
- package/dist/pwa-manifest.js.map +1 -0
- package/dist/query.js +4 -4
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +42 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +6 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-manifest.d.ts +3 -0
- package/dist/route-manifest.d.ts.map +1 -1
- package/dist/route-manifest.js +26 -3
- package/dist/route-manifest.js.map +1 -1
- package/dist/route-types.d.ts +24 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +195 -0
- package/dist/route-types.js.map +1 -0
- package/dist/router.d.ts +28 -6
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +129 -40
- package/dist/router.js.map +1 -1
- package/dist/vitals.d.ts +33 -0
- package/dist/vitals.d.ts.map +1 -0
- package/dist/vitals.js +16 -0
- package/dist/vitals.js.map +1 -0
- package/dist/vite.d.ts +16 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +248 -43
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +70 -0
- package/dist/zones.d.ts.map +1 -0
- package/dist/zones.js +365 -0
- package/dist/zones.js.map +1 -0
- package/package.json +67 -14
- package/src/backend-only.ts +16 -0
- package/src/boundary.ts +18 -5
- package/src/build-plan.ts +90 -35
- package/src/build-vite.ts +77 -11
- package/src/build.ts +466 -96
- package/src/bun-dev-entry.ts +1 -1
- package/src/cdn.ts +216 -0
- package/src/client.ts +135 -36
- package/src/conformance.ts +16 -3
- package/src/csp.ts +194 -0
- package/src/dev-client.ts +349 -0
- package/src/dev-diagnostics.ts +60 -7
- package/src/dev-error.ts +49 -5
- package/src/dev-feed.ts +1140 -0
- package/src/dev-indicator.ts +113 -0
- package/src/dev-session.ts +729 -0
- package/src/dev-sourcemap.ts +281 -0
- package/src/dev-ssr-graph.ts +16 -5
- package/src/dev.ts +86 -43
- package/src/diagnostic-catalog.ts +243 -0
- package/src/diagnostic-prompt.ts +355 -0
- package/src/diagnostic.ts +63 -66
- package/src/draft.ts +4 -1
- package/src/fn.ts +19 -3
- package/src/fs.ts +13 -2
- package/src/index.ts +42 -10
- package/src/internal/cdn/cloudflare.ts +146 -0
- package/src/internal/cdn/core.ts +316 -0
- package/src/internal/cdn/fastly.ts +62 -0
- package/src/internal/cdn/vercel.ts +95 -0
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/codegen.ts +161 -24
- package/src/internal/dev-csp.ts +176 -0
- package/src/internal/dev-reserved.ts +52 -0
- package/src/internal/html-spans.ts +50 -0
- package/src/internal/loading-runtime.ts +244 -0
- package/src/internal/matches-runtime.ts +62 -0
- package/src/internal/mount-shadow.ts +88 -0
- package/src/internal/node-builtins.ts +79 -0
- package/src/internal/output-guard.ts +701 -0
- package/src/internal/page-execution.ts +795 -185
- package/src/internal/parity.ts +150 -36
- package/src/internal/private-env.ts +334 -0
- package/src/internal/public-url.ts +22 -0
- package/src/internal/render-document.ts +114 -67
- package/src/internal/response-controls.ts +284 -0
- package/src/internal/runtime-contract.ts +10 -15
- package/src/internal/secret-scan.ts +638 -0
- package/src/internal/server-boundary.ts +6 -69
- package/src/internal/source-map.ts +256 -0
- package/src/internal/target-compat.ts +92 -0
- package/src/internal/timing-safe-equal.ts +13 -0
- package/src/internal/vitals-runtime.ts +53 -0
- package/src/internal/web-app.ts +262 -84
- package/src/internal/zone-graph.ts +593 -0
- package/src/isr.ts +325 -37
- package/src/llms-txt.ts +28 -4
- package/src/manifest.ts +682 -16
- package/src/module-graph.ts +53 -9
- package/src/navigation.ts +29 -6
- package/src/nonce.ts +104 -0
- package/src/plugins/css-modules.ts +5 -3
- package/src/plugins/kit.ts +24 -0
- package/src/plugins/svg.ts +135 -4
- package/src/plugins/vite-leak-guard.ts +529 -16
- package/src/plugins/vite-zone-guard.ts +263 -0
- package/src/plugins/zone-guard.ts +119 -0
- package/src/prerender.ts +49 -3
- package/src/public-dir.ts +69 -8
- package/src/pwa-manifest.ts +226 -0
- package/src/query.ts +4 -4
- package/src/render-seam.ts +46 -1
- package/src/route-manifest.ts +15 -3
- package/src/route-types.ts +264 -0
- package/src/router.ts +163 -63
- package/src/vitals.ts +44 -0
- package/src/vite.ts +257 -42
- 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,6 +31,7 @@ import {
|
|
|
30
31
|
detectServerOnlyInClient,
|
|
31
32
|
formatNodeBuiltinLeak,
|
|
32
33
|
formatServerOnlyLeak,
|
|
34
|
+
parseBuildTarget,
|
|
33
35
|
parseManifestClientEntry,
|
|
34
36
|
parseManifestCssLoading,
|
|
35
37
|
parseManifestRouteStyles,
|
|
@@ -48,16 +50,42 @@ import {
|
|
|
48
50
|
assertIdentityParity,
|
|
49
51
|
collectDevelopmentParityInput,
|
|
50
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"
|
|
51
67
|
import {
|
|
52
68
|
generateServerFnStub,
|
|
53
69
|
SERVER_FN_MODULE,
|
|
54
|
-
SERVER_ONLY_MODULE,
|
|
55
|
-
SERVER_ONLY_REPLACEMENT,
|
|
56
70
|
serverFnNamespace,
|
|
57
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"
|
|
58
85
|
// `buildTarget(static)` drives the SSG prerender engine directly (it's also re-exported below).
|
|
59
|
-
import { fromBunMetafile } from "./module-graph.ts"
|
|
60
86
|
import { prerenderRoutes } from "./prerender.ts"
|
|
87
|
+
import { CONTENT_TYPES, IMMUTABLE, ONE_DAY } from "./public-dir.ts"
|
|
88
|
+
import { createZoneClassifier } from "./zones.ts"
|
|
61
89
|
|
|
62
90
|
export * from "./build-plan.ts"
|
|
63
91
|
|
|
@@ -82,6 +110,8 @@ interface BunMetafile {
|
|
|
82
110
|
>
|
|
83
111
|
}
|
|
84
112
|
|
|
113
|
+
// The secret scan every client build, `public/` copy and prerender runs.
|
|
114
|
+
export type { SecretExemption, SecretRule } from "./internal/secret-scan.ts"
|
|
85
115
|
// Build-time SSG: prerender opted-in static + dynamic routes to `index.html` (+ static `_data.json`),
|
|
86
116
|
// run after `buildClient`.
|
|
87
117
|
export {
|
|
@@ -135,6 +165,12 @@ export interface BuildClientOptions {
|
|
|
135
165
|
* wins over an auto-exposed var (it's layered last). Sourced from `Bun.env` (falls back to
|
|
136
166
|
* `process.env`) at build time. */
|
|
137
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[]
|
|
138
174
|
}
|
|
139
175
|
|
|
140
176
|
/**
|
|
@@ -159,22 +195,6 @@ export function publicEnvDefines(
|
|
|
159
195
|
return defines
|
|
160
196
|
}
|
|
161
197
|
|
|
162
|
-
/**
|
|
163
|
-
* Percent-encode one path segment exactly the way a browser encodes it into a request URL.
|
|
164
|
-
*
|
|
165
|
-
* NOT `encodeURIComponent`. That escapes the sub-delimiters `, @ + = & ; $`, which a browser sends raw -
|
|
166
|
-
* so a file named `report,2026.csv` would be recorded as `/report%2C2026.csv` while the request arrives
|
|
167
|
-
* as `/report,2026.csv`, the allowlist lookup misses, and the file 404s in production only. `encodeURI`
|
|
168
|
-
* agrees with `URL.pathname` on every character except `?` and `#`, which terminate a path and so must
|
|
169
|
-
* be escaped explicitly here.
|
|
170
|
-
*/
|
|
171
|
-
function encodePathSegment(segment: string): string {
|
|
172
|
-
return encodeURI(segment).replace(
|
|
173
|
-
/[?#]/g,
|
|
174
|
-
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
175
|
-
)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
198
|
/**
|
|
179
199
|
* Cloudflare Pages rejects a `_routes.json` with more than 100 include+exclude rules combined, or any
|
|
180
200
|
* single rule longer than 100 characters.
|
|
@@ -247,7 +267,7 @@ function collapsibleDirs(
|
|
|
247
267
|
}
|
|
248
268
|
|
|
249
269
|
/**
|
|
250
|
-
* Build the
|
|
270
|
+
* Build the cloudflare `_routes.json` rules for a set of copied public files, within Cloudflare's budget.
|
|
251
271
|
*
|
|
252
272
|
* `exclude` is what Pages serves straight from the CDN instead of invoking the worker, so naming every
|
|
253
273
|
* public file is ideal - and impossible past ~99 of them. A `public/` of icons, fonts and share images
|
|
@@ -334,7 +354,7 @@ export async function copyPublicDir(from: string, to: string): Promise<string[]>
|
|
|
334
354
|
const target = join(to, rel)
|
|
335
355
|
await mkdir(join(target, ".."), { recursive: true })
|
|
336
356
|
await cp(candidateReal, target)
|
|
337
|
-
copied.push(
|
|
357
|
+
copied.push(publicUrlPath(rel))
|
|
338
358
|
}
|
|
339
359
|
return copied.sort()
|
|
340
360
|
}
|
|
@@ -411,6 +431,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
411
431
|
// layered after these in the `define` object). `Bun` may be absent under non-Bun typecheck - guard it.
|
|
412
432
|
const buildEnv = (typeof Bun !== "undefined" ? Bun.env : undefined) ?? process.env
|
|
413
433
|
const publicDefines = publicEnvDefines(options.publicEnvPrefix ?? "PUBLIC_", buildEnv)
|
|
434
|
+
const privateEnv = privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_")
|
|
414
435
|
// Keep the generated source beside the project, not inside `outDir`: module resolution starts at the
|
|
415
436
|
// importing file, so an absolute `--out /tmp/deploy` must still resolve the app's dependencies.
|
|
416
437
|
// A unique directory also avoids clobbering a user file or colliding with parallel builds.
|
|
@@ -432,6 +453,10 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
432
453
|
...routeManifest.routes.map((r) => r.file),
|
|
433
454
|
...Object.values(routeManifest.layouts).map((l) => l.file),
|
|
434
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),
|
|
435
460
|
]),
|
|
436
461
|
].sort()
|
|
437
462
|
|
|
@@ -440,12 +465,14 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
440
465
|
// entry→CSS link for per-route splitting: keyed by the unique source path, so it survives
|
|
441
466
|
// same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. Not yet in
|
|
442
467
|
// `@types/bun`'s `BuildConfig`, so spread it in (spread props skip the excess-property check).
|
|
443
|
-
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.
|
|
444
472
|
const result = await (async () => {
|
|
445
473
|
try {
|
|
446
474
|
return await Bun.build({
|
|
447
475
|
entrypoints: [entryFile, ...routeFiles.map(resolve)],
|
|
448
|
-
outdir: outDir,
|
|
449
476
|
target: "browser",
|
|
450
477
|
naming: "[name]-[hash].[ext]",
|
|
451
478
|
publicPath,
|
|
@@ -456,11 +483,16 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
456
483
|
...buildExtras,
|
|
457
484
|
minify: options.minify ?? true,
|
|
458
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
|
+
}),
|
|
459
492
|
...declaredSingleCopyPlugins(root),
|
|
460
493
|
reactDedupePlugin(routesDir),
|
|
461
494
|
preactDedupePlugin(routesDir),
|
|
462
495
|
svelteDedupePlugin(routesDir),
|
|
463
|
-
serverOnlyEmptyPlugin(),
|
|
464
496
|
serverFnStubPlugin(),
|
|
465
497
|
...(options.plugins ?? []),
|
|
466
498
|
],
|
|
@@ -468,10 +500,11 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
468
500
|
// Replace `process.env.*` at compile time so an app module reading config off `process.env` doesn't
|
|
469
501
|
// hit a `process is not defined` crash in the browser. Bun does longest-match: NODE_ENV resolves to
|
|
470
502
|
// the build mode (React's prod/dev branch); each PUBLIC_* var resolves to its baked VALUE; every
|
|
471
|
-
// other `process.env.X` becomes undefined (the bare `process.env` → `
|
|
472
|
-
// 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.
|
|
473
506
|
define: {
|
|
474
|
-
"process.env": "
|
|
507
|
+
"process.env": "{}",
|
|
475
508
|
"process.env.NODE_ENV": JSON.stringify(mode),
|
|
476
509
|
...publicDefines,
|
|
477
510
|
...options.define,
|
|
@@ -481,40 +514,125 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
481
514
|
rmSync(entryDir, { recursive: true, force: true })
|
|
482
515
|
}
|
|
483
516
|
})()
|
|
517
|
+
const cwd = process.cwd()
|
|
518
|
+
const clientMeta = (result as unknown as { metafile?: BunMetafile }).metafile
|
|
484
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))
|
|
485
535
|
throw new Error(
|
|
486
536
|
`[nifra/web] client build failed:\n${result.logs.map((l) => String(l)).join("\n")}`,
|
|
487
537
|
)
|
|
488
538
|
}
|
|
489
539
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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)
|
|
497
577
|
// #4: a `node:` builtin (e.g. `node:crypto`) pulled into a CLIENT chunk builds fine (Bun substitutes a
|
|
498
578
|
// browser polyfill) but breaks/leaks at runtime. Fail with the chain (entry → … → builtin), through the
|
|
499
579
|
// SHARED formatter so the Vite pipeline's identical guard reads byte-for-byte the same.
|
|
500
580
|
const nodeBuiltinLeak = formatNodeBuiltinLeak(detectNodeBuiltinsInClient(clientGraph))
|
|
501
581
|
if (nodeBuiltinLeak !== undefined) throw new Error(nodeBuiltinLeak)
|
|
502
582
|
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
// `*.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.
|
|
506
585
|
const serverOnlyLeak = formatServerOnlyLeak(detectServerOnlyInClient(clientGraph))
|
|
507
586
|
if (serverOnlyLeak !== undefined) throw new Error(serverOnlyLeak)
|
|
508
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
|
+
|
|
509
627
|
// Rename any chunk whose basename isn't URL-safe (dynamic-route files become `[slug]-hash.js`) and
|
|
510
628
|
// rewrite the references - otherwise the lazy import 404s and the route silently never hydrates.
|
|
511
|
-
const renamed = sanitizeOutputNames(
|
|
629
|
+
const renamed = sanitizeOutputNames(outputs)
|
|
512
630
|
const toUrl = (path: string): string =>
|
|
513
631
|
`${publicPath}${renamed.get(basename(path)) ?? basename(path)}`
|
|
514
632
|
// Entry-point outputs come back in entrypoint order: [bootstrap, ...routeFiles]. Map each route file
|
|
515
633
|
// to its chunk URL by that order (guarded against drift), then a route's chunks = its layout chain +
|
|
516
634
|
// own file.
|
|
517
|
-
const entryPoints =
|
|
635
|
+
const entryPoints = outputs.filter((o) => o.kind === "entry-point")
|
|
518
636
|
const bootstrap = entryPoints[0]
|
|
519
637
|
if (bootstrap === undefined) throw new Error("[nifra/web] build produced no entry-point output")
|
|
520
638
|
if (entryPoints.length !== routeFiles.length + 1) {
|
|
@@ -553,7 +671,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
553
671
|
// per-route `cssBundle` outputs follow. Dropping those per-route outputs (the old `aggregate`-only
|
|
554
672
|
// shape) left them in `manifest.assets` but not `css`, so a route-scoped stylesheet normalized to
|
|
555
673
|
// `asset:css` and tripped the module-graph parity contract - a bundler-dependent divergence.
|
|
556
|
-
const cssAssets =
|
|
674
|
+
const cssAssets = outputs.filter((o) => o.kind === "asset" && o.path.endsWith(".css"))
|
|
557
675
|
const css: readonly string[] = [
|
|
558
676
|
...cssAssets.filter((o) => cssNameOf(o.path) === bootstrapName),
|
|
559
677
|
...cssAssets.filter((o) => cssNameOf(o.path) !== bootstrapName),
|
|
@@ -565,7 +683,6 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
565
683
|
// same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. A page then
|
|
566
684
|
// links only its layout chain + own CSS (deduped); an empty array means the page needs no CSS at all.
|
|
567
685
|
// Absent (→ aggregate fallback) only if Bun emits no metafile/cssBundle - never silently incomplete.
|
|
568
|
-
const cwd = process.cwd()
|
|
569
686
|
const cssByEntry = new Map<string, string>()
|
|
570
687
|
for (const out of Object.values(clientMeta?.outputs ?? {})) {
|
|
571
688
|
if (out.entryPoint !== undefined && out.cssBundle !== undefined) {
|
|
@@ -585,24 +702,35 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
585
702
|
// SSR pages unstyled. Keep routeStyles absent until at least one authored route entry owns CSS.
|
|
586
703
|
const routeCssEntries = [...cssByEntry.keys()].filter((entry) => entry !== resolvePath(entryFile))
|
|
587
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)
|
|
588
708
|
for (const route of routeManifest.routes) {
|
|
589
709
|
routeStyles[route.id] = stylesFor([
|
|
590
710
|
...route.layoutIds.map((id) => routeManifest.layouts[id]?.file ?? ""),
|
|
591
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,
|
|
592
722
|
])
|
|
593
723
|
}
|
|
594
|
-
if (routeManifest.notFound) routeStyles._404 = stylesFor([routeManifest.notFound.file])
|
|
595
724
|
}
|
|
596
725
|
|
|
597
726
|
// Copy `public/` into the output next to the hashed assets. A missing directory is normal (most
|
|
598
727
|
// apps have none) and must not fail the build.
|
|
599
|
-
const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public")
|
|
600
728
|
const publicFiles =
|
|
601
729
|
publicDir !== undefined && existsSync(publicDir) ? await copyPublicDir(publicDir, outDir) : []
|
|
602
730
|
|
|
603
731
|
const manifest: BuildManifest = {
|
|
604
732
|
entry: toUrl(bootstrap.path),
|
|
605
|
-
assets:
|
|
733
|
+
assets: outputs.map((o) => toUrl(o.path)),
|
|
606
734
|
routes,
|
|
607
735
|
...(publicFiles.length > 0 ? { publicFiles } : {}),
|
|
608
736
|
...(css.length > 0 ? { css } : {}),
|
|
@@ -661,6 +789,14 @@ export interface BuildServerOptions {
|
|
|
661
789
|
* - on Cloudflare, ship them with wrangler's `no_bundle` + `find_additional_modules` + an ESModule
|
|
662
790
|
* `rule` (Node/Deno import the chunks natively). Eager (one self-contained file) stays the default. */
|
|
663
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
|
|
664
800
|
}
|
|
665
801
|
|
|
666
802
|
/** The built worker bundle - point your `wrangler.toml`'s `main` at `worker`. */
|
|
@@ -751,12 +887,55 @@ const SVELTE_DEDUPE_PATTERN = dedupePolicyFor("svelte").bunPattern ?? /^svelte($
|
|
|
751
887
|
* fixed subpath list), Svelte has many internal subpaths, so each matched import is resolved dynamically.
|
|
752
888
|
* `svelte/compiler` (build-time only, not in the bundle) doesn't match the filter and is left alone. No-op
|
|
753
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.
|
|
754
896
|
*/
|
|
755
897
|
export const svelteDedupePlugin = (from: string): BunPlugin => ({
|
|
756
898
|
name: "nifra-svelte-dedupe",
|
|
757
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
|
+
}
|
|
758
929
|
build.onResolve({ filter: SVELTE_DEDUPE_PATTERN }, (args) => {
|
|
759
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
|
+
}
|
|
760
939
|
return { path: Bun.resolveSync(args.path, from) }
|
|
761
940
|
} catch {
|
|
762
941
|
return undefined // not resolvable from the app root - leave Bun's default resolution
|
|
@@ -765,6 +944,19 @@ export const svelteDedupePlugin = (from: string): BunPlugin => ({
|
|
|
765
944
|
},
|
|
766
945
|
})
|
|
767
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
|
+
|
|
768
960
|
/**
|
|
769
961
|
* The app's declared single-copy rule, applied to the bundle.
|
|
770
962
|
*
|
|
@@ -781,33 +973,60 @@ const declaredSingleCopyPlugins = (root: string): readonly BunPlugin[] =>
|
|
|
781
973
|
readSingleCopyDeclaration(root) === undefined ? [] : [declaredSingleCopyPlugin({ cwd: root })]
|
|
782
974
|
|
|
783
975
|
/**
|
|
784
|
-
*
|
|
785
|
-
* `
|
|
786
|
-
*
|
|
787
|
-
* import resolves to `undefined` rather than a "missing export" bundle error (verified), and the real
|
|
788
|
-
* import subtree is gone. The complement to the node-builtin guard: when a server-only import is co-located
|
|
789
|
-
* in a route file (so it can't be tree-shaken out and the guard fails loud), moving it into a `*.server`
|
|
790
|
-
* 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.
|
|
791
979
|
*/
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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"
|
|
801
1022
|
|
|
802
1023
|
/**
|
|
803
1024
|
* Server functions in the CLIENT build: replace each `*.fn.ts` module with stubs that call the routes
|
|
804
1025
|
* the server mounted, so the function bodies - and everything they import - never reach a browser.
|
|
805
1026
|
*
|
|
806
|
-
* The
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
* `internal/server-boundary.ts` so the Vite pipeline emits identical stubs from the same code; two
|
|
810
|
-
* 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.
|
|
811
1030
|
*
|
|
812
1031
|
* CLIENT-only. The server build keeps the real module, which is what `serverFunctions()` mounts.
|
|
813
1032
|
*/
|
|
@@ -886,6 +1105,7 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
886
1105
|
entrypoints: [serverEntry],
|
|
887
1106
|
outdir: outDir,
|
|
888
1107
|
target,
|
|
1108
|
+
metafile: true,
|
|
889
1109
|
conditions: [...conditions],
|
|
890
1110
|
define: {
|
|
891
1111
|
...(options.define ?? { "process.env.NODE_ENV": '"production"' }),
|
|
@@ -915,6 +1135,48 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
915
1135
|
`[nifra/web] server build failed:\n${result.logs.map((l) => String(l)).join("\n")}`,
|
|
916
1136
|
)
|
|
917
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
|
+
}
|
|
918
1180
|
const entryOutput = result.outputs.find((o) => o.kind === "entry-point")
|
|
919
1181
|
if (entryOutput === undefined) {
|
|
920
1182
|
throw new Error("[nifra/web] server build produced no entry-point output")
|
|
@@ -931,12 +1193,37 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
931
1193
|
// …) - so we GENERATE it here (per target) instead of asking each app to ship five near-identical files.
|
|
932
1194
|
// ===================================================================================================
|
|
933
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
|
+
|
|
934
1221
|
/**
|
|
935
1222
|
* Codegen the per-target **server entry** module (source text) for `buildServer` to bundle. It imports
|
|
936
1223
|
* the app's `adapter` (from `framework.ts`), the optional `backend` (from `backend.ts`), and the
|
|
937
1224
|
* generated `{ manifest, clientEntry }` (from `./server-manifest`), builds `createWebApp`, then wires
|
|
938
1225
|
* the right host:
|
|
939
|
-
* - `
|
|
1226
|
+
* - `cloudflare` / `vercel`: `export default` the fetch handler (the platform serves /assets/* itself).
|
|
940
1227
|
* - `deno`: same fetch-handler default, plus `Deno.serve` self-host when run directly.
|
|
941
1228
|
* - `bun` / `node`: a self-hosting server that ALSO serves the client bundle from disk (those
|
|
942
1229
|
* runtimes have a filesystem; the static `/assets/*` sit next to the entry).
|
|
@@ -953,19 +1240,42 @@ export function generateServerEntry(options: {
|
|
|
953
1240
|
/** Import specifier for the module exporting `use`, or `undefined`. Must resolve to the
|
|
954
1241
|
* same edge-safe module as `adapterImport`. */
|
|
955
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
|
|
956
1246
|
/** Document `<title>` passed to `createWebApp`. */
|
|
957
1247
|
readonly title?: string
|
|
958
1248
|
/** Encoded root-relative public file paths copied into the deploy directory. */
|
|
959
1249
|
readonly publicFiles?: readonly string[]
|
|
1250
|
+
/** Trust the edge platform's client-address header for `c.clientIp`. See {@link ServerEntryClientIp}. */
|
|
1251
|
+
readonly clientIp?: ServerEntryClientIp
|
|
960
1252
|
}): string {
|
|
961
1253
|
const {
|
|
962
1254
|
target,
|
|
963
1255
|
adapterImport,
|
|
964
1256
|
backendImport,
|
|
965
1257
|
useImport,
|
|
1258
|
+
optionImports = {},
|
|
966
1259
|
title = "nifra",
|
|
967
1260
|
publicFiles = [],
|
|
1261
|
+
clientIp,
|
|
968
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
|
+
}
|
|
969
1279
|
if (target === "static") {
|
|
970
1280
|
throw new Error("[nifra/web] generateServerEntry: `static` has no server entry (SSG only)")
|
|
971
1281
|
}
|
|
@@ -973,21 +1283,24 @@ export function generateServerEntry(options: {
|
|
|
973
1283
|
if (backendImport !== undefined) lines.push('import { inProcessClient } from "@nifrajs/client"')
|
|
974
1284
|
lines.push(`import { adapter } from ${JSON.stringify(adapterImport)}`)
|
|
975
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
|
+
}
|
|
976
1289
|
if (backendImport !== undefined) {
|
|
977
1290
|
lines.push(`import { backend } from ${JSON.stringify(backendImport)}`)
|
|
978
1291
|
}
|
|
979
1292
|
lines.push(
|
|
980
1293
|
'import { clientEntry, cssLoading, manifest, styles, routeStyles } from "./server-manifest"',
|
|
981
1294
|
)
|
|
982
|
-
//
|
|
983
|
-
|
|
984
|
-
if (usesToFetch) lines.push('import { toFetchHandler } from "@nifrajs/core/server"')
|
|
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"')
|
|
985
1297
|
if (target === "node") lines.push('import { serve } from "@nifrajs/node"')
|
|
986
1298
|
lines.push(
|
|
987
1299
|
"",
|
|
988
1300
|
"const app = createWebApp({",
|
|
989
1301
|
" adapter,",
|
|
990
1302
|
...(useImport !== undefined ? [" use,"] : []),
|
|
1303
|
+
...importedOptions.map((name) => ` ${name},`),
|
|
991
1304
|
" manifest,",
|
|
992
1305
|
" clientEntry,",
|
|
993
1306
|
" styles,",
|
|
@@ -995,11 +1308,15 @@ export function generateServerEntry(options: {
|
|
|
995
1308
|
" cssLoading,",
|
|
996
1309
|
...(backendImport !== undefined ? [" api: inProcessClient(backend),"] : []),
|
|
997
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
|
+
: []),
|
|
998
1315
|
"})",
|
|
999
1316
|
"",
|
|
1000
1317
|
)
|
|
1001
1318
|
|
|
1002
|
-
if (target === "
|
|
1319
|
+
if (target === "cloudflare") {
|
|
1003
1320
|
// Cloudflare Pages advanced mode: `_routes.json` keeps static paths off the worker entirely, and
|
|
1004
1321
|
// everything else falls through to this handler (SSR).
|
|
1005
1322
|
//
|
|
@@ -1049,22 +1366,34 @@ export function generateServerEntry(options: {
|
|
|
1049
1366
|
' segment !== "." && segment !== ".." && /^[A-Za-z0-9._-]+$/.test(segment)',
|
|
1050
1367
|
" return segments.every(safe) ? '.' + pathname : undefined",
|
|
1051
1368
|
"}",
|
|
1052
|
-
|
|
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
|
+
"})",
|
|
1053
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'
|
|
1054
1382
|
if (target === "bun") {
|
|
1055
1383
|
lines.push(
|
|
1056
1384
|
"const server = Bun.serve({",
|
|
1057
1385
|
" port: Number(Bun.env.PORT ?? 3000),",
|
|
1058
|
-
" async fetch(req) {",
|
|
1386
|
+
" async fetch(req, server) {",
|
|
1059
1387
|
" const { pathname } = new URL(req.url)",
|
|
1060
1388
|
" const filePath = staticPath(pathname)",
|
|
1061
1389
|
" if (filePath !== undefined) {",
|
|
1062
1390
|
" const file = Bun.file(new URL(filePath, STATIC_ROOT))",
|
|
1063
1391
|
' if (!(await file.exists())) return new Response("not found", { status: 404 })',
|
|
1064
|
-
|
|
1065
|
-
' return new Response(file, { headers: { "content-type": TYPES[ext] ?? "application/octet-stream" } })',
|
|
1392
|
+
" return new Response(file, { headers: staticHeaders(pathname) })",
|
|
1066
1393
|
" }",
|
|
1067
|
-
|
|
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 } })",
|
|
1068
1397
|
" },",
|
|
1069
1398
|
"})",
|
|
1070
1399
|
// The `${...}` here is literal OUTPUT (a template in the GENERATED file), not a template in this
|
|
@@ -1078,19 +1407,18 @@ export function generateServerEntry(options: {
|
|
|
1078
1407
|
'import { readFile } from "node:fs/promises"',
|
|
1079
1408
|
"await serve(",
|
|
1080
1409
|
" {",
|
|
1081
|
-
" async fetch(req) {",
|
|
1410
|
+
" async fetch(req, platform) {",
|
|
1082
1411
|
" const { pathname } = new URL(req.url)",
|
|
1083
1412
|
" const filePath = staticPath(pathname)",
|
|
1084
1413
|
" if (filePath !== undefined) {",
|
|
1085
1414
|
" try {",
|
|
1086
1415
|
" const body = await readFile(new URL(filePath, STATIC_ROOT))",
|
|
1087
|
-
|
|
1088
|
-
' return new Response(body, { headers: { "content-type": TYPES[ext] ?? "application/octet-stream" } })',
|
|
1416
|
+
" return new Response(body, { headers: staticHeaders(pathname) })",
|
|
1089
1417
|
" } catch {",
|
|
1090
1418
|
' return new Response("not found", { status: 404 })',
|
|
1091
1419
|
" }",
|
|
1092
1420
|
" }",
|
|
1093
|
-
" return app.fetch(req)",
|
|
1421
|
+
" return app.fetch(req, platform)",
|
|
1094
1422
|
" },",
|
|
1095
1423
|
" },",
|
|
1096
1424
|
" { port: Number(process.env.PORT ?? 3000) },",
|
|
@@ -1100,22 +1428,21 @@ export function generateServerEntry(options: {
|
|
|
1100
1428
|
}
|
|
1101
1429
|
// deno
|
|
1102
1430
|
lines.push(
|
|
1103
|
-
"const handler = toFetchHandler(app)",
|
|
1104
1431
|
"// @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1105
|
-
'Deno.serve({ port: Number(Deno.env.get("PORT") ?? "3000") }, async (req) => {',
|
|
1432
|
+
'Deno.serve({ port: Number(Deno.env.get("PORT") ?? "3000") }, async (req, info) => {',
|
|
1106
1433
|
" const { pathname } = new URL(req.url)",
|
|
1107
1434
|
" const filePath = staticPath(pathname)",
|
|
1108
1435
|
" if (filePath !== undefined) {",
|
|
1109
1436
|
" try {",
|
|
1110
1437
|
" // @ts-ignore - Deno.readFile is present on the Deno runtime.",
|
|
1111
1438
|
" const body = await Deno.readFile(new URL(filePath, STATIC_ROOT))",
|
|
1112
|
-
|
|
1113
|
-
' return new Response(body, { headers: { "content-type": TYPES[ext] ?? "application/octet-stream" } })',
|
|
1439
|
+
" return new Response(body, { headers: staticHeaders(pathname) })",
|
|
1114
1440
|
" } catch {",
|
|
1115
1441
|
' return new Response("not found", { status: 404 })',
|
|
1116
1442
|
" }",
|
|
1117
1443
|
" }",
|
|
1118
|
-
|
|
1444
|
+
` ${markFramed}`,
|
|
1445
|
+
" return app.fetch(req, { clientIp: info.remoteAddr.hostname })",
|
|
1119
1446
|
"})",
|
|
1120
1447
|
)
|
|
1121
1448
|
return `${lines.join("\n")}\n`
|
|
@@ -1155,6 +1482,9 @@ export interface BuildTargetOptions {
|
|
|
1155
1482
|
* applied before page routes are declared), or `undefined`. Must resolve to the same edge-safe
|
|
1156
1483
|
* module as `adapterImport`. */
|
|
1157
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
|
|
1158
1488
|
/** Factory that builds the app for `static` prerendering, GIVEN the client build's manifest - so the
|
|
1159
1489
|
* emitted hydration `<script src>` uses the REAL content-hashed entry (`client.entry`) plus the same
|
|
1160
1490
|
* styles/route-preload the server targets use. A pre-built instance can't work here: the hash isn't known
|
|
@@ -1173,6 +1503,8 @@ export interface BuildTargetOptions {
|
|
|
1173
1503
|
readonly publicDir?: string | false
|
|
1174
1504
|
/** Prefix of environment variables allowed into the client bundle (default `"PUBLIC_"`). */
|
|
1175
1505
|
readonly publicEnvPrefix?: string
|
|
1506
|
+
/** Reviewed false positives of the secret scan over the bundle, `public/` and prerendered pages. */
|
|
1507
|
+
readonly secretExemptions?: readonly SecretExemption[]
|
|
1176
1508
|
/**
|
|
1177
1509
|
* Vite-only CSS output policy. `false` emits one aggregate stylesheet. The native Bun build does not
|
|
1178
1510
|
* support this switch and fails closed if it is supplied instead of silently ignoring it.
|
|
@@ -1187,6 +1519,9 @@ export interface BuildTargetOptions {
|
|
|
1187
1519
|
readonly cssLoading?: CssLoadingMode
|
|
1188
1520
|
/** Document `<title>` for the generated server entry. */
|
|
1189
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
|
|
1190
1525
|
}
|
|
1191
1526
|
|
|
1192
1527
|
/** Minimal app surface `buildTarget`'s static path needs - a fetch handler (a built `createWebApp`). */
|
|
@@ -1214,15 +1549,13 @@ export interface BuildTargetResult {
|
|
|
1214
1549
|
* `<outDir>/assets/*`, then per target:
|
|
1215
1550
|
* - `static`: prerenders opted-in routes (`prerenderRoutes`) to `<outDir>/<path>/index.html` (+
|
|
1216
1551
|
* `_data.json`); needs `prerenderApp`. No server.
|
|
1217
|
-
* - `
|
|
1218
|
-
* - `vercel`:
|
|
1219
|
-
*
|
|
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`.
|
|
1220
1556
|
* - `deno`/`node`/`bun`: the self-hosting server bundle (`server.js`) next to the assets.
|
|
1221
1557
|
* The server entry is GENERATED (`generateServerEntry`) and bundled (`buildServer`); the app supplies
|
|
1222
1558
|
* only adapter/backend/routes. Returns the manifest + a size report. Throws on any build failure.
|
|
1223
|
-
*
|
|
1224
|
-
* Note: the heavier platform wrappers (`.vercel/output` v3 layout, wrangler ISR `find_additional_modules`)
|
|
1225
|
-
* remain app-owned scripts; this command targets the common single-bundle deploys. See the CLI docs.
|
|
1226
1559
|
*/
|
|
1227
1560
|
/** The default (Bun) strategy - `buildClient`/`buildServer` from this module. */
|
|
1228
1561
|
export const bunBundler: Bundler = {
|
|
@@ -1243,6 +1576,7 @@ export const bunBundler: Bundler = {
|
|
|
1243
1576
|
...(input.cssCodeSplit !== undefined ? { cssCodeSplit } : {}),
|
|
1244
1577
|
...(input.publicDir !== undefined ? { publicDir: input.publicDir } : {}),
|
|
1245
1578
|
...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
|
|
1579
|
+
...(input.secretExemptions !== undefined ? { secretExemptions: input.secretExemptions } : {}),
|
|
1246
1580
|
})
|
|
1247
1581
|
},
|
|
1248
1582
|
buildServer: (input) =>
|
|
@@ -1257,6 +1591,9 @@ export const bunBundler: Bundler = {
|
|
|
1257
1591
|
...(input.plugins ? { plugins: input.plugins as BunPlugin[] } : {}),
|
|
1258
1592
|
...(input.define ? { define: input.define } : {}),
|
|
1259
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 } : {}),
|
|
1260
1597
|
}),
|
|
1261
1598
|
}
|
|
1262
1599
|
|
|
@@ -1284,15 +1621,16 @@ export async function buildTargetWith(
|
|
|
1284
1621
|
options.cssCodeSplit === undefined ? undefined : normalizeCssCodeSplit(options.cssCodeSplit)
|
|
1285
1622
|
const requestedCssLoading =
|
|
1286
1623
|
options.cssLoading === undefined ? undefined : normalizeCssLoading(options.cssLoading)
|
|
1287
|
-
const targetPlan = planBuildTarget(target, outDir)
|
|
1624
|
+
const targetPlan = planBuildTarget(parseBuildTarget(target), outDir)
|
|
1288
1625
|
const { rmSync } = await import("node:fs")
|
|
1289
1626
|
rmSync(outDir, { recursive: true, force: true })
|
|
1290
1627
|
rmSync(workDir, { recursive: true, force: true })
|
|
1291
|
-
const
|
|
1628
|
+
const staticRoot = targetPlan.staticDir === "" ? outDir : `${outDir}/${targetPlan.staticDir}`
|
|
1629
|
+
const assetsDir = `${staticRoot}/assets`
|
|
1292
1630
|
mkdirSync(assetsDir, { recursive: true })
|
|
1293
1631
|
mkdirSync(workDir, { recursive: true })
|
|
1294
1632
|
|
|
1295
|
-
// (1) Client bundle → <
|
|
1633
|
+
// (1) Client bundle → <static root>/assets/* (every target ships the same hashed client bundle).
|
|
1296
1634
|
let client = await bundler.buildClient({
|
|
1297
1635
|
routesDir,
|
|
1298
1636
|
outDir: assetsDir,
|
|
@@ -1302,6 +1640,9 @@ export async function buildTargetWith(
|
|
|
1302
1640
|
define: { "process.env.NODE_ENV": '"production"', ...(options.define ?? {}) },
|
|
1303
1641
|
publicDir: false,
|
|
1304
1642
|
...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
|
|
1643
|
+
...(options.secretExemptions !== undefined
|
|
1644
|
+
? { secretExemptions: options.secretExemptions }
|
|
1645
|
+
: {}),
|
|
1305
1646
|
...(requestedCssCodeSplit !== undefined ? { cssCodeSplit: requestedCssCodeSplit } : {}),
|
|
1306
1647
|
...(requestedCssLoading !== undefined ? { cssLoading: requestedCssLoading } : {}),
|
|
1307
1648
|
root: resolvePath(dirname(routesDir)),
|
|
@@ -1316,8 +1657,21 @@ export async function buildTargetWith(
|
|
|
1316
1657
|
options.publicDir === false
|
|
1317
1658
|
? undefined
|
|
1318
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
|
+
}
|
|
1319
1671
|
const publicFiles =
|
|
1320
|
-
publicDir !== undefined && existsSync(publicDir)
|
|
1672
|
+
publicDir !== undefined && existsSync(publicDir)
|
|
1673
|
+
? await copyPublicDir(publicDir, staticRoot)
|
|
1674
|
+
: []
|
|
1321
1675
|
if (publicFiles.length > 0) {
|
|
1322
1676
|
client = { ...client, publicFiles }
|
|
1323
1677
|
}
|
|
@@ -1343,6 +1697,7 @@ export async function buildTargetWith(
|
|
|
1343
1697
|
app,
|
|
1344
1698
|
routes: manifest.routes,
|
|
1345
1699
|
outDir,
|
|
1700
|
+
secrets: secretScan,
|
|
1346
1701
|
})
|
|
1347
1702
|
if (result.prerendered.length === 0) {
|
|
1348
1703
|
// A static build that renders nothing is almost always a misconfig (no `prerender = true` / no
|
|
@@ -1375,9 +1730,11 @@ export async function buildTargetWith(
|
|
|
1375
1730
|
target,
|
|
1376
1731
|
adapterImport: options.adapterImport,
|
|
1377
1732
|
...(options.useImport !== undefined ? { useImport: options.useImport } : {}),
|
|
1733
|
+
...(options.optionImports !== undefined ? { optionImports: options.optionImports } : {}),
|
|
1378
1734
|
...(options.backendImport !== undefined ? { backendImport: options.backendImport } : {}),
|
|
1379
1735
|
...(options.title !== undefined ? { title: options.title } : {}),
|
|
1380
1736
|
...(publicFiles.length > 0 ? { publicFiles } : {}),
|
|
1737
|
+
...(options.clientIp !== undefined ? { clientIp: options.clientIp } : {}),
|
|
1381
1738
|
}),
|
|
1382
1739
|
)
|
|
1383
1740
|
const { worker } = await bundler.buildServer({
|
|
@@ -1396,11 +1753,13 @@ export async function buildTargetWith(
|
|
|
1396
1753
|
? { cssLoading: requestedCssLoading }
|
|
1397
1754
|
: {}),
|
|
1398
1755
|
root: resolvePath(dirname(routesDir)),
|
|
1756
|
+
generatedFiles: [resolvePath(workDir, options.adapterImport)],
|
|
1757
|
+
...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
|
|
1399
1758
|
})
|
|
1400
1759
|
|
|
1401
1760
|
// (3) Assemble the deploy dir for the target.
|
|
1402
1761
|
const { cpSync } = await import("node:fs")
|
|
1403
|
-
if (targetPlan.target === "
|
|
1762
|
+
if (targetPlan.target === "cloudflare") {
|
|
1404
1763
|
cpSync(worker, `${outDir}/${targetPlan.outputFile}`)
|
|
1405
1764
|
// The app's real patterns, so a directory is only collapsed into a glob once the route table
|
|
1406
1765
|
// proves nothing can be served beneath it.
|
|
@@ -1422,7 +1781,18 @@ export async function buildTargetWith(
|
|
|
1422
1781
|
)
|
|
1423
1782
|
}
|
|
1424
1783
|
} else if (targetPlan.target === "vercel") {
|
|
1784
|
+
const fn = dirname(`${outDir}/${targetPlan.outputFile}`)
|
|
1785
|
+
mkdirSync(fn, { recursive: true })
|
|
1425
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
|
+
)
|
|
1426
1796
|
} else {
|
|
1427
1797
|
cpSync(worker, `${outDir}/${targetPlan.outputFile}`)
|
|
1428
1798
|
}
|