@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,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
|
+
}
|