Skip to content

Commit 5baac73

Browse files
feat: added e2e (thanks @himself65)
1 parent fb2f124 commit 5baac73

10 files changed

+231
-13
lines changed

create-app.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export async function createApp({
2929
packageManager,
3030
eslint,
3131
frontend,
32-
openAIKey,
32+
openAiKey: openAIKey,
3333
model,
3434
communityProjectPath,
3535
}: InstallAppArgs): Promise<void> {

e2e/basic.spec.ts

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/* eslint-disable turbo/no-undeclared-env-vars */
2+
import { expect, test } from "@playwright/test";
3+
import { exec } from "child_process";
4+
import { execSync } from "node:child_process";
5+
import crypto from "node:crypto";
6+
import { mkdir } from "node:fs/promises";
7+
import { fileURLToPath } from "node:url";
8+
import waitPort from "wait-port";
9+
import type {
10+
TemplateEngine,
11+
TemplateFramework,
12+
TemplateType,
13+
TemplateUI,
14+
} from "../templates";
15+
16+
let cwd: string;
17+
test.beforeEach(async () => {
18+
cwd = fileURLToPath(
19+
new URL(`.cache/${crypto.randomUUID()}`, import.meta.url),
20+
);
21+
await mkdir(cwd, { recursive: true });
22+
});
23+
24+
const templateTypes: TemplateType[] = ["streaming", "simple"];
25+
const templateFrameworks: TemplateFramework[] = ["nextjs", "express"];
26+
const templateEngines: TemplateEngine[] = ["simple", "context"];
27+
const templateUIs: TemplateUI[] = ["shadcn", "html"];
28+
29+
for (const templateType of templateTypes) {
30+
for (const templateFramework of templateFrameworks) {
31+
for (const templateEngine of templateEngines) {
32+
for (const templateUI of templateUIs) {
33+
const shouldGenerateFrontendEnum =
34+
templateFramework === "express" || templateFramework === "fastapi"
35+
? ["--frontend", "--no-frontend"]
36+
: [""];
37+
for (const shouldGenerateFrontend of shouldGenerateFrontendEnum) {
38+
if (templateEngine === "context") {
39+
// we don't test context templates because it needs OPEN_AI_KEY
40+
continue;
41+
}
42+
test(`try create-llama ${templateType} ${templateFramework} ${templateEngine} ${templateUI} ${shouldGenerateFrontend}`, async ({
43+
page,
44+
}) => {
45+
const createLlama = fileURLToPath(
46+
new URL("../dist/index.js", import.meta.url),
47+
);
48+
49+
const name = [
50+
templateType,
51+
templateFramework,
52+
templateEngine,
53+
templateUI,
54+
shouldGenerateFrontend,
55+
].join("-");
56+
const command = [
57+
"node",
58+
createLlama,
59+
name,
60+
"--template",
61+
templateType,
62+
"--framework",
63+
templateFramework,
64+
"--engine",
65+
templateEngine,
66+
"--ui",
67+
templateUI,
68+
"--open-ai-key",
69+
process.env.OPEN_AI_KEY || "",
70+
shouldGenerateFrontend,
71+
"--eslint",
72+
].join(" ");
73+
console.log(`running command '${command}' in ${cwd}`);
74+
execSync(command, {
75+
stdio: "inherit",
76+
cwd,
77+
});
78+
79+
const port = Math.floor(Math.random() * 10000) + 10000;
80+
81+
if (
82+
shouldGenerateFrontend === "--frontend" &&
83+
templateFramework === "express"
84+
) {
85+
execSync("npm install", {
86+
stdio: "inherit",
87+
cwd: `${cwd}/${name}/frontend`,
88+
});
89+
execSync("npm install", {
90+
stdio: "inherit",
91+
cwd: `${cwd}/${name}/backend`,
92+
});
93+
} else {
94+
execSync("npm install", {
95+
stdio: "inherit",
96+
cwd: `${cwd}/${name}`,
97+
});
98+
}
99+
100+
if (shouldGenerateFrontend === "--no-frontend") {
101+
return;
102+
}
103+
104+
const cp = exec("npm run dev", {
105+
cwd:
106+
shouldGenerateFrontend === "--frontend"
107+
? `${cwd}/${name}/frontend`
108+
: `${cwd}/${name}`,
109+
env: {
110+
...process.env,
111+
PORT: `${port}`,
112+
},
113+
});
114+
115+
await waitPort({
116+
host: "localhost",
117+
port,
118+
timeout: 1000 * 60,
119+
});
120+
121+
await page.goto(`http://localhost:${port}`);
122+
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
123+
cp.kill();
124+
});
125+
}
126+
}
127+
}
128+
}
129+
}

e2e/tsconfig.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"compilerOptions": {
4+
"module": "ESNext",
5+
"target": "ESNext",
6+
"verbatimModuleSyntax": true
7+
},
8+
"include": [
9+
"./**/*.ts"
10+
]
11+
}

index.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,42 @@ const program = new Commander.Command(packageJson.name)
6161
`
6262
6363
Explicitly tell the CLI to reset any stored preferences
64+
`,
65+
)
66+
.option(
67+
"--template <template>",
68+
`
69+
Select a template to bootstrap the application with.
70+
`,
71+
)
72+
.option(
73+
"--engine <engine>",
74+
`
75+
Select a chat engine to bootstrap the application with.
76+
`,
77+
)
78+
.option(
79+
"--framework <framework>",
80+
`
81+
Select a framework to bootstrap the application with.
82+
`,
83+
)
84+
.option(
85+
"--open-ai-key <key>",
86+
`
87+
Provide an OpenAI API key.
88+
`,
89+
)
90+
.option(
91+
"--ui <ui>",
92+
`
93+
Select a UI to bootstrap the application with.
94+
`,
95+
)
96+
.option(
97+
"--frontend",
98+
`
99+
Whether to generate a frontend for your backend.
64100
`,
65101
)
66102
.allowUnknownOption()
@@ -157,7 +193,7 @@ async function run(): Promise<void> {
157193
packageManager,
158194
eslint: program.eslint,
159195
frontend: program.frontend,
160-
openAIKey: program.openAIKey,
196+
openAiKey: program.openAiKey,
161197
model: program.model,
162198
communityProjectPath: program.communityProjectPath,
163199
});

package.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "create-llama",
33
"version": "0.0.11",
4+
"type": "module",
45
"keywords": [
56
"rag",
67
"llamaindex",
@@ -23,9 +24,11 @@
2324
"dev": "ncc build ./index.ts -w -o dist/",
2425
"build": "ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
2526
"lint": "eslint . --ignore-pattern dist",
27+
"e2e": "playwright test",
2628
"prepublishOnly": "cd ../../ && turbo run build"
2729
},
2830
"devDependencies": {
31+
"@playwright/test": "^1.40.0",
2932
"@types/async-retry": "1.4.2",
3033
"@types/ci-info": "2.0.0",
3134
"@types/cross-spawn": "6.0.0",
@@ -47,9 +50,10 @@
4750
"tar": "6.1.15",
4851
"terminal-link": "^3.0.0",
4952
"update-check": "1.5.4",
50-
"validate-npm-package-name": "3.0.0"
53+
"validate-npm-package-name": "3.0.0",
54+
"wait-port": "^1.1.0"
5155
},
5256
"engines": {
5357
"node": ">=16.14.0"
5458
}
55-
}
59+
}

playwright.config.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/* eslint-disable turbo/no-undeclared-env-vars */
2+
import { defineConfig, devices } from "@playwright/test";
3+
4+
export default defineConfig({
5+
testDir: "./e2e",
6+
fullyParallel: true,
7+
forbidOnly: !!process.env.CI,
8+
retries: process.env.CI ? 2 : 0,
9+
workers: process.env.CI ? 1 : undefined,
10+
timeout: 1000 * 60 * 5,
11+
reporter: "html",
12+
use: {
13+
trace: "on-first-retry",
14+
},
15+
projects: [
16+
{
17+
name: "chromium",
18+
use: { ...devices["Desktop Chrome"] },
19+
},
20+
],
21+
});

questions.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const defaults: QuestionArgs = {
1414
ui: "html",
1515
eslint: true,
1616
frontend: false,
17-
openAIKey: "",
17+
openAiKey: "",
1818
model: "gpt-3.5-turbo",
1919
communityProjectPath: "",
2020
};
@@ -131,8 +131,11 @@ export const askQuestions = async (
131131
}
132132

133133
if (program.framework === "express" || program.framework === "fastapi") {
134+
if (process.argv.includes("--no-frontend")) {
135+
program.frontend = false;
136+
}
134137
// if a backend-only framework is selected, ask whether we should create a frontend
135-
if (!program.frontend) {
138+
if (program.frontend === undefined) {
136139
if (ciInfo.isCI) {
137140
program.frontend = getPrefOrDefault("frontend");
138141
} else {
@@ -157,6 +160,9 @@ export const askQuestions = async (
157160
preferences.frontend = Boolean(frontend);
158161
}
159162
}
163+
} else {
164+
// single project if framework is nextjs
165+
program.frontend = false;
160166
}
161167

162168
if (program.framework === "nextjs" || program.frontend) {
@@ -239,7 +245,7 @@ export const askQuestions = async (
239245
}
240246
}
241247

242-
if (!program.openAIKey) {
248+
if (!program.openAiKey) {
243249
const { key } = await prompts(
244250
{
245251
type: "text",
@@ -248,8 +254,8 @@ export const askQuestions = async (
248254
},
249255
handlers,
250256
);
251-
program.openAIKey = key;
252-
preferences.openAIKey = key;
257+
program.openAiKey = key;
258+
preferences.openAiKey = key;
253259
}
254260

255261
if (
@@ -274,4 +280,10 @@ export const askQuestions = async (
274280
preferences.eslint = Boolean(eslint);
275281
}
276282
}
283+
284+
// TODO: consider using zod to validate the input (doesn't work like this as not every option is required)
285+
// templateUISchema.parse(program.ui);
286+
// templateEngineSchema.parse(program.engine);
287+
// templateFrameworkSchema.parse(program.framework);
288+
// templateTypeSchema.parse(program.template);``
277289
};

templates/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -341,15 +341,15 @@ export const installTemplate = async (
341341
// This is a backend, so we need to copy the test data and create the env file.
342342

343343
// Copy the environment file to the target directory.
344-
await createEnvLocalFile(props.root, props.openAIKey);
344+
await createEnvLocalFile(props.root, props.openAiKey);
345345

346346
// Copy test pdf file
347347
await copyTestData(
348348
props.root,
349349
props.framework,
350350
props.packageManager,
351351
props.engine,
352-
props.openAIKey,
352+
props.openAiKey,
353353
);
354354
}
355355
};

templates/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface InstallTemplateArgs {
1616
ui: TemplateUI;
1717
eslint: boolean;
1818
customApiPath?: string;
19-
openAIKey?: string;
19+
openAiKey?: string;
2020
forBackend?: string;
2121
model: string;
2222
communityProjectPath?: string;

tsconfig.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,10 @@
77
"esModuleInterop": true,
88
"skipLibCheck": false
99
},
10-
"exclude": ["templates", "dist"]
10+
"exclude": ["templates", "dist"],
11+
"references": [
12+
{
13+
"path": "./e2e/tsconfig.json"
14+
}
15+
]
1116
}

0 commit comments

Comments
 (0)