1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 07:14:47 -05:00

BREAKING: remove deno vendor (#25343)

This commit is contained in:
Asher Gomez 2024-09-03 17:00:57 +10:00 committed by GitHub
parent 2533d68cab
commit 5f08d634f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 33 additions and 4393 deletions

View file

@ -412,13 +412,6 @@ pub struct UpgradeFlags {
pub version_or_hash_or_channel: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VendorFlags {
pub specifiers: Vec<String>,
pub output_path: Option<String>,
pub force: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PublishFlags {
pub token: Option<String>,
@ -463,7 +456,7 @@ pub enum DenoSubcommand {
Test(TestFlags),
Types,
Upgrade(UpgradeFlags),
Vendor(VendorFlags),
Vendor,
Publish(PublishFlags),
Help(HelpFlags),
}
@ -3008,58 +3001,14 @@ update to a different location, use the --output flag:
})
}
// TODO(bartlomieju): this subcommand is now deprecated, remove it in Deno 2.
fn vendor_subcommand() -> Command {
command("vendor",
"⚠️ Warning: `deno vendor` is deprecated and will be removed in Deno 2.0.
Add `\"vendor\": true` to your `deno.json` or use the `--vendor` flag instead.
"⚠️ `deno vendor` was removed in Deno 2.
Vendor remote modules into a local directory.
Analyzes the provided modules along with their dependencies, downloads
remote modules to the output directory, and produces an import map that
maps remote specifiers to the downloaded files.
deno vendor main.ts
deno run --import-map vendor/import_map.json main.ts
Remote modules and multiple modules may also be specified:
deno vendor main.ts test.deps.ts jsr:@std/path",
See the Deno 1.x to 2.x Migration Guide for migration instructions: https://docs.deno.com/runtime/manual/advanced/migrate_deprecations",
UnstableArgsConfig::ResolutionOnly
)
.hide(true)
.defer(|cmd| cmd
.arg(
Arg::new("specifiers")
.num_args(1..)
.action(ArgAction::Append)
.required_unless_present("help"),
)
.arg(
Arg::new("output")
.long("output")
.help("The directory to output the vendored modules to")
.value_parser(value_parser!(String))
.value_hint(ValueHint::DirPath),
)
.arg(
Arg::new("force")
.long("force")
.short('f')
.help(
"Forcefully overwrite conflicting files in existing output directory",
)
.action(ArgAction::SetTrue),
)
.arg(no_config_arg())
.arg(config_arg())
.arg(import_map_arg())
.arg(lock_arg())
.arg(node_modules_dir_arg())
.arg(vendor_arg())
.arg(reload_arg())
.arg(ca_file_arg())
.arg(unsafely_ignore_certificate_errors_arg())
)
}
fn publish_subcommand() -> Command {
@ -4751,24 +4700,8 @@ fn upgrade_parse(flags: &mut Flags, matches: &mut ArgMatches) {
});
}
fn vendor_parse(flags: &mut Flags, matches: &mut ArgMatches) {
unstable_args_parse(flags, matches, UnstableArgsConfig::ResolutionOnly);
ca_file_arg_parse(flags, matches);
unsafely_ignore_certificate_errors_parse(flags, matches);
config_args_parse(flags, matches);
import_map_arg_parse(flags, matches);
lock_arg_parse(flags, matches);
node_modules_and_vendor_dir_arg_parse(flags, matches);
reload_arg_parse(flags, matches);
flags.subcommand = DenoSubcommand::Vendor(VendorFlags {
specifiers: matches
.remove_many::<String>("specifiers")
.map(|p| p.collect())
.unwrap_or_default(),
output_path: matches.remove_one::<String>("output"),
force: matches.get_flag("force"),
});
fn vendor_parse(flags: &mut Flags, _matches: &mut ArgMatches) {
flags.subcommand = DenoSubcommand::Vendor
}
fn publish_parse(flags: &mut Flags, matches: &mut ArgMatches) {
@ -9671,57 +9604,6 @@ mod tests {
assert!(&error_message.contains("--watch[=<FILES>...]"));
}
#[test]
fn vendor_minimal() {
let r = flags_from_vec(svec!["deno", "vendor", "mod.ts",]);
assert_eq!(
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Vendor(VendorFlags {
specifiers: svec!["mod.ts"],
force: false,
output_path: None,
}),
..Flags::default()
}
);
}
#[test]
fn vendor_all() {
let r = flags_from_vec(svec![
"deno",
"vendor",
"--config",
"deno.json",
"--import-map",
"import_map.json",
"--lock",
"lock.json",
"--force",
"--output",
"out_dir",
"--reload",
"mod.ts",
"deps.test.ts",
]);
assert_eq!(
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Vendor(VendorFlags {
specifiers: svec!["mod.ts", "deps.test.ts"],
force: true,
output_path: Some(String::from("out_dir")),
}),
config_flag: ConfigFlag::Path("deno.json".to_owned()),
import_map_path: Some("import_map.json".to_string()),
lock: Some(String::from("lock.json")),
reload: true,
..Flags::default()
}
);
}
#[test]
fn task_subcommand() {
let r = flags_from_vec(svec!["deno", "task", "build", "hello", "world",]);

View file

@ -1217,11 +1217,6 @@ impl CliOptions {
NPM_PROCESS_STATE.is_some()
}
/// Overrides the import map specifier to use.
pub fn set_import_map_specifier(&mut self, path: Option<ModuleSpecifier>) {
self.overrides.import_map_specifier = Some(path);
}
pub fn has_node_modules_dir(&self) -> bool {
self.maybe_node_modules_folder.is_some()
}
@ -1230,21 +1225,6 @@ impl CliOptions {
self.maybe_node_modules_folder.as_ref()
}
pub fn with_node_modules_dir_path(&self, path: PathBuf) -> Self {
Self {
flags: self.flags.clone(),
initial_cwd: self.initial_cwd.clone(),
maybe_node_modules_folder: Some(path),
npmrc: self.npmrc.clone(),
maybe_lockfile: self.maybe_lockfile.clone(),
start_dir: self.start_dir.clone(),
overrides: self.overrides.clone(),
disable_deprecated_api_warning: self.disable_deprecated_api_warning,
verbose_deprecated_api_warning: self.verbose_deprecated_api_warning,
deno_dir_provider: self.deno_dir_provider.clone(),
}
}
pub fn node_modules_dir(
&self,
) -> Result<Option<NodeModulesDirMode>, AnyError> {

View file

@ -283,9 +283,7 @@ async fn run_subcommand(flags: Arc<Flags>) -> Result<i32, AnyError> {
"This deno was built without the \"upgrade\" feature. Please upgrade using the installation method originally used to install Deno.",
1,
),
DenoSubcommand::Vendor(vendor_flags) => spawn_subcommand(async {
tools::vendor::vendor(flags, vendor_flags).await
}),
DenoSubcommand::Vendor => exit_with_message("⚠️ `deno vendor` was removed in Deno 2.\n\nSee the Deno 1.x to 2.x Migration Guide for migration instructions: https://docs.deno.com/runtime/manual/advanced/migrate_deprecations", 1),
DenoSubcommand::Publish(publish_flags) => spawn_subcommand(async {
tools::registry::publish(flags, publish_flags).await
}),

View file

@ -19,4 +19,3 @@ pub mod serve;
pub mod task;
pub mod test;
pub mod upgrade;
pub mod vendor;

View file

@ -1,113 +0,0 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use deno_ast::swc::ast::ExportDefaultDecl;
use deno_ast::swc::ast::ExportSpecifier;
use deno_ast::swc::ast::ModuleExportName;
use deno_ast::swc::ast::NamedExport;
use deno_ast::swc::ast::Program;
use deno_ast::swc::visit::noop_visit_type;
use deno_ast::swc::visit::Visit;
use deno_ast::swc::visit::VisitWith;
use deno_ast::ParsedSource;
/// Gets if the parsed source has a default export.
pub fn has_default_export(source: &ParsedSource) -> bool {
let mut visitor = DefaultExportFinder {
has_default_export: false,
};
let program = source.program();
let program: &Program = &program;
program.visit_with(&mut visitor);
visitor.has_default_export
}
struct DefaultExportFinder {
has_default_export: bool,
}
impl Visit for DefaultExportFinder {
noop_visit_type!();
fn visit_export_default_decl(&mut self, _: &ExportDefaultDecl) {
self.has_default_export = true;
}
fn visit_named_export(&mut self, named_export: &NamedExport) {
if named_export
.specifiers
.iter()
.any(export_specifier_has_default)
{
self.has_default_export = true;
}
}
}
fn export_specifier_has_default(s: &ExportSpecifier) -> bool {
match s {
ExportSpecifier::Default(_) => true,
ExportSpecifier::Namespace(_) => false,
ExportSpecifier::Named(named) => {
let export_name = named.exported.as_ref().unwrap_or(&named.orig);
match export_name {
ModuleExportName::Str(_) => false,
ModuleExportName::Ident(ident) => &*ident.sym == "default",
}
}
}
}
#[cfg(test)]
mod test {
use deno_ast::MediaType;
use deno_ast::ModuleSpecifier;
use deno_ast::ParseParams;
use deno_ast::ParsedSource;
use super::has_default_export;
#[test]
fn has_default_when_export_default_decl() {
let parsed_source = parse_module("export default class Class {}");
assert!(has_default_export(&parsed_source));
}
#[test]
fn has_default_when_named_export() {
let parsed_source = parse_module("export {default} from './test.ts';");
assert!(has_default_export(&parsed_source));
}
#[test]
fn has_default_when_named_export_alias() {
let parsed_source =
parse_module("export {test as default} from './test.ts';");
assert!(has_default_export(&parsed_source));
}
#[test]
fn not_has_default_when_named_export_not_exported() {
let parsed_source =
parse_module("export {default as test} from './test.ts';");
assert!(!has_default_export(&parsed_source));
}
#[test]
fn not_has_default_when_not() {
let parsed_source = parse_module("export {test} from './test.ts'; export class Test{} export * from './test';");
assert!(!has_default_export(&parsed_source));
}
fn parse_module(text: &str) -> ParsedSource {
deno_ast::parse_module(ParseParams {
specifier: ModuleSpecifier::parse("file:///mod.ts").unwrap(),
capture_tokens: false,
maybe_syntax: None,
media_type: MediaType::TypeScript,
scope_analysis: false,
text: text.into(),
})
.unwrap()
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,508 +0,0 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use deno_ast::LineAndColumnIndex;
use deno_ast::ModuleSpecifier;
use deno_ast::SourceTextInfo;
use deno_core::error::AnyError;
use deno_graph::source::ResolutionMode;
use deno_graph::Module;
use deno_graph::ModuleGraph;
use deno_graph::Position;
use deno_graph::Range;
use deno_graph::Resolution;
use import_map::ImportMap;
use import_map::SpecifierMap;
use indexmap::IndexMap;
use log::warn;
use crate::args::JsxImportSourceConfig;
use crate::cache::ParsedSourceCache;
use super::mappings::Mappings;
use super::specifiers::is_remote_specifier;
use super::specifiers::is_remote_specifier_text;
struct ImportMapBuilder<'a> {
base_dir: &'a ModuleSpecifier,
mappings: &'a Mappings,
imports: ImportsBuilder<'a>,
scopes: IndexMap<String, ImportsBuilder<'a>>,
}
impl<'a> ImportMapBuilder<'a> {
pub fn new(base_dir: &'a ModuleSpecifier, mappings: &'a Mappings) -> Self {
ImportMapBuilder {
base_dir,
mappings,
imports: ImportsBuilder::new(base_dir, mappings),
scopes: Default::default(),
}
}
pub fn base_dir(&self) -> &ModuleSpecifier {
self.base_dir
}
pub fn scope(
&mut self,
base_specifier: &ModuleSpecifier,
) -> &mut ImportsBuilder<'a> {
self
.scopes
.entry(
self
.mappings
.relative_specifier_text(self.base_dir, base_specifier),
)
.or_insert_with(|| ImportsBuilder::new(self.base_dir, self.mappings))
}
pub fn into_import_map(
self,
maybe_original_import_map: Option<&ImportMap>,
) -> ImportMap {
fn get_local_imports(
new_relative_path: &str,
original_imports: &SpecifierMap,
) -> Vec<(String, String)> {
let mut result = Vec::new();
for entry in original_imports.entries() {
if let Some(raw_value) = entry.raw_value {
if raw_value.starts_with("./") || raw_value.starts_with("../") {
let sub_index = raw_value.find('/').unwrap() + 1;
result.push((
entry.raw_key.to_string(),
format!("{}{}", new_relative_path, &raw_value[sub_index..]),
));
}
}
}
result
}
fn add_local_imports<'a>(
new_relative_path: &str,
original_imports: &SpecifierMap,
get_new_imports: impl FnOnce() -> &'a mut SpecifierMap,
) {
let local_imports =
get_local_imports(new_relative_path, original_imports);
if !local_imports.is_empty() {
let new_imports = get_new_imports();
for (key, value) in local_imports {
if let Err(warning) = new_imports.append(key, value) {
warn!("{}", warning);
}
}
}
}
let mut import_map = ImportMap::new(self.base_dir.clone());
if let Some(original_im) = maybe_original_import_map {
let original_base_dir = ModuleSpecifier::from_directory_path(
original_im
.base_url()
.to_file_path()
.unwrap()
.parent()
.unwrap(),
)
.unwrap();
let new_relative_path = self
.mappings
.relative_specifier_text(self.base_dir, &original_base_dir);
// add the imports
add_local_imports(&new_relative_path, original_im.imports(), || {
import_map.imports_mut()
});
for scope in original_im.scopes() {
if scope.raw_key.starts_with("./") || scope.raw_key.starts_with("../") {
let sub_index = scope.raw_key.find('/').unwrap() + 1;
let new_key =
format!("{}{}", new_relative_path, &scope.raw_key[sub_index..]);
add_local_imports(&new_relative_path, scope.imports, || {
import_map.get_or_append_scope_mut(&new_key).unwrap()
});
}
}
}
let imports = import_map.imports_mut();
for (key, value) in self.imports.imports {
if !imports.contains(&key) {
imports.append(key, value).unwrap();
}
}
for (scope_key, scope_value) in self.scopes {
if !scope_value.imports.is_empty() {
let imports = import_map.get_or_append_scope_mut(&scope_key).unwrap();
for (key, value) in scope_value.imports {
if !imports.contains(&key) {
imports.append(key, value).unwrap();
}
}
}
}
import_map
}
}
struct ImportsBuilder<'a> {
base_dir: &'a ModuleSpecifier,
mappings: &'a Mappings,
imports: IndexMap<String, String>,
}
impl<'a> ImportsBuilder<'a> {
pub fn new(base_dir: &'a ModuleSpecifier, mappings: &'a Mappings) -> Self {
Self {
base_dir,
mappings,
imports: Default::default(),
}
}
pub fn add(&mut self, key: String, specifier: &ModuleSpecifier) {
let value = self
.mappings
.relative_specifier_text(self.base_dir, specifier);
// skip creating identity entries
if key != value {
self.imports.insert(key, value);
}
}
}
pub struct BuildImportMapInput<'a> {
pub base_dir: &'a ModuleSpecifier,
pub modules: &'a [&'a Module],
pub graph: &'a ModuleGraph,
pub mappings: &'a Mappings,
pub maybe_original_import_map: Option<&'a ImportMap>,
pub maybe_jsx_import_source: Option<&'a JsxImportSourceConfig>,
pub resolver: &'a dyn deno_graph::source::Resolver,
pub parsed_source_cache: &'a ParsedSourceCache,
}
pub fn build_import_map(
input: BuildImportMapInput<'_>,
) -> Result<String, AnyError> {
let BuildImportMapInput {
base_dir,
modules,
graph,
mappings,
maybe_original_import_map,
maybe_jsx_import_source,
resolver,
parsed_source_cache,
} = input;
let mut builder = ImportMapBuilder::new(base_dir, mappings);
visit_modules(graph, modules, mappings, &mut builder, parsed_source_cache)?;
for base_specifier in mappings.base_specifiers() {
builder
.imports
.add(base_specifier.to_string(), base_specifier);
}
// add the jsx import source to the destination import map, if mapped in the original import map
if let Some(jsx_import_source) = maybe_jsx_import_source {
if let Some(specifier_text) = jsx_import_source.maybe_specifier_text() {
if let Ok(resolved_url) = resolver.resolve(
&specifier_text,
&deno_graph::Range {
specifier: jsx_import_source.base_url.clone(),
start: deno_graph::Position::zeroed(),
end: deno_graph::Position::zeroed(),
},
ResolutionMode::Execution,
) {
builder.imports.add(specifier_text, &resolved_url);
}
}
}
Ok(builder.into_import_map(maybe_original_import_map).to_json())
}
fn visit_modules(
graph: &ModuleGraph,
modules: &[&Module],
mappings: &Mappings,
import_map: &mut ImportMapBuilder,
parsed_source_cache: &ParsedSourceCache,
) -> Result<(), AnyError> {
for module in modules {
let module = match module {
Module::Js(module) => module,
// skip visiting Json modules as they are leaves
Module::Json(_)
| Module::Npm(_)
| Module::Node(_)
| Module::External(_) => continue,
};
let parsed_source =
parsed_source_cache.get_parsed_source_from_js_module(module)?;
let text_info = parsed_source.text_info_lazy().clone();
for dep in module.dependencies.values() {
visit_resolution(
&dep.maybe_code,
graph,
import_map,
&module.specifier,
mappings,
&text_info,
&module.source,
);
visit_resolution(
&dep.maybe_type,
graph,
import_map,
&module.specifier,
mappings,
&text_info,
&module.source,
);
}
if let Some(types_dep) = &module.maybe_types_dependency {
visit_resolution(
&types_dep.dependency,
graph,
import_map,
&module.specifier,
mappings,
&text_info,
&module.source,
);
}
}
Ok(())
}
fn visit_resolution(
resolution: &Resolution,
graph: &ModuleGraph,
import_map: &mut ImportMapBuilder,
referrer: &ModuleSpecifier,
mappings: &Mappings,
text_info: &SourceTextInfo,
source_text: &str,
) {
if let Some(resolved) = resolution.ok() {
let text = text_from_range(text_info, source_text, &resolved.range);
// if the text is empty then it's probably an x-TypeScript-types
if !text.is_empty() {
handle_dep_specifier(
text,
&resolved.specifier,
graph,
import_map,
referrer,
mappings,
);
}
}
}
fn handle_dep_specifier(
text: &str,
unresolved_specifier: &ModuleSpecifier,
graph: &ModuleGraph,
import_map: &mut ImportMapBuilder,
referrer: &ModuleSpecifier,
mappings: &Mappings,
) {
let specifier = match graph.get(unresolved_specifier) {
Some(module) => module.specifier().clone(),
// Ignore when None. The graph was previous validated so this is a
// dynamic import that was missing and is ignored for vendoring
None => return,
};
// check if it's referencing a remote module
if is_remote_specifier(&specifier) {
handle_remote_dep_specifier(
text,
unresolved_specifier,
&specifier,
import_map,
referrer,
mappings,
)
} else if specifier.scheme() == "file" {
handle_local_dep_specifier(
text,
unresolved_specifier,
&specifier,
import_map,
referrer,
mappings,
);
}
}
fn handle_remote_dep_specifier(
text: &str,
unresolved_specifier: &ModuleSpecifier,
specifier: &ModuleSpecifier,
import_map: &mut ImportMapBuilder,
referrer: &ModuleSpecifier,
mappings: &Mappings,
) {
if is_remote_specifier_text(text) {
let base_specifier = mappings.base_specifier(specifier);
if text.starts_with(base_specifier.as_str()) {
let sub_path = &text[base_specifier.as_str().len()..];
let relative_text =
mappings.relative_specifier_text(base_specifier, specifier);
let expected_sub_path = relative_text.trim_start_matches("./");
if expected_sub_path != sub_path {
import_map.imports.add(text.to_string(), specifier);
}
} else {
// it's probably a redirect. Add it explicitly to the import map
import_map.imports.add(text.to_string(), specifier);
}
} else {
let expected_relative_specifier_text =
mappings.relative_specifier_text(referrer, specifier);
if expected_relative_specifier_text == text {
return;
}
if !is_remote_specifier(referrer) {
// local module referencing a remote module using
// non-remote specifier text means it was something in
// the original import map, so add a mapping to it
import_map.imports.add(text.to_string(), specifier);
return;
}
let base_referrer = mappings.base_specifier(referrer);
let base_dir = import_map.base_dir().clone();
let imports = import_map.scope(base_referrer);
if text.starts_with("./") || text.starts_with("../") {
// resolve relative specifier key
let mut local_base_specifier = mappings.local_uri(base_referrer);
local_base_specifier = local_base_specifier
// path includes "/" so make it relative
.join(&format!(".{}", unresolved_specifier.path()))
.unwrap_or_else(|_| {
panic!(
"Error joining {} to {}",
unresolved_specifier.path(),
local_base_specifier
)
});
local_base_specifier.set_query(unresolved_specifier.query());
imports.add(
mappings.relative_specifier_text(&base_dir, &local_base_specifier),
specifier,
);
// add a mapping that uses the local directory name and the remote
// filename in order to support files importing this relatively
imports.add(
{
let local_path = mappings.local_path(specifier);
let mut value =
ModuleSpecifier::from_directory_path(local_path.parent().unwrap())
.unwrap();
value.set_query(specifier.query());
value.set_path(&format!(
"{}{}",
value.path(),
specifier.path_segments().unwrap().last().unwrap(),
));
mappings.relative_specifier_text(&base_dir, &value)
},
specifier,
);
} else {
// absolute (`/`) or bare specifier should be left as-is
imports.add(text.to_string(), specifier);
}
}
}
fn handle_local_dep_specifier(
text: &str,
unresolved_specifier: &ModuleSpecifier,
specifier: &ModuleSpecifier,
import_map: &mut ImportMapBuilder,
referrer: &ModuleSpecifier,
mappings: &Mappings,
) {
if !is_remote_specifier(referrer) {
// do not handle local modules referencing local modules
return;
}
// The remote module is referencing a local file. This could occur via an
// existing import map. In this case, we'll have to add an import map
// entry in order to map the path back to the local path once vendored.
let base_dir = import_map.base_dir().clone();
let base_specifier = mappings.base_specifier(referrer);
let imports = import_map.scope(base_specifier);
if text.starts_with("./") || text.starts_with("../") {
let referrer_local_uri = mappings.local_uri(referrer);
let mut specifier_local_uri =
referrer_local_uri.join(text).unwrap_or_else(|_| {
panic!(
"Error joining {} to {}",
unresolved_specifier.path(),
referrer_local_uri
)
});
specifier_local_uri.set_query(unresolved_specifier.query());
imports.add(
mappings.relative_specifier_text(&base_dir, &specifier_local_uri),
specifier,
);
} else {
imports.add(text.to_string(), specifier);
}
}
fn text_from_range<'a>(
text_info: &SourceTextInfo,
text: &'a str,
range: &Range,
) -> &'a str {
let result = &text[byte_range(text_info, range)];
if result.starts_with('"') || result.starts_with('\'') {
// remove the quotes
&result[1..result.len() - 1]
} else {
result
}
}
fn byte_range(
text_info: &SourceTextInfo,
range: &Range,
) -> std::ops::Range<usize> {
let start = byte_index(text_info, &range.start);
let end = byte_index(text_info, &range.end);
start..end
}
fn byte_index(text_info: &SourceTextInfo, pos: &Position) -> usize {
// todo(https://github.com/denoland/deno_graph/issues/79): use byte indexes all the way down
text_info.loc_to_source_pos(LineAndColumnIndex {
line_index: pos.line,
column_index: pos.character,
}) - text_info.range().start
}

