@geastack/core 0.1.23

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 (73) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +36 -0
  3. package/app_frame_perf.cpp +77 -0
  4. package/css.d.ts +1 -0
  5. package/events.cpp +67 -0
  6. package/gea_app_entry.cpp +317 -0
  7. package/gea_sources.mjs +126 -0
  8. package/gea_sources.sh +36 -0
  9. package/include/app.h +60 -0
  10. package/include/apps.h +46 -0
  11. package/include/audio.h +90 -0
  12. package/include/ble.h +212 -0
  13. package/include/bluetooth/hid_profile.h +92 -0
  14. package/include/camera.h +107 -0
  15. package/include/canvas.h +290 -0
  16. package/include/display.h +390 -0
  17. package/include/display_present.h +869 -0
  18. package/include/event.h +32 -0
  19. package/include/events.h +116 -0
  20. package/include/gea/embedded.h +13 -0
  21. package/include/gea/reactive_runtime.h +32 -0
  22. package/include/gea.h +14 -0
  23. package/include/gea_perf_config.h +96 -0
  24. package/include/geaos/abi.h +165 -0
  25. package/include/geolocation.h +126 -0
  26. package/include/gpio.h +61 -0
  27. package/include/graphics/font.h +122 -0
  28. package/include/i2c.h +20 -0
  29. package/include/image.h +156 -0
  30. package/include/imu.h +23 -0
  31. package/include/input.h +33 -0
  32. package/include/memory.h +52 -0
  33. package/include/pixel.h +907 -0
  34. package/include/platform/file_cache.h +18 -0
  35. package/include/power.h +11 -0
  36. package/include/runtime.h +83 -0
  37. package/include/services/app_runner.h +10 -0
  38. package/include/services/app_state.h +14 -0
  39. package/include/services/battery_service.h +12 -0
  40. package/include/services/bluetooth_service.h +13 -0
  41. package/include/services/diagnostics.h +76 -0
  42. package/include/services/diagnostics_platform.h +54 -0
  43. package/include/services/frame_scheduler.h +71 -0
  44. package/include/services/mirror.h +49 -0
  45. package/include/services/network_services.h +25 -0
  46. package/include/services/ota.h +32 -0
  47. package/include/services/runtime_log.h +11 -0
  48. package/include/services/storage_service.h +26 -0
  49. package/include/touch.h +38 -0
  50. package/include/ui/signal.h +149 -0
  51. package/include/wifi.h +67 -0
  52. package/index.d.ts +1940 -0
  53. package/jsx.d.ts +1 -0
  54. package/package.json +70 -0
  55. package/runtime/ble.ts +49 -0
  56. package/runtime/color.ts +21 -0
  57. package/runtime/compiler.ts +43 -0
  58. package/runtime/host.ts +1083 -0
  59. package/runtime/images.ts +129 -0
  60. package/runtime/primitives.ts +49 -0
  61. package/runtime-surface.ts +180 -0
  62. package/runtime.cpp +431 -0
  63. package/runtime.ts +34 -0
  64. package/scripts/build-gea-vite-geatsc.mjs +4514 -0
  65. package/scripts/dotenv-defines.mjs +98 -0
  66. package/scripts/gea-bundle-type-hints.mjs +546 -0
  67. package/scripts/gea-embedded-compat-staging.mjs +17 -0
  68. package/scripts/gea-embedded-compat-transform.mjs +427 -0
  69. package/scripts/gea-vite-module-graph-plugin.mjs +578 -0
  70. package/scripts/generate-gea-embedded-assets.mjs +296 -0
  71. package/scripts/generate-gea-embedded-fonts.mjs +1252 -0
  72. package/scripts/generate-installed-app-plan.mjs +55 -0
  73. package/scripts/native-object-cache.sh +244 -0
