@swell/cli 2.0.20 → 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.
@@ -117,11 +117,10 @@ export declare enum ConfigInputFields {
117
117
  VERSION = "version"
118
118
  }
119
119
  export interface FrontendProjectType {
120
- buildCommand: string;
121
- configFileName: string;
122
- deployPath: string;
120
+ buildCommand?: string;
123
121
  devCommand: string;
124
- installCommand: string;
122
+ installCommand?: string;
123
+ mainPackage: string;
125
124
  name: string;
126
125
  slug: string;
127
126
  }
@@ -94,39 +94,122 @@ export var ConfigInputFields;
94
94
  ConfigInputFields["VALUES"] = "values";
95
95
  ConfigInputFields["VERSION"] = "version";
96
96
  })(ConfigInputFields || (ConfigInputFields = {}));
97
- export const FrontendProjectTypes = [
98
- {
99
- buildCommand: 'npx @cloudflare/next-on-pages',
100
- configFileName: 'next.config',
101
- deployPath: '.vercel/output/static',
102
- devCommand: 'npx next dev --port ${PORT}',
103
- installCommand: 'npx create-next-app@latest frontend --typescript --use-npm --src-dir --app --eslint --import-alias "@/*" --tailwind',
104
- name: 'Next.js',
105
- slug: 'nextjs',
106
- },
97
+ // Legacy apps (pre-workspace) - Astro only (Proxima, Sunrise)
98
+ // Uses direct commands without workspace prefix
99
+ const LegacyFrontendProjectTypes = [
107
100
  {
108
101
  buildCommand: 'npx astro build',
109
- configFileName: 'astro.config',
110
- deployPath: 'dist',
111
102
  devCommand: 'npx astro dev --port ${PORT}',
112
- installCommand: 'npm create astro@latest frontend -- --install --no-git --yes --skip-houston --typescript strict',
103
+ mainPackage: 'astro',
113
104
  name: 'Astro',
114
105
  slug: 'astro',
115
106
  },
116
107
  ];
117
- const frontendProjectConfigExtensions = ['.mjs', '.js', '.ts', '.cjs'];
108
+ // Modern apps (workspace-based) - All frameworks
109
+ export const FrontendProjectTypes = [
110
+ {
111
+ buildCommand: 'npm exec --workspace=frontend -- astro build',
112
+ devCommand: 'npm exec --workspace=frontend -- astro dev --port ${PORT}',
113
+ installCommand: 'npm create cloudflare@latest -- frontend --framework=astro --deploy=false --git=false -- --no-git --yes --skip-houston --typescript strict',
114
+ mainPackage: 'astro',
115
+ name: 'Astro',
116
+ slug: 'astro',
117
+ },
118
+ {
119
+ buildCommand: 'npm exec --workspace=frontend -- ng build',
120
+ devCommand: 'npm exec --workspace=frontend -- ng serve --port ${PORT}',
121
+ installCommand: 'npm create cloudflare@latest -- frontend --framework=angular --deploy=false --git=false -- --style=sass --zoneless --ai-config=none',
122
+ mainPackage: '@angular/core',
123
+ name: 'Angular',
124
+ slug: 'angular',
125
+ },
126
+ {
127
+ devCommand: 'npm exec --workspace=frontend -- wrangler dev --port ${PORT}',
128
+ installCommand: 'npm create cloudflare@latest -- frontend --framework=hono --deploy=false --git=false',
129
+ mainPackage: 'hono',
130
+ name: 'Hono',
131
+ slug: 'hono',
132
+ },
133
+ {
134
+ buildCommand: 'npm exec --workspace=frontend -- nuxt build',
135
+ devCommand: 'npm exec --workspace=frontend -- nuxt dev --port ${PORT}',
136
+ installCommand: 'npm create cloudflare@latest -- frontend --framework=nuxt --deploy=false --git=false -- --no-modules -f',
137
+ mainPackage: 'nuxt',
138
+ name: 'Nuxt',
139
+ slug: 'nuxt',
140
+ },
141
+ {
142
+ buildCommand: 'npm exec --workspace=frontend -- opennextjs-cloudflare build',
143
+ //devCommand: 'npm exec --workspace=frontend -- opennextjs-cloudflare build && npm exec --workspace=frontend -- opennextjs-cloudflare preview --port=${PORT}',
144
+ devCommand: 'npm exec --workspace=frontend -- next dev --turbopack --port ${PORT}',
145
+ installCommand: 'npm create cloudflare@latest -- frontend --framework=next --deploy=false --git=false -- --typescript --use-npm --src-dir --app --eslint --import-alias "@/*" --tailwind --turbopack',
146
+ mainPackage: 'next',
147
+ name: 'Next.js',
148
+ slug: 'nextjs',
149
+ },
150
+ ];
118
151
  export function getAppSlugId(app) {
119
152
  return toAppId(app.private_id) || app.public_id || app.id;
120
153
  }
154
+ function hasWorkspaceStructure(appPath) {
155
+ // Check if root package.json has workspaces field including 'frontend'
156
+ const rootPkgPath = path.join(appPath, 'package.json');
157
+ if (!filePathExists(rootPkgPath)) {
158
+ return false;
159
+ }
160
+ try {
161
+ const content = fs.readFileSync(rootPkgPath, 'utf-8');
162
+ const pkg = JSON.parse(content);
163
+ return Array.isArray(pkg.workspaces) && pkg.workspaces.includes('frontend');
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ }
121
169
  export function getFrontendProjectType(appPath) {
122
- for (const projectType of FrontendProjectTypes) {
123
- for (const extension of frontendProjectConfigExtensions) {
124
- const configFile = path.join(appPath, 'frontend', `${projectType.configFileName}${extension}`);
125
- if (filePathExists(configFile)) {
126
- return projectType;
170
+ // Detect workspace structure and select appropriate config
171
+ const hasWorkspace = hasWorkspaceStructure(appPath);
172
+ const projectTypes = hasWorkspace
173
+ ? FrontendProjectTypes
174
+ : LegacyFrontendProjectTypes;
175
+ // Try frontend/package.json first (new workspace structure), then root package.json (old structure)
176
+ const pkgPaths = [
177
+ path.join(appPath, 'frontend', 'package.json'),
178
+ path.join(appPath, 'package.json'),
179
+ ];
180
+ for (const pkgPath of pkgPaths) {
181
+ if (!filePathExists(pkgPath)) {
182
+ continue;
183
+ }
184
+ try {
185
+ const content = fs.readFileSync(pkgPath, 'utf-8');
186
+ const pkg = JSON.parse(content);
187
+ for (const projectType of projectTypes) {
188
+ if (pkg.dependencies?.[projectType.mainPackage] ||
189
+ pkg.devDependencies?.[projectType.mainPackage]) {
190
+ // Create a copy to avoid mutating the original
191
+ const detectedType = { ...projectType };
192
+ // If buildCommand not explicitly set in framework definition, detect it
193
+ if (detectedType.buildCommand === undefined) {
194
+ // Check if package.json has a "build" script
195
+ if (pkg.scripts?.build) {
196
+ detectedType.buildCommand = 'npm run build';
197
+ }
198
+ else {
199
+ // No build script found, set to empty string (no build needed)
200
+ detectedType.buildCommand = '';
201
+ }
202
+ }
203
+ return detectedType;
204
+ }
127
205
  }
128
206
  }
207
+ catch {
208
+ // Ignore JSON parse errors, try next path
209
+ continue;
210
+ }
129
211
  }
212
+ return undefined;
130
213
  }
131
214
  export function getConfigTypeFromPath(path) {
132
215
  for (const type in ConfigPaths) {
@@ -11,7 +11,8 @@ export async function bundleFunction(filePath) {
11
11
  metafile: true,
12
12
  platform: 'node',
13
13
  target: ['chrome58'],
14
- write: false, // Do not write the output to a file
14
+ write: false,
15
+ logLevel: 'silent', // Do not output compile errors
15
16
  });
16
17
  const { metafile: { inputs }, outputFiles: [{ text: origCode }], } = buildResult;
17
18
  // TODO: make a separate method to get dependencies for watch command
@@ -1,8 +1,5 @@
1
1
  export declare const API_BASE_URL: any;
2
- export declare const ADMIN_API_BASE_URL: any;
3
- export declare const LOGIN_HOST: any;
4
- export declare const APP_PAGES_HOST: any;
5
- export declare const STOREFRONT_FRONTEND_HOST: any;
2
+ export declare const LOCAL_PROXY_PROVIDER: any;
6
3
  export declare function getAdminApiBaseUrl(storeId: string): any;
7
4
  export declare function getLoginHost(storeId?: string): any;
8
5
  export declare function getAppFrontendHost(storeId: string, installedAppId: string): any;
@@ -4,15 +4,16 @@ import { env } from './config.js';
4
4
  // url for backend api calls
5
5
  export const API_BASE_URL = env.get('API_BASE_URL') || defaultEnv.API_BASE_URL;
6
6
  // url for admin api calls
7
- export const ADMIN_API_BASE_URL = env.get('ADMIN_API_BASE_URL') || defaultEnv.ADMIN_API_BASE_URL;
7
+ const ADMIN_API_BASE_URL = env.get('ADMIN_API_BASE_URL') || defaultEnv.ADMIN_API_BASE_URL;
8
8
  // url host for admin login
9
- export const LOGIN_HOST = env.get('LOGIN_HOST') || defaultEnv.LOGIN_HOST;
9
+ const LOGIN_HOST = env.get('LOGIN_HOST') || defaultEnv.LOGIN_HOST;
10
10
  // url host for app frontend
11
- export const APP_PAGES_HOST = env.get('APP_PAGES_HOST') || defaultEnv.APP_PAGES_HOST;
11
+ const APP_PAGES_HOST = env.get('APP_PAGES_HOST') || defaultEnv.APP_PAGES_HOST;
12
12
  // url host for storefront app frontend
13
- export const STOREFRONT_FRONTEND_HOST = env.get('STOREFRONT_FRONTEND_HOST') ||
13
+ const STOREFRONT_FRONTEND_HOST = env.get('STOREFRONT_FRONTEND_HOST') ||
14
14
  env.get('STOREFRONT_PAGES_HOST') || // Deprecated
15
15
  defaultEnv.STOREFRONT_FRONTEND_HOST;
16
+ export const LOCAL_PROXY_PROVIDER = env.get('LOCAL_PROXY_PROVIDER') || defaultEnv.LOCAL_PROXY_PROVIDER;
16
17
  // helpers to get constants with replacements
17
18
  export function getAdminApiBaseUrl(storeId) {
18
19
  return ADMIN_API_BASE_URL.replace('${STORE_ID}', storeId);
@@ -0,0 +1 @@
1
+ export declare function getProxyUrl(port: number): Promise<string>;
package/dist/lib/proxy.js CHANGED
@@ -1 +1,29 @@
1
- "use strict";
1
+ import localtunnel from 'localtunnel';
2
+ import ngrok from 'ngrok';
3
+ import { LOCAL_PROXY_PROVIDER } from './constants.js';
4
+ export async function getProxyUrl(port) {
5
+ const provider = LOCAL_PROXY_PROVIDER || 'localtunnel';
6
+ try {
7
+ switch (provider) {
8
+ case 'local': {
9
+ return `http://localhost:${port}`;
10
+ }
11
+ case 'ngrok': {
12
+ return await ngrok.connect({
13
+ addr: port,
14
+ region: 'us', // TODO: make it configurable
15
+ });
16
+ }
17
+ // eslint-disable-next-line unicorn/no-useless-switch-case
18
+ case 'localtunnel':
19
+ default: {
20
+ const tunnel = await localtunnel({ port });
21
+ return tunnel.url;
22
+ }
23
+ }
24
+ }
25
+ catch (error) {
26
+ console.log(error);
27
+ throw new Error(`Unable to start tunnel on port ${port} (${provider}): ${error.message}`);
28
+ }
29
+ }
@@ -48,10 +48,28 @@ declare function executeModuleHandler(req: SwellRequest, context: Event): Promis
48
48
  * @returns {boolean}
49
49
  */
50
50
  declare function isOrdinaryObject(val: any): boolean;
51
- declare const origialConsoleLog: {
52
- (...data: any[]): void;
53
- (message?: any, ...optionalParams: any[]): void;
54
- };
51
+ declare namespace originalConsole {
52
+ let log: {
53
+ (...data: any[]): void;
54
+ (message?: any, ...optionalParams: any[]): void;
55
+ };
56
+ let info: {
57
+ (...data: any[]): void;
58
+ (message?: any, ...optionalParams: any[]): void;
59
+ };
60
+ let debug: {
61
+ (...data: any[]): void;
62
+ (message?: any, ...optionalParams: any[]): void;
63
+ };
64
+ let warn: {
65
+ (...data: any[]): void;
66
+ (message?: any, ...optionalParams: any[]): void;
67
+ };
68
+ let error: {
69
+ (...data: any[]): void;
70
+ (message?: any, ...optionalParams: any[]): void;
71
+ };
72
+ }
55
73
  /**
56
74
  * Class representing a Swell request.
57
75
  */
@@ -68,6 +86,7 @@ declare class SwellRequest {
68
86
  logParams: any;
69
87
  apiHost: any;
70
88
  id: any;
89
+ isLocalDev: boolean;
71
90
  swell: SwellAPI;
72
91
  body: {};
73
92
  query: {};
@@ -1,5 +1,11 @@
1
1
  "use strict";
2
- const origialConsoleLog = console.log;
2
+ const originalConsole = {
3
+ log: console.log,
4
+ info: console.info,
5
+ debug: console.debug,
6
+ warn: console.warn,
7
+ error: console.error,
8
+ };
3
9
  addEventListener('fetch', (event) => {
4
10
  event.respondWith(request(event.request, event.env, event));
5
11
  });
@@ -93,6 +99,8 @@ class SwellRequest {
93
99
  this.logParams = this.parseJson(req.headers.get('Swell-Request-Log'));
94
100
  this.apiHost = req.headers.get('Swell-API-Host') || 'https://api.schema.io';
95
101
  this.id = req.headers.get('Swell-Request-ID') || this.logParams?.req_id;
102
+ // Check if the request is from a local development environment
103
+ this.isLocalDev = req.headers.get('Swell-Local-Dev') === 'true';
96
104
  // Swell client
97
105
  this.swell = new SwellAPI(this, context);
98
106
  // URL of the original request
@@ -142,7 +150,7 @@ class SwellRequest {
142
150
  }
143
151
  }
144
152
  log(level, ...line) {
145
- origialConsoleLog(...line);
153
+ originalConsole[level]?.(...line);
146
154
  this._logs.push({
147
155
  date: Date.now(),
148
156
  line: line.map((l) => (l instanceof Error ? l.stack : JSON.stringify(l))),
@@ -150,7 +158,7 @@ class SwellRequest {
150
158
  });
151
159
  }
152
160
  getIngestableLogs(response) {
153
- if (!this.logParams) {
161
+ if (!this.logParams || this.isLocalDev) {
154
162
  return;
155
163
  }
156
164
  if (this.logParams.$start) {
@@ -384,4 +392,3 @@ function isOrdinaryObject(val) {
384
392
  val !== null &&
385
393
  Object.getPrototypeOf(val) === Object.prototype);
386
394
  }
387
- ;
@@ -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
+ execFrontend(command: string, onOutput?: (string: string) => any | false): Promise<void>;
30
+ exec(command: string, cwd?: string, onOutput?: (string: string) => any | false): Promise<void>;
29
31
  getAllAppStorefronts(params?: {
30
32
  type?: string;
31
33
  }): Promise<any>;
@@ -79,9 +81,11 @@ export declare abstract class PushAppCommand extends RemoteAppCommand {
79
81
  updateFrontendDeployment(currentStore: string, projectType: FrontendProjectType, deploymentUrl: string, deploymentHash: string): Promise<void>;
80
82
  watchForChanges({ logChanges, onChange, syncAll, }?: {
81
83
  logChanges?: boolean;
82
- onChange?: () => void;
84
+ onChange?: (appConfig?: AppConfig, result?: any) => void;
83
85
  syncAll?: boolean;
84
86
  }): Promise<void>;
87
+ startProxyServer(port?: number): Promise<number>;
88
+ updateLocalProxy(proxyUrl: string, storefrontId?: string): Promise<void>;
85
89
  wranglerDeployFrontend(projectType: FrontendProjectType): Promise<string>;
86
90
  private confirmRemoveInstalledApp;
87
91
  }
@@ -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;