UNPKG

@iyowei/create-esm

Version:

这是一个命令行工具,给到 ESM 源码文件路径后,自动创建一个项目、打包、发布,确保一个指令即可在项目中安装使用,进而支持跨项目使用。

439 lines (382 loc) 12.2 kB
/** * TODO: 将所有的文案、消息等都迁移到 messages.js 文件中 * TODO: 参数构造过程模块化 */ import { basename, extname, join } from 'path'; import { statSync, existsSync, readFileSync } from 'fs'; import { log } from 'console'; import notEmptyString from '@iyowei/not-empty-string'; import alphaSort from 'alpha-sort'; import isScoped from 'is-scoped'; import shell from 'shelljs'; import chalk from 'chalk'; // eslint-disable-line import prompts from 'prompts'; import isReadmePath from '@iyowei/is-readme-path'; import precinct from 'detective-es6'; import { isESMSync } from '@iyowei/is-esm'; import isEmpty from 'lodash/isEmpty.js'; import { prints, copiers, stockrooms } from '@iyowei/create-templates'; import jsModuleDependenciesToBeInstalled from '@iyowei/js-module-dependencies-to-be-installed'; import { HINT_NO_FILE_INPUT, hints } from '../messages.js'; import { getGlobalConfigurations } from './global.js'; import terminateCli from '../terminateCli.js'; import confirmedOptions from './options.js'; import questions from './questions.js'; import { rules as argsRules, ARG_NAME, ARG_OUTPUT, ARG_DEPENDENCIES, ARG_GITHUB_ORG, ARG_TDD, ARG_BENCHMARK, ARG_BREAKPOINT, } from './args.js'; const questioners = []; const ext = ['.js', '.mjs']; function hasReadme(paths = []) { return paths.some(isReadmePath); } function getJSTypeFileInputs(inputs) { return inputs.filter( (cur) => cur.dirent.isFile() && ext.includes(extname(cur.path)), ); } function treatDependencies(cli) { // 解析 JS 文件中的需要安装依赖 const scaned = Array.from( new Set( cli.input .filter((cur) => ext.includes(extname(cur))) .map((cur) => { const specifiers = precinct(readFileSync(cur, 'utf-8')); return jsModuleDependenciesToBeInstalled(specifiers); }) .flat(), ), ); if (!isEmpty(scaned)) { confirmedOptions.set(ARG_DEPENDENCIES, scaned); } // 即使命令行中已指定,但仍需要二次确认的参数,除非要求,否则交互式提问用户 if (!isEmpty(cli.flags[ARG_DEPENDENCIES])) { confirmedOptions.set( ARG_DEPENDENCIES, Array.from(new Set([...scaned, ...cli.flags[ARG_DEPENDENCIES]])), ); } if (cli.flags.doubleCheckDependencies) { questioners.push({ type: 'list', separator: ' ', initial: '', name: ARG_DEPENDENCIES, message: isEmpty(confirmedOptions.get(ARG_DEPENDENCIES)) ? '未指定需要安装的依赖,如果需要,以空格间隔输入' : `将安装 ${confirmedOptions .get(ARG_DEPENDENCIES) .reduce( (acc, cur) => (!acc ? `"${cur}"` : `${acc}、"${cur}"`), '', )},如果需要指定更多,以空格间隔输入`, }); } } function treatInputs(cli) { if (isEmpty(cli.input)) { terminateCli('请指定文件、文件夹'); } const inputsExist = cli.input.filter((cur) => !existsSync(cur)); // 校验输入文件中是否有不存在的 if (!isEmpty(inputsExist)) { log(` ${chalk.redBright.bold('以下输入的文件不存在,请检查是否输入有误,')} ${inputsExist.reduce( (acc, cur) => (!acc ? `- ${cur}` : `${acc}\n - ${cur}`), '', )} `); shell.exit(1); } // ==========================================================================> const inputs = cli.input.map((cur) => ({ path: cur, name: basename(cur), dirent: statSync(cur), })); if (isEmpty(getJSTypeFileInputs(inputs))) { terminateCli(hints[HINT_NO_FILE_INPUT]); } // ==========================================================================> const existNonESMFile = cli.input .filter((cur) => ext.includes(extname(cur))) .some((cur) => !isESMSync(cur)); if (existNonESMFile) { terminateCli('仅适用 ESM 模块文件'); } // ==========================================================================> if (!isEmpty(cli.flags[ARG_GITHUB_ORG]) && cli.flags.personal) { terminateCli('`--personal`、`--github-org` 两个参数不能同时出现'); } // ==========================================================================> if (!hasReadme(cli.input)) { confirmedOptions.set('generateReadme', true); } // ==========================================================================> const rootFiles = inputs.filter((cur) => cur.dirent.isFile()); // 输入文件集合中存在非 JS 文件时,让用户选择发包时需要包含的文件 if (rootFiles.some((cur) => !ext.includes(extname(cur.name)))) { questioners.push({ type: 'multiselect', name: 'pkgFiles', message: '选择发包时需要包含的文件', choices: inputs.map((cur) => ({ title: cur.name, value: cur.dirent.isDirectory() ? `${basename(cur.path)}/**` : `${basename(cur.path)}`, })), instructions: false, }); } else { confirmedOptions.set( 'pkgFiles', inputs.map((cur) => cur.dirent.isDirectory() ? `${basename(cur.path)}/**` : `${basename(cur.path)}`, ), ); } confirmedOptions.set('targets', inputs); const jsFileTypeInputs = getJSTypeFileInputs(inputs); if (jsFileTypeInputs.length === 1) { const jsInput = jsFileTypeInputs[0]; confirmedOptions.set( 'pkgExports', Object.assign(jsInput, { relativePath: `./${jsInput.name}`, bareRelativePath: jsInput.name, }), ); } else { // 有多份文件,此时需要用户指定将哪份文件作为导出文件 questioners.push({ name: 'pkgExports', type: 'select', choices: jsFileTypeInputs.reduce((acc, cur) => { const { name, path, dirent } = cur; acc.push({ title: name, value: { path, relativePath: `./${name}`, bareRelativePath: name, dirent, name, }, }); return acc; }, []), message: '选择 NPM 包导出文件', instructions: false, }); } } // 部分 ”交互式提问“ 自动根据某些参数是否提供、是否有默认值等特征出现或隐藏 function treatArgsWithQuestionIfNotGiven(cli) { const defaults = getGlobalConfigurations(); Object.entries(argsRules).forEach((kv) => { const arg = kv[0]; const { cliRequired, isDefault } = kv[1]; // 必需参数,使用命令行中提供的配置,未提供的情况下交互式提问用户 if (cliRequired) { if (!cli.flags[arg]) { questioners.push(questions[arg]); } else { confirmedOptions.set( arg, !argsRules[arg].format ? cli.flags[arg] : argsRules[arg].format(cli.flags[arg]), ); } } // 非必需参数,优先使用命令行中提供的配置,其次是系统默认配置,如果未设置系统默认配置,就交互式提问用户 if (!cliRequired) { if (cli.flags[arg]) { confirmedOptions.set( arg, !argsRules[arg].format ? cli.flags[arg] : argsRules[arg].format(cli.flags[arg]), ); } else if (isDefault && !defaults[arg]) { questioners.push(questions[arg]); } else { confirmedOptions.set(arg, defaults[arg]); } } }); } export default async function make(cli) { treatInputs(cli); treatArgsWithQuestionIfNotGiven(cli); treatDependencies(cli); // 有问题就显示交互式界面提问用户,questioners 更新相关的参数处理必须在前面 if (!isEmpty(questioners)) { const rslt = await prompts(questioners, { onCancel: () => { shell.exit(1); }, }); Object.entries(rslt).forEach((cur) => { const k = cur[0]; const v = cur[1]; confirmedOptions.set(k, v); }); } [ [ ARG_NAME, isScoped(confirmedOptions.get(ARG_NAME)) ? confirmedOptions.get(ARG_NAME).split('/')[1] : confirmedOptions.get(ARG_NAME), ], ['pkgName', confirmedOptions.get(ARG_NAME)], [ 'newProjectPath', isScoped(confirmedOptions.get(ARG_NAME)) ? join( confirmedOptions.get(ARG_OUTPUT), confirmedOptions.get(ARG_NAME).split('/')[1], ) : join( confirmedOptions.get(ARG_OUTPUT), confirmedOptions.get(ARG_NAME), ), ], [ ARG_DEPENDENCIES, confirmedOptions .get(ARG_DEPENDENCIES) .filter((cur) => notEmptyString(cur)) .sort(alphaSort()), ], [ 'namespace', isScoped(confirmedOptions.get(ARG_NAME)) ? confirmedOptions.get(ARG_NAME).split('/')[0].substring(1) : '', ], ].forEach((cur) => { const k = cur[0]; const v = cur[1]; confirmedOptions.set(k, v); }); /** * 包名 * * 检测到命名空间 * * `--github-org` 有值否 * - 有,在不同名的 Github Org 下创建项目, * - 空,到底是在同名 Github Org 下创建还是创建到个人名下,取决于是否选择了个人名下, * - true,创建到个人名下 * - false,创建到同名 Github Org 下 * * 未检测到命名空间, * * `--github-org` 有值否 * - 有,在特定 Github Org 下创建项目, * - 空,创建到个人名下 * * `--github-org` 为不同名 Github Org 存在 * `--personal` 因为 `--github-org` 可能为空存在 * * 两者不能同时出现 */ if (isEmpty(confirmedOptions.get('namespace'))) { // no namespace if (!isEmpty(cli.flags[ARG_GITHUB_ORG])) { // has github org confirmedOptions.set(ARG_GITHUB_ORG, cli.flags[ARG_GITHUB_ORG]); } } else if (!isEmpty(cli.flags[ARG_GITHUB_ORG])) { // has namespace, has github org confirmedOptions.set(ARG_GITHUB_ORG, cli.flags[ARG_GITHUB_ORG]); } else if (!cli.flags.personal) { // has namespace, no github org, not personal confirmedOptions.set('githubOrgNameSameWithNpmOrg', true); } if (cli.flags[ARG_TDD]) { confirmedOptions.set( 'devDependencies', confirmedOptions.get('devDependencies').concat(['mocha']), ); confirmedOptions.set(ARG_TDD, cli.flags[ARG_TDD]); } if (cli.flags[ARG_BENCHMARK]) { confirmedOptions.set( 'devDependencies', confirmedOptions .get('devDependencies') .concat(['benchmark', 'microtime']), ); confirmedOptions.set(ARG_BENCHMARK, cli.flags[ARG_BENCHMARK]); } confirmedOptions.set( 'copiers', [ ...confirmedOptions .get('targets') .map((cur) => cur.path) .map((cur) => ({ source: cur, output: join(confirmedOptions.get('newProjectPath'), basename(cur)), })), ...copiers.common.map((cur) => ({ source: cur, output: join(confirmedOptions.get('newProjectPath'), basename(cur)), })), ...copiers.esm.map((cur) => ({ source: cur, output: join(confirmedOptions.get('newProjectPath'), basename(cur)), })), cli.flags[ARG_TDD] && { source: copiers.mocha, output: join( confirmedOptions.get('newProjectPath'), basename(copiers.mocha), ), }, cli.flags[ARG_BENCHMARK] && { source: copiers[ARG_BENCHMARK], output: join( confirmedOptions.get('newProjectPath'), basename(copiers[ARG_BENCHMARK]), ), }, ].filter(Boolean), ); confirmedOptions.set( 'prints', Object.entries(prints).reduce((acc, cur) => { const k = cur[0]; const v = cur[1]; acc[k] = { source: v, output: join(confirmedOptions.get('newProjectPath'), basename(v)), }; return acc; }, {}), ); confirmedOptions.set('gitignore', { source: stockrooms.gitignore, output: join(confirmedOptions.get('newProjectPath'), '.gitignore'), }); if (cli.flags[ARG_BREAKPOINT]) { confirmedOptions.set(ARG_BREAKPOINT, cli.flags[ARG_BREAKPOINT]); } return confirmedOptions.getAll(); }