@@ -0,0 +1,4514 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import process from 'node:process'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { createRequire } from 'node:module'
7
+ import { fileURLToPath, pathToFileURL } from 'node:url'
8
+ import { inlineJsonImports, transformGeaEmbeddedCompatSource } from './gea-embedded-compat-transform.mjs'
9
+ import { shouldIgnoreCompatStagingDirectory } from './gea-embedded-compat-staging.mjs'
10
+ import { dotEnvDefines, inlineProcessEnv } from './dotenv-defines.mjs'
11
+ import {
12
+ MODULE_HINT_SCOPE,
13
+ applyBundledTypeHints,
14
+ collectReachableSourceFiles,
15
+ collectSourceFiles,
16
+ discoverLocalImport,
17
+ parseTsx,
18
+ resolveFunctionHintName,
19
+ restoreModuleGraphElAnnotations,
20
+ } from './gea-bundle-type-hints.mjs'
21
+
22
+ const args = process.argv.slice(2)
23
+ const requireFromLib = createRequire(new URL('../package.json', import.meta.url))
24
+ const coreRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
25
+ const elementsRoot = path.resolve(coreRoot, '..', 'elements')
26
+ const engineRoot = path.resolve(coreRoot, '..', 'engine')
27
+
28
+ // Where @geastack/apple actually is, rather than where a repository layout
29
+ // would put it. The Apple target ships INSIDE that package, so the repo root it
30
+ // reports is the package itself; an app that installs it from npm has it in a
31
+ // node_modules chain instead, and `<repo>/packages/geastack-apple` describes
32
+ // neither. Guessing the path is what made an installed app fail to resolve
33
+ // @geastack/apple/AppKit at all.
34
+ function resolveApplePackageRoot(appDir = '') {
35
+ // GEA_APPLE_ROOT is for a caller that IS the apple package: its own target
36
+ // scripts build apps against the working copy, not an installed copy.
37
+ const declared = process.env.GEA_APPLE_ROOT || ''
38
+ if (declared && readPackageName(declared) === '@geastack/apple') return fs.realpathSync(declared)
39
+ for (const start of [appDir, coreRoot]) {
40
+ if (!start) continue
41
+ let dir = path.resolve(start)
42
+ for (;;) {
43
+ if (readPackageName(dir) === '@geastack/apple') return fs.realpathSync(dir)
44
+ const candidate = path.join(dir, 'node_modules', '@geastack', 'apple')
45
+ if (fs.existsSync(path.join(candidate, 'package.json'))) return fs.realpathSync(candidate)
46
+ const parent = path.dirname(dir)
47
+ if (parent === dir) break
48
+ dir = parent
49
+ }
50
+ }
51
+ // No guessed fallback. The old one returned `<repo>/packages/geastack-apple`,
52
+ // a layout that existed only in this project's own working copy, so an
53
+ // installed app was handed a path that was not there and failed later,
54
+ // somewhere else, with a message about the wrong thing.
55
+ fail(`cannot resolve @geastack/apple from ${appDir || coreRoot} — run \`npm install\` there, or set GEA_APPLE_ROOT`)
56
+ }
57
+
58
+ function readPackageName(dir) {
59
+ try {
60
+ return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).name
61
+ } catch {
62
+ return ''
63
+ }
64
+ }
65
+ // The module identity a build pipeline must resolve `@geastack/core` to. The
66
+ // package answers "." with two files (`types: ./index.d.ts` for an editor,
67
+ // `default: ./runtime.ts` for anything that loads it), and everything that
68
+ // compiles the framework from source needs the second one -- see the comment
69
+ // at the top of runtime.ts. Stated to Vite as an alias and to TypeScript as a
70
+ // `paths` entry, so both consumers of the staged tree agree.
71
+ const runtimePath = path.join(coreRoot, 'runtime.ts')
72
+ const tryResolve = (m) => { try { return requireFromLib.resolve(m) } catch { return '' } }
73
+ const geaPluginResolved = fileURLToPath(import.meta.resolve('@geajs/vite-plugin'))
74
+ const { parse } = requireFromLib('@babel/parser')
75
+ const traverseModule = requireFromLib('@babel/traverse')
76
+ const t = requireFromLib('@babel/types')
77
+ const traverse = traverseModule.default || traverseModule
78
+ const useStaticCssRules = !['0', 'false', 'no'].includes(String(process.env.GEA_STATIC_CSS_RULES ?? '1').toLowerCase())
79
+ const useStaticCssTape = !['0', 'false', 'no'].includes(String(process.env.GEA_STATIC_CSS_TAPE ?? '1').toLowerCase())
80
+ const staticCssTapeMinChunk = Math.max(1, Number.parseInt(process.env.GEA_STATIC_CSS_TAPE_MIN_CHUNK ?? '5', 10) || 1)
81
+
82
+ function readOption(name) {
83
+ const index = args.indexOf(name)
84
+ if (index === -1) return undefined
85
+ return args[index + 1]
86
+ }
87
+
88
+ function hasFlag(name) {
89
+ return args.includes(name)
90
+ }
91
+
92
+ function readAllOptions(name) {
93
+ const values = []
94
+ for (let i = 0; i < args.length; i++) {
95
+ if (args[i] === name) values.push(args[i + 1])
96
+ }
97
+ return values.filter(Boolean)
98
+ }
99
+
100
+ function fail(message) {
101
+ process.stderr.write(`${message}\n`)
102
+ process.exit(1)
103
+ }
104
+
105
+ function acquireOutputPipelineLock(outDir) {
106
+ const lockDir = path.join(path.dirname(outDir), `.${path.basename(outDir)}.gea-pipeline.lock`)
107
+ const pidFile = path.join(lockDir, 'pid')
108
+ const tryAcquire = () => {
109
+ try {
110
+ fs.mkdirSync(lockDir)
111
+ fs.writeFileSync(pidFile, `${process.pid}\n`)
112
+ return true
113
+ } catch (error) {
114
+ if (error?.code !== 'EEXIST') throw error
115
+ return false
116
+ }
117
+ }
118
+
119
+ if (!tryAcquire()) {
120
+ const ownerText = fs.existsSync(pidFile) ? fs.readFileSync(pidFile, 'utf8').trim() : ''
121
+ const owner = /^[0-9]+$/.test(ownerText) ? Number(ownerText) : null
122
+ let ownerIsAlive = false
123
+ if (owner !== null) {
124
+ try {
125
+ process.kill(owner, 0)
126
+ ownerIsAlive = true
127
+ } catch {}
128
+ }
129
+ if (!ownerIsAlive) {
130
+ fs.rmSync(lockDir, { recursive: true, force: true })
131
+ if (!tryAcquire()) fail(`generation output became busy while recovering a stale lock: ${outDir}`)
132
+ } else {
133
+ fail(`generation output is already owned by pid ${owner}: ${outDir}`)
134
+ }
135
+ }
136
+
137
+ let released = false
138
+ const release = () => {
139
+ if (released) return
140
+ released = true
141
+ const ownerText = fs.existsSync(pidFile) ? fs.readFileSync(pidFile, 'utf8').trim() : ''
142
+ if (ownerText === String(process.pid)) fs.rmSync(lockDir, { recursive: true, force: true })
143
+ }
144
+ process.on('exit', release)
145
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
146
+ process.once(signal, () => {
147
+ release()
148
+ process.kill(process.pid, signal)
149
+ })
150
+ }
151
+ }
152
+
153
+ function writeFileIfChanged(filePath, contents) {
154
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === contents) return
155
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
156
+ fs.writeFileSync(filePath, contents)
157
+ }
158
+
159
+ function run(command, commandArgs, options = {}) {
160
+ const result = spawnSync(command, commandArgs, {
161
+ stdio: 'inherit',
162
+ ...options,
163
+ })
164
+ if (result.status !== 0) {
165
+ const rendered = [command, ...commandArgs].join(' ')
166
+ fail(`command failed (${result.status ?? result.signal}): ${rendered}`)
167
+ }
168
+ }
169
+
170
+ function findFirstJsFile(dir) {
171
+ const stack = [dir]
172
+ const files = []
173
+ while (stack.length > 0) {
174
+ const current = stack.pop()
175
+ if (!current || !fs.existsSync(current)) continue
176
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
177
+ const full = path.join(current, entry.name)
178
+ if (entry.isDirectory()) stack.push(full)
179
+ else if (entry.isFile() && entry.name.endsWith('.js')) files.push(full)
180
+ }
181
+ }
182
+ files.sort()
183
+ return files[0]
184
+ }
185
+
186
+ function copyCssUrlAssets(srcCss, dstCss) {
187
+ const css = fs.readFileSync(srcCss, 'utf8')
188
+ const srcDir = path.dirname(srcCss)
189
+ const dstDir = path.dirname(dstCss)
190
+ const urlRe = /url\(\s*(?:"([^"]+)"|'([^']+)'|([^)"']+))\s*\)/gi
191
+ let match
192
+ while ((match = urlRe.exec(css)) !== null) {
193
+ const raw = (match[1] ?? match[2] ?? match[3] ?? '').trim()
194
+ if (!raw || raw.startsWith('#') || raw.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(raw)) continue
195
+ const clean = raw.split(/[?#]/, 1)[0]
196
+ if (!clean) continue
197
+ const srcAsset = path.resolve(srcDir, clean)
198
+ if (!fs.existsSync(srcAsset) || !fs.statSync(srcAsset).isFile()) continue
199
+ const dstAsset = path.resolve(dstDir, clean)
200
+ if (dstAsset === srcAsset) continue
201
+ fs.mkdirSync(path.dirname(dstAsset), { recursive: true })
202
+ fs.copyFileSync(srcAsset, dstAsset)
203
+ }
204
+ }
205
+
206
+ // Stages one source tree into the compat dir; the caller clears the compat
207
+ // dir first (an app plus its escaped shared trees stage as multiple calls).
208
+ function copyCompatSourceTree(srcDir, dstDir, envDefines = {}) {
209
+ fs.mkdirSync(dstDir, { recursive: true })
210
+ const transformable = new Set(['.js', '.jsx', '.ts', '.tsx'])
211
+ const dstRoot = path.resolve(dstDir)
212
+ const stack = ['.']
213
+ while (stack.length > 0) {
214
+ const rel = stack.pop()
215
+ const currentSrc = path.join(srcDir, rel)
216
+ const currentDst = path.join(dstDir, rel)
217
+ for (const entry of fs.readdirSync(currentSrc, { withFileTypes: true })) {
218
+ if (entry.isDirectory()) {
219
+ if (shouldIgnoreCompatStagingDirectory(entry.name)) continue
220
+ const childRel = path.join(rel, entry.name)
221
+ const childSrc = path.resolve(srcDir, childRel)
222
+ const relativeToDst = path.relative(dstRoot, childSrc)
223
+ if (relativeToDst === '' || (!relativeToDst.startsWith('..') && !path.isAbsolute(relativeToDst))) continue
224
+ fs.mkdirSync(path.join(dstDir, childRel), { recursive: true })
225
+ stack.push(childRel)
226
+ continue
227
+ }
228
+ if (!entry.isFile()) continue
229
+ const childRel = path.join(rel, entry.name)
230
+ const src = path.join(srcDir, childRel)
231
+ const dst = path.join(dstDir, childRel)
232
+ fs.mkdirSync(path.dirname(dst), { recursive: true })
233
+ if (transformable.has(path.extname(entry.name))) {
234
+ const code = fs.readFileSync(src, 'utf8')
235
+ // JSON-import folding first, so the compat transform and the
236
+ // vite-plugin-gea IR extractor both see the data as inline literals
237
+ // (the fully native store/record lowering path). Then bake in .env vars
238
+ // (process.env.<KEY> -> literal) so the module-graph snapshots geatsc
239
+ // compiles carry the value — Vite's `define` only rewrites the bundle.
240
+ fs.writeFileSync(dst, inlineProcessEnv(transformGeaEmbeddedCompatSource(inlineJsonImports(code, src), src), envDefines))
241
+ } else {
242
+ fs.copyFileSync(src, dst)
243
+ if (path.extname(entry.name) === '.css') copyCssUrlAssets(src, dst)
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ // The workspace `node_modules` the app's bare specifiers actually resolve
250
+ // through, mirrored beside the staged copy of the app.
251
+ //
252
+ // Staging copies SOURCES only, so `@geastack/core` -- which resolves in the
253
+ // real tree through the hoisted `examples/node_modules/@geastack/core`
254
+ // workspace symlink -- resolves through nothing at all in the staged tree. The
255
+ // module graph hides that for every module it TRACKS, because it hands geatsc
256
+ // each tracked edge's resolved id; a module the graph shook out but the
257
+ // typechecker still loads (a barrel's `export { X } from './raster/pipeline'`
258
+ // whose exports the app never uses) falls through to ordinary node resolution
259
+ // and finds nothing. `examples/apps/gea3d-cube` compiled to "Cannot find module
260
+ // '@geastack/core'" for exactly two such files.
261
+ //
262
+ // EVERY ancestor level is mirrored, not just the nearest: node resolution walks
263
+ // up through all of them, and in a workspace the app's own `node_modules` holds
264
+ // its direct deps while the hoisted one a few levels up holds the framework.
265
+ // Linking only the first found reproduces the failure with a link in place.
266
+ function linkCompatNodeModules(appDir, stagedAppDir, compatSrcDir) {
267
+ const stagingRoot = path.resolve(compatSrcDir)
268
+ let real = path.resolve(appDir)
269
+ let staged = path.resolve(stagedAppDir)
270
+ for (;;) {
271
+ const source = path.join(real, 'node_modules')
272
+ if (fs.existsSync(source)) {
273
+ const link = path.join(staged, 'node_modules')
274
+ if (!fs.existsSync(link)) {
275
+ fs.mkdirSync(staged, { recursive: true })
276
+ try {
277
+ fs.symlinkSync(source, link, 'dir')
278
+ } catch {
279
+ // A filesystem without symlinks is not a reason to fail the build:
280
+ // every module the graph tracked still resolves through its stated id.
281
+ }
282
+ }
283
+ }
284
+ if (staged === stagingRoot) return
285
+ const nextReal = path.dirname(real)
286
+ const nextStaged = path.dirname(staged)
287
+ if (nextReal === real || nextStaged === staged) return
288
+ real = nextReal
289
+ staged = nextStaged
290
+ }
291
+ }
292
+
293
+ // Directories OUTSIDE the app dir reached through relative imports (the
294
+ // examples/shared component trees). Outermost-only: a nested hit is covered by
295
+ // staging its containing directory wholesale (which also brings the css/asset
296
+ // files discoverLocalImport skips).
297
+ function escapedCompatImportDirs(appDir, entry) {
298
+ const escaped = collectReachableSourceFiles(appDir, entry).filter((file) => path.relative(appDir, file).startsWith('..'))
299
+ const dirs = [...new Set(escaped.map((file) => path.dirname(file)))].sort()
300
+ return dirs.filter((dir, index) => !dirs.slice(0, index).some((outer) => outer !== dir && !path.relative(outer, dir).startsWith('..')))
301
+ }
302
+
303
+ function commonAncestorDir(dirs) {
304
+ let ancestor = dirs[0]
305
+ for (const dir of dirs.slice(1)) {
306
+ while (path.relative(ancestor, dir).startsWith('..')) ancestor = path.dirname(ancestor)
307
+ }
308
+ return ancestor
309
+ }
310
+
311
+ // The compat transform rewrites core imports to the bare specifier
312
+ // `gea-embedded`, because that is the name the generated Vite config aliases to
313
+ // `runtime.ts` -- the module identity a source-compiling pipeline needs, rather
314
+ // than the `index.d.ts` that `@geastack/core` answers with.
315
+ //
316
+ // geatsc typechecks this same staged tree, and nothing had ever told TypeScript
317
+ // that name. `gea-embedded` resolved to nothing for any app: the `paths` entry
318
+ // eleven of them still carry points at `examples/lib/gea-embedded`, a directory
319
+ // the repo split removed. One authority renaming a module the other cannot see
320
+ // is how `Cannot find module 'gea-embedded'` reaches a program whose Vite build
321
+ // succeeded (examples/hid-clicker, through the shared Settings tree).
322
+ //
323
+ // So state the alias to TypeScript too, against the staged runtime when the
324
+ // staging mirrored core into the tree -- the module graph is rooted there, and
325
+ // pointing the checker outside it would hand back the second module identity
326
+ // this whole rename exists to avoid.
327
+ function stateRuntimeAliasToTypeScript(stagedAppDir, compatSrcDir, stagingRoot) {
328
+ const mirrored = path.join(compatSrcDir, path.relative(stagingRoot, runtimePath))
329
+ const target = !path.relative(compatSrcDir, mirrored).startsWith('..') && fs.existsSync(mirrored) ? mirrored : runtimePath
330
+ const configPath = path.join(stagedAppDir, 'tsconfig.json')
331
+ // Six apps under examples/ ship no tsconfig at all (ttf-bench and the ios-*
332
+ // family). Handing the checker no project at all is not neutral: it defaults
333
+ // `lib` to include lib.dom, and `@geastack/core` declares `document`,
334
+ // `window`, `console`, `Image`, `WebSocket` and the rest itself -- so every
335
+ // one of them collides ("Definitions of the following identifiers conflict")
336
+ // and `jsx` defaults to unset, which discards the element tree the plugin
337
+ // reads. What those apps need is not a new policy: it is the one their forty
338
+ // five siblings already state in their own tsconfig, so state it for them.
339
+ let config = {
340
+ compilerOptions: {
341
+ target: 'ES2022',
342
+ module: 'ESNext',
343
+ moduleResolution: 'Bundler',
344
+ lib: ['ES2022'],
345
+ strict: true,
346
+ noEmit: true,
347
+ skipLibCheck: true,
348
+ jsx: 'preserve',
349
+ },
350
+ include: ['**/*.ts', '**/*.tsx', '**/*.d.ts'],
351
+ }
352
+ if (fs.existsSync(configPath)) {
353
+ try {
354
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
355
+ } catch (error) {
356
+ // An app tsconfig this script cannot read is the app's to fix, not a
357
+ // reason to fail a build that would otherwise resolve every other module.
358
+ process.stdout.write(`gea-embedded-compat: leaving ${configPath} alone (${error.message})\n`)
359
+ return
360
+ }
361
+ }
362
+ const options = config.compilerOptions ?? (config.compilerOptions = {})
363
+ options.baseUrl = options.baseUrl ?? '.'
364
+ const base = path.resolve(stagedAppDir, options.baseUrl)
365
+ // Both specifiers, because the Vite config aliases both to the same
366
+ // `runtimePath` and an app may write either. `@geastack/core` is the one that
367
+ // matters for `declare module '@geastack/core' { interface
368
+ // GeaIntrinsicElements { ... } }`: left unmapped it resolves to the package's
369
+ // `index.d.ts` OUTSIDE the staged tree, while `JSX.IntrinsicElements` is wired
370
+ // to the staged `runtime.ts` -- so an app's own tags merge into a copy the JSX
371
+ // namespace never reads, and every use of one is `Property 'x' does not exist
372
+ // on type 'JSX.IntrinsicElements'` (companion/examples/gea-companion's
373
+ // `<symbol>`). That is the two-module-identities trap runtime.ts's own header
374
+ // warns about, reached here through a third copy that staging made.
375
+ const stagedRuntime = [path.relative(base, target)]
376
+ options.paths = { ...(options.paths ?? {}), 'gea-embedded': stagedRuntime, '@geastack/core': stagedRuntime }
377
+ // Staging mirrors `examples/node_modules` into the tree, and `@types/react`
378
+ // lives there. TypeScript auto-includes EVERY `@types/*` package it can reach
379
+ // unless `types` is stated, and React's declares a GLOBAL `JSX` namespace --
380
+ // so it does not merely add tags, it supplies the whole namespace. An app
381
+ // whose own JSX declarations resolve never notices. An app whose do not takes
382
+ // React's instead, and every `class` attribute becomes `Property 'class' does
383
+ // not exist ... Did you mean 'className'?` against `DetailedHTMLProps`.
384
+ //
385
+ // `virtual-list/env.d.ts` is the case: it states its types with
386
+ // `/// <reference types="@geastack/core" />`, and a triple-slash TYPES
387
+ // reference resolves through `typeRoots`/`node_modules` ONLY -- never through
388
+ // the `paths` set just above -- while no reachable `node_modules` carries an
389
+ // `@geastack` scope at all. So the reference silently resolves to nothing and
390
+ // React wins by default. These apps declare what they need in their own
391
+ // `.d.ts` and in the staged `runtime.ts`; none of them wants an ambient
392
+ // package. Say so, and keep an app's own `types` if it states one.
393
+ options.types = options.types ?? []
394
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`)
395
+ }
396
+
397
+ function resolveAppleNativeAliasModules(appDir) {
398
+ const appPackageBoundary = path.join(appDir, 'package.json')
399
+ const appRequire = createRequire(appPackageBoundary)
400
+ let threeSrcDir = ''
401
+ let threeUtilsModule = ''
402
+ let threeWebGLAnimationModule = ''
403
+ let threeWebXRManagerModule = ''
404
+ let troikaThreeTextModule = ''
405
+ let browserTroikaModule = ''
406
+ try {
407
+ threeSrcDir = path.dirname(fs.realpathSync(appRequire.resolve('three/src/Three.js')))
408
+ } catch {
409
+ // Native apps that do not use Three need no source-identity alias.
410
+ }
411
+ try {
412
+ threeWebGLAnimationModule = fs.realpathSync(appRequire.resolve('@geastack/native-webgl-angle/nativeWebGLAnimation'))
413
+ threeWebXRManagerModule = fs.realpathSync(appRequire.resolve('@geastack/native-webgl-angle/nativeWebXRManager'))
414
+ threeUtilsModule = fs.realpathSync(appRequire.resolve('@geastack/native-webgl-angle/nativeThreeUtils'))
415
+ } catch {
416
+ // A native app that does not use the ANGLE package needs no Three XR replacement.
417
+ }
418
+ try {
419
+ browserTroikaModule = appRequire.resolve('troika-three-text')
420
+ } catch {
421
+ // No browser Troika package means there is no package surface to replace.
422
+ }
423
+ try {
424
+ troikaThreeTextModule = fs.realpathSync(appRequire.resolve('@geastack/native-webgl-angle/troika-three-text'))
425
+ } catch {
426
+ if (browserTroikaModule) {
427
+ fail(
428
+ `Apple-native build resolved "troika-three-text" from ${appPackageBoundary}, but could not resolve ` +
429
+ `"@geastack/native-webgl-angle/troika-three-text" from the same application package boundary. ` +
430
+ 'Install the native package in the application; a bare Vite alias cannot be chained safely.'
431
+ )
432
+ }
433
+ }
434
+ return { threeSrcDir, threeUtilsModule, threeWebGLAnimationModule, threeWebXRManagerModule, troikaThreeTextModule }
435
+ }
436
+
437
+ function writeCompatViteConfig({ configPath, compatSrcDir, appDir, entry, viteOutDir, geaIrPath, appleNative, moduleGraphOutDir, envDefines }) {
438
+ const engineComponentsPath = path.join(engineRoot, 'components/index.ts')
439
+ const engineComponentSubpath = path.join(engineRoot, 'components/$1')
440
+ const elementsPath = path.join(elementsRoot, 'components/index.ts')
441
+ const elementSubpath = path.join(elementsRoot, 'components/$1')
442
+ // Only an apple-native build needs the apple package, and only an
443
+ // apple-native build emits the aliases below. Resolving it unconditionally
444
+ // made the (fail-closed) lookup a hard requirement of every ESP32, web and
445
+ // Linux build too.
446
+ const applePackageRoot = appleNative ? resolveApplePackageRoot(appDir) : ''
447
+ const appleRuntimeSubpath = applePackageRoot ? path.join(applePackageRoot, 'runtime/$1.js') : ''
448
+ const applePackagePath = applePackageRoot ? path.join(applePackageRoot, 'dist/index.js') : ''
449
+ const moduleGraphPluginPath = path.join(coreRoot, 'scripts/gea-vite-module-graph-plugin.mjs')
450
+ const { threeSrcDir, threeUtilsModule, threeWebGLAnimationModule, threeWebXRManagerModule, troikaThreeTextModule } = appleNative
451
+ ? resolveAppleNativeAliasModules(appDir)
452
+ : { threeSrcDir: '', threeUtilsModule: '', threeWebGLAnimationModule: '', threeWebXRManagerModule: '', troikaThreeTextModule: '' }
453
+ const q = (value) => JSON.stringify(value)
454
+ // Compile the @geajs/core reactive runtime (Component/Store/compiler-runtime)
455
+ // from its TYPED SOURCE, not the minified dist. The minified build reuses short
456
+ // identifiers across nested scopes, which the C++ symbol collector cannot
457
+ // disambiguate, and — with all types erased — geatsc is forced to lower the
458
+ // whole reactive runtime dynamically (gea_cpp_value, ill-formed auto-vectors,
459
+ // proxy derefs). From source, everything is typed and lowers natively, exactly
460
+ // like three.js/hono/mongodb. The env var still wins when explicitly set;
461
+ // otherwise resolve the package's source entry via its node_modules symlink.
462
+ let compilerRuntimeEntry = process.env.GEA_COMPILER_RUNTIME_SOURCE || ''
463
+ if (!compilerRuntimeEntry) {
464
+ for (const base of [appDir, coreRoot]) {
465
+ try {
466
+ const pkgDir = fs.realpathSync(path.join(base, 'node_modules/@geajs/core'))
467
+ const srcIndex = path.join(pkgDir, 'src/index.ts')
468
+ if (fs.existsSync(srcIndex) && fs.existsSync(path.join(pkgDir, 'src/compiler-runtime.ts'))) {
469
+ compilerRuntimeEntry = srcIndex
470
+ break
471
+ }
472
+ } catch {
473
+ // No @geajs/core source under this base (e.g. published-only install) —
474
+ // fall through; the alias stays empty and the dist runtime is used.
475
+ }
476
+ }
477
+ }
478
+ const compilerRuntimeAlias = compilerRuntimeEntry && fs.existsSync(compilerRuntimeEntry)
479
+ ? ` { find: new RegExp('^@geajs/core$'), replacement: ${q(compilerRuntimeEntry)} },\n`
480
+ : ''
481
+ const appleAliases = appleNative
482
+ ? ` { find: /^@geajs\\/apple\\/(.+)$/, replacement: ${q(appleRuntimeSubpath)} },
483
+ { find: /^@geastack\\/apple\\/(.+)$/, replacement: ${q(appleRuntimeSubpath)} },
484
+ { find: '@geajs/apple', replacement: ${q(applePackagePath)} },
485
+ { find: '@geastack/apple', replacement: ${q(applePackagePath)} },
486
+ `
487
+ : ''
488
+ // App .env vars, inlined as process.env.<KEY> string literals. geatsc compiles
489
+ // this bundle (not the browser), so the values are baked in at compile time and
490
+ // no `process` object exists at runtime — process.env.X is already the literal.
491
+ const defineEntries = Object.entries(envDefines || {})
492
+ .map(([key, literal]) => ` ${q(key)}: ${q(literal)},`)
493
+ .join('\n')
494
+ const config = `import { resolve } from 'node:path'
495
+ import { geaPlugin } from ${q(geaPluginResolved)}
496
+ import { geaAppleNativeModuleAliases, geaModuleGraphPlugins } from ${q(moduleGraphPluginPath)}
497
+
498
+ const root = ${q(compatSrcDir)}
499
+ const geaModuleGraph = geaModuleGraphPlugins({ outDir: ${q(moduleGraphOutDir || '')}, entryReachableOnly: true })
500
+ const geaAppleNativeAliases = geaAppleNativeModuleAliases({
501
+ threeSrcDir: ${q(threeSrcDir)},
502
+ threeUtilsModule: ${q(threeUtilsModule)},
503
+ threeWebGLAnimationModule: ${q(threeWebGLAnimationModule)},
504
+ threeWebXRManagerModule: ${q(threeWebXRManagerModule)},
505
+ troikaThreeTextModule: ${q(troikaThreeTextModule)},
506
+ })
507
+
508
+ export default {
509
+ root,
510
+ define: {
511
+ ${defineEntries}
512
+ },
513
+ plugins: [geaPlugin({ ir: { enabled: true, outFile: ${q(geaIrPath)} } }), ...geaModuleGraph],
514
+ resolve: {
515
+ alias: [
516
+ ${compilerRuntimeAlias}
517
+ { find: new RegExp('^@geastack/core$'), replacement: ${q(runtimePath)} },
518
+ { find: new RegExp('^@geastack/engine$'), replacement: ${q(engineComponentsPath)} },
519
+ { find: new RegExp('^@geastack/engine/components$'), replacement: ${q(engineComponentsPath)} },
520
+ { find: new RegExp('^@geastack/engine/components/(.+)$'), replacement: ${q(engineComponentSubpath)} },
521
+ { find: new RegExp('^@geastack/elements$'), replacement: ${q(elementsPath)} },
522
+ { find: new RegExp('^@geastack/elements/components$'), replacement: ${q(elementsPath)} },
523
+ { find: new RegExp('^@geastack/elements/components/(.+)$'), replacement: ${q(elementSubpath)} },
524
+ { find: new RegExp('^gea-embedded$'), replacement: ${q(runtimePath)} },
525
+ ${appleAliases}${appleNative ? ' ...geaAppleNativeAliases,\n' : ''} ],
526
+ },
527
+ build: {
528
+ lib: {
529
+ entry: resolve(root, ${q(entry)}),
530
+ formats: ['es'],
531
+ fileName: () => 'index.js',
532
+ cssFileName: 'index',
533
+ },
534
+ outDir: ${q(viteOutDir)},
535
+ emptyOutDir: true,
536
+ modulePreload: { polyfill: false },
537
+ minify: false,
538
+ cssCodeSplit: false,
539
+ // Vite 8 / rolldown defaults output.topLevelVar to true, which rewrites
540
+ // top-level \`const\`/\`let\` to \`var\` (a JS-runtime TDZ tweak). That is
541
+ // pointless here — geatsc compiles this bundle to C++, not a browser — and
542
+ // it erases const-ness that geatsc's static analysis relies on (e.g. a
543
+ // const array length / loop bound, which gates dropping typed-array bounds
544
+ // checks). Keep declarations as authored.
545
+ rollupOptions: { output: { topLevelVar: false } },
546
+ },
547
+ // JSON imports that survive source-level folding (see inlineJsonImports)
548
+ // must reach geatsc as an inline object literal, never as the
549
+ // JSON.parse("...") string form vite's stringify:'auto' emits past 10kB —
550
+ // geatsc can only statically type the literal form.
551
+ json: { stringify: false },
552
+ }
553
+ `
554
+ fs.writeFileSync(configPath, config)
555
+ }
556
+
557
+ function posixPath(value) {
558
+ return value.split(path.sep).join('/')
559
+ }
560
+
561
+ function relativeModuleSpecifier(fromDir, toFile) {
562
+ let specifier = posixPath(path.relative(fromDir, toFile))
563
+ if (!specifier.startsWith('.')) specifier = `./${specifier}`
564
+ return specifier
565
+ }
566
+
567
+ function writeAppendJsCompatEntry({ compatSrcDir, compatEntry, appendFiles }) {
568
+ if (appendFiles.length === 0) return compatEntry
569
+ const entryDirRel = path.dirname(compatEntry)
570
+ const entryDirAbs = path.join(compatSrcDir, entryDirRel)
571
+ const entryAbs = path.join(compatSrcDir, compatEntry)
572
+ const wrapperRel = path.join(entryDirRel, '__gea_appended_entry.tsx')
573
+ const wrapperAbs = path.join(compatSrcDir, wrapperRel)
574
+ const entryImport = relativeModuleSpecifier(path.dirname(wrapperAbs), entryAbs)
575
+ const parts = [`import ${JSON.stringify(entryImport)}\n`]
576
+ for (const appendFile of appendFiles) {
577
+ if (!fs.existsSync(appendFile)) fail(`missing appended JS file: ${appendFile}`)
578
+ parts.push(`\n// appended from ${posixPath(appendFile)}\n${fs.readFileSync(appendFile, 'utf8')}\n`)
579
+ }
580
+ writeFileIfChanged(wrapperAbs, parts.join(''))
581
+ return wrapperRel
582
+ }
583
+
584
+ function stripCssComments(css) {
585
+ return css.replace(/\/\*[\s\S]*?\*\//g, '')
586
+ }
587
+
588
+ function stripAtBlocks(css) {
589
+ return css.replace(/@[^{;]+(?:;|\{[\s\S]*?\})/g, '')
590
+ }
591
+
592
+ function splitTopLevelCss(value, delimiter) {
593
+ const out = []
594
+ let start = 0
595
+ let depth = 0
596
+ for (let i = 0; i < value.length; i++) {
597
+ const char = value[i]
598
+ if (char === '(') depth++
599
+ else if (char === ')' && depth > 0) depth--
600
+ else if (char === delimiter && depth === 0) {
601
+ out.push(value.slice(start, i).trim())
602
+ start = i + 1
603
+ }
604
+ }
605
+ out.push(value.slice(start).trim())
606
+ return out.filter(Boolean)
607
+ }
608
+
609
+ function splitCssWords(value) {
610
+ const out = []
611
+ let i = 0
612
+ while (i < value.length) {
613
+ while (i < value.length && /\s/.test(value[i])) i++
614
+ const start = i
615
+ let depth = 0
616
+ while (i < value.length) {
617
+ const char = value[i]
618
+ if (char === '(') depth++
619
+ else if (char === ')' && depth > 0) depth--
620
+ else if (/\s/.test(char) && depth === 0) break
621
+ i++
622
+ }
623
+ if (i > start) out.push(value.slice(start, i))
624
+ }
625
+ return out
626
+ }
627
+
628
+ function findCssFiles(dir) {
629
+ const files = []
630
+ const stack = [dir]
631
+ while (stack.length > 0) {
632
+ const current = stack.pop()
633
+ if (!current || !fs.existsSync(current)) continue
634
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
635
+ const full = path.join(current, entry.name)
636
+ if (entry.isDirectory()) stack.push(full)
637
+ else if (entry.isFile() && entry.name.endsWith('.css')) files.push(full)
638
+ }
639
+ }
640
+ files.sort()
641
+ return files
642
+ }
643
+
644
+ function usesGeaEmbeddedCompat(appDir) {
645
+ const transformable = new Set(['.js', '.jsx', '.ts', '.tsx'])
646
+ const stack = [appDir]
647
+ while (stack.length > 0) {
648
+ const current = stack.pop()
649
+ if (!current || !fs.existsSync(current)) continue
650
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
651
+ const full = path.join(current, entry.name)
652
+ if (entry.isDirectory()) {
653
+ if (['node_modules', 'dist', 'build', '.vite'].includes(entry.name)) continue
654
+ stack.push(full)
655
+ continue
656
+ }
657
+ if (!entry.isFile() || !transformable.has(path.extname(entry.name))) continue
658
+ const code = fs.readFileSync(full, 'utf8')
659
+ if (code.includes("'@geastack/core'") || code.includes('"@geastack/core"')) return true
660
+ }
661
+ }
662
+ return false
663
+ }
664
+
665
+ function usesAppleNative(appDir) {
666
+ const transformable = new Set(['.js', '.jsx', '.ts', '.tsx'])
667
+ const stack = [appDir]
668
+ while (stack.length > 0) {
669
+ const current = stack.pop()
670
+ if (!current || !fs.existsSync(current)) continue
671
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
672
+ const full = path.join(current, entry.name)
673
+ if (entry.isDirectory()) {
674
+ if (['node_modules', 'dist', 'build', '.vite'].includes(entry.name)) continue
675
+ stack.push(full)
676
+ continue
677
+ }
678
+ if (!entry.isFile() || !transformable.has(path.extname(entry.name))) continue
679
+ const code = fs.readFileSync(full, 'utf8')
680
+ if (
681
+ code.includes("'@geajs/apple/") ||
682
+ code.includes('"@geajs/apple/') ||
683
+ code.includes("'@geastack/apple/") ||
684
+ code.includes('"@geastack/apple/')
685
+ ) return true
686
+ }
687
+ }
688
+ return false
689
+ }
690
+
691
+ function escapeRegExp(value) {
692
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
693
+ }
694
+
695
+ function typeTextReferencesName(typeText, name) {
696
+ return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(typeText)
697
+ }
698
+
699
+ function resolveTypeImportPath(base) {
700
+ for (const candidate of [
701
+ base,
702
+ `${base}.d.ts`,
703
+ `${base}.ts`,
704
+ `${base}.tsx`,
705
+ path.join(base, 'index.d.ts'),
706
+ path.join(base, 'index.ts'),
707
+ path.join(base, 'index.tsx'),
708
+ ]) {
709
+ if (fs.existsSync(candidate)) return candidate
710
+ }
711
+ return null
712
+ }
713
+
714
+ function typeImportModuleSpecifier(resolvedPath) {
715
+ return resolvedPath
716
+ .replace(/\\/g, '/')
717
+ .replace(/\.d\.ts$/, '')
718
+ .replace(/\.(?:tsx?|jsx?)$/, '')
719
+ }
720
+
721
+ function resolveTypeImportModule(importingFile, source) {
722
+ if (source === '@geastack/core') {
723
+ return typeImportModuleSpecifier(path.join(coreRoot, 'index.d.ts'))
724
+ }
725
+ if (source === '@geastack/engine' || source === '@geastack/engine/components') {
726
+ return typeImportModuleSpecifier(path.join(engineRoot, 'components/index.d.ts'))
727
+ }
728
+ if (source === '@geastack/elements' || source === '@geastack/elements/components') {
729
+ return typeImportModuleSpecifier(path.join(elementsRoot, 'components/index.d.ts'))
730
+ }
731
+ if (source.startsWith('@geastack/core/')) {
732
+ const resolved = resolveTypeImportPath(path.join(coreRoot, source.slice('@geastack/core/'.length)))
733
+ return resolved ? typeImportModuleSpecifier(resolved) : source
734
+ }
735
+ if (source.startsWith('@geastack/engine/components/')) {
736
+ const resolved = resolveTypeImportPath(path.join(engineRoot, 'components', source.slice('@geastack/engine/components/'.length)))
737
+ return resolved ? typeImportModuleSpecifier(resolved) : source
738
+ }
739
+ if (source.startsWith('@geastack/engine/')) {
740
+ const resolved = resolveTypeImportPath(path.join(engineRoot, source.slice('@geastack/engine/'.length)))
741
+ return resolved ? typeImportModuleSpecifier(resolved) : source
742
+ }
743
+ if (source.startsWith('@geastack/elements/components/')) {
744
+ const resolved = resolveTypeImportPath(path.join(elementsRoot, 'components', source.slice('@geastack/elements/components/'.length)))
745
+ return resolved ? typeImportModuleSpecifier(resolved) : source
746
+ }
747
+ if (source.startsWith('@geastack/elements/')) {
748
+ const resolved = resolveTypeImportPath(path.join(elementsRoot, source.slice('@geastack/elements/'.length)))
749
+ return resolved ? typeImportModuleSpecifier(resolved) : source
750
+ }
751
+ if (source.startsWith('.') || source.startsWith('/')) {
752
+ const base = source.startsWith('/') ? source : path.resolve(path.dirname(importingFile), source)
753
+ const resolved = resolveTypeImportPath(base)
754
+ return typeImportModuleSpecifier(resolved ?? base)
755
+ }
756
+ return source
757
+ }
758
+
759
+ function renderTypeImports(imports) {
760
+ const bySource = new Map()
761
+ for (const info of imports) {
762
+ const source = resolveTypeImportModule(info.file, info.source)
763
+ const group = bySource.get(source) ?? []
764
+ group.push(info)
765
+ bySource.set(source, group)
766
+ }
767
+ // Emit each external type as a global `type X = import('mod').Y` ALIAS rather
768
+ // than an `import type { … }` statement. An import statement would make the
769
+ // sidecar a module, so its declarations (and the bundle's `/// <reference>` to
770
+ // it) would no longer be ambient — the restored JSDoc `@type {GeaEmbeddedImage[]}`
771
+ // in the bundle could not resolve the name. An import-type alias is a type
772
+ // expression (not an import statement), so the sidecar stays a script and the
773
+ // alias is globally visible.
774
+ return [...bySource.entries()]
775
+ .sort(([a], [b]) => a.localeCompare(b))
776
+ .flatMap(([source, group]) =>
777
+ group
778
+ .sort((a, b) => a.local.localeCompare(b.local))
779
+ .map((info) => `type ${info.local} = import(${JSON.stringify(source)}).${info.imported}`)
780
+ )
781
+ }
782
+
783
+ // True for `T[]` / `Array<T>` / `ReadonlyArray<T>` where T is a named type
784
+ // reference (an interface/type alias). Excludes primitive-element arrays
785
+ // (`number[]`, `string[]`) and non-array annotations. Babel TS AST.
786
+ function isNamedInterfaceArrayTypeNode(typeNode) {
787
+ if (t.isTSArrayType(typeNode)) {
788
+ return t.isTSTypeReference(typeNode.elementType) && t.isIdentifier(typeNode.elementType.typeName)
789
+ }
790
+ if (t.isTSTypeReference(typeNode) && t.isIdentifier(typeNode.typeName)) {
791
+ const name = typeNode.typeName.name
792
+ if (name !== 'Array' && name !== 'ReadonlyArray') return false
793
+ const args = typeNode.typeParameters?.params
794
+ if (!args || args.length !== 1) return false
795
+ return t.isTSTypeReference(args[0]) && t.isIdentifier(args[0].typeName)
796
+ }
797
+ return false
798
+ }
799
+
800
+ // True for `string[]` / `number[]` / `boolean[]` (and the `Array<…>` form). An
801
+ // empty primitive-element array annotation is just as lossy in the bundle as a
802
+ // named one — `const keys: string[] = []` infers `any[]` once the pushes live in
803
+ // other functions — so it gets the same JSDoc restoration.
804
+ function isPrimitiveArrayTypeNode(typeNode) {
805
+ const isPrimitiveElement = (el) =>
806
+ !!el && (t.isTSStringKeyword(el) || t.isTSNumberKeyword(el) || t.isTSBooleanKeyword(el))
807
+ if (t.isTSArrayType(typeNode)) return isPrimitiveElement(typeNode.elementType)
808
+ if (t.isTSTypeReference(typeNode) && t.isIdentifier(typeNode.typeName)) {
809
+ const name = typeNode.typeName.name
810
+ if (name !== 'Array' && name !== 'ReadonlyArray') return false
811
+ const args = typeNode.typeParameters?.params
812
+ return !!args && args.length === 1 && isPrimitiveElement(args[0])
813
+ }
814
+ return false
815
+ }
816
+
817
+ // Array annotations worth restoring as JSDoc: named-element (interface/alias) or
818
+ // primitive-element. Both lower to a typed `std::vector<…>` instead of a boxed
819
+ // `std::vector<gea_cpp_value>` once the element type is known.
820
+ function isHintableArrayTypeNode(typeNode) {
821
+ return isNamedInterfaceArrayTypeNode(typeNode) || isPrimitiveArrayTypeNode(typeNode)
822
+ }
823
+
824
+ // A bare named-type reference (`GeaEmbeddedImage`, `FetchResponse`, an app
825
+ // interface) — NOT an array. Bundling erases it to `any` -> `gea_cpp_value`, so a
826
+ // field/var/return typed this way loses its static shape (and any `nativeTypes`
827
+ // host-handle lowering). Restored as JSDoc, it lowers to the typed struct/native
828
+ // type instead. Excludes `Array`/`ReadonlyArray` (handled by the array forms).
829
+ function isNamedTypeReferenceNode(typeNode) {
830
+ if (!t.isTSTypeReference(typeNode) || !t.isIdentifier(typeNode.typeName)) return false
831
+ const name = typeNode.typeName.name
832
+ if (name === 'Array' || name === 'ReadonlyArray') return false
833
+ return !typeNode.typeParameters
834
+ }
835
+
836
+ // Field/var/return annotations worth restoring: typed arrays or bare named types.
837
+ function isHintableTypeNode(typeNode) {
838
+ return isHintableArrayTypeNode(typeNode) || isNamedTypeReferenceNode(typeNode)
839
+ }
840
+
841
+
842
+ function isJsonParseCallNode(expr) {
843
+ return (
844
+ t.isCallExpression(expr) &&
845
+ t.isMemberExpression(expr.callee) &&
846
+ !expr.callee.computed &&
847
+ t.isIdentifier(expr.callee.object, { name: 'JSON' }) &&
848
+ t.isIdentifier(expr.callee.property, { name: 'parse' })
849
+ )
850
+ }
851
+
852
+ // `const x = JSON.parse(...) as T` (or `const x: T = JSON.parse(...)`): the
853
+ // bundler erases the `as` cast / annotation, so capture the target type text and
854
+ // restore it as a JSDoc `@type`. geatsc then decodes the parse straight into the
855
+ // typed struct (one pass, no gea_cpp_value tree) instead of boxing. Gated on a
856
+ // JSON.parse initializer so only the exact typed-parse case is promoted.
857
+ function jsonParseCastTypeText(declarator, code) {
858
+ if (!declarator.init) return null
859
+ if (t.isTSAsExpression(declarator.init) && isJsonParseCallNode(declarator.init.expression)) {
860
+ const typeNode = declarator.init.typeAnnotation
861
+ if (typeNode?.start != null && typeNode?.end != null) return code.slice(typeNode.start, typeNode.end).trim()
862
+ }
863
+ if (isJsonParseCallNode(declarator.init)) {
864
+ const typeNode = declarator.id.typeAnnotation?.typeAnnotation
865
+ if (typeNode?.start != null && typeNode?.end != null) return code.slice(typeNode.start, typeNode.end).trim()
866
+ }
867
+ return null
868
+ }
869
+
870
+ function collectBundledTypeHints(appDir, entry) {
871
+ const declarations = new Map()
872
+ const typeImports = new Map()
873
+ const returnHints = new Map()
874
+ const paramHints = new Map()
875
+ const classHints = new Map()
876
+ // Class field type annotations (`hours: Forecast[]`). Bundling strips them,
877
+ // so we restore them as JSDoc `@type` and pull the referenced interface into
878
+ // the sidecar. Needed for typed store-array storage that reuses geatsc's
879
+ // `__gea_type_<Name>` struct — the element interface (e.g. `Forecast`) is
880
+ // referenced ONLY by class properties, so without this it never reaches the
881
+ // geatsc checker and the struct is never emitted. Keyed by `Class#prop`.
882
+ const propertyHints = new Map()
883
+ // Function-local typed arrays (`const rows: Row[] = []`) also lose their
884
+ // annotations in the bundle. Restore these as local JSDoc so helper
885
+ // functions that build typed store arrays do not box each row through
886
+ // `std::vector<gea_cpp_value>`. Keyed by `Function#local` or
887
+ // `Class#method#local`.
888
+ const localHints = new Map()
889
+ // Param/return annotations on arrow- and function-EXPRESSIONS that are NOT
890
+ // directly assigned to a named binding — returned closures (`return (x:
891
+ // number) => …`), callback arguments (`arr.map((v: Row) => …)`), and the
892
+ // like. The name-keyed paramHints/returnHints above only reach NAMED
893
+ // functions, methods, and `const f = (…) => …` declarators, so these
894
+ // anonymous callables otherwise lose their param types and every parameter
895
+ // widens to `gea_cpp_value` (the bundle is `.js`, so `isParameterOptional`
896
+ // treats every un-hinted param as optional and the emitter erases the slot —
897
+ // boxing each call's args). Restored as INLINE JSDoc on the callable node.
898
+ // Keyed by `${enclosingHintScope}|${paramNamesJoined}`; the value is the
899
+ // ORDERED list of callables sharing that key, consumed positionally on apply
900
+ // (vite preserves intra-scope expression order), so repeated shapes still map
901
+ // 1:1. Matching on the exact param-name tuple makes a wrong-node application
902
+ // essentially impossible — a mismatch just skips (no regression).
903
+ const inlineCallableHints = new Map()
904
+ const collectInlineCallableHint = (astPath, code) => {
905
+ const node = astPath.node
906
+ const parent = astPath.parent
907
+ // `const f = (x) => …` / `const f = function (x) {}` already restore via the
908
+ // name-keyed VariableDeclarator path; skip to avoid redundant JSDoc.
909
+ if (parent && t.isVariableDeclarator(parent) && parent.init === node) return
910
+ // The structural param-name key only matches across bundling when every
911
+ // parameter is a plain identifier; destructuring/rest params break it.
912
+ if (!node.params.every((p) => t.isIdentifier(p))) return
913
+ const params = []
914
+ for (const param of node.params) {
915
+ const typeNode = param.typeAnnotation?.typeAnnotation
916
+ if (!typeNode || typeNode.start == null || typeNode.end == null) continue
917
+ params.push({ name: param.name, type: code.slice(typeNode.start, typeNode.end).trim(), optional: param.optional === true })
918
+ }
919
+ const returnTypeNode = node.returnType?.typeAnnotation
920
+ const returnType =
921
+ returnTypeNode && returnTypeNode.start != null && returnTypeNode.end != null
922
+ ? code.slice(returnTypeNode.start, returnTypeNode.end).trim()
923
+ : undefined
924
+ if (params.length === 0 && !returnType) return
925
+ const scope = resolveFunctionHintName(astPath) ?? MODULE_HINT_SCOPE
926
+ const key = `${scope}|${node.params.map((p) => p.name).join(',')}`
927
+ const list = inlineCallableHints.get(key) ?? []
928
+ list.push({ params, returnType })
929
+ inlineCallableHints.set(key, list)
930
+ }
931
+ const collectParameterHints = (functionName, params, code) => {
932
+ const hints = []
933
+ for (const param of params) {
934
+ let name = null
935
+ let typeNode = null
936
+ let optional = false
937
+ let rest = false
938
+ if (t.isIdentifier(param)) {
939
+ name = param.name
940
+ typeNode = param.typeAnnotation?.typeAnnotation
941
+ optional = param.optional === true
942
+ } else if (t.isRestElement(param) && t.isIdentifier(param.argument)) {
943
+ name = param.argument.name
944
+ typeNode = param.typeAnnotation?.typeAnnotation ?? param.argument.typeAnnotation?.typeAnnotation
945
+ rest = true
946
+ }
947
+ if (!name || !typeNode) continue
948
+ if (typeNode.start == null || typeNode.end == null) continue
949
+ let typeText = code.slice(typeNode.start, typeNode.end).trim()
950
+ if (rest) {
951
+ const restElementType =
952
+ t.isTSArrayType(typeNode) && typeNode.elementType.start != null && typeNode.elementType.end != null
953
+ ? code.slice(typeNode.elementType.start, typeNode.elementType.end).trim()
954
+ : t.isTSTypeReference(typeNode) &&
955
+ t.isIdentifier(typeNode.typeName) &&
956
+ (typeNode.typeName.name === 'Array' || typeNode.typeName.name === 'ReadonlyArray') &&
957
+ typeNode.typeParameters?.params?.length === 1 &&
958
+ typeNode.typeParameters.params[0].start != null &&
959
+ typeNode.typeParameters.params[0].end != null
960
+ ? code.slice(typeNode.typeParameters.params[0].start, typeNode.typeParameters.params[0].end).trim()
961
+ : typeText
962
+ typeText = `...${restElementType}`
963
+ }
964
+ // Carry the `?` through to the restored JSDoc (`@param {T} [name]`), so
965
+ // the checker types the bundled param `T | undefined` and geatsc keeps a
966
+ // null-capable native slot (std::optional<record>) instead of widening.
967
+ hints.push({ name, type: typeText, optional })
968
+ }
969
+ if (hints.length > 0) paramHints.set(functionName, hints)
970
+ }
971
+ const classNoRuntimeBridgeDirective = (node) =>
972
+ node.leadingComments?.some((comment) => /@gea-no-runtime-bridge\b/.test(comment.value)) === true
973
+ // The gea-embedded runtime's public API (loadImage, loadImageFile, …) is
974
+ // inlined into the bundle by vite, so its signatures vanish exactly like app
975
+ // code's. Walk the runtime sources FIRST so an app function with the same
976
+ // name overrides the runtime hint.
977
+ const hintSourceFiles = [
978
+ ...collectSourceFiles(path.join(coreRoot, 'runtime')),
979
+ ...collectReachableSourceFiles(appDir, entry),
980
+ ]
981
+ for (const file of hintSourceFiles) {
982
+ const code = fs.readFileSync(file, 'utf8')
983
+ const ast = parseTsx(code)
984
+ traverse(ast, {
985
+ ImportDeclaration(astPath) {
986
+ const node = astPath.node
987
+ const source = node.source?.value
988
+ if (typeof source !== 'string') return
989
+ const declarationTypeOnly = node.importKind === 'type'
990
+ for (const specifier of node.specifiers) {
991
+ if (!t.isImportSpecifier(specifier)) continue
992
+ if (!declarationTypeOnly && specifier.importKind !== 'type') continue
993
+ if (!t.isIdentifier(specifier.local)) continue
994
+ const imported = t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value
995
+ typeImports.set(specifier.local.name, { file, source, imported, local: specifier.local.name })
996
+ }
997
+ },
998
+ TSInterfaceDeclaration(astPath) {
999
+ const node = astPath.node
1000
+ if (!node.id?.name || node.start == null || node.end == null) return
1001
+ declarations.set(node.id.name, code.slice(node.start, node.end).replace(/^export\s+/, '').trim())
1002
+ },
1003
+ TSTypeAliasDeclaration(astPath) {
1004
+ const node = astPath.node
1005
+ if (!node.id?.name || node.start == null || node.end == null) return
1006
+ declarations.set(node.id.name, code.slice(node.start, node.end).replace(/^export\s+/, '').trim())
1007
+ },
1008
+ FunctionDeclaration(astPath) {
1009
+ const node = astPath.node
1010
+ if (!node.id?.name) return
1011
+ collectParameterHints(node.id.name, node.params, code)
1012
+ if (!node.returnType?.typeAnnotation) return
1013
+ const typeNode = node.returnType.typeAnnotation
1014
+ if (typeNode.start == null || typeNode.end == null) return
1015
+ returnHints.set(node.id.name, code.slice(typeNode.start, typeNode.end).trim())
1016
+ },
1017
+ ArrowFunctionExpression(astPath) {
1018
+ collectInlineCallableHint(astPath, code)
1019
+ },
1020
+ FunctionExpression(astPath) {
1021
+ collectInlineCallableHint(astPath, code)
1022
+ },
1023
+ Class(astPath) {
1024
+ const node = astPath.node
1025
+ if (!node.id?.name) return
1026
+ if (classNoRuntimeBridgeDirective(node)) classHints.set(node.id.name, '@gea-no-runtime-bridge')
1027
+ },
1028
+ VariableDeclarator(astPath) {
1029
+ const node = astPath.node
1030
+ if (!t.isIdentifier(node.id)) return
1031
+ if (node.init && (t.isArrowFunctionExpression(node.init) || t.isFunctionExpression(node.init))) {
1032
+ collectParameterHints(node.id.name, node.init.params, code)
1033
+ const typeNode = node.init.returnType?.typeAnnotation
1034
+ if (typeNode && typeNode.start != null && typeNode.end != null) {
1035
+ returnHints.set(node.id.name, code.slice(typeNode.start, typeNode.end).trim())
1036
+ }
1037
+ }
1038
+ const jsonParseType = jsonParseCastTypeText(node, code)
1039
+ if (jsonParseType) {
1040
+ const fnName = resolveFunctionHintName(astPath)
1041
+ if (fnName) localHints.set(`${fnName}#${node.id.name}`, jsonParseType)
1042
+ return
1043
+ }
1044
+ const localTypeNode = node.id.typeAnnotation?.typeAnnotation
1045
+ if (!localTypeNode || localTypeNode.start == null || localTypeNode.end == null) return
1046
+ // Hintable arrays AND named type references (`const bytes: Uint8Array =
1047
+ // await res.arrayBuffer()`): bundling erases the annotation, the checker
1048
+ // types the local `any` → gea_cpp_value, and a native-typed initializer
1049
+ // (the fetch body's std::vector<std::uint8_t>) gets BOXED at the
1050
+ // assignment — per-byte, catastrophically. Named refs are the same hint
1051
+ // class ClassProperty already restores.
1052
+ if (!isHintableTypeNode(localTypeNode)) return
1053
+ // Module-level typed arrays (`const cacheImgs: GeaEmbeddedImage[] = []`)
1054
+ // lose their element type the same way function-local ones do, so key
1055
+ // them under a module scope rather than skipping.
1056
+ const functionName = resolveFunctionHintName(astPath) ?? MODULE_HINT_SCOPE
1057
+ localHints.set(`${functionName}#${node.id.name}`, code.slice(localTypeNode.start, localTypeNode.end).trim())
1058
+ },
1059
+ // Class method params carry the same annotations as free functions
1060
+ // (`multiply(other: Quaternion)`), but bundling strips them and the
1061
+ // checker then types `other` as `any` -> `gea_cpp_value`, losing the
1062
+ // concrete fields. Restore them as JSDoc keyed by `Class#method` so the
1063
+ // qualified name avoids collisions when two classes share a method name.
1064
+ ClassMethod(astPath) {
1065
+ const node = astPath.node
1066
+ if ((node.kind !== 'method' && node.kind !== 'constructor') || !t.isIdentifier(node.key)) return
1067
+ const classPath = astPath.findParent((p) => p.isClassDeclaration() || p.isClassExpression())
1068
+ const className = classPath?.node?.id?.name
1069
+ if (!className) return
1070
+ const key = `${className}#${node.kind === 'constructor' ? 'constructor' : node.key.name}`
1071
+ collectParameterHints(key, node.params, code)
1072
+ const typeNode = node.returnType?.typeAnnotation
1073
+ if (typeNode && typeNode.start != null && typeNode.end != null) {
1074
+ returnHints.set(key, code.slice(typeNode.start, typeNode.end).trim())
1075
+ }
1076
+ },
1077
+ // Class field annotations carry the element type of typed store arrays
1078
+ // (`hours: Forecast[]`). Bundling drops them; capture the annotation text
1079
+ // keyed by `Class#prop` so it can be re-applied as a JSDoc `@type`.
1080
+ //
1081
+ // Restricted to arrays whose element is a NAMED type reference
1082
+ // (`Forecast[]` / `Array<Forecast>`). This is exactly the typed
1083
+ // store-array reuse case the `__gea_type_<Name>` lowering targets.
1084
+ // Restoring annotations for primitive arrays (`number[]`) or scalars is
1085
+ // unnecessary here and would promote fields geatsc otherwise keeps
1086
+ // dynamic — exposing unrelated typed-storage lowering paths.
1087
+ ClassProperty(astPath) {
1088
+ const node = astPath.node
1089
+ if (node.computed || !t.isIdentifier(node.key)) return
1090
+ const typeNode = node.typeAnnotation?.typeAnnotation
1091
+ if (!typeNode || typeNode.start == null || typeNode.end == null) return
1092
+ const classPath = astPath.findParent((p) => p.isClassDeclaration() || p.isClassExpression())
1093
+ const className = classPath?.node?.id?.name
1094
+ if (!className) return
1095
+ // Named-element arrays (`Forecast[]`) and bare named-type fields (a host
1096
+ // handle like `drone: GeaEmbeddedImage`) keep their static type.
1097
+ // Primitive arrays (`number[]`) are restored too: the element-write
1098
+ // lowering gap that once forced them dynamic (`q[i] = q[i+1]` swaps
1099
+ // miscompiling on a typed field) is fixed — geatsc now lowers typed
1100
+ // field element writes through `gea::runtime::array::vector_set`
1101
+ // (pinned by compiler-js-callback-param-typing.test.ts's sibling
1102
+ // class-field cases). Without the restoration, a `fetchQueue:
1103
+ // number[]` store field stays `std::vector<gea_cpp_value>` and every
1104
+ // queue operation boxes.
1105
+ //
1106
+ // `@gea-no-runtime-bridge` marks hand-written native adapter classes.
1107
+ // Their annotated scalar fields are part of the typed ABI boundary
1108
+ // (`__nativeHandle: number`, `canvasWidth: () => number`, etc.), so
1109
+ // restore every explicit field annotation for those classes only.
1110
+ const restoreAllAnnotatedFields = classNoRuntimeBridgeDirective(classPath.node)
1111
+ if (
1112
+ !restoreAllAnnotatedFields &&
1113
+ !isNamedInterfaceArrayTypeNode(typeNode) &&
1114
+ !isNamedTypeReferenceNode(typeNode) &&
1115
+ !isPrimitiveArrayTypeNode(typeNode)
1116
+ ) return
1117
+ propertyHints.set(`${className}#${node.key.name}`, code.slice(typeNode.start, typeNode.end).trim())
1118
+ },
1119
+ // A `class X extends ReactiveComponent<T>` (or `Component<T>`) gives
1120
+ // `this.el` the element type `T | null` (Component<RootElement> declares
1121
+ // `el: RootElement | null`). The lean ReactiveComponent transform strips
1122
+ // the base and its inherited `el`; the framework re-injects a plain `el`
1123
+ // field (vendor/gea transform.ts), but bundling leaves it UNTYPED so geatsc
1124
+ // boxes it to `gea_cpp_value` — and a typed canvas element must stay native
1125
+ // (it maps to `gea::embedded::ui::NodeHandle`, so `getContext('2d')` and the
1126
+ // mount's root binding flow with zero boxing). `el` has no source field to
1127
+ // annotate, so recover `T` from the heritage generic and key the hint under
1128
+ // `Class#el`; the ClassProperty restorer then stamps `@type {T | null}` onto
1129
+ // the injected field. Mirrors a non-stripped `Component<T>` whose `el`
1130
+ // already lowers to a NodeHandle via nativeTypes.
1131
+ Class(astPath) {
1132
+ const node = astPath.node
1133
+ if (!t.isIdentifier(node.id) || !t.isIdentifier(node.superClass)) return
1134
+ if (node.superClass.name !== 'ReactiveComponent' && node.superClass.name !== 'Component') return
1135
+ const typeArgs = node.superTypeParameters ?? node.superTypeArguments
1136
+ const typeArg = typeArgs?.params?.[0]
1137
+ if (!typeArg || typeArg.start == null || typeArg.end == null) return
1138
+ propertyHints.set(`${node.id.name}#el`, `${code.slice(typeArg.start, typeArg.end).trim()} | null`)
1139
+ },
1140
+ })
1141
+ }
1142
+
1143
+ const neededTypeNames = new Set()
1144
+ for (const returnType of returnHints.values()) {
1145
+ for (const name of declarations.keys()) {
1146
+ if (typeTextReferencesName(returnType, name)) neededTypeNames.add(name)
1147
+ }
1148
+ }
1149
+ for (const params of paramHints.values()) {
1150
+ for (const param of params) {
1151
+ for (const name of declarations.keys()) {
1152
+ if (typeTextReferencesName(param.type, name)) neededTypeNames.add(name)
1153
+ }
1154
+ }
1155
+ }
1156
+ for (const propertyType of propertyHints.values()) {
1157
+ for (const name of declarations.keys()) {
1158
+ if (typeTextReferencesName(propertyType, name)) neededTypeNames.add(name)
1159
+ }
1160
+ }
1161
+ for (const localType of localHints.values()) {
1162
+ for (const name of declarations.keys()) {
1163
+ if (typeTextReferencesName(localType, name)) neededTypeNames.add(name)
1164
+ }
1165
+ }
1166
+ // Transitively pull in type names referenced *inside* an already-needed
1167
+ // declaration. `type ControlButton = { id: ControlButtonId }` is named only
1168
+ // by a `@returns {ControlButton[]}`; without following the reference to
1169
+ // `ControlButtonId`, that nested alias is dropped from the sidecar and the
1170
+ // checker types `id` as `any` -> gea_cpp_value (losing static lowering and,
1171
+ // for sky-hop, exposing a heap-corruption crash on the dynamic value's
1172
+ // destructor). Fixpoint over declaration bodies so the whole referenced
1173
+ // closure (nested records, `A & B` intersections, string-literal unions) is
1174
+ // preserved. Self-references terminate because the set only grows and is
1175
+ // bounded by `declarations`.
1176
+ let addedTransitive = true
1177
+ while (addedTransitive) {
1178
+ addedTransitive = false
1179
+ for (const needed of [...neededTypeNames]) {
1180
+ const declText = declarations.get(needed)
1181
+ if (!declText) continue
1182
+ for (const name of declarations.keys()) {
1183
+ if (neededTypeNames.has(name)) continue
1184
+ if (typeTextReferencesName(declText, name)) {
1185
+ neededTypeNames.add(name)
1186
+ addedTransitive = true
1187
+ }
1188
+ }
1189
+ }
1190
+ }
1191
+ if (
1192
+ returnHints.size === 0 &&
1193
+ paramHints.size === 0 &&
1194
+ classHints.size === 0 &&
1195
+ propertyHints.size === 0 &&
1196
+ localHints.size === 0 &&
1197
+ inlineCallableHints.size === 0
1198
+ ) {
1199
+ return { imports: [], declarations: [], returnHints, paramHints, classHints, propertyHints, localHints, inlineCallableHints }
1200
+ }
1201
+ const declarationTexts = [...neededTypeNames].map((name) => declarations.get(name)).filter(Boolean)
1202
+ // Names emitted as LOCAL declarations in the sidecar. A name re-emitted as
1203
+ // BOTH a local declaration and a re-imported type collides (two competing
1204
+ // symbols for the same name). Subtract them: a locally-declared interface
1205
+ // (`interface CityState {…}`) trivially matches `/\bCityState\b/` against its
1206
+ // OWN declaration text, so without this guard `typeTextReferencesName` would
1207
+ // wrongly re-emit `import type { CityState }` next to the local declaration.
1208
+ const locallyDeclaredNames = new Set(
1209
+ [...neededTypeNames].filter((name) => declarations.get(name))
1210
+ )
1211
+ // A hint can reference an EXTERNAL type directly (not via a local declaration)
1212
+ // — e.g. `const cacheImgs: GeaEmbeddedImage[] = []` where `GeaEmbeddedImage` is
1213
+ // a type-only import from 'gea-embedded'. Re-import any such type so the
1214
+ // restored `@type {GeaEmbeddedImage[]}` resolves in the sidecar.
1215
+ const hintTypeTexts = [
1216
+ ...returnHints.values(),
1217
+ ...[...paramHints.values()].flatMap((params) => params.map((param) => param.type)),
1218
+ ...propertyHints.values(),
1219
+ ...localHints.values(),
1220
+ ...[...inlineCallableHints.values()].flatMap((entries) =>
1221
+ entries.flatMap((entry) => [...entry.params.map((param) => param.type), ...(entry.returnType ? [entry.returnType] : [])])
1222
+ ),
1223
+ ]
1224
+ const referencedImports = [...typeImports].flatMap(([local, info]) =>
1225
+ !locallyDeclaredNames.has(local) &&
1226
+ (declarationTexts.some((declText) => typeTextReferencesName(declText, local)) ||
1227
+ hintTypeTexts.some((hintText) => typeTextReferencesName(hintText, local)))
1228
+ ? [info]
1229
+ : []
1230
+ )
1231
+ return {
1232
+ imports: renderTypeImports(referencedImports),
1233
+ declarations: declarationTexts,
1234
+ returnHints,
1235
+ paramHints,
1236
+ classHints,
1237
+ propertyHints,
1238
+ localHints,
1239
+ inlineCallableHints,
1240
+ }
1241
+ }
1242
+
1243
+ function writeAppleNativeViteConfig({ configPath, baseConfigPath, appDir, moduleGraphOutDir }) {
1244
+ const applePackageRoot = resolveApplePackageRoot(appDir)
1245
+ const appleRuntimeSubpath = path.join(applePackageRoot, 'runtime/$1.js')
1246
+ const applePackagePath = path.join(applePackageRoot, 'dist/index.js')
1247
+ const moduleGraphPluginPath = path.join(coreRoot, 'scripts/gea-vite-module-graph-plugin.mjs')
1248
+ const { threeSrcDir, threeUtilsModule, threeWebGLAnimationModule, threeWebXRManagerModule, troikaThreeTextModule } = resolveAppleNativeAliasModules(appDir)
1249
+ const q = (value) => JSON.stringify(value)
1250
+ fs.writeFileSync(
1251
+ configPath,
1252
+ `import baseConfig from ${q(baseConfigPath)}
1253
+ import { geaAppleNativeModuleAliases, geaModuleGraphPlugins } from ${q(moduleGraphPluginPath)}
1254
+
1255
+ const appleAliases = [
1256
+ { find: /^@geajs\\/apple\\/(.+)$/, replacement: ${q(appleRuntimeSubpath)} },
1257
+ { find: /^@geastack\\/apple\\/(.+)$/, replacement: ${q(appleRuntimeSubpath)} },
1258
+ { find: '@geajs/apple', replacement: ${q(applePackagePath)} },
1259
+ { find: '@geastack/apple', replacement: ${q(applePackagePath)} },
1260
+ ]
1261
+ const geaModuleGraph = geaModuleGraphPlugins({ outDir: ${q(moduleGraphOutDir || '')}, entryReachableOnly: true })
1262
+ const geaAppleNativeAliases = geaAppleNativeModuleAliases({
1263
+ threeSrcDir: ${q(threeSrcDir)},
1264
+ threeUtilsModule: ${q(threeUtilsModule)},
1265
+ threeWebGLAnimationModule: ${q(threeWebGLAnimationModule)},
1266
+ threeWebXRManagerModule: ${q(threeWebXRManagerModule)},
1267
+ troikaThreeTextModule: ${q(troikaThreeTextModule)},
1268
+ })
1269
+
1270
+ function normalizeAlias(alias) {
1271
+ if (!alias) return []
1272
+ if (Array.isArray(alias)) return alias
1273
+ return Object.entries(alias).map(([find, replacement]) => ({ find, replacement }))
1274
+ }
1275
+
1276
+ export default async function geaAppleNativeViteConfig(env) {
1277
+ const resolved = typeof baseConfig === 'function' ? await baseConfig(env) : baseConfig
1278
+ const baseResolve = resolved.resolve ?? {}
1279
+ return {
1280
+ ...resolved,
1281
+ resolve: {
1282
+ ...baseResolve,
1283
+ alias: [...geaAppleNativeAliases, ...appleAliases, ...normalizeAlias(baseResolve.alias)],
1284
+ },
1285
+ plugins: [...(resolved.plugins ?? []), ...geaModuleGraph],
1286
+ }
1287
+ }
1288
+ `
1289
+ )
1290
+ }
1291
+
1292
+ // Frameworks that only exist on the *other* Apple platform. The fixture ships
1293
+ // every framework (UIKit + AppKit), but UIKit and AppKit declare members with the
1294
+ // same names (font, textColor, hidden, addSubview, ...). When a native member is
1295
+ // accessed on a dynamic gea_cpp_value receiver the emitter can't tell which class
1296
+ // it is, so it picks the first registered binding — UIKit's `::UILabel`/`::UIFont`
1297
+ // etc., which don't exist in a macOS (AppKit) build and fail to compile.
1298
+ const applePlatformOffFrameworks = {
1299
+ macos: new Set(['UIKit']),
1300
+ ios: new Set(['AppKit']),
1301
+ }
1302
+
1303
+ async function writeAppleNativeMetadata(outDir, appDir, platform = '') {
1304
+ const applePackage = path.join(resolveApplePackageRoot(appDir), 'dist/index.js')
1305
+ if (!fs.existsSync(applePackage)) {
1306
+ fail(`missing @geastack/apple build output: ${applePackage}. Run the Apple bindings build first.`)
1307
+ }
1308
+ const { appleSdkFixture, generateAppleBridgeMetadata } = await import(pathToFileURL(applePackage).href)
1309
+ const offPlatform = applePlatformOffFrameworks[platform] ?? new Set()
1310
+ // Keep the off-platform class SHELLS — other frameworks extend them (e.g.
1311
+ // MKMapView/MTKView extend UIKit.UIView) so the wrapper structs must still be
1312
+ // declared — but strip their members, constructors, free functions and
1313
+ // constants. Shared member names then register a binding only for the
1314
+ // on-platform class, so native access on a dynamic receiver resolves to the
1315
+ // platform's class instead of leaking an off-platform ObjC type.
1316
+ const fixture =
1317
+ offPlatform.size > 0
1318
+ ? {
1319
+ ...appleSdkFixture,
1320
+ frameworks: appleSdkFixture.frameworks.map((framework) =>
1321
+ offPlatform.has(framework.name)
1322
+ ? {
1323
+ ...framework,
1324
+ classes: (framework.classes ?? []).map((cls) => ({ ...cls, constructors: [], methods: [], properties: [] })),
1325
+ functions: [],
1326
+ constants: [],
1327
+ }
1328
+ : framework
1329
+ ),
1330
+ }
1331
+ : appleSdkFixture
1332
+ const metadataPath = path.join(outDir, 'gea-apple-metadata.json')
1333
+ fs.writeFileSync(metadataPath, `${JSON.stringify(generateAppleBridgeMetadata(fixture), null, 2)}\n`)
1334
+ return metadataPath
1335
+ }
1336
+
1337
+ function collectMemberPath(node) {
1338
+ if (t.isIdentifier(node)) return [node.name]
1339
+ if (!t.isMemberExpression(node) || node.computed) return null
1340
+ const objectPath = collectMemberPath(node.object)
1341
+ if (!objectPath || !t.isIdentifier(node.property)) return null
1342
+ objectPath.push(node.property.name)
1343
+ return objectPath
1344
+ }
1345
+
1346
+ function evalStaticLiteral(node) {
1347
+ if (t.isBooleanLiteral(node) || t.isNumericLiteral(node) || t.isStringLiteral(node)) return node.value
1348
+ if (t.isNullLiteral(node)) return null
1349
+ if (t.isUnaryExpression(node, { operator: '-' })) {
1350
+ const value = evalStaticLiteral(node.argument)
1351
+ if (typeof value === 'number') return -value
1352
+ }
1353
+ if (t.isUnaryExpression(node, { operator: '+' })) {
1354
+ const value = evalStaticLiteral(node.argument)
1355
+ if (typeof value === 'number') return value
1356
+ }
1357
+ return undefined
1358
+ }
1359
+
1360
+ function evalStaticObject(node) {
1361
+ if (!t.isObjectExpression(node)) return null
1362
+ const result = {}
1363
+ for (const prop of node.properties) {
1364
+ if (!t.isObjectProperty(prop) || prop.computed) return null
1365
+ const key = t.isIdentifier(prop.key) ? prop.key.name : t.isStringLiteral(prop.key) ? prop.key.value : null
1366
+ if (!key) return null
1367
+ const value = evalStaticLiteral(prop.value)
1368
+ if (value === undefined) return null
1369
+ result[key] = value
1370
+ }
1371
+ return result
1372
+ }
1373
+
1374
+ function collectDisplayConfig(appDir, entry) {
1375
+ const pending = [path.resolve(appDir, entry)]
1376
+ const processed = new Set()
1377
+ const displayConfig = {}
1378
+
1379
+ while (pending.length > 0) {
1380
+ const srcId = pending.shift()
1381
+ if (!srcId || processed.has(srcId) || !fs.existsSync(srcId)) continue
1382
+ processed.add(srcId)
1383
+
1384
+ // Only parse JS/TS modules. The import walk follows every local import,
1385
+ // including asset imports (e.g. troika font `import url from './x.woff'`);
1386
+ // feeding a binary asset to the TS parser blows up on its first non-ASCII
1387
+ // byte (a .woff's `wOFF` header → "Unexpected character" at column 4).
1388
+ // Assets carry no Display config, so skip them.
1389
+ if (!/\.(?:mjs|cjs|jsx?|tsx?)$/i.test(srcId)) continue
1390
+
1391
+ const code = fs.readFileSync(srcId, 'utf8')
1392
+ const ast = parseTsx(code)
1393
+ const displayVars = new Set()
1394
+
1395
+ traverse(ast, {
1396
+ ImportDeclaration(astPath) {
1397
+ const source = astPath.node.source.value
1398
+ if (source.includes('@geastack/core')) {
1399
+ for (const spec of astPath.node.specifiers) {
1400
+ if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported, { name: 'Display' }) && t.isIdentifier(spec.local)) {
1401
+ displayVars.add(spec.local.name)
1402
+ }
1403
+ }
1404
+ }
1405
+ discoverLocalImport(source, srcId, pending)
1406
+ },
1407
+ })
1408
+
1409
+ traverse(ast, {
1410
+ ExpressionStatement(astPath) {
1411
+ if (astPath.parent.type !== 'Program') return
1412
+ const expr = astPath.node.expression
1413
+ if (!t.isCallExpression(expr)) return
1414
+ const memberPath = collectMemberPath(expr.callee)
1415
+ if (!memberPath || memberPath.length !== 2 || !displayVars.has(memberPath[0])) return
1416
+ const config = evalStaticObject(expr.arguments[0])
1417
+ if (!config) return
1418
+ if (memberPath[1] === 'setFlushConfig') {
1419
+ const chunkRows = positiveInteger(config.rows)
1420
+ const queueDepth = positiveInteger(config.depth)
1421
+ if (chunkRows > 0) displayConfig.flushChunkRows = chunkRows
1422
+ if (queueDepth > 0) displayConfig.flushQueueDepth = queueDepth
1423
+ } else if (memberPath[1] === 'setMemoryConfig') {
1424
+ const commandBufferCommands = positiveInteger(config.commandBufferCommands)
1425
+ if (commandBufferCommands > 0) displayConfig.commandBufferCommands = commandBufferCommands
1426
+ if (typeof config.backgroundCache === 'boolean') displayConfig.backgroundCache = config.backgroundCache
1427
+ }
1428
+ },
1429
+ })
1430
+ }
1431
+
1432
+ return displayConfig
1433
+ }
1434
+
1435
+ function positiveInteger(value) {
1436
+ if (typeof value !== 'number') return 0
1437
+ const rounded = Math.floor(value)
1438
+ return rounded > 0 ? rounded : 0
1439
+ }
1440
+
1441
+ function writeDisplayConfigHeader(outDir, displayDefaults) {
1442
+ const lines = ['#pragma once']
1443
+ const chunkRows = positiveInteger(displayDefaults.flushChunkRows)
1444
+ const queueDepth = positiveInteger(displayDefaults.flushQueueDepth)
1445
+ const commandBufferCommands = positiveInteger(displayDefaults.commandBufferCommands)
1446
+ if (chunkRows > 0) lines.push(`#define GEA_EMBEDDED_DISPLAY_FLUSH_CHUNK_MAX ${chunkRows}`)
1447
+ if (queueDepth > 0) lines.push(`#define GEA_EMBEDDED_DISPLAY_FLUSH_QUEUE_DEPTH ${queueDepth}`)
1448
+ if (commandBufferCommands > 0) lines.push(`#define GEA_EMBEDDED_DISPLAY_COMMAND_BUFFER_COMMANDS ${commandBufferCommands}`)
1449
+ if (typeof displayDefaults.backgroundCache === 'boolean') {
1450
+ lines.push(`#define GEA_EMBEDDED_DISPLAY_BACKGROUND_CACHE_IN_SRAM ${displayDefaults.backgroundCache ? 1 : 0}`)
1451
+ }
1452
+ writeFileIfChanged(path.join(outDir, 'gea_embedded_app_config.h'), `${lines.join('\n')}\n`)
1453
+ }
1454
+
1455
+ // Parse plain CSS rules (already free of @media) into StyleSheet registration
1456
+ // calls. When `mediaCondition` is non-empty, every emitted rule is gated on it
1457
+ // and re-evaluated against the viewport at runtime.
1458
+ function cssRegisterCall(kind) {
1459
+ if (useStaticCssRules) {
1460
+ if (kind === 'class') return 'registerStaticRule'
1461
+ if (kind === 'element') return 'registerStaticElementRule'
1462
+ if (kind === 'selector') return 'registerStaticSelectorRule'
1463
+ if (kind === 'keyframe') return 'registerStaticKeyframeRule'
1464
+ }
1465
+ if (kind === 'class') return 'registerRule'
1466
+ if (kind === 'element') return 'registerElementRule'
1467
+ if (kind === 'selector') return 'registerSelectorRule'
1468
+ return 'registerKeyframeRule'
1469
+ }
1470
+
1471
+ function staticSelectorKind(kind) {
1472
+ if (kind === 'class') return 'gea::embedded::ui::StaticStyleSelectorKind::Class'
1473
+ if (kind === 'element') return 'gea::embedded::ui::StaticStyleSelectorKind::Element'
1474
+ return 'gea::embedded::ui::StaticStyleSelectorKind::Selector'
1475
+ }
1476
+
1477
+ let staticCssSelectorPlanCache = new Set()
1478
+ let staticCssMediaConditionPlanCache = new Set()
1479
+
1480
+ function stripStaticSelectorPseudoElement(selector) {
1481
+ return selector.replace(/::(?:before|after)\s*$/i, '').trim()
1482
+ }
1483
+
1484
+ function parseStaticSimpleSelectorSpec(rawSimple) {
1485
+ const simple = rawSimple.trim()
1486
+ if (!simple || simple.includes('::')) return null
1487
+ const spec = {
1488
+ tag: null,
1489
+ id: null,
1490
+ classes: [],
1491
+ wantsRoot: false,
1492
+ wantsFirstChild: false,
1493
+ wantsLastChild: false,
1494
+ }
1495
+ let i = 0
1496
+ if (simple[i] !== '.' && simple[i] !== '#' && simple[i] !== ':') {
1497
+ const start = i
1498
+ while (i < simple.length && simple[i] !== '.' && simple[i] !== '#' && simple[i] !== ':') i++
1499
+ const tag = simple.slice(start, i).trim().toLowerCase()
1500
+ if (!tag) return null
1501
+ spec.tag = tag
1502
+ }
1503
+ while (i < simple.length) {
1504
+ const marker = simple[i++]
1505
+ const start = i
1506
+ while (i < simple.length && simple[i] !== '.' && simple[i] !== '#' && simple[i] !== ':') i++
1507
+ const token = simple.slice(start, i).trim()
1508
+ if (!token) return null
1509
+ if (marker === '.') {
1510
+ spec.classes.push(token)
1511
+ } else if (marker === '#') {
1512
+ spec.id = token
1513
+ } else if (marker === ':') {
1514
+ if (token === 'root') spec.wantsRoot = true
1515
+ else if (token === 'first-child') spec.wantsFirstChild = true
1516
+ else if (token === 'last-child') spec.wantsLastChild = true
1517
+ else return null
1518
+ } else {
1519
+ return null
1520
+ }
1521
+ }
1522
+ if (!spec.tag && !spec.id && spec.classes.length === 0 &&
1523
+ !spec.wantsRoot && !spec.wantsFirstChild && !spec.wantsLastChild) {
1524
+ return null
1525
+ }
1526
+ return spec
1527
+ }
1528
+
1529
+ function parseStaticSelectorPlanSpec(selector) {
1530
+ const text = stripStaticSelectorPseudoElement(selector)
1531
+ if (!text) return null
1532
+ const parts = []
1533
+ let nextDirect = false
1534
+ let i = 0
1535
+ while (i < text.length) {
1536
+ while (i < text.length && text.charCodeAt(i) <= 32) i++
1537
+ if (i >= text.length) break
1538
+ if (text[i] === '>') {
1539
+ nextDirect = true
1540
+ i++
1541
+ continue
1542
+ }
1543
+ const start = i
1544
+ while (i < text.length && text.charCodeAt(i) > 32 && text[i] !== '>') i++
1545
+ if (i <= start) continue
1546
+ const simple = parseStaticSimpleSelectorSpec(text.slice(start, i))
1547
+ if (!simple) return null
1548
+ parts.push({ simple, directParent: nextDirect })
1549
+ nextDirect = false
1550
+ }
1551
+ return parts.length > 0 ? { text, parts } : null
1552
+ }
1553
+
1554
+ function staticSimpleSelectorSpecCode(simple) {
1555
+ const tag = simple.tag ? JSON.stringify(simple.tag) : 'nullptr'
1556
+ const id = simple.id ? JSON.stringify(simple.id) : 'nullptr'
1557
+ const classes = simple.classes.map((className) => JSON.stringify(className)).join(', ')
1558
+ return `{${tag}, ${id}, {${classes}}, ${simple.wantsRoot ? 'true' : 'false'}, ${simple.wantsFirstChild ? 'true' : 'false'}, ${simple.wantsLastChild ? 'true' : 'false'}}`
1559
+ }
1560
+
1561
+ function staticSelectorPartSpecCode(part) {
1562
+ return `{${staticSimpleSelectorSpecCode(part.simple)}, ${part.directParent ? 'true' : 'false'}}`
1563
+ }
1564
+
1565
+ function staticSelectorPlanRegistration(selector) {
1566
+ if (!useStaticCssRules) return []
1567
+ const plan = parseStaticSelectorPlanSpec(selector)
1568
+ if (!plan) return []
1569
+ if (staticCssSelectorPlanCache.has(plan.text)) return []
1570
+ staticCssSelectorPlanCache.add(plan.text)
1571
+ const parts = plan.parts.map(staticSelectorPartSpecCode).join(', ')
1572
+ return [
1573
+ `__gea_stylesheet.registerStaticSelectorPlan(${JSON.stringify(selector)}, {${parts}});`,
1574
+ ]
1575
+ }
1576
+
1577
+ function parseStaticMediaNumber(raw) {
1578
+ const value = raw.trim()
1579
+ if (!value) return null
1580
+ const slash = value.indexOf('/')
1581
+ if (slash >= 0) {
1582
+ const width = Number.parseFloat(value.slice(0, slash))
1583
+ const height = Number.parseFloat(value.slice(slash + 1))
1584
+ if (!Number.isFinite(width) || !Number.isFinite(height) || height === 0) return null
1585
+ return width / height
1586
+ }
1587
+ const match = value.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(.*)$/)
1588
+ if (!match) return null
1589
+ const number = Number(match[1])
1590
+ if (!Number.isFinite(number)) return null
1591
+ const unit = match[2].trim().toLowerCase()
1592
+ if (unit === 'dpi') return number / 96
1593
+ return number
1594
+ }
1595
+
1596
+ function staticMediaFeatureForName(name) {
1597
+ const feature = name.trim().toLowerCase()
1598
+ const exact = (kind) => ({ kind, compare: 'Equal' })
1599
+ const min = (kind) => ({ kind, compare: 'Min' })
1600
+ const max = (kind) => ({ kind, compare: 'Max' })
1601
+ switch (feature) {
1602
+ case 'orientation': return exact('Orientation')
1603
+ case 'monochrome': return exact('Monochrome')
1604
+ case 'min-monochrome': return min('Monochrome')
1605
+ case 'max-monochrome': return max('Monochrome')
1606
+ case 'width': return exact('Width')
1607
+ case 'min-width': return min('Width')
1608
+ case 'max-width': return max('Width')
1609
+ case 'height': return exact('Height')
1610
+ case 'min-height': return min('Height')
1611
+ case 'max-height': return max('Height')
1612
+ case 'aspect-ratio': return exact('AspectRatio')
1613
+ case 'min-aspect-ratio': return min('AspectRatio')
1614
+ case 'max-aspect-ratio': return max('AspectRatio')
1615
+ case 'resolution':
1616
+ case 'device-pixel-ratio':
1617
+ return exact('Resolution')
1618
+ case 'min-resolution':
1619
+ case 'min-device-pixel-ratio':
1620
+ return min('Resolution')
1621
+ case 'max-resolution':
1622
+ case 'max-device-pixel-ratio':
1623
+ return max('Resolution')
1624
+ default:
1625
+ return null
1626
+ }
1627
+ }
1628
+
1629
+ function alwaysFalseStaticMediaTerm() {
1630
+ return { kind: 'AlwaysFalse', compare: 'Equal', value: 0, orientation: 0 }
1631
+ }
1632
+
1633
+ function compileStaticMediaFeatureTerm(name, rawValue) {
1634
+ const feature = staticMediaFeatureForName(name)
1635
+ if (!feature) return alwaysFalseStaticMediaTerm()
1636
+ if (feature.kind === 'Orientation') {
1637
+ const orientation = rawValue.trim().toLowerCase()
1638
+ if (orientation === 'portrait') return { ...feature, value: 0, orientation: 1 }
1639
+ if (orientation === 'landscape') return { ...feature, value: 0, orientation: 2 }
1640
+ return alwaysFalseStaticMediaTerm()
1641
+ }
1642
+ const value = parseStaticMediaNumber(rawValue)
1643
+ if (value === null) return alwaysFalseStaticMediaTerm()
1644
+ return { ...feature, value, orientation: 0 }
1645
+ }
1646
+
1647
+ function parseStaticMediaQuerySpec(rawQuery) {
1648
+ const query = rawQuery.trim()
1649
+ if (!query) return { valid: false, terms: [] }
1650
+ const terms = []
1651
+ let i = 0
1652
+ while (true) {
1653
+ const open = query.indexOf('(', i)
1654
+ if (open < 0) break
1655
+ const close = query.indexOf(')', open)
1656
+ if (close < 0) return { valid: false, terms: [] }
1657
+ const feature = query.slice(open + 1, close)
1658
+ const colon = feature.indexOf(':')
1659
+ if (colon >= 0) {
1660
+ terms.push(compileStaticMediaFeatureTerm(feature.slice(0, colon), feature.slice(colon + 1)))
1661
+ } else if (feature.trim().toLowerCase() === 'monochrome') {
1662
+ terms.push({ kind: 'Monochrome', compare: 'Boolean', value: 0, orientation: 0 })
1663
+ }
1664
+ i = close + 1
1665
+ }
1666
+ return { valid: true, terms }
1667
+ }
1668
+
1669
+ function staticMediaTermSpecCode(term) {
1670
+ return `{gea::embedded::ui::StaticStyleMediaFeatureKind::${term.kind}, gea::embedded::ui::StaticStyleMediaCompare::${term.compare}, ${term.value}, ${term.orientation}}`
1671
+ }
1672
+
1673
+ function staticMediaQuerySpecCode(query) {
1674
+ return `{{${query.terms.map(staticMediaTermSpecCode).join(', ')}}, ${query.valid ? 'true' : 'false'}}`
1675
+ }
1676
+
1677
+ function staticMediaConditionPlanRegistration(condition) {
1678
+ if (!useStaticCssRules || !condition) return []
1679
+ if (staticCssMediaConditionPlanCache.has(condition)) return []
1680
+ const queries = splitTopLevelCss(condition, ',').map(parseStaticMediaQuerySpec)
1681
+ if (queries.length === 0) return []
1682
+ staticCssMediaConditionPlanCache.add(condition)
1683
+ return [
1684
+ `__gea_stylesheet.registerStaticMediaConditionPlan(${JSON.stringify(condition)}, {${queries.map(staticMediaQuerySpecCode).join(', ')}});`,
1685
+ ]
1686
+ }
1687
+
1688
+ function roundCssNumber(value) {
1689
+ return value < 0 ? Math.ceil(value - 0.5) : Math.floor(value + 0.5)
1690
+ }
1691
+
1692
+ function clampInt16(value) {
1693
+ if (value < -32768) return -32768
1694
+ if (value > 32767) return 32767
1695
+ return value
1696
+ }
1697
+
1698
+ function simpleStaticLength(value) {
1699
+ const match = value.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(px|%)?$/i)
1700
+ if (!match) return null
1701
+ const number = Number(match[1])
1702
+ if (!Number.isFinite(number)) return null
1703
+ const unit = (match[2] || '').toLowerCase()
1704
+ return { number, unit }
1705
+ }
1706
+
1707
+ function parseStaticCssLength(rawValue) {
1708
+ const value = rawValue.trim()
1709
+ const match = value.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(px|%|vw|vh|vmin|vmax|dvw|dvh)?$/i)
1710
+ if (!match) return null
1711
+ let number = Number(match[1])
1712
+ if (!Number.isFinite(number)) return null
1713
+ if (Object.is(number, -0)) number = 0
1714
+ const unit = (match[2] || '').toLowerCase()
1715
+ const units = new Map([
1716
+ ['', 'Raw'],
1717
+ ['px', 'Px'],
1718
+ ['%', 'Percent'],
1719
+ ['vw', 'Vw'],
1720
+ ['vh', 'Vh'],
1721
+ ['vmin', 'Vmin'],
1722
+ ['vmax', 'Vmax'],
1723
+ ['dvw', 'Dvw'],
1724
+ ['dvh', 'Dvh'],
1725
+ ])
1726
+ return units.has(unit) ? { number, unit: units.get(unit) } : null
1727
+ }
1728
+
1729
+ let staticCssLengthExpressionCounter = 0
1730
+ let staticCssLengthExpressionCache = new Map()
1731
+
1732
+ function cssNumberCode(value) {
1733
+ if (!Number.isFinite(value)) return '0'
1734
+ return String(Object.is(value, -0) ? 0 : value)
1735
+ }
1736
+
1737
+ function staticLengthSpecCode(spec) {
1738
+ return `{gea::embedded::ui::StaticStyleLengthUnit::${spec.unit}, ${spec.value}}`
1739
+ }
1740
+
1741
+ function zeroStaticLengthSpec() {
1742
+ return { unit: 'Raw', value: '0' }
1743
+ }
1744
+
1745
+ function staticCssFunctionInner(rawValue, name) {
1746
+ const value = rawValue.trim()
1747
+ const lower = value.toLowerCase()
1748
+ const prefix = `${name}(`
1749
+ if (!lower.startsWith(prefix) || !value.endsWith(')')) return null
1750
+ return value.slice(prefix.length, -1)
1751
+ }
1752
+
1753
+ function findTopLevelCssOperator(value, operators) {
1754
+ let depth = 0
1755
+ for (let i = 0; i < value.length; i++) {
1756
+ const char = value[i]
1757
+ if (char === '(') {
1758
+ depth++
1759
+ continue
1760
+ }
1761
+ if (char === ')') {
1762
+ if (depth > 0) depth--
1763
+ continue
1764
+ }
1765
+ if (depth !== 0 || !operators.includes(char) || i === 0) continue
1766
+ if ((char === '+' || char === '-') && /[eE]/.test(value[i - 1] ?? '')) continue
1767
+ return i
1768
+ }
1769
+ return -1
1770
+ }
1771
+
1772
+ function emitStaticLengthExpression(kind, specs, options = {}) {
1773
+ const key = options.key ?? `${kind}:${specs.map((spec) => `${spec.unit}:${spec.value}`).join('|')}:${options.scalar ?? 0}:${options.name ?? ''}:${options.hasFallback ? 1 : 0}`
1774
+ const cached = staticCssLengthExpressionCache.get(key)
1775
+ if (cached) return { setup: [], spec: { unit: 'Expression', value: `static_cast<float>(${cached})` } }
1776
+ const name = `gea_css_len_expr_${staticCssLengthExpressionCounter++}`
1777
+ staticCssLengthExpressionCache.set(key, name)
1778
+ const empty = zeroStaticLengthSpec()
1779
+ const a = specs[0] ?? empty
1780
+ const b = specs[1] ?? empty
1781
+ const c = specs[2] ?? empty
1782
+ const expressionName = options.name ? JSON.stringify(options.name) : 'nullptr'
1783
+ const scalar = cssNumberCode(options.scalar ?? 0)
1784
+ const hasFallback = options.hasFallback ? 'true' : 'false'
1785
+ const call = `const std::uint16_t ${name} = __gea_stylesheet.registerStaticLengthExpression(gea::embedded::ui::StaticStyleLengthExpressionKind::${kind}, ${staticLengthSpecCode(a)}, ${staticLengthSpecCode(b)}, ${staticLengthSpecCode(c)}, ${scalar}, ${expressionName}, ${hasFallback});`
1786
+ return { setup: [call], spec: { unit: 'Expression', value: `static_cast<float>(${name})` } }
1787
+ }
1788
+
1789
+ function parseStaticCssLengthSpec(rawValue) {
1790
+ const value = rawValue.trim()
1791
+ const simple = parseStaticCssLength(value)
1792
+ if (simple) return { setup: [], spec: { unit: simple.unit, value: cssNumberCode(simple.number) } }
1793
+
1794
+ const varInner = staticCssFunctionInner(value, 'var')
1795
+ if (varInner !== null) {
1796
+ const parts = splitTopLevelCss(varInner, ',')
1797
+ if (parts.length < 1 || parts.length > 2) return null
1798
+ const name = parts[0].trim()
1799
+ if (!/^--[A-Za-z0-9_-]+$/.test(name)) return null
1800
+ const fallback = parts.length === 2 ? parseStaticCssLengthSpec(parts[1]) : null
1801
+ if (parts.length === 2 && !fallback) return null
1802
+ const emitted = emitStaticLengthExpression('Var',
1803
+ [fallback?.spec ?? zeroStaticLengthSpec()],
1804
+ { name, hasFallback: Boolean(fallback), key: `var:${name}:${fallback ? `${fallback.spec.unit}:${fallback.spec.value}` : ''}` })
1805
+ return { setup: [...(fallback?.setup ?? []), ...emitted.setup], spec: emitted.spec }
1806
+ }
1807
+
1808
+ const clampInner = staticCssFunctionInner(value, 'clamp')
1809
+ if (clampInner !== null) {
1810
+ const parts = splitTopLevelCss(clampInner, ',')
1811
+ if (parts.length !== 3) return null
1812
+ const parsed = parts.map(parseStaticCssLengthSpec)
1813
+ if (parsed.some((entry) => !entry)) return null
1814
+ const emitted = emitStaticLengthExpression('Clamp',
1815
+ parsed.map((entry) => entry.spec),
1816
+ { key: `clamp:${parts.map((part) => part.trim()).join('|')}` })
1817
+ return { setup: [...parsed.flatMap((entry) => entry.setup), ...emitted.setup], spec: emitted.spec }
1818
+ }
1819
+
1820
+ for (const [functionName, kind] of [['min', 'Min'], ['max', 'Max']]) {
1821
+ const inner = staticCssFunctionInner(value, functionName)
1822
+ if (inner === null) continue
1823
+ const parts = splitTopLevelCss(inner, ',')
1824
+ if (parts.length < 1) return null
1825
+ const parsed = parts.map(parseStaticCssLengthSpec)
1826
+ if (parsed.some((entry) => !entry)) return null
1827
+ let setup = parsed[0].setup.slice()
1828
+ let current = parsed[0].spec
1829
+ for (let i = 1; i < parsed.length; i++) {
1830
+ setup.push(...parsed[i].setup)
1831
+ const emitted = emitStaticLengthExpression(kind,
1832
+ [current, parsed[i].spec],
1833
+ { key: `${functionName}:${parts.slice(0, i + 1).map((part) => part.trim()).join('|')}` })
1834
+ setup.push(...emitted.setup)
1835
+ current = emitted.spec
1836
+ }
1837
+ return { setup, spec: current }
1838
+ }
1839
+
1840
+ const calcInner = staticCssFunctionInner(value, 'calc')
1841
+ if (calcInner !== null) {
1842
+ const text = calcInner.trim()
1843
+ for (const [operators, kinds] of [
1844
+ ['/*', { '/': 'Divide', '*': 'Multiply' }],
1845
+ ['+-', { '+': 'Add', '-': 'Subtract' }],
1846
+ ]) {
1847
+ const index = findTopLevelCssOperator(text, operators)
1848
+ if (index <= 0) continue
1849
+ const op = text[index]
1850
+ const left = parseStaticCssLengthSpec(text.slice(0, index))
1851
+ if (!left) return null
1852
+ if (op === '/' || op === '*') {
1853
+ const scalar = Number(text.slice(index + 1).trim())
1854
+ if (!Number.isFinite(scalar)) return null
1855
+ const emitted = emitStaticLengthExpression(kinds[op],
1856
+ [left.spec],
1857
+ { scalar, key: `calc:${text}` })
1858
+ return { setup: [...left.setup, ...emitted.setup], spec: emitted.spec }
1859
+ }
1860
+ const right = parseStaticCssLengthSpec(text.slice(index + 1))
1861
+ if (!right) return null
1862
+ const emitted = emitStaticLengthExpression(kinds[op],
1863
+ [left.spec, right.spec],
1864
+ { key: `calc:${text}` })
1865
+ return { setup: [...left.setup, ...right.setup, ...emitted.setup], spec: emitted.spec }
1866
+ }
1867
+ return parseStaticCssLengthSpec(text)
1868
+ }
1869
+
1870
+ return null
1871
+ }
1872
+
1873
+ function cssAlignKeywordValue(value) {
1874
+ const align = new Map([
1875
+ ['flex-start', 0],
1876
+ ['start', 0],
1877
+ ['stretch', 0],
1878
+ ['normal', 0],
1879
+ ['center', 1],
1880
+ ['flex-end', 2],
1881
+ ['end', 2],
1882
+ ['space-between', 3],
1883
+ ['space-around', 4],
1884
+ ['baseline', 5],
1885
+ ])
1886
+ return align.has(value) ? align.get(value) : null
1887
+ }
1888
+
1889
+ function clampCssByte(value) {
1890
+ if (value < 0) return 0
1891
+ if (value > 255) return 255
1892
+ return value
1893
+ }
1894
+
1895
+ function parseCssAlpha(value) {
1896
+ let alpha = Number(value)
1897
+ if (!Number.isFinite(alpha)) return null
1898
+ if (alpha <= 1) alpha *= 255
1899
+ return clampCssByte(Math.floor(alpha + 0.5))
1900
+ }
1901
+
1902
+ function parseStaticCssColor(rawValue) {
1903
+ const value = rawValue.trim()
1904
+ if (value.toLowerCase() === 'transparent') return { r: 0, g: 0, b: 0, a: 0 }
1905
+ const hex = value.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/)
1906
+ if (hex) {
1907
+ const text = hex[1]
1908
+ if (text.length === 3 || text.length === 4) {
1909
+ const r = parseInt(text[0] + text[0], 16)
1910
+ const g = parseInt(text[1] + text[1], 16)
1911
+ const b = parseInt(text[2] + text[2], 16)
1912
+ const a = text.length === 4 ? parseInt(text[3] + text[3], 16) : 255
1913
+ return { r, g, b, a }
1914
+ }
1915
+ const r = parseInt(text.slice(0, 2), 16)
1916
+ const g = parseInt(text.slice(2, 4), 16)
1917
+ const b = parseInt(text.slice(4, 6), 16)
1918
+ const a = text.length === 8 ? parseInt(text.slice(6, 8), 16) : 255
1919
+ return { r, g, b, a }
1920
+ }
1921
+ const rgb = value.match(/^rgba?\((.*)\)$/i)
1922
+ if (!rgb) return null
1923
+ const parts = splitTopLevelCss(rgb[1], ',')
1924
+ if (parts.length < 3 || parts.length > 4) return null
1925
+ const channels = parts.slice(0, 3).map((part) => Number(part.trim()))
1926
+ if (channels.some((channel) => !Number.isFinite(channel))) return null
1927
+ const alpha = parts.length === 4 ? parseCssAlpha(parts[3].trim()) : 255
1928
+ if (alpha === null) return null
1929
+ return {
1930
+ r: clampCssByte(Math.trunc(channels[0])),
1931
+ g: clampCssByte(Math.trunc(channels[1])),
1932
+ b: clampCssByte(Math.trunc(channels[2])),
1933
+ a: alpha,
1934
+ }
1935
+ }
1936
+
1937
+ function cssStaticColorTarget(property) {
1938
+ switch (property) {
1939
+ case 'color':
1940
+ return 'Color'
1941
+ case 'background':
1942
+ case 'background-color':
1943
+ return 'Background'
1944
+ case 'active-background':
1945
+ case 'active-background-color':
1946
+ return 'ActiveBackground'
1947
+ case 'border-color':
1948
+ return 'Border'
1949
+ case 'border-top-color':
1950
+ return 'BorderTop'
1951
+ case 'border-right-color':
1952
+ return 'BorderRight'
1953
+ case 'border-bottom-color':
1954
+ return 'BorderBottom'
1955
+ case 'border-left-color':
1956
+ return 'BorderLeft'
1957
+ default:
1958
+ return null
1959
+ }
1960
+ }
1961
+
1962
+ function cssStaticColorValue(property, rawValue) {
1963
+ const target = cssStaticColorTarget(property)
1964
+ if (!target) return null
1965
+ const color = parseStaticCssColor(rawValue)
1966
+ return color ? { target, color } : null
1967
+ }
1968
+
1969
+ function cssStaticColorVarValue(property, rawValue) {
1970
+ const target = cssStaticColorTarget(property)
1971
+ if (!target) return null
1972
+ const inner = staticCssFunctionInner(rawValue.trim(), 'var')
1973
+ if (inner === null) return null
1974
+ const parts = splitTopLevelCss(inner, ',')
1975
+ if (parts.length < 1 || parts.length > 2) return null
1976
+ const name = parts[0].trim()
1977
+ if (!/^--[A-Za-z0-9_-]+$/.test(name)) return null
1978
+ const fallback = parts.length === 2 ? parseStaticCssColor(parts[1]) : null
1979
+ if (parts.length === 2 && !fallback) return null
1980
+ return { target, name, hasFallback: Boolean(fallback), color: fallback ?? { r: 0, g: 0, b: 0, a: 255 } }
1981
+ }
1982
+
1983
+ function parseStaticCssColorRef(rawValue) {
1984
+ const color = parseStaticCssColor(rawValue)
1985
+ if (color) return { name: null, hasFallback: true, color }
1986
+ const inner = staticCssFunctionInner(rawValue.trim(), 'var')
1987
+ if (inner === null) return null
1988
+ const parts = splitTopLevelCss(inner, ',')
1989
+ if (parts.length < 1 || parts.length > 2) return null
1990
+ const name = parts[0].trim()
1991
+ if (!/^--[A-Za-z0-9_-]+$/.test(name)) return null
1992
+ const fallback = parts.length === 2 ? parseStaticCssColor(parts[1]) : null
1993
+ if (parts.length === 2 && !fallback) return null
1994
+ return { name, hasFallback: Boolean(fallback), color: fallback ?? { r: 0, g: 0, b: 0, a: 255 } }
1995
+ }
1996
+
1997
+ function cssIgnoredProperty(property) {
1998
+ return new Set([
1999
+ 'color-scheme',
2000
+ 'background-position',
2001
+ 'box-sizing',
2002
+ 'font',
2003
+ 'grid-column',
2004
+ 'isolation',
2005
+ 'letter-spacing',
2006
+ 'outline',
2007
+ 'scroll-snap-align',
2008
+ 'scroll-snap-type',
2009
+ 'scrollbar-width',
2010
+ 'text-shadow',
2011
+ 'transform-style',
2012
+ 'transition',
2013
+ 'cursor',
2014
+ '-webkit-tap-highlight-color',
2015
+ ]).has(property)
2016
+ }
2017
+
2018
+ function cssStaticFontWeightValue(rawValue) {
2019
+ const value = rawValue.trim().toLowerCase()
2020
+ if (!value || value === 'normal') return 400
2021
+ if (value === 'bold' || value === 'bolder') return 700
2022
+ if (value === 'lighter') return 300
2023
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(value)) return null
2024
+ const number = roundCssNumber(Number(value))
2025
+ return Math.max(1, Math.min(1000, number))
2026
+ }
2027
+
2028
+ function cssStaticPropertyValue(property, rawValue) {
2029
+ const value = rawValue.trim()
2030
+ const lower = value.toLowerCase()
2031
+ const direct = (prop, v) => ({ property: `gea::embedded::ui::Property::${prop}`, value: v })
2032
+ const directLength = (prop, length) => {
2033
+ if (!length) return null
2034
+ if (length.unit === '') return direct(prop, roundCssNumber(length.number))
2035
+ if (length.unit === 'px' && length.number === 0) return direct(prop, 0)
2036
+ return null
2037
+ }
2038
+ const directSize = (lengthProp, percentProp, length) => {
2039
+ if (!length) return null
2040
+ if (length.unit === '%') return direct(percentProp, clampInt16(roundCssNumber(length.number * 10)))
2041
+ return directLength(lengthProp, length)
2042
+ }
2043
+ const numeric = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/
2044
+ const length = simpleStaticLength(value)
2045
+ switch (property) {
2046
+ case 'display': {
2047
+ const values = new Map([['block', 0], ['none', 1], ['grid', 2], ['inline-grid', 2], ['flex', 3], ['inline-flex', 3]])
2048
+ return values.has(value) ? direct('Display', values.get(value)) : null
2049
+ }
2050
+ case 'flex-direction':
2051
+ if (value === 'column') return direct('FlexDirection', 0)
2052
+ if (value === 'row') return direct('FlexDirection', 1)
2053
+ return null
2054
+ case 'flex-wrap':
2055
+ if (value === 'nowrap') return direct('FlexWrap', 0)
2056
+ if (value === 'wrap') return direct('FlexWrap', 1)
2057
+ return null
2058
+ case 'justify-content':
2059
+ return cssAlignKeywordValue(value) !== null ? direct('JustifyContent', cssAlignKeywordValue(value)) : null
2060
+ case 'align-items':
2061
+ return cssAlignKeywordValue(value) !== null ? direct('AlignItems', cssAlignKeywordValue(value)) : null
2062
+ case 'justify-items':
2063
+ return cssAlignKeywordValue(value) !== null ? direct('JustifyItems', cssAlignKeywordValue(value)) : null
2064
+ case 'align-content':
2065
+ return cssAlignKeywordValue(value) !== null ? direct('AlignContent', cssAlignKeywordValue(value)) : null
2066
+ case 'align-self':
2067
+ if (value === 'auto') return direct('AlignSelf', -1)
2068
+ return cssAlignKeywordValue(value) !== null ? direct('AlignSelf', cssAlignKeywordValue(value)) : null
2069
+ case 'position':
2070
+ if (value === 'static') return direct('Position', 0)
2071
+ if (value === 'absolute') return direct('Position', 1)
2072
+ if (value === 'relative') return direct('Position', 2)
2073
+ return null
2074
+ case 'gap':
2075
+ return directLength('Gap', length)
2076
+ case 'width':
2077
+ return directSize('Width', 'WidthPercent', length)
2078
+ case 'height':
2079
+ return directSize('Height', 'HeightPercent', length)
2080
+ case 'min-width':
2081
+ return directLength('MinWidth', length)
2082
+ case 'min-height':
2083
+ return directLength('MinHeight', length)
2084
+ case 'max-width':
2085
+ return directLength('MaxWidth', length)
2086
+ case 'max-height':
2087
+ return directLength('MaxHeight', length)
2088
+ case 'flex-basis':
2089
+ return directLength('FlexBasis', length)
2090
+ case 'flex-shrink':
2091
+ return numeric.test(value) ? direct('FlexShrink', roundCssNumber(Number(value))) : null
2092
+ case 'font-weight': {
2093
+ const weight = cssStaticFontWeightValue(value)
2094
+ return weight === null ? null : direct('FontWeight', weight)
2095
+ }
2096
+ case 'padding-top':
2097
+ return directLength('PaddingTop', length)
2098
+ case 'padding-right':
2099
+ return directLength('PaddingRight', length)
2100
+ case 'padding-bottom':
2101
+ return directLength('PaddingBottom', length)
2102
+ case 'padding-left':
2103
+ return directLength('PaddingLeft', length)
2104
+ case 'margin-top':
2105
+ return directLength('MarginTop', length)
2106
+ case 'margin-right':
2107
+ return directLength('MarginRight', length)
2108
+ case 'margin-bottom':
2109
+ return directLength('MarginBottom', length)
2110
+ case 'margin-left':
2111
+ return directLength('MarginLeft', length)
2112
+ case 'top':
2113
+ return directSize('Top', 'TopPercent', length)
2114
+ case 'right':
2115
+ return directSize('Right', 'RightPercent', length)
2116
+ case 'bottom':
2117
+ return directSize('Bottom', 'BottomPercent', length)
2118
+ case 'left':
2119
+ return directSize('Left', 'LeftPercent', length)
2120
+ case 'z-index':
2121
+ return numeric.test(value) ? direct('ZIndex', roundCssNumber(Number(value))) : null
2122
+ case 'opacity': {
2123
+ if (!numeric.test(value)) return null
2124
+ let opacity = Number(value)
2125
+ if (opacity <= 1) opacity *= 255
2126
+ if (opacity < 0) opacity = 0
2127
+ if (opacity > 255) opacity = 255
2128
+ return direct('Opacity', Math.floor(opacity + 0.5))
2129
+ }
2130
+ case 'object-fit':
2131
+ if (lower === 'fill') return direct('ImageFit', 0)
2132
+ if (lower === 'contain') return direct('ImageFit', 1)
2133
+ if (lower === 'cover') return direct('ImageFit', 2)
2134
+ if (lower === 'none') return direct('ImageFit', 3)
2135
+ if (lower === 'scale-down') return direct('ImageFit', 4)
2136
+ return null
2137
+ case 'rotate': {
2138
+ const angle = parseStaticAngleTenths(value)
2139
+ return angle === null ? null : direct('TransformRotate', angle)
2140
+ }
2141
+ case 'text-align':
2142
+ if (value === 'left' || value === 'start') return direct('TextAlign', 0)
2143
+ if (value === 'center') return direct('TextAlign', 1)
2144
+ if (value === 'right' || value === 'end') return direct('TextAlign', 2)
2145
+ return null
2146
+ case 'text-decoration':
2147
+ case 'text-decoration-line':
2148
+ if (value === 'none') return direct('TextDecoration', 0)
2149
+ if (value === 'underline') return direct('TextDecoration', 1)
2150
+ if (value === 'line-through' || value === 'strikethrough') return direct('TextDecoration', 2)
2151
+ return null
2152
+ case 'text-transform':
2153
+ if (lower === 'none') return direct('TextTransform', 0)
2154
+ if (lower === 'uppercase') return direct('TextTransform', 1)
2155
+ if (lower === 'lowercase') return direct('TextTransform', 2)
2156
+ if (lower === 'capitalize') return direct('TextTransform', 3)
2157
+ return null
2158
+ case 'white-space':
2159
+ if (lower === 'normal' || lower === 'pre-line') return direct('WhiteSpace', 0)
2160
+ if (lower === 'nowrap' || lower === 'pre') return direct('WhiteSpace', 1)
2161
+ return null
2162
+ case 'text-overflow':
2163
+ if (lower === 'clip') return direct('TextOverflow', 0)
2164
+ if (lower === 'ellipsis') return direct('TextOverflow', 1)
2165
+ return null
2166
+ case 'backface-visibility':
2167
+ if (lower === 'visible') return direct('Backface', 0)
2168
+ if (lower === 'hidden') return direct('Backface', 1)
2169
+ return null
2170
+ case 'pointer-events':
2171
+ if (lower === 'auto') return direct('PointerEvents', 0)
2172
+ if (lower === 'none') return direct('PointerEvents', 1)
2173
+ return null
2174
+ case 'overflow':
2175
+ if (value === 'visible') return direct('Overflow', 0)
2176
+ if (value === 'hidden') return direct('Overflow', 1)
2177
+ if (value === 'scroll' || value === 'auto') return direct('Overflow', 2)
2178
+ return null
2179
+ case 'overflow-x':
2180
+ if (value === 'visible') return direct('OverflowX', 0)
2181
+ if (value === 'hidden') return direct('OverflowX', 1)
2182
+ if (value === 'scroll' || value === 'auto') return direct('OverflowX', 2)
2183
+ return null
2184
+ case 'overflow-y':
2185
+ if (value === 'visible') return direct('OverflowY', 0)
2186
+ if (value === 'hidden') return direct('OverflowY', 1)
2187
+ if (value === 'scroll' || value === 'auto') return direct('OverflowY', 2)
2188
+ return null
2189
+ case 'flex-grow':
2190
+ return numeric.test(value) ? direct('Flex', roundCssNumber(Number(value))) : null
2191
+ default:
2192
+ return null
2193
+ }
2194
+ }
2195
+
2196
+ function cssStaticPropertyValues(property, rawValue) {
2197
+ const single = cssStaticPropertyValue(property, rawValue)
2198
+ if (single) return [single]
2199
+
2200
+ const value = rawValue.trim()
2201
+ const length = simpleStaticLength(value)
2202
+ const direct = (prop, v) => ({ property: `gea::embedded::ui::Property::${prop}`, value: v })
2203
+ const directLengthValue = (candidate) => {
2204
+ if (!candidate) return null
2205
+ if (candidate.unit === '') return roundCssNumber(candidate.number)
2206
+ if (candidate.unit === 'px' && candidate.number === 0) return 0
2207
+ return null
2208
+ }
2209
+ const boxLength = directLengthValue(length)
2210
+ if (boxLength !== null) {
2211
+ if (property === 'border') return [direct('BorderWidth', boxLength)]
2212
+ if (property === 'border-top') return [direct('BorderTopWidth', boxLength)]
2213
+ if (property === 'border-right') return [direct('BorderRightWidth', boxLength)]
2214
+ if (property === 'border-bottom') return [direct('BorderBottomWidth', boxLength)]
2215
+ if (property === 'border-left') return [direct('BorderLeftWidth', boxLength)]
2216
+ if (property === 'margin') {
2217
+ return [
2218
+ direct('MarginTop', boxLength),
2219
+ direct('MarginRight', boxLength),
2220
+ direct('MarginBottom', boxLength),
2221
+ direct('MarginLeft', boxLength),
2222
+ ]
2223
+ }
2224
+ if (property === 'padding') {
2225
+ return [
2226
+ direct('PaddingTop', boxLength),
2227
+ direct('PaddingRight', boxLength),
2228
+ direct('PaddingBottom', boxLength),
2229
+ direct('PaddingLeft', boxLength),
2230
+ ]
2231
+ }
2232
+ if (property === 'inset') {
2233
+ return [
2234
+ direct('Top', boxLength),
2235
+ direct('Right', boxLength),
2236
+ direct('Bottom', boxLength),
2237
+ direct('Left', boxLength),
2238
+ ]
2239
+ }
2240
+ }
2241
+
2242
+ if (property === 'place-items') {
2243
+ const align = cssAlignKeywordValue(value)
2244
+ if (align !== null) {
2245
+ return [
2246
+ direct('AlignItems', align),
2247
+ direct('JustifyItems', align),
2248
+ direct('JustifyContent', align),
2249
+ ]
2250
+ }
2251
+ }
2252
+
2253
+ if (property === 'scale') {
2254
+ const scale = parseStaticScalePermille(value)
2255
+ if (scale !== null) {
2256
+ return [
2257
+ direct('TransformScaleX', scale),
2258
+ direct('TransformScaleY', scale),
2259
+ ]
2260
+ }
2261
+ }
2262
+
2263
+ return []
2264
+ }
2265
+
2266
+ function cssStaticPrimaryFontFamily(rawValue) {
2267
+ let text = rawValue.trim()
2268
+ if (!text) return null
2269
+ const quote = text[0]
2270
+ if (quote === '\'' || quote === '"') {
2271
+ const end = text.indexOf(quote, 1)
2272
+ return end < 0 ? null : text.slice(1, end)
2273
+ }
2274
+ const comma = text.indexOf(',')
2275
+ if (comma >= 0) text = text.slice(0, comma)
2276
+ const family = text.trim()
2277
+ return family || null
2278
+ }
2279
+
2280
+ function cssStaticFontFamilyValue(property, rawValue) {
2281
+ if (property !== 'font-family') return null
2282
+ const family = cssStaticPrimaryFontFamily(rawValue)
2283
+ return family ? { family } : null
2284
+ }
2285
+
2286
+ function cssStaticLineHeightValue(property, rawValue) {
2287
+ if (property !== 'line-height') return null
2288
+ const value = rawValue.trim()
2289
+ const lower = value.toLowerCase()
2290
+ if (lower === 'normal') {
2291
+ return { setup: [], kind: 'Normal', spec: zeroStaticLengthSpec() }
2292
+ }
2293
+ const numberPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/
2294
+ if (numberPattern.test(value)) {
2295
+ return { setup: [], kind: 'Scalar', spec: { unit: 'Raw', value: cssNumberCode(Number(value)) } }
2296
+ }
2297
+ const percent = value.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))%$/)
2298
+ if (percent) {
2299
+ return { setup: [], kind: 'Percent', spec: { unit: 'Percent', value: cssNumberCode(Number(percent[1])) } }
2300
+ }
2301
+ const parsed = parseStaticCssLengthSpec(value)
2302
+ return parsed ? { ...parsed, kind: 'Length' } : null
2303
+ }
2304
+
2305
+ function cssStaticFlexValue(property, rawValue) {
2306
+ if (property !== 'flex') return null
2307
+ let grow = 0
2308
+ let numberIndex = 0
2309
+ let basis = zeroStaticLengthSpec()
2310
+ let hasBasis = false
2311
+ const setup = []
2312
+ const words = splitCssWords(rawValue.trim())
2313
+ if (words.length === 0) return null
2314
+ for (const token of words) {
2315
+ const lower = token.toLowerCase()
2316
+ if (lower === 'none') {
2317
+ grow = 0
2318
+ hasBasis = false
2319
+ continue
2320
+ }
2321
+ if (
2322
+ lower === 'auto' ||
2323
+ lower === 'content' ||
2324
+ lower === 'max-content' ||
2325
+ lower === 'min-content' ||
2326
+ lower === 'fit-content' ||
2327
+ lower.includes('%')
2328
+ ) {
2329
+ hasBasis = false
2330
+ continue
2331
+ }
2332
+ const number = Number(token)
2333
+ if (Number.isFinite(number) && token.match(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/)) {
2334
+ if (numberIndex === 0) grow = roundCssNumber(number)
2335
+ numberIndex++
2336
+ continue
2337
+ }
2338
+ const parsed = parseStaticCssLengthSpec(token)
2339
+ if (!parsed) return null
2340
+ setup.push(...parsed.setup)
2341
+ basis = parsed.spec
2342
+ hasBasis = true
2343
+ }
2344
+ return { setup, grow, basis, hasBasis }
2345
+ }
2346
+
2347
+ function cssStaticBoxShorthandValue(property, rawValue) {
2348
+ const targets = {
2349
+ padding: ['PaddingTop', 'PaddingRight', 'PaddingBottom', 'PaddingLeft'],
2350
+ margin: ['MarginTop', 'MarginRight', 'MarginBottom', 'MarginLeft'],
2351
+ inset: ['Top', 'Right', 'Bottom', 'Left'],
2352
+ }[property]
2353
+ if (!targets) return null
2354
+ const words = splitCssWords(rawValue.trim())
2355
+ if (words.length < 1 || words.length > 4) return null
2356
+ const parsed = words.map(parseStaticCssLengthSpec)
2357
+ if (parsed.some((entry) => !entry)) return null
2358
+ const specs =
2359
+ parsed.length === 1 ? [parsed[0], parsed[0], parsed[0], parsed[0]] :
2360
+ parsed.length === 2 ? [parsed[0], parsed[1], parsed[0], parsed[1]] :
2361
+ parsed.length === 3 ? [parsed[0], parsed[1], parsed[2], parsed[1]] :
2362
+ [parsed[0], parsed[1], parsed[2], parsed[3]]
2363
+ return {
2364
+ setup: parsed.flatMap((entry) => entry.setup),
2365
+ entries: targets.map((target, index) => ({ target, spec: specs[index].spec })),
2366
+ }
2367
+ }
2368
+
2369
+ const staticBorderRadiusCorners = new Map([
2370
+ ['border-top-left-radius', 'TopLeft'],
2371
+ ['border-top-right-radius', 'TopRight'],
2372
+ ['border-bottom-right-radius', 'BottomRight'],
2373
+ ['border-bottom-left-radius', 'BottomLeft'],
2374
+ ])
2375
+
2376
+ function cssStaticBorderRadiusValue(property, rawValue) {
2377
+ if (staticBorderRadiusCorners.has(property)) {
2378
+ const parsed = parseStaticCssLengthSpec(rawValue)
2379
+ return parsed
2380
+ ? { setup: parsed.setup, corner: staticBorderRadiusCorners.get(property), spec: parsed.spec }
2381
+ : null
2382
+ }
2383
+ if (property !== 'border-radius') return null
2384
+ const axes = splitTopLevelCss(rawValue, '/')
2385
+ const parts = splitCssWords(axes.length > 0 ? axes[0] : rawValue)
2386
+ if (parts.length < 1 || parts.length > 4) return null
2387
+ const parsed = parts.map(parseStaticCssLengthSpec)
2388
+ if (parsed.some((entry) => !entry)) return null
2389
+ const specs =
2390
+ parsed.length === 1 ? [parsed[0], parsed[0], parsed[0], parsed[0]] :
2391
+ parsed.length === 2 ? [parsed[0], parsed[1], parsed[0], parsed[1]] :
2392
+ parsed.length === 3 ? [parsed[0], parsed[1], parsed[2], parsed[1]] :
2393
+ [parsed[0], parsed[1], parsed[2], parsed[3]]
2394
+ return {
2395
+ setup: parsed.flatMap((entry) => entry.setup),
2396
+ specs: specs.map((entry) => entry.spec),
2397
+ }
2398
+ }
2399
+
2400
+ function cssStaticBorderValue(property, rawValue) {
2401
+ if (property !== 'border') return null
2402
+ const parts = splitCssWords(rawValue.trim())
2403
+ if (parts.length === 0) return null
2404
+ const width = parseStaticCssLengthSpec(parts[0])
2405
+ if (!width) return null
2406
+ let color = null
2407
+ for (const part of parts) {
2408
+ const lower = part.toLowerCase()
2409
+ if (!part.startsWith('#') && !lower.includes('rgb')) continue
2410
+ color = parseStaticCssColor(part)
2411
+ break
2412
+ }
2413
+ return color ? { setup: width.setup, width: width.spec, color } : null
2414
+ }
2415
+
2416
+ function cssStaticBorderShorthandValue(property, rawValue) {
2417
+ const targets = {
2418
+ border: { width: 'BorderWidth', color: 'Border', colorProperty: 'border-color', side: false },
2419
+ 'border-top': { width: 'BorderTopWidth', color: 'BorderTop', colorProperty: 'border-top-color', side: true },
2420
+ 'border-right': { width: 'BorderRightWidth', color: 'BorderRight', colorProperty: 'border-right-color', side: true },
2421
+ 'border-bottom': { width: 'BorderBottomWidth', color: 'BorderBottom', colorProperty: 'border-bottom-color', side: true },
2422
+ 'border-left': { width: 'BorderLeftWidth', color: 'BorderLeft', colorProperty: 'border-left-color', side: true },
2423
+ }[property]
2424
+ if (!targets) return null
2425
+ const parts = splitCssWords(rawValue.trim())
2426
+ if (parts.length === 0) return null
2427
+ const width = parseStaticCssLengthSpec(parts[0])
2428
+ if (!width) return null
2429
+ let color = null
2430
+ let colorVar = null
2431
+ for (const part of parts) {
2432
+ const literal = parseStaticCssColor(part)
2433
+ if (literal) {
2434
+ color = { target: targets.color, color: literal }
2435
+ break
2436
+ }
2437
+ const variable = cssStaticColorVarValue(targets.colorProperty, part)
2438
+ if (variable) {
2439
+ colorVar = variable
2440
+ break
2441
+ }
2442
+ }
2443
+ if (!targets.side && color && !colorVar) return null
2444
+ return { setup: width.setup, widthTarget: targets.width, width: width.spec, color, colorVar }
2445
+ }
2446
+
2447
+ function staticColorCode(color) {
2448
+ return `{${color.r}, ${color.g}, ${color.b}, ${color.a}}`
2449
+ }
2450
+
2451
+ function staticColorRefCode(ref) {
2452
+ const name = ref.name ? JSON.stringify(ref.name) : 'nullptr'
2453
+ return `{${name}, ${staticColorCode(ref.color)}, ${ref.hasFallback ? 'true' : 'false'}}`
2454
+ }
2455
+
2456
+ function staticLinearGradientCode(gradient) {
2457
+ return `{${gradient.angleTenths}, ${staticColorCode(gradient.from)}, ${staticColorCode(gradient.mid)}, ${staticColorCode(gradient.to)}, ${gradient.midStopPermille}, ${gradient.toStopPermille}, ${gradient.hasMid ? 'true' : 'false'}}`
2458
+ }
2459
+
2460
+ function staticLinearGradientRefCode(gradient) {
2461
+ return `{${gradient.angleTenths}, ${staticColorRefCode(gradient.from)}, ${staticColorRefCode(gradient.mid)}, ${staticColorRefCode(gradient.to)}, ${gradient.midStopPermille}, ${gradient.toStopPermille}, ${gradient.hasMid ? 'true' : 'false'}}`
2462
+ }
2463
+
2464
+ function staticRadialGradientRefCode(gradient) {
2465
+ return `{${staticColorRefCode(gradient.from)}, ${staticColorRefCode(gradient.to)}, ${gradient.stopPermille}, ${gradient.cxPermille}, ${gradient.cyPermille}, ${gradient.rxPermille}, ${gradient.ryPermille}, ${gradient.enabled ? 'true' : 'false'}}`
2466
+ }
2467
+
2468
+ function zeroStaticLinearGradient() {
2469
+ const transparent = { r: 0, g: 0, b: 0, a: 0 }
2470
+ return {
2471
+ angleTenths: 1800,
2472
+ from: transparent,
2473
+ mid: transparent,
2474
+ to: transparent,
2475
+ midStopPermille: 500,
2476
+ toStopPermille: 1000,
2477
+ hasMid: false,
2478
+ }
2479
+ }
2480
+
2481
+ function zeroStaticLinearGradientRef() {
2482
+ const transparentRef = { name: null, hasFallback: true, color: { r: 0, g: 0, b: 0, a: 0 } }
2483
+ return {
2484
+ angleTenths: 1800,
2485
+ from: transparentRef,
2486
+ mid: transparentRef,
2487
+ to: transparentRef,
2488
+ midStopPermille: 500,
2489
+ toStopPermille: 1000,
2490
+ hasMid: false,
2491
+ }
2492
+ }
2493
+
2494
+ function zeroStaticRadialGradientRef() {
2495
+ const transparentRef = { name: null, hasFallback: true, color: { r: 0, g: 0, b: 0, a: 0 } }
2496
+ return {
2497
+ from: transparentRef,
2498
+ to: transparentRef,
2499
+ stopPermille: 1000,
2500
+ cxPermille: 500,
2501
+ cyPermille: 500,
2502
+ rxPermille: 1000,
2503
+ ryPermille: 1000,
2504
+ enabled: false,
2505
+ }
2506
+ }
2507
+
2508
+ function staticGridLineCode(line) {
2509
+ if (!line) return `{false, ${staticLengthSpecCode(zeroStaticLengthSpec())}, {0, 0, 0, 255}}`
2510
+ return `{true, ${staticLengthSpecCode(line.width)}, ${staticColorCode(line.color)}}`
2511
+ }
2512
+
2513
+ function staticGridTemplateTrackCode(track) {
2514
+ return `{${track.type}, ${track.value}, ${staticLengthSpecCode(track.length)}}`
2515
+ }
2516
+
2517
+ function cssHasDynamicValue(value) {
2518
+ const lower = value.toLowerCase()
2519
+ return lower.includes('var(') ||
2520
+ lower.includes('calc(') ||
2521
+ lower.includes('min(') ||
2522
+ lower.includes('max(') ||
2523
+ lower.includes('clamp(')
2524
+ }
2525
+
2526
+ function staticColorEquals(a, b) {
2527
+ return a && b && a.r === b.r && a.g === b.g && a.b === b.b && a.a === b.a
2528
+ }
2529
+
2530
+ function staticLengthSpecEquals(a, b) {
2531
+ return a && b && a.unit === b.unit && a.value === b.value
2532
+ }
2533
+
2534
+ function staticLengthSpecIsSafeGridLine(spec) {
2535
+ if (!spec || (spec.unit !== 'Raw' && spec.unit !== 'Px')) return false
2536
+ const value = Number(spec.value)
2537
+ return Number.isFinite(value) && value >= 1
2538
+ }
2539
+
2540
+ function parseStaticGradientStop(rawStop) {
2541
+ const words = splitCssWords(rawStop.trim())
2542
+ if (words.length === 0) return null
2543
+ const color = parseStaticCssColor(words[0])
2544
+ if (!color) return null
2545
+ let stopPermille = null
2546
+ let length = null
2547
+ if (words.length > 1) {
2548
+ const stop = words[1].trim()
2549
+ const percent = stop.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))%$/)
2550
+ if (percent) {
2551
+ stopPermille = roundCssNumber(Number(percent[1]) * 10)
2552
+ } else {
2553
+ const parsedLength = parseStaticCssLengthSpec(stop)
2554
+ if (parsedLength) length = parsedLength
2555
+ }
2556
+ }
2557
+ return { color, stopPermille, length }
2558
+ }
2559
+
2560
+ function parseStaticGradientStopRef(rawStop) {
2561
+ const words = splitCssWords(rawStop.trim())
2562
+ if (words.length === 0) return null
2563
+ const color = parseStaticCssColorRef(words[0])
2564
+ if (!color) return null
2565
+ let stopPermille = null
2566
+ if (words.length > 1) {
2567
+ const stop = words[1].trim()
2568
+ const percent = stop.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))%$/)
2569
+ if (percent) stopPermille = roundCssNumber(Number(percent[1]) * 10)
2570
+ }
2571
+ return { color, stopPermille }
2572
+ }
2573
+
2574
+ function parseStaticGradientHeader(parts) {
2575
+ if (parts.length < 2) return null
2576
+ const first = parts[0].trim()
2577
+ const lower = first.toLowerCase()
2578
+ if (first.includes('deg') || first.includes('turn') || first.includes('rad')) {
2579
+ const angle = parseStaticAngleTenths(first)
2580
+ return angle === null ? null : { angleTenths: angle, colorStart: 1 }
2581
+ }
2582
+ if (lower.startsWith('to ')) {
2583
+ let angleTenths = 1800
2584
+ if (lower.includes('right')) angleTenths = 900
2585
+ else if (lower.includes('left')) angleTenths = 2700
2586
+ else if (lower.includes('top')) angleTenths = 0
2587
+ return { angleTenths, colorStart: 1 }
2588
+ }
2589
+ return { angleTenths: 1800, colorStart: 0 }
2590
+ }
2591
+
2592
+ function parseStaticLinearGradientRefLayer(rawLayer) {
2593
+ const inner = staticCssFunctionInner(rawLayer.trim(), 'linear-gradient')
2594
+ if (inner === null) return null
2595
+ const parts = splitTopLevelCss(inner, ',')
2596
+ const header = parseStaticGradientHeader(parts)
2597
+ if (!header || header.colorStart >= parts.length) return null
2598
+
2599
+ const from = parseStaticGradientStopRef(parts[header.colorStart])
2600
+ const to = parseStaticGradientStopRef(parts[parts.length - 1])
2601
+ if (!from || !to) return null
2602
+
2603
+ let mid = { color: { name: null, hasFallback: true, color: { r: 0, g: 0, b: 0, a: 0 } }, stopPermille: 500 }
2604
+ let hasMid = false
2605
+ if (parts.length === header.colorStart + 2) {
2606
+ if (from.stopPermille !== null && from.stopPermille > 0 && from.stopPermille < 1000) {
2607
+ mid = { color: from.color, stopPermille: from.stopPermille }
2608
+ hasMid = true
2609
+ }
2610
+ } else if (parts.length > header.colorStart + 2) {
2611
+ const parsedMid = parseStaticGradientStopRef(parts[header.colorStart + 1])
2612
+ if (!parsedMid) return null
2613
+ mid = {
2614
+ color: parsedMid.color,
2615
+ stopPermille: parsedMid.stopPermille === null ? 500 : Math.max(0, Math.min(1000, parsedMid.stopPermille)),
2616
+ }
2617
+ hasMid = true
2618
+ }
2619
+
2620
+ let toStopPermille = to.stopPermille === null ? 1000 : Math.max(0, Math.min(60000, to.stopPermille))
2621
+ if (toStopPermille <= 0) toStopPermille = 1
2622
+ if (hasMid && toStopPermille <= mid.stopPermille) toStopPermille = mid.stopPermille + 1
2623
+
2624
+ return {
2625
+ angleTenths: header.angleTenths,
2626
+ from: from.color,
2627
+ mid: mid.color,
2628
+ to: to.color,
2629
+ midStopPermille: mid.stopPermille,
2630
+ toStopPermille,
2631
+ hasMid,
2632
+ }
2633
+ }
2634
+
2635
+ function parseStaticPercentPermille(rawPart, fallback) {
2636
+ const length = parseStaticCssLength(rawPart.trim())
2637
+ if (!length || length.unit !== 'Percent') return fallback
2638
+ return roundCssNumber(length.number * 10)
2639
+ }
2640
+
2641
+ function parseStaticRadialGradientRefLayer(rawLayer) {
2642
+ const inner = staticCssFunctionInner(rawLayer.trim(), 'radial-gradient')
2643
+ if (inner === null) return null
2644
+ const parts = splitTopLevelCss(inner, ',')
2645
+ if (parts.length < 2) return null
2646
+
2647
+ let colorStart = 0
2648
+ let cxPermille = 500
2649
+ let cyPermille = 500
2650
+ let rxPermille = 1000
2651
+ let ryPermille = 1000
2652
+ if (!parseStaticGradientStopRef(parts[0])) {
2653
+ colorStart = 1
2654
+ const words = splitCssWords(parts[0])
2655
+ let atIndex = words.length
2656
+ for (let i = 0; i < words.length; i++) {
2657
+ if (words[i].toLowerCase() === 'at') {
2658
+ atIndex = i
2659
+ break
2660
+ }
2661
+ }
2662
+ const sizeWords = []
2663
+ for (let i = 0; i < atIndex; i++) {
2664
+ const lower = words[i].toLowerCase()
2665
+ if (lower === 'circle' || lower === 'ellipse' || lower === 'closest-side' ||
2666
+ lower === 'closest-corner' || lower === 'farthest-side' || lower === 'farthest-corner')
2667
+ continue
2668
+ sizeWords.push(words[i])
2669
+ }
2670
+ if (sizeWords.length > 0) rxPermille = parseStaticPercentPermille(sizeWords[0], rxPermille)
2671
+ if (sizeWords.length > 1) ryPermille = parseStaticPercentPermille(sizeWords[1], ryPermille)
2672
+ else if (sizeWords.length > 0) ryPermille = rxPermille
2673
+
2674
+ if (atIndex < words.length) {
2675
+ const centerWords = words.slice(atIndex + 1)
2676
+ if (centerWords.length > 0) {
2677
+ const x = parseStaticOriginPart(centerWords[0], cxPermille)
2678
+ if (x === null) return null
2679
+ cxPermille = x
2680
+ }
2681
+ if (centerWords.length > 1) {
2682
+ const y = parseStaticOriginPart(centerWords[1], cyPermille)
2683
+ if (y === null) return null
2684
+ cyPermille = y
2685
+ }
2686
+ }
2687
+ }
2688
+ if (colorStart + 1 >= parts.length) return null
2689
+ const from = parseStaticGradientStopRef(parts[colorStart])
2690
+ const to = parseStaticGradientStopRef(parts[parts.length - 1])
2691
+ if (!from || !to) return null
2692
+ let stopPermille = to.stopPermille === null ? 1000 : Math.max(0, Math.min(1000, to.stopPermille))
2693
+ if (stopPermille <= 0) stopPermille = 1
2694
+ return {
2695
+ from: from.color,
2696
+ to: to.color,
2697
+ stopPermille,
2698
+ cxPermille,
2699
+ cyPermille,
2700
+ rxPermille,
2701
+ ryPermille,
2702
+ enabled: true,
2703
+ }
2704
+ }
2705
+
2706
+ function parseStaticLinearGradientLayer(rawLayer) {
2707
+ const inner = staticCssFunctionInner(rawLayer.trim(), 'linear-gradient')
2708
+ if (inner === null) return null
2709
+ const parts = splitTopLevelCss(inner, ',')
2710
+ const header = parseStaticGradientHeader(parts)
2711
+ if (!header || header.colorStart >= parts.length) return null
2712
+
2713
+ const from = parseStaticGradientStop(parts[header.colorStart])
2714
+ const to = parseStaticGradientStop(parts[parts.length - 1])
2715
+ if (!from || !to) return null
2716
+
2717
+ let mid = { color: { r: 0, g: 0, b: 0, a: 0 }, stopPermille: 500 }
2718
+ let hasMid = false
2719
+ if (parts.length === header.colorStart + 2) {
2720
+ if (from.stopPermille !== null && from.stopPermille > 0 && from.stopPermille < 1000) {
2721
+ mid = { color: from.color, stopPermille: from.stopPermille }
2722
+ hasMid = true
2723
+ }
2724
+ } else if (parts.length > header.colorStart + 2) {
2725
+ const parsedMid = parseStaticGradientStop(parts[header.colorStart + 1])
2726
+ if (!parsedMid) return null
2727
+ mid = {
2728
+ color: parsedMid.color,
2729
+ stopPermille: parsedMid.stopPermille === null ? 500 : Math.max(0, Math.min(1000, parsedMid.stopPermille)),
2730
+ }
2731
+ hasMid = true
2732
+ }
2733
+
2734
+ let toStopPermille = to.stopPermille === null ? 1000 : Math.max(0, Math.min(60000, to.stopPermille))
2735
+ if (toStopPermille <= 0) toStopPermille = 1
2736
+ if (hasMid && toStopPermille <= mid.stopPermille) toStopPermille = mid.stopPermille + 1
2737
+
2738
+ return {
2739
+ angleTenths: header.angleTenths,
2740
+ from: from.color,
2741
+ mid: mid.color,
2742
+ to: to.color,
2743
+ midStopPermille: mid.stopPermille,
2744
+ toStopPermille,
2745
+ hasMid,
2746
+ }
2747
+ }
2748
+
2749
+ function staticGradientLineIsVertical(angleTenths) {
2750
+ const angleRadians = angleTenths * Math.PI / 1800
2751
+ const dx = Math.sin(angleRadians)
2752
+ const dy = -Math.cos(angleRadians)
2753
+ return Math.abs(dx) >= Math.abs(dy)
2754
+ }
2755
+
2756
+ function parseStaticGradientLineLayer(rawLayer) {
2757
+ const inner = staticCssFunctionInner(rawLayer.trim(), 'linear-gradient')
2758
+ if (inner === null) return { matched: false, line: null, setup: [] }
2759
+ const parts = splitTopLevelCss(inner, ',')
2760
+ const header = parseStaticGradientHeader(parts)
2761
+ if (!header || header.colorStart + 1 >= parts.length) return { matched: false, line: null, setup: [] }
2762
+
2763
+ const first = parseStaticGradientStop(parts[header.colorStart])
2764
+ const second = parseStaticGradientStop(parts[header.colorStart + 1])
2765
+ if (!first || !second || !first.length || second.color.a !== 0) return { matched: false, line: null, setup: [] }
2766
+ if (first.color.a === 0 || !staticLengthSpecIsSafeGridLine(first.length.spec))
2767
+ return { matched: true, line: null, setup: [] }
2768
+ if (second.length && !staticLengthSpecEquals(first.length.spec, second.length.spec))
2769
+ return { matched: true, line: null, setup: [] }
2770
+
2771
+ const axis = staticGradientLineIsVertical(header.angleTenths) ? 'x' : 'y'
2772
+ return {
2773
+ matched: true,
2774
+ setup: first.length.setup,
2775
+ line: { axis, width: first.length.spec, color: first.color },
2776
+ }
2777
+ }
2778
+
2779
+ function cssStaticBackgroundValue(property, rawValue) {
2780
+ if (property !== 'background' && property !== 'background-image') return null
2781
+ if (cssHasDynamicValue(rawValue)) return null
2782
+ const layers = splitTopLevelCss(rawValue, ',')
2783
+ if (layers.length === 0) return null
2784
+
2785
+ const out = {
2786
+ setup: [],
2787
+ gradient: null,
2788
+ overlayGradient: zeroStaticLinearGradient(),
2789
+ hasOverlayGradient: false,
2790
+ gridX: null,
2791
+ gridY: null,
2792
+ gridOrder: [],
2793
+ }
2794
+
2795
+ for (const layer of layers) {
2796
+ const line = parseStaticGradientLineLayer(layer)
2797
+ if (line.matched) {
2798
+ if (!line.line) return null
2799
+ out.setup.push(...line.setup)
2800
+ if (line.line.axis === 'x') out.gridX = line.line
2801
+ else out.gridY = line.line
2802
+ out.gridOrder.push(line.line.axis)
2803
+ continue
2804
+ }
2805
+
2806
+ const gradient = parseStaticLinearGradientLayer(layer)
2807
+ if (gradient) {
2808
+ if (out.gradient) {
2809
+ out.overlayGradient = out.gradient
2810
+ out.hasOverlayGradient = true
2811
+ }
2812
+ out.gradient = gradient
2813
+ continue
2814
+ }
2815
+ if (layer.toLowerCase().includes('var(')) return null
2816
+ }
2817
+
2818
+ if (!out.gradient) return null
2819
+ if (out.gridX && out.gridY && !staticColorEquals(out.gridX.color, out.gridY.color) && out.gridOrder.join('') !== 'xy') return null
2820
+ return out
2821
+ }
2822
+
2823
+ function colorRefUsesVar(ref) {
2824
+ return Boolean(ref && ref.name)
2825
+ }
2826
+
2827
+ function linearGradientRefUsesVar(gradient) {
2828
+ return colorRefUsesVar(gradient.from) ||
2829
+ colorRefUsesVar(gradient.mid) ||
2830
+ colorRefUsesVar(gradient.to)
2831
+ }
2832
+
2833
+ function cssStaticBackgroundFullValue(property, rawValue) {
2834
+ if (property !== 'background' && property !== 'background-image') return null
2835
+ const lower = rawValue.toLowerCase()
2836
+ if (lower.includes('calc(') || lower.includes('min(') || lower.includes('max(') || lower.includes('clamp('))
2837
+ return null
2838
+ const layers = splitTopLevelCss(rawValue, ',')
2839
+ if (layers.length === 0) return null
2840
+
2841
+ const out = {
2842
+ setup: [],
2843
+ gradient: null,
2844
+ overlayGradient: zeroStaticLinearGradientRef(),
2845
+ hasOverlayGradient: false,
2846
+ radialGradient: zeroStaticRadialGradientRef(),
2847
+ gridX: null,
2848
+ gridY: null,
2849
+ gridOrder: [],
2850
+ needsFullApi: false,
2851
+ }
2852
+
2853
+ for (const layer of layers) {
2854
+ const line = parseStaticGradientLineLayer(layer)
2855
+ if (line.matched) {
2856
+ if (!line.line) return null
2857
+ out.setup.push(...line.setup)
2858
+ if (line.line.axis === 'x') out.gridX = line.line
2859
+ else out.gridY = line.line
2860
+ out.gridOrder.push(line.line.axis)
2861
+ continue
2862
+ }
2863
+
2864
+ const radialGradient = parseStaticRadialGradientRefLayer(layer)
2865
+ if (radialGradient) {
2866
+ out.radialGradient = radialGradient
2867
+ out.needsFullApi = true
2868
+ continue
2869
+ }
2870
+
2871
+ const gradient = parseStaticLinearGradientRefLayer(layer)
2872
+ if (gradient) {
2873
+ if (out.gradient) {
2874
+ out.overlayGradient = out.gradient
2875
+ out.hasOverlayGradient = true
2876
+ }
2877
+ out.gradient = gradient
2878
+ if (linearGradientRefUsesVar(gradient)) out.needsFullApi = true
2879
+ continue
2880
+ }
2881
+
2882
+ if (layer.toLowerCase().includes('var(')) return null
2883
+ }
2884
+
2885
+ if (!out.gradient) return null
2886
+ if (out.gridX && out.gridY && !staticColorEquals(out.gridX.color, out.gridY.color) && out.gridOrder.join('') !== 'xy') return null
2887
+ return out.needsFullApi ? out : null
2888
+ }
2889
+
2890
+ function cssStaticBackgroundSizeValue(property, rawValue) {
2891
+ if (property !== 'background-size') return null
2892
+ if (cssHasDynamicValue(rawValue)) return null
2893
+ const layers = splitTopLevelCss(rawValue, ',')
2894
+ if (layers.length === 0) return null
2895
+ const parts = splitCssWords(layers[0])
2896
+ if (parts.length === 0) return null
2897
+ const x = parseStaticCssLengthSpec(parts[0])
2898
+ if (!x) return null
2899
+ const y = parts.length > 1 ? parseStaticCssLengthSpec(parts[1]) : x
2900
+ if (!y) return null
2901
+ return { setup: [...x.setup, ...(y === x ? [] : y.setup)], stepX: x.spec, stepY: y.spec }
2902
+ }
2903
+
2904
+ function parseStaticGridTemplateTrack(rawToken) {
2905
+ const token = rawToken.trim()
2906
+ const lower = token.toLowerCase()
2907
+ if (!token) return null
2908
+ if (lower === 'auto') return { type: 0, value: 0, length: zeroStaticLengthSpec() }
2909
+ if (lower.startsWith('minmax(')) {
2910
+ const inner = staticCssFunctionInner(token, 'minmax')
2911
+ if (inner === null) return null
2912
+ const parts = splitTopLevelCss(inner, ',')
2913
+ if (parts.length === 0) return null
2914
+ return parseStaticGridTemplateTrack(parts.length > 1 ? parts[1] : parts[0])
2915
+ }
2916
+ if (lower.endsWith('fr')) {
2917
+ const number = Number(lower.slice(0, -2).trim())
2918
+ if (!Number.isFinite(number)) return null
2919
+ return { type: 2, value: Math.max(1, roundCssNumber(number)), length: zeroStaticLengthSpec() }
2920
+ }
2921
+ const length = parseStaticCssLengthSpec(token)
2922
+ if (!length) return null
2923
+ return { type: 1, value: 0, length: length.spec, setup: length.setup }
2924
+ }
2925
+
2926
+ function cssStaticGridTemplateValue(property, rawValue) {
2927
+ let target = null
2928
+ if (property === 'grid-template-columns') target = 'Columns'
2929
+ else if (property === 'grid-template-rows') target = 'Rows'
2930
+ const lowerValue = rawValue.toLowerCase()
2931
+ if (!target || lowerValue.includes('var(') || lowerValue.includes('calc(') || lowerValue.includes('clamp('))
2932
+ return null
2933
+
2934
+ const tracks = []
2935
+ const setup = []
2936
+ for (const token of splitCssWords(rawValue.trim())) {
2937
+ const lower = token.toLowerCase()
2938
+ if (lower.startsWith('repeat(')) {
2939
+ const inner = staticCssFunctionInner(token, 'repeat')
2940
+ if (inner === null) return null
2941
+ const parts = splitTopLevelCss(inner, ',')
2942
+ if (parts.length < 2) return null
2943
+ let repeatCount = roundCssNumber(Number(parts[0].trim()))
2944
+ if (!Number.isFinite(repeatCount)) return null
2945
+ repeatCount = Math.max(0, Math.min(8, repeatCount))
2946
+ const track = parseStaticGridTemplateTrack(parts[1])
2947
+ if (!track) return null
2948
+ if (track.setup) setup.push(...track.setup)
2949
+ for (let i = 0; i < repeatCount && tracks.length < 8; i++) tracks.push(track)
2950
+ continue
2951
+ }
2952
+ const track = parseStaticGridTemplateTrack(token)
2953
+ if (!track) return null
2954
+ if (track.setup) setup.push(...track.setup)
2955
+ if (tracks.length < 8) tracks.push(track)
2956
+ }
2957
+ return { target, tracks, setup }
2958
+ }
2959
+
2960
+ function parseStaticTimeMs(rawToken) {
2961
+ const match = rawToken.trim().toLowerCase().match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(ms|s)$/)
2962
+ if (!match) return null
2963
+ const amount = Number(match[1])
2964
+ if (!Number.isFinite(amount)) return null
2965
+ const ms = match[2] === 's' ? amount * 1000 : amount
2966
+ return Math.max(0, roundCssNumber(ms))
2967
+ }
2968
+
2969
+ function parseStaticAnimationEasing(rawToken) {
2970
+ const token = rawToken.trim()
2971
+ const lower = token.toLowerCase()
2972
+ const keywords = new Map([
2973
+ ['linear', 'Linear'],
2974
+ ['ease', 'Ease'],
2975
+ ['ease-in', 'EaseIn'],
2976
+ ['ease-out', 'EaseOut'],
2977
+ ['ease-in-out', 'EaseInOut'],
2978
+ ])
2979
+ if (keywords.has(lower)) {
2980
+ return { kind: keywords.get(lower), x1: 0, y1: 0, x2: 1, y2: 1, steps: 1 }
2981
+ }
2982
+ const cubic = staticCssFunctionInner(lower, 'cubic-bezier')
2983
+ if (cubic !== null) {
2984
+ const parts = splitTopLevelCss(cubic, ',').map((part) => Number(part.trim()))
2985
+ if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) return null
2986
+ return { kind: 'CubicBezier', x1: parts[0], y1: parts[1], x2: parts[2], y2: parts[3], steps: 1 }
2987
+ }
2988
+ const steps = staticCssFunctionInner(lower, 'steps')
2989
+ if (steps !== null) {
2990
+ const count = Number.parseInt(steps.trim(), 10)
2991
+ return { kind: 'Steps', x1: 0, y1: 0, x2: 1, y2: 1, steps: count > 0 ? count : 1 }
2992
+ }
2993
+ return null
2994
+ }
2995
+
2996
+ function staticAnimationEasingCode(easing) {
2997
+ return `{gea::embedded::ui::StaticStyleAnimationEasingKind::${easing.kind}, ${cssNumberCode(easing.x1)}, ${cssNumberCode(easing.y1)}, ${cssNumberCode(easing.x2)}, ${cssNumberCode(easing.y2)}, ${easing.steps}}`
2998
+ }
2999
+
3000
+ function cssStaticAnimationValue(property, rawValue) {
3001
+ if (property !== 'animation') return null
3002
+ const spec = {
3003
+ name: null,
3004
+ durationMs: 0,
3005
+ delayMs: 0,
3006
+ iterations: 1,
3007
+ direction: 'Normal',
3008
+ fill: 'None',
3009
+ easing: { kind: 'Ease', x1: 0, y1: 0, x2: 1, y2: 1, steps: 1 },
3010
+ }
3011
+ let sawDuration = false
3012
+ let sawName = false
3013
+ for (const rawToken of splitCssWords(rawValue)) {
3014
+ const token = rawToken.trim()
3015
+ const lower = token.toLowerCase()
3016
+ const timeMs = parseStaticTimeMs(lower)
3017
+ if (timeMs !== null) {
3018
+ if (!sawDuration) {
3019
+ spec.durationMs = timeMs
3020
+ sawDuration = true
3021
+ } else {
3022
+ spec.delayMs = timeMs
3023
+ }
3024
+ continue
3025
+ }
3026
+ if (lower === 'infinite') {
3027
+ spec.iterations = -1
3028
+ continue
3029
+ }
3030
+ if (/^\d+$/.test(lower)) {
3031
+ spec.iterations = Number.parseInt(lower, 10)
3032
+ continue
3033
+ }
3034
+ if (lower === 'reverse') {
3035
+ spec.direction = 'Reverse'
3036
+ continue
3037
+ }
3038
+ if (lower === 'alternate') {
3039
+ spec.direction = 'Alternate'
3040
+ continue
3041
+ }
3042
+ if (lower === 'alternate-reverse') {
3043
+ spec.direction = 'AlternateReverse'
3044
+ continue
3045
+ }
3046
+ if (lower === 'forwards') {
3047
+ spec.fill = 'Forwards'
3048
+ continue
3049
+ }
3050
+ if (lower === 'backwards') {
3051
+ spec.fill = 'Backwards'
3052
+ continue
3053
+ }
3054
+ if (lower === 'both') {
3055
+ spec.fill = 'Both'
3056
+ continue
3057
+ }
3058
+ const easing = parseStaticAnimationEasing(token)
3059
+ if (easing) {
3060
+ spec.easing = easing
3061
+ continue
3062
+ }
3063
+ if (lower === 'normal' || lower === 'running' || lower === 'none') continue
3064
+ if (!sawName) {
3065
+ spec.name = token
3066
+ sawName = true
3067
+ }
3068
+ }
3069
+ return spec.name && spec.durationMs > 0 ? spec : null
3070
+ }
3071
+
3072
+ function cssStaticCustomLengthValue(property, rawValue) {
3073
+ if (!property.startsWith('--')) return null
3074
+ const parsed = parseStaticCssLengthSpec(rawValue)
3075
+ return parsed ? { name: property, ...parsed } : null
3076
+ }
3077
+
3078
+ function cssStaticCustomColorValue(property, rawValue) {
3079
+ if (!property.startsWith('--')) return null
3080
+ const color = parseStaticCssColor(rawValue)
3081
+ return color ? { name: property, color } : null
3082
+ }
3083
+
3084
+ function cssStaticLengthPropertyTarget(property) {
3085
+ const map = new Map([
3086
+ ['gap', 'Gap'],
3087
+ ['width', 'Width'],
3088
+ ['height', 'Height'],
3089
+ ['min-width', 'MinWidth'],
3090
+ ['min-height', 'MinHeight'],
3091
+ ['max-width', 'MaxWidth'],
3092
+ ['max-height', 'MaxHeight'],
3093
+ ['flex-basis', 'FlexBasis'],
3094
+ ['padding-top', 'PaddingTop'],
3095
+ ['padding-right', 'PaddingRight'],
3096
+ ['padding-bottom', 'PaddingBottom'],
3097
+ ['padding-left', 'PaddingLeft'],
3098
+ ['margin-top', 'MarginTop'],
3099
+ ['margin-right', 'MarginRight'],
3100
+ ['margin-bottom', 'MarginBottom'],
3101
+ ['margin-left', 'MarginLeft'],
3102
+ ['border-width', 'BorderWidth'],
3103
+ ['border-top-width', 'BorderTopWidth'],
3104
+ ['border-right-width', 'BorderRightWidth'],
3105
+ ['border-bottom-width', 'BorderBottomWidth'],
3106
+ ['border-left-width', 'BorderLeftWidth'],
3107
+ ['font-size', 'FontSize'],
3108
+ ['perspective', 'Perspective'],
3109
+ ['top', 'Top'],
3110
+ ['right', 'Right'],
3111
+ ['bottom', 'Bottom'],
3112
+ ['left', 'Left'],
3113
+ ])
3114
+ return map.get(property) ?? null
3115
+ }
3116
+
3117
+ function cssStaticLengthValue(property, rawValue) {
3118
+ const target = cssStaticLengthPropertyTarget(property)
3119
+ if (!target) return null
3120
+ if (rawValue.trim().toLowerCase() === 'auto' && (target === 'Width' || target === 'Height')) {
3121
+ return { target, length: { unit: 'Auto', number: 0 } }
3122
+ }
3123
+ const length = parseStaticCssLength(rawValue)
3124
+ if (!length) return null
3125
+ return { target, length }
3126
+ }
3127
+
3128
+ function cssStaticLengthExpressionValue(property, rawValue) {
3129
+ const target = cssStaticLengthPropertyTarget(property)
3130
+ if (!target) return null
3131
+ const parsed = parseStaticCssLengthSpec(rawValue)
3132
+ if (!parsed || parsed.spec.unit !== 'Expression') return null
3133
+ return { target, ...parsed }
3134
+ }
3135
+
3136
+ function cssStaticMaskImageValue(property, rawValue) {
3137
+ if (property !== 'mask-image' && property !== '-webkit-mask-image') return null
3138
+ const value = rawValue.trim()
3139
+ const lower = value.toLowerCase()
3140
+ if (!value || lower === 'none') return { setup: [], spec: zeroStaticLengthSpec() }
3141
+ if (!lower.includes('linear-gradient') || !lower.includes('to right') || !lower.includes('transparent'))
3142
+ return null
3143
+ const match = value.match(/calc\(\s*100%\s*-\s*([^)]+?)\s*\)/i)
3144
+ if (!match) return null
3145
+ const parsed = parseStaticCssLengthSpec(match[1])
3146
+ if (!parsed) return null
3147
+ return parsed
3148
+ }
3149
+
3150
+ function cssStaticBoxShadowNoneValue(property, rawValue) {
3151
+ if (property !== 'box-shadow') return null
3152
+ const value = rawValue.trim()
3153
+ if (!value || value.toLowerCase() === 'none') return {}
3154
+ for (const layer of splitTopLevelCss(value, ',')) {
3155
+ for (const token of splitCssWords(layer)) {
3156
+ if (token.trim().toLowerCase() === 'inset') return null
3157
+ }
3158
+ }
3159
+ return {}
3160
+ }
3161
+
3162
+ function parseStaticOriginPart(rawPart, fallback) {
3163
+ const part = rawPart.trim().toLowerCase()
3164
+ if (!part) return fallback
3165
+ if (part === 'left' || part === 'top') return 0
3166
+ if (part === 'center') return 500
3167
+ if (part === 'right' || part === 'bottom') return 1000
3168
+ const length = parseStaticCssLength(part)
3169
+ if (!length || length.unit !== 'Percent') return null
3170
+ return roundCssNumber(length.number * 10)
3171
+ }
3172
+
3173
+ function cssStaticOriginValue(property, rawValue) {
3174
+ let target = null
3175
+ if (property === 'transform-origin') target = 'TransformOrigin'
3176
+ else if (property === 'perspective-origin') target = 'PerspectiveOrigin'
3177
+ if (!target) return null
3178
+ const parts = rawValue.trim().split(/\s+/).filter(Boolean)
3179
+ if (parts.length > 2) return null
3180
+ const x = parseStaticOriginPart(parts[0] ?? '', 500)
3181
+ const y = parseStaticOriginPart(parts[1] ?? '', 500)
3182
+ if (x === null || y === null) return null
3183
+ return { target, x, y }
3184
+ }
3185
+
3186
+ const staticTransformFlags = {
3187
+ rotateX: 1 << 0,
3188
+ rotateY: 1 << 1,
3189
+ rotateZ: 1 << 2,
3190
+ translateX: 1 << 3,
3191
+ translateY: 1 << 4,
3192
+ translateZ: 1 << 5,
3193
+ scaleX: 1 << 8,
3194
+ scaleY: 1 << 9,
3195
+ }
3196
+
3197
+ function parseStaticAngleTenths(rawValue) {
3198
+ const value = rawValue.trim()
3199
+ const match = value.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(deg|rad|turn)?$/i)
3200
+ if (!match) return null
3201
+ let degrees = Number(match[1])
3202
+ if (!Number.isFinite(degrees)) return null
3203
+ const unit = (match[2] || 'deg').toLowerCase()
3204
+ if (unit === 'rad') degrees = degrees * 180 / Math.PI
3205
+ else if (unit === 'turn') degrees *= 360
3206
+ return roundCssNumber(degrees * 10)
3207
+ }
3208
+
3209
+ function parseStaticScalePermille(rawValue) {
3210
+ const value = Number(rawValue.trim())
3211
+ if (!Number.isFinite(value)) return null
3212
+ return roundCssNumber(value * 1000)
3213
+ }
3214
+
3215
+ function parseStaticTransformValue(rawValue) {
3216
+ const value = rawValue.trim()
3217
+ const out = {
3218
+ setup: [],
3219
+ flags: 0,
3220
+ rotateX: 0,
3221
+ rotateY: 0,
3222
+ rotateZ: 0,
3223
+ translateX: zeroStaticLengthSpec(),
3224
+ translateY: zeroStaticLengthSpec(),
3225
+ translateZ: zeroStaticLengthSpec(),
3226
+ scaleX: 1000,
3227
+ scaleY: 1000,
3228
+ }
3229
+ let sawTransform = false
3230
+ let i = 0
3231
+ while (i < value.length) {
3232
+ while (i < value.length && /\s/.test(value[i])) i++
3233
+ if (i >= value.length) break
3234
+ const nameStart = i
3235
+ while (i < value.length && /[A-Za-z3]/.test(value[i])) i++
3236
+ if (i === nameStart) return null
3237
+ while (i < value.length && /\s/.test(value[i])) i++
3238
+ if (i >= value.length || value[i] !== '(') return null
3239
+ const name = value.slice(nameStart, i).toLowerCase()
3240
+ const argStart = ++i
3241
+ let depth = 1
3242
+ while (i < value.length && depth > 0) {
3243
+ if (value[i] === '(') depth++
3244
+ else if (value[i] === ')') depth--
3245
+ i++
3246
+ }
3247
+ if (depth !== 0) return null
3248
+ const arg = value.slice(argStart, i - 1)
3249
+ const args = splitTopLevelCss(arg, ',')
3250
+ sawTransform = true
3251
+
3252
+ const setLength = (field, flag, raw) => {
3253
+ const parsed = parseStaticCssLengthSpec(raw)
3254
+ if (!parsed) return false
3255
+ out.setup.push(...parsed.setup)
3256
+ out[field] = parsed.spec
3257
+ out.flags |= flag
3258
+ return true
3259
+ }
3260
+
3261
+ if (name === 'rotate' || name === 'rotatez') {
3262
+ const angle = parseStaticAngleTenths(arg)
3263
+ if (angle === null) return null
3264
+ out.rotateZ = angle
3265
+ out.flags |= staticTransformFlags.rotateZ
3266
+ } else if (name === 'rotatex') {
3267
+ const angle = parseStaticAngleTenths(arg)
3268
+ if (angle === null) return null
3269
+ out.rotateX = angle
3270
+ out.flags |= staticTransformFlags.rotateX
3271
+ } else if (name === 'rotatey') {
3272
+ const angle = parseStaticAngleTenths(arg)
3273
+ if (angle === null) return null
3274
+ out.rotateY = angle
3275
+ out.flags |= staticTransformFlags.rotateY
3276
+ } else if (name === 'translatex') {
3277
+ if (!setLength('translateX', staticTransformFlags.translateX, arg)) return null
3278
+ } else if (name === 'translatey') {
3279
+ if (!setLength('translateY', staticTransformFlags.translateY, arg)) return null
3280
+ } else if (name === 'translatez') {
3281
+ if (!setLength('translateZ', staticTransformFlags.translateZ, arg)) return null
3282
+ } else if (name === 'translate' || name === 'translate3d') {
3283
+ if (args.length < 1) return null
3284
+ if (!setLength('translateX', staticTransformFlags.translateX, args[0])) return null
3285
+ if (args.length > 1 && !setLength('translateY', staticTransformFlags.translateY, args[1])) return null
3286
+ if (args.length > 2 && !setLength('translateZ', staticTransformFlags.translateZ, args[2])) return null
3287
+ } else if (name === 'scale') {
3288
+ const sx = parseStaticScalePermille(args[0] ?? arg)
3289
+ const sy = parseStaticScalePermille(args[1] ?? args[0] ?? arg)
3290
+ if (sx === null || sy === null) return null
3291
+ out.scaleX = sx
3292
+ out.scaleY = sy
3293
+ out.flags |= staticTransformFlags.scaleX | staticTransformFlags.scaleY
3294
+ } else if (name === 'scalex') {
3295
+ const scale = parseStaticScalePermille(arg)
3296
+ if (scale === null) return null
3297
+ out.scaleX = scale
3298
+ out.flags |= staticTransformFlags.scaleX
3299
+ } else if (name === 'scaley') {
3300
+ const scale = parseStaticScalePermille(arg)
3301
+ if (scale === null) return null
3302
+ out.scaleY = scale
3303
+ out.flags |= staticTransformFlags.scaleY
3304
+ } else {
3305
+ return null
3306
+ }
3307
+ }
3308
+ return sawTransform ? out : null
3309
+ }
3310
+
3311
+ function cssStaticTransformValue(property, rawValue) {
3312
+ if (property !== 'transform') return null
3313
+ return parseStaticTransformValue(rawValue)
3314
+ }
3315
+
3316
+ function staticTransformArgs(transform) {
3317
+ return `${transform.flags}, ${transform.rotateX}, ${transform.rotateY}, ${transform.rotateZ}, ${staticLengthSpecCode(transform.translateX)}, ${staticLengthSpecCode(transform.translateY)}, ${staticLengthSpecCode(transform.translateZ)}, ${transform.scaleX}, ${transform.scaleY}`
3318
+ }
3319
+
3320
+ const staticAnimatableLengthTargets = new Set(['Width', 'Height', 'Top', 'Left'])
3321
+ const staticAnimatableColorTargets = new Set(['Color', 'Background'])
3322
+ const staticAnimatableDirectProperties = new Set([
3323
+ 'gea::embedded::ui::Property::Opacity',
3324
+ 'gea::embedded::ui::Property::TransformRotate',
3325
+ 'gea::embedded::ui::Property::TransformScaleX',
3326
+ 'gea::embedded::ui::Property::TransformScaleY',
3327
+ ])
3328
+
3329
+ function cssStaticFilterBlurValue(property, rawValue) {
3330
+ if (property !== 'filter') return null
3331
+ const value = rawValue.trim()
3332
+ const lower = value.toLowerCase()
3333
+ if (!value || lower === 'none') return { setup: [], spec: zeroStaticLengthSpec() }
3334
+ const blur = lower.indexOf('blur(')
3335
+ if (blur < 0) return { setup: [], spec: zeroStaticLengthSpec() }
3336
+ let i = blur + 5
3337
+ let depth = 1
3338
+ const argStart = i
3339
+ while (i < value.length && depth > 0) {
3340
+ if (value[i] === '(') depth++
3341
+ else if (value[i] === ')') depth--
3342
+ i++
3343
+ }
3344
+ if (depth !== 0 || i <= argStart) return null
3345
+ return parseStaticCssLengthSpec(value.slice(argStart, i - 1))
3346
+ }
3347
+
3348
+ function emitStaticCssKeyframeRegistrations(blockName, offset, name, value) {
3349
+ if (!useStaticCssRules) return null
3350
+ if (cssIgnoredProperty(name)) return []
3351
+
3352
+ if (name === 'transform') {
3353
+ const transform = parseStaticTransformValue(value)
3354
+ if (transform) {
3355
+ return [
3356
+ ...transform.setup,
3357
+ `__gea_stylesheet.registerStaticTransformKeyframeRule(${JSON.stringify(blockName)}, ${offset}, ${staticTransformArgs(transform)});`,
3358
+ ]
3359
+ }
3360
+ }
3361
+
3362
+ const directProperties = cssStaticPropertyValues(name, value)
3363
+ if (directProperties.length > 0 && directProperties.every((direct) => staticAnimatableDirectProperties.has(direct.property))) {
3364
+ return directProperties.map((direct) =>
3365
+ `__gea_stylesheet.registerStaticPropertyKeyframeRule(${JSON.stringify(blockName)}, ${offset}, ${direct.property}, ${direct.value});`
3366
+ )
3367
+ }
3368
+
3369
+ const color = cssStaticColorValue(name, value)
3370
+ if (color && staticAnimatableColorTargets.has(color.target)) {
3371
+ return [
3372
+ `__gea_stylesheet.registerStaticColorKeyframeRule(${JSON.stringify(blockName)}, ${offset}, gea::embedded::ui::StaticStyleColorProperty::${color.target}, ${color.color.r}, ${color.color.g}, ${color.color.b}, ${color.color.a});`,
3373
+ ]
3374
+ }
3375
+
3376
+ const colorVar = cssStaticColorVarValue(name, value)
3377
+ if (colorVar && staticAnimatableColorTargets.has(colorVar.target)) {
3378
+ return [
3379
+ `__gea_stylesheet.registerStaticColorVarKeyframeRule(${JSON.stringify(blockName)}, ${offset}, gea::embedded::ui::StaticStyleColorProperty::${colorVar.target}, ${JSON.stringify(colorVar.name)}, ${colorVar.hasFallback ? 'true' : 'false'}, ${colorVar.color.r}, ${colorVar.color.g}, ${colorVar.color.b}, ${colorVar.color.a});`,
3380
+ ]
3381
+ }
3382
+
3383
+ const lengthExpression = cssStaticLengthExpressionValue(name, value)
3384
+ if (lengthExpression && staticAnimatableLengthTargets.has(lengthExpression.target)) {
3385
+ return [
3386
+ ...lengthExpression.setup,
3387
+ `__gea_stylesheet.registerStaticLengthSpecKeyframeRule(${JSON.stringify(blockName)}, ${offset}, gea::embedded::ui::StaticStyleLengthProperty::${lengthExpression.target}, ${staticLengthSpecCode(lengthExpression.spec)});`,
3388
+ ]
3389
+ }
3390
+
3391
+ const length = cssStaticLengthValue(name, value)
3392
+ if (length && staticAnimatableLengthTargets.has(length.target)) {
3393
+ return [
3394
+ `__gea_stylesheet.registerStaticLengthKeyframeRule(${JSON.stringify(blockName)}, ${offset}, gea::embedded::ui::StaticStyleLengthProperty::${length.target}, gea::embedded::ui::StaticStyleLengthUnit::${length.length.unit}, ${length.length.number});`,
3395
+ ]
3396
+ }
3397
+
3398
+ const filterBlur = cssStaticFilterBlurValue(name, value)
3399
+ if (filterBlur) {
3400
+ return [
3401
+ ...filterBlur.setup,
3402
+ `__gea_stylesheet.registerStaticFilterBlurKeyframeRule(${JSON.stringify(blockName)}, ${offset}, ${staticLengthSpecCode(filterBlur.spec)});`,
3403
+ ]
3404
+ }
3405
+
3406
+ return null
3407
+ }
3408
+
3409
+ function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
3410
+ const mediaArg = mediaCondition ? `, ${JSON.stringify(mediaCondition)}` : ''
3411
+ if (useStaticCssRules) {
3412
+ if (cssIgnoredProperty(name)) return []
3413
+ const customLength = cssStaticCustomLengthValue(name, value)
3414
+ if (customLength) {
3415
+ return [
3416
+ ...customLength.setup,
3417
+ `__gea_stylesheet.registerStaticCustomLengthRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${JSON.stringify(customLength.name)}, ${staticLengthSpecCode(customLength.spec)}${mediaArg});`,
3418
+ ]
3419
+ }
3420
+ const customColor = cssStaticCustomColorValue(name, value)
3421
+ if (customColor) {
3422
+ return [
3423
+ `__gea_stylesheet.registerStaticCustomColorRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${JSON.stringify(customColor.name)}, ${customColor.color.r}, ${customColor.color.g}, ${customColor.color.b}, ${customColor.color.a}${mediaArg});`,
3424
+ ]
3425
+ }
3426
+ const colorVar = cssStaticColorVarValue(name, value)
3427
+ if (colorVar) {
3428
+ return [
3429
+ `__gea_stylesheet.registerStaticColorVarRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleColorProperty::${colorVar.target}, ${JSON.stringify(colorVar.name)}, ${colorVar.hasFallback ? 'true' : 'false'}, ${colorVar.color.r}, ${colorVar.color.g}, ${colorVar.color.b}, ${colorVar.color.a}${mediaArg});`,
3430
+ ]
3431
+ }
3432
+ const directProperties = cssStaticPropertyValues(name, value)
3433
+ if (directProperties.length > 0) {
3434
+ if (directProperties.length === 1) {
3435
+ const direct = directProperties[0]
3436
+ return [
3437
+ `__gea_stylesheet.registerStaticPropertyRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${direct.property}, ${direct.value}${mediaArg});`,
3438
+ ]
3439
+ }
3440
+ const entries = directProperties
3441
+ .map((direct) => `{${direct.property}, ${direct.value}}`)
3442
+ .join(', ')
3443
+ return [
3444
+ `__gea_stylesheet.registerStaticPropertyGroupRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, {${entries}}${mediaArg});`,
3445
+ ]
3446
+ }
3447
+ const flex = cssStaticFlexValue(name, value)
3448
+ if (flex) {
3449
+ return [
3450
+ ...flex.setup,
3451
+ `__gea_stylesheet.registerStaticFlexRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${flex.grow}, ${staticLengthSpecCode(flex.basis)}, ${flex.hasBasis ? 'true' : 'false'}${mediaArg});`,
3452
+ ]
3453
+ }
3454
+ const boxShorthand = cssStaticBoxShorthandValue(name, value)
3455
+ if (boxShorthand) {
3456
+ return [
3457
+ ...boxShorthand.setup,
3458
+ ...boxShorthand.entries.map((entry) =>
3459
+ `__gea_stylesheet.registerStaticLengthSpecRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::${entry.target}, ${staticLengthSpecCode(entry.spec)}${mediaArg});`
3460
+ ),
3461
+ ]
3462
+ }
3463
+ const fontFamily = cssStaticFontFamilyValue(name, value)
3464
+ if (fontFamily) {
3465
+ return [
3466
+ `__gea_stylesheet.registerStaticFontFamilyRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${JSON.stringify(fontFamily.family)}${mediaArg});`,
3467
+ ]
3468
+ }
3469
+ const lineHeight = cssStaticLineHeightValue(name, value)
3470
+ if (lineHeight) {
3471
+ return [
3472
+ ...lineHeight.setup,
3473
+ `__gea_stylesheet.registerStaticLineHeightRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLineHeightKind::${lineHeight.kind}, ${staticLengthSpecCode(lineHeight.spec)}${mediaArg});`,
3474
+ ]
3475
+ }
3476
+ const borderShorthand = cssStaticBorderShorthandValue(name, value)
3477
+ if (borderShorthand) {
3478
+ const calls = [
3479
+ ...borderShorthand.setup,
3480
+ `__gea_stylesheet.registerStaticLengthSpecRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::${borderShorthand.widthTarget}, ${staticLengthSpecCode(borderShorthand.width)}${mediaArg});`,
3481
+ ]
3482
+ if (borderShorthand.colorVar) {
3483
+ const colorVar = borderShorthand.colorVar
3484
+ calls.push(`__gea_stylesheet.registerStaticColorVarRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleColorProperty::${colorVar.target}, ${JSON.stringify(colorVar.name)}, ${colorVar.hasFallback ? 'true' : 'false'}, ${colorVar.color.r}, ${colorVar.color.g}, ${colorVar.color.b}, ${colorVar.color.a}${mediaArg});`)
3485
+ } else if (borderShorthand.color) {
3486
+ const color = borderShorthand.color
3487
+ calls.push(`__gea_stylesheet.registerStaticColorRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleColorProperty::${color.target}, ${color.color.r}, ${color.color.g}, ${color.color.b}, ${color.color.a}${mediaArg});`)
3488
+ }
3489
+ return calls
3490
+ }
3491
+ const border = cssStaticBorderValue(name, value)
3492
+ if (border) {
3493
+ return [
3494
+ ...border.setup,
3495
+ `__gea_stylesheet.registerStaticBorderRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLengthSpecCode(border.width)}, ${border.color.r}, ${border.color.g}, ${border.color.b}, ${border.color.a}${mediaArg});`,
3496
+ ]
3497
+ }
3498
+ const borderRadius = cssStaticBorderRadiusValue(name, value)
3499
+ if (borderRadius) {
3500
+ if (borderRadius.corner) {
3501
+ return [
3502
+ ...borderRadius.setup,
3503
+ `__gea_stylesheet.registerStaticBorderRadiusCornerRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleBorderRadiusCorner::${borderRadius.corner}, ${staticLengthSpecCode(borderRadius.spec)}${mediaArg});`,
3504
+ ]
3505
+ }
3506
+ return [
3507
+ ...borderRadius.setup,
3508
+ `__gea_stylesheet.registerStaticBorderRadiusRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${borderRadius.specs.map(staticLengthSpecCode).join(', ')}${mediaArg});`,
3509
+ ]
3510
+ }
3511
+ const filterBlur = cssStaticFilterBlurValue(name, value)
3512
+ if (filterBlur) {
3513
+ return [
3514
+ ...filterBlur.setup,
3515
+ `__gea_stylesheet.registerStaticFilterBlurRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLengthSpecCode(filterBlur.spec)}${mediaArg});`,
3516
+ ]
3517
+ }
3518
+ const fullBackground = cssStaticBackgroundFullValue(name, value)
3519
+ if (fullBackground) {
3520
+ return [
3521
+ ...fullBackground.setup,
3522
+ `__gea_stylesheet.registerStaticBackgroundFullRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientRefCode(fullBackground.gradient)}, ${staticLinearGradientRefCode(fullBackground.overlayGradient)}, ${fullBackground.hasOverlayGradient ? 'true' : 'false'}, ${staticRadialGradientRefCode(fullBackground.radialGradient)}, ${staticGridLineCode(fullBackground.gridX)}, ${staticGridLineCode(fullBackground.gridY)}${mediaArg});`,
3523
+ ]
3524
+ }
3525
+ const background = cssStaticBackgroundValue(name, value)
3526
+ if (background) {
3527
+ return [
3528
+ ...background.setup,
3529
+ `__gea_stylesheet.registerStaticBackgroundRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientCode(background.gradient)}, ${staticLinearGradientCode(background.overlayGradient)}, ${background.hasOverlayGradient ? 'true' : 'false'}, ${staticGridLineCode(background.gridX)}, ${staticGridLineCode(background.gridY)}${mediaArg});`,
3530
+ ]
3531
+ }
3532
+ const backgroundSize = cssStaticBackgroundSizeValue(name, value)
3533
+ if (backgroundSize) {
3534
+ return [
3535
+ ...backgroundSize.setup,
3536
+ `__gea_stylesheet.registerStaticBackgroundSizeRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLengthSpecCode(backgroundSize.stepX)}, ${staticLengthSpecCode(backgroundSize.stepY)}${mediaArg});`,
3537
+ ]
3538
+ }
3539
+ const gridTemplate = cssStaticGridTemplateValue(name, value)
3540
+ if (gridTemplate) {
3541
+ return [
3542
+ ...gridTemplate.setup,
3543
+ `__gea_stylesheet.registerStaticGridTemplateRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleGridTemplateProperty::${gridTemplate.target}, {${gridTemplate.tracks.map(staticGridTemplateTrackCode).join(', ')}}${mediaArg});`,
3544
+ ]
3545
+ }
3546
+ const maskImage = cssStaticMaskImageValue(name, value)
3547
+ if (maskImage) {
3548
+ return [
3549
+ ...maskImage.setup,
3550
+ `__gea_stylesheet.registerStaticLengthSpecRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::MaskImage, ${staticLengthSpecCode(maskImage.spec)}${mediaArg});`,
3551
+ ]
3552
+ }
3553
+ const boxShadowNone = cssStaticBoxShadowNoneValue(name, value)
3554
+ if (boxShadowNone) {
3555
+ return [
3556
+ `__gea_stylesheet.registerStaticBoxShadowNoneRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}${mediaArg});`,
3557
+ ]
3558
+ }
3559
+ const animation = cssStaticAnimationValue(name, value)
3560
+ if (animation) {
3561
+ return [
3562
+ `__gea_stylesheet.registerStaticAnimationRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${JSON.stringify(animation.name)}, ${animation.durationMs}, ${animation.delayMs}, ${animation.iterations}, gea::embedded::ui::StaticStyleAnimationDirection::${animation.direction}, gea::embedded::ui::StaticStyleAnimationFill::${animation.fill}, ${staticAnimationEasingCode(animation.easing)}${mediaArg});`,
3563
+ ]
3564
+ }
3565
+ const color = cssStaticColorValue(name, value)
3566
+ if (color) {
3567
+ return [
3568
+ `__gea_stylesheet.registerStaticColorRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleColorProperty::${color.target}, ${color.color.r}, ${color.color.g}, ${color.color.b}, ${color.color.a}${mediaArg});`,
3569
+ ]
3570
+ }
3571
+ const length = cssStaticLengthValue(name, value)
3572
+ if (length) {
3573
+ return [
3574
+ `__gea_stylesheet.registerStaticLengthRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::${length.target}, gea::embedded::ui::StaticStyleLengthUnit::${length.length.unit}, ${length.length.number}${mediaArg});`,
3575
+ ]
3576
+ }
3577
+ const lengthExpression = cssStaticLengthExpressionValue(name, value)
3578
+ if (lengthExpression) {
3579
+ return [
3580
+ ...lengthExpression.setup,
3581
+ `__gea_stylesheet.registerStaticLengthSpecRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::${lengthExpression.target}, ${staticLengthSpecCode(lengthExpression.spec)}${mediaArg});`,
3582
+ ]
3583
+ }
3584
+ const origin = cssStaticOriginValue(name, value)
3585
+ if (origin) {
3586
+ return [
3587
+ `__gea_stylesheet.registerStaticOriginRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleOriginProperty::${origin.target}, ${origin.x}, ${origin.y}${mediaArg});`,
3588
+ ]
3589
+ }
3590
+ const transform = cssStaticTransformValue(name, value)
3591
+ if (transform) {
3592
+ return [
3593
+ ...transform.setup,
3594
+ `__gea_stylesheet.registerStaticTransformRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticTransformArgs(transform)}${mediaArg});`,
3595
+ ]
3596
+ }
3597
+ }
3598
+ return [
3599
+ `__gea_stylesheet.${cssRegisterCall(kind)}(${JSON.stringify(selector)}, ${JSON.stringify(name)}, ${JSON.stringify(value)}${mediaArg});`,
3600
+ ]
3601
+ }
3602
+
3603
+ function emitCssRules(cssText, mediaCondition, calls) {
3604
+ calls.push(...staticMediaConditionPlanRegistration(mediaCondition))
3605
+ const ruleRegex = /([^{}]+)\{([^{}]*)\}/g
3606
+ let ruleMatch
3607
+ while ((ruleMatch = ruleRegex.exec(cssText)) !== null) {
3608
+ const selectors = ruleMatch[1].split(',').map((selector) => selector.trim()).filter(Boolean)
3609
+ // Simple selectors keep their compact legacy registration path.
3610
+ // Everything richer (e.g. :root, .a.b, descendants, child selectors,
3611
+ // first/last-child, and ::before/::after) goes through the runtime
3612
+ // selector matcher.
3613
+ const classNames = []
3614
+ const activeClassNames = []
3615
+ const elementNames = []
3616
+ const selectorRules = []
3617
+ for (const selector of selectors) {
3618
+ const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1]
3619
+ if (className) {
3620
+ classNames.push(className)
3621
+ continue
3622
+ }
3623
+ const activeClass = selector.match(/^\.([A-Za-z0-9_-]+):active$/)?.[1]
3624
+ if (activeClass) {
3625
+ activeClassNames.push(activeClass)
3626
+ continue
3627
+ }
3628
+ const elementName = selector.match(/^([A-Za-z][A-Za-z0-9-]*)$/)?.[1]
3629
+ if (elementName) {
3630
+ elementNames.push(elementName.toLowerCase())
3631
+ continue
3632
+ }
3633
+ selectorRules.push(selector)
3634
+ }
3635
+ if (classNames.length === 0 && activeClassNames.length === 0 && elementNames.length === 0 && selectorRules.length === 0) continue
3636
+ for (const selector of selectorRules) {
3637
+ calls.push(...staticSelectorPlanRegistration(selector))
3638
+ }
3639
+ const declarations = ruleMatch[2]
3640
+ .split(';')
3641
+ .map((decl) => decl.trim())
3642
+ .filter(Boolean)
3643
+ for (const declaration of declarations) {
3644
+ const colon = declaration.indexOf(':')
3645
+ if (colon <= 0) continue
3646
+ const name = declaration.slice(0, colon).trim()
3647
+ const value = declaration.slice(colon + 1).trim()
3648
+ if (!name || !value) continue
3649
+ for (const className of classNames) {
3650
+ calls.push(...emitCssRuleRegistrations('class', className, name, value, mediaCondition))
3651
+ }
3652
+ for (const className of activeClassNames) {
3653
+ // `background-color` in a :active rule becomes
3654
+ // `active-background-color`, which the framework's CSS parser
3655
+ // routes to Property::ActiveBackgroundColor (and auto-sets
3656
+ // HasActiveBackground). Other properties (color, opacity) get
3657
+ // the same `active-` prefix so they only swap in during a tap.
3658
+ const activeName = name.startsWith('active-') ? name : `active-${name}`
3659
+ calls.push(...emitCssRuleRegistrations('class', className, activeName, value, mediaCondition))
3660
+ }
3661
+ for (const elementName of elementNames) {
3662
+ calls.push(...emitCssRuleRegistrations('element', elementName, name, value, mediaCondition))
3663
+ }
3664
+ for (const selector of selectorRules) {
3665
+ calls.push(...emitCssRuleRegistrations('selector', selector, name, value, mediaCondition))
3666
+ }
3667
+ }
3668
+ }
3669
+ }
3670
+
3671
+ // Split CSS into its top-level rules and its @media blocks. The rule regex
3672
+ // can't handle the nested braces inside @media, so we brace-match each block
3673
+ // out first and hand its body to emitCssRules tagged with the raw condition.
3674
+ function extractMediaBlocks(css) {
3675
+ const media = []
3676
+ let base = ''
3677
+ let i = 0
3678
+ while (i < css.length) {
3679
+ const at = css.indexOf('@media', i)
3680
+ if (at === -1) {
3681
+ base += css.slice(i)
3682
+ break
3683
+ }
3684
+ base += css.slice(i, at)
3685
+ const braceOpen = css.indexOf('{', at)
3686
+ if (braceOpen === -1) {
3687
+ base += css.slice(at)
3688
+ break
3689
+ }
3690
+ const condition = css.slice(at + 6, braceOpen).replace(/\s+/g, ' ').trim()
3691
+ let depth = 1
3692
+ let j = braceOpen + 1
3693
+ for (; j < css.length && depth > 0; j++) {
3694
+ if (css[j] === '{') depth++
3695
+ else if (css[j] === '}') depth--
3696
+ }
3697
+ // j points just past the matched closing brace; body excludes it.
3698
+ media.push({ condition, body: css.slice(braceOpen + 1, depth === 0 ? j - 1 : j) })
3699
+ i = j
3700
+ }
3701
+ return { base, media }
3702
+ }
3703
+
3704
+ function extractKeyframeBlocks(css) {
3705
+ const keyframes = []
3706
+ let base = ''
3707
+ let i = 0
3708
+ while (i < css.length) {
3709
+ const at = css.indexOf('@keyframes', i)
3710
+ if (at === -1) {
3711
+ base += css.slice(i)
3712
+ break
3713
+ }
3714
+ base += css.slice(i, at)
3715
+ const braceOpen = css.indexOf('{', at)
3716
+ if (braceOpen === -1) {
3717
+ base += css.slice(at)
3718
+ break
3719
+ }
3720
+ const name = css.slice(at + '@keyframes'.length, braceOpen).trim()
3721
+ let depth = 1
3722
+ let j = braceOpen + 1
3723
+ for (; j < css.length && depth > 0; j++) {
3724
+ if (css[j] === '{') depth++
3725
+ else if (css[j] === '}') depth--
3726
+ }
3727
+ keyframes.push({ name, body: css.slice(braceOpen + 1, depth === 0 ? j - 1 : j) })
3728
+ i = j
3729
+ }
3730
+ return { base, keyframes }
3731
+ }
3732
+
3733
+ function keyframeOffsetPermille(selector) {
3734
+ const trimmed = selector.trim()
3735
+ if (trimmed === 'from') return 0
3736
+ if (trimmed === 'to') return 1000
3737
+ const percent = trimmed.match(/^(-?\d+(?:\.\d+)?)%$/)
3738
+ if (!percent) return null
3739
+ const value = Math.round(Number(percent[1]) * 10)
3740
+ return Math.max(0, Math.min(1000, value))
3741
+ }
3742
+
3743
+ function emitCssKeyframes(blocks, calls) {
3744
+ for (const block of blocks) {
3745
+ if (!block.name) continue
3746
+ const frameRegex = /([^{}]+)\{([^{}]*)\}/g
3747
+ let frameMatch
3748
+ while ((frameMatch = frameRegex.exec(block.body)) !== null) {
3749
+ const offsets = splitTopLevelCss(frameMatch[1], ',')
3750
+ .map(keyframeOffsetPermille)
3751
+ .filter(offset => offset !== null)
3752
+ if (offsets.length === 0) continue
3753
+ const declarations = frameMatch[2]
3754
+ .split(';')
3755
+ .map(decl => decl.trim())
3756
+ .filter(Boolean)
3757
+ for (const declaration of declarations) {
3758
+ const colon = declaration.indexOf(':')
3759
+ if (colon <= 0) continue
3760
+ const name = declaration.slice(0, colon).trim()
3761
+ const value = declaration.slice(colon + 1).trim()
3762
+ if (!name || !value) continue
3763
+ for (const offset of offsets) {
3764
+ const staticCalls = emitStaticCssKeyframeRegistrations(block.name, offset, name, value)
3765
+ if (staticCalls) {
3766
+ calls.push(...staticCalls)
3767
+ continue
3768
+ }
3769
+ calls.push(`__gea_stylesheet.${cssRegisterCall('keyframe')}(${JSON.stringify(block.name)}, ${offset}, ${JSON.stringify(name)}, ${JSON.stringify(value)});`)
3770
+ }
3771
+ }
3772
+ }
3773
+ }
3774
+ }
3775
+
3776
+ function splitTopLevelCppArgs(value) {
3777
+ const out = []
3778
+ let start = 0
3779
+ let paren = 0
3780
+ let brace = 0
3781
+ let inString = false
3782
+ let escaped = false
3783
+ for (let i = 0; i < value.length; i++) {
3784
+ const char = value[i]
3785
+ if (inString) {
3786
+ if (escaped) escaped = false
3787
+ else if (char === '\\') escaped = true
3788
+ else if (char === '"') inString = false
3789
+ continue
3790
+ }
3791
+ if (char === '"') {
3792
+ inString = true
3793
+ continue
3794
+ }
3795
+ if (char === '(') paren++
3796
+ else if (char === ')' && paren > 0) paren--
3797
+ else if (char === '{') brace++
3798
+ else if (char === '}' && brace > 0) brace--
3799
+ else if (char === ',' && paren === 0 && brace === 0) {
3800
+ out.push(value.slice(start, i).trim())
3801
+ start = i + 1
3802
+ }
3803
+ }
3804
+ out.push(value.slice(start).trim())
3805
+ return out.filter(Boolean)
3806
+ }
3807
+
3808
+ function staticCssTapeLengthSpecParts(code) {
3809
+ const trimmed = code.trim()
3810
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return null
3811
+ const parts = splitTopLevelCppArgs(trimmed.slice(1, -1))
3812
+ if (parts.length !== 2) return null
3813
+ return { unit: parts[0], value: parts[1] }
3814
+ }
3815
+
3816
+ function staticCssTapeEntry(call, stringIndex) {
3817
+ if (!useStaticCssTape || !useStaticCssRules) return null
3818
+ const match = call.match(/^__gea_stylesheet\.(registerStatic[A-Za-z0-9_]+)\(([\s\S]*)\);$/)
3819
+ if (!match) return null
3820
+ const method = match[1]
3821
+ const args = splitTopLevelCppArgs(match[2])
3822
+ const media = (index) => args[index] ?? 'nullptr'
3823
+ const selectorKind = (value) => `static_cast<std::uint8_t>(${value})`
3824
+ const enumSlot = (value) => `static_cast<std::uint16_t>(${value})`
3825
+ const lengthUnit = (value) => `static_cast<std::uint8_t>(${value})`
3826
+ const entry = (kind, code) => ({ kind, code, call })
3827
+
3828
+ switch (method) {
3829
+ case 'registerStaticPropertyRule':
3830
+ if (args.length < 4 || args.length > 5) return null
3831
+ return entry('Property', `{${args[3]}, ${stringIndex(args[1])}, ${stringIndex(media(4))}, ${enumSlot(args[2])}, ${selectorKind(args[0])}}`)
3832
+ case 'registerStaticLengthRule':
3833
+ if (args.length < 5 || args.length > 6) return null
3834
+ return entry('Length', `{${args[4]}, ${stringIndex(args[1])}, ${stringIndex(media(5))}, ${enumSlot(args[2])}, ${selectorKind(args[0])}, ${lengthUnit(args[3])}}`)
3835
+ case 'registerStaticLengthSpecRule':
3836
+ if (args.length < 4 || args.length > 5) return null
3837
+ {
3838
+ const length = staticCssTapeLengthSpecParts(args[3])
3839
+ if (!length) return null
3840
+ return entry('LengthSpec', `{${length.value}, ${stringIndex(args[1])}, ${stringIndex(media(4))}, ${enumSlot(args[2])}, ${selectorKind(args[0])}, ${lengthUnit(length.unit)}}`)
3841
+ }
3842
+ case 'registerStaticColorRule':
3843
+ if (args.length < 7 || args.length > 8) return null
3844
+ return entry('Color', `{${stringIndex(args[1])}, ${stringIndex(media(7))}, ${enumSlot(args[2])}, ${selectorKind(args[0])}, ${args[3]}, ${args[4]}, ${args[5]}, ${args[6]}}`)
3845
+ case 'registerStaticColorVarRule':
3846
+ if (args.length < 9 || args.length > 10) return null
3847
+ return entry('ColorVar', `{${stringIndex(args[1])}, ${stringIndex(media(9))}, ${stringIndex(args[3])}, ${enumSlot(args[2])}, ${selectorKind(args[0])}, ${args[4]}, ${args[5]}, ${args[6]}, ${args[7]}, ${args[8]}}`)
3848
+ case 'registerStaticCustomLengthRule':
3849
+ if (args.length < 4 || args.length > 5) return null
3850
+ {
3851
+ const length = staticCssTapeLengthSpecParts(args[3])
3852
+ if (!length) return null
3853
+ return entry('CustomLength', `{${length.value}, ${stringIndex(args[1])}, ${stringIndex(media(4))}, ${stringIndex(args[2])}, ${selectorKind(args[0])}, ${lengthUnit(length.unit)}}`)
3854
+ }
3855
+ case 'registerStaticCustomColorRule':
3856
+ if (args.length < 7 || args.length > 8) return null
3857
+ return entry('CustomColor', `{${stringIndex(args[1])}, ${stringIndex(media(7))}, ${stringIndex(args[2])}, ${selectorKind(args[0])}, ${args[3]}, ${args[4]}, ${args[5]}, ${args[6]}}`)
3858
+ case 'registerStaticBoxShadowNoneRule':
3859
+ if (args.length < 2 || args.length > 3) return null
3860
+ return entry('BoxShadowNone', `{${stringIndex(args[1])}, ${stringIndex(media(2))}, ${selectorKind(args[0])}}`)
3861
+ case 'registerStaticFontFamilyRule':
3862
+ if (args.length < 3 || args.length > 4) return null
3863
+ return entry('FontFamily', `{${stringIndex(args[1])}, ${stringIndex(media(3))}, ${stringIndex(args[2])}, ${selectorKind(args[0])}}`)
3864
+ case 'registerStaticFilterBlurRule':
3865
+ if (args.length < 3 || args.length > 4) return null
3866
+ {
3867
+ const length = staticCssTapeLengthSpecParts(args[2])
3868
+ if (!length) return null
3869
+ return entry('FilterBlur', `{${length.value}, ${stringIndex(args[1])}, ${stringIndex(media(3))}, ${selectorKind(args[0])}, ${lengthUnit(length.unit)}}`)
3870
+ }
3871
+ case 'registerStaticLineHeightRule':
3872
+ if (args.length < 4 || args.length > 5) return null
3873
+ {
3874
+ const length = staticCssTapeLengthSpecParts(args[3])
3875
+ if (!length) return null
3876
+ return entry('LineHeight', `{${length.value}, ${stringIndex(args[1])}, ${stringIndex(media(4))}, ${enumSlot(args[2])}, ${selectorKind(args[0])}, ${lengthUnit(length.unit)}}`)
3877
+ }
3878
+ default:
3879
+ return null
3880
+ }
3881
+ }
3882
+
3883
+ function staticCssTapePrelude(stringLiterals) {
3884
+ const strings = stringLiterals.map((value) => `\t${value}`).join(',\n')
3885
+ return `
3886
+ const char * const __gea_static_css_tape_strings[] = {
3887
+ ${strings}
3888
+ };
3889
+ enum class __GeaStaticCssTapeKind : std::uint8_t {
3890
+ \tProperty,
3891
+ \tLength,
3892
+ \tLengthSpec,
3893
+ \tColor,
3894
+ \tColorVar,
3895
+ \tCustomLength,
3896
+ \tCustomColor,
3897
+ \tBoxShadowNone,
3898
+ \tFontFamily,
3899
+ \tFilterBlur,
3900
+ \tLineHeight
3901
+ };
3902
+ struct __GeaStaticCssPropertyOp {
3903
+ \tint value;
3904
+ \tstd::uint16_t selector;
3905
+ \tstd::uint16_t media;
3906
+ \tstd::uint16_t property;
3907
+ \tstd::uint8_t selectorKind;
3908
+ };
3909
+ struct __GeaStaticCssLengthOp {
3910
+ \tfloat value;
3911
+ \tstd::uint16_t selector;
3912
+ \tstd::uint16_t media;
3913
+ \tstd::uint16_t property;
3914
+ \tstd::uint8_t selectorKind;
3915
+ \tstd::uint8_t unit;
3916
+ };
3917
+ struct __GeaStaticCssColorOp {
3918
+ \tstd::uint16_t selector;
3919
+ \tstd::uint16_t media;
3920
+ \tstd::uint16_t property;
3921
+ \tstd::uint8_t selectorKind;
3922
+ \tstd::uint8_t r;
3923
+ \tstd::uint8_t g;
3924
+ \tstd::uint8_t b;
3925
+ \tstd::uint8_t a;
3926
+ };
3927
+ struct __GeaStaticCssColorVarOp {
3928
+ \tstd::uint16_t selector;
3929
+ \tstd::uint16_t media;
3930
+ \tstd::uint16_t name;
3931
+ \tstd::uint16_t property;
3932
+ \tstd::uint8_t selectorKind;
3933
+ \tbool hasFallback;
3934
+ \tstd::uint8_t r;
3935
+ \tstd::uint8_t g;
3936
+ \tstd::uint8_t b;
3937
+ \tstd::uint8_t a;
3938
+ };
3939
+ struct __GeaStaticCssCustomLengthOp {
3940
+ \tfloat value;
3941
+ \tstd::uint16_t selector;
3942
+ \tstd::uint16_t media;
3943
+ \tstd::uint16_t name;
3944
+ \tstd::uint8_t selectorKind;
3945
+ \tstd::uint8_t unit;
3946
+ };
3947
+ struct __GeaStaticCssCustomColorOp {
3948
+ \tstd::uint16_t selector;
3949
+ \tstd::uint16_t media;
3950
+ \tstd::uint16_t name;
3951
+ \tstd::uint8_t selectorKind;
3952
+ \tstd::uint8_t r;
3953
+ \tstd::uint8_t g;
3954
+ \tstd::uint8_t b;
3955
+ \tstd::uint8_t a;
3956
+ };
3957
+ struct __GeaStaticCssSelectorOnlyOp {
3958
+ \tstd::uint16_t selector;
3959
+ \tstd::uint16_t media;
3960
+ \tstd::uint8_t selectorKind;
3961
+ };
3962
+ struct __GeaStaticCssNamedOp {
3963
+ \tstd::uint16_t selector;
3964
+ \tstd::uint16_t media;
3965
+ \tstd::uint16_t name;
3966
+ \tstd::uint8_t selectorKind;
3967
+ };
3968
+ struct __GeaStaticCssLengthOnlyOp {
3969
+ \tfloat value;
3970
+ \tstd::uint16_t selector;
3971
+ \tstd::uint16_t media;
3972
+ \tstd::uint8_t selectorKind;
3973
+ \tstd::uint8_t unit;
3974
+ };
3975
+ struct __GeaStaticCssLineHeightOp {
3976
+ \tfloat value;
3977
+ \tstd::uint16_t selector;
3978
+ \tstd::uint16_t media;
3979
+ \tstd::uint16_t kind;
3980
+ \tstd::uint8_t selectorKind;
3981
+ \tstd::uint8_t unit;
3982
+ };
3983
+ auto __gea_css_tape_string = [&](std::uint16_t index) -> const char * {
3984
+ \treturn __gea_static_css_tape_strings[index];
3985
+ };
3986
+ auto __gea_css_tape_selector_kind = [](std::uint8_t kind) {
3987
+ \treturn static_cast<gea::embedded::ui::StaticStyleSelectorKind>(kind);
3988
+ };
3989
+ auto __gea_css_tape_length = [](std::uint8_t unit, float value) {
3990
+ \treturn gea::embedded::ui::StaticStyleLengthSpec{static_cast<gea::embedded::ui::StaticStyleLengthUnit>(unit), value};
3991
+ };
3992
+ auto __gea_register_static_css_tape = [&](
3993
+ \tstd::initializer_list<__GeaStaticCssTapeKind> order,
3994
+ \tstd::initializer_list<__GeaStaticCssPropertyOp> propertyOps,
3995
+ \tstd::initializer_list<__GeaStaticCssLengthOp> lengthOps,
3996
+ \tstd::initializer_list<__GeaStaticCssLengthOp> lengthSpecOps,
3997
+ \tstd::initializer_list<__GeaStaticCssColorOp> colorOps,
3998
+ \tstd::initializer_list<__GeaStaticCssColorVarOp> colorVarOps,
3999
+ \tstd::initializer_list<__GeaStaticCssCustomLengthOp> customLengthOps,
4000
+ \tstd::initializer_list<__GeaStaticCssCustomColorOp> customColorOps,
4001
+ \tstd::initializer_list<__GeaStaticCssSelectorOnlyOp> boxShadowNoneOps,
4002
+ \tstd::initializer_list<__GeaStaticCssNamedOp> fontFamilyOps,
4003
+ \tstd::initializer_list<__GeaStaticCssLengthOnlyOp> filterBlurOps,
4004
+ \tstd::initializer_list<__GeaStaticCssLineHeightOp> lineHeightOps) {
4005
+ \tauto propertyIt = propertyOps.begin();
4006
+ \tauto lengthIt = lengthOps.begin();
4007
+ \tauto lengthSpecIt = lengthSpecOps.begin();
4008
+ \tauto colorIt = colorOps.begin();
4009
+ \tauto colorVarIt = colorVarOps.begin();
4010
+ \tauto customLengthIt = customLengthOps.begin();
4011
+ \tauto customColorIt = customColorOps.begin();
4012
+ \tauto boxShadowNoneIt = boxShadowNoneOps.begin();
4013
+ \tauto fontFamilyIt = fontFamilyOps.begin();
4014
+ \tauto filterBlurIt = filterBlurOps.begin();
4015
+ \tauto lineHeightIt = lineHeightOps.begin();
4016
+ \tfor (const auto kind : order) {
4017
+ \t\tswitch (kind) {
4018
+ \t\tcase __GeaStaticCssTapeKind::Property: {
4019
+ \t\t\tconst auto &op = *propertyIt++;
4020
+ \t\t\t__gea_stylesheet.registerStaticPropertyRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::Property>(op.property), op.value, __gea_css_tape_string(op.media));
4021
+ \t\t\tbreak;
4022
+ \t\t}
4023
+ \t\tcase __GeaStaticCssTapeKind::Length: {
4024
+ \t\t\tconst auto &op = *lengthIt++;
4025
+ \t\t\t__gea_stylesheet.registerStaticLengthRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::StaticStyleLengthProperty>(op.property), static_cast<gea::embedded::ui::StaticStyleLengthUnit>(op.unit), op.value, __gea_css_tape_string(op.media));
4026
+ \t\t\tbreak;
4027
+ \t\t}
4028
+ \t\tcase __GeaStaticCssTapeKind::LengthSpec: {
4029
+ \t\t\tconst auto &op = *lengthSpecIt++;
4030
+ \t\t\t__gea_stylesheet.registerStaticLengthSpecRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::StaticStyleLengthProperty>(op.property), __gea_css_tape_length(op.unit, op.value), __gea_css_tape_string(op.media));
4031
+ \t\t\tbreak;
4032
+ \t\t}
4033
+ \t\tcase __GeaStaticCssTapeKind::Color: {
4034
+ \t\t\tconst auto &op = *colorIt++;
4035
+ \t\t\t__gea_stylesheet.registerStaticColorRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::StaticStyleColorProperty>(op.property), op.r, op.g, op.b, op.a, __gea_css_tape_string(op.media));
4036
+ \t\t\tbreak;
4037
+ \t\t}
4038
+ \t\tcase __GeaStaticCssTapeKind::ColorVar: {
4039
+ \t\t\tconst auto &op = *colorVarIt++;
4040
+ \t\t\t__gea_stylesheet.registerStaticColorVarRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::StaticStyleColorProperty>(op.property), __gea_css_tape_string(op.name), op.hasFallback, op.r, op.g, op.b, op.a, __gea_css_tape_string(op.media));
4041
+ \t\t\tbreak;
4042
+ \t\t}
4043
+ \t\tcase __GeaStaticCssTapeKind::CustomLength: {
4044
+ \t\t\tconst auto &op = *customLengthIt++;
4045
+ \t\t\t__gea_stylesheet.registerStaticCustomLengthRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), __gea_css_tape_string(op.name), __gea_css_tape_length(op.unit, op.value), __gea_css_tape_string(op.media));
4046
+ \t\t\tbreak;
4047
+ \t\t}
4048
+ \t\tcase __GeaStaticCssTapeKind::CustomColor: {
4049
+ \t\t\tconst auto &op = *customColorIt++;
4050
+ \t\t\t__gea_stylesheet.registerStaticCustomColorRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), __gea_css_tape_string(op.name), op.r, op.g, op.b, op.a, __gea_css_tape_string(op.media));
4051
+ \t\t\tbreak;
4052
+ \t\t}
4053
+ \t\tcase __GeaStaticCssTapeKind::BoxShadowNone: {
4054
+ \t\t\tconst auto &op = *boxShadowNoneIt++;
4055
+ \t\t\t__gea_stylesheet.registerStaticBoxShadowNoneRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), __gea_css_tape_string(op.media));
4056
+ \t\t\tbreak;
4057
+ \t\t}
4058
+ \t\tcase __GeaStaticCssTapeKind::FontFamily: {
4059
+ \t\t\tconst auto &op = *fontFamilyIt++;
4060
+ \t\t\t__gea_stylesheet.registerStaticFontFamilyRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), __gea_css_tape_string(op.name), __gea_css_tape_string(op.media));
4061
+ \t\t\tbreak;
4062
+ \t\t}
4063
+ \t\tcase __GeaStaticCssTapeKind::FilterBlur: {
4064
+ \t\t\tconst auto &op = *filterBlurIt++;
4065
+ \t\t\t__gea_stylesheet.registerStaticFilterBlurRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), __gea_css_tape_length(op.unit, op.value), __gea_css_tape_string(op.media));
4066
+ \t\t\tbreak;
4067
+ \t\t}
4068
+ \t\tcase __GeaStaticCssTapeKind::LineHeight: {
4069
+ \t\t\tconst auto &op = *lineHeightIt++;
4070
+ \t\t\t__gea_stylesheet.registerStaticLineHeightRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::StaticStyleLineHeightKind>(op.kind), __gea_css_tape_length(op.unit, op.value), __gea_css_tape_string(op.media));
4071
+ \t\t\tbreak;
4072
+ \t\t}
4073
+ \t\t}
4074
+ \t}
4075
+ };
4076
+ `
4077
+ }
4078
+
4079
+ function compactStaticCssTapeCalls(calls) {
4080
+ if (!useStaticCssTape || !useStaticCssRules) return { calls, usedTape: false }
4081
+ const kinds = [
4082
+ 'Property',
4083
+ 'Length',
4084
+ 'LengthSpec',
4085
+ 'Color',
4086
+ 'ColorVar',
4087
+ 'CustomLength',
4088
+ 'CustomColor',
4089
+ 'BoxShadowNone',
4090
+ 'FontFamily',
4091
+ 'FilterBlur',
4092
+ 'LineHeight',
4093
+ ]
4094
+ const out = []
4095
+ const stringLiterals = ['nullptr']
4096
+ const stringIds = new Map([['nullptr', 0]])
4097
+ const stringIndex = (value) => {
4098
+ const literal = value === 'nullptr' ? 'nullptr' : value
4099
+ const cached = stringIds.get(literal)
4100
+ if (cached !== undefined) return cached
4101
+ const id = stringLiterals.length
4102
+ stringIds.set(literal, id)
4103
+ stringLiterals.push(literal)
4104
+ return id
4105
+ }
4106
+ let chunk = []
4107
+ let usedTape = false
4108
+ const flush = () => {
4109
+ if (chunk.length === 0) return
4110
+ if (chunk.length < staticCssTapeMinChunk) {
4111
+ out.push(...chunk.map((entry) => entry.call))
4112
+ chunk = []
4113
+ return
4114
+ }
4115
+ usedTape = true
4116
+ const order = chunk.map((entry) => `__GeaStaticCssTapeKind::${entry.kind}`).join(', ')
4117
+ const listFor = (kind) => {
4118
+ const entries = chunk.filter((entry) => entry.kind === kind).map((entry) => entry.code)
4119
+ if (entries.length === 0) return '{}'
4120
+ return `{\n${entries.map((entry) => `\t\t${entry}`).join(',\n')}\n\t}`
4121
+ }
4122
+ out.push(`__gea_register_static_css_tape(\n\t{${order}},\n\t${kinds.map(listFor).join(',\n\t')});`)
4123
+ chunk = []
4124
+ }
4125
+ for (const call of calls) {
4126
+ const entry = staticCssTapeEntry(call, stringIndex)
4127
+ if (entry) {
4128
+ chunk.push(entry)
4129
+ continue
4130
+ }
4131
+ flush()
4132
+ out.push(call)
4133
+ }
4134
+ flush()
4135
+ return { calls: out, usedTape, stringLiterals }
4136
+ }
4137
+
4138
+ function cssRegistrationCode(viteOutDir) {
4139
+ const calls = []
4140
+ staticCssLengthExpressionCounter = 0
4141
+ staticCssLengthExpressionCache = new Map()
4142
+ staticCssSelectorPlanCache = new Set()
4143
+ staticCssMediaConditionPlanCache = new Set()
4144
+ for (const file of findCssFiles(viteOutDir)) {
4145
+ const css = stripCssComments(fs.readFileSync(file, 'utf8'))
4146
+ const withoutKeyframes = extractKeyframeBlocks(css)
4147
+ emitCssKeyframes(withoutKeyframes.keyframes, calls)
4148
+ const { base, media } = extractMediaBlocks(withoutKeyframes.base)
4149
+ // Base rules first, then @media rules: StyleSheet applies in registration
4150
+ // order (last writer wins), so a matching breakpoint overrides the base.
4151
+ emitCssRules(stripAtBlocks(base), '', calls)
4152
+ for (const block of media) {
4153
+ emitCssRules(stripAtBlocks(block.body), block.condition, calls)
4154
+ }
4155
+ }
4156
+ if (calls.length === 0) return ''
4157
+ const compacted = compactStaticCssTapeCalls(calls)
4158
+ const tapePrelude = compacted.usedTape ? staticCssTapePrelude(compacted.stringLiterals) : ''
4159
+ return `{\nauto &__gea_stylesheet = gea::embedded::ui::StyleSheet::instance();\n${tapePrelude}__gea_stylesheet.beginRuleRegistrationBatch();\n${compacted.calls.join('\n')}\n__gea_stylesheet.endRuleRegistrationBatch();\n}\n`
4160
+ }
4161
+
4162
+ const appDir = path.resolve(readOption('--app-dir') ?? fail('missing --app-dir <dir>'))
4163
+ const outDir = path.resolve(readOption('--out-dir') ?? fail('missing --out-dir <dir>'))
4164
+ acquireOutputPipelineLock(outDir)
4165
+ // Which geatsc to compile with. `--geatsc-bin` wins; `GEA_GEATSC_BIN` is the
4166
+ // same choice expressed as an environment variable, for callers that cannot
4167
+ // add a flag -- a test harness that shells out to several runners, say. It is
4168
+ // the variable `targets/web/build-web.sh` already forwards as `--geatsc-bin`,
4169
+ // so this only lets it reach here directly. Unset, everything resolves exactly
4170
+ // as before: the installed `@geastack/compiler`'s own `dist/cli.js`.
4171
+ //
4172
+ // The point is being able to FREEZE the compiler for the length of a
4173
+ // measurement run. `dist/` is shared and can be rebuilt by anyone at any
4174
+ // moment; a run whose rows were compiled by different compilers is not one
4175
+ // compass, and mtimes alone cannot tell a real change from a no-op rebuild.
4176
+ const geatscBin = path.resolve(readOption('--geatsc-bin') ?? process.env.GEA_GEATSC_BIN ?? path.join(path.dirname(requireFromLib.resolve('@geastack/compiler/package.json')), 'dist/cli.js'))
4177
+ const viteBin = path.resolve(readOption('--vite-bin') ?? path.join(path.dirname(requireFromLib.resolve('vite/package.json')), JSON.parse(fs.readFileSync(requireFromLib.resolve('vite/package.json'),'utf8')).bin.vite))
4178
+ const viteOutDir = path.resolve(readOption('--vite-out-dir') ?? path.join(outDir, 'dist'))
4179
+ const bundlePath = path.resolve(readOption('--bundle-out') ?? path.join(outDir, 'gea-vite-bundle.js'))
4180
+ const geaIrPath = path.resolve(readOption('--gea-ir-out') ?? path.join(viteOutDir, 'gea-ir.json'))
4181
+ const moduleGraphOutOption = readOption('--module-graph-out') ?? process.env.GEA_VITE_MODULE_GRAPH_OUT
4182
+ const moduleGraphOnly = hasFlag('--module-graph-only')
4183
+ const moduleGraphCompileRequested = hasFlag('--compile-module-graph')
4184
+ const moduleGraphCompileDisabled = hasFlag('--no-compile-module-graph')
4185
+ const geaPluginSpecifier = path.resolve(readOption('--geatsc-gea-plugin') ?? requireFromLib.resolve('@geastack/geatsc-plugin-gea'))
4186
+ const extraGeatscPlugins = readAllOptions('--extra-geatsc-plugin').map((plugin) => path.resolve(plugin))
4187
+ const appendJsFiles = readAllOptions('--append-js').map((file) => path.resolve(file))
4188
+ const entry = readOption('--entry') ?? 'index.tsx'
4189
+ const entrySymbol = readOption('--entry-symbol')
4190
+ const cppBoard = readOption('--cpp-board')
4191
+ const cxxStandard = readOption('--cxx-standard')
4192
+ const pixelPanelEndian = readOption('--pixel-panel-endian')
4193
+ const cppPreludeSymbol = readOption('--cpp-prelude-symbol')
4194
+ const fontSymbolPrefix = readOption('--font-symbol-prefix')
4195
+ const fontViewportWidths = readAllOptions('--font-viewport-width')
4196
+ const fontViewportHeights = readAllOptions('--font-viewport-height')
4197
+ const fontDevicePixelRatios = readAllOptions('--font-device-pixel-ratio')
4198
+ const runtimeTtfFonts = hasFlag('--runtime-ttf-fonts')
4199
+
4200
+ // Physical panel resolution per board, mirroring each target's
4201
+ // GEA_EMBEDDED_DISPLAY_WIDTH/HEIGHT (the lib/gea-embedded/include/display.h
4202
+ // default is 410x502; boards override via target_compile_definitions). The
4203
+ // font generator pre-rasterizes viewport-relative font sizes (vw/vh/vmin/vmax)
4204
+ // and needs the real viewport to bake glyphs at the exact pixel size the
4205
+ // runtime's parseLengthForNode will request. Resolved from --cpp-board so the
4206
+ // board CMakeLists need not thread dimensions through; an explicit
4207
+ // --font-viewport-{width,height} still wins for non-board callers.
4208
+ const BOARD_VIEWPORTS = {
4209
+ 'esp32-s3-epaper-1.54': { width: 200, height: 200 },
4210
+ 'esp32-m5stack-m5paper': { width: 540, height: 960 },
4211
+ 'esp32-s3-lilygo-t5-epaper-4.7': { width: 540, height: 960 },
4212
+ 'esp32-s3-m5stack-papers3': { width: 540, height: 960 },
4213
+ 'esp32-s3-m5stack-sticks3': { width: 135, height: 240 },
4214
+ 'esp32-s3-elecrow-rotary-2.1': { width: 480, height: 480 },
4215
+ 'esp32-s3-touch-amoled-2.06': { width: 410, height: 502 },
4216
+ 'esp32-s3-touch-amoled-1.8': { width: 368, height: 448 },
4217
+ 'esp32-s3-lilygo-t-display-s3-long': { width: 180, height: 640 },
4218
+ 'esp32-p4-waveshare-touch-lcd-7': { width: 720, height: 1280 },
4219
+ 'esp32-p4-waveshare-touch-lcd-3.5': { width: 320, height: 480 },
4220
+ 'waveshare-rp2350-amoled-2.41': { width: 450, height: 600 },
4221
+ }
4222
+ const boardViewport = cppBoard ? BOARD_VIEWPORTS[cppBoard] : undefined
4223
+ const effectiveFontViewportWidths = fontViewportWidths.length > 0
4224
+ ? fontViewportWidths
4225
+ : boardViewport
4226
+ ? [String(boardViewport.width)]
4227
+ : []
4228
+ const effectiveFontViewportHeights = fontViewportHeights.length > 0
4229
+ ? fontViewportHeights
4230
+ : boardViewport
4231
+ ? [String(boardViewport.height)]
4232
+ : []
4233
+ // Glyph antialiasing per board. 1-bit B/W e-paper reads best with crisp,
4234
+ // non-antialiased glyphs — sample at 1:1 (supersample 1) so every glyph pixel
4235
+ // is fully on/off instead of a thresholded AA edge. Grayscale/color panels keep
4236
+ // the default 4x supersampled AA coverage. An explicit --font-supersample wins.
4237
+ const BOARD_FONT_SUPERSAMPLE = {
4238
+ 'esp32-s3-epaper-1.54': 1,
4239
+ }
4240
+ const fontSupersampleOverride = readOption('--font-supersample')
4241
+ const effectiveFontSupersample = fontSupersampleOverride
4242
+ ?? (cppBoard && BOARD_FONT_SUPERSAMPLE[cppBoard] !== undefined ? String(BOARD_FONT_SUPERSAMPLE[cppBoard]) : undefined)
4243
+ // Baked glyph atlas depth per board. 8 bits of coverage per pixel is the
4244
+ // default. A panel that can only show four gray levels gains nothing from the
4245
+ // other six bits, and reading the atlas out of XIP flash costs four times the
4246
+ // bandwidth — bake those boards at 2 bits/px instead, quantized offline. An
4247
+ // explicit --font-atlas-bits wins.
4248
+ const BOARD_FONT_ATLAS_BITS = {
4249
+ 'esp32-c3-xteink-x3': 2,
4250
+ // PaperS3 has a 16-level framebuffer, but glyph antialiasing only needs the
4251
+ // four offline-quantized coverage levels. This makes the existing nonlinear
4252
+ // edge treatment (48/128/208 thresholds plus stem/speckle fixups) part of the
4253
+ // baked atlas and avoids carrying 8-bit coverage through XIP at runtime.
4254
+ 'esp32-s3-m5stack-papers3': 2,
4255
+ }
4256
+ const fontAtlasBitsOverride = readOption('--font-atlas-bits')
4257
+ const effectiveFontAtlasBits = fontAtlasBitsOverride
4258
+ ?? (cppBoard && BOARD_FONT_ATLAS_BITS[cppBoard] !== undefined ? String(BOARD_FONT_ATLAS_BITS[cppBoard]) : undefined)
4259
+ const geaEmbeddedCompat = hasFlag('--gea-embedded-compat') || usesGeaEmbeddedCompat(appDir)
4260
+ const appleNative = !hasFlag('--no-apple-native') && (hasFlag('--apple-native') || usesAppleNative(appDir))
4261
+ const moduleGraphCompile = !moduleGraphCompileDisabled && (moduleGraphCompileRequested || geaEmbeddedCompat || appleNative)
4262
+ const moduleGraphEnabled = moduleGraphOnly || moduleGraphCompile || !!moduleGraphOutOption
4263
+ const moduleGraphOutDir = moduleGraphOutOption
4264
+ ? path.resolve(moduleGraphOutOption)
4265
+ : moduleGraphEnabled
4266
+ ? path.join(outDir, 'module-graph')
4267
+ : ''
4268
+ let viteConfig = geaEmbeddedCompat
4269
+ ? path.join(outDir, 'gea-embedded-compat.vite.config.mjs')
4270
+ : path.resolve(readOption('--vite-config') ?? path.join(appDir, 'vite.config.ts'))
4271
+ const appleNativePluginSpecifier = path.resolve(readOption('--geatsc-apple-native-plugin') ?? tryResolve('@geastack/geatsc-plugin-apple-native'))
4272
+
4273
+ if (!geaEmbeddedCompat && !fs.existsSync(viteConfig)) fail(`missing Vite config: ${viteConfig}`)
4274
+ if (!fs.existsSync(viteBin)) fail(`missing Vite CLI: ${viteBin}`)
4275
+ if (!fs.existsSync(geatscBin)) fail(`missing geatsc CLI: ${geatscBin}`)
4276
+ if (appleNative && !fs.existsSync(appleNativePluginSpecifier)) fail(`missing Apple native geatsc plugin: ${appleNativePluginSpecifier}`)
4277
+ for (const plugin of extraGeatscPlugins) {
4278
+ if (!fs.existsSync(plugin)) fail(`missing extra geatsc plugin: ${plugin}`)
4279
+ }
4280
+
4281
+ fs.mkdirSync(outDir, { recursive: true })
4282
+ writeDisplayConfigHeader(outDir, collectDisplayConfig(appDir, entry))
4283
+
4284
+ // Where the entry actually lives once compat staging has copied the sources.
4285
+ // The module graph is rooted at the staged tree, so geatsc must be pointed at
4286
+ // the staged entry too: an --entry naming the original app dir matches no
4287
+ // module in the graph, which silently empties the reachability root set.
4288
+ let stagedEntryFile = null
4289
+
4290
+ // Where the font generator looks for @font-face. It scans a DIRECTORY, so the
4291
+ // app dir only works while every stylesheet lives under it. An app whose entry
4292
+ // imports components from a sibling tree declares its face there, and the app
4293
+ // dir holds no CSS at all -- the atlas then comes out empty and every string
4294
+ // falls back to the built-in bitmap font. Compat staging has already gathered
4295
+ // the real source set (the app dir plus each escaped import dir, mirrored at
4296
+ // their original depth so relative url()s still resolve), so point the scan at
4297
+ // that instead. Identical for an app that is its own staging root.
4298
+ let fontSourceDir = appDir
4299
+
4300
+ if (geaEmbeddedCompat) {
4301
+ const compatSrcDir = path.join(outDir, 'gea-embedded-compat-src')
4302
+ fs.rmSync(compatSrcDir, { recursive: true, force: true })
4303
+ // Relative imports may escape the app dir (examples/shared components).
4304
+ // Mirror the escaped trees at their original depth below the common
4305
+ // ancestor so the staged sources keep resolving each other, and aim the
4306
+ // vite entry at the app's staged location.
4307
+ const escapedDirs = escapedCompatImportDirs(appDir, entry)
4308
+ // App .env vars, baked into the staged sources as process.env.<KEY> literals.
4309
+ const envDefines = dotEnvDefines(appDir)
4310
+ let compatEntry = entry
4311
+ let stagingRoot = appDir
4312
+ if (escapedDirs.length === 0) {
4313
+ copyCompatSourceTree(appDir, compatSrcDir, envDefines)
4314
+ } else {
4315
+ stagingRoot = commonAncestorDir([appDir, ...escapedDirs])
4316
+ copyCompatSourceTree(appDir, path.join(compatSrcDir, path.relative(stagingRoot, appDir)), envDefines)
4317
+ for (const dir of escapedDirs) copyCompatSourceTree(dir, path.join(compatSrcDir, path.relative(stagingRoot, dir)), envDefines)
4318
+ compatEntry = path.join(path.relative(stagingRoot, appDir), entry)
4319
+ }
4320
+ linkCompatNodeModules(appDir, path.join(compatSrcDir, path.dirname(compatEntry)), compatSrcDir)
4321
+ stateRuntimeAliasToTypeScript(path.join(compatSrcDir, path.relative(stagingRoot, appDir)), compatSrcDir, stagingRoot)
4322
+ if (moduleGraphCompile) {
4323
+ compatEntry = writeAppendJsCompatEntry({ compatSrcDir, compatEntry, appendFiles: appendJsFiles })
4324
+ }
4325
+ stagedEntryFile = path.join(compatSrcDir, compatEntry)
4326
+ fontSourceDir = compatSrcDir
4327
+ writeCompatViteConfig({ configPath: viteConfig, compatSrcDir, appDir, entry: compatEntry, viteOutDir, geaIrPath, appleNative, moduleGraphOutDir, envDefines })
4328
+ } else if (appleNative) {
4329
+ const baseViteConfig = viteConfig
4330
+ viteConfig = path.join(outDir, 'gea-apple-native.vite.config.mjs')
4331
+ writeAppleNativeViteConfig({ configPath: viteConfig, baseConfigPath: baseViteConfig, appDir, moduleGraphOutDir })
4332
+ }
4333
+
4334
+ run('node', [
4335
+ viteBin,
4336
+ 'build',
4337
+ '--config',
4338
+ viteConfig,
4339
+ '--outDir',
4340
+ viteOutDir,
4341
+ '--emptyOutDir',
4342
+ '--minify',
4343
+ 'false',
4344
+ ], {
4345
+ env: {
4346
+ ...process.env,
4347
+ GEA_IR_OUT: geaIrPath,
4348
+ ...(moduleGraphOutDir ? { GEA_VITE_MODULE_GRAPH_OUT: moduleGraphOutDir } : {}),
4349
+ },
4350
+ })
4351
+ if (moduleGraphOnly) {
4352
+ if (!moduleGraphOutDir) fail('--module-graph-only requires --module-graph-out <dir> or GEA_VITE_MODULE_GRAPH_OUT')
4353
+ process.stdout.write(`module-graph=${path.join(moduleGraphOutDir, 'gea-module-graph.json')}\n`)
4354
+ process.stdout.write(`module-graph-sources=${path.join(moduleGraphOutDir, 'sources')}\n`)
4355
+ process.exit(0)
4356
+ }
4357
+
4358
+ const viteBundle = findFirstJsFile(viteOutDir)
4359
+ if (!viteBundle) fail(`Vite produced no JS bundle in ${viteOutDir}`)
4360
+ const cssPrelude = geaEmbeddedCompat ? cssRegistrationCode(viteOutDir) : ''
4361
+ const cssPreludePath = path.join(outDir, 'gea-style-registration.cppfrag')
4362
+ if (cssPrelude || cppPreludeSymbol) {
4363
+ fs.writeFileSync(cssPreludePath, cssPrelude)
4364
+ } else {
4365
+ fs.rmSync(cssPreludePath, { force: true })
4366
+ }
4367
+ run('node', [
4368
+ path.join(coreRoot, 'scripts/generate-gea-embedded-fonts.mjs'),
4369
+ '--app-dir',
4370
+ fontSourceDir,
4371
+ '--css-dir',
4372
+ viteOutDir,
4373
+ '--out-cpp',
4374
+ path.join(outDir, 'gea_embedded_font_generated.cpp'),
4375
+ '--out-h',
4376
+ path.join(outDir, 'gea_embedded_font_generated.h'),
4377
+ ...effectiveFontViewportWidths.flatMap((width) => ['--viewport-width', width]),
4378
+ ...effectiveFontViewportHeights.flatMap((height) => ['--viewport-height', height]),
4379
+ ...fontDevicePixelRatios.flatMap((ratio) => ['--device-pixel-ratio', ratio]),
4380
+ ...(effectiveFontSupersample ? ['--supersample', effectiveFontSupersample] : []),
4381
+ ...(effectiveFontAtlasBits ? ['--atlas-bits', effectiveFontAtlasBits] : []),
4382
+ ...(fontSymbolPrefix ? ['--symbol-prefix', fontSymbolPrefix] : []),
4383
+ ...(runtimeTtfFonts ? ['--runtime-ttf-fonts'] : []),
4384
+ ])
4385
+ // Embed app assets (images referenced as `<img src="...">`) as `.rodata` linked
4386
+ // into the app binary, registered by path. Like the font TU above, this always
4387
+ // emits a (possibly empty) gea_embedded_assets_generated.cpp so every target's
4388
+ // source list can include it unconditionally. Symbols live in an anonymous
4389
+ // namespace, so several isolated programs' asset TUs can be linked together (a
4390
+ // launcher bundle) without colliding.
4391
+ run('node', [
4392
+ path.join(coreRoot, 'scripts/generate-gea-embedded-assets.mjs'),
4393
+ '--app-dir',
4394
+ appDir,
4395
+ '--out-cpp',
4396
+ path.join(outDir, 'gea_embedded_assets_generated.cpp'),
4397
+ ])
4398
+ const typeHints = collectBundledTypeHints(appDir, entry)
4399
+ const typeHintsPath = path.join(path.dirname(bundlePath), 'gea-type-hints.d.ts')
4400
+ let bundledCode = fs.readFileSync(viteBundle, 'utf8')
4401
+ let typeHintsReferencePath = ''
4402
+ const typeHintParts = [...(typeHints.imports ?? []), ...typeHints.declarations]
4403
+ if (typeHintParts.length > 0) {
4404
+ fs.writeFileSync(typeHintsPath, `${typeHintParts.join('\n\n')}\n`)
4405
+ typeHintsReferencePath = `./${path.basename(typeHintsPath)}`
4406
+ } else {
4407
+ fs.rmSync(typeHintsPath, { force: true })
4408
+ }
4409
+ bundledCode = applyBundledTypeHints(bundledCode, typeHints, typeHintsReferencePath)
4410
+ // The vite-plugin's lean ReactiveComponent transform emits side-effectful
4411
+ // `(globalThis.__GEA_IR_KEEP__ ||= []).push(Child, …)` keep-alives so the
4412
+ // bundler can't tree-shake IR-live child classes and the store globals they
4413
+ // reference. The references did their job during bundling — strip the marker
4414
+ // statements so geatsc never compiles a globalThis property write.
4415
+ bundledCode = bundledCode.replace(/^.*__GEA_IR_KEEP__.*$\n?/gm, '')
4416
+ // Compiled-template root caches (`var _tplN_root = null`) hold a cached node
4417
+ // that later gets `.cloneNode(true)` — typing them as Node makes geatsc store
4418
+ // them as a native NodeHandle so cloneNode/firstChild/childNodes dispatch to
4419
+ // the typed tree methods instead of falling onto an opaque boxed object (the
4420
+ // untyped null inferred gea_cpp_value, whose generic boxing has no DOM method
4421
+ // table — the app mounted nothing and the screen stayed black).
4422
+ bundledCode = bundledCode.replace(/\bvar (_tpl\d+_root) = null;/g, '/** @type {Node} */ var $1 = null;')
4423
+ // JS `(cache || (cache = make()))` returns the VALUE; on native NodeHandles
4424
+ // the C++ `||` lowering yields bool — statement-ize the idiom instead.
4425
+ bundledCode = bundledCode.replace(/\((_tpl\d+_root) \|\| \(\1 = (_tpl\d+_create)\(\)\)\)\.cloneNode\(true\)/g,
4426
+ '($1 = $1 || $2(), $1).cloneNode(true)')
4427
+ fs.writeFileSync(bundlePath, bundledCode)
4428
+ if (!moduleGraphCompile) {
4429
+ for (const appendFile of appendJsFiles) {
4430
+ if (!fs.existsSync(appendFile)) fail(`missing appended JS file: ${appendFile}`)
4431
+ fs.appendFileSync(bundlePath, `\n${fs.readFileSync(appendFile, 'utf8')}\n`)
4432
+ }
4433
+ }
4434
+ // Module-graph snapshots are captured pre-type-strip, so they keep their
4435
+ // source annotations — EXCEPT the `el = null` field the lean ReactiveComponent
4436
+ // transform injects untyped after stripping the `Component<T>` heritage.
4437
+ // Re-attach the heritage generic as a real TS annotation (`el: T | null`) so
4438
+ // geatsc lowers `this.el` to the typed native handle instead of a boxed value.
4439
+ if (moduleGraphCompile && moduleGraphOutDir) {
4440
+ restoreModuleGraphElAnnotations(moduleGraphOutDir, typeHints.propertyHints)
4441
+ }
4442
+
4443
+ const applePlatform = readOption('--apple-platform') ?? ''
4444
+ const appleNativeMetadataPath = appleNative ? await writeAppleNativeMetadata(outDir, appDir, applePlatform) : ''
4445
+ const geatscArgs = [
4446
+ geatscBin,
4447
+ moduleGraphCompile ? 'compile-module-graph' : 'compile',
4448
+ moduleGraphCompile ? path.join(moduleGraphOutDir, 'gea-module-graph.json') : bundlePath,
4449
+ '--out-dir',
4450
+ outDir,
4451
+ '--plugin',
4452
+ geaPluginSpecifier,
4453
+ '--plugin-option',
4454
+ `gea.ir=${geaIrPath}`,
4455
+ '--plugin-option',
4456
+ `gea.microtasks-namespace=${microtasksNamespaceForEntry(entrySymbol)}`,
4457
+ ]
4458
+ if (!moduleGraphCompile) geatscArgs.push('--target', 'cpp')
4459
+ if (moduleGraphCompile) {
4460
+ geatscArgs.push('--entry', stagedEntryFile ?? path.join(appDir, entry), '--module-graph-stage', 'hybrid')
4461
+ }
4462
+ if (appleNative) {
4463
+ geatscArgs.push('--plugin', appleNativePluginSpecifier, '--plugin-option', `apple.metadata=${appleNativeMetadataPath}`)
4464
+ }
4465
+ for (const plugin of extraGeatscPlugins) {
4466
+ geatscArgs.push('--plugin', plugin)
4467
+ }
4468
+ if (cssPrelude || cppPreludeSymbol) geatscArgs.push('--plugin-option', `gea.cpp-prelude=${cssPreludePath}`)
4469
+ if (cppPreludeSymbol) geatscArgs.push('--plugin-option', `gea.cpp-prelude-symbol=${cppPreludeSymbol}`)
4470
+ if (pixelPanelEndian) geatscArgs.push('--plugin-option', `gea.pixel-panel-endian=${pixelPanelEndian}`)
4471
+ if (cppBoard) geatscArgs.push('--cpp-board', cppBoard)
4472
+ if (cxxStandard) geatscArgs.push('--cxx-standard', cxxStandard)
4473
+ if (hasFlag('--allow-any')) geatscArgs.push('--allow-any')
4474
+ if (entrySymbol) geatscArgs.push('--entry-symbol', entrySymbol)
4475
+ if (hasFlag('--isolate-symbols')) geatscArgs.push('--isolate-symbols')
4476
+ // One C++ unit per source module plus a shared header, instead of one unit for
4477
+ // the whole program. Every target already reads `geatsc-sources.txt` as a LIST,
4478
+ // so nothing downstream changes; see the compiler's docs/TRANSLATION-UNITS.md
4479
+ // for what it buys (parallel and incremental C++ builds) and what it costs.
4480
+ // The env var is honored beside the flag so a target need not plumb one: every
4481
+ // build in the tree goes through this script, and the boards' CMakeLists spell
4482
+ // their geatsc command line by hand, so an env var reaches all of them at once
4483
+ // (`GEA_PER_FILE_UNITS=1 <any build>`). build-macos.sh sets the flag itself and
4484
+ // folds the variable into its freshness signature, because a layout change
4485
+ // invalidates every generated file.
4486
+ if (process.env.GEA_CPP_TRANSLATION_UNITS === 'balanced') geatscArgs.push('--translation-units', 'balanced')
4487
+ else if (hasFlag('--per-file-units') || process.env.GEA_PER_FILE_UNITS === '1') geatscArgs.push('--translation-units', 'per-file')
4488
+ if (hasFlag('--gea-ir-backend') || hasFlag('--gea-replace-renderers')) {
4489
+ geatscArgs.push('--plugin-option', 'gea.replace-renderers=true')
4490
+ }
4491
+ // The whole-program analysis of a three.js app (a 284-module graph)
4492
+ // peaks well above node's default heap cap: on 2026-09-14 this exact spawn
4493
+ // died with `Ineffective mark-compacts near heap limit` after five minutes,
4494
+ // and the build reported "C++ generation failed". The compiler's own
4495
+ // measurement scripts are always run with this
4496
+ // limit; the build has to state the same one, not depend on the caller's
4497
+ // NODE_OPTIONS.
4498
+ run('node', ['--max-old-space-size=32768', ...geatscArgs])
4499
+
4500
+ process.stdout.write(`gea-vite-bundle=${bundlePath}\n`)
4501
+ if (moduleGraphOutDir) process.stdout.write(`module-graph=${path.join(moduleGraphOutDir, 'gea-module-graph.json')}\n`)
4502
+ const geatscSourceListPath = path.join(outDir, 'geatsc-sources.txt')
4503
+ process.stdout.write(`geatsc-sources=${geatscSourceListPath}\n`)
4504
+ if (fs.existsSync(geatscSourceListPath)) {
4505
+ const [entrySource] = fs.readFileSync(geatscSourceListPath, 'utf8').split(/\r?\n/).filter(Boolean)
4506
+ if (entrySource) process.stdout.write(`geatsc-entry-source=${entrySource}\n`)
4507
+ }
4508
+
4509
+ function microtasksNamespaceForEntry(symbol) {
4510
+ if (symbol && symbol.endsWith('_top_level')) {
4511
+ return `gea::framework::app::generated::${symbol.replace(/_top_level$/, '')}`
4512
+ }
4513
+ return 'gea::framework::app::generated'
4514
+ }