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