Skip to content

Commit

Permalink
feat(turbopack-ecmascript): use import attributes for annotations (ve…
Browse files Browse the repository at this point in the history
  • Loading branch information
ForsakenHarmony authored May 16, 2024
1 parent c3b2ffd commit 609181e
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 151 deletions.
7 changes: 7 additions & 0 deletions crates/turbopack-core/src/reference_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,18 @@ pub enum CommonJsReferenceSubType {
Undefined,
}

#[turbo_tasks::value(serialization = "auto_for_input")]
#[derive(Debug, Clone, PartialOrd, Ord, Hash)]
pub enum ImportWithType {
Json,
}

#[turbo_tasks::value(serialization = "auto_for_input")]
#[derive(Debug, Default, Clone, PartialOrd, Ord, Hash)]
pub enum EcmaScriptModulesReferenceSubType {
ImportPart(Vc<ModulePart>),
Import,
ImportWithType(ImportWithType),
DynamicImport,
Custom(u8),
#[default]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,33 @@ use swc_core::{
common::DUMMY_SP,
ecma::{
ast::{
Expr, ExprStmt, Ident, ImportDecl, ImportSpecifier, ImportStarAsSpecifier,
KeyValueProp, Lit, Module, ModuleDecl, ModuleItem, ObjectLit, Program, Prop, PropName,
PropOrSpread, Stmt, Str,
ImportDecl, ImportSpecifier, ImportStarAsSpecifier, Module, ModuleDecl, ModuleItem,
Program,
},
utils::private_ident,
},
quote,
};
use turbopack_ecmascript::TURBOPACK_HELPER;
use turbopack_ecmascript::{
annotations::{with_clause, ANNOTATION_TRANSITION},
TURBOPACK_HELPER,
};

pub fn create_proxy_module(transition_name: &str, target_import: &str) -> Program {
let ident = private_ident!("clientProxy");
Program::Module(Module {
body: vec![
ModuleItem::Stmt(Stmt::Expr(ExprStmt {
expr: Box::new(Expr::Lit(Lit::Str(Str {
value: format!("TURBOPACK {{ transition: {transition_name} }}").into(),
raw: None,
span: DUMMY_SP,
}))),
span: DUMMY_SP,
})),
ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
specifiers: vec![ImportSpecifier::Namespace(ImportStarAsSpecifier {
local: ident.clone(),
span: DUMMY_SP,
})],
src: Box::new(target_import.into()),
type_only: false,
with: Some(Box::new(ObjectLit {
span: DUMMY_SP,
props: vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(Ident::new(TURBOPACK_HELPER.into(), DUMMY_SP)),
value: Box::new(Expr::Lit(true.into())),
})))],
})),
with: Some(with_clause(&[
(TURBOPACK_HELPER.as_str(), "true"),
(ANNOTATION_TRANSITION, transition_name),
])),
span: DUMMY_SP,
phase: Default::default(),
})),
Expand Down
210 changes: 100 additions & 110 deletions crates/turbopack-ecmascript/src/analyzer/imports.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{collections::BTreeMap, fmt::Display, mem::take};
use std::{collections::BTreeMap, fmt::Display};

use indexmap::{IndexMap, IndexSet};
use once_cell::sync::Lazy;
Expand All @@ -14,64 +14,89 @@ use turbo_tasks::Vc;
use turbopack_core::{issue::IssueSource, source::Source};

use super::{JsValue, ModuleValue};
use crate::utils::unparen;

#[turbo_tasks::value(serialization = "auto_for_input")]
#[derive(Default, Debug, Clone, Hash, PartialOrd, Ord)]
pub struct ImportAnnotations {
// TODO store this in more structured way
#[turbo_tasks(trace_ignore)]
map: BTreeMap<JsWord, Option<JsWord>>,
map: BTreeMap<JsWord, JsWord>,
}

/// Enables a specified transtion for the annotated import
static ANNOTATION_TRANSITION: Lazy<JsWord> = Lazy::new(|| "transition".into());
/// Enables a specified transition for the annotated import
static ANNOTATION_TRANSITION: Lazy<JsWord> =
Lazy::new(|| crate::annotations::ANNOTATION_TRANSITION.into());

/// Changes the chunking type for the annotated import
static ANNOTATION_CHUNKING_TYPE: Lazy<JsWord> = Lazy::new(|| "chunking-type".into());
static ANNOTATION_CHUNKING_TYPE: Lazy<JsWord> =
Lazy::new(|| crate::annotations::ANNOTATION_CHUNKING_TYPE.into());

