225 lines
6.9 KiB
TypeScript
225 lines
6.9 KiB
TypeScript
import { joinPathFragments, readJsonFile } from '@nrwl/devkit';
|
|
import { findNodes } from '@nrwl/workspace/src/utilities/typescript';
|
|
import { MappedProjectGraphNode } from '@nrwl/workspace/src/utils/runtime-lint-utils';
|
|
import { existsSync, readFileSync } from 'fs';
|
|
import { dirname } from 'path';
|
|
import ts = require('typescript');
|
|
import { logger } from '@nrwl/devkit';
|
|
import { appRootPath } from '@nrwl/devkit';
|
|
|
|
function tryReadBaseJson() {
|
|
try {
|
|
return readJsonFile(joinPathFragments(appRootPath, 'tsconfig.base.json'));
|
|
} catch (e) {
|
|
logger.warn(`Error reading "tsconfig.base.json": \n${JSON.stringify(e)}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param importScope like `@myorg/somelib`
|
|
* @returns
|
|
*/
|
|
export function getBarrelEntryPointByImportScope(
|
|
importScope: string
|
|
): string[] | null {
|
|
const tsConfigBase = tryReadBaseJson();
|
|
return tsConfigBase?.compilerOptions?.paths[importScope] || null;
|
|
}
|
|
|
|
export function getBarrelEntryPointProjectNode(
|
|
importScope: MappedProjectGraphNode<any>
|
|
): { path: string; importScope: string }[] | null {
|
|
const tsConfigBase = tryReadBaseJson();
|
|
|
|
if (tsConfigBase?.compilerOptions?.paths) {
|
|
const potentialEntryPoints = Object.keys(tsConfigBase.compilerOptions.paths)
|
|
.filter((entry) => {
|
|
const sourceFolderPaths = tsConfigBase.compilerOptions.paths[entry];
|
|
return sourceFolderPaths.some((sourceFolderPath) => {
|
|
return sourceFolderPath.includes(importScope.data.root);
|
|
});
|
|
})
|
|
.map((entry) =>
|
|
tsConfigBase.compilerOptions.paths[entry].map((x) => ({
|
|
path: x,
|
|
importScope: entry,
|
|
}))
|
|
);
|
|
|
|
return potentialEntryPoints.flat();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function hasMemberExport(exportedMember, filePath) {
|
|
const fileContent = readFileSync(filePath, 'utf8');
|
|
|
|
// use the TypeScript AST to find the path to the file where exportedMember is defined
|
|
const sourceFile = ts.createSourceFile(
|
|
filePath,
|
|
fileContent,
|
|
ts.ScriptTarget.Latest,
|
|
true
|
|
);
|
|
|
|
// search whether there is already an export with our node
|
|
return (
|
|
findNodes(sourceFile, ts.SyntaxKind.Identifier).filter(
|
|
(identifier: any) => identifier.text === exportedMember
|
|
).length > 0
|
|
);
|
|
}
|
|
|
|
export function getRelativeImportPath(exportedMember, filePath, basePath) {
|
|
const fileContent = readFileSync(filePath, 'utf8');
|
|
|
|
// use the TypeScript AST to find the path to the file where exportedMember is defined
|
|
const sourceFile = ts.createSourceFile(
|
|
filePath,
|
|
fileContent,
|
|
ts.ScriptTarget.Latest,
|
|
true
|
|
);
|
|
|
|
// Search in the current file whether there's an export already!
|
|
const memberNodes = findNodes(sourceFile, ts.SyntaxKind.Identifier).filter(
|
|
(identifier: any) => identifier.text === exportedMember
|
|
);
|
|
|
|
let hasExport = false;
|
|
for (const memberNode of memberNodes || []) {
|
|
if (memberNode) {
|
|
// recursively navigate upwards to find the ExportKey modifier
|
|
let parent = memberNode;
|
|
do {
|
|
parent = parent.parent;
|
|
if (parent) {
|
|
// if we are inside a parameter list or decorator or param assignment
|
|
// then this is not what we're searching for, so break :)
|
|
if (
|
|
parent.kind === ts.SyntaxKind.Parameter ||
|
|
parent.kind === ts.SyntaxKind.PropertyAccessExpression ||
|
|
parent.kind === ts.SyntaxKind.TypeReference ||
|
|
parent.kind === ts.SyntaxKind.HeritageClause ||
|
|
parent.kind === ts.SyntaxKind.Decorator
|
|
) {
|
|
hasExport = false;
|
|
break;
|
|
}
|
|
|
|
// if our identifier is within an ExportDeclaration but is not just
|
|
// a re-export of some other module, we're good
|
|
if (
|
|
parent.kind === ts.SyntaxKind.ExportDeclaration &&
|
|
!(parent as any).moduleSpecifier
|
|
) {
|
|
hasExport = true;
|
|
break;
|
|
}
|
|
|
|
if (
|
|
parent.modifiers &&
|
|
parent.modifiers.find(
|
|
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword
|
|
)
|
|
) {
|
|
/**
|
|
* if we get to a function export declaration we need to verify whether the
|
|
* exported function is actually the member we are searching for. Otherwise
|
|
* we might end up finding a function that just uses our searched identifier
|
|
* internally.
|
|
*
|
|
* Example: assume we try to find a constant member: `export const SOME_CONSTANT = 'bla'`
|
|
*
|
|
* Then we might end up in a file that uses it like
|
|
*
|
|
* import { SOME_CONSTANT } from '@myorg/samelib'
|
|
*
|
|
* export function someFunction() {
|
|
* return `Hi, ${SOME_CONSTANT}`
|
|
* }
|
|
*
|
|
* We want to avoid accidentally picking the someFunction export since we're searching upwards
|
|
* starting from `SOME_CONSTANT` identifier usages.
|
|
*/
|
|
if (parent.kind === ts.SyntaxKind.FunctionDeclaration) {
|
|
const parentName = (parent as any).name?.text;
|
|
if (parentName === exportedMember) {
|
|
hasExport = true;
|
|
break;
|
|
}
|
|
} else {
|
|
hasExport = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} while (!!parent);
|
|
}
|
|
|
|
if (hasExport) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasExport) {
|
|
// we found the file, now grab the path
|
|
return filePath;
|
|
}
|
|
|
|
// if we didn't find an export, let's try to follow
|
|
// all export declarations and see whether any of those
|
|
// exports the node we're searching for
|
|
const exportDeclarations = findNodes(
|
|
sourceFile,
|
|
ts.SyntaxKind.ExportDeclaration
|
|
) as ts.ExportDeclaration[];
|
|
for (const exportDeclaration of exportDeclarations) {
|
|
if ((exportDeclaration as any).moduleSpecifier) {
|
|
// verify whether the export declaration we're looking at is a named export
|
|
// cause in that case we need to check whether our searched member is
|
|
// part of the exports
|
|
if (
|
|
exportDeclaration.exportClause &&
|
|
findNodes(exportDeclaration, ts.SyntaxKind.Identifier).filter(
|
|
(identifier: any) => identifier.text === exportedMember
|
|
).length === 0
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const modulePath = (exportDeclaration as any).moduleSpecifier.text;
|
|
|
|
let moduleFilePath = joinPathFragments(
|
|
'./',
|
|
dirname(filePath),
|
|
`${modulePath}.ts`
|
|
);
|
|
if (!existsSync(moduleFilePath)) {
|
|
// might be a index.ts
|
|
moduleFilePath = joinPathFragments(
|
|
'./',
|
|
dirname(filePath),
|
|
`${modulePath}/index.ts`
|
|
);
|
|
}
|
|
|
|
if (hasMemberExport(exportedMember, moduleFilePath)) {
|
|
const foundFilePath = getRelativeImportPath(
|
|
exportedMember,
|
|
moduleFilePath,
|
|
basePath
|
|
);
|
|
if (foundFilePath) {
|
|
return foundFilePath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|