2024-01-01 14:58:21 -05:00
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2021-12-16 05:45:41 -05:00
2024-02-15 14:49:35 -05:00
use crate ::args ::jsr_url ;
2023-02-09 22:00:23 -05:00
use crate ::args ::CliOptions ;
2022-12-06 14:12:51 -05:00
use crate ::args ::Lockfile ;
2024-03-07 08:59:57 -05:00
use crate ::args ::DENO_DISABLE_PEDANTIC_NODE_WARNINGS ;
2022-12-09 09:40:48 -05:00
use crate ::cache ;
2023-08-01 20:49:09 -04:00
use crate ::cache ::GlobalHttpCache ;
2023-10-25 18:13:22 -04:00
use crate ::cache ::ModuleInfoCache ;
2023-04-14 16:22:33 -04:00
use crate ::cache ::ParsedSourceCache ;
2021-12-16 05:45:41 -05:00
use crate ::colors ;
use crate ::errors ::get_error_class_name ;
2023-04-14 16:22:33 -04:00
use crate ::file_fetcher ::FileFetcher ;
2023-04-21 21:02:46 -04:00
use crate ::npm ::CliNpmResolver ;
2023-02-15 11:30:54 -05:00
use crate ::resolver ::CliGraphResolver ;
2023-12-07 15:59:13 -05:00
use crate ::resolver ::SloppyImportsResolver ;
2022-12-09 09:40:48 -05:00
use crate ::tools ::check ;
2023-04-14 18:05:46 -04:00
use crate ::tools ::check ::TypeChecker ;
2023-10-19 01:05:00 -04:00
use crate ::util ::file_watcher ::WatcherCommunicator ;
2024-01-08 12:18:42 -05:00
use crate ::util ::fs ::canonicalize_path ;
2024-05-28 14:58:43 -04:00
use deno_emit ::LoaderChecksum ;
use deno_graph ::JsrLoadError ;
use deno_graph ::ModuleLoadError ;
2024-04-17 10:19:55 -04:00
use deno_runtime ::fs_util ::specifier_to_file_path ;
2022-06-28 16:45:55 -04:00
2024-02-19 10:28:41 -05:00
use deno_config ::WorkspaceMemberConfig ;
2022-12-09 09:40:48 -05:00
use deno_core ::anyhow ::bail ;
2021-12-16 05:45:41 -05:00
use deno_core ::error ::custom_error ;
use deno_core ::error ::AnyError ;
2023-04-14 16:22:33 -04:00
use deno_core ::parking_lot ::Mutex ;
2021-12-16 05:45:41 -05:00
use deno_core ::ModuleSpecifier ;
2023-04-14 16:22:33 -04:00
use deno_graph ::source ::Loader ;
2024-02-27 13:30:21 -05:00
use deno_graph ::source ::ResolutionMode ;
2023-10-20 00:02:08 -04:00
use deno_graph ::source ::ResolveError ;
2023-06-06 17:07:46 -04:00
use deno_graph ::GraphKind ;
2023-02-22 14:15:25 -05:00
use deno_graph ::Module ;
2023-03-21 11:46:40 -04:00
use deno_graph ::ModuleError ;
2021-12-16 05:45:41 -05:00
use deno_graph ::ModuleGraph ;
use deno_graph ::ModuleGraphError ;
2023-01-24 15:14:49 -05:00
use deno_graph ::ResolutionError ;
use deno_graph ::SpecifierError ;
2023-12-01 15:12:10 -05:00
use deno_runtime ::deno_fs ::FileSystem ;
2023-04-21 21:02:46 -04:00
use deno_runtime ::deno_node ;
2023-01-07 11:25:34 -05:00
use deno_runtime ::permissions ::PermissionsContainer ;
2023-09-18 10:46:44 -04:00
use deno_semver ::package ::PackageNv ;
use deno_semver ::package ::PackageReq ;
2023-01-27 17:36:23 -05:00
use import_map ::ImportMapError ;
2023-02-24 14:42:45 -05:00
use std ::collections ::HashSet ;
2023-06-14 18:29:19 -04:00
use std ::path ::PathBuf ;
2021-12-16 05:45:41 -05:00
use std ::sync ::Arc ;
2023-02-15 11:30:54 -05:00
#[ derive(Clone, Copy) ]
pub struct GraphValidOptions {
pub check_js : bool ,
pub follow_type_only : bool ,
pub is_vendoring : bool ,
2024-05-28 14:58:43 -04:00
/// Whether to exit the process for lockfile errors.
/// Otherwise, surfaces lockfile errors as errors.
pub exit_lockfile_errors : bool ,
2023-02-15 11:30:54 -05:00
}
2023-02-09 22:00:23 -05:00
/// Check if `roots` and their deps are available. Returns `Ok(())` if
/// so. Returns `Err(_)` if there is a known module graph or resolution
/// error statically reachable from `roots`.
///
/// It is preferable to use this over using deno_graph's API directly
/// because it will have enhanced error message information specifically
/// for the CLI.
pub fn graph_valid (
graph : & ModuleGraph ,
2024-05-28 14:58:43 -04:00
fs : & Arc < dyn FileSystem > ,
2023-02-09 22:00:23 -05:00
roots : & [ ModuleSpecifier ] ,
2023-02-15 11:30:54 -05:00
options : GraphValidOptions ,
2023-02-09 22:00:23 -05:00
) -> Result < ( ) , AnyError > {
2024-05-28 14:58:43 -04:00
if options . exit_lockfile_errors {
graph_exit_lock_errors ( graph ) ;
}
2023-02-15 11:30:54 -05:00
let mut errors = graph
. walk (
2024-06-05 11:04:16 -04:00
roots . iter ( ) ,
2023-02-15 11:30:54 -05:00
deno_graph ::WalkOptions {
check_js : options . check_js ,
follow_type_only : options . follow_type_only ,
follow_dynamic : options . is_vendoring ,
2024-04-11 19:00:17 -04:00
prefer_fast_check_graph : false ,
2023-02-15 11:30:54 -05:00
} ,
)
. errors ( )
. flat_map ( | error | {
let is_root = match & error {
2023-10-24 17:43:19 -04:00
ModuleGraphError ::ResolutionError ( _ )
| ModuleGraphError ::TypesResolutionError ( _ ) = > false ,
2023-03-21 11:46:40 -04:00
ModuleGraphError ::ModuleError ( error ) = > {
roots . contains ( error . specifier ( ) )
}
2023-02-15 11:30:54 -05:00
} ;
2023-10-24 17:43:19 -04:00
let mut message = match & error {
ModuleGraphError ::ResolutionError ( resolution_error ) = > {
enhanced_resolution_error_message ( resolution_error )
}
ModuleGraphError ::TypesResolutionError ( resolution_error ) = > {
format! (
" Failed resolving types. {} " ,
2023-12-07 15:59:13 -05:00
enhanced_resolution_error_message ( resolution_error )
2023-10-24 17:43:19 -04:00
)
}
2024-05-28 14:58:43 -04:00
ModuleGraphError ::ModuleError ( error ) = > {
enhanced_lockfile_error_message ( error )
. or_else ( | | enhanced_sloppy_imports_error_message ( fs , error ) )
. unwrap_or_else ( | | format! ( " {} " , error ) )
2023-12-07 15:59:13 -05:00
}
2023-02-15 11:30:54 -05:00
} ;
2021-12-16 05:45:41 -05:00
2023-02-15 11:30:54 -05:00
if let Some ( range ) = error . maybe_range ( ) {
if ! is_root & & ! range . specifier . as_str ( ) . contains ( " /$deno$eval " ) {
2023-12-01 15:12:10 -05:00
message . push_str ( " \n at " ) ;
message . push_str ( & format_range_with_colors ( range ) ) ;
2023-02-15 11:30:54 -05:00
}
2021-12-16 05:45:41 -05:00
}
2024-04-16 16:46:31 -04:00
if graph . graph_kind ( ) = = GraphKind ::TypesOnly
& & matches! (
error ,
ModuleGraphError ::ModuleError ( ModuleError ::UnsupportedMediaType ( .. ) )
)
{
log ::debug! ( " Ignoring: {} " , message ) ;
return None ;
}
2023-02-15 11:30:54 -05:00
if options . is_vendoring {
// warn about failing dynamic imports when vendoring, but don't fail completely
2023-03-21 11:46:40 -04:00
if matches! (
error ,
ModuleGraphError ::ModuleError ( ModuleError ::MissingDynamic ( _ , _ ) )
) {
2024-04-16 16:46:31 -04:00
log ::warn! ( " Ignoring: {} " , message ) ;
2023-02-15 11:30:54 -05:00
return None ;
}
// ignore invalid downgrades and invalid local imports when vendoring
2023-10-24 17:43:19 -04:00
match & error {
ModuleGraphError ::ResolutionError ( err )
| ModuleGraphError ::TypesResolutionError ( err ) = > {
if matches! (
err ,
ResolutionError ::InvalidDowngrade { .. }
| ResolutionError ::InvalidLocalImport { .. }
) {
return None ;
}
2023-02-15 11:30:54 -05:00
}
2023-10-24 17:43:19 -04:00
ModuleGraphError ::ModuleError ( _ ) = > { }
2023-02-15 11:30:54 -05:00
}
}
Some ( custom_error ( get_error_class_name ( & error . into ( ) ) , message ) )
} ) ;
if let Some ( error ) = errors . next ( ) {
Err ( error )
} else {
Ok ( ( ) )
}
2021-12-16 05:45:41 -05:00
}
2024-05-28 14:58:43 -04:00
pub fn graph_exit_lock_errors ( graph : & ModuleGraph ) {
for error in graph . module_errors ( ) {
exit_for_lockfile_error ( error ) ;
}
}
2024-02-15 14:49:35 -05:00
2024-05-28 14:58:43 -04:00
fn exit_for_lockfile_error ( err : & ModuleError ) {
if let Some ( err_message ) = enhanced_lockfile_error_message ( err ) {
log ::error! ( " {} {} " , colors ::red ( " error: " ) , err_message ) ;
std ::process ::exit ( 10 ) ;
2021-12-16 05:45:41 -05:00
}
}
2022-12-09 09:40:48 -05:00
2023-10-25 18:13:22 -04:00
pub struct CreateGraphOptions < ' a > {
pub graph_kind : GraphKind ,
pub roots : Vec < ModuleSpecifier > ,
2024-02-16 17:10:06 -05:00
pub is_dynamic : bool ,
2024-01-09 18:20:52 -05:00
/// Specify `None` to use the default CLI loader.
pub loader : Option < & ' a mut dyn Loader > ,
2023-10-25 18:13:22 -04:00
}
2024-02-20 16:29:57 -05:00
pub struct ModuleGraphCreator {
2023-04-14 16:22:33 -04:00
options : Arc < CliOptions > ,
2023-09-29 09:26:25 -04:00
npm_resolver : Arc < dyn CliNpmResolver > ,
2024-02-20 16:29:57 -05:00
module_graph_builder : Arc < ModuleGraphBuilder > ,
2023-04-14 18:05:46 -04:00
type_checker : Arc < TypeChecker > ,
2023-04-14 16:22:33 -04:00
}
2023-02-22 14:15:25 -05:00
2024-02-20 16:29:57 -05:00
impl ModuleGraphCreator {
2023-04-14 16:22:33 -04:00
pub fn new (
options : Arc < CliOptions > ,
2023-09-29 09:26:25 -04:00
npm_resolver : Arc < dyn CliNpmResolver > ,
2024-02-20 16:29:57 -05:00
module_graph_builder : Arc < ModuleGraphBuilder > ,
2023-04-14 18:05:46 -04:00
type_checker : Arc < TypeChecker > ,
2023-04-14 16:22:33 -04:00
) -> Self {
Self {
options ,
npm_resolver ,
2024-02-20 16:29:57 -05:00
module_graph_builder ,
2023-04-14 18:05:46 -04:00
type_checker ,
2023-04-14 16:22:33 -04:00
}
2022-12-09 09:40:48 -05:00
}
2023-10-25 18:13:22 -04:00
pub async fn create_graph (
& self ,
graph_kind : GraphKind ,
roots : Vec < ModuleSpecifier > ,
) -> Result < deno_graph ::ModuleGraph , AnyError > {
2024-02-20 16:29:57 -05:00
let mut cache = self . module_graph_builder . create_graph_loader ( ) ;
2023-10-25 18:13:22 -04:00
self
. create_graph_with_loader ( graph_kind , roots , & mut cache )
. await
}
2023-04-14 16:22:33 -04:00
pub async fn create_graph_with_loader (
& self ,
2023-06-06 17:07:46 -04:00
graph_kind : GraphKind ,
2023-04-14 16:22:33 -04:00
roots : Vec < ModuleSpecifier > ,
loader : & mut dyn Loader ,
2024-02-19 10:28:41 -05:00
) -> Result < ModuleGraph , AnyError > {
2023-10-25 18:13:22 -04:00
self
. create_graph_with_options ( CreateGraphOptions {
2024-02-16 17:10:06 -05:00
is_dynamic : false ,
2023-10-25 18:13:22 -04:00
graph_kind ,
roots ,
2024-01-09 18:20:52 -05:00
loader : Some ( loader ) ,
2023-10-25 18:13:22 -04:00
} )
. await
}
2024-03-07 11:30:30 -05:00
pub async fn create_and_validate_publish_graph (
2024-02-19 10:28:41 -05:00
& self ,
packages : & [ WorkspaceMemberConfig ] ,
2024-03-07 11:30:30 -05:00
build_fast_check_graph : bool ,
2024-02-19 10:28:41 -05:00
) -> Result < ModuleGraph , AnyError > {
let mut roots = Vec ::new ( ) ;
for package in packages {
roots . extend ( package . config_file . resolve_export_value_urls ( ) ? ) ;
}
2024-02-20 16:29:57 -05:00
let mut graph = self
2024-02-19 10:28:41 -05:00
. create_graph_with_options ( CreateGraphOptions {
is_dynamic : false ,
graph_kind : deno_graph ::GraphKind ::All ,
roots ,
loader : None ,
} )
2024-02-20 16:29:57 -05:00
. await ? ;
2024-03-07 11:30:30 -05:00
self . graph_valid ( & graph ) ? ;
2024-02-21 08:35:25 -05:00
if self . options . type_check_mode ( ) . is_true ( ) {
self . type_check_graph ( graph . clone ( ) ) . await ? ;
}
2024-03-07 11:30:30 -05:00
if build_fast_check_graph {
self . module_graph_builder . build_fast_check_graph (
& mut graph ,
BuildFastCheckGraphOptions {
workspace_fast_check : true ,
} ,
) ? ;
}
2024-02-20 16:29:57 -05:00
Ok ( graph )
2024-02-19 10:28:41 -05:00
}
2023-10-25 18:13:22 -04:00
pub async fn create_graph_with_options (
& self ,
options : CreateGraphOptions < '_ > ,
2024-02-19 10:28:41 -05:00
) -> Result < ModuleGraph , AnyError > {
2023-10-25 18:13:22 -04:00
let mut graph = ModuleGraph ::new ( options . graph_kind ) ;
2024-02-16 17:10:06 -05:00
2023-04-14 16:22:33 -04:00
self
2024-02-20 16:29:57 -05:00
. module_graph_builder
2024-02-16 17:10:06 -05:00
. build_graph_with_npm_resolution ( & mut graph , options )
2023-04-14 16:22:33 -04:00
. await ? ;
2023-09-29 09:26:25 -04:00
if let Some ( npm_resolver ) = self . npm_resolver . as_managed ( ) {
if graph . has_node_specifier & & self . options . type_check_mode ( ) . is_true ( ) {
npm_resolver . inject_synthetic_types_node_package ( ) . await ? ;
}
2023-01-24 09:05:54 -05:00
}
2023-04-14 16:22:33 -04:00
Ok ( graph )
}
pub async fn create_graph_and_maybe_check (
& self ,
roots : Vec < ModuleSpecifier > ,
) -> Result < Arc < deno_graph ::ModuleGraph > , AnyError > {
2023-06-07 10:09:10 -04:00
let graph_kind = self . options . type_check_mode ( ) . as_graph_kind ( ) ;
2023-06-14 18:29:19 -04:00
2024-02-16 17:10:06 -05:00
let graph = self
. create_graph_with_options ( CreateGraphOptions {
is_dynamic : false ,
graph_kind ,
2023-04-14 16:22:33 -04:00
roots ,
2024-02-16 17:10:06 -05:00
loader : None ,
} )
2023-04-14 16:22:33 -04:00
. await ? ;
2024-03-07 11:30:30 -05:00
self . graph_valid ( & graph ) ? ;
2023-04-14 16:22:33 -04:00
2023-06-07 10:09:10 -04:00
if self . options . type_check_mode ( ) . is_true ( ) {
2024-02-20 16:29:57 -05:00
// provide the graph to the type checker, then get it back after it's done
2024-02-21 08:35:25 -05:00
let graph = self . type_check_graph ( graph ) . await ? ;
2024-02-20 16:29:57 -05:00
Ok ( graph )
} else {
Ok ( Arc ::new ( graph ) )
2022-12-09 09:40:48 -05:00
}
}
2024-02-21 08:35:25 -05:00
2024-03-07 11:30:30 -05:00
pub fn graph_valid ( & self , graph : & ModuleGraph ) -> Result < ( ) , AnyError > {
self . module_graph_builder . graph_valid ( graph )
}
2024-02-21 08:35:25 -05:00
async fn type_check_graph (
& self ,
graph : ModuleGraph ,
) -> Result < Arc < ModuleGraph > , AnyError > {
self
. type_checker
. check (
graph ,
check ::CheckOptions {
build_fast_check_graph : true ,
lib : self . options . ts_type_lib_window ( ) ,
log_ignored_options : true ,
reload : self . options . reload_flag ( ) ,
type_check_mode : self . options . type_check_mode ( ) ,
} ,
)
. await
}
2024-02-20 16:29:57 -05:00
}
2022-12-09 09:40:48 -05:00
2024-02-20 16:29:57 -05:00
pub struct BuildFastCheckGraphOptions {
/// Whether to do fast check on workspace members. This
/// is mostly only useful when publishing.
pub workspace_fast_check : bool ,
}
pub struct ModuleGraphBuilder {
options : Arc < CliOptions > ,
caches : Arc < cache ::Caches > ,
fs : Arc < dyn FileSystem > ,
resolver : Arc < CliGraphResolver > ,
npm_resolver : Arc < dyn CliNpmResolver > ,
module_info_cache : Arc < ModuleInfoCache > ,
parsed_source_cache : Arc < ParsedSourceCache > ,
lockfile : Option < Arc < Mutex < Lockfile > > > ,
maybe_file_watcher_reporter : Option < FileWatcherReporter > ,
emit_cache : cache ::EmitCache ,
file_fetcher : Arc < FileFetcher > ,
global_http_cache : Arc < GlobalHttpCache > ,
}
impl ModuleGraphBuilder {
#[ allow(clippy::too_many_arguments) ]
pub fn new (
options : Arc < CliOptions > ,
caches : Arc < cache ::Caches > ,
fs : Arc < dyn FileSystem > ,
resolver : Arc < CliGraphResolver > ,
npm_resolver : Arc < dyn CliNpmResolver > ,
module_info_cache : Arc < ModuleInfoCache > ,
parsed_source_cache : Arc < ParsedSourceCache > ,
lockfile : Option < Arc < Mutex < Lockfile > > > ,
maybe_file_watcher_reporter : Option < FileWatcherReporter > ,
emit_cache : cache ::EmitCache ,
file_fetcher : Arc < FileFetcher > ,
global_http_cache : Arc < GlobalHttpCache > ,
) -> Self {
Self {
options ,
caches ,
fs ,
resolver ,
npm_resolver ,
module_info_cache ,
parsed_source_cache ,
lockfile ,
maybe_file_watcher_reporter ,
emit_cache ,
file_fetcher ,
global_http_cache ,
2024-01-09 18:20:52 -05:00
}
}
2023-04-14 16:22:33 -04:00
pub async fn build_graph_with_npm_resolution < ' a > (
2024-02-16 17:10:06 -05:00
& self ,
graph : & mut ModuleGraph ,
options : CreateGraphOptions < ' a > ,
) -> Result < ( ) , AnyError > {
enum MutLoaderRef < ' a > {
Borrowed ( & ' a mut dyn Loader ) ,
Owned ( cache ::FetchCacher ) ,
}
impl < ' a > MutLoaderRef < ' a > {
pub fn as_mut_loader ( & mut self ) -> & mut dyn Loader {
match self {
Self ::Borrowed ( loader ) = > * loader ,
Self ::Owned ( loader ) = > loader ,
}
}
}
2024-05-28 14:58:43 -04:00
struct LockfileLocker < ' a > ( & ' a Mutex < Lockfile > ) ;
impl < ' a > deno_graph ::source ::Locker for LockfileLocker < ' a > {
fn get_remote_checksum (
& self ,
specifier : & deno_ast ::ModuleSpecifier ,
) -> Option < LoaderChecksum > {
self
. 0
. lock ( )
. remote ( )
. get ( specifier . as_str ( ) )
. map ( | s | LoaderChecksum ::new ( s . clone ( ) ) )
}
fn has_remote_checksum (
& self ,
specifier : & deno_ast ::ModuleSpecifier ,
) -> bool {
self . 0. lock ( ) . remote ( ) . contains_key ( specifier . as_str ( ) )
}
fn set_remote_checksum (
& mut self ,
specifier : & deno_ast ::ModuleSpecifier ,
checksum : LoaderChecksum ,
) {
self
. 0
. lock ( )
. insert_remote ( specifier . to_string ( ) , checksum . into_string ( ) )
}
fn get_pkg_manifest_checksum (
& self ,
package_nv : & PackageNv ,
) -> Option < LoaderChecksum > {
self
. 0
. lock ( )
. content
. packages
. jsr
. get ( & package_nv . to_string ( ) )
. map ( | s | LoaderChecksum ::new ( s . integrity . clone ( ) ) )
}
fn set_pkg_manifest_checksum (
& mut self ,
package_nv : & PackageNv ,
checksum : LoaderChecksum ,
) {
// a value would only exist in here if two workers raced
// to insert the same package manifest checksum
self
. 0
. lock ( )
. insert_package ( package_nv . to_string ( ) , checksum . into_string ( ) ) ;
}
}
2024-02-16 17:10:06 -05:00
let maybe_imports = self . options . to_maybe_imports ( ) ? ;
2024-05-28 16:23:11 -04:00
let analyzer = self
. module_info_cache
. as_module_analyzer ( & self . parsed_source_cache ) ;
2024-02-16 17:10:06 -05:00
let mut loader = match options . loader {
Some ( loader ) = > MutLoaderRef ::Borrowed ( loader ) ,
None = > MutLoaderRef ::Owned ( self . create_graph_loader ( ) ) ,
} ;
2024-02-20 16:29:57 -05:00
let cli_resolver = & self . resolver ;
2024-02-16 17:10:06 -05:00
let graph_resolver = cli_resolver . as_graph_resolver ( ) ;
2024-06-05 11:04:16 -04:00
let graph_npm_resolver = cli_resolver . create_graph_npm_resolver ( ) ;
2024-02-16 17:10:06 -05:00
let maybe_file_watcher_reporter = self
. maybe_file_watcher_reporter
. as_ref ( )
. map ( | r | r . as_reporter ( ) ) ;
2024-02-20 16:29:57 -05:00
let workspace_members =
self . options . resolve_deno_graph_workspace_members ( ) ? ;
2024-05-28 14:58:43 -04:00
let mut locker = self
. lockfile
. as_ref ( )
. map ( | lockfile | LockfileLocker ( lockfile ) ) ;
2024-02-16 17:10:06 -05:00
self
. build_graph_with_npm_resolution_and_build_options (
graph ,
options . roots ,
loader . as_mut_loader ( ) ,
deno_graph ::BuildOptions {
2024-04-18 21:43:28 -04:00
imports : maybe_imports ,
2024-02-16 17:10:06 -05:00
is_dynamic : options . is_dynamic ,
2024-04-18 20:51:16 -04:00
passthrough_jsr_specifiers : false ,
2024-04-18 21:43:28 -04:00
workspace_members : & workspace_members ,
2024-02-21 16:58:37 -05:00
executor : Default ::default ( ) ,
2024-04-11 19:00:17 -04:00
file_system : & DenoGraphFsAdapter ( self . fs . as_ref ( ) ) ,
2024-04-18 21:43:28 -04:00
jsr_url_provider : & CliJsrUrlProvider ,
2024-06-05 11:04:16 -04:00
npm_resolver : Some ( & graph_npm_resolver ) ,
2024-04-11 19:00:17 -04:00
module_analyzer : & analyzer ,
2024-02-16 17:10:06 -05:00
reporter : maybe_file_watcher_reporter ,
2024-04-18 21:43:28 -04:00
resolver : Some ( graph_resolver ) ,
2024-05-28 14:58:43 -04:00
locker : locker . as_mut ( ) . map ( | l | l as _ ) ,
2024-02-16 17:10:06 -05:00
} ,
)
. await
}
async fn build_graph_with_npm_resolution_and_build_options < ' a > (
2023-04-14 16:22:33 -04:00
& self ,
graph : & mut ModuleGraph ,
roots : Vec < ModuleSpecifier > ,
2024-05-28 14:58:43 -04:00
loader : & ' a mut dyn deno_graph ::source ::Loader ,
2023-04-14 16:22:33 -04:00
options : deno_graph ::BuildOptions < ' a > ,
) -> Result < ( ) , AnyError > {
2023-05-23 18:51:48 -04:00
// ensure an "npm install" is done if the user has explicitly
// opted into using a node_modules directory
if self . options . node_modules_dir_enablement ( ) = = Some ( true ) {
2023-09-30 12:06:38 -04:00
if let Some ( npm_resolver ) = self . npm_resolver . as_managed ( ) {
npm_resolver . ensure_top_level_package_json_install ( ) . await ? ;
}
2023-05-23 18:51:48 -04:00
}
2024-05-28 14:58:43 -04:00
// fill the graph with the information from the lockfile
let is_first_execution = graph . roots . is_empty ( ) ;
if is_first_execution {
// populate the information from the lockfile
2023-08-29 13:03:02 -04:00
if let Some ( lockfile ) = & self . lockfile {
let lockfile = lockfile . lock ( ) ;
for ( from , to ) in & lockfile . content . redirects {
if let Ok ( from ) = ModuleSpecifier ::parse ( from ) {
if let Ok ( to ) = ModuleSpecifier ::parse ( to ) {
2023-09-18 10:46:44 -04:00
if ! matches! ( from . scheme ( ) , " file " | " npm " | " jsr " ) {
2023-08-29 13:03:02 -04:00
graph . redirects . insert ( from , to ) ;
}
}
}
}
2023-09-18 10:46:44 -04:00
for ( key , value ) in & lockfile . content . packages . specifiers {
if let Some ( key ) = key
. strip_prefix ( " jsr: " )
. and_then ( | key | PackageReq ::from_str ( key ) . ok ( ) )
{
if let Some ( value ) = value
. strip_prefix ( " jsr: " )
. and_then ( | value | PackageNv ::from_str ( value ) . ok ( ) )
{
2024-01-09 18:20:52 -05:00
graph . packages . add_nv ( key , value ) ;
2023-09-18 10:46:44 -04:00
}
}
}
}
}
2023-09-07 09:09:16 -04:00
2024-05-28 14:58:43 -04:00
let initial_redirects_len = graph . redirects . len ( ) ;
let initial_package_deps_len = graph . packages . package_deps_sum ( ) ;
let initial_package_mappings_len = graph . packages . mappings ( ) . len ( ) ;
let initial_npm_packages = graph . npm_packages . len ( ) ;
2022-12-09 09:40:48 -05:00
2024-05-28 14:58:43 -04:00
graph . build ( roots , loader , options ) . await ;
2023-09-07 09:09:16 -04:00
2024-05-28 16:24:07 -04:00
let has_npm_packages_changed =
graph . npm_packages . len ( ) ! = initial_npm_packages ;
// skip installing npm packages if we don't have to
if is_first_execution
& & self . npm_resolver . root_node_modules_path ( ) . is_some ( )
| | has_npm_packages_changed
{
if let Some ( npm_resolver ) = self . npm_resolver . as_managed ( ) {
// ensure that the top level package.json is installed if a
// specifier was matched in the package.json
if self . resolver . found_package_json_dep ( ) {
npm_resolver . ensure_top_level_package_json_install ( ) . await ? ;
}
2023-02-22 14:15:25 -05:00
2024-05-28 16:24:07 -04:00
// resolve the dependencies of any pending dependencies
// that were inserted by building the graph
npm_resolver . resolve_pending ( ) . await ? ;
}
2023-09-29 09:26:25 -04:00
}
2023-04-11 18:10:51 -04:00
2024-05-28 14:58:43 -04:00
let has_redirects_changed = graph . redirects . len ( ) ! = initial_redirects_len ;
let has_jsr_package_deps_changed =
graph . packages . package_deps_sum ( ) ! = initial_package_deps_len ;
let has_jsr_package_mappings_changed =
graph . packages . mappings ( ) . len ( ) ! = initial_package_mappings_len ;
if has_redirects_changed
| | has_jsr_package_deps_changed
| | has_jsr_package_mappings_changed
{
if let Some ( lockfile ) = & self . lockfile {
let mut lockfile = lockfile . lock ( ) ;
// https redirects
if has_redirects_changed {
let graph_redirects = graph . redirects . iter ( ) . filter ( | ( from , _ ) | {
! matches! ( from . scheme ( ) , " npm " | " file " | " deno " )
} ) ;
for ( from , to ) in graph_redirects {
lockfile . insert_redirect ( from . to_string ( ) , to . to_string ( ) ) ;
}
}
// jsr package mappings
if has_jsr_package_mappings_changed {
for ( from , to ) in graph . packages . mappings ( ) {
lockfile . insert_package_specifier (
format! ( " jsr: {} " , from ) ,
format! ( " jsr: {} " , to ) ,
) ;
}
}
// jsr packages
if has_jsr_package_deps_changed {
for ( name , deps ) in graph . packages . packages_with_deps ( ) {
lockfile
. add_package_deps ( & name . to_string ( ) , deps . map ( | s | s . to_string ( ) ) ) ;
}
}
}
}
2023-04-14 16:22:33 -04:00
Ok ( ( ) )
}
2023-02-22 14:15:25 -05:00
2024-02-20 16:29:57 -05:00
pub fn build_fast_check_graph (
& self ,
graph : & mut ModuleGraph ,
options : BuildFastCheckGraphOptions ,
) -> Result < ( ) , AnyError > {
if ! graph . graph_kind ( ) . include_types ( ) {
return Ok ( ( ) ) ;
}
log ::debug! ( " Building fast check graph " ) ;
let fast_check_cache = if ! options . workspace_fast_check {
Some ( cache ::FastCheckCache ::new ( self . caches . fast_check_db ( ) ) )
} else {
None
} ;
let parser = self . parsed_source_cache . as_capturing_parser ( ) ;
let cli_resolver = & self . resolver ;
let graph_resolver = cli_resolver . as_graph_resolver ( ) ;
2024-06-05 11:04:16 -04:00
let graph_npm_resolver = cli_resolver . create_graph_npm_resolver ( ) ;
2024-02-20 16:29:57 -05:00
let workspace_members = if options . workspace_fast_check {
Some ( self . options . resolve_deno_graph_workspace_members ( ) ? )
} else {
None
} ;
graph . build_fast_check_type_graph (
deno_graph ::BuildFastCheckTypeGraphOptions {
2024-04-24 17:43:34 -04:00
jsr_url_provider : & CliJsrUrlProvider ,
2024-02-20 16:29:57 -05:00
fast_check_cache : fast_check_cache . as_ref ( ) . map ( | c | c as _ ) ,
fast_check_dts : false ,
module_parser : Some ( & parser ) ,
resolver : Some ( graph_resolver ) ,
2024-06-05 11:04:16 -04:00
npm_resolver : Some ( & graph_npm_resolver ) ,
2024-02-20 16:29:57 -05:00
workspace_fast_check : if let Some ( members ) = & workspace_members {
deno_graph ::WorkspaceFastCheckOption ::Enabled ( members )
} else {
deno_graph ::WorkspaceFastCheckOption ::Disabled
} ,
} ,
) ;
Ok ( ( ) )
}
2023-04-14 16:22:33 -04:00
/// Creates the default loader used for creating a graph.
pub fn create_graph_loader ( & self ) -> cache ::FetchCacher {
2023-04-26 16:23:28 -04:00
self . create_fetch_cacher ( PermissionsContainer ::allow_all ( ) )
2023-04-14 16:22:33 -04:00
}
pub fn create_fetch_cacher (
& self ,
2023-04-26 16:23:28 -04:00
permissions : PermissionsContainer ,
2023-04-14 16:22:33 -04:00
) -> cache ::FetchCacher {
cache ::FetchCacher ::new (
self . emit_cache . clone ( ) ,
self . file_fetcher . clone ( ) ,
self . options . resolve_file_header_overrides ( ) ,
2023-08-01 20:49:09 -04:00
self . global_http_cache . clone ( ) ,
2023-10-25 14:39:00 -04:00
self . npm_resolver . clone ( ) ,
2023-10-25 18:13:22 -04:00
self . module_info_cache . clone ( ) ,
2023-04-26 16:23:28 -04:00
permissions ,
2023-04-14 16:22:33 -04:00
)
}
2024-03-07 11:30:30 -05:00
/// Check if `roots` and their deps are available. Returns `Ok(())` if
/// so. Returns `Err(_)` if there is a known module graph or resolution
/// error statically reachable from `roots` and not a dynamic import.
pub fn graph_valid ( & self , graph : & ModuleGraph ) -> Result < ( ) , AnyError > {
2024-06-05 11:04:16 -04:00
self . graph_roots_valid (
graph ,
& graph . roots . iter ( ) . cloned ( ) . collect ::< Vec < _ > > ( ) ,
)
2024-03-07 11:30:30 -05:00
}
pub fn graph_roots_valid (
& self ,
graph : & ModuleGraph ,
roots : & [ ModuleSpecifier ] ,
) -> Result < ( ) , AnyError > {
graph_valid (
graph ,
2024-05-28 14:58:43 -04:00
& self . fs ,
2024-03-07 11:30:30 -05:00
roots ,
GraphValidOptions {
is_vendoring : false ,
follow_type_only : self . options . type_check_mode ( ) . is_true ( ) ,
check_js : self . options . check_js ( ) ,
2024-05-28 14:58:43 -04:00
exit_lockfile_errors : true ,
2024-03-07 11:30:30 -05:00
} ,
)
}
2023-02-22 14:15:25 -05:00
}
2022-12-09 09:40:48 -05:00
pub fn error_for_any_npm_specifier (
2023-02-22 14:15:25 -05:00
graph : & ModuleGraph ,
2022-12-09 09:40:48 -05:00
) -> Result < ( ) , AnyError > {
2023-02-22 14:15:25 -05:00
for module in graph . modules ( ) {
match module {
Module ::Npm ( module ) = > {
2023-05-10 20:06:59 -04:00
bail! ( " npm specifiers have not yet been implemented for this subcommand (https://github.com/denoland/deno/issues/15960). Found: {} " , module . specifier )
2022-12-09 09:40:48 -05:00
}
2023-02-22 14:15:25 -05:00
Module ::Node ( module ) = > {
2023-05-10 20:06:59 -04:00
bail! ( " Node specifiers have not yet been implemented for this subcommand (https://github.com/denoland/deno/issues/15960). Found: node:{} " , module . module_name )
2023-02-22 14:15:25 -05:00
}
2024-01-31 22:15:22 -05:00
Module ::Js ( _ ) | Module ::Json ( _ ) | Module ::External ( _ ) = > { }
2023-02-22 14:15:25 -05:00
}
2022-12-09 09:40:48 -05:00
}
2023-02-22 14:15:25 -05:00
Ok ( ( ) )
2022-12-09 09:40:48 -05:00
}
2023-01-24 15:14:49 -05:00
/// Adds more explanatory information to a resolution error.
pub fn enhanced_resolution_error_message ( error : & ResolutionError ) -> String {
2023-01-27 10:43:16 -05:00
let mut message = format! ( " {error} " ) ;
2023-01-24 15:14:49 -05:00
2023-01-27 17:36:23 -05:00
if let Some ( specifier ) = get_resolution_error_bare_node_specifier ( error ) {
2024-03-07 08:59:57 -05:00
if ! * DENO_DISABLE_PEDANTIC_NODE_WARNINGS {
message . push_str ( & format! (
2023-01-27 17:36:23 -05:00
" \n If you want to use a built-in Node module, add a \" node: \" prefix (ex. \" node:{specifier} \" ). "
) ) ;
2024-03-07 08:59:57 -05:00
}
2023-01-27 17:36:23 -05:00
}
message
}
2024-05-28 14:58:43 -04:00
fn enhanced_sloppy_imports_error_message (
fs : & Arc < dyn FileSystem > ,
2023-12-07 15:59:13 -05:00
error : & ModuleError ,
2024-05-28 14:58:43 -04:00
) -> Option < String > {
match error {
ModuleError ::LoadingErr ( specifier , _ , ModuleLoadError ::Loader ( _ ) ) // ex. "Is a directory" error
2024-03-07 11:30:30 -05:00
| ModuleError ::Missing ( specifier , _ ) = > {
2024-05-28 14:58:43 -04:00
let additional_message = SloppyImportsResolver ::new ( fs . clone ( ) )
. resolve ( specifier , ResolutionMode ::Execution )
. as_suggestion_message ( ) ? ;
Some ( format! (
" {} {} or run with --unstable-sloppy-imports " ,
error ,
additional_message ,
) )
}
_ = > None ,
}
}
fn enhanced_lockfile_error_message ( err : & ModuleError ) -> Option < String > {
match err {
ModuleError ::LoadingErr (
specifier ,
_ ,
ModuleLoadError ::Jsr ( JsrLoadError ::ContentChecksumIntegrity (
checksum_err ,
) ) ,
) = > {
Some ( format! (
concat! (
" Integrity check failed in package. The package may have been tampered with. \n \n " ,
" Specifier: {} \n " ,
" Actual: {} \n " ,
" Expected: {} \n \n " ,
" If you modified your global cache, run again with the --reload flag to restore " ,
" its state. If you want to modify dependencies locally run again with the " ,
" --vendor flag or specify ` \" vendor \" : true` in a deno.json then modify the contents " ,
" of the vendor/ folder. "
) ,
2024-02-27 13:30:21 -05:00
specifier ,
2024-05-28 14:58:43 -04:00
checksum_err . actual ,
checksum_err . expected ,
) )
}
ModuleError ::LoadingErr (
_specifier ,
_ ,
ModuleLoadError ::Jsr (
JsrLoadError ::PackageVersionManifestChecksumIntegrity (
package_nv ,
checksum_err ,
) ,
) ,
) = > {
Some ( format! (
concat! (
" Integrity check failed for package. The source code is invalid, as it does not match the expected hash in the lock file. \n \n " ,
" Package: {} \n " ,
" Actual: {} \n " ,
" Expected: {} \n \n " ,
" This could be caused by: \n " ,
" * the lock file may be corrupt \n " ,
" * the source itself may be corrupt \n \n " ,
" Use the --lock-write flag to regenerate the lockfile or --reload to reload the source code from the server. "
) ,
package_nv ,
checksum_err . actual ,
checksum_err . expected ,
) )
}
ModuleError ::LoadingErr (
specifier ,
_ ,
ModuleLoadError ::HttpsChecksumIntegrity ( checksum_err ) ,
) = > {
Some ( format! (
concat! (
" Integrity check failed for remote specifier. The source code is invalid, as it does not match the expected hash in the lock file. \n \n " ,
" Specifier: {} \n " ,
" Actual: {} \n " ,
" Expected: {} \n \n " ,
" This could be caused by: \n " ,
" * the lock file may be corrupt \n " ,
" * the source itself may be corrupt \n \n " ,
" Use the --lock-write flag to regenerate the lockfile or --reload to reload the source code from the server. "
) ,
specifier ,
checksum_err . actual ,
checksum_err . expected ,
) )
2023-12-07 15:59:13 -05:00
}
_ = > None ,
}
}
2023-01-27 17:36:23 -05:00
pub fn get_resolution_error_bare_node_specifier (
error : & ResolutionError ,
) -> Option < & str > {
2023-05-28 14:44:41 -04:00
get_resolution_error_bare_specifier ( error )
. filter ( | specifier | deno_node ::is_builtin_node_module ( specifier ) )
2023-01-27 17:36:23 -05:00
}
fn get_resolution_error_bare_specifier (
error : & ResolutionError ,
) -> Option < & str > {
2023-01-24 15:14:49 -05:00
if let ResolutionError ::InvalidSpecifier {
error : SpecifierError ::ImportPrefixMissing ( specifier , _ ) ,
..
} = error
{
2023-01-27 17:36:23 -05:00
Some ( specifier . as_str ( ) )
} else if let ResolutionError ::ResolverError { error , .. } = error {
2023-10-20 00:02:08 -04:00
if let ResolveError ::Other ( error ) = ( * error ) . as_ref ( ) {
if let Some ( ImportMapError ::UnmappedBareSpecifier ( specifier , _ ) ) =
error . downcast_ref ::< ImportMapError > ( )
{
Some ( specifier . as_str ( ) )
} else {
None
}
2023-01-27 17:36:23 -05:00
} else {
None
2023-01-24 15:14:49 -05:00
}
2023-01-27 17:36:23 -05:00
} else {
None
2023-01-24 15:14:49 -05:00
}
2023-01-27 17:36:23 -05:00
}
2023-01-24 15:14:49 -05:00
2023-06-14 18:29:19 -04:00
/// Gets if any of the specified root's "file:" dependents are in the
/// provided changed set.
pub fn has_graph_root_local_dependent_changed (
graph : & ModuleGraph ,
root : & ModuleSpecifier ,
2024-01-08 12:18:42 -05:00
canonicalized_changed_paths : & HashSet < PathBuf > ,
2023-06-14 18:29:19 -04:00
) -> bool {
let mut dependent_specifiers = graph . walk (
2024-06-05 11:04:16 -04:00
std ::iter ::once ( root ) ,
2023-06-14 18:29:19 -04:00
deno_graph ::WalkOptions {
follow_dynamic : true ,
follow_type_only : true ,
2024-04-11 19:00:17 -04:00
prefer_fast_check_graph : true ,
2023-06-14 18:29:19 -04:00
check_js : true ,
} ,
) ;
while let Some ( ( s , _ ) ) = dependent_specifiers . next ( ) {
2024-01-08 12:18:42 -05:00
if let Ok ( path ) = specifier_to_file_path ( s ) {
if let Ok ( path ) = canonicalize_path ( & path ) {
if canonicalized_changed_paths . contains ( & path ) {
return true ;
}
}
} else {
2023-06-14 18:29:19 -04:00
// skip walking this remote module's dependencies
dependent_specifiers . skip_previous_dependencies ( ) ;
}
}
false
}
#[ derive(Clone, Debug) ]
pub struct FileWatcherReporter {
2023-10-30 20:25:58 -04:00
watcher_communicator : Arc < WatcherCommunicator > ,
2023-06-14 18:29:19 -04:00
file_paths : Arc < Mutex < Vec < PathBuf > > > ,
}
impl FileWatcherReporter {
2023-10-30 20:25:58 -04:00
pub fn new ( watcher_communicator : Arc < WatcherCommunicator > ) -> Self {
2023-06-14 18:29:19 -04:00
Self {
2023-10-19 01:05:00 -04:00
watcher_communicator ,
2023-06-14 18:29:19 -04:00
file_paths : Default ::default ( ) ,
}
}
pub fn as_reporter ( & self ) -> & dyn deno_graph ::source ::Reporter {
self
}
}
impl deno_graph ::source ::Reporter for FileWatcherReporter {
fn on_load (
& self ,
specifier : & ModuleSpecifier ,
modules_done : usize ,
modules_total : usize ,
) {
let mut file_paths = self . file_paths . lock ( ) ;
if specifier . scheme ( ) = = " file " {
file_paths . push ( specifier . to_file_path ( ) . unwrap ( ) ) ;
}
if modules_done = = modules_total {
2023-10-19 01:05:00 -04:00
self
. watcher_communicator
. watch_paths ( file_paths . drain ( .. ) . collect ( ) )
. unwrap ( ) ;
2023-06-14 18:29:19 -04:00
}
}
}
2023-12-01 15:12:10 -05:00
pub struct DenoGraphFsAdapter < ' a > (
pub & ' a dyn deno_runtime ::deno_fs ::FileSystem ,
) ;
impl < ' a > deno_graph ::source ::FileSystem for DenoGraphFsAdapter < ' a > {
fn read_dir (
& self ,
dir_url : & deno_graph ::ModuleSpecifier ,
) -> Vec < deno_graph ::source ::DirEntry > {
use deno_core ::anyhow ;
use deno_graph ::source ::DirEntry ;
use deno_graph ::source ::DirEntryKind ;
let dir_path = match dir_url . to_file_path ( ) {
Ok ( path ) = > path ,
// ignore, treat as non-analyzable
Err ( ( ) ) = > return vec! [ ] ,
} ;
let entries = match self . 0. read_dir_sync ( & dir_path ) {
Ok ( dir ) = > dir ,
Err ( err )
if matches! (
err . kind ( ) ,
std ::io ::ErrorKind ::PermissionDenied | std ::io ::ErrorKind ::NotFound
) = >
{
return vec! [ ] ;
}
Err ( err ) = > {
return vec! [ DirEntry {
kind : DirEntryKind ::Error (
anyhow ::Error ::from ( err )
. context ( " Failed to read directory. " . to_string ( ) ) ,
) ,
url : dir_url . clone ( ) ,
} ] ;
}
} ;
let mut dir_entries = Vec ::with_capacity ( entries . len ( ) ) ;
for entry in entries {
let entry_path = dir_path . join ( & entry . name ) ;
dir_entries . push ( if entry . is_directory {
DirEntry {
kind : DirEntryKind ::Dir ,
url : ModuleSpecifier ::from_directory_path ( & entry_path ) . unwrap ( ) ,
}
} else if entry . is_file {
DirEntry {
kind : DirEntryKind ::File ,
url : ModuleSpecifier ::from_file_path ( & entry_path ) . unwrap ( ) ,
}
} else if entry . is_symlink {
DirEntry {
kind : DirEntryKind ::Symlink ,
url : ModuleSpecifier ::from_file_path ( & entry_path ) . unwrap ( ) ,
}
} else {
continue ;
} ) ;
}
dir_entries
}
}
pub fn format_range_with_colors ( range : & deno_graph ::Range ) -> String {
format! (
" {}:{}:{} " ,
colors ::cyan ( range . specifier . as_str ( ) ) ,
colors ::yellow ( & ( range . start . line + 1 ) . to_string ( ) ) ,
colors ::yellow ( & ( range . start . character + 1 ) . to_string ( ) )
)
}
2024-02-20 16:29:57 -05:00
#[ derive(Debug, Default, Clone, Copy) ]
2024-04-24 17:43:34 -04:00
pub struct CliJsrUrlProvider ;
2024-02-20 16:29:57 -05:00
impl deno_graph ::source ::JsrUrlProvider for CliJsrUrlProvider {
fn url ( & self ) -> & 'static ModuleSpecifier {
jsr_url ( )
}
}
2023-01-27 17:36:23 -05:00
#[ cfg(test) ]
mod test {
use std ::sync ::Arc ;
use deno_ast ::ModuleSpecifier ;
2023-10-20 00:02:08 -04:00
use deno_graph ::source ::ResolveError ;
2023-01-27 17:36:23 -05:00
use deno_graph ::Position ;
use deno_graph ::Range ;
use deno_graph ::ResolutionError ;
use deno_graph ::SpecifierError ;
2023-12-06 19:03:18 -05:00
use super ::* ;
2023-01-27 17:36:23 -05:00
#[ test ]
fn import_map_node_resolution_error ( ) {
let cases = vec! [ ( " fs " , Some ( " fs " ) ) , ( " other " , None ) ] ;
for ( input , output ) in cases {
let import_map = import_map ::ImportMap ::new (
ModuleSpecifier ::parse ( " file:///deno.json " ) . unwrap ( ) ,
) ;
let specifier = ModuleSpecifier ::parse ( " file:///file.ts " ) . unwrap ( ) ;
let err = import_map . resolve ( input , & specifier ) . err ( ) . unwrap ( ) ;
let err = ResolutionError ::ResolverError {
2023-10-20 00:02:08 -04:00
error : Arc ::new ( ResolveError ::Other ( err . into ( ) ) ) ,
2023-01-27 17:36:23 -05:00
specifier : input . to_string ( ) ,
range : Range {
specifier ,
start : Position ::zeroed ( ) ,
end : Position ::zeroed ( ) ,
} ,
} ;
assert_eq! ( get_resolution_error_bare_node_specifier ( & err ) , output ) ;
}
}
#[ test ]
fn bare_specifier_node_resolution_error ( ) {
let cases = vec! [ ( " process " , Some ( " process " ) ) , ( " other " , None ) ] ;
for ( input , output ) in cases {
let specifier = ModuleSpecifier ::parse ( " file:///file.ts " ) . unwrap ( ) ;
let err = ResolutionError ::InvalidSpecifier {
range : Range {
specifier ,
start : Position ::zeroed ( ) ,
end : Position ::zeroed ( ) ,
} ,
error : SpecifierError ::ImportPrefixMissing ( input . to_string ( ) , None ) ,
} ;
assert_eq! ( get_resolution_error_bare_node_specifier ( & err ) , output , ) ;
}
}
2023-01-24 15:14:49 -05:00
}