@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/web/plugins/vite-leak-guard` - nifra's
|
|
2
|
+
* `@nifrajs/web/plugins/vite-leak-guard` - nifra's client-leak guards, for a Vite/Rollup production
|
|
3
3
|
* build.
|
|
4
4
|
*
|
|
5
5
|
* ## Why this exists
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* pipeline arriving WITHOUT them, or with a hastily re-implemented "mostly ported" copy, is the failure the
|
|
13
13
|
* neutral module-graph seam was built to prevent.
|
|
14
14
|
*
|
|
15
|
-
* So this is not a second implementation. The
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
15
|
+
* So this is not a second implementation. The zone verification (`verifyClientGraph`,
|
|
16
|
+
* `accountEmittedFiles`), the `node:` and `backend-only` detectors and their failure MESSAGES are the
|
|
17
|
+
* exact same functions the Bun build calls. This plugin only adapts Rollup's bundle shape into the
|
|
18
18
|
* neutral `ClientModuleGraph` (`fromRollupBundle`) and runs them. A leak reads identically whichever
|
|
19
19
|
* bundler produced it, and there is one place to change if a guard changes.
|
|
20
20
|
*
|
|
@@ -25,19 +25,57 @@
|
|
|
25
25
|
* `fromRollupBundle` needs. Findings fail the build through Rollup's `this.error`, so the message lands in
|
|
26
26
|
* the build output the same way a Bun-build throw does.
|
|
27
27
|
*
|
|
28
|
-
* Add it to the `plugins` of a Vite production build (the LAST plugin, so it sees the final graph)
|
|
28
|
+
* Add it to the `plugins` of a Vite production build (the LAST plugin, so it sees the final graph),
|
|
29
|
+
* with `viteBareBuiltinExternal()` first so a bare built-in such as `fs/promises` stays named:
|
|
29
30
|
*
|
|
30
31
|
* // vite.config.ts (production client build)
|
|
31
|
-
* import { viteLeakGuard } from "@nifrajs/web/plugins/vite-leak-guard"
|
|
32
|
-
* export default {
|
|
32
|
+
* import { viteBareBuiltinExternal, viteLeakGuard } from "@nifrajs/web/plugins/vite-leak-guard"
|
|
33
|
+
* export default {
|
|
34
|
+
* plugins: [viteBareBuiltinExternal(), viteAssetUrlGuard({ appRoot: import.meta.dirname })],
|
|
35
|
+
* build: {
|
|
36
|
+
* rollupOptions: { external: [/^node:/], plugins: [viteLeakGuard({ appRoot: import.meta.dirname })] },
|
|
37
|
+
* },
|
|
38
|
+
* }
|
|
33
39
|
*/
|
|
40
|
+
import { existsSync, statSync } from "node:fs"
|
|
41
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path"
|
|
34
42
|
import {
|
|
35
43
|
detectNodeBuiltinsInClient,
|
|
36
44
|
detectServerOnlyInClient,
|
|
37
45
|
formatNodeBuiltinLeak,
|
|
38
46
|
formatServerOnlyLeak,
|
|
39
|
-
} from "../build.ts"
|
|
40
|
-
import {
|
|
47
|
+
} from "../build-plan.ts"
|
|
48
|
+
import { isBareNodeBuiltin } from "../internal/node-builtins.ts"
|
|
49
|
+
import { privateEnvCheck } from "../internal/private-env.ts"
|
|
50
|
+
import {
|
|
51
|
+
emittedScanFiles,
|
|
52
|
+
formatSecretFindings,
|
|
53
|
+
graphScanInput,
|
|
54
|
+
originName,
|
|
55
|
+
publicScanFiles,
|
|
56
|
+
type SecretExemption,
|
|
57
|
+
type SecretScanFile,
|
|
58
|
+
scanForSecrets,
|
|
59
|
+
textOf,
|
|
60
|
+
} from "../internal/secret-scan.ts"
|
|
61
|
+
import {
|
|
62
|
+
accountEmittedFiles,
|
|
63
|
+
assetDenial,
|
|
64
|
+
type EmittedFile,
|
|
65
|
+
formatClientGraphVerdict,
|
|
66
|
+
formatServerGraphVerdict,
|
|
67
|
+
formatUnaccountedOutput,
|
|
68
|
+
rollupModuleSource,
|
|
69
|
+
verifyClientGraph,
|
|
70
|
+
verifyServerGraph,
|
|
71
|
+
} from "../internal/zone-graph.ts"
|
|
72
|
+
import {
|
|
73
|
+
type ClientModuleGraph,
|
|
74
|
+
fromRollupBundle,
|
|
75
|
+
type GraphModule,
|
|
76
|
+
type RollupBundleLike,
|
|
77
|
+
} from "../module-graph.ts"
|
|
78
|
+
import { createZoneClassifier, type ZoneClassifier } from "../zones.ts"
|
|
41
79
|
|
|
42
80
|
/**
|
|
43
81
|
* The Rollup plugin-context slice this uses: `getModuleInfo` for a module's resolved imports, and `error`
|
|
@@ -47,7 +85,10 @@ interface RollupPluginContext {
|
|
|
47
85
|
getModuleInfo(id: string): {
|
|
48
86
|
readonly importedIds?: readonly string[]
|
|
49
87
|
readonly dynamicallyImportedIds?: readonly string[]
|
|
88
|
+
readonly isExternal?: boolean
|
|
50
89
|
} | null
|
|
90
|
+
/** Every module the build loaded, including ones tree-shaking left out of every chunk. */
|
|
91
|
+
getModuleIds?(): Iterable<string>
|
|
51
92
|
/**
|
|
52
93
|
* Takes an `Error`, never a string.
|
|
53
94
|
*
|
|
@@ -60,6 +101,50 @@ interface RollupPluginContext {
|
|
|
60
101
|
error(error: Error): never
|
|
61
102
|
}
|
|
62
103
|
|
|
104
|
+
/** The parts of a Rollup output file the accounting reads. */
|
|
105
|
+
interface RollupOutputLike {
|
|
106
|
+
readonly type?: string
|
|
107
|
+
readonly fileName?: string
|
|
108
|
+
readonly code?: string
|
|
109
|
+
readonly source?: string | Uint8Array
|
|
110
|
+
readonly map?: { readonly sources?: readonly (string | null)[] } | null
|
|
111
|
+
readonly originalFileNames?: readonly string[]
|
|
112
|
+
readonly viteMetadata?: {
|
|
113
|
+
readonly importedCss?: ReadonlySet<string>
|
|
114
|
+
readonly importedAssets?: ReadonlySet<string>
|
|
115
|
+
}
|
|
116
|
+
readonly moduleIds?: readonly string[]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface LeakGuardOptions {
|
|
120
|
+
/** The app root: the directory holding `routes/`, `frontend/`, `backend/` and `shared/` (default
|
|
121
|
+
* the working directory). */
|
|
122
|
+
readonly appRoot?: string
|
|
123
|
+
/** The routes directory (default `<appRoot>/routes`). */
|
|
124
|
+
readonly routesDir?: string
|
|
125
|
+
/** Vite's `root`, which emitted asset names are relative to (default `appRoot`). */
|
|
126
|
+
readonly root?: string
|
|
127
|
+
/** Where the bundle is written; source map paths resolve against it (default `<root>/dist`). */
|
|
128
|
+
readonly outDir?: string
|
|
129
|
+
/** The build's own generated entry modules. */
|
|
130
|
+
readonly generatedFiles?: readonly string[]
|
|
131
|
+
/** Output files another guarded build already verified (a worker sub-build's chunks). */
|
|
132
|
+
readonly verified?: Set<string>
|
|
133
|
+
/** The public-env prefix browser code may read (default `"PUBLIC_"`; `""` exposes nothing). */
|
|
134
|
+
readonly publicEnvPrefix?: string
|
|
135
|
+
/** The secret scan of what the bundle publishes. */
|
|
136
|
+
readonly secrets?: LeakGuardSecretOptions
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface LeakGuardSecretOptions {
|
|
140
|
+
/** The build environment whose non-public values must not ship (default `process.env`). */
|
|
141
|
+
readonly env?: Readonly<Record<string, string | undefined>>
|
|
142
|
+
/** Reviewed false positives; each names its rule, its file (or env variable) and a reason. */
|
|
143
|
+
readonly exemptions?: readonly SecretExemption[]
|
|
144
|
+
/** A `public/` directory whose files ship beside the bundle, scanned with it. */
|
|
145
|
+
readonly publicDir?: string
|
|
146
|
+
}
|
|
147
|
+
|
|
63
148
|
/** The minimal Rollup plugin shape this returns - `generateBundle` bound to the plugin context. */
|
|
64
149
|
export interface LeakGuardPlugin {
|
|
65
150
|
readonly name: string
|
|
@@ -78,14 +163,22 @@ export interface LeakGuardPlugin {
|
|
|
78
163
|
generateBundle(this: RollupPluginContext, options: unknown, bundle: RollupBundleLike): void
|
|
79
164
|
}
|
|
80
165
|
|
|
166
|
+
const CSS_FILE = /\.(?:css|scss|sass|less|styl)(?:$|\?)/
|
|
167
|
+
|
|
81
168
|
/**
|
|
82
|
-
* A Vite/Rollup plugin that fails the build when
|
|
83
|
-
* bundle
|
|
169
|
+
* A Vite/Rollup plugin that fails the build when anything the zones keep on the server reaches the client
|
|
170
|
+
* bundle, with the same checks and messages as nifra's Bun build: every module classified, the graph
|
|
171
|
+
* evidence complete, every emitted file traced back to it, and the `node:` and `backend-only` guards.
|
|
172
|
+
* It runs in `generateBundle`, before Rollup writes a byte.
|
|
84
173
|
*/
|
|
85
|
-
export function viteLeakGuard(): LeakGuardPlugin {
|
|
174
|
+
export function viteLeakGuard(options: LeakGuardOptions = {}): LeakGuardPlugin {
|
|
175
|
+
const appRoot = resolve(options.appRoot ?? process.cwd())
|
|
176
|
+
const root = resolve(options.root ?? appRoot)
|
|
177
|
+
const outDir = resolve(root, options.outDir ?? "dist")
|
|
86
178
|
const plugin: LeakGuardPlugin = {
|
|
87
179
|
name: "nifra:leak-guard",
|
|
88
180
|
generateBundle(_options, bundle) {
|
|
181
|
+
const classifier = classifierFor(appRoot, options)
|
|
89
182
|
// Resolved import edges per module, straight from Rollup's graph. Dynamic imports count too: a
|
|
90
183
|
// `node:`/server-only module reached only via `import()` still ships to the browser.
|
|
91
184
|
const importsOf = (id: string): readonly string[] => {
|
|
@@ -93,12 +186,25 @@ export function viteLeakGuard(): LeakGuardPlugin {
|
|
|
93
186
|
if (info === null) return []
|
|
94
187
|
return [...(info.importedIds ?? []), ...(info.dynamicallyImportedIds ?? [])]
|
|
95
188
|
}
|
|
96
|
-
const graph = fromRollupBundle(bundle, importsOf)
|
|
97
|
-
|
|
98
|
-
|
|
189
|
+
const graph = completeGraph(fromRollupBundle(bundle, importsOf), this, importsOf)
|
|
190
|
+
const sources = { classifier, sourceOf: rollupModuleSource }
|
|
191
|
+
const privateEnv = privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_")
|
|
192
|
+
const outputs = Object.values(bundle as Readonly<Record<string, RollupOutputLike>>)
|
|
193
|
+
for (const output of outputs) {
|
|
194
|
+
if (output.type !== "asset" && output.fileName !== undefined)
|
|
195
|
+
options.verified?.add(output.fileName)
|
|
196
|
+
}
|
|
99
197
|
const leak =
|
|
198
|
+
formatClientGraphVerdict(verifyClientGraph(graph, { ...sources, privateEnv })) ??
|
|
100
199
|
formatNodeBuiltinLeak(detectNodeBuiltinsInClient(graph)) ??
|
|
101
|
-
formatServerOnlyLeak(detectServerOnlyInClient(graph))
|
|
200
|
+
formatServerOnlyLeak(detectServerOnlyInClient(graph)) ??
|
|
201
|
+
formatUnaccountedOutput(
|
|
202
|
+
accountEmittedFiles(emittedFiles(outputs, graph, root, options.verified), graph, {
|
|
203
|
+
...sources,
|
|
204
|
+
outDir,
|
|
205
|
+
}),
|
|
206
|
+
) ??
|
|
207
|
+
secretLeak(outputs, graph, classifier, appRoot, options)
|
|
102
208
|
if (leak === undefined) return
|
|
103
209
|
// Record before throwing: the throw is what fails the build for a standalone user of this plugin,
|
|
104
210
|
// and the record is what lets a caller that owns the build re-raise the real message (see `leak`).
|
|
@@ -108,3 +214,410 @@ export function viteLeakGuard(): LeakGuardPlugin {
|
|
|
108
214
|
}
|
|
109
215
|
return plugin
|
|
110
216
|
}
|
|
217
|
+
|
|
218
|
+
/** The secret scan over a finished bundle and the `public/` files that ship beside it. */
|
|
219
|
+
function secretLeak(
|
|
220
|
+
outputs: readonly RollupOutputLike[],
|
|
221
|
+
graph: ClientModuleGraph,
|
|
222
|
+
classifier: ReturnType<typeof createZoneClassifier>,
|
|
223
|
+
appRoot: string,
|
|
224
|
+
options: LeakGuardOptions,
|
|
225
|
+
): string | undefined {
|
|
226
|
+
const { sources, originsOf } = graphScanInput(graph, rollupModuleSource, classifier)
|
|
227
|
+
const artifacts: SecretScanFile[] = []
|
|
228
|
+
for (const output of outputs) {
|
|
229
|
+
const name = output.fileName
|
|
230
|
+
if (name === undefined || name.startsWith(".vite/")) continue
|
|
231
|
+
if (output.type === "chunk") {
|
|
232
|
+
artifacts.push(...emittedScanFiles(name, output.code ?? "", originsOf(name)))
|
|
233
|
+
if (output.map) artifacts.push({ name: `${name}.map`, text: JSON.stringify(output.map) })
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
const text =
|
|
237
|
+
typeof output.source === "string"
|
|
238
|
+
? output.source
|
|
239
|
+
: output.source === undefined
|
|
240
|
+
? undefined
|
|
241
|
+
: textOf(output.source)
|
|
242
|
+
if (text !== undefined) artifacts.push({ name, text })
|
|
243
|
+
}
|
|
244
|
+
const publicDir = options.secrets?.publicDir
|
|
245
|
+
if (publicDir !== undefined && existsSync(publicDir)) {
|
|
246
|
+
artifacts.push(...publicScanFiles(publicDir, originName(appRoot, resolve(publicDir))))
|
|
247
|
+
}
|
|
248
|
+
return formatSecretFindings(
|
|
249
|
+
scanForSecrets({
|
|
250
|
+
sources,
|
|
251
|
+
artifacts,
|
|
252
|
+
env: options.secrets?.env ?? process.env,
|
|
253
|
+
publicEnvPrefix: options.publicEnvPrefix ?? "PUBLIC_",
|
|
254
|
+
...(options.secrets?.exemptions ? { exemptions: options.secrets.exemptions } : {}),
|
|
255
|
+
}),
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** What {@link viteServerZoneGuard} needs: the zones of the app, nothing about the output. */
|
|
260
|
+
export type ServerZoneGuardOptions = Pick<
|
|
261
|
+
LeakGuardOptions,
|
|
262
|
+
"appRoot" | "routesDir" | "generatedFiles" | "publicEnvPrefix"
|
|
263
|
+
>
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The server build's half of the zone rules, with the same checks and message as nifra's Bun server
|
|
267
|
+
* build: every first-party module zoned, backend code never importing frontend code, shared code
|
|
268
|
+
* importing only shared code. It records the refusal in `leak` for the same reason the client guard
|
|
269
|
+
* does.
|
|
270
|
+
*/
|
|
271
|
+
export function viteServerZoneGuard(options: ServerZoneGuardOptions = {}): LeakGuardPlugin {
|
|
272
|
+
const appRoot = resolve(options.appRoot ?? process.cwd())
|
|
273
|
+
const plugin: LeakGuardPlugin = {
|
|
274
|
+
name: "nifra:server-zone-guard",
|
|
275
|
+
generateBundle(_options, bundle) {
|
|
276
|
+
const classifier = classifierFor(appRoot, options)
|
|
277
|
+
const importsOf = (id: string): readonly string[] => {
|
|
278
|
+
const info = this.getModuleInfo(id)
|
|
279
|
+
if (info === null) return []
|
|
280
|
+
return [...(info.importedIds ?? []), ...(info.dynamicallyImportedIds ?? [])]
|
|
281
|
+
}
|
|
282
|
+
const graph = completeGraph(fromRollupBundle(bundle, importsOf), this, importsOf)
|
|
283
|
+
const leak = formatServerGraphVerdict(
|
|
284
|
+
verifyServerGraph(graph, {
|
|
285
|
+
classifier,
|
|
286
|
+
sourceOf: rollupModuleSource,
|
|
287
|
+
privateEnv: privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_"),
|
|
288
|
+
}),
|
|
289
|
+
)
|
|
290
|
+
if (leak === undefined) return
|
|
291
|
+
plugin.leak = leak
|
|
292
|
+
this.error(new Error(leak))
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
return plugin
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The zone classifier for the app `options` names. */
|
|
299
|
+
function classifierFor(appRoot: string, options: ServerZoneGuardOptions): ZoneClassifier {
|
|
300
|
+
return createZoneClassifier({
|
|
301
|
+
appRoot,
|
|
302
|
+
...(options.routesDir !== undefined ? { routesDir: options.routesDir } : {}),
|
|
303
|
+
...(options.generatedFiles !== undefined ? { generatedFiles: options.generatedFiles } : {}),
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The chunk graph plus every module the build loaded but tree-shook out of every chunk, with external
|
|
309
|
+
* edges marked: an import the bundle left external is evidence the graph has to account for.
|
|
310
|
+
*/
|
|
311
|
+
function completeGraph(
|
|
312
|
+
graph: ClientModuleGraph,
|
|
313
|
+
context: RollupPluginContext,
|
|
314
|
+
importsOf: (id: string) => readonly string[],
|
|
315
|
+
): ClientModuleGraph {
|
|
316
|
+
const external = (id: string): boolean => context.getModuleInfo(id)?.isExternal === true
|
|
317
|
+
const modules: Record<string, GraphModule> = {}
|
|
318
|
+
for (const [id, module] of Object.entries(graph.modules)) {
|
|
319
|
+
modules[id] = {
|
|
320
|
+
imports: module.imports.map((im) =>
|
|
321
|
+
im.path !== undefined && !im.path.startsWith("node:") && external(im.path)
|
|
322
|
+
? { ...im, external: true }
|
|
323
|
+
: im,
|
|
324
|
+
),
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
for (const id of context.getModuleIds?.() ?? []) {
|
|
328
|
+
if (modules[id] !== undefined || external(id)) continue
|
|
329
|
+
modules[id] = {
|
|
330
|
+
imports: importsOf(id).map((path) => (external(path) ? { path, external: true } : { path })),
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { modules, chunks: graph.chunks }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Every file in the bundle, with what it claims to come from. */
|
|
337
|
+
function emittedFiles(
|
|
338
|
+
outputs: readonly RollupOutputLike[],
|
|
339
|
+
graph: ClientModuleGraph,
|
|
340
|
+
root: string,
|
|
341
|
+
verified: ReadonlySet<string> | undefined,
|
|
342
|
+
): EmittedFile[] {
|
|
343
|
+
const fromRoot = (name: string): string => (isAbsolute(name) ? name : resolve(root, name))
|
|
344
|
+
const moduleFiles = (ids: readonly string[]): string[] =>
|
|
345
|
+
ids.flatMap((id) => {
|
|
346
|
+
const source = rollupModuleSource(id)
|
|
347
|
+
return source.kind === "builtin" || source.file === undefined ? [] : [source.file]
|
|
348
|
+
})
|
|
349
|
+
const referencedBy = new Map<string, string[]>()
|
|
350
|
+
for (const output of outputs) {
|
|
351
|
+
if (output.type !== "chunk") continue
|
|
352
|
+
for (const name of [
|
|
353
|
+
...(output.viteMetadata?.importedCss ?? []),
|
|
354
|
+
...(output.viteMetadata?.importedAssets ?? []),
|
|
355
|
+
]) {
|
|
356
|
+
referencedBy.set(name, [
|
|
357
|
+
...(referencedBy.get(name) ?? []),
|
|
358
|
+
...moduleFiles(output.moduleIds ?? []),
|
|
359
|
+
])
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const cssModules = moduleFiles(Object.keys(graph.modules).filter((id) => CSS_FILE.test(id)))
|
|
363
|
+
const files: EmittedFile[] = []
|
|
364
|
+
for (const output of outputs) {
|
|
365
|
+
const name = output.fileName
|
|
366
|
+
// Vite's own bookkeeping (`.vite/manifest.json`); nifra deletes it before anything is deployed.
|
|
367
|
+
if (name === undefined || name.startsWith(".vite/")) continue
|
|
368
|
+
if (output.type === "chunk") {
|
|
369
|
+
files.push({ name, kind: "code", text: output.code ?? "" })
|
|
370
|
+
if (output.map)
|
|
371
|
+
files.push({ name: `${name}.map`, kind: "map", text: JSON.stringify(output.map) })
|
|
372
|
+
continue
|
|
373
|
+
}
|
|
374
|
+
const text = typeof output.source === "string" ? output.source : undefined
|
|
375
|
+
if (name.endsWith(".map")) {
|
|
376
|
+
files.push({ name, kind: "map", ...(text !== undefined ? { text } : {}) })
|
|
377
|
+
continue
|
|
378
|
+
}
|
|
379
|
+
if (verified?.has(name)) continue
|
|
380
|
+
// Vite names a synthesized asset (the `cssCodeSplit: false` aggregate is `style.css`) after no
|
|
381
|
+
// real file; only names that exist are evidence.
|
|
382
|
+
const original = (output.originalFileNames ?? [])
|
|
383
|
+
.map(fromRoot)
|
|
384
|
+
.filter((file) => existsSync(file))
|
|
385
|
+
const referenced = referencedBy.get(name) ?? []
|
|
386
|
+
const sources =
|
|
387
|
+
original.length > 0
|
|
388
|
+
? original
|
|
389
|
+
: referenced.length > 0
|
|
390
|
+
? referenced
|
|
391
|
+
: name.endsWith(".css")
|
|
392
|
+
? cssModules
|
|
393
|
+
: []
|
|
394
|
+
files.push({ name, kind: "asset", sources })
|
|
395
|
+
}
|
|
396
|
+
return files
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** The resolve-hook context slice {@link viteBareBuiltinExternal} uses. */
|
|
400
|
+
interface ResolveContext {
|
|
401
|
+
resolve(
|
|
402
|
+
source: string,
|
|
403
|
+
importer: string | undefined,
|
|
404
|
+
options: { readonly skipSelf: boolean },
|
|
405
|
+
): Promise<{ readonly id: string } | null>
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The minimal Vite plugin shape {@link viteBareBuiltinExternal} returns. */
|
|
409
|
+
export interface BareBuiltinPlugin {
|
|
410
|
+
readonly name: string
|
|
411
|
+
readonly enforce: "pre"
|
|
412
|
+
resolveId(
|
|
413
|
+
this: ResolveContext,
|
|
414
|
+
source: string,
|
|
415
|
+
importer: string | undefined,
|
|
416
|
+
): Promise<{ readonly id: string; readonly external?: boolean } | null>
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Keep a bare Node built-in (`fs/promises`, `path`) visible to {@link viteLeakGuard}. Vite resolves a
|
|
421
|
+
* bare built-in that is not an installed package to one shared `__vite-browser-external` stub: the
|
|
422
|
+
* import builds, does nothing in the browser, and no longer names the module. This plugin externalizes
|
|
423
|
+
* it as `node:<name>` instead, so the guard fails the build naming it, as the Bun build does. A package
|
|
424
|
+
* of the same name the app installed (`events`, `buffer`) still resolves to that package.
|
|
425
|
+
*/
|
|
426
|
+
export function viteBareBuiltinExternal(): BareBuiltinPlugin {
|
|
427
|
+
return {
|
|
428
|
+
name: "nifra:bare-builtin-external",
|
|
429
|
+
enforce: "pre",
|
|
430
|
+
async resolveId(source, importer) {
|
|
431
|
+
if (!isBareNodeBuiltin(source)) return null
|
|
432
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true })
|
|
433
|
+
if (resolved !== null && !resolved.id.startsWith("__vite-browser-external")) return resolved
|
|
434
|
+
return { id: `node:${source}`, external: true }
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** The plugin-context slice {@link viteAssetUrlGuard} uses. */
|
|
440
|
+
interface TransformContext extends ResolveContext {
|
|
441
|
+
parse(code: string): unknown
|
|
442
|
+
error(error: Error): never
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
type InlineLimit = number | ((file: string, content: Uint8Array) => boolean | undefined)
|
|
446
|
+
|
|
447
|
+
/** The minimal Vite plugin shape {@link viteAssetUrlGuard} returns. */
|
|
448
|
+
export interface AssetUrlGuardPlugin {
|
|
449
|
+
readonly name: string
|
|
450
|
+
readonly apply: "build"
|
|
451
|
+
/** The refusal that failed the build, recorded for the reason {@link LeakGuardPlugin.leak} gives. */
|
|
452
|
+
leak?: string
|
|
453
|
+
config(config: { readonly build?: { readonly assetsInlineLimit?: InlineLimit } }): {
|
|
454
|
+
readonly build: { readonly assetsInlineLimit: InlineLimit }
|
|
455
|
+
}
|
|
456
|
+
transform(
|
|
457
|
+
this: TransformContext,
|
|
458
|
+
code: string,
|
|
459
|
+
id: string,
|
|
460
|
+
options?: { readonly ssr?: boolean },
|
|
461
|
+
): Promise<null>
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const POSTFIX = /[?#].*$/s
|
|
465
|
+
const URL_SCHEME = /^(?:[a-z][a-z\d+.-]*:|\/\/)/i
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Keep server files out of a Vite client build's assets. A `new URL("./x", import.meta.url)` makes
|
|
469
|
+
* Vite copy the file it names into the output, or inline it into the chunk as a `data:` URL when it is
|
|
470
|
+
* small - no module and no import edge, so {@link viteLeakGuard}'s graph never sees it. This refuses a
|
|
471
|
+
* reference whose file the zones keep off the browser (backend code, a route's backend half, a server
|
|
472
|
+
* function's source, a file in no zone), and stops Vite inlining such a file anywhere else (a
|
|
473
|
+
* stylesheet's `url()`), so it is emitted where the leak guard's output accounting names it.
|
|
474
|
+
*
|
|
475
|
+
* Add it to the top-level `plugins`, after any framework plugin: it reads each module once that
|
|
476
|
+
* module is JavaScript, before Vite resolves the references.
|
|
477
|
+
*/
|
|
478
|
+
export function viteAssetUrlGuard(options: ServerZoneGuardOptions = {}): AssetUrlGuardPlugin {
|
|
479
|
+
const appRoot = resolve(options.appRoot ?? process.cwd())
|
|
480
|
+
let classifier: ZoneClassifier | undefined
|
|
481
|
+
const denial = (file: string): string | undefined => {
|
|
482
|
+
classifier ??= classifierFor(appRoot, options)
|
|
483
|
+
return assetDenial(classifier.classify(file))
|
|
484
|
+
}
|
|
485
|
+
const shown = (file: string): string => relative(appRoot, file).replaceAll("\\", "/")
|
|
486
|
+
const plugin: AssetUrlGuardPlugin = {
|
|
487
|
+
name: "nifra:asset-url-guard",
|
|
488
|
+
apply: "build",
|
|
489
|
+
config(config) {
|
|
490
|
+
const own = config.build?.assetsInlineLimit
|
|
491
|
+
return {
|
|
492
|
+
build: {
|
|
493
|
+
assetsInlineLimit: (file, content) =>
|
|
494
|
+
denial(file) !== undefined
|
|
495
|
+
? false
|
|
496
|
+
: typeof own === "function"
|
|
497
|
+
? own(file, content)
|
|
498
|
+
: own === undefined
|
|
499
|
+
? undefined
|
|
500
|
+
: content.length < own,
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
async transform(code, id, transformOptions) {
|
|
505
|
+
if (transformOptions?.ssr === true || !code.includes("import.meta.url")) return null
|
|
506
|
+
const importer = id.replace(POSTFIX, "")
|
|
507
|
+
if (!isAbsolute(importer) || importer.includes("/node_modules/") || CSS_FILE.test(importer))
|
|
508
|
+
return null
|
|
509
|
+
let program: unknown
|
|
510
|
+
try {
|
|
511
|
+
program = this.parse(code)
|
|
512
|
+
} catch {
|
|
513
|
+
return null
|
|
514
|
+
}
|
|
515
|
+
const problems: string[] = []
|
|
516
|
+
for (const url of newUrlLiterals(program)) {
|
|
517
|
+
if (URL_SCHEME.test(url)) continue
|
|
518
|
+
for (const file of await assetUrlFiles(this, url, importer)) {
|
|
519
|
+
const reason = denial(file)
|
|
520
|
+
if (reason !== undefined) {
|
|
521
|
+
problems.push(
|
|
522
|
+
`${shown(importer)}: new URL(${JSON.stringify(url)}, import.meta.url) names ${shown(file)}: ${reason}`,
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (problems.length === 0) return null
|
|
528
|
+
const leak =
|
|
529
|
+
`[nifra/web] the browser build references files that may not ship to a browser:\n${problems.map((line) => ` - ${line}`).join("\n")}\n` +
|
|
530
|
+
"The bundler copies or inlines the file such a URL names, so it would ship as it is. Reach " +
|
|
531
|
+
"backend code through a route's backend half (x.backend.ts) or a *.fn.ts server function; a " +
|
|
532
|
+
"file the browser loads belongs in frontend/ or public/."
|
|
533
|
+
plugin.leak = leak
|
|
534
|
+
return this.error(new Error(leak))
|
|
535
|
+
},
|
|
536
|
+
}
|
|
537
|
+
return plugin
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* The files a `new URL(url, import.meta.url)` can name, resolved the ways Vite tries: beside the
|
|
542
|
+
* module, then through the resolver (aliases, extensions, a root-relative path). Existing files only;
|
|
543
|
+
* a reference to nothing ships nothing.
|
|
544
|
+
*/
|
|
545
|
+
async function assetUrlFiles(
|
|
546
|
+
context: ResolveContext,
|
|
547
|
+
url: string,
|
|
548
|
+
importer: string,
|
|
549
|
+
): Promise<string[]> {
|
|
550
|
+
const files = new Set<string>()
|
|
551
|
+
const add = (path: string | undefined): void => {
|
|
552
|
+
const file = path?.replace(POSTFIX, "")
|
|
553
|
+
if (file === undefined || !isAbsolute(file) || file.includes("\0")) return
|
|
554
|
+
if (statSync(file, { throwIfNoEntry: false })?.isFile() === true) files.add(file)
|
|
555
|
+
}
|
|
556
|
+
if (!url.startsWith("/")) add(resolve(dirname(importer), url.replace(POSTFIX, "")))
|
|
557
|
+
const sources = url.startsWith(".") || url.startsWith("/") ? [url] : [url, `./${url}`]
|
|
558
|
+
for (const source of sources) {
|
|
559
|
+
try {
|
|
560
|
+
add((await context.resolve(source, importer, { skipSelf: true }))?.id)
|
|
561
|
+
} catch {
|
|
562
|
+
// An unresolvable reference is one Vite leaves unchanged.
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return [...files]
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const field = (node: unknown, key: string): unknown =>
|
|
569
|
+
typeof node === "object" && node !== null ? Reflect.get(node, key) : undefined
|
|
570
|
+
|
|
571
|
+
/** `import.meta.url`, and not `new.target.url`. */
|
|
572
|
+
const isImportMetaUrl = (node: unknown): boolean =>
|
|
573
|
+
field(node, "type") === "MemberExpression" &&
|
|
574
|
+
field(node, "computed") === false &&
|
|
575
|
+
field(field(node, "property"), "name") === "url" &&
|
|
576
|
+
field(field(field(node, "object"), "meta"), "name") === "import"
|
|
577
|
+
|
|
578
|
+
/** A string literal's text as written and as it evaluates; nothing for a template with holes. */
|
|
579
|
+
function literalTexts(node: unknown): string[] {
|
|
580
|
+
const type = field(node, "type")
|
|
581
|
+
if (type === "Literal") {
|
|
582
|
+
const value = field(node, "value")
|
|
583
|
+
const raw = field(node, "raw")
|
|
584
|
+
if (typeof value !== "string") return []
|
|
585
|
+
return typeof raw === "string" ? [value, raw.slice(1, -1)] : [value]
|
|
586
|
+
}
|
|
587
|
+
if (type !== "TemplateLiteral") return []
|
|
588
|
+
const holes = field(node, "expressions")
|
|
589
|
+
const quasis = field(node, "quasis")
|
|
590
|
+
if (!Array.isArray(holes) || holes.length > 0 || !Array.isArray(quasis)) return []
|
|
591
|
+
const value = field(quasis[0], "value")
|
|
592
|
+
return [field(value, "cooked"), field(value, "raw")].filter(
|
|
593
|
+
(text): text is string => typeof text === "string",
|
|
594
|
+
)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* The URL text of every `new URL("...", import.meta.url)` in a parsed module. Parsed rather than
|
|
599
|
+
* matched, so a comment inside the call cannot hide one; a template with holes is skipped, as Vite
|
|
600
|
+
* turns it into an `import.meta.glob` the module graph sees.
|
|
601
|
+
*/
|
|
602
|
+
function newUrlLiterals(program: unknown): string[] {
|
|
603
|
+
const found = new Set<string>()
|
|
604
|
+
const stack: unknown[] = [program]
|
|
605
|
+
while (stack.length > 0) {
|
|
606
|
+
const node = stack.pop()
|
|
607
|
+
if (typeof node !== "object" || node === null) continue
|
|
608
|
+
const args = field(node, "arguments")
|
|
609
|
+
if (
|
|
610
|
+
field(node, "type") === "NewExpression" &&
|
|
611
|
+
field(field(node, "callee"), "type") === "Identifier" &&
|
|
612
|
+
field(field(node, "callee"), "name") === "URL" &&
|
|
613
|
+
Array.isArray(args) &&
|
|
614
|
+
isImportMetaUrl(args[1])
|
|
615
|
+
) {
|
|
616
|
+
for (const text of literalTexts(args[0])) found.add(text)
|
|
617
|
+
}
|
|
618
|
+
for (const child of Object.values(node)) {
|
|
619
|
+
if (typeof child === "object" && child !== null) stack.push(child)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return [...found]
|
|
623
|
+
}
|