Skip to content

Commit 78aa2c8

Browse files
Merge pull request #319 from Workiva/react-18-updater
FED-3903 React 18 Upgrade Codemod
2 parents 376bac3 + ade7817 commit 78aa2c8

File tree

10 files changed

+681
-122
lines changed

10 files changed

+681
-122
lines changed

bin/react_18_upgrade.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2025 Workiva Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
export 'package:over_react_codemod/src/executables/react_18_upgrade.dart';
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2025 Workiva Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'dart:io';
16+
17+
import 'package:args/args.dart';
18+
import 'package:codemod/codemod.dart';
19+
import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/dart_script_updater.dart';
20+
import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/html_script_updater.dart';
21+
import 'package:over_react_codemod/src/util.dart';
22+
23+
const _changesRequiredOutput = """
24+
To update your code, run the following commands in your repository:
25+
dart pub global activate over_react_codemod
26+
dart pub global run over_react_codemod:react_18_upgrade
27+
""";
28+
29+
/// Updates React JS paths in HTML and Dart files from the React 17 versions to the React 18 versions.
30+
void main(List<String> args) async {
31+
final parser = ArgParser.allowAnything();
32+
33+
final parsedArgs = parser.parse(args);
34+
35+
// Work around allowAnything not allowing you to pass flags.
36+
if (parsedArgs.arguments.contains('--help')) {
37+
// Print command description; flags and other output will get printed via runInteractiveCodemodSequence.
38+
print(
39+
'Updates React JS paths in HTML and Dart files from the React 17 versions to the React 18 versions.\n');
40+
}
41+
42+
exitCode = await runInteractiveCodemodSequence(
43+
allHtmlPathsIncludingTemplates(),
44+
[
45+
// Update react.js bundle files to React 18 versions in html files
46+
...react17to18ReactJsScriptNames.keys.map((key) => HtmlScriptUpdater(
47+
key, react17to18ReactJsScriptNames[key]!,
48+
updateAttributes: false)),
49+
// Remove React 17 react_dom bundle files in html files
50+
...react17ReactDomJsOnlyScriptNames
51+
.map((name) => HtmlScriptUpdater.remove(name)),
52+
],
53+
defaultYes: true,
54+
args: parsedArgs.rest,
55+
additionalHelpOutput: parser.usage,
56+
changesRequiredOutput: _changesRequiredOutput,
57+
);
58+
59+
if (exitCode != 0) return;
60+
61+
exitCode = await runInteractiveCodemodSequence(
62+
allDartPathsExceptHidden(),
63+
[
64+
// Update react.js bundle files to React 18 versions in Dart files
65+
...react17to18ReactJsScriptNames.keys.map((key) => DartScriptUpdater(
66+
key, react17to18ReactJsScriptNames[key]!,
67+
updateAttributes: false)),
68+
// Remove React 17 react_dom bundle files in Dart files
69+
...react17ReactDomJsOnlyScriptNames
70+
.map((name) => DartScriptUpdater.remove(name)),
71+
],
72+
defaultYes: true,
73+
args: parsedArgs.rest,
74+
additionalHelpOutput: parser.usage,
75+
changesRequiredOutput: _changesRequiredOutput,
76+
);
77+
}
78+
79+
const reactPath = 'packages/react/';
80+
81+
const react17to18ReactJsScriptNames = {
82+
'${reactPath}react.js': '${reactPath}js/react.dev.js',
83+
'${reactPath}react_with_addons.js': '${reactPath}js/react.dev.js',
84+
'${reactPath}react_prod.js': '${reactPath}js/react.min.js',
85+
'${reactPath}react_with_react_dom_prod.js': '${reactPath}js/react.min.js',
86+
};
87+
88+
const react17ReactDomJsOnlyScriptNames = [
89+
'${reactPath}react_dom.js',
90+
'${reactPath}react_dom_prod.js',
91+
];

