@ankhorage/studio 1.13.4 → 2.0.0

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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/dist/app/StudioApp.d.ts.map +1 -1
  3. package/dist/app/StudioApp.js +1 -16
  4. package/dist/app/StudioApp.js.map +1 -1
  5. package/dist/app/workspace/WorkspacePrimitives.d.ts +6 -2
  6. package/dist/app/workspace/WorkspacePrimitives.d.ts.map +1 -1
  7. package/dist/app/workspace/WorkspacePrimitives.js.map +1 -1
  8. package/dist/core/apiBase.d.ts +6 -0
  9. package/dist/core/apiBase.d.ts.map +1 -0
  10. package/dist/core/apiBase.js +14 -0
  11. package/dist/core/apiBase.js.map +1 -0
  12. package/dist/core/constants.d.ts.map +1 -1
  13. package/dist/core/constants.js +7 -11
  14. package/dist/core/constants.js.map +1 -1
  15. package/dist/host/http/projectRuntimeRoutes.d.ts +11 -0
  16. package/dist/host/http/projectRuntimeRoutes.d.ts.map +1 -0
  17. package/dist/host/http/projectRuntimeRoutes.js +42 -0
  18. package/dist/host/http/projectRuntimeRoutes.js.map +1 -0
  19. package/dist/host/http/server.d.ts +2 -2
  20. package/dist/host/http/server.d.ts.map +1 -1
  21. package/dist/host/http/server.js +3 -28
  22. package/dist/host/http/server.js.map +1 -1
  23. package/dist/host/http/serverWithSecrets.d.ts +1 -1
  24. package/dist/host/http/serverWithSecrets.d.ts.map +1 -1
  25. package/dist/host/layout/generatedImportComposer.d.ts.map +1 -1
  26. package/dist/host/layout/generatedImportComposer.js +17 -10
  27. package/dist/host/layout/generatedImportComposer.js.map +1 -1
  28. package/dist/host/layout/layoutGenerator.d.ts.map +1 -1
  29. package/dist/host/layout/layoutGenerator.js +36 -17
  30. package/dist/host/layout/layoutGenerator.js.map +1 -1
  31. package/dist/host/layout/templates/rootLayout.d.ts +1 -0
  32. package/dist/host/layout/templates/rootLayout.d.ts.map +1 -1
  33. package/dist/host/layout/templates/rootLayout.js +31 -22
  34. package/dist/host/layout/templates/rootLayout.js.map +1 -1
  35. package/dist/host/layout/templates/screen.d.ts.map +1 -1
  36. package/dist/host/layout/templates/screen.js +41 -22
  37. package/dist/host/layout/templates/screen.js.map +1 -1
  38. package/dist/host/media/projectBundledMediaRegistry.d.ts.map +1 -1
  39. package/dist/host/media/projectBundledMediaRegistry.js +7 -1
  40. package/dist/host/media/projectBundledMediaRegistry.js.map +1 -1
  41. package/dist/host/orchestrator/generatedAppFiles.d.ts.map +1 -1
  42. package/dist/host/orchestrator/generatedAppFiles.js +9 -14
  43. package/dist/host/orchestrator/generatedAppFiles.js.map +1 -1
  44. package/dist/host/orchestrator/infraSession.d.ts +8 -4
  45. package/dist/host/orchestrator/infraSession.d.ts.map +1 -1
  46. package/dist/host/orchestrator/infraSession.js +34 -17
  47. package/dist/host/orchestrator/infraSession.js.map +1 -1
  48. package/dist/host/orchestrator/scaffolder.d.ts +3 -4
  49. package/dist/host/orchestrator/scaffolder.d.ts.map +1 -1
  50. package/dist/host/orchestrator/scaffolder.js +17 -20
  51. package/dist/host/orchestrator/scaffolder.js.map +1 -1
  52. package/dist/host/orchestrator/studioInfraUp.d.ts +10 -2
  53. package/dist/host/orchestrator/studioInfraUp.d.ts.map +1 -1
  54. package/dist/host/orchestrator/studioInfraUp.js +8 -4
  55. package/dist/host/orchestrator/studioInfraUp.js.map +1 -1
  56. package/dist/host/orchestrator/templates.d.ts +20 -36
  57. package/dist/host/orchestrator/templates.d.ts.map +1 -1
  58. package/dist/host/orchestrator/templates.js +730 -119
  59. package/dist/host/orchestrator/templates.js.map +1 -1
  60. package/dist/runtime/runtimeActions.js +1 -1
  61. package/dist/runtime/runtimeActions.js.map +1 -1
  62. package/dist/ui/createStudioAppBarActions.js.map +1 -1
  63. package/package.json +33 -25
@@ -1,24 +1,15 @@
1
1
  import { resolveExpoRuntimeDependencyMap, resolveExpoRuntimeNativeOutput, resolveExpoRuntimeNativeSchemeMap, } from '@ankhorage/expo-runtime/planning';
2
- const EXPO_MODULES_CORE_VERSION = '~3.0.30';
2
+ import { EXPO_PLATFORM } from '@ankhorage/expo-runtime/platform';
3
3
  const CONTRACTS_VERSION = '^8.0.0';
4
4
  const DATA_SOURCES_VERSION = '^2.0.0';
5
- const RUNTIME_VERSION = '^2.0.0';
6
- const STUDIO_VERSION = '^1.12.5';
5
+ const RUNTIME_VERSION = '^2.2.0';
6
+ const STUDIO_VERSION = '^2.0.0';
7
7
  const UTILITY_VERSION = '^0.2.0';
8
8
  const SUPABASE_AUTH_VERSION = '^1.2.1';
9
9
  const SUPABASE_STORAGE_VERSION = '^0.2.0';
10
- const ZORA_VERSION = '^2.13.2';
11
- const EXPO_RUNTIME_VERSION = '^2.6.0';
12
- const DEVTOOLS_VERSION = '^1.4.1';
13
- const EXPO_VERSION = '~54.0.37';
14
- const EXPO_DOCUMENT_PICKER_VERSION = '~14.0.8';
15
- const EXPO_FILE_SYSTEM_VERSION = '~19.0.23';
16
- const EXPO_IMAGE_PICKER_VERSION = '~17.0.11';
17
- const EXPO_SECURE_STORE_VERSION = '~15.0.8';
18
- const EXPO_UPDATES_VERSION = '~29.0.20';
19
- const EXPO_WEB_BROWSER_VERSION = '~15.0.11';
20
- const BABEL_MODULE_RESOLVER_VERSION = '^5.0.2';
21
- const TYPESCRIPT_VERSION = '~5.9.3';
10
+ const ZORA_VERSION = '^3.0.0';
11
+ const EXPO_RUNTIME_VERSION = '^3.0.1';
12
+ const DEVTOOLS_VERSION = '^1.6.0';
22
13
  const LEGACY_WEB_ONLY_TARGETS = { web: { enabled: true } };