View file

@ -1,255 +0,0 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use deno_ast::MediaType;
use deno_ast::ModuleSpecifier;
use deno_core::error::AnyError;
use deno_graph::Module;
use deno_graph::ModuleGraph;
use deno_graph::Position;
use crate::util::path::path_with_stem_suffix;
use crate::util::path::relative_specifier;
use super::specifiers::dir_name_for_root;
use super::specifiers::get_unique_path;
use super::specifiers::make_url_relative;
use super::specifiers::partition_by_root_specifiers;
use super::specifiers::sanitize_filepath;
pub struct ProxiedModule {
pub output_path: PathBuf,
pub declaration_specifier: ModuleSpecifier,
}
/// Constructs and holds the remote specifier to local path mappings.
pub struct Mappings {
mappings: HashMap<ModuleSpecifier, PathBuf>,
base_specifiers: Vec<ModuleSpecifier>,
proxies: HashMap<ModuleSpecifier, ProxiedModule>,
}
impl Mappings {
pub fn from_remote_modules(
graph: &ModuleGraph,
remote_modules: &[&Module],
output_dir: &Path,
) -> Result<Self, AnyError> {
let partitioned_specifiers = partition_by_root_specifiers(
remote_modules.iter().map(|m| m.specifier()),
);
let mut mapped_paths = HashSet::new();
let mut mappings = HashMap::new();
let mut proxies = HashMap::new();
let mut base_specifiers = Vec::new();
for (root, specifiers) in partitioned_specifiers.into_iter() {
let base_dir = get_unique_path(
output_dir.join(dir_name_for_root(&root)),
&mut mapped_paths,
);
for specifier in specifiers {
let module = graph.get(&specifier).unwrap();
let media_type = match module {
Module::Js(module) => module.media_type,
Module::Json(_) => MediaType::Json,
Module::Node(_) | Module::Npm(_) | Module::External(_) => continue,
};
let sub_path = sanitize_filepath(&make_url_relative(&root, &{
let mut specifier = specifier.clone();
specifier.set_query(None);
specifier
})?);
let new_path = path_with_extension(
&base_dir.join(if cfg!(windows) {
sub_path.replace('/', "\\")
} else {
sub_path
}),
&media_type.as_ts_extension()[1..],
);
mappings
.insert(specifier, get_unique_path(new_path, &mut mapped_paths));
}
base_specifiers.push(root.clone());
mappings.insert(root, base_dir);
}
// resolve all the "proxy" paths to use for when an x-typescript-types header is specified
for module in remote_modules {
if let Some(module) = module.js() {
if let Some(resolved) = &module
.maybe_types_dependency
.as_ref()
.and_then(|d| d.dependency.ok())
{
let range = &resolved.range;
// hack to tell if it's an x-typescript-types header
let is_ts_types_header = range.start == Position::zeroed()
&& range.end == Position::zeroed();
if is_ts_types_header {
let module_path = mappings.get(&module.specifier).unwrap();
let proxied_path = get_unique_path(
path_with_stem_suffix(module_path, ".proxied"),
&mut mapped_paths,
);
proxies.insert(
module.specifier.clone(),
ProxiedModule {
output_path: proxied_path,
declaration_specifier: resolved.specifier.clone(),
},
);
}
}
}
}
Ok(Self {
mappings,
base_specifiers,
proxies,
})
}
pub fn local_uri(&self, specifier: &ModuleSpecifier) -> ModuleSpecifier {
if specifier.scheme() == "file" {
specifier.clone()
} else {
let local_path = self.local_path(specifier);
if specifier.path().ends_with('/') {
ModuleSpecifier::from_directory_path(&local_path)
} else {
ModuleSpecifier::from_file_path(&local_path)
}
.unwrap_or_else(|_| {
panic!("Could not convert {} to uri.", local_path.display())
})
}
}
pub fn local_path(&self, specifier: &ModuleSpecifier) -> PathBuf {
if specifier.scheme() == "file" {
specifier.to_file_path().unwrap()
} else {
self
.mappings
.get(specifier)
.unwrap_or_else(|| panic!("Could not find local path for {specifier}"))
.to_path_buf()
}
}
pub fn relative_specifier_text(
&self,
from: &ModuleSpecifier,
to: &ModuleSpecifier,
) -> String {
let from = self.local_uri(from);
let to = self.local_uri(to);
relative_specifier(&from, &to).unwrap()
}
pub fn base_specifiers(&self) -> &Vec<ModuleSpecifier> {
&self.base_specifiers
}
pub fn base_specifier(
&self,
child_specifier: &ModuleSpecifier,
) -> &ModuleSpecifier {
self
.base_specifiers
.iter()
.find(|s| child_specifier.as_str().starts_with(s.as_str()))
.unwrap_or_else(|| {
panic!("Could not find base specifier for {child_specifier}")
})
}
pub fn proxied_path(&self, specifier: &ModuleSpecifier) -> Option<PathBuf> {
self.proxies.get(specifier).map(|s| s.output_path.clone())
}
pub fn proxied_modules(
&self,
) -> std::collections::hash_map::Iter<'_, ModuleSpecifier, ProxiedModule> {
self.proxies.iter()
}
}
fn path_with_extension(path: &Path, new_ext: &str) -> PathBuf {
if let Some(file_stem) = path.file_stem().map(|f| f.to_string_lossy()) {
if let Some(old_ext) = path.extension().map(|f| f.to_string_lossy()) {
if file_stem.to_lowercase().ends_with(".d") {
if new_ext.to_lowercase() == format!("d.{}", old_ext.to_lowercase()) {
// maintain casing
return path.to_path_buf();
}
return path.with_file_name(format!(
"{}.{}",
&file_stem[..file_stem.len() - ".d".len()],
new_ext
));
}
if new_ext.to_lowercase() == old_ext.to_lowercase() {
// maintain casing
return path.to_path_buf();
}
let media_type = MediaType::from_path(path);
if media_type == MediaType::Unknown {
return path.with_file_name(format!(
"{}.{}",
path.file_name().unwrap().to_string_lossy(),
new_ext
));
}
}
}
path.with_extension(new_ext)
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_path_with_extension() {
assert_eq!(
path_with_extension(&PathBuf::from("/test.D.TS"), "ts"),
PathBuf::from("/test.ts")
);
assert_eq!(
path_with_extension(&PathBuf::from("/test.D.MTS"), "js"),
PathBuf::from("/test.js")
);
assert_eq!(
path_with_extension(&PathBuf::from("/test.D.TS"), "d.ts"),
// maintains casing
PathBuf::from("/test.D.TS"),
);
assert_eq!(
path_with_extension(&PathBuf::from("/test.TS"), "ts"),
// maintains casing
PathBuf::from("/test.TS"),
);
assert_eq!(
path_with_extension(&PathBuf::from("/test.ts"), "js"),
PathBuf::from("/test.js")
);
assert_eq!(
path_with_extension(&PathBuf::from("/test.js"), "js"),
PathBuf::from("/test.js")
);
assert_eq!(
path_with_extension(&PathBuf::from("/chai@1.2.3"), "js"),
PathBuf::from("/chai@1.2.3.js")
);
}
}

