@swell/cli 2.0.20 → 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 (94) 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 +18 -4
  12. package/dist/commands/app/dev.js +383 -5
  13. package/dist/commands/app/frontend/dev.d.ts +6 -5
  14. package/dist/commands/app/frontend/dev.js +127 -58
  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 +14 -0
  48. package/dist/create-app-command.js +198 -15
  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 +2 -0
  52. package/dist/env/local.js +2 -0
  53. package/dist/env/production.d.ts +2 -0
  54. package/dist/env/production.js +2 -0
  55. package/dist/env/review.d.ts +2 -0
  56. package/dist/env/review.js +2 -0
  57. package/dist/env/staging.d.ts +2 -0
  58. package/dist/env/staging.js +2 -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/app-config.d.ts +49 -0
  63. package/dist/lib/apps/app-config.js +21 -14
  64. package/dist/lib/apps/index.d.ts +5 -4
  65. package/dist/lib/apps/index.js +109 -21
  66. package/dist/lib/bundle.js +2 -1
  67. package/dist/lib/config.d.ts +1 -1
  68. package/dist/lib/constants.d.ts +2 -4
  69. package/dist/lib/constants.js +6 -4
  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/proxy.d.ts +1 -0
  78. package/dist/lib/proxy.js +29 -1
  79. package/dist/lib/sessions.js +4 -3
  80. package/dist/lib/style.js +0 -1
  81. package/dist/lib/swell-function-wrapper.d.ts +23 -4
  82. package/dist/lib/swell-function-wrapper.js +11 -4
  83. package/dist/lib/theme-sync.d.ts +6 -6
  84. package/dist/lib/theme-sync.js +49 -39
  85. package/dist/push-app-command.d.ts +12 -8
  86. package/dist/push-app-command.js +108 -29
  87. package/dist/remote-app-command.d.ts +1 -1
  88. package/dist/remote-app-command.js +4 -2
  89. package/dist/swell-api-command.d.ts +13 -0
  90. package/dist/swell-api-command.js +75 -0
  91. package/dist/swell-command.d.ts +1 -1
  92. package/dist/swell-command.js +9 -9
  93. package/package.json +10 -1
  94. package/oclif.manifest.json +0 -1895
@@ -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,16 +1,30 @@
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
- 'storefront-id': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
6
- 'storefront-select': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
7
6
  'no-push': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
8
7
  port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
+ 'frontend-port': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
+ 'storefront-id': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
10
+ 'storefront-select': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
11
  };
10
- static summary: string;
11
- static delayOrientation: boolean;
12
12
  static orientation: {
13
13
  env: string;
14
14
  };
15
+ static summary: string;
16
+ functionErrors: Map<string, string>;
17
+ functionPorts: Map<string, number>;
18
+ tmpDir: string;
15
19
  run(): Promise<void>;
20
+ runAppFrontendDevIfApplicable(frontendPort?: number): Promise<number | void>;
21
+ private createFunctionRouter;
22
+ private createTmpDirectory;
23
+ private generateWranglerConfig;
24
+ private getAppFunctions;
25
+ private logAllFunctions;
26
+ private logFunction;
27
+ private onChangeFunctionWatcher;
28
+ private startAppFunctionServer;
29
+ private startFunctionServers;
16
30
  }
@@ -1,14 +1,34 @@
1
1
  import { Flags } from '@oclif/core';
2
+ import getPort, { portNumbers } from 'get-port';
3
+ import { spawn } from 'node:child_process';
4
+ import * as fs from 'node:fs';
5
+ import * as http from 'node:http';
6
+ import * as os from 'node:os';
7
+ import * as path from 'node:path';
8
+ import ora from 'ora';
9
+ import { ConfigType, allConfigFilesInDir, appConfigFromFile, } from '../../lib/apps/index.js';
10
+ import { bundleFunction } from '../../lib/bundle.js';
11
+ import style from '../../lib/style.js';
2
12
  import { PushAppCommand } from '../../push-app-command.js';
3
- import AppFrontendDev from './frontend/dev.js';
4
13
  export default class AppDev extends PushAppCommand {
14
+ static delayOrientation = true;
5
15
  static examples = [
6
16
  'swell app dev',
7
17
  'swell app dev --storefront-id <id>',
8
18
  'swell app dev --port 3000',
19
+ 'swell app dev --port 3000 --frontend-port 4000',
9
20
  ];
10
21
  static flags = {
11
- ...AppFrontendDev.flags,
22
+ 'no-push': Flags.boolean({
23
+ description: 'skip pushing app files initially',
24
+ }),
25
+ port: Flags.integer({
26
+ char: 'p',
27
+ description: 'override the default port to run the app locally',
28
+ }),
29
+ 'frontend-port': Flags.integer({
30
+ description: 'specify the port for the frontend dev server when running with frontend',
31
+ }),
12
32
  'storefront-id': Flags.string({
13
33
  description: 'for storefront apps, identify a storefront to preview and push theme files to',
14
34
  }),
@@ -17,12 +37,370 @@ export default class AppDev extends PushAppCommand {
17
37
  description: 'for storefront apps, prompt to select a storefront to preview',
18
38
  }),
19
39
  };
