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