UNPKG

create-ipfs-app

Version:

Create IPFS apps with no build configuration.

1,254 lines (1,169 loc) 37.9 kB
"use strict"; 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, };