@swell/cli 2.3.0 → 2.3.2
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 +3 -0
- package/dist/commands/app/dev.js +39 -0
- package/dist/commands/app/frontend/dev.js +7 -3
- package/dist/commands/app/version.js +1 -1
- package/dist/lib/proxy.js +62 -5
- package/dist/push-app-command.d.ts +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +2 -1
|
@@ -16,7 +16,10 @@ export default class AppDev extends PushAppCommand {
|
|
|
16
16
|
functionErrors: Map<string, string>;
|
|
17
17
|
functionPorts: Map<string, number>;
|
|
18
18
|
tmpDir: string;
|
|
19
|
+
frontendPort: number | null;
|
|
20
|
+
isCleaningUp: boolean;
|
|
19
21
|
run(): Promise<void>;
|
|
22
|
+
private cleanupOnExit;
|
|
20
23
|
runAppFrontendDevIfApplicable(frontendPort?: number): Promise<number | void>;
|
|
21
24
|
private createFunctionRouter;
|
|
22
25
|
private createTmpDirectory;
|
package/dist/commands/app/dev.js
CHANGED
|
@@ -46,6 +46,10 @@ export default class AppDev extends PushAppCommand {
|
|
|
46
46
|
functionPorts = new Map();
|
|
47
47
|
// Directory for compiled function files and wrangler context
|
|
48
48
|
tmpDir = '';
|
|
49
|
+
// Port for frontend dev server (used for routing non-function requests)
|
|
50
|
+
frontendPort = null;
|
|
51
|
+
// Guard against multiple cleanup calls
|
|
52
|
+
isCleaningUp = false;
|
|
49
53
|
async run() {
|
|
50
54
|
const { flags } = await this.parse(AppDev);
|
|
51
55
|
const { port, 'frontend-port': frontendPort } = flags;
|
|
@@ -62,6 +66,21 @@ export default class AppDev extends PushAppCommand {
|
|
|
62
66
|
const serverPort = await this.startProxyServer(port);
|
|
63
67
|
await this.startAppFunctionServer(spinner, serverPort);
|
|
64
68
|
await this.runAppFrontendDevIfApplicable(frontendPort);
|
|
69
|
+
// Register cleanup handlers to clear local proxy on exit
|
|
70
|
+
process.on('SIGINT', this.cleanupOnExit.bind(this));
|
|
71
|
+
process.on('SIGTERM', this.cleanupOnExit.bind(this));
|
|
72
|
+
}
|
|
73
|
+
async cleanupOnExit() {
|
|
74
|
+
if (this.isCleaningUp)
|
|
75
|
+
return;
|
|
76
|
+
this.isCleaningUp = true;
|
|
77
|
+
try {
|
|
78
|
+
await this.updateLocalProxy(null, this.storefront?.id);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Ignore errors during cleanup
|
|
82
|
+
}
|
|
83
|
+
// Note: proxy.ts signal handlers will call process.exit()
|
|
65
84
|
}
|
|
66
85
|
async runAppFrontendDevIfApplicable(frontendPort) {
|
|
67
86
|
const projectType = this.getFrontendProjectType(false);
|
|
@@ -70,6 +89,8 @@ export default class AppDev extends PushAppCommand {
|
|
|
70
89
|
this.argv.push('--app-dev');
|
|
71
90
|
// add proxy-port flag (use provided port or auto-detect)
|
|
72
91
|
const proxyPort = frontendPort || (await getPort({ port: portNumbers(4000, 4100) }));
|
|
92
|
+
// Store frontend port for function router to use for fallback routing
|
|
93
|
+
this.frontendPort = proxyPort;
|
|
73
94
|
this.argv.push('--proxy-port', String(proxyPort));
|
|
74
95
|
this.config.runCommand('app:frontend:dev', this.argv);
|
|
75
96
|
return proxyPort;
|
|
@@ -111,6 +132,24 @@ export default class AppDev extends PushAppCommand {
|
|
|
111
132
|
});
|
|
112
133
|
req.pipe(proxyReq);
|
|
113
134
|
}
|
|
135
|
+
else if (this.frontendPort) {
|
|
136
|
+
// Proxy non-function requests to frontend dev server
|
|
137
|
+
const proxyReq = http.request({
|
|
138
|
+
hostname: 'localhost',
|
|
139
|
+
port: this.frontendPort,
|
|
140
|
+
path: req.url,
|
|
141
|
+
method: req.method,
|
|
142
|
+
headers: req.headers,
|
|
143
|
+
}, (proxyRes) => {
|
|
144
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
145
|
+
proxyRes.pipe(res);
|
|
146
|
+
});
|
|
147
|
+
proxyReq.on('error', (error) => {
|
|
148
|
+
res.writeHead(502);
|
|
149
|
+
res.end(`Frontend dev server error: ${error.message}`);
|
|
150
|
+
});
|
|
151
|
+
req.pipe(proxyReq);
|
|
152
|
+
}
|
|
114
153
|
else {
|
|
115
154
|
res.writeHead(404);
|
|
116
155
|
const functionError = this.functionErrors.get(functionName);
|
|
@@ -26,7 +26,7 @@ export default class AppFrontendDev extends PushAppCommand {
|
|
|
26
26
|
}),
|
|
27
27
|
'app-dev': Flags.boolean({
|
|
28
28
|
description: 'indicates frontend app is running in app dev mode',
|
|
29
|
-
default:
|
|
29
|
+
default: false,
|
|
30
30
|
}),
|
|
31
31
|
};
|
|
32
32
|
static orientation = {
|
|
@@ -57,7 +57,11 @@ export default class AppFrontendDev extends PushAppCommand {
|
|
|
57
57
|
if (!isAppDev) {
|
|
58
58
|
this.log(`Starting app dev server...\n`);
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
// When running as child of `swell app dev`, don't create a new tunnel.
|
|
61
|
+
// The parent command already created the tunnel and handles routing.
|
|
62
|
+
const serverPort = isAppDev && proxyPort
|
|
63
|
+
? proxyPort
|
|
64
|
+
: await this.startProxyServer(proxyPort || port);
|
|
61
65
|
await this.execFrontendProject(serverPort, isAppDev);
|
|
62
66
|
}
|
|
63
67
|
async execFrontendProject(serverPort, isAppDev) {
|
|
@@ -83,7 +87,7 @@ export default class AppFrontendDev extends PushAppCommand {
|
|
|
83
87
|
}
|
|
84
88
|
// 3. READY DETECTION: Detect server ready, show our message
|
|
85
89
|
if (!serverReadyDetected &&
|
|
86
|
-
(/✓ ready in \d
|
|
90
|
+
(/✓ ready in \d+/i.test(string) || /✓ starting/i.test(string))) {
|
|
87
91
|
serverReadyDetected = true;
|
|
88
92
|
this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
|
|
89
93
|
const sessionId = localConfig.getSessionId(currentStore);
|
|
@@ -168,7 +168,7 @@ behavior by using the --no-git-tag option.`;
|
|
|
168
168
|
spinner,
|
|
169
169
|
});
|
|
170
170
|
if (!versionResponse || !versionResponse.version) {
|
|
171
|
-
|
|
171
|
+
throw new Error('Something went wrong. Server could not create version.');
|
|
172
172
|
}
|
|
173
173
|
// update the local app config file since remote was successful
|
|
174
174
|
this.swellConfig.set('version', nextVersion);
|
package/dist/lib/proxy.js
CHANGED
|
@@ -1,8 +1,62 @@
|
|
|
1
|
+
import { bin, install, Tunnel } from 'cloudflared';
|
|
1
2
|
import localtunnel from 'localtunnel';
|
|
2
3
|
import ngrok from 'ngrok';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import ora from 'ora';
|
|
3
6
|
import { LOCAL_PROXY_PROVIDER } from './constants.js';
|
|
7
|
+
let activeTunnel = null;
|
|
8
|
+
let cleanupRegistered = false;
|
|
9
|
+
async function startCloudflaredTunnel(port) {
|
|
10
|
+
// Stop existing tunnel if called again
|
|
11
|
+
if (activeTunnel) {
|
|
12
|
+
activeTunnel.stop();
|
|
13
|
+
activeTunnel = null;
|
|
14
|
+
}
|
|
15
|
+
// Ensure cloudflared binary is installed
|
|
16
|
+
if (!fs.existsSync(bin)) {
|
|
17
|
+
const spinner = ora('Installing cloudflared tunnel (first time only)...').start();
|
|
18
|
+
await install(bin);
|
|
19
|
+
spinner.stop();
|
|
20
|
+
}
|
|
21
|
+
// Use Tunnel.quick() for quick tunnels without a Cloudflare account
|
|
22
|
+
const tunnel = Tunnel.quick(`http://localhost:${port}`);
|
|
23
|
+
activeTunnel = tunnel;
|
|
24
|
+
// Wait for the URL to be available
|
|
25
|
+
const url = await new Promise((resolve, reject) => {
|
|
26
|
+
const timeout = setTimeout(() => {
|
|
27
|
+
tunnel.stop();
|
|
28
|
+
activeTunnel = null;
|
|
29
|
+
reject(new Error('Timeout waiting for cloudflared tunnel URL. Check if cloudflared can connect to Cloudflare.'));
|
|
30
|
+
}, 60000);
|
|
31
|
+
tunnel.on('url', (tunnelUrl) => {
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
resolve(tunnelUrl);
|
|
34
|
+
});
|
|
35
|
+
tunnel.on('error', (error) => {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
activeTunnel = null;
|
|
38
|
+
reject(error);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
// Register cleanup handlers only once, referencing activeTunnel for current tunnel
|
|
42
|
+
if (!cleanupRegistered) {
|
|
43
|
+
cleanupRegistered = true;
|
|
44
|
+
process.on('exit', () => activeTunnel?.stop());
|
|
45
|
+
process.on('SIGINT', () => {
|
|
46
|
+
activeTunnel?.stop();
|
|
47
|
+
// eslint-disable-next-line no-process-exit
|
|
48
|
+
process.exit();
|
|
49
|
+
});
|
|
50
|
+
process.on('SIGTERM', () => {
|
|
51
|
+
activeTunnel?.stop();
|
|
52
|
+
// eslint-disable-next-line no-process-exit
|
|
53
|
+
process.exit();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
4
58
|
export async function getProxyUrl(port) {
|
|
5
|
-
const provider = LOCAL_PROXY_PROVIDER || '
|
|
59
|
+
const provider = LOCAL_PROXY_PROVIDER || 'cloudflared';
|
|
6
60
|
try {
|
|
7
61
|
switch (provider) {
|
|
8
62
|
case 'local': {
|
|
@@ -14,16 +68,19 @@ export async function getProxyUrl(port) {
|
|
|
14
68
|
region: 'us', // TODO: make it configurable
|
|
15
69
|
});
|
|
16
70
|
}
|
|
17
|
-
|
|
18
|
-
case 'localtunnel':
|
|
19
|
-
default: {
|
|
71
|
+
case 'localtunnel': {
|
|
20
72
|
const tunnel = await localtunnel({ port });
|
|
21
73
|
return tunnel.url;
|
|
22
74
|
}
|
|
75
|
+
// eslint-disable-next-line unicorn/no-useless-switch-case
|
|
76
|
+
case 'cloudflared':
|
|
77
|
+
default: {
|
|
78
|
+
return await startCloudflaredTunnel(port);
|
|
79
|
+
}
|
|
23
80
|
}
|
|
24
81
|
}
|
|
25
82
|
catch (error) {
|
|
26
|
-
console.
|
|
83
|
+
console.error(error);
|
|
27
84
|
throw new Error(`Unable to start tunnel on port ${port} (${provider}): ${error.message}`);
|
|
28
85
|
}
|
|
29
86
|
}
|
|
@@ -80,7 +80,7 @@ export declare abstract class PushAppCommand extends RemoteAppCommand {
|
|
|
80
80
|
setWatchingFiles(): Promise<void>;
|
|
81
81
|
startProxyServer(port?: number): Promise<number>;
|
|
82
82
|
updateFrontendDeployment(currentStore: string, projectType: FrontendProjectType, deploymentUrl: string, deploymentHash: string): Promise<void>;
|
|
83
|
-
updateLocalProxy(proxyUrl: string, storefrontId?: string): Promise<void>;
|
|
83
|
+
updateLocalProxy(proxyUrl: string | null, storefrontId?: string): Promise<void>;
|
|
84
84
|
watchForChanges({ logChanges, onChange, syncAll, }?: {
|
|
85
85
|
logChanges?: boolean;
|
|
86
86
|
onChange?: (appConfig?: AppConfig, result?: any) => void;
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swell/cli",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Swell's command line interface/utility",
|
|
6
6
|
"keywords": [
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"ajv-formats": "3.0.1",
|
|
38
38
|
"blake3-wasm": "2.1.5",
|
|
39
39
|
"chalk": "5.3.0",
|
|
40
|
+
"cloudflared": "0.7.1",
|
|
40
41
|
"conf": "11.0.2",
|
|
41
42
|
"configstore": "6.0.0",
|
|
42
43
|
"esbuild": "0.19.5",
|