@swell/cli 2.0.1-alpha.17 → 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 (67) hide show
  1. package/dist/app-command.d.ts +1 -1
  2. package/dist/app-command.js +3 -3
  3. package/dist/commands/app/dev.d.ts +3 -5
  4. package/dist/commands/app/dev.js +3 -6
  5. package/dist/commands/app/frontend/deploy.d.ts +1 -3
  6. package/dist/commands/app/frontend/deploy.js +2 -5
  7. package/dist/commands/app/frontend/dev.d.ts +4 -6
  8. package/dist/commands/app/frontend/dev.js +12 -15
  9. package/dist/commands/app/info.js +1 -3
  10. package/dist/commands/app/init.js +2 -2
  11. package/dist/commands/app/install.js +0 -1
  12. package/dist/commands/app/pull.d.ts +3 -3
  13. package/dist/commands/app/pull.js +16 -16
  14. package/dist/commands/app/push.d.ts +4 -4
  15. package/dist/commands/app/push.js +28 -31
  16. package/dist/commands/app/release.d.ts +3 -3
  17. package/dist/commands/app/release.js +43 -45
  18. package/dist/commands/app/version.d.ts +4 -4
  19. package/dist/commands/app/version.js +28 -28
  20. package/dist/commands/create/app.d.ts +15 -15
  21. package/dist/commands/create/app.js +66 -66
  22. package/dist/commands/create/frontend.js +4 -7
  23. package/dist/commands/env.d.ts +12 -0
  24. package/dist/commands/env.js +31 -0
  25. package/dist/commands/login.js +1 -3
  26. package/dist/commands/logs.js +0 -2
  27. package/dist/commands/theme/dev.d.ts +6 -8
  28. package/dist/commands/theme/dev.js +30 -33
  29. package/dist/commands/theme/init.d.ts +1 -1
  30. package/dist/commands/theme/init.js +1 -1
  31. package/dist/commands/theme/pull.d.ts +7 -7
  32. package/dist/commands/theme/pull.js +21 -21
  33. package/dist/commands/theme/push.d.ts +7 -7
  34. package/dist/commands/theme/push.js +24 -27
  35. package/dist/create-app-command.d.ts +7 -7
  36. package/dist/create-app-command.js +146 -147
  37. package/dist/create-config-command.js +0 -2
  38. package/dist/env/local.d.ts +8 -0
  39. package/dist/env/local.js +7 -0
  40. package/dist/env/production.d.ts +8 -0
  41. package/dist/env/production.js +7 -0
  42. package/dist/env/review.d.ts +8 -0
  43. package/dist/env/review.js +7 -0
  44. package/dist/env/staging.d.ts +8 -0
  45. package/dist/env/staging.js +7 -0
  46. package/dist/lib/api.d.ts +3 -3
  47. package/dist/lib/api.js +35 -35
  48. package/dist/lib/app-config.d.ts +2 -2
  49. package/dist/lib/app-config.js +12 -11
  50. package/dist/lib/apps/app-config.d.ts +19 -19
  51. package/dist/lib/apps/app-config.js +59 -65
  52. package/dist/lib/apps/index.d.ts +56 -56
  53. package/dist/lib/apps/index.js +40 -42
  54. package/dist/lib/apps/paths.d.ts +1 -1
  55. package/dist/lib/apps/paths.js +4 -4
  56. package/dist/lib/config.d.ts +4 -1
  57. package/dist/lib/config.js +35 -1
  58. package/dist/lib/constants.js +7 -8
  59. package/dist/lib/create/index.js +2 -2
  60. package/dist/lib/stores.d.ts +4 -2
  61. package/dist/lib/stores.js +4 -2
  62. package/dist/push-app-command.d.ts +34 -34
  63. package/dist/push-app-command.js +391 -402
  64. package/dist/remote-app-command.d.ts +24 -24
  65. package/dist/remote-app-command.js +245 -259
  66. package/oclif.manifest.json +152 -119
  67. package/package.json +2 -1
@@ -1,13 +1,13 @@
1
+ import { confirm } from '@inquirer/prompts';
1
2
  import { inflect } from 'inflection';
3
+ import sortBy from 'lodash/sortBy.js';
2
4
  import * as path from 'node:path';
3
5
  import { default as ora } from 'ora';
4
- import sortBy from 'lodash/sortBy.js';
5
- import { confirm } from '@inquirer/prompts';
6
6
  import { AppCommand } from './app-command.js';
