@openrewrite/rewrite 8.69.0-20251210-214835 → 8.69.0-20251211-110844
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.map +1 -1
- package/dist/cli/cli-utils.js +6 -0
- package/dist/cli/cli-utils.js.map +1 -1
- package/dist/cli/rewrite.d.ts.map +1 -1
- package/dist/cli/rewrite.js +55 -23
- package/dist/cli/rewrite.js.map +1 -1
- package/dist/data-table.d.ts +23 -0
- package/dist/data-table.d.ts.map +1 -1
- package/dist/data-table.js +125 -6
- package/dist/data-table.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/javascript/node-resolution-result.d.ts +9 -0
- package/dist/javascript/node-resolution-result.d.ts.map +1 -1
- package/dist/javascript/node-resolution-result.js +10 -1
- package/dist/javascript/node-resolution-result.js.map +1 -1
- package/dist/javascript/package-manager.d.ts +76 -89
- package/dist/javascript/package-manager.d.ts.map +1 -1
- package/dist/javascript/package-manager.js +114 -139
- package/dist/javascript/package-manager.js.map +1 -1
- package/dist/javascript/recipes/add-dependency.d.ts +57 -0
- package/dist/javascript/recipes/add-dependency.d.ts.map +1 -0
- package/dist/javascript/recipes/add-dependency.js +404 -0
- package/dist/javascript/recipes/add-dependency.js.map +1 -0
- package/dist/javascript/recipes/index.d.ts +1 -0
- package/dist/javascript/recipes/index.d.ts.map +1 -1
- package/dist/javascript/recipes/index.js +1 -0
- package/dist/javascript/recipes/index.js.map +1 -1
- package/dist/javascript/recipes/upgrade-dependency-version.d.ts +3 -24
- package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -1
- package/dist/javascript/recipes/upgrade-dependency-version.js +34 -157
- package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -1
- package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts +2 -19
- package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts.map +1 -1
- package/dist/javascript/recipes/upgrade-transitive-dependency-version.js +21 -137
- package/dist/javascript/recipes/upgrade-transitive-dependency-version.js.map +1 -1
- package/dist/javascript/search/find-dependency.d.ts.map +1 -1
- package/dist/javascript/search/find-dependency.js +8 -47
- package/dist/javascript/search/find-dependency.js.map +1 -1
- package/dist/json/tree.d.ts +30 -0
- package/dist/json/tree.d.ts.map +1 -1
- package/dist/json/tree.js +113 -0
- package/dist/json/tree.js.map +1 -1
- package/dist/parser.d.ts +9 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +27 -0
- package/dist/parser.js.map +1 -1
- package/dist/reference.d.ts.map +1 -1
- package/dist/reference.js +1 -1
- package/dist/reference.js.map +1 -1
- package/dist/version.txt +1 -1
- package/dist/visitor.js +1 -1
- package/dist/visitor.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/cli-utils.ts +6 -0
- package/src/cli/rewrite.ts +53 -17
- package/src/data-table.ts +83 -2
- package/src/index.ts +2 -1
- package/src/javascript/node-resolution-result.ts +16 -0
- package/src/javascript/package-manager.ts +197 -174
- package/src/javascript/recipes/add-dependency.ts +467 -0
- package/src/javascript/recipes/index.ts +1 -0
- package/src/javascript/recipes/upgrade-dependency-version.ts +52 -199
- package/src/javascript/recipes/upgrade-transitive-dependency-version.ts +39 -165
- package/src/javascript/search/find-dependency.ts +13 -52
- package/src/json/tree.ts +98 -1
- package/src/parser.ts +17 -0
- package/src/reference.ts +1 -1
- package/src/visitor.ts +1 -1
package/src/cli/rewrite.ts
CHANGED
|
@@ -21,6 +21,7 @@ import * as path from 'path';
|
|
|
21
21
|
import {spawn} from 'child_process';
|
|
22
22
|
import {Recipe, RecipeRegistry} from '../recipe';
|
|
23
23
|
import {ExecutionContext} from '../execution';
|
|
24
|
+
import {CsvDataTableStore, DATA_TABLE_STORE} from '../data-table';
|
|
24
25
|
import {ProgressCallback, scheduleRunStreaming} from '../run';
|
|
25
26
|
import {TreePrinters} from '../print';
|
|
26
27
|
import {activate} from '../index';
|
|
@@ -145,6 +146,8 @@ interface CliOptions {
|
|
|
145
146
|
option: string[];
|
|
146
147
|
verbose: boolean;
|
|
147
148
|
debug: boolean;
|
|
149
|
+
dataTables?: string;
|
|
150
|
+
dataTablesOnly: boolean;
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
/**
|
|
@@ -184,6 +187,8 @@ async function main() {
|
|
|
184
187
|
.option('-o, --option <option...>', 'Recipe options in format "key=value"', [])
|
|
185
188
|
.option('-v, --verbose', 'Enable verbose output', false)
|
|
186
189
|
.option('--debug', 'Start with Node.js debugger (--inspect-brk)', false)
|
|
190
|
+
.option('--data-tables <dir>', 'Export data tables as CSV files to this directory')
|
|
191
|
+
.option('--data-tables-only', 'Only output data tables, skip source file diffs', false)
|
|
187
192
|
.parse();
|
|
188
193
|
|
|
189
194
|
const recipeArg = program.args[0];
|
|
@@ -377,8 +382,18 @@ async function main() {
|
|
|
377
382
|
// Run the recipe with streaming output - for non-scanning recipes,
|
|
378
383
|
// parsing and processing happen concurrently without collecting all files first
|
|
379
384
|
const ctx = new ExecutionContext();
|
|
385
|
+
|
|
386
|
+
// Set up data table store if requested
|
|
387
|
+
let csvDataTableStore: CsvDataTableStore | undefined;
|
|
388
|
+
if (opts.dataTables) {
|
|
389
|
+
csvDataTableStore = new CsvDataTableStore(opts.dataTables);
|
|
390
|
+
csvDataTableStore.acceptRows(true);
|
|
391
|
+
ctx.messages[DATA_TABLE_STORE] = csvDataTableStore;
|
|
392
|
+
}
|
|
393
|
+
|
|
380
394
|
let changeCount = 0;
|
|
381
395
|
let processedCount = 0;
|
|
396
|
+
const skipSourceOutput = opts.dataTablesOnly;
|
|
382
397
|
|
|
383
398
|
// Progress callback for spinner updates during all phases (disabled in verbose mode)
|
|
384
399
|
const onProgress: ProgressCallback | undefined = opts.verbose ? undefined : (phase, current, total, sourcePath) => {
|
|
@@ -414,23 +429,27 @@ async function main() {
|
|
|
414
429
|
await fsp.writeFile(filePath, content);
|
|
415
430
|
|
|
416
431
|
changeCount++;
|
|
417
|
-
if (!
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
432
|
+
if (!skipSourceOutput) {
|
|
433
|
+
if (!opts.verbose) spinner.stop();
|
|
434
|
+
const displayPath = toCwdRelative(result.after.sourcePath, projectRoot);
|
|
435
|
+
if (result.before) {
|
|
436
|
+
console.log(` Modified: ${displayPath}`);
|
|
437
|
+
} else {
|
|
438
|
+
console.log(` Created: ${displayPath}`);
|
|
439
|
+
}
|
|
440
|
+
if (!opts.verbose) spinner.start(statusMsg);
|
|
423
441
|
}
|
|
424
|
-
if (!opts.verbose) spinner.start(statusMsg);
|
|
425
442
|
} else if (result.before) {
|
|
426
443
|
const filePath = path.join(projectRoot, result.before.sourcePath);
|
|
427
444
|
await fsp.unlink(filePath);
|
|
428
445
|
changeCount++;
|
|
429
|
-
if (!
|
|
430
|
-
|
|
431
|
-
|
|
446
|
+
if (!skipSourceOutput) {
|
|
447
|
+
if (!opts.verbose) spinner.stop();
|
|
448
|
+
console.log(` Deleted: ${toCwdRelative(result.before.sourcePath, projectRoot)}`);
|
|
449
|
+
if (!opts.verbose) spinner.start(statusMsg);
|
|
450
|
+
}
|
|
432
451
|
}
|
|
433
|
-
} else {
|
|
452
|
+
} else if (!skipSourceOutput) {
|
|
434
453
|
// Dry-run mode: show diff or just list paths
|
|
435
454
|
const diff = await result.diff();
|
|
436
455
|
const hasChanges = diff.split('\n').some(line =>
|
|
@@ -466,12 +485,29 @@ async function main() {
|
|
|
466
485
|
process.exit(recipe.hasErrors ? 1 : 0);
|
|
467
486
|
}
|
|
468
487
|
|
|
469
|
-
if
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
488
|
+
// Report on data tables if any were written
|
|
489
|
+
if (csvDataTableStore) {
|
|
490
|
+
const tableNames = csvDataTableStore.tableNames;
|
|
491
|
+
const rowCounts = csvDataTableStore.rowCounts;
|
|
492
|
+
if (tableNames.length > 0) {
|
|
493
|
+
console.log(`\nData tables written to ${opts.dataTables}:`);
|
|
494
|
+
for (const name of tableNames) {
|
|
495
|
+
console.log(` ${name}.csv (${rowCounts[name]} rows)`);
|
|
496
|
+
}
|
|
497
|
+
} else if (opts.dataTablesOnly) {
|
|
498
|
+
console.log('No data tables produced.');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Report on source file changes (unless --data-tables-only)
|
|
503
|
+
if (!skipSourceOutput) {
|
|
504
|
+
if (changeCount === 0) {
|
|
505
|
+
console.log('No changes to make.');
|
|
506
|
+
} else if (opts.apply) {
|
|
507
|
+
console.log(`\n${changeCount} file(s) changed.`);
|
|
508
|
+
} else if (!opts.list) {
|
|
509
|
+
console.log(`\n${changeCount} file(s) would be changed. Run with --apply to apply changes.`);
|
|
510
|
+
}
|
|
475
511
|
}
|
|
476
512
|
}
|
|
477
513
|
|
package/src/data-table.ts
CHANGED
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
16
18
|
import {ExecutionContext} from "./execution";
|
|
17
19
|
import {OptionDescriptor} from "./recipe";
|
|
18
20
|
|
|
19
|
-
const DATA_TABLE_STORE = Symbol("org.openrewrite.dataTables.store");
|
|
20
|
-
const COLUMNS_KEY = Symbol("org.openrewrite.dataTables.columns");
|
|
21
|
+
export const DATA_TABLE_STORE = Symbol.for("org.openrewrite.dataTables.store");
|
|
22
|
+
const COLUMNS_KEY = Symbol.for("org.openrewrite.dataTables.columns");
|
|
21
23
|
|
|
22
24
|
export function Column(descriptor: ColumnDescriptor) {
|
|
23
25
|
return function (target: any, propertyKey: string) {
|
|
@@ -103,3 +105,82 @@ export interface ColumnDescriptor {
|
|
|
103
105
|
displayName: string,
|
|
104
106
|
description: string
|
|
105
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Escape a value for CSV output following RFC 4180.
|
|
111
|
+
* Quotes the value if it contains commas, quotes, or newlines.
|
|
112
|
+
*/
|
|
113
|
+
function escapeCsv(value: unknown): string {
|
|
114
|
+
if (value === null || value === undefined) {
|
|
115
|
+
return '""';
|
|
116
|
+
}
|
|
117
|
+
const str = String(value);
|
|
118
|
+
// If the value contains comma, quote, or newline, wrap in quotes and escape internal quotes
|
|
119
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
|
120
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
121
|
+
}
|
|
122
|
+
return str;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A DataTableStore that writes rows directly to CSV files as they are inserted.
|
|
127
|
+
*/
|
|
128
|
+
export class CsvDataTableStore implements DataTableStore {
|
|
129
|
+
private _acceptRows = false;
|
|
130
|
+
private readonly _initializedTables = new Set<string>();
|
|
131
|
+
private readonly _rowCounts: { [dataTable: string]: number } = {};
|
|
132
|
+
|
|
133
|
+
constructor(private readonly outputDir: string) {
|
|
134
|
+
// Ensure output directory exists
|
|
135
|
+
fs.mkdirSync(outputDir, {recursive: true});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
insertRow<Row>(dataTable: DataTable<Row>, _ctx: ExecutionContext, row: Row): void {
|
|
139
|
+
if (!this._acceptRows) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const descriptor = dataTable.descriptor;
|
|
144
|
+
const tableName = descriptor.name;
|
|
145
|
+
const csvPath = path.join(this.outputDir, tableName + '.csv');
|
|
146
|
+
|
|
147
|
+
// Write header rows on first insert for this table
|
|
148
|
+
if (!this._initializedTables.has(tableName)) {
|
|
149
|
+
this._initializedTables.add(tableName);
|
|
150
|
+
this._rowCounts[tableName] = 0;
|
|
151
|
+
|
|
152
|
+
const columns = descriptor.columns;
|
|
153
|
+
const headerRow = columns.map(col => escapeCsv(col.displayName)).join(',');
|
|
154
|
+
const descriptionRow = columns.map(col => escapeCsv(col.description)).join(',');
|
|
155
|
+
|
|
156
|
+
fs.writeFileSync(csvPath, headerRow + '\n' + descriptionRow + '\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Write the data row
|
|
160
|
+
const columns = descriptor.columns;
|
|
161
|
+
const rowValues = columns.map(col => {
|
|
162
|
+
const value = (row as any)[col.name];
|
|
163
|
+
return escapeCsv(value);
|
|
164
|
+
});
|
|
165
|
+
fs.appendFileSync(csvPath, rowValues.join(',') + '\n');
|
|
166
|
+
this._rowCounts[tableName]++;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
acceptRows(accept: boolean): void {
|
|
170
|
+
this._acceptRows = accept;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get the number of rows written for each data table.
|
|
175
|
+
*/
|
|
176
|
+
get rowCounts(): { [dataTable: string]: number } {
|
|
177
|
+
return {...this._rowCounts};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get the names of all data tables that have been written to.
|
|
182
|
+
*/
|
|
183
|
+
get tableNames(): string[] {
|
|
184
|
+
return [...this._initializedTables];
|
|
185
|
+
}
|
|
186
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -38,9 +38,10 @@ export async function activate(registry: RecipeRegistry): Promise<void> {
|
|
|
38
38
|
const {ModernizeOctalEscapeSequences, ModernizeOctalLiterals, RemoveDuplicateObjectKeys} = await import("./javascript/migrate/es6/index.js");
|
|
39
39
|
const {ExportAssignmentToExportDefault} = await import("./javascript/migrate/typescript/index.js");
|
|
40
40
|
const {UseObjectPropertyShorthand, PreferOptionalChain, AddParseIntRadix} = await import("./javascript/cleanup/index.js");
|
|
41
|
-
const {AsyncCallbackInSyncArrayMethod, AutoFormat, UpgradeDependencyVersion, UpgradeTransitiveDependencyVersion, OrderImports, ChangeImport} = await import("./javascript/recipes/index.js");
|
|
41
|
+
const {AddDependency, AsyncCallbackInSyncArrayMethod, AutoFormat, UpgradeDependencyVersion, UpgradeTransitiveDependencyVersion, OrderImports, ChangeImport} = await import("./javascript/recipes/index.js");
|
|
42
42
|
const {FindDependency} = await import("./javascript/search/index.js");
|
|
43
43
|
|
|
44
|
+
registry.register(AddDependency);
|
|
44
45
|
registry.register(ExportAssignmentToExportDefault);
|
|
45
46
|
registry.register(FindDependency);
|
|
46
47
|
registry.register(OrderImports);
|
|
@@ -89,6 +89,22 @@ export const enum NpmrcScope {
|
|
|
89
89
|
Project = 'Project', // .npmrc in project root
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Represents a dependency scope in package.json that uses object structure {name: version}.
|
|
94
|
+
* Note: `bundledDependencies` is excluded because it's a string[] of package names, not a version map.
|
|
95
|
+
*/
|
|
96
|
+
export type DependencyScope = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* All dependency scopes in package.json that use object structure {name: version}.
|
|
100
|
+
*/
|
|
101
|
+
export const allDependencyScopes: readonly DependencyScope[] = [
|
|
102
|
+
'dependencies',
|
|
103
|
+
'devDependencies',
|
|
104
|
+
'peerDependencies',
|
|
105
|
+
'optionalDependencies'
|
|
106
|
+
] as const;
|
|
107
|
+
|
|
92
108
|
export const NpmrcKind = "org.openrewrite.javascript.marker.NodeResolutionResult$Npmrc" as const;
|
|
93
109
|
|
|
94
110
|
/**
|
|
@@ -14,7 +14,16 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
createNodeResolutionResultMarker,
|
|
19
|
+
findNodeResolutionResult,
|
|
20
|
+
PackageJsonContent,
|
|
21
|
+
PackageLockContent,
|
|
22
|
+
PackageManager,
|
|
23
|
+
readNpmrcConfigs
|
|
24
|
+
} from "./node-resolution-result";
|
|
25
|
+
import {replaceMarkerByKind} from "../markers";
|
|
26
|
+
import {Json} from "../json";
|
|
18
27
|
import * as fs from "fs";
|
|
19
28
|
import * as fsp from "fs/promises";
|
|
20
29
|
import * as path from "path";
|
|
@@ -108,7 +117,7 @@ const LOCK_FILE_DETECTION: ReadonlyArray<LockFileDetectionConfig> = [
|
|
|
108
117
|
/**
|
|
109
118
|
* Result of running a package manager command.
|
|
110
119
|
*/
|
|
111
|
-
|
|
120
|
+
interface PackageManagerResult {
|
|
112
121
|
success: boolean;
|
|
113
122
|
stdout?: string;
|
|
114
123
|
stderr?: string;
|
|
@@ -118,7 +127,7 @@ export interface PackageManagerResult {
|
|
|
118
127
|
/**
|
|
119
128
|
* Options for running package manager install.
|
|
120
129
|
*/
|
|
121
|
-
|
|
130
|
+
interface InstallOptions {
|
|
122
131
|
/** Working directory */
|
|
123
132
|
cwd: string;
|
|
124
133
|
|
|
@@ -164,13 +173,6 @@ export function getLockFileDetectionConfig(): ReadonlyArray<LockFileDetectionCon
|
|
|
164
173
|
return LOCK_FILE_DETECTION;
|
|
165
174
|
}
|
|
166
175
|
|
|
167
|
-
/**
|
|
168
|
-
* Gets the configuration for a package manager.
|
|
169
|
-
*/
|
|
170
|
-
export function getPackageManagerConfig(pm: PackageManager): PackageManagerConfig {
|
|
171
|
-
return PACKAGE_MANAGER_CONFIGS[pm];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
176
|
/**
|
|
175
177
|
* Gets the lock file name for a package manager.
|
|
176
178
|
*/
|
|
@@ -185,22 +187,10 @@ export function getAllLockFileNames(): string[] {
|
|
|
185
187
|
return LOCK_FILE_DETECTION.map(c => c.filename);
|
|
186
188
|
}
|
|
187
189
|
|
|
188
|
-
/**
|
|
189
|
-
* Checks if a file path is a lock file.
|
|
190
|
-
*/
|
|
191
|
-
export function isLockFile(filePath: string): boolean {
|
|
192
|
-
const fileName = path.basename(filePath);
|
|
193
|
-
return getAllLockFileNames().includes(fileName);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
190
|
/**
|
|
197
191
|
* Runs the package manager install command.
|
|
198
|
-
*
|
|
199
|
-
* @param pm The package manager to use
|
|
200
|
-
* @param options Install options
|
|
201
|
-
* @returns Result of the install command
|
|
202
192
|
*/
|
|
203
|
-
|
|
193
|
+
function runInstall(pm: PackageManager, options: InstallOptions): PackageManagerResult {
|
|
204
194
|
const config = PACKAGE_MANAGER_CONFIGS[pm];
|
|
205
195
|
const command = options.lockOnly ? config.installLockOnlyCommand : config.installCommand;
|
|
206
196
|
const [cmd, ...args] = command;
|
|
@@ -244,121 +234,6 @@ export function runInstall(pm: PackageManager, options: InstallOptions): Package
|
|
|
244
234
|
}
|
|
245
235
|
}
|
|
246
236
|
|
|
247
|
-
/**
|
|
248
|
-
* Options for adding/upgrading a package.
|
|
249
|
-
*/
|
|
250
|
-
export interface AddPackageOptions {
|
|
251
|
-
/** Working directory */
|
|
252
|
-
cwd: string;
|
|
253
|
-
|
|
254
|
-
/** Package name to add/upgrade */
|
|
255
|
-
packageName: string;
|
|
256
|
-
|
|
257
|
-
/** Version constraint (e.g., "^5.0.0") */
|
|
258
|
-
version: string;
|
|
259
|
-
|
|
260
|
-
/** If true, only update lock file without installing to node_modules */
|
|
261
|
-
lockOnly?: boolean;
|
|
262
|
-
|
|
263
|
-
/** Timeout in milliseconds (default: 120000 = 2 minutes) */
|
|
264
|
-
timeout?: number;
|
|
265
|
-
|
|
266
|
-
/** Additional environment variables */
|
|
267
|
-
env?: Record<string, string>;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Runs a package manager command to add or upgrade a package.
|
|
272
|
-
* This updates both package.json and the lock file.
|
|
273
|
-
*
|
|
274
|
-
* @param pm The package manager to use
|
|
275
|
-
* @param options Add package options
|
|
276
|
-
* @returns Result of the command
|
|
277
|
-
*/
|
|
278
|
-
export function runAddPackage(pm: PackageManager, options: AddPackageOptions): PackageManagerResult {
|
|
279
|
-
const packageSpec = `${options.packageName}@${options.version}`;
|
|
280
|
-
|
|
281
|
-
// Build command based on package manager
|
|
282
|
-
let cmd: string;
|
|
283
|
-
let args: string[];
|
|
284
|
-
|
|
285
|
-
switch (pm) {
|
|
286
|
-
case PackageManager.Npm:
|
|
287
|
-
cmd = 'npm';
|
|
288
|
-
args = ['install', packageSpec];
|
|
289
|
-
if (options.lockOnly) {
|
|
290
|
-
args.push('--package-lock-only');
|
|
291
|
-
}
|
|
292
|
-
break;
|
|
293
|
-
case PackageManager.YarnClassic:
|
|
294
|
-
cmd = 'yarn';
|
|
295
|
-
args = ['add', packageSpec];
|
|
296
|
-
if (options.lockOnly) {
|
|
297
|
-
args.push('--ignore-scripts');
|
|
298
|
-
}
|
|
299
|
-
break;
|
|
300
|
-
case PackageManager.YarnBerry:
|
|
301
|
-
cmd = 'yarn';
|
|
302
|
-
args = ['add', packageSpec];
|
|
303
|
-
if (options.lockOnly) {
|
|
304
|
-
args.push('--mode', 'skip-build');
|
|
305
|
-
}
|
|
306
|
-
break;
|
|
307
|
-
case PackageManager.Pnpm:
|
|
308
|
-
cmd = 'pnpm';
|
|
309
|
-
args = ['add', packageSpec];
|
|
310
|
-
if (options.lockOnly) {
|
|
311
|
-
args.push('--lockfile-only');
|
|
312
|
-
}
|
|
313
|
-
break;
|
|
314
|
-
case PackageManager.Bun:
|
|
315
|
-
cmd = 'bun';
|
|
316
|
-
args = ['add', packageSpec];
|
|
317
|
-
if (options.lockOnly) {
|
|
318
|
-
args.push('--ignore-scripts');
|
|
319
|
-
}
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
const result = spawnSync(cmd, args, {
|
|
325
|
-
cwd: options.cwd,
|
|
326
|
-
encoding: 'utf-8',
|
|
327
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
328
|
-
timeout: options.timeout ?? 120000,
|
|
329
|
-
env: options.env ? {...process.env, ...options.env} : process.env,
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
if (result.error) {
|
|
333
|
-
return {
|
|
334
|
-
success: false,
|
|
335
|
-
error: result.error.message,
|
|
336
|
-
stderr: result.stderr,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (result.status !== 0) {
|
|
341
|
-
return {
|
|
342
|
-
success: false,
|
|
343
|
-
stdout: result.stdout,
|
|
344
|
-
stderr: result.stderr,
|
|
345
|
-
error: `Command exited with code ${result.status}`,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return {
|
|
350
|
-
success: true,
|
|
351
|
-
stdout: result.stdout,
|
|
352
|
-
stderr: result.stderr,
|
|
353
|
-
};
|
|
354
|
-
} catch (error: any) {
|
|
355
|
-
return {
|
|
356
|
-
success: false,
|
|
357
|
-
error: error.message,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
237
|
/**
|
|
363
238
|
* Runs a package manager list command to get dependency information.
|
|
364
239
|
*
|
|
@@ -390,55 +265,203 @@ export function runList(pm: PackageManager, cwd: string, timeout: number = 30000
|
|
|
390
265
|
}
|
|
391
266
|
|
|
392
267
|
/**
|
|
393
|
-
*
|
|
268
|
+
* Result of running install in a temporary directory.
|
|
269
|
+
*/
|
|
270
|
+
export interface TempInstallResult {
|
|
271
|
+
/** Whether the install succeeded */
|
|
272
|
+
success: boolean;
|
|
273
|
+
/** The updated lock file content (if successful and lock file exists) */
|
|
274
|
+
lockFileContent?: string;
|
|
275
|
+
/** Error message (if failed) */
|
|
276
|
+
error?: string;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Generic accumulator for dependency recipes that run package manager operations.
|
|
281
|
+
* Used by scanning recipes to track state across scanning and editing phases.
|
|
394
282
|
*
|
|
395
|
-
* @
|
|
396
|
-
* @returns True if the package manager is available
|
|
283
|
+
* @typeParam T The recipe-specific project update info type
|
|
397
284
|
*/
|
|
398
|
-
export
|
|
399
|
-
|
|
400
|
-
|
|
285
|
+
export interface DependencyRecipeAccumulator<T> {
|
|
286
|
+
/** Projects that need updating: packageJsonPath -> update info */
|
|
287
|
+
projectsToUpdate: Map<string, T>;
|
|
401
288
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
289
|
+
/** After running package manager, store the updated lock file content */
|
|
290
|
+
updatedLockFiles: Map<string, string>;
|
|
291
|
+
|
|
292
|
+
/** Updated package.json content (after npm install may have modified it) */
|
|
293
|
+
updatedPackageJsons: Map<string, string>;
|
|
294
|
+
|
|
295
|
+
/** Track which projects have been processed (npm install has run) */
|
|
296
|
+
processedProjects: Set<string>;
|
|
297
|
+
|
|
298
|
+
/** Track projects where npm install failed: packageJsonPath -> error message */
|
|
299
|
+
failedProjects: Map<string, string>;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Creates a new empty accumulator for dependency recipes.
|
|
304
|
+
*/
|
|
305
|
+
export function createDependencyRecipeAccumulator<T>(): DependencyRecipeAccumulator<T> {
|
|
306
|
+
return {
|
|
307
|
+
projectsToUpdate: new Map(),
|
|
308
|
+
updatedLockFiles: new Map(),
|
|
309
|
+
updatedPackageJsons: new Map(),
|
|
310
|
+
processedProjects: new Set(),
|
|
311
|
+
failedProjects: new Map()
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Checks if a source path is a lock file and returns the updated content if available.
|
|
317
|
+
* This is a helper for dependency recipes that need to update lock files.
|
|
318
|
+
*
|
|
319
|
+
* @param sourcePath The source path to check
|
|
320
|
+
* @param acc The recipe accumulator containing updated lock file content
|
|
321
|
+
* @returns The updated lock file content if this is a lock file that was updated, undefined otherwise
|
|
322
|
+
*/
|
|
323
|
+
export function getUpdatedLockFileContent<T>(
|
|
324
|
+
sourcePath: string,
|
|
325
|
+
acc: DependencyRecipeAccumulator<T>
|
|
326
|
+
): string | undefined {
|
|
327
|
+
for (const lockFileName of getAllLockFileNames()) {
|
|
328
|
+
if (sourcePath.endsWith(lockFileName)) {
|
|
329
|
+
// Find the corresponding package.json path
|
|
330
|
+
const packageJsonPath = sourcePath.replace(lockFileName, 'package.json');
|
|
331
|
+
const updateInfo = acc.projectsToUpdate.get(packageJsonPath);
|
|
332
|
+
|
|
333
|
+
if (updateInfo && acc.updatedLockFiles.has(sourcePath)) {
|
|
334
|
+
return acc.updatedLockFiles.get(sourcePath);
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
411
338
|
}
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Base interface for project update info used by dependency recipes.
|
|
344
|
+
* Recipes extend this with additional fields specific to their needs.
|
|
345
|
+
*/
|
|
346
|
+
export interface BaseProjectUpdateInfo {
|
|
347
|
+
/** Absolute path to the project directory */
|
|
348
|
+
projectDir: string;
|
|
349
|
+
/** Relative path to package.json (from source root) */
|
|
350
|
+
packageJsonPath: string;
|
|
351
|
+
/** The package manager used by this project */
|
|
352
|
+
packageManager: PackageManager;
|
|
412
353
|
}
|
|
413
354
|
|
|
414
355
|
/**
|
|
415
|
-
*
|
|
356
|
+
* Stores the result of a package manager install into the accumulator.
|
|
357
|
+
* This handles the common pattern of storing updated lock files and tracking failures.
|
|
358
|
+
*
|
|
359
|
+
* @param result The result from runInstallInTempDir
|
|
360
|
+
* @param acc The recipe accumulator
|
|
361
|
+
* @param updateInfo The project update info (must have packageJsonPath and packageManager)
|
|
362
|
+
* @param modifiedPackageJson The modified package.json content that was used for install
|
|
416
363
|
*/
|
|
417
|
-
export function
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
364
|
+
export function storeInstallResult<T extends BaseProjectUpdateInfo>(
|
|
365
|
+
result: TempInstallResult,
|
|
366
|
+
acc: DependencyRecipeAccumulator<T>,
|
|
367
|
+
updateInfo: T,
|
|
368
|
+
modifiedPackageJson: string
|
|
369
|
+
): void {
|
|
370
|
+
if (result.success) {
|
|
371
|
+
acc.updatedPackageJsons.set(updateInfo.packageJsonPath, modifiedPackageJson);
|
|
372
|
+
|
|
373
|
+
if (result.lockFileContent) {
|
|
374
|
+
const lockFileName = getLockFileName(updateInfo.packageManager);
|
|
375
|
+
const lockFilePath = updateInfo.packageJsonPath.replace('package.json', lockFileName);
|
|
376
|
+
acc.updatedLockFiles.set(lockFilePath, result.lockFileContent);
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
acc.failedProjects.set(updateInfo.packageJsonPath, result.error || 'Unknown error');
|
|
429
380
|
}
|
|
430
381
|
}
|
|
431
382
|
|
|
432
383
|
/**
|
|
433
|
-
*
|
|
384
|
+
* Runs the package manager install for a project if it hasn't been processed yet.
|
|
385
|
+
* Updates the accumulator's processedProjects set after running.
|
|
386
|
+
*
|
|
387
|
+
* @param sourcePath The source path (package.json path) being processed
|
|
388
|
+
* @param acc The recipe accumulator
|
|
389
|
+
* @param runInstall Function that performs the actual install (recipe-specific)
|
|
390
|
+
* @returns The failure message if install failed, undefined otherwise
|
|
434
391
|
*/
|
|
435
|
-
export
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
392
|
+
export async function runInstallIfNeeded<T>(
|
|
393
|
+
sourcePath: string,
|
|
394
|
+
acc: DependencyRecipeAccumulator<T>,
|
|
395
|
+
runInstall: () => Promise<void>
|
|
396
|
+
): Promise<string | undefined> {
|
|
397
|
+
if (!acc.processedProjects.has(sourcePath)) {
|
|
398
|
+
await runInstall();
|
|
399
|
+
acc.processedProjects.add(sourcePath);
|
|
400
|
+
}
|
|
401
|
+
return acc.failedProjects.get(sourcePath);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Updates the NodeResolutionResult marker on a JSON document after a package manager operation.
|
|
406
|
+
* This recreates the marker based on the updated package.json and lock file content.
|
|
407
|
+
*
|
|
408
|
+
* @param doc The JSON document containing the marker
|
|
409
|
+
* @param updateInfo Project update info with paths and package manager
|
|
410
|
+
* @param acc The recipe accumulator containing updated content
|
|
411
|
+
* @returns The document with the updated marker, or unchanged if no existing marker
|
|
412
|
+
*/
|
|
413
|
+
export async function updateNodeResolutionMarker<T extends BaseProjectUpdateInfo>(
|
|
414
|
+
doc: Json.Document,
|
|
415
|
+
updateInfo: T & { originalPackageJson: string },
|
|
416
|
+
acc: DependencyRecipeAccumulator<T>
|
|
417
|
+
): Promise<Json.Document> {
|
|
418
|
+
const existingMarker = findNodeResolutionResult(doc);
|
|
419
|
+
if (!existingMarker) {
|
|
420
|
+
return doc;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Parse the updated package.json and lock file to create new marker
|
|
424
|
+
const updatedPackageJson = acc.updatedPackageJsons.get(updateInfo.packageJsonPath);
|
|
425
|
+
const lockFileName = getLockFileName(updateInfo.packageManager);
|
|
426
|
+
const updatedLockFile = acc.updatedLockFiles.get(
|
|
427
|
+
updateInfo.packageJsonPath.replace('package.json', lockFileName)
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
let packageJsonContent: PackageJsonContent;
|
|
431
|
+
let lockContent: PackageLockContent | undefined;
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
packageJsonContent = JSON.parse(updatedPackageJson || updateInfo.originalPackageJson);
|
|
435
|
+
} catch {
|
|
436
|
+
return doc; // Failed to parse, keep original marker
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (updatedLockFile) {
|
|
440
|
+
try {
|
|
441
|
+
lockContent = JSON.parse(updatedLockFile);
|
|
442
|
+
} catch {
|
|
443
|
+
// Continue without lock file content
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Read npmrc configs from the project directory
|
|
448
|
+
const npmrcConfigs = await readNpmrcConfigs(updateInfo.projectDir);
|
|
449
|
+
|
|
450
|
+
// Create new marker
|
|
451
|
+
const newMarker = createNodeResolutionResultMarker(
|
|
452
|
+
existingMarker.path,
|
|
453
|
+
packageJsonContent,
|
|
454
|
+
lockContent,
|
|
455
|
+
existingMarker.workspacePackagePaths,
|
|
456
|
+
existingMarker.packageManager,
|
|
457
|
+
npmrcConfigs.length > 0 ? npmrcConfigs : undefined
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// Replace the marker in the document
|
|
461
|
+
return {
|
|
462
|
+
...doc,
|
|
463
|
+
markers: replaceMarkerByKind(doc.markers, newMarker)
|
|
464
|
+
};
|
|
442
465
|
}
|
|
443
466
|
|
|
444
467
|
/**
|