zdev 0.2.4 → 0.2.6
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/index.js +82 -32
- package/package.json +1 -1
- package/src/commands/list.ts +19 -5
- package/src/commands/start.ts +9 -35
- package/src/vite-patch.test.ts +271 -0
- package/src/vite-patch.ts +100 -0
package/dist/index.js
CHANGED
|
@@ -2461,8 +2461,65 @@ Next steps:`);
|
|
|
2461
2461
|
}
|
|
2462
2462
|
|
|
2463
2463
|
// src/commands/start.ts
|
|
2464
|
-
import { existsSync as existsSync5, readFileSync as
|
|
2464
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
2465
2465
|
import { resolve as resolve4, basename as basename3, join as join3 } from "path";
|
|
2466
|
+
|
|
2467
|
+
// src/vite-patch.ts
|
|
2468
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
2469
|
+
function patchViteAllowedHosts(viteConfigPath, devDomain) {
|
|
2470
|
+
if (!devDomain) {
|
|
2471
|
+
return { patched: false, reason: "no devDomain configured" };
|
|
2472
|
+
}
|
|
2473
|
+
let content;
|
|
2474
|
+
try {
|
|
2475
|
+
content = readFileSync4(viteConfigPath, "utf-8");
|
|
2476
|
+
} catch {
|
|
2477
|
+
return { patched: false, reason: "could not read vite config" };
|
|
2478
|
+
}
|
|
2479
|
+
const domainPattern = `.${devDomain}`;
|
|
2480
|
+
if (content.includes(domainPattern)) {
|
|
2481
|
+
return { patched: false, reason: "domain already present" };
|
|
2482
|
+
}
|
|
2483
|
+
if (/allowedHosts\s*:\s*true/.test(content)) {
|
|
2484
|
+
return { patched: false, reason: "already allows all hosts (true)" };
|
|
2485
|
+
}
|
|
2486
|
+
if (/allowedHosts\s*:\s*["']all["']/.test(content)) {
|
|
2487
|
+
return { patched: false, reason: 'already allows all hosts ("all")' };
|
|
2488
|
+
}
|
|
2489
|
+
const entry = `"${domainPattern}"`;
|
|
2490
|
+
let patched = false;
|
|
2491
|
+
if (/allowedHosts\s*:\s*\[/.test(content)) {
|
|
2492
|
+
content = content.replace(/(allowedHosts\s*:\s*\[)/, `$1${entry}, `);
|
|
2493
|
+
patched = true;
|
|
2494
|
+
} else if (/server\s*:\s*\{/.test(content)) {
|
|
2495
|
+
content = content.replace(/(server\s*:\s*\{)/, `$1
|
|
2496
|
+
allowedHosts: [${entry}],`);
|
|
2497
|
+
patched = true;
|
|
2498
|
+
} else if (/defineConfig\s*\(\s*\{/.test(content)) {
|
|
2499
|
+
content = content.replace(/(defineConfig\s*\(\s*\{)/, `$1
|
|
2500
|
+
server: {
|
|
2501
|
+
allowedHosts: [${entry}],
|
|
2502
|
+
},`);
|
|
2503
|
+
patched = true;
|
|
2504
|
+
} else if (/export\s+default\s*\{/.test(content)) {
|
|
2505
|
+
content = content.replace(/(export\s+default\s*\{)/, `$1
|
|
2506
|
+
server: {
|
|
2507
|
+
allowedHosts: [${entry}],
|
|
2508
|
+
},`);
|
|
2509
|
+
patched = true;
|
|
2510
|
+
}
|
|
2511
|
+
if (!patched) {
|
|
2512
|
+
return { patched: false, reason: "unrecognized config format" };
|
|
2513
|
+
}
|
|
2514
|
+
try {
|
|
2515
|
+
writeFileSync5(viteConfigPath, content);
|
|
2516
|
+
} catch {
|
|
2517
|
+
return { patched: false, reason: "could not write vite config" };
|
|
2518
|
+
}
|
|
2519
|
+
return { patched: true, reason: "added allowedHosts" };
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// src/commands/start.ts
|
|
2466
2523
|
function detectWebDir(worktreePath) {
|
|
2467
2524
|
const commonDirs = ["web", "frontend", "app", "client", "packages/web", "apps/web"];
|
|
2468
2525
|
for (const dir of commonDirs) {
|
|
@@ -2548,8 +2605,8 @@ async function start(featureName, projectPath = ".", options = {}) {
|
|
|
2548
2605
|
const destPath = join3(webPath, pattern);
|
|
2549
2606
|
if (existsSync5(srcPath) && !existsSync5(destPath)) {
|
|
2550
2607
|
try {
|
|
2551
|
-
const content =
|
|
2552
|
-
|
|
2608
|
+
const content = readFileSync5(srcPath);
|
|
2609
|
+
writeFileSync6(destPath, content);
|
|
2553
2610
|
console.log(` Copied ${pattern}`);
|
|
2554
2611
|
} catch (e) {
|
|
2555
2612
|
console.log(` Could not copy ${pattern}`);
|
|
@@ -2604,30 +2661,13 @@ async function start(featureName, projectPath = ".", options = {}) {
|
|
|
2604
2661
|
const viteConfigTsPath = join3(webPath, "vite.config.ts");
|
|
2605
2662
|
const viteConfigJsPath = join3(webPath, "vite.config.js");
|
|
2606
2663
|
const viteConfigPath = existsSync5(viteConfigTsPath) ? viteConfigTsPath : existsSync5(viteConfigJsPath) ? viteConfigJsPath : null;
|
|
2607
|
-
if (viteConfigPath) {
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
allowedHosts: true,`);
|
|
2615
|
-
patched = true;
|
|
2616
|
-
} else if (viteConfig.includes("defineConfig({")) {
|
|
2617
|
-
viteConfig = viteConfig.replace(/defineConfig\(\{/, `defineConfig({
|
|
2618
|
-
server: {
|
|
2619
|
-
allowedHosts: true,
|
|
2620
|
-
},`);
|
|
2621
|
-
patched = true;
|
|
2622
|
-
}
|
|
2623
|
-
if (patched) {
|
|
2624
|
-
writeFileSync5(viteConfigPath, viteConfig);
|
|
2625
|
-
console.log(` Patched ${basename3(viteConfigPath)} for external access`);
|
|
2626
|
-
run("git", ["update-index", "--skip-worktree", basename3(viteConfigPath)], { cwd: webPath });
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
} catch (e) {
|
|
2630
|
-
console.log(` Could not patch vite config (non-critical)`);
|
|
2664
|
+
if (viteConfigPath && config.devDomain) {
|
|
2665
|
+
const result = patchViteAllowedHosts(viteConfigPath, config.devDomain);
|
|
2666
|
+
if (result.patched) {
|
|
2667
|
+
console.log(` Patched ${basename3(viteConfigPath)}: ${result.reason}`);
|
|
2668
|
+
run("git", ["update-index", "--skip-worktree", basename3(viteConfigPath)], { cwd: webPath });
|
|
2669
|
+
} else {
|
|
2670
|
+
console.log(` Vite config: ${result.reason}`);
|
|
2631
2671
|
}
|
|
2632
2672
|
}
|
|
2633
2673
|
console.log(`
|
|
@@ -2773,8 +2813,10 @@ function getTmuxSessions(pattern) {
|
|
|
2773
2813
|
}
|
|
2774
2814
|
if (!result.success)
|
|
2775
2815
|
return [];
|
|
2776
|
-
|
|
2777
|
-
`).filter(Boolean)
|
|
2816
|
+
const sessions = result.stdout.split(`
|
|
2817
|
+
`).filter(Boolean);
|
|
2818
|
+
const patternWords = pattern.split("-").filter((w) => w.length > 2);
|
|
2819
|
+
return sessions.filter((name) => patternWords.some((word) => name.toLowerCase().includes(word.toLowerCase())));
|
|
2778
2820
|
}
|
|
2779
2821
|
async function list(options = {}) {
|
|
2780
2822
|
const config = loadConfig();
|
|
@@ -2811,7 +2853,15 @@ No active features.
|
|
|
2811
2853
|
const frontendRunning = alloc.pids.frontend ? isProcessRunning(alloc.pids.frontend) : false;
|
|
2812
2854
|
const convexRunning = alloc.pids.convex ? isProcessRunning(alloc.pids.convex) : false;
|
|
2813
2855
|
const featureSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
2814
|
-
const
|
|
2856
|
+
const projectSlug = alloc.project.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
2857
|
+
const featureOnly = alloc.branch.replace("feature/", "").toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
2858
|
+
let tmuxSessions = getTmuxSessions(featureSlug);
|
|
2859
|
+
if (tmuxSessions.length === 0) {
|
|
2860
|
+
tmuxSessions = getTmuxSessions(projectSlug);
|
|
2861
|
+
}
|
|
2862
|
+
if (tmuxSessions.length === 0 && featureOnly !== projectSlug) {
|
|
2863
|
+
tmuxSessions = getTmuxSessions(featureOnly);
|
|
2864
|
+
}
|
|
2815
2865
|
const hasTmux = tmuxSessions.length > 0;
|
|
2816
2866
|
const isRunning = frontendRunning || convexRunning || hasTmux;
|
|
2817
2867
|
const isFullyRunning = frontendRunning && convexRunning || hasTmux && tmuxSessions.length >= 2;
|
|
@@ -3383,11 +3433,11 @@ ${diffStat}
|
|
|
3383
3433
|
}
|
|
3384
3434
|
|
|
3385
3435
|
// src/index.ts
|
|
3386
|
-
import { readFileSync as
|
|
3436
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3387
3437
|
import { fileURLToPath } from "url";
|
|
3388
3438
|
import { dirname, join as join4 } from "path";
|
|
3389
3439
|
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
3390
|
-
var pkg = JSON.parse(
|
|
3440
|
+
var pkg = JSON.parse(readFileSync6(join4(__dirname2, "..", "package.json"), "utf-8"));
|
|
3391
3441
|
var program2 = new Command;
|
|
3392
3442
|
program2.name("zdev").description(`\uD83D\uDC02 zdev v${pkg.version} - Multi-agent worktree development environment`).version(pkg.version);
|
|
3393
3443
|
program2.command("create <name>").description("Create a new TanStack Start project").option("--convex", "Add Convex backend integration").option("--flat", "Flat structure (no monorepo)").action(async (name, options) => {
|
package/package.json
CHANGED
package/src/commands/list.ts
CHANGED
|
@@ -22,10 +22,14 @@ function getTmuxSessions(pattern: string): string[] {
|
|
|
22
22
|
|
|
23
23
|
if (!result.success) return [];
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const sessions = result.stdout.split("\n").filter(Boolean);
|
|
26
|
+
|
|
27
|
+
// Match if session name contains any word from the pattern
|
|
28
|
+
const patternWords = pattern.split("-").filter(w => w.length > 2);
|
|
29
|
+
|
|
30
|
+
return sessions.filter(name =>
|
|
31
|
+
patternWords.some(word => name.toLowerCase().includes(word.toLowerCase()))
|
|
32
|
+
);
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export interface ListOptions {
|
|
@@ -74,8 +78,18 @@ export async function list(options: ListOptions = {}): Promise<void> {
|
|
|
74
78
|
: false;
|
|
75
79
|
|
|
76
80
|
// Check for tmux sessions related to this feature
|
|
81
|
+
// Try multiple patterns: full name, project name, feature name
|
|
77
82
|
const featureSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
78
|
-
const
|
|
83
|
+
const projectSlug = alloc.project.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
84
|
+
const featureOnly = alloc.branch.replace("feature/", "").toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
85
|
+
|
|
86
|
+
let tmuxSessions = getTmuxSessions(featureSlug);
|
|
87
|
+
if (tmuxSessions.length === 0) {
|
|
88
|
+
tmuxSessions = getTmuxSessions(projectSlug);
|
|
89
|
+
}
|
|
90
|
+
if (tmuxSessions.length === 0 && featureOnly !== projectSlug) {
|
|
91
|
+
tmuxSessions = getTmuxSessions(featureOnly);
|
|
92
|
+
}
|
|
79
93
|
const hasTmux = tmuxSessions.length > 0;
|
|
80
94
|
|
|
81
95
|
const isRunning = frontendRunning || convexRunning || hasTmux;
|
package/src/commands/start.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { resolve, basename, join } from "path";
|
|
3
|
+
import { patchViteAllowedHosts } from "../vite-patch.js";
|
|
3
4
|
import {
|
|
4
5
|
isGitRepo,
|
|
5
6
|
getRepoName,
|
|
@@ -219,41 +220,14 @@ export async function start(
|
|
|
219
220
|
const viteConfigPath = existsSync(viteConfigTsPath) ? viteConfigTsPath :
|
|
220
221
|
existsSync(viteConfigJsPath) ? viteConfigJsPath : null;
|
|
221
222
|
|
|
222
|
-
if (viteConfigPath) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// Try to add allowedHosts to existing server block
|
|
231
|
-
if (viteConfig.includes("server:") || viteConfig.includes("server :")) {
|
|
232
|
-
// Add allowedHosts inside existing server block
|
|
233
|
-
viteConfig = viteConfig.replace(
|
|
234
|
-
/server\s*:\s*\{/,
|
|
235
|
-
"server: {\n allowedHosts: true,"
|
|
236
|
-
);
|
|
237
|
-
patched = true;
|
|
238
|
-
} else if (viteConfig.includes("defineConfig({")) {
|
|
239
|
-
// No server block, add new one
|
|
240
|
-
viteConfig = viteConfig.replace(
|
|
241
|
-
/defineConfig\(\{/,
|
|
242
|
-
"defineConfig({\n server: {\n allowedHosts: true,\n },"
|
|
243
|
-
);
|
|
244
|
-
patched = true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (patched) {
|
|
248
|
-
writeFileSync(viteConfigPath, viteConfig);
|
|
249
|
-
console.log(` Patched ${basename(viteConfigPath)} for external access`);
|
|
250
|
-
|
|
251
|
-
// Mark file as skip-worktree so it won't be committed
|
|
252
|
-
run("git", ["update-index", "--skip-worktree", basename(viteConfigPath)], { cwd: webPath });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
} catch (e) {
|
|
256
|
-
console.log(` Could not patch vite config (non-critical)`);
|
|
223
|
+
if (viteConfigPath && config.devDomain) {
|
|
224
|
+
const result = patchViteAllowedHosts(viteConfigPath, config.devDomain);
|
|
225
|
+
if (result.patched) {
|
|
226
|
+
console.log(` Patched ${basename(viteConfigPath)}: ${result.reason}`);
|
|
227
|
+
// Mark file as skip-worktree so it won't be committed
|
|
228
|
+
run("git", ["update-index", "--skip-worktree", basename(viteConfigPath)], { cwd: webPath });
|
|
229
|
+
} else {
|
|
230
|
+
console.log(` Vite config: ${result.reason}`);
|
|
257
231
|
}
|
|
258
232
|
}
|
|
259
233
|
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { patchViteAllowedHosts } from "./vite-patch.js";
|
|
3
|
+
import { writeFileSync, readFileSync, mkdtempSync, rmSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
|
|
7
|
+
const DEV_DOMAIN = "dev.web3citadel.com";
|
|
8
|
+
const DOMAIN_PATTERN = ".dev.web3citadel.com";
|
|
9
|
+
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
let configPath: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tmpDir = mkdtempSync(join(tmpdir(), "vite-patch-test-"));
|
|
15
|
+
configPath = join(tmpDir, "vite.config.ts");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function writeConfig(content: string): string {
|
|
23
|
+
writeFileSync(configPath, content);
|
|
24
|
+
return configPath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readConfig(): string {
|
|
28
|
+
return readFileSync(configPath, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("patchViteAllowedHosts", () => {
|
|
32
|
+
// ── Skip conditions ─────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
it("skips when devDomain is empty", () => {
|
|
35
|
+
writeConfig(`export default defineConfig({ plugins: [] })`);
|
|
36
|
+
const result = patchViteAllowedHosts(configPath, "");
|
|
37
|
+
expect(result.patched).toBe(false);
|
|
38
|
+
expect(result.reason).toBe("no devDomain configured");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("skips when file cannot be read", () => {
|
|
42
|
+
const result = patchViteAllowedHosts("/nonexistent/vite.config.ts", DEV_DOMAIN);
|
|
43
|
+
expect(result.patched).toBe(false);
|
|
44
|
+
expect(result.reason).toBe("could not read vite config");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("skips when domain pattern already present", () => {
|
|
48
|
+
writeConfig(`
|
|
49
|
+
export default defineConfig({
|
|
50
|
+
server: {
|
|
51
|
+
allowedHosts: ["${DOMAIN_PATTERN}"],
|
|
52
|
+
},
|
|
53
|
+
plugins: [],
|
|
54
|
+
})`);
|
|
55
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
56
|
+
expect(result.patched).toBe(false);
|
|
57
|
+
expect(result.reason).toBe("domain already present");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("skips when allowedHosts is true", () => {
|
|
61
|
+
writeConfig(`
|
|
62
|
+
export default defineConfig({
|
|
63
|
+
server: {
|
|
64
|
+
allowedHosts: true,
|
|
65
|
+
},
|
|
66
|
+
plugins: [],
|
|
67
|
+
})`);
|
|
68
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
69
|
+
expect(result.patched).toBe(false);
|
|
70
|
+
expect(result.reason).toBe("already allows all hosts (true)");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('skips when allowedHosts is "all"', () => {
|
|
74
|
+
writeConfig(`
|
|
75
|
+
export default defineConfig({
|
|
76
|
+
server: {
|
|
77
|
+
allowedHosts: "all",
|
|
78
|
+
},
|
|
79
|
+
plugins: [],
|
|
80
|
+
})`);
|
|
81
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
82
|
+
expect(result.patched).toBe(false);
|
|
83
|
+
expect(result.reason).toBe('already allows all hosts ("all")');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns unrecognized for non-matching format", () => {
|
|
87
|
+
writeConfig(`// just a comment, no config object`);
|
|
88
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
89
|
+
expect(result.patched).toBe(false);
|
|
90
|
+
expect(result.reason).toBe("unrecognized config format");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── Inline defineConfig ─────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
it("injects server block into inline defineConfig", () => {
|
|
96
|
+
writeConfig(`import { defineConfig } from "vite"
|
|
97
|
+
import react from "@vitejs/plugin-react"
|
|
98
|
+
|
|
99
|
+
export default defineConfig({
|
|
100
|
+
plugins: [react()],
|
|
101
|
+
})`);
|
|
102
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
103
|
+
expect(result.patched).toBe(true);
|
|
104
|
+
const output = readConfig();
|
|
105
|
+
expect(output).toContain(`server: {`);
|
|
106
|
+
expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
|
|
107
|
+
expect(output).toContain(`plugins: [react()]`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ── Variable-assigned defineConfig ──────────────────────────────
|
|
111
|
+
|
|
112
|
+
it("injects server block into variable-assigned defineConfig", () => {
|
|
113
|
+
writeConfig(`import { defineConfig } from 'vite'
|
|
114
|
+
import viteReact from '@vitejs/plugin-react'
|
|
115
|
+
|
|
116
|
+
const config = defineConfig({
|
|
117
|
+
plugins: [
|
|
118
|
+
viteReact(),
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
export default config`);
|
|
123
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
124
|
+
expect(result.patched).toBe(true);
|
|
125
|
+
const output = readConfig();
|
|
126
|
+
expect(output).toContain(`server: {`);
|
|
127
|
+
expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
|
|
128
|
+
expect(output).toContain(`export default config`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── Plain export default { } ────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
it("injects server block into plain export default object", () => {
|
|
134
|
+
writeConfig(`export default {
|
|
135
|
+
plugins: [],
|
|
136
|
+
}`);
|
|
137
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
138
|
+
expect(result.patched).toBe(true);
|
|
139
|
+
const output = readConfig();
|
|
140
|
+
expect(output).toContain(`server: {`);
|
|
141
|
+
expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ── Existing server block, no allowedHosts ──────────────────────
|
|
145
|
+
|
|
146
|
+
it("merges allowedHosts into existing server block", () => {
|
|
147
|
+
writeConfig(`export default defineConfig({
|
|
148
|
+
server: {
|
|
149
|
+
port: 3000,
|
|
150
|
+
host: "0.0.0.0",
|
|
151
|
+
},
|
|
152
|
+
plugins: [],
|
|
153
|
+
})`);
|
|
154
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
155
|
+
expect(result.patched).toBe(true);
|
|
156
|
+
const output = readConfig();
|
|
157
|
+
expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
|
|
158
|
+
expect(output).toContain(`port: 3000`);
|
|
159
|
+
expect(output).toContain(`host: "0.0.0.0"`);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ── Existing allowedHosts array, different domain ───────────────
|
|
163
|
+
|
|
164
|
+
it("appends domain to existing allowedHosts array", () => {
|
|
165
|
+
writeConfig(`export default defineConfig({
|
|
166
|
+
server: {
|
|
167
|
+
allowedHosts: ["other.example.com"],
|
|
168
|
+
},
|
|
169
|
+
plugins: [],
|
|
170
|
+
})`);
|
|
171
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
172
|
+
expect(result.patched).toBe(true);
|
|
173
|
+
const output = readConfig();
|
|
174
|
+
expect(output).toContain(`"${DOMAIN_PATTERN}"`);
|
|
175
|
+
expect(output).toContain(`"other.example.com"`);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── Real-world configs from Workstellar repos ───────────────────
|
|
179
|
+
|
|
180
|
+
it("patches nft-indexer-provisioner style config", () => {
|
|
181
|
+
writeConfig(`import { defineConfig } from "vite"
|
|
182
|
+
import react from "@vitejs/plugin-react"
|
|
183
|
+
import tailwindcss from "@tailwindcss/vite"
|
|
184
|
+
import { resolve } from "path"
|
|
185
|
+
|
|
186
|
+
export default defineConfig({
|
|
187
|
+
plugins: [react(), tailwindcss()],
|
|
188
|
+
resolve: {
|
|
189
|
+
alias: {
|
|
190
|
+
"@": resolve(__dirname, "./src"),
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
})`);
|
|
194
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
195
|
+
expect(result.patched).toBe(true);
|
|
196
|
+
const output = readConfig();
|
|
197
|
+
expect(output).toContain(`server: {`);
|
|
198
|
+
expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
|
|
199
|
+
// Preserve existing content
|
|
200
|
+
expect(output).toContain(`plugins: [react(), tailwindcss()]`);
|
|
201
|
+
expect(output).toContain(`resolve:`);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("patches clean-it-platform style config (variable assignment)", () => {
|
|
205
|
+
writeConfig(`import { defineConfig } from 'vite'
|
|
206
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
207
|
+
import viteReact from '@vitejs/plugin-react'
|
|
208
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
209
|
+
|
|
210
|
+
const config = defineConfig({
|
|
211
|
+
plugins: [
|
|
212
|
+
tailwindcss(),
|
|
213
|
+
tanstackStart(),
|
|
214
|
+
viteReact(),
|
|
215
|
+
],
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
export default config`);
|
|
219
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
220
|
+
expect(result.patched).toBe(true);
|
|
221
|
+
const output = readConfig();
|
|
222
|
+
expect(output).toContain(`server: {`);
|
|
223
|
+
expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
|
|
224
|
+
expect(output).toContain(`export default config`);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("skips nft-marketplace style config (already has allowedHosts: true)", () => {
|
|
228
|
+
writeConfig(`import { defineConfig } from 'vite'
|
|
229
|
+
import viteReact from '@vitejs/plugin-react'
|
|
230
|
+
|
|
231
|
+
const config = defineConfig({
|
|
232
|
+
server: {
|
|
233
|
+
allowedHosts: true,
|
|
234
|
+
},
|
|
235
|
+
plugins: [
|
|
236
|
+
viteReact(),
|
|
237
|
+
],
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
export default config`);
|
|
241
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
242
|
+
expect(result.patched).toBe(false);
|
|
243
|
+
expect(result.reason).toBe("already allows all hosts (true)");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("patches degen-safe style config (variable, no server block, extra options)", () => {
|
|
247
|
+
writeConfig(`import { defineConfig } from 'vite'
|
|
248
|
+
import viteReact from '@vitejs/plugin-react'
|
|
249
|
+
|
|
250
|
+
const config = defineConfig({
|
|
251
|
+
plugins: [
|
|
252
|
+
viteReact(),
|
|
253
|
+
],
|
|
254
|
+
optimizeDeps: {
|
|
255
|
+
include: ['@raydium-io/raydium-sdk-v2'],
|
|
256
|
+
},
|
|
257
|
+
ssr: {
|
|
258
|
+
external: ['lodash'],
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
export default config`);
|
|
263
|
+
const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
|
|
264
|
+
expect(result.patched).toBe(true);
|
|
265
|
+
const output = readConfig();
|
|
266
|
+
expect(output).toContain(`server: {`);
|
|
267
|
+
expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
|
|
268
|
+
expect(output).toContain(`optimizeDeps:`);
|
|
269
|
+
expect(output).toContain(`ssr:`);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
2
|
+
|
|
3
|
+
export interface PatchResult {
|
|
4
|
+
patched: boolean;
|
|
5
|
+
reason: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Patch a Vite config file to include `server.allowedHosts` with the given devDomain.
|
|
10
|
+
*
|
|
11
|
+
* Handles these real-world patterns:
|
|
12
|
+
* 1. `export default defineConfig({ ... })`
|
|
13
|
+
* 2. `const config = defineConfig({ ... }); export default config`
|
|
14
|
+
* 3. `export default { ... }` (plain object, no defineConfig)
|
|
15
|
+
* 4. Existing `server:` block — merges allowedHosts into it
|
|
16
|
+
* 5. Existing `allowedHosts` array — appends domain if missing
|
|
17
|
+
*
|
|
18
|
+
* Skip conditions:
|
|
19
|
+
* - File already contains the domain pattern string
|
|
20
|
+
* - `allowedHosts` is set to `true` or `"all"` (already permissive)
|
|
21
|
+
* - devDomain is empty
|
|
22
|
+
*/
|
|
23
|
+
export function patchViteAllowedHosts(
|
|
24
|
+
viteConfigPath: string,
|
|
25
|
+
devDomain: string
|
|
26
|
+
): PatchResult {
|
|
27
|
+
if (!devDomain) {
|
|
28
|
+
return { patched: false, reason: "no devDomain configured" };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let content: string;
|
|
32
|
+
try {
|
|
33
|
+
content = readFileSync(viteConfigPath, "utf-8");
|
|
34
|
+
} catch {
|
|
35
|
+
return { patched: false, reason: "could not read vite config" };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const domainPattern = `.${devDomain}`;
|
|
39
|
+
|
|
40
|
+
// Skip if domain pattern already present in the file
|
|
41
|
+
if (content.includes(domainPattern)) {
|
|
42
|
+
return { patched: false, reason: "domain already present" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Skip if allowedHosts is already set to a permissive value
|
|
46
|
+
if (/allowedHosts\s*:\s*true/.test(content)) {
|
|
47
|
+
return { patched: false, reason: "already allows all hosts (true)" };
|
|
48
|
+
}
|
|
49
|
+
if (/allowedHosts\s*:\s*["']all["']/.test(content)) {
|
|
50
|
+
return { patched: false, reason: 'already allows all hosts ("all")' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const entry = `"${domainPattern}"`;
|
|
54
|
+
let patched = false;
|
|
55
|
+
|
|
56
|
+
// Case 1: Existing allowedHosts array — append our domain
|
|
57
|
+
if (/allowedHosts\s*:\s*\[/.test(content)) {
|
|
58
|
+
content = content.replace(
|
|
59
|
+
/(allowedHosts\s*:\s*\[)/,
|
|
60
|
+
`$1${entry}, `
|
|
61
|
+
);
|
|
62
|
+
patched = true;
|
|
63
|
+
}
|
|
64
|
+
// Case 2: Existing server block without allowedHosts — inject allowedHosts
|
|
65
|
+
else if (/server\s*:\s*\{/.test(content)) {
|
|
66
|
+
content = content.replace(
|
|
67
|
+
/(server\s*:\s*\{)/,
|
|
68
|
+
`$1\n allowedHosts: [${entry}],`
|
|
69
|
+
);
|
|
70
|
+
patched = true;
|
|
71
|
+
}
|
|
72
|
+
// Case 3: defineConfig({ — add server block
|
|
73
|
+
else if (/defineConfig\s*\(\s*\{/.test(content)) {
|
|
74
|
+
content = content.replace(
|
|
75
|
+
/(defineConfig\s*\(\s*\{)/,
|
|
76
|
+
`$1\n server: {\n allowedHosts: [${entry}],\n },`
|
|
77
|
+
);
|
|
78
|
+
patched = true;
|
|
79
|
+
}
|
|
80
|
+
// Case 4: export default { — add server block
|
|
81
|
+
else if (/export\s+default\s*\{/.test(content)) {
|
|
82
|
+
content = content.replace(
|
|
83
|
+
/(export\s+default\s*\{)/,
|
|
84
|
+
`$1\n server: {\n allowedHosts: [${entry}],\n },`
|
|
85
|
+
);
|
|
86
|
+
patched = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!patched) {
|
|
90
|
+
return { patched: false, reason: "unrecognized config format" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
writeFileSync(viteConfigPath, content);
|
|
95
|
+
} catch {
|
|
96
|
+
return { patched: false, reason: "could not write vite config" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { patched: true, reason: "added allowedHosts" };
|
|
100
|
+
}
|