@swell/cli 2.0.1-alpha.16 → 2.0.1-alpha.18
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/dist/app-command.d.ts +1 -1
- package/dist/app-command.js +9 -9
- package/dist/commands/app/dev.d.ts +16 -0
- package/dist/commands/app/dev.js +28 -0
- package/dist/commands/app/frontend/deploy.d.ts +15 -0
- package/dist/commands/app/frontend/deploy.js +41 -0
- package/dist/commands/app/frontend/dev.d.ts +19 -0
- package/dist/commands/app/frontend/dev.js +108 -0
- package/dist/commands/app/info.js +14 -9
- package/dist/commands/app/init.d.ts +12 -0
- package/dist/commands/app/init.js +34 -0
- package/dist/commands/app/install.d.ts +0 -3
- package/dist/commands/app/install.js +12 -21
- package/dist/commands/app/pull.d.ts +15 -0
- package/dist/commands/app/pull.js +50 -0
- package/dist/commands/app/push.d.ts +9 -12
- package/dist/commands/app/push.js +57 -131
- package/dist/commands/app/release.d.ts +13 -0
- package/dist/commands/app/release.js +148 -0
- package/dist/commands/app/version.d.ts +5 -4
- package/dist/commands/app/version.js +60 -39
- package/dist/commands/app/watch.d.ts +2 -4
- package/dist/commands/app/watch.js +3 -83
- package/dist/commands/create/app.d.ts +35 -11
- package/dist/commands/create/app.js +131 -128
- package/dist/commands/create/frontend.d.ts +12 -0
- package/dist/commands/create/frontend.js +38 -0
- package/dist/commands/create/function.d.ts +2 -2
- package/dist/commands/create/function.js +2 -2
- package/dist/commands/create/notification.d.ts +2 -2
- package/dist/commands/create/notification.js +2 -2
- package/dist/commands/env.d.ts +12 -0
- package/dist/commands/env.js +31 -0
- package/dist/commands/login.js +7 -7
- package/dist/commands/logs.js +0 -3
- package/dist/commands/theme/dev.d.ts +21 -0
- package/dist/commands/theme/dev.js +164 -0
- package/dist/commands/theme/init.d.ts +9 -0
- package/dist/commands/theme/init.js +15 -0
- package/dist/commands/theme/pull.d.ts +20 -0
- package/dist/commands/theme/pull.js +65 -0
- package/dist/commands/theme/push.d.ts +19 -0
- package/dist/commands/theme/push.js +89 -0
- package/dist/create-app-command.d.ts +20 -0
- package/dist/create-app-command.js +253 -0
- package/dist/create-collection-command.d.ts +3 -3
- package/dist/create-collection-command.js +4 -4
- package/dist/{create-command.d.ts → create-config-command.d.ts} +1 -1
- package/dist/{create-command.js → create-config-command.js} +3 -5
- package/dist/env/local.d.ts +8 -0
- package/dist/env/local.js +7 -0
- package/dist/env/production.d.ts +8 -0
- package/dist/env/production.js +7 -0
- package/dist/env/review.d.ts +8 -0
- package/dist/env/review.js +7 -0
- package/dist/env/staging.d.ts +8 -0
- package/dist/env/staging.js +7 -0
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.js +39 -13
- package/dist/lib/app-config.d.ts +12 -5
- package/dist/lib/app-config.js +57 -21
- package/dist/lib/apps/app-config.d.ts +17 -3
- package/dist/lib/apps/app-config.js +194 -66
- package/dist/lib/apps/index.d.ts +92 -12
- package/dist/lib/apps/index.js +145 -47
- package/dist/lib/apps/paths.d.ts +14 -7
- package/dist/lib/apps/paths.js +73 -28
- package/dist/lib/config.d.ts +9 -2
- package/dist/lib/config.js +49 -1
- package/dist/lib/constants.d.ts +5 -0
- package/dist/lib/constants.js +23 -4
- package/dist/lib/create/content.d.ts +1 -1
- package/dist/lib/create/index.js +5 -1
- package/dist/lib/info.js +2 -2
- package/dist/lib/stores.d.ts +5 -3
- package/dist/lib/stores.js +7 -5
- package/dist/lib/style.d.ts +2 -0
- package/dist/lib/style.js +2 -0
- package/dist/push-app-command.d.ts +73 -0
- package/dist/push-app-command.js +754 -0
- package/dist/remote-app-command.d.ts +35 -9
- package/dist/remote-app-command.js +417 -159
- package/oclif.manifest.json +817 -71
- package/package.json +13 -1
- package/dist/commands/app/_pull.d.ts +0 -8
- package/dist/commands/app/_pull.js +0 -45
|
@@ -1,60 +1,117 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
1
2
|
import { inflect } from 'inflection';
|
|
2
|
-
import
|
|
3
|
+
import sortBy from 'lodash/sortBy.js';
|
|
3
4
|
import * as path from 'node:path';
|
|
4
|
-
import ora from 'ora';
|
|
5
|
+
import { default as ora } from 'ora';
|
|
5
6
|
import { AppCommand } from './app-command.js';
|
|
6
|
-
import {
|
|
7
|
-
import { FunctionProcessingError, IgnoringFileError, allConfigFilesPaths, appConfigFromFile,
|
|
8
|
-
import { default as
|
|
9
|
-
import { getLoginHost } from './lib/constants.js';
|
|
7
|
+
import { readRcFile, writeRcFile } from './lib/app-config.js';
|
|
8
|
+
import { AppConfig, FunctionProcessingError, IgnoringFileError, SwellJsonFields, allBaseFilesInDir, allConfigFilesPaths, appConfigFromFile, batchAppFilesByType, deleteFile, hashFile, writeFile, writeJsonFile, } from './lib/apps/index.js';
|
|
9
|
+
import { default as localConfig } from './lib/config.js';
|
|
10
|
+
import { getAppFrontendHost, getLocalFrontendHost, getLoginHost, getStorefrontFrontendHost, } from './lib/constants.js';
|
|
10
11
|
import { toAppId } from './lib/create/index.js';
|
|
11
12
|
import style from './lib/style.js';
|
|
13
|
+
const PUSH_CONCURRENCY = 3;
|
|
12
14
|
/**
|
|
13
15
|
* Extends the AppCommand with methods that ease managing remote apps and
|
|
14
16
|
* configurations.
|
|
15
17
|
*/
|
|
16
18
|
export class RemoteAppCommand extends AppCommand {
|
|
17
19
|
// allows child commands to set orientation
|
|
20
|
+
static delayOrientation = false;
|
|
18
21
|
// most of the times this will be the target environment
|
|
19
22
|
static orientation;
|
|
20
|
-
//
|
|
23
|
+
// API instance of the current app definition
|
|
21
24
|
app = {};
|
|
25
|
+
// Indicates the app was created during the command execution
|
|
26
|
+
appCreated = false;
|
|
27
|
+
appType = 'any';
|
|
28
|
+
// API instance of the target storefront, if applicable
|
|
29
|
+
storefront = null;
|
|
30
|
+
// Indicates app configs are being synced along with theme configs
|
|
31
|
+
themeSyncApp = false;
|
|
32
|
+
appFrontendUrl(storeId, installedAppId) {
|
|
33
|
+
return getAppFrontendHost(storeId, installedAppId);
|
|
34
|
+
}
|
|
35
|
+
async cleanRemoteConfigs(remoteConfigs, deleteDeprecated = true) {
|
|
36
|
+
// Delete remote configs that do not have file_path etc
|
|
37
|
+
const deprecatedConfigs = remoteConfigs.filter((config) => this.isConfigDeprecated(config));
|
|
38
|
+
if (deleteDeprecated && deprecatedConfigs.length > 0) {
|
|
39
|
+
const spinner = ora();
|
|
40
|
+
spinner.start('Cleaning up deprecated configurations, please wait...');
|
|
41
|
+
await Promise.all(deprecatedConfigs.map((config) => this.removeRemoteFile(config, false)));
|
|
42
|
+
spinner.stop();
|
|
43
|
+
}
|
|
44
|
+
const validConfigs = remoteConfigs.filter((config) => !this.isConfigDeprecated(config));
|
|
45
|
+
return validConfigs;
|
|
46
|
+
}
|
|
22
47
|
dashboardUrl(flags = {}, storeId) {
|
|
23
48
|
const { env } = flags;
|
|
24
49
|
const envFlag = env || this.ctor.orientation?.env;
|
|
25
50
|
const outputEnv = envFlag === 'live' ? '' : envFlag ? 'test' : '';
|
|
26
51
|
return `${getLoginHost(storeId)}/admin/${outputEnv ? `${outputEnv}/` : ''}apps/${this.app.id}`;
|
|
27
52
|
}
|
|
28
|
-
async deleteConfig(configId) {
|
|
29
|
-
|
|
30
|
-
this.error('App is not defined');
|
|
31
|
-
}
|
|
53
|
+
async deleteConfig(configId, filePath, appConfigId) {
|
|
54
|
+
const themeId = this.app.type === 'theme' && this.storefront?.theme_id;
|
|
32
55
|
return this.api.delete({
|
|
33
|
-
adminPath: `/apps/${this.app.id}/configs/${configId}`,
|
|
56
|
+
adminPath: `/apps/${this.app.id}/configs/${appConfigId || configId}`,
|
|
57
|
+
}, {
|
|
58
|
+
query: {
|
|
59
|
+
...(filePath ? { file_path: filePath } : undefined),
|
|
60
|
+
...(themeId ? { theme_id: themeId } : undefined),
|
|
61
|
+
...(appConfigId ? { app_config_id: appConfigId } : undefined),
|
|
62
|
+
...(this.themeSyncApp ? { $sync_app: true } : undefined),
|
|
63
|
+
},
|
|
34
64
|
});
|
|
35
65
|
}
|
|
36
66
|
async getApp(id = '') {
|
|
37
67
|
const app = await this.api.get({ adminPath: `/apps/${id}` });
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
const themeId = this.appType === 'theme' && this.storefront?.theme_id;
|
|
69
|
+
const canPullConfigs = themeId || app.owned;
|
|
70
|
+
if (canPullConfigs) {
|
|
71
|
+
// TODO: paginate configs to support up to 10,000 (otherwise 1,000)
|
|
72
|
+
// Get app configs for processing
|
|
73
|
+
const configs = await this.api.get({ adminPath: `/apps/${id}/configs` }, {
|
|
74
|
+
query: {
|
|
75
|
+
limit: 1000,
|
|
76
|
+
...(themeId ? { theme_id: themeId } : undefined),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
// Instantiate AppConfig for each object
|
|
80
|
+
app.configs = (configs.results || []).map((config) => AppConfig.create({
|
|
81
|
+
...config,
|
|
82
|
+
appPath: this.appPath,
|
|
83
|
+
}));
|
|
84
|
+
// Pull app configs to compare with theme for push/pull
|
|
85
|
+
if (themeId && this.themeSyncApp) {
|
|
86
|
+
const appConfigs = await this.api.get({ adminPath: `/apps/${id}/configs` }, { query: { limit: 1000 } });
|
|
87
|
+
for (const appConfig of appConfigs.results || []) {
|
|
88
|
+
const themeConfig = app.configs.find((config) => config.filePath === appConfig.file_path);
|
|
89
|
+
if (themeConfig && themeConfig.fileHash !== appConfig.hash) {
|
|
90
|
+
themeConfig.hash = appConfig.hash;
|
|
91
|
+
}
|
|
92
|
+
else if (!themeConfig && appConfig) {
|
|
93
|
+
app.configs.push(AppConfig.create({
|
|
94
|
+
...appConfig,
|
|
95
|
+
appConfigId: appConfig.id,
|
|
96
|
+
appPath: this.appPath,
|
|
97
|
+
id: undefined,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const themeConfig of app.configs) {
|
|
102
|
+
const appConfig = appConfigs.results.find((config) => themeConfig.filePath === config.file_path);
|
|
103
|
+
if (!appConfig) {
|
|
104
|
+
themeConfig.hash = 'x';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
48
107
|
}
|
|
49
|
-
app.configs.push(config);
|
|
50
108
|
}
|
|
51
|
-
this.debugJson('getApp: remote app', app);
|
|
52
109
|
return app;
|
|
53
110
|
}
|
|
54
111
|
async getAppWithConfig(id = '') {
|
|
55
112
|
const swellId = this.app?.id;
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
113
|
+
const swellConfigId = this.swellConfig.get('id');
|
|
114
|
+
if (!swellConfigId) {
|
|
58
115
|
this.error(`App ${style.appConfigName('id')} is not defined in swell.json.`);
|
|
59
116
|
}
|
|
60
117
|
let appId = id || swellId;
|
|
@@ -63,8 +120,8 @@ export class RemoteAppCommand extends AppCommand {
|
|
|
63
120
|
if (!privateId) {
|
|
64
121
|
this.error(`App ${style.appConfigName('id')} is not defined in swell.json.`);
|
|
65
122
|
}
|
|
66
|
-
if (privateId !==
|
|
67
|
-
this.error(`App ${style.appConfigName('id')} must be declared in snake_case: ${style.appConfigValue(
|
|
123
|
+
if (privateId !== swellConfigId) {
|
|
124
|
+
this.error(`App ${style.appConfigName('id')} must be declared in snake_case: ${style.appConfigValue(swellConfigId)}.`);
|
|
68
125
|
}
|
|
69
126
|
appId = privateId;
|
|
70
127
|
}
|
|
@@ -72,9 +129,9 @@ export class RemoteAppCommand extends AppCommand {
|
|
|
72
129
|
let app = {};
|
|
73
130
|
try {
|
|
74
131
|
app = await this.getApp(appId);
|
|
132
|
+
this.app.id = app.id;
|
|
75
133
|
}
|
|
76
134
|
catch (error) {
|
|
77
|
-
this.debug('getApp: app not found', error);
|
|
78
135
|
let errorJson;
|
|
79
136
|
try {
|
|
80
137
|
errorJson = JSON.parse(error.message);
|
|
@@ -103,100 +160,121 @@ export class RemoteAppCommand extends AppCommand {
|
|
|
103
160
|
}
|
|
104
161
|
return app;
|
|
105
162
|
}
|
|
163
|
+
async getConfigData(configId) {
|
|
164
|
+
const themeId = this.app.type === 'theme' && this.storefront?.theme_id;
|
|
165
|
+
return this.api.get({
|
|
166
|
+
adminPath: `/apps/${this.app.id}/configs/${configId}/data`,
|
|
167
|
+
}, { query: { ...(themeId ? { theme_id: themeId } : undefined) } });
|
|
168
|
+
}
|
|
106
169
|
async getCreateUpdateApp(updateApp) {
|
|
107
170
|
const spinner = ora();
|
|
108
|
-
let
|
|
171
|
+
let sourceApp;
|
|
109
172
|
try {
|
|
110
|
-
|
|
173
|
+
sourceApp = await readRcFile(this.swellRcPath());
|
|
111
174
|
}
|
|
112
175
|
catch (error) {
|
|
113
176
|
if (error.code !== 'ENOENT') {
|
|
114
177
|
throw error;
|
|
115
178
|
}
|
|
116
179
|
}
|
|
117
|
-
|
|
180
|
+
if (!this.swellConfig.store.id) {
|
|
181
|
+
throw new Error('App swell.json is missing or `id` is not defined.');
|
|
182
|
+
}
|
|
118
183
|
const body = {
|
|
119
|
-
...this.
|
|
184
|
+
...this.swellConfig.store,
|
|
120
185
|
// we store locally the private id as the id, but we need to switch it
|
|
121
186
|
// when posting to the API
|
|
122
187
|
id: undefined,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
? { private_id: this.appConfig.store.id }
|
|
188
|
+
...(this.swellConfig.store.id
|
|
189
|
+
? { private_id: this.swellConfig.store.id }
|
|
126
190
|
: undefined),
|
|
127
|
-
...(
|
|
191
|
+
...(sourceApp?.id ? { source_id: sourceApp.id } : undefined),
|
|
128
192
|
};
|
|
129
|
-
const
|
|
130
|
-
if (logoIcon) {
|
|
131
|
-
const icon = fs.readFileSync(logoIcon, { encoding: 'base64' });
|
|
132
|
-
body.logo_icon = icon;
|
|
133
|
-
}
|
|
134
|
-
/* eslint-enable camelcase */
|
|
193
|
+
const appLabel = this.app.type === 'theme' ? 'theme' : 'app';
|
|
135
194
|
let app = updateApp || {};
|
|
136
|
-
let appCreated = false;
|
|
137
195
|
if (!app.id) {
|
|
138
196
|
// May already have a dev instance
|
|
139
197
|
let existingDevApp;
|
|
140
198
|
try {
|
|
141
199
|
existingDevApp = await this.api.get({
|
|
142
|
-
adminPath: `/apps/${this.
|
|
200
|
+
adminPath: `/apps/${this.swellConfig.store.id}`,
|
|
143
201
|
});
|
|
144
202
|
}
|
|
145
203
|
catch {
|
|
146
204
|
// noop
|
|
147
205
|
}
|
|
148
|
-
spinner.start(`Creating
|
|
206
|
+
spinner.start(`Creating ${appLabel}...`);
|
|
149
207
|
if (existingDevApp) {
|
|
150
|
-
app = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${existingDevApp.id}` }, { body }), () => spinner.fail('Error
|
|
208
|
+
app = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${existingDevApp.id}` }, { body }), () => spinner.fail('Error updating app.'));
|
|
151
209
|
}
|
|
152
210
|
else {
|
|
153
|
-
app = await this.handleRequestErrors(async () => this.api.post({ adminPath: '/apps' }, { body }), () => spinner.fail('Error creating app'));
|
|
154
|
-
appCreated = true;
|
|
211
|
+
app = await this.handleRequestErrors(async () => this.api.post({ adminPath: '/apps' }, { body }), () => spinner.fail('Error creating app.'));
|
|
212
|
+
this.appCreated = true;
|
|
155
213
|
}
|
|
156
214
|
}
|
|
157
215
|
else if (app.id) {
|
|
158
|
-
spinner.start(`Updating
|
|
159
|
-
app = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${app.id}` }, { body }), () => spinner.fail('Error updating app'));
|
|
216
|
+
spinner.start(`Updating ${appLabel}...`);
|
|
217
|
+
app = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${app.id}` }, { body }), () => spinner.fail('Error updating app.'));
|
|
160
218
|
}
|
|
161
219
|
if (app?.id) {
|
|
162
|
-
|
|
220
|
+
if (!sourceApp) {
|
|
221
|
+
await writeRcFile(this.swellRcPath(), { id: app.id });
|
|
222
|
+
}
|
|
163
223
|
// if not failed, we want to display correct messaging
|
|
164
224
|
if (spinner.isSpinning) {
|
|
165
|
-
if (appCreated) {
|
|
166
|
-
spinner.succeed(
|
|
167
|
-
this.log();
|
|
225
|
+
if (this.appCreated) {
|
|
226
|
+
spinner.succeed(`Development ${appLabel} created.`);
|
|
168
227
|
}
|
|
169
228
|
else {
|
|
170
|
-
spinner.
|
|
171
|
-
}
|
|
172
|
-
this.log('App details');
|
|
173
|
-
spinner.succeed('app details');
|
|
174
|
-
if (logoIcon) {
|
|
175
|
-
spinner.succeed('app logo');
|
|
229
|
+
spinner.succeed(`${appLabel === 'theme' ? 'Theme app' : 'App'} details up to date.`);
|
|
176
230
|
}
|
|
231
|
+
this.log();
|
|
177
232
|
}
|
|
178
|
-
this.
|
|
179
|
-
return app;
|
|
233
|
+
return this.getApp(app.id);
|
|
180
234
|
}
|
|
181
235
|
return {};
|
|
182
236
|
}
|
|
237
|
+
async getInstalledApp() {
|
|
238
|
+
// check if app is already installed on store
|
|
239
|
+
return this.api.get({ adminPath: `/client/apps/${this.app.id}` }, {});
|
|
240
|
+
}
|
|
241
|
+
getLocalAppConfigs() {
|
|
242
|
+
const localConfigs = [];
|
|
243
|
+
for (const { configFile, configType } of allConfigFilesPaths(this.appPath)) {
|
|
244
|
+
const config = appConfigFromFile(configFile, configType, this.appPath);
|
|
245
|
+
localConfigs.push(config);
|
|
246
|
+
}
|
|
247
|
+
for (const filePath of allBaseFilesInDir(this.appPath)) {
|
|
248
|
+
const config = appConfigFromFile(filePath, 'file', this.appPath);
|
|
249
|
+
localConfigs.push(config);
|
|
250
|
+
}
|
|
251
|
+
return localConfigs;
|
|
252
|
+
}
|
|
253
|
+
async getVersion(version) {
|
|
254
|
+
const versionsResponse = await this.api.get({ adminPath: `/apps/${this.app.id}/versions` }, {
|
|
255
|
+
query: {
|
|
256
|
+
limit: 1,
|
|
257
|
+
...(version ? { version } : undefined),
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
return versionsResponse.results?.[0];
|
|
261
|
+
}
|
|
183
262
|
async getVersions(version) {
|
|
184
263
|
const versionsResponse = await this.api.get({ adminPath: `/apps/${this.app.id}/versions` }, {
|
|
185
264
|
query: {
|
|
186
|
-
limit: 1000,
|
|
187
|
-
sort: 'version desc',
|
|
188
265
|
...(version ? { version } : undefined),
|
|
189
266
|
},
|
|
190
267
|
});
|
|
191
|
-
this.debugJson('getApp: versionsResponse', versionsResponse);
|
|
192
268
|
return versionsResponse.results || [];
|
|
193
269
|
}
|
|
194
270
|
async handleRequestErrors(request, spinnerError) {
|
|
271
|
+
let showedErrors = false;
|
|
195
272
|
try {
|
|
196
273
|
const response = await request();
|
|
197
274
|
if (response?.status !== 200 && response?.errors) {
|
|
198
275
|
this.showErrors(response.errors, spinnerError);
|
|
199
|
-
|
|
276
|
+
showedErrors = true;
|
|
277
|
+
this.error('Something went wrong.');
|
|
200
278
|
}
|
|
201
279
|
return response;
|
|
202
280
|
}
|
|
@@ -208,13 +286,13 @@ export class RemoteAppCommand extends AppCommand {
|
|
|
208
286
|
catch {
|
|
209
287
|
// noop, likely JSON parse error
|
|
210
288
|
}
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
289
|
+
if (!showedErrors) {
|
|
290
|
+
if (jsonError && jsonError.error) {
|
|
291
|
+
this.showErrors(jsonError, spinnerError);
|
|
292
|
+
}
|
|
293
|
+
else if (typeof error.message === 'string') {
|
|
294
|
+
this.showErrors(error, spinnerError);
|
|
295
|
+
}
|
|
218
296
|
}
|
|
219
297
|
throw error;
|
|
220
298
|
}
|
|
@@ -222,63 +300,262 @@ export class RemoteAppCommand extends AppCommand {
|
|
|
222
300
|
async init() {
|
|
223
301
|
await super.init();
|
|
224
302
|
this.app = this.initApp();
|
|
303
|
+
this.storefront = null;
|
|
304
|
+
const klass = this.ctor;
|
|
305
|
+
if (klass.orientation && !klass.delayOrientation) {
|
|
306
|
+
await this.logOrientation();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async installApp(version, spinner) {
|
|
310
|
+
return this.api.post({ adminPath: `/client/apps` }, {
|
|
311
|
+
body: { app_id: this.app.id, version },
|
|
312
|
+
onAsyncGetPath: (response) => ({
|
|
313
|
+
adminPath: `/client/app-async-status/${response.id}`,
|
|
314
|
+
}),
|
|
315
|
+
spinner,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
isConfigDeprecated(config) {
|
|
319
|
+
return !config.file_path;
|
|
320
|
+
}
|
|
321
|
+
localFrontendUrl(storeId, sessionId) {
|
|
322
|
+
return getLocalFrontendHost(storeId, sessionId);
|
|
323
|
+
}
|
|
324
|
+
async logOrientation() {
|
|
225
325
|
const klass = this.ctor;
|
|
226
326
|
const { flags } = await this.parse(klass);
|
|
227
327
|
if (klass.orientation) {
|
|
228
328
|
this.showOrientation(klass.orientation, flags);
|
|
229
329
|
}
|
|
230
330
|
}
|
|
231
|
-
async
|
|
232
|
-
|
|
233
|
-
|
|
331
|
+
async postConfigData(configJson) {
|
|
332
|
+
const themeId = this.app.type === 'theme' && this.storefront?.theme_id;
|
|
333
|
+
return this.api.post({ adminPath: `/apps/${this.app.id}/configs` }, {
|
|
334
|
+
body: {
|
|
335
|
+
...configJson,
|
|
336
|
+
...(themeId ? { theme_id: themeId } : undefined),
|
|
337
|
+
...(this.themeSyncApp ? { $sync_app: true } : undefined),
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async pullAppConfigs(force, confirmRemove = true) {
|
|
342
|
+
const pulled = [];
|
|
343
|
+
const removed = [];
|
|
344
|
+
const localConfigs = this.getLocalAppConfigs();
|
|
345
|
+
const remoteConfigs = await this.cleanRemoteConfigs(await this.pullAppJsonAndRemoteConfigs(), false);
|
|
346
|
+
const batches = batchAppFilesByType(remoteConfigs);
|
|
347
|
+
for (const batch of batches) {
|
|
348
|
+
const remoteBatchConfigs = remoteConfigs.filter((config) => config.type === batch.type);
|
|
349
|
+
const localBatchConfigs = localConfigs.filter((config) => config.type === batch.type);
|
|
350
|
+
const pullFiles = batch.configs.filter((batchConfig) => {
|
|
351
|
+
if (force)
|
|
352
|
+
return true;
|
|
353
|
+
try {
|
|
354
|
+
const localConfig = localBatchConfigs.find((config) => config.filePath === batchConfig.filePath);
|
|
355
|
+
if (localConfig) {
|
|
356
|
+
const hash = hashFile(batchConfig.appPath);
|
|
357
|
+
if (hash === batchConfig.hash) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch { }
|
|
363
|
+
return true;
|
|
364
|
+
});
|
|
365
|
+
const filesToRemove = localBatchConfigs.filter((config) => !batch.configs.some((batchConfig) => batchConfig.filePath === config.filePath));
|
|
366
|
+
// Skip if there are no config changes
|
|
367
|
+
if (pullFiles.length === 0 && filesToRemove.length === 0) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const updatedLog = pullFiles.length > 0
|
|
371
|
+
? `(${pullFiles.length}/${remoteBatchConfigs.length})`
|
|
372
|
+
: '';
|
|
373
|
+
if (pullFiles.length > 0 || filesToRemove.length > 0) {
|
|
374
|
+
this.log(`${style.appConfigName(`${batch.label} ${updatedLog}`)}`);
|
|
375
|
+
}
|
|
376
|
+
if (pullFiles.length > 0) {
|
|
377
|
+
while (pullFiles.length > 0) {
|
|
378
|
+
// eslint-disable-next-line no-await-in-loop
|
|
379
|
+
await Promise.all(pullFiles
|
|
380
|
+
.splice(0, PUSH_CONCURRENCY)
|
|
381
|
+
.map(async (config) => {
|
|
382
|
+
await this.pullRemoteFile(config);
|
|
383
|
+
pulled.push(config.id);
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Remove any deleted files
|
|
388
|
+
if (filesToRemove.length > 0) {
|
|
389
|
+
if (confirmRemove && !force) {
|
|
390
|
+
const removeList = filesToRemove
|
|
391
|
+
.map((config) => config.filePath)
|
|
392
|
+
.join('\n');
|
|
393
|
+
// eslint-disable-next-line no-await-in-loop
|
|
394
|
+
const confirmed = await confirm({
|
|
395
|
+
default: false,
|
|
396
|
+
message: `The following files are not in the remote app:\n\n${removeList}\n\nDo you want to delete these files locally?`,
|
|
397
|
+
});
|
|
398
|
+
if (!confirmed) {
|
|
399
|
+
filesToRemove.splice(0);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
while (filesToRemove.length > 0) {
|
|
403
|
+
// eslint-disable-next-line no-await-in-loop
|
|
404
|
+
await Promise.all(filesToRemove.splice(0, PUSH_CONCURRENCY).map(async (config) => {
|
|
405
|
+
await this.removeLocalFile(config);
|
|
406
|
+
removed.push(config.id);
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
this.log();
|
|
411
|
+
}
|
|
412
|
+
if (pulled.length === 0 && removed.length === 0) {
|
|
413
|
+
this.log(`${style.success('✔')} All files up to date.\n`);
|
|
414
|
+
}
|
|
415
|
+
return { pulled, removed };
|
|
416
|
+
}
|
|
417
|
+
async pullAppJsonAndRemoteConfigs() {
|
|
418
|
+
const configs = this.app.configs || [];
|
|
419
|
+
const allOtherConfigs = sortBy(configs.filter((config) => config.filePath !== 'swell.json'), 'filePath');
|
|
420
|
+
const swellJson = configs.find((config) => config.filePath === 'swell.json');
|
|
421
|
+
if (swellJson) {
|
|
422
|
+
await this.pullRemoteFile(swellJson, false);
|
|
234
423
|
}
|
|
235
|
-
|
|
236
|
-
|
|
424
|
+
else {
|
|
425
|
+
const swellConfigJson = {};
|
|
426
|
+
for (const key of Object.keys(SwellJsonFields)) {
|
|
427
|
+
const configKey = SwellJsonFields[key];
|
|
428
|
+
swellConfigJson[configKey] = this.app[configKey];
|
|
429
|
+
}
|
|
430
|
+
swellConfigJson.id = toAppId(this.app.private_id);
|
|
431
|
+
await writeJsonFile(path.join(this.appPath, 'swell.json'), swellConfigJson);
|
|
432
|
+
}
|
|
433
|
+
const swellConfig = appConfigFromFile('swell.json', 'file', this.appPath);
|
|
434
|
+
swellConfig.id = swellJson?.id;
|
|
435
|
+
return [swellConfig, ...allOtherConfigs];
|
|
237
436
|
}
|
|
238
|
-
async
|
|
239
|
-
const { localPath } = appConfig;
|
|
437
|
+
async pullRemoteFile(config, log = true) {
|
|
240
438
|
const spinner = ora();
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
439
|
+
log && spinner.start(`Pulling ${config.filePath}`);
|
|
440
|
+
try {
|
|
441
|
+
const result = await this.handleRequestErrors(async () => this.getConfigData(config.id), () => log && spinner.fail(`Error pulling ${config.filePath}`));
|
|
442
|
+
await writeFile(config.appPath, result);
|
|
443
|
+
log && spinner.succeed(`${this.timestampStyled()} ${config.filePath}`);
|
|
444
|
+
return result;
|
|
244
445
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
446
|
+
catch (error) {
|
|
447
|
+
log && spinner.fail(error.message);
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async pushAppConfigs(force) {
|
|
452
|
+
const pushed = [];
|
|
453
|
+
const removed = [];
|
|
454
|
+
const localConfigs = this.getLocalAppConfigs();
|
|
455
|
+
const remoteConfigs = await this.cleanRemoteConfigs(this.app?.configs || []);
|
|
456
|
+
// Do not allow more than 10K files
|
|
457
|
+
if (localConfigs.length > 10000) {
|
|
458
|
+
this.error(`Apps only support up to 10,000 files. Found ${localConfigs.length} files.`);
|
|
459
|
+
}
|
|
460
|
+
const batches = batchAppFilesByType(localConfigs);
|
|
461
|
+
for (const batch of batches) {
|
|
462
|
+
const remoteBatchConfigs = remoteConfigs.filter((config) => config.type === batch.type);
|
|
463
|
+
const pushFiles = batch.configs.filter((batchConfig) => {
|
|
464
|
+
const remoteConfig = remoteBatchConfigs.find((config) => batchConfig.filePath === config.filePath);
|
|
465
|
+
return Boolean(force || !remoteConfig || remoteConfig.hash !== batchConfig.hash);
|
|
466
|
+
});
|
|
467
|
+
const filesToRemove = remoteBatchConfigs.filter((config) => !batch.configs.some((batchConfig) => batchConfig.filePath === config.filePath));
|
|
468
|
+
// Skip if there are no config changes
|
|
469
|
+
if (pushFiles.length === 0 && filesToRemove.length === 0) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const updatedLog = pushFiles.length > 0
|
|
473
|
+
? remoteBatchConfigs.length > 0
|
|
474
|
+
? `(${pushFiles.length}/${batch.configs.length})`
|
|
475
|
+
: `(${pushFiles.length})`
|
|
476
|
+
: '';
|
|
477
|
+
if (pushFiles.length > 0 || filesToRemove.length > 0) {
|
|
478
|
+
this.log(`${style.appConfigName(`${batch.label} ${updatedLog}`)}`);
|
|
479
|
+
}
|
|
480
|
+
if (pushFiles.length > 0) {
|
|
481
|
+
const pushConcurrency = batch.type === 'ASSET' ? 1 : PUSH_CONCURRENCY;
|
|
482
|
+
while (pushFiles.length > 0) {
|
|
483
|
+
// eslint-disable-next-line no-await-in-loop
|
|
484
|
+
await Promise.all(pushFiles
|
|
485
|
+
.splice(0, pushConcurrency)
|
|
486
|
+
.map(async (config) => {
|
|
487
|
+
const pushedConfig = await this.pushRemoteFile(config);
|
|
488
|
+
if (pushedConfig) {
|
|
489
|
+
pushed.push(pushedConfig.id);
|
|
490
|
+
if (config.filePath === 'swell.json') {
|
|
491
|
+
this.app = await this.getApp(this.app.id);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Remove any deleted files
|
|
498
|
+
if (filesToRemove.length > 0) {
|
|
499
|
+
while (filesToRemove.length > 0) {
|
|
500
|
+
// eslint-disable-next-line no-await-in-loop
|
|
501
|
+
await Promise.all(filesToRemove.splice(0, PUSH_CONCURRENCY).map(async (config) => {
|
|
502
|
+
await this.removeRemoteFile(config);
|
|
503
|
+
removed.push(config.id);
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
this.log();
|
|
249
508
|
}
|
|
250
|
-
|
|
251
|
-
|
|
509
|
+
if (pushed.length === 0 && removed.length === 0) {
|
|
510
|
+
this.log(`${style.success('✔')} All files up to date.\n`);
|
|
511
|
+
}
|
|
512
|
+
return { pushed, removed };
|
|
513
|
+
}
|
|
514
|
+
async pushRemoteFile(config, log = true) {
|
|
515
|
+
const spinner = ora();
|
|
516
|
+
log && spinner.start(`Pushing ${config.filePath}`);
|
|
252
517
|
try {
|
|
253
|
-
postData = await
|
|
518
|
+
const postData = await config.postData();
|
|
254
519
|
if (!postData) {
|
|
255
520
|
spinner.stop();
|
|
256
521
|
return;
|
|
257
522
|
}
|
|
523
|
+
const result = await this.handleRequestErrors(async () => this.postConfigData(postData), () => log && spinner.fail(`Error pushing ${config.filePath}`));
|
|
524
|
+
log && spinner.succeed(`${this.timestampStyled()} ${config.filePath}`);
|
|
525
|
+
return result;
|
|
258
526
|
}
|
|
259
527
|
catch (error) {
|
|
260
528
|
if (error instanceof IgnoringFileError) {
|
|
261
|
-
|
|
529
|
+
log &&
|
|
530
|
+
spinner.fail(`Ignoring file: ${error.message} in ${config.filePath}`);
|
|
262
531
|
return;
|
|
263
532
|
}
|
|
264
533
|
if (error instanceof FunctionProcessingError) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
spinner.fail(`${error.message} ${error.originalError?.message?.toLowerCase()}`);
|
|
534
|
+
log &&
|
|
535
|
+
spinner.fail(`${error.message} ${error.originalError?.message?.toLowerCase()}`);
|
|
268
536
|
return;
|
|
269
537
|
}
|
|
270
|
-
spinner.fail(error.message);
|
|
538
|
+
log && spinner.fail(error.message);
|
|
271
539
|
throw error;
|
|
272
540
|
}
|
|
273
|
-
|
|
274
|
-
|
|
541
|
+
}
|
|
542
|
+
async removeLocalFile(config, log = true) {
|
|
543
|
+
if (!config.fileData) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const spinner = ora();
|
|
547
|
+
log && spinner.start(`Removing ${config.filePath}`);
|
|
548
|
+
const result = await this.handleRequestErrors(async () => deleteFile(config.appPath), () => log && spinner.fail(`Error removing ${config.filePath}`));
|
|
549
|
+
log &&
|
|
550
|
+
spinner.warn(`${this.timestampStyled()} ${style.strike(config.filePath)}`);
|
|
275
551
|
return result;
|
|
276
552
|
}
|
|
277
|
-
async
|
|
553
|
+
async removeRemoteFile(config, log = true) {
|
|
278
554
|
const spinner = ora();
|
|
279
|
-
spinner.start(`Removing ${config.
|
|
280
|
-
const result = await this.handleRequestErrors(async () => this.deleteConfig(config.id), () => spinner.fail(`Error removing ${config.
|
|
281
|
-
|
|
555
|
+
log && spinner.start(`Removing ${config.filePath}`);
|
|
556
|
+
const result = await this.handleRequestErrors(async () => this.deleteConfig(config.id, config.filePath, config.appConfigId), () => log && spinner.fail(`Error removing ${config.filePath}`));
|
|
557
|
+
log &&
|
|
558
|
+
spinner.warn(`${this.timestampStyled()} ${style.strike(config.filePath)}`);
|
|
282
559
|
return result;
|
|
283
560
|
}
|
|
284
561
|
showErrors(errors, spinnerError) {
|
|
@@ -290,6 +567,8 @@ export class RemoteAppCommand extends AppCommand {
|
|
|
290
567
|
this.logError(inflect('Error', actualErrors.length));
|
|
291
568
|
}
|
|
292
569
|
for (const err of actualErrors) {
|
|
570
|
+
if (!errors[err]?.message)
|
|
571
|
+
continue;
|
|
293
572
|
// we need to display `id` instead of `private_id`
|
|
294
573
|
// developers don't have any knowledge of private ids
|
|
295
574
|
const displayErr = err === 'private_id' ? 'id' : err;
|
|
@@ -302,48 +581,30 @@ export class RemoteAppCommand extends AppCommand {
|
|
|
302
581
|
.join(' '));
|
|
303
582
|
}
|
|
304
583
|
}
|
|
584
|
+
storefrontFrontendUrl(storeId, storefront, branchId, preview = true) {
|
|
585
|
+
const storefrontSlug = storefront && preview
|
|
586
|
+
? `${storeId}--${storefront.id.toLowerCase()}`
|
|
587
|
+
: storeId;
|
|
588
|
+
return getStorefrontFrontendHost(storefrontSlug, branchId);
|
|
589
|
+
}
|
|
305
590
|
swellRcPath() {
|
|
306
591
|
return path.join(this.appPath, '.swellrc');
|
|
307
592
|
}
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
for (const appConfig of batch.configs) {
|
|
324
|
-
// eslint-disable-next-line no-await-in-loop
|
|
325
|
-
const installedConfig = await this.pushConfigFile(appConfig);
|
|
326
|
-
if (installedConfig)
|
|
327
|
-
installed.push(installedConfig.id);
|
|
328
|
-
}
|
|
329
|
-
this.log();
|
|
330
|
-
}
|
|
331
|
-
// pull the app again after the latest push
|
|
332
|
-
// this allows us to get the latest configs updates that the remote app
|
|
333
|
-
// might have done
|
|
334
|
-
this.app = await this.getAppWithConfig(this.app.id);
|
|
335
|
-
this.debugJson('syncAppConfigs: app', this.app);
|
|
336
|
-
// if the remote app has configs that are not in the local app, we want to
|
|
337
|
-
// remove them
|
|
338
|
-
for (const config of this.app?.configs || []) {
|
|
339
|
-
const appConfig = config;
|
|
340
|
-
if (!appConfig.localPath) {
|
|
341
|
-
// eslint-disable-next-line no-await-in-loop
|
|
342
|
-
await this.removeConfig(appConfig);
|
|
343
|
-
removed.push(appConfig.id);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return { installed, removed };
|
|
593
|
+
timestampStyled() {
|
|
594
|
+
const date = new Date();
|
|
595
|
+
return style.dim(`[${date.toLocaleTimeString()}]`);
|
|
596
|
+
}
|
|
597
|
+
async updateInstalledApp(version, spinner) {
|
|
598
|
+
await this.api.put({ adminPath: `/client/apps/${this.app.id}` }, {
|
|
599
|
+
body: { version },
|
|
600
|
+
onAsyncGetPath: (response) => ({
|
|
601
|
+
adminPath: `/client/app-async-status/${response.id}`,
|
|
602
|
+
}),
|
|
603
|
+
spinner,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
async updateVersion(version, body, spinner) {
|
|
607
|
+
await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${this.app.id}/versions` }, { body: { version, ...body } }), () => spinner?.fail('Error updating version.'));
|
|
347
608
|
}
|
|
348
609
|
/**
|
|
349
610
|
* Reads the .swellrc file and sets up the remote app configuration.
|
|
@@ -351,30 +612,27 @@ export class RemoteAppCommand extends AppCommand {
|
|
|
351
612
|
* @returns App
|
|
352
613
|
*/
|
|
353
614
|
initApp() {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
catch (error) {
|
|
361
|
-
// if the file doesn't exist, we don't want to throw an error as we might
|
|
362
|
-
// want to create it later.
|
|
363
|
-
if (error.code !== 'ENOENT') {
|
|
364
|
-
throw error;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
if (this.appConfig.get('id')) {
|
|
615
|
+
const app = this.swellConfig.store || {};
|
|
616
|
+
// id refers to remote app id, so it's undefined initially
|
|
617
|
+
app.id = undefined;
|
|
618
|
+
// swell.json id refers to private id
|
|
619
|
+
if (this.swellConfig.get('id')) {
|
|
368
620
|
// use the same formula as the backend api
|
|
369
|
-
|
|
370
|
-
|
|
621
|
+
app.private_id = toAppId(this.swellConfig.get('id'));
|
|
622
|
+
}
|
|
623
|
+
// Default name to private id
|
|
624
|
+
if (!app.name) {
|
|
625
|
+
app.name = app.private_id || app.id;
|
|
371
626
|
}
|
|
372
|
-
return
|
|
627
|
+
return app;
|
|
373
628
|
}
|
|
374
629
|
showOrientation(orientation, flags) {
|
|
375
630
|
const orientationOutput = [];
|
|
376
|
-
const currentStore =
|
|
631
|
+
const currentStore = localConfig.getDefaultStore();
|
|
377
632
|
const { env, store } = flags;
|
|
633
|
+
orientationOutput.push(`[${this.app.type === 'theme' ? 'theme' : 'app'}] ${style.appName(this.app.name)}${this.app.version ? ` v${this.app.version}` : ''}${this.storefront && this.storefront.name !== this.app.name
|
|
634
|
+
? ` [storefront] ${style.appName(this.storefront.name)}`
|
|
635
|
+
: ``}`);
|
|
378
636
|
if (store) {
|
|
379
637
|
orientationOutput.push(`[store] ${style.storeId(store)}`);
|
|
380
638
|
}
|