UNPKG

create-surgio-store

Version:
469 lines (429 loc) 12.9 kB
'use strict' const chalk = require('chalk') const commander = require('commander') const path = require('path') const fs = require('fs-extra') const os = require('os') const spawn = require('cross-spawn') const inquirer = require('inquirer') const Handlebars = require('handlebars') const Promise = require('bluebird') const { join, resolve } = path const packageJson = require('./package.json') const errorLogFilePatterns = [ 'npm-error.log', 'npm-debug.log', 'yarn-error.log', 'yarn-debug.log', ] let projectName const program = new commander.Command(packageJson.name) .version(packageJson.version) .arguments('<project-directory>') .usage(`${chalk.green('<project-directory>')} [options]`) .action((name) => { projectName = name }) .option('--use-cnpm', '使用国内镜像安装依赖') .option('--verbose', '打印调试日志') .allowUnknownOption() .parse(process.argv) if (typeof projectName === 'undefined') { console.error('请指定一个目录保存项目:') console.log( ` ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}` ) console.log() console.log('例如:') console.log( ` ${chalk.cyan(program.name())} ${chalk.green('my-surge-rules')}` ) console.log() console.log(`运行 ${chalk.cyan(`${program.name()} --help`)} 查看详细指引`) process.exit(1) } createFn(projectName, program.verbose, program.useCnpm).catch((error) => { console.error('发生了错误') console.log() console.error(error) process.exit(1) }) async function createFn(name, verbose, useCnpm) { const root = resolve(name) const appName = path.basename(root) fs.ensureDirSync(name) if (!isSafeToCreateProjectIn(root, name)) { process.exit(1) } console.log(`创建目录中,地址:${chalk.green(root)}.`) console.log() const packageJson = { name: appName, version: '0.1.0', private: true, scripts: { update: 'surgio generate', start: 'node server.js', lint: 'eslint . --ext .js', format: 'prettier --write .', }, engine: { node: '>=18.0.0', }, } const allDependencies = ['surgio@^3'] const useAliyunOss = process.stdout.isTTY ? await inquirer.prompt({ type: 'confirm', name: 'useAliyunOss', message: '是否配置将配置文件上传至阿里云 OSS?(默认:是)', default: true, }) : true const useGateway = process.stdout.isTTY ? await inquirer.prompt({ type: 'confirm', name: 'useGateway', message: '是否配置使用网关?(默认:是)', default: true, }) : true const allowAnalytics = process.stdout.isTTY ? await inquirer.prompt({ type: 'confirm', name: 'allowAnalytics', message: '是否允许我收集非常有限的报错信息用于排查问题?(默认:是)', default: true, }) : true if (useAliyunOss) { packageJson.scripts.update = 'surgio generate && surgio upload' } if (allowAnalytics) { console.log('若对信息收集感到不适,可以稍后在 surgio.conf.js 中关闭') } if (useGateway) { allDependencies.push('@surgio/gateway@^2') } // VSCode settings await fs.mkdirp(join(root, '.vscode')) await fs.writeFile( join(root, '.vscode/extensions.json'), JSON.stringify( { extensions: [ 'dbaeumer.vscode-eslint', 'eseom.nunjucks-template', 'esbenp.prettier-vscode', ], }, null, 2 ), { encoding: 'utf8', } ) await fs.writeFile( join(root, '.vscode/settings.json'), JSON.stringify( { 'files.associations': { '*.tpl': 'nunjucks', }, 'editor.formatOnSave': true, '[javascript]': { 'editor.formatter': 'esbenp.prettier-vscode', }, }, null, 2 ), { encoding: 'utf8', } ) await fs.writeFile( join(root, 'package.json'), JSON.stringify(packageJson, null, 2) + os.EOL ) if (useCnpm) { await fs.writeFile( join(root, '.npmrc'), `registry=https://registry.npmmirror.com/` + os.EOL ) // eslint-disable-next-line require-atomic-updates process.env.FSEVENTS_BINARY_HOST_MIRROR = 'https://npmmirror.com/mirrors/fsevents' } process.chdir(root) if (!checkThatNpmCanReadCwd()) { process.exit(1) } console.log('正在安装依赖,可能需要一点时间。安装过程中请不要关闭') await install(root, allDependencies, verbose, useCnpm).catch((reason) => { console.log() console.log('终止安装') if (reason.command) { console.log(` ${chalk.cyan(reason.command)} 命令运行失败`) } else { console.log(chalk.red('发生未知错误')) console.log(reason) } console.log() // On 'exit' we will delete these files from target directory. const knownGeneratedFiles = [ 'package.json', 'package-lock.json', 'node_modules', ] const currentFiles = fs.readdirSync(join(root)) currentFiles.forEach((file) => { knownGeneratedFiles.forEach((fileToMatch) => { // This removes all knownGeneratedFiles. if (file === fileToMatch) { console.log(`准备删除已生成的文件... ${chalk.cyan(file)}`) fs.removeSync(join(root, file)) } }) }) const remainingFiles = fs.readdirSync(join(root)) if (!remainingFiles.length) { // Delete target folder if empty console.log( `删除位于 ${chalk.cyan(resolve(root, '..'))}${chalk.cyan( `${appName}/` )}` ) process.chdir(resolve(root, '..')) fs.removeSync(join(root)) } console.log('已完成') process.exit(1) }) await renderTemplates(root, { useAliyunOss, allowAnalytics, useGateway, }) await copyFolders(root) console.log('大功告成!') console.log() console.log('你可以在目录中执行命令:') console.log() console.log(` ${chalk.cyan('npm run update')}`) console.log(` 更新所有配置文件,上传至阿里云 OSS(如果已开启该功能)`) console.log() console.log(` ${chalk.cyan('npx surgio generate')}`) console.log(` 生成新的配置文件`) console.log() console.log(` ${chalk.cyan('npx surgio upload')}`) console.log(` 上传所有配置文件`) console.log() console.log('目录中已包含一些用于演示的配置,快去试试吧!') console.log() console.log(` ${chalk.cyan('cd')} ${appName}`) console.log(` ${chalk.cyan('npm run update')}`) console.log() console.log(`使用文档: ${chalk.green('https://surgio.js.org/')}`) console.log(`交流群: ${chalk.green('https://t.me/surgiotg')}`) console.log() } function isSafeToCreateProjectIn(root, name) { const validFiles = [ '.DS_Store', 'Thumbs.db', '.git', '.gitignore', '.idea', 'README.md', 'LICENSE', '.hg', '.hgignore', '.hgcheck', '.npmignore', 'mkdocs.yml', 'docs', '.travis.yml', '.gitlab-ci.yml', '.gitattributes', ] console.log() 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) => !errorLogFilePatterns.some((pattern) => file.indexOf(pattern) === 0) ) if (conflicts.length > 0) { console.log(`${chalk.green(name)} 目录中包含冲突的文件:`) console.log() for (const file of conflicts) { console.log(` ${file}`) } console.log() console.log('尝试使用一个新的目录名,或者将上述文件(夹)删除后重试') return false } // Remove any remnant files from a previous installation const currentFiles = fs.readdirSync(join(root)) currentFiles.forEach((file) => { errorLogFilePatterns.forEach((errorLogFilePattern) => { // This will catch `(npm-debug|yarn-error|yarn-debug).log*` files if (file.indexOf(errorLogFilePattern) === 0) { fs.removeSync(join(root, file)) } }) }) return true } 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.indexOf(prefix) === 0) 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 install(root, dependencies, verbose) { return new Promise((resolve, reject) => { let command let args command = 'npm' args = ['install', '--save', '--save-exact', '--loglevel', 'error'].concat( dependencies ) 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() }) }) } async function renderTemplates( root, { useAliyunOss, allowAnalytics, useGateway } ) { const confTpl = Handlebars.compile( await fs.readFile(join(__dirname, 'template/surgio.conf.js.hbs'), { encoding: 'utf8', }) ) const gitignoreTpl = Handlebars.compile( await fs.readFile(join(__dirname, 'template/gitignore.hbs'), { encoding: 'utf8', }) ) const eslintrcTpl = Handlebars.compile( await fs.readFile(join(__dirname, 'template/eslintrc.js.hbs'), { encoding: 'utf8', }) ) const prettierrcTpl = Handlebars.compile( await fs.readFile(join(__dirname, 'template/prettierrc.js.hbs'), { encoding: 'utf8', }) ) const serverTpl = Handlebars.compile( await fs.readFile(join(__dirname, 'template/server.js.hbs'), { encoding: 'utf8', }) ) const paths = { conf: join(root, 'surgio.conf.js'), gitignore: join(root, '.gitignore'), eslintrc: join(root, '.eslintrc.js'), prettierrc: join(root, '.prettierrc.js'), server: join(root, 'server.js'), } await fs.writeFile( paths.conf, confTpl({ useAliyunOss, allowAnalytics, useGateway, }) + os.EOL ) await fs.writeFile(paths.gitignore, gitignoreTpl({}) + os.EOL) await fs.writeFile(paths.eslintrc, eslintrcTpl({}) + os.EOL) await fs.writeFile(paths.prettierrc, prettierrcTpl({}) + os.EOL) if (useGateway) { await fs.writeFile(paths.server, serverTpl({}) + os.EOL) } console.log(`配置已生成至 ${chalk.green(paths.conf)},请将配置补全`) if (useAliyunOss) { console.log('阿里云 OSS 配置可以在管理面板中找到') } console.log() } async function copyFolders(root) { const folders = ['provider', 'template'] await Promise.each(folders, async (item) => { const source = join(__dirname, `template/${item}`) await fs.copy(source, join(root, item)) }) }