@swell/cli 2.1.0 → 2.2.1

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.
Files changed (112) hide show
  1. package/dist/commands/api/delete.d.ts +15 -0
  2. package/dist/commands/api/delete.js +26 -0
  3. package/dist/commands/api/get.d.ts +15 -0
  4. package/dist/commands/api/get.js +31 -0
  5. package/dist/commands/api/index.d.ts +7 -0
  6. package/dist/commands/api/index.js +16 -0
  7. package/dist/commands/api/post.d.ts +16 -0
  8. package/dist/commands/api/post.js +33 -0
  9. package/dist/commands/api/put.d.ts +16 -0
  10. package/dist/commands/api/put.js +33 -0
  11. package/dist/commands/app/dev.d.ts +9 -9
  12. package/dist/commands/app/dev.js +163 -139
  13. package/dist/commands/app/frontend/dev.d.ts +1 -1
  14. package/dist/commands/app/frontend/dev.js +84 -78
  15. package/dist/commands/app/install.js +3 -3
  16. package/dist/commands/app/pull.d.ts +1 -1
  17. package/dist/commands/app/pull.js +6 -6
  18. package/dist/commands/app/push.d.ts +3 -3
  19. package/dist/commands/app/push.js +8 -6
  20. package/dist/commands/app/release.d.ts +1 -1
  21. package/dist/commands/app/release.js +1 -1
  22. package/dist/commands/app/version.d.ts +1 -1
  23. package/dist/commands/app/version.js +5 -3
  24. package/dist/commands/create/app.d.ts +10 -2
  25. package/dist/commands/create/app.js +226 -57
  26. package/dist/commands/create/content.d.ts +4 -0
  27. package/dist/commands/create/content.js +83 -15
  28. package/dist/commands/create/function.js +27 -5
  29. package/dist/commands/create/index.js +5 -1
  30. package/dist/commands/create/model.d.ts +4 -0
  31. package/dist/commands/create/model.js +120 -25
  32. package/dist/commands/create/tests.d.ts +15 -0
  33. package/dist/commands/create/tests.js +79 -0
  34. package/dist/commands/inspect/content.d.ts +25 -0
  35. package/dist/commands/inspect/content.js +159 -0
  36. package/dist/commands/inspect/index.d.ts +7 -0
  37. package/dist/commands/inspect/index.js +16 -0
  38. package/dist/commands/inspect/models.d.ts +27 -0
  39. package/dist/commands/inspect/models.js +197 -0
  40. package/dist/commands/schema.d.ts +27 -0
  41. package/dist/commands/schema.js +231 -0
  42. package/dist/commands/theme/dev.d.ts +1 -1
  43. package/dist/commands/theme/dev.js +2 -2
  44. package/dist/commands/theme/init.d.ts +1 -1
  45. package/dist/commands/theme/init.js +1 -1
  46. package/dist/commands/theme/pull.d.ts +2 -2
  47. package/dist/commands/theme/pull.js +2 -2
  48. package/dist/commands/theme/push.d.ts +1 -1
  49. package/dist/commands/theme/push.js +1 -1
  50. package/dist/create-app-command.d.ts +12 -2
  51. package/dist/create-app-command.js +103 -80
  52. package/dist/create-collection-command.js +4 -1
  53. package/dist/create-config-command.js +4 -0
  54. package/dist/env/local.d.ts +1 -0
  55. package/dist/env/local.js +1 -0
  56. package/dist/env/production.d.ts +1 -0
  57. package/dist/env/production.js +1 -0
  58. package/dist/env/review.d.ts +1 -0
  59. package/dist/env/review.js +1 -0
  60. package/dist/env/staging.d.ts +1 -0
  61. package/dist/env/staging.js +1 -0
  62. package/dist/lib/api.d.ts +16 -5
  63. package/dist/lib/api.js +67 -25
  64. package/dist/lib/app-config.js +1 -0
  65. package/dist/lib/apps/index.d.ts +2 -0
  66. package/dist/lib/apps/index.js +20 -14
  67. package/dist/lib/config.d.ts +1 -1
  68. package/dist/lib/constants.d.ts +1 -0
  69. package/dist/lib/constants.js +1 -0
  70. package/dist/lib/create/content.d.ts +5 -1
  71. package/dist/lib/create/content.js +8 -1
  72. package/dist/lib/create/function.d.ts +2 -1
  73. package/dist/lib/create/function.js +7 -3
  74. package/dist/lib/create/model.d.ts +1 -0
  75. package/dist/lib/create/schemas.d.ts +6 -0
  76. package/dist/lib/create/schemas.js +6 -0
  77. package/dist/lib/create/tests/templates/env-dts.d.ts +1 -0
  78. package/dist/lib/create/tests/templates/env-dts.js +14 -0
  79. package/dist/lib/create/tests/templates/index.d.ts +8 -0
  80. package/dist/lib/create/tests/templates/index.js +8 -0
  81. package/dist/lib/create/tests/templates/integration-test.d.ts +1 -0
  82. package/dist/lib/create/tests/templates/integration-test.js +19 -0
  83. package/dist/lib/create/tests/templates/mock-request.d.ts +2 -0
  84. package/dist/lib/create/tests/templates/mock-request.js +112 -0
  85. package/dist/lib/create/tests/templates/setup-globals.d.ts +1 -0
  86. package/dist/lib/create/tests/templates/setup-globals.js +23 -0
  87. package/dist/lib/create/tests/templates/swell-client.d.ts +1 -0
  88. package/dist/lib/create/tests/templates/swell-client.js +126 -0
  89. package/dist/lib/create/tests/templates/tsconfig.d.ts +1 -0
  90. package/dist/lib/create/tests/templates/tsconfig.js +15 -0
  91. package/dist/lib/create/tests/templates/unit-test.d.ts +1 -0
  92. package/dist/lib/create/tests/templates/unit-test.js +42 -0
  93. package/dist/lib/create/tests/templates/vitest-config.d.ts +2 -0
  94. package/dist/lib/create/tests/templates/vitest-config.js +121 -0
  95. package/dist/lib/create/tests/types.d.ts +21 -0
  96. package/dist/lib/create/tests/types.js +1 -0
  97. package/dist/lib/create/tests.d.ts +4 -0
  98. package/dist/lib/create/tests.js +113 -0
  99. package/dist/lib/sessions.js +4 -3
  100. package/dist/lib/style.js +0 -1
  101. package/dist/lib/theme-sync.d.ts +6 -6
  102. package/dist/lib/theme-sync.js +49 -39
  103. package/dist/push-app-command.d.ts +7 -7
  104. package/dist/push-app-command.js +38 -36
  105. package/dist/remote-app-command.d.ts +1 -1
  106. package/dist/remote-app-command.js +4 -2
  107. package/dist/swell-api-command.d.ts +13 -0
  108. package/dist/swell-api-command.js +75 -0
  109. package/dist/swell-command.d.ts +1 -1
  110. package/dist/swell-command.js +9 -9
  111. package/oclif.manifest.json +639 -46
  112. package/package.json +8 -1
