@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/prompt.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
/**
|
|
4
|
+
* Asks a sequence of questions, working both interactively and from piped input.
|
|
5
|
+
*
|
|
6
|
+
* Piped stdin needs different handling than a TTY. `readline.question()` only
|
|
7
|
+
* captures the *next* 'line' event, but a pipe delivers its whole buffer at once,
|
|
8
|
+
* so readline emits every line immediately and answers after the first are lost —
|
|
9
|
+
* leaving a later `question()` promise that never settles and a process that exits
|
|
10
|
+
* with "unsettled top-level await". Draining stdin up front and serving answers
|
|
11
|
+
* from a queue removes the race entirely, which matters because piping answers is
|
|
12
|
+
* exactly how CI drives `buildatlas credentials add`.
|
|
13
|
+
*/
|
|
14
|
+
export class Prompter {
|
|
15
|
+
rl;
|
|
16
|
+
queue;
|
|
17
|
+
interactive = process.stdin.isTTY === true;
|
|
18
|
+
async drainStdin() {
|
|
19
|
+
const chunks = [];
|
|
20
|
+
for await (const chunk of process.stdin)
|
|
21
|
+
chunks.push(chunk);
|
|
22
|
+
return Buffer.concat(chunks).toString('utf8').split('\n');
|
|
23
|
+
}
|
|
24
|
+
async next(question) {
|
|
25
|
+
this.queue ??= await this.drainStdin();
|
|
26
|
+
// Echo the prompt so piped runs still produce a readable transcript.
|
|
27
|
+
process.stdout.write(question);
|
|
28
|
+
const value = this.queue.shift();
|
|
29
|
+
if (value === undefined) {
|
|
30
|
+
throw new Error(`No input available for prompt: ${question.trim()}`);
|
|
31
|
+
}
|
|
32
|
+
process.stdout.write('\n');
|
|
33
|
+
return value.trim();
|
|
34
|
+
}
|
|
35
|
+
async ask(question, opts = {}) {
|
|
36
|
+
const suffix = opts.default ? pc.dim(` [${opts.default}]`) : '';
|
|
37
|
+
const text = `${question}${suffix}: `;
|
|
38
|
+
const answer = this.interactive
|
|
39
|
+
? (await (this.rl ??= createInterface({
|
|
40
|
+
input: process.stdin,
|
|
41
|
+
output: process.stdout,
|
|
42
|
+
})).question(text)).trim()
|
|
43
|
+
: await this.next(text);
|
|
44
|
+
return answer || opts.default || '';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reads a value without echoing it.
|
|
48
|
+
*
|
|
49
|
+
* Passwords must not appear on screen — fine alone at a desk, not fine over a shared
|
|
50
|
+
* screen or a recorded session.
|
|
51
|
+
*
|
|
52
|
+
* Raw mode rather than muting readline's echo: readline's write hook is private on
|
|
53
|
+
* current Node, so overriding it silently does nothing and the password appears in
|
|
54
|
+
* full. Reading the bytes ourselves is the only approach that actually hides them.
|
|
55
|
+
*
|
|
56
|
+
* The subtlety is handing stdin back afterwards. An earlier version paused the stream
|
|
57
|
+
* during cleanup, which dropped buffered input and split the *next* answer — typing
|
|
58
|
+
* "US" at the following prompt submitted "U" and left "S" behind. So the stream is
|
|
59
|
+
* left flowing and only the mode and the listener are undone.
|
|
60
|
+
*/
|
|
61
|
+
async askHidden(question) {
|
|
62
|
+
const text = `${question}: `;
|
|
63
|
+
if (!this.interactive)
|
|
64
|
+
return this.next(text);
|
|
65
|
+
// readline and a raw-mode reader cannot both own stdin. Closing it here means the
|
|
66
|
+
// next ask() lazily creates a fresh one.
|
|
67
|
+
this.close();
|
|
68
|
+
process.stdout.write(text);
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const stdin = process.stdin;
|
|
71
|
+
let value = '';
|
|
72
|
+
const cleanup = () => {
|
|
73
|
+
stdin.removeListener('data', onData);
|
|
74
|
+
stdin.setRawMode(false);
|
|
75
|
+
// Deliberately no stdin.pause(): pausing here is what corrupted the next answer.
|
|
76
|
+
process.stdout.write('\n');
|
|
77
|
+
};
|
|
78
|
+
const onData = (chunk) => {
|
|
79
|
+
for (const ch of chunk.toString('utf8')) {
|
|
80
|
+
if (ch === '\r' || ch === '\n' || ch === '\u0004') {
|
|
81
|
+
cleanup();
|
|
82
|
+
resolve(value);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Ctrl-C must still abort: raw mode means the terminal no longer turns it
|
|
86
|
+
// into SIGINT for us.
|
|
87
|
+
if (ch === '\u0003') {
|
|
88
|
+
cleanup();
|
|
89
|
+
reject(new Error('Cancelled'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (ch === '\u007f' || ch === '\b') {
|
|
93
|
+
value = value.slice(0, -1);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Drop other control characters rather than storing them in something the
|
|
97
|
+
// user cannot see.
|
|
98
|
+
if (ch >= ' ')
|
|
99
|
+
value += ch;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
stdin.setRawMode(true);
|
|
103
|
+
stdin.resume();
|
|
104
|
+
stdin.on('data', onData);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Numbered menu.
|
|
109
|
+
*
|
|
110
|
+
* Numbered rather than arrow-key driven so the same code path serves a TTY and a
|
|
111
|
+
* pipe — CI answers by echoing "1" — and so it degrades gracefully in terminals
|
|
112
|
+
* that mangle cursor control.
|
|
113
|
+
*/
|
|
114
|
+
async select(question, options, opts = {}) {
|
|
115
|
+
const width = Math.max(...options.map((o) => o.label.length));
|
|
116
|
+
console.log(`\n${pc.bold(question)}`);
|
|
117
|
+
for (const [i, o] of options.entries()) {
|
|
118
|
+
const hint = o.hint ? pc.dim(` ${o.hint}`) : '';
|
|
119
|
+
console.log(` ${pc.cyan(String(i + 1))}) ${o.label.padEnd(width)}${hint}`);
|
|
120
|
+
}
|
|
121
|
+
const fallback = opts.default ?? options[0].value;
|
|
122
|
+
const defaultIndex = options.findIndex((o) => o.value === fallback) + 1;
|
|
123
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
124
|
+
const answer = await this.ask('Choose', { default: String(defaultIndex) });
|
|
125
|
+
const byNumber = options[Number(answer) - 1];
|
|
126
|
+
if (byNumber)
|
|
127
|
+
return byNumber.value;
|
|
128
|
+
// Accept the value itself too, so a scripted `--kind`-style answer works.
|
|
129
|
+
const byValue = options.find((o) => o.value === answer);
|
|
130
|
+
if (byValue)
|
|
131
|
+
return byValue.value;
|
|
132
|
+
console.log(pc.red(` Enter 1-${options.length}.`));
|
|
133
|
+
}
|
|
134
|
+
throw new Error('No valid selection.');
|
|
135
|
+
}
|
|
136
|
+
async confirm(question, defaultYes = true) {
|
|
137
|
+
const answer = await this.ask(`${question} ${pc.dim(defaultYes ? '(Y/n)' : '(y/N)')}`, {
|
|
138
|
+
default: defaultYes ? 'y' : 'n',
|
|
139
|
+
});
|
|
140
|
+
return answer.toLowerCase().startsWith('y');
|
|
141
|
+
}
|
|
142
|
+
close() {
|
|
143
|
+
this.rl?.close();
|
|
144
|
+
this.rl = undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Runs `fn` with a prompter and always closes it, so the process can exit. */
|
|
148
|
+
export async function withPrompter(fn) {
|
|
149
|
+
const prompter = new Prompter();
|
|
150
|
+
try {
|
|
151
|
+
return await fn(prompter);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
prompter.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare function toMatcher(patterns: string[]): (relativePath: string) => boolean;
|
|
2
|
+
/** The exclusions every project gets, exported so a test can assert on them. */
|
|
3
|
+
export declare const alwaysExcluded: readonly string[];
|
|
4
|
+
export interface TarballResult {
|
|
5
|
+
path: string;
|
|
6
|
+
sizeBytes: number;
|
|
7
|
+
sha256: string;
|
|
8
|
+
/**
|
|
9
|
+
* Build-affecting files present locally but excluded by the ignore rules. Surfaced
|
|
10
|
+
* so a silently different install on the runner is not silent.
|
|
11
|
+
*/
|
|
12
|
+
excludedButRelevant: string[];
|
|
13
|
+
/**
|
|
14
|
+
* The biggest directories that went in, largest first.
|
|
15
|
+
*
|
|
16
|
+
* Reported because an oversized upload is invisible at the moment it matters. A real
|
|
17
|
+
* Flutter app shipped 1,175 MB and nobody noticed until a build sat in "Fetch source"
|
|
18
|
+
* for an hour and sixteen minutes — at which point the only evidence was a duration.
|
|
19
|
+
* Naming the directories turns that into something fixable before the upload starts.
|
|
20
|
+
*/
|
|
21
|
+
largest: {
|
|
22
|
+
path: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
}[];
|
|
25
|
+
cleanup(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** Above this, an upload is worth questioning rather than just performing. */
|
|
28
|
+
export declare const LARGE_UPLOAD_BYTES: number;
|
|
29
|
+
/**
|
|
30
|
+
* Packs the project for upload, honouring `.easignore` when present and falling
|
|
31
|
+
* back to `.gitignore` — the same precedence eas-cli uses, so a project that
|
|
32
|
+
* builds on EAS packs identically here.
|
|
33
|
+
*
|
|
34
|
+
* Uploading the working tree rather than a git ref is deliberate: developers
|
|
35
|
+
* expect `buildatlas build` to build what is on their disk right now, including
|
|
36
|
+
* uncommitted changes.
|
|
37
|
+
*/
|
|
38
|
+
export declare function packProject(projectDir: string): Promise<TarballResult>;
|
|
39
|
+
//# sourceMappingURL=tarball.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tarball.d.ts","sourceRoot":"","sources":["../src/tarball.ts"],"names":[],"mappings":"AAyFA,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAwB/E;AAED,gFAAgF;AAChF,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAAoB,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B;;;;;;;OAOG;IACH,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,QAAoB,CAAC;AAEpD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAuF5E"}
|
package/dist/tarball.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { readFile, rm, stat } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import { create } from 'tar';
|
|
8
|
+
/**
|
|
9
|
+
* Directories never worth uploading. `node_modules` is the big one: the build
|
|
10
|
+
* reinstalls dependencies from the lockfile anyway, and including it would turn a
|
|
11
|
+
* 2 MB upload into 400 MB while risking native artifacts built for the wrong
|
|
12
|
+
* architecture leaking into the build.
|
|
13
|
+
*/
|
|
14
|
+
const ALWAYS_EXCLUDED = [
|
|
15
|
+
'node_modules',
|
|
16
|
+
'.git',
|
|
17
|
+
'.expo',
|
|
18
|
+
'ios/Pods',
|
|
19
|
+
'ios/build',
|
|
20
|
+
'android/build',
|
|
21
|
+
'android/.gradle',
|
|
22
|
+
'android/app/build',
|
|
23
|
+
'dist',
|
|
24
|
+
'credentials',
|
|
25
|
+
'credentials.json',
|
|
26
|
+
'.DS_Store',
|
|
27
|
+
// A native Android project is its own Gradle root: the module is `app/`, not
|
|
28
|
+
// `android/app/`, so none of the `android/…` entries above match anything and the
|
|
29
|
+
// whole of Gradle's output went up with the source. On a trivial test app that is a
|
|
30
|
+
// 1 MB upload turned into 21 MB — minutes on a slow uplink, every build, for files
|
|
31
|
+
// the runner deletes before it compiles. `build` is matched as a path segment so it
|
|
32
|
+
// also covers the modules of a multi-module project (`feature/build`), which is where
|
|
33
|
+
// the weight really is. Gradle owns that directory name by convention; a source
|
|
34
|
+
// directory called `build` would be a surprising thing to find in any project this
|
|
35
|
+
// tool builds, and `dist` has been excluded on exactly the same reasoning since the
|
|
36
|
+
// first version.
|
|
37
|
+
'build',
|
|
38
|
+
'.gradle',
|
|
39
|
+
'.kotlin',
|
|
40
|
+
'.idea',
|
|
41
|
+
'.cxx',
|
|
42
|
+
// Flutter and Dart output, none of which the runner uses and all of which it deletes
|
|
43
|
+
// before compiling.
|
|
44
|
+
//
|
|
45
|
+
// A real Flutter app uploaded **1,175 MB** of source. The runner then spent 1h 16m
|
|
46
|
+
// downloading it over a link from Hetzner to Addis, to run a two-minute build. None
|
|
47
|
+
// of the weight was source: `.dart_tool` holds the package and build caches,
|
|
48
|
+
// `ios/.symlinks` links the entire pub cache copy of every plugin, `ephemeral`
|
|
49
|
+
// directories hold a generated copy of the Flutter engine per platform, and `.fvm`
|
|
50
|
+
// can hold a whole SDK — 1.5 GB on its own.
|
|
51
|
+
//
|
|
52
|
+
// Flutter's own .gitignore covers most of this, but it is split across
|
|
53
|
+
// `ios/.gitignore`, `android/.gitignore`, `macos/.gitignore` and the rest, and only
|
|
54
|
+
// the project root's is read here. Naming them directly is simpler than implementing
|
|
55
|
+
// nested gitignore semantics, and it works for a project whose ignore files have been
|
|
56
|
+
// edited or lost.
|
|
57
|
+
'.dart_tool',
|
|
58
|
+
'.symlinks',
|
|
59
|
+
'ephemeral',
|
|
60
|
+
// `.fvm/versions`, not `.fvm`: the directory holds whole SDKs, but `.fvm/fvm_config.json`
|
|
61
|
+
// beside them is the *pin* — the legacy location the worker still reads. Excluding the
|
|
62
|
+
// parent would have dropped the pin and built the project with whatever SDK the runner
|
|
63
|
+
// defaults to, silently, which is the exact failure the pinning feature exists to
|
|
64
|
+
// prevent.
|
|
65
|
+
'.fvm/versions',
|
|
66
|
+
'.fvm/flutter_sdk',
|
|
67
|
+
'Pods',
|
|
68
|
+
'.flutter-plugins',
|
|
69
|
+
'.flutter-plugins-dependencies',
|
|
70
|
+
'DerivedData',
|
|
71
|
+
// Desktop targets of a Flutter app. A mobile build never reads them, and a Linux or
|
|
72
|
+
// Windows build directory is pure weight on an iOS upload.
|
|
73
|
+
'windows/flutter/ephemeral',
|
|
74
|
+
'linux/flutter/ephemeral',
|
|
75
|
+
];
|
|
76
|
+
async function readIgnoreFile(projectDir, name) {
|
|
77
|
+
try {
|
|
78
|
+
const raw = await readFile(join(projectDir, name), 'utf8');
|
|
79
|
+
return raw
|
|
80
|
+
.split('\n')
|
|
81
|
+
.map((l) => l.trim())
|
|
82
|
+
.filter((l) => l && !l.startsWith('#') && !l.startsWith('!'));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function toMatcher(patterns) {
|
|
89
|
+
// Deliberately simple prefix/suffix/segment matching rather than full gitignore
|
|
90
|
+
// semantics. Getting gitignore exactly right is a project of its own; excluding
|
|
91
|
+
// slightly too little is safe (the build still works), while a subtle bug that
|
|
92
|
+
// excludes a source file produces a baffling build failure.
|
|
93
|
+
//
|
|
94
|
+
// A leading slash is kept rather than stripped, because in a .gitignore it means
|
|
95
|
+
// something: `/build` is the root directory only. Dropped, it became the literal
|
|
96
|
+
// path "/build", which no relative path can ever equal — so a root-anchored pattern
|
|
97
|
+
// silently matched nothing at all. Anchored patterns are now matched at the root,
|
|
98
|
+
// which is what they say.
|
|
99
|
+
const normalized = patterns.map((p) => p.replace(/^\.\//, '').replace(/\/$/, ''));
|
|
100
|
+
return (relativePath) => {
|
|
101
|
+
const segments = relativePath.split('/');
|
|
102
|
+
return normalized.some((pattern) => {
|
|
103
|
+
if (pattern.startsWith('/')) {
|
|
104
|
+
const anchored = pattern.slice(1);
|
|
105
|
+
return relativePath === anchored || relativePath.startsWith(`${anchored}/`);
|
|
106
|
+
}
|
|
107
|
+
if (pattern.startsWith('*.'))
|
|
108
|
+
return relativePath.endsWith(pattern.slice(1));
|
|
109
|
+
if (pattern.includes('/'))
|
|
110
|
+
return relativePath === pattern || relativePath.startsWith(`${pattern}/`);
|
|
111
|
+
return segments.includes(pattern);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** The exclusions every project gets, exported so a test can assert on them. */
|
|
116
|
+
export const alwaysExcluded = ALWAYS_EXCLUDED;
|
|
117
|
+
/** Above this, an upload is worth questioning rather than just performing. */
|
|
118
|
+
export const LARGE_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
119
|
+
/**
|
|
120
|
+
* Packs the project for upload, honouring `.easignore` when present and falling
|
|
121
|
+
* back to `.gitignore` — the same precedence eas-cli uses, so a project that
|
|
122
|
+
* builds on EAS packs identically here.
|
|
123
|
+
*
|
|
124
|
+
* Uploading the working tree rather than a git ref is deliberate: developers
|
|
125
|
+
* expect `buildatlas build` to build what is on their disk right now, including
|
|
126
|
+
* uncommitted changes.
|
|
127
|
+
*/
|
|
128
|
+
export async function packProject(projectDir) {
|
|
129
|
+
const easIgnore = await readIgnoreFile(projectDir, '.easignore');
|
|
130
|
+
const ignorePatterns = easIgnore.length
|
|
131
|
+
? easIgnore
|
|
132
|
+
: await readIgnoreFile(projectDir, '.gitignore');
|
|
133
|
+
const isIgnored = toMatcher([...ALWAYS_EXCLUDED, ...ignorePatterns]);
|
|
134
|
+
// Files that change how the build behaves and are commonly gitignored. Excluding
|
|
135
|
+
// them is silent and the consequence is remote: a project whose `.npmrc` sets
|
|
136
|
+
// `node-linker=hoisted` gets a different node_modules layout on the runner than on
|
|
137
|
+
// the developer's machine, then fails while bundling for reasons that point nowhere
|
|
138
|
+
// near a missing file.
|
|
139
|
+
//
|
|
140
|
+
// Reported rather than force-included, because an .npmrc is gitignored for a good
|
|
141
|
+
// reason more often than not — it can hold registry auth tokens, and uploading
|
|
142
|
+
// those to a build server should be a decision rather than a default.
|
|
143
|
+
// Files that change how a build behaves and are commonly gitignored. The point is
|
|
144
|
+
// not to upload them — they can hold tokens — but to say they are missing, because
|
|
145
|
+
// the failure otherwise looks like a code problem. `.env` is the one people lose most:
|
|
146
|
+
// react-native-config and react-native-dotenv read a file rather than the
|
|
147
|
+
// environment, so an excluded .env compiles into an app with empty configuration and
|
|
148
|
+
// no error anywhere.
|
|
149
|
+
const buildAffecting = [
|
|
150
|
+
'.npmrc',
|
|
151
|
+
'.yarnrc.yml',
|
|
152
|
+
'.nvmrc',
|
|
153
|
+
'.node-version',
|
|
154
|
+
'.env',
|
|
155
|
+
'.env.local',
|
|
156
|
+
'.env.production',
|
|
157
|
+
'.env.staging',
|
|
158
|
+
];
|
|
159
|
+
const excludedButRelevant = [];
|
|
160
|
+
for (const name of buildAffecting) {
|
|
161
|
+
if (!isIgnored(name))
|
|
162
|
+
continue;
|
|
163
|
+
try {
|
|
164
|
+
await stat(join(projectDir, name));
|
|
165
|
+
excludedButRelevant.push(name);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Not present, so nothing is being lost.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const outPath = join(tmpdir(), `buildatlas-src-${process.pid}-${Date.now()}.tar.gz`);
|
|
172
|
+
// Totalled per top-level directory as the archive is walked, so this costs one map
|
|
173
|
+
// update per file rather than a second pass over the tree.
|
|
174
|
+
const byDirectory = new Map();
|
|
175
|
+
await create({
|
|
176
|
+
gzip: true,
|
|
177
|
+
file: outPath,
|
|
178
|
+
cwd: projectDir,
|
|
179
|
+
filter: (path, stat) => {
|
|
180
|
+
const relative = path.replace(/^\.\/?/, '');
|
|
181
|
+
if (relative !== '' && isIgnored(relative))
|
|
182
|
+
return false;
|
|
183
|
+
if (stat.size) {
|
|
184
|
+
// Two segments deep: "ios" is not as useful as "ios/.symlinks", and three is
|
|
185
|
+
// more detail than a warning can use.
|
|
186
|
+
const key = relative.split('/').slice(0, 2).join('/') || relative;
|
|
187
|
+
byDirectory.set(key, (byDirectory.get(key) ?? 0) + stat.size);
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
},
|
|
191
|
+
}, ['.']);
|
|
192
|
+
const largest = [...byDirectory.entries()]
|
|
193
|
+
.map(([path, bytes]) => ({ path, bytes }))
|
|
194
|
+
.sort((a, b) => b.bytes - a.bytes)
|
|
195
|
+
.slice(0, 5);
|
|
196
|
+
const hash = createHash('sha256');
|
|
197
|
+
await pipeline(createReadStream(outPath), hash);
|
|
198
|
+
const { size } = await stat(outPath);
|
|
199
|
+
return {
|
|
200
|
+
excludedButRelevant,
|
|
201
|
+
largest,
|
|
202
|
+
path: outPath,
|
|
203
|
+
sizeBytes: size,
|
|
204
|
+
sha256: hash.digest('hex'),
|
|
205
|
+
cleanup: () => rm(outPath, { force: true }),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=tarball.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tarball.js","sourceRoot":"","sources":["../src/tarball.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tarball.test.d.ts","sourceRoot":"","sources":["../src/tarball.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { alwaysExcluded, toMatcher } from './tarball.js';
|
|
4
|
+
/**
|
|
5
|
+
* What goes into the upload, and what must not.
|
|
6
|
+
*
|
|
7
|
+
* Two bugs live here. A native Android project (its own Gradle root, module in `app/`
|
|
8
|
+
* rather than `android/app/`) matched none of the `android/…` exclusions, so every
|
|
9
|
+
* build uploaded Gradle's entire output directory — 21 MB instead of 1 MB on a trivial
|
|
10
|
+
* app, over an uplink in Addis Ababa, for files the runner deletes before compiling.
|
|
11
|
+
* And a root-anchored `.gitignore` entry like `/build` was normalised into the literal
|
|
12
|
+
* path "/build", which no relative path can equal, so it excluded nothing.
|
|
13
|
+
*/
|
|
14
|
+
describe('tarball exclusions', () => {
|
|
15
|
+
const isIgnored = toMatcher([...alwaysExcluded]);
|
|
16
|
+
it('excludes Gradle output in a native Android project', () => {
|
|
17
|
+
assert.equal(isIgnored('app/build/outputs/apk/release/app-release.apk'), true);
|
|
18
|
+
assert.equal(isIgnored('.gradle/8.13/checksums/checksums.lock'), true);
|
|
19
|
+
assert.equal(isIgnored('.kotlin/sessions'), true);
|
|
20
|
+
assert.equal(isIgnored('.idea/workspace.xml'), true);
|
|
21
|
+
// Multi-module: the weight is in the modules, not the root.
|
|
22
|
+
assert.equal(isIgnored('feature/profile/build/intermediates/x.jar'), true);
|
|
23
|
+
});
|
|
24
|
+
it('still excludes the Expo layout it always did', () => {
|
|
25
|
+
assert.equal(isIgnored('android/app/build/outputs/x.apk'), true);
|
|
26
|
+
assert.equal(isIgnored('ios/Pods/Firebase/x.h'), true);
|
|
27
|
+
assert.equal(isIgnored('node_modules/react/index.js'), true);
|
|
28
|
+
});
|
|
29
|
+
it('keeps the source a build actually needs', () => {
|
|
30
|
+
assert.equal(isIgnored('app/build.gradle.kts'), false);
|
|
31
|
+
assert.equal(isIgnored('app/src/main/java/com/example/Main.kt'), false);
|
|
32
|
+
assert.equal(isIgnored('gradle/libs.versions.toml'), false);
|
|
33
|
+
assert.equal(isIgnored('gradlew'), false);
|
|
34
|
+
assert.equal(isIgnored('settings.gradle.kts'), false);
|
|
35
|
+
assert.equal(isIgnored('buildatlas.json'), false);
|
|
36
|
+
// `build` as a path segment, not as a prefix of a file name.
|
|
37
|
+
assert.equal(isIgnored('scripts/buildinfo.ts'), false);
|
|
38
|
+
});
|
|
39
|
+
it('honours a root-anchored ignore pattern at the root only', () => {
|
|
40
|
+
const matcher = toMatcher(['/local.properties', '/captures']);
|
|
41
|
+
assert.equal(matcher('local.properties'), true);
|
|
42
|
+
assert.equal(matcher('captures/trace.hprof'), true);
|
|
43
|
+
// Anchored means the root: the same name deeper down is a different file.
|
|
44
|
+
assert.equal(matcher('app/local.properties'), false);
|
|
45
|
+
});
|
|
46
|
+
it('still handles the unanchored forms', () => {
|
|
47
|
+
const matcher = toMatcher(['*.iml', 'ios/Pods', '.DS_Store']);
|
|
48
|
+
assert.equal(matcher('app/app.iml'), true);
|
|
49
|
+
assert.equal(matcher('ios/Pods/Manifest.lock'), true);
|
|
50
|
+
assert.equal(matcher('app/src/.DS_Store'), true);
|
|
51
|
+
assert.equal(matcher('app/src/main.kt'), false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('what a Flutter project must not upload', () => {
|
|
55
|
+
// A real Flutter app uploaded 1,175 MB and the runner spent 1h 16m downloading it to
|
|
56
|
+
// run a two-minute build. None of the weight was source. Flutter's own ignore rules
|
|
57
|
+
// cover most of this, but they live in ios/.gitignore, macos/.gitignore and the rest,
|
|
58
|
+
// and only the project root's file is read here.
|
|
59
|
+
const excluded = [
|
|
60
|
+
'.dart_tool/package_config.json',
|
|
61
|
+
'ios/.symlinks/plugins/path_provider/ios/Classes/x.m',
|
|
62
|
+
'ios/Pods/Firebase/x.h',
|
|
63
|
+
'macos/Flutter/ephemeral/FlutterMacOS.framework/x',
|
|
64
|
+
'linux/flutter/ephemeral/x',
|
|
65
|
+
'windows/flutter/ephemeral/x',
|
|
66
|
+
'.fvm/versions/3.24.3/bin/flutter',
|
|
67
|
+
'.fvm/flutter_sdk/bin/flutter',
|
|
68
|
+
'.flutter-plugins-dependencies',
|
|
69
|
+
'ios/DerivedData/Build/x.o',
|
|
70
|
+
];
|
|
71
|
+
for (const path of excluded) {
|
|
72
|
+
it(`leaves out ${path}`, () => {
|
|
73
|
+
assert.equal(toMatcher([...alwaysExcluded])(path), true);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const kept = [
|
|
77
|
+
'lib/main.dart',
|
|
78
|
+
'pubspec.yaml',
|
|
79
|
+
'pubspec.lock',
|
|
80
|
+
'ios/Runner/Info.plist',
|
|
81
|
+
'ios/Runner.xcodeproj/project.pbxproj',
|
|
82
|
+
'android/app/src/main/AndroidManifest.xml',
|
|
83
|
+
'assets/images/logo.png',
|
|
84
|
+
// The pins themselves. Dropping either means the runner silently builds with the
|
|
85
|
+
// wrong SDK, which is worse than any amount of upload weight.
|
|
86
|
+
'.fvmrc',
|
|
87
|
+
'.fvm/fvm_config.json',
|
|
88
|
+
];
|
|
89
|
+
for (const path of kept) {
|
|
90
|
+
it(`still ships ${path}`, () => {
|
|
91
|
+
// The other half, and the one that breaks a build rather than slowing it: an
|
|
92
|
+
// over-eager rule here means a project that cannot compile at all.
|
|
93
|
+
assert.equal(toMatcher([...alwaysExcluded])(path), false);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=tarball.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tarball.test.js","sourceRoot":"","sources":["../src/tarball.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ship-et/buildatlas-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "CLI for BuildAtlas — self-hosted Expo builds and OTA updates. Drop-in for `eas build`.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Digicom",
|
|
7
|
+
"homepage": "https://github.com/ship-et/BuildAtlas#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ship-et/BuildAtlas.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ship-et/BuildAtlas/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"registry": "https://registry.npmjs.org",
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"expo",
|
|
22
|
+
"eas",
|
|
23
|
+
"react-native",
|
|
24
|
+
"build",
|
|
25
|
+
"ci",
|
|
26
|
+
"ota",
|
|
27
|
+
"self-hosted",
|
|
28
|
+
"eas-build"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20.11"
|
|
33
|
+
},
|
|
34
|
+
"bin": {
|
|
35
|
+
"buildatlas": "dist/index.js",
|
|
36
|
+
"ba": "dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"dev": "tsx src/index.ts",
|
|
45
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"test": "node --env-file-if-exists=../../.env --test 'src/**/*.test.ts'",
|
|
48
|
+
"prepublishOnly": "rm -rf dist && tsc && chmod +x dist/index.js",
|
|
49
|
+
"prepare": "if command -v tsc >/dev/null 2>&1; then tsc && chmod +x dist/index.js; else echo 'buildatlas-cli: skipping the dist build, typescript is not installed'; fi"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"commander": "^15.0.0",
|
|
53
|
+
"picocolors": "^1.1.1",
|
|
54
|
+
"tar": "^7.5.22",
|
|
55
|
+
"zod": "^4.4.3"
|
|
56
|
+
}
|
|
57
|
+
}
|