@swell/cli 2.0.20 → 2.2.0

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 (94) hide show
  1. package/dist/commands/api/delete.d.ts +15 -0
  2. package/dist/commands/api/delete.js +26 -0
  3. package/dist/commands/api/get.d.ts +15 -0
  4. package/dist/commands/api/get.js +31 -0
  5. package/dist/commands/api/index.d.ts +7 -0
  6. package/dist/commands/api/index.js +16 -0
  7. package/dist/commands/api/post.d.ts +16 -0
  8. package/dist/commands/api/post.js +33 -0
  9. package/dist/commands/api/put.d.ts +16 -0
  10. package/dist/commands/api/put.js +33 -0
  11. package/dist/commands/app/dev.d.ts +18 -4
  12. package/dist/commands/app/dev.js +383 -5
  13. package/dist/commands/app/frontend/dev.d.ts +6 -5
  14. package/dist/commands/app/frontend/dev.js +127 -58
  15. package/dist/commands/app/install.js +3 -3
  16. package/dist/commands/app/pull.d.ts +1 -1
  17. package/dist/commands/app/pull.js +6 -6
  18. package/dist/commands/app/push.d.ts +3 -3
  19. package/dist/commands/app/push.js +8 -6
  20. package/dist/commands/app/release.d.ts +1 -1
  21. package/dist/commands/app/release.js +1 -1
  22. package/dist/commands/app/version.d.ts +1 -1
  23. package/dist/commands/app/version.js +5 -3
  24. package/dist/commands/create/app.d.ts +10 -2
  25. package/dist/commands/create/app.js +226 -57
  26. package/dist/commands/create/content.d.ts +4 -0
  27. package/dist/commands/create/content.js +83 -15
  28. package/dist/commands/create/function.js +27 -5
  29. package/dist/commands/create/model.d.ts +4 -0
  30. package/dist/commands/create/model.js +120 -25
  31. package/dist/commands/inspect/content.d.ts +25 -0
  32. package/dist/commands/inspect/content.js +159 -0
  33. package/dist/commands/inspect/index.d.ts +7 -0
  34. package/dist/commands/inspect/index.js +16 -0
  35. package/dist/commands/inspect/models.d.ts +27 -0
  36. package/dist/commands/inspect/models.js +197 -0
  37. package/dist/commands/schema.d.ts +27 -0
  38. package/dist/commands/schema.js +231 -0
  39. package/dist/commands/theme/dev.d.ts +1 -1
  40. package/dist/commands/theme/dev.js +2 -2
  41. package/dist/commands/theme/init.d.ts +1 -1
  42. package/dist/commands/theme/init.js +1 -1
  43. package/dist/commands/theme/pull.d.ts +2 -2
  44. package/dist/commands/theme/pull.js +2 -2
  45. package/dist/commands/theme/push.d.ts +1 -1
  46. package/dist/commands/theme/push.js +1 -1
  47. package/dist/create-app-command.d.ts +14 -0
  48. package/dist/create-app-command.js +198 -15
  49. package/dist/create-collection-command.js +4 -1
  50. package/dist/create-config-command.js +4 -0
  51. package/dist/env/local.d.ts +2 -0
  52. package/dist/env/local.js +2 -0
  53. package/dist/env/production.d.ts +2 -0
  54. package/dist/env/production.js +2 -0
  55. package/dist/env/review.d.ts +2 -0
  56. package/dist/env/review.js +2 -0
  57. package/dist/env/staging.d.ts +2 -0
  58. package/dist/env/staging.js +2 -0
  59. package/dist/lib/api.d.ts +16 -5
  60. package/dist/lib/api.js +67 -25
  61. package/dist/lib/app-config.js +1 -0
  62. package/dist/lib/apps/app-config.d.ts +49 -0
  63. package/dist/lib/apps/app-config.js +21 -14
  64. package/dist/lib/apps/index.d.ts +5 -4
  65. package/dist/lib/apps/index.js +109 -21
  66. package/dist/lib/bundle.js +2 -1
  67. package/dist/lib/config.d.ts +1 -1
  68. package/dist/lib/constants.d.ts +2 -4
  69. package/dist/lib/constants.js +6 -4
  70. package/dist/lib/create/content.d.ts +5 -1
  71. package/dist/lib/create/content.js +8 -1
  72. package/dist/lib/create/function.d.ts +2 -1
  73. package/dist/lib/create/function.js +7 -3
  74. package/dist/lib/create/model.d.ts +1 -0
  75. package/dist/lib/create/schemas.d.ts +6 -0
  76. package/dist/lib/create/schemas.js +6 -0
  77. package/dist/lib/proxy.d.ts +1 -0
  78. package/dist/lib/proxy.js +29 -1
  79. package/dist/lib/sessions.js +4 -3
  80. package/dist/lib/style.js +0 -1
  81. package/dist/lib/swell-function-wrapper.d.ts +23 -4
  82. package/dist/lib/swell-function-wrapper.js +11 -4
  83. package/dist/lib/theme-sync.d.ts +6 -6
  84. package/dist/lib/theme-sync.js +49 -39
  85. package/dist/push-app-command.d.ts +12 -8
  86. package/dist/push-app-command.js +108 -29
  87. package/dist/remote-app-command.d.ts +1 -1
  88. package/dist/remote-app-command.js +4 -2
  89. package/dist/swell-api-command.d.ts +13 -0
  90. package/dist/swell-api-command.js +75 -0
  91. package/dist/swell-command.d.ts +1 -1
  92. package/dist/swell-command.js +9 -9
  93. package/package.json +10 -1
  94. package/oclif.manifest.json +0 -1895
