Skip to content

Commit bc34a1f

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 9168186 commit bc34a1f

13 files changed

Lines changed: 788 additions & 478 deletions

File tree

.github/workflows/checks.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ jobs:
1515
with:
1616
node-version-file: '.nvmrc'
1717

18+
- name: Install Bun
19+
uses: oven-sh/setup-bun@v2
20+
1821
- name: Install pnpm
1922
uses: pnpm/action-setup@v4
2023

.github/workflows/coverage.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ jobs:
2323
with:
2424
node-version-file: '.nvmrc'
2525

26+
- name: Install Bun
27+
uses: oven-sh/setup-bun@v2
28+
2629
- uses: browser-actions/setup-chrome@v1
2730
- run: chrome --version
2831

.gitignore

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

4949
packages/registry/chain-registry
5050
packages/registry/community-assetlist
51+
packages/create-sei/test-output-*

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: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,24 @@
99
"module": "dist/main.js",
1010
"type": "module",
1111
"bin": "./dist/main.js",
12+
"exports": {
13+
".": {
14+
"import": "./dist/main.js"
15+
}
16+
},
1217
"scripts": {
13-
"build": "rm -rf dist && tsc && chmod +x dist/main.js && rsync -av --exclude-from=.rsyncignore ./templates/ ./dist/templates/ && rsync -av --exclude-from=.rsyncignore ./extensions/ ./dist/extensions/",
14-
"dev": "node --loader ts-node/esm src/main.ts",
15-
"test": "jest"
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/",
19+
"dev": "tsc --watch",
20+
"clean": "rm -rf dist",
21+
"test": "bun test"
1622
},
1723
"dependencies": {
1824
"boxen": "^7.1.1",
1925
"commander": "^12.1.0",
2026
"inquirer": "^9.2.15"
2127
},
22-
"devDependencies": {
23-
"@jest/globals": "^29.7.0",
24-
"@types/jest": "^29.5.12",
25-
"@types/node": "^20.14.10",
26-
"jest": "^29.7.0",
27-
"ts-jest": "^29.1.2",
28-
"ts-node": "^10.9.2",
29-
"typescript": "^5.5.3"
28+
29+
"publishConfig": {
30+
"access": "public"
3031
}
3132
}
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)