summaryrefslogtreecommitdiff
path: root/cli/tools/vendor/build.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools/vendor/build.rs')
-rw-r--r--cli/tools/vendor/build.rs577
1 files changed, 577 insertions, 0 deletions
diff --git a/cli/tools/vendor/build.rs b/cli/tools/vendor/build.rs
new file mode 100644
index 000000000..58f351dd8
--- /dev/null
+++ b/cli/tools/vendor/build.rs
@@ -0,0 +1,577 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use std::path::Path;
+
+use deno_core::error::AnyError;
+use deno_graph::Module;
+use deno_graph::ModuleGraph;
+use deno_graph::ModuleKind;
+
+use super::analyze::has_default_export;
+use super::import_map::build_import_map;
+use super::mappings::Mappings;
+use super::mappings::ProxiedModule;
+use super::specifiers::is_remote_specifier;
+
+/// Allows substituting the environment for testing purposes.
+pub trait VendorEnvironment {
+ fn create_dir_all(&self, dir_path: &Path) -> Result<(), AnyError>;
+ fn write_file(&self, file_path: &Path, text: &str) -> Result<(), AnyError>;
+}
+
+pub struct RealVendorEnvironment;
+
+impl VendorEnvironment for RealVendorEnvironment {
+ fn create_dir_all(&self, dir_path: &Path) -> Result<(), AnyError> {
+ Ok(std::fs::create_dir_all(dir_path)?)
+ }
+
+ fn write_file(&self, file_path: &Path, text: &str) -> Result<(), AnyError> {
+ Ok(std::fs::write(file_path, text)?)
+ }
+}
+
+/// Vendors remote modules and returns how many were vendored.
+pub fn build(
+ graph: &ModuleGraph,
+ output_dir: &Path,
+ environment: &impl VendorEnvironment,
+) -> Result<usize, AnyError> {
+ assert!(output_dir.is_absolute());
+ let all_modules = graph.modules();
+ let remote_modules = all_modules
+ .iter()
+ .filter(|m| is_remote_specifier(&m.specifier))
+ .copied()
+ .collect::<Vec<_>>();
+ let mappings =
+ Mappings::from_remote_modules(graph, &remote_modules, output_dir)?;
+
+ // write out all the files
+ for module in &remote_modules {
+ let source = match &module.maybe_source {
+ Some(source) => source,
+ None => continue,
+ };
+ let local_path = mappings
+ .proxied_path(&module.specifier)
+ .unwrap_or_else(|| mappings.local_path(&module.specifier));
+ if !matches!(module.kind, ModuleKind::Esm | ModuleKind::Asserted) {
+ log::warn!(
+ "Unsupported module kind {:?} for {}",
+ module.kind,
+ module.specifier
+ );
+ continue;
+ }
+ environment.create_dir_all(local_path.parent().unwrap())?;
+ environment.write_file(&local_path, source)?;
+ }
+
+ // write out the proxies
+ for (specifier, proxied_module) in mappings.proxied_modules() {
+ let proxy_path = mappings.local_path(specifier);
+ let module = graph.get(specifier).unwrap();
+ let text = build_proxy_module_source(module, proxied_module);
+
+ environment.write_file(&proxy_path, &text)?;
+ }
+
+ // create the import map
+ if !mappings.base_specifiers().is_empty() {
+ let import_map_text = build_import_map(graph, &all_modules, &mappings);
+ environment
+ .write_file(&output_dir.join("import_map.json"), &import_map_text)?;
+ }
+
+ Ok(remote_modules.len())
+}
+
+fn build_proxy_module_source(
+ module: &Module,
+ proxied_module: &ProxiedModule,
+) -> String {
+ let mut text = format!(
+ "// @deno-types=\"{}\"\n",
+ proxied_module.declaration_specifier
+ );
+ let relative_specifier = format!(
+ "./{}",
+ proxied_module
+ .output_path
+ .file_name()
+ .unwrap()
+ .to_string_lossy()
+ );
+
+ // for simplicity, always include the `export *` statement as it won't error
+ // even when the module does not contain a named export
+ text.push_str(&format!("export * from \"{}\";\n", relative_specifier));
+
+ // add a default export if one exists in the module
+ if let Some(parsed_source) = module.maybe_parsed_source.as_ref() {
+ if has_default_export(parsed_source) {
+ text.push_str(&format!(
+ "export {{ default }} from \"{}\";\n",
+ relative_specifier
+ ));
+ }
+ }
+
+ text
+}
+
+#[cfg(test)]
+mod test {
+ use crate::tools::vendor::test::VendorTestBuilder;
+ use deno_core::serde_json::json;
+ use pretty_assertions::assert_eq;
+
+ #[tokio::test]
+ async fn no_remote_modules() {
+ let mut builder = VendorTestBuilder::with_default_setup();
+ let output = builder
+ .with_loader(|loader| {
+ loader.add("/mod.ts", "");
+ })
+ .build()
+ .await
+ .unwrap();
+
+ assert_eq!(output.import_map, None,);
+ assert_eq!(output.files, vec![],);
+ }
+
+ #[tokio::test]
+ async fn local_specifiers_to_remote() {
+ let mut builder = VendorTestBuilder::with_default_setup();
+ let output = builder
+ .with_loader(|loader| {
+ loader
+ .add(
+ "/mod.ts",
+ concat!(
+ r#"import "https://localhost/mod.ts";"#,
+ r#"import "https://localhost/other.ts?test";"#,
+ r#"import "https://localhost/redirect.ts";"#,
+ ),
+ )
+ .add("https://localhost/mod.ts", "export class Mod {}")
+ .add("https://localhost/other.ts?test", "export class Other {}")
+ .add_redirect(
+ "https://localhost/redirect.ts",
+ "https://localhost/mod.ts",
+ );
+ })
+ .build()
+ .await
+ .unwrap();
+
+ assert_eq!(
+ output.import_map,
+ Some(json!({
+ "imports": {
+ "https://localhost/": "./localhost/",
+ "https://localhost/other.ts?test": "./localhost/other.ts",
+ "https://localhost/redirect.ts": "./localhost/mod.ts",
+ }
+ }))
+ );
+ assert_eq!(
+ output.files,
+ to_file_vec(&[
+ ("/vendor/localhost/mod.ts", "export class Mod {}"),
+ ("/vendor/localhost/other.ts", "export class Other {}"),
+ ]),
+ );
+ }
+
+ #[tokio::test]
+ async fn remote_specifiers() {
+ let mut builder = VendorTestBuilder::with_default_setup();
+ let output = builder
+ .with_loader(|loader| {
+ loader
+ .add(
+ "/mod.ts",
+ concat!(
+ r#"import "https://localhost/mod.ts";"#,
+ r#"import "https://other/mod.ts";"#,
+ ),
+ )
+ .add(
+ "https://localhost/mod.ts",
+ concat!(
+ "export * from './other.ts';",
+ "export * from './redirect.ts';",
+ "export * from '/absolute.ts';",
+ ),
+ )
+ .add("https://localhost/other.ts", "export class Other {}")
+ .add_redirect(
+ "https://localhost/redirect.ts",
+ "https://localhost/other.ts",
+ )
+ .add("https://localhost/absolute.ts", "export class Absolute {}")
+ .add("https://other/mod.ts", "export * from './sub/mod.ts';")
+ .add(
+ "https://other/sub/mod.ts",
+ concat!(
+ "export * from '../sub2/mod.ts';",
+ "export * from '../sub2/other?asdf';",
+ // reference a path on a different origin
+ "export * from 'https://localhost/other.ts';",
+ "export * from 'https://localhost/redirect.ts';",
+ ),
+ )
+ .add("https://other/sub2/mod.ts", "export class Mod {}")
+ .add_with_headers(
+ "https://other/sub2/other?asdf",
+ "export class Other {}",
+ &[("content-type", "application/javascript")],
+ );
+ })
+ .build()
+ .await
+ .unwrap();
+
+ assert_eq!(
+ output.import_map,
+ Some(json!({
+ "imports": {
+ "https://localhost/": "./localhost/",
+ "https://localhost/redirect.ts": "./localhost/other.ts",
+ "https://other/": "./other/"
+ },
+ "scopes": {
+ "./localhost/": {
+ "./localhost/redirect.ts": "./localhost/other.ts",
+ "/absolute.ts": "./localhost/absolute.ts",
+ },
+ "./other/": {
+ "./other/sub2/other?asdf": "./other/sub2/other.js"
+ }
+ }
+ }))
+ );
+ assert_eq!(
+ output.files,
+ to_file_vec(&[
+ ("/vendor/localhost/absolute.ts", "export class Absolute {}"),
+ (
+ "/vendor/localhost/mod.ts",
+ concat!(
+ "export * from './other.ts';",
+ "export * from './redirect.ts';",
+ "export * from '/absolute.ts';",
+ )
+ ),
+ ("/vendor/localhost/other.ts", "export class Other {}"),
+ ("/vendor/other/mod.ts", "export * from './sub/mod.ts';"),
+ (
+ "/vendor/other/sub/mod.ts",
+ concat!(
+ "export * from '../sub2/mod.ts';",
+ "export * from '../sub2/other?asdf';",
+ "export * from 'https://localhost/other.ts';",
+ "export * from 'https://localhost/redirect.ts';",
+ )
+ ),
+ ("/vendor/other/sub2/mod.ts", "export class Mod {}"),
+ ("/vendor/other/sub2/other.js", "export class Other {}"),
+ ]),
+ );
+ }
+
+ #[tokio::test]
+ async fn same_target_filename_specifiers() {
+ let mut builder = VendorTestBuilder::with_default_setup();
+ let output = builder
+ .with_loader(|loader| {
+ loader
+ .add(
+ "/mod.ts",
+ concat!(
+ r#"import "https://localhost/MOD.TS";"#,
+ r#"import "https://localhost/mod.TS";"#,
+ r#"import "https://localhost/mod.ts";"#,
+ r#"import "https://localhost/mod.ts?test";"#,
+ r#"import "https://localhost/CAPS.TS";"#,
+ ),
+ )
+ .add("https://localhost/MOD.TS", "export class Mod {}")
+ .add("https://localhost/mod.TS", "export class Mod2 {}")
+ .add("https://localhost/mod.ts", "export class Mod3 {}")
+ .add("https://localhost/mod.ts?test", "export class Mod4 {}")
+ .add("https://localhost/CAPS.TS", "export class Caps {}");
+ })
+ .build()
+ .await
+ .unwrap();
+
+ assert_eq!(
+ output.import_map,
+ Some(json!({
+ "imports": {
+ "https://localhost/": "./localhost/",
+ "https://localhost/mod.TS": "./localhost/mod_2.TS",
+ "https://localhost/mod.ts": "./localhost/mod_3.ts",
+ "https://localhost/mod.ts?test": "./localhost/mod_4.ts",
+ }
+ }))
+ );
+ assert_eq!(
+ output.files,
+ to_file_vec(&[
+ ("/vendor/localhost/CAPS.TS", "export class Caps {}"),
+ ("/vendor/localhost/MOD.TS", "export class Mod {}"),
+ ("/vendor/localhost/mod_2.TS", "export class Mod2 {}"),
+ ("/vendor/localhost/mod_3.ts", "export class Mod3 {}"),
+ ("/vendor/localhost/mod_4.ts", "export class Mod4 {}"),
+ ]),
+ );
+ }
+
+ #[tokio::test]
+ async fn multiple_entrypoints() {
+ let mut builder = VendorTestBuilder::with_default_setup();
+ let output = builder
+ .add_entry_point("/test.deps.ts")
+ .with_loader(|loader| {
+ loader
+ .add("/mod.ts", r#"import "https://localhost/mod.ts";"#)
+ .add(
+ "/test.deps.ts",
+ r#"export * from "https://localhost/test.ts";"#,
+ )
+ .add("https://localhost/mod.ts", "export class Mod {}")
+ .add("https://localhost/test.ts", "export class Test {}");
+ })
+ .build()
+ .await
+ .unwrap();
+
+ assert_eq!(
+ output.import_map,
+ Some(json!({
+ "imports": {
+ "https://localhost/": "./localhost/",
+ }
+ }))
+ );
+ assert_eq!(
+ output.files,
+ to_file_vec(&[
+ ("/vendor/localhost/mod.ts", "export class Mod {}"),
+ ("/vendor/localhost/test.ts", "export class Test {}"),
+ ]),
+ );
+ }
+
+ #[tokio::test]
+ async fn json_module() {
+ let mut builder = VendorTestBuilder::with_default_setup();
+ let output = builder
+ .with_loader(|loader| {
+ loader
+ .add(
+ "/mod.ts",
+ r#"import data from "https://localhost/data.json" assert { type: "json" };"#,
+ )
+ .add("https://localhost/data.json", "{}");
+ })
+ .build()
+ .await
+ .unwrap();
+
+ assert_eq!(
+ output.import_map,
+ Some(json!({
+ "imports": {
+ "https://localhost/": "./localhost/"
+ }
+ }))
+ );
+ assert_eq!(
+ output.files,
+ to_file_vec(&[("/vendor/localhost/data.json", "{}"),]),
+ );
+ }
+
+ #[tokio::test]
+ async fn data_urls() {
+ let mut builder = VendorTestBuilder::with_default_setup();
+
+ let mod_file_text = r#"import * as b from "data:application/typescript,export%20*%20from%20%22https://localhost/mod.ts%22;";"#;
+
+ let output = builder
+ .with_loader(|loader| {
+ loader
+ .add("/mod.ts", &mod_file_text)
+ .add("https://localhost/mod.ts", "export class Example {}");
+ })
+ .build()
+ .await
+ .unwrap();
+
+ assert_eq!(
+ output.import_map,
+ Some(json!({
+ "imports": {
+ "https://localhost/": "./localhost/"
+ }
+ }))
+ );
+ assert_eq!(
+ output.files,
+ to_file_vec(&[("/vendor/localhost/mod.ts", "export class Example {}"),]),
+ );
+ }
+
+ #[tokio::test]
+ async fn x_typescript_types_no_default() {
+ let mut builder = VendorTestBuilder::with_default_setup();
+ let output = builder
+ .with_loader(|loader| {
+ loader
+ .add("/mod.ts", r#"import "https://localhost/mod.js";"#)
+ .add_with_headers(
+ "https://localhost/mod.js",
+ "export class Mod {}",
+ &[("x-typescript-types", "https://localhost/mod.d.ts")],
+ )
+ .add("https://localhost/mod.d.ts", "export class Mod {}");
+ })
+ .build()
+ .await
+ .unwrap();
+
+ assert_eq!(
+ output.import_map,
+ Some(json!({
+ "imports": {
+ "https://localhost/": "./localhost/"
+ }
+ }))
+ );
+ assert_eq!(
+ output.files,
+ to_file_vec(&[
+ ("/vendor/localhost/mod.d.ts", "export class Mod {}"),
+ (
+ "/vendor/localhost/mod.js",
+ concat!(
+ "// @deno-types=\"https://localhost/mod.d.ts\"\n",
+ "export * from \"./mod.proxied.js\";\n"
+ )
+ ),
+ ("/vendor/localhost/mod.proxied.js", "export class Mod {}"),
+ ]),
+ );
+ }
+
+ #[tokio::test]
+ async fn x_typescript_types_default_export() {
+ let mut builder = VendorTestBuilder::with_default_setup();
+ let output = builder
+ .with_loader(|loader| {
+ loader
+ .add("/mod.ts", r#"import "https://localhost/mod.js";"#)
+ .add_with_headers(
+ "https://localhost/mod.js",
+ "export default class Mod {}",
+ &[("x-typescript-types", "https://localhost/mod.d.ts")],
+ )
+ .add("https://localhost/mod.d.ts", "export default class Mod {}");
+ })
+ .build()
+ .await
+ .unwrap();
+
+ assert_eq!(
+ output.import_map,
+ Some(json!({
+ "imports": {
+ "https://localhost/": "./localhost/"
+ }
+ }))
+ );
+ assert_eq!(
+ output.files,
+ to_file_vec(&[
+ ("/vendor/localhost/mod.d.ts", "export default class Mod {}"),
+ (
+ "/vendor/localhost/mod.js",
+ concat!(
+ "// @deno-types=\"https://localhost/mod.d.ts\"\n",
+ "export * from \"./mod.proxied.js\";\n",
+ "export { default } from \"./mod.proxied.js\";\n",
+ )
+ ),
+ (
+ "/vendor/localhost/mod.proxied.js",
+ "export default class Mod {}"
+ ),
+ ]),
+ );
+ }
+
+ #[tokio::test]
+ async fn subdir() {
+ let mut builder = VendorTestBuilder::with_default_setup();
+ let output = builder
+ .with_loader(|loader| {
+ loader
+ .add(
+ "/mod.ts",
+ r#"import "http://localhost:4545/sub/logger/mod.ts?testing";"#,
+ )
+ .add(
+ "http://localhost:4545/sub/logger/mod.ts?testing",
+ "export * from './logger.ts?test';",
+ )
+ .add(
+ "http://localhost:4545/sub/logger/logger.ts?test",
+ "export class Logger {}",
+ );
+ })
+ .build()
+ .await
+ .unwrap();
+
+ assert_eq!(
+ output.import_map,
+ Some(json!({
+ "imports": {
+ "http://localhost:4545/": "./localhost_4545/",
+ "http://localhost:4545/sub/logger/mod.ts?testing": "./localhost_4545/sub/logger/mod.ts",
+ },
+ "scopes": {
+ "./localhost_4545/": {
+ "./localhost_4545/sub/logger/logger.ts?test": "./localhost_4545/sub/logger/logger.ts"
+ }
+ }
+ }))
+ );
+ assert_eq!(
+ output.files,
+ to_file_vec(&[
+ (
+ "/vendor/localhost_4545/sub/logger/logger.ts",
+ "export class Logger {}",
+ ),
+ (
+ "/vendor/localhost_4545/sub/logger/mod.ts",
+ "export * from './logger.ts?test';"
+ ),
+ ]),
+ );
+ }
+
+ fn to_file_vec(items: &[(&str, &str)]) -> Vec<(String, String)> {
+ items
+ .iter()
+ .map(|(f, t)| (f.to_string(), t.to_string()))
+ .collect()
+ }
+}