@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.
- package/README.md +1 -1
- package/dist/app/StudioApp.d.ts.map +1 -1
- package/dist/app/StudioApp.js +1 -16
- package/dist/app/StudioApp.js.map +1 -1
- package/dist/app/workspace/WorkspacePrimitives.d.ts +6 -2
- package/dist/app/workspace/WorkspacePrimitives.d.ts.map +1 -1
- package/dist/app/workspace/WorkspacePrimitives.js.map +1 -1
- package/dist/core/apiBase.d.ts +6 -0
- package/dist/core/apiBase.d.ts.map +1 -0
- package/dist/core/apiBase.js +14 -0
- package/dist/core/apiBase.js.map +1 -0
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +7 -11
- package/dist/core/constants.js.map +1 -1
- package/dist/host/http/projectRuntimeRoutes.d.ts +11 -0
- package/dist/host/http/projectRuntimeRoutes.d.ts.map +1 -0
- package/dist/host/http/projectRuntimeRoutes.js +42 -0
- package/dist/host/http/projectRuntimeRoutes.js.map +1 -0
- package/dist/host/http/server.d.ts +2 -2
- package/dist/host/http/server.d.ts.map +1 -1
- package/dist/host/http/server.js +3 -28
- package/dist/host/http/server.js.map +1 -1
- package/dist/host/http/serverWithSecrets.d.ts +1 -1
- package/dist/host/http/serverWithSecrets.d.ts.map +1 -1
- package/dist/host/layout/generatedImportComposer.d.ts.map +1 -1
- package/dist/host/layout/generatedImportComposer.js +17 -10
- package/dist/host/layout/generatedImportComposer.js.map +1 -1
- package/dist/host/layout/layoutGenerator.d.ts.map +1 -1
- package/dist/host/layout/layoutGenerator.js +36 -17
- package/dist/host/layout/layoutGenerator.js.map +1 -1
- package/dist/host/layout/templates/rootLayout.d.ts +1 -0
- package/dist/host/layout/templates/rootLayout.d.ts.map +1 -1
- package/dist/host/layout/templates/rootLayout.js +31 -22
- package/dist/host/layout/templates/rootLayout.js.map +1 -1
- package/dist/host/layout/templates/screen.d.ts.map +1 -1
- package/dist/host/layout/templates/screen.js +41 -22
- package/dist/host/layout/templates/screen.js.map +1 -1
- package/dist/host/media/projectBundledMediaRegistry.d.ts.map +1 -1
- package/dist/host/media/projectBundledMediaRegistry.js +7 -1
- package/dist/host/media/projectBundledMediaRegistry.js.map +1 -1
- package/dist/host/orchestrator/generatedAppFiles.d.ts.map +1 -1
- package/dist/host/orchestrator/generatedAppFiles.js +9 -14
- package/dist/host/orchestrator/generatedAppFiles.js.map +1 -1
- package/dist/host/orchestrator/infraSession.d.ts +8 -4
- package/dist/host/orchestrator/infraSession.d.ts.map +1 -1
- package/dist/host/orchestrator/infraSession.js +34 -17
- package/dist/host/orchestrator/infraSession.js.map +1 -1
- package/dist/host/orchestrator/scaffolder.d.ts +3 -4
- package/dist/host/orchestrator/scaffolder.d.ts.map +1 -1
- package/dist/host/orchestrator/scaffolder.js +17 -20
- package/dist/host/orchestrator/scaffolder.js.map +1 -1
- package/dist/host/orchestrator/studioInfraUp.d.ts +10 -2
- package/dist/host/orchestrator/studioInfraUp.d.ts.map +1 -1
- package/dist/host/orchestrator/studioInfraUp.js +8 -4
- package/dist/host/orchestrator/studioInfraUp.js.map +1 -1
- package/dist/host/orchestrator/templates.d.ts +20 -36
- package/dist/host/orchestrator/templates.d.ts.map +1 -1
- package/dist/host/orchestrator/templates.js +730 -119
- package/dist/host/orchestrator/templates.js.map +1 -1
- package/dist/runtime/runtimeActions.js +1 -1
- package/dist/runtime/runtimeActions.js.map +1 -1
- package/dist/ui/createStudioAppBarActions.js.map +1 -1
- package/package.json +33 -25
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import { resolveExpoRuntimeDependencyMap, resolveExpoRuntimeNativeOutput, resolveExpoRuntimeNativeSchemeMap, } from '@ankhorage/expo-runtime/planning';
|
|
2
|
-
|
|
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.
|
|
6
|
-
const STUDIO_VERSION = '^
|
|
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 = '^
|
|
11
|
-
const EXPO_RUNTIME_VERSION = '^
|
|
12
|
-
const DEVTOOLS_VERSION = '^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')}
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
83
|
+
permissions: ${serializeJsValue(permissions, 3)},`
|
|
102
84
|
: '';
|
|
103
85
|
const schemeLine = args.scheme
|
|
104
86
|
? `
|
|
105
|
-
|
|
87
|
+
scheme: ${serializeStringLiteral(args.scheme)},`
|
|
106
88
|
: '';
|
|
107
89
|
return `{
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
97
|
+
scheme: ${serializeStringLiteral(args.scheme)},`
|
|
116
98
|
: '';
|
|
117
99
|
return `{
|
|
118
|
-
|
|
119
|
-
|
|
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(`
|
|
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(`
|
|
111
|
+
sections.push(` ios: ${serializeIosConfig({ target: args.targets.ios, scheme: nativeSchemes.ios })},`);
|
|
130
112
|
}
|
|
131
113
|
if (args.targets.web?.enabled) {
|
|
132
|
-
sections.push(`
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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 })}
|
|
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
|
|
163
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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:
|
|
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: '
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|