@impulselab/cli 0.1.0 → 0.1.2
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 +92 -9
- package/package.json +20 -9
- package/src/commands/add.test.ts +0 -147
- package/src/commands/add.ts +0 -335
- package/src/commands/init.ts +0 -114
- package/src/commands/list.ts +0 -79
- package/src/config/config-path.ts +0 -7
- package/src/config/has-config.ts +0 -9
- package/src/config/index.ts +0 -4
- package/src/config/read-config.ts +0 -20
- package/src/config/write-config.ts +0 -11
- package/src/config.test.ts +0 -64
- package/src/index.ts +0 -64
- package/src/installer.ts +0 -71
- package/src/registry/fetch-module-file.ts +0 -21
- package/src/registry/fetch-module-manifest.ts +0 -43
- package/src/registry/github-urls.ts +0 -13
- package/src/registry/index.ts +0 -5
- package/src/registry/list-available-modules.ts +0 -113
- package/src/registry/parse-module-id.ts +0 -30
- package/src/registry/registry.test.ts +0 -181
- package/src/schemas/impulse-config.ts +0 -21
- package/src/schemas/index.ts +0 -9
- package/src/schemas/module-dependency.ts +0 -3
- package/src/schemas/module-file.ts +0 -8
- package/src/schemas/module-manifest.ts +0 -23
- package/src/schemas/module-transform.ts +0 -15
- package/src/transforms/add-env.ts +0 -53
- package/src/transforms/add-nav-item.test.ts +0 -125
- package/src/transforms/add-nav-item.ts +0 -70
- package/src/transforms/append-export.test.ts +0 -50
- package/src/transforms/append-export.ts +0 -34
- package/src/transforms/index.ts +0 -32
- package/src/transforms/merge-schema.test.ts +0 -70
- package/src/transforms/merge-schema.ts +0 -35
- package/src/transforms/register-route.test.ts +0 -177
- package/src/transforms/register-route.ts +0 -47
- package/src/types.ts +0 -9
- package/tsconfig.json +0 -8
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import fsExtra from "fs-extra";
|
|
2
|
-
const { readFile, outputFile, pathExists } = fsExtra;
|
|
3
|
-
import path from "path";
|
|
4
|
-
import * as p from "@clack/prompts";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Prompts for an env var value and appends to .env if not already present.
|
|
8
|
-
* target: path to .env file (e.g. ".env")
|
|
9
|
-
* value: the env var name (e.g. "AUTH_SECRET")
|
|
10
|
-
*/
|
|
11
|
-
export async function addEnv(
|
|
12
|
-
target: string,
|
|
13
|
-
value: string,
|
|
14
|
-
cwd: string,
|
|
15
|
-
dryRun: boolean
|
|
16
|
-
): Promise<void> {
|
|
17
|
-
const file = path.join(cwd, target);
|
|
18
|
-
let content = "";
|
|
19
|
-
|
|
20
|
-
if (await pathExists(file)) {
|
|
21
|
-
content = await readFile(file, "utf-8");
|
|
22
|
-
// Check if key already set — escape value to avoid regex injection
|
|
23
|
-
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
-
const keyPattern = new RegExp(`^${escaped}=`, "m");
|
|
25
|
-
if (keyPattern.test(content)) return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let envValue = "";
|
|
29
|
-
if (!dryRun) {
|
|
30
|
-
const answer = await p.text({
|
|
31
|
-
message: `Enter value for ${value} (leave blank to skip):`,
|
|
32
|
-
placeholder: "",
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
if (p.isCancel(answer)) {
|
|
36
|
-
p.log.warn(`Skipped env var: ${value}`);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
envValue = String(answer);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const line = `${value}=${envValue}`;
|
|
44
|
-
|
|
45
|
-
if (!dryRun) {
|
|
46
|
-
const newContent = content
|
|
47
|
-
? content.endsWith("\n")
|
|
48
|
-
? `${content}${line}\n`
|
|
49
|
-
: `${content}\n${line}\n`
|
|
50
|
-
: `${line}\n`;
|
|
51
|
-
await outputFile(file, newContent, "utf-8");
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
3
|
-
import { outputFile, readFile } from "fs-extra";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import os from "os";
|
|
6
|
-
import { addNavItem } from "./add-nav-item";
|
|
7
|
-
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
tmpDir = await mkdtemp(path.join(os.tmpdir(), "impulse-nav-test-"));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("addNavItem", () => {
|
|
19
|
-
it("inserts item into a flat nav array", async () => {
|
|
20
|
-
const navFile = path.join(tmpDir, "src/config/nav.ts");
|
|
21
|
-
await outputFile(
|
|
22
|
-
navFile,
|
|
23
|
-
`export const navItems = [\n { title: "Home", href: "/" },\n];\n`
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
await addNavItem(
|
|
27
|
-
"src/config/nav.ts",
|
|
28
|
-
'{ title: "Auth", href: "/auth" }',
|
|
29
|
-
tmpDir,
|
|
30
|
-
false
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const result = await readFile(navFile, "utf-8");
|
|
34
|
-
expect(result).toContain('{ title: "Auth", href: "/auth" }');
|
|
35
|
-
expect(result).toContain('{ title: "Home", href: "/" }');
|
|
36
|
-
// insertion uses trailing newline — closing ] must be on its own line
|
|
37
|
-
expect(result).toMatch(/\},\n\]/);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("inserts item into a typed array declaration (const navItems: NavItem[] = [...])", async () => {
|
|
41
|
-
const navFile = path.join(tmpDir, "src/config/nav.ts");
|
|
42
|
-
await outputFile(
|
|
43
|
-
navFile,
|
|
44
|
-
`export const navItems: NavItem[] = [\n { title: "Home", href: "/" },\n];\n`
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
await addNavItem(
|
|
48
|
-
"src/config/nav.ts",
|
|
49
|
-
'{ title: "Auth", href: "/auth" }',
|
|
50
|
-
tmpDir,
|
|
51
|
-
false
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const result = await readFile(navFile, "utf-8");
|
|
55
|
-
expect(result).toContain('{ title: "Auth", href: "/auth" }');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("inserts item into a nav array with nested arrays (nested subnav)", async () => {
|
|
59
|
-
const navFile = path.join(tmpDir, "src/config/nav.ts");
|
|
60
|
-
await outputFile(
|
|
61
|
-
navFile,
|
|
62
|
-
`export const navItems = [\n {\n title: "Settings",\n href: "/settings",\n children: [\n { title: "Profile", href: "/settings/profile" },\n ],\n },\n];\n`
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
await addNavItem(
|
|
66
|
-
"src/config/nav.ts",
|
|
67
|
-
'{ title: "Auth", href: "/auth" }',
|
|
68
|
-
tmpDir,
|
|
69
|
-
false
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const result = await readFile(navFile, "utf-8");
|
|
73
|
-
// Item should be appended before the outer closing bracket
|
|
74
|
-
expect(result).toContain('{ title: "Auth", href: "/auth" }');
|
|
75
|
-
// Nested structure must be preserved
|
|
76
|
-
expect(result).toContain("children:");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("does not insert duplicate items", async () => {
|
|
80
|
-
const navFile = path.join(tmpDir, "src/config/nav.ts");
|
|
81
|
-
const item = '{ title: "Auth", href: "/auth" }';
|
|
82
|
-
await outputFile(navFile, `export const navItems = [\n ${item},\n];\n`);
|
|
83
|
-
|
|
84
|
-
await addNavItem("src/config/nav.ts", item, tmpDir, false);
|
|
85
|
-
|
|
86
|
-
const result = await readFile(navFile, "utf-8");
|
|
87
|
-
expect(result.split(item).length - 1).toBe(1);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("does not write file in dry-run mode", async () => {
|
|
91
|
-
const navFile = path.join(tmpDir, "src/config/nav.ts");
|
|
92
|
-
const original = `export const navItems = [\n { title: "Home", href: "/" },\n];\n`;
|
|
93
|
-
await outputFile(navFile, original);
|
|
94
|
-
|
|
95
|
-
await addNavItem(
|
|
96
|
-
"src/config/nav.ts",
|
|
97
|
-
'{ title: "Auth", href: "/auth" }',
|
|
98
|
-
tmpDir,
|
|
99
|
-
true
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const result = await readFile(navFile, "utf-8");
|
|
103
|
-
expect(result).toBe(original);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("throws if file not found", async () => {
|
|
107
|
-
await expect(
|
|
108
|
-
addNavItem("nonexistent/nav.ts", '{ title: "X" }', tmpDir, false)
|
|
109
|
-
).rejects.toThrow("target file not found");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("throws if no nav array can be located", async () => {
|
|
113
|
-
const navFile = path.join(tmpDir, "src/config/nav.ts");
|
|
114
|
-
await outputFile(navFile, `export const foo = "bar";\n`);
|
|
115
|
-
|
|
116
|
-
await expect(
|
|
117
|
-
addNavItem(
|
|
118
|
-
"src/config/nav.ts",
|
|
119
|
-
'{ title: "Auth", href: "/auth" }',
|
|
120
|
-
tmpDir,
|
|
121
|
-
false
|
|
122
|
-
)
|
|
123
|
-
).rejects.toThrow("could not locate nav array");
|
|
124
|
-
});
|
|
125
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import fsExtra from "fs-extra";
|
|
2
|
-
const { readFile, outputFile, pathExists } = fsExtra;
|
|
3
|
-
import path from "path";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Adds a navigation item to the sidebar config file.
|
|
7
|
-
* target: path to the nav config file (e.g. "src/config/nav.ts")
|
|
8
|
-
* value: the nav item JSON string to inject
|
|
9
|
-
* (e.g. '{ title: "Auth", href: "/auth", icon: "lock" }')
|
|
10
|
-
*/
|
|
11
|
-
export async function addNavItem(
|
|
12
|
-
target: string,
|
|
13
|
-
value: string,
|
|
14
|
-
cwd: string,
|
|
15
|
-
dryRun: boolean
|
|
16
|
-
): Promise<void> {
|
|
17
|
-
const file = path.join(cwd, target);
|
|
18
|
-
|
|
19
|
-
if (!(await pathExists(file))) {
|
|
20
|
-
throw new Error(
|
|
21
|
-
`add-nav-item: target file not found: ${target}`
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const content = await readFile(file, "utf-8");
|
|
26
|
-
if (content.includes(value)) return;
|
|
27
|
-
|
|
28
|
-
// Find the opening bracket of the navItems array using a pattern match,
|
|
29
|
-
// then walk brackets to find the matching close — handles nested arrays.
|
|
30
|
-
// The type annotation group handles typed declarations: `const navItems: NavItem[] = [`
|
|
31
|
-
const openPattern = /(?:const\s+\w+(?:\s*:\s*[\w<>\[\], ]+)?\s*=\s*\[|items\s*:\s*\[)/;
|
|
32
|
-
const openMatch = openPattern.exec(content);
|
|
33
|
-
|
|
34
|
-
if (!openMatch) {
|
|
35
|
-
throw new Error(
|
|
36
|
-
`add-nav-item: could not locate nav array in ${target}. Add "${value}" manually.`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Position of the '[' that opens the array
|
|
41
|
-
const bracketStart = openMatch.index + openMatch[0].length - 1;
|
|
42
|
-
|
|
43
|
-
// Walk forward counting brackets to find the matching ']'
|
|
44
|
-
let depth = 0;
|
|
45
|
-
let insertPos = -1;
|
|
46
|
-
for (let i = bracketStart; i < content.length; i++) {
|
|
47
|
-
if (content[i] === "[") depth++;
|
|
48
|
-
else if (content[i] === "]") {
|
|
49
|
-
depth--;
|
|
50
|
-
if (depth === 0) {
|
|
51
|
-
insertPos = i;
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (insertPos === -1) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
`add-nav-item: could not find closing bracket for nav array in ${target}. Add "${value}" manually.`
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (!dryRun) {
|
|
64
|
-
const newContent =
|
|
65
|
-
content.slice(0, insertPos) +
|
|
66
|
-
` ${value},\n` +
|
|
67
|
-
content.slice(insertPos);
|
|
68
|
-
await outputFile(file, newContent, "utf-8");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
3
|
-
import { outputFile, readFile } from "fs-extra";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import os from "os";
|
|
6
|
-
import { appendExport } from "./append-export";
|
|
7
|
-
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
tmpDir = await mkdtemp(path.join(os.tmpdir(), "impulse-test-"));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("appendExport", () => {
|
|
19
|
-
it("creates the file if it does not exist", async () => {
|
|
20
|
-
await appendExport("src/index.ts", "export * from './auth'", tmpDir, false);
|
|
21
|
-
const content = await readFile(path.join(tmpDir, "src/index.ts"), "utf-8");
|
|
22
|
-
expect(content).toBe("export * from './auth'\n");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("appends to existing file", async () => {
|
|
26
|
-
await outputFile(
|
|
27
|
-
path.join(tmpDir, "src/index.ts"),
|
|
28
|
-
"export * from './existing'\n"
|
|
29
|
-
);
|
|
30
|
-
await appendExport("src/index.ts", "export * from './auth'", tmpDir, false);
|
|
31
|
-
const content = await readFile(path.join(tmpDir, "src/index.ts"), "utf-8");
|
|
32
|
-
expect(content).toBe("export * from './existing'\nexport * from './auth'\n");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("does not duplicate if already present", async () => {
|
|
36
|
-
await outputFile(
|
|
37
|
-
path.join(tmpDir, "src/index.ts"),
|
|
38
|
-
"export * from './auth'\n"
|
|
39
|
-
);
|
|
40
|
-
await appendExport("src/index.ts", "export * from './auth'", tmpDir, false);
|
|
41
|
-
const content = await readFile(path.join(tmpDir, "src/index.ts"), "utf-8");
|
|
42
|
-
expect(content).toBe("export * from './auth'\n");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("does not write in dry-run mode", async () => {
|
|
46
|
-
await appendExport("src/index.ts", "export * from './auth'", tmpDir, true);
|
|
47
|
-
const { pathExists } = await import("fs-extra");
|
|
48
|
-
expect(await pathExists(path.join(tmpDir, "src/index.ts"))).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import fsExtra from "fs-extra";
|
|
2
|
-
const { readFile, outputFile, pathExists } = fsExtra;
|
|
3
|
-
import path from "path";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Appends an export line to a barrel file if not already present.
|
|
7
|
-
* target: relative path to the barrel file (e.g. "src/server/api/index.ts")
|
|
8
|
-
* value: the export line to append (e.g. "export * from './auth'")
|
|
9
|
-
*/
|
|
10
|
-
export async function appendExport(
|
|
11
|
-
target: string,
|
|
12
|
-
value: string,
|
|
13
|
-
cwd: string,
|
|
14
|
-
dryRun: boolean
|
|
15
|
-
): Promise<void> {
|
|
16
|
-
const file = path.join(cwd, target);
|
|
17
|
-
|
|
18
|
-
if (!(await pathExists(file))) {
|
|
19
|
-
if (!dryRun) {
|
|
20
|
-
await outputFile(file, `${value}\n`, "utf-8");
|
|
21
|
-
}
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const content = await readFile(file, "utf-8");
|
|
26
|
-
if (content.includes(value)) return; // already present
|
|
27
|
-
|
|
28
|
-
if (!dryRun) {
|
|
29
|
-
const newContent = content.endsWith("\n")
|
|
30
|
-
? `${content}${value}\n`
|
|
31
|
-
: `${content}\n${value}\n`;
|
|
32
|
-
await outputFile(file, newContent, "utf-8");
|
|
33
|
-
}
|
|
34
|
-
}
|
package/src/transforms/index.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { appendExport } from "./append-export";
|
|
2
|
-
import { registerRoute } from "./register-route";
|
|
3
|
-
import { addNavItem } from "./add-nav-item";
|
|
4
|
-
import { mergeSchema } from "./merge-schema";
|
|
5
|
-
import { addEnv } from "./add-env";
|
|
6
|
-
import type { ModuleTransform } from "../types";
|
|
7
|
-
|
|
8
|
-
export async function runTransform(
|
|
9
|
-
transform: ModuleTransform,
|
|
10
|
-
cwd: string,
|
|
11
|
-
dryRun: boolean
|
|
12
|
-
): Promise<void> {
|
|
13
|
-
switch (transform.type) {
|
|
14
|
-
case "append-export":
|
|
15
|
-
await appendExport(transform.target, transform.value, cwd, dryRun);
|
|
16
|
-
break;
|
|
17
|
-
case "register-route":
|
|
18
|
-
await registerRoute(transform.target, transform.value, cwd, dryRun);
|
|
19
|
-
break;
|
|
20
|
-
case "add-nav-item":
|
|
21
|
-
await addNavItem(transform.target, transform.value, cwd, dryRun);
|
|
22
|
-
break;
|
|
23
|
-
case "merge-schema":
|
|
24
|
-
await mergeSchema(transform.target, transform.value, cwd, dryRun);
|
|
25
|
-
break;
|
|
26
|
-
case "add-env":
|
|
27
|
-
await addEnv(transform.target, transform.value, cwd, dryRun);
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export { appendExport, registerRoute, addNavItem, mergeSchema, addEnv };
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
3
|
-
import { outputFile, readFile } from "fs-extra";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import os from "os";
|
|
6
|
-
import { mergeSchema } from "./merge-schema";
|
|
7
|
-
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
tmpDir = await mkdtemp(path.join(os.tmpdir(), "impulse-test-"));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("mergeSchema", () => {
|
|
19
|
-
it("creates schema index if it does not exist", async () => {
|
|
20
|
-
await mergeSchema(
|
|
21
|
-
"src/server/db/schema/index.ts",
|
|
22
|
-
"export * from './auth'",
|
|
23
|
-
tmpDir,
|
|
24
|
-
false
|
|
25
|
-
);
|
|
26
|
-
const content = await readFile(
|
|
27
|
-
path.join(tmpDir, "src/server/db/schema/index.ts"),
|
|
28
|
-
"utf-8"
|
|
29
|
-
);
|
|
30
|
-
expect(content).toBe("export * from './auth'\n");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("appends to existing schema index", async () => {
|
|
34
|
-
await outputFile(
|
|
35
|
-
path.join(tmpDir, "src/server/db/schema/index.ts"),
|
|
36
|
-
"export * from './users'\n"
|
|
37
|
-
);
|
|
38
|
-
await mergeSchema(
|
|
39
|
-
"src/server/db/schema/index.ts",
|
|
40
|
-
"export * from './auth'",
|
|
41
|
-
tmpDir,
|
|
42
|
-
false
|
|
43
|
-
);
|
|
44
|
-
const content = await readFile(
|
|
45
|
-
path.join(tmpDir, "src/server/db/schema/index.ts"),
|
|
46
|
-
"utf-8"
|
|
47
|
-
);
|
|
48
|
-
expect(content).toBe(
|
|
49
|
-
"export * from './users'\nexport * from './auth'\n"
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("skips duplicates", async () => {
|
|
54
|
-
await outputFile(
|
|
55
|
-
path.join(tmpDir, "src/server/db/schema/index.ts"),
|
|
56
|
-
"export * from './auth'\n"
|
|
57
|
-
);
|
|
58
|
-
await mergeSchema(
|
|
59
|
-
"src/server/db/schema/index.ts",
|
|
60
|
-
"export * from './auth'",
|
|
61
|
-
tmpDir,
|
|
62
|
-
false
|
|
63
|
-
);
|
|
64
|
-
const content = await readFile(
|
|
65
|
-
path.join(tmpDir, "src/server/db/schema/index.ts"),
|
|
66
|
-
"utf-8"
|
|
67
|
-
);
|
|
68
|
-
expect(content).toBe("export * from './auth'\n");
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import fsExtra from "fs-extra";
|
|
2
|
-
const { readFile, outputFile, pathExists } = fsExtra;
|
|
3
|
-
import path from "path";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Adds a Drizzle schema import to the main schema index file.
|
|
7
|
-
* target: path to the schema index (e.g. "src/server/db/schema/index.ts")
|
|
8
|
-
* value: import/export line (e.g. "export * from './auth'")
|
|
9
|
-
*/
|
|
10
|
-
export async function mergeSchema(
|
|
11
|
-
target: string,
|
|
12
|
-
value: string,
|
|
13
|
-
cwd: string,
|
|
14
|
-
dryRun: boolean
|
|
15
|
-
): Promise<void> {
|
|
16
|
-
const file = path.join(cwd, target);
|
|
17
|
-
|
|
18
|
-
if (!(await pathExists(file))) {
|
|
19
|
-
// Create new schema index
|
|
20
|
-
if (!dryRun) {
|
|
21
|
-
await outputFile(file, `${value}\n`, "utf-8");
|
|
22
|
-
}
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const content = await readFile(file, "utf-8");
|
|
27
|
-
if (content.includes(value)) return;
|
|
28
|
-
|
|
29
|
-
if (!dryRun) {
|
|
30
|
-
const newContent = content.endsWith("\n")
|
|
31
|
-
? `${content}${value}\n`
|
|
32
|
-
: `${content}\n${value}\n`;
|
|
33
|
-
await outputFile(file, newContent, "utf-8");
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
3
|
-
import { outputFile, readFile } from "fs-extra";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import os from "os";
|
|
6
|
-
import { registerRoute } from "./register-route";
|
|
7
|
-
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
tmpDir = await mkdtemp(path.join(os.tmpdir(), "impulse-route-test-"));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const satisfiesRouter = `import { createRouter } from "@orpc/server";
|
|
19
|
-
import { healthRouter } from "./health";
|
|
20
|
-
|
|
21
|
-
export const appRouter = createRouter({
|
|
22
|
-
health: healthRouter,
|
|
23
|
-
} satisfies RouterInput);
|
|
24
|
-
`;
|
|
25
|
-
|
|
26
|
-
const asRouter = `import { createRouter } from "@orpc/server";
|
|
27
|
-
import { healthRouter } from "./health";
|
|
28
|
-
|
|
29
|
-
export const appRouter = {
|
|
30
|
-
health: healthRouter,
|
|
31
|
-
} as AppRouter;
|
|
32
|
-
`;
|
|
33
|
-
|
|
34
|
-
// Router that contains an inline `as` type cast inside a nested expression
|
|
35
|
-
// — the fix must NOT insert into the inline cast.
|
|
36
|
-
const routerWithInlineAs = `import { createRouter } from "@orpc/server";
|
|
37
|
-
|
|
38
|
-
function helper(x: unknown) {
|
|
39
|
-
return (x as string).toUpperCase();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export const appRouter = createRouter({
|
|
43
|
-
health: healthRouter,
|
|
44
|
-
} satisfies RouterInput);
|
|
45
|
-
`;
|
|
46
|
-
|
|
47
|
-
// Router where another typed object's `}` appears on its own line before
|
|
48
|
-
// `satisfies`/`as` on the next line — cross-line match must be avoided.
|
|
49
|
-
const routerWithCrossLineSatisfies = `const config = {
|
|
50
|
-
setting: true,
|
|
51
|
-
}
|
|
52
|
-
satisfies ConfigType;
|
|
53
|
-
|
|
54
|
-
export const appRouter = createRouter({
|
|
55
|
-
health: healthRouter,
|
|
56
|
-
} satisfies RouterInput);
|
|
57
|
-
`;
|
|
58
|
-
|
|
59
|
-
describe("registerRoute", () => {
|
|
60
|
-
it("inserts route into a 'satisfies' router", async () => {
|
|
61
|
-
const routerFile = path.join(tmpDir, "src/server/api/router.ts");
|
|
62
|
-
await outputFile(routerFile, satisfiesRouter);
|
|
63
|
-
|
|
64
|
-
await registerRoute(
|
|
65
|
-
"src/server/api/router.ts",
|
|
66
|
-
"auth: authRouter",
|
|
67
|
-
tmpDir,
|
|
68
|
-
false
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const result = await readFile(routerFile, "utf-8");
|
|
72
|
-
expect(result).toContain("auth: authRouter");
|
|
73
|
-
expect(result).toContain("health: healthRouter");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("inserts route into an 'as' router", async () => {
|
|
77
|
-
const routerFile = path.join(tmpDir, "src/server/api/router.ts");
|
|
78
|
-
await outputFile(routerFile, asRouter);
|
|
79
|
-
|
|
80
|
-
await registerRoute(
|
|
81
|
-
"src/server/api/router.ts",
|
|
82
|
-
"auth: authRouter",
|
|
83
|
-
tmpDir,
|
|
84
|
-
false
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const result = await readFile(routerFile, "utf-8");
|
|
88
|
-
expect(result).toContain("auth: authRouter");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("does not match when } and satisfies/as are on separate lines (cross-line false match)", async () => {
|
|
92
|
-
const routerFile = path.join(tmpDir, "src/server/api/router.ts");
|
|
93
|
-
await outputFile(routerFile, routerWithCrossLineSatisfies);
|
|
94
|
-
|
|
95
|
-
await registerRoute(
|
|
96
|
-
"src/server/api/router.ts",
|
|
97
|
-
"auth: authRouter",
|
|
98
|
-
tmpDir,
|
|
99
|
-
false
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const result = await readFile(routerFile, "utf-8");
|
|
103
|
-
// Route must appear near the appRouter closing, not near the config object
|
|
104
|
-
const insertIdx = result.indexOf("auth: authRouter");
|
|
105
|
-
const configSatisfiesIdx = result.indexOf("}\nsatisfies ConfigType");
|
|
106
|
-
expect(insertIdx).toBeGreaterThan(configSatisfiesIdx);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("does not match inline 'as' casts inside nested expressions", async () => {
|
|
110
|
-
const routerFile = path.join(tmpDir, "src/server/api/router.ts");
|
|
111
|
-
await outputFile(routerFile, routerWithInlineAs);
|
|
112
|
-
|
|
113
|
-
await registerRoute(
|
|
114
|
-
"src/server/api/router.ts",
|
|
115
|
-
"auth: authRouter",
|
|
116
|
-
tmpDir,
|
|
117
|
-
false
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
const result = await readFile(routerFile, "utf-8");
|
|
121
|
-
// Route must appear just before `} satisfies`, not before `} as string`
|
|
122
|
-
const insertIdx = result.indexOf("auth: authRouter");
|
|
123
|
-
const satisfiesIdx = result.indexOf("} satisfies");
|
|
124
|
-
expect(insertIdx).toBeGreaterThan(-1);
|
|
125
|
-
expect(insertIdx).toBeLessThan(satisfiesIdx);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("does not insert duplicate routes", async () => {
|
|
129
|
-
const routerFile = path.join(tmpDir, "src/server/api/router.ts");
|
|
130
|
-
await outputFile(routerFile, satisfiesRouter);
|
|
131
|
-
|
|
132
|
-
await registerRoute(
|
|
133
|
-
"src/server/api/router.ts",
|
|
134
|
-
"health: healthRouter",
|
|
135
|
-
tmpDir,
|
|
136
|
-
false
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
const result = await readFile(routerFile, "utf-8");
|
|
140
|
-
expect(result.split("health: healthRouter").length - 1).toBe(1);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("does not write file in dry-run mode", async () => {
|
|
144
|
-
const routerFile = path.join(tmpDir, "src/server/api/router.ts");
|
|
145
|
-
await outputFile(routerFile, satisfiesRouter);
|
|
146
|
-
|
|
147
|
-
await registerRoute(
|
|
148
|
-
"src/server/api/router.ts",
|
|
149
|
-
"auth: authRouter",
|
|
150
|
-
tmpDir,
|
|
151
|
-
true
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
const result = await readFile(routerFile, "utf-8");
|
|
155
|
-
expect(result).toBe(satisfiesRouter);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("throws if file not found", async () => {
|
|
159
|
-
await expect(
|
|
160
|
-
registerRoute("nonexistent/router.ts", "auth: authRouter", tmpDir, false)
|
|
161
|
-
).rejects.toThrow("target file not found");
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("throws if no router closing brace found", async () => {
|
|
165
|
-
const routerFile = path.join(tmpDir, "src/server/api/router.ts");
|
|
166
|
-
await outputFile(routerFile, `export const foo = "bar";\n`);
|
|
167
|
-
|
|
168
|
-
await expect(
|
|
169
|
-
registerRoute(
|
|
170
|
-
"src/server/api/router.ts",
|
|
171
|
-
"auth: authRouter",
|
|
172
|
-
tmpDir,
|
|
173
|
-
false
|
|
174
|
-
)
|
|
175
|
-
).rejects.toThrow("could not find router closing brace");
|
|
176
|
-
});
|
|
177
|
-
});
|