@brust/core 2.0.0-alpha.1 → 2.0.0-alpha.10

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.
Files changed (63) hide show
  1. package/README.md +66 -6
  2. package/native/index.d.ts +87 -5
  3. package/native/index.js +61 -54
  4. package/package.json +22 -9
  5. package/src/actions/client.ts +22 -5
  6. package/src/actions/load.ts +2 -1
  7. package/src/actions/prefix.ts +27 -3
  8. package/src/actions-bridge.ts +76 -19
  9. package/src/browser.ts +3 -1
  10. package/src/build/actions-only.ts +86 -0
  11. package/src/build/build-eval.ts +319 -0
  12. package/src/build/build-icons.ts +69 -0
  13. package/src/build/bundle.ts +278 -43
  14. package/src/build/cf-assets.ts +83 -0
  15. package/src/build/cf-core.ts +90 -0
  16. package/src/build/cf-entry.ts +76 -0
  17. package/src/build/cf-wrangler.ts +169 -0
  18. package/src/build/cloudflare.ts +317 -0
  19. package/src/build/compile.ts +72 -4
  20. package/src/build/config.ts +274 -15
  21. package/src/build/css.ts +275 -0
  22. package/src/build/features.ts +110 -0
  23. package/src/build/index.ts +223 -20
  24. package/src/build/manifest.ts +59 -8
  25. package/src/build/pkg-src.ts +383 -0
  26. package/src/build/scan.ts +61 -20
  27. package/src/build/server-only.ts +39 -0
  28. package/src/build/ssg-render.ts +65 -0
  29. package/src/build/ssg.ts +146 -0
  30. package/src/build/static-worker.ts +91 -0
  31. package/src/build/static.ts +258 -0
  32. package/src/build/tailwind.ts +117 -0
  33. package/src/build/tsconfig-paths.ts +67 -0
  34. package/src/build-time.ts +10 -0
  35. package/src/cache-adapter/bridge.ts +91 -0
  36. package/src/cache-adapter/conformance.ts +226 -0
  37. package/src/cache-adapter/index.ts +10 -0
  38. package/src/cache-adapter/load.ts +144 -0
  39. package/src/cache-adapter/memory.ts +229 -0
  40. package/src/cache-adapter/runtime.ts +124 -0
  41. package/src/cache-adapter/types.ts +96 -0
  42. package/src/cache-adapter/workers.ts +118 -0
  43. package/src/cache.ts +4 -1
  44. package/src/cf.ts +19 -0
  45. package/src/cli.ts +50 -2
  46. package/src/config.ts +127 -25
  47. package/src/css-modules.d.ts +8 -0
  48. package/src/dev/bun.ts +365 -0
  49. package/src/dev/client.ts +92 -0
  50. package/src/dev/devtools-data.ts +23 -0
  51. package/src/dev/devtools.ts +356 -0
  52. package/src/dev/diag.ts +30 -0
  53. package/src/dev/proxy.ts +236 -0
  54. package/src/dev/tui.ts +144 -0
  55. package/src/dev/watch.ts +81 -0
  56. package/src/dev.ts +93 -0
  57. package/src/handlers.ts +282 -0
  58. package/src/index.ts +3 -1
  59. package/src/native.ts +49 -2
  60. package/src/routes.ts +20 -1
  61. package/src/run.ts +48 -11
  62. package/src/server-handlers.ts +32 -0
  63. package/src/worker.ts +129 -245
