@vlandoss/localproxy 0.0.2 → 0.0.3-git-4fcca67.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/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 `localproxy` to your global workspace
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
- localproxy help
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:* localproxy <command>
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.2",
3
+ "version": "0.0.3-git-4fcca67.0",
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
  }
@@ -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 localDomains = caddyService.getLocalDomains();
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);
@@ -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
- await printFile(caddyfilePath);
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 localDomains = caddyService.getLocalDomains();
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);
@@ -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 localDomains = caddyService.getLocalDomains();
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, port } = domain;
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 -> :%s", host, port);
37
+ logger.success("`%s` is configured -> %s", host, formattedPorts);
30
38
  } else {
31
- logger.warn("`%s` is not configured -> :%s", host, port);
39
+ logger.warn("`%s` is not configured -> %s", host, formattedPorts);
32
40
  }
33
41
  }
34
42
  });
@@ -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
+ }