@blitzjs/cli
Version:
Blitz.js CLI
275 lines (273 loc) • 12.7 kB
JavaScript
;
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`",
},
];