@@ -2,8 +2,8 @@ import { structUtils } from '@yarnpkg/core';
22import * as _flatMap from 'lodash.flatmap' ;
33import { OutOfSyncError } from '../../errors' ;
44import { LockfileType } from '../../parsers' ;
5- import { NormalisedPkgs } from '../types' ;
6- import { getGraphDependencies , PkgNode } from '../util' ;
5+ import { NormalisedPkgs , WorkspacePackageManifest } from '../types' ;
6+ import { Dependencies , getGraphDependencies , PkgNode } from '../util' ;
77import * as semver from 'semver' ;
88import * as debugModule from 'debug' ;
99
@@ -99,6 +99,39 @@ export const yarnLockFileKeyNormalizer =
9999 return new Set < string > ( _flatMap ( allKeys ) ) ;
100100 } ;
101101
102+ /**
103+ * Yarn Berry merges a workspace package's dependencies, devDependencies and peerDependencies
104+ * into a single `dependencies` block in yarn.lock, so the dev marker is lost. When such a
105+ * workspace package is consumed as a production dependency, walking that merged block would
106+ * promote its dev-only tooling (e.g. webpack, babel) into the production graph.
107+ *
108+ * Given the consumed workspace member's own package.json (via `workspaceManifest`), drop the
109+ * dev-only entries: names that appear in devDependencies but not in dependencies /
110+ * optionalDependencies / peerDependencies (prod wins on overlap).
111+ */
112+ const pruneWorkspaceDevDependencies = (
113+ deps : Dependencies ,
114+ workspaceManifest : WorkspacePackageManifest ,
115+ ) : Dependencies => {
116+ const nonDev = new Set < string > ( [
117+ ...Object . keys ( workspaceManifest . dependencies || { } ) ,
118+ ...Object . keys ( workspaceManifest . optionalDependencies || { } ) ,
119+ ...Object . keys ( workspaceManifest . peerDependencies || { } ) ,
120+ ] ) ;
121+ const dev = new Set < string > (
122+ Object . keys ( workspaceManifest . devDependencies || { } ) ,
123+ ) ;
124+
125+ return Object . entries ( deps ) . reduce ( ( acc : Dependencies , [ name , depInfo ] ) => {
126+ // Drop only names that are exclusively devDependencies of the workspace member.
127+ if ( dev . has ( name ) && ! nonDev . has ( name ) ) {
128+ return acc ;
129+ }
130+ acc [ name ] = depInfo ;
131+ return acc ;
132+ } , { } ) ;
133+ } ;
134+
102135export const getYarnLockV2ChildNode = (
103136 name : string ,
104137 depInfo : {
@@ -116,6 +149,8 @@ export const getYarnLockV2ChildNode = (
116149 includeOptionalDeps : boolean ,
117150 resolutions : Record < string , string > ,
118151 parentNode : PkgNode ,
152+ includeDevDeps = false ,
153+ workspacePackages ?: Record < string , WorkspacePackageManifest > ,
119154) => {
120155 // First, check if a resolution would be used
121156 const resolvedVersionFromResolution = ( ( ) => {
@@ -256,7 +291,7 @@ export const getYarnLockV2ChildNode = (
256291 optionalDependencies,
257292 } = pkgData ;
258293
259- const formattedDependencies = getGraphDependencies ( dependencies || { } , {
294+ let formattedDependencies = getGraphDependencies ( dependencies || { } , {
260295 isDev : depInfo . isDev ,
261296 } ) ;
262297 const formattedOptionalDependencies = includeOptionalDeps
@@ -266,6 +301,23 @@ export const getYarnLockV2ChildNode = (
266301 } )
267302 : { } ;
268303
304+ // Key by the resolved package name, not the parent's name, so npm aliases
305+ // (e.g. "alias": "npm:@scope/real-pkg@1") still match the workspace manifest.
306+ const workspaceManifestFromResolution =
307+ workspacePackages ?. [
308+ depInfo . alias ? depInfo . alias . aliasTargetDepName : name
309+ ] ;
310+ if (
311+ ! includeDevDeps &&
312+ workspaceManifestFromResolution &&
313+ pkgData . resolution ?. includes ( '@workspace:' )
314+ ) {
315+ formattedDependencies = pruneWorkspaceDevDependencies (
316+ formattedDependencies ,
317+ workspaceManifestFromResolution ,
318+ ) ;
319+ }
320+
269321 return {
270322 id : `${ name } @${ versionFromResolution } ` ,
271323 name : depInfo . alias ? depInfo . alias . aliasTargetDepName : name ,
@@ -301,7 +353,7 @@ export const getYarnLockV2ChildNode = (
301353 }
302354 } else {
303355 const depData = pkgs [ childNodeKey ] ;
304- const dependencies = getGraphDependencies ( depData . dependencies || { } , {
356+ let dependencies = getGraphDependencies ( depData . dependencies || { } , {
305357 isDev : depInfo . isDev ,
306358 } ) ;
307359 const optionalDependencies = includeOptionalDeps
@@ -310,6 +362,24 @@ export const getYarnLockV2ChildNode = (
310362 isOptional : true ,
311363 } )
312364 : { } ;
365+
366+ // Key by the resolved package name, not the parent's name, so npm aliases
367+ // (e.g. "alias": "npm:@scope/real-pkg@1") still match the workspace manifest.
368+ const workspaceManifest =
369+ workspacePackages ?. [
370+ depInfo . alias ? depInfo . alias . aliasTargetDepName : name
371+ ] ;
372+ if (
373+ ! includeDevDeps &&
374+ workspaceManifest &&
375+ depData . resolution ?. includes ( '@workspace:' )
376+ ) {
377+ dependencies = pruneWorkspaceDevDependencies (
378+ dependencies ,
379+ workspaceManifest ,
380+ ) ;
381+ }
382+
313383 return {
314384 id : `${ name } @${ depData . version } ` ,
315385 name : depInfo . alias ? depInfo . alias . aliasTargetDepName : name ,
0 commit comments