@swell/cli 2.0.1-alpha.9 → 2.0.2

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