Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_resolver_reference_index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"bindings/devup-ui-binding/package.json": "Patch"
},
"note": "Resolving the imports of a file parses it once to find where it references each module, instead of once per import resolved. A component with ten imports was parsed again ten times during extraction just to locate possible resolver errors; diagnostics are unchanged",
"date": "2026-11-15T00:00:00.000Z"
}
45 changes: 24 additions & 21 deletions libs/binding/src/resolver_state.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::cell::OnceCell;
use std::collections::{BTreeSet, HashMap};

use extractor::ResolvedModule;
use extractor::module_reference::{ModuleReferenceLocation, original_module_reference_locations};
use extractor::module_reference::{ModuleReferenceLocation, original_module_references};

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
struct OriginSite {
Expand All @@ -18,6 +19,18 @@ struct OriginSites {
struct ModuleSource {
code: String,
ingress: Option<OriginSites>,
/// Where the code references each module, found on the first request
references: OnceCell<HashMap<String, Vec<ModuleReferenceLocation>>>,
}

impl ModuleSource {
fn new(code: &str, ingress: Option<OriginSites>) -> Self {
Self {
code: code.to_string(),
ingress,
references: OnceCell::new(),
}
}
}

/// A callback request with at least one original reference site.
Expand Down Expand Up @@ -55,44 +68,34 @@ pub struct ResolverState {
impl ResolverState {
pub fn new(filename: &str, source: &str) -> Self {
Self {
sources: HashMap::from([(
filename.to_string(),
ModuleSource {
code: source.to_string(),
ingress: None,
},
)]),
sources: HashMap::from([(filename.to_string(), ModuleSource::new(source, None))]),
failures: BTreeSet::new(),
}
}

pub fn cache(&mut self, module: &ResolvedModule, request: &ResolverRequest) {
self.sources.insert(
module.path.clone(),
ModuleSource {
code: module.code.clone(),
ingress: Some(request.origins.clone()),
},
ModuleSource::new(&module.code, Some(request.origins.clone())),
);
}

pub fn add_source(&mut self, filename: &str, code: &str) {
self.sources.insert(
filename.to_string(),
ModuleSource {
code: code.to_string(),
ingress: None,
},
);
self.sources
.insert(filename.to_string(), ModuleSource::new(code, None));
}

/// Bind original references before calling JS; recursive exploratory reads
/// without a semantic reference retain the real import that loaded the module.
pub fn request(&self, specifier: &str, importer: &str) -> Option<ResolverRequest> {
let source = self.sources.get(importer)?;
let mut sites = original_module_reference_locations(importer, &source.code, specifier)
let mut sites = source
.references
.get_or_init(|| original_module_references(importer, &source.code))
.get(specifier)
.into_iter()
.map(|location| OriginSite {
.flatten()
.map(|&location| OriginSite {
filename: importer.to_string(),
location,
});
Expand Down
83 changes: 55 additions & 28 deletions libs/extractor/src/module_reference.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashMap;

use oxc_allocator::Allocator;
use oxc_ast::{
AstKind,
Expand All @@ -24,6 +26,18 @@ pub fn original_module_reference_locations(
source: &str,
specifier: &str,
) -> Vec<ModuleReferenceLocation> {
original_module_references(filename, source)
.remove(specifier)
.unwrap_or_default()
}

/// The locations of every module the source references, by specifier, from one
/// parse: what `original_module_reference_locations` finds for each of them.
#[must_use]
pub fn original_module_references(
filename: &str,
source: &str,
) -> HashMap<String, Vec<ModuleReferenceLocation>> {
let allocator = Allocator::default();
let source_type = SourceType::from_path(filename).unwrap_or_else(|_| SourceType::ts());
let parsed = Parser::new(&allocator, source, source_type).parse();
Expand All @@ -36,10 +50,13 @@ pub fn original_module_reference_locations(
Statement::ExportAllDeclaration(export) => Some(&export.source),
_ => None,
};
if let Some(literal) = literal
&& literal.value == specifier
{
offsets.push(literal.span.start);
if let Some(literal) = literal {
offsets.extend(
literal
.value
.as_str()
.map(|value| (literal.span.start, value)),
);
}
}
let semantic = SemanticBuilder::new()
Expand All @@ -55,39 +72,49 @@ pub fn original_module_reference_locations(
let node = semantic.scoping().get_reference(*reference).node_id();
if let AstKind::CallExpression(call) = semantic.nodes().parent_kind(node)
&& let [Argument::StringLiteral(literal)] = call.arguments.as_slice()
&& literal.value == specifier
{
offsets.push(literal.span.start);
offsets.extend(
literal
.value
.as_str()
.map(|value| (literal.span.start, value)),
);
}
}
}
offsets.sort_unstable();
offsets.dedup();
offsets
.into_iter()
.filter_map(|offset| {
let prefix = source.get(..usize::try_from(offset).ok()?)?;
let mut location = ModuleReferenceLocation { line: 1, column: 1 };
let mut chars = prefix.chars().peekable();
while let Some(character) = chars.next() {
match character {
'\r' => {
if chars.peek() == Some(&'\n') {
chars.next();
}
location.line += 1;
location.column = 1;
}
'\n' | '\u{2028}' | '\u{2029}' => {
location.line += 1;
location.column = 1;
let mut references = HashMap::<String, Vec<ModuleReferenceLocation>>::new();
let mut location = ModuleReferenceLocation { line: 1, column: 1 };
let mut chars = source.char_indices().peekable();
for (offset, specifier) in offsets {
let offset = usize::try_from(offset).unwrap_or(usize::MAX);
while let Some(&(index, character)) = chars.peek() {
if index >= offset {
break;
}
chars.next();
match character {
'\r' => {
if chars.peek().is_some_and(|&(_, next)| next == '\n') {
chars.next();
}
_ => location.column += 1,
location.line += 1;
location.column = 1;
}
'\n' | '\u{2028}' | '\u{2029}' => {
location.line += 1;
location.column = 1;
}
_ => location.column += 1,
}
Some(location)
})
.collect()
}
references
.entry(specifier.to_string())
.or_default()
.push(location);
}
references
}

#[cfg(test)]
Expand Down
30 changes: 30 additions & 0 deletions libs/extractor/src/module_reference_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,33 @@ fn locations_when_duplicate_specifiers_and_unicode_are_present() {
]
);
}

#[test]
fn references_index_every_specifier_from_one_parse() {
// Given
let source = "import { a } from './a';\nconst b = require('./b');\nexport * from './a';";
// When
let references = original_module_references("theme.ts", source);
// Then
assert_eq!(references.len(), 2);
assert_eq!(
references["./a"],
vec![
ModuleReferenceLocation {
line: 1,
column: 19
},
ModuleReferenceLocation {
line: 3,
column: 15
},
]
);
assert_eq!(
references["./b"],
vec![ModuleReferenceLocation {
line: 2,
column: 19
}]
);
}
Loading