@teambit/dependency-resolver 1.0.912 → 1.0.913

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,13 +4,16 @@ import { IssuesClasses } from '@teambit/component-issues';
4
4
  import type { Component } from '@teambit/component';
5
5
  import { componentIdToPackageName } from '@teambit/pkg.modules.component-package-name';
6
6
  import { fromPairs, pickBy, mapValues, uniq, difference } from 'lodash';
7
- import { SemVer } from 'semver';
7
+ import semver, { SemVer } from 'semver';
8
8
  import pMapSeries from 'p-map-series';
9
9
  import { snapToSemver } from '@teambit/component-package-version';
10
+ import type { Logger } from '@teambit/logger';
10
11
  import type { DependencyList, PackageName } from '../dependencies';
11
12
  import { ComponentDependency } from '../dependencies';
12
13
  import type { WorkspacePolicy, EnvPolicy } from '../policy';
13
14
  import { VariantPolicy } from '../policy';
15
+ import type { VariantPolicyEntry } from '../policy/variant-policy';
16
+ import { createVariantPolicyEntry } from '../policy/variant-policy';
14
17
  import type { DependencyResolverMain } from '../dependency-resolver.main.runtime';
15
18
  import type { ComponentsManifestsMap } from '../types';
16
19
  import { ComponentManifest } from './component-manifest';