View file

@ -1,578 +0,0 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use deno_ast::ModuleSpecifier;
use deno_ast::TextChange;
use deno_core::anyhow::bail;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
use deno_core::futures::FutureExt;
use deno_core::resolve_url_or_path;
use deno_graph::GraphKind;
use deno_runtime::colors;
use log::warn;
use crate::args::CliOptions;
use crate::args::ConfigFile;
use crate::args::Flags;
use crate::args::FmtOptionsConfig;
use crate::args::VendorFlags;
use crate::factory::CliFactory;
use crate::tools::fmt::format_json;
use crate::util::fs::canonicalize_path;
use crate::util::fs::resolve_from_cwd;
use crate::util::path::relative_specifier;
use deno_runtime::fs_util::specifier_to_file_path;
mod analyze;
mod build;
mod import_map;
mod mappings;
mod specifiers;
#[cfg(test)]
mod test;
pub async fn vendor(
flags: Arc<Flags>,
vendor_flags: VendorFlags,
) -> Result<(), AnyError> {
log::info!(
"{}",
colors::yellow("⚠️ Warning: `deno vendor` is deprecated and will be removed in Deno 2.0.\nAdd `\"vendor\": true` to your `deno.json` or use the `--vendor` flag instead."),
);
let mut cli_options = CliOptions::from_flags(flags)?;
let raw_output_dir = match &vendor_flags.output_path {
Some(output_path) => PathBuf::from(output_path).to_owned(),
None => PathBuf::from("vendor/"),
};
let output_dir = resolve_from_cwd(&raw_output_dir)?;
validate_output_dir(&output_dir, &vendor_flags)?;
validate_options(&mut cli_options, &output_dir)?;
let factory = CliFactory::from_cli_options(Arc::new(cli_options));
let cli_options = factory.cli_options()?;
if cli_options.workspace().config_folders().len() > 1 {
bail!("deno vendor is not supported in a workspace. Set `\"vendor\": true` in the workspace deno.json file instead");
}
let entry_points =
resolve_entry_points(&vendor_flags, cli_options.initial_cwd())?;
let jsx_import_source = cli_options
.workspace()
.to_maybe_jsx_import_source_config()?;
let module_graph_creator = factory.module_graph_creator().await?.clone();
let workspace_resolver = factory.workspace_resolver().await?;
let root_folder = cli_options.workspace().root_folder_configs();
let maybe_config_file = root_folder.deno_json.as_ref();
let output = build::build(build::BuildInput {
entry_points,
build_graph: move |entry_points| {
async move {
module_graph_creator
.create_graph(GraphKind::All, entry_points)
.await
}
.boxed_local()
},
parsed_source_cache: factory.parsed_source_cache(),
output_dir: &output_dir,
maybe_original_import_map: workspace_resolver.maybe_import_map(),
maybe_jsx_import_source: jsx_import_source.as_ref(),
resolver: factory.resolver().await?.as_graph_resolver(),
environment: &build::RealVendorEnvironment,
})
.await?;
let vendored_count = output.vendored_count;
let graph = output.graph;
let npm_package_count = graph.npm_packages.len();
let try_add_node_modules_dir = npm_package_count > 0
&& cli_options
.node_modules_dir()?
.map(|m| m.uses_node_modules_dir())
.unwrap_or(true);
log::info!(
concat!("Vendored {} {} into {} directory.",),
vendored_count,
if vendored_count == 1 {
"module"
} else {
"modules"
},
raw_output_dir.display(),
);
let try_add_import_map = vendored_count > 0;
let modified_result = maybe_update_config_file(
&output_dir,
maybe_config_file,
try_add_import_map,
try_add_node_modules_dir,
);
// cache the node_modules folder when it's been added to the config file
if modified_result.added_node_modules_dir {
let node_modules_path =
cli_options.node_modules_dir_path().cloned().or_else(|| {
maybe_config_file
.as_ref()
.map(|d| &d.specifier)
.filter(|c| c.scheme() == "file")
.and_then(|c| c.to_file_path().ok())
.map(|config_path| config_path.parent().unwrap().join("node_modules"))
});
if let Some(node_modules_path) = node_modules_path {
let cli_options =
cli_options.with_node_modules_dir_path(node_modules_path);
let factory = CliFactory::from_cli_options(Arc::new(cli_options));
if let Some(managed) = factory.npm_resolver().await?.as_managed() {
managed.cache_packages().await?;
}
}
log::info!(
concat!(
"Vendored {} npm {} into node_modules directory. Set `nodeModulesDir: false` ",
"in the Deno configuration file to disable vendoring npm packages in the future.",
),
npm_package_count,
if npm_package_count == 1 {
"package"
} else {
"packages"
},
);
}
if vendored_count > 0 {
let import_map_path = raw_output_dir.join("import_map.json");
if modified_result.updated_import_map {
log::info!(
concat!(
"\nUpdated your local Deno configuration file with a reference to the ",
"new vendored import map at {}. Invoking Deno subcommands will now ",
"automatically resolve using the vendored modules. You may override ",
"this by providing the `--import-map <other-import-map>` flag or by ",
"manually editing your Deno configuration file.",
),
import_map_path.display(),
);
} else {
log::info!(
concat!(
"\nTo use vendored modules, specify the `--import-map {}` flag when ",
r#"invoking Deno subcommands or add an `"importMap": "<path_to_vendored_import_map>"` "#,
"entry to a deno.json file.",
),
import_map_path.display(),
);
}
}
Ok(())
}
fn validate_output_dir(
output_dir: &Path,
flags: &VendorFlags,
) -> Result<(), AnyError> {
if !flags.force && !is_dir_empty(output_dir)? {
bail!(concat!(
"Output directory was not empty. Please specify an empty directory or use ",
"--force to ignore this error and potentially overwrite its contents.",
));
}
Ok(())
}
fn validate_options(
options: &mut CliOptions,
output_dir: &Path,
) -> Result<(), AnyError> {
let import_map_specifier = options
.resolve_specified_import_map_specifier()?
.or_else(|| {
let config_file = options.workspace().root_deno_json()?;
config_file
.to_import_map_specifier()
.ok()
.flatten()
.or_else(|| {
if config_file.is_an_import_map() {
Some(config_file.specifier.clone())
} else {
None
}
})
});
// check the import map
if let Some(import_map_path) = import_map_specifier
.and_then(|p| specifier_to_file_path(&p).ok())
.and_then(|p| canonicalize_path(&p).ok())
{
// make the output directory in order to canonicalize it for the check below
std::fs::create_dir_all(output_dir)?;
let output_dir = canonicalize_path(output_dir).with_context(|| {
format!("Failed to canonicalize: {}", output_dir.display())
})?;
if import_map_path.starts_with(output_dir) {
// canonicalize to make the test for this pass on the CI
let cwd = canonicalize_path(&std::env::current_dir()?)?;
// We don't allow using the output directory to help generate the
// new state because this may lead to cryptic error messages.
log::warn!(
concat!(
"Ignoring import map. Specifying an import map file ({}) in the ",
"deno vendor output directory is not supported. If you wish to use ",
"an import map while vendoring, please specify one located outside ",
"this directory."
),
import_map_path
.strip_prefix(&cwd)
.unwrap_or(&import_map_path)
.display()
.to_string(),
);
// don't use an import map in the config
options.set_import_map_specifier(None);
}
}
Ok(())
}
fn maybe_update_config_file(
output_dir: &Path,
maybe_config_file: Option<&Arc<ConfigFile>>,
try_add_import_map: bool,
try_add_node_modules_dir: bool,
) -> ModifiedResult {
assert!(output_dir.is_absolute());
let config_file = match maybe_config_file {
Some(config_file) => config_file,
None => return ModifiedResult::default(),
};
if config_file.specifier.scheme() != "file" {
return ModifiedResult::default();
}
let fmt_config_options = config_file
.to_fmt_config()
.ok()
.map(|config| config.options)
.unwrap_or_default();
let result = update_config_file(
config_file,
&fmt_config_options,
if try_add_import_map {
Some(
ModuleSpecifier::from_file_path(output_dir.join("import_map.json"))
.unwrap(),
)
} else {
None
},
try_add_node_modules_dir,
);
match result {
Ok(modified_result) => modified_result,
Err(err) => {
warn!("Error updating config file. {:#}", err);
ModifiedResult::default()
}
}
}
fn update_config_file(
config_file: &ConfigFile,
fmt_options: &FmtOptionsConfig,
import_map_specifier: Option<ModuleSpecifier>,
try_add_node_modules_dir: bool,
) -> Result<ModifiedResult, AnyError> {
let config_path = specifier_to_file_path(&config_file.specifier)?;
let config_text = std::fs::read_to_string(&config_path)?;
let import_map_specifier =
import_map_specifier.and_then(|import_map_specifier| {
relative_specifier(&config_file.specifier, &import_map_specifier)
});
let modified_result = update_config_text(
&config_text,
fmt_options,
import_map_specifier.as_deref(),
try_add_node_modules_dir,
)?;
if let Some(new_text) = &modified_result.new_text {
std::fs::write(config_path, new_text)?;
}
Ok(modified_result)
}
#[derive(Default)]
struct ModifiedResult {
updated_import_map: bool,
added_node_modules_dir: bool,
new_text: Option<String>,
}
fn update_config_text(
text: &str,
fmt_options: &FmtOptionsConfig,
import_map_specifier: Option<&str>,
try_add_node_modules_dir: bool,
) -> Result<ModifiedResult, AnyError> {
use jsonc_parser::ast::ObjectProp;
use jsonc_parser::ast::Value;
let text = if text.trim().is_empty() { "{}\n" } else { text };
let ast =
jsonc_parser::parse_to_ast(text, &Default::default(), &Default::default())?;
let obj = match ast.value {
Some(Value::Object(obj)) => obj,
_ => bail!("Failed updating config file due to no object."),
};
let mut modified_result = ModifiedResult::default();
let mut text_changes = Vec::new();
let mut should_format = false;
if try_add_node_modules_dir {
// Only modify the nodeModulesDir property if it's not set
// as this allows people to opt-out of this when vendoring
// by specifying `nodeModulesDir: false`
if obj.get("nodeModulesDir").is_none() {
let insert_position = obj.range.end - 1;
text_changes.push(TextChange {
range: insert_position..insert_position,
new_text: r#""nodeModulesDir": "auto""#.to_string(),
});
should_format = true;
modified_result.added_node_modules_dir = true;
}
}
if let Some(import_map_specifier) = import_map_specifier {
let import_map_specifier = import_map_specifier.replace('\"', "\\\"");
match obj.get("importMap") {
Some(ObjectProp {
value: Value::StringLit(lit),
..
}) => {
text_changes.push(TextChange {
range: lit.range.start..lit.range.end,
new_text: format!("\"{}\"", import_map_specifier),
});
modified_result.updated_import_map = true;
}
None => {
// insert it crudely at a position that won't cause any issues
// with comments and format after to make it look nice
let insert_position = obj.range.end - 1;
text_changes.push(TextChange {
range: insert_position..insert_position,
new_text: format!(r#""importMap": "{}""#, import_map_specifier),
});
should_format = true;
modified_result.updated_import_map = true;
}
// shouldn't happen
Some(_) => {
bail!("Failed updating importMap in config file due to invalid type.")
}
}
}
if text_changes.is_empty() {
return Ok(modified_result);
}
let new_text = deno_ast::apply_text_changes(text, text_changes);
modified_result.new_text = if should_format {
format_json(&PathBuf::from("deno.json"), &new_text, fmt_options)
.ok()
.map(|formatted_text| formatted_text.unwrap_or(new_text))
} else {
Some(new_text)
};
Ok(modified_result)
}
fn is_dir_empty(dir_path: &Path) -> Result<bool, AnyError> {
match std::fs::read_dir(dir_path) {
Ok(mut dir) => Ok(dir.next().is_none()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(true),
Err(err) => {
bail!("Error reading directory {}: {}", dir_path.display(), err)
}
}
}
fn resolve_entry_points(
flags: &VendorFlags,
initial_cwd: &Path,
) -> Result<Vec<ModuleSpecifier>, AnyError> {
flags
.specifiers
.iter()
.map(|p| resolve_url_or_path(p, initial_cwd).map_err(|e| e.into()))
.collect::<Result<Vec<_>, _>>()
}
#[cfg(test)]
mod internal_test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn update_config_text_no_existing_props_add_prop() {
let result = update_config_text(
"{\n}",
&Default::default(),
Some("./vendor/import_map.json"),
false,
)
.unwrap();
assert!(result.updated_import_map);
assert!(!result.added_node_modules_dir);
assert_eq!(
result.new_text.unwrap(),
r#"{
"importMap": "./vendor/import_map.json"
}
"#
);
let result = update_config_text(
"{\n}",
&Default::default(),
Some("./vendor/import_map.json"),
true,
)
.unwrap();
assert!(result.updated_import_map);
assert!(result.added_node_modules_dir);
assert_eq!(
result.new_text.unwrap(),
r#"{
"nodeModulesDir": "auto",
"importMap": "./vendor/import_map.json"
}
"#
);
let result =
update_config_text("{\n}", &Default::default(), None, true).unwrap();
assert!(!result.updated_import_map);
assert!(result.added_node_modules_dir);
assert_eq!(
result.new_text.unwrap(),
r#"{
"nodeModulesDir": "auto"
}
"#
);
}
#[test]
fn update_config_text_existing_props_add_prop() {
let result = update_config_text(
r#"{
"tasks": {
"task1": "other"
}
}
"#,
&Default::default(),
Some("./vendor/import_map.json"),
false,
)
.unwrap();
assert_eq!(
result.new_text.unwrap(),
r#"{
"tasks": {
"task1": "other"
},
"importMap": "./vendor/import_map.json"
}
"#
);
// trailing comma
let result = update_config_text(
r#"{
"tasks": {
"task1": "other"
},
}
"#,
&Default::default(),
Some("./vendor/import_map.json"),
false,
)
.unwrap();
assert_eq!(
result.new_text.unwrap(),
r#"{
"tasks": {
"task1": "other"
},
"importMap": "./vendor/import_map.json"
}
"#
);
}
#[test]
fn update_config_text_update_prop() {
let result = update_config_text(
r#"{
"importMap": "./local.json"
}
"#,
&Default::default(),
Some("./vendor/import_map.json"),
false,
)
.unwrap();
assert_eq!(
result.new_text.unwrap(),
r#"{
"importMap": "./vendor/import_map.json"
}
"#
);
}
#[test]
fn no_update_node_modules_dir() {
// will not update if this is already set (even if it's "none")
let result = update_config_text(
r#"{
"nodeModulesDir": "none"
}
"#,
&Default::default(),
None,
true,
)
.unwrap();
assert!(!result.added_node_modules_dir);
assert!(!result.updated_import_map);
assert_eq!(result.new_text, None);
let result = update_config_text(
r#"{
"nodeModulesDir": "auto"
}
"#,
&Default::default(),
None,
true,
)
.unwrap();
assert!(!result.added_node_modules_dir);
assert!(!result.updated_import_map);
assert_eq!(result.new_text, None);
}
}

