Skip to content

Commit f9f3b0f

Browse files
committed
Add hermes-transform infra
This adds `hermes-transform` as a dependency for scripts to allow us to run some codemods updating tests. If this turns out useful, there's obviously more that can be done to the script, but as these internal codemods are pretty one-off, I'm also okay to keep it fairly rough. The included codemod is a starting point to convert tests to `crateRoot`. Due to many different patterns in tests, this does just the following: - convert `it` and `expect` calls that contain `ReactDOM.render` to the async variant, so `await` can be used inside. - convert `ReactDOM.render(el, container)` to `root.render(el)`. Note that this does not create `root` and that needs to be updated manually.
1 parent b300304 commit f9f3b0f

4 files changed

Lines changed: 221 additions & 1 deletion

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"gzip-size": "^5.1.1",
7171
"hermes-eslint": "^0.18.2",
7272
"hermes-parser": "^0.18.2",
73+
"hermes-transform": "^0.18.2",
7374
"jest": "^29.4.2",
7475
"jest-cli": "^29.4.2",
7576
"jest-diff": "^29.4.2",
@@ -130,6 +131,7 @@
130131
"download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)",
131132
"download-build-in-codesandbox-ci": "cd scripts/release && yarn install && cd ../../ && yarn download-build-for-head || yarn build --type=node react/index react-dom/index react-dom/src/server react-dom/test-utils scheduler/index react/jsx-runtime react/jsx-dev-runtime",
132133
"check-release-dependencies": "node ./scripts/release/check-release-dependencies",
134+
"codemod": "flow-node scripts/codemod/index.js",
133135
"generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js"
134136
},
135137
"resolutions": {

scripts/codemod/index.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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+
* @flow
8+
*/
9+
10+
import type {ESNode, CallExpression} from 'hermes-estree';
11+
import type {TransformContext} from 'hermes-transform';
12+
13+
const {transform, t} = require('hermes-transform');
14+
const {SimpleTraverser} = require('hermes-parser');
15+
const Glob = require('glob');
16+
const {readFileSync, writeFileSync} = require('fs');
17+
const Prettier = require('prettier');
18+
19+
/* eslint-disable no-for-of-loops/no-for-of-loops */
20+
21+
function containsReactDOMRenderCall(func: ESNode): boolean {
22+
if (
23+
func.type !== 'ArrowFunctionExpression' &&
24+
func.type !== 'FunctionExpression'
25+
) {
26+
throw new Error('expected a function');
27+
}
28+
let result = false;
29+
SimpleTraverser.traverse(func.body, {
30+
enter(node: ESNode) {
31+
if (
32+
node.type === 'CallExpression' &&
33+
node.callee.type === 'MemberExpression' &&
34+
node.callee.object.type === 'Identifier' &&
35+
node.callee.object.name === 'ReactDOM' &&
36+
node.callee.property.type === 'Identifier' &&
37+
node.callee.property.name === 'render'
38+
) {
39+
result = true;
40+
throw SimpleTraverser.Break;
41+
}
42+
},
43+
leave() {},
44+
});
45+
return result;
46+
}
47+
48+
function updateItToAsync(context: TransformContext) {
49+
return {
50+
CallExpression(node: CallExpression) {
51+
if (
52+
node.callee.type === 'Identifier' &&
53+
node.callee.name === 'it' &&
54+
node.arguments.length === 2
55+
) {
56+
const fn = node.arguments[1];
57+
if (
58+
fn.type !== 'ArrowFunctionExpression' &&
59+
fn.type !== 'FunctionExpression'
60+
) {
61+
throw new Error('expected a function as argument to it()');
62+
}
63+
if (containsReactDOMRenderCall(fn)) {
64+
context.replaceNode(
65+
fn,
66+
t.ArrowFunctionExpression({
67+
params: [],
68+
body: fn.body,
69+
async: true,
70+
}),
71+
);
72+
}
73+
}
74+
},
75+
};
76+
}
77+
78+
function updateExpectToAsync(context: TransformContext) {
79+
return {
80+
CallExpression(node: CallExpression) {
81+
if (
82+
node.callee.type === 'MemberExpression' &&
83+
node.callee.object.type === 'CallExpression' &&
84+
node.callee.object.callee.type === 'Identifier' &&
85+
node.callee.object.callee.name === 'expect' &&
86+
node.callee.object.arguments.length === 1 &&
87+
(node.callee.object.arguments[0].type === 'ArrowFunctionExpression' ||
88+
node.callee.object.arguments[0].type === 'FunctionExpression') &&
89+
containsReactDOMRenderCall(node.callee.object.arguments[0])
90+
) {
91+
const cloned = context.deepCloneNode(node);
92+
// $FlowFixMe
93+
cloned.callee.object.arguments[0] = t.ArrowFunctionExpression({
94+
params: [],
95+
body: t.BlockStatement({
96+
// $FlowFixMe
97+
body: [cloned.callee.object.arguments[0].body],
98+
}),
99+
async: true,
100+
});
101+
context.replaceNode(
102+
node,
103+
t.AwaitExpression({
104+
argument: cloned,
105+
}),
106+
);
107+
}
108+
},
109+
};
110+
}
111+
112+
function replaceReactDOMRender(context: TransformContext) {
113+
return {
114+
CallExpression(node: CallExpression) {
115+
if (
116+
node.callee.type === 'MemberExpression' &&
117+
node.callee.object.type === 'Identifier' &&
118+
node.callee.object.name === 'ReactDOM' &&
119+
node.callee.property.type === 'Identifier' &&
120+
node.callee.property.name === 'render'
121+
) {
122+
const renderRoot = t.CallExpression({
123+
callee: t.MemberExpression({
124+
object: t.Identifier({name: 'root'}),
125+
property: t.Identifier({name: 'render'}),
126+
computed: false,
127+
}),
128+
arguments: [node.arguments[0]],
129+
});
130+
context.replaceNode(
131+
node,
132+
t.AwaitExpression({
133+
argument: t.CallExpression({
134+
callee: t.Identifier({name: 'act'}),
135+
arguments: [
136+
t.ArrowFunctionExpression({
137+
async: false,
138+
params: [],
139+
body: t.BlockStatement({
140+
body: [
141+
t.ExpressionStatement({
142+
expression: renderRoot,
143+
}),
144+
],
145+
}),
146+
}),
147+
],
148+
}),
149+
}),
150+
);
151+
}
152+
},
153+
};
154+
}
155+
156+
const visitors = [
157+
updateItToAsync,
158+
updateExpectToAsync,
159+
replaceReactDOMRender,
160+
//
161+
];
162+
163+
async function transformFile(filename: string) {
164+
const originalCode = readFileSync(filename, 'utf8');
165+
const prettierConfig = await Prettier.resolveConfig(filename);
166+
let transformedCode = originalCode;
167+
for (const createVisitors of visitors) {
168+
transformedCode = await transform(
169+
transformedCode,
170+
createVisitors,
171+
prettierConfig,
172+
);
173+
}
174+
if (originalCode !== transformedCode) {
175+
writeFileSync(filename, transformedCode, 'utf8');
176+
return true;
177+
}
178+
return false;
179+
}
180+
181+
async function main(args: $ReadOnlyArray<string>) {
182+
if (args.length !== 1) {
183+
console.error('Usage: yarn codemod <PATTERN>');
184+
process.exit(1);
185+
}
186+
const files = Glob.sync(args[0]);
187+
let updatedCount = 0;
188+
for (const file of files) {
189+
const updated = await transformFile(file);
190+
if (updated) {
191+
updatedCount++;
192+
console.log(`updated ${file}`);
193+
}
194+
}
195+
console.log(`${files.length} processed, ${updatedCount} updated`);
196+
}
197+
198+
main(process.argv.slice(2)).catch(err => {
199+
console.error('Error while transforming:', err);
200+
});

scripts/shared/pathsByLanguageVersion.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const esNextPaths = [
2020
'packages/react-interactions/**/*.js',
2121
'packages/shared/**/*.js',
2222
// Shims and Flow environment
23+
'scripts/codemod/*.js',
2324
'scripts/flow/*.js',
2425
'scripts/rollup/shims/**/*.js',
2526
];

yarn.lock

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7860,6 +7860,11 @@ flow-bin@^0.226.0:
78607860
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.226.0.tgz#b245847b749ab20756ef74c91d96619f68d18430"
78617861
integrity sha512-q8hXSRhZ+I14jS0KGDDsPYCvPufvBexk6nJXSOsSP6DgCuXbvCOByWhsXRAjPtmXKmO8v9RKSJm1kRaWaf0fZw==
78627862

7863+
flow-enums-runtime@^0.0.6:
7864+
version "0.0.6"
7865+
resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787"
7866+
integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==
7867+
78637868
flow-remove-types@^2.226.0:
78647869
version "2.226.0"
78657870
resolved "https://registry.yarnpkg.com/flow-remove-types/-/flow-remove-types-2.226.0.tgz#08ff7e195137ce43042e11bfa04303184971dac2"
@@ -8593,7 +8598,7 @@ has@^1.0.1, has@^1.0.3:
85938598
dependencies:
85948599
function-bind "^1.1.1"
85958600

8596-
hermes-eslint@^0.18.2:
8601+
hermes-eslint@0.18.2, hermes-eslint@^0.18.2:
85978602
version "0.18.2"
85988603
resolved "https://registry.yarnpkg.com/hermes-eslint/-/hermes-eslint-0.18.2.tgz#af09ea1700eb32502caf135b181ffed6091ccb72"
85998604
integrity sha512-FWKVoHyHaXRjOfjoTgoc4OTkC+KThYdhLFyggoXIYLMDHF9hkg5yHSih3cyK3hT73te6+aaGHePzwaOai69uoA==
@@ -8614,6 +8619,18 @@ hermes-parser@0.18.2, hermes-parser@^0.18.2:
86148619
dependencies:
86158620
hermes-estree "0.18.2"
86168621

8622+
hermes-transform@^0.18.2:
8623+
version "0.18.2"
8624+
resolved "https://registry.yarnpkg.com/hermes-transform/-/hermes-transform-0.18.2.tgz#ab0dbf586b76980dadc2b396ee7936b206cc1904"
8625+
integrity sha512-cM5J6cnC/9d7mIjUeUzv2/3nmGjbevA3FemPjevgWG5udEIhlJB5z5zwcrkVdp16W/Q6bVJdaT1pc4LPmlzsCA==
8626+
dependencies:
8627+
"@babel/code-frame" "^7.16.0"
8628+
esquery "^1.4.0"
8629+
flow-enums-runtime "^0.0.6"
8630+
hermes-eslint "0.18.2"
8631+
hermes-estree "0.18.2"
8632+
hermes-parser "0.18.2"
8633+
86178634
homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1:
86188635
version "1.0.3"
86198636
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"

0 commit comments

Comments
 (0)