7
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';
8
+ import { AppConfig, FunctionProcessingError, IgnoringFileError, SwellJsonFields, allBaseFilesInDir, allConfigFilesPaths, appConfigFromFile, batchAppFilesByType, deleteFile, hashFile, writeFile, writeJsonFile, } from './lib/apps/index.js';
9
9
  import { default as localConfig } from './lib/config.js';
10
- import { getLoginHost, getAppFrontendHost, getStorefrontFrontendHost, getLocalFrontendHost, } from './lib/constants.js';
10
+ import { getAppFrontendHost, getLocalFrontendHost, getLoginHost, getStorefrontFrontendHost, } from './lib/constants.js';
11
11
  import { toAppId } from './lib/create/index.js';
12
12
  import style from './lib/style.js';
13
13
  const PUSH_CONCURRENCY = 3;
@@ -17,37 +17,51 @@ const PUSH_CONCURRENCY = 3;
17
17
  */
18
18
  export class RemoteAppCommand extends AppCommand {
19
19
  // allows child commands to set orientation
20
+ static delayOrientation = false;
20
21
  // most of the times this will be the target environment
21
22
  static orientation;
22
- static delayOrientation = false;
23
- appType = 'any';
24
23
  // API instance of the current app definition
25
24
  app = {};
26
25
  // Indicates the app was created during the command execution
27
26
  appCreated = false;
27
+ appType = 'any';
28
28
  // API instance of the target storefront, if applicable
29
29
  storefront = null;
30
30
  // Indicates app configs are being synced along with theme configs
31
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
+ }
32
47
  dashboardUrl(flags = {}, storeId) {
33
48
  const { env } = flags;
34
49
  const envFlag = env || this.ctor.orientation?.env;
35
50
  const outputEnv = envFlag === 'live' ? '' : envFlag ? 'test' : '';
36
51
  return `${getLoginHost(storeId)}/admin/${outputEnv ? `${outputEnv}/` : ''}apps/${this.app.id}`;
37
52
  }
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);
53
+ async deleteConfig(configId, filePath, appConfigId) {
54
+ const themeId = this.app.type === 'theme' && this.storefront?.theme_id;
55
+ return this.api.delete({
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
+ },
64
+ });
51
65
  }
