【umi】03 跟着create umi 学创建cli

427 阅读2分钟

pnpm dlx create-umi@latest 是在做什么

由于好奇 umi 运行这行命令的时候就行是在做什么,于是扒了源码看下

pnpm dlx create-umi@latest

其实就是一个cli的创建过程, 详create-umi,我们可以看看package.json就知道,使用father打包, bin文件入口bin/create-umi.js

  "bin": {
    "create-umi": "bin/create-umi.js"
  },
  "scripts": {
    "build": "umi-scripts father build",
    "build:deps": "umi-scripts bundleDeps",
    "dev": "umi-scripts father dev",
    "test": "umi-scripts jest-turbo"
  },
 

看看bin/create-umi.js里面是啥

// bin/create-umi.js
#!/usr/bin/env node

process.env.FS_LOGGER = 'none';
require('../dist/cli');

依赖于dist的cli文件

import { chalk, isLocalDev, yParser } from '@umijs/utils';

const args = yParser(process.argv.slice(2), {
  alias: {
    version: ['v'],
    help: ['h'],
  },
  boolean: ['version'],
});

if (args.version && !args._[0]) {
  args._[0] = 'version';
  const local = isLocalDev() ? chalk.cyan('@local') : '';
  const { name, version } = require('../package.json');
  console.log(`${name}@${version}${local}`);
} else {
  require('./')
    .default({
      cwd: process.cwd(),
      args,
    })
    .catch((err: Error) => {
      console.error(`Create failed, ${err.message}`);
      console.error(err);
    });
}

然后找到 index.ts的入口,这里做的事情就是:

1、 根据@clack/prompts交互式命令获取用户选择的参数

import {
  BaseGenerator,
  chalk,
  clackPrompts,
  execa,
  fsExtra,
  getGitInfo,
  installWithNpmClient,
  logger,
  pkgUp,
  tryPaths,
  yParser,
} from '@umijs/utils';
import { existsSync } from 'fs';
import { dirname, join } from 'path';

  const internalTemplatePrompts = async () => {
    intro(chalk.bgHex('#19BDD2')(' create-umi '));

    await selectAppTemplate();
    if (isCancel(appTemplate)) {
      exitPrompt();
    }

    await selectNpmClient();
    if (isCancel(npmClient)) {
      exitPrompt();
    }

    await selectRegistry();
    if (isCancel(registry)) {
      exitPrompt();
    }
    outro(chalk.green(`You're all set!`));
  };

2、 然后根据这些参数去生成模板



 const injectInternalTemplateFiles = async () => {
    // BaseGenerator 这个类就是拷贝模板文件
    const generator = new BaseGenerator({
      path: join(__dirname, '..', 'templates', appTemplate),
      target,
      slient: true,
      data: useDefaultData
        ? defaultData
        : ({
            version: version.includes('-canary.') ? version : `^${version}`,
            npmClient,
            registry,
            author,
            email,
            withHusky,
            extraNpmrc: isPnpm ? pnpmExtraNpmrc : '',
            pluginName,
          } satisfies ITemplateParams),
    });
    await generator.run();
  };

这里有几个点,

3、注意点

3.1、umi-scripts是在做什么

详细可以见umi-scripts, 其目的就是在node环境中可以执行ts 或者esm

image.png

3.2、为什么chalk@clack/prompts等要从@umi/utils引入,

答案在上篇【umi】02 如何在cjs 环境用esm包的回答,主要就是借助了umi-scripts bundleDeps命令将对应的node_modulesncc进行了cjs的转化

手写一个cli

自己也仿照umi,动手写了一个,功能在就是在指定的文件目录创建文件模板,因为在日常工作中,我们新建一个文件,总是复制,然后改成自己的名字,觉得很麻烦

1、在package.json 写入

  "bin": {
    "create-tpl": "bin/create-tpl.js"
  },

2、bin文件

// bin/create-tpl.js
#!/usr/bin/env node

// 表示禁用文件系统的日志记录
process.env.FS_LOGGER = 'none';
require('../dist/cjs/cli');

