fix: detect enableGlobalVirtualStore toggle in workspace state check (#12147)

* fix: detect enableGlobalVirtualStore toggle in workspace state check (#12142)

The optimisticRepeatInstall fast path bypasses validateModules entirely,
so toggling enableGlobalVirtualStore off was not detected — pnpm printed
"Already up to date" without reinstalling deps in the new virtual store
location.

Add enableGlobalVirtualStore to WORKSPACE_STATE_SETTING_KEYS so
checkDepsStatus detects the change and falls through to the full
mutateModules path (which includes the virtualStoreDir compatibility
check and purge).

* refactor: bidirectional settings comparison in checkDepsStatus

Iterate over WORKSPACE_STATE_SETTING_KEYS instead of
Object.entries(workspaceState.settings) so that settings absent from
legacy state files (e.g. enableGlobalVirtualStore) are still compared
against the current config. Removes the ad-hoc allowBuilds null-check
block since the main loop now covers it with the ?? {} coalescing.

* fix(workspace-state): track enableGlobalVirtualStore in pacquet state

Port pnpm/pnpm#12147 to pacquet. pnpm's checkDepsStatus now iterates the
full WORKSPACE_STATE_SETTING_KEYS list, so a key pacquet omits from
.pnpm-workspace-state-v1.json is read as `undefined` and compared
against pnpm's resolved config. `enableGlobalVirtualStore` resolves to a
concrete default (true, or false under CI), so omitting it would make
pnpm reinstall after a pacquet install — and pacquet itself never
detected the toggle (its own copy of pnpm/pnpm#12142).

Add `enable_global_virtual_store` to WorkspaceStateSettings;
current_settings writes `then_some(true)` (omit when off, mirroring
pnpm omitting its undefined default); settings_match coerces
`None`/`Some(false)` as equal before comparing, matching the existing
allow_builds/package_extensions handling.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
Abdullah Alaqeel
2026-06-08 18:15:49 +03:00
committed by GitHub
parent 027196babe
commit 97e1982d28
7 changed files with 367 additions and 56 deletions

View File

@@ -0,0 +1,7 @@
---
"@pnpm/workspace.state": patch
"@pnpm/deps.status": patch
"pnpm": patch
---
Fix `pnpm install` ignoring `enableGlobalVirtualStore` toggle by including it in the workspace state settings check [#12142](https://github.com/pnpm/pnpm/issues/12142).

View File

@@ -34,7 +34,7 @@ import type {
ProjectManifest,
} from '@pnpm/types'
import { findWorkspaceProjectsNoCheck } from '@pnpm/workspace.projects-reader'
import { loadWorkspaceState, updateWorkspaceState, type WorkspaceState, type WorkspaceStateSettings } from '@pnpm/workspace.state'
import { loadWorkspaceState, updateWorkspaceState, WORKSPACE_STATE_SETTING_KEYS, type WorkspaceState, type WorkspaceStateSettings } from '@pnpm/workspace.state'
import { readWorkspaceManifest } from '@pnpm/workspace.workspace-manifest-reader'
import { equals, filter, isEmpty, once } from 'ramda'
@@ -134,13 +134,16 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions, workspaceState: W
if (workspaceState.settings) {
const ignoredSettings = new Set<keyof WorkspaceStateSettings>(opts.ignoredWorkspaceStateSettings)
ignoredSettings.add('catalogs') // 'catalogs' is always ignored
for (const [settingName, settingValue] of Object.entries(workspaceState.settings)) {
ignoredSettings.add('catalogs')
for (const settingName of WORKSPACE_STATE_SETTING_KEYS) {
if (ignoredSettings.has(settingName as keyof WorkspaceStateSettings)) continue
const currentSettingValue = settingName === 'allowBuilds'
const storedValue = settingName === 'allowBuilds'
? workspaceState.settings[settingName] ?? {}
: workspaceState.settings[settingName as keyof WorkspaceStateSettings]
const currentValue = settingName === 'allowBuilds'
? opts.allowBuilds ?? {}
: opts[settingName as keyof WorkspaceStateSettings]
if (!equals(settingValue, currentSettingValue)) {
if (!equals(storedValue, currentValue)) {
return {
upToDate: false,
issue: `The value of the ${settingName} setting has changed`,
@@ -148,18 +151,6 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions, workspaceState: W
}
}
}
if (
!ignoredSettings.has('allowBuilds') &&
workspaceState.settings.allowBuilds == null &&
opts.allowBuilds != null &&
!isEmpty(opts.allowBuilds)
) {
return {
upToDate: false,
issue: 'The value of the allowBuilds setting has changed',
workspaceState,
}
}
}
if ((opts.configDependencies != null || workspaceState.configDependencies != null) && !equals(opts.configDependencies ?? {}, workspaceState.configDependencies ?? {})) {
return {

View File

@@ -259,6 +259,65 @@ describe('checkDepsStatus - settings change detection', () => {
expect(result.issue).not.toBe('The value of the allowBuilds setting has changed')
})
it('returns upToDate: false when enableGlobalVirtualStore is toggled off', async () => {
const lastValidatedTimestamp = Date.now() - 10_000
const mockWorkspaceState: WorkspaceState = {
lastValidatedTimestamp,
pnpmfiles: [],
settings: {
excludeLinksFromLockfile: false,
linkWorkspacePackages: true,
preferWorkspacePackages: true,
enableGlobalVirtualStore: true,
},
projects: {},
filteredInstall: false,
}
jest.mocked(loadWorkspaceState).mockReturnValue(mockWorkspaceState)
const opts: CheckDepsStatusOptions = {
rootProjectManifest: {},
rootProjectManifestDir: '/project',
pnpmfile: [],
...mockWorkspaceState.settings,
enableGlobalVirtualStore: false,
}
const result = await checkDepsStatus(opts)
expect(result.upToDate).toBe(false)
expect(result.issue).toBe('The value of the enableGlobalVirtualStore setting has changed')
})
it('returns upToDate: false when enableGlobalVirtualStore is toggled on from a legacy state file that lacks the key', async () => {
const lastValidatedTimestamp = Date.now() - 10_000
const mockWorkspaceState: WorkspaceState = {
lastValidatedTimestamp,
pnpmfiles: [],
settings: {
excludeLinksFromLockfile: false,
linkWorkspacePackages: true,
preferWorkspacePackages: true,
},
projects: {},
filteredInstall: false,
}
jest.mocked(loadWorkspaceState).mockReturnValue(mockWorkspaceState)
const opts: CheckDepsStatusOptions = {
rootProjectManifest: {},
rootProjectManifestDir: '/project',
pnpmfile: [],
...mockWorkspaceState.settings,
enableGlobalVirtualStore: true,
}
const result = await checkDepsStatus(opts)
expect(result.upToDate).toBe(false)
expect(result.issue).toBe('The value of the enableGlobalVirtualStore setting has changed')
})
})
describe('checkDepsStatus - pnpmfile modification', () => {
@@ -338,6 +397,9 @@ describe('checkDepsStatus - pnpmfile modification', () => {
excludeLinksFromLockfile: false,
linkWorkspacePackages: true,
preferWorkspacePackages: true,
patchedDependencies: {
foo: '/project/patches/foo.patch',
},
},
projects: {
[projectRootDir]: {

View File

@@ -165,27 +165,26 @@ pub fn check_optimistic_repeat_install(
/// Compare today's settings against what the previous install
/// recorded.
///
/// Only the fields pacquet actively populates via [`current_settings`]
/// participate in the comparison. Fields the upstream pnpm CLI writes
/// but pacquet hasn't ported yet (e.g. `excludeLinksFromLockfile`) are
/// ignored — pacquet doesn't consume them during install, so a
/// difference can't affect the materialised `node_modules`. Without
/// this carve-out a cross-package-manager scenario (pnpm wrote the
/// state, pacquet reads it next) would always reject the fast path
/// because pnpm's defaults fill those fields while pacquet's
/// `current_settings` leaves them `None`.
/// Only the fields pacquet populates via [`current_settings`]
/// participate in the comparison; the rest are listed at the end of
/// this function with the reason each is safe to skip.
///
/// As each ported setting in pnpm/pnpm#12009 lands end-to-end and
/// gets surfaced through `current_settings`, it joins the comparison
/// here automatically.
///
/// Mirrors pnpm's `Object.entries(workspaceState.settings)` walk in
/// [`checkDepsStatus`](https://github.com/pnpm/pnpm/blob/72d997cc34/deps/status/src/checkDepsStatus.ts):
/// pnpm iterates fields *in the state*, which by symmetry only
/// includes fields the writer cared about. The `allowBuilds` coercion
/// mirrors pnpm's [`opts.allowBuilds ?? {}`](https://github.com/pnpm/pnpm/blob/72d997cc34/deps/status/src/checkDepsStatus.ts#L141)
/// on the read side and pnpm's tolerance of an absent
/// `allowBuilds` key in the recorded state on the write side.
/// pnpm's [`checkDepsStatus`](https://github.com/pnpm/pnpm/blob/20f9362161/deps/status/src/checkDepsStatus.ts#L138)
/// iterates the full `WORKSPACE_STATE_SETTING_KEYS` list, reading a key
/// absent from the recorded state as `undefined`. So the reverse
/// scenario (pacquet wrote the state, pnpm reads it next) stays on the
/// fast path only for keys whose pnpm-resolved value is also
/// `undefined`. Every key pnpm resolves to a concrete default —
/// `excludeLinksFromLockfile` (`false`), `minimumReleaseAge` (`1440`),
/// `minimumReleaseAgeIgnoreMissingTime` (`true`) — must therefore be
/// written by [`current_settings`] and compared here, or pnpm would
/// report drift and re-run a (no-op) install on every command after a
/// pacquet install. `enableGlobalVirtualStore` is `undefined` by
/// default (concrete only under `--global`/CI), so pacquet's omit-when-
/// off encoding already matches. The `allowBuilds` coercion mirrors
/// pnpm's [`opts.allowBuilds ?? {}`](https://github.com/pnpm/pnpm/blob/20f9362161/deps/status/src/checkDepsStatus.ts#L143)
/// on the read side and pnpm's tolerance of an absent `allowBuilds` key
/// in the recorded state on the write side.
fn settings_match(
state: &WorkspaceState,
config: &Config,
@@ -202,11 +201,19 @@ fn settings_match(
&& recorded.dedupe_peer_dependents == live.dedupe_peer_dependents
&& recorded.dedupe_peers == live.dedupe_peers
&& recorded.dev == live.dev
&& enable_global_virtual_store_match(
recorded.enable_global_virtual_store,
live.enable_global_virtual_store,
)
&& recorded.exclude_links_from_lockfile == live.exclude_links_from_lockfile
&& recorded.hoist_pattern == live.hoist_pattern
&& recorded.hoist_workspace_packages == live.hoist_workspace_packages
&& recorded.ignored_optional_dependencies == live.ignored_optional_dependencies
&& recorded.inject_workspace_packages == live.inject_workspace_packages
&& recorded.link_workspace_packages == live.link_workspace_packages
&& recorded.minimum_release_age == live.minimum_release_age
&& recorded.minimum_release_age_ignore_missing_time
== live.minimum_release_age_ignore_missing_time
&& recorded.node_linker == live.node_linker
&& recorded.optional == live.optional
&& recorded.overrides == live.overrides
@@ -219,17 +226,36 @@ fn settings_match(
&& recorded.prefer_workspace_packages == live.prefer_workspace_packages
&& recorded.production == live.production
&& recorded.public_hoist_pattern == live.public_hoist_pattern
// Deliberately *not* compared (tracked at pnpm/pnpm#12009 — drop
// each from this list once `current_settings` writes its value):
// Deliberately *not* compared. pnpm leaves the first group
// `undefined` by default, so omitting them here still matches pnpm's
// all-key freshness check (`undefined == undefined`):
// catalogs (pnpm always ignores; see
// ignoredSettings.add('catalogs'))
// excludeLinksFromLockfile
// minimumReleaseAge* (pacquet supports it but doesn't
// round-trip through workspace state
// yet — separate follow-up).
// trustPolicy* (same situation as minimumReleaseAge)
// workspacePackagePatterns (already covered via
// pnpm-workspace.yaml `packages:`)
// minimumReleaseAgeStrict (pnpm sets it only when the user
// explicitly sets minimumReleaseAge)
// minimumReleaseAgeExclude
// trustPolicy* (all `undefined` until configured)
// workspacePackagePatterns (concrete for a multi-package
// workspace, but lives in the
// workspace manifest, not `Config`;
// threading it into `current_settings`
// is a separate follow-up. pacquet
// detects project-set changes via
// `project_structure_matches`).
}
/// `enableGlobalVirtualStore` has no `?? default` coercion on pnpm's
/// read side, but its `undefined` default and an explicit `false` both
/// mean "global virtual store off". pnpm omits the key for the former
/// and records `false` only when CI forces it; pacquet omits both.
/// Normalize the absent and `false` forms before comparing so a
/// pnpm-written file (omitted or `false`) matches a pacquet install
/// with the store off, while a real `true`/`false` flip still trips.
fn enable_global_virtual_store_match(
state_value: Option<bool>,
current_value: Option<bool>,
) -> bool {
state_value.unwrap_or(false) == current_value.unwrap_or(false)
}
/// Pnpm writes `Some({})` for an empty `allowBuilds`; pacquet writes
@@ -290,6 +316,12 @@ pub(crate) fn current_settings(
dedupe_peer_dependents: Some(config.dedupe_peer_dependents),
dedupe_peers: Some(config.dedupe_peers),
dev: Some(included.dev_dependencies),
// Mirror pnpm's writer, which omits the key for its `undefined`
// default and records a concrete value only when forced. pacquet
// has no `--global` flow, so the only "on" value it ever writes
// is `true`; an off store maps back to the omitted `None`.
enable_global_virtual_store: config.enable_global_virtual_store.then_some(true),
exclude_links_from_lockfile: Some(config.exclude_links_from_lockfile),
hoist_pattern: config.hoist_pattern.clone(),
hoist_workspace_packages: Some(config.hoist_workspace_packages),
ignored_optional_dependencies: config.ignored_optional_dependencies.clone(),
@@ -297,6 +329,10 @@ pub(crate) fn current_settings(
link_workspace_packages: Some(link_workspace_packages_to_json(
config.link_workspace_packages,
)),
minimum_release_age: config.minimum_release_age,
minimum_release_age_ignore_missing_time: Some(
config.minimum_release_age_ignore_missing_time,
),
node_linker: Some(map_node_linker(node_linker)),
optional: Some(included.optional_dependencies),
overrides: config

View File

@@ -338,6 +338,204 @@ fn returns_skipped_when_inject_workspace_packages_drifts() {
assert!(matches!(decision, Decision::Skipped { reason } if reason.contains("settings")));
}
/// Drift in `enableGlobalVirtualStore` invalidates the cached state.
/// Toggling it moves the virtual store between `<storeDir>/links` and
/// each project's `node_modules/.pnpm`, so the previous install's
/// layout no longer matches a fresh resolution. Mirrors pnpm's fix for
/// [#12142](https://github.com/pnpm/pnpm/issues/12142): the toggle was
/// invisible to the freshness check until the key joined the comparison.
#[test]
fn returns_skipped_when_enable_global_virtual_store_drifts() {
let dir = tempdir().unwrap();
let workspace_root = dir.path();
let manifest_path = workspace_root.join("package.json");
fs::write(&manifest_path, r#"{"name":"root","version":"1.0.0"}"#).unwrap();
let manifest = PackageManifest::from_path(manifest_path).unwrap();
let mut config = Config::new();
config.modules_dir = workspace_root.join("node_modules");
fs::create_dir_all(&config.modules_dir).unwrap();
config.enable_global_virtual_store = true;
let config = config.leak();
let mut stale_config = Config::new();
stale_config.modules_dir = config.modules_dir.clone();
stale_config.enable_global_virtual_store = false;
let stale_settings =
current_settings(&stale_config, pacquet_config::NodeLinker::Isolated, isolated_included());
let mut projects = BTreeMap::new();
projects.insert(
workspace_root.to_string_lossy().into_owned(),
ProjectEntry { name: Some("root".into()), version: Some("1.0.0".into()) },
);
write_state(workspace_root, now_millis() + 60_000, stale_settings, projects);
let decision = check_optimistic_repeat_install(
workspace_root,
config,
pacquet_config::NodeLinker::Isolated,
isolated_included(),
&[(workspace_root.to_path_buf(), &manifest)],
false,
);
assert!(matches!(decision, Decision::Skipped { reason } if reason.contains("settings")));
}
/// A pnpm-written state that records `enableGlobalVirtualStore: false`
/// (the value pnpm forces under CI) stays on the fast path for a pacquet
/// install with the store off, which omits the key. `false` and the
/// omitted `None` are the same "store off" state, so the coercion in
/// `enable_global_virtual_store_match` keeps the cross-package-manager
/// file from tripping a needless reinstall.
#[test]
fn returns_up_to_date_when_recorded_global_virtual_store_is_explicit_off() {
let (dir, config, manifest) =
setup_fresh_install(pacquet_config::NodeLinker::Isolated, "root", "1.0.0", "");
let mut settings =
current_settings(config, pacquet_config::NodeLinker::Isolated, isolated_included());
settings.enable_global_virtual_store = Some(false);
let mut projects = BTreeMap::new();
projects.insert(
dir.path().to_string_lossy().into_owned(),
ProjectEntry { name: Some("root".into()), version: Some("1.0.0".into()) },
);
write_state(dir.path(), now_millis(), settings, projects);
let decision = check_optimistic_repeat_install(
dir.path(),
config,
pacquet_config::NodeLinker::Isolated,
isolated_included(),
&[(dir.path().to_path_buf(), &manifest)],
false,
);
assert_eq!(decision, Decision::UpToDate);
}
/// Drift in `excludeLinksFromLockfile` invalidates the cached state.
/// pnpm resolves it to a concrete `false` default and records it, so
/// pacquet must record and compare it too — otherwise pnpm's all-key
/// freshness check reports drift on every command after a pacquet
/// install.
#[test]
fn returns_skipped_when_exclude_links_from_lockfile_drifts() {
let dir = tempdir().unwrap();
let workspace_root = dir.path();
let manifest_path = workspace_root.join("package.json");
fs::write(&manifest_path, r#"{"name":"root","version":"1.0.0"}"#).unwrap();
let manifest = PackageManifest::from_path(manifest_path).unwrap();
let mut config = Config::new();
config.modules_dir = workspace_root.join("node_modules");
fs::create_dir_all(&config.modules_dir).unwrap();
config.exclude_links_from_lockfile = true;
let config = config.leak();
let mut stale_config = Config::new();
stale_config.modules_dir = config.modules_dir.clone();
stale_config.exclude_links_from_lockfile = false;
let stale_settings =
current_settings(&stale_config, pacquet_config::NodeLinker::Isolated, isolated_included());
let mut projects = BTreeMap::new();
projects.insert(
workspace_root.to_string_lossy().into_owned(),
ProjectEntry { name: Some("root".into()), version: Some("1.0.0".into()) },
);
write_state(workspace_root, now_millis() + 60_000, stale_settings, projects);
let decision = check_optimistic_repeat_install(
workspace_root,
config,
pacquet_config::NodeLinker::Isolated,
isolated_included(),
&[(workspace_root.to_path_buf(), &manifest)],
false,
);
assert!(matches!(decision, Decision::Skipped { reason } if reason.contains("settings")));
}
/// Drift in `minimumReleaseAge` invalidates the cached state. pnpm
/// resolves it to a concrete `1440` default and records it verbatim
/// (the raw value, not the `Some(0)`-disabled resolution), so pacquet
/// records and compares the raw value too.
#[test]
fn returns_skipped_when_minimum_release_age_drifts() {
let dir = tempdir().unwrap();
let workspace_root = dir.path();
let manifest_path = workspace_root.join("package.json");
fs::write(&manifest_path, r#"{"name":"root","version":"1.0.0"}"#).unwrap();
let manifest = PackageManifest::from_path(manifest_path).unwrap();
let mut config = Config::new();
config.modules_dir = workspace_root.join("node_modules");
fs::create_dir_all(&config.modules_dir).unwrap();
config.minimum_release_age = Some(2880);
let config = config.leak();
let mut stale_config = Config::new();
stale_config.modules_dir = config.modules_dir.clone();
stale_config.minimum_release_age = Some(1440);
let stale_settings =
current_settings(&stale_config, pacquet_config::NodeLinker::Isolated, isolated_included());
let mut projects = BTreeMap::new();
projects.insert(
workspace_root.to_string_lossy().into_owned(),
ProjectEntry { name: Some("root".into()), version: Some("1.0.0".into()) },
);
write_state(workspace_root, now_millis() + 60_000, stale_settings, projects);
let decision = check_optimistic_repeat_install(
workspace_root,
config,
pacquet_config::NodeLinker::Isolated,
isolated_included(),
&[(workspace_root.to_path_buf(), &manifest)],
false,
);
assert!(matches!(decision, Decision::Skipped { reason } if reason.contains("settings")));
}
/// Drift in `minimumReleaseAgeIgnoreMissingTime` invalidates the cached
/// state. pnpm resolves it to a concrete `true` default and records it,
/// so pacquet records and compares it too.
#[test]
fn returns_skipped_when_minimum_release_age_ignore_missing_time_drifts() {
let dir = tempdir().unwrap();
let workspace_root = dir.path();
let manifest_path = workspace_root.join("package.json");
fs::write(&manifest_path, r#"{"name":"root","version":"1.0.0"}"#).unwrap();
let manifest = PackageManifest::from_path(manifest_path).unwrap();
let mut config = Config::new();
config.modules_dir = workspace_root.join("node_modules");
fs::create_dir_all(&config.modules_dir).unwrap();
config.minimum_release_age_ignore_missing_time = false;
let config = config.leak();
let mut stale_config = Config::new();
stale_config.modules_dir = config.modules_dir.clone();
stale_config.minimum_release_age_ignore_missing_time = true;
let stale_settings =
current_settings(&stale_config, pacquet_config::NodeLinker::Isolated, isolated_included());
let mut projects = BTreeMap::new();
projects.insert(
workspace_root.to_string_lossy().into_owned(),
ProjectEntry { name: Some("root".into()), version: Some("1.0.0".into()) },
);
write_state(workspace_root, now_millis() + 60_000, stale_settings, projects);
let decision = check_optimistic_repeat_install(
workspace_root,
config,
pacquet_config::NodeLinker::Isolated,
isolated_included(),
&[(workspace_root.to_path_buf(), &manifest)],
false,
);
assert!(matches!(decision, Decision::Skipped { reason } if reason.contains("settings")));
}
/// Drift in `ignoredOptionalDependencies` invalidates the cached
/// state.
///
@@ -721,12 +919,9 @@ fn returns_up_to_date_when_state_carries_unported_pnpm_settings() {
let mut settings =
current_settings(config, pacquet_config::NodeLinker::Isolated, isolated_included());
// Populate every field pacquet doesn't surface through
// `current_settings` today. Each is an upstream pnpm setting
// listed in pnpm/pnpm#12009.
settings.exclude_links_from_lockfile = Some(false);
// `catalogs` is always ignored by pnpm itself; pacquet
// mirrors that.
// Populate fields pacquet records but `settings_match` does not
// compare, to prove a difference on them keeps the fast path.
// `catalogs` is always ignored by pnpm itself; pacquet mirrors that.
settings.catalogs = Some(serde_json::json!({"default": {"react": "^18.0.0"}}));
// `workspacePackagePatterns` is recorded by pnpm from
// pnpm-workspace.yaml's `packages:` field, which pacquet

View File

@@ -91,10 +91,11 @@ pub struct WorkspaceState {
/// <https://github.com/pnpm/pnpm/blob/7ff112bac6/workspace/state/src/types.ts>.
///
/// Every field is `Option` so pacquet can omit settings it does not
/// track yet; missing keys round-trip as JSON `undefined`, which pnpm's
/// `Object.entries(workspaceState.settings)` loop simply skips. Match
/// what the install actually used — if pacquet's resolved value differs
/// from pnpm's, pnpm correctly reinstalls.
/// track yet. pnpm iterates the full `WORKSPACE_STATE_SETTING_KEYS`
/// list and reads an omitted key as `undefined`, so a key pacquet omits
/// stays compatible only while pnpm's resolved value for it is also
/// `undefined`. Match what the install actually used — if pacquet's
/// resolved value differs from pnpm's, pnpm correctly reinstalls.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceStateSettings {
@@ -114,6 +115,13 @@ pub struct WorkspaceStateSettings {
pub dedupe_peers: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dev: Option<bool>,
/// `None` and `Some(false)` both mean "global virtual store off" —
/// pnpm omits the key for its `undefined` default and only writes a
/// concrete value when `--global` (always `true`) or CI (`false`)
/// forces one. The freshness check coerces the two off-forms before
/// comparing (see `enable_global_virtual_store_match`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_global_virtual_store: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exclude_links_from_lockfile: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -126,6 +134,17 @@ pub struct WorkspaceStateSettings {
pub inject_workspace_packages: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub link_workspace_packages: Option<serde_json::Value>,
/// Minutes a published version must age before it may be installed.
/// pnpm resolves this to a concrete `24 * 60` default, so it must be
/// recorded for pnpm's all-key freshness check to stay on the fast
/// path after a pacquet install.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub minimum_release_age: Option<u64>,
/// Whether versions whose registry metadata lacks a `time` field
/// pass the maturity check. pnpm defaults this to `true`, so it is
/// recorded for the same reason as [`Self::minimum_release_age`].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub minimum_release_age_ignore_missing_time: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub node_linker: Option<NodeLinker>,
#[serde(default, skip_serializing_if = "Option::is_none")]

View File

@@ -16,6 +16,7 @@ export interface WorkspaceState {
}
export const WORKSPACE_STATE_SETTING_KEYS = [
'enableGlobalVirtualStore',
'allowBuilds',
'autoInstallPeers',
'catalogs',