Skip to content

Commit d802c23

Browse files
committed
refactor(create-sei): stabilize CLI architecture and test coverage
- extract wizard logic into reusable lib module\n- expand unit and e2e coverage for create-sei flows\n- make e2e setup self-contained and auto-build CLI when needed\n- fix test tsconfig inheritance and create-sei build determinism
1 parent 4598afa commit d802c23

11 files changed

Lines changed: 756 additions & 196 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,4 @@ Thumbs.db
5454

5555
packages/registry/chain-registry
5656
packages/registry/community-assetlist
57+
packages/create-sei/test-output-*

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@
3131
"@types/node": "^22.13.13",
3232
"mint": "^4.1.57",
3333
"typescript": "^5.8.2"
34-
}
34+
},
35+
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
3536
}

packages/create-sei/.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
node_modules
22
dist
3-
4-
test
5-
```
3+
test-output-*

packages/create-sei/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
}
1616
},
1717
"scripts": {
18-
"build": "tsc -b && chmod +x dist/main.js && rsync -av --exclude-from=.rsyncignore ./templates/ ./dist/templates/ && rsync -av --exclude-from=.rsyncignore ./extensions/ ./dist/extensions/",
18+
"build": "tsc -b --force && chmod +x dist/main.js && rsync -av --exclude-from=.rsyncignore ./templates/ ./dist/templates/ && rsync -av --exclude-from=.rsyncignore ./extensions/ ./dist/extensions/",
1919
"dev": "tsc --watch",
2020
"clean": "rm -rf dist",
2121
"test": "bun test"
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2+
import * as fs from "node:fs";
3+
import path from "node:path";
4+
5+
const packageDir = path.resolve(import.meta.dir, "../..");
6+
const cliPath = path.join(packageDir, "dist", "main.js");
7+
const e2eDir = path.join(packageDir, "test-output-e2e");
8+
const e2eTmpDir = path.join(e2eDir, ".tmp");
9+
const baseProjectName = "e2e-basic";
10+
const precompilesProjectName = "e2e-precompiles";
11+
const e2eTmpDirEnv = `${e2eTmpDir}${path.sep}`;
12+
const e2eSpawnEnv = {
13+
...process.env,
14+
TMPDIR: e2eTmpDirEnv,
15+
BUN_TMPDIR: e2eTmpDirEnv,
16+
};
17+
18+
async function runCli(
19+
args: string[],
20+
cwd: string,
21+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
22+
const proc = Bun.spawn(["node", cliPath, ...args], {
23+
cwd,
24+
stdout: "pipe",
25+
stderr: "pipe",
26+
env: { ...process.env, NO_COLOR: "1" },
27+
});
28+
29+
const [stdout, stderr] = await Promise.all([
30+
new Response(proc.stdout).text(),
31+
new Response(proc.stderr).text(),
32+
]);
33+
const exitCode = await proc.exited;
34+
35+
return { stdout, stderr, exitCode };
36+
}
37+
38+
async function pathExists(targetPath: string): Promise<boolean> {
39+
return fs.promises
40+
.access(targetPath)
41+
.then(() => true)
42+
.catch(() => false);
43+
}
44+
45+
async function ensureCliBuilt(): Promise<void> {
46+
if (await pathExists(cliPath)) {
47+
return;
48+
}
49+
50+
const proc = Bun.spawn(["bun", "run", "build"], {
51+
cwd: packageDir,
52+
stdout: "pipe",
53+
stderr: "pipe",
54+
env: { ...process.env, NO_COLOR: "1" },
55+
});
56+
const [stdout, stderr] = await Promise.all([
57+
new Response(proc.stdout).text(),
58+
new Response(proc.stderr).text(),
59+
]);
60+
await proc.exited;
61+
if (proc.exitCode !== 0 || !(await pathExists(cliPath))) {
62+
throw new Error(
63+
`Failed to build create-sei CLI before e2e tests.\nstdout:\n${stdout}\nstderr:\n${stderr}`,
64+
);
65+
}
66+
}
67+
68+
async function ensureProject(
69+
projectName: string,
70+
args: string[] = [],
71+
): Promise<void> {
72+
const projectDir = path.join(e2eDir, projectName);
73+
if (await pathExists(projectDir)) {
74+
return;
75+
}
76+
77+
const { exitCode, stderr } = await runCli(
78+
["app", "--name", projectName, ...args],
79+
e2eDir,
80+
);
81+
if (exitCode !== 0) {
82+
throw new Error(
83+
`Failed to create fixture project '${projectName}'.\nstderr:\n${stderr}`,
84+
);
85+
}
86+
}
87+
88+
async function installDeps(projectDir: string): Promise<void> {
89+
const proc = Bun.spawn(["bun", "install"], {
90+
cwd: projectDir,
91+
stdout: "pipe",
92+
stderr: "pipe",
93+
env: e2eSpawnEnv,
94+
});
95+
const [stdout, stderr] = await Promise.all([
96+
new Response(proc.stdout).text(),
97+
new Response(proc.stderr).text(),
98+
]);
99+
await proc.exited;
100+
if (proc.exitCode !== 0) {
101+
throw new Error(
102+
`bun install failed in '${projectDir}'.\nstdout:\n${stdout}\nstderr:\n${stderr}`,
103+
);
104+
}
105+
}
106+
107+
describe("create-sei CLI e2e", () => {
108+
beforeAll(async () => {
109+
await fs.promises.rm(e2eDir, { recursive: true, force: true });
110+
await fs.promises.mkdir(e2eDir, { recursive: true });
111+
await fs.promises.mkdir(e2eTmpDir, { recursive: true });
112+
await ensureCliBuilt();
113+
}, 120_000);
114+
115+
afterAll(async () => {
116+
await fs.promises.rm(e2eDir, { recursive: true, force: true });
117+
}, 120_000);
118+
119+
test("app --name creates a project directory", async () => {
120+
const projectName = "e2e-create-check";
121+
const { exitCode } = await runCli(["app", "--name", projectName], e2eDir);
122+
expect(exitCode).toBe(0);
123+
124+
const projectDir = path.join(e2eDir, projectName);
125+
const exists = await pathExists(projectDir);
126+
expect(exists).toBe(true);
127+
});
128+
129+
test("generated project has valid package.json", async () => {
130+
await ensureProject(baseProjectName);
131+
const pkgPath = path.join(e2eDir, baseProjectName, "package.json");
132+
const raw = await fs.promises.readFile(pkgPath, "utf-8");
133+
const pkg = JSON.parse(raw);
134+
135+
expect(pkg.scripts).toBeDefined();
136+
expect(pkg.scripts.dev).toBe("next dev");
137+
expect(pkg.scripts.build).toBe("next build");
138+
expect(pkg.dependencies).toBeDefined();
139+
expect(pkg.dependencies.next).toBeDefined();
140+
expect(pkg.dependencies.react).toBeDefined();
141+
expect(pkg.dependencies.viem).toBeDefined();
142+
});
143+
144+
test("generated project has expected file structure", async () => {
145+
await ensureProject(baseProjectName);
146+
const projectDir = path.join(e2eDir, baseProjectName);
147+
const expectedFiles = [
148+
"package.json",
149+
"tsconfig.json",
150+
"next.config.mjs",
151+
"src",
152+
];
153+
154+
for (const file of expectedFiles) {
155+
const exists = await pathExists(path.join(projectDir, file));
156+
expect(exists).toBe(true);
157+
}
158+
});
159+
160+
test("generated project can install dependencies", async () => {
161+
await ensureProject(baseProjectName);
162+
const projectDir = path.join(e2eDir, baseProjectName);
163+
await installDeps(projectDir);
164+
165+
// node_modules should exist
166+
const nmExists = await pathExists(path.join(projectDir, "node_modules"));
167+
expect(nmExists).toBe(true);
168+
}, 60_000);
169+
170+
test("generated project can build successfully", async () => {
171+
await ensureProject(baseProjectName);
172+
const projectDir = path.join(e2eDir, baseProjectName);
173+
await installDeps(projectDir);
174+
175+
const proc = Bun.spawn(["bun", "run", "build"], {
176+
cwd: projectDir,
177+
stdout: "pipe",
178+
stderr: "pipe",
179+
env: e2eSpawnEnv,
180+
});
181+
182+
const [stdout, stderr] = await Promise.all([
183+
new Response(proc.stdout).text(),
184+
new Response(proc.stderr).text(),
185+
]);
186+
await proc.exited;
187+
const exitCode = proc.exitCode;
188+
189+
if (exitCode !== 0) {
190+
console.error("Build stdout:", stdout);
191+
console.error("Build stderr:", stderr);
192+
}
193+
expect(exitCode).toBe(0);
194+
}, 120_000);
195+
196+
test("app --name --extension precompiles creates project with extension", async () => {
197+
const projectName = "e2e-precompiles-create-check";
198+
const { exitCode, stdout } = await runCli(
199+
["app", "--name", projectName, "--extension", "precompiles"],
200+
e2eDir,
201+
);
202+
expect(exitCode).toBe(0);
203+
expect(stdout).toContain("Applied extension: precompiles");
204+
205+
// Extension should have overwritten package.json
206+
const pkgPath = path.join(e2eDir, projectName, "package.json");
207+
const pkg = JSON.parse(await fs.promises.readFile(pkgPath, "utf-8"));
208+
expect(pkg.name).toBe("template-next-create-sei-app-precompiles");
209+
});
210+
211+
test("extension project can install dependencies", async () => {
212+
await ensureProject(precompilesProjectName, ["--extension", "precompiles"]);
213+
const projectDir = path.join(e2eDir, precompilesProjectName);
214+
await installDeps(projectDir);
215+
}, 60_000);
216+
217+
test("extension project can build successfully", async () => {
218+
await ensureProject(precompilesProjectName, ["--extension", "precompiles"]);
219+
const projectDir = path.join(e2eDir, precompilesProjectName);
220+
await installDeps(projectDir);
221+
222+
const proc = Bun.spawn(["bun", "run", "build"], {
223+
cwd: projectDir,
224+
stdout: "pipe",
225+
stderr: "pipe",
226+
env: e2eSpawnEnv,
227+
});
228+
229+
const [stdout, stderr] = await Promise.all([
230+
new Response(proc.stdout).text(),
231+
new Response(proc.stderr).text(),
232+
]);
233+
await proc.exited;
234+
235+
if (proc.exitCode !== 0) {
236+
console.error("Build stdout:", stdout);
237+
console.error("Build stderr:", stderr);
238+
}
239+
expect(proc.exitCode).toBe(0);
240+
}, 120_000);
241+
242+
test("list-extensions command outputs available extensions", async () => {
243+
const { exitCode, stdout } = await runCli(["list-extensions"], e2eDir);
244+
expect(exitCode).toBe(0);
245+
expect(stdout).toContain("Available extensions:");
246+
expect(stdout).toContain("precompiles");
247+
});
248+
249+
test("app with invalid name does not create directory", async () => {
250+
const { exitCode, stdout } = await runCli(
251+
["app", "--name", "INVALID NAME!"],
252+
e2eDir,
253+
);
254+
expect(exitCode).toBe(0);
255+
expect(stdout).toContain("Invalid package name");
256+
257+
const exists = await pathExists(path.join(e2eDir, "INVALID NAME!"));
258+
expect(exists).toBe(false);
259+
});
260+
261+
test("app with nonexistent extension falls back to base template", async () => {
262+
const { exitCode, stdout } = await runCli(
263+
["app", "--name", "e2e-fallback", "--extension", "does-not-exist"],
264+
e2eDir,
265+
);
266+
expect(exitCode).toBe(0);
267+
expect(stdout).toContain("Warning");
268+
expect(stdout).toContain("does-not-exist");
269+
270+
// Should still have created the project from base template
271+
const pkgPath = path.join(e2eDir, "e2e-fallback", "package.json");
272+
const pkg = JSON.parse(await fs.promises.readFile(pkgPath, "utf-8"));
273+
expect(pkg.scripts.dev).toBe("next dev");
274+
});
275+
});

0 commit comments

Comments
 (0)