View file

@ -1,208 +0,0 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::path::PathBuf;
use deno_ast::ModuleSpecifier;
use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
use crate::util::path::is_banned_path_char;
use crate::util::path::path_with_stem_suffix;
use crate::util::path::root_url_to_safe_local_dirname;
/// Partitions the provided specifiers by the non-path and non-query parts of a specifier.
pub fn partition_by_root_specifiers<'a>(
specifiers: impl Iterator<Item = &'a ModuleSpecifier>,
) -> BTreeMap<ModuleSpecifier, Vec<ModuleSpecifier>> {
let mut root_specifiers: BTreeMap<ModuleSpecifier, Vec<ModuleSpecifier>> =
Default::default();
for remote_specifier in specifiers {
let mut root_specifier = remote_specifier.clone();
root_specifier.set_query(None);
root_specifier.set_path("/");
let specifiers = root_specifiers.entry(root_specifier).or_default();
specifiers.push(remote_specifier.clone());
}
root_specifiers
}
/// Gets the directory name to use for the provided root.
pub fn dir_name_for_root(root: &ModuleSpecifier) -> PathBuf {
root_url_to_safe_local_dirname(root)
}
/// Gets a unique file path given the provided file path
/// and the set of existing file paths. Inserts to the
/// set when finding a unique path.
pub fn get_unique_path(
mut path: PathBuf,
unique_set: &mut HashSet<String>,
) -> PathBuf {
let original_path = path.clone();
let mut count = 2;
// case insensitive comparison so the output works on case insensitive file systems
while !unique_set.insert(path.to_string_lossy().to_lowercase()) {
path = path_with_stem_suffix(&original_path, &format!("_{count}"));
count += 1;
}
path
}
pub fn make_url_relative(
root: &ModuleSpecifier,
url: &ModuleSpecifier,
) -> Result<String, AnyError> {
root.make_relative(url).ok_or_else(|| {
anyhow!(
"Error making url ({}) relative to root: {}",
url.to_string(),
root.to_string()
)
})
}
pub fn is_remote_specifier(specifier: &ModuleSpecifier) -> bool {
matches!(specifier.scheme().to_lowercase().as_str(), "http" | "https")
}
pub fn is_remote_specifier_text(text: &str) -> bool {
let text = text.trim_start().to_lowercase();
text.starts_with("http:") || text.starts_with("https:")
}
pub fn sanitize_filepath(text: &str) -> String {
text
.chars()
.map(|c| if is_banned_path_char(c) { '_' } else { c })
.collect()
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn partition_by_root_specifiers_same_sub_folder() {
run_partition_by_root_specifiers_test(
vec![
"https://deno.land/x/mod/A.ts",
"https://deno.land/x/mod/other/A.ts",
],
vec![(
"https://deno.land/",
vec![
"https://deno.land/x/mod/A.ts",
"https://deno.land/x/mod/other/A.ts",
],
)],
);
}
#[test]
fn partition_by_root_specifiers_different_sub_folder() {
run_partition_by_root_specifiers_test(
vec![
"https://deno.land/x/mod/A.ts",
"https://deno.land/x/other/A.ts",
],
vec![(
"https://deno.land/",
vec![
"https://deno.land/x/mod/A.ts",
"https://deno.land/x/other/A.ts",
],
)],
);
}
#[test]
fn partition_by_root_specifiers_different_hosts() {
run_partition_by_root_specifiers_test(
vec![
"https://deno.land/mod/A.ts",
"http://deno.land/B.ts",
"https://deno.land:8080/C.ts",
"https://localhost/mod/A.ts",
"https://other/A.ts",
],
vec![
("http://deno.land/", vec!["http://deno.land/B.ts"]),
("https://deno.land/", vec!["https://deno.land/mod/A.ts"]),
(
"https://deno.land:8080/",
vec!["https://deno.land:8080/C.ts"],
),
("https://localhost/", vec!["https://localhost/mod/A.ts"]),
("https://other/", vec!["https://other/A.ts"]),
],
);
}
fn run_partition_by_root_specifiers_test(
input: Vec<&str>,
expected: Vec<(&str, Vec<&str>)>,
) {
let input = input
.iter()
.map(|s| ModuleSpecifier::parse(s).unwrap())
.collect::<Vec<_>>();
let output = partition_by_root_specifiers(input.iter());
// the assertion is much easier to compare when everything is strings
let output = output
.into_iter()
.map(|(s, vec)| {
(
s.to_string(),
vec.into_iter().map(|s| s.to_string()).collect::<Vec<_>>(),
)
})
.collect::<Vec<_>>();
let expected = expected
.into_iter()
.map(|(s, vec)| {
(
s.to_string(),
vec.into_iter().map(|s| s.to_string()).collect::<Vec<_>>(),
)
})
.collect::<Vec<_>>();
assert_eq!(output, expected);
}
#[test]
fn test_unique_path() {
let mut paths = HashSet::new();
assert_eq!(
get_unique_path(PathBuf::from("/test"), &mut paths),
PathBuf::from("/test")
);
assert_eq!(
get_unique_path(PathBuf::from("/test"), &mut paths),
PathBuf::from("/test_2")
);
assert_eq!(
get_unique_path(PathBuf::from("/test"), &mut paths),
PathBuf::from("/test_3")
);
assert_eq!(
get_unique_path(PathBuf::from("/TEST"), &mut paths),
PathBuf::from("/TEST_4")
);
assert_eq!(
get_unique_path(PathBuf::from("/test.txt"), &mut paths),
PathBuf::from("/test.txt")
);
assert_eq!(
get_unique_path(PathBuf::from("/test.txt"), &mut paths),
PathBuf::from("/test_2.txt")
);
assert_eq!(
get_unique_path(PathBuf::from("/TEST.TXT"), &mut paths),
PathBuf::from("/TEST_3.TXT")
);
}
}

