@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.
- package/LICENSE +202 -0
- package/README.md +36 -0
- package/app_frame_perf.cpp +77 -0
- package/css.d.ts +1 -0
- package/events.cpp +67 -0
- package/gea_app_entry.cpp +317 -0
- package/gea_sources.mjs +126 -0
- package/gea_sources.sh +36 -0
- package/include/app.h +60 -0
- package/include/apps.h +46 -0
- package/include/audio.h +90 -0
- package/include/ble.h +212 -0
- package/include/bluetooth/hid_profile.h +92 -0
- package/include/camera.h +107 -0
- package/include/canvas.h +290 -0
- package/include/display.h +390 -0
- package/include/display_present.h +869 -0
- package/include/event.h +32 -0
- package/include/events.h +116 -0
- package/include/gea/embedded.h +13 -0
- package/include/gea/reactive_runtime.h +32 -0
- package/include/gea.h +14 -0
- package/include/gea_perf_config.h +96 -0
- package/include/geaos/abi.h +165 -0
- package/include/geolocation.h +126 -0
- package/include/gpio.h +61 -0
- package/include/graphics/font.h +122 -0
- package/include/i2c.h +20 -0
- package/include/image.h +156 -0
- package/include/imu.h +23 -0
- package/include/input.h +33 -0
- package/include/memory.h +52 -0
- package/include/pixel.h +907 -0
- package/include/platform/file_cache.h +18 -0
- package/include/power.h +11 -0
- package/include/runtime.h +83 -0
- package/include/services/app_runner.h +10 -0
- package/include/services/app_state.h +14 -0
- package/include/services/battery_service.h +12 -0
- package/include/services/bluetooth_service.h +13 -0
- package/include/services/diagnostics.h +76 -0
- package/include/services/diagnostics_platform.h +54 -0
- package/include/services/frame_scheduler.h +71 -0
- package/include/services/mirror.h +49 -0
- package/include/services/network_services.h +25 -0
- package/include/services/ota.h +32 -0
- package/include/services/runtime_log.h +11 -0
- package/include/services/storage_service.h +26 -0
- package/include/touch.h +38 -0
- package/include/ui/signal.h +149 -0
- package/include/wifi.h +67 -0
- package/index.d.ts +1940 -0
- package/jsx.d.ts +1 -0
- package/package.json +70 -0
- package/runtime/ble.ts +49 -0
- package/runtime/color.ts +21 -0
- package/runtime/compiler.ts +43 -0
- package/runtime/host.ts +1083 -0
- package/runtime/images.ts +129 -0
- package/runtime/primitives.ts +49 -0
- package/runtime-surface.ts +180 -0
- package/runtime.cpp +431 -0
- package/runtime.ts +34 -0
- package/scripts/build-gea-vite-geatsc.mjs +4514 -0
- package/scripts/dotenv-defines.mjs +98 -0
- package/scripts/gea-bundle-type-hints.mjs +546 -0
- package/scripts/gea-embedded-compat-staging.mjs +17 -0
- package/scripts/gea-embedded-compat-transform.mjs +427 -0
- package/scripts/gea-vite-module-graph-plugin.mjs +578 -0
- package/scripts/generate-gea-embedded-assets.mjs +296 -0
- package/scripts/generate-gea-embedded-fonts.mjs +1252 -0
- package/scripts/generate-installed-app-plan.mjs +55 -0
- 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
|
+
}
|