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