【umi】 依赖预打包是怎么一回事

出现的原因是项目的中各种依赖的版本根本不受semver的控制,说到semver的话,回顾下semver规范

semver规范

版本号格式

MAJOR.MINOR.PATCH

  • MAJOR:非常大的改动,可能是是api等完全不兼容

  • MINOR:以向后兼容的方式添加功能时

  • PATCH:以向后兼容的方式修复bug时

另外发布版本的时候,通常会看到带有这些字母的,比如1.0.0-alpha.11.0.0-rc.1,他们的顺序应该是这样的

js 复制代码
Alpha < Beta < RC < Canary < Stable
  • Alpha:Alpha 版本是软件或系统的内部测试版本,仅供内部人员使用。这个阶段的软件通常会有很多 Bug,一般不向外部发布。Alpha 是希腊字母的第一位,表示最初级的版本

  • Beta:Beta 版本是公开测试版,这一版本通常是在 Alpha 版本后推出。相对于 Alpha 版本,Beta 版本已有了很大的改进,消除了严重的错误,但仍可能存在一些缺陷,需要经过多次测试来进一步消除。在这个阶段,软件会一直加入新的功能

  • RC(Release Candidate) :RC 是发行候选版本。与 Beta 版本最大的差别在于,Beta 阶段会一直加入新的功能,但是到了 RC 版本,几乎就不会加入新的功能了,而主要着重于除错。RC 版本是最终发放给用户的最接近正式版的版本,发行后改正 bug 就是正式版了,就是正式版之前的最后一个测试版

  • Canary:即"金丝雀发布"。在软件开发中,Canary 版本通常是指向一小部分用户推出的新版本,用于测试新功能的稳定性和可用性。如果在这一小部分用户中没有发现问题,那么新版本就会推向所有用户。

package.json 中的^和~

~1.2.2 只能安装1.2.x, 更新当前的补丁版本, 1.2.2 <=n<1.3.0

^1.2.2 只能安装1.x.x, 更新当前的次要和补丁版本,1.2.2 <=n<2.0.0

日常工作有时我们会删除 lock文件,然后再pnpm i, 这个时候就会根据规则自动更新, 然而有些依赖是不会遵守这些规则的,

对于这种问题,社区有不少解决方案

长期: 锁依赖

临时

1、patch-package: 代码引入式修复问题依赖

2、 锁定指定依赖版本等

js 复制代码
npm   overrides (>= 8.3.0)
yarn   resolutions
pnpm   overrides/resolutions

而umi采用了依赖预打包

依赖预打包

中间商锁依赖,定期主动更新,并对此负责

# SEE Conf: Umi 4 设计思路文字稿看来, 分为代码和类型两部分,通过nccdts-packer 实现的

这个命令"build:deps": "umi-scripts bundleDeps",, 在umi源码每个package下面几乎都有, 随便拉一个出来看看bundler-webpack, 之前也做过简单的认识【umi】02 如何在cjs 环境用esm包, 确实是转化为cjs了

可以看到许多版本都是写死了,这不太符合我们的习惯,我们一般都是用插入符^ 或者~。

然后升级的时候,我看到好像也不是批量去升级的,比如这些pr feat: vite deps upgrade to 4.2dep: webpack upgrade to 5.88.2等, 不知道对于这些写死版本的依赖,是怎么做到定期更新的?而且并不是每一个直接依赖都是写死的版本,有些为啥是"postcss": "^8.4.21",, 什么样的包才能预编译?

还是先看看预编译的核心代码吧,这下看不懂也要看了

这段对于package.json的处理还是比较好理解,就是拿到package.json下面的compiledConfig做一些初始化处理,compiledConfig会传入这六个参数

