diff options
author | Titus Wormer <tituswormer@gmail.com> | 2022-10-11 09:54:56 +0200 |
---|---|---|
committer | Titus Wormer <tituswormer@gmail.com> | 2022-10-11 09:55:16 +0200 |
commit | a4b56e7b971fa81c56a59b465f90c8016f01320d (patch) | |
tree | 7002a44087e57c8158a51dd30b6eb89eb260af2b /tests | |
parent | 1fd94f512834aa7bd70f22a60229ce01edfc754e (diff) | |
download | markdown-rs-a4b56e7b971fa81c56a59b465f90c8016f01320d.tar.gz markdown-rs-a4b56e7b971fa81c56a59b465f90c8016f01320d.tar.bz2 markdown-rs-a4b56e7b971fa81c56a59b465f90c8016f01320d.zip |
Add support for proper positional info in swc tree
* Fix some positional info in SWC error messages
* Add positional info in `to_document` on duplicate layouts
* Add support for `path` on `Program` (`to_swc`, `to_document`, `jsx_rewrite`),
for the path of a file on disk
* Add support for `development` to `jsx-rewrite`, which when defined will embed
info on where tags were written into the runtime code when they are not passed
* Refactor to move some utilities to `micromark_swc_utils.rs`, `swc_utils.rs`
Diffstat (limited to '')
-rw-r--r-- | tests/mdx_esm.rs | 5 | ||||
-rw-r--r-- | tests/mdx_expression_flow.rs | 3 | ||||
-rw-r--r-- | tests/mdx_expression_text.rs | 5 | ||||
-rw-r--r-- | tests/mdx_jsx_text.rs | 10 | ||||
-rw-r--r-- | tests/test_utils/hast.rs | 10 | ||||
-rw-r--r-- | tests/test_utils/jsx_rewrite.rs | 261 | ||||
-rw-r--r-- | tests/test_utils/micromark_swc_utils.rs | 134 | ||||
-rw-r--r-- | tests/test_utils/mod.rs | 2 | ||||
-rw-r--r-- | tests/test_utils/swc.rs | 145 | ||||
-rw-r--r-- | tests/test_utils/swc_utils.rs | 96 | ||||
-rw-r--r-- | tests/test_utils/to_document.rs | 65 | ||||
-rw-r--r-- | tests/test_utils/to_hast.rs | 22 | ||||
-rw-r--r-- | tests/test_utils/to_swc.rs | 78 | ||||
-rw-r--r-- | tests/xxx_document.rs | 16 | ||||
-rw-r--r-- | tests/xxx_hast.rs | 12 | ||||
-rw-r--r-- | tests/xxx_jsx_rewrite.rs | 32 | ||||
-rw-r--r-- | tests/xxx_swc.rs | 434 |
17 files changed, 884 insertions, 446 deletions
diff --git a/tests/mdx_esm.rs b/tests/mdx_esm.rs index d6021a1..31f493b 100644 --- a/tests/mdx_esm.rs +++ b/tests/mdx_esm.rs @@ -101,7 +101,7 @@ fn mdx_esm() -> Result<(), String> { assert_eq!( micromark_with_options("import 1/1", &swc).err().unwrap(), - "1:9: Could not parse esm with swc: Expected 'from', got 'numeric literal (1, 1)'", + "1:8: Could not parse esm with swc: Expected 'from', got 'numeric literal (1, 1)'", "should crash on invalid import/exports (2)" ); @@ -250,7 +250,8 @@ fn mdx_esm() -> Result<(), String> { Node::Root(Root { children: vec![Node::MdxjsEsm(MdxjsEsm { value: "import a from 'b'\nexport {a}".to_string(), - position: Some(Position::new(1, 1, 0, 2, 11, 28)) + position: Some(Position::new(1, 1, 0, 2, 11, 28)), + stops: vec![(0, 0), (17, 17), (18, 18)] })], position: Some(Position::new(1, 1, 0, 2, 11, 28)) }), diff --git a/tests/mdx_expression_flow.rs b/tests/mdx_expression_flow.rs index 0b13149..c9ff560 100644 --- a/tests/mdx_expression_flow.rs +++ b/tests/mdx_expression_flow.rs @@ -94,7 +94,8 @@ fn mdx_expression_flow_agnostic() -> Result<(), String> { Node::Root(Root { children: vec![Node::MdxFlowExpression(MdxFlowExpression { value: "alpha +\nbravo".to_string(), - position: Some(Position::new(1, 1, 0, 2, 7, 15)) + position: Some(Position::new(1, 1, 0, 2, 7, 15)), + stops: vec![(0, 1), (7, 8), (8, 9)] })], position: Some(Position::new(1, 1, 0, 2, 7, 15)) }), diff --git a/tests/mdx_expression_text.rs b/tests/mdx_expression_text.rs index d893a70..0aee081 100644 --- a/tests/mdx_expression_text.rs +++ b/tests/mdx_expression_text.rs @@ -119,7 +119,7 @@ fn mdx_expression_text_gnostic_core() -> Result<(), String> { assert_eq!( micromark_with_options("a {var b = \"c\"} d", &swc).err().unwrap(), - "1:7: Could not parse expression with swc: Unexpected token `var`. Expected this, import, async, function, [ for array literal, { for object literal, @ for decorator, function, class, null, true, false, number, bigint, string, regexp, ` for template literal, (, or an identifier", + "1:4: Could not parse expression with swc: Unexpected token `var`. Expected this, import, async, function, [ for array literal, { for object literal, @ for decorator, function, class, null, true, false, number, bigint, string, regexp, ` for template literal, (, or an identifier", "should crash on non-expressions" ); @@ -213,7 +213,8 @@ fn mdx_expression_text_agnostic() -> Result<(), String> { }), Node::MdxTextExpression(MdxTextExpression { value: "alpha".to_string(), - position: Some(Position::new(1, 3, 2, 1, 10, 9)) + position: Some(Position::new(1, 3, 2, 1, 10, 9)), + stops: vec![(0, 3)] }), Node::Text(Text { value: " b.".to_string(), diff --git a/tests/mdx_jsx_text.rs b/tests/mdx_jsx_text.rs index 9064e83..22a701a 100644 --- a/tests/mdx_jsx_text.rs +++ b/tests/mdx_jsx_text.rs @@ -169,7 +169,10 @@ fn mdx_jsx_text_core() -> Result<(), String> { children: vec![ Node::MdxJsxTextElement(MdxJsxTextElement { name: Some("a".to_string()), - attributes: vec![AttributeContent::Expression("...b".to_string())], + attributes: vec![AttributeContent::Expression( + "...b".to_string(), + vec![(0, 4)] + )], children: vec![], position: Some(Position::new(1, 1, 0, 1, 13, 12)) }), @@ -235,7 +238,10 @@ fn mdx_jsx_text_core() -> Result<(), String> { }), AttributeContent::Property(MdxJsxAttribute { name: "f".to_string(), - value: Some(AttributeValue::Expression("g".to_string())), + value: Some(AttributeValue::Expression( + "g".to_string(), + vec![(0, 18)] + )), }), ], children: vec![], diff --git a/tests/test_utils/hast.rs b/tests/test_utils/hast.rs index 1ad8789..48460ca 100644 --- a/tests/test_utils/hast.rs +++ b/tests/test_utils/hast.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -// ^-- fix later +// ^-- To do: fix later extern crate alloc; extern crate micromark; @@ -9,7 +9,7 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -pub use micromark::mdast::{AttributeContent, AttributeValue, MdxJsxAttribute}; +pub use micromark::mdast::{AttributeContent, AttributeValue, MdxJsxAttribute, Stop}; use micromark::unist::Position; /// Nodes. @@ -254,6 +254,9 @@ pub struct MdxExpression { pub value: String, /// Positional info. pub position: Option<Position>, + + // Custom data on where each slice of `value` came from. + pub stops: Vec<Stop>, } /// MDX: ESM. @@ -269,4 +272,7 @@ pub struct MdxjsEsm { pub value: String, /// Positional info. pub position: Option<Position>, + + // Custom data on where each slice of `value` came from. + pub stops: Vec<Stop>, } diff --git a/tests/test_utils/jsx_rewrite.rs b/tests/test_utils/jsx_rewrite.rs index b6ffad6..9dd2605 100644 --- a/tests/test_utils/jsx_rewrite.rs +++ b/tests/test_utils/jsx_rewrite.rs @@ -1,9 +1,13 @@ extern crate swc_common; extern crate swc_ecma_ast; -use crate::{ - micromark::{id_cont_ as id_cont, id_start_ as id_start}, - test_utils::to_swc::Program, +use crate::test_utils::{ + micromark_swc_utils::{position_to_string, span_to_position}, + swc_utils::{ + create_binary_expression, create_ident, create_ident_expression, create_member_expression, + }, + to_swc::Program, }; +use micromark::{id_cont_ as id_cont, id_start_ as id_start, unist::Position, Location}; use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; /// Configuration. @@ -21,10 +25,17 @@ pub struct Options { /// Rewrite JSX in an MDX file so that components can be passed in and provided. #[allow(dead_code)] -pub fn jsx_rewrite(mut program: Program, options: &Options) -> Program { +pub fn jsx_rewrite( + mut program: Program, + options: &Options, + location: Option<&Location>, +) -> Program { let mut state = State { scopes: vec![], + location, provider: options.provider_import_source.is_some(), + path: program.path.clone(), + development: options.development, create_provider_import: false, create_error_helper: false, }; @@ -44,7 +55,10 @@ pub fn jsx_rewrite(mut program: Program, options: &Options) -> Program { // If potentially missing components are used, add the helper used for // errors. if state.create_error_helper { - program.module.body.push(create_error_helper()); + program + .module + .body + .push(create_error_helper(state.development, state.path)); } program @@ -88,8 +102,7 @@ struct Info { /// ``` /// vec![("a".into(), false), ("a.b".into(), true)] /// ``` - // To do: add positional info later. - references: Vec<(String, bool)>, + references: Vec<(String, bool, Option<Position>)>, } /// Scope (block or function/global). @@ -103,9 +116,14 @@ struct Scope { /// Context. #[derive(Debug, Default, Clone)] -struct State { +struct State<'a> { + location: Option<&'a Location>, + /// Path to file. + path: Option<String>, /// List of current scopes. scopes: Vec<Scope>, + /// Whether the user is in development mode. + development: bool, /// Whether the user uses a provider. provider: bool, /// Whether a provider is referenced. @@ -117,7 +135,7 @@ struct State { create_error_helper: bool, } -impl State { +impl<'a> State<'a> { /// Open a new scope. fn enter(&mut self, info: Option<Info>) { self.scopes.push(Scope { @@ -432,8 +450,47 @@ impl State { // if (!a) _missingMdxReference("a", false); // if (!a.b) _missingMdxReference("a.b", true); // ``` - for (id, component) in info.references { + for (id, component, position) in info.references { self.create_error_helper = true; + + let mut args = vec![ + swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: id.clone().into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + }, + swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Bool( + swc_ecma_ast::Bool { + value: component, + span: swc_common::DUMMY_SP, + }, + ))), + }, + ]; + + // Add the source location if it exists and if `development` is on. + if let Some(position) = position.as_ref() { + if self.development { + args.push(swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: position_to_string(position).into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + }) + } + } + statements.push(swc_ecma_ast::Stmt::If(swc_ecma_ast::IfStmt { test: Box::new(swc_ecma_ast::Expr::Unary(swc_ecma_ast::UnaryExpr { op: swc_ecma_ast::UnaryOp::Bang, @@ -446,27 +503,7 @@ impl State { callee: swc_ecma_ast::Callee::Expr(Box::new(create_ident_expression( "_missingMdxReference", ))), - args: vec![ - swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: id.into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - }, - swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Bool( - swc_ecma_ast::Bool { - value: component, - span: swc_common::DUMMY_SP, - }, - ))), - }, - ], + args, type_args: None, span: swc_common::DUMMY_SP, })), @@ -545,6 +582,7 @@ impl State { None } } + /// Get the top-level scope’s info, mutably. fn current_top_level_info_mut(&mut self) -> Option<&mut Info> { if let Some(scope) = self.scopes.get_mut(1) { @@ -623,7 +661,7 @@ impl State { } } -impl VisitMut for State { +impl<'a> VisitMut for State<'a> { noop_visit_mut_type!(); /// Rewrite JSX identifiers. @@ -632,6 +670,7 @@ impl VisitMut for State { if let Some(info) = self.current_top_level_info() { // Rewrite only if we can rewrite. if is_props_receiving_fn(&info.name) || self.provider { + let position = span_to_position(&node.span, self.location); match &node.opening.name { // `<x.y>`, `<Foo.Bar>`, `<x.y.z>`. swc_ecma_ast::JSXElementName::JSXMemberExpr(d) => { @@ -656,7 +695,6 @@ impl VisitMut for State { if !in_scope { let info_mut = self.current_top_level_info_mut().unwrap(); - // To do: add positional info. let mut index = 1; while index <= ids.len() { let full_id = ids[0..index].join("."); @@ -668,7 +706,9 @@ impl VisitMut for State { reference.1 = true; } } else { - info_mut.references.push((full_id, component)) + info_mut + .references + .push((full_id, component, position.clone())) } index += 1; } @@ -749,7 +789,7 @@ impl VisitMut for State { { reference.1 = true; } else { - info_mut.references.push((id.clone(), true)) + info_mut.references.push((id.clone(), true, position)) } } @@ -948,8 +988,8 @@ fn create_import_provider(source: &str) -> swc_ecma_ast::ModuleItem { /// throw new Error("Expected " + (component ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it."); /// } /// ``` -fn create_error_helper() -> swc_ecma_ast::ModuleItem { - let parameters = vec![ +fn create_error_helper(development: bool, path: Option<String>) -> swc_ecma_ast::ModuleItem { + let mut parameters = vec![ swc_ecma_ast::Param { pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { id: create_ident("id"), @@ -968,7 +1008,19 @@ fn create_error_helper() -> swc_ecma_ast::ModuleItem { }, ]; - let message = vec![ + // Accept a source location (which might be undefiend). + if development { + parameters.push(swc_ecma_ast::Param { + pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: create_ident("place"), + type_ann: None, + }), + decorators: vec![], + span: swc_common::DUMMY_SP, + }) + } + + let mut message = vec![ swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { value: "Expected ".into(), span: swc_common::DUMMY_SP, @@ -1009,8 +1061,43 @@ fn create_error_helper() -> swc_ecma_ast::ModuleItem { })), ]; - // To do: in development, add `place` param, and use the positional info. - // Also, then, add file path. + // `place ? "\nIt’s referenced in your code at `" + place+ "`" : ""` + if development { + message.push(swc_ecma_ast::Expr::Paren(swc_ecma_ast::ParenExpr { + expr: Box::new(swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { + test: Box::new(create_ident_expression("place")), + cons: Box::new(create_binary_expression( + vec![ + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: "\nIt’s referenced in your code at `".into(), + span: swc_common::DUMMY_SP, + raw: None, + })), + create_ident_expression("place"), + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: if let Some(path) = path { + format!("` in `{}`", path).into() + } else { + "`".into() + }, + span: swc_common::DUMMY_SP, + raw: None, + })), + ], + swc_ecma_ast::BinaryOp::Add, + )), + alt: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: "".into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + span: swc_common::DUMMY_SP, + })), + span: swc_common::DUMMY_SP, + })) + } swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Fn( swc_ecma_ast::FnDecl { @@ -1047,100 +1134,6 @@ fn create_error_helper() -> swc_ecma_ast::ModuleItem { ))) } -/// Generate a binary expression. -/// -/// ```js -/// a + b + c -/// a || b -/// ``` -fn create_binary_expression( - mut exprs: Vec<swc_ecma_ast::Expr>, - op: swc_ecma_ast::BinaryOp, -) -> swc_ecma_ast::Expr { - exprs.reverse(); - - let mut left = None; - - while let Some(right_expr) = exprs.pop() { - left = Some(if let Some(left_expr) = left { - swc_ecma_ast::Expr::Bin(swc_ecma_ast::BinExpr { - left: Box::new(left_expr), - right: Box::new(right_expr), - op, - span: swc_common::DUMMY_SP, - }) - } else { - right_expr - }); - } - - left.expect("expected one or more expressions") -} - -/// Generate a member expression. -/// -/// ```js -/// a.b -/// a -/// ``` -fn create_member_expression(name: &str) -> swc_ecma_ast::Expr { - let bytes = name.as_bytes(); - let mut index = 0; - let mut start = 0; - let mut parts = vec![]; - - while index < bytes.len() { - if bytes[index] == b'.' { - parts.push(&name[start..index]); - start = index + 1; - } - - index += 1; - } - - if parts.len() > 1 { - let mut member = swc_ecma_ast::MemberExpr { - obj: Box::new(create_ident_expression(parts[0])), - prop: swc_ecma_ast::MemberProp::Ident(create_ident(parts[1])), - span: swc_common::DUMMY_SP, - }; - let mut index = 2; - while index < parts.len() { - member = swc_ecma_ast::MemberExpr { - obj: Box::new(swc_ecma_ast::Expr::Member(member)), - prop: swc_ecma_ast::MemberProp::Ident(create_ident(parts[1])), - span: swc_common::DUMMY_SP, - }; - index += 1; - } - swc_ecma_ast::Expr::Member(member) - } else { - create_ident_expression(name) - } -} - -/// Generate an ident expression. -/// -/// ```js -/// a -/// ``` -fn create_ident_expression(sym: &str) -> swc_ecma_ast::Expr { - swc_ecma_ast::Expr::Ident(create_ident(sym)) -} - -/// Generate an ident. -/// -/// ```js -/// a -/// ``` -fn create_ident(sym: &str) -> swc_ecma_ast::Ident { - swc_ecma_ast::Ident { - sym: sym.into(), - optional: false, - span: swc_common::DUMMY_SP, - } -} - /// Check if this function is a props receiving component: it’s one of ours. fn is_props_receiving_fn(name: &Option<String>) -> bool { if let Some(name) = name { diff --git a/tests/test_utils/micromark_swc_utils.rs b/tests/test_utils/micromark_swc_utils.rs new file mode 100644 index 0000000..13678d5 --- /dev/null +++ b/tests/test_utils/micromark_swc_utils.rs @@ -0,0 +1,134 @@ +extern crate swc_common; +use micromark::{ + mdast::Stop, + unist::{Point, Position}, + Location, +}; +use swc_common::{BytePos, Span, SyntaxContext, DUMMY_SP}; +use swc_ecma_visit::{noop_visit_mut_type, VisitMut}; + +/// Turn a unist position, into an SWC span, of two byte positions. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn position_to_span(position: Option<&Position>) -> Span { + position.map_or(DUMMY_SP, |d| Span { + lo: point_to_bytepos(&d.start), + hi: point_to_bytepos(&d.end), + ctxt: SyntaxContext::empty(), + }) +} + +/// Turn an SWC span, of two byte positions, into a unist position. +/// +/// This assumes the span comes from a fixed tree, or is a dummy. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn span_to_position(span: &Span, location: Option<&Location>) -> Option<Position> { + let lo = span.lo.0 as usize; + let hi = span.hi.0 as usize; + + if lo > 0 && hi > 0 { + if let Some(location) = location { + if let Some(start) = location.to_point(lo - 1) { + if let Some(end) = location.to_point(hi - 1) { + return Some(Position { start, end }); + } + } + } + } + + None +} + +/// Turn a unist point into an SWC byte position. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn point_to_bytepos(point: &Point) -> BytePos { + BytePos(point.offset as u32 + 1) +} + +/// Turn an SWC byte position into a unist point. +/// +/// This assumes the byte position comes from a fixed tree, or is a dummy. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn bytepos_to_point(bytepos: &BytePos, location: Option<&Location>) -> Option<Point> { + let pos = bytepos.0 as usize; + + if pos > 0 { + if let Some(location) = location { + return location.to_point(pos - 1); + } + } + + None +} + +/// Prefix an error message with an optional point. +pub fn prefix_error_with_point(reason: String, point: Option<&Point>) -> String { + if let Some(point) = point { + format!("{}: {}", point_to_string(point), reason) + } else { + reason + } +} + +/// Serialize a unist position for humans. +pub fn position_to_string(position: &Position) -> String { + format!( + "{}-{}", + point_to_string(&position.start), + point_to_string(&position.end) + ) +} + +/// Serialize a unist point for humans. +pub fn point_to_string(point: &Point) -> String { + format!("{}:{}", point.line, point.column) +} + +/// Visitor to fix SWC byte positions. +/// +/// This assumes the byte position comes from an **unfixed** tree. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +#[derive(Debug, Default, Clone)] +pub struct RewriteContext<'a> { + pub prefix_len: usize, + pub stops: &'a [Stop], + pub location: Option<&'a Location>, +} + +impl<'a> VisitMut for RewriteContext<'a> { + noop_visit_mut_type!(); + + // Rewrite spans. + fn visit_mut_span(&mut self, span: &mut Span) { + let mut result = DUMMY_SP; + let lo_rel = span.lo.0 as usize; + let hi_rel = span.hi.0 as usize; + + if lo_rel > self.prefix_len && hi_rel > self.prefix_len { + if let Some(lo_abs) = + Location::relative_to_absolute(self.stops, lo_rel - 1 - self.prefix_len) + { + if let Some(hi_abs) = + Location::relative_to_absolute(self.stops, hi_rel - 1 - self.prefix_len) + { + result = Span { + lo: BytePos(lo_abs as u32 + 1), + hi: BytePos(hi_abs as u32 + 1), + ctxt: SyntaxContext::empty(), + }; + } + } + } + + *span = result; + } +} diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index 339992c..99ded2f 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -1,6 +1,8 @@ pub mod hast; pub mod jsx_rewrite; +pub mod micromark_swc_utils; pub mod swc; +pub mod swc_utils; pub mod to_document; pub mod to_hast; pub mod to_swc; diff --git a/tests/test_utils/swc.rs b/tests/test_utils/swc.rs index fb91a3b..78859b6 100644 --- a/tests/test_utils/swc.rs +++ b/tests/test_utils/swc.rs @@ -2,7 +2,10 @@ extern crate micromark; extern crate swc_common; extern crate swc_ecma_ast; extern crate swc_ecma_parser; -use micromark::{MdxExpressionKind, MdxSignal}; +use crate::test_utils::micromark_swc_utils::{ + bytepos_to_point, prefix_error_with_point, RewriteContext, +}; +use micromark::{mdast::Stop, unist::Point, Location, MdxExpressionKind, MdxSignal}; use swc_common::{ source_map::Pos, sync::Lrc, BytePos, FileName, FilePathMapping, SourceFile, SourceMap, Spanned, }; @@ -11,10 +14,7 @@ use swc_ecma_codegen::{text_writer::JsWriter, Emitter}; use swc_ecma_parser::{ error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsConfig, Syntax, }; - -// To do: -// Use lexer in the future: -// <https://docs.rs/swc_ecma_parser/0.99.1/swc_ecma_parser/lexer/index.html> +use swc_ecma_visit::VisitMutWith; /// Lex ESM in MDX with SWC. #[allow(dead_code)] @@ -24,40 +24,42 @@ pub fn parse_esm(value: &str) -> MdxSignal { let result = parse_file_as_module(&file, syntax, version, None, &mut errors); match result { - Err(error) => swc_error_to_signal(&error, value.len(), 0, "esm"), + Err(error) => swc_error_to_signal(&error, "esm", value.len(), 0), Ok(tree) => { if errors.is_empty() { check_esm_ast(&tree) } else { - if errors.len() > 1 { - println!("parse_esm: todo: multiple errors? {:?}", errors); - } - swc_error_to_signal(&errors[0], value.len(), 0, "esm") + swc_error_to_signal(&errors[0], "esm", value.len(), 0) } } } } /// Parse ESM in MDX with SWC. -/// To do: figure out how to fix positional info. /// See `drop_span` in `swc_ecma_utils` for inspiration? #[allow(dead_code)] -pub fn parse_esm_to_tree(value: &str) -> Result<swc_ecma_ast::Module, String> { +pub fn parse_esm_to_tree( + value: &str, + stops: &[Stop], + location: Option<&Location>, +) -> Result<swc_ecma_ast::Module, String> { let (file, syntax, version) = create_config(value.to_string()); let mut errors = vec![]; - let result = parse_file_as_module(&file, syntax, version, None, &mut errors); + let mut rewrite_context = RewriteContext { + stops, + location, + prefix_len: 0, + }; match result { - Err(error) => Err(swc_error_to_string(&error)), - Ok(module) => { + Err(error) => Err(swc_error_to_error(&error, "esm", &rewrite_context)), + Ok(mut module) => { if errors.is_empty() { + module.visit_mut_with(&mut rewrite_context); Ok(module) } else { - if errors.len() > 1 { - println!("parse_esm_to_tree: todo: multiple errors? {:?}", errors); - } - Err(swc_error_to_string(&errors[0])) + Err(swc_error_to_error(&errors[0], "esm", &rewrite_context)) } } } @@ -87,33 +89,31 @@ pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal { let result = parse_file_as_expr(&file, syntax, version, None, &mut errors); match result { - Err(error) => swc_error_to_signal(&error, value.len(), prefix.len(), "expression"), + Err(error) => swc_error_to_signal(&error, "expression", value.len(), prefix.len()), Ok(tree) => { if errors.is_empty() { - let place = fix_swc_position(tree.span().hi.to_usize(), prefix.len()); + let expression_end = fix_swc_position(tree.span().hi.to_usize(), prefix.len()); let result = check_expression_ast(&tree, kind); if matches!(result, MdxSignal::Ok) { - whitespace_and_comments(place, value) + whitespace_and_comments(expression_end, value) } else { result } } else { - if errors.len() > 1 { - unreachable!("parse_expression: todo: multiple errors? {:?}", errors); - } - swc_error_to_signal(&errors[0], value.len(), prefix.len(), "expression") + swc_error_to_signal(&errors[0], "expression", value.len(), prefix.len()) } } } } /// Parse ESM in MDX with SWC. -/// To do: figure out how to fix positional info. /// See `drop_span` in `swc_ecma_utils` for inspiration? #[allow(dead_code)] pub fn parse_expression_to_tree( value: &str, kind: &MdxExpressionKind, + stops: &[Stop], + location: Option<&Location>, ) -> Result<Box<swc_ecma_ast::Expr>, String> { // For attribute expression, a spread is needed, for which we have to prefix // and suffix the input. @@ -127,11 +127,21 @@ pub fn parse_expression_to_tree( let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix)); let mut errors = vec![]; let result = parse_file_as_expr(&file, syntax, version, None, &mut errors); + let mut rewrite_context = RewriteContext { + stops, + location, + prefix_len: prefix.len(), + }; match result { - Err(error) => Err(swc_error_to_string(&error)), - Ok(expr) => { + Err(error) => Err(swc_error_to_error(&error, "expression", &rewrite_context)), + Ok(mut expr) => { if errors.is_empty() { + // Fix positions. + expr.visit_mut_with(&mut rewrite_context); + + let expr_bytepos = expr.span().lo; + if matches!(kind, MdxExpressionKind::AttributeExpression) { let mut obj = None; @@ -143,27 +153,37 @@ pub fn parse_expression_to_tree( if let Some(mut obj) = obj { if obj.props.len() > 1 { - Err("Unexpected extra content in spread: only a single spread is supported".into()) + Err(create_error_message( + "Unexpected extra content in spread: only a single spread is supported", + "expression", + bytepos_to_point(&obj.span.lo, location).as_ref() + )) } else if let Some(swc_ecma_ast::PropOrSpread::Spread(d)) = obj.props.pop() { Ok(d.expr) } else { - Err("Unexpected prop in spread: only a spread is supported".into()) + Err(create_error_message( + "Unexpected prop in spread: only a spread is supported", + "expression", + bytepos_to_point(&obj.span.lo, location).as_ref(), + )) } } else { - Err("Expected an object spread (`{...spread}`)".into()) + Err(create_error_message( + "Expected an object spread (`{...spread}`)", + "expression", + bytepos_to_point(&expr_bytepos, location).as_ref(), + )) } } else { Ok(expr) } } else { - if errors.len() > 1 { - println!( - "parse_expression_to_tree: todo: multiple errors? {:?}", - errors - ); - } - Err(swc_error_to_string(&errors[0])) + Err(swc_error_to_error( + &errors[0], + "expression", + &rewrite_context, + )) } } } @@ -203,10 +223,10 @@ fn check_esm_ast(tree: &Module) -> MdxSignal { let node = &tree.body[index]; if !node.is_module_decl() { - let place = fix_swc_position(node.span().hi.to_usize(), 0); + let relative = fix_swc_position(node.span().lo.to_usize(), 0); return MdxSignal::Error( "Unexpected statement in code: only import/exports are supported".to_string(), - place, + relative, ); } @@ -248,24 +268,47 @@ fn check_expression_ast(tree: &Expr, kind: &MdxExpressionKind) -> MdxSignal { /// * Else, yields `MdxSignal::Error`. fn swc_error_to_signal( error: &SwcError, + name: &str, value_len: usize, prefix_len: usize, - name: &str, ) -> MdxSignal { - let place = fix_swc_position(error.span().hi.to_usize(), prefix_len); - let message = format!( - "Could not parse {} with swc: {}", - name, - swc_error_to_string(error) - ); + let reason = create_error_reason(&swc_error_to_string(error), name); + let error_end = fix_swc_position(error.span().hi.to_usize(), prefix_len); - if place >= value_len { - MdxSignal::Eof(message) + if error_end >= value_len { + MdxSignal::Eof(reason) } else { - MdxSignal::Error(message, place) + MdxSignal::Error( + reason, + fix_swc_position(error.span().lo.to_usize(), prefix_len), + ) } } +fn swc_error_to_error(error: &SwcError, name: &str, context: &RewriteContext) -> String { + create_error_message( + &swc_error_to_string(error), + name, + context + .location + .and_then(|location| { + location.relative_to_point( + context.stops, + fix_swc_position(error.span().lo.to_usize(), context.prefix_len), + ) + }) + .as_ref(), + ) +} + +fn create_error_message(reason: &str, name: &str, point: Option<&Point>) -> String { + prefix_error_with_point(create_error_reason(name, reason), point) +} + +fn create_error_reason(reason: &str, name: &str) -> String { + format!("Could not parse {} with swc: {}", name, reason) +} + /// Turn an SWC error into a string. fn swc_error_to_string(error: &SwcError) -> String { error.kind().msg().into() diff --git a/tests/test_utils/swc_utils.rs b/tests/test_utils/swc_utils.rs new file mode 100644 index 0000000..1e1a526 --- /dev/null +++ b/tests/test_utils/swc_utils.rs @@ -0,0 +1,96 @@ +extern crate swc_common; +extern crate swc_ecma_ast; + +use swc_common::DUMMY_SP; +use swc_ecma_ast::{BinExpr, BinaryOp, Expr, Ident, MemberExpr, MemberProp}; + +/// Generate an ident. +/// +/// ```js +/// a +/// ``` +pub fn create_ident(sym: &str) -> Ident { + Ident { + sym: sym.into(), + optional: false, + span: DUMMY_SP, + } +} + +/// Generate an ident expression. +/// +/// ```js +/// a +/// ``` +pub fn create_ident_expression(sym: &str) -> Expr { + Expr::Ident(create_ident(sym)) +} + +/// Generate a binary expression. +/// +/// ```js +/// a + b + c +/// a || b +/// ``` +pub fn create_binary_expression(mut exprs: Vec<Expr>, op: BinaryOp) -> Expr { + exprs.reverse(); + + let mut left = None; + + while let Some(right_expr) = exprs.pop() { + left = Some(if let Some(left_expr) = left { + Expr::Bin(BinExpr { + left: Box::new(left_expr), + right: Box::new(right_expr), + op, + span: swc_common::DUMMY_SP, + }) + } else { + right_expr + }); + } + + left.expect("expected one or more expressions") +} + +/// Generate a member expression. +/// +/// ```js +/// a.b +/// a +/// ``` +pub fn create_member_expression(name: &str) -> Expr { + let bytes = name.as_bytes(); + let mut index = 0; + let mut start = 0; + let mut parts = vec![]; + + while index < bytes.len() { + if bytes[index] == b'.' { + parts.push(&name[start..index]); + start = index + 1; + } + + index += 1; + } + + if parts.len() > 1 { + let mut member = MemberExpr { + obj: Box::new(create_ident_expression(parts[0])), + prop: MemberProp::Ident(create_ident(parts[1])), + span: swc_common::DUMMY_SP, + }; + let mut index = 2; + while index < parts.len() { + member = MemberExpr { + obj: Box::new(Expr::Member(member)), + prop: MemberProp::Ident(create_ident(parts[1])), + span: swc_common::DUMMY_SP, + }; + index += 1; + } + Expr::Member(member) + } else { + create_ident_expression(name) + } +} diff --git a/tests/test_utils/to_document.rs b/tests/test_utils/to_document.rs index ded028a..938df1b 100644 --- a/tests/test_utils/to_document.rs +++ b/tests/test_utils/to_document.rs @@ -1,6 +1,12 @@ -extern crate swc_common; extern crate swc_ecma_ast; -use crate::test_utils::to_swc::Program; +use crate::test_utils::{ + micromark_swc_utils::{bytepos_to_point, prefix_error_with_point, span_to_position}, + to_swc::Program, +}; +use micromark::{ + unist::{Point, Position}, + Location, +}; /// JSX runtimes. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -60,7 +66,11 @@ impl Default for Options { } #[allow(dead_code)] -pub fn to_document(mut program: Program, options: &Options) -> Result<Program, String> { +pub fn to_document( + mut program: Program, + options: &Options, + location: Option<&Location>, +) -> Result<Program, String> { // New body children. let mut replacements = vec![]; @@ -158,8 +168,8 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S // is. let mut input = program.module.body.split_off(0); input.reverse(); - // To do: place position in this. let mut layout = false; + let mut layout_position = None; let content = true; while let Some(module_item) = input.pop() { @@ -174,13 +184,15 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultDecl( decl, )) => { - // To do: use positional info. if layout { - return Err("Cannot specify multiple layouts".into()); + return Err(create_double_layout_message( + bytepos_to_point(&decl.span.lo, location).as_ref(), + layout_position.as_ref(), + )); } - // To do: set positional info. layout = true; + layout_position = span_to_position(&decl.span, location); match decl.decl { swc_ecma_ast::DefaultDecl::Class(cls) => { replacements.push(create_layout_decl(swc_ecma_ast::Expr::Class(cls))) @@ -190,22 +202,26 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S } swc_ecma_ast::DefaultDecl::TsInterfaceDecl(_) => { return Err( - "Cannot use TypeScript interface declarations as default export in MDX files. The default export is reserved for a layout, which must be a component" - .into(), - ) + prefix_error_with_point( + "Cannot use TypeScript interface declarations as default export in MDX files. The default export is reserved for a layout, which must be a component".into(), + bytepos_to_point(&decl.span.lo, location).as_ref() + ) + ); } } } swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultExpr( expr, )) => { - // To do: use positional info. if layout { - return Err("Cannot specify multiple layouts".into()); + return Err(create_double_layout_message( + bytepos_to_point(&expr.span.lo, location).as_ref(), + layout_position.as_ref(), + )); } - // To do: set positional info. layout = true; + layout_position = span_to_position(&expr.span, location); replacements.push(create_layout_decl(*expr.expr)); } // ```js @@ -239,12 +255,14 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S // Looks like some TC39 proposal. Ignore for now // and only do things if this is an ID. if let swc_ecma_ast::ModuleExportName::Ident(ident) = &named.orig { - // To do: use positional info. if layout { - return Err("Cannot specify multiple layouts".into()); + return Err(create_double_layout_message( + bytepos_to_point(&ident.span.lo, location).as_ref(), + layout_position.as_ref(), + )); } - // To do: set positional info. layout = true; + layout_position = span_to_position(&ident.span, location); take = true; id = Some(ident.clone()); } @@ -623,3 +641,18 @@ fn create_layout_decl(expr: swc_ecma_ast::Expr) -> swc_ecma_ast::ModuleItem { }, )))) } + +/// Create an error message about multiple layouts. +fn create_double_layout_message(at: Option<&Point>, previous: Option<&Position>) -> String { + prefix_error_with_point( + format!( + "Cannot specify multiple layouts{}", + if let Some(previous) = previous { + format!(" (previous: {:?})", previous) + } else { + "".into() + } + ), + at, + ) +} diff --git a/tests/test_utils/to_hast.rs b/tests/test_utils/to_hast.rs index 0716daa..4907e23 100644 --- a/tests/test_utils/to_hast.rs +++ b/tests/test_utils/to_hast.rs @@ -827,10 +827,23 @@ fn transform_math(_state: &mut State, _node: &mdast::Node, math: &mdast::Math) - /// [`MdxFlowExpression`][mdast::MdxFlowExpression],[`MdxTextExpression`][mdast::MdxTextExpression]. fn transform_mdx_expression(_state: &mut State, node: &mdast::Node) -> Result { - Result::Node(hast::Node::MdxExpression(hast::MdxExpression { - value: node.to_string(), - position: node.position().cloned(), - })) + match node { + mdast::Node::MdxFlowExpression(node) => { + Result::Node(hast::Node::MdxExpression(hast::MdxExpression { + value: node.value.clone(), + position: node.position.clone(), + stops: node.stops.clone(), + })) + } + mdast::Node::MdxTextExpression(node) => { + Result::Node(hast::Node::MdxExpression(hast::MdxExpression { + value: node.value.clone(), + position: node.position.clone(), + stops: node.stops.clone(), + })) + } + _ => unreachable!("expected expression"), + } } /// [`MdxJsxFlowElement`][mdast::MdxJsxFlowElement],[`MdxJsxTextElement`][mdast::MdxJsxTextElement]. @@ -858,6 +871,7 @@ fn transform_mdxjs_esm( Result::Node(hast::Node::MdxjsEsm(hast::MdxjsEsm { value: mdxjs_esm.value.clone(), position: mdxjs_esm.position.clone(), + stops: mdxjs_esm.stops.clone(), })) } diff --git a/tests/test_utils/to_swc.rs b/tests/test_utils/to_swc.rs index 6c7312b..02de514 100644 --- a/tests/test_utils/to_swc.rs +++ b/tests/test_utils/to_swc.rs @@ -1,13 +1,18 @@ extern crate swc_common; extern crate swc_ecma_ast; -use crate::test_utils::hast; -use crate::test_utils::swc::{parse_esm_to_tree, parse_expression_to_tree}; +use crate::test_utils::{ + hast, + micromark_swc_utils::position_to_span, + swc::{parse_esm_to_tree, parse_expression_to_tree}, + swc_utils::create_ident, +}; use core::str; -use micromark::{unist::Position, MdxExpressionKind}; +use micromark::{Location, MdxExpressionKind}; /// Result. #[derive(Debug, PartialEq, Eq)] pub struct Program { + pub path: Option<String>, /// JS AST. pub module: swc_ecma_ast::Module, /// Comments relating to AST. @@ -22,7 +27,7 @@ enum Space { } #[derive(Debug)] -struct Context { +struct Context<'a> { /// Whether we’re in HTML or SVG. /// /// Not used yet, likely useful in the future. @@ -31,22 +36,22 @@ struct Context { comments: Vec<swc_common::comments::Comment>, /// Declarations and stuff. esm: Vec<swc_ecma_ast::ModuleItem>, -} - -impl Context { - /// Create a new context. - fn new() -> Context { - Context { - space: Space::Html, - comments: vec![], - esm: vec![], - } - } + /// Optional way to turn relative positions into points. + location: Option<&'a Location>, } #[allow(dead_code)] -pub fn to_swc(tree: &hast::Node) -> Result<Program, String> { - let mut context = Context::new(); +pub fn to_swc( + tree: &hast::Node, + path: Option<String>, + location: Option<&Location>, +) -> Result<Program, String> { + let mut context = Context { + space: Space::Html, + comments: vec![], + esm: vec![], + location, + }; let expr = match one(&mut context, tree)? { Some(swc_ecma_ast::JSXElementChild::JSXFragment(x)) => { Some(swc_ecma_ast::Expr::JSXFragment(x)) @@ -81,6 +86,7 @@ pub fn to_swc(tree: &hast::Node) -> Result<Program, String> { } Ok(Program { + path, module, comments: context.comments, }) @@ -260,12 +266,14 @@ fn transform_mdx_jsx_element( }, ))) } - Some(hast::AttributeValue::Expression(value)) => { + Some(hast::AttributeValue::Expression(value, stops)) => { Some(swc_ecma_ast::JSXAttrValue::JSXExprContainer( swc_ecma_ast::JSXExprContainer { expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( value, &MdxExpressionKind::AttributeValueExpression, + stops, + context.location, )?), span: swc_common::DUMMY_SP, }, @@ -280,9 +288,13 @@ fn transform_mdx_jsx_element( value, }) } - hast::AttributeContent::Expression(value) => { - let expr = - parse_expression_to_tree(value, &MdxExpressionKind::AttributeExpression)?; + hast::AttributeContent::Expression(value, stops) => { + let expr = parse_expression_to_tree( + value, + &MdxExpressionKind::AttributeExpression, + stops, + context.location, + )?; swc_ecma_ast::JSXAttrOrSpread::SpreadElement(swc_ecma_ast::SpreadElement { dot3_token: swc_common::DUMMY_SP, expr, @@ -303,7 +315,7 @@ fn transform_mdx_jsx_element( /// [`MdxExpression`][hast::MdxExpression]. fn transform_mdx_expression( - _context: &mut Context, + context: &mut Context, node: &hast::Node, expression: &hast::MdxExpression, ) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { @@ -312,6 +324,8 @@ fn transform_mdx_expression( expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( &expression.value, &MdxExpressionKind::Expression, + &expression.stops, + context.location, )?), span: position_to_span(node.position()), }, @@ -324,7 +338,7 @@ fn transform_mdxjs_esm( _node: &hast::Node, esm: &hast::MdxjsEsm, ) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { - let mut module = parse_esm_to_tree(&esm.value)?; + let mut module = parse_esm_to_tree(&esm.value, &esm.stops, context.location)?; let mut index = 0; // To do: check that identifiers are not duplicated across esm blocks. @@ -455,15 +469,6 @@ fn create_fragment( } } -/// Create an ident. -fn create_ident(sym: &str) -> swc_ecma_ast::Ident { - swc_ecma_ast::Ident { - sym: sym.into(), - optional: false, - span: swc_common::DUMMY_SP, - } -} - /// Create a JSX element name. fn create_jsx_name(name: &str) -> swc_ecma_ast::JSXElementName { match parse_jsx_name(name) { @@ -515,15 +520,6 @@ fn create_jsx_attr_name(name: &str) -> swc_ecma_ast::JSXAttrName { } } -/// Turn a unist positions into an SWC span. -fn position_to_span(position: Option<&Position>) -> swc_common::Span { - position.map_or(swc_common::DUMMY_SP, |d| swc_common::Span { - lo: swc_common::BytePos(d.start.offset as u32), - hi: swc_common::BytePos(d.end.offset as u32), - ctxt: swc_common::SyntaxContext::empty(), - }) -} - fn inter_element_whitespace(value: &str) -> bool { let bytes = value.as_bytes(); let mut index = 0; diff --git a/tests/xxx_document.rs b/tests/xxx_document.rs index d5c8eef..7e43b1c 100644 --- a/tests/xxx_document.rs +++ b/tests/xxx_document.rs @@ -3,7 +3,7 @@ extern crate swc_common; extern crate swc_ecma_ast; extern crate swc_ecma_codegen; mod test_utils; -use micromark::{micromark_to_mdast, Constructs, ParseOptions}; +use micromark::{micromark_to_mdast, Constructs, Location, ParseOptions}; use pretty_assertions::assert_eq; use test_utils::{ swc::{parse_esm, parse_expression, serialize}, @@ -13,6 +13,7 @@ use test_utils::{ }; fn from_markdown(value: &str) -> Result<String, String> { + let location = Location::new(value.as_bytes()); let mdast = micromark_to_mdast( value, &ParseOptions { @@ -23,7 +24,8 @@ fn from_markdown(value: &str) -> Result<String, String> { }, )?; let hast = to_hast(&mdast); - let program = to_document(to_swc(&hast)?, &DocumentOptions::default())?; + let swc_tree = to_swc(&hast, None, Some(&location))?; + let program = to_document(swc_tree, &DocumentOptions::default(), Some(&location))?; let value = serialize(&program.module); Ok(value) } @@ -156,8 +158,6 @@ export default MDXContent; "should support a named export w/o source, w/o a specifiers", ); - // ........... - assert_eq!( from_markdown("export {a, b as default} from 'c'")?, "export { a } from 'c'; @@ -201,5 +201,13 @@ export default MDXContent; "should support a named export w/ source, w/o a specifiers", ); + assert_eq!( + from_markdown("export default a = 1\n\nexport default b = 2") + .err() + .unwrap(), + "3:1: Cannot specify multiple layouts (previous: 1:1-1:21 (0-20))", + "should crash on a comment spread" + ); + Ok(()) } diff --git a/tests/xxx_hast.rs b/tests/xxx_hast.rs index 886bcee..b0856a2 100644 --- a/tests/xxx_hast.rs +++ b/tests/xxx_hast.rs @@ -1137,10 +1137,12 @@ fn hast() { to_hast(&mdast::Node::MdxFlowExpression(mdast::MdxFlowExpression { value: "a".into(), position: None, + stops: vec![] })), hast::Node::MdxExpression(hast::MdxExpression { value: "a".into(), - position: None + position: None, + stops: vec![] }), "should support an `MdxFlowExpression`", ); @@ -1149,10 +1151,12 @@ fn hast() { to_hast(&mdast::Node::MdxTextExpression(mdast::MdxTextExpression { value: "a".into(), position: None, + stops: vec![] })), hast::Node::MdxExpression(hast::MdxExpression { value: "a".into(), - position: None + position: None, + stops: vec![] }), "should support an `MdxTextExpression`", ); @@ -1193,10 +1197,12 @@ fn hast() { to_hast(&mdast::Node::MdxjsEsm(mdast::MdxjsEsm { value: "a".into(), position: None, + stops: vec![] })), hast::Node::MdxjsEsm(hast::MdxjsEsm { value: "a".into(), - position: None + position: None, + stops: vec![] }), "should support an `MdxjsEsm`", ); diff --git a/tests/xxx_jsx_rewrite.rs b/tests/xxx_jsx_rewrite.rs index 7a1c379..c383f13 100644 --- a/tests/xxx_jsx_rewrite.rs +++ b/tests/xxx_jsx_rewrite.rs @@ -3,7 +3,7 @@ extern crate swc_common; extern crate swc_ecma_ast; extern crate swc_ecma_codegen; mod test_utils; -use micromark::{micromark_to_mdast, Constructs, ParseOptions}; +use micromark::{micromark_to_mdast, Constructs, Location, ParseOptions}; use pretty_assertions::assert_eq; use test_utils::{ jsx_rewrite::{jsx_rewrite, Options as RewriteOptions}, @@ -14,6 +14,7 @@ use test_utils::{ }; fn from_markdown(value: &str, options: &RewriteOptions) -> Result<String, String> { + let location = Location::new(value.as_bytes()); let mdast = micromark_to_mdast( value, &ParseOptions { @@ -24,8 +25,9 @@ fn from_markdown(value: &str, options: &RewriteOptions) -> Result<String, String }, )?; let hast = to_hast(&mdast); - let program = to_document(to_swc(&hast)?, &DocumentOptions::default())?; - let program = jsx_rewrite(program, options); + let swc_tree = to_swc(&hast, Some("example.mdx".into()), Some(&location))?; + let program = to_document(swc_tree, &DocumentOptions::default(), Some(&location))?; + let program = jsx_rewrite(program, options, Some(&location)); let value = serialize(&program.module); Ok(value) } @@ -351,5 +353,29 @@ function _missingMdxReference(id, component) { "should support providing components with JSX identifiers that are not JS identifiers in locally defined components", ); + assert_eq!( + from_markdown("# <Hi />", &RewriteOptions { + development: true, + ..Default::default() + })?, + "function _createMdxContent(props) { + const _components = Object.assign({ + h1: \"h1\" + }, props.components), { Hi } = _components; + if (!Hi) _missingMdxReference(\"Hi\", true, \"1:3-1:9\"); + return <_components.h1 ><Hi /></_components.h1>; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = props.components || {}; + return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); +} +export default MDXContent; +function _missingMdxReference(id, component, place) { + throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\" + (place ? \"\\nIt’s referenced in your code at `\" + place + \"` in `example.mdx`\" : \"\")); +} +", + "should create missing reference helpers w/o positional info in `development` mode", + ); + Ok(()) } diff --git a/tests/xxx_swc.rs b/tests/xxx_swc.rs index 26814cf..68a141d 100644 --- a/tests/xxx_swc.rs +++ b/tests/xxx_swc.rs @@ -12,14 +12,19 @@ use test_utils::{ #[test] fn swc() -> Result<(), String> { - let comment_ast = to_swc(&hast::Node::Comment(hast::Comment { - value: "a".into(), - position: None, - }))?; + let comment_ast = to_swc( + &hast::Node::Comment(hast::Comment { + value: "a".into(), + position: None, + }), + None, + None, + )?; assert_eq!( comment_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( @@ -66,19 +71,24 @@ fn swc() -> Result<(), String> { "should support a `Comment` (serialize)", ); - let element_ast = to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["b".into()]), - )], - children: vec![], - position: None, - }))?; + let element_ast = to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["b".into()]), + )], + children: vec![], + position: None, + }), + None, + None, + )?; assert_eq!( element_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( @@ -139,15 +149,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None, + })], position: None, - })], - position: None, - }))? + }), + None, + None + )? .module ), "<a >{\"a\"}</a>;\n", @@ -156,12 +170,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![("b".into(), hast::PropertyValue::String("c".into()),)], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![("b".into(), hast::PropertyValue::String("c".into()),)], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b=\"c\"/>;\n", @@ -170,12 +188,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![("b".into(), hast::PropertyValue::Boolean(true),)], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![("b".into(), hast::PropertyValue::Boolean(true),)], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b/>;\n", @@ -184,12 +206,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![("b".into(), hast::PropertyValue::Boolean(false),)], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![("b".into(), hast::PropertyValue::Boolean(false),)], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a />;\n", @@ -198,15 +224,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![( - "b".into(), - hast::PropertyValue::CommaSeparated(vec!["c".into(), "d".into()]), - )], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![( + "b".into(), + hast::PropertyValue::CommaSeparated(vec!["c".into(), "d".into()]), + )], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b=\"c, d\"/>;\n", @@ -215,16 +245,20 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ("data123".into(), hast::PropertyValue::Boolean(true),), - ("dataFoo".into(), hast::PropertyValue::Boolean(true),), - ("dataBAR".into(), hast::PropertyValue::Boolean(true),) - ], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ("data123".into(), hast::PropertyValue::Boolean(true),), + ("dataFoo".into(), hast::PropertyValue::Boolean(true),), + ("dataBAR".into(), hast::PropertyValue::Boolean(true),) + ], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a data-123 data-foo data-b-a-r/>;\n", @@ -233,32 +267,41 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ("role".into(), hast::PropertyValue::Boolean(true),), - ("ariaValueNow".into(), hast::PropertyValue::Boolean(true),), - ("ariaDescribedBy".into(), hast::PropertyValue::Boolean(true),) - ], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ("role".into(), hast::PropertyValue::Boolean(true),), + ("ariaValueNow".into(), hast::PropertyValue::Boolean(true),), + ("ariaDescribedBy".into(), hast::PropertyValue::Boolean(true),) + ], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a role aria-valuenow aria-describedby/>;\n", "should support an `Element` w/ aria attributes", ); - let mdx_element_ast = to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: None, - attributes: vec![], - children: vec![], - position: None, - }))?; + let mdx_element_ast = to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: None, + attributes: vec![], + children: vec![], + position: None, + }), + None, + None, + )?; assert_eq!( mdx_element_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( @@ -293,12 +336,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a />;\n", @@ -307,12 +354,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a:b".into()), - attributes: vec![], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a:b".into()), + attributes: vec![], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a:b />;\n", @@ -321,12 +372,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a.b.c".into()), - attributes: vec![], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a.b.c".into()), + attributes: vec![], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a.b.c />;\n", @@ -335,15 +390,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "b".into(), + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "b".into(), + position: None, + })], position: None, - })], - position: None, - }))? + }), + None, + None + )? .module ), "<a >{\"b\"}</a>;\n", @@ -352,15 +411,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { - name: "b".into(), - value: None - })], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { + name: "b".into(), + value: None + })], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b/>;\n", @@ -369,15 +432,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { - name: "b".into(), - value: Some(hast::AttributeValue::Literal("c".into())) - })], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { + name: "b".into(), + value: Some(hast::AttributeValue::Literal("c".into())) + })], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b=\"c\"/>;\n", @@ -386,15 +453,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { - name: "b".into(), - value: Some(hast::AttributeValue::Expression("c".into())) - })], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { + name: "b".into(), + value: Some(hast::AttributeValue::Expression("c".into(), vec![])) + })], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b={c}/>;\n", @@ -403,26 +474,36 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Expression("...c".into())], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![hast::AttributeContent::Expression("...c".into(), vec![])], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a {...c}/>;\n", "should support an `MdxElement` (element, expression attribute)", ); - let mdx_expression_ast = to_swc(&hast::Node::MdxExpression(hast::MdxExpression { - value: "a".into(), - position: None, - }))?; + let mdx_expression_ast = to_swc( + &hast::Node::MdxExpression(hast::MdxExpression { + value: "a".into(), + position: None, + stops: vec![], + }), + None, + None, + )?; assert_eq!( mdx_expression_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( @@ -439,13 +520,8 @@ fn swc() -> Result<(), String> { swc_ecma_ast::JSXExprContainer { expr: swc_ecma_ast::JSXExpr::Expr(Box::new( swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - // To do: fix positions. - span: swc_common::Span { - lo: swc_common::BytePos(1), - hi: swc_common::BytePos(2), - ctxt: swc_common::SyntaxContext::empty(), - }, sym: "a".into(), + span: swc_common::DUMMY_SP, optional: false, }) )), @@ -471,14 +547,20 @@ fn swc() -> Result<(), String> { "should support an `MdxExpression` (serialize)", ); - let mdxjs_esm_ast = to_swc(&hast::Node::MdxjsEsm(hast::MdxjsEsm { - value: "import a from 'b'".into(), - position: None, - }))?; + let mdxjs_esm_ast = to_swc( + &hast::Node::MdxjsEsm(hast::MdxjsEsm { + value: "import a from 'b'".into(), + position: None, + stops: vec![], + }), + None, + None, + )?; assert_eq!( mdxjs_esm_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::ModuleDecl( @@ -488,39 +570,19 @@ fn swc() -> Result<(), String> { local: swc_ecma_ast::Ident { sym: "a".into(), optional: false, - // To do: fix positions. - span: swc_common::Span { - lo: swc_common::BytePos(8), - hi: swc_common::BytePos(9), - ctxt: swc_common::SyntaxContext::empty(), - }, - }, - // To do: fix positions. - span: swc_common::Span { - lo: swc_common::BytePos(8), - hi: swc_common::BytePos(9), - ctxt: swc_common::SyntaxContext::empty(), + span: swc_common::DUMMY_SP, }, + span: swc_common::DUMMY_SP, } )], src: Box::new(swc_ecma_ast::Str { value: "b".into(), - // To do: fix positions. - span: swc_common::Span { - lo: swc_common::BytePos(15), - hi: swc_common::BytePos(18), - ctxt: swc_common::SyntaxContext::empty(), - }, + span: swc_common::DUMMY_SP, raw: Some("\'b\'".into()), }), type_only: false, asserts: None, - // To do: fix positions. - span: swc_common::Span { - lo: swc_common::BytePos(1), - hi: swc_common::BytePos(18), - ctxt: swc_common::SyntaxContext::empty(), - }, + span: swc_common::DUMMY_SP, }) )], span: swc_common::DUMMY_SP, @@ -536,17 +598,22 @@ fn swc() -> Result<(), String> { "should support an `MdxjsEsm` (serialize)", ); - let root_ast = to_swc(&hast::Node::Root(hast::Root { - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), + let root_ast = to_swc( + &hast::Node::Root(hast::Root { + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None, + })], position: None, - })], - position: None, - }))?; + }), + None, + None, + )?; assert_eq!( root_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( @@ -592,14 +659,19 @@ fn swc() -> Result<(), String> { "should support a `Root` (serialize)", ); - let text_ast = to_swc(&hast::Node::Text(hast::Text { - value: "a".into(), - position: None, - }))?; + let text_ast = to_swc( + &hast::Node::Text(hast::Text { + value: "a".into(), + position: None, + }), + None, + None, + )?; assert_eq!( text_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( |