create-vite 源码阅读


highlight: atom-one-dark
theme: scrolls-light

我正在参与掘金会员专属活动-源码共读第一期,点击参与
今天我们来看看vite的脚手架。

前言

create-vite 主要是用于创建一个项目并根据用户选择配置的template将模板文件写入当前创建的目录中。
vite提供了多个模板及其ts版本。

重要插件

使用minimist解析命令行参数
使用prompts包来实现命令行指引配置的功能。
使用kolorist包实现不同颜色的关键词。

来看一下这些包的简单使用,方便后续查看create-vite中的源码

minimist

通过process.argv获取命令行参数的字符串数组,前两个值是固定的,第一个是node程序路径,第二个则是当前执行的文件路径。之后的才是输入的各种参数

const { argv } = process;
console.log('argv', argv);
> node src/index.js  -v vv -f

argv [
  '/usr/local/Cellar/node@16/16.18.1/bin/node',
  '/Users/zgm/Documents/Web/node/node-js/src/index.js',
  '-v',
  'vv',
  '-f'
]

使用minimist后可以把输入的参数进行解析,使用_保存命令中的各种参数,当匹配到-或者--字符时忽略后边的所有的参数。---字符后边的命令会添加到对象中,当命令后边有参数(非options),那么该命令的值就是后边的参数,否则值为true

const argv = minimist(process.argv.slice(2), { string: ['_'] }); // _ 中的属性会被转为字符串类型
console.log(argv);
> node src/index.js  -v vv -f

{ _: [], v: 'vv', f: true }

源码:

const argv = minimist(process.argv.slice(2), { string: ['_'] });
console.log(argv);
> node src/index.js _asdf 123 321  -v vv -f

{ _: [ '_asdf', '123', '321' ], v: 'vv', f: true }

prompts

轻量级,美观且用户友好的交互式提示库。

单个提示的传入一个对象即可

const prompts = require('prompts');

(async () => {
  const response = await prompts({
    type: 'text',
    name: 'weather',
    message: 'What is the weather today?',
  });

  console.log(response);
})();

多个就需要传入数组

const prompts = require('prompts');

(async () => {
  const response = await prompts([
    {
      type: 'text',
      name: 'weather',
      message: 'What is the weather today?',
    },
    {
      type: 'confirm',
      name: 'out',
      message: 'Are you going out for fun now?',
    },
  ]);

  console.log(response);
})();

动态prompts

typenull时可以跳过当前这个prompt

type为一个函数时,有 3 个参数,第一个是上一条prompt的值,第二个是之前所有prompts的键值组成的对象,第三个则是当前prompt对象。

(async () => {
  const response = await prompts([
    {
      type: 'text',
      name: 'weather',
      message: 'What is the weather today?',
    },
    {
      type: 'confirm',
      name: 'out',
      message: 'Are you going out for fun now?',
    },
    {
      type: (pre) => (pre ? 'text' : null),
      name: 'fun',
      message: 'Have fun then',
    },
  ]);

  console.log(response);
})();

kolorist

用于输出不同颜色的字符。

const { yellow, green, cyan, blue } = require('kolorist');

console.log(yellow('yellow'));
console.log(green('green'));
console.log(cyan('cyan'));
console.log(blue('blue'));

image.png

源码解析 create-vite/index.js

源码可以去 clone 若川 大大的相关仓库

配置项目名

获取命令后的第一个参数默认为项目名,当未获取到参数时提示用户进行配置,否则跳过进行下一步。

// 命令行第一个参数,替换反斜杠 / 为空字符串
let targetDir = formatTargetDir(argv._[0]); // 如果字符串最后一个字符是 '/',则去除

// 命令行参数 --template 或者 -t
let template = argv.template || argv.t;

const defaultTargetDir = 'vite-project';
// 获取项目名
const getProjectName = () => (targetDir === '.' ? path.basename(path.resolve()) : targetDir);

交互解析

