@principal-ai/codebase-composition 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/DependencyLayerModule.d.ts +44 -0
- package/dist/modules/DependencyLayerModule.d.ts.map +1 -0
- package/dist/modules/DependencyLayerModule.js +286 -0
- package/dist/modules/DependencyLayerModule.js.map +1 -0
- package/dist/modules/FileSystemModule.d.ts +30 -0
- package/dist/modules/FileSystemModule.d.ts.map +1 -0
- package/dist/modules/FileSystemModule.js +46 -0
- package/dist/modules/FileSystemModule.js.map +1 -0
- package/dist/modules/FileTypeLayerModule.d.ts +34 -0
- package/dist/modules/FileTypeLayerModule.d.ts.map +1 -0
- package/dist/modules/FileTypeLayerModule.js +169 -0
- package/dist/modules/FileTypeLayerModule.js.map +1 -0
- package/dist/modules/FrameworkLayerModule.d.ts +22 -0
- package/dist/modules/FrameworkLayerModule.d.ts.map +1 -0
- package/dist/modules/FrameworkLayerModule.js +388 -0
- package/dist/modules/FrameworkLayerModule.js.map +1 -0
- package/dist/modules/PackageLayerModule.d.ts +23 -0
- package/dist/modules/PackageLayerModule.d.ts.map +1 -0
- package/dist/modules/PackageLayerModule.js +810 -0
- package/dist/modules/PackageLayerModule.js.map +1 -0
- package/dist/modules/TypeExtractionModule.d.ts +37 -0
- package/dist/modules/TypeExtractionModule.d.ts.map +1 -0
- package/dist/modules/TypeExtractionModule.js +180 -0
- package/dist/modules/TypeExtractionModule.js.map +1 -0
- package/dist/modules/VersionControlLayerModule.d.ts +10 -0
- package/dist/modules/VersionControlLayerModule.d.ts.map +1 -0
- package/dist/modules/VersionControlLayerModule.js +32 -0
- package/dist/modules/VersionControlLayerModule.js.map +1 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/index.d.ts +4 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/index.d.ts.map +1 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/index.js +7 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/index.js.map +1 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/category.d.ts +15 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/category.d.ts.map +1 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/category.js +3 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/category.js.map +1 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/product.d.ts +34 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/product.d.ts.map +1 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/product.js +23 -0
- package/dist/modules/__fixtures__/typescript-packages/complex-types/src/models/product.js.map +1 -0
- package/dist/modules/__fixtures__/typescript-packages/simple-types/index.d.ts +39 -0
- package/dist/modules/__fixtures__/typescript-packages/simple-types/index.d.ts.map +1 -0
- package/dist/modules/__fixtures__/typescript-packages/simple-types/index.js +39 -0
- package/dist/modules/__fixtures__/typescript-packages/simple-types/index.js.map +1 -0
- package/dist/modules/extractors/TypeScriptExtractor.d.ts +18 -0
- package/dist/modules/extractors/TypeScriptExtractor.d.ts.map +1 -0
- package/dist/modules/extractors/TypeScriptExtractor.js +361 -0
- package/dist/modules/extractors/TypeScriptExtractor.js.map +1 -0
- package/dist/modules/index.d.ts +13 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +21 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/providers/GitVersionControlProvider.d.ts +108 -0
- package/dist/providers/GitVersionControlProvider.d.ts.map +1 -0
- package/dist/providers/GitVersionControlProvider.js +380 -0
- package/dist/providers/GitVersionControlProvider.js.map +1 -0
- package/dist/providers/PackageManagerApiProvider.d.ts +78 -0
- package/dist/providers/PackageManagerApiProvider.d.ts.map +1 -0
- package/dist/providers/PackageManagerApiProvider.js +14 -0
- package/dist/providers/PackageManagerApiProvider.js.map +1 -0
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +10 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/services/FilesystemService.d.ts +59 -0
- package/dist/services/FilesystemService.d.ts.map +1 -0
- package/dist/services/FilesystemService.js +391 -0
- package/dist/services/FilesystemService.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +7 -0
- package/dist/services/index.js.map +1 -0
- package/dist/types/file-system.d.ts +7 -0
- package/dist/types/file-system.d.ts.map +1 -0
- package/dist/types/file-system.js +7 -0
- package/dist/types/file-system.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/layer-types.d.ts +187 -0
- package/dist/types/layer-types.d.ts.map +1 -0
- package/dist/types/layer-types.js +7 -0
- package/dist/types/layer-types.js.map +1 -0
- package/dist/types/version-control-layer.d.ts +53 -0
- package/dist/types/version-control-layer.d.ts.map +1 -0
- package/dist/types/version-control-layer.js +3 -0
- package/dist/types/version-control-layer.js.map +1 -0
- package/dist/types/workspace-boundaries.d.ts +17 -0
- package/dist/types/workspace-boundaries.d.ts.map +1 -0
- package/dist/types/workspace-boundaries.js +7 -0
- package/dist/types/workspace-boundaries.js.map +1 -0
- package/package.json +42 -0
- package/src/index.ts +62 -0
- package/src/modules/DependencyLayerModule.ts +329 -0
- package/src/modules/FileSystemModule.ts +65 -0
- package/src/modules/FileTypeLayerModule.ts +199 -0
- package/src/modules/FrameworkLayerModule.ts +437 -0
- package/src/modules/PackageLayerModule.ts +979 -0
- package/src/modules/TypeExtractionModule.test.ts +340 -0
- package/src/modules/TypeExtractionModule.ts +180 -0
- package/src/modules/VersionControlLayerModule.ts +31 -0
- package/src/modules/__fixtures__/typescript-packages/complex-types/package.json +6 -0
- package/src/modules/__fixtures__/typescript-packages/complex-types/src/index.ts +6 -0
- package/src/modules/__fixtures__/typescript-packages/complex-types/src/models/category.ts +15 -0
- package/src/modules/__fixtures__/typescript-packages/complex-types/src/models/product.ts +48 -0
- package/src/modules/__fixtures__/typescript-packages/javascript-only/index.js +18 -0
- package/src/modules/__fixtures__/typescript-packages/javascript-only/package.json +5 -0
- package/src/modules/__fixtures__/typescript-packages/simple-types/index.ts +53 -0
- package/src/modules/__fixtures__/typescript-packages/simple-types/package.json +6 -0
- package/src/modules/extractors/README.md +55 -0
- package/src/modules/extractors/TypeScriptExtractor.ts +409 -0
- package/src/modules/index.ts +13 -0
- package/src/providers/GitVersionControlProvider.ts +500 -0
- package/src/providers/PackageManagerApiProvider.ts +108 -0
- package/src/providers/README.md +88 -0
- package/src/providers/index.ts +17 -0
- package/src/services/FilesystemService.ts +530 -0
- package/src/services/index.ts +2 -0
- package/src/types/file-system.ts +11 -0
- package/src/types/index.ts +24 -0
- package/src/types/layer-types.ts +264 -0
- package/src/types/version-control-layer.ts +87 -0
- package/src/types/workspace-boundaries.ts +17 -0
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
import * as TOML from 'js-toml';
|
|
2
|
+
import { parsePipRequirementsFile, Requirement } from 'pip-requirements-js';
|
|
3
|
+
|
|
4
|
+
import { FileTree } from '@principal-ai/repository-abstraction';
|
|
5
|
+
import { PackageLayer, FileSet, PackageCommand, ConfigFile } from '../types/layer-types';
|
|
6
|
+
import { WorkspaceBoundary } from '../types/workspace-boundaries';
|
|
7
|
+
|
|
8
|
+
// Type aliases for configuration objects
|
|
9
|
+
type ESLintConfig = unknown;
|
|
10
|
+
type PrettierConfig = unknown;
|
|
11
|
+
type JestConfig = unknown;
|
|
12
|
+
type BabelConfig = unknown;
|
|
13
|
+
|
|
14
|
+
// Dependency value types
|
|
15
|
+
type DependencyObject = {
|
|
16
|
+
version?: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
type DependencyValue = string | DependencyObject;
|
|
20
|
+
|
|
21
|
+
// Package manifest types
|
|
22
|
+
type PackageManifest = NodePackageJson | PyProjectToml | CargoToml;
|
|
23
|
+
|
|
24
|
+
interface NodePackageJson {
|
|
25
|
+
name?: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
dependencies?: Record<string, string>;
|
|
28
|
+
devDependencies?: Record<string, string>;
|
|
29
|
+
peerDependencies?: Record<string, string>;
|
|
30
|
+
scripts?: Record<string, string>;
|
|
31
|
+
workspaces?: string[] | { packages: string[] };
|
|
32
|
+
packageManager?: string;
|
|
33
|
+
eslintConfig?: ESLintConfig;
|
|
34
|
+
prettier?: PrettierConfig;
|
|
35
|
+
jest?: JestConfig;
|
|
36
|
+
babel?: BabelConfig;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface PyProjectToml {
|
|
40
|
+
tool?: {
|
|
41
|
+
poetry?: {
|
|
42
|
+
name?: string;
|
|
43
|
+
version?: string;
|
|
44
|
+
dependencies?: Record<string, DependencyValue>;
|
|
45
|
+
'dev-dependencies'?: Record<string, DependencyValue>;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
project?: {
|
|
49
|
+
name?: string;
|
|
50
|
+
version?: string;
|
|
51
|
+
dependencies?: string[];
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CargoToml {
|
|
56
|
+
package?: {
|
|
57
|
+
name?: string;
|
|
58
|
+
version?: string;
|
|
59
|
+
};
|
|
60
|
+
dependencies?: Record<string, DependencyValue>;
|
|
61
|
+
'dev-dependencies'?: Record<string, DependencyValue>;
|
|
62
|
+
workspace?: {
|
|
63
|
+
members?: string[];
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Generic parser interface
|
|
68
|
+
interface PackageManifestParser<T> {
|
|
69
|
+
manifestFileName: string;
|
|
70
|
+
packageType: 'node' | 'python' | 'cargo';
|
|
71
|
+
|
|
72
|
+
canParse(filename: string): boolean;
|
|
73
|
+
parseContent(rawContent: string): T | null;
|
|
74
|
+
extractPackageData(content: T, path: string): PackageLayer['packageData'] | null;
|
|
75
|
+
detectWorkspaces(content: T): string[] | null;
|
|
76
|
+
detectPackageManager(content: T, lockFiles: string[]): string;
|
|
77
|
+
detectConfigs?(
|
|
78
|
+
packagePath: string,
|
|
79
|
+
fileTree: FileTree,
|
|
80
|
+
manifestContent: T,
|
|
81
|
+
): PackageLayer['configFiles'];
|
|
82
|
+
detectDocsFolder?(
|
|
83
|
+
packagePath: string,
|
|
84
|
+
fileTree: FileTree,
|
|
85
|
+
manifestContent: T,
|
|
86
|
+
): string | undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Node.js package.json parser
|
|
90
|
+
class NodePackageParser implements PackageManifestParser<NodePackageJson> {
|
|
91
|
+
manifestFileName = 'package.json';
|
|
92
|
+
packageType = 'node' as const;
|
|
93
|
+
|
|
94
|
+
canParse(filename: string): boolean {
|
|
95
|
+
return filename.endsWith('package.json');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
parseContent(rawContent: string): NodePackageJson | null {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(rawContent) as NodePackageJson;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
extractPackageData(content: NodePackageJson, path: string): PackageLayer['packageData'] | null {
|
|
107
|
+
// Handle cases where path is just "package.json" (root package)
|
|
108
|
+
// or a proper path like "packages/foo/package.json"
|
|
109
|
+
let packagePath: string;
|
|
110
|
+
if (path === 'package.json') {
|
|
111
|
+
packagePath = ''; // Root directory - empty string for root
|
|
112
|
+
} else if (path.endsWith('/package.json')) {
|
|
113
|
+
packagePath = path.slice(0, -13); // Remove '/package.json'
|
|
114
|
+
} else {
|
|
115
|
+
// Shouldn't happen, but handle gracefully
|
|
116
|
+
packagePath = path;
|
|
117
|
+
}
|
|
118
|
+
const availableCommands = this.extractCommands(content, packagePath);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
name: content.name || 'unnamed',
|
|
122
|
+
version: content.version,
|
|
123
|
+
path: packagePath,
|
|
124
|
+
packageManager: 'unknown' as const,
|
|
125
|
+
dependencies: content.dependencies || {},
|
|
126
|
+
devDependencies: content.devDependencies || {},
|
|
127
|
+
peerDependencies: content.peerDependencies || {},
|
|
128
|
+
isMonorepoRoot: !!content.workspaces,
|
|
129
|
+
isWorkspace: false, // Will be determined by context
|
|
130
|
+
parentPackage: undefined,
|
|
131
|
+
availableCommands,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private extractCommands(content: NodePackageJson, packagePath: string): PackageCommand[] {
|
|
136
|
+
const commands: PackageCommand[] = [];
|
|
137
|
+
|
|
138
|
+
// Extract npm scripts
|
|
139
|
+
if (content.scripts) {
|
|
140
|
+
Object.entries(content.scripts).forEach(([name, script]) => {
|
|
141
|
+
// Use npm as default, will be updated with correct package manager later
|
|
142
|
+
commands.push({
|
|
143
|
+
name,
|
|
144
|
+
command: `npm run ${name}`,
|
|
145
|
+
description: script.length > 50 ? script.substring(0, 47) + '...' : script,
|
|
146
|
+
type: 'script',
|
|
147
|
+
workingDirectory: packagePath,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Add standard npm commands - these will also be updated with correct package manager
|
|
153
|
+
const standardCommands = [
|
|
154
|
+
{ name: 'install', command: 'npm install', description: 'Install dependencies' },
|
|
155
|
+
{ name: 'ci', command: 'npm ci', description: 'Clean install dependencies' },
|
|
156
|
+
{ name: 'update', command: 'npm update', description: 'Update dependencies' },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
standardCommands.forEach(cmd => {
|
|
160
|
+
commands.push({
|
|
161
|
+
...cmd,
|
|
162
|
+
type: 'standard',
|
|
163
|
+
workingDirectory: packagePath,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return commands;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
detectWorkspaces(content: NodePackageJson): string[] | null {
|
|
171
|
+
if (Array.isArray(content.workspaces)) {
|
|
172
|
+
return content.workspaces;
|
|
173
|
+
}
|
|
174
|
+
if (
|
|
175
|
+
content.workspaces &&
|
|
176
|
+
typeof content.workspaces === 'object' &&
|
|
177
|
+
'packages' in content.workspaces
|
|
178
|
+
) {
|
|
179
|
+
return content.workspaces.packages || null;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
detectPackageManager(content: NodePackageJson, lockFiles: string[]): string {
|
|
185
|
+
// Check for packageManager field (corepack)
|
|
186
|
+
if (content.packageManager) {
|
|
187
|
+
const pm = content.packageManager.split('@')[0];
|
|
188
|
+
if (['npm', 'yarn', 'pnpm'].includes(pm)) {
|
|
189
|
+
return pm;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check lock files
|
|
194
|
+
if (lockFiles.some((f: string) => f.endsWith('yarn.lock'))) return 'yarn';
|
|
195
|
+
if (lockFiles.some((f: string) => f.endsWith('pnpm-lock.yaml'))) return 'pnpm';
|
|
196
|
+
if (lockFiles.some((f: string) => f.endsWith('package-lock.json'))) return 'npm';
|
|
197
|
+
|
|
198
|
+
// Default to npm instead of unknown for Node packages
|
|
199
|
+
return 'npm';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
detectConfigs(
|
|
203
|
+
packagePath: string,
|
|
204
|
+
fileTree: FileTree,
|
|
205
|
+
manifestContent: NodePackageJson,
|
|
206
|
+
): PackageLayer['configFiles'] {
|
|
207
|
+
const configs: PackageLayer['configFiles'] = {};
|
|
208
|
+
|
|
209
|
+
// Define config patterns for Node/JavaScript projects
|
|
210
|
+
const configPatterns: Record<string, string[]> = {
|
|
211
|
+
knip: [
|
|
212
|
+
'knip.json',
|
|
213
|
+
'knip.jsonc',
|
|
214
|
+
'.knip.json',
|
|
215
|
+
'knip.config.js',
|
|
216
|
+
'knip.config.ts',
|
|
217
|
+
'knip.config.mjs',
|
|
218
|
+
],
|
|
219
|
+
eslint: [
|
|
220
|
+
'.eslintrc',
|
|
221
|
+
'.eslintrc.js',
|
|
222
|
+
'.eslintrc.json',
|
|
223
|
+
'.eslintrc.yml',
|
|
224
|
+
'.eslintrc.yaml',
|
|
225
|
+
'eslint.config.js',
|
|
226
|
+
'eslint.config.mjs',
|
|
227
|
+
'.eslintrc.cjs',
|
|
228
|
+
],
|
|
229
|
+
prettier: [
|
|
230
|
+
'.prettierrc',
|
|
231
|
+
'.prettierrc.js',
|
|
232
|
+
'.prettierrc.json',
|
|
233
|
+
'.prettierrc.yml',
|
|
234
|
+
'.prettierrc.yaml',
|
|
235
|
+
'prettier.config.js',
|
|
236
|
+
'.prettierrc.toml',
|
|
237
|
+
],
|
|
238
|
+
typescript: ['tsconfig.json', 'tsconfig.base.json'],
|
|
239
|
+
jest: [
|
|
240
|
+
'jest.config.js',
|
|
241
|
+
'jest.config.ts',
|
|
242
|
+
'jest.config.mjs',
|
|
243
|
+
'jest.config.cjs',
|
|
244
|
+
'jest.config.json',
|
|
245
|
+
],
|
|
246
|
+
vitest: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'],
|
|
247
|
+
webpack: ['webpack.config.js', 'webpack.config.ts', 'webpack.config.mjs'],
|
|
248
|
+
vite: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'],
|
|
249
|
+
rollup: ['rollup.config.js', 'rollup.config.ts', 'rollup.config.mjs'],
|
|
250
|
+
babel: ['.babelrc', '.babelrc.js', '.babelrc.json', 'babel.config.js', 'babel.config.json'],
|
|
251
|
+
dockerfile: ['Dockerfile', 'dockerfile', 'Dockerfile.dev', 'Dockerfile.prod'],
|
|
252
|
+
gitignore: ['.gitignore'],
|
|
253
|
+
editorconfig: ['.editorconfig'],
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Normalize package path
|
|
257
|
+
const normalizedPath =
|
|
258
|
+
packagePath === '.' || packagePath === ''
|
|
259
|
+
? ''
|
|
260
|
+
: packagePath.endsWith('/')
|
|
261
|
+
? packagePath
|
|
262
|
+
: packagePath + '/';
|
|
263
|
+
|
|
264
|
+
// Check files in tree
|
|
265
|
+
if (fileTree.allFiles) {
|
|
266
|
+
for (const [tool, patterns] of Object.entries(configPatterns)) {
|
|
267
|
+
for (const pattern of patterns) {
|
|
268
|
+
const fullPath = normalizedPath + pattern;
|
|
269
|
+
const fileExists = fileTree.allFiles.some(
|
|
270
|
+
f => f.path === fullPath || f.relativePath === fullPath,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (fileExists) {
|
|
274
|
+
configs[tool] = {
|
|
275
|
+
path: fullPath,
|
|
276
|
+
exists: true,
|
|
277
|
+
type: this.getConfigFileType(pattern),
|
|
278
|
+
};
|
|
279
|
+
break; // Found config for this tool
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check for inline configs in package.json
|
|
286
|
+
if (manifestContent) {
|
|
287
|
+
if (manifestContent.eslintConfig) {
|
|
288
|
+
configs.eslint = {
|
|
289
|
+
path: normalizedPath + 'package.json',
|
|
290
|
+
exists: true,
|
|
291
|
+
type: 'json',
|
|
292
|
+
isInline: true,
|
|
293
|
+
inlineField: 'eslintConfig',
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (manifestContent.prettier) {
|
|
298
|
+
configs.prettier = {
|
|
299
|
+
path: normalizedPath + 'package.json',
|
|
300
|
+
exists: true,
|
|
301
|
+
type: 'json',
|
|
302
|
+
isInline: true,
|
|
303
|
+
inlineField: 'prettier',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (manifestContent.jest) {
|
|
308
|
+
configs.jest = {
|
|
309
|
+
path: normalizedPath + 'package.json',
|
|
310
|
+
exists: true,
|
|
311
|
+
type: 'json',
|
|
312
|
+
isInline: true,
|
|
313
|
+
inlineField: 'jest',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (manifestContent.babel) {
|
|
318
|
+
configs.babel = {
|
|
319
|
+
path: normalizedPath + 'package.json',
|
|
320
|
+
exists: true,
|
|
321
|
+
type: 'json',
|
|
322
|
+
isInline: true,
|
|
323
|
+
inlineField: 'babel',
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return configs;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private getConfigFileType(filename: string): ConfigFile['type'] {
|
|
332
|
+
if (filename.endsWith('.json') || filename.endsWith('.jsonc')) return 'json';
|
|
333
|
+
if (filename.endsWith('.yml') || filename.endsWith('.yaml')) return 'yaml';
|
|
334
|
+
if (filename.endsWith('.toml')) return 'toml';
|
|
335
|
+
if (filename.endsWith('.js') || filename.endsWith('.mjs') || filename.endsWith('.cjs'))
|
|
336
|
+
return 'js';
|
|
337
|
+
if (filename.endsWith('.ts') || filename.endsWith('.mts') || filename.endsWith('.cts'))
|
|
338
|
+
return 'ts';
|
|
339
|
+
if (filename.endsWith('.ini')) return 'ini';
|
|
340
|
+
return 'custom';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
detectDocsFolder(
|
|
344
|
+
packagePath: string,
|
|
345
|
+
fileTree: FileTree,
|
|
346
|
+
_manifestContent: NodePackageJson,
|
|
347
|
+
): string | undefined {
|
|
348
|
+
// Look for common documentation folder names
|
|
349
|
+
const docsFolderNames = ['docs', 'documentation', 'doc'];
|
|
350
|
+
|
|
351
|
+
// Normalize package path
|
|
352
|
+
let packagePrefix = packagePath === '.' || packagePath === '' ? '' : packagePath;
|
|
353
|
+
if (packagePrefix && !packagePrefix.endsWith('/')) {
|
|
354
|
+
packagePrefix += '/';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!fileTree.allFiles) return undefined;
|
|
358
|
+
|
|
359
|
+
// Look for root-level docs folders first (preferred)
|
|
360
|
+
for (const folderName of docsFolderNames) {
|
|
361
|
+
const expectedPath = packagePrefix + folderName;
|
|
362
|
+
const hasFiles = fileTree.allFiles.some(file =>
|
|
363
|
+
file.path?.toLowerCase().startsWith(expectedPath.toLowerCase() + '/'),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
if (hasFiles) {
|
|
367
|
+
// Find the actual folder name with correct case
|
|
368
|
+
const actualPath = fileTree.allFiles.find(file =>
|
|
369
|
+
file.path?.toLowerCase().startsWith(expectedPath.toLowerCase() + '/'),
|
|
370
|
+
)?.path;
|
|
371
|
+
|
|
372
|
+
if (actualPath) {
|
|
373
|
+
const actualFolderPath = actualPath.substring(
|
|
374
|
+
0,
|
|
375
|
+
actualPath.indexOf('/', packagePrefix.length),
|
|
376
|
+
);
|
|
377
|
+
return actualFolderPath.substring(packagePrefix.length);
|
|
378
|
+
}
|
|
379
|
+
return folderName; // Fallback to lowercase version
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// If no explicit docs folder found, look for directories with high concentration of docs
|
|
384
|
+
const potentialFolders = new Map<string, { docFiles: number; totalFiles: number }>();
|
|
385
|
+
|
|
386
|
+
for (const file of fileTree.allFiles) {
|
|
387
|
+
if (!file.path) continue;
|
|
388
|
+
|
|
389
|
+
// Only consider files within this package
|
|
390
|
+
if (packagePrefix !== '' && !file.path.startsWith(packagePrefix)) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const relativePath =
|
|
395
|
+
packagePrefix === '' ? file.path : file.path.substring(packagePrefix.length);
|
|
396
|
+
const pathParts = relativePath.split('/');
|
|
397
|
+
|
|
398
|
+
// Only look at direct subdirectories (depth 1)
|
|
399
|
+
if (pathParts.length === 2) {
|
|
400
|
+
const dirName = pathParts[0];
|
|
401
|
+
const fileName = pathParts[1];
|
|
402
|
+
|
|
403
|
+
if (!potentialFolders.has(dirName)) {
|
|
404
|
+
potentialFolders.set(dirName, { docFiles: 0, totalFiles: 0 });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const stats = potentialFolders.get(dirName)!;
|
|
408
|
+
stats.totalFiles++;
|
|
409
|
+
|
|
410
|
+
if (this.isDocumentationFile(fileName)) {
|
|
411
|
+
stats.docFiles++;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Find the best candidate folder (highest percentage of docs, minimum 2 doc files)
|
|
417
|
+
let bestFolder: string | undefined;
|
|
418
|
+
let bestScore = 0.5; // Must be at least 50% docs
|
|
419
|
+
|
|
420
|
+
for (const [folderName, stats] of Array.from(potentialFolders.entries())) {
|
|
421
|
+
if (stats.docFiles >= 2) {
|
|
422
|
+
const score = stats.docFiles / stats.totalFiles;
|
|
423
|
+
if (score > bestScore) {
|
|
424
|
+
bestScore = score;
|
|
425
|
+
bestFolder = folderName;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return bestFolder;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private isDocumentationFile(fileName: string): boolean {
|
|
434
|
+
const lowerName = fileName.toLowerCase();
|
|
435
|
+
|
|
436
|
+
// Documentation file extensions
|
|
437
|
+
const docExtensions = ['.md', '.mdx', '.rst', '.txt', '.adoc', '.asciidoc'];
|
|
438
|
+
const hasDocExtension = docExtensions.some(ext => lowerName.endsWith(ext));
|
|
439
|
+
|
|
440
|
+
if (hasDocExtension) return true;
|
|
441
|
+
|
|
442
|
+
// Documentation file name patterns
|
|
443
|
+
const docPatterns = [
|
|
444
|
+
/^readme/i,
|
|
445
|
+
/^changelog/i,
|
|
446
|
+
/^changes/i,
|
|
447
|
+
/^history/i,
|
|
448
|
+
/^license/i,
|
|
449
|
+
/^copying/i,
|
|
450
|
+
/^install/i,
|
|
451
|
+
/^usage/i,
|
|
452
|
+
/^guide/i,
|
|
453
|
+
/^tutorial/i,
|
|
454
|
+
/^manual/i,
|
|
455
|
+
/^faq/i,
|
|
456
|
+
/^api/i,
|
|
457
|
+
/^reference/i,
|
|
458
|
+
/^spec/i,
|
|
459
|
+
/^specification/i,
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
return docPatterns.some(pattern => pattern.test(fileName));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Python pyproject.toml parser - simplified version
|
|
467
|
+
class PythonPackageParser implements PackageManifestParser<PyProjectToml> {
|
|
468
|
+
manifestFileName = 'pyproject.toml';
|
|
469
|
+
packageType = 'python' as const;
|
|
470
|
+
|
|
471
|
+
canParse(filename: string): boolean {
|
|
472
|
+
return (
|
|
473
|
+
filename.endsWith('pyproject.toml') ||
|
|
474
|
+
filename.endsWith('setup.py') ||
|
|
475
|
+
filename.endsWith('requirements.txt')
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
parseContent(rawContent: string): PyProjectToml | null {
|
|
480
|
+
try {
|
|
481
|
+
return TOML.load(rawContent) as PyProjectToml;
|
|
482
|
+
} catch {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
extractPackageData(content: PyProjectToml, path: string): PackageLayer['packageData'] | null {
|
|
488
|
+
// Extract package directory path from manifest path
|
|
489
|
+
let packagePath: string;
|
|
490
|
+
const manifestName = path.split('/').pop() || '';
|
|
491
|
+
if (path === manifestName) {
|
|
492
|
+
// Just the filename, package is at root
|
|
493
|
+
packagePath = ''; // Empty string for root
|
|
494
|
+
} else {
|
|
495
|
+
// Remove the manifest filename from path
|
|
496
|
+
packagePath = path.substring(0, path.lastIndexOf('/'));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Handle pyproject.toml structure
|
|
500
|
+
const poetryData = content.tool?.poetry;
|
|
501
|
+
const projectData = content.project;
|
|
502
|
+
|
|
503
|
+
const name = poetryData?.name || projectData?.name || 'unnamed';
|
|
504
|
+
const version = poetryData?.version || projectData?.version;
|
|
505
|
+
|
|
506
|
+
// Extract dependencies
|
|
507
|
+
const dependencies: Record<string, string> = {};
|
|
508
|
+
const devDependencies: Record<string, string> = {};
|
|
509
|
+
|
|
510
|
+
// Poetry format
|
|
511
|
+
if (poetryData?.dependencies) {
|
|
512
|
+
Object.entries(poetryData.dependencies).forEach(([key, value]) => {
|
|
513
|
+
if (key !== 'python') {
|
|
514
|
+
dependencies[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (poetryData?.['dev-dependencies']) {
|
|
520
|
+
Object.entries(poetryData['dev-dependencies']).forEach(([key, value]) => {
|
|
521
|
+
devDependencies[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// PEP 621 format
|
|
526
|
+
if (projectData?.dependencies && Array.isArray(projectData.dependencies)) {
|
|
527
|
+
try {
|
|
528
|
+
// Use pip-requirements-js to properly parse PEP 508 dependency specifications
|
|
529
|
+
const depString = projectData.dependencies.join('\n');
|
|
530
|
+
const requirements = parsePipRequirementsFile(depString);
|
|
531
|
+
|
|
532
|
+
requirements.forEach((req: Requirement) => {
|
|
533
|
+
if (req.type === 'ProjectName' && req.name) {
|
|
534
|
+
let version = '*';
|
|
535
|
+
if (req.versionSpec && req.versionSpec.length > 0) {
|
|
536
|
+
// Use the first version spec for simplicity
|
|
537
|
+
const firstSpec = req.versionSpec[0];
|
|
538
|
+
version = firstSpec.version || '*';
|
|
539
|
+
}
|
|
540
|
+
dependencies[req.name] = version;
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
} catch (error) {
|
|
544
|
+
console.warn(
|
|
545
|
+
'Failed to parse PEP 621 dependencies with pip-requirements-js, falling back to simple parsing:',
|
|
546
|
+
error,
|
|
547
|
+
);
|
|
548
|
+
// Fallback to simple parsing if pip-requirements-js fails
|
|
549
|
+
projectData.dependencies.forEach((dep: string) => {
|
|
550
|
+
const cleanDep = dep.trim();
|
|
551
|
+
const spaceIndex = cleanDep.indexOf(' ');
|
|
552
|
+
if (spaceIndex > 0) {
|
|
553
|
+
dependencies[cleanDep.substring(0, spaceIndex)] = '*';
|
|
554
|
+
} else {
|
|
555
|
+
dependencies[cleanDep] = '*';
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const packageManager = this.detectPythonPackageManager(content);
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
name,
|
|
565
|
+
version,
|
|
566
|
+
path: packagePath,
|
|
567
|
+
packageManager,
|
|
568
|
+
dependencies,
|
|
569
|
+
devDependencies,
|
|
570
|
+
peerDependencies: {},
|
|
571
|
+
isMonorepoRoot: false,
|
|
572
|
+
isWorkspace: false,
|
|
573
|
+
parentPackage: undefined,
|
|
574
|
+
availableCommands: [],
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
detectWorkspaces(_content: PyProjectToml): string[] | null {
|
|
579
|
+
// Python doesn't have built-in workspace support like npm
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
detectPackageManager(content: PyProjectToml, _lockFiles: string[]): string {
|
|
584
|
+
return this.detectPythonPackageManager(content);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private detectPythonPackageManager(
|
|
588
|
+
content: PyProjectToml,
|
|
589
|
+
): 'pip' | 'poetry' | 'pipenv' | 'unknown' {
|
|
590
|
+
// Check for Poetry
|
|
591
|
+
if (content.tool?.poetry) return 'poetry';
|
|
592
|
+
|
|
593
|
+
// Default to pip
|
|
594
|
+
return 'pip';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
detectDocsFolder(
|
|
598
|
+
packagePath: string,
|
|
599
|
+
fileTree: FileTree,
|
|
600
|
+
_manifestContent: PyProjectToml,
|
|
601
|
+
): string | undefined {
|
|
602
|
+
// Reuse the same logic as NodePackageParser
|
|
603
|
+
const nodeParser = new NodePackageParser();
|
|
604
|
+
return nodeParser.detectDocsFolder(packagePath, fileTree, {} as NodePackageJson);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Rust Cargo.toml parser - simplified version
|
|
609
|
+
class CargoPackageParser implements PackageManifestParser<CargoToml> {
|
|
610
|
+
manifestFileName = 'Cargo.toml';
|
|
611
|
+
packageType = 'cargo' as const;
|
|
612
|
+
|
|
613
|
+
canParse(filename: string): boolean {
|
|
614
|
+
return filename.endsWith('Cargo.toml');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
parseContent(rawContent: string): CargoToml | null {
|
|
618
|
+
try {
|
|
619
|
+
return TOML.load(rawContent) as CargoToml;
|
|
620
|
+
} catch {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
extractPackageData(content: CargoToml, path: string): PackageLayer['packageData'] | null {
|
|
626
|
+
const packageData = content.package;
|
|
627
|
+
if (!packageData) return null;
|
|
628
|
+
|
|
629
|
+
// Extract package directory path from manifest path
|
|
630
|
+
let packagePath: string;
|
|
631
|
+
if (path === 'Cargo.toml') {
|
|
632
|
+
packagePath = ''; // Root directory - empty string for root
|
|
633
|
+
} else if (path.endsWith('/Cargo.toml')) {
|
|
634
|
+
packagePath = path.slice(0, -11); // Remove '/Cargo.toml'
|
|
635
|
+
} else {
|
|
636
|
+
// Shouldn't happen, but handle gracefully
|
|
637
|
+
packagePath = path;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Extract dependencies
|
|
641
|
+
const dependencies: Record<string, string> = {};
|
|
642
|
+
const devDependencies: Record<string, string> = {};
|
|
643
|
+
|
|
644
|
+
if (content.dependencies) {
|
|
645
|
+
Object.entries(content.dependencies).forEach(([key, value]) => {
|
|
646
|
+
dependencies[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (content['dev-dependencies']) {
|
|
651
|
+
Object.entries(content['dev-dependencies']).forEach(([key, value]) => {
|
|
652
|
+
devDependencies[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
name: packageData.name || 'unnamed',
|
|
658
|
+
version: packageData.version,
|
|
659
|
+
path: packagePath,
|
|
660
|
+
packageManager: 'cargo' as const,
|
|
661
|
+
dependencies,
|
|
662
|
+
devDependencies,
|
|
663
|
+
peerDependencies: {},
|
|
664
|
+
isMonorepoRoot: !!content.workspace,
|
|
665
|
+
isWorkspace: false,
|
|
666
|
+
parentPackage: undefined,
|
|
667
|
+
availableCommands: [],
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
detectWorkspaces(content: CargoToml): string[] | null {
|
|
672
|
+
return content.workspace?.members || null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
detectPackageManager(): string {
|
|
676
|
+
return 'cargo';
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
detectDocsFolder(
|
|
680
|
+
packagePath: string,
|
|
681
|
+
fileTree: FileTree,
|
|
682
|
+
_manifestContent: CargoToml,
|
|
683
|
+
): string | undefined {
|
|
684
|
+
// Reuse the same logic as NodePackageParser
|
|
685
|
+
const nodeParser = new NodePackageParser();
|
|
686
|
+
return nodeParser.detectDocsFolder(packagePath, fileTree, {} as NodePackageJson);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export class PackageLayerModule {
|
|
691
|
+
private parsers: PackageManifestParser<PackageManifest>[] = [
|
|
692
|
+
new NodePackageParser(),
|
|
693
|
+
new PythonPackageParser(),
|
|
694
|
+
new CargoPackageParser(),
|
|
695
|
+
];
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Extract package information from the file tree
|
|
699
|
+
* @param fileTree - The file tree to analyze
|
|
700
|
+
* @param fileReader - Optional function to read file contents. If not provided, manifest detection only.
|
|
701
|
+
*/
|
|
702
|
+
async discoverPackages(
|
|
703
|
+
fileTree: FileTree,
|
|
704
|
+
fileReader?: (path: string) => Promise<string>,
|
|
705
|
+
): Promise<PackageLayer[]> {
|
|
706
|
+
const packages: PackageLayer[] = [];
|
|
707
|
+
|
|
708
|
+
if (!fileTree.allFiles) return packages;
|
|
709
|
+
|
|
710
|
+
// Find all package manifest files
|
|
711
|
+
const manifestFiles = fileTree.allFiles.filter(file => {
|
|
712
|
+
if (!file.path) return false;
|
|
713
|
+
return this.parsers.some(parser => parser.canParse(file.path));
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// Get all files for lock file detection
|
|
717
|
+
const allFilePaths = fileTree.allFiles.map(f => f.path).filter(Boolean) as string[];
|
|
718
|
+
|
|
719
|
+
for (const manifestFile of manifestFiles) {
|
|
720
|
+
if (!manifestFile.path) continue;
|
|
721
|
+
|
|
722
|
+
// Find appropriate parser
|
|
723
|
+
const parser = this.parsers.find(p => p.canParse(manifestFile.path));
|
|
724
|
+
if (!parser) continue;
|
|
725
|
+
|
|
726
|
+
// Get manifest content
|
|
727
|
+
let content: PackageManifest | null = null;
|
|
728
|
+
|
|
729
|
+
if (fileReader) {
|
|
730
|
+
try {
|
|
731
|
+
const fileContent = await fileReader(manifestFile.path);
|
|
732
|
+
if (fileContent) {
|
|
733
|
+
// Use the parser's parseContent method for type-safe parsing
|
|
734
|
+
content = parser.parseContent(fileContent);
|
|
735
|
+
}
|
|
736
|
+
} catch (error) {
|
|
737
|
+
console.warn(`Could not read or parse ${manifestFile.path}:`, error);
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
} else {
|
|
741
|
+
// If no file reader provided, create minimal package info from path
|
|
742
|
+
const packageName = manifestFile.path.split('/').slice(-2, -1)[0] || 'unnamed';
|
|
743
|
+
content = this.createMinimalManifest(parser.packageType, packageName);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (!content) continue;
|
|
747
|
+
|
|
748
|
+
// Extract package data
|
|
749
|
+
const packageData = parser.extractPackageData(content, manifestFile.path);
|
|
750
|
+
if (!packageData) {
|
|
751
|
+
console.warn(
|
|
752
|
+
`[PackageLayerModule] extractPackageData returned null for ${manifestFile.path}`,
|
|
753
|
+
{ content, parser: parser.packageType },
|
|
754
|
+
);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Detect package manager
|
|
759
|
+
const dirPath = manifestFile.path.substring(0, manifestFile.path.lastIndexOf('/'));
|
|
760
|
+
const lockFilesInDir = allFilePaths.filter(f => f.startsWith(dirPath) && this.isLockFile(f));
|
|
761
|
+
packageData.packageManager = parser.detectPackageManager(
|
|
762
|
+
content,
|
|
763
|
+
lockFilesInDir,
|
|
764
|
+
) as PackageLayer['packageData']['packageManager'];
|
|
765
|
+
|
|
766
|
+
// Update commands with correct package manager for Node packages
|
|
767
|
+
if (parser.packageType === 'node' && packageData.availableCommands) {
|
|
768
|
+
// Only replace if we detected a valid package manager
|
|
769
|
+
if (
|
|
770
|
+
packageData.packageManager &&
|
|
771
|
+
packageData.packageManager !== 'unknown' &&
|
|
772
|
+
packageData.packageManager !== 'npm'
|
|
773
|
+
) {
|
|
774
|
+
packageData.availableCommands = packageData.availableCommands.map(cmd => {
|
|
775
|
+
// Update both script and standard commands
|
|
776
|
+
if (cmd.command.includes('npm ')) {
|
|
777
|
+
let pmCommand = cmd.command;
|
|
778
|
+
|
|
779
|
+
// Handle different package manager syntaxes
|
|
780
|
+
if (packageData.packageManager === 'yarn') {
|
|
781
|
+
pmCommand = pmCommand
|
|
782
|
+
.replace('npm run', 'yarn')
|
|
783
|
+
.replace('npm install', 'yarn install')
|
|
784
|
+
.replace('npm ci', 'yarn install --frozen-lockfile')
|
|
785
|
+
.replace('npm update', 'yarn upgrade');
|
|
786
|
+
} else if (packageData.packageManager === 'pnpm') {
|
|
787
|
+
pmCommand = pmCommand.replace('npm', 'pnpm');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return { ...cmd, command: pmCommand };
|
|
791
|
+
}
|
|
792
|
+
return cmd;
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Create file set for the manifest
|
|
798
|
+
const fileSet: FileSet = {
|
|
799
|
+
id: `package-manifest-${packageData.path}`,
|
|
800
|
+
name: parser.manifestFileName,
|
|
801
|
+
patterns: [
|
|
802
|
+
{
|
|
803
|
+
type: 'exact',
|
|
804
|
+
pattern: manifestFile.path,
|
|
805
|
+
description: `${parser.packageType} package manifest`,
|
|
806
|
+
},
|
|
807
|
+
],
|
|
808
|
+
matchedFiles: [manifestFile.path],
|
|
809
|
+
fileCount: 1,
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// Detect configuration files if the parser supports it
|
|
813
|
+
let configFiles: PackageLayer['configFiles'] | undefined;
|
|
814
|
+
if (parser.detectConfigs) {
|
|
815
|
+
configFiles = parser.detectConfigs(packageData.path, fileTree, content);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Detect documentation folder if the parser supports it
|
|
819
|
+
let docsFolder: string | undefined;
|
|
820
|
+
if (parser.detectDocsFolder) {
|
|
821
|
+
docsFolder = parser.detectDocsFolder(packageData.path, fileTree, content);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Create package layer
|
|
825
|
+
const layer: PackageLayer = {
|
|
826
|
+
id: `package-${parser.packageType}-${packageData.path.replace(/[^a-zA-Z0-9-]/g, '-')}`,
|
|
827
|
+
name: packageData.name,
|
|
828
|
+
type: parser.packageType,
|
|
829
|
+
enabled: true,
|
|
830
|
+
derivedFrom: {
|
|
831
|
+
fileSets: [fileSet],
|
|
832
|
+
derivationType: 'content',
|
|
833
|
+
description: `${parser.packageType} package defined in ${parser.manifestFileName}`,
|
|
834
|
+
contentExtraction: {
|
|
835
|
+
method: 'parse',
|
|
836
|
+
parser: {
|
|
837
|
+
format: parser.manifestFileName.endsWith('.json') ? 'json' : 'toml',
|
|
838
|
+
paths:
|
|
839
|
+
parser.packageType === 'node'
|
|
840
|
+
? ['']
|
|
841
|
+
: parser.packageType === 'python'
|
|
842
|
+
? ['tool.poetry', 'project']
|
|
843
|
+
: ['package'],
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
packageData,
|
|
848
|
+
configFiles,
|
|
849
|
+
docsFolder,
|
|
850
|
+
pillar: 'foundationHealth',
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
packages.push(layer);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Determine workspace relationships
|
|
857
|
+
this.resolveWorkspaceRelationships(packages);
|
|
858
|
+
|
|
859
|
+
return packages;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Create package layers from workspace boundaries (backwards compatibility)
|
|
864
|
+
*/
|
|
865
|
+
createPackageLayersFromBoundaries(boundaries: WorkspaceBoundary[]): PackageLayer[] {
|
|
866
|
+
return boundaries.map(boundary => {
|
|
867
|
+
const fileSet: FileSet = {
|
|
868
|
+
id: `package-manifest-${boundary.rootPath || 'root'}`,
|
|
869
|
+
name: 'package.json',
|
|
870
|
+
patterns: [
|
|
871
|
+
{
|
|
872
|
+
type: 'exact',
|
|
873
|
+
pattern: boundary.packageJsonPath,
|
|
874
|
+
description: 'Node.js package manifest',
|
|
875
|
+
},
|
|
876
|
+
],
|
|
877
|
+
matchedFiles: [boundary.packageJsonPath],
|
|
878
|
+
fileCount: 1,
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const packageData: PackageLayer['packageData'] = {
|
|
882
|
+
name: boundary.packageData?.name || boundary.name,
|
|
883
|
+
version: boundary.packageData?.version,
|
|
884
|
+
path: boundary.rootPath || '.',
|
|
885
|
+
packageManager: 'unknown',
|
|
886
|
+
dependencies: {},
|
|
887
|
+
devDependencies: {},
|
|
888
|
+
peerDependencies: {},
|
|
889
|
+
isMonorepoRoot: !!boundary.packageData?.workspaces,
|
|
890
|
+
isWorkspace: !boundary.isRoot && boundaries.length > 1,
|
|
891
|
+
parentPackage: boundary.isRoot ? undefined : 'root',
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const layer: PackageLayer = {
|
|
895
|
+
id: `package-node-${boundary.id}`,
|
|
896
|
+
name: packageData.name,
|
|
897
|
+
type: 'node',
|
|
898
|
+
enabled: true,
|
|
899
|
+
derivedFrom: {
|
|
900
|
+
fileSets: [fileSet],
|
|
901
|
+
derivationType: 'content',
|
|
902
|
+
description: 'Node.js package defined in package.json',
|
|
903
|
+
},
|
|
904
|
+
packageData,
|
|
905
|
+
pillar: 'foundationHealth',
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
return layer;
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
private isLockFile(path: string): boolean {
|
|
913
|
+
const lockFiles = [
|
|
914
|
+
'package-lock.json',
|
|
915
|
+
'yarn.lock',
|
|
916
|
+
'pnpm-lock.yaml',
|
|
917
|
+
'poetry.lock',
|
|
918
|
+
'Pipfile.lock',
|
|
919
|
+
'Cargo.lock',
|
|
920
|
+
];
|
|
921
|
+
|
|
922
|
+
return lockFiles.some(lock => path.endsWith(lock));
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private resolveWorkspaceRelationships(packages: PackageLayer[]): void {
|
|
926
|
+
// Find root packages (typically at the repository root)
|
|
927
|
+
const _rootPackages = packages.filter(
|
|
928
|
+
p => p.packageData.path === '.' || p.packageData.path === '',
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
// Mark workspace packages
|
|
932
|
+
packages.forEach(pkg => {
|
|
933
|
+
if (pkg.packageData.path !== '.' && pkg.packageData.path !== '') {
|
|
934
|
+
pkg.packageData.isWorkspace = true;
|
|
935
|
+
|
|
936
|
+
// Find parent package
|
|
937
|
+
const parentPath = pkg.packageData.path.includes('/')
|
|
938
|
+
? pkg.packageData.path.substring(0, pkg.packageData.path.lastIndexOf('/'))
|
|
939
|
+
: '.';
|
|
940
|
+
|
|
941
|
+
const parent = packages.find(p => p.packageData.path === parentPath);
|
|
942
|
+
if (parent) {
|
|
943
|
+
pkg.packageData.parentPackage = parent.packageData.name;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Create a minimal manifest for when file content isn't available
|
|
951
|
+
*/
|
|
952
|
+
private createMinimalManifest(packageType: string, packageName: string): PackageManifest {
|
|
953
|
+
switch (packageType) {
|
|
954
|
+
case 'node':
|
|
955
|
+
return {
|
|
956
|
+
name: packageName,
|
|
957
|
+
version: '0.0.0',
|
|
958
|
+
dependencies: {},
|
|
959
|
+
devDependencies: {},
|
|
960
|
+
};
|
|
961
|
+
case 'python':
|
|
962
|
+
return {
|
|
963
|
+
project: {
|
|
964
|
+
name: packageName,
|
|
965
|
+
version: '0.0.0',
|
|
966
|
+
},
|
|
967
|
+
};
|
|
968
|
+
case 'cargo':
|
|
969
|
+
return {
|
|
970
|
+
package: {
|
|
971
|
+
name: packageName,
|
|
972
|
+
version: '0.0.0',
|
|
973
|
+
},
|
|
974
|
+
};
|
|
975
|
+
default:
|
|
976
|
+
return {};
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|