summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Whitaker <17734409+nathanwhit@users.noreply.github.com>2024-07-02 15:00:16 -0700
committerGitHub <noreply@github.com>2024-07-02 15:00:16 -0700
commitc13b6d1413859d03b41b97d4c671fccfd388b2cc (patch)
tree503c5d2c51c71f3daa79950b6862b725e9211822
parentd379c0b299411a847765e2879f8ed14bdb2d0298 (diff)
feat(cli): Add `--frozen` flag to error out if lockfile is out of date (#24355)
Closes #18296. Adds a `--frozen` (alias `--frozen-lockfile`) flag that errors out if the lockfile is out of date. This is useful for running in CI (where an out of date lockfile is usually a mistake) or to prevent accidental changes in dependencies. ![Screenshot 2024-06-26 at 7 11 13 PM](https://github.com/denoland/deno/assets/17734409/538404b8-b422-4f05-89e8-4c9b1c248576)
-rw-r--r--cli/args/flags.rs65
-rw-r--r--cli/args/lockfile.rs55
-rw-r--r--cli/lsp/config.rs2
-rw-r--r--cli/module_loader.rs11
-rw-r--r--cli/npm/managed/mod.rs28
-rw-r--r--cli/resolver.rs5
-rw-r--r--cli/tools/installer.rs4
-rw-r--r--tests/specs/lockfile/frozen_lockfile/__test__.jsonc157
-rw-r--r--tests/specs/lockfile/frozen_lockfile/add.ts2
-rw-r--r--tests/specs/lockfile/frozen_lockfile/deno.json3
-rw-r--r--tests/specs/lockfile/frozen_lockfile/frozen_new_dep_cache.out11
-rw-r--r--tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_http.out10
-rw-r--r--tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_jsr.out16
-rw-r--r--tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_npm.out15
-rw-r--r--tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_cache.out12
-rw-r--r--tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_run.out14
-rw-r--r--tests/specs/lockfile/frozen_lockfile/frozen_new_dep_run.out12
-rw-r--r--tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed.out25
-rw-r--r--tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed_install.out25
-rw-r--r--tests/specs/lockfile/frozen_lockfile/http-dynamic.ts2
-rw-r--r--tests/specs/lockfile/frozen_lockfile/jsr-dynamic.ts2
-rw-r--r--tests/specs/lockfile/frozen_lockfile/jsr.ts1
-rw-r--r--tests/specs/lockfile/frozen_lockfile/jsr2.ts2
-rw-r--r--tests/specs/lockfile/frozen_lockfile/no_lockfile_run.out20
-rw-r--r--tests/specs/lockfile/frozen_lockfile/package.json1
-rw-r--r--tests/specs/lockfile/frozen_lockfile/sub-dynamic.ts2
-rw-r--r--tests/specs/lockfile/frozen_lockfile/sub.ts2
-rw-r--r--tests/specs/lockfile/frozen_lockfile/update_lockfile.out2
-rw-r--r--tests/specs/npm/npmrc_not_next_to_package_json/main.out1
29 files changed, 477 insertions, 30 deletions
diff --git a/cli/args/flags.rs b/cli/args/flags.rs
index 1743d58c6..5f58911c2 100644
--- a/cli/args/flags.rs
+++ b/cli/args/flags.rs
@@ -498,6 +498,7 @@ pub struct Flags {
pub argv: Vec<String>,
pub subcommand: DenoSubcommand,
+ pub frozen_lockfile: bool,
pub ca_stores: Option<Vec<String>>,
pub ca_data: Option<CaData>,
pub cache_blocklist: Vec<String>,
@@ -1487,12 +1488,15 @@ Future runs of this module will trigger no downloads or compilation unless
--reload is specified.",
)
.defer(|cmd| {
- compile_args(cmd).arg(check_arg(false)).arg(
- Arg::new("file")
- .num_args(1..)
- .required(true)
- .value_hint(ValueHint::FilePath),
- )
+ compile_args(cmd)
+ .arg(check_arg(false))
+ .arg(
+ Arg::new("file")
+ .num_args(1..)
+ .required(true)
+ .value_hint(ValueHint::FilePath),
+ )
+ .arg(frozen_lockfile_arg())
})
}
@@ -3271,6 +3275,7 @@ fn runtime_args(
app
};
app
+ .arg(frozen_lockfile_arg())
.arg(cached_only_arg())
.arg(location_arg())
.arg(v8_flags_arg())
@@ -3384,6 +3389,17 @@ fn cached_only_arg() -> Arg {
.help("Require that remote dependencies are already cached")
}
+fn frozen_lockfile_arg() -> Arg {
+ Arg::new("frozen")
+ .long("frozen")
+ .alias("frozen-lockfile")
+ .value_parser(value_parser!(bool))
+ .num_args(0..=1)
+ .require_equals(true)
+ .default_missing_value("true")
+ .help("Error out if lockfile is out of date")
+}
+
/// Used for subcommands that operate on executable scripts only.
/// `deno fmt` has its own `--ext` arg because its possible values differ.
/// If --ext is not provided and the script doesn't have a file extension,
@@ -3774,6 +3790,7 @@ fn bundle_parse(flags: &mut Flags, matches: &mut ArgMatches) {
fn cache_parse(flags: &mut Flags, matches: &mut ArgMatches) {
compile_args_parse(flags, matches);
+ frozen_lockfile_arg_parse(flags, matches);
let files = matches.remove_many::<String>("file").unwrap().collect();
flags.subcommand = DenoSubcommand::Cache(CacheFlags { files });
}
@@ -4576,6 +4593,7 @@ fn runtime_args_parse(
) {
compile_args_parse(flags, matches);
cached_only_arg_parse(flags, matches);
+ frozen_lockfile_arg_parse(flags, matches);
if include_perms {
permission_args_parse(flags, matches);
}
@@ -4667,6 +4685,12 @@ fn cached_only_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) {
}
}
+fn frozen_lockfile_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) {
+ if let Some(&v) = matches.get_one::<bool>("frozen") {
+ flags.frozen_lockfile = v;
+ }
+}
+
fn ext_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) {
flags.ext = matches.remove_one::<String>("ext");
}
@@ -9845,4 +9869,33 @@ mod tests {
}
);
}
+
+ #[test]
+ fn run_with_frozen_lockfile() {
+ let cases = [
+ (Some("--frozen"), true),
+ (Some("--frozen=true"), true),
+ (Some("--frozen=false"), false),
+ (None, false),
+ ];
+ for (flag, frozen) in cases {
+ let mut args = svec!["deno", "run"];
+ if let Some(f) = flag {
+ args.push(f.into());
+ }
+ args.push("script.ts".into());
+ let r = flags_from_vec(args);
+ assert_eq!(
+ r.unwrap(),
+ Flags {
+ subcommand: DenoSubcommand::Run(RunFlags::new_default(
+ "script.ts".to_string(),
+ )),
+ frozen_lockfile: frozen,
+ code_cache_enabled: true,
+ ..Flags::default()
+ }
+ );
+ }
+ }
}
diff --git a/cli/args/lockfile.rs b/cli/args/lockfile.rs
index 555261336..d18c871aa 100644
--- a/cli/args/lockfile.rs
+++ b/cli/args/lockfile.rs
@@ -23,6 +23,7 @@ use deno_lockfile::Lockfile;
pub struct CliLockfile {
lockfile: Mutex<Lockfile>,
pub filename: PathBuf,
+ pub frozen: bool,
}
pub struct Guard<'a, T> {
@@ -44,11 +45,12 @@ impl<'a, T> std::ops::DerefMut for Guard<'a, T> {
}
impl CliLockfile {
- pub fn new(lockfile: Lockfile) -> Self {
+ pub fn new(lockfile: Lockfile, frozen: bool) -> Self {
let filename = lockfile.filename.clone();
Self {
lockfile: Mutex::new(lockfile),
filename,
+ frozen,
}
}
@@ -71,6 +73,7 @@ impl CliLockfile {
}
pub fn write_if_changed(&self) -> Result<(), AnyError> {
+ self.error_if_changed()?;
let mut lockfile = self.lockfile.lock();
let Some(bytes) = lockfile.resolve_write_bytes() else {
return Ok(()); // nothing to do
@@ -127,23 +130,55 @@ impl CliLockfile {
};
let lockfile = if flags.lock_write {
- CliLockfile::new(Lockfile::new_empty(filename, true))
+ CliLockfile::new(
+ Lockfile::new_empty(filename, true),
+ flags.frozen_lockfile,
+ )
} else {
- Self::read_from_path(filename)?
+ Self::read_from_path(filename, flags.frozen_lockfile)?
};
Ok(Some(lockfile))
}
- pub fn read_from_path(filename: PathBuf) -> Result<CliLockfile, AnyError> {
+ pub fn read_from_path(
+ filename: PathBuf,
+ frozen: bool,
+ ) -> Result<CliLockfile, AnyError> {
match std::fs::read_to_string(&filename) {
- Ok(text) => Ok(CliLockfile::new(Lockfile::with_lockfile_content(
- filename, &text, false,
- )?)),
- Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
- Ok(CliLockfile::new(Lockfile::new_empty(filename, false)))
- }
+ Ok(text) => Ok(CliLockfile::new(
+ Lockfile::with_lockfile_content(filename, &text, false)?,
+ frozen,
+ )),
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(
+ CliLockfile::new(Lockfile::new_empty(filename, false), frozen),
+ ),
Err(err) => Err(err).with_context(|| {
format!("Failed reading lockfile '{}'", filename.display())
}),
}
}
+ pub fn error_if_changed(&self) -> Result<(), AnyError> {
+ if !self.frozen {
+ return Ok(());
+ }
+ let lockfile = self.lockfile.lock();
+ if lockfile.has_content_changed {
+ let suggested = if *super::DENO_FUTURE {
+ "`deno cache --frozen=false`, `deno install --frozen=false`,"
+ } else {
+ "`deno cache --frozen=false`"
+ };
+
+ let contents =
+ std::fs::read_to_string(&lockfile.filename).unwrap_or_default();
+ let new_contents = lockfile.as_json_string();
+ let diff = crate::util::diff::diff(&contents, &new_contents);
+ // has an extra newline at the end
+ let diff = diff.trim_end();
+ Err(deno_core::anyhow::anyhow!(
+ "The lockfile is out of date. Run {suggested} or rerun with `--frozen=false` to update it.\nchanges:\n{diff}"
+ ))
+ } else {
+ Ok(())
+ }
+ }
}
diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs
index 89b2a2e60..e1f3e3207 100644
--- a/cli/lsp/config.rs
+++ b/cli/lsp/config.rs
@@ -1825,7 +1825,7 @@ fn resolve_node_modules_dir(
}
fn resolve_lockfile_from_path(lockfile_path: PathBuf) -> Option<CliLockfile> {
- match CliLockfile::read_from_path(lockfile_path) {
+ match CliLockfile::read_from_path(lockfile_path, false) {
Ok(value) => {
if value.filename.exists() {
if let Ok(specifier) = ModuleSpecifier::from_file_path(&value.filename)
diff --git a/cli/module_loader.rs b/cli/module_loader.rs
index ed1a9526f..0e81736e5 100644
--- a/cli/module_loader.rs
+++ b/cli/module_loader.rs
@@ -72,10 +72,13 @@ use deno_semver::npm::NpmPackageReqReference;
pub async fn load_top_level_deps(factory: &CliFactory) -> Result<(), AnyError> {
let npm_resolver = factory.npm_resolver().await?;
if let Some(npm_resolver) = npm_resolver.as_managed() {
- npm_resolver.ensure_top_level_package_json_install().await?;
- // TODO(nathanwhit): we call `cache_packages` if the lockfile is modified,
- // so by calling it here it's possible we end up calling it twice
- npm_resolver.cache_packages().await?;
+ if !npm_resolver.ensure_top_level_package_json_install().await? {
+ if let Some(lockfile) = factory.maybe_lockfile() {
+ lockfile.error_if_changed()?;
+ }
+
+ npm_resolver.cache_packages().await?;
+ }
}
// cache as many entries in the import map as we can
if let Some(import_map) = factory.maybe_import_map().await? {
diff --git a/cli/npm/managed/mod.rs b/cli/npm/managed/mod.rs
index f0fc0f7f7..393fc8632 100644
--- a/cli/npm/managed/mod.rs
+++ b/cli/npm/managed/mod.rs
@@ -361,12 +361,15 @@ impl ManagedCliNpmResolver {
}
/// Adds package requirements to the resolver and ensures everything is setup.
+ /// This includes setting up the `node_modules` directory, if applicable.
pub async fn add_package_reqs(
&self,
packages: &[PackageReq],
) -> Result<(), AnyError> {
- let result = self.add_package_reqs_raw(packages).await;
- result.dependencies_result
+ self
+ .add_package_reqs_raw(packages)
+ .await
+ .dependencies_result
}
pub async fn add_package_reqs_raw(
@@ -381,6 +384,12 @@ impl ManagedCliNpmResolver {
}
let mut result = self.resolution.add_package_reqs(packages).await;
+
+ if result.dependencies_result.is_ok() {
+ if let Some(lockfile) = self.maybe_lockfile.as_ref() {
+ result.dependencies_result = lockfile.error_if_changed();
+ }
+ }
if result.dependencies_result.is_ok() {
result.dependencies_result =
self.cache_packages().await.map_err(AnyError::from);
@@ -442,14 +451,19 @@ impl ManagedCliNpmResolver {
self.resolution.resolve_pkg_id_from_pkg_req(req)
}
+ /// Ensures that the top level `package.json` dependencies are installed.
+ /// This may set up the `node_modules` directory.
+ ///
+ /// Returns `true` if any changes (such as caching packages) were made.
+ /// If this returns `false`, `node_modules` has _not_ been set up.
pub async fn ensure_top_level_package_json_install(
&self,
- ) -> Result<(), AnyError> {
+ ) -> Result<bool, AnyError> {
let Some(reqs) = self.package_json_deps_provider.reqs() else {
- return Ok(());
+ return Ok(false);
};
if !self.top_level_install_flag.raise() {
- return Ok(()); // already did this
+ return Ok(false); // already did this
}
// check if something needs resolving before bothering to load all
// the package information (which is slow)
@@ -460,11 +474,11 @@ impl ManagedCliNpmResolver {
log::debug!(
"All package.json deps resolvable. Skipping top level install."
);
- return Ok(()); // everything is already resolvable
+ return Ok(false); // everything is already resolvable
}
let reqs = reqs.into_iter().cloned().collect::<Vec<_>>();
- self.add_package_reqs(&reqs).await
+ self.add_package_reqs(&reqs).await.map(|_| true)
}
pub async fn cache_package_info(
diff --git a/cli/resolver.rs b/cli/resolver.rs
index 2117f250b..9305cd1c9 100644
--- a/cli/resolver.rs
+++ b/cli/resolver.rs
@@ -824,7 +824,10 @@ impl<'a> deno_graph::source::NpmResolver for WorkerCliNpmGraphResolver<'a> {
};
let top_level_result = if self.found_package_json_dep_flag.is_raised() {
- npm_resolver.ensure_top_level_package_json_install().await
+ npm_resolver
+ .ensure_top_level_package_json_install()
+ .await
+ .map(|_| ())
} else {
Ok(())
};
diff --git a/cli/tools/installer.rs b/cli/tools/installer.rs
index 9704e5966..9d5b6baf3 100644
--- a/cli/tools/installer.rs
+++ b/cli/tools/installer.rs
@@ -466,6 +466,10 @@ async fn resolve_shim_data(
executable_args.push("--cached-only".to_string());
}
+ if flags.frozen_lockfile {
+ executable_args.push("--frozen".to_string());
+ }
+
if resolve_no_prompt(&flags.permissions) {
executable_args.push("--no-prompt".to_string());
}
diff --git a/tests/specs/lockfile/frozen_lockfile/__test__.jsonc b/tests/specs/lockfile/frozen_lockfile/__test__.jsonc
new file mode 100644
index 000000000..76712a913
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/__test__.jsonc
@@ -0,0 +1,157 @@
+{
+ "tempDir": true,
+ "tests": {
+ "error_with_new_npm_dep": {
+ "steps": [
+ {
+ "args": "cache add.ts",
+ "output": "[WILDCARD]"
+ },
+ {
+ // sub.ts imports from an npm package
+ // that's not in the lockfile
+ "args": "run --frozen sub.ts",
+ "output": "frozen_new_dep_run.out",
+ "exitCode": 1
+ },
+ {
+ "args": "cache --frozen sub.ts",
+ "output": "frozen_new_dep_cache.out",
+ "exitCode": 1
+ },
+ {
+ // update the lockfile
+ "args": "cache sub.ts",
+ "output": "update_lockfile.out"
+ },
+ {
+ "args": "run --frozen sub.ts",
+ "output": "3 - 2 = 1\n"
+ }
+ ]
+ },
+ "error_with_new_jsr_dep": {
+ "steps": [
+ {
+ "args": "cache jsr.ts",
+ "output": "[WILDCARD]"
+ },
+ {
+ "args": "run --frozen jsr2.ts",
+ "output": "frozen_new_dep_jsr_run.out",
+ "exitCode": 1
+ },
+ {
+ "args": "cache --frozen jsr2.ts",
+ "output": "frozen_new_dep_jsr_cache.out",
+ "exitCode": 1
+ },
+ {
+ // update the lockfile
+ "args": "cache jsr2.ts",
+ "output": ""
+ },
+ {
+ "args": "run --frozen jsr2.ts",
+ "output": "1 + 2 = 3\n"
+ }
+ ]
+ },
+ "error_when_package_json_changed": {
+ "steps": [
+ {
+ "args": "cache add.ts",
+ "output": "[WILDCARD]"
+ },
+ {
+ "args": [
+ "eval",
+ "Deno.writeTextFileSync(\"package.json\", JSON.stringify({ dependencies: { \"@denotest/bin\": \"0.7.0\" } }))"
+ ],
+ "output": ""
+ },
+ {
+ "args": "cache --frozen add.ts",
+ "output": "frozen_package_json_changed.out",
+ "exitCode": 1
+ },
+ {
+ "envs": {
+ "DENO_FUTURE": "1"
+ },
+ "args": "install --frozen",
+ "output": "frozen_package_json_changed_install.out",
+ "exitCode": 1
+ }
+ ]
+ },
+ "no_error_when_in_lockfile": {
+ "steps": [
+ {
+ "args": "cache add.ts",
+ "output": "[WILDCARD]"
+ },
+ {
+ "args": "run --frozen add.ts",
+ "output": "1 + 2 = 3\n"
+ }
+ ]
+ },
+ "errors_if_creates_lockfile": {
+ "steps": [
+ {
+ "args": [
+ "eval",
+ "Deno.statSync('lock.json')"
+ ],
+ "output": "[WILDCARD]NotFound[WILDCARD]",
+ "exitCode": 1
+ },
+ {
+ "args": "run --frozen add.ts",
+ "output": "no_lockfile_run.out",
+ "exitCode": 1
+ }
+ ]
+ },
+ "non_analyzable_dynamic_npm": {
+ "steps": [
+ {
+ "args": "cache add.ts",
+ "output": "[WILDCARD]"
+ },
+ {
+ "args": "run --frozen sub-dynamic.ts",
+ "output": "frozen_new_dep_dynamic_npm.out",
+ "exitCode": 1
+ }
+ ]
+ },
+ "non_analyzable_dynamic_jsr": {
+ "steps": [
+ {
+ "args": "cache add.ts",
+ "output": "[WILDCARD]"
+ },
+ {
+ "args": "run --frozen --allow-net jsr-dynamic.ts",
+ "output": "frozen_new_dep_dynamic_jsr.out",
+ "exitCode": 1
+ }
+ ]
+ },
+ "non_analyzable_dynamic_http": {
+ "steps": [
+ {
+ "args": "cache add.ts",
+ "output": "[WILDCARD]"
+ },
+ {
+ "args": "run --frozen --allow-net http-dynamic.ts",
+ "output": "frozen_new_dep_dynamic_http.out",
+ "exitCode": 1
+ }
+ ]
+ }
+ }
+}
diff --git a/tests/specs/lockfile/frozen_lockfile/add.ts b/tests/specs/lockfile/frozen_lockfile/add.ts
new file mode 100644
index 000000000..ce70a43ec
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/add.ts
@@ -0,0 +1,2 @@
+import { add } from "npm:@denotest/add@1";
+console.log(`1 + 2 = ${add(1, 2)}`);
diff --git a/tests/specs/lockfile/frozen_lockfile/deno.json b/tests/specs/lockfile/frozen_lockfile/deno.json
new file mode 100644
index 000000000..176354f98
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/deno.json
@@ -0,0 +1,3 @@
+{
+ "nodeModulesDir": true
+}
diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_cache.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_cache.out
new file mode 100644
index 000000000..bf6c03367
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_cache.out
@@ -0,0 +1,11 @@
+error: The lockfile is out of date. Run `deno cache --frozen=false` or rerun with `--frozen=false` to update it.
+changes:
+ 5 | - "npm:@denotest/add@1": "npm:@denotest/add@1.0.0"
+ 5 | + "npm:@denotest/add@1": "npm:@denotest/add@1.0.0",
+ 6 | + "npm:@denotest/subtract@1": "npm:@denotest/subtract@1.0.0"
+11 | - }
+12 | + },
+13 | + "@denotest/subtract@1.0.0": {
+14 | + "integrity": "[WILDCARD]",
+15 | + "dependencies": {}
+16 | + }
diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_http.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_http.out
new file mode 100644
index 000000000..99c884e9c
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_http.out
@@ -0,0 +1,10 @@
+Download http://localhost:4545/welcome.ts
+error: Uncaught (in promise) TypeError: The lockfile is out of date. Run `deno cache --frozen=false` or rerun with `--frozen=false` to update it.
+changes:
+14 | - "remote": {}
+14 | + "remote": {
+15 | + "http://localhost:4545/welcome.ts": "[WILDCARD]"
+16 | + }
+const _ = await import(scheme + "localhost:4545/welcome.ts");
+ ^
+ at [WILDCARD] \ No newline at end of file
diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_jsr.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_jsr.out
new file mode 100644
index 000000000..e77853ab2
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_jsr.out
@@ -0,0 +1,16 @@
+Download http://127.0.0.1:4250/@denotest/add/meta.json
+Download http://127.0.0.1:4250/@denotest/add/1.0.0_meta.json
+Download http://127.0.0.1:4250/@denotest/add/1.0.0/mod.ts
+error: Uncaught (in promise) TypeError: The lockfile is out of date. Run `deno cache --frozen=false` or rerun with `--frozen=false` to update it.
+changes:
+ 5 | - "npm:@denotest/add@1": "npm:@denotest/add@1.0.0"
+ 5 | + "jsr:@denotest/add@1": "jsr:@denotest/add@1.0.0",
+ 6 | + "npm:@denotest/add@1": "npm:@denotest/add@1.0.0"
+ 7 | + },
+ 8 | + "jsr": {
+ 9 | + "@denotest/add@1.0.0": {
+10 | + "integrity": "[WILDCARD]"
+11 | + }
+const { add } = await import(scheme + "@denotest/add@1");
+ ^
+ at [WILDCARD] \ No newline at end of file
diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_npm.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_npm.out
new file mode 100644
index 000000000..9b2efa462
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_dynamic_npm.out
@@ -0,0 +1,15 @@
+Download http://localhost:4260/@denotest/subtract
+error: Uncaught (in promise) TypeError: The lockfile is out of date. Run `deno cache --frozen=false` or rerun with `--frozen=false` to update it.
+changes:
+ 5 | - "npm:@denotest/add@1": "npm:@denotest/add@1.0.0"
+ 5 | + "npm:@denotest/add@1": "npm:@denotest/add@1.0.0",
+ 6 | + "npm:@denotest/subtract@1": "npm:@denotest/subtract@1.0.0"
+11 | - }
+12 | + },
+13 | + "@denotest/subtract@1.0.0": {
+14 | + "integrity": "[WILDCARD]",
+15 | + "dependencies": {}
+16 | + }
+const { subtract } = await import(scheme + "@denotest/subtract@1");
+ ^
+ at [WILDCARD]
diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_cache.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_cache.out
new file mode 100644
index 000000000..bb523deff
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_cache.out
@@ -0,0 +1,12 @@
+error: The lockfile is out of date. Run `deno cache --frozen=false` or rerun with `--frozen=false` to update it.
+changes:
+ 5 | - "jsr:@denotest/add@1": "jsr:@denotest/add@1.0.0"
+ 6 | - },
+ 7 | - "jsr": {
+ 5 | + "jsr:@denotest/add@0.2.0": "jsr:@denotest/add@0.2.0",
+ 6 | + "jsr:@denotest/add@1": "jsr:@denotest/add@1.0.0"
+ 7 | + },
+ 8 | + "jsr": {
+ 9 | + "@denotest/add@0.2.0": {
+10 | + "integrity": "[WILDCARD]"
+11 | + },
diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_run.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_run.out
new file mode 100644
index 000000000..3a0d51678
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_jsr_run.out
@@ -0,0 +1,14 @@
+Download http://127.0.0.1:4250/@denotest/add/0.2.0_meta.json
+Download http://127.0.0.1:4250/@denotest/add/0.2.0/mod.ts
+error: The lockfile is out of date. Run `deno cache --frozen=false` or rerun with `--frozen=false` to update it.
+changes:
+ 5 | - "jsr:@denotest/add@1": "jsr:@denotest/add@1.0.0"
+ 6 | - },
+ 7 | - "jsr": {
+ 5 | + "jsr:@denotest/add@0.2.0": "jsr:@denotest/add@0.2.0",
+ 6 | + "jsr:@denotest/add@1": "jsr:@denotest/add@1.0.0"
+ 7 | + },
+ 8 | + "jsr": {
+ 9 | + "@denotest/add@0.2.0": {
+10 | + "integrity": "[WILDCARD]"
+11 | + },
diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_run.out b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_run.out
new file mode 100644
index 000000000..6dad6f6f4
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/frozen_new_dep_run.out
@@ -0,0 +1,12 @@
+Download http://localhost:4260/@denotest/subtract
+error: The lockfile is out of date. Run `deno cache --frozen=false` or rerun with `--frozen=false` to update it.
+changes:
+ 5 | - "npm:@denotest/add@1": "npm:@denotest/add@1.0.0"
+ 5 | + "npm:@denotest/add@1": "npm:@denotest/add@1.0.0",
+ 6 | + "npm:@denotest/subtract@1": "npm:@denotest/subtract@1.0.0"
+11 | - }
+12 | + },
+13 | + "@denotest/subtract@1.0.0": {
+14 | + "integrity": "[WILDCARD]",
+15 | + "dependencies": {}
+16 | + }
diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed.out b/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed.out
new file mode 100644
index 000000000..066aa2303
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed.out
@@ -0,0 +1,25 @@
+Download http://localhost:4260/@denotest/bin
+error: The lockfile is out of date. Run `deno cache --frozen=false` or rerun with `--frozen=false` to update it.
+changes:
+ 5 | - "npm:@denotest/add@1": "npm:@denotest/add@1.0.0"
+ 5 | + "npm:@denotest/add@1": "npm:@denotest/add@1.0.0",
+ 6 | + "npm:@denotest/bin@0.7.0": "npm:@denotest/bin@0.7.0"
+11 | - }
+12 | - }
+13 | - },
+14 | - "remote": {}
+12 | + },
+13 | + "@denotest/bin@0.7.0": {
+14 | + "integrity": "[WILDCARD]",
+15 | + "dependencies": {}
+16 | + }
+17 | + }
+18 | + },
+19 | + "remote": {},
+20 | + "workspace": {
+21 | + "packageJson": {
+22 | + "dependencies": [
+23 | + "npm:@denotest/bin@0.7.0"
+24 | + ]
+25 | + }
+26 | + }
diff --git a/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed_install.out b/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed_install.out
new file mode 100644
index 000000000..14530d573
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/frozen_package_json_changed_install.out
@@ -0,0 +1,25 @@
+⚠️ `deno install` behavior will change in Deno 2. To preserve the current behavior use the `-g` or `--global` flag.
+error: The lockfile is out of date. Run `deno cache --frozen=false`, `deno install --frozen=false`, or rerun with `--frozen=false` to update it.
+changes:
+ 5 | - "npm:@denotest/add@1": "npm:@denotest/add@1.0.0"
+ 5 | + "npm:@denotest/add@1": "npm:@denotest/add@1.0.0",
+ 6 | + "npm:@denotest/bin@0.7.0": "npm:@denotest/bin@0.7.0"
+11 | - }
+12 | - }
+13 | - },
+14 | - "remote": {}
+12 | + },
+13 | + "@denotest/bin@0.7.0": {
+14 | + "integrity": "[WILDCARD]",
+15 | + "dependencies": {}
+16 | + }
+17 | + }
+18 | + },
+19 | + "remote": {},
+20 | + "workspace": {
+21 | + "packageJson": {
+22 | + "dependencies": [
+23 | + "npm:@denotest/bin@0.7.0"
+24 | + ]
+25 | + }
+26 | + }
diff --git a/tests/specs/lockfile/frozen_lockfile/http-dynamic.ts b/tests/specs/lockfile/frozen_lockfile/http-dynamic.ts
new file mode 100644
index 000000000..bb773e3c9
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/http-dynamic.ts
@@ -0,0 +1,2 @@
+const scheme = "http://";
+const _ = await import(scheme + "localhost:4545/welcome.ts");
diff --git a/tests/specs/lockfile/frozen_lockfile/jsr-dynamic.ts b/tests/specs/lockfile/frozen_lockfile/jsr-dynamic.ts
new file mode 100644
index 000000000..95d4dd02c
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/jsr-dynamic.ts
@@ -0,0 +1,2 @@
+const scheme = "jsr:";
+const { add } = await import(scheme + "@denotest/add@1");
diff --git a/tests/specs/lockfile/frozen_lockfile/jsr.ts b/tests/specs/lockfile/frozen_lockfile/jsr.ts
new file mode 100644
index 000000000..1ffb7dc70
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/jsr.ts
@@ -0,0 +1 @@
+import { add } from "jsr:@denotest/add@1";
diff --git a/tests/specs/lockfile/frozen_lockfile/jsr2.ts b/tests/specs/lockfile/frozen_lockfile/jsr2.ts
new file mode 100644
index 000000000..bfc0bf48f
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/jsr2.ts
@@ -0,0 +1,2 @@
+import { sum } from "jsr:@denotest/add@0.2.0";
+console.log(`1 + 2 = ${sum(1, 2)}`);
diff --git a/tests/specs/lockfile/frozen_lockfile/no_lockfile_run.out b/tests/specs/lockfile/frozen_lockfile/no_lockfile_run.out
new file mode 100644
index 000000000..2ae84b110
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/no_lockfile_run.out
@@ -0,0 +1,20 @@
+Download http://localhost:4260/@denotest/add
+error: The lockfile is out of date. Run `deno cache --frozen=false` or rerun with `--frozen=false` to update it.
+changes:
+ 1 | -
+ 1 | +{
+ 2 | + "version": "3",
+ 3 | + "packages": {
+ 4 | + "specifiers": {
+ 5 | + "npm:@denotest/add@1": "npm:@denotest/add@1.0.0"
+ 6 | + },
+ 7 | + "npm": {
+ 8 | + "@denotest/add@1.0.0": {
+ 9 | + "integrity": "[WILDCARD]",
+10 | + "dependencies": {}
+11 | + }
+12 | + }
+13 | + },
+14 | + "remote": {}
+15 | +}
+16 | +
diff --git a/tests/specs/lockfile/frozen_lockfile/package.json b/tests/specs/lockfile/frozen_lockfile/package.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/package.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/specs/lockfile/frozen_lockfile/sub-dynamic.ts b/tests/specs/lockfile/frozen_lockfile/sub-dynamic.ts
new file mode 100644
index 000000000..255dec2b2
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/sub-dynamic.ts
@@ -0,0 +1,2 @@
+const scheme = "npm:";
+const { subtract } = await import(scheme + "@denotest/subtract@1");
diff --git a/tests/specs/lockfile/frozen_lockfile/sub.ts b/tests/specs/lockfile/frozen_lockfile/sub.ts
new file mode 100644
index 000000000..f212b290d
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/sub.ts
@@ -0,0 +1,2 @@
+import { subtract } from "npm:@denotest/subtract@1";
+console.log(`3 - 2 = ${subtract(3, 2)}`);
diff --git a/tests/specs/lockfile/frozen_lockfile/update_lockfile.out b/tests/specs/lockfile/frozen_lockfile/update_lockfile.out
new file mode 100644
index 000000000..0eebd114e
--- /dev/null
+++ b/tests/specs/lockfile/frozen_lockfile/update_lockfile.out
@@ -0,0 +1,2 @@
+Download http://localhost:4260/@denotest/subtract/1.0.0.tgz
+Initialize @denotest/subtract@1.0.0
diff --git a/tests/specs/npm/npmrc_not_next_to_package_json/main.out b/tests/specs/npm/npmrc_not_next_to_package_json/main.out
index 933d44f1c..8f42fb6d8 100644
--- a/tests/specs/npm/npmrc_not_next_to_package_json/main.out
+++ b/tests/specs/npm/npmrc_not_next_to_package_json/main.out
@@ -4,4 +4,3 @@
[WILDCARD]
Download http://localhost:4260/@denotest/esm-basic/1.0.0.tgz
Initialize @denotest/esm-basic@1.0.0
-[WILDCARD]