3、入口文件

// index.ts
import { writeFileSync, mkdirSync } from 'fs';
import { join } from 'path';
import { renderTep } from './template';

enum ETemplate {
  compoent = 'component',
  page = 'page',
}

const DEFAULT_DATA = {
  appTemplate: ETemplate.page,
};

interface IGeneratorOpts {
  cwd: string;
  args: any;
  defaultData?: {
    appTemplate?: ETemplate;
  };
}

export default async ({
  cwd,
  args,
  defaultData = DEFAULT_DATA,
}: IGeneratorOpts) => {
  const chalkModule = await import('chalk'); // 由于不想从@umi/utils 引入 chalk ,所以想用动态导入的方式
  const clackPromptsModule = await import('@clack/prompts');
  const chalk = chalkModule.default;
  const { select, text, isCancel, outro } = clackPromptsModule;

  let appTemplate = defaultData?.appTemplate || ETemplate.page;
  let fileName = '';
  let [name] = args._;
  let basePath = name ? join(cwd, name) : join(cwd, 'src');

  const exitPrompt = () => {
    outro(chalk.red('Exit!'));
    process.exit(1);
  };

  const selectAppTemplate = async () => {
    appTemplate = (await select({
      message: 'Pick Template Name',
      options: [
        { label: 'Component', value: ETemplate.compoent },
        {
          label: 'Page',
          value: ETemplate.page,
        },
      ],
    })) as ETemplate;
  };

  const inputFileName = async () => {
    fileName = (await text({
      message: 'Input File Name',
      validate: (value) => {
        if (!value.match(/[a-z]+[A-Z]{1}[a-z]+/g)) {
          return 'Please Input Camel Case Name!';
        }
      },
    })) as ETemplate;
  };

  const internalTemplatePrompts = async () => {
    await selectAppTemplate();

    if (isCancel(appTemplate)) {
      exitPrompt();
    }

    await inputFileName();
    if (isCancel(appTemplate)) {
      exitPrompt();
    }

    const isPage = appTemplate === ETemplate.page;
    const { LessTep, TsxTep, TsTep } = renderTep(fileName, isPage);
    const styleFileName = isPage ? 'style.module.less' : 'style.less';
    const path = isPage ? join(basePath, 'pages') : join(basePath, 'compoents');

    mkdirSync(`${path}/${fileName}`); // 新建文件夹
    process.chdir(`${path}/${fileName}`); // 更改目录

    writeFileSync(styleFileName, LessTep);
    writeFileSync(`${fileName}.tsx`, TsxTep);
    writeFileSync(`index.ts`, TsTep);
  };

  await internalTemplatePrompts();
};

4、联调测试

  • pnpm link <dir> 将 中的软件包链接到执行命令的软件包的 node_modules。
  • pnpm link --dir <dir> 将当前工作目录中的软件包链接到 。
  • pnpm link --global 将当前目录的软件包链接到全局
  • pnpm link --global <pkg> 链接全局指定的pkg
  • 或者在package.json写入
  "devDependencies": {
  "create-tpl": "workspace:*"
  }

然后pnpm i皆可,就不用pnpm link了

举例

如果foo 项目是公用的项目

cd ~/projects/foo  
pnpm install # install dependencies of foo  
cd ~/projects/my-project  
pnpm link ~/projects/foo # link foo to my-project
cd ~/projects/foo  
pnpm install # install dependencies of foo  
cd ~/projects/my-project  
pnpm link ~/projects/foo # link foo to my-project

5、效果

image.png

image.png

完整代码

6、问题

由于我不是很想从@umi/utils里面导入chalk@clack/prompts等,

image.png 于是我在代码里面采用动态导入了,但是在执行father doctor && npm run build 后,却包这个错误

image.png 但是换成从@umi/utils导入就可以了

image.png

明明对应的声明文件都有,可是偏偏不行, 所以father doctor是在做什么

7、 其他注意点

用esm的方式写cli,有很多包不想在cjs直接引入,比如pathfs等, 安装"@types/node"即可