@geastack/core 0.1.39 → 0.1.40
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/events.cpp +2 -0
- package/gea_app_entry.cpp +2 -2
- package/gea_sources.mjs +4 -3
- package/include/audio.h +11 -1
- package/include/audio_oscillator.h +52 -0
- package/include/camera.h +21 -0
- package/include/captive_dns.h +71 -0
- package/include/display_present.h +66 -28
- package/include/events.h +31 -3
- package/include/gea/embedded.h +2 -0
- package/include/graphics/font.h +19 -0
- package/include/image.h +39 -1
- package/include/input.h +12 -1
- package/include/platform/atomic_file.h +72 -0
- package/include/platform/file_cache.h +6 -3
- package/include/power.h +2 -0
- package/include/services/frame_scheduler.h +6 -0
- package/include/wifi.h +60 -45
- package/index.d.ts +223 -44
- package/package.json +3 -3
- package/runtime/audio-worklet.ts +7 -11
- package/runtime/host.ts +132 -77
- package/runtime/images.ts +5 -0
- package/runtime-surface.ts +1 -0
- package/runtime.cpp +23 -5
- package/scripts/bitmap-font-source.mjs +155 -0
- package/scripts/build-gea-vite-geatsc.mjs +97 -6
- package/scripts/gea-embedded-asset-policy.mjs +38 -0
- package/scripts/gea-embedded-compat-transform.mjs +4 -2
- package/scripts/gea-vite-module-graph-plugin.mjs +64 -15
- package/scripts/generate-gea-embedded-assets.mjs +17 -3
- package/scripts/generate-gea-embedded-fonts.mjs +986 -670
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
function integer(value, name, minimum = 0, maximum = 65535) {
|
|
4
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
|
5
|
+
throw new Error(`Invalid bitmap font ${name}`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function loadBitmapFontSource(filename) {
|
|
12
|
+
const source = JSON.parse(fs.readFileSync(filename, "utf8"));
|
|
13
|
+
|
|
14
|
+
if (
|
|
15
|
+
source.schemaVersion !== 1 ||
|
|
16
|
+
source.format !== "gea-bitmap-font" ||
|
|
17
|
+
!Array.isArray(source.fonts)
|
|
18
|
+
) {
|
|
19
|
+
throw new Error(`Invalid bitmap font source: ${filename}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return source;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildBitmapFont(source, tuple, packGlyphAtlas) {
|
|
26
|
+
const face = source.fonts.find((font) => font.sizePx === tuple.sizePx);
|
|
27
|
+
|
|
28
|
+
if (!face)
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Bitmap font ${tuple.family} has no exact ${tuple.sizePx}px face`,
|
|
31
|
+
);
|
|
32
|
+
const lineHeight = integer(face.lineHeight, "lineHeight", 1, 1024);
|
|
33
|
+
const ascender = integer(face.ascender, "ascender", 0, lineHeight);
|
|
34
|
+
const descender = integer(face.descender, "descender", 0, lineHeight);
|
|
35
|
+
if (ascender + descender !== lineHeight)
|
|
36
|
+
throw new Error("Invalid bitmap font line metrics");
|
|
37
|
+
const glyphs = [];
|
|
38
|
+
const bitmaps = [];
|
|
39
|
+
const available = new Set();
|
|
40
|
+
const requested = tuple.charset;
|
|
41
|
+
const fallbackCodepoint =
|
|
42
|
+
face.fallbackCodepoint === undefined
|
|
43
|
+
? -1
|
|
44
|
+
: integer(face.fallbackCodepoint, "fallbackCodepoint", 0, 0x10ffff);
|
|
45
|
+
|
|
46
|
+
if (!Array.isArray(face.glyphs) || face.glyphs.length === 0)
|
|
47
|
+
throw new Error("Bitmap font has no glyphs");
|
|
48
|
+
for (const item of face.glyphs) {
|
|
49
|
+
const cp = integer(item.codepoint, "codepoint", 0, 0x10ffff);
|
|
50
|
+
|
|
51
|
+
if (available.has(cp)) throw new Error("Duplicate bitmap font glyph");
|
|
52
|
+
available.add(cp);
|
|
53
|
+
const width = integer(item.width, "glyph width", 0, 512);
|
|
54
|
+
const height = integer(item.height, "glyph height", 0, 512);
|
|
55
|
+
const advance16 = integer(item.advance16, "advance16", 0, 32767);
|
|
56
|
+
const bearingX = integer(item.bearingX, "bearingX", -1024, 1024);
|
|
57
|
+
const bearingY = integer(item.bearingY, "bearingY", -1024, 1024);
|
|
58
|
+
if (
|
|
59
|
+
typeof item.coverage !== "string" ||
|
|
60
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
|
|
61
|
+
item.coverage,
|
|
62
|
+
)
|
|
63
|
+
) {
|
|
64
|
+
throw new Error("Invalid bitmap glyph coverage encoding");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const coverage = Buffer.from(item.coverage, "base64");
|
|
68
|
+
|
|
69
|
+
if (coverage.length !== width * height)
|
|
70
|
+
throw new Error("Invalid bitmap glyph coverage length");
|
|
71
|
+
if (
|
|
72
|
+
requested &&
|
|
73
|
+
!requested.has(cp) &&
|
|
74
|
+
cp !== 63 &&
|
|
75
|
+
cp !== fallbackCodepoint
|
|
76
|
+
)
|
|
77
|
+
continue;
|
|
78
|
+
glyphs.push({
|
|
79
|
+
codepoint: cp,
|
|
80
|
+
sourceX: 0,
|
|
81
|
+
sourceY: 0,
|
|
82
|
+
width,
|
|
83
|
+
height,
|
|
84
|
+
advance: (advance16 + 8) >> 4,
|
|
85
|
+
bearingX,
|
|
86
|
+
bearingY,
|
|
87
|
+
advance16,
|
|
88
|
+
});
|
|
89
|
+
bitmaps.push(coverage);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (fallbackCodepoint >= 0 && !available.has(fallbackCodepoint))
|
|
93
|
+
throw new Error("Bitmap fallback glyph is absent");
|
|
94
|
+
if (glyphs.length === 0)
|
|
95
|
+
throw new Error("Bitmap font charset selects no available glyphs");
|
|
96
|
+
const records = glyphs
|
|
97
|
+
.map((glyph, index) => ({ glyph, bitmap: bitmaps[index] }))
|
|
98
|
+
.sort((a, b) => a.glyph.codepoint - b.glyph.codepoint);
|
|
99
|
+
const sortedGlyphs = records.map((record) => record.glyph);
|
|
100
|
+
const { atlasWidth, atlasHeight } = packGlyphAtlas(sortedGlyphs);
|
|
101
|
+
const width = Math.max(1, atlasWidth);
|
|
102
|
+
const height = Math.max(1, atlasHeight);
|
|
103
|
+
const atlasData = new Uint8Array(width * height);
|
|
104
|
+
|
|
105
|
+
for (const { glyph, bitmap } of records) {
|
|
106
|
+
for (let row = 0; row < glyph.height; row++) {
|
|
107
|
+
atlasData.set(
|
|
108
|
+
bitmap.subarray(row * glyph.width, (row + 1) * glyph.width),
|
|
109
|
+
(glyph.sourceY + row) * width + glyph.sourceX,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const selected = new Set(sortedGlyphs.map((glyph) => glyph.codepoint));
|
|
115
|
+
const kerning = [];
|
|
116
|
+
const pairs = new Set();
|
|
117
|
+
for (const pair of face.kerning || []) {
|
|
118
|
+
const left = integer(pair.left, "kerning left", 0, 0x10ffff);
|
|
119
|
+
const right = integer(pair.right, "kerning right", 0, 0x10ffff);
|
|
120
|
+
const adjustment16 = integer(
|
|
121
|
+
pair.adjustment16,
|
|
122
|
+
"kerning adjustment",
|
|
123
|
+
-32768,
|
|
124
|
+
32767,
|
|
125
|
+
);
|
|
126
|
+
const key = `${left}:${right}`;
|
|
127
|
+
|
|
128
|
+
if (pairs.has(key)) throw new Error("Duplicate bitmap font kerning pair");
|
|
129
|
+
pairs.add(key);
|
|
130
|
+
if (!available.has(left) || !available.has(right))
|
|
131
|
+
throw new Error("Kerning references absent glyph");
|
|
132
|
+
if (selected.has(left) && selected.has(right) && adjustment16 !== 0)
|
|
133
|
+
kerning.push({ left, right, adjustment16 });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
kerning.sort((a, b) => a.left - b.left || a.right - b.right);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
id: tuple.id,
|
|
140
|
+
family: tuple.family,
|
|
141
|
+
familyId: tuple.familyId,
|
|
142
|
+
sizePx: tuple.sizePx,
|
|
143
|
+
lineHeight,
|
|
144
|
+
ascender,
|
|
145
|
+
descender,
|
|
146
|
+
glyphs: sortedGlyphs,
|
|
147
|
+
atlasWidth: width,
|
|
148
|
+
atlasHeight: height,
|
|
149
|
+
atlasBits: 8,
|
|
150
|
+
atlasData,
|
|
151
|
+
kerning,
|
|
152
|
+
importedBitmap: true,
|
|
153
|
+
fallbackCodepoint,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -253,7 +253,18 @@ function copyCompatSourceTree(srcDir, dstDir, envDefines = {}) {
|
|
|
253
253
|
for (const module of native.modules) {
|
|
254
254
|
nativeModules.set(`${module.kind}:${module.url}`, { ...module, stagedEntry: path.resolve(path.dirname(dst), path.relative(path.dirname(src), module.entry)) })
|
|
255
255
|
}
|
|
256
|
-
|
|
256
|
+
let debugMap
|
|
257
|
+
const preparedSource = inlineJsonImports(native.code, src)
|
|
258
|
+
const transformed = transformGeaEmbeddedCompatSource(preparedSource, src,
|
|
259
|
+
process.env.GEA_NATIVE_DEBUGGER === '1' ? map => { debugMap = map } : undefined)
|
|
260
|
+
fs.writeFileSync(dst, inlineProcessEnv(transformed, envDefines))
|
|
261
|
+
if (process.env.GEA_NATIVE_DEBUGGER === '1') {
|
|
262
|
+
fs.writeFileSync(dst + '.gea-source.json', JSON.stringify({
|
|
263
|
+
source: src, original: code, staged: fs.readFileSync(dst, 'utf8'),
|
|
264
|
+
map: debugMap || null, prepared: preparedSource,
|
|
265
|
+
unmappedTransforms: preparedSource !== code || transformed !== inlineProcessEnv(transformed, envDefines)
|
|
266
|
+
}))
|
|
267
|
+
}
|
|
257
268
|
} else {
|
|
258
269
|
fs.copyFileSync(src, dst)
|
|
259
270
|
if (path.extname(entry.name) === '.css') copyCssUrlAssets(src, dst)
|
|
@@ -451,7 +462,72 @@ function resolveAppleNativeAliasModules(appDir) {
|
|
|
451
462
|
return { threeSrcDir, threeUtilsModule, threeWebGLAnimationModule, threeWebXRManagerModule, troikaThreeTextModule }
|
|
452
463
|
}
|
|
453
464
|
|
|
454
|
-
|
|
465
|
+
// `gea.moduleAliases` in the application's package.json: exact import
|
|
466
|
+
// specifiers redirected to files inside the application, for a module whose
|
|
467
|
+
// host exists on one target only (an app's macOS audio host, say) and that
|
|
468
|
+
// another target replaces with its own implementation.
|
|
469
|
+
// Each replacement names the app's STAGED copy when there is one: the compat
|
|
470
|
+
// build compiles the staged tree, so an alias into the app directory itself
|
|
471
|
+
// pulled a second copy of every module the replacement imports (skytail's
|
|
472
|
+
// mosaico kit index brought its own kit-types.ts, and the two KitCategory
|
|
473
|
+
// records refused to convert).
|
|
474
|
+
function resolveAppModuleAliases(appDir, stagedAppDir = appDir) {
|
|
475
|
+
let manifest = {}
|
|
476
|
+
try {
|
|
477
|
+
manifest = JSON.parse(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')).gea ?? {}
|
|
478
|
+
} catch {
|
|
479
|
+
return []
|
|
480
|
+
}
|
|
481
|
+
const raw = manifest.moduleAliases
|
|
482
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return []
|
|
483
|
+
const aliases = []
|
|
484
|
+
for (const [specifier, target] of Object.entries(raw)) {
|
|
485
|
+
if (typeof target !== 'string' || !target) fail(`gea.moduleAliases["${specifier}"] must name a file inside the application`)
|
|
486
|
+
if (!fs.existsSync(path.resolve(appDir, target))) fail(`gea.moduleAliases["${specifier}"] does not exist: ${target}`)
|
|
487
|
+
aliases.push({ specifier, replacement: path.resolve(stagedAppDir, target) })
|
|
488
|
+
}
|
|
489
|
+
return aliases
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// The gea-threejs port, when the application depends on it: its src/ directory,
|
|
493
|
+
// and the native troika text module when the application uses troika.
|
|
494
|
+
// Opt-in: an application that sets `"gea": { "geaThreejs": true }` in its
|
|
495
|
+
// package.json. Depending on the port is not enough on its own, so an app that
|
|
496
|
+
// installs it for some other reason keeps compiling three.js itself.
|
|
497
|
+
function resolveGeaThreeAliasModules(appDir) {
|
|
498
|
+
const appRequire = createRequire(path.join(appDir, 'package.json'))
|
|
499
|
+
let geaThreeSrcDir = ''
|
|
500
|
+
let troikaThreeTextModule = ''
|
|
501
|
+
let optedIn = false
|
|
502
|
+
try {
|
|
503
|
+
optedIn = JSON.parse(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')).gea?.geaThreejs === true
|
|
504
|
+
} catch {
|
|
505
|
+
// An unreadable manifest opts into nothing; the build reports it elsewhere.
|
|
506
|
+
}
|
|
507
|
+
if (!optedIn) return { geaThreeSrcDir, troikaThreeTextModule }
|
|
508
|
+
try {
|
|
509
|
+
geaThreeSrcDir = path.join(path.dirname(fs.realpathSync(appRequire.resolve('@geastack/gea-threejs/package.json'))), 'src')
|
|
510
|
+
} catch {
|
|
511
|
+
return { geaThreeSrcDir, troikaThreeTextModule }
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
appRequire.resolve('troika-three-text')
|
|
515
|
+
} catch {
|
|
516
|
+
return { geaThreeSrcDir, troikaThreeTextModule }
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
troikaThreeTextModule = fs.realpathSync(appRequire.resolve('@geastack/native-webgl-angle/troika-three-text'))
|
|
520
|
+
} catch {
|
|
521
|
+
fail(
|
|
522
|
+
`This build resolved "troika-three-text" and "@geastack/gea-threejs" from ${appDir}, but could not resolve ` +
|
|
523
|
+
'"@geastack/native-webgl-angle/troika-three-text", the native text module that replaces it. ' +
|
|
524
|
+
'Install @geastack/native-webgl-angle in the application.'
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
return { geaThreeSrcDir, troikaThreeTextModule }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function writeCompatViteConfig({ configPath, compatSrcDir, appDir, stagedAppDir, entry, viteOutDir, geaIrPath, appleNative, moduleGraphOutDir, envDefines }) {
|
|
455
531
|
const engineComponentsPath = path.join(engineRoot, 'components/index.ts')
|
|
456
532
|
const engineComponentSubpath = path.join(engineRoot, 'components/$1')
|
|
457
533
|
const elementsPath = path.join(elementsRoot, 'components/index.ts')
|
|
@@ -467,7 +543,13 @@ function writeCompatViteConfig({ configPath, compatSrcDir, appDir, entry, viteOu
|
|
|
467
543
|
const { threeSrcDir, threeUtilsModule, threeWebGLAnimationModule, threeWebXRManagerModule, troikaThreeTextModule } = appleNative
|
|
468
544
|
? resolveAppleNativeAliasModules(appDir)
|
|
469
545
|
: { threeSrcDir: '', threeUtilsModule: '', threeWebGLAnimationModule: '', threeWebXRManagerModule: '', troikaThreeTextModule: '' }
|
|
546
|
+
// Apple-native keeps three.js itself (it renders through ANGLE).
|
|
547
|
+
const geaThree = appleNative ? { geaThreeSrcDir: '', troikaThreeTextModule: '' } : resolveGeaThreeAliasModules(appDir)
|
|
470
548
|
const q = (value) => JSON.stringify(value)
|
|
549
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')
|
|
550
|
+
const appModuleAliases = resolveAppModuleAliases(appDir, stagedAppDir)
|
|
551
|
+
.map(({ specifier, replacement }) => ` { find: new RegExp(${q('^' + escapeRegExp(specifier) + '$')}), replacement: ${q(replacement)} },\n`)
|
|
552
|
+
.join('')
|
|
471
553
|
// Compile the @geajs/core reactive runtime (Component/Store/compiler-runtime)
|
|
472
554
|
// from its TYPED SOURCE, not the minified dist. The minified build reuses short
|
|
473
555
|
// identifiers across nested scopes, which the C++ symbol collector cannot
|
|
@@ -496,7 +578,7 @@ function writeCompatViteConfig({ configPath, compatSrcDir, appDir, entry, viteOu
|
|
|
496
578
|
const config = `import { resolve } from 'node:path'
|
|
497
579
|
import { geaPlugin } from ${q(geaPluginResolved)}
|
|
498
580
|
import { geaNativeStylePlugin } from ${q(path.join(coreRoot, 'scripts/gea-native-style-plugin.mjs'))}
|
|
499
|
-
import { geaAppleNativeModuleAliases, geaModuleGraphPlugins } from ${q(moduleGraphPluginPath)}
|
|
581
|
+
import { geaAppleNativeModuleAliases, geaModuleGraphPlugins, geaThreeModuleAliases } from ${q(moduleGraphPluginPath)}
|
|
500
582
|
|
|
501
583
|
const root = ${q(compatSrcDir)}
|
|
502
584
|
const geaModuleGraph = geaModuleGraphPlugins({ outDir: ${q(moduleGraphOutDir || '')}, entryReachableOnly: true })
|
|
@@ -507,6 +589,10 @@ const geaAppleNativeAliases = geaAppleNativeModuleAliases({
|
|
|
507
589
|
threeWebXRManagerModule: ${q(threeWebXRManagerModule)},
|
|
508
590
|
troikaThreeTextModule: ${q(troikaThreeTextModule)},
|
|
509
591
|
})
|
|
592
|
+
const geaThreeAliases = geaThreeModuleAliases({
|
|
593
|
+
geaThreeSrcDir: ${q(geaThree.geaThreeSrcDir)},
|
|
594
|
+
troikaThreeTextModule: ${q(geaThree.troikaThreeTextModule)},
|
|
595
|
+
})
|
|
510
596
|
|
|
511
597
|
export default {
|
|
512
598
|
root,
|
|
@@ -516,6 +602,7 @@ ${defineEntries}
|
|
|
516
602
|
plugins: [geaNativeStylePlugin({ runtimeEntry: ${q(compilerRuntimeEntry)} }), geaPlugin({ ir: { enabled: true, outFile: ${q(geaIrPath)} } }), ...geaModuleGraph],
|
|
517
603
|
resolve: {
|
|
518
604
|
alias: [
|
|
605
|
+
${appModuleAliases} ...geaThreeAliases,
|
|
519
606
|
${compilerRuntimeAlias}
|
|
520
607
|
{ find: new RegExp('^@geastack/core$'), replacement: ${q(runtimePath)} },
|
|
521
608
|
{ find: new RegExp('^@geastack/engine$'), replacement: ${q(engineComponentsPath)} },
|
|
@@ -2011,7 +2098,6 @@ function cssIgnoredProperty(property) {
|
|
|
2011
2098
|
'scroll-snap-type',
|
|
2012
2099
|
'scrollbar-width',
|
|
2013
2100
|
'text-shadow',
|
|
2014
|
-
'transition',
|
|
2015
2101
|
'cursor',
|
|
2016
2102
|
'-webkit-tap-highlight-color',
|
|
2017
2103
|
]).has(property)
|
|
@@ -4428,7 +4514,7 @@ if (geaEmbeddedCompat) {
|
|
|
4428
4514
|
}
|
|
4429
4515
|
stagedEntryFile = path.join(compatSrcDir, compatEntry)
|
|
4430
4516
|
fontSourceDir = compatSrcDir
|
|
4431
|
-
writeCompatViteConfig({ configPath: viteConfig, compatSrcDir, appDir, entry: compatEntry, viteOutDir, geaIrPath, appleNative, moduleGraphOutDir, envDefines })
|
|
4517
|
+
writeCompatViteConfig({ configPath: viteConfig, compatSrcDir, appDir, stagedAppDir: path.join(compatSrcDir, path.relative(stagingRoot, appDir)), entry: compatEntry, viteOutDir, geaIrPath, appleNative, moduleGraphOutDir, envDefines })
|
|
4432
4518
|
} else if (appleNative) {
|
|
4433
4519
|
const baseViteConfig = viteConfig
|
|
4434
4520
|
viteConfig = path.join(outDir, 'gea-apple-native.vite.config.mjs')
|
|
@@ -4559,6 +4645,10 @@ const geatscArgs = [
|
|
|
4559
4645
|
`gea.ir=${geaIrPath}`,
|
|
4560
4646
|
'--plugin-option',
|
|
4561
4647
|
`gea.microtasks-namespace=${microtasksNamespaceForEntry(entrySymbol)}`,
|
|
4648
|
+
'--plugin-option',
|
|
4649
|
+
`gea.native-signature-core-root=${coreRoot}`,
|
|
4650
|
+
'--plugin-option',
|
|
4651
|
+
`gea.native-signature-host-root=${process.env.GEA_HOST_DIR || path.resolve(coreRoot, '..', 'host')}`,
|
|
4562
4652
|
]
|
|
4563
4653
|
if (!moduleGraphCompile) geatscArgs.push('--target', 'cpp')
|
|
4564
4654
|
if (moduleGraphCompile) {
|
|
@@ -4578,6 +4668,7 @@ if (cxxStandard) geatscArgs.push('--cxx-standard', cxxStandard)
|
|
|
4578
4668
|
if (hasFlag('--allow-any') || compilerSettings.allowAny) geatscArgs.push('--allow-any')
|
|
4579
4669
|
if (entrySymbol) geatscArgs.push('--entry-symbol', entrySymbol)
|
|
4580
4670
|
if (hasFlag('--isolate-symbols')) geatscArgs.push('--isolate-symbols')
|
|
4671
|
+
if (process.env.GEA_NATIVE_DEBUGGER === '1') geatscArgs.push('--debug-source')
|
|
4581
4672
|
// One C++ unit per source module plus a shared header, instead of one unit for
|
|
4582
4673
|
// the whole program. Every target already reads `geatsc-sources.txt` as a LIST,
|
|
4583
4674
|
// so nothing downstream changes; see the compiler's docs/TRANSLATION-UNITS.md
|
|
@@ -4606,7 +4697,7 @@ if (nativeModules.size) {
|
|
|
4606
4697
|
if (!fs.existsSync(module.stagedEntry)) fail(`missing ${module.kind} module: ${module.entry}`)
|
|
4607
4698
|
if (module.kind === 'worklet') {
|
|
4608
4699
|
const implementation = path.join(coreRoot, 'runtime/audio-worklet.ts')
|
|
4609
|
-
const bindings = `import { AudioWorkletProcessor, registerProcessor } from ${JSON.stringify(implementation)}\n`
|
|
4700
|
+
const bindings = `import { AudioWorkletProcessor, registerProcessor } from ${JSON.stringify(implementation.slice(0, -3))}\n`
|
|
4610
4701
|
const source = fs.readFileSync(module.stagedEntry, 'utf8')
|
|
4611
4702
|
fs.writeFileSync(module.stagedEntry, bindings + source)
|
|
4612
4703
|
const compilerRequire = createRequire(pathToFileURL(geatscBin))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function compileTimeOnlyAssetPaths(manifest) {
|
|
4
|
+
const configured = manifest?.gea?.embeddedAssets?.compileTimeOnly;
|
|
5
|
+
|
|
6
|
+
if (configured === undefined) {
|
|
7
|
+
return new Set();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!Array.isArray(configured)) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"gea.embeddedAssets.compileTimeOnly must be an array of app-relative file paths",
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result = new Set();
|
|
17
|
+
|
|
18
|
+
for (const entry of configured) {
|
|
19
|
+
if (
|
|
20
|
+
typeof entry !== "string" ||
|
|
21
|
+
entry.length === 0 ||
|
|
22
|
+
entry.includes("\\") ||
|
|
23
|
+
path.posix.isAbsolute(entry) ||
|
|
24
|
+
path.posix.normalize(entry) !== entry ||
|
|
25
|
+
entry === "." ||
|
|
26
|
+
entry === ".." ||
|
|
27
|
+
entry.startsWith("../")
|
|
28
|
+
) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
"Compile-time asset paths must be normalized relative paths inside the app",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
result.add(entry);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
@@ -212,7 +212,7 @@ function transformTopLevelStatement(stmt) {
|
|
|
212
212
|
return { changed: false, nodes: [stmt] }
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
export function transformGeaEmbeddedCompatSource(code, filename) {
|
|
215
|
+
export function transformGeaEmbeddedCompatSource(code, filename, onSourceMap) {
|
|
216
216
|
if ((!code.includes('<') || !code.includes('>')) && !code.includes('@geastack/core') && !code.includes('@geajs/core')) return code
|
|
217
217
|
|
|
218
218
|
const ast = parser.parse(code, {
|
|
@@ -246,7 +246,9 @@ export function transformGeaEmbeddedCompatSource(code, filename) {
|
|
|
246
246
|
ensureComponentImport(ast, statesProps)
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
|
|
249
|
+
const result = generate(ast, { retainLines: true, sourceMaps: !!onSourceMap, sourceFileName: filename }, code)
|
|
250
|
+
if (onSourceMap) onSourceMap(result.map)
|
|
251
|
+
return result.code
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
// ─── JSON-import constant folding ────────────────────────────────────────────
|
|
@@ -6,6 +6,7 @@ import path from 'node:path'
|
|
|
6
6
|
const JS_LIKE_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx)$/i
|
|
7
7
|
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
|
|
8
8
|
const STATIC_ASSET_MODULE_SUFFIX = '.geaassetmodule.js'
|
|
9
|
+
const RAW_MODULE_SUFFIX = '.gearawmodule.js'
|
|
9
10
|
const STATIC_IMPORT_RE = /\bimport\s+(?:[^'"()]*?\s+from\s*)?["']([^"']+)["']/g
|
|
10
11
|
const EXPORT_FROM_RE = /\bexport\s+(?:[^'"]*?\s+from\s*)["']([^"']+)["']/g
|
|
11
12
|
const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g
|
|
@@ -65,6 +66,30 @@ export function geaAppleNativeModuleAliases(options = {}) {
|
|
|
65
66
|
return aliases
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// An app that opts into @geastack/gea-threejs (three.js ported to strictly
|
|
70
|
+
// typed TypeScript, with a native software renderer) by setting
|
|
71
|
+
// `"gea": { "geaThreejs": true }` gets three's import surface redirected to
|
|
72
|
+
// the port on non-apple native targets: bare `three`,
|
|
73
|
+
// every `three/src/*.js` path (to its `.ts` source), and `troika-three-text`
|
|
74
|
+
// (to the native atlas text, whose own `three/src` imports land on the port
|
|
75
|
+
// through the same aliases).
|
|
76
|
+
export function geaThreeModuleAliases(options = {}) {
|
|
77
|
+
const aliases = []
|
|
78
|
+
if (!options.geaThreeSrcDir) return aliases
|
|
79
|
+
if (!path.isAbsolute(options.geaThreeSrcDir)) {
|
|
80
|
+
throw new TypeError('geaThreeSrcDir must be an absolute directory resolved from the application package boundary')
|
|
81
|
+
}
|
|
82
|
+
aliases.push({ find: /^three$/, replacement: path.join(options.geaThreeSrcDir, 'Three.ts') })
|
|
83
|
+
aliases.push({ find: /^three\/src\/(.+)\.js$/, replacement: path.join(options.geaThreeSrcDir, '$1.ts') })
|
|
84
|
+
if (options.troikaThreeTextModule) {
|
|
85
|
+
if (!path.isAbsolute(options.troikaThreeTextModule)) {
|
|
86
|
+
throw new TypeError('troikaThreeTextModule must be an absolute module resolved from the application package boundary')
|
|
87
|
+
}
|
|
88
|
+
aliases.push({ find: /^troika-three-text$/, replacement: options.troikaThreeTextModule })
|
|
89
|
+
}
|
|
90
|
+
return aliases
|
|
91
|
+
}
|
|
92
|
+
|
|
68
93
|
export function geaModuleGraphPlugins(options = {}) {
|
|
69
94
|
const outDir = options.outDir || process.env.GEA_VITE_MODULE_GRAPH_OUT || ''
|
|
70
95
|
if (!outDir) return []
|
|
@@ -76,6 +101,7 @@ export function geaModuleGraphPlugins(options = {}) {
|
|
|
76
101
|
transformed: new Map(),
|
|
77
102
|
imports: new Map(),
|
|
78
103
|
assets: new Map(),
|
|
104
|
+
rawModules: new Map(),
|
|
79
105
|
// GEA_MODULE_GRAPH_ENTRY_REACHABLE_ONLY=0 keeps the unpruned graph, so a
|
|
80
106
|
// build can be A/B'd against the reachability filter without editing the
|
|
81
107
|
// generated vite config.
|
|
@@ -95,32 +121,44 @@ export function geaModuleGraphPlugins(options = {}) {
|
|
|
95
121
|
// Vite normally turns a plain static-asset import into a JS module that
|
|
96
122
|
// exports the emitted browser URL. The native compiler consumes source
|
|
97
123
|
// snapshots instead of the browser bundle, so expose the same binding
|
|
98
|
-
// through a compiler-visible synthetic JS module.
|
|
99
|
-
//
|
|
124
|
+
// through a compiler-visible synthetic JS module. Raw imports export
|
|
125
|
+
// file contents instead; other query imports retain Vite semantics.
|
|
100
126
|
const normalizedImporter = normalizeModuleId(importer)
|
|
127
|
+
const raw = source.endsWith('?raw')
|
|
128
|
+
const assetSpecifier = raw ? source.slice(0, -4) : source
|
|
101
129
|
if (
|
|
102
130
|
!importer ||
|
|
103
131
|
!shouldTrackModule(normalizedImporter) ||
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
!STATIC_ASSET_RE.test(
|
|
132
|
+
assetSpecifier.includes('?') ||
|
|
133
|
+
assetSpecifier.includes('#') ||
|
|
134
|
+
(!raw && !STATIC_ASSET_RE.test(assetSpecifier))
|
|
107
135
|
) return null
|
|
108
136
|
|
|
109
137
|
let resolvedId = ''
|
|
110
|
-
const resolved = await this.resolve(
|
|
138
|
+
const resolved = await this.resolve(assetSpecifier, importer, { skipSelf: true })
|
|
111
139
|
if (resolved && !resolved.external) resolvedId = normalizeModuleId(resolved.id)
|
|
112
|
-
if (!resolvedId &&
|
|
113
|
-
resolvedId = path.resolve(path.dirname(normalizedImporter),
|
|
140
|
+
if (!resolvedId && assetSpecifier.startsWith('.')) {
|
|
141
|
+
resolvedId = path.resolve(path.dirname(normalizedImporter), assetSpecifier)
|
|
114
142
|
}
|
|
115
143
|
if (!path.isAbsolute(resolvedId) || !fs.existsSync(resolvedId) || !fs.statSync(resolvedId).isFile()) return null
|
|
116
144
|
|
|
117
145
|
const assetSource = fs.realpathSync(resolvedId)
|
|
146
|
+
if (raw) {
|
|
147
|
+
const moduleId = `${assetSource}${RAW_MODULE_SUFFIX}`
|
|
148
|
+
state.rawModules.set(moduleId, fs.readFileSync(assetSource, 'utf8'))
|
|
149
|
+
return moduleId
|
|
150
|
+
}
|
|
151
|
+
|
|
118
152
|
const moduleId = `${assetSource}${STATIC_ASSET_MODULE_SUFFIX}`
|
|
119
153
|
if (!state.assets.has(moduleId)) state.assets.set(moduleId, describeStaticAsset(assetSource))
|
|
120
154
|
return moduleId
|
|
121
155
|
},
|
|
122
156
|
load(id) {
|
|
123
|
-
const
|
|
157
|
+
const normalizedId = normalizeModuleId(id)
|
|
158
|
+
if (state.rawModules.has(normalizedId)) {
|
|
159
|
+
return `export default ${JSON.stringify(state.rawModules.get(normalizedId))}\n`
|
|
160
|
+
}
|
|
161
|
+
const asset = state.assets.get(normalizedId)
|
|
124
162
|
if (!asset) return null
|
|
125
163
|
return `export default ${JSON.stringify(asset.url)}\n`
|
|
126
164
|
},
|
|
@@ -183,10 +221,16 @@ function writeModuleGraph(pluginContext, state) {
|
|
|
183
221
|
const id = normalizeModuleId(rawId)
|
|
184
222
|
if (shouldTrackModule(id) && !rawIdsByNormalizedId.has(id)) rawIdsByNormalizedId.set(id, rawId)
|
|
185
223
|
}
|
|
224
|
+
// A framework transform can consume an imported asset while leaving the
|
|
225
|
+
// original TS snapshot's binding intact. Rollup then never loads its module;
|
|
226
|
+
// the native graph still needs the resolved literal module for that binding.
|
|
227
|
+
for (const id of [...state.assets.keys(), ...state.rawModules.keys()]) {
|
|
228
|
+
if (!rawIdsByNormalizedId.has(id)) rawIdsByNormalizedId.set(id, id)
|
|
229
|
+
}
|
|
186
230
|
const modules = []
|
|
187
231
|
const allIds = Array.from(rawIdsByNormalizedId.keys()).sort()
|
|
188
232
|
const reachableIds = state.entryReachableOnly
|
|
189
|
-
? entryReachableModuleIds(pluginContext, rawIdsByNormalizedId, allIds)
|
|
233
|
+
? entryReachableModuleIds(pluginContext, state, rawIdsByNormalizedId, allIds)
|
|
190
234
|
: allIds
|
|
191
235
|
const live = liveModuleIds(state, reachableIds)
|
|
192
236
|
const ids = reachableIds.filter((id) => live.keep.has(id))
|
|
@@ -194,11 +238,13 @@ function writeModuleGraph(pluginContext, state) {
|
|
|
194
238
|
for (const id of ids) {
|
|
195
239
|
const info = pluginContext.getModuleInfo(rawIdsByNormalizedId.get(id))
|
|
196
240
|
const prune = (code) => pruneDeadReExports(code, id, state, live)
|
|
197
|
-
const
|
|
198
|
-
const
|
|
241
|
+
const asset = state.assets.get(id)
|
|
242
|
+
const literal = asset?.url ?? state.rawModules.get(id)
|
|
243
|
+
const syntheticSource = literal === undefined ? undefined : `export default ${JSON.stringify(literal)}\n`
|
|
244
|
+
const originalCode = prune(normalizeSnapshotSourceForGeatsc(state.original.get(id) ?? syntheticSource ?? readFileSource(id)))
|
|
245
|
+
const transformedCode = prune(normalizeSnapshotSourceForGeatsc(state.transformed.get(id) ?? syntheticSource))
|
|
199
246
|
const originalPath = writeSnapshot(state, id, 'original', originalCode)
|
|
200
247
|
const transformedPath = writeSnapshot(state, id, 'transformed', transformedCode)
|
|
201
|
-
const asset = state.assets.get(id)
|
|
202
248
|
modules.push({
|
|
203
249
|
id,
|
|
204
250
|
file: path.isAbsolute(id) ? id : null,
|
|
@@ -397,7 +443,7 @@ function pruneDeadReExports(code, id, state, live) {
|
|
|
397
443
|
})
|
|
398
444
|
}
|
|
399
445
|
|
|
400
|
-
function entryReachableModuleIds(pluginContext, rawIdsByNormalizedId, allIds) {
|
|
446
|
+
function entryReachableModuleIds(pluginContext, state, rawIdsByNormalizedId, allIds) {
|
|
401
447
|
const tracked = new Set(allIds)
|
|
402
448
|
const reachable = new Set()
|
|
403
449
|
const pending = []
|
|
@@ -416,7 +462,10 @@ function entryReachableModuleIds(pluginContext, rawIdsByNormalizedId, allIds) {
|
|
|
416
462
|
if (!id || reachable.has(id)) continue
|
|
417
463
|
reachable.add(id)
|
|
418
464
|
const info = pluginContext.getModuleInfo(rawIdsByNormalizedId.get(id))
|
|
419
|
-
|
|
465
|
+
const snapshotAssets = (state.imports.get(id) ?? [])
|
|
466
|
+
.map((item) => item.resolvedId)
|
|
467
|
+
.filter((target) => state.assets.has(target) || state.rawModules.has(target))
|
|
468
|
+
for (const dependency of [...(info?.importedIds ?? []), ...(info?.dynamicallyImportedIds ?? []), ...snapshotAssets]) {
|
|
420
469
|
const normalized = normalizeModuleId(dependency)
|
|
421
470
|
if (tracked.has(normalized) && !reachable.has(normalized)) pending.push(normalized)
|
|
422
471
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Scans an app directory for binary assets (images/fonts) and emits a C++ translation
|
|
2
|
+
// Scans an app directory for binary assets (images/fonts/audio) and emits a C++ translation
|
|
3
3
|
// unit that embeds each one as a named, externally-visible `.rodata` symbol.
|
|
4
4
|
//
|
|
5
5
|
// This is the build half of "store images as files, read them as files": the
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
// an asset filename; the linker keeps one copy.
|
|
24
24
|
|
|
25
25
|
import { describeStaticAsset } from './gea-static-asset.mjs'
|
|
26
|
+
import { compileTimeOnlyAssetPaths } from './gea-embedded-asset-policy.mjs'
|
|
26
27
|
import fs from 'node:fs'
|
|
27
28
|
import path from 'node:path'
|
|
28
29
|
import process from 'node:process'
|
|
@@ -44,8 +45,19 @@ function fail(message) {
|
|
|
44
45
|
|
|
45
46
|
const appDir = path.resolve(readOption('--app-dir') ?? fail('missing --app-dir <dir>'))
|
|
46
47
|
const outCpp = path.resolve(readOption('--out-cpp') ?? fail('missing --out-cpp <file>'))
|
|
48
|
+
const manifestPath = path.join(appDir, 'package.json')
|
|
49
|
+
const compileTimeOnly = compileTimeOnlyAssetPaths(
|
|
50
|
+
fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, 'utf8')) : {},
|
|
51
|
+
)
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
for (const relative of compileTimeOnly) {
|
|
54
|
+
const file = path.join(appDir, relative)
|
|
55
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
56
|
+
fail(`compile-time-only asset is not a file: ${relative}`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ASSET_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ttf', '.otf', '.wav'])
|
|
49
61
|
// Launcher/package icons are metadata, not runtime `<Image>` assets. If an app
|
|
50
62
|
// wants to render one, keep a copy under `public/` so the reference is explicit.
|
|
51
63
|
const PRUNE_DIRS = new Set(['node_modules', 'dist', 'build'])
|
|
@@ -83,7 +95,9 @@ function collectAssets(dir) {
|
|
|
83
95
|
const rel = path.relative(appDir, full).split(path.sep).join('/')
|
|
84
96
|
if (!PRUNE_DIRS.has(entry.name) && !entry.name.startsWith('.') && rel !== 'icons') walk(full)
|
|
85
97
|
} else if (entry.isFile() && ASSET_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
|
|
86
|
-
|
|
98
|
+
if (!compileTimeOnly.has(relPath(full))) {
|
|
99
|
+
found.push(full)
|
|
100
|
+
}
|
|
87
101
|
}
|
|
88
102
|
}
|
|
89
103
|
}
|