@@ -1,16 +1,17 @@
1
1
  import { Flags } from '@oclif/core';
2
- import ora from 'ora';
3
2
  import getPort, { portNumbers } from 'get-port';
3
+ import { spawn } from 'node:child_process';
4
4
  import * as fs from 'node:fs';
5
- import * as path from 'node:path';
6
- import * as os from 'node:os';
7
5
  import * as http from 'node:http';
8
- import { spawn } from 'node:child_process';
6
+ import * as os from 'node:os';
7
+ import * as path from 'node:path';
8
+ import ora from 'ora';
9
9
  import { ConfigType, allConfigFilesInDir, appConfigFromFile, } from '../../lib/apps/index.js';
10
10
  import { bundleFunction } from '../../lib/bundle.js';
11
11
  import style from '../../lib/style.js';
12
12
  import { PushAppCommand } from '../../push-app-command.js';
13
13
  export default class AppDev extends PushAppCommand {
14
+ static delayOrientation = true;
14
15
  static examples = [
15
16
  'swell app dev',
16
17
  'swell app dev --storefront-id <id>',
@@ -36,16 +37,15 @@ export default class AppDev extends PushAppCommand {
36
37
  description: 'for storefront apps, prompt to select a storefront to preview',
37
38
  }),
38
39
  };
39
- static summary = `Run an app in dev mode from your local machine.`;
40
- static delayOrientation = true;
41
40
  static orientation = {
42
41
  env: 'test',
43
42
  };
44
- // Directory for compiled function files and wrangler context
45
- tmpDir = '';
43
+ static summary = `Run an app in dev mode from your local machine.`;
44
+ functionErrors = new Map();
46
45
  // All available functions
47
46
  functionPorts = new Map();
