Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1ea9786
fix(deps): exclude @types/* packages from component peer dependencies
davidfirst Feb 5, 2026
c4637a1
refactor(deps): extract helper methods to reduce cognitive complexity
davidfirst Feb 5, 2026
a62dafc
Merge branch 'master' into fix/exclude-types-from-component-peers
davidfirst Feb 5, 2026
7cb0f29
Merge branch 'master' into fix/exclude-types-from-component-peers
davidfirst Feb 6, 2026
2016587
Merge branch 'master' into fix/exclude-types-from-component-peers
davidfirst Jun 10, 2026
ef69ec4
test(deps): add e2e test for excluding @types/* from component peers
davidfirst Jun 10, 2026
ec32978
Merge branch 'master' into fix/exclude-types-from-component-peers
davidfirst Jun 10, 2026
f67480f
Merge branch 'master' into fix/exclude-types-from-component-peers
davidfirst Jun 11, 2026
f9964c2
Merge branch 'master' into fix/exclude-types-from-component-peers
davidfirst Jun 11, 2026
0176b9a
Merge branch 'master' into fix/exclude-types-from-component-peers
davidfirst Jun 12, 2026
3651d5c
Merge remote-tracking branch 'origin/master' into fix/exclude-types-f…
davidfirst Jun 23, 2026
4004b33
Merge remote-tracking branch 'origin/master' into fix/exclude-types-f…
davidfirst Jun 24, 2026
ddfde6c
Merge branch 'master' into fix/exclude-types-from-component-peers
davidfirst Jun 25, 2026
6972bf2
Merge branch 'master' into fix/exclude-types-from-component-peers
davidfirst Jun 26, 2026
7ed4421
Merge branch 'master' into fix/exclude-types-from-component-peers
davidfirst Jun 26, 2026
c224e06
fix(deps): pin react/react-dom 17->19 in overrides to avoid capsule p…
davidfirst Jun 26, 2026
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
46 changes: 46 additions & 0 deletions e2e/harmony/dependencies/env-jsonc-policies.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@ const ENV_JSONC_LEVEL3 = {
},
};

export const ENV_POLICY_WITH_TYPES_PEER = {
peers: [
{
name: 'react',
version: '^18.0.0',
supportedRange: '^17.0.0 || ^18.0.0',
},
{
name: '@types/react',
version: '18.0.25',
supportedRange: '^18.0.0',
},
],
};

function generateEnvJsoncWithExtends(extendsName: string, envJsonc: object) {
return {
extends: extendsName,
Expand Down Expand Up @@ -577,4 +592,35 @@ describe('env-jsonc-policies', function () {
expect(newSupportedRange).to.include(newVersion);
});
});
describe('@types/* package listed as a peer in env.jsonc', function () {
this.timeout(0);
let componentShowParsed;
let envId;
before(() => {
helper = new Helper();
helper.scopeHelper.setWorkspaceWithRemoteScope();
envId = `${helper.scopes.remote}/react-based-env`;
helper.command.create('react', 'button', '-p button --env teambit.react/react');
// having @types/react in the workspace policy makes it auto-detected for the component
// (since the component uses react), so the env peer override below actually applies to it.
helper.workspaceJsonc.addPolicyToDependencyResolver({ dependencies: { '@types/react': '18.0.25' } });
helper.env.setCustomNewEnv(undefined, undefined, { policy: ENV_POLICY_WITH_TYPES_PEER });
helper.command.setEnv('button', envId);
helper.command.install();
componentShowParsed = helper.command.showComponentParsed('button');
});
after(() => {
helper.scopeHelper.destroy();
});
it('should still add regular (non-@types/*) peer deps from env.jsonc to the component', () => {
expect(componentShowParsed.peerPackageDependencies).to.include({ react: '^17.0.0 || ^18.0.0' });
});
it('should not add auto-detected @types/* peers to the component peer deps', () => {
expect(componentShowParsed.peerPackageDependencies).to.not.have.property('@types/react');
});
it('should not leak auto-detected @types/* peers into the component runtime or dev deps', () => {
expect(componentShowParsed.packageDependencies).to.not.have.property('@types/react');
expect(componentShowParsed.devPackageDependencies).to.not.have.property('@types/react');
});
});
});
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 @@ -570,107 +570,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/')) {
Comment thread
davidfirst marked this conversation as resolved.
delete this.allPackagesDependencies.peerPackageDependencies[pkgName];
return true;
}
Comment thread
davidfirst marked this conversation as resolved.

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