How to extract the alias file location from a CommonJs module re-export?

86 Views Asked by At

Given the following ES module;

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

export {child, local}

I can extract the file references for exports in a ts.SourceFile: (given a valid program and checker in tsc v5.2.2)

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'
});

Given the equivalent CommonJs module I am struggling to achieve the same result:

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

exports = {child, local}

I have tried the following:

  • Using compilerOption 'module':'NodeNext':
const fileSymbol = checker.getSymbolAtLocation(sourceFile);
console.log(fileSymbol); //undefined
  • Using compilerOption 'module':'NodeNext'
const fileSymbol = checker.getSymbolAtLocation(sourceFile);
console.log(fileSymbol?.exports) //Map(0) {}

(source as any).locals.forEach((symbol: ts.Symbol) => {
    console.log(symbol.name);  //'child' or 'local'

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

    if (ts.isBindingElement(nodes[0])) {
        const node = nodes[0];
        let bindingSymbol = checker.getSymbolAtLocation(node);
        console.log(bindingSymbol); // undefined;

        bindingSymbol = checker.getSymbolAtLocation(node.name);
        const alias = checker.getAliasedSymbol(bindingSymbol!);
        console.log(alias) //undefined;

        /**
         * and from here I am stuck as to how to get the target filename of the export
         */
    }
});

I have been exploring the flowNode and throwing things at the checker to try and find the hook to replicate the ES workflow, but am only going further down rabbit holes...

Help in the right direction in replicating the ES module logic will be appreciated.

1

There are 1 best solutions below

0
Michael Jonker On

This is what I have come up with in all it's hacky nastiness:

Firstly, I switched to what TypeScript wants for a commonJs module (which I now know is export = and not exports = ), which opened up access to fileSymbol.exports.

The commonJs module is now:

const local = 'localValue';
const child = require('./child');

export = { local, child };

From there I can do;

const fileSymbol = checker.getSymbolAtLocation(source!);
const exportAssignment = fileSymbol!.exports!.get('export=' as any)!;

const childSymbol = checker
    .getTypeOfSymbol(exportAssignment)
    .getProperty('child')!;
const child = childSymbol!.declarations![0];
const localChild = checker.getExportSpecifierLocalTargetSymbol(
    (child as any).name,
);
const fileStub = (localChild?.valueDeclaration as any).initializer!.arguments[0].text;
const pathStub = path.join(path.dirname(source.fileName), fileStub);
const targetFile = program
    .getRootFileNames()
    .find((fileName) => fileName.startsWith(pathStub + '.'));

console.log(targetFile);

I hope that I am missing something in the API and somebody will point me towards a concise solution...