Skip to content

Commit 970cd6f

Browse files
ssrliveCopilot
andcommitted
Implement text and bytes typed module imports
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0cdf8da commit 970cd6f

File tree

10 files changed

+261
-119
lines changed

10 files changed

+261
-119
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ABC

ci/feature_probes/import-bytes.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// module
2+
3+
import("./import-bytes-target.bin", { with: { type: "bytes" } }).then(function(mod) {
4+
var value = mod && mod.default;
5+
if (
6+
value instanceof Uint8Array &&
7+
value.buffer instanceof ArrayBuffer &&
8+
value.buffer.immutable === true &&
9+
value.length === 4 &&
10+
value[0] === 65 &&
11+
value[1] === 66 &&
12+
value[2] === 67 &&
13+
value[3] === 10
14+
) {
15+
console.log("OK");
16+
}
17+
}).catch(function() {});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello from text

ci/feature_probes/import-text.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// module
2+
3+
import("./import-text-target.txt", { with: { type: "text" } }).then(function(mod) {
4+
if (mod && mod.default === "hello from text\n") {
5+
console.log("OK");
6+
}
7+
}).catch(function() {});

src/core/compiler.rs

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,10 @@ impl<'gc> Compiler<'gc> {
725725
}
726726

727727
/// Check if an import source refers to the current module (self-import)
728-
fn is_self_import(&self, source: &str) -> bool {
728+
fn is_self_import(&self, source: &str, import_type: Option<&str>) -> bool {
729+
if import_type.is_some() {
730+
return false;
731+
}
729732
if let Some(ref fname) = self.script_filename {
730733
let import_base = source.strip_prefix("./").unwrap_or(source);
731734
let self_base = std::path::Path::new(fname).file_name().and_then(|n| n.to_str()).unwrap_or("");
@@ -736,11 +739,10 @@ impl<'gc> Compiler<'gc> {
736739
}
737740

738741
/// Resolve an import specifier to a loaded module path, if available.
739-
fn resolve_import_path(&self, source: &str) -> Option<String> {
742+
fn resolve_import_path(&self, source: &str, import_type: Option<&str>) -> Option<String> {
740743
if let Some(ref fname) = self.script_filename {
741744
let base_path = std::path::Path::new(fname);
742-
let resolved = crate::core::resolve_module_path(source, base_path);
743-
let resolved_str = resolved.to_string_lossy().to_string();
745+
let resolved_str = crate::core::resolve_module_request_key(source, base_path, import_type);
744746
if self.loaded_module_exports.contains_key(&resolved_str) {
745747
Some(resolved_str)
746748
} else {
@@ -834,7 +836,7 @@ impl<'gc> Compiler<'gc> {
834836
let mut reexport_map: std::collections::HashMap<String, (String, String)> = std::collections::HashMap::new();
835837
for stmt in statements {
836838
if let StatementKind::Export(specs, _, Some(source)) = &*stmt.kind {
837-
if self.is_self_import(source) {
839+
if self.is_self_import(source, None) {
838840
continue; // Skip self-re-exports
839841
}
840842
for spec in specs {
@@ -848,7 +850,7 @@ impl<'gc> Compiler<'gc> {
848850
}
849851
crate::core::statement::ExportSpecifier::Star => {
850852
// export * from "module" — expand all exports (except default)
851-
if let Some(resolved) = self.resolve_import_path(source)
853+
if let Some(resolved) = self.resolve_import_path(source, None)
852854
&& let Some(exports) = self.loaded_module_exports.get(&resolved)
853855
{
854856
for export_name in exports.keys() {
@@ -868,16 +870,16 @@ impl<'gc> Compiler<'gc> {
868870
}
869871

870872
for stmt in statements {
871-
if let StatementKind::Import(specifiers, source) = &*stmt.kind
872-
&& self.is_self_import(source)
873+
if let StatementKind::Import(specifiers, source, import_type) = &*stmt.kind
874+
&& self.is_self_import(source, import_type.as_deref())
873875
{
874876
for spec in specifiers {
875877
match spec {
876878
ImportSpecifier::Named(name, alias) => {
877879
let local = alias.as_deref().unwrap_or(name).to_string();
878880
// Check if this is a re-export → redirect to loaded module
879881
if let Some((re_src, orig_name)) = reexport_map.get(name)
880-
&& let Some(resolved) = self.resolve_import_path(re_src)
882+
&& let Some(resolved) = self.resolve_import_path(re_src, None)
881883
{
882884
self.chunk.loaded_module_vars.insert(local.clone(), (resolved, orig_name.clone()));
883885
self.chunk.const_import_bindings.insert(local);
@@ -892,7 +894,7 @@ impl<'gc> Compiler<'gc> {
892894
ImportSpecifier::Default(local) => {
893895
// Check if default is a re-export
894896
if let Some((re_src, orig_name)) = reexport_map.get("default")
895-
&& let Some(resolved) = self.resolve_import_path(re_src)
897+
&& let Some(resolved) = self.resolve_import_path(re_src, None)
896898
{
897899
self.chunk.loaded_module_vars.insert(local.clone(), (resolved, orig_name.clone()));
898900
self.chunk.const_import_bindings.insert(local.clone());
@@ -922,7 +924,7 @@ impl<'gc> Compiler<'gc> {
922924

923925
// Include re-exported names in the namespace
924926
for (export_name, (re_src, orig_name)) in &reexport_map {
925-
if let Some(resolved) = self.resolve_import_path(re_src) {
927+
if let Some(resolved) = self.resolve_import_path(re_src, None) {
926928
let ns_reexport_key = format!("__ns_reexport_{}_{}", local, export_name);
927929
self.chunk
928930
.loaded_module_vars
@@ -954,7 +956,7 @@ impl<'gc> Compiler<'gc> {
954956
.collect();
955957

956958
for (export_name, (re_src, orig_name)) in &reexport_map {
957-
if let Some(resolved) = self.resolve_import_path(re_src) {
959+
if let Some(resolved) = self.resolve_import_path(re_src, None) {
958960
let ns_reexport_key = format!("__ns_reexport_{}_{}", local, export_name);
959961
self.chunk
960962
.loaded_module_vars
@@ -4379,7 +4381,7 @@ impl<'gc> Compiler<'gc> {
43794381
self.chunk.write_u16(idx);
43804382
}
43814383
}
4382-
StatementKind::Import(specifiers, source) => {
4384+
StatementKind::Import(specifiers, source, import_type) => {
43834385
let define_binding = |this: &mut Self, local_name: &str| {
43844386
this.emit_define_var(local_name);
43854387
};
@@ -4417,7 +4419,7 @@ impl<'gc> Compiler<'gc> {
44174419

44184420
for spec in specifiers {
44194421
// Check for self-import first
4420-
if self.is_self_import(source) {
4422+
if self.is_self_import(source, import_type.as_deref()) {
44214423
match spec {
44224424
ImportSpecifier::Named(name, alias) => {
44234425
let local = alias.as_deref().unwrap_or(name).to_string();
@@ -4585,7 +4587,7 @@ impl<'gc> Compiler<'gc> {
45854587
define_binding(self, local);
45864588
}
45874589
(_, ImportSpecifier::Namespace(local)) => {
4588-
if let Some(resolved_str) = self.resolve_import_path(source) {
4590+
if let Some(resolved_str) = self.resolve_import_path(source, import_type.as_deref()) {
45894591
// Resolved from loaded module — value injected at runtime.
45904592
// Still emit placeholder + define to keep stack balanced.
45914593
self.chunk.write_opcode(Opcode::NewObject);
@@ -4600,7 +4602,7 @@ impl<'gc> Compiler<'gc> {
46004602
define_binding(self, local);
46014603
}
46024604
(_, ImportSpecifier::DeferredNamespace(local)) => {
4603-
if let Some(resolved_str) = self.resolve_import_path(source) {
4605+
if let Some(resolved_str) = self.resolve_import_path(source, import_type.as_deref()) {
46044606
self.chunk.write_opcode(Opcode::NewObject);
46054607
self.chunk.write_byte(0);
46064608
self.chunk
@@ -4614,7 +4616,7 @@ impl<'gc> Compiler<'gc> {
46144616
define_binding(self, local);
46154617
}
46164618
(_, ImportSpecifier::Default(local)) => {
4617-
let resolved = self.resolve_import_path(source);
4619+
let resolved = self.resolve_import_path(source, import_type.as_deref());
46184620
if let Some(resolved_str) = resolved {
46194621
// Resolved from loaded module — value injected at runtime.
46204622
let idx = self.chunk.add_constant(Value::Undefined);
@@ -4633,7 +4635,7 @@ impl<'gc> Compiler<'gc> {
46334635
}
46344636
(_, ImportSpecifier::Named(name, alias)) => {
46354637
let local = alias.as_deref().unwrap_or(name).to_string();
4636-
let resolved = self.resolve_import_path(source);
4638+
let resolved = self.resolve_import_path(source, import_type.as_deref());
46374639
if let Some(resolved_str) = resolved {
46384640
// Resolved from loaded module — value injected at runtime.
46394641
let idx = self.chunk.add_constant(Value::Undefined);

src/core/mod.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ pub(crate) enum ModuleRequestPhase {
169169
pub(crate) struct ModuleRequest {
170170
pub specifier: String,
171171
pub phase: ModuleRequestPhase,
172+
pub import_type: Option<String>,
172173
}
173174

174175
/// Resolve a module specifier relative to a base path.
@@ -184,6 +185,19 @@ pub(crate) fn resolve_module_path(specifier: &str, base_path: &std::path::Path)
184185
spec_path.to_path_buf()
185186
}
186187

188+
pub(crate) fn module_request_key_from_resolved_path(resolved_path: &std::path::Path, import_type: Option<&str>) -> String {
189+
let resolved = resolved_path.to_string_lossy().to_string();
190+
match import_type {
191+
Some(import_type) => format!("{resolved}\0{import_type}"),
192+
None => resolved,
193+
}
194+
}
195+
196+
pub(crate) fn resolve_module_request_key(specifier: &str, base_path: &std::path::Path, import_type: Option<&str>) -> String {
197+
let resolved_path = resolve_module_path(specifier, base_path);
198+
module_request_key_from_resolved_path(&resolved_path, import_type)
199+
}
200+
187201
/// Remove `.` and resolve `..` components from a path without touching the filesystem.
188202
fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
189203
let mut result = std::path::PathBuf::new();
@@ -2367,7 +2381,7 @@ fn collect_module_declared_names(statements: &[Statement], names: &mut std::coll
23672381
names.insert(def.name.clone());
23682382
}
23692383
}
2370-
SK::Import(specs, _) => {
2384+
SK::Import(specs, _, _) => {
23712385
for spec in specs {
23722386
match spec {
23732387
crate::core::statement::ImportSpecifier::Default(n) => {
@@ -2570,9 +2584,9 @@ pub(crate) fn collect_module_sources(statements: &[Statement], self_basename: &s
25702584

25712585
for stmt in statements {
25722586
match &*stmt.kind {
2573-
StatementKind::Import(_, source) => {
2587+
StatementKind::Import(_, source, import_type) => {
25742588
let import_base = source.strip_prefix("./").unwrap_or(source);
2575-
if import_base == self_basename {
2589+
if import_type.is_none() && import_base == self_basename {
25762590
continue;
25772591
}
25782592
if known_builtins.contains(&source.as_str()) {
@@ -2604,29 +2618,30 @@ pub(crate) fn collect_module_requests(statements: &[Statement], self_basename: &
26042618

26052619
for stmt in statements {
26062620
let maybe_request = match &*stmt.kind {
2607-
StatementKind::Import(specifiers, source) => {
2621+
StatementKind::Import(specifiers, source, import_type) => {
26082622
let phase = if specifiers.iter().any(|spec| matches!(spec, ImportSpecifier::DeferredNamespace(_))) {
26092623
ModuleRequestPhase::Defer
26102624
} else {
26112625
ModuleRequestPhase::Evaluation
26122626
};
2613-
Some((source, phase))
2627+
Some((source, phase, import_type))
26142628
}
2615-
StatementKind::Export(_specs, _, Some(source)) => Some((source, ModuleRequestPhase::Evaluation)),
2629+
StatementKind::Export(_specs, _, Some(source)) => Some((source, ModuleRequestPhase::Evaluation, &None)),
26162630
_ => None,
26172631
};
26182632

2619-
let Some((source, phase)) = maybe_request else {
2633+
let Some((source, phase, import_type)) = maybe_request else {
26202634
continue;
26212635
};
26222636
let import_base = source.strip_prefix("./").unwrap_or(source);
2623-
if import_base == self_basename || known_builtins.contains(&source.as_str()) {
2637+
if (import_type.is_none() && import_base == self_basename) || known_builtins.contains(&source.as_str()) {
26242638
continue;
26252639
}
2626-
if seen.insert((source.clone(), phase)) {
2640+
if seen.insert((source.clone(), phase, import_type.clone())) {
26272641
requests.push(ModuleRequest {
26282642
specifier: source.clone(),
26292643
phase,
2644+
import_type: import_type.clone(),
26302645
});
26312646
}
26322647
}

src/core/parser.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2683,7 +2683,7 @@ fn parse_import_statement(t: &[TokenData], index: &mut usize) -> Result<Statemen
26832683
}
26842684
}
26852685
}
2686-
consume_import_attributes_clause(t, index)?;
2686+
let import_type = consume_import_attributes_clause(t, index)?;
26872687
if *index < t.len() && matches!(t[*index].token, Token::Semicolon) {
26882688
*index += 1;
26892689
}
@@ -2716,19 +2716,19 @@ fn parse_import_statement(t: &[TokenData], index: &mut usize) -> Result<Statemen
27162716
}
27172717
}
27182718
Ok(Statement {
2719-
kind: Box::new(StatementKind::Import(specifiers, source)),
2719+
kind: Box::new(StatementKind::Import(specifiers, source, import_type)),
27202720
line: t[start].line,
27212721
column: t[start].column,
27222722
})
27232723
}
2724-
fn consume_import_attributes_clause(t: &[TokenData], index: &mut usize) -> Result<(), JSError> {
2724+
fn consume_import_attributes_clause(t: &[TokenData], index: &mut usize) -> Result<Option<String>, JSError> {
27252725
if *index >= t.len() {
2726-
return Ok(());
2726+
return Ok(None);
27272727
}
27282728
let is_with_clause = (matches!(t[*index].token, Token::With) || matches!(&t[*index].token, Token::Identifier(s) if s == "with"))
27292729
&& !raw_identifier_source_has_escape(&t[*index]);
27302730
if !is_with_clause {
2731-
return Ok(());
2731+
return Ok(None);
27322732
}
27332733
*index += 1;
27342734
while *index < t.len() && matches!(t[*index].token, Token::LineTerminator) {
@@ -2739,6 +2739,7 @@ fn consume_import_attributes_clause(t: &[TokenData], index: &mut usize) -> Resul
27392739
}
27402740
*index += 1; // skip '{'
27412741
let mut seen_keys: Vec<String> = Vec::new();
2742+
let mut import_type = None;
27422743
loop {
27432744
while *index < t.len() && matches!(t[*index].token, Token::LineTerminator) {
27442745
*index += 1;
@@ -2748,7 +2749,7 @@ fn consume_import_attributes_clause(t: &[TokenData], index: &mut usize) -> Resul
27482749
}
27492750
if matches!(t[*index].token, Token::RBrace) {
27502751
*index += 1;
2751-
return Ok(());
2752+
return Ok(import_type);
27522753
}
27532754
// Parse attribute key: IdentifierName or StringLiteral
27542755
let key = match &t[*index].token {
@@ -2774,7 +2775,7 @@ fn consume_import_attributes_clause(t: &[TokenData], index: &mut usize) -> Resul
27742775
if seen_keys.contains(&key) {
27752776
return Err(raise_syntax_error!(format!("Duplicate import attribute key '{}'", key)));
27762777
}
2777-
seen_keys.push(key);
2778+
seen_keys.push(key.clone());
27782779
while *index < t.len() && matches!(t[*index].token, Token::LineTerminator) {
27792780
*index += 1;
27802781
}
@@ -2790,6 +2791,14 @@ fn consume_import_attributes_clause(t: &[TokenData], index: &mut usize) -> Resul
27902791
if *index >= t.len() || !matches!(t[*index].token, Token::StringLit(_)) {
27912792
return Err(raise_parse_error!("Import attribute value must be a string"));
27922793
}
2794+
let value = if let Token::StringLit(s) = &t[*index].token {
2795+
utf16_to_utf8(s)
2796+
} else {
2797+
unreachable!()
2798+
};
2799+
if key == "type" {
2800+
import_type = Some(value);
2801+
}
27932802
*index += 1;
27942803
while *index < t.len() && matches!(t[*index].token, Token::LineTerminator) {
27952804
*index += 1;
@@ -2953,7 +2962,7 @@ fn parse_export_statement(t: &[TokenData], index: &mut usize) -> Result<Statemen
29532962
}
29542963
}
29552964
}
2956-
consume_import_attributes_clause(t, index)?;
2965+
let _ = consume_import_attributes_clause(t, index)?;
29572966
finish_statement_without_semicolon(t, index)?;
29582967
} else if *index < t.len() && matches!(t[*index].token, Token::LBrace) {
29592968
*index += 1;
@@ -3036,7 +3045,7 @@ fn parse_export_statement(t: &[TokenData], index: &mut usize) -> Result<Statemen
30363045
"A string literal cannot be used as an exported binding without `from`"
30373046
));
30383047
}
3039-
consume_import_attributes_clause(t, index)?;
3048+
let _ = consume_import_attributes_clause(t, index)?;
30403049
finish_statement_without_semicolon(t, index)?;
30413050
} else {
30423051
let stmt = match t[*index].token {

src/core/statement.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ pub enum StatementKind {
4949
Continue(Option<String>),
5050
Debugger,
5151
Label(String, Box<Statement>),
52-
Import(Vec<ImportSpecifier>, String), // import specifiers, module name
52+
Import(Vec<ImportSpecifier>, String, Option<String>), // import specifiers, module name, optional import type
5353
Export(Vec<ExportSpecifier>, Option<Box<Statement>>, Option<String>), // export specifiers, optional inner declaration, optional source
54-
Using(Vec<(String, Expr)>), // using declarations: using x = expr, y = expr;
55-
AwaitUsing(Vec<(String, Expr)>), // await using declarations: await using x = expr;
54+
Using(Vec<(String, Expr)>), // using declarations: using x = expr, y = expr;
55+
AwaitUsing(Vec<(String, Expr)>), // await using declarations: await using x = expr;
5656
}
5757

5858
#[derive(Clone, Copy, Debug, PartialEq)]

0 commit comments

Comments
 (0)