// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use crate::deno_dir; use crate::file_fetcher::SourceFileFetcher; use crate::flags; use crate::http_cache; use crate::import_map::ImportMap; use crate::inspector::InspectorServer; use crate::lockfile::Lockfile; use crate::media_type::MediaType; use crate::module_graph::ModuleGraphFile; use crate::module_graph::ModuleGraphLoader; use crate::module_graph2::GraphBuilder2; use crate::module_graph2::TranspileOptions; use crate::permissions::Permissions; use crate::specifier_handler::FetchHandler; use crate::tsc::CompiledModule; use crate::tsc::TargetLib; use crate::tsc::TsCompiler; use deno_core::error::AnyError; use deno_core::ModuleSpecifier; use std::cell::RefCell; use std::env; use std::rc::Rc; use std::sync::Arc; use std::sync::Mutex; pub fn exit_unstable(api_name: &str) { eprintln!( "Unstable API '{}'. The --unstable flag must be provided.", api_name ); std::process::exit(70); } /// This structure represents state of single "deno" program. /// /// It is shared by all created workers (thus V8 isolates). pub struct ProgramState { /// Flags parsed from `argv` contents. pub flags: flags::Flags, /// Permissions parsed from `flags`. pub permissions: Permissions, pub dir: deno_dir::DenoDir, pub file_fetcher: SourceFileFetcher, pub ts_compiler: TsCompiler, pub lockfile: Option>, pub maybe_import_map: Option, pub maybe_inspector_server: Option>, } impl ProgramState { pub fn new(flags: flags::Flags) -> Result, AnyError> { let custom_root = env::var("DENO_DIR").map(String::into).ok(); let dir = deno_dir::DenoDir::new(custom_root)?; let deps_cache_location = dir.root.join("deps"); let http_cache = http_cache::HttpCache::new(&deps_cache_location); let ca_file = flags.ca_file.clone().or_else(|| env::var("DENO_CERT").ok()); let file_fetcher = SourceFileFetcher::new( http_cache, !flags.reload, flags.cache_blocklist.clone(), flags.no_remote, flags.cached_only, ca_file.as_deref(), )?; let ts_compiler = TsCompiler::new( file_fetcher.clone(), flags.clone(), dir.gen_cache.clone(), )?; let lockfile = if let Some(filename) = &flags.lock { let lockfile = Lockfile::new(filename.clone(), flags.lock_write)?; Some(Mutex::new(lockfile)) } else { None }; let maybe_import_map: Option = match flags.import_map_path.as_ref() { None => None, Some(file_path) => { if !flags.unstable { exit_unstable("--importmap") } Some(ImportMap::load(file_path)?) } }; let maybe_inspect_host = flags.inspect.or(flags.inspect_brk); let maybe_inspector_server = match maybe_inspect_host { Some(host) => Some(Arc::new(InspectorServer::new(host))), None => None, }; let program_state = ProgramState { dir, permissions: Permissions::from_flags(&flags), flags, file_fetcher, ts_compiler, lockfile, maybe_import_map, maybe_inspector_server, }; Ok(Arc::new(program_state)) } /// This function is called when new module load is /// initialized by the JsRuntime. Its resposibility is to collect /// all dependencies and if it is required then also perform TS typecheck /// and traspilation. pub async fn prepare_module_load( self: &Arc, module_specifier: ModuleSpecifier, maybe_referrer: Option, target_lib: TargetLib, permissions: Permissions, is_dyn_import: bool, maybe_import_map: Option, ) -> Result<(), AnyError> { let module_specifier = module_specifier.clone(); if self.flags.no_check { debug!("Transpiling root: {}", module_specifier); // TODO(kitsonk) note that self.permissions != permissions, which is // something that should be handled better in the future. let handler = Rc::new(RefCell::new(FetchHandler::new(self, permissions.clone())?)); let mut builder = GraphBuilder2::new(handler, maybe_import_map); builder.insert(&module_specifier).await?; let mut graph = builder.get_graph(&self.lockfile)?; let (stats, maybe_ignored_options) = graph.transpile(TranspileOptions { debug: self.flags.log_level == Some(log::Level::Debug), maybe_config_path: self.flags.config_path.clone(), })?; if let Some(ignored_options) = maybe_ignored_options { eprintln!("{}", ignored_options); } debug!("{}", stats); } else { let mut module_graph_loader = ModuleGraphLoader::new( self.file_fetcher.clone(), maybe_import_map, permissions.clone(), is_dyn_import, false, ); module_graph_loader .add_to_graph(&module_specifier, maybe_referrer) .await?; let module_graph = module_graph_loader.get_graph(); let out = self .file_fetcher .fetch_cached_source_file(&module_specifier, permissions.clone()) .expect("Source file not found"); let module_graph_files = module_graph.values().collect::>(); // Check integrity of every file in module graph if let Some(ref lockfile) = self.lockfile { let mut g = lockfile.lock().unwrap(); for graph_file in &module_graph_files { let check_passed = g.check_or_insert(&graph_file.url, &graph_file.source_code); if !check_passed { eprintln!( "Subresource integrity check failed --lock={}\n{}", g.filename.display(), graph_file.url ); std::process::exit(10); } } } // Check if we need to compile files. let should_compile = needs_compilation( self.ts_compiler.compile_js, out.media_type, &module_graph_files, ); let allow_js = should_allow_js(&module_graph_files); if should_compile { self .ts_compiler .compile(self, &out, target_lib, &module_graph, allow_js) .await?; } } if let Some(ref lockfile) = self.lockfile { let g = lockfile.lock().unwrap(); g.write()?; } Ok(()) } // TODO(bartlomieju): this method doesn't need to be async anymore /// This method is used after `prepare_module_load` finishes and JsRuntime /// starts loading source and executing source code. This method shouldn't /// perform any IO (besides $DENO_DIR) and only operate on sources collected /// during `prepare_module_load`. pub async fn fetch_compiled_module( &self, module_specifier: ModuleSpecifier, _maybe_referrer: Option, ) -> Result { let out = self .file_fetcher .fetch_cached_source_file(&module_specifier, Permissions::allow_all()) .expect("Cached source file doesn't exist"); // Check if we need to compile files let was_compiled = match out.media_type { MediaType::TypeScript | MediaType::TSX | MediaType::JSX => true, MediaType::JavaScript => self.ts_compiler.compile_js, _ => false, }; let compiled_module = if was_compiled { match self.ts_compiler.get_compiled_module(&out.url) { Ok(module) => module, Err(e) => { let msg = format!( "Failed to get compiled source code of \"{}\".\nReason: {}\n\ If the source file provides only type exports, prefer to use \"import type\" or \"export type\" syntax instead.", out.url, e.to_string() ); info!("{} {}", crate::colors::yellow("Warning"), msg); CompiledModule { code: "".to_string(), name: out.url.to_string(), } } } } else { CompiledModule { code: out.source_code, name: out.url.to_string(), } }; Ok(compiled_module) } /// Quits the process if the --unstable flag was not provided. /// /// This is intentionally a non-recoverable check so that people cannot probe /// for unstable APIs from stable programs. pub fn check_unstable(&self, api_name: &str) { if !self.flags.unstable { exit_unstable(api_name); } } #[cfg(test)] pub fn mock( argv: Vec, maybe_flags: Option, ) -> Arc { ProgramState::new(flags::Flags { argv, ..maybe_flags.unwrap_or_default() }) .unwrap() } } /// Determine if TS compiler should be run with `allowJs` setting on. This /// is the case when there's either: /// - a JavaScript file with non-JavaScript import /// - JSX import fn should_allow_js(module_graph_files: &[&ModuleGraphFile]) -> bool { module_graph_files.iter().any(|module_file| { if module_file.media_type == MediaType::JSX { true } else if module_file.media_type == MediaType::JavaScript { module_file.imports.iter().any(|import_desc| { let import_file = module_graph_files .iter() .find(|f| { f.specifier == import_desc.resolved_specifier.to_string().as_str() }) .expect("Failed to find imported file"); let media_type = import_file.media_type; media_type == MediaType::TypeScript || media_type == MediaType::TSX || media_type == MediaType::JSX }) } else { false } }) } // Compilation happens if either: // - `checkJs` is set to true in TS config // - entry point is a TS file // - any dependency in module graph is a TS file fn needs_compilation( compile_js: bool, media_type: MediaType, module_graph_files: &[&ModuleGraphFile], ) -> bool { let mut needs_compilation = match media_type { MediaType::TypeScript | MediaType::TSX | MediaType::JSX => true, MediaType::JavaScript => compile_js, _ => false, }; needs_compilation |= module_graph_files.iter().any(|module_file| { let media_type = module_file.media_type; media_type == (MediaType::TypeScript) || media_type == (MediaType::TSX) || media_type == (MediaType::JSX) }); needs_compilation } #[test] fn thread_safe() { fn f(_: S) {} f(ProgramState::mock(vec![], None)); } #[test] fn test_should_allow_js() { use crate::ast::Location; use crate::module_graph::ImportDescriptor; assert!(should_allow_js(&[ &ModuleGraphFile { specifier: "file:///some/file.ts".to_string(), url: "file:///some/file.ts".to_string(), redirect: None, filename: "some/file.ts".to_string(), imports: vec![], version_hash: "1".to_string(), referenced_files: vec![], lib_directives: vec![], types_directives: vec![], type_headers: vec![], media_type: MediaType::TypeScript, source_code: "function foo() {}".to_string(), }, &ModuleGraphFile { specifier: "file:///some/file1.js".to_string(), url: "file:///some/file1.js".to_string(), redirect: None, filename: "some/file1.js".to_string(), version_hash: "1".to_string(), imports: vec![ImportDescriptor { specifier: "./file.ts".to_string(), resolved_specifier: ModuleSpecifier::resolve_url( "file:///some/file.ts", ) .unwrap(), type_directive: None, resolved_type_directive: None, location: Location { filename: "file:///some/file1.js".to_string(), line: 0, col: 0, }, }], referenced_files: vec![], lib_directives: vec![], types_directives: vec![], type_headers: vec![], media_type: MediaType::JavaScript, source_code: "function foo() {}".to_string(), }, ],)); assert!(should_allow_js(&[ &ModuleGraphFile { specifier: "file:///some/file.jsx".to_string(), url: "file:///some/file.jsx".to_string(), redirect: None, filename: "some/file.jsx".to_string(), imports: vec![], version_hash: "1".to_string(), referenced_files: vec![], lib_directives: vec![], types_directives: vec![], type_headers: vec![], media_type: MediaType::JSX, source_code: "function foo() {}".to_string(), }, &ModuleGraphFile { specifier: "file:///some/file.ts".to_string(), url: "file:///some/file.ts".to_string(), redirect: None, filename: "some/file.ts".to_string(), version_hash: "1".to_string(), imports: vec![ImportDescriptor { specifier: "./file.jsx".to_string(), resolved_specifier: ModuleSpecifier::resolve_url( "file:///some/file.jsx", ) .unwrap(), type_directive: None, resolved_type_directive: None, location: Location { filename: "file:///some/file1.ts".to_string(), line: 0, col: 0, }, }], referenced_files: vec![], lib_directives: vec![], types_directives: vec![], type_headers: vec![], media_type: MediaType::TypeScript, source_code: "function foo() {}".to_string(), }, ])); assert!(!should_allow_js(&[ &ModuleGraphFile { specifier: "file:///some/file.js".to_string(), url: "file:///some/file.js".to_string(), redirect: None, filename: "some/file.js".to_string(), imports: vec![], referenced_files: vec![], lib_directives: vec![], types_directives: vec![], version_hash: "1".to_string(), type_headers: vec![], media_type: MediaType::JavaScript, source_code: "function foo() {}".to_string(), }, &ModuleGraphFile { specifier: "file:///some/file1.js".to_string(), url: "file:///some/file1.js".to_string(), redirect: None, filename: "some/file1.js".to_string(), imports: vec![ImportDescriptor { specifier: "./file.js".to_string(), resolved_specifier: ModuleSpecifier::resolve_url( "file:///some/file.js", ) .unwrap(), type_directive: None, resolved_type_directive: None, location: Location { filename: "file:///some/file.js".to_string(), line: 0, col: 0, }, }], referenced_files: vec![], lib_directives: vec![], types_directives: vec![], version_hash: "1".to_string(), type_headers: vec![], media_type: MediaType::JavaScript, source_code: "function foo() {}".to_string(), }, ],)); } #[test] fn test_needs_compilation() { assert!(!needs_compilation( false, MediaType::JavaScript, &[&ModuleGraphFile { specifier: "some/file.js".to_string(), url: "file:///some/file.js".to_string(), redirect: None, filename: "some/file.js".to_string(), imports: vec![], referenced_files: vec![], lib_directives: vec![], types_directives: vec![], type_headers: vec![], version_hash: "1".to_string(), media_type: MediaType::JavaScript, source_code: "function foo() {}".to_string(), }], )); assert!(!needs_compilation(false, MediaType::JavaScript, &[])); assert!(needs_compilation(true, MediaType::JavaScript, &[])); assert!(needs_compilation(false, MediaType::TypeScript, &[])); assert!(needs_compilation(false, MediaType::JSX, &[])); assert!(needs_compilation(false, MediaType::TSX, &[])); assert!(needs_compilation( false, MediaType::JavaScript, &[ &ModuleGraphFile { specifier: "file:///some/file.ts".to_string(), url: "file:///some/file.ts".to_string(), redirect: None, filename: "some/file.ts".to_string(), imports: vec![], referenced_files: vec![], lib_directives: vec![], types_directives: vec![], type_headers: vec![], media_type: MediaType::TypeScript, version_hash: "1".to_string(), source_code: "function foo() {}".to_string(), }, &ModuleGraphFile { specifier: "file:///some/file1.js".to_string(), url: "file:///some/file1.js".to_string(), redirect: None, filename: "some/file1.js".to_string(), imports: vec![], referenced_files: vec![], lib_directives: vec![], types_directives: vec![], type_headers: vec![], version_hash: "1".to_string(), media_type: MediaType::JavaScript, source_code: "function foo() {}".to_string(), }, ], )); }