@@ -0,0 +1,319 @@
1
+ // Build-time evaluation of precompute slots (L2, G4/H1). The compiler marks the slots that read
2
+ // nothing request- or time-dependent (`ir.build_eval`) and emits one thunk per slot (`evalTs`).
3
+ // Here they are bundled once, run in a Bun subprocess (`TZ=UTC`, a timeout per slot, JSON-only
4
+ // values, a size cap) and the results go back to the compiler as slot -> literal overrides: the
5
+ // slot, and a job left with no slots, disappear. A pure helper that fails keeps its job with a
6
+ // `build-eval` warning; a failing `buildTime(…)` is a build error (the user asked for it).
7
+ //
8
+ // F149 (R34.90): an inlined instance whose child job reads only constants and allowed code
9
+ // (`ir.build_eval_instances`) runs that child's job module, the file the worker loads, once per distinct
10
+ // input in the same sandbox; its relative imports the compiler re-emitted folded (`stubs`) are served from
11
+ // that code. The outputs go back as `<instance key>#<slot>` values: the instance and its record disappear.
12
+ import { realpathSync } from 'node:fs'
13
+ import { join } from 'node:path'
14
+ import type { BunPlugin } from 'bun'
15
+ import { type SourceOf, PRODUCTION, sourceDirPlugin, stage, write } from './bundle'
16
+ import type { Compiled } from './compile'
17
+ import { BuildError } from './errors'
18
+ import { serverOnlyEmptyPlugin } from './server-only'
19
+
20
+ const V1 = { timeoutMs: 2_000, capBytes: 64 * 1024 }
21
+ const BUILD_TIME = { timeoutMs: 10_000, capBytes: 256 * 1024 }
22
+
23
+ interface Candidate {
24
+ key: string
25
+ bt: boolean
26
+ where: string
27
+ }
28
+
29
+ /** The runner the subprocess executes: loads the bundle, guards the sources of nondeterminism,
30
+ * and prints `S <i>` before and `R <i> <json>` / `E <i> <message>` after each slot. */
31
+ const RUNNER = `
32
+ const mod = await import(process.argv[2])
33
+ const start = Number(process.argv[3])
34
+ const slots = mod.default
35
+ const deny = (what) => () => { throw new Error('build-eval: ' + what + ' is not allowed (it differs between runs)') }
36
+ const RealDate = globalThis.Date
37
+ globalThis.Date = new Proxy(RealDate, {
38
+ construct(t, a) { if (a.length === 0) deny('new Date()')(); return Reflect.construct(t, a) },
39
+ apply() { deny('Date()')() },
40
+ get(t, p, r) { if (p === 'now') deny('Date.now()')(); return Reflect.get(t, p, r) },
41
+ })
42
+ Math.random = deny('Math.random()')
43
+ if (globalThis.performance) globalThis.performance.now = deny('performance.now()')
44
+ if (globalThis.crypto) { for (const k of ['randomUUID', 'getRandomValues']) { try { globalThis.crypto[k] = deny('crypto.' + k + '()') } catch {} } }
45
+ globalThis.fetch = deny('fetch()')
46
+ const out = (l) => process.stdout.write(l + '\\n')
47
+ const env = process.env
48
+ try { Object.defineProperty(process, 'env', { configurable: true, get: deny('process.env') }) } catch {}
49
+ const enc = (v, path) => {
50
+ if (v === null || typeof v === 'boolean' || typeof v === 'string') return v
51
+ if (typeof v === 'number') { if (!Number.isFinite(v)) throw new Error('not JSON: ' + path + ' is ' + v); return v }
52
+ if (Array.isArray(v)) return v.map((x, i) => { if (x === undefined && !(i in v)) throw new Error('not JSON: sparse array at ' + path); return enc(x, path + '[' + i + ']') })
53
+ if (typeof v === 'object' && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null)) {
54
+ return { $: Object.keys(v).map((k) => { if (v[k] === undefined) throw new Error('not JSON: ' + path + '.' + k + ' is undefined'); return [k, enc(v[k], path + '.' + k)] }) }
55
+ }
56
+ throw new Error('not JSON: ' + path + ' is ' + (v === undefined ? 'undefined' : typeof v === 'object' ? (v.constructor && v.constructor.name) : typeof v))
57
+ }
58
+ for (let i = start; i < slots.length; i++) {
59
+ out('S ' + i)
60
+ try {
61
+ const v = await slots[i].fn()
62
+ out('R ' + i + ' ' + JSON.stringify(enc(v, 'value')))
63
+ } catch (e) {
64
+ out('E ' + i + ' ' + String(e && e.message ? e.message : e).split('\\n')[0])
65
+ }
66
+ }
67
+ `
68
+
69
+ /** The evaluator sees an explicit allowlist, never the build host's environment: `process.env`,
70
+ * `Bun.env` and `import.meta.env` all read from it, however they are spelled. */
71
+ function evalEnv(): Record<string, string> {
72
+ const env: Record<string, string> = { TZ: 'UTC', NODE_ENV: 'production' }
73
+ for (const k of ['PATH', 'HOME']) if (process.env[k] !== undefined) env[k] = process.env[k]!
74
+ return env
75
+ }
76
+
77
+ type Outcome = { ok: true; value: unknown } | { ok: false; reason: string }
78
+
79
+ /** Runs slots `[0, n)` of `entry` one after the other, restarting the subprocess after a timeout or crash. */
80
+ async function runAll(entry: string, runner: string, cands: Candidate[], cwd: string): Promise<Outcome[]> {
81
+ const out: (Outcome | undefined)[] = new Array(cands.length).fill(undefined)
82
+ let next = 0
83
+ while (next < cands.length) {
84
+ const proc = Bun.spawn([process.execPath, runner, entry, String(next)], { cwd, env: evalEnv(), stdout: 'pipe', stderr: 'pipe' })
85
+ const reader = (proc.stdout as ReadableStream<Uint8Array>).getReader()
86
+ const dec = new TextDecoder()
87
+ let buf = ''
88
+ let current = -1
89
+ let timer: ReturnType<typeof setTimeout> | undefined
90
+ let timedOut = false
91
+ const arm = (i: number) => {
92
+ clearTimeout(timer)
93
+ const limit = cands[i]!.bt ? BUILD_TIME.timeoutMs : V1.timeoutMs
94
+ timer = setTimeout(() => {
95
+ timedOut = true
96
+ proc.kill('SIGKILL')
97
+ }, limit)
98
+ }
99
+ const line = (l: string) => {
100
+ const m = /^([SRE]) (\d+)(?: (.*))?$/.exec(l)
101
+ if (!m) return
102
+ const i = Number(m[2])
103
+ if (m[1] === 'S') {
104
+ current = i
105
+ arm(i)
106
+ return
107
+ }
108
+ clearTimeout(timer)
109
+ const cap = cands[i]!.bt ? BUILD_TIME.capBytes : V1.capBytes
110
+ if (m[1] === 'E') out[i] = { ok: false, reason: m[3] ?? 'threw' }
111
+ else if ((m[3] ?? '').length > cap) out[i] = { ok: false, reason: `value is larger than ${cap / 1024} KiB` }
112
+ else out[i] = { ok: true, value: JSON.parse(m[3]!) }
113
+ next = i + 1
114
+ current = -1
115
+ }
116
+ for (;;) {
117
+ const { done, value } = await reader.read()
118
+ if (done) break
119
+ buf += dec.decode(value, { stream: true })
120
+ let nl: number
121
+ while ((nl = buf.indexOf('\n')) >= 0) {
122
+ line(buf.slice(0, nl))
123
+ buf = buf.slice(nl + 1)
124
+ }
125
+ }
126
+ clearTimeout(timer)
127
+ await proc.exited
128
+ if (current >= 0) {
129
+ const limit = (cands[current]!.bt ? BUILD_TIME.timeoutMs : V1.timeoutMs) / 1000
130
+ const stderr = (await new Response(proc.stderr as ReadableStream).text()).trim().split('\n')[0]
131
+ out[current] = { ok: false, reason: timedOut ? `timed out after ${limit} s` : `the evaluator exited${stderr ? `: ${stderr}` : ''}` }
132
+ next = current + 1
133
+ } else if (next < cands.length && !out[next]) {
134
+ // Exited before reporting anything (the bundle failed to load).
135
+ const stderr = (await new Response(proc.stderr as ReadableStream).text()).trim().split('\n').slice(0, 3).join(' | ')
136
+ for (let i = next; i < cands.length; i++) out[i] = { ok: false, reason: `the evaluator could not start${stderr ? `: ${stderr}` : ''}` }
137
+ next = cands.length
138
+ }
139
+ }
140
+ return out as Outcome[]
141
+ }
142
+
143
+ /** `@brust/core` inside the evaluator is the side-effect-free `buildTime` module, not the server. */
144
+ function corePlugin(appRoot: string): BunPlugin {
145
+ return {
146
+ name: 'brust-eval-core',
147
+ setup(b) {
148
+ b.onResolve({ filter: /^@brust\/core$/ }, () => ({ path: Bun.resolveSync('@brust/core/build-time', appRoot) }))
149
+ },
150
+ }
151
+ }
152
+
153
+ /** Evaluates every build-time slot and every build-evaluated instance of `compiled`; returns `key -> encoded
154
+ * JSON value` for those that succeeded. Throws a `BuildError` for a failing `buildTime(…)`; logs
155
+ * `warning build-eval` for a pure helper or an instance that fails (its job stays). */
156
+ export async function evaluateBuildSlots(
157
+ dist: string,
158
+ compiled: Map<string, Compiled>,
159
+ sourceOf: SourceOf,
160
+ appRoot: string,
161
+ log: (s: string) => void,
162
+ ): Promise<Record<string, unknown>> {
163
+ // `BRUST_BUILD_EVAL=0` turns evaluation off (every slot and instance stays a job): the reference for dual runs.
164
+ if (process.env.BRUST_BUILD_EVAL === '0') return {}
165
+ const slots = await evaluateSlots(dist, compiled, sourceOf, appRoot, log)
166
+ const instances = await evaluateInstances(dist, compiled, sourceOf, appRoot, log)
167
+ return { ...slots, ...instances }
168
+ }
169
+
170
+ /** L2: the slots of `ir.build_eval`. */
171
+ async function evaluateSlots(
172
+ dist: string,
173
+ compiled: Map<string, Compiled>,
174
+ sourceOf: SourceOf,
175
+ appRoot: string,
176
+ log: (s: string) => void,
177
+ ): Promise<Record<string, unknown>> {
178
+ const cands: Candidate[] = []
179
+ const imports: string[] = []
180
+ const entries: string[] = []
181
+ for (const c of compiled.values()) {
182
+ const slots = (c.ir.build_eval ?? []) as { key: string; build_time: boolean; line: number; col: number }[]
183
+ if (c.evalTs === undefined || slots.length === 0) continue
184
+ const file = join(dist, '.stage', 'eval', `${c.id}.eval.ts`)
185
+ write(file, c.evalTs)
186
+ stage(sourceOf, file, c)
187
+ const n = imports.length
188
+ imports.push(`import { slots as s${n} } from ${JSON.stringify(file)}`)
189
+ for (const s of slots) {
190
+ const where = `${c.file.slice(appRoot.length + 1)}:${s.line}:${s.col}`
191
+ cands.push({ key: s.key, bt: s.build_time, where })
192
+ entries.push(` { fn: s${n}[${JSON.stringify(s.key)}] },`)
193
+ }
194
+ }
195
+ if (cands.length === 0) return {}
196
+ const entry = join(dist, '.stage', 'eval-entry.ts')
197
+ write(entry, `${imports.join('\n')}\nexport default [\n${entries.join('\n')}\n]\n`)
198
+ const res = await Bun.build({
199
+ entrypoints: [entry],
200
+ target: 'bun',
201
+ format: 'esm',
202
+ define: PRODUCTION,
203
+ plugins: [serverOnlyEmptyPlugin(), corePlugin(appRoot), sourceDirPlugin(sourceOf, appRoot)],
204
+ })
205
+ if (!res.success) throw new BuildError('build-eval', `cannot bundle the build-time evaluation: ${res.logs.map(String).join('\n')}`)
206
+ const bundle = join(dist, '.stage', 'eval.js')
207
+ write(bundle, await res.outputs[0]!.text())
208
+ const runner = join(dist, '.stage', 'eval-run.js')
209
+ write(runner, RUNNER)
210
+ const results = await runAll(bundle, runner, cands, appRoot)
211
+ const values: Record<string, unknown> = {}
212
+ results.forEach((r, i) => {
213
+ const c = cands[i]!
214
+ if (r.ok) values[c.key] = r.value
215
+ else if (c.bt) throw new BuildError('build-time', `${c.where} buildTime(…) failed at build: ${r.reason}`)
216
+ else log(`warning build-eval ${c.where} not evaluated at build, the job stays: ${r.reason}`)
217
+ })
218
+ return values
219
+ }
220
+
221
+ type EvalInstance = NonNullable<Compiled['ir']['build_eval_instances']>[number]
222
+
223
+ /** Serves a relative import of a staged job module from the stub the compiler printed for it (rule 3). */
224
+ function stubPlugin(stubs: Map<string, Map<string, string>>): BunPlugin {
225
+ return {
226
+ name: 'brust-eval-stub',
227
+ setup(b) {
228
+ b.onResolve({ filter: /^\.\.?\// }, (a) => {
229
+ const file = stubs.get(a.importer)?.get(a.path)
230
+ return file === undefined ? undefined : { path: file }
231
+ })
232
+ },
233
+ }
234
+ }
235
+
236
+ /** F149 (R34.90): runs each distinct `(child, inputs)` of `ir.build_eval_instances` through the child's own job
237
+ * module (`serverTs`, what the worker loads) once; returns `<key>#<slot> -> encoded value` for every instance
238
+ * whose evaluation succeeded with all of its outputs. A failure is a warning and the instance keeps its job. */
239
+ async function evaluateInstances(
240
+ dist: string,
241
+ compiled: Map<string, Compiled>,
242
+ sourceOf: SourceOf,
243
+ appRoot: string,
244
+ log: (s: string) => void,
245
+ ): Promise<Record<string, unknown>> {
246
+ const groups = new Map<string, { inst: EvalInstance; where: string; keys: string[] }>()
247
+ const modules = new Map<string, string>()
248
+ const stubs = new Map<string, Map<string, string>>()
249
+ const imports: string[] = []
250
+ for (const c of compiled.values()) {
251
+ for (const inst of c.ir.build_eval_instances ?? []) {
252
+ const child = compiled.get(inst.child_id)
253
+ if (child?.serverTs === undefined) continue
254
+ if (!modules.has(child.id)) {
255
+ const file = join(dist, '.stage', 'eval', `${child.id}.job.ts`)
256
+ write(file, child.serverTs)
257
+ stage(sourceOf, file, child)
258
+ const served = new Map<string, string>()
259
+ for (const [i, st] of (inst.stubs ?? []).entries()) {
260
+ const stub = join(dist, '.stage', 'eval', 'stubs', child.id, `${i}.ts`)
261
+ write(stub, st.code)
262
+ stage(sourceOf, stub, child)
263
+ served.set(st.source, stub)
264
+ }
265
+ if (served.size > 0) {
266
+ stubs.set(file, served)
267
+ stubs.set(realpathSync(file), served)
268
+ }
269
+ modules.set(child.id, `j${modules.size}`)
270
+ imports.push(`import { precompute as ${modules.get(child.id)} } from ${JSON.stringify(file)}`)
271
+ }
272
+ const id = `${inst.child_id}\0${JSON.stringify(inst.inputs)}`
273
+ const where = `${c.file.slice(appRoot.length + 1)}:${inst.line}:${inst.col}`
274
+ const g = groups.get(id)
275
+ if (g) g.keys.push(inst.key)
276
+ else groups.set(id, { inst, where, keys: [inst.key] })
277
+ }
278
+ }
279
+ if (groups.size === 0) return {}
280
+ const list = [...groups.values()]
281
+ const entry = join(dist, '.stage', 'eval-instances-entry.ts')
282
+ write(entry, `${imports.join('\n')}\nexport default [\n${list.map((g) => ` { fn: () => ${modules.get(g.inst.child_id)}(${JSON.stringify(g.inst.inputs)}) },`).join('\n')}\n]\n`)
283
+ const res = await Bun.build({
284
+ entrypoints: [entry],
285
+ target: 'bun',
286
+ format: 'esm',
287
+ define: PRODUCTION,
288
+ plugins: [stubPlugin(stubs), sourceDirPlugin(sourceOf, appRoot)],
289
+ })
290
+ if (!res.success) {
291
+ log(`warning build-eval the instance jobs could not be bundled for build evaluation, they stay per-request jobs: ${res.logs.map(String).join(' | ').split('\n')[0]}`)
292
+ return {}
293
+ }
294
+ const bundle = join(dist, '.stage', 'eval-instances.js')
295
+ write(bundle, await res.outputs[0]!.text())
296
+ const runner = join(dist, '.stage', 'eval-run.js')
297
+ write(runner, RUNNER)
298
+ const results = await runAll(bundle, runner, list.map((g) => ({ key: g.inst.key, bt: false, where: g.where })), appRoot)
299
+ const values: Record<string, unknown> = {}
300
+ results.forEach((r, i) => {
301
+ const g = list[i]!
302
+ const pairs = r.ok ? outputsOf(r.value) : undefined
303
+ const missing = pairs && g.inst.outputs.find((o) => !pairs.has(o))
304
+ if (!r.ok || !pairs || missing !== undefined) {
305
+ const reason = !r.ok ? r.reason : !pairs ? 'the job did not return an object' : `the job did not return \`${missing}\``
306
+ log(`warning build-eval ${g.where} <${g.inst.child_id}> not evaluated at build, its job stays: ${reason}`)
307
+ return
308
+ }
309
+ for (const key of g.keys) for (const o of g.inst.outputs) values[`${key}#${o}`] = pairs.get(o)
310
+ })
311
+ return values
312
+ }
313
+
314
+ /** The slots of an encoded job result (`{ $: [[slot, value], …] }`, see the runner's `enc`). */
315
+ function outputsOf(v: unknown): Map<string, unknown> | undefined {
316
+ const pairs = (v as { $?: unknown } | null)?.$
317
+ if (!Array.isArray(pairs)) return undefined
318
+ return new Map(pairs.map((p) => [String((p as unknown[])[0]), (p as unknown[])[1]]))
319
+ }
@@ -0,0 +1,69 @@
1
+ // lucide-react icons rendered at build time (L23). The compiler records an `IconRequest` for every
2
+ // icon it could inline; here each is rendered with `react-dom/server` (the app's own React and the
3
+ // installed lucide-react, so the markup is exactly what React would produce for that version),
4
+ // dynamic props rendered as sentinels the compiler lowers back to attributes. The result goes back
5
+ // with the next compile (`iconSvgs`). An icon that cannot be rendered keeps the L22 island.
6
+ import type { Compiled } from './compile'
7
+
8
+ interface Request {
9
+ key: string
10
+ source: string
11
+ export: string
12
+ props: Record<string, unknown>
13
+ dynamic: string[]
14
+ provider: Record<string, unknown>
15
+ line: number
16
+ col: number
17
+ }
18
+
19
+ const sentinel = (name: string) => `__BRUST_ICON_${name}__`
20
+
21
+ export interface IconMarkup {
22
+ /** Dynamic props rendered as sentinels. */
23
+ svg: string
24
+ /** The same icon with the dynamic props absent: React's default for an `undefined` prop. */
25
+ base: string
26
+ }
27
+
28
+ export async function renderIcons(compiled: Map<string, Compiled>, appRoot: string, log: (s: string) => void): Promise<Record<string, IconMarkup>> {
29
+ const all: { c: Compiled; r: Request }[] = []
30
+ for (const c of compiled.values()) for (const r of (c.ir.icon_requests ?? []) as Request[]) all.push({ c, r })
31
+ if (all.length === 0) return {}
32
+ // One React for the icon module and the renderer: both resolve from the app.
33
+ const resolveFrom = (spec: string, from: string) => Bun.resolveSync(spec, from)
34
+ const server = await import(resolveFrom('react-dom/server', appRoot))
35
+ const react = await import(resolveFrom('react', appRoot))
36
+ const out: Record<string, IconMarkup> = {}
37
+ const mods = new Map<string, Record<string, unknown>>()
38
+ for (const { c, r } of all) {
39
+ const where = `${c.file.slice(appRoot.length + 1)}:${r.line}:${r.col}`
40
+ try {
41
+ let mod = mods.get(r.source)
42
+ if (!mod) {
43
+ mod = (await import(resolveFrom(r.source, appRoot))) as Record<string, unknown>
44
+ mods.set(r.source, mod)
45
+ }
46
+ const Comp = mod[r.export] as unknown
47
+ if (typeof Comp !== 'function' && (typeof Comp !== 'object' || Comp === null)) throw new Error(`${r.source} has no export \`${r.export}\``)
48
+ const render = (dynamic: Record<string, unknown>): string => {
49
+ let el = react.createElement(Comp, { ...r.props, ...dynamic })
50
+ if (Object.keys(r.provider).length > 0) {
51
+ const Provider = mod.LucideProvider
52
+ if (!Provider) throw new Error(`${r.source} has no LucideProvider (the installed lucide-react predates it)`)
53
+ el = react.createElement(Provider, r.provider, el)
54
+ }
55
+ const markup: string = server.renderToStaticMarkup(el)
56
+ if (!markup.startsWith('<svg') || !markup.endsWith('</svg>')) throw new Error('it does not render an <svg>')
57
+ if (/NaN|undefined/.test(markup)) throw new Error('a dynamic prop is combined with another (for example absoluteStrokeWidth with a dynamic size)')
58
+ return markup
59
+ }
60
+ const svg = render(Object.fromEntries(r.dynamic.map((d) => [d, sentinel(d)])))
61
+ for (const d of r.dynamic) if (!svg.includes(sentinel(d))) throw new Error(`prop \`${d}\` does not reach the markup`)
62
+ // `size={undefined}` renders React's default (or the provider's value): render that too.
63
+ out[r.key] = { svg, base: r.dynamic.length > 0 ? render({}) : svg }
64
+ } catch (e) {
65
+ log(`warning icon-render ${where} ${r.export} from ${r.source} stays a React island: ${e instanceof Error ? e.message : String(e)}`)
66
+ }
67
+ }
68
+ return out
69
+ }