/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://docs.moderne.io/licensing/moderne-source-available-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createNodeResolutionResultMarker,
findNodeResolutionResult,
PackageJsonContent,
PackageLockContent,
PackageManager
} from "./node-resolution-result";
import {replaceMarkerByKind} from "../markers";
import {Json, JsonParser, JsonVisitor} from "../json";
import {isDocuments, Yaml, YamlParser, YamlVisitor} from "../yaml";
import {PlainTextParser} from "../text";
import {SourceFile} from "../tree";
import {TreeVisitor} from "../visitor";
import {ExecutionContext} from "../execution";
import {getPlatformCommand} from "../shell-utils";
import * as fs from "fs";
import * as fsp from "fs/promises";
import * as path from "path";
import * as os from "os";
import {spawnSync} from "child_process";
import {isDeepStrictEqual} from "util";
import * as YAML from "yaml";
/**
* Configuration for each package manager.
*/
interface PackageManagerConfig {
/** The lock file name for this package manager */
lockFile: string;
/** Command to install dependencies and update lock file only (no node_modules) */
installLockOnlyCommand: string[];
/** Command to install dependencies fully */
installCommand: string[];
/** Command to list dependencies in JSON format (if supported) */
listCommand?: string[];
}
/**
* Package manager configurations.
*/
const PACKAGE_MANAGER_CONFIGS: Record = {
[PackageManager.Npm]: {
lockFile: 'package-lock.json',
// --ignore-scripts prevents prepublish/prepare scripts from running in temp directory.
// --legacy-peer-deps disables npm 7+'s strict peer-dep enforcement. The install here is
// purely a validity check on the bumped package.json; the resulting node_modules is not
// shipped to the user. Strict peer enforcement creates false negatives in common
// ecosystem upgrades (e.g. Angular major bumps where third-party libs lag), which would
// otherwise prevent the bump from being written even when it is correct.
installLockOnlyCommand: ['npm', 'install', '--package-lock-only', '--ignore-scripts', '--legacy-peer-deps'],
installCommand: ['npm', 'install', '--ignore-scripts', '--legacy-peer-deps'],
listCommand: ['npm', 'list', '--json', '--all'],
},
[PackageManager.YarnClassic]: {
lockFile: 'yarn.lock',
// Yarn Classic doesn't have a lock-only mode; --ignore-scripts prevents lifecycle scripts
installLockOnlyCommand: ['yarn', 'install', '--ignore-scripts'],
installCommand: ['yarn', 'install', '--ignore-scripts'],
listCommand: ['yarn', 'list', '--json'],
},
[PackageManager.YarnBerry]: {
lockFile: 'yarn.lock',
// --mode skip-build skips post-install scripts in Yarn Berry
installLockOnlyCommand: ['yarn', 'install', '--mode', 'skip-build'],
installCommand: ['yarn', 'install', '--mode', 'skip-build'],
listCommand: ['yarn', 'info', '--all', '--json'],
},
[PackageManager.Pnpm]: {
lockFile: 'pnpm-lock.yaml',
// --ignore-scripts prevents lifecycle scripts from running in temp directory.
// --no-strict-peer-dependencies disables pnpm's strict peer-dep enforcement (see the
// `npm` entry above for rationale).
installLockOnlyCommand: ['pnpm', 'install', '--lockfile-only', '--ignore-scripts', '--no-strict-peer-dependencies'],
installCommand: ['pnpm', 'install', '--ignore-scripts', '--no-strict-peer-dependencies'],
listCommand: ['pnpm', 'list', '--json', '--depth=Infinity'],
},
[PackageManager.Bun]: {
lockFile: 'bun.lock',
// Bun doesn't have a lock-only mode; --ignore-scripts prevents lifecycle scripts
installLockOnlyCommand: ['bun', 'install', '--ignore-scripts'],
installCommand: ['bun', 'install', '--ignore-scripts'],
},
};
/**
* Configuration for lock file detection.
*/
export interface LockFileDetectionConfig {
/** The lock file name */
filename: string;
/** The package manager, or a function to detect it from file content */
packageManager: PackageManager | ((content: string) => PackageManager);
/** If true, prefer walking node_modules over parsing lock file (lock file may omit details) */
preferNodeModules?: boolean;
}
/**
* Lock file detection configuration with priority order.
* Priority order determines which package manager is detected when multiple lock files exist.
*/
const LOCK_FILE_DETECTION: ReadonlyArray = [
{filename: 'package-lock.json', packageManager: PackageManager.Npm},
{filename: 'bun.lock', packageManager: PackageManager.Bun},
{filename: 'pnpm-lock.yaml', packageManager: PackageManager.Pnpm, preferNodeModules: true},
{
filename: 'yarn.lock',
packageManager: (content) =>
content.includes('__metadata:') ? PackageManager.YarnBerry : PackageManager.YarnClassic,
// yarn.lock omits transitive dependency details (engines/license), so prefer node_modules
preferNodeModules: true
},
];
/**
* Lock file names that should be parsed as JSON/JSONC format.
*/
export const JSON_LOCK_FILE_NAMES = ['bun.lock', 'package-lock.json'] as const;
/**
* Lock file names that should be parsed as YAML format.
*/
export const YAML_LOCK_FILE_NAMES = ['pnpm-lock.yaml'] as const;
/**
* Lock file names that should be parsed as plain text (custom formats like yarn.lock v1).
* Note: yarn.lock for Yarn Berry (v2+) is actually YAML format and should be parsed as such.
* Use `getLockFileFormat` with content to determine the correct format for yarn.lock.
*/
export const TEXT_LOCK_FILE_NAMES = ['yarn.lock'] as const;
/**
* Detects if a yarn.lock file is Yarn Berry (v2+) format based on content.
* Yarn Berry lock files contain a `__metadata:` key which is not present in Classic.
*
* @param content The yarn.lock file content
* @returns true if this is a Yarn Berry lock file (YAML format), false for Classic
*/
export function isYarnBerryLockFile(content: string): boolean {
return content.includes('__metadata:');
}
/**
* Result of running a package manager command.
*/
interface PackageManagerResult {
success: boolean;
stdout?: string;
stderr?: string;
error?: string;
}
/**
* Options for running package manager install.
*/
interface InstallOptions {
/** Working directory */
cwd: string;
/** If true, only update lock file without installing to node_modules */
lockOnly?: boolean;
/** Timeout in milliseconds (default: 120000 = 2 minutes) */
timeout?: number;
/** Additional environment variables */
env?: Record;
}
/**
* Detects the package manager used in a directory by checking for lock files.
*
* @param dir The directory to check
* @returns The detected package manager, or undefined if none found
*/
export function detectPackageManager(dir: string): PackageManager | undefined {
for (const config of LOCK_FILE_DETECTION) {
const lockPath = path.join(dir, config.filename);
if (fs.existsSync(lockPath)) {
if (typeof config.packageManager === 'function') {
try {
const content = fs.readFileSync(lockPath, 'utf-8');
return config.packageManager(content);
} catch {
continue;
}
}
return config.packageManager;
}
}
return undefined;
}
/**
* Gets the lock file detection configuration.
* Returns the array of lock file configs in priority order.
*/
export function getLockFileDetectionConfig(): ReadonlyArray {
return LOCK_FILE_DETECTION;
}
/**
* Gets the lock file name for a package manager.
*/
export function getLockFileName(pm: PackageManager): string {
return PACKAGE_MANAGER_CONFIGS[pm].lockFile;
}
/**
* Gets all supported lock file names.
*/
export function getAllLockFileNames(): string[] {
return LOCK_FILE_DETECTION.map(c => c.filename);
}
/**
* Runs the package manager install command.
*/
function runInstall(pm: PackageManager, options: InstallOptions): PackageManagerResult {
const config = PACKAGE_MANAGER_CONFIGS[pm];
const command = options.lockOnly ? config.installLockOnlyCommand : config.installCommand;
const [cmd, ...args] = command;
try {
const result = spawnSync(getPlatformCommand(cmd), args, {
cwd: options.cwd,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: options.timeout ?? 120000,
env: options.env ? {...process.env, ...options.env} : process.env,
...(os.platform() === 'win32' ? { shell: true } : {})
});
if (result.error) {
return {
success: false,
error: result.error.message,
stderr: result.stderr,
};
}
if (result.status !== 0) {
return {
success: false,
stdout: result.stdout,
stderr: result.stderr,
error: `Command exited with code ${result.status}`,
};
}
return {
success: true,
stdout: result.stdout,
stderr: result.stderr,
};
} catch (error: any) {
return {
success: false,
error: error.message,
};
}
}
/**
* Runs a package manager list command to get dependency information.
*
* @param pm The package manager to use
* @param cwd Working directory
* @param timeout Timeout in milliseconds
* @returns The JSON output, or undefined if failed
*/
export function runList(pm: PackageManager, cwd: string, timeout: number = 30000): string | undefined {
const config = PACKAGE_MANAGER_CONFIGS[pm];
if (!config.listCommand) {
return undefined;
}
const [cmd, ...args] = config.listCommand;
const result = spawnSync(getPlatformCommand(cmd), args, {
cwd,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout,
...(os.platform() === 'win32' ? { shell: true } : {})
});
if (result.error || result.status !== 0) {
return undefined;
}
return result.stdout;
}
/**
* Result of running install in a temporary directory.
*/
export interface TempInstallResult {
/** Whether the install succeeded */
success: boolean;
/** The updated lock file content (if successful and lock file exists) */
lockFileContent?: string;
/** Error message (if failed) */
error?: string;
}
/**
* Generic accumulator for dependency recipes that run package manager operations.
* Used by scanning recipes to track state across scanning and editing phases.
*
* @typeParam T The recipe-specific project update info type
*/
export interface DependencyRecipeAccumulator {
/** Projects that need updating: packageJsonPath -> update info */
projectsToUpdate: Map;
/** After running package manager, store the updated lock file content */
updatedLockFiles: Map;
/** Updated package.json content (after npm install may have modified it) */
updatedPackageJsons: Map;
/** Track which projects have been processed (npm install has run) */
processedProjects: Set;
/** Track projects where npm install failed: packageJsonPath -> error message */
failedProjects: Map;
}
/**
* Creates a new empty accumulator for dependency recipes.
*/
export function createDependencyRecipeAccumulator(): DependencyRecipeAccumulator {
return {
projectsToUpdate: new Map(),
updatedLockFiles: new Map(),
updatedPackageJsons: new Map(),
processedProjects: new Set(),
failedProjects: new Map()
};
}
/**
* Checks if a source path is a lock file and returns the updated content if available.
* This is a helper for dependency recipes that need to update lock files.
*
* @param sourcePath The source path to check
* @param acc The recipe accumulator containing updated lock file content
* @returns The updated lock file content if this is a lock file that was updated, undefined otherwise
*/
export function getUpdatedLockFileContent(
sourcePath: string,
acc: DependencyRecipeAccumulator
): string | undefined {
for (const lockFileName of getAllLockFileNames()) {
if (sourcePath.endsWith(lockFileName)) {
// Find the corresponding package.json path
const packageJsonPath = sourcePath.replace(lockFileName, 'package.json');
const updateInfo = acc.projectsToUpdate.get(packageJsonPath);
if (updateInfo && acc.updatedLockFiles.has(sourcePath)) {
return acc.updatedLockFiles.get(sourcePath);
}
break;
}
}
return undefined;
}
/**
* Determines the appropriate parser for a lock file based on its filename and optionally content.
*
* For yarn.lock files, the format depends on the Yarn version:
* - Yarn Classic (v1): Custom plain text format
* - Yarn Berry (v2+): YAML format
*
* If content is provided for yarn.lock, it will be used to detect the format.
* Otherwise, defaults to 'text' (Yarn Classic).
*
* @param lockFileName The lock file name (e.g., "pnpm-lock.yaml", "package-lock.json", "yarn.lock")
* @param content Optional file content (used for yarn.lock format detection)
* @returns 'yaml' for YAML lock files, 'json' for JSON lock files, 'text' for plain text
*/
export function getLockFileFormat(lockFileName: string, content?: string): 'yaml' | 'json' | 'text' {
if ((YAML_LOCK_FILE_NAMES as readonly string[]).includes(lockFileName)) {
return 'yaml';
}
if (lockFileName === 'yarn.lock') {
// Yarn Berry (v2+) uses YAML format, Classic uses custom text format
if (content && isYarnBerryLockFile(content)) {
return 'yaml';
}
return 'text';
}
if ((TEXT_LOCK_FILE_NAMES as readonly string[]).includes(lockFileName)) {
return 'text';
}
// package-lock.json, bun.lock
return 'json';
}
/**
* Re-parses updated lock file content using the appropriate parser.
* This is used by dependency recipes to create the updated lock file SourceFile.
*
* For yarn.lock files, the content is used to detect whether it's Yarn Berry (YAML)
* or Yarn Classic (plain text) format.
*
* @param content The updated lock file content
* @param sourcePath The source path of the lock file
* @param lockFileName The lock file name (e.g., "pnpm-lock.yaml", "yarn.lock")
* @returns The parsed SourceFile (Json.Document, Yaml.Documents, or PlainText)
*/
export async function parseLockFileContent(
content: string,
sourcePath: string,
lockFileName: string
): Promise {
// Pass content to getLockFileFormat for yarn.lock detection
const format = getLockFileFormat(lockFileName, content);
switch (format) {
case 'yaml': {
const parser = new YamlParser({});
return await parser.parseOne({text: content, sourcePath}) as Yaml.Documents;
}
case 'text': {
const parser = new PlainTextParser({});
return await parser.parseOne({text: content, sourcePath});
}
case 'json':
default: {
const parser = new JsonParser({});
return await parser.parseOne({text: content, sourcePath}) as Json.Document;
}
}
}
/**
* Base interface for project update info used by dependency recipes.
* Recipes extend this with additional fields specific to their needs.
*/
export interface BaseProjectUpdateInfo {
/** Relative path to package.json (from source root) */
packageJsonPath: string;
/** The package manager used by this project */
packageManager: PackageManager;
}
/**
* Stores the result of a package manager install into the accumulator.
* This handles the common pattern of storing updated lock files and tracking failures.
*
* @param result The result from runInstallInTempDir
* @param acc The recipe accumulator
* @param updateInfo The project update info (must have packageJsonPath and packageManager)
* @param modifiedPackageJson The modified package.json content that was used for install
*/
export function storeInstallResult(
result: TempInstallResult,
acc: DependencyRecipeAccumulator,
updateInfo: T,
modifiedPackageJson: string
): void {
if (result.success) {
acc.updatedPackageJsons.set(updateInfo.packageJsonPath, modifiedPackageJson);
if (result.lockFileContent) {
const lockFileName = getLockFileName(updateInfo.packageManager);
const lockFilePath = updateInfo.packageJsonPath.replace('package.json', lockFileName);
acc.updatedLockFiles.set(lockFilePath, result.lockFileContent);
}
} else {
acc.failedProjects.set(updateInfo.packageJsonPath, result.error || 'Unknown error');
}
}
/**
* Runs the package manager install for a project if it hasn't been processed yet.
* Updates the accumulator's processedProjects set after running.
*
* @param sourcePath The source path (package.json path) being processed
* @param acc The recipe accumulator
* @param runInstall Function that performs the actual install (recipe-specific)
* @returns The failure message if install failed, undefined otherwise
*/
export async function runInstallIfNeeded(
sourcePath: string,
acc: DependencyRecipeAccumulator,
runInstall: () => Promise
): Promise {
if (!acc.processedProjects.has(sourcePath)) {
await runInstall();
acc.processedProjects.add(sourcePath);
}
return acc.failedProjects.get(sourcePath);
}
/**
* Updates the NodeResolutionResult marker on a JSON document after a package manager operation.
* This recreates the marker based on the updated package.json and lock file content.
*
* @param doc The JSON document containing the marker
* @param updateInfo Project update info with paths and package manager
* @param acc The recipe accumulator containing updated content
* @returns The document with the updated marker, or unchanged if no existing marker
*/
export async function updateNodeResolutionMarker(
doc: Json.Document,
updateInfo: T & { originalPackageJson: string },
acc: DependencyRecipeAccumulator
): Promise {
const existingMarker = findNodeResolutionResult(doc);
if (!existingMarker) {
return doc;
}
// Parse the updated package.json and lock file to create new marker
const updatedPackageJson = acc.updatedPackageJsons.get(updateInfo.packageJsonPath);
const lockFileName = getLockFileName(updateInfo.packageManager);
const updatedLockFile = acc.updatedLockFiles.get(
updateInfo.packageJsonPath.replace('package.json', lockFileName)
);
let packageJsonContent: PackageJsonContent;
let lockContent: PackageLockContent | undefined;
try {
packageJsonContent = JSON.parse(updatedPackageJson || updateInfo.originalPackageJson);
} catch {
return doc; // Failed to parse, keep original marker
}
if (updatedLockFile) {
try {
// Parse lock file based on format
if (updateInfo.packageManager === PackageManager.Pnpm) {
// pnpm-lock.yaml is YAML format
lockContent = YAML.parse(updatedLockFile);
} else if (updateInfo.packageManager === PackageManager.YarnClassic ||
updateInfo.packageManager === PackageManager.YarnBerry) {
// yarn.lock has a custom format - skip parsing here
// The marker will still be updated with package.json info
lockContent = undefined;
} else {
// npm (package-lock.json) and bun (bun.lock) use JSON
lockContent = JSON.parse(updatedLockFile);
}
} catch {
// Continue without lock file content
}
}
// Create new marker, preserving existing npmrc configs from the parser
// (recipes don't have filesystem access to re-read them)
const newMarker = createNodeResolutionResultMarker(
existingMarker.path,
packageJsonContent,
lockContent,
existingMarker.workspacePackagePaths,
existingMarker.packageManager,
existingMarker.npmrcConfigs
);
// If the new marker is structurally identical to the existing one (ignoring the
// auto-generated id), return doc unchanged to avoid minting a fresh AST identity
// for a no-op marker update. This prevents the empty-diff guard from firing on
// recipes that don't modify package.json (e.g. lock-file-only fix strategies).
const {id: _existingId, ...existingRest} = existingMarker;
const {id: _newId, ...newRest} = newMarker;
if (isDeepStrictEqual(existingRest, newRest)) {
return doc;
}
// Replace the marker in the document
return {
...doc,
markers: replaceMarkerByKind(doc.markers, newMarker)
};
}
/**
* Options for running install in a temporary directory.
*/
export interface TempInstallOptions {
/** Timeout in milliseconds (default: 120000 = 2 minutes) */
timeout?: number;
/**
* If true, only update the lock file without installing node_modules.
* If false, perform a full install which creates node_modules in the temp dir.
* Default: true (lock-only is faster and sufficient for most cases)
*/
lockOnly?: boolean;
/**
* Original lock file content to use. If provided, this content will be written
* to the temp directory instead of copying from projectDir.
* This allows recipes to work with in-memory SourceFiles without filesystem access.
*/
originalLockFileContent?: string;
/**
* Config file contents to use. Keys are filenames (e.g., '.npmrc'), values are content.
* If provided, these will be written to the temp directory instead of copying from projectDir.
*/
configFiles?: Record;
}
/**
* Options for running install in a temporary directory with workspace support.
*/
export interface WorkspaceTempInstallOptions extends TempInstallOptions {
/**
* Workspace package.json files. Keys are relative paths from the project root
* (e.g., "packages/foo/package.json"), values are the package.json content.
* The root package.json should have a "workspaces" field pointing to these packages.
*/
workspacePackages?: Record;
}
/**
* Internal implementation for running package manager install in a temporary directory.
* Supports both simple projects and workspaces.
*/
async function runInstallInTempDirCore(
pm: PackageManager,
rootPackageJson: string,
options: WorkspaceTempInstallOptions = {}
): Promise {
const {
timeout = 120000,
lockOnly = true,
originalLockFileContent,
configFiles: configFileContents,
workspacePackages
} = options;
const lockFileName = getLockFileName(pm);
const tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'openrewrite-pm-'));
try {
// Write root package.json to temp directory
await fsp.writeFile(path.join(tempDir, 'package.json'), rootPackageJson);
// Write workspace package.json files (creating subdirectories as needed)
if (workspacePackages) {
for (const [relativePath, content] of Object.entries(workspacePackages)) {
const fullPath = path.join(tempDir, relativePath);
await fsp.mkdir(path.dirname(fullPath), {recursive: true});
await fsp.writeFile(fullPath, content);
}
}
// Write lock file if provided
if (originalLockFileContent !== undefined) {
await fsp.writeFile(path.join(tempDir, lockFileName), originalLockFileContent);
}
// Write config files if provided
if (configFileContents) {
for (const [configFile, content] of Object.entries(configFileContents)) {
await fsp.writeFile(path.join(tempDir, configFile), content);
}
}
// Run package manager install
const result = runInstall(pm, {
cwd: tempDir,
lockOnly,
timeout
});
if (!result.success) {
// Combine error message with stderr for more useful diagnostics
const errorParts: string[] = [];
if (result.error) {
errorParts.push(result.error);
}
if (result.stderr) {
// Trim and limit stderr to avoid excessively long error messages
const stderr = result.stderr.trim();
if (stderr) {
errorParts.push(stderr.length > 2000 ? stderr.slice(0, 2000) + '...' : stderr);
}
}
return {
success: false,
error: errorParts.length > 0 ? errorParts.join('\n\n') : 'Unknown error'
};
}
// Read back the updated lock file
const updatedLockPath = path.join(tempDir, lockFileName);
let lockFileContent: string | undefined;
if (fs.existsSync(updatedLockPath)) {
lockFileContent = await fsp.readFile(updatedLockPath, 'utf-8');
}
return {
success: true,
lockFileContent
};
} finally {
// Cleanup temp directory
try {
await fsp.rm(tempDir, {recursive: true, force: true});
} catch {
// Ignore cleanup errors
}
}
}
/**
* Runs package manager install in a temporary directory.
*
* This function:
* 1. Creates a temp directory
* 2. Writes the provided package.json content
* 3. Writes the lock file content (if provided)
* 4. Writes config files (if provided)
* 5. Runs the package manager install
* 6. Returns the updated lock file content
* 7. Cleans up the temp directory
*
* @param pm The package manager to use
* @param modifiedPackageJson The modified package.json content to use
* @param options Optional settings for timeout, lock-only mode, and file contents
* @returns Result containing success status and lock file content or error
*/
export async function runInstallInTempDir(
pm: PackageManager,
modifiedPackageJson: string,
options: TempInstallOptions = {}
): Promise {
return runInstallInTempDirCore(pm, modifiedPackageJson, options);
}
/**
* Runs package manager install in a temporary directory with workspace support.
*
* This function:
* 1. Creates a temp directory
* 2. Writes the root package.json content
* 3. Writes workspace package.json files (creating subdirectories as needed)
* 4. Writes the lock file content (if provided)
* 5. Writes config files (if provided)
* 6. Runs the package manager install at the root
* 7. Returns the updated lock file content
* 8. Cleans up the temp directory
*
* @param pm The package manager to use
* @param rootPackageJson The root package.json content (should contain "workspaces" field)
* @param options Optional settings including workspace packages, timeout, lock-only mode, and file contents
* @returns Result containing success status and lock file content or error
*/
export async function runWorkspaceInstallInTempDir(
pm: PackageManager,
rootPackageJson: string,
options: WorkspaceTempInstallOptions = {}
): Promise {
return runInstallInTempDirCore(pm, rootPackageJson, options);
}
/**
* Creates a lock file visitor that handles updating YAML lock files (pnpm-lock.yaml).
* This is a reusable component for dependency recipes.
*
* @param acc The recipe accumulator containing updated lock file content
* @returns A YamlVisitor that updates YAML lock files
*/
export function createYamlLockFileVisitor(
acc: DependencyRecipeAccumulator
): YamlVisitor {
return new class extends YamlVisitor {
protected async visitDocuments(docs: Yaml.Documents, _ctx: ExecutionContext): Promise {
const sourcePath = docs.sourcePath;
const updatedLockContent = getUpdatedLockFileContent(sourcePath, acc);
if (updatedLockContent) {
const lockFileName = path.basename(sourcePath);
const parsed = await parseLockFileContent(updatedLockContent, sourcePath, lockFileName) as Yaml.Documents;
// Preserve original ID for RPC compatibility
return {
...docs,
documents: parsed.documents,
suffix: parsed.suffix
} as Yaml.Documents;
}
return docs;
}
};
}
/**
* Creates a composite visitor that delegates to the appropriate editor based on tree type.
* This handles both JSON (package-lock.json, bun.lock) and YAML (pnpm-lock.yaml) lock files.
*
* @param jsonEditor The JSON visitor for handling JSON files
* @param acc The recipe accumulator for YAML lock file handling
* @returns A TreeVisitor that handles both JSON and YAML files
*/
export function createLockFileEditor(
jsonEditor: JsonVisitor,
acc: DependencyRecipeAccumulator
): TreeVisitor {
const yamlEditor = createYamlLockFileVisitor(acc);
return new class extends TreeVisitor {
async visit(tree: any, ctx: ExecutionContext): Promise {
if (isDocuments(tree)) {
return yamlEditor.visit(tree, ctx);
} else if (tree && tree.kind === Json.Kind.Document) {
return jsonEditor.visit(tree, ctx);
}
return tree;
}
};
}