js 复制代码
import { readWantedLockfile } from '@pnpm/lockfile-file';
// @ts-ignore
import ncc from '@vercel/ncc';
import { Package } from 'dts-packer';
import resolve from 'resolve';
import 'zx/globals';
import { PATHS } from './.internal/constants';
// @ts-ignore
// import { Package } from '/Users/chencheng/code/github.com/sorrycc/dts-packer/dist/Package.js';

 // 编译打包 package.json 文件中 compiledConfig 配置的依赖库
(async () => {
  const base = process.cwd();
  const pkg = fs.readJSONSync(path.join(base, 'package.json'));
  const pkgDeps = pkg.dependencies || {};
  const {
    deps,
    externals = {},
    noMinify = [],
    extraDtsDeps = [],
    extraDtsExternals = [],
    excludeDtsDeps = [],
  } = pkg.compiledConfig;

  const webpackExternals: Record<string, string> = {};
  const dtsExternals = [...extraDtsDeps, ...extraDtsExternals];
  
  // 这里就是用$LOCAL这个标识区分下,如果有这个标识的话就是从当前包引入,
  // 没有的话从@umijs/bundler-utils 引入, https://github.com/umijs/umi/tree/master/packages/bundler-utils/compiled
  Object.keys(externals).forEach((name) => {
    const val = externals[name];
    if (val === '$$LOCAL') {
      dtsExternals.push(name);
      webpackExternals[name] = `${pkg.name}/compiled/${name}`;
    } else {
      webpackExternals[name] = val;
    }
  });

// 遍历dep,判断有没有argv.dep,没有走argv['extra-dts-only'],如果还是没有走`deps.concat(extraDtsDeps)`

  for (const dep of argv.dep
    ? [argv.dep]
    : argv['extra-dts-only']
    ? extraDtsDeps
    : deps.concat(extraDtsDeps)) {
    
    const isDep = dep.charAt(0) !== '.';
    await buildDep({
      ...(isDep ? { pkgName: dep } : { file: dep }),
      // path.basename(path.dirname('./bundles/webpack/bundle')) 这句话是返回目录名字,
      // 即这个路径的目录名(即`'./bundles/webpack'`,`path.basename`方法返回一个路径的最后一部分,即`webpack`
      target: `compiled/${isDep ? dep : path.basename(path.dirname(dep))}`, // 编译输出的文件目录
      base,
      webpackExternals,
      dtsExternals,
      clean: argv.clean,
      minify: !noMinify.includes(dep), // 排除不需要压缩的
      dtsOnly: extraDtsDeps.includes(dep),
      noDts: excludeDtsDeps.includes(dep), // 不需要生成d.tsde的依赖
      isDependency: dep in pkgDeps, 
    });
  }
})();

deps.concat(extraDtsDeps) 打印出,只是没有想通为啥有file的, 因为是一个目录