View file

@ -1,357 +0,0 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use deno_ast::ModuleSpecifier;
use deno_config::workspace::WorkspaceResolver;
use deno_core::anyhow::anyhow;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::futures;
use deno_core::futures::FutureExt;
use deno_core::serde_json;
use deno_graph::source::LoadFuture;
use deno_graph::source::LoadResponse;
use deno_graph::source::Loader;
use deno_graph::DefaultModuleAnalyzer;
use deno_graph::GraphKind;
use deno_graph::ModuleGraph;
use import_map::ImportMap;
use crate::args::JsxImportSourceConfig;
use crate::cache::ParsedSourceCache;
use crate::resolver::CliGraphResolver;
use crate::resolver::CliGraphResolverOptions;
use super::build::VendorEnvironment;
// Utilities that help `deno vendor` get tested in memory.
type RemoteFileText = String;
type RemoteFileHeaders = Option<HashMap<String, String>>;
type RemoteFileResult = Result<(RemoteFileText, RemoteFileHeaders), String>;
#[derive(Clone, Default)]
pub struct TestLoader {
files: HashMap<ModuleSpecifier, RemoteFileResult>,
redirects: HashMap<ModuleSpecifier, ModuleSpecifier>,
}
impl TestLoader {
pub fn add(
&mut self,
path_or_specifier: impl AsRef<str>,
text: impl AsRef<str>,
) -> &mut Self {
self.add_result(path_or_specifier, Ok((text.as_ref().to_string(), None)))
}
pub fn add_failure(
&mut self,
path_or_specifier: impl AsRef<str>,
message: impl AsRef<str>,
) -> &mut Self {
self.add_result(path_or_specifier, Err(message.as_ref().to_string()))
}
fn add_result(
&mut self,
path_or_specifier: impl AsRef<str>,
result: RemoteFileResult,
) -> &mut Self {
if path_or_specifier
.as_ref()
.to_lowercase()
.starts_with("http")
{
self.files.insert(
ModuleSpecifier::parse(path_or_specifier.as_ref()).unwrap(),
result,
);
} else {
let path = make_path(path_or_specifier.as_ref());
let specifier = ModuleSpecifier::from_file_path(path).unwrap();
self.files.insert(specifier, result);
}
self
}
pub fn add_with_headers(
&mut self,
specifier: impl AsRef<str>,
text: impl AsRef<str>,
headers: &[(&str, &str)],
) -> &mut Self {
let headers = headers
.iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect();
self.files.insert(
ModuleSpecifier::parse(specifier.as_ref()).unwrap(),
Ok((text.as_ref().to_string(), Some(headers))),
);
self
}
pub fn add_redirect(
&mut self,
from: impl AsRef<str>,
to: impl AsRef<str>,
) -> &mut Self {
self.redirects.insert(
ModuleSpecifier::parse(from.as_ref()).unwrap(),
ModuleSpecifier::parse(to.as_ref()).unwrap(),
);
self
}
}
impl Loader for TestLoader {
fn load(
&self,
specifier: &ModuleSpecifier,
_options: deno_graph::source::LoadOptions,
) -> LoadFuture {
if let Some(redirect) = self.redirects.get(specifier) {
return Box::pin(futures::future::ready(Ok(Some(
LoadResponse::Redirect {
specifier: redirect.clone(),
},
))));
}
let result = self.files.get(specifier).map(|result| match result {
Ok(result) => Ok(LoadResponse::Module {
specifier: specifier.clone(),
content: result.0.clone().into_bytes().into(),
maybe_headers: result.1.clone(),
}),
Err(err) => Err(err),
});
let result = match result {
Some(Ok(result)) => Ok(Some(result)),
Some(Err(err)) => Err(anyhow!("{}", err)),
None if specifier.scheme() == "data" => {
deno_graph::source::load_data_url(specifier)
}
None => Ok(None),
};
Box::pin(futures::future::ready(result))
}
}
#[derive(Default)]
struct TestVendorEnvironment {
directories: RefCell<HashSet<PathBuf>>,
files: RefCell<HashMap<PathBuf, String>>,
}
impl VendorEnvironment for TestVendorEnvironment {
fn create_dir_all(&self, dir_path: &Path) -> Result<(), AnyError> {
let mut directories = self.directories.borrow_mut();
for path in dir_path.ancestors() {
if !directories.insert(path.to_path_buf()) {
break;
}
}
Ok(())
}
fn write_file(&self, file_path: &Path, text: &[u8]) -> Result<(), AnyError> {
let parent = file_path.parent().unwrap();
if !self.directories.borrow().contains(parent) {
bail!("Directory not found: {}", parent.display());
}
self.files.borrow_mut().insert(
file_path.to_path_buf(),
String::from_utf8(text.to_vec()).unwrap(),
);
Ok(())
}
}
pub struct VendorOutput {
pub files: Vec<(String, String)>,
pub import_map: Option<serde_json::Value>,
}
#[derive(Default)]
pub struct VendorTestBuilder {
entry_points: Vec<ModuleSpecifier>,
loader: TestLoader,
maybe_original_import_map: Option<ImportMap>,
environment: TestVendorEnvironment,
jsx_import_source_config: Option<JsxImportSourceConfig>,
}
impl VendorTestBuilder {
pub fn with_default_setup() -> Self {
let mut builder = VendorTestBuilder::default();
builder.add_entry_point("/mod.ts");
builder
}
pub fn resolve_to_url(&self, path: &str) -> ModuleSpecifier {
ModuleSpecifier::from_file_path(make_path(path)).unwrap()
}
pub fn new_import_map(&self, base_path: &str) -> ImportMap {
let base = self.resolve_to_url(base_path);
ImportMap::new(base)
}
pub fn set_original_import_map(
&mut self,
import_map: ImportMap,
) -> &mut Self {
self.maybe_original_import_map = Some(import_map);
self
}
pub fn add_entry_point(&mut self, entry_point: impl AsRef<str>) -> &mut Self {
let entry_point = make_path(entry_point.as_ref());
self
.entry_points
.push(ModuleSpecifier::from_file_path(entry_point).unwrap());
self
}
pub fn set_jsx_import_source_config(
&mut self,
jsx_import_source_config: JsxImportSourceConfig,
) -> &mut Self {
self.jsx_import_source_config = Some(jsx_import_source_config);
self
}
pub async fn build(&mut self) -> Result<VendorOutput, AnyError> {
let output_dir = make_path("/vendor");
let entry_points = self.entry_points.clone();
let loader = self.loader.clone();
let parsed_source_cache = ParsedSourceCache::default();
let resolver = Arc::new(build_resolver(
output_dir.parent().unwrap(),
self.jsx_import_source_config.clone(),
self.maybe_original_import_map.clone(),
));
super::build::build(super::build::BuildInput {
entry_points,
build_graph: {
let resolver = resolver.clone();
move |entry_points| {
async move {
Ok(
build_test_graph(
entry_points,
loader,
resolver.as_graph_resolver(),
&DefaultModuleAnalyzer,
)
.await,
)
}
.boxed_local()
}
},
parsed_source_cache: &parsed_source_cache,
output_dir: &output_dir,
maybe_original_import_map: self.maybe_original_import_map.as_ref(),
maybe_jsx_import_source: self.jsx_import_source_config.as_ref(),
resolver: resolver.as_graph_resolver(),
environment: &self.environment,
})
.await?;
let mut files = self.environment.files.borrow_mut();
let import_map = files.remove(&output_dir.join("import_map.json"));
let mut files = files
.iter()
.map(|(path, text)| (path_to_string(path), text.to_string()))
.collect::<Vec<_>>();
files.sort_by(|a, b| a.0.cmp(&b.0));
Ok(VendorOutput {
import_map: import_map.map(|text| serde_json::from_str(&text).unwrap()),
files,
})
}
pub fn with_loader(&mut self, action: impl Fn(&mut TestLoader)) -> &mut Self {
action(&mut self.loader);
self
}
}
fn build_resolver(
root_dir: &Path,
maybe_jsx_import_source_config: Option<JsxImportSourceConfig>,
maybe_original_import_map: Option<ImportMap>,
) -> CliGraphResolver {
CliGraphResolver::new(CliGraphResolverOptions {
node_resolver: None,
npm_resolver: None,
sloppy_imports_resolver: None,
workspace_resolver: Arc::new(WorkspaceResolver::new_raw(
Arc::new(ModuleSpecifier::from_directory_path(root_dir).unwrap()),
maybe_original_import_map,
Vec::new(),
Vec::new(),
deno_config::workspace::PackageJsonDepResolution::Enabled,
)),
maybe_jsx_import_source_config,
maybe_vendor_dir: None,
bare_node_builtins_enabled: false,
})
}
async fn build_test_graph(
roots: Vec<ModuleSpecifier>,
loader: TestLoader,
resolver: &dyn deno_graph::source::Resolver,
analyzer: &dyn deno_graph::ModuleAnalyzer,
) -> ModuleGraph {
let mut graph = ModuleGraph::new(GraphKind::All);
graph
.build(
roots,
&loader,
deno_graph::BuildOptions {
resolver: Some(resolver),
module_analyzer: analyzer,
..Default::default()
},
)
.await;
graph
}
fn make_path(text: &str) -> PathBuf {
// This should work all in memory. We're waiting on
// https://github.com/servo/rust-url/issues/730 to provide
// a cross platform path here
assert!(text.starts_with('/'));
if cfg!(windows) {
PathBuf::from(format!("C:{}", text.replace('/', "\\")))
} else {
PathBuf::from(text)
}
}
fn path_to_string<P>(path: P) -> String
where
P: AsRef<Path>,
{
let path = path.as_ref();
// inverse of the function above
let path = path.to_string_lossy();
if cfg!(windows) {
path.replace("C:\\", "\\").replace('\\', "/")
} else {
path.to_string()
}
}

