import glance import gleam/dict import gleam/list import gleam/result pub type Module { /// A Module is a wrapper of a glance.Module, but maintains some additional /// information such as the name of the package and the names of any dependencies /// of that module. Module(name: String, module: glance.Module, dependencies: List(String)) } pub type Package { /// A Package has a name and a collection of modules. Each module is named /// according to its import. A module can be converted to a filename ///(relative to the global source / directory) simply by appending '.gleam' Package(name: String, modules: dict.Dict(String, Module)) } pub type LoadError(a) { LoadError(a) ParseError( glance_error: glance.Error, module_name: String, module_content: String, ) } /// Load a package using just it's name and a loader function. The name is /// required to be identical to the entry point module for the package (which /// is normal in gleam packages). /// /// Recursively loads all imports in the entrypoint using the supplied /// loader function. pub fn load_package( package_name: String, loader: fn(String) -> Result(String, a), ) -> Result(Package, LoadError(a)) { let package = Package(package_name, dict.new()) load_module_recurse(package, [package_name], loader) } /// Given an existing Gleam Module and its name, parse its imports to determine /// what its dependencies are. Return a Glimpse Module that wraps the Gleam module. /// /// Note: If the returned Module has any dependencies, it is up to the caller to ensure /// they are loaded, along with their dependencies. pub fn load_module(module: glance.Module, name: String) -> Module { module.imports |> list.map(fn(import_def) { case import_def { glance.Definition(definition: import_, ..) -> import_.module } }) |> Module(name, module, _) } /// given a module and package, identify any dependencies in the module that /// are *not* present in the package, and return them as a list. /// /// The dependencies will be a list of strings like `gleam/io` or `glance` pub fn filter_new_dependencies(module: Module, package: Package) -> List(String) { module.dependencies |> list.filter(fn(dep) { !dict.has_key(package.modules, dep) }) } fn load_module_recurse( package: Package, modules: List(String), loader: fn(String) -> Result(String, a), ) -> Result(Package, LoadError(a)) { case modules { [] -> Ok(package) [module_name, ..rest] -> { case dict.has_key(package.modules, module_name) { True -> load_module_recurse(package, rest, loader) False -> { use module_content <- result.try( loader(module_name) |> result.map_error(LoadError), ) use glance_module <- result.try( glance.module(module_content) |> result.map_error(ParseError(_, module_name, module_content)), ) let glimpse_module = load_module(glance_module, module_name) let unknown_dependencies = filter_new_dependencies(glimpse_module, package) let recurse_package = Package( ..package, modules: dict.insert(package.modules, module_name, glimpse_module), ) load_module_recurse( recurse_package, list.append(unknown_dependencies, rest), loader, ) } } } } }