@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,578 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ const JS_LIKE_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx)$/i
6
+ const STATIC_ASSET_RE = /\.(?:apng|bmp|png|jpe?g|jfif|pjpeg|pjp|gif|svg|ico|webp|avif|cur|jxl|mp4|webm|ogg|mp3|wav|flac|aac|opus|mov|m4a|vtt|woff2?|eot|ttf|otf|webmanifest|pdf|txt|glb|gltf|hdr)$/i
7
+ const STATIC_ASSET_MODULE_SUFFIX = '.geaassetmodule.js'
8
+ const STATIC_IMPORT_RE = /\bimport\s+(?:[^'"()]*?\s+from\s*)?["']([^"']+)["']/g
9
+ const EXPORT_FROM_RE = /\bexport\s+(?:[^'"]*?\s+from\s*)["']([^"']+)["']/g
10
+ const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g
11
+
12
+ // Native targets replace browser-only package surfaces with reusable native
13
+ // packages. Keeping these aliases here makes the target choice part of Core's
14
+ // generated native build rather than an application-specific Vite override.
15
+ export function geaAppleNativeModuleAliases(options = {}) {
16
+ const aliases = []
17
+ if (options.threeWebGLAnimationModule) {
18
+ if (!path.isAbsolute(options.threeWebGLAnimationModule)) {
19
+ throw new TypeError('threeWebGLAnimationModule must be an absolute module resolved from the application package boundary')
20
+ }
21
+ aliases.push({
22
+ find: /^\.\/webgl\/WebGLAnimation\.js$/,
23
+ replacement: options.threeWebGLAnimationModule,
24
+ })
25
+ }
26
+ if (options.threeWebXRManagerModule) {
27
+ if (!path.isAbsolute(options.threeWebXRManagerModule)) {
28
+ throw new TypeError('threeWebXRManagerModule must be an absolute module resolved from the application package boundary')
29
+ }
30
+ aliases.push({
31
+ find: /^\.\/webxr\/WebXRManager\.js$/,
32
+ replacement: options.threeWebXRManagerModule,
33
+ })
34
+ }
35
+ if (options.threeUtilsModule) {
36
+ if (!path.isAbsolute(options.threeUtilsModule)) {
37
+ throw new TypeError('threeUtilsModule must be an absolute module resolved from the application package boundary')
38
+ }
39
+ aliases.push({
40
+ find: /^(?:\.\.\/)+utils\.js$/,
41
+ replacement: options.threeUtilsModule,
42
+ })
43
+ }
44
+ if (options.troikaThreeTextModule) {
45
+ // Vite aliases are not a guaranteed recursive resolver chain. Freeze the
46
+ // native module selected at the application boundary into one absolute hop.
47
+ if (!path.isAbsolute(options.troikaThreeTextModule)) {
48
+ throw new TypeError('troikaThreeTextModule must be an absolute module resolved from the application package boundary')
49
+ }
50
+ aliases.push({
51
+ find: /^troika-three-text$/,
52
+ replacement: options.troikaThreeTextModule,
53
+ })
54
+ }
55
+ if (options.threeSrcDir) {
56
+ if (!path.isAbsolute(options.threeSrcDir)) {
57
+ throw new TypeError('threeSrcDir must be an absolute directory resolved from the application package boundary')
58
+ }
59
+ aliases.push({
60
+ find: /^three\/src\/(.+)$/,
61
+ replacement: path.join(options.threeSrcDir, '$1'),
62
+ })
63
+ }
64
+ return aliases
65
+ }
66
+
67
+ export function geaModuleGraphPlugins(options = {}) {
68
+ const outDir = options.outDir || process.env.GEA_VITE_MODULE_GRAPH_OUT || ''
69
+ if (!outDir) return []
70
+
71
+ const state = {
72
+ outDir: path.resolve(outDir),
73
+ root: '',
74
+ original: new Map(),
75
+ transformed: new Map(),
76
+ imports: new Map(),
77
+ assets: new Map(),
78
+ // GEA_MODULE_GRAPH_ENTRY_REACHABLE_ONLY=0 keeps the unpruned graph, so a
79
+ // build can be A/B'd against the reachability filter without editing the
80
+ // generated vite config.
81
+ entryReachableOnly: options.entryReachableOnly === true &&
82
+ process.env.GEA_MODULE_GRAPH_ENTRY_REACHABLE_ONLY !== '0',
83
+ }
84
+
85
+ return [
86
+ {
87
+ name: 'gea-module-graph-static-assets',
88
+ enforce: 'pre',
89
+ apply: 'build',
90
+ configResolved(config) {
91
+ state.root = config.root
92
+ },
93
+ async resolveId(source, importer) {
94
+ // Vite normally turns a plain static-asset import into a JS module that
95
+ // exports the emitted browser URL. The native compiler consumes source
96
+ // snapshots instead of the browser bundle, so expose the same binding
97
+ // through a compiler-visible synthetic JS module. Query imports retain
98
+ // Vite's distinct ?raw/?inline/etc. semantics.
99
+ const normalizedImporter = normalizeModuleId(importer)
100
+ if (
101
+ !importer ||
102
+ !shouldTrackModule(normalizedImporter) ||
103
+ source.includes('?') ||
104
+ source.includes('#') ||
105
+ !STATIC_ASSET_RE.test(source)
106
+ ) return null
107
+
108
+ let resolvedId = ''
109
+ const resolved = await this.resolve(source, importer, { skipSelf: true })
110
+ if (resolved && !resolved.external) resolvedId = normalizeModuleId(resolved.id)
111
+ if (!resolvedId && source.startsWith('.')) {
112
+ resolvedId = path.resolve(path.dirname(normalizedImporter), source)
113
+ }
114
+ if (!path.isAbsolute(resolvedId) || !fs.existsSync(resolvedId) || !fs.statSync(resolvedId).isFile()) return null
115
+
116
+ const assetSource = fs.realpathSync(resolvedId)
117
+ const moduleId = `${assetSource}${STATIC_ASSET_MODULE_SUFFIX}`
118
+ if (!state.assets.has(moduleId)) state.assets.set(moduleId, describeStaticAsset(assetSource))
119
+ return moduleId
120
+ },
121
+ load(id) {
122
+ const asset = state.assets.get(normalizeModuleId(id))
123
+ if (!asset) return null
124
+ return `export default ${JSON.stringify(asset.url)}\n`
125
+ },
126
+ },
127
+ {
128
+ name: 'gea-module-graph-original',
129
+ enforce: 'pre',
130
+ apply: 'build',
131
+ configResolved(config) {
132
+ state.root = config.root
133
+ },
134
+ transform(code, id) {
135
+ captureSource(state.original, id, code)
136
+ return null
137
+ },
138
+ },
139
+ {
140
+ name: 'gea-module-graph-transformed',
141
+ // 'pre', not 'post': this plugin is registered AFTER the gea plugin, so a
142
+ // pre-enforced transform still sees the code with the gea component/store
143
+ // transforms applied — but BEFORE vite's core TS transform strips every
144
+ // type annotation. geatsc (the consumer of these snapshots) is a
145
+ // TypeScript compiler: erased annotations are pure loss for it. A `.tsx`
146
+ // module compiled from the post-strip snapshot lost e.g. `const
147
+ // cacheImgs: GeaEmbeddedImage[] = []` and boxed the whole array into
148
+ // `std::vector<gea_cpp_value>` (maps' tile cache), instead of the typed
149
+ // `std::vector<gea::host::GeaEmbeddedImage>` the annotation lowers to.
150
+ enforce: 'pre',
151
+ apply: 'build',
152
+ configResolved(config) {
153
+ state.root = config.root
154
+ },
155
+ async transform(code, id) {
156
+ captureSource(state.transformed, id, code)
157
+ await captureResolvedImports(this, state, id, code)
158
+ return null
159
+ },
160
+ generateBundle(_options, bundle) {
161
+ reportTreeShakenModules(this, state, bundle)
162
+ writeModuleGraph(this, state)
163
+ },
164
+ },
165
+ ]
166
+ }
167
+
168
+ export default geaModuleGraphPlugins
169
+
170
+ function captureSource(target, id, code) {
171
+ const normalized = normalizeModuleId(id)
172
+ if (!shouldTrackModule(normalized)) return
173
+ target.set(normalized, code)
174
+ }
175
+
176
+ function writeModuleGraph(pluginContext, state) {
177
+ fs.rmSync(state.outDir, { recursive: true, force: true })
178
+ fs.mkdirSync(path.join(state.outDir, 'sources'), { recursive: true })
179
+
180
+ const rawIdsByNormalizedId = new Map()
181
+ for (const rawId of pluginContext.getModuleIds()) {
182
+ const id = normalizeModuleId(rawId)
183
+ if (shouldTrackModule(id) && !rawIdsByNormalizedId.has(id)) rawIdsByNormalizedId.set(id, rawId)
184
+ }
185
+ const modules = []
186
+ const allIds = Array.from(rawIdsByNormalizedId.keys()).sort()
187
+ const reachableIds = state.entryReachableOnly
188
+ ? entryReachableModuleIds(pluginContext, rawIdsByNormalizedId, allIds)
189
+ : allIds
190
+ const live = liveModuleIds(state, reachableIds)
191
+ const ids = reachableIds.filter((id) => live.keep.has(id))
192
+
193
+ for (const id of ids) {
194
+ const info = pluginContext.getModuleInfo(rawIdsByNormalizedId.get(id))
195
+ const prune = (code) => pruneDeadReExports(code, id, state, live)
196
+ const originalCode = prune(normalizeSnapshotSourceForGeatsc(state.original.get(id) ?? readFileSource(id)))
197
+ const transformedCode = prune(normalizeSnapshotSourceForGeatsc(state.transformed.get(id)))
198
+ const originalPath = writeSnapshot(state, id, 'original', originalCode)
199
+ const transformedPath = writeSnapshot(state, id, 'transformed', transformedCode)
200
+ const asset = state.assets.get(id)
201
+ modules.push({
202
+ id,
203
+ file: path.isAbsolute(id) ? id : null,
204
+ relativeFile: path.isAbsolute(id) && state.root ? path.relative(state.root, id) : null,
205
+ imports: (state.imports.get(id) ?? []).filter(
206
+ (imported) => !imported.tracked || !imported.resolvedId || live.keep.has(imported.resolvedId),
207
+ ),
208
+ importedIds: normalizeIdList(info?.importedIds ?? []).filter((dependency) => live.keep.has(dependency)),
209
+ dynamicallyImportedIds: normalizeIdList(info?.dynamicallyImportedIds ?? []).filter((dependency) => live.keep.has(dependency)),
210
+ importers: normalizeIdList(info?.importers ?? []).filter((importer) => live.keep.has(importer)),
211
+ isEntry: Boolean(info?.isEntry),
212
+ hasModuleSideEffects: info?.moduleSideEffects ?? null,
213
+ originalSource: originalPath,
214
+ transformedSource: transformedPath,
215
+ originalBytes: originalCode === undefined ? 0 : Buffer.byteLength(originalCode),
216
+ transformedBytes: transformedCode === undefined ? 0 : Buffer.byteLength(transformedCode),
217
+ ...(asset ? {
218
+ assetSource: asset.source,
219
+ assetBytes: asset.bytes,
220
+ assetSha256: asset.sha256,
221
+ assetUrl: asset.url,
222
+ } : {}),
223
+ })
224
+ }
225
+
226
+ const assets = modules
227
+ .filter((module) => module.assetSource)
228
+ .map((module) => ({
229
+ moduleId: module.id,
230
+ source: module.assetSource,
231
+ relativeFile: state.root && path.isAbsolute(module.assetSource)
232
+ ? path.relative(state.root, module.assetSource)
233
+ : null,
234
+ bytes: module.assetBytes,
235
+ sha256: module.assetSha256,
236
+ url: module.assetUrl,
237
+ }))
238
+
239
+ const manifest = {
240
+ version: 1,
241
+ generatedAt: new Date().toISOString(),
242
+ root: state.root,
243
+ moduleCount: modules.length,
244
+ assetCount: assets.length,
245
+ assets,
246
+ modules,
247
+ }
248
+ fs.writeFileSync(path.join(state.outDir, 'gea-module-graph.json'), `${JSON.stringify(manifest, null, 2)}\n`)
249
+ }
250
+
251
+ /**
252
+ * Report what Rollup's own tree-shaking decided, next to what the graph is
253
+ * about to hand geatsc.
254
+ *
255
+ * The graph is assembled from MODULE-level reachability, and every gea runtime
256
+ * module is reachable because `compiler-runtime.ts` is a pure re-export barrel:
257
+ * one import of the virtual runtime pulls in `keyed-list`, `reactive-html`,
258
+ * `relational-class`, `conditional-truthy` and the rest whether or not the
259
+ * application calls any of them. geatsc then has to lower all of it, so a
260
+ * lowering gap in a module the product never runs blocks the whole build —
261
+ * which is exactly where a real app stopped (a nested function in
262
+ * `conditional-truthy.ts`, a module its App.tsx never imports a name from).
263
+ *
264
+ * Rollup already answered the SYMBOL-level question by the time this hook runs:
265
+ * a module that survived shaking appears in some chunk's `modules` map, and one
266
+ * that did not appear at all is dead code by the bundler's own account. This
267
+ * logs both sets unconditionally — no environment variable — so the size of the
268
+ * gap between "in the graph" and "in the bundle" is visible on every build
269
+ * instead of having to be re-measured by hand.
270
+ */
271
+ function reportTreeShakenModules(pluginContext, state, bundle) {
272
+ const rendered = (state.rendered = new Set())
273
+ for (const output of Object.values(bundle ?? {})) {
274
+ if (output?.type !== 'chunk') continue
275
+ for (const id of Object.keys(output.modules ?? {})) {
276
+ const normalized = normalizeModuleId(id)
277
+ if (shouldTrackModule(normalized)) rendered.add(normalized)
278
+ }
279
+ }
280
+ const tracked = []
281
+ for (const rawId of pluginContext.getModuleIds()) {
282
+ const id = normalizeModuleId(rawId)
283
+ if (shouldTrackModule(id) && !tracked.includes(id)) tracked.push(id)
284
+ }
285
+ const shaken = tracked.filter((id) => !rendered.has(id)).sort()
286
+ process.stderr.write(
287
+ `[gea-module-graph] ${tracked.length} tracked module(s), ${rendered.size} rendered by rollup, ${shaken.length} shaken out\n`,
288
+ )
289
+ for (const id of shaken) {
290
+ process.stderr.write(`[gea-module-graph] shaken-out: ${state.root ? path.relative(state.root, id) : id}\n`)
291
+ }
292
+ }
293
+
294
+ /**
295
+ * The modules the graph actually hands geatsc, and which of them are pure
296
+ * re-export hubs whose dead re-exports must be pruned with them.
297
+ *
298
+ * Rollup's rendered set is the authority on what is live; this function only
299
+ * adds back what would otherwise become UNRESOLVABLE. Two kinds of module are
300
+ * added back:
301
+ *
302
+ * - a re-export hub (`compiler-runtime.ts`, the `virtual:gea-*` runtime
303
+ * module) that a surviving module imports through. Rollup inlines the
304
+ * bindings and never renders the hub itself, but geatsc resolves imports by
305
+ * module, so the hub has to stay — with its dead re-export lines removed,
306
+ * which is what `pruneDeadReExports` does.
307
+ * - any other module a surviving module still imports. Rollup dropping it
308
+ * while a surviving snapshot names it means the snapshot, not the bundle,
309
+ * is the binding contract here; keeping it is the fail-safe direction.
310
+ *
311
+ * A hub contributes only rendered modules and further hubs, which is the whole
312
+ * point: `compiler-runtime.ts` re-exports every gea runtime module, so
313
+ * following its edges unconditionally would resurrect the entire runtime and
314
+ * leave the graph exactly as unpruned as before.
315
+ */
316
+ function liveModuleIds(state, ids) {
317
+ const tracked = new Set(ids)
318
+ const rendered = state.rendered
319
+ // No bundle information at all (a pipeline that never reached
320
+ // `generateBundle`, or a build that rendered nothing) is safer left intact
321
+ // than silently reduced.
322
+ if (!rendered || rendered.size === 0) return { keep: tracked, hubs: new Set() }
323
+ const keep = new Set([...tracked].filter((id) => rendered.has(id)))
324
+ if (keep.size === 0) return { keep: tracked, hubs: new Set() }
325
+
326
+ const hubs = new Set()
327
+ const isHub = (id) => {
328
+ if (hubs.has(id)) return true
329
+ if (!reExportHubIds(state).has(id)) return false
330
+ hubs.add(id)
331
+ return true
332
+ }
333
+ const resurrected = []
334
+ let changed = true
335
+ while (changed) {
336
+ changed = false
337
+ for (const id of [...keep]) {
338
+ const viaHub = isHub(id)
339
+ for (const imported of state.imports.get(id) ?? []) {
340
+ const target = imported.resolvedId
341
+ if (!imported.tracked || !target || !tracked.has(target) || keep.has(target)) continue
342
+ if (viaHub && !rendered.has(target) && !isHub(target)) continue
343
+ keep.add(target)
344
+ resurrected.push(target)
345
+ changed = true
346
+ }
347
+ }
348
+ }
349
+
350
+ process.stderr.write(
351
+ `[gea-module-graph] pruned to ${keep.size}/${tracked.size} module(s): ` +
352
+ `${[...keep].filter((id) => rendered.has(id)).length} rendered, ${hubs.size} re-export hub(s), ` +
353
+ `${resurrected.filter((id) => !hubs.has(id)).length} kept because a surviving module still imports them\n`,
354
+ )
355
+ for (const id of resurrected.filter((target) => !hubs.has(target)).sort()) {
356
+ process.stderr.write(`[gea-module-graph] kept-unrendered: ${state.root ? path.relative(state.root, id) : id}\n`)
357
+ }
358
+ return { keep, hubs }
359
+ }
360
+
361
+ /**
362
+ * Modules whose entire body is imports and `export ... from` — no declarations,
363
+ * no statements. Only these are safe to rewrite by dropping re-export lines.
364
+ */
365
+ function reExportHubIds(state) {
366
+ if (state.hubIds) return state.hubIds
367
+ const hubIds = (state.hubIds = new Set())
368
+ for (const [id, code] of state.transformed.size > 0 ? state.transformed : state.original) {
369
+ const source = code ?? ''
370
+ const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[^\S\n]*\/\/.*$/gm, '')
371
+ const remainder = withoutComments
372
+ .replace(/\bimport\s+(?:[^'"()]*?\s+from\s*)?["'][^"']+["']\s*;?/g, '')
373
+ .replace(/\bexport\s+(?:[^'"]*?\s+from\s*)["'][^"']+["']\s*;?/g, '')
374
+ if (remainder.trim().length === 0 && /\bexport\b/.test(withoutComments)) hubIds.add(id)
375
+ }
376
+ return hubIds
377
+ }
378
+
379
+ /**
380
+ * Remove a hub's `export ... from` statements whose target did not survive.
381
+ *
382
+ * Left in place they name modules the graph no longer carries, which is an
383
+ * unresolvable import for geatsc rather than the dead code rollup treated them
384
+ * as. Non-hub modules are returned untouched — this rewrite is only sound for a
385
+ * body that is nothing but re-exports.
386
+ */
387
+ function pruneDeadReExports(code, id, state, live) {
388
+ if (code === undefined || !live.hubs.has(id)) return code
389
+ const targetBySpecifier = new Map()
390
+ for (const imported of state.imports.get(id) ?? []) {
391
+ if (imported.resolvedId) targetBySpecifier.set(imported.specifier, imported.resolvedId)
392
+ }
393
+ return code.replace(/\bexport\s+(?:[^'"]*?\s+from\s*)["']([^"']+)["']\s*;?/g, (statement, specifier) => {
394
+ const target = targetBySpecifier.get(specifier)
395
+ return target && !live.keep.has(target) ? '' : statement
396
+ })
397
+ }
398
+
399
+ function entryReachableModuleIds(pluginContext, rawIdsByNormalizedId, allIds) {
400
+ const tracked = new Set(allIds)
401
+ const reachable = new Set()
402
+ const pending = []
403
+
404
+ for (const id of allIds) {
405
+ const info = pluginContext.getModuleInfo(rawIdsByNormalizedId.get(id))
406
+ if (info?.isEntry) pending.push(id)
407
+ }
408
+
409
+ // A malformed or synthetic-only build with no visible entry is safer left
410
+ // intact than silently reduced to an empty compiler graph.
411
+ if (pending.length === 0) return allIds
412
+
413
+ while (pending.length > 0) {
414
+ const id = pending.pop()
415
+ if (!id || reachable.has(id)) continue
416
+ reachable.add(id)
417
+ const info = pluginContext.getModuleInfo(rawIdsByNormalizedId.get(id))
418
+ for (const dependency of [...(info?.importedIds ?? []), ...(info?.dynamicallyImportedIds ?? [])]) {
419
+ const normalized = normalizeModuleId(dependency)
420
+ if (tracked.has(normalized) && !reachable.has(normalized)) pending.push(normalized)
421
+ }
422
+ }
423
+
424
+ return allIds.filter((id) => reachable.has(id))
425
+ }
426
+
427
+ function describeStaticAsset(source) {
428
+ const contents = fs.readFileSync(source)
429
+ const sha256 = crypto.createHash('sha256').update(contents).digest('hex')
430
+ // The hash makes the logical URL collision-safe and stable for unchanged
431
+ // bytes. The basename remains available to native bundle adapters, while the
432
+ // manifest is the authoritative URL -> source mapping.
433
+ const basename = encodeURIComponent(path.basename(source))
434
+ return {
435
+ source,
436
+ bytes: contents.length,
437
+ sha256,
438
+ url: `gea-asset://sha256/${sha256}/${basename}`,
439
+ }
440
+ }
441
+
442
+ async function captureResolvedImports(pluginContext, state, id, code) {
443
+ const normalized = normalizeModuleId(id)
444
+ if (!shouldTrackModule(normalized)) return
445
+
446
+ const imports = []
447
+ const seen = new Set()
448
+ const sources = [state.original.get(normalized), code].filter((source) => source !== undefined)
449
+ for (const source of sources) {
450
+ for (const imported of collectImportSpecifiers(source)) {
451
+ const key = `${imported.kind}\0${imported.specifier}`
452
+ if (seen.has(key)) continue
453
+ seen.add(key)
454
+ imports.push(imported)
455
+ }
456
+ }
457
+
458
+ const resolvedImports = []
459
+ for (const source of imports) {
460
+ let resolvedId = null
461
+ try {
462
+ const resolved = await pluginContext.resolve(source.specifier, id, { skipSelf: true })
463
+ if (resolved && !resolved.external) resolvedId = normalizeModuleId(resolved.id)
464
+ } catch {
465
+ resolvedId = null
466
+ }
467
+ resolvedImports.push({
468
+ ...source,
469
+ resolvedId,
470
+ tracked: resolvedId ? shouldTrackModule(resolvedId) : false,
471
+ })
472
+ }
473
+ state.imports.set(normalized, resolvedImports)
474
+ }
475
+
476
+ function collectImportSpecifiers(code) {
477
+ const imports = []
478
+ const seen = new Set()
479
+ collectImportMatches(imports, seen, code, STATIC_IMPORT_RE, 'import')
480
+ collectImportMatches(imports, seen, code, EXPORT_FROM_RE, 'export')
481
+ collectImportMatches(imports, seen, code, DYNAMIC_IMPORT_RE, 'dynamic-import')
482
+ return imports
483
+ }
484
+
485
+ function collectImportMatches(imports, seen, code, pattern, kind) {
486
+ pattern.lastIndex = 0
487
+ let match
488
+ while ((match = pattern.exec(code))) {
489
+ const specifier = match[1]
490
+ const key = `${kind}\0${specifier}`
491
+ if (seen.has(key)) continue
492
+ seen.add(key)
493
+ imports.push({ kind, specifier })
494
+ }
495
+ }
496
+
497
+ function writeSnapshot(state, id, stage, code) {
498
+ if (code === undefined) return null
499
+ const cleanExt = path.extname(id) || '.js'
500
+ const safeExt = JS_LIKE_RE.test(`x${cleanExt}`) ? cleanExt : '.js'
501
+ const name = `${hashId(id)}.${stage}${safeExt}`
502
+ const abs = path.join(state.outDir, 'sources', name)
503
+ fs.writeFileSync(abs, code)
504
+ return path.relative(state.outDir, abs)
505
+ }
506
+
507
+ function normalizeSnapshotSourceForGeatsc(code) {
508
+ if (code === undefined) return undefined
509
+ // The vite-plugin leaves keep-alive statements so Rollup preserves child
510
+ // component classes long enough for IR extraction. The module-graph path
511
+ // compiles snapshots directly, so remove the bundler-only marker before
512
+ // geatsc turns it into native constructor/prototype materialization.
513
+ let next = code.replace(/^.*__GEA_IR_KEEP__.*$\n?/gm, '')
514
+ next = next.replace(/\bvar (_tpl\d+_root) = null;/g, '/** @type {Node} */ var $1 = null;')
515
+ next = next.replace(
516
+ /\((_tpl\d+_root) \|\| \(\1 = (_tpl\d+_create)\(\)\)\)\.cloneNode\(true\)/g,
517
+ '($1 = $1 || $2(), $1).cloneNode(true)',
518
+ )
519
+ return next
520
+ }
521
+
522
+ function readFileSource(id) {
523
+ if (!path.isAbsolute(id) || !fs.existsSync(id)) return undefined
524
+ try {
525
+ return fs.readFileSync(id, 'utf8')
526
+ } catch {
527
+ return undefined
528
+ }
529
+ }
530
+
531
+ function normalizeIdList(ids) {
532
+ return ids.map((id) => normalizeModuleId(id)).filter((id) => shouldTrackModule(id)).sort()
533
+ }
534
+
535
+ // The gea vite plugin serves its compiler runtime from a VIRTUAL module
536
+ // (`virtual:gea-compiler-runtime`, resolved to the rollup-private `\0`-prefixed
537
+ // id). Every compiled JSX module imports its component bases and DOM binding
538
+ // helpers from it — `CompiledStaticComponent`, `GEA_STATIC_TEMPLATE`,
539
+ // `delegateEvent`, `reactiveValueRead` — so dropping it from the graph, as a
540
+ // blanket `\0` filter does, hands geatsc a program whose class heritage,
541
+ // template-method parameters and template roots all resolve to nothing. The
542
+ // symptoms are the downstream fail-closed guards, one per compile:
543
+ // "Source class heritage … effectful-or-dynamic-base-expression",
544
+ // "representation-plan coverage gap for Parameter … text=\"d\"", and
545
+ // "… for VariableDeclaration … text=\"_tpl0_root = null\"".
546
+ //
547
+ // Give those modules a synthetic absolute path instead. It never exists on
548
+ // disk, and it does not need to: the manifest carries the snapshot geatsc
549
+ // actually reads (compiler/module-graph.ts installs `sourceTextByFile` over the
550
+ // TS compiler host), and a path is what the rest of this file — id
551
+ // normalization, reachability, snapshot naming, and the specifier -> file map
552
+ // geatsc resolves imports through — is written against.
553
+ //
554
+ // Scoped to `virtual:gea-*` deliberately: rollup and vite use the same `\0`
555
+ // space for internal helper modules (preload, commonjs shims) that are bundler
556
+ // mechanics, not application source.
557
+ const GEA_VIRTUAL_MODULE_PREFIX = '\0virtual:gea-'
558
+ const GEA_VIRTUAL_MODULE_ROOT = '/@gea-virtual'
559
+
560
+ function normalizeModuleId(id) {
561
+ if (!id) return id
562
+ if (id.startsWith(GEA_VIRTUAL_MODULE_PREFIX)) {
563
+ const name = id.slice(1).replace(/[?#].*$/, '').replace(/[^A-Za-z0-9_.-]/g, '_')
564
+ return path.posix.join(GEA_VIRTUAL_MODULE_ROOT, `${name}.ts`)
565
+ }
566
+ if (id.startsWith('\0')) return id
567
+ const withoutQuery = id.replace(/[?#].*$/, '')
568
+ return path.isAbsolute(withoutQuery) ? path.normalize(withoutQuery) : withoutQuery
569
+ }
570
+
571
+ function shouldTrackModule(id) {
572
+ if (!id || id.startsWith('\0')) return false
573
+ return JS_LIKE_RE.test(id)
574
+ }
575
+
576
+ function hashId(id) {
577
+ return crypto.createHash('sha256').update(id).digest('hex').slice(0, 16)
578
+ }