View file

@ -1,6 +1,5 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::env::current_dir;
use std::fs::OpenOptions;
use std::io::Error;
use std::io::ErrorKind;
@ -18,7 +17,6 @@ use deno_config::glob::WalkEntry;
use deno_core::anyhow::anyhow;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
pub use deno_core::normalize_path;
use deno_core::unsync::spawn_blocking;
use deno_core::ModuleSpecifier;
use deno_runtime::deno_fs::FileSystem;
@ -255,18 +253,6 @@ fn canonicalize_path_maybe_not_exists_with_custom_fn(
}
}
pub fn resolve_from_cwd(path: &Path) -> Result<PathBuf, AnyError> {
let resolved_path = if path.is_absolute() {
path.to_owned()
} else {
let cwd =
current_dir().context("Failed to get current working directory")?;
cwd.join(path)
};
Ok(normalize_path(resolved_path))
}
/// Collects module specifiers that satisfy the given predicate as a file path, by recursively walking `include`.
/// Specifiers that start with http and https are left intact.
/// Note: This ignores all .git and node_modules folders.
@ -715,30 +701,13 @@ pub fn specifier_from_file_path(
mod tests {
use super::*;
use deno_core::futures;
use deno_core::normalize_path;
use deno_core::parking_lot::Mutex;
use pretty_assertions::assert_eq;
use test_util::PathRef;
use test_util::TempDir;
use tokio::sync::Notify;
#[test]
fn resolve_from_cwd_child() {
let cwd = current_dir().unwrap();
assert_eq!(resolve_from_cwd(Path::new("a")).unwrap(), cwd.join("a"));
}
#[test]
fn resolve_from_cwd_dot() {
let cwd = current_dir().unwrap();
assert_eq!(resolve_from_cwd(Path::new(".")).unwrap(), cwd);
}
#[test]
fn resolve_from_cwd_parent() {
let cwd = current_dir().unwrap();
assert_eq!(resolve_from_cwd(Path::new("a/..")).unwrap(), cwd);
}
#[test]
fn test_normalize_path() {
assert_eq!(normalize_path(Path::new("a/../b")), PathBuf::from("b"));
@ -756,14 +725,6 @@ mod tests {
}
}
#[test]
fn resolve_from_cwd_absolute() {
let expected = Path::new("a");
let cwd = current_dir().unwrap();
let absolute_expected = cwd.join(expected);
assert_eq!(resolve_from_cwd(expected).unwrap(), absolute_expected);
}
#[test]
fn test_collect_specifiers() {
fn create_files(dir_path: &PathRef, files: &[&str]) {

View file

@ -145,34 +145,6 @@ pub fn relative_specifier(
Some(to_percent_decoded_str(&text))
}
/// Gets a path with the specified file stem suffix.
///
/// Ex. `file.ts` with suffix `_2` returns `file_2.ts`
pub fn path_with_stem_suffix(path: &Path, suffix: &str) -> PathBuf {
if let Some(file_name) = path.file_name().map(|f| f.to_string_lossy()) {
if let Some(file_stem) = path.file_stem().map(|f| f.to_string_lossy()) {
if let Some(ext) = path.extension().map(|f| f.to_string_lossy()) {
return if file_stem.to_lowercase().ends_with(".d") {
path.with_file_name(format!(
"{}{}.{}.{}",
&file_stem[..file_stem.len() - ".d".len()],
suffix,
// maintain casing
&file_stem[file_stem.len() - "d".len()..],
ext
))
} else {
path.with_file_name(format!("{file_stem}{suffix}.{ext}"))
};
}
}
path.with_file_name(format!("{file_name}{suffix}"))
} else {
path.with_file_name(suffix)
}
}
#[cfg_attr(windows, allow(dead_code))]
pub fn relative_path(from: &Path, to: &Path) -> Option<PathBuf> {
pathdiff::diff_paths(to, from)
@ -405,46 +377,6 @@ mod test {
}
}
#[test]
fn test_path_with_stem_suffix() {
assert_eq!(
path_with_stem_suffix(&PathBuf::from("/"), "_2"),
PathBuf::from("/_2")
);
assert_eq!(
path_with_stem_suffix(&PathBuf::from("/test"), "_2"),
PathBuf::from("/test_2")
);
assert_eq!(
path_with_stem_suffix(&PathBuf::from("/test.txt"), "_2"),
PathBuf::from("/test_2.txt")
);
assert_eq!(
path_with_stem_suffix(&PathBuf::from("/test/subdir"), "_2"),
PathBuf::from("/test/subdir_2")
);
assert_eq!(
path_with_stem_suffix(&PathBuf::from("/test/subdir.other.txt"), "_2"),
PathBuf::from("/test/subdir.other_2.txt")
);
assert_eq!(
path_with_stem_suffix(&PathBuf::from("/test.d.ts"), "_2"),
PathBuf::from("/test_2.d.ts")
);
assert_eq!(
path_with_stem_suffix(&PathBuf::from("/test.D.TS"), "_2"),
PathBuf::from("/test_2.D.TS")
);
assert_eq!(
path_with_stem_suffix(&PathBuf::from("/test.d.mts"), "_2"),
PathBuf::from("/test_2.d.mts")
);
assert_eq!(
path_with_stem_suffix(&PathBuf::from("/test.d.cts"), "_2"),
PathBuf::from("/test_2.d.cts")
);
}
#[test]
fn test_to_percent_decoded_str() {
let str = to_percent_decoded_str("%F0%9F%A6%95");

View file

@ -68,8 +68,6 @@ mod task;
mod test;
#[path = "upgrade_tests.rs"]
mod upgrade;
#[path = "vendor_tests.rs"]
mod vendor;
#[path = "watcher_tests.rs"]
mod watcher;
#[path = "worker_tests.rs"]

View file

@ -1,751 +0,0 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use deno_core::serde_json;
use deno_core::serde_json::json;
use pretty_assertions::assert_eq;
use std::fmt::Write as _;
use std::path::PathBuf;
use test_util as util;
use test_util::itest;
use test_util::TempDir;
use util::http_server;
use util::new_deno_dir;
use util::TestContextBuilder;
const DEPRECATION_NOTICE: &str = "⚠️ Warning: `deno vendor` is deprecated and will be removed in Deno 2.0.\nAdd `\"vendor\": true` to your `deno.json` or use the `--vendor` flag instead.\n";
#[test]
fn output_dir_exists() {
let t = TempDir::new();
t.write("mod.ts", "");
t.create_dir_all("vendor");
t.write("vendor/mod.ts", "");
let deno = util::deno_cmd()
.current_dir(t.path())
.env("NO_COLOR", "1")
.arg("vendor")
.arg("mod.ts")
.stderr_piped()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(
String::from_utf8_lossy(&output.stderr).trim(),
format!("{}error: Output directory was not empty. Please specify an empty directory or use --force to ignore this error and potentially overwrite its contents.", &DEPRECATION_NOTICE)
);
assert!(!output.status.success());
// ensure it errors when using the `--output` arg too
let deno = util::deno_cmd()
.current_dir(t.path())
.env("NO_COLOR", "1")
.arg("vendor")
.arg("--output")
.arg("vendor")
.arg("mod.ts")
.stderr_piped()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(
String::from_utf8_lossy(&output.stderr).trim(),
format!("{}error: Output directory was not empty. Please specify an empty directory or use --force to ignore this error and potentially overwrite its contents.", &DEPRECATION_NOTICE)
);
assert!(!output.status.success());
// now use `--force`
let status = util::deno_cmd()
.current_dir(t.path())
.env("NO_COLOR", "1")
.arg("vendor")
.arg("mod.ts")
.arg("--force")
.spawn()
.unwrap()
.wait()
.unwrap();
assert!(status.success());
}
#[test]
fn standard_test() {
let _server = http_server();
let t = TempDir::new();
let vendor_dir = t.path().join("vendor2");
t.write(
"my_app.ts",
"import {Logger} from 'http://localhost:4545/vendor/query_reexport.ts?testing'; new Logger().log('outputted');",
);
let deno = util::deno_cmd()
.current_dir(t.path())
.arg("vendor")
.arg("my_app.ts")
.arg("--output")
.arg("vendor2")
.env("NO_COLOR", "1")
.piped_output()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(
String::from_utf8_lossy(&output.stderr).trim(),
format!(
concat!(
"{}",
"Download http://localhost:4545/vendor/query_reexport.ts?testing\n",
"Download http://localhost:4545/vendor/logger.ts?test\n",
"{}",
),
&DEPRECATION_NOTICE,
success_text("2 modules", "vendor2", true),
)
);
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "");
assert!(output.status.success());
assert!(vendor_dir.exists());
assert!(!t.path().join("vendor").exists());
let import_map: serde_json::Value =
serde_json::from_str(&t.read_to_string("vendor2/import_map.json")).unwrap();
assert_eq!(
import_map,
json!({
"imports": {
"http://localhost:4545/vendor/query_reexport.ts?testing": "./localhost_4545/vendor/query_reexport.ts",
"http://localhost:4545/": "./localhost_4545/",
},
"scopes": {
"./localhost_4545/": {
"./localhost_4545/vendor/logger.ts?test": "./localhost_4545/vendor/logger.ts"
}
}
}),
);
// try running the output with `--no-remote`
let deno = util::deno_cmd()
.current_dir(t.path())
.env("NO_COLOR", "1")
.arg("run")
.arg("--no-remote")
.arg("--check")
.arg("--quiet")
.arg("--import-map")
.arg("vendor2/import_map.json")
.arg("my_app.ts")
.piped_output()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(String::from_utf8_lossy(&output.stderr).trim(), "");
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "outputted");
assert!(output.status.success());
}
#[test]
fn import_map_output_dir() {
let _server = http_server();
let t = TempDir::new();
t.write("mod.ts", "");
t.create_dir_all("vendor");
t.write(
"vendor/import_map.json",
// will be ignored
"{ \"imports\": { \"https://localhost:4545/\": \"./localhost/\" }}",
);
t.write(
"deno.json",
"{ \"import_map\": \"./vendor/import_map.json\" }",
);
t.write(
"my_app.ts",
"import {Logger} from 'http://localhost:4545/vendor/logger.ts'; new Logger().log('outputted');",
);
let deno = util::deno_cmd()
.current_dir(t.path())
.env("NO_COLOR", "1")
.arg("vendor")
.arg("--force")
.arg("--import-map")
.arg("vendor/import_map.json")
.arg("my_app.ts")
.piped_output()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(
String::from_utf8_lossy(&output.stderr).trim(),
format!(
concat!(
"{}{}\n",
"Download http://localhost:4545/vendor/logger.ts\n",
"{}\n\n{}",
),
&DEPRECATION_NOTICE,
ignoring_import_map_text(),
vendored_text("1 module", "vendor/"),
success_text_updated_deno_json("vendor/"),
)
);
assert!(output.status.success());
}
#[test]
fn remote_module_test() {
let _server = http_server();
let t = TempDir::new();
let vendor_dir = t.path().join("vendor");
let deno = util::deno_cmd()
.current_dir(t.path())
.env("NO_COLOR", "1")
.arg("vendor")
.arg("http://localhost:4545/vendor/query_reexport.ts")
.piped_output()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(
String::from_utf8_lossy(&output.stderr).trim(),
format!(
concat!(
"{}",
"Download http://localhost:4545/vendor/query_reexport.ts\n",
"Download http://localhost:4545/vendor/logger.ts?test\n",
"{}",
),
&DEPRECATION_NOTICE,
success_text("2 modules", "vendor/", true),
)
);
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "");
assert!(output.status.success());
assert!(vendor_dir.exists());
assert!(vendor_dir
.join("localhost_4545/vendor/query_reexport.ts")
.exists());
assert!(vendor_dir.join("localhost_4545/vendor/logger.ts").exists());
let import_map: serde_json::Value =
serde_json::from_str(&t.read_to_string("vendor/import_map.json")).unwrap();
assert_eq!(
import_map,
json!({
"imports": {
"http://localhost:4545/": "./localhost_4545/",
},
"scopes": {
"./localhost_4545/": {
"./localhost_4545/vendor/logger.ts?test": "./localhost_4545/vendor/logger.ts",
}
}
}),
);
}
#[test]
fn existing_import_map_no_remote() {
let _server = http_server();
let t = TempDir::new();
t.write(
"mod.ts",
"import {Logger} from 'http://localhost:4545/vendor/logger.ts';",
);
let import_map_filename = "imports2.json";
let import_map_text =
r#"{ "imports": { "http://localhost:4545/vendor/": "./logger/" } }"#;
t.write(import_map_filename, import_map_text);
t.create_dir_all("logger");
t.write("logger/logger.ts", "export class Logger {}");
let deno = util::deno_cmd()
.current_dir(t.path())
.env("NO_COLOR", "1")
.arg("vendor")
.arg("mod.ts")
.arg("--import-map")
.arg(import_map_filename)
.stderr_piped()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(
String::from_utf8_lossy(&output.stderr).trim(),
format!(
"{}{}",
&DEPRECATION_NOTICE,
success_text("0 modules", "vendor/", false)
)
);
assert!(output.status.success());
// it should not have found any remote dependencies because
// the provided import map mapped it to a local directory
assert_eq!(t.read_to_string(import_map_filename), import_map_text);
}
#[test]
fn existing_import_map_mixed_with_remote() {
let _server = http_server();
let deno_dir = new_deno_dir();
let t = TempDir::new();
t.write(
"mod.ts",
"import {Logger} from 'http://localhost:4545/vendor/logger.ts';",
);
let status = util::deno_cmd_with_deno_dir(&deno_dir)
.current_dir(t.path())
.arg("vendor")
.arg("mod.ts")
.spawn()
.unwrap()
.wait()
.unwrap();
assert!(status.success());
assert_eq!(
t.read_to_string("vendor/import_map.json"),
r#"{
"imports": {
"http://localhost:4545/": "./localhost_4545/"
}
}
"#,
);
// make the import map specific to support vendoring mod.ts in the next step
t.write(
"vendor/import_map.json",
r#"{
"imports": {
"http://localhost:4545/vendor/logger.ts": "./localhost_4545/vendor/logger.ts"
}
}
"#,
);
t.write(
"mod.ts",
concat!(
"import {Logger} from 'http://localhost:4545/vendor/logger.ts';\n",
"import {Logger as OtherLogger} from 'http://localhost:4545/vendor/mod.ts';\n",
),
);
// now vendor with the existing import map in a separate vendor directory
let deno = util::deno_cmd_with_deno_dir(&deno_dir)
.env("NO_COLOR", "1")
.current_dir(t.path())
.arg("vendor")
.arg("mod.ts")
.arg("--import-map")
.arg("vendor/import_map.json")
.arg("--output")
.arg("vendor2")
.piped_output()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(
String::from_utf8_lossy(&output.stderr).trim(),
format!(
"{}Download http://localhost:4545/vendor/mod.ts\n{}",
&DEPRECATION_NOTICE,
success_text("1 module", "vendor2", true),
)
);
assert!(output.status.success());
// tricky scenario here where the output directory now contains a mapping
// back to the previous vendor location
assert_eq!(
t.read_to_string("vendor2/import_map.json"),
r#"{
"imports": {
"http://localhost:4545/vendor/logger.ts": "../vendor/localhost_4545/vendor/logger.ts",
"http://localhost:4545/": "./localhost_4545/"
},
"scopes": {
"./localhost_4545/": {
"./localhost_4545/vendor/logger.ts": "../vendor/localhost_4545/vendor/logger.ts"
}
}
}
"#,
);
// ensure it runs
let status = util::deno_cmd()
.current_dir(t.path())
.arg("run")
.arg("--check")
.arg("--no-remote")
.arg("--import-map")
.arg("vendor2/import_map.json")
.arg("mod.ts")
.spawn()
.unwrap()
.wait()
.unwrap();
assert!(status.success());
}
#[test]
fn dynamic_import() {
let _server = http_server();
let t = TempDir::new();
t.write(
"mod.ts",
"import {Logger} from 'http://localhost:4545/vendor/dynamic.ts'; new Logger().log('outputted');",
);
let status = util::deno_cmd()
.current_dir(t.path())
.arg("vendor")
.arg("mod.ts")
.spawn()
.unwrap()
.wait()
.unwrap();
assert!(status.success());
let import_map: serde_json::Value =
serde_json::from_str(&t.read_to_string("vendor/import_map.json")).unwrap();
assert_eq!(
import_map,
json!({
"imports": {
"http://localhost:4545/": "./localhost_4545/",
}
}),
);
// try running the output with `--no-remote`
let deno = util::deno_cmd()
.current_dir(t.path())
.env("NO_COLOR", "1")
.arg("run")
.arg("--allow-read=.")
.arg("--no-remote")
.arg("--check")
.arg("--quiet")
.arg("--import-map")
.arg("vendor/import_map.json")
.arg("mod.ts")
.piped_output()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(String::from_utf8_lossy(&output.stderr).trim(), "");
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "outputted");
assert!(output.status.success());
}
#[test]
fn dynamic_non_analyzable_import() {
let _server = http_server();
let t = TempDir::new();
t.write(
"mod.ts",
"import {Logger} from 'http://localhost:4545/vendor/dynamic_non_analyzable.ts'; new Logger().log('outputted');",
);
let deno = util::deno_cmd()
.current_dir(t.path())
.env("NO_COLOR", "1")
.arg("vendor")
.arg("--reload")
.arg("mod.ts")
.piped_output()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
// todo(https://github.com/denoland/deno_graph/issues/138): it should warn about
// how it couldn't analyze the dynamic import
assert_eq!(
String::from_utf8_lossy(&output.stderr).trim(),
format!(
"{}Download http://localhost:4545/vendor/dynamic_non_analyzable.ts\n{}",
&DEPRECATION_NOTICE,
success_text("1 module", "vendor/", true),
)
);
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "");
assert!(output.status.success());
}
itest!(dynamic_non_existent {
args: "vendor http://localhost:4545/vendor/dynamic_non_existent.ts",
temp_cwd: true,
exit_code: 0,
http_server: true,
output: "vendor/dynamic_non_existent.ts.out",
});
#[test]
fn update_existing_config_test() {
let _server = http_server();
let t = TempDir::new();
t.write(
"my_app.ts",
"import {Logger} from 'http://localhost:4545/vendor/logger.ts'; new Logger().log('outputted');",
);
t.write("deno.json", "{\n}");
let deno = util::deno_cmd()
.current_dir(t.path())
.arg("vendor")
.arg("my_app.ts")
.arg("--output")
.arg("vendor2")
.env("NO_COLOR", "1")
.piped_output()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(
String::from_utf8_lossy(&output.stderr).trim(),
format!(
"{}Download http://localhost:4545/vendor/logger.ts\n{}\n\n{}",
&DEPRECATION_NOTICE,
vendored_text("1 module", "vendor2"),
success_text_updated_deno_json("vendor2",)
)
);
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "");
assert!(output.status.success());
// try running the output with `--no-remote` and not specifying a `--vendor`
let deno = util::deno_cmd()
.current_dir(t.path())
.env("NO_COLOR", "1")
.arg("run")
.arg("--no-remote")
.arg("--check")
.arg("--quiet")
.arg("my_app.ts")
.piped_output()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(String::from_utf8_lossy(&output.stderr).trim(), "");
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "outputted");
assert!(output.status.success());
}
#[test]
fn update_existing_empty_config_test() {
let _server = http_server();
let t = TempDir::new();
t.write(
"my_app.ts",
"import {Logger} from 'http://localhost:4545/vendor/logger.ts'; new Logger().log('outputted');",
);
t.write("deno.json", "");
let deno = util::deno_cmd()
.current_dir(t.path())
.arg("vendor")
.arg("my_app.ts")
.arg("--output")
.arg("vendor2")
.env("NO_COLOR", "1")
.piped_output()
.spawn()
.unwrap();
let output = deno.wait_with_output().unwrap();
assert_eq!(
String::from_utf8_lossy(&output.stderr).trim(),
format!(
"⚠️ Warning: `deno vendor` is deprecated and will be removed in Deno 2.0.
Add `\"vendor\": true` to your `deno.json` or use the `--vendor` flag instead.
Download http://localhost:4545/vendor/logger.ts\n{}\n\n{}",
vendored_text("1 module", "vendor2"),
success_text_updated_deno_json("vendor2",)
)
);
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "");
assert!(output.status.success());
}
// TODO(2.0): decide if this test should be updated or removed
#[test]
#[ignore]
fn vendor_npm_node_specifiers() {
let context = TestContextBuilder::for_npm().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write(
"my_app.ts",
concat!(
"import { path, getValue, setValue } from 'http://localhost:4545/vendor/npm_and_node_specifier.ts';\n",
"setValue(5);\n",
"console.log(path.isAbsolute(Deno.cwd()), getValue());",
),
);
temp_dir.write("deno.json", "{}");
let output = context.new_command().args("vendor my_app.ts").run();
output.assert_matches_text(format!(
concat!(
"⚠️ Warning: `deno vendor` is deprecated and will be removed in Deno 2.0.\n",
"Add `\"vendor\": true` to your `deno.json` or use the `--vendor` flag instead.\n",
"Download http://localhost:4545/vendor/npm_and_node_specifier.ts\n",
"Download http://localhost:4260/@denotest/esm-basic\n",
"Download http://localhost:4260/@denotest/esm-basic/1.0.0.tgz\n",
"{}\n",
"Initialize @denotest/esm-basic@1.0.0\n",
"{}\n\n",
"{}\n",
),
vendored_text("1 module", "vendor/"),
vendored_npm_package_text("1 npm package"),
success_text_updated_deno_json("vendor/")
));
let output = context.new_command().args("run -A -q my_app.ts").run();
output.assert_matches_text("true 5\n");
assert!(temp_dir.path().join("node_modules").exists());
assert!(temp_dir.path().join("deno.lock").exists());
// now try re-vendoring with a lockfile
let output = context.new_command().args("vendor --force my_app.ts").run();
output.assert_matches_text(format!(
"{}{}\n{}\n\n{}\n",
&DEPRECATION_NOTICE,
ignoring_import_map_text(),
vendored_text("1 module", "vendor/"),
success_text_updated_deno_json("vendor/"),
));
// delete the node_modules folder
temp_dir.remove_dir_all("node_modules");
// vendor with --node-modules-dir=false
let output = context
.new_command()
.args("vendor --node-modules-dir=false --force my_app.ts")
.run();
output.assert_matches_text(format!(
"{}{}\n{}\n\n{}\n",
&DEPRECATION_NOTICE,
ignoring_import_map_text(),
vendored_text("1 module", "vendor/"),
success_text_updated_deno_json("vendor/")
));
assert!(!temp_dir.path().join("node_modules").exists());
// delete the deno.json
temp_dir.remove_file("deno.json");
// vendor with --node-modules-dir
let output = context
.new_command()
.args("vendor --node-modules-dir --force my_app.ts")
.run();
output.assert_matches_text(format!(
"{}Initialize @denotest/esm-basic@1.0.0\n{}\n\n{}\n",
&DEPRECATION_NOTICE,
vendored_text("1 module", "vendor/"),
use_import_map_text("vendor/")
));
}
#[test]
fn vendor_only_npm_specifiers() {
let context = TestContextBuilder::for_npm().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write(
"my_app.ts",
concat!(
"import { getValue, setValue } from 'npm:@denotest/esm-basic';\n",
"setValue(5);\n",
"console.log(path.isAbsolute(Deno.cwd()), getValue());",
),
);
temp_dir.write("deno.json", "{}");
let output = context.new_command().args("vendor my_app.ts").run();
output.assert_matches_text(format!(
concat!(
"⚠️ Warning: `deno vendor` is deprecated and will be removed in Deno 2.0.\n",
"Add `\"vendor\": true` to your `deno.json` or use the `--vendor` flag instead.\n",
"Download http://localhost:4260/@denotest/esm-basic\n",
"Download http://localhost:4260/@denotest/esm-basic/1.0.0.tgz\n",
"{}\n",
"Initialize @denotest/esm-basic@1.0.0\n",
"{}\n",
),
vendored_text("0 modules", "vendor/"),
vendored_npm_package_text("1 npm package"),
));
}
fn success_text(module_count: &str, dir: &str, has_import_map: bool) -> String {
let mut text = format!("Vendored {module_count} into {dir} directory.");
if has_import_map {
write!(text, "\n\n{}", use_import_map_text(dir)).unwrap();
}
text
}
fn use_import_map_text(dir: &str) -> String {
format!(
concat!(
"To use vendored modules, specify the `--import-map {}import_map.json` flag when ",
r#"invoking Deno subcommands or add an `"importMap": "<path_to_vendored_import_map>"` "#,
"entry to a deno.json file.",
),
if dir != "vendor/" {
format!("{}{}", dir.trim_end_matches('/'), if cfg!(windows) { '\\' } else {'/'})
} else {
dir.to_string()
}
)
}
fn vendored_text(module_count: &str, dir: &str) -> String {
format!("Vendored {} into {} directory.", module_count, dir)
}
fn vendored_npm_package_text(package_count: &str) -> String {
format!(
concat!(
"Vendored {} into node_modules directory. Set `nodeModulesDir: false` ",
"in the Deno configuration file to disable vendoring npm packages in the future.",
),
package_count
)
}
fn success_text_updated_deno_json(dir: &str) -> String {
format!(
concat!(
"Updated your local Deno configuration file with a reference to the ",
"new vendored import map at {}import_map.json. Invoking Deno subcommands will ",
"now automatically resolve using the vendored modules. You may override ",
"this by providing the `--import-map <other-import-map>` flag or by ",
"manually editing your Deno configuration file.",
),
if dir != "vendor/" {
format!(
"{}{}",
dir.trim_end_matches('/'),
if cfg!(windows) { '\\' } else { '/' }
)
} else {
dir.to_string()
}
)
}
fn ignoring_import_map_text() -> String {
format!(
concat!(
"Ignoring import map. Specifying an import map file ({}) in the deno ",
"vendor output directory is not supported. If you wish to use an ",
"import map while vendoring, please specify one located outside this ",
"directory.",
),
PathBuf::from("vendor").join("import_map.json").display(),
)
}