52
66
  async getApp(id = '') {
53
67
  const app = await this.api.get({ adminPath: `/apps/${id}` });
@@ -78,8 +92,8 @@ export class RemoteAppCommand extends AppCommand {
78
92
  else if (!themeConfig && appConfig) {
79
93
  app.configs.push(AppConfig.create({
80
94
  ...appConfig,
81
- appPath: this.appPath,
82
95
  appConfigId: appConfig.id,
96
+ appPath: this.appPath,
83
97
  id: undefined,
84
98
  }));
85
99
  }
@@ -146,6 +160,12 @@ export class RemoteAppCommand extends AppCommand {
146
160
  }
147
161
  return app;
148
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
+ }
149
169
  async getCreateUpdateApp(updateApp) {
150
170
  const spinner = ora();
151
171
  let sourceApp;
@@ -160,7 +180,6 @@ export class RemoteAppCommand extends AppCommand {
160
180
  if (!this.swellConfig.store.id) {
161
181
  throw new Error('App swell.json is missing or `id` is not defined.');
162
182
  }
163
- /* eslint-disable camelcase */
164
183
  const body = {
165
184
  ...this.swellConfig.store,
166
185
  // we store locally the private id as the id, but we need to switch it
@@ -172,7 +191,6 @@ export class RemoteAppCommand extends AppCommand {
172
191
  ...(sourceApp?.id ? { source_id: sourceApp.id } : undefined),
173
192
  };
174
193
  const appLabel = this.app.type === 'theme' ? 'theme' : 'app';
175
- /* eslint-enable camelcase */
176
194
  let app = updateApp || {};
177
195
  if (!app.id) {
178
196
  // May already have a dev instance
@@ -220,45 +238,34 @@ export class RemoteAppCommand extends AppCommand {
220
238
  // check if app is already installed on store
221
239
  return this.api.get({ adminPath: `/client/apps/${this.app.id}` }, {});
222
240
  }
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
- });
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;
242
252
  }
243
- async getVersions(version) {
253
+ async getVersion(version) {
244
254
  const versionsResponse = await this.api.get({ adminPath: `/apps/${this.app.id}/versions` }, {
245
255
  query: {
256
+ limit: 1,
246
257
  ...(version ? { version } : undefined),
247
258
  },
248
259
  });
249
- return versionsResponse.results || [];
260
+ return versionsResponse.results?.[0];
250
261
  }
251
- async getVersion(version) {
262
+ async getVersions(version) {
252
263
  const versionsResponse = await this.api.get({ adminPath: `/apps/${this.app.id}/versions` }, {
253
264
  query: {
254
- limit: 1,
255
265
  ...(version ? { version } : undefined),
256
266
  },
257
267
  });
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.'));
268
+ return versionsResponse.results || [];
262
269
  }
263
270
  async handleRequestErrors(request, spinnerError) {
264
271
  let showedErrors = false;
@@ -299,6 +306,21 @@ export class RemoteAppCommand extends AppCommand {
299
306
  await this.logOrientation();
300
307
  }
301
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
+ }
302
324
  async logOrientation() {
303
325
  const klass = this.ctor;
304
326
  const { flags } = await this.parse(klass);
@@ -316,191 +338,6 @@ export class RemoteAppCommand extends AppCommand {
316
338
  },
317
339
  });
318
340
  }
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
- });
337
- }
338
- async pushRemoteFile(config, log = true) {
339
- const spinner = ora();
340
- log && spinner.start(`Pushing ${config.filePath}`);
341
- try {
342
- const postData = await config.postData();
343
- if (!postData) {
344
- spinner.stop();
345
- return;
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;
350
- }
351
- catch (error) {
352
- if (error instanceof IgnoringFileError) {
353
- log &&
354
- spinner.fail(`Ignoring file: ${error.message} in ${config.filePath}`);
355
- return;
356
- }
357
- if (error instanceof FunctionProcessingError) {
358
- log &&
359
- spinner.fail(`${error.message} ${error.originalError?.message?.toLowerCase()}`);
360
- return;
361
- }
362
- log && spinner.fail(error.message);
363
- throw error;
364
- }
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)}`);
386
- return result;
387
- }
388
- async removeLocalFile(config, log = true) {
389
- if (!config.fileData) {
390
- return;
391
- }
392
- const spinner = ora();
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)}`);
397
- return result;
398
- }
399
- showErrors(errors, spinnerError) {
400
- const actualErrors = Object.keys(errors).filter((err) => Object.prototype.hasOwnProperty.call(errors, err));
401
- if (spinnerError) {
402
- spinnerError();
403
- }
404
- else {
405
- this.logError(inflect('Error', actualErrors.length));
406
- }
407
- for (const err of actualErrors) {
408
- if (!errors[err]?.message)
409
- continue;
410
- // we need to display `id` instead of `private_id`
411
- // developers don't have any knowledge of private ids
412
- const displayErr = err === 'private_id' ? 'id' : err;
413
- this.logError([
414
- ' -',
415
- `${displayErr}:`.toLowerCase().replace(/^error:/, ''),
416
- errors[err].message,
417
- ]
418
- .filter(Boolean)
419
- .join(' '));
420
- }
421
- }
422
- swellRcPath() {
423
- return path.join(this.appPath, '.swellrc');
424
- }
425
- getLocalAppConfigs() {
426
- const localConfigs = [];
427
- for (const { configFile, configType } of allConfigFilesPaths(this.appPath)) {
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) {
462
- continue;
463
- }
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();
498
- }
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
341
  async pullAppConfigs(force, confirmRemove = true) {
505
342
  const pulled = [];
506
343
  const removed = [];
@@ -525,13 +362,9 @@ export class RemoteAppCommand extends AppCommand {
525
362
  catch { }
526
363
  return true;
527
364
  });
528
- const filesToRemove = localBatchConfigs.filter((config) => {
529
- return !batch.configs.find((batchConfig) => {
530
- return batchConfig.filePath === config.filePath;
531
- });
532
- });
365
+ const filesToRemove = localBatchConfigs.filter((config) => !batch.configs.some((batchConfig) => batchConfig.filePath === config.filePath));
533
366
  // Skip if there are no config changes
