@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.
Files changed (86) hide show
  1. package/dist/app-command.d.ts +1 -1
  2. package/dist/app-command.js +9 -9
  3. package/dist/commands/app/dev.d.ts +16 -0
  4. package/dist/commands/app/dev.js +28 -0
  5. package/dist/commands/app/frontend/deploy.d.ts +15 -0
  6. package/dist/commands/app/frontend/deploy.js +41 -0
  7. package/dist/commands/app/frontend/dev.d.ts +19 -0
  8. package/dist/commands/app/frontend/dev.js +108 -0
  9. package/dist/commands/app/info.js +14 -9
  10. package/dist/commands/app/init.d.ts +12 -0
  11. package/dist/commands/app/init.js +34 -0
  12. package/dist/commands/app/install.d.ts +0 -3
  13. package/dist/commands/app/install.js +12 -21
  14. package/dist/commands/app/pull.d.ts +15 -0
  15. package/dist/commands/app/pull.js +50 -0
  16. package/dist/commands/app/push.d.ts +9 -12
  17. package/dist/commands/app/push.js +57 -131
  18. package/dist/commands/app/release.d.ts +13 -0
  19. package/dist/commands/app/release.js +148 -0
  20. package/dist/commands/app/version.d.ts +5 -4
  21. package/dist/commands/app/version.js +60 -39
  22. package/dist/commands/app/watch.d.ts +2 -4
  23. package/dist/commands/app/watch.js +3 -83
  24. package/dist/commands/create/app.d.ts +35 -11
  25. package/dist/commands/create/app.js +131 -128
  26. package/dist/commands/create/frontend.d.ts +12 -0
  27. package/dist/commands/create/frontend.js +38 -0
  28. package/dist/commands/create/function.d.ts +2 -2
  29. package/dist/commands/create/function.js +2 -2
  30. package/dist/commands/create/notification.d.ts +2 -2
  31. package/dist/commands/create/notification.js +2 -2
  32. package/dist/commands/env.d.ts +12 -0
  33. package/dist/commands/env.js +31 -0
  34. package/dist/commands/login.js +7 -7
  35. package/dist/commands/logs.js +0 -3
  36. package/dist/commands/theme/dev.d.ts +21 -0
  37. package/dist/commands/theme/dev.js +164 -0
  38. package/dist/commands/theme/init.d.ts +9 -0
  39. package/dist/commands/theme/init.js +15 -0
  40. package/dist/commands/theme/pull.d.ts +20 -0
  41. package/dist/commands/theme/pull.js +65 -0
  42. package/dist/commands/theme/push.d.ts +19 -0
  43. package/dist/commands/theme/push.js +89 -0
  44. package/dist/create-app-command.d.ts +20 -0
  45. package/dist/create-app-command.js +253 -0
  46. package/dist/create-collection-command.d.ts +3 -3
  47. package/dist/create-collection-command.js +4 -4
  48. package/dist/{create-command.d.ts → create-config-command.d.ts} +1 -1
  49. package/dist/{create-command.js → create-config-command.js} +3 -5
  50. package/dist/env/local.d.ts +8 -0
  51. package/dist/env/local.js +7 -0
  52. package/dist/env/production.d.ts +8 -0
  53. package/dist/env/production.js +7 -0
  54. package/dist/env/review.d.ts +8 -0
  55. package/dist/env/review.js +7 -0
  56. package/dist/env/staging.d.ts +8 -0
  57. package/dist/env/staging.js +7 -0
  58. package/dist/lib/api.d.ts +1 -0
  59. package/dist/lib/api.js +39 -13
  60. package/dist/lib/app-config.d.ts +12 -5
  61. package/dist/lib/app-config.js +57 -21
  62. package/dist/lib/apps/app-config.d.ts +17 -3
  63. package/dist/lib/apps/app-config.js +194 -66
  64. package/dist/lib/apps/index.d.ts +92 -12
  65. package/dist/lib/apps/index.js +145 -47
  66. package/dist/lib/apps/paths.d.ts +14 -7
  67. package/dist/lib/apps/paths.js +73 -28
  68. package/dist/lib/config.d.ts +9 -2
  69. package/dist/lib/config.js +49 -1
  70. package/dist/lib/constants.d.ts +5 -0
  71. package/dist/lib/constants.js +23 -4
  72. package/dist/lib/create/content.d.ts +1 -1
  73. package/dist/lib/create/index.js +5 -1
  74. package/dist/lib/info.js +2 -2
  75. package/dist/lib/stores.d.ts +5 -3
  76. package/dist/lib/stores.js +7 -5
  77. package/dist/lib/style.d.ts +2 -0
  78. package/dist/lib/style.js +2 -0
  79. package/dist/push-app-command.d.ts +73 -0
  80. package/dist/push-app-command.js +754 -0
  81. package/dist/remote-app-command.d.ts +35 -9
  82. package/dist/remote-app-command.js +417 -159
  83. package/oclif.manifest.json +817 -71
  84. package/package.json +13 -1
  85. package/dist/commands/app/_pull.d.ts +0 -8
  86. 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 * as fs from 'node:fs';
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 { appConfigFileExists, readRcFile, writeRcFile, } from './lib/app-config.js';
7
- import { FunctionProcessingError, IgnoringFileError, allConfigFilesPaths, appConfigFromFile, appLogoIcon, batchAppConfigsByType, configFilePath, isAppLogoIcon, } from './lib/apps/index.js';
8
- import { default as swellConfig } from './lib/config.js';
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
- // an API instance of the current app definition
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
- if (!this.app) {
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
- // Get app configs for processing
39
- app.configs = [];
40
- const configsResponse = await this.api.get({ adminPath: `/apps/${id}/configs` }, { query: { limit: 1000 } });
41
- this.debugJson('getApp: configsResponse', configsResponse);
42
- // for each config, we check if there's a local file and we add it to the
43
- // AppConfig object
44
- for (const config of configsResponse.results || []) {
45
- const localPath = configFilePath(this.appPath, config);
46
- if (appConfigFileExists(localPath)) {
47
- config.localPath = localPath;
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 appConfigId = this.appConfig.get('id');
57
- if (!appConfigId) {
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 !== appConfigId) {
67
- this.error(`App ${style.appConfigName('id')} must be declared in snake_case: ${style.appConfigValue(appConfigId)}.`);
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 remoteApp;
171
+ let sourceApp;
109
172
  try {
110
- remoteApp = await readRcFile(this.swellRcPath());
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
- /* eslint-disable camelcase */
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.appConfig.store,
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
- logo_icon: null,
124
- ...(this.appConfig.store.id
125
- ? { private_id: this.appConfig.store.id }
188
+ ...(this.swellConfig.store.id
189
+ ? { private_id: this.swellConfig.store.id }
126
190
  : undefined),
127
- ...(remoteApp?.id ? { source_id: remoteApp.id } : undefined),
191
+ ...(sourceApp?.id ? { source_id: sourceApp.id } : undefined),
128
192
  };
129
- const logoIcon = appLogoIcon(this.appPath);
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.appConfig.store.id}`,
200
+ adminPath: `/apps/${this.swellConfig.store.id}`,
143
201
  });
144
202
  }
145
203
  catch {
146
204
  // noop
147
205
  }
148
- spinner.start(`Creating app...`);
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 creating app'));
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 app...`);
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
- await writeRcFile(this.swellRcPath(), { id: app.id });
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('Development app created.');
167
- this.log();
225
+ if (this.appCreated) {
226
+ spinner.succeed(`Development ${appLabel} created.`);
168
227
  }
169
228
  else {
170
- spinner.stop();
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.log();
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
- this.error('API request failed');
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 (jsonError && jsonError.error) {
212
- this.showErrors(jsonError, spinnerError);
213
- this.error('API request failed');
214
- }
215
- else if (typeof error.message === 'string') {
216
- this.showErrors(error, spinnerError);
217
- this.error(error.message);
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 postConfig(configJson) {
232
- if (!this.app) {
233
- this.error('App is not defined');
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
- this.debug('postConfig: configJson', configJson);
236
- return this.api.post({ adminPath: `/apps/${this.app.id}/configs` }, { body: configJson });
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 pushConfigFile(appConfig) {
239
- const { localPath } = appConfig;
437
+ async pullRemoteFile(config, log = true) {
240
438
  const spinner = ora();
241
- let postData;
242
- if (!localPath) {
243
- throw new Error('Missing local config path');
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
- // we don't want to treat the app logo asset as config file
246
- // it is dealt with at the app level
247
- if (isAppLogoIcon(localPath)) {
248
- return;
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
- const pathDesc = localPath.split('/').slice(-2).join('/');
251
- spinner.start(`Pushing ${pathDesc}`);
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 appConfig.postData();
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
- spinner.fail(`Ignoring file: ${error.message} in ${localPath}`);
529
+ log &&
530
+ spinner.fail(`Ignoring file: ${error.message} in ${config.filePath}`);
262
531
  return;
263
532
  }
264
533
  if (error instanceof FunctionProcessingError) {
265
- this.debug(error);
266
- this.debug(error.originalError);
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
- const result = await this.handleRequestErrors(async () => this.postConfig(postData), () => spinner.fail(`Error pushing ${pathDesc}`));
274
- spinner.succeed(`Pushed ${pathDesc}`);
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 removeConfig(config) {
553
+ async removeRemoteFile(config, log = true) {
278
554
  const spinner = ora();
279
- spinner.start(`Removing ${config.type} ${config.name}`);
280
- const result = await this.handleRequestErrors(async () => this.deleteConfig(config.id), () => spinner.fail(`Error removing ${config.type} ${config.name}`));
281
- spinner.warn(`Removed ${config.type} ${config.name}`);
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
- async syncAppConfigs() {
309
- const configsToPush = [];
310
- const installed = [];
311
- const removed = [];
312
- for (const { configFile, configType } of allConfigFilesPaths(this.appPath)) {
313
- // skip logo icon as it is dealt with at the app level
314
- if (isAppLogoIcon(configFile)) {
315
- continue;
316
- }
317
- const appConfig = appConfigFromFile(configFile, configType);
318
- configsToPush.push(appConfig);
319
- }
320
- const batches = batchAppConfigsByType(configsToPush);
321
- for (const batch of batches) {
322
- this.log(style.appConfigName(batch.label));
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
- let remoteApp = {};
355
- try {
356
- this.debug('initApp: swellRcPath', this.swellRcPath());
357
- remoteApp = readRcFile(this.swellRcPath());
358
- this.debug('initApp: remoteApp', remoteApp);
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
- // eslint-disable-next-line camelcase
370
- remoteApp.private_id = toAppId(this.appConfig.get('id'));
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 remoteApp;
627
+ return app;
373
628
  }
374
629
  showOrientation(orientation, flags) {
375
630
  const orientationOutput = [];
376
- const currentStore = swellConfig.getDefaultStore();
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
  }