@@ -49,7 +52,10 @@ const DEFAULT_CREATE_OPTIONS: CreateFromComponentsOptions = {
49
52
  export class WorkspaceManifestFactory {
50
53
  constructor(
51
54
  private dependencyResolver: DependencyResolverMain,
52
- private aspectLoader: AspectLoaderMain
55
+ private aspectLoader: AspectLoaderMain,
56
+ private logger?: Logger,
57
+ private resolveEnvPeersFromRoot: boolean = true,
58
+ private forceEnvPeersToRoot: boolean = false
53
59
  ) {}
54
60
 
55
61
  async createFromComponents(
@@ -97,14 +103,29 @@ export class WorkspaceManifestFactory {
97
103
  components,
98
104
  optsWithDefaults.createManifestForComponentsWithoutDependencies
99
105
  );
100
- const envSelfPeers = this.getEnvsSelfPeersPolicy(componentsManifestsMap);
106
+ let envSelfPeers: VariantPolicy;
107
+ let peerOverrides: Record<string, string> = {};
108
+ if (this.resolveEnvPeersFromRoot) {
109
+ const workspacePackageNames = new Set(
110
+ components.map((component) => this.dependencyResolver.getPackageName(component))
111
+ );
112
+ const result = this.mergeEnvPeersToRoot(componentsManifestsMap, workspacePackageNames);
113
+ envSelfPeers = result.rootPolicy;
114
+ peerOverrides = result.peerOverrides;
115
+ if (result.componentPeerOverrides.size > 0) {
116
+ this.injectConflictingPeersToComponents(componentsManifestsMap, result.componentPeerOverrides);
117
+ }
118
+ } else {
119
+ envSelfPeers = this.getEnvsSelfPeersPolicy(componentsManifestsMap);
120
+ }
101
121
  const workspaceManifest = new WorkspaceManifest(
102
122
  name,
103
123
  version,
104
124
  dedupedDependencies.rootDependencies,
105
125
  envSelfPeers,
106
126
  rootDir,
107
- componentsManifestsMap
127
+ componentsManifestsMap,
128
+ peerOverrides
108
129
  );
109
130
  return workspaceManifest;
110
131
  }
@@ -116,6 +137,230 @@ export class WorkspaceManifestFactory {
116
137
  return rootPolicy.filter((dep) => !coreAspectPkgNames.has(dep.dependencyId));
117
138
  }
118
139
 
140
+ /**
141
+ * Merge env peer dependencies to the workspace root.
142
+ * - Non-conflicting peers: always merge to root
143
+ * - Conflicting peers with workspaceSingleton: merge majority to root, warn about unsatisfied envs
144
+ * - Conflicting peers without workspaceSingleton: skip root, return as componentPeerOverrides for per-component injection
145
+ */
146
+ private mergeEnvPeersToRoot(
147
+ componentsManifestsMap: ComponentsManifestsMap,
148
+ workspacePackageNames: Set<string>
149
+ ): {
150
+ rootPolicy: VariantPolicy;
151
+ componentPeerOverrides: Map<string, Record<string, string>>;
152
+ peerOverrides: Record<string, string>;
153
+ } {
154
+ // Collect all env selfPolicies grouped by package name
155
+ // Map<packageName, Map<version, Set<envId>>>
156
+ const peerVersionsMap = new Map<string, Map<string, Set<string>>>();
157
+ // Track workspaceSingleton flag per package (true if ANY env marks it)
158
+ const singletonFlags = new Map<string, boolean>();
159
+ // Track overrides flag per package (true if ANY env marks it)
160
+ const overridesFlags = new Map<string, boolean>();
161
+
162
+ for (const manifest of componentsManifestsMap.values()) {
163
+ const envId = manifest.envPolicy.envId || 'unknown';
164
+ const selfPolicy = manifest.envPolicy.selfPolicy;
165
+ for (const entry of selfPolicy.entries) {
166
+ // Skip peers that are workspace components — they're linked, not installed
167
+ if (workspacePackageNames.has(entry.dependencyId)) continue;
168
+ const pkgName = entry.dependencyId;
169
+ const version = entry.value.version;
170
+ if (!peerVersionsMap.has(pkgName)) {
171
+ peerVersionsMap.set(pkgName, new Map());
172
+ }
173
+ const versionsMap = peerVersionsMap.get(pkgName)!;
174
+ if (!versionsMap.has(version)) {
175
+ versionsMap.set(version, new Set());
176
+ }
177
+ versionsMap.get(version)!.add(envId);
178
+ if (entry.value.workspaceSingleton) {
179
+ singletonFlags.set(pkgName, true);
180
+ }
181
+ if (entry.value.override) {
182
+ overridesFlags.set(pkgName, true);
183
+ }
184
+ }
185
+ }
186
+
187
+ // Resolve each peer dependency
188
+ const resolvedEntries: VariantPolicyEntry[] = [];
189
+ // Map<envId, Record<pkgName, version>> for per-component injection
190
+ const componentPeerOverrides = new Map<string, Record<string, string>>();
191
+ // Collect pnpm overrides from entries with overrides: true
192
+ const peerOverrides: Record<string, string> = {};
193
+ // Track packages with conflicts for the hint message
194
+ const conflictingPackages: string[] = [];
195
+
196
+ for (const [pkgName, versionsMap] of peerVersionsMap.entries()) {
197
+ const versions = Array.from(versionsMap.keys());
198
+
199
+ if (versions.length === 1) {
200
+ // No conflict — merge to root
201
+ const entry = this.createResolvedEntry(componentsManifestsMap, pkgName, versions[0]);
202
+ resolvedEntries.push(entry);
203
+ if (overridesFlags.get(pkgName) && versions[0] !== '+') {
204
+ peerOverrides[pkgName] = versions[0];
205
+ }
206
+ continue;
207
+ }
208
+
209
+ // overrides:true implies singleton — an override forces a single version across the workspace
210
+ const isSingleton =
211
+ this.forceEnvPeersToRoot || (singletonFlags.get(pkgName) ?? false) || (overridesFlags.get(pkgName) ?? false);
212
+
213
+ if (isSingleton) {
214
+ // Conflict + workspaceSingleton: merge majority to root, warn about unsatisfied
215
+ const chosenVersion = this.resolveConflictingPeerVersions(pkgName, versionsMap);
216
+ resolvedEntries.push(this.createResolvedEntry(componentsManifestsMap, pkgName, chosenVersion));
217
+ conflictingPackages.push(pkgName);
218
+ if (overridesFlags.get(pkgName) && chosenVersion !== '+') {
219
+ peerOverrides[pkgName] = chosenVersion;
220
+ }
221
+ } else {
222
+ // Conflict + no workspaceSingleton: per-component injection
223
+ // Each env gets its own version injected into its components
224
+ for (const [version, envIds] of versionsMap.entries()) {
225
+ // Skip "+" placeholder — it's resolved at the workspace root level
226
+ if (version === '+') continue;
227
+ for (const envId of envIds) {
228
+ if (!componentPeerOverrides.has(envId)) {
229
+ componentPeerOverrides.set(envId, {});
230
+ }
231
+ componentPeerOverrides.get(envId)![pkgName] = version;
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ if (conflictingPackages.length > 0) {
238
+ this.logger?.console?.(
239
+ `\n💡 To resolve workspace peer version conflicts, pin a version in workspace.jsonc under "teambit.dependencies/dependency-resolver" > "overrides", ` +
240
+ `e.g. "overrides": { "${conflictingPackages[0]}": "<version>" }\n`
241
+ );
242
+ }
243
+
244
+ return {
245
+ rootPolicy: VariantPolicy.fromArray(resolvedEntries),
246
+ componentPeerOverrides,
247
+ peerOverrides,
248
+ };
249
+ }
250
+
251
+ private createResolvedEntry(
252
+ componentsManifestsMap: ComponentsManifestsMap,
253
+ pkgName: string,
254
+ version: string
255
+ ): VariantPolicyEntry {
256
+ const existingEntry = this.findEntryWithVersion(componentsManifestsMap, pkgName, version);
257
+ if (existingEntry) return existingEntry;
258
+ return createVariantPolicyEntry(pkgName, version, 'runtime', {
259
+ source: 'env-own',
260
+ force: true,
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Inject conflicting non-singleton peer deps into component manifests.
266
+ * Each component gets the version specified by its env.
267
+ */
268
+ private injectConflictingPeersToComponents(
269
+ componentsManifestsMap: ComponentsManifestsMap,
270
+ componentPeerOverrides: Map<string, Record<string, string>>
271
+ ): void {
272
+ for (const manifest of componentsManifestsMap.values()) {
273
+ const envId = manifest.envPolicy.envId || 'unknown';
274
+ const overrides = componentPeerOverrides.get(envId);
275
+ if (!overrides) continue;
276
+ manifest.dependencies.dependencies = {
277
+ ...overrides,
278
+ ...manifest.dependencies.dependencies,
279
+ };
280
+ }
281
+ }
282
+
283
+ private resolveConflictingPeerVersions(pkgName: string, versionsMap: Map<string, Set<string>>): string {
284
+ const versions = Array.from(versionsMap.keys());
285
+ const envsByVersion = Array.from(versionsMap.entries());
286
+
287
+ // Filter out non-semver versions (like '+') for comparison
288
+ const semverVersions = versions.filter((v) => v !== '+' && semver.coerce(v) !== null);
289
+
290
+ if (semverVersions.length === 0) {
291
+ // All versions are non-semver (e.g., '+'), just pick the first
292
+ return versions[0];
293
+ }
294
+
295
+ // For each version, count how many envs it could satisfy
296
+ let bestVersion = semverVersions[0];
297
+ let bestCount = 0;
298
+
299
+ for (const candidateVersion of semverVersions) {
300
+ const coercedCandidate = semver.coerce(candidateVersion);
301
+ if (!coercedCandidate) continue;
302
+ let satisfiedCount = 0;
303
+ for (const [version, envIds] of envsByVersion) {
304
+ if (version === candidateVersion || version === '+') {
305
+ satisfiedCount += envIds.size;
306
+ } else if (semver.validRange(version)) {
307
+ // Check if the candidate could satisfy the range
308
+ if (semver.satisfies(coercedCandidate.version, version)) {
309
+ satisfiedCount += envIds.size;
310
+ }
311
+ }
312
+ }
313
+ const coercedBest = semver.coerce(bestVersion);
314
+ if (
315
+ satisfiedCount > bestCount ||
316
+ (satisfiedCount === bestCount && coercedBest && semver.gt(coercedCandidate.version, coercedBest.version))
317
+ ) {
318
+ bestVersion = candidateVersion;
319
+ bestCount = satisfiedCount;
320
+ }
321
+ }
322
+
323
+ // Only warn about envs whose version is NOT satisfied by the chosen version
324
+ const coercedBestForCheck = semver.coerce(bestVersion);
325
+ const unsatisfiedEnvs = envsByVersion.filter(([version]) => {
326
+ if (version === bestVersion || version === '+') return false;
327
+ if (coercedBestForCheck && semver.validRange(version) && semver.satisfies(coercedBestForCheck.version, version))
328
+ return false;
329
+ return true;
330
+ });
331
+
332
+ if (unsatisfiedEnvs.length > 0) {
333
+ const allDetails = envsByVersion
334
+ .map(([version, envIds]) => ` ${version} (from envs: ${Array.from(envIds).join(', ')})`)
335
+ .join('\n');
336
+ this.logger?.warn(
337
+ `Conflicting env peer dependency versions for "${pkgName}":\n${allDetails}\n → Resolved to: ${bestVersion}`
338
+ );
339
+ const unsatisfiedDetails = unsatisfiedEnvs
340
+ .map(([version, envIds]) => `${version} (${Array.from(envIds).join(', ')})`)
341
+ .join(', ');
342
+ this.logger?.consoleWarning?.(
343
+ `Conflicting env peer versions for "${pkgName}": using ${bestVersion}, but not compatible with: ${unsatisfiedDetails}`
344
+ );
345
+ }
346
+
347
+ return bestVersion;
348
+ }
349
+
350
+ private findEntryWithVersion(
351
+ componentsManifestsMap: ComponentsManifestsMap,
352
+ pkgName: string,
353
+ version: string
354
+ ): VariantPolicyEntry | undefined {
355
+ for (const manifest of componentsManifestsMap.values()) {
356
+ const entry = manifest.envPolicy.selfPolicy.entries.find(
357
+ (e) => e.dependencyId === pkgName && e.value.version === version
358
+ );
359
+ if (entry) return entry;
360
+ }
361
+ return undefined;
362
+ }
363
+
119
364
  private getEnvsSelfPeersPolicy(componentsManifestsMap: ComponentsManifestsMap) {
120
365
  const foundEnvs: EnvPolicy[] = [];
121
366
  for (const component of componentsManifestsMap.values()) {
@@ -215,36 +460,34 @@ export class WorkspaceManifestFactory {
215
460
  );
216
461
  });
217
462
 
218
- const envPeerDependencies = await this._getEnvPeerDependencies(component, packageNames);
219
- // When includeAllEnvPeers is true, use ALL env peer deps to ensure consistent
220
- // peer dependency contexts across all components. Otherwise, pnpm creates separate
221
- // "injected" copies in .pnpm/ for components with different peer sets, causing
222
- // TypeScript to see duplicate types from different physical paths.
223
- // When false, only include peer deps that the component actually uses, to avoid
224
- // writing unnecessary deps to the generated install manifest.
225
- let peerDepsForManifest: Record<string, string>;
226
- if (includeAllEnvPeers ?? true) {
227
- peerDepsForManifest = envPeerDependencies;
228
- } else {
229
- peerDepsForManifest = pickBy(envPeerDependencies, (_val, pkgName) => {
230
- return (
231
- depManifestBeforeFiltering.dependencies[pkgName] ||
232
- depManifestBeforeFiltering.devDependencies[pkgName] ||
233
- depManifestBeforeFiltering.peerDependencies[pkgName]
234
- );
235
- });
236
- // In case the env has peer dependencies on both react and react-dom, we want to make sure to keep the versions
237
- // in sync with each other, otherwise it may cause issues in the workspace
238
- // This is a special case for react and react-dom, as most component do import from react, making it a peer dependency,
239
- // but not necessarily import from react-dom, which from env.jsonc peers in that case not added to the peers of the component.
240
- // and if the versions are not in sync, it may cause issues in the workspace
241
- // an example:
242
- // my-comp depend on react, and using @testing-library/react which depend on react-dom (as peer),
243
- // the component don't have react-dom as peer dependency, but when we install the dependencies in the workspace,
244
- // it will install the latest version of react-dom which may not be compatible with the version of react that my-comp
245
- // is using, and it may cause issues in the workspace.
246
- if (peerDepsForManifest.react && envPeerDependencies['react-dom']) {
247
- peerDepsForManifest['react-dom'] = envPeerDependencies['react-dom'];
463
+ let peerDepsForManifest: Record<string, string> = {};
464
+ if (!this.resolveEnvPeersFromRoot) {
465
+ // Legacy behavior: inject env peer deps into each component's manifest
466
+ const envPeerDependencies = await this._getEnvPeerDependencies(component, packageNames);
467
+ if (includeAllEnvPeers ?? true) {
468
+ peerDepsForManifest = envPeerDependencies;
469
+ } else {
470
+ peerDepsForManifest = pickBy(envPeerDependencies, (_val, pkgName) => {
471
+ return (
472
+ depManifestBeforeFiltering.dependencies[pkgName] ||
473
+ depManifestBeforeFiltering.devDependencies[pkgName] ||
474
+ depManifestBeforeFiltering.peerDependencies[pkgName]
475
+ );
476
+ });
477
+
478
+ // In case the env has peer dependencies on both react and react-dom, we want to make sure to keep the versions
479
+ // in sync with each other, otherwise it may cause issues in the workspace
480
+ // This is a special case for react and react-dom, as most component do import from react, making it a peer dependency,
481
+ // but not necessarily import from react-dom, which from env.jsonc peers in that case not added to the peers of the component.
482
+ // and if the versions are not in sync, it may cause issues in the workspace
483
+ // an example:
484
+ // my-comp depend on react, and using @testing-library/react which depend on react-dom (as peer),
485
+ // the component don't have react-dom as peer dependency, but when we install the dependencies in the workspace,
486
+ // it will install the latest version of react-dom which may not be compatible with the version of react that my-comp
487
+ // is using, and it may cause issues in the workspace.
488
+ if (peerDepsForManifest.react && envPeerDependencies['react-dom']) {
489
+ peerDepsForManifest['react-dom'] = envPeerDependencies['react-dom'];
490
+ }
248
491
  }
249
492
  }
250
493
 
@@ -7,6 +7,7 @@ import { Manifest } from './manifest';
7
7
 
8
8
  export interface WorkspaceManifestToJsonOptions extends ManifestToJsonOptions {
9
9
  installPeersFromEnvs?: boolean;
10
+ resolveEnvPeersFromRoot?: boolean;
10
11
  }
11
12
 
12
13
  export class WorkspaceManifest extends Manifest {
@@ -17,7 +18,8 @@ export class WorkspaceManifest extends Manifest {
17
18
  public dependencies: ManifestDependenciesObject,
18
19
  private envSelfPeersPolicy: VariantPolicy | undefined,
19
20
  private rootDir: string,
20
- public componentsManifestsMap: ComponentsManifestsMap
21
+ public componentsManifestsMap: ComponentsManifestsMap,
22
+ public peerOverrides: Record<string, string> = {}
21
23
  ) {
22
24
  super(name, version, dependencies);
23
25
  }
@@ -30,12 +32,21 @@ export class WorkspaceManifest extends Manifest {
30
32
 
31
33
  toJson(options: WorkspaceManifestToJsonOptions = {}): Record<string, any> {
32
34
  const manifest = super.toJson(options);
33
- if (options.installPeersFromEnvs) {
35
+ if (options.installPeersFromEnvs || options.resolveEnvPeersFromRoot) {
34
36
  const peersManifest = this.envSelfPeersPolicy?.toVersionManifest();
35
37
  // Resolve "+" version placeholders from peersManifest
36
38
  const resolvedPeersManifest = this._resolvePlusVersions(peersManifest || {});
37
39
  manifest.dependencies = manifest.dependencies || {};
38
- Object.assign(manifest.dependencies, resolvedPeersManifest);
40
+ // Env peers are added as defaults — workspace.jsonc policy takes priority across all dep sections
41
+ for (const [pkgName, version] of Object.entries(resolvedPeersManifest)) {
42
+ const alreadyDefined = manifest.dependencies?.[pkgName] ||
43
+ manifest.devDependencies?.[pkgName] ||
44
+ manifest.peerDependencies?.[pkgName] ||
45
+ manifest.optionalDependencies?.[pkgName];
46
+ if (!alreadyDefined) {
47
+ manifest.dependencies[pkgName] = version;
48
+ }
49
+ }
39
50
  }
40
51
  return manifest;
41
52
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@teambit/dependency-resolver",
3
- "version": "1.0.912",
3
+ "version": "1.0.913",
4
4
  "homepage": "https://bit.cloud/teambit/dependencies/dependency-resolver",
5
5
  "main": "dist/index.js",
6
6
  "componentId": {
7
7
  "scope": "teambit.dependencies",
8
8
  "name": "dependency-resolver",
9
- "version": "1.0.912"
9
+ "version": "1.0.913"
10
10
  },
11
11
  "dependencies": {
12
12
  "chalk": "4.1.2",
@@ -52,14 +52,14 @@
52
52
  "@teambit/component-issues": "0.0.170",
53
53
  "@teambit/component-package-version": "0.0.449",
54
54
  "@teambit/legacy-bit-id": "1.1.3",
55
- "@teambit/toolbox.object.sorter": "0.0.2",
56
55
  "@teambit/legacy.consumer-config": "0.0.101",
57
56
  "@teambit/toolbox.crypto.sha1": "0.0.15",
58
- "@teambit/component": "1.0.912",
59
- "@teambit/envs": "1.0.912",
60
- "@teambit/aspect-loader": "1.0.912",
61
- "@teambit/objects": "0.0.419",
62
- "@teambit/graphql": "1.0.912"
57
+ "@teambit/toolbox.object.sorter": "0.0.2",
58
+ "@teambit/component": "1.0.913",
59
+ "@teambit/envs": "1.0.913",
60
+ "@teambit/aspect-loader": "1.0.913",
61
+ "@teambit/objects": "0.0.420",
62
+ "@teambit/graphql": "1.0.913"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/fs-extra": "9.0.7",
@@ -23,6 +23,19 @@ export type EnvJsoncPolicyEntry = {
23
23
 
24
24
  export type EnvJsoncPolicyPeerEntry = EnvJsoncPolicyEntry & {
25
25
  supportedRange: string;
26
+ /**
27
+ * When true, this peer dependency will be resolved as a single version at the workspace root,
28
+ * even if different envs specify different versions. Useful for @types packages and workspace-level
29
+ * tools (eslint, prettier) that must resolve from the workspace root.
30
+ * When false (default), conflicts are resolved per-component via env roots.
31
+ */
32
+ workspaceSingleton?: boolean;
33
+ /**
34
+ * When true, generates a pnpm override for this peer using its version,
35
+ * forcing all transitive dependencies to use the same version.
36
+ * Useful to prevent old versions from being pulled by published packages.
37
+ */
38
+ override?: boolean;
26
39
  };
27
40
 
28
41
  export type VersionKeyName = 'version' | 'supportedRange';
@@ -45,14 +58,16 @@ export type EnvPolicyConfigObject = EnvPolicyEnvJsoncConfigObject | EnvPolicyLeg
45
58
  export class EnvPolicy extends VariantPolicy {
46
59
  constructor(
47
60
  _policiesEntries: VariantPolicyEntry[],
48
- readonly selfPolicy: VariantPolicy
61
+ readonly selfPolicy: VariantPolicy,
62
+ readonly envId?: string
49
63
  ) {
50
64
  super(_policiesEntries);
51
65
  }
52
66
 
53
67
  static fromConfigObject(
54
68
  configObject: EnvPolicyConfigObject,
55
- { includeLegacyPeersInSelfPolicy }: VariantPolicyFromConfigObjectOptions = {}
69
+ { includeLegacyPeersInSelfPolicy }: VariantPolicyFromConfigObjectOptions = {},
70
+ envId?: string
56
71
  ): EnvPolicy {
57
72
  validateEnvPolicyConfigObject(configObject);
58
73
 
@@ -95,7 +110,7 @@ export class EnvPolicy extends VariantPolicy {
95
110
  );
96
111
  const newPolicy = VariantPolicy.fromArray(componentPeersEntries.concat(otherEntries));
97
112
  const finalComponentPolicy = VariantPolicy.mergePolices([legacyPolicy, newPolicy]);
98
- return new EnvPolicy(finalComponentPolicy.entries, selfPolicy);
113
+ return new EnvPolicy(finalComponentPolicy.entries, selfPolicy, envId);
99
114
  }
100
115
 
101
116
  static getEmpty(): EnvPolicy {
@@ -134,7 +149,12 @@ function entriesFromKey(
134
149
  `env.jsonc: "policy.${keyName}" entry must be a property with a "${versionKey}" field. got "${entry}"`
135
150
  );
136
151
  }
137
- return createVariantPolicyEntry(entry.name, entry[versionKey], lifecycleType, {
152
+ const peerEntry = entry as EnvJsoncPolicyPeerEntry;
153
+ const hasPeerProps = 'workspaceSingleton' in entry || 'override' in entry;
154
+ const value = hasPeerProps
155
+ ? { version: entry[versionKey], workspaceSingleton: peerEntry.workspaceSingleton, override: peerEntry.override }
156
+ : entry[versionKey];
157
+ return createVariantPolicyEntry(entry.name, value, lifecycleType, {
138
158
  ...options,
139
159
  source: options.source ?? 'env',
140
160
  hidden: entry.hidden,
@@ -40,6 +40,8 @@ export type VariantPolicyEntryValue = {
40
40
  version: VariantPolicyEntryVersion;
41
41
  resolveFromEnv?: boolean;
42
42
  optional?: boolean;
43
+ workspaceSingleton?: boolean;
44
+ override?: boolean;
43
45
  };
44
46
 
45
47
  export type DependencySource = 'auto' | 'env' | 'env-own' | 'slots' | 'config';
@@ -343,10 +345,14 @@ export function createVariantPolicyEntry(
343
345
  const version = typeof value === 'string' ? value : value.version;
344
346
  const resolveFromEnv = typeof value === 'string' ? false : value.resolveFromEnv;
345
347
  const optional = typeof value === 'string' ? undefined : value.optional;
348
+ const workspaceSingleton = typeof value === 'string' ? undefined : value.workspaceSingleton;
349
+ const override = typeof value === 'string' ? undefined : value.override;
346
350
 
347
351
  const entryValue: VariantPolicyEntryValue = {
348
352
  version,
349
353
  resolveFromEnv,
354
+ workspaceSingleton,
355
+ override,
350
356
  };
351
357
  const entry: VariantPolicyEntry = {
352
358
  ...opts,