diff --git a/e2e/functionalities/hidden-peer-dependencies.e2e.ts b/e2e/functionalities/hidden-peer-dependencies.e2e.ts new file mode 100644 index 000000000000..af0881c07cc0 --- /dev/null +++ b/e2e/functionalities/hidden-peer-dependencies.e2e.ts @@ -0,0 +1,64 @@ +import path from 'path'; +import { expect } from 'chai'; +import fs from 'fs-extra'; +import { Helper } from '@teambit/legacy.e2e-helper'; +import { resolveFrom } from '@teambit/toolbox.modules.module-resolver'; + +describe('hidden peer dependency via env.jsonc', function () { + this.timeout(0); + let helper: Helper; + let workspaceCapsulesRootDir: string; + const peerPkgName = 'is-odd'; + + before(() => { + helper = new Helper(); + helper.scopeHelper.reInitWorkspace(); + // create a single component comp1 + helper.fixtures.populateComponents(1); + // ensure comp1 actually requires is-odd so it's considered a used peer + helper.fs.appendFile('comp1/index.js', 'const isOdd = require("is-odd");'); + // create env with hidden is-odd peer + helper.env.setCustomNewEnv( + undefined, + undefined, + { + policy: { + peers: [ + { + name: peerPkgName, + version: '1.0.0', + supportedRange: '*', + hidden: true, + }, + ], + }, + }, + false, + 'custom-env/env1', + 'custom-env/env1' + ); + // apply env to comp1 + helper.extensions.addExtensionToVariant('comp1', `${helper.scopes.remote}/custom-env/env1`, {}); + helper.extensions.addExtensionToVariant('custom-env', 'teambit.envs/env', {}); + helper.extensions.workspaceJsonc.addKeyValToDependencyResolver('rootComponents', true); + helper.command.install('--add-missing-deps'); + helper.command.build('--skip-tests'); + workspaceCapsulesRootDir = helper.command.capsuleListParsed().workspaceCapsulesRootDir; + }); + + after(() => { + helper.scopeHelper.destroy(); + }); + + it('should have is-odd/package.json present in the workspace root node_modules', () => { + const isOddPkgJsonPath = resolveFrom(helper.fixtures.scopes.localPath, ['is-odd/package.json']); + expect(fs.existsSync(isOddPkgJsonPath)).to.be.true; + }); + + it('should exclude is-odd from capsule package.json peerDependencies', () => { + const pkgJsonPath = path.join(workspaceCapsulesRootDir, `${helper.scopes.remote}_comp1/package.json`); + const pkgJson = fs.readJsonSync(pkgJsonPath); + const peerDeps = pkgJson.peerDependencies || {}; + expect(peerDeps[peerPkgName]).to.be.undefined; + }); +}); diff --git a/scopes/component/isolator/isolator.main.runtime.ts b/scopes/component/isolator/isolator.main.runtime.ts index 61ca506e1f17..af2489c5ae84 100644 --- a/scopes/component/isolator/isolator.main.runtime.ts +++ b/scopes/component/isolator/isolator.main.runtime.ts @@ -1340,10 +1340,28 @@ export class IsolatorMain { addDependencies(packageJson); const currentVersion = getComponentPackageVersion(component); packageJson.addOrUpdateProperty('version', currentVersion); + this.removeHiddenPeerDependencies(packageJson, component); return packageJson; } + /** + * Remove peers that are marked as hidden in env.jsonc/policies for capsules only. + * This keeps these peers in workspace installs, while omitting them from capsule package.json. + */ + private removeHiddenPeerDependencies(packageJson: PackageJsonFile, component: Component) { + if (Object.keys(packageJson.packageJsonObject.peerDependencies ?? {}).length === 0) { + return; + } + const hiddenPeers = this.dependencyResolver.getHiddenPeerDependencies(component); + hiddenPeers.forEach((dep) => { + const peerName = dep.getPackageName?.(); + if (peerName) { + delete packageJson.packageJsonObject.peerDependencies[peerName]; + } + }); + } + /** * currently, it writes all artifacts. * later, this responsibility might move to pkg extension, which could write only artifacts diff --git a/scopes/dependencies/dependency-resolver/dependencies/dependency-list.ts b/scopes/dependencies/dependency-resolver/dependencies/dependency-list.ts index 4992be8a578e..538aee77758b 100644 --- a/scopes/dependencies/dependency-resolver/dependencies/dependency-list.ts +++ b/scopes/dependencies/dependency-resolver/dependencies/dependency-list.ts @@ -122,6 +122,10 @@ export class DependencyList { return this.filter((dep) => !dep.hidden); } + getHiddenPeers(): DependencyList { + return this.filter((dep) => dep.hidden === true && dep.lifecycle === 'peer'); + } + toTypeArray(typeName: string): T[] { const list: T[] = this.dependencies.filter((dep) => dep.type === typeName) as any as T[]; return list; diff --git a/scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts b/scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts index 35805d76cf47..4060d47e81bf 100644 --- a/scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts +++ b/scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts @@ -376,6 +376,16 @@ export class DependencyResolverMain { return depList.filterHidden(); } + getHiddenPeerDependencies(component: IComponent): DependencyList { + const entry = component.get(DependencyResolverAspect.id); + if (!entry) { + return DependencyList.fromArray([]); + } + const serializedDependencies: SerializedDependency[] = entry?.data?.dependencies || []; + const depList = this.getDependenciesFromSerializedDependencies(serializedDependencies); + return depList.getHiddenPeers(); + } + getDependenciesFromLegacyComponent( component: LegacyComponent, { includeHidden = false }: GetDependenciesOptions = {}