lib/src/rmui_bundle_update_suggestors/constants.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ class Link {
5353

5454
/// A pattern for finding a link tag with a matching path.
5555
RegExp get pattern => RegExp(
56-
r'<link[^>]*href="(?<path_prefix>[^"]*)' + pathSubpattern + r'"[^>]*>');
56+
r'(?<preceding_whitespace>[^\S\r\n]*)<link[^>]*href="(?<path_prefix>[^"]*)' +
57+
pathSubpattern +
58+
r'"[^>]*>(?<trailing_new_line>\n?)');
5759

5860
@override
5961
String toString() =>

lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart

Lines changed: 95 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,20 @@ class DartScriptUpdater extends RecursiveAstVisitor<void>
2828
final String existingScriptPath;
2929
final String newScriptPath;
3030

31-
DartScriptUpdater(this.existingScriptPath, this.newScriptPath);
31+
/// Whether or not to update attributes on script/link tags (like type/crossorigin)
32+
/// while also updating the script path.
33+
final bool updateAttributes;
34+
final bool removeTag;
35+
36+
DartScriptUpdater(this.existingScriptPath, this.newScriptPath,
37+
{this.updateAttributes = true})
38+
: removeTag = false;
39+
40+
/// Use this constructor to remove the whole tag instead of updating it.
41+
DartScriptUpdater.remove(this.existingScriptPath)
42+
: removeTag = true,
43+
updateAttributes = false,
44+
newScriptPath = 'will be ignored';
3245

3346
@override
3447
void visitSimpleStringLiteral(SimpleStringLiteral node) {
@@ -39,74 +52,107 @@ class DartScriptUpdater extends RecursiveAstVisitor<void>
3952
...Script(pathSubpattern: existingScriptPath)
4053
.pattern
4154
.allMatches(stringValue),
42-
...Script(pathSubpattern: newScriptPath).pattern.allMatches(stringValue)
55+
...?(!removeTag
56+
? Script(pathSubpattern: newScriptPath)
57+
.pattern
58+
.allMatches(stringValue)
59+
: null)
4360
];
4461
final relevantLinkTags = [
4562
...Link(pathSubpattern: existingScriptPath)
4663
.pattern
4764
.allMatches(stringValue),
48-
...Link(pathSubpattern: newScriptPath).pattern.allMatches(stringValue)
65+
...?(!removeTag
66+
? Link(pathSubpattern: newScriptPath).pattern.allMatches(stringValue)
67+
: null)
4968
];
5069

5170
// Do not update if neither the existingScriptPath nor newScriptPath are in the file.
5271
if (relevantScriptTags.isEmpty && relevantLinkTags.isEmpty) return;
5372