48
- functionErrors = new Map();
47
+ // Directory for compiled function files and wrangler context
48
+ tmpDir = '';
49
49
  async run() {
50
50
  const { flags } = await this.parse(AppDev);
51
51
  const { port, 'frontend-port': frontendPort } = flags;
@@ -63,28 +63,6 @@ export default class AppDev extends PushAppCommand {
63
63
  await this.startAppFunctionServer(spinner, serverPort);
64
64
  await this.runAppFrontendDevIfApplicable(frontendPort);
65
65
  }
66
- async startAppFunctionServer(spinner, serverPort) {
67
- // Get all functions in this app
68
- const functions = await this.getAppFunctions();
69
- if (functions.length === 0) {
70
- spinner.stop();
71
- return;
72
- }
73
- // Create TMP directory for wrangler configs and bundled functions
74
- await this.createTmpDirectory();
75
- // Bundle functions and start wrangler dev servers for each
76
- await this.startFunctionServers(functions);
77
- // Start watching for function file changes
78
- this.watchForChanges({
79
- onChange: this.onChangeFunctionWatcher.bind(this),
80
- });
81
- // Create a routing server that proxies requests to function servers
82
- await this.createFunctionRouter(serverPort);
83
- spinner.succeed(`App function server running on port ${serverPort}\n`);
84
- this.log(`${style.appConfigName(`Functions:`)}`);
85
- await this.logAllFunctions(functions);
86
- this.log(`${style.success('→')} Call functions at: http://localhost:${serverPort}/<function-name>\n`);
87
- }
88
66
  async runAppFrontendDevIfApplicable(frontendPort) {
89
67
  const projectType = this.getFrontendProjectType(false);
90
68
  if (projectType) {
@@ -97,6 +75,72 @@ export default class AppDev extends PushAppCommand {
97
75
  return proxyPort;
98
76
  }
99
77
  }
78
+ async createFunctionRouter(serverPort) {
79
+ const server = http.createServer((req, res) => {
80
+ const url = new URL(req.url, `http://localhost:${serverPort}`);
81
+ const functionName = url.pathname.slice(1); // Remove leading slash
82
+ if (this.functionPorts.has(functionName)) {
83
+ const targetPort = this.functionPorts.get(functionName);
84
+ // Proxy the request to the function server
85
+ const proxyReq = http.request({
86
+ hostname: 'localhost',
87
+ port: targetPort,
88
+ path: req.url,
89
+ method: req.method,
90
+ headers: {
91
+ ...req.headers,
92
+ 'Swell-Local-Dev': 'true',
93
+ },
94
+ }, (proxyRes) => {
95
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
96
+ proxyRes.pipe(res);
97
+ });
98
+ proxyReq.on('error', (error) => {
99
+ res.writeHead(500);
100
+ res.end(`Error proxying to function ${functionName}: ${error.message}`);
101
+ });
102
+ // Log when response is finished, method, status, and time to execute by proxy
103
+ const startTime = Date.now();
104
+ res.on('finish', async () => {
105
+ const duration = Date.now() - startTime;
106
+ // Log after a short delay to ensure output order
107
+ await new Promise((r) => {
108
+ setTimeout(r, 100);
109
+ });
110
+ this.log(`\n${style.appConfigValue(`→ ${functionName}`)} ${this.timestampStyled()} [${res.statusCode}] ${req.method} ${req.url} (${duration}ms)\n`);
111
+ });
112
+ req.pipe(proxyReq);
113
+ }
114
+ else {
115
+ res.writeHead(404);
116
+ const functionError = this.functionErrors.get(functionName);
117
+ const functionsAvailable = [...this.functionPorts.keys()];
118
+ res.end(functionError
119
+ ? `Function Error: ${functionError}`
120
+ : `Function '${functionName}' not found. ${functionsAvailable.length > 0
121
+ ? `Available functions: ${[...this.functionPorts.keys()].join(', ')}`
122
+ : ''}`);
123
+ }
124
+ });
125
+ server.listen(serverPort);
126
+ }
127
+ async createTmpDirectory() {
128
+ const tmpBase = path.join(os.tmpdir(), 'swell-cli');
129
+ const appTmpDir = path.join(tmpBase, this.app.id || 'unknown-app', 'functions');
130
+ // Create the directory structure
131
+ await fs.promises.mkdir(appTmpDir, { recursive: true });
132
+ this.tmpDir = appTmpDir;
133
+ }
134
+ generateWranglerConfig(functionName, bundledPath) {
135
+ return `
136
+ name = "${functionName}"
137
+ main = "${bundledPath}"
138
+ compatibility_date = "2023-05-18"
139
+
140
+ [vars]
141
+ ENVIRONMENT = "development"
142
+ `.trim();
143
+ }
100
144
  async getAppFunctions() {
101
145
  const functions = [];
102
146
  // Get all function files from the functions directory
@@ -109,13 +153,14 @@ export default class AppDev extends PushAppCommand {
109
153
  functions.push(config);
110
154
  }
111
155
  }
112
- catch (error) {
156
+ catch {
113
157
  // functions directory doesn't exist
114
158
  }
115
159
  return functions;
116
160
  }
117
161
  async logAllFunctions(functions) {
118
162
  for (const func of functions) {
163
+ // eslint-disable-next-line no-await-in-loop
119
164
  await this.logFunction(func);
120
165
  }
121
166
  this.log();
@@ -144,13 +189,16 @@ export default class AppDev extends PushAppCommand {
144
189
  }
145
190
  else if (config?.route) {
146
191
  this.log(` Trigger: Route`);
147
- this.log(` Methods: ${style.dim(config.route.methods
148
- .map((m) => String(m).toUpperCase())
149
- .join(', '))}`);
192
+ if (config.route.methods?.length) {
193
+ this.log(` Methods: ${style.dim(config.route.methods
194
+ .map((m) => String(m).toUpperCase())
195
+ .join(', '))}`);
196
+ }
150
197
  if (config.route.headers) {
151
198
  const headers = Object.entries(config.route.headers).map(([key, value]) => `${key}: ${value}`);
152
199
  this.log(` Headers:`);
153
- headers.forEach((header) => this.log(` ${style.dim(header)}`));
200
+ for (const header of headers)
201
+ this.log(` ${style.dim(header)}`);
154
202
  }
155
203
  if (config.route.cache?.timeout) {
156
204
  this.log(` Cache timeout: ${style.dim(config.route.cache.timeout)}`);
@@ -159,6 +207,9 @@ export default class AppDev extends PushAppCommand {
159
207
  this.log(` Public: ${style.dim('true')}`);
160
208
  }
161
209
  }
210
+ if (config?.extension) {
211
+ this.log(` Extension: ${style.dim(config.extension)}`);
212
+ }
162
213
  if (config?.description) {
163
214
  this.log(` Description: ${style.dim(config.description)}`);
164
215
  }
@@ -167,15 +218,68 @@ export default class AppDev extends PushAppCommand {
167
218
  this.log(` ${style.error('Error loading config:', error.message)}`);
168
219
  }
169
220
  }
170
- async createTmpDirectory() {
171
- const tmpBase = path.join(os.tmpdir(), 'swell-cli');
172
- const appTmpDir = path.join(tmpBase, this.app.id || 'unknown-app', 'functions');
173
- // Create the directory structure
174
- await fs.promises.mkdir(appTmpDir, { recursive: true });
175
- this.tmpDir = appTmpDir;
221
+ async onChangeFunctionWatcher(appConfig, action, result) {
222
+ // If no result, skip
223
+ if (!result) {
224
+ return;
225
+ }
226
+ // If deleted result, skip and remove port
227
+ if (action === 'remove' || !result) {
228
+ this.functionPorts.delete(appConfig?.name);
229
+ this.log();
230
+ return;
231
+ }
232
+ // Skip everything except functions
233
+ if (appConfig?.type !== ConfigType.FUNCTION) {
234
+ return;
235
+ }
236
+ try {
237
+ const fullPath = path.join(this.appPath, appConfig.filePath);
238
+ // Re-bundle the function
239
+ const { code } = await bundleFunction(fullPath);
240
+ // Update the bundled file (wrangler dev will auto-reload)
241
+ const bundledPath = path.join(this.tmpDir, `${appConfig.name}.js`);
242
+ await fs.promises.writeFile(bundledPath, code);
243
+ if (this.functionPorts.has(appConfig.name)) {
244
+ this.log(`\nUpdating function ${appConfig.name}...`);
245
+ }
246
+ else {
247
+ this.log(`\nStarting function ${appConfig.name}...`);
248
+ await this.startFunctionServers([appConfig]);
249
+ }
250
+ await this.logFunction(appConfig);
251
+ }
252
+ catch (error) {
253
+ this.log(`${style.error('Error re-bundling function')} ${appConfig.name}: ${error.message}`);
254
+ }
255
+ this.log();
256
+ }
257
+ async startAppFunctionServer(spinner, serverPort) {
258
+ // Get all functions in this app
259
+ const functions = await this.getAppFunctions();
260
+ if (functions.length === 0) {
261
+ spinner.stop();
262
+ return;
263
+ }
264
+ // Create TMP directory for wrangler configs and bundled functions
265
+ await this.createTmpDirectory();
266
+ // Bundle functions and start wrangler dev servers for each
267
+ await this.startFunctionServers(functions);
268
+ // Start watching for function file changes
269
+ this.watchForChanges({
270
+ onChange: this.onChangeFunctionWatcher.bind(this),
271
+ });
272
+ // Create a routing server that proxies requests to function servers
273
+ await this.createFunctionRouter(serverPort);
274
+ spinner.succeed(`App function server running on port ${serverPort}\n`);
275
+ this.log(`${style.appConfigName(`Functions:`)}`);
276
+ await this.logAllFunctions(functions);
277
+ this.log(`${style.success('→')} Call functions at: http://localhost:${serverPort}/<function-name>\n`);
176
278
  }
177
279
  async startFunctionServers(functions) {
178
280
  const functionStatus = new Map();
281
+ const allocatedInspectorPorts = new Set();
282
+ /* eslint-disable no-await-in-loop */
179
283
  for (const func of functions) {
180
284
  const functionName = func.name;
181
285
  if (this.functionPorts.has(functionName)) {
@@ -183,6 +287,11 @@ export default class AppDev extends PushAppCommand {
183
287
  continue;
184
288
  }
185
289
  const functionPort = await getPort({ port: portNumbers(9000, 9100) });
290
+ const inspectorPort = await getPort({
291
+ port: portNumbers(9229, 9329),
292
+ exclude: [...allocatedInspectorPorts],
293
+ });
294
+ allocatedInspectorPorts.add(inspectorPort);
186
295
  try {
187
296
  // Bundle the function
188
297
  const fullPath = path.join(this.appPath, func.filePath);
@@ -201,15 +310,16 @@ export default class AppDev extends PushAppCommand {
201
310
  'dev',
202
311
  `--config=${configPath}`,
203
312
  `--port=${functionPort}`,
313
+ `--inspector-port=${inspectorPort}`,
204
314
  ], {
205
315
  cwd: this.tmpDir,
206
- //stdio: 'pipe', // Capture output for debugging
316
+ // stdio: 'pipe', // Capture output for debugging
207
317
  detached: false,
208
318
  });
209
319
  const handleFunctionOutput = (data) => {
210
320
  const output = data.toString();
211
321
  // Remove ANSI escape sequences that cause line clearing
212
- const cleanOutput = output.replace('\u001b[2K\u001b[1A\u001b[2K\u001b[G', '');
322
+ const cleanOutput = output.replace('\u001B[2K\u001B[1A\u001B[2K\u001B[G', '');
213
323
  const lines = cleanOutput
214
324
  .split('\n')
215
325
  .filter((line) => line.trim());
@@ -262,6 +372,12 @@ export default class AppDev extends PushAppCommand {
262
372
  functionStatus.set(functionName, 'error');
263
373
  this.functionErrors.set(functionName, error.message);
264
374
  });
375
+ wranglerProcess.on('exit', (code, _signal) => {
376
+ if (functionStatus.get(functionName) === 'starting') {
377
+ functionStatus.set(functionName, 'error');
378
+ this.functionErrors.set(functionName, `Process exited with code ${code}`);
379
+ }
380
+ });
265
381
  // Store the port for routing
266
382
  this.functionPorts.set(functionName, functionPort);
267
383
  }
@@ -270,13 +386,14 @@ export default class AppDev extends PushAppCommand {
270
386
  this.functionErrors.set(functionName, error.message);
271
387
  }
272
388
  }
389
+ /* eslint-enable no-await-in-loop */
273
390
  await new Promise((resolve) => {
274
- if (!functions.length) {
391
+ if (functions.length === 0) {
275
392
  resolve();
276
393
  return;
277
394
  }
278
395
  const checkAllRunning = () => {
279
- const allRunning = Array.from(functionStatus.values()).every((status) => status !== 'starting');
396
+ const allRunning = [...functionStatus.values()].every((status) => status !== 'starting');
280
397
  if (allRunning) {
281
398
  resolve();
282
399
  }
@@ -288,97 +405,4 @@ export default class AppDev extends PushAppCommand {
288
405
  });
289
406
  return { functionStatus };
290
407
  }
291
- generateWranglerConfig(functionName, bundledPath) {
292
- return `
293
- name = "${functionName}"
294
- main = "${bundledPath}"
295
- compatibility_date = "2023-05-18"
296
-
297
- [vars]
298
- ENVIRONMENT = "development"
299
- `.trim();
300
- }
301
- async createFunctionRouter(serverPort) {
302
- const server = http.createServer((req, res) => {
303
- const url = new URL(req.url, `http://localhost:${serverPort}`);
304
- const functionName = url.pathname.slice(1); // Remove leading slash
305
- if (this.functionPorts.has(functionName)) {
306
- const targetPort = this.functionPorts.get(functionName);
307
- // Proxy the request to the function server
308
- const proxyReq = http.request({
309
- hostname: 'localhost',
310
- port: targetPort,
311
- path: req.url,
312
- method: req.method,
313
- headers: {
314
- ...req.headers,
315
- 'Swell-Local-Dev': 'true',
316
- },
317
- }, (proxyRes) => {
318
- res.writeHead(proxyRes.statusCode, proxyRes.headers);
319
- proxyRes.pipe(res);
320
- });
321
- proxyReq.on('error', (error) => {
322
- res.writeHead(500);
323
- res.end(`Error proxying to function ${functionName}: ${error.message}`);
324
- });
325
- // Log when response is finished, method, status, and time to execute by proxy
326
- const startTime = Date.now();
327
- res.on('finish', async () => {
328
- const duration = Date.now() - startTime;
329
- // Log after a short delay to ensure output order
330
- await new Promise((r) => setTimeout(r, 100));
331
- this.log(`\n${style.appConfigValue(`→ ${functionName}`)} ${this.timestampStyled()} [${res.statusCode}] ${req.method} ${req.url} (${duration}ms)\n`);
332
- });
333
- req.pipe(proxyReq);
334
- }
335
- else {
336
- res.writeHead(404);
337
- const functionError = this.functionErrors.get(functionName);
338
- const functionsAvailable = Array.from(this.functionPorts.keys());
339
- res.end(functionError
340
- ? `Function Error: ${functionError}`
341
- : `Function '${functionName}' not found. ${functionsAvailable.length > 0
342
- ? `Available functions: ${Array.from(this.functionPorts.keys()).join(', ')}`
343
- : ''}`);
344
- }
345
- });
346
- server.listen(serverPort);
347
- }
348
- async onChangeFunctionWatcher(appConfig, action, result) {
349
- // If no result, skip
350
- if (!result) {
351
- return;
352
- }
353
- // If deleted result, skip and remove port
354
- if (action === 'remove' || !result) {
355
- this.functionPorts.delete(appConfig?.name);
356
- this.log();
357
- return;
358
- }
359
- // Skip everything except functions
360
- if (appConfig?.type !== ConfigType.FUNCTION) {
361
- return;
362
- }
363
- try {
364
- const fullPath = path.join(this.appPath, appConfig.filePath);
365
- // Re-bundle the function
366
- const { code } = await bundleFunction(fullPath);
367
- // Update the bundled file (wrangler dev will auto-reload)
368
- const bundledPath = path.join(this.tmpDir, `${appConfig.name}.js`);
369
- await fs.promises.writeFile(bundledPath, code);
370
- if (!this.functionPorts.has(appConfig.name)) {
371
- this.log(`\nStarting function ${appConfig.name}...`);
372
- await this.startFunctionServers([appConfig]);
373
- }
374
- else {
375
- this.log(`\nUpdating function ${appConfig.name}...`);
376
- }
377
- await this.logFunction(appConfig);
378
- }
379
- catch (error) {
380
- this.log(`${style.error('Error re-bundling function')} ${appConfig.name}: ${error.message}`);
381
- }
382
- this.log();
383
- }
384
408
  }
@@ -14,7 +14,7 @@ export default class AppFrontendDev extends PushAppCommand {
14
14
  env: string;
15
15
  };
16
16
  static summary: string;
17
+ logAppLocalUrl(storeId: string, sessionId: string): void;
17
18
  run(): Promise<void>;
18
19
  private execFrontendProject;
19
- logAppLocalUrl(storeId: string, sessionId: string): void;
20
20
  }
@@ -33,6 +33,12 @@ export default class AppFrontendDev extends PushAppCommand {
33
33
  env: 'test',
34
34
  };
35
35
  static summary = `Run a frontend app in dev mode from your local machine.`;
36
+ logAppLocalUrl(storeId, sessionId) {
37
+ const frontendAppRoute = this.app.type === 'storefront' && this.app.storefront?.external
38
+ ? `/${this.app.private_id}`
39
+ : '';
40
+ this.log(`View it at ${style.link(`${this.localFrontendUrl(storeId, sessionId)}${frontendAppRoute}`)}\n`);
41
+ }
36
42
  async run() {
37
43
  const { flags } = await this.parse(AppFrontendDev);
38
44
  const { port, 'proxy-port': proxyPort, 'app-dev': isAppDev } = flags;
@@ -63,21 +69,21 @@ export default class AppFrontendDev extends PushAppCommand {
63
69
  let serverReadyDetected = false;
64
70
  await this.execFrontend(projectType.devCommand.replace('${PORT}', String(serverPort)), (string) => {
65
71
  // Framework-specific output handling and ready detection
66
- if (projectType.slug === 'nextjs') {
67
- // 1. NOISE FILTER: Hide unwanted output
68
- if (string.includes('Using vars defined in .dev.vars')) {
69
- return false;
70
- }
71
- // 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
72
- if (/- Local:\s+http:\/\//i.test(string) ||
73
- /- Network:\s+http:\/\//i.test(string) ||
74
- /Ready on http:\/\/localhost/i.test(string)) {
75
- return false;
76
- }
77
- // 3. READY DETECTION: Detect server ready, show our message
78
- if (!serverReadyDetected) {
79
- if (/✓ Ready in \d+/i.test(string) ||
80
- /✓ Starting/i.test(string)) {
72
+ switch (projectType.slug) {
73
+ case 'nextjs': {
74
+ // 1. NOISE FILTER: Hide unwanted output
75
+ if (string.includes('Using vars defined in .dev.vars')) {
76
+ return false;
77
+ }
78
+ // 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
79
+ if (/- local:\s+http:\/\//i.test(string) ||
80
+ /- network:\s+http:\/\//i.test(string) ||
81
+ /ready on http:\/\/localhost/i.test(string)) {
82
+ return false;
83
+ }
84
+ // 3. READY DETECTION: Detect server ready, show our message
85
+ if (!serverReadyDetected &&
86
+ (/✓ ready in \d+/.test(string) || /✓ starting/.test(string))) {
81
87
  serverReadyDetected = true;
82
88
  this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
83
89
  const sessionId = localConfig.getSessionId(currentStore);
@@ -86,38 +92,16 @@ export default class AppFrontendDev extends PushAppCommand {
86
92
  this.watchForChanges();
87
93
  }
88
94
  }
95
+ break;
89
96
  }
90
- }
91
- else if (projectType.slug === 'astro') {
92
- // 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
93
- if (string.includes('┃ Local') ||
94
- string.includes('┃ Network')) {
95
- return false;
96
- }
97
- // 3. READY DETECTION: Detect server ready, show our message
98
- if (!serverReadyDetected && /watching for file changes/i.test(string)) {
99
- serverReadyDetected = true;
100
- this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
101
- const sessionId = localConfig.getSessionId(currentStore);
102
- setTimeout(() => this.logAppLocalUrl(currentStore, sessionId), 100);
103
- if (!isAppDev) {
104
- this.watchForChanges();
97
+ case 'astro': {
98
+ // 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
99
+ if (string.includes('┃ Local') || string.includes('┃ Network')) {
100
+ return false;
105
101
  }
106
- }
107
- }
108
- else if (projectType.slug === 'angular') {
109
- // 1. NOISE FILTER: Hide unwanted output
110
- if (string.includes('press h + enter to show help')) {
111
- return false;
112
- }
113
- // 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
114
- if (/➜\s+Local:\s+http:\/\//i.test(string)) {
115
- return false;
116
- }
117
- // 3. READY DETECTION: Detect server ready, show our message
118
- if (!serverReadyDetected) {
119
- if (/Application bundle generation complete/i.test(string) ||
120
- /Watch mode enabled/i.test(string)) {
102
+ // 3. READY DETECTION: Detect server ready, show our message
103
+ if (!serverReadyDetected &&
104
+ /watching for file changes/i.test(string)) {
121
105
  serverReadyDetected = true;
122
106
  this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
123
107
  const sessionId = localConfig.getSessionId(currentStore);
@@ -126,46 +110,68 @@ export default class AppFrontendDev extends PushAppCommand {
126
110
  this.watchForChanges();
127
111
  }
128
112
  }
113
+ break;
129
114
  }
130
- }
131
- else if (projectType.slug === 'hono') {
132
- // 2. URL FILTER: Hide localhost URLs (confusing - users should use Swell tunnel)
133
- // Note: For Hono, the ready signal IS the URL line, so we handle both in section 3
134
- // 3. READY DETECTION: Detect server ready, show our message, hide the URL
135
- if (!serverReadyDetected && /Ready on http:\/\/localhost:\d+/i.test(string)) {
136
- serverReadyDetected = true;
137
- this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
138
- const sessionId = localConfig.getSessionId(currentStore);
139
- setTimeout(() => this.logAppLocalUrl(currentStore, sessionId), 100);
140
- if (!isAppDev) {
141
- this.watchForChanges();
115
+ case 'angular': {
116
+ // 1. NOISE FILTER: Hide unwanted output
117
+ if (string.includes('press h + enter to show help')) {
118
+ return false;
119
+ }
120
+ // 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
121
+ if (/➜\s+local:\s+http:\/\//i.test(string)) {
122
+ return false;
142
123
  }
143
- return false; // Hide the "Ready on localhost" line
124
+ // 3. READY DETECTION: Detect server ready, show our message
125
+ if (!serverReadyDetected &&
126
+ (/application bundle generation complete/.test(string) ||
127
+ /watch mode enabled/.test(string))) {
128
+ serverReadyDetected = true;
129
+ this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
130
+ const sessionId = localConfig.getSessionId(currentStore);
131
+ setTimeout(() => this.logAppLocalUrl(currentStore, sessionId), 100);
132
+ if (!isAppDev) {
133
+ this.watchForChanges();
134
+ }
135
+ }
136
+ break;
144
137
  }
145
- }
146
- else if (projectType.slug === 'nuxt') {
147
- // 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
148
- if (/➜ Local:\s+http:\/\//i.test(string) ||
149
- /➜ Network:/i.test(string)) {
150
- return false;
138
+ case 'hono': {
139
+ // 2. URL FILTER: Hide localhost URLs (confusing - users should use Swell tunnel)
140
+ // Note: For Hono, the ready signal IS the URL line, so we handle both in section 3
141
+ // 3. READY DETECTION: Detect server ready, show our message, hide the URL
142
+ if (!serverReadyDetected &&
143
+ /ready on http:\/\/localhost:\d+/i.test(string)) {
144
+ serverReadyDetected = true;
145
+ this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
146
+ const sessionId = localConfig.getSessionId(currentStore);
147
+ setTimeout(() => this.logAppLocalUrl(currentStore, sessionId), 100);
148
+ if (!isAppDev) {
149
+ this.watchForChanges();
150
+ }
151
+ return false; // Hide the "Ready on localhost" line
152
+ }
153
+ break;
151
154
  }
152
- // 3. READY DETECTION: Detect server ready, show our message
153
- if (!serverReadyDetected && /Nuxt \d+\.\d+\.\d+/i.test(string)) {
154
- serverReadyDetected = true;
155
- this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
156
- const sessionId = localConfig.getSessionId(currentStore);
157
- setTimeout(() => this.logAppLocalUrl(currentStore, sessionId), 100);
158
- if (!isAppDev) {
159
- this.watchForChanges();
155
+ case 'nuxt': {
156
+ // 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
157
+ if (/➜ local:\s+http:\/\//i.test(string) ||
158
+ /➜ network:/i.test(string)) {
159
+ return false;
160
+ }
161
+ // 3. READY DETECTION: Detect server ready, show our message
162
+ if (!serverReadyDetected && /nuxt \d+\.\d+\.\d+/i.test(string)) {
163
+ serverReadyDetected = true;
164
+ this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
165
+ const sessionId = localConfig.getSessionId(currentStore);
166
+ setTimeout(() => this.logAppLocalUrl(currentStore, sessionId), 100);
167
+ if (!isAppDev) {
168
+ this.watchForChanges();
169
+ }
160
170
  }
171
+ break;
161
172
  }
173
+ // No default
162
174
  }
163
175
  });
164
176
  }
165
- logAppLocalUrl(storeId, sessionId) {
166
- const frontendAppRoute = this.app.type === 'storefront' && this.app.storefront?.external
167
- ? `/${this.app.private_id}`
168
- : '';
169
- this.log(`View it at ${style.link(`${this.localFrontendUrl(storeId, sessionId)}${frontendAppRoute}`)}\n`);
170
- }
171
177
  }
@@ -2,8 +2,8 @@ import { confirm } from '@inquirer/prompts';
2
2
  import { Flags } from '@oclif/core';
3
3
  import ora from 'ora';
4
4
  import * as semver from 'semver';
5
- import config from '../../lib/config.js';
6
5
  import { readRcFile } from '../../lib/app-config.js';
6
+ import config from '../../lib/config.js';
7
7
  import { selectEnvironmentId, selectLoggedInStoreId, } from '../../lib/stores.js';
8
8
  import style from '../../lib/style.js';
9
9
  import { RemoteAppCommand } from '../../remote-app-command.js';
@@ -48,7 +48,7 @@ Without a store specified, install the app in the Live environment of the curren
48
48
  if (versions.length === 0) {
49
49
  this.error(`${version ? `Version ${version} not found` : 'No versions found'} for ${style.appConfigValue(this.app.name)} app. ${version
50
50
  ? ''
51
- : `Create a version with the ${style.command('swell version')} command first.`}`);
51
+ : `Create a version with the ${style.command('swell app version')} command first.`}`);
52
52
  }
53
53
  const versionToInstall = version
54
54
  ? versions.find((ver) => ver.version === version)?.version
@@ -129,7 +129,7 @@ Without a store specified, install the app in the Live environment of the curren
129
129
  const sourceApp = await readRcFile(this.swellRcPath());
130
130
  sourceAppId = sourceApp?.id;
131
131
  }
132
- catch (error) {
132
+ catch {
133
133
  // noop
134
134
  }
135
135
  const existingSourcedApps = sourceAppId