From a1cf77475ee5d3253c2d19159b40ccb01b6f0cbb Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Thu, 3 Jul 2025 17:56:16 -0700 Subject: [PATCH 01/14] Add initial codemod with test setup --- bin/react_18_upgrade.dart | 15 ++ lib/src/executables/react_18_upgrade.dart | 63 +++++++ .../constants.dart | 27 +++ .../html_script_updater.dart | 122 +++++++------- pubspec.yaml | 1 + test/executables/react_18_upgrade_test.dart | 154 ++++++++++++++++++ 6 files changed, 325 insertions(+), 57 deletions(-) create mode 100644 bin/react_18_upgrade.dart create mode 100644 lib/src/executables/react_18_upgrade.dart create mode 100644 lib/src/react_18_upgrade_suggestors/constants.dart create mode 100644 test/executables/react_18_upgrade_test.dart diff --git a/bin/react_18_upgrade.dart b/bin/react_18_upgrade.dart new file mode 100644 index 00000000..7485030c --- /dev/null +++ b/bin/react_18_upgrade.dart @@ -0,0 +1,15 @@ +// Copyright 2025 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'package:over_react_codemod/src/executables/react_18_upgrade.dart'; diff --git a/lib/src/executables/react_18_upgrade.dart b/lib/src/executables/react_18_upgrade.dart new file mode 100644 index 00000000..8efdaa3f --- /dev/null +++ b/lib/src/executables/react_18_upgrade.dart @@ -0,0 +1,63 @@ +// Copyright 2025 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:codemod/codemod.dart'; +import 'package:over_react_codemod/src/ignoreable.dart'; +import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/constants.dart'; +import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/dart_script_updater.dart'; +import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/html_script_updater.dart'; +import 'package:over_react_codemod/src/util.dart'; +import 'package:over_react_codemod/src/util/pubspec_upgrader.dart'; + +import '../react_18_upgrade_suggestors/constants.dart'; + +const _changesRequiredOutput = """ + To update your code, run the following commands in your repository: + dart pub global activate over_react_codemod + dart pub global run over_react_codemod:rmui_bundle_update +"""; + +void main(List args) async { + final parser = ArgParser.allowAnything(); + + final parsedArgs = parser.parse(args); + + // Update RMUI bundle script to all HTML files (and templates). + exitCode = await runInteractiveCodemodSequence( + allHtmlPathsIncludingTemplates(), + react17to18ReactJsScriptNames.keys.map((key) => HtmlScriptUpdater(key, react17to18ReactJsScriptNames[key]!, updateAttributes: false)), + defaultYes: true, + args: parsedArgs.rest, + additionalHelpOutput: parser.usage, + changesRequiredOutput: _changesRequiredOutput, + ); + + if (exitCode != 0) return; + + // Update RMUI bundle script to all Dart files. + exitCode = await runInteractiveCodemodSequence( + allDartPathsExceptHidden(), + [ + DartScriptUpdater(rmuiBundleDev, rmuiBundleDevUpdated), + DartScriptUpdater(rmuiBundleProd, rmuiBundleProdUpdated), + ], + defaultYes: true, + args: parsedArgs.rest, + additionalHelpOutput: parser.usage, + changesRequiredOutput: _changesRequiredOutput, + ); +} diff --git a/lib/src/react_18_upgrade_suggestors/constants.dart b/lib/src/react_18_upgrade_suggestors/constants.dart new file mode 100644 index 00000000..91d73342 --- /dev/null +++ b/lib/src/react_18_upgrade_suggestors/constants.dart @@ -0,0 +1,27 @@ +// Copyright 2025 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const reactPath = 'packages/react/'; + +const react17to18ReactJsScriptNames = { + '${reactPath}react.js': '${reactPath}js/react.dev.js', + '${reactPath}react_with_addons.js': '${reactPath}js/react.dev.js', + '${reactPath}react_prod.js': '${reactPath}js/react.min.js', + '${reactPath}react_with_react_dom_prod.js': '${reactPath}js/react.min.js', +}; + +const react17ReactDomJsOnlyScriptNames = [ + '${reactPath}react_dom.js', + '${reactPath}react_dom_prod.js', +]; diff --git a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart index ea337f8b..f710ccee 100644 --- a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart @@ -23,8 +23,11 @@ import 'constants.dart'; class HtmlScriptUpdater { final String existingScriptPath; final String newScriptPath; + /// Whether or not to update attributes on script/link tags (like type/crossorigin) + /// while also updating the script path. + final bool updateAttributes; - HtmlScriptUpdater(this.existingScriptPath, this.newScriptPath); + HtmlScriptUpdater(this.existingScriptPath, this.newScriptPath, {this.updateAttributes = true}); Stream call(FileContext context) async* { final relevantScriptTags = [ @@ -49,64 +52,69 @@ class HtmlScriptUpdater { final patches = []; + if(updateAttributes) { // Add type="module" attribute to script tag. - for (final scriptTagMatch in relevantScriptTags) { - final scriptTag = scriptTagMatch.group(0); - if (scriptTag == null) continue; - final typeAttributes = getAttributePattern('type').allMatches(scriptTag); - if (typeAttributes.isNotEmpty) { - final attribute = typeAttributes.first; - final value = attribute.group(1); - if (value == 'module') { - continue; - } else { - // If the value of the type attribute is not "module", overwrite it. - patches.add(Patch( - typeModuleAttribute, - scriptTagMatch.start + attribute.start, - scriptTagMatch.start + attribute.end, - )); - } - } else { - // If the type attribute does not exist, add it. - final srcAttribute = getAttributePattern('src').allMatches(scriptTag); - patches.add(Patch( - ' ${typeModuleAttribute}', - scriptTagMatch.start + srcAttribute.first.end, - scriptTagMatch.start + srcAttribute.first.end, - )); - } - } + for (final scriptTagMatch in relevantScriptTags) { + final scriptTag = scriptTagMatch.group(0); + if (scriptTag == null) continue; + final typeAttributes = + getAttributePattern('type').allMatches(scriptTag); + if (typeAttributes.isNotEmpty) { + final attribute = typeAttributes.first; + final value = attribute.group(1); + if (value == 'module') { + continue; + } else { + // If the value of the type attribute is not "module", overwrite it. + patches.add(Patch( + typeModuleAttribute, + scriptTagMatch.start + attribute.start, + scriptTagMatch.start + attribute.end, + )); + } + } else { + // If the type attribute does not exist, add it. + final srcAttribute = getAttributePattern('src').allMatches( + scriptTag); + patches.add(Patch( + ' ${typeModuleAttribute}', + scriptTagMatch.start + srcAttribute.first.end, + scriptTagMatch.start + srcAttribute.first.end, + )); + } + } - // Add crossorigin="" attribute to link tag. - for (final linkTagToMatch in relevantLinkTags) { - final linkTag = linkTagToMatch.group(0); - if (linkTag == null) continue; - final crossOriginAttributes = - getAttributePattern('crossorigin').allMatches(linkTag); - if (crossOriginAttributes.isNotEmpty) { - final attribute = crossOriginAttributes.first; - final value = attribute.group(1); - if (value == '') { - continue; - } else { - // If the value of the crossorigin attribute is not "", overwrite it. - patches.add(Patch( - crossOriginAttribute, - linkTagToMatch.start + attribute.start, - linkTagToMatch.start + attribute.end, - )); - } - } else { - // If the crossorigin attribute does not exist, add it. - final hrefAttribute = getAttributePattern('href').allMatches(linkTag); - patches.add(Patch( - ' ${crossOriginAttribute}', - linkTagToMatch.start + hrefAttribute.first.end, - linkTagToMatch.start + hrefAttribute.first.end, - )); - } - } + // Add crossorigin="" attribute to link tag. + for (final linkTagToMatch in relevantLinkTags) { + final linkTag = linkTagToMatch.group(0); + if (linkTag == null) continue; + final crossOriginAttributes = + getAttributePattern('crossorigin').allMatches(linkTag); + if (crossOriginAttributes.isNotEmpty) { + final attribute = crossOriginAttributes.first; + final value = attribute.group(1); + if (value == '') { + continue; + } else { + // If the value of the crossorigin attribute is not "", overwrite it. + patches.add(Patch( + crossOriginAttribute, + linkTagToMatch.start + attribute.start, + linkTagToMatch.start + attribute.end, + )); + } + } else { + // If the crossorigin attribute does not exist, add it. + final hrefAttribute = getAttributePattern('href').allMatches( + linkTag); + patches.add(Patch( + ' ${crossOriginAttribute}', + linkTagToMatch.start + hrefAttribute.first.end, + linkTagToMatch.start + hrefAttribute.first.end, + )); + } + } + } // Update existing path to new path. final scriptMatches = existingScriptPath.allMatches(context.sourceText); diff --git a/pubspec.yaml b/pubspec.yaml index 0bf527b2..5e46683c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,3 +55,4 @@ executables: intl_message_migration: sort_intl: unify_package_rename: + react_18_upgrade: diff --git a/test/executables/react_18_upgrade_test.dart b/test/executables/react_18_upgrade_test.dart new file mode 100644 index 00000000..c5aa66df --- /dev/null +++ b/test/executables/react_18_upgrade_test.dart @@ -0,0 +1,154 @@ +// Copyright 2025 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:codemod/src/run_interactive_codemod.dart' show codemodArgParser; +import 'package:meta/meta.dart'; +import 'package:over_react_codemod/src/util/package_util.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +// Change this to `true` and all of the functional tests in this file will print +// the stdout/stderr of the codemod processes. +final _debug = false; + +void main() { + group('react_18_upgrade executable', () { + final react18CodemodScript = + p.join(findPackageRootFor(p.current), 'bin/react_18_upgrade.dart'); + + testCodemod( + 'applies all patches via --yes-to-all,' + 'and also correctly runs `pub get` if needed, migrates components,' + ' adds MUI imports, and removes WSD imports all in a single run', + script: react18CodemodScript, + input: inputFiles(), + expectedOutput: expectedOutputFiles(), + args: ['--yes-to-all']); + + testCodemod('--fail-on-changes exits with 0 when no changes needed', + script: react18CodemodScript, + input: expectedOutputFiles(), + expectedOutput: expectedOutputFiles(), + args: ['--fail-on-changes'], body: (out, err) { + expect(out, contains('No changes needed.')); + }); + }); +} + +d.DirectoryDescriptor inputFiles( + {Iterable additionalFilesInLib = const []}) => + d.dir('project', [ + // todo add link versions + d.file('dev.html', /*language=html*/ ''' + + '''), + d.file('dev_with_addons.html', /*language=html*/ ''' + + '''), + d.file('prod.html', /*language=html*/ ''' + + '''), + d.file('prod_with_addons.html', /*language=html*/ ''' + + ''') + ]); + +d.DirectoryDescriptor expectedOutputFiles({ + Iterable additionalFilesInLib = const [], + String rmuiVersionConstraint = '^1.1.1', +}) => + d.dir('project', [ + d.file('dev.html', /*language=html*/ ''' + + '''), + d.file('dev_with_addons.html', /*language=html*/ ''' + + '''), + d.file('prod.html', /*language=html*/ ''' + + '''), + d.file('prod_with_addons.html', /*language=html*/ ''' + + ''') + ]); + +// Adapted from `testCodemod` in https://github.com/Workiva/dart_codemod/blob/c5d245308554b0e1e7a15a54fbd2c79a9231e2be/test/functional/run_interactive_codemod_test.dart#L39 +// Intentionally does not run `pub get` on the project, since we want to test that the MUI executable does that. +@isTest +Future testCodemod( + String description, { + required String script, + required d.DirectoryDescriptor input, + d.DirectoryDescriptor? expectedOutput, + List? args, + void Function(String out, String err)? body, + int? expectedExitCode, + List? stdinLines, + }) async { + test(description, () async { + final projectDir = input; + await projectDir.create(); + + final processArgs = [ + script, + ...?args, + ]; + if (_debug) { + processArgs.add('--verbose'); + } + final process = await Process.start('dart', processArgs, + workingDirectory: projectDir.io.path); + + // If _debug, split these single-subscription streams into two + // so that we can display the output as it comes in. + final stdoutStreams = StreamSplitter.splitFrom( + process.stdout.transform(utf8.decoder), _debug ? 2 : 1); + final stderrStreams = StreamSplitter.splitFrom( + process.stderr.transform(utf8.decoder), _debug ? 2 : 1); + if (_debug) { + stdoutStreams[1] + .transform(LineSplitter()) + .forEach((line) => print('STDOUT: $line')); + stderrStreams[1] + .transform(LineSplitter()) + .forEach((line) => print('STDERR: $line')); + } + + stdinLines?.forEach(process.stdin.writeln); + final codemodExitCode = await process.exitCode; + expectedExitCode ??= 0; + + final codemodStdout = await stdoutStreams[0].join(); + final codemodStderr = await stderrStreams[0].join(); + + expect(codemodExitCode, expectedExitCode, + reason: 'Expected codemod to exit with code $expectedExitCode, but ' + 'it exited with $codemodExitCode.\n' + 'Process stderr:\n$codemodStderr'); + + if (expectedOutput != null) { + // Expect that the modified projet matches the gold files. + await expectedOutput.validate(); + } + + if (body != null) { + body(codemodStdout, codemodStderr); + } + }); +} From 63110b5e709550a0a816dcd66ab202fc1eed65a9 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Mon, 7 Jul 2025 11:09:46 -0700 Subject: [PATCH 02/14] Update removal --- lib/src/executables/react_18_upgrade.dart | 15 +- .../html_script_updater.dart | 155 +++++++------ test/executables/react_18_upgrade_test.dart | 215 +++++++++--------- .../html_script_updater_test.dart | 26 ++- 4 files changed, 226 insertions(+), 185 deletions(-) diff --git a/lib/src/executables/react_18_upgrade.dart b/lib/src/executables/react_18_upgrade.dart index 8efdaa3f..701bfb2d 100644 --- a/lib/src/executables/react_18_upgrade.dart +++ b/lib/src/executables/react_18_upgrade.dart @@ -36,7 +36,7 @@ void main(List args) async { final parsedArgs = parser.parse(args); - // Update RMUI bundle script to all HTML files (and templates). + // Update react.js bundle files to React 18 versions exitCode = await runInteractiveCodemodSequence( allHtmlPathsIncludingTemplates(), react17to18ReactJsScriptNames.keys.map((key) => HtmlScriptUpdater(key, react17to18ReactJsScriptNames[key]!, updateAttributes: false)), @@ -48,6 +48,19 @@ void main(List args) async { if (exitCode != 0) return; + // Remove React 17 react_dom bundle files + exitCode = await runInteractiveCodemodSequence( + allHtmlPathsIncludingTemplates(), + // todo clean up this api + react17ReactDomJsOnlyScriptNames.map((name) => HtmlScriptUpdater(name, 'abc', removeTag: true)), + defaultYes: true, + args: parsedArgs.rest, + additionalHelpOutput: parser.usage, + changesRequiredOutput: _changesRequiredOutput, + ); + + if (exitCode != 0) return; + // Update RMUI bundle script to all Dart files. exitCode = await runInteractiveCodemodSequence( allDartPathsExceptHidden(), diff --git a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart index f710ccee..9b249e00 100644 --- a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart @@ -26,8 +26,9 @@ class HtmlScriptUpdater { /// Whether or not to update attributes on script/link tags (like type/crossorigin) /// while also updating the script path. final bool updateAttributes; + final bool removeTag; - HtmlScriptUpdater(this.existingScriptPath, this.newScriptPath, {this.updateAttributes = true}); + HtmlScriptUpdater(this.existingScriptPath, this.newScriptPath, {this.updateAttributes = true, this.removeTag = false}); Stream call(FileContext context) async* { final relevantScriptTags = [ @@ -52,79 +53,89 @@ class HtmlScriptUpdater { final patches = []; - if(updateAttributes) { - // Add type="module" attribute to script tag. - for (final scriptTagMatch in relevantScriptTags) { - final scriptTag = scriptTagMatch.group(0); - if (scriptTag == null) continue; - final typeAttributes = - getAttributePattern('type').allMatches(scriptTag); - if (typeAttributes.isNotEmpty) { - final attribute = typeAttributes.first; - final value = attribute.group(1); - if (value == 'module') { - continue; - } else { - // If the value of the type attribute is not "module", overwrite it. - patches.add(Patch( - typeModuleAttribute, - scriptTagMatch.start + attribute.start, - scriptTagMatch.start + attribute.end, - )); - } - } else { - // If the type attribute does not exist, add it. - final srcAttribute = getAttributePattern('src').allMatches( - scriptTag); - patches.add(Patch( - ' ${typeModuleAttribute}', - scriptTagMatch.start + srcAttribute.first.end, - scriptTagMatch.start + srcAttribute.first.end, - )); - } - } + if(removeTag) { + [...relevantScriptTags, ...relevantLinkTags].forEach((tag) async { + patches.add(Patch( + '', + tag.start, + tag.end, + )); + }); + } else{ + if (updateAttributes) { + // Add type="module" attribute to script tag. + for (final scriptTagMatch in relevantScriptTags) { + final scriptTag = scriptTagMatch.group(0); + if (scriptTag == null) continue; + final typeAttributes = + getAttributePattern('type').allMatches(scriptTag); + if (typeAttributes.isNotEmpty) { + final attribute = typeAttributes.first; + final value = attribute.group(1); + if (value == 'module') { + continue; + } else { + // If the value of the type attribute is not "module", overwrite it. + patches.add(Patch( + typeModuleAttribute, + scriptTagMatch.start + attribute.start, + scriptTagMatch.start + attribute.end, + )); + } + } else { + // If the type attribute does not exist, add it. + final srcAttribute = + getAttributePattern('src').allMatches(scriptTag); + patches.add(Patch( + ' ${typeModuleAttribute}', + scriptTagMatch.start + srcAttribute.first.end, + scriptTagMatch.start + srcAttribute.first.end, + )); + } + } - // Add crossorigin="" attribute to link tag. - for (final linkTagToMatch in relevantLinkTags) { - final linkTag = linkTagToMatch.group(0); - if (linkTag == null) continue; - final crossOriginAttributes = - getAttributePattern('crossorigin').allMatches(linkTag); - if (crossOriginAttributes.isNotEmpty) { - final attribute = crossOriginAttributes.first; - final value = attribute.group(1); - if (value == '') { - continue; - } else { - // If the value of the crossorigin attribute is not "", overwrite it. - patches.add(Patch( - crossOriginAttribute, - linkTagToMatch.start + attribute.start, - linkTagToMatch.start + attribute.end, - )); - } - } else { - // If the crossorigin attribute does not exist, add it. - final hrefAttribute = getAttributePattern('href').allMatches( - linkTag); - patches.add(Patch( - ' ${crossOriginAttribute}', - linkTagToMatch.start + hrefAttribute.first.end, - linkTagToMatch.start + hrefAttribute.first.end, - )); - } - } - } + // Add crossorigin="" attribute to link tag. + for (final linkTagToMatch in relevantLinkTags) { + final linkTag = linkTagToMatch.group(0); + if (linkTag == null) continue; + final crossOriginAttributes = + getAttributePattern('crossorigin').allMatches(linkTag); + if (crossOriginAttributes.isNotEmpty) { + final attribute = crossOriginAttributes.first; + final value = attribute.group(1); + if (value == '') { + continue; + } else { + // If the value of the crossorigin attribute is not "", overwrite it. + patches.add(Patch( + crossOriginAttribute, + linkTagToMatch.start + attribute.start, + linkTagToMatch.start + attribute.end, + )); + } + } else { + // If the crossorigin attribute does not exist, add it. + final hrefAttribute = + getAttributePattern('href').allMatches(linkTag); + patches.add(Patch( + ' ${crossOriginAttribute}', + linkTagToMatch.start + hrefAttribute.first.end, + linkTagToMatch.start + hrefAttribute.first.end, + )); + } + } + } - // Update existing path to new path. - final scriptMatches = existingScriptPath.allMatches(context.sourceText); - scriptMatches.forEach((match) async { - patches.add(Patch( - newScriptPath, - match.start, - match.end, - )); - }); + // Update existing path to new path. + final scriptMatches = existingScriptPath.allMatches(context.sourceText); + scriptMatches.forEach((match) async { + patches.add(Patch( + newScriptPath, + match.start, + match.end, + )); + }); + } yield* Stream.fromIterable(patches); } diff --git a/test/executables/react_18_upgrade_test.dart b/test/executables/react_18_upgrade_test.dart index c5aa66df..66978889 100644 --- a/test/executables/react_18_upgrade_test.dart +++ b/test/executables/react_18_upgrade_test.dart @@ -23,132 +23,127 @@ import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; -// Change this to `true` and all of the functional tests in this file will print -// the stdout/stderr of the codemod processes. -final _debug = false; +import 'mui_migration_test.dart'; void main() { group('react_18_upgrade executable', () { final react18CodemodScript = p.join(findPackageRootFor(p.current), 'bin/react_18_upgrade.dart'); - testCodemod( - 'applies all patches via --yes-to-all,' - 'and also correctly runs `pub get` if needed, migrates components,' - ' adds MUI imports, and removes WSD imports all in a single run', - script: react18CodemodScript, - input: inputFiles(), - expectedOutput: expectedOutputFiles(), - args: ['--yes-to-all']); - - testCodemod('--fail-on-changes exits with 0 when no changes needed', - script: react18CodemodScript, - input: expectedOutputFiles(), - expectedOutput: expectedOutputFiles(), - args: ['--fail-on-changes'], body: (out, err) { - expect(out, contains('No changes needed.')); - }); - }); -} - -d.DirectoryDescriptor inputFiles( - {Iterable additionalFilesInLib = const []}) => - d.dir('project', [ - // todo add link versions - d.file('dev.html', /*language=html*/ ''' + group('updates script tags', () { + testCodemod( + 'dev', + script: react18CodemodScript, + input: d.dir('project', [ + // todo add link versions + d.file('dev.html', /*language=html*/ ''' '''), - d.file('dev_with_addons.html', /*language=html*/ ''' + d.file('dev_with_addons.html', /*language=html*/ ''' '''), - d.file('prod.html', /*language=html*/ ''' + ]), + expectedOutput: d.dir('project', [ + d.file('dev.html', /*language=html*/ ''' + +'''), + d.file('dev_with_addons.html', /*language=html*/ ''' + +'''), + ]), + args: ['--yes-to-all']); + + testCodemod( + 'prod', + script: react18CodemodScript, + input: d.dir('project', [ + d.file('prod.html', /*language=html*/ ''' '''), - d.file('prod_with_addons.html', /*language=html*/ ''' + d.file('prod_with_addons.html', /*language=html*/ ''' ''') - ]); - -d.DirectoryDescriptor expectedOutputFiles({ - Iterable additionalFilesInLib = const [], - String rmuiVersionConstraint = '^1.1.1', -}) => - d.dir('project', [ - d.file('dev.html', /*language=html*/ ''' - - '''), - d.file('dev_with_addons.html', /*language=html*/ ''' - - '''), - d.file('prod.html', /*language=html*/ ''' + ]), + expectedOutput: d.dir('project', [ + d.file('prod.html', /*language=html*/ ''' - '''), - d.file('prod_with_addons.html', /*language=html*/ ''' +'''), + d.file('prod_with_addons.html', /*language=html*/ ''' - ''') - ]); - -// Adapted from `testCodemod` in https://github.com/Workiva/dart_codemod/blob/c5d245308554b0e1e7a15a54fbd2c79a9231e2be/test/functional/run_interactive_codemod_test.dart#L39 -// Intentionally does not run `pub get` on the project, since we want to test that the MUI executable does that. -@isTest -Future testCodemod( - String description, { - required String script, - required d.DirectoryDescriptor input, - d.DirectoryDescriptor? expectedOutput, - List? args, - void Function(String out, String err)? body, - int? expectedExitCode, - List? stdinLines, - }) async { - test(description, () async { - final projectDir = input; - await projectDir.create(); - - final processArgs = [ - script, - ...?args, - ]; - if (_debug) { - processArgs.add('--verbose'); - } - final process = await Process.start('dart', processArgs, - workingDirectory: projectDir.io.path); - - // If _debug, split these single-subscription streams into two - // so that we can display the output as it comes in. - final stdoutStreams = StreamSplitter.splitFrom( - process.stdout.transform(utf8.decoder), _debug ? 2 : 1); - final stderrStreams = StreamSplitter.splitFrom( - process.stderr.transform(utf8.decoder), _debug ? 2 : 1); - if (_debug) { - stdoutStreams[1] - .transform(LineSplitter()) - .forEach((line) => print('STDOUT: $line')); - stderrStreams[1] - .transform(LineSplitter()) - .forEach((line) => print('STDERR: $line')); - } - - stdinLines?.forEach(process.stdin.writeln); - final codemodExitCode = await process.exitCode; - expectedExitCode ??= 0; - - final codemodStdout = await stdoutStreams[0].join(); - final codemodStderr = await stderrStreams[0].join(); +''') + ]), + args: ['--yes-to-all']); + }); + + group('updates link tags', () { + testCodemod( + 'dev', + script: react18CodemodScript, + input: d.dir('project', [ + // todo add link versions + d.file('dev.html', /*language=html*/ ''' + + '''), + d.file('dev_with_addons.html', /*language=html*/ ''' + + '''), + ]), + expectedOutput: d.dir('project', [ + d.file('dev.html', /*language=html*/ ''' + + '''), + d.file('dev_with_addons.html', /*language=html*/ ''' + + '''), + ]), + args: ['--yes-to-all']); + + testCodemod( + 'prod', + script: react18CodemodScript, + input: d.dir('project', [ + d.file('prod.html', /*language=html*/ ''' + + '''), + d.file('prod_with_addons.html', /*language=html*/ ''' + + ''') + ]), + expectedOutput: d.dir('project', [ + d.file('prod.html', /*language=html*/ ''' + + '''), + d.file('prod_with_addons.html', /*language=html*/ ''' + + ''') + ]), + args: ['--yes-to-all']); + }); - expect(codemodExitCode, expectedExitCode, - reason: 'Expected codemod to exit with code $expectedExitCode, but ' - 'it exited with $codemodExitCode.\n' - 'Process stderr:\n$codemodStderr'); - - if (expectedOutput != null) { - // Expect that the modified projet matches the gold files. - await expectedOutput.validate(); - } - - if (body != null) { - body(codemodStdout, codemodStderr); - } + testCodemod('--fail-on-changes exits with 0 when no changes needed', + script: react18CodemodScript, + input: d.dir('project', [ + d.file('dev.html', /*language=html*/ ''' +'''), + d.file('dev_with_addons.html', /*language=html*/ ''' +'''), + d.file('prod.html', /*language=html*/ ''' +'''), + d.file('prod_with_addons.html', /*language=html*/ ''' +''') + ]), + expectedOutput: d.dir('project', [ + d.file('dev.html', /*language=html*/ ''' +'''), + d.file('dev_with_addons.html', /*language=html*/ ''' +'''), + d.file('prod.html', /*language=html*/ ''' +'''), + d.file('prod_with_addons.html', /*language=html*/ ''' +''') + ]), + args: ['--fail-on-changes'], body: (out, err) { + expect(out, contains('No changes needed.')); + }); }); } diff --git a/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart b/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart index f7139991..a006936a 100644 --- a/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart +++ b/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart @@ -240,11 +240,11 @@ void main() { expectedPatchCount: 4, shouldDartfmtOutput: false, input: '' - '\n' + '\n' '\n' '', expectedOutput: '' - '\n' + '\n' '\n' '', ); @@ -264,5 +264,27 @@ void main() { '', ); }); + + test('removeTag arg', () async { + final removeTagSuggestor = getSuggestorTester(HtmlScriptUpdater(rmuiBundleDev, rmuiBundleDevUpdated, removeTag: true)); + + await removeTagSuggestor( + expectedPatchCount: 5, + shouldDartfmtOutput: false, + input: '' + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '', + expectedOutput: '\n\n\n' + '\n\n' + '\n' + '', + ); + }); }); } From 1b5f08353898d5b9fb514ddc07076d3b00b483fb Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Mon, 7 Jul 2025 16:10:05 -0700 Subject: [PATCH 03/14] Clean up dart part of script --- lib/src/executables/react_18_upgrade.dart | 42 ++++-- .../constants.dart | 27 ---- .../dart_script_updater.dart | 127 +++++++++++------- .../html_script_updater.dart | 31 +++-- test/executables/react_18_upgrade_test.dart | 27 +++- .../dart_script_updater_test.dart | 40 ++++++ 6 files changed, 192 insertions(+), 102 deletions(-) delete mode 100644 lib/src/react_18_upgrade_suggestors/constants.dart diff --git a/lib/src/executables/react_18_upgrade.dart b/lib/src/executables/react_18_upgrade.dart index 701bfb2d..c86af650 100644 --- a/lib/src/executables/react_18_upgrade.dart +++ b/lib/src/executables/react_18_upgrade.dart @@ -16,14 +16,10 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:codemod/codemod.dart'; -import 'package:over_react_codemod/src/ignoreable.dart'; import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/constants.dart'; import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/dart_script_updater.dart'; import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/html_script_updater.dart'; import 'package:over_react_codemod/src/util.dart'; -import 'package:over_react_codemod/src/util/pubspec_upgrader.dart'; - -import '../react_18_upgrade_suggestors/constants.dart'; const _changesRequiredOutput = """ To update your code, run the following commands in your repository: @@ -36,7 +32,7 @@ void main(List args) async { final parsedArgs = parser.parse(args); - // Update react.js bundle files to React 18 versions + // Update react.js bundle files to React 18 versions in html files exitCode = await runInteractiveCodemodSequence( allHtmlPathsIncludingTemplates(), react17to18ReactJsScriptNames.keys.map((key) => HtmlScriptUpdater(key, react17to18ReactJsScriptNames[key]!, updateAttributes: false)), @@ -48,11 +44,10 @@ void main(List args) async { if (exitCode != 0) return; - // Remove React 17 react_dom bundle files + // Remove React 17 react_dom bundle files in html files exitCode = await runInteractiveCodemodSequence( allHtmlPathsIncludingTemplates(), - // todo clean up this api - react17ReactDomJsOnlyScriptNames.map((name) => HtmlScriptUpdater(name, 'abc', removeTag: true)), + react17ReactDomJsOnlyScriptNames.map((name) => HtmlScriptUpdater.remove(name)), defaultYes: true, args: parsedArgs.rest, additionalHelpOutput: parser.usage, @@ -61,16 +56,37 @@ void main(List args) async { if (exitCode != 0) return; - // Update RMUI bundle script to all Dart files. + // Update react.js bundle files to React 18 versions in Dart files + exitCode = await runInteractiveCodemodSequence( + allDartPathsExceptHidden(), + react17to18ReactJsScriptNames.keys.map((key) => DartScriptUpdater(key, react17to18ReactJsScriptNames[key]!, updateAttributes: false)), + defaultYes: true, + args: parsedArgs.rest, + additionalHelpOutput: parser.usage, + changesRequiredOutput: _changesRequiredOutput, + ); + + // Remove React 17 react_dom bundle files in html files exitCode = await runInteractiveCodemodSequence( allDartPathsExceptHidden(), - [ - DartScriptUpdater(rmuiBundleDev, rmuiBundleDevUpdated), - DartScriptUpdater(rmuiBundleProd, rmuiBundleProdUpdated), - ], + react17ReactDomJsOnlyScriptNames.map((name) => DartScriptUpdater.remove(name)), defaultYes: true, args: parsedArgs.rest, additionalHelpOutput: parser.usage, changesRequiredOutput: _changesRequiredOutput, ); } + +const reactPath = 'packages/react/'; + +const react17to18ReactJsScriptNames = { + '${reactPath}react.js': '${reactPath}js/react.dev.js', + '${reactPath}react_with_addons.js': '${reactPath}js/react.dev.js', + '${reactPath}react_prod.js': '${reactPath}js/react.min.js', + '${reactPath}react_with_react_dom_prod.js': '${reactPath}js/react.min.js', +}; + +const react17ReactDomJsOnlyScriptNames = [ + '${reactPath}react_dom.js', + '${reactPath}react_dom_prod.js', +]; diff --git a/lib/src/react_18_upgrade_suggestors/constants.dart b/lib/src/react_18_upgrade_suggestors/constants.dart deleted file mode 100644 index 91d73342..00000000 --- a/lib/src/react_18_upgrade_suggestors/constants.dart +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2025 Workiva Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const reactPath = 'packages/react/'; - -const react17to18ReactJsScriptNames = { - '${reactPath}react.js': '${reactPath}js/react.dev.js', - '${reactPath}react_with_addons.js': '${reactPath}js/react.dev.js', - '${reactPath}react_prod.js': '${reactPath}js/react.min.js', - '${reactPath}react_with_react_dom_prod.js': '${reactPath}js/react.min.js', -}; - -const react17ReactDomJsOnlyScriptNames = [ - '${reactPath}react_dom.js', - '${reactPath}react_dom_prod.js', -]; diff --git a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart index b0929c28..7dde3ecf 100644 --- a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart @@ -26,9 +26,22 @@ import 'constants.dart'; class DartScriptUpdater extends RecursiveAstVisitor with AstVisitingSuggestor { final String existingScriptPath; - final String newScriptPath; + late final String newScriptPath; + /// Whether or not to update attributes on script/link tags (like type/crossorigin) + /// while also updating the script path. + late final bool updateAttributes; + late final bool removeTag; - DartScriptUpdater(this.existingScriptPath, this.newScriptPath); + DartScriptUpdater(this.existingScriptPath, this.newScriptPath, {this.updateAttributes = true}) { + removeTag = false; + } + + /// Use this constructor to remove the whole tag instead of updating it. + DartScriptUpdater.remove(this.existingScriptPath) { + removeTag = true; + updateAttributes = false; + newScriptPath = 'will be ignored'; + } @override void visitSimpleStringLiteral(SimpleStringLiteral node) { @@ -39,74 +52,88 @@ class DartScriptUpdater extends RecursiveAstVisitor ...Script(pathSubpattern: existingScriptPath) .pattern .allMatches(stringValue), - ...Script(pathSubpattern: newScriptPath).pattern.allMatches(stringValue) + ...?(!removeTag ? Script(pathSubpattern: newScriptPath).pattern.allMatches(stringValue): null) ]; final relevantLinkTags = [ ...Link(pathSubpattern: existingScriptPath) .pattern .allMatches(stringValue), - ...Link(pathSubpattern: newScriptPath).pattern.allMatches(stringValue) + ...?(!removeTag ? Link(pathSubpattern: newScriptPath).pattern.allMatches(stringValue) : null) ]; // Do not update if neither the existingScriptPath nor newScriptPath are in the file. if (relevantScriptTags.isEmpty && relevantLinkTags.isEmpty) return; - // Add type="module" attribute to script tag. - for (final scriptTagMatch in relevantScriptTags) { - final scriptTag = scriptTagMatch.group(0); - if (scriptTag == null) continue; - final typeAttributes = getAttributePattern('type').allMatches(scriptTag); - if (typeAttributes.isNotEmpty) { - final attribute = typeAttributes.first; - final value = attribute.group(1); - if (value == 'module') { - continue; + if(removeTag) { + [...relevantScriptTags, ...relevantLinkTags].forEach((tag) async { + yieldPatch( + '', + node.offset, + node.end + (node.literal.next.toString() == ',' ? 1 : 0), + ); + }); + return; + } + + if (updateAttributes){ + // Add type="module" attribute to script tag. + for (final scriptTagMatch in relevantScriptTags) { + final scriptTag = scriptTagMatch.group(0); + if (scriptTag == null) continue; + final typeAttributes = + getAttributePattern('type').allMatches(scriptTag); + if (typeAttributes.isNotEmpty) { + final attribute = typeAttributes.first; + final value = attribute.group(1); + if (value == 'module') { + continue; + } else { + // If the value of the type attribute is not "module", overwrite it. + yieldPatch( + typeModuleAttribute, + node.offset + scriptTagMatch.start + attribute.start, + node.offset + scriptTagMatch.start + attribute.end, + ); + } } else { - // If the value of the type attribute is not "module", overwrite it. + // If the type attribute does not exist, add it. + final srcAttribute = getAttributePattern('src').allMatches(scriptTag); yieldPatch( - typeModuleAttribute, - node.offset + scriptTagMatch.start + attribute.start, - node.offset + scriptTagMatch.start + attribute.end, + ' ${typeModuleAttribute}', + node.offset + scriptTagMatch.start + srcAttribute.first.end, + node.offset + scriptTagMatch.start + srcAttribute.first.end, ); } - } else { - // If the type attribute does not exist, add it. - final srcAttribute = getAttributePattern('src').allMatches(scriptTag); - yieldPatch( - ' ${typeModuleAttribute}', - node.offset + scriptTagMatch.start + srcAttribute.first.end, - node.offset + scriptTagMatch.start + srcAttribute.first.end, - ); } - } - // Add crossorigin="" attribute to link tag. - for (final linkTagMatch in relevantLinkTags) { - final linkTag = linkTagMatch.group(0); - if (linkTag == null) continue; - final crossOriginAttributes = - getAttributePattern('crossorigin').allMatches(linkTag); - if (crossOriginAttributes.isNotEmpty) { - final attribute = crossOriginAttributes.first; - final value = attribute.group(1); - if (value == '') { - continue; + // Add crossorigin="" attribute to link tag. + for (final linkTagMatch in relevantLinkTags) { + final linkTag = linkTagMatch.group(0); + if (linkTag == null) continue; + final crossOriginAttributes = + getAttributePattern('crossorigin').allMatches(linkTag); + if (crossOriginAttributes.isNotEmpty) { + final attribute = crossOriginAttributes.first; + final value = attribute.group(1); + if (value == '') { + continue; + } else { + // If the value of the crossorigin attribute is not "", overwrite it. + yieldPatch( + crossOriginAttribute, + node.offset + linkTagMatch.start + attribute.start, + node.offset + linkTagMatch.start + attribute.end, + ); + } } else { - // If the value of the crossorigin attribute is not "", overwrite it. + // If the crossorigin attribute does not exist, add it. + final hrefAttribute = getAttributePattern('href').allMatches(linkTag); yieldPatch( - crossOriginAttribute, - node.offset + linkTagMatch.start + attribute.start, - node.offset + linkTagMatch.start + attribute.end, + ' ${crossOriginAttribute}', + node.offset + linkTagMatch.start + hrefAttribute.first.end, + node.offset + linkTagMatch.start + hrefAttribute.first.end, ); } - } else { - // If the crossorigin attribute does not exist, add it. - final hrefAttribute = getAttributePattern('href').allMatches(linkTag); - yieldPatch( - ' ${crossOriginAttribute}', - node.offset + linkTagMatch.start + hrefAttribute.first.end, - node.offset + linkTagMatch.start + hrefAttribute.first.end, - ); } } diff --git a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart index 9b249e00..e68834ec 100644 --- a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart @@ -22,30 +22,39 @@ import 'constants.dart'; /// Meant to be run on HTML files (use [DartScriptUpdater] to run on Dart files). class HtmlScriptUpdater { final String existingScriptPath; - final String newScriptPath; + late final String newScriptPath; /// Whether or not to update attributes on script/link tags (like type/crossorigin) /// while also updating the script path. - final bool updateAttributes; - final bool removeTag; + late final bool updateAttributes; + late final bool removeTag; - HtmlScriptUpdater(this.existingScriptPath, this.newScriptPath, {this.updateAttributes = true, this.removeTag = false}); + HtmlScriptUpdater(this.existingScriptPath, this.newScriptPath, {this.updateAttributes = true}) { + removeTag = false; + } + + /// Use this constructor to remove the whole tag instead of updating it. + HtmlScriptUpdater.remove(this.existingScriptPath) { + removeTag = true; + updateAttributes = false; + newScriptPath = 'will be ignored'; + } Stream call(FileContext context) async* { final relevantScriptTags = [ ...Script(pathSubpattern: existingScriptPath) .pattern .allMatches(context.sourceText), - ...Script(pathSubpattern: newScriptPath) + ...?(!removeTag ? Script(pathSubpattern: newScriptPath) .pattern - .allMatches(context.sourceText) + .allMatches(context.sourceText) : null) ]; final relevantLinkTags = [ ...Link(pathSubpattern: existingScriptPath) .pattern .allMatches(context.sourceText), - ...Link(pathSubpattern: newScriptPath) + ...?(!removeTag ? Link(pathSubpattern: newScriptPath) .pattern - .allMatches(context.sourceText) + .allMatches(context.sourceText) : null) ]; // Do not update if neither the existingScriptPath nor newScriptPath are in the file. @@ -61,7 +70,10 @@ class HtmlScriptUpdater { tag.end, )); }); - } else{ + yield* Stream.fromIterable(patches); + return; + } + if (updateAttributes) { // Add type="module" attribute to script tag. for (final scriptTagMatch in relevantScriptTags) { @@ -124,7 +136,6 @@ class HtmlScriptUpdater { )); } } - } // Update existing path to new path. final scriptMatches = existingScriptPath.allMatches(context.sourceText); diff --git a/test/executables/react_18_upgrade_test.dart b/test/executables/react_18_upgrade_test.dart index 66978889..fbff7c5d 100644 --- a/test/executables/react_18_upgrade_test.dart +++ b/test/executables/react_18_upgrade_test.dart @@ -35,7 +35,6 @@ void main() { 'dev', script: react18CodemodScript, input: d.dir('project', [ - // todo add link versions d.file('dev.html', /*language=html*/ ''' '''), @@ -80,7 +79,6 @@ void main() { 'dev', script: react18CodemodScript, input: d.dir('project', [ - // todo add link versions d.file('dev.html', /*language=html*/ ''' '''), @@ -120,6 +118,31 @@ void main() { args: ['--yes-to-all']); }); + group('in Dart files', () { + testCodemod( + 'list', + script: react18CodemodScript, + input: d.dir('project', [ + d.file('main.dart', /*language=dart*/ ''' + List _reactHtmlHeaders = const [ + '', + '', + '', + '', + ]; + ''') + ]), + expectedOutput: d.dir('project', [ + d.file('main.dart', /*language=dart*/ ''' + List _reactHtmlHeaders = const [ + '', + '', + ]; + ''') + ]), + args: ['--yes-to-all']); + }); + testCodemod('--fail-on-changes exits with 0 when no changes needed', script: react18CodemodScript, input: d.dir('project', [ diff --git a/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart b/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart index de1f64eb..2172ce03 100644 --- a/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart +++ b/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart @@ -284,5 +284,45 @@ void main() { ''', ); }); + + test('updateAttributes arg', () async { + final updateSuggestor = getSuggestorTester(DartScriptUpdater(rmuiBundleDev, rmuiBundleDevUpdated, updateAttributes: false),); + + await updateSuggestor( + expectedPatchCount: 2, + input: ''' + List _reactHtmlHeaders = const [ + '', + '', + ]; + ''', + expectedOutput: ''' + List _reactHtmlHeaders = const [ + '', + '', + ]; + ''', + );}); + + test('remove constructor', () async { + final removeTagSuggestor = getSuggestorTester(DartScriptUpdater.remove(rmuiBundleDev)); + + await removeTagSuggestor( + expectedPatchCount: 2, + input: ''' + List _reactHtmlHeaders = const [ + '', + '', + '', + '', + ]; + ''', + expectedOutput: ''' + List _reactHtmlHeaders = const [ + '', + '', + ]; + ''', + );}); }); } From 1104ae3c3f86f396524cfc0287eebd99172cce47 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Mon, 7 Jul 2025 16:26:42 -0700 Subject: [PATCH 04/14] Fix broken test --- lib/src/executables/react_18_upgrade.dart | 1 - .../html_script_updater.dart | 6 ++---- test/executables/react_18_upgrade_test.dart | 2 ++ .../html_script_updater_test.dart | 9 +++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/src/executables/react_18_upgrade.dart b/lib/src/executables/react_18_upgrade.dart index c86af650..e9737083 100644 --- a/lib/src/executables/react_18_upgrade.dart +++ b/lib/src/executables/react_18_upgrade.dart @@ -16,7 +16,6 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:codemod/codemod.dart'; -import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/constants.dart'; import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/dart_script_updater.dart'; import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/html_script_updater.dart'; import 'package:over_react_codemod/src/util.dart'; diff --git a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart index e68834ec..1f0a42d3 100644 --- a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart @@ -70,10 +70,7 @@ class HtmlScriptUpdater { tag.end, )); }); - yield* Stream.fromIterable(patches); - return; - } - + } else{ if (updateAttributes) { // Add type="module" attribute to script tag. for (final scriptTagMatch in relevantScriptTags) { @@ -136,6 +133,7 @@ class HtmlScriptUpdater { )); } } + } // Update existing path to new path. final scriptMatches = existingScriptPath.allMatches(context.sourceText); diff --git a/test/executables/react_18_upgrade_test.dart b/test/executables/react_18_upgrade_test.dart index fbff7c5d..4ed62782 100644 --- a/test/executables/react_18_upgrade_test.dart +++ b/test/executables/react_18_upgrade_test.dart @@ -136,7 +136,9 @@ void main() { d.file('main.dart', /*language=dart*/ ''' List _reactHtmlHeaders = const [ '', + '', + ]; ''') ]), diff --git a/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart b/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart index a006936a..aa8245a6 100644 --- a/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart +++ b/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart @@ -266,10 +266,10 @@ void main() { }); test('removeTag arg', () async { - final removeTagSuggestor = getSuggestorTester(HtmlScriptUpdater(rmuiBundleDev, rmuiBundleDevUpdated, removeTag: true)); + final removeTagSuggestor = getSuggestorTester(HtmlScriptUpdater.remove(rmuiBundleDev)); await removeTagSuggestor( - expectedPatchCount: 5, + expectedPatchCount: 4, shouldDartfmtOutput: false, input: '' '\n' @@ -278,10 +278,11 @@ void main() { '\n' '\n' '\n' - '\n' + '\n' '', expectedOutput: '\n\n\n' - '\n\n' + '\n' + '\n' '\n' '', ); From 3a45f94dad97245da9584c129009a213b2617165 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Mon, 7 Jul 2025 16:27:27 -0700 Subject: [PATCH 05/14] Format --- lib/src/executables/react_18_upgrade.dart | 14 +++++-- .../dart_script_updater.dart | 18 ++++++--- .../html_script_updater.dart | 24 +++++++----- test/executables/react_18_upgrade_test.dart | 27 ++++--------- .../dart_script_updater_test.dart | 14 +++++-- .../html_script_updater_test.dart | 39 ++++++++++--------- 6 files changed, 76 insertions(+), 60 deletions(-) diff --git a/lib/src/executables/react_18_upgrade.dart b/lib/src/executables/react_18_upgrade.dart index e9737083..25a032fb 100644 --- a/lib/src/executables/react_18_upgrade.dart +++ b/lib/src/executables/react_18_upgrade.dart @@ -34,7 +34,9 @@ void main(List args) async { // Update react.js bundle files to React 18 versions in html files exitCode = await runInteractiveCodemodSequence( allHtmlPathsIncludingTemplates(), - react17to18ReactJsScriptNames.keys.map((key) => HtmlScriptUpdater(key, react17to18ReactJsScriptNames[key]!, updateAttributes: false)), + react17to18ReactJsScriptNames.keys.map((key) => HtmlScriptUpdater( + key, react17to18ReactJsScriptNames[key]!, + updateAttributes: false)), defaultYes: true, args: parsedArgs.rest, additionalHelpOutput: parser.usage, @@ -46,7 +48,8 @@ void main(List args) async { // Remove React 17 react_dom bundle files in html files exitCode = await runInteractiveCodemodSequence( allHtmlPathsIncludingTemplates(), - react17ReactDomJsOnlyScriptNames.map((name) => HtmlScriptUpdater.remove(name)), + react17ReactDomJsOnlyScriptNames + .map((name) => HtmlScriptUpdater.remove(name)), defaultYes: true, args: parsedArgs.rest, additionalHelpOutput: parser.usage, @@ -58,7 +61,9 @@ void main(List args) async { // Update react.js bundle files to React 18 versions in Dart files exitCode = await runInteractiveCodemodSequence( allDartPathsExceptHidden(), - react17to18ReactJsScriptNames.keys.map((key) => DartScriptUpdater(key, react17to18ReactJsScriptNames[key]!, updateAttributes: false)), + react17to18ReactJsScriptNames.keys.map((key) => DartScriptUpdater( + key, react17to18ReactJsScriptNames[key]!, + updateAttributes: false)), defaultYes: true, args: parsedArgs.rest, additionalHelpOutput: parser.usage, @@ -68,7 +73,8 @@ void main(List args) async { // Remove React 17 react_dom bundle files in html files exitCode = await runInteractiveCodemodSequence( allDartPathsExceptHidden(), - react17ReactDomJsOnlyScriptNames.map((name) => DartScriptUpdater.remove(name)), + react17ReactDomJsOnlyScriptNames + .map((name) => DartScriptUpdater.remove(name)), defaultYes: true, args: parsedArgs.rest, additionalHelpOutput: parser.usage, diff --git a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart index 7dde3ecf..c8a404bd 100644 --- a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart @@ -27,12 +27,14 @@ class DartScriptUpdater extends RecursiveAstVisitor with AstVisitingSuggestor { final String existingScriptPath; late final String newScriptPath; + /// Whether or not to update attributes on script/link tags (like type/crossorigin) /// while also updating the script path. late final bool updateAttributes; late final bool removeTag; - DartScriptUpdater(this.existingScriptPath, this.newScriptPath, {this.updateAttributes = true}) { + DartScriptUpdater(this.existingScriptPath, this.newScriptPath, + {this.updateAttributes = true}) { removeTag = false; } @@ -52,19 +54,25 @@ class DartScriptUpdater extends RecursiveAstVisitor ...Script(pathSubpattern: existingScriptPath) .pattern .allMatches(stringValue), - ...?(!removeTag ? Script(pathSubpattern: newScriptPath).pattern.allMatches(stringValue): null) + ...?(!removeTag + ? Script(pathSubpattern: newScriptPath) + .pattern + .allMatches(stringValue) + : null) ]; final relevantLinkTags = [ ...Link(pathSubpattern: existingScriptPath) .pattern .allMatches(stringValue), - ...?(!removeTag ? Link(pathSubpattern: newScriptPath).pattern.allMatches(stringValue) : null) + ...?(!removeTag + ? Link(pathSubpattern: newScriptPath).pattern.allMatches(stringValue) + : null) ]; // Do not update if neither the existingScriptPath nor newScriptPath are in the file. if (relevantScriptTags.isEmpty && relevantLinkTags.isEmpty) return; - if(removeTag) { + if (removeTag) { [...relevantScriptTags, ...relevantLinkTags].forEach((tag) async { yieldPatch( '', @@ -75,7 +83,7 @@ class DartScriptUpdater extends RecursiveAstVisitor return; } - if (updateAttributes){ + if (updateAttributes) { // Add type="module" attribute to script tag. for (final scriptTagMatch in relevantScriptTags) { final scriptTag = scriptTagMatch.group(0); diff --git a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart index 1f0a42d3..4069d009 100644 --- a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart @@ -23,12 +23,14 @@ import 'constants.dart'; class HtmlScriptUpdater { final String existingScriptPath; late final String newScriptPath; + /// Whether or not to update attributes on script/link tags (like type/crossorigin) /// while also updating the script path. late final bool updateAttributes; late final bool removeTag; - HtmlScriptUpdater(this.existingScriptPath, this.newScriptPath, {this.updateAttributes = true}) { + HtmlScriptUpdater(this.existingScriptPath, this.newScriptPath, + {this.updateAttributes = true}) { removeTag = false; } @@ -44,17 +46,21 @@ class HtmlScriptUpdater { ...Script(pathSubpattern: existingScriptPath) .pattern .allMatches(context.sourceText), - ...?(!removeTag ? Script(pathSubpattern: newScriptPath) - .pattern - .allMatches(context.sourceText) : null) + ...?(!removeTag + ? Script(pathSubpattern: newScriptPath) + .pattern + .allMatches(context.sourceText) + : null) ]; final relevantLinkTags = [ ...Link(pathSubpattern: existingScriptPath) .pattern .allMatches(context.sourceText), - ...?(!removeTag ? Link(pathSubpattern: newScriptPath) - .pattern - .allMatches(context.sourceText) : null) + ...?(!removeTag + ? Link(pathSubpattern: newScriptPath) + .pattern + .allMatches(context.sourceText) + : null) ]; // Do not update if neither the existingScriptPath nor newScriptPath are in the file. @@ -62,7 +68,7 @@ class HtmlScriptUpdater { final patches = []; - if(removeTag) { + if (removeTag) { [...relevantScriptTags, ...relevantLinkTags].forEach((tag) async { patches.add(Patch( '', @@ -70,7 +76,7 @@ class HtmlScriptUpdater { tag.end, )); }); - } else{ + } else { if (updateAttributes) { // Add type="module" attribute to script tag. for (final scriptTagMatch in relevantScriptTags) { diff --git a/test/executables/react_18_upgrade_test.dart b/test/executables/react_18_upgrade_test.dart index 4ed62782..26e88bd0 100644 --- a/test/executables/react_18_upgrade_test.dart +++ b/test/executables/react_18_upgrade_test.dart @@ -12,12 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:convert'; -import 'dart:io'; - -import 'package:async/async.dart'; -import 'package:codemod/src/run_interactive_codemod.dart' show codemodArgParser; -import 'package:meta/meta.dart'; import 'package:over_react_codemod/src/util/package_util.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -28,11 +22,10 @@ import 'mui_migration_test.dart'; void main() { group('react_18_upgrade executable', () { final react18CodemodScript = - p.join(findPackageRootFor(p.current), 'bin/react_18_upgrade.dart'); + p.join(findPackageRootFor(p.current), 'bin/react_18_upgrade.dart'); group('updates script tags', () { - testCodemod( - 'dev', + testCodemod('dev', script: react18CodemodScript, input: d.dir('project', [ d.file('dev.html', /*language=html*/ ''' @@ -52,8 +45,7 @@ void main() { ]), args: ['--yes-to-all']); - testCodemod( - 'prod', + testCodemod('prod', script: react18CodemodScript, input: d.dir('project', [ d.file('prod.html', /*language=html*/ ''' @@ -75,8 +67,7 @@ void main() { }); group('updates link tags', () { - testCodemod( - 'dev', + testCodemod('dev', script: react18CodemodScript, input: d.dir('project', [ d.file('dev.html', /*language=html*/ ''' @@ -96,8 +87,7 @@ void main() { ]), args: ['--yes-to-all']); - testCodemod( - 'prod', + testCodemod('prod', script: react18CodemodScript, input: d.dir('project', [ d.file('prod.html', /*language=html*/ ''' @@ -119,8 +109,7 @@ void main() { }); group('in Dart files', () { - testCodemod( - 'list', + testCodemod('list', script: react18CodemodScript, input: d.dir('project', [ d.file('main.dart', /*language=dart*/ ''' @@ -168,7 +157,7 @@ void main() { ''') ]), args: ['--fail-on-changes'], body: (out, err) { - expect(out, contains('No changes needed.')); - }); + expect(out, contains('No changes needed.')); + }); }); } diff --git a/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart b/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart index 2172ce03..b4384571 100644 --- a/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart +++ b/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart @@ -286,7 +286,10 @@ void main() { }); test('updateAttributes arg', () async { - final updateSuggestor = getSuggestorTester(DartScriptUpdater(rmuiBundleDev, rmuiBundleDevUpdated, updateAttributes: false),); + final updateSuggestor = getSuggestorTester( + DartScriptUpdater(rmuiBundleDev, rmuiBundleDevUpdated, + updateAttributes: false), + ); await updateSuggestor( expectedPatchCount: 2, @@ -302,10 +305,12 @@ void main() { '', ]; ''', - );}); + ); + }); test('remove constructor', () async { - final removeTagSuggestor = getSuggestorTester(DartScriptUpdater.remove(rmuiBundleDev)); + final removeTagSuggestor = + getSuggestorTester(DartScriptUpdater.remove(rmuiBundleDev)); await removeTagSuggestor( expectedPatchCount: 2, @@ -323,6 +328,7 @@ void main() { '', ]; ''', - );}); + ); + }); }); } diff --git a/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart b/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart index aa8245a6..17cd6707 100644 --- a/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart +++ b/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart @@ -266,26 +266,27 @@ void main() { }); test('removeTag arg', () async { - final removeTagSuggestor = getSuggestorTester(HtmlScriptUpdater.remove(rmuiBundleDev)); + final removeTagSuggestor = + getSuggestorTester(HtmlScriptUpdater.remove(rmuiBundleDev)); - await removeTagSuggestor( - expectedPatchCount: 4, - shouldDartfmtOutput: false, - input: '' - '\n' - '\n' - '\n' - '\n' - '\n' - '\n' - '\n' - '', - expectedOutput: '\n\n\n' - '\n' - '\n' - '\n' - '', - ); + await removeTagSuggestor( + expectedPatchCount: 4, + shouldDartfmtOutput: false, + input: '' + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '', + expectedOutput: '\n\n\n' + '\n' + '\n' + '\n' + '', + ); }); }); } From 5bc71de6e06292dee4eba106afa9810324053e5b Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Mon, 7 Jul 2025 16:48:21 -0700 Subject: [PATCH 06/14] Fix html whitespace issue --- lib/src/executables/react_18_upgrade.dart | 2 +- lib/src/rmui_bundle_update_suggestors/constants.dart | 5 +++-- lib/src/rmui_preparation_suggestors/constants.dart | 2 +- .../html_script_updater_test.dart | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/executables/react_18_upgrade.dart b/lib/src/executables/react_18_upgrade.dart index 25a032fb..d25a7284 100644 --- a/lib/src/executables/react_18_upgrade.dart +++ b/lib/src/executables/react_18_upgrade.dart @@ -23,7 +23,7 @@ import 'package:over_react_codemod/src/util.dart'; const _changesRequiredOutput = """ To update your code, run the following commands in your repository: dart pub global activate over_react_codemod - dart pub global run over_react_codemod:rmui_bundle_update + dart pub global run over_react_codemod:react_18_upgrade """; void main(List args) async { diff --git a/lib/src/rmui_bundle_update_suggestors/constants.dart b/lib/src/rmui_bundle_update_suggestors/constants.dart index b8fe71e0..b361298f 100644 --- a/lib/src/rmui_bundle_update_suggestors/constants.dart +++ b/lib/src/rmui_bundle_update_suggestors/constants.dart @@ -52,8 +52,9 @@ class Link { const Link({required this.pathSubpattern}); /// A pattern for finding a link tag with a matching path. - RegExp get pattern => RegExp( - r']*href="(?[^"]*)' + pathSubpattern + r'"[^>]*>'); + RegExp get pattern => RegExp(r']*href="(?[^"]*)' + + pathSubpattern + + r'"[^>]*>(?\n*)'); @override String toString() => diff --git a/lib/src/rmui_preparation_suggestors/constants.dart b/lib/src/rmui_preparation_suggestors/constants.dart index 698af75c..2672ac7b 100644 --- a/lib/src/rmui_preparation_suggestors/constants.dart +++ b/lib/src/rmui_preparation_suggestors/constants.dart @@ -43,7 +43,7 @@ class Script { RegExp get pattern => RegExp( r'(?[^\S\r\n]*).*)' + pathSubpattern + - r'".*'); + r'".*(?\n*)'); @override String toString() => diff --git a/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart b/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart index 17cd6707..0a08745b 100644 --- a/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart +++ b/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart @@ -281,7 +281,7 @@ void main() { '\n' '\n' '', - expectedOutput: '\n\n\n' + expectedOutput: '' '\n' '\n' '\n' From 33472f9c60e892c1d4958e2384df26e6946bdb37 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Mon, 7 Jul 2025 17:01:25 -0700 Subject: [PATCH 07/14] Fix failing tests --- lib/src/rmui_bundle_update_suggestors/constants.dart | 2 +- lib/src/rmui_preparation_suggestors/constants.dart | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/src/rmui_bundle_update_suggestors/constants.dart b/lib/src/rmui_bundle_update_suggestors/constants.dart index b361298f..114319c7 100644 --- a/lib/src/rmui_bundle_update_suggestors/constants.dart +++ b/lib/src/rmui_bundle_update_suggestors/constants.dart @@ -54,7 +54,7 @@ class Link { /// A pattern for finding a link tag with a matching path. RegExp get pattern => RegExp(r']*href="(?[^"]*)' + pathSubpattern + - r'"[^>]*>(?\n*)'); + r'"[^>]*>(?\n?)'); @override String toString() => diff --git a/lib/src/rmui_preparation_suggestors/constants.dart b/lib/src/rmui_preparation_suggestors/constants.dart index 2672ac7b..8281e564 100644 --- a/lib/src/rmui_preparation_suggestors/constants.dart +++ b/lib/src/rmui_preparation_suggestors/constants.dart @@ -24,14 +24,18 @@ final rmuiBundleProd = ScriptToAdd(path: 'packages/react_material_ui/react-material-ui.umd.js'); /// The script pattern for finding react-dart JS scripts. -const reactJsScript = Script(pathSubpattern: r'packages/react/react\w*.js'); +const reactJsScript = Script( + pathSubpattern: r'packages/react/react\w*.js', + includeTrailingNewLine: false); /// A script that can be searched for via a script tag [pattern] for a /// specific path ([pathSubpattern]). class Script { final String pathSubpattern; + final bool includeTrailingNewLine; - const Script({required this.pathSubpattern}); + const Script( + {required this.pathSubpattern, this.includeTrailingNewLine = true}); /// A pattern for finding a script tag with a matching path, /// including preceding whitespace and any path prefix. @@ -43,7 +47,8 @@ class Script { RegExp get pattern => RegExp( r'(?[^\S\r\n]*).*)' + pathSubpattern + - r'".*(?\n*)'); + r'".*' + + (includeTrailingNewLine ? r'(?\n?)' : '')); @override String toString() => From c5970418cd1818a2a25aad7c9d9c661004b1d961 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 8 Jul 2025 13:20:48 -0700 Subject: [PATCH 08/14] Fix string const in dart file --- .../dart_script_updater.dart | 11 ++- test/executables/react_18_upgrade_test.dart | 46 +++++++++ .../dart_script_updater_test.dart | 97 ++++++++++++++++++- 3 files changed, 150 insertions(+), 4 deletions(-) diff --git a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart index c8a404bd..0018c20c 100644 --- a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart @@ -74,10 +74,17 @@ class DartScriptUpdater extends RecursiveAstVisitor if (removeTag) { [...relevantScriptTags, ...relevantLinkTags].forEach((tag) async { + final tagEnd = node.offset + tag.end; + final tagStart = node.offset + tag.start; + final possibleCommaEnd = node.literal.next.toString() == ',' ? 1 : 0; + // If the tag end is also the node end, then also remove the end quote and comma if it exists. + final patchEnd = node.end - tagEnd <= 1 ? (node.end + possibleCommaEnd) : tagEnd; + final patchStart = tagStart - node.offset <= 1 ? node.offset : tagStart; + yieldPatch( '', - node.offset, - node.end + (node.literal.next.toString() == ',' ? 1 : 0), + patchStart, + patchEnd, ); }); return; diff --git a/test/executables/react_18_upgrade_test.dart b/test/executables/react_18_upgrade_test.dart index 26e88bd0..dbec27a8 100644 --- a/test/executables/react_18_upgrade_test.dart +++ b/test/executables/react_18_upgrade_test.dart @@ -132,6 +132,52 @@ void main() { ''') ]), args: ['--yes-to-all']); + + testCodemod('string const', + script: react18CodemodScript, + input: d.dir('project', [ + d.file('main.dart', r''' + const expectedWithReact = \'\'\' + + + + {{testName}} + + + + + + + {{testScript}} + + + + + \'\'\'; + ''') + ]), + expectedOutput: d.dir('project', [ + d.file('main.dart', r''' + const expectedWithReact = \'\'\' + + + + {{testName}} + + + + + + + {{testScript}} + + + + + \'\'\'; + ''') + ]), + args: ['--yes-to-all']); }); testCodemod('--fail-on-changes exits with 0 when no changes needed', diff --git a/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart b/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart index b4384571..b778444b 100644 --- a/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart +++ b/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:html'; + import 'package:codemod/codemod.dart'; import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/constants.dart'; import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/dart_script_updater.dart'; import 'package:test/test.dart'; +import '../../bin/react_18_upgrade.dart'; import '../util.dart'; void main() { @@ -285,6 +288,50 @@ void main() { ); }); + test('string const', () async { + await testSuggestor( + expectedPatchCount: 2, + input: ''' + const expectedWithReact = \'\'\' + + + + {{testName}} + + + + + + + {{testScript}} + + + + + \'\'\'; + ''', + expectedOutput: ''' + const expectedWithReact = \'\'\' + + + + {{testName}} + + + + + + + {{testScript}} + + + + + \'\'\'; + ''', + ); + }); + test('updateAttributes arg', () async { final updateSuggestor = getSuggestorTester( DartScriptUpdater(rmuiBundleDev, rmuiBundleDevUpdated, @@ -308,16 +355,18 @@ void main() { ); }); - test('remove constructor', () async { + group('remove constructor', () { final removeTagSuggestor = getSuggestorTester(DartScriptUpdater.remove(rmuiBundleDev)); + test('list', () async { await removeTagSuggestor( - expectedPatchCount: 2, + expectedPatchCount: 3, input: ''' List _reactHtmlHeaders = const [ '', '', + '', '', '', ]; @@ -330,5 +379,49 @@ void main() { ''', ); }); + + test('string const', () async { + await removeTagSuggestor( + expectedPatchCount: 1, + input: ''' + const expectedWithReact = \'\'\' + + + + {{testName}} + + + + + + + {{testScript}} + + + + + \'\'\'; + ''', + expectedOutput: ''' + const expectedWithReact = \'\'\' + + + + {{testName}} + + + + + + {{testScript}} + + + + + \'\'\'; + ''', + ); + }); + }); }); } From 4df4e0b7db4e8f37bb9d5951789e0278f6844faf Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 8 Jul 2025 13:27:09 -0700 Subject: [PATCH 09/14] Clean up --- .../dart_script_updater.dart | 11 ++++---- test/executables/react_18_upgrade_test.dart | 7 ++--- .../dart_script_updater_test.dart | 28 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart index 0018c20c..0727fe09 100644 --- a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart @@ -77,14 +77,15 @@ class DartScriptUpdater extends RecursiveAstVisitor final tagEnd = node.offset + tag.end; final tagStart = node.offset + tag.start; final possibleCommaEnd = node.literal.next.toString() == ',' ? 1 : 0; - // If the tag end is also the node end, then also remove the end quote and comma if it exists. - final patchEnd = node.end - tagEnd <= 1 ? (node.end + possibleCommaEnd) : tagEnd; - final patchStart = tagStart - node.offset <= 1 ? node.offset : tagStart; + final isTagSameAsNode = + tagStart - node.offset <= 1 && node.end - tagEnd <= 1; yieldPatch( '', - patchStart, - patchEnd, + // If [tag] spans the whole string literal in [node], then also include + // the quotes and comma in the removal. + isTagSameAsNode ? node.offset : tagStart, + isTagSameAsNode ? (node.end + possibleCommaEnd) : tagEnd, ); }); return; diff --git a/test/executables/react_18_upgrade_test.dart b/test/executables/react_18_upgrade_test.dart index dbec27a8..4a2a4d9c 100644 --- a/test/executables/react_18_upgrade_test.dart +++ b/test/executables/react_18_upgrade_test.dart @@ -136,7 +136,7 @@ void main() { testCodemod('string const', script: react18CodemodScript, input: d.dir('project', [ - d.file('main.dart', r''' + d.file('main.dart', ''' const expectedWithReact = \'\'\' @@ -157,14 +157,13 @@ void main() { ''') ]), expectedOutput: d.dir('project', [ - d.file('main.dart', r''' + d.file('main.dart', ''' const expectedWithReact = \'\'\' {{testName}} - - + diff --git a/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart b/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart index b778444b..2e5b48f5 100644 --- a/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart +++ b/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart @@ -359,10 +359,10 @@ void main() { final removeTagSuggestor = getSuggestorTester(DartScriptUpdater.remove(rmuiBundleDev)); - test('list', () async { - await removeTagSuggestor( - expectedPatchCount: 3, - input: ''' + test('list', () async { + await removeTagSuggestor( + expectedPatchCount: 3, + input: ''' List _reactHtmlHeaders = const [ '', '', @@ -371,19 +371,19 @@ void main() { '', ]; ''', - expectedOutput: ''' + expectedOutput: ''' List _reactHtmlHeaders = const [ '', '', ]; ''', - ); - }); + ); + }); - test('string const', () async { - await removeTagSuggestor( - expectedPatchCount: 1, - input: ''' + test('string const', () async { + await removeTagSuggestor( + expectedPatchCount: 1, + input: ''' const expectedWithReact = \'\'\' @@ -402,7 +402,7 @@ void main() { \'\'\'; ''', - expectedOutput: ''' + expectedOutput: ''' const expectedWithReact = \'\'\' @@ -420,8 +420,8 @@ void main() { \'\'\'; ''', - ); - }); + ); + }); }); }); } From 98d67de34c4fd8486d1f301233f8ebd0a857a5db Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 8 Jul 2025 13:34:42 -0700 Subject: [PATCH 10/14] Clean up --- lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart index 0727fe09..8d5e52aa 100644 --- a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart @@ -78,6 +78,7 @@ class DartScriptUpdater extends RecursiveAstVisitor final tagStart = node.offset + tag.start; final possibleCommaEnd = node.literal.next.toString() == ',' ? 1 : 0; final isTagSameAsNode = + // Check if the only difference between [tag] and [node] is the quotes around [node]. tagStart - node.offset <= 1 && node.end - tagEnd <= 1; yieldPatch( From 2ec5f5e022756be892bd43dcede01ec75478343a Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 8 Jul 2025 13:36:01 -0700 Subject: [PATCH 11/14] Fix lint --- .../dart_script_updater_test.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart b/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart index 2e5b48f5..833df717 100644 --- a/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart +++ b/test/rmui_bundle_updater_suggestors/dart_script_updater_test.dart @@ -12,14 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:html'; - import 'package:codemod/codemod.dart'; import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/constants.dart'; import 'package:over_react_codemod/src/rmui_bundle_update_suggestors/dart_script_updater.dart'; import 'package:test/test.dart'; -import '../../bin/react_18_upgrade.dart'; import '../util.dart'; void main() { From 4622c57333baf71a7311eb57f14da2ed92b1a4d7 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Mon, 14 Jul 2025 09:24:09 -0500 Subject: [PATCH 12/14] Address feedback --- lib/src/executables/react_18_upgrade.dart | 56 +++++++++---------- .../dart_script_updater.dart | 30 +++++----- .../html_script_updater.dart | 24 ++++---- test/executables/react_18_upgrade_test.dart | 2 - 4 files changed, 52 insertions(+), 60 deletions(-) diff --git a/lib/src/executables/react_18_upgrade.dart b/lib/src/executables/react_18_upgrade.dart index d25a7284..dd882f27 100644 --- a/lib/src/executables/react_18_upgrade.dart +++ b/lib/src/executables/react_18_upgrade.dart @@ -26,30 +26,30 @@ const _changesRequiredOutput = """ dart pub global run over_react_codemod:react_18_upgrade """; +/// Updates React JS paths in HTML and Dart files from the React 17 versions to the React 18 versions. void main(List args) async { final parser = ArgParser.allowAnything(); final parsedArgs = parser.parse(args); - // Update react.js bundle files to React 18 versions in html files - exitCode = await runInteractiveCodemodSequence( - allHtmlPathsIncludingTemplates(), - react17to18ReactJsScriptNames.keys.map((key) => HtmlScriptUpdater( - key, react17to18ReactJsScriptNames[key]!, - updateAttributes: false)), - defaultYes: true, - args: parsedArgs.rest, - additionalHelpOutput: parser.usage, - changesRequiredOutput: _changesRequiredOutput, - ); - - if (exitCode != 0) return; + // Work around allowAnything not allowing you to pass flags. + if (parsedArgs.arguments.contains('--help')) { + // Print command description; flags and other output will get printed via runInteractiveCodemodSequence. + print( + 'Updates React JS paths in HTML and Dart files from the React 17 versions to the React 18 versions.\n'); + } - // Remove React 17 react_dom bundle files in html files exitCode = await runInteractiveCodemodSequence( allHtmlPathsIncludingTemplates(), - react17ReactDomJsOnlyScriptNames - .map((name) => HtmlScriptUpdater.remove(name)), + [ + // Update react.js bundle files to React 18 versions in html files + ...react17to18ReactJsScriptNames.keys.map((key) => HtmlScriptUpdater( + key, react17to18ReactJsScriptNames[key]!, + updateAttributes: false)), + // Remove React 17 react_dom bundle files in html files + ...react17ReactDomJsOnlyScriptNames + .map((name) => HtmlScriptUpdater.remove(name)), + ], defaultYes: true, args: parsedArgs.rest, additionalHelpOutput: parser.usage, @@ -58,23 +58,17 @@ void main(List args) async { if (exitCode != 0) return; - // Update react.js bundle files to React 18 versions in Dart files - exitCode = await runInteractiveCodemodSequence( - allDartPathsExceptHidden(), - react17to18ReactJsScriptNames.keys.map((key) => DartScriptUpdater( - key, react17to18ReactJsScriptNames[key]!, - updateAttributes: false)), - defaultYes: true, - args: parsedArgs.rest, - additionalHelpOutput: parser.usage, - changesRequiredOutput: _changesRequiredOutput, - ); - - // Remove React 17 react_dom bundle files in html files exitCode = await runInteractiveCodemodSequence( allDartPathsExceptHidden(), - react17ReactDomJsOnlyScriptNames - .map((name) => DartScriptUpdater.remove(name)), + [ + // Update react.js bundle files to React 18 versions in Dart files + ...react17to18ReactJsScriptNames.keys.map((key) => DartScriptUpdater( + key, react17to18ReactJsScriptNames[key]!, + updateAttributes: false)), + // Remove React 17 react_dom bundle files in Dart files + ...react17ReactDomJsOnlyScriptNames + .map((name) => DartScriptUpdater.remove(name)), + ], defaultYes: true, args: parsedArgs.rest, additionalHelpOutput: parser.usage, diff --git a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart index 8d5e52aa..3588a8f9 100644 --- a/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/dart_script_updater.dart @@ -26,24 +26,22 @@ import 'constants.dart'; class DartScriptUpdater extends RecursiveAstVisitor with AstVisitingSuggestor { final String existingScriptPath; - late final String newScriptPath; + final String newScriptPath; /// Whether or not to update attributes on script/link tags (like type/crossorigin) /// while also updating the script path. - late final bool updateAttributes; - late final bool removeTag; + final bool updateAttributes; + final bool removeTag; DartScriptUpdater(this.existingScriptPath, this.newScriptPath, - {this.updateAttributes = true}) { - removeTag = false; - } + {this.updateAttributes = true}) + : removeTag = false; /// Use this constructor to remove the whole tag instead of updating it. - DartScriptUpdater.remove(this.existingScriptPath) { - removeTag = true; - updateAttributes = false; - newScriptPath = 'will be ignored'; - } + DartScriptUpdater.remove(this.existingScriptPath) + : removeTag = true, + updateAttributes = false, + newScriptPath = 'will be ignored'; @override void visitSimpleStringLiteral(SimpleStringLiteral node) { @@ -73,7 +71,7 @@ class DartScriptUpdater extends RecursiveAstVisitor if (relevantScriptTags.isEmpty && relevantLinkTags.isEmpty) return; if (removeTag) { - [...relevantScriptTags, ...relevantLinkTags].forEach((tag) async { + for (final tag in [...relevantScriptTags, ...relevantLinkTags]) { final tagEnd = node.offset + tag.end; final tagStart = node.offset + tag.start; final possibleCommaEnd = node.literal.next.toString() == ',' ? 1 : 0; @@ -85,10 +83,14 @@ class DartScriptUpdater extends RecursiveAstVisitor '', // If [tag] spans the whole string literal in [node], then also include // the quotes and comma in the removal. - isTagSameAsNode ? node.offset : tagStart, + isTagSameAsNode + // Remove from the end of the previous token to take any preceding newline with it, + // so that we don't leave behind an empty line. + ? node.beginToken.previous?.end ?? node.offset + : tagStart, isTagSameAsNode ? (node.end + possibleCommaEnd) : tagEnd, ); - }); + } return; } diff --git a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart index 4069d009..61576227 100644 --- a/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart +++ b/lib/src/rmui_bundle_update_suggestors/html_script_updater.dart @@ -22,24 +22,22 @@ import 'constants.dart'; /// Meant to be run on HTML files (use [DartScriptUpdater] to run on Dart files). class HtmlScriptUpdater { final String existingScriptPath; - late final String newScriptPath; + final String newScriptPath; /// Whether or not to update attributes on script/link tags (like type/crossorigin) /// while also updating the script path. - late final bool updateAttributes; - late final bool removeTag; + final bool updateAttributes; + final bool removeTag; HtmlScriptUpdater(this.existingScriptPath, this.newScriptPath, - {this.updateAttributes = true}) { - removeTag = false; - } + {this.updateAttributes = true}) + : removeTag = false; /// Use this constructor to remove the whole tag instead of updating it. - HtmlScriptUpdater.remove(this.existingScriptPath) { - removeTag = true; - updateAttributes = false; - newScriptPath = 'will be ignored'; - } + HtmlScriptUpdater.remove(this.existingScriptPath) + : removeTag = true, + updateAttributes = false, + newScriptPath = 'will be ignored'; Stream call(FileContext context) async* { final relevantScriptTags = [ @@ -69,13 +67,13 @@ class HtmlScriptUpdater { final patches = []; if (removeTag) { - [...relevantScriptTags, ...relevantLinkTags].forEach((tag) async { + for (final tag in [...relevantScriptTags, ...relevantLinkTags]) { patches.add(Patch( '', tag.start, tag.end, )); - }); + } } else { if (updateAttributes) { // Add type="module" attribute to script tag. diff --git a/test/executables/react_18_upgrade_test.dart b/test/executables/react_18_upgrade_test.dart index 4a2a4d9c..42b3cb3e 100644 --- a/test/executables/react_18_upgrade_test.dart +++ b/test/executables/react_18_upgrade_test.dart @@ -125,9 +125,7 @@ void main() { d.file('main.dart', /*language=dart*/ ''' List _reactHtmlHeaders = const [ '', - '', - ]; ''') ]), From 89e33d136a3ca9a82473b73503388695e131ce42 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Mon, 14 Jul 2025 09:27:34 -0500 Subject: [PATCH 13/14] Add indentation test to fail --- .../html_script_updater_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart b/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart index 0a08745b..a8378323 100644 --- a/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart +++ b/test/rmui_bundle_updater_suggestors/html_script_updater_test.dart @@ -270,12 +270,13 @@ void main() { getSuggestorTester(HtmlScriptUpdater.remove(rmuiBundleDev)); await removeTagSuggestor( - expectedPatchCount: 4, shouldDartfmtOutput: false, input: '' '\n' + ' \n' '\n' '\n' + ' \n' '\n' '\n' '\n' From ade7817dca9104ee49d1f30c9864d66692d8bfd5 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Mon, 14 Jul 2025 09:38:48 -0500 Subject: [PATCH 14/14] Update indentation removal for links --- lib/src/rmui_bundle_update_suggestors/constants.dart | 7 ++++--- test/executables/react_18_upgrade_test.dart | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/src/rmui_bundle_update_suggestors/constants.dart b/lib/src/rmui_bundle_update_suggestors/constants.dart index 114319c7..33e532e5 100644 --- a/lib/src/rmui_bundle_update_suggestors/constants.dart +++ b/lib/src/rmui_bundle_update_suggestors/constants.dart @@ -52,9 +52,10 @@ class Link { const Link({required this.pathSubpattern}); /// A pattern for finding a link tag with a matching path. - RegExp get pattern => RegExp(r']*href="(?[^"]*)' + - pathSubpattern + - r'"[^>]*>(?\n?)'); + RegExp get pattern => RegExp( + r'(?[^\S\r\n]*)]*href="(?[^"]*)' + + pathSubpattern + + r'"[^>]*>(?\n?)'); @override String toString() => diff --git a/test/executables/react_18_upgrade_test.dart b/test/executables/react_18_upgrade_test.dart index 42b3cb3e..85924a9b 100644 --- a/test/executables/react_18_upgrade_test.dart +++ b/test/executables/react_18_upgrade_test.dart @@ -80,10 +80,10 @@ void main() { expectedOutput: d.dir('project', [ d.file('dev.html', /*language=html*/ ''' - '''), +'''), d.file('dev_with_addons.html', /*language=html*/ ''' - '''), +'''), ]), args: ['--yes-to-all']); @@ -100,10 +100,10 @@ void main() { expectedOutput: d.dir('project', [ d.file('prod.html', /*language=html*/ ''' - '''), +'''), d.file('prod_with_addons.html', /*language=html*/ ''' - ''') +''') ]), args: ['--yes-to-all']); });