@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.
- package/dist/commands/api/delete.d.ts +15 -0
- package/dist/commands/api/delete.js +26 -0
- package/dist/commands/api/get.d.ts +15 -0
- package/dist/commands/api/get.js +31 -0
- package/dist/commands/api/index.d.ts +7 -0
- package/dist/commands/api/index.js +16 -0
- package/dist/commands/api/post.d.ts +16 -0
- package/dist/commands/api/post.js +33 -0
- package/dist/commands/api/put.d.ts +16 -0
- package/dist/commands/api/put.js +33 -0
- package/dist/commands/app/dev.d.ts +18 -4
- package/dist/commands/app/dev.js +383 -5
- package/dist/commands/app/frontend/dev.d.ts +6 -5
- package/dist/commands/app/frontend/dev.js +127 -58
- package/dist/commands/app/install.js +3 -3
- package/dist/commands/app/pull.d.ts +1 -1
- package/dist/commands/app/pull.js +6 -6
- package/dist/commands/app/push.d.ts +3 -3
- package/dist/commands/app/push.js +8 -6
- package/dist/commands/app/release.d.ts +1 -1
- package/dist/commands/app/release.js +1 -1
- package/dist/commands/app/version.d.ts +1 -1
- package/dist/commands/app/version.js +5 -3
- package/dist/commands/create/app.d.ts +10 -2
- package/dist/commands/create/app.js +226 -57
- package/dist/commands/create/content.d.ts +4 -0
- package/dist/commands/create/content.js +83 -15
- package/dist/commands/create/function.js +27 -5
- package/dist/commands/create/model.d.ts +4 -0
- package/dist/commands/create/model.js +120 -25
- package/dist/commands/inspect/content.d.ts +25 -0
- package/dist/commands/inspect/content.js +159 -0
- package/dist/commands/inspect/index.d.ts +7 -0
- package/dist/commands/inspect/index.js +16 -0
- package/dist/commands/inspect/models.d.ts +27 -0
- package/dist/commands/inspect/models.js +197 -0
- package/dist/commands/schema.d.ts +27 -0
- package/dist/commands/schema.js +231 -0
- package/dist/commands/theme/dev.d.ts +1 -1
- package/dist/commands/theme/dev.js +2 -2
- package/dist/commands/theme/init.d.ts +1 -1
- package/dist/commands/theme/init.js +1 -1
- package/dist/commands/theme/pull.d.ts +2 -2
- package/dist/commands/theme/pull.js +2 -2
- package/dist/commands/theme/push.d.ts +1 -1
- package/dist/commands/theme/push.js +1 -1
- package/dist/create-app-command.d.ts +14 -0
- package/dist/create-app-command.js +198 -15
- package/dist/create-collection-command.js +4 -1
- package/dist/create-config-command.js +4 -0
- package/dist/env/local.d.ts +2 -0
- package/dist/env/local.js +2 -0
- package/dist/env/production.d.ts +2 -0
- package/dist/env/production.js +2 -0
- package/dist/env/review.d.ts +2 -0
- package/dist/env/review.js +2 -0
- package/dist/env/staging.d.ts +2 -0
- package/dist/env/staging.js +2 -0
- package/dist/lib/api.d.ts +16 -5
- package/dist/lib/api.js +67 -25
- package/dist/lib/app-config.js +1 -0
- package/dist/lib/apps/app-config.d.ts +49 -0
- package/dist/lib/apps/app-config.js +21 -14
- package/dist/lib/apps/index.d.ts +5 -4
- package/dist/lib/apps/index.js +109 -21
- package/dist/lib/bundle.js +2 -1
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/constants.d.ts +2 -4
- package/dist/lib/constants.js +6 -4
- package/dist/lib/create/content.d.ts +5 -1
- package/dist/lib/create/content.js +8 -1
- package/dist/lib/create/function.d.ts +2 -1
- package/dist/lib/create/function.js +7 -3
- package/dist/lib/create/model.d.ts +1 -0
- package/dist/lib/create/schemas.d.ts +6 -0
- package/dist/lib/create/schemas.js +6 -0
- package/dist/lib/proxy.d.ts +1 -0
- package/dist/lib/proxy.js +29 -1
- package/dist/lib/sessions.js +4 -3
- package/dist/lib/style.js +0 -1
- package/dist/lib/swell-function-wrapper.d.ts +23 -4
- package/dist/lib/swell-function-wrapper.js +11 -4
- package/dist/lib/theme-sync.d.ts +6 -6
- package/dist/lib/theme-sync.js +49 -39
- package/dist/push-app-command.d.ts +12 -8
- package/dist/push-app-command.js +108 -29
- package/dist/remote-app-command.d.ts +1 -1
- package/dist/remote-app-command.js +4 -2
- package/dist/swell-api-command.d.ts +13 -0
- package/dist/swell-api-command.js +75 -0
- package/dist/swell-command.d.ts +1 -1
- package/dist/swell-command.js +9 -9
- package/package.json +10 -1
- 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,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
|
}
|
package/dist/commands/app/dev.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
}
|