217 lines
7.6 KiB
JavaScript
217 lines
7.6 KiB
JavaScript
import path from 'path';
|
|
|
|
import { trackDownIdentifier } from './track-down-identifier.js';
|
|
import { swcTraverse, getPathFromNode } from './swc-traverse.js';
|
|
import { AstService } from '../core/AstService.js';
|
|
import { toPosixPath } from './to-posix-path.js';
|
|
import { fsAdapter } from './fs-adapter.js';
|
|
|
|
/**
|
|
* @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
|
|
* @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot
|
|
* @typedef {import('../../../types/index.js').SwcBinding} SwcBinding
|
|
* @typedef {import('../../../types/index.js').SwcPath} SwcPath
|
|
* @typedef {import('@swc/core').Node} SwcNode
|
|
*/
|
|
|
|
/**
|
|
* @param {{rootPath:PathFromSystemRoot; localPath:PathRelativeFromProjectRoot}} opts
|
|
* @returns {PathRelativeFromProjectRoot}
|
|
*/
|
|
export function getFilePathOrExternalSource({ rootPath, localPath }) {
|
|
if (!localPath.startsWith('.')) {
|
|
// We are not resolving external files like '@lion/input-amount/x.js',
|
|
// but we give a 100% score if from and to are same here..
|
|
return localPath;
|
|
}
|
|
return /** @type {PathRelativeFromProjectRoot} */ (
|
|
toPosixPath(path.resolve(rootPath, localPath))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Assume we had:
|
|
* ```js
|
|
* const x = 88;
|
|
* const y = x;
|
|
* export const myIdentifier = y;
|
|
* ```
|
|
* - We started in getSourceCodeFragmentOfDeclaration (looking for 'myIdentifier'), which found VariableDeclarator of export myIdentifier
|
|
* - getReferencedDeclaration is called with { referencedIdentifierName: 'y', globalScopeBindings: {x: SwcBinding; y: SwcBinding} }
|
|
* - now we will look in globalScopeBindings, till we find declaration of 'y'
|
|
* - Is it a ref? Call ourselves with referencedIdentifierName ('x' in example above)
|
|
* - is it a non ref declaration? Return the path of the node
|
|
* @param {{ referencedIdentifierName:string, globalScopeBindings:{[key:string]:SwcBinding}; }} opts
|
|
* @returns {SwcPath|null}
|
|
*/
|
|
export function getReferencedDeclaration({ referencedIdentifierName, globalScopeBindings }) {
|
|
// We go from referencedIdentifierName 'y' to binding (VariableDeclarator path) 'y';
|
|
const refDeclaratorBinding = globalScopeBindings[referencedIdentifierName];
|
|
|
|
// We provided a referencedIdentifierName that is not in the globalScopeBindings
|
|
if (!refDeclaratorBinding) {
|
|
return null;
|
|
}
|
|
|
|
if (['ImportSpecifier', 'ImportDefaultSpecifier'].includes(refDeclaratorBinding.path.node.type)) {
|
|
return refDeclaratorBinding.path;
|
|
}
|
|
|
|
if (refDeclaratorBinding.identifier.init.type === 'Identifier') {
|
|
return getReferencedDeclaration({
|
|
referencedIdentifierName: refDeclaratorBinding.identifier.init.value,
|
|
globalScopeBindings,
|
|
});
|
|
}
|
|
|
|
return refDeclaratorBinding.path.get('init');
|
|
}
|
|
|
|
/**
|
|
* @example
|
|
* ```js
|
|
* // ------ input file --------
|
|
* const x = 88;
|
|
* const y = x;
|
|
* export const myIdentifier = y;
|
|
* // --------------------------
|
|
*
|
|
* await getSourceCodeFragmentOfDeclaration(code) // finds "88"
|
|
* ```
|
|
*
|
|
* @param {{ filePath: PathFromSystemRoot; exportedIdentifier: string; projectRootPath: PathFromSystemRoot }} opts
|
|
* @returns {Promise<{ sourceNodePath: SwcPath; sourceFragment: string|null; externalImportSource: string|null; }>}
|
|
*/
|
|
export async function getSourceCodeFragmentOfDeclaration({
|
|
exportedIdentifier,
|
|
projectRootPath,
|
|
filePath,
|
|
}) {
|
|
const code = fsAdapter.fs.readFileSync(filePath, 'utf8');
|
|
|
|
// compensate for swc span bug: https://github.com/swc-project/swc/issues/1366#issuecomment-1516539812
|
|
const offset = AstService._getSwcOffset();
|
|
// TODO: fix swc-to-babel lib to make this compatible with 'swc-to-babel' mode of getAst
|
|
const swcAst = AstService._getSwcAst(code);
|
|
|
|
/** @type {SwcPath} */
|
|
let finalNodePath;
|
|
|
|
swcTraverse(
|
|
swcAst,
|
|
{
|
|
Module(astPath) {
|
|
astPath.stop();
|
|
|
|
// Situations
|
|
// - Identifier is part of default export (in this case 'exportedIdentifier' is '[default]' )
|
|
// - declared right away (for instance a class)
|
|
// - referenced (possibly recursively) by other declaration
|
|
// - Identifier is part of a named export
|
|
// - declared right away
|
|
// - referenced (possibly recursively) by other declaration
|
|
|
|
const globalScopeBindings = getPathFromNode(astPath.node.body?.[0])?.scope.bindings;
|
|
|
|
if (exportedIdentifier === '[default]') {
|
|
const defaultExportPath = /** @type {SwcPath} */ (
|
|
getPathFromNode(
|
|
astPath.node.body.find((/** @type {{ type: string; }} */ child) =>
|
|
['ExportDefaultDeclaration', 'ExportDefaultExpression'].includes(child.type),
|
|
),
|
|
)
|
|
);
|
|
const isReferenced = defaultExportPath?.node.expression?.type === 'Identifier';
|
|
|
|
if (!isReferenced) {
|
|
finalNodePath = defaultExportPath.get('decl') || defaultExportPath.get('expression');
|
|
} else {
|
|
finalNodePath = /** @type {SwcPath} */ (
|
|
getReferencedDeclaration({
|
|
referencedIdentifierName: defaultExportPath.node.expression.value,
|
|
// @ts-expect-error
|
|
globalScopeBindings,
|
|
})
|
|
);
|
|
}
|
|
} else {
|
|
const variableDeclaratorPath = astPath.scope.bindings[exportedIdentifier].path;
|
|
const varDeclNode = variableDeclaratorPath.node;
|
|
const isReferenced = varDeclNode.init?.type === 'Identifier';
|
|
const contentPath = varDeclNode.init
|
|
? variableDeclaratorPath.get('init')
|
|
: variableDeclaratorPath;
|
|
|
|
const name = varDeclNode.init
|
|
? varDeclNode.init.value
|
|
: varDeclNode.id?.value || varDeclNode.imported?.value || varDeclNode.orig?.value;
|
|
|
|
if (!isReferenced) {
|
|
// it must be an exported declaration
|
|
finalNodePath = contentPath;
|
|
} else {
|
|
finalNodePath = /** @type {SwcPath} */ (
|
|
getReferencedDeclaration({
|
|
referencedIdentifierName: name,
|
|
// @ts-expect-error
|
|
globalScopeBindings,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{ needsAdvancedPaths: true },
|
|
);
|
|
|
|
// @ts-expect-error
|
|
if (finalNodePath.type === 'ImportSpecifier') {
|
|
// @ts-expect-error
|
|
const importDeclNode = finalNodePath.parentPath.node;
|
|
const source = importDeclNode.source.value;
|
|
// @ts-expect-error
|
|
const identifierName = finalNodePath.node.imported?.value || finalNodePath.node.local?.value;
|
|
const currentFilePath = filePath;
|
|
|
|
const rootFile = await trackDownIdentifier(
|
|
source,
|
|
identifierName,
|
|
currentFilePath,
|
|
projectRootPath,
|
|
);
|
|
const filePathOrSrc = getFilePathOrExternalSource({
|
|
rootPath: projectRootPath,
|
|
localPath: /** @type {PathRelativeFromProjectRoot} */ (rootFile.file),
|
|
});
|
|
|
|
// TODO: allow resolving external project file paths
|
|
if (!filePathOrSrc.startsWith('/')) {
|
|
// So we have external project; smth like '@lion/input/x.js'
|
|
return {
|
|
// @ts-expect-error
|
|
sourceNodePath: finalNodePath,
|
|
sourceFragment: null,
|
|
externalImportSource: filePathOrSrc,
|
|
};
|
|
}
|
|
|
|
return getSourceCodeFragmentOfDeclaration({
|
|
filePath: /** @type {PathFromSystemRoot} */ (filePathOrSrc),
|
|
exportedIdentifier: rootFile.specifier,
|
|
projectRootPath,
|
|
});
|
|
}
|
|
|
|
return {
|
|
// @ts-expect-error
|
|
sourceNodePath: finalNodePath,
|
|
sourceFragment: code.slice(
|
|
// @ts-expect-error
|
|
finalNodePath.node.span.start - 1 - offset,
|
|
// @ts-expect-error
|
|
finalNodePath.node.span.end - 1 - offset,
|
|
),
|
|
// sourceFragment: finalNodePath.node?.raw || finalNodePath.node?.value,
|
|
externalImportSource: null,
|
|
};
|
|
}
|