Skip to content

Commit

Permalink
feat(cli): support React 17 JSX transforms (#12631)
Browse files Browse the repository at this point in the history
Closes #8440
  • Loading branch information
kitsonk authored Nov 9, 2021
1 parent 45425c1 commit f5eb177
Show file tree
Hide file tree
Showing 40 changed files with 1,115 additions and 173 deletions.
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ winres = "0.1.11"
[dependencies]
deno_ast = { version = "0.5.0", features = ["bundler", "codegen", "dep_graph", "module_specifier", "proposal", "react", "sourcemap", "transforms", "typescript", "view", "visit"] }
deno_core = { version = "0.105.0", path = "../core" }
deno_doc = "0.19.0"
deno_graph = "0.10.0"
deno_doc = "0.20.0"
deno_graph = "0.11.1"
deno_lint = { version = "0.19.0", features = ["docs"] }
deno_runtime = { version = "0.31.0", path = "../runtime" }
deno_tls = { version = "0.10.0", path = "../ext/tls" }
Expand Down
143 changes: 140 additions & 3 deletions cli/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,26 @@ pub struct EmitOptions {
pub inline_source_map: bool,
/// Should the sources be inlined in the source map. Defaults to `true`.
pub inline_sources: bool,
// Should a corresponding .map file be created for the output. This should be
// false if inline_source_map is true. Defaults to `false`.
/// Should a corresponding .map file be created for the output. This should be
/// false if inline_source_map is true. Defaults to `false`.
pub source_map: bool,
/// `true` if the program should use an implicit JSX import source/the "new"
/// JSX transforms.
pub jsx_automatic: bool,
/// If JSX is automatic, if it is in development mode, meaning that it should
/// import `jsx-dev-runtime` and transform JSX using `jsxDEV` import from the
/// JSX import source as well as provide additional debug information to the
/// JSX factory.
pub jsx_development: bool,
/// When transforming JSX, what value should be used for the JSX factory.
/// Defaults to `React.createElement`.
pub jsx_factory: String,
/// When transforming JSX, what value should be used for the JSX fragment
/// factory. Defaults to `React.Fragment`.
pub jsx_fragment_factory: String,
/// The string module specifier to implicitly import JSX factories from when
/// transpiling JSX.
pub jsx_import_source: Option<String>,
/// Should JSX be transformed or preserved. Defaults to `true`.
pub transform_jsx: bool,
/// Should import declarations be transformed to variable declarations.
Expand All @@ -146,8 +157,11 @@ impl Default for EmitOptions {
inline_source_map: true,
inline_sources: true,
source_map: false,
jsx_automatic: false,
jsx_development: false,
jsx_factory: "React.createElement".into(),
jsx_fragment_factory: "React.Fragment".into(),
jsx_import_source: None,
transform_jsx: true,
repl_imports: false,
}
Expand All @@ -164,15 +178,25 @@ impl From<config_file::TsConfig> for EmitOptions {
"error" => ImportsNotUsedAsValues::Error,
_ => ImportsNotUsedAsValues::Remove,
};
let (transform_jsx, jsx_automatic, jsx_development) =
match options.jsx.as_str() {
"react" => (true, false, false),
"react-jsx" => (true, true, false),
"react-jsxdev" => (true, true, true),
_ => (false, false, false),
};
EmitOptions {
emit_metadata: options.emit_decorator_metadata,
imports_not_used_as_values,
inline_source_map: options.inline_source_map,
inline_sources: options.inline_sources,
source_map: options.source_map,
jsx_automatic,
jsx_development,
jsx_factory: options.jsx_factory,
jsx_fragment_factory: options.jsx_fragment_factory,
transform_jsx: options.jsx == "react",
jsx_import_source: options.jsx_import_source,
transform_jsx,
repl_imports: false,
}
}
Expand Down Expand Up @@ -355,6 +379,13 @@ fn fold_program(
// this will use `Object.assign()` instead of the `_extends` helper
// when spreading props.
use_builtins: true,
runtime: if options.jsx_automatic {
Some(react::Runtime::Automatic)
} else {
None
},
development: options.jsx_development,
import_source: options.jsx_import_source.clone().unwrap_or_default(),
..Default::default()
},
top_level_mark,
Expand Down Expand Up @@ -495,6 +526,112 @@ function App() {
assert_eq!(&code[..expected.len()], expected);
}

#[test]
fn test_transpile_jsx_import_source_pragma() {
let specifier = resolve_url_or_path("https://deno.land/x/mod.tsx")
.expect("could not resolve specifier");
let source = r#"
/** @jsxImportSource jsx_lib */
function App() {
return (
<div><></></div>
);
}"#;
let module = parse_module(ParseParams {
specifier: specifier.as_str().to_string(),
source: SourceTextInfo::from_string(source.to_string()),
media_type: deno_ast::MediaType::Jsx,
capture_tokens: false,
maybe_syntax: None,
scope_analysis: true,
})
.unwrap();
let (code, _) = transpile(&module, &EmitOptions::default()).unwrap();
let expected = r#"import { jsx as _jsx, Fragment as _Fragment } from "jsx_lib/jsx-runtime";
/** @jsxImportSource jsx_lib */ function App() {
return(/*#__PURE__*/ _jsx("div", {
children: /*#__PURE__*/ _jsx(_Fragment, {
})
}));
"#;
assert_eq!(&code[..expected.len()], expected);
}

#[test]
fn test_transpile_jsx_import_source_no_pragma() {
let specifier = resolve_url_or_path("https://deno.land/x/mod.tsx")
.expect("could not resolve specifier");
let source = r#"
function App() {
return (
<div><></></div>
);
}"#;
let module = parse_module(ParseParams {
specifier: specifier.as_str().to_string(),
source: SourceTextInfo::from_string(source.to_string()),
media_type: deno_ast::MediaType::Jsx,
capture_tokens: false,
maybe_syntax: None,
scope_analysis: true,
})
.unwrap();
let emit_options = EmitOptions {
jsx_automatic: true,
jsx_import_source: Some("jsx_lib".to_string()),
..Default::default()
};
let (code, _) = transpile(&module, &emit_options).unwrap();
let expected = r#"import { jsx as _jsx, Fragment as _Fragment } from "jsx_lib/jsx-runtime";
function App() {
return(/*#__PURE__*/ _jsx("div", {
children: /*#__PURE__*/ _jsx(_Fragment, {
})
}));
}
"#;
assert_eq!(&code[..expected.len()], expected);
}

// TODO(@kitsonk) https://github.com/swc-project/swc/issues/2656
// #[test]
// fn test_transpile_jsx_import_source_no_pragma_dev() {
// let specifier = resolve_url_or_path("https://deno.land/x/mod.tsx")
// .expect("could not resolve specifier");
// let source = r#"
// function App() {
// return (
// <div><></></div>
// );
// }"#;
// let module = parse_module(ParseParams {
// specifier: specifier.as_str().to_string(),
// source: SourceTextInfo::from_string(source.to_string()),
// media_type: deno_ast::MediaType::Jsx,
// capture_tokens: false,
// maybe_syntax: None,
// scope_analysis: true,
// })
// .unwrap();
// let emit_options = EmitOptions {
// jsx_automatic: true,
// jsx_import_source: Some("jsx_lib".to_string()),
// jsx_development: true,
// ..Default::default()
// };
// let (code, _) = transpile(&module, &emit_options).unwrap();
// let expected = r#"import { jsx as _jsx, Fragment as _Fragment } from "jsx_lib/jsx-dev-runtime";
// function App() {
// return(/*#__PURE__*/ _jsx("div", {
// children: /*#__PURE__*/ _jsx(_Fragment, {
// })
// }));
// }
// "#;
// assert_eq!(&code[..expected.len()], expected);
// }

#[test]
fn test_transpile_decorators() {
let specifier = resolve_url_or_path("https://deno.land/x/mod.ts")
Expand Down
10 changes: 5 additions & 5 deletions cli/compat/esm_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ use regex::Regex;
use std::path::PathBuf;

#[derive(Debug, Default)]
pub(crate) struct NodeEsmResolver<'a> {
maybe_import_map_resolver: Option<ImportMapResolver<'a>>,
pub(crate) struct NodeEsmResolver {
maybe_import_map_resolver: Option<ImportMapResolver>,
}

impl<'a> NodeEsmResolver<'a> {
pub fn new(maybe_import_map_resolver: Option<ImportMapResolver<'a>>) -> Self {
impl NodeEsmResolver {
pub fn new(maybe_import_map_resolver: Option<ImportMapResolver>) -> Self {
Self {
maybe_import_map_resolver,
}
Expand All @@ -30,7 +30,7 @@ impl<'a> NodeEsmResolver<'a> {
}
}

impl Resolver for NodeEsmResolver<'_> {
impl Resolver for NodeEsmResolver {
fn resolve(
&self,
specifier: &str,
Expand Down
55 changes: 49 additions & 6 deletions cli/config_file.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.

use crate::fs_util::canonicalize_path;

use deno_core::error::anyhow;
use deno_core::error::custom_error;
use deno_core::error::AnyError;
use deno_core::error::Context;
use deno_core::serde::Deserialize;
Expand All @@ -17,6 +19,9 @@ use std::fmt;
use std::path::Path;
use std::path::PathBuf;

pub(crate) type MaybeImportsResult =
Result<Option<Vec<(ModuleSpecifier, Vec<String>)>>, AnyError>;

/// The transpile options that are significant out of a user provided tsconfig
/// file, that we want to deserialize out of the final config for a transpile.
#[derive(Debug, Deserialize)]
Expand All @@ -31,13 +36,16 @@ pub struct EmitConfigOptions {
pub jsx: String,
pub jsx_factory: String,
pub jsx_fragment_factory: String,
pub jsx_import_source: Option<String>,
}

/// There are certain compiler options that can impact what modules are part of
/// a module graph, which need to be deserialized into a structure for analysis.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompilerOptions {
pub jsx: Option<String>,
pub jsx_import_source: Option<String>,
pub types: Option<Vec<String>>,
}

Expand Down Expand Up @@ -404,15 +412,50 @@ impl ConfigFile {

/// If the configuration file contains "extra" modules (like TypeScript
/// `"types"`) options, return them as imports to be added to a module graph.
pub fn to_maybe_imports(
&self,
) -> Option<Vec<(ModuleSpecifier, Vec<String>)>> {
pub fn to_maybe_imports(&self) -> MaybeImportsResult {
let mut imports = Vec::new();
let compiler_options_value =
if let Some(value) = self.json.compiler_options.as_ref() {
value
} else {
return Ok(None);
};
let compiler_options: CompilerOptions =
serde_json::from_value(compiler_options_value.clone())?;
let referrer = ModuleSpecifier::from_file_path(&self.path)
.map_err(|_| custom_error("TypeError", "bad config file specifier"))?;
if let Some(types) = compiler_options.types {
imports.extend(types);
}
if compiler_options.jsx == Some("react-jsx".to_string()) {
imports.push(format!(
"{}/jsx-runtime",
compiler_options.jsx_import_source.ok_or_else(|| custom_error("TypeError", "Compiler option 'jsx' set to 'react-jsx', but no 'jsxImportSource' defined."))?
));
} else if compiler_options.jsx == Some("react-jsxdev".to_string()) {
imports.push(format!(
"{}/jsx-dev-runtime",
compiler_options.jsx_import_source.ok_or_else(|| custom_error("TypeError", "Compiler option 'jsx' set to 'react-jsxdev', but no 'jsxImportSource' defined."))?
));
}
if !imports.is_empty() {
Ok(Some(vec![(referrer, imports)]))
} else {
Ok(None)
}
}

/// Based on the compiler options in the configuration file, return the
/// implied JSX import source module.
pub fn to_maybe_jsx_import_source_module(&self) -> Option<String> {
let compiler_options_value = self.json.compiler_options.as_ref()?;
let compiler_options: CompilerOptions =
serde_json::from_value(compiler_options_value.clone()).ok()?;
let referrer = ModuleSpecifier::from_file_path(&self.path).ok()?;
let types = compiler_options.types?;
Some(vec![(referrer, types)])
match compiler_options.jsx.as_deref() {
Some("react-jsx") => Some("jsx-runtime".to_string()),
Some("react-jsxdev") => Some("jsx-dev-runtime".to_string()),
_ => None,
}
}

pub fn to_fmt_config(&self) -> Result<Option<FmtConfig>, AnyError> {
Expand Down
9 changes: 7 additions & 2 deletions cli/dts/lib.deno.unstable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,20 @@ declare namespace Deno {
/** Emit the source alongside the source maps within a single file; requires
* `inlineSourceMap` or `sourceMap` to be set. Defaults to `false`. */
inlineSources?: boolean;
/** Support JSX in `.tsx` files: `"react"`, `"preserve"`, `"react-native"`.
/** Support JSX in `.tsx` files: `"react"`, `"preserve"`, `"react-native"`,
* `"react-jsx", `"react-jsxdev"`.
* Defaults to `"react"`. */
jsx?: "react" | "preserve" | "react-native";
jsx?: "react" | "preserve" | "react-native" | "react-jsx" | "react-jsx-dev";
/** Specify the JSX factory function to use when targeting react JSX emit,
* e.g. `React.createElement` or `h`. Defaults to `React.createElement`. */
jsxFactory?: string;
/** Specify the JSX fragment factory function to use when targeting react
* JSX emit, e.g. `Fragment`. Defaults to `React.Fragment`. */
jsxFragmentFactory?: string;
/** Declares the module specifier to be used for importing the `jsx` and
* `jsxs` factory functions when using jsx as `"react-jsx"` or
* `"react-jsxdev"`. Defaults to `"react"`. */
jsxImportSource?: string;
/** Resolve keyof to string valued property names only (no numbers or
* symbols). Defaults to `false`. */
keyofStringsOnly?: string;
Expand Down
Loading

0 comments on commit f5eb177

Please sign in to comment.