@openrewrite/rewrite 8.70.0-20251219-180817 → 8.70.0-20251219-215811
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/dist/cli/cli-utils.d.ts +6 -6
- package/dist/cli/cli-utils.d.ts.map +1 -1
- package/dist/cli/cli-utils.js +50 -228
- package/dist/cli/cli-utils.js.map +1 -1
- package/dist/javascript/assertions.d.ts.map +1 -1
- package/dist/javascript/assertions.js +87 -12
- package/dist/javascript/assertions.js.map +1 -1
- package/dist/javascript/autodetect.d.ts +11 -11
- package/dist/javascript/autodetect.d.ts.map +1 -1
- package/dist/javascript/autodetect.js +18 -21
- package/dist/javascript/autodetect.js.map +1 -1
- package/dist/javascript/format/prettier-config-loader.d.ts.map +1 -1
- package/dist/javascript/format/prettier-config-loader.js +1 -1
- package/dist/javascript/format/prettier-config-loader.js.map +1 -1
- package/dist/javascript/index.d.ts +1 -0
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +1 -0
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/markers.d.ts.map +1 -1
- package/dist/javascript/markers.js +135 -6
- package/dist/javascript/markers.js.map +1 -1
- package/dist/javascript/node-resolution-result.d.ts +4 -1
- package/dist/javascript/node-resolution-result.d.ts.map +1 -1
- package/dist/javascript/node-resolution-result.js +22 -1
- package/dist/javascript/node-resolution-result.js.map +1 -1
- package/dist/javascript/package-json-parser.d.ts +7 -0
- package/dist/javascript/package-json-parser.d.ts.map +1 -1
- package/dist/javascript/package-json-parser.js +19 -1
- package/dist/javascript/package-json-parser.js.map +1 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +1 -13
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/preconditions.js +4 -4
- package/dist/javascript/preconditions.js.map +1 -1
- package/dist/javascript/project-parser.d.ts +137 -0
- package/dist/javascript/project-parser.d.ts.map +1 -0
- package/dist/javascript/project-parser.js +726 -0
- package/dist/javascript/project-parser.js.map +1 -0
- package/dist/javascript/style.d.ts +9 -26
- package/dist/javascript/style.d.ts.map +1 -1
- package/dist/javascript/style.js +18 -42
- package/dist/javascript/style.js.map +1 -1
- package/dist/json/parser.d.ts.map +1 -1
- package/dist/json/parser.js +1 -0
- package/dist/json/parser.js.map +1 -1
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +1 -1
- package/dist/markers.js.map +1 -1
- package/dist/parser.d.ts +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/rpc/index.d.ts +0 -1
- package/dist/rpc/index.d.ts.map +1 -1
- package/dist/rpc/index.js +4 -2
- package/dist/rpc/index.js.map +1 -1
- package/dist/rpc/request/index.d.ts +1 -0
- package/dist/rpc/request/index.d.ts.map +1 -1
- package/dist/rpc/request/index.js +1 -0
- package/dist/rpc/request/index.js.map +1 -1
- package/dist/rpc/request/parse-project.d.ts +25 -0
- package/dist/rpc/request/parse-project.d.ts.map +1 -0
- package/dist/rpc/request/parse-project.js +304 -0
- package/dist/rpc/request/parse-project.js.map +1 -0
- package/dist/rpc/rewrite-rpc.d.ts.map +1 -1
- package/dist/rpc/rewrite-rpc.js +1 -0
- package/dist/rpc/rewrite-rpc.js.map +1 -1
- package/dist/text/parser.d.ts.map +1 -1
- package/dist/text/parser.js +1 -0
- package/dist/text/parser.js.map +1 -1
- package/dist/version.txt +1 -1
- package/dist/yaml/parser.d.ts.map +1 -1
- package/dist/yaml/parser.js +52 -4
- package/dist/yaml/parser.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/cli-utils.ts +46 -237
- package/src/javascript/assertions.ts +74 -10
- package/src/javascript/autodetect.ts +22 -15
- package/src/javascript/format/prettier-config-loader.ts +2 -2
- package/src/javascript/index.ts +1 -0
- package/src/javascript/markers.ts +157 -7
- package/src/javascript/node-resolution-result.ts +23 -2
- package/src/javascript/package-json-parser.ts +19 -1
- package/src/javascript/parser.ts +1 -16
- package/src/javascript/preconditions.ts +1 -1
- package/src/javascript/project-parser.ts +657 -0
- package/src/javascript/style.ts +43 -28
- package/src/json/parser.ts +3 -1
- package/src/markers.ts +1 -1
- package/src/parser.ts +1 -1
- package/src/rpc/index.ts +7 -5
- package/src/rpc/request/index.ts +1 -0
- package/src/rpc/request/parse-project.ts +283 -0
- package/src/rpc/rewrite-rpc.ts +2 -0
- package/src/text/parser.ts +3 -1
- package/src/yaml/parser.ts +53 -5
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 the original author or authors.
|
|
3
|
+
* <p>
|
|
4
|
+
* Licensed under the Moderne Source Available License (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
* <p>
|
|
8
|
+
* https://docs.moderne.io/licensing/moderne-source-available-license
|
|
9
|
+
* <p>
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as fsp from "fs/promises";
|
|
19
|
+
import {spawnSync} from "child_process";
|
|
20
|
+
import picomatch from "picomatch";
|
|
21
|
+
import {produce} from "immer";
|
|
22
|
+
import {SourceFile} from "../tree";
|
|
23
|
+
import {Parsers} from "../parser";
|
|
24
|
+
import {PrettierConfigLoader} from "./format/prettier-config-loader";
|
|
25
|
+
import {ExecutionContext} from "../execution";
|
|
26
|
+
import {Marker} from "../markers";
|
|
27
|
+
|
|
28
|
+
// Lock file names defined here to avoid circular dependency with package-manager.ts
|
|
29
|
+
// These must be kept in sync with the definitions in package-manager.ts
|
|
30
|
+
const JSON_LOCK_FILE_NAMES = ['bun.lock', 'package-lock.json'] as const;
|
|
31
|
+
const YAML_LOCK_FILE_NAMES = ['pnpm-lock.yaml'] as const;
|
|
32
|
+
const TEXT_LOCK_FILE_NAMES = ['yarn.lock'] as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detects if a yarn.lock file is Yarn Berry (v2+) format based on content.
|
|
36
|
+
* Yarn Berry lock files contain a `__metadata:` key which is not present in Classic.
|
|
37
|
+
*/
|
|
38
|
+
function isYarnBerryLockFile(content: string): boolean {
|
|
39
|
+
return content.includes('__metadata:');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Options for project parsing.
|
|
44
|
+
*/
|
|
45
|
+
export interface ProjectParserOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Optional execution context.
|
|
48
|
+
*/
|
|
49
|
+
ctx?: ExecutionContext;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Glob patterns to exclude from source file discovery.
|
|
53
|
+
* Default excludes: node_modules, dist, build, .git, coverage, *.min.js, *.bundle.js
|
|
54
|
+
*/
|
|
55
|
+
exclusions?: string[];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether to use git to discover files (respects .gitignore).
|
|
59
|
+
* Default: true if in a git repository.
|
|
60
|
+
*/
|
|
61
|
+
useGit?: boolean;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Progress callback for file parsing.
|
|
65
|
+
*/
|
|
66
|
+
onProgress?: (phase: "discovering" | "parsing", current: number, total: number, filePath?: string) => void;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether to enable verbose logging.
|
|
70
|
+
*/
|
|
71
|
+
verbose?: boolean;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Optional predicate to filter which files should be parsed.
|
|
75
|
+
* Called with the absolute file path after file discovery.
|
|
76
|
+
* Return true to include the file, false to exclude it.
|
|
77
|
+
* If not provided, all discovered files are parsed.
|
|
78
|
+
*/
|
|
79
|
+
fileFilter?: (absolutePath: string) => boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Result of file discovery.
|
|
84
|
+
*/
|
|
85
|
+
export interface DiscoveredFiles {
|
|
86
|
+
/** package.json files - parsed with PackageJsonParser for NodeResolutionResult */
|
|
87
|
+
packageJsonFiles: string[];
|
|
88
|
+
/** Lock files grouped by parser type */
|
|
89
|
+
lockFiles: {
|
|
90
|
+
json: string[]; // package-lock.json, bun.lock
|
|
91
|
+
yaml: string[]; // pnpm-lock.yaml, yarn.lock (Berry)
|
|
92
|
+
text: string[]; // yarn.lock (Classic)
|
|
93
|
+
};
|
|
94
|
+
/** JavaScript/TypeScript files (includes .prettierrc.js, prettier.config.js) */
|
|
95
|
+
jsFiles: string[];
|
|
96
|
+
/** JSON files (tsconfig.json, .prettierrc.json, other .json) */
|
|
97
|
+
jsonFiles: string[];
|
|
98
|
+
/** YAML files (.prettierrc.yaml, other .yaml/.yml) */
|
|
99
|
+
yamlFiles: string[];
|
|
100
|
+
/** Plain text config files (.prettierignore, .gitignore, etc.) */
|
|
101
|
+
textFiles: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Default exclusion patterns for file discovery.
|
|
106
|
+
*/
|
|
107
|
+
export const DEFAULT_EXCLUSIONS = [
|
|
108
|
+
"**/node_modules/**",
|
|
109
|
+
"**/dist/**",
|
|
110
|
+
"**/build/**",
|
|
111
|
+
"**/.git/**",
|
|
112
|
+
"**/coverage/**",
|
|
113
|
+
"**/*.min.js",
|
|
114
|
+
"**/*.bundle.js"
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Source file extensions for JavaScript/TypeScript.
|
|
119
|
+
*/
|
|
120
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
121
|
+
".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* All lock file names for quick lookup.
|
|
126
|
+
*/
|
|
127
|
+
const ALL_LOCK_FILE_NAMES = new Set<string>([
|
|
128
|
+
...JSON_LOCK_FILE_NAMES,
|
|
129
|
+
...YAML_LOCK_FILE_NAMES,
|
|
130
|
+
...TEXT_LOCK_FILE_NAMES
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Plain text config files (no extension).
|
|
135
|
+
*/
|
|
136
|
+
const TEXT_CONFIG_FILES = new Set([
|
|
137
|
+
".prettierignore",
|
|
138
|
+
".gitignore",
|
|
139
|
+
".npmignore",
|
|
140
|
+
".eslintignore"
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Parses an entire JavaScript/TypeScript project.
|
|
145
|
+
*
|
|
146
|
+
* This class handles:
|
|
147
|
+
* - File discovery (source files, package.json, lock files)
|
|
148
|
+
* - Prettier configuration detection (once per project)
|
|
149
|
+
* - Parsing all files with appropriate parsers
|
|
150
|
+
* - Sharing PrettierConfigLoader across all JavaScript parsers
|
|
151
|
+
*
|
|
152
|
+
* Can be used directly for CLI tools or wrapped by RPC handlers.
|
|
153
|
+
*/
|
|
154
|
+
export class ProjectParser {
|
|
155
|
+
private readonly projectPath: string;
|
|
156
|
+
private readonly exclusions: string[];
|
|
157
|
+
private readonly ctx: ExecutionContext;
|
|
158
|
+
private readonly useGit: boolean;
|
|
159
|
+
private readonly onProgress?: ProjectParserOptions["onProgress"];
|
|
160
|
+
private readonly verbose: boolean;
|
|
161
|
+
private readonly fileFilter?: (absolutePath: string) => boolean;
|
|
162
|
+
|
|
163
|
+
constructor(projectPath: string, options: ProjectParserOptions = {}) {
|
|
164
|
+
this.projectPath = path.resolve(projectPath);
|
|
165
|
+
this.exclusions = options.exclusions ?? DEFAULT_EXCLUSIONS;
|
|
166
|
+
this.ctx = options.ctx ?? new ExecutionContext();
|
|
167
|
+
this.useGit = options.useGit ?? this.isGitRepository();
|
|
168
|
+
this.onProgress = options.onProgress;
|
|
169
|
+
this.verbose = options.verbose ?? false;
|
|
170
|
+
this.fileFilter = options.fileFilter;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Creates and initializes a PrettierConfigLoader for this project.
|
|
175
|
+
* Use this when you need to handle Prettier detection separately from parsing.
|
|
176
|
+
*/
|
|
177
|
+
async createPrettierLoader(): Promise<PrettierConfigLoader> {
|
|
178
|
+
this.log("Detecting Prettier configuration...");
|
|
179
|
+
const prettierLoader = new PrettierConfigLoader(this.projectPath);
|
|
180
|
+
await prettierLoader.detectPrettier();
|
|
181
|
+
return prettierLoader;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Builds an Autodetect marker from the given source files.
|
|
186
|
+
* Samples all files to detect common formatting styles.
|
|
187
|
+
* Uses dynamic import to avoid circular dependencies.
|
|
188
|
+
*/
|
|
189
|
+
async buildAutodetectMarker(sourceFiles: SourceFile[]): Promise<Marker> {
|
|
190
|
+
// Dynamic import to break circular dependency at module load time:
|
|
191
|
+
// parse-project.ts → project-parser.ts → autodetect.ts → visitor.ts → java → rpc → parse-project.ts
|
|
192
|
+
// By deferring the import until runtime, all modules are already loaded when this executes.
|
|
193
|
+
const {Autodetect} = await import("./autodetect.js");
|
|
194
|
+
const {JS} = await import("./tree.js");
|
|
195
|
+
|
|
196
|
+
const detector = Autodetect.detector();
|
|
197
|
+
for (const sf of sourceFiles) {
|
|
198
|
+
if (sf.kind === JS.Kind.CompilationUnit) {
|
|
199
|
+
await detector.sample(sf);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return detector.build();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Parses all source files in the project.
|
|
207
|
+
* Yields source files as they are parsed.
|
|
208
|
+
*/
|
|
209
|
+
async *parse(): AsyncGenerator<SourceFile> {
|
|
210
|
+
// Discover files
|
|
211
|
+
this.log("Discovering files...");
|
|
212
|
+
this.onProgress?.("discovering", 0, 0);
|
|
213
|
+
let discovered = await this.discoverFiles();
|
|
214
|
+
|
|
215
|
+
// Apply file filter if provided
|
|
216
|
+
if (this.fileFilter) {
|
|
217
|
+
discovered = this.applyFileFilter(discovered);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const totalFiles = this.countFiles(discovered);
|
|
221
|
+
this.log(`Found ${totalFiles} files to parse`);
|
|
222
|
+
|
|
223
|
+
// Detect Prettier configuration once for the project
|
|
224
|
+
const prettierLoader = await this.createPrettierLoader();
|
|
225
|
+
|
|
226
|
+
let current = 0;
|
|
227
|
+
|
|
228
|
+
// Parse package.json files first (they get NodeResolutionResult markers)
|
|
229
|
+
if (discovered.packageJsonFiles.length > 0) {
|
|
230
|
+
this.log(`Parsing ${discovered.packageJsonFiles.length} package.json files...`);
|
|
231
|
+
const parser = Parsers.createParser("packageJson", {
|
|
232
|
+
ctx: this.ctx,
|
|
233
|
+
relativeTo: this.projectPath
|
|
234
|
+
});
|
|
235
|
+
for await (const sf of parser.parse(...discovered.packageJsonFiles)) {
|
|
236
|
+
current++;
|
|
237
|
+
this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
|
|
238
|
+
yield sf;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Parse JSON lock files
|
|
243
|
+
if (discovered.lockFiles.json.length > 0) {
|
|
244
|
+
this.log(`Parsing ${discovered.lockFiles.json.length} JSON lock files...`);
|
|
245
|
+
const parser = Parsers.createParser("json", {
|
|
246
|
+
ctx: this.ctx,
|
|
247
|
+
relativeTo: this.projectPath
|
|
248
|
+
});
|
|
249
|
+
for await (const sf of parser.parse(...discovered.lockFiles.json)) {
|
|
250
|
+
current++;
|
|
251
|
+
this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
|
|
252
|
+
yield sf;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Parse YAML lock files
|
|
257
|
+
if (discovered.lockFiles.yaml.length > 0) {
|
|
258
|
+
this.log(`Parsing ${discovered.lockFiles.yaml.length} YAML lock files...`);
|
|
259
|
+
const parser = Parsers.createParser("yaml", {
|
|
260
|
+
ctx: this.ctx,
|
|
261
|
+
relativeTo: this.projectPath
|
|
262
|
+
});
|
|
263
|
+
for await (const sf of parser.parse(...discovered.lockFiles.yaml)) {
|
|
264
|
+
current++;
|
|
265
|
+
this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
|
|
266
|
+
yield sf;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Parse text lock files (yarn.lock Classic)
|
|
271
|
+
if (discovered.lockFiles.text.length > 0) {
|
|
272
|
+
this.log(`Parsing ${discovered.lockFiles.text.length} text lock files...`);
|
|
273
|
+
const parser = Parsers.createParser("plainText", {
|
|
274
|
+
ctx: this.ctx,
|
|
275
|
+
relativeTo: this.projectPath
|
|
276
|
+
});
|
|
277
|
+
for await (const sf of parser.parse(...discovered.lockFiles.text)) {
|
|
278
|
+
current++;
|
|
279
|
+
this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
|
|
280
|
+
yield sf;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Parse JavaScript/TypeScript source files
|
|
285
|
+
if (discovered.jsFiles.length > 0) {
|
|
286
|
+
this.log(`Parsing ${discovered.jsFiles.length} JavaScript/TypeScript files...`);
|
|
287
|
+
const parser = Parsers.createParser("javascript", {
|
|
288
|
+
ctx: this.ctx,
|
|
289
|
+
relativeTo: this.projectPath
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Check if Prettier is available
|
|
293
|
+
const detection = await prettierLoader.detectPrettier();
|
|
294
|
+
|
|
295
|
+
if (detection.available) {
|
|
296
|
+
// Prettier is available: add per-file PrettierStyle markers
|
|
297
|
+
for await (const sf of parser.parse(...discovered.jsFiles)) {
|
|
298
|
+
current++;
|
|
299
|
+
this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
|
|
300
|
+
|
|
301
|
+
const prettierMarker = await prettierLoader.getConfigMarker(
|
|
302
|
+
path.join(this.projectPath, sf.sourcePath)
|
|
303
|
+
);
|
|
304
|
+
if (prettierMarker) {
|
|
305
|
+
yield produce(sf, draft => {
|
|
306
|
+
draft.markers.markers = draft.markers.markers.concat([prettierMarker]);
|
|
307
|
+
});
|
|
308
|
+
} else {
|
|
309
|
+
yield sf;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
// Prettier is NOT available: auto-detect styles from parsed files
|
|
314
|
+
this.log("Prettier not found, auto-detecting styles...");
|
|
315
|
+
|
|
316
|
+
// Dynamic import to break circular dependency at module load time
|
|
317
|
+
// (see buildAutodetectMarker for explanation)
|
|
318
|
+
const {Autodetect} = await import("./autodetect.js");
|
|
319
|
+
const {JS} = await import("./tree.js");
|
|
320
|
+
|
|
321
|
+
const parsedFiles: SourceFile[] = [];
|
|
322
|
+
|
|
323
|
+
// Parse all JS files and collect them for sampling
|
|
324
|
+
for await (const sf of parser.parse(...discovered.jsFiles)) {
|
|
325
|
+
current++;
|
|
326
|
+
this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
|
|
327
|
+
if (sf.kind === JS.Kind.CompilationUnit) {
|
|
328
|
+
parsedFiles.push(sf);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Sample all parsed files and build Autodetect marker
|
|
333
|
+
const detector = Autodetect.detector();
|
|
334
|
+
for (const sf of parsedFiles) {
|
|
335
|
+
await detector.sample(sf);
|
|
336
|
+
}
|
|
337
|
+
const autodetectMarker = detector.build();
|
|
338
|
+
this.log(`Auto-detected styles: indent=${detector.getTabsAndIndentsStyle().indentSize}, ` +
|
|
339
|
+
`useTabs=${detector.getTabsAndIndentsStyle().useTabCharacter}`);
|
|
340
|
+
|
|
341
|
+
// Yield all files with the Autodetect marker
|
|
342
|
+
for (const sf of parsedFiles) {
|
|
343
|
+
yield produce(sf, draft => {
|
|
344
|
+
draft.markers.markers = draft.markers.markers.concat([autodetectMarker]);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Parse other YAML files
|
|
351
|
+
if (discovered.yamlFiles.length > 0) {
|
|
352
|
+
this.log(`Parsing ${discovered.yamlFiles.length} YAML files...`);
|
|
353
|
+
const parser = Parsers.createParser("yaml", {
|
|
354
|
+
ctx: this.ctx,
|
|
355
|
+
relativeTo: this.projectPath
|
|
356
|
+
});
|
|
357
|
+
for await (const sf of parser.parse(...discovered.yamlFiles)) {
|
|
358
|
+
current++;
|
|
359
|
+
this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
|
|
360
|
+
yield sf;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Parse other JSON files
|
|
365
|
+
if (discovered.jsonFiles.length > 0) {
|
|
366
|
+
this.log(`Parsing ${discovered.jsonFiles.length} JSON files...`);
|
|
367
|
+
const parser = Parsers.createParser("json", {
|
|
368
|
+
ctx: this.ctx,
|
|
369
|
+
relativeTo: this.projectPath
|
|
370
|
+
});
|
|
371
|
+
for await (const sf of parser.parse(...discovered.jsonFiles)) {
|
|
372
|
+
current++;
|
|
373
|
+
this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
|
|
374
|
+
yield sf;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Parse text config files (.prettierignore, .gitignore, etc.)
|
|
379
|
+
if (discovered.textFiles.length > 0) {
|
|
380
|
+
this.log(`Parsing ${discovered.textFiles.length} text config files...`);
|
|
381
|
+
const parser = Parsers.createParser("plainText", {
|
|
382
|
+
ctx: this.ctx,
|
|
383
|
+
relativeTo: this.projectPath
|
|
384
|
+
});
|
|
385
|
+
for await (const sf of parser.parse(...discovered.textFiles)) {
|
|
386
|
+
current++;
|
|
387
|
+
this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
|
|
388
|
+
yield sf;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Discovers all files in the project that should be parsed.
|
|
395
|
+
*/
|
|
396
|
+
async discoverFiles(): Promise<DiscoveredFiles> {
|
|
397
|
+
const discovered: DiscoveredFiles = {
|
|
398
|
+
packageJsonFiles: [],
|
|
399
|
+
lockFiles: {
|
|
400
|
+
json: [],
|
|
401
|
+
yaml: [],
|
|
402
|
+
text: []
|
|
403
|
+
},
|
|
404
|
+
jsFiles: [],
|
|
405
|
+
jsonFiles: [],
|
|
406
|
+
yamlFiles: [],
|
|
407
|
+
textFiles: []
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
let files: string[];
|
|
411
|
+
if (this.useGit) {
|
|
412
|
+
files = await this.discoverFilesWithGit();
|
|
413
|
+
} else {
|
|
414
|
+
files = await this.discoverFilesWithWalk();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Classify files
|
|
418
|
+
const yarnLockFiles: string[] = [];
|
|
419
|
+
|
|
420
|
+
for (const file of files) {
|
|
421
|
+
const basename = path.basename(file);
|
|
422
|
+
const ext = path.extname(file).toLowerCase();
|
|
423
|
+
|
|
424
|
+
if (basename === "package.json") {
|
|
425
|
+
discovered.packageJsonFiles.push(file);
|
|
426
|
+
} else if (SOURCE_EXTENSIONS.has(ext)) {
|
|
427
|
+
discovered.jsFiles.push(file);
|
|
428
|
+
} else if ((JSON_LOCK_FILE_NAMES as readonly string[]).includes(basename)) {
|
|
429
|
+
discovered.lockFiles.json.push(file);
|
|
430
|
+
} else if (basename === "yarn.lock") {
|
|
431
|
+
// yarn.lock needs content-based classification
|
|
432
|
+
yarnLockFiles.push(file);
|
|
433
|
+
} else if ((YAML_LOCK_FILE_NAMES as readonly string[]).includes(basename)) {
|
|
434
|
+
discovered.lockFiles.yaml.push(file);
|
|
435
|
+
} else if ((TEXT_LOCK_FILE_NAMES as readonly string[]).includes(basename)) {
|
|
436
|
+
discovered.lockFiles.text.push(file);
|
|
437
|
+
} else if (ext === ".json") {
|
|
438
|
+
discovered.jsonFiles.push(file);
|
|
439
|
+
} else if (ext === ".yaml" || ext === ".yml") {
|
|
440
|
+
discovered.yamlFiles.push(file);
|
|
441
|
+
} else if (TEXT_CONFIG_FILES.has(basename)) {
|
|
442
|
+
discovered.textFiles.push(file);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Classify yarn.lock files by content
|
|
447
|
+
for (const yarnLockPath of yarnLockFiles) {
|
|
448
|
+
const format = await this.classifyYarnLockFile(yarnLockPath);
|
|
449
|
+
if (format === "yaml") {
|
|
450
|
+
discovered.lockFiles.yaml.push(yarnLockPath);
|
|
451
|
+
} else {
|
|
452
|
+
discovered.lockFiles.text.push(yarnLockPath);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return discovered;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Discovers files using git ls-files (respects .gitignore).
|
|
461
|
+
*/
|
|
462
|
+
private async discoverFilesWithGit(): Promise<string[]> {
|
|
463
|
+
const files: string[] = [];
|
|
464
|
+
|
|
465
|
+
// Get tracked files
|
|
466
|
+
const tracked = spawnSync("git", ["ls-files"], {
|
|
467
|
+
cwd: this.projectPath,
|
|
468
|
+
encoding: "utf8"
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
if (tracked.status !== 0 || tracked.error) {
|
|
472
|
+
// Fall back to walk if git fails
|
|
473
|
+
return this.discoverFilesWithWalk();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (tracked.stdout) {
|
|
477
|
+
for (const line of tracked.stdout.split("\n")) {
|
|
478
|
+
const trimmed = line.trim();
|
|
479
|
+
if (trimmed) {
|
|
480
|
+
const fullPath = path.join(this.projectPath, trimmed);
|
|
481
|
+
// git ls-files can return deleted files that are still tracked
|
|
482
|
+
if (fs.existsSync(fullPath)) {
|
|
483
|
+
files.push(fullPath);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Get untracked but not ignored files
|
|
490
|
+
const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
491
|
+
cwd: this.projectPath,
|
|
492
|
+
encoding: "utf8"
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
if (untracked.stdout) {
|
|
496
|
+
for (const line of untracked.stdout.split("\n")) {
|
|
497
|
+
const trimmed = line.trim();
|
|
498
|
+
if (trimmed) {
|
|
499
|
+
// Untracked files should exist, but check anyway for robustness
|
|
500
|
+
const fullPath = path.join(this.projectPath, trimmed);
|
|
501
|
+
if (fs.existsSync(fullPath)) {
|
|
502
|
+
files.push(fullPath);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Filter by our exclusion patterns and accepted file types
|
|
509
|
+
return files.filter(file => this.isAcceptedFile(file));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Discovers files by walking the directory tree.
|
|
514
|
+
*/
|
|
515
|
+
private async discoverFilesWithWalk(): Promise<string[]> {
|
|
516
|
+
const files: string[] = [];
|
|
517
|
+
const isExcluded = picomatch(this.exclusions);
|
|
518
|
+
|
|
519
|
+
const walk = async (dir: string, relativePath: string = "") => {
|
|
520
|
+
let entries: fs.Dirent[];
|
|
521
|
+
try {
|
|
522
|
+
entries = await fsp.readdir(dir, {withFileTypes: true});
|
|
523
|
+
} catch {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
for (const entry of entries) {
|
|
528
|
+
const fullPath = path.join(dir, entry.name);
|
|
529
|
+
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
530
|
+
|
|
531
|
+
// Check exclusion patterns
|
|
532
|
+
if (isExcluded(relPath) || isExcluded(`${relPath}/`)) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Always skip node_modules
|
|
537
|
+
if (entry.name === "node_modules") {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (entry.isDirectory()) {
|
|
542
|
+
await walk(fullPath, relPath);
|
|
543
|
+
} else if (entry.isFile() && this.isAcceptedFile(fullPath)) {
|
|
544
|
+
files.push(fullPath);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
await walk(this.projectPath);
|
|
550
|
+
return files;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Checks if a file should be accepted for parsing.
|
|
555
|
+
*/
|
|
556
|
+
private isAcceptedFile(filePath: string): boolean {
|
|
557
|
+
const basename = path.basename(filePath);
|
|
558
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
559
|
+
|
|
560
|
+
// Check exclusion patterns with relative path
|
|
561
|
+
const relativePath = path.relative(this.projectPath, filePath);
|
|
562
|
+
const isExcluded = picomatch(this.exclusions);
|
|
563
|
+
if (isExcluded(relativePath)) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// JavaScript/TypeScript files
|
|
568
|
+
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// JSON files
|
|
573
|
+
if (ext === ".json") {
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// YAML files
|
|
578
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Lock files (some have non-standard extensions)
|
|
583
|
+
if (ALL_LOCK_FILE_NAMES.has(basename)) {
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Text config files
|
|
588
|
+
if (TEXT_CONFIG_FILES.has(basename)) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Classifies a yarn.lock file as YAML (Berry) or text (Classic).
|
|
597
|
+
*/
|
|
598
|
+
private async classifyYarnLockFile(filePath: string): Promise<"yaml" | "text"> {
|
|
599
|
+
try {
|
|
600
|
+
const content = await fsp.readFile(filePath, "utf-8");
|
|
601
|
+
return isYarnBerryLockFile(content) ? "yaml" : "text";
|
|
602
|
+
} catch {
|
|
603
|
+
return "text";
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Checks if the project is a git repository.
|
|
609
|
+
*/
|
|
610
|
+
private isGitRepository(): boolean {
|
|
611
|
+
return fs.existsSync(path.join(this.projectPath, ".git"));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Counts total files to parse.
|
|
616
|
+
*/
|
|
617
|
+
private countFiles(discovered: DiscoveredFiles): number {
|
|
618
|
+
return (
|
|
619
|
+
discovered.packageJsonFiles.length +
|
|
620
|
+
discovered.lockFiles.json.length +
|
|
621
|
+
discovered.lockFiles.yaml.length +
|
|
622
|
+
discovered.lockFiles.text.length +
|
|
623
|
+
discovered.jsFiles.length +
|
|
624
|
+
discovered.jsonFiles.length +
|
|
625
|
+
discovered.yamlFiles.length +
|
|
626
|
+
discovered.textFiles.length
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Applies the file filter to discovered files.
|
|
632
|
+
*/
|
|
633
|
+
private applyFileFilter(discovered: DiscoveredFiles): DiscoveredFiles {
|
|
634
|
+
const filter = this.fileFilter!;
|
|
635
|
+
return {
|
|
636
|
+
packageJsonFiles: discovered.packageJsonFiles.filter(filter),
|
|
637
|
+
lockFiles: {
|
|
638
|
+
json: discovered.lockFiles.json.filter(filter),
|
|
639
|
+
yaml: discovered.lockFiles.yaml.filter(filter),
|
|
640
|
+
text: discovered.lockFiles.text.filter(filter)
|
|
641
|
+
},
|
|
642
|
+
jsFiles: discovered.jsFiles.filter(filter),
|
|
643
|
+
jsonFiles: discovered.jsonFiles.filter(filter),
|
|
644
|
+
yamlFiles: discovered.yamlFiles.filter(filter),
|
|
645
|
+
textFiles: discovered.textFiles.filter(filter)
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Logs a message if verbose mode is enabled.
|
|
651
|
+
*/
|
|
652
|
+
private log(message: string): void {
|
|
653
|
+
if (this.verbose) {
|
|
654
|
+
console.log(message);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|