eas-cli 16.19.3 → 16.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -85
- package/build/api.d.ts +1 -0
- package/build/api.js +5 -1
- package/build/commandUtils/workflow/validation.d.ts +6 -0
- package/build/commandUtils/workflow/validation.js +160 -0
- package/build/commands/build/configure.js +2 -19
- package/build/commands/env/create.js +0 -1
- package/build/commands/project/new.d.ts +33 -0
- package/build/commands/project/new.js +349 -0
- package/build/commands/project/onboarding.js +1 -1
- package/build/commands/workflow/run.js +0 -2
- package/build/commands/workflow/validate.d.ts +1 -0
- package/build/commands/workflow/validate.js +8 -39
- package/build/graphql/generated.d.ts +0 -6
- package/build/graphql/mutations/EnvironmentVariableMutation.d.ts +5 -24
- package/build/log.js +2 -1
- package/build/onboarding/installDependencies.d.ts +4 -1
- package/build/onboarding/installDependencies.js +13 -5
- package/build/utils/prompts.d.ts +4 -0
- package/build/utils/prompts.js +25 -1
- package/build/utils/workflowFile.d.ts +1 -3
- package/build/utils/workflowFile.js +2 -8
- package/oclif.manifest.json +23 -1
- package/package.json +3 -3
package/build/api.d.ts
CHANGED
|
@@ -19,4 +19,5 @@ export declare class ApiV2Client {
|
|
|
19
19
|
export declare function getExpoApiBaseUrl(): string;
|
|
20
20
|
export declare function getExpoWebsiteBaseUrl(): string;
|
|
21
21
|
export declare function getEASUpdateURL(projectId: string, manifestHostOverride: string | null): string;
|
|
22
|
+
export declare function getExpoApiWorkflowSchemaURL(): string;
|
|
22
23
|
export {};
|
package/build/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getEASUpdateURL = exports.getExpoWebsiteBaseUrl = exports.getExpoApiBaseUrl = exports.ApiV2Client = void 0;
|
|
3
|
+
exports.getExpoApiWorkflowSchemaURL = exports.getEASUpdateURL = exports.getExpoWebsiteBaseUrl = exports.getExpoApiBaseUrl = exports.ApiV2Client = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const ApiV2Error_1 = require("./ApiV2Error");
|
|
6
6
|
const fetch_1 = tslib_1.__importStar(require("./fetch"));
|
|
@@ -105,3 +105,7 @@ function getEASUpdateURL(projectId, manifestHostOverride) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
exports.getEASUpdateURL = getEASUpdateURL;
|
|
108
|
+
function getExpoApiWorkflowSchemaURL() {
|
|
109
|
+
return getExpoApiBaseUrl() + '/v2/workflows/schema';
|
|
110
|
+
}
|
|
111
|
+
exports.getExpoApiWorkflowSchemaURL = getExpoApiWorkflowSchemaURL;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
|
2
|
+
export declare function validateWorkflowFileAsync(workflowFileContents: {
|
|
3
|
+
yamlConfig: string;
|
|
4
|
+
filePath: string;
|
|
5
|
+
}, projectDir: string, graphqlClient: ExpoGraphqlClient, projectId: string): Promise<void>;
|
|
6
|
+
export declare function logWorkflowValidationErrors(error: unknown): void;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logWorkflowValidationErrors = exports.validateWorkflowFileAsync = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const eas_json_1 = require("@expo/eas-json");
|
|
6
|
+
const errors_1 = require("@expo/eas-json/build/errors");
|
|
7
|
+
const core_1 = require("@urql/core");
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
10
|
+
const YAML = tslib_1.__importStar(require("yaml"));
|
|
11
|
+
const api_1 = require("../../api");
|
|
12
|
+
const WorkflowRevisionMutation_1 = require("../../graphql/mutations/WorkflowRevisionMutation");
|
|
13
|
+
const log_1 = tslib_1.__importDefault(require("../../log"));
|
|
14
|
+
const ajv_1 = require("../../metadata/utils/ajv");
|
|
15
|
+
const workflowFile_1 = require("../../utils/workflowFile");
|
|
16
|
+
const jobTypesWithBuildProfile = new Set(['build', 'repack']);
|
|
17
|
+
const buildProfileIsInterpolated = (profileName) => {
|
|
18
|
+
return profileName.includes('${{') && profileName.includes('}}');
|
|
19
|
+
};
|
|
20
|
+
async function validateWorkflowFileAsync(workflowFileContents, projectDir, graphqlClient, projectId) {
|
|
21
|
+
const parsedYaml = parsedYamlFromWorkflowContents(workflowFileContents);
|
|
22
|
+
log_1.default.debug(`Parsed workflow: ${JSON.stringify(parsedYaml, null, 2)}`);
|
|
23
|
+
// Check if the parsed result is empty or null
|
|
24
|
+
log_1.default.debug(`Validating workflow is not empty...`);
|
|
25
|
+
validateWorkflowIsNotEmpty(parsedYaml);
|
|
26
|
+
const workflowSchema = await fetchWorkflowSchemaAsync();
|
|
27
|
+
// Check that all job types are valid
|
|
28
|
+
log_1.default.debug(`Validating workflow job types...`);
|
|
29
|
+
validateWorkflowJobTypes(parsedYaml, workflowSchema);
|
|
30
|
+
// Check for build jobs that do not match any EAS build profiles
|
|
31
|
+
log_1.default.debug(`Validating workflow build jobs...`);
|
|
32
|
+
await validateWorkflowBuildJobsAsync(parsedYaml, projectDir);
|
|
33
|
+
// Check that result passes validation against workflow schema
|
|
34
|
+
log_1.default.debug(`Validating workflow structure...`);
|
|
35
|
+
validateWorkflowStructure(parsedYaml, workflowSchema);
|
|
36
|
+
// Check for other errors using the server-side validation
|
|
37
|
+
log_1.default.debug(`Validating workflow on server...`);
|
|
38
|
+
await validateWorkflowOnServerAsync(graphqlClient, projectId, workflowFileContents);
|
|
39
|
+
}
|
|
40
|
+
exports.validateWorkflowFileAsync = validateWorkflowFileAsync;
|
|
41
|
+
function logWorkflowValidationErrors(error) {
|
|
42
|
+
if (error instanceof errors_1.MissingEasJsonError) {
|
|
43
|
+
throw new Error('Workflows require a valid eas.json. Please run "eas build:configure" to create it.');
|
|
44
|
+
}
|
|
45
|
+
else if (error instanceof errors_1.InvalidEasJsonError) {
|
|
46
|
+
throw new Error('Workflows require a valid eas.json. Please fix the errors in your eas.json and try again.\n\n' +
|
|
47
|
+
error.message);
|
|
48
|
+
}
|
|
49
|
+
else if (error instanceof YAML.YAMLParseError) {
|
|
50
|
+
log_1.default.error(`YAML syntax error: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
else if (error instanceof core_1.CombinedError) {
|
|
53
|
+
workflowFile_1.WorkflowFile.maybePrintWorkflowFileValidationErrors({
|
|
54
|
+
error,
|
|
55
|
+
});
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
else if (error instanceof Error) {
|
|
59
|
+
log_1.default.error(`Error: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
log_1.default.error(`Unexpected error: ${String(error)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.logWorkflowValidationErrors = logWorkflowValidationErrors;
|
|
66
|
+
function validateWorkflowIsNotEmpty(parsedYaml) {
|
|
67
|
+
if (parsedYaml === null ||
|
|
68
|
+
parsedYaml === undefined ||
|
|
69
|
+
(typeof parsedYaml === 'object' && Object.keys(parsedYaml).length === 0)) {
|
|
70
|
+
throw new Error('YAML file is empty or contains only comments.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function validateWorkflowOnServerAsync(graphqlClient, projectId, workflowFileContents) {
|
|
74
|
+
await WorkflowRevisionMutation_1.WorkflowRevisionMutation.validateWorkflowYamlConfigAsync(graphqlClient, {
|
|
75
|
+
appId: projectId,
|
|
76
|
+
yamlConfig: workflowFileContents.yamlConfig,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function validateWorkflowBuildJobsAsync(parsedYaml, projectDir) {
|
|
80
|
+
const jobs = jobsFromWorkflow(parsedYaml);
|
|
81
|
+
const buildJobs = jobs.filter(job => jobTypesWithBuildProfile.has(job.value.type));
|
|
82
|
+
if (buildJobs.length === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const easJsonAccessor = eas_json_1.EasJsonAccessor.fromProjectPath(projectDir);
|
|
86
|
+
const buildProfileNames = new Set(easJsonAccessor && (await eas_json_1.EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor)));
|
|
87
|
+
const invalidBuildJobs = buildJobs.filter(job => !buildProfileNames.has(job.value.params.profile) &&
|
|
88
|
+
// If a profile name is interpolated, we can't check if it's valid until the workflow actually runs
|
|
89
|
+
!buildProfileIsInterpolated(job.value.params.profile));
|
|
90
|
+
if (invalidBuildJobs.length > 0) {
|
|
91
|
+
const invalidBuildProfiles = new Set(invalidBuildJobs.map(job => job.value.params.profile));
|
|
92
|
+
throw new Error(`The build jobs in this workflow refer to the following build profiles that are not present in your eas.json file: ${[
|
|
93
|
+
...invalidBuildProfiles,
|
|
94
|
+
].join(', ')}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function validateWorkflowJobTypes(parsedYaml, workflowJsonSchema) {
|
|
98
|
+
const jobs = jobsFromWorkflow(parsedYaml);
|
|
99
|
+
const jobTypes = jobTypesFromWorkflowSchema(workflowJsonSchema);
|
|
100
|
+
const invalidJobs = jobs.filter(job => job.value.type && !jobTypes.includes(job.value.type));
|
|
101
|
+
if (invalidJobs.length > 0) {
|
|
102
|
+
throw new Error(`The following jobs have invalid types: ${invalidJobs
|
|
103
|
+
.map(job => job.key)
|
|
104
|
+
.join(', ')}. Valid types are: ${jobTypes.join(', ')}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function validateWorkflowStructure(parsedYaml, workflowJsonSchema) {
|
|
108
|
+
delete workflowJsonSchema['$schema'];
|
|
109
|
+
const ajv = (0, ajv_1.createValidator)();
|
|
110
|
+
const validate = ajv.compile(workflowJsonSchema);
|
|
111
|
+
const result = validate(parsedYaml);
|
|
112
|
+
if (!result) {
|
|
113
|
+
log_1.default.debug(JSON.stringify({
|
|
114
|
+
errors: validate.errors,
|
|
115
|
+
}, null, 2));
|
|
116
|
+
const readableErrors = (0, ajv_1.getReadableErrors)(validate.errors ?? []);
|
|
117
|
+
const processedErrors = new Set();
|
|
118
|
+
for (const err of readableErrors) {
|
|
119
|
+
if (err.message) {
|
|
120
|
+
processedErrors.add(err.message);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
throw new Error([...processedErrors].join('\n'));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function parsedYamlFromWorkflowContents(workflowFileContents) {
|
|
127
|
+
const parsedYaml = YAML.parse(workflowFileContents.yamlConfig);
|
|
128
|
+
return parsedYaml;
|
|
129
|
+
}
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
131
|
+
async function fetchWorkflowSchemaAsync() {
|
|
132
|
+
// EXPO_TESTING_WORKFLOW_SCHEMA_PATH is used only for testing against a different schema
|
|
133
|
+
if (process.env.EXPO_TESTING_WORKFLOW_SCHEMA_PATH) {
|
|
134
|
+
const schemaPath = path_1.default.resolve(process.env.EXPO_TESTING_WORKFLOW_SCHEMA_PATH);
|
|
135
|
+
log_1.default.debug(`Loading workflow schema from ${schemaPath}`);
|
|
136
|
+
const jsonString = await fs_1.promises.readFile(schemaPath, 'utf-8');
|
|
137
|
+
const jsonFromFile = JSON.parse(jsonString);
|
|
138
|
+
return jsonFromFile.data;
|
|
139
|
+
}
|
|
140
|
+
// Otherwise, we fetch from <ApiBaseUrl>/v2/workflows/schema
|
|
141
|
+
const schemaUrl = (0, api_1.getExpoApiWorkflowSchemaURL)();
|
|
142
|
+
log_1.default.debug(`Fetching workflow schema from ${schemaUrl}`);
|
|
143
|
+
const response = await fetch(schemaUrl);
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new Error(`Unable to fetch EAS Workflow schema, received status: ${response.status}`);
|
|
146
|
+
}
|
|
147
|
+
const jsonResponse = (await response.json());
|
|
148
|
+
return jsonResponse.data;
|
|
149
|
+
}
|
|
150
|
+
function jobsFromWorkflow(parsedYaml) {
|
|
151
|
+
return Object.entries(parsedYaml?.jobs).flatMap(([key, value]) => {
|
|
152
|
+
return {
|
|
153
|
+
key,
|
|
154
|
+
value,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function jobTypesFromWorkflowSchema(workflowJsonSchema) {
|
|
159
|
+
return workflowJsonSchema?.properties?.jobs?.additionalProperties?.anyOf.map((props) => props.properties.type.const);
|
|
160
|
+
}
|
|
@@ -12,10 +12,10 @@ const log_1 = tslib_1.__importStar(require("../../log"));
|
|
|
12
12
|
const platform_1 = require("../../platform");
|
|
13
13
|
const projectUtils_1 = require("../../project/projectUtils");
|
|
14
14
|
const workflow_1 = require("../../project/workflow");
|
|
15
|
-
const prompts_1 = require("../../prompts");
|
|
16
15
|
const UpdatesModule_1 = require("../../update/android/UpdatesModule");
|
|
17
16
|
const configure_2 = require("../../update/configure");
|
|
18
17
|
const UpdatesModule_2 = require("../../update/ios/UpdatesModule");
|
|
18
|
+
const prompts_1 = require("../../utils/prompts");
|
|
19
19
|
class BuildConfigure extends EasCommand_1.default {
|
|
20
20
|
static description = 'configure the project to support EAS Build';
|
|
21
21
|
static flags = {
|
|
@@ -93,24 +93,7 @@ class BuildConfigure extends EasCommand_1.default {
|
|
|
93
93
|
exports.default = BuildConfigure;
|
|
94
94
|
async function promptForPlatformAsync() {
|
|
95
95
|
log_1.default.addNewLineIfNone();
|
|
96
|
-
|
|
97
|
-
type: 'select',
|
|
96
|
+
return await (0, prompts_1.promptPlatformAsync)({
|
|
98
97
|
message: 'Which platforms would you like to configure for EAS Build?',
|
|
99
|
-
name: 'platform',
|
|
100
|
-
choices: [
|
|
101
|
-
{
|
|
102
|
-
title: 'All',
|
|
103
|
-
value: platform_1.RequestedPlatform.All,
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
title: 'iOS',
|
|
107
|
-
value: platform_1.RequestedPlatform.Ios,
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
title: 'Android',
|
|
111
|
-
value: platform_1.RequestedPlatform.Android,
|
|
112
|
-
},
|
|
113
|
-
],
|
|
114
98
|
});
|
|
115
|
-
return platform;
|
|
116
99
|
}
|
|
@@ -130,7 +130,6 @@ class EnvCreate extends EasCommand_1.default {
|
|
|
130
130
|
value,
|
|
131
131
|
visibility,
|
|
132
132
|
environments,
|
|
133
|
-
isGlobal: true, // TODO: every account-wide variable is global for now so it's not user facing
|
|
134
133
|
type: type ?? generated_1.EnvironmentSecretType.String,
|
|
135
134
|
}, ownerAccount.id);
|
|
136
135
|
if (!variable) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import EasCommand from '../../commandUtils/EasCommand';
|
|
2
|
+
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
|
3
|
+
import { AppFragment } from '../../graphql/generated';
|
|
4
|
+
import { PackageManager } from '../../onboarding/installDependencies';
|
|
5
|
+
import { Choice } from '../../prompts';
|
|
6
|
+
import { Actor } from '../../user/User';
|
|
7
|
+
export declare function promptForTargetDirectoryAsync(targetProjectDirFromArgs?: string): Promise<string>;
|
|
8
|
+
export declare function cloneTemplateAsync(targetProjectDir: string): Promise<string>;
|
|
9
|
+
export declare function installProjectDependenciesAsync(projectDir: string): Promise<PackageManager>;
|
|
10
|
+
export declare function getAccountChoices(actor: Actor, namesWithSufficientPermissions: Set<string>): Choice[];
|
|
11
|
+
export declare function createProjectAsync(graphqlClient: ExpoGraphqlClient, actor: Actor, projectDir: string): Promise<string>;
|
|
12
|
+
export declare function stripInvalidCharactersForBundleIdentifier(string: string): string;
|
|
13
|
+
export declare function generateConfigFilesAsync(projectDir: string, app: AppFragment): Promise<void>;
|
|
14
|
+
export declare function generateAppConfigAsync(projectDir: string, app: AppFragment): Promise<void>;
|
|
15
|
+
export declare function generateEasConfigAsync(projectDir: string): Promise<void>;
|
|
16
|
+
export declare function updatePackageJsonAsync(projectDir: string): Promise<void>;
|
|
17
|
+
export declare function copyProjectTemplatesAsync(projectDir: string): Promise<void>;
|
|
18
|
+
export declare function mergeReadmeAsync(projectDir: string): Promise<void>;
|
|
19
|
+
export declare function initializeGitRepositoryAsync(projectDir: string): Promise<void>;
|
|
20
|
+
export declare const formatScriptCommand: (script: string, packageManager: PackageManager) => string;
|
|
21
|
+
export default class New extends EasCommand {
|
|
22
|
+
static aliases: string[];
|
|
23
|
+
static description: string;
|
|
24
|
+
static flags: {};
|
|
25
|
+
static hidden: boolean;
|
|
26
|
+
static args: {
|
|
27
|
+
name: string;
|
|
28
|
+
}[];
|
|
29
|
+
static contextDefinition: {
|
|
30
|
+
loggedIn: import("../../commandUtils/context/LoggedInContextField").default;
|
|
31
|
+
};
|
|
32
|
+
runAsync(): Promise<void>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatScriptCommand = exports.initializeGitRepositoryAsync = exports.mergeReadmeAsync = exports.copyProjectTemplatesAsync = exports.updatePackageJsonAsync = exports.generateEasConfigAsync = exports.generateAppConfigAsync = exports.generateConfigFilesAsync = exports.stripInvalidCharactersForBundleIdentifier = exports.createProjectAsync = exports.getAccountChoices = exports.installProjectDependenciesAsync = exports.cloneTemplateAsync = exports.promptForTargetDirectoryAsync = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const eas_json_1 = require("@expo/eas-json");
|
|
6
|
+
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
7
|
+
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
8
|
+
const nullthrows_1 = tslib_1.__importDefault(require("nullthrows"));
|
|
9
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
10
|
+
const ts_deepmerge_1 = tslib_1.__importDefault(require("ts-deepmerge"));
|
|
11
|
+
const api_1 = require("../../api");
|
|
12
|
+
const url_1 = require("../../build/utils/url");
|
|
13
|
+
const EasCommand_1 = tslib_1.__importDefault(require("../../commandUtils/EasCommand"));
|
|
14
|
+
const generated_1 = require("../../graphql/generated");
|
|
15
|
+
const AppMutation_1 = require("../../graphql/mutations/AppMutation");
|
|
16
|
+
const AppQuery_1 = require("../../graphql/queries/AppQuery");
|
|
17
|
+
const log_1 = tslib_1.__importStar(require("../../log"));
|
|
18
|
+
const git_1 = require("../../onboarding/git");
|
|
19
|
+
const installDependencies_1 = require("../../onboarding/installDependencies");
|
|
20
|
+
const runCommand_1 = require("../../onboarding/runCommand");
|
|
21
|
+
const ora_1 = require("../../ora");
|
|
22
|
+
const expoConfig_1 = require("../../project/expoConfig");
|
|
23
|
+
const fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync_1 = require("../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync");
|
|
24
|
+
const prompts_1 = require("../../prompts");
|
|
25
|
+
const User_1 = require("../../user/User");
|
|
26
|
+
const easCli_1 = require("../../utils/easCli");
|
|
27
|
+
async function promptForTargetDirectoryAsync(targetProjectDirFromArgs) {
|
|
28
|
+
log_1.default.log(`🚚 Let's start by cloning the default Expo template project from GitHub and installing dependencies.`);
|
|
29
|
+
log_1.default.newLine();
|
|
30
|
+
if (targetProjectDirFromArgs) {
|
|
31
|
+
return targetProjectDirFromArgs;
|
|
32
|
+
}
|
|
33
|
+
const result = await (0, prompts_1.promptAsync)({
|
|
34
|
+
type: 'text',
|
|
35
|
+
name: 'targetProjectDir',
|
|
36
|
+
message: 'Where would you like to create your new project directory?',
|
|
37
|
+
initial: path_1.default.join(process.cwd(), 'new-expo-project'),
|
|
38
|
+
});
|
|
39
|
+
return result.targetProjectDir;
|
|
40
|
+
}
|
|
41
|
+
exports.promptForTargetDirectoryAsync = promptForTargetDirectoryAsync;
|
|
42
|
+
async function cloneTemplateAsync(targetProjectDir) {
|
|
43
|
+
const githubUsername = 'expo';
|
|
44
|
+
const githubRepositoryName = 'expo-template-default';
|
|
45
|
+
log_1.default.log(`📂 Cloning the project to ${targetProjectDir}`);
|
|
46
|
+
log_1.default.newLine();
|
|
47
|
+
const cloneMethod = (await (0, git_1.canAccessRepositoryUsingSshAsync)({
|
|
48
|
+
githubUsername,
|
|
49
|
+
githubRepositoryName,
|
|
50
|
+
}))
|
|
51
|
+
? 'ssh'
|
|
52
|
+
: 'https';
|
|
53
|
+
log_1.default.log(chalk_1.default.dim(`We detected that ${cloneMethod} is your preferred git clone method`));
|
|
54
|
+
log_1.default.newLine();
|
|
55
|
+
const { targetProjectDir: finalTargetProjectDirectory } = await (0, git_1.runGitCloneAsync)({
|
|
56
|
+
githubUsername,
|
|
57
|
+
githubRepositoryName,
|
|
58
|
+
targetProjectDir,
|
|
59
|
+
cloneMethod,
|
|
60
|
+
});
|
|
61
|
+
return finalTargetProjectDirectory;
|
|
62
|
+
}
|
|
63
|
+
exports.cloneTemplateAsync = cloneTemplateAsync;
|
|
64
|
+
async function installProjectDependenciesAsync(projectDir) {
|
|
65
|
+
const packageManager = await (0, installDependencies_1.promptForPackageManagerAsync)();
|
|
66
|
+
await (0, installDependencies_1.installDependenciesAsync)({
|
|
67
|
+
projectDir,
|
|
68
|
+
packageManager,
|
|
69
|
+
});
|
|
70
|
+
const dependencies = ['expo-updates', '@expo/metro-runtime'];
|
|
71
|
+
for (const dependency of dependencies) {
|
|
72
|
+
await (0, runCommand_1.runCommandAsync)({
|
|
73
|
+
cwd: projectDir,
|
|
74
|
+
command: 'npx',
|
|
75
|
+
args: ['expo', 'install', dependency],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return packageManager;
|
|
79
|
+
}
|
|
80
|
+
exports.installProjectDependenciesAsync = installProjectDependenciesAsync;
|
|
81
|
+
function getAccountChoices(actor, namesWithSufficientPermissions) {
|
|
82
|
+
const sortedAccounts = actor.accounts.sort((a, _b) => actor.__typename === 'User' ? (a.name === actor.username ? -1 : 1) : 0);
|
|
83
|
+
return sortedAccounts.map(account => {
|
|
84
|
+
const isPersonalAccount = actor.__typename === 'User' && account.name === actor.username;
|
|
85
|
+
const accountDisplayName = isPersonalAccount
|
|
86
|
+
? `${account.name} (personal account)`
|
|
87
|
+
: account.name;
|
|
88
|
+
const disabled = !namesWithSufficientPermissions.has(account.name);
|
|
89
|
+
return {
|
|
90
|
+
title: accountDisplayName,
|
|
91
|
+
value: { name: account.name },
|
|
92
|
+
...(disabled && {
|
|
93
|
+
disabled: true,
|
|
94
|
+
description: 'You do not have the required permissions to create projects on this account.',
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
exports.getAccountChoices = getAccountChoices;
|
|
100
|
+
async function createProjectAsync(graphqlClient, actor, projectDir) {
|
|
101
|
+
const allAccounts = actor.accounts;
|
|
102
|
+
const accountNamesWhereUserHasSufficientPermissionsToCreateApp = new Set(allAccounts
|
|
103
|
+
.filter(a => a.users.find(it => it.actor.id === actor.id)?.role !== generated_1.Role.ViewOnly)
|
|
104
|
+
.map(it => it.name));
|
|
105
|
+
let accountName = allAccounts[0].name;
|
|
106
|
+
if (allAccounts.length > 1) {
|
|
107
|
+
const choices = getAccountChoices(actor, accountNamesWhereUserHasSufficientPermissionsToCreateApp);
|
|
108
|
+
accountName = (await (0, prompts_1.promptAsync)({
|
|
109
|
+
type: 'select',
|
|
110
|
+
name: 'account',
|
|
111
|
+
message: 'Which account should own this project?',
|
|
112
|
+
choices,
|
|
113
|
+
})).account.name;
|
|
114
|
+
}
|
|
115
|
+
const projectName = (0, User_1.getActorUsername)(actor) + '-app';
|
|
116
|
+
const projectFullName = `@${accountName}/${projectName}`;
|
|
117
|
+
const existingProjectIdOnServer = await (0, fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync_1.findProjectIdByAccountNameAndSlugNullableAsync)(graphqlClient, accountName, projectName);
|
|
118
|
+
if (existingProjectIdOnServer) {
|
|
119
|
+
throw new Error(`Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Project ID configuration canceled. Re-run the command to select a different account/project.`);
|
|
120
|
+
}
|
|
121
|
+
if (!accountNamesWhereUserHasSufficientPermissionsToCreateApp.has(accountName)) {
|
|
122
|
+
throw new Error(`You don't have permission to create a new project on the ${accountName} account and no matching project already exists on the account.`);
|
|
123
|
+
}
|
|
124
|
+
const projectDashboardUrl = (0, url_1.getProjectDashboardUrl)(accountName, projectName);
|
|
125
|
+
const projectLink = (0, log_1.link)(projectDashboardUrl, { text: projectFullName });
|
|
126
|
+
const account = (0, nullthrows_1.default)(allAccounts.find(a => a.name === accountName));
|
|
127
|
+
const spinner = (0, ora_1.ora)(`Creating ${chalk_1.default.bold(projectFullName)}`).start();
|
|
128
|
+
let projectId;
|
|
129
|
+
try {
|
|
130
|
+
projectId = await AppMutation_1.AppMutation.createAppAsync(graphqlClient, {
|
|
131
|
+
accountId: account.id,
|
|
132
|
+
projectName,
|
|
133
|
+
});
|
|
134
|
+
spinner.succeed(`Created ${chalk_1.default.bold(projectLink)}`);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
spinner.fail();
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
const exp = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, { skipPlugins: true });
|
|
141
|
+
await (0, expoConfig_1.createOrModifyExpoConfigAsync)(projectDir, {
|
|
142
|
+
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
|
|
143
|
+
}, { skipSDKVersionRequirement: true });
|
|
144
|
+
log_1.default.withTick(`Project successfully linked (ID: ${chalk_1.default.bold(projectId)}) (modified app.json)`);
|
|
145
|
+
return projectId;
|
|
146
|
+
}
|
|
147
|
+
exports.createProjectAsync = createProjectAsync;
|
|
148
|
+
function stripInvalidCharactersForBundleIdentifier(string) {
|
|
149
|
+
return string.replaceAll(/[^A-Za-z0-9]/g, '');
|
|
150
|
+
}
|
|
151
|
+
exports.stripInvalidCharactersForBundleIdentifier = stripInvalidCharactersForBundleIdentifier;
|
|
152
|
+
async function generateConfigFilesAsync(projectDir, app) {
|
|
153
|
+
await generateAppConfigAsync(projectDir, app);
|
|
154
|
+
await generateEasConfigAsync(projectDir);
|
|
155
|
+
await updatePackageJsonAsync(projectDir);
|
|
156
|
+
await copyProjectTemplatesAsync(projectDir);
|
|
157
|
+
await mergeReadmeAsync(projectDir);
|
|
158
|
+
}
|
|
159
|
+
exports.generateConfigFilesAsync = generateConfigFilesAsync;
|
|
160
|
+
async function generateAppConfigAsync(projectDir, app) {
|
|
161
|
+
// Android package name requires each component to start with a lowercase letter.
|
|
162
|
+
const isUsernameValidSegment = /^[^a-z]/.test(app.ownerAccount.name);
|
|
163
|
+
const userPrefix = isUsernameValidSegment ? 'user' : '';
|
|
164
|
+
const isSlugValidSegment = /^[^a-z]/.test(app.slug);
|
|
165
|
+
const slugPrefix = isSlugValidSegment ? 'app' : '';
|
|
166
|
+
const bundleIdentifier = `com.${userPrefix}${stripInvalidCharactersForBundleIdentifier(app.ownerAccount.name)}.${slugPrefix}${stripInvalidCharactersForBundleIdentifier(app.slug)}`;
|
|
167
|
+
const updateUrl = (0, api_1.getEASUpdateURL)(app.id, /* manifestHostOverride */ null);
|
|
168
|
+
const { expo: baseExpoConfig } = await fs_extra_1.default.readJson(path_1.default.join(projectDir, 'app.json'));
|
|
169
|
+
const expoConfig = {
|
|
170
|
+
name: app.name ?? app.slug,
|
|
171
|
+
slug: app.slug,
|
|
172
|
+
scheme: stripInvalidCharactersForBundleIdentifier(app.name ?? app.slug),
|
|
173
|
+
extra: {
|
|
174
|
+
eas: {
|
|
175
|
+
projectId: app.id,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
owner: app.ownerAccount.name,
|
|
179
|
+
updates: {
|
|
180
|
+
url: updateUrl,
|
|
181
|
+
},
|
|
182
|
+
runtimeVersion: {
|
|
183
|
+
policy: 'appVersion',
|
|
184
|
+
},
|
|
185
|
+
ios: {
|
|
186
|
+
bundleIdentifier,
|
|
187
|
+
},
|
|
188
|
+
android: {
|
|
189
|
+
package: bundleIdentifier,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
const mergedConfig = (0, ts_deepmerge_1.default)(baseExpoConfig, expoConfig);
|
|
193
|
+
const appJsonPath = path_1.default.join(projectDir, 'app.json');
|
|
194
|
+
await fs_extra_1.default.writeJson(appJsonPath, { expo: mergedConfig }, { spaces: 2 });
|
|
195
|
+
log_1.default.withTick(`Generated ${chalk_1.default.bold('app.json')}. ${(0, log_1.learnMore)('https://docs.expo.dev/versions/latest/config/app/')}`);
|
|
196
|
+
log_1.default.log();
|
|
197
|
+
}
|
|
198
|
+
exports.generateAppConfigAsync = generateAppConfigAsync;
|
|
199
|
+
async function generateEasConfigAsync(projectDir) {
|
|
200
|
+
const easBuildGitHubConfig = {
|
|
201
|
+
android: {
|
|
202
|
+
image: 'latest',
|
|
203
|
+
},
|
|
204
|
+
ios: {
|
|
205
|
+
image: 'latest',
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
const easJson = {
|
|
209
|
+
cli: {
|
|
210
|
+
version: `>= ${easCli_1.easCliVersion}`,
|
|
211
|
+
appVersionSource: eas_json_1.AppVersionSource.REMOTE,
|
|
212
|
+
},
|
|
213
|
+
build: {
|
|
214
|
+
development: {
|
|
215
|
+
developmentClient: true,
|
|
216
|
+
distribution: 'internal',
|
|
217
|
+
...easBuildGitHubConfig,
|
|
218
|
+
},
|
|
219
|
+
'development-simulator': {
|
|
220
|
+
extends: 'development',
|
|
221
|
+
ios: {
|
|
222
|
+
simulator: true,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
preview: {
|
|
226
|
+
distribution: 'internal',
|
|
227
|
+
channel: 'main',
|
|
228
|
+
...easBuildGitHubConfig,
|
|
229
|
+
},
|
|
230
|
+
production: {
|
|
231
|
+
channel: 'production',
|
|
232
|
+
autoIncrement: true,
|
|
233
|
+
...easBuildGitHubConfig,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
submit: {
|
|
237
|
+
production: {},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
const easJsonPath = path_1.default.join(projectDir, 'eas.json');
|
|
241
|
+
await fs_extra_1.default.writeJson(easJsonPath, easJson, { spaces: 2 });
|
|
242
|
+
log_1.default.withTick(`Generated ${chalk_1.default.bold('eas.json')}. ${(0, log_1.learnMore)('https://docs.expo.dev/build-reference/eas-json/')}`);
|
|
243
|
+
log_1.default.log();
|
|
244
|
+
}
|
|
245
|
+
exports.generateEasConfigAsync = generateEasConfigAsync;
|
|
246
|
+
async function updatePackageJsonAsync(projectDir) {
|
|
247
|
+
const packageJsonPath = path_1.default.join(projectDir, 'package.json');
|
|
248
|
+
const packageJson = await fs_extra_1.default.readJson(packageJsonPath);
|
|
249
|
+
if (!packageJson.scripts) {
|
|
250
|
+
packageJson.scripts = {};
|
|
251
|
+
}
|
|
252
|
+
packageJson.scripts.preview = 'npx eas-cli@latest workflow:run publish-preview-update.yml';
|
|
253
|
+
packageJson.scripts['development-builds'] =
|
|
254
|
+
'npx eas-cli@latest workflow:run create-development-builds.yml';
|
|
255
|
+
packageJson.scripts.deploy = 'npx eas-cli@latest workflow:run deploy-to-production.yml';
|
|
256
|
+
await fs_extra_1.default.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
257
|
+
log_1.default.withTick('Updated package.json with scripts');
|
|
258
|
+
log_1.default.log();
|
|
259
|
+
}
|
|
260
|
+
exports.updatePackageJsonAsync = updatePackageJsonAsync;
|
|
261
|
+
async function copyProjectTemplatesAsync(projectDir) {
|
|
262
|
+
const templatesSourceDir = path_1.default.join(__dirname, 'templates', '.eas', 'workflows');
|
|
263
|
+
const easWorkflowsTargetDir = path_1.default.join(projectDir, '.eas', 'workflows');
|
|
264
|
+
await fs_extra_1.default.copy(templatesSourceDir, easWorkflowsTargetDir, {
|
|
265
|
+
overwrite: true,
|
|
266
|
+
errorOnExist: false,
|
|
267
|
+
});
|
|
268
|
+
log_1.default.withTick('Created EAS workflow files');
|
|
269
|
+
log_1.default.log();
|
|
270
|
+
}
|
|
271
|
+
exports.copyProjectTemplatesAsync = copyProjectTemplatesAsync;
|
|
272
|
+
async function mergeReadmeAsync(projectDir) {
|
|
273
|
+
const readmeTemplatePath = path_1.default.join(__dirname, 'templates', 'readme-additions.md');
|
|
274
|
+
const projectReadmePath = path_1.default.join(projectDir, 'README.md');
|
|
275
|
+
const readmeAdditions = await fs_extra_1.default.readFile(readmeTemplatePath, 'utf8');
|
|
276
|
+
const existingReadme = await fs_extra_1.default.readFile(projectReadmePath, 'utf8');
|
|
277
|
+
const targetSection = '## Get a fresh project';
|
|
278
|
+
const sectionIndex = existingReadme.indexOf(targetSection);
|
|
279
|
+
let mergedReadme;
|
|
280
|
+
if (sectionIndex !== -1) {
|
|
281
|
+
// Insert before "## Get a fresh project" section
|
|
282
|
+
const beforeSection = existingReadme.substring(0, sectionIndex).trim();
|
|
283
|
+
const afterSection = existingReadme.substring(sectionIndex);
|
|
284
|
+
mergedReadme = beforeSection + '\n\n' + readmeAdditions.trim() + '\n\n' + afterSection;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Append to the end if section doesn't exist
|
|
288
|
+
mergedReadme = existingReadme.trim() + '\n\n' + readmeAdditions.trim() + '\n';
|
|
289
|
+
}
|
|
290
|
+
await fs_extra_1.default.writeFile(projectReadmePath, mergedReadme);
|
|
291
|
+
log_1.default.withTick('Updated README.md with EAS configuration details');
|
|
292
|
+
log_1.default.log();
|
|
293
|
+
}
|
|
294
|
+
exports.mergeReadmeAsync = mergeReadmeAsync;
|
|
295
|
+
async function initializeGitRepositoryAsync(projectDir) {
|
|
296
|
+
await fs_extra_1.default.remove(path_1.default.join(projectDir, '.git'));
|
|
297
|
+
const commands = [['init'], ['add', '.'], ['commit', '-m', 'Initial commit']];
|
|
298
|
+
for (const args of commands) {
|
|
299
|
+
await (0, runCommand_1.runCommandAsync)({
|
|
300
|
+
cwd: projectDir,
|
|
301
|
+
command: 'git',
|
|
302
|
+
args,
|
|
303
|
+
});
|
|
304
|
+
log_1.default.log();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
exports.initializeGitRepositoryAsync = initializeGitRepositoryAsync;
|
|
308
|
+
const formatScriptCommand = (script, packageManager) => {
|
|
309
|
+
if (packageManager === 'npm') {
|
|
310
|
+
return `npm run ${script}`;
|
|
311
|
+
}
|
|
312
|
+
return `${packageManager} ${script}`;
|
|
313
|
+
};
|
|
314
|
+
exports.formatScriptCommand = formatScriptCommand;
|
|
315
|
+
class New extends EasCommand_1.default {
|
|
316
|
+
static aliases = ['new'];
|
|
317
|
+
static description = "create a new project set up with Expo's services.";
|
|
318
|
+
static flags = {};
|
|
319
|
+
static hidden = true;
|
|
320
|
+
static args = [{ name: 'TARGET_PROJECT_DIRECTORY' }];
|
|
321
|
+
static contextDefinition = {
|
|
322
|
+
...this.ContextOptions.LoggedIn,
|
|
323
|
+
};
|
|
324
|
+
async runAsync() {
|
|
325
|
+
const { args } = await this.parse(New);
|
|
326
|
+
const { loggedIn: { actor, graphqlClient }, } = await this.getContextAsync(New, { nonInteractive: false });
|
|
327
|
+
if (actor.__typename === 'Robot') {
|
|
328
|
+
throw new Error('This command is not available for robot users. Make sure you are not using a robot token and try again.');
|
|
329
|
+
}
|
|
330
|
+
log_1.default.warn('This command is not yet implemented. It will create a new project, but it will not be fully configured.');
|
|
331
|
+
log_1.default.log(`👋 Welcome to Expo, ${actor.username}!`);
|
|
332
|
+
log_1.default.newLine();
|
|
333
|
+
const targetProjectDirectory = await promptForTargetDirectoryAsync(args.TARGET_PROJECT_DIRECTORY);
|
|
334
|
+
const projectDirectory = await cloneTemplateAsync(targetProjectDirectory);
|
|
335
|
+
const packageManager = await installProjectDependenciesAsync(projectDirectory);
|
|
336
|
+
const projectId = await createProjectAsync(graphqlClient, actor, projectDirectory);
|
|
337
|
+
const app = await AppQuery_1.AppQuery.byIdAsync(graphqlClient, projectId);
|
|
338
|
+
await generateConfigFilesAsync(projectDirectory, app);
|
|
339
|
+
await initializeGitRepositoryAsync(projectDirectory);
|
|
340
|
+
log_1.default.log('🎉 We finished creating your new project.');
|
|
341
|
+
log_1.default.log('Next steps:');
|
|
342
|
+
log_1.default.withInfo(`Run \`cd ${projectDirectory}\` to navigate to your project.`);
|
|
343
|
+
log_1.default.withInfo(`Run \`${(0, exports.formatScriptCommand)('preview', packageManager)}\` to create a preview build on EAS. ${(0, log_1.learnMore)('https://docs.expo.dev/eas/workflows/examples/publish-preview-update/')}`);
|
|
344
|
+
log_1.default.withInfo(`Run \`${(0, exports.formatScriptCommand)('start', packageManager)}\` to start developing locally. ${(0, log_1.learnMore)('https://docs.expo.dev/get-started/start-developing/')}`);
|
|
345
|
+
log_1.default.withInfo(`See the README.md for more information about your project.`);
|
|
346
|
+
log_1.default.newLine();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
exports.default = New;
|
|
@@ -100,7 +100,7 @@ class Onboarding extends EasCommand_1.default {
|
|
|
100
100
|
}))
|
|
101
101
|
? 'ssh'
|
|
102
102
|
: 'https';
|
|
103
|
-
log_1.default.log(chalk_1.default.dim(`We detected that ${cloneMethod} is your
|
|
103
|
+
log_1.default.log(chalk_1.default.dim(`We detected that ${cloneMethod} is your preferred git clone method`));
|
|
104
104
|
log_1.default.log();
|
|
105
105
|
const { targetProjectDir: finalTargetProjectDirectory } = await (0, git_1.runGitCloneAsync)({
|
|
106
106
|
githubUsername,
|
|
@@ -132,8 +132,6 @@ class WorkflowRun extends EasCommand_1.default {
|
|
|
132
132
|
if (error instanceof core_2.CombinedError) {
|
|
133
133
|
workflowFile_1.WorkflowFile.maybePrintWorkflowFileValidationErrors({
|
|
134
134
|
error,
|
|
135
|
-
accountName: account.name,
|
|
136
|
-
projectName,
|
|
137
135
|
});
|
|
138
136
|
throw error;
|
|
139
137
|
}
|
|
@@ -11,6 +11,7 @@ export declare class WorkflowValidate extends EasCommand {
|
|
|
11
11
|
};
|
|
12
12
|
static contextDefinition: {
|
|
13
13
|
loggedIn: import("../../commandUtils/context/LoggedInContextField").default;
|
|
14
|
+
projectId: import("../../commandUtils/context/ProjectIdContextField").ProjectIdContextField;
|
|
14
15
|
projectDir: import("../../commandUtils/context/ProjectDirContextField").default;
|
|
15
16
|
getDynamicPublicProjectConfigAsync: import("../../commandUtils/context/DynamicProjectConfigContextField").DynamicPublicProjectConfigContextField;
|
|
16
17
|
getDynamicPrivateProjectConfigAsync: import("../../commandUtils/context/DynamicProjectConfigContextField").DynamicPrivateProjectConfigContextField;
|