@swell/cli 2.0.19 → 2.1.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.
@@ -3,6 +3,7 @@ import { $ } from 'execa';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
5
  import Stream from 'node:stream';
6
+ import getPort, { portNumbers } from 'get-port';
6
7
  import ora from 'ora';
7
8
  import { default as swellConfig } from './lib/app-config.js';
8
9
  import { ConfigType, FrontendProjectTypes, allConfigFilesInDir, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
@@ -12,8 +13,10 @@ import { default as localConfig } from './lib/config.js';
12
13
  import { toAppId } from './lib/create/index.js';
13
14
  import style from './lib/style.js';
14
15
  import { RemoteAppCommand } from './remote-app-command.js';
16
+ import { getProxyUrl } from './lib/proxy.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,12 @@ export class PushAppCommand extends RemoteAppCommand {
283
307
  return true;
284
308
  }
285
309
  }
286
- async exec(command, onOutput) {
310
+ async execFrontend(command, onOutput) {
311
+ return this.exec(command, this.frontendPath, onOutput);
312
+ }
313
+ async exec(command, cwd, onOutput) {
287
314
  const $$ = $({
288
- cwd: this.frontendPath,
315
+ cwd: cwd || this.appPath,
289
316
  shell: true,
290
317
  stderr: onOutput ? 'pipe' : 'inherit',
291
318
  stdin: 'inherit',
@@ -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: ${FrontendProjectTypes.map((type) => type.slug).join(', ')}.`);
404
439
  }
405
440
  return projectType;
406
441
  }
@@ -670,12 +705,19 @@ export class PushAppCommand extends RemoteAppCommand {
670
705
  this.watchingChangeQueue.clear();
671
706
  while (queue.length > 0) {
672
707
  // 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
- }));
708
+ try {
709
+ await Promise.all(queue
710
+ .splice(0, PUSH_CONCURRENCY)
711
+ .map(async ({ action, appConfig }) => {
712
+ const result = await (action === 'remove'
713
+ ? this.removeRemoteFile(appConfig, this.logWatchChanges)
714
+ : this.pushRemoteFile(appConfig, this.logWatchChanges));
715
+ this.onWatchChange?.(appConfig, action, result);
716
+ }));
717
+ }
718
+ catch (_err) {
719
+ //noop
720
+ }
679
721
  }
680
722
  // refetch app to get updated configs
681
723
  this.app = await this.getAppWithConfig(this.app.id);
@@ -723,19 +765,48 @@ export class PushAppCommand extends RemoteAppCommand {
723
765
  }
724
766
  fs.watch(this.appPath, { recursive: true }, this.watchListener);
725
767
  }
768
+ async startProxyServer(port) {
769
+ // Find an open port starting at 3000
770
+ const freePort = port ||
771
+ (await getPort({ port: portNumbers(3000, 3100) })) ||
772
+ DEV_SERVER_FALLBACK_PORT;
773
+ // Start proxy
774
+ const proxyUrl = await getProxyUrl(freePort);
775
+ await this.updateLocalProxy(proxyUrl, this.storefront?.id);
776
+ return freePort;
777
+ }
778
+ async updateLocalProxy(proxyUrl, storefrontId) {
779
+ const storefront = this.app.type === 'storefront' &&
780
+ (await this.getAppStorefront({ storefront_id: storefrontId }));
781
+ await this.handleRequestErrors(async () => this.api.put({ adminPath: `/client/apps/${this.app.id}/local-proxy` }, {
782
+ body: {
783
+ proxy_url: proxyUrl,
784
+ storefront_id: storefront?.id || null,
785
+ storefront_slug: storefront?.slug || null,
786
+ },
787
+ }));
788
+ }
726
789
  async wranglerDeployFrontend(projectType) {
727
790
  let deploymentUrl;
728
791
  let interactiveError = false;
792
+ let pagesProjectError = false;
729
793
  this.log(`\nDeploying to Cloudflare...\n`);
730
794
  try {
731
- await this.exec(`npx wrangler pages deploy ${this.appPath}/frontend/${projectType.deployPath}`, (string) => {
795
+ await this.execFrontend(`npx wrangler deploy`, (string) => {
732
796
  // Dependent on wrangler output
733
797
  // Parse the deployment URL from the wrangler output.
734
- const match = string.match(/Take a peek over at (http\S+)/);
798
+ const match = string.match(/^\s*(https:\/\/\S+\.workers\.dev)\s*[\s\S]*?Current Version ID: [\w-]+/m);
735
799
  if (match && match.length > 0) {
736
800
  deploymentUrl = match[1];
737
801
  return false;
738
802
  }
803
+ // Check for Pages project error
804
+ if (string.includes("It looks like you've run a Workers-specific command in a Pages project") ||
805
+ string.includes('please run `wrangler pages deploy` instead') ||
806
+ string.includes('Missing entry-point')) {
807
+ pagesProjectError = true;
808
+ return false;
809
+ }
739
810
  if (interactiveError ||
740
811
  string.includes('non-interactive mode') ||
741
812
  string.includes('non-interactive environment')) {
@@ -746,10 +817,16 @@ export class PushAppCommand extends RemoteAppCommand {
746
817
  }
747
818
  catch (error) {
748
819
  // 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);
820
+ if (pagesProjectError) {
821
+ this.error(style.funcWarn(`⚠️ Version incompatibility!\n` +
822
+ `Your project is configured for Cloudflare Pages, but Swell CLI v2.1.0+ uses Cloudflare Workers.\n\n` +
823
+ `To fix this, you can either:\n` +
824
+ `1. Update your app project (for Swell official apps) or reconfigure it for workers\n` +
825
+ ` (https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/)\n` +
826
+ `2. Use an older Swell CLI version: npm install -g @swell/cli@2.0.20\n`));
827
+ }
828
+ else if (interactiveError) {
829
+ 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
830
  }
754
831
  else {
755
832
  throw error;
@@ -194,9 +194,29 @@ export class RemoteAppCommand extends AppCommand {
194
194
  const appLabel = this.app.type === 'theme' ? 'theme' : 'app';
195
195
  let app = updateApp || {};
196
196
  if (!app.id) {
197
+ // May already have a dev instance
198
+ let existingDevApp;
199
+ try {
200
+ existingDevApp = await this.api.get({
201
+ adminPath: `/apps/${this.swellConfig.store.id}`,
202
+ });
203
+ // Check if existing dev app is owned by the current store
204
+ const currentStore = localConfig.getDefaultStore();
205
+ if (existingDevApp?.client_id !== currentStore) {
206
+ existingDevApp = null;
207
+ }
208
+ }
209
+ catch {
210
+ // noop
211
+ }
197
212
  spinner.start(`Creating ${appLabel}...`);
198
- app = await this.handleRequestErrors(async () => this.api.post({ adminPath: '/apps' }, { body }), () => spinner.fail('Error creating app.'));
199
- this.appCreated = true;
213
+ if (existingDevApp) {
214
+ app = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${existingDevApp.id}` }, { body }), () => spinner.fail('Error updating app.'));
215
+ }
216
+ else {
217
+ app = await this.handleRequestErrors(async () => this.api.post({ adminPath: '/apps' }, { body }), () => spinner.fail('Error creating app.'));
218
+ this.appCreated = true;
219
+ }
200
220
  }
