@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,64 +1,23 @@
1
- import * as path from 'node:path';
2
- import * as fs from 'node:fs';
3
- import { confirm, select, input } from '@inquirer/prompts';
4
- import Stream from 'stream';
1
+ import { confirm, input, select } from '@inquirer/prompts';
5
2
  import { $ } from 'execa';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import Stream from 'node:stream';
6
6
  import ora from 'ora';
7
- import { ConfigType, FrontendProjectTypes, findAppConfig, globAllFilesByPath, allConfigFilesInDir, appConfigFromFile, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getAppSlugId, hashString, filePathExists, isPathDirectory, } from './lib/apps/index.js';
8
- import { default as localConfig } from './lib/config.js';
9
7
  import { default as swellConfig } from './lib/app-config.js';
10
- import style from './lib/style.js';
8
+ import { ConfigType, FrontendProjectTypes, allConfigFilesInDir, appConfigFromFile, filePathExists, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
9
+ import { default as localConfig } from './lib/config.js';
11
10
  import { toAppId } from './lib/create/index.js';
11
+ import style from './lib/style.js';
12
12
  import { RemoteAppCommand } from './remote-app-command.js';
13
13
  const PUSH_CONCURRENCY = 3;
14
14
  export class PushAppCommand extends RemoteAppCommand {
15
15
  frontendPath = '';
16
- watchingFiles = [];
17
- watchingChangeQueue = [];
18
- watchingChangeQueueRunning = false;
19
16
  logWatchChanges = true;
20
17
  onWatchChange;
21
- async exec(command, onOutput) {
22
- const $$ = $({
23
- cwd: this.frontendPath,
24
- shell: true,
25
- stdin: 'inherit',
26
- stderr: onOutput ? 'pipe' : 'inherit',
27
- stdout: onOutput ? 'pipe' : 'inherit',
28
- });
29
- if (onOutput) {
30
- const outStream = new Stream.Writable();
31
- outStream._write = (chunk, _encoding, next) => {
32
- const string = chunk.toString();
33
- const out = onOutput(string);
34
- if (out !== false) {
35
- console.log(string);
36
- }
37
- next();
38
- };
39
- await $$ `${command}`.pipeStdout?.(outStream).pipeStderr?.(outStream);
40
- }
41
- else {
42
- await $$ `${command}`;
43
- }
44
- }
45
- setWatchingFiles() {
46
- this.watchingFiles = globAllFilesByPath(this.appPath);
47
- }
48
- async watchForChanges({ logChanges, syncAll, onChange, } = {}) {
49
- this.logWatchChanges = logChanges ?? true;
50
- this.onWatchChange = onChange;
51
- this.setWatchingFiles();
52
- if (syncAll) {
53
- await this.pushAppConfigs();
54
- fs.watch(this.appPath, { recursive: true }, async (_eventType, _filename) => {
55
- await this.pushAppConfigs();
56
- this.setWatchingFiles();
57
- });
58
- return;
59
- }
60
- fs.watch(this.appPath, { recursive: true }, this.watchListener);
61
- }
18
+ watchingChangeQueue = [];
19
+ watchingChangeQueueRunning = false;
20
+ watchingFiles = [];
62
21
  watchListener = async (eventType, filename) => {
63
22
  const configFile = filename;
64
23
  const pathParsed = path.parse(configFile);
@@ -117,51 +76,112 @@ export class PushAppCommand extends RemoteAppCommand {
117
76
  }
118
77
  }
119
78
  };
120
- async handleWatchFileChange(configFile, configType, event) {
121
- let config;
122
- if (event === 'deleted') {
123
- config = findAppConfig(this.app, configFile, configType);
124
- if (!config) {
125
- return;
126
- }
127
- this.watchingChangeQueue.push({
128
- action: 'remove',
129
- configFile,
130
- appConfig: config,
131
- });
132
- }
133
- else {
134
- config = appConfigFromFile(configFile, configType, this.appPath);
135
- this.watchingChangeQueue.push({
136
- action: 'push',
137
- configFile,
138
- appConfig: config,
139
- });
79
+ async buildFrontend(projectType) {
80
+ this.log(`Building ${projectType.name} frontend...\n`);
81
+ // TODO: check package.json for a "build" command and run that if it exists
82
+ await this.exec(projectType.buildCommand);
83
+ }
84
+ async chooseAppToPull(query) {
85
+ const typeLabelPlural = this.appType === 'theme' ? 'themes' : 'apps';
86
+ const typeLabelPrefixed = this.appType === 'theme' ? 'a theme' : 'an app';
87
+ const typeQuery = this.appType === 'theme' ? { type: 'theme' } : { type: 'storefront' };
88
+ const apps = await this.api.get({
89
+ adminPath: `/apps`,
90
+ }, { query: { limit: null, ...typeQuery, ...query } });
91
+ if (!apps.count) {
92
+ const currentStore = localConfig.getDefaultStore();
93
+ this.error(`There are no ${typeLabelPlural} associated with your store ${style.storeId(currentStore)}`);
140
94
  }
141
- await this.runWatchChangeQueue();
95
+ const appSlugId = (await select({
96
+ choices: apps.results.map((app) => ({
97
+ name: `${style.appConfigValue(app.name)} (${getAppSlugId(app)})`,
98
+ value: app.private_id,
99
+ })),
100
+ message: `Choose ${typeLabelPrefixed} to pull`,
101
+ }));
102
+ const appId = apps.results.find((app) => app.private_id === appSlugId).id;
103
+ const app = await this.getApp(appId);
104
+ this.log();
105
+ return app;
142
106
  }
143
- async runWatchChangeQueue() {
144
- if (this.watchingChangeQueueRunning) {
145
- return;
107
+ async createAppStorefront(hasOtherStorefronts = true) {
108
+ let appId;
109
+ let themeAppId;
110
+ if (this.app.type === 'theme') {
111
+ themeAppId = this.app.id;
112
+ const storefrontApps = await this.handleRequestErrors(async () => this.api.get({
113
+ adminPath: `/apps?type=storefront&kind=shop`,
114
+ }));
115
+ const compatibleStorefrontApps = storefrontApps.results.filter((app) => app.installed &&
116
+ app.storefront?.theme?.provider === 'app' &&
117
+ this.app.theme?.storefront?.app === toAppId(app.private_id));
118
+ if (compatibleStorefrontApps.length === 0) {
119
+ this.error('No compatible storefront apps found for this theme. Install a new storefront app first.');
120
+ }
121
+ const targetApp = compatibleStorefrontApps.length === 1
122
+ ? compatibleStorefrontApps[0]
123
+ : await select({
124
+ choices: compatibleStorefrontApps.map((app) => ({
125
+ name: `${style.appConfigValue(app.name)} v${app.version}`,
126
+ value: app,
127
+ })),
128
+ message: 'Choose a storefront app',
129
+ });
130
+ appId = targetApp.id;
131
+ this.log(`\n${style.success('✔')} Using ${style.appConfigValue(targetApp.name)} v${targetApp.version}\n`);
146
132
  }
147
- while (this.watchingChangeQueue.length) {
148
- this.watchingChangeQueueRunning = true;
149
- await Promise.all(this.watchingChangeQueue
150
- .splice(0, PUSH_CONCURRENCY)
151
- .map(async ({ action, configFile, appConfig }) => {
152
- let result;
153
- if (action === 'remove') {
154
- result = await this.removeRemoteFile(appConfig, this.logWatchChanges);
155
- }
156
- else {
157
- result = await this.pushRemoteFile(appConfig, this.logWatchChanges);
158
- }
159
- this.onWatchChange?.(appConfig, result);
133
+ else if (this.app.type === 'storefront') {
134
+ appId = this.app.id;
135
+ const themeApps = await this.handleRequestErrors(async () => this.api.get({
136
+ adminPath: `/apps?type=theme`,
160
137
  }));
138
+ const compatibleThemeApps = themeApps.results.filter((app) => app.installed &&
139
+ Object.keys(app.compatibilities || {}).includes(this.app.private_id));
140
+ if (compatibleThemeApps.length === 0) {
141
+ this.error('No compatible theme apps found for this storefront. Install a new theme app first.');
142
+ }
143
+ let targetApp;
144
+ if (compatibleThemeApps.length === 1) {
145
+ targetApp = compatibleThemeApps[0];
146
+ }
147
+ else {
148
+ targetApp = await select({
149
+ choices: compatibleThemeApps.map((app) => ({
150
+ name: `${style.appConfigValue(app.name)} v${app.version}`,
151
+ value: app,
152
+ })),
153
+ message: 'Choose a theme app',
154
+ });
155
+ this.log();
156
+ }
157
+ themeAppId = targetApp.id;
158
+ this.log(`${style.success('✔')} Using ${style.appConfigValue(targetApp.name)} v${targetApp.version}\n`);
161
159
  }
162
- this.watchingChangeQueueRunning = false;
163
- // refetch app to get updated configs
164
- this.app = await this.getAppWithConfig(this.app.id);
160
+ else {
161
+ this.error('App type not supported for storefront creation');
162
+ }
163
+ let name;
164
+ if (hasOtherStorefronts) {
165
+ name = await input({
166
+ default: this.app.name,
167
+ message: 'Name your storefront',
168
+ });
169
+ if (name === this.app.name) {
170
+ name = undefined; // undefined allows the api to auto increment name
171
+ }
172
+ }
173
+ return this.handleRequestErrors(async () => this.api.post({
174
+ adminPath: `/storefronts`,
175
+ }, {
176
+ body: {
177
+ app_id: appId,
178
+ name,
179
+ theme_app_id: themeAppId,
180
+ },
181
+ onAsyncGetPath: (response) => ({
182
+ adminPath: `/storefronts/${response.id}`,
183
+ }),
184
+ }));
165
185
  }
166
186
  async deployAppFrontend(force, log) {
167
187
  const currentStore = localConfig.getDefaultStore();
@@ -188,45 +208,14 @@ export class PushAppCommand extends RemoteAppCommand {
188
208
  }
189
209
  }
190
210
  }
191
- getFrontendDeploymentHash() {
192
- const localConfigs = this.getLocalAppConfigs();
193
- let frontendHashes = localConfigs
194
- .filter((config) => config.filePath?.startsWith(`frontend${path.sep}`))
195
- .map((config) => config.hash)
196
- .sort()
197
- .join('|');
198
- if (frontendHashes.length > 0) {
199
- const packageHash = localConfigs.find((config) => config.filePath === 'package.json')?.hash;
200
- frontendHashes += `|${packageHash}`;
201
- }
202
- return frontendHashes?.length > 0 ? hashString(frontendHashes) : undefined;
203
- }
204
- getFrontendProjectType(required = true) {
205
- this.frontendPath = path.join(this.appPath, 'frontend');
206
- if (!required) {
207
- const frontendExists = filePathExists(this.frontendPath);
208
- if (!frontendExists) {
209
- return null;
210
- }
211
- }
212
- const projectType = getFrontendProjectType(this.appPath);
213
- if (!projectType) {
214
- this.error(`No valid frontend app found in ${this.appPath}/${this.frontendPath}. Supported frameworks include: ${FrontendProjectTypes.map((type) => type.slug).join(', ')}.`);
215
- }
216
- return projectType;
217
- }
218
- async ensureLoggedIn() {
211
+ async deployFrontend(projectType) {
212
+ // TODO: check package.json for a "deploy" command and run that if it exists
213
+ // Note it must return a URL somehow
219
214
  try {
220
- const user = await this.api.get({ adminPath: 'user' });
221
- if (!user) {
222
- throw new Error();
223
- }
215
+ return this.wranglerDeployFrontend(projectType);
224
216
  }
225
- catch {
226
- this.log('Logging in...\n');
227
- await this.config.runCommand('login');
228
- this.log();
229
- return true;
217
+ catch (error) {
218
+ this.error(`Error deploying frontend: ${error.message}`);
230
219
  }
231
220
  }
232
221
  async ensureAppExists(file, shouldCreate = true) {
@@ -237,10 +226,9 @@ export class PushAppCommand extends RemoteAppCommand {
237
226
  currentStore = localConfig.getDefaultStore();
238
227
  this.api.setStoreEnv(currentStore, 'test');
239
228
  }
240
- const app = await this.getAppWithConfig('').catch((_error) => {
241
- // Ignore so we can continue to create it
242
- return null;
243
- });
229
+ const app = await this.getAppWithConfig('').catch((_error) =>
230
+ // Ignore so we can continue to create it
231
+ null);
244
232
  const isTheme = app?.type === 'theme';
245
233
  if (app && !isTheme && this.appType === 'theme') {
246
234
  this.error(`App ${style.appConfigValue(app?.name)} is not a theme.`);
@@ -279,88 +267,71 @@ export class PushAppCommand extends RemoteAppCommand {
279
267
  }
280
268
  return true;
281
269
  }
282
- async confirmRemoveInstalledApp(app, currentStore) {
283
- if (!app?.private_id) {
270
+ async ensureLoggedIn() {
271
+ try {
272
+ const user = await this.api.get({ adminPath: 'user' });
273
+ if (!user) {
274
+ throw new Error('Failed to log in');
275
+ }
276
+ }
277
+ catch {
278
+ this.log('Logging in...\n');
279
+ await this.config.runCommand('login');
280
+ this.log();
284
281
  return true;
285
282
  }
286
- const existingInstalledApp = await this.api.get({
287
- adminPath: `/client/apps/${app.private_id}`,
288
- }, { query: { versioned: true } });
289
- if (existingInstalledApp) {
290
- const continuePush = await confirm({
291
- default: false,
292
- message: `${style.storeId(currentStore)} has an installed version of ${app.name ? style.appConfigValue(app.name) : 'this app'} v${existingInstalledApp.version}. Do you want to replace it with a development instance?`,
293
- });
294
- if (!continuePush) {
295
- return false;
296
- }
297
- // uninstall the existing app
298
- const spinner = ora();
299
- spinner.start('Removing installed app...');
300
- try {
301
- await this.api.put({
302
- adminPath: `/client/apps/${app.id}`,
303
- }, {
304
- body: { uninstalled: true },
305
- });
306
- spinner.succeed('Installed app removed.');
307
- this.log();
308
- }
309
- catch (error) {
310
- spinner.fail(`Error removing installed app from ${style.storeId(currentStore)}.`);
311
- this.logError(error.message);
312
- return false;
313
- }
314
- }
315
- return existingInstalledApp;
316
283
  }
317
- resolveAppPath(appId, targetPath) {
318
- let resolvedAppId = appId || '';
319
- let resolvedPath = targetPath || resolvedAppId;
320
- // If app_id matches directory, use it as target path
321
- if (!targetPath &&
322
- isPathDirectory(path.resolve(process.cwd(), resolvedAppId))) {
323
- resolvedPath = resolvedAppId;
284
+ async exec(command, onOutput) {
285
+ const $$ = $({
286
+ cwd: this.frontendPath,
287
+ shell: true,
288
+ stderr: onOutput ? 'pipe' : 'inherit',
289
+ stdin: 'inherit',
290
+ stdout: onOutput ? 'pipe' : 'inherit',
291
+ });
292
+ if (onOutput) {
293
+ const outStream = new Stream.Writable();
294
+ outStream._write = (chunk, _encoding, next) => {
295
+ const string = chunk.toString();
296
+ const out = onOutput(string);
297
+ if (out !== false) {
298
+ console.log(string);
299
+ }
300
+ next();
301
+ };
302
+ await $$ `${command}`.pipeStdout?.(outStream).pipeStderr?.(outStream);
324
303
  }
325
- // Reset app config and path if appId is provided
326
- if (resolvedPath && this.appPath === '.') {
327
- this.appPath = path.resolve(process.cwd(), resolvedPath);
304
+ else {
305
+ await $$ `${command}`;
328
306
  }
329
- return this.appPath;
330
307
  }
331
- async getAppStorefront(params) {
308
+ async getAllAppStorefronts(params) {
332
309
  const query = Object.entries(params || {})
333
310
  .map(([key, value]) => `${key}=${value}`)
334
311
  .join('&');
335
- try {
336
- return await this.handleRequestErrors(async () => this.api.get({
337
- adminPath: `/apps/${this.app.id}/storefront?${query}`,
338
- }));
339
- }
340
- catch {
341
- // noop
342
- }
312
+ return this.handleRequestErrors(async () => this.api.get({
313
+ adminPath: `/apps/${this.app.id}/storefronts?${query}`,
314
+ }));
343
315
  }
344
316
  async getAllStorefronts(params) {
345
317
  const query = Object.entries(params || {})
346
318
  .map(([key, value]) => `${key}=${value}`)
347
319
  .join('&');
348
- return await this.handleRequestErrors(async () => this.api.get({
320
+ return this.handleRequestErrors(async () => this.api.get({
349
321
  adminPath: `/storefronts?${query}`,
350
322
  }));
351
323
  }
352
- async getAllAppStorefronts(params) {
324
+ async getAppStorefront(params) {
353
325
  const query = Object.entries(params || {})
354
326
  .map(([key, value]) => `${key}=${value}`)
355
327
  .join('&');
356
- return await this.handleRequestErrors(async () => this.api.get({
357
- adminPath: `/apps/${this.app.id}/storefronts?${query}`,
358
- }));
359
- }
360
- saveCurrentStorefront() {
361
- const currentStorefront = localConfig.getDefaultStorefront(this.appPath);
362
- if (this.storefront && this.storefront.id !== currentStorefront) {
363
- localConfig.setDefaultStorefront(this.appPath, this.storefront.id);
328
+ try {
329
+ return this.handleRequestErrors(async () => this.api.get({
330
+ adminPath: `/apps/${this.app.id}/storefront?${query}`,
331
+ }));
332
+ }
333
+ catch {
334
+ // noop
364
335
  }
365
336
  }
366
337
  async getAppToPull(args, shouldChoose = true) {
@@ -391,67 +362,50 @@ export class PushAppCommand extends RemoteAppCommand {
391
362
  }
392
363
  async getExistingApp(appId) {
393
364
  try {
394
- return await this.getApp(appId);
365
+ return this.getApp(appId);
395
366
  }
396
- catch (err) {
397
- if (err.status === 404) {
367
+ catch (error) {
368
+ if (error.status === 404) {
398
369
  const labelPrefix = this.appType === 'theme' ? 'Theme' : 'App';
399
370
  this.error(`${labelPrefix} '${appId}' not found.`);
400
371
  }
401
372
  else {
402
- throw err;
373
+ throw error;
403
374
  }
404
375
  }
405
376
  }
406
- async getStorefrontToPush(flags = {}) {
407
- const targetStorefront = flags['storefront-id'];
408
- const currentStorefront = localConfig.getDefaultStorefront(this.appPath);
409
- let storefrontId = targetStorefront || currentStorefront;
410
- if (!storefrontId || flags['storefront-select']) {
411
- const storefronts = await this.getAllAppStorefronts({
412
- type: this.appType,
413
- });
414
- if (storefronts.count > 0) {
415
- storefrontId = await select({
416
- message: 'Choose a storefront',
417
- choices: [
418
- ...storefronts.results.map((storefront) => ({
419
- name: `${style.appConfigValue(storefront.name)} (${storefront.id}) ${storefront.primary ? '[primary]' : ''}`,
420
- value: storefront.id,
421
- })),
422
- {
423
- name: 'Create new storefront',
424
- value: null,
425
- },
426
- ],
427
- });
428
- }
429
- if (!storefrontId) {
430
- this.storefront = await this.createAppStorefront(storefronts.count > 0);
431
- }
377
+ getFrontendDeploymentHash() {
378
+ const localConfigs = this.getLocalAppConfigs();
379
+ let frontendHashes = localConfigs
380
+ .filter((config) => config.filePath?.startsWith(`frontend${path.sep}`))
381
+ .map((config) => config.hash)
382
+ .sort()
383
+ .join('|');
384
+ if (frontendHashes.length > 0) {
385
+ const packageHash = localConfigs.find((config) => config.filePath === 'package.json')?.hash;
386
+ frontendHashes += `|${packageHash}`;
432
387
  }
433
- if (storefrontId) {
434
- this.storefront = await this.getAppStorefront({
435
- storefront_id: storefrontId,
436
- type: this.appType,
437
- });
438
- if (!this.storefront) {
439
- if (targetStorefront) {
440
- this.error(`Storefront ${style.appConfigValue(storefrontId)} not found.`);
441
- }
442
- this.log(`Storefront ${style.appConfigValue(storefrontId)} not found.\n`);
443
- localConfig.setDefaultStorefront(this.appPath, '');
444
- return this.getStorefrontToPush();
388
+ return frontendHashes?.length > 0 ? hashString(frontendHashes) : undefined;
389
+ }
390
+ getFrontendProjectType(required = true) {
391
+ this.frontendPath = path.join(this.appPath, 'frontend');
392
+ if (!required) {
393
+ const frontendExists = filePathExists(this.frontendPath);
394
+ if (!frontendExists) {
395
+ return null;
445
396
  }
446
397
  }
447
- this.app = await this.getApp(this.app.id);
448
- return this.storefront;
398
+ const projectType = getFrontendProjectType(this.appPath);
399
+ if (!projectType) {
400
+ this.error(`No valid frontend app found in ${this.appPath}/${this.frontendPath}. Supported frameworks include: ${FrontendProjectTypes.map((type) => type.slug).join(', ')}.`);
401
+ }
402
+ return projectType;
449
403
  }
450
404
  async getStorefrontToPull(args = {}, flags = {}) {
451
- const { targetPath } = args;
405
+ const { appId, targetPath } = args;
452
406
  const targetApp = await this.getAppToPull(args, false);
453
407
  const targetStorefront = flags['storefront-id'];
454
- this.resolveAppPath(args.appId, targetPath);
408
+ this.resolveAppPath(appId, targetPath);
455
409
  // Only use default if path exists
456
410
  let currentStorefront;
457
411
  if (isPathDirectory(this.appPath)) {
@@ -466,13 +420,11 @@ export class PushAppCommand extends RemoteAppCommand {
466
420
  : { type: this.appType });
467
421
  if (storefronts.count > 0) {
468
422
  storefrontId = await select({
423
+ choices: storefronts.results.map((storefront) => ({
424
+ name: `${style.appConfigValue(storefront.name)} (${storefront.id}) ${storefront.primary ? '[primary]' : ''}`,
425
+ value: storefront.id,
426
+ })),
469
427
  message: 'Choose a storefront',
470
- choices: [
471
- ...storefronts.results.map((storefront) => ({
472
- name: `${style.appConfigValue(storefront.name)} (${storefront.id}) ${storefront.primary ? '[primary]' : ''}`,
473
- value: storefront.id,
474
- })),
475
- ],
476
428
  });
477
429
  this.storefront = storefronts.results.find((storefront) => storefront.id === storefrontId);
478
430
  const appId = this.appType === 'theme'
@@ -508,111 +460,84 @@ export class PushAppCommand extends RemoteAppCommand {
508
460
  this.swellConfig = swellConfig(this.appPath);
509
461
  return this.storefront;
510
462
  }
511
- async chooseAppToPull(query) {
512
- const typeLabelPlural = this.appType === 'theme' ? 'themes' : 'apps';
513
- const typeLabelPrefixed = this.appType === 'theme' ? 'a theme' : 'an app';
514
- const typeQuery = this.appType === 'theme' ? { type: 'theme' } : { type: 'storefront' };
515
- const apps = await this.api.get({
516
- adminPath: `/apps`,
517
- }, { query: { limit: null, ...typeQuery, ...query } });
518
- if (!apps.count) {
519
- const currentStore = localConfig.getDefaultStore();
520
- this.error(`There are no ${typeLabelPlural} associated with your store ${style.storeId(currentStore)}`);
521
- }
522
- const appSlugId = (await select({
523
- choices: apps.results.map((app) => ({
524
- name: `${style.appConfigValue(app.name)} (${getAppSlugId(app)})`,
525
- value: app.private_id,
526
- })),
527
- message: `Choose ${typeLabelPrefixed} to pull`,
528
- }));
529
- const appId = apps.results.find((app) => app.private_id === appSlugId).id;
530
- const app = await this.getApp(appId);
531
- this.log();
532
- return app;
533
- }
534
- async createAppStorefront(hasOtherStorefronts = true) {
535
- let appId;
536
- let themeAppId;
537
- if (this.app.type === 'theme') {
538
- themeAppId = this.app.id;
539
- const storefrontApps = await this.handleRequestErrors(async () => this.api.get({
540
- adminPath: `/apps?type=storefront&kind=shop`,
541
- }));
542
- const compatibleStorefrontApps = storefrontApps.results.filter((app) => app.installed &&
543
- app.storefront?.theme?.provider === 'app' &&
544
- this.app.theme?.storefront?.app === toAppId(app.private_id));
545
- if (!compatibleStorefrontApps.length) {
546
- this.error('No compatible storefront apps found for this theme. Install a new storefront app first.');
547
- }
548
- let targetApp;
549
- if (compatibleStorefrontApps.length === 1) {
550
- targetApp = compatibleStorefrontApps[0];
551
- }
552
- else {
553
- targetApp = await select({
554
- message: 'Choose a storefront app',
555
- choices: compatibleStorefrontApps.map((app) => ({
556
- name: `${style.appConfigValue(app.name)} v${app.version}`,
557
- value: app,
558
- })),
463
+ async getStorefrontToPush(flags = {}) {
464
+ const targetStorefront = flags['storefront-id'];
465
+ const currentStorefront = localConfig.getDefaultStorefront(this.appPath);
466
+ let storefrontId = targetStorefront || currentStorefront;
467
+ if (!storefrontId || flags['storefront-select']) {
468
+ const storefronts = await this.getAllAppStorefronts({
469
+ type: this.appType,
470
+ });
471
+ if (storefronts.count > 0) {
472
+ storefrontId = await select({
473
+ choices: [
474
+ ...storefronts.results.map((storefront) => ({
475
+ name: `${style.appConfigValue(storefront.name)} (${storefront.id}) ${storefront.primary ? '[primary]' : ''}`,
476
+ value: storefront.id,
477
+ })),
478
+ {
479
+ name: 'Create new storefront',
480
+ value: null,
481
+ },
482
+ ],
483
+ message: 'Choose a storefront',
559
484
  });
560
485
  }
561
- appId = targetApp.id;
562
- this.log(`\n${style.success('✔')} Using ${style.appConfigValue(targetApp.name)} v${targetApp.version}\n`);
563
- }
564
- else if (this.app.type === 'storefront') {
565
- appId = this.app.id;
566
- const themeApps = await this.handleRequestErrors(async () => this.api.get({
567
- adminPath: `/apps?type=theme`,
568
- }));
569
- const compatibleThemeApps = themeApps.results.filter((app) => app.installed &&
570
- Object.keys(app.compatibilities || {}).includes(this.app.private_id));
571
- if (!compatibleThemeApps.length) {
572
- this.error('No compatible theme apps found for this storefront. Install a new theme app first.');
486
+ if (!storefrontId) {
487
+ this.storefront = await this.createAppStorefront(storefronts.count > 0);
573
488
  }
574
- let targetApp;
575
- if (compatibleThemeApps.length === 1) {
576
- targetApp = compatibleThemeApps[0];
489
+ }
490
+ if (storefrontId) {
491
+ this.storefront = await this.getAppStorefront({
492
+ storefront_id: storefrontId,
493
+ type: this.appType,
494
+ });
495
+ if (!this.storefront) {
496
+ if (targetStorefront) {
497
+ this.error(`Storefront ${style.appConfigValue(storefrontId)} not found.`);
498
+ }
499
+ this.log(`Storefront ${style.appConfigValue(storefrontId)} not found.\n`);
500
+ localConfig.setDefaultStorefront(this.appPath, '');
501
+ return this.getStorefrontToPush();
577
502
  }
578
- else {
579
- targetApp = await select({
580
- message: 'Choose a theme app',
581
- choices: compatibleThemeApps.map((app) => ({
582
- name: `${style.appConfigValue(app.name)} v${app.version}`,
583
- value: app,
584
- })),
585
- });
586
- this.log();
503
+ }
504
+ this.app = await this.getApp(this.app.id);
505
+ return this.storefront;
506
+ }
507
+ async handleWatchFileChange(configFile, configType, event) {
508
+ let config;
509
+ if (event === 'deleted') {
510
+ config = findAppConfig(this.app, configFile, configType);
511
+ if (!config) {
512
+ return;
587
513
  }
588
- themeAppId = targetApp.id;
589
- this.log(`${style.success('')} Using ${style.appConfigValue(targetApp.name)} v${targetApp.version}\n`);
514
+ this.watchingChangeQueue.push({
515
+ action: 'remove',
516
+ appConfig: config,
517
+ configFile,
518
+ });
590
519
  }
591
520
  else {
592
- this.error('App type not supported for storefront creation');
593
- }
594
- let name;
595
- if (hasOtherStorefronts) {
596
- name = await input({
597
- message: 'Name your storefront',
598
- default: this.app.name,
521
+ config = appConfigFromFile(configFile, configType, this.appPath);
522
+ this.watchingChangeQueue.push({
523
+ action: 'push',
524
+ appConfig: config,
525
+ configFile,
599
526
  });
600
- if (name === this.app.name) {
601
- name = undefined; // undefined allows the api to auto increment name
602
- }
603
527
  }
604
- return this.handleRequestErrors(async () => this.api.post({
605
- adminPath: `/storefronts`,
606
- }, {
607
- body: {
608
- name,
609
- app_id: appId,
610
- theme_app_id: themeAppId,
611
- },
612
- onAsyncGetPath: (response) => ({
613
- adminPath: `/storefronts/${response.id}`,
614
- }),
615
- }));
528
+ await this.runWatchChangeQueue();
529
+ }
530
+ logAppFrontendUrl() {
531
+ const currentStore = localConfig.getDefaultStore();
532
+ // Shouldn't happen, but just in case
533
+ if (!this.app.installed?.id) {
534
+ return;
535
+ }
536
+ this.log(`View your app at ${style.link(this.appFrontendUrl(currentStore, this.app.installed.id))}.`);
537
+ }
538
+ logDashboardUrl(flags) {
539
+ const currentStore = localConfig.getDefaultStore();
540
+ this.log(`View the ${style.appConfigValue(this.app.name)} app in your dashboard at ${style.link(this.dashboardUrl(flags, currentStore))}.`);
616
541
  }
617
542
  logStorefrontConnected() {
618
543
  if (!this.storefront) {
@@ -624,9 +549,15 @@ export class PushAppCommand extends RemoteAppCommand {
624
549
  : '';
625
550
  this.log(`[storefront] ${style.appName(name)}${themeLog}\n`);
626
551
  }
627
- logDashboardUrl(flags) {
552
+ logStorefrontFrontendUrl(storefront, branchId) {
628
553
  const currentStore = localConfig.getDefaultStore();
629
- this.log(`View the ${style.appConfigValue(this.app.name)} app in your dashboard at ${style.link(this.dashboardUrl(flags, currentStore))}.`);
554
+ this.log(`View your storefront at ${style.link(this.storefrontFrontendUrl(currentStore, storefront, branchId))}.`);
555
+ }
556
+ async pushFile(relativePath) {
557
+ const basePath = relativePath.split(path.sep)[0];
558
+ const configType = getConfigTypeFromPath(basePath) || ConfigType.FILE;
559
+ const config = appConfigFromFile(relativePath, configType, this.appPath);
560
+ await this.pushRemoteFile(config);
630
561
  }
631
562
  async pushFilePath(relativePath, force) {
632
563
  const topDir = relativePath.split(path.sep)[0];
@@ -638,23 +569,18 @@ export class PushAppCommand extends RemoteAppCommand {
638
569
  for (const { configFile } of allConfigFilesInDir(this.appPath, relativePath, configTypeKey)) {
639
570
  const config = appConfigFromFile(configFile, configType, this.appPath);
640
571
  allFiles.push(config);
641
- const exConfig = exAppConfigs.find((exConfig) => {
642
- return config.filePath === exConfig.filePath;
643
- });
572
+ const exConfig = exAppConfigs.find((exConfig) => config.filePath === exConfig.filePath);
644
573
  if (!force && exConfig && exConfig.hash === config.hash) {
645
574
  continue;
646
575
  }
647
576
  pushFiles.push(config);
648
577
  }
649
- const filesToRemove = exAppConfigs.filter((config) => {
650
- return !allFiles.find((appConfig) => {
651
- return appConfig.filePath === config.filePath;
652
- });
653
- });
578
+ const filesToRemove = exAppConfigs.filter((config) => !allFiles.some((appConfig) => appConfig.filePath === config.filePath));
654
579
  const hasChanges = pushFiles.length > 0 || filesToRemove.length > 0;
655
580
  if (pushFiles.length > 0) {
656
581
  const pushConcurrency = configTypeKey === 'ASSET' ? 1 : PUSH_CONCURRENCY;
657
- while (pushFiles.length) {
582
+ while (pushFiles.length > 0) {
583
+ // eslint-disable-next-line no-await-in-loop
658
584
  await Promise.all(pushFiles
659
585
  .splice(0, pushConcurrency)
660
586
  .map(async (appConfig) => {
@@ -664,7 +590,8 @@ export class PushAppCommand extends RemoteAppCommand {
664
590
  }
665
591
  // Remove any deleted files
666
592
  if (filesToRemove.length > 0) {
667
- while (filesToRemove.length) {
593
+ while (filesToRemove.length > 0) {
594
+ // eslint-disable-next-line no-await-in-loop
668
595
  await Promise.all(filesToRemove.splice(0, PUSH_CONCURRENCY).map(async (appConfig) => {
669
596
  await this.removeRemoteFile(appConfig);
670
597
  }));
@@ -678,26 +605,79 @@ export class PushAppCommand extends RemoteAppCommand {
678
605
  this.log(`${style.success('✔')} Pushed ${style.path(relativePath)} to ${style.appConfigValue(this.app.name)}.`);
679
606
  }
680
607
  }
681
- async pushFile(relativePath) {
682
- const basePath = relativePath.split(path.sep)[0];
683
- const configType = getConfigTypeFromPath(basePath) || ConfigType.FILE;
684
- const config = appConfigFromFile(relativePath, configType, this.appPath);
685
- await this.pushRemoteFile(config);
608
+ resolveAppPath(appId, targetPath) {
609
+ const resolvedAppId = appId || '';
610
+ let resolvedPath = targetPath || resolvedAppId;
611
+ // If app_id matches directory, use it as target path
612
+ if (!targetPath &&
613
+ isPathDirectory(path.resolve(process.cwd(), resolvedAppId))) {
614
+ resolvedPath = resolvedAppId;
615
+ }
616
+ // Reset app config and path if appId is provided
617
+ if (resolvedPath && this.appPath === '.') {
618
+ this.appPath = path.resolve(process.cwd(), resolvedPath);
619
+ }
620
+ return this.appPath;
686
621
  }
687
- async deployFrontend(projectType) {
688
- // TODO: check package.json for a "deploy" command and run that if it exists
689
- // Note it must return a URL somehow
690
- try {
691
- return this.wranglerDeployFrontend(projectType);
622
+ async runWatchChangeQueue() {
623
+ if (this.watchingChangeQueueRunning) {
624
+ return;
692
625
  }
693
- catch (error) {
694
- this.error(`Error deploying frontend: ${error.message}`);
626
+ while (this.watchingChangeQueue.length > 0) {
627
+ this.watchingChangeQueueRunning = true;
628
+ // eslint-disable-next-line no-await-in-loop
629
+ await Promise.all(this.watchingChangeQueue
630
+ .splice(0, PUSH_CONCURRENCY)
631
+ .map(async ({ action, appConfig }) => {
632
+ const result = await (action === 'remove'
633
+ ? this.removeRemoteFile(appConfig, this.logWatchChanges)
634
+ : this.pushRemoteFile(appConfig, this.logWatchChanges));
635
+ this.onWatchChange?.(appConfig, result);
636
+ }));
695
637
  }
638
+ this.watchingChangeQueueRunning = false;
639
+ // refetch app to get updated configs
640
+ this.app = await this.getAppWithConfig(this.app.id);
696
641
  }
697
- async buildFrontend(projectType) {
698
- this.log(`Building ${projectType.name} frontend...\n`);
699
- // TODO: check package.json for a "build" command and run that if it exists
700
- await this.exec(projectType.buildCommand);
642
+ saveCurrentStorefront() {
643
+ const currentStorefront = localConfig.getDefaultStorefront(this.appPath);
644
+ if (this.storefront && this.storefront.id !== currentStorefront) {
645
+ localConfig.setDefaultStorefront(this.appPath, this.storefront.id);
646
+ }
647
+ }
648
+ setWatchingFiles() {
649
+ this.watchingFiles = globAllFilesByPath(this.appPath);
650
+ }
651
+ async updateFrontendDeployment(currentStore, projectType, deploymentUrl, deploymentHash) {
652
+ const spinner = ora();
653
+ spinner.start(`Updating frontend deployment...`);
654
+ await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${this.app.id}` }, {
655
+ body: {
656
+ $client_id: currentStore,
657
+ $environment_id: 'test',
658
+ frontend: {
659
+ deployment_hash: deploymentHash,
660
+ framework: projectType.slug,
661
+ service: 'cloudflare',
662
+ url: deploymentUrl,
663
+ },
664
+ },
665
+ }), () => spinner.fail('Error updating app deployment'));
666
+ spinner.succeed('Updated frontend deployment.\n');
667
+ }
668
+ async watchForChanges({ logChanges, onChange, syncAll, } = {}) {
669
+ this.logWatchChanges = logChanges ?? true;
670
+ this.onWatchChange = onChange;
671
+ this.setWatchingFiles();
672
+ if (syncAll) {
673
+ await this.pushAppConfigs();
674
+ fs.watch(this.appPath, { recursive: true }, async (_eventType, _filename) => {
675
+ await this.pushAppConfigs();
676
+ this.setWatchingFiles();
677
+ });
678
+ return;
679
+ }
680
+ fs.watch(this.appPath, { recursive: true }, this.watchListener);
701
681
  }
702
682
  async wranglerDeployFrontend(projectType) {
703
683
  let deploymentUrl;
@@ -706,11 +686,13 @@ export class PushAppCommand extends RemoteAppCommand {
706
686
  try {
707
687
  await this.exec(`npx wrangler pages deploy ${this.appPath}/frontend/${projectType.deployPath}`, (string) => {
708
688
  // Dependent on wrangler output
709
- if (string.includes('Take a peek over at')) {
710
- deploymentUrl = String(string.split('Take a peek over at ')[1]).trim();
689
+ // Parse the deployment URL from the wrangler output.
690
+ const match = string.match(/Take a peek over at (http\S+)/);
691
+ if (match && match.length > 0) {
692
+ deploymentUrl = match[1];
711
693
  return false;
712
694
  }
713
- else if (interactiveError ||
695
+ if (interactiveError ||
714
696
  string.includes('non-interactive mode') ||
715
697
  string.includes('non-interactive environment')) {
716
698
  interactiveError = true;
@@ -718,14 +700,15 @@ export class PushAppCommand extends RemoteAppCommand {
718
700
  }
719
701
  });
720
702
  }
721
- catch (err) {
703
+ catch (error) {
722
704
  // noop
723
705
  if (interactiveError) {
724
706
  this.log(style.funcWarn(`Your Cloudflare environment must be initialized by logging in with \`wrangler login\`, and exporting the \`CLOUDFLARE_ACCOUNT_ID\` environment variable. Refer to https://developers.cloudflare.com/workers/wrangler/configuration/ for details, and re-run this command to connect the deployment with your app.`));
707
+ // eslint-disable-next-line no-process-exit, unicorn/no-process-exit
725
708
  process.exit(1);
726
709
  }
727
710
  else {
728
- throw err;
711
+ throw error;
729
712
  }
730
713
  }
731
714
  if (!deploymentUrl) {
@@ -733,33 +716,39 @@ export class PushAppCommand extends RemoteAppCommand {
733
716
  }
734
717
  return deploymentUrl;
735
718
  }
736
- async updateFrontendDeployment(currentStore, projectType, deploymentUrl, deploymentHash) {
737
- const spinner = ora();
738
- spinner.start(`Updating frontend deployment...`);
739
- await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${this.app.id}` }, {
740
- body: {
741
- $client_id: currentStore,
742
- $environment_id: 'test',
743
- frontend: {
744
- url: deploymentUrl,
745
- service: 'cloudflare',
746
- framework: projectType.slug,
747
- deployment_hash: deploymentHash,
748
- },
749
- },
750
- }), () => spinner.fail('Error updating app deployment'));
751
- spinner.succeed('Updated frontend deployment.\n');
752
- }
753
- logAppFrontendUrl() {
754
- const currentStore = localConfig.getDefaultStore();
755
- // Shouldn't happen, but just in case
756
- if (!this.app.installed?.id) {
757
- return;
719
+ async confirmRemoveInstalledApp(app, currentStore) {
720
+ if (!app?.private_id) {
721
+ return true;
758
722
  }
759
- this.log(`View your app at ${style.link(this.appFrontendUrl(currentStore, this.app.installed.id))}.`);
760
- }
761
- logStorefrontFrontendUrl(storefront, branchId) {
762
- const currentStore = localConfig.getDefaultStore();
763
- this.log(`View your storefront at ${style.link(this.storefrontFrontendUrl(currentStore, storefront, branchId))}.`);
723
+ const existingInstalledApp = await this.api.get({
724
+ adminPath: `/client/apps/${app.private_id}`,
725
+ }, { query: { versioned: true } });
726
+ if (existingInstalledApp) {
727
+ const continuePush = await confirm({
728
+ default: false,
729
+ message: `${style.storeId(currentStore)} has an installed version of ${app.name ? style.appConfigValue(app.name) : 'this app'} v${existingInstalledApp.version}. Do you want to replace it with a development instance?`,
730
+ });
731
+ if (!continuePush) {
732
+ return false;
733
+ }
734
+ // uninstall the existing app
735
+ const spinner = ora();
736
+ spinner.start('Removing installed app...');
737
+ try {
738
+ await this.api.put({
739
+ adminPath: `/client/apps/${app.id}`,
740
+ }, {
741
+ body: { uninstalled: true },
742
+ });
743
+ spinner.succeed('Installed app removed.');
744
+ this.log();
745
+ }
746
+ catch (error) {
747
+ spinner.fail(`Error removing installed app from ${style.storeId(currentStore)}.`);
748
+ this.logError(error.message);
749
+ return false;
750
+ }
751
+ }
752
+ return existingInstalledApp;
764
753
  }
765
754
  }