Skip to content

Commit 105eb91

Browse files
authored
feat(ts-parser): TS monorepo support (#75)
* feat: added ts monorepo support * feat: remove yarn config * fix: remove yarn config * chore: addressed comments * feat: handle missing tsconfig fallback
1 parent 76af2f7 commit 105eb91

File tree

6 files changed

+873
-62
lines changed

6 files changed

+873
-62
lines changed

ts-parser/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ See `./index.ts` for more information.
3737

3838
4. If the repository you're analyzing is too large, you may need to adjust Node.js's maximum memory allocation.
3939

40+
## Terminology
41+
42+
**Package vs Module**: In JavaScript/TypeScript terminology, a "Package" typically refers to an npm package (defined by `package.json`). However, in our UniAST output, what JavaScript/TypeScript calls a "Package" corresponds to a "Module" in the UniAST structure. This means:
43+
44+
- **TypeScript/JavaScript Package** (npm package with `package.json`) → **UniAST Module**
45+
- **TypeScript/JavaScript Module** (individual `.ts`/`.js` files) → **UniAST Package**
46+
47+
This terminology mapping is used consistently throughout the parser to align with the UniAST specification, but it may initially seem counterintuitive to developers familiar with JavaScript/TypeScript conventions.
48+
4049

4150
## Some known issues
4251

ts-parser/src/parser/RepositoryParser.ts

Lines changed: 96 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,78 @@ import * as fs from 'fs';
44
import { Repository, Node, Relation, Identity, Function } from '../types/uniast';
55
import { ModuleParser } from './ModuleParser';
66
import { TsConfigCache } from '../utils/tsconfig-cache';
7+
import { MonorepoUtils } from '../utils/monorepo';
78

89
export class RepositoryParser {
9-
private project: Project;
10-
private moduleParser: ModuleParser;
10+
private project?: Project;
11+
private moduleParser?: ModuleParser;
1112
private tsConfigCache: TsConfigCache;
13+
private projectRoot: string;
14+
private tsConfigPath?: string;
1215

1316
constructor(projectRoot: string, tsConfigPath?: string) {
1417
this.tsConfigCache = TsConfigCache.getInstance();
18+
this.projectRoot = projectRoot;
19+
this.tsConfigPath = tsConfigPath;
20+
}
1521

22+
async parseRepository(repoPath: string, options: { loadExternalSymbols?: boolean, noDist?: boolean, srcPatterns?: string[] } = {}): Promise<Repository> {
23+
const absolutePath = path.resolve(repoPath);
1624

17-
let configPath = path.join(projectRoot, 'tsconfig.json');
25+
const repository: Repository = {
26+
ASTVersion: "v0.1.3",
27+
id: path.basename(absolutePath),
28+
Modules: {},
29+
Graph: {}
30+
};
31+
32+
const isMonorepo = MonorepoUtils.isMonorepo(absolutePath);
33+
34+
if (isMonorepo) {
35+
const packages = MonorepoUtils.getMonorepoPackages(absolutePath);
36+
console.log(`Monorepo detected. Found ${packages.length} packages.`);
37+
38+
for (const pkg of packages) {
39+
const packageTsConfigPath = path.join(pkg.absolutePath, 'tsconfig.json');
40+
try {
41+
let project: Project;
42+
if (fs.existsSync(packageTsConfigPath)) {
43+
console.log(`Parsing package ${pkg.name || pkg.path} with tsconfig ${packageTsConfigPath}`);
44+
project = new Project({
45+
tsConfigFilePath: packageTsConfigPath,
46+
compilerOptions: {
47+
allowJs: true,
48+
skipLibCheck: true,
49+
forceConsistentCasingInFileNames: true
50+
}
51+
});
52+
} else {
53+
console.log(`No tsconfig.json found for package ${pkg.name || pkg.path}, using default configuration.`);
54+
project = this.createProjectWithDefaultConfig();
55+
}
56+
57+
const moduleParser = new ModuleParser(project, this.projectRoot);
58+
const module = await moduleParser.parseModule(pkg.absolutePath, pkg.path, options);
59+
repository.Modules[module.Name] = module;
60+
} catch (error) {
61+
console.warn(`Failed to parse package ${pkg.name || pkg.path}:`, error);
62+
}
63+
}
64+
} else {
65+
console.log('Single project detected.');
66+
this.project = this.createProjectForSingleRepo(this.projectRoot, this.tsConfigPath);
67+
this.moduleParser = new ModuleParser(this.project, this.projectRoot);
68+
const module = await this.moduleParser.parseModule(absolutePath, '.', options);
69+
repository.Modules[module.Name] = module;
70+
}
71+
72+
this.buildGlobalGraph(repository);
73+
return repository;
74+
}
75+
76+
private createProjectForSingleRepo(projectRoot: string, tsConfigPath?: string): Project {
77+
let configPath = path.join(projectRoot, 'tsconfig.json');
1878

19-
// If a custom tsconfig path is provided, use it
2079
if (tsConfigPath) {
2180
let absoluteTsConfigPath = tsConfigPath;
2281
if (!path.isAbsolute(absoluteTsConfigPath)) {
@@ -27,8 +86,7 @@ export class RepositoryParser {
2786
}
2887

2988
if (fs.existsSync(configPath)) {
30-
// if tsconfig.json exists, use it to configure the project
31-
this.project = new Project({
89+
const project = new Project({
3290
tsConfigFilePath: configPath,
3391
compilerOptions: {
3492
allowJs: true,
@@ -62,8 +120,7 @@ export class RepositoryParser {
62120
console.warn("parse tsconfig warning:", err.messageText)
63121
});
64122
}
65-
this.project.addSourceFilesAtPaths(parsedConfig.fileNames);
66-
// Get references
123+
project.addSourceFilesAtPaths(parsedConfig.fileNames);
67124
const references = parsedConfig.projectReferences;
68125
if (!references) {
69126
continue;
@@ -78,63 +135,43 @@ export class RepositoryParser {
78135
}
79136
}
80137
}
138+
return project;
81139
} else {
82-
// if tsconfig.json does not exist, use default configuration
83-
this.project = new Project({
84-
compilerOptions: {
85-
target: 99,
86-
module: 1,
87-
allowJs: true,
88-
checkJs: false,
89-
skipLibCheck: true,
90-
skipDefaultLibCheck: true,
91-
strict: false,
92-
noImplicitAny: false,
93-
strictNullChecks: false,
94-
strictFunctionTypes: false,
95-
strictBindCallApply: false,
96-
strictPropertyInitialization: false,
97-
noImplicitReturns: false,
98-
noFallthroughCasesInSwitch: false,
99-
noUncheckedIndexedAccess: false,
100-
noImplicitOverride: false,
101-
noPropertyAccessFromIndexSignature: false,
102-
allowUnusedLabels: false,
103-
allowUnreachableCode: false,
104-
exactOptionalPropertyTypes: false,
105-
noImplicitThis: false,
106-
alwaysStrict: false,
107-
noImplicitUseStrict: false,
108-
forceConsistentCasingInFileNames: true
109-
}
110-
});
140+
return this.createProjectWithDefaultConfig();
111141
}
112-
113-
this.moduleParser = new ModuleParser(this.project, projectRoot);
114142
}
115143

116-
117-
async parseRepository(repoPath: string, options: { loadExternalSymbols?: boolean, noDist?: boolean, srcPatterns?: string[] } = {}): Promise<Repository> {
118-
const absolutePath = path.resolve(repoPath);
119-
120-
const repository: Repository = {
121-
ASTVersion: "v0.1.3",
122-
id: path.basename(absolutePath),
123-
Modules: {},
124-
Graph: {}
125-
};
126-
127-
// Parse main module only
128-
const mainModule = await this.moduleParser.parseModule(absolutePath, '.', options);
129-
repository.Modules[mainModule.Name] = mainModule;
130-
131-
// Build global symbol graph
132-
this.buildGlobalGraph(repository);
133-
134-
return repository;
144+
private createProjectWithDefaultConfig(): Project {
145+
return new Project({
146+
compilerOptions: {
147+
target: 99,
148+
module: 1,
149+
allowJs: true,
150+
checkJs: false,
151+
skipLibCheck: true,
152+
skipDefaultLibCheck: true,
153+
strict: false,
154+
noImplicitAny: false,
155+
strictNullChecks: false,
156+
strictFunctionTypes: false,
157+
strictBindCallApply: false,
158+
strictPropertyInitialization: false,
159+
noImplicitReturns: false,
160+
noFallthroughCasesInSwitch: false,
161+
noUncheckedIndexedAccess: false,
162+
noImplicitOverride: false,
163+
noPropertyAccessFromIndexSignature: false,
164+
allowUnusedLabels: false,
165+
allowUnreachableCode: false,
166+
exactOptionalPropertyTypes: false,
167+
noImplicitThis: false,
168+
alwaysStrict: false,
169+
noImplicitUseStrict: false,
170+
forceConsistentCasingInFileNames: true
171+
}
172+
});
135173
}
136174

137-
138175
private buildGlobalGraph(repository: Repository): void {
139176
// First pass: Create all nodes from functions, types, and variables
140177
for (const [, module] of Object.entries(repository.Modules)) {

0 commit comments

Comments
 (0)