266 lines
8.9 KiB
JavaScript
266 lines
8.9 KiB
JavaScript
const fs = require('fs');
|
|
const { default: traverse } = require('@babel/traverse');
|
|
const {
|
|
isRelativeSourcePath,
|
|
toRelativeSourcePath,
|
|
} = require('../../utils/relative-source-path.js');
|
|
const { resolveImportPath } = require('../../utils/resolve-import-path.js');
|
|
const { AstService } = require('../../services/AstService.js');
|
|
const { LogService } = require('../../services/LogService.js');
|
|
const { memoizeAsync } = require('../../utils/memoize.js');
|
|
|
|
/** @typedef {import('./types').RootFile} RootFile */
|
|
|
|
/**
|
|
* Other than with import, no binding is created for MyClass by Babel(?)
|
|
* This means 'path.scope.getBinding('MyClass')' returns undefined
|
|
* and we have to find a different way to retrieve this value.
|
|
* @param {object} astPath Babel ast traversal path
|
|
* @param {string} identifierName the name that should be tracked (and that exists inside scope of astPath)
|
|
*/
|
|
function getBindingAndSourceReexports(astPath, identifierName) {
|
|
// Get to root node of file and look for exports like `export { identifierName } from 'src';`
|
|
let source;
|
|
let bindingType;
|
|
let bindingPath;
|
|
|
|
let curPath = astPath;
|
|
while (curPath.parentPath) {
|
|
curPath = curPath.parentPath;
|
|
}
|
|
const rootPath = curPath;
|
|
rootPath.traverse({
|
|
ExportSpecifier(path) {
|
|
// eslint-disable-next-line arrow-body-style
|
|
const found =
|
|
path.node.exported.name === identifierName || path.node.local.name === identifierName;
|
|
if (found) {
|
|
bindingPath = path;
|
|
bindingType = 'ExportSpecifier';
|
|
source = path.parentPath.node.source.value;
|
|
path.stop();
|
|
}
|
|
},
|
|
});
|
|
return [source, bindingType, bindingPath];
|
|
}
|
|
|
|
/**
|
|
* @desc returns source and importedIdentifierName: We might be an import that was locally renamed.
|
|
* Since we are traversing, we are interested in the imported name. Or in case of a re-export,
|
|
* the local name.
|
|
* @param {object} astPath Babel ast traversal path
|
|
* @param {string} identifierName the name that should be tracked (and that exists inside scope of astPath)
|
|
*/
|
|
function getImportSourceFromAst(astPath, identifierName) {
|
|
let source;
|
|
let importedIdentifierName;
|
|
|
|
const binding = astPath.scope.getBinding(identifierName);
|
|
let bindingType = binding && binding.path.type;
|
|
let bindingPath = binding && binding.path;
|
|
const matchingTypes = ['ImportSpecifier', 'ImportDefaultSpecifier', 'ExportSpecifier'];
|
|
|
|
if (binding && matchingTypes.includes(bindingType)) {
|
|
source = binding.path.parentPath.node.source.value;
|
|
} else {
|
|
// no binding
|
|
[source, bindingType, bindingPath] = getBindingAndSourceReexports(astPath, identifierName);
|
|
}
|
|
|
|
const shouldLookForDefaultExport = bindingType === 'ImportDefaultSpecifier';
|
|
if (shouldLookForDefaultExport) {
|
|
importedIdentifierName = '[default]';
|
|
} else if (source) {
|
|
const { node } = bindingPath;
|
|
importedIdentifierName = (node.imported && node.imported.name) || node.local.name;
|
|
}
|
|
return { source, importedIdentifierName };
|
|
}
|
|
|
|
let trackDownIdentifier;
|
|
/**
|
|
* @example
|
|
*```js
|
|
* // 1. Starting point
|
|
* // target-proj/my-comp-import.js
|
|
* import { MyComp as TargetComp } from 'ref-proj';
|
|
*
|
|
* // 2. Intermediate stop: a re-export
|
|
* // ref-proj/exportsIndex.js (package.json has main: './exportsIndex.js')
|
|
* export { RefComp as MyComp } from './src/RefComp.js';
|
|
*
|
|
* // 3. End point: our declaration
|
|
* // ref-proj/src/RefComp.js
|
|
* export class RefComp extends LitElement {...}
|
|
*```
|
|
*
|
|
* @param {string} source an importSpecifier source, like 'ref-proj' or '../file'
|
|
* @param {string} identifierName imported reference/Identifier name, like 'MyComp'
|
|
* @param {string} currentFilePath file path, like '/path/to/target-proj/my-comp-import.js'
|
|
* @param {string} rootPath dir path, like '/path/to/target-proj'
|
|
* @returns {object} file: path of file containing the binding (exported declaration),
|
|
* like '/path/to/ref-proj/src/RefComp.js'
|
|
*/
|
|
async function trackDownIdentifierFn(source, identifierName, currentFilePath, rootPath, depth = 0) {
|
|
let rootFilePath; // our result path
|
|
let rootSpecifier; // the name under which it was imported
|
|
|
|
if (!isRelativeSourcePath(source)) {
|
|
// So, it is an external ref like '@lion/core' or '@open-wc/scoped-elements/index.js'
|
|
// At this moment in time, we don't know if we have file system access to this particular
|
|
// project. Therefore, we limit ourselves to tracking down local references.
|
|
// In case this helper is used inside an analyzer like 'match-subclasses', the external
|
|
// (search-target) project can be accessed and paths can be resolved to local ones,
|
|
// just like in 'match-imports' analyzer.
|
|
/** @type {RootFile} */
|
|
const result = { file: source, specifier: identifierName };
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @prop resolvedSourcePath
|
|
* @type {string}
|
|
* @example resolveImportPath('../file') // => '/path/to/target-proj/file.js'
|
|
*/
|
|
const resolvedSourcePath = await resolveImportPath(source, currentFilePath);
|
|
LogService.debug(`[trackDownIdentifier] ${resolvedSourcePath}`);
|
|
const code = fs.readFileSync(resolvedSourcePath, 'utf8');
|
|
const ast = AstService.getAst(code, 'babel', { filePath: resolvedSourcePath });
|
|
const shouldLookForDefaultExport = identifierName === '[default]';
|
|
|
|
let reexportMatch = null; // named specifier declaration
|
|
let pendingTrackDownPromise;
|
|
|
|
traverse(ast, {
|
|
ExportDefaultDeclaration(path) {
|
|
if (!shouldLookForDefaultExport) {
|
|
return;
|
|
}
|
|
|
|
let newSource;
|
|
if (path.node.declaration.type === 'Identifier') {
|
|
newSource = getImportSourceFromAst(path, path.node.declaration.name).source;
|
|
}
|
|
|
|
if (newSource) {
|
|
pendingTrackDownPromise = trackDownIdentifier(
|
|
newSource,
|
|
'[default]',
|
|
resolvedSourcePath,
|
|
rootPath,
|
|
depth + 1,
|
|
);
|
|
} else {
|
|
// We found our file!
|
|
rootSpecifier = identifierName;
|
|
rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath);
|
|
}
|
|
path.stop();
|
|
},
|
|
ExportNamedDeclaration: {
|
|
enter(path) {
|
|
if (reexportMatch || shouldLookForDefaultExport) {
|
|
return;
|
|
}
|
|
// Are we dealing with a re-export ?
|
|
if (path.node.specifiers && path.node.specifiers.length) {
|
|
const exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName);
|
|
|
|
if (exportMatch) {
|
|
const localName = exportMatch.local.name;
|
|
let newSource;
|
|
if (path.node.source) {
|
|
/**
|
|
* @example
|
|
* export { x } from 'y'
|
|
*/
|
|
newSource = path.node.source.value;
|
|
} else {
|
|
/**
|
|
* @example
|
|
* import { x } from 'y'
|
|
* export { x }
|
|
*/
|
|
newSource = getImportSourceFromAst(path, identifierName).source;
|
|
if (!newSource) {
|
|
/**
|
|
* @example
|
|
* const x = 12;
|
|
* export { x }
|
|
*/
|
|
return;
|
|
}
|
|
}
|
|
reexportMatch = true;
|
|
pendingTrackDownPromise = trackDownIdentifier(
|
|
newSource,
|
|
localName,
|
|
resolvedSourcePath,
|
|
rootPath,
|
|
depth + 1,
|
|
);
|
|
path.stop();
|
|
}
|
|
}
|
|
},
|
|
exit(path) {
|
|
if (!reexportMatch) {
|
|
// We didn't find a re-exported Identifier, that means the reference is declared
|
|
// in current file...
|
|
rootSpecifier = identifierName;
|
|
rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath);
|
|
path.stop();
|
|
}
|
|
},
|
|
},
|
|
});
|
|
|
|
if (pendingTrackDownPromise) {
|
|
// We can't handle promises inside Babel traverse, so we do it here...
|
|
const resObj = await pendingTrackDownPromise;
|
|
rootFilePath = resObj.file;
|
|
rootSpecifier = resObj.specifier;
|
|
}
|
|
return /** @type {RootFile } */ { file: rootFilePath, specifier: rootSpecifier };
|
|
}
|
|
|
|
trackDownIdentifier = memoizeAsync(trackDownIdentifierFn);
|
|
|
|
/**
|
|
* @param {BabelPath} astPath
|
|
* @param {string} identifierNameInScope
|
|
* @param {string} fullCurrentFilePath
|
|
* @param {string} projectPath
|
|
*/
|
|
async function trackDownIdentifierFromScopeFn(
|
|
astPath,
|
|
identifierNameInScope,
|
|
fullCurrentFilePath,
|
|
projectPath,
|
|
) {
|
|
const sourceObj = getImportSourceFromAst(astPath, identifierNameInScope);
|
|
|
|
/** @type {RootFile} */
|
|
let rootFile;
|
|
if (sourceObj.source) {
|
|
rootFile = await trackDownIdentifier(
|
|
sourceObj.source,
|
|
sourceObj.importedIdentifierName,
|
|
fullCurrentFilePath,
|
|
projectPath,
|
|
);
|
|
} else {
|
|
const specifier = sourceObj.importedIdentifierName || identifierNameInScope;
|
|
rootFile = { file: '[current]', specifier };
|
|
}
|
|
return rootFile;
|
|
}
|
|
|
|
const trackDownIdentifierFromScope = memoizeAsync(trackDownIdentifierFromScopeFn);
|
|
|
|
module.exports = {
|
|
trackDownIdentifier,
|
|
getImportSourceFromAst,
|
|
trackDownIdentifierFromScope,
|
|
};
|