diff --git a/packages/zpm/src/install.rs b/packages/zpm/src/install.rs index d6de18b0..b953063d 100644 --- a/packages/zpm/src/install.rs +++ b/packages/zpm/src/install.rs @@ -898,6 +898,12 @@ impl InstallOpResult { pub struct InstallState { pub last_installed_at: u128, pub installed_workspaces: Option>, + + /// Whether the installed workspaces were installed without their + /// devDependencies (`yarn workspaces focus --production`); their + /// coverage then only includes production dependencies. + pub production: bool, + pub install_config_hash: Option, pub content_flags: BTreeMap, pub resolution_tree: ResolutionTree, @@ -937,6 +943,7 @@ impl Default for InstallState { Self { last_installed_at: 0, installed_workspaces: Some(BTreeSet::new()), + production: false, install_config_hash: None, content_flags: BTreeMap::new(), resolution_tree: ResolutionTree::default(), @@ -964,6 +971,7 @@ pub struct Install { pub install_state: InstallState, pub roots: BTreeSet, pub installed_workspaces: Option>, + pub production: bool, pub resolved_islands: Vec, pub skip_build: bool, pub skip_link_step: bool, @@ -1154,6 +1162,9 @@ impl Install { self.install_state.installed_workspaces = self.installed_workspaces.clone(); + self.install_state.production + = self.production; + self.install_state.install_config_hash = Some(project.install_config_hash()); @@ -1272,6 +1283,7 @@ impl<'a> InstallManager<'a> { } pub fn with_context(mut self, context: InstallContext<'a>) -> Self { + self.result.production = context.prune_dev_dependencies; self.context = context; self } diff --git a/packages/zpm/src/project.rs b/packages/zpm/src/project.rs index 92c99886..023426e3 100644 --- a/packages/zpm/src/project.rs +++ b/packages/zpm/src/project.rs @@ -1549,9 +1549,16 @@ impl Project { self.local_cache_path().fs_exists() }; + // After a production install, the user explicitly asked to leave + // devDependencies out; don't undo that by extending the install + let production + = self.install_state.as_ref().is_some_and(|install_state| install_state.production); + let include_dev_dependencies + = !production; + let required_workspaces = self.try_workspace_by_rel_path(&self.package_cwd)? - .map(|workspace| self.workspace_dependency_closure([workspace.name.clone()], true)) + .map(|workspace| self.workspace_dependency_closure([workspace.name.clone()], include_dev_dependencies)) .transpose()?; if cache_exists { @@ -1591,7 +1598,7 @@ impl Project { .chain(required_workspaces.iter()) .cloned(); - Some(self.workspace_dependency_closure(roots, true)?) + Some(self.workspace_dependency_closure(roots, include_dev_dependencies)?) } else { None } @@ -1605,7 +1612,7 @@ impl Project { check_checksums: false, check_resolutions: false, enforced_resolutions: BTreeMap::new(), - prune_dev_dependencies: false, + prune_dev_dependencies: production, refresh_lockfile: false, silent_or_error: true, mode: None, diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/features/lazyInstalls.test.ts b/tests/acceptance-tests/pkg-tests-specs/sources/features/lazyInstalls.test.ts index c020b95f..6f96185a 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/features/lazyInstalls.test.ts +++ b/tests/acceptance-tests/pkg-tests-specs/sources/features/lazyInstalls.test.ts @@ -200,6 +200,39 @@ describe(`Features`, () => { }), ); + test( + `it should not run install after a production focused install`, + makeTemporaryEnv({}, async ({path, run}) => { + await setupMonorepo(path); + + // A devDependency on another workspace: production installs leave it out + await xfs.writeJsonPromise(ppath.join(path, `packages/foo/package.json` as PortablePath), { + name: `foo`, + dependencies: { + [`no-deps`]: `1.0.0`, + }, + devDependencies: { + [`bar`]: `workspace:*`, + }, + }); + + await run(`install`); + await run(`workspaces`, `focus`, `foo`, `--production`, {cwd: ppath.join(path, `packages/foo` as PortablePath)}); + + const installStatePath = ppath.join(path, `.yarn/ignore/install` as PortablePath); + const stateBefore = await xfs.statPromise(installStatePath); + + await new Promise(resolve => setTimeout(resolve, 10)); + + await run(`node`, `-e`, `require('no-deps')`, { + cwd: ppath.join(path, `packages/foo` as PortablePath), + }); + + const stateAfter = await xfs.statPromise(installStatePath); + expect(stateAfter.mtimeMs).toEqual(stateBefore.mtimeMs); + }), + ); + test( `it should not run install after an immutable install`, makeTemporaryEnv({}, async ({path, run}) => { @@ -223,6 +256,34 @@ describe(`Features`, () => { }), ); + test( + `it should keep devDependencies out when a production focused install gets extended`, + makeTemporaryEnv({}, async ({path, run}) => { + await setupMonorepo(path); + + await xfs.writeJsonPromise(ppath.join(path, `packages/foo/package.json` as PortablePath), { + name: `foo`, + dependencies: { + [`no-deps`]: `1.0.0`, + }, + devDependencies: { + [`one-fixed-dep`]: `1.0.0`, + }, + }); + + await run(`install`); + await run(`workspaces`, `focus`, `foo`, `--production`, {cwd: ppath.join(path, `packages/foo` as PortablePath)}); + + // A command from another workspace extends the focused install + await run(`node`, `-e`, `require('no-deps')`, { + cwd: ppath.join(path, `packages/bar` as PortablePath), + }); + + const pnp = await xfs.readFilePromise(ppath.join(path, `.pnp.cjs` as PortablePath), `utf8`); + expect(pnp).not.toContain(`one-fixed-dep`); + }), + ); + test( `it should extend a focused install when the active workspace is missing and the lockfile is fresh`, makeTemporaryEnv({}, async ({path, run, source}) => {