@vlandoss/localproxy 0.0.2 → 0.0.3
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/README.md +3 -3
- package/package.json +4 -1
- package/src/commands/clean.ts +3 -1
- package/src/commands/setup.ts +7 -9
- package/src/commands/status.ts +12 -4
- package/src/services/caddy.ts +0 -24
- package/src/services/caddyfile/index.ts +1 -0
- package/src/services/caddyfile/parser.ts +123 -0
- package/src/services/caddyfile/service.ts +48 -0
- package/src/services/file.ts +16 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ http://admin.localhost
|
|
|
28
28
|
pnpm add -g @vlandoss/localproxy
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
It will adds the `
|
|
31
|
+
It will adds the `localp` CLI to your global workspace
|
|
32
32
|
|
|
33
33
|
## Usage
|
|
34
34
|
|
|
@@ -38,7 +38,7 @@ It will adds the `localproxy` to your global workspace
|
|
|
38
38
|
Run the help command:
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
|
-
|
|
41
|
+
localp help
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
## Troubleshooting
|
|
@@ -46,5 +46,5 @@ localproxy help
|
|
|
46
46
|
To enable debug mode, set the `DEBUG` environment variable to `localproxy:*` before running *any* command.
|
|
47
47
|
|
|
48
48
|
```sh
|
|
49
|
-
DEBUG=localproxy:*
|
|
49
|
+
DEBUG=localproxy:* localp <command>
|
|
50
50
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vlandoss/localproxy",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Simple local development proxy automation",
|
|
5
5
|
"homepage": "https://github.com/variableland/dx/tree/main/packages/localproxy#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -34,5 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"bun": ">=1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "vitest run"
|
|
37
40
|
}
|
|
38
41
|
}
|
package/src/commands/clean.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createCommand } from "commander";
|
|
2
2
|
import { CaddyService } from "~/services/caddy";
|
|
3
|
+
import { CaddyfileService } from "~/services/caddyfile";
|
|
3
4
|
import { HostsService } from "~/services/hosts";
|
|
4
5
|
import { logger } from "~/services/logger";
|
|
5
6
|
import type { Context } from "~/types";
|
|
@@ -16,7 +17,8 @@ export function createCleanCommand({ caddyfilePath }: Context) {
|
|
|
16
17
|
const caddyService = new CaddyService(caddyfilePath);
|
|
17
18
|
await caddyService.stop(options);
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
+
const caddyfileService = new CaddyfileService(caddyfilePath);
|
|
21
|
+
const localDomains = await caddyfileService.getLocalDomains();
|
|
20
22
|
const hosts = localDomains.map((d) => d.host);
|
|
21
23
|
|
|
22
24
|
const hostsService = new HostsService(hosts);
|
package/src/commands/setup.ts
CHANGED
|
@@ -3,6 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { editor as editorPrompt } from "@inquirer/prompts";
|
|
4
4
|
import { createCommand } from "commander";
|
|
5
5
|
import { CaddyService } from "~/services/caddy";
|
|
6
|
+
import { CaddyfileService } from "~/services/caddyfile";
|
|
7
|
+
import { FileService } from "~/services/file";
|
|
6
8
|
import { HostsService } from "~/services/hosts";
|
|
7
9
|
import { logger } from "~/services/logger";
|
|
8
10
|
import { quietShell } from "~/services/shell";
|
|
@@ -14,13 +16,6 @@ type CommandOptions = {
|
|
|
14
16
|
|
|
15
17
|
const debug = logger.subdebug("setup");
|
|
16
18
|
|
|
17
|
-
async function printFile(filePath: string) {
|
|
18
|
-
const fileContent = (await fs.readFile(filePath)).toString();
|
|
19
|
-
|
|
20
|
-
console.log(`${filePath}:\n`);
|
|
21
|
-
console.log(fileContent.trim());
|
|
22
|
-
}
|
|
23
|
-
|
|
24
19
|
async function checkInternalTools() {
|
|
25
20
|
const { $ } = quietShell;
|
|
26
21
|
|
|
@@ -72,12 +67,15 @@ export function createSetupCommand({ binDir, installDir, caddyfilePath }: Contex
|
|
|
72
67
|
|
|
73
68
|
await fs.writeFile(caddyfilePath, fileContent);
|
|
74
69
|
|
|
75
|
-
|
|
70
|
+
const fileService = new FileService(caddyfilePath);
|
|
71
|
+
await fileService.print();
|
|
76
72
|
|
|
77
73
|
const caddyService = new CaddyService(caddyfilePath);
|
|
78
74
|
await caddyService.reboot(options);
|
|
79
75
|
|
|
80
|
-
const
|
|
76
|
+
const caddyfileService = new CaddyfileService(caddyfilePath);
|
|
77
|
+
|
|
78
|
+
const localDomains = await caddyfileService.getLocalDomains();
|
|
81
79
|
const hosts = localDomains.map((d) => d.host);
|
|
82
80
|
|
|
83
81
|
const hostsService = new HostsService(hosts);
|
package/src/commands/status.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createCommand } from "commander";
|
|
2
2
|
import { CaddyService } from "~/services/caddy";
|
|
3
|
+
import { CaddyfileService } from "~/services/caddyfile";
|
|
4
|
+
import { FileService } from "~/services/file";
|
|
3
5
|
import { HostsService } from "~/services/hosts";
|
|
4
6
|
import { logger } from "~/services/logger";
|
|
5
7
|
import type { Context } from "~/types";
|
|
@@ -16,19 +18,25 @@ export function createStatusCommand({ caddyfilePath }: Context) {
|
|
|
16
18
|
logger.warn("Caddy is not running. Use `localp start` to start it.");
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
const
|
|
21
|
+
const fileService = new FileService(caddyfilePath);
|
|
22
|
+
await fileService.print();
|
|
23
|
+
|
|
24
|
+
const caddyfileService = new CaddyfileService(caddyfilePath);
|
|
25
|
+
const localDomains = await caddyfileService.getLocalDomains();
|
|
20
26
|
const hosts = localDomains.map((d) => d.host);
|
|
21
27
|
|
|
22
28
|
const hostsService = new HostsService(hosts);
|
|
23
29
|
|
|
24
30
|
for (const domain of localDomains) {
|
|
25
|
-
const { host,
|
|
31
|
+
const { host, ports } = domain;
|
|
32
|
+
|
|
26
33
|
const found = await hostsService.findHost(host);
|
|
34
|
+
const formattedPorts = ports.map((p) => `:${p}`).join(", ");
|
|
27
35
|
|
|
28
36
|
if (found) {
|
|
29
|
-
logger.success("`%s` is configured ->
|
|
37
|
+
logger.success("`%s` is configured -> %s", host, formattedPorts);
|
|
30
38
|
} else {
|
|
31
|
-
logger.warn("`%s` is not configured ->
|
|
39
|
+
logger.warn("`%s` is not configured -> %s", host, formattedPorts);
|
|
32
40
|
}
|
|
33
41
|
}
|
|
34
42
|
});
|
package/src/services/caddy.ts
CHANGED
|
@@ -9,11 +9,6 @@ type ExecOption = {
|
|
|
9
9
|
verbose: boolean;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export type LocalDomain = {
|
|
13
|
-
host: string;
|
|
14
|
-
port: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
12
|
export class CaddyService {
|
|
18
13
|
#configPath: string;
|
|
19
14
|
#pidFilePath: string;
|
|
@@ -108,23 +103,4 @@ export class CaddyService {
|
|
|
108
103
|
|
|
109
104
|
return isRunning;
|
|
110
105
|
}
|
|
111
|
-
|
|
112
|
-
getLocalDomains(): LocalDomain[] {
|
|
113
|
-
const caddyfileContent = fs.readFileSync(this.#configPath, "utf-8");
|
|
114
|
-
|
|
115
|
-
const REGEX = /^(.+)\.localhost/gm;
|
|
116
|
-
const matches = caddyfileContent.matchAll(REGEX);
|
|
117
|
-
|
|
118
|
-
const hosts = Array.from(matches, (m) => m[0]).filter(Boolean) as string[];
|
|
119
|
-
|
|
120
|
-
const localDomains: LocalDomain[] = hosts.map((host) => {
|
|
121
|
-
const index = caddyfileContent.indexOf(host);
|
|
122
|
-
const port = caddyfileContent.slice(index).match(/localhost:(\d+)/)?.[1] || "80";
|
|
123
|
-
return { host, port };
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
debug("detected domains: %o", localDomains);
|
|
127
|
-
|
|
128
|
-
return localDomains;
|
|
129
|
-
}
|
|
130
106
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./service";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
type SiteBlock = {
|
|
2
|
+
sites: string[];
|
|
3
|
+
directives: Directive[];
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
type Directive = {
|
|
7
|
+
type: string;
|
|
8
|
+
matchToken?: string;
|
|
9
|
+
arguments: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type Caddyfile = {
|
|
13
|
+
siteBlocks: SiteBlock[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class CaddyfileParser {
|
|
17
|
+
#content: string;
|
|
18
|
+
|
|
19
|
+
constructor(content: string) {
|
|
20
|
+
this.#content = content;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
parse(): Caddyfile {
|
|
24
|
+
const siteBlocks = this.#extractSiteBlocks(this.#content);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
siteBlocks,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#extractSiteBlocks(content: string) {
|
|
32
|
+
const siteBlocks: SiteBlock[] = [];
|
|
33
|
+
|
|
34
|
+
const bracedBlockRegex = /([^{]+)\s*\{([^}]*)\}/gs;
|
|
35
|
+
|
|
36
|
+
let match: RegExpExecArray | null | undefined;
|
|
37
|
+
const processedContent = new Set<string>();
|
|
38
|
+
|
|
39
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: loop over content
|
|
40
|
+
while ((match = bracedBlockRegex.exec(content)) !== null) {
|
|
41
|
+
const [fullMatch, sitesStr, directivesStr] = match;
|
|
42
|
+
processedContent.add(fullMatch.trim());
|
|
43
|
+
|
|
44
|
+
const sites = sitesStr ? this.#parseSites(sitesStr.trim()) : [];
|
|
45
|
+
const directives = directivesStr ? this.#parseDirectives(directivesStr.trim()) : [];
|
|
46
|
+
|
|
47
|
+
if (sites.length > 0) {
|
|
48
|
+
siteBlocks.push({ sites, directives });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return siteBlocks;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#parseSites(sitesStr: string) {
|
|
56
|
+
if (!sitesStr.trim()) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return sitesStr
|
|
61
|
+
.split(/[,\s]+/)
|
|
62
|
+
.map((site) => site.trim())
|
|
63
|
+
.filter((site) => site.length > 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#parseDirectives(directivesStr: string) {
|
|
67
|
+
const directives: Directive[] = [];
|
|
68
|
+
|
|
69
|
+
if (!directivesStr.trim()) {
|
|
70
|
+
return directives;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Split into lines and process each directive
|
|
74
|
+
const lines = directivesStr
|
|
75
|
+
.split("\n")
|
|
76
|
+
.map((line) => line.trim())
|
|
77
|
+
.filter((line) => line.length > 0);
|
|
78
|
+
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
const directive = this.#parseDirectiveLine(line);
|
|
81
|
+
|
|
82
|
+
if (directive) {
|
|
83
|
+
directives.push(directive);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return directives;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#parseDirectiveLine(line: string): Directive | null {
|
|
91
|
+
// Simple regex to parse: directive [matcher] arg1 arg2 ...
|
|
92
|
+
const directiveRegex = /^(\w+)(?:\s+(.+))?$/;
|
|
93
|
+
const match = line.match(directiveRegex);
|
|
94
|
+
|
|
95
|
+
if (!match) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const [, directiveType, argsStr] = match;
|
|
100
|
+
|
|
101
|
+
const directive: Directive = {
|
|
102
|
+
type: directiveType as string,
|
|
103
|
+
arguments: [],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (argsStr) {
|
|
107
|
+
const args = argsStr.split(/\s+/).filter((arg) => arg.length > 0);
|
|
108
|
+
|
|
109
|
+
if (args.length > 0 && this.#looksLikeMatcher(args[0] as string)) {
|
|
110
|
+
directive.matchToken = args[0];
|
|
111
|
+
directive.arguments = args.slice(1);
|
|
112
|
+
} else {
|
|
113
|
+
directive.arguments = args;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return directive;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#looksLikeMatcher(token: string) {
|
|
121
|
+
return token === "*" || token.startsWith("/") || token.startsWith("@");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import { logger } from "../logger";
|
|
3
|
+
import { CaddyfileParser } from "./parser";
|
|
4
|
+
|
|
5
|
+
const debug = logger.subdebug("caddyfile-service");
|
|
6
|
+
|
|
7
|
+
export type LocalDomain = {
|
|
8
|
+
host: string;
|
|
9
|
+
ports: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const LOCALHOST_REGEX = /localhost(:\d+)?/;
|
|
13
|
+
|
|
14
|
+
export class CaddyfileService {
|
|
15
|
+
#filepath: string;
|
|
16
|
+
|
|
17
|
+
constructor(filepath: string) {
|
|
18
|
+
this.#filepath = filepath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getLocalDomains(): Promise<LocalDomain[]> {
|
|
22
|
+
const caddyfileContent = await fs.readFile(this.#filepath, "utf-8");
|
|
23
|
+
|
|
24
|
+
const caddyfileParser = new CaddyfileParser(caddyfileContent);
|
|
25
|
+
|
|
26
|
+
const caddyfile = caddyfileParser.parse();
|
|
27
|
+
|
|
28
|
+
const domains = caddyfile.siteBlocks.flatMap((block) => {
|
|
29
|
+
const ports = block.directives
|
|
30
|
+
.filter((d) => d.type === "reverse_proxy" && d.arguments.some((arg) => this.#isLocalhost(arg)))
|
|
31
|
+
.flatMap((d) => d.arguments)
|
|
32
|
+
.filter((arg) => this.#isLocalhost(arg))
|
|
33
|
+
.map((arg) => arg.split(":")[1] as string);
|
|
34
|
+
|
|
35
|
+
return block.sites.map((site) => {
|
|
36
|
+
return { host: site, ports };
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
debug("detected domains: %o", domains);
|
|
41
|
+
|
|
42
|
+
return domains;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#isLocalhost(arg: string) {
|
|
46
|
+
return LOCALHOST_REGEX.test(arg);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
export class FileService {
|
|
4
|
+
#filePath: string;
|
|
5
|
+
|
|
6
|
+
constructor(filePath: string) {
|
|
7
|
+
this.#filePath = filePath;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async print() {
|
|
11
|
+
const fileContent = (await fs.readFile(this.#filePath)).toString();
|
|
12
|
+
|
|
13
|
+
console.log(`${this.#filePath}:\n`);
|
|
14
|
+
console.log(fileContent.trim());
|
|
15
|
+
}
|
|
16
|
+
}
|