mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
fix(repl): support transforming JSX/TSX (#20695)
Closes https://github.com/denoland/deno/issues/16771 --------- Co-authored-by: David Sherret <dsherret@gmail.com> Co-authored-by: Marvin Hagemeister <marvin@deno.com>
This commit is contained in:
parent
f1df22ce0a
commit
53248e9bb3
2 changed files with 210 additions and 21 deletions
|
@ -497,11 +497,10 @@ fn syntax_error() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_error_jsx() {
|
||||
// JSX is not supported in the REPL
|
||||
fn jsx_errors_without_pragma() {
|
||||
util::with_pty(&["repl"], |mut console| {
|
||||
console.write_line("const element = <div />;");
|
||||
console.expect("Expression expected");
|
||||
console.expect("React is not defined");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -897,11 +896,11 @@ fn repl_unit_tests() {
|
|||
console.expect("test1 ... ok (");
|
||||
console.expect("test2 ... FAILED (");
|
||||
console.expect(" ERRORS ");
|
||||
console.expect("test2 => <anonymous>:7:6");
|
||||
console.expect("test2 => <anonymous>:6:6");
|
||||
console.expect("error: Error: some message");
|
||||
console.expect(" at <anonymous>:8:9");
|
||||
console.expect(" at <anonymous>:7:9");
|
||||
console.expect(" FAILURES ");
|
||||
console.expect("test2 => <anonymous>:7:6");
|
||||
console.expect("test2 => <anonymous>:6:6");
|
||||
console.expect("FAILED | 1 passed (1 step) | 1 failed (");
|
||||
console.expect("undefined");
|
||||
|
||||
|
|
|
@ -18,12 +18,17 @@ use crate::tools::test::TestEvent;
|
|||
use crate::tools::test::TestEventSender;
|
||||
|
||||
use deno_ast::swc::ast as swc_ast;
|
||||
use deno_ast::swc::common::comments::CommentKind;
|
||||
use deno_ast::swc::visit::noop_visit_type;
|
||||
use deno_ast::swc::visit::Visit;
|
||||
use deno_ast::swc::visit::VisitWith;
|
||||
use deno_ast::DiagnosticsError;
|
||||
use deno_ast::ImportsNotUsedAsValues;
|
||||
use deno_ast::ModuleSpecifier;
|
||||
use deno_ast::ParsedSource;
|
||||
use deno_ast::SourcePos;
|
||||
use deno_ast::SourceRangedForSpanned;
|
||||
use deno_ast::SourceTextInfo;
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::futures::channel::mpsc::UnboundedReceiver;
|
||||
use deno_core::futures::FutureExt;
|
||||
|
@ -34,12 +39,40 @@ use deno_core::unsync::spawn;
|
|||
use deno_core::LocalInspectorSession;
|
||||
use deno_graph::source::ResolutionMode;
|
||||
use deno_graph::source::Resolver;
|
||||
use deno_graph::Position;
|
||||
use deno_graph::PositionRange;
|
||||
use deno_graph::SpecifierWithRange;
|
||||
use deno_runtime::worker::MainWorker;
|
||||
use deno_semver::npm::NpmPackageReqReference;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Match;
|
||||
use regex::Regex;
|
||||
|
||||
use super::cdp;
|
||||
|
||||
fn comment_source_to_position_range(
|
||||
comment_start: SourcePos,
|
||||
m: &Match,
|
||||
text_info: &SourceTextInfo,
|
||||
is_jsx_import_source: bool,
|
||||
) -> PositionRange {
|
||||
// the comment text starts after the double slash or slash star, so add 2
|
||||
let comment_start = comment_start + 2;
|
||||
// -1 and +1 to include the quotes, but not for jsx import sources because
|
||||
// they don't have quotes
|
||||
let padding = if is_jsx_import_source { 0 } else { 1 };
|
||||
PositionRange {
|
||||
start: Position::from_source_pos(
|
||||
comment_start + m.start() - padding,
|
||||
text_info,
|
||||
),
|
||||
end: Position::from_source_pos(
|
||||
comment_start + m.end() + padding,
|
||||
text_info,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// We store functions used in the repl on this object because
|
||||
/// the user might modify the `Deno` global or delete it outright.
|
||||
pub static REPL_INTERNALS_NAME: Lazy<String> = Lazy::new(|| {
|
||||
|
@ -131,6 +164,12 @@ pub struct TsEvaluateResponse {
|
|||
pub value: cdp::EvaluateResponse,
|
||||
}
|
||||
|
||||
struct ReplJsxState {
|
||||
factory: String,
|
||||
frag_factory: String,
|
||||
import_source: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ReplSession {
|
||||
npm_resolver: Arc<dyn CliNpmResolver>,
|
||||
resolver: Arc<CliGraphResolver>,
|
||||
|
@ -145,6 +184,7 @@ pub struct ReplSession {
|
|||
test_event_sender: TestEventSender,
|
||||
/// This is only optional because it's temporarily taken when evaluating.
|
||||
test_event_receiver: Option<tokio::sync::mpsc::UnboundedReceiver<TestEvent>>,
|
||||
jsx: ReplJsxState,
|
||||
}
|
||||
|
||||
impl ReplSession {
|
||||
|
@ -212,6 +252,11 @@ impl ReplSession {
|
|||
main_module,
|
||||
test_event_sender,
|
||||
test_event_receiver: Some(test_event_receiver),
|
||||
jsx: ReplJsxState {
|
||||
factory: "React.createElement".to_string(),
|
||||
frag_factory: "React.Fragment".to_string(),
|
||||
import_source: None,
|
||||
},
|
||||
};
|
||||
|
||||
// inject prelude
|
||||
|
@ -515,32 +560,39 @@ impl ReplSession {
|
|||
&mut self,
|
||||
expression: &str,
|
||||
) -> Result<TsEvaluateResponse, AnyError> {
|
||||
let parsed_module = deno_ast::parse_module(deno_ast::ParseParams {
|
||||
specifier: "repl.ts".to_string(),
|
||||
text_info: deno_ast::SourceTextInfo::from_string(expression.to_string()),
|
||||
media_type: deno_ast::MediaType::TypeScript,
|
||||
capture_tokens: false,
|
||||
maybe_syntax: None,
|
||||
scope_analysis: false,
|
||||
})?;
|
||||
let parsed_source =
|
||||
match parse_source_as(expression.to_string(), deno_ast::MediaType::Tsx) {
|
||||
Ok(parsed) => parsed,
|
||||
Err(err) => {
|
||||
if let Ok(parsed) = parse_source_as(
|
||||
expression.to_string(),
|
||||
deno_ast::MediaType::TypeScript,
|
||||
) {
|
||||
parsed
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self
|
||||
.check_for_npm_or_node_imports(&parsed_module.program())
|
||||
.check_for_npm_or_node_imports(&parsed_source.program())
|
||||
.await?;
|
||||
|
||||
let transpiled_src = parsed_module
|
||||
self.analyze_and_handle_jsx(&parsed_source);
|
||||
|
||||
let transpiled_src = parsed_source
|
||||
.transpile(&deno_ast::EmitOptions {
|
||||
emit_metadata: false,
|
||||
source_map: false,
|
||||
inline_source_map: false,
|
||||
inline_sources: false,
|
||||
imports_not_used_as_values: ImportsNotUsedAsValues::Preserve,
|
||||
// JSX is not supported in the REPL
|
||||
transform_jsx: false,
|
||||
transform_jsx: true,
|
||||
jsx_automatic: false,
|
||||
jsx_development: false,
|
||||
jsx_factory: "React.createElement".into(),
|
||||
jsx_fragment_factory: "React.Fragment".into(),
|
||||
jsx_factory: self.jsx.factory.clone(),
|
||||
jsx_fragment_factory: self.jsx.frag_factory.clone(),
|
||||
jsx_import_source: None,
|
||||
precompile_jsx: false,
|
||||
var_decl_imports: true,
|
||||
|
@ -548,7 +600,7 @@ impl ReplSession {
|
|||
.text;
|
||||
|
||||
let value = self
|
||||
.evaluate_expression(&format!("'use strict'; void 0;\n{transpiled_src}"))
|
||||
.evaluate_expression(&format!("'use strict'; void 0;{transpiled_src}"))
|
||||
.await?;
|
||||
|
||||
Ok(TsEvaluateResponse {
|
||||
|
@ -557,6 +609,26 @@ impl ReplSession {
|
|||
})
|
||||
}
|
||||
|
||||
fn analyze_and_handle_jsx(&mut self, parsed_source: &ParsedSource) {
|
||||
let Some(analyzed_pragmas) = analyze_jsx_pragmas(parsed_source) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !analyzed_pragmas.has_any() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(jsx) = analyzed_pragmas.jsx {
|
||||
self.jsx.factory = jsx.text;
|
||||
}
|
||||
if let Some(jsx_frag) = analyzed_pragmas.jsx_fragment {
|
||||
self.jsx.frag_factory = jsx_frag.text;
|
||||
}
|
||||
if let Some(jsx_import_source) = analyzed_pragmas.jsx_import_source {
|
||||
self.jsx.import_source = Some(jsx_import_source.text);
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_for_npm_or_node_imports(
|
||||
&mut self,
|
||||
program: &swc_ast::Program,
|
||||
|
@ -679,3 +751,121 @@ impl Visit for ImportCollector {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_source_as(
|
||||
source: String,
|
||||
media_type: deno_ast::MediaType,
|
||||
) -> Result<deno_ast::ParsedSource, AnyError> {
|
||||
let specifier = if media_type == deno_ast::MediaType::Tsx {
|
||||
"repl.tsx"
|
||||
} else {
|
||||
"repl.ts"
|
||||
};
|
||||
|
||||
let parsed = deno_ast::parse_module(deno_ast::ParseParams {
|
||||
specifier: specifier.to_string(),
|
||||
text_info: deno_ast::SourceTextInfo::from_string(source),
|
||||
media_type,
|
||||
capture_tokens: true,
|
||||
maybe_syntax: None,
|
||||
scope_analysis: false,
|
||||
})?;
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
// TODO(bartlomieju): remove these and use regexes from `deno_graph`
|
||||
/// Matches the `@jsxImportSource` pragma.
|
||||
static JSX_IMPORT_SOURCE_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?i)^[\s*]*@jsxImportSource\s+(\S+)").unwrap());
|
||||
/// Matches the `@jsx` pragma.
|
||||
static JSX_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?i)^[\s*]*@jsx\s+(\S+)").unwrap());
|
||||
/// Matches the `@jsxFrag` pragma.
|
||||
static JSX_FRAG_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?i)^[\s*]*@jsxFrag\s+(\S+)").unwrap());
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct AnalyzedJsxPragmas {
|
||||
/// Information about `@jsxImportSource` pragma.
|
||||
jsx_import_source: Option<SpecifierWithRange>,
|
||||
|
||||
/// Matches the `@jsx` pragma.
|
||||
jsx: Option<SpecifierWithRange>,
|
||||
|
||||
/// Matches the `@jsxFrag` pragma.
|
||||
jsx_fragment: Option<SpecifierWithRange>,
|
||||
}
|
||||
|
||||
impl AnalyzedJsxPragmas {
|
||||
fn has_any(&self) -> bool {
|
||||
self.jsx_import_source.is_some()
|
||||
|| self.jsx.is_some()
|
||||
|| self.jsx_fragment.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Analyze provided source and return information about carious pragmas
|
||||
/// used to configure the JSX tranforms.
|
||||
fn analyze_jsx_pragmas(
|
||||
parsed_source: &ParsedSource,
|
||||
) -> Option<AnalyzedJsxPragmas> {
|
||||
if !matches!(
|
||||
parsed_source.media_type(),
|
||||
deno_ast::MediaType::Jsx | deno_ast::MediaType::Tsx
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut analyzed_pragmas = AnalyzedJsxPragmas::default();
|
||||
|
||||
for c in parsed_source.get_leading_comments().iter() {
|
||||
if c.kind != CommentKind::Block {
|
||||
continue; // invalid
|
||||
}
|
||||
|
||||
if let Some(captures) = JSX_IMPORT_SOURCE_RE.captures(&c.text) {
|
||||
if let Some(m) = captures.get(1) {
|
||||
analyzed_pragmas.jsx_import_source = Some(SpecifierWithRange {
|
||||
text: m.as_str().to_string(),
|
||||
range: comment_source_to_position_range(
|
||||
c.start(),
|
||||
&m,
|
||||
parsed_source.text_info(),
|
||||
true,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(captures) = JSX_RE.captures(&c.text) {
|
||||
if let Some(m) = captures.get(1) {
|
||||
analyzed_pragmas.jsx = Some(SpecifierWithRange {
|
||||
text: m.as_str().to_string(),
|
||||
range: comment_source_to_position_range(
|
||||
c.start(),
|
||||
&m,
|
||||
parsed_source.text_info(),
|
||||
false,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(captures) = JSX_FRAG_RE.captures(&c.text) {
|
||||
if let Some(m) = captures.get(1) {
|
||||
analyzed_pragmas.jsx_fragment = Some(SpecifierWithRange {
|
||||
text: m.as_str().to_string(),
|
||||
range: comment_source_to_position_range(
|
||||
c.start(),
|
||||
&m,
|
||||
parsed_source.text_info(),
|
||||
false,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(analyzed_pragmas)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue