Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 146 additions & 95 deletions scopes/dependencies/dependencies/dependencies-loader/apply-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,107 +555,158 @@ export class ApplyOverrides {
}

const originallyExists: string[] = [];
let missingPackages: string[] = [];
// We want to also add missing packages to the peer list as we know to resolve the version from the env anyway
const missingPackages = this.getMissingPackagesFromIssues();

['dependencies', 'devDependencies', 'peerDependencies'].forEach((field) => {
forEach(autoDetectOverrides[field], (pkgVal, pkgName) => {
this.applyAutoDetectedOverrideForPackage(field, pkgName, pkgVal, originallyExists, missingPackages);
});
});
}

private getMissingPackagesFromIssues(): string[] {
const missingData = this.issues.getIssueByName('MissingPackagesDependenciesOnFs')?.data as
| MissingPackagesData[]
| undefined;
if (missingData) {
missingPackages = uniq(missingData.map((d) => d.missingPackages).flat());
if (!missingData) return [];
return uniq(missingData.map((d) => d.missingPackages).flat());
}

private isPackageAutoDetected(pkgName: string, originallyExists: string[], missingPackages: string[]): boolean {
// Check component dependencies
const existsInAnyCompDeps =
this.allDependencies.dependencies.some((dep) => dep.packageName === pkgName) ||
this.allDependencies.devDependencies.some((dep) => dep.packageName === pkgName) ||
this.allDependencies.peerDependencies.some((dep) => dep.packageName === pkgName);

// Check originAllPackagesDependencies instead of allPackagesDependencies
// as it might be already removed at this point if it was set with "-" in runtime/dev
const existsInOriginalPkgDeps =
this.originAllPackagesDependencies.packageDependencies[pkgName] ||
this.originAllPackagesDependencies.devPackageDependencies[pkgName] ||
this.originAllPackagesDependencies.peerPackageDependencies[pkgName];

// Check if it was originally exists in the component
// This handles policies like: dependencies: {"my-dep": "-"}, devDependencies: {"my-dep": "1.0.0"}
// where we might remove it before getting to devDeps
return (
existsInAnyCompDeps ||
!!existsInOriginalPkgDeps ||
originallyExists.includes(pkgName) ||
missingPackages.includes(pkgName)
);
}

private applyAutoDetectedOverrideForPackage(
field: string,
pkgName: string,
pkgVal: string,
originallyExists: string[],
missingPackages: string[]
): void {
if (this.overridesDependencies.shouldIgnorePeerPackage(pkgName)) {
return;
}
['dependencies', 'devDependencies', 'peerDependencies'].forEach((field) => {
forEach(autoDetectOverrides[field], (pkgVal, pkgName) => {
if (this.overridesDependencies.shouldIgnorePeerPackage(pkgName)) {
return;
}

const existsInCompsDeps = this.allDependencies.dependencies.find((dep) => dep.packageName === pkgName);
const existsInCompsDevDeps = this.allDependencies.devDependencies.find((dep) => dep.packageName === pkgName);
const existsInCompsPeerDeps = this.allDependencies.peerDependencies.find((dep) => dep.packageName === pkgName);

// Validate it was auto detected, we only affect stuff that were detected
const isAutoDetected =
existsInCompsDeps ||
existsInCompsDevDeps ||
existsInCompsPeerDeps ||
// We are checking originAllPackagesDependencies instead of allPackagesDependencies
// as it might be already removed from allPackagesDependencies at this point if it was set with
// "-" in runtime/dev
// in such case we still want to apply it here
this.originAllPackagesDependencies.packageDependencies[pkgName] ||
this.originAllPackagesDependencies.devPackageDependencies[pkgName] ||
this.originAllPackagesDependencies.peerPackageDependencies[pkgName] ||
// Check if it was originally exists in the component
// as we might have a policy which looks like this:
// "components": {
// "dependencies": {
// "my-dep": "-"
// },
// "devDependencies": {
// "my-dep": "1.0.0"
// },
// }
// in that case we might remove it before getting to the devDeps then we will think that it wasn't required in the component
// which is incorrect
originallyExists.includes(pkgName) ||
missingPackages.includes(pkgName);

if (!isAutoDetected) {
return;
}
originallyExists.push(pkgName);
const key = DepsKeysToAllPackagesDepsKeys[field];
delete this.allPackagesDependencies[key][pkgName];
// When changing peer dependency we want it to be stronger than the other types
if (field === 'peerDependencies') {
delete this.allPackagesDependencies.devPackageDependencies[pkgName];
delete this.allPackagesDependencies.packageDependencies[pkgName];
if (existsInCompsDeps) {
this.allDependencies.dependencies = this.allDependencies.dependencies.filter(
(dep) => dep.packageName !== pkgName
);
}
if (existsInCompsDevDeps) {
this.allDependencies.devDependencies = this.allDependencies.devDependencies.filter(
(dep) => dep.packageName !== pkgName
);
}
}
if (!this.isPackageAutoDetected(pkgName, originallyExists, missingPackages)) {
return;
}

// This was restored to fix an issue with a case where
// You have a package dep in env.jsonc under peers (like @testing-library/react)
// Then you change the env.jsonc and move it from peer to devDependencies
// the deps resolver data will be correct, but in the legacy data
// it will still be in the peerPackageDependencies, so we need to remove it from there
// to avoid having it in package.json as a peer dependency
// which then will affect the installation of the component
delete this.allPackagesDependencies.packageDependencies[pkgName];
delete this.allPackagesDependencies.devPackageDependencies[pkgName];
delete this.allPackagesDependencies.peerPackageDependencies[pkgName];

// If it exists in comps deps / comp dev deps, we don't want to add it to the allPackagesDependencies
// as it will make the same dep both a dev and runtime dep
// since we are here only for auto detected deps, it means we already resolved the version correctly
// so we don't need to really modify the version
// also the version here might have a range (^ or ~ for example) so we can't
// just put it as is, as it is not valid for component deps to have range
if (
pkgVal !== MANUALLY_REMOVE_DEPENDENCY &&
((!existsInCompsDeps && !existsInCompsDevDeps) || field === 'peerDependencies')
) {
if ((existsInCompsDeps || existsInCompsDevDeps) && field === 'peerDependencies') {
const comp = (existsInCompsDeps ?? existsInCompsDevDeps) as Dependency;
comp.versionRange = pkgVal;
this.allDependencies.peerDependencies.push(comp);
} else {
this.allPackagesDependencies[key][pkgName] = pkgVal;
}
if (existsInCompsPeerDeps) {
existsInCompsPeerDeps.versionRange = pkgVal;
}
}
});
});
originallyExists.push(pkgName);

const existsInCompsDeps = this.allDependencies.dependencies.find((dep) => dep.packageName === pkgName);
const existsInCompsDevDeps = this.allDependencies.devDependencies.find((dep) => dep.packageName === pkgName);
const existsInCompsPeerDeps = this.allDependencies.peerDependencies.find((dep) => dep.packageName === pkgName);

// When changing peer dependency we want it to be stronger than the other types
if (field === 'peerDependencies') {
const shouldSkip = this.handlePeerDependencyOverride(pkgName, existsInCompsDeps, existsInCompsDevDeps);
if (shouldSkip) return;
}

// Clear package from all dependency types to avoid duplicates
// This fixes cases where a package moves between dep types in env.jsonc
this.removePackageFromAllDependencyTypes(pkgName);

this.applyPackageVersion(field, pkgName, pkgVal, existsInCompsDeps, existsInCompsDevDeps, existsInCompsPeerDeps);
}

/**
* Handle peer dependency specific logic.
* @returns true if the package should be skipped (not added to dependencies)
*/
private handlePeerDependencyOverride(
pkgName: string,
existsInCompsDeps: Dependency | undefined,
existsInCompsDevDeps: Dependency | undefined
): boolean {
delete this.allPackagesDependencies.devPackageDependencies[pkgName];
delete this.allPackagesDependencies.packageDependencies[pkgName];

// @types/* packages should not be added to components at all when in env peers.
// When a component is installed in a workspace, its env is installed as well.
// Packages listed as peers in env.jsonc become runtime dependencies of the env itself,
// so they're always installed alongside the env. Since @types/* packages are only needed
// for TypeScript compilation (which the env handles), there's no need for components
// to have them in their own dependencies.
if (pkgName.startsWith('@types/')) {
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early return at line 654 prevents cleanup of @types/* packages from allDependencies arrays when they exist as component dependencies. This can occur when the iteration order processes 'devDependencies' or 'dependencies' before 'peerDependencies'.

For example, if an @types/* package appears in both env.jsonc devDependencies and peerDependencies:

  1. First iteration (devDependencies): package is added to allDependencies.devDependencies
  2. Second iteration (peerDependencies): handlePeerDependencyOverride deletes from allPackagesDependencies but returns early before reaching lines 657-666 which would filter it from allDependencies arrays

To fix this, move the @types/* package filtering logic from allDependencies.dependencies and allDependencies.devDependencies before the early return, similar to lines 657-666 but specifically for @types/* packages.

Suggested change
if (pkgName.startsWith('@types/')) {
if (pkgName.startsWith('@types/')) {
// Ensure @types/* is removed from all component dependency arrays, even if it was
// previously added as a dependency or devDependency before being seen as a peer.
this.allDependencies.dependencies = this.allDependencies.dependencies.filter(
(dep) => dep.packageName !== pkgName
);
this.allDependencies.devDependencies = this.allDependencies.devDependencies.filter(
(dep) => dep.packageName !== pkgName
);
this.allDependencies.peerDependencies = this.allDependencies.peerDependencies.filter(
(dep) => dep.packageName !== pkgName
);

Copilot uses AI. Check for mistakes.
delete this.allPackagesDependencies.peerPackageDependencies[pkgName];
return true;
}

if (existsInCompsDeps) {
this.allDependencies.dependencies = this.allDependencies.dependencies.filter(
(dep) => dep.packageName !== pkgName
);
}
if (existsInCompsDevDeps) {
this.allDependencies.devDependencies = this.allDependencies.devDependencies.filter(
(dep) => dep.packageName !== pkgName
);
}

return false;
}

private removePackageFromAllDependencyTypes(pkgName: string): void {
delete this.allPackagesDependencies.packageDependencies[pkgName];
delete this.allPackagesDependencies.devPackageDependencies[pkgName];
delete this.allPackagesDependencies.peerPackageDependencies[pkgName];
}

private applyPackageVersion(
field: string,
pkgName: string,
pkgVal: string,
existsInCompsDeps: Dependency | undefined,
existsInCompsDevDeps: Dependency | undefined,
existsInCompsPeerDeps: Dependency | undefined
): void {
// If it exists in comps deps / comp dev deps, we don't want to add it to the allPackagesDependencies
// as it will make the same dep both a dev and runtime dep
// since we are here only for auto detected deps, it means we already resolved the version correctly
// so we don't need to really modify the version
// also the version here might have a range (^ or ~ for example) so we can't
// just put it as is, as it is not valid for component deps to have range
const shouldAddPackage =
pkgVal !== MANUALLY_REMOVE_DEPENDENCY &&
((!existsInCompsDeps && !existsInCompsDevDeps) || field === 'peerDependencies');

if (!shouldAddPackage) return;

const key = DepsKeysToAllPackagesDepsKeys[field];

if ((existsInCompsDeps || existsInCompsDevDeps) && field === 'peerDependencies') {
const comp = (existsInCompsDeps ?? existsInCompsDevDeps) as Dependency;
comp.versionRange = pkgVal;
this.allDependencies.peerDependencies.push(comp);
} else {
this.allPackagesDependencies[key][pkgName] = pkgVal;
}

if (existsInCompsPeerDeps) {
existsInCompsPeerDeps.versionRange = pkgVal;
}
}

private async applyAutoDetectedPeersFromEnvOnEnvItSelf(): Promise<void> {
Expand Down