Skip to content

Commit 86c049e

Browse files
committed
more testing & bug fixes
1 parent 81acd7c commit 86c049e

18 files changed

Lines changed: 373 additions & 36 deletions

File tree

TESTING_PLAN.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ We searched public GitHub repos for the exact code patterns each of our 6 React
1212

1313
**Phase 2 import note:** On 2026-04-20, 14 additional JSSG codemods were brought forward from `align-with-legacy-codemods`. These codemods target older or niche migration surfaces, so their immediate validation strategy is fixture-first rather than open-source repo-first. Only `class` remains legacy-only.
1414

15+
**Additional rollout research (2026-04-21):** A second repo sweep found three especially useful follow-on candidates beyond the original four test repos:
16+
- `salesforce/design-system-react` at `825de01` (React 17) remains a high-value second source for `replace-reactdom-render`, `replace-act-import`, and `use-context-hook`.
17+
- `MetaMask/metamask-extension` at `9c3b57c` (React 17) is a strong `replace-act-import` validation target with 18 real test-file imports.
18+
- `react-native-snap-carousel` at `9c39995` gives `react-native-view-prop-types` an exact real-world source surface across 4 files, including the tricky case where `ViewPropTypes` is already imported.
19+
- `DataTurks` at `039d57e` gives `error-boundaries` an exact `unstable_handleError` source hit in production code.
20+
- `airbnb/react-dates` at `b7bad38` is a useful class-heavy repo for generic class codemods such as `pure-component` and `sort-comp`, though it does not expose many exact legacy API hits.
21+
- `rsuite` at `0b1482d` still has a large raw `ReactDOM.render` count, but current HEAD is heavily docs/example-driven, so it is a lower-quality source than `salesforce/design-system-react` for rollout decisions.
22+
1523
---
1624

1725
## Recommended Test Repos
@@ -208,6 +216,17 @@ Real-repo candidate matches already found in the current testing repos:
208216
| `remove-context-provider` | `calcom/cal.diy` | Many `Context.Provider` wrappers in source packages |
209217
| `update-react-imports` | `youzan/zent`, `calcom/cal.diy` | Broad modern React import surface, especially in TS/TSX |
210218

219+
Additional real-repo candidate matches confirmed on 2026-04-21:
220+
221+
| Codemod | Repo Candidate | Notes |
222+
|---------|----------------|-------|
223+
| `error-boundaries` | `DataTurks` | Exact `unstable_handleError` hit in `bazaar/src/components/ErrorBoundary/ErrorBoundary.js` |
224+
| `react-native-view-prop-types` | `react-native-snap-carousel` | Exact `View.propTypes` source hits in 4 files; includes existing `ViewPropTypes` import edge case |
225+
| `pure-component` | `airbnb/react-dates` | 21 class-component files on current HEAD, mostly wrappers/examples and a few library classes |
226+
| `sort-comp` | `airbnb/react-dates`, `salesforce/design-system-react` | Good class-heavy surfaces for behavior comparison, even though the pattern itself is structural rather than API-specific |
227+
| `replace-reactdom-render` | `salesforce/design-system-react` | 316 real JS/JSX files under `components/`, much stronger than docs-heavy `rsuite` HEAD for rollout decisions |
228+
| `replace-act-import` | `MetaMask/metamask-extension` | 18 exact imports under `ui/` tests |
229+
211230
---
212231

213232
## Test Execution Plan

TESTING_REPORT.md

Lines changed: 133 additions & 23 deletions
Large diffs are not rendered by default.

codemods/jssg/react-native-view-prop-types/scripts/codemod.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,37 @@ function viewRequireProperty(statement: SgNode<TSX>): SgNode<TSX> | null {
166166
.find((property) => property.text() === "View") ?? null;
167167
}
168168

