Skip to content

Commit cfd6080

Browse files
developitTimer
authored andcommitted
Feature: Add transparent JSX optimization (#8350)
* Add transparent JSX optimization. * fix duplicate React import * fix React not being imported when only a single Fragment node is present in a source module * remove babel-plugin-react-require * Fix JSX optimization for CommonJS source files.
1 parent 771c0b4 commit cfd6080

File tree

5 files changed

+205
-7
lines changed

5 files changed

+205
-7
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { PluginObj } from '@babel/core'
2+
import { NodePath } from '@babel/traverse'
3+
import * as BabelTypes from '@babel/types'
4+
5+
export default function({
6+
types: t,
7+
}: {
8+
types: typeof BabelTypes
9+
}): PluginObj<any> {
10+
return {
11+
inherits: require('babel-plugin-syntax-jsx'),
12+
visitor: {
13+
JSXElement(path, state) {
14+
state.set('jsx', true)
15+
},
16+
17+
// Fragment syntax is still JSX since it compiles to createElement(),
18+
// but JSXFragment is not a JSXElement
19+
JSXFragment(path, state) {
20+
state.set('jsx', true)
21+
},
22+
23+
Program: {
24+
exit(path: NodePath<BabelTypes.Program>, state) {
25+
if (state.get('jsx')) {
26+
const pragma = t.identifier(state.opts.pragma)
27+
let importAs = pragma
28+
29+
// if there's already a React in scope, use that instead of adding an import
30+
const existingBinding =
31+
state.opts.reuseImport !== false &&
32+
state.opts.importAs &&
33+
path.scope.getBinding(state.opts.importAs)
34+
35+
// var _jsx = _pragma.createElement;
36+
if (state.opts.property) {
37+
if (state.opts.importAs) {
38+
importAs = t.identifier(state.opts.importAs)
39+
} else {
40+
importAs = path.scope.generateUidIdentifier('pragma')
41+
}
42+
43+
const mapping = t.variableDeclaration('var', [
44+
t.variableDeclarator(
45+
pragma,
46+
t.memberExpression(
47+
importAs,
48+
t.identifier(state.opts.property)
49+
)
50+
),
51+
])
52+
53+
// if the React binding came from a require('react'),
54+
// make sure that our usage comes after it.
55+
if (
56+
existingBinding &&
57+
t.isVariableDeclarator(existingBinding.path.node) &&
58+
t.isCallExpression(existingBinding.path.node.init) &&
59+
t.isIdentifier(existingBinding.path.node.init.callee) &&
60+
existingBinding.path.node.init.callee.name === 'require'
61+
) {
62+
existingBinding.path.parentPath.insertAfter(mapping)
63+
} else {
64+
// @ts-ignore
65+
path.unshiftContainer('body', mapping)
66+
}
67+
}
68+
69+
if (!existingBinding) {
70+
const importSpecifier = t.importDeclaration(
71+
[
72+
state.opts.import
73+
? // import { $import as _pragma } from '$module'
74+
t.importSpecifier(
75+
importAs,
76+
t.identifier(state.opts.import)
77+
)
78+
: state.opts.importNamespace
79+
? t.importNamespaceSpecifier(importAs)
80+
: // import _pragma from '$module'
81+
t.importDefaultSpecifier(importAs),
82+
],
83+
t.stringLiteral(state.opts.module || 'react')
84+
)
85+
86+
// @ts-ignore
87+
path.unshiftContainer('body', importSpecifier)
88+
}
89+
}
90+
},
91+
},
92+
},
93+
}
94+
}

packages/next/build/babel/preset.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,25 @@ module.exports = (
9797
// This adds @babel/plugin-transform-react-jsx-source and
9898
// @babel/plugin-transform-react-jsx-self automatically in development
9999
development: isDevelopment || isTest,
100+
pragma: '__jsx',
100101
...options['preset-react'],
101102
},
102103
],
103104
require('@babel/preset-typescript'),
104105
],
105106
plugins: [
106-
require('babel-plugin-react-require'),
107+
[
108+
require('./plugins/jsx-pragma'),
109+
{
110+
// This produces the following injected import for modules containing JSX:
111+
// import React from 'react';
112+
// var __jsx = React.createElement;
113+
module: 'react',
114+
importAs: 'React',
115+
pragma: '__jsx',
116+
property: 'createElement',
117+
},
118+
],
107119
require('@babel/plugin-syntax-dynamic-import'),
108120
require('./plugins/react-loadable-plugin'),
109121
[

packages/next/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@
7272
"autodll-webpack-plugin": "0.4.2",
7373
"babel-core": "7.0.0-bridge.0",
7474
"babel-loader": "8.0.6",
75-
"babel-plugin-react-require": "3.0.0",
7675
"babel-plugin-transform-define": "1.3.1",
7776
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
7877
"chalk": "2.4.2",

test/unit/next-babel.test.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/* eslint-env jest */
2+
import { transform } from '@babel/core'
3+
4+
const trim = s =>
5+
s
6+
.join('\n')
7+
.trim()
8+
.replace(/^\s+/gm, '')
9+
10+
// avoid generating __source annotations in JSX during testing:
11+
const NODE_ENV = process.env.NODE_ENV
12+
process.env.NODE_ENV = 'production'
13+
const preset = require('next/dist/build/babel/preset')
14+
process.env.NODE_ENV = NODE_ENV
15+
16+
const babel = (code, esm = false) =>
17+
transform(code, {
18+
filename: 'noop.js',
19+
presets: [preset],
20+
babelrc: false,
21+
configFile: false,
22+
sourceType: 'module',
23+
compact: true,
24+
caller: {
25+
name: 'tests',
26+
supportsStaticESM: esm
27+
}
28+
}).code
29+
30+
describe('next/babel', () => {
31+
it('should transform JSX to use a local identifier in modern mode', () => {
32+
const output = babel(`const a = () => <a href="/">home</a>;`, true)
33+
34+
// it should add a React import:
35+
expect(output).toMatch(`import React from"react"`)
36+
// it should hoist JSX factory to a module level variable:
37+
expect(output).toMatch(`var __jsx=React.createElement`)
38+
// it should use that factory for all JSX:
39+
expect(output).toMatch(`__jsx("a",{href:"/"`)
40+
41+
expect(
42+
babel(`const a = ()=><a href="/">home</a>`, true)
43+
).toMatchInlineSnapshot(
44+
`"import React from\\"react\\";var __jsx=React.createElement;var a=function a(){return __jsx(\\"a\\",{href:\\"/\\"},\\"home\\");};"`
45+
)
46+
})
47+
48+
it('should transform JSX to use a local identifier in CommonJS mode', () => {
49+
const output = babel(trim`
50+
const a = () => <React.Fragment><a href="/">home</a></React.Fragment>;
51+
`)
52+
53+
// Grab generated names from the compiled output.
54+
// It looks something like this:
55+
// var _react = _interopRequireDefault(require("react"));
56+
// var __jsx = _react["default"].createElement;
57+
// react: _react
58+
// reactNamespace: _react["default"]
59+
const [, react, reactNamespace] = output.match(
60+
/(([a-z0-9_]+)(\[[^\]]*?\]|\.[a-z0-9_]+)*?)\.Fragment/i
61+
)
62+
63+
expect(output).toMatch(`var ${reactNamespace}=`)
64+
expect(output).toMatch(`require("react")`)
65+
expect(output).toMatch(`var __jsx=${react}.createElement`)
66+
// Fragment should use the same React namespace import:
67+
expect(output).toMatch(`__jsx(${react}.Fragment`)
68+
expect(output).toMatch(`__jsx("a",{href:"/"`)
69+
70+
expect(babel(`const a = ()=><a href="/">home</a>`)).toMatchInlineSnapshot(
71+
`"\\"use strict\\";var _interopRequireDefault=require(\\"@babel/runtime-corejs2/helpers/interopRequireDefault\\");var _react=_interopRequireDefault(require(\\"react\\"));var __jsx=_react[\\"default\\"].createElement;var a=function a(){return __jsx(\\"a\\",{href:\\"/\\"},\\"home\\");};"`
72+
)
73+
})
74+
75+
it('should support Fragment syntax', () => {
76+
const output = babel(`const a = () => <>hello</>;`, true)
77+
78+
expect(output).toMatch(`React.Fragment`)
79+
80+
expect(babel(`const a = () => <>hello</>;`, true)).toMatchInlineSnapshot(
81+
`"import React from\\"react\\";var __jsx=React.createElement;var a=function a(){return __jsx(React.Fragment,null,\\"hello\\");};"`
82+
)
83+
})
84+
85+
it('should support commonjs', () => {
86+
const output = babel(
87+
trim`
88+
const React = require('react');
89+
module.exports = () => <div>test2</div>;
90+
`,
91+
true
92+
)
93+
94+
expect(output).toMatchInlineSnapshot(
95+
`"var React=require('react');var __jsx=React.createElement;module.exports=function(){return __jsx(\\"div\\",null,\\"test2\\");};"`
96+
)
97+
})
98+
})

yarn.lock

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3204,11 +3204,6 @@ babel-plugin-jest-hoist@^24.6.0:
32043204
dependencies:
32053205
"@types/babel__traverse" "^7.0.6"
32063206

3207-
babel-plugin-react-require@3.0.0:
3208-
version "3.0.0"
3209-
resolved "https://registry.yarnpkg.com/babel-plugin-react-require/-/babel-plugin-react-require-3.0.0.tgz#2e4e7b4496b93a654a1c80042276de4e4eeb20e3"
3210-
integrity sha1-Lk57RJa5OmVKHIAEInbeTk7rIOM=
3211-
32123207
babel-plugin-syntax-jsx@6.18.0:
32133208
version "6.18.0"
32143209
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"

0 commit comments

Comments
 (0)