eoas 1.0.1
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/.eslintignore +1 -0
- package/.eslintrc.js +73 -0
- package/.prettierrc +9 -0
- package/README.md +17 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +6 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +7 -0
- package/package.json +92 -0
- package/src/commands/generate-certs.ts +95 -0
- package/src/commands/init.ts +117 -0
- package/src/commands/publish.ts +277 -0
- package/src/index.d.ts +7 -0
- package/src/lib/assets.ts +118 -0
- package/src/lib/auth.ts +67 -0
- package/src/lib/expoConfig.ts +265 -0
- package/src/lib/log.ts +122 -0
- package/src/lib/ora.ts +113 -0
- package/src/lib/package.ts +6 -0
- package/src/lib/prompts.ts +97 -0
- package/src/lib/repo.ts +62 -0
- package/src/lib/runtimeVersion.ts +177 -0
- package/src/lib/utils.ts +3 -0
- package/src/lib/vcs/README.md +1 -0
- package/src/lib/vcs/clients/git.ts +390 -0
- package/src/lib/vcs/clients/gitNoCommit.ts +45 -0
- package/src/lib/vcs/clients/noVcs.ts +23 -0
- package/src/lib/vcs/git.ts +68 -0
- package/src/lib/vcs/index.ts +25 -0
- package/src/lib/vcs/local.ts +88 -0
- package/src/lib/vcs/vcs.ts +90 -0
- package/src/lib/workflow.ts +47 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// This file is copied from eas-cli[https://github.com/expo/eas-cli] to ensure consistent user experience across the CLI.
|
|
2
|
+
import { ExpoConfig, getConfig, getConfigFilePaths } from '@expo/config';
|
|
3
|
+
import { Env } from '@expo/eas-build-job';
|
|
4
|
+
import spawnAsync from '@expo/spawn-async';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import Joi from 'joi';
|
|
7
|
+
import jscodeshift, { Collection } from 'jscodeshift';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
import Log from './log';
|
|
11
|
+
import { isExpoInstalled } from './package';
|
|
12
|
+
|
|
13
|
+
export enum RequestedPlatform {
|
|
14
|
+
Android = 'android',
|
|
15
|
+
Ios = 'ios',
|
|
16
|
+
All = 'all',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type PublicExpoConfig = Omit<
|
|
20
|
+
ExpoConfig,
|
|
21
|
+
'_internal' | 'hooks' | 'ios' | 'android' | 'updates'
|
|
22
|
+
> & {
|
|
23
|
+
ios?: Omit<ExpoConfig['ios'], 'config'>;
|
|
24
|
+
android?: Omit<ExpoConfig['android'], 'config'>;
|
|
25
|
+
updates?: Omit<ExpoConfig['updates'], 'codeSigningCertificate' | 'codeSigningMetadata'>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface ExpoConfigOptions {
|
|
29
|
+
env?: Env;
|
|
30
|
+
skipSDKVersionRequirement?: boolean;
|
|
31
|
+
skipPlugins?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ExpoConfigOptionsInternal extends ExpoConfigOptions {
|
|
35
|
+
isPublicConfig?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let wasExpoConfigWarnPrinted = false;
|
|
39
|
+
|
|
40
|
+
async function getExpoConfigInternalAsync(
|
|
41
|
+
projectDir: string,
|
|
42
|
+
opts: ExpoConfigOptionsInternal = {}
|
|
43
|
+
): Promise<ExpoConfig> {
|
|
44
|
+
const originalProcessEnv: NodeJS.ProcessEnv = process.env;
|
|
45
|
+
try {
|
|
46
|
+
process.env = {
|
|
47
|
+
...process.env,
|
|
48
|
+
...opts.env,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let exp: ExpoConfig;
|
|
52
|
+
if (isExpoInstalled(projectDir)) {
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await spawnAsync(
|
|
55
|
+
'npx',
|
|
56
|
+
['expo', 'config', '--json', ...(opts.isPublicConfig ? ['--type', 'public'] : [])],
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
cwd: projectDir,
|
|
60
|
+
env: {
|
|
61
|
+
...process.env,
|
|
62
|
+
...opts.env,
|
|
63
|
+
EXPO_NO_DOTENV: '1',
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
exp = JSON.parse(stdout);
|
|
68
|
+
} catch (err: any) {
|
|
69
|
+
if (!wasExpoConfigWarnPrinted) {
|
|
70
|
+
Log.warn(
|
|
71
|
+
`Failed to read the app config from the project using "npx expo config" command: ${err.message}.`
|
|
72
|
+
);
|
|
73
|
+
Log.warn('Falling back to the version of "@expo/config" shipped with the EAS CLI.');
|
|
74
|
+
wasExpoConfigWarnPrinted = true;
|
|
75
|
+
}
|
|
76
|
+
exp = getConfig(projectDir, {
|
|
77
|
+
skipSDKVersionRequirement: true,
|
|
78
|
+
...(opts.isPublicConfig ? { isPublicConfig: true } : {}),
|
|
79
|
+
...(opts.skipPlugins ? { skipPlugins: true } : {}),
|
|
80
|
+
}).exp;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
exp = getConfig(projectDir, {
|
|
84
|
+
skipSDKVersionRequirement: true,
|
|
85
|
+
...(opts.isPublicConfig ? { isPublicConfig: true } : {}),
|
|
86
|
+
...(opts.skipPlugins ? { skipPlugins: true } : {}),
|
|
87
|
+
}).exp;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { error } = MinimalAppConfigSchema.validate(exp, {
|
|
91
|
+
allowUnknown: true,
|
|
92
|
+
abortEarly: true,
|
|
93
|
+
});
|
|
94
|
+
if (error) {
|
|
95
|
+
throw new Error(`Invalid app config.\n${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
return exp;
|
|
98
|
+
} finally {
|
|
99
|
+
process.env = originalProcessEnv;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const MinimalAppConfigSchema = Joi.object({
|
|
104
|
+
slug: Joi.string().required(),
|
|
105
|
+
name: Joi.string().required(),
|
|
106
|
+
version: Joi.string(),
|
|
107
|
+
android: Joi.object({
|
|
108
|
+
versionCode: Joi.number().integer(),
|
|
109
|
+
}),
|
|
110
|
+
ios: Joi.object({
|
|
111
|
+
buildNumber: Joi.string(),
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export async function getPrivateExpoConfigAsync(
|
|
116
|
+
projectDir: string,
|
|
117
|
+
opts: ExpoConfigOptions = {}
|
|
118
|
+
): Promise<ExpoConfig> {
|
|
119
|
+
ensureExpoConfigExists(projectDir);
|
|
120
|
+
return await getExpoConfigInternalAsync(projectDir, { ...opts, isPublicConfig: false });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function ensureExpoConfigExists(projectDir: string): void {
|
|
124
|
+
const paths = getConfigFilePaths(projectDir);
|
|
125
|
+
if (!paths?.staticConfigPath && !paths?.dynamicConfigPath) {
|
|
126
|
+
// eslint-disable-next-line node/no-sync
|
|
127
|
+
fs.writeFileSync(path.join(projectDir, 'app.json'), JSON.stringify({ expo: {} }, null, 2));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function isUsingStaticExpoConfig(projectDir: string): boolean {
|
|
132
|
+
const paths = getConfigFilePaths(projectDir);
|
|
133
|
+
return !!(paths.staticConfigPath?.endsWith('app.json') && !paths.dynamicConfigPath);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function getPublicExpoConfigAsync(
|
|
137
|
+
projectDir: string,
|
|
138
|
+
opts: ExpoConfigOptions = {}
|
|
139
|
+
): Promise<PublicExpoConfig> {
|
|
140
|
+
ensureExpoConfigExists(projectDir);
|
|
141
|
+
|
|
142
|
+
return await getExpoConfigInternalAsync(projectDir, { ...opts, isPublicConfig: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getExpoConfigUpdateUrl(config: ExpoConfig): string | undefined {
|
|
146
|
+
return config.updates?.url;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function createOrModifyExpoConfigAsync(
|
|
150
|
+
projectDir: string,
|
|
151
|
+
exp: Partial<ExpoConfig>
|
|
152
|
+
): Promise<void> {
|
|
153
|
+
try {
|
|
154
|
+
ensureExpoConfigExists(projectDir);
|
|
155
|
+
const configPathJS = path.join(projectDir, 'app.config.js');
|
|
156
|
+
const configPathTS = path.join(projectDir, 'app.config.ts');
|
|
157
|
+
|
|
158
|
+
// eslint-disable-next-line node/no-sync
|
|
159
|
+
const hasJsConfig = fs.existsSync(configPathJS);
|
|
160
|
+
|
|
161
|
+
if (isUsingStaticExpoConfig(projectDir)) {
|
|
162
|
+
Log.withInfo(
|
|
163
|
+
'You are using a static app config. We will create a dynamic config file for you.'
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const newConfigContent = `export default ({ config }) => ({
|
|
167
|
+
...config,
|
|
168
|
+
...${stringifyWithEnv(exp)}
|
|
169
|
+
});`;
|
|
170
|
+
// eslint-disable-next-line node/no-sync
|
|
171
|
+
fs.writeFileSync(configPathJS, newConfigContent);
|
|
172
|
+
} else if (hasJsConfig) {
|
|
173
|
+
// eslint-disable-next-line node/no-sync
|
|
174
|
+
const existingCode = fs.readFileSync(configPathJS, 'utf8');
|
|
175
|
+
const j = jscodeshift;
|
|
176
|
+
const ast: Collection = j(existingCode);
|
|
177
|
+
|
|
178
|
+
ast.find(j.ArrowFunctionExpression).forEach(path => {
|
|
179
|
+
if (
|
|
180
|
+
path.value.body &&
|
|
181
|
+
j.BlockStatement.check(path.value.body) &&
|
|
182
|
+
path.value.body.body.length > 0
|
|
183
|
+
) {
|
|
184
|
+
const returnStatement = path.value.body.body.find(node => j.ReturnStatement.check(node));
|
|
185
|
+
if (
|
|
186
|
+
returnStatement &&
|
|
187
|
+
j.ReturnStatement.check(returnStatement) &&
|
|
188
|
+
returnStatement.argument
|
|
189
|
+
) {
|
|
190
|
+
const configObject = returnStatement.argument;
|
|
191
|
+
if (j.ObjectExpression.check(configObject)) {
|
|
192
|
+
updateObjectExpression(j, configObject, exp);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
const updatedCode = ast.toSource({
|
|
198
|
+
quote: 'auto',
|
|
199
|
+
trailingComma: true,
|
|
200
|
+
reuseWhitespace: true,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// eslint-disable-next-line node/no-sync
|
|
204
|
+
fs.writeFileSync(configPathJS, updatedCode);
|
|
205
|
+
} else if (configPathTS) {
|
|
206
|
+
Log.warn('TypeScript support is not yet implemented.');
|
|
207
|
+
throw new Error('TypeScript support is not yet implemented.');
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
Log.withInfo('An error occurred while updating the Expo config. Please update it manually.');
|
|
211
|
+
Log.newLine();
|
|
212
|
+
Log.warn('Please modify your app.config.ts file manually by adding the following code:');
|
|
213
|
+
Log.newLine();
|
|
214
|
+
Log.withInfo(`${stringifyWithEnv(exp)}`);
|
|
215
|
+
Log.newLine();
|
|
216
|
+
throw e;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function updateObjectExpression(
|
|
221
|
+
j: typeof jscodeshift,
|
|
222
|
+
configObject: ReturnType<typeof j.objectExpression>,
|
|
223
|
+
updates: Record<string, any>
|
|
224
|
+
): void {
|
|
225
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
226
|
+
const existingProperty = configObject.properties.find(prop => {
|
|
227
|
+
return (
|
|
228
|
+
prop.type === 'Property' &&
|
|
229
|
+
((prop.key.type === 'Identifier' && prop.key.name === key) ||
|
|
230
|
+
(prop.key.type === 'StringLiteral' && prop.key.value === key))
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (existingProperty) {
|
|
235
|
+
configObject.properties = configObject.properties.filter(prop => prop !== existingProperty);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const newProperty = j.objectProperty(j.identifier(key), createValueNode(j, value));
|
|
239
|
+
|
|
240
|
+
configObject.properties.push(newProperty);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function createValueNode(j: typeof jscodeshift, value: any): any {
|
|
245
|
+
if (typeof value === 'string' && value.startsWith('process.env.')) {
|
|
246
|
+
return j.memberExpression(
|
|
247
|
+
j.memberExpression(j.identifier('process'), j.identifier('env')),
|
|
248
|
+
j.identifier(value.split('.')[2])
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (typeof value === 'object' && value !== null) {
|
|
253
|
+
return j.objectExpression(
|
|
254
|
+
Object.entries(value).map(
|
|
255
|
+
([key, val]) => j.objectProperty(j.stringLiteral(key), createValueNode(j, val)) // Force stringLiteral pour garder les guillemets
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return j.literal(value);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function stringifyWithEnv(obj: Record<string, any>): string {
|
|
264
|
+
return JSON.stringify(obj, null, 2).replace(/"process\.env\.(\w+)"/g, 'process.env.$1');
|
|
265
|
+
}
|
package/src/lib/log.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// This file is copied from eas-cli[https://github.com/expo/eas-cli] to ensure consistent user experience across the CLI.
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import figures from 'figures';
|
|
4
|
+
import { boolish } from 'getenv';
|
|
5
|
+
import logSymbols from 'log-symbols';
|
|
6
|
+
import terminalLink from 'terminal-link';
|
|
7
|
+
|
|
8
|
+
type Color = (...text: string[]) => string;
|
|
9
|
+
|
|
10
|
+
export default class Log {
|
|
11
|
+
public static readonly isDebug = boolish('DEBUG', false);
|
|
12
|
+
|
|
13
|
+
public static log(...args: any[]): void {
|
|
14
|
+
Log.consoleLog(...args);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public static newLine(): void {
|
|
18
|
+
Log.consoleLog();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public static addNewLineIfNone(): void {
|
|
22
|
+
if (!Log.isLastLineNewLine) {
|
|
23
|
+
Log.newLine();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public static error(...args: any[]): void {
|
|
28
|
+
Log.consoleLog(...Log.withTextColor(args, chalk.red));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public static warn(...args: any[]): void {
|
|
32
|
+
Log.consoleLog(...Log.withTextColor(args, chalk.yellow));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public static debug(...args: any[]): void {
|
|
36
|
+
if (Log.isDebug) {
|
|
37
|
+
Log.consoleLog(...args);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public static gray(...args: any[]): void {
|
|
42
|
+
Log.consoleLog(...Log.withTextColor(args, chalk.gray));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public static warnDeprecatedFlag(flag: string, message: string): void {
|
|
46
|
+
Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public static fail(message: string): void {
|
|
50
|
+
Log.log(`${chalk.red(logSymbols.error)} ${message}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public static succeed(message: string): void {
|
|
54
|
+
Log.log(`${chalk.green(logSymbols.success)} ${message}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public static withTick(...args: any[]): void {
|
|
58
|
+
Log.consoleLog(chalk.green(figures.tick), ...args);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public static withInfo(...args: any[]): void {
|
|
62
|
+
Log.consoleLog(chalk.green(figures.info), ...args);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private static consoleLog(...args: any[]): void {
|
|
66
|
+
Log.updateIsLastLineNewLine(args);
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.log(...args);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private static withTextColor(args: any[], chalkColor: Color): string[] {
|
|
72
|
+
return args.map(arg => chalkColor(arg));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private static isLastLineNewLine = false;
|
|
76
|
+
private static updateIsLastLineNewLine(args: any[]): void {
|
|
77
|
+
if (args.length === 0) {
|
|
78
|
+
Log.isLastLineNewLine = true;
|
|
79
|
+
} else {
|
|
80
|
+
const lastArg = args[args.length - 1];
|
|
81
|
+
if (typeof lastArg === 'string' && (lastArg === '' || lastArg.match(/[\r\n]$/))) {
|
|
82
|
+
Log.isLastLineNewLine = true;
|
|
83
|
+
} else {
|
|
84
|
+
Log.isLastLineNewLine = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Prints a link for given URL, using text if provided, otherwise text is just the URL.
|
|
92
|
+
* Format links as dim (unless disabled) and with an underline.
|
|
93
|
+
*
|
|
94
|
+
* @example https://expo.dev
|
|
95
|
+
*/
|
|
96
|
+
export function link(
|
|
97
|
+
url: string,
|
|
98
|
+
{ text = url, fallback, dim = true }: { text?: string; dim?: boolean; fallback?: string } = {}
|
|
99
|
+
): string {
|
|
100
|
+
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
|
|
101
|
+
const output = terminalLink(text, url, {
|
|
102
|
+
fallback: () =>
|
|
103
|
+
fallback ?? (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
|
|
104
|
+
});
|
|
105
|
+
return dim ? chalk.dim(output) : output;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Provide a consistent "Learn more" link experience.
|
|
110
|
+
* Format links as dim (unless disabled) with an underline.
|
|
111
|
+
*
|
|
112
|
+
* @example Learn more: https://expo.dev
|
|
113
|
+
*/
|
|
114
|
+
export function learnMore(
|
|
115
|
+
url: string,
|
|
116
|
+
{
|
|
117
|
+
learnMoreMessage: maybeLearnMoreMessage,
|
|
118
|
+
dim = true,
|
|
119
|
+
}: { learnMoreMessage?: string; dim?: boolean } = {}
|
|
120
|
+
): string {
|
|
121
|
+
return link(url, { text: maybeLearnMoreMessage ?? 'Learn more', dim });
|
|
122
|
+
}
|
package/src/lib/ora.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// This file is copied from eas-cli[https://github.com/expo/eas-cli] to ensure consistent user experience across the CLI.
|
|
2
|
+
import { boolish } from 'getenv';
|
|
3
|
+
// eslint-disable-next-line
|
|
4
|
+
import oraReal, { Options, Ora } from 'ora';
|
|
5
|
+
|
|
6
|
+
import Log from './log';
|
|
7
|
+
|
|
8
|
+
export { Ora, Options };
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line no-console
|
|
11
|
+
const logReal = console.log;
|
|
12
|
+
// eslint-disable-next-line no-console
|
|
13
|
+
const infoReal = console.info;
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
const warnReal = console.warn;
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
const errorReal = console.error;
|
|
18
|
+
|
|
19
|
+
const isCi = boolish('CI', false);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A custom ora spinner that sends the stream to stdout in CI, or non-TTY, instead of stderr (the default).
|
|
23
|
+
*
|
|
24
|
+
* @param options
|
|
25
|
+
* @returns
|
|
26
|
+
*/
|
|
27
|
+
export function ora(options?: Options | string): Ora {
|
|
28
|
+
const inputOptions = typeof options === 'string' ? { text: options } : options ?? {};
|
|
29
|
+
const disabled = Log.isDebug || !process.stdin.isTTY || isCi;
|
|
30
|
+
const spinner = oraReal({
|
|
31
|
+
// Ensure our non-interactive mode emulates CI mode.
|
|
32
|
+
isEnabled: !disabled,
|
|
33
|
+
// In non-interactive mode, send the stream to stdout so it prevents looking like an error.
|
|
34
|
+
stream: disabled ? process.stdout : process.stderr,
|
|
35
|
+
...inputOptions,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const oraStart = spinner.start.bind(spinner);
|
|
39
|
+
const oraStop = spinner.stop.bind(spinner);
|
|
40
|
+
const oraStopAndPersist = spinner.stopAndPersist.bind(spinner);
|
|
41
|
+
|
|
42
|
+
const logWrap = (method: any, args: any[]): void => {
|
|
43
|
+
oraStop();
|
|
44
|
+
method(...args);
|
|
45
|
+
spinner.start();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const wrapNativeLogs = (): void => {
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.log = (...args: any) => {
|
|
51
|
+
logWrap(logReal, args);
|
|
52
|
+
};
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.info = (...args: any) => {
|
|
55
|
+
logWrap(infoReal, args);
|
|
56
|
+
};
|
|
57
|
+
// eslint-disable-next-line no-console
|
|
58
|
+
console.warn = (...args: any) => {
|
|
59
|
+
logWrap(warnReal, args);
|
|
60
|
+
};
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.error = (...args: any) => {
|
|
63
|
+
logWrap(errorReal, args);
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const resetNativeLogs = (): void => {
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.log = logReal;
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.info = infoReal;
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.warn = warnReal;
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.error = errorReal;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
spinner.start = (text): Ora => {
|
|
79
|
+
// wrapNativeLogs wraps calls to console so they always:
|
|
80
|
+
// 1. stop the spinner
|
|
81
|
+
// 2. log the message
|
|
82
|
+
// 3. start the spinner again
|
|
83
|
+
// Every restart of the spinner causes the spinner message to be logged again
|
|
84
|
+
// which makes logs look like
|
|
85
|
+
//
|
|
86
|
+
// - Exporting...
|
|
87
|
+
// [expo-cli] Starting Metro Bundler
|
|
88
|
+
// - Exporting...
|
|
89
|
+
// [expo-cli] Android Bundling complete 3492ms
|
|
90
|
+
// - Exporting...
|
|
91
|
+
//
|
|
92
|
+
// Skipping wrapping native logs removes the repeated interleaved "Exporting..." messages.
|
|
93
|
+
if (!disabled) {
|
|
94
|
+
wrapNativeLogs();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return oraStart(text);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
spinner.stopAndPersist = (options): Ora => {
|
|
101
|
+
const result = oraStopAndPersist(options);
|
|
102
|
+
resetNativeLogs();
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
spinner.stop = (): Ora => {
|
|
107
|
+
const result = oraStop();
|
|
108
|
+
resetNativeLogs();
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return spinner;
|
|
113
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// This file is copied from eas-cli[https://github.com/expo/eas-cli] to ensure consistent user experience across the CLI.
|
|
2
|
+
import { constants } from 'os';
|
|
3
|
+
import prompts, { Answers, Choice, Options } from 'prompts';
|
|
4
|
+
|
|
5
|
+
export interface ExpoChoice<T> extends Choice {
|
|
6
|
+
value: T;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function promptAsync<T extends string = string>(
|
|
10
|
+
questions: prompts.PromptObject<T> | prompts.PromptObject<T>[],
|
|
11
|
+
options: Options = {}
|
|
12
|
+
): Promise<Answers<T>> {
|
|
13
|
+
if (!process.stdin.isTTY) {
|
|
14
|
+
const message = Array.isArray(questions) ? questions[0]?.message : questions.message;
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Input is required, but stdin is not readable. Failed to display prompt: ${message}`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return await prompts<T>(questions, {
|
|
20
|
+
onCancel() {
|
|
21
|
+
process.exit(constants.signals.SIGINT + 128); // Exit code 130 used when process is interrupted with ctrl+c.
|
|
22
|
+
},
|
|
23
|
+
...options,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function confirmAsync(
|
|
28
|
+
question: prompts.PromptObject<any>,
|
|
29
|
+
options?: Options
|
|
30
|
+
): Promise<boolean> {
|
|
31
|
+
const { value } = await promptAsync(
|
|
32
|
+
{
|
|
33
|
+
initial: true,
|
|
34
|
+
...question,
|
|
35
|
+
name: 'value',
|
|
36
|
+
type: 'confirm',
|
|
37
|
+
},
|
|
38
|
+
options
|
|
39
|
+
);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function selectAsync<T>(
|
|
44
|
+
message: string,
|
|
45
|
+
choices: ExpoChoice<T>[],
|
|
46
|
+
config?: {
|
|
47
|
+
options?: Options;
|
|
48
|
+
initial?: T;
|
|
49
|
+
warningMessageForDisabledEntries?: string;
|
|
50
|
+
}
|
|
51
|
+
): Promise<T> {
|
|
52
|
+
const initial = config?.initial ? choices.findIndex(({ value }) => value === config.initial) : 0;
|
|
53
|
+
const { value } = await promptAsync(
|
|
54
|
+
{
|
|
55
|
+
message,
|
|
56
|
+
choices,
|
|
57
|
+
initial,
|
|
58
|
+
name: 'value',
|
|
59
|
+
type: 'select',
|
|
60
|
+
warn: config?.warningMessageForDisabledEntries,
|
|
61
|
+
},
|
|
62
|
+
config?.options ?? {}
|
|
63
|
+
);
|
|
64
|
+
return value ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function toggleConfirmAsync(
|
|
68
|
+
questions: prompts.PromptObject<any>,
|
|
69
|
+
options?: Options
|
|
70
|
+
): Promise<boolean> {
|
|
71
|
+
const { value } = await promptAsync(
|
|
72
|
+
{
|
|
73
|
+
active: 'yes',
|
|
74
|
+
inactive: 'no',
|
|
75
|
+
...questions,
|
|
76
|
+
name: 'value',
|
|
77
|
+
type: 'toggle',
|
|
78
|
+
},
|
|
79
|
+
options
|
|
80
|
+
);
|
|
81
|
+
return value ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function pressAnyKeyToContinueAsync(): Promise<void> {
|
|
85
|
+
process.stdin.setRawMode(true);
|
|
86
|
+
process.stdin.resume();
|
|
87
|
+
process.stdin.setEncoding('utf8');
|
|
88
|
+
|
|
89
|
+
await new Promise<void>(res => {
|
|
90
|
+
process.stdin.on('data', key => {
|
|
91
|
+
if (String(key) === '\u0003') {
|
|
92
|
+
process.exit(constants.signals.SIGINT + 128); // ctrl-c
|
|
93
|
+
}
|
|
94
|
+
res();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
package/src/lib/repo.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// This file is copied from eas-cli[https://github.com/expo/eas-cli] to ensure consistent user experience across the CLI.
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
import Log from './log';
|
|
5
|
+
import { confirmAsync, promptAsync } from './prompts';
|
|
6
|
+
import { Client } from './vcs/vcs';
|
|
7
|
+
|
|
8
|
+
export async function commitPromptAsync(
|
|
9
|
+
vcsClient: Client,
|
|
10
|
+
{
|
|
11
|
+
initialCommitMessage,
|
|
12
|
+
commitAllFiles,
|
|
13
|
+
}: {
|
|
14
|
+
initialCommitMessage?: string;
|
|
15
|
+
commitAllFiles?: boolean;
|
|
16
|
+
} = {}
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const { message } = await promptAsync({
|
|
19
|
+
type: 'text',
|
|
20
|
+
name: 'message',
|
|
21
|
+
message: 'Commit message:',
|
|
22
|
+
initial: initialCommitMessage,
|
|
23
|
+
validate: (input: string) => input !== '',
|
|
24
|
+
});
|
|
25
|
+
await vcsClient.commitAsync({
|
|
26
|
+
commitAllFiles,
|
|
27
|
+
commitMessage: message,
|
|
28
|
+
nonInteractive: false,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function ensureRepoIsCleanAsync(
|
|
33
|
+
vcsClient: Client,
|
|
34
|
+
nonInteractive = false
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
if (!(await vcsClient.isCommitRequiredAsync())) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
Log.addNewLineIfNone();
|
|
40
|
+
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
|
|
41
|
+
Log.log(
|
|
42
|
+
`This operation needs to be run on a clean working tree. ${chalk.bold(
|
|
43
|
+
'Commit all your changes before proceeding'
|
|
44
|
+
)}.`
|
|
45
|
+
);
|
|
46
|
+
if (nonInteractive) {
|
|
47
|
+
Log.log('The following files need to be committed:');
|
|
48
|
+
await vcsClient.showChangedFilesAsync();
|
|
49
|
+
|
|
50
|
+
throw new Error('Commit all changes. Aborting...');
|
|
51
|
+
}
|
|
52
|
+
const answer = await confirmAsync({
|
|
53
|
+
message: `Commit changes to git?`,
|
|
54
|
+
type: 'confirm',
|
|
55
|
+
name: 'confirm git commit',
|
|
56
|
+
});
|
|
57
|
+
if (answer) {
|
|
58
|
+
await commitPromptAsync(vcsClient, { commitAllFiles: true });
|
|
59
|
+
} else {
|
|
60
|
+
throw new Error('Commit all changes. Aborting...');
|
|
61
|
+
}
|
|
62
|
+
}
|