23
14
  function serializeStringLiteral(value) {
24
15
  return `'${value
@@ -40,7 +31,7 @@ function serializeJsValue(value, indentLevel = 0) {
40
31
  }
41
32
  return `[\n${value
42
33
  .map((entry) => `${nextIndent}${serializeJsValue(entry, indentLevel + 1)}`)
43
- .join(',\n')}\n${indent}]`;
34
+ .join(',\n')},\n${indent}]`;
44
35
  }
45
36
  if (value && typeof value === 'object') {
46
37
  const entries = Object.entries(value);
@@ -63,25 +54,16 @@ function serializeSplashScreenPlugin(splashScreen) {
63
54
  if (splashScreen == null) {
64
55
  return null;
65
56
  }
66
- const serializedConfig = serializeJsValue(splashScreen, 3);
67
- return `[
68
- 'expo-splash-screen',
69
- ${serializedConfig},
70
- ]`;
57
+ return serializeJsValue(['expo-splash-screen', splashScreen], 3);
71
58
  }
72
59
  function serializeRuntimePlugin(plugin) {
73
- if (typeof plugin === 'string') {
74
- return serializeJsValue(plugin);
75
- }
76
- const [name, options] = plugin;
77
- const serializedOptions = serializeJsValue(options, 3);
78
- return `[
79
- ${serializeJsValue(name)},
80
- ${serializedOptions},
81
- ]`;
60
+ return serializeJsValue(plugin, 3);
82
61
  }
83
62
  function serializePluginsWithRuntimePlan(args) {
84
- const entries = resolveExpoRuntimeNativeOutput(args.runtimePlan).configPlugins.map(serializeRuntimePlugin);
63
+ const entries = [
64
+ serializeStringLiteral(EXPO_PLATFORM.navigation.expoRouter.name),
65
+ ...resolveExpoRuntimeNativeOutput(args.runtimePlan).configPlugins.map(serializeRuntimePlugin),
66
+ ];
85
67
  const splashPlugin = serializeSplashScreenPlugin(args.splashScreen);
86
68
  if (splashPlugin !== null) {
87
69
  entries.push(splashPlugin);
@@ -90,50 +72,50 @@ function serializePluginsWithRuntimePlan(args) {
90
72
  return '[...(config.plugins ?? [])]';
91
73
  }
92
74
  return `[
93
- ...(config.plugins ?? []),
94
- ${entries.join(',\n ')},
95
- ]`;
75
+ ...(config.plugins ?? []),
76
+ ${entries.join(',\n ')},
77
+ ]`;
96
78
  }
97
79
  function serializeAndroidConfig(args) {
98
80
  const permissions = resolveExpoRuntimeNativeOutput(args.runtimePlan).androidPermissions;
99
81
  const extraLines = permissions.length > 0
100
82
  ? `
101
- permissions: ${serializeJsValue(permissions, 2)},`
83
+ permissions: ${serializeJsValue(permissions, 3)},`
102
84
  : '';
103
85
  const schemeLine = args.scheme
104
86
  ? `
105
- scheme: ${serializeStringLiteral(args.scheme)},`
87
+ scheme: ${serializeStringLiteral(args.scheme)},`
106
88
  : '';
107
89
  return `{
108
- ...config.android,${extraLines}
109
- package: ${serializeStringLiteral(args.target.package)},${schemeLine}
110
- }`;
90
+ ...config.android,${extraLines}
91
+ package: ${serializeStringLiteral(args.target.package)},${schemeLine}
92
+ }`;
111
93
  }
112
94
  function serializeIosConfig(args) {
113
95
  const schemeLine = args.scheme
114
96
  ? `
115
- scheme: ${serializeStringLiteral(args.scheme)},`
97
+ scheme: ${serializeStringLiteral(args.scheme)},`
116
98
  : '';
117
99
  return `{
118
- ...config.ios,
119
- bundleIdentifier: ${serializeStringLiteral(args.target.bundleIdentifier)},${schemeLine}
120
- }`;
100
+ ...config.ios,
101
+ bundleIdentifier: ${serializeStringLiteral(args.target.bundleIdentifier)},${schemeLine}
102
+ }`;
121
103
  }
122
104
  function serializeTargetSections(args) {
123
105
  const sections = [];
124
106
  const nativeSchemes = resolveExpoRuntimeNativeSchemeMap(args.targets);
125
107
  if (args.targets.android?.enabled) {
126
- sections.push(` android: ${serializeAndroidConfig({ target: args.targets.android, scheme: nativeSchemes.android, runtimePlan: args.runtimePlan })},`);
108
+ sections.push(` android: ${serializeAndroidConfig({ target: args.targets.android, scheme: nativeSchemes.android, runtimePlan: args.runtimePlan })},`);
127
109
  }
128
110
  if (args.targets.ios?.enabled) {
129
- sections.push(` ios: ${serializeIosConfig({ target: args.targets.ios, scheme: nativeSchemes.ios })},`);
111
+ sections.push(` ios: ${serializeIosConfig({ target: args.targets.ios, scheme: nativeSchemes.ios })},`);
130
112
  }
131
113
  if (args.targets.web?.enabled) {
132
- sections.push(` web: {
133
- ...config.web,
134
- output: 'static',
135
- favicon: './assets/favicon.png',
136
- },`);
114
+ sections.push(` web: {
115
+ ...config.web,
116
+ output: 'static',
117
+ favicon: './assets/favicon.png',
118
+ },`);
137
119
  }
138
120
  return sections.length > 0
139
121
  ? String.fromCharCode(10) + sections.join(String.fromCharCode(10))
@@ -143,49 +125,671 @@ export function getAppConfigTs({ name, slug, targets, splashScreen = null, runti
143
125
  const targetSections = serializeTargetSections({ targets, runtimePlan });
144
126
  return `import type { ConfigContext, ExpoConfig } from 'expo/config';
145
127
 
146
- export default ({ config }: ConfigContext): ExpoConfig => {
128
+ function omitManagedTargetConfig(config: ConfigContext['config']) {
147
129
  const baseConfig = { ...config };
148
130
  delete baseConfig.scheme;
149
131
  delete baseConfig.android;
150
132
  delete baseConfig.ios;
151
133
  delete baseConfig.web;
134
+ return baseConfig;
135
+ }
136
+
137
+ export default ({ config }: ConfigContext): ExpoConfig => {
138
+ const baseConfig = omitManagedTargetConfig(config);
152
139
 
153
140
  return {
154
141
  ...baseConfig,
155
142
  name: ${serializeStringLiteral(name)},
156
143
  slug: ${serializeStringLiteral(slug)},
157
- plugins: ${serializePluginsWithRuntimePlan({ splashScreen, runtimePlan })},${targetSections}
144
+ plugins: ${serializePluginsWithRuntimePlan({ splashScreen, runtimePlan })},
145
+ experiments: {
146
+ ...config.experiments,
147
+ reactCompiler: true,
148
+ typedRoutes: true,
149
+ },${targetSections}
158
150
  };
159
151
  };
160
152
  `;
161
153
  }
162
- export function getMetroConfigJs() {
163
- const metroConfigJs = `const { getDefaultConfig } = require('expo/metro-config');
154
+ export function getAndroidRunTs(args) {
155
+ return `import { spawn } from 'node:child_process';
156
+ import { readFile } from 'node:fs/promises';
157
+ import path from 'node:path';
164
158
 
165
- module.exports = getDefaultConfig(__dirname);
166
- `;
167
- return metroConfigJs;
159
+ const PUBLIC_SUPABASE_URL = 'EXPO_PUBLIC_SUPABASE_URL';
160
+ const PUBLIC_STUDIO_API_URL = 'EXPO_PUBLIC_API_URL';
161
+ const STUDIO_HOST_URL = 'ANKH_STUDIO_HOST_URL';
162
+ const DEFAULT_STUDIO_HOST_URL = 'http://127.0.0.1:3000';
163
+ const DEFAULT_STUDIO_API_URL = 'http://127.0.0.1:3000/api';
164
+ const LOOPBACK_HOSTNAMES = new Set(['127.0.0.1', '::1', '[::1]', 'localhost']);
165
+ const ADB_TRACK_READY_TIMEOUT_MS = 5_000;
166
+ const ADB_REVERSE_ATTEMPTS = 5;
167
+ const ADB_REVERSE_RETRY_DELAY_MS = 100;
168
+ const projectId = ${JSON.stringify(args.projectId)};
169
+ const includeStudio = ${JSON.stringify(args.includeStudio)};
170
+ const projectRoot = process.cwd();
171
+
172
+ const environmentPath = path.join(projectRoot, '.env.local');
173
+ const supabaseUrl = await readEffectiveEnvValue(environmentPath, PUBLIC_SUPABASE_URL);
174
+ const studioApiUrl = includeStudio
175
+ ? await readEffectiveEnvValue(environmentPath, PUBLIC_STUDIO_API_URL)
176
+ : undefined;
177
+ const supabaseMapping = supabaseUrl
178
+ ? await prepareAndroidLoopbackBridge(supabaseUrl)
179
+ : undefined;
180
+ const studioApiMapping = includeStudio
181
+ ? await prepareStudioApiBridge(studioApiUrl ?? DEFAULT_STUDIO_API_URL)
182
+ : undefined;
183
+ const reverseMappings = deduplicateReverseMappings([supabaseMapping, studioApiMapping]);
184
+ const expoArgs = process.argv.slice(2);
185
+
186
+ const expoExecutable = path.join(
187
+ projectRoot,
188
+ 'node_modules',
189
+ '.bin',
190
+ process.platform === 'win32' ? 'expo.cmd' : 'expo',
191
+ );
192
+ const androidBridge = reverseMappings.length > 0
193
+ ? await startAndroidBridgeSupervisor(reverseMappings, expoArgs)
194
+ : undefined;
195
+ await runExpoCommand(expoExecutable, ['run:android', ...expoArgs], androidBridge);
196
+
197
+ interface AndroidReverseMapping {
198
+ readonly local: string;
199
+ readonly remote: string;
168
200
  }
169
- export function getBabelConfigJs() {
170
- return `module.exports = function (api) {
171
- api.cache(true);
172
201
 
173
- return {
174
- presets: ['babel-preset-expo'],
175
- plugins: [
176
- [
177
- 'module-resolver',
178
- {
179
- root: ['./src'],
180
- alias: {
181
- '@': './src',
182
- '@root': './',
183
- },
184
- },
185
- ],
186
- ],
202
+ async function prepareAndroidLoopbackBridge(
203
+ value: string,
204
+ ): Promise<AndroidReverseMapping | undefined> {
205
+ let url: URL;
206
+ try {
207
+ url = new URL(value);
208
+ } catch (error) {
209
+ throw new Error(\`\${PUBLIC_SUPABASE_URL} is not a valid URL. Run Infrastructure Up again.\`, {
210
+ cause: error,
211
+ });
212
+ }
213
+
214
+ if (!LOOPBACK_HOSTNAMES.has(url.hostname)) return undefined;
215
+
216
+ const port = resolveUrlPort(url, PUBLIC_SUPABASE_URL);
217
+ if (!(await isLocalGatewayReachable(url))) {
218
+ await ensureStudioInfrastructureRuntime();
219
+ if (!(await isLocalGatewayReachable(url))) {
220
+ throw new Error(
221
+ \`Studio reported that infrastructure runtime recovery succeeded, but the local Supabase gateway is still unavailable at \${url.origin}. Run Infrastructure Up and try again.\`,
222
+ );
223
+ }
224
+ }
225
+ return createTcpReverseMapping(port);
226
+ }
227
+
228
+ async function prepareStudioApiBridge(value: string): Promise<AndroidReverseMapping | undefined> {
229
+ let url: URL;
230
+ try {
231
+ url = new URL(value);
232
+ } catch (error) {
233
+ throw new Error(\`\${PUBLIC_STUDIO_API_URL} is not a valid URL: \${value}\`, {
234
+ cause: error,
235
+ });
236
+ }
237
+
238
+ if (!LOOPBACK_HOSTNAMES.has(url.hostname)) return undefined;
239
+ const port = resolveUrlPort(url, PUBLIC_STUDIO_API_URL);
240
+
241
+ if (!(await isStudioHostReachable(url))) {
242
+ throw new Error(
243
+ \`Studio Host is unavailable at \${url.origin}. Start \\\`bun run dev:host\\\` and try again.\`,
244
+ );
245
+ }
246
+ return createTcpReverseMapping(port);
247
+ }
248
+
249
+ function createTcpReverseMapping(port: string): AndroidReverseMapping {
250
+ return { local: \`tcp:\${port}\`, remote: \`tcp:\${port}\` };
251
+ }
252
+
253
+ function deduplicateReverseMappings(
254
+ mappings: readonly (AndroidReverseMapping | undefined)[],
255
+ ): readonly AndroidReverseMapping[] {
256
+ const unique = new Map<string, AndroidReverseMapping>();
257
+ for (const mapping of mappings) {
258
+ if (mapping) unique.set(\`\${mapping.local}:\${mapping.remote}\`, mapping);
259
+ }
260
+ return [...unique.values()];
261
+ }
262
+
263
+ interface AndroidTransport {
264
+ readonly key: string;
265
+ readonly model: string | undefined;
266
+ readonly serial: string;
267
+ readonly transportId: string;
268
+ }
269
+
270
+ interface AndroidBridgeSupervisor {
271
+ readonly failure: Promise<never>;
272
+ stop(): Promise<void>;
273
+ }
274
+
275
+ async function startAndroidBridgeSupervisor(
276
+ mappings: readonly AndroidReverseMapping[],
277
+ expoArgs: readonly string[],
278
+ ): Promise<AndroidBridgeSupervisor> {
279
+ const requestedDevice = resolveRequestedAndroidDevice(expoArgs);
280
+ const tracker = spawn('adb', ['track-devices', '-l'], {
281
+ cwd: projectRoot,
282
+ env: process.env,
283
+ stdio: ['ignore', 'pipe', 'pipe'],
284
+ });
285
+ let bufferedOutput: Buffer = Buffer.alloc(0);
286
+ let latestTransports = new Map<string, AndroidTransport>();
287
+ let selectedSerial: string | undefined;
288
+ const verifiedTransports = new Set<string>();
289
+ let trackerStderr = '';
290
+ let stopping = false;
291
+ let failed = false;
292
+ let readySettled = false;
293
+ let stopPromise: Promise<void> | undefined;
294
+ let processing = Promise.resolve();
295
+ let resolveReady!: () => void;
296
+ let rejectReady!: (error: Error) => void;
297
+ let rejectFailure!: (error: Error) => void;
298
+
299
+ const ready = new Promise<void>((resolve, reject) => {
300
+ resolveReady = resolve;
301
+ rejectReady = reject;
302
+ });
303
+ const failure = new Promise<never>((_resolve, reject) => {
304
+ rejectFailure = reject;
305
+ });
306
+ void failure.catch(() => undefined);
307
+ const trackerExit = new Promise<void>((resolve) => {
308
+ tracker.once('close', (code, signal) => {
309
+ resolve();
310
+ if (stopping || failed) return;
311
+ const detail = trackerStderr.trim();
312
+ failSupervisor(
313
+ new Error(
314
+ \`ADB transport tracking stopped unexpectedly\${signal ? \` from signal \${signal}\` : \` with code \${code ?? 1}\`}\${detail ? \`: \${detail}\` : '.'}\`,
315
+ ),
316
+ );
317
+ });
318
+ });
319
+
320
+ tracker.once('error', failSupervisor);
321
+ tracker.stderr?.setEncoding('utf8');
322
+ tracker.stderr?.on('data', (chunk: string) => {
323
+ trackerStderr = (trackerStderr + chunk).slice(-4_096);
324
+ });
325
+ tracker.stdout?.on('data', (chunk: Buffer | string) => {
326
+ if (stopping || failed) return;
327
+ bufferedOutput = Buffer.concat([
328
+ bufferedOutput,
329
+ Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk),
330
+ ]);
331
+ let frames: readonly string[];
332
+ try {
333
+ const parsed = readAdbTrackFrames(bufferedOutput);
334
+ bufferedOutput = parsed.remaining;
335
+ frames = parsed.frames;
336
+ } catch (error) {
337
+ failSupervisor(error);
338
+ return;
339
+ }
340
+
341
+ for (const frame of frames) {
342
+ let transports: readonly AndroidTransport[];
343
+ try {
344
+ transports = parseAndroidTransports(frame);
345
+ } catch (error) {
346
+ failSupervisor(error);
347
+ return;
348
+ }
349
+ latestTransports = new Map(transports.map((transport) => [transport.key, transport]));
350
+ processing = processing.then(async () => {
351
+ const currentTransports = [...latestTransports.values()];
352
+ selectedSerial ??= await resolveAndroidTargetSerial(currentTransports, requestedDevice);
353
+ for (const key of verifiedTransports) {
354
+ if (!latestTransports.has(key)) verifiedTransports.delete(key);
355
+ }
356
+ for (const transport of currentTransports) {
357
+ if (transport.serial !== selectedSerial) continue;
358
+ if (latestTransports.get(transport.key) !== transport) continue;
359
+ if (verifiedTransports.has(transport.key)) continue;
360
+ if (await ensureTransportReverses(transport, mappings, () => latestTransports)) {
361
+ verifiedTransports.add(transport.key);
362
+ console.info(
363
+ \`[android-dev] Bridged Android transport \${transport.serial} (transport_id \${transport.transportId}) \${mappings.map((mapping) => \`\${mapping.local} -> \${mapping.remote}\`).join(', ')}.\`,
364
+ );
365
+ }
366
+ }
367
+ settleReady();
368
+ });
369
+ void processing.catch(failSupervisor);
370
+ }
371
+ });
372
+
373
+ const readyTimeout = setTimeout(() => {
374
+ failSupervisor(
375
+ new Error(
376
+ \`ADB did not provide an initial transport snapshot within \${ADB_TRACK_READY_TIMEOUT_MS}ms. Ensure adb is installed and try again.\`,
377
+ ),
378
+ );
379
+ }, ADB_TRACK_READY_TIMEOUT_MS);
380
+ try {
381
+ await ready;
382
+ } catch (error) {
383
+ await stop();
384
+ throw error;
385
+ } finally {
386
+ clearTimeout(readyTimeout);
387
+ }
388
+
389
+ return { failure, stop };
390
+
391
+ function settleReady(): void {
392
+ if (readySettled) return;
393
+ readySettled = true;
394
+ resolveReady();
395
+ }
396
+
397
+ function failSupervisor(error: unknown): void {
398
+ if (stopping || failed) return;
399
+ failed = true;
400
+ const resolvedError = toError(error);
401
+ if (!readySettled) {
402
+ readySettled = true;
403
+ rejectReady(resolvedError);
404
+ }
405
+ rejectFailure(resolvedError);
406
+ if (tracker.exitCode === null && tracker.signalCode === null) tracker.kill('SIGTERM');
407
+ }
408
+
409
+ function stop(): Promise<void> {
410
+ stopPromise ??= (async () => {
411
+ stopping = true;
412
+ if (tracker.exitCode === null && tracker.signalCode === null) tracker.kill('SIGTERM');
413
+ await trackerExit;
414
+ await processing.catch(() => undefined);
415
+ })();
416
+ return stopPromise;
417
+ }
418
+ }
419
+
420
+ function resolveRequestedAndroidDevice(args: readonly string[]): string | undefined {
421
+ for (let index = 0; index < args.length; index += 1) {
422
+ const argument = args[index];
423
+ if (argument === undefined) continue;
424
+ const assignment = /^(?:--device|-d)=(.*)$/u.exec(argument);
425
+ if (assignment) {
426
+ const value = assignment[1];
427
+ if (value) return value;
428
+ throw new Error(
429
+ 'Android bridge supervision requires a device value. Use --device <device-name>.',
430
+ );
431
+ }
432
+ if (argument !== '--device' && argument !== '-d') continue;
433
+ const value = args[index + 1];
434
+ if (value && !value.startsWith('-')) return value;
435
+ throw new Error(
436
+ 'Interactive Expo device selection cannot be supervised safely. Use --device <device-name>.',
437
+ );
438
+ }
439
+ return undefined;
440
+ }
441
+
442
+ async function resolveAndroidTargetSerial(
443
+ transports: readonly AndroidTransport[],
444
+ requestedDevice: string | undefined,
445
+ ): Promise<string | undefined> {
446
+ if (!requestedDevice) {
447
+ // Expo CLI selects the first attached device when --device is omitted.
448
+ return transports[0]?.serial;
449
+ }
450
+
451
+ const directMatch = transports.find(
452
+ (transport) =>
453
+ transport.serial === requestedDevice ||
454
+ (!transport.serial.startsWith('emulator-') && transport.model === requestedDevice),
455
+ );
456
+ if (directMatch) return directMatch.serial;
457
+
458
+ for (const transport of transports) {
459
+ if (!transport.serial.startsWith('emulator-')) continue;
460
+ const result = await runCapturedCommand('adb', [
461
+ '-s',
462
+ transport.serial,
463
+ 'emu',
464
+ 'avd',
465
+ 'name',
466
+ ]);
467
+ const avdName = result.stdout.split(/\\r?\\n/u).find((line) => line.trim())?.trim();
468
+ if (avdName === requestedDevice) return transport.serial;
469
+ }
470
+
471
+ return undefined;
472
+ }
473
+
474
+ function readAdbTrackFrames(buffer: Buffer): {
475
+ readonly frames: readonly string[];
476
+ readonly remaining: Buffer;
477
+ } {
478
+ const frames: string[] = [];
479
+ let offset = 0;
480
+ while (buffer.length - offset >= 4) {
481
+ const header = buffer.subarray(offset, offset + 4).toString('ascii');
482
+ if (!/^[0-9a-f]{4}$/iu.test(header)) {
483
+ throw new Error(\`ADB transport tracking returned an invalid frame header: \${header}.\`);
484
+ }
485
+ const length = Number.parseInt(header, 16);
486
+ if (buffer.length - offset - 4 < length) break;
487
+ const start = offset + 4;
488
+ frames.push(buffer.subarray(start, start + length).toString('utf8'));
489
+ offset = start + length;
490
+ }
491
+ return { frames, remaining: buffer.subarray(offset) };
492
+ }
493
+
494
+ function parseAndroidTransports(snapshot: string): readonly AndroidTransport[] {
495
+ const transports: AndroidTransport[] = [];
496
+ for (const line of snapshot.split(/\\r?\\n/u)) {
497
+ const fields = line.trim().split(/\\s+/u);
498
+ if (fields.length < 2 || fields[1] !== 'device') continue;
499
+ const serial = fields[0];
500
+ const modelField = fields.find((field) => field.startsWith('model:'));
501
+ const model = modelField?.slice('model:'.length);
502
+ const transportField = fields.find((field) => field.startsWith('transport_id:'));
503
+ const transportId = transportField?.slice('transport_id:'.length);
504
+ if (!serial || !transportId || !/^\\d+$/u.test(transportId)) {
505
+ throw new Error(
506
+ \`ADB did not report a transport_id for authorized Android device '\${serial ?? 'unknown'}'. Update Android platform-tools and try again.\`,
507
+ );
508
+ }
509
+ transports.push({ key: \`\${serial}:\${transportId}\`, model, serial, transportId });
510
+ }
511
+ return transports;
512
+ }
513
+
514
+ async function ensureTransportReverses(
515
+ transport: AndroidTransport,
516
+ mappings: readonly AndroidReverseMapping[],
517
+ getLatestTransports: () => ReadonlyMap<string, AndroidTransport>,
518
+ ): Promise<boolean> {
519
+ for (const mapping of mappings) {
520
+ if (!(await ensureTransportReverse(transport, mapping, getLatestTransports))) return false;
521
+ }
522
+ return true;
523
+ }
524
+
525
+ async function ensureTransportReverse(
526
+ transport: AndroidTransport,
527
+ mapping: AndroidReverseMapping,
528
+ getLatestTransports: () => ReadonlyMap<string, AndroidTransport>,
529
+ ): Promise<boolean> {
530
+ let lastError: Error | undefined;
531
+ for (let attempt = 1; attempt <= ADB_REVERSE_ATTEMPTS; attempt += 1) {
532
+ if (!getLatestTransports().has(transport.key)) return false;
533
+ try {
534
+ if (!(await hasTransportReverse(transport.serial, mapping))) {
535
+ await runCapturedCommand('adb', [
536
+ '-s',
537
+ transport.serial,
538
+ 'reverse',
539
+ mapping.local,
540
+ mapping.remote,
541
+ ]);
542
+ }
543
+ if (await hasTransportReverse(transport.serial, mapping)) return true;
544
+ throw new Error(
545
+ \`adb reverse --list did not contain \${mapping.local} -> \${mapping.remote}.\`,
546
+ );
547
+ } catch (error) {
548
+ lastError = toError(error);
549
+ if (!getLatestTransports().has(transport.key)) return false;
550
+ if (attempt < ADB_REVERSE_ATTEMPTS) await delay(ADB_REVERSE_RETRY_DELAY_MS);
551
+ }
552
+ }
553
+ throw new Error(
554
+ \`Could not bridge Android device \${transport.serial} (transport_id \${transport.transportId}) after \${ADB_REVERSE_ATTEMPTS} attempts: \${lastError?.message ?? 'unknown ADB error'}\`,
555
+ { cause: lastError },
556
+ );
557
+ }
558
+
559
+ async function hasTransportReverse(
560
+ serial: string,
561
+ mapping: AndroidReverseMapping,
562
+ ): Promise<boolean> {
563
+ const result = await runCapturedCommand('adb', ['-s', serial, 'reverse', '--list']);
564
+ return result.stdout.split(/\\r?\\n/u).some((line) => {
565
+ const fields = line.trim().split(/\\s+/u);
566
+ return fields.at(-2) === mapping.local && fields.at(-1) === mapping.remote;
567
+ });
568
+ }
569
+
570
+ interface CapturedCommandResult {
571
+ readonly stdout: string;
572
+ readonly stderr: string;
573
+ }
574
+
575
+ async function runCapturedCommand(
576
+ command: string,
577
+ args: readonly string[],
578
+ ): Promise<CapturedCommandResult> {
579
+ const child = spawn(command, args, {
580
+ cwd: projectRoot,
581
+ env: process.env,
582
+ stdio: ['ignore', 'pipe', 'pipe'],
583
+ });
584
+ let stdout = '';
585
+ let stderr = '';
586
+ child.stdout?.setEncoding('utf8');
587
+ child.stderr?.setEncoding('utf8');
588
+ child.stdout?.on('data', (chunk: string) => {
589
+ stdout += chunk;
590
+ });
591
+ child.stderr?.on('data', (chunk: string) => {
592
+ stderr += chunk;
593
+ });
594
+ const exitCode = await new Promise<number>((resolve, reject) => {
595
+ child.once('error', reject);
596
+ child.once('exit', (code, signal) => {
597
+ if (signal) {
598
+ reject(new Error(\`\${command} exited from signal \${signal}.\`));
599
+ return;
600
+ }
601
+ resolve(code ?? 1);
602
+ });
603
+ });
604
+ if (exitCode !== 0) {
605
+ const detail = stderr.trim() || stdout.trim();
606
+ throw new Error(
607
+ \`\${command} \${args.join(' ')} exited with code \${exitCode}\${detail ? \`: \${detail}\` : '.'}\`,
608
+ );
609
+ }
610
+ return { stdout, stderr };
611
+ }
612
+
613
+ function resolveUrlPort(url: URL, environmentKey: string): string {
614
+ if (url.port) return url.port;
615
+ if (url.protocol === 'http:') return '80';
616
+ if (url.protocol === 'https:') return '443';
617
+ throw new Error(\`\${environmentKey} must use HTTP or HTTPS.\`);
618
+ }
619
+
620
+ async function isLocalGatewayReachable(url: URL): Promise<boolean> {
621
+ const healthUrl = new URL('/auth/v1/health', url.origin);
622
+ try {
623
+ await fetch(healthUrl, { signal: AbortSignal.timeout(5_000) });
624
+ return true;
625
+ } catch {
626
+ return false;
627
+ }
628
+ }
629
+
630
+ async function isStudioHostReachable(url: URL): Promise<boolean> {
631
+ const healthUrl = new URL('/health', url.origin);
632
+ try {
633
+ const response = await fetch(healthUrl, { signal: AbortSignal.timeout(5_000) });
634
+ return response.ok;
635
+ } catch {
636
+ return false;
637
+ }
638
+ }
639
+
640
+ async function ensureStudioInfrastructureRuntime(): Promise<void> {
641
+ const studioHostValue = process.env[STUDIO_HOST_URL] ?? DEFAULT_STUDIO_HOST_URL;
642
+ let endpoint: URL;
643
+ try {
644
+ endpoint = new URL(
645
+ \`/api/projects/\${encodeURIComponent(projectId)}/infra/runtime/ensure\`,
646
+ studioHostValue,
647
+ );
648
+ } catch (error) {
649
+ throw new Error(\`\${STUDIO_HOST_URL} is not a valid URL: \${studioHostValue}\`, {
650
+ cause: error,
651
+ });
652
+ }
653
+
654
+ let response: Response;
655
+ try {
656
+ response = await fetch(endpoint, {
657
+ method: 'POST',
658
+ signal: AbortSignal.timeout(30_000),
659
+ });
660
+ } catch (error) {
661
+ throw new Error(
662
+ \`Could not ask the Studio host at \${endpoint.origin} to restore project '\${projectId}' infrastructure runtime. Start the Studio host, run Infrastructure Up if needed, and try again.\`,
663
+ { cause: error },
664
+ );
665
+ }
666
+
667
+ if (response.ok) return;
668
+
669
+ const detail = await readErrorDetail(response);
670
+ throw new Error(
671
+ \`Studio could not restore project '\${projectId}' infrastructure runtime (HTTP \${response.status})\${detail ? \`: \${detail}\` : '.'} Run Infrastructure Up and try again.\`,
672
+ );
673
+ }
674
+
675
+ async function readErrorDetail(response: Response): Promise<string | undefined> {
676
+ try {
677
+ const body: unknown = await response.json();
678
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) return undefined;
679
+ const error = Reflect.get(body, 'error');
680
+ return typeof error === 'string' && error.trim() ? error.trim() : undefined;
681
+ } catch {
682
+ return undefined;
683
+ }
684
+ }
685
+
686
+ async function readEnvValue(filePath: string, key: string): Promise<string | undefined> {
687
+ let content: string;
688
+ try {
689
+ content = await readFile(filePath, 'utf8');
690
+ } catch (error) {
691
+ if ((error as { code?: unknown }).code === 'ENOENT') return undefined;
692
+ throw error;
693
+ }
694
+
695
+ for (const line of content.split(/\\r?\\n/u)) {
696
+ const trimmed = line.trim();
697
+ if (!trimmed || trimmed.startsWith('#')) continue;
698
+ const separator = trimmed.indexOf('=');
699
+ if (separator < 1) continue;
700
+ const candidateKey = trimmed.slice(0, separator).replace(/^export\\s+/u, '').trim();
701
+ if (candidateKey !== key) continue;
702
+ return stripMatchingQuotes(trimmed.slice(separator + 1).trim());
703
+ }
704
+
705
+ return undefined;
706
+ }
707
+
708
+ async function readEffectiveEnvValue(
709
+ filePath: string,
710
+ key: string,
711
+ ): Promise<string | undefined> {
712
+ const processValue = process.env[key];
713
+ if (typeof processValue === 'string' && processValue.length > 0) return processValue;
714
+ return readEnvValue(filePath, key);
715
+ }
716
+
717
+ function stripMatchingQuotes(value: string): string {
718
+ const first = value.at(0);
719
+ const last = value.at(-1);
720
+ return value.length >= 2 && first === last && (first === "'" || first === '"')
721
+ ? value.slice(1, -1)
722
+ : value;
723
+ }
724
+
725
+ async function runExpoCommand(
726
+ command: string,
727
+ args: readonly string[],
728
+ androidBridge?: AndroidBridgeSupervisor,
729
+ ): Promise<void> {
730
+ const child = spawn(command, args, {
731
+ cwd: projectRoot,
732
+ env: process.env,
733
+ stdio: 'inherit',
734
+ });
735
+ const exit = new Promise<{ readonly code: number; readonly signal?: NodeJS.Signals }>(
736
+ (resolve, reject) => {
737
+ child.once('error', reject);
738
+ child.once('exit', (code, signal) => {
739
+ if (signal) {
740
+ resolve({ code: code ?? 1, signal });
741
+ return;
742
+ }
743
+ resolve({ code: code ?? 1 });
744
+ });
745
+ },
746
+ );
747
+ const stopFromSignal = (signal: NodeJS.Signals) => {
748
+ void androidBridge?.stop();
749
+ if (child.exitCode === null && child.signalCode === null) child.kill(signal);
187
750
  };
188
- };
751
+ const onSigint = () => stopFromSignal('SIGINT');
752
+ const onSigterm = () => stopFromSignal('SIGTERM');
753
+ process.once('SIGINT', onSigint);
754
+ process.once('SIGTERM', onSigterm);
755
+
756
+ try {
757
+ const expoOutcome = exit.then((result) => ({ kind: 'expo' as const, result }));
758
+ const outcome = androidBridge
759
+ ? await Promise.race([
760
+ expoOutcome,
761
+ androidBridge.failure.catch((error: unknown) => ({
762
+ kind: 'bridge' as const,
763
+ error: toError(error),
764
+ })),
765
+ ])
766
+ : await expoOutcome;
767
+
768
+ if (outcome.kind === 'bridge') {
769
+ if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
770
+ await exit;
771
+ throw outcome.error;
772
+ }
773
+ if (outcome.result.signal) {
774
+ throw new Error(\`\${command} exited from signal \${outcome.result.signal}.\`);
775
+ }
776
+ if (outcome.result.code !== 0) {
777
+ throw new Error(\`\${command} exited with code \${outcome.result.code}.\`);
778
+ }
779
+ } finally {
780
+ process.off('SIGINT', onSigint);
781
+ process.off('SIGTERM', onSigterm);
782
+ await androidBridge?.stop();
783
+ }
784
+ }
785
+
786
+ function delay(milliseconds: number): Promise<void> {
787
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
788
+ }
789
+
790
+ function toError(error: unknown): Error {
791
+ return error instanceof Error ? error : new Error(String(error));
792
+ }
189
793
  `;
190
794
  }
191
795
  export function getPackageJson(args) {
@@ -193,14 +797,22 @@ export function getPackageJson(args) {
193
797
  const runtimeDependencies = resolveExpoRuntimeDependencyMap(runtimePlan);
194
798
  const pkgJson = {
195
799
  name,
196
- main: 'index.js',
800
+ main: `${EXPO_PLATFORM.navigation.expoRouter.name}/entry`,
801
+ engines: {
802
+ node: EXPO_PLATFORM.tooling.node.version,
803
+ },
197
804
  packageManager: 'bun@1.3.14',
198
805
  version: '1.0.0',
199
806
  scripts: {
200
807
  start: 'expo start',
201
- ...(targets.android?.enabled ? { android: 'expo run:android' } : {}),
808
+ ...(targets.android?.enabled ? { android: 'bun scripts/ankh-android.ts' } : {}),
202
809
  ...(targets.ios?.enabled ? { ios: 'expo run:ios' } : {}),
203
810
  ...(targets.web?.enabled ? { web: 'expo start --web' } : {}),
811
+ ...(targets.web?.enabled
812
+ ? { 'export:web': 'expo export --platform web --output-dir dist-web' }
813
+ : {}),
814
+ doctor: 'expo-doctor',
815
+ typecheck: 'tsc --noEmit',
204
816
  lint: 'ankhorage-eslint . --max-warnings=0',
205
817
  'lint:fix': 'ankhorage-eslint . --fix --max-warnings=0',
206
818
  format: 'ankhorage-prettier --write .',
@@ -211,61 +823,62 @@ export function getPackageJson(args) {
211
823
  '@ankhorage/data-sources': DATA_SOURCES_VERSION,
212
824
  '@ankhorage/expo-runtime': EXPO_RUNTIME_VERSION,
213
825
  '@ankhorage/runtime': RUNTIME_VERSION,
214
- '@ankhorage/studio': STUDIO_VERSION,
826
+ ...(includeStudio ? { '@ankhorage/studio': STUDIO_VERSION } : {}),
215
827
  ...(authProvider !== null ? { '@ankhorage/utility': UTILITY_VERSION } : {}),
216
828
  ...(authProvider === 'supabase'
217
829
  ? {
218
830
  '@ankhorage/supabase-auth': SUPABASE_AUTH_VERSION,
219
- 'expo-secure-store': EXPO_SECURE_STORE_VERSION,
220
- 'expo-web-browser': EXPO_WEB_BROWSER_VERSION,
831
+ ...createPlatformDependencyMap([
832
+ EXPO_PLATFORM.packages.secureStore,
833
+ EXPO_PLATFORM.packages.webBrowser,
834
+ ]),
221
835
  }
222
836
  : {}),
223
837
  ...(storageProvider === 'supabase'
224
838
  ? { '@ankhorage/supabase-storage': SUPABASE_STORAGE_VERSION }
225
839
  : {}),
226
840
  '@ankhorage/zora': ZORA_VERSION,
841
+ '@react-native-picker/picker': '2.11.4',
842
+ '@react-native-vector-icons/fontawesome': '^13.1.3',
843
+ '@react-native-vector-icons/fontawesome5': '^13.1.3',
844
+ '@react-native-vector-icons/fontawesome6': '^13.1.3',
845
+ '@react-native-vector-icons/ionicons': '^13.1.3',
227
846
  ...runtimeDependencies,
228
847
  ...(includeStudio
229
- ? {
230
- '@expo/vector-icons': '^15.0.3',
231
- '@react-native-picker/picker': '2.11.1',
232
- 'expo-document-picker': EXPO_DOCUMENT_PICKER_VERSION,
233
- 'expo-file-system': EXPO_FILE_SYSTEM_VERSION,
234
- 'expo-image-picker': EXPO_IMAGE_PICKER_VERSION,
235
- }
848
+ ? createPlatformDependencyMap([
849
+ EXPO_PLATFORM.packages.documentPicker,
850
+ EXPO_PLATFORM.packages.fileSystem,
851
+ EXPO_PLATFORM.packages.imagePicker,
852
+ ])
236
853
  : {}),
237
854
  '@react-navigation/bottom-tabs': '^7.18.2',
238
855
  '@react-navigation/drawer': '^7.5.0',
239
- 'babel-preset-expo': '^54.0.10',
240
- expo: EXPO_VERSION,
241
- 'expo-constants': '~18.0.14',
242
- 'expo-font': '~14.0.12',
243
- 'expo-linear-gradient': '~15.0.8',
244
- 'expo-linking': '~8.0.12',
245
- 'expo-modules-core': EXPO_MODULES_CORE_VERSION,
246
- 'expo-router': '~6.0.24',
247
- 'expo-splash-screen': '~31.0.10',
248
- 'expo-status-bar': '^3.0.9',
249
- 'expo-updates': EXPO_UPDATES_VERSION,
250
- react: '19.1.0',
251
- 'react-dom': '19.1.0',
252
- 'react-native': '0.81.5',
253
- 'react-native-gesture-handler': '~2.28.0',
254
- 'react-native-reanimated': '~4.1.1',
255
- 'react-native-safe-area-context': '~5.6.0',
256
- 'react-native-screens': '~4.16.0',
257
- 'react-native-svg': '~15.12.1',
258
- 'react-native-web': '^0.21.2',
259
- 'react-native-worklets': '0.5.1',
260
- 'reanimated-color-picker': '^4.2.0',
856
+ ...createPlatformDependencyMap([
857
+ EXPO_PLATFORM.runtime.expo,
858
+ EXPO_PLATFORM.runtime.react,
859
+ EXPO_PLATFORM.runtime.reactDom,
860
+ EXPO_PLATFORM.runtime.reactNative,
861
+ EXPO_PLATFORM.runtime.reactNativeWeb,
862
+ EXPO_PLATFORM.navigation.expoRouter,
863
+ EXPO_PLATFORM.navigation.safeArea,
864
+ EXPO_PLATFORM.navigation.screens,
865
+ EXPO_PLATFORM.animation.gestureHandler,
866
+ EXPO_PLATFORM.animation.reanimated,
867
+ EXPO_PLATFORM.animation.worklets,
868
+ EXPO_PLATFORM.ui.svg,
869
+ EXPO_PLATFORM.packages.constants,
870
+ EXPO_PLATFORM.packages.linking,
871
+ EXPO_PLATFORM.packages.splashScreen,
872
+ EXPO_PLATFORM.packages.statusBar,
873
+ ]),
261
874
  },
262
875
  devDependencies: {
263
876
  '@ankhorage/devtools': DEVTOOLS_VERSION,
264
- '@types/node': '^25.6.0',
265
- '@types/react': '~19.1.0',
877
+ [EXPO_PLATFORM.tooling.expoDoctor.name]: EXPO_PLATFORM.tooling.expoDoctor.version,
878
+ [EXPO_PLATFORM.tooling.nodeTypes.name]: EXPO_PLATFORM.tooling.nodeTypes.version,
879
+ '@types/react': '~19.2.2',
266
880
  '@types/culori': '^4.0.1',
267
- 'babel-plugin-module-resolver': BABEL_MODULE_RESOLVER_VERSION,
268
- typescript: TYPESCRIPT_VERSION,
881
+ [EXPO_PLATFORM.tooling.typescript.name]: EXPO_PLATFORM.tooling.typescript.version,
269
882
  },
270
883
  };
271
884
  return pkgJson;
@@ -297,16 +910,11 @@ export function getPrettierRcJs() {
297
910
  return `module.exports = require('@ankhorage/devtools/prettier');
298
911
  `;
299
912
  }
300
- export function getIndexJs() {
301
- return `import 'expo-router/entry';
302
- `;
303
- }
304
913
  export function getTsConfigJson() {
305
914
  const tsConfigJson = `{
306
915
  "extends": "expo/tsconfig.base",
307
916
  "compilerOptions": {
308
917
  "forceConsistentCasingInFileNames": true,
309
- "jsx": "react-native",
310
918
  "outDir": "dist",
311
919
  "module": "esnext",
312
920
  "moduleResolution": "bundler",
@@ -318,9 +926,12 @@ export function getTsConfigJson() {
318
926
  "@/*": ["./src/*"]
319
927
  }
320
928
  },
321
- "include": ["**/*.ts", "**/*.tsx"]
929
+ "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
322
930
  }
323
931
  `;
324
932
  return tsConfigJson;
325
933
  }
934
+ function createPlatformDependencyMap(packages) {
935
+ return Object.fromEntries(packages.map(({ name, version }) => [name, version]));
936
+ }
326
937
  //# sourceMappingURL=templates.js.map