Skip to content

Commit

Permalink
Introduce JSX support (#5668)
Browse files Browse the repository at this point in the history
* Implement jsx option

* Add tests

* Pass jsx options to SWC

Signed-off-by: Martin Idel <[email protected]>

* Add JsxElement to ast-types

* Add JsxOpeningElement

* Add JSXElementChild -> JSXElement

* Add JSXElementChild -> JSXText

* WIP Add JsxAttribute

* Add JSXElementChild -> JSXExprContainer and JSXEmptyExpr

* Add JsxFragment

* Align AST types with official types and verify

This adds acorn-jsx to test the AST against.
Tests will fail if the check fails.

* Sort converters into alphabetical order

* Associate identifier with variable

* Use correct JSX types

* Ensure React is included when JSX is used

* Rework JSX option and global variable handling for preserving

* Start work on transpilation support

* Improve option and transpilation support

* Handle missing jsx factory

* Add additional tests and support expressions

* Handle JSXText

* Support attributes transpilation

* Support fragments

* Extract shared code from fragments

* Support JSXText in all possible positions

* Support all possible JSX attribute types

* Support JSXMemberExpression

* Support JSXSpreadChild

* Support JSXSpreadAttribute

* Use correct span for empty expressions

* Improve formatting for macro names

* Move converters into separate files

* Make everything except the parse function only crate public

* use macros for JSX where possible

* Initial jsx rendering logic for simple cases

* Split classic and automatic mode

* Handle jsx without children

* Handle jsx with children

* Fix fragment rendering

* Prepare for fallback rendering

We still need to switch the arguments to old mode as well
for the fallback case.

* Reenable tests for now

* Update linting

* Improve JSX rendering and move to parent element

* Align classic attribute rendering with automatic

* Refine classic mode rendering

to make extraction easier

* Refine automatic mode rendering

* Fix automatic rendering

* Extract attribute rendering

* Extract shared functionality

* Move initialize and include functionality to shared base element

* Extract fragment opening rendering to JSXOpeningFragment

* Share rendering functionality with fragments

* Deconflict closing elements

* Handle native elements

* Add additional checks

* Ensure CLI supports presets

* Add JSX options to REPL

* Add documentation

* Improve coverage

* Fix local browser build

* Add jsx example

* Refine docs

---------

Signed-off-by: Martin Idel <[email protected]>
Co-authored-by: Martin Idel <[email protected]>
Co-authored-by: Felix Huttmann <[email protected]>
Co-authored-by: Alexander Droll <[email protected]>
Co-authored-by: Timo Peter <[email protected]>
  • Loading branch information
5 people authored Oct 2, 2024
1 parent ed98e08 commit ca186ee
Show file tree
Hide file tree
Showing 350 changed files with 4,171 additions and 680 deletions.
4 changes: 4 additions & 0 deletions browser/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,7 @@ export function resolve(...paths: string[]): string {

return resolvedParts.join('/');
}

// Used for running the browser build locally in Vite
export const win32 = {};
export const posix = {};
3 changes: 2 additions & 1 deletion browser/src/wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { parse } from '../../wasm/bindings_wasm.js';
export async function parseAsync(
code: string,
allowReturnOutsideFunction: boolean,
jsx: boolean,
_signal?: AbortSignal | undefined | null
) {
return parse(code, allowReturnOutsideFunction);
return parse(code, allowReturnOutsideFunction, jsx);
}
4 changes: 4 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import replaceBrowserModules from '../../build-plugins/replace-browser-modules';
import '../declarations.d';
import { examplesPlugin } from './create-examples';
import { renderMermaidGraphsPlugin } from './mermaid';
import { replacePathPicomatch } from './replace-path-picomatch';
import { transposeTables } from './transpose-tables';
import { buildEnd, callback, transformPageData } from './verify-anchors';

Expand Down Expand Up @@ -157,7 +158,10 @@ export default defineConfig({
title: 'Rollup',
transformPageData,
vite: {
optimizeDeps: { exclude: ['@rollup/pluginutils'] },
plugins: [
replacePathPicomatch(),
replaceBrowserModules(),
renderMermaidGraphsPlugin(),
replaceBrowserModules(),
{
Expand Down
22 changes: 22 additions & 0 deletions docs/.vitepress/replace-path-picomatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import path from 'path';
import type { Plugin } from 'vite';

export function replacePathPicomatch(): Plugin {
return {
enforce: 'pre',
load(id) {
if (id === 'picomatch') {
return 'export default {}';
}
},
name: 'replace-picomatch',
resolveId(source) {
if (source === 'picomatch') {
return { id: 'picomatch' };
}
if (source === 'path') {
return path.resolve(__dirname, '../../browser/src/path.ts');
}
}
};
}
264 changes: 220 additions & 44 deletions docs/configuration-options/index.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions docs/repl/examples/00/example.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"title": "Tree-shaking",
"title": "Named exports",
"options": {
"treeshake": true
"output": {
"exports": "auto",
"esModule": "if-default-prop"
}
}
}
19 changes: 16 additions & 3 deletions docs/repl/examples/00/modules/main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
// TREE-SHAKING
import { cube } from './maths.js';
// NAMED EXPORTS
// There are many ways to export bindings
// from an ES2015 module
export var foo = 1;

console.log(cube(5)); // 125
export function bar() {
// try changing this to `foo++`
// when generating CommonJS
return foo;
}

function baz() {
return bar();
}

export * from './qux';
export { baz };
File renamed without changes.
7 changes: 2 additions & 5 deletions docs/repl/examples/01/example.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{
"title": "Named exports",
"title": "Tree-shaking",
"options": {
"output": {
"exports": "auto",
"esModule": "if-default-prop"
}
"treeshake": true
}
}
19 changes: 3 additions & 16 deletions docs/repl/examples/01/modules/main.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
// NAMED EXPORTS
// There are many ways to export bindings
// from an ES2015 module
export var foo = 1;
// TREE-SHAKING
import { cube } from './maths.js';

export function bar() {
// try changing this to `foo++`
// when generating CommonJS
return foo;
}

function baz() {
return bar();
}

export * from './qux';
export { baz };
console.log(cube(5)); // 125
File renamed without changes.
14 changes: 7 additions & 7 deletions docs/repl/examples/07/example.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"title": "Multiple Entry Modules",
"entryModules": ["main.js", "otherEntry.js"],
"options": {
"output": {
"minifyInternalExports": false,
"preserveModules": false
}
}
"entryModules": ["main.js", "otherEntry.js"],
"options": {
"output": {
"minifyInternalExports": false,
"preserveModules": false
}
}
}
8 changes: 8 additions & 0 deletions docs/repl/examples/08/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"title": "JSX",
"options": {
"jsx": {
"mode": "preserve"
}
}
}
6 changes: 6 additions & 0 deletions docs/repl/examples/08/modules/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// JSX SUPPORT
// Try different jsx.mode and see how it is transformed
import './other.js';
const Foo = ({ world }) => <div>Hello {world}!</div>;

console.log(<Foo world="World" />);
2 changes: 2 additions & 0 deletions docs/repl/examples/08/modules/other.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const Foo = () => <div>This is deconflicted!</div>;
console.log(<Foo />);
46 changes: 44 additions & 2 deletions docs/repl/stores/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export const useOptions = defineStore('options2', () => {
return value != null && interopFormats.has(value);
});
const externalImports = computed(() => rollupOutputStore.output?.externalImports || []);
const isJsxEnabled = computed(() => optionJsx.value.value === true);
const isTreeshakeEnabled = computed(() =>
[undefined, true].includes(optionTreeshake.value.value as any)
);
Expand Down Expand Up @@ -375,6 +376,39 @@ export const useOptions = defineStore('options2', () => {
defaultValue: false,
name: 'shimMissingExports'
});
const optionJsx = getSelect({
defaultValue: false,
name: 'jsx',
options: () => [false, true, 'preserve', 'preserve-react', 'react', 'react-jsx']
});
const optionJsxFactory = getString({
available: isJsxEnabled,
name: 'jsx.factory'
});
const optionJsxFragment = getString({
available: computed(() => isJsxEnabled.value && optionJsxMode.value.value !== 'automatic'),
name: 'jsx.fragment'
});
const optionJsxImportSource = getString({
available: isJsxEnabled,
name: 'jsx.importSource'
});
const optionJsxJsxImportSource = getString({
available: computed(() => isJsxEnabled.value && optionJsxMode.value.value === 'automatic'),
name: 'jsx.jsxImportSource'
});
const optionJsxMode = getSelect({
available: isJsxEnabled,
defaultValue: 'classic',
name: 'jsx.mode',
options: () => ['classic', 'automatic', 'preserve']
});
const optionJsxPreset = getSelect({
available: isJsxEnabled,
defaultValue: null,
name: 'jsx.preset',
options: () => [null, 'preserve', 'preserve-react', 'react', 'react-jsx']
});
const optionTreeshake = getSelect({
defaultValue: true,
name: 'treeshake',
Expand Down Expand Up @@ -422,6 +456,13 @@ export const useOptions = defineStore('options2', () => {
const optionList: OptionType[] = [
optionContext,
optionExperimentalLogSideEffects,
optionJsx,
optionJsxMode,
optionJsxFactory,
optionJsxFragment,
optionJsxImportSource,
optionJsxJsxImportSource,
optionJsxPreset,
optionOutputAmdAutoId,
optionOutputAmdBasePath,
optionOutputAmdDefine,
Expand Down Expand Up @@ -511,7 +552,8 @@ export const useOptions = defineStore('options2', () => {
while ((key = path.shift())) {
subOptions = subOptions?.[key];
}
value.value = name === 'treeshake' && typeof subOptions === 'object' ? true : subOptions;
value.value =
['jsx', 'treeshake'].includes(name) && typeof subOptions === 'object' ? true : subOptions;
}
}
};
Expand Down Expand Up @@ -652,7 +694,7 @@ function getOptionsObject(options: Ref<Option[]>): Ref<RollupOptions> {
let key: string | undefined;
let subOptions: any = object;
while ((key = path.shift())) {
// Special logic to handle treeshake option
// Special logic to handle jsx/treeshake option
if (subOptions[key] === true) {
subOptions[key] = {};
}
Expand Down
1 change: 1 addition & 0 deletions docs/repl/stores/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const useRollup = defineStore('rollup', () => {
instance
};
} catch (error) {
console.error(error);
loaded.value = {
error: error as Error,
instance: null
Expand Down
4 changes: 2 additions & 2 deletions native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

/* auto-generated by NAPI-RS */

export declare function parse(code: string, allowReturnOutsideFunction: boolean): Buffer
export declare function parseAsync(code: string, allowReturnOutsideFunction: boolean, signal?: AbortSignal | undefined | null): Promise<Buffer>
export declare function parse(code: string, allowReturnOutsideFunction: boolean, jsx: boolean): Buffer
export declare function parseAsync(code: string, allowReturnOutsideFunction: boolean, jsx: boolean, signal?: AbortSignal | undefined | null): Promise<Buffer>
export declare function xxhashBase64Url(input: Uint8Array): string
export declare function xxhashBase36(input: Uint8Array): string
export declare function xxhashBase16(input: Uint8Array): string
31 changes: 1 addition & 30 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
"@vue/language-server": "^2.1.6",
"acorn": "^8.12.1",
"acorn-import-assertions": "^1.9.0",
"acorn-jsx": "^5.3.2",
"buble": "^0.20.0",
"builtin-modules": "^4.0.0",
"chokidar": "^3.6.0",
Expand Down
16 changes: 13 additions & 3 deletions rust/bindings_napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ static ALLOC: mimalloc_rust::GlobalMiMalloc = mimalloc_rust::GlobalMiMalloc;
pub struct ParseTask {
pub code: String,
pub allow_return_outside_function: bool,
pub jsx: bool,
}

#[napi]
Expand All @@ -20,7 +21,14 @@ impl Task for ParseTask {
type JsValue = Buffer;

fn compute(&mut self) -> Result<Self::Output> {
Ok(parse_ast(self.code.clone(), self.allow_return_outside_function).into())
Ok(
parse_ast(
self.code.clone(),
self.allow_return_outside_function,
self.jsx,
)
.into(),
)
}

fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
Expand All @@ -29,20 +37,22 @@ impl Task for ParseTask {
}

#[napi]
pub fn parse(code: String, allow_return_outside_function: bool) -> Buffer {
parse_ast(code, allow_return_outside_function).into()
pub fn parse(code: String, allow_return_outside_function: bool, jsx: bool) -> Buffer {
parse_ast(code, allow_return_outside_function, jsx).into()
}

#[napi]
pub fn parse_async(
code: String,
allow_return_outside_function: bool,
jsx: bool,
signal: Option<AbortSignal>,
) -> AsyncTask<ParseTask> {
AsyncTask::with_optional_signal(
ParseTask {
code,
allow_return_outside_function,
jsx,
},
signal,
)
Expand Down
4 changes: 2 additions & 2 deletions rust/bindings_wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use parse_ast::parse_ast;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn parse(code: String, allow_return_outside_function: bool) -> Vec<u8> {
parse_ast(code, allow_return_outside_function)
pub fn parse(code: String, allow_return_outside_function: bool, jsx: bool) -> Vec<u8> {
parse_ast(code, allow_return_outside_function, jsx)
}

#[wasm_bindgen(js_name=xxhashBase64Url)]
Expand Down
2 changes: 1 addition & 1 deletion rust/parse_ast/src/ast_nodes/array_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::convert_ast::converter::ast_constants::{
use crate::convert_ast::converter::AstConverter;

impl<'a> AstConverter<'a> {
pub fn store_array_expression(&mut self, array_literal: &ArrayLit) {
pub(crate) fn store_array_expression(&mut self, array_literal: &ArrayLit) {
let end_position = self.add_type_and_start(
&TYPE_ARRAY_EXPRESSION,
&array_literal.span,
Expand Down
2 changes: 1 addition & 1 deletion rust/parse_ast/src/ast_nodes/array_pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::convert_ast::converter::ast_constants::{
use crate::convert_ast::converter::AstConverter;

impl<'a> AstConverter<'a> {
pub fn store_array_pattern(&mut self, array_pattern: &ArrayPat) {
pub(crate) fn store_array_pattern(&mut self, array_pattern: &ArrayPat) {
let end_position = self.add_type_and_start(
&TYPE_ARRAY_PATTERN,
&array_pattern.span,
Expand Down
Loading

0 comments on commit ca186ee

Please sign in to comment.