@impulselab/cli 0.1.0 → 0.1.1
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 +86 -8
- package/package.json +18 -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,181 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, rm, mkdir } from "fs/promises";
|
|
3
|
-
import { outputJson } from "fs-extra";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import os from "os";
|
|
6
|
-
import {
|
|
7
|
-
parseModuleId,
|
|
8
|
-
isSubModuleId,
|
|
9
|
-
fetchModuleManifest,
|
|
10
|
-
listAvailableModules,
|
|
11
|
-
} from "./index";
|
|
12
|
-
|
|
13
|
-
let tmpDir: string;
|
|
14
|
-
|
|
15
|
-
beforeEach(async () => {
|
|
16
|
-
tmpDir = await mkdtemp(path.join(os.tmpdir(), "impulse-registry-test-"));
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// parseModuleId
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
describe("parseModuleId", () => {
|
|
28
|
-
it("returns parent=name, child=null for a plain module", () => {
|
|
29
|
-
expect(parseModuleId("attio")).toEqual({ parent: "attio", child: null });
|
|
30
|
-
expect(parseModuleId("email")).toEqual({ parent: "email", child: null });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("parses parent/child for sub-module syntax", () => {
|
|
34
|
-
expect(parseModuleId("attio/quote-to-cash")).toEqual({
|
|
35
|
-
parent: "attio",
|
|
36
|
-
child: "quote-to-cash",
|
|
37
|
-
});
|
|
38
|
-
expect(parseModuleId("attio/gocardless")).toEqual({
|
|
39
|
-
parent: "attio",
|
|
40
|
-
child: "gocardless",
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("handles only the first slash (ignores nested slashes)", () => {
|
|
45
|
-
expect(parseModuleId("a/b/c")).toEqual({ parent: "a", child: "b/c" });
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// isSubModuleId
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
describe("isSubModuleId", () => {
|
|
54
|
-
it("returns false for plain module names", () => {
|
|
55
|
-
expect(isSubModuleId("attio")).toBe(false);
|
|
56
|
-
expect(isSubModuleId("email")).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("returns true for slash notation", () => {
|
|
60
|
-
expect(isSubModuleId("attio/quote-to-cash")).toBe(true);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// fetchModuleManifest — local mode
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
const baseManifest = {
|
|
69
|
-
name: "attio",
|
|
70
|
-
version: "1.0.0",
|
|
71
|
-
description: "Attio module",
|
|
72
|
-
files: [],
|
|
73
|
-
transforms: [],
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
async function writeManifest(dir: string, manifest: Record<string, unknown>): Promise<void> {
|
|
77
|
-
await mkdir(dir, { recursive: true });
|
|
78
|
-
await outputJson(path.join(dir, "module.json"), manifest);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
describe("fetchModuleManifest (local)", () => {
|
|
82
|
-
it("resolves a top-level module from localPath", async () => {
|
|
83
|
-
await writeManifest(path.join(tmpDir, "attio"), baseManifest);
|
|
84
|
-
const result = await fetchModuleManifest("attio", tmpDir);
|
|
85
|
-
expect(result.name).toBe("attio");
|
|
86
|
-
expect(result.version).toBe("1.0.0");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("resolves a sub-module from localPath using slash syntax", async () => {
|
|
90
|
-
const subManifest = {
|
|
91
|
-
name: "attio/quote-to-cash",
|
|
92
|
-
version: "1.0.0",
|
|
93
|
-
description: "Quote-to-cash sub-module",
|
|
94
|
-
parentModule: "attio",
|
|
95
|
-
files: [],
|
|
96
|
-
transforms: [],
|
|
97
|
-
};
|
|
98
|
-
await writeManifest(
|
|
99
|
-
path.join(tmpDir, "attio", "sub-modules", "quote-to-cash"),
|
|
100
|
-
subManifest
|
|
101
|
-
);
|
|
102
|
-
const result = await fetchModuleManifest("attio/quote-to-cash", tmpDir);
|
|
103
|
-
expect(result.name).toBe("attio/quote-to-cash");
|
|
104
|
-
expect(result.parentModule).toBe("attio");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("throws when local module is not found", async () => {
|
|
108
|
-
await expect(fetchModuleManifest("nonexistent", tmpDir)).rejects.toThrow(
|
|
109
|
-
"Local module not found"
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("throws when local sub-module is not found", async () => {
|
|
114
|
-
// parent exists but sub-module does not
|
|
115
|
-
await writeManifest(path.join(tmpDir, "attio"), baseManifest);
|
|
116
|
-
await expect(
|
|
117
|
-
fetchModuleManifest("attio/missing-sub", tmpDir)
|
|
118
|
-
).rejects.toThrow("Local module not found");
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
// listAvailableModules — local mode with sub-modules
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
|
|
126
|
-
describe("listAvailableModules (local)", () => {
|
|
127
|
-
it("lists top-level modules from local directory", async () => {
|
|
128
|
-
await writeManifest(path.join(tmpDir, "attio"), baseManifest);
|
|
129
|
-
await writeManifest(path.join(tmpDir, "email"), {
|
|
130
|
-
...baseManifest,
|
|
131
|
-
name: "email",
|
|
132
|
-
description: "Email module",
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
const result = await listAvailableModules(tmpDir);
|
|
136
|
-
const names = result.map((m) => m.name);
|
|
137
|
-
expect(names).toContain("attio");
|
|
138
|
-
expect(names).toContain("email");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("includes subModules array when sub-modules directory exists", async () => {
|
|
142
|
-
await writeManifest(path.join(tmpDir, "attio"), baseManifest);
|
|
143
|
-
await writeManifest(
|
|
144
|
-
path.join(tmpDir, "attio", "sub-modules", "quote-to-cash"),
|
|
145
|
-
{ ...baseManifest, name: "attio/quote-to-cash", parentModule: "attio" }
|
|
146
|
-
);
|
|
147
|
-
await writeManifest(
|
|
148
|
-
path.join(tmpDir, "attio", "sub-modules", "gocardless"),
|
|
149
|
-
{ ...baseManifest, name: "attio/gocardless", parentModule: "attio" }
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
const result = await listAvailableModules(tmpDir);
|
|
153
|
-
const attio = result.find((m) => m.name === "attio");
|
|
154
|
-
expect(attio?.subModules).toBeDefined();
|
|
155
|
-
expect(attio?.subModules).toContain("quote-to-cash");
|
|
156
|
-
expect(attio?.subModules).toContain("gocardless");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("returns undefined subModules when no sub-modules directory", async () => {
|
|
160
|
-
await writeManifest(path.join(tmpDir, "email"), {
|
|
161
|
-
...baseManifest,
|
|
162
|
-
name: "email",
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const result = await listAvailableModules(tmpDir);
|
|
166
|
-
const email = result.find((m) => m.name === "email");
|
|
167
|
-
expect(email?.subModules).toBeUndefined();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("skips directories without module.json", async () => {
|
|
171
|
-
// Create a dir without module.json
|
|
172
|
-
await mkdir(path.join(tmpDir, "not-a-module"), { recursive: true });
|
|
173
|
-
await writeManifest(path.join(tmpDir, "email"), {
|
|
174
|
-
...baseManifest,
|
|
175
|
-
name: "email",
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
const result = await listAvailableModules(tmpDir);
|
|
179
|
-
expect(result.map((m) => m.name)).not.toContain("not-a-module");
|
|
180
|
-
});
|
|
181
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const ImpulseConfigSchema = z.object({
|
|
4
|
-
version: z.string().default("1"),
|
|
5
|
-
projectName: z.string(),
|
|
6
|
-
srcPath: z.string().default("src"),
|
|
7
|
-
dbPath: z.string().default("src/server/db"),
|
|
8
|
-
routesPath: z.string().default("src/server/api"),
|
|
9
|
-
installedModules: z
|
|
10
|
-
.array(
|
|
11
|
-
z.object({
|
|
12
|
-
name: z.string(),
|
|
13
|
-
version: z.string(),
|
|
14
|
-
installedAt: z.string(),
|
|
15
|
-
files: z.array(z.string()),
|
|
16
|
-
})
|
|
17
|
-
)
|
|
18
|
-
.default([]),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export type ImpulseConfig = z.infer<typeof ImpulseConfigSchema>;
|
package/src/schemas/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export { ModuleFileSchema } from "./module-file";
|
|
2
|
-
export type { ModuleFile } from "./module-file";
|
|
3
|
-
export { ModuleDependencySchema } from "./module-dependency";
|
|
4
|
-
export { ModuleTransformSchema } from "./module-transform";
|
|
5
|
-
export type { ModuleTransform } from "./module-transform";
|
|
6
|
-
export { ModuleManifestSchema } from "./module-manifest";
|
|
7
|
-
export type { ModuleManifest } from "./module-manifest";
|
|
8
|
-
export { ImpulseConfigSchema } from "./impulse-config";
|
|
9
|
-
export type { ImpulseConfig } from "./impulse-config";
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { ModuleFileSchema } from "./module-file";
|
|
3
|
-
import { ModuleDependencySchema } from "./module-dependency";
|
|
4
|
-
import { ModuleTransformSchema } from "./module-transform";
|
|
5
|
-
|
|
6
|
-
export const ModuleManifestSchema = z.object({
|
|
7
|
-
name: z.string(),
|
|
8
|
-
version: z.string(),
|
|
9
|
-
description: z.string(),
|
|
10
|
-
/** If set, this module is a sub-module of the named parent (e.g. "attio"). */
|
|
11
|
-
parentModule: z.string().optional(),
|
|
12
|
-
/** Names of sub-modules available under this parent module (e.g. ["quote-to-cash", "gocardless"]). */
|
|
13
|
-
subModules: z.array(z.string()).default([]),
|
|
14
|
-
dependencies: z.array(ModuleDependencySchema).default([]),
|
|
15
|
-
moduleDependencies: z.array(z.string()).default([]),
|
|
16
|
-
files: z.array(ModuleFileSchema).default([]),
|
|
17
|
-
transforms: z.array(ModuleTransformSchema).default([]),
|
|
18
|
-
/** Documentation metadata listing env vars this module requires (displayed in install summary). */
|
|
19
|
-
envVars: z.array(z.string()).default([]),
|
|
20
|
-
postInstall: z.array(z.string()).optional(),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export type ModuleManifest = z.infer<typeof ModuleManifestSchema>;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const ModuleTransformSchema = z.object({
|
|
4
|
-
type: z.enum([
|
|
5
|
-
"append-export",
|
|
6
|
-
"register-route",
|
|
7
|
-
"add-nav-item",
|
|
8
|
-
"merge-schema",
|
|
9
|
-
"add-env",
|
|
10
|
-
]),
|
|
11
|
-
target: z.string(),
|
|
12
|
-
value: z.string(),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export type ModuleTransform = z.infer<typeof ModuleTransformSchema>;
|
|
@@ -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 };
|