Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449//! Document-level ES module graph loading and execution.//!//! Builds the dependency graph rooted at the page's `<script type="module">`//! elements, fetches each statically-imported module through the resource//! loader (reusing the cache and SOP enforcement of classic script loading),//! and executes the modules in dependency order so each module's exports are//! published before any dependent imports them.//!//! Linkage itself — turning `import`/`export` into registry reads and writes —//! lives in [`we_js::module`]. This loader is responsible only for fetching the//! graph and choosing an evaluation order.//!//! Specifier resolution handles relative (`./`, `../`, `/`) and absolute-URL//! specifiers directly, and bare specifiers (e.g. `react`) through the page's//! [`ImportMap`]. A bare specifier with no matching map entry is left//! unresolved; its imported bindings evaluate to `undefined`.//!//! Statically-discoverable dynamic-import targets — `import("<literal>")` calls//! whose specifier is a string literal — are fetched and executed eagerly as//! ordinary graph dependencies so the runtime `import()` resolves the module//! namespace from the populated registry (see [`we_js::dynamic_import`]).//! Runtime-computed specifiers are not preloaded and reject at call time.
use crate::import_map::ImportMap;use crate::loader::{Resource, ResourceLoader, ResourceRequestType};use std::collections::HashMap;use std::time::Instant;use we_js::ast::Program;use we_js::module;use we_js::parser::Parser;use we_js::vm::Vm;use we_url::{Origin, Url};
/// A top-level module script discovered on the page.pub struct EntryModule { /// Registry key under which the module publishes its exports. For an /// external module this is its resolved absolute URL; for an inline module /// it is a synthetic per-document key. pub key: String, /// Base URL used to resolve this module's import specifiers. For an inline /// module this is the document base URL. pub base: Url, /// The module source text. pub source: String,}
/// A parsed module plus the metadata needed to compile and link it.struct ModuleRecord { program: Program, /// Maps each import specifier *as written* to its resolved registry key. resolve: HashMap<String, String>, /// Resolved keys of this module's dependencies, in source order. deps: Vec<String>, /// Source byte length, used only for diagnostics. source_len: usize,}
fn trace_modules_enabled() -> bool { std::env::var_os("WE_TRACE_MODULES").is_some()}
/// Classify and resolve a module specifier against a base URL, consulting the/// page's import map.////// Returns `Some(url)` for relative (`/`, `./`, `../`) and absolute-URL/// specifiers, and for bare specifiers that the import map maps to an address./// Returns `None` for an unmapped bare specifier.fn resolve_specifier(specifier: &str, base: &Url, import_map: &ImportMap) -> Option<Url> { import_map.resolve(specifier, base)}
/// Fetch a module's source text through the resource loader.fn fetch_module_source(loader: &mut ResourceLoader, url: &Url, origin: &Origin) -> Option<String> { match loader.fetch_subresource_with_referrer_policy( url, origin, ResourceRequestType::Script, None, ) { Ok(Resource::Script { text, .. }) => Some(text), Ok(Resource::Other { data, .. }) => String::from_utf8(data).ok(), Ok(_) => { eprintln!("[module] unexpected resource type for {url}"); None } Err(e) => { eprintln!("[module] failed to fetch {url}: {e}"); None } }}
/// Build the module dependency graph, fetching transitively-imported modules.////// Records are keyed by registry key. Bare/unresolvable specifiers are skipped/// (they remain unbound at link time). A module that fails to parse is omitted,/// which leaves its importers' bindings unbound rather than aborting the page.fn build_graph( entries: &[EntryModule], loader: &mut ResourceLoader, origin: &Origin, import_map: &ImportMap,) -> HashMap<String, ModuleRecord> { let mut graph: HashMap<String, ModuleRecord> = HashMap::new(); let trace = trace_modules_enabled();
// Worklist of (key, base, source) modules still to be parsed and scanned. let mut pending: Vec<(String, Url, String)> = entries .iter() .map(|e| (e.key.clone(), e.base.clone(), e.source.clone())) .collect();
while let Some((key, base, source)) = pending.pop() { if graph.contains_key(&key) { continue; } let source_len = source.len(); let parse_started = Instant::now(); let program = match Parser::parse_module(&source) { Ok(p) => p, Err(e) => { eprintln!("[module] parse error in {key}: {e}"); continue; } }; if trace { eprintln!( "[module-trace] parsed {key} bytes={source_len} parse_ms={}", parse_started.elapsed().as_millis() ); }
let mut resolve = HashMap::new(); let mut deps = Vec::new(); for specifier in module::module_dependencies(&program) { if resolve.contains_key(&specifier) { continue; } if let Some(dep_url) = resolve_specifier(&specifier, &base, import_map) { let dep_key = dep_url.serialize(); resolve.insert(specifier.clone(), dep_key.clone()); if !deps.contains(&dep_key) { deps.push(dep_key.clone()); } if !graph.contains_key(&dep_key) { if let Some(dep_source) = fetch_module_source(loader, &dep_url, origin) { if trace { eprintln!( "[module-trace] fetched dep {dep_key} bytes={}", dep_source.len() ); } pending.push((dep_key, dep_url, dep_source)); } } } else { eprintln!( "[module] cannot resolve bare specifier {specifier:?} (no import map entry)" ); } }
// Eagerly fetch and execute statically-discoverable dynamic-import // targets so a later `import("<literal>")` resolves from the populated // module registry. These are sequenced as ordinary dependencies (they // run before the importing module) but, unlike static imports, are not // added to `resolve`: dynamic import reads the registry at call time // rather than binding names at link time. for specifier in module::dynamic_import_specifiers(&program) { if let Some(dep_url) = resolve_specifier(&specifier, &base, import_map) { let dep_key = dep_url.serialize(); if !deps.contains(&dep_key) { deps.push(dep_key.clone()); } if !graph.contains_key(&dep_key) { if let Some(dep_source) = fetch_module_source(loader, &dep_url, origin) { if trace { eprintln!( "[module-trace] fetched dynamic dep {dep_key} bytes={}", dep_source.len() ); } pending.push((dep_key, dep_url, dep_source)); } } } }
graph.insert( key, ModuleRecord { program, resolve, deps, source_len, }, ); }
graph}
/// Produce a dependency-first evaluation order via post-order DFS, visiting/// `entries` in document order. Cycles are broken by tracking in-progress keys;/// a module already on the current DFS path is not re-entered.fn evaluation_order(entries: &[EntryModule], graph: &HashMap<String, ModuleRecord>) -> Vec<String> { let mut order = Vec::new(); let mut visited: HashMap<String, ()> = HashMap::new(); let mut on_path: HashMap<String, ()> = HashMap::new();
fn visit( key: &str, graph: &HashMap<String, ModuleRecord>, visited: &mut HashMap<String, ()>, on_path: &mut HashMap<String, ()>, order: &mut Vec<String>, ) { if visited.contains_key(key) || on_path.contains_key(key) { return; } on_path.insert(key.to_string(), ()); if let Some(record) = graph.get(key) { for dep in &record.deps { visit(dep, graph, visited, on_path, order); } } on_path.remove(key); visited.insert(key.to_string(), ()); order.push(key.to_string()); }
for entry in entries { visit(&entry.key, graph, &mut visited, &mut on_path, &mut order); } order}
/// Load and execute the page's ES module graph in `vm`.////// Installs the module-linkage registry, fetches the transitive dependency/// graph rooted at `entries`, and evaluates every module in dependency order./// No-op when `entries` is empty.pub fn load_and_execute_modules( vm: &mut Vm, loader: &mut ResourceLoader, origin: &Origin, entries: Vec<EntryModule>, import_map: &ImportMap,) { if entries.is_empty() { return; }
module::init_module_linkage(vm);
let graph = build_graph(&entries, loader, origin, import_map); let order = evaluation_order(&entries, &graph); let trace = trace_modules_enabled();
if trace { eprintln!( "[module-trace] graph modules={} eval_order={}", graph.len(), order.len() ); }
for key in order { let Some(record) = graph.get(&key) else { continue; }; let compile_started = Instant::now(); match module::compile_module(&record.program, &key, &record.resolve) { Ok(func) => { if trace { eprintln!( "[module-trace] execute start {key} bytes={} deps={} compile_ms={}", record.source_len, record.deps.len(), compile_started.elapsed().as_millis() ); } let execute_started = Instant::now(); if let Err(e) = vm.execute(&func) { eprintln!("[module] runtime error in {key}: {e}"); } if trace { eprintln!( "[module-trace] execute end {key} execute_ms={}", execute_started.elapsed().as_millis() ); } } Err(e) => { eprintln!("[module] compile error in {key}: {e}"); } } }}
/// Eagerly load modules referenced by literal `import("<specifier>")` calls in/// a parsed classic or module program.////// The JS runtime resolves dynamic imports from the shared `__we_modules`/// registry. Document-level module scripts populate that registry through/// `load_and_execute_modules`; classic scripts can also contain dynamic imports,/// so the browser layer preloads their statically-discoverable targets here/// before the event loop settles the returned promises.pub fn preload_dynamic_imports_for_program( vm: &mut Vm, loader: &mut ResourceLoader, origin: &Origin, base: &Url, import_map: &ImportMap, program: &Program,) { let mut entries = Vec::new();
for specifier in module::dynamic_import_specifiers(program) { let Some(dep_url) = resolve_specifier(&specifier, base, import_map) else { eprintln!("[module] cannot resolve dynamic import specifier {specifier:?}"); continue; }; let dep_key = dep_url.serialize(); if entries .iter() .any(|entry: &EntryModule| entry.key == dep_key) { continue; } if let Some(source) = fetch_module_source(loader, &dep_url, origin) { entries.push(EntryModule { key: dep_key, base: dep_url, source, }); } }
load_and_execute_modules(vm, loader, origin, entries, import_map);}
#[cfg(test)]mod tests { use super::*;
fn url(s: &str) -> Url { Url::parse(s).unwrap() }
#[test] fn resolves_relative_specifiers() { let base = url("https://example.com/app/main.js"); let map = ImportMap::default(); assert_eq!( resolve_specifier("./util.js", &base, &map) .unwrap() .serialize(), "https://example.com/app/util.js" ); assert_eq!( resolve_specifier("../lib/x.js", &base, &map) .unwrap() .serialize(), "https://example.com/lib/x.js" ); assert_eq!( resolve_specifier("/root.js", &base, &map) .unwrap() .serialize(), "https://example.com/root.js" ); }
#[test] fn resolves_absolute_url_specifiers() { let base = url("https://example.com/app/main.js"); let map = ImportMap::default(); assert_eq!( resolve_specifier("https://cdn.test/m.js", &base, &map) .unwrap() .serialize(), "https://cdn.test/m.js" ); assert!(resolve_specifier("data:text/javascript,export const x=1", &base, &map).is_some()); }
#[test] fn bare_specifiers_unresolved_without_map() { let base = url("https://example.com/app/main.js"); let map = ImportMap::default(); assert!(resolve_specifier("react", &base, &map).is_none()); assert!(resolve_specifier("@scope/pkg", &base, &map).is_none()); assert!(resolve_specifier("lodash/get", &base, &map).is_none()); }
#[test] fn bare_specifier_resolved_through_import_map() { let base = url("https://example.com/app/main.js"); let doc_base = url("https://example.com/"); let map = ImportMap::parse( r#"{ "imports": { "react": "https://cdn.test/react.js" } }"#, &doc_base, ); assert_eq!( resolve_specifier("react", &base, &map).unwrap().serialize(), "https://cdn.test/react.js" ); }
#[test] fn evaluation_order_is_dependency_first() { // Two entry modules; the first depends on a shared library. let base = url("https://example.com/"); let entries = vec![ EntryModule { key: "https://example.com/a.js".into(), base: base.clone(), source: "import { x } from \"./lib.js\"; x;".into(), }, EntryModule { key: "https://example.com/b.js".into(), base: base.clone(), source: "export const y = 2;".into(), }, ]; let mut loader = ResourceLoader::new(); // lib.js is fetched from the network; offline it will be absent, so the // graph just records the edge. Build the graph and check ordering with a // synthetic record for lib.js inserted manually. let mut graph = build_graph(&entries, &mut loader, &base.origin(), &ImportMap::default()); // Inject lib.js so the order includes it before a.js. graph.insert( "https://example.com/lib.js".into(), ModuleRecord { program: Parser::parse_module("export const x = 1;").unwrap(), resolve: HashMap::new(), deps: Vec::new(), source_len: "export const x = 1;".len(), }, ); let order = evaluation_order(&entries, &graph); let pos = |k: &str| order.iter().position(|x| x == k); assert!(pos("https://example.com/lib.js") < pos("https://example.com/a.js")); assert!(pos("https://example.com/a.js").is_some()); assert!(pos("https://example.com/b.js").is_some()); }}