@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/module-graph.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface GraphImport {
|
|
|
23
23
|
readonly path?: string
|
|
24
24
|
/** The specifier as written in source - the only form that survives an unresolved import. */
|
|
25
25
|
readonly original?: string
|
|
26
|
+
/** The bundler left this import in the emitted code instead of bundling its target. Bun does this
|
|
27
|
+
* for a dynamic `import("node:fs")` in a browser build: the import ships, the module does not. */
|
|
28
|
+
readonly external?: boolean
|
|
29
|
+
/** A dynamic `import()`. A bundler can drop one in dead code while the module around it ships. */
|
|
30
|
+
readonly dynamic?: boolean
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export interface GraphModule {
|
|
@@ -34,6 +39,13 @@ export interface GraphChunk {
|
|
|
34
39
|
readonly entryPoint?: string
|
|
35
40
|
/** Module ids that landed in this chunk. */
|
|
36
41
|
readonly modules: readonly string[]
|
|
42
|
+
/** The names this chunk exports, when the bundler reports them (entry chunks). */
|
|
43
|
+
readonly exports?: readonly string[]
|
|
44
|
+
/**
|
|
45
|
+
* What the emitted code itself imports: another output by its key in `chunks`, anything else as
|
|
46
|
+
* written. Module-level edges cannot say this - a bundler drops some and keeps others.
|
|
47
|
+
*/
|
|
48
|
+
readonly imports?: readonly string[]
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
/** What a client build looks like to the guards, whichever bundler produced it. */
|
|
@@ -52,12 +64,26 @@ const normalizeModuleId = (id: string): string => id.replaceAll("\\", "/")
|
|
|
52
64
|
/** The slice of Bun's metafile this seam consumes. Not yet in `@types/bun`; shape per the docs. */
|
|
53
65
|
export interface BunMetafileLike {
|
|
54
66
|
readonly inputs?: Readonly<
|
|
55
|
-
Record<
|
|
67
|
+
Record<
|
|
68
|
+
string,
|
|
69
|
+
{
|
|
70
|
+
readonly imports?: ReadonlyArray<{
|
|
71
|
+
path?: string
|
|
72
|
+
original?: string
|
|
73
|
+
external?: boolean
|
|
74
|
+
kind?: string
|
|
75
|
+
}>
|
|
76
|
+
}
|
|
77
|
+
>
|
|
56
78
|
>
|
|
57
79
|
readonly outputs?: Readonly<
|
|
58
80
|
Record<
|
|
59
81
|
string,
|
|
60
|
-
{
|
|
82
|
+
{
|
|
83
|
+
readonly entryPoint?: string
|
|
84
|
+
readonly inputs?: Readonly<Record<string, unknown>>
|
|
85
|
+
readonly exports?: readonly string[]
|
|
86
|
+
}
|
|
61
87
|
>
|
|
62
88
|
>
|
|
63
89
|
}
|
|
@@ -65,29 +91,37 @@ export interface BunMetafileLike {
|
|
|
65
91
|
/**
|
|
66
92
|
* Adapt a `Bun.build` metafile to the neutral graph.
|
|
67
93
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
94
|
+
* Fails closed: the graph is the evidence that nothing backend reached the browser, so a build that
|
|
95
|
+
* produced no metafile, or one without inputs or outputs, is an error rather than an empty graph that
|
|
96
|
+
* would report nothing.
|
|
71
97
|
*/
|
|
72
98
|
export function fromBunMetafile(meta: BunMetafileLike | undefined): ClientModuleGraph {
|
|
99
|
+
if (meta?.inputs === undefined || meta.outputs === undefined) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"[nifra/web] the client build produced no module graph (metafile), so nifra cannot prove what reached the browser. Refusing to write the bundle",
|
|
102
|
+
)
|
|
103
|
+
}
|
|
73
104
|
const modules: Record<string, GraphModule> = {}
|
|
74
|
-
for (const [rawId, input] of Object.entries(meta
|
|
105
|
+
for (const [rawId, input] of Object.entries(meta.inputs)) {
|
|
75
106
|
const id = normalizeModuleId(rawId)
|
|
76
107
|
modules[id] = {
|
|
77
108
|
imports: (input.imports ?? []).map((im) => ({
|
|
78
109
|
...(im.path === undefined ? {} : { path: normalizeModuleId(im.path) }),
|
|
79
110
|
...(im.original === undefined ? {} : { original: im.original }),
|
|
111
|
+
...(im.external === true ? { external: true } : {}),
|
|
112
|
+
...(im.kind === "dynamic-import" ? { dynamic: true } : {}),
|
|
80
113
|
})),
|
|
81
114
|
}
|
|
82
115
|
}
|
|
83
116
|
const chunks: Record<string, GraphChunk> = {}
|
|
84
|
-
for (const [rawPath, output] of Object.entries(meta
|
|
117
|
+
for (const [rawPath, output] of Object.entries(meta.outputs)) {
|
|
85
118
|
const path = normalizeModuleId(rawPath)
|
|
86
119
|
chunks[path] = {
|
|
87
120
|
...(output.entryPoint !== undefined
|
|
88
121
|
? { entryPoint: normalizeModuleId(output.entryPoint) }
|
|
89
122
|
: {}),
|
|
90
123
|
modules: Object.keys(output.inputs ?? {}).map(normalizeModuleId),
|
|
124
|
+
...(output.exports !== undefined ? { exports: output.exports } : {}),
|
|
91
125
|
}
|
|
92
126
|
}
|
|
93
127
|
return { modules, chunks }
|
|
@@ -105,6 +139,12 @@ export interface RollupChunkLike {
|
|
|
105
139
|
readonly facadeModuleId?: string | null
|
|
106
140
|
/** Every module id that landed in this chunk. Rollup's `moduleIds`. */
|
|
107
141
|
readonly moduleIds?: readonly string[]
|
|
142
|
+
/** The names this chunk exports. Rollup's `exports`. */
|
|
143
|
+
readonly exports?: readonly string[]
|
|
144
|
+
/** Outputs and externals this chunk imports statically / dynamically. Rollup's `imports` and
|
|
145
|
+
* `dynamicImports`. */
|
|
146
|
+
readonly imports?: readonly string[]
|
|
147
|
+
readonly dynamicImports?: readonly string[]
|
|
108
148
|
}
|
|
109
149
|
export type RollupBundleLike = Readonly<Record<string, RollupChunkLike>>
|
|
110
150
|
|
|
@@ -118,8 +158,8 @@ export type RollupBundleLike = Readonly<Record<string, RollupChunkLike>>
|
|
|
118
158
|
* guards read the resolved `path` as their fallback (a `node:` prefix, the `server-only` basename), so
|
|
119
159
|
* detection is unaffected; only the human-readable chain shows resolved paths instead of as-written ones.
|
|
120
160
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
161
|
+
* An empty bundle yields an empty graph; the Rollup output always carries its own graph, so there is
|
|
162
|
+
* no missing-evidence case to fail on here.
|
|
123
163
|
*/
|
|
124
164
|
export function fromRollupBundle(
|
|
125
165
|
bundle: RollupBundleLike,
|
|
@@ -154,6 +194,10 @@ export function fromRollupBundle(
|
|
|
154
194
|
// how a non-entry Bun output omits `entryPoint`, so the guards' entry set stays the real entries.
|
|
155
195
|
...(output.facadeModuleId ? { entryPoint: normalizeModuleId(output.facadeModuleId) } : {}),
|
|
156
196
|
modules: chunkModules,
|
|
197
|
+
...(output.exports !== undefined ? { exports: output.exports } : {}),
|
|
198
|
+
...(output.imports !== undefined
|
|
199
|
+
? { imports: [...output.imports, ...(output.dynamicImports ?? [])] }
|
|
200
|
+
: {}),
|
|
157
201
|
}
|
|
158
202
|
}
|
|
159
203
|
return { modules, chunks }
|
package/src/navigation.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* native `<a href>` full-page navigation - progressive enhancement, no throw.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { ClientRouter } from "./router.ts"
|
|
15
16
|
import { serializeSearch } from "./search.ts"
|
|
16
17
|
|
|
17
18
|
/** Options for a programmatic navigation. */
|
|
@@ -102,6 +103,18 @@ export function resolveNavigate(
|
|
|
102
103
|
return { to: to.to + query, options: resolved }
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/**
|
|
107
|
+
* When a link warms its route's chunk and loader data ahead of a click. Set it with the
|
|
108
|
+
* `data-nifra-prefetch` attribute on the link or on any ancestor (the nearest wins):
|
|
109
|
+
* - `intent` (the default) - on hover or keyboard focus.
|
|
110
|
+
* - `viewport` - once the link scrolls into view.
|
|
111
|
+
* - `render` - as soon as a page shows the link.
|
|
112
|
+
* - `none` - never; the route loads when the link is followed.
|
|
113
|
+
*
|
|
114
|
+
* A warmed route is used by a click within 30 seconds; after that the click loads it again.
|
|
115
|
+
*/
|
|
116
|
+
export type PrefetchMode = "intent" | "viewport" | "render" | "none"
|
|
117
|
+
|
|
105
118
|
/**
|
|
106
119
|
* A history-aware navigate. A **string** `to` is a same-origin path (`/users/7?tab=a`) navigated to
|
|
107
120
|
* (push, or replace with `{ replace: true }`); a **number** is a history delta (`-1` back, `1`
|
|
@@ -109,14 +122,24 @@ export function resolveNavigate(
|
|
|
109
122
|
*/
|
|
110
123
|
export type BrowserNavigate = (to: string | number, options?: NavigateOptions) => void
|
|
111
124
|
|
|
112
|
-
// The active browser navigate (set by `installHistory`, cleared on
|
|
113
|
-
// per page; absent on the server and before hydration.
|
|
125
|
+
// The active browser navigate and the router behind it (set by `installHistory`, cleared on
|
|
126
|
+
// teardown). Module-scoped: one app per page; absent on the server and before hydration.
|
|
114
127
|
let browserNavigate: BrowserNavigate | undefined
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
128
|
+
let browserRouter: ClientRouter | undefined
|
|
129
|
+
|
|
130
|
+
/** Register (or clear, with `undefined`) the browser navigate and its router - called by
|
|
131
|
+
* `installHistory`. Not for app use. */
|
|
132
|
+
export function setBrowserNavigate(
|
|
133
|
+
navigate: BrowserNavigate | undefined,
|
|
134
|
+
router?: ClientRouter,
|
|
135
|
+
): void {
|
|
119
136
|
browserNavigate = navigate
|
|
137
|
+
browserRouter = router
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The mounted client router, or `undefined` on the server / before `installHistory` has run. */
|
|
141
|
+
export function getBrowserRouter(): ClientRouter | undefined {
|
|
142
|
+
return browserRouter
|
|
120
143
|
}
|
|
121
144
|
|
|
122
145
|
/** The active browser navigate, or `undefined` on the server / before `installHistory` has run. A
|
package/src/nonce.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Context supplied while resolving a document's CSP nonce. */
|
|
2
|
+
export interface NonceContext<Env = unknown> {
|
|
3
|
+
readonly request: Request
|
|
4
|
+
readonly env: Env
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Generate a nonce for one request, or return `undefined` to keep the document nonce-free. */
|
|
8
|
+
export type NonceGenerator<Env = unknown> = (
|
|
9
|
+
context: NonceContext<Env>,
|
|
10
|
+
) => string | undefined | Promise<string | undefined>
|
|
11
|
+
|
|
12
|
+
/** Context supplied to the optional Content-Security-Policy header callback. */
|
|
13
|
+
export interface NonceHeaderContext {
|
|
14
|
+
readonly nonce: string
|
|
15
|
+
readonly request: Request
|
|
16
|
+
readonly response: Response
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Return the complete Content-Security-Policy value for a nonce-bearing document. */
|
|
20
|
+
export type NonceHeader = (context: NonceHeaderContext) => string | undefined
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A request-aware nonce resolver. It is callable so existing `createWebApp({ nonce })` code can pass
|
|
24
|
+
* it directly. When created with a `header` callback, `createWebApp` also installs its response hook
|
|
25
|
+
* automatically and applies that callback's CSP value to the same request that received the nonce.
|
|
26
|
+
*/
|
|
27
|
+
export interface NonceResolver<Env = unknown> extends NonceGenerator<Env> {
|
|
28
|
+
/** Read the currently resolved nonce for a request, if one has been generated. */
|
|
29
|
+
readonly get?: (request: Request) => string | undefined
|
|
30
|
+
/** Internal integration seam used by `createWebApp` when a header callback was supplied. */
|
|
31
|
+
readonly onResponse?: (response: Response, request: Request) => Response
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CreateNonceResolverOptions<Env = unknown> {
|
|
35
|
+
/** Custom nonce generator. The default is a fresh CSP base64-compatible nonce per request. */
|
|
36
|
+
readonly generate?: NonceGenerator<Env>
|
|
37
|
+
/** Build the complete CSP value. App-specific sources remain the caller's responsibility. */
|
|
38
|
+
readonly header?: NonceHeader
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a request-memoized CSP nonce resolver for `createWebApp`.
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* const nonce = createNonceResolver({
|
|
46
|
+
* header: ({ nonce }) => `default-src 'self'; script-src 'self' 'nonce-${nonce}'`,
|
|
47
|
+
* })
|
|
48
|
+
* const app = createWebApp({ adapter, manifest, clientEntry, nonce })
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* A nonce is generated only when the page executor asks for one. Data-mode/API responses never ask,
|
|
52
|
+
* so the response hook has no value to attach and leaves those responses without a CSP nonce header.
|
|
53
|
+
*/
|
|
54
|
+
export function createNonceResolver<Env = unknown>(
|
|
55
|
+
options: CreateNonceResolverOptions<Env> = {},
|
|
56
|
+
): NonceResolver<Env> {
|
|
57
|
+
const values = new WeakMap<Request, string>()
|
|
58
|
+
const pending = new WeakMap<Request, Promise<string | undefined>>()
|
|
59
|
+
// CSP nonce-source values use the base64-value grammar. A UUID contains hyphens, so remove them
|
|
60
|
+
// before emitting it; the remaining 122 random bits are sufficient for a per-response nonce and
|
|
61
|
+
// stay valid across browsers and all fetch runtimes without a Node-only encoder.
|
|
62
|
+
const generate = options.generate ?? (() => crypto.randomUUID().replaceAll("-", ""))
|
|
63
|
+
|
|
64
|
+
const resolve = (context: NonceContext<Env>): string | Promise<string | undefined> => {
|
|
65
|
+
const existing = values.get(context.request)
|
|
66
|
+
if (existing !== undefined) return existing
|
|
67
|
+
const ongoing = pending.get(context.request)
|
|
68
|
+
if (ongoing !== undefined) return ongoing
|
|
69
|
+
const generated = (async (): Promise<string | undefined> => {
|
|
70
|
+
const value = await generate(context)
|
|
71
|
+
if (value === undefined) return undefined
|
|
72
|
+
if (value.trim() === "") {
|
|
73
|
+
throw new TypeError("[nifra/web] createNonceResolver generated an empty CSP nonce")
|
|
74
|
+
}
|
|
75
|
+
values.set(context.request, value)
|
|
76
|
+
return value
|
|
77
|
+
})()
|
|
78
|
+
pending.set(context.request, generated)
|
|
79
|
+
generated.then(
|
|
80
|
+
() => pending.delete(context.request),
|
|
81
|
+
() => pending.delete(context.request),
|
|
82
|
+
)
|
|
83
|
+
return generated
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const get = (request: Request): string | undefined => values.get(request)
|
|
87
|
+
if (options.header === undefined) return Object.assign(resolve, { get })
|
|
88
|
+
|
|
89
|
+
const onResponse = (response: Response, request: Request): Response => {
|
|
90
|
+
const nonce = values.get(request)
|
|
91
|
+
values.delete(request)
|
|
92
|
+
if (nonce === undefined) return response
|
|
93
|
+
const value = options.header?.({ nonce, request, response })
|
|
94
|
+
if (value === undefined) return response
|
|
95
|
+
const headers = new Headers(response.headers)
|
|
96
|
+
headers.set("content-security-policy", value)
|
|
97
|
+
return new Response(response.body, {
|
|
98
|
+
status: response.status,
|
|
99
|
+
statusText: response.statusText,
|
|
100
|
+
headers,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
return Object.assign(resolve, { get, onResponse })
|
|
104
|
+
}
|
|
@@ -51,9 +51,11 @@ function scopedKeyframeName(filePath: string, name: string): string {
|
|
|
51
51
|
|
|
52
52
|
/** Matches `@keyframes name` / `@-webkit-keyframes name` (etc.); captures the leading keyword and name. */
|
|
53
53
|
const KEYFRAMES_PRELUDE = /(@(?:-\w+-)?keyframes\s+)([A-Za-z_][\w-]*)/i
|
|
54
|
-
/** An `animation` / `animation-name` declaration
|
|
55
|
-
* keyframe-name token(s) to remap).
|
|
56
|
-
|
|
54
|
+
/** An `animation` / `animation-name` declaration, vendor-prefixed or after a comment: capture the
|
|
55
|
+
* `prop:` head + the value (which holds the keyframe-name token(s) to remap). A comment cannot hold its
|
|
56
|
+
* own closing delimiter, so each one matches one way - no backtracking across them. */
|
|
57
|
+
const ANIMATION_DECL =
|
|
58
|
+
/^((?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:-[a-z]+-)?animation(?:-name)?\s*:\s*)([\s\S]*)$/i
|
|
57
59
|
const isKeyframesAt = (at: string): boolean => at === "keyframes" || at.endsWith("-keyframes")
|
|
58
60
|
|
|
59
61
|
/** `s[i]` is a quote: return the index just past the matching close quote (honoring `\` escapes). */
|
package/src/plugins/kit.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { existsSync } from "node:fs"
|
|
|
8
8
|
import { dirname, join, relative } from "node:path"
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
10
10
|
import type { BunPlugin } from "bun"
|
|
11
|
+
import { type RawSourceMap, ssrSourceMaps } from "../internal/source-map.ts"
|
|
11
12
|
|
|
12
13
|
/** The argument Bun passes to a plugin's `setup` - Bun doesn't export the type, so derive it. */
|
|
13
14
|
export type PluginBuilder = Parameters<BunPlugin["setup"]>[0]
|
|
@@ -123,6 +124,29 @@ export function devServerCompile(): boolean {
|
|
|
123
124
|
return process.env[DEV_HMR_ENV] === "1"
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
export { concatSourceMaps, type RawSourceMap } from "../internal/source-map.ts"
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A compiled module carrying its map back to the file it came from, during a dev server only. The client
|
|
131
|
+
* half gets it inline: Bun's bundler keeps a plugin's output as the bundle map's source, and the dev
|
|
132
|
+
* server's frame mapper follows the inline map from there. The SSR half goes in the process registry
|
|
133
|
+
* instead, because Bun's runtime ignores a plugin's inline map when it reports a stack.
|
|
134
|
+
*/
|
|
135
|
+
export function withDevSourceMap(
|
|
136
|
+
code: string,
|
|
137
|
+
map: RawSourceMap | undefined,
|
|
138
|
+
path: string,
|
|
139
|
+
generate: "dom" | "ssr",
|
|
140
|
+
): string {
|
|
141
|
+
if (map === undefined || !devServerCompile()) return code
|
|
142
|
+
if (generate === "ssr") {
|
|
143
|
+
ssrSourceMaps().set(portablePath(path), map)
|
|
144
|
+
return code
|
|
145
|
+
}
|
|
146
|
+
const encoded = Buffer.from(JSON.stringify(map)).toString("base64")
|
|
147
|
+
return `${code}\n//# sourceMappingURL=data:application/json;base64,${encoded}\n`
|
|
148
|
+
}
|
|
149
|
+
|
|
126
150
|
/**
|
|
127
151
|
* The app root and the routes directory of the running dev server, for plugins that need to tell one of
|
|
128
152
|
* the app's own components from a dependency or a route module. Set alongside {@link DEV_HMR_ENV}; read
|
package/src/plugins/svg.ts
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* is untouched - only the `?component` marker is intercepted.
|
|
12
12
|
*
|
|
13
13
|
* Optimization is optional: pass `svgo` (or install the `svgo` peer) to minify/clean the SVG first.
|
|
14
|
-
* The
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* The file must be one well-formed `<svg>` element; anything else is refused rather than guessed at.
|
|
15
|
+
* Its text reaches JSX as text - a brace in a `<style>` rule or a `<title>` is a character, never an
|
|
16
|
+
* expression. The transform camelCases hyphenated + `xlink:`/`xmlns:` attributes, maps `class` →
|
|
17
|
+
* `className`, parses an inline `style="..."` into an object, and drops XML declarations/comments.
|
|
17
18
|
*/
|
|
18
19
|
import type { BunPlugin } from "bun"
|
|
19
20
|
import { normalizeFilePath, requirePeer } from "./kit.ts"
|
|
@@ -113,10 +114,140 @@ export function stripSvgPreamble(xml: string): string {
|
|
|
113
114
|
return parts.join("").trim()
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/** How a framework's template must spell an SVG file's text and names, for {@link svgTemplateMarkup}. */
|
|
118
|
+
export interface SvgMarkupRules {
|
|
119
|
+
/** Text content as the template must spell it; `cdata` text arrives as a CDATA section held it. */
|
|
120
|
+
readonly text: (text: string, cdata: boolean) => string
|
|
121
|
+
/** An attribute value (without its quotes) as the template must spell it. Default as written. */
|
|
122
|
+
readonly attributeValue?: (value: string) => string
|
|
123
|
+
/** Why the template would read an element or attribute name as more than markup, or `undefined`. */
|
|
124
|
+
readonly refuse?: (name: string, kind: "element" | "attribute") => string | undefined
|
|
125
|
+
/** Elements whose content the template reads as raw text, copied as it is. */
|
|
126
|
+
readonly rawText?: ReadonlySet<string>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const svgMarkupError = (detail: string): Error =>
|
|
130
|
+
new Error(`[nifra/web] an SVG component must be plain SVG markup: ${detail}`)
|
|
131
|
+
|
|
132
|
+
const TAG_NAME = /[A-Za-z_:][\w:.-]*/y
|
|
133
|
+
const SPACE = /\s*/y
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* An SVG file's markup rewritten for a framework template: one `<svg>` root, its text and attribute
|
|
137
|
+
* values put through `rules` so none of it reads as template syntax. Markup a well-formed SVG cannot
|
|
138
|
+
* hold is refused rather than passed on - a brace, a bare name or an unquoted value in a tag, content
|
|
139
|
+
* after the root - since each is a way for the file to reach the template as code.
|
|
140
|
+
*/
|
|
141
|
+
export function svgTemplateMarkup(xml: string, rules: SvgMarkupRules): string {
|
|
142
|
+
const svg = stripSvgPreamble(xml)
|
|
143
|
+
if (!/^<svg[\s/>]/.test(svg)) throw svgMarkupError("it does not start with an <svg> element")
|
|
144
|
+
let out = ""
|
|
145
|
+
let depth = 0
|
|
146
|
+
let i = 0
|
|
147
|
+
while (i < svg.length) {
|
|
148
|
+
if (svg.startsWith("<![CDATA[", i)) {
|
|
149
|
+
const close = svg.indexOf("]]>", i + 9)
|
|
150
|
+
if (close === -1) throw svgMarkupError("a CDATA section is not closed")
|
|
151
|
+
out += rules.text(svg.slice(i + 9, close), true)
|
|
152
|
+
i = close + 3
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (svg[i] !== "<") {
|
|
156
|
+
const next = svg.indexOf("<", i)
|
|
157
|
+
const end = next === -1 ? svg.length : next
|
|
158
|
+
out += rules.text(svg.slice(i, end), false)
|
|
159
|
+
i = end
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
const tag = readSvgTag(svg, i, rules)
|
|
163
|
+
out += tag.markup
|
|
164
|
+
i = tag.end
|
|
165
|
+
if (tag.closing) depth--
|
|
166
|
+
else if (!tag.selfClosing) {
|
|
167
|
+
depth++
|
|
168
|
+
if (rules.rawText?.has(tag.name) === true) {
|
|
169
|
+
// Wherever the template's own reading ends this element, in any case, raw text ends here too.
|
|
170
|
+
const close = svg.toLowerCase().indexOf(`</${tag.name.toLowerCase()}`, i)
|
|
171
|
+
const end = close === -1 ? svg.length : close
|
|
172
|
+
out += svg.slice(i, end)
|
|
173
|
+
i = end
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (depth === 0) {
|
|
177
|
+
if (svg.slice(i).trim() !== "") throw svgMarkupError("there is content after the root </svg>")
|
|
178
|
+
return out
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
throw svgMarkupError("the root <svg> element is not closed")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** One tag of {@link svgTemplateMarkup}'s input, rebuilt with each attribute value through `rules`. */
|
|
185
|
+
function readSvgTag(
|
|
186
|
+
svg: string,
|
|
187
|
+
start: number,
|
|
188
|
+
rules: SvgMarkupRules,
|
|
189
|
+
): { markup: string; end: number; name: string; closing: boolean; selfClosing: boolean } {
|
|
190
|
+
const closing = svg[start + 1] === "/"
|
|
191
|
+
let i = start + (closing ? 2 : 1)
|
|
192
|
+
TAG_NAME.lastIndex = i
|
|
193
|
+
const name = TAG_NAME.exec(svg)?.[0]
|
|
194
|
+
if (name === undefined) throw svgMarkupError(`a "<" at offset ${start} opens no tag`)
|
|
195
|
+
const refused = rules.refuse?.(name, "element")
|
|
196
|
+
if (refused !== undefined) throw svgMarkupError(refused)
|
|
197
|
+
i += name.length
|
|
198
|
+
let markup = `<${closing ? "/" : ""}${name}`
|
|
199
|
+
for (;;) {
|
|
200
|
+
SPACE.lastIndex = i
|
|
201
|
+
const space = SPACE.exec(svg)?.[0] ?? ""
|
|
202
|
+
i += space.length
|
|
203
|
+
if (svg[i] === ">")
|
|
204
|
+
return { markup: `${markup}>`, end: i + 1, name, closing, selfClosing: false }
|
|
205
|
+
if (!closing && svg.startsWith("/>", i)) {
|
|
206
|
+
return { markup: `${markup}/>`, end: i + 2, name, closing, selfClosing: true }
|
|
207
|
+
}
|
|
208
|
+
if (i >= svg.length) throw svgMarkupError(`the tag <${name}> is not closed`)
|
|
209
|
+
TAG_NAME.lastIndex = i
|
|
210
|
+
const attribute = closing || space === "" ? undefined : TAG_NAME.exec(svg)?.[0]
|
|
211
|
+
if (attribute === undefined) {
|
|
212
|
+
throw svgMarkupError(`<${name}> holds ${JSON.stringify(svg[i])} where an attribute belongs`)
|
|
213
|
+
}
|
|
214
|
+
const refusedAttribute = rules.refuse?.(attribute, "attribute")
|
|
215
|
+
if (refusedAttribute !== undefined) throw svgMarkupError(refusedAttribute)
|
|
216
|
+
i += attribute.length
|
|
217
|
+
SPACE.lastIndex = i
|
|
218
|
+
i += SPACE.exec(svg)?.[0].length ?? 0
|
|
219
|
+
if (svg[i] !== "=") throw svgMarkupError(`the attribute ${attribute} on <${name}> has no value`)
|
|
220
|
+
SPACE.lastIndex = i + 1
|
|
221
|
+
i += 1 + (SPACE.exec(svg)?.[0].length ?? 0)
|
|
222
|
+
const quote = svg[i]
|
|
223
|
+
const close = quote === '"' || quote === "'" ? svg.indexOf(quote, i + 1) : -1
|
|
224
|
+
if (close === -1) {
|
|
225
|
+
throw svgMarkupError(`the value of ${attribute} on <${name}> is not a quoted string`)
|
|
226
|
+
}
|
|
227
|
+
const value = svg.slice(i + 1, close)
|
|
228
|
+
markup += ` ${attribute}=${quote}${rules.attributeValue?.(value) ?? value}${quote}`
|
|
229
|
+
i = close + 1
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Text as JSX text: a brace, `>` or `=` as a character reference, so none of it is an expression. */
|
|
234
|
+
const jsxText = (text: string, cdata: boolean): string =>
|
|
235
|
+
(cdata ? text.replaceAll("&", "&").replaceAll("<", "<") : text).replace(
|
|
236
|
+
/[{}>=]/g,
|
|
237
|
+
(char) => `&#${char.charCodeAt(0)};`,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
/** JSX reads `<a.b>` and `<Name>` as a value in scope, called as a component - never an SVG element. */
|
|
241
|
+
const jsxRefuse = (name: string, kind: "element" | "attribute"): string | undefined =>
|
|
242
|
+
kind === "element" && (name.includes(".") || /^[A-Z]/.test(name))
|
|
243
|
+
? `JSX reads the element <${name}> as a value in scope, not an SVG element`
|
|
244
|
+
: undefined
|
|
245
|
+
|
|
116
246
|
/** Convert an SVG XML string into a JSX-safe `<svg>…</svg>` element with `{...props}` spread on the root. */
|
|
117
247
|
export function svgToJsx(xml: string, options: SvgToJsxOptions = {}): string {
|
|
118
248
|
const classProp = options.classProp ?? "className"
|
|
119
|
-
|
|
249
|
+
// Text is escaped first, `=` included, so the attribute rewrites below match inside tags only.
|
|
250
|
+
let out = svgTemplateMarkup(xml, { text: jsxText, refuse: jsxRefuse })
|
|
120
251
|
|
|
121
252
|
// Namespaced attributes → camelCase (xlink:href → xlinkHref, xmlns:xlink → xmlnsXlink).
|
|
122
253
|
out = out.replace(/\b([a-z]+):([a-z]+)=/gi, (_m, ns: string, name: string) => {
|