UNPKG

@blitzjs/cli

Version:
275 lines (273 loc) 12.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Install = exports.RecipeLocation = void 0; const tslib_1 = require("tslib"); const command_1 = require("@oclif/command"); const global_agent_1 = require("global-agent"); const logging_1 = require("next/dist/server/lib/logging"); const path_1 = require("path"); const stream_1 = require("stream"); const util_1 = require("util"); const command_2 = require("../command"); const debug = require("debug")("blitz:cli"); const pipeline = (0, util_1.promisify)(stream_1.Stream.pipeline); function got(url) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { return require("got")(url).catch((e) => { if (e.response.statusCode === 403) { (0, logging_1.baseLogger)({ displayDateTime: false }).error(e.response.body); } else { return e; } }); }); } function gotJSON(url) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { debug("[gotJSON] Downloading json from ", url); const res = yield got(url); return JSON.parse(res.body); }); } function isUrlValid(url) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { return (yield got(url).catch((e) => e)).statusCode === 200; }); } function requireJSON(file) { return JSON.parse(require("fs-extra").readFileSync(file).toString("utf-8")); } function checkLockFileExists(filename) { return require("fs-extra").existsSync((0, path_1.resolve)(filename)); } const GH_ROOT = "https://github.com/"; const API_ROOT = "https://api.github.com/repos/"; const RAW_ROOT = "https://raw.githubusercontent.com/"; const CODE_ROOT = "https://codeload.github.com/"; var RecipeLocation; (function (RecipeLocation) { RecipeLocation[RecipeLocation["Local"] = 0] = "Local"; RecipeLocation[RecipeLocation["Remote"] = 1] = "Remote"; })(RecipeLocation = exports.RecipeLocation || (exports.RecipeLocation = {})); class Install extends command_2.Command { // exposed for testing normalizeRecipePath(recipeArg) { const isNativeRecipe = /^([\w\-_]*)$/.test(recipeArg); const isUrlRecipe = recipeArg.startsWith(GH_ROOT); const isGitHubShorthandRecipe = /^([\w-_]*)\/([\w-_]*)$/.test(recipeArg); if (isNativeRecipe || isUrlRecipe || isGitHubShorthandRecipe) { let repoUrl; let subdirectory; switch (true) { case isUrlRecipe: repoUrl = recipeArg; break; case isNativeRecipe: repoUrl = `${GH_ROOT}blitz-js/legacy-framework`; subdirectory = `recipes/${recipeArg}`; break; case isGitHubShorthandRecipe: repoUrl = `${GH_ROOT}${recipeArg}`; break; default: throw new Error("should be impossible, the 3 cases are the only way to get into this switch"); } return { path: repoUrl, subdirectory, location: RecipeLocation.Remote, }; } else { return { path: recipeArg, location: RecipeLocation.Local, }; } } getOfficialRecipeList() { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { return yield gotJSON(`${API_ROOT}blitz-js/blitz/git/trees/canary?recursive=1`).then((release) => release.tree.reduce((recipesList, item) => { const filePath = item.path.split("/"); const [directory, recipeName] = filePath; if (directory === "recipes" && filePath.length === 2 && item.type === "tree") { recipesList.push(recipeName); } return recipesList; }, [])); }); } showRecipesPrompt(recipesList) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { debug("recipesList", recipesList); const { recipeName } = (yield this.enquirer.prompt({ type: "select", name: "recipeName", message: "Select a recipe to install", choices: recipesList, })); return recipeName; }); } /** * Clones the repository into a temp directory, returning the path to the new directory * * Exposed for unit testing * * @param repoFullName username and repository name in the form {{user}}/{{repo}} * @param defaultBranch the name of the repository's default branch */ cloneRepo(repoFullName, defaultBranch, subdirectory) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { debug("[cloneRepo] starting..."); const recipeDir = (0, path_1.join)(process.cwd(), ".blitz", "recipe-install"); // clean up from previous run in case of error require("rimraf").sync(recipeDir); require("fs-extra").mkdirsSync(recipeDir); process.chdir(recipeDir); debug("Extracting recipe to ", recipeDir); const repoName = repoFullName.split("/")[1]; // `tar` top-level filter is `${repoName}-${defaultBranch}`, and then we want to get our recipe path // within that folder const extractPath = subdirectory ? [`${repoName}-${defaultBranch}/${subdirectory}`] : undefined; const depth = subdirectory ? subdirectory.split("/").length + 1 : 1; yield pipeline(require("got").stream(`${CODE_ROOT}${repoFullName}/tar.gz/${defaultBranch}`), require("tar").extract({ strip: depth }, extractPath)); return recipeDir; }); } installRecipeAtPath(recipePath, ...runArgs) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { const recipe = require(recipePath).default; yield recipe.run(...runArgs); }); } /** * Setup proxy support for blitz install * * Loads proxy variables from enviroment and blitz.config.js * */ setupProxySupport() { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { const { loadConfigProduction } = yield Promise.resolve().then(() => (0, tslib_1.__importStar)(require("next/dist/server/config-shared"))); const blitzConfig = loadConfigProduction(process.cwd()); const cli = blitzConfig.cli; const httpProxy = (cli === null || cli === void 0 ? void 0 : cli.httpProxy) || process.env.http_proxy || process.env.HTTP_PROXY; const httpsProxy = (cli === null || cli === void 0 ? void 0 : cli.httpsProxy) || process.env.https_proxy || process.env.HTTPS_PROXY; const noProxy = (cli === null || cli === void 0 ? void 0 : cli.noProxy) || process.env.no_proxy || process.env.NO_PROXY; if (httpProxy || httpsProxy) { global.GLOBAL_AGENT = { HTTP_PROXY: httpProxy, HTTPS_PROXY: httpsProxy, NO_PROXY: noProxy, }; (0, global_agent_1.bootstrap)(); } }); } run() { var _a; return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { this.parse(Install); require("../utils/setup-ts-node").setupTsnode(); yield this.setupProxySupport(); const { args, flags, argv } = this.parse(Install); let selectedRecipe = args.recipe; if (!selectedRecipe) { const officialRecipeList = yield this.getOfficialRecipeList(); selectedRecipe = yield this.showRecipesPrompt(officialRecipeList); } const recipeInfo = this.normalizeRecipePath(selectedRecipe); const originalCwd = process.cwd(); // Take all the args after the recipe string // // ['material-ui', '--yes', 'prop=true'] // --> ['material-ui', 'prop=true'] // --> ['prop=true'] // --> { prop: 'true' } const cliArgs = argv .filter((arg) => !arg.startsWith("--")) .slice(1) .reduce((acc, arg) => (Object.assign(Object.assign({}, acc), { [arg.split("=")[0]]: arg.split("=")[1] ? JSON.parse(`"${arg.split("=")[1]}"`) : true })), {}); const cliFlags = { yesToAll: flags.yes || false, }; debug("recipeInfo", recipeInfo); const chalk = (yield Promise.resolve().then(() => (0, tslib_1.__importStar)(require("chalk")))).default; if (recipeInfo.location === RecipeLocation.Remote) { const apiUrl = recipeInfo.path.replace(GH_ROOT, API_ROOT); const rawUrl = recipeInfo.path.replace(GH_ROOT, RAW_ROOT); const repoInfo = yield gotJSON(apiUrl); const packageJsonPath = (0, path_1.join)(`${rawUrl}`, repoInfo.default_branch, (_a = recipeInfo.subdirectory) !== null && _a !== void 0 ? _a : "", "package.json"); if (!(yield isUrlValid(packageJsonPath))) { debug("Url is invalid for ", packageJsonPath); (0, logging_1.baseLogger)({ displayDateTime: false }).error(`Could not find recipe "${args.recipe}"\n`); console.log(`${chalk.bold("Please provide one of the following:")} 1. The name of a recipe to install (e.g. "tailwind") ${chalk.dim("- Available recipes listed at https://github.com/blitz-js/blitz/tree/canary/recipes")} 2. The full name of a GitHub repository (e.g. "blitz-js/example-recipe"), 3. A full URL to a Github repository (e.g. "https://github.com/blitz-js/example-recipe"), or 4. A file path to a locally-written recipe.\n`); process.exit(1); } else { let spinner = logging_1.log.spinner(`Cloning GitHub repository for ${selectedRecipe} recipe`).start(); const recipeRepoPath = yield this.cloneRepo(repoInfo.full_name, repoInfo.default_branch, recipeInfo.subdirectory); spinner.stop(); spinner = logging_1.log.spinner("Installing package.json dependencies").start(); let pkgManager = "npm"; let installArgs = ["install", "--legacy-peer-deps", "--ignore-scripts"]; if (checkLockFileExists("yarn.lock")) { pkgManager = "yarn"; installArgs = ["install", "--ignore-scripts"]; } else if (checkLockFileExists("pnpm-lock.yaml")) { pkgManager = "pnpm"; installArgs = ["install", "--ignore-scripts"]; } yield new Promise((resolve) => { const installProcess = require("cross-spawn")(pkgManager, installArgs); installProcess.on("exit", resolve); }); spinner.stop(); const recipePackageMain = requireJSON("./package.json").main; const recipeEntry = (0, path_1.resolve)(recipePackageMain); process.chdir(originalCwd); yield this.installRecipeAtPath(recipeEntry, cliArgs, cliFlags); require("rimraf").sync(recipeRepoPath); } } else { yield this.installRecipeAtPath((0, path_1.resolve)(args.recipe), cliArgs, cliFlags); } }); } } exports.Install = Install; Install.description = "Install a Recipe into your Blitz app"; Install.aliases = ["i"]; Install.strict = false; Install.flags = { help: command_1.flags.help({ char: "h" }), yes: command_1.flags.boolean({ char: "y", default: false, description: "Install the recipe automatically without user confirmation", }), env: command_1.flags.string({ char: "e", description: "Set app environment name", }), }; Install.args = [ { name: "recipe", required: false, description: "Name of a Blitz recipe from @blitzjs/legacy-framework/recipes, or a file path to a local recipe definition", }, { name: "recipe-flags", description: "A list of flags to pass to the recipe. Blitz will only parse these in the form `key=value`", }, ];