create-ipfs-app
Version:
Create IPFS apps with no build configuration.
1,254 lines (1,169 loc) • 37.9 kB
JavaScript
;
const https = require("https");
const chalk = require("chalk");
const commander = require("commander");
const dns = require("dns");
const envinfo = require("envinfo");
const execSync = require("child_process").execSync;
const fs = require("fs-extra");
const hyperquest = require("hyperquest");
const prompts = require("prompts");
const os = require("os");
const path = require("path");
const semver = require("semver");
const spawn = require("cross-spawn");
const tmp = require("tmp");
const unpack = require("tar-pack").unpack;
const url = require("url");
const replace = require("replace-in-file");
const gradient = require("gradient-string");
const validateProjectName = require("validate-npm-package-name");
const packageJson = require("./package.json");
function isUsingYarn() {
return (process.env.npm_config_user_agent || "").indexOf("yarn") === 0;
}
let projectName;
function init() {
const program = new commander.Command(packageJson.name)
.version(packageJson.version)
.arguments("<project-directory>")
.usage(`${chalk.green("<project-directory>")} [options]`)
.action((name) => {
projectName = name;
})
.option("--verbose", "print additional logs")
.option("--info", "print environment debug info")
.option(
"--template <path-to-template>",
"specify a template for the created project"
)
.option("--moralis <api-key>", "moralis.io web3 api key")
.option("--pinata <api-key:api-secret>", "pinata.cloud api key:secret")
.option("--web3 <api-token>", "web3.storage api token")
.option(
"--filebase <api-key:api-secret:bucket-name>",
"filebase.com api key:secret:name"
)
.option("--use-pnp")
.allowUnknownOption()
.on("--help", () => {
logo();
})
.parse(process.argv);
if (program.info) {
logo();
console.log(chalk.bold("\nEnvironment Info:"));
console.log(
`\n current version of ${packageJson.name}: ${packageJson.version}`
);
console.log(` running from ${__dirname}`);
return envinfo
.run(
{
System: ["OS", "CPU"],
Binaries: ["Node", "npm", "Yarn"],
Browsers: [
"Chrome",
"Edge",
"Internet Explorer",
"Firefox",
"Safari",
],
npmPackages: ["react", "react-dom", "react-scripts", "ipfs-scripts"],
npmGlobalPackages: ["create-ipfs-app"],
},
{
duplicates: true,
showNotFound: true,
}
)
.then(console.log);
}
if (typeof projectName === "undefined") {
console.error("Please specify the project directory:");
console.log(
` ${chalk.cyan(program.name())} ${chalk.green("<project-directory>")}`
);
console.log();
console.log("For example:");
console.log(
` ${chalk.cyan(program.name())} ${chalk.green("my-ipfs-app")}`
);
console.log();
console.log(
`Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`
);
process.exit(1);
}
const ipfs = [];
if (program.infura) {
// ipfs.push(`INFURA="${Buffer.from(program.infura).toString("base64")}"`);
}
if (program.moralis) {
ipfs.push(`MORALIS="${program.moralis}"`);
}
if (program.pinata) {
ipfs.push(`PINATA="${program.pinata}"`);
}
if (program.web3) {
ipfs.push(`WEB3="${program.web3}"`);
}
if (program.filebase) {
ipfs.push(`FILEBASE="${program.filebase}"`);
}
// We first check the registry directly via the API, and if that fails, we try
// the slower `npm view [package] version` command.
//
// This is important for users in environments where direct access to npm is
// blocked by a firewall, and packages are provided exclusively via a private
// registry.
checkForLatestVersion()
.catch(() => {
try {
return execSync("npm view create-ipfs-app version").toString().trim();
} catch (e) {
return null;
}
})
.then((latest) => {
if (latest && semver.lt(packageJson.version, latest)) {
console.log();
console.error(
chalk.yellow(
`You are running \`create-ipfs-app\` ${packageJson.version}, which is behind the latest release (${latest}).\n\n` +
"We recommend always using the latest version of create-ipfs-app if possible."
)
);
console.log();
console.log(
"The latest instructions for creating a new app can be found here:\n" +
"https://create-ipfs-app.dev/"
);
console.log();
} else {
const useYarn = isUsingYarn();
createApp(
projectName,
program.verbose,
program.scriptsVersion,
program.template,
ipfs,
useYarn,
program.usePnp
);
}
});
}
function createApp(name, verbose, version, template, ipfs, useYarn, usePnp) {
const unsupportedNodeVersion = !semver.satisfies(
// Coerce strings with metadata (i.e. `15.0.0-nightly`).
semver.coerce(process.version),
">=14"
);
if (unsupportedNodeVersion) {
console.log(
chalk.yellow(
`You are using Node ${process.version} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
`Please update to Node 14 or higher for a better, fully supported experience.\n`
)
);
// Fall back to latest supported react-scripts on Node 4
version = "react-scripts@0.9.x";
}
const root = path.resolve(name);
const appName = path.basename(root);
checkAppName(appName);
fs.ensureDirSync(name);
if (!isSafeToCreateProjectIn(root, name)) {
process.exit(1);
}
console.log();
logo();
console.log();
console.log(
gradient.pastel(
`Creating a new IPFS app «${name}» with deployment on ${ipfs
.map((s) => s.split("=")[0])
.join(",")}`
)
);
console.log();
const packageJson = {
name: appName,
version: "0.1.0",
private: true,
};
fs.writeFileSync(
path.join(root, "package.json"),
JSON.stringify(packageJson, null, 2) + os.EOL
);
if (ipfs.length) {
fs.writeFileSync(path.join(root, ".env"), ipfs.join(os.EOL) + os.EOL);
}
const originalDirectory = process.cwd();
process.chdir(root);
if (!useYarn && !checkThatNpmCanReadCwd()) {
process.exit(1);
}
if (!useYarn) {
const npmInfo = checkNpmVersion();
if (!npmInfo.hasMinNpm) {
if (npmInfo.npmVersion) {
console.log(
chalk.yellow(
`You are using npm ${npmInfo.npmVersion} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
`Please update to npm 6 or higher for a better, fully supported experience.\n`
)
);
}
// Fall back to latest supported react-scripts for npm 3
version = "react-scripts@0.9.x";
}
} else if (usePnp) {
const yarnInfo = checkYarnVersion();
if (yarnInfo.yarnVersion) {
if (!yarnInfo.hasMinYarnPnp) {
console.log(
chalk.yellow(
`You are using Yarn ${yarnInfo.yarnVersion} together with the --use-pnp flag, but Plug'n'Play is only supported starting from the 1.12 release.\n\n` +
`Please update to Yarn 1.12 or higher for a better, fully supported experience.\n`
)
);
// 1.11 had an issue with webpack-dev-middleware, so better not use PnP with it (never reached stable, but still)
usePnp = false;
}
if (!yarnInfo.hasMaxYarnPnp) {
console.log(
chalk.yellow(
"The --use-pnp flag is no longer necessary with yarn 2 and will be deprecated and removed in a future release.\n"
)
);
// 2 supports PnP by default and breaks when trying to use the flag
usePnp = false;
}
}
}
run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp,
ipfs
);
}
function install(root, useYarn, usePnp, dependencies, verbose, isOnline) {
return new Promise((resolve, reject) => {
let command;
let args;
if (useYarn) {
command = "yarnpkg";
args = ["add", "--exact"];
if (!isOnline) {
args.push("--offline");
}
if (usePnp) {
args.push("--enable-pnp");
}
[].push.apply(args, dependencies);
// Explicitly set cwd() to work around issues like
// https://github.com/facebook/create-react-app/issues/3326.
// Unfortunately we can only do this for Yarn because npm support for
// equivalent --prefix flag doesn't help with this issue.
// This is why for npm, we run checkThatNpmCanReadCwd() early instead.
args.push("--cwd");
args.push(root);
if (!isOnline) {
console.log(chalk.yellow("You appear to be offline."));
console.log(chalk.yellow("Falling back to the local Yarn cache."));
console.log();
}
} else {
command = "npm";
args = [
"install",
"--no-audit", // https://github.com/facebook/create-react-app/issues/11174
"--save",
"--save-exact",
"--loglevel",
"error",
].concat(dependencies);
if (usePnp) {
console.log(chalk.yellow("NPM doesn't support PnP."));
console.log(chalk.yellow("Falling back to the regular installs."));
console.log();
}
}
if (verbose) {
args.push("--verbose");
}
const child = spawn(command, args, { stdio: "inherit" });
child.on("close", (code) => {
if (code !== 0) {
reject({
command: `${command} ${args.join(" ")}`,
});
return;
}
resolve();
});
});
}
function run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp,
ipfs
) {
Promise.all([
getInstallPackage(version, originalDirectory),
getTemplateInstallPackage(template, originalDirectory),
]).then(([packageToInstall, templateToInstall]) => {
const allDependencies = [
"react",
"react-dom",
packageToInstall,
"ipfs-scripts",
];
console.log(
gradient.atlas(
"Installing packages. This might take a couple of minutes."
)
);
console.log();
Promise.all([
getPackageInfo(packageToInstall),
getPackageInfo(templateToInstall),
])
.then(([packageInfo, templateInfo]) =>
checkIfOnline(useYarn).then((isOnline) => ({
isOnline,
packageInfo,
templateInfo,
}))
)
.then(({ isOnline, packageInfo, templateInfo }) => {
let packageVersion = semver.coerce(packageInfo.version);
const templatesVersionMinimum = "3.3.0";
// Assume compatibility if we can't test the version.
if (!semver.valid(packageVersion)) {
packageVersion = templatesVersionMinimum;
}
// Only support templates when used alongside new react-scripts versions.
const supportsTemplates = semver.gte(
packageVersion,
templatesVersionMinimum
);
if (supportsTemplates) {
allDependencies.push(templateToInstall);
} else if (template) {
console.log("");
console.log(
`The ${chalk.cyan(packageInfo.name)} version you're using ${
packageInfo.name === "react-scripts" ? "is not" : "may not be"
} compatible with the ${chalk.cyan("--template")} option.`
);
console.log("");
}
return install(
root,
useYarn,
usePnp,
allDependencies,
verbose,
isOnline
).then(() => ({
packageInfo,
supportsTemplates,
templateInfo,
}));
})
.then(async ({ packageInfo, supportsTemplates, templateInfo }) => {
const packageName = packageInfo.name;
const templateName = supportsTemplates ? templateInfo.name : undefined;
checkNodeVersion(packageName);
setCaretRangeForRuntimeDeps(packageName);
const pnpPath = path.resolve(process.cwd(), ".pnp.js");
const nodeArgs = fs.existsSync(pnpPath) ? ["--require", pnpPath] : [];
await executeNodeScript(
{
cwd: process.cwd(),
args: nodeArgs,
},
[root, appName, verbose, originalDirectory, templateName],
`
const init = require('${packageName}/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`
);
/*
Added script deploy:moralis
Added script deploy:pinata
Added script deploy:web3
Added script deploy:filebase
Changed IPFS index page
*/
setTimeout(function () {
replace.sync({
files: path.join(process.cwd(), "package.json"),
from: '"scripts": {',
to:
'"scripts": {' +
'\n "predeploy": "npm run build",' +
'\n "deploy:moralis": "ipfs-scripts moralis",' +
'\n "deploy:pinata": "ipfs-scripts pinata",' +
'\n "deploy:web3": "ipfs-scripts web3",' +
'\n "deploy:filebase": "ipfs-scripts filebase",',
});
replace.sync({
files: path.join(process.cwd(), "src", "App.css"),
from: "background-color: #282c34;",
to: "background: linear-gradient(to bottom, #041727 0, #062b3f 100%);",
});
replace.sync({
files: [
path.join(process.cwd(), "src", "App.js"),
path.join(process.cwd(), "src", "App.test.js"),
path.join(process.cwd(), "src", "App.ts"),
path.join(process.cwd(), "src", "App.test.ts"),
],
from: ["reactjs.org", "Learn React", /learn react/g],
to: ["ipfs.tech", "Learn IPFS", "learn ipfs"],
});
replace.sync({
files: path.join(process.cwd(), "public", "index.html"),
from: ["create-react-app", "React App"],
to: ["create-ipfs-app", "IPFS App"],
});
fs.copyFileSync(
path.join(__dirname, "public", "logo.svg"),
path.join(process.cwd(), "src", "logo.svg")
);
fs.copyFileSync(
path.join(__dirname, "public", "logo192.png"),
path.join(process.cwd(), "public", "logo192.png")
);
fs.copyFileSync(
path.join(__dirname, "public", "logo512.png"),
path.join(process.cwd(), "public", "logo512.png")
);
fs.copyFileSync(
path.join(__dirname, "public", "favicon.ico"),
path.join(process.cwd(), "public", "favicon.ico")
);
execSync("git add -A", { stdio: "ignore" });
execSync('git commit -m "Initialize project using Create IPFS App"', {
stdio: "ignore",
});
// Change displayed command to yarn instead of yarnpkg
const displayedCommand = useYarn ? "yarn" : "npm";
console.log(
gradient.rainbow.multiline(" ___ _ _ ___ ___ ___ ___ ___ ")
);
console.log(
gradient.rainbow.multiline("/ __| | | |/ __/ __/ _ \\/ __/ __|")
);
console.log(
gradient.rainbow.multiline("\\__ \\ |_| | (_| (_| __/\\__ \\__ \\")
);
console.log(
gradient.rainbow.multiline("|___/\\__,_|\\___\\___\\___||___/___/")
);
console.log();
console.log(gradient.cristal(`${process.cwd()}`));
console.log();
console.log(
gradient.teen(
"Inside that directory, you can run several commands:"
)
);
console.log();
console.log(gradient.morning(` ${displayedCommand} start`));
console.log();
console.log(gradient.morning(` ${displayedCommand} test`));
console.log();
console.log(
gradient.morning(
` ${displayedCommand} ${useYarn ? "" : "run "}build`
)
);
console.log();
console.log(
gradient.morning(
` ${displayedCommand} ${useYarn ? "" : "run "}eject`
)
);
console.log();
if (ipfs.filter((s) => !!(s.indexOf("WEB3=") + 1)).length) {
console.log(
gradient.morning(
` ${displayedCommand} ${useYarn ? "" : "run "}deploy:web3`
)
);
console.log();
}
if (ipfs.filter((s) => !!(s.indexOf("MORALIS=") + 1)).length) {
console.log(
gradient.morning(
` ${displayedCommand} ${useYarn ? "" : "run "}deploy:moralis`
)
);
console.log();
}
if (ipfs.filter((s) => !!(s.indexOf("PINATA=") + 1)).length) {
console.log(
gradient.morning(
` ${displayedCommand} ${useYarn ? "" : "run "}deploy:pinata`
)
);
console.log();
}
if (ipfs.filter((s) => !!(s.indexOf("FILEBASE=") + 1)).length) {
console.log(
gradient.morning(
` ${displayedCommand} ${useYarn ? "" : "run "}deploy:filebase`
)
);
console.log();
}
console.log(gradient.cristal("We suggest that you begin by typing:"));
console.log();
console.log(gradient.summer(" cd " + appName));
console.log(` ${gradient.summer(`${displayedCommand} start`)}`);
console.log();
console.log(gradient.cristal("Let's build real decentralization!"));
console.log();
}, 1000);
/*
End
*/
if (version === "react-scripts@0.9.x") {
console.log(
chalk.yellow(
`\nNote: the project was bootstrapped with an old unsupported version of tools.\n` +
`Please update to Node >=14 and npm >=6 to get supported tools in new projects.\n`
)
);
}
})
.catch((reason) => {
console.log();
console.log("Aborting installation.");
if (reason.command) {
console.log(` ${chalk.cyan(reason.command)} has failed.`);
} else {
console.log(
chalk.red("Unexpected error. Please report it as a bug:")
);
console.log(reason);
}
console.log();
// On 'exit' we will delete these files from target directory.
const knownGeneratedFiles = ["package.json", "node_modules"];
const currentFiles = fs.readdirSync(path.join(root));
currentFiles.forEach((file) => {
knownGeneratedFiles.forEach((fileToMatch) => {
// This removes all knownGeneratedFiles.
if (file === fileToMatch) {
console.log(`Deleting generated file... ${chalk.cyan(file)}`);
fs.removeSync(path.join(root, file));
}
});
});
const remainingFiles = fs.readdirSync(path.join(root));
if (!remainingFiles.length) {
// Delete target folder if empty
console.log(
`Deleting ${chalk.cyan(`${appName}/`)} from ${chalk.cyan(
path.resolve(root, "..")
)}`
);
process.chdir(path.resolve(root, ".."));
fs.removeSync(path.join(root));
}
console.log("Done.");
process.exit(1);
});
});
}
function getInstallPackage(version, originalDirectory) {
let packageToInstall = "react-scripts";
const validSemver = semver.valid(version);
if (validSemver) {
packageToInstall += `@${validSemver}`;
} else if (version) {
if (version[0] === "@" && !version.includes("/")) {
packageToInstall += version;
} else if (version.match(/^file:/)) {
packageToInstall = `file:${path.resolve(
originalDirectory,
version.match(/^file:(.*)?$/)[1]
)}`;
} else {
// for tar.gz or alternative paths
packageToInstall = version;
}
}
const scriptsToWarn = [
{
name: "react-scripts-ts",
message: chalk.yellow(
`The react-scripts-ts package is deprecated. TypeScript is now supported natively in Create React App. You can use the ${chalk.green(
"--template typescript"
)} option instead when generating your app to include TypeScript support. Would you like to continue using react-scripts-ts?`
),
},
];
for (const script of scriptsToWarn) {
if (packageToInstall.startsWith(script.name)) {
return prompts({
type: "confirm",
name: "useScript",
message: script.message,
initial: false,
}).then((answer) => {
if (!answer.useScript) {
process.exit(0);
}
return packageToInstall;
});
}
}
return Promise.resolve(packageToInstall);
}
function getTemplateInstallPackage(template, originalDirectory) {
let templateToInstall = "cra-template";
if (template) {
if (template.match(/^file:/)) {
templateToInstall = `file:${path.resolve(
originalDirectory,
template.match(/^file:(.*)?$/)[1]
)}`;
} else if (
template.includes("://") ||
template.match(/^.+\.(tgz|tar\.gz)$/)
) {
// for tar.gz or alternative paths
templateToInstall = template;
} else {
// Add prefix 'cra-template-' to non-prefixed templates, leaving any
// @scope/ and @version intact.
const packageMatch = template.match(/^(@[^/]+\/)?([^@]+)?(@.+)?$/);
const scope = packageMatch[1] || "";
const templateName = packageMatch[2] || "";
const version = packageMatch[3] || "";
if (
templateName === templateToInstall ||
templateName.startsWith(`${templateToInstall}-`)
) {
// Covers:
// - cra-template
// - @SCOPE/cra-template
// - cra-template-NAME
// - @SCOPE/cra-template-NAME
templateToInstall = `${scope}${templateName}${version}`;
} else if (version && !scope && !templateName) {
// Covers using @SCOPE only
templateToInstall = `${version}/${templateToInstall}`;
} else {
// Covers templates without the `cra-template` prefix:
// - NAME
// - @SCOPE/NAME
templateToInstall = `${scope}${templateToInstall}-${templateName}${version}`;
}
}
}
return Promise.resolve(templateToInstall);
}
function getTemporaryDirectory() {
return new Promise((resolve, reject) => {
// Unsafe cleanup lets us recursively delete the directory if it contains
// contents; by default it only allows removal if it's empty
tmp.dir({ unsafeCleanup: true }, (err, tmpdir, callback) => {
if (err) {
reject(err);
} else {
resolve({
tmpdir: tmpdir,
cleanup: () => {
try {
callback();
} catch (ignored) {
// Callback might throw and fail, since it's a temp directory the
// OS will clean it up eventually...
}
},
});
}
});
});
}
function extractStream(stream, dest) {
return new Promise((resolve, reject) => {
stream.pipe(
unpack(dest, (err) => {
if (err) {
reject(err);
} else {
resolve(dest);
}
})
);
});
}
// Extract package name from tarball url or path.
function getPackageInfo(installPackage) {
if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {
return getTemporaryDirectory()
.then((obj) => {
let stream;
if (/^http/.test(installPackage)) {
stream = hyperquest(installPackage);
} else {
stream = fs.createReadStream(installPackage);
}
return extractStream(stream, obj.tmpdir).then(() => obj);
})
.then((obj) => {
const { name, version } = require(path.join(
obj.tmpdir,
"package.json"
));
obj.cleanup();
return { name, version };
})
.catch((err) => {
// The package name could be with or without semver version, e.g. react-scripts-0.2.0-alpha.1.tgz
// However, this function returns package name only without semver version.
console.log(
`Could not extract the package name from the archive: ${err.message}`
);
const assumedProjectName = installPackage.match(
/^.+\/(.+?)(?:-\d+.+)?\.(tgz|tar\.gz)$/
)[1];
console.log(
`Based on the filename, assuming it is "${chalk.cyan(
assumedProjectName
)}"`
);
return Promise.resolve({ name: assumedProjectName });
});
} else if (installPackage.startsWith("git+")) {
// Pull package name out of git urls e.g:
// git+https://github.com/mycompany/react-scripts.git
// git+ssh://github.com/mycompany/react-scripts.git#v1.2.3
return Promise.resolve({
name: installPackage.match(/([^/]+)\.git(#.*)?$/)[1],
});
} else if (installPackage.match(/.+@/)) {
// Do not match @scope/ when stripping off @version or @tag
return Promise.resolve({
name: installPackage.charAt(0) + installPackage.substr(1).split("@")[0],
version: installPackage.split("@")[1],
});
} else if (installPackage.match(/^file:/)) {
const installPackagePath = installPackage.match(/^file:(.*)?$/)[1];
const { name, version } = require(path.join(
installPackagePath,
"package.json"
));
return Promise.resolve({ name, version });
}
return Promise.resolve({ name: installPackage });
}
function checkNpmVersion() {
let hasMinNpm = false;
let npmVersion = null;
try {
npmVersion = execSync("npm --version").toString().trim();
hasMinNpm = semver.gte(npmVersion, "6.0.0");
} catch (err) {
// ignore
}
return {
hasMinNpm: hasMinNpm,
npmVersion: npmVersion,
};
}
function checkYarnVersion() {
const minYarnPnp = "1.12.0";
const maxYarnPnp = "2.0.0";
let hasMinYarnPnp = false;
let hasMaxYarnPnp = false;
let yarnVersion = null;
try {
yarnVersion = execSync("yarnpkg --version").toString().trim();
if (semver.valid(yarnVersion)) {
hasMinYarnPnp = semver.gte(yarnVersion, minYarnPnp);
hasMaxYarnPnp = semver.lt(yarnVersion, maxYarnPnp);
} else {
// Handle non-semver compliant yarn version strings, which yarn currently
// uses for nightly builds. The regex truncates anything after the first
// dash. See #5362.
const trimmedYarnVersionMatch = /^(.+?)[-+].+$/.exec(yarnVersion);
if (trimmedYarnVersionMatch) {
const trimmedYarnVersion = trimmedYarnVersionMatch.pop();
hasMinYarnPnp = semver.gte(trimmedYarnVersion, minYarnPnp);
hasMaxYarnPnp = semver.lt(trimmedYarnVersion, maxYarnPnp);
}
}
} catch (err) {
// ignore
}
return {
hasMinYarnPnp: hasMinYarnPnp,
hasMaxYarnPnp: hasMaxYarnPnp,
yarnVersion: yarnVersion,
};
}
function checkNodeVersion(packageName) {
const packageJsonPath = path.resolve(
process.cwd(),
"node_modules",
packageName,
"package.json"
);
if (!fs.existsSync(packageJsonPath)) {
return;
}
const packageJson = require(packageJsonPath);
if (!packageJson.engines || !packageJson.engines.node) {
return;
}
if (!semver.satisfies(process.version, packageJson.engines.node)) {
console.error(
chalk.red(
"You are running Node %s.\n" +
"Create IPFS App requires Node %s or higher. \n" +
"Please update your version of Node."
),
process.version,
packageJson.engines.node
);
process.exit(1);
}
}
function checkAppName(appName) {
const validationResult = validateProjectName(appName);
if (!validationResult.validForNewPackages) {
console.error(
chalk.red(
`Cannot create a project named ${chalk.green(
`"${appName}"`
)} because of npm naming restrictions:\n`
)
);
[
...(validationResult.errors || []),
...(validationResult.warnings || []),
].forEach((error) => {
console.error(chalk.red(` * ${error}`));
});
console.error(chalk.red("\nPlease choose a different project name."));
process.exit(1);
}
// TODO: there should be a single place that holds the dependencies
const dependencies = [
"react",
"react-dom",
"react-scripts",
"ipfs-scripts",
].sort();
if (dependencies.includes(appName)) {
console.error(
chalk.red(
`Cannot create a project named ${chalk.green(
`"${appName}"`
)} because a dependency with the same name exists.\n` +
`Due to the way npm works, the following names are not allowed:\n\n`
) +
chalk.cyan(dependencies.map((depName) => ` ${depName}`).join("\n")) +
chalk.red("\n\nPlease choose a different project name.")
);
process.exit(1);
}
}
function makeCaretRange(dependencies, name) {
const version = dependencies[name];
if (typeof version === "undefined") {
console.error(chalk.red(`Missing ${name} dependency in package.json`));
process.exit(1);
}
let patchedVersion = `^${version}`;
if (!semver.validRange(patchedVersion)) {
console.error(
`Unable to patch ${name} dependency version because version ${chalk.red(
version
)} will become invalid ${chalk.red(patchedVersion)}`
);
patchedVersion = version;
}
dependencies[name] = patchedVersion;
}
function setCaretRangeForRuntimeDeps(packageName) {
const packagePath = path.join(process.cwd(), "package.json");
const packageJson = require(packagePath);
if (typeof packageJson.dependencies === "undefined") {
console.error(chalk.red("Missing dependencies in package.json"));
process.exit(1);
}
const packageVersion = packageJson.dependencies[packageName];
if (typeof packageVersion === "undefined") {
console.error(chalk.red(`Unable to find ${packageName} in package.json`));
process.exit(1);
}
makeCaretRange(packageJson.dependencies, "react");
makeCaretRange(packageJson.dependencies, "react-dom");
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + os.EOL);
}
// If project only contains files generated by GH, it’s safe.
// Also, if project contains remnant error logs from a previous
// installation, lets remove them now.
// We also special case IJ-based products .idea because it integrates with CRA:
// https://github.com/facebook/create-react-app/pull/368#issuecomment-243446094
function isSafeToCreateProjectIn(root, name) {
const validFiles = [
".DS_Store",
".git",
".gitattributes",
".gitignore",
".gitlab-ci.yml",
".hg",
".hgcheck",
".hgignore",
".idea",
".npmignore",
".travis.yml",
"docs",
"LICENSE",
"README.md",
"mkdocs.yml",
"Thumbs.db",
];
// These files should be allowed to remain on a failed install, but then
// silently removed during the next create.
const errorLogFilePatterns = [
"npm-debug.log",
"yarn-error.log",
"yarn-debug.log",
];
const isErrorLog = (file) => {
return errorLogFilePatterns.some((pattern) => file.startsWith(pattern));
};
const conflicts = fs
.readdirSync(root)
.filter((file) => !validFiles.includes(file))
// IntelliJ IDEA creates module files before CRA is launched
.filter((file) => !/\.iml$/.test(file))
// Don't treat log files from previous installation as conflicts
.filter((file) => !isErrorLog(file));
if (conflicts.length > 0) {
console.log(
`The directory ${chalk.green(name)} contains files that could conflict:`
);
console.log();
for (const file of conflicts) {
try {
const stats = fs.lstatSync(path.join(root, file));
if (stats.isDirectory()) {
console.log(` ${chalk.blue(`${file}/`)}`);
} else {
console.log(` ${file}`);
}
} catch (e) {
console.log(` ${file}`);
}
}
console.log();
console.log(
"Either try using a new directory name, or remove the files listed above."
);
return false;
}
// Remove any log files from a previous installation.
fs.readdirSync(root).forEach((file) => {
if (isErrorLog(file)) {
fs.removeSync(path.join(root, file));
}
});
return true;
}
function getProxy() {
if (process.env.https_proxy) {
return process.env.https_proxy;
} else {
try {
// Trying to read https-proxy from .npmrc
let httpsProxy = execSync("npm config get https-proxy").toString().trim();
return httpsProxy !== "null" ? httpsProxy : undefined;
} catch (e) {
return;
}
}
}
// See https://github.com/facebook/create-react-app/pull/3355
function checkThatNpmCanReadCwd() {
const cwd = process.cwd();
let childOutput = null;
try {
// Note: intentionally using spawn over exec since
// the problem doesn't reproduce otherwise.
// `npm config list` is the only reliable way I could find
// to reproduce the wrong path. Just printing process.cwd()
// in a Node process was not enough.
childOutput = spawn.sync("npm", ["config", "list"]).output.join("");
} catch (err) {
// Something went wrong spawning node.
// Not great, but it means we can't do this check.
// We might fail later on, but let's continue.
return true;
}
if (typeof childOutput !== "string") {
return true;
}
const lines = childOutput.split("\n");
// `npm config list` output includes the following line:
// "; cwd = C:\path\to\current\dir" (unquoted)
// I couldn't find an easier way to get it.
const prefix = "; cwd = ";
const line = lines.find((line) => line.startsWith(prefix));
if (typeof line !== "string") {
// Fail gracefully. They could remove it.
return true;
}
const npmCWD = line.substring(prefix.length);
if (npmCWD === cwd) {
return true;
}
console.error(
chalk.red(
`Could not start an npm process in the right directory.\n\n` +
`The current directory is: ${chalk.bold(cwd)}\n` +
`However, a newly started npm process runs in: ${chalk.bold(
npmCWD
)}\n\n` +
`This is probably caused by a misconfigured system terminal shell.`
)
);
if (process.platform === "win32") {
console.error(
chalk.red(`On Windows, this can usually be fixed by running:\n\n`) +
` ${chalk.cyan(
"reg"
)} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
` ${chalk.cyan(
"reg"
)} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` +
chalk.red(`Try to run the above two lines in the terminal.\n`) +
chalk.red(
`To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/`
)
);
}
return false;
}
function checkIfOnline(useYarn) {
if (!useYarn) {
// Don't ping the Yarn registry.
// We'll just assume the best case.
return Promise.resolve(true);
}
return new Promise((resolve) => {
dns.lookup("registry.yarnpkg.com", (err) => {
let proxy;
if (err != null && (proxy = getProxy())) {
// If a proxy is defined, we likely can't resolve external hostnames.
// Try to resolve the proxy name as an indication of a connection.
dns.lookup(url.parse(proxy).hostname, (proxyErr) => {
resolve(proxyErr == null);
});
} else {
resolve(err == null);
}
});
});
}
function executeNodeScript({ cwd, args }, data, source) {
return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[...args, "-e", source, "--", JSON.stringify(data)],
{ cwd, stdio: "ignore" }
);
child.on("close", (code) => {
if (code !== 0) {
reject({
command: `node ${args.join(" ")}`,
});
return;
}
resolve();
});
});
}
function checkForLatestVersion() {
return new Promise((resolve, reject) => {
https
.get(
"https://registry.npmjs.org/-/package/create-ipfs-app/dist-tags",
(res) => {
if (res.statusCode === 200) {
let body = "";
res.on("data", (data) => (body += data));
res.on("end", () => {
resolve(JSON.parse(body).latest);
});
} else {
reject();
}
}
)
.on("error", () => {
reject();
});
});
}
function logo() {
console.log(
gradient.rainbow.multiline(
` _ _ __ `
)
);
console.log(
gradient.rainbow.multiline(
` ___ _ __ ___ __ _| |_ ___ (_)_ __ / _|___ __ _ _ __ _ __ `
)
);
console.log(
gradient.rainbow.multiline(
" / __| '__/ _ \\/ _` | __/ _ \\___| | '_ \\| |_/ __|___ / _` | '_ \\| '_ \\ "
)
);
console.log(
gradient.rainbow.multiline(
`| (__| | | __/ (_| | || __/___| | |_) | _\\__ \\___| (_| | |_) | |_) |`
)
);
console.log(
gradient.rainbow.multiline(
` \\___|_| \\___|\\__,_|\\__\\___| |_| .__/|_| |___/ \\__,_| .__/| .__/`
)
);
console.log(
gradient.rainbow.multiline(
` |_| |_| |_| `
)
);
}
module.exports = {
init,
getTemplateInstallPackage,
};