@geastack/core 0.1.33 → 0.1.35
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/app_frame_perf.cpp +12 -0
- package/audio-worklet.d.ts +49 -0
- package/gea_app_entry.cpp +36 -3
- package/gea_sources.mjs +2 -2
- package/include/app.h +6 -0
- package/include/audio.h +11 -0
- package/include/audio_stream.h +73 -0
- package/include/canvas.h +1 -0
- package/include/pixel.h +40 -6
- package/index.d.ts +228 -8
- package/package.json +5 -2
- package/runtime/audio-worklet.ts +51 -0
- package/runtime-surface.ts +7 -0
- package/scripts/build-gea-vite-geatsc.mjs +51 -3
- package/scripts/gea-bundle-type-hints.mjs +9 -1
- package/scripts/gea-embedded-compat-staging.mjs +2 -0
- package/scripts/gea-static-asset.mjs +15 -0
- package/scripts/gea-vite-module-graph-plugin.mjs +1 -14
- package/scripts/gea-worker-modules.mjs +192 -0
- package/scripts/generate-gea-embedded-assets.mjs +2 -1
- package/typed-arrays.d.ts +13 -0
- package/workers.d.ts +66 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(new URL("../package.json", import.meta.url));
|
|
8
|
+
const { parse } = require("@babel/parser");
|
|
9
|
+
const traversal = require("@babel/traverse");
|
|
10
|
+
const traverse = traversal.default ?? traversal;
|
|
11
|
+
|
|
12
|
+
/** Follow ordinary imports and native module edges without inspecting unused tests. */
|
|
13
|
+
export function collectWorkerModuleSources(
|
|
14
|
+
entry,
|
|
15
|
+
importedFiles,
|
|
16
|
+
readSource = (file) => fs.readFileSync(file, "utf8"),
|
|
17
|
+
) {
|
|
18
|
+
const pending = [entry];
|
|
19
|
+
const visited = new Set();
|
|
20
|
+
const sources = new Map();
|
|
21
|
+
while (pending.length) {
|
|
22
|
+
const file = pending.pop();
|
|
23
|
+
if (visited.has(file)) continue;
|
|
24
|
+
visited.add(file);
|
|
25
|
+
pending.push(...importedFiles(file));
|
|
26
|
+
if (!/\.[cm]?[jt]sx?$/.test(file)) continue;
|
|
27
|
+
const resolved = resolveWorkerModules(readSource(file), file);
|
|
28
|
+
sources.set(file, resolved);
|
|
29
|
+
pending.push(...resolved.modules.map((module) => module.entry));
|
|
30
|
+
}
|
|
31
|
+
return sources;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Native equivalent of a bundler's static worker URL resolution. */
|
|
35
|
+
export function resolveWorkerModules(
|
|
36
|
+
code,
|
|
37
|
+
fileName,
|
|
38
|
+
resolveModule = (specifier) =>
|
|
39
|
+
path.resolve(path.dirname(fileName), specifier),
|
|
40
|
+
) {
|
|
41
|
+
const source = parse(code, {
|
|
42
|
+
sourceType: "module",
|
|
43
|
+
plugins: ["typescript", "jsx"],
|
|
44
|
+
});
|
|
45
|
+
const modules = [];
|
|
46
|
+
const edits = [];
|
|
47
|
+
const staticURL = (argument) => {
|
|
48
|
+
if (argument?.type === "StringLiteral") return argument.value;
|
|
49
|
+
if (
|
|
50
|
+
argument?.type !== "NewExpression" ||
|
|
51
|
+
argument.callee.type !== "Identifier" ||
|
|
52
|
+
argument.callee.name !== "URL"
|
|
53
|
+
)
|
|
54
|
+
return null;
|
|
55
|
+
const [url, base] = argument.arguments;
|
|
56
|
+
if (
|
|
57
|
+
url?.type !== "StringLiteral" ||
|
|
58
|
+
base?.type !== "MemberExpression" ||
|
|
59
|
+
base.computed ||
|
|
60
|
+
base.property.name !== "url"
|
|
61
|
+
)
|
|
62
|
+
return null;
|
|
63
|
+
if (
|
|
64
|
+
base.object.type !== "MetaProperty" ||
|
|
65
|
+
base.object.meta.name !== "import" ||
|
|
66
|
+
base.object.property.name !== "meta"
|
|
67
|
+
)
|
|
68
|
+
return null;
|
|
69
|
+
return url.value;
|
|
70
|
+
};
|
|
71
|
+
const visit = (astPath) => {
|
|
72
|
+
const node = astPath.node;
|
|
73
|
+
const binding = astPath.scope.getBinding("Worker");
|
|
74
|
+
const typeOnlyBinding =
|
|
75
|
+
binding &&
|
|
76
|
+
(binding.path.node.importKind === "type" ||
|
|
77
|
+
binding.path.parent.importKind === "type");
|
|
78
|
+
const worker =
|
|
79
|
+
node.type === "NewExpression" &&
|
|
80
|
+
node.callee.type === "Identifier" &&
|
|
81
|
+
node.callee.name === "Worker" &&
|
|
82
|
+
(!binding || typeOnlyBinding);
|
|
83
|
+
const call = node.callee;
|
|
84
|
+
const worklet =
|
|
85
|
+
node.type === "CallExpression" &&
|
|
86
|
+
call.type === "MemberExpression" &&
|
|
87
|
+
!call.computed &&
|
|
88
|
+
call.property.name === "addModule" &&
|
|
89
|
+
call.object.type === "MemberExpression" &&
|
|
90
|
+
!call.object.computed &&
|
|
91
|
+
call.object.property.name === "audioWorklet";
|
|
92
|
+
if (!worker && !worklet) return;
|
|
93
|
+
const argument = node.arguments[0];
|
|
94
|
+
const specifier = staticURL(argument);
|
|
95
|
+
if (specifier === null)
|
|
96
|
+
throw new Error(
|
|
97
|
+
`${fileName}: native worker/worklet module URL must be statically resolvable`,
|
|
98
|
+
);
|
|
99
|
+
if (!specifier.startsWith("."))
|
|
100
|
+
throw new Error(
|
|
101
|
+
`${fileName}: native worker/worklet modules must use a relative module URL`,
|
|
102
|
+
);
|
|
103
|
+
const entry = resolveModule(specifier);
|
|
104
|
+
const url = pathToFileURL(entry).href;
|
|
105
|
+
const kind = worker ? "worker" : "worklet";
|
|
106
|
+
const id = crypto
|
|
107
|
+
.createHash("sha256")
|
|
108
|
+
.update(`${kind}:${url}`)
|
|
109
|
+
.digest("hex")
|
|
110
|
+
.slice(0, 16);
|
|
111
|
+
modules.push({
|
|
112
|
+
kind,
|
|
113
|
+
entry,
|
|
114
|
+
url,
|
|
115
|
+
symbol: `gea_${kind}_${id}_top_level`,
|
|
116
|
+
id,
|
|
117
|
+
});
|
|
118
|
+
edits.push({
|
|
119
|
+
start: argument.start,
|
|
120
|
+
end: argument.end,
|
|
121
|
+
text: JSON.stringify(url),
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
traverse(source, { NewExpression: visit, CallExpression: visit });
|
|
125
|
+
let transformed = code;
|
|
126
|
+
for (const edit of edits.sort((a, b) => b.start - a.start))
|
|
127
|
+
transformed =
|
|
128
|
+
transformed.slice(0, edit.start) +
|
|
129
|
+
edit.text +
|
|
130
|
+
transformed.slice(edit.end);
|
|
131
|
+
return { code: transformed, modules };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function writeWorkerRegistry(modules, output) {
|
|
135
|
+
const includes = ['#include "host/worker.h"'];
|
|
136
|
+
if (modules.some((module) => module.kind === "worklet"))
|
|
137
|
+
includes.push('#include "host/audio_worklet.h"');
|
|
138
|
+
const declarations = modules.map((module) => `void ${module.symbol}();`);
|
|
139
|
+
const registrations = modules.map((module) => {
|
|
140
|
+
const target = module.kind === "worker" ? "workers" : "audio_worklet";
|
|
141
|
+
return ` gea::host::${target}::registerModule(${JSON.stringify(module.url)}, ${module.symbol});`;
|
|
142
|
+
});
|
|
143
|
+
const content = `${includes.join("\n")}\n${declarations.join("\n")}\nnamespace {\nstruct RegisterWorkerModules {\nRegisterWorkerModules() {\n${registrations.join("\n")}\n}\n};\nRegisterWorkerModules registeredWorkerModules;\n}\n`;
|
|
144
|
+
if (!fs.existsSync(output) || fs.readFileSync(output, "utf8") !== content)
|
|
145
|
+
fs.writeFileSync(output, content);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Validate the supported native registration subset using symbol/type identity. */
|
|
149
|
+
export function validateWorkletRegistrations(ts, entry, implementation) {
|
|
150
|
+
const program = ts.createProgram([entry], {
|
|
151
|
+
target: ts.ScriptTarget.ESNext,
|
|
152
|
+
module: ts.ModuleKind.ESNext,
|
|
153
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
154
|
+
allowJs: true,
|
|
155
|
+
skipLibCheck: true,
|
|
156
|
+
noEmit: true,
|
|
157
|
+
});
|
|
158
|
+
const checker = program.getTypeChecker();
|
|
159
|
+
const platformFile = fs.realpathSync(implementation);
|
|
160
|
+
const fail = (node, message) => {
|
|
161
|
+
const source = node.getSourceFile();
|
|
162
|
+
const location = source.getLineAndCharacterOfPosition(node.getStart());
|
|
163
|
+
throw new Error(`${source.fileName}:${location.line + 1}: ${message}`);
|
|
164
|
+
};
|
|
165
|
+
const visit = (node) => {
|
|
166
|
+
if (ts.isCallExpression(node)) {
|
|
167
|
+
const declaration = checker.getResolvedSignature(node)?.declaration;
|
|
168
|
+
if (
|
|
169
|
+
declaration &&
|
|
170
|
+
fs.realpathSync(declaration.getSourceFile().fileName) === platformFile &&
|
|
171
|
+
declaration.name?.getText() === "registerProcessor"
|
|
172
|
+
) {
|
|
173
|
+
let constructor = node.arguments[1];
|
|
174
|
+
if (!constructor) fail(node, "registerProcessor requires a processor class");
|
|
175
|
+
while (ts.isParenthesizedExpression(constructor) || ts.isAsExpression(constructor) || ts.isTypeAssertionExpression(constructor))
|
|
176
|
+
constructor = constructor.expression;
|
|
177
|
+
let symbol = checker.getSymbolAtLocation(constructor);
|
|
178
|
+
if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
179
|
+
const classDeclaration = ts.isClassExpression(constructor) ? constructor : symbol?.valueDeclaration;
|
|
180
|
+
if (!classDeclaration || (!ts.isClassDeclaration(classDeclaration) && !ts.isClassExpression(classDeclaration)))
|
|
181
|
+
fail(node, "Native registerProcessor requires a directly named class constructor; indirect constructor values are unsupported");
|
|
182
|
+
const type = checker.getTypeAtLocation(constructor);
|
|
183
|
+
if (type.getProperty("parameterDescriptors"))
|
|
184
|
+
fail(node, "AudioParam parameterDescriptors are not supported by this native AudioWorklet host");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
ts.forEachChild(node, visit);
|
|
188
|
+
};
|
|
189
|
+
for (const source of program.getSourceFiles()) {
|
|
190
|
+
if (!source.isDeclarationFile && fs.realpathSync(source.fileName) !== platformFile) visit(source);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// shared binary) without duplicate-definition errors when they share
|
|
23
23
|
// an asset filename; the linker keeps one copy.
|
|
24
24
|
|
|
25
|
+
import { describeStaticAsset } from './gea-static-asset.mjs'
|
|
25
26
|
import fs from 'node:fs'
|
|
26
27
|
import path from 'node:path'
|
|
27
28
|
import process from 'node:process'
|
|
@@ -276,7 +277,7 @@ if (assets.length === 0) {
|
|
|
276
277
|
for (const file of assets) {
|
|
277
278
|
const rel = relPath(file)
|
|
278
279
|
const sym = assetSymbol(rel)
|
|
279
|
-
const aliases = [rel]
|
|
280
|
+
const aliases = [rel, describeStaticAsset(file).url]
|
|
280
281
|
if (rel.startsWith('public/')) aliases.push(rel.slice('public/'.length))
|
|
281
282
|
const condition = aliases
|
|
282
283
|
.map((alias) => `(std::strcmp(path, ${JSON.stringify(alias)}) == 0 || (path[0] == '/' && std::strcmp(path + 1, ${JSON.stringify(alias)}) == 0))`)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Standard binary base64 APIs, with the default base64 alphabet and padding. */
|
|
2
|
+
declare global {
|
|
3
|
+
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> {
|
|
4
|
+
toBase64(): string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Uint8ArrayConstructor {
|
|
8
|
+
/** Decode using the standard default (loose) final-chunk handling. */
|
|
9
|
+
fromBase64(string: string): Uint8Array<ArrayBuffer>
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {}
|
package/workers.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Web worker APIs implemented by the native host. */
|
|
2
|
+
export type WorkerMessage = string | ArrayBuffer
|
|
3
|
+
|
|
4
|
+
export interface MessageEvent<T = WorkerMessage> {
|
|
5
|
+
readonly data: T
|
|
6
|
+
readonly ports: MessagePort[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WorkerErrorEvent {
|
|
10
|
+
readonly message: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MessagePort {
|
|
14
|
+
onmessage: ((event: MessageEvent) => void) | null
|
|
15
|
+
postMessage(message: WorkerMessage, transfer?: (ArrayBuffer | MessagePort)[]): void
|
|
16
|
+
start(): void
|
|
17
|
+
close(): void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MessageChannel {
|
|
21
|
+
readonly port1: MessagePort
|
|
22
|
+
readonly port2: MessagePort
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MessageChannelConstructor {
|
|
26
|
+
new (): MessageChannel
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WorkerOptions {
|
|
30
|
+
type?: 'classic' | 'module'
|
|
31
|
+
name?: string
|
|
32
|
+
credentials?: 'omit' | 'same-origin' | 'include'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Worker {
|
|
36
|
+
onmessage: ((event: MessageEvent) => void) | null
|
|
37
|
+
onerror: ((event: WorkerErrorEvent) => void) | null
|
|
38
|
+
postMessage(message: WorkerMessage, transfer?: (ArrayBuffer | MessagePort)[]): void
|
|
39
|
+
terminate(): void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WorkerConstructor {
|
|
43
|
+
new (scriptURL: string | URL, options?: WorkerOptions): Worker
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DedicatedWorkerGlobalScope {
|
|
47
|
+
onmessage: ((event: MessageEvent) => void) | null
|
|
48
|
+
postMessage(message: WorkerMessage, transfer?: (ArrayBuffer | MessagePort)[]): void
|
|
49
|
+
close(): void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
declare global {
|
|
53
|
+
interface ImportMeta {
|
|
54
|
+
readonly url: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface URL {
|
|
58
|
+
readonly href: string
|
|
59
|
+
toString(): string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const URL: { new (url: string | URL, base?: string | URL): URL }
|
|
63
|
+
const Worker: WorkerConstructor
|
|
64
|
+
const MessageChannel: MessageChannelConstructor
|
|
65
|
+
const self: DedicatedWorkerGlobalScope
|
|
66
|
+
}
|