@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,98 @@
1
+ // Minimal .env loader for gea app builds — no `dotenv` dependency.
2
+ //
3
+ // Reads a `.env` file at the root of an app project and turns every variable it
4
+ // declares into Vite `define` entries that inline `import.meta.env.<KEY>` and
5
+ // the legacy `process.env.<KEY>` spelling as string literals at build time.
6
+ // Both the embedded build (build-gea-vite-geatsc.mjs,
7
+ // which compiles the Vite bundle to C++ via geatsc) and the web/simulator build
8
+ // use this, so an app can just reference `process.env.MY_VAR` and it "just works"
9
+ // on device and in the browser — exactly like Node inlines env vars, but resolved
10
+ // at compile time. Only keys actually present in the file are injected; nothing
11
+ // from the ambient shell environment leaks into the bundle.
12
+
13
+ import fs from 'node:fs'
14
+ import path from 'node:path'
15
+
16
+ // Parse an env file into a plain { KEY: value } object. Supports comments (#),
17
+ // blank lines, optional `export ` prefixes, and single/double-quoted values.
18
+ // Returns {} if the file is absent.
19
+ export function readDotEnv(dir, file = '.env') {
20
+ const out = {}
21
+ let text
22
+ try {
23
+ text = fs.readFileSync(path.join(dir, file), 'utf8')
24
+ } catch {
25
+ return out
26
+ }
27
+ for (const rawLine of text.split(/\r?\n/)) {
28
+ const line = rawLine.trim()
29
+ if (!line || line.startsWith('#')) continue
30
+ const eq = line.indexOf('=')
31
+ if (eq === -1) continue
32
+ let key = line.slice(0, eq).trim()
33
+ if (key.startsWith('export ')) key = key.slice('export '.length).trim()
34
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue
35
+ let value = line.slice(eq + 1).trim()
36
+ const quoted = value.length >= 2 && ((value[0] === '"' && value.endsWith('"')) || (value[0] === "'" && value.endsWith("'")))
37
+ if (quoted) value = value.slice(1, -1)
38
+ out[key] = value
39
+ }
40
+ return out
41
+ }
42
+
43
+ // Wrap a raw string value in double quotes and escape the characters that would
44
+ // otherwise break (or inject into) a JS string literal. The value gets pasted
45
+ // verbatim into source code as the replacement for `process.env.<KEY>`, so it
46
+ // must already be a valid quoted literal.
47
+ function quoteJsString(value) {
48
+ let out = '"'
49
+ for (const ch of String(value)) {
50
+ const code = ch.charCodeAt(0)
51
+ if (ch === '\\') out += '\\\\'
52
+ else if (ch === '"') out += '\\"'
53
+ else if (ch === '\n') out += '\\n'
54
+ else if (ch === '\r') out += '\\r'
55
+ else if (ch === '\t') out += '\\t'
56
+ else if (code < 0x20) out += `\\u${code.toString(16).padStart(4, '0')}`
57
+ else out += ch
58
+ }
59
+ return out + '"'
60
+ }
61
+
62
+ // Vite `define` map that replaces each `process.env.<KEY>` with the value's
63
+ // string literal. Vite `define` substitutes the value verbatim into the code, so
64
+ // it must already be a quoted literal.
65
+ //
66
+ // The set of keys is the union of `.env` and `.env.example`: `.env.example`
67
+ // declares the contract (which vars the app reads), so any key it lists is always
68
+ // defined — defaulting to "" when `.env` is missing or omits it. That keeps builds
69
+ // working with no `.env` present (the app just sees empty strings), while `.env`
70
+ // supplies the real values.
71
+ export function dotEnvDefines(dir) {
72
+ const values = readDotEnv(dir, '.env')
73
+ const keys = new Set([...Object.keys(values), ...Object.keys(readDotEnv(dir, '.env.example'))])
74
+ const defines = {}
75
+ for (const key of keys) {
76
+ const literal = quoteJsString(values[key] ?? '')
77
+ defines[`import.meta.env.${key}`] = literal
78
+ defines[`process.env.${key}`] = literal
79
+ }
80
+ return defines
81
+ }
82
+
83
+ // Bake `process.env.<KEY>` references into a source string as literals, using the
84
+ // same map dotEnvDefines() produces. This runs on the STAGED app sources before
85
+ // Vite reads them — the embedded build compiles the module-graph snapshots
86
+ // (original + transformed) that geatsc captures, and Vite's `define` only rewrites
87
+ // the final bundle, not those snapshots. Substituting at the source keeps all three
88
+ // (original, transformed, bundle) consistent.
89
+ export function inlineProcessEnv(code, defines) {
90
+ let out = code
91
+ for (const [expr, literal] of Object.entries(defines)) {
92
+ // Match the exact member expression, not a longer identifier
93
+ // (process.env.FOO must not match inside process.env.FOOBAR).
94
+ const escaped = expr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
95
+ out = out.replace(new RegExp(`${escaped}(?![\\w$])`, 'g'), literal)
96
+ }
97
+ return out
98
+ }
@@ -0,0 +1,546 @@
1
+ // JSDoc type-hint RESTORATION for the bundled-app pipeline.
2
+ //
3
+ // Vite bundling erases TS annotations; build-gea-vite-geatsc.mjs collects the
4
+ // source annotations (collectBundledTypeHints) and this module re-applies them
5
+ // to the bundle as JSDoc so the geatsc checker recovers the static types.
6
+ // Extracted from build-gea-vite-geatsc.mjs so the apply side is unit-testable
7
+ // (see packages/geatsc/test/gea-bundle-type-hints.test.ts) — a silent
8
+ // restoration miss downgrades typed slots to gea_cpp_value with no compile
9
+ // error, only boxed codegen.
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+ import { createRequire } from 'node:module'
13
+
14
+ const requireFromLib = createRequire(new URL('../package.json', import.meta.url))
15
+ const { parse } = requireFromLib('@babel/parser')
16
+ const traverseModule = requireFromLib('@babel/traverse')
17
+ const t = requireFromLib('@babel/types')
18
+ const traverse = traverseModule.default || traverseModule
19
+
20
+ // Scope key for a module-level (non-function) typed binding in the hint maps.
21
+ export const MODULE_HINT_SCOPE = '<module>'
22
+
23
+ export function parseTsx(code) {
24
+ return parse(code, {
25
+ sourceType: 'module',
26
+ plugins: ['jsx', 'typescript'],
27
+ })
28
+ }
29
+
30
+ function nearestPackageJson(startDir) {
31
+ let current = startDir
32
+ while (current && current !== path.dirname(current)) {
33
+ const candidate = path.join(current, 'package.json')
34
+ if (fs.existsSync(candidate)) return candidate
35
+ current = path.dirname(current)
36
+ }
37
+ return null
38
+ }
39
+
40
+ function selectPackageImportTarget(value) {
41
+ if (typeof value === 'string') return value
42
+ if (Array.isArray(value)) {
43
+ for (const item of value) {
44
+ const target = selectPackageImportTarget(item)
45
+ if (target) return target
46
+ }
47
+ return null
48
+ }
49
+ if (!value || typeof value !== 'object') return null
50
+ for (const key of ['default', 'import', 'node', 'types']) {
51
+ const target = selectPackageImportTarget(value[key])
52
+ if (target) return target
53
+ }
54
+ for (const item of Object.values(value)) {
55
+ const target = selectPackageImportTarget(item)
56
+ if (target) return target
57
+ }
58
+ return null
59
+ }
60
+
61
+ function resolvePackageImportTarget(imports, source) {
62
+ if (!imports || typeof imports !== 'object') return null
63
+ const exactTarget = selectPackageImportTarget(imports[source])
64
+ if (exactTarget) return exactTarget
65
+ for (const [key, value] of Object.entries(imports)) {
66
+ const starIndex = key.indexOf('*')
67
+ if (starIndex === -1) continue
68
+ const prefix = key.slice(0, starIndex)
69
+ const suffix = key.slice(starIndex + 1)
70
+ if (!source.startsWith(prefix) || !source.endsWith(suffix)) continue
71
+ const target = selectPackageImportTarget(value)
72
+ if (!target) continue
73
+ const matched = source.slice(prefix.length, source.length - suffix.length)
74
+ return target.replaceAll('*', matched)
75
+ }
76
+ return null
77
+ }
78
+
79
+ function parseBarePackageSpecifier(source) {
80
+ if (!source || source.startsWith('.') || source.startsWith('#') || source.startsWith('/')) return null
81
+ const parts = source.split('/')
82
+ if (source.startsWith('@')) {
83
+ if (parts.length < 2) return null
84
+ return {
85
+ packageName: `${parts[0]}/${parts[1]}`,
86
+ subpath: parts.length > 2 ? `./${parts.slice(2).join('/')}` : '.',
87
+ }
88
+ }
89
+ return {
90
+ packageName: parts[0],
91
+ subpath: parts.length > 1 ? `./${parts.slice(1).join('/')}` : '.',
92
+ }
93
+ }
94
+
95
+ function packageDependencySpec(packageJson, packageName) {
96
+ for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
97
+ const value = packageJson?.[field]?.[packageName]
98
+ if (typeof value === 'string') return value
99
+ }
100
+ return null
101
+ }
102
+
103
+ function resolvePackageExportBase(packageDir, packageJson, subpath) {
104
+ const exportsTarget = resolvePackageImportTarget(packageJson.exports, subpath)
105
+ if (exportsTarget && exportsTarget.startsWith('.')) return path.resolve(packageDir, exportsTarget)
106
+ if (subpath !== '.') return path.resolve(packageDir, subpath.slice(2))
107
+ for (const key of ['source', 'module', 'main', 'types']) {
108
+ const target = packageJson[key]
109
+ if (typeof target === 'string') return path.resolve(packageDir, target)
110
+ }
111
+ return path.resolve(packageDir, 'index')
112
+ }
113
+
114
+ function resolveFileDependencyImportBase(source, srcId) {
115
+ const parsed = parseBarePackageSpecifier(source)
116
+ if (!parsed) return null
117
+ const importingPackageJsonPath = nearestPackageJson(path.dirname(srcId))
118
+ if (!importingPackageJsonPath) return null
119
+ let importingPackageJson
120
+ try {
121
+ importingPackageJson = JSON.parse(fs.readFileSync(importingPackageJsonPath, 'utf8'))
122
+ } catch {
123
+ return null
124
+ }
125
+ const spec = packageDependencySpec(importingPackageJson, parsed.packageName)
126
+ if (!spec?.startsWith('file:')) return null
127
+ const packageDir = path.resolve(path.dirname(importingPackageJsonPath), spec.slice('file:'.length))
128
+ const packageJsonPath = path.join(packageDir, 'package.json')
129
+ if (!fs.existsSync(packageJsonPath)) return null
130
+ let packageJson
131
+ try {
132
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
133
+ } catch {
134
+ return null
135
+ }
136
+ return resolvePackageExportBase(packageDir, packageJson, parsed.subpath)
137
+ }
138
+
139
+ function resolveLocalImportBase(source, srcId) {
140
+ if (source.startsWith('.')) return path.resolve(path.dirname(srcId), source)
141
+ if (!source.startsWith('#')) return resolveFileDependencyImportBase(source, srcId)
142
+ const packageJsonPath = nearestPackageJson(path.dirname(srcId))
143
+ if (!packageJsonPath) return null
144
+ let packageJson
145
+ try {
146
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
147
+ } catch {
148
+ return null
149
+ }
150
+ const target = resolvePackageImportTarget(packageJson.imports, source)
151
+ if (!target || !target.startsWith('.')) return null
152
+ return path.resolve(path.dirname(packageJsonPath), target)
153
+ }
154
+
155
+ export function discoverLocalImport(source, srcId, localImports) {
156
+ if (source.endsWith('.css')) return
157
+ const base = resolveLocalImportBase(source, srcId)
158
+ if (!base) return
159
+ for (const candidate of [
160
+ base,
161
+ base + '.tsx',
162
+ base + '.ts',
163
+ base + '.jsx',
164
+ base + '.js',
165
+ path.resolve(base, 'index.tsx'),
166
+ path.resolve(base, 'index.ts'),
167
+ path.resolve(base, 'index.jsx'),
168
+ path.resolve(base, 'index.js'),
169
+ ]) {
170
+ // A directory import resolves `base` to the directory itself — only a
171
+ // FILE candidate is loadable; directories fall through to index.*.
172
+ if (fs.statSync(candidate, { throwIfNoEntry: false })?.isFile()) {
173
+ // Only enqueue JS/TS modules. The bare `base` candidate can be an
174
+ // extension-carrying ASSET import (troika fonts: `import url from
175
+ // './x.woff'`) — every walker that consumes this list parses the file,
176
+ // and feeding a binary asset to the TS parser blows up on its first
177
+ // non-ASCII byte (`wOFF` header → "Unexpected character" at column 4).
178
+ // Assets aren't parseable modules; skip them (the added-extension
179
+ // candidates below are always JS/TS, so a genuine module still resolves).
180
+ if (!/\.(?:mjs|cjs|jsx?|tsx?)$/i.test(candidate)) break
181
+ localImports.push(candidate)
182
+ break
183
+ }
184
+ }
185
+ }
186
+
187
+ export function collectSourceFiles(appDir) {
188
+ const transformable = new Set(['.js', '.jsx', '.ts', '.tsx'])
189
+ const files = []
190
+ const stack = [appDir]
191
+ while (stack.length > 0) {
192
+ const current = stack.pop()
193
+ if (!current || !fs.existsSync(current)) continue
194
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
195
+ const full = path.join(current, entry.name)
196
+ if (entry.isDirectory()) {
197
+ if (['node_modules', 'dist', 'build', '.vite'].includes(entry.name)) continue
198
+ stack.push(full)
199
+ continue
200
+ }
201
+ if (entry.isFile() && transformable.has(path.extname(entry.name))) files.push(full)
202
+ }
203
+ }
204
+ files.sort()
205
+ return files
206
+ }
207
+
208
+ // Only the modules actually REACHABLE from the build entry contribute hints.
209
+ // Apps ship alternate entries (maps' index.vector.tsx next to index.device.tsx)
210
+ // whose same-named functions otherwise OVERWRITE the live module's hints in the
211
+ // name-keyed maps — the restored JSDoc then carries the other entry's param
212
+ // names/types, silently fails to match the bundled function, and the param
213
+ // widens to gea_cpp_value.
214
+ export function collectReachableSourceFiles(appDir, entry) {
215
+ const start = path.resolve(appDir, entry)
216
+ if (!fs.existsSync(start)) return collectSourceFiles(appDir)
217
+ const pending = [start]
218
+ const seen = new Set()
219
+ while (pending.length > 0) {
220
+ const file = pending.pop()
221
+ if (!file || seen.has(file) || !fs.existsSync(file)) continue
222
+ seen.add(file)
223
+ let ast
224
+ try {
225
+ ast = parseTsx(fs.readFileSync(file, 'utf8'))
226
+ } catch {
227
+ continue
228
+ }
229
+ traverse(ast, {
230
+ ImportDeclaration(astPath) {
231
+ discoverLocalImport(astPath.node.source.value, file, pending)
232
+ },
233
+ ExportNamedDeclaration(astPath) {
234
+ const source = astPath.node.source?.value
235
+ if (typeof source === 'string') discoverLocalImport(source, file, pending)
236
+ },
237
+ ExportAllDeclaration(astPath) {
238
+ const source = astPath.node.source?.value
239
+ if (typeof source === 'string') discoverLocalImport(source, file, pending)
240
+ },
241
+ })
242
+ }
243
+ return [...seen].sort()
244
+ }
245
+
246
+ export function insertionIndent(code, offset) {
247
+ const lineStart = code.lastIndexOf('\n', offset - 1) + 1
248
+ return code.slice(lineStart, offset).match(/^[ \t]*/)?.[0] ?? ''
249
+ }
250
+
251
+ // Resolve a class member's owning class name in the BUNDLE, where source
252
+ // `class WeatherStore extends Store {}` is emitted as the anonymous expression
253
+ // `var WeatherStore = class extends Store {}`. Prefer the class node's own id,
254
+ // then fall back to the enclosing `var <Name> = class …` declarator.
255
+ export function resolveBundledClassName(astPath) {
256
+ const classPath =
257
+ astPath.isClassDeclaration?.() || astPath.isClassExpression?.()
258
+ ? astPath
259
+ : astPath.findParent((p) => p.isClassDeclaration() || p.isClassExpression())
260
+ if (!classPath) return undefined
261
+ if (classPath.node?.id?.name) return classPath.node.id.name
262
+ const declaratorPath = classPath.findParent((p) => p.isVariableDeclarator())
263
+ if (declaratorPath && t.isIdentifier(declaratorPath.node.id)) return declaratorPath.node.id.name
264
+ return undefined
265
+ }
266
+
267
+ export function resolveFunctionHintName(astPath) {
268
+ const functionPath = astPath.getFunctionParent?.()
269
+ const node = functionPath?.node
270
+ if (!node) return undefined
271
+ if (t.isFunctionDeclaration(node) && node.id?.name) return node.id.name
272
+ if ((t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) && functionPath) {
273
+ const declaratorPath = functionPath.findParent((p) => p.isVariableDeclarator())
274
+ if (declaratorPath && t.isIdentifier(declaratorPath.node.id)) return declaratorPath.node.id.name
275
+ }
276
+ if (functionPath?.isClassMethod?.() || functionPath?.isClassPrivateMethod?.()) {
277
+ const method = functionPath.node
278
+ if (!t.isIdentifier(method.key)) return undefined
279
+ const className = resolveBundledClassName(functionPath)
280
+ return className ? `${className}#${method.key.name}` : undefined
281
+ }
282
+ return undefined
283
+ }
284
+
285
+ // Module-graph-snapshot counterpart of the `Class#el` bundle hint. The lean
286
+ // ReactiveComponent transform (vite-plugin-gea) strips the
287
+ // `extends ReactiveComponent<T>` heritage and re-injects the inherited `el`
288
+ // slot as a bare `el = null` class field — annotation-less, because the
289
+ // transform has no type context. The snapshot is otherwise fully typed
290
+ // (captured pre-type-strip), so this one field is what separates `this.el`
291
+ // from its native NodeHandle lowering: an unannotated `el` types as `any`,
292
+ // stores boxed, and the boxed record carries no `__gea_node_id` — so
293
+ // `this.el.getContext('2d')` binds the canvas context to node -1 and every
294
+ // draw silently no-ops. Re-attach the `T | null` annotation recovered from
295
+ // the app source's heritage generic as a REAL TS annotation: snapshots are
296
+ // .tsx, where JSDoc `@type` is ignored by the checker.
297
+ export function restoreModuleGraphElAnnotations(moduleGraphDir, propertyHints) {
298
+ const manifestPath = path.join(moduleGraphDir, 'gea-module-graph.json')
299
+ if (!fs.existsSync(manifestPath)) return
300
+ const elHints = new Map()
301
+ for (const [key, typeText] of propertyHints ?? []) {
302
+ const match = /^([A-Za-z_$][\w$]*)#el$/.exec(key)
303
+ if (match) elHints.set(match[1], typeText)
304
+ }
305
+ if (elHints.size === 0) return
306
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
307
+ const manifestDir = path.dirname(manifestPath)
308
+ const snapshotPaths = new Set()
309
+ for (const module of manifest.modules ?? []) {
310
+ for (const source of [module.originalSource, module.transformedSource]) {
311
+ if (source) snapshotPaths.add(path.resolve(manifestDir, source))
312
+ }
313
+ }
314
+ for (const snapshotPath of snapshotPaths) {
315
+ if (!fs.existsSync(snapshotPath)) continue
316
+ const code = fs.readFileSync(snapshotPath, 'utf8')
317
+ if (!code.includes('el = null')) continue
318
+ const patched = annotateInjectedElFields(code, elHints)
319
+ if (patched !== code) fs.writeFileSync(snapshotPath, patched)
320
+ }
321
+ }
322
+
323
+ function annotateInjectedElFields(code, elHints) {
324
+ let ast
325
+ try {
326
+ ast = parseTsx(code)
327
+ } catch {
328
+ return code
329
+ }
330
+ const insertions = []
331
+ traverse(ast, {
332
+ Class(astPath) {
333
+ const className = resolveBundledClassName(astPath)
334
+ const typeText = className ? elHints.get(className) : undefined
335
+ if (!typeText) return
336
+ // Every type name the annotation references must already resolve in the
337
+ // snapshot (surviving `import type` line, or a local alias/interface) —
338
+ // an unresolvable name would trade a boxed `el` for a checker error.
339
+ if (!hintTypeNamesResolvable(code, typeText)) return
340
+ for (const member of astPath.node.body.body) {
341
+ if (
342
+ t.isClassProperty(member) &&
343
+ !member.computed &&
344
+ t.isIdentifier(member.key, { name: 'el' }) &&
345
+ !member.typeAnnotation &&
346
+ t.isNullLiteral(member.value) &&
347
+ member.key.end != null
348
+ ) {
349
+ insertions.push({ offset: member.key.end, text: `: ${typeText}` })
350
+ }
351
+ }
352
+ },
353
+ })
354
+ let patched = code
355
+ for (const { offset, text } of insertions.sort((a, b) => b.offset - a.offset)) {
356
+ patched = `${patched.slice(0, offset)}${text}${patched.slice(offset)}`
357
+ }
358
+ return patched
359
+ }
360
+
361
+ // DOM-lib element names resolve ambiently (no import) in the geatsc checker.
362
+ const AMBIENT_HINT_TYPE_NAMES = new Set([
363
+ 'Node',
364
+ 'Element',
365
+ 'HTMLElement',
366
+ 'HTMLCanvasElement',
367
+ 'DocumentFragment',
368
+ 'Text',
369
+ 'Comment',
370
+ ])
371
+
372
+ function hintTypeNamesResolvable(code, typeText) {
373
+ const names = new Set(typeText.match(/\b[A-Za-z_$][\w$]*\b/g) ?? [])
374
+ names.delete('null')
375
+ names.delete('undefined')
376
+ for (const name of names) {
377
+ if (AMBIENT_HINT_TYPE_NAMES.has(name)) continue
378
+ const namePattern = name.replace(/\$/g, '\\$')
379
+ const importedOrDeclared = new RegExp(
380
+ `(^|\\n)\\s*import\\b[^\\n;]*\\b${namePattern}\\b|\\b(?:type|interface|class)\\s+${namePattern}\\b`
381
+ )
382
+ if (!importedOrDeclared.test(code)) return false
383
+ }
384
+ return true
385
+ }
386
+
387
+ export function applyBundledTypeHints(code, hints, referencePath) {
388
+ const propertyHints = hints.propertyHints ?? new Map()
389
+ const localHints = hints.localHints ?? new Map()
390
+ const inlineCallableHints = hints.inlineCallableHints ?? new Map()
391
+ const classHints = hints.classHints ?? new Map()
392
+ if (
393
+ hints.returnHints.size === 0 &&
394
+ hints.paramHints.size === 0 &&
395
+ propertyHints.size === 0 &&
396
+ localHints.size === 0 &&
397
+ inlineCallableHints.size === 0 &&
398
+ classHints.size === 0
399
+ )
400
+ return code
401
+ const ast = parse(code, {
402
+ sourceType: 'unambiguous',
403
+ plugins: ['jsx'],
404
+ })
405
+ const insertions = new Map()
406
+ const jsDocForFunction = (name) => {
407
+ const params = hints.paramHints.get(name) ?? []
408
+ const returnType = hints.returnHints.get(name)
409
+ if (params.length === 0 && !returnType) return null
410
+ const lines = ['/**']
411
+ // JSDoc bracket syntax marks optional params, so the checker types them
412
+ // `T | undefined` and geatsc lowers a null-capable native slot.
413
+ for (const param of params) lines.push(` * @param {${param.type}} ${param.optional ? `[${param.name}]` : param.name}`)
414
+ if (returnType) lines.push(` * @returns {${returnType}}`)
415
+ lines.push(' */')
416
+ return lines.join('\n')
417
+ }
418
+ // SINGLE-LINE JSDoc for inline callables: a newline between `return` and the
419
+ // arrow it precedes would trigger ASI (`return;`), so the comment and its
420
+ // trailing separator must stay on the callable's own line. TS still parses
421
+ // multiple `@param` tags packed on one line.
422
+ const jsDocForInlineCallable = (entry) => {
423
+ const parts = entry.params.map((param) => `@param {${param.type}} ${param.optional ? `[${param.name}]` : param.name}`)
424
+ if (entry.returnType) parts.push(`@returns {${entry.returnType}}`)
425
+ if (parts.length === 0) return null
426
+ return `/** ${parts.join(' ')} */`
427
+ }
428
+ const jsDocForClass = (name) => {
429
+ const tag = classHints.get(name)
430
+ return tag ? `/** ${tag} */` : null
431
+ }
432
+ const inlineConsumed = new Map()
433
+ const normalizedJsDocLine = (line) => line.trim().replace(/\s+/g, ' ')
434
+ const mergeIntoLeadingJsDoc = (offset, jsDocText) => {
435
+ const tagLines = jsDocText
436
+ .split('\n')
437
+ .slice(1, -1)
438
+ .filter((line) => line.trim().startsWith('* @'))
439
+ if (tagLines.length === 0) return false
440
+ const before = code.slice(0, offset)
441
+ const bodyEnd = before.trimEnd().length
442
+ if (bodyEnd < 2 || before.slice(bodyEnd - 2, bodyEnd) !== '*/') return false
443
+ const blockStart = before.lastIndexOf('/**', bodyEnd - 2)
444
+ if (blockStart < 0) return false
445
+ const block = code.slice(blockStart, bodyEnd)
446
+ if (!block.includes('\n')) return false
447
+ const existingLines = new Set(block.split('\n').map(normalizedJsDocLine))
448
+ const missingLines = tagLines.filter((line) => !existingLines.has(normalizedJsDocLine(line)))
449
+ if (missingLines.length === 0) return true
450
+ const closeLineStart = code.lastIndexOf('\n', bodyEnd - 2) + 1
451
+ if (closeLineStart <= blockStart) return false
452
+ const current = insertions.get(closeLineStart) ?? ''
453
+ insertions.set(closeLineStart, `${current}${missingLines.join('\n')}\n`)
454
+ return true
455
+ }
456
+ const visitInlineCallable = (astPath) => {
457
+ const node = astPath.node
458
+ const parent = astPath.parent
459
+ if (parent && t.isVariableDeclarator(parent) && parent.init === node) return
460
+ if (!node.params.every((p) => t.isIdentifier(p))) return
461
+ const scope = resolveFunctionHintName(astPath) ?? MODULE_HINT_SCOPE
462
+ const key = `${scope}|${node.params.map((p) => p.name).join(',')}`
463
+ const entries = inlineCallableHints.get(key)
464
+ if (!entries || entries.length === 0) return
465
+ const index = inlineConsumed.get(key) ?? 0
466
+ if (index >= entries.length) return
467
+ inlineConsumed.set(key, index + 1)
468
+ const jsDocText = jsDocForInlineCallable(entries[index])
469
+ if (jsDocText && node.start != null && !insertions.has(node.start)) {
470
+ insertions.set(node.start, `${jsDocText} `)
471
+ }
472
+ }
473
+ const addInsertion = (offset, jsDocText) => {
474
+ if (offset == null || insertions.has(offset)) return
475
+ if (jsDocText.startsWith('/**') && mergeIntoLeadingJsDoc(offset, jsDocText)) return
476
+ insertions.set(offset, `${jsDocText}\n${insertionIndent(code, offset)}`)
477
+ }
478
+ traverse(ast, {
479
+ FunctionDeclaration(astPath) {
480
+ const node = astPath.node
481
+ const name = node.id?.name
482
+ if (!name) return
483
+ const jsDocText = jsDocForFunction(name)
484
+ if (jsDocText) addInsertion(node.start, jsDocText)
485
+ },
486
+ ArrowFunctionExpression(astPath) {
487
+ visitInlineCallable(astPath)
488
+ },
489
+ FunctionExpression(astPath) {
490
+ visitInlineCallable(astPath)
491
+ },
492
+ VariableDeclaration(astPath) {
493
+ for (const declaration of astPath.node.declarations) {
494
+ if (!t.isIdentifier(declaration.id)) continue
495
+ if (!declaration.init || (!t.isArrowFunctionExpression(declaration.init) && !t.isFunctionExpression(declaration.init))) continue
496
+ const jsDocText = jsDocForFunction(declaration.id.name)
497
+ if (jsDocText) addInsertion(astPath.node.start, jsDocText)
498
+ }
499
+ },
500
+ ClassMethod(astPath) {
501
+ const node = astPath.node
502
+ if ((node.kind !== 'method' && node.kind !== 'constructor') || !t.isIdentifier(node.key)) return
503
+ // The bundle emits `var TetrisStore = class extends Store {}` — an
504
+ // ANONYMOUS class expression. Resolve through the enclosing declarator
505
+ // (resolveBundledClassName), or no class-method hint ever applies and
506
+ // unannotated method RETURNS fall back to gea_cpp_value (params often
507
+ // survive via call-site inference, masking the loss).
508
+ const className = resolveBundledClassName(astPath)
509
+ if (!className) return
510
+ const memberName = node.kind === 'constructor' ? 'constructor' : node.key.name
511
+ const jsDocText = jsDocForFunction(`${className}#${memberName}`)
512
+ if (jsDocText) addInsertion(node.start, jsDocText)
513
+ },
514
+ Class(astPath) {
515
+ const node = astPath.node
516
+ const className = resolveBundledClassName(astPath)
517
+ if (!className) return
518
+ const jsDocText = jsDocForClass(className)
519
+ if (jsDocText) addInsertion(node.start, jsDocText)
520
+ },
521
+ ClassProperty(astPath) {
522
+ const node = astPath.node
523
+ if (node.computed || !t.isIdentifier(node.key)) return
524
+ const className = resolveBundledClassName(astPath)
525
+ if (!className) return
526
+ const typeText = propertyHints.get(`${className}#${node.key.name}`)
527
+ if (typeText) addInsertion(node.start, `/** @type {${typeText}} */`)
528
+ },
529
+ VariableDeclarator(astPath) {
530
+ const node = astPath.node
531
+ if (!t.isIdentifier(node.id)) return
532
+ const declarationPath = astPath.parentPath
533
+ if (!declarationPath?.isVariableDeclaration?.()) return
534
+ if (declarationPath.node.declarations.length !== 1) return
535
+ const functionName = resolveFunctionHintName(astPath) ?? MODULE_HINT_SCOPE
536
+ const typeText = localHints.get(`${functionName}#${node.id.name}`)
537
+ if (typeText) addInsertion(declarationPath.node.start, `/** @type {${typeText}} */`)
538
+ },
539
+ })
540
+ let out = code
541
+ for (const [offset, text] of [...insertions].sort((a, b) => b[0] - a[0])) {
542
+ out = `${out.slice(0, offset)}${text}${out.slice(offset)}`
543
+ }
544
+ if (!referencePath) return out
545
+ return `/// <reference path="${referencePath}" />\n${out}`
546
+ }
@@ -0,0 +1,17 @@
1
+ export const COMPAT_STAGING_IGNORED_DIRECTORIES = Object.freeze([
2
+ 'node_modules',
3
+ 'dist',
4
+ 'build',
5
+ '.build',
6
+ '.build-test',
7
+ '.vite',
8
+ '.scratch',
9
+ '.test-tmp',
10
+ 'generated-output',
11
+ ])
12
+
13
+ const ignoredDirectorySet = new Set(COMPAT_STAGING_IGNORED_DIRECTORIES)
14
+
15
+ export function shouldIgnoreCompatStagingDirectory(name) {
16
+ return ignoredDirectorySet.has(name)
17
+ }