TypeScript 集成

文件位置

  • src/typescript.js — TypeScript 编译器版本发现与加载
  • src/loaders/ts-loader.js — TypeScript Loader 包装

TypeScript 版本解析策略

// src/typescript.js
const { Module } = require('module');
const m = new Module('', null);
const { quiet, typescriptLookupPath } = JSON.parse(process.env.__NCC_OPTS || '{}');
m.paths = Module._nodeModulePaths(
  process.env.TYPESCRIPT_LOOKUP_PATH || typescriptLookupPath || (process.cwd() + '/')
);

let typescript;
try {
  typescript = m.require('typescript');
  // → "ncc: Using typescript@X.Y.Z (local user-provided)"
} catch (e) {
  typescript = require('typescript');
  // → "ncc: Using typescript@X.Y.Z (ncc built-in)"
}
module.exports = typescript;

解析优先级

  1. TYPESCRIPT_LOOKUP_PATH 环境变量:最高优先级,允许完全自定义
  2. typescriptLookupPath(来自 __NCC_OPTS:默认为入口文件的 resolve 路径
  3. process.cwd() + '/':回退到当前工作目录
  4. ncc 内置 TypeScript:如果以上路径都找不到 TypeScript,使用 ncc 自己打包的版本

实现机制

使用 Node.js 内部的 Module._nodeModulePaths() 生成模块搜索路径,然后通过虚拟 Module 实例的 require() 进行加载。这绕过了 ncc 自身的模块解析(因为 typescript.js 在构建时是原样复制到 dist/ 的,不经过 Webpack 编译)。

ts-loader 集成

配置

// src/index.js:359-377 - Webpack 规则
{
  test: /\.tsx?$/,
  use: [
    { loader: "uncacheable.js" },  // 确保不缓存
    {
      loader: "ts-loader.js",
      options: {
        transpileOnly,
        compiler: __dirname + "/typescript.js",
        compilerOptions: {
          module: 'esnext',
          target: 'esnext',
          ...fullTsconfig.compilerOptions,
          allowSyntheticDefaultImports: true,
          noEmit: false,
          outDir: '//'
        }
      }
    }
  ]
}

编译器选项覆盖

选项强制值原因
module'esnext'让 Webpack 处理模块语法
target'esnext'不做语法降级(由 Webpack target 控制)
allowSyntheticDefaultImportstrue确保 CJS 模块可以 default import
noEmitfalse必须产生输出(否则 ts-loader 不工作)
outDir'//'防止输出到实际磁盘路径

用户 tsconfig 中的 compilerOptions 通过展开运算符合并,但上面的强制值会覆盖用户设置。

--transpile-only 模式

当使用 -t / --transpile-only 选项时:

  • ts-loader 跳过类型检查,只做语法转换
  • 构建速度显著提升
  • 类型错误不会阻断构建
  • 嵌套资源构建(assetBuilds)自动启用此模式以节省 CPU

TypeScript 错误报告

默认模式下(非 transpileOnly),TypeScript 类型错误会通过 Webpack 的错误收集机制冒泡:

// src/index.js:400-403
if (stats.hasErrors()) {
  const errLog = [...stats.compilation.errors].map(err => err.message).join('\n');
  return reject(new Error(errLog));
}

错误信息包含文件名和行列号,格式如:file.ts(3,16): error TS2322: ...

TSConfig Paths 支持

// src/index.js:126-135
resolvePlugins.push(new TsconfigPathsPlugin(tsconfigPathsOptions));

const tsconfig = tsconfigPaths.loadConfig();
if (tsconfig.resultType === "success") {
  tsconfigMatchPath = tsconfigPaths.createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths);
}

如果 tsconfig 中定义了 paths(路径别名),ncc 会:

  1. 注册 TsconfigPathsPlugin 使 Webpack 理解这些别名
  2. 创建 matchPath 函数用于自定义解析

如果 tsconfig 的 compilerOptions.allowJstrueTsconfigPathsPlugin 会扩展到所有支持的扩展名。

构建时的 TypeScript 打包

scripts/build.js 中,ts-loader 被单独编译为一个 bundle:

// scripts/build.js:56-66
const { code: tsLoader } = await ncc(
  __dirname + "/../src/loaders/ts-loader",
  { filename: "ts-loader.js", minify, cache, v8cache, noAssetBuilds: true }
);

这个 bundle 包含 ts-loader 本身,但不包含 TypeScript 编译器(通过 compiler 选项指向外部的 typescript.js)。TypeScript 库的 .d.ts 类型文件通过 copy 命令单独复制到 dist/ncc/loaders/typescript/lib/