Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): support React 17 JSX transforms #12631

Merged
merged 11 commits into from
Nov 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
kitsonk marked this conversation as resolved.
Show resolved Hide resolved
/// 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