169+
function hasImportSpecifier(importStmt: SgNode<TSX>, name: string): boolean {
170+
return importStmt.findAll({ rule: { kind: "import_specifier" } })
171+
.some((specifier) => specifier.field("name")?.text() === name);
172+
}
173+
174+
function hasRequireProperty(statement: SgNode<TSX>, name: string): boolean {
175+
return statement.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } })
176+
.some((property) => property.text() === name);
177+
}
178+
179+
function hasHasteViewPropTypesImport(rootNode: SgNode<TSX>): boolean {
180+
return rootNode.findAll({ rule: { kind: "import_statement" } })
181+
.some((importStmt) => {
182+
const source = importStmt.field("source");
183+
return source?.kind() === "string" &&
184+
stringValue(source) === "ViewPropTypes";
185+
});
186+
}
187+
188+
function hasHasteViewPropTypesRequire(rootNode: SgNode<TSX>): boolean {
189+
return rootNode.findAll({
190+
rule: {
191+
any: [
192+
{ pattern: "const ViewPropTypes = require('ViewPropTypes')" },
193+
{ pattern: "let ViewPropTypes = require('ViewPropTypes')" },
194+
{ pattern: "var ViewPropTypes = require('ViewPropTypes')" },
195+
],
196+
},
197+
}).length > 0;
198+
}
199+
169200
function statementInsertionIndex(source: string, node: SgNode<TSX>): number {
170201
let index = node.range().end.index;
171202
while (index < source.length && (source[index] === ";" || source[index] === "\r" || source[index] === "\n")) {
@@ -184,6 +215,14 @@ const transform: Transform<TSX> = async (root) => {
184215
const hasteRequireStatement = findHasteRequireStatement(rootNode);
185216
const reactNativeImportStatement = findReactNativeImportStatement(rootNode);
186217
const reactNativeRequireStatement = findReactNativeRequireStatement(rootNode);
218+
const hasExistingHasteViewPropTypesImport = hasHasteViewPropTypesImport(rootNode);
219+
const hasExistingHasteViewPropTypesRequire = hasHasteViewPropTypesRequire(rootNode);
220+
const hasExistingReactNativeImportViewPropTypes = reactNativeImportStatement
221+
? hasImportSpecifier(reactNativeImportStatement, "ViewPropTypes")
222+
: false;
223+
const hasExistingReactNativeRequireViewPropTypes = reactNativeRequireStatement
224+
? hasRequireProperty(reactNativeRequireStatement, "ViewPropTypes")
225+
: false;
187226

188227
let replacements = 0;
189228
for (const member of rootNode.findAll({ rule: { kind: "member_expression" } })) {
@@ -209,7 +248,9 @@ const transform: Transform<TSX> = async (root) => {
209248
}).some(isViewBindingNode);
210249

211250
if (hasteImportStatement) {
212-
if (keepViewBinding) {
251+
if (hasExistingHasteViewPropTypesImport) {
252+
// Keep the existing import intact; the file already binds ViewPropTypes.
253+
} else if (keepViewBinding) {
213254
edits.push({
214255
startPos: hasteImportStatement.range().end.index,
215256
endPos: hasteImportStatement.range().end.index,
@@ -219,7 +260,9 @@ const transform: Transform<TSX> = async (root) => {
219260
edits.push(hasteImportStatement.replace("import ViewPropTypes from 'ViewPropTypes';"));
220261
}
221262
} else if (hasteRequireStatement) {
222-
if (keepViewBinding) {
263+
if (hasExistingHasteViewPropTypesRequire) {
264+
// Keep the existing require intact; the file already binds ViewPropTypes.
265+
} else if (keepViewBinding) {
223266
edits.push({
224267
startPos: statementInsertionIndex(source, hasteRequireStatement),
225268
endPos: statementInsertionIndex(source, hasteRequireStatement),
@@ -231,7 +274,9 @@ const transform: Transform<TSX> = async (root) => {
231274
} else if (reactNativeImportStatement) {
232275
const specifier = viewImportSpecifier(reactNativeImportStatement);
233276
if (specifier) {
234-
if (keepViewBinding) {
277+
if (hasExistingReactNativeImportViewPropTypes) {
278+
// Keep the existing import intact; the file already binds ViewPropTypes.
279+
} else if (keepViewBinding) {
235280
edits.push({
236281
startPos: specifier.range().end.index,
237282
endPos: specifier.range().end.index,
@@ -244,7 +289,9 @@ const transform: Transform<TSX> = async (root) => {
244289
} else if (reactNativeRequireStatement) {
245290
const property = viewRequireProperty(reactNativeRequireStatement);
246291
if (property) {
247-
if (keepViewBinding) {
292+
if (hasExistingReactNativeRequireViewPropTypes) {
293+
// Keep the existing require intact; the file already binds ViewPropTypes.
294+
} else if (keepViewBinding) {
248295
edits.push({
249296
startPos: property.range().end.index,
250297
endPos: property.range().end.index,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
import { View, ViewPropTypes } from 'react-native';
3+
4+
function Component() {
5+
return <View />;
6+
}
7+
8+
Component.propTypes = {
9+
style: ViewPropTypes ? ViewPropTypes.style : ViewPropTypes.style,
10+
};
11+
12+
export default Component;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
import { View, ViewPropTypes } from 'react-native';
3+
4+
function Component() {
5+
return <View />;
6+
}
7+
8+
Component.propTypes = {
9+
style: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style,
10+
};
11+
12+
export default Component;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"react-native-view-prop-types-replacements": [
3+
{
4+
"cardinality": {
5+
"file": "tests/destructured-import-existing-view-prop-types/input.tsx"
6+
},
7+
"count": 1
8+
}
9+
]
10+
}

codemods/jssg/react-proptypes-to-prop-types/scripts/codemod.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,11 @@ function usesVarForRequires(rootNode: SgNode<TSX, "program">): boolean {
104104
return rootNode.find({ rule: { kind: "lexical_declaration" } }) === null;
105105
}
106106

107-
function firstSortedImportPosition(rootNode: SgNode<TSX, "program">, moduleName: string): number {
107+
function firstSortedImportPosition(
108+
rootNode: SgNode<TSX, "program">,
109+
moduleName: string,
110+
source: string,
111+
): number {
108112
const imports = rootNode.findAll({ rule: { kind: "import_statement" } });
109113
const lowerModule = moduleName.toLowerCase();
110114
let target: SgNode<TSX> | null = null;
@@ -122,7 +126,8 @@ function firstSortedImportPosition(rootNode: SgNode<TSX, "program">, moduleName:
122126

123127
if (target) return target.range().start.index;
124128
const last = imports[imports.length - 1];
125-
return last ? last.range().end.index : 0;
129+
if (!last) return 0;
130+
return statementRange(last, source).end;
126131
}
127132

128133
function firstSortedRequirePosition(rootNode: SgNode<TSX, "program">, moduleName: string): number {
@@ -313,7 +318,7 @@ const transform: Transform<TSX> = async (root, options) => {
313318

314319
if (!addedPropTypesImportByReplacement && !hasImportOrRequire(rootNode, moduleName)) {
315320
if (usesImportSyntax(rootNode)) {
316-
const pos = firstSortedImportPosition(rootNode, moduleName);
321+
const pos = firstSortedImportPosition(rootNode, moduleName, source);
317322
edits.push({
318323
startPos: pos,
319324
endPos: pos,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {React, ReactDOM} from 'nylas-exports'
2+
import {Menu} from 'nylas-component-kit'
3+
4+
import PropTypes from 'prop-types';
5+
export default class DropdownMenu extends React.Component {
6+
static propTypes = {
7+
item: PropTypes.object,
8+
}
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {React, ReactDOM} from 'nylas-exports'
2+
import {Menu} from 'nylas-component-kit'
3+
4+
export default class DropdownMenu extends React.Component {
5+
static propTypes = {
6+
item: React.PropTypes.object,
7+
}
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"react-proptypes-migrations": [
3+
{
4+
"cardinality": {
5+
"file": "tests/custom-react-import-insert-newline/input.tsx"
6+
},
7+
"count": 1
8+
}
9+
]
10+
}

0 commit comments

Comments
 (0)