534
- if (!pullFiles.length && !filesToRemove.length) {
367
+ if (pullFiles.length === 0 && filesToRemove.length === 0) {
535
368
  continue;
536
369
  }
537
370
  const updatedLog = pullFiles.length > 0
@@ -541,7 +374,8 @@ export class RemoteAppCommand extends AppCommand {
541
374
  this.log(`${style.appConfigName(`${batch.label} ${updatedLog}`)}`);
542
375
  }
543
376
  if (pullFiles.length > 0) {
544
- while (pullFiles.length) {
377
+ while (pullFiles.length > 0) {
378
+ // eslint-disable-next-line no-await-in-loop
545
379
  await Promise.all(pullFiles
546
380
  .splice(0, PUSH_CONCURRENCY)
547
381
  .map(async (config) => {
@@ -556,15 +390,17 @@ export class RemoteAppCommand extends AppCommand {
556
390
  const removeList = filesToRemove
557
391
  .map((config) => config.filePath)
558
392
  .join('\n');
393
+ // eslint-disable-next-line no-await-in-loop
559
394
  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
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?`,
562
397
  });
563
398
  if (!confirmed) {
564
399
  filesToRemove.splice(0);
565
400
  }
566
401
  }
567
- while (filesToRemove.length) {
402
+ while (filesToRemove.length > 0) {
403
+ // eslint-disable-next-line no-await-in-loop
568
404
  await Promise.all(filesToRemove.splice(0, PUSH_CONCURRENCY).map(async (config) => {
569
405
  await this.removeLocalFile(config);
570
406
  removed.push(config.id);
@@ -587,7 +423,7 @@ export class RemoteAppCommand extends AppCommand {
587
423
  }
588
424
  else {
589
425
  const swellConfigJson = {};
590
- for (const key in SwellJsonFields) {
426
+ for (const key of Object.keys(SwellJsonFields)) {
591
427
  const configKey = SwellJsonFields[key];
592
428
  swellConfigJson[configKey] = this.app[configKey];
593
429
  }
@@ -598,40 +434,190 @@ export class RemoteAppCommand extends AppCommand {
598
434
  swellConfig.id = swellJson?.id;
599
435
  return [swellConfig, ...allOtherConfigs];
600
436
  }
601
- isConfigDeprecated(config) {
602
- return !config.file_path;
437
+ async pullRemoteFile(config, log = true) {
438
+ const spinner = ora();
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;
445
+ }
446
+ catch (error) {
447
+ log && spinner.fail(error.message);
448
+ throw error;
449
+ }
603
450
  }
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);
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;
612
471
  }
613
- spinner.stop();
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();
614
508
  }
615
- const validConfigs = remoteConfigs.filter((config) => !this.isConfigDeprecated(config));
616
- return validConfigs;
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}`);
517
+ try {
518
+ const postData = await config.postData();
519
+ if (!postData) {
520
+ spinner.stop();
521
+ return;
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;
526
+ }
527
+ catch (error) {
528
+ if (error instanceof IgnoringFileError) {
529
+ log &&
530
+ spinner.fail(`Ignoring file: ${error.message} in ${config.filePath}`);
531
+ return;
532
+ }
533
+ if (error instanceof FunctionProcessingError) {
534
+ log &&
535
+ spinner.fail(`${error.message} ${error.originalError?.message?.toLowerCase()}`);
536
+ return;
537
+ }
538
+ log && spinner.fail(error.message);
539
+ throw error;
540
+ }
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)}`);
551
+ return result;
552
+ }
553
+ async removeRemoteFile(config, log = true) {
554
+ const spinner = ora();
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)}`);
559
+ return result;
560
+ }
561
+ showErrors(errors, spinnerError) {
562
+ const actualErrors = Object.keys(errors).filter((err) => Object.prototype.hasOwnProperty.call(errors, err));
563
+ if (spinnerError) {
564
+ spinnerError();
565
+ }
566
+ else {
567
+ this.logError(inflect('Error', actualErrors.length));
568
+ }
569
+ for (const err of actualErrors) {
570
+ if (!errors[err]?.message)
571
+ continue;
572
+ // we need to display `id` instead of `private_id`
573
+ // developers don't have any knowledge of private ids
574
+ const displayErr = err === 'private_id' ? 'id' : err;
575
+ this.logError([
576
+ ' -',
577
+ `${displayErr}:`.toLowerCase().replace(/^error:/, ''),
578
+ errors[err].message,
579
+ ]
580
+ .filter(Boolean)
581
+ .join(' '));
582
+ }
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
+ }
590
+ swellRcPath() {
591
+ return path.join(this.appPath, '.swellrc');
617
592
  }
618
593
  timestampStyled() {
619
594
  const date = new Date();
620
595
  return style.dim(`[${date.toLocaleTimeString()}]`);
621
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.'));
608
+ }
622
609
  /**
623
610
  * Reads the .swellrc file and sets up the remote app configuration.
624
611
  *
625
612
  * @returns App
626
613
  */
627
614
  initApp() {
628
- let app = this.swellConfig.store || {};
615
+ const app = this.swellConfig.store || {};
629
616
  // id refers to remote app id, so it's undefined initially
630
617
  app.id = undefined;
631
618
  // swell.json id refers to private id
632
619
  if (this.swellConfig.get('id')) {
633
620
  // use the same formula as the backend api
634
- // eslint-disable-next-line camelcase
635
621
  app.private_id = toAppId(this.swellConfig.get('id'));
636
622
  }
637
623
  // Default name to private id