Skip to content

Commit 04b4265

Browse files
committed
Transform JSX to Lazy Requires instead of Wrappers
This ensures that we can keep overriding what runtime to use by resetting modules while still using the automatic JSX plugin.
1 parent 7048484 commit 04b4265

File tree

5 files changed

+128
-88
lines changed

5 files changed

+128
-88
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
'use strict';
8+
9+
describe('transform-lazy-jsx-import', () => {
10+
it('should use the mocked version of the "react" runtime in jsx', () => {
11+
jest.resetModules();
12+
const mock = jest.fn(type => 'fakejsx: ' + type);
13+
if (__DEV__) {
14+
jest.mock('react/jsx-dev-runtime', () => {
15+
return {
16+
jsxDEV: mock,
17+
};
18+
});
19+
} else {
20+
jest.mock('react/jsx-runtime', () => ({
21+
jsx: mock,
22+
jsxs: mock,
23+
}));
24+
}
25+
// eslint-disable-next-line react/react-in-jsx-scope
26+
const x = <div />;
27+
expect(x).toBe('fakejsx: div');
28+
expect(mock).toHaveBeenCalledTimes(1);
29+
});
30+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
'use strict';
8+
9+
// Most of our tests call jest.resetModules in a beforeEach and the
10+
// re-require all the React modules. However, the JSX runtime is injected by
11+
// the compiler, so those bindings don't get updated. This causes warnings
12+
// logged by the JSX runtime to not have a component stack, because component
13+
// stack relies on the the secret internals object that lives on the React
14+
// module, which because of the resetModules call is longer the same one.
15+
//
16+
// To workaround this issue, use a transform that calls require() again before
17+
// every JSX invocation.
18+
//
19+
// Longer term we should migrate all our tests away from using require() and
20+
// resetModules, and use import syntax instead so this kind of thing doesn't
21+
// happen.
22+
23+
module.exports = function replaceJSXImportWithLazy(babel) {
24+
const {types: t} = babel;
25+
26+
function getInlineRequire(moduleName) {
27+
return t.callExpression(t.identifier('require'), [
28+
t.stringLiteral(moduleName),
29+
]);
30+
}
31+
32+
return {
33+
visitor: {
34+
CallExpression: function (path, pass) {
35+
let callee = path.node.callee;
36+
if (callee.type === 'SequenceExpression') {
37+
callee = callee.expressions[callee.expressions.length - 1];
38+
}
39+
if (callee.type === 'Identifier') {
40+
// Sometimes we seem to hit this before the imports are transformed
41+
// into requires and so we hit this case.
42+
switch (callee.name) {
43+
case '_jsxDEV':
44+
path.node.callee = t.memberExpression(
45+
getInlineRequire('react/jsx-dev-runtime'),
46+
t.identifier('jsxDEV')
47+
);
48+
return;
49+
case '_jsx':
50+
path.node.callee = t.memberExpression(
51+
getInlineRequire('react/jsx-runtime'),
52+
t.identifier('jsx')
53+
);
54+
return;
55+
case '_jsxs':
56+
path.node.callee = t.memberExpression(
57+
getInlineRequire('react/jsx-runtime'),
58+
t.identifier('jsxs')
59+
);
60+
return;
61+
}
62+
return;
63+
}
64+
if (callee.type !== 'MemberExpression') {
65+
return;
66+
}
67+
if (callee.property.type !== 'Identifier') {
68+
// Needs to be jsx, jsxs, jsxDEV.
69+
return;
70+
}
71+
if (callee.object.type !== 'Identifier') {
72+
// Needs to be _reactJsxDevRuntime or _reactJsxRuntime.
73+
return;
74+
}
75+
// Replace the cached identifier with a new require call.
76+
// Relying on the identifier name is a little flaky. Should ideally pick
77+
// this from the import. For some reason it sometimes has the react prefix
78+
// and other times it doesn't.
79+
switch (callee.object.name) {
80+
case '_reactJsxDevRuntime':
81+
case '_jsxDevRuntime':
82+
callee.object = getInlineRequire('react/jsx-dev-runtime');
83+
return;
84+
case '_reactJsxRuntime':
85+
case '_jsxRuntime':
86+
callee.object = getInlineRequire('react/jsx-runtime');
87+
return;
88+
}
89+
},
90+
},
91+
};
92+
};

scripts/jest/devtools/setupEnv.js

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -40,48 +40,3 @@ global._test_react_version_focus = (range, testName, callback) => {
4040
global._test_ignore_for_react_version = (testName, callback) => {
4141
test.skip(testName, callback);
4242
};
43-
44-
// Most of our tests call jest.resetModules in a beforeEach and the
45-
// re-require all the React modules. However, the JSX runtime is injected by
46-
// the compiler, so those bindings don't get updated. This causes warnings
47-
// logged by the JSX runtime to not have a component stack, because component
48-
// stack relies on the the secret internals object that lives on the React
49-
// module, which because of the resetModules call is longer the same one.
50-
//
51-
// To workaround this issue, we use a proxy that re-requires the latest
52-
// JSX Runtime from the require cache on every function invocation.
53-
//
54-
// Longer term we should migrate all our tests away from using require() and
55-
// resetModules, and use import syntax instead so this kind of thing doesn't
56-
// happen.
57-
if (semver.gte(ReactVersionTestingAgainst, '17.0.0')) {
58-
lazyRequireFunctionExports('react/jsx-dev-runtime');
59-
60-
// TODO: We shouldn't need to do this in the production runtime, but until
61-
// we remove string refs they also depend on the shared state object. Remove
62-
// once we remove string refs.
63-
lazyRequireFunctionExports('react/jsx-runtime');
64-
}
65-
66-
function lazyRequireFunctionExports(moduleName) {
67-
jest.mock(moduleName, () => {
68-
return new Proxy(jest.requireActual(moduleName), {
69-
get(originalModule, prop) {
70-
// If this export is a function, return a wrapper function that lazily
71-
// requires the implementation from the current module cache.
72-
if (typeof originalModule[prop] === 'function') {
73-
// eslint-disable-next-line no-eval
74-
const wrapper = eval(`
75-
(function () {
76-
return jest.requireActual(moduleName)[prop].apply(this, arguments);
77-
})
78-
// We use this to trick the filtering of Flight to exclude this frame.
79-
//# sourceURL=<anonymous>`);
80-
return wrapper;
81-
} else {
82-
return originalModule[prop];
83-
}
84-
},
85-
});
86-
});
87-
}

scripts/jest/preprocessor.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ const pathToTransformTestGatePragma = require.resolve(
2525
const pathToTransformReactVersionPragma = require.resolve(
2626
'../babel/transform-react-version-pragma'
2727
);
28+
const pathToTransformLazyJSXImport = require.resolve(
29+
'../babel/transform-lazy-jsx-import'
30+
);
2831
const pathToBabelrc = path.join(__dirname, '..', '..', 'babel.config.js');
2932
const pathToErrorCodes = require.resolve('../error-codes/codes.json');
3033

@@ -93,6 +96,8 @@ module.exports = {
9396
);
9497
}
9598

99+
plugins.push(pathToTransformLazyJSXImport);
100+
96101
let sourceAst = hermesParser.parse(src, {babel: true});
97102
return {
98103
code: babel.transformFromAstSync(
@@ -122,6 +127,7 @@ module.exports = {
122127
pathToTransformInfiniteLoops,
123128
pathToTransformTestGatePragma,
124129
pathToTransformReactVersionPragma,
130+
pathToTransformLazyJSXImport,
125131
pathToErrorCodes,
126132
],
127133
[

scripts/jest/setupTests.js

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -264,46 +264,3 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
264264
return fn(flags);
265265
};
266266
}
267-
268-
// Most of our tests call jest.resetModules in a beforeEach and the
269-
// re-require all the React modules. However, the JSX runtime is injected by
270-
// the compiler, so those bindings don't get updated. This causes warnings
271-
// logged by the JSX runtime to not have a component stack, because component
272-
// stack relies on the the secret internals object that lives on the React
273-
// module, which because of the resetModules call is longer the same one.
274-
//
275-
// To workaround this issue, we use a proxy that re-requires the latest
276-
// JSX Runtime from the require cache on every function invocation.
277-
//
278-
// Longer term we should migrate all our tests away from using require() and
279-
// resetModules, and use import syntax instead so this kind of thing doesn't
280-
// happen.
281-
lazyRequireFunctionExports('react/jsx-dev-runtime');
282-
283-
// TODO: We shouldn't need to do this in the production runtime, but until
284-
// we remove string refs they also depend on the shared state object. Remove
285-
// once we remove string refs.
286-
lazyRequireFunctionExports('react/jsx-runtime');
287-
288-
function lazyRequireFunctionExports(moduleName) {
289-
jest.mock(moduleName, () => {
290-
return new Proxy(jest.requireActual(moduleName), {
291-
get(originalModule, prop) {
292-
// If this export is a function, return a wrapper function that lazily
293-
// requires the implementation from the current module cache.
294-
if (typeof originalModule[prop] === 'function') {
295-
// eslint-disable-next-line no-eval
296-
const wrapper = eval(`
297-
(function () {
298-
return jest.requireActual(moduleName)[prop].apply(this, arguments);
299-
})
300-
// We use this to trick the filtering of Flight to exclude this frame.
301-
//# sourceURL=<anonymous>`);
302-
return wrapper;
303-
} else {
304-
return originalModule[prop];
305-
}
306-
},
307-
});
308-
});
309-
}

0 commit comments

Comments
 (0)