@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.
- package/dist/commands/app/dev.d.ts +16 -2
- package/dist/commands/app/dev.js +359 -3
- package/dist/commands/app/frontend/dev.d.ts +7 -6
- package/dist/commands/app/frontend/dev.js +123 -60
- package/dist/create-app-command.d.ts +4 -0
- package/dist/create-app-command.js +161 -1
- package/dist/env/local.d.ts +1 -0
- package/dist/env/local.js +1 -0
- package/dist/env/production.d.ts +1 -0
- package/dist/env/production.js +1 -0
- package/dist/env/review.d.ts +1 -0
- package/dist/env/review.js +1 -0
- package/dist/env/staging.d.ts +1 -0
- package/dist/env/staging.js +1 -0
- package/dist/lib/apps/app-config.d.ts +49 -0
- package/dist/lib/apps/app-config.js +21 -14
- package/dist/lib/apps/index.d.ts +3 -4
- package/dist/lib/apps/index.js +102 -19
- package/dist/lib/bundle.js +2 -1
- package/dist/lib/constants.d.ts +1 -4
- package/dist/lib/constants.js +5 -4
- package/dist/lib/proxy.d.ts +1 -0
- package/dist/lib/proxy.js +29 -1
- package/dist/lib/swell-function-wrapper.d.ts +23 -4
- package/dist/lib/swell-function-wrapper.js +11 -4
- package/dist/push-app-command.d.ts +8 -4
- package/dist/push-app-command.js +100 -23
- package/oclif.manifest.json +52 -11
- package/package.json +3 -1
package/dist/lib/apps/index.d.ts
CHANGED
|
@@ -117,11 +117,10 @@ export declare enum ConfigInputFields {
|
|
|
117
117
|
VERSION = "version"
|
|
118
118
|
}
|
|
119
119
|
export interface FrontendProjectType {
|
|
120
|
-
buildCommand
|
|
121
|
-
configFileName: string;
|
|
122
|
-
deployPath: string;
|
|
120
|
+
buildCommand?: string;
|
|
123
121
|
devCommand: string;
|
|
124
|
-
installCommand
|
|
122
|
+
installCommand?: string;
|
|
123
|
+
mainPackage: string;
|
|
125
124
|
name: string;
|
|
126
125
|
slug: string;
|
|
127
126
|
}
|
package/dist/lib/apps/index.js
CHANGED
|
@@ -94,39 +94,122 @@ export var ConfigInputFields;
|
|
|
94
94
|
ConfigInputFields["VALUES"] = "values";
|
|
95
95
|
ConfigInputFields["VERSION"] = "version";
|
|
96
96
|
})(ConfigInputFields || (ConfigInputFields = {}));
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
+
mainPackage: 'astro',
|
|
113
104
|
name: 'Astro',
|
|
114
105
|
slug: 'astro',
|
|
115
106
|
},
|
|
116
107
|
];
|
|
117
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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) {
|
package/dist/lib/bundle.js
CHANGED
|
@@ -11,7 +11,8 @@ export async function bundleFunction(filePath) {
|
|
|
11
11
|
metafile: true,
|
|
12
12
|
platform: 'node',
|
|
13
13
|
target: ['chrome58'],
|
|
14
|
-
write: false,
|
|
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
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
export declare const API_BASE_URL: any;
|
|
2
|
-
export declare const
|
|
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;
|
package/dist/lib/constants.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
9
|
+
const LOGIN_HOST = env.get('LOGIN_HOST') || defaultEnv.LOGIN_HOST;
|
|
10
10
|
// url host for app frontend
|
|
11
|
-
|
|
11
|
+
const APP_PAGES_HOST = env.get('APP_PAGES_HOST') || defaultEnv.APP_PAGES_HOST;
|
|
12
12
|
// url host for storefront app frontend
|
|
13
|
-
|
|
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);
|
package/dist/lib/proxy.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getProxyUrl(port: number): Promise<string>;
|
package/dist/lib/proxy.js
CHANGED
|
@@ -1 +1,29 @@
|
|
|
1
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/push-app-command.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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.
|
|
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(
|
|
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 (
|
|
750
|
-
this.
|
|
751
|
-
|
|
752
|
-
|
|
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;
|