201
221
  else if (app.id) {
202
222
  spinner.start(`Updating ${appLabel}...`);
@@ -296,7 +316,7 @@ export class RemoteAppCommand extends AppCommand {
296
316
  return this.api.post({ adminPath: `/client/apps` }, {
297
317
  body: { app_id: this.app.id, version },
298
318
  onAsyncGetPath: (response) => ({
299
- adminPath: `/client/app-async-status/${response.id}`,
319
+ adminPath: `/client/app-async-status/${response.app_id}`,
300
320
  }),
301
321
  spinner,
302
322
  });
@@ -584,7 +604,7 @@ export class RemoteAppCommand extends AppCommand {
584
604
  await this.api.put({ adminPath: `/client/apps/${this.app.id}` }, {
585
605
  body: { version },
586
606
  onAsyncGetPath: (response) => ({
587
- adminPath: `/client/app-async-status/${response.id}`,
607
+ adminPath: `/client/app-async-status/${response.app_id}`,
588
608
  }),
589
609
  spinner,
590
610
  });
@@ -349,7 +349,8 @@
349
349
  "examples": [
350
350
  "swell app dev",
351
351
  "swell app dev --storefront-id <id>",
352
- "swell app dev --port 3000"
352
+ "swell app dev --port 3000",
353
+ "swell app dev --port 3000 --frontend-port 4000"
353
354
  ],
354
355
  "flags": {
355
356
  "app-path": {
@@ -373,6 +374,13 @@
373
374
  "multiple": false,
374
375
  "type": "option"
375
376
  },
377
+ "frontend-port": {
378
+ "description": "specify the port for the frontend dev server when running with frontend",
379
+ "name": "frontend-port",
380
+ "hasDynamicHelp": false,
381
+ "multiple": false,
382
+ "type": "option"
383
+ },
376
384
  "storefront-id": {
377
385
  "description": "for storefront apps, identify a storefront to preview and push theme files to",
378
386
  "name": "storefront-id",
@@ -480,8 +488,11 @@
480
488
  "hasDynamicHelp": false,
481
489
  "multiple": false,
482
490
  "options": [
483
- "nextjs",
484
- "astro"
491
+ "astro",
492
+ "angular",
493
+ "hono",
494
+ "nuxt",
495
+ "nextjs"
485
496
  ],
486
497
  "type": "option"
487
498
  },
@@ -921,8 +932,11 @@
921
932
  "hasDynamicHelp": false,
922
933
  "multiple": false,
923
934
  "options": [
924
- "nextjs",
925
- "astro"
935
+ "astro",
936
+ "angular",
937
+ "hono",
938
+ "nuxt",
939
+ "nextjs"
926
940
  ],
927
941
  "type": "option"
928
942
  },
@@ -1100,8 +1114,11 @@
1100
1114
  "hasDynamicHelp": false,
1101
1115
  "multiple": false,
1102
1116
  "options": [
1103
- "nextjs",
1104
- "astro"
1117
+ "astro",
1118
+ "angular",
1119
+ "hono",
1120
+ "nuxt",
1121
+ "nextjs"
1105
1122
  ],
1106
1123
  "type": "option"
1107
1124
  },
@@ -1581,8 +1598,11 @@
1581
1598
  "hasDynamicHelp": false,
1582
1599
  "multiple": false,
1583
1600
  "options": [
1584
- "nextjs",
1585
- "astro"
1601
+ "astro",
1602
+ "angular",
1603
+ "hono",
1604
+ "nuxt",
1605
+ "nextjs"
1586
1606
  ],
1587
1607
  "type": "option"
1588
1608
  },
@@ -1830,7 +1850,7 @@
1830
1850
  "examples": [
1831
1851
  "swell app frontend dev",
1832
1852
  "swell app frontend dev --no-push",
1833
- "swell app frontend dev --port 3000",
1853
+ "swell app frontend dev --proxy-port 3000",
1834
1854
  "swell app frontend dev --storefront-select",
1835
1855
  "swell app frontend dev --storefront-id <id>"
1836
1856
  ],
@@ -1856,6 +1876,13 @@
1856
1876
  "multiple": false,
1857
1877
  "type": "option"
1858
1878
  },