/// Changes the type of the resolved module (only "json" is supported currently)
static ATTRIBUTE_MODULE_TYPE: Lazy<JsWord> = Lazy::new(|| "type".into());

impl ImportAnnotations {
fn insert(&mut self, key: JsWord, value: Option<JsWord>) {
self.map.insert(key, value);
}
pub fn parse(with: Option<&ObjectLit>) -> ImportAnnotations {
let Some(with) = with else {
return ImportAnnotations::default();
};

let mut map = BTreeMap::new();

// The `with` clause is way more restrictive than `ObjectLit`, it only allows
// string -> value and value can only be a string.
// We just ignore everything else here till the SWC ast is more restrictive.
for (key, value) in with.props.iter().filter_map(|prop| {
let kv = prop.as_prop()?.as_key_value()?;

fn clear(&mut self) {
self.map.clear();
let Lit::Str(str) = kv.value.as_lit()? else {
return None;
};

Some((&kv.key, str))
}) {
let key = match key {
PropName::Ident(ident) => ident.sym.as_str(),
PropName::Str(str) => str.value.as_str(),
// the rest are invalid, ignore for now till SWC ast is correct
_ => continue,
};

map.insert(key.into(), value.value.as_str().into());
}

ImportAnnotations { map }
}

/// Returns the content on the transition annotation
pub fn transition(&self) -> Option<&str> {
self.map
.get(&ANNOTATION_TRANSITION)
.and_then(|w| w.as_ref().map(|w| &**w))
self.get(&ANNOTATION_TRANSITION)
}

/// Returns the content on the chunking-type annotation
pub fn chunking_type(&self) -> Option<&str> {
self.map
.get(&ANNOTATION_CHUNKING_TYPE)
.and_then(|w| w.as_ref().map(|w| &**w))
self.get(&ANNOTATION_CHUNKING_TYPE)
}

/// Returns the content on the type attribute
pub fn module_type(&self) -> Option<&str> {
self.get(&ATTRIBUTE_MODULE_TYPE)
}

pub fn get(&self, key: &JsWord) -> Option<&str> {
self.map.get(key).map(|w| w.as_str())
}
}

impl Display for ImportAnnotations {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut it = self.map.iter();
if let Some((k, v)) = it.next() {
if let Some(v) = v {
write!(f, "{{ {k}: {v}")?
} else {
write!(f, "{{ {k}")?
}
write!(f, "{{ {k}: {v}")?
} else {
return f.write_str("{}");
};
for (k, v) in it {
if let Some(v) = v {
write!(f, "; {k}: {v}")?
} else {
write!(f, "; {k}")?
}
write!(f, ", {k}: {v}")?
}
f.write_str(" }")
}
Expand Down Expand Up @@ -172,7 +197,6 @@ impl ImportMap {

m.visit_with(&mut Analyzer {
data: &mut data,
current_annotations: ImportAnnotations::default(),
source,
});

Expand All @@ -182,7 +206,6 @@ impl ImportMap {

struct Analyzer<'a> {
data: &'a mut ImportMap,
current_annotations: ImportAnnotations,
source: Option<Vc<Box<dyn Source>>>,
}

Expand Down Expand Up @@ -222,42 +245,9 @@ fn to_word(name: &ModuleExportName) -> JsWord {
}

impl Visit for Analyzer<'_> {
fn visit_module_item(&mut self, n: &ModuleItem) {
if let ModuleItem::Stmt(Stmt::Expr(ExprStmt { expr, .. })) = n {
if let Expr::Lit(Lit::Str(s)) = unparen(expr) {
if s.value.starts_with("TURBOPACK") {
let value = &*s.value;
let value = value["TURBOPACK".len()..].trim();
if !value.starts_with('{') || !value.ends_with('}') {
// TODO report issue
} else {
value[1..value.len() - 1]
.trim()
.split(';')
.map(|p| p.trim())
.filter(|p| !p.is_empty())
.for_each(|part| {
if let Some(colon) = part.find(':') {
self.current_annotations.insert(
part[..colon].trim_end().into(),
Some(part[colon + 1..].trim_start().into()),
);
} else {
self.current_annotations.insert(part.into(), None);
}
})
}
n.visit_children_with(self);
return;
}
}
}
n.visit_children_with(self);
self.current_annotations.clear();
}

fn visit_import_decl(&mut self, import: &ImportDecl) {
let annotations = take(&mut self.current_annotations);
let annotations = ImportAnnotations::parse(import.with.as_deref());

self.ensure_reference(
import.span,
import.src.value.clone(),
Expand Down Expand Up @@ -295,7 +285,8 @@ impl Visit for Analyzer<'_> {
fn visit_export_all(&mut self, export: &ExportAll) {
self.data.has_exports = true;

let annotations = take(&mut self.current_annotations);
let annotations = ImportAnnotations::parse(export.with.as_deref());

self.ensure_reference(
export.span,
export.src.value.clone(),
Expand All @@ -313,53 +304,52 @@ impl Visit for Analyzer<'_> {

fn visit_named_export(&mut self, export: &NamedExport) {
self.data.has_exports = true;
if let Some(ref src) = export.src {
let annotations = take(&mut self.current_annotations);

self.ensure_reference(
export.span,
src.value.clone(),
ImportedSymbol::ModuleEvaluation,
annotations.clone(),
);
let Some(ref src) = export.src else {
return;
};

for spec in export.specifiers.iter() {
let symbol = get_import_symbol_from_export(spec);

let i = self.ensure_reference(
export.span,
src.value.clone(),
symbol,
annotations.clone(),
);

match spec {
ExportSpecifier::Namespace(n) => {
self.data.reexports.push((
i,
Reexport::Namespace {
exported: to_word(&n.name),
},
));
}
ExportSpecifier::Default(d) => {
self.data.reexports.push((
i,
Reexport::Named {
imported: js_word!("default"),
exported: d.exported.sym.clone(),
},
));
}
ExportSpecifier::Named(n) => {
self.data.reexports.push((
i,
Reexport::Named {
imported: to_word(&n.orig),
exported: to_word(n.exported.as_ref().unwrap_or(&n.orig)),
},
));
}
let annotations = ImportAnnotations::parse(export.with.as_deref());

self.ensure_reference(
export.span,
src.value.clone(),
ImportedSymbol::ModuleEvaluation,
annotations.clone(),
);

for spec in export.specifiers.iter() {
let symbol = get_import_symbol_from_export(spec);

let i =
self.ensure_reference(export.span, src.value.clone(), symbol, annotations.clone());

match spec {
ExportSpecifier::Namespace(n) => {
self.data.reexports.push((
i,
Reexport::Namespace {
exported: to_word(&n.name),
},
));
}
ExportSpecifier::Default(d) => {
self.data.reexports.push((
i,
Reexport::Named {
imported: js_word!("default"),
exported: d.exported.sym.clone(),
},
));
}
ExportSpecifier::Named(n) => {
self.data.reexports.push((
i,
Reexport::Named {
imported: to_word(&n.orig),
exported: to_word(n.exported.as_ref().unwrap_or(&n.orig)),
},
));
}
}
}
Expand Down
34 changes: 34 additions & 0 deletions crates/turbopack-ecmascript/src/annotations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use swc_core::{
common::DUMMY_SP,
ecma::ast::{Expr, KeyValueProp, ObjectLit, Prop, PropName, PropOrSpread},
};

/// Changes the chunking type for the annotated import
pub const ANNOTATION_CHUNKING_TYPE: &str = "turbopack-chunking-type";

/// Enables a specified transition for the annotated import
pub const ANNOTATION_TRANSITION: &str = "turbopack-transition";

pub fn with_chunking_type(chunking_type: &str) -> Box<ObjectLit> {
with_clause(&[(ANNOTATION_CHUNKING_TYPE, chunking_type)])
}

pub fn with_transition(transition_name: &str) -> Box<ObjectLit> {
with_clause(&[(ANNOTATION_TRANSITION, transition_name)])
}

pub fn with_clause<'a>(
entries: impl IntoIterator<Item = &'a (&'a str, &'a str)>,
) -> Box<ObjectLit> {
Box::new(ObjectLit {
span: DUMMY_SP,
props: entries.into_iter().map(|(k, v)| with_prop(k, v)).collect(),
})
}

fn with_prop(key: &str, value: &str) -> PropOrSpread {
PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Str(key.into()),
value: Box::new(Expr::Lit(value.into())),
})))
}
Loading

0 comments on commit 609181e

Please sign in to comment.