@@ -1,29 +1,37 @@
1
- import http from 'http';
2
1
  import getPort, { portNumbers } from 'get-port';
2
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3
+ // @ts-ignore - http-proxy has incomplete TypeScript definitions
4
+ import httpProxy from 'http-proxy';
5
+ import http from 'node:http';
3
6
  import util from 'node:util';
4
7
  import zlib from 'node:zlib';
5
- // @ts-ignore
6
- import httpProxy from 'http-proxy';
7
- // @ts-ignore
8
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
9
+ // @ts-ignore - ws library uses named exports differently
10
+ // eslint-disable-next-line import/no-named-as-default
8
11
  import WebSocket, { WebSocketServer } from 'ws';
9
12
  const gunzipAsync = util.promisify(zlib.gunzip);
10
13
  const inflateAsync = util.promisify(zlib.inflate);
11
14
  const brotliDecompressAsync = util.promisify(zlib.brotliDecompress);
12
15
  const FALLBACK_PORT = 3000;
13
16
  export class ThemeSync {
14
- server;
17
+ options;
15
18
  proxy;
19
+ server;
16
20
  wss;
17
- options;
18
- async selectPort() {
19
- return (await getPort({ port: portNumbers(3000, 3100) })) || FALLBACK_PORT;
20
- }
21
21
  constructor(options) {
22
22
  this.options = options;
23
23
  this.server = http.createServer();
24
24
  this.proxy = httpProxy.createProxyServer({});
25
25
  this.wss = new WebSocketServer({ server: this.server });
26
26
  }
27
+ reload() {
28
+ // Get all connected WebSocket clients
29
+ const clients = [...this.wss.clients].filter((client) => client.readyState === WebSocket.OPEN);
30
+ // Notify clients to reload
31
+ for (const client of clients) {
32
+ client.send('reload');
33
+ }
34
+ }
27
35
  async start() {
28
36
  // Select port if not specified
29
37
  if (!this.options.port) {
@@ -85,38 +93,52 @@ export class ThemeSync {
85
93
  });
86
94
  });
87
95
  }
