Loader 系统

概述

ncc 使用一组自定义 Webpack Loader 来处理模块加载过程中的各种边界情况。大部分 Loader 是轻量级的包装器或简单的文本处理器。

Loader 执行顺序

Webpack Loader 从配置数组的最后一个开始执行(从下往上)。ncc 的规则及其实际执行顺序:

flowchart LR
    subgraph "所有 .js/.mjs/.tsx?/.node 文件"
        R["relocate-loader"] --> E["empty-loader"]
    end

    subgraph "额外:.tsx? 文件"
        U["uncacheable"] --> T["ts-loader"]
    end

    subgraph "所有非 .node/.json 文件"
        S["shebang-loader"]
    end

    subgraph "@@notfound.js"
        N["notfound-loader"]
    end

实际一个 .ts 文件会经过:shebang-loaderuncacheablets-loaderrelocate-loaderempty-loader

各 Loader 详解

relocate-loader (src/loaders/relocate-loader.js)

module.exports = require('@vercel/webpack-asset-relocator-loader');

职责:这是 ncc 最核心的 Loader,负责静态分析和重定位文件系统引用。

实际实现在 @vercel/webpack-asset-relocator-loader 包中,ncc 只是简单地 re-export。它的功能包括:

  • 分析 __dirname__filenamerequire.resolve() 等路径引用
  • 将引用的文件/目录作为 asset 输出
  • 处理动态 require() 表达式
  • 提供 initAssetCache()getAssetMeta()getSymlinks() API 供后处理使用

配置选项(传入 src/index.js:349-355):

选项说明
customEmit用户提供自定义资源发射函数
filterAssetBaseprocess.cwd()只输出此目录内的资源
existingAssetNames数组已知资源名列表,避免冲突
escapeNonAnalyzableRequirestrue转义无法分析的 require
wrapperCompatibilitytrue兼容 wrapper 模式
debugLog用户指定是否输出调试日志

ts-loader (src/loaders/ts-loader.js)

// src/loaders/ts-loader.js
const logger = require("ts-loader/dist/logger");
const makeLogger = logger.makeLogger;
logger.makeLogger = function(loaderOptions, colors) {
  const instance = makeLogger(loaderOptions, colors);
  const logWarning = instance.logWarning;
  instance.logWarning = function(message) {
    if (message.indexOf('This version may or may not be compatible with ts-loader') !== -1)
      return;  // 过滤兼容性警告
    return logWarning(message);
  };
  return instance;
};
module.exports = require("ts-loader");
module.exports.typescript = require("typescript");

职责:TypeScript → JavaScript 转换。

设计要点

  1. 猴子补丁 ts-loader 的 logger,过滤 TypeScript 版本兼容性警告
  2. 导出内置的 typescript 模块,供 ts-loader 内部使用
  3. 配合 src/typescript.js 实现用户本地 TypeScript 优先加载

Webpack 配置中的选项src/index.js:364-376):

{
  transpileOnly,                    // 来自 --transpile-only 选项
  compiler: __dirname + "/typescript.js",  // 使用自定义 TS 解析
  compilerOptions: {
    module: 'esnext',
    target: 'esnext',
    ...fullTsconfig.compilerOptions,  // 合并用户 tsconfig
    allowSyntheticDefaultImports: true,
    noEmit: false,
    outDir: '//'                     // 防止输出到实际目录
  }
}

empty-loader (src/loaders/empty-loader.js)

// src/loaders/empty-loader.js
const emptyModules = { 'uglify-js': true, 'uglify-es': true };

module.exports = function(input, map) {
  const id = this.resourcePath;
  const pkgBase = getPackageBase(id);
  if (pkgBase) {
    const pkgName = baseParts[baseParts.length - 1];
    if (pkgName in emptyModules) {
      console.warn(`ncc: Ignoring build of ${pkgName}...`);
      return '';  // 返回空字符串
    }
  }
  this.callback(null, input, map);
};

职责:将不可静态分析的包替换为空模块。

当前被过滤的包uglify-jsuglify-es — 这些包的动态特性会导致 Webpack 分析失败。

shebang-loader (src/loaders/shebang-loader.js)

// 简单的一行 re-export
module.exports = require('shebang-loader');

职责:移除文件开头的 shebang 行(#!/usr/bin/env node),防止 JavaScript 解析器报错。原始 shebang 在 finalizeHandler 中恢复。

notfound-loader (src/loaders/notfound-loader.js)

// src/loaders/notfound-loader.js
module.exports = function(input, map) {
  if (this.cacheable) this.cacheable();
  const id = this.resourceQuery.substr(1);  // 从 ?query 获取模块名
  input = input.replace('\'UNKNOWN\'', JSON.stringify(id));
  this.callback(null, input, map);
};

职责:将 @@notfound.js 中的 'UNKNOWN' 占位符替换为实际的模块名称。

配合 src/@@notfound.js

module.exports = __non_webpack_require__('UNKNOWN');

最终输出:module.exports = __non_webpack_require__('actual-module-name')

这使得无法在构建时解析的模块在运行时通过 Node.js 原生的 require() 加载。

stringify-loader (src/loaders/stringify-loader.js)

module.exports = require('shebang-loader');  // 实际是 re-export

职责:将文件内容转为字符串模块导出(module.exports = "...")。用于处理非 JavaScript 资源文件。

uncacheable (src/loaders/uncacheable.js)

// src/loaders/uncacheable.js
module.exports = function(input) {
  this.cacheable(false);
  return input;
};

职责:标记模块为不可缓存。这确保 TypeScript 文件在 tsconfig 变更时总是重新编译,而不是使用 Webpack 缓存的版本。

@@notfound.js 机制

完整流程:

  1. Webpack resolve 插件检测到模块无法解析
  2. 将请求重定向到 src/@@notfound.js?actual-module-name
  3. notfound-loader 处理该文件,将 'UNKNOWN' 替换为真实名称
  4. 最终输出 module.exports = __non_webpack_require__('actual-module-name')
  5. 运行时通过 Node.js 原生 require 加载

设计意图:让 ncc 对缺失依赖保持宽容。某些包有条件性依赖(如数据库驱动),在构建环境中可能不存在但运行时可用。