js 复制代码
export async function buildDep(opts: any) {
  console.log(chalk.green(`Build dep ${opts.pkgName || opts.file}`));

// /Users/nanlan/Documents/com-project/umi/packages/bundler-webpack/node_modules
  const nodeModulesPath = path.join(opts.base, 'node_modules');
  
 // /Users/nanlan/Documents/com-project/umi/packages/bundler-webpack/compiled/pkgName
  const target = path.join(opts.base, opts.target);

  if (opts.clean) {
    fs.removeSync(target);
  }

  let entry;
  if (opts.pkgName) {
    let resolvePath = opts.pkgName;
    // mini-css-extract-plugin 用 dist/cjs 为入口会有问题
    if (opts.pkgName === 'mini-css-extract-plugin') {
      resolvePath = 'mini-css-extract-plugin/dist/index';
    }
   // entry = /Users/nanlan/Documents/com-project/umi/packages/bundler-webpack/node_modules/less-loader/dist/cjs.js,
   // 很神奇,为啥会拼上/dist/cjs.js
    entry = resolve.sync(resolvePath, {
      basedir: nodeModulesPath,
    });
  } else {
    entry = path.join(opts.base, opts.file);
  }

  if (!opts.dtsOnly) {
    if (opts.isDependency) {
      fs.ensureDirSync(target);
      fs.writeFileSync(
        path.join(target, 'index.js'),
        `
const exported = require("${opts.pkgName}");
Object.keys(exported).forEach(function (key) {
  if (key === "default" || key === "__esModule") return;
  if (key in exports && exports[key] === exported[key]) return;
  Object.defineProperty(exports, key, {
    enumerable: true,
    get: function get() {
      return exported[key];
    }
  });
});
      `.trim() + '\n',
        'utf-8',
      );
    } else {
      const filesToCopy: string[] = [];
      if (opts.file === './bundles/webpack/bundle') {
        delete opts.webpackExternals['webpack'];
      }

      // babel pre rewrite
      if (opts.file === './bundles/babel/bundle') {
        // See https://github.com/umijs/umi/issues/10356
        // The inherited `browserslist` config is dynamic loaded
        const babelCorePkg = require.resolve('@babel/core/package.json', {
          paths: [path.join(PATHS.PACKAGES, './bundler-utils')],
        });
        // And need overrides a consistent version of `browserslist` in `packages.json#pnpm.overrides`
        const browserslistPkg = require.resolve('browserslist/package.json', {
          paths: [path.dirname(babelCorePkg)],
        });
        const nodePartFile = path.join(
          path.dirname(browserslistPkg),
          'node.js',
        );
        const originContent = fs.readFileSync(nodePartFile, 'utf-8');
        // https://github.com/browserslist/browserslist/blob/fc5fc088c640466df62a6b6c86154b19be3de821/node.js#L176
        fs.writeFileSync(
          nodePartFile,
          originContent.replace(
            /require\(require\.resolve/g,
            'eval("require")(require.resolve',
          ),
          'utf-8',
        );
      }

     // 利用ncc 转化为 单个的es5文件
      let { code, assets } = await ncc(entry, {
        externals: opts.webpackExternals,
        minify: !!opts.minify,
        target: 'es5',
        assetBuilds: false,
        customEmit(filePath: string, { id }: any) {
          if (
            (opts.file === './bundles/webpack/bundle' &&
              filePath.endsWith('.runtime.js')) ||
            (opts.pkgName === 'terser-webpack-plugin' &&
              filePath.endsWith('./utils') &&
              id.endsWith('terser-webpack-plugin/dist/index.js')) ||
            (opts.pkgName === 'css-minimizer-webpack-plugin' &&
              filePath.endsWith('./utils') &&
              id.endsWith('css-minimizer-webpack-plugin/dist/index.js'))
          ) {
            filesToCopy.push(
              resolve.sync(filePath, {
                basedir: path.dirname(id),
              }),
            );
            return `'./${path.basename(filePath)}'`;
          }
        },
      });

      // assets
      // console.log('filesToCopy', filesToCopy);
      for (const key of Object.keys(assets)) {
        const asset = assets[key];
        const data = asset.source;
        const filePath = path.join(target, key);
        fs.ensureDirSync(path.dirname(filePath));
        fs.writeFileSync(path.join(target, key), data);
      }

      // filesToCopy
      for (const fileToCopy of filesToCopy) {
        let content = fs.readFileSync(fileToCopy, 'utf-8');
        for (const key of Object.keys(opts.webpackExternals)) {
          content = content.replace(
            new RegExp(`require\\\(['"]${key}['"]\\\)`, 'gm'),
            `require('${opts.webpackExternals[key]}')`,
          );
          content = content.replace(
            new RegExp(`require\\\(['"]${key}/package(\.json)?['"]\\\)`, 'gm'),
            `require('${opts.webpackExternals[key]}/package.json')`,
          );
        }
        fs.writeFileSync(
          path.join(target, path.basename(fileToCopy)),
          content,
          'utf-8',
        );
      }

      // entry code
      fs.ensureDirSync(target);
    
    //省略代码, 一些特殊模块的处理

}

写入license 和 package.json ,利用new Package提取有.d.ts的依赖

js 复制代码
  // license & package.json
  if (opts.pkgName) {
  // 如果依赖是dependencies内,则写入index.d.ts,着实有点懵?
    if (opts.isDependency) {
      fs.ensureDirSync(target);
      fs.writeFileSync(
        path.join(target, 'index.d.ts'),
        `export * from '${opts.pkgName}';\n`,
        'utf-8',
      );
    } else {

      fs.ensureDirSync(target);
      const pkgRoot = path.dirname(
        resolve.sync(`${opts.pkgName}/package.json`, {
          basedir: opts.base,
        }),
      );
       // 写入LICENSE
      if (fs.existsSync(path.join(pkgRoot, 'LICENSE'))) {
        fs.writeFileSync(
          path.join(target, 'LICENSE'),
          fs.readFileSync(path.join(pkgRoot, 'LICENSE'), 'utf-8'),
          'utf-8',
        );
      }
      const { name, author, license, types, typing, typings, version } =
        JSON.parse(
          fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'),
        );
        // 写入package.json
      fs.writeJSONSync(path.join(target, 'package.json'), {
        ...{},
        ...{ name },
        ...{ version },
        ...(author ? { author } : undefined),
        ...(license ? { license } : undefined),
        ...(types ? { types } : undefined),
        ...(typing ? { typing } : undefined),
        ...(typings ? { typings } : undefined),
      });

      // dts
      if (opts.noDts) {
        console.log(chalk.yellow(`Do not build dts for ${opts.pkgName}`));
      } else {
      // import { Package } from 'dts-packer'; 
      //  这个是作者自己写的包,貌似是提取.d.ts文件
        new Package({
          cwd: opts.base,
          name: opts.pkgName,
          typesRoot: target,
          externals: opts.dtsExternals,
        });

        // patch
        if (opts.pkgName === 'webpack-5-chain') {
          const filePath = path.join(target, 'types/index.d.ts');
          // 替换成从umi引入
          fs.writeFileSync(
            filePath,
            fs
              .readFileSync(filePath, 'utf-8')
              .replace(
                `} from 'webpack';`,
                `} from '@umijs/bunder-webpack/compiled/webpack';`,
              ),
            'utf-8',
          );
        }

        // for bundler-utils
        if (opts.pkgName === 'less') {
          const dtsPath = path.join(opts.base, 'compiled/less/index.d.ts');

          fs.writeFileSync(
            dtsPath,
            fs
              .readFileSync(dtsPath, 'utf-8')
              .replace(
                'declare module "less"',
                'declare module "@umijs/bundler-utils/compiled/less"',
              ),
            'utf-8',
          );
        }
      }
    }
  }

  // copy files in packages
  if (opts.file && !opts.dtsOnly) {
    const packagesDir = path.join(
      opts.base,
      path.dirname(opts.file),
      'packages',
    );
    if (fs.existsSync(packagesDir)) {
      const files = fs.readdirSync(packagesDir);
      files.forEach((file) => {
        if (file.charAt(0) === '.') return;
        if (!fs.statSync(path.join(packagesDir, file)).isFile()) return;
        fs.copyFileSync(path.join(packagesDir, file), path.join(target, file));
      });
    }
  }

依赖预打包的关键代码就是这些, 主要使用了nccdts-packer从node_modules获取源码来转化的。那么看完其实有几个问题的

1、怎么做到定期更新的

2、哪些包需要预打包,哪些包不需要?

3、既然大部分依赖预打包是从node_modules获取,那么webpackbabael 本地有大量的代码,难道是源码?

希望在后续的学习中能破解这谜题

相关推荐
四喜花露水25 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy34 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称1 小时前
购物车-多元素组合动画css
前端·css
编程一生2 小时前
回调数据丢了?
运维·服务器·前端
丶21362 小时前
【鉴权】深入了解 Cookie:Web 开发中的客户端存储小数据
前端·安全·web
Missmiaomiao3 小时前
npm install慢
前端·npm·node.js