try {
  result = await prompts(
    [
      // 项目名
      {
        type: targetDir ? null : 'text',
        name: 'projectName',
        message: reset('Project name:'), // reset() 是 kolorist 中的 api
        initial: defaultTargetDir, // 默认为 vite-project
        onState: (state) => {
          targetDir = formatTargetDir(state.value) || defaultTargetDir;
        },
      },
      // 判断是否需要重写目录(是否为空文件夹)
      {
        type: () => (!fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm'),
        name: 'overwrite',
        message: () =>
          (targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`) +
          ` is not empty. Remove existing files and continue?`,
      },
      {
        type: (_, { overwrite } = {}) => {
          // 用户不要重写
          if (overwrite === false) {
            throw new Error(red('✖') + ' Operation cancelled');
          }
          return null;
        },
        name: 'overwriteChecker',
      },
      // 验证项目名
      {
        type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
        name: 'packageName',
        message: reset('Package name:'),
        initial: () => toValidPackageName(getProjectName()),
        validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name',
      },
      // 选择框架模板
      {
        type: template && TEMPLATES.includes(template) ? null : 'select',
        name: 'framework',
        message:
          typeof template === 'string' && !TEMPLATES.includes(template)
            ? reset(`"${template}" isn't a valid template. Please choose from below: `)
            : reset('Select a framework:'),
        initial: 0,
        choices: FRAMEWORKS.map((framework) => {
          const frameworkColor = framework.color;
          return {
            title: frameworkColor(framework.name),
            value: framework,
          };
        }),
      },
      // 选择模版对应的语言如 ts
      {
        type: (framework) => (framework && framework.variants ? 'select' : null),
        name: 'variant',
        message: reset('Select a variant:'),
        // @ts-ignore
        choices: (framework) =>
          framework.variants.map((variant) => {
            const variantColor = variant.color;
            return {
              title: variantColor(variant.name),
              value: variant.name,
            };
          }),
      },
    ],
    {
      onCancel: () => {
        throw new Error(red('✖') + ' Operation cancelled');
      },
    },
  );
} catch (cancelled) {
  console.log(cancelled.message);
  return;
}

删除已有文件夹/新建文件夹

// user choice associated with prompts
const { framework, overwrite, packageName, variant } = result;

// 目录
const root = path.join(cwd, targetDir);

if (overwrite) {
  // 删除文件夹
  emptyDir(root);
} else if (!fs.existsSync(root)) {
  // 新建文件夹
  fs.mkdirSync(root, { recursive: true });
}

// 递归删除文件夹,相当于 rm -rf xxx。
function emptyDir(dir) {
  if (!fs.existsSync(dir)) {
    return;
  }
  for (const file of fs.readdirSync(dir)) {
    fs.rmSync(path.resolve(dir, file), { recursive: true, force: true });
  }
}

写入模板文件

// determine template
template = variant || framework || template;

console.log(`\nScaffolding project in ${root}...`);

// 获取模板对应文件夹
const templateDir = path.resolve(fileURLToPath(import.meta.url), '..', `template-${template}`);

const write = (file, content) => {
  //  这里的 renameFiles,是因为在某些编辑器或者电脑上不支持.gitignore。
  const targetPath = renameFiles[file] ? path.join(root, renameFiles[file]) : path.join(root, file);
  // 如果要针对某个文件做自定义写入
  if (content) {
    fs.writeFileSync(targetPath, content);
  } else {
    copy(path.join(templateDir, file), targetPath);
  }
};

const files = fs.readdirSync(templateDir);
for (const file of files.filter((f) => f !== 'package.json')) {
  write(file);
}

/* package.json 文件单独处理 */
// 将 json 数据转为 js 对象
const pkg = JSON.parse(fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8'));
// 修改项目名为输入的或文件夹名
pkg.name = packageName || getProjectName();
// 将修改后的内容转为 json 格式,写入文件
write('package.json', JSON.stringify(pkg, null, 2));
/* --------- */

/* 打印安装完成后的信息 */
// 获取包管理软件的信息
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
const pkgManager = pkgInfo ? pkgInfo.name : 'npm';

console.log(`\nDone. Now run:\n`);

// 如果命令行不在项目根目录
if (root !== cwd) {
  console.log(`  cd ${path.relative(cwd, root)}`);
}

switch (pkgManager) {
  case 'yarn':
    console.log('  yarn');
    console.log('  yarn dev');
    break;
  default:
    console.log(`  ${pkgManager} install`);
    console.log(`  ${pkgManager} run dev`);
    break;
}
console.log();

延伸函数 copy && copyDir

如果是文件夹用 copyDir 拷贝

function copy(src, dest) {
  const stat = fs.statSync(src);
  if (stat.isDirectory()) {
    copyDir(src, dest);
  } else {
    fs.copyFileSync(src, dest);
  }
}

/**
 * @param {string} srcDir
 * @param {string} destDir
 */
function copyDir(srcDir, destDir) {
  fs.mkdirSync(destDir, { recursive: true });
  for (const file of fs.readdirSync(srcDir)) {
    const srcFile = path.resolve(srcDir, file);
    const destFile = path.resolve(destDir, file);
    copy(srcFile, destFile);
  }
}

知识拓展:resolve 和 join 的区别

resolve 和 join 都用来进行路径片段的连接,但是区别有两点:

  1. resolve 会生成绝对路径,而 join 只是返回当前连接的路径。
  2. resolve 会以最后出现的 ‘/’为起点,作为根路径,忽略前面的片段,而 join 不会。
console.log(path.resolve()) // returns /workspace/demo
console.log(path.resolve('')) // returns /workspace/demo
console.log(path.resolve(\_\_dirname)) // returns /workspace/demo
console.log(path.resolve('/img/books', '/net')) // returns '/net'
console.log(path.resolve('img/books', '/net')) // returns '/net'
console.log(path.resolve('img/books', './net')) // returns '/workspace/demo/img/books/net'
console.log(path.resolve('/img/books', './net')) // returns '/img/books/net'
console.log(path.resolve('/img/books', 'net')) // returns '/img/books/net'
console.log(path.resolve('/img/books', '../net')) // returns '/img/net'
console.log(path.resolve('src','/img/books', '../net')) // returns '/img/net'
console.log(path.resolve('src','./img/books', '../net')) // returns '/workspace/demo/src/img/net'
console.log(path.resolve('src','img/books', '../net')) // returns '/workspace/demo/src/img/net'

path.join('/img', 'book', 'net/abc', 'inter', '..'); // returns /img/book/net/abc
console.log(path.join('/img/books', '../net')) // returns /img/net
console.log(path.join('img/books', '../net')) // returns img/net
console.log(path.join('/img/books', './net')) // returns /img/books/net
console.log(path.join('img/books', './net')) // returns img/books/net
console.log(path.join('/img/books', 'net')) // returns /img/books/net
console.log(path.join('img/books', 'net')) // returns /img/books/net
console.log(path.join('/img/books', '/net')) // returns /img/books/net
console.log(path.join('img/books', '/net')) // returns img/books/net
© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容