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