@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.
@@ -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
- const FALLBACK_PORT = 3000;
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
- // Find an open port starting at 3000
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
- const bs = BrowserSync.create();
157
- bs.init({
158
- logLevel: 'silent',
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: () => bs.reload(),
158
+ onChange: () => themeSync.reload(),
173
159
  });
174
160
  spinner.stop();
175
- this.log(`Watching for changes. View your theme at ${style.link(`http://localhost:${freePort}`)}\n`);
161
+ this.log(`Watching for changes. View your theme at ${style.link(`http://localhost:${port}`)}\n`);
176
162
  }
177
163
  }
@@ -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
+ }
@@ -1891,5 +1891,5 @@
1891
1891
  ]
1892
1892
  }
1893
1893
  },
1894
- "version": "2.0.14"
1894
+ "version": "2.0.16"
1895
1895
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swell/cli",
3
- "version": "2.0.14",
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",