Skip to content

Commit f3351ba

Browse files
committed
Add support for automatic runtime
1 parent ebf23e9 commit f3351ba

File tree

3 files changed

+328
-43
lines changed

3 files changed

+328
-43
lines changed

index.js

Lines changed: 163 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ module.exports = buildJsx
55
var walk = require('estree-walker').walk
66
var isIdentifierName = require('estree-util-is-identifier-name').name
77

8+
var regex = /@(jsx|jsxFrag|jsxImportSource|jsxRuntime)\s+(\S+)/g
9+
810
function buildJsx(tree, options) {
911
var settings = options || {}
10-
var pragma = settings.pragma
11-
var pragmaFrag = settings.pragmaFrag
12+
var automatic = settings.runtime === 'automatic'
13+
var annotations = {}
14+
var imports = {}
1215

1316
walk(tree, {enter: enter, leave: leave})
1417

@@ -26,12 +29,36 @@ function buildJsx(tree, options) {
2629
index = -1
2730

2831
while (++index < comments.length) {
29-
if ((match = /@jsx\s+(\S+)/.exec(comments[index].value))) {
30-
pragma = match[1]
32+
regex.lastIndex = 0
33+
34+
while ((match = regex.exec(comments[index].value))) {
35+
annotations[match[1]] = match[2]
3136
}
37+
}
38+
39+
if (annotations.jsxRuntime) {
40+
if (annotations.jsxRuntime === 'automatic') {
41+
automatic = true
42+
43+
if (annotations.jsx) {
44+
throw new Error('Unexpected `@jsx` pragma w/ automatic runtime')
45+
}
3246

33-
if ((match = /@jsxFrag\s+(\S+)/.exec(comments[index].value))) {
34-
pragmaFrag = match[1]
47+
if (annotations.jsxFrag) {
48+
throw new Error('Unexpected `@jsxFrag` pragma w/ automatic runtime')
49+
}
50+
} else if (annotations.jsxRuntime === 'classic') {
51+
automatic = false
52+
53+
if (annotations.jsxImportSource) {
54+
throw new Error('Unexpected `@jsxImportSource` w/ classic runtime')
55+
}
56+
} else {
57+
throw new Error(
58+
'Unexpected `jsxRuntime` `' +
59+
annotations.jsxRuntime +
60+
'`, expected `automatic` or `classic`'
61+
)
3562
}
3663
}
3764
}
@@ -41,19 +68,70 @@ function buildJsx(tree, options) {
4168
// eslint-disable-next-line complexity
4269
function leave(node) {
4370
var parameters
71+
var children
4472
var fields
4573
var objects
4674
var index
4775
var child
4876
var name
4977
var props
5078
var attributes
79+
var spread
80+
var key
81+
var callee
82+
var specifiers
83+
var prop
84+
85+
if (node.type === 'Program') {
86+
specifiers = []
87+
88+
if (imports.fragment) {
89+
specifiers.push({
90+
type: 'ImportSpecifier',
91+
imported: {type: 'Identifier', name: 'Fragment'},
92+
local: {type: 'Identifier', name: '_Fragment'}
93+
})
94+
}
95+
96+
if (imports.jsx) {
97+
specifiers.push({
98+
type: 'ImportSpecifier',
99+
imported: {type: 'Identifier', name: 'jsx'},
100+
local: {type: 'Identifier', name: '_jsx'}
101+
})
102+
}
103+
104+
if (imports.jsxs) {
105+
specifiers.push({
106+
type: 'ImportSpecifier',
107+
imported: {type: 'Identifier', name: 'jsxs'},
108+
local: {type: 'Identifier', name: '_jsxs'}
109+
})
110+
}
111+
112+
if (specifiers.length) {
113+
node.body.unshift({
114+
type: 'ImportDeclaration',
115+
specifiers: specifiers,
116+
source: {
117+
type: 'Literal',
118+
value:
119+
(annotations.jsxImportSource ||
120+
settings.importSource ||
121+
'react') + '/jsx-runtime'
122+
}
123+
})
124+
}
125+
}
51126

52127
if (node.type !== 'JSXElement' && node.type !== 'JSXFragment') {
53128
return
54129
}
55130

56131
parameters = []
132+
children = []
133+
objects = []
134+
fields = []
57135
index = -1
58136

59137
// Figure out `children`.
@@ -86,22 +164,20 @@ function buildJsx(tree, options) {
86164
}
87165
// Otherwise, this is an already compiled call.
88166

89-
parameters.push(child)
167+
children.push(child)
90168
}
91169

92170
// Do the stuff needed for elements.
93171
if (node.openingElement) {
94172
name = toIdentifier(node.openingElement.name)
95173

96-
// If the name could be an identifier, but start with something other than
97-
// a lowercase letter, it’s not a component.
174+
// If the name could be an identifier, but start with a lowercase letter,
175+
// it’s not a component.
98176
if (name.type === 'Identifier' && /^[a-z]/.test(name.name)) {
99177
name = create(name, {type: 'Literal', value: name.name})
100178
}
101179

102180
attributes = node.openingElement.attributes
103-
objects = []
104-
fields = []
105181
index = -1
106182

107183
// Place props in the right order, because we might have duplicates
@@ -114,46 +190,100 @@ function buildJsx(tree, options) {
114190
}
115191

116192
objects.push(attributes[index].argument)
193+
spread = true
117194
} else {
118-
fields.push(toProperty(attributes[index]))
195+
prop = toProperty(attributes[index])
196+
197+
if (automatic && prop.key.name === 'key') {
198+
if (spread) {
199+
throw new Error(
200+
'Expected `key` to come before any spread expressions'
201+
)
202+
}
203+
204+
key = prop.value
205+
} else {
206+
fields.push(prop)
207+
}
119208
}
120209
}
210+
}
211+
// …and fragments.
212+
else if (automatic) {
213+
imports.fragment = true
214+
name = {type: 'Identifier', name: '_Fragment'}
215+
} else {
216+
name = toMemberExpression(
217+
annotations.jsxFrag || settings.pragmaFrag || 'React.Fragment'
218+
)
219+
}
220+
221+
if (automatic && children.length) {
222+
fields.push({
223+
type: 'Property',
224+
key: {type: 'Identifier', name: 'children'},
225+
value:
226+
children.length > 1
227+
? {type: 'ArrayExpression', elements: children}
228+
: children[0],
229+
kind: 'init'
230+
})
231+
} else {
232+
parameters = children
233+
}
234+
235+
if (fields.length) {
236+
objects.push({type: 'ObjectExpression', properties: fields})
237+
}
121238

122-
if (fields.length) {
123-
objects.push({type: 'ObjectExpression', properties: fields})
239+
if (objects.length > 1) {
240+
// Don’t mutate the first object, shallow clone instead.
241+
if (objects[0].type !== 'ObjectExpression') {
242+
objects.unshift({type: 'ObjectExpression', properties: []})
124243
}
125244

126-
if (objects.length > 1) {
127-
// Don’t mutate the first object, shallow clone instead.
128-
if (objects[0].type !== 'ObjectExpression') {
129-
objects.unshift({type: 'ObjectExpression', properties: []})
130-
}
245+
props = {
246+
type: 'CallExpression',
247+
callee: toMemberExpression('Object.assign'),
248+
arguments: objects
249+
}
250+
} else if (objects.length) {
251+
props = objects[0]
252+
}
131253

132-
props = {
133-
type: 'CallExpression',
134-
callee: toMemberExpression('Object.assign'),
135-
arguments: objects
136-
}
137-
} else if (objects.length) {
138-
props = objects[0]
254+
if (automatic) {
255+
if (children.length > 1) {
256+
imports.jsxs = true
257+
callee = {type: 'Identifier', name: '_jsxs'}
258+
} else {
259+
imports.jsx = true
260+
callee = {type: 'Identifier', name: '_jsx'}
261+
}
262+
263+
parameters.push(props || {type: 'ObjectExpression', properties: []})
264+
265+
if (key) {
266+
parameters.push(key)
139267
}
140268
}
141-
// …and fragments.
269+
// Classic.
142270
else {
143-
name = toMemberExpression(pragmaFrag || 'React.Fragment')
144-
}
271+
// There are props or children.
272+
if (props || parameters.length) {
273+
parameters.unshift(props || {type: 'Literal', value: null})
274+
}
145275

146-
// There are props or children.
147-
if (props || parameters.length) {
148-
parameters.unshift(props || {type: 'Literal', value: null})
276+
callee = toMemberExpression(
277+
annotations.jsx || settings.pragma || 'React.createElement'
278+
)
149279
}
150280

151281
parameters.unshift(name)
152282

153283
this.replace(
154284
create(node, {
155285
type: 'CallExpression',
156-
callee: toMemberExpression(pragma || 'React.createElement'),
286+
callee: callee,
157287
arguments: parameters
158288
})
159289
)

readme.md

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,24 +88,39 @@ Turn JSX in `tree` ([`Program`][program]) into hyperscript calls.
8888

8989
##### `options`
9090

91+
###### `options.runtime`
92+
93+
Choose the [runtime][].
94+
(`string`, `'automatic'` or `'classic'`, default: `'classic'`).
95+
Comment form: `@jsxRuntime theRuntime`.
96+
97+
###### `options.importSource`
98+
99+
Place to import `jsx`, `jsxs`, and/or `Fragment` from, when the effective
100+
runtime is automatic (`string`, default: `'react'`).
101+
Comment: `@jsxImportSource theSource`.
102+
Note that `/jsx-runtime` is appended to this provided source.
103+
91104
###### `options.pragma`
92105

93-
Identifier or member expression to call (`string`, default: a comment with
94-
`@jsx\s+(\S+)` or `'React.createElement'`).
106+
Identifier or member expression to call when the effective runtime is classic
107+
(`string`, default: `'React.createElement'`).
108+
Comment: `@jsx identifier`.
95109

96110
###### `options.pragmaFrag`
97111

98-
Identifier or member expression to use as a symbol for fragments (`string`,
99-
default: a comment with `@jsxFrag\s+(\S+)` or `'React.Fragment'`).
112+
Identifier or member expression to use as a sumbol for fragments when the
113+
effective runtime is classic (`string`, default: `'React.Fragment'`).
114+
Comment: `@jsxFrag identifier`.
100115

101116
###### Returns
102117

103118
`Node` — The given `tree`.
104119

105120
###### Notes
106121

107-
To support `pragma`, `pragmaFrag` from comments, those comments have to be
108-
in the program.
122+
To support configuration from comments, those comments have to be in the
123+
program.
109124
This is done automatically by [`espree`][espree].
110125
For [`acorn`][acorn], it can be done like so:
111126

@@ -123,11 +138,9 @@ they work on slightly different syntax trees.
123138

124139
Some differences:
125140

126-
* Only a classic runtime, so no `runtime` option, `importSource` option, or
127-
`@jsxImportSource` comment
128141
* No pure annotations or dev things
129-
* `this` is not a component: `<this>` -> `h("this")`, not `h(this)`
130-
* Namespaces are supported: `<a:b c:d>` -> `h("a:b", {"c:d": true})`,
142+
* `this` is not a component: `<this>` -> `h('this')`, not `h(this)`
143+
* Namespaces are supported: `<a:b c:d>` -> `h('a:b', {'c:d': true})`,
131144
which throws by default in Babel or can be turned on with `throwIfNamespace`
132145
* No `useSpread`, `useBuiltIns`, or `filter` options
133146

@@ -176,3 +189,5 @@ Some differences:
176189
[program]: https://github.com/estree/estree/blob/master/es5.md#programs
177190

178191
[pr]: https://github.com/mdx-js/mdx/pull/1399
192+
193+
[runtime]: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html

0 commit comments

Comments
 (0)