View file

@ -0,0 +1,13 @@
{
"steps": [
{
"args": "vendor",
"output": "vendor.out",
"exitCode": 1
},
{
"args": "vendor --help",
"output": "vendor_help.out"
}
]
}

3
tests/specs/vendor/removed/vendor.out vendored Normal file
View file

@ -0,0 +1,3 @@
error: ⚠️ `deno vendor` was removed in Deno 2.
See the Deno 1.x to 2.x Migration Guide for migration instructions: https://docs.deno.com/runtime/manual/advanced/migrate_deprecations

View file

@ -0,0 +1,10 @@
⚠️ `deno vendor` was removed in Deno 2.
See the Deno 1.x to 2.x Migration Guide for migration instructions: https://docs.deno.com/runtime/manual/advanced/migrate_deprecations
Usage: deno vendor [OPTIONS]
Options:
-q, --quiet Suppress diagnostic output
--unstable Enable all unstable features and APIs. Instead of using this flag, consider enabling individual unstable features
To view the list of individual unstable feature flags, run this command again with --help=unstable

View file

@ -1,3 +0,0 @@
const { Logger } = await import("./logger.ts");
export { Logger };

View file

@ -1,4 +0,0 @@
const value = (() => "./logger.ts")();
const { Logger } = await import(value);
export { Logger };