88
- reload() {
89
- // Get all connected WebSocket clients
90
- const clients = Array.from(this.wss.clients).filter((client) => client.readyState === WebSocket.OPEN);
91
- // Notify clients to reload
92
- clients.forEach((client) => {
93
- client.send('reload');
94
- });
96
+ decodeRequestBody(body, encoding) {
97
+ switch (encoding) {
98
+ case 'gzip': {
99
+ return gunzipAsync(body);
100
+ }
101
+ case 'deflate': {
102
+ return inflateAsync(body);
103
+ }
104
+ case 'br': {
105
+ return brotliDecompressAsync(body);
106
+ }
107
+ default: {
108
+ return Promise.reject(new Error(`Unsupported content encoding: ${encoding}`));
109
+ }
110
+ }
111
+ }
112
+ // Inject WebSocket client script into proxied HTML
113
+ injectScript(html, host) {
114
+ const script = `
115
+ <script>
116
+ const ws = new WebSocket('ws://${host}/ws');
117
+ ws.onmessage = () => {
118
+ window.location.reload();
119
+ };
120
+ </script>
121
+ `;
122
+ return html.replace('</body>', `${script}</body>`);
95
123
  }
96
124
  async parseRequestBody(proxyRes, body) {
97
125
  // May contain multiple encodings
98
126
  const contentEncoding = String(proxyRes.headers['content-encoding'] ?? '')
99
127
  .split(',')
100
128
  .reverse();
129
+ // Sequential decoding of content-encoding layers is required
130
+ /* eslint-disable no-await-in-loop */
101
131
  for (const value of contentEncoding) {
102
132
  const encoding = value.trim().toLowerCase();
103
133
  if (encoding) {
104
134
  body = await this.decodeRequestBody(body, encoding);
105
135
  }
106
136
  }
137
+ /* eslint-enable no-await-in-loop */
107
138
  return body.toString('utf8');
108
139
  }
109
- decodeRequestBody(body, encoding) {
110
- switch (encoding) {
111
- case 'gzip':
112
- return gunzipAsync(body);
113
- case 'deflate':
114
- return inflateAsync(body);
115
- case 'br':
116
- return brotliDecompressAsync(body);
117
- default:
118
- return Promise.reject(new Error(`Unsupported content encoding: ${encoding}`));
119
- }
140
+ async selectPort() {
141
+ return (await getPort({ port: portNumbers(3000, 3100) })) || FALLBACK_PORT;
120
142
  }
121
143
  shouldInjectScript(proxyRes, html) {
122
144
  // Check if this is an HTML response
@@ -129,16 +151,4 @@ export class ThemeSync {
129
151
  }
130
152
  return true;
131
153
  }
132
- // Inject WebSocket client script into proxied HTML
133
- injectScript(html, host) {
134
- const script = `
135
- <script>
136
- const ws = new WebSocket('ws://${host}/ws');
137
- ws.onmessage = () => {
138
- window.location.reload();
139
- };
140
- </script>
141
- `;
142
- return html.replace('</body>', `${script}</body>`);
143
- }
144
154
  }
@@ -12,20 +12,22 @@ interface WatchingChange {
12
12
  export declare abstract class PushAppCommand extends RemoteAppCommand {
13
13
  frontendPath: string;
14
14
  logWatchChanges: boolean;
15
- onWatchChange?: (appConfig?: AppConfig, result?: any) => void;
15
+ onWatchChange?: (appConfig?: AppConfig, action?: string, result?: any) => void;
16
16
  watchingChangeQueue: Map<string, WatchingChange>;
17
17
  watchingFiles: Set<string>;
18
18
  watchingIgnoreFilter: GlobbyFilterFunction | null;
19
19
  watchingTimer: NodeJS.Timeout | null;
20
20
  watchListener: (eventType: fs.WatchEventType, filename: null | string) => Promise<void>;
21
- buildFrontend(projectType: FrontendProjectType): Promise<void>;
21
+ protected showFrontendMigrationError(): never;
22
+ buildFrontend(projectType?: FrontendProjectType): Promise<void>;
22
23
  chooseAppToPull(query?: any): Promise<App>;
23
24
  createAppStorefront(hasOtherStorefronts?: boolean): Promise<any>;
24
25
  deployAppFrontend(force?: boolean, log?: boolean): Promise<void>;
25
26
  deployFrontend(projectType: FrontendProjectType): Promise<string>;
26
27
  ensureAppExists(file?: string, shouldCreate?: boolean): Promise<boolean>;
27
28
  ensureLoggedIn(): Promise<true | undefined>;
28
- exec(command: string, onOutput?: (string: string) => any | false): Promise<void>;
29
+ exec(command: string, cwd?: string, onOutput?: (string: string) => any | false): Promise<void>;
30
+ execFrontend(command: string, onOutput?: (string: string) => any | false): Promise<void>;
29
31
  getAllAppStorefronts(params?: {
30
32
  type?: string;
31
33
  }): Promise<any>;
@@ -49,19 +51,19 @@ export declare abstract class PushAppCommand extends RemoteAppCommand {
49
51
  appId?: string;
50
52
  targetPath?: string;
51
53
  }, flags?: {
54
+ env?: string;
52
55
  'storefront-id'?: string;
53
56
  'storefront-select'?: boolean;
54
- env?: string;
55
57
  }): Promise<any>;
56
58
  getStorefrontToPush(flags?: {
59
+ env?: string;
57
60
  'storefront-id'?: string;
58
61
  'storefront-select'?: boolean;
59
- env?: string;
60
62
  }): Promise<any>;
61
63
  setStorefrontEnv(flags: {
64
+ env?: string;
62
65
  'storefront-id'?: string;
63
66
  'storefront-select'?: boolean;
64
- env?: string;
65
67
  }, defaultStorefront?: any): Promise<void>;
66
68
  handleWatchFileChange(configFile: string, configType: ConfigType, event: WatchFileEvent): Promise<void>;
67
69
  logAppFrontendUrl(): void;
@@ -76,13 +78,15 @@ export declare abstract class PushAppCommand extends RemoteAppCommand {
76
78
  runWatchChangeQueue(): Promise<void>;
77
79
  saveCurrentStorefront(): void;
78
80
  setWatchingFiles(): Promise<void>;
81
+ startProxyServer(port?: number): Promise<number>;
79
82
  updateFrontendDeployment(currentStore: string, projectType: FrontendProjectType, deploymentUrl: string, deploymentHash: string): Promise<void>;
83
+ updateLocalProxy(proxyUrl: string, storefrontId?: string): Promise<void>;
80
84
  watchForChanges({ logChanges, onChange, syncAll, }?: {
81
85
  logChanges?: boolean;
82
- onChange?: () => void;
86
+ onChange?: (appConfig?: AppConfig, result?: any) => void;
83
87
  syncAll?: boolean;
84
88
  }): Promise<void>;
85
- wranglerDeployFrontend(projectType: FrontendProjectType): Promise<string>;
89
+ wranglerDeployFrontend(_projectType: FrontendProjectType): Promise<string>;
86
90
  private confirmRemoveInstalledApp;
87
91
  }
88
92
  export {};
@@ -1,19 +1,22 @@
1
1
  import { confirm, input, select } from '@inquirer/prompts';
2
2
  import { $ } from 'execa';
3
+ import getPort, { portNumbers } from 'get-port';
3
4
  import * as fs from 'node:fs';
4
5
  import * as path from 'node:path';
5
6
  import Stream from 'node:stream';
6
7
  import ora from 'ora';
7
8
  import { default as swellConfig } from './lib/app-config.js';
8
- import { ConfigType, FrontendProjectTypes, allConfigFilesInDir, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
9
- import { selectEnvironmentId } from './lib/stores.js';
9
+ import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
10
10
  import { getGlobIgnorePathsChecker } from './lib/apps/paths.js';
11
11
  import { default as localConfig } from './lib/config.js';
12
12
  import { toAppId } from './lib/create/index.js';
13
+ import { getProxyUrl } from './lib/proxy.js';
14
+ import { selectEnvironmentId } from './lib/stores.js';
13
15
  import style from './lib/style.js';
14
16
  import { RemoteAppCommand } from './remote-app-command.js';
15
17
  const PUSH_CONCURRENCY = 3;
16
18
  const WATCH_WINDOW_MS = 100;
19
+ const DEV_SERVER_FALLBACK_PORT = 3000;
17
20
  export class PushAppCommand extends RemoteAppCommand {
18
21
  frontendPath = '';
19
22
  logWatchChanges = true;
@@ -45,6 +48,7 @@ export class PushAppCommand extends RemoteAppCommand {
45
48
  if (!configType) {
46
49
  return;
47
50
  }
51
+ await this.setWatchingFiles();
48
52
  const isWatching = this.watchingFiles.has(configFile);
49
53
  let watchFileEvent;
50
54
  // per node docs:
@@ -75,6 +79,7 @@ export class PushAppCommand extends RemoteAppCommand {
75
79
  await this.handleWatchFileChange(configFile, configType, watchFileEvent);
76
80
  }
77
81
  catch (error) {
82
+ console.error(error);
78
83
  // we don't want to break the watcher if an error is thrown while
79
84
  // handling a file change
80
85
  // for tests though, we want to break the watcher
@@ -87,10 +92,29 @@ export class PushAppCommand extends RemoteAppCommand {
87
92
  }
88
93
  }
89
94
  };
95
+ showFrontendMigrationError() {
96
+ return this.error(style.funcWarn(`⚠️ Your frontend directory exists but appears to use an older structure.\n` +
97
+ `Swell CLI v2.1.0+ requires a Workers-based frontend with package.json.\n\n` +
98
+ `${style.basicHighlight('Migration options:')}\n\n` +
99
+ `1. ${style.basicHighlight('Migrate to Workers')} (recommended)\n` +
100
+ ` If you are using a Swell official application (e.g. Proxima), update to the latest version.\n` +
101
+ ` Otherwise, initialize your frontend as a Workers project.\n` +
102
+ ` Follow Cloudflare's official guide: ${style.link('https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/')}\n\n` +
103
+ `2. ${style.basicHighlight('Use older CLI version')}\n` +
104
+ ` Continue with Pages deployment:\n` +
105
+ ` ${style.dim('$ npm install -g @swell/cli@2.0.20')}\n`));
106
+ }
90
107
  async buildFrontend(projectType) {
108
+ if (!projectType) {
109
+ this.showFrontendMigrationError();
110
+ return; // unreachable but helps TypeScript narrow the type
111
+ }
112
+ if (!projectType.buildCommand) {
113
+ // No build command needed for this framework
114
+ return;
115
+ }
91
116
  this.log(`Building ${projectType.name} frontend...\n`);
92
- // TODO: check package.json for a "build" command and run that if it exists
93
- await this.exec(projectType.buildCommand);
117
+ await this.execFrontend(projectType.buildCommand);
94
118
  }
95
119
  async chooseAppToPull(query) {
96
120
  const typeLabelPlural = this.appType === 'theme' ? 'themes' : 'apps';
@@ -241,7 +265,7 @@ export class PushAppCommand extends RemoteAppCommand {
241
265
  null);
242
266
  const isTheme = app?.type === 'theme';
243
267
  if (app && !isTheme && this.appType === 'theme') {
244
- this.error(`App ${style.appConfigValue(app?.name)} is not a theme.`);
268
+ this.error(`App ${style.appConfigValue(app?.name)} is not a theme. Use 'swell app' commands instead.`);
245
269
  }
246
270
  // Confirm development app unless theme without app syncing
247
271
  if (!isTheme || this.themeSyncApp) {
@@ -283,9 +307,9 @@ export class PushAppCommand extends RemoteAppCommand {
283
307
  return true;
284
308
  }
285
309
  }
286
- async exec(command, onOutput) {
310
+ async exec(command, cwd, onOutput) {
287
311
  const $$ = $({
288
- cwd: this.frontendPath,
312
+ cwd: cwd || this.appPath,
289
313
  shell: true,
290
314
  stderr: onOutput ? 'pipe' : 'inherit',
291
315
  stdin: 'inherit',
@@ -307,6 +331,9 @@ export class PushAppCommand extends RemoteAppCommand {
307
331
  await $$ `${command}`;
308
332
  }
309
333
  }
334
+ async execFrontend(command, onOutput) {
335
+ return this.exec(command, this.frontendPath, onOutput);
336
+ }
310
337
  async getAllAppStorefronts(params) {
311
338
  const query = Object.entries(params || {})
312
339
  .map(([key, value]) => `${key}=${value}`)
@@ -392,15 +419,23 @@ export class PushAppCommand extends RemoteAppCommand {
392
419
  }
393
420
  getFrontendProjectType(required = true) {
394
421
  this.frontendPath = path.join(this.appPath, 'frontend');
395
- if (!required) {
396
- const frontendExists = filePathExists(this.frontendPath);
397
- if (!frontendExists) {
398
- return null;
399
- }
422
+ const frontendExists = filePathExists(this.frontendPath);
423
+ if (!required && !frontendExists) {
424
+ return null;
400
425
  }
401
426
  const projectType = getFrontendProjectType(this.appPath);
402
427
  if (!projectType) {
403
- this.error(`No valid frontend app found in ${this.appPath}/${this.frontendPath}. Supported frameworks include: ${FrontendProjectTypes.map((type) => type.slug).join(', ')}.`);
428
+ // If frontend not required, just return null (don't error)
429
+ if (!required) {
430
+ return null;
431
+ }
432
+ // Frontend IS required but not found
433
+ // Check if frontend directory exists but lacks package.json (old structure)
434
+ if (frontendExists) {
435
+ this.showFrontendMigrationError();
436
+ }
437
+ // Frontend directory doesn't exist at all
438
+ this.error(`No valid frontend app found in ${this.frontendPath}. Supported frameworks include: ${getFrontendProjectValidValues(false)}.`);
404
439
  }
405
440
  return projectType;
406
441
  }
@@ -526,8 +561,8 @@ export class PushAppCommand extends RemoteAppCommand {
526
561
  await this.api.setStoreEnv(currentStore, defaultStorefront?.env);
527
562
  }
528
563
  }
529
- else if (flags['env']) {
530
- await this.api.setStoreEnv(currentStore, flags['env']);
564
+ else if (flags.env) {
565
+ await this.api.setStoreEnv(currentStore, flags.env);
531
566
  }
532
567
  else {
533
568
  const hasTestEnv = await this.api.isTestEnvEnabled();
@@ -668,15 +703,24 @@ export class PushAppCommand extends RemoteAppCommand {
668
703
  }
669
704
  const queue = [...this.watchingChangeQueue.values()];
670
705
  this.watchingChangeQueue.clear();
706
+ // Process queue in batches with concurrency control
707
+ /* eslint-disable no-await-in-loop */
671
708
  while (queue.length > 0) {
672
- // eslint-disable-next-line no-await-in-loop
673
- await Promise.all(queue.splice(0, PUSH_CONCURRENCY).map(async ({ action, appConfig }) => {
674
- const result = await (action === 'remove'
675
- ? this.removeRemoteFile(appConfig, this.logWatchChanges)
676
- : this.pushRemoteFile(appConfig, this.logWatchChanges));
677
- this.onWatchChange?.(appConfig, result);
678
- }));
709
+ try {
710
+ await Promise.all(queue
711
+ .splice(0, PUSH_CONCURRENCY)
712
+ .map(async ({ action, appConfig }) => {
713
+ const result = await (action === 'remove'
714
+ ? this.removeRemoteFile(appConfig, this.logWatchChanges)
715
+ : this.pushRemoteFile(appConfig, this.logWatchChanges));
716
+ this.onWatchChange?.(appConfig, action, result);
717
+ }));
718
+ }
719
+ catch {
720
+ // noop
721
+ }
679
722
  }
723
+ /* eslint-enable no-await-in-loop */
680
724
  // refetch app to get updated configs
681
725
  this.app = await this.getAppWithConfig(this.app.id);
682
726
  // Process next accumulated changes
@@ -691,6 +735,16 @@ export class PushAppCommand extends RemoteAppCommand {
691
735
  async setWatchingFiles() {
692
736
  this.watchingFiles = new Set(await globAllFilesByPath(this.appPath));
693
737
  }
738
+ async startProxyServer(port) {
739
+ // Find an open port starting at 3000
740
+ const freePort = port ||
741
+ (await getPort({ port: portNumbers(3000, 3100) })) ||
742
+ DEV_SERVER_FALLBACK_PORT;
743
+ // Start proxy
744
+ const proxyUrl = await getProxyUrl(freePort);
745
+ await this.updateLocalProxy(proxyUrl, this.storefront?.id);
746
+ return freePort;
747
+ }
694
748
  async updateFrontendDeployment(currentStore, projectType, deploymentUrl, deploymentHash) {
695
749
  const spinner = ora();
696
750
  spinner.start('Updating frontend deployment...');
@@ -708,6 +762,17 @@ export class PushAppCommand extends RemoteAppCommand {
708
762
  }), () => spinner.fail('Error updating app deployment'));
709
763
  spinner.succeed('Updated frontend deployment.\n');
710
764
  }
765
+ async updateLocalProxy(proxyUrl, storefrontId) {
766
+ const storefront = this.app.type === 'storefront' &&
767
+ (await this.getAppStorefront({ storefront_id: storefrontId }));
768
+ await this.handleRequestErrors(async () => this.api.put({ adminPath: `/client/apps/${this.app.id}/local-proxy` }, {
769
+ body: {
770
+ proxy_url: proxyUrl,
771
+ storefront_id: storefront?.id || null,
772
+ storefront_slug: storefront?.slug || null,
773
+ },
774
+ }));
775
+ }
711
776
  async watchForChanges({ logChanges, onChange, syncAll, } = {}) {
712
777
  this.watchingIgnoreFilter = await getGlobIgnorePathsChecker(this.appPath);
713
778
  this.logWatchChanges = logChanges ?? true;
@@ -723,19 +788,27 @@ export class PushAppCommand extends RemoteAppCommand {
723
788
  }
724
789
  fs.watch(this.appPath, { recursive: true }, this.watchListener);
725
790
  }
726
- async wranglerDeployFrontend(projectType) {
791
+ async wranglerDeployFrontend(_projectType) {
727
792
  let deploymentUrl;
728
793
  let interactiveError = false;
794
+ let pagesProjectError = false;
729
795
  this.log(`\nDeploying to Cloudflare...\n`);
730
796
  try {
731
- await this.exec(`npx wrangler pages deploy ${this.appPath}/frontend/${projectType.deployPath}`, (string) => {
797
+ await this.execFrontend(`npx wrangler deploy`, (string) => {
732
798
  // Dependent on wrangler output
733
799
  // Parse the deployment URL from the wrangler output.
734
- const match = string.match(/Take a peek over at (http\S+)/);
800
+ const match = string.match(/^\s*(https:\/\/\S+\.workers\.dev)\s*[\S\s]*?Current Version ID: [\w-]+/m);
735
801
  if (match && match.length > 0) {
736
802
  deploymentUrl = match[1];
737
803
  return false;
738
804
  }
805
+ // Check for Pages project error
806
+ if (string.includes("It looks like you've run a Workers-specific command in a Pages project") ||
807
+ string.includes('please run `wrangler pages deploy` instead') ||
808
+ string.includes('Missing entry-point')) {
809
+ pagesProjectError = true;
810
+ return false;
811
+ }
739
812
  if (interactiveError ||
740
813
  string.includes('non-interactive mode') ||
741
814
  string.includes('non-interactive environment')) {
@@ -746,10 +819,16 @@ export class PushAppCommand extends RemoteAppCommand {
746
819
  }
747
820
  catch (error) {
748
821
  // noop
749
- if (interactiveError) {
750
- 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.`));
751
- // eslint-disable-next-line no-process-exit, unicorn/no-process-exit
752
- process.exit(1);
822
+ if (pagesProjectError) {
823
+ this.error(style.funcWarn(`⚠️ Version incompatibility!\n` +
824
+ `Your project is configured for Cloudflare Pages, but Swell CLI v2.1.0+ uses Cloudflare Workers.\n\n` +
825
+ `To fix this, you can either:\n` +
826
+ `1. Update your app project (for Swell official apps) or reconfigure it for workers\n` +
827
+ ` (https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/)\n` +
828
+ `2. Use an older Swell CLI version: npm install -g @swell/cli@2.0.20\n`));
829
+ }
830
+ else if (interactiveError) {
831
+ this.error(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.`));
753
832
  }
754
833
  else {
755
834
  throw error;
@@ -5,11 +5,11 @@ import { App, AppConfig, AppVersion } from './lib/apps/index.js';
5
5
  * configurations.
6
6
  */
7
7
  export declare abstract class RemoteAppCommand extends AppCommand {
8
- appType: string;
9
8
  static delayOrientation: boolean;
10
9
  static orientation: AppCommandOrientation;
11
10
  app: App;
12
11
  appCreated: boolean;
12
+ appType: string;
13
13
  storefront?: any;
14
14
  storefrontLocalUrl?: string;
15
15
  themeSyncApp?: boolean;
@@ -15,7 +15,6 @@ import style from './lib/style.js';
15
15
  * configurations.
16
16
  */
17
17
  export class RemoteAppCommand extends AppCommand {
18
- appType = 'any';
19
18
  // allows child commands to set orientation
20
19
  static delayOrientation = false;
21
20
  // most of the times this will be the target environment
@@ -24,6 +23,7 @@ export class RemoteAppCommand extends AppCommand {
24
23
  app = {};
25
24
  // Indicates the app was created during the command execution
26
25
  appCreated = false;
26
+ appType = 'any';
27
27
  // API instance of the target storefront, if applicable
28
28
  storefront = null;
29
29
  // Indicates we are syncing with a local app frontend via proxy
@@ -638,7 +638,9 @@ export class RemoteAppCommand extends AppCommand {
638
638
  const { env, store } = flags;
639
639
  if (this.app.name) {
640
640
  // Show theme version if not syncing app
641
- const version = !this.themeSyncApp && this.app.type === 'theme' && this.storefront?.theme_version
641
+ const version = !this.themeSyncApp &&
642
+ this.app.type === 'theme' &&
643
+ this.storefront?.theme_version
642
644
  ? this.storefront?.theme_version
643
645
  : this.app.version;
644
646
  orientationOutput.push(`[${this.app.type === 'theme' ? 'theme' : 'app'}] ${style.appName(this.app.name)}${version ? ` v${version}` : ''}`);
@@ -0,0 +1,13 @@
1
+ import { HttpMethod } from './lib/api.js';
2
+ import { SwellCommand } from './swell-command.js';
3
+ export declare abstract class SwellApiCommand extends SwellCommand {
4
+ protected abstract get method(): HttpMethod;
5
+ protected request(command: typeof SwellApiCommand, requestOptions?: Api.RequestOptions): Promise<void>;
6
+ protected catch(error: Error): Promise<any>;
7
+ private parseCommand;
8
+ private processBody;
9
+ private isFilePath;
10
+ private handleResponse;
11
+ private onSuccess;
12
+ private onError;
13
+ }
@@ -0,0 +1,75 @@
1
+ import * as fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { FetchError } from 'node-fetch';
4
+ import { SwellCommand } from './swell-command.js';
5
+ export class SwellApiCommand extends SwellCommand {
6
+ async request(command, requestOptions = {}) {
7
+ const { paths, options } = await this.parseCommand(command);
8
+ const response = await this.api[this.method](paths, {
9
+ rawResponse: true,
10
+ ...options,
11
+ ...requestOptions,
12
+ });
13
+ await this.handleResponse(response);
14
+ }
15
+ async catch(error) {
16
+ if (error instanceof FetchError) {
17
+ const message = `Could not connect to Swell API. Please try again later: ${error.message}`;
18
+ return this.onError(message, { exit: 2, code: error.code });
19
+ }
20
+ return this.onError(error.message, { exit: 1 });
21
+ }
22
+ async parseCommand(options, argv) {
23
+ const parsedInput = await super.parse(options, argv);
24
+ const { args, flags } = parsedInput;
25
+ const { path } = args;
26
+ const { live, body } = flags;
27
+ if (!live) {
28
+ await this.api.setEnv('test');
29
+ }
30
+ if (!path.startsWith('/')) {
31
+ throw new Error('Path must start with a forward slash (/)');
32
+ }
33
+ return {
34
+ ...parsedInput,
35
+ paths: { adminPath: `/data${path}` },
36
+ options: {
37
+ body: await this.processBody(body),
38
+ },
39
+ };
40
+ }
41
+ async processBody(body) {
42
+ if (!body) {
43
+ return;
44
+ }
45
+ const bodyData = this.isFilePath(body)
46
+ ? await fs.promises.readFile(path.resolve(body), 'utf8')
47
+ : body;
48
+ return JSON.parse(bodyData);
49
+ }
50
+ isFilePath(filePath) {
51
+ return (path.isAbsolute(filePath) ||
52
+ filePath.startsWith('./') ||
53
+ filePath.startsWith('../'));
54
+ }
55
+ async handleResponse(response) {
56
+ const responseText = await response.text();
57
+ if (responseText.length <= 2) {
58
+ throw new Error('Not found');
59
+ }
60
+ const result = JSON.parse(responseText);
61
+ if (result.error || result.errors || !response.ok) {
62
+ throw new Error(responseText);
63
+ }
64
+ this.onSuccess(result);
65
+ }
66
+ onSuccess(result) {
67
+ this.log(JSON.stringify(result, null, 2));
68
+ }
69
+ onError(message, { code, exit }) {
70
+ this.error(message, {
71
+ code,
72
+ exit,
73
+ });
74
+ }
75
+ }
@@ -14,8 +14,8 @@ export declare abstract class SwellCommand extends Command {
14
14
  api: Api;
15
15
  requiresAuth: boolean;
16
16
  constructor(argv: string[], config: Config);
17
- catch(error: any): Promise<void>;
18
17
  debugJson(...args: any[]): void;
19
18
  logError(message: string): void;
20
19
  logSuccess(message: string): void;
20
+ protected catch(error: any): Promise<void>;
21
21
  }
@@ -21,15 +21,6 @@ export class SwellCommand extends Command {
21
21
  super(argv, config);
22
22
  this.api = new Api();
23
23
  }
24
- async catch(error) {
25
- if (error instanceof GitUnknownError) {
26
- this.error(style.error(`git: ${error.stderr || error.stdout || error.message}`));
27
- }
28
- else if (error instanceof FetchError) {
29
- this.error(`Could not connect to Swell API. Please try again later: ${error.message}`);
30
- }
31
- throw error;
32
- }
33
24
  debugJson(...args) {
34
25
  const stringified = args.map((arg) => typeof arg === 'string' ? arg : JSON.stringify(arg, null, 4));
35
26
  this.debug(...stringified);
@@ -40,4 +31,13 @@ export class SwellCommand extends Command {
40
31
  logSuccess(message) {
41
32
  this.log(`${style.success(message)}`);
42
33
  }
34
+ async catch(error) {
35
+ if (error instanceof GitUnknownError) {
36
+ this.error(style.error(`git: ${error.stderr || error.stdout || error.message}`));
37
+ }
38
+ else if (error instanceof FetchError) {
39
+ this.error(`Could not connect to Swell API. Please try again later: ${error.message}`);
40
+ }
41
+ throw error;
42
+ }
43
43
  }