mirror of
https://github.com/denoland/deno.git
synced 2025-10-01 22:51:14 +00:00

This adds support for using a local copy of an npm package. ```js // deno.json { "patch": [ "../path/to/local_npm_package" ], // required until Deno 2.3, but it will still be considered unstable "unstable": ["npm-patch"] } ``` 1. Requires using a node_modules folder. 2. When using `"nodeModulesDir": "auto"`, it recreates the folder in the node_modules directory on each run which will slightly increase startup time. 3. When using the default with a package.json (`"nodeModulesDir": "manual"`), updating the package requires running `deno install`. This is to get the package into the node_modules directory of the current workspace. This is necessary instead of linking because packages can have multiple "copy packages" due to peer dep resolution. Caveat: Specifying a local copy of an npm package or making changes to its dependencies will purge npm packages from the lockfile. This might cause npm resolution to resolve differently and it may end up not using the local copy of the npm package. It's very difficult to only invalidate resolution midway through the graph and then only rebuild that part of the resolution, so this is just a first pass that can be improved in the future. In practice, this probably won't be an issue for most people. Another limitation is this also requires the npm package name to exist in the registry at the moment.
215 lines
6.2 KiB
Rust
215 lines
6.2 KiB
Rust
// Copyright 2018-2025 the Deno authors. MIT license.
|
|
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use deno_core::parking_lot::Mutex;
|
|
use deno_error::JsError;
|
|
use deno_error::JsErrorBox;
|
|
use deno_npm::registry::NpmRegistryApi;
|
|
use deno_npm::resolution::NpmResolutionSnapshot;
|
|
use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot;
|
|
use deno_resolver::npm::managed::ManagedNpmResolverCreateOptions;
|
|
use deno_resolver::npm::managed::NpmResolutionCell;
|
|
use thiserror::Error;
|
|
|
|
use super::CliNpmRegistryInfoProvider;
|
|
use super::WorkspaceNpmPatchPackages;
|
|
use crate::args::CliLockfile;
|
|
use crate::sys::CliSys;
|
|
|
|
pub type CliManagedNpmResolverCreateOptions =
|
|
ManagedNpmResolverCreateOptions<CliSys>;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum CliNpmResolverManagedSnapshotOption {
|
|
ResolveFromLockfile(Arc<CliLockfile>),
|
|
Specified(Option<ValidSerializedNpmResolutionSnapshot>),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum SyncState {
|
|
Pending(Option<CliNpmResolverManagedSnapshotOption>),
|
|
Err(ResolveSnapshotError),
|
|
Success,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct NpmResolutionInitializer {
|
|
npm_registry_info_provider: Arc<CliNpmRegistryInfoProvider>,
|
|
npm_resolution: Arc<NpmResolutionCell>,
|
|
patch_packages: Arc<WorkspaceNpmPatchPackages>,
|
|
queue: tokio::sync::Mutex<()>,
|
|
sync_state: Mutex<SyncState>,
|
|
}
|
|
|
|
impl NpmResolutionInitializer {
|
|
pub fn new(
|
|
npm_registry_info_provider: Arc<CliNpmRegistryInfoProvider>,
|
|
npm_resolution: Arc<NpmResolutionCell>,
|
|
patch_packages: Arc<WorkspaceNpmPatchPackages>,
|
|
snapshot_option: CliNpmResolverManagedSnapshotOption,
|
|
) -> Self {
|
|
Self {
|
|
npm_registry_info_provider,
|
|
npm_resolution,
|
|
patch_packages,
|
|
queue: tokio::sync::Mutex::new(()),
|
|
sync_state: Mutex::new(SyncState::Pending(Some(snapshot_option))),
|
|
}
|
|
}
|
|
|
|
#[cfg(debug_assertions)]
|
|
pub fn debug_assert_initialized(&self) {
|
|
if !matches!(*self.sync_state.lock(), SyncState::Success) {
|
|
panic!("debug assert: npm resolution must be initialized before calling this code");
|
|
}
|
|
}
|
|
|
|
pub async fn ensure_initialized(&self) -> Result<(), JsErrorBox> {
|
|
// fast exit if not pending
|
|
{
|
|
match &*self.sync_state.lock() {
|
|
SyncState::Pending(_) => {}
|
|
SyncState::Err(err) => return Err(JsErrorBox::from_err(err.clone())),
|
|
SyncState::Success => return Ok(()),
|
|
}
|
|
}
|
|
|
|
// only allow one task in here at a time
|
|
let _guard = self.queue.lock().await;
|
|
|
|
let snapshot_option = {
|
|
let mut sync_state = self.sync_state.lock();
|
|
match &mut *sync_state {
|
|
SyncState::Pending(snapshot_option) => {
|
|
// this should never panic, but if it does it means that a
|
|
// previous future was dropped while initialization occurred...
|
|
// that should never happen because this is initialized during
|
|
// startup
|
|
snapshot_option.take().unwrap()
|
|
}
|
|
// another thread updated the state while we were waiting
|
|
SyncState::Err(resolve_snapshot_error) => {
|
|
return Err(JsErrorBox::from_err(resolve_snapshot_error.clone()));
|
|
}
|
|
SyncState::Success => {
|
|
return Ok(());
|
|
}
|
|
}
|
|
};
|
|
|
|
match resolve_snapshot(
|
|
&self.npm_registry_info_provider,
|
|
snapshot_option,
|
|
&self.patch_packages,
|
|
)
|
|
.await
|
|
{
|
|
Ok(maybe_snapshot) => {
|
|
if let Some(snapshot) = maybe_snapshot {
|
|
self
|
|
.npm_resolution
|
|
.set_snapshot(NpmResolutionSnapshot::new(snapshot));
|
|
}
|
|
let mut sync_state = self.sync_state.lock();
|
|
*sync_state = SyncState::Success;
|
|
Ok(())
|
|
}
|
|
Err(err) => {
|
|
let mut sync_state = self.sync_state.lock();
|
|
*sync_state = SyncState::Err(err.clone());
|
|
Err(JsErrorBox::from_err(err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error, Clone, JsError)]
|
|
#[error("failed reading lockfile '{}'", lockfile_path.display())]
|
|
#[class(inherit)]
|
|
pub struct ResolveSnapshotError {
|
|
lockfile_path: PathBuf,
|
|
#[inherit]
|
|
#[source]
|
|
source: SnapshotFromLockfileError,
|
|
}
|
|
|
|
impl ResolveSnapshotError {
|
|
pub fn maybe_integrity_check_error(
|
|
&self,
|
|
) -> Option<&deno_npm::resolution::IntegrityCheckFailedError> {
|
|
match &self.source {
|
|
SnapshotFromLockfileError::SnapshotFromLockfile(
|
|
deno_npm::resolution::SnapshotFromLockfileError::IntegrityCheckFailed(
|
|
err,
|
|
),
|
|
) => Some(err),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn resolve_snapshot(
|
|
registry_info_provider: &Arc<CliNpmRegistryInfoProvider>,
|
|
snapshot: CliNpmResolverManagedSnapshotOption,
|
|
patch_packages: &WorkspaceNpmPatchPackages,
|
|
) -> Result<Option<ValidSerializedNpmResolutionSnapshot>, ResolveSnapshotError>
|
|
{
|
|
match snapshot {
|
|
CliNpmResolverManagedSnapshotOption::ResolveFromLockfile(lockfile) => {
|
|
if !lockfile.overwrite() {
|
|
let snapshot = snapshot_from_lockfile(
|
|
lockfile.clone(),
|
|
®istry_info_provider.as_npm_registry_api(),
|
|
patch_packages,
|
|
)
|
|
.await
|
|
.map_err(|source| ResolveSnapshotError {
|
|
lockfile_path: lockfile.filename.clone(),
|
|
source,
|
|
})?;
|
|
Ok(Some(snapshot))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
CliNpmResolverManagedSnapshotOption::Specified(snapshot) => Ok(snapshot),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error, Clone, JsError)]
|
|
pub enum SnapshotFromLockfileError {
|
|
#[error(transparent)]
|
|
#[class(inherit)]
|
|
IncompleteError(
|
|
#[from] deno_npm::resolution::IncompleteSnapshotFromLockfileError,
|
|
),
|
|
#[error(transparent)]
|
|
#[class(inherit)]
|
|
SnapshotFromLockfile(#[from] deno_npm::resolution::SnapshotFromLockfileError),
|
|
}
|
|
|
|
async fn snapshot_from_lockfile(
|
|
lockfile: Arc<CliLockfile>,
|
|
api: &dyn NpmRegistryApi,
|
|
patch_packages: &WorkspaceNpmPatchPackages,
|
|
) -> Result<ValidSerializedNpmResolutionSnapshot, SnapshotFromLockfileError> {
|
|
let (incomplete_snapshot, skip_integrity_check) = {
|
|
let lock = lockfile.lock();
|
|
(
|
|
deno_npm::resolution::incomplete_snapshot_from_lockfile(&lock)?,
|
|
lock.overwrite,
|
|
)
|
|
};
|
|
let snapshot = deno_npm::resolution::snapshot_from_lockfile(
|
|
deno_npm::resolution::SnapshotFromLockfileParams {
|
|
incomplete_snapshot,
|
|
api,
|
|
patch_packages: &patch_packages.0,
|
|
skip_integrity_check,
|
|
},
|
|
)
|
|
.await?;
|
|
Ok(snapshot)
|
|
}
|