View file

@ -1,11 +0,0 @@
// this should still vendor
// deno-lint-ignore no-constant-condition
if (false) {
await import("./non-existent.js");
}
export class Logger {
log(text: string) {
console.log(text);
}
}

View file

@ -1,9 +0,0 @@
⚠️ Warning: `deno vendor` is deprecated and will be removed in Deno 2.0.
Add `"vendor": true` to your `deno.json` or use the `--vendor` flag instead.
Download http://localhost:4545/vendor/dynamic_non_existent.ts
Download http://localhost:4545/vendor/non-existent.js
Ignoring: Dynamic import not found "http://localhost:4545/vendor/non-existent.js".
at http://localhost:4545/vendor/dynamic_non_existent.ts:4:16
Vendored 1 module into vendor/ directory.
To use vendored modules, specify the `--import-map vendor/import_map.json` flag when invoking Deno subcommands or add an `"importMap": "<path_to_vendored_import_map>"` entry to a deno.json file.

View file

@ -1,5 +0,0 @@
export class Logger {
log(text: string) {
console.log(text);
}
}

View file

@ -1 +0,0 @@
export * from "./logger.ts";

View file

@ -1,2 +0,0 @@
export { default as path } from "node:path";
export { getValue, setValue } from "npm:@denotest/esm-basic";

View file

@ -1 +0,0 @@
export * from "./logger.ts?test";