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