diff --git a/.changeset/cas-install-mode.md b/.changeset/cas-install-mode.md new file mode 100644 index 0000000000..9035cfcd3e --- /dev/null +++ b/.changeset/cas-install-mode.md @@ -0,0 +1,5 @@ +--- +"pacquet": minor +--- + +Added experimental `nodeLinker: { type: loaded }` installation. Compatible dependencies load directly from the content-addressable store through an automatically registered Node.js loader. `nodeLinker.excluded` selects packages and their dependency trees to install in the global virtual store. diff --git a/.changeset/store-backed-module-loader.md b/.changeset/store-backed-module-loader.md new file mode 100644 index 0000000000..0492ebae51 --- /dev/null +++ b/.changeset/store-backed-module-loader.md @@ -0,0 +1,7 @@ +--- +"@pnpm/esm-loader": minor +--- + +Added an experimental Node.js loader that reads ESM, CommonJS, and JSON modules directly from the content-addressable store using an explicit dependency manifest. Applications can load compatible packages without materializing package directories. + +Packages can opt out of CAS loading by using a normal global virtual store installation with their dependency trees. diff --git a/.github/scripts/prepare-docs-sync.test.mjs b/.github/scripts/prepare-docs-sync.test.mjs index 328b3e3efe..b782dcc1a4 100644 --- a/.github/scripts/prepare-docs-sync.test.mjs +++ b/.github/scripts/prepare-docs-sync.test.mjs @@ -8,7 +8,7 @@ import { prepareDocsSync } from './prepare-docs-sync.mjs' function fixture (t, tag = 'v12.8.2') { const repo = mkdtempSync(path.join(os.tmpdir(), 'pnpm-docs-release-')) - t.after(() => rmSync(repo, { recursive: true, force: true })) + t.after(() => rmSync(repo, { recursive: true, force: true, maxRetries: 3 })) const git = (...args) => execFileSync('git', args, { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() const version = tag.startsWith('pnpr@') ? tag.slice(5) : tag.slice(1) for (const [product, current] of [['pnpm/npm/pnpm', '12.8.2'], ['pnpm11/pnpm', '11.28.2'], ['pnpr/npm/pnpr', '0.1.0-alpha.14']]) { diff --git a/.github/scripts/run-ts-tests-chunk.mjs b/.github/scripts/run-ts-tests-chunk.mjs index 0454031dfd..53e9dc4306 100644 --- a/.github/scripts/run-ts-tests-chunk.mjs +++ b/.github/scripts/run-ts-tests-chunk.mjs @@ -1,9 +1,11 @@ #!/usr/bin/env node import { spawn, spawnSync } from 'node:child_process' +import console from 'node:console' import { readFileSync } from 'node:fs' import { mkdir, readdir, stat, writeFile } from 'node:fs/promises' import path from 'node:path' import { performance } from 'node:perf_hooks' +import process from 'node:process' import { readTestDurations, selectChunk, taskWeight } from './ts-test-chunks.mjs' @@ -179,6 +181,9 @@ async function runScriptTask (pkg) { await runCommand('node', ['--test', 'test/test.js', 'test/e2e.test.js'], { cwd: pkg.path }) } else if (pkg.manifest.name === '@pnpm/eslint-config') { await runCommand('node', ['--test', 'test/perfectionist.test.js'], { cwd: pkg.path }) + } else if (pkg.manifest.name === '@pnpm/esm-loader') { + await runCommand('node', ['--test', 'test/loader.test.mjs', 'scripts/test-unrs-resolver.mjs'], { cwd: pkg.path }) + await runCommand('node', ['scripts/bundle-runtime.mjs', '--check'], { cwd: pkg.path }) } else { throw new Error(`Unsupported non-Jest .test script in ${relDir}: ${pkg.manifest.scripts['.test']}`) } diff --git a/.meta-updater/src/index.ts b/.meta-updater/src/index.ts index f7100e6b58..19d4f57333 100644 --- a/.meta-updater/src/index.ts +++ b/.meta-updater/src/index.ts @@ -14,14 +14,13 @@ import { writeJsonFile } from 'write-json-file' const CLI_PKG_NAME = 'pnpm' -// The Rust products' npm wrapper packages. Their manifests are release -// artifacts owned by the respective generate-packages.mjs scripts and are -// versioned independently of the TypeScript packages, so none of the -// normalizations below may touch them. -const RUST_WRAPPER_PKGS = new Set([ +// These packages have their own release metadata and build pipelines; +// the TypeScript package entry points, engine range, and Jest scripts do not apply. +const NON_TYPESCRIPT_PKGS = new Set([ 'pacquet', '@pnpm/napi', '@pnpm/pnpr', + '@pnpm/esm-loader', ]) // Files that must be packed with mode 0755 in both `pnpm` and `@pnpm/exe`. @@ -78,7 +77,7 @@ export default async (workspaceDir: string) => { // eslint-disable-line if (!manifest) { return manifest } - if (manifest.name && RUST_WRAPPER_PKGS.has(manifest.name)) { + if (manifest.name && NON_TYPESCRIPT_PKGS.has(manifest.name)) { return manifest } if (manifest.name === 'monorepo-root') { diff --git a/.typos.toml b/.typos.toml index 9e83f2b87e..55614d73ac 100644 --- a/.typos.toml +++ b/.typos.toml @@ -4,6 +4,8 @@ [files] extend-exclude = [ + # Generated JavaScript contains minified dependency identifiers. + "pnpm/crates/deps-restorer/src/cas-loader.mjs.inc", # Generated SVG IDs and partial terminal-animation frames are not prose. "pnpm/docs/static/img/logos/pnpm-light-no-text.svg", "pnpm/docs/static/img/demos/pnpm-install.svg", diff --git a/Cargo.lock b/Cargo.lock index fe5452ca8d..05451794d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5001,6 +5001,7 @@ dependencies = [ "text-block-macros", "tokio", "tracing", + "url", "walkdir", "windows-sys 0.61.2", ] diff --git a/__patches__/unrs-resolver@1.12.2.patch b/__patches__/unrs-resolver@1.12.2.patch new file mode 100644 index 0000000000..095cfec024 --- /dev/null +++ b/__patches__/unrs-resolver@1.12.2.patch @@ -0,0 +1,99 @@ +diff --git a/index.js b/index.js +index 2d0388b17de6d0e4e4d0c04292aa7690fb9367aa..b7ce489cd0e84f15dd27fe06bbdb16efdcd0e577 100644 +--- a/index.js ++++ b/index.js +@@ -1,3 +1,8 @@ ++if (process.env.UNRS_RESOLVER_NODE_RESOLUTION === '1') { ++ module.exports = require('./node-resolution.cjs') ++ return ++} ++ + // prettier-ignore + /* eslint-disable */ + // @ts-nocheck +diff --git a/node-resolution.cjs b/node-resolution.cjs +new file mode 100644 +index 0000000000000000000000000000000000000000..0a2457a22815666d936ef8b2547ca28fa7b78c10 +--- /dev/null ++++ b/node-resolution.cjs +@@ -0,0 +1,66 @@ ++'use strict' ++ ++const { createRequire, isBuiltin, registerHooks } = require('node:module') ++const path = require('node:path') ++ ++// Opt-in Node semantics: aliases, tsconfig paths, and other unrs-specific ++// resolution policies are not applied in this mode. ++class ResolverFactory { ++ constructor(options = {}) { ++ this.options = options ++ } ++ ++ static default() { ++ return new ResolverFactory() ++ } ++ ++ cloneWithOptions(options) { ++ return new ResolverFactory({ ...this.options, ...options }) ++ } ++ ++ clearCache() {} ++ ++ sync(directory, request) { ++ if (isBuiltin(request)) { ++ return { builtin: { resolved: request, isRuntimeModule: request.startsWith('node:') } } ++ } ++ const conditions = this.options.conditionNames ++ const hooks = conditions && registerHooks({ ++ resolve(specifier, context, nextResolve) { ++ return nextResolve(specifier, { ...context, conditions }) ++ }, ++ }) ++ try { ++ const requireFrom = createRequire(path.join(directory, '__unrs_resolve__.cjs')) ++ return { path: requireFrom.resolve(request) } ++ } catch (error) { ++ if (error.code === 'MODULE_NOT_FOUND' || error.code === 'ERR_MODULE_NOT_FOUND' || ++ error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || error.code === 'ERR_PACKAGE_IMPORT_NOT_DEFINED' || ++ error.code === 'ERR_PNPM_LOADER_UNDECLARED_DEPENDENCY') { ++ return { error: error.message } ++ } ++ throw error ++ } finally { ++ hooks?.deregister() ++ } ++ } ++ ++ async async(directory, request) { ++ return this.sync(directory, request) ++ } ++ ++ resolveFileSync(filename, request) { ++ return this.sync(path.dirname(filename), request) ++ } ++ ++ async resolveFileAsync(filename, request) { ++ return this.resolveFileSync(filename, request) ++ } ++} ++ ++module.exports = { ++ ResolverFactory, ++ EnforceExtension: { Auto: 0, Enabled: 1, Disabled: 2 }, ++ ModuleType: { Module: 'module', CommonJs: 'commonjs', Json: 'json', Wasm: 'wasm', Addon: 'addon' }, ++ sync: (directory, request, options) => new ResolverFactory(options).sync(directory, request), ++} +diff --git a/package.json b/package.json +index 6214d141fda5719227f13bf3b815dc69b0d08f53..bef4e2d859f0296140e162b43e495469d1d4de28 100644 +--- a/package.json ++++ b/package.json +@@ -15,7 +15,8 @@ + "browser.js", + "index.d.ts", + "index.js", +- "postinstall.js" ++ "postinstall.js", ++ "node-resolution.cjs" + ], + "main": "index.js", + "browser": "browser.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cf74a2643..e8116251a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -990,6 +990,7 @@ patchedDependencies: '@tybys/wasm-util@0.10.4': 055e4dc4f0d5875fa7a4fe053318a52f9018a9d1020af186bd2fc41987d7a665 graceful-fs@4.2.11: 68ebc232025360cb3dcd3081f4067f4e9fc022ab6b6f71a3230e86c7a5b337d1 normalize-registry-url@2.0.1: e80227ac379f28c9f982c0588f5dfbcd517912bdd3a5c77a75e944c328fd8932 + unrs-resolver@1.12.2: 9e44fe60fe5cd3ec628b4745dca0de2208757084e03808abb85a391117419bee importers: @@ -1114,6 +1115,12 @@ importers: specifier: workspace:* version: 'link:' + pnpm/esm-loader: + dependencies: + enhanced-resolve: + specifier: 5.25.1 + version: 5.25.1 + pnpm/npm/napi: {} pnpm/npm/pnpm: @@ -14630,6 +14637,10 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.25.1: + resolution: {integrity: sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==} + engines: {node: '>=10.13.0'} + enhanced-resolve@5.26.0: resolution: {integrity: sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==} engines: {node: '>=10.13.0'} @@ -22253,6 +22264,11 @@ snapshots: dependencies: once: 1.4.0 + enhanced-resolve@5.25.1: + dependencies: + graceful-fs: 4.2.11(patch_hash=68ebc232025360cb3dcd3081f4067f4e9fc022ab6b6f71a3230e86c7a5b337d1) + tapable: 2.3.3 + enhanced-resolve@5.26.0: dependencies: graceful-fs: 4.2.11(patch_hash=68ebc232025360cb3dcd3081f4067f4e9fc022ab6b6f71a3230e86c7a5b337d1) @@ -22340,12 +22356,12 @@ snapshots: eslint: 10.12.0(jiti@2.6.1)(supports-color@10.2.2) semver: 7.8.5 - eslint-import-context@0.1.9(unrs-resolver@1.12.2): + eslint-import-context@0.1.9(unrs-resolver@1.12.2(patch_hash=9e44fe60fe5cd3ec628b4745dca0de2208757084e03808abb85a391117419bee)): dependencies: get-tsconfig: 4.14.3 stable-hash-x: 0.2.0 optionalDependencies: - unrs-resolver: 1.12.2 + unrs-resolver: 1.12.2(patch_hash=9e44fe60fe5cd3ec628b4745dca0de2208757084e03808abb85a391117419bee) eslint-plugin-es-x@7.8.0(eslint@10.12.0(jiti@2.6.1)(supports-color@10.2.2)): dependencies: @@ -22360,12 +22376,12 @@ snapshots: comment-parser: 1.4.9 debug: 4.4.3(supports-color@10.2.2) eslint: 10.12.0(jiti@2.6.1)(supports-color@10.2.2) - eslint-import-context: 0.1.9(unrs-resolver@1.12.2) + eslint-import-context: 0.1.9(unrs-resolver@1.12.2(patch_hash=9e44fe60fe5cd3ec628b4745dca0de2208757084e03808abb85a391117419bee)) is-glob: 4.0.3 minimatch: 10.2.6 semver: 7.8.5 stable-hash-x: 0.2.0 - unrs-resolver: 1.12.2 + unrs-resolver: 1.12.2(patch_hash=9e44fe60fe5cd3ec628b4745dca0de2208757084e03808abb85a391117419bee) optionalDependencies: '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) transitivePeerDependencies: @@ -23582,7 +23598,7 @@ snapshots: jest-util: 30.4.1 jest-validate: 30.4.1 slash: 3.0.0 - unrs-resolver: 1.12.2 + unrs-resolver: 1.12.2(patch_hash=9e44fe60fe5cd3ec628b4745dca0de2208757084e03808abb85a391117419bee) jest-runner@30.4.2(@babel/types@7.29.8)(supports-color@10.2.2): dependencies: @@ -25855,7 +25871,7 @@ snapshots: unpipe@1.0.0: {} - unrs-resolver@1.12.2: + unrs-resolver@1.12.2(patch_hash=9e44fe60fe5cd3ec628b4745dca0de2208757084e03808abb85a391117419bee): dependencies: napi-postinstall: 0.3.4 optionalDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 06246d3562..bae80a189b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -52,6 +52,7 @@ packages: - pnpm11/workspace/* - pnpm11/yaml/* - pnpm/npm/napi + - pnpm/esm-loader - pnpm/npm/pnpm - pnpm/tasks/registry-mock - pnpm/tasks/ecosystem-e2e/webcontainer @@ -663,6 +664,7 @@ patchedDependencies: '@tybys/wasm-util@0.10.4': __patches__/@tybys__wasm-util@0.10.4.patch graceful-fs@4.2.11: __patches__/graceful-fs@4.2.11.patch normalize-registry-url@2.0.1: __patches__/normalize-registry-url@2.0.1.patch + unrs-resolver@1.12.2: __patches__/unrs-resolver@1.12.2.patch patchesDir: __patches__ diff --git a/pnpm/crates/cli/src/cli_args/dispatch/configuration.rs b/pnpm/crates/cli/src/cli_args/dispatch/configuration.rs index 90ee08199e..6553c5fe9e 100644 --- a/pnpm/crates/cli/src/cli_args/dispatch/configuration.rs +++ b/pnpm/crates/cli/src/cli_args/dispatch/configuration.rs @@ -215,7 +215,7 @@ pub(in crate::cli_args) fn seed_config( Config { npmrc_auth_file: npmrc_auth_file.map(Path::to_path_buf), ignore_workspace, - cli_setting_values: config_overrides.unported_settings().clone(), + cli_setting_values: config_overrides.initial_setting_values(), ..Config::default() } } diff --git a/pnpm/crates/cli/src/cli_args/exec.rs b/pnpm/crates/cli/src/cli_args/exec.rs index b5166a6156..2d8a1417f5 100644 --- a/pnpm/crates/cli/src/cli_args/exec.rs +++ b/pnpm/crates/cli/src/cli_args/exec.rs @@ -239,16 +239,8 @@ fn command_in_dir( if let Some(name) = &project_name { cmd.env("PNPM_PACKAGE_NAME", name); } - let mut node_options = configured_node_options(config); - if let Some(pnp_path) = pnp_path_for_execution(config, project) { - node_options = Some(make_node_require_option(&pnp_path, node_options.as_deref())); - } - if let Some(package_map_path) = package_map_path_for_execution(config, project) { - node_options = - Some(make_node_package_map_option(&package_map_path, node_options.as_deref())); - } // pnpm forwards `nodeOptions` as `NODE_OPTIONS` to the child. - if let Some(node_options) = node_options { + if let Some(node_options) = execution_node_options(config, project) { cmd.env("NODE_OPTIONS", node_options); } @@ -335,3 +327,20 @@ fn command_search_path( prepend.extend(pnpm_python_installer::execution_paths(config, project).iter().cloned()); prepend_dirs_to_path(&prepend).map_err(ExecError::from) } + +fn execution_node_options(config: &Config, project: &Path) -> Option { + let mut node_options = configured_node_options(config); + if let Some(pnp_path) = pnp_path_for_execution(config, project) { + node_options = Some(make_node_require_option(&pnp_path, node_options.as_deref())); + } + if let Some(package_map_path) = package_map_path_for_execution(config, project) { + node_options = + Some(make_node_package_map_option(&package_map_path, node_options.as_deref())); + } + let mut loader_env = HashMap::new(); + if let Some(options) = node_options.take() { + loader_env.insert("NODE_OPTIONS".to_string(), options); + } + config.add_cas_loader_env(project, &mut loader_env); + loader_env.remove("NODE_OPTIONS") +} diff --git a/pnpm/crates/cli/src/cli_args/exec/recursive.rs b/pnpm/crates/cli/src/cli_args/exec/recursive.rs index dc9f2008ca..32f5fee5e7 100644 --- a/pnpm/crates/cli/src/cli_args/exec/recursive.rs +++ b/pnpm/crates/cli/src/cli_args/exec/recursive.rs @@ -262,7 +262,8 @@ fn prepare_exec_tasks( &full_task_graph, workspace_root, |_, _| Vec::new(), - ); + ) + .with_modules_dir(&config.modules_dir); let mut task_graph = resumed_exec_task_graph( args, &selection.selected, diff --git a/pnpm/crates/cli/src/cli_args/pipeline/execution.rs b/pnpm/crates/cli/src/cli_args/pipeline/execution.rs index b79a00511d..d8612c20e1 100644 --- a/pnpm/crates/cli/src/cli_args/pipeline/execution.rs +++ b/pnpm/crates/cli/src/cli_args/pipeline/execution.rs @@ -372,6 +372,7 @@ pub(super) fn task_environment( base_extra_env: &HashMap, ) -> HashMap { let mut extra_env = base_extra_env.clone(); + config.add_cas_loader_env(root, &mut extra_env); if let Some(pnp_path) = pnp_path_for_execution(config, root) { let node_options = extra_env.get("NODE_OPTIONS").map(String::as_str); extra_env.insert( diff --git a/pnpm/crates/cli/src/cli_args/run/execution.rs b/pnpm/crates/cli/src/cli_args/run/execution.rs index f452eea8b4..53cf4ace61 100644 --- a/pnpm/crates/cli/src/cli_args/run/execution.rs +++ b/pnpm/crates/cli/src/cli_args/run/execution.rs @@ -180,6 +180,7 @@ pub(super) fn selected_scripts( /// non-default linker needs prepended to `NODE_OPTIONS`. pub(super) fn script_extra_env(config: &Config, dir: &Path) -> HashMap { let mut extra_env = config.extra_env_with_node_options(); + config.add_cas_loader_env(dir, &mut extra_env); if let Some(pnp_path) = pnp_path_for_execution(config, dir) { let node_options = extra_env.get("NODE_OPTIONS").map(String::as_str); extra_env.insert( diff --git a/pnpm/crates/cli/src/cli_args/run/recursive.rs b/pnpm/crates/cli/src/cli_args/run/recursive.rs index 0c047079b0..98e2082839 100644 --- a/pnpm/crates/cli/src/cli_args/run/recursive.rs +++ b/pnpm/crates/cli/src/cli_args/run/recursive.rs @@ -285,7 +285,8 @@ impl RecursiveRun<'_, '_> { &full_task_graph, self.workspace_root, |node, script| self.script_commands(node, script), - ); + ) + .with_modules_dir(&self.config.modules_dir); let mut task_graph = resume_task_graph( &task_run_state_context, self.args, diff --git a/pnpm/crates/cli/src/cli_args/run/recursive/execution.rs b/pnpm/crates/cli/src/cli_args/run/recursive/execution.rs index c78f97c436..b95ec9ce67 100644 --- a/pnpm/crates/cli/src/cli_args/run/recursive/execution.rs +++ b/pnpm/crates/cli/src/cli_args/run/recursive/execution.rs @@ -344,6 +344,7 @@ fn project_extra_env( extra_env: &HashMap, ) -> HashMap { let mut extra_env = extra_env.clone(); + config.add_cas_loader_env(root, &mut extra_env); if let Some(pnp_path) = pnp_path_for_execution(config, root) { let node_options = extra_env.get("NODE_OPTIONS").map(String::as_str); extra_env.insert( diff --git a/pnpm/crates/cli/src/cli_args/task_run_state.rs b/pnpm/crates/cli/src/cli_args/task_run_state.rs index 30f5c18fa9..cfe75beacd 100644 --- a/pnpm/crates/cli/src/cli_args/task_run_state.rs +++ b/pnpm/crates/cli/src/cli_args/task_run_state.rs @@ -151,6 +151,12 @@ impl TaskRunStateContext { Self { state_dir, latest_state_path, invocation, keys_by_id, ids_by_key } } + pub fn with_modules_dir(mut self, modules_dir: &Path) -> Self { + self.state_dir = modules_dir.join(STATE_DIR); + self.latest_state_path = self.state_dir.join(LATEST_STATE_FILE); + self + } + pub fn read_completed_tasks(&self) -> miette::Result>> { let Some(latest_run) = self.resumable_latest_run()? else { return Ok(None) }; let (run, finished) = match self.newest_state(&latest_run) { diff --git a/pnpm/crates/cli/src/config_overrides.rs b/pnpm/crates/cli/src/config_overrides.rs index 70d949f8f3..63ebdaa17b 100644 --- a/pnpm/crates/cli/src/config_overrides.rs +++ b/pnpm/crates/cli/src/config_overrides.rs @@ -7,7 +7,7 @@ use apply::normalize_registry_url; use pnpm_config::{ ColorMode, Config, EnvVar, GLOBAL_LAYOUT_VERSION, GetCurrentDir, GetHomeDir, LinkProbe, - LinkWorkspacePackages, NodeLinker, PackageImportMethod, PmOnFail, RuntimeOnFail, + LinkWorkspacePackages, NodeLinkerSetting, PackageImportMethod, PmOnFail, RuntimeOnFail, SaveWorkspaceProtocol, TrustPolicy, VerifyDepsBeforeRun, default_state_dir, naming_cases::to_kebab_case, resolve_child_concurrency, }; @@ -103,7 +103,7 @@ pub struct ConfigOverrides { /// whichever directory ends up anchoring the install. modules_dir: Option, virtual_store_dir: Option, - node_linker: Option, + node_linker: Option, optimistic_repeat_install: Option, package_import_method: Option, pm_on_fail: Option, @@ -197,6 +197,15 @@ impl ConfigOverrides { &self.unported } + pub(crate) fn initial_setting_values(&self) -> BTreeMap { + let mut values = self.unported_settings().clone(); + // Linker selection must reach directory derivation before command overrides run. + if let Some(linker) = &self.node_linker { + values.insert("node-linker".to_string(), setting_value(linker).to_string()); + } + values + } + /// Pull `--config.=` tokens and [`BARE_SETTING_FLAGS`](tokens::BARE_SETTING_FLAGS) /// spellings out of `argv` and collect them. Returns the parsed /// overrides together with the remaining argv tokens (in their diff --git a/pnpm/crates/cli/src/config_overrides/apply.rs b/pnpm/crates/cli/src/config_overrides/apply.rs index f54bbec4ec..5c7901833e 100644 --- a/pnpm/crates/cli/src/config_overrides/apply.rs +++ b/pnpm/crates/cli/src/config_overrides/apply.rs @@ -354,8 +354,8 @@ impl ConfigOverrides { } fn apply_linker_and_run_overrides(&self, config: &mut Config) { - if let Some(value) = self.node_linker { - config.node_linker = value; + if let Some(value) = &self.node_linker { + value.clone().apply_to(config); config.explicit_settings.insert("nodeLinker".to_string(), setting_value(value)); // A CLI-selected hoisted linker turns the default on just // like a yaml-selected one — pnpm merges CLI options before diff --git a/pnpm/crates/cli/src/config_overrides/tables.rs b/pnpm/crates/cli/src/config_overrides/tables.rs index ce9cbc20cd..534f7328e7 100644 --- a/pnpm/crates/cli/src/config_overrides/tables.rs +++ b/pnpm/crates/cli/src/config_overrides/tables.rs @@ -165,7 +165,7 @@ impl ConfigOverrides { self.modules_dir = Some(value.to_string()); } "node-linker" => { - self.node_linker = parse_enum(value); + self.node_linker = serde_json::from_str(value).ok().or_else(|| parse_enum(value)); } "public-hoist-pattern" => { self.public_hoist_pattern.get_or_insert_default().push(value.to_string()); diff --git a/pnpm/crates/cli/tests/suite/cas_linker.rs b/pnpm/crates/cli/tests/suite/cas_linker.rs new file mode 100644 index 0000000000..508c864852 --- /dev/null +++ b/pnpm/crates/cli/tests/suite/cas_linker.rs @@ -0,0 +1,478 @@ +use assert_cmd::prelude::*; +use command_extra::CommandExtra; +use pnpm_testing_utils::bin::{AddMockedRegistry, CommandTempCwd}; +use serde_json::Value; +use std::{fs, path::Path, process::Command}; + +fn assert_no_node_modules(root: &Path) { + for entry in walkdir::WalkDir::new(root) { + let entry = entry.expect("walk application"); + assert_ne!(entry.file_name(), "node_modules", "{}", entry.path().display()); + } +} + +#[test] +fn loaded_linker_cli_object_applies_exclusions_before_deriving_installation_layout() { + let CommandTempCwd { + pacquet, + root, + workspace, + npmrc_info, + .. + } = CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + fs::write( + workspace.join("package.json"), + r#"{"dependencies":{"@pnpm.e2e/pkg-with-1-dep":"100.0.0"}}"#, + ) + .unwrap(); + let flag = r#"--config.node-linker={"type":"loaded","excluded":["@pnpm.e2e/pkg-with-1-dep"]}"#; + pacquet + .with_args(["install", flag]) + .assert() + .success(); + assert_no_node_modules(&workspace); + let manifest: Value = + serde_json::from_slice(&fs::read(workspace.join(".pnpm-store.json")).unwrap()).unwrap(); + assert_eq!(manifest["packages"]["@pnpm.e2e/pkg-with-1-dep@100.0.0"]["resolution"], "node"); + let output = Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["config", "get", "nodeLinker", "--json", flag]) + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert_eq!( + serde_json::from_slice::(&output.stdout).unwrap(), + serde_json::json!({"type": "loaded", "excluded": ["@pnpm.e2e/pkg-with-1-dep"]}), + ); + drop((root, mock_instance)); +} + +#[test] +fn cas_install_runs_scripts_bins_and_children_without_node_modules() { + let CommandTempCwd { + pacquet, + root, + workspace, + npmrc_info, + .. + } = CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + fs::write( + workspace.join("package.json"), + serde_json::json!({ + "name": "application", + "scripts": { "test": "node app.cjs" }, + "dependencies": { "@pnpm.e2e/pkg-with-1-dep": "100.0.0" }, + "devDependencies": { "@pnpm.e2e/hello-world-js-bin": "1.0.0" } + }) + .to_string(), + ) + .unwrap(); + let yaml_path = workspace.join("pnpm-workspace.yaml"); + let mut yaml = fs::read_to_string(&yaml_path).unwrap(); + yaml.push_str("nodeLinker:\n type: loaded\n"); + fs::write(yaml_path, yaml).unwrap(); + fs::write(workspace.join("app.cjs"), "const assert = require('node:assert/strict'); const value = require('@pnpm.e2e/pkg-with-1-dep')(); assert.equal(value.name, '@pnpm.e2e/dep-of-pkg-with-1-dep'); require('node:child_process').execFileSync(process.execPath, ['-e', `require('@pnpm.e2e/pkg-with-1-dep')()`], {stdio: 'inherit'}); console.log('CAS works');").unwrap(); + pacquet + .with_arg("install") + .assert() + .success(); + assert_no_node_modules(&workspace); + #[cfg(unix)] + Command::new(workspace.join(".pnpm/.bin/hello-world-js-bin")) + .with_current_dir(&workspace) + .env_remove("NODE_OPTIONS") + .assert() + .success(); + assert!(workspace.join(".pnpm/.pnpm-workspace-state-v1.json").is_file()); + let loader_mtime = fs::metadata(workspace.join(".pnpm-store-loader.mjs")) + .unwrap() + .modified() + .unwrap(); + for args in [ + vec!["run", "test"], + vec!["run", "test"], + vec!["exec", "node", "app.cjs"], + vec!["node", "app.cjs"], + ] { + let output = Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_arg("--config.verify-deps-before-run=error") + .with_args(args) + .env_remove("NODE_OPTIONS") + .assert() + .success(); + assert!(String::from_utf8_lossy(&output.get_output().stdout).contains("CAS works")); + } + assert_eq!( + fs::metadata(workspace.join(".pnpm-store-loader.mjs")) + .unwrap() + .modified() + .unwrap(), + loader_mtime, + ); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["exec", "hello-world-js-bin"]) + .assert() + .success(); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["install", "--frozen-lockfile"]) + .assert() + .success(); + assert_no_node_modules(&workspace); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["run", "test"]) + .assert() + .success(); + let manifest: Value = + serde_json::from_slice(&fs::read(workspace.join(".pnpm-store.json")).unwrap()).unwrap(); + assert!(manifest["packages"]["@pnpm.e2e/pkg-with-1-dep@100.0.0"]["files"].is_object()); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["install", "--prod", "--frozen-lockfile"]) + .assert() + .success(); + let production: Value = + serde_json::from_slice(&fs::read(workspace.join(".pnpm-store.json")).unwrap()).unwrap(); + assert!(production["packages"]["."]["dependencies"]["@pnpm.e2e/hello-world-js-bin"].is_null()); + assert!(!workspace.join(".pnpm/.bin/hello-world-js-bin").exists()); + assert_no_node_modules(&workspace); + drop((root, mock_instance)); +} + +#[test] +fn cas_opt_out_materializes_complete_dependency_tree_in_gvs() { + let CommandTempCwd { + pacquet, + root, + workspace, + npmrc_info, + .. + } = CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + fs::write(workspace.join("package.json"), r#"{"dependencies":{"@pnpm.e2e/pkg-with-1-dep":"100.0.0","@pnpm.e2e/hello-world-js-bin":"1.0.0"}}"#).unwrap(); + let yaml_path = workspace.join("pnpm-workspace.yaml"); + let mut yaml = fs::read_to_string(&yaml_path).unwrap(); + yaml.push_str("nodeLinker:\n type: loaded\n excluded:\n - '@pnpm.e2e/pkg-with-1-dep'\n"); + fs::write(yaml_path, yaml).unwrap(); + pacquet + .with_arg("install") + .assert() + .success(); + assert_no_node_modules(&workspace); + let manifest: Value = + serde_json::from_slice(&fs::read(workspace.join(".pnpm-store.json")).unwrap()).unwrap(); + let packages = manifest["packages"].as_object().unwrap(); + let materialized: Vec<_> = packages + .values() + .filter(|package| package["resolution"] == "node") + .collect(); + assert_eq!(materialized.len(), 2); + for package in materialized { + let path = Path::new(package["root"].as_str().unwrap()); + assert!(path.join("package.json").is_file()); + assert!(!path.starts_with(&workspace)); + } + assert!(packages["@pnpm.e2e/hello-world-js-bin@1.0.0"]["files"].is_object()); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["store", "prune"]) + .assert() + .success(); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["exec", "hello-world-js-bin"]) + .assert() + .success(); + + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args([ + "exec", + "node", + "-e", + "const p=require('@pnpm.e2e/pkg-with-1-dep'); console.log(p().name)", + ]) + .assert() + .success(); + drop((root, mock_instance)); +} + +#[test] +fn cas_workspace_preserves_workspace_and_alias_dependency_contexts() { + let CommandTempCwd { + pacquet, + root, + workspace, + npmrc_info, + .. + } = CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + fs::create_dir_all(workspace.join("packages/child")).unwrap(); + fs::write( + workspace.join("package.json"), + r#"{"name":"root","dependencies":{"child":"workspace:*"}}"#, + ) + .unwrap(); + fs::write(workspace.join("packages/child/package.json"), r#"{"name":"child","version":"1.0.0","main":"index.cjs","bin":{"nested":"bin.cjs"},"scripts":{"test":"node index.cjs"},"dependencies":{"aliased":"npm:@pnpm.e2e/pkg-with-1-dep@100.0.0"}}"#).unwrap(); + fs::write(workspace.join("packages/child/index.cjs"), "require('node:assert/strict').equal(require('aliased')().name, '@pnpm.e2e/dep-of-pkg-with-1-dep'); module.exports = 42;").unwrap(); + fs::write(workspace.join("packages/child/bin.cjs"), r"const assert = require('node:assert/strict'); const options = process.env.NODE_OPTIONS; assert.equal(options.split(/\s+/).filter(option => option.includes('.pnpm-store-loader.mjs')).length, 1); if (!process.env.NESTED_SHIM) require('node:child_process').execFileSync('nested', [], {env: {...process.env, NESTED_SHIM: 'true'}, stdio: 'inherit', shell: process.platform === 'win32'});").unwrap(); + let yaml_path = workspace.join("pnpm-workspace.yaml"); + let mut yaml = fs::read_to_string(&yaml_path).unwrap(); + yaml.push_str("nodeLinker:\n type: loaded\npackages:\n - packages/*\n"); + fs::write(yaml_path, yaml).unwrap(); + pacquet + .with_arg("install") + .assert() + .success(); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args([ + "exec", + "node", + "-e", + "require('node:assert/strict').equal(require('child'),42)", + ]) + .assert() + .success(); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["--filter", "child", "test"]) + .assert() + .success(); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["--config.verify-deps-before-run=error", "exec", "nested"]) + .env_remove("NODE_OPTIONS") + .assert() + .success(); + assert_no_node_modules(&workspace); + drop((root, mock_instance)); +} + +#[test] +fn frozen_cas_install_applies_opt_out_changes_and_repairs_missing_manifest() { + let CommandTempCwd { + pacquet, + root, + workspace, + npmrc_info, + .. + } = CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + fs::write( + workspace.join("package.json"), + r#"{"dependencies":{"@pnpm.e2e/pkg-with-1-dep":"100.0.0"}}"#, + ) + .unwrap(); + let yaml_path = workspace.join("pnpm-workspace.yaml"); + let mut yaml = fs::read_to_string(&yaml_path).unwrap(); + yaml.push_str("nodeLinker:\n type: loaded\n"); + fs::write(&yaml_path, &yaml).unwrap(); + pacquet + .with_arg("install") + .assert() + .success(); + for materialize in [true, false] { + let config = if materialize { + format!("{yaml} excluded:\n - '@pnpm.e2e/pkg-with-1-dep'\n") + } else { + yaml.clone() + }; + fs::write(&yaml_path, config).unwrap(); + fs::remove_file(workspace.join(".pnpm-store.json")).unwrap(); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["install", "--frozen-lockfile"]) + .assert() + .success(); + let manifest: Value = + serde_json::from_slice(&fs::read(workspace.join(".pnpm-store.json")).unwrap()).unwrap(); + assert_eq!( + manifest["packages"]["@pnpm.e2e/pkg-with-1-dep@100.0.0"]["resolution"] == "node", + materialize, + ); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["exec", "node", "-e", "require('@pnpm.e2e/pkg-with-1-dep')()"]) + .assert() + .success(); + } + for filename in [".pnpm-store.json", ".pnpm-store-loader.mjs"] { + fs::remove_file(workspace.join(filename)).unwrap(); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["exec", "node", "-e", "require('@pnpm.e2e/pkg-with-1-dep')()"]) + .env_remove("NODE_OPTIONS") + .assert() + .success(); + assert!(workspace.join(filename).is_file()); + } + assert_no_node_modules(&workspace); + drop((root, mock_instance)); +} + +#[test] +fn cas_requires_opt_out_for_builds_and_links_generated_bins_after_building() { + let CommandTempCwd { + pacquet, + root, + workspace, + npmrc_info, + .. + } = CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + fs::write( + workspace.join("package.json"), + r#"{"dependencies":{"@pnpm.e2e/has-bin-and-needs-build":"1.0.0"}}"#, + ) + .unwrap(); + let yaml_path = workspace.join("pnpm-workspace.yaml"); + let mut yaml = fs::read_to_string(&yaml_path).unwrap(); + yaml.push_str("allowBuilds:\n '@pnpm.e2e/has-bin-and-needs-build': true\n '@pnpm.e2e/pre-and-postinstall-scripts-example': true\n '@pnpm.e2e/install-script-example': true\nnodeLinker:\n type: loaded\n"); + fs::write(&yaml_path, &yaml).unwrap(); + let failure = pacquet + .with_arg("install") + .assert() + .failure(); + assert!(String::from_utf8_lossy(&failure.get_output().stderr).contains("nodeLinker.excluded")); + fs::write( + &yaml_path, + format!("{yaml} excluded:\n - '@pnpm.e2e/has-bin-and-needs-build'\n"), + ) + .unwrap(); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_arg("install") + .assert() + .success(); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["exec", "has-bin-and-needs-build"]) + .assert() + .success(); + assert_no_node_modules(&workspace); + drop((root, mock_instance)); +} + +#[test] +fn cas_remove_prunes_command_shims_and_keeps_empty_install_runnable() { + let CommandTempCwd { + pacquet, + root, + workspace, + npmrc_info, + .. + } = CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + fs::write( + workspace.join("package.json"), + r#"{"dependencies":{"@pnpm.e2e/hello-world-js-bin":"1.0.0"}}"#, + ) + .unwrap(); + let yaml_path = workspace.join("pnpm-workspace.yaml"); + let mut yaml = fs::read_to_string(&yaml_path).unwrap(); + yaml.push_str("nodeLinker:\n type: loaded\n"); + fs::write(&yaml_path, yaml).unwrap(); + pacquet + .with_arg("install") + .assert() + .success(); + assert!(workspace.join(".pnpm/.bin/hello-world-js-bin").is_file()); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["remove", "@pnpm.e2e/hello-world-js-bin"]) + .assert() + .success(); + assert!(!workspace.join(".pnpm/.bin/hello-world-js-bin").exists()); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["exec", "node", "-e", "console.log('empty install')"]) + .assert() + .success(); + assert_no_node_modules(&workspace); + drop((root, mock_instance)); +} + +#[test] +fn loaded_project_local_store_survives_prune_and_requires_registration() { + let CommandTempCwd { + pacquet, + root, + workspace, + npmrc_info, + .. + } = CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + fs::write( + workspace.join("package.json"), + r#"{"dependencies":{"@pnpm.e2e/hello-world-js-bin":"1.0.0"}}"#, + ) + .unwrap(); + let yaml_path = workspace.join("pnpm-workspace.yaml"); + let mut yaml = fs::read_to_string(&yaml_path) + .unwrap() + .lines() + .filter(|line| !line.starts_with("storeDir:")) + .collect::>() + .join("\n"); + yaml.push('\n'); + yaml.push_str("nodeLinker:\n type: loaded\nstoreDir: .pnpm-store\n"); + fs::write(yaml_path, yaml).unwrap(); + pacquet + .with_arg("install") + .assert() + .success(); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["store", "prune"]) + .assert() + .success(); + Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_args(["exec", "hello-world-js-bin"]) + .assert() + .success(); + assert_no_node_modules(&workspace); + let manifest: Value = + serde_json::from_slice(&fs::read(workspace.join(".pnpm-store.json")).unwrap()).unwrap(); + let registry = Path::new(manifest["storeDir"].as_str().unwrap()).join("projects"); + fs::remove_dir_all(®istry).unwrap(); + fs::write(®istry, "blocked registry").unwrap(); + let failure = Command::cargo_bin("pnpm") + .unwrap() + .with_current_dir(&workspace) + .with_arg("install") + .assert() + .failure(); + assert!( + String::from_utf8_lossy(&failure.get_output().stderr) + .contains("ERR_PNPM_STORE_DIR_REGISTER_PROJECT_CREATE_REGISTRY_DIR"), + ); + drop((root, mock_instance)); +} diff --git a/pnpm/crates/cli/tests/suite/config_dependencies.rs b/pnpm/crates/cli/tests/suite/config_dependencies.rs index 2e554bab82..d29ee92479 100644 --- a/pnpm/crates/cli/tests/suite/config_dependencies.rs +++ b/pnpm/crates/cli/tests/suite/config_dependencies.rs @@ -807,3 +807,29 @@ fn set_config_dependencies(workspace: &Path, config_deps: &str) { fs::write(&yaml_path, format!("{base}\nconfigDependencies:\n {config_deps}\n")) .expect("write pnpm-workspace.yaml"); } + +#[test] +fn update_config_hook_switches_loaded_linker_back_to_isolated_layout() { + let CommandTempCwd { root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + fs::write( + workspace.join("package.json"), + serde_json::json!({"dependencies": {"@pnpm.e2e/foo": "100.0.0"}}).to_string(), + ) + .unwrap(); + let yaml_path = workspace.join("pnpm-workspace.yaml"); + let mut yaml = fs::read_to_string(&yaml_path).unwrap(); + yaml.push_str("nodeLinker:\n type: loaded\n"); + fs::write(yaml_path, yaml).unwrap(); + fs::write(workspace.join(".pnpmfile.cjs"), "module.exports = { hooks: { updateConfig(config) { config.nodeLinker = 'isolated'; return config } } }").unwrap(); + pacquet_at(&workspace) + .with_arg("install") + .assert() + .success(); + assert!(workspace.join("node_modules/@pnpm.e2e/foo/package.json").is_file()); + assert!(!workspace.join(".pnpm/.modules.yaml").exists()); + let modules = fs::read_to_string(workspace.join("node_modules/.modules.yaml")).unwrap(); + assert!(!modules.contains("enableGlobalVirtualStore: true")); + drop((root, mock_instance)); +} diff --git a/pnpm/crates/cli/tests/suite/main.rs b/pnpm/crates/cli/tests/suite/main.rs index 0cb960ae63..216c924c9c 100644 --- a/pnpm/crates/cli/tests/suite/main.rs +++ b/pnpm/crates/cli/tests/suite/main.rs @@ -21,6 +21,7 @@ mod bundled_dependencies; mod cache; mod cargo_git_install; mod cargo_install; +mod cas_linker; mod cat_file; mod cat_index; mod catalog; diff --git a/pnpm/crates/cmd-shim/src/bin_resolver.rs b/pnpm/crates/cmd-shim/src/bin_resolver.rs index 3eaee0800b..b5b9af6a4c 100644 --- a/pnpm/crates/cmd-shim/src/bin_resolver.rs +++ b/pnpm/crates/cmd-shim/src/bin_resolver.rs @@ -44,9 +44,21 @@ pub fn pkg_owns_bin(bin_name: &str, pkg_name: &str) -> bool { /// rooted at `pkg_path`. /// /// An empty-string `bin` declares no command, as it does in pnpm v11. +#[must_use] pub fn get_bins_from_package_manifest( manifest: &Value, pkg_path: &Path, +) -> Vec { + get_bins_from_package_manifest_with_walk(manifest, pkg_path, |directory| { + Sys::walk_files(directory).map(Iterator::collect::>) + }) +} + +/// Resolve bins with a caller-provided recursive file walk, including virtual files. +pub fn get_bins_from_package_manifest_with_walk>( + manifest: &Value, + pkg_path: &Path, + walk_files: impl FnOnce(&Path) -> std::io::Result, ) -> Vec { let pkg_name = manifest.get("name").and_then(Value::as_str); if let Some(bin) = manifest @@ -60,7 +72,7 @@ pub fn get_bins_from_package_manifest( .and_then(|d| d.get("bin")) .and_then(Value::as_str) { - return commands_from_directories_bin::(bin_dir_rel, pkg_path); + return commands_from_directories_bin(bin_dir_rel, pkg_path, walk_files); } Vec::new() } @@ -69,9 +81,10 @@ pub fn get_bins_from_package_manifest( /// [`Command`] per file, the `directories.bin` branch of bin resolution. /// /// Symlinks are not followed. -fn commands_from_directories_bin( +fn commands_from_directories_bin>( bin_dir_rel: &str, pkg_path: &Path, + walk_files: impl FnOnce(&Path) -> std::io::Result, ) -> Vec { let bin_dir = pkg_path.join(bin_dir_rel); if !is_subdir(pkg_path, &bin_dir) { @@ -80,7 +93,7 @@ fn commands_from_directories_bin( // Treat a top-level walk error as "no bins". The trait's production // impl already drops per-entry errors inside its iterator, so an `Err` // here only fires when the walker can't even open `bin_dir`. - let Ok(paths) = Sys::walk_files(&bin_dir) else { + let Ok(paths) = walk_files(&bin_dir) else { return Vec::new(); }; let mut commands = Vec::new(); diff --git a/pnpm/crates/config/src/cas.rs b/pnpm/crates/config/src/cas.rs new file mode 100644 index 0000000000..aa9b2af348 --- /dev/null +++ b/pnpm/crates/config/src/cas.rs @@ -0,0 +1,68 @@ +pub use pnpm_store_dir::{CAS_LOADER_FILENAME, CAS_MANIFEST_FILENAME}; + +use crate::{Config, NodeLinker}; +use std::{collections::HashMap, path::Path}; + +impl Config { + pub(super) fn apply_cas_layout(&mut self) { + if self.node_linker == NodeLinker::Loaded { + self.loaded_layout_defaults.get_or_insert_with(|| { + (self.modules_dir.clone(), self.enable_global_virtual_store) + }); + self.enable_global_virtual_store = true; + if !self.explicit_settings.contains_key("modulesDir") { + self.modules_dir.set_file_name(".pnpm"); + } + } else if let Some((modules_dir, global_store)) = self + .loaded_layout_defaults + .take() + { + if !self.explicit_settings.contains_key("modulesDir") { + self.modules_dir = modules_dir; + if self.modules_dir + .file_name() + .is_some_and(|name| name == ".pnpm") + { + self.modules_dir.set_file_name("node_modules"); + } + } + self.enable_global_virtual_store = self.explicit_settings + .get("enableGlobalVirtualStore") + .and_then(serde_json::Value::as_bool) + .unwrap_or(global_store); + } + self.follow_modules_dir_with_install_state_dir(); + } + + #[must_use] + pub fn workspace_state_modules_dir(&self, root: &Path) -> std::path::PathBuf { + if self.node_linker == NodeLinker::Loaded { + self.modules_dir.clone() + } else { + root.join("node_modules") + } + } + + pub fn add_cas_loader_env(&self, project: &Path, env: &mut HashMap) { + if self.node_linker != NodeLinker::Loaded || self.virtual_store_only { + return; + } + let loader = self.lockfile_dir_for(project).join(CAS_LOADER_FILENAME); + let url = url::Url::from_file_path(&loader).expect("the install root is absolute"); + let option = format!("--import={url}"); + let previous = env + .get("NODE_OPTIONS") + .cloned() + .or_else(|| std::env::var("NODE_OPTIONS").ok()) + .unwrap_or_default(); + if !previous + .split_whitespace() + .any(|part| part == option) + { + env.insert( + "NODE_OPTIONS".to_string(), + format!("{previous} {option}").trim().to_string(), + ); + } + } +} diff --git a/pnpm/crates/config/src/env_overlay.rs b/pnpm/crates/config/src/env_overlay.rs index 425c77a1d4..9302bdc554 100644 --- a/pnpm/crates/config/src/env_overlay.rs +++ b/pnpm/crates/config/src/env_overlay.rs @@ -15,7 +15,7 @@ //! as a low-priority auth-file fallback. use crate::{ - AuditLevel, CatalogMode, ColorMode, HoistingLimits, InitType, LogLevel, NodeLinker, + AuditLevel, CatalogMode, ColorMode, HoistingLimits, InitType, LogLevel, NodeLinkerSetting, NodePackageMapType, PackageImportMethod, PmOnFail, ReporterType, ResolutionMode, RuntimeOnFail, SaveWorkspaceProtocol, ScriptsPrependNodePath, TrustPolicy, VerifyDepsBeforeRun, VirtualStoreType, WorkspaceSettings, api::EnvVar, @@ -182,7 +182,7 @@ impl WorkspaceSettings { string_field!(settings, reader, store_dir, "STORE_DIR"); string_field!(settings, reader, state_dir, "STATE_DIR"); string_field!(settings, reader, modules_dir, "MODULES_DIR"); - enum_field!(settings, reader, node_linker, "NODE_LINKER", NodeLinker); + enum_field!(settings, reader, node_linker, "NODE_LINKER", NodeLinkerSetting); json_field!( settings, reader, diff --git a/pnpm/crates/config/src/layout.rs b/pnpm/crates/config/src/layout.rs index 5a463d2ec0..004c4edc8a 100644 --- a/pnpm/crates/config/src/layout.rs +++ b/pnpm/crates/config/src/layout.rs @@ -33,6 +33,7 @@ impl Config { virtual_store_dir_explicit: bool, global_virtual_store_dir_explicit: bool, ) { + self.apply_cas_layout(); if !global_virtual_store_dir_explicit { self.global_virtual_store_dir = match &self.configured_virtual_store_dir { Some(dir) if self.enable_global_virtual_store && virtual_store_dir_explicit => { @@ -148,7 +149,11 @@ impl Config { self.modules_dir = match self.explicit_settings.get("modulesDir").and_then(serde_json::Value::as_str) { Some(raw) => dir.join(raw), - None => dir.join("node_modules"), + None => dir.join(if self.node_linker == NodeLinker::Loaded { + ".pnpm" + } else { + "node_modules" + }), }; match self.explicit_settings.get("virtualStoreDir").and_then(serde_json::Value::as_str) { Some(raw) if !self.enable_global_virtual_store => { @@ -277,7 +282,11 @@ impl Config { let raw = self.explicit_settings .get("modulesDir") .and_then(serde_json::Value::as_str) - .unwrap_or("node_modules"); + .unwrap_or(if self.node_linker == NodeLinker::Loaded { + ".pnpm" + } else { + "node_modules" + }); project_dir.join(raw) }); pnpm_fs::lexical_normalize(&modules_dir) diff --git a/pnpm/crates/config/src/lib.rs b/pnpm/crates/config/src/lib.rs index 17b1b92058..67e51101d2 100644 --- a/pnpm/crates/config/src/lib.rs +++ b/pnpm/crates/config/src/lib.rs @@ -28,6 +28,8 @@ pub use crate::{ UnexpandedWindowsEnvVar, ensure_windows_dir_envs, ensure_windows_home_dir_env, }, }; +pub use cas::{CAS_LOADER_FILENAME, CAS_MANIFEST_FILENAME}; +pub use node_linker::{NodeLinkerOptions, NodeLinkerSetting}; pub use pnpm_matcher as matcher; pub use setting_types::{ AuditConfig, AuditLevel, CatalogMode, ColorMode, HoistingLimits, InitType, @@ -55,10 +57,12 @@ pub use workspace_yaml::{ }; mod api; +mod cas; mod ci_detection; mod defaults; mod env_overlay; mod global_bin_check; +mod node_linker; mod npmrc_auth; mod override_version_references; mod store_path; diff --git a/pnpm/crates/config/src/node_linker.rs b/pnpm/crates/config/src/node_linker.rs new file mode 100644 index 0000000000..2bf88dddcb --- /dev/null +++ b/pnpm/crates/config/src/node_linker.rs @@ -0,0 +1,49 @@ +use crate::{Config, NodeLinker}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum NodeLinkerSetting { + Type(NodeLinker), + Options(NodeLinkerOptions), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case", deny_unknown_fields)] +pub enum NodeLinkerOptions { + Loaded { + #[serde(default)] + excluded: Vec, + }, +} + +impl From for NodeLinkerSetting { + fn from(linker: NodeLinker) -> Self { + Self::Type(linker) + } +} + +impl NodeLinkerSetting { + /// Replace the selected linker and its exclusions together. + pub fn apply_to(self, config: &mut Config) { + let (linker, excluded) = match self { + Self::Type(linker) => (linker, Vec::new()), + Self::Options(NodeLinkerOptions::Loaded { excluded }) => (NodeLinker::Loaded, excluded), + }; + config.node_linker = linker; + config.node_linker_excluded = excluded; + } + + pub(crate) fn from_resolved(config: &Config) -> Self { + if config.node_linker == NodeLinker::Loaded { + Self::Options(NodeLinkerOptions::Loaded { + excluded: config.node_linker_excluded.clone(), + }) + } else { + Self::Type(config.node_linker) + } + } +} + +#[cfg(test)] +mod tests; diff --git a/pnpm/crates/config/src/node_linker/tests.rs b/pnpm/crates/config/src/node_linker/tests.rs new file mode 100644 index 0000000000..6046417ead --- /dev/null +++ b/pnpm/crates/config/src/node_linker/tests.rs @@ -0,0 +1,135 @@ +use crate::{Config, NodeLinker, WorkspaceSettings, api::EnvVar}; +use serde_json::json; +use std::{collections::BTreeMap, path::Path}; + +#[test] +fn loaded_object_applies_exclusions_and_round_trips_through_resolved_settings() { + let settings: WorkspaceSettings = serde_saphyr::from_str( + "nodeLinker:\n type: loaded\n excluded:\n - vitest\n - esbuild\n", + ) + .unwrap(); + let mut config = Config::default(); + settings.apply_to(&mut config, Path::new(".")); + assert_eq!(config.node_linker, NodeLinker::Loaded); + assert_eq!(config.node_linker_excluded, ["vitest", "esbuild"]); + let reported = serde_json::to_value(WorkspaceSettings::from_resolved(&config)).unwrap(); + assert_eq!( + reported["nodeLinker"], + json!({"type": "loaded", "excluded": ["vitest", "esbuild"]}), + ); + assert!(reported.get("casMaterialize").is_none()); + let mut restored = Config::default(); + serde_json::from_value::(reported) + .unwrap() + .apply_to(&mut restored, Path::new(".")); + assert_eq!(restored.node_linker, config.node_linker); + assert_eq!(restored.node_linker_excluded, config.node_linker_excluded); +} + +#[test] +fn scalar_linkers_and_loaded_object_without_exclusions_are_supported() { + for (value, expected) in [ + (json!("isolated"), NodeLinker::Isolated), + (json!("hoisted"), NodeLinker::Hoisted), + (json!("pnp"), NodeLinker::Pnp), + (json!("loaded"), NodeLinker::Loaded), + (json!({"type": "loaded"}), NodeLinker::Loaded), + ] { + let settings: WorkspaceSettings = + serde_json::from_value(json!({"nodeLinker": value})).unwrap(); + let mut config = Config::default(); + settings.apply_to(&mut config, Path::new(".")); + assert_eq!(config.node_linker, expected); + assert!(config.node_linker_excluded.is_empty()); + } +} + +#[test] +fn node_linker_objects_reject_invalid_and_not_yet_supported_options() { + for value in [ + json!({"excluded": ["vitest"]}), + json!({"type": "unknown"}), + json!({"type": "loaded", "exclude": ["vitest"]}), + json!({"type": "loaded", "excluded": "vitest"}), + json!({"type": "loaded", "excluded": [false]}), + json!({"type": "loaded", "hoist": false}), + json!({"type": "isolated", "hoist": {"public": []}}), + ] { + assert!(serde_json::from_value::(json!({"nodeLinker": value})).is_err()); + } +} + +#[test] +fn higher_priority_linker_values_replace_the_type_and_exclusions_together() { + let mut config = Config::default(); + for (value, expected, excluded) in [ + (r#"{"type":"loaded","excluded":["vitest"]}"#, NodeLinker::Loaded, vec!["vitest"]), + (r#"{"type":"loaded","excluded":["esbuild"]}"#, NodeLinker::Loaded, vec!["esbuild"]), + (r#"{"type":"loaded","excluded":[]}"#, NodeLinker::Loaded, vec![]), + (r#"{"type":"loaded","excluded":["vitest"]}"#, NodeLinker::Loaded, vec!["vitest"]), + ("hoisted", NodeLinker::Hoisted, vec![]), + ("loaded", NodeLinker::Loaded, vec![]), + ] { + WorkspaceSettings::from_string_values(&BTreeMap::from([( + "node-linker".to_string(), + value.to_string(), + )])) + .apply_to(&mut config, Path::new(".")); + assert_eq!(config.node_linker, expected); + assert_eq!(config.node_linker_excluded, excluded); + } +} + +#[test] +fn environment_accepts_loaded_object_and_reset_clears_exclusions() { + struct Environment; + impl EnvVar for Environment { + fn var(name: &str) -> Option { + (name == "PNPM_CONFIG_NODE_LINKER").then(|| { + r#"{"type":"loaded","excluded":["vitest"]}"#.to_string() + }) + } + } + let defaults = Config::default(); + let mut config = Config::default(); + WorkspaceSettings::from_pnpm_config_env::().apply_to(&mut config, Path::new(".")); + assert_eq!(config.node_linker, NodeLinker::Loaded); + assert_eq!(config.node_linker_excluded, ["vitest"]); + assert!(WorkspaceSettings::reset_setting_to_default::( + &mut config, + &defaults, + "nodeLinker", + Path::new("."), + )); + assert_eq!(config.node_linker, defaults.node_linker); + assert!(config.node_linker_excluded.is_empty()); +} + +#[test] +fn switching_loaded_linker_restores_layout_defaults_and_preserves_explicit_settings() { + for explicit in [false, true] { + let original = + Path::new("/project").join(if explicit { "custom-modules" } else { "node_modules" }); + let mut config = Config { + modules_dir: original.clone(), + enable_global_virtual_store: explicit, + ..Config::default() + }; + if explicit { + config.explicit_settings.insert("modulesDir".into(), json!("custom-modules")); + config.explicit_settings.insert("enableGlobalVirtualStore".into(), json!(true)); + } + config.node_linker = NodeLinker::Loaded; + config.apply_global_virtual_store_derivation(false, false); + assert!(config.enable_global_virtual_store); + assert_eq!( + config.modules_dir, + if explicit { original.clone() } else { Path::new("/project").join(".pnpm") }, + ); + config.node_linker = NodeLinker::Isolated; + config.apply_global_virtual_store_derivation(false, false); + assert_eq!(config.modules_dir, original); + assert_eq!(config.enable_global_virtual_store, explicit); + assert_eq!(config.install_state_dir, original.join(".pnpm")); + } +} diff --git a/pnpm/crates/config/src/pnpm_default_parity.rs b/pnpm/crates/config/src/pnpm_default_parity.rs index 4d7d4b714e..657a4a3862 100644 --- a/pnpm/crates/config/src/pnpm_default_parity.rs +++ b/pnpm/crates/config/src/pnpm_default_parity.rs @@ -235,6 +235,7 @@ fn node_linker_scalar(value: NodeLinker) -> Scalar { NodeLinker::Isolated => s("isolated"), NodeLinker::Hoisted => s("hoisted"), NodeLinker::Pnp => s("pnp"), + NodeLinker::Loaded => s("loaded"), } } diff --git a/pnpm/crates/config/src/setting_types.rs b/pnpm/crates/config/src/setting_types.rs index bcf980c7a4..6af5b0b4ca 100644 --- a/pnpm/crates/config/src/setting_types.rs +++ b/pnpm/crates/config/src/setting_types.rs @@ -111,6 +111,7 @@ pub enum NodeLinker { /// Yarn Berry. It is recommended to also set symlink setting to false when using pnp as /// your linker. Pnp, + Loaded, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] diff --git a/pnpm/crates/config/src/settings.rs b/pnpm/crates/config/src/settings.rs index 1fe3499414..564b0226c6 100644 --- a/pnpm/crates/config/src/settings.rs +++ b/pnpm/crates/config/src/settings.rs @@ -285,6 +285,9 @@ pub struct Config { /// Defines what linker should be used for installing Node packages. pub node_linker: NodeLinker, + /// Package names excluded from loaded linking; their complete trees use the global virtual store. + pub node_linker_excluded: Vec, + /// When true, pacquet writes `node_modules/.package-map.json` for /// Node's `--experimental-package-map` loader flag. Default /// `false`, matching pnpm's opt-in setting. @@ -322,6 +325,9 @@ pub struct Config { #[default(_code = "default_enable_global_virtual_store()")] pub enable_global_virtual_store: bool, + /// Layout defaults restored when a hook switches away from the loaded linker. + pub loaded_layout_defaults: Option<(PathBuf, bool)>, + /// The shared package store used when [`Self::enable_global_virtual_store`] /// is on. Derived by [`Self::apply_global_virtual_store_derivation`] from /// `globalVirtualStoreDir`, `virtualStoreDir`, or `/links`. diff --git a/pnpm/crates/config/src/workspace_yaml.rs b/pnpm/crates/config/src/workspace_yaml.rs index 884f4e5a84..852b9d5378 100644 --- a/pnpm/crates/config/src/workspace_yaml.rs +++ b/pnpm/crates/config/src/workspace_yaml.rs @@ -14,8 +14,8 @@ pub use settings::{MacosBackupSettings, WorkspaceSettings}; use crate::{ AuditConfig, AuditLevel, CatalogMode, Config, HoistingLimits, InitType, LinkWorkspacePackages, - NodeLinker, NodePackageMapType, PackageImportMethod, PmOnFail, ResolutionMode, RuntimeOnFail, - SaveWorkspaceProtocol, ScriptsPrependNodePath, TrustPolicy, VerifyDepsBeforeRun, + NodeLinkerSetting, NodePackageMapType, PackageImportMethod, PmOnFail, ResolutionMode, + RuntimeOnFail, SaveWorkspaceProtocol, ScriptsPrependNodePath, TrustPolicy, VerifyDepsBeforeRun, VirtualStoreType, api::{EnvVar, GetCurrentDir, GetHomeDir, LinkProbe}, config_types::is_config_file_key, @@ -212,7 +212,7 @@ macro_rules! identically_named_settings { stream, aggregate_output, use_stderr, ignore_workspace, shell_emulator, skip_manifest_obfuscation, sort, use_beta_cli, hoist, shamefully_hoist, - node_linker, node_experimental_package_map, node_package_map_type, + node_experimental_package_map, node_package_map_type, symlink, package_import_method, modules_cache_max_age, virtual_store_dir_max_length, peers_suffix_max_length, diff --git a/pnpm/crates/config/src/workspace_yaml/apply.rs b/pnpm/crates/config/src/workspace_yaml/apply.rs index 24e7260ee8..be5cf8b9e4 100644 --- a/pnpm/crates/config/src/workspace_yaml/apply.rs +++ b/pnpm/crates/config/src/workspace_yaml/apply.rs @@ -123,6 +123,10 @@ impl WorkspaceSettings { overlay_some(&mut config.save_prefix, self.save_prefix.take()); overlay(&mut config.tag_version_prefix, self.tag_version_prefix.take()); + if let Some(node_linker) = self.node_linker.take() { + node_linker.apply_to(config); + } + overlay(&mut config.hoist_pattern, self.hoist_pattern.take()); overlay(&mut config.public_hoist_pattern, self.public_hoist_pattern.take()); diff --git a/pnpm/crates/config/src/workspace_yaml/reset.rs b/pnpm/crates/config/src/workspace_yaml/reset.rs index f1b32520d7..2b2655c6fb 100644 --- a/pnpm/crates/config/src/workspace_yaml/reset.rs +++ b/pnpm/crates/config/src/workspace_yaml/reset.rs @@ -190,6 +190,7 @@ impl WorkspaceSettings { "publicHoistPattern" => reset_public_hoist_pattern(config, defaults), "nodeLinker" => { config.node_linker = defaults.node_linker; + config.node_linker_excluded.clone_from(&defaults.node_linker_excluded); config.apply_prefer_symlinked_executables_derivation(); } "preferSymlinkedExecutables" => { diff --git a/pnpm/crates/config/src/workspace_yaml/resolved.rs b/pnpm/crates/config/src/workspace_yaml/resolved.rs index e7c71199d1..7060fa8cf2 100644 --- a/pnpm/crates/config/src/workspace_yaml/resolved.rs +++ b/pnpm/crates/config/src/workspace_yaml/resolved.rs @@ -39,6 +39,7 @@ impl WorkspaceSettings { } let mut settings = identically_named_settings!(read); + settings.node_linker = Some(crate::NodeLinkerSetting::from_resolved(config)); settings = settings.with_resolved_paths(config); settings = settings.with_resolved_presence(config); settings = settings.with_resolved_scripts(config); diff --git a/pnpm/crates/config/src/workspace_yaml/settings.rs b/pnpm/crates/config/src/workspace_yaml/settings.rs index efeed1ef76..b83674586a 100644 --- a/pnpm/crates/config/src/workspace_yaml/settings.rs +++ b/pnpm/crates/config/src/workspace_yaml/settings.rs @@ -2,9 +2,9 @@ use super::{ AllowBuild, AuditConfig, AuditLevel, AuditSettings, BTreeMap, BTreeSet, CargoSettings, CatalogMode, ConfigDependency, Deserialize, Deserializer, DroppedKeys, EnvVar, ErrorKind, GLOBAL_CONFIG_YAML_FILENAME, HashMap, HoistingLimits, IgnoredAny, IndexMap, InitType, - LinkWorkspacePackages, LoadWorkspaceYamlError, LockfileSetting, NodeLinker, NodePackageMapType, - PackageConfigsSetting, PackageExtension, PackageImportMethod, Path, PathBuf, - PeerDependencyRules, Pipe, Placeholder, PmOnFail, PnpmfileSetting, PythonSettings, + LinkWorkspacePackages, LoadWorkspaceYamlError, LockfileSetting, NodeLinkerSetting, + NodePackageMapType, PackageConfigsSetting, PackageExtension, PackageImportMethod, Path, + PathBuf, PeerDependencyRules, Pipe, Placeholder, PmOnFail, PnpmfileSetting, PythonSettings, RegistryEntry, RemoteSideEffectsCacheSettings, ResolutionMode, RuntimeOnFail, SCHEMA_DIRECTIVE_KEY, SaveWorkspaceProtocol, ScriptsPrependNodePath, SideEffectsCacheSetting, SupportedArchitectures, SystemEnv, TaskSettings, Tool, ToolSettings, TrustPolicy, UpdateConfig, @@ -220,7 +220,7 @@ pub struct WorkspaceSettings { pub macos_backup: Option, pub state_dir: Option, pub modules_dir: Option, - pub node_linker: Option, + pub node_linker: Option, pub node_experimental_package_map: Option, pub node_package_map_type: Option, pub symlink: Option, diff --git a/pnpm/crates/config/src/workspace_yaml/tests/configuration_parses_common_settings_from.rs b/pnpm/crates/config/src/workspace_yaml/tests/configuration_parses_common_settings_from.rs index 365b2fcd07..3a9426bad2 100644 --- a/pnpm/crates/config/src/workspace_yaml/tests/configuration_parses_common_settings_from.rs +++ b/pnpm/crates/config/src/workspace_yaml/tests/configuration_parses_common_settings_from.rs @@ -30,7 +30,7 @@ packages: assert_eq!(settings.dedupe_peers, Some(true)); assert_eq!(settings.auto_dedupe, Some(true)); assert_eq!(settings.prefer_workspace_packages, Some(true)); - assert!(matches!(settings.node_linker, Some(NodeLinker::Hoisted))); + assert_eq!(settings.node_linker, Some(NodeLinker::Hoisted.into())); assert_eq!(settings.node_experimental_package_map, Some(true)); assert_eq!(settings.node_package_map_type, Some(NodePackageMapType::Loose)); } diff --git a/pnpm/crates/config/src/workspace_yaml/tests/env_placeholders.rs b/pnpm/crates/config/src/workspace_yaml/tests/env_placeholders.rs index b7734c2a02..597a6c977b 100644 --- a/pnpm/crates/config/src/workspace_yaml/tests/env_placeholders.rs +++ b/pnpm/crates/config/src/workspace_yaml/tests/env_placeholders.rs @@ -21,14 +21,14 @@ impl EnvVar for Env { fn a_fallback_names_an_enum_variant() { let settings = parse_settings::("nodeLinker: ${PNPM_TEST_UNSET:-isolated}\n").unwrap(); - assert_eq!(settings.node_linker, Some(NodeLinker::Isolated)); + assert_eq!(settings.node_linker, Some(NodeLinker::Isolated.into())); } #[test] fn an_environment_value_wins_over_the_fallback() { let settings = parse_settings::("nodeLinker: ${PNPM_TEST_LINKER:-isolated}\n").unwrap(); - assert_eq!(settings.node_linker, Some(NodeLinker::Hoisted)); + assert_eq!(settings.node_linker, Some(NodeLinker::Hoisted.into())); } #[test] @@ -169,7 +169,7 @@ fn placeholders_the_document_reads_without_do_not_crowd_out_the_one_that_counts( parse_settings::(&format!("nodeLinker: ${{PNPM_TEST_UNSET:-isolated}}\n{padding}")) .unwrap(); - assert_eq!(settings.node_linker, Some(NodeLinker::Isolated)); + assert_eq!(settings.node_linker, Some(NodeLinker::Isolated.into())); } /// The second read must not turn a quoted scalar into the value it spells, diff --git a/pnpm/crates/config/src/workspace_yaml/tests/files.rs b/pnpm/crates/config/src/workspace_yaml/tests/files.rs index 6ec124ece6..e127a10c08 100644 --- a/pnpm/crates/config/src/workspace_yaml/tests/files.rs +++ b/pnpm/crates/config/src/workspace_yaml/tests/files.rs @@ -202,7 +202,7 @@ fn load_at_expands_env_placeholders_in_typed_fields() { assert_eq!(settings.color, Some(ColorMode::Auto)); assert_eq!(settings.global_shims, Some(GlobalShimsSetting::Toggle(false))); - assert_eq!(settings.node_linker, Some(NodeLinker::Isolated)); + assert_eq!(settings.node_linker, Some(NodeLinker::Isolated.into())); assert_eq!(settings.side_effects_cache, Some(SideEffectsCacheSetting::Enabled(false))); } diff --git a/pnpm/crates/deps-restorer/Cargo.toml b/pnpm/crates/deps-restorer/Cargo.toml index 7c0eddd22d..8310d2ded6 100644 --- a/pnpm/crates/deps-restorer/Cargo.toml +++ b/pnpm/crates/deps-restorer/Cargo.toml @@ -60,6 +60,8 @@ tempfile = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +url = { workspace = true } + [dev-dependencies] pnpm-testing-utils = { workspace = true } diff --git a/pnpm/crates/deps-restorer/src/cas-loader.mjs.inc b/pnpm/crates/deps-restorer/src/cas-loader.mjs.inc new file mode 100644 index 0000000000..b75cadded1 --- /dev/null +++ b/pnpm/crates/deps-restorer/src/cas-loader.mjs.inc @@ -0,0 +1,139 @@ +import { createRequire as bootstrapRequire } from 'node:module'; const require = bootstrapRequire(import.meta.url); +var Is=Object.create;var Qt=Object.defineProperty;var Ws=Object.getOwnPropertyDescriptor;var Us=Object.getOwnPropertyNames;var Bs=Object.getPrototypeOf,qs=Object.prototype.hasOwnProperty;var b=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,n)=>(typeof require<"u"?require:e)[n]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var w=(t,e)=>()=>{try{return e||t((e={exports:{}}).exports,e),e.exports}catch(n){throw e=0,n}};var bs=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of Us(e))!qs.call(t,r)&&r!==n&&Qt(t,r,{get:()=>e[r],enumerable:!(i=Ws(e,r))||i.enumerable});return t};var Js=(t,e,n)=>(n=t!=null?Is(Bs(t)):{},bs(e||!t||!t.__esModule?Qt(n,"default",{value:t,enumerable:!0}):n,t));var Ye=w((Hu,Xt)=>{"use strict";var zs=t=>{let e=!1,n;return()=>(e||(n=t(),e=!0,t=void 0),n)};Xt.exports=zs});var cn=w((Lu,on)=>{"use strict";var{nextTick:rn}=b("process"),Zt=t=>{let e=t.length-1;for(;e>=0;){let n=t.charCodeAt(e);if(n===47||n===92)break;e--}return e<0?"":t.slice(0,e)},Ce=(t,e,n)=>{if(t.length===1){t[0](e,n),t.length=0;return}let i;for(let r of t)try{r(e,n)}catch(s){i||(i=s)}if(t.length=0,i)throw i},re,he=null,pe,ce=[],K=0,Vt=[],en=1024,sn=()=>{let t=re,e=he,n=pe;if(re=void 0,he=null,pe=void 0,K===0){t(e,n);return}let i=ce,r=K;ce=Vt,K=0;let s=-3;try{for(t(e,n),s=0;sen&&(i.length=en),Vt=i}},tn=(t,e,n)=>{if(re===void 0)re=t,he=e,pe=n,rn(sn);else{let i=ce;i[K]=t,i[K+1]=e,i[K+2]=n,K+=3}},Ge=class{constructor(e,n,i){this._provider=e,this._syncProvider=n,this._providerContext=i,this._activeAsyncOperations=new Map,this.provide=this._provider?(r,s,o)=>{if(typeof s=="function"&&(o=s,s=void 0),typeof r!="string"&&!Buffer.isBuffer(r)&&!(r instanceof URL)&&typeof r!="number"){o(new TypeError("path must be a string, Buffer, URL or number"));return}if(s)return this._provider.call(this._providerContext,r,s,o);let c=this._activeAsyncOperations.get(r);if(c){c.push(o);return}this._activeAsyncOperations.set(r,c=[o]),e(r,(l,f)=>{this._activeAsyncOperations.delete(r),Ce(c,l,f)})}:null,this.provideSync=this._syncProvider?(r,s)=>this._syncProvider.call(this._providerContext,r,s):null}purge(){}purgeParent(){}},ie=0,Re=1,nn=2,Ke=class{constructor(e,n,i,r){this._duration=e,this._provider=n,this._syncProvider=i,this._providerContext=r,this._activeAsyncOperations=new Map,this._data=new Map,this._levels=[];for(let s=0;s<10;s++)this._levels.push(new Set);if(e!==1/0)for(let s=5e3;s{this._activeAsyncOperations.delete(r),this._storeResult(r,c,l),this._enterAsyncMode(),Ce(o,c,l)})}provideSync(e,n){if(typeof e!="string"&&!Buffer.isBuffer(e)&&!(e instanceof URL)&&typeof e!="number")throw new TypeError("path must be a string");let i=typeof e!="string"?e.toString():e;if(n)return this._syncProvider.call(this._providerContext,e,n);this._mode===Re&&this._runDecays();let r=this._data.get(i);if(r!==void 0){if(r.err)throw r.err;return r.result}let s=this._activeAsyncOperations.get(i);this._activeAsyncOperations.delete(i);let o;try{o=this._syncProvider.call(this._providerContext,e)}catch(c){throw this._storeResult(i,c,void 0),this._enterSyncModeWhenIdle(),s&&Ce(s,c,void 0),c}return this._storeResult(i,null,o),this._enterSyncModeWhenIdle(),s&&Ce(s,null,o),o}purge(e,n){if(e==null){if(this._mode!==ie){this._data.clear();for(let r of this._levels)r.clear();this._enterIdleMode()}return}if(n!=null&&n.exact===!0){if(typeof e=="string"||Buffer.isBuffer(e)||e instanceof URL||typeof e=="number"){let r=typeof e!="string"?e.toString():e,s=this._data.get(r);s!==void 0&&(this._data.delete(r),s.level.delete(r))}else for(let r of e){let s=typeof r!="string"?r.toString():r,o=this._data.get(s);o!==void 0&&(this._data.delete(s),o.level.delete(s))}this._data.size===0&&this._enterIdleMode();return}if(typeof e=="string"||Buffer.isBuffer(e)||e instanceof URL||typeof e=="number"){let r=typeof e!="string"?e.toString():e;if(r===""){if(this._mode!==ie){this._data.clear();for(let s of this._levels)s.clear();this._enterIdleMode()}return}for(let[s,o]of this._data)s.startsWith(r)&&(this._data.delete(s),o.level.delete(s));this._data.size===0&&this._enterIdleMode()}else{for(let[r,s]of this._data)for(let o of e){let c=typeof o!="string"?o.toString():o;if(r.startsWith(c)){this._data.delete(r),s.level.delete(r);break}}this._data.size===0&&this._enterIdleMode()}}purgeParent(e){if(e==null)this.purge();else if(typeof e=="string"||Buffer.isBuffer(e)||e instanceof URL||typeof e=="number"){let n=typeof e!="string"?e.toString():e;this.purge(Zt(n))}else{let n=new Set;for(let i of e){let r=typeof i!="string"?i.toString():i;n.add(Zt(r))}this.purge(n)}}_storeResult(e,n,i){if(this._data.has(e))return;let r=this._levels[this._currentLevel];this._data.set(e,{err:n,result:i,level:r}),r.add(e)}_decayLevel(){let e=(this._currentLevel+1)%this._levels.length,n=this._levels[e];this._currentLevel=e;for(let i of n)this._data.delete(i);n.clear(),this._data.size===0?this._enterIdleMode():this._nextDecay+=this._tickInterval}_runDecays(){for(;this._nextDecay<=Date.now()&&this._mode!==ie;)this._decayLevel()}_enterAsyncMode(){let e=0;switch(this._mode){case nn:return;case ie:this._nextDecay=Date.now()+this._tickInterval,e=this._tickInterval;break;case Re:if(this._runDecays(),this._mode===ie)return;e=Math.max(0,this._nextDecay-Date.now());break}if(this._mode=nn,this._duration===1/0)return;let n=setTimeout(()=>{this._mode=Re,this._runDecays()},e);n.unref&&n.unref(),this._timeout=n}_enterSyncModeWhenIdle(){this._mode===ie&&(this._mode=Re,this._nextDecay=Date.now()+this._tickInterval)}_enterIdleMode(){this._mode=ie,this._nextDecay=void 0,this._timeout&&clearTimeout(this._timeout)}},oe=(t,e,n,i)=>t>0?new Ke(t,e,n,i):new Ge(e,n,i);on.exports=class{constructor(e,n){this.fileSystem=e,this._lstatBackend=oe(n,this.fileSystem.lstat,this.fileSystem.lstatSync,this.fileSystem);let i=this._lstatBackend.provide;this.lstat=i;let r=this._lstatBackend.provideSync;this.lstatSync=r,this._statBackend=oe(n,this.fileSystem.stat,this.fileSystem.statSync,this.fileSystem);let s=this._statBackend.provide;this.stat=s;let o=this._statBackend.provideSync;this.statSync=o,this._readdirBackend=oe(n,this.fileSystem.readdir,this.fileSystem.readdirSync,this.fileSystem);let c=this._readdirBackend.provide;this.readdir=c;let l=this._readdirBackend.provideSync;this.readdirSync=l,this._readFileBackend=oe(n,this.fileSystem.readFile,this.fileSystem.readFileSync,this.fileSystem);let f=this._readFileBackend.provide;this.readFile=f;let u=this._readFileBackend.provideSync;this.readFileSync=u,this._readJsonBackend=oe(n,this.fileSystem.readJson||this.readFile&&((_,A)=>{this.readFile(_,(P,E)=>{if(P)return A(P);if(!E||E.length===0)return A(new Error("No file content"));let x;try{x=JSON.parse(E.toString("utf8"))}catch(W){return A(W)}A(null,x)})}),this.fileSystem.readJsonSync||this.readFileSync&&(_=>{let A=this.readFileSync(_);return JSON.parse(A.toString("utf8"))}),this.fileSystem);let a=this._readJsonBackend.provide;this.readJson=a;let d=this._readJsonBackend.provideSync;this.readJsonSync=d,this._readlinkBackend=oe(n,this.fileSystem.readlink,this.fileSystem.readlinkSync,this.fileSystem);let h=this._readlinkBackend.provide;this.readlink=h;let p=this._readlinkBackend.provideSync;this.readlinkSync=p,this._realpathBackend=oe(n,this.fileSystem.realpath,this.fileSystem.realpathSync,this.fileSystem);let g=this._realpathBackend.provide;this.realpath=g;let y=this._realpathBackend.provideSync;this.realpathSync=y}purge(e,n){this._statBackend.purge(e,n),this._lstatBackend.purge(e,n),n!=null&&n.exact===!0?this._readdirBackend.purge(e,n):this._readdirBackend.purgeParent(e),this._readFileBackend.purge(e,n),this._readlinkBackend.purge(e,n),this._readJsonBackend.purge(e,n),this._realpathBackend.purge(e,n)}}});var un=w((ju,ln)=>{var se=b("constants"),Ys=process.cwd,Le=null,Gs=process.env.GRACEFUL_FS_PLATFORM||process.platform;process.cwd=function(){return Le||(Le=Ys.call(process)),Le};try{process.cwd()}catch{}typeof process.chdir=="function"&&(Qe=process.chdir,process.chdir=function(t){Le=null,Qe.call(process,t)},Object.setPrototypeOf&&Object.setPrototypeOf(process.chdir,Qe));var Qe;ln.exports=Ks;function Ks(t){se.hasOwnProperty("O_SYMLINK")&&process.version.match(/^v0\.6\.[0-2]|^v0\.5\./)&&e(t),t.lutimes||n(t),t.chown=s(t.chown),t.fchown=s(t.fchown),t.lchown=s(t.lchown),t.chmod=i(t.chmod),t.fchmod=i(t.fchmod),t.lchmod=i(t.lchmod),t.chownSync=o(t.chownSync),t.fchownSync=o(t.fchownSync),t.lchownSync=o(t.lchownSync),t.chmodSync=r(t.chmodSync),t.fchmodSync=r(t.fchmodSync),t.lchmodSync=r(t.lchmodSync),t.stat=c(t.stat),t.fstat=c(t.fstat),t.lstat=c(t.lstat),t.statSync=l(t.statSync),t.fstatSync=l(t.fstatSync),t.lstatSync=l(t.lstatSync),t.chmod&&!t.lchmod&&(t.lchmod=function(u,a,d){d&&process.nextTick(d)},t.lchmodSync=function(){}),t.chown&&!t.lchown&&(t.lchown=function(u,a,d,h){h&&process.nextTick(h)},t.lchownSync=function(){}),Gs==="win32"&&(t.rename=typeof t.rename!="function"?t.rename:(function(u){function a(d,h,p){var g=Date.now(),y=0;u(d,h,function _(A){if(A&&(A.code==="EACCES"||A.code==="EPERM"||A.code==="EBUSY")&&Date.now()-g<6e4){setTimeout(function(){t.stat(h,function(P,E){P&&P.code==="ENOENT"?u(d,h,_):p(A)})},y),y<100&&(y+=10);return}p&&p(A)})}return Object.setPrototypeOf&&Object.setPrototypeOf(a,u),a})(t.rename)),t.read=typeof t.read!="function"?t.read:(function(u){function a(d,h,p,g,y,_){var A;if(_&&typeof _=="function"){var P=0;A=function(E,x,W){if(E&&E.code==="EAGAIN"&&P<10)return P++,u.call(t,d,h,p,g,y,A);_.apply(this,arguments)}}return u.call(t,d,h,p,g,y,A)}return Object.setPrototypeOf&&Object.setPrototypeOf(a,u),a})(t.read),t.readSync=typeof t.readSync!="function"?t.readSync:(function(u){return function(a,d,h,p,g){for(var y=0;;)try{return u.call(t,a,d,h,p,g)}catch(_){if(_.code==="EAGAIN"&&y<10){y++;continue}throw _}}})(t.readSync);function e(u){u.lchmod=function(a,d,h){u.open(a,se.O_WRONLY|se.O_SYMLINK,d,function(p,g){if(p){h&&h(p);return}u.fchmod(g,d,function(y){u.close(g,function(_){h&&h(y||_)})})})},u.lchmodSync=function(a,d){var h=u.openSync(a,se.O_WRONLY|se.O_SYMLINK,d),p=!0,g;try{g=u.fchmodSync(h,d),p=!1}finally{if(p)try{u.closeSync(h)}catch{}else u.closeSync(h)}return g}}function n(u){se.hasOwnProperty("O_SYMLINK")&&u.futimes?(u.lutimes=function(a,d,h,p){u.open(a,se.O_SYMLINK,function(g,y){if(g){p&&p(g);return}u.futimes(y,d,h,function(_){u.close(y,function(A){p&&p(_||A)})})})},u.lutimesSync=function(a,d,h){var p=u.openSync(a,se.O_SYMLINK),g,y=!0;try{g=u.futimesSync(p,d,h),y=!1}finally{if(y)try{u.closeSync(p)}catch{}else u.closeSync(p)}return g}):u.futimes&&(u.lutimes=function(a,d,h,p){p&&process.nextTick(p)},u.lutimesSync=function(){})}function i(u){return u&&function(a,d,h){return u.call(t,a,d,function(p){f(p)&&(p=null),h&&h.apply(this,arguments)})}}function r(u){return u&&function(a,d){try{return u.call(t,a,d)}catch(h){if(!f(h))throw h}}}function s(u){return u&&function(a,d,h,p){return u.call(t,a,d,h,function(g){f(g)&&(g=null),p&&p.apply(this,arguments)})}}function o(u){return u&&function(a,d,h){try{return u.call(t,a,d,h)}catch(p){if(!f(p))throw p}}}function c(u){return u&&function(a,d,h){typeof d=="function"&&(h=d,d=null);function p(g,y){y&&(y.uid<0&&(y.uid+=4294967296),y.gid<0&&(y.gid+=4294967296)),h&&h.apply(this,arguments)}return d?u.call(t,a,d,p):u.call(t,a,p)}}function l(u){return u&&function(a,d){var h=d?u.call(t,a,d):u.call(t,a);return h&&(h.uid<0&&(h.uid+=4294967296),h.gid<0&&(h.gid+=4294967296)),h}}function f(u){if(!u||u.code==="ENOSYS")return!0;var a=!process.getuid||process.getuid()!==0;return!!(a&&(u.code==="EINVAL"||u.code==="EPERM"))}}});var dn=w((Nu,fn)=>{var an=b("stream").Stream;fn.exports=Qs;function Qs(t){return{ReadStream:e,WriteStream:n};function e(i,r){if(!(this instanceof e))return new e(i,r);an.call(this);var s=this;this.path=i,this.fd=null,this.readable=!0,this.paused=!1,this.flags="r",this.mode=438,this.bufferSize=64*1024,r=r||{};for(var o=Object.keys(r),c=0,l=o.length;cthis.end)throw new Error("start must be <= end");this.pos=this.start}if(this.fd!==null){process.nextTick(function(){s._read()});return}t.open(this.path,this.flags,this.mode,function(u,a){if(u){s.emit("error",u),s.readable=!1;return}s.fd=a,s.emit("open",a),s._read()})}function n(i,r){if(!(this instanceof n))return new n(i,r);an.call(this),this.path=i,this.fd=null,this.writable=!0,this.flags="w",this.encoding="binary",this.mode=438,this.bytesWritten=0,r=r||{};for(var s=Object.keys(r),o=0,c=s.length;o= zero");this.pos=this.start}this.busy=!1,this._queue=[],this.fd===null&&(this._open=t.open,this._queue.push([this._open,this.path,this.flags,this.mode,void 0]),this.flush())}}});var pn=w((Iu,hn)=>{"use strict";hn.exports=Zs;var Xs=Object.getPrototypeOf||function(t){return t.__proto__};function Zs(t){if(t===null||typeof t!="object")return t;if(t instanceof Object)var e={__proto__:Xs(t)};else var e=Object.create(null);return Object.getOwnPropertyNames(t).forEach(function(n){Object.defineProperty(e,n,Object.getOwnPropertyDescriptor(t,n))}),e}});var _n=w((Wu,Ve)=>{var T=b("fs"),Vs=un(),eo=dn(),to=pn(),je=b("util"),N,Ie;typeof Symbol=="function"&&typeof Symbol.for=="function"?(N=Symbol.for("graceful-fs.queue"),Ie=Symbol.for("graceful-fs.previous")):(N="___graceful-fs.queue",Ie="___graceful-fs.previous");function no(){}function mn(t,e){Object.defineProperty(t,N,{get:function(){return e}})}var le=no;je.debuglog?le=je.debuglog("gfs4"):/\bgfs4\b/i.test(process.env.NODE_DEBUG||"")&&(le=function(){var t=je.format.apply(je,arguments);t="GFS4: "+t.split(/\n/).join(` +GFS4: `),console.error(t)});T[N]||(gn=global[N]||[],mn(T,gn),T.close=(function(t){function e(n,i){return t.call(T,n,function(r){r||yn(),typeof i=="function"&&i.apply(this,arguments)})}return Object.defineProperty(e,Ie,{value:t}),e})(T.close),T.closeSync=(function(t){function e(n){t.apply(T,arguments),yn()}return Object.defineProperty(e,Ie,{value:t}),e})(T.closeSync),/\bgfs4\b/i.test(process.env.NODE_DEBUG||"")&&process.on("exit",function(){le(T[N]),b("assert").equal(T[N].length,0)}));var gn;global[N]||mn(global,T[N]);Ve.exports=Xe(to(T));process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH&&!T.__patched&&(Ve.exports=Xe(T),T.__patched=!0);function Xe(t){Vs(t),t.gracefulify=Xe,t.createReadStream=x,t.createWriteStream=W;var e=t.readFile;t.readFile=n;function n(R,O,F){return typeof O=="function"&&(F=O,O=null),L(R,O,F);function L(H,C,M,m){return e(H,C,function(S){S&&(S.code==="EMFILE"||S.code==="ENFILE")?ge([L,[H,C,M],S,m||Date.now(),Date.now()]):typeof M=="function"&&M.apply(this,arguments)})}}var i=t.writeFile;t.writeFile=r;function r(R,O,F,L){return typeof F=="function"&&(L=F,F=null),H(R,O,F,L);function H(C,M,m,S,j){return i(C,M,m,function(D){D&&(D.code==="EMFILE"||D.code==="ENFILE")?ge([H,[C,M,m,S],D,j||Date.now(),Date.now()]):typeof S=="function"&&S.apply(this,arguments)})}}var s=t.appendFile;s&&(t.appendFile=o);function o(R,O,F,L){return typeof F=="function"&&(L=F,F=null),H(R,O,F,L);function H(C,M,m,S,j){return s(C,M,m,function(D){D&&(D.code==="EMFILE"||D.code==="ENFILE")?ge([H,[C,M,m,S],D,j||Date.now(),Date.now()]):typeof S=="function"&&S.apply(this,arguments)})}}var c=t.copyFile;c&&(t.copyFile=l);function l(R,O,F,L){return typeof F=="function"&&(L=F,F=0),H(R,O,F,L);function H(C,M,m,S,j){return c(C,M,m,function(D){D&&(D.code==="EMFILE"||D.code==="ENFILE"||D.code==="EBUSY")?ge([H,[C,M,m,S],D,j||Date.now(),Date.now()]):typeof S=="function"&&S.apply(this,arguments)})}}var f=t.readdir;t.readdir=a;var u=/^v[0-5]\./;function a(R,O,F){typeof O=="function"&&(F=O,O=null);var L=u.test(process.version)?function(M,m,S,j){return f(M,H(M,m,S,j))}:function(M,m,S,j){return f(M,m,H(M,m,S,j))};return L(R,O,F);function H(C,M,m,S){return function(j,D){j&&(j.code==="EMFILE"||j.code==="ENFILE")?ge([L,[C,M,m],j,S||Date.now(),Date.now()]):(D&&D.sort&&D.sort(),typeof m=="function"&&m.call(this,j,D))}}}if(process.version.substr(0,4)==="v0.8"){var d=eo(t);_=d.ReadStream,P=d.WriteStream}var h=t.ReadStream;h&&(_.prototype=Object.create(h.prototype),_.prototype.open=A);var p=t.WriteStream;p&&(P.prototype=Object.create(p.prototype),P.prototype.open=E),Object.defineProperty(t,"ReadStream",{get:function(){return _},set:function(R){_=R},enumerable:!0,configurable:!0}),Object.defineProperty(t,"WriteStream",{get:function(){return P},set:function(R){P=R},enumerable:!0,configurable:!0});var g=_;Object.defineProperty(t,"FileReadStream",{get:function(){return g},set:function(R){g=R},enumerable:!0,configurable:!0});var y=P;Object.defineProperty(t,"FileWriteStream",{get:function(){return y},set:function(R){y=R},enumerable:!0,configurable:!0});function _(R,O){return this instanceof _?(h.apply(this,arguments),this):_.apply(Object.create(_.prototype),arguments)}function A(){var R=this;V(R.path,R.flags,R.mode,function(O,F){O?(R.autoClose&&R.destroy(),R.emit("error",O)):(R.fd=F,R.emit("open",F),R.read())})}function P(R,O){return this instanceof P?(p.apply(this,arguments),this):P.apply(Object.create(P.prototype),arguments)}function E(){var R=this;V(R.path,R.flags,R.mode,function(O,F){O?(R.destroy(),R.emit("error",O)):(R.fd=F,R.emit("open",F))})}function x(R,O){return new t.ReadStream(R,O)}function W(R,O){return new t.WriteStream(R,O)}var ne=t.open;t.open=V;function V(R,O,F,L){return typeof F=="function"&&(L=F,F=null),H(R,O,F,L);function H(C,M,m,S,j){return ne(C,M,m,function(D,v){D&&(D.code==="EMFILE"||D.code==="ENFILE")?ge([H,[C,M,m,S],D,j||Date.now(),Date.now()]):typeof S=="function"&&S.apply(this,arguments)})}}return t}function ge(t){le("ENQUEUE",t[0].name,t[1]),T[N].push(t),Ze()}var Ne;function yn(){for(var t=Date.now(),e=0;e2&&(T[N][e][3]=t,T[N][e][4]=t);Ze()}function Ze(){if(clearTimeout(Ne),Ne=void 0,T[N].length!==0){var t=T[N].shift(),e=t[0],n=t[1],i=t[2],r=t[3],s=t[4];if(r===void 0)le("RETRY",e.name,n),e.apply(null,n);else if(Date.now()-r>=6e4){le("TIMEOUT",e.name,n);var o=n.pop();typeof o=="function"&&o.call(null,i)}else{var c=Date.now()-s,l=Math.max(s-r,1),f=Math.min(l*1.2,100);c>=f?(le("RETRY",e.name,n),e.apply(null,n.concat([r]))):T[N].push(t)}Ne===void 0&&(Ne=setTimeout(Ze,0))}}});var Q=w((Uu,wn)=>{"use strict";wn.exports=function(e,n,i){if(e.length===0)return i();let r=0,s=()=>{let o;return n(e[r++],(c,l)=>{if(c||l!==void 0||r>=e.length)return i(c,l,r);if(o===!1)for(;s(););o=!0},r),o||(o=!1),o};for(;s(););}});var Pn=w((Bu,An)=>{"use strict";var We=Symbol("singleComment"),Sn=Symbol("multiComment"),io=(t,e,n)=>"",ro=(t,e,n)=>t.slice(e,n).replace(/[^ \t\r\n]/g," "),so=(t,e)=>{let n=e-1,i=0;for(;t[n]==="\\";)n-=1,i+=1;return!!(i%2)};function oo(t,{whitespace:e=!0,trailingCommas:n=!1}={}){if(typeof t!="string")throw new TypeError(`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof t}\``);let i=e?ro:io,r=!1,s=!1,o=0,c="",l="",f=-1;for(let a=0;a{"use strict";var co=Ye(),lo=Pn(),Rn=new WeakMap,uo=co(()=>new TextDecoder("utf-8",{ignoreBOM:!0})),En=t=>typeof t=="string"?t:typeof Buffer<"u"&&Buffer.isBuffer(t)?t.toString("utf8"):uo().decode(t);function ao(t,e,n,i){let{stripComments:r=!1}=n,{readJson:s}=t;if(s&&!r){s(e,(o,c)=>{if(o)return i(o);i(null,c)});return}t.readFile(e,(o,c)=>{if(o)return i(o);let l=c,f=r&&typeof l=="object";if(f){let a=Rn.get(l);if(a!==void 0)return i(null,a)}let u;try{let a=En(l),d=r?lo(a,{trailingCommas:!0,whitespace:!0}):a;u=JSON.parse(d)}catch(a){return i(a)}f&&Rn.set(l,u),i(null,u)})}et.exports.decodeText=En;et.exports.readJson=ao});var ue=w((bu,Ue)=>{"use strict";var fo=Q(),{decodeText:ho}=tt(),po=47,go=92;function vn(t){if(t.length<=1)return null;for(let e=t.length-1;e>=0;e--){let n=t.charCodeAt(e);if(n===po||n===go)return t.slice(0,e||1)}return null}function yo(t,e,n,i,r,s){let o=(f,u)=>{let a=t.join(e,f);function d(h,p){if(h)return r.log?r.log(`${a} (directory description file): ${h}`):h.message=`${a} (directory description file): ${h}`,u(h);u(null,{content:p,directory:e,path:a})}t.fileSystem.readJson?t.fileSystem.readJson(a,(h,p)=>{if(h)return typeof h.code<"u"?(r.missingDependencies&&r.missingDependencies.add(a),u()):(r.fileDependencies&&r.fileDependencies.add(a),d(h));r.fileDependencies&&r.fileDependencies.add(a),d(null,p)}):t.fileSystem.readFile(a,(h,p)=>{if(h)return r.missingDependencies&&r.missingDependencies.add(a),u();r.fileDependencies&&r.fileDependencies.add(a);let g;if(p)try{g=JSON.parse(ho(p))}catch(y){return d(y)}else return d(new Error("No content in file"));d(null,g)})},c,l=(f,u)=>{if(f)return s(f);if(u)return s(null,u);let a=vn(e);return a?(e=a,c()):s()};c=()=>{if(i&&i.directory===e)return s(null,i);fo(n,o,l)},c()}function mo(t,e){if(t){if(Array.isArray(e)){let n=t;for(let i=0;i{"use strict";var Ee=b("path"),{fileURLToPath:$n}=b("url"),nt=35,X=47,ae=92,ve=65,$e=90,it=97,rt=122,fe=46,st=58,_o=70,wo=102,So=/^file:\//i,ye=Ee.posix.normalize,ot=Ee.win32.normalize,$=Object.freeze({Empty:0,Normal:1,Relative:2,AbsoluteWin:3,AbsolutePosix:4,Internal:5}),Ao=/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i,Po=/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i,de=t=>{switch(t.length){case 0:return $.Empty;case 1:{switch(t.charCodeAt(0)){case fe:return $.Relative;case X:return $.AbsolutePosix;case nt:return $.Internal}return $.Normal}case 2:{let i=t.charCodeAt(0);switch(i){case fe:{switch(t.charCodeAt(1)){case fe:case X:return $.Relative}return $.Normal}case X:return $.AbsolutePosix;case nt:return $.Internal}let r=t.charCodeAt(1);return r===st&&(i>=ve&&i<=$e||i>=it&&i<=rt)||i===ae&&r===ae?$.AbsoluteWin:$.Normal}}let e=t.charCodeAt(0);switch(e){case fe:{switch(t.charCodeAt(1)){case X:return $.Relative;case fe:return t.charCodeAt(2)===X?$.Relative:$.Normal}return $.Normal}case X:return $.AbsolutePosix;case nt:return $.Internal}let n=t.charCodeAt(1);if(n===st){let i=t.charCodeAt(2);if((i===ae||i===X)&&(e>=ve&&e<=$e||e>=it&&e<=rt))return $.AbsoluteWin}return e===ae&&n===ae?$.AbsoluteWin:$.Normal},Fn=t=>{switch(de(t)){case $.Empty:return t;case $.AbsoluteWin:return ot(t);case $.Relative:{let e=ye(t);return de(e)===$.Relative?e:`./${e}`}}return ye(t)},On=(t,e)=>{if(!e)return Fn(t);let n=de(e);switch(n){case $.AbsolutePosix:return ye(e);case $.AbsoluteWin:return ot(e)}switch(de(t)){case $.Normal:case $.Relative:case $.AbsolutePosix:return ye(`${t}/${e}`);case $.AbsoluteWin:return ot(`${t}\\${e}`)}switch(n){case $.Empty:return t;case $.Relative:{let i=ye(t);return de(i)===$.Relative?i:`./${i}`}}return ye(t)},Dn=t=>de(t)===$.AbsoluteWin?Ee.win32.dirname(t):Ee.posix.dirname(t),Ro=()=>{let t=new Map;return{fn:(n,i)=>{let r,s=t.get(n);if(s===void 0)t.set(n,s=new Map);else if(r=s.get(i),r!==void 0)return r;return r=On(n,i),s.set(i,r),r},cache:t}},Eo=()=>{let t=new Map;return{fn:n=>{let i=t.get(n);if(i!==void 0)return i;let r=Dn(n);return t.set(n,r),r},cache:t}},vo=()=>{let t=new Map;return{fn:(n,i)=>{let r,s=t.get(n);if(s===void 0)t.set(n,s=new Map);else if(r=s.get(i),r!==void 0)return r;return r=Ee.basename(n,i),s.set(i,r),r},cache:t}},$o=t=>{let e=t.length;if(e===0||t.charCodeAt(0)!==fe)return!1;if(e===1)return!0;let n=t.charCodeAt(1);return n===X?!0:n!==fe?!1:e===2?!0:t.charCodeAt(2)===X},ct=t=>{let e=t.charCodeAt(0);return e===ae?!0:t.charCodeAt(1)!==st?!1:e>=ve&&e<=$e||e>=it&&e<=rt},Fe=(t,e)=>t===X||e&&t===ae,Mn=(t,e)=>{let n=t.length;for(;n>0&&Fe(t.charCodeAt(n-1),e);)n--;return n},Fo=(t,e,n)=>n.slice(0,e).replace(/\//g,"\\").toLowerCase()===t.slice(0,e).replace(/\//g,"\\").toLowerCase(),xn=(t,e,n,i)=>{if(n.length127||o>127)return Fo(t,e,n);let c=s>=ve&&s<=$e?s+32:s,l=o>=ve&&o<=$e?o+32:o;if(c!==l)return!1}return!0},Oo=(t,e)=>{let n=ct(t),i=Mn(t,n);return xn(t,i,e,n)?e.length===i||Fe(e.charCodeAt(i),n):!1},Do=(t,e)=>{let n=ct(t),i=Mn(t,n);return e.length<=i||!xn(t,i,e,n)?!1:Fe(e.charCodeAt(i),n)},Tn=t=>{let e=t.charCodeAt(0);return(e===wo||e===_o)&&So.test(t)},Mo=t=>t instanceof URL||Tn(t)?$n(t):t;I.exports.PathType=$;I.exports.createCachedBasename=vo;I.exports.createCachedDirname=Eo;I.exports.createCachedJoin=Ro;I.exports.deprecatedInvalidSegmentRegEx=Ao;I.exports.dirname=Dn;I.exports.getType=de;I.exports.invalidSegmentRegEx=Po;I.exports.isFileURL=Tn;I.exports.isInside=Oo;I.exports.isRelativeRequest=$o;I.exports.isSubPath=Do;I.exports.isWindowsPath=ct;I.exports.join=On;I.exports.normalize=Fn;I.exports.toPath=Mo});var Hn=w((zu,kn)=>{"use strict";var{isRelativeRequest:xo}=z();kn.exports=function(e,n){if(typeof n.__innerRequest=="string"&&n.__innerRequest_request===n.request&&n.__innerRequest_relativePath===n.relativePath)return n.__innerRequest;let i;return n.request?(i=n.request,n.relativePath&&xo(i)&&(i=e.join(n.relativePath,i))):i=n.relativePath,n.__innerRequest_request=n.request,n.__innerRequest_relativePath=n.relativePath,n.__innerRequest=i}});var jn=w((Gu,Ln)=>{"use strict";var To=ue(),ko=Hn(),Cn=Symbol("NoFieldObject");Ln.exports=class{constructor(e,n,i){this.source=e,this.field=n,this.target=i,this._fieldDataCache=new WeakMap}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("AliasFieldPlugin",(i,r,s)=>{if(!i.descriptionFileData)return s();let o=ko(e,i);if(!o)return s();let{descriptionFileData:c}=i,l=this._fieldDataCache.get(c);if(l===void 0){let a=To.getField(c,this.field);l=a===null||typeof a!="object"?Cn:a,this._fieldDataCache.set(c,l)}if(l===Cn)return r.log&&r.log(`Field '${this.field}' doesn't contain a valid alias configuration`),s();let f=Object.prototype.hasOwnProperty.call(l,o)?l[o]:o.startsWith("./")?l[o.slice(2)]:void 0;if(f===o||f===void 0)return s();if(f===!1){let a={...i,path:!1};return typeof r.yield=="function"?(r.yield(a),s(null,null)):s(null,a)}let u={...i,path:i.descriptionFileRoot,request:f,fullySpecified:!1};e.doResolve(n,u,`aliased from description file ${i.descriptionFilePath} with mapping '${o}' to '${f}'`,r,(a,d)=>{if(a)return s(a);if(d===void 0)return s(null,null);s(null,d)})})}}});var ut=w((Ku,lt)=>{"use strict";var Nn=Q(),{PathType:In,getType:Ho}=z(),Co=[],Lo={all:Co,byFirstChar:new Map,hasAnyFirstChar:!1,useBuckets:!1};function jo(t,e){if(e.length===0)return Lo;let n=Array.from({length:e.length}),i=new Map,r=!1;for(let o=0;o0?l.charCodeAt(0):-1;let g={name:l,nameWithSlash:`${l}/`,alias:c.alias,onlyModule:!!c.onlyModule,absolutePath:f,wildcardPrefix:d,wildcardSuffix:h,firstCharCode:p,arrayAlias:Array.isArray(c.alias)};if(n[o]=g,p===-1)r=!0;else{let y=i.get(p);y===void 0&&(y=[],i.set(p,y)),y.push(g)}}let s=!r&&i.size>=2;return{all:n,byFirstChar:i,hasAnyFirstChar:r,useBuckets:s}}function No(t,e,n,i,r,s){if(e.all.length===0)return s();let o=i.request||i.path;if(!o)return s();let c=o.charCodeAt(0),l=!!i.request,f;if(e.useBuckets){let u=e.byFirstChar.get(c);if(u===void 0)return s();f=u}else f=e.all;Nn(f,(u,a)=>{let{firstCharCode:d}=u;if(d!==-1&&d!==c)return a();let h=!1,{absolutePath:p}=u,g=o===u.name||!u.onlyModule&&(l&&o.startsWith(u.nameWithSlash)||p!==null&&o.startsWith(p)),y=!u.onlyModule&&u.wildcardPrefix!==null;if(g||y){let _=(P,E)=>{if(P===!1){let W={...i,path:!1};return typeof r.yield=="function"?(r.yield(W),E(null,null)):E(null,W)}let x;if(y&&o.startsWith(u.wildcardPrefix)&&o.endsWith(u.wildcardSuffix)){let W=o.slice(u.wildcardPrefix.length,o.length-u.wildcardSuffix.length);x=P.toString().replace("*",W)}if(g&&o!==P&&!o.startsWith(`${P}/`)){let W=o.slice(u.name.length);x=P+W}if(x!==void 0){h=!0;let W={...i,request:x,fullySpecified:!1};return t.doResolve(n,W,`aliased with mapping '${u.name}': '${P}' to '${x}'`,r,(ne,V)=>ne?E(ne):V?E(null,V):E())}return E()},A=(P,E)=>P?a(P):E?a(null,E):h?a(null,null):a();return u.arrayAlias?Nn(u.alias,_,A):_(u.alias,A)}return a()},s)}lt.exports.aliasResolveHandler=No;lt.exports.compileAliasOptions=jo});var Un=w((Xu,Wn)=>{"use strict";var{aliasResolveHandler:Io,compileAliasOptions:Wo}=ut();Wn.exports=class{constructor(e,n,i){this.source=e,this.options=Array.isArray(n)?n:[n],this.target=i}apply(e){let n=e.ensureHook(this.target),i=Wo(e,this.options);e.getHook(this.source).tapAsync("AliasPlugin",(r,s,o)=>{Io(e,i,n,r,s,o)})}}});var qn=w((Vu,Bn)=>{"use strict";Bn.exports=class{constructor(e,n,i){this.source=e,this.appending=n,this.target=i}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("AppendPlugin",(i,r,s)=>{let o={...i,path:i.path+this.appending,relativePath:i.relativePath&&i.relativePath+this.appending};e.doResolve(n,o,this.appending,r,s)})}}});var Jn=w((ta,bn)=>{"use strict";bn.exports=class{constructor(e,n,i,r,s){this.source=e,this.test=n,this.message=i,this.allowAlternatives=r,this.target=s}apply(e){let n=e.ensureHook(this.target),{test:i,message:r,allowAlternatives:s}=this,o=Object.keys(i);e.getHook(this.source).tapAsync("ConditionalPlugin",(c,l,f)=>{for(let u of o)if(c[u]!==i[u])return f();e.doResolve(n,c,r,l,s?f:(u,a)=>{if(u)return f(u);if(a===void 0)return f(null,null);f(null,a)})})}}});var Gn=w((ia,Yn)=>{"use strict";var zn=ue(),Uo=/\\/g;Yn.exports=class{constructor(e,n,i,r){this.source=e,this.filenames=n,this.pathIsFile=i,this.target=r}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("DescriptionFilePlugin",(i,r,s)=>{let{path:o}=i;if(!o)return s();let c=this.pathIsFile?zn.cdUp(o):o;if(!c)return s();zn.loadDescriptionFile(e,c,this.filenames,i.descriptionFilePath?{path:i.descriptionFilePath,content:i.descriptionFileData,directory:i.descriptionFileRoot}:void 0,r,(l,f)=>{if(l)return s(l);if(!f)return r.log&&r.log(`No description file found in ${c} or above`),s();let u=o.slice(f.directory.length),a=`.${u.includes("\\")?u.replace(Uo,"/"):u}`,d={...i,descriptionFilePath:f.path,descriptionFileData:f.content,descriptionFileRoot:f.directory,relativePath:a};e.doResolve(n,d,`using description file: ${f.path} (relative path: ${a})`,r,(h,p)=>{if(h)return s(h);if(p===void 0)return s(null,null);s(null,p)})})})}}});var Qn=w((sa,Kn)=>{"use strict";Kn.exports=class{constructor(e,n){this.source=e,this.target=n}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("DirectoryExistsPlugin",(i,r,s)=>{let o=e.fileSystem,c=i.path;if(!c)return s();o.stat(c,(l,f)=>{if(l||!f||!f.isDirectory())return r.missingDependencies&&r.missingDependencies.add(c),r.log&&r.log(l||!f?`${c} doesn't exist`:`${c} is not a directory`),s();r.fileDependencies&&r.fileDependencies.add(c),e.doResolve(n,i,`existing directory ${c}`,r,s)})})}}});var Oe=w((oa,Zn)=>{"use strict";var{fileURLToPath:Bo}=b("url"),{isFileURL:qo}=z(),bo=/^(#?(?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/,Xn=/\0(.)/g;function Jo(t){if(t.length>=4&&t.charCodeAt(1)===92&&t.charCodeAt(3)===92){let e=t.charCodeAt(2);if(e===63||e===46)return 4}return 0}function zo(t){if(!t)return null;if(qo(t)&&(t=Bo(t)),t.indexOf("\0")!==-1){let s=bo.exec(t);return s?[s[1].replace(Xn,"$1"),s[2]?s[2].replace(Xn,"$1"):"",s[3]||""]:null}let n=t.charCodeAt(0)===92?Jo(t):0,i=t.indexOf("?",n),r=t.indexOf("#",n||1);return r<0?i<0?[t,"",""]:[t.slice(0,i),t.slice(i),""]:i<0||r{"use strict";var{parseIdentifier:li}=Oe(),ee=47,at=46,Yo=35,Go=/\*/g,Ko=/\$/g,Vn=new WeakMap;function ei(t){let e=Vn.get(t);return e===void 0&&(e=Object.keys(t),Vn.set(t,e)),e}var ti=new WeakMap;function Qo(t){let e=t,n=ti.get(e);if(n!==void 0)return n;let i=Object.getOwnPropertyNames(t);n=Array.from({length:i.length});for(let r=0;r0&&s.charCodeAt(l-1)===ee;n[r]={key:s,patternIndex:o,wildcardPrefix:o===-1?"":s.slice(0,o),wildcardSuffix:o===-1?"":s.slice(o+1),isLegacySubpath:o===-1&&f,isPattern:o!==-1,isSubpathMapping:f,isValidPattern:c}}return ti.set(e,n),n}function ni(t,e){let n=t.indexOf("*"),i=e.indexOf("*"),r=n===-1?t.length:n+1,s=i===-1?e.length:i+1;return r>s?-1:s>r||n===-1?1:i===-1||t.length>e.length?-1:e.length>t.length?1:0}var ii=new WeakMap;function Xo(t,e){let n=t.length,i=n>0&&t.charCodeAt(n-1)===ee;if(!t.includes("*")&&!i&&Object.prototype.hasOwnProperty.call(e,t))return[e[t],"",!1,!1,t];let s="",o=null,c,l=Qo(e);for(let u=0;u0;){let i=n[n.length-1],[r,s,o]=i;for(let c=o;cn.length===0?".":`./${n}`,ec,nc)};function ic(t){if(t.charCodeAt(0)!==Yo)throw new Error('Request should start with "#"');if(t.length===1)throw new Error("Request should have at least 2 characters");if(t.charCodeAt(t.length-1)===ee)throw new Error("Only requesting file allowed");return t.slice(1)}function rc(t,e){let n=li(t);if(!n)return;let[i]=n;if(i.charCodeAt(i.length-1)===ee!==e)throw new Error(e?`Expecting folder to folder mapping. ${JSON.stringify(t)} should end with "/"`:`Expecting file to file mapping. ${JSON.stringify(t)} should not end with "/"`)}ft.exports.processImportsField=function(e){return fi(e,n=>`#${n}`,ic,rc)}});var hi=w((ua,di)=>{"use strict";var sc=ue(),oc=Q(),{processExportsField:cc}=dt(),{parseIdentifier:lc}=Oe(),{deprecatedInvalidSegmentRegEx:uc,invalidSegmentRegEx:ac}=z();di.exports=class{constructor(e,n,i,r,s){this.source=e,this.target=r,this.conditionNames=n,this.fieldName=i,this.restrictions=!!s,this._fieldProcessorCache=new WeakMap}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("ExportsFieldPlugin",(i,r,s)=>{if(!i.descriptionFileData||i.relativePath!=="."||i.request===void 0)return s();let{descriptionFileData:o}=i,c=i.query||i.fragment?(i.request==="."?"./":i.request)+i.query+i.fragment:i.request,l,f;try{let a=this._fieldProcessorCache.get(o);if(a===void 0&&!this._fieldProcessorCache.has(o)){let d=sc.getField(o,this.fieldName);a=d?cc(d):null,this._fieldProcessorCache.set(o,a)}if(!a)return s();if(i.directory)return s(new Error(`Resolving to directories is not possible with the exports field (request was ${c}/)`));[l,f]=a(c,this.conditionNames)}catch(a){return r.log&&r.log(`Exports field in ${i.descriptionFilePath} can't be processed: ${a}`),s(a)}if(l.length===0){let a=[...this.conditionNames],d=a.length===1?`the condition "${a[0]}"`:`the conditions ${JSON.stringify(a)}`;return s(new Error(`"${c}" is not exported under ${d} from package ${i.descriptionFileRoot} (see exports field in ${i.descriptionFilePath})`))}let u=this.restrictions?{blocked:!1}:void 0;oc(l,(a,d,h)=>{let p=lc(a);if(!p)return d();let[g,y,_]=p;if(!g.startsWith("./"))return l.length===h?d(new Error(`Invalid "exports" target "${a}" defined for "${f}" in the package config ${i.descriptionFilePath}, targets must start with "./"`)):d();let A=g.slice(2);if(ac.test(A)&&uc.test(A))return l.length===h?d(new Error(`Invalid "exports" target "${a}" defined for "${f}" in the package config ${i.descriptionFilePath}, targets must start with "./"`)):d();let P={...i,request:void 0,path:e.join(i.descriptionFileRoot,g),relativePath:g,query:y,fragment:_};u&&(P.__restrictionsMarker=u),e.doResolve(n,P,`using exports field: ${a}`,r,(E,x)=>{if(E)return d(E);if(x===void 0)return d(null,null);d(null,x)})},(a,d)=>{if(a)return s(a);if(!d)return u&&u.blocked?s(null,null):s(new Error(`Package path ${c} is exported from package ${i.descriptionFileRoot}, but no valid target file was found (see exports field in ${i.descriptionFilePath})`));u&&delete d.__restrictionsMarker,s(null,d)})})}}});var gi=w((fa,pi)=>{"use strict";var fc=Q();pi.exports=class{constructor(e,n,i){this.source=e,this.options=n,this.target=i}apply(e){let n=e.ensureHook(this.target),{extension:i,alias:r}=this.options;e.getHook(this.source).tapAsync("ExtensionAliasPlugin",(s,o,c)=>{let l=s.request!==void 0,f=l?s.request:s.path;if(!f||!f.endsWith(i))return c();let u=typeof r=="string",a=f.slice(0,-i.length),d=!l&&s.relativePath&&s.relativePath.endsWith(i)?s.relativePath.slice(0,-i.length):null,h=(g,y,_)=>{let A=`${a}${g}`,P=l?{...s,request:A,fullySpecified:!0}:{...s,path:A,relativePath:d!==null?`${d}${g}`:s.relativePath,fullySpecified:!0};return e.doResolve(n,P,`aliased from extension alias with mapping '${i}' to '${g}'`,o,(E,x)=>{if(!u&&_)return _!==this.options.alias.length?(o.log&&o.log(`Failed to alias from extension alias with mapping '${i}' to '${g}' for '${A}': ${E}`),y(null,x)):y(E,x);y(E,x)})},p=(g,y)=>g?c(g):y?c(null,y):(u?r===i:r.includes(i))?c():c(null,null);u?h(r,p):r.length>1?fc(r,h,p):h(r[0],p)})}}});var mi=w((ha,yi)=>{"use strict";yi.exports=class{constructor(e,n){this.source=e,this.target=n}apply(e){let n=e.ensureHook(this.target),i=e.fileSystem;e.getHook(this.source).tapAsync("FileExistsPlugin",(r,s,o)=>{let c=r.path;if(!c)return o();i.stat(c,(l,f)=>{if(l||!f||!f.isFile())return s.missingDependencies&&s.missingDependencies.add(c),s.log&&s.log(l||!f?`${c} doesn't exist`:`${c} is not a file`),o();s.fileDependencies&&s.fileDependencies.add(c),e.doResolve(n,r,`existing file: ${c}`,s,o)})})}}});var wi=w((ga,_i)=>{"use strict";var dc=ue(),hc=Q(),{processImportsField:pc}=dt(),{parseIdentifier:gc}=Oe(),{invalidSegmentRegEx:yc}=z(),mc=46;_i.exports=class{constructor(e,n,i,r,s){this.source=e,this.targetFile=r,this.targetPackage=s,this.conditionNames=n,this.fieldName=i,this._fieldProcessorCache=new WeakMap}apply(e){let n=e.ensureHook(this.targetFile),i=e.ensureHook(this.targetPackage);e.getHook(this.source).tapAsync("ImportsFieldPlugin",(r,s,o)=>{if(!r.descriptionFileData||r.request===void 0)return o();let{descriptionFileData:c}=r,l=r.query||r.fragment?r.request+r.query+r.fragment:r.request,f,u;try{let a=this._fieldProcessorCache.get(c);if(a===void 0&&!this._fieldProcessorCache.has(c)){let d=dc.getField(c,this.fieldName);a=d?pc(d):null,this._fieldProcessorCache.set(c,a)}if(!a)return o();if(r.directory)return o(new Error(`Resolving to directories is not possible with the imports field (request was ${l}/)`));[f,u]=a(l,this.conditionNames)}catch(a){return s.log&&s.log(`Imports field in ${r.descriptionFilePath} can't be processed: ${a}`),o(a)}if(f.length===0)return o(new Error(`Package import ${l} is not imported from package ${r.descriptionFileRoot} (see imports field in ${r.descriptionFilePath})`));hc(f,(a,d,h)=>{let p=gc(a);if(!p)return d();let[g,y,_]=p;switch(g.charCodeAt(0)){case mc:{let A=g.slice(2);if(yc.test(A))return f.length===h?d(new Error(`Invalid "imports" target "${a}" defined for "${u}" in the package config ${r.descriptionFilePath}, targets must start with "./"`)):d();let P={...r,request:void 0,path:e.join(r.descriptionFileRoot,g),relativePath:g,query:y,fragment:_};e.doResolve(n,P,`using imports field: ${a}`,s,(E,x)=>{if(E)return d(E);if(x===void 0)return d(null,null);d(null,x)});break}default:{let A={...r,request:g,relativePath:g,fullySpecified:!0,query:y,fragment:_};e.doResolve(i,A,`using imports field: ${a}`,s,(P,E)=>{if(P)return d(P);if(E===void 0)return d(null,null);d(null,E)})}}},(a,d)=>o(a,d||null))})}}});var Ai=w((ma,Si)=>{"use strict";Si.exports=class{constructor(e,n){this.source=e,this.target=n}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("JoinRequestPartPlugin",(i,r,s)=>{let o=i.request||"",c=o.indexOf("/",3);c>=0&&o.charCodeAt(2)===64&&(c=o.indexOf("/",c+1));let l,f,u;c<0?(l=o,f=".",u=!1):(l=o.slice(0,c),f=`.${o.slice(c)}`,u=i.fullySpecified);let a={...i,path:e.join(i.path,l),relativePath:i.relativePath&&e.join(i.relativePath,l),request:f,fullySpecified:u};e.doResolve(n,a,null,r,s)})}}});var Ri=w((wa,Pi)=>{"use strict";Pi.exports=class{constructor(e,n){this.source=e,this.target=n}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("JoinRequestPlugin",(i,r,s)=>{let o=i.path,c=i.request,l={...i,path:e.join(o,c),relativePath:i.relativePath&&e.join(i.relativePath,c),request:void 0};e.doResolve(n,l,null,r,s)})}}});var Fi=w((Aa,$i)=>{"use strict";var _c=ue(),Ei=Symbol("alreadyTriedMainField"),vi=Symbol("NoMain");$i.exports=class{constructor(e,n,i){this.source=e,this.options=n,this.target=i,this._mainModuleCache=new WeakMap}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("MainFieldPlugin",(i,r,s)=>{if(i.path!==i.descriptionFileRoot||i[Ei]===i.descriptionFilePath||!i.descriptionFilePath)return s();let o=i.descriptionFileData,c=this._mainModuleCache.get(o);if(c===void 0){let u=_c.getField(o,this.options.name);if(!u||typeof u!="string"||u==="."||u==="./")return this._mainModuleCache.set(o,vi),s();this.options.forceRelative&&!/^\.\.?\//.test(u)&&(u=`./${u}`),c=u,this._mainModuleCache.set(o,c)}else if(c===vi)return s();let l=e.basename(i.descriptionFilePath),f={...i,request:c,module:!1,directory:c.endsWith("/"),[Ei]:i.descriptionFilePath};return e.doResolve(n,f,`use ${c} from ${this.options.name} in ${l}`,r,s)})}}});var pt=w((Pa,ht)=>{"use strict";function Di(t){if(t==="/")return{paths:["/"],segments:[""]};let e=t.split(/(.*?[\\/]+)/),n=[t],i=[e[e.length-1]],r=e[e.length-1];t=t.slice(0,Math.max(0,t.length-r.length-1));for(let s=e.length-2;s>2;s-=2)n.push(t),r=e[s],t=t.slice(0,Math.max(0,t.length-r.length))||"/",i.push(r.slice(0,-1));return[,r]=e,i.push(r),n.push(r),{paths:n,segments:i}}var Oi=new WeakMap;function wc(t,e){let n=Oi.get(t);if(n===void 0)n=new Map,Oi.set(t,n);else{let r=n.get(e);if(r!==void 0)return r}let i=Di(e);return n.set(e,i),i}ht.exports=Di;ht.exports.getPathsCached=wc});var gt=w((Ra,xi)=>{"use strict";var Sc=Q(),{getPathsCached:Ac}=pt(),Mi=new WeakMap;function Pc(t,e,n,i,r,s){let o=t.fileSystem,c=i.path,l,f=Mi.get(e);if(f===void 0?(f=new Map,Mi.set(e,f)):l=f.get(c),l===void 0){let{paths:a}=Ac(o,c),d=a.length,h=e.length;l=new Array(d*h);let p=0,g=t.pathCache.join.fn;for(let y=0;y{o.stat(a,(h,p)=>{if(!h&&p&&p.isDirectory()){let g={...i,path:a,request:u,module:!1},y=`looking for modules in ${a}`;return t.doResolve(n,g,y,r,d)}return r.log&&r.log(`${a} doesn't exist or is not a directory`),r.missingDependencies&&r.missingDependencies.add(a),d()})},s)}xi.exports={modulesResolveHandler:Pc}});var ki=w((va,Ti)=>{"use strict";var{modulesResolveHandler:Rc}=gt();Ti.exports=class{constructor(e,n,i){this.source=e,this.directories=[...n],this.target=i}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("ModulesInHierarchicalDirectoriesPlugin",(i,r,s)=>{Rc(e,this.directories,n,i,r,s)})}}});var Ci=w((Fa,Hi)=>{"use strict";Hi.exports=class{constructor(e,n,i){this.source=e,this.path=n,this.target=i}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("ModulesInRootPlugin",(i,r,s)=>{let o={...i,path:this.path,request:`./${i.request}`,module:!1};e.doResolve(n,o,`looking for modules in ${this.path}`,r,s)})}}});var ji=w((Da,Li)=>{"use strict";Li.exports=class{constructor(e,n){this.source=e,this.target=n}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("NextPlugin",(i,r,s)=>{e.doResolve(n,i,null,r,s)})}}});var Ii=w((xa,Ni)=>{"use strict";Ni.exports=class{constructor(e,n,i){this.source=e,this.requestOptions=n,this.target=i}apply(e){let n=e.ensureHook(this.target),{requestOptions:i}=this;e.getHook(this.source).tapAsync("ParsePlugin",(r,s,o)=>{let c=e.parse(r.request),l={...r};if(l.request=c.request,l.query=c.query||r.query||"",l.fragment=c.fragment||r.fragment||"",l.module=c.module,l.directory=c.directory,l.file=c.file,l.internal=c.internal,Object.assign(l,i),c&&s.log&&(c.module&&s.log("Parsed request is a module"),c.directory&&s.log("Parsed request is a directory")),l.request&&!l.query&&l.fragment){let f=l.fragment.endsWith("/"),u={...l,directory:f,request:l.request+(l.directory?"/":"")+(f?l.fragment.slice(0,-1):l.fragment),fragment:""};e.doResolve(n,u,null,s,(a,d)=>{if(a)return o(a);if(d)return o(null,d);e.doResolve(n,l,null,s,o)});return}e.doResolve(n,l,null,s,o)})}}});var Ui=w((ka,Wi)=>{"use strict";Wi.exports=class{constructor(e,n,i,r){this.source=e,this.pnpApi=n,this.target=i,this.alternateTarget=r}apply(e){let n=e.ensureHook(this.target),i=e.ensureHook(this.alternateTarget);e.getHook(this.source).tapAsync("PnpPlugin",(r,s,o)=>{let c=r.request;if(!c)return o();let l=`${r.path}/`,f=/^(@[^/]+\/)?[^/]+/.exec(c);if(!f)return o();let[u]=f,a=`.${c.slice(u.length)}`,d,h;try{if(d=this.pnpApi.resolveToUnqualified(u,l,{considerBuiltins:!1}),d===null){e.doResolve(i,r,"issuer is not managed by a pnpapi",s,(g,y)=>g?o(g):y?o(null,y):o(null,null));return}s.fileDependencies&&(h=this.pnpApi.resolveToUnqualified("pnpapi",l,{considerBuiltins:!1}))}catch(g){if(g.code==="MODULE_NOT_FOUND"&&g.pnpCode==="UNDECLARED_DEPENDENCY"){if(s.log){s.log("request is not managed by the pnpapi");for(let y of g.message.split(` +`).filter(Boolean))s.log(` ${y}`)}return o()}return o(g)}if(d===u)return o();h&&s.fileDependencies&&s.fileDependencies.add(h);let p={...r,path:d,request:a,ignoreSymlinks:!0,fullySpecified:r.fullySpecified&&a!=="."};e.doResolve(n,p,`resolved by pnp to ${d}`,s,(g,y)=>g?o(g):y?o(null,y):o(null,null))})}}});var Y=w((Ha,Ji)=>{"use strict";var Ec=b("util"),vc=Ec.deprecate(()=>{},"Hook.context is deprecated and will be removed");function Bi(...t){return this.call=this._createCall("sync"),this.call(...t)}function qi(...t){return this.callAsync=this._createCall("async"),this.callAsync(...t)}function bi(...t){return this.promise=this._createCall("promise"),this.promise(...t)}var Be=class{constructor(e=[],n=void 0){this._args=e,this.name=n,this.taps=[],this.interceptors=[],this._call=Bi,this.call=Bi,this._callAsync=qi,this.callAsync=qi,this._promise=bi,this.promise=bi,this._x=void 0,this.compile=this.compile,this.tap=this.tap,this.tapAsync=this.tapAsync,this.tapPromise=this.tapPromise}compile(e){throw new Error("Abstract: should be overridden")}_createCall(e){return this.compile({taps:this.taps,interceptors:this.interceptors,args:this._args,type:e})}_tap(e,n,i){if(typeof n=="string"){let r=n.trim();if(r==="")throw new Error("Missing name for tap");n={type:e,fn:i,name:r}}else{if(typeof n!="object"||n===null)throw new Error("Invalid tap options");let{name:r}=n;if(typeof r=="string"&&(r=r.trim()),typeof r!="string"||r==="")throw new Error("Missing name for tap");typeof n.context<"u"&&vc();let s=!0;for(let o in n)if(o!=="name"){s=!1;break}s?n={type:e,fn:i,name:r}:(n.name=r,n=Object.assign({type:e,fn:i},n))}n=this._runRegisterInterceptors(n),this._insert(n)}tap(e,n){this._tap("sync",e,n)}tapAsync(e,n){this._tap("async",e,n)}tapPromise(e,n){this._tap("promise",e,n)}_runRegisterInterceptors(e){let{interceptors:n}=this,{length:i}=n;if(i===0)return e;for(let r=0;rObject.assign({},e,typeof i=="string"?{name:i}:i);return{name:this.name,tap:(i,r)=>this.tap(n(i),r),tapAsync:(i,r)=>this.tapAsync(n(i),r),tapPromise:(i,r)=>this.tapPromise(n(i),r),intercept:i=>this.intercept(i),isUsed:()=>this.isUsed(),withOptions:i=>this.withOptions(n(i))}}isUsed(){return this.taps.length>0||this.interceptors.length>0}intercept(e){if(this._resetCompilation(),this.interceptors.push(Object.assign({},e)),e.register)for(let n=0;n0;){s--;let o=n[s];n[s+1]=o;let c=o.stage||0;if(r){if(r.has(o.name)){r.delete(o.name);continue}if(r.size>0)continue}if(!(c>i)){s++;break}}n[s]=e}};Object.setPrototypeOf(Be.prototype,null);Ji.exports=Be});var G=w((Ca,zi)=>{"use strict";var yt=class{constructor(e){this.config=e,this.options=void 0,this._args=void 0}create(e){this.init(e);let n;switch(e.type){case"sync":n=new Function(this.args(),`"use strict"; +${this.header()}${this.contentWithInterceptors({onError:i=>`throw ${i}; +`,onResult:i=>`return ${i}; +`,resultReturns:!0,onDone:()=>"",rethrowIfPossible:!0})}`);break;case"async":n=new Function(this.args({after:"_callback"}),`"use strict"; +${this.header()}${this.contentWithInterceptors({onError:i=>`_callback(${i}); +`,onResult:i=>`_callback(null, ${i}); +`,onDone:()=>`_callback(); +`})}`);break;case"promise":{let i=!1,r=this.contentWithInterceptors({onError:o=>(i=!0,`_error(${o}); +`),onResult:o=>`_resolve(${o}); +`,onDone:()=>`_resolve(); +`}),s="";s+=`"use strict"; +`,s+=this.header(),s+=`return new Promise((function(_resolve, _reject) { +`,i&&(s+=`var _sync = true; +`,s+=`function _error(_err) { +`,s+=`if(_sync) +`,s+=`_resolve(Promise.resolve().then((function() { throw _err; }))); +`,s+=`else +`,s+=`_reject(_err); +`,s+=`}; +`),s+=r,i&&(s+=`_sync = false; +`),s+=`})); +`,n=new Function(this.args(),s);break}}return this.deinit(),n}setup(e,n){let{taps:i}=n,{length:r}=i,s=Array.from({length:r});for(let o=0;o0){let{onError:n,onResult:i,onDone:r}=e,s="";for(let o=0;o{let c="";for(let l=0;l{let c="";for(let l=0;l{let o="";for(let c=0;c0&&(e+=`var _taps = this.taps; +`,e+=`var _interceptors = this.interceptors; +`),e}needContext(){let{taps:e}=this.options;for(let n=0;ng?"":r(),d="",h=r,p=0;for(let g=l-1;g>=0;g--){let y=g;h!==r&&(c[y].type!=="sync"||p++>20)&&(p=0,d+=`function _next${y}() { +`,d+=h(),d+=`} +`,h=()=>`${u?"return ":""}_next${y}(); +`);let A=h,P=this.callTap(y,{onError:E=>e(y,E,A,a),onResult:n&&(E=>n(y,E,A,a)),onDone:!n&&A,rethrowIfPossible:o&&(f<0||yP}return d+=h(),d}callTapsLooping({onError:e,onDone:n,rethrowIfPossible:i}){if(this.options.taps.length===0)return n();let r=this.options.taps.every(o=>o.type==="sync"),s="";r||(s+=`var _looper = (function() { +`,s+=`var _loopAsync = false; +`),s+=`var _loop; +`,s+=`do { +`,s+=`_loop = false; +`;for(let o=0;o{let u="";return u+=`if(${c} !== undefined) { +`,u+=`_loop = true; +`,r||(u+=`if(_loopAsync) _looper(); +`),u+=f(!0),u+=`} else { +`,u+=l(),u+=`} +`,u},onDone:n&&(()=>{let o="";return o+=`if(!_loop) { +`,o+=n(),o+=`} +`,o}),rethrowIfPossible:i&&r}),s+=`} while(_loop); +`,r||(s+=`_loopAsync = true; +`,s+=`}); +`,s+=`_looper(); +`),s}callTapsParallel({onError:e,onResult:n,onDone:i,rethrowIfPossible:r,onTap:s=(o,c)=>c()}){let{taps:o}=this.options,c=o.length;if(c<=1)return this.callTapsSeries({onError:e,onResult:n,onDone:i,rethrowIfPossible:r});let l=()=>i?`if(--_counter === 0) _done(); +`:"--_counter;",f=a=>a||!i?`_counter = 0; +`:`_counter = 0; +_done(); +`,u="";u+=`do { +`,u+=`var _counter = ${c}; +`,i&&(u+=`var _done = (function() { +`,u+=i(),u+=`}); +`);for(let a=0;athis.callTap(a,{onError:d=>{let h="";return h+=`if(_counter > 0) { +`,h+=e(a,d,l,f),h+=`} +`,h},onResult:n&&(d=>{let h="";return h+=`if(_counter > 0) { +`,h+=n(a,d,l,f),h+=`} +`,h}),onDone:!n&&(()=>l()),rethrowIfPossible:r}),l,f);return u+=`} while(false); +`,u}args({before:e,after:n}={}){let i=this._joinedArgs;return i===void 0&&(i=this._args.length===0?"":this._args.join(", "),this._joinedArgs=i),!e&&!n?i:i.length===0?e&&n?`${e}, ${n}`:e||n:e&&n?`${e}, ${i}, ${n}`:e?`${e}, ${i}`:`${i}, ${n}`}getTapFn(e){return`_x[${e}]`}getTap(e){return`_taps[${e}]`}getInterceptor(e){return`_interceptors[${e}]`}};zi.exports=yt});var Ki=w((La,Gi)=>{"use strict";var $c=Y(),Fc=G(),mt=class extends Fc{content({onError:e,onResult:n,onDone:i}){let r="";return r+=`var _results = new Array(${this.options.taps.length}); +`,r+=`var _checkDone = function() { +`,r+=`for(var i = 0; i < _results.length; i++) { +`,r+=`var item = _results[i]; +`,r+=`if(item === undefined) return false; +`,r+=`if(item.result !== undefined) { +`,r+=n("item.result"),r+=`return true; +`,r+=`} +`,r+=`if(item.error) { +`,r+=e("item.error"),r+=`return true; +`,r+=`} +`,r+=`} +`,r+=`return false; +`,r+=`} +`,r+=this.callTapsParallel({onError:(s,o,c,l)=>{let f="";return f+=`if(${s} < _results.length && ((_results.length = ${s+1}), (_results[${s}] = { error: ${o} }), _checkDone())) { +`,f+=l(!0),f+=`} else { +`,f+=c(),f+=`} +`,f},onResult:(s,o,c,l)=>{let f="";return f+=`if(${s} < _results.length && (${o} !== undefined && (_results.length = ${s+1}), (_results[${s}] = { result: ${o} }), _checkDone())) { +`,f+=l(!0),f+=`} else { +`,f+=c(),f+=`} +`,f},onTap:(s,o,c,l)=>{let f="";return s>0&&(f+=`if(${s} >= _results.length) { +`,f+=c(),f+=`} else { +`),f+=o(),s>0&&(f+=`} +`),f},onDone:i}),r}},Yi=new mt;function Oc(t){return Yi.setup(this,t),Yi.create(t)}function _t(t=[],e=void 0){let n=new $c(t,e);return n.constructor=_t,n.compile=Oc,n._call=void 0,n.call=void 0,n}_t.prototype=null;Gi.exports=_t});var Zi=w((ja,Xi)=>{"use strict";var Dc=Y(),Mc=G(),wt=class extends Mc{content({onError:e,onDone:n}){return this.callTapsParallel({onError:(i,r,s,o)=>e(r)+o(!0),onDone:n})}},Qi=new wt;function xc(t){return Qi.setup(this,t),Qi.create(t)}function St(t=[],e=void 0){let n=new Dc(t,e);return n.constructor=St,n.compile=xc,n._call=void 0,n.call=void 0,n}St.prototype=null;Xi.exports=St});var tr=w((Na,er)=>{"use strict";var Tc=Y(),kc=G(),At=class extends kc{content({onError:e,onResult:n,resultReturns:i,onDone:r}){return this.callTapsSeries({onError:(s,o,c,l)=>e(o)+l(!0),onResult:(s,o,c)=>`if(${o} !== undefined) { +${n(o)} +} else { +${c()}} +`,resultReturns:i,onDone:r})}},Vi=new At;function Hc(t){return Vi.setup(this,t),Vi.create(t)}function Pt(t=[],e=void 0){let n=new Tc(t,e);return n.constructor=Pt,n.compile=Hc,n._call=void 0,n.call=void 0,n}Pt.prototype=null;er.exports=Pt});var rr=w((Ia,ir)=>{"use strict";var Cc=Y(),Lc=G(),Rt=class extends Lc{content({onError:e,onDone:n}){return this.callTapsSeries({onError:(i,r,s,o)=>e(r)+o(!0),onDone:n})}},nr=new Rt;function jc(t){return nr.setup(this,t),nr.create(t)}function Et(t=[],e=void 0){let n=new Cc(t,e);return n.constructor=Et,n.compile=jc,n._call=void 0,n.call=void 0,n}Et.prototype=null;ir.exports=Et});var cr=w((Wa,or)=>{"use strict";var Nc=Y(),Ic=G(),vt=class extends Ic{content({onError:e,onDone:n}){return this.callTapsLooping({onError:(i,r,s,o)=>e(r)+o(!0),onDone:n})}},sr=new vt;function Wc(t){return sr.setup(this,t),sr.create(t)}function $t(t=[],e=void 0){let n=new Nc(t,e);return n.constructor=$t,n.compile=Wc,n._call=void 0,n.call=void 0,n}$t.prototype=null;or.exports=$t});var ar=w((Ua,ur)=>{"use strict";var Uc=Y(),Bc=G(),Ft=class extends Bc{content({onError:e,onResult:n,_onDone:i}){return this.callTapsSeries({onError:(r,s,o,c)=>e(s)+c(!0),onResult:(r,s,o)=>{let c="";return c+=`if(${s} !== undefined) { +`,c+=`${this._args[0]} = ${s}; +`,c+=`} +`,c+=o(),c},onDone:()=>n(this._args[0])})}},lr=new Ft;function qc(t){return lr.setup(this,t),lr.create(t)}function Ot(t=[],e=void 0){if(t.length<1)throw new Error("Waterfall hooks must have at least one argument");let n=new Uc(t,e);return n.constructor=Ot,n.compile=qc,n._call=void 0,n.call=void 0,n}Ot.prototype=null;ur.exports=Ot});var dr=w((Ba,fr)=>{"use strict";var Dt=b("util"),bc=(t,e)=>e,me=class{constructor(e,n=void 0){this._map=new Map,this.name=n,this._factory=e,this._interceptors=[]}get(e){return this._map.get(e)}for(e){let n=this._map,i=n.get(e);if(i!==void 0)return i;let r=this._factory(e),s=this._interceptors;for(let o=0;o{"use strict";var Mt=class t{constructor(e,n=void 0){this.hooks=e,this.name=n}tap(e,n){let{hooks:i}=this;for(let r=0;rn.withOptions(e)),this.name)}};hr.exports=Mt});var mr=w((ba,yr)=>{"use strict";var Jc=Y(),zc=G(),xt=class extends zc{content({onError:e,onResult:n,resultReturns:i,onDone:r,rethrowIfPossible:s}){return this.callTapsSeries({onError:(o,c)=>e(c),onResult:(o,c,l)=>`if(${c} !== undefined) { +${n(c)}; +} else { +${l()}} +`,resultReturns:i,onDone:r,rethrowIfPossible:s})}},gr=new xt,Yc=()=>{throw new Error("tapAsync is not supported on a SyncBailHook")},Gc=()=>{throw new Error("tapPromise is not supported on a SyncBailHook")};function Kc(t){return gr.setup(this,t),gr.create(t)}function Tt(t=[],e=void 0){let n=new Jc(t,e);return n.constructor=Tt,n.tapAsync=Yc,n.tapPromise=Gc,n.compile=Kc,n}Tt.prototype=null;yr.exports=Tt});var Sr=w((Ja,wr)=>{"use strict";var Qc=Y(),Xc=G(),kt=class extends Xc{content({onError:e,onDone:n,rethrowIfPossible:i}){return this.callTapsSeries({onError:(r,s)=>e(s),onDone:n,rethrowIfPossible:i})}},_r=new kt,Zc=()=>{throw new Error("tapAsync is not supported on a SyncHook")},Vc=()=>{throw new Error("tapPromise is not supported on a SyncHook")};function el(t){return _r.setup(this,t),_r.create(t)}function Ht(t=[],e=void 0){let n=new Qc(t,e);return n.constructor=Ht,n.tapAsync=Zc,n.tapPromise=Vc,n.compile=el,n}Ht.prototype=null;wr.exports=Ht});var Rr=w((za,Pr)=>{"use strict";var tl=Y(),nl=G(),Ct=class extends nl{content({onError:e,onDone:n,rethrowIfPossible:i}){return this.callTapsLooping({onError:(r,s)=>e(s),onDone:n,rethrowIfPossible:i})}},Ar=new Ct,il=()=>{throw new Error("tapAsync is not supported on a SyncLoopHook")},rl=()=>{throw new Error("tapPromise is not supported on a SyncLoopHook")};function sl(t){return Ar.setup(this,t),Ar.create(t)}function Lt(t=[],e=void 0){let n=new tl(t,e);return n.constructor=Lt,n.tapAsync=il,n.tapPromise=rl,n.compile=sl,n}Lt.prototype=null;Pr.exports=Lt});var $r=w((Ya,vr)=>{"use strict";var ol=Y(),cl=G(),jt=class extends cl{content({onError:e,onResult:n,resultReturns:i,rethrowIfPossible:r}){return this.callTapsSeries({onError:(s,o)=>e(o),onResult:(s,o,c)=>{let l="";return l+=`if(${o} !== undefined) { +`,l+=`${this._args[0]} = ${o}; +`,l+=`} +`,l+=c(),l},onDone:()=>n(this._args[0]),doneReturns:i,rethrowIfPossible:r})}},Er=new jt,ll=()=>{throw new Error("tapAsync is not supported on a SyncWaterfallHook")},ul=()=>{throw new Error("tapPromise is not supported on a SyncWaterfallHook")};function al(t){return Er.setup(this,t),Er.create(t)}function Nt(t=[],e=void 0){if(t.length<1)throw new Error("Waterfall hooks must have at least one argument");let n=new ol(t,e);return n.constructor=Nt,n.tapAsync=ll,n.tapPromise=ul,n.compile=al,n}Nt.prototype=null;vr.exports=Nt});var Fr=w((Ga,B)=>{"use strict";B.exports.AsyncParallelBailHook=Ki();B.exports.AsyncParallelHook=Zi();B.exports.AsyncSeriesBailHook=tr();B.exports.AsyncSeriesHook=rr();B.exports.AsyncSeriesLoopHook=cr();B.exports.AsyncSeriesWaterfallHook=ar();B.exports.HookMap=dr();B.exports.MultiHook=pr();B.exports.SyncBailHook=mr();B.exports.SyncHook=Sr();B.exports.SyncLoopHook=Rr();B.exports.SyncWaterfallHook=$r();B.exports.__esModule=!0});var Dr=w((Ka,Or)=>{"use strict";Or.exports=function(e,n,i){let r=e.log,s;if(r)if(i){let o=!1;s=c=>{o||(r(i),o=!0),r(` ${c}`)}}else s=r;return{log:s,yield:e.yield,fileDependencies:e.fileDependencies,contextDependencies:e.contextDependencies,missingDependencies:e.missingDependencies,stack:n}}});var Lr=w((Qa,Cr)=>{"use strict";var{AsyncSeriesBailHook:Mr,AsyncSeriesHook:fl,SyncHook:xr}=Fr(),dl=Dr(),{parseIdentifier:hl}=Oe(),{PathType:De,createCachedBasename:pl,createCachedDirname:gl,createCachedJoin:yl,getType:qe,isFileURL:ml,normalize:_l,toPath:It}=z(),wl=Promise.withResolvers?(t,e,n,i,r)=>{let{promise:s,resolve:o,reject:c}=Promise.withResolvers();return t.resolve(e,n,i,r,(l,f)=>{l?c(l):o(f)}),s}:(t,e,n,i,r)=>new Promise((s,o)=>{t.resolve(e,n,i,r,(c,l)=>{c?o(c):s(l)})}),Tr=new WeakMap,kr=/#/g,Me=class{constructor(e,n,i,r){this.name=e.name,this.path=n.path,this.request=n.request||"",this.query=n.query||"",this.fragment=n.fragment||"",this.directory=!!n.directory,this.module=!!n.module,this.parent=i,this.preSeeded=r}has(e){let n=this;for(;n;){if(n.name===e.name&&n.path===e.path&&n.request===e.request&&n.query===e.query&&n.fragment===e.fragment&&n.directory===e.directory&&n.module===e.module)return!0;n=n.parent}return this.preSeeded!==void 0&&this.preSeeded.has(e.toString())}get size(){let e=this.preSeeded?this.preSeeded.size:0,n=this;for(;n;)e++,n=n.parent;return e}*[Symbol.iterator](){if(this.preSeeded!==void 0)for(let i of this.preSeeded)yield i;let e=[],n=this;for(;n;)e.push(n),n=n.parent;for(let i=e.length-1;i>=0;i--)yield e[i].toString()}toString(){return`${this.name}: (${this.path}) ${this.request}${this.query}${this.fragment}${this.directory?" directory":""}${this.module?" module":""}`}};function Hr(t){return t.replace(/-([a-z])/g,e=>e.slice(1).toUpperCase())}var Wt=class t{static createStackEntry(e,n,i,r){return new Me(e,n,i,r)}constructor(e,n){this.fileSystem=e,this.options=n;let i=Tr.get(e);i||(i={join:yl(),dirname:gl(),basename:pl()},Tr.set(e,i)),this.pathCache=i,this.hooks={resolveStep:new xr(["hook","request"],"resolveStep"),noResolve:new xr(["request","error"],"noResolve"),resolve:new Mr(["request","resolveContext"],"resolve"),result:new fl(["result","resolveContext"],"result")}}ensureHook(e){if(typeof e!="string")return e;if(e=Hr(e),e.startsWith("before"))return this.ensureHook(e[6].toLowerCase()+e.slice(7)).withOptions({stage:-10});if(e.startsWith("after"))return this.ensureHook(e[5].toLowerCase()+e.slice(6)).withOptions({stage:10});let n=this.hooks[e];return n||(this.hooks[e]=new Mr(["request","resolveContext"],e),this.hooks[e])}getHook(e){if(typeof e!="string")return e;if(e=Hr(e),e.startsWith("before"))return this.getHook(e[6].toLowerCase()+e.slice(7)).withOptions({stage:-10});if(e.startsWith("after"))return this.getHook(e[5].toLowerCase()+e.slice(6)).withOptions({stage:10});let n=this.hooks[e];if(!n)throw new Error(`Hook ${e} doesn't exist`);return n}resolveSync(e,n,i,r){let s,o,c=!1;if(this.resolve(e,n,i,r||{},(l,f)=>{s=l,o=f,c=!0}),!c)throw new Error("Cannot 'resolveSync' because the fileSystem is not sync. Use 'resolve'!");if(s)throw s;if(o===void 0)throw new Error("No result");return o}resolvePromise(e,n,i,r){return wl(this,e,n,i,r||{})}resolve(e,n,i,r,s){if(!(typeof s=="function"&&typeof e=="object"&&e!==null&&typeof r=="object"&&r!==null)){if((typeof e=="string"||e instanceof URL)&&(typeof s!="function"&&(s=r),r=i,i=n,n=e,e={}),typeof r=="function"?(s=r,r={}):(!r||typeof r!="object")&&(r={}),typeof s!="function")throw new TypeError("callback argument is not a function");(!e||typeof e!="object")&&(e={})}if(typeof n!="string")if(n instanceof URL)n=It(n);else return s(new Error("path argument is not a string"));else ml(n)&&(n=It(n));if(typeof i!="string")if(i instanceof URL)i=It(i);else return s(new Error("request argument is not a string"));let o={context:e,path:n,request:i},c,l=!1,f;if(typeof r.yield=="function"){let h=r.yield;c=p=>{h(p),l=!0},f=p=>{p&&c(p),s(null)}}let u=h=>{let p=h.path;if(p===!1)return s(null,!1,h);let g=p.includes("#")?p.replace(kr,"\0#"):p,y=h.query,_;return y?_=y.includes("#")?y.replace(kr,"\0#"):y:_="",s(null,`${g}${_}${h.fragment||""}`,h)},a=(h,p)=>{let g=new Error(`Can't ${h}`);return g.details=p.join(` +`),this.hooks.noResolve.call(o,g),s(g)};if(r.log){let h=`resolve '${i}' in '${n}'`,p=r.log,g=[];return this.doResolve(this.hooks.resolve,o,h,{log:y=>{p(y),g.push(y)},yield:c,fileDependencies:r.fileDependencies,contextDependencies:r.contextDependencies,missingDependencies:r.missingDependencies,stack:r.stack},(y,_)=>y?s(y):l||_&&c?f(_):_?u(_):a(h,g))}let d=c?{log:void 0,yield:c,fileDependencies:r.fileDependencies,contextDependencies:r.contextDependencies,missingDependencies:r.missingDependencies,stack:r.stack}:r;return this.doResolve(this.hooks.resolve,o,null,d,(h,p)=>{if(h)return s(h);if(l||p&&c)return f(p);if(p)return u(p);let g=`resolve '${i}' in '${n}'`,y=[];return this.doResolve(this.hooks.resolve,o,g,{log:_=>y.push(_),yield:c,stack:r.stack},(_,A)=>_?s(_):l||A&&c?f(A):a(g,y))})}doResolve(e,n,i,r,s){let o=r.stack,c,l;o instanceof Me?(c=o,l=o.preSeeded):o&&(l=o);let f=t.createStackEntry(e,n,c,l);if(c!==void 0?c.has(f):l!==void 0&&l.has(f.toString())){let u=new Error(`Recursion in resolving +Stack: + ${[...f].join(` + `)}`);return u.recursion=!0,r.log&&r.log("abort resolving because of recursion"),s(u)}if(this.hooks.resolveStep.call(e,n),e.isUsed()){if(!r.log&&o instanceof Me)return r.stack=f,e.callAsync(n,r,(a,d)=>{if(r.stack=o,a)return s(a);if(d)return s(null,d);s()});let u=dl(r,f,i);return e.callAsync(n,u,(a,d)=>{if(a)return s(a);if(d)return s(null,d);s()})}s()}parse(e){let n={request:"",query:"",fragment:"",module:!1,directory:!1,file:!1,internal:!1},i=hl(e);if(!i)return n;if([n.request,n.query,n.fragment]=i,n.request.length>0){let r=qe(n.request);n.module=r===De.Normal,n.internal=e===n.request?r===De.Internal:qe(e)===De.Internal,n.directory=n.request.endsWith("/"),n.directory&&(n.request=n.request.slice(0,-1))}return n}isModule(e){return qe(e)===De.Normal}isPrivate(e){return qe(e)===De.Internal}isDirectory(e){return e.endsWith("/")}normalize(e){return _l(e)}join(e,n){return this.pathCache.join.fn(e,n)}dirname(e){return this.pathCache.dirname.fn(e)}basename(e,n){return this.pathCache.basename.fn(e,n)}};Cr.exports=Wt});var Nr=w((Za,jr)=>{"use strict";var{isInside:Sl,normalize:Al}=z();jr.exports=class{constructor(e,n){this.source=e,this.restrictions=n,this._restrictions=[];for(let i of n)this._restrictions.push(typeof i=="string"?{type:"path",rule:Al(i)}:{type:"regexp",rule:i})}apply(e){e.getHook(this.source).tapAsync("RestrictionsPlugin",(n,i,r)=>{if(typeof n.path=="string"){let{path:s}=n;for(let o of this._restrictions){if(o.type==="path"){if(Sl(o.rule,s))continue;i.log&&i.log(`${s} is not inside of the restriction ${o.rule}`)}else{if(o.rule.test(s))continue;i.log&&i.log(`${s} doesn't match the restriction ${o.rule}`)}return n.__restrictionsMarker&&(n.__restrictionsMarker.blocked=!0),r(null,null)}}r()})}}});var Wr=w((ef,Ir)=>{"use strict";Ir.exports=class{constructor(e){this.source=e}apply(e){this.source.tapAsync("ResultPlugin",(n,i,r)=>{let s={...n};i.log&&i.log(`reporting result ${s.path}`),e.hooks.result.callAsync(s,i,o=>{if(o)return r(o);typeof i.yield=="function"?(i.yield(s),r(null,null)):r(null,s)})})}}});var Br=w((tf,Ur)=>{"use strict";var Pl=Q(),Ut=class{constructor(e,n,i){this.roots=[...n],this.source=e,this.target=i}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("RootsPlugin",(i,r,s)=>{let o=i.request;if(!o||!o.startsWith("/"))return s();Pl(this.roots,(c,l)=>{let f=e.join(c,o.slice(1)),u={...i,path:f,relativePath:i.relativePath&&f};e.doResolve(n,u,`root path ${c}`,r,l)},s)})}};Ur.exports=Ut});var Jr=w((rf,br)=>{"use strict";var qr=ue(),Rl=47,Bt=Symbol("NoSelfRef");br.exports=class{constructor(e,n,i){this.source=e,this.target=i,this.fieldName=n,this._nameCache=new WeakMap}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("SelfReferencePlugin",(i,r,s)=>{if(!i.descriptionFileData)return s();let o=i.request;if(!o)return s();let{descriptionFileData:c}=i,l=this._nameCache.get(c);if(l===void 0){if(!qr.getField(c,this.fieldName))return this._nameCache.set(c,Bt),s();let u=qr.getField(c,"name");if(typeof u!="string")return this._nameCache.set(c,Bt),s();l=u,this._nameCache.set(c,l)}else if(l===Bt)return s();if(o.startsWith(l)&&(o.length===l.length||o.charCodeAt(l.length)===Rl)){let f=`.${o.slice(l.length)}`,u={...i,request:f,path:i.descriptionFileRoot,relativePath:"."};e.doResolve(n,u,"self reference",r,s)}else return s()})}}});var Gr=w((of,Yr)=>{"use strict";var El=Q(),{getPathsCached:vl}=pt(),{PathType:zr,getType:$l}=z();Yr.exports=class{constructor(e,n){this.source=e,this.target=n}apply(e){let n=e.ensureHook(this.target),i=e.fileSystem;e.getHook(this.source).tapAsync("SymlinkPlugin",(r,s,o)=>{if(r.ignoreSymlinks)return o();let c=vl(i,r.path),{paths:l,segments:f}=c,u=null,a=!1,d=-1;El(l,(h,p)=>{d++,s.fileDependencies&&s.fileDependencies.add(h),i.readlink(h,(g,y)=>{if(!g&&y){u===null&&(u=[...f]),u[d]=y,a=!0;let _=$l(y.toString());if(_===zr.AbsoluteWin||_===zr.AbsolutePosix)return p(null,d)}p()})},(h,p)=>{if(!a)return o();let g=u,_=(typeof p=="number"?g.slice(0,p+1):g).reduceRight((P,E)=>e.join(P,E)),A={...r,path:_};e.doResolve(n,A,`resolved symlink to ${_}`,s,(P,E)=>P?o(P):E?o(null,E):o(null,null))})})}}});var Qr=w((cf,Kr)=>{"use strict";function Fl(t){this.fs=t,this.lstat=void 0,this.lstatSync=void 0;let{lstatSync:e}=t;e&&(this.lstat=((r,s,o)=>{let c;try{c=o?e.call(t,r,s):e.call(t,r)}catch(l){return(o||s)(l)}(o||s)(null,c)}),this.lstatSync=((r,s)=>e.call(t,r,s))),this.stat=((r,s,o)=>{let c;try{c=o?t.statSync(r,s):t.statSync(r)}catch(l){return(o||s)(l)}(o||s)(null,c)}),this.statSync=((r,s)=>t.statSync(r,s)),this.readdir=((r,s,o)=>{let c;try{c=o?t.readdirSync(r,s):t.readdirSync(r)}catch(l){return(o||s)(l,[])}(o||s)(null,c)}),this.readdirSync=((r,s)=>t.readdirSync(r,s)),this.readFile=((r,s,o)=>{let c;try{c=o?t.readFileSync(r,s):t.readFileSync(r)}catch(l){return(o||s)(l)}(o||s)(null,c)}),this.readFileSync=((r,s)=>t.readFileSync(r,s)),this.readlink=((r,s,o)=>{let c;try{c=o?t.readlinkSync(r,s):t.readlinkSync(r)}catch(l){return(o||s)(l)}(o||s)(null,c)}),this.readlinkSync=((r,s)=>t.readlinkSync(r,s)),this.readJson=void 0,this.readJsonSync=void 0;let{readJsonSync:n}=t;n&&(this.readJson=((r,s)=>{let o;try{o=n.call(t,r)}catch(c){return s(c)}s(null,o)}),this.readJsonSync=(r=>n.call(t,r))),this.realpath=void 0,this.realpathSync=void 0;let{realpathSync:i}=t;i&&(this.realpath=((r,s,o)=>{let c;try{c=o?i.call(t,r,s):i.call(t,r)}catch(l){return(o||s)(l)}(o||s)(null,c)}),this.realpathSync=((r,s)=>i.call(t,r,s)))}Kr.exports=Fl});var Zr=w((uf,Xr)=>{"use strict";Xr.exports=class{constructor(e,n,i){this.source=e,this.message=n,this.target=i}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("TryNextPlugin",(i,r,s)=>{e.doResolve(n,i,this.message,r,s)})}}});var Jt=w((df,ss)=>{"use strict";var{aliasResolveHandler:Ol,compileAliasOptions:Dl}=ut(),{modulesResolveHandler:Ml}=gt(),{readJson:xl}=tt(),{PathType:af,isSubPath:Tl,normalize:kl}=z(),Vr=Symbol("NULL_CONTEXT"),es=new WeakMap,xe="tsconfig.json",Hl={stripComments:!0},ts=/[/\\]\*$/;function ns(t){let e=t.indexOf("*");return e===-1?t.length:e}function Cl(t){return[...t].sort((e,n)=>ns(n)-ns(e))}function qt(t,e){return t=t||{},e=e||{},{...t,...e,compilerOptions:{...t.compilerOptions,...e.compilerOptions}}}function bt(t,e){return t.includes("${configDir}")?t.replace(/\$\{configDir\}/g,e):t}function is(t,e,n,i){let r=i?n.join(t,i):t,s=Cl(Object.keys(e)),o=[],c=[];for(let l of s){let u=e[l].map(a=>{let d=bt(a,t);return n.join(r,d)});if(u.length>0)if(l==="*")for(let a=0;a({path:i})):e.references==="auto"?this.references="auto":this.references=[],this.baseUrl=e.baseUrl}else this.isAutoConfigFile=e===!0,this.configFile=this.isAutoConfigFile?xe:e,this.references=[],this.baseUrl=void 0}apply(e){let n=e.ensureHook("internal-resolve"),i=e.ensureHook("module");e.getHook("raw-resolve").tapAsync("TsconfigPathsPlugin",(r,s,o)=>{this._getTsconfigPathsMap(e,r,s,(c,l)=>{if(c)return o(c);if(!l)return o();let f=this._selectPathsDataForContext(r.path,l);if(!f)return o();Ol(e,f.alias,n,r,s,(u,a)=>u?o(u):a?o(null,a):o())})}),e.getHook("raw-module").tapAsync("TsconfigPathsPlugin",(r,s,o)=>{this._getTsconfigPathsMap(e,r,s,(c,l)=>{if(c)return o(c);if(!l)return o();let f=this._selectPathsDataForContext(r.path,l);if(!f)return o();Ml(e,f.modules,i,r,s,o)})})}_getTsconfigPathsMap(e,n,i,r){if(typeof n.tsconfigPathsMap<"u"){let o=n.tsconfigPathsMap;if(!o)return r(null,null);if(i.fileDependencies)for(let c of o.fileDependencies)i.fileDependencies.add(c);return r(null,o)}if(this.isAutoConfigFile){this._findTsconfigUpward(e,n.path||process.cwd(),(o,c)=>{if(o)return n.tsconfigPathsMap=null,r(o);if(!c)return n.tsconfigPathsMap=null,r(null,null);let l=c;if(n.tsconfigPathsMap=l,i.fileDependencies)for(let f of l.fileDependencies)i.fileDependencies.add(f);r(null,l)});return}let s=e.join(n.path||process.cwd(),this.configFile);this._loadTsconfigPathsMap(e,s,(o,c)=>{if(o)return n.tsconfigPathsMap=null,r(o);let l=c;if(n.tsconfigPathsMap=l,i.fileDependencies)for(let f of l.fileDependencies)i.fileDependencies.add(f);r(null,l)})}_findTsconfigUpward(e,n,i){let{fileSystem:r}=e,s=this.configFile,o=c=>{let l=e.join(c,s);r.stat(l,f=>{if(!f){this._loadTsconfigPathsMap(e,l,(a,d)=>{if(a)return i(null,null);i(null,d)});return}let u=e.dirname(c);if(u===c)return i(null,null);o(u)})};o(n)}_loadTsconfigPathsMap(e,n,i){let r=new Set;this._loadTsconfig(e,n,r,void 0,(s,o)=>{if(s)return i(s);let c=o,l=c.compilerOptions||{},f=e.dirname(n),u=this.baseUrl!==void 0?this.baseUrl:l.baseUrl,a=is(f,l.paths||{},e,u),d={},h=null;if(this.references==="auto"?h=c.references:Array.isArray(this.references)&&(h=this.references),!Array.isArray(h))return i(null,rs(a,f,d,r));this._loadTsconfigReferences(e,f,h,r,d,p=>{if(p)return i(p);i(null,rs(a,f,d,r))})})}_selectPathsDataForContext(e,n){let{main:i,allContexts:r,contextList:s}=n;if(!e)return i;let o;if(s.length>=2)if(o=es.get(n),o!==void 0){let u=o.get(e);if(u!==void 0)return u===Vr?null:u}else o=new Map,es.set(n,o);let c=null,l=0;for(let u=0;ul&&Tl(a,e)&&(c=a,l=a.length)}let f=c===null?null:r[c];return o!==void 0&&o.set(e,f===null?Vr:f),f}_loadTsconfigFromExtends(e,n,i,r,s,o){let{fileSystem:c}=e,l=e.dirname(n);i=bt(i,l);let f=i;typeof i=="string"&&!i.includes(".json")&&(i+=".json");let u=e.join(l,i),a=d=>{this._loadTsconfig(e,d,r,s,(h,p)=>{if(h)return o(h);let g=p,y=g.compilerOptions||{baseUrl:void 0};if(y.baseUrl){let _=e.dirname(d);y.baseUrl=Ll(_,e,y.baseUrl)}delete g.references,o(null,g)})};c.stat(u,d=>{if(!d)return a(u);let h=null;if(typeof f=="string"&&f.startsWith("@")&&f.split("/").length===2?h=`${f}/${xe}`:i.includes("/")&&!f.startsWith(".")&&!f.startsWith("/")?h=i:!f.startsWith(".")&&!f.startsWith("/")&&(h=`${f}/${xe}`),h===null)return a(u);let p=kl(`node_modules/${h}`);this._findExtendsInNodeModules(e,l,p,g=>{a(g||e.join(l,p))})})}_findExtendsInNodeModules(e,n,i,r){let{fileSystem:s}=e,o=c=>{let l=e.join(c,i);s.stat(l,f=>{if(!f)return r(l);let u=e.dirname(c);if(u===c)return r(null);o(u)})};o(n)}_loadTsconfigReferences(e,n,i,r,s,o,c){if(i.length===0)return o(null);let l=c||new Set,f=i.length,u=()=>{--f===0&&o(null)};for(let a of i){let d=bt(a.path,n),h=e.join(e.join(n,d),xe);if(l.has(h)){u();continue}l.add(h),this._loadTsconfig(e,h,r,void 0,(p,g)=>{if(p)return u();let y=g;if(y.compilerOptions&&y.compilerOptions.paths){let _=e.dirname(h);s[_]=is(_,y.compilerOptions.paths||{},e,y.compilerOptions.baseUrl)}this.references==="auto"&&Array.isArray(y.references)?this._loadTsconfigReferences(e,e.dirname(h),y.references,r,s,u,l):u()})}}_loadTsconfig(e,n,i,r,s){let o=r||new Set;if(o.has(n))return s(null,{});o.add(n),xl(e.fileSystem,n,Hl,(c,l)=>{if(c)return s(c);let f=l;i.add(n);let u=f.extends;if(!u)return s(null,f);if(!Array.isArray(u)){this._loadTsconfigFromExtends(e,n,u,i,o,(p,g)=>{if(p)return s(p);s(null,qt(g,f))});return}let a={},d=0,h=()=>{if(d>=u.length)return s(null,qt(a,f));this._loadTsconfigFromExtends(e,n,u[d++],i,o,(p,g)=>{if(p)return s(p);a=qt(a,g),h()})};h()})}}});var ls=w((pf,cs)=>{"use strict";var{isRelativeRequest:os}=z();function jl(t,e,n){let i=n.join(t,e);return os(i)?i:`./${i}`}function Nl(t){return t.descriptionFileRoot&&!t.module?t.descriptionFileRoot:t.path}function Il(t,e){let n=t.request;return!n||!t.relativePath||!os(n)?n:jl(t.relativePath,n,e)}function Wl(t,e,n,i){return JSON.stringify({type:t,context:n?e.context:"",path:Nl(e),query:e.query,fragment:e.fragment,request:Il(e,i)})}cs.exports=class{constructor(e,n,i,r,s){this.source=e,this.filterPredicate=n,this.withContext=r,this.cache=i,this.target=s}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("UnsafeCachePlugin",(i,r,s)=>{if(!this.filterPredicate(i))return e.doResolve(n,i,null,r,s);let o=typeof r.yield=="function",c=Wl(o?"yield":"default",i,this.withContext,e),l=this.cache[c];if(l){if(o){let d=r.yield;if(Array.isArray(l))for(let h of l)d(h);else d(l);return s(null,null)}return s(null,l)}let f,u,a=[];o&&(f=r.yield,u=d=>{a.push(d)}),e.doResolve(n,i,null,u?{...r,yield:u}:r,(d,h)=>{if(d)return s(d);if(o){h&&a.push(h);for(let p of a)f(p);return this.cache[c]=a,s(null,null)}if(h)return s(null,this.cache[c]=h);s()})})}}});var as=w((yf,us)=>{"use strict";us.exports=class{constructor(e,n,i){this.source=e,this.filename=n,this.target=i}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("UseFilePlugin",(i,r,s)=>{let o=e.join(i.path,this.filename),c={...i,path:o,relativePath:i.relativePath&&e.join(i.relativePath,this.filename)};e.doResolve(n,c,`using path: ${o}`,r,s)})}}});var _s=w((mf,ms)=>{"use strict";var{versions:Ul}=b("process"),fs=jn(),zt=Un(),Bl=qn(),_e=Jn(),Te=Gn(),ds=Qn(),ql=hi(),Yt=gi(),bl=mi(),Jl=wi(),zl=Ai(),be=Ri(),Yl=Fi(),Gt=ki(),Gl=Ci(),J=ji(),Kl=Ii(),Ql=Ui(),Xl=Lr(),Zl=Nr(),Vl=Wr(),eu=Br(),tu=Jr(),nu=Gr(),iu=Qr(),ru=Zr(),su=Jt(),ou=ls(),hs=as(),{PathType:ps,getType:cu,toPath:Z}=z();function lu(t){if(t===void 0&&Ul.pnp){let e=b("module").findPnpApi;if(e)return{resolveToUnqualified(n,i,r){let s=e(i);return s?s.resolveToUnqualified(n,i,r):null}}}return t||null}function gs(t){return t===!1?!1:Array.isArray(t)?t.map(Z):Z(t)}function ys(t){return typeof t=="object"&&!Array.isArray(t)&&t!==null?Object.keys(t).map(e=>{let n={name:e,onlyModule:!1,alias:gs(t[e])};return/\$$/.test(e)&&(n.onlyModule=!0,n.name=e.slice(0,-1)),n}):t?t.map(e=>({...e,alias:gs(e.alias)})):[]}function uu(t){if(t===void 0)return!1;if(typeof t=="boolean")return t;if(typeof t=="string"||t instanceof URL)return Z(t);let e={};return t.configFile!==void 0&&(e.configFile=Z(t.configFile)),t.baseUrl!==void 0&&(e.baseUrl=Z(t.baseUrl)),t.references!==void 0&&(e.references=Array.isArray(t.references)?t.references.map(Z):t.references),e}function au(t,e){let n=[],i=new Set(t);for(let r of i)if(e(r)){let s=n.length>0?n[n.length-1]:void 0;Array.isArray(s)?s.push(r):n.push([r])}else n.push(r);return n}function fu(t){let e=new Set(t.mainFields||["main"]),n=[];for(let i of e)typeof i=="string"?n.push({name:[i],forceRelative:!0}):Array.isArray(i)?n.push({name:i,forceRelative:!0}):n.push({name:Array.isArray(i.name)?i.name:[i.name],forceRelative:i.forceRelative});return{alias:ys(t.alias),fallback:ys(t.fallback),aliasFields:new Set(t.aliasFields),cachePredicate:t.cachePredicate||function(){return!0},cacheWithContext:typeof t.cacheWithContext<"u"?t.cacheWithContext:!0,exportsFields:new Set(t.exportsFields||["exports"]),importsFields:new Set(t.importsFields||["imports"]),conditionNames:new Set(t.conditionNames),descriptionFiles:[...new Set(t.descriptionFiles||["package.json"])],enforceExtension:t.enforceExtension===void 0?!!(t.extensions&&t.extensions.includes("")):t.enforceExtension,extensions:new Set(t.extensions||[".js",".json",".node"]),extensionAlias:t.extensionAlias?Object.keys(t.extensionAlias).map(i=>({extension:i,alias:t.extensionAlias[i]})):[],extensionAliasForExports:t.extensionAliasForExports||!1,fileSystem:t.useSyncFileSystemCalls?new iu(t.fileSystem):t.fileSystem,unsafeCache:t.unsafeCache&&typeof t.unsafeCache!="object"?{}:t.unsafeCache||!1,symlinks:typeof t.symlinks<"u"?t.symlinks:!0,resolver:t.resolver,modules:au(Array.isArray(t.modules)?t.modules.map(Z):t.modules?[Z(t.modules)]:["node_modules"],i=>{let r=cu(i);return r===ps.Normal||r===ps.Relative}),mainFields:n,mainFiles:new Set(t.mainFiles||["index"]),plugins:t.plugins||[],pnpApi:lu(t.pnpApi),roots:new Set(t.roots?t.roots.map(Z):void 0),fullySpecified:t.fullySpecified||!1,resolveToContext:t.resolveToContext||!1,preferRelative:t.preferRelative||!1,preferAbsolute:t.preferAbsolute||!1,restrictions:new Set(t.restrictions&&t.restrictions.map(i=>i instanceof RegExp?i:Z(i))),tsconfig:uu(t.tsconfig)}}ms.exports.createResolver=function(e){let n=fu(e),{alias:i,fallback:r,aliasFields:s,extensionAliasForExports:o,cachePredicate:c,cacheWithContext:l,conditionNames:f,descriptionFiles:u,enforceExtension:a,exportsFields:d,extensionAlias:h,importsFields:p,extensions:g,fileSystem:y,fullySpecified:_,mainFields:A,mainFiles:P,modules:E,plugins:x,pnpApi:W,resolveToContext:ne,preferRelative:V,preferAbsolute:R,symlinks:O,unsafeCache:F,resolver:L,restrictions:H,roots:C,tsconfig:M}=n,m=[...x],S=L||new Xl(y,n);S.ensureHook("resolve"),S.ensureHook("internalResolve"),S.ensureHook("newInternalResolve"),S.ensureHook("importsResolve"),S.ensureHook("parsedResolve"),S.ensureHook("describedResolve"),S.ensureHook("rawResolve"),S.ensureHook("normalResolve"),S.ensureHook("internal"),S.ensureHook("rawModule"),S.ensureHook("alternateRawModule"),S.ensureHook("module"),S.ensureHook("resolveAsModule"),S.ensureHook("undescribedResolveInPackage"),S.ensureHook("resolveInPackage"),S.ensureHook("resolveInExistingDirectory"),S.ensureHook("importsFieldRelative"),o&&S.ensureHook("exportsFieldRelative"),S.ensureHook("relative"),S.ensureHook("describedRelative"),S.ensureHook("directory"),S.ensureHook("undescribedExistingDirectory"),S.ensureHook("existingDirectory"),S.ensureHook("undescribedRawFile"),S.ensureHook("rawFile"),S.ensureHook("file"),S.ensureHook("finalFile"),S.ensureHook("existingFile"),S.ensureHook("resolved"),S.hooks.newInteralResolve=S.hooks.newInternalResolve;for(let{source:v,resolveOptions:ze}of[{source:"resolve",resolveOptions:{fullySpecified:_}},{source:"internal-resolve",resolveOptions:{fullySpecified:!1}},{source:"imports-resolve",resolveOptions:{fullySpecified:!1,internal:!1}}])m.push(new Kl(v,ze,"parsed-resolve"));m.push(new Te("parsed-resolve",u,!1,"described-resolve")),m.push(new J("after-parsed-resolve","described-resolve")),F?m.push(new ou("described-resolve",c,F,l,"raw-resolve")):m.push(new J("described-resolve","raw-resolve")),r.length>0&&m.push(new zt("described-resolve",r,"internal-resolve")),i.length>0&&m.push(new zt("raw-resolve",i,"internal-resolve")),M&&m.push(new su(M));for(let v of s)m.push(new fs("raw-resolve",v,"internal-resolve"));for(let v of h)m.push(new Yt("raw-resolve",v,"normal-resolve"));m.push(new J("raw-resolve","normal-resolve")),V&&m.push(new be("after-normal-resolve","relative")),m.push(new _e("after-normal-resolve",{module:!0},"resolve as module",!1,"raw-module")),m.push(new _e("after-normal-resolve",{internal:!0},"resolve as internal import",!1,"internal")),R&&m.push(new be("after-normal-resolve","relative")),C.size>0&&m.push(new eu("after-normal-resolve",C,"relative")),!V&&!R&&m.push(new be("after-normal-resolve","relative"));for(let v of p)m.push(new Jl("internal",f,v,"imports-field-relative","imports-resolve"));for(let v of h)m.push(new Yt("imports-field-relative",v,"relative"));m.push(new J("imports-field-relative","relative"));for(let v of d)m.push(new tu("raw-module",v,"resolve-as-module"));for(let v of E)Array.isArray(v)?v.includes("node_modules")&&W?(m.push(new Gt("raw-module",v.filter(ze=>ze!=="node_modules"),"module")),m.push(new Ql("raw-module",W,"undescribed-resolve-in-package","alternate-raw-module")),m.push(new Gt("alternate-raw-module",["node_modules"],"module"))):m.push(new Gt("raw-module",v,"module")):m.push(new Gl("raw-module",v,"module"));m.push(new zl("module","resolve-as-module")),ne||m.push(new _e("resolve-as-module",{directory:!1,request:"."},"single file module",!0,"undescribed-raw-file")),m.push(new ds("resolve-as-module","undescribed-resolve-in-package")),m.push(new Te("undescribed-resolve-in-package",u,!1,"resolve-in-package")),m.push(new J("after-undescribed-resolve-in-package","resolve-in-package"));let j=o?"exports-field-relative":"relative";for(let v of d)m.push(new ql("resolve-in-package",f,v,j,H.size>0));if(m.push(new J("resolve-in-package","resolve-in-existing-directory")),o){for(let v of h)m.push(new Yt("exports-field-relative",v,"relative"));m.push(new J("exports-field-relative","relative"))}if(m.push(new be("resolve-in-existing-directory","relative")),m.push(new Te("relative",u,!0,"described-relative")),m.push(new J("after-relative","described-relative")),ne?m.push(new J("described-relative","directory")):(m.push(new _e("described-relative",{directory:!1},null,!0,"raw-file")),m.push(new _e("described-relative",{fullySpecified:!1},"as directory",!0,"directory"))),m.push(new ds("directory","undescribed-existing-directory")),ne)m.push(new J("undescribed-existing-directory","resolved"));else{m.push(new Te("undescribed-existing-directory",u,!1,"existing-directory"));for(let v of P)m.push(new hs("undescribed-existing-directory",v,"undescribed-raw-file"));for(let v of A)m.push(new Yl("existing-directory",v,"resolve-in-existing-directory"));for(let v of P)m.push(new hs("existing-directory",v,"undescribed-raw-file"));m.push(new Te("undescribed-raw-file",u,!0,"raw-file")),m.push(new J("after-undescribed-raw-file","raw-file")),m.push(new _e("raw-file",{fullySpecified:!0},null,!1,"file")),a||m.push(new ru("raw-file","no extension","file"));for(let v of g)m.push(new Bl("raw-file",v,"file"));i.length>0&&m.push(new zt("file",i,"internal-resolve"));for(let v of s)m.push(new fs("file",v,"internal-resolve"));m.push(new J("file","final-file")),m.push(new bl("final-file","existing-file")),O&&m.push(new nu("existing-file","existing-file")),m.push(new J("existing-file","resolved"))}let{resolved:D}=S.hooks;H.size>0&&m.push(new Zl(D,H)),m.push(new Vl(D));for(let v of m)typeof v=="function"?v.call(S,S):v&&v.apply(S);return S}});var Ss=w((wf,ws)=>{"use strict";ws.exports=class{constructor(e,n){this.source=e,this.target=n}apply(e){let n=e.ensureHook(this.target);e.getHook(this.source).tapAsync("CloneBasenamePlugin",(i,r,s)=>{let o=i.path,c=e.basename(o),l=e.join(o,c),f={...i,path:l,relativePath:i.relativePath&&e.join(i.relativePath,c)};e.doResolve(n,f,`using path: ${l}`,r,s)})}}});var Ps=w((Af,As)=>{"use strict";As.exports=class{constructor(e){this.source=e}apply(e){let{source:n}=this;e.getHook(this.source).tapAsync("LogInfoPlugin",(i,r,s)=>{if(!r.log)return s();let{log:o}=r,c=`[${n}] `;i.path&&o(`${c}Resolving in directory: ${i.path}`),i.request&&o(`${c}Resolving request: ${i.request}`),i.module&&o(`${c}Request is an module request.`),i.directory&&o(`${c}Request is a directory request.`),i.query&&o(`${c}Resolving request query: ${i.query}`),i.fragment&&o(`${c}Resolving request fragment: ${i.fragment}`),i.descriptionFilePath&&o(`${c}Has description data from ${i.descriptionFilePath}`),i.relativePath&&o(`${c}Relative path from description file is: ${i.relativePath}`),s()})}}});var Fs=w((Pf,$s)=>{"use strict";var we=Ye(),Es=we(()=>cn()),ke=we(()=>{let t=_n(),e=Es();return new e(t,4e3)}),Se=we(()=>({environments:["node+es3+es5+process+native"]})),Ae=we(()=>_s()),vs=we(()=>Ae().createResolver({conditionNames:["node"],extensions:[".js",".json",".node"],fileSystem:ke()})),du=(t,e,n,i,r)=>{(typeof t=="string"||t instanceof URL)&&(r=i,i=n,n=e,e=t,t=Se()),typeof r!="function"&&(r=i),vs().resolve(t,e,n,i,r)},hu=we(()=>Ae().createResolver({conditionNames:["node"],extensions:[".js",".json",".node"],useSyncFileSystemCalls:!0,fileSystem:ke()})),pu=(t,e,n,i)=>((typeof t=="string"||t instanceof URL)&&(i=n,n=e,e=t,t=Se()),hu().resolveSync(t,e,n,i)),gu=(t,e,n,i)=>((typeof t=="string"||t instanceof URL)&&(i=n,n=e,e=t,t=Se()),vs().resolvePromise(t,e,n,i));function yu(t){let e=Ae().createResolver({...t,fileSystem:t.fileSystem||ke()});return function(i,r,s,o,c){(typeof i=="string"||i instanceof URL)&&(c=o,o=s,s=r,r=i,i=Se()),typeof c!="function"&&(c=o),e.resolve(i,r,s,o,c)}}function mu(t){let e=Ae().createResolver({useSyncFileSystemCalls:!0,...t,fileSystem:t.fileSystem||ke()});return function(i,r,s,o){return(typeof i=="string"||i instanceof URL)&&(o=s,s=r,r=i,i=Se()),e.resolveSync(i,r,s,o)}}function _u(t){let e=Ae().createResolver({...t,fileSystem:t.fileSystem||ke()});return function(i,r,s,o){return(typeof i=="string"||i instanceof URL)&&(o=s,s=r,r=i,i=Se()),e.resolvePromise(i,r,s,o)}}var Rs=(t,e)=>{let n=Object.getOwnPropertyDescriptors(e);return Object.defineProperties(t,n),Object.freeze(t)};$s.exports=Rs(du,{get sync(){return pu},get promise(){return gu},create:Rs(yu,{get sync(){return mu},get promise(){return _u}}),get ResolverFactory(){return Ae()},get CachedInputFileSystem(){return Es()},get CloneBasenamePlugin(){return Ss()},get LogInfoPlugin(){return Ps()},get TsconfigPathsPlugin(){return Jt()},get forEachBail(){return Q()}})});import{URL as Tu}from"node:url";import{isBuiltin as Hs,registerHooks as Fu}from"node:module";import te from"node:path";import{fileURLToPath as Pe,pathToFileURL as Ls,URL as js}from"node:url";var Ts=Js(Fs(),1);import xs from"node:path";import{Buffer as wu}from"node:buffer";import{createHash as Ds}from"node:crypto";import Je from"node:fs";import U from"node:path";import{fileURLToPath as Su}from"node:url";function k(t,e){return Object.assign(new Error(e),{code:t})}function Ms(t){let e=Su(t),n=U.dirname(e),i=JSON.parse(Je.readFileSync(e,"utf8"));if(!He(i)||i.version!==1||typeof i.storeDir!="string")throw k("ERR_PNPM_LOADER_MANIFEST",`Invalid store manifest: ${e}`);let r=U.join(n,".pnpm-loader"),s=U.resolve(n,i.storeDir),o=Pu(i.packages,n,r),c=new Map,l=new Set([r]);for(let a of o.values())Eu(a,c,l,s);for(let a of c.keys())if(l.has(a))throw k("ERR_PNPM_LOADER_MANIFEST",`File is also a directory: ${a}`);let f=Au(o),u=$u({files:c,directories:l,virtualRoot:r});return{packages:o,owner:f,filesystem:u,files:c,virtualRoot:r}}function Au(t){let e=new Map([...t.values()].map(i=>[i.root,i])),n=new Map;return function(r){if(n.has(r))return n.get(r);let s=r;for(;!e.has(s)&&U.dirname(s)!==s;)s=U.dirname(s);let o=e.get(s);return n.set(r,o),o}}function Pu(t,e,n){if(!He(t))throw k("ERR_PNPM_LOADER_MANIFEST","The store manifest needs a packages object");let i=new Map,r=new Map;for(let[s,o]of Object.entries(t)){Ru(s,o);let c=Object.hasOwn(o,"files");if(c===Object.hasOwn(o,"root"))throw k("ERR_PNPM_LOADER_MANIFEST",`${s} must have either files or root`);let l=c?U.join(n,Ds("sha256").update(s).digest("hex")):U.resolve(e,o.root),f=r.get(l);if(!c&&q(n,l)||f&&(f.resolution!=="node"||o.resolution!=="node"))throw k("ERR_PNPM_LOADER_MANIFEST",`Conflicting package root: ${l}`);r.set(l,o),i.set(s,{...o,id:s,root:l,stored:c,dependencies:new Map(Object.entries(o.dependencies??{}))})}for(let s of i.values())for(let[o,c]of s.dependencies)if(!i.has(c))throw k("ERR_PNPM_LOADER_MANIFEST",`Unknown dependency ${o} -> ${c} in ${s.id}`);return i}function Ru(t,e){if(!He(e)||!He(e.dependencies??{})||Object.hasOwn(e,"files")&&!He(e.files)||Object.hasOwn(e,"root")&&typeof e.root!="string"||e.resolution!==void 0&&(e.resolution!=="node"||typeof e.root!="string"))throw k("ERR_PNPM_LOADER_MANIFEST",`Invalid package entry: ${t}`)}function He(t){return t!==null&&typeof t=="object"&&!Array.isArray(t)}function Eu(t,e,n,i){if(t.stored){n.add(t.root),Object.hasOwn(t.files,"package.json")||e.set(U.join(t.root,"package.json"),{source:wu.from("{}")});for(let[r,s]of Object.entries(t.files)){if(!vu(r)||typeof s!="string"||!/^[a-f0-9]{128}(?:-exec)?$/.test(s))throw k("ERR_PNPM_LOADER_MANIFEST",`Invalid store file ${t.id}/${r}`);let o=U.join(t.root,r);e.set(o,{blob:U.join(i,"files",s.slice(0,2),s.slice(2)),hash:s.slice(0,128)});for(let c=U.dirname(o);q(t.root,c);c=U.dirname(c))n.add(c)}}}function vu(t){return t.length>0&&!t.includes("\\")&&!t.includes("\0")&&!t.includes(":")&&t.split("/").every(e=>e!==""&&e!=="."&&e!==".."&&e!=="node_modules")}function q(t,e){return e===t||e.startsWith(`${t}${U.sep}`)}function $u({files:t,directories:e,virtualRoot:n}){let i=new Map;function r(c){let l=U.resolve(c);if(!q(n,l))return Je.statSync(c);if(!t.has(l)&&!e.has(l))throw Os(c);return{isFile:()=>t.has(l),isDirectory:()=>e.has(l),isSymbolicLink:()=>!1}}function s(c,l){let f=U.resolve(c);if(!q(n,f))return Je.readFileSync(c,l);let u=t.get(f);if(!u)throw Os(c);if(u.source)return l?u.source.toString(l):u.source;let a=Je.readFileSync(u.blob);if(Ds("sha512").update(a).digest("hex")!==u.hash)throw k("ERR_PNPM_LOADER_INTEGRITY",`Store file failed integrity verification: ${u.blob}`);return l?a.toString(l):a}function o(c){let l=U.resolve(c);if(i.has(l))return i.get(l);let f=s(c,"utf8"),u=JSON.parse(f.charCodeAt(0)===65279?f.slice(1):f);return q(n,l)&&i.set(l,u),u}return{statSync:r,readFileSync:s,readJsonSync:o}}function Os(t){return k("ENOENT",`No file in store manifest: ${t}`)}function ks(t){let e=new Map,n,i={resolveToUnqualified(r,s){let o=t.owner(xs.resolve(s)),c=o?.dependencies.get(r);if(c===void 0)throw k("ERR_PNPM_LOADER_UNDECLARED_DEPENDENCY",`Package ${o?.id??s} does not declare ${r}`);return n=t.packages.get(c),n.root}};return function(s,o,{conditions:c,filename:l}){let f=JSON.stringify(c),u=e.get(f);u||(u=Ts.default.ResolverFactory.createResolver({fileSystem:t.filesystem,useSyncFileSystemCalls:!0,conditionNames:c,extensions:[".js",".json",".node"],mainFields:["main"],mainFiles:["index"],fullySpecified:!c.includes("require"),symlinks:!1,pnpApi:i}),e.set(f,u)),n=t.owner(l??o);let a,d;if(u.resolve({},xs.dirname(o),s,{},(h,p,g)=>{d=h,a=g}),d)throw d;if(!a?.path)throw k("ERR_MODULE_NOT_FOUND",`Cannot resolve ${s} from ${o}`);if(n?.stored&&!q(n.root,a.path))throw k("ERR_PNPM_LOADER_PATH_ESCAPE",`Resolved ${s} escapes package ${n.id}`);return a}}function Ns(t){return Fu(Ou(t))}function Ou(t){let e=Ms(t),n=ks(e),i=new Map;return{resolve(r,s,o){if(!s.parentURL&&!Hs(r)){let d=xu(r,e,i);if(d)return d}if(Hs(r)||!s.parentURL?.startsWith("file:"))return o(r,s);let c=Pe(s.parentURL),l=e.owner(c);if(!l)return o(r,s);let f=Du(r,s);if(f===null||l.resolution==="node"&&(!f.filename||!q(e.virtualRoot,f.filename))||f.filename&&(!l.stored||!f.relative)&&!q(e.virtualRoot,f.filename))return o(r,s);if(l.stored&&f.relative&&!q(l.root,f.filename))throw k("ERR_PNPM_LOADER_PATH_ESCAPE",`Import ${r} escapes package ${l.id}`);let u=n(f.specifier,c,{conditions:s.conditions,filename:f.filename}),a=Ls(u.path);return a.search=f.search||u.query||"",a.hash=f.hash||u.fragment||"",q(e.virtualRoot,u.path)&&Kt(u.path,e,i),{url:a.href,shortCircuit:!0}},load(r,s,o){if(!r.startsWith("file:"))return o(r,s);let c=Pe(r);if(!q(e.virtualRoot,c))return o(r,s);let l=Kt(c,e,i);if(l==="json"&&!s.conditions.includes("require")&&s.importAttributes?.type!=="json")throw k("ERR_IMPORT_ATTRIBUTE_MISSING",`Module ${r} needs an import attribute of type json`);return Mu(l,s.importAttributes,r),{format:l,source:e.filesystem.readFileSync(c),shortCircuit:!0}}}}function Du(t,e){let i=e.conditions.includes("require")&&(t==="."||t==="..")||t.startsWith("./")||t.startsWith("../");if(te.isAbsolute(t)&&!t.startsWith("/")||e.conditions.includes("require")&&(i||te.isAbsolute(t))){let s=te.resolve(te.dirname(Pe(e.parentURL)),t);return{filename:s,relative:i,specifier:Cs(s)}}if(t.startsWith("file:")||t.startsWith("./")||t.startsWith("../")||t.startsWith("/")){let s=new js(t,e.parentURL),o=Pe(s);return{filename:o,relative:i,specifier:Cs(o),search:s.search,hash:s.hash}}if(t.includes(":"))return null;if(!t.startsWith("#")&&(t.includes("\\")||t.split("/").includes("..")))throw k("ERR_PNPM_LOADER_PATH_ESCAPE",`Invalid package subpath: ${t}`);return{specifier:t}}function Cs(t){return t.replaceAll("#","\0#").replaceAll("?","\0?")}function Kt(t,e,n){let i=te.extname(t);if(i===".mjs")return"module";if(i===".cjs")return"commonjs";if(i===".json")return"json";if(i!==".js"&&i!=="")throw k("ERR_PNPM_LOADER_UNSUPPORTED_FORMAT",`Cannot load ${i||"extensionless"} store file: ${t}`);let r=e.owner(t);if(!r)throw k("ENOENT",`No package in store manifest: ${t}`);for(let s=te.dirname(t);q(r.root,s);s=te.dirname(s)){let o=te.join(s,"package.json");if(e.files.has(o))return n.has(o)||n.set(o,e.filesystem.readJsonSync(o)),n.get(o).type==="module"?"module":"commonjs"}return"commonjs"}function Mu(t,e={},n){for(let[i,r]of Object.entries(e))if(i!=="type"||r!=="json")throw k("ERR_IMPORT_ATTRIBUTE_UNSUPPORTED",`Unsupported import attribute ${i}=${r} for ${n}`);if(t!=="json"&&e.type==="json")throw k("ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE",`Module ${n} is not JSON`)}function xu(t,e,n){let i=t.startsWith("file:")?new js(t):te.isAbsolute(t)?Ls(t):null;return!i||!e.files.has(Pe(i))?null:{url:i.href,format:Kt(Pe(i),e,n),shortCircuit:!0}}Ns(new Tu("./.pnpm-store.json",import.meta.url)); diff --git a/pnpm/crates/deps-restorer/src/cas.rs b/pnpm/crates/deps-restorer/src/cas.rs new file mode 100644 index 0000000000..2e90ba9977 --- /dev/null +++ b/pnpm/crates/deps-restorer/src/cas.rs @@ -0,0 +1,154 @@ +pub(crate) use bins::BinInstall; +pub(crate) use link::link_phase; + +use crate::{ + SkippedSnapshots, + linking::LinkPhaseInputs, + package_map::{PackageMapOptions, lockfile_to_package_map}, +}; +use pnpm_lockfile::PkgIdWithPatchHash; +use std::{collections::BTreeMap, io, path::Path}; +use url::Url; + +use pnpm_config::{ + CAS_LOADER_FILENAME as LOADER_FILENAME, CAS_MANIFEST_FILENAME as MANIFEST_FILENAME, + NodePackageMapType, +}; +use pnpm_store_dir::{StoreLoaderManifest as StoreManifest, StoreLoaderPackage as StorePackage}; + +mod bin_state; +mod bins; +mod link; + +const LOADER: &str = include_str!("cas-loader.mjs.inc"); + +pub(crate) fn write_installation( + inputs: &LinkPhaseInputs<'_>, + skipped: &SkippedSnapshots, +) -> io::Result<()> { + let manifest = build_manifest(inputs, skipped)?; + let root = inputs.ctx.workspace_root; + pnpm_fs::write_atomic(&root.join(MANIFEST_FILENAME), &serde_json::to_vec(&manifest)?)?; + write_file(&root.join(LOADER_FILENAME), LOADER.as_bytes())?; + bins::write_bins( + &bins::BinInstall { + trusted_importer_ids: inputs.projects.trusted_importer_ids, + config: inputs.ctx.config, + root, + importers: &inputs.graph.lockfile.importers, + }, + &manifest, + ) +} + +fn build_manifest( + inputs: &LinkPhaseInputs<'_>, + skipped: &SkippedSnapshots, +) -> io::Result { + let config = inputs.ctx.config; + let lockfile = inputs.graph.lockfile; + let selected = inputs.ctx + .select_loaded_snapshots(inputs.graph.lockfile.snapshots.as_ref()) + .expect("loaded linker selection"); + let base = Url::from_directory_path(&config.modules_dir) + .map_err(|()| io::Error::other("CAS modules directory must be absolute"))?; + let snapshots: std::collections::HashMap<_, _> = lockfile.snapshots + .iter() + .flatten() + .map(|(key, _)| (key.to_string(), key)) + .collect(); + let mut packages = BTreeMap::new(); + for (id, package) in package_map(inputs).packages { + let snapshot = snapshots.get(&id).copied(); + if snapshot.is_some_and(|key| skipped.contains(key)) { + continue; + } + let entry = store_package( + inputs, + (&id, package), + (&base, snapshot.map(|key| selected.contains_key(key))), + )?; + packages.insert(id, entry); + } + let ids: std::collections::HashSet<_> = packages.keys().cloned().collect(); + for package in packages.values_mut() { + package.dependencies.retain(|_, id| ids.contains(id)); + } + Ok(StoreManifest { version: 1, store_dir: config.store_dir.root().to_path_buf(), packages }) +} + +fn package_map(inputs: &LinkPhaseInputs<'_>) -> crate::package_map::PackageMap { + let config = inputs.ctx.config; + let lockfile = inputs.graph.lockfile; + lockfile_to_package_map( + lockfile, + &PackageMapOptions { + lockfile_dir: inputs.ctx.workspace_root, + modules_dir: &config.modules_dir, + package_map_type: NodePackageMapType::Standard, + layout: inputs.ctx.linker.layout, + project_manifests: inputs.projects.manifests, + }, + ) +} + +fn store_package( + inputs: &LinkPhaseInputs<'_>, + (id, package): (&str, crate::package_map::PackageMapPackage), + (base, materialized): (&Url, Option), +) -> io::Result { + let root = base + .join(&package.url) + .map_err(io::Error::other)? + .to_file_path() + .map_err(|()| io::Error::other("Invalid package directory URL"))?; + let mut entry = StorePackage { + root: Some(root), + files: None, + resolution: None, + dependencies: package.dependencies, + }; + if materialized == Some(true) { + entry.resolution = Some("node".to_string()); + entry.dependencies.clear(); + } else if materialized == Some(false) { + entry.files = Some(package_files(inputs, id)?); + entry.root = None; + } + Ok(entry) +} + +fn package_files(inputs: &LinkPhaseInputs<'_>, id: &str) -> io::Result> { + let key = PkgIdWithPatchHash::from(pnpm_deps_path::get_pkg_id_with_patch_hash(id).to_string()); + let paths = inputs.packages.cas_paths_by_pkg_id + .as_ref() + .and_then(|paths| paths.get(&key)) + .ok_or_else(|| io::Error::other(format!("Missing CAS files for {id}")))?; + let files_dir = inputs.ctx.config.store_dir.root().join("files"); + paths.cas_paths + .iter() + .map(|(name, path)| { + let relative = path + .strip_prefix(&files_dir) + .map_err(|_| { + io::Error::other(format!( + "{id} has non-CAS files; add it to nodeLinker.excluded", + )) + })?; + let hash = relative + .components() + .map(|part| part.as_os_str().to_string_lossy()) + .collect::(); + Ok((name.clone(), hash)) + }) + .collect() +} + +fn write_file(path: &Path, contents: &[u8]) -> io::Result<()> { + pnpm_fs::ensure_file(path, contents, None).map_err(io::Error::other) +} + +pub(crate) fn refresh_bins(inputs: &BinInstall<'_>) -> io::Result<()> { + let manifest = serde_json::from_slice(&std::fs::read(inputs.root.join(MANIFEST_FILENAME))?)?; + bins::write_bins(inputs, &manifest) +} diff --git a/pnpm/crates/deps-restorer/src/cas/bin_state.rs b/pnpm/crates/deps-restorer/src/cas/bin_state.rs new file mode 100644 index 0000000000..96049c9cb9 --- /dev/null +++ b/pnpm/crates/deps-restorer/src/cas/bin_state.rs @@ -0,0 +1,33 @@ +use super::write_file; +use pnpm_cmd_shim::{ + Host, PackageBinSource, get_bins_from_package_manifest, is_safe_bin_name, remove_bin, +}; +use std::{collections::BTreeSet, io, path::Path}; + +pub(super) fn reconcile_bins(sources: &[PackageBinSource], directory: &Path) -> io::Result<()> { + let current: BTreeSet<_> = sources + .iter() + .flat_map(|source| { + get_bins_from_package_manifest::(&source.manifest, &source.location) + .into_iter() + .map(|command| command.name) + }) + .collect(); + let state = directory + .parent() + .expect("bin directory has a parent") + .join("cas-bin-names.json"); + let previous: BTreeSet = match std::fs::read(&state) { + Ok(contents) => serde_json::from_slice(&contents)?, + Err(error) if error.kind() == io::ErrorKind::NotFound => BTreeSet::new(), + Err(error) => return Err(error), + }; + for name in previous.difference(¤t) { + if !is_safe_bin_name(name) { + return Err(io::Error::other(format!("Invalid recorded CAS bin name: {name}"))); + } + remove_bin(&directory.join(name))?; + } + std::fs::create_dir_all(state.parent().expect("bin state has a parent"))?; + write_file(&state, &serde_json::to_vec(¤t)?) +} diff --git a/pnpm/crates/deps-restorer/src/cas/bins.rs b/pnpm/crates/deps-restorer/src/cas/bins.rs new file mode 100644 index 0000000000..f6cb742e59 --- /dev/null +++ b/pnpm/crates/deps-restorer/src/cas/bins.rs @@ -0,0 +1,137 @@ +use super::{LOADER_FILENAME, StoreManifest, write_file}; +use pnpm_cmd_shim::{ + FsWalkFiles, Host, PackageBinSource, get_bins_from_package_manifest_with_walk, + link_bins_of_packages, +}; +use sha2::{Digest, Sha256}; +use std::{collections::HashMap, io, path::Path, sync::Arc}; +use url::Url; + +pub(crate) struct BinInstall<'a> { + pub config: &'a pnpm_config::Config, + pub root: &'a Path, + pub trusted_importer_ids: &'a std::collections::HashSet, + pub importers: &'a std::collections::HashMap, +} + +pub(super) fn write_bins(inputs: &BinInstall<'_>, manifest: &StoreManifest) -> io::Result<()> { + validate_importers(inputs)?; + let mut cached = HashMap::new(); + for importer in inputs.importers.keys() { + let Some(project) = manifest.packages.get(importer) else { continue }; + let mut sources = Vec::new(); + for id in project.dependencies.values() { + if !cached.contains_key(id) { + cached.insert(id.clone(), bin_source(inputs, manifest, id)?); + } + if let Some(source) = cached.get(id).and_then(Option::as_ref) { + sources.push(source.clone()); + } + } + let directory = inputs.root + .join(importer) + .join(inputs.config.modules_dir_name()) + .join(".bin"); + link_bins_of_packages::( + &sources, + &directory, + &crate::shim_link_options(inputs.config, pnpm_config::NodeLinker::Loaded), + ) + .map_err(io::Error::other)?; + super::bin_state::reconcile_bins(&sources, &directory)?; + } + Ok(()) +} + +fn validate_importers(inputs: &BinInstall<'_>) -> io::Result<()> { + for importer in inputs.importers.keys() { + if !inputs.trusted_importer_ids.contains(importer) { + crate::validate_importer_id(importer).map_err(io::Error::other)?; + } + } + Ok(()) +} + +fn bin_source( + inputs: &BinInstall<'_>, + manifest: &StoreManifest, + id: &str, +) -> io::Result> { + let Some(package) = manifest.packages.get(id) else { return Ok(None) }; + let hash = format!("{:x}", Sha256::digest(id.as_bytes())); + let root = package.root + .clone() + .unwrap_or_else(|| inputs.root.join(".pnpm-loader").join(&hash)); + let metadata = read_manifest(package, &manifest.store_dir, &root)?; + if package.resolution.as_deref() == Some("node") { + return Ok(Some(PackageBinSource::new(root, Arc::new(metadata)))); + } + let commands = package_bins(package, &metadata, &root); + if commands.is_empty() { + return Ok(None); + } + let entry_dir = inputs.config.install_state_dir.join("cas-bins").join(hash); + std::fs::create_dir_all(&entry_dir)?; + let mut metadata = metadata; + let mut bins = serde_json::Map::new(); + for (index, command) in commands.iter().enumerate() { + let filename = format!("{index}.mjs"); + write_entry(inputs.root, &entry_dir.join(&filename), &command.path)?; + bins.insert(command.name.clone(), filename.into()); + } + metadata["bin"] = bins.into(); + Ok(Some(PackageBinSource::new(entry_dir, Arc::new(metadata)))) +} + +fn package_bins( + package: &super::StorePackage, + metadata: &serde_json::Value, + root: &Path, +) -> Vec { + get_bins_from_package_manifest_with_walk(metadata, root, |directory| { + let Some(files) = &package.files else { + return Host::walk_files(directory).map(Iterator::collect::>); + }; + Ok(files + .keys() + .map(|name| root.join(name)) + .filter(|path| pnpm_fs::is_subdir(directory, path)) + .collect::>()) + }) +} + +fn write_entry(root: &Path, path: &Path, target: &Path) -> io::Result<()> { + let loader = file_url(&root.join(LOADER_FILENAME))?; + let target = serde_json::to_string(target)?; + let loader = serde_json::to_string(&loader)?; + let source = format!( + "#!/usr/bin/env node\nimport {loader};\nimport {{ runMain }} from 'node:module';\nprocess.argv[1] = {target};\nconst option = '--import=' + {loader};\nconst previous = process.env.NODE_OPTIONS ?? '';\nif (!previous.split(/\\s+/).includes(option)) process.env.NODE_OPTIONS = (previous + ' ' + option).trim();\nrunMain({target});\n", + ); + write_file(path, source.as_bytes()) +} + +fn file_url(path: &Path) -> io::Result { + Url::from_file_path(path) + .map(String::from) + .map_err(|()| io::Error::other(format!("Invalid file path: {}", path.display()))) +} + +fn read_manifest( + package: &super::StorePackage, + store: &Path, + root: &Path, +) -> io::Result { + let path = match &package.files { + Some(files) => { + let Some(hash) = files.get("package.json") else { + return Ok(serde_json::json!({})); + }; + pnpm_store_dir::loader_blob_path(&store.join("files"), hash)? + } + None => root.join("package.json"), + }; + pnpm_package_manifest::parse_manifest_bytes(&std::fs::read(path)?).map_err(io::Error::other) +} + +#[cfg(test)] +mod tests; diff --git a/pnpm/crates/deps-restorer/src/cas/bins/tests.rs b/pnpm/crates/deps-restorer/src/cas/bins/tests.rs new file mode 100644 index 0000000000..c0d3846523 --- /dev/null +++ b/pnpm/crates/deps-restorer/src/cas/bins/tests.rs @@ -0,0 +1,96 @@ +use super::{BinInstall, StoreManifest, write_bins}; +use std::collections::{BTreeMap, HashMap}; + +#[test] +fn stored_directories_bin_creates_commands_from_virtual_files() { + let temporary = tempfile::tempdir().unwrap(); + let root = temporary.path(); + let store = root.join("store"); + let digest = "a".repeat(128); + let blob = pnpm_store_dir::loader_blob_path(&store.join("files"), &digest).unwrap(); + std::fs::create_dir_all(blob.parent().unwrap()).unwrap(); + std::fs::write(&blob, r#"{"name":"tool","directories":{"bin":"./commands"}}"#).unwrap(); + let config = pnpm_config::Config { + modules_dir: root.join(".pnpm"), + install_state_dir: root.join(".pnpm"), + ..pnpm_config::Config::default() + }; + let manifest = StoreManifest { + version: 1, + store_dir: store, + packages: BTreeMap::from([ + ( + ".".into(), + super::super::StorePackage { + root: Some(root.to_path_buf()), + dependencies: BTreeMap::from([("tool".into(), "tool@1".into())]), + files: None, + resolution: None, + }, + ), + ( + "tool@1".into(), + super::super::StorePackage { + files: Some(BTreeMap::from([ + ("package.json".into(), digest.clone()), + ("commands/hello".into(), digest.clone()), + ("commands/nested/another".into(), digest), + ])), + root: None, + resolution: None, + dependencies: BTreeMap::new(), + }, + ), + ]), + }; + let importers = HashMap::from([(".".into(), pnpm_lockfile::ProjectSnapshot::default())]); + write_bins( + &BinInstall { + trusted_importer_ids: &std::collections::HashSet::new(), + config: &config, + root, + importers: &importers, + }, + &manifest, + ) + .unwrap(); + assert!(root.join(".pnpm/.bin/hello").is_file()); + assert!(root.join(".pnpm/.bin/another").is_file()); + assert!(!root.join(".pnpm-loader").exists()); +} + +#[test] +fn loaded_bins_reject_untrusted_importer_paths_before_writing() { + let temporary = tempfile::tempdir().unwrap(); + let root = temporary.path(); + let config = pnpm_config::Config::default(); + let manifest = + StoreManifest { version: 1, store_dir: root.join("store"), packages: BTreeMap::new() }; + let trusted = std::collections::HashSet::new(); + for importer in ["../outside", "/outside", "C:/outside", "packages/./child", "packages//child"] + { + let importers = + HashMap::from([(importer.to_string(), pnpm_lockfile::ProjectSnapshot::default())]); + let inputs = BinInstall { + config: &config, + root, + importers: &importers, + trusted_importer_ids: &trusted, + }; + assert!(write_bins(&inputs, &manifest).is_err(), "{importer}"); + } + let importers = + HashMap::from([("../configured".into(), pnpm_lockfile::ProjectSnapshot::default())]); + let trusted = std::collections::HashSet::from(["../configured".into()]); + write_bins( + &BinInstall { + config: &config, + root, + importers: &importers, + trusted_importer_ids: &trusted, + }, + &manifest, + ) + .unwrap(); + assert_eq!(std::fs::read_dir(root).unwrap().count(), 0); +} diff --git a/pnpm/crates/deps-restorer/src/cas/link.rs b/pnpm/crates/deps-restorer/src/cas/link.rs new file mode 100644 index 0000000000..5b63b5dd9f --- /dev/null +++ b/pnpm/crates/deps-restorer/src/cas/link.rs @@ -0,0 +1,29 @@ +use crate::{ + LinkVirtualStoreBins, SkippedSnapshots, + linking::{LinkPhaseError, LinkPhaseInputs, LinkPhaseOutput}, +}; + +pub(crate) fn link_phase( + inputs: &LinkPhaseInputs<'_>, + skipped: &SkippedSnapshots, +) -> Result { + let selected = inputs.ctx + .select_loaded_snapshots(inputs.graph.lockfile.snapshots.as_ref()) + .expect("loaded linker selection"); + let selected_keys: Vec<_> = selected.keys().cloned().collect(); + LinkVirtualStoreBins { + layout: inputs.ctx.linker.layout, + snapshots: Some(selected), + selected_snapshots: Some(&selected_keys), + packages: inputs.graph.lockfile.packages.as_ref(), + package_manifests: inputs.packages.package_manifests, + skipped, + link_options: inputs.ctx.linker.bin_options, + } + .run() + .map_err(LinkPhaseError::LinkVirtualStoreBins)?; + if !inputs.ctx.config.virtual_store_only { + crate::cas::write_installation(inputs, skipped).map_err(LinkPhaseError::Cas)?; + } + Ok(LinkPhaseOutput { hoisted_build_snapshots: Some(selected_keys), ..LinkPhaseOutput::empty() }) +} diff --git a/pnpm/crates/deps-restorer/src/create_virtual_store.rs b/pnpm/crates/deps-restorer/src/create_virtual_store.rs index 9f18a70100..bfdb7fc2b5 100644 --- a/pnpm/crates/deps-restorer/src/create_virtual_store.rs +++ b/pnpm/crates/deps-restorer/src/create_virtual_store.rs @@ -1,3 +1,4 @@ +pub(crate) mod cas; pub(crate) use cache_keys::{dir_clone_cacheable, package_content_changed}; pub use pnpm_package_manifest::requires_build_from_cas_paths; @@ -261,6 +262,14 @@ pub struct CreateVirtualStore<'a> { /// Error type of [`CreateVirtualStore`]. #[derive(Debug, Display, Error, Diagnostic)] pub enum CreateVirtualStoreError { + #[display("Cannot install with nodeLinker.type=loaded: {message}")] + #[diagnostic(code(ERR_PNPM_CAS_CONFIGURATION))] + CasConfiguration { message: &'static str }, + #[display( + "Package {package} needs materialization with the loaded linker. Add its name to nodeLinker.excluded." + )] + #[diagnostic(code(ERR_PNPM_CAS_REQUIRES_MATERIALIZATION))] + CasRequiresMaterialization { package: String }, #[diagnostic(transparent)] InstallPackageBySnapshot(#[error(source)] InstallPackageBySnapshotError), diff --git a/pnpm/crates/deps-restorer/src/create_virtual_store/cas.rs b/pnpm/crates/deps-restorer/src/create_virtual_store/cas.rs new file mode 100644 index 0000000000..c027847a0a --- /dev/null +++ b/pnpm/crates/deps-restorer/src/create_virtual_store/cas.rs @@ -0,0 +1,111 @@ +use super::{CreateVirtualStore, CreateVirtualStoreError, CreateVirtualStoreOutput}; +use pnpm_config::NodeLinker; +use pnpm_lockfile::{PackageKey, SnapshotEntry}; +use pnpm_reporter::Reporter; +use std::collections::{HashMap, HashSet}; + +impl CreateVirtualStore<'_> { + pub async fn run( + mut self, + ) -> Result { + if self.ctx.linker.kind == NodeLinker::Loaded { + return Box::pin(self.run_cas::()).await; + } + self.run_inner::().await + } + + pub(super) async fn run_cas( + self, + ) -> Result { + validate_config(self.ctx.config)?; + let selected = self.ctx + .select_loaded_snapshots(self.entries.snapshots) + .expect("loaded linker selection"); + let fetch_context = crate::InstallContext { + linker: crate::ModuleLinkerContext { kind: NodeLinker::Hoisted, ..self.ctx.linker }, + ..self.ctx.clone() + }; + let materialize_context = crate::InstallContext { + linker: crate::ModuleLinkerContext { kind: NodeLinker::Isolated, ..self.ctx.linker }, + ..self.ctx.clone() + }; + let mut installer = CreateVirtualStore { ctx: &fetch_context, ..self }; + let fetched = installer.run_inner::().await?; + validate_cas_builds(&fetched, selected, installer.ctx.config)?; + installer.ctx = &materialize_context; + installer.entries.snapshots = (!selected.is_empty()).then_some(selected); + let mut materialized = installer.run_inner::().await?; + materialized.cas_paths_by_pkg_id = fetched.cas_paths_by_pkg_id; + materialized.package_manifests.extend(fetched.package_manifests); + materialized.fetch_failed.extend(fetched.fetch_failed); + Ok(materialized) + } +} + +pub(crate) fn materialized_snapshots( + config: &pnpm_config::Config, + snapshots: Option<&HashMap>, +) -> HashMap { + let Some(snapshots) = snapshots else { return HashMap::new() }; + let mut pending: Vec<_> = snapshots + .keys() + .filter(|key| { + config.node_linker_excluded.contains(&key.name.to_string()) + || crate::snapshot_has_patch(key) + }) + .cloned() + .collect(); + let mut selected = HashSet::new(); + while let Some(key) = pending.pop() { + if !selected.insert(key.clone()) { + continue; + } + let Some(snapshot) = snapshots.get(&key) else { continue }; + pending.extend( + snapshot.dependencies + .iter() + .flatten() + .chain(snapshot.optional_dependencies.iter().flatten()) + .filter_map(|(alias, reference)| reference.resolve(alias)), + ); + } + snapshots + .iter() + .filter(|(key, _)| selected.contains(*key)) + .map(|(key, snapshot)| (key.clone(), snapshot.clone())) + .collect() +} + +fn validate_cas_builds( + fetched: &CreateVirtualStoreOutput, + selected: &HashMap, + config: &pnpm_config::Config, +) -> Result<(), CreateVirtualStoreError> { + if config.ignore_scripts { + return Ok(()); + } + for (key, requires_build) in &fetched.requires_build_by_snapshot { + if *requires_build && !selected.contains_key(key) { + return Err(CreateVirtualStoreError::CasRequiresMaterialization { + package: key.name.to_string(), + }); + } + } + Ok(()) +} + +fn validate_config(config: &pnpm_config::Config) -> Result<(), CreateVirtualStoreError> { + let message = if !config.enable_global_virtual_store { + Some("CAS opt-outs require the global virtual store") + } else if !config.symlink { + Some("CAS opt-outs require symlink=true for their dependency trees") + } else if config.node_experimental_package_map { + Some("nodeExperimentalPackageMap cannot be combined with the CAS loader") + } else { + None + }; + match message { + Some(message) => Err(CreateVirtualStoreError::CasConfiguration { message }), + None => Ok(()), + } +} diff --git a/pnpm/crates/deps-restorer/src/create_virtual_store/pipeline.rs b/pnpm/crates/deps-restorer/src/create_virtual_store/pipeline.rs index e1797ea4e0..fbf2d36da1 100644 --- a/pnpm/crates/deps-restorer/src/create_virtual_store/pipeline.rs +++ b/pnpm/crates/deps-restorer/src/create_virtual_store/pipeline.rs @@ -25,8 +25,8 @@ impl<'a> CreateVirtualStore<'a> { /// recovered from `index.db` for the warm-batch slots — the /// bin linker uses these to avoid re-reading `package.json` per /// child. See [`PackageManifests`](crate::create_virtual_store::PackageManifests). - pub async fn run( - mut self, + pub(super) async fn run_inner( + &mut self, ) -> Result { let Some(wanted) = self.wanted()? else { return Ok(nothing_to_materialize(self.is_hoisted())); @@ -329,7 +329,7 @@ impl<'a> CreateVirtualStore<'a> { template: LinkSlotsParallel { import: crate::PackageImportOptions::from_config( config, - self.ctx.logged_methods, + self.ctx.caches.logged_methods, self.ctx.requester, ), link: crate::VirtualStoreLinkOptions { diff --git a/pnpm/crates/deps-restorer/src/create_virtual_store/tests.rs b/pnpm/crates/deps-restorer/src/create_virtual_store/tests.rs index 0a1bc1fd76..53cec379ce 100644 --- a/pnpm/crates/deps-restorer/src/create_virtual_store/tests.rs +++ b/pnpm/crates/deps-restorer/src/create_virtual_store/tests.rs @@ -224,6 +224,13 @@ impl SeededStoreInstall { supported_architectures: None, }, ctx: &crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: &logged_methods, + git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: None, + }, + linker: crate::ModuleLinkerContext { layout: &layout, kind: NodeLinker::Isolated, @@ -234,10 +241,6 @@ impl SeededStoreInstall { requester: &requester, allow_build_policy: &allow_build_policy, - - logged_methods: &logged_methods, - git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), - dir_clone_cache: None, }, entries: LockfileEntries { diff --git a/pnpm/crates/deps-restorer/src/create_virtual_store/tests/installation.rs b/pnpm/crates/deps-restorer/src/create_virtual_store/tests/installation.rs index d260c4f6ed..60a9eb0d02 100644 --- a/pnpm/crates/deps-restorer/src/create_virtual_store/tests/installation.rs +++ b/pnpm/crates/deps-restorer/src/create_virtual_store/tests/installation.rs @@ -127,6 +127,13 @@ async fn shared_store_context_materializes_a_warm_package() { supported_architectures: None, }, ctx: &crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: &logged_methods, + git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: None, + }, + linker: crate::ModuleLinkerContext { layout: &layout, kind: NodeLinker::Isolated, @@ -137,10 +144,6 @@ async fn shared_store_context_materializes_a_warm_package() { requester: &requester, allow_build_policy: &allow_build_policy, - - logged_methods: &logged_methods, - git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), - dir_clone_cache: None, }, entries: LockfileEntries { packages: Some(&packages), snapshots: Some(&snapshots) }, diff --git a/pnpm/crates/deps-restorer/src/create_virtual_store/tests/optional_progress.rs b/pnpm/crates/deps-restorer/src/create_virtual_store/tests/optional_progress.rs index 75ff8cc6dd..14e0ea6a09 100644 --- a/pnpm/crates/deps-restorer/src/create_virtual_store/tests/optional_progress.rs +++ b/pnpm/crates/deps-restorer/src/create_virtual_store/tests/optional_progress.rs @@ -95,6 +95,13 @@ async fn install_unfetchable_optional_dependency(progress_reported: &SharedRepor supported_architectures: None, }, ctx: &crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: &logged_methods, + git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: None, + }, + linker: crate::ModuleLinkerContext { layout: &layout, kind: NodeLinker::Isolated, @@ -105,10 +112,6 @@ async fn install_unfetchable_optional_dependency(progress_reported: &SharedRepor requester: &requester, allow_build_policy: &allow_build_policy, - - logged_methods: &logged_methods, - git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), - dir_clone_cache: None, }, entries: LockfileEntries { packages: Some(&packages), snapshots: Some(&snapshots) }, diff --git a/pnpm/crates/deps-restorer/src/create_virtual_store/tests/store.rs b/pnpm/crates/deps-restorer/src/create_virtual_store/tests/store.rs index 093c06ff2a..14ef0a0656 100644 --- a/pnpm/crates/deps-restorer/src/create_virtual_store/tests/store.rs +++ b/pnpm/crates/deps-restorer/src/create_virtual_store/tests/store.rs @@ -120,6 +120,13 @@ async fn cold_batch_links_slots_in_parallel() { supported_architectures: None, }, ctx: &crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: &logged_methods, + git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: None, + }, + linker: crate::ModuleLinkerContext { layout: &layout, kind: NodeLinker::Isolated, @@ -130,10 +137,6 @@ async fn cold_batch_links_slots_in_parallel() { requester: &requester, allow_build_policy: &allow_build_policy, - - logged_methods: &logged_methods, - git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), - dir_clone_cache: None, }, entries: LockfileEntries { packages: Some(&packages), snapshots: Some(&snapshots) }, @@ -322,6 +325,13 @@ async fn gvs_link_pass_materializes_shared_slot_once() { supported_architectures: None, }, ctx: &crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: &logged_methods, + git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: None, + }, + linker: crate::ModuleLinkerContext { layout: &layout, kind: NodeLinker::Isolated, @@ -332,10 +342,6 @@ async fn gvs_link_pass_materializes_shared_slot_once() { requester: &requester, allow_build_policy: &allow_build_policy, - - logged_methods: &logged_methods, - git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), - dir_clone_cache: None, }, entries: LockfileEntries { packages: Some(&packages), snapshots: Some(&snapshots) }, diff --git a/pnpm/crates/deps-restorer/src/install_context.rs b/pnpm/crates/deps-restorer/src/install_context.rs index 5052a2c255..40f2597a58 100644 --- a/pnpm/crates/deps-restorer/src/install_context.rs +++ b/pnpm/crates/deps-restorer/src/install_context.rs @@ -1,6 +1,11 @@ use crate::AllowBuildPolicy; use pnpm_config::{Config, NodeLinker}; -use std::{path::Path, sync::atomic::AtomicU8}; +use pnpm_lockfile::{PackageKey, SnapshotEntry}; +use std::{ + collections::HashMap, + path::Path, + sync::{Arc, OnceLock, atomic::AtomicU8}, +}; /// What every phase of one install reads and none of them changes. /// @@ -12,8 +17,10 @@ use std::{path::Path, sync::atomic::AtomicU8}; /// Only values fixed for the whole install belong here. The skip set is /// the near miss: phases mutate it between one another, so it stays a /// parameter of its own. +#[derive(Clone)] pub struct InstallContext<'a> { pub linker: crate::ModuleLinkerContext<'a>, + pub caches: InstallCaches<'a>, pub config: &'static Config, /// Install root — the directory containing `pnpm-lock.yaml`. For a /// real workspace this is the workspace root; for a single project, @@ -25,6 +32,12 @@ pub struct InstallContext<'a> { /// The `allowBuilds` gate, shared by the fetch phase's git fetcher /// and the build phase's lifecycle scripts. pub allow_build_policy: &'a AllowBuildPolicy, +} + +#[derive(Clone)] +pub struct InstallCaches<'a> { + /// The immutable materialized selection shared by all phases and context clones. + pub materialized_graph: Arc>>, /// Install-scoped dedupe state for `pnpm:package-import-method`. /// See `link_file::log_method_once`. pub logged_methods: &'a AtomicU8, @@ -35,6 +48,18 @@ pub struct InstallContext<'a> { } impl InstallContext<'_> { + #[must_use] + pub fn select_loaded_snapshots( + &self, + snapshots: Option<&HashMap>, + ) -> Option<&HashMap> { + (self.config.node_linker == NodeLinker::Loaded).then(|| { + self.caches.materialized_graph.get_or_init(|| { + crate::create_virtual_store::cas::materialized_snapshots(self.config, snapshots) + }) + }) + } + /// Whether this install writes package directories into the project /// trees rather than into a virtual store. #[must_use] diff --git a/pnpm/crates/deps-restorer/src/install_frozen_lockfile.rs b/pnpm/crates/deps-restorer/src/install_frozen_lockfile.rs index b33cd844e1..1d74c7702e 100644 --- a/pnpm/crates/deps-restorer/src/install_frozen_lockfile.rs +++ b/pnpm/crates/deps-restorer/src/install_frozen_lockfile.rs @@ -261,6 +261,7 @@ fn build_extra_env( workspace_root: &std::path::Path, ) -> HashMap { let mut extra_env = config.extra_env_with_node_options(); + config.add_cas_loader_env(workspace_root, &mut extra_env); if matches!(node_linker, NodeLinker::Pnp) { let node_options = extra_env.get("NODE_OPTIONS").map(String::as_str); extra_env.insert( @@ -330,6 +331,13 @@ impl<'a> InstallFrozenLockfile<'a> { verification_override: Option>, ) -> Result { let ctx = crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: self.logged_methods, + git_source_cache: &plan.git_source_cache, + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: plan.dir_clone_cache.as_ref(), + }, + linker: crate::ModuleLinkerContext { layout: &plan.layout, kind: self.platform.node_linker, @@ -340,10 +348,6 @@ impl<'a> InstallFrozenLockfile<'a> { requester: self.projects.requester, allow_build_policy, - - logged_methods: self.logged_methods, - git_source_cache: &plan.git_source_cache, - dir_clone_cache: plan.dir_clone_cache.as_ref(), }; // Spawn the batched store-index writer here so it lives diff --git a/pnpm/crates/deps-restorer/src/install_frozen_lockfile/build_phase.rs b/pnpm/crates/deps-restorer/src/install_frozen_lockfile/build_phase.rs index dfc0b26be2..7266ddb9ea 100644 --- a/pnpm/crates/deps-restorer/src/install_frozen_lockfile/build_phase.rs +++ b/pnpm/crates/deps-restorer/src/install_frozen_lockfile/build_phase.rs @@ -21,6 +21,9 @@ mod tests; /// a failed build reports identically regardless of which path ran it. #[derive(Debug, Display, Error, Diagnostic)] pub enum BuildPhaseError { + #[display("Failed to refresh CAS command shims: {_0}")] + #[diagnostic(code(ERR_PNPM_CAS_BINS))] + CasBins(#[error(source)] std::io::Error), /// `patchedDependencies` couldn't be resolved from /// `pnpm-workspace.yaml`. #[diagnostic(transparent)] @@ -160,15 +163,29 @@ pub fn run_build_phase( strict_dep_builds: config.strict_dep_builds, })); + finish_bin_links(inputs, &build_output, &bin_state)?; + Ok(build_output) +} + +fn finish_bin_links( + inputs: &BuildPhaseInputs<'_>, + build_output: &crate::BuildModulesOutput, + bin_state: &crate::build_options::BuildBinState, +) -> Result<(), BuildPhaseError> { + let config = inputs.policy.config; // `virtual_store_only` links no importer bins, so there is nothing // for the pass below to re-resolve. Dependency *build* scripts still // ran above — only the importer-facing linking stops, matching // `pnpm fetch`. if config.virtual_store_only { - return Ok(build_output); + return Ok(()); } - finish_dependency_bin_links(inputs, &build_output, &bin_state)?; + if config.node_linker == pnpm_config::NodeLinker::Loaded { + finish_cas_bin_links(inputs, build_output, bin_state)?; + return Ok(()); + } + finish_dependency_bin_links(inputs, build_output, bin_state)?; // Post-`BuildModules` per-importer top-level bin link // (pnpm/pacquet#342). Resolves direct-over-hoisted precedence and @@ -186,7 +203,31 @@ pub fn run_build_phase( )?; } - Ok(build_output) + Ok(()) +} + +fn finish_cas_bin_links( + inputs: &BuildPhaseInputs<'_>, + build_output: &crate::BuildModulesOutput, + bin_state: &crate::build_options::BuildBinState, +) -> Result<(), BuildPhaseError> { + let config = inputs.policy.config; + let selected = inputs.graph.loaded_snapshots.expect("loaded linker selection"); + let native_inputs = BuildPhaseInputs { + graph: crate::BuildPhaseGraph { snapshots: Some(selected), ..inputs.graph }, + ..*inputs + }; + finish_dependency_bin_links(&native_inputs, build_output, bin_state)?; + if build_output.mutated_slots { + crate::cas::refresh_bins(&crate::cas::BinInstall { + config, + root: inputs.directories.workspace_root, + importers: inputs.graph.importers, + trusted_importer_ids: inputs.graph.trusted_importer_ids, + }) + .map_err(BuildPhaseError::CasBins)?; + } + Ok(()) } fn finish_dependency_bin_links( diff --git a/pnpm/crates/deps-restorer/src/install_frozen_lockfile/build_phase/tests.rs b/pnpm/crates/deps-restorer/src/install_frozen_lockfile/build_phase/tests.rs index dc4e505206..541a6eb395 100644 --- a/pnpm/crates/deps-restorer/src/install_frozen_lockfile/build_phase/tests.rs +++ b/pnpm/crates/deps-restorer/src/install_frozen_lockfile/build_phase/tests.rs @@ -65,6 +65,8 @@ async fn build_generated_peer_bin_is_considered_without_lockfile_has_bin() { link_options: &LinkBinsOptions::default(), }, graph: crate::BuildPhaseGraph { + trusted_importer_ids: &std::collections::HashSet::new(), + loaded_snapshots: None, snapshots: Some(&snapshots), packages: Some(&packages), importers: &HashMap::new(), @@ -140,6 +142,8 @@ async fn directory_peer_bin_is_considered_without_lockfile_has_bin() { link_options: &LinkBinsOptions::default(), }, graph: crate::BuildPhaseGraph { + trusted_importer_ids: &std::collections::HashSet::new(), + loaded_snapshots: None, snapshots: Some(&snapshots), packages: Some(&packages), importers: &HashMap::new(), @@ -251,6 +255,8 @@ async fn ignored_scripts_fast_path_defers_only_materialized_snapshots() { link_options: &LinkBinsOptions::default(), }, graph: crate::BuildPhaseGraph { + trusted_importer_ids: &std::collections::HashSet::new(), + loaded_snapshots: None, snapshots: None, packages: None, importers: &importers, diff --git a/pnpm/crates/deps-restorer/src/install_frozen_lockfile/materialization.rs b/pnpm/crates/deps-restorer/src/install_frozen_lockfile/materialization.rs index bf00146787..6647eda8ad 100644 --- a/pnpm/crates/deps-restorer/src/install_frozen_lockfile/materialization.rs +++ b/pnpm/crates/deps-restorer/src/install_frozen_lockfile/materialization.rs @@ -52,6 +52,8 @@ impl<'a> InstallFrozenLockfile<'a> { cache: phase.fetched.build_cache(engine_name.as_deref(), phase.store_index_writer), directories: build_directories(install.projects.workspace_root, ctx, phase.linked), graph: crate::BuildPhaseGraph { + trusted_importer_ids: &install.importer_sets().0, + loaded_snapshots: ctx.select_loaded_snapshots(snapshots), snapshots, packages, importers: &install.lockfiles.wanted.importers, @@ -339,7 +341,7 @@ fn build_directories<'a>( hoisted_pkg_roots_by_key: linked.hoisted_pkg_roots_by_key.as_ref(), is_hoisted: ctx.is_hoisted(), publicly_hoisted_for_post_build: &linked.publicly_hoisted_for_post_build, - logged_methods: ctx.logged_methods, + logged_methods: ctx.caches.logged_methods, link_options: ctx.linker.bin_options, } } diff --git a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/fetch.rs b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/fetch.rs index 8c955fd7c2..5c2a0490c6 100644 --- a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/fetch.rs +++ b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/fetch.rs @@ -358,7 +358,7 @@ impl InstallPackageBySnapshot<'_> { }, import: crate::PackageImportOptions::from_config( config, - self.ctx.logged_methods, + self.ctx.caches.logged_methods, self.ctx.requester, ), source: crate::SlotImportSource { diff --git a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/fetch/sources.rs b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/fetch/sources.rs index 6f44269e9d..e92d84c01b 100644 --- a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/fetch/sources.rs +++ b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/fetch/sources.rs @@ -113,7 +113,7 @@ impl InstallPackageBySnapshot<'_> { let GitFetchOutput { cas_paths, built: _built } = GitFetcher { scripts: prepare_script_options(config, exec_scripts_prepend_node_path(config)), source: pnpm_git_fetcher::GitSource { - cache: self.ctx.git_source_cache, + cache: self.ctx.caches.git_source_cache, repo: &git_resolution.repo, commit: &git_resolution.commit, path: git_resolution.path.as_deref(), diff --git a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests.rs b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests.rs index c3e8ecd345..449938a973 100644 --- a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests.rs +++ b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests.rs @@ -133,6 +133,13 @@ async fn run_snapshot_install_with_session( custom_fetcher_session: Some(session), }, ctx: &crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: &logged_methods, + git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: None, + }, + linker: crate::ModuleLinkerContext { layout: &layout, kind: pnpm_config::NodeLinker::Hoisted, @@ -143,10 +150,6 @@ async fn run_snapshot_install_with_session( requester: "/project", allow_build_policy: &allow_build_policy, - - logged_methods: &logged_methods, - git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), - dir_clone_cache: None, }, skipped: &skipped, diff --git a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/installation.rs b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/installation.rs index bf76323881..2003914258 100644 --- a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/installation.rs +++ b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/installation.rs @@ -201,6 +201,13 @@ async fn cold_batch_falls_back_when_prefetch_failed() { custom_fetcher_session: None, }, ctx: &crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: &logged_methods, + git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: None, + }, + linker: crate::ModuleLinkerContext { layout: &layout, kind: pnpm_config::NodeLinker::Hoisted, @@ -211,10 +218,6 @@ async fn cold_batch_falls_back_when_prefetch_failed() { requester: "/project", allow_build_policy: &allow_build_policy, - - logged_methods: &logged_methods, - git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), - dir_clone_cache: None, }, skipped: &skipped, diff --git a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/runtimes.rs b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/runtimes.rs index d6acc26d7f..a9039ce3fe 100644 --- a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/runtimes.rs +++ b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/runtimes.rs @@ -268,6 +268,13 @@ async fn installing_a_runtime_persists_the_synthesized_manifest_into_the_store_i custom_fetcher_session: None, }, ctx: &crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: &logged_methods, + git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: None, + }, + linker: crate::ModuleLinkerContext { layout: &layout, kind: pnpm_config::NodeLinker::Hoisted, @@ -278,10 +285,6 @@ async fn installing_a_runtime_persists_the_synthesized_manifest_into_the_store_i requester: "/project", allow_build_policy: &allow_build_policy, - - logged_methods: &logged_methods, - git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), - dir_clone_cache: None, }, skipped: &skipped, @@ -349,6 +352,13 @@ async fn installing_a_runtime_persists_the_synthesized_manifest_into_the_store_i custom_fetcher_session: None, }, ctx: &crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: &warm_logged, + git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: None, + }, + linker: crate::ModuleLinkerContext { layout: &layout, kind: pnpm_config::NodeLinker::Hoisted, @@ -359,13 +369,9 @@ async fn installing_a_runtime_persists_the_synthesized_manifest_into_the_store_i requester: "/project", allow_build_policy: &allow_build_policy, - // Deliberately not the cold run's counter: the assertion // below is that the warm path logs its import method on its // own. - logged_methods: &warm_logged, - git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), - dir_clone_cache: None, }, skipped: &skipped, diff --git a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/store.rs b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/store.rs index 7a502e773f..f0b7bcd2bc 100644 --- a/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/store.rs +++ b/pnpm/crates/deps-restorer/src/install_package_by_snapshot/tests/store.rs @@ -71,6 +71,13 @@ async fn cold_batch_reuses_in_flight_prefetch_from_mem_cache() { custom_fetcher_session: None, }, ctx: &crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: &logged_methods, + git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: None, + }, + linker: crate::ModuleLinkerContext { layout: &layout, kind: pnpm_config::NodeLinker::Hoisted, @@ -81,10 +88,6 @@ async fn cold_batch_reuses_in_flight_prefetch_from_mem_cache() { requester: "/project", allow_build_policy: &allow_build_policy, - - logged_methods: &logged_methods, - git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), - dir_clone_cache: None, }, skipped: &skipped, @@ -164,6 +167,13 @@ async fn without_mem_cache_skips_coordination_and_downloads() { custom_fetcher_session: None, }, ctx: &crate::InstallContext { + caches: crate::InstallCaches { + logged_methods: &logged_methods, + git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), + materialized_graph: std::sync::Arc::default(), + dir_clone_cache: None, + }, + linker: crate::ModuleLinkerContext { layout: &layout, kind: pnpm_config::NodeLinker::Hoisted, @@ -174,10 +184,6 @@ async fn without_mem_cache_skips_coordination_and_downloads() { requester: "/project", allow_build_policy: &allow_build_policy, - - logged_methods: &logged_methods, - git_source_cache: &pnpm_git_fetcher::GitSourceCache::default(), - dir_clone_cache: None, }, skipped: &skipped, diff --git a/pnpm/crates/deps-restorer/src/install_package_from_registry/tests.rs b/pnpm/crates/deps-restorer/src/install_package_from_registry/tests.rs index d07182f51d..d520be86d1 100644 --- a/pnpm/crates/deps-restorer/src/install_package_from_registry/tests.rs +++ b/pnpm/crates/deps-restorer/src/install_package_from_registry/tests.rs @@ -31,6 +31,7 @@ fn create_config( cache_dir: &Path, ) -> Config { Config { + node_linker_excluded: Vec::new(), tools: std::collections::BTreeMap::new(), indexes_by_ecosystem: std::collections::BTreeMap::new(), network_concurrency_by_registry: std::collections::BTreeMap::new(), @@ -82,6 +83,7 @@ fn create_config( symlink: false, install_state_dir: virtual_store_dir.to_path_buf(), configured_virtual_store_dir: None, + loaded_layout_defaults: None, enable_global_virtual_store: false, global_shims: Default::default(), virtual_store_only: false, diff --git a/pnpm/crates/deps-restorer/src/lib.rs b/pnpm/crates/deps-restorer/src/lib.rs index 4c9240de52..d03c83a20a 100644 --- a/pnpm/crates/deps-restorer/src/lib.rs +++ b/pnpm/crates/deps-restorer/src/lib.rs @@ -5,6 +5,7 @@ pub(crate) use std::process; pub mod build_graph; pub mod build_modules; +pub mod cas; pub mod create_symlink_layout; pub mod create_virtual_dir_by_snapshot; pub mod create_virtual_store; diff --git a/pnpm/crates/deps-restorer/src/linking.rs b/pnpm/crates/deps-restorer/src/linking.rs index 1e0d53ea1d..53ff940898 100644 --- a/pnpm/crates/deps-restorer/src/linking.rs +++ b/pnpm/crates/deps-restorer/src/linking.rs @@ -60,6 +60,9 @@ pub enum LinkPhaseError { #[display("failed to write PnP loader: {_0}")] #[diagnostic(code(ERR_PNPM_PACKAGE_MANAGER_WRITE_PNP_FILE))] WritePnpFile(#[error(source)] crate::WritePnpFileError), + #[display("Failed to write CAS installation: {_0}")] + #[diagnostic(code(ERR_PNPM_CAS_INSTALL))] + Cas(#[error(source)] std::io::Error), } impl From for LinkPhaseError { @@ -139,7 +142,7 @@ impl LinkPhaseOutput { } /// The result of a run that materialized nothing. - fn empty() -> Self { + pub(crate) fn empty() -> Self { LinkPhaseOutput { hoisted_dependencies: crate::HoistedDependencies::new(), hoisted_locations: BTreeMap::new(), @@ -171,6 +174,9 @@ pub fn run_link_phase( inputs: LinkPhaseInputs<'_>, skipped: &mut SkippedSnapshots, ) -> Result { + if inputs.ctx.linker.kind == NodeLinker::Loaded { + return crate::cas::link_phase(&inputs, skipped); + } let hoist = plan_hoist(&inputs, skipped); // `nodeLinker: hoisted` writes no virtual store — `CreateVirtualStore` @@ -450,10 +456,10 @@ fn link_hoisted_projects( host_node: inputs.host_node, supported_architectures: inputs.supported_architectures, materialization: crate::HoistedMaterialization { - logged_methods: inputs.ctx.logged_methods, + logged_methods: inputs.ctx.caches.logged_methods, requester: inputs.ctx.requester, requires_build_by_snapshot: inputs.packages.requires_build_by_snapshot, - dir_clone_cache: inputs.ctx.dir_clone_cache, + dir_clone_cache: inputs.ctx.caches.dir_clone_cache, }, }, skipped, diff --git a/pnpm/crates/deps-restorer/src/package_map.rs b/pnpm/crates/deps-restorer/src/package_map.rs index c8f375170b..83845fc0af 100644 --- a/pnpm/crates/deps-restorer/src/package_map.rs +++ b/pnpm/crates/deps-restorer/src/package_map.rs @@ -30,13 +30,13 @@ pub const PACKAGE_MAP_FILENAME: &str = ".package-map.json"; #[derive(Debug, PartialEq, Eq, Serialize)] pub struct PackageMap { - packages: BTreeMap, + pub(crate) packages: BTreeMap, } #[derive(Debug, PartialEq, Eq, Serialize)] pub struct PackageMapPackage { - url: String, - dependencies: BTreeMap, + pub(crate) url: String, + pub(crate) dependencies: BTreeMap, } #[derive(Debug, derive_more::Display, derive_more::Error)] diff --git a/pnpm/crates/deps-restorer/src/phase_options.rs b/pnpm/crates/deps-restorer/src/phase_options.rs index 56370394ba..6fe21bcd25 100644 --- a/pnpm/crates/deps-restorer/src/phase_options.rs +++ b/pnpm/crates/deps-restorer/src/phase_options.rs @@ -148,6 +148,8 @@ pub struct HoistedLinkGraph<'a> { #[derive(Clone, Copy)] pub struct BuildPhaseGraph<'a> { + pub trusted_importer_ids: &'a std::collections::HashSet, + pub loaded_snapshots: Option<&'a HashMap>, pub snapshots: Option<&'a HashMap>, pub packages: Option<&'a HashMap>, pub importers: &'a HashMap, diff --git a/pnpm/crates/modules-yaml/src/lib.rs b/pnpm/crates/modules-yaml/src/lib.rs index 9ae332a079..eb1a82339e 100644 --- a/pnpm/crates/modules-yaml/src/lib.rs +++ b/pnpm/crates/modules-yaml/src/lib.rs @@ -255,6 +255,7 @@ pub enum NodeLinker { Hoisted, Isolated, Pnp, + Loaded, } /// Pinned identifier for the `node_modules` layout pacquet emits. diff --git a/pnpm/crates/napi/src/install/overlay.rs b/pnpm/crates/napi/src/install/overlay.rs index 59e4b8417e..b135de08ff 100644 --- a/pnpm/crates/napi/src/install/overlay.rs +++ b/pnpm/crates/napi/src/install/overlay.rs @@ -306,6 +306,7 @@ fn parse_node_linker(value: &str) -> Option { "hoisted" => Some(pnpm_config::NodeLinker::Hoisted), "isolated" => Some(pnpm_config::NodeLinker::Isolated), "pnp" => Some(pnpm_config::NodeLinker::Pnp), + "loaded" => Some(pnpm_config::NodeLinker::Loaded), _ => None, } } diff --git a/pnpm/crates/pack/src/node_linker.rs b/pnpm/crates/pack/src/node_linker.rs index 62f7e1c61e..937cdcaa5a 100644 --- a/pnpm/crates/pack/src/node_linker.rs +++ b/pnpm/crates/pack/src/node_linker.rs @@ -24,5 +24,6 @@ fn node_linker_str(node_linker: NodeLinker) -> &'static str { NodeLinker::Isolated => "isolated", NodeLinker::Hoisted => "hoisted", NodeLinker::Pnp => "pnp", + NodeLinker::Loaded => "loaded", } } diff --git a/pnpm/crates/package-manager/src/install/apply_materialization.rs b/pnpm/crates/package-manager/src/install/apply_materialization.rs index 349b6c7a30..1d2337183b 100644 --- a/pnpm/crates/package-manager/src/install/apply_materialization.rs +++ b/pnpm/crates/package-manager/src/install/apply_materialization.rs @@ -19,7 +19,7 @@ use super::{ BTreeMap, HoistedDependencies, Host, InstallError, Lockfile, Materialized, Reporter, build_workspace_state, recorded_auto_dedupe, update_workspace_state_or_warn, }; -use crate::optimistic_repeat_install::filesystem_now_ms; +use crate::optimistic_repeat_install::filesystem_now_ms_in_modules; pub(super) struct ApplyMaterializationInputs<'a, 'selection> { pub(crate) completion: crate::install::state_options::ApplyCompletionContext, @@ -298,9 +298,6 @@ fn write_applied_workspace_state( inputs: &ApplyMaterializationInputs<'_, '_>, ) { let phase_start = std::time::Instant::now(); - // Write `node_modules/.pnpm-workspace-state-v1.json`. - // pnpm's `verifyDepsBeforeRun` gate bails to "outdated" the - // moment this file is missing, forcing `pnpm install` to rerun. let mut state = build_workspace_state::( &inputs.projects.workspace_root, inputs.completion.config, @@ -310,7 +307,9 @@ fn write_applied_workspace_state( &inputs.completion.catalogs, inputs.projects.importers.manifests, inputs.projects.filtered_install, - filesystem_now_ms(&inputs.projects.workspace_root), + filesystem_now_ms_in_modules(&inputs.completion.config.workspace_state_modules_dir( + &inputs.projects.workspace_root, + )), ); state.settings.auto_dedupe = recorded_auto_dedupe( inputs.completion.config, @@ -318,6 +317,10 @@ fn write_applied_workspace_state( && inputs.materialized.fresh_lockfile.is_some()) .then_some(true), ); - update_workspace_state_or_warn::(&inputs.projects.workspace_root, &state); + update_workspace_state_or_warn::( + &inputs.projects.workspace_root, + &state, + inputs.completion.config, + ); tracing::info!(target: "pacquet::install::phase", phase = "apply.workspace_state", elapsed_ms = phase_start.elapsed().as_millis() as u64, "phase complete"); } diff --git a/pnpm/crates/package-manager/src/install/errors.rs b/pnpm/crates/package-manager/src/install/errors.rs index 4792cc64e0..7d8b5a6742 100644 --- a/pnpm/crates/package-manager/src/install/errors.rs +++ b/pnpm/crates/package-manager/src/install/errors.rs @@ -29,6 +29,9 @@ pub(super) fn map_fresh_lockfile_error(error: InstallWithFreshLockfileError) -> /// Error type of [`Install`](crate::Install). #[derive(Debug, Display, Error, Diagnostic)] pub enum InstallError { + #[diagnostic(transparent)] + RegisterLoadedProject(#[error(source)] pnpm_store_dir::RegisterProjectError), + #[diagnostic(transparent)] StoreLock(#[error(source)] pnpm_store_dir::StoreLockError), diff --git a/pnpm/crates/package-manager/src/install/lifecycle.rs b/pnpm/crates/package-manager/src/install/lifecycle.rs index ef24c620a9..5bfb6bf3fe 100644 --- a/pnpm/crates/package-manager/src/install/lifecycle.rs +++ b/pnpm/crates/package-manager/src/install/lifecycle.rs @@ -45,6 +45,7 @@ pub(super) fn project_lifecycle_extra_env( workspace_root: &Path, ) -> HashMap { let mut extra_env = config.extra_env_with_node_options(); + config.add_cas_loader_env(workspace_root, &mut extra_env); if matches!(node_linker, NodeLinker::Pnp) { let node_options = extra_env.get("NODE_OPTIONS").map(String::as_str); extra_env.insert( diff --git a/pnpm/crates/package-manager/src/install/modules_state.rs b/pnpm/crates/package-manager/src/install/modules_state.rs index 065cd56c3b..eb05a610f5 100644 --- a/pnpm/crates/package-manager/src/install/modules_state.rs +++ b/pnpm/crates/package-manager/src/install/modules_state.rs @@ -31,6 +31,7 @@ pub(super) fn map_node_linker(linker: NodeLinker) -> ModulesNodeLinker { NodeLinker::Isolated => ModulesNodeLinker::Isolated, NodeLinker::Hoisted => ModulesNodeLinker::Hoisted, NodeLinker::Pnp => ModulesNodeLinker::Pnp, + NodeLinker::Loaded => ModulesNodeLinker::Loaded, } } diff --git a/pnpm/crates/package-manager/src/install/prepare_modules_state.rs b/pnpm/crates/package-manager/src/install/prepare_modules_state.rs index 3936f279c6..733544bf3f 100644 --- a/pnpm/crates/package-manager/src/install/prepare_modules_state.rs +++ b/pnpm/crates/package-manager/src/install/prepare_modules_state.rs @@ -17,7 +17,7 @@ use super::{ use crate::{ install::state_options::RecordedWorkspace, optimistic_repeat_install::recorded_elsewhere, }; -use pnpm_workspace_state::load_workspace_state; +use pnpm_workspace_state::load_workspace_state_in_modules; pub(super) struct PrepareModulesStateInputs<'a, 'install> { pub(crate) tree: crate::install::state_options::ModulesTreeContext<'a>, @@ -70,7 +70,7 @@ pub(super) async fn prepare_modules_state<'install, Reporter: self::Reporter + ' prepare_modules_layout(&inputs, modules_manifest, is_inconsistent)?; - let recorded_state = load_workspace_state(inputs.tree.workspace_root).ok().flatten(); + let recorded_state = load_recorded_workspace_state(&inputs); let recorded = recorded_workspace( recorded_state.as_ref(), modules_manifest.is_some() || inputs.lockfiles.current.is_some(), @@ -298,3 +298,10 @@ fn modules_layout_drifted( }; !modules_layout_consistent_with(modules, config, node_linker) } + +fn load_recorded_workspace_state( + inputs: &PrepareModulesStateInputs<'_, '_>, +) -> Option { + let modules_dir = inputs.tree.config.workspace_state_modules_dir(inputs.tree.workspace_root); + load_workspace_state_in_modules(&modules_dir).ok().flatten() +} diff --git a/pnpm/crates/package-manager/src/install/prepare_modules_state/up_to_date.rs b/pnpm/crates/package-manager/src/install/prepare_modules_state/up_to_date.rs index 6b0ee061a5..33b7409703 100644 --- a/pnpm/crates/package-manager/src/install/prepare_modules_state/up_to_date.rs +++ b/pnpm/crates/package-manager/src/install/prepare_modules_state/up_to_date.rs @@ -7,7 +7,7 @@ use super::super::{ recorded_allow_builds_differ, unapproved_recorded_ignored_builds, update_workspace_state_or_warn, verify_lockfile_eagerly, }; -use crate::optimistic_repeat_install::{filesystem_now_ms, materialized_shape_matches}; +use crate::optimistic_repeat_install::{filesystem_now_ms_in_modules, materialized_shape_matches}; /// Whether any package in the lockfile resolves to a local directory. /// @@ -264,13 +264,19 @@ pub(super) fn refresh_up_to_date_workspace( context.projects.catalogs, context.projects.manifests, context.carried_state.filtered_install, - filesystem_now_ms(context.tree.workspace_root), + filesystem_now_ms_in_modules(&context.tree.config.workspace_state_modules_dir( + context.tree.workspace_root, + )), ); state.settings.auto_dedupe = crate::install::recorded_auto_dedupe( context.tree.config, context.carried_state.recorded_auto_dedupe, ); - update_workspace_state_or_warn::(context.tree.workspace_root, &state); + update_workspace_state_or_warn::( + context.tree.workspace_root, + &state, + context.tree.config, + ); } pub(super) async fn verify_up_to_date_lockfile( wanted_lockfile: &Lockfile, diff --git a/pnpm/crates/package-manager/src/install/run/fast_path.rs b/pnpm/crates/package-manager/src/install/run/fast_path.rs index 6fddd8c975..b3b5af4b7d 100644 --- a/pnpm/crates/package-manager/src/install/run/fast_path.rs +++ b/pnpm/crates/package-manager/src/install/run/fast_path.rs @@ -47,7 +47,7 @@ pub(super) fn repeat_install_verdict( check: &UpToDateCheck<'_>, ) -> Result { if !check.resolve_only { - register_workspace_in_store(check.workspace.config, check.workspace.workspace_root); + register_workspace_in_store(check.workspace.config, check.workspace.workspace_root)?; } let eligible = check.mutation.is_full_install() && matches!(check.update_seed_policy, UpdateSeedPolicy::KeepAll) @@ -185,10 +185,18 @@ pub(super) fn report_already_up_to_date( /// slots of an unregistered project. /// /// Best-effort: a registry write failure shouldn't fail the install, so it is -/// surfaced as `tracing::warn!` instead. -pub(crate) fn register_workspace_in_store(config: &Config, workspace_root: &Path) { +/// surfaced as `tracing::warn!` instead. Loaded installs require durable +/// registration because their blobs have no project hardlinks to protect them. +pub(crate) fn register_workspace_in_store( + config: &Config, + workspace_root: &Path, +) -> Result<(), InstallError> { + if config.node_linker == pnpm_config::NodeLinker::Loaded { + return pnpm_store_dir::register_loaded_project(&config.store_dir, workspace_root) + .map_err(InstallError::RegisterLoadedProject); + } if config.frozen_store && !config.enable_global_virtual_store { - return; + return Ok(()); } // Create the store root before calling `register_project` so its // `path_contains` guard can canonicalize the path instead of falling @@ -210,6 +218,7 @@ pub(crate) fn register_workspace_in_store(config: &Config, workspace_root: &Path "Failed to register workspace root in the store project registry; install continues", ); } + Ok(()) } impl super::RunExecution<'_> { diff --git a/pnpm/crates/package-manager/src/install/tests/reporting.rs b/pnpm/crates/package-manager/src/install/tests/reporting.rs index 9156e80110..2f21738929 100644 --- a/pnpm/crates/package-manager/src/install/tests/reporting.rs +++ b/pnpm/crates/package-manager/src/install/tests/reporting.rs @@ -802,7 +802,7 @@ fn a_lost_workspace_state_write_warns_through_the_reporter() { let _guard = RECORDER.lock().unwrap_or_else(std::sync::PoisonError::into_inner); MESSAGES.lock().unwrap().clear(); // Returns `()`: a lost cache write is not the command's failure. - update_workspace_state_or_warn::(&blocker, &state); + update_workspace_state_or_warn::(&blocker, &state, &Config::default()); let recorded = MESSAGES.lock().unwrap().clone(); assert_eq!(recorded.len(), 1, "exactly one warning, got: {recorded:?}"); diff --git a/pnpm/crates/package-manager/src/install/workspace_state.rs b/pnpm/crates/package-manager/src/install/workspace_state.rs index 03442fad43..9c81674d39 100644 --- a/pnpm/crates/package-manager/src/install/workspace_state.rs +++ b/pnpm/crates/package-manager/src/install/workspace_state.rs @@ -21,7 +21,7 @@ use super::{ }; use crate::optimistic_repeat_install::{refreshed_validation_baseline_ms, validation_baseline_ms}; use pnpm_reporter::Reporter; -use pnpm_workspace_state::update_workspace_state; +use pnpm_workspace_state::update_workspace_state_in_modules; /// Persist `.pnpm-workspace-state-v1.json`, warning instead of failing /// the command when the write is lost. @@ -33,8 +33,10 @@ use pnpm_workspace_state::update_workspace_state; pub(crate) fn update_workspace_state_or_warn( workspace_root: &Path, state: &WorkspaceState, + config: &Config, ) { - if let Err(error) = update_workspace_state(workspace_root, state) { + let modules_dir = config.workspace_state_modules_dir(workspace_root); + if let Err(error) = update_workspace_state_in_modules(&modules_dir, state) { tracing::warn!( target: "pacquet::install", ?error, @@ -122,7 +124,7 @@ pub fn install_already_up_to_date(check: &UpToDateFastPathCheck<'_>) -> Option, selected_project_dirs: &[&Path], ) -> Option { - match pnpm_workspace_state::load_workspace_state(inputs.lockfile_root) { + if inputs.config.node_linker == pnpm_config::NodeLinker::Loaded + && [pnpm_config::CAS_MANIFEST_FILENAME, pnpm_config::CAS_LOADER_FILENAME] + .iter() + .any(|filename| !inputs.lockfile_root.join(filename).is_file()) + { + return cannot_check_deps(); + } + match pnpm_workspace_state::load_workspace_state_in_modules( + &inputs.config.workspace_state_modules_dir(inputs.lockfile_root), + ) { Ok(Some(workspace_state)) => check_discovered_deps( inputs.config, inputs.manifest, diff --git a/pnpm/crates/package-manager/src/install_with_fresh_lockfile/materialization.rs b/pnpm/crates/package-manager/src/install_with_fresh_lockfile/materialization.rs index a0bab9f503..3eba05e6f4 100644 --- a/pnpm/crates/package-manager/src/install_with_fresh_lockfile/materialization.rs +++ b/pnpm/crates/package-manager/src/install_with_fresh_lockfile/materialization.rs @@ -324,6 +324,14 @@ pub(super) fn fresh_install_context<'b>( allow_build_policy: &'b AllowBuildPolicy, ) -> pnpm_deps_restorer::InstallContext<'b> { pnpm_deps_restorer::InstallContext { + caches: pnpm_deps_restorer::InstallCaches { + materialized_graph: std::sync::Arc::default(), + + logged_methods: install.drivers.logged_methods, + git_source_cache: &caches.git_source_cache, + dir_clone_cache, + }, + linker: pnpm_deps_restorer::ModuleLinkerContext { layout, kind: install.execution.node_linker, @@ -334,9 +342,5 @@ pub(super) fn fresh_install_context<'b>( requester: install.projects.requester, allow_build_policy, - - logged_methods: install.drivers.logged_methods, - git_source_cache: &caches.git_source_cache, - dir_clone_cache, } } diff --git a/pnpm/crates/package-manager/src/install_with_fresh_lockfile/on_disk.rs b/pnpm/crates/package-manager/src/install_with_fresh_lockfile/on_disk.rs index 356336ed6e..79b6f4ee30 100644 --- a/pnpm/crates/package-manager/src/install_with_fresh_lockfile/on_disk.rs +++ b/pnpm/crates/package-manager/src/install_with_fresh_lockfile/on_disk.rs @@ -31,6 +31,7 @@ pub(super) fn build_extra_env( if let Some(node_options) = &config.node_options { env.insert("NODE_OPTIONS".to_string(), node_options.clone()); } + config.add_cas_loader_env(workspace_root, &mut env); if matches!(node_linker, NodeLinker::Pnp) { let node_options = env.get("NODE_OPTIONS").map(String::as_str); env.insert( @@ -239,6 +240,23 @@ impl<'a> OnDiskInputs<'a> { })); Ok(linked) } + fn build_graph<'b>( + &'b self, + materialized: &'b CreateVirtualStoreOutput, + linked: &'b pnpm_deps_restorer::linking::LinkPhaseOutput, + ) -> pnpm_deps_restorer::BuildPhaseGraph<'b> { + pnpm_deps_restorer::BuildPhaseGraph { + trusted_importer_ids: self.projects.project_anchor_importer_ids, + loaded_snapshots: self.ctx.select_loaded_snapshots( + self.projects.materialization_lockfile.snapshots.as_ref(), + ), + snapshots: self.projects.materialization_lockfile.snapshots.as_ref(), + packages: self.projects.materialization_lockfile.packages.as_ref(), + importers: &self.projects.materialization_lockfile.importers, + dependency_groups: self.install.projects.dependency_groups, + materialized_snapshots: linked.build_snapshots(&materialized.materialized_snapshots), + } + } /// Run lifecycle scripts, report ignored builds, and re-link top-level /// bins: the build phase the frozen path runs, so `pacquet add esbuild` @@ -252,14 +270,17 @@ impl<'a> OnDiskInputs<'a> { /// the writer task starts winding down while the caller finishes. The /// install driver awaits it as `store_index_teardown`. async fn build( - self, + mut self, materialized: &CreateVirtualStoreOutput, linked: &pnpm_deps_restorer::linking::LinkPhaseOutput, skipped: &SkippedSnapshots, ) -> Result { let top_level_bin_root = self.symlink_root(); - let engine_name = - settle_engine_name(self.runtime.deferred_engine_name, self.runtime.engine_name).await; + let engine_name = settle_engine_name( + self.runtime.deferred_engine_name.take(), + self.runtime.engine_name.take(), + ) + .await; let extra_env = build_extra_env(self.ctx.config, self.ctx.linker.kind, self.ctx.workspace_root); publish_deps_requiring_build( @@ -273,15 +294,7 @@ impl<'a> OnDiskInputs<'a> { &self.store.store_index_writer, ), directories: build_directories(self.ctx, linked, top_level_bin_root), - graph: pnpm_deps_restorer::BuildPhaseGraph { - snapshots: self.projects.materialization_lockfile.snapshots.as_ref(), - packages: self.projects.materialization_lockfile.packages.as_ref(), - importers: &self.projects.materialization_lockfile.importers, - dependency_groups: self.install.projects.dependency_groups, - materialized_snapshots: linked.build_snapshots( - &materialized.materialized_snapshots, - ), - }, + graph: self.build_graph(materialized, linked), policy: pnpm_deps_restorer::BuildPhasePolicy { config: self.ctx.config, patch_groups: self.patched_dependencies, @@ -457,7 +470,7 @@ fn build_directories<'a>( hoisted_pkg_roots_by_key: linked.hoisted_pkg_roots_by_key.as_ref(), is_hoisted: ctx.is_hoisted(), publicly_hoisted_for_post_build: &linked.publicly_hoisted_for_post_build, - logged_methods: ctx.logged_methods, + logged_methods: ctx.caches.logged_methods, link_options: ctx.linker.bin_options, } } diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install.rs index 8b5f682360..af2b6037d3 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install.rs @@ -35,9 +35,10 @@ pub(crate) use settings::{ recorded_supported_architectures_match, settings_match, }; pub(crate) use timestamps::{ - FileMtime, file_mtime, file_mtime_from_metadata, filesystem_now_ms, lockfile_modified_since, - manifest_drift_reference_ms, modified_at_or_after, mtime_ms, refreshed_validation_baseline_ms, - validation_baseline_ms, wanted_lockfile_mtime, + FileMtime, file_mtime, file_mtime_from_metadata, filesystem_now_ms, + filesystem_now_ms_in_modules, lockfile_modified_since, manifest_drift_reference_ms, + modified_at_or_after, mtime_ms, refreshed_validation_baseline_ms, validation_baseline_ms, + wanted_lockfile_mtime, }; mod current_lockfile; @@ -73,7 +74,7 @@ use pnpm_package_is_installable::SupportedArchitectures; use pnpm_package_manifest::{DependencyGroup, PackageManifest}; use pnpm_workspace_state::{ NodeLinker as WorkspaceStateNodeLinker, TrustPolicy as WorkspaceStateTrustPolicy, - WorkspaceState, WorkspaceStateSettings, load_workspace_state, update_workspace_state, + WorkspaceState, WorkspaceStateSettings, load_workspace_state, }; /// Outcome of [`check_optimistic_repeat_install`]. @@ -296,6 +297,9 @@ impl<'a> ManifestDrift<'a> { /// The configuration alone can rule the fast path out, before anything is /// read from disk. fn config_blocks_fast_path(config: &Config) -> Option<&'static str> { + if config.node_linker == pnpm_config::NodeLinker::Loaded { + return Some("CAS installation requires manifest verification"); + } if !config.optimistic_repeat_install { return Some("optimistic_repeat_install disabled"); } diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/deps_status.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/deps_status.rs index 532a2fd828..40b8a99876 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/deps_status.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/deps_status.rs @@ -4,13 +4,13 @@ use super::{ Config, Host, Lockfile, LockfileConflictCheckFailure, ManifestStat, NodeLinker, OptimisticRepeatInstallCheck, Path, WorkspaceState, catalogs_cache_matches, current_lockfile_file_has_content, current_lockfile_unusable_with_non_empty_wanted, - filesystem_now_ms, first_lockfile_requiring_conflict_safe_install, + filesystem_now_ms_in_modules, first_lockfile_requiring_conflict_safe_install, first_project_missing_modules_dir, first_selected_project_missing_from_current_lockfile, first_selected_project_missing_modules_dir, first_setting_drift, modified_manifests_match_lockfile, patches_modified_since, pnpmfiles_drift, project_structure_matches, relocation::{prove_move, rekeyed_validation_now, relocated_state}, - update_workspace_state, wanted_lockfile_file_has_content, + wanted_lockfile_file_has_content, }; /// Outcome of [`check_deps_status_before_run`]. @@ -83,8 +83,13 @@ pub fn check_deps_status_before_run( } let projects_to_check = drift.projects_to_check(modified); - let filesystem_now = - check.is_workspace_install.then(|| filesystem_now_ms(check.workspace_root)).flatten(); + let filesystem_now = check.is_workspace_install + .then(|| { + filesystem_now_ms_in_modules(&check.config.workspace_state_modules_dir( + check.workspace_root, + )) + }) + .flatten(); // The TypeScript run/exec handler does not forward `dedupePeers` // into `checkDepsStatus`, so its pre-run lockfile check uses the // false default even when the workspace setting is true. @@ -327,7 +332,7 @@ fn record_content_check_state( new_state.settings.dev = state.settings.dev; new_state.settings.optional = state.settings.optional; new_state.settings.production = state.settings.production; - refresh_content_check_state(workspace_root, &new_state); + refresh_content_check_state(check, &new_state); } /// Read-only twin of [`crate::optimistic_repeat_install::regenerate_wanted_lockfile_if_missing`](crate::optimistic_repeat_install::settle::regenerate_wanted_lockfile_if_missing) for the @@ -404,8 +409,14 @@ fn lockfile_conflict_drift( None } -fn refresh_content_check_state(workspace_root: &std::path::Path, new_state: &WorkspaceState) { - if let Err(error) = update_workspace_state(workspace_root, new_state) { +fn refresh_content_check_state( + check: &OptimisticRepeatInstallCheck<'_>, + new_state: &WorkspaceState, +) { + let modules_dir = check.config.workspace_state_modules_dir(check.workspace_root); + if let Err(error) = + pnpm_workspace_state::update_workspace_state_in_modules(&modules_dir, new_state) + { tracing::warn!( target: "pacquet::run", ?error, diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/modules_dirs.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/modules_dirs.rs index b3e2acec1c..60991ab562 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/modules_dirs.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/modules_dirs.rs @@ -244,7 +244,7 @@ fn modules_dir_exists( ) -> bool { match node_linker { NodeLinker::Hoisted => root_modules_dir_exists, - NodeLinker::Isolated | NodeLinker::Pnp => { + NodeLinker::Isolated | NodeLinker::Pnp | NodeLinker::Loaded => { if is_root { root_modules_dir_exists } else { diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/relocation.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/relocation.rs index 42a6cf467c..43cae5a036 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/relocation.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/relocation.rs @@ -5,7 +5,7 @@ use super::{ Decision, Host, Lockfile, ManifestDrift, OptimisticRepeatInstallCheck, PackageManifest, Path, PathBuf, WorkspaceState, current_lockfile::assert_loaded_current_lockfile_records, - current_pnpmfiles, filesystem_now_ms, manifest_agreement::check_projects_content, + current_pnpmfiles, filesystem_now_ms_in_modules, manifest_agreement::check_projects_content, project_structure_matches, settle_repeat_install, }; use std::{collections::BTreeSet, path::Component}; @@ -139,7 +139,9 @@ pub(super) fn rekeyed_validation_now( if drift.modified().is_empty() && !drift.lockfile_modified { Some(state.last_validated_timestamp) } else { - filesystem_now_ms(check.workspace_root) + filesystem_now_ms_in_modules(&check.config.workspace_state_modules_dir( + check.workspace_root, + )) } } diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/settings.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/settings.rs index 99601c3c4c..d4999906c3 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/settings.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/settings.rs @@ -210,7 +210,12 @@ impl SettingsComparison<'_> { "minimumReleaseAgeStrict", recorded.minimum_release_age_strict != live.minimum_release_age_strict, ); - return_drift_if!(self, "nodeLinker", recorded.node_linker != live.node_linker); + return_drift_if!( + self, + "nodeLinker", + recorded.node_linker != live.node_linker + || recorded.node_linker_excluded != live.node_linker_excluded + ); return_drift_if!(self, "optional", recorded.optional != live.optional); return_drift_if!(self, "overrides", recorded.overrides != live.overrides); let package_extensions_drift = !package_extensions_match( @@ -334,9 +339,6 @@ pub(crate) fn current_settings( dedupe_peer_dependents: Some(config.dedupe_peer_dependents), dedupe_peers: Some(config.dedupe_peers), auto_dedupe: config.auto_dedupe.then_some(true), - lockfile_include_resolution_settings: config - .lockfile_include_resolution_settings - .then_some(true), 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 @@ -352,6 +354,9 @@ pub(crate) fn current_settings( config.link_workspace_packages, )), node_linker: Some(map_node_linker(node_linker)), + node_linker_excluded: (node_linker == NodeLinker::Loaded).then(|| { + config.node_linker_excluded.clone() + }), optional: Some(included.optional_dependencies), overrides: recorded_overrides(config), package_extensions: config.package_extensions @@ -387,6 +392,9 @@ fn recorded_allow_builds( fn current_policy_settings(config: &Config) -> WorkspaceStateSettings { WorkspaceStateSettings { + lockfile_include_resolution_settings: config + .lockfile_include_resolution_settings + .then_some(true), minimum_release_age: config.minimum_release_age, minimum_release_age_exclude: config.minimum_release_age_exclude.clone(), minimum_release_age_ignore_missing_time: Some( @@ -458,6 +466,7 @@ pub(crate) fn map_node_linker(linker: NodeLinker) -> WorkspaceStateNodeLinker { NodeLinker::Isolated => WorkspaceStateNodeLinker::Isolated, NodeLinker::Hoisted => WorkspaceStateNodeLinker::Hoisted, NodeLinker::Pnp => WorkspaceStateNodeLinker::Pnp, + NodeLinker::Loaded => WorkspaceStateNodeLinker::Loaded, } } diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/timestamps.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/timestamps.rs index f886e09e03..0f7ec3bfc7 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/timestamps.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/timestamps.rs @@ -121,8 +121,10 @@ pub(crate) fn validation_baseline_ms( /// created or stat'd, leaving the caller with the mtime-derived /// baseline. pub(crate) fn filesystem_now_ms(workspace_root: &Path) -> Option { - let state_path = pnpm_workspace_state::get_file_path(workspace_root); - let parent = state_path.parent()?; + filesystem_now_ms_in_modules(&workspace_root.join("node_modules")) +} + +pub(crate) fn filesystem_now_ms_in_modules(parent: &Path) -> Option { fs::create_dir_all(parent).ok()?; let probe = tempfile::tempfile_in(parent).ok()?; file_mtime_from_metadata(&probe.metadata().ok()?).map(|mtime| mtime.ms) diff --git a/pnpm/crates/store-dir/src/lib.rs b/pnpm/crates/store-dir/src/lib.rs index 978b4bf8e5..f6f5ff9993 100644 --- a/pnpm/crates/store-dir/src/lib.rs +++ b/pnpm/crates/store-dir/src/lib.rs @@ -9,10 +9,13 @@ pub use project_registry::*; pub use prune::*; pub use store_dir::*; pub use store_index::*; +pub use store_loader_manifest::*; pub use store_lock::{StoreLockError, StoreOperationLock}; pub use symlinks::{SYMLINK_MODE, SideEffectsOverlay, is_symlink_mode, normalize_symlink_target}; pub use upload::*; +pub use loader_references::loader_blob_path; + mod add_files_from_dir; mod cas_file; mod check_pkg_files_integrity; @@ -26,3 +29,6 @@ mod store_dir; mod store_lock; mod symlinks; mod upload; + +mod loader_references; +mod store_loader_manifest; diff --git a/pnpm/crates/store-dir/src/loader_references.rs b/pnpm/crates/store-dir/src/loader_references.rs new file mode 100644 index 0000000000..193b94f76b --- /dev/null +++ b/pnpm/crates/store-dir/src/loader_references.rs @@ -0,0 +1,99 @@ +use crate::{CAS_MANIFEST_FILENAME, StoreDir, StoreLoaderManifest, get_registered_projects}; +use std::{ + collections::{BTreeMap, HashSet}, + fs, io, + path::{Path, PathBuf}, +}; + +#[derive(Default)] +pub(crate) struct LoaderReferences { + pub files: HashSet, + pub package_roots: Vec, +} + +pub(crate) fn loader_references(store: &StoreDir) -> io::Result { + let projects = get_registered_projects(store).map_err(io::Error::other)?; + if projects.is_empty() { + return Ok(LoaderReferences::default()); + } + let canonical_store = dunce::canonicalize(store.root())?; + let mut references = LoaderReferences::default(); + for project in projects { + let Some(mut manifest) = read_manifest(&project.join(CAS_MANIFEST_FILENAME))? else { + continue; + }; + if !same_store(&project.join(&manifest.store_dir), &canonical_store)? { + continue; + } + manifest.store_dir = store.root().to_path_buf(); + add_references(&mut references, manifest, &project)?; + } + Ok(references) +} + +fn read_manifest(path: &Path) -> io::Result> { + let contents = match fs::read(path) { + Ok(contents) => contents, + Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(error) => { + return Err(io::Error::other(format!("Cannot read {}: {error}", path.display()))); + } + }; + let manifest: StoreLoaderManifest = serde_json::from_slice(&contents) + .map_err(|error| io::Error::other(format!("Cannot read {}: {error}", path.display())))?; + if manifest.version != 1 { + return Err(io::Error::other(format!( + "Unsupported store manifest version at {}", + path.display(), + ))); + } + Ok(Some(manifest)) +} + +fn same_store(path: &Path, canonical_store: &Path) -> io::Result { + match dunce::canonicalize(path) { + Ok(path) => Ok(path == canonical_store), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false), + Err(error) => Err(error), + } +} + +fn add_references( + references: &mut LoaderReferences, + manifest: StoreLoaderManifest, + project: &Path, +) -> io::Result<()> { + let files_dir = manifest.store_dir.join("files"); + for package in manifest.packages.into_values() { + if package.resolution.as_deref() == Some("node") + && let Some(root) = package.root + { + references.package_roots.push(project.join(root)); + } + for hash in package.files.into_iter().flat_map(BTreeMap::into_values) { + references.files.insert(loader_blob_path(&files_dir, &hash)?); + } + } + Ok(()) +} + +/// Resolve a validated SHA-512 digest, optionally executable, below a CAS files directory. +pub fn loader_blob_path(files_dir: &Path, hash: &str) -> io::Result { + let digest = hash.strip_suffix("-exec").unwrap_or(hash); + if digest.len() != 128 + || !digest + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err(io::Error::other(format!( + "Invalid CAS file hash in store manifest for {}", + files_dir.display(), + ))); + } + Ok(files_dir + .join(&hash[..2]) + .join(&hash[2..])) +} + +#[cfg(test)] +mod tests; diff --git a/pnpm/crates/store-dir/src/loader_references/tests.rs b/pnpm/crates/store-dir/src/loader_references/tests.rs new file mode 100644 index 0000000000..003ea5a4b9 --- /dev/null +++ b/pnpm/crates/store-dir/src/loader_references/tests.rs @@ -0,0 +1,66 @@ +use crate::{CAS_MANIFEST_FILENAME, StoreDir, register_project}; +use std::fs; + +#[test] +fn prune_keeps_loader_blobs_until_the_manifest_is_removed() { + let root = tempfile::tempdir().unwrap(); + let project = root.path().join("project"); + let store = StoreDir::new(root.path().join("store")); + fs::create_dir_all(&project).unwrap(); + fs::create_dir_all(store.root()).unwrap(); + register_project(&store, &project).unwrap(); + let hash = "a".repeat(128); + let blob = store.file_path_by_hex_str(&hash, ""); + fs::create_dir_all(blob.parent().unwrap()).unwrap(); + fs::write(&blob, "module.exports = 42").unwrap(); + let manifest = serde_json::json!({ + "version": 1, "storeDir": store.root(), + "packages": { "example@1": { "files": { "index.js": hash } } } + }); + fs::write(project.join(CAS_MANIFEST_FILENAME), manifest.to_string()).unwrap(); + store.prune().unwrap(); + assert!(blob.is_file()); + fs::remove_file(project.join(CAS_MANIFEST_FILENAME)).unwrap(); + store.prune().unwrap(); + assert!(!blob.exists()); +} + +#[test] +fn invalid_loader_manifest_aborts_prune_before_deleting_files() { + let root = tempfile::tempdir().unwrap(); + let project = root.path().join("project"); + let store = StoreDir::new(root.path().join("store")); + fs::create_dir_all(&project).unwrap(); + fs::create_dir_all(store.root()).unwrap(); + register_project(&store, &project).unwrap(); + let blob = store.file_path_by_hex_str(&"a".repeat(128), ""); + fs::create_dir_all(blob.parent().unwrap()).unwrap(); + fs::write(&blob, "retained").unwrap(); + for contents in [ + "{".to_string(), + serde_json::json!({ + "version": 1, "storeDir": store.root(), + "packages": { "example@1": { "files": { "index.js": "../../outside" } } } + }) + .to_string(), + ] { + fs::write(project.join(CAS_MANIFEST_FILENAME), contents).unwrap(); + assert!(store.prune().is_err()); + assert!(blob.is_file()); + } +} + +#[test] +fn loader_blob_paths_reject_absolute_traversal_and_invalid_digests() { + let files = std::path::Path::new("store/files"); + for hash in ["/tmp/package.json", "../../package.json", "a", "é", &"A".repeat(128)] { + assert!(super::loader_blob_path(files, hash).is_err(), "{hash}"); + } + for suffix in ["", "-exec"] { + let hash = format!("{}{suffix}", "a".repeat(128)); + assert_eq!( + super::loader_blob_path(files, &hash).unwrap(), + files.join("aa").join(&hash[2..]), + ); + } +} diff --git a/pnpm/crates/store-dir/src/project_registry.rs b/pnpm/crates/store-dir/src/project_registry.rs index 6763535224..ad035d9f37 100644 --- a/pnpm/crates/store-dir/src/project_registry.rs +++ b/pnpm/crates/store-dir/src/project_registry.rs @@ -88,6 +88,15 @@ pub fn register_project( return Ok(()); } + register_loaded_project(store_dir, project_dir) +} + +/// Register a loader's ownership even when its store is inside the project. +/// Pruning reads the manifest directly without recursively walking the registry link. +pub fn register_loaded_project( + store_dir: &StoreDir, + project_dir: &Path, +) -> Result<(), RegisterProjectError> { let registry_dir = store_dir.projects(); fs::create_dir_all(®istry_dir) .map_err(|error| RegisterProjectError::CreateRegistryDir { diff --git a/pnpm/crates/store-dir/src/prune.rs b/pnpm/crates/store-dir/src/prune.rs index 5f21c3c41a..3bc44a634b 100644 --- a/pnpm/crates/store-dir/src/prune.rs +++ b/pnpm/crates/store-dir/src/prune.rs @@ -36,6 +36,9 @@ use std::{ /// Error type of [`StoreDir::prune`]. #[derive(Debug, Display, Error, Diagnostic)] pub enum PruneError { + #[display("Failed to read CAS loader references: {_0}")] + #[diagnostic(code(ERR_PNPM_STORE_LOADER_REFERENCES))] + LoaderReferences(#[error(source)] io::Error), #[diagnostic(transparent)] StoreLock(#[error(source)] StoreLockError), @@ -89,8 +92,11 @@ impl StoreDir { /// [#344]: https://github.com/pnpm/pacquet/issues/344 pub fn prune(&self) -> Result<(), PruneError> { let _store_lock = self.lock_for_prune().map_err(PruneError::StoreLock)?; - self.prune_global_virtual_store()?; - let stats = crate::prune_cas::prune_cas(self).map_err(PruneError::PruneCas)?; + let references = crate::loader_references::loader_references(self) + .map_err(PruneError::LoaderReferences)?; + self.prune_global_virtual_store(&references.package_roots)?; + let stats = crate::prune_cas::prune_cas_with_references(self, &references.files) + .map_err(PruneError::PruneCas)?; eprintln!( "Removed {} file{} ({} bytes)", stats.files, @@ -112,7 +118,7 @@ impl StoreDir { Ok(()) } - fn prune_global_virtual_store(&self) -> Result<(), PruneError> { + fn prune_global_virtual_store(&self, loader_roots: &[PathBuf]) -> Result<(), PruneError> { let links_dir = self.links(); if !path_exists(&links_dir) { return Ok(()); @@ -128,7 +134,7 @@ impl StoreDir { projects.len(), ); - let reachable = mark_reachable_slots(&links_dir, &projects)?; + let reachable = mark_reachable_slots(&links_dir, &projects, loader_roots)?; // Projects without the global virtual store register too, so no link // from any registered project leaves the slots' users as unknown as // an empty registry does. @@ -154,6 +160,7 @@ impl StoreDir { fn mark_reachable_slots( links_dir: &Path, projects: &[PathBuf], + loader_roots: &[PathBuf], ) -> Result, PruneError> { // Canonicalize the links root once and pass it down. The // mark walk compares every target's canonical form against @@ -164,6 +171,7 @@ fn mark_reachable_slots( dunce::canonicalize(links_dir).unwrap_or_else(|_| links_dir.to_path_buf()); let mut reachable: HashSet = HashSet::new(); let mut visited: HashSet = HashSet::new(); + mark_loader_roots(loader_roots, &canonical_links, &mut reachable, &mut visited)?; for project_dir in projects { for modules_dir in find_all_node_modules_dirs(project_dir)? { walk_symlinks_to_store(&modules_dir, &canonical_links, &mut reachable, &mut visited)?; @@ -172,6 +180,22 @@ fn mark_reachable_slots( Ok(reachable) } +fn mark_loader_roots( + loader_roots: &[PathBuf], + canonical_links: &Path, + reachable: &mut HashSet, + visited: &mut HashSet, +) -> Result<(), PruneError> { + for root in loader_roots { + if let Some(slot) = store_slot_from_target(root, canonical_links) { + let directory = canonical_links.join(&slot).join("node_modules"); + reachable.insert(slot); + walk_symlinks_to_store(&directory, canonical_links, reachable, visited)?; + } + } + Ok(()) +} + /// Find every `node_modules/` directory under `project_dir`, /// including those inside workspace packages. Descends into every /// non-hidden subdir until it sees `node_modules`, at which point it @@ -299,10 +323,14 @@ fn linked_store_slot(entry_path: &Path, canonical_links: &Path) -> Option Option { // Canonicalise the target so a symlink-bearing path prefix doesn't fool // the `starts_with` check against the (already-canonical) links root. let canonical_target = - dunce::canonicalize(&absolute_target).unwrap_or_else(|_| absolute_target.clone()); + dunce::canonicalize(absolute_target).unwrap_or_else(|_| absolute_target.to_path_buf()); // Slot path is the segment after `canonical_links` up to (but excluding) // the first `node_modules` component. Layout: @@ -310,10 +338,16 @@ fn linked_store_slot(entry_path: &Path, canonical_links: &Path) -> Option///`. let rel = canonical_target.strip_prefix(canonical_links).ok()?; let parts: Vec<_> = rel.components().collect(); + if parts + .iter() + .any(|part| !matches!(part, std::path::Component::Normal(_))) + { + return None; + } let node_modules = parts .iter() .position(|component| component.as_os_str() == std::ffi::OsStr::new("node_modules"))?; - Some(parts[..node_modules].iter().collect()) + (node_modules > 0).then(|| parts[..node_modules].iter().collect()) } /// Sweep phase: walk `////` diff --git a/pnpm/crates/store-dir/src/prune/tests.rs b/pnpm/crates/store-dir/src/prune/tests.rs index 7c194bb77b..c2c00aa3ad 100644 --- a/pnpm/crates/store-dir/src/prune/tests.rs +++ b/pnpm/crates/store-dir/src/prune/tests.rs @@ -199,3 +199,23 @@ fn prune_marks_transitive_slot_reachable() { assert!(foo.exists(), "direct dep slot survives"); assert!(bar.exists(), "transitive dep slot also survives"); } + +#[test] +fn nonexistent_loader_roots_cannot_escape_links_through_parent_components() { + let temporary = tempdir().unwrap(); + let links = temporary.path().join("links"); + fs::create_dir_all(&links).unwrap(); + let links = dunce::canonicalize(links).unwrap(); + for target in [ + links.join("../outside/node_modules/missing"), + links.join("scope/name/../../../../outside/node_modules/missing"), + links.join("node_modules/missing"), + ] { + assert_eq!(super::store_slot_from_target(&target, &links), None); + } + let slot = PathBuf::from("scope/name/1/hash"); + assert_eq!( + super::store_slot_from_target(&links.join(&slot).join("node_modules/missing"), &links), + Some(slot), + ); +} diff --git a/pnpm/crates/store-dir/src/prune_cas.rs b/pnpm/crates/store-dir/src/prune_cas.rs index 4c525c43a9..b670d4e82b 100644 --- a/pnpm/crates/store-dir/src/prune_cas.rs +++ b/pnpm/crates/store-dir/src/prune_cas.rs @@ -65,7 +65,10 @@ pub(crate) struct PruneCasStats { pub undecodable_packages: usize, } -pub(crate) fn prune_cas(store_dir: &StoreDir) -> Result { +pub(crate) fn prune_cas_with_references( + store_dir: &StoreDir, + referenced: &HashSet, +) -> Result { remove_tmp(store_dir)?; let mut stats = PruneCasStats::default(); @@ -78,9 +81,24 @@ pub(crate) fn prune_cas(store_dir: &StoreDir) -> Result, + stats: &mut PruneCasStats, +) -> Result<(), PruneCasError> { let mut index = StoreIndex::open_in(store_dir).map_err(PruneCasError::StoreIndex)?; let mut rows_to_delete = Vec::new(); index.for_each_raw(|key, data| { @@ -105,7 +123,7 @@ pub(crate) fn prune_cas(store_dir: &StoreDir) -> Result, stats: &mut PruneCasStats, + referenced: &HashSet, ) -> Result<(), PruneCasError> { for entry in read_entries(shard_dir)? { let path = entry.path(); - let metadata = match fs::metadata(&path) { - Ok(metadata) => metadata, - Err(error) if error.kind() == ErrorKind::NotFound => continue, - Err(error) => return Err(PruneCasError::InspectFile { path, error }), - }; - if !metadata.is_file() || hard_link_count(&path, &metadata)? != 1 { + if referenced.contains(&path) { continue; } - match fs::remove_file(&path) { - Ok(()) => { - stats.files += 1; - stats.bytes += metadata.len(); - } - Err(error) if error.kind() == ErrorKind::NotFound => {} - Err(error) => return Err(PruneCasError::RemoveFile { path, error }), - } + let Some(bytes) = remove_unlinked_file(&path)? else { continue }; + stats.files += 1; + stats.bytes += bytes; let shard = shard_name.to_string_lossy(); let file = entry.file_name(); let file = file.to_string_lossy(); @@ -140,6 +149,22 @@ fn prune_shard( Ok(()) } +fn remove_unlinked_file(path: &Path) -> Result, PruneCasError> { + let metadata = match fs::metadata(path) { + Ok(metadata) => metadata, + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(PruneCasError::InspectFile { path: path.to_path_buf(), error }), + }; + if !metadata.is_file() || hard_link_count(path, &metadata)? != 1 { + return Ok(None); + } + match fs::remove_file(path) { + Ok(()) => Ok(Some(metadata.len())), + Err(error) if error.kind() == ErrorKind::NotFound => Ok(None), + Err(error) => Err(PruneCasError::RemoveFile { path: path.to_path_buf(), error }), + } +} + fn read_entries(path: &Path) -> Result, PruneCasError> { let entries = match fs::read_dir(path) { Ok(entries) => entries, diff --git a/pnpm/crates/store-dir/src/prune_cas/tests.rs b/pnpm/crates/store-dir/src/prune_cas/tests.rs index 4d4ea773b7..d4c026df29 100644 --- a/pnpm/crates/store-dir/src/prune_cas/tests.rs +++ b/pnpm/crates/store-dir/src/prune_cas/tests.rs @@ -1,4 +1,4 @@ -use super::prune_cas; +use super::{PruneCasError, PruneCasStats, prune_cas_with_references}; use crate::{CafsFileInfo, PackageFilesIndex, StoreDir, StoreIndex}; use std::{collections::HashMap, fs}; @@ -158,3 +158,7 @@ fn keeps_undecodable_rows_and_prunes_the_rest() { ["a-unreadable"], ); } + +fn prune_cas(store: &StoreDir) -> Result { + prune_cas_with_references(store, &std::collections::HashSet::new()) +} diff --git a/pnpm/crates/store-dir/src/store_loader_manifest.rs b/pnpm/crates/store-dir/src/store_loader_manifest.rs new file mode 100644 index 0000000000..c85b4254cc --- /dev/null +++ b/pnpm/crates/store-dir/src/store_loader_manifest.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, path::PathBuf}; + +pub const CAS_MANIFEST_FILENAME: &str = ".pnpm-store.json"; +pub const CAS_LOADER_FILENAME: &str = ".pnpm-store-loader.mjs"; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreLoaderManifest { + pub version: u8, + pub store_dir: PathBuf, + pub packages: BTreeMap, +} + +#[derive(Serialize, Deserialize)] +pub struct StoreLoaderPackage { + #[serde(skip_serializing_if = "Option::is_none")] + pub root: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub files: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + #[serde(default)] + pub dependencies: BTreeMap, +} diff --git a/pnpm/crates/workspace-state/src/lib.rs b/pnpm/crates/workspace-state/src/lib.rs index ca6f850fce..c92affd849 100644 --- a/pnpm/crates/workspace-state/src/lib.rs +++ b/pnpm/crates/workspace-state/src/lib.rs @@ -166,6 +166,8 @@ pub struct WorkspaceStateSettings { #[serde(default, skip_serializing_if = "Option::is_none")] pub node_linker: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub node_linker_excluded: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] pub optional: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub overrides: Option>, @@ -223,6 +225,7 @@ pub enum NodeLinker { Hoisted, Isolated, Pnp, + Loaded, } /// Error returned by [`update_workspace_state`]. @@ -260,7 +263,15 @@ pub fn update_workspace_state( workspace_dir: &Path, state: &WorkspaceState, ) -> Result<(), UpdateWorkspaceStateError> { - let file_path = get_file_path(workspace_dir); + update_workspace_state_in_modules(&workspace_dir.join("node_modules"), state) +} + +/// Atomically write workspace state inside the selected installation directory. +pub fn update_workspace_state_in_modules( + modules_dir: &Path, + state: &WorkspaceState, +) -> Result<(), UpdateWorkspaceStateError> { + let file_path = modules_dir.join(WORKSPACE_STATE_FILENAME); let parent = file_path.parent().expect("workspace-state path always has a parent"); pnpm_fs::create_dir_all_with_retry(parent) .map_err(|source| UpdateWorkspaceStateError::CreateDir { @@ -283,7 +294,14 @@ pub fn update_workspace_state( pub fn load_workspace_state( workspace_dir: &Path, ) -> Result, LoadWorkspaceStateError> { - let file_path = get_file_path(workspace_dir); + load_workspace_state_in_modules(&workspace_dir.join("node_modules")) +} + +/// Read workspace state inside the selected installation directory. +pub fn load_workspace_state_in_modules( + modules_dir: &Path, +) -> Result, LoadWorkspaceStateError> { + let file_path = modules_dir.join(WORKSPACE_STATE_FILENAME); let text = match fs::read_to_string(&file_path) { Ok(text) => text, Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(None), diff --git a/pnpm/docs/settings.md b/pnpm/docs/settings.md index bb5f217ce1..369842ba5c 100644 --- a/pnpm/docs/settings.md +++ b/pnpm/docs/settings.md @@ -181,6 +181,7 @@ Every setting is listed below, grouped by topic. Follow a setting to read its do * [modulesDir](./settings/node-modules.md#modulesdir) * [nodeLinker](./settings/node-modules.md#nodelinker) +* [nodeLinker.excluded](./settings/node-modules.md#nodelinkerexcluded) * [nodeExperimentalPackageMap](./settings/node-modules.md#nodeexperimentalpackagemap) * [nodePackageMapType](./settings/node-modules.md#nodepackagemaptype) * [symlink](./settings/node-modules.md#symlink) diff --git a/pnpm/docs/settings/node-modules.md b/pnpm/docs/settings/node-modules.md index 713ffad3c0..7800584efa 100644 --- a/pnpm/docs/settings/node-modules.md +++ b/pnpm/docs/settings/node-modules.md @@ -17,9 +17,11 @@ The directory in which dependencies will be installed (instead of ### nodeLinker * Default: **isolated** -* Type: **isolated**, **hoisted**, **pnp** +* Type: **isolated**, **hoisted**, **pnp**, **loaded**, or a loaded-linker object -Defines what linker should be used for installing Node packages. +Defines what linker should be used for installing Node packages. The existing scalar values remain supported. The experimental loaded linker also accepts an object with `type: loaded` and an optional `excluded` list. Other linker object forms are not supported yet. + +Configuration sources replace the whole `nodeLinker` value. A higher-priority scalar or an object without `excluded` clears exclusions from lower-priority sources. The environment variable `PNPM_CONFIG_NODE_LINKER` and `--config.node-linker` accept the object as JSON. * **isolated** - dependencies are symlinked from a virtual store at `node_modules/.pnpm`. * **hoisted** - a flat `node_modules` without symlinks is created. Same as the `node_modules` created by npm or Yarn Classic. One of Yarn's libraries is used for hoisting, when this setting is used. Legitimate reasons to use this setting: @@ -30,10 +32,26 @@ Defines what linker should be used for installing Node packages. * **pnp** - no `node_modules`. Plug'n'Play is an innovative strategy for Node that is [used by Yarn Berry][pnp]. It is recommended to also set `symlink` setting to `false` when using `pnp` as your linker. +* **loaded** - experimental loading directly from the content-addressable store. pnpm preloads its Node.js loader for scripts and commands. Selected compatibility packages and their dependency trees use the global virtual store. See [Experimental store loader](../store-loader.md). + [pnp]: https://yarnpkg.com/features/pnp [--preserve-symlinks]: https://nodejs.org/api/cli.html#cli_preserve_symlinks [`"bundledDependencies"`]: https://docs.npmjs.com/cli/v8/configuring-npm/package-json#bundleddependencies +### nodeLinker.excluded + +* Default: **[]** +* Type: **string[]** + +With `nodeLinker.type: loaded`, materialize these package names and their complete dependency trees in the global virtual store. Names match all installed versions and peer contexts. Packages outside these trees load directly from CAS. Build approval settings still apply. + +```yaml +nodeLinker: + type: loaded + excluded: + - vitest +``` + ### nodeExperimentalPackageMap Added in: v11.8.0 diff --git a/pnpm/docs/sidebars.json b/pnpm/docs/sidebars.json index c3478105f0..5b2382806c 100644 --- a/pnpm/docs/sidebars.json +++ b/pnpm/docs/sidebars.json @@ -239,6 +239,7 @@ "logos", "limitations", "symlinked-node-modules-structure", + "store-loader", "how-peers-are-resolved", "lockfile", "uninstall", diff --git a/pnpm/docs/store-loader.md b/pnpm/docs/store-loader.md new file mode 100644 index 0000000000..1e0aa2f2c8 --- /dev/null +++ b/pnpm/docs/store-loader.md @@ -0,0 +1,137 @@ +--- +title: Experimental store loader +--- + +pnpm's experimental loaded linker lets Node.js load JavaScript and JSON directly from the content-addressable store without creating an application `node_modules` directory. The loader is included with pnpm. + +This is an experimental install mode. Enable it persistently in `pnpm-workspace.yaml` so install and execution commands use the same layout: + +```yaml +nodeLinker: + type: loaded + excluded: + - vitest +``` + +Run `pnpm install`, then use `pnpm run`, `pnpm test`, and `pnpm exec` normally. pnpm generates `.pnpm-store.json`, writes its bundled runtime to `.pnpm-store-loader.mjs`, and preloads it for scripts and commands. Node child processes inherit the preload through `NODE_OPTIONS`. Generated JavaScript bin shims also register the loader when invoked directly. + +`nodeLinker.excluded` contains exact package names. All installed versions and peer contexts of a selected name, plus their complete dependency trees, are materialized as normal GVS packages. Other registry packages stay in CAS. Patched packages and their dependency trees are materialized automatically. Packages needing build scripts must be selected explicitly unless scripts are disabled. The normal build approval policy still applies. + +The mode enables the global virtual store. With the default layout, application state and bin shims live under `.pnpm`, and no application `node_modules` directory is created. GVS packages have their normal dependency links outside the application. Workspace sources remain in their project directories. The project retains its current lockfile and store registration; keep these files while using the installation. + +When switching an existing project, remove its old `node_modules` directories before installing. Changing the mode does not remove directories belonging to the previous layout. + +Add `.pnpm/`, `.pnpm-store.json`, and `.pnpm-store-loader.mjs` to `.gitignore`. These are generated, machine-local installation files. Reinstall after moving the project. CAS installs currently regenerate and verify their runtime manifest on repeat installs rather than using the optimistic installation shortcut. + +The loader is embedded in the pnpm executable and needs no separately installed runtime package. The standalone `@pnpm/esm-loader` API remains available for manual integration. Plain `node app.mjs` outside pnpm needs the explicit preload described below. + +`symlink: false` and `nodeExperimentalPackageMap` cannot be combined with this mode. Independent test-runner resolvers and direct filesystem reads can still require additional materialization or resolver integration. + +## Running an application + +The loader requires Node.js 26.10.0 or later. After a CAS install, run Node directly with: + +```sh +node --import ./.pnpm-store-loader.mjs app.mjs +``` + +For standalone integration, make the loader available separately from the application's dependencies and preload its registration module: + +```sh +PNPM_LOADER_MANIFEST=/absolute/path/to/.pnpm-store.json \ + node --import /absolute/path/to/esm-loader/register.mjs app.mjs +``` + +On Windows, set `PNPM_LOADER_MANIFEST` using your shell's environment-variable syntax. If the variable is omitted, the loader reads `.pnpm-store.json` from the current working directory. + +A custom preload can register an explicit manifest: + +```js +import { registerStoreLoader } from '@pnpm/esm-loader' + +registerStoreLoader(new URL('./.pnpm-store.json', import.meta.url)) +``` + +Run it with `node --import ./preload.mjs app.mjs`. The registration returns Node's hook handle, which has a `deregister()` method. Register before importing application dependencies. Workers inherit a `--import` preload through Node's default execution arguments. + +## Manifest format + +The manifest has three top-level fields: + +| Field | Meaning | +| --- | --- | +| `version` | `1` | +| `storeDir` | The versioned store directory containing `files/`. Relative paths resolve from the manifest's directory. | +| `packages` | An object keyed by package instance ID, including each peer dependency context. | + +Each package entry has a `dependencies` object mapping import names to package instance IDs, and exactly one of: + +- `root`: a real project or workspace directory, relative to the manifest or absolute. +- `files`: an object mapping package-relative filenames to their lowercase SHA-512 hex digest, with `-exec` appended for executable blobs. + +For example, the following structure describes a project importing a stored package. Replace the digest placeholders with the actual 128-character SHA-512 hashes: + +```json +{ + "version": 1, + "storeDir": "/home/user/.local/share/pnpm/store/v11", + "packages": { + ".": { + "root": ".", + "dependencies": { "example": "example@1.0.0" } + }, + "example@1.0.0": { + "dependencies": {}, + "files": { + "package.json": "", + "index.js": "" + } + } + } +} +``` + +A file with digest `abcdef...` is read from `/files/ab/cdef...`. No package metadata is inferred from the blob filename. Include `package.json` in the file map to supply `type`, `main`, `exports`, and `imports`. + +Use separate package IDs for separate dependency contexts, even when every file digest is identical. For example, `plugin@1.0.0(peer@1.0.0)` and `plugin@1.0.0(peer@2.0.0)` can share their `files` map while having different dependency targets. Their runtime module instances remain separate. + +The manifest describes the effective files to load. A producer can represent cached build outputs by applying a side-effects cache diff to the base index, including deletions and replacements, before generating this file map. Selecting a valid cache entry and authorizing builds remain the installer's responsibility. + +## Opting out packages into the global virtual store + +A package that needs physical files can use a normal global virtual store (GVS) installation, including its full locked dependency tree and dependency links. The application does not need a `node_modules` directory. GVS itself retains its conventional `node_modules` layout. + +Point the package's manifest entry at its real GVS directory and set `resolution` to `node`: + +```json +{ + "root": "/path/to/store/links/@/tool/1.0.0//node_modules/tool", + "resolution": "node" +} +``` + +The application's dependency map still points to this package's instance ID. Imports originating inside the opted-out package use Node's normal resolution and its physical dependency links. The entry does not need a loader dependency map. Explicit paths to CAS modules remain loadable through the hooks. Map materialized transitive package instances to their GVS roots too, so application imports of those instances use the same modules. Multiple opted-out IDs can share a GVS root when pnpm deduplicates them. + +Use pnpm's normal installer to populate GVS; copying just the selected package's files does not create a complete opt-out. The installer owns graph hashes, package import, linking, build policy, and store registration. Retain the installation that references those entries so store pruning can track their usage. The standalone loader does not install packages or register its manifest with store pruning. Normal CAS installs register the project, and `pnpm store prune` preserves both manifest-referenced blobs and materialized dependency trees. + +This is an explicit compatibility fallback, not an install-time static analyzer. It can materialize a large dependency tree, but ordinary GVS entries can be reused across projects. Packages outside that tree stay in CAS. A tool that directly reads application dependencies outside its own tree may still need additional opt-outs or resolver integration. + +## Supported behavior + +The loader supports ESM and CommonJS, relative modules, dynamic imports, `createRequire`, JSON imports, package `exports` and `imports`, conditional exports, export patterns, package self references, aliases, and workspace dependency contexts. Resolution uses `enhanced-resolve` with Node-oriented settings. + +Stored modules have virtual file URLs under `.pnpm-loader/` next to the manifest. That directory is never created. These URLs retain package identity and source filenames; the loader reads the corresponding bytes from CAS and verifies their hashes. The virtual namespace is reserved and cannot also be a workspace root. + +Only declared dependencies are available through bare imports. There is no fallback to an application's existing `node_modules` tree for a dependency missing from the manifest. Built-in modules continue to use Node's loader. Absolute paths returned by resolvers remain loadable. Relative local application files use Node's normal resolution. Packages without a `package.json` receive an in-memory empty manifest to keep resolution within their package boundary. + +## Compatibility limits + +This loader does not emulate a general filesystem. `import.meta.url`, `__filename`, `__dirname`, and `require.resolve()` for stored modules identify virtual locations. Passing them to ordinary `fs` APIs will not read store assets. Packages that read adjacent assets, scan directories, write into their package directory, or depend on a physical filename need materialization or a separate filesystem integration. + +Native addons, WebAssembly, TypeScript, bundled `node_modules` directories, and package-internal symlinks are not supported by this runtime. JavaScript bins may have no file extension. Native addons may also require neighboring shared libraries, even if their build output is cached. + +Use `.mjs` or `"type": "module"` for ESM. Stored `.js` files without a package type are treated as CommonJS; Node's syntax detection for ambiguous `.js` files is not implemented. The resolver is not a promise of complete Node resolution parity. + +The loader does not run lifecycle scripts, select side-effects cache entries, create command shims, or generate manifests from lockfiles. A package with install scripts can run from stored build outputs if its effective files otherwise meet these compatibility requirements. Runtime and development dependencies have the same filesystem constraints. + +The manifest is trusted configuration, as a lockfile and preload script are. Dependency restrictions and blob verification do not sandbox package code. diff --git a/pnpm/esm-loader/index.mjs b/pnpm/esm-loader/index.mjs new file mode 100644 index 0000000000..c76500cb3a --- /dev/null +++ b/pnpm/esm-loader/index.mjs @@ -0,0 +1,123 @@ +import { isBuiltin, registerHooks } from 'node:module' +import path from 'node:path' +import { fileURLToPath, pathToFileURL, URL } from 'node:url' + +import { createResolver } from './resolver.mjs' +import { loaderError, openStore, within } from './store.mjs' + +/** Register a version 1 store manifest. Returns Node's deregisterable hook handle. */ +export function registerStoreLoader (manifestURL) { + return registerHooks(createStoreHooks(manifestURL)) +} + +export function createStoreHooks (manifestURL) { + const store = openStore(manifestURL) + const resolvePackage = createResolver(store) + const manifests = new Map() + return { + resolve (specifier, context, nextResolve) { + if (!context.parentURL && !isBuiltin(specifier)) { + const entry = storedEntry(specifier, store, manifests) + if (entry) return entry + } + if (isBuiltin(specifier) || !context.parentURL?.startsWith('file:')) { + return nextResolve(specifier, context) + } + const parent = fileURLToPath(context.parentURL) + const owner = store.owner(parent) + if (!owner) return nextResolve(specifier, context) + const request = fileRequest(specifier, context) + if (request === null) return nextResolve(specifier, context) + if (owner.resolution === 'node' && (!request.filename || !within(store.virtualRoot, request.filename))) { + return nextResolve(specifier, context) + } + if (request.filename && (!owner.stored || !request.relative) && !within(store.virtualRoot, request.filename)) { + return nextResolve(specifier, context) + } + if (owner.stored && request.relative && !within(owner.root, request.filename)) { + throw loaderError('ERR_PNPM_LOADER_PATH_ESCAPE', `Import ${specifier} escapes package ${owner.id}`) + } + const resolved = resolvePackage(request.specifier, parent, { conditions: context.conditions, filename: request.filename }) + const url = pathToFileURL(resolved.path) + url.search = request.search || resolved.query || '' + url.hash = request.hash || resolved.fragment || '' + if (within(store.virtualRoot, resolved.path)) { + moduleFormat(resolved.path, store, manifests) + } + return { url: url.href, shortCircuit: true } + }, + load (url, context, nextLoad) { + if (!url.startsWith('file:')) return nextLoad(url, context) + const filename = fileURLToPath(url) + if (!within(store.virtualRoot, filename)) return nextLoad(url, context) + const format = moduleFormat(filename, store, manifests) + if (format === 'json' && !context.conditions.includes('require') && context.importAttributes?.type !== 'json') { + throw loaderError('ERR_IMPORT_ATTRIBUTE_MISSING', `Module ${url} needs an import attribute of type json`) + } + validateAttributes(format, context.importAttributes, url) + return { format, source: store.filesystem.readFileSync(filename), shortCircuit: true } + }, + } +} + +function fileRequest (specifier, context) { + const dotDirectory = context.conditions.includes('require') && (specifier === '.' || specifier === '..') + const relative = dotDirectory || specifier.startsWith('./') || specifier.startsWith('../') + const nativeAbsolute = path.isAbsolute(specifier) && !specifier.startsWith('/') + if (nativeAbsolute || (context.conditions.includes('require') && (relative || path.isAbsolute(specifier)))) { + const filename = path.resolve(path.dirname(fileURLToPath(context.parentURL)), specifier) + return { filename, relative, specifier: escapeFilename(filename) } + } + if (specifier.startsWith('file:') || specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('/')) { + const url = new URL(specifier, context.parentURL) + const filename = fileURLToPath(url) + return { filename, relative, specifier: escapeFilename(filename), search: url.search, hash: url.hash } + } + if (specifier.includes(':')) return null + if (!specifier.startsWith('#') && (specifier.includes('\\') || specifier.split('/').includes('..'))) { + throw loaderError('ERR_PNPM_LOADER_PATH_ESCAPE', `Invalid package subpath: ${specifier}`) + } + return { specifier } +} + +function escapeFilename (filename) { + return filename.replaceAll('#', '\0#').replaceAll('?', '\0?') +} + +function moduleFormat (filename, store, manifests) { + const extension = path.extname(filename) + if (extension === '.mjs') return 'module' + if (extension === '.cjs') return 'commonjs' + if (extension === '.json') return 'json' + if (extension !== '.js' && extension !== '') { + throw loaderError('ERR_PNPM_LOADER_UNSUPPORTED_FORMAT', `Cannot load ${extension || 'extensionless'} store file: ${filename}`) + } + const owner = store.owner(filename) + if (!owner) throw loaderError('ENOENT', `No package in store manifest: ${filename}`) + for (let directory = path.dirname(filename); within(owner.root, directory); directory = path.dirname(directory)) { + const manifestPath = path.join(directory, 'package.json') + if (!store.files.has(manifestPath)) continue + if (!manifests.has(manifestPath)) { + manifests.set(manifestPath, store.filesystem.readJsonSync(manifestPath)) + } + return manifests.get(manifestPath).type === 'module' ? 'module' : 'commonjs' + } + return 'commonjs' +} + +function validateAttributes (format, attributes = {}, url) { + for (const [name, value] of Object.entries(attributes)) { + if (name !== 'type' || value !== 'json') { + throw loaderError('ERR_IMPORT_ATTRIBUTE_UNSUPPORTED', `Unsupported import attribute ${name}=${value} for ${url}`) + } + } + if (format !== 'json' && attributes.type === 'json') { + throw loaderError('ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE', `Module ${url} is not JSON`) + } +} + +function storedEntry (specifier, store, manifests) { + const url = specifier.startsWith('file:') ? new URL(specifier) : path.isAbsolute(specifier) ? pathToFileURL(specifier) : null + if (!url || !store.files.has(fileURLToPath(url))) return null + return { url: url.href, format: moduleFormat(fileURLToPath(url), store, manifests), shortCircuit: true } +} diff --git a/pnpm/esm-loader/install-register.mjs b/pnpm/esm-loader/install-register.mjs new file mode 100644 index 0000000000..4d464a942d --- /dev/null +++ b/pnpm/esm-loader/install-register.mjs @@ -0,0 +1,5 @@ +import { URL } from 'node:url' + +import { registerStoreLoader } from './index.mjs' + +registerStoreLoader(new URL('./.pnpm-store.json', import.meta.url)) diff --git a/pnpm/esm-loader/package.json b/pnpm/esm-loader/package.json new file mode 100644 index 0000000000..aad67c5d23 --- /dev/null +++ b/pnpm/esm-loader/package.json @@ -0,0 +1,29 @@ +{ + "name": "@pnpm/esm-loader", + "version": "0.1.0", + "description": "Experimental Node.js loader for pnpm content-addressable store files", + "license": "MIT", + "type": "module", + "exports": { + ".": "./index.mjs", + "./register": "./register.mjs" + }, + "files": [ + "*.mjs" + ], + "engines": { + "node": ">=26.10.0" + }, + "scripts": { + "test": "node --test test/*.test.mjs && node scripts/bundle-runtime.mjs --check", + ".test": "node --test test/*.test.mjs && node scripts/bundle-runtime.mjs --check" + }, + "dependencies": { + "enhanced-resolve": "5.25.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/pnpm/pnpm.git", + "directory": "pnpm/esm-loader" + } +} diff --git a/pnpm/esm-loader/register.mjs b/pnpm/esm-loader/register.mjs new file mode 100644 index 0000000000..c40ac0151f --- /dev/null +++ b/pnpm/esm-loader/register.mjs @@ -0,0 +1,7 @@ +import path from 'node:path' +import process from 'node:process' +import { pathToFileURL } from 'node:url' + +import { registerStoreLoader } from './index.mjs' + +registerStoreLoader(pathToFileURL(path.resolve(process.env.PNPM_LOADER_MANIFEST ?? '.pnpm-store.json'))) diff --git a/pnpm/esm-loader/resolver.mjs b/pnpm/esm-loader/resolver.mjs new file mode 100644 index 0000000000..c73e61cacc --- /dev/null +++ b/pnpm/esm-loader/resolver.mjs @@ -0,0 +1,51 @@ +import path from 'node:path' +import enhancedResolve from 'enhanced-resolve' + +import { loaderError, within } from './store.mjs' + +export function createResolver (store) { + const resolvers = new Map() + let selected + const pnpApi = { + resolveToUnqualified (name, issuer) { + const owner = store.owner(path.resolve(issuer)) + const dependency = owner?.dependencies.get(name) + if (dependency === undefined) { + throw loaderError('ERR_PNPM_LOADER_UNDECLARED_DEPENDENCY', `Package ${owner?.id ?? issuer} does not declare ${name}`) + } + selected = store.packages.get(dependency) + return selected.root + }, + } + return function resolve (specifier, parent, { conditions, filename }) { + const key = JSON.stringify(conditions) + let resolver = resolvers.get(key) + if (!resolver) { + resolver = enhancedResolve.ResolverFactory.createResolver({ + fileSystem: store.filesystem, + useSyncFileSystemCalls: true, + conditionNames: conditions, + extensions: ['.js', '.json', '.node'], + mainFields: ['main'], + mainFiles: ['index'], + fullySpecified: !conditions.includes('require'), + symlinks: false, + pnpApi, + }) + resolvers.set(key, resolver) + } + selected = store.owner(filename ?? parent) + let resolved + let failure + resolver.resolve({}, path.dirname(parent), specifier, {}, (error, result, details) => { + failure = error + resolved = details + }) + if (failure) throw failure + if (!resolved?.path) throw loaderError('ERR_MODULE_NOT_FOUND', `Cannot resolve ${specifier} from ${parent}`) + if (selected?.stored && !within(selected.root, resolved.path)) { + throw loaderError('ERR_PNPM_LOADER_PATH_ESCAPE', `Resolved ${specifier} escapes package ${selected.id}`) + } + return resolved + } +} diff --git a/pnpm/esm-loader/scripts/audit-cas.mjs b/pnpm/esm-loader/scripts/audit-cas.mjs new file mode 100644 index 0000000000..44d15f1201 --- /dev/null +++ b/pnpm/esm-loader/scripts/audit-cas.mjs @@ -0,0 +1,15 @@ +import fs from 'node:fs' +import { registerHooks } from 'node:module' +import process from 'node:process' + +const loaded = new Set() +if (process.env.PNPM_LOADER_AUDIT) { + registerHooks({ + load (url, context, nextLoad) { + const result = nextLoad(url, context) + if (url.includes('/.pnpm-loader/')) loaded.add(url) + return result + }, + }) + process.on('exit', () => fs.appendFileSync(process.env.PNPM_LOADER_AUDIT, JSON.stringify([...loaded]) + '\n')) +} diff --git a/pnpm/esm-loader/scripts/bundle-runtime.mjs b/pnpm/esm-loader/scripts/bundle-runtime.mjs new file mode 100644 index 0000000000..239e96a55b --- /dev/null +++ b/pnpm/esm-loader/scripts/bundle-runtime.mjs @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict' +import { Buffer } from 'node:buffer' +import fs from 'node:fs' +import { createRequire } from 'node:module' +import process from 'node:process' +import { fileURLToPath, URL } from 'node:url' + +const require = createRequire(new URL('../../../pnpm11/pnpm/package.json', import.meta.url)) +const result = await require('esbuild').build({ + write: false, + entryPoints: [fileURLToPath(new URL('../install-register.mjs', import.meta.url))], + bundle: true, + minify: true, + platform: 'node', + format: 'esm', + outfile: fileURLToPath(new URL('../../../pnpm/crates/deps-restorer/src/cas-loader.mjs.inc', import.meta.url)), + banner: { js: "import { createRequire as bootstrapRequire } from 'node:module'; const require = bootstrapRequire(import.meta.url);" }, +}) + +const output = result.outputFiles[0] +if (process.argv.includes('--check')) { + assert.deepEqual(fs.readFileSync(output.path), Buffer.from(output.contents), 'Regenerate the embedded CAS runtime with scripts/bundle-runtime.mjs') +} else { + fs.writeFileSync(output.path, output.contents) +} diff --git a/pnpm/esm-loader/scripts/compile-svelte-corpus.mjs b/pnpm/esm-loader/scripts/compile-svelte-corpus.mjs new file mode 100644 index 0000000000..5130aef6de --- /dev/null +++ b/pnpm/esm-loader/scripts/compile-svelte-corpus.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict' +import console from 'node:console' +import { createHash } from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { pathToFileURL } from 'node:url' + +import './audit-cas.mjs' + +const repo = process.argv[2] +const { compile } = await import(pathToFileURL(path.join(repo, 'packages/svelte/src/compiler/index.js'))) +const directory = 'packages/svelte/tests/runtime-runes/samples' +const names = fs.readdirSync(path.join(repo, directory)).sort() + .filter(name => fs.existsSync(path.join(repo, directory, name, 'main.svelte'))).slice(0, 100) +assert.equal(names.length, 100) +const outputs = [] +for (const name of names) { + for (const generate of ['client', 'server']) { + const filename = `${directory}/${name}/main.svelte` + const output = compile(fs.readFileSync(path.join(repo, filename), 'utf8'), { filename, generate, experimental: { async: true } }) + const hash = createHash('sha256').update(JSON.stringify({ js: output.js, css: output.css, warnings: output.warnings })).digest('hex') + outputs.push({ filename, generate, hash }) + } +} +console.log(JSON.stringify(outputs)) diff --git a/pnpm/esm-loader/scripts/gvs-installation.mjs b/pnpm/esm-loader/scripts/gvs-installation.mjs new file mode 100644 index 0000000000..06c8c1c951 --- /dev/null +++ b/pnpm/esm-loader/scripts/gvs-installation.mjs @@ -0,0 +1,168 @@ +import { execFileSync } from 'node:child_process' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' + +import { LOCKFILE_VERSION } from '../../../pnpm11/core/constants/lib/index.js' +import { parse as parseDependencyPath, refToRelative } from '../../../pnpm11/deps/path/lib/index.js' +import { readModulesManifest } from '../../../pnpm11/installing/modules-yaml/lib/index.js' +import { readCurrentLockfile, writeWantedLockfile } from '../../../pnpm11/lockfile/fs/lib/index.js' +import { lockfileYamlDump } from '../../../pnpm11/lockfile/fs/lib/write.js' +import { pruneSharedLockfile } from '../../../pnpm11/lockfile/pruner/lib/index.js' +import { nameVerFromPkgSnapshot } from '../../../pnpm11/lockfile/utils/lib/index.js' +import { readWorkspaceManifest } from '../../../pnpm11/workspace/workspace-manifest-reader/lib/index.js' +import { within } from '../store.mjs' +import { findDependency } from './repository-fixture.mjs' + +export async function installOptOuts (source, manifest, names) { + const selected = selectPackages(manifest, names) + const modules = await readModulesManifest(path.join(source.repo, 'node_modules')) + if (!modules) throw new Error(`No installed dependency graph in ${source.repo}`) + const lockfileOptions = { ignoreIncompatible: false, wantedVersions: [LOCKFILE_VERSION] } + const lockfile = await readCurrentLockfile(modules.virtualStoreDir, lockfileOptions) ?? + await readCurrentLockfile(path.join(source.repo, 'node_modules/.pnpm'), lockfileOptions) + if (!lockfile) throw new Error(`No installed lockfile in ${modules.virtualStoreDir}`) + const packageManifest = JSON.parse(fs.readFileSync(path.join(source.repo, 'package.json'), 'utf8')) + const config = { ...packageManifest.pnpm, ...await readWorkspaceManifest(source.repo) } + const locations = snapshotLocations(lockfile, source.repo) + const installation = fs.mkdtempSync(path.join(os.tmpdir(), 'pnpm-loader-gvs-')) + const { importer, aliases } = createSelectedImporter(selected, { source, locations }) + const selectedLockfile = pruneSharedLockfile({ ...lockfile, importers: { '.': importer } }) + // The frozen snapshots already include hook results; staging must not execute the hooks again. + delete selectedLockfile.pnpmfileChecksum + delete selectedLockfile.untrackedPnpmfileReadPackageHook + rejectWorkspaceLinks(selectedLockfile) + fs.writeFileSync(path.join(installation, 'package.json'), JSON.stringify({ private: true, dependencies: importer.specifiers })) + const selectedConfig = pruneUnusedPatches(selectedLockfile, config) + fs.writeFileSync(path.join(installation, 'pnpm-workspace.yaml'), lockfileYamlDump(installationConfig(selectedConfig, source.repo))) + await writeWantedLockfile(installation, selectedLockfile) + const output = execFileSync('pnpm', ['install', '--frozen-lockfile', '--ignore-scripts'], { + cwd: installation, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, + env: { ...process.env, NODE_OPTIONS: '', NODE_PATH: '', CI: 'true' }, + }) + fs.writeFileSync(path.join(installation, 'install.log'), output) + const roots = aliases.map(({ id, alias }) => ({ id, directory: findDependency(installation, alias) })) + const closure = mapInstalledClosure(manifest, roots, { source, locations }) + const globalVirtualStoreDir = await verifyGlobalStore({ installation, config, repo: source.repo }, closure.materialized) + return { installation, globalVirtualStoreDir, selected, ...closure } +} + +function pruneUnusedPatches (lockfile, config) { + const hashes = new Set(Object.keys(lockfile.packages ?? {}).map(id => parseDependencyPath(id).patchHash)) + lockfile.patchedDependencies = Object.fromEntries(Object.entries(lockfile.patchedDependencies ?? {}).filter(([, hash]) => hashes.has(`(patch_hash=${hash})`))) + const patchedDependencies = Object.fromEntries(Object.entries(config.patchedDependencies ?? {}).filter(([name]) => Object.hasOwn(lockfile.patchedDependencies, name))) + return { ...config, patchedDependencies } +} + +function createSelectedImporter (selected, { source, locations }) { + const importer = { specifiers: {}, dependencies: {} } + const aliases = selected.map((id, index) => { + const metadata = locations.get(source.packages[id]) + if (!metadata) throw new Error(`Cannot find the locked package context for ${id}`) + const alias = `opt-out-${index}` + importer.specifiers[alias] = `npm:${metadata.name}@${metadata.version}` + importer.dependencies[alias] = metadata.depPath + return { id, alias } + }) + return { importer, aliases } +} + +async function verifyGlobalStore ({ installation, config, repo }, materialized) { + const installedModules = await readModulesManifest(path.join(installation, 'node_modules')) + if (!installedModules) throw new Error(`Missing installation state in ${installation}`) + const globalVirtualStoreDir = fs.realpathSync(config.globalVirtualStoreDir + ? path.resolve(repo, config.globalVirtualStoreDir) + : path.join(installedModules.storeDir, 'links')) + for (const directory of Object.values(materialized)) { + if (!within(globalVirtualStoreDir, directory)) throw new Error(`Package was not installed into GVS: ${directory}`) + } + return globalVirtualStoreDir +} + +function selectPackages (manifest, names) { + if (names.length === 0) throw new Error('Select at least one package to opt out of CAS loading') + const selected = new Set() + for (const name of names) { + const matches = Object.keys(manifest.packages).filter(id => manifest.packages[id].files && (id === name || id.startsWith(name + '@'))) + if (matches.length === 0) throw new Error(`No stored package matches ${name}`) + for (const id of matches) selected.add(id) + } + return [...selected] +} + +function snapshotLocations (lockfile, repo) { + const locations = new Map() + const pending = Object.entries(lockfile.importers).map(([relative, snapshot]) => ({ directory: path.resolve(repo, relative), snapshot })) + while (pending.length > 0) { + const { directory, snapshot } = pending.pop() + const dependencies = { ...snapshot.dependencies, ...snapshot.devDependencies, ...snapshot.optionalDependencies } + for (const [alias, reference] of Object.entries(dependencies)) { + const depPath = refToRelative(reference, alias) + const pkgSnapshot = lockfile.packages?.[depPath] + if (!pkgSnapshot) continue + const installed = findDependency(directory, alias) + if (!installed || locations.has(installed)) continue + locations.set(installed, { depPath, pkgSnapshot, ...nameVerFromPkgSnapshot(depPath, pkgSnapshot) }) + pending.push({ directory: installed, snapshot: pkgSnapshot }) + } + } + return locations +} + +function installationConfig (config, repo) { + const patchedDependencies = Object.fromEntries(Object.entries(config.patchedDependencies ?? {}).map(([name, filename]) => [name, path.resolve(repo, filename)])) + return { + ...config, packages: [], patchedDependencies, + virtualStoreType: 'global', storeDir: config.storeDir && path.resolve(repo, config.storeDir), + virtualStoreDir: undefined, globalVirtualStoreDir: config.globalVirtualStoreDir && path.resolve(repo, config.globalVirtualStoreDir), + ignoreScripts: true, ignorePnpmfile: true, + nodeLinker: 'isolated', hoistPattern: [], publicHoistPattern: [], + } +} + +function rejectWorkspaceLinks (lockfile) { + for (const [id, pkg] of Object.entries(lockfile.packages ?? {})) { + if (id.startsWith('file:') || pkg.resolution?.type === 'directory') { + throw new Error(`Opt-out graph contains a local package: ${id}`) + } + for (const target of Object.values({ ...pkg.dependencies, ...pkg.optionalDependencies })) { + if (target.startsWith('link:') || target.startsWith('file:')) { + throw new Error(`Opt-out graph contains a local dependency: ${id} -> ${target}`) + } + } + } +} + +function mapInstalledClosure (manifest, roots, { source, locations }) { + const materialized = new Map() + const unlockedDependencies = [] + const skippedOptionalDependencies = [] + const pending = [...roots] + while (pending.length > 0) { + const { id, directory } = pending.pop() + if (!directory) throw new Error(`Missing installed GVS package for ${id}`) + if (materialized.has(id)) { + if (materialized.get(id) !== directory) throw new Error(`Conflicting GVS contexts for ${id}`) + continue + } + const pkg = manifest.packages[id] + if (!pkg.files) throw new Error(`Opt-out graph reaches workspace package ${id}`) + materialized.set(id, directory) + const snapshot = locations.get(source.packages[id])?.pkgSnapshot + for (const [name, target] of Object.entries(pkg.dependencies)) { + if (target === id) continue + if (!Object.hasOwn(snapshot?.dependencies ?? {}, name) && !Object.hasOwn(snapshot?.optionalDependencies ?? {}, name)) { + unlockedDependencies.push({ issuer: id, name, target }) + continue + } + const dependencyRoot = findDependency(directory, name) + if (!dependencyRoot && Object.hasOwn(snapshot.optionalDependencies ?? {}, name)) { + skippedOptionalDependencies.push({ issuer: id, name, target }) + continue + } + pending.push({ id: target, directory: dependencyRoot }) + } + } + return { materialized: Object.fromEntries(materialized), unlockedDependencies, skippedOptionalDependencies } +} diff --git a/pnpm/esm-loader/scripts/repository-fixture.mjs b/pnpm/esm-loader/scripts/repository-fixture.mjs new file mode 100644 index 0000000000..26ea9987c6 --- /dev/null +++ b/pnpm/esm-loader/scripts/repository-fixture.mjs @@ -0,0 +1,145 @@ +import { execFileSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import fs from 'node:fs' +import { createRequire } from 'node:module' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath, URL } from 'node:url' + +export async function prepareRepository (repo, options = {}) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pnpm-loader-battle-')) + const projects = JSON.parse(execFileSync('pnpm', ['-r', 'list', '--depth=-1', '--json'], { cwd: repo, encoding: 'utf8' })) + const workspacePaths = new Set(projects.map(project => project.path)) + workspacePaths.add(repo) + const state = { + repo, root, workspacePaths, ids: new Map(), missing: [], fileCount: 0, bytes: 0, + manifest: { version: 1, storeDir: './store', packages: {} }, + } + const entries = options.fullRepository ? [...workspacePaths] : [repo, path.join(repo, 'pnpm11/pnpm'), path.join(repo, 'pnpm11/cli/parse-cli-args')] + for (const directory of entries) addPackage(state, directory) + inheritWorkspaceDependencies(state.manifest) + if (options.fullRepository) copyDirectory(repo, path.join(root, 'repo')) + else copyWorkspaces(state) + await bundleLoader(root) + fs.writeFileSync(path.join(root, '.pnpm-store.json'), JSON.stringify(state.manifest)) + fs.writeFileSync(path.join(root, 'repository-source.json'), JSON.stringify({ repo, packages: Object.fromEntries([...state.ids].map(([directory, id]) => [id, directory])) })) + fs.writeFileSync(path.join(root, 'missing-dependencies.json'), JSON.stringify(state.missing, null, 2)) + fs.writeFileSync(path.join(root, 'repo/run-jest.cjs'), "require('jest').run(process.argv.slice(2))\n") + assertIsolated(root) + return state +} + +function addPackage (state, directory) { + if (state.ids.has(directory)) return state.ids.get(directory) + const pkg = JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8').trimStart()) + const workspace = state.workspacePaths.has(directory) + const id = workspace ? path.relative(state.repo, directory) || '.' : `${pkg.name}@${pkg.version}:${digest(directory, 'sha256').slice(0, 16)}` + const entry = { dependencies: {} } + state.ids.set(directory, id) + state.manifest.packages[id] = entry + if (typeof pkg.name === 'string' && findDependency(directory, pkg.name) === directory) entry.dependencies[pkg.name] = id + if (workspace) entry.root = path.join('repo', path.relative(state.repo, directory)) + else entry.files = storeFiles(state, directory) + const dependencies = { ...pkg.dependencies, ...pkg.optionalDependencies, ...pkg.peerDependencies, ...(workspace ? pkg.devDependencies : {}) } + for (const name of Object.keys(dependencies)) { + const resolved = findDependency(directory, name) + if (resolved) entry.dependencies[name] = addPackage(state, resolved) + else state.missing.push({ issuer: id, name }) + } + return id +} + +function storeFiles (state, directory) { + const files = {} + for (const [relative, filename] of walkFiles(directory)) { + const source = fs.readFileSync(filename) + const executable = (fs.statSync(filename).mode & 0o111) !== 0 + const hash = digest(source, 'sha512') + (executable ? '-exec' : '') + files[relative] = hash + const blob = path.join(state.root, 'store/files', hash.slice(0, 2), hash.slice(2)) + if (!fs.existsSync(blob)) { + fs.mkdirSync(path.dirname(blob), { recursive: true }) + fs.writeFileSync(blob, source) + state.bytes += source.length + } + state.fileCount++ + } + return files +} + +function digest (source, algorithm) { + return createHash(algorithm).update(source).digest('hex') +} + +function walkFiles (directory, prefix = '') { + const files = [] + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (excluded(entry.name)) continue + const relative = prefix + entry.name + const filename = path.join(directory, entry.name) + if (entry.isDirectory()) files.push(...walkFiles(filename, relative + '/')) + else if (entry.isFile() || (entry.isSymbolicLink() && fs.statSync(filename).isFile())) files.push([relative, filename]) + } + return files +} + +export function findDependency (issuer, name) { + for (let directory = issuer; ; directory = path.dirname(directory)) { + const filename = path.join(directory, 'node_modules', name, 'package.json') + if (fs.existsSync(filename)) return fs.realpathSync(path.dirname(filename)) + if (path.dirname(directory) === directory) return null + } +} + +function copyWorkspaces ({ repo, root, manifest }) { + fs.mkdirSync(path.join(root, 'repo'), { recursive: true }) + copyDirectory(path.join(repo, 'pnpm11'), path.join(root, 'repo/pnpm11')) + for (const entry of Object.values(manifest.packages)) { + if (!entry.root || entry.root === 'repo' || entry.root.startsWith(`repo${path.sep}pnpm11`)) continue + copyDirectory(path.join(repo, path.relative('repo', entry.root)), path.join(root, entry.root)) + } + for (const name of ['package.json', 'pnpm-workspace.yaml', 'pnpm-lock.yaml']) { + fs.copyFileSync(path.join(repo, name), path.join(root, 'repo', name)) + } +} + +function copyDirectory (source, target) { + fs.cpSync(source, target, { + recursive: true, verbatimSymlinks: true, + filter: filename => !path.relative(source, filename).split(path.sep).some(excluded), + }) +} + +function excluded (name) { + return ['node_modules', '.git', 'target', '.cache', '.jest-cache', 'coverage', '_tmp'].includes(name) +} + +export function assertIsolated (root, directory = root) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const filename = path.join(directory, entry.name) + if (entry.name === 'node_modules') throw new Error(`Unexpected node_modules: ${filename}`) + if (entry.isDirectory()) assertIsolated(root, filename) + if (entry.isSymbolicLink()) { + const target = path.resolve(path.dirname(filename), fs.readlinkSync(filename)) + if (!target.startsWith(root + path.sep)) throw new Error(`External symlink: ${filename} -> ${target}`) + } + } +} + +function inheritWorkspaceDependencies (manifest) { + const workspaces = Object.values(manifest.packages).filter(entry => entry.root).sort((first, second) => first.root.length - second.root.length) + for (const entry of workspaces) { + const parent = workspaces.findLast(candidate => entry.root.startsWith(candidate.root + path.sep)) + if (parent) entry.dependencies = { ...parent.dependencies, ...entry.dependencies } + } +} + +export async function bundleLoader (root) { + fs.copyFileSync(new URL('./audit-cas.mjs', import.meta.url), path.join(root, 'audit-cas.mjs')) + const require = createRequire(new URL('../../../pnpm11/pnpm/package.json', import.meta.url)) + await require('esbuild').build({ + entryPoints: [fileURLToPath(new URL('../register.mjs', import.meta.url))], + bundle: true, platform: 'node', format: 'esm', outfile: path.join(root, 'loader.mjs'), + banner: { js: "import { createRequire as bootstrapRequire } from 'node:module'; const require = bootstrapRequire(import.meta.url);" }, + }) +} diff --git a/pnpm/esm-loader/scripts/repository-scenario.mjs b/pnpm/esm-loader/scripts/repository-scenario.mjs new file mode 100644 index 0000000000..39832b1e87 --- /dev/null +++ b/pnpm/esm-loader/scripts/repository-scenario.mjs @@ -0,0 +1,46 @@ +import { spawnSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { pathToFileURL } from 'node:url' + +export function runSuite (root, manifestPath, scenario) { + const auditPath = path.join(root, 'suite-cas-loads.jsonl') + const result = spawnSync(process.execPath, [ + '--import', pathToFileURL(path.join(root, 'loader.mjs')).href, '--experimental-vm-modules', + ...(scenario.nodeArgs ?? [path.join(root, scenario.entry), ...scenario.args]), + ], { + cwd: path.join(root, scenario.cwd), encoding: 'utf8', timeout: 120000, + env: { ...process.env, NODE_PATH: '', ...auditEnvironment(root, auditPath), PNPM_LOADER_MANIFEST: manifestPath, UNRS_RESOLVER_NODE_RESOLUTION: '1' }, + }) + if (result.error && result.error.code !== 'ETIMEDOUT') throw result.error + return { ...result, ...readAudit(auditPath) } +} + +export function readScenario (root) { + const report = path.join(root, 'ecosystem-results.json') + if (fs.existsSync(report)) { + const { runner = 'vitest', cwd = '.', entry = `repo/run-${runner}.mjs`, args } = JSON.parse(fs.readFileSync(report, 'utf8')) + const scenario = { cwd: path.join('repo', cwd) } + return runner === 'node' ? { ...scenario, nodeArgs: args } : { ...scenario, entry, args } + } + return { + entry: 'repo/run-jest.cjs', cwd: 'repo/pnpm11/cli/parse-cli-args', + args: ['--runInBand', '--no-cache', '--coverage=false', '--runTestsByPath', 'test/index.ts'], + } +} + +export function auditEnvironment (root, filename) { + fs.writeFileSync(filename, '') + const preloads = ['loader.mjs', 'audit-cas.mjs'].map(name => '--import=' + pathToFileURL(path.join(root, name)).href) + return { NODE_OPTIONS: preloads.join(' '), PNPM_LOADER_AUDIT: filename } +} + +export function readAudit (filename) { + const lines = fs.readFileSync(filename, 'utf8').trim() + const loaded = new Set(lines ? lines.split('\n').flatMap(line => JSON.parse(line)) : []) + return { + casModules: loaded.size, + casPackages: new Set([...loaded].map(url => url.split('/.pnpm-loader/')[1].split('/')[0])).size, + } +} diff --git a/pnpm/esm-loader/scripts/test-ecosystem.mjs b/pnpm/esm-loader/scripts/test-ecosystem.mjs new file mode 100644 index 0000000000..d6c293446e --- /dev/null +++ b/pnpm/esm-loader/scripts/test-ecosystem.mjs @@ -0,0 +1,63 @@ +import { execFileSync, spawnSync } from 'node:child_process' +import console from 'node:console' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { pathToFileURL } from 'node:url' + +import { assertIsolated, findDependency, prepareRepository } from './repository-fixture.mjs' +import { auditEnvironment, readAudit } from './repository-scenario.mjs' + +const runner = { '--node': 'node', '--mocha': 'mocha', '--jest': 'jest' }[process.argv[2]] ?? 'vitest' +const inputs = process.argv.slice(runner === 'vitest' ? 2 : 3) +const cwd = inputs[0] === '--cwd' ? inputs.splice(0, 2)[1] : '.' +const [repository, ...args] = inputs +if (!repository || args.length === 0) throw new Error('Usage: test-ecosystem.mjs [--node|--mocha|--jest] [--cwd ] ') +if (!cwd || path.isAbsolute(cwd) || path.normalize(cwd).split(path.sep).includes('..')) throw new Error('The working directory must be inside the checkout') +const repo = fs.realpathSync(repository) +const env = { ...process.env, NODE_OPTIONS: '', NODE_PATH: '' } +const runnerRoot = runner === 'node' ? null : findDependency(path.join(repo, cwd), runner) +if (runner !== 'node' && !runnerRoot) throw new Error(`Cannot find ${runner} from ${path.join(repo, cwd)}`) +const runnerMain = runnerRoot ? readBin(runnerRoot, runner) : null +const baselineArgs = runner === 'node' ? args : [path.join(runnerRoot, runnerMain), ...args] +const baseline = run(baselineArgs, { cwd: path.join(repo, cwd), env }) +console.log(baseline.stdout, baseline.stderr) +if (baseline.status !== 0) throw new Error('The normal repository baseline failed') +const prepared = await prepareRepository(repo, { fullRepository: true }) +const { root, manifest } = prepared +const entry = path.join('repo', cwd, `run-${runner}.mjs`) +const runnerEntry = path.join(root, entry) +if (runner !== 'node') fs.writeFileSync(runnerEntry, `await import(new URL(${JSON.stringify('./' + runnerMain)}, import.meta.resolve('${runner}/package.json')))\n`) +const auditPath = path.join(root, 'cas-loads.jsonl') +const casArgs = runner === 'node' ? args : [runnerEntry, ...args] +const cas = run(['--import', pathToFileURL(path.join(root, 'loader.mjs')).href, ...casArgs], { + cwd: path.join(root, 'repo', cwd), env: { ...env, ...auditEnvironment(root, auditPath), PNPM_LOADER_MANIFEST: path.join(root, '.pnpm-store.json') }, +}) +for (const [name, result] of Object.entries({ baseline, cas })) { + fs.writeFileSync(path.join(root, name + '.stdout'), result.stdout) + fs.writeFileSync(path.join(root, name + '.stderr'), result.stderr) +} +assertIsolated(root) +const report = { + repo, revision: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim(), + node: process.version, runner, cwd, entry, args, baseline: baseline.status, cas: cas.status, noNodeModules: true, + casError: cas.error?.message, casSignal: cas.signal, + packages: prepared.ids.size, storedPackages: Object.values(manifest.packages).filter(pkg => pkg.files).length, + files: prepared.fileCount, bytes: prepared.bytes, ...readAudit(auditPath), +} +fs.writeFileSync(path.join(root, 'ecosystem-results.json'), JSON.stringify(report, null, 2)) +console.log(JSON.stringify({ root, ...report }, null, 2), cas.stdout, cas.stderr) +process.exitCode = cas.status ?? 1 + +function readBin (directory, name) { + const { bin } = JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8')) + const entry = typeof bin === 'string' ? bin : bin?.[name] + if (typeof entry !== 'string') throw new Error(`No ${name} binary declared in ${directory}`) + return entry +} + +function run (args, options) { + const result = spawnSync(process.execPath, args, { ...options, encoding: 'utf8', timeout: 180000, maxBuffer: 16 * 1024 * 1024 }) + if (result.error && result.error.code !== 'ETIMEDOUT') throw result.error + return result +} diff --git a/pnpm/esm-loader/scripts/test-gvs-repository.mjs b/pnpm/esm-loader/scripts/test-gvs-repository.mjs new file mode 100644 index 0000000000..60a62a091a --- /dev/null +++ b/pnpm/esm-loader/scripts/test-gvs-repository.mjs @@ -0,0 +1,30 @@ +import console from 'node:console' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' + +import { installOptOuts } from './gvs-installation.mjs' +import { assertIsolated, bundleLoader } from './repository-fixture.mjs' +import { readScenario, runSuite } from './repository-scenario.mjs' + +const [fixture, ...names] = process.argv.slice(2) +if (!fixture) throw new Error('Usage: test-gvs-repository.mjs ') +const root = path.resolve(fixture) +assertIsolated(root) +await bundleLoader(root) +const manifest = JSON.parse(fs.readFileSync(path.join(root, '.pnpm-store.json'), 'utf8')) +const source = JSON.parse(fs.readFileSync(path.join(root, 'repository-source.json'), 'utf8')) +const installed = await installOptOuts(source, manifest, names) +for (const [id, directory] of Object.entries(installed.materialized)) { + manifest.packages[id] = { root: directory, resolution: 'node' } +} +const manifestPath = path.join(root, '.pnpm-gvs.json') +fs.writeFileSync(manifestPath, JSON.stringify(manifest)) +const result = runSuite(root, manifestPath, readScenario(root)) +fs.writeFileSync(path.join(root, 'gvs.stdout'), result.stdout) +fs.writeFileSync(path.join(root, 'gvs.stderr'), result.stderr) +assertIsolated(root) +const report = { ...installed, status: result.status, error: result.error?.message, signal: result.signal, casModules: result.casModules, casPackages: result.casPackages, noProjectNodeModules: true } +fs.writeFileSync(path.join(root, 'gvs-results.json'), JSON.stringify(report, null, 2)) +console.log(JSON.stringify({ installation: installed.installation, globalVirtualStoreDir: installed.globalVirtualStoreDir, selected: installed.selected, materializedPackages: Object.keys(installed.materialized).length, status: result.status, casModules: result.casModules, casPackages: result.casPackages, noProjectNodeModules: true }, null, 2), result.stdout, result.stderr) +process.exitCode = result.status ?? 1 diff --git a/pnpm/esm-loader/scripts/test-repository.mjs b/pnpm/esm-loader/scripts/test-repository.mjs new file mode 100644 index 0000000000..6694e0591d --- /dev/null +++ b/pnpm/esm-loader/scripts/test-repository.mjs @@ -0,0 +1,66 @@ +import { spawnSync } from 'node:child_process' +import console from 'node:console' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath, URL } from 'node:url' + +import { assertIsolated, prepareRepository } from './repository-fixture.mjs' + +const repo = path.resolve(fileURLToPath(new URL('../../..', import.meta.url))) +const suite = 'pnpm11/cli/parse-cli-args' +const baseline = run(process.execPath, [path.join(repo, 'node_modules/jest/bin/jest.js'), '--runInBand', '--coverage=false', '--runTestsByPath', 'test/index.ts'], { + cwd: path.join(repo, suite), env: { ...process.env, NODE_OPTIONS: '--experimental-vm-modules --disable-warning=ExperimentalWarning' }, +}) +if (baseline.status !== 0) throw new Error(`Baseline failed:\n${baseline.stderr}`) +console.log(baseline.stderr) +const prepared = await prepareRepository(repo) +const { root } = prepared +console.log(`Prepared ${prepared.ids.size} packages and ${prepared.fileCount} files (${Math.round(prepared.bytes / 1024 / 1024)} MiB) in ${root}`) +const env = { + ...process.env, NODE_PATH: '', NODE_OPTIONS: '', XDG_CONFIG_HOME: path.join(root, 'config'), + PNPM_LOADER_MANIFEST: path.join(root, '.pnpm-store.json'), UNRS_RESOLVER_NODE_RESOLUTION: '1', +} +const cases = [ + { name: 'parse-cli-args', directory: suite, entry: 'repo/run-jest.cjs', args: ['--runInBand', '--coverage=false', '--runTestsByPath', 'test/index.ts'] }, + { name: 'pnpm-cli-tests', directory: 'pnpm11/pnpm', entry: 'repo/run-jest.cjs', args: ['--runInBand', '--coverage=false', '--runTestsByPath', 'test/withCommand.test.ts'] }, + { name: 'unbundled-version', directory: 'pnpm11/pnpm', entry: 'repo/pnpm11/pnpm/lib/pnpm.js', args: ['with', 'current', '--version'] }, + { name: 'unbundled-help', directory: 'pnpm11/pnpm', entry: 'repo/pnpm11/pnpm/lib/pnpm.js', args: ['with', 'current', 'help'] }, +] +const results = [] +for (const testCase of cases) { + const args = ['--import', path.join(root, 'loader.mjs'), '--experimental-vm-modules', path.join(root, testCase.entry), ...testCase.args] + const result = run(process.execPath, args, { cwd: path.join(root, 'repo', testCase.directory), env }) + fs.writeFileSync(path.join(root, testCase.name + '.stdout'), result.stdout) + fs.writeFileSync(path.join(root, testCase.name + '.stderr'), result.stderr) + results.push({ name: testCase.name, status: result.status, args }) + console.log(`${testCase.name}: exit ${result.status}\n${result.stderr}`) +} +diagnoseJestResolver(prepared, env) +assertIsolated(root) +fs.writeFileSync(path.join(root, 'results.json'), JSON.stringify({ baselinePassed: true, noNodeModules: true, packages: prepared.ids.size, files: prepared.fileCount, results }, null, 2)) +console.log(`Results and retained fixture: ${root}`) +process.exitCode = results.some(result => result.status !== 0) ? 1 : 0 + +function run (command, args, options) { + const result = spawnSync(command, args, { ...options, encoding: 'utf8', timeout: 120000, maxBuffer: 16 * 1024 * 1024 }) + if (result.error) throw result.error + return result +} + +function diagnoseJestResolver ({ root, manifest }, env) { + const diagnostic = JSON.parse(JSON.stringify(manifest)) + const jestId = diagnostic.packages['.'].dependencies.jest + const cliId = diagnostic.packages[jestId].dependencies['jest-cli'] + const configId = diagnostic.packages[cliId].dependencies['jest-config'] + diagnostic.packages['.'].dependencies['jest-resolve'] = diagnostic.packages[configId].dependencies['jest-resolve'] + const manifestPath = path.join(root, '.pnpm-diagnostic.json') + const entry = path.join(root, 'repo/diagnose.cjs') + fs.writeFileSync(manifestPath, JSON.stringify(diagnostic)) + fs.writeFileSync(entry, "const Resolver = require('jest-resolve').default; Resolver.findNodeModule('@pnpm/jest-config/jest-preset.js', { basedir: process.cwd(), throwIfNotFound: true });\n") + const result = run(process.execPath, ['--import', path.join(root, 'loader.mjs'), entry], { + cwd: path.join(root, 'repo', suite), env: { ...env, PNPM_LOADER_MANIFEST: manifestPath }, + }) + fs.writeFileSync(path.join(root, 'jest-resolver.stderr'), result.stderr) + console.log(`Resolver diagnostics: ${path.join(root, 'jest-resolver.stderr')}`) +} diff --git a/pnpm/esm-loader/scripts/test-selective-repository.mjs b/pnpm/esm-loader/scripts/test-selective-repository.mjs new file mode 100644 index 0000000000..6efe0eefdc --- /dev/null +++ b/pnpm/esm-loader/scripts/test-selective-repository.mjs @@ -0,0 +1,78 @@ +import console from 'node:console' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { pathToFileURL } from 'node:url' + +import { openStore } from '../store.mjs' +import { assertIsolated, bundleLoader } from './repository-fixture.mjs' +import { readScenario, runSuite } from './repository-scenario.mjs' + +if (!process.argv[2]) throw new Error('Pass the retained fixture path from test-repository.mjs') +const root = path.resolve(process.argv[2]) +await bundleLoader(root) +const manifestURL = pathToFileURL(path.join(root, '.pnpm-store.json')) +const manifest = JSON.parse(fs.readFileSync(manifestURL, 'utf8')) +const store = openStore(manifestURL) +const manifestPath = path.join(root, '.pnpm-selective.json') +const materialized = [] +const attempts = [] +const scenario = readScenario(root) +let materializedBytes = 0 +let materializedFiles = 0 +let status = 1 +assertIsolated(root) +for (const name of process.argv.slice(3)) { + const packages = [...store.packages.values()].filter(pkg => pkg.stored && pkg.id.startsWith(name + '@')) + if (packages.length === 0) throw new Error(`No stored package matches ${name}`) + for (const pkg of packages) materializeAndRecord(pkg) +} +for (let attempt = 0; attempt <= store.packages.size; attempt++) { + fs.writeFileSync(manifestPath, JSON.stringify(manifest)) + const result = runSuite(root, manifestPath, scenario) + status = result.status ?? 1 + fs.writeFileSync(path.join(root, `selective-${attempt}.stderr`), result.stderr) + fs.writeFileSync(path.join(root, `selective-${attempt}.stdout`), result.stdout) + const missingPackage = findMissingPackage(result.stderr, store) + attempts.push({ status, missingPackage: missingPackage?.id }) + if (status === 0 || !missingPackage || materialized.includes(missingPackage.id)) { + console.log(result.stdout, result.stderr) + break + } + materializeAndRecord(missingPackage) +} +assertIsolated(root) +const report = { status, noNodeModules: true, materialized, materializedFiles, materializedBytes, attempts } +fs.writeFileSync(path.join(root, 'selective-results.json'), JSON.stringify(report, null, 2)) +console.log(`${materialized.length} packages, ${materializedFiles} files, ${(materializedBytes / 1024 / 1024).toFixed(1)} MiB materialized; exit ${status}`) +process.exitCode = status + +function findMissingPackage (stderr, store) { + const line = stderr.split('\n').find(line => (line.includes('ENOENT') || line.includes('Cannot load .node')) && line.includes(store.virtualRoot + path.sep)) + if (!line) return undefined + return [...store.packages.values()].find(pkg => pkg.stored && line.includes(pkg.root + path.sep)) +} + +function materialize (root, store, pkg) { + const directory = path.join('materialized', path.basename(pkg.root)) + let bytes = 0 + for (const [name, hash] of Object.entries(pkg.files)) { + const source = store.filesystem.readFileSync(path.join(pkg.root, name)) + const filename = path.join(root, directory, name) + fs.mkdirSync(path.dirname(filename), { recursive: true }) + fs.writeFileSync(filename, source) + fs.chmodSync(filename, hash.endsWith('-exec') ? 0o755 : 0o644) + bytes += source.length + } + return { directory, files: Object.keys(pkg.files).length, bytes } +} + +function materializeAndRecord (pkg) { + if (materialized.includes(pkg.id)) return + const stats = materialize(root, store, pkg) + manifest.packages[pkg.id] = { root: stats.directory, dependencies: Object.fromEntries(pkg.dependencies) } + materialized.push(pkg.id) + materializedFiles += stats.files + materializedBytes += stats.bytes + console.log(`Materialized ${pkg.id}`) +} diff --git a/pnpm/esm-loader/scripts/test-svelte-compiler.mjs b/pnpm/esm-loader/scripts/test-svelte-compiler.mjs new file mode 100644 index 0000000000..dbf8c8c6bf --- /dev/null +++ b/pnpm/esm-loader/scripts/test-svelte-compiler.mjs @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import console from 'node:console' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath, pathToFileURL, URL } from 'node:url' + +import { assertIsolated } from './repository-fixture.mjs' +import { readAudit } from './repository-scenario.mjs' + +if (!process.argv[2]) throw new Error('Pass the retained Svelte fixture path from test-ecosystem.mjs') +const root = path.resolve(process.argv[2]) +const { repo, revision } = JSON.parse(fs.readFileSync(path.join(root, 'ecosystem-results.json'), 'utf8')) +const entry = fileURLToPath(new URL('./compile-svelte-corpus.mjs', import.meta.url)) +const env = { ...process.env, NODE_OPTIONS: '', NODE_PATH: '' } +const baseline = run([entry, repo], env) +const auditPath = path.join(root, 'compiler-cas-loads.jsonl') +fs.writeFileSync(auditPath, '') +const cas = run(['--import', pathToFileURL(path.join(root, 'loader.mjs')).href, entry, path.join(root, 'repo')], { + ...env, PNPM_LOADER_MANIFEST: path.join(root, '.pnpm-store.json'), PNPM_LOADER_AUDIT: auditPath, +}) +assert.deepEqual(cas, baseline) +assertIsolated(root) +const report = { + revision, node: process.version, compiled: cas.length, outputsMatch: true, materializedPackages: 0, + ...readAudit(auditPath), +} +assert.ok(report.casPackages > 0) +fs.writeFileSync(path.join(root, 'compiler-results.json'), JSON.stringify(report, null, 2)) +console.log(report) + +function run (args, env) { + const result = spawnSync(process.execPath, args, { env, encoding: 'utf8', timeout: 120000, maxBuffer: 16 * 1024 * 1024 }) + if (result.error) throw result.error + assert.equal(result.status, 0, result.stderr) + return JSON.parse(result.stdout) +} diff --git a/pnpm/esm-loader/scripts/test-unrs-resolver.mjs b/pnpm/esm-loader/scripts/test-unrs-resolver.mjs new file mode 100644 index 0000000000..04c2f67772 --- /dev/null +++ b/pnpm/esm-loader/scripts/test-unrs-resolver.mjs @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' +import { test } from 'node:test' +import { URL } from 'node:url' + +import { fixture } from '../test/fixture.mjs' + +const repoRequire = createRequire(new URL('../../../package.json', import.meta.url)) +const jestRequire = createRequire(repoRequire.resolve('jest')) +const cliRequire = createRequire(jestRequire.resolve('jest-cli')) +const configRequire = createRequire(cliRequire.resolve('jest-config')) +const resolverRequire = createRequire(configRequire.resolve('jest-resolve')) +const unrs = path.dirname(resolverRequire.resolve('unrs-resolver')) + +test('patched unrs uses Node hooks without a native binding or node_modules', context => { + const setup = fixture(context) + setup.add('unrs', Object.fromEntries(['index.js', 'node-resolution.cjs', 'package.json'].map(name => [name, fs.readFileSync(path.join(unrs, name))]))) + setup.add('example', { + 'package.json': JSON.stringify({ exports: { custom: './custom.js', require: './index.js' } }), + 'index.js': 'module.exports = 42', 'custom.js': 'module.exports = 7', + }) + setup.manifest.packages['.'].dependencies = { 'unrs-resolver': 'unrs', example: 'example' } + const result = setup.run(` + import assert from 'node:assert/strict' + import { createRequire } from 'node:module' + import { fileURLToPath } from 'node:url' + const require = createRequire(import.meta.url) + const { ResolverFactory, sync } = require('unrs-resolver') + const resolver = ResolverFactory.default() + const normal = resolver.sync(process.cwd(), 'example') + assert.equal(normal.path, require.resolve('example')) + assert.deepEqual(await resolver.async(process.cwd(), 'example'), normal) + assert.deepEqual(resolver.resolveFileSync(fileURLToPath(import.meta.url), 'example'), normal) + assert.deepEqual(await resolver.resolveFileAsync(fileURLToPath(import.meta.url), 'example'), normal) + assert.deepEqual(sync(process.cwd(), 'example'), normal) + const custom = resolver.cloneWithOptions({ conditionNames: ['custom', 'node'] }) + assert.match(custom.sync(process.cwd(), 'example').path, /custom.js$/) + assert.match(custom.sync(process.cwd(), 'missing').error, /missing/) + assert.match(require.resolve('example'), /index.js$/) + assert.deepEqual(resolver.sync(process.cwd(), 'node:fs').builtin, { resolved: 'node:fs', isRuntimeModule: true }) + resolver.clearCache() + console.log('ok') + `, { env: { UNRS_RESOLVER_NODE_RESOLUTION: '1' } }) + assert.equal(result.stdout.trim(), 'ok') +}) diff --git a/pnpm/esm-loader/store.mjs b/pnpm/esm-loader/store.mjs new file mode 100644 index 0000000000..123e79ca42 --- /dev/null +++ b/pnpm/esm-loader/store.mjs @@ -0,0 +1,156 @@ +import { Buffer } from 'node:buffer' +import { createHash } from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +export function loaderError (code, message) { + return Object.assign(new Error(message), { code }) +} + +export function openStore (manifestURL) { + const manifestPath = fileURLToPath(manifestURL) + const base = path.dirname(manifestPath) + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) + if (!isRecord(manifest) || manifest.version !== 1 || typeof manifest.storeDir !== 'string') { + throw loaderError('ERR_PNPM_LOADER_MANIFEST', `Invalid store manifest: ${manifestPath}`) + } + const virtualRoot = path.join(base, '.pnpm-loader') + const storeDir = path.resolve(base, manifest.storeDir) + const packages = readPackages(manifest.packages, base, virtualRoot) + const files = new Map() + const directories = new Set([virtualRoot]) + for (const pkg of packages.values()) indexFiles(pkg, files, directories, storeDir) + for (const filename of files.keys()) { + if (directories.has(filename)) throw loaderError('ERR_PNPM_LOADER_MANIFEST', `File is also a directory: ${filename}`) + } + const owner = packageLookup(packages) + const filesystem = virtualFilesystem({ files, directories, virtualRoot }) + return { packages, owner, filesystem, files, virtualRoot } +} + +function packageLookup (packages) { + const roots = new Map([...packages.values()].map(pkg => [pkg.root, pkg])) + const cache = new Map() + return function owner (filename) { + if (cache.has(filename)) return cache.get(filename) + let directory = filename + while (!roots.has(directory) && path.dirname(directory) !== directory) { + directory = path.dirname(directory) + } + const pkg = roots.get(directory) + cache.set(filename, pkg) + return pkg + } +} + +function readPackages (entries, base, virtualRoot) { + if (!isRecord(entries)) { + throw loaderError('ERR_PNPM_LOADER_MANIFEST', 'The store manifest needs a packages object') + } + const packages = new Map() + const roots = new Map() + for (const [id, entry] of Object.entries(entries)) { + validatePackage(id, entry) + const stored = Object.hasOwn(entry, 'files') + if (stored === Object.hasOwn(entry, 'root')) { + throw loaderError('ERR_PNPM_LOADER_MANIFEST', `${id} must have either files or root`) + } + const root = stored + ? path.join(virtualRoot, createHash('sha256').update(id).digest('hex')) + : path.resolve(base, entry.root) + const previous = roots.get(root) + if ((!stored && within(virtualRoot, root)) || (previous && (previous.resolution !== 'node' || entry.resolution !== 'node'))) { + throw loaderError('ERR_PNPM_LOADER_MANIFEST', `Conflicting package root: ${root}`) + } + roots.set(root, entry) + packages.set(id, { ...entry, id, root, stored, dependencies: new Map(Object.entries(entry.dependencies ?? {})) }) + } + for (const pkg of packages.values()) { + for (const [alias, target] of pkg.dependencies) { + if (!packages.has(target)) { + throw loaderError('ERR_PNPM_LOADER_MANIFEST', `Unknown dependency ${alias} -> ${target} in ${pkg.id}`) + } + } + } + return packages +} + +function validatePackage (id, entry) { + if (!isRecord(entry) || !isRecord(entry.dependencies ?? {}) || + (Object.hasOwn(entry, 'files') && !isRecord(entry.files)) || + (Object.hasOwn(entry, 'root') && typeof entry.root !== 'string') || + (entry.resolution !== undefined && (entry.resolution !== 'node' || typeof entry.root !== 'string'))) { + throw loaderError('ERR_PNPM_LOADER_MANIFEST', `Invalid package entry: ${id}`) + } +} + +function isRecord (value) { + return value !== null && typeof value === 'object' && !Array.isArray(value) +} + +function indexFiles (pkg, files, directories, storeDir) { + if (!pkg.stored) return + directories.add(pkg.root) + if (!Object.hasOwn(pkg.files, 'package.json')) { + files.set(path.join(pkg.root, 'package.json'), { source: Buffer.from('{}') }) + } + for (const [name, hash] of Object.entries(pkg.files)) { + if (!validFilename(name) || typeof hash !== 'string' || !/^[a-f0-9]{128}(?:-exec)?$/.test(hash)) { + throw loaderError('ERR_PNPM_LOADER_MANIFEST', `Invalid store file ${pkg.id}/${name}`) + } + const filename = path.join(pkg.root, name) + files.set(filename, { blob: path.join(storeDir, 'files', hash.slice(0, 2), hash.slice(2)), hash: hash.slice(0, 128) }) + for (let parent = path.dirname(filename); within(pkg.root, parent); parent = path.dirname(parent)) { + directories.add(parent) + } + } +} + +function validFilename (name) { + return name.length > 0 && !name.includes('\\') && !name.includes('\0') && !name.includes(':') && + name.split('/').every(part => part !== '' && part !== '.' && part !== '..' && part !== 'node_modules') +} + +export function within (root, filename) { + return filename === root || filename.startsWith(`${root}${path.sep}`) +} + +function virtualFilesystem ({ files, directories, virtualRoot }) { + const jsonCache = new Map() + function statSync (filename) { + const normalized = path.resolve(filename) + if (!within(virtualRoot, normalized)) return fs.statSync(filename) + if (!files.has(normalized) && !directories.has(normalized)) throw missing(filename) + return { + isFile: () => files.has(normalized), + isDirectory: () => directories.has(normalized), + isSymbolicLink: () => false, + } + } + function readFileSync (filename, encoding) { + const normalized = path.resolve(filename) + if (!within(virtualRoot, normalized)) return fs.readFileSync(filename, encoding) + const entry = files.get(normalized) + if (!entry) throw missing(filename) + if (entry.source) return encoding ? entry.source.toString(encoding) : entry.source + const source = fs.readFileSync(entry.blob) + if (createHash('sha512').update(source).digest('hex') !== entry.hash) { + throw loaderError('ERR_PNPM_LOADER_INTEGRITY', `Store file failed integrity verification: ${entry.blob}`) + } + return encoding ? source.toString(encoding) : source + } + function readJsonSync (filename) { + const normalized = path.resolve(filename) + if (jsonCache.has(normalized)) return jsonCache.get(normalized) + const source = readFileSync(filename, 'utf8') + const parsed = JSON.parse(source.charCodeAt(0) === 0xFEFF ? source.slice(1) : source) + if (within(virtualRoot, normalized)) jsonCache.set(normalized, parsed) + return parsed + } + return { statSync, readFileSync, readJsonSync } +} + +function missing (filename) { + return loaderError('ENOENT', `No file in store manifest: ${filename}`) +} diff --git a/pnpm/esm-loader/test/fixture.mjs b/pnpm/esm-loader/test/fixture.mjs new file mode 100644 index 0000000000..828b89453c --- /dev/null +++ b/pnpm/esm-loader/test/fixture.mjs @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { URL } from 'node:url' + +const register = new URL('../register.mjs', import.meta.url).href + +export function fixture (context) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pnpm-store-loader-')) + context.after(() => fs.rmSync(root, { recursive: true, force: true })) + const manifest = { version: 1, storeDir: './store', packages: { '.': { root: '.', dependencies: {} } } } + function write (filename, content) { + const target = path.join(root, filename) + fs.mkdirSync(path.dirname(target), { recursive: true }) + fs.writeFileSync(target, content) + } + function add (id, contents, dependencies = {}) { + const files = {} + for (const [name, source] of Object.entries(contents)) { + const hash = createHash('sha512').update(source).digest('hex') + files[name] = hash + write(`store/files/${hash.slice(0, 2)}/${hash.slice(2)}`, source) + } + manifest.packages[id] = { files, dependencies } + return files + } + function run (source, options = {}) { + write('.pnpm-store.json', JSON.stringify(manifest)) + write('app.mjs', source) + const result = spawnSync(process.execPath, ['--import', register, ...(options.args ?? []), 'app.mjs'], { + cwd: root, encoding: 'utf8', env: { ...process.env, ...options.env, PNPM_LOADER_MANIFEST: path.join(root, '.pnpm-store.json') }, + }) + assert.equal(fs.existsSync(path.join(root, 'node_modules')), false) + assert.equal(fs.existsSync(path.join(root, '.pnpm-loader')), false) + if (!options.failure) assert.equal(result.status, 0, result.stderr) + else assert.notEqual(result.status, 0, result.stdout) + return result + } + return { root, manifest, write, add, run } +} diff --git a/pnpm/esm-loader/test/loader.test.mjs b/pnpm/esm-loader/test/loader.test.mjs new file mode 100644 index 0000000000..64de723f6f --- /dev/null +++ b/pnpm/esm-loader/test/loader.test.mjs @@ -0,0 +1,346 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { test } from 'node:test' +import { pathToFileURL } from 'node:url' + +import { createStoreHooks } from '../index.mjs' +import { openStore } from '../store.mjs' +import { fixture } from './fixture.mjs' + + +const esm = JSON.stringify({ name: 'example', type: 'module', exports: './index.js' }) + +test('runs ESM and CommonJS from store blobs without materializing packages', context => { + const setup = fixture(context) + setup.add('example@1', { + 'package.json': esm, + 'index.js': "import cjs from 'legacy'; import { suffix } from './suffix.js'; export default cjs + suffix", + 'suffix.js': "export const suffix = ' world'", + }, { legacy: 'legacy@1' }) + setup.add('legacy@1', { + 'package.json': '{"main":"index.cjs"}', + 'index.cjs': "module.exports = require('./value')", + 'value.js': "module.exports = 'hello'", + }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + assert.equal(setup.run("import value from 'example'; console.log(value)").stdout.trim(), 'hello world') +}) + +test('resolves CommonJS dot directories within the package and rejects parent escapes', context => { + const setup = fixture(context) + setup.add('example@1', { + 'package.json': '{"main":"index.cjs"}', + 'index.cjs': 'module.exports = 42', + 'bin/entry.cjs': "module.exports = [require('..'), require('.')];", + 'bin/index.js': 'module.exports = 7', + 'escape.cjs': "require('..')", + }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + assert.equal(setup.run("import value from 'example/bin/entry.cjs'; console.log(value.join(','))").stdout.trim(), '42,7') + assert.match(setup.run("import 'example/escape.cjs'", { failure: true }).stderr, /ERR_PNPM_LOADER_PATH_ESCAPE/) +}) + +test('keeps distinct peer contexts even when every source blob is identical', context => { + const setup = fixture(context) + const sources = { 'package.json': esm, 'index.js': "export { default } from 'peer'" } + setup.add('example@1(peer@1)', sources, { peer: 'peer@1' }) + setup.add('example@1(peer@2)', sources, { peer: 'peer@2' }) + for (const version of [1, 2]) setup.add(`peer@${version}`, { 'index.js': `module.exports = ${version}` }) + setup.manifest.packages['.'].dependencies = { first: 'example@1(peer@1)', second: 'example@1(peer@2)' } + assert.equal(setup.run("import first from 'first'; import second from 'second'; console.log(first, second)").stdout.trim(), '1 2') +}) + +test('resolves conditional exports, private imports, self references, and patterns', context => { + const setup = fixture(context) + setup.add('example@1', { + 'package.json': JSON.stringify({ + name: 'example', type: 'module', + exports: { '.': { custom: './custom.js', import: './index.js', require: './require.cjs' }, './features/*': './src/*.js', './blocked': null }, + imports: { '#value': './value.js', '#dep': 'external' }, + }), + 'index.js': "import value from '#value'; import { feature } from 'example/features/test'; import dep from '#dep'; export default value + feature + dep", + 'custom.js': 'export default 99', + 'require.cjs': 'module.exports = 12', + 'src/test.js': 'export const feature = 2', + 'value.js': 'export default 1', + }, { external: 'external@1' }) + setup.add('external@1', { 'index.js': 'module.exports = 3' }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + const source = "import value from 'example'; import { createRequire } from 'node:module'; console.log(value, createRequire(import.meta.url)('example'))" + assert.equal(setup.run(source).stdout.trim(), '6 12') + assert.equal(setup.run(source, { args: ['--conditions=custom'] }).stdout.trim(), '99 [Module: null prototype] { __esModule: true, default: 99 }') + assert.match(setup.run("import 'example/blocked'", { failure: true }).stderr, /not exported/) +}) + +test('loads JSON, nested package types, builtins and dynamic imports', context => { + const setup = fixture(context) + setup.add('example@1', { + 'package.json': esm, + 'index.js': "import data from './data.json' with { type: 'json' }; import cjs from './nested/value.js'; import { basename } from 'node:path'; export default [data.value, cjs, basename('/a/b'), (await import('./dynamic.mjs')).default]", + 'data.json': '{"value":1}', + 'nested/package.json': '{"type":"commonjs"}', + 'nested/value.js': 'module.exports = require("../data.json").value + 1', + 'dynamic.mjs': 'export default 3', + }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + assert.equal(setup.run("import value from 'example'; console.log(JSON.stringify(value))").stdout.trim(), '[1,2,"b",3]') +}) + +test('enforces declared dependencies, exports, and ESM file extensions', context => { + const setup = fixture(context) + setup.manifest.packages['.'].dependencies.example = 'example@1' + const files = { 'package.json': esm, 'index.js': "import 'undeclared'", 'private.js': 'export default 1' } + setup.add('example@1', files) + assert.match(setup.run("import 'example'", { failure: true }).stderr, /ERR_PNPM_LOADER_UNDECLARED_DEPENDENCY/) + assert.match(setup.run("import 'example/private.js'", { failure: true }).stderr, /not exported/) + setup.add('example@1', { ...files, 'index.js': "import './private'" }) + assert.match(setup.run("import 'example'", { failure: true }).stderr, /resolve/) +}) + +test('verifies store integrity before executing a module', context => { + const setup = fixture(context) + const files = setup.add('example@1', { 'package.json': esm, 'index.js': 'export default 1' }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + const hash = files['index.js'] + setup.write(`store/files/${hash.slice(0, 2)}/${hash.slice(2)}`, "throw new Error('tampered source ran')") + const result = setup.run("import 'example'", { failure: true }) + assert.match(result.stderr, /ERR_PNPM_LOADER_INTEGRITY/) + assert.doesNotMatch(result.stderr, /tampered source ran/) +}) + +test('rejects traversal in manifests and module requests', context => { + const setup = fixture(context) + const files = setup.add('example@1', { 'package.json': esm, 'index.js': "import '../../app.mjs'" }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + assert.match(setup.run("import 'example'", { failure: true }).stderr, /ERR_PNPM_LOADER_PATH_ESCAPE/) + setup.manifest.packages['example@1'].files['../escape.js'] = files['index.js'] + assert.match(setup.run("import 'example'", { failure: true }).stderr, /ERR_PNPM_LOADER_MANIFEST/) +}) + +test('does not intercept files outside registered project roots', context => { + const setup = fixture(context) + setup.manifest.packages['.'].root = './project' + assert.equal(setup.run("import path from 'node:path'; console.log(path.basename('/a/b'))").stdout.trim(), 'b') +}) + +test('preserves URL query identity and encoded filenames', context => { + const setup = fixture(context) + setup.add('example@1', { + 'package.json': esm, + 'index.js': "import first from './value%20%23.js?first'; import same from './value%20%23.js?first'; import other from './value%20%23.js?other'; export default [first === same, first !== other]", + 'value #.js': 'export default {}', + }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + assert.equal(setup.run("import value from 'example'; console.log(JSON.stringify(value))").stdout.trim(), '[true,true]') +}) + +test('enforces JSON import attributes while allowing require and resolution of JSON', context => { + const setup = fixture(context) + setup.add('example@1', { + 'package.json': JSON.stringify({ type: 'module', exports: { '.': './data.json', './code': './index.js' } }), + 'data.json': '{"value":42}', 'index.js': 'export default 1', + }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + assert.match(setup.run("console.log(import.meta.resolve('example'))").stdout, /data.json/) + assert.match(setup.run("import value from 'example'", { failure: true }).stderr, /ERR_IMPORT_ATTRIBUTE_MISSING/) + assert.match(setup.run("import value from 'example/code' with { type: 'json' }", { failure: true }).stderr, /ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE/) + assert.equal(setup.run("import { createRequire } from 'node:module'; console.log(createRequire(import.meta.url)('example').value)").stdout.trim(), '42') +}) + +test('supports CommonJS entry points, cycles, and createRequire inside store modules', context => { + const setup = fixture(context) + setup.add('example@1', { + 'package.json': '{"type":"module","exports":"./index.js"}', + 'index.js': "import { createRequire } from 'node:module'; export default createRequire(import.meta.url)('./first.cjs')", + 'first.cjs': "exports.value = 1; exports.next = require('./second.cjs').value", + 'second.cjs': "exports.value = require('./first.cjs').value + 1", + }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + setup.write('entry.cjs', "import('example').then(({ default: value }) => console.log(JSON.stringify(value)))") + assert.equal(setup.run("import './entry.cjs'").stdout.trim(), '{"value":1,"next":2}') +}) + +test('inherits registration in workers and routes workspace dependency contexts', context => { + const setup = fixture(context) + setup.manifest.packages.workspace = { root: './workspace', dependencies: { example: 'example@1' } } + setup.add('example@1', { 'index.js': 'module.exports = 42' }) + setup.write('workspace/worker.mjs', "import value from 'example'; import { parentPort } from 'node:worker_threads'; parentPort.postMessage(value)") + assert.equal(setup.run("import { Worker } from 'node:worker_threads'; const worker = new Worker(new URL('./workspace/worker.mjs', import.meta.url)); worker.on('message', value => console.log(value))").stdout.trim(), '42') + assert.match(setup.run("import 'example'", { failure: true }).stderr, /ERR_PNPM_LOADER_UNDECLARED_DEPENDENCY/) +}) + +test('rejects unsupported native addons explicitly', context => { + const setup = fixture(context) + setup.add('addon@1', { 'package.json': '{"main":"binding.node"}', 'binding.node': 'not a native binary' }) + setup.manifest.packages['.'].dependencies.addon = 'addon@1' + assert.match(setup.run("import 'addon'", { failure: true }).stderr, /ERR_PNPM_LOADER_UNSUPPORTED_FORMAT/) + assert.match(setup.run("import { createRequire } from 'node:module'; createRequire(import.meta.url)('addon')", { failure: true }).stderr, /ERR_PNPM_LOADER_UNSUPPORTED_FORMAT/) +}) + +test('handles executable store blobs, missing files, and unknown dependency targets', context => { + const setup = fixture(context) + const files = setup.add('example@1', { 'package.json': esm, 'index.js': '#!/usr/bin/env node\nexport default 42' }) + const hash = files['index.js'] + const blob = `store/files/${hash.slice(0, 2)}/${hash.slice(2)}` + fs.renameSync(path.join(setup.root, blob), path.join(setup.root, `${blob}-exec`)) + files['index.js'] += '-exec' + setup.manifest.packages['.'].dependencies.example = 'example@1' + assert.equal(setup.run("import value from 'example'; console.log(value)").stdout.trim(), '42') + fs.unlinkSync(path.join(setup.root, `${blob}-exec`)) + assert.match(setup.run("import 'example'", { failure: true }).stderr, /ENOENT/) + setup.manifest.packages['.'].dependencies.missing = 'missing@1' + assert.match(setup.run("import 'example'", { failure: true }).stderr, /ERR_PNPM_LOADER_MANIFEST/) +}) + +test('preserves literal CommonJS filenames and require.resolve results', context => { + const setup = fixture(context) + setup.add('example@1', { + 'index.js': "module.exports = require(require.resolve('./value #.cjs'))", + 'value #.cjs': 'module.exports = 42', + }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + assert.equal(setup.run("import value from 'example'; console.log(value)").stdout.trim(), '42') +}) + +test('rejects malformed manifests before executing application code', context => { + const setup = fixture(context) + for (const invalid of [null, [], 'invalid', { root: 1 }, { files: null }, { root: '.', files: {} }, { root: '.', dependencies: [] }, { root: '.', resolution: 'node' }]) { + setup.manifest.packages.invalid = invalid + assert.match(setup.run("throw new Error('application ran')", { failure: true }).stderr, /ERR_PNPM_LOADER_MANIFEST/) + } +}) + +test('does not resolve inherited properties as dependency declarations', context => { + const setup = fixture(context) + setup.add('example@1', { 'index.js': "require('toString')" }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + assert.match(setup.run("import 'example'", { failure: true }).stderr, /ERR_PNPM_LOADER_UNDECLARED_DEPENDENCY/) +}) + +test('rejects package subpaths that escape into another dependency context', context => { + const setup = fixture(context) + setup.add('example@1', { 'index.js': 'module.exports = 1' }) + setup.add('hidden@1', { 'index.js': 'module.exports = 2' }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + const hiddenRoot = createHash('sha256').update('hidden@1').digest('hex') + assert.match(setup.run(`import 'example/../${hiddenRoot}/index.js'`, { failure: true }).stderr, /ERR_PNPM_LOADER_PATH_ESCAPE/) +}) + +test('loads resolved absolute paths from a stored tool', context => { + const setup = fixture(context) + setup.add('tool@1', { 'index.js': 'exports.load = filename => require(filename)' }) + setup.add('value@1', { 'index.js': 'module.exports = 42' }) + setup.manifest.packages['.'].dependencies = { tool: 'tool@1', value: 'value@1' } + setup.write('config.cjs', 'module.exports = 7') + assert.equal(setup.run("import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const tool = require('tool'); console.log(tool.load(require.resolve('value')), tool.load(require.resolve('./config.cjs')))").stdout.trim(), '42 7') +}) + +test('reads package types from manifests with a UTF-8 BOM', context => { + const setup = fixture(context) + setup.add('example@1', { 'package.json': '\uFEFF' + esm, 'index.js': 'export default 42' }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + assert.equal(setup.run("import value from 'example'; console.log(value)").stdout.trim(), '42') +}) + +test('runs opted-out packages and their linked dependencies with native resolution', context => { + const setup = fixture(context) + setup.manifest.packages['.'].root = './project' + setup.manifest.packages['.'].dependencies = { tool: 'tool@1', stored: 'stored@1' } + setup.add('stored@1', { 'index.js': 'module.exports = "CAS"' }) + for (const version of [1, 2]) { + const tool = `gvs/tool-${version}/node_modules/tool` + const dependency = `gvs/value-${version}/node_modules/value` + setup.write(`${tool}/package.json`, '{"name":"tool","main":"index.cjs"}') + setup.write(`${tool}/index.cjs`, ` + const fs = require('node:fs') + const path = require('node:path') + const { execFileSync } = require('node:child_process') + exports.value = require('value') + exports.asset = fs.readFileSync(path.join(path.dirname(require.resolve('value')), 'asset.txt'), 'utf8') + exports.child = execFileSync(process.execPath, ['-e', 'console.log(require("value"))'], { + cwd: __dirname, env: { ...process.env, NODE_OPTIONS: '' }, encoding: 'utf8', + }).trim() + `) + setup.write(`${dependency}/package.json`, '{"name":"value","main":"index.cjs"}') + setup.write(`${dependency}/index.cjs`, `module.exports = ${version}`) + setup.write(`${dependency}/asset.txt`, `asset ${version}`) + fs.symlinkSync(path.join(setup.root, dependency), path.join(setup.root, `gvs/tool-${version}/node_modules/value`), 'junction') + setup.manifest.packages[`tool@${version}`] = { root: tool, resolution: 'node' } + setup.manifest.packages[`value@${version}`] = { root: dependency, resolution: 'node' } + } + setup.manifest.packages['.'].dependencies.other = 'tool@2' + setup.manifest.packages.alias = { ...setup.manifest.packages['tool@1'] } + setup.manifest.packages['.'].dependencies.alias = 'alias' + setup.write('project/main.mjs', ` + import assert from 'node:assert/strict' + import { createRequire } from 'node:module' + import { fileURLToPath } from 'node:url' + import first from 'tool' + import second from 'other' + import alias from 'alias' + import stored from 'stored' + assert.equal(first, alias) + assert.deepEqual(first, { value: 1, asset: 'asset 1', child: '1' }) + assert.deepEqual(second, { value: 2, asset: 'asset 2', child: '2' }) + assert.equal(stored, 'CAS') + assert.equal(createRequire(import.meta.resolve('tool'))(fileURLToPath(import.meta.resolve('stored'))), 'CAS') + assert.equal(await import.meta.resolve('tool'), new URL('../gvs/tool-1/node_modules/tool/index.cjs', import.meta.url).href) + `) + setup.run("await import('./project/main.mjs')") + assert.equal(fs.existsSync(path.join(setup.root, 'project/node_modules')), false) +}) + +test('rejects native resolution for stored packages and unknown resolution modes', context => { + const setup = fixture(context) + setup.add('example@1', { 'index.js': 'module.exports = 1' }) + setup.manifest.packages['example@1'].resolution = 'node' + assert.match(setup.run('', { failure: true }).stderr, /ERR_PNPM_LOADER_MANIFEST/) + setup.manifest.packages['example@1'] = { root: './physical', resolution: 'unknown' } + assert.match(setup.run('', { failure: true }).stderr, /ERR_PNPM_LOADER_MANIFEST/) +}) + +test('runs a stored extensionless CommonJS bin as the main module', context => { + const setup = fixture(context) + setup.add('bin@1', { + 'package.json': '{"name":"bin","main":"cli"}', + cli: "#!/usr/bin/env node\nif (require.main !== module) throw new Error('not main'); console.log('main bin')", + }) + setup.manifest.packages['.'].dependencies.bin = 'bin@1' + const output = setup.run("import { createRequire, runMain } from 'node:module'; runMain(createRequire(import.meta.url).resolve('bin'))") + assert.equal(output.stdout.trim(), 'main bin') +}) + +test('unknown virtual package paths produce a loader ENOENT error', context => { + const setup = fixture(context) + delete setup.manifest.packages['.'] + setup.write('.pnpm-store.json', JSON.stringify(setup.manifest)) + const hooks = createStoreHooks(pathToFileURL(path.join(setup.root, '.pnpm-store.json'))) + assert.throws(() => hooks.load(pathToFileURL(path.join(setup.root, '.pnpm-loader/unknown/index.js')).href, { conditions: [] }, () => assert.fail()), { code: 'ENOENT' }) +}) + +test('caches verified virtual JSON without caching mutable workspace manifests', context => { + const setup = fixture(context) + const files = setup.add('example@1', { 'package.json': esm }) + setup.write('.pnpm-store.json', JSON.stringify(setup.manifest)) + setup.write('package.json', '{"name":"before"}') + const store = openStore(pathToFileURL(path.join(setup.root, '.pnpm-store.json'))) + const virtual = path.join(store.packages.get('example@1').root, 'package.json') + assert.equal(store.filesystem.readJsonSync(virtual).name, 'example') + const hash = files['package.json'] + fs.rmSync(path.join(setup.root, 'store/files', hash.slice(0, 2), hash.slice(2))) + assert.equal(store.filesystem.readJsonSync(virtual).name, 'example') + const workspaceManifest = path.join(setup.root, 'package.json') + assert.equal(store.filesystem.readJsonSync(workspaceManifest).name, 'before') + setup.write('package.json', '{"name":"after"}') + assert.equal(store.filesystem.readJsonSync(workspaceManifest).name, 'after') +}) + +test('rejects package main paths escaping into physical workspace files', context => { + const setup = fixture(context) + setup.write('outside.cjs', 'module.exports = 42') + setup.add('example@1', { 'package.json': '{"main":"../../outside.cjs"}' }) + setup.manifest.packages['.'].dependencies.example = 'example@1' + assert.match(setup.run("import value from 'example'; console.log(value)", { failure: true }).stderr, /ERR_PNPM_LOADER_PATH_ESCAPE/) +}) diff --git a/pnpm11/__utils__/jest-config/config.js b/pnpm11/__utils__/jest-config/config.js index de9eda94d3..0727bab6b1 100644 --- a/pnpm11/__utils__/jest-config/config.js +++ b/pnpm11/__utils__/jest-config/config.js @@ -1,7 +1,9 @@ -import path from 'path' +import { createRequire } from 'node:module' +import path from 'node:path' +import process from 'node:process' const config = { - resolver: path.join(import.meta.dirname, 'node_modules/ts-jest-resolver'), + resolver: createRequire(import.meta.url).resolve('ts-jest-resolver'), extensionsToTreatAsEsm: ['.ts'], transform: { '^.+\\.tsx?$': path.join(import.meta.dirname, 'jest.transform.js'), @@ -11,7 +13,7 @@ const config = { collectCoverage: true, coveragePathIgnorePatterns: ["/node_modules/"], testPathIgnorePatterns: ["/fixtures/", "/__fixtures__/", "/test/(.+/)?utils/"], - modulePathIgnorePatterns: ['\/__fixtures__\/.*'], + modulePathIgnorePatterns: ['/__fixtures__/.*'], testTimeout: 4 * 60 * 1000, // 4 minutes setupFilesAfterEnv: [path.join(import.meta.dirname, "setupFilesAfterEnv.js")], maxWorkers: "50%", @@ -24,7 +26,7 @@ if (process.env.PNPM_SCRIPT_SRC_DIR) { // We are running test script from pnpm command, this seems to confuse tests // Clean up env from pnpm variables so that nested pnpm runs won't get affected on config read for (const key of Object.keys(process.env)) { - if (/^p?npm_(config|package|lifecycle|node|command|execpath)(_|$)/ui.test(key)) { + if (/^p?npm_(?:config|package|lifecycle|node|command|execpath)(?:_|$)/iu.test(key)) { delete process.env[key] } } diff --git a/pnpm11/deps/status/test/checkDepsStatus.test.ts b/pnpm11/deps/status/test/checkDepsStatus.test.ts index 4a71a08910..e72c3c8d11 100644 --- a/pnpm11/deps/status/test/checkDepsStatus.test.ts +++ b/pnpm11/deps/status/test/checkDepsStatus.test.ts @@ -1184,7 +1184,7 @@ describe('checkDepsStatus - missing wanted lockfile fallback', () => { // lockfiles ends the check without reading it. A read that fails must not // surface as an unhandled rejection, which crashes the pnpm process. it('does not leave the failed read of an unused wanted lockfile unhandled', async () => { - const lastValidatedTimestamp = Date.now() - 10_000 + const lastValidatedTimestamp = 1_700_000_000_123 const mockWorkspaceState: WorkspaceState = { lastValidatedTimestamp, pnpmfiles: [],