1879
+ "frontend-port": {
1880
+ "description": "specify the port for the frontend dev server when running with frontend",
1881
+ "name": "frontend-port",
1882
+ "hasDynamicHelp": false,
1883
+ "multiple": false,
1884
+ "type": "option"
1885
+ },
1859
1886
  "storefront-id": {
1860
1887
  "description": "identify a storefront to preview with and push theme files to",
1861
1888
  "name": "storefront-id",
@@ -1868,6 +1895,20 @@
1868
1895
  "name": "storefront-select",
1869
1896
  "allowNo": false,
1870
1897
  "type": "boolean"
1898
+ },
1899
+ "proxy-port": {
1900
+ "description": "specify the port for an existing frontend proxy",
1901
+ "name": "proxy-port",
1902
+ "default": 3001,
1903
+ "hasDynamicHelp": false,
1904
+ "multiple": false,
1905
+ "type": "option"
1906
+ },
1907
+ "app-dev": {
1908
+ "description": "indicates frontend app is running in app dev mode",
1909
+ "name": "app-dev",
1910
+ "allowNo": false,
1911
+ "type": "boolean"
1871
1912
  }
1872
1913
  },
1873
1914
  "hasDynamicHelp": false,
@@ -1891,5 +1932,5 @@
1891
1932
  ]
1892
1933
  }
1893
1934
  },
1894
- "version": "2.0.19"
1935
+ "version": "2.1.0"
1895
1936
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swell/cli",
3
- "version": "2.0.19",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "Swell's command line interface/utility",
6
6
  "keywords": [
@@ -44,6 +44,7 @@
44
44
  "http-proxy": "1.18.1",
45
45
  "inflection": "3.0.0",
46
46
  "istextorbinary": "9.5.0",
47
+ "localtunnel": "2.0.2",
47
48
  "lodash": "4.17.21",
48
49
  "mime-detect": "1.2.0",
49
50
  "ngrok": "5.0.0-beta.2",
@@ -63,6 +64,7 @@
63
64
  "@types/configstore": "6.0.1",
64
65
  "@types/http-proxy": "1.17.16",
65
66
  "@types/inquirer": "9.0.6",
67
+ "@types/localtunnel": "2.0.4",
66
68
  "@types/mocha": "10.0.3",
67
69
  "@types/node": "20.8.8",
68
70
  "@types/qs": "6.9.15",