@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,427 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
|
|
5
|
+
const require = createRequire(new URL('../vendor/gea/package.json', import.meta.url))
|
|
6
|
+
const parser = require('@babel/parser')
|
|
7
|
+
const traverse = require('@babel/traverse').default
|
|
8
|
+
const generate = require('@babel/generator').default
|
|
9
|
+
const t = require('@babel/types')
|
|
10
|
+
|
|
11
|
+
const ignoredKeys = new Set([
|
|
12
|
+
'loc',
|
|
13
|
+
'start',
|
|
14
|
+
'end',
|
|
15
|
+
'leadingComments',
|
|
16
|
+
'innerComments',
|
|
17
|
+
'trailingComments',
|
|
18
|
+
'extra',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
function hasJsx(node) {
|
|
22
|
+
if (!node || typeof node !== 'object') return false
|
|
23
|
+
if (Array.isArray(node)) return node.some(hasJsx)
|
|
24
|
+
if (typeof node.type === 'string' && node.type.startsWith('JSX')) return true
|
|
25
|
+
for (const key of Object.keys(node)) {
|
|
26
|
+
if (ignoredKeys.has(key)) continue
|
|
27
|
+
if (hasJsx(node[key])) return true
|
|
28
|
+
}
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isComponentName(name) {
|
|
33
|
+
return /^[A-Z]/.test(name)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isFunctionComponent(fn, name) {
|
|
37
|
+
return !!fn && isComponentName(name) && hasJsx(fn.body)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function functionTemplateBody(fn) {
|
|
41
|
+
if (t.isBlockStatement(fn.body)) {
|
|
42
|
+
return fn.body.body.map((stmt) => t.cloneNode(stmt, true))
|
|
43
|
+
}
|
|
44
|
+
return [t.returnStatement(t.cloneNode(fn.body, true))]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function componentClassFromFunction(name, fn) {
|
|
48
|
+
const params = fn.params.map((param) => t.cloneNode(param, true))
|
|
49
|
+
const method = t.classMethod(
|
|
50
|
+
'method',
|
|
51
|
+
t.identifier('template'),
|
|
52
|
+
params,
|
|
53
|
+
t.blockStatement(functionTemplateBody(fn)),
|
|
54
|
+
)
|
|
55
|
+
if (fn.returnType) method.returnType = t.cloneNode(fn.returnType, true)
|
|
56
|
+
const declaration = t.classDeclaration(t.identifier(name), t.identifier('Component'), t.classBody([method]))
|
|
57
|
+
// A function component's props are its first parameter's type, and the class
|
|
58
|
+
// this becomes has to say so: `Component`'s second type parameter is where a
|
|
59
|
+
// component states them, and it defaults to `void`. Leaving it defaulted made
|
|
60
|
+
// the generated `template({ label, value }: { ... })` an override whose
|
|
61
|
+
// parameter is neither assignable to `void` nor assignable from it -- a
|
|
62
|
+
// checker error on every function component that takes props, which is what
|
|
63
|
+
// the settings/hid-clicker/app-launcher pipelines hit. The type is COPIED
|
|
64
|
+
// from the parameter rather than inferred, so the two spellings cannot drift.
|
|
65
|
+
const propsType = params[0]?.typeAnnotation
|
|
66
|
+
if (propsType && t.isTSTypeAnnotation(propsType)) {
|
|
67
|
+
declaration.superTypeParameters = t.tsTypeParameterInstantiation([
|
|
68
|
+
t.tsTypeReference(t.identifier('GeaElement')),
|
|
69
|
+
t.cloneNode(propsType.typeAnnotation, true),
|
|
70
|
+
])
|
|
71
|
+
}
|
|
72
|
+
return declaration
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function componentClassFromVariableDeclarator(declarator) {
|
|
76
|
+
if (!t.isIdentifier(declarator.id)) return null
|
|
77
|
+
const name = declarator.id.name
|
|
78
|
+
const init = declarator.init
|
|
79
|
+
if (!isFunctionComponent(init, name)) return null
|
|
80
|
+
return componentClassFromFunction(name, init)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// `GeaElement` is an interface, so it must be imported `type`-only: the staged
|
|
84
|
+
// tree is bundled as well as checked, and a value import of a name the runtime
|
|
85
|
+
// module does not export at runtime is an unresolved export, not a no-op.
|
|
86
|
+
function runtimeImportSpecifier(name) {
|
|
87
|
+
const specifier = t.importSpecifier(t.identifier(name), t.identifier(name))
|
|
88
|
+
if (name === 'GeaElement') specifier.importKind = 'type'
|
|
89
|
+
return specifier
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The names a generated component class spells: `Component` always, and
|
|
93
|
+
// `GeaElement` only when one of them states props -- it is the first type
|
|
94
|
+
// argument you have to pass in order to reach the second. `runtime.ts`
|
|
95
|
+
// re-exports it (via `runtime-surface.ts`) for exactly this kind of reason, so
|
|
96
|
+
// it resolves from the same module `Component` does.
|
|
97
|
+
function ensureComponentImport(ast, alsoGeaElement) {
|
|
98
|
+
const wanted = alsoGeaElement ? ['Component', 'GeaElement'] : ['Component']
|
|
99
|
+
for (const node of ast.program.body) {
|
|
100
|
+
if (!t.isImportDeclaration(node)) continue
|
|
101
|
+
if (node.source.value !== 'gea-embedded' && node.source.value !== '@geajs/core') continue
|
|
102
|
+
const present = new Set(
|
|
103
|
+
node.specifiers.filter((spec) => t.isImportSpecifier(spec)).map((spec) => spec.imported.name),
|
|
104
|
+
)
|
|
105
|
+
for (const name of wanted) {
|
|
106
|
+
if (!present.has(name)) node.specifiers.push(runtimeImportSpecifier(name))
|
|
107
|
+
}
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ast.program.body.unshift(
|
|
112
|
+
t.importDeclaration(wanted.map(runtimeImportSpecifier), t.stringLiteral('gea-embedded')),
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function runtimeCoreSpecifierName(spec) {
|
|
117
|
+
if (!t.isImportSpecifier(spec)) return null
|
|
118
|
+
const imported = spec.imported
|
|
119
|
+
const name = t.isIdentifier(imported) ? imported.name : imported.value
|
|
120
|
+
return name === 'Component' || name === 'Store' ? name : null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function importHasSpecifier(node, importedName, localName) {
|
|
124
|
+
return node.specifiers.some((spec) => {
|
|
125
|
+
if (!t.isImportSpecifier(spec)) return false
|
|
126
|
+
const imported = spec.imported
|
|
127
|
+
const name = t.isIdentifier(imported) ? imported.name : imported.value
|
|
128
|
+
return name === importedName && spec.local.name === localName
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function ensureGeaEmbeddedRuntimeImport(ast, specs) {
|
|
133
|
+
let target = ast.program.body.find(
|
|
134
|
+
(node) => t.isImportDeclaration(node) && node.source.value === 'gea-embedded',
|
|
135
|
+
)
|
|
136
|
+
if (!target) {
|
|
137
|
+
target = t.importDeclaration([], t.stringLiteral('gea-embedded'))
|
|
138
|
+
ast.program.body.unshift(target)
|
|
139
|
+
}
|
|
140
|
+
for (const spec of specs) {
|
|
141
|
+
const importedName = runtimeCoreSpecifierName(spec)
|
|
142
|
+
if (!importedName) continue
|
|
143
|
+
if (importHasSpecifier(target, importedName, spec.local.name)) continue
|
|
144
|
+
target.specifiers.push(t.cloneNode(spec, true))
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeRuntimeCoreImports(ast) {
|
|
149
|
+
let changed = false
|
|
150
|
+
const nextBody = []
|
|
151
|
+
const movedSpecs = []
|
|
152
|
+
|
|
153
|
+
for (const node of ast.program.body) {
|
|
154
|
+
if (!t.isImportDeclaration(node) || node.source.value !== '@geastack/core') {
|
|
155
|
+
nextBody.push(node)
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const keep = []
|
|
160
|
+
for (const spec of node.specifiers) {
|
|
161
|
+
if (runtimeCoreSpecifierName(spec)) {
|
|
162
|
+
movedSpecs.push(spec)
|
|
163
|
+
changed = true
|
|
164
|
+
} else {
|
|
165
|
+
keep.push(spec)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (keep.length > 0) {
|
|
170
|
+
node.specifiers = keep
|
|
171
|
+
nextBody.push(node)
|
|
172
|
+
} else {
|
|
173
|
+
changed = true
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!changed) return false
|
|
178
|
+
ast.program.body = nextBody
|
|
179
|
+
ensureGeaEmbeddedRuntimeImport(ast, movedSpecs)
|
|
180
|
+
return true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function transformTopLevelStatement(stmt) {
|
|
184
|
+
if (t.isFunctionDeclaration(stmt) && stmt.id && isFunctionComponent(stmt, stmt.id.name)) {
|
|
185
|
+
return { changed: true, nodes: [componentClassFromFunction(stmt.id.name, stmt)] }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (t.isExportNamedDeclaration(stmt) && t.isFunctionDeclaration(stmt.declaration)) {
|
|
189
|
+
const decl = stmt.declaration
|
|
190
|
+
if (decl.id && isFunctionComponent(decl, decl.id.name)) {
|
|
191
|
+
return {
|
|
192
|
+
changed: true,
|
|
193
|
+
nodes: [t.exportNamedDeclaration(componentClassFromFunction(decl.id.name, decl), [])],
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (t.isVariableDeclaration(stmt) && stmt.declarations.length === 1) {
|
|
199
|
+
const classDecl = componentClassFromVariableDeclarator(stmt.declarations[0])
|
|
200
|
+
if (classDecl) return { changed: true, nodes: [classDecl] }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
t.isExportNamedDeclaration(stmt) &&
|
|
205
|
+
t.isVariableDeclaration(stmt.declaration) &&
|
|
206
|
+
stmt.declaration.declarations.length === 1
|
|
207
|
+
) {
|
|
208
|
+
const classDecl = componentClassFromVariableDeclarator(stmt.declaration.declarations[0])
|
|
209
|
+
if (classDecl) return { changed: true, nodes: [t.exportNamedDeclaration(classDecl, [])] }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { changed: false, nodes: [stmt] }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function transformGeaEmbeddedCompatSource(code, filename) {
|
|
216
|
+
if ((!code.includes('<') || !code.includes('>')) && !code.includes('@geastack/core')) return code
|
|
217
|
+
|
|
218
|
+
const ast = parser.parse(code, {
|
|
219
|
+
sourceType: 'module',
|
|
220
|
+
plugins: ['jsx', 'typescript', 'classProperties', 'classPrivateProperties'],
|
|
221
|
+
sourceFilename: filename,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
let changed = false
|
|
225
|
+
let transformedComponents = false
|
|
226
|
+
const nextBody = []
|
|
227
|
+
for (const stmt of ast.program.body) {
|
|
228
|
+
const transformed = transformTopLevelStatement(stmt)
|
|
229
|
+
if (transformed.changed) {
|
|
230
|
+
changed = true
|
|
231
|
+
transformedComponents = true
|
|
232
|
+
}
|
|
233
|
+
nextBody.push(...transformed.nodes)
|
|
234
|
+
}
|
|
235
|
+
ast.program.body = nextBody
|
|
236
|
+
changed = normalizeRuntimeCoreImports(ast) || changed
|
|
237
|
+
if (!changed) return code
|
|
238
|
+
|
|
239
|
+
if (transformedComponents) {
|
|
240
|
+
// `GeaElement` is needed only if one of the classes we just made states
|
|
241
|
+
// props -- that is the only place the generated code spells it.
|
|
242
|
+
const statesProps = ast.program.body.some((node) => {
|
|
243
|
+
const declaration = t.isExportNamedDeclaration(node) ? node.declaration : node
|
|
244
|
+
return t.isClassDeclaration(declaration) && declaration.superTypeParameters != null
|
|
245
|
+
})
|
|
246
|
+
ensureComponentImport(ast, statesProps)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return generate(ast, { retainLines: true }, code).code
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── JSON-import constant folding ────────────────────────────────────────────
|
|
253
|
+
//
|
|
254
|
+
// `import data from './x.json'` followed by `data.notes` / `data.selectedId`
|
|
255
|
+
// reads. The geatsc C++ pipeline has no native lowering for a JSON module's
|
|
256
|
+
// default-export object: vite inlines it as a module-level object literal,
|
|
257
|
+
// which geatsc's global collector defers and BOXES into a `gea_cpp_value`
|
|
258
|
+
// global — every read goes through dynamic `record_get_literal`, and a store
|
|
259
|
+
// field seeded from it loses its array-of-objects shape (the vite-plugin-gea
|
|
260
|
+
// IR extractor sees a member expression, not a literal), so keyed lists
|
|
261
|
+
// silently render nothing.
|
|
262
|
+
//
|
|
263
|
+
// JSON modules are static data, so fold them at the source level instead:
|
|
264
|
+
// every member-access read of the imported binding is replaced with the
|
|
265
|
+
// actual JSON value as an inline literal, and the import is dropped once all
|
|
266
|
+
// references fold. Downstream (IR shape extraction, typed store-array
|
|
267
|
+
// storage, geatsc record lowering) then sees exactly what it would have seen
|
|
268
|
+
// had the data been written inline — the fully native path.
|
|
269
|
+
//
|
|
270
|
+
// Folding duplicates the value per reference site, which would split JS
|
|
271
|
+
// object identity if the same subtree were read through two aliases and
|
|
272
|
+
// mutated through one. To stay semantics-preserving we fold only when it is
|
|
273
|
+
// provably safe and otherwise leave the file untouched (boxed but correct):
|
|
274
|
+
// - every reference to the binding must be a member-access read chain
|
|
275
|
+
// (no bare uses, calls, exports, assignments, ++/--, delete, spread);
|
|
276
|
+
// - per top-level JSON key, either all folded reads are scalars
|
|
277
|
+
// (copies carry no identity) or there is exactly one read.
|
|
278
|
+
|
|
279
|
+
function jsonValueToAst(value) {
|
|
280
|
+
if (value === null) return t.nullLiteral()
|
|
281
|
+
switch (typeof value) {
|
|
282
|
+
case 'string':
|
|
283
|
+
return t.stringLiteral(value)
|
|
284
|
+
case 'boolean':
|
|
285
|
+
return t.booleanLiteral(value)
|
|
286
|
+
case 'number':
|
|
287
|
+
if (!Number.isFinite(value)) return null
|
|
288
|
+
return value < 0 ? t.unaryExpression('-', t.numericLiteral(-value)) : t.numericLiteral(value)
|
|
289
|
+
case 'object': {
|
|
290
|
+
if (Array.isArray(value)) {
|
|
291
|
+
const elements = value.map(jsonValueToAst)
|
|
292
|
+
if (elements.some((element) => element === null)) return null
|
|
293
|
+
return t.arrayExpression(elements)
|
|
294
|
+
}
|
|
295
|
+
const properties = []
|
|
296
|
+
for (const [key, child] of Object.entries(value)) {
|
|
297
|
+
const childAst = jsonValueToAst(child)
|
|
298
|
+
if (childAst === null) return null
|
|
299
|
+
const keyNode = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? t.identifier(key) : t.stringLiteral(key)
|
|
300
|
+
properties.push(t.objectProperty(keyNode, childAst))
|
|
301
|
+
}
|
|
302
|
+
return t.objectExpression(properties)
|
|
303
|
+
}
|
|
304
|
+
default:
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Static member-access key for `obj.key` / `obj["key"]` / `obj[0]`, else null.
|
|
310
|
+
function memberAccessKey(member) {
|
|
311
|
+
if (!member.computed && t.isIdentifier(member.property)) return member.property.name
|
|
312
|
+
if (member.computed && t.isStringLiteral(member.property)) return member.property.value
|
|
313
|
+
if (member.computed && t.isNumericLiteral(member.property)) return member.property.value
|
|
314
|
+
return null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Walk up the longest static member-access chain rooted at `refPath` and
|
|
318
|
+
// return { topPath, keys } — the outermost member-expression path plus the
|
|
319
|
+
// JSON key path it selects. Returns null when the chain breaks (computed
|
|
320
|
+
// non-literal key) or the chain is used as a write target.
|
|
321
|
+
function memberChainFromReference(refPath) {
|
|
322
|
+
let top = refPath
|
|
323
|
+
const keys = []
|
|
324
|
+
while (t.isMemberExpression(top.parent) && top.parent.object === top.node) {
|
|
325
|
+
const key = memberAccessKey(top.parent)
|
|
326
|
+
if (key === null) return null
|
|
327
|
+
keys.push(key)
|
|
328
|
+
top = top.parentPath
|
|
329
|
+
}
|
|
330
|
+
if (keys.length === 0) return null
|
|
331
|
+
const parent = top.parent
|
|
332
|
+
if (t.isAssignmentExpression(parent) && parent.left === top.node) return null
|
|
333
|
+
if (t.isUpdateExpression(parent)) return null
|
|
334
|
+
if (t.isUnaryExpression(parent) && parent.operator === 'delete') return null
|
|
335
|
+
return { topPath: top, keys }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function jsonValueAtPath(root, keys) {
|
|
339
|
+
let current = root
|
|
340
|
+
for (const key of keys) {
|
|
341
|
+
if (current === null || typeof current !== 'object') return undefined
|
|
342
|
+
current = current[key]
|
|
343
|
+
}
|
|
344
|
+
return current
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const isScalarJsonValue = (value) => value === null || typeof value !== 'object'
|
|
348
|
+
|
|
349
|
+
export function inlineJsonImports(code, filename) {
|
|
350
|
+
if (!code.includes('.json')) return code
|
|
351
|
+
|
|
352
|
+
let ast
|
|
353
|
+
try {
|
|
354
|
+
ast = parser.parse(code, {
|
|
355
|
+
sourceType: 'module',
|
|
356
|
+
plugins: ['jsx', 'typescript', 'classProperties', 'classPrivateProperties'],
|
|
357
|
+
sourceFilename: filename,
|
|
358
|
+
})
|
|
359
|
+
} catch {
|
|
360
|
+
return code
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let changed = false
|
|
364
|
+
traverse(ast, {
|
|
365
|
+
ImportDeclaration(importPath) {
|
|
366
|
+
const source = importPath.node.source.value
|
|
367
|
+
if (!source.endsWith('.json') || !source.startsWith('.')) return
|
|
368
|
+
// Only the default-import form (TS `resolveJsonModule`'s canonical
|
|
369
|
+
// shape). Named/namespace specifiers on a JSON import are left alone.
|
|
370
|
+
if (importPath.node.specifiers.length !== 1 || !t.isImportDefaultSpecifier(importPath.node.specifiers[0])) return
|
|
371
|
+
const localName = importPath.node.specifiers[0].local.name
|
|
372
|
+
const jsonFile = path.resolve(path.dirname(filename), source)
|
|
373
|
+
let data
|
|
374
|
+
try {
|
|
375
|
+
data = JSON.parse(fs.readFileSync(jsonFile, 'utf8'))
|
|
376
|
+
} catch {
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
const binding = importPath.scope.getBinding(localName)
|
|
380
|
+
if (!binding || binding.constantViolations.length > 0) return
|
|
381
|
+
|
|
382
|
+
const folds = []
|
|
383
|
+
const readsByTopLevelKey = new Map()
|
|
384
|
+
for (const refPath of binding.referencePaths) {
|
|
385
|
+
const chain = memberChainFromReference(refPath)
|
|
386
|
+
if (!chain) {
|
|
387
|
+
warnSkippedJsonFold(filename, source, localName, 'it is used outside a static property read')
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
const value = jsonValueAtPath(data, chain.keys)
|
|
391
|
+
if (value === undefined) {
|
|
392
|
+
warnSkippedJsonFold(filename, source, localName, `'${chain.keys.join('.')}' does not exist in the JSON`)
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
const literal = jsonValueToAst(value)
|
|
396
|
+
if (literal === null) {
|
|
397
|
+
warnSkippedJsonFold(filename, source, localName, `'${chain.keys.join('.')}' has a non-JSON value`)
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
const group = readsByTopLevelKey.get(chain.keys[0]) ?? []
|
|
401
|
+
group.push(value)
|
|
402
|
+
readsByTopLevelKey.set(chain.keys[0], group)
|
|
403
|
+
folds.push({ topPath: chain.topPath, literal })
|
|
404
|
+
}
|
|
405
|
+
for (const [key, reads] of readsByTopLevelKey) {
|
|
406
|
+
if (reads.length > 1 && reads.some((value) => !isScalarJsonValue(value))) {
|
|
407
|
+
warnSkippedJsonFold(filename, source, localName, `'${key}' is read more than once and holds an object/array (folding would split its identity)`)
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (const fold of folds) fold.topPath.replaceWith(fold.literal)
|
|
413
|
+
importPath.remove()
|
|
414
|
+
changed = true
|
|
415
|
+
},
|
|
416
|
+
})
|
|
417
|
+
if (!changed) return code
|
|
418
|
+
|
|
419
|
+
return generate(ast, { retainLines: true }, code).code
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function warnSkippedJsonFold(filename, source, localName, reason) {
|
|
423
|
+
process.stderr.write(
|
|
424
|
+
`${filename}: warning: JSON import '${localName}' from '${source}' was not inlined because ${reason}. ` +
|
|
425
|
+
`On the embedded target the JSON object stays a dynamic boxed value (slow reads, and store list fields seeded from it do not render).\n`,
|
|
426
|
+
)
|
|
427
|
+
}
|