20
- static summary = `Run an app in dev mode from your local machine.`;
21
- static delayOrientation = true;
22
40
  static orientation = {
23
41
  env: 'test',
24
42
  };
43
+ static summary = `Run an app in dev mode from your local machine.`;
44
+ functionErrors = new Map();
45
+ // All available functions
46
+ functionPorts = new Map();
47
+ // Directory for compiled function files and wrangler context
48
+ tmpDir = '';
25
49
  async run() {
26
- await this.config.runCommand('app:frontend:dev', this.argv);
50
+ const { flags } = await this.parse(AppDev);
51
+ const { port, 'frontend-port': frontendPort } = flags;
52
+ const noPush = flags['no-push'];
53
+ if (!(await this.ensureAppExists(undefined, false))) {
54
+ return;
55
+ }
56
+ if (!noPush) {
57
+ await this.pushAppConfigs();
58
+ }
59
+ this.saveCurrentStorefront();
60
+ const spinner = ora();
61
+ spinner.start(`Starting app dev server...\n`);
62
+ const serverPort = await this.startProxyServer(port);
63
+ await this.startAppFunctionServer(spinner, serverPort);
64
+ await this.runAppFrontendDevIfApplicable(frontendPort);
65
+ }
66
+ async runAppFrontendDevIfApplicable(frontendPort) {
67
+ const projectType = this.getFrontendProjectType(false);
68
+ if (projectType) {
69
+ // add app-dev flag to avoid watching for changes
70
+ this.argv.push('--app-dev');
71
+ // add proxy-port flag (use provided port or auto-detect)
72
+ const proxyPort = frontendPort || (await getPort({ port: portNumbers(4000, 4100) }));
73
+ this.argv.push('--proxy-port', String(proxyPort));
74
+ this.config.runCommand('app:frontend:dev', this.argv);
75
+ return proxyPort;
76
+ }
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
+ }
144
+ async getAppFunctions() {
145
+ const functions = [];
146
+ // Get all function files from the functions directory
147
+ try {
148
+ for (const { configFile } of allConfigFilesInDir(this.appPath, 'functions', ConfigType.FUNCTION)) {
149
+ const config = appConfigFromFile(configFile, ConfigType.FUNCTION, this.appPath);
150
+ if (!config.isRootFunction()) {
151
+ continue; // Skip if not a root function config
152
+ }
153
+ functions.push(config);
154
+ }
155
+ }
156
+ catch {
157
+ // functions directory doesn't exist
158
+ }
159
+ return functions;
160
+ }
161
+ async logAllFunctions(functions) {
162
+ for (const func of functions) {
163
+ // eslint-disable-next-line no-await-in-loop
164
+ await this.logFunction(func);
165
+ }
166
+ this.log();
167
+ }
168
+ async logFunction(func) {
169
+ const functionName = path.parse(func.filePath).name;
170
+ this.log(` ${style.appConfigValue(functionName)}`);
171
+ if (this.functionErrors.has(functionName)) {
172
+ this.log(` ${style.error('Error starting function:')} ${this.functionErrors.get(functionName)}`);
173
+ return;
174
+ }
175
+ try {
176
+ // Bundle the function to get its config
177
+ const fullPath = path.join(this.appPath, func.filePath);
178
+ const { config } = await bundleFunction(fullPath);
179
+ if (config?.model?.events) {
180
+ const events = Array.isArray(config.model.events)
181
+ ? config.model.events
182
+ : [config.model.events];
183
+ this.log(` Trigger: Model`);
184
+ this.log(` Events: ${style.dim(events.join(', '))}`);
185
+ }
186
+ else if (config?.cron?.schedule) {
187
+ this.log(` Trigger: Cron`);
188
+ this.log(` Schedule: ${style.dim(config.cron.schedule)}`);
189
+ }
190
+ else if (config?.route) {
191
+ this.log(` Trigger: Route`);
192
+ this.log(` Methods: ${style.dim(config.route.methods
193
+ .map((m) => String(m).toUpperCase())
194
+ .join(', '))}`);
195
+ if (config.route.headers) {
196
+ const headers = Object.entries(config.route.headers).map(([key, value]) => `${key}: ${value}`);
197
+ this.log(` Headers:`);
198
+ for (const header of headers)
199
+ this.log(` ${style.dim(header)}`);
200
+ }
201
+ if (config.route.cache?.timeout) {
202
+ this.log(` Cache timeout: ${style.dim(config.route.cache.timeout)}`);
203
+ }
204
+ if (config.route.public) {
205
+ this.log(` Public: ${style.dim('true')}`);
206
+ }
207
+ }
208
+ if (config?.extension) {
209
+ this.log(` Extension: ${style.dim(config.extension)}`);
210
+ }
211
+ if (config?.description) {
212
+ this.log(` Description: ${style.dim(config.description)}`);
213
+ }
214
+ }
215
+ catch (error) {
216
+ this.log(` ${style.error('Error loading config:', error.message)}`);
217
+ }
218
+ }
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`);
276
+ }
277
+ async startFunctionServers(functions) {
278
+ const functionStatus = new Map();
279
+ const allocatedInspectorPorts = new Set();
280
+ /* eslint-disable no-await-in-loop */
281
+ for (const func of functions) {
282
+ const functionName = func.name;
283
+ if (this.functionPorts.has(functionName)) {
284
+ // Function server already running
285
+ continue;
286
+ }
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);
293
+ try {
294
+ // Bundle the function
295
+ const fullPath = path.join(this.appPath, func.filePath);
296
+ const { code } = await bundleFunction(fullPath);
297
+ // Write bundled function to tmp directory
298
+ const bundledPath = path.join(this.tmpDir, `${functionName}.js`);
299
+ await fs.promises.writeFile(bundledPath, code);
300
+ // Generate wrangler config
301
+ const wranglerConfig = this.generateWranglerConfig(functionName, bundledPath);
302
+ const configPath = path.join(this.tmpDir, `${functionName}.toml`);
303
+ await fs.promises.writeFile(configPath, wranglerConfig);
304
+ functionStatus.set(functionName, 'starting');
305
+ // Start wrangler process in background
306
+ const wranglerProcess = spawn('npx', [
307
+ 'wrangler',
308
+ 'dev',
309
+ `--config=${configPath}`,
310
+ `--port=${functionPort}`,
311
+ `--inspector-port=${inspectorPort}`,
312
+ ], {
313
+ cwd: this.tmpDir,
314
+ // stdio: 'pipe', // Capture output for debugging
315
+ detached: false,
316
+ });
317
+ const handleFunctionOutput = (data) => {
318
+ const output = data.toString();
319
+ // Remove ANSI escape sequences that cause line clearing
320
+ const cleanOutput = output.replace('\u001B[2K\u001B[1A\u001B[2K\u001B[G', '');
321
+ const lines = cleanOutput
322
+ .split('\n')
323
+ .filter((line) => line.trim());
324
+ for (const line of lines) {
325
+ // Capture running status
326
+ if (line.includes('Ready on http://localhost:')) {
327
+ functionStatus.set(functionName, 'running');
328
+ this.functionErrors.delete(functionName);
329
+ continue;
330
+ }
331
+ // Catch startup error
332
+ if (functionStatus.get(functionName) === 'starting' &&
333
+ line.includes('✘ [ERROR]')) {
334
+ functionStatus.set(functionName, 'error');
335
+ this.functionErrors.set(functionName, line);
336
+ continue;
337
+ }
338
+ if (functionStatus.get(functionName) !== 'running') {
339
+ // No output until function is running
340
+ continue;
341
+ }
342
+ // Hide specific wrangler startup/info messages
343
+ if (line.includes('⎔ Starting local server') ||
344
+ line.includes('⎔ Reloading local server') ||
345
+ line.includes('Starting local server') ||
346
+ line.includes('⛅️ wrangler') ||
347
+ line.includes('-----') ||
348
+ line.includes('▲ [WARNING]') ||
349
+ line.includes('The version of Wrangler') ||
350
+ line.includes('Please update to the latest') ||
351
+ line.includes('Run `npm install') ||
352
+ line.includes('After installation') ||
353
+ line.includes('Your worker has access') ||
354
+ line.includes('- Vars:') ||
355
+ line.includes('- Bindings:')) {
356
+ continue;
357
+ }
358
+ // Skip wrangler info lines
359
+ if (line.includes('[wrangler:')) {
360
+ continue;
361
+ }
362
+ if (line) {
363
+ console.log(`${style.appConfigValue(`→ ${functionName}`)} ${this.timestampStyled()} ${line}`);
364
+ }
365
+ }
366
+ };
367
+ wranglerProcess.stdout?.on('data', handleFunctionOutput);
368
+ wranglerProcess.stderr?.on('data', handleFunctionOutput);
369
+ wranglerProcess.on('error', (error) => {
370
+ functionStatus.set(functionName, 'error');
371
+ this.functionErrors.set(functionName, error.message);
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
+ });
379
+ // Store the port for routing
380
+ this.functionPorts.set(functionName, functionPort);
381
+ }
382
+ catch (error) {
383
+ functionStatus.set(functionName, 'error');
384
+ this.functionErrors.set(functionName, error.message);
385
+ }
386
+ }
387
+ /* eslint-enable no-await-in-loop */
388
+ await new Promise((resolve) => {
389
+ if (functions.length === 0) {
390
+ resolve();
391
+ return;
392
+ }
393
+ const checkAllRunning = () => {
394
+ const allRunning = [...functionStatus.values()].every((status) => status !== 'starting');
395
+ if (allRunning) {
396
+ resolve();
397
+ }
398
+ else {
399
+ setTimeout(checkAllRunning, 250);
400
+ }
401
+ };
402
+ checkAllRunning();
403
+ });
404
+ return { functionStatus };
27
405
  }
28
406
  }