【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 本地有大量的代码,难道是源码?

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

相关推荐
AlienZHOU2 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc5 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师7 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen7 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒7 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow8 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林8188 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang800788 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明8 小时前
第 6 章 组件进阶
前端·vue.js
江华森8 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端