Skip to content
Snippets Groups Projects
create-app.ts 6.97 KiB
Newer Older
/* eslint-disable import/no-extraneous-dependencies */
Marcus Schiesser's avatar
Marcus Schiesser committed
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,
Marcus Schiesser's avatar
Marcus Schiesser committed
  getRepoInfo,
  hasRepo,
Marcus Schiesser's avatar
Marcus Schiesser committed
} from "./helpers/examples";
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";
Marcus Schiesser's avatar
Marcus Schiesser committed
import type { TemplateMode, TemplateType } from "./templates";
import { getTemplateFile, installTemplate } from "./templates";

export class DownloadError extends Error {}

export async function createApp({
  appPath,
  packageManager,
  example,
  examplePath,
  tailwind,
  eslint,
  srcDir,
  importAlias,
}: {
Marcus Schiesser's avatar
Marcus Schiesser committed
  appPath: string;
  packageManager: PackageManager;
  example?: string;
  examplePath?: string;
  tailwind: boolean;
  eslint: boolean;
  srcDir: boolean;
  importAlias: string;
}): Promise<void> {
Marcus Schiesser's avatar
Marcus Schiesser committed
  let repoInfo: RepoInfo | undefined;
  const mode: TemplateMode = "nextjs";
  const template: TemplateType = "simple";

  if (example) {
Marcus Schiesser's avatar
Marcus Schiesser committed
    let repoUrl: URL | undefined;
Marcus Schiesser's avatar
Marcus Schiesser committed
      repoUrl = new URL(example);
    } catch (error: any) {
Marcus Schiesser's avatar
Marcus Schiesser committed
      if (error.code !== "ERR_INVALID_URL") {
        console.error(error);
        process.exit(1);
Marcus Schiesser's avatar
Marcus Schiesser committed
      if (repoUrl.origin !== "https://github.com") {
        console.error(
          `Invalid URL: ${red(
Marcus Schiesser's avatar
Marcus Schiesser committed
            `"${example}"`,
          )}. Only GitHub repositories are supported. Please use a GitHub URL and try again.`,
        );
        process.exit(1);
Marcus Schiesser's avatar
Marcus Schiesser committed
      repoInfo = await getRepoInfo(repoUrl, examplePath);

      if (!repoInfo) {
        console.error(
          `Found invalid GitHub URL: ${red(
Marcus Schiesser's avatar
Marcus Schiesser committed
            `"${example}"`,
          )}. Please fix the URL and try again.`,
        );
        process.exit(1);
Marcus Schiesser's avatar
Marcus Schiesser committed
      const found = await hasRepo(repoInfo);

      if (!found) {
        console.error(
          `Could not locate the repository for ${red(
Marcus Schiesser's avatar
Marcus Schiesser committed
            `"${example}"`,
          )}. Please check that the repository exists and try again.`,
        );
        process.exit(1);
Marcus Schiesser's avatar
Marcus Schiesser committed
    } else if (example !== "__internal-testing-retry") {
      const found = await existsInRepo(example);

      if (!found) {
        console.error(
          `Could not locate an example named ${red(
Marcus Schiesser's avatar
Marcus Schiesser committed
            `"${example}"`,
          )}. It could be due to the following:\n`,
          `1. Your spelling of example ${red(
Marcus Schiesser's avatar
Marcus Schiesser committed
            `"${example}"`,
          )} might be incorrect.\n`,
Marcus Schiesser's avatar
Marcus Schiesser committed
          `2. You might not be connected to the internet or you are behind a proxy.`,
        );
        process.exit(1);
Marcus Schiesser's avatar
Marcus Schiesser committed
  const root = path.resolve(appPath);

  if (!(await isWriteable(path.dirname(root)))) {
    console.error(
Marcus Schiesser's avatar
Marcus Schiesser committed
      "The application path is not writable, please check folder permissions and try again.",
    );
    console.error(
Marcus Schiesser's avatar
Marcus Schiesser committed
      "It is likely you do not have write permissions for this folder.",
    );
    process.exit(1);
Marcus Schiesser's avatar
Marcus Schiesser committed
  const appName = path.basename(root);
Marcus Schiesser's avatar
Marcus Schiesser committed
  await makeDir(root);
  if (!isFolderEmpty(root, appName)) {
Marcus Schiesser's avatar
Marcus Schiesser committed
    process.exit(1);
Marcus Schiesser's avatar
Marcus Schiesser committed
  const useYarn = packageManager === "yarn";
  const isOnline = !useYarn || (await getOnline());
  const originalDirectory = process.cwd();
Marcus Schiesser's avatar
Marcus Schiesser committed
  console.log(`Creating a new LlamaIndex app in ${green(root)}.`);
  console.log();
Marcus Schiesser's avatar
Marcus Schiesser committed
  process.chdir(root);
Marcus Schiesser's avatar
Marcus Schiesser committed
  const packageJsonPath = path.join(root, "package.json");
  let hasPackageJson = false;

  if (example) {
    /**
     * If an example repository is provided, clone it.
     */
    try {
      if (repoInfo) {
Marcus Schiesser's avatar
Marcus Schiesser committed
        const repoInfo2 = repoInfo;
        console.log(
          `Downloading files from repo ${cyan(
Marcus Schiesser's avatar
Marcus Schiesser committed
            example,
          )}. This might take a moment.`,
        );
        console.log();
        await retry(() => downloadAndExtractRepo(root, repoInfo2), {
          retries: 3,
Marcus Schiesser's avatar
Marcus Schiesser committed
        });
      } else {
        console.log(
          `Downloading files for example ${cyan(
Marcus Schiesser's avatar
Marcus Schiesser committed
            example,
          )}. This might take a moment.`,
        );
        console.log();
        await retry(() => downloadAndExtractExample(root, example), {
          retries: 3,
Marcus Schiesser's avatar
Marcus Schiesser committed
        });
      }
    } catch (reason) {
      function isErrorLike(err: unknown): err is { message: string } {
        return (
Marcus Schiesser's avatar
Marcus Schiesser committed
          typeof err === "object" &&
          err !== null &&
Marcus Schiesser's avatar
Marcus Schiesser committed
          typeof (err as { message?: unknown }).message === "string"
        );
      }
      throw new DownloadError(
Marcus Schiesser's avatar
Marcus Schiesser committed
        isErrorLike(reason) ? reason.message : reason + "",
      );
    }
    // Copy `.gitignore` if the application did not provide one
Marcus Schiesser's avatar
Marcus Schiesser committed
    const ignorePath = path.join(root, ".gitignore");
    if (!fs.existsSync(ignorePath)) {
      fs.copyFileSync(
Marcus Schiesser's avatar
Marcus Schiesser committed
        getTemplateFile({ template, mode, file: "gitignore" }),
        ignorePath,
      );
    }

    // Copy `next-env.d.ts` to any example that is typescript
Marcus Schiesser's avatar
Marcus Schiesser committed
    const tsconfigPath = path.join(root, "tsconfig.json");
    if (fs.existsSync(tsconfigPath)) {
      fs.copyFileSync(
Marcus Schiesser's avatar
Marcus Schiesser committed
        getTemplateFile({ template, mode: "nextjs", file: "next-env.d.ts" }),
        path.join(root, "next-env.d.ts"),
      );
Marcus Schiesser's avatar
Marcus Schiesser committed
    hasPackageJson = fs.existsSync(packageJsonPath);
    if (hasPackageJson) {
Marcus Schiesser's avatar
Marcus Schiesser committed
      console.log("Installing packages. This might take a couple of minutes.");
      console.log();
Marcus Schiesser's avatar
Marcus Schiesser committed
      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,
Marcus Schiesser's avatar
Marcus Schiesser committed
    });
  }

  if (tryGitInit(root)) {
Marcus Schiesser's avatar
Marcus Schiesser committed
    console.log("Initialized a git repository.");
    console.log();
Marcus Schiesser's avatar
Marcus Schiesser committed
  let cdpath: string;
  if (path.join(originalDirectory, appName) === appPath) {
Marcus Schiesser's avatar
Marcus Schiesser committed
    cdpath = appName;
  } else {
Marcus Schiesser's avatar
Marcus Schiesser committed
    cdpath = appPath;
Marcus Schiesser's avatar
Marcus Schiesser committed
  console.log(`${green("Success!")} Created ${appName} at ${appPath}`);

  if (hasPackageJson) {
Marcus Schiesser's avatar
Marcus Schiesser committed
    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`)}`);
Marcus Schiesser's avatar
Marcus Schiesser committed
  console.log();