/* * 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 {PackageManager} from "./node-resolution-result";
/**
* Parsed dependency path for scoped overrides.
* Segments represent the chain of dependencies, e.g., "express>accepts" becomes
* [{name: "express"}, {name: "accepts"}]
*/
export interface DependencyPathSegment {
name: string;
version?: string;
}
/**
* Parses a dependency path string into segments.
* Accepts both '>' (pnpm style) and '/' (yarn style) as separators.
* Examples:
* "express>accepts" -> [{name: "express"}, {name: "accepts"}]
* "express@4.0.0>accepts" -> [{name: "express", version: "4.0.0"}, {name: "accepts"}]
* "@scope/pkg>dep" -> [{name: "@scope/pkg"}, {name: "dep"}]
*/
export function parseDependencyPath(path: string): DependencyPathSegment[] {
// We can't just replace all '/' with '>' because scoped packages contain '/'
// Strategy: Split on '>' first, then for each part that contains '/' and doesn't
// start with '@', treat it as a '/'-separated path (yarn style)
const segments: DependencyPathSegment[] = [];
// Split on '>' (pnpm style separator)
const gtParts = path.split('>');
for (const gtPart of gtParts) {
// Check if this part needs further splitting by '/'
// Only split if it contains '/' AND either:
// - doesn't start with '@' (not a scoped package), OR
// - contains multiple '/' (e.g., "@scope/pkg/dep" is yarn-style path)
if (gtPart.includes('/')) {
if (gtPart.startsWith('@')) {
// Scoped package: @scope/pkg or @scope/pkg@version or @scope/pkg/dep (yarn path)
// Find the first '/' which is part of the scope
const firstSlash = gtPart.indexOf('/');
const afterFirstSlash = gtPart.substring(firstSlash + 1);
// Check if there's another '/' after the scope (yarn-style nesting)
const secondSlash = afterFirstSlash.indexOf('/');
if (secondSlash !== -1) {
// yarn-style: @scope/pkg/dep - split further
// First get the scoped package part
const scopedPart = gtPart.substring(0, firstSlash + 1 + secondSlash);
segments.push(parseSegment(scopedPart));
// Then handle the rest as separate segments
const rest = afterFirstSlash.substring(secondSlash + 1);
for (const subPart of rest.split('/')) {
if (subPart) {
segments.push(parseSegment(subPart));
}
}
} else {
// Simple scoped package: @scope/pkg or @scope/pkg@version
segments.push(parseSegment(gtPart));
}
} else {
// Non-scoped with '/': yarn-style path like "express/accepts"
for (const slashPart of gtPart.split('/')) {
if (slashPart) {
segments.push(parseSegment(slashPart));
}
}
}
} else {
// No '/', just parse the segment directly
segments.push(parseSegment(gtPart));
}
}
return segments;
}
/**
* Parses a single segment (package name, possibly with version).
*/
function parseSegment(part: string): DependencyPathSegment {
// Handle scoped packages: @scope/name or @scope/name@version
if (part.startsWith('@')) {
// Find the version separator (last @ that's not the scope prefix)
const slashIndex = part.indexOf('/');
if (slashIndex === -1) {
return {name: part};
}
const afterSlash = part.substring(slashIndex + 1);
const atIndex = afterSlash.lastIndexOf('@');
if (atIndex > 0) {
return {
name: part.substring(0, slashIndex + 1 + atIndex),
version: afterSlash.substring(atIndex + 1)
};
}
return {name: part};
}
// Non-scoped package: name or name@version
const atIndex = part.lastIndexOf('@');
if (atIndex > 0) {
return {
name: part.substring(0, atIndex),
version: part.substring(atIndex + 1)
};
}
return {name: part};
}
/**
* Generates an npm-style override entry (nested objects).
* npm uses nested objects for scoped overrides:
* { "express": { "accepts": "^2.0.0" } }
* or for global overrides:
* { "lodash": "^4.17.21" }
*/
function generateNpmOverride(
packageName: string,
newVersion: string,
pathSegments?: DependencyPathSegment[]
): Record