This repository was archived by the owner on May 14, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 34
feat(install): validate .modules.yaml on read — error on layout drift (#464 §A) #470
Open
zkochan
wants to merge
2
commits into
main
Choose a base branch
from
feat/464
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| //! End-to-end coverage for `.modules.yaml` validation on re-install. | ||
| //! | ||
| //! Each test runs `pacquet install --frozen-lockfile` once to write | ||
| //! `.modules.yaml`, then mutates `pnpm-workspace.yaml` and re-runs | ||
| //! the install. The second install must error with a typed | ||
| //! `ValidateModulesError` variant rather than silently rebuilding | ||
| //! the layout under the new settings. | ||
| //! | ||
| //! Mirrors the scenarios at upstream's | ||
| //! [`installing/deps-installer/test/install/hoist.ts:209`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/test/install/hoist.ts#L209) | ||
| //! and [`:220`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/test/install/hoist.ts#L220), | ||
| //! both of which were stubbed as `known_failures` against | ||
| //! pnpm/pacquet#433 in the hoist PR — they're actually blocked on | ||
| //! *this* validation, not partial install. Now that #464 §A landed, | ||
| //! they pass. | ||
|
|
||
| #![cfg(unix)] // pnpm CLI: 'program not found' on Windows runners. | ||
|
|
||
| pub mod _utils; | ||
| pub use _utils::*; | ||
|
|
||
| use assert_cmd::prelude::*; | ||
| use command_extra::CommandExtra; | ||
| use pacquet_testing_utils::bin::{AddMockedRegistry, CommandTempCwd}; | ||
| use std::{fs, path::Path, process::Command}; | ||
|
|
||
| fn generate_lockfile(pnpm: Command) { | ||
| pnpm.with_args(["install", "--lockfile-only", "--ignore-scripts"]).assert().success(); | ||
| } | ||
|
|
||
| fn write_workspace_yaml(workspace: &Path, extra: &str) { | ||
| let yaml = format!("storeDir: ../pacquet-store\ncacheDir: ../pacquet-cache\n{extra}"); | ||
| fs::write(workspace.join("pnpm-workspace.yaml"), yaml).expect("write pnpm-workspace.yaml"); | ||
| } | ||
|
|
||
| fn write_manifest(workspace: &Path, deps: serde_json::Value) { | ||
| let manifest = serde_json::json!({ "dependencies": deps }); | ||
| fs::write(workspace.join("package.json"), manifest.to_string()).expect("write package.json"); | ||
| } | ||
|
|
||
| /// First install with `hoistPattern: ['*']` (the default), second | ||
| /// install with `hoistPattern: ['only-this-thing']`. The second | ||
| /// install must error with `HOIST_PATTERN_DIFF` rather than | ||
| /// silently leaving the old hoist symlinks in place. | ||
| /// | ||
| /// Mirrors upstream's | ||
| /// [`hoist.ts:209` "hoistPattern=* throws exception when executed on node_modules installed w/o the option"](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/test/install/hoist.ts#L209) | ||
| /// — that test exercises the same drift in the opposite direction | ||
| /// (install without hoist, then install with `*`); the error | ||
| /// shape is symmetric. | ||
| #[test] | ||
| fn re_install_with_changed_hoist_pattern_errors() { | ||
| let CommandTempCwd { pacquet: pacquet_first, pnpm, root, workspace, npmrc_info, .. } = | ||
| CommandTempCwd::init().add_mocked_registry(); | ||
| let AddMockedRegistry { mock_instance, .. } = npmrc_info; | ||
|
|
||
| write_manifest( | ||
| &workspace, | ||
| serde_json::json!({ "@pnpm.e2e/hello-world-js-bin-parent": "1.0.0" }), | ||
| ); | ||
| generate_lockfile(pnpm); | ||
|
|
||
| // First install — default hoist pattern (`['*']`). | ||
| pacquet_first.with_args(["install", "--frozen-lockfile"]).assert().success(); | ||
| assert!( | ||
| workspace.join("node_modules/.modules.yaml").exists(), | ||
| "first install must write .modules.yaml", | ||
| ); | ||
|
|
||
| // Re-install with a different hoist pattern via yaml override. | ||
| write_workspace_yaml(&workspace, "hoistPattern:\n - 'only-this-thing'\n"); | ||
| let pacquet_second = std::process::Command::cargo_bin("pacquet") | ||
| .expect("find the pacquet binary") | ||
| .with_current_dir(&workspace); | ||
| let output = | ||
| pacquet_second.with_args(["install", "--frozen-lockfile"]).output().expect("run pacquet"); | ||
| assert!( | ||
| !output.status.success(), | ||
| "re-install with changed hoistPattern must fail; stderr=\n{}", | ||
| String::from_utf8_lossy(&output.stderr), | ||
| ); | ||
| let stderr = String::from_utf8_lossy(&output.stderr); | ||
| assert!( | ||
| stderr.contains("hoist-pattern") || stderr.contains("hoist_pattern"), | ||
| "error message must mention hoist-pattern; got:\n{stderr}", | ||
| ); | ||
|
|
||
| drop((root, mock_instance)); | ||
| } | ||
|
|
||
| /// First install with `publicHoistPattern: ['*']`, second install | ||
| /// with `publicHoistPattern: []`. The second install must error | ||
| /// with `PUBLIC_HOIST_PATTERN_DIFF`. | ||
| #[test] | ||
| fn re_install_with_changed_public_hoist_pattern_errors() { | ||
| let CommandTempCwd { pacquet: pacquet_first, pnpm, root, workspace, npmrc_info, .. } = | ||
| CommandTempCwd::init().add_mocked_registry(); | ||
| let AddMockedRegistry { mock_instance, .. } = npmrc_info; | ||
|
|
||
| write_manifest( | ||
| &workspace, | ||
| serde_json::json!({ "@pnpm.e2e/hello-world-js-bin-parent": "1.0.0" }), | ||
| ); | ||
| generate_lockfile(pnpm); | ||
| write_workspace_yaml(&workspace, "publicHoistPattern:\n - '*'\nhoistPattern: []\n"); | ||
|
|
||
| pacquet_first.with_args(["install", "--frozen-lockfile"]).assert().success(); | ||
|
|
||
| write_workspace_yaml(&workspace, "publicHoistPattern: []\nhoistPattern: []\n"); | ||
| let pacquet_second = std::process::Command::cargo_bin("pacquet") | ||
| .expect("find the pacquet binary") | ||
| .with_current_dir(&workspace); | ||
| let output = | ||
| pacquet_second.with_args(["install", "--frozen-lockfile"]).output().expect("run pacquet"); | ||
| assert!( | ||
| !output.status.success(), | ||
| "re-install with changed publicHoistPattern must fail; stderr=\n{}", | ||
| String::from_utf8_lossy(&output.stderr), | ||
| ); | ||
| let stderr = String::from_utf8_lossy(&output.stderr); | ||
| assert!( | ||
| stderr.contains("public-hoist-pattern") || stderr.contains("public_hoist_pattern"), | ||
| "error must mention public-hoist-pattern; got:\n{stderr}", | ||
| ); | ||
|
|
||
| drop((root, mock_instance)); | ||
| } | ||
|
|
||
| /// Re-installing with the same effective layout (no yaml change) | ||
| /// must NOT error — only drift triggers the validation. Guards | ||
| /// against the validator firing on every re-install when nothing | ||
| /// changed. | ||
| #[test] | ||
| fn re_install_with_no_change_succeeds() { | ||
| let CommandTempCwd { pacquet: pacquet_first, pnpm, root, workspace, npmrc_info, .. } = | ||
| CommandTempCwd::init().add_mocked_registry(); | ||
| let AddMockedRegistry { mock_instance, .. } = npmrc_info; | ||
|
|
||
| write_manifest( | ||
| &workspace, | ||
| serde_json::json!({ "@pnpm.e2e/hello-world-js-bin-parent": "1.0.0" }), | ||
| ); | ||
| generate_lockfile(pnpm); | ||
|
|
||
| pacquet_first.with_args(["install", "--frozen-lockfile"]).assert().success(); | ||
|
|
||
| let pacquet_second = std::process::Command::cargo_bin("pacquet") | ||
| .expect("find the pacquet binary") | ||
| .with_current_dir(&workspace); | ||
| pacquet_second.with_args(["install", "--frozen-lockfile"]).assert().success(); | ||
|
|
||
| drop((root, mock_instance)); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.