From ccf8a0b9171f5a68362e48a86d025fdbea30c174 Mon Sep 17 00:00:00 2001 From: Marcus Schiesser <mail@marcusschiesser.de> Date: Thu, 26 Oct 2023 11:13:20 +0700 Subject: [PATCH] removed URL download --- README.md | 30 ++----- create-app.ts | 211 ++++---------------------------------------- helpers/examples.ts | 133 ---------------------------- index.ts | 206 +++++++++++++++--------------------------- 4 files changed, 93 insertions(+), 487 deletions(-) delete mode 100644 helpers/examples.ts diff --git a/README.md b/README.md index 74134373..b52ddc03 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -# Create Next App +# Create LlamaIndex App -The easiest way to get started with LlamaIndex is by using `create-llama`. This CLI tool enables you to quickly start building a new LlamaIndex application, with everything set up for you. You can create a new app using the default LlamaIndex template, or by using one of the [official LlamaIndex examples](https://github.com/vercel/next.js/tree/canary/examples). To get started, use the following command: +The easiest way to get started with LlamaIndex is by using `create-llama`. This CLI tool enables you to quickly start building a new LlamaIndex application, with everything set up for you. +To get started, use the following command: ### Interactive @@ -9,9 +10,9 @@ You can create a new project interactively by running: ```bash npx create-llama@latest # or -yarn create next-app +yarn create llama-app # or -pnpm create next-app +pnpm create llama-app # or bunx create-llama ``` @@ -53,26 +54,5 @@ Options: Explicitly tell the CLI to bootstrap the app using Bun - -e, --example [name]|[github-url] - - An example to bootstrap the app with. You can use an example name - from the official LlamaIndex repo or a GitHub URL. The URL can use - any branch and/or subdirectory - - --example-path <path-to-example> - - In a rare case, your GitHub URL might contain a branch name with - a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). - In this case, you must specify the path to the example separately: - --example-path foo/bar ``` -### Why use Create Next App? - -`create-llama` allows you to create a new LlamaIndex app within seconds. It is officially maintained by the creators of LlamaIndex, and includes a number of benefits: - -- **Interactive Experience**: Running `npx create-llama@latest` (with no arguments) launches an interactive experience that guides you through setting up a project. -- **Zero Dependencies**: Initializing a project is as quick as one second. Create Next App has zero dependencies. -- **Offline Support**: Create Next App will automatically detect if you're offline and bootstrap your project using your local package cache. -- **Support for Examples**: Create Next App can bootstrap your application using an example from the LlamaIndex examples collection (e.g. `npx create-llama --example api-routes`). -- **Tested**: The package is part of the LlamaIndex monorepo and tested using the same integration test suite as LlamaIndex itself, ensuring it works as expected with every release. diff --git a/create-app.ts b/create-app.ts index fb76bc8a..18e18117 100644 --- a/create-app.ts +++ b/create-app.ts @@ -1,34 +1,19 @@ /* eslint-disable import/no-extraneous-dependencies */ -import retry from "async-retry"; -import fs from "fs"; import path from "path"; -import { cyan, green, red } from "picocolors"; -import type { RepoInfo } from "./helpers/examples"; -import { - downloadAndExtractExample, - downloadAndExtractRepo, - existsInRepo, - getRepoInfo, - hasRepo, -} from "./helpers/examples"; +import { green } from "picocolors"; import type { PackageManager } from "./helpers/get-pkg-manager"; import { tryGitInit } from "./helpers/git"; -import { install } from "./helpers/install"; import { isFolderEmpty } from "./helpers/is-folder-empty"; import { getOnline } from "./helpers/is-online"; import { isWriteable } from "./helpers/is-writeable"; import { makeDir } from "./helpers/make-dir"; import type { TemplateMode, TemplateType } from "./templates"; -import { getTemplateFile, installTemplate } from "./templates"; - -export class DownloadError extends Error {} +import { installTemplate } from "./templates"; export async function createApp({ appPath, packageManager, - example, - examplePath, tailwind, eslint, srcDir, @@ -36,78 +21,14 @@ export async function createApp({ }: { appPath: string; packageManager: PackageManager; - example?: string; - examplePath?: string; tailwind: boolean; eslint: boolean; srcDir: boolean; importAlias: string; }): Promise<void> { - let repoInfo: RepoInfo | undefined; const mode: TemplateMode = "nextjs"; const template: TemplateType = "simple"; - if (example) { - let repoUrl: URL | undefined; - - try { - repoUrl = new URL(example); - } catch (error: any) { - if (error.code !== "ERR_INVALID_URL") { - console.error(error); - process.exit(1); - } - } - - if (repoUrl) { - if (repoUrl.origin !== "https://github.com") { - console.error( - `Invalid URL: ${red( - `"${example}"`, - )}. Only GitHub repositories are supported. Please use a GitHub URL and try again.`, - ); - process.exit(1); - } - - repoInfo = await getRepoInfo(repoUrl, examplePath); - - if (!repoInfo) { - console.error( - `Found invalid GitHub URL: ${red( - `"${example}"`, - )}. Please fix the URL and try again.`, - ); - process.exit(1); - } - - const found = await hasRepo(repoInfo); - - if (!found) { - console.error( - `Could not locate the repository for ${red( - `"${example}"`, - )}. Please check that the repository exists and try again.`, - ); - process.exit(1); - } - } else if (example !== "__internal-testing-retry") { - const found = await existsInRepo(example); - - if (!found) { - console.error( - `Could not locate an example named ${red( - `"${example}"`, - )}. It could be due to the following:\n`, - `1. Your spelling of example ${red( - `"${example}"`, - )} might be incorrect.\n`, - `2. You might not be connected to the internet or you are behind a proxy.`, - ); - process.exit(1); - } - } - } - const root = path.resolve(appPath); if (!(await isWriteable(path.dirname(root)))) { @@ -129,130 +50,34 @@ export async function createApp({ const useYarn = packageManager === "yarn"; const isOnline = !useYarn || (await getOnline()); - const originalDirectory = process.cwd(); console.log(`Creating a new LlamaIndex app in ${green(root)}.`); console.log(); process.chdir(root); - const packageJsonPath = path.join(root, "package.json"); - let hasPackageJson = false; - - if (example) { - /** - * If an example repository is provided, clone it. - */ - try { - if (repoInfo) { - const repoInfo2 = repoInfo; - console.log( - `Downloading files from repo ${cyan( - example, - )}. This might take a moment.`, - ); - console.log(); - await retry(() => downloadAndExtractRepo(root, repoInfo2), { - retries: 3, - }); - } else { - console.log( - `Downloading files for example ${cyan( - example, - )}. This might take a moment.`, - ); - console.log(); - await retry(() => downloadAndExtractExample(root, example), { - retries: 3, - }); - } - } catch (reason) { - function isErrorLike(err: unknown): err is { message: string } { - return ( - typeof err === "object" && - err !== null && - typeof (err as { message?: unknown }).message === "string" - ); - } - throw new DownloadError( - isErrorLike(reason) ? reason.message : reason + "", - ); - } - // Copy `.gitignore` if the application did not provide one - const ignorePath = path.join(root, ".gitignore"); - if (!fs.existsSync(ignorePath)) { - fs.copyFileSync( - getTemplateFile({ template, mode, file: "gitignore" }), - ignorePath, - ); - } - - // Copy `next-env.d.ts` to any example that is typescript - const tsconfigPath = path.join(root, "tsconfig.json"); - if (fs.existsSync(tsconfigPath)) { - fs.copyFileSync( - getTemplateFile({ template, mode: "nextjs", file: "next-env.d.ts" }), - path.join(root, "next-env.d.ts"), - ); - } - - hasPackageJson = fs.existsSync(packageJsonPath); - if (hasPackageJson) { - console.log("Installing packages. This might take a couple of minutes."); - console.log(); - - await install(packageManager, isOnline); - console.log(); - } - } else { - /** - * If an example repository is not provided for cloning, proceed - * by installing from a template. - */ - await installTemplate({ - appName, - root, - template, - mode, - packageManager, - isOnline, - tailwind, - eslint, - srcDir, - importAlias, - }); - } + /** + * If an example repository is not provided for cloning, proceed + * by installing from a template. + */ + await installTemplate({ + appName, + root, + template, + mode, + packageManager, + isOnline, + tailwind, + eslint, + srcDir, + importAlias, + }); if (tryGitInit(root)) { console.log("Initialized a git repository."); console.log(); } - let cdpath: string; - if (path.join(originalDirectory, appName) === appPath) { - cdpath = appName; - } else { - cdpath = appPath; - } - console.log(`${green("Success!")} Created ${appName} at ${appPath}`); - - if (hasPackageJson) { - console.log("Inside that directory, you can run several commands:"); - console.log(); - console.log(cyan(` ${packageManager} ${useYarn ? "" : "run "}dev`)); - console.log(" Starts the development server."); - console.log(); - console.log(cyan(` ${packageManager} ${useYarn ? "" : "run "}build`)); - console.log(" Builds the app for production."); - console.log(); - console.log(cyan(` ${packageManager} start`)); - console.log(" Runs the built app in production mode."); - console.log(); - console.log("We suggest that you begin by typing:"); - console.log(); - console.log(cyan(" cd"), cdpath); - console.log(` ${cyan(`${packageManager} ${useYarn ? "" : "run "}dev`)}`); - } console.log(); } diff --git a/helpers/examples.ts b/helpers/examples.ts deleted file mode 100644 index b8c74be6..00000000 --- a/helpers/examples.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -import { createWriteStream, promises as fs } from "fs"; -import got from "got"; -import { tmpdir } from "os"; -import { join } from "path"; -import { Stream } from "stream"; -import tar from "tar"; -import { promisify } from "util"; - -const pipeline = promisify(Stream.pipeline); - -export type RepoInfo = { - username: string; - name: string; - branch: string; - filePath: string; -}; - -export async function isUrlOk(url: string): Promise<boolean> { - const res = await got.head(url).catch((e) => e); - return res.statusCode === 200; -} - -export async function getRepoInfo( - url: URL, - examplePath?: string, -): Promise<RepoInfo | undefined> { - const [, username, name, t, _branch, ...file] = url.pathname.split("/"); - const filePath = examplePath - ? examplePath.replace(/^\//, "") - : file.join("/"); - - if ( - // Support repos whose entire purpose is to be a LlamaIndex example, e.g. - // https://github.com/:username/:my-cool-nextjs-example-repo-name. - t === undefined || - // Support GitHub URL that ends with a trailing slash, e.g. - // https://github.com/:username/:my-cool-nextjs-example-repo-name/ - // In this case "t" will be an empty string while the next part "_branch" will be undefined - (t === "" && _branch === undefined) - ) { - const infoResponse = await got( - `https://api.github.com/repos/${username}/${name}`, - ).catch((e) => e); - if (infoResponse.statusCode !== 200) { - return; - } - const info = JSON.parse(infoResponse.body); - return { username, name, branch: info["default_branch"], filePath }; - } - - // If examplePath is available, the branch name takes the entire path - const branch = examplePath - ? `${_branch}/${file.join("/")}`.replace(new RegExp(`/${filePath}|/$`), "") - : _branch; - - if (username && name && branch && t === "tree") { - return { username, name, branch, filePath }; - } -} - -export function hasRepo({ - username, - name, - branch, - filePath, -}: RepoInfo): Promise<boolean> { - const contentsUrl = `https://api.github.com/repos/${username}/${name}/contents`; - const packagePath = `${filePath ? `/${filePath}` : ""}/package.json`; - - return isUrlOk(contentsUrl + packagePath + `?ref=${branch}`); -} - -export function existsInRepo(nameOrUrl: string): Promise<boolean> { - try { - const url = new URL(nameOrUrl); - return isUrlOk(url.href); - } catch { - return isUrlOk( - `https://api.github.com/repos/vercel/next.js/contents/examples/${encodeURIComponent( - nameOrUrl, - )}`, - ); - } -} - -async function downloadTar(url: string) { - const tempFile = join(tmpdir(), `next.js-cna-example.temp-${Date.now()}`); - await pipeline(got.stream(url), createWriteStream(tempFile)); - return tempFile; -} - -export async function downloadAndExtractRepo( - root: string, - { username, name, branch, filePath }: RepoInfo, -) { - const tempFile = await downloadTar( - `https://codeload.github.com/${username}/${name}/tar.gz/${branch}`, - ); - - await tar.x({ - file: tempFile, - cwd: root, - strip: filePath ? filePath.split("/").length + 1 : 1, - filter: (p) => - p.startsWith( - `${name}-${branch.replace(/\//g, "-")}${ - filePath ? `/${filePath}/` : "/" - }`, - ), - }); - - await fs.unlink(tempFile); -} - -export async function downloadAndExtractExample(root: string, name: string) { - if (name === "__internal-testing-retry") { - throw new Error("This is an internal example for testing the CLI."); - } - - const tempFile = await downloadTar( - "https://codeload.github.com/vercel/next.js/tar.gz/canary", - ); - - await tar.x({ - file: tempFile, - cwd: root, - strip: 2 + name.split("/").length, - filter: (p) => p.includes(`next.js-canary/examples/${name}/`), - }); - - await fs.unlink(tempFile); -} diff --git a/index.ts b/index.ts index b74215f9..01fac2d5 100644 --- a/index.ts +++ b/index.ts @@ -8,7 +8,7 @@ import path from "path"; import { blue, bold, cyan, green, red, yellow } from "picocolors"; import prompts from "prompts"; import checkForUpdate from "update-check"; -import { createApp, DownloadError } from "./create-app"; +import { createApp } from "./create-app"; import { getPkgManager } from "./helpers/get-pkg-manager"; import { isFolderEmpty } from "./helpers/is-folder-empty"; import { validateNpmName } from "./helpers/validate-pkg"; @@ -78,25 +78,6 @@ const program = new Commander.Command(packageJson.name) ` Explicitly tell the CLI to bootstrap the application using Bun -`, - ) - .option( - "-e, --example [name]|[github-url]", - ` - - An example to bootstrap the app with. You can use an example name - from the official LlamaIndex repo or a GitHub URL. The URL can use - any branch and/or subdirectory -`, - ) - .option( - "--example-path <path-to-example>", - ` - - In a rare case, your GitHub URL might contain a branch name with - a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). - In this case, you must specify the path to the example separately: - --example-path foo/bar `, ) .option( @@ -179,13 +160,6 @@ async function run(): Promise<void> { process.exit(1); } - if (program.example === true) { - console.error( - "Please provide an example name or url, otherwise remove the example option.", - ); - process.exit(1); - } - /** * Verify the project dir is empty or doesn't exist */ @@ -197,130 +171,90 @@ async function run(): Promise<void> { process.exit(1); } - const example = typeof program.example === "string" && program.example.trim(); const preferences = (conf.get("preferences") || {}) as Record< string, boolean | string >; - /** - * If the user does not provide the necessary flags, prompt them for whether - * to use TS or JS. - */ - if (!example) { - const defaults: typeof preferences = { - typescript: true, - eslint: true, - tailwind: true, - app: true, - srcDir: false, - importAlias: "@/*", - customizeImportAlias: false, - }; - const getPrefOrDefault = (field: string) => - preferences[field] ?? defaults[field]; - - if ( - !process.argv.includes("--eslint") && - !process.argv.includes("--no-eslint") - ) { - if (ciInfo.isCI) { - program.eslint = getPrefOrDefault("eslint"); - } else { - const styledEslint = blue("ESLint"); - const { eslint } = await prompts({ - onState: onPromptState, - type: "toggle", - name: "eslint", - message: `Would you like to use ${styledEslint}?`, - initial: getPrefOrDefault("eslint"), - active: "Yes", - inactive: "No", - }); - program.eslint = Boolean(eslint); - preferences.eslint = Boolean(eslint); - } + + const defaults: typeof preferences = { + eslint: true, + tailwind: true, + app: true, + srcDir: false, + importAlias: "@/*", + customizeImportAlias: false, + }; + const getPrefOrDefault = (field: string) => + preferences[field] ?? defaults[field]; + + if ( + !process.argv.includes("--eslint") && + !process.argv.includes("--no-eslint") + ) { + if (ciInfo.isCI) { + program.eslint = getPrefOrDefault("eslint"); + } else { + const styledEslint = blue("ESLint"); + const { eslint } = await prompts({ + onState: onPromptState, + type: "toggle", + name: "eslint", + message: `Would you like to use ${styledEslint}?`, + initial: getPrefOrDefault("eslint"), + active: "Yes", + inactive: "No", + }); + program.eslint = Boolean(eslint); + preferences.eslint = Boolean(eslint); } + } - if ( - typeof program.importAlias !== "string" || - !program.importAlias.length - ) { - if (ciInfo.isCI) { + if (typeof program.importAlias !== "string" || !program.importAlias.length) { + if (ciInfo.isCI) { + // We don't use preferences here because the default value is @/* regardless of existing preferences + program.importAlias = defaults.importAlias; + } else { + const styledImportAlias = blue("import alias"); + + const { customizeImportAlias } = await prompts({ + onState: onPromptState, + type: "toggle", + name: "customizeImportAlias", + message: `Would you like to customize the default ${styledImportAlias} (${defaults.importAlias})?`, + initial: getPrefOrDefault("customizeImportAlias"), + active: "Yes", + inactive: "No", + }); + + if (!customizeImportAlias) { // We don't use preferences here because the default value is @/* regardless of existing preferences program.importAlias = defaults.importAlias; } else { - const styledImportAlias = blue("import alias"); - - const { customizeImportAlias } = await prompts({ + const { importAlias } = await prompts({ onState: onPromptState, - type: "toggle", - name: "customizeImportAlias", - message: `Would you like to customize the default ${styledImportAlias} (${defaults.importAlias})?`, - initial: getPrefOrDefault("customizeImportAlias"), - active: "Yes", - inactive: "No", + type: "text", + name: "importAlias", + message: `What ${styledImportAlias} would you like configured?`, + initial: getPrefOrDefault("importAlias"), + validate: (value) => + /.+\/\*/.test(value) + ? true + : "Import alias must follow the pattern <prefix>/*", }); - - if (!customizeImportAlias) { - // We don't use preferences here because the default value is @/* regardless of existing preferences - program.importAlias = defaults.importAlias; - } else { - const { importAlias } = await prompts({ - onState: onPromptState, - type: "text", - name: "importAlias", - message: `What ${styledImportAlias} would you like configured?`, - initial: getPrefOrDefault("importAlias"), - validate: (value) => - /.+\/\*/.test(value) - ? true - : "Import alias must follow the pattern <prefix>/*", - }); - program.importAlias = importAlias; - preferences.importAlias = importAlias; - } + program.importAlias = importAlias; + preferences.importAlias = importAlias; } } } - try { - await createApp({ - appPath: resolvedProjectPath, - packageManager, - example: example && example !== "default" ? example : undefined, - examplePath: program.examplePath, - tailwind: true, - eslint: program.eslint, - srcDir: program.srcDir, - importAlias: program.importAlias, - }); - } catch (reason) { - if (!(reason instanceof DownloadError)) { - throw reason; - } - - const res = await prompts({ - onState: onPromptState, - type: "confirm", - name: "builtin", - message: - `Could not download "${example}" because of a connectivity issue between your machine and GitHub.\n` + - `Do you want to use the default template instead?`, - initial: true, - }); - if (!res.builtin) { - throw reason; - } - - await createApp({ - appPath: resolvedProjectPath, - packageManager, - eslint: program.eslint, - tailwind: true, - srcDir: program.srcDir, - importAlias: program.importAlias, - }); - } + await createApp({ + appPath: resolvedProjectPath, + packageManager, + tailwind: true, + eslint: program.eslint, + srcDir: program.srcDir, + importAlias: program.importAlias, + }); conf.set("preferences", preferences); } -- GitLab