@swell/cli 2.1.0 → 2.2.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.
Files changed (87) 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 +158 -136
  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/model.d.ts +4 -0
  30. package/dist/commands/create/model.js +120 -25
  31. package/dist/commands/inspect/content.d.ts +25 -0
  32. package/dist/commands/inspect/content.js +159 -0
  33. package/dist/commands/inspect/index.d.ts +7 -0
  34. package/dist/commands/inspect/index.js +16 -0
  35. package/dist/commands/inspect/models.d.ts +27 -0
  36. package/dist/commands/inspect/models.js +197 -0
  37. package/dist/commands/schema.d.ts +27 -0
  38. package/dist/commands/schema.js +231 -0
  39. package/dist/commands/theme/dev.d.ts +1 -1
  40. package/dist/commands/theme/dev.js +2 -2
  41. package/dist/commands/theme/init.d.ts +1 -1
  42. package/dist/commands/theme/init.js +1 -1
  43. package/dist/commands/theme/pull.d.ts +2 -2
  44. package/dist/commands/theme/pull.js +2 -2
  45. package/dist/commands/theme/push.d.ts +1 -1
  46. package/dist/commands/theme/push.js +1 -1
  47. package/dist/create-app-command.d.ts +12 -2
  48. package/dist/create-app-command.js +103 -80
  49. package/dist/create-collection-command.js +4 -1
  50. package/dist/create-config-command.js +4 -0
  51. package/dist/env/local.d.ts +1 -0
  52. package/dist/env/local.js +1 -0
  53. package/dist/env/production.d.ts +1 -0
  54. package/dist/env/production.js +1 -0
  55. package/dist/env/review.d.ts +1 -0
  56. package/dist/env/review.js +1 -0
  57. package/dist/env/staging.d.ts +1 -0
  58. package/dist/env/staging.js +1 -0
  59. package/dist/lib/api.d.ts +16 -5
  60. package/dist/lib/api.js +67 -25
  61. package/dist/lib/app-config.js +1 -0
  62. package/dist/lib/apps/index.d.ts +2 -0
  63. package/dist/lib/apps/index.js +20 -15
  64. package/dist/lib/config.d.ts +1 -1
  65. package/dist/lib/constants.d.ts +1 -0
  66. package/dist/lib/constants.js +1 -0
  67. package/dist/lib/create/content.d.ts +5 -1
  68. package/dist/lib/create/content.js +8 -1
  69. package/dist/lib/create/function.d.ts +2 -1
  70. package/dist/lib/create/function.js +7 -3
  71. package/dist/lib/create/model.d.ts +1 -0
  72. package/dist/lib/create/schemas.d.ts +6 -0
  73. package/dist/lib/create/schemas.js +6 -0
  74. package/dist/lib/sessions.js +4 -3
  75. package/dist/lib/style.js +0 -1
  76. package/dist/lib/theme-sync.d.ts +6 -6
  77. package/dist/lib/theme-sync.js +49 -39
  78. package/dist/push-app-command.d.ts +7 -7
  79. package/dist/push-app-command.js +38 -36
  80. package/dist/remote-app-command.d.ts +1 -1
  81. package/dist/remote-app-command.js +4 -2
  82. package/dist/swell-api-command.d.ts +13 -0
  83. package/dist/swell-api-command.js +75 -0
  84. package/dist/swell-command.d.ts +1 -1
  85. package/dist/swell-command.js +9 -9
  86. package/package.json +8 -1
  87. package/oclif.manifest.json +0 -1936
@@ -0,0 +1,15 @@
1
+ import { HttpMethod } from '../../lib/api.js';
2
+ import { SwellApiCommand } from '../../swell-api-command.js';
3
+ export default class ApiDelete extends SwellApiCommand {
4
+ static summary: string;
5
+ static description: string;
6
+ static args: {
7
+ path: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ };
12
+ static examples: string[];
13
+ get method(): HttpMethod;
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,26 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { HttpMethod } from '../../lib/api.js';
3
+ import { SwellApiCommand } from '../../swell-api-command.js';
4
+ export default class ApiDelete extends SwellApiCommand {
5
+ static summary = 'Send a DELETE request to the Swell API.';
6
+ static description = 'Remove existing resources from the Swell Backend API.';
7
+ static args = {
8
+ path: Args.string({
9
+ description: 'API endpoint path (must start with /)',
10
+ required: true,
11
+ }),
12
+ };
13
+ static flags = {
14
+ live: Flags.boolean({
15
+ description: 'Use live environment instead of test',
16
+ default: false,
17
+ }),
18
+ };
19
+ static examples = ['swell api delete /products/abc123'];
20
+ get method() {
21
+ return HttpMethod.DELETE;
22
+ }
23
+ async run() {
24
+ await this.request(ApiDelete);
25
+ }
26
+ }
@@ -0,0 +1,15 @@
1
+ import { HttpMethod } from '../../lib/api.js';
2
+ import { SwellApiCommand } from '../../swell-api-command.js';
3
+ export default class ApiGet extends SwellApiCommand {
4
+ static summary: string;
5
+ static description: string;
6
+ static args: {
7
+ path: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ };
12
+ static examples: string[];
13
+ get method(): HttpMethod;
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,31 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { HttpMethod } from '../../lib/api.js';
3
+ import { SwellApiCommand } from '../../swell-api-command.js';
4
+ export default class ApiGet extends SwellApiCommand {
5
+ static summary = 'Send a GET request to the Swell API.';
6
+ static description = `Retrieve data from the Swell Backend API.
7
+ The command performs a GET request to the specified endpoint path.`;
8
+ static args = {
9
+ path: Args.string({
10
+ description: 'API endpoint path (must start with /)',
11
+ required: true,
12
+ }),
13
+ };
14
+ static flags = {
15
+ live: Flags.boolean({
16
+ description: 'Use live environment instead of test',
17
+ default: false,
18
+ }),
19
+ };
20
+ static examples = [
21
+ 'swell api get /products',
22
+ 'swell api get "/products?limit=10&category=shoes"',
23
+ 'swell api get /products/abc123',
24
+ ];
25
+ get method() {
26
+ return HttpMethod.GET;
27
+ }
28
+ async run() {
29
+ await this.request(ApiGet);
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Api extends Command {
3
+ static summary: string;
4
+ static description: string;
5
+ static examples: string[];
6
+ run(): Promise<void>;
7
+ }
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Api extends Command {
3
+ static summary = 'Send requests directly to the Swell Backend API.';
4
+ static description = `Run Swell API requests directly from the command line.
5
+ Supports GET, POST, PUT, and DELETE methods with optional test/live environments.`;
6
+ static examples = [
7
+ 'swell api get /products',
8
+ 'swell api post /products --body \'{"name":"New product"}\'',
9
+ 'swell api put /products/abc123 --body ./update.json',
10
+ 'swell api delete /products/abc123',
11
+ ];
12
+ async run() {
13
+ this.log('Use "swell api get|post|put|delete" to interact with the Swell API.');
14
+ this.log('Run "swell api --help" for more information.');
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ import { HttpMethod } from '../../lib/api.js';
2
+ import { SwellApiCommand } from '../../swell-api-command.js';
3
+ export default class ApiPost extends SwellApiCommand {
4
+ static summary: string;
5
+ static description: string;
6
+ static args: {
7
+ path: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ body: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
+ live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
+ };
13
+ static examples: string[];
14
+ get method(): HttpMethod;
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,33 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { HttpMethod } from '../../lib/api.js';
3
+ import { SwellApiCommand } from '../../swell-api-command.js';
4
+ export default class ApiPost extends SwellApiCommand {
5
+ static summary = 'Send a POST request to the Swell API.';
6
+ static description = `Create new resources in the Swell Backend API.
7
+ You can provide a JSON body directly or specify a file path containing JSON data.`;
8
+ static args = {
9
+ path: Args.string({
10
+ description: 'API endpoint path (must start with /)',
11
+ required: true,
12
+ }),
13
+ };
14
+ static flags = {
15
+ body: Flags.string({
16
+ description: 'Request body (inline JSON or file path)',
17
+ }),
18
+ live: Flags.boolean({
19
+ description: 'Use live environment instead of test',
20
+ default: false,
21
+ }),
22
+ };
23
+ static examples = [
24
+ 'swell api post /products --body \'{"name": "Product", "price": 100}\'',
25
+ 'swell api post /products --body ./product.json',
26
+ ];
27
+ get method() {
28
+ return HttpMethod.POST;
29
+ }
30
+ async run() {
31
+ await this.request(ApiPost);
32
+ }
33
+ }
@@ -0,0 +1,16 @@
1
+ import { HttpMethod } from '../../lib/api.js';
2
+ import { SwellApiCommand } from '../../swell-api-command.js';
3
+ export default class ApiPut extends SwellApiCommand {
4
+ static summary: string;
5
+ static description: string;
6
+ static args: {
7
+ path: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ body: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
+ live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
+ };
13
+ static examples: string[];
14
+ get method(): HttpMethod;
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,33 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { HttpMethod } from '../../lib/api.js';
3
+ import { SwellApiCommand } from '../../swell-api-command.js';
4
+ export default class ApiPut extends SwellApiCommand {
5
+ static summary = 'Send a PUT request to the Swell API.';
6
+ static description = `Update existing resources in the Swell Backend API.
7
+ You can provide a JSON body directly or specify a file path containing JSON data.`;
8
+ static args = {
9
+ path: Args.string({
10
+ description: 'API endpoint path (must start with /)',
11
+ required: true,
12
+ }),
13
+ };
14
+ static flags = {
15
+ body: Flags.string({
16
+ description: 'Request body (inline JSON or file path)',
17
+ }),
18
+ live: Flags.boolean({
19
+ description: 'Use live environment instead of test',
20
+ default: false,
21
+ }),
22
+ };
23
+ static examples = [
24
+ 'swell api put /products/{id} --body \'{"id": "abc123", "name": "Updated"}\'',
25
+ 'swell api put /products/abc123 --body ./product.json',
26
+ ];
27
+ get method() {
28
+ return HttpMethod.PUT;
29
+ }
30
+ async run() {
31
+ await this.request(ApiPut);
32
+ }
33
+ }
@@ -1,5 +1,6 @@
1
1
  import { PushAppCommand } from '../../push-app-command.js';
2
2
  export default class AppDev extends PushAppCommand {
3
+ static delayOrientation: boolean;
3
4
  static examples: string[];
4
5
  static flags: {
5
6
  'no-push': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
@@ -8,23 +9,22 @@ export default class AppDev extends PushAppCommand {
8
9
  'storefront-id': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
10
  'storefront-select': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
10
11
  };
11
- static summary: string;
12
- static delayOrientation: boolean;
13
12
  static orientation: {
14
13
  env: string;
15
14
  };
16
- tmpDir: string;
17
- functionPorts: Map<string, number>;
15
+ static summary: string;
18
16
  functionErrors: Map<string, string>;
17
+ functionPorts: Map<string, number>;
18
+ tmpDir: string;
19
19
  run(): Promise<void>;
20
- private startAppFunctionServer;
21
20
  runAppFrontendDevIfApplicable(frontendPort?: number): Promise<number | void>;
21
+ private createFunctionRouter;
22
+ private createTmpDirectory;
23
+ private generateWranglerConfig;
22
24
  private getAppFunctions;
23
25
  private logAllFunctions;
24
26
  private logFunction;
25
- private createTmpDirectory;
26
- private startFunctionServers;
27
- private generateWranglerConfig;
28
- private createFunctionRouter;
29
27
  private onChangeFunctionWatcher;
28
+ private startAppFunctionServer;
29
+ private startFunctionServers;
30
30
  }
@@ -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();
@@ -150,7 +195,8 @@ export default class AppDev extends PushAppCommand {
150
195
  if (config.route.headers) {
151
196
  const headers = Object.entries(config.route.headers).map(([key, value]) => `${key}: ${value}`);
152
197
  this.log(` Headers:`);
153
- headers.forEach((header) => this.log(` ${style.dim(header)}`));
198
+ for (const header of headers)
199
+ this.log(` ${style.dim(header)}`);
154
200
  }
155
201
  if (config.route.cache?.timeout) {
156
202
  this.log(` Cache timeout: ${style.dim(config.route.cache.timeout)}`);
@@ -159,6 +205,9 @@ export default class AppDev extends PushAppCommand {
159
205
  this.log(` Public: ${style.dim('true')}`);
160
206
  }
161
207
  }
208
+ if (config?.extension) {
209
+ this.log(` Extension: ${style.dim(config.extension)}`);
210
+ }
162
211
  if (config?.description) {
163
212
  this.log(` Description: ${style.dim(config.description)}`);
164
213
  }
@@ -167,15 +216,68 @@ export default class AppDev extends PushAppCommand {
167
216
  this.log(` ${style.error('Error loading config:', error.message)}`);
168
217
  }
169
218
  }
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;
219
+ async onChangeFunctionWatcher(appConfig, action, result) {
220
+ // If no result, skip
221
+ if (!result) {
222
+ return;
223
+ }
224
+ // If deleted result, skip and remove port
225
+ if (action === 'remove' || !result) {
226
+ this.functionPorts.delete(appConfig?.name);
227
+ this.log();
228
+ return;
229
+ }
230
+ // Skip everything except functions
231
+ if (appConfig?.type !== ConfigType.FUNCTION) {
232
+ return;
233
+ }
234
+ try {
235
+ const fullPath = path.join(this.appPath, appConfig.filePath);
236
+ // Re-bundle the function
237
+ const { code } = await bundleFunction(fullPath);
238
+ // Update the bundled file (wrangler dev will auto-reload)
239
+ const bundledPath = path.join(this.tmpDir, `${appConfig.name}.js`);
240
+ await fs.promises.writeFile(bundledPath, code);
241
+ if (this.functionPorts.has(appConfig.name)) {
242
+ this.log(`\nUpdating function ${appConfig.name}...`);
243
+ }
244
+ else {
245
+ this.log(`\nStarting function ${appConfig.name}...`);
246
+ await this.startFunctionServers([appConfig]);
247
+ }
248
+ await this.logFunction(appConfig);
249
+ }
250
+ catch (error) {
251
+ this.log(`${style.error('Error re-bundling function')} ${appConfig.name}: ${error.message}`);
252
+ }
253
+ this.log();
254
+ }
255
+ async startAppFunctionServer(spinner, serverPort) {
256
+ // Get all functions in this app
257
+ const functions = await this.getAppFunctions();
258
+ if (functions.length === 0) {
259
+ spinner.stop();
260
+ return;
261
+ }
262
+ // Create TMP directory for wrangler configs and bundled functions
263
+ await this.createTmpDirectory();
264
+ // Bundle functions and start wrangler dev servers for each
265
+ await this.startFunctionServers(functions);
266
+ // Start watching for function file changes
267
+ this.watchForChanges({
268
+ onChange: this.onChangeFunctionWatcher.bind(this),
269
+ });
270
+ // Create a routing server that proxies requests to function servers
271
+ await this.createFunctionRouter(serverPort);
272
+ spinner.succeed(`App function server running on port ${serverPort}\n`);
273
+ this.log(`${style.appConfigName(`Functions:`)}`);
274
+ await this.logAllFunctions(functions);
275
+ this.log(`${style.success('→')} Call functions at: http://localhost:${serverPort}/<function-name>\n`);
176
276
  }
177
277
  async startFunctionServers(functions) {
178
278
  const functionStatus = new Map();
279
+ const allocatedInspectorPorts = new Set();
280
+ /* eslint-disable no-await-in-loop */
179
281
  for (const func of functions) {
180
282
  const functionName = func.name;
181
283
  if (this.functionPorts.has(functionName)) {
@@ -183,6 +285,11 @@ export default class AppDev extends PushAppCommand {
183
285
  continue;
184
286
  }
185
287
  const functionPort = await getPort({ port: portNumbers(9000, 9100) });
288
+ const inspectorPort = await getPort({
289
+ port: portNumbers(9229, 9329),
290
+ exclude: [...allocatedInspectorPorts],
291
+ });
292
+ allocatedInspectorPorts.add(inspectorPort);
186
293
  try {
187
294
  // Bundle the function
188
295
  const fullPath = path.join(this.appPath, func.filePath);
@@ -201,15 +308,16 @@ export default class AppDev extends PushAppCommand {
201
308
  'dev',
202
309
  `--config=${configPath}`,
203
310
  `--port=${functionPort}`,
311
+ `--inspector-port=${inspectorPort}`,
204
312
  ], {
205
313
  cwd: this.tmpDir,
206
- //stdio: 'pipe', // Capture output for debugging
314
+ // stdio: 'pipe', // Capture output for debugging
207
315
  detached: false,
208
316
  });
209
317
  const handleFunctionOutput = (data) => {
210
318
  const output = data.toString();
211
319
  // Remove ANSI escape sequences that cause line clearing
212
- const cleanOutput = output.replace('\u001b[2K\u001b[1A\u001b[2K\u001b[G', '');
320
+ const cleanOutput = output.replace('\u001B[2K\u001B[1A\u001B[2K\u001B[G', '');
213
321
  const lines = cleanOutput
214
322
  .split('\n')
215
323
  .filter((line) => line.trim());
@@ -262,6 +370,12 @@ export default class AppDev extends PushAppCommand {
262
370
  functionStatus.set(functionName, 'error');
263
371
  this.functionErrors.set(functionName, error.message);
264
372
  });
373
+ wranglerProcess.on('exit', (code, _signal) => {
374
+ if (functionStatus.get(functionName) === 'starting') {
375
+ functionStatus.set(functionName, 'error');
376
+ this.functionErrors.set(functionName, `Process exited with code ${code}`);
377
+ }
378
+ });
265
379
  // Store the port for routing
266
380
  this.functionPorts.set(functionName, functionPort);
267
381
  }
@@ -270,13 +384,14 @@ export default class AppDev extends PushAppCommand {
270
384
  this.functionErrors.set(functionName, error.message);
271
385
  }
272
386
  }
387
+ /* eslint-enable no-await-in-loop */
273
388
  await new Promise((resolve) => {
274
- if (!functions.length) {
389
+ if (functions.length === 0) {
275
390
  resolve();
276
391
  return;
277
392
  }
278
393
  const checkAllRunning = () => {
279
- const allRunning = Array.from(functionStatus.values()).every((status) => status !== 'starting');
394
+ const allRunning = [...functionStatus.values()].every((status) => status !== 'starting');
280
395
  if (allRunning) {
281
396
  resolve();
282
397
  }
@@ -288,97 +403,4 @@ export default class AppDev extends PushAppCommand {
288
403
  });
289
404
  return { functionStatus };
290
405
  }
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
406
  }