You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After auditing all 26 working transforms in codemods/, a large amount of import-handling, line-removal, scope-walking, and indentation logic is reimplemented per codemod. Promoting a small set of shared helpers (some already in @jssg/utils, some currently internal to userland-migrations/utils) would let us delete an estimated ~1.5–2 kLOC of plumbing and make the transforms easier to write and maintain.
This issue tracks the rollout. Each util is broken out into its own actionable sub-task with the concrete codemods that benefit.
Utils under consideration
Already published in @jssg/utils:
getImport
addImport
removeImport
Currently internal in userland-migrations/utils/src/ast-grep (candidates to upstream):
P0 — Upstream import-statement + require-call + module-dependencies to @jssg/utils (or extend getImport to accept from: string[] and return all matches). Removes the copy-pasted ~40-line hasReact + importSource + requireSource triplet from at least 18 codemods. Bottleneck for react-to-react-dom, react-native-view-prop-types, update-react-imports, replace-react-test-renderer-import, and the Component/PureComponent-across-React-aliases finders.
P0 — Add update-binding / remove-binding to @jssg/utils.removeImport already covers the drop the whole specifier half, but the rename / merge / append / fall-back-to-side-effect-import logic is rebuilt from scratch in many transforms.
P1 — Extend addImport with sorted/last-import insertion.react-proptypes-to-prop-types (firstSortedImportPosition/firstSortedRequirePosition), replace-reactdom-render (currently inserts at offset 0), and remove-legacy-context reinvent it.
P1 — Add remove-lines (or removeStatement) returning Edit[].lineStart / consumeLineBreak / consumeBlankLine / removalEdit are duplicated almost verbatim across many transforms.
P2 — Promote resolve-binding-path. Several transforms do bespoke member_expression walks to detect React.foo vs imported foo.
P2 — Add get-scope with multi-kind stop list (e.g. [statement_block, class_body, program]).
Each box below should become its own follow-up issue/PR. Migrating a transform usually means deleting the local helpers, importing the equivalent from @jssg/utils, and re-running the snapshot tests.
Migrate codemods/prop-types-typescript (indent detection for inserted interfaces).
Out of scope
These transforms don't materially benefit from any of the listed utils:
codemods/error-boundaries — pure declarative property_identifier rename.
codemods/rename-unsafe-lifecycles — pure declarative property_identifier rename.
codemods/remove-context-provider — pure JSX-name rewrite, no imports/scope.
codemods/react-19-migration-recipe — workflow recipe, no transform script.
Notes / open questions
Should multi-source detection live as an extension of getImport (e.g. from: string[], returns Binding[]), or as a separate getModuleDependencies API? Several transforms (react-to-react-dom, pure-component, sort-comp, etc.) need both ESM imports and CJS require calls and late var X; X = require(...) patterns in one pass.
update-binding needs to support: rename, merge with existing specifier, append new specifier, drop named clause while preserving default, and fall back to bare side-effect import 'src'; when the last specifier is removed (legacy ImportSpecifier.remove() behavior). Make sure the API surface is rich enough.
addImport needs an insertion-point strategy parameter (e.g. placement: "sorted" | "after-last-import" | "top"), and ideally should de-dupe against an existing matching import.
Summary
After auditing all 26 working transforms in
codemods/, a large amount of import-handling, line-removal, scope-walking, and indentation logic is reimplemented per codemod. Promoting a small set of shared helpers (some already in@jssg/utils, some currently internal touserland-migrations/utils) would let us delete an estimated ~1.5–2 kLOC of plumbing and make the transforms easier to write and maintain.This issue tracks the rollout. Each util is broken out into its own actionable sub-task with the concrete codemods that benefit.
Utils under consideration
Already published in
@jssg/utils:getImportaddImportremoveImportCurrently internal in
userland-migrations/utils/src/ast-grep(candidates to upstream):get-scopeindent(detectIndentUnit,getLineIndent)remove-linesupdate-bindingremove-bindingresolve-binding-pathimport-statementrequire-callmodule-dependenciesAggregate ranking
import-statement+require-call+module-dependenciesgetImport(extended for multi-source)update-binding/remove-bindingremoveImportaddImport(with sorted-insertion heuristic)remove-lines(Edit-returning variant)resolve-binding-pathget-scope(multi-kind stop list)indent(detectIndentUnit,getLineIndent)Recommendations
import-statement+require-call+module-dependenciesto@jssg/utils(or extendgetImportto acceptfrom: string[]and return all matches). Removes the copy-pasted ~40-linehasReact+importSource+requireSourcetriplet from at least 18 codemods. Bottleneck forreact-to-react-dom,react-native-view-prop-types,update-react-imports,replace-react-test-renderer-import, and theComponent/PureComponent-across-React-aliases finders.update-binding/remove-bindingto@jssg/utils.removeImportalready covers thedrop the whole specifierhalf, but the rename / merge / append / fall-back-to-side-effect-import logic is rebuilt from scratch in many transforms.addImportwith sorted/last-import insertion.react-proptypes-to-prop-types(firstSortedImportPosition/firstSortedRequirePosition),replace-reactdom-render(currently inserts at offset 0), andremove-legacy-contextreinvent it.remove-lines(orremoveStatement) returningEdit[].lineStart/consumeLineBreak/consumeBlankLine/removalEditare duplicated almost verbatim across many transforms.resolve-binding-path. Several transforms do bespokemember_expressionwalks to detectReact.foovs importedfoo.get-scopewith multi-kind stop list (e.g.[statement_block,class_body,program]).indent(detectIndentUnit,getLineIndent).Sub-tasks (per util)
P0 —
import-statement/require-call/module-dependenciesgetModuleDependenciesAPI) to@jssg/utils.codemods/create-element-to-jsx(hasReact,requireSource,importSource).codemods/find-dom-node(hasReact,requireSource,importSource).codemods/pure-render-mixin(hasReact).codemods/sort-comp(hasReact,reactImportAliases).codemods/pure-component(reactImportAliasesover multiple sources).codemods/react-to-react-dom(findModuleImports,findRequireBinding,findAssignmentBinding).codemods/react-native-view-prop-types(the sixfind*finders).codemods/replace-react-test-renderer-import(replacementPatternForLiteral).codemods/update-react-imports(isReactImport,getReactSpecifier,namedSpecifiers).codemods/remove-memoization(collectReactImports,buildReactObjectNames,buildNamedImportMap).codemods/replace-create-factory(collectReactImports).codemods/remove-legacy-context(collectReactImportInfo).codemods/replace-act-import(test-utils import lookup).codemods/replace-use-form-state(findReactDOMMemberImportNames,findNamedUseFormStateImports).codemods/replace-reactdom-render(findReactDomMemberImportNames,findNamedImportNames).codemods/use-context-hook(findReactMemberImportNames,findNamedUseContextImports).codemods/react-proptypes-to-prop-types(propTypesBindingFromModule).codemods/prop-types-typescript(collectPropTypesImports).P0 —
update-binding/remove-binding@jssg/utils(rename / merge / append / fall-back-to-side-effect-import).codemods/react-dom-to-react-dom-factories(renameDOM→createElement).codemods/use-context-hook(renameuseContext→use, preserving alias).codemods/update-react-imports(default/namespace → named conversion, dedupe).codemods/replace-use-form-state(dropuseFormStatefromreact-dom, add toreact).codemods/replace-act-import(removeNodeWithComma+ specifier merge).codemods/pure-component(buildImportSpecifierEdits).codemods/replace-create-factory(importStatementReplacement).codemods/remove-memoization(importStatementReplacement).codemods/react-to-react-dom(splitreact→react/react-dom/react-dom/server).codemods/react-proptypes-to-prop-types(dropPropTypesfrom React).codemods/react-native-view-prop-types(appendViewPropTypestoreact-native).codemods/prop-types-typescript(cleanupPropTypesImports).codemods/remove-legacy-context(specifier rewrites in React import).P1 —
addImportwith sorted/last-import insertionaddImportAPI in@jssg/utilsto support sorted-by-source and last-import-anchor placement.codemods/replace-reactdom-render(createRootinsertion currently at offset 0).codemods/replace-act-import(mergeactinto existing react named import or insert new).codemods/replace-use-form-state(useActionStateinsertion).codemods/react-proptypes-to-prop-types(firstSortedImportPosition/firstSortedRequirePosition).codemods/react-to-react-dom(buildImportLine/buildRequireLine).codemods/remove-legacy-context(the appendedimport * as React from "react";).codemods/react-native-view-prop-types(ViewPropTypesinsertion).codemods/update-react-imports(rebuild named clause).P1 —
remove-lines/removeStatementEdit-returning variant to@jssg/utils.codemods/manual-bind-to-arrow(statementRemovalEdit,nodeRemovalEdit).codemods/replace-default-props(removeStatementEdit).codemods/remove-legacy-context(removalEdit, line-helpers).codemods/replace-create-factory(removeImportStatementEdit).codemods/remove-memoization(removeImportStatementEdit).codemods/pure-component(lineDeletionRange).codemods/pure-render-mixin(declarationRemovalRange).codemods/replace-act-import(line-aware specifier removal).codemods/update-react-imports(statementRange-based removal).codemods/react-proptypes-to-prop-types(statementRange,statementRangeWithLeadingLineComments).codemods/prop-types-typescript(cleanupPropTypesImportsregex pass).P2 —
resolve-binding-path@jssg/utils.codemods/find-dom-node(getDOMNoderesolution).codemods/react-dom-to-react-dom-factories(React.DOM.foo→React.createElement(...)).codemods/replace-reactdom-render(ReactDOM.rendervs importedrender).codemods/replace-act-import(getTestUtilsImport).codemods/replace-use-form-state(ReactDOM.useFormStateresolution).codemods/react-to-react-dom(React.findDOMNode/React.renderToStringclassification).codemods/replace-create-factory(createFactoryCallInfo).codemods/remove-memoization(memoizationCallInfo).codemods/update-react-imports(React.foo→ barefoo).codemods/use-context-hook(React.useContextvs nameduseContext).P2 —
get-scope(multi-kind stop list)@jssg/utils.codemods/manual-bind-to-arrow(getClassBody, ancestor walks).codemods/replace-string-ref(isInsideReactClassComponent).codemods/replace-default-props(enclosingStatement,topLevelStatement).codemods/react-proptypes-to-prop-types(nearestStatement).codemods/prop-types-typescript(topLevelStatement).codemods/pure-component/codemods/pure-render-mixin(recurring ancestor walks).codemods/replace-act-import(ancestor walks).codemods/replace-create-factory/codemods/remove-memoization(isInsideImport,selectedAncestorForIndex).P2 —
indent(detectIndentUnit,getLineIndent)@jssg/utils.codemods/replace-reactdom-render(reindentText,getIndent).codemods/replace-default-props(bodyStatementIndent).codemods/create-element-to-jsx(lineIndent).codemods/manual-bind-to-arrow(leading-whitespace probing).codemods/prop-types-typescript(indent detection for inserted interfaces).Out of scope
These transforms don't materially benefit from any of the listed utils:
codemods/error-boundaries— pure declarativeproperty_identifierrename.codemods/rename-unsafe-lifecycles— pure declarativeproperty_identifierrename.codemods/remove-context-provider— pure JSX-name rewrite, no imports/scope.codemods/react-19-migration-recipe— workflow recipe, no transform script.Notes / open questions
getImport(e.g.from: string[], returnsBinding[]), or as a separategetModuleDependenciesAPI? Several transforms (react-to-react-dom,pure-component,sort-comp, etc.) need both ESM imports and CJSrequirecalls and latevar X; X = require(...)patterns in one pass.update-bindingneeds to support: rename, merge with existing specifier, append new specifier, drop named clause while preserving default, and fall back to bare side-effectimport 'src';when the last specifier is removed (legacyImportSpecifier.remove()behavior). Make sure the API surface is rich enough.addImportneeds an insertion-point strategy parameter (e.g.placement: "sorted" | "after-last-import" | "top"), and ideally should de-dupe against an existing matching import.