@swell/cli 2.0.14 → 2.0.16
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/theme/dev.js +9 -23
- package/dist/lib/apps/index.js +1 -1
- package/dist/lib/proxy.d.ts +0 -0
- package/dist/lib/proxy.js +1 -0
- package/dist/lib/theme-sync.d.ts +19 -0
- package/dist/lib/theme-sync.js +144 -0
- package/oclif.manifest.json +1 -1
- package/package.json +6 -4
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import BrowserSync from 'browser-sync';
|
|
3
2
|
import { findUp } from 'find-up';
|
|
4
|
-
import getPort, { portNumbers } from 'get-port';
|
|
5
3
|
import { execSync, spawn } from 'node:child_process';
|
|
6
4
|
import fs from 'node:fs';
|
|
7
5
|
import ora from 'ora';
|
|
8
6
|
import { default as localConfig } from '../../lib/config.js';
|
|
9
7
|
import style from '../../lib/style.js';
|
|
10
8
|
import { PushAppCommand } from '../../push-app-command.js';
|
|
11
|
-
|
|
9
|
+
import { ThemeSync } from '../../lib/theme-sync.js';
|
|
12
10
|
export default class AppThemeDev extends PushAppCommand {
|
|
13
11
|
appType = 'theme';
|
|
14
12
|
static examples = [
|
|
@@ -140,38 +138,26 @@ export default class AppThemeDev extends PushAppCommand {
|
|
|
140
138
|
}
|
|
141
139
|
async startThemeDevServer(local) {
|
|
142
140
|
const currentStore = localConfig.getDefaultStore();
|
|
143
|
-
const sessionId = localConfig.getSessionId(currentStore);
|
|
144
141
|
const spinner = ora();
|
|
145
142
|
const bundle = process.argv.includes('--bundle');
|
|
146
143
|
if (bundle) {
|
|
147
144
|
await this.findAndInstallDependencies();
|
|
148
145
|
await this.findAndTryScriptCommands();
|
|
149
146
|
}
|
|
150
|
-
|
|
151
|
-
const freePort = (await getPort({ port: portNumbers(3000, 3100) })) || FALLBACK_PORT;
|
|
152
|
-
spinner.start(`Starting theme dev server on port ${freePort}...`);
|
|
147
|
+
spinner.start(`Starting theme dev server...`);
|
|
153
148
|
const storefrontUrl = local
|
|
154
149
|
? this.storefrontLocalUrl
|
|
155
150
|
: this.storefrontFrontendUrl(currentStore, this.storefront, undefined, true);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
notify: false,
|
|
160
|
-
open: false,
|
|
161
|
-
proxy: {
|
|
162
|
-
proxyReq: [
|
|
163
|
-
(proxyReq) => {
|
|
164
|
-
proxyReq.setHeader('swell-request-id', `${currentStore}_${Date.now()}`);
|
|
165
|
-
},
|
|
166
|
-
],
|
|
167
|
-
target: storefrontUrl,
|
|
168
|
-
},
|
|
169
|
-
ui: false,
|
|
151
|
+
// Create and start theme sync
|
|
152
|
+
const themeSync = new ThemeSync({
|
|
153
|
+
targetUrl: storefrontUrl,
|
|
170
154
|
});
|
|
155
|
+
const port = await themeSync.start();
|
|
156
|
+
spinner.succeed(`Theme sync server started on port ${port}\n`);
|
|
171
157
|
await this.watchForChanges({
|
|
172
|
-
onChange: () =>
|
|
158
|
+
onChange: () => themeSync.reload(),
|
|
173
159
|
});
|
|
174
160
|
spinner.stop();
|
|
175
|
-
this.log(`Watching for changes. View your theme at ${style.link(`http://localhost:${
|
|
161
|
+
this.log(`Watching for changes. View your theme at ${style.link(`http://localhost:${port}`)}\n`);
|
|
176
162
|
}
|
|
177
163
|
}
|
package/dist/lib/apps/index.js
CHANGED
|
@@ -114,7 +114,7 @@ export const FrontendProjectTypes = [
|
|
|
114
114
|
slug: 'astro',
|
|
115
115
|
},
|
|
116
116
|
];
|
|
117
|
-
const frontendProjectConfigExtensions = ['.mjs', '.js'];
|
|
117
|
+
const frontendProjectConfigExtensions = ['.mjs', '.js', '.ts', '.cjs'];
|
|
118
118
|
export function getAppSlugId(app) {
|
|
119
119
|
return toAppId(app.private_id) || app.public_id || app.id;
|
|
120
120
|
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface ThemeSyncOptions {
|
|
2
|
+
port?: number;
|
|
3
|
+
targetUrl: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class ThemeSync {
|
|
6
|
+
private readonly server;
|
|
7
|
+
private readonly proxy;
|
|
8
|
+
private readonly wss;
|
|
9
|
+
private readonly options;
|
|
10
|
+
private selectPort;
|
|
11
|
+
constructor(options: ThemeSyncOptions);
|
|
12
|
+
start(): Promise<number>;
|
|
13
|
+
reload(): void;
|
|
14
|
+
private parseRequestBody;
|
|
15
|
+
private decodeRequestBody;
|
|
16
|
+
private shouldInjectScript;
|
|
17
|
+
private injectScript;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import getPort, { portNumbers } from 'get-port';
|
|
3
|
+
import util from 'node:util';
|
|
4
|
+
import zlib from 'node:zlib';
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
import httpProxy from 'http-proxy';
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
9
|
+
const gunzipAsync = util.promisify(zlib.gunzip);
|
|
10
|
+
const inflateAsync = util.promisify(zlib.inflate);
|
|
11
|
+
const brotliDecompressAsync = util.promisify(zlib.brotliDecompress);
|
|
12
|
+
const FALLBACK_PORT = 3000;
|
|
13
|
+
export class ThemeSync {
|
|
14
|
+
server;
|
|
15
|
+
proxy;
|
|
16
|
+
wss;
|
|
17
|
+
options;
|
|
18
|
+
async selectPort() {
|
|
19
|
+
return (await getPort({ port: portNumbers(3000, 3100) })) || FALLBACK_PORT;
|
|
20
|
+
}
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.server = http.createServer();
|
|
24
|
+
this.proxy = httpProxy.createProxyServer({});
|
|
25
|
+
this.wss = new WebSocketServer({ server: this.server });
|
|
26
|
+
}
|
|
27
|
+
async start() {
|
|
28
|
+
// Select port if not specified
|
|
29
|
+
if (!this.options.port) {
|
|
30
|
+
this.options.port = await this.selectPort();
|
|
31
|
+
}
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
// Create proxy server
|
|
34
|
+
this.server.on('request', (req, res) => {
|
|
35
|
+
this.proxy.web(req, res, {
|
|
36
|
+
target: this.options.targetUrl,
|
|
37
|
+
changeOrigin: true,
|
|
38
|
+
selfHandleResponse: true,
|
|
39
|
+
}, (err) => {
|
|
40
|
+
if (err) {
|
|
41
|
+
console.error('Proxy error:', err);
|
|
42
|
+
if (res instanceof http.ServerResponse) {
|
|
43
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
44
|
+
res.end('Proxy error');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
this.proxy.on('proxyReq', (proxyReq) => {
|
|
50
|
+
proxyReq.setHeader('swell-request-id', `${Date.now()}`);
|
|
51
|
+
});
|
|
52
|
+
this.proxy.on('proxyRes', (proxyRes, req, res) => {
|
|
53
|
+
let body = Buffer.from('');
|
|
54
|
+
proxyRes.on('data', (chunk) => {
|
|
55
|
+
body = Buffer.concat([body, chunk]);
|
|
56
|
+
});
|
|
57
|
+
// Preserve headers
|
|
58
|
+
for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) {
|
|
59
|
+
const header = proxyRes.rawHeaders[i];
|
|
60
|
+
res.setHeader(header, proxyRes.rawHeaders[i + 1]);
|
|
61
|
+
}
|
|
62
|
+
// Use utf8 encoding for the response
|
|
63
|
+
res.setHeader('Content-Encoding', 'utf8');
|
|
64
|
+
proxyRes.on('end', async () => {
|
|
65
|
+
const html = await this.parseRequestBody(proxyRes, body);
|
|
66
|
+
if (this.shouldInjectScript(proxyRes, html)) {
|
|
67
|
+
const modifiedHtml = this.injectScript(html, `localhost:${this.options.port}`);
|
|
68
|
+
res.end(modifiedHtml);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
res.end(html);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
// Handle proxy errors
|
|
76
|
+
this.proxy.on('error', (err) => {
|
|
77
|
+
console.error('Proxy error:', err);
|
|
78
|
+
});
|
|
79
|
+
// Start server
|
|
80
|
+
this.server.listen(this.options.port, () => {
|
|
81
|
+
resolve(this.options.port);
|
|
82
|
+
});
|
|
83
|
+
this.server.on('error', (err) => {
|
|
84
|
+
reject(err);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
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
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async parseRequestBody(proxyRes, body) {
|
|
97
|
+
// May contain multiple encodings
|
|
98
|
+
const contentEncoding = String(proxyRes.headers['content-encoding'] ?? '')
|
|
99
|
+
.split(',')
|
|
100
|
+
.reverse();
|
|
101
|
+
for (const value of contentEncoding) {
|
|
102
|
+
const encoding = value.trim().toLowerCase();
|
|
103
|
+
if (encoding) {
|
|
104
|
+
body = await this.decodeRequestBody(body, encoding);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return body.toString('utf8');
|
|
108
|
+
}
|
|
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
|
+
}
|
|
120
|
+
}
|
|
121
|
+
shouldInjectScript(proxyRes, html) {
|
|
122
|
+
// Check if this is an HTML response
|
|
123
|
+
const contentType = proxyRes.headers['content-type'];
|
|
124
|
+
if (!contentType?.includes('text/html')) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (!html.includes('</body>')) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
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
|
+
}
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swell/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Swell's command line interface/utility",
|
|
6
6
|
"keywords": [
|
|
@@ -32,9 +32,7 @@
|
|
|
32
32
|
"@oclif/errors": "1.3.6",
|
|
33
33
|
"@oclif/plugin-help": "6.0.4",
|
|
34
34
|
"@oclif/plugin-plugins": "3.9.4",
|
|
35
|
-
"@types/browser-sync": "2.29.0",
|
|
36
35
|
"blake3-wasm": "2.1.5",
|
|
37
|
-
"browser-sync": "3.0.2",
|
|
38
36
|
"chalk": "5.3.0",
|
|
39
37
|
"conf": "11.0.2",
|
|
40
38
|
"configstore": "6.0.0",
|
|
@@ -43,6 +41,7 @@
|
|
|
43
41
|
"find-up": "7.0.0",
|
|
44
42
|
"get-port": "7.0.0",
|
|
45
43
|
"globby": "^14.1.0",
|
|
44
|
+
"http-proxy": "1.18.1",
|
|
46
45
|
"inflection": "3.0.0",
|
|
47
46
|
"istextorbinary": "9.5.0",
|
|
48
47
|
"lodash": "4.17.21",
|
|
@@ -54,17 +53,20 @@
|
|
|
54
53
|
"ora": "7.0.1",
|
|
55
54
|
"qs": "6.12.3",
|
|
56
55
|
"semver": "7.5.4",
|
|
57
|
-
"table": "6.8.1"
|
|
56
|
+
"table": "6.8.1",
|
|
57
|
+
"ws": "8.18.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@inquirer/testing": "2.1.8",
|
|
61
61
|
"@oclif/test": "3.0.3",
|
|
62
62
|
"@types/chai": "4.3.9",
|
|
63
63
|
"@types/configstore": "6.0.1",
|
|
64
|
+
"@types/http-proxy": "1.17.16",
|
|
64
65
|
"@types/inquirer": "9.0.6",
|
|
65
66
|
"@types/mocha": "10.0.3",
|
|
66
67
|
"@types/node": "20.8.8",
|
|
67
68
|
"@types/qs": "6.9.15",
|
|
69
|
+
"@types/ws": "8.18.1",
|
|
68
70
|
"@typescript-eslint/eslint-plugin": "6.9.0",
|
|
69
71
|
"chai": "4.3.10",
|
|
70
72
|
"dotenv": "16.3.1",
|