@impulselab/cli 0.1.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/index.d.ts +1 -0
- package/dist/index.js +891 -0
- package/package.json +33 -0
- package/src/commands/add.test.ts +147 -0
- package/src/commands/add.ts +335 -0
- package/src/commands/init.ts +114 -0
- package/src/commands/list.ts +79 -0
- package/src/config/config-path.ts +7 -0
- package/src/config/has-config.ts +9 -0
- package/src/config/index.ts +4 -0
- package/src/config/read-config.ts +20 -0
- package/src/config/write-config.ts +11 -0
- package/src/config.test.ts +64 -0
- package/src/index.ts +64 -0
- package/src/installer.ts +71 -0
- package/src/registry/fetch-module-file.ts +21 -0
- package/src/registry/fetch-module-manifest.ts +43 -0
- package/src/registry/github-urls.ts +13 -0
- package/src/registry/index.ts +5 -0
- package/src/registry/list-available-modules.ts +113 -0
- package/src/registry/parse-module-id.ts +30 -0
- package/src/registry/registry.test.ts +181 -0
- package/src/schemas/impulse-config.ts +21 -0
- package/src/schemas/index.ts +9 -0
- package/src/schemas/module-dependency.ts +3 -0
- package/src/schemas/module-file.ts +8 -0
- package/src/schemas/module-manifest.ts +23 -0
- package/src/schemas/module-transform.ts +15 -0
- package/src/transforms/add-env.ts +53 -0
- package/src/transforms/add-nav-item.test.ts +125 -0
- package/src/transforms/add-nav-item.ts +70 -0
- package/src/transforms/append-export.test.ts +50 -0
- package/src/transforms/append-export.ts +34 -0
- package/src/transforms/index.ts +32 -0
- package/src/transforms/merge-schema.test.ts +70 -0
- package/src/transforms/merge-schema.ts +35 -0
- package/src/transforms/register-route.test.ts +177 -0
- package/src/transforms/register-route.ts +47 -0
- package/src/types.ts +9 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fsExtra from "fs-extra";
|
|
2
|
+
const { readFile, outputFile, pathExists } = fsExtra;
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registers an oRPC route in the router file.
|
|
7
|
+
* target: path to the router file (e.g. "src/server/api/router.ts")
|
|
8
|
+
* value: the route line to add (e.g. "auth: authRouter")
|
|
9
|
+
*/
|
|
10
|
+
export async function registerRoute(
|
|
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
|
+
throw new Error(
|
|
20
|
+
`register-route: target file not found: ${target}. Please ensure your router file exists.`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const content = await readFile(file, "utf-8");
|
|
25
|
+
if (content.includes(value)) return;
|
|
26
|
+
|
|
27
|
+
// Find the router object closing brace and insert before it.
|
|
28
|
+
// Anchored to start-of-line (multiline) with [ \t]* (no newlines) to avoid
|
|
29
|
+
// matching `}` on one line with `satisfies`/`as` on a subsequent line.
|
|
30
|
+
const routerPattern = /^([ \t]*\})[ \t]*(?:satisfies|as)\s/m;
|
|
31
|
+
const match = routerPattern.exec(content);
|
|
32
|
+
|
|
33
|
+
if (!match) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`register-route: could not find router closing brace in ${target}. Add "${value}" manually.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!dryRun) {
|
|
40
|
+
const insertPos = match.index;
|
|
41
|
+
const newContent =
|
|
42
|
+
content.slice(0, insertPos) +
|
|
43
|
+
` ${value},\n` +
|
|
44
|
+
content.slice(insertPos);
|
|
45
|
+
await outputFile(file, newContent, "utf-8");
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { ModuleFileSchema } from "./schemas/module-file";
|
|
2
|
+
export type { ModuleFile } from "./schemas/module-file";
|
|
3
|
+
export { ModuleDependencySchema } from "./schemas/module-dependency";
|
|
4
|
+
export { ModuleTransformSchema } from "./schemas/module-transform";
|
|
5
|
+
export type { ModuleTransform } from "./schemas/module-transform";
|
|
6
|
+
export { ModuleManifestSchema } from "./schemas/module-manifest";
|
|
7
|
+
export type { ModuleManifest } from "./schemas/module-manifest";
|
|
8
|
+
export { ImpulseConfigSchema } from "./schemas/impulse-config";
|
|
9
|
+
export type { ImpulseConfig } from "./schemas/impulse-config";
|