@ship-et/buildatlas-cli 0.0.0-stage → 0.12.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/LICENSE +21 -0
- package/README.md +81 -2
- package/dist/browser-login.d.ts +12 -0
- package/dist/browser-login.d.ts.map +1 -0
- package/dist/browser-login.js +94 -0
- package/dist/browser-login.js.map +1 -0
- package/dist/client.d.ts +33 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +164 -0
- package/dist/client.js.map +1 -0
- package/dist/client.test.d.ts +2 -0
- package/dist/client.test.d.ts.map +1 -0
- package/dist/client.test.js +158 -0
- package/dist/client.test.js.map +1 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +88 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/build.d.ts +4 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +362 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/builds.d.ts +3 -0
- package/dist/commands/builds.d.ts.map +1 -0
- package/dist/commands/builds.js +102 -0
- package/dist/commands/builds.js.map +1 -0
- package/dist/commands/credentials-wizard.d.ts +25 -0
- package/dist/commands/credentials-wizard.d.ts.map +1 -0
- package/dist/commands/credentials-wizard.js +274 -0
- package/dist/commands/credentials-wizard.js.map +1 -0
- package/dist/commands/credentials.d.ts +3 -0
- package/dist/commands/credentials.d.ts.map +1 -0
- package/dist/commands/credentials.js +249 -0
- package/dist/commands/credentials.js.map +1 -0
- package/dist/commands/env.d.ts +37 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +220 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/expo-tokens.d.ts +3 -0
- package/dist/commands/expo-tokens.d.ts.map +1 -0
- package/dist/commands/expo-tokens.js +103 -0
- package/dist/commands/expo-tokens.js.map +1 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +204 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/projects.d.ts +11 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +148 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/submit.d.ts +10 -0
- package/dist/commands/submit.d.ts.map +1 -0
- package/dist/commands/submit.js +118 -0
- package/dist/commands/submit.js.map +1 -0
- package/dist/commands/update.d.ts +10 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +93 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/config.d.ts +66 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +255 -0
- package/dist/config.js.map +1 -0
- package/dist/detach.d.ts +18 -0
- package/dist/detach.d.ts.map +1 -0
- package/dist/detach.js +42 -0
- package/dist/detach.js.map +1 -0
- package/dist/eas-credentials-zip.d.ts +38 -0
- package/dist/eas-credentials-zip.d.ts.map +1 -0
- package/dist/eas-credentials-zip.js +63 -0
- package/dist/eas-credentials-zip.js.map +1 -0
- package/dist/eas-json.d.ts +21 -0
- package/dist/eas-json.d.ts.map +1 -0
- package/dist/eas-json.js +32 -0
- package/dist/eas-json.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/logs.d.ts +13 -0
- package/dist/logs.d.ts.map +1 -0
- package/dist/logs.js +70 -0
- package/dist/logs.js.map +1 -0
- package/dist/packaging.test.d.ts +2 -0
- package/dist/packaging.test.d.ts.map +1 -0
- package/dist/packaging.test.js +79 -0
- package/dist/packaging.test.js.map +1 -0
- package/dist/profiles.test.d.ts +2 -0
- package/dist/profiles.test.d.ts.map +1 -0
- package/dist/profiles.test.js +56 -0
- package/dist/profiles.test.js.map +1 -0
- package/dist/project-hint.d.ts +12 -0
- package/dist/project-hint.d.ts.map +1 -0
- package/dist/project-hint.js +19 -0
- package/dist/project-hint.js.map +1 -0
- package/dist/project-type.d.ts +43 -0
- package/dist/project-type.d.ts.map +1 -0
- package/dist/project-type.js +88 -0
- package/dist/project-type.js.map +1 -0
- package/dist/project-type.test.d.ts +2 -0
- package/dist/project-type.test.d.ts.map +1 -0
- package/dist/project-type.test.js +77 -0
- package/dist/project-type.test.js.map +1 -0
- package/dist/prompt.d.ts +56 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +157 -0
- package/dist/prompt.js.map +1 -0
- package/dist/tarball.d.ts +39 -0
- package/dist/tarball.d.ts.map +1 -0
- package/dist/tarball.js +208 -0
- package/dist/tarball.js.map +1 -0
- package/dist/tarball.test.d.ts +2 -0
- package/dist/tarball.test.d.ts.map +1 -0
- package/dist/tarball.test.js +97 -0
- package/dist/tarball.test.js.map +1 -0
- package/package.json +55 -4
package/dist/logs.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
/**
|
|
3
|
+
* Follows a build's log over SSE and resolves with its terminal status.
|
|
4
|
+
*
|
|
5
|
+
* Reconnects on a dropped connection, resuming from the last sequence number
|
|
6
|
+
* seen. That cursor is the whole reason the server numbers chunks: a build can run
|
|
7
|
+
* 20 minutes, and a laptop closing its lid mid-build should not truncate the log
|
|
8
|
+
* or replay it from the top.
|
|
9
|
+
*/
|
|
10
|
+
export async function streamLogs(config, buildId, { quiet = false } = {}) {
|
|
11
|
+
let lastSeq = 0;
|
|
12
|
+
let attempt = 0;
|
|
13
|
+
for (;;) {
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(new URL(`/api/builds/${buildId}/logs/stream?after=${lastSeq}`, config.apiUrl), { headers: { authorization: `Bearer ${config.token ?? ''}`, accept: 'text/event-stream' } });
|
|
16
|
+
if (!res.ok || !res.body) {
|
|
17
|
+
throw new Error(`log stream failed: ${res.status} ${res.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
attempt = 0;
|
|
20
|
+
const reader = res.body.getReader();
|
|
21
|
+
const decoder = new TextDecoder();
|
|
22
|
+
let buffer = '';
|
|
23
|
+
for (;;) {
|
|
24
|
+
const { done, value } = await reader.read();
|
|
25
|
+
if (done)
|
|
26
|
+
break;
|
|
27
|
+
buffer += decoder.decode(value, { stream: true });
|
|
28
|
+
// SSE frames are separated by a blank line; anything after the last one
|
|
29
|
+
// is a partial frame and must stay in the buffer.
|
|
30
|
+
const frames = buffer.split('\n\n');
|
|
31
|
+
buffer = frames.pop() ?? '';
|
|
32
|
+
for (const frame of frames) {
|
|
33
|
+
const event = frame.match(/^event: (.+)$/m)?.[1];
|
|
34
|
+
const dataLine = frame.match(/^data: (.*)$/m)?.[1];
|
|
35
|
+
if (!event || !dataLine)
|
|
36
|
+
continue;
|
|
37
|
+
if (event === 'log') {
|
|
38
|
+
const chunk = JSON.parse(dataLine);
|
|
39
|
+
if (chunk.seq)
|
|
40
|
+
lastSeq = Math.max(lastSeq, Number(chunk.seq));
|
|
41
|
+
if (!quiet && chunk.data) {
|
|
42
|
+
const text = chunk.stream === 'sys' ? pc.cyan(chunk.data) : chunk.data;
|
|
43
|
+
process.stdout.write(text);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (event === 'end') {
|
|
47
|
+
const { status } = JSON.parse(dataLine);
|
|
48
|
+
return status;
|
|
49
|
+
}
|
|
50
|
+
else if (event === 'error') {
|
|
51
|
+
const { message } = JSON.parse(dataLine);
|
|
52
|
+
throw new Error(message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Server closed without an `end` event — the build is probably still
|
|
57
|
+
// running behind a proxy timeout, so reconnect from the cursor.
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (++attempt > 5)
|
|
61
|
+
throw err;
|
|
62
|
+
const delayMs = Math.min(1000 * 2 ** attempt, 15_000);
|
|
63
|
+
if (!quiet) {
|
|
64
|
+
console.error(pc.dim(`\n[log stream interrupted, retrying in ${delayMs / 1000}s]`));
|
|
65
|
+
}
|
|
66
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=logs.js.map
|
package/dist/logs.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs.js","sourceRoot":"","sources":["../src/logs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packaging.test.d.ts","sourceRoot":"","sources":["../src/packaging.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
/**
|
|
7
|
+
* The CLI must not import a workspace package.
|
|
8
|
+
*
|
|
9
|
+
* `buildatlas` is published to npm on its own; `@buildatlas/shared` is private and is
|
|
10
|
+
* not published. An import of it resolves through the workspace symlink here and is
|
|
11
|
+
* absent for everyone who installs from npm, so the CLI compiles, tests pass, runs fine
|
|
12
|
+
* in the repository, and fails on first use for every real user:
|
|
13
|
+
*
|
|
14
|
+
* Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@buildatlas/shared'
|
|
15
|
+
* imported from .../node_modules/buildatlas/dist/commands/init.js
|
|
16
|
+
*
|
|
17
|
+
* That shipped in 0.8.0-beta.3, beta.4 and 0.9.0. Nothing in typecheck, lint or the test
|
|
18
|
+
* suite could catch it, because in this repository the import is correct — which is
|
|
19
|
+
* precisely why it needs a test of its own.
|
|
20
|
+
*/
|
|
21
|
+
const srcDir = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
async function sourceFiles(dir) {
|
|
23
|
+
const found = [];
|
|
24
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
25
|
+
const full = join(dir, entry.name);
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
found.push(...(await sourceFiles(full)));
|
|
28
|
+
}
|
|
29
|
+
else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) {
|
|
30
|
+
found.push(full);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return found;
|
|
34
|
+
}
|
|
35
|
+
test('no CLI source imports a workspace package', async () => {
|
|
36
|
+
const offenders = [];
|
|
37
|
+
for (const file of await sourceFiles(srcDir)) {
|
|
38
|
+
const text = await readFile(file, 'utf8');
|
|
39
|
+
// Import and export statements only. The rule is about what the module graph pulls
|
|
40
|
+
// in at runtime, so a mention in a comment — explaining exactly this — is fine.
|
|
41
|
+
const statements = text.matchAll(/^\s*(?:import|export)\b[^;]*?from\s+['"]([^'"]+)['"]/gm);
|
|
42
|
+
for (const match of statements) {
|
|
43
|
+
const specifier = match[1];
|
|
44
|
+
if (specifier.startsWith('@buildatlas/')) {
|
|
45
|
+
offenders.push(`${file.replace(srcDir, 'src')} imports ${specifier}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// `await import(...)` would fail identically at runtime.
|
|
49
|
+
for (const match of text.matchAll(/import\(\s*['"](@buildatlas\/[^'"]+)['"]\s*\)/g)) {
|
|
50
|
+
offenders.push(`${file.replace(srcDir, 'src')} dynamically imports ${match[1]}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
assert.deepEqual(offenders, [], 'The published CLI cannot resolve workspace packages. Copy what is needed into ' +
|
|
54
|
+
'packages/cli/src and keep it honest with an agreement test — see project-type.ts.\n' +
|
|
55
|
+
offenders.join('\n'));
|
|
56
|
+
});
|
|
57
|
+
test('every runtime dependency is declared in package.json', async () => {
|
|
58
|
+
// The same class of failure one step out: importing a package that happens to be
|
|
59
|
+
// hoisted into the workspace root resolves here and is missing for a real install.
|
|
60
|
+
const pkg = JSON.parse(await readFile(join(srcDir, '..', 'package.json'), 'utf8'));
|
|
61
|
+
const declared = new Set(Object.keys(pkg.dependencies ?? {}));
|
|
62
|
+
const missing = new Set();
|
|
63
|
+
for (const file of await sourceFiles(srcDir)) {
|
|
64
|
+
const text = await readFile(file, 'utf8');
|
|
65
|
+
for (const match of text.matchAll(/^\s*(?:import|export)\b[^;]*?from\s+['"]([^'"]+)['"]/gm)) {
|
|
66
|
+
const specifier = match[1];
|
|
67
|
+
if (specifier.startsWith('.') || specifier.startsWith('node:'))
|
|
68
|
+
continue;
|
|
69
|
+
// Scoped packages keep two segments; the rest keep one.
|
|
70
|
+
const name = specifier.startsWith('@')
|
|
71
|
+
? specifier.split('/').slice(0, 2).join('/')
|
|
72
|
+
: specifier.split('/')[0];
|
|
73
|
+
if (!declared.has(name))
|
|
74
|
+
missing.add(name);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
assert.deepEqual([...missing], [], `Imported but not in packages/cli/package.json dependencies: ${[...missing].join(', ')}`);
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=packaging.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packaging.test.js","sourceRoot":"","sources":["../src/packaging.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAEvD,KAAK,UAAU,WAAW,CAAC,GAAW;IACpC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3C,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YAC1E,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,IAAI,CAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;IAC3D,MAAM,SAAS,GAAa,EAAE,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,MAAM,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,mFAAmF;QACnF,gFAAgF;QAChF,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAC9B,wDAAwD,CACzD,CAAC;QACF,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;YAC/B,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;YAC5B,IAAI,SAAS,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;gBACzC,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,YAAY,SAAS,EAAE,CAAC,CAAC;YACxE,CAAC;QACH,CAAC;QACD,yDAAyD;QACzD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,gDAAgD,CAAC,EAAE,CAAC;YACpF,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,wBAAwB,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IAED,MAAM,CAAC,SAAS,CACd,SAAS,EACT,EAAE,EACF,gFAAgF;QAC9E,qFAAqF;QACrF,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CACvB,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;IACtE,iFAAiF;IACjF,mFAAmF;IACnF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CACpB,MAAM,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CACd,CAAC;IAC/C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC;IAE9D,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,IAAI,IAAI,MAAM,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,wDAAwD,CAAC,EAAE,CAAC;YAC5F,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;YAC5B,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC;gBAAE,SAAS;YACzE,wDAAwD;YACxD,MAAM,IAAI,GAAG,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC;gBACpC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;gBAC5C,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;YAC7B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IAED,MAAM,CAAC,SAAS,CACd,CAAC,GAAG,OAAO,CAAC,EACZ,EAAE,EACF,+DAA+D,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACzF,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.test.d.ts","sourceRoot":"","sources":["../src/profiles.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { afterEach, beforeEach, describe, it } from 'node:test';
|
|
6
|
+
import { assertProfileExists } from './commands/build.js';
|
|
7
|
+
/**
|
|
8
|
+
* Which config file the CLI believes in.
|
|
9
|
+
*
|
|
10
|
+
* It only ever read eas.json, so a native Android or iOS project with a perfectly good
|
|
11
|
+
* buildatlas.json was told "No eas.json in this directory" — wrong, and pointing at a
|
|
12
|
+
* file the project should not need. Supporting projects that never touched EAS is the
|
|
13
|
+
* entire reason buildatlas.json exists.
|
|
14
|
+
*/
|
|
15
|
+
let dir;
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
dir = await mkdtemp(join(tmpdir(), 'ba-profiles-'));
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await rm(dir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
const write = (name, profiles) => writeFile(join(dir, name), JSON.stringify({ build: Object.fromEntries(profiles.map((p) => [p, {}])) }));
|
|
23
|
+
describe('assertProfileExists', () => {
|
|
24
|
+
it('accepts a profile from buildatlas.json with no eas.json present', async () => {
|
|
25
|
+
// The case that failed: a native Android project that has never used EAS.
|
|
26
|
+
await write('buildatlas.json', ['development', 'preview', 'production']);
|
|
27
|
+
await assertProfileExists(dir, 'preview');
|
|
28
|
+
});
|
|
29
|
+
it('prefers buildatlas.json when both exist', async () => {
|
|
30
|
+
// Same order the server resolves in. If they disagree, the one the build will
|
|
31
|
+
// actually use is the one to validate against.
|
|
32
|
+
await write('buildatlas.json', ['preview']);
|
|
33
|
+
await write('eas.json', ['production']);
|
|
34
|
+
await assertProfileExists(dir, 'preview');
|
|
35
|
+
await assert.rejects(() => assertProfileExists(dir, 'production'), /buildatlas\.json/);
|
|
36
|
+
});
|
|
37
|
+
it('still reads eas.json when that is all there is', async () => {
|
|
38
|
+
await write('eas.json', ['production']);
|
|
39
|
+
await assertProfileExists(dir, 'production');
|
|
40
|
+
});
|
|
41
|
+
it('names the file it actually looked in', async () => {
|
|
42
|
+
// "not in eas.json" for a project with only buildatlas.json sends people to edit
|
|
43
|
+
// the wrong file — or to create one they do not need.
|
|
44
|
+
await write('buildatlas.json', ['preview']);
|
|
45
|
+
await assert.rejects(() => assertProfileExists(dir, 'nope'), (err) => err.message.includes('buildatlas.json') && err.message.includes('preview'));
|
|
46
|
+
});
|
|
47
|
+
it('allows a built-in profile with no config at all', async () => {
|
|
48
|
+
// Zero-configuration is supposed to work: the server falls back to defaults, so
|
|
49
|
+
// refusing here would block the case the defaults exist for.
|
|
50
|
+
await assertProfileExists(dir, 'production');
|
|
51
|
+
});
|
|
52
|
+
it('refuses an unknown profile with no config, and says what is available', async () => {
|
|
53
|
+
await assert.rejects(() => assertProfileExists(dir, 'staging'), (err) => err.message.includes('buildatlas init') && err.message.includes('development'));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=profiles.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.test.js","sourceRoot":"","sources":["../src/profiles.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;;;;;GAOG;AAEH,IAAI,GAAW,CAAC;AAEhB,UAAU,CAAC,KAAK,IAAI,EAAE;IACpB,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC;AACtD,CAAC,CAAC,CAAC;AACH,SAAS,CAAC,KAAK,IAAI,EAAE;IACnB,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAClD,CAAC,CAAC,CAAC;AAEH,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,QAAkB,EAAE,EAAE,CACjD,SAAS,CACP,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EACf,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAC5E,CAAC;AAEJ,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,0EAA0E;QAC1E,MAAM,KAAK,CAAC,iBAAiB,EAAE,CAAC,aAAa,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;QACzE,MAAM,mBAAmB,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,8EAA8E;QAC9E,+CAA+C;QAC/C,MAAM,KAAK,CAAC,iBAAiB,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;QAC5C,MAAM,KAAK,CAAC,UAAU,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;QACxC,MAAM,mBAAmB,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC1C,MAAM,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,GAAG,EAAE,YAAY,CAAC,EAAE,kBAAkB,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,KAAK,CAAC,UAAU,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;QACxC,MAAM,mBAAmB,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,iFAAiF;QACjF,sDAAsD;QACtD,MAAM,KAAK,CAAC,iBAAiB,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;QAC5C,MAAM,MAAM,CAAC,OAAO,CAClB,GAAG,EAAE,CAAC,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,EACtC,CAAC,GAAU,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAC3F,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,gFAAgF;QAChF,6DAA6D;QAC7D,MAAM,mBAAmB,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,KAAK,IAAI,EAAE;QACrF,MAAM,MAAM,CAAC,OAAO,CAClB,GAAG,EAAE,CAAC,mBAAmB,CAAC,GAAG,EAAE,SAAS,CAAC,EACzC,CAAC,GAAU,EAAE,EAAE,CACb,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CACjF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Says where an inferred slug came from, so a wrong guess is visible.
|
|
3
|
+
*
|
|
4
|
+
* Naming the source matters more than it looks: uploading a keystore to the wrong
|
|
5
|
+
* project is worse than an error, and the only clue that it is about to happen is
|
|
6
|
+
* this line. A stated `--project` or env var needs no announcement.
|
|
7
|
+
*/
|
|
8
|
+
export declare function reportProject(r: {
|
|
9
|
+
slug: string;
|
|
10
|
+
source: string;
|
|
11
|
+
}): void;
|
|
12
|
+
//# sourceMappingURL=project-hint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-hint.d.ts","sourceRoot":"","sources":["../src/project-hint.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUvE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
/**
|
|
3
|
+
* Says where an inferred slug came from, so a wrong guess is visible.
|
|
4
|
+
*
|
|
5
|
+
* Naming the source matters more than it looks: uploading a keystore to the wrong
|
|
6
|
+
* project is worse than an error, and the only clue that it is about to happen is
|
|
7
|
+
* this line. A stated `--project` or env var needs no announcement.
|
|
8
|
+
*/
|
|
9
|
+
export function reportProject(r) {
|
|
10
|
+
if (r.source === 'flag' || r.source === 'env')
|
|
11
|
+
return;
|
|
12
|
+
const where = r.source === 'config'
|
|
13
|
+
? 'app.json extra.buildatlas.project'
|
|
14
|
+
: r.source === 'app.config.js'
|
|
15
|
+
? 'app.config.js, via expo config'
|
|
16
|
+
: 'app.json';
|
|
17
|
+
console.log(pc.dim(`project: ${r.slug} (from ${where})`));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=project-hint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-hint.js","sourceRoot":"","sources":["../src/project-hint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,CAAmC;IAC/D,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,MAAM,KAAK,KAAK;QAAE,OAAO;IAEtD,MAAM,KAAK,GACT,CAAC,CAAC,MAAM,KAAK,QAAQ;QACnB,CAAC,CAAC,mCAAmC;QACrC,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,eAAe;YAC5B,CAAC,CAAC,gCAAgC;YAClC,CAAC,CAAC,UAAU,CAAC;IACnB,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-type detection, for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* A local copy of the rules in `packages/shared/src/build-config.ts`, and the
|
|
5
|
+
* duplication is deliberate — the CLI ships as a standalone npm package while
|
|
6
|
+
* `@buildatlas/shared` is a private workspace package that does not resolve for anyone
|
|
7
|
+
* who installs `buildatlas`. Importing it compiles and runs perfectly in this
|
|
8
|
+
* repository and fails with ERR_MODULE_NOT_FOUND for every real user, which is exactly
|
|
9
|
+
* what happened in 0.8.0-beta.3 through 0.9.0.
|
|
10
|
+
*
|
|
11
|
+
* The obvious risk of a copy is that the two drift and the CLI reports one type while
|
|
12
|
+
* the runner builds another. `project-type.test.ts` runs both implementations over the
|
|
13
|
+
* same matrix of project shapes and asserts they agree, so a change to either that
|
|
14
|
+
* changes behaviour fails the suite.
|
|
15
|
+
*
|
|
16
|
+
* Only detection lives here. Validating a descriptor stays in shared, where the worker
|
|
17
|
+
* does it: this side never needs to reject a config, only to guess a sensible starting
|
|
18
|
+
* point for `buildatlas init`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PROJECT_TYPES: readonly ['expo', 'react-native', 'flutter', 'ios', 'android'];
|
|
21
|
+
export type ProjectType = (typeof PROJECT_TYPES)[number];
|
|
22
|
+
export interface ProjectShape {
|
|
23
|
+
hasPackageJson: boolean;
|
|
24
|
+
hasExpoDependency: boolean;
|
|
25
|
+
hasReactNativeDependency: boolean;
|
|
26
|
+
hasExpoConfig: boolean;
|
|
27
|
+
hasIosDirectory: boolean;
|
|
28
|
+
hasAndroidDirectory: boolean;
|
|
29
|
+
hasRootXcodeProject: boolean;
|
|
30
|
+
hasRootGradle: boolean;
|
|
31
|
+
/** A `pubspec.yaml` that depends on the Flutter SDK. */
|
|
32
|
+
hasFlutterPubspec: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface DetectedProject {
|
|
35
|
+
type: ProjectType;
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
export declare class ProjectTypeError extends Error {
|
|
39
|
+
}
|
|
40
|
+
export declare function detectProjectType(shape: ProjectShape): DetectedProject;
|
|
41
|
+
/** Profiles for a project with no descriptor. Mirrors DEFAULT_PROFILES in shared. */
|
|
42
|
+
export declare const DEFAULT_PROFILES: Record<string, Record<string, unknown>>;
|
|
43
|
+
//# sourceMappingURL=project-type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-type.d.ts","sourceRoot":"","sources":["../src/project-type.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,eAAO,MAAM,aAAa,YAAI,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,CAAU,CAAC;AAC5F,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,OAAO,CAAC;IACxB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,wBAAwB,EAAE,OAAO,CAAC;IAClC,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,aAAa,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAE9C,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,eAAe,CA0DtE;AAED,qFAAqF;AACrF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAcpE,CAAC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-type detection, for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* A local copy of the rules in `packages/shared/src/build-config.ts`, and the
|
|
5
|
+
* duplication is deliberate — the CLI ships as a standalone npm package while
|
|
6
|
+
* `@buildatlas/shared` is a private workspace package that does not resolve for anyone
|
|
7
|
+
* who installs `buildatlas`. Importing it compiles and runs perfectly in this
|
|
8
|
+
* repository and fails with ERR_MODULE_NOT_FOUND for every real user, which is exactly
|
|
9
|
+
* what happened in 0.8.0-beta.3 through 0.9.0.
|
|
10
|
+
*
|
|
11
|
+
* The obvious risk of a copy is that the two drift and the CLI reports one type while
|
|
12
|
+
* the runner builds another. `project-type.test.ts` runs both implementations over the
|
|
13
|
+
* same matrix of project shapes and asserts they agree, so a change to either that
|
|
14
|
+
* changes behaviour fails the suite.
|
|
15
|
+
*
|
|
16
|
+
* Only detection lives here. Validating a descriptor stays in shared, where the worker
|
|
17
|
+
* does it: this side never needs to reject a config, only to guess a sensible starting
|
|
18
|
+
* point for `buildatlas init`.
|
|
19
|
+
*/
|
|
20
|
+
export const PROJECT_TYPES = ['expo', 'react-native', 'flutter', 'ios', 'android'];
|
|
21
|
+
export class ProjectTypeError extends Error {
|
|
22
|
+
}
|
|
23
|
+
export function detectProjectType(shape) {
|
|
24
|
+
const hasNativeDirs = shape.hasIosDirectory || shape.hasAndroidDirectory;
|
|
25
|
+
// First, because it is the one unambiguous signal: a pubspec depending on the Flutter
|
|
26
|
+
// SDK is a Flutter app, and its android/ and ios/ directories are Flutter's own.
|
|
27
|
+
if (shape.hasFlutterPubspec) {
|
|
28
|
+
return {
|
|
29
|
+
type: 'flutter',
|
|
30
|
+
reason: 'a pubspec.yaml depending on the Flutter SDK — built with the flutter tool',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (shape.hasPackageJson && (shape.hasExpoDependency || shape.hasExpoConfig)) {
|
|
34
|
+
if (!hasNativeDirs) {
|
|
35
|
+
return {
|
|
36
|
+
type: 'expo',
|
|
37
|
+
reason: 'an Expo project with no committed native directories — prebuild generates them',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
type: 'react-native',
|
|
42
|
+
reason: 'Expo dependencies but committed native directories — treated as bare, so prebuild ' +
|
|
43
|
+
'does not overwrite them. Set projectType to "expo" to prebuild anyway.',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (shape.hasPackageJson && shape.hasReactNativeDependency) {
|
|
47
|
+
return {
|
|
48
|
+
type: 'react-native',
|
|
49
|
+
reason: hasNativeDirs
|
|
50
|
+
? 'React Native with committed native directories'
|
|
51
|
+
: 'React Native without native directories — they will have to be generated or committed',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (!shape.hasPackageJson && (shape.hasRootXcodeProject || shape.hasIosDirectory)) {
|
|
55
|
+
return { type: 'ios', reason: 'an Xcode project with no package.json' };
|
|
56
|
+
}
|
|
57
|
+
if (!shape.hasPackageJson && (shape.hasRootGradle || shape.hasAndroidDirectory)) {
|
|
58
|
+
return { type: 'android', reason: 'a Gradle project with no package.json' };
|
|
59
|
+
}
|
|
60
|
+
if (shape.hasPackageJson && hasNativeDirs) {
|
|
61
|
+
return {
|
|
62
|
+
type: 'react-native',
|
|
63
|
+
reason: 'a JavaScript project with committed native directories',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
throw new ProjectTypeError('Could not tell what kind of project this is. It has no package.json, no pubspec.yaml, ' +
|
|
67
|
+
'no Xcode project and no Gradle build.\n' +
|
|
68
|
+
' Set the type explicitly in buildatlas.json:\n' +
|
|
69
|
+
' { "projectType": "ios", "build": { "production": {} } }\n' +
|
|
70
|
+
` Valid types: ${PROJECT_TYPES.join(', ')}.`);
|
|
71
|
+
}
|
|
72
|
+
/** Profiles for a project with no descriptor. Mirrors DEFAULT_PROFILES in shared. */
|
|
73
|
+
export const DEFAULT_PROFILES = {
|
|
74
|
+
development: {
|
|
75
|
+
distribution: 'internal',
|
|
76
|
+
android: { buildType: 'apk' },
|
|
77
|
+
ios: { simulator: true },
|
|
78
|
+
},
|
|
79
|
+
preview: {
|
|
80
|
+
distribution: 'internal',
|
|
81
|
+
android: { buildType: 'apk' },
|
|
82
|
+
},
|
|
83
|
+
production: {
|
|
84
|
+
distribution: 'store',
|
|
85
|
+
android: { buildType: 'app-bundle' },
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=project-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-type.js","sourceRoot":"","sources":["../src/project-type.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,CAAU,CAAC;AAqB5F,MAAM,OAAO,gBAAiB,SAAQ,KAAK;CAAG;AAE9C,MAAM,UAAU,iBAAiB,CAAC,KAAmB;IACnD,MAAM,aAAa,GAAG,KAAK,CAAC,eAAe,IAAI,KAAK,CAAC,mBAAmB,CAAC;IAEzE,sFAAsF;IACtF,iFAAiF;IACjF,IAAI,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC5B,OAAO;YACL,IAAI,EAAE,SAAS;YACf,MAAM,EAAE,2EAA2E;SACpF,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC,KAAK,CAAC,iBAAiB,IAAI,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;QAC7E,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,OAAO;gBACL,IAAI,EAAE,MAAM;gBACZ,MAAM,EAAE,gFAAgF;aACzF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,MAAM,EACJ,oFAAoF;gBACpF,wEAAwE;SAC3E,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,cAAc,IAAI,KAAK,CAAC,wBAAwB,EAAE,CAAC;QAC3D,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,aAAa;gBACnB,CAAC,CAAC,gDAAgD;gBAClD,CAAC,CAAC,uFAAuF;SAC5F,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,cAAc,IAAI,CAAC,KAAK,CAAC,mBAAmB,IAAI,KAAK,CAAC,eAAe,CAAC,EAAE,CAAC;QAClF,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,uCAAuC,EAAE,CAAC;IAC1E,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,cAAc,IAAI,CAAC,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAChF,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,uCAAuC,EAAE,CAAC;IAC9E,CAAC;IAED,IAAI,KAAK,CAAC,cAAc,IAAI,aAAa,EAAE,CAAC;QAC1C,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,wDAAwD;SACjE,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,gBAAgB,CACxB,wFAAwF;QACtF,yCAAyC;QACzC,iDAAiD;QACjD,iEAAiE;QACjE,kBAAkB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAChD,CAAC;AACJ,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,MAAM,gBAAgB,GAA4C;IACvE,WAAW,EAAE;QACX,YAAY,EAAE,UAAU;QACxB,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;QAC7B,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;KACzB;IACD,OAAO,EAAE;QACP,YAAY,EAAE,UAAU;QACxB,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;KAC9B;IACD,UAAU,EAAE;QACV,YAAY,EAAE,OAAO;QACrB,OAAO,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE;KACrC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-type.test.d.ts","sourceRoot":"","sources":["../src/project-type.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { detectProjectType as detectInShared } from '@buildatlas/shared/build-config';
|
|
4
|
+
import { PROJECT_TYPES, detectProjectType } from './project-type.js';
|
|
5
|
+
/**
|
|
6
|
+
* The CLI's copy of detection must agree with the server's.
|
|
7
|
+
*
|
|
8
|
+
* `project-type.ts` says this test exists; it did not. The duplication is deliberate —
|
|
9
|
+
* the CLI ships standalone and cannot import `@buildatlas/shared` at runtime — but the
|
|
10
|
+
* failure mode of a copy is silent drift: the CLI reports one type, the runner builds
|
|
11
|
+
* another, and the only symptom is a build that behaves unlike what `buildatlas init`
|
|
12
|
+
* described. A test file is not shipped, so importing shared here is safe.
|
|
13
|
+
*/
|
|
14
|
+
const bare = {
|
|
15
|
+
hasPackageJson: false,
|
|
16
|
+
hasExpoDependency: false,
|
|
17
|
+
hasReactNativeDependency: false,
|
|
18
|
+
hasExpoConfig: false,
|
|
19
|
+
hasFlutterPubspec: false,
|
|
20
|
+
hasIosDirectory: false,
|
|
21
|
+
hasAndroidDirectory: false,
|
|
22
|
+
hasRootXcodeProject: false,
|
|
23
|
+
hasRootGradle: false,
|
|
24
|
+
};
|
|
25
|
+
const shapes = {
|
|
26
|
+
'managed Expo': { hasPackageJson: true, hasExpoDependency: true },
|
|
27
|
+
'Expo with committed native dirs': {
|
|
28
|
+
hasPackageJson: true,
|
|
29
|
+
hasExpoDependency: true,
|
|
30
|
+
hasIosDirectory: true,
|
|
31
|
+
hasAndroidDirectory: true,
|
|
32
|
+
},
|
|
33
|
+
'bare React Native': { hasPackageJson: true, hasReactNativeDependency: true },
|
|
34
|
+
'Flutter': { hasFlutterPubspec: true },
|
|
35
|
+
'Flutter with native dirs': {
|
|
36
|
+
hasFlutterPubspec: true,
|
|
37
|
+
hasIosDirectory: true,
|
|
38
|
+
hasAndroidDirectory: true,
|
|
39
|
+
},
|
|
40
|
+
'native iOS at the root': { hasRootXcodeProject: true },
|
|
41
|
+
'native Android at the root': { hasRootGradle: true },
|
|
42
|
+
'JavaScript with native dirs only': { hasPackageJson: true, hasAndroidDirectory: true },
|
|
43
|
+
};
|
|
44
|
+
describe('detection agrees with the server', () => {
|
|
45
|
+
for (const [name, overrides] of Object.entries(shapes)) {
|
|
46
|
+
it(name, () => {
|
|
47
|
+
const shape = { ...bare, ...overrides };
|
|
48
|
+
assert.equal(detectProjectType(shape).type, detectInShared(shape).type, name);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
it('both refuse a tree with nothing in it', () => {
|
|
52
|
+
assert.throws(() => detectProjectType(bare));
|
|
53
|
+
assert.throws(() => detectInShared(bare));
|
|
54
|
+
});
|
|
55
|
+
it('lists the same types, in the same order', () => {
|
|
56
|
+
// The order is user-visible: it is printed in the "valid types" error.
|
|
57
|
+
assert.deepEqual([...PROJECT_TYPES], ['expo', 'react-native', 'flutter', 'ios', 'android']);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('Flutter detection', () => {
|
|
61
|
+
it('wins over the native directories Flutter itself generates', () => {
|
|
62
|
+
// android/ and ios/ in a Flutter app are Flutter's own. Building them directly with
|
|
63
|
+
// Gradle or xcodebuild skips compiling the Dart and produces an app that launches
|
|
64
|
+
// to a black screen rather than failing — the worst available outcome.
|
|
65
|
+
const detected = detectProjectType({
|
|
66
|
+
...bare,
|
|
67
|
+
hasFlutterPubspec: true,
|
|
68
|
+
hasIosDirectory: true,
|
|
69
|
+
hasAndroidDirectory: true,
|
|
70
|
+
});
|
|
71
|
+
assert.equal(detected.type, 'flutter');
|
|
72
|
+
});
|
|
73
|
+
it('does not claim a project that merely has native directories', () => {
|
|
74
|
+
assert.notEqual(detectProjectType({ ...bare, hasRootGradle: true }).type, 'flutter');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=project-type.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-type.test.js","sourceRoot":"","sources":["../src/project-type.test.ts"],"names":[],"mappings":"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"}
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asks a sequence of questions, working both interactively and from piped input.
|
|
3
|
+
*
|
|
4
|
+
* Piped stdin needs different handling than a TTY. `readline.question()` only
|
|
5
|
+
* captures the *next* 'line' event, but a pipe delivers its whole buffer at once,
|
|
6
|
+
* so readline emits every line immediately and answers after the first are lost —
|
|
7
|
+
* leaving a later `question()` promise that never settles and a process that exits
|
|
8
|
+
* with "unsettled top-level await". Draining stdin up front and serving answers
|
|
9
|
+
* from a queue removes the race entirely, which matters because piping answers is
|
|
10
|
+
* exactly how CI drives `buildatlas credentials add`.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Prompter {
|
|
13
|
+
private rl;
|
|
14
|
+
private queue;
|
|
15
|
+
readonly interactive: boolean;
|
|
16
|
+
private drainStdin;
|
|
17
|
+
private next;
|
|
18
|
+
ask(question: string, opts?: {
|
|
19
|
+
default?: string;
|
|
20
|
+
}): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Reads a value without echoing it.
|
|
23
|
+
*
|
|
24
|
+
* Passwords must not appear on screen — fine alone at a desk, not fine over a shared
|
|
25
|
+
* screen or a recorded session.
|
|
26
|
+
*
|
|
27
|
+
* Raw mode rather than muting readline's echo: readline's write hook is private on
|
|
28
|
+
* current Node, so overriding it silently does nothing and the password appears in
|
|
29
|
+
* full. Reading the bytes ourselves is the only approach that actually hides them.
|
|
30
|
+
*
|
|
31
|
+
* The subtlety is handing stdin back afterwards. An earlier version paused the stream
|
|
32
|
+
* during cleanup, which dropped buffered input and split the *next* answer — typing
|
|
33
|
+
* "US" at the following prompt submitted "U" and left "S" behind. So the stream is
|
|
34
|
+
* left flowing and only the mode and the listener are undone.
|
|
35
|
+
*/
|
|
36
|
+
askHidden(question: string): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Numbered menu.
|
|
39
|
+
*
|
|
40
|
+
* Numbered rather than arrow-key driven so the same code path serves a TTY and a
|
|
41
|
+
* pipe — CI answers by echoing "1" — and so it degrades gracefully in terminals
|
|
42
|
+
* that mangle cursor control.
|
|
43
|
+
*/
|
|
44
|
+
select<T extends string>(question: string, options: {
|
|
45
|
+
value: T;
|
|
46
|
+
label: string;
|
|
47
|
+
hint?: string;
|
|
48
|
+
}[], opts?: {
|
|
49
|
+
default?: T;
|
|
50
|
+
}): Promise<T>;
|
|
51
|
+
confirm(question: string, defaultYes?: boolean): Promise<boolean>;
|
|
52
|
+
close(): void;
|
|
53
|
+
}
|
|
54
|
+
/** Runs `fn` with a prompter and always closes it, so the process can exit. */
|
|
55
|
+
export declare function withPrompter<T>(fn: (prompter: Prompter) => Promise<T>): Promise<T>;
|
|
56
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,EAAE,CAAwB;IAClC,OAAO,CAAC,KAAK,CAAuB;IACpC,QAAQ,CAAC,WAAW,UAAgC;YAEtC,UAAU;YAMV,IAAI;IAYZ,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAY5E;IAED;;;;;;;;;;;;;;OAcG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgDjD;IAED;;;;;;OAMG;IACG,MAAM,CAAC,CAAC,SAAS,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;QAAE,KAAK,EAAE,CAAC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,EACrD,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,CAAC,CAAA;KAAO,GACzB,OAAO,CAAC,CAAC,CAAC,CAqBZ;IAEK,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,UAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAKnE;IAED,KAAK,IAAI,IAAI,CAGZ;CACF;AAED,+EAA+E;AAC/E,wBAAsB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAOxF"}
|