You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用TypeScript Checker从CommonJS模块重导出提取别名文件路径?

问题背景

在tsc v5.2.2中,针对ES模块可以通过checker和program轻松提取导出的文件引用:

ES模块示例代码

import {child} from './childFile';
const local = 'local';

export {child, local}

ES模块提取逻辑

const fileSymbol = checker.getSymbolAtLocation(sourceFile);

fileSymbol?.exports?.forEach((symbol) => {
    console.log(symbol.name); //'local' or 'child'

    const nodes = symbol.declarations!;
    console.log(ts.SyntaxKind[nodes[0].kind]); //ExportSpecifier

    const targets = checker.getAliasedSymbol(symbol)?.declarations!;
    console.log(targets[0].getSourceFile().fileName); //'./localFile' or './childFile'
});

但针对等价的CommonJS模块,无法复用上述逻辑:

CommonJS模块示例代码

const {child} = require('./child');
const local = 'local';

exports = {child, local}

尝试过的方案及问题

  • 使用module: NodeNext编译选项时,checker.getSymbolAtLocation(sourceFile)返回undefined
  • 访问source.locals能获取到child和local的符号,但处理BindingElement时,checker.getAliasedSymbol返回undefined,无法追踪到对应的文件引用

解决方案

CommonJS模块的导出是通过赋值语句实现的,TypeScript不会为其生成ES模块那样的fileSymbol.exports,需要手动解析赋值节点并追踪变量来源:

具体实现代码

// 1. 定位到exports赋值语句
const exportsAssignment = sourceFile.statements.find(
  stmt => ts.isExpressionStatement(stmt) && 
         ts.isAssignmentExpression(stmt.expression) && 
         stmt.expression.left.getText() === "exports"
) as ts.ExpressionStatement | undefined;

if (!exportsAssignment) return;

// 2. 获取导出对象字面量
const exportObj = (exportsAssignment.expression as ts.AssignmentExpression).right;
if (!ts.isObjectLiteralExpression(exportObj)) return;

// 3. 遍历每个导出属性,追踪来源
exportObj.properties.forEach(prop => {
  if (!ts.isPropertyAssignment(prop)) return;
  const exportName = prop.name.getText();
  
  // 获取属性对应的变量引用
  const valueExpr = prop.initializer;
  if (!ts.isIdentifier(valueExpr)) return;
  
  const varSymbol = checker.getSymbolAtLocation(valueExpr);
  if (!varSymbol?.declarations) return;

  // 处理来自require解构的变量(如child)
  const bindingDecl = varSymbol.declarations.find(ts.isBindingElement) as ts.BindingElement | undefined;
  if (bindingDecl) {
    // 找到父级的变量声明
    const varDecl = bindingDecl.parent.parent as ts.VariableDeclaration;
    // 定位require调用
    const requireCall = varDecl.initializer;
    if (ts.isCallExpression(requireCall) && requireCall.expression.getText() === "require") {
      const pathArg = requireCall.arguments[0];
      if (ts.isStringLiteral(pathArg)) {
        // 解析文件路径为真实文件名
        const resolveResult = program.resolveModuleName(
          pathArg.text,
          sourceFile.fileName,
          program.getCompilerOptions(),
          program.getModuleResolutionHost()
        );
        const targetFile = resolveResult.resolvedModule?.resolvedFileName;
        console.log(`导出 ${exportName} 来自文件: ${targetFile}`);
      }
    }
  } 
  // 处理本地变量(如local)
  else if (varSymbol.declarations.some(ts.isVariableDeclaration)) {
    console.log(`导出 ${exportName} 是本地变量`);
  }
});

关键说明

  • CommonJS的exports赋值是普通表达式,需要手动遍历语句找到目标节点
  • 对于解构自require的变量,需要逐层追踪到require调用表达式,提取路径后用program.resolveModuleName解析(和TypeScript模块解析逻辑一致)
  • 本地变量可通过判断声明类型直接识别

内容的提问来源于stack exchange,提问作者Michael Jonker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 23:57:16