2023-01-02 16:00:42 -05:00
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
2021-12-16 05:45:41 -05:00
2023-02-09 22:00:23 -05:00
use crate ::args ::CliOptions ;
2022-12-06 14:12:51 -05:00
use crate ::args ::Lockfile ;
2023-02-24 14:42:45 -05:00
use crate ::args ::TsTypeLib ;
2022-12-09 09:40:48 -05:00
use crate ::args ::TypeCheckMode ;
use crate ::cache ;
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 ;
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 ;
2022-06-28 16:45:55 -04:00
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 ;
2023-02-24 14:42:45 -05:00
use deno_core ::parking_lot ::RwLock ;
2021-12-16 05:45:41 -05:00
use deno_core ::ModuleSpecifier ;
2023-03-04 20:07:11 -05:00
use deno_core ::TaskQueue ;
use deno_core ::TaskQueuePermit ;
2023-04-14 16:22:33 -04:00
use deno_graph ::source ::Loader ;
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-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-01-27 17:36:23 -05:00
use import_map ::ImportMapError ;
2023-02-24 14:42:45 -05:00
use std ::collections ::HashMap ;
use std ::collections ::HashSet ;
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 ,
}
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` and not a dynamic import.
pub fn graph_valid_with_cli_options (
graph : & ModuleGraph ,
roots : & [ ModuleSpecifier ] ,
options : & CliOptions ,
) -> Result < ( ) , AnyError > {
graph_valid (
graph ,
roots ,
2023-02-15 11:30:54 -05:00
GraphValidOptions {
is_vendoring : false ,
2023-02-09 22:00:23 -05:00
follow_type_only : options . type_check_mode ( ) ! = TypeCheckMode ::None ,
check_js : options . check_js ( ) ,
} ,
)
2021-12-16 05:45:41 -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 ,
roots : & [ ModuleSpecifier ] ,
2023-02-15 11:30:54 -05:00
options : GraphValidOptions ,
2023-02-09 22:00:23 -05:00
) -> Result < ( ) , AnyError > {
2023-02-15 11:30:54 -05:00
let mut errors = graph
. walk (
roots ,
deno_graph ::WalkOptions {
check_js : options . check_js ,
follow_type_only : options . follow_type_only ,
follow_dynamic : options . is_vendoring ,
} ,
)
. errors ( )
. flat_map ( | error | {
let is_root = match & error {
ModuleGraphError ::ResolutionError ( _ ) = > false ,
2023-03-21 11:46:40 -04:00
ModuleGraphError ::ModuleError ( error ) = > {
roots . contains ( error . specifier ( ) )
}
2023-02-15 11:30:54 -05:00
} ;
let mut message = if let ModuleGraphError ::ResolutionError ( err ) = & error {
enhanced_resolution_error_message ( err )
} else {
format! ( " {error} " )
} ;
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-03-26 19:10:47 -04:00
message . push_str ( & format! (
" \n at {}:{}:{} " ,
colors ::cyan ( range . specifier . as_str ( ) ) ,
colors ::yellow ( & ( range . start . line + 1 ) . to_string ( ) ) ,
colors ::yellow ( & ( range . start . character + 1 ) . to_string ( ) )
) ) ;
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 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 ( _ , _ ) )
) {
2023-02-15 11:30:54 -05:00
log ::warn! ( " Ignoring: {:#} " , message ) ;
return None ;
}
// ignore invalid downgrades and invalid local imports when vendoring
if let ModuleGraphError ::ResolutionError ( err ) = & error {
if matches! (
err ,
ResolutionError ::InvalidDowngrade { .. }
| ResolutionError ::InvalidLocalImport { .. }
) {
return None ;
}
}
}
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
}
2022-12-06 14:12:51 -05:00
/// Checks the lockfile against the graph and and exits on errors.
pub fn graph_lock_or_exit ( graph : & ModuleGraph , lockfile : & mut Lockfile ) {
for module in graph . modules ( ) {
2023-02-22 14:15:25 -05:00
let source = match module {
Module ::Esm ( module ) = > & module . source ,
Module ::Json ( module ) = > & module . source ,
Module ::Node ( _ ) | Module ::Npm ( _ ) | Module ::External ( _ ) = > continue ,
} ;
if ! lockfile . check_or_insert_remote ( module . specifier ( ) . as_str ( ) , source ) {
let err = format! (
concat! (
" The source code is invalid, as it does not match the expected hash in the lock file. \n " ,
" Specifier: {} \n " ,
" Lock file: {} " ,
) ,
module . specifier ( ) ,
lockfile . filename . display ( ) ,
) ;
log ::error! ( " {} {} " , colors ::red ( " error: " ) , err ) ;
std ::process ::exit ( 10 ) ;
2022-12-06 14:12:51 -05:00
}
2021-12-16 05:45:41 -05:00
}
}
2022-12-09 09:40:48 -05:00
2023-04-14 16:22:33 -04:00
pub struct ModuleGraphBuilder {
options : Arc < CliOptions > ,
resolver : Arc < CliGraphResolver > ,
2023-04-21 21:02:46 -04:00
npm_resolver : Arc < CliNpmResolver > ,
2023-04-14 16:22:33 -04:00
parsed_source_cache : Arc < ParsedSourceCache > ,
lockfile : Option < Arc < Mutex < Lockfile > > > ,
emit_cache : cache ::EmitCache ,
file_fetcher : Arc < FileFetcher > ,
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
2023-04-14 16:22:33 -04:00
impl ModuleGraphBuilder {
#[ allow(clippy::too_many_arguments) ]
pub fn new (
options : Arc < CliOptions > ,
resolver : Arc < CliGraphResolver > ,
2023-04-21 21:02:46 -04:00
npm_resolver : Arc < CliNpmResolver > ,
2023-04-14 16:22:33 -04:00
parsed_source_cache : Arc < ParsedSourceCache > ,
lockfile : Option < Arc < Mutex < Lockfile > > > ,
emit_cache : cache ::EmitCache ,
file_fetcher : Arc < FileFetcher > ,
2023-04-14 18:05:46 -04:00
type_checker : Arc < TypeChecker > ,
2023-04-14 16:22:33 -04:00
) -> Self {
Self {
options ,
resolver ,
npm_resolver ,
parsed_source_cache ,
lockfile ,
emit_cache ,
file_fetcher ,
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-04-14 16:22:33 -04:00
pub async fn create_graph_with_loader (
& self ,
roots : Vec < ModuleSpecifier > ,
loader : & mut dyn Loader ,
) -> Result < deno_graph ::ModuleGraph , AnyError > {
let maybe_imports = self . options . to_maybe_imports ( ) ? ;
let cli_resolver = self . resolver . clone ( ) ;
let graph_resolver = cli_resolver . as_graph_resolver ( ) ;
let graph_npm_resolver = cli_resolver . as_graph_npm_resolver ( ) ;
let analyzer = self . parsed_source_cache . as_analyzer ( ) ;
let mut graph = ModuleGraph ::default ( ) ;
self
. build_graph_with_npm_resolution (
& mut graph ,
roots ,
loader ,
deno_graph ::BuildOptions {
is_dynamic : false ,
imports : maybe_imports ,
resolver : Some ( graph_resolver ) ,
npm_resolver : Some ( graph_npm_resolver ) ,
module_analyzer : Some ( & * analyzer ) ,
reporter : None ,
} ,
)
. await ? ;
if graph . has_node_specifier
& & self . options . type_check_mode ( ) ! = TypeCheckMode ::None
{
self
. npm_resolver
2023-01-24 09:05:54 -05:00
. inject_synthetic_types_node_package ( )
. await ? ;
}
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 > {
let mut cache = self . create_graph_loader ( ) ;
let maybe_imports = self . options . to_maybe_imports ( ) ? ;
let cli_resolver = self . resolver . clone ( ) ;
let graph_resolver = cli_resolver . as_graph_resolver ( ) ;
let graph_npm_resolver = cli_resolver . as_graph_npm_resolver ( ) ;
let analyzer = self . parsed_source_cache . as_analyzer ( ) ;
let mut graph = ModuleGraph ::default ( ) ;
self
. build_graph_with_npm_resolution (
& mut graph ,
roots ,
& mut cache ,
deno_graph ::BuildOptions {
is_dynamic : false ,
imports : maybe_imports ,
resolver : Some ( graph_resolver ) ,
npm_resolver : Some ( graph_npm_resolver ) ,
module_analyzer : Some ( & * analyzer ) ,
reporter : None ,
} ,
)
. await ? ;
let graph = Arc ::new ( graph ) ;
2023-04-14 18:05:46 -04:00
graph_valid_with_cli_options ( & graph , & graph . roots , & self . options ) ? ;
2023-04-14 16:22:33 -04:00
if let Some ( lockfile ) = & self . lockfile {
graph_lock_or_exit ( & graph , & mut lockfile . lock ( ) ) ;
2022-12-09 09:40:48 -05:00
}
2023-04-14 16:22:33 -04:00
if self . options . type_check_mode ( ) ! = TypeCheckMode ::None {
2023-04-14 18:05:46 -04:00
self
. type_checker
. check (
graph . clone ( ) ,
check ::CheckOptions {
2023-04-14 16:22:33 -04:00
lib : self . options . ts_type_lib_window ( ) ,
2023-04-14 18:05:46 -04:00
log_ignored_options : true ,
reload : self . options . reload_flag ( ) ,
} ,
)
. await ? ;
2022-12-09 09:40:48 -05:00
}
2023-04-14 16:22:33 -04:00
Ok ( graph )
2022-12-09 09:40:48 -05:00
}
2023-04-14 16:22:33 -04:00
pub async fn build_graph_with_npm_resolution < ' a > (
& self ,
graph : & mut ModuleGraph ,
roots : Vec < ModuleSpecifier > ,
loader : & mut dyn deno_graph ::source ::Loader ,
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 ) {
self . resolver . force_top_level_package_json_install ( ) . await ? ;
}
2023-04-14 16:22:33 -04:00
graph . build ( roots , loader , options ) . await ;
2022-12-09 09:40:48 -05:00
2023-04-14 16:22:33 -04:00
// ensure that the top level package.json is installed if a
// specifier was matched in the package.json
self
. resolver
. top_level_package_json_install_if_necessary ( )
. await ? ;
2023-02-22 14:15:25 -05:00
2023-04-14 16:22:33 -04:00
// resolve the dependencies of any pending dependencies
// that were inserted by building the graph
self . npm_resolver . resolve_pending ( ) . await ? ;
2023-04-11 18:10:51 -04:00
2023-04-14 16:22:33 -04:00
Ok ( ( ) )
}
2023-02-22 14:15:25 -05:00
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-04-26 16:23:28 -04:00
permissions ,
2023-04-14 16:22:33 -04:00
self . options . node_modules_dir_specifier ( ) ,
)
}
pub async fn create_graph (
& self ,
roots : Vec < ModuleSpecifier > ,
) -> Result < deno_graph ::ModuleGraph , AnyError > {
let mut cache = self . create_graph_loader ( ) ;
self . create_graph_with_loader ( roots , & mut cache ) . await
}
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
}
Module ::Esm ( _ ) | Module ::Json ( _ ) | Module ::External ( _ ) = > { }
}
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 ) {
message . push_str ( & format! (
" \n If you want to use a built-in Node module, add a \" node: \" prefix (ex. \" node:{specifier} \" ). "
) ) ;
}
message
}
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 {
if let Some ( ImportMapError ::UnmappedBareSpecifier ( specifier , _ ) ) =
error . downcast_ref ::< ImportMapError > ( )
{
Some ( specifier . as_str ( ) )
} 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-02-24 14:42:45 -05:00
#[ derive(Default, Debug) ]
struct GraphData {
graph : Arc < ModuleGraph > ,
checked_libs : HashMap < TsTypeLib , HashSet < ModuleSpecifier > > ,
}
/// Holds the `ModuleGraph` and what parts of it are type checked.
2023-04-14 16:22:33 -04:00
#[ derive(Default) ]
2023-02-24 14:42:45 -05:00
pub struct ModuleGraphContainer {
2023-03-04 20:07:11 -05:00
// Allow only one request to update the graph data at a time,
// but allow other requests to read from it at any time even
// while another request is updating the data.
update_queue : Arc < TaskQueue > ,
2023-02-24 14:42:45 -05:00
graph_data : Arc < RwLock < GraphData > > ,
}
impl ModuleGraphContainer {
2023-04-14 16:22:33 -04:00
pub fn clear ( & self ) {
self . graph_data . write ( ) . graph = Default ::default ( ) ;
}
2023-02-24 14:42:45 -05:00
/// Acquires a permit to modify the module graph without other code
/// having the chance to modify it. In the meantime, other code may
/// still read from the existing module graph.
pub async fn acquire_update_permit ( & self ) -> ModuleGraphUpdatePermit {
2023-03-04 20:07:11 -05:00
let permit = self . update_queue . acquire ( ) . await ;
2023-02-24 14:42:45 -05:00
ModuleGraphUpdatePermit {
permit ,
graph_data : self . graph_data . clone ( ) ,
graph : ( * self . graph_data . read ( ) . graph ) . clone ( ) ,
}
}
pub fn graph ( & self ) -> Arc < ModuleGraph > {
self . graph_data . read ( ) . graph . clone ( )
}
/// Mark `roots` and all of their dependencies as type checked under `lib`.
/// Assumes that all of those modules are known.
pub fn set_type_checked ( & self , roots : & [ ModuleSpecifier ] , lib : TsTypeLib ) {
// It's ok to analyze and update this while the module graph itself is
// being updated in a permit because the module graph update is always
// additive and this will be a subset of the original graph
let graph = self . graph ( ) ;
let entries = graph . walk (
roots ,
deno_graph ::WalkOptions {
check_js : true ,
follow_dynamic : true ,
follow_type_only : true ,
} ,
) ;
// now update
let mut data = self . graph_data . write ( ) ;
let checked_lib_set = data . checked_libs . entry ( lib ) . or_default ( ) ;
for ( specifier , _ ) in entries {
checked_lib_set . insert ( specifier . clone ( ) ) ;
}
}
/// Check if `roots` are all marked as type checked under `lib`.
pub fn is_type_checked (
& self ,
roots : & [ ModuleSpecifier ] ,
lib : TsTypeLib ,
) -> bool {
let data = self . graph_data . read ( ) ;
match data . checked_libs . get ( & lib ) {
Some ( checked_lib_set ) = > roots . iter ( ) . all ( | r | {
let found = data . graph . resolve ( r ) ;
checked_lib_set . contains ( & found )
} ) ,
None = > false ,
}
}
}
/// A permit for updating the module graph. When complete and
/// everything looks fine, calling `.commit()` will store the
/// new graph in the ModuleGraphContainer.
pub struct ModuleGraphUpdatePermit < ' a > {
2023-03-04 20:07:11 -05:00
permit : TaskQueuePermit < ' a > ,
2023-02-24 14:42:45 -05:00
graph_data : Arc < RwLock < GraphData > > ,
graph : ModuleGraph ,
}
impl < ' a > ModuleGraphUpdatePermit < ' a > {
/// Gets the module graph for mutation.
pub fn graph_mut ( & mut self ) -> & mut ModuleGraph {
& mut self . graph
}
/// Saves the mutated module graph in the container
/// and returns an Arc to the new module graph.
pub fn commit ( self ) -> Arc < ModuleGraph > {
let graph = Arc ::new ( self . graph ) ;
self . graph_data . write ( ) . graph = graph . clone ( ) ;
drop ( self . permit ) ; // explicit drop for clarity
graph
}
}
2023-01-27 17:36:23 -05:00
#[ cfg(test) ]
mod test {
use std ::sync ::Arc ;
use deno_ast ::ModuleSpecifier ;
use deno_graph ::Position ;
use deno_graph ::Range ;
use deno_graph ::ResolutionError ;
use deno_graph ::SpecifierError ;
use crate ::graph_util ::get_resolution_error_bare_node_specifier ;
#[ 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 {
error : Arc ::new ( err . into ( ) ) ,
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
}