54-
// Add type="module" attribute to script tag.
55-
for (final scriptTagMatch in relevantScriptTags) {
56-
final scriptTag = scriptTagMatch.group(0);
57-
if (scriptTag == null) continue;
58-
final typeAttributes = getAttributePattern('type').allMatches(scriptTag);
59-
if (typeAttributes.isNotEmpty) {
60-
final attribute = typeAttributes.first;
61-
final value = attribute.group(1);
62-
if (value == 'module') {
63-
continue;
73+
if (removeTag) {
74+
for (final tag in [...relevantScriptTags, ...relevantLinkTags]) {
75+
final tagEnd = node.offset + tag.end;
76+
final tagStart = node.offset + tag.start;
77+
final possibleCommaEnd = node.literal.next.toString() == ',' ? 1 : 0;
78+
final isTagSameAsNode =
79+
// Check if the only difference between [tag] and [node] is the quotes around [node].
80+
tagStart - node.offset <= 1 && node.end - tagEnd <= 1;
81+
82+
yieldPatch(
83+
'',
84+
// If [tag] spans the whole string literal in [node], then also include
85+
// the quotes and comma in the removal.
86+
isTagSameAsNode
87+
// Remove from the end of the previous token to take any preceding newline with it,
88+
// so that we don't leave behind an empty line.
89+
? node.beginToken.previous?.end ?? node.offset
90+
: tagStart,
91+
isTagSameAsNode ? (node.end + possibleCommaEnd) : tagEnd,
92+
);
93+
}
94+
return;
95+
}
96+
97+
if (updateAttributes) {
98+
// Add type="module" attribute to script tag.
99+
for (final scriptTagMatch in relevantScriptTags) {
100+
final scriptTag = scriptTagMatch.group(0);
101+
if (scriptTag == null) continue;
102+
final typeAttributes =
103+
getAttributePattern('type').allMatches(scriptTag);
104+
if (typeAttributes.isNotEmpty) {
105+
final attribute = typeAttributes.first;
106+
final value = attribute.group(1);
107+
if (value == 'module') {
108+
continue;
109+
} else {
110+
// If the value of the type attribute is not "module", overwrite it.
111+
yieldPatch(
112+
typeModuleAttribute,
113+
node.offset + scriptTagMatch.start + attribute.start,
114+
node.offset + scriptTagMatch.start + attribute.end,
115+
);
116+
}
64117
} else {
65-
// If the value of the type attribute is not "module", overwrite it.
118+
// If the type attribute does not exist, add it.
119+
final srcAttribute = getAttributePattern('src').allMatches(scriptTag);
66120
yieldPatch(
67-
typeModuleAttribute,
68-
node.offset + scriptTagMatch.start + attribute.start,
69-
node.offset + scriptTagMatch.start + attribute.end,
121+
' ${typeModuleAttribute}',
122+
node.offset + scriptTagMatch.start + srcAttribute.first.end,
123+
node.offset + scriptTagMatch.start + srcAttribute.first.end,
70124
);
71125
}
72-
} else {
73-
// If the type attribute does not exist, add it.
74-
final srcAttribute = getAttributePattern('src').allMatches(scriptTag);
75-
yieldPatch(
76-
' ${typeModuleAttribute}',
77-
node.offset + scriptTagMatch.start + srcAttribute.first.end,
78-
node.offset + scriptTagMatch.start + srcAttribute.first.end,
79-
);
80126
}
81-
}
82127

83-
// Add crossorigin="" attribute to link tag.
84-
for (final linkTagMatch in relevantLinkTags) {
85-
final linkTag = linkTagMatch.group(0);
86-
if (linkTag == null) continue;
87-
final crossOriginAttributes =
88-
getAttributePattern('crossorigin').allMatches(linkTag);
89-
if (crossOriginAttributes.isNotEmpty) {
90-
final attribute = crossOriginAttributes.first;
91-
final value = attribute.group(1);
92-
if (value == '') {
93-
continue;
128+
// Add crossorigin="" attribute to link tag.
129+
for (final linkTagMatch in relevantLinkTags) {
130+
final linkTag = linkTagMatch.group(0);
131+
if (linkTag == null) continue;
132+
final crossOriginAttributes =
133+
getAttributePattern('crossorigin').allMatches(linkTag);
134+
if (crossOriginAttributes.isNotEmpty) {
135+
final attribute = crossOriginAttributes.first;
136+
final value = attribute.group(1);
137+
if (value == '') {
138+
continue;
139+
} else {
140+
// If the value of the crossorigin attribute is not "", overwrite it.
141+
yieldPatch(
142+
crossOriginAttribute,
143+
node.offset + linkTagMatch.start + attribute.start,
144+
node.offset + linkTagMatch.start + attribute.end,
145+
);
146+
}
94147
} else {
95-
// If the value of the crossorigin attribute is not "", overwrite it.
148+
// If the crossorigin attribute does not exist, add it.
149+
final hrefAttribute = getAttributePattern('href').allMatches(linkTag);
96150
yieldPatch(
97-
crossOriginAttribute,
98-
node.offset + linkTagMatch.start + attribute.start,
99-
node.offset + linkTagMatch.start + attribute.end,
151+
' ${crossOriginAttribute}',
152+
node.offset + linkTagMatch.start + hrefAttribute.first.end,
153+
node.offset + linkTagMatch.start + hrefAttribute.first.end,
100154
);
101155
}
102-
} else {
103-
// If the crossorigin attribute does not exist, add it.
104-
final hrefAttribute = getAttributePattern('href').allMatches(linkTag);
105-
yieldPatch(
106-
' ${crossOriginAttribute}',
107-
node.offset + linkTagMatch.start + hrefAttribute.first.end,
108-
node.offset + linkTagMatch.start + hrefAttribute.first.end,
109-
);
110156
}
111157
}
112158

0 commit comments

Comments
 (0)