From 07fd79ec35a0edc76c1cbf1d0e309463d469f450 Mon Sep 17 00:00:00 2001 From: Marcus Schiesser <mail@marcusschiesser.de> Date: Fri, 3 Nov 2023 16:28:12 +0700 Subject: [PATCH] feat: generate fullstack app with fastapi or express --- create-app.ts | 64 +++++++++++++++++++++++--------- index.ts | 69 ++++++++++++++++------------------- package.json | 3 +- templates/README-fullstack.md | 18 +++++++++ templates/index.ts | 22 +++++++---- templates/types.ts | 13 ++----- 6 files changed, 117 insertions(+), 72 deletions(-) create mode 100644 templates/README-fullstack.md diff --git a/create-app.ts b/create-app.ts index 34d51b7a..1091d526 100644 --- a/create-app.ts +++ b/create-app.ts @@ -7,8 +7,10 @@ import { getOnline } from "./helpers/is-online"; import { isWriteable } from "./helpers/is-writeable"; import { makeDir } from "./helpers/make-dir"; +import fs from "fs"; +import terminalLink from "terminal-link"; import type { InstallTemplateArgs } from "./templates"; -import { installPythonTemplate, installTemplate } from "./templates"; +import { installTemplate } from "./templates"; export async function createApp({ template, @@ -18,9 +20,13 @@ export async function createApp({ appPath, packageManager, eslint, - customApiPath, -}: Omit<InstallTemplateArgs, "appName" | "root" | "isOnline"> & { + frontend, +}: Omit< + InstallTemplateArgs, + "appName" | "root" | "isOnline" | "customApiPath" +> & { appPath: string; + frontend: boolean; }): Promise<void> { const root = path.resolve(appPath); @@ -47,30 +53,54 @@ export async function createApp({ console.log(`Creating a new LlamaIndex app in ${green(root)}.`); console.log(); - process.chdir(root); + const args = { + appName, + root, + template, + framework, + engine, + ui, + packageManager, + isOnline, + eslint, + }; - if (framework === "fastapi") { - await installPythonTemplate({ appName, root, template, framework }); - } else { + if (frontend) { + // install backend + const backendRoot = path.join(root, "backend"); + await makeDir(backendRoot); + await installTemplate({ ...args, root: backendRoot }); + // install frontend + const frontendRoot = path.join(root, "frontend"); + await makeDir(frontendRoot); await installTemplate({ - appName, - root, - template, - framework, - engine, - ui, - packageManager, - isOnline, - eslint, - customApiPath, + ...args, + root: frontendRoot, + framework: "nextjs", + customApiPath: "http://localhost:8000/api/chat", }); + // copy readme for fullstack + await fs.promises.copyFile( + path.join(__dirname, "templates", "README-fullstack.md"), + path.join(root, "README.md"), + ); + } else { + await installTemplate(args); } + process.chdir(root); if (tryGitInit(root)) { console.log("Initialized a git repository."); console.log(); } console.log(`${green("Success!")} Created ${appName} at ${appPath}`); + + console.log( + `Now have a look at the ${terminalLink( + "README.md", + "file://${appName}/README.md", + )} and learn how to get started.`, + ); console.log(); } diff --git a/index.ts b/index.ts index 769bb95f..5756144b 100644 --- a/index.ts +++ b/index.ts @@ -11,7 +11,6 @@ import checkForUpdate from "update-check"; import { createApp } from "./create-app"; import { getPkgManager } from "./helpers/get-pkg-manager"; import { isFolderEmpty } from "./helpers/is-folder-empty"; -import { isUrl } from "./helpers/is-url"; import { validateNpmName } from "./helpers/validate-pkg"; import packageJson from "./package.json"; @@ -167,7 +166,7 @@ async function run(): Promise<void> { engine: "simple", ui: "html", eslint: true, - customApiPath: "http://localhost:8000/api/chat", + frontend: false, }; const getPrefOrDefault = (field: string) => preferences[field] ?? defaults[field]; @@ -224,7 +223,36 @@ async function run(): Promise<void> { } } - if (program.framework === "nextjs") { + if (program.framework === "express" || program.framework === "fastapi") { + // if a backend-only framework is selected, ask whether we should create a frontend + if (!program.frontend) { + if (ciInfo.isCI) { + program.frontend = getPrefOrDefault("frontend"); + } else { + const styledNextJS = blue("NextJS"); + const styledBackend = green( + program.framework === "express" + ? "Express " + : program.framework === "fastapi" + ? "FastAPI (Python) " + : "", + ); + const { frontend } = await prompts({ + onState: onPromptState, + type: "toggle", + name: "frontend", + message: `Would you like to generate a ${styledNextJS} frontend for your ${styledBackend}backend?`, + initial: getPrefOrDefault("frontend"), + active: "Yes", + inactive: "No", + }); + program.frontend = Boolean(frontend); + preferences.frontend = Boolean(frontend); + } + } + } + + if (program.framework === "nextjs" || program.frontend) { if (!program.ui) { if (ciInfo.isCI) { program.ui = getPrefOrDefault("ui"); @@ -253,15 +281,6 @@ async function run(): Promise<void> { if (ciInfo.isCI) { program.engine = getPrefOrDefault("engine"); } else { - const external = - program.framework === "nextjs" - ? [ - { - title: "External chat engine (e.g. FastAPI)", - value: "external", - }, - ] - : []; const { engine } = await prompts( { type: "select", @@ -270,7 +289,6 @@ async function run(): Promise<void> { choices: [ { title: "SimpleChatEngine", value: "simple" }, { title: "ContextChatEngine", value: "context" }, - ...external, ], initial: 0, }, @@ -280,29 +298,6 @@ async function run(): Promise<void> { preferences.engine = engine; } } - if ( - program.framework === "nextjs" && - program.engine === "external" && - !program.customApiPath - ) { - if (ciInfo.isCI) { - program.customApiPath = getPrefOrDefault("customApiPath"); - } else { - const { customApiPath } = await prompts( - { - type: "text", - name: "customApiPath", - message: - "URL path of your external chat engine (used for development)?", - validate: (url) => (isUrl(url) ? true : "Please enter a valid URL"), - initial: getPrefOrDefault("customApiPath"), - }, - handlers, - ); - program.customApiPath = customApiPath; - preferences.customApiPath = customApiPath; - } - } } if ( @@ -336,7 +331,7 @@ async function run(): Promise<void> { appPath: resolvedProjectPath, packageManager, eslint: program.eslint, - customApiPath: program.customApiPath, + frontend: program.frontend, }); conf.set("preferences", preferences); } diff --git a/package.json b/package.json index 144a1750..de886a91 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@types/validate-npm-package-name": "3.0.0", "@vercel/ncc": "0.34.0", "async-retry": "1.3.1", + "async-sema": "3.0.1", "ci-info": "watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540", "commander": "2.20.0", "conf": "10.2.0", @@ -50,7 +51,7 @@ "tar": "6.1.15", "update-check": "1.5.4", "validate-npm-package-name": "3.0.0", - "async-sema": "3.0.1" + "terminal-link": "^3.0.0" }, "engines": { "node": ">=16.14.0" diff --git a/templates/README-fullstack.md b/templates/README-fullstack.md new file mode 100644 index 00000000..5a41b8cf --- /dev/null +++ b/templates/README-fullstack.md @@ -0,0 +1,18 @@ +This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama). + +## Getting Started + +First, startup the backend as described in the [backend README](./backend/README.md). + +Second, run the development server of the frontend as described in the [frontend README](./frontend/README.md). + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +## Learn More + +To learn more about LlamaIndex, take a look at the following resources: + +- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features). +- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features). + +You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome! diff --git a/templates/index.ts b/templates/index.ts index 7fb46e45..3c6f63b1 100644 --- a/templates/index.ts +++ b/templates/index.ts @@ -7,12 +7,12 @@ import path from "path"; import { bold, cyan } from "picocolors"; import { version } from "../package.json"; -import { InstallPythonTemplateArgs, InstallTemplateArgs } from "./types"; +import { InstallTemplateArgs } from "./types"; /** * Install a LlamaIndex internal template to a given `root` directory. */ -export const installTemplate = async ({ +const installTSTemplate = async ({ appName, root, packageManager, @@ -60,7 +60,7 @@ export const installTemplate = async ({ */ let relativeEngineDestPath; const compPath = path.join(__dirname, "components"); - if (framework === "express" || framework === "nextjs") { + if (engine && (framework === "express" || framework === "nextjs")) { console.log("\nUsing chat engine:", engine, "\n"); const enginePath = path.join(compPath, "engines", engine); relativeEngineDestPath = @@ -101,7 +101,7 @@ export const installTemplate = async ({ llamaindex: version, }; - if (engine === "external" && customApiPath) { + if (framework === "nextjs" && customApiPath) { console.log( "\nUsing external API with custom API path:", customApiPath, @@ -166,12 +166,11 @@ export const installTemplate = async ({ await install(packageManager, isOnline); }; -export const installPythonTemplate = async ({ - appName, +const installPythonTemplate = async ({ root, template, framework, -}: InstallPythonTemplateArgs) => { +}: Pick<InstallTemplateArgs, "root" | "framework" | "template">) => { console.log("\nInitializing Python project with template:", template, "\n"); const templatePath = path.join(__dirname, "types", template, framework); await copy("**", root, { @@ -198,4 +197,13 @@ export const installPythonTemplate = async ({ ); }; +export const installTemplate = async (props: InstallTemplateArgs) => { + process.chdir(props.root); + if (props.framework === "fastapi") { + await installPythonTemplate(props); + } else { + await installTSTemplate(props); + } +}; + export * from "./types"; diff --git a/templates/types.ts b/templates/types.ts index d4c032f7..6f314b7b 100644 --- a/templates/types.ts +++ b/templates/types.ts @@ -2,16 +2,9 @@ import { PackageManager } from "../helpers/get-pkg-manager"; export type TemplateType = "simple" | "streaming"; export type TemplateFramework = "nextjs" | "express" | "fastapi"; -export type TemplateEngine = "simple" | "context" | "external"; +export type TemplateEngine = "simple" | "context"; export type TemplateUI = "html" | "shadcn"; -export interface InstallPythonTemplateArgs { - appName: string; - root: string; - template: TemplateType; - framework: TemplateFramework; -} - export interface InstallTemplateArgs { appName: string; root: string; @@ -19,8 +12,8 @@ export interface InstallTemplateArgs { isOnline: boolean; template: TemplateType; framework: TemplateFramework; - engine: TemplateEngine; + engine?: TemplateEngine; ui: TemplateUI; eslint: boolean; - customApiPath: string; + customApiPath?: string; } -- GitLab