From 1e55fb6c34fafbe20dcaaa4088d205b2949a4083 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Oct 2026 21:22:26 +0000 Subject: [PATCH 01/10] Refactor four React JSSG codemods onto @jssg/utils import helpers Use getImport/getAllImports, addImport, and updateImport from @jssg/utils@^0.0.10 in use-context-hook, react-dom-to-react-dom-factories, unprefix-stable-apis, and fragment-ref-wrappers. Add fixtures that fail under ad-hoc import handling (CJS named useContext, aliased DOM/React, type-only unprefix, single-quote Fragment merge). Co-authored-by: Alex Bit --- codemods/fragment-ref-wrappers/package.json | 3 + .../fragment-ref-wrappers/scripts/codemod.ts | 139 ++++-------- .../no-react-import-ref-prop/expected.tsx | 2 +- .../tests/single-quote-merge/expected.tsx | 11 + .../tests/single-quote-merge/input.tsx | 11 + .../tests/single-quote-merge/metrics.json | 10 + .../package.json | 2 +- .../scripts/codemod.ts | 130 +++++++---- .../tests/aliased-dom-import/expected.tsx | 3 + .../tests/aliased-dom-import/input.tsx | 3 + .../tests/aliased-dom-import/metrics.json | 10 + .../tests/aliased-react-default/expected.tsx | 3 + .../tests/aliased-react-default/input.tsx | 3 + .../tests/aliased-react-default/metrics.json | 10 + codemods/unprefix-stable-apis/package.json | 3 + .../unprefix-stable-apis/scripts/codemod.ts | 214 +++++++++--------- .../tests/type-only-named-import/expected.tsx | 3 + .../tests/type-only-named-import/input.tsx | 3 + .../tests/type-only-named-import/metrics.json | 10 + codemods/use-context-hook/package.json | 2 +- codemods/use-context-hook/scripts/codemod.ts | 86 +++---- .../tests/cjs-named-use-context/expected.tsx | 4 + .../tests/cjs-named-use-context/input.tsx | 4 + .../tests/cjs-named-use-context/metrics.json | 11 + 24 files changed, 377 insertions(+), 303 deletions(-) create mode 100644 codemods/fragment-ref-wrappers/tests/single-quote-merge/expected.tsx create mode 100644 codemods/fragment-ref-wrappers/tests/single-quote-merge/input.tsx create mode 100644 codemods/fragment-ref-wrappers/tests/single-quote-merge/metrics.json create mode 100644 codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/input.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/metrics.json create mode 100644 codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/input.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/metrics.json create mode 100644 codemods/unprefix-stable-apis/tests/type-only-named-import/expected.tsx create mode 100644 codemods/unprefix-stable-apis/tests/type-only-named-import/input.tsx create mode 100644 codemods/unprefix-stable-apis/tests/type-only-named-import/metrics.json create mode 100644 codemods/use-context-hook/tests/cjs-named-use-context/expected.tsx create mode 100644 codemods/use-context-hook/tests/cjs-named-use-context/input.tsx create mode 100644 codemods/use-context-hook/tests/cjs-named-use-context/metrics.json diff --git a/codemods/fragment-ref-wrappers/package.json b/codemods/fragment-ref-wrappers/package.json index ef54177..258a4de 100644 --- a/codemods/fragment-ref-wrappers/package.json +++ b/codemods/fragment-ref-wrappers/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "@types/node": "latest", "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/fragment-ref-wrappers/scripts/codemod.ts b/codemods/fragment-ref-wrappers/scripts/codemod.ts index 0af8f1a..43804dd 100644 --- a/codemods/fragment-ref-wrappers/scripts/codemod.ts +++ b/codemods/fragment-ref-wrappers/scripts/codemod.ts @@ -1,6 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { addImport, getImport } from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -26,13 +27,6 @@ const FRAGMENT_INSTANCE_METHODS = new Set([ "scrollIntoView", ]); -type ReactImports = { - namespaces: Set; - fragmentLocalName: string | null; - firstNamedImport: SgNode | null; - lastImport: SgNode | null; -}; - type Wrapper = { element: SgNode; opening: SgNode; @@ -46,48 +40,6 @@ function namedChildren(node: SgNode | null): SgNode[] { return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); } -function sourceText(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) return fragment.text(); - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; -} - -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? sourceText(source) : null; -} - -function isTypeOnlyImport(node: SgNode): boolean { - return node.children().some((child) => !child.isNamed() && child.text() === "type"); -} - -function collectReactImports(rootNode: SgNode): ReactImports { - const result: ReactImports = { namespaces: new Set(), fragmentLocalName: null, firstNamedImport: null, lastImport: null }; - - for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (imp.parent()?.kind() !== "program") continue; - result.lastImport = imp; - if (importSource(imp) !== REACT_MODULE || isTypeOnlyImport(imp)) continue; - - const importClause = imp.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); - if (defaultIdentifier) result.namespaces.add(defaultIdentifier.text()); - const namespaceImport = imp.find({ rule: { kind: "namespace_import" } }); - const namespaceName = namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }); - if (namespaceName) result.namespaces.add(namespaceName.text()); - - if (imp.find({ rule: { kind: "named_imports" } }) && !result.firstNamedImport) result.firstNamedImport = imp; - for (const specifier of imp.findAll({ rule: { kind: "import_specifier" } })) { - if (specifier.field("name")?.text() === "Fragment") { - result.fragmentLocalName = specifier.field("alias")?.text() ?? "Fragment"; - } - } - } - - return result; -} - function attributeName(attr: SgNode): string | null { const name = attr.children().find((child) => child.kind() === "property_identifier"); return name ? name.text() : null; @@ -164,36 +116,33 @@ function refUsagesAreFragmentSafe(rootNode: SgNode, wrapper: Wra return true; } -function fragmentImportEdit(rootNode: SgNode, imports: ReactImports): Edit | null { - if (imports.firstNamedImport) { - const namedImports = imports.firstNamedImport.find({ rule: { kind: "named_imports" } }); - if (namedImports) { - const text = namedImports.text(); - let insertPos = namedImports.range().end.index - 1; - let offset = text.length - 1; - while (offset > 0 && /\s/.test(text[offset - 1] ?? "")) { - offset--; - insertPos--; - } - const inner = text.slice(1, -1).trim(); - return { startPos: insertPos, endPos: insertPos, insertedText: inner.length > 0 ? ", Fragment" : " Fragment " }; - } - } - - const statement = 'import { Fragment } from "react";'; - if (imports.lastImport) { - const insertPos = imports.lastImport.range().end.index; - return { startPos: insertPos, endPos: insertPos, insertedText: `\n${statement}` }; - } +function isLeadingDirective(node: SgNode | undefined): boolean { + if (!node || node.kind() !== "expression_statement") return false; + return namedChildren(node)[0]?.kind() === "string"; +} - // Keep a leading directive such as "use client" first. - const first = namedChildren(rootNode)[0]; - if (first && first.kind() === "expression_statement" && namedChildren(first)[0]?.kind() === "string") { - const insertPos = first.range().end.index; - return { startPos: insertPos, endPos: insertPos, insertedText: `\n\n${statement}` }; +function fragmentImportEdit(rootNode: SgNode): Edit | null { + const edit = addImport(rootNode, { + type: "named", + specifiers: [{ name: "Fragment" }], + from: REACT_MODULE, + }); + if (!edit) return null; + + // addImport inserts at file start when there are no imports; keep "use client" first. + if (edit.startPos === 0) { + const first = namedChildren(rootNode)[0]; + if (isLeadingDirective(first)) { + const insertPos = first!.range().end.index; + return { + startPos: insertPos, + endPos: insertPos, + insertedText: `\n\n${edit.insertedText.replace(/^\n+/, "").replace(/\n$/, "")}`, + }; + } } - return { startPos: 0, endPos: 0, insertedText: `${statement}\n\n` }; + return edit; } const transform: Transform = async (root) => { @@ -208,22 +157,34 @@ const transform: Transform = async (root) => { } if (wrappers.length === 0) return null; - const imports = collectReactImports(rootNode); + const fragmentImport = getImport(rootNode, { + type: "named", + name: "Fragment", + from: REACT_MODULE, + }); + const reactBinding = getImport(rootNode, { type: "default", from: REACT_MODULE }); + let fragmentName: string; - if (imports.fragmentLocalName) { - fragmentName = imports.fragmentLocalName; + if (fragmentImport) { + fragmentName = fragmentImport.alias; + } else if (reactBinding) { + fragmentName = `${reactBinding.alias}.Fragment`; } else { - const namespace = [...imports.namespaces][0]; - if (namespace) { - fragmentName = `${namespace}.Fragment`; - } else { - if (rootNode.find({ rule: { any: [{ kind: "identifier", regex: "^Fragment$" }, { kind: "type_identifier", regex: "^Fragment$" }] } })) { - return null; - } - fragmentName = "Fragment"; - const importEdit = fragmentImportEdit(rootNode, imports); - if (importEdit) edits.push(importEdit); + if ( + rootNode.find({ + rule: { + any: [ + { kind: "identifier", regex: "^Fragment$" }, + { kind: "type_identifier", regex: "^Fragment$" }, + ], + }, + }) + ) { + return null; } + fragmentName = "Fragment"; + const importEdit = fragmentImportEdit(rootNode); + if (importEdit) edits.push(importEdit); } for (const wrapper of wrappers) { diff --git a/codemods/fragment-ref-wrappers/tests/no-react-import-ref-prop/expected.tsx b/codemods/fragment-ref-wrappers/tests/no-react-import-ref-prop/expected.tsx index 0432667..a4553bf 100644 --- a/codemods/fragment-ref-wrappers/tests/no-react-import-ref-prop/expected.tsx +++ b/codemods/fragment-ref-wrappers/tests/no-react-import-ref-prop/expected.tsx @@ -1,6 +1,6 @@ "use client"; -import { Fragment } from "react"; +import { Fragment } from 'react'; export function Group({ ref, children }) { return ( diff --git a/codemods/fragment-ref-wrappers/tests/single-quote-merge/expected.tsx b/codemods/fragment-ref-wrappers/tests/single-quote-merge/expected.tsx new file mode 100644 index 0000000..07a33f9 --- /dev/null +++ b/codemods/fragment-ref-wrappers/tests/single-quote-merge/expected.tsx @@ -0,0 +1,11 @@ +import { useRef, Fragment } from 'react'; + +export function Group({ children }) { + const ref = useRef(null); + const focus = () => ref.current.focus(); + return ( + + {children} + + ); +} diff --git a/codemods/fragment-ref-wrappers/tests/single-quote-merge/input.tsx b/codemods/fragment-ref-wrappers/tests/single-quote-merge/input.tsx new file mode 100644 index 0000000..fa08218 --- /dev/null +++ b/codemods/fragment-ref-wrappers/tests/single-quote-merge/input.tsx @@ -0,0 +1,11 @@ +import { useRef } from 'react'; + +export function Group({ children }) { + const ref = useRef(null); + const focus = () => ref.current.focus(); + return ( +
+ {children} +
+ ); +} diff --git a/codemods/fragment-ref-wrappers/tests/single-quote-merge/metrics.json b/codemods/fragment-ref-wrappers/tests/single-quote-merge/metrics.json new file mode 100644 index 0000000..594e4bc --- /dev/null +++ b/codemods/fragment-ref-wrappers/tests/single-quote-merge/metrics.json @@ -0,0 +1,10 @@ +{ + "fragment-ref-wrapper-replacements": [ + { + "cardinality": { + "file": "tests/single-quote-merge/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/package.json b/codemods/react-dom-to-react-dom-factories/package.json index 88df66c..1464aa8 100644 --- a/codemods/react-dom-to-react-dom-factories/package.json +++ b/codemods/react-dom-to-react-dom-factories/package.json @@ -13,6 +13,6 @@ "typescript": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts b/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts index 41d61f9..183d685 100644 --- a/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts +++ b/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts @@ -1,7 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport } from "@jssg/utils/javascript/imports"; +import { getAllImports, getImport, updateImport } from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -48,11 +48,50 @@ function isReactRequireCall(node: SgNode | null): boolean { stringValue(firstArg) === "react"; } -function isReactIdentifier(node: SgNode | null): boolean { - return node?.kind() === "identifier" && node.text() === "React"; +function collectReactAliases(rootNode: SgNode): Set { + const names = new Set(); + for (const binding of getAllImports(rootNode, { type: "default", from: "react" })) { + names.add(binding.alias); + } + return names; +} + +function isReactIdentifier(node: SgNode | null, reactAliases: Set): boolean { + return node?.kind() === "identifier" && reactAliases.has(node.text()); +} + +function collectDomLocalNames( + rootNode: SgNode, + reactAliases: Set, +): Set { + const names = new Set(); + const reactDomImport = getImport(rootNode, { type: "named", name: "DOM", from: "react" }); + if (reactDomImport && !reactDomImport.isNamespace) { + names.add(reactDomImport.alias); + } + + for (const pattern of rootNode.findAll({ rule: { kind: "object_pattern" } })) { + if (!isReactDomDestructurePattern(pattern, reactAliases)) continue; + for (const shorthand of pattern.findAll({ + rule: { kind: "shorthand_property_identifier_pattern" }, + })) { + if (shorthand.text() === "DOM") names.add("DOM"); + } + for (const pair of pattern.findAll({ rule: { kind: "pair_pattern" } })) { + if (pair.field("key")?.text() !== "DOM") continue; + const value = pair.field("value"); + if (value && value.kind() === "identifier") names.add(value.text()); + } + } + + return names; } -function isReactDomFactoryMember(callee: SgNode): { factory: string; target: string } | null { +function isReactDomFactoryMember( + callee: SgNode, + domLocalNames: Set, + reactAliases: Set, +): { factory: string; target: string } | null { if (callee.kind() !== "member_expression") { return null; } @@ -63,8 +102,11 @@ function isReactDomFactoryMember(callee: SgNode): { factory: string; target return null; } - if (object.kind() === "identifier" && object.text() === "DOM") { - return { factory: property.text(), target: "createElement" }; + if (object.kind() === "identifier" && domLocalNames.has(object.text())) { + // updateImport renames DOM → createElement and preserves any local alias. + // Destructured `DOM` shorthand is rewritten to `createElement` in place. + const target = object.text() === "DOM" ? "createElement" : object.text(); + return { factory: property.text(), target }; } if (object.kind() !== "member_expression") { @@ -77,14 +119,17 @@ function isReactDomFactoryMember(callee: SgNode): { factory: string; target return null; } - if (outerProperty.text() === "DOM" && isReactIdentifier(outerObject)) { - return { factory: property.text(), target: "React.createElement" }; + if (outerProperty.text() === "DOM" && isReactIdentifier(outerObject, reactAliases)) { + return { factory: property.text(), target: `${outerObject.text()}.createElement` }; } return null; } -function isReactDomDestructurePattern(node: SgNode): boolean { +function isReactDomDestructurePattern( + node: SgNode, + reactAliases: Set, +): boolean { if (node.kind() !== "object_pattern") { return false; } @@ -95,7 +140,7 @@ function isReactDomDestructurePattern(node: SgNode): boolean { } const init = declarator.field("value"); - if (!(isReactIdentifier(init) || isReactRequireCall(init))) { + if (!(isReactIdentifier(init, reactAliases) || isReactRequireCall(init))) { return false; } @@ -105,23 +150,15 @@ function isReactDomDestructurePattern(node: SgNode): boolean { function matchedFactoryCallInfo( call: SgNode, - hasReactDomBinding: boolean, + domLocalNames: Set, + reactAliases: Set, ): { factory: string; target: string } | null { const callee = call.field("function"); if (!callee) { return null; } - const match = isReactDomFactoryMember(callee); - if (!match) { - return null; - } - - if (match.target === "createElement" && !hasReactDomBinding) { - return null; - } - - return match; + return isReactDomFactoryMember(callee, domLocalNames, reactAliases); } function applyInlineEdits(source: string, edits: InlineEdit[]): string { @@ -133,11 +170,17 @@ function applyInlineEdits(source: string, edits: InlineEdit[]): string { ); } -function rewriteNodeText(node: SgNode, hasReactDomBinding: boolean): string { - const callMatch = node.kind() === "call_expression" ? matchedFactoryCallInfo(node, hasReactDomBinding) : null; +function rewriteNodeText( + node: SgNode, + domLocalNames: Set, + reactAliases: Set, +): string { + const callMatch = node.kind() === "call_expression" + ? matchedFactoryCallInfo(node, domLocalNames, reactAliases) + : null; if (callMatch) { const argsText = callArguments(node) - .map((arg) => rewriteNodeText(arg, hasReactDomBinding)) + .map((arg) => rewriteNodeText(arg, domLocalNames, reactAliases)) .join(", "); const prefix = argsText.length > 0 ? ", " : ""; return `${callMatch.target}('${callMatch.factory}'${prefix}${argsText})`; @@ -148,7 +191,7 @@ function rewriteNodeText(node: SgNode, hasReactDomBinding: boolean): string const edits: InlineEdit[] = []; for (const child of namedChildren(node)) { - const rewritten = rewriteNodeText(child, hasReactDomBinding); + const rewritten = rewriteNodeText(child, domLocalNames, reactAliases); if (rewritten === child.text()) { continue; } @@ -169,42 +212,33 @@ const transform: Transform = async (root) => { const metric = useMetricAtom("react-dom-factory-replacements"); const reactDomImport = getImport(rootNode, { type: "named", name: "DOM", from: "react" }); - const hasReactDomPattern = rootNode.findAll({ rule: { kind: "object_pattern" } }) - .some(isReactDomDestructurePattern); - const hasReactDomBinding = reactDomImport !== null || hasReactDomPattern; + const reactAliases = collectReactAliases(rootNode); + const domLocalNames = collectDomLocalNames(rootNode, reactAliases); const matchedCalls = rootNode.findAll({ rule: { kind: "call_expression" } }) - .filter((call) => matchedFactoryCallInfo(call, hasReactDomBinding) !== null); + .filter((call) => matchedFactoryCallInfo(call, domLocalNames, reactAliases) !== null); for (const call of matchedCalls) { const hasMatchedAncestor = call.ancestors().some((ancestor) => ancestor.kind() === "call_expression" && - matchedFactoryCallInfo(ancestor, hasReactDomBinding) !== null + matchedFactoryCallInfo(ancestor, domLocalNames, reactAliases) !== null ); if (hasMatchedAncestor) { continue; } - edits.push(call.replace(rewriteNodeText(call, hasReactDomBinding))); + edits.push(call.replace(rewriteNodeText(call, domLocalNames, reactAliases))); } - for (const importSpec of rootNode.findAll({ rule: { kind: "import_specifier" } })) { - const imported = importSpec.field("name"); - if (!imported || imported.text() !== "DOM") { - continue; - } - - const importStmt = importSpec.ancestors().find((ancestor) => ancestor.kind() === "import_statement"); - if (!importStmt) { - continue; - } - - const source = importStmt.field("source"); - if (!source || source.kind() !== "string" || stringValue(source) !== "react") { - continue; + if (reactDomImport && !reactDomImport.isNamespace) { + const importEdit = updateImport(rootNode, { + type: "named", + from: "react", + specifiers: [{ name: "DOM", to: "createElement" }], + }); + if (importEdit) { + edits.push(importEdit); } - - edits.push(imported.replace("createElement")); } for (const shorthand of rootNode.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } })) { @@ -213,7 +247,7 @@ const transform: Transform = async (root) => { } const pattern = shorthand.ancestors().find((ancestor) => ancestor.kind() === "object_pattern"); - if (!pattern || !isReactDomDestructurePattern(pattern)) { + if (!pattern || !isReactDomDestructurePattern(pattern, reactAliases)) { continue; } diff --git a/codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/expected.tsx new file mode 100644 index 0000000..ddba581 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/expected.tsx @@ -0,0 +1,3 @@ +import { createElement as Factories } from "react"; + +Factories('div', null); diff --git a/codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/input.tsx new file mode 100644 index 0000000..e157b7a --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/input.tsx @@ -0,0 +1,3 @@ +import { DOM as Factories } from "react"; + +Factories.div(null); diff --git a/codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/metrics.json b/codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/metrics.json new file mode 100644 index 0000000..fac2e7d --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/aliased-dom-import/metrics.json @@ -0,0 +1,10 @@ +{ + "react-dom-factory-replacements": [ + { + "cardinality": { + "file": "tests/aliased-dom-import/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/expected.tsx new file mode 100644 index 0000000..06e322e --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/expected.tsx @@ -0,0 +1,3 @@ +import R from "react"; + +R.createElement('div', null); diff --git a/codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/input.tsx new file mode 100644 index 0000000..5c358ab --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/input.tsx @@ -0,0 +1,3 @@ +import R from "react"; + +R.DOM.div(null); diff --git a/codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/metrics.json b/codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/metrics.json new file mode 100644 index 0000000..95187a3 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/aliased-react-default/metrics.json @@ -0,0 +1,10 @@ +{ + "react-dom-factory-replacements": [ + { + "cardinality": { + "file": "tests/aliased-react-default/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/unprefix-stable-apis/package.json b/codemods/unprefix-stable-apis/package.json index 0ad7a92..1bbe8aa 100644 --- a/codemods/unprefix-stable-apis/package.json +++ b/codemods/unprefix-stable-apis/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "@types/node": "latest", "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/unprefix-stable-apis/scripts/codemod.ts b/codemods/unprefix-stable-apis/scripts/codemod.ts index 1551751..b7c6cb9 100644 --- a/codemods/unprefix-stable-apis/scripts/codemod.ts +++ b/codemods/unprefix-stable-apis/scripts/codemod.ts @@ -1,6 +1,8 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import { parse } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { getAllImports, updateImport } from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -26,57 +28,32 @@ function stableName(name: string | undefined): string | null { return Object.prototype.hasOwnProperty.call(RENAMES, name) ? RENAMES[name] ?? null : null; } -function namedChildren(node: SgNode | null): SgNode[] { - if (!node) return []; - return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); -} - -function sourceText(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) return fragment.text(); - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; -} - -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? sourceText(source) : null; -} - -function isReactRequireCall(node: SgNode | null): boolean { - if (!node || node.kind() !== "call_expression") return false; - const fn = node.field("function"); - if (!fn || fn.kind() !== "identifier" || fn.text() !== "require") return false; - const args = namedChildren(node.field("arguments")); - const first = args[0]; - return args.length === 1 && !!first && first.kind() === "string" && sourceText(first) === REACT_MODULE; -} - function isInsideImport(node: SgNode): boolean { return node.ancestors().some((ancestor) => ancestor.kind() === "import_statement"); } +function isInsideReactRequireDestructure(node: SgNode): boolean { + return node.ancestors().some((ancestor) => { + if (ancestor.kind() !== "variable_declarator") return false; + const name = ancestor.field("name"); + if (!name || name.kind() !== "object_pattern") return false; + const init = ancestor.field("value"); + if (!init || init.kind() !== "call_expression") return false; + const fn = init.field("function"); + if (!fn || fn.kind() !== "identifier" || fn.text() !== "require") return false; + const args = init.field("arguments"); + const firstString = args?.find({ rule: { kind: "string" } }); + const fragment = firstString?.find({ rule: { kind: "string_fragment" } }); + return fragment?.text() === REACT_MODULE; + }); +} + /** Local names that refer to the React module object (default import, namespace import, require binding). */ function findReactNamespaceBindings(rootNode: SgNode): Set { const names = new Set(); - - for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(imp) !== REACT_MODULE) continue; - const importClause = imp.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); - if (defaultIdentifier) names.add(defaultIdentifier.text()); - const namespaceImport = imp.find({ rule: { kind: "namespace_import" } }); - const namespaceName = namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }); - if (namespaceName) names.add(namespaceName.text()); + for (const binding of getAllImports(rootNode, { type: "default", from: REACT_MODULE })) { + names.add(binding.alias); } - - for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - const name = declarator.field("name"); - if (name && name.kind() === "identifier" && isReactRequireCall(declarator.field("value"))) { - names.add(name.text()); - } - } - return names; } @@ -99,83 +76,96 @@ function isNameTaken(rootNode: SgNode, name: string, ignore: SgN }); } -const transform: Transform = async (root) => { - const rootNode = root.root(); - const edits: Edit[] = []; - const metric = useMetricAtom("unprefix-stable-apis-renames"); - let renames = 0; +type NamedRename = { name: string; to: string; alias?: string }; - // Old local name -> new local name, for bindings whose local identifier changes. +function collectNamedRenames(rootNode: SgNode): { + specifiers: NamedRename[]; + localRenames: Map; + renames: number; + handledStarts: Set; +} { + const specifiers: NamedRename[] = []; const localRenames = new Map(); - // Ranges of nodes we already rewrote, so the usage-site pass skips them. const handledStarts = new Set(); - - // 1. ESM named imports from "react". - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(importNode) !== REACT_MODULE) continue; - - for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { - const nameNode = specifier.field("name"); - const newName = stableName(nameNode?.text()); - if (!nameNode || !newName) continue; - const oldName = nameNode.text(); - const alias = specifier.field("alias"); - - if (alias) { - edits.push(specifier.replace(alias.text() === newName ? newName : `${newName} as ${alias.text()}`)); - } else if (isNameTaken(rootNode, newName, specifier)) { - edits.push(specifier.replace(`${newName} as ${oldName}`)); + let renames = 0; + const seen = new Set(); + + for (const [oldName, newName] of Object.entries(RENAMES)) { + for (const binding of getAllImports(rootNode, { + type: "named", + name: oldName, + from: REACT_MODULE, + })) { + // Named queries fall back to namespace imports when no specifier matches. + if (binding.isNamespace) continue; + + const key = `${binding.node.range().start.index}:${oldName}`; + if (seen.has(key)) continue; + seen.add(key); + + handledStarts.add(binding.node.range().start.index); + const parent = binding.node.parent(); + if (parent) handledStarts.add(parent.range().start.index); + + const hasExplicitAlias = binding.alias !== oldName; + if (hasExplicitAlias) { + specifiers.push({ name: oldName, to: newName }); + } else if (isNameTaken(rootNode, newName, binding.node)) { + specifiers.push({ name: oldName, to: newName, alias: oldName }); } else { - edits.push(specifier.replace(newName)); + specifiers.push({ name: oldName, to: newName }); localRenames.set(oldName, newName); } - handledStarts.add(specifier.range().start.index); - renames++; + renames += 1; } } - // 2. CommonJS destructuring: const { unstable_ViewTransition } = require("react"). - for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - const pattern = declarator.field("name"); - if (!pattern || pattern.kind() !== "object_pattern" || !isReactRequireCall(declarator.field("value"))) continue; - - for (const entry of namedChildren(pattern)) { - if (entry.kind() === "pair_pattern") { - const key = entry.field("key"); - const newName = stableName(key?.text()); - if (!key || !newName) continue; - const value = entry.field("value"); - if (value && value.kind() === "identifier" && value.text() === newName) { - // { unstable_ViewTransition: ViewTransition } -> { ViewTransition } - edits.push(entry.replace(newName)); - } else { - edits.push(key.replace(newName)); - } - handledStarts.add(entry.range().start.index); - renames++; - } else if (entry.kind() === "shorthand_property_identifier_pattern") { - const oldName = entry.text(); - const newName = stableName(oldName); - if (!newName) continue; - if (isNameTaken(rootNode, newName, entry)) { - edits.push(entry.replace(`${newName}: ${oldName}`)); - } else { - edits.push(entry.replace(newName)); - localRenames.set(oldName, newName); - } - handledStarts.add(entry.range().start.index); - renames++; - } - } + return { specifiers, localRenames, renames, handledStarts }; +} + +function applyNamedImportRenames(source: string, specifiers: NamedRename[]): string { + if (specifiers.length === 0) return source; + + let current = source; + let program = parse("tsx", current).root(); + + // updateImport rewrites one clause per call; loop until every clause is updated. + while (true) { + const edit = updateImport(program, { + type: "named", + from: REACT_MODULE, + specifiers, + }); + if (!edit) break; + current = program.commitEdits([edit]); + program = parse("tsx", current).root(); } - // 3. Member access through the module object: React.unstable_ViewTransition, . + return current; +} + +const transform: Transform = async (root) => { + const rootNode = root.root(); + const edits: Edit[] = []; + const metric = useMetricAtom("unprefix-stable-apis-renames"); + + const { + specifiers, + localRenames, + renames: importRenames, + handledStarts, + } = collectNamedRenames(rootNode); + let renames = importRenames; + + // Member access through the module object: React.unstable_ViewTransition, . const namespaceBindings = findReactNamespaceBindings(rootNode); if (namespaceBindings.size > 0) { for (const member of rootNode.findAll({ rule: { kind: "member_expression" } })) { const object = member.field("object"); const property = member.field("property"); - if (!object || !property || object.kind() !== "identifier" || !namespaceBindings.has(object.text())) continue; + if (!object || !property || object.kind() !== "identifier" || !namespaceBindings.has(object.text())) { + continue; + } const newName = stableName(property.text()); if (!newName) continue; edits.push(property.replace(newName)); @@ -183,7 +173,7 @@ const transform: Transform = async (root) => { } } - // 4. Usage sites of bindings whose local name changed. + // Usage sites of bindings whose local name changed. for (const [oldName, newName] of localRenames) { const usages = rootNode.findAll({ rule: { @@ -197,7 +187,10 @@ const transform: Transform = async (root) => { for (const usage of usages) { if (isInsideImport(usage)) continue; - if (usage.ancestors().some((ancestor) => handledStarts.has(ancestor.range().start.index))) continue; + if (isInsideReactRequireDestructure(usage)) continue; + if (usage.ancestors().some((ancestor) => handledStarts.has(ancestor.range().start.index))) { + continue; + } if (handledStarts.has(usage.range().start.index)) continue; const parent = usage.parent(); @@ -221,10 +214,15 @@ const transform: Transform = async (root) => { } } - if (edits.length === 0) return null; + if (edits.length === 0 && specifiers.length === 0) return null; + + let source = edits.length > 0 ? rootNode.commitEdits(edits) : rootNode.text(); + source = applyNamedImportRenames(source, specifiers); + + if (source === rootNode.text()) return null; metric.increment({ file: metricFile(root.filename()) }, renames); - return rootNode.commitEdits(edits); + return source; }; export default transform; diff --git a/codemods/unprefix-stable-apis/tests/type-only-named-import/expected.tsx b/codemods/unprefix-stable-apis/tests/type-only-named-import/expected.tsx new file mode 100644 index 0000000..e9ebe4a --- /dev/null +++ b/codemods/unprefix-stable-apis/tests/type-only-named-import/expected.tsx @@ -0,0 +1,3 @@ +import { type Activity } from "react"; + +export type Panel = typeof Activity; diff --git a/codemods/unprefix-stable-apis/tests/type-only-named-import/input.tsx b/codemods/unprefix-stable-apis/tests/type-only-named-import/input.tsx new file mode 100644 index 0000000..d1a025a --- /dev/null +++ b/codemods/unprefix-stable-apis/tests/type-only-named-import/input.tsx @@ -0,0 +1,3 @@ +import { type unstable_Activity } from "react"; + +export type Panel = typeof unstable_Activity; diff --git a/codemods/unprefix-stable-apis/tests/type-only-named-import/metrics.json b/codemods/unprefix-stable-apis/tests/type-only-named-import/metrics.json new file mode 100644 index 0000000..772166b --- /dev/null +++ b/codemods/unprefix-stable-apis/tests/type-only-named-import/metrics.json @@ -0,0 +1,10 @@ +{ + "unprefix-stable-apis-renames": [ + { + "cardinality": { + "file": "tests/type-only-named-import/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/use-context-hook/package.json b/codemods/use-context-hook/package.json index d8a357e..235ffe0 100644 --- a/codemods/use-context-hook/package.json +++ b/codemods/use-context-hook/package.json @@ -13,6 +13,6 @@ "@types/node": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/use-context-hook/scripts/codemod.ts b/codemods/use-context-hook/scripts/codemod.ts index 8e1e7d8..d60620b 100644 --- a/codemods/use-context-hook/scripts/codemod.ts +++ b/codemods/use-context-hook/scripts/codemod.ts @@ -1,7 +1,8 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import { parse } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport } from "@jssg/utils/javascript/imports"; +import { getAllImports, updateImport } from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -12,59 +13,30 @@ function escapeRegex(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -type NamedUseContextImport = { specifier: SgNode; localName: string }; - -function sourceText(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) return fragment.text(); - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; -} - -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? sourceText(source) : null; -} - function findReactMemberImportNames(rootNode: SgNode): Set { const names = new Set(); - const reactBinding = getImport(rootNode, { type: "default", from: "react" }); - if (reactBinding) { - names.add(reactBinding.alias); - } - - for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(imp) !== "react") continue; - - const importClause = imp.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); - if (defaultIdentifier) names.add(defaultIdentifier.text()); - - const namespaceImport = imp.find({ rule: { kind: "namespace_import" } }); - const namespaceName = - namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }); - if (namespaceName) names.add(namespaceName.text()); + for (const binding of getAllImports(rootNode, { type: "default", from: "react" })) { + names.add(binding.alias); } - return names; } -function findNamedUseContextImports(rootNode: SgNode): NamedUseContextImport[] { - const imports: NamedUseContextImport[] = []; - - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(importNode) !== "react") continue; +function applyUseContextImportRenames(source: string): string { + let current = source; + let program = parse("tsx", current).root(); - for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { - if (specifier.field("name")?.text() !== "useContext") continue; - imports.push({ - specifier, - localName: specifier.field("alias")?.text() ?? "useContext", - }); - } + while (true) { + const edit = updateImport(program, { + type: "named", + from: "react", + specifiers: [{ name: "useContext", to: "use" }], + }); + if (!edit) break; + current = program.commitEdits([edit]); + program = parse("tsx", current).root(); } - return imports; + return current; } const transform: Transform = async (root) => { @@ -74,7 +46,11 @@ const transform: Transform = async (root) => { const transformMetric = useMetricAtom("use-context-transformations"); const reactMemberImportNames = findReactMemberImportNames(rootNode); - const namedUseContextImports = findNamedUseContextImports(rootNode); + const namedUseContextImports = getAllImports(rootNode, { + type: "named", + name: "useContext", + from: "react", + }); const shouldSkipLegacyIdentifierRename = (node: SgNode): boolean => { const parent = node.parent(); @@ -136,7 +112,7 @@ const transform: Transform = async (root) => { } const activeLocalNames = new Map(); - for (const localName of new Set(namedUseContextImports.map((entry) => entry.localName))) { + for (const localName of new Set(namedUseContextImports.map((entry) => entry.alias))) { const useContextCalls = rootNode.findAll({ rule: { kind: "call_expression", @@ -169,12 +145,6 @@ const transform: Transform = async (root) => { } } - for (const entry of namedUseContextImports) { - if (!activeLocalNames.has(entry.localName)) continue; - const replacementText = entry.localName === "useContext" ? "use" : `use as ${entry.localName}`; - edits.push(entry.specifier.replace(replacementText)); - } - const unaliasedCallCount = activeLocalNames.get("useContext") ?? 0; if (unaliasedCallCount > 0) { transformMetric.increment({ @@ -196,11 +166,17 @@ const transform: Transform = async (root) => { }, aliasedCallCount); } - if (edits.length === 0) { + const needsImportRename = activeLocalNames.size > 0; + if (edits.length === 0 && !needsImportRename) { return null; } - return rootNode.commitEdits(edits); + let source = edits.length > 0 ? rootNode.commitEdits(edits) : rootNode.text(); + if (needsImportRename) { + source = applyUseContextImportRenames(source); + } + + return source === rootNode.text() ? null : source; }; export default transform; diff --git a/codemods/use-context-hook/tests/cjs-named-use-context/expected.tsx b/codemods/use-context-hook/tests/cjs-named-use-context/expected.tsx new file mode 100644 index 0000000..f508cb8 --- /dev/null +++ b/codemods/use-context-hook/tests/cjs-named-use-context/expected.tsx @@ -0,0 +1,4 @@ +const { use } = require("react"); +const ThemeContext = require("./ThemeContext"); + +const theme = use(ThemeContext); diff --git a/codemods/use-context-hook/tests/cjs-named-use-context/input.tsx b/codemods/use-context-hook/tests/cjs-named-use-context/input.tsx new file mode 100644 index 0000000..3ea8b38 --- /dev/null +++ b/codemods/use-context-hook/tests/cjs-named-use-context/input.tsx @@ -0,0 +1,4 @@ +const { useContext } = require("react"); +const ThemeContext = require("./ThemeContext"); + +const theme = useContext(ThemeContext); diff --git a/codemods/use-context-hook/tests/cjs-named-use-context/metrics.json b/codemods/use-context-hook/tests/cjs-named-use-context/metrics.json new file mode 100644 index 0000000..088f551 --- /dev/null +++ b/codemods/use-context-hook/tests/cjs-named-use-context/metrics.json @@ -0,0 +1,11 @@ +{ + "use-context-transformations": [ + { + "cardinality": { + "file": "tests/cjs-named-use-context/input.tsx", + "pattern": "useContext" + }, + "count": 1 + } + ] +} \ No newline at end of file From 4c5a61b98007045ed6ffe0649abe47cfd5f44678 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Oct 2026 21:27:11 +0000 Subject: [PATCH 02/10] Use @jssg/utils import helpers in four React JSSG codemods Refactor replace-act-import, replace-use-form-state, replace-reactdom-render, and prop-types-typescript to rely on getImport/getAllImports/addImport/ removeImport instead of ad-hoc import rewriting, and add fixtures that fail under quote-blind or line-regex handling but pass with utils. Co-authored-by: Alex Bit --- codemods/prop-types-typescript/package.json | 3 + .../prop-types-typescript/scripts/codemod.ts | 134 +++++- .../scripts/parity-cases.mjs | 28 ++ codemods/replace-act-import/package.json | 2 +- .../replace-act-import/scripts/codemod.ts | 382 ++++++++---------- .../tests/cjs-default-require/expected.tsx | 2 +- .../tests/cjs-named-require/expected.tsx | 2 + .../tests/cjs-named-require/input.tsx | 2 + .../tests/cjs-named-require/metrics.json | 11 + .../tests/default-import/expected.tsx | 2 +- .../merge-into-react-import/expected.tsx | 2 + .../tests/merge-into-react-import/input.tsx | 3 + .../merge-into-react-import/metrics.json | 11 + .../expected.tsx | 3 +- .../tests/multiple-named-imports/expected.tsx | 4 +- .../tests/named-import-2/expected.tsx | 3 +- .../tests/named-import/expected.tsx | 2 +- .../tests/single-quote-named/expected.tsx | 2 + .../tests/single-quote-named/input.tsx | 2 + .../tests/single-quote-named/metrics.json | 11 + .../tests/wildcard-import/expected.tsx | 2 +- codemods/replace-reactdom-render/package.json | 2 +- .../scripts/codemod.ts | 83 ++-- .../tests/cjs-react-dom-render/expected.tsx | 5 + .../tests/cjs-react-dom-render/input.tsx | 3 + .../tests/cjs-react-dom-render/metrics.json | 11 + .../tests/default/expected.tsx | 2 +- .../tests/multiline-jsx/expected.tsx | 2 +- .../expected.tsx | 2 +- .../expected.tsx | 2 +- .../nested-top-level-expression/expected.tsx | 2 +- .../tests/nested/expected.tsx | 2 +- .../tests/render-and-unmount/expected.tsx | 2 +- .../tests/render-with-callback/expected.tsx | 2 +- .../expected.tsx | 6 + .../single-quote-create-root-import/input.tsx | 4 + .../metrics.json | 11 + .../tests/unmount-member/expected.tsx | 2 +- .../tests/unmount-named/expected.tsx | 2 +- codemods/replace-use-form-state/package.json | 2 +- .../replace-use-form-state/scripts/codemod.ts | 370 +++++++++-------- .../tests/cjs-named/expected.tsx | 2 + .../tests/cjs-named/input.tsx | 2 + .../tests/cjs-named/metrics.json | 11 + .../tests/default-import/expected.tsx | 2 +- .../merge-into-existing-react/expected.tsx | 2 + .../tests/merge-into-existing-react/input.tsx | 3 + .../merge-into-existing-react/metrics.json | 11 + .../expected.tsx | 3 +- .../tests/named-import-2/expected.tsx | 2 +- .../tests/named-import-3/expected.tsx | 2 +- .../tests/named-import/expected.tsx | 2 +- .../tests/wildcard-import/expected.tsx | 2 +- package.json | 2 +- pnpm-lock.yaml | 106 +++-- 55 files changed, 745 insertions(+), 537 deletions(-) create mode 100644 codemods/replace-act-import/tests/cjs-named-require/expected.tsx create mode 100644 codemods/replace-act-import/tests/cjs-named-require/input.tsx create mode 100644 codemods/replace-act-import/tests/cjs-named-require/metrics.json create mode 100644 codemods/replace-act-import/tests/merge-into-react-import/expected.tsx create mode 100644 codemods/replace-act-import/tests/merge-into-react-import/input.tsx create mode 100644 codemods/replace-act-import/tests/merge-into-react-import/metrics.json create mode 100644 codemods/replace-act-import/tests/single-quote-named/expected.tsx create mode 100644 codemods/replace-act-import/tests/single-quote-named/input.tsx create mode 100644 codemods/replace-act-import/tests/single-quote-named/metrics.json create mode 100644 codemods/replace-reactdom-render/tests/cjs-react-dom-render/expected.tsx create mode 100644 codemods/replace-reactdom-render/tests/cjs-react-dom-render/input.tsx create mode 100644 codemods/replace-reactdom-render/tests/cjs-react-dom-render/metrics.json create mode 100644 codemods/replace-reactdom-render/tests/single-quote-create-root-import/expected.tsx create mode 100644 codemods/replace-reactdom-render/tests/single-quote-create-root-import/input.tsx create mode 100644 codemods/replace-reactdom-render/tests/single-quote-create-root-import/metrics.json create mode 100644 codemods/replace-use-form-state/tests/cjs-named/expected.tsx create mode 100644 codemods/replace-use-form-state/tests/cjs-named/input.tsx create mode 100644 codemods/replace-use-form-state/tests/cjs-named/metrics.json create mode 100644 codemods/replace-use-form-state/tests/merge-into-existing-react/expected.tsx create mode 100644 codemods/replace-use-form-state/tests/merge-into-existing-react/input.tsx create mode 100644 codemods/replace-use-form-state/tests/merge-into-existing-react/metrics.json diff --git a/codemods/prop-types-typescript/package.json b/codemods/prop-types-typescript/package.json index 9c09fbf..8785ef8 100644 --- a/codemods/prop-types-typescript/package.json +++ b/codemods/prop-types-typescript/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "@types/node": "latest", "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/prop-types-typescript/scripts/codemod.ts b/codemods/prop-types-typescript/scripts/codemod.ts index 0c065d5..1e024aa 100644 --- a/codemods/prop-types-typescript/scripts/codemod.ts +++ b/codemods/prop-types-typescript/scripts/codemod.ts @@ -1,6 +1,8 @@ import type { Edit, SgNode, Transform } from "codemod:ast-grep"; +import { parse } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { getAllImports, getImport, removeImport } from "@jssg/utils/javascript/imports"; type PreserveMode = "none" | "unconverted" | "all"; type RenderedType = @@ -875,25 +877,69 @@ function interfaceInsertionEdit( return { startPos: start, endPos: start, insertedText: `${rendered}${newline}${newline}` }; } -function collectPropTypesImports(rootNode: SgNode): Array<{ localName: string }> { - const imports: Array<{ localName: string }> = []; +type PropTypesImport = + | { kind: "default" | "namespace"; localName: string } + | { kind: "named"; localName: string; exportedName: string }; + +function collectPropTypesImports(rootNode: SgNode): PropTypesImport[] { + const imports: PropTypesImport[] = []; + const seen = new Set(); + + const defaultImp = getImport(rootNode, { type: "default", from: "prop-types" }); + if (defaultImp) { + const kind = defaultImp.isNamespace ? "namespace" : "default"; + const key = `${kind}:${defaultImp.alias}`; + if (!seen.has(key)) { + seen.add(key); + imports.push({ kind, localName: defaultImp.alias }); + } + } + // Discover named bindings from prop-types import statements, then confirm via getImport. for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { if (importSource(importNode) !== "prop-types") { continue; } - const importClause = importNode.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); - if (defaultIdentifier) { - imports.push({ localName: defaultIdentifier.text() }); + for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { + const exportedName = specifier.field("name")?.text(); + if (!exportedName) continue; + + const resolved = getImport(rootNode, { + type: "named", + name: exportedName, + from: "prop-types", + }); + if (!resolved || resolved.isNamespace) continue; + + const key = `named:${exportedName}:${resolved.alias}`; + if (seen.has(key)) continue; + seen.add(key); + imports.push({ + kind: "named", + localName: resolved.alias, + exportedName, + }); } + } - for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { - const localName = specifier.field("alias")?.text() ?? specifier.field("name")?.text(); - if (localName) { - imports.push({ localName }); - } + // CJS / additional aliases: getAllImports for any named exports already discovered. + for (const named of imports.filter((entry): entry is Extract => + entry.kind === "named" + )) { + for (const resolved of getAllImports(rootNode, { + type: "named", + name: named.exportedName, + from: "prop-types", + }).filter((imp) => !imp.isNamespace)) { + const key = `named:${named.exportedName}:${resolved.alias}`; + if (seen.has(key)) continue; + seen.add(key); + imports.push({ + kind: "named", + localName: resolved.alias, + exportedName: named.exportedName, + }); } } @@ -904,26 +950,72 @@ function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -function cleanupPropTypesImports(source: string, imports: Array<{ localName: string }>): string { +function parseProgram(source: string): SgNode { + return parse("tsx", source).root(); +} + +function cleanupPropTypesImports(source: string, imports: PropTypesImport[]): string { if (imports.length === 0) { return source; } - const sourceWithoutImports = source - .split(/\r?\n/) - .filter((line) => !/^\s*import .* from ['"]prop-types['"]\s*;?\s*$/.test(line)) - .join("\n"); - const hasRemainingUsage = imports.some(({ localName }) => - new RegExp(`\\b${escapeRegExp(localName)}\\s*\\.`, "m").test(sourceWithoutImports) + new RegExp(`\\b${escapeRegExp(localName)}\\s*\\.`, "m").test(source) ); if (hasRemainingUsage) { return source; } - return source - .replace(/^\s*import .* from ['"]prop-types['"]\s*;?\r?\n?/gm, "") - .replace(/^\s*\r?\n/, ""); + let nextSource = source; + + // removeImport only clears one match per call — loop until gone. + while (true) { + const program = parseProgram(nextSource); + const defaultImp = getImport(program, { type: "default", from: "prop-types" }); + if (!defaultImp || defaultImp.isNamespace) break; + const removeEdit = removeImport(program, { type: "default", from: "prop-types" }); + if (!removeEdit) break; + nextSource = program.commitEdits([removeEdit]); + } + + while (true) { + const program = parseProgram(nextSource); + const namespaceImp = getImport(program, { type: "default", from: "prop-types" }); + if (!namespaceImp?.isNamespace) break; + const removeEdit = removeImport(program, { type: "namespace", from: "prop-types" }); + if (!removeEdit) break; + nextSource = program.commitEdits([removeEdit]); + } + + const namedExports = [ + ...new Set( + imports + .filter((entry): entry is Extract => entry.kind === "named") + .map((entry) => entry.exportedName), + ), + ]; + + for (const exportedName of namedExports) { + while (true) { + const program = parseProgram(nextSource); + const remaining = getAllImports(program, { + type: "named", + name: exportedName, + from: "prop-types", + }).filter((imp) => !imp.isNamespace); + if (remaining.length === 0) break; + + const removeEdit = removeImport(program, { + type: "named", + specifiers: [exportedName], + from: "prop-types", + }); + if (!removeEdit) break; + nextSource = program.commitEdits([removeEdit]); + } + } + + return nextSource.replace(/^\s*\r?\n/, ""); } function preserveModeFromOptions(rawValue: unknown): PreserveMode { diff --git a/codemods/prop-types-typescript/scripts/parity-cases.mjs b/codemods/prop-types-typescript/scripts/parity-cases.mjs index 57d8901..deb2caf 100644 --- a/codemods/prop-types-typescript/scripts/parity-cases.mjs +++ b/codemods/prop-types-typescript/scripts/parity-cases.mjs @@ -1147,6 +1147,34 @@ interface MyComponentProps { export function MyComponent(props: MyComponentProps) { return {props.children} } +`, + }, + { + name: "multiline-prop-types-import", + input: `import PropTypes, { + Validator, +} from "prop-types" +import React from "react" + +export const MyComponent = (props) => { + return +} + +MyComponent.propTypes = { + bar: PropTypes.string.isRequired, + foo: PropTypes.number, +} +`, + expected: `import React from "react" + +interface MyComponentProps { + bar: string + foo?: number +} + +export const MyComponent = (props: MyComponentProps) => { + return +} `, }, ]; diff --git a/codemods/replace-act-import/package.json b/codemods/replace-act-import/package.json index baa6f6b..9ef2598 100644 --- a/codemods/replace-act-import/package.json +++ b/codemods/replace-act-import/package.json @@ -13,6 +13,6 @@ "@types/node": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/replace-act-import/scripts/codemod.ts b/codemods/replace-act-import/scripts/codemod.ts index 9579539..305a2fd 100644 --- a/codemods/replace-act-import/scripts/codemod.ts +++ b/codemods/replace-act-import/scripts/codemod.ts @@ -1,7 +1,13 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import { parse } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport, removeImport } from "@jssg/utils/javascript/imports"; +import { + addImport, + getAllImports, + getImport, + removeImport, +} from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -11,9 +17,6 @@ function metricFile(filename: string): string { const TEST_UTILS_MODULE = "react-dom/test-utils"; const REACT_MODULE = "react"; -type TestUtilsImport = { name: string; type: "default" | "namespace"; moduleType: "esm" | "cjs" }; -type NamedActImport = { importNode: SgNode; specifier: SgNode; alias: string | null }; - function sourceText(node: SgNode): string | null { const fragment = node.find({ rule: { kind: "string_fragment" } }); if (fragment) return fragment.text(); @@ -27,61 +30,6 @@ function importSource(node: SgNode): string | null { return source ? sourceText(source) : null; } -function isWhitespace(char: string | undefined): boolean { - return char === " " || char === "\t" || char === "\n" || char === "\r"; -} - -function removeNodeWithComma(node: SgNode, source: string): Edit { - let start = node.range().start.index; - let end = node.range().end.index; - - let i = end; - while (isWhitespace(source[i])) i++; - if (source[i] === ",") { - end = i + 1; - while (isWhitespace(source[end])) end++; - } else { - let j = start - 1; - while (isWhitespace(source[j])) j--; - if (source[j] === ",") start = j; - } - - return { startPos: start, endPos: end, insertedText: "" }; -} - -function namedImportText(alias: string | null): string { - return alias && alias !== "act" ? `act as ${alias}` : "act"; -} - -function countImportSpecifiers(importNode: SgNode): number { - return importNode.findAll({ rule: { kind: "import_specifier" } }).length; -} - -function findExistingReactNamedImport(rootNode: SgNode): SgNode | null { - return rootNode.findAll({ rule: { kind: "import_statement" } }).find((imp) => - importSource(imp) === REACT_MODULE && imp.find({ rule: { kind: "named_imports" } }) !== null - ) ?? null; -} - -function findNamedActImports(rootNode: SgNode): NamedActImport[] { - const imports: NamedActImport[] = []; - - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(importNode) !== TEST_UTILS_MODULE) continue; - - for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { - if (specifier.field("name")?.text() !== "act") continue; - imports.push({ - importNode, - specifier, - alias: specifier.field("alias")?.text() ?? null, - }); - } - } - - return imports; -} - function exportedSpecifierName(specifier: SgNode): string | null { const identifiers = specifier.children().filter((child) => child.isNamed() && child.kind() === "identifier" @@ -103,182 +51,156 @@ function hasActExportFromReact(rootNode: SgNode): boolean { return false; } -function getTestUtilsImport(rootNode: SgNode): (TestUtilsImport & { importNode?: SgNode }) | null { - const defaultImp = getImport(rootNode, { type: "default", from: TEST_UTILS_MODULE }); - if (defaultImp) { - const importNode = defaultImp.node.ancestors().find((a) => - a.kind() === "import_statement" || - a.kind() === "lexical_declaration" || - a.kind() === "variable_declaration" - ); - return { - name: defaultImp.alias, - type: defaultImp.isNamespace ? "namespace" : "default", - moduleType: defaultImp.moduleType, - importNode: importNode ?? undefined, - }; - } +function parseProgram(source: string): SgNode { + return parse("tsx", source).root(); +} + +function actSpecifier(localName: string): { name: string; alias?: string } { + return localName === "act" ? { name: "act" } : { name: "act", alias: localName }; +} + +type Metric = ReturnType; + +function migrateMemberActImports( + initialSource: string, + metric: Metric, + filename: string, +): string | null { + let source = initialSource; + let program = parseProgram(source); + const testUtilsImport = getImport(program, { + type: "default", + from: TEST_UTILS_MODULE, + }); + if (!testUtilsImport) return null; - const importDecls = rootNode.findAll({ + const actMemberCalls = program.findAll({ rule: { - kind: "import_statement", + kind: "member_expression", has: { - all: [ - { kind: "string", regex: "react-dom/test-utils" }, - { kind: "namespace_import" }, - ], + kind: "property_identifier", + regex: "^act$", }, }, }); - for (const imp of importDecls) { - const namespaceImport = imp.find({ rule: { kind: "namespace_import" } }); - if (namespaceImport) { - const ident = namespaceImport.field("name") ?? namespaceImport.find({ rule: { kind: "identifier" } }); - if (ident) return { name: ident.text(), type: "namespace", moduleType: "esm", importNode: imp }; - } + const memberEdits: Edit[] = []; + for (const member of actMemberCalls) { + const objNode = member.field("object"); + if (objNode?.text() !== testUtilsImport.alias) continue; + const reactBinding = getImport(program, { type: "default", from: REACT_MODULE }); + memberEdits.push(objNode.replace(reactBinding?.alias ?? "React")); + metric.increment({ file: metricFile(filename), pattern: "member-call" }); } - return null; -} -const transform: Transform = async (root) => { - const rootNode = root.root(); - const source = rootNode.text(); - const edits: Edit[] = []; - const metric = useMetricAtom("replace-act-import-migrations"); - - const testUtilsImport = getTestUtilsImport(rootNode); - const namedActImports = findNamedActImports(rootNode); - - if (testUtilsImport) { - const actMemberCalls = rootNode.findAll({ - rule: { - kind: "member_expression", - has: { - kind: "property_identifier", - regex: "^act$", - }, - }, - }); - - if (actMemberCalls.length > 0) { - const reactDefault = getImport(rootNode, { type: "default", from: REACT_MODULE }); - const reactNamespace = rootNode.find({ - rule: { - kind: "import_statement", - has: { - all: [ - { kind: "string", regex: "^react$" }, - { kind: "namespace_import" }, - ], + if (memberEdits.length === 0) return null; + + source = program.commitEdits(memberEdits); + program = parseProgram(source); + + const reactBinding = getImport(program, { type: "default", from: REACT_MODULE }); + + // Add React before removing test-utils so insertion is anchored after an + // existing import (avoids commitEdits panics on leading-newline + JSX files). + if (!reactBinding) { + const addEdit = addImport( + program, + testUtilsImport.isNamespace + ? { type: "namespace", name: "React", from: REACT_MODULE } + : { + type: "default", + name: "React", + from: REACT_MODULE, + moduleType: testUtilsImport.moduleType, }, - }, - }); - const nsIdent = reactNamespace?.find({ rule: { kind: "namespace_import" } })?.field("name"); - const reactName = reactDefault?.alias ?? nsIdent?.text() ?? "React"; - - if (testUtilsImport.importNode) { - if (reactDefault || reactNamespace) { - const removeEdit = removeImport(rootNode, { - type: testUtilsImport.type, - from: TEST_UTILS_MODULE, - }); - if (removeEdit) edits.push(removeEdit); - } else { - const newImportText = testUtilsImport.moduleType === "cjs" - ? 'const React = require("react");' - : testUtilsImport.type === "namespace" - ? 'import * as React from "react";' - : 'import React from "react";'; - edits.push(testUtilsImport.importNode.replace(newImportText)); - } - } - - for (const member of actMemberCalls) { - const objNode = member.field("object"); - if (objNode && objNode.text() === testUtilsImport.name) { - edits.push(objNode.replace(reactName)); - metric.increment({ file: metricFile(root.filename()), pattern: "member-call" }); - } - } + ); + if (addEdit) { + source = program.commitEdits([addEdit]); + program = parseProgram(source); } } - if (namedActImports.length > 0) { - const existingReactNamedImport = findExistingReactNamedImport(rootNode); - const existingReactActSpecifiers = new Set(); - if (existingReactNamedImport) { - for (const specifier of existingReactNamedImport.findAll({ rule: { kind: "import_specifier" } })) { - if (specifier.field("name")?.text() !== "act") continue; - existingReactActSpecifiers.add(namedImportText(specifier.field("alias")?.text() ?? null)); - } - } + const removeEdit = removeImport(program, { + type: testUtilsImport.isNamespace ? "namespace" : "default", + from: TEST_UTILS_MODULE, + }); + if (removeEdit) { + source = program.commitEdits([removeEdit]); + } - const wantedActSpecifiers = Array.from( - new Set(namedActImports.map((entry) => namedImportText(entry.alias))), - ); - const specifiersToAdd = wantedActSpecifiers.filter((text) => !existingReactActSpecifiers.has(text)); - - let replacementImportId: number | null = null; - if (existingReactNamedImport) { - if (specifiersToAdd.length > 0) { - const namedImports = existingReactNamedImport.find({ rule: { kind: "named_imports" } }); - if (namedImports) { - const insertPos = namedImports.range().end.index - 1; - edits.push({ - startPos: insertPos, - endPos: insertPos, - insertedText: `, ${specifiersToAdd.join(", ")}`, - }); - } - } - } else if (specifiersToAdd.length > 0) { - const firstNamedActImport = namedActImports[0]!.importNode; - const firstImportActCount = namedActImports.filter((entry) => - entry.importNode.id() === firstNamedActImport.id(), - ).length; - const replacementText = `import { ${specifiersToAdd.join(", ")} } from "react";`; - - if (countImportSpecifiers(firstNamedActImport) === firstImportActCount) { - edits.push(firstNamedActImport.replace(replacementText)); - replacementImportId = firstNamedActImport.id(); - } else { - edits.push({ - startPos: firstNamedActImport.range().start.index, - endPos: firstNamedActImport.range().start.index, - insertedText: `${replacementText}\n`, - }); - } - } + return source; +} - const importsById = new Map; specifiers: SgNode[] }>(); - for (const entry of namedActImports) { - const current = importsById.get(entry.importNode.id()); - if (current) { - current.specifiers.push(entry.specifier); - } else { - importsById.set(entry.importNode.id(), { - importNode: entry.importNode, - specifiers: [entry.specifier], - }); - } - } +function migrateNamedActImports( + initialSource: string, + metric: Metric, + filename: string, +): string | null { + let source = initialSource; + let program = parseProgram(source); + // getAllImports falls back to namespace imports when no named match exists. + const namedActs = getAllImports(program, { + type: "named", + name: "act", + from: TEST_UTILS_MODULE, + }).filter((imp) => !imp.isNamespace); + + if (namedActs.length === 0) { + return null; + } - for (const [importId, group] of importsById) { - if (replacementImportId === importId) continue; + const moduleType = namedActs[0]!.moduleType; + const seenLocals = new Set(); + const specifiers: Array<{ name: string; alias?: string }> = []; + for (const imp of namedActs) { + if (seenLocals.has(imp.alias)) continue; + seenLocals.add(imp.alias); + specifiers.push(actSpecifier(imp.alias)); + } - if (countImportSpecifiers(group.importNode) === group.specifiers.length) { - edits.push(group.importNode.replace("")); - } else { - for (const specifier of group.specifiers) { - edits.push(removeNodeWithComma(specifier, source)); - } - } - } + // addImport skips by exported name, so all aliases of `act` must be added together. + // Add before remove so new imports are anchored after existing statements. + const addEdit = addImport(program, { + type: "named", + specifiers, + from: REACT_MODULE, + moduleType, + }); + if (addEdit) { + source = program.commitEdits([addEdit]); + } - metric.increment({ file: metricFile(root.filename()), pattern: "named-import" }); + // removeImport only removes the first matching statement per call. + while (true) { + program = parseProgram(source); + // Guard: removeImport can throw when only a namespace import remains, + // because getImport falls back to namespaces for missing named specs. + const remaining = getAllImports(program, { + type: "named", + name: "act", + from: TEST_UTILS_MODULE, + }).filter((imp) => !imp.isNamespace); + if (remaining.length === 0) break; + + const removeEdit = removeImport(program, { + type: "named", + specifiers: ["act"], + from: TEST_UTILS_MODULE, + }); + if (!removeEdit) break; + source = program.commitEdits([removeEdit]); } + metric.increment({ file: metricFile(filename), pattern: "named-import" }); + return source; +} + +function migrateReExports( + rootNode: SgNode, + metric: Metric, + filename: string, +): Edit[] { + const edits: Edit[] = []; const exportDecls = rootNode.findAll({ rule: { kind: "export_statement", @@ -301,12 +223,36 @@ const transform: Transform = async (root) => { endPos: insertPos, insertedText: "\nexport { act } from \"react\";", }); - metric.increment({ file: metricFile(root.filename()), pattern: "re-export" }); + metric.increment({ file: metricFile(filename), pattern: "re-export" }); } } + return edits; +} + +const transform: Transform = async (root) => { + const rootNode = root.root(); + const metric = useMetricAtom("replace-act-import-migrations"); + const filename = root.filename(); + + const reExportEdits = migrateReExports(rootNode, metric, filename); + let source = reExportEdits.length > 0 + ? rootNode.commitEdits(reExportEdits) + : rootNode.text(); + let changed = reExportEdits.length > 0; + + const memberResult = migrateMemberActImports(source, metric, filename); + if (memberResult !== null) { + source = memberResult; + changed = true; + } + + const namedResult = migrateNamedActImports(source, metric, filename); + if (namedResult !== null) { + source = namedResult; + changed = true; + } - if (edits.length === 0) return null; - return rootNode.commitEdits(edits); + return changed ? source : null; }; export default transform; diff --git a/codemods/replace-act-import/tests/cjs-default-require/expected.tsx b/codemods/replace-act-import/tests/cjs-default-require/expected.tsx index 39ac3e5..9de872e 100644 --- a/codemods/replace-act-import/tests/cjs-default-require/expected.tsx +++ b/codemods/replace-act-import/tests/cjs-default-require/expected.tsx @@ -1,4 +1,4 @@ -const React = require("react"); +const React = require('react'); React.act(() => { root.render(); diff --git a/codemods/replace-act-import/tests/cjs-named-require/expected.tsx b/codemods/replace-act-import/tests/cjs-named-require/expected.tsx new file mode 100644 index 0000000..6d3a67d --- /dev/null +++ b/codemods/replace-act-import/tests/cjs-named-require/expected.tsx @@ -0,0 +1,2 @@ +const { act } = require('react'); +act(); diff --git a/codemods/replace-act-import/tests/cjs-named-require/input.tsx b/codemods/replace-act-import/tests/cjs-named-require/input.tsx new file mode 100644 index 0000000..c8e7893 --- /dev/null +++ b/codemods/replace-act-import/tests/cjs-named-require/input.tsx @@ -0,0 +1,2 @@ +const { act } = require("react-dom/test-utils"); +act(); diff --git a/codemods/replace-act-import/tests/cjs-named-require/metrics.json b/codemods/replace-act-import/tests/cjs-named-require/metrics.json new file mode 100644 index 0000000..98b4205 --- /dev/null +++ b/codemods/replace-act-import/tests/cjs-named-require/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-act-import-migrations": [ + { + "cardinality": { + "file": "tests/cjs-named-require/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-act-import/tests/default-import/expected.tsx b/codemods/replace-act-import/tests/default-import/expected.tsx index 3e51ec4..29a9453 100644 --- a/codemods/replace-act-import/tests/default-import/expected.tsx +++ b/codemods/replace-act-import/tests/default-import/expected.tsx @@ -1,2 +1,2 @@ -import React from "react"; +import React from 'react'; React.act(); diff --git a/codemods/replace-act-import/tests/merge-into-react-import/expected.tsx b/codemods/replace-act-import/tests/merge-into-react-import/expected.tsx new file mode 100644 index 0000000..fe17b36 --- /dev/null +++ b/codemods/replace-act-import/tests/merge-into-react-import/expected.tsx @@ -0,0 +1,2 @@ +import { useState, act } from 'react' +act() diff --git a/codemods/replace-act-import/tests/merge-into-react-import/input.tsx b/codemods/replace-act-import/tests/merge-into-react-import/input.tsx new file mode 100644 index 0000000..c351b34 --- /dev/null +++ b/codemods/replace-act-import/tests/merge-into-react-import/input.tsx @@ -0,0 +1,3 @@ +import { useState } from 'react' +import { act } from 'react-dom/test-utils' +act() diff --git a/codemods/replace-act-import/tests/merge-into-react-import/metrics.json b/codemods/replace-act-import/tests/merge-into-react-import/metrics.json new file mode 100644 index 0000000..b419357 --- /dev/null +++ b/codemods/replace-act-import/tests/merge-into-react-import/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-act-import-migrations": [ + { + "cardinality": { + "file": "tests/merge-into-react-import/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx b/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx index 7f9f63a..9e84d9e 100644 --- a/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx +++ b/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx @@ -1,6 +1,5 @@ -import { act, act as act2 } from "react"; import { render } from "react-dom/test-utils"; - +import { act, act as act2 } from 'react'; render(); act(); diff --git a/codemods/replace-act-import/tests/multiple-named-imports/expected.tsx b/codemods/replace-act-import/tests/multiple-named-imports/expected.tsx index dec384a..ff3cbaf 100644 --- a/codemods/replace-act-import/tests/multiple-named-imports/expected.tsx +++ b/codemods/replace-act-import/tests/multiple-named-imports/expected.tsx @@ -1,6 +1,4 @@ -import { FC , act, act as act2} from "react"; - - +import { FC, act, act as act2 } from "react"; act(); act2(); diff --git a/codemods/replace-act-import/tests/named-import-2/expected.tsx b/codemods/replace-act-import/tests/named-import-2/expected.tsx index 6a1ffe1..dc7dd53 100644 --- a/codemods/replace-act-import/tests/named-import-2/expected.tsx +++ b/codemods/replace-act-import/tests/named-import-2/expected.tsx @@ -1,4 +1,3 @@ -import { FC , act} from "react"; - +import { FC, act } from "react"; act(); diff --git a/codemods/replace-act-import/tests/named-import/expected.tsx b/codemods/replace-act-import/tests/named-import/expected.tsx index 44ab638..108d5cd 100644 --- a/codemods/replace-act-import/tests/named-import/expected.tsx +++ b/codemods/replace-act-import/tests/named-import/expected.tsx @@ -1,2 +1,2 @@ -import { act } from "react"; +import { act } from 'react'; act(); diff --git a/codemods/replace-act-import/tests/single-quote-named/expected.tsx b/codemods/replace-act-import/tests/single-quote-named/expected.tsx new file mode 100644 index 0000000..3bdfc9c --- /dev/null +++ b/codemods/replace-act-import/tests/single-quote-named/expected.tsx @@ -0,0 +1,2 @@ +import { act } from 'react'; +act() diff --git a/codemods/replace-act-import/tests/single-quote-named/input.tsx b/codemods/replace-act-import/tests/single-quote-named/input.tsx new file mode 100644 index 0000000..25b4c34 --- /dev/null +++ b/codemods/replace-act-import/tests/single-quote-named/input.tsx @@ -0,0 +1,2 @@ +import { act } from 'react-dom/test-utils' +act() diff --git a/codemods/replace-act-import/tests/single-quote-named/metrics.json b/codemods/replace-act-import/tests/single-quote-named/metrics.json new file mode 100644 index 0000000..809a7f9 --- /dev/null +++ b/codemods/replace-act-import/tests/single-quote-named/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-act-import-migrations": [ + { + "cardinality": { + "file": "tests/single-quote-named/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-act-import/tests/wildcard-import/expected.tsx b/codemods/replace-act-import/tests/wildcard-import/expected.tsx index 4acf1aa..35875f9 100644 --- a/codemods/replace-act-import/tests/wildcard-import/expected.tsx +++ b/codemods/replace-act-import/tests/wildcard-import/expected.tsx @@ -1,2 +1,2 @@ -import * as React from "react"; +import * as React from 'react'; React.act(); diff --git a/codemods/replace-reactdom-render/package.json b/codemods/replace-reactdom-render/package.json index 77e5f31..aa36ea3 100644 --- a/codemods/replace-reactdom-render/package.json +++ b/codemods/replace-reactdom-render/package.json @@ -13,6 +13,6 @@ "@types/node": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/replace-reactdom-render/scripts/codemod.ts b/codemods/replace-reactdom-render/scripts/codemod.ts index 636a820..cea5224 100644 --- a/codemods/replace-reactdom-render/scripts/codemod.ts +++ b/codemods/replace-reactdom-render/scripts/codemod.ts @@ -1,7 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport } from "@jssg/utils/javascript/imports"; +import { addImport, getAllImports, getImport } from "@jssg/utils/javascript/imports"; function reindentText(text: string, fromCol: number, toCol: number): string { if (fromCol === toCol) return text; @@ -35,16 +35,15 @@ function importSource(node: SgNode): string | null { } function findReactDomMemberImportNames(rootNode: SgNode): Set { - const names = new Set(); + const names = new Set( + getAllImports(rootNode, { type: "default", from: "react-dom" }).map((imp) => imp.alias), + ); - for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(imp) !== "react-dom") continue; - - const importClause = imp.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); - if (defaultIdentifier) names.add(defaultIdentifier.text()); - - const namespaceImport = imp.find({ rule: { kind: "namespace_import" } }); + // getAllImports(type: "default") omits namespace imports when a default binding + // from the same module also exists — recover those aliases. + for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (importSource(importNode) !== "react-dom") continue; + const namespaceImport = importNode.find({ rule: { kind: "namespace_import" } }); const namespaceName = namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }); if (namespaceName) names.add(namespaceName.text()); @@ -53,40 +52,6 @@ function findReactDomMemberImportNames(rootNode: SgNode): Set, exportedName: string): Set { - const names = new Set(); - - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(importNode) !== "react-dom") continue; - - for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { - if (specifier.field("name")?.text() !== exportedName) continue; - names.add(specifier.field("alias")?.text() ?? exportedName); - } - } - - return names; -} - -function lineStartIndex(source: string, index: number): number { - let cursor = index; - while (cursor > 0 && source[cursor - 1] !== "\n" && source[cursor - 1] !== "\r") { - cursor--; - } - return cursor; -} - -function nearestAncestorOfKind(node: SgNode, kind: string): SgNode | null { - let current = node.parent(); - while (current) { - if (current.kind() === kind) { - return current; - } - current = current.parent(); - } - return null; -} - function directExpressionStatement(node: SgNode): SgNode | null { const parent = node.parent(); return parent?.kind() === "expression_statement" ? parent : null; @@ -98,14 +63,28 @@ const transform: Transform = async (root) => { const transformMetric = useMetricAtom("reactdom-render-replacements"); + const reactDomMemberImports = getAllImports(rootNode, { type: "default", from: "react-dom" }); const reactDomMemberImportNames = findReactDomMemberImportNames(rootNode); - const reactDomRenderImportNames = findNamedImportNames(rootNode, "render"); - const reactDomUnmountImportNames = findNamedImportNames(rootNode, "unmountComponentAtNode"); + const reactDomRenderImports = getAllImports(rootNode, { type: "named", name: "render", from: "react-dom" }) + .filter((imp) => !imp.isNamespace); + const reactDomRenderImportNames = new Set(reactDomRenderImports.map((imp) => imp.alias)); + const reactDomUnmountImports = getAllImports(rootNode, { + type: "named", + name: "unmountComponentAtNode", + from: "react-dom", + }).filter((imp) => !imp.isNamespace); + const reactDomUnmountImportNames = new Set(reactDomUnmountImports.map((imp) => imp.alias)); if (reactDomMemberImportNames.size === 0 && reactDomRenderImportNames.size === 0 && reactDomUnmountImportNames.size === 0) { return null; } + const createRootModuleType = + [...reactDomMemberImports, ...reactDomRenderImports, ...reactDomUnmountImports] + .some((imp) => imp.moduleType === "esm") + ? "esm" + : "cjs"; + let hasTransformations = false; let rootIndex = 0; @@ -325,11 +304,15 @@ const transform: Transform = async (root) => { from: "react-dom/client", }); if (!hasCreateRootImport) { - edits.push({ - startPos: 0, - endPos: 0, - insertedText: 'import { createRoot } from "react-dom/client";\n', + const addEdit = addImport(rootNode, { + type: "named", + specifiers: [{ name: "createRoot" }], + from: "react-dom/client", + moduleType: createRootModuleType, }); + if (addEdit) { + edits.push(addEdit); + } } return rootNode.commitEdits(edits); diff --git a/codemods/replace-reactdom-render/tests/cjs-react-dom-render/expected.tsx b/codemods/replace-reactdom-render/tests/cjs-react-dom-render/expected.tsx new file mode 100644 index 0000000..e8b9b06 --- /dev/null +++ b/codemods/replace-reactdom-render/tests/cjs-react-dom-render/expected.tsx @@ -0,0 +1,5 @@ +const ReactDOM = require("react-dom"); +const { createRoot } = require('react-dom/client'); + +const root = createRoot(node); +root.render(); diff --git a/codemods/replace-reactdom-render/tests/cjs-react-dom-render/input.tsx b/codemods/replace-reactdom-render/tests/cjs-react-dom-render/input.tsx new file mode 100644 index 0000000..4c7768c --- /dev/null +++ b/codemods/replace-reactdom-render/tests/cjs-react-dom-render/input.tsx @@ -0,0 +1,3 @@ +const ReactDOM = require("react-dom"); + +ReactDOM.render(, node); diff --git a/codemods/replace-reactdom-render/tests/cjs-react-dom-render/metrics.json b/codemods/replace-reactdom-render/tests/cjs-react-dom-render/metrics.json new file mode 100644 index 0000000..dddbc58 --- /dev/null +++ b/codemods/replace-reactdom-render/tests/cjs-react-dom-render/metrics.json @@ -0,0 +1,11 @@ +{ + "reactdom-render-replacements": [ + { + "cardinality": { + "file": "tests/cjs-react-dom-render/input.tsx", + "pattern": "ReactDOM.render" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-reactdom-render/tests/default/expected.tsx b/codemods/replace-reactdom-render/tests/default/expected.tsx index 7a3e571..5297b7b 100644 --- a/codemods/replace-reactdom-render/tests/default/expected.tsx +++ b/codemods/replace-reactdom-render/tests/default/expected.tsx @@ -1,6 +1,6 @@ -import { createRoot } from "react-dom/client"; import ReactDom from "react-dom"; import Component from "Component"; +import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById("app")); root.render(); diff --git a/codemods/replace-reactdom-render/tests/multiline-jsx/expected.tsx b/codemods/replace-reactdom-render/tests/multiline-jsx/expected.tsx index f0f2aa4..a4710b3 100644 --- a/codemods/replace-reactdom-render/tests/multiline-jsx/expected.tsx +++ b/codemods/replace-reactdom-render/tests/multiline-jsx/expected.tsx @@ -1,5 +1,5 @@ -import { createRoot } from "react-dom/client"; import * as ReactDOM from "react-dom"; +import { createRoot } from 'react-dom/client'; function show(container) { const root = createRoot(container); diff --git a/codemods/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx b/codemods/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx index b9416e9..4b1a7bb 100644 --- a/codemods/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx +++ b/codemods/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx @@ -1,6 +1,6 @@ -import { createRoot } from "react-dom/client"; import ReactDOM from "react-dom"; import * as DOM from "react-dom"; +import { createRoot } from 'react-dom/client'; const root = createRoot(node1); root.render(); diff --git a/codemods/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx b/codemods/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx index 4a7b7a9..3297793 100644 --- a/codemods/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx +++ b/codemods/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx @@ -1,6 +1,6 @@ -import { createRoot } from "react-dom/client"; import { render } from "react-dom"; import { render as render2 } from "react-dom"; +import { createRoot } from 'react-dom/client'; const root = createRoot(node1); root.render(); diff --git a/codemods/replace-reactdom-render/tests/nested-top-level-expression/expected.tsx b/codemods/replace-reactdom-render/tests/nested-top-level-expression/expected.tsx index 586c3ec..e9a8409 100644 --- a/codemods/replace-reactdom-render/tests/nested-top-level-expression/expected.tsx +++ b/codemods/replace-reactdom-render/tests/nested-top-level-expression/expected.tsx @@ -1,5 +1,5 @@ -import { createRoot } from "react-dom/client"; import { render } from "react-dom"; +import { createRoot } from 'react-dom/client'; describe("suite", () => { const run = () => { diff --git a/codemods/replace-reactdom-render/tests/nested/expected.tsx b/codemods/replace-reactdom-render/tests/nested/expected.tsx index 4f659df..9cc555a 100644 --- a/codemods/replace-reactdom-render/tests/nested/expected.tsx +++ b/codemods/replace-reactdom-render/tests/nested/expected.tsx @@ -1,5 +1,5 @@ -import { createRoot } from "react-dom/client"; import { render } from "react-dom"; +import { createRoot } from 'react-dom/client'; const fn = () => { if (true) { diff --git a/codemods/replace-reactdom-render/tests/render-and-unmount/expected.tsx b/codemods/replace-reactdom-render/tests/render-and-unmount/expected.tsx index a1cde91..65656ad 100644 --- a/codemods/replace-reactdom-render/tests/render-and-unmount/expected.tsx +++ b/codemods/replace-reactdom-render/tests/render-and-unmount/expected.tsx @@ -1,5 +1,5 @@ -import { createRoot } from "react-dom/client"; import * as ReactDOM from "react-dom"; +import { createRoot } from 'react-dom/client'; function mount(container) { const root = createRoot(container); diff --git a/codemods/replace-reactdom-render/tests/render-with-callback/expected.tsx b/codemods/replace-reactdom-render/tests/render-with-callback/expected.tsx index 78b9abf..79dac1e 100644 --- a/codemods/replace-reactdom-render/tests/render-with-callback/expected.tsx +++ b/codemods/replace-reactdom-render/tests/render-with-callback/expected.tsx @@ -1,5 +1,5 @@ -import { createRoot } from "react-dom/client"; import ReactDOM from "react-dom"; +import { createRoot } from 'react-dom/client'; function mount(container) { const root = createRoot(container); diff --git a/codemods/replace-reactdom-render/tests/single-quote-create-root-import/expected.tsx b/codemods/replace-reactdom-render/tests/single-quote-create-root-import/expected.tsx new file mode 100644 index 0000000..e415998 --- /dev/null +++ b/codemods/replace-reactdom-render/tests/single-quote-create-root-import/expected.tsx @@ -0,0 +1,6 @@ +import ReactDom from 'react-dom'; +import Component from 'Component'; +import { createRoot } from 'react-dom/client'; + +const root = createRoot(document.getElementById('app')); +root.render(); diff --git a/codemods/replace-reactdom-render/tests/single-quote-create-root-import/input.tsx b/codemods/replace-reactdom-render/tests/single-quote-create-root-import/input.tsx new file mode 100644 index 0000000..c93b141 --- /dev/null +++ b/codemods/replace-reactdom-render/tests/single-quote-create-root-import/input.tsx @@ -0,0 +1,4 @@ +import ReactDom from 'react-dom'; +import Component from 'Component'; + +ReactDom.render(, document.getElementById('app')); diff --git a/codemods/replace-reactdom-render/tests/single-quote-create-root-import/metrics.json b/codemods/replace-reactdom-render/tests/single-quote-create-root-import/metrics.json new file mode 100644 index 0000000..dd7442e --- /dev/null +++ b/codemods/replace-reactdom-render/tests/single-quote-create-root-import/metrics.json @@ -0,0 +1,11 @@ +{ + "reactdom-render-replacements": [ + { + "cardinality": { + "file": "tests/single-quote-create-root-import/input.tsx", + "pattern": "ReactDOM.render" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-reactdom-render/tests/unmount-member/expected.tsx b/codemods/replace-reactdom-render/tests/unmount-member/expected.tsx index b32b914..b8d58df 100644 --- a/codemods/replace-reactdom-render/tests/unmount-member/expected.tsx +++ b/codemods/replace-reactdom-render/tests/unmount-member/expected.tsx @@ -1,5 +1,5 @@ -import { createRoot } from "react-dom/client"; import * as ReactDOM from "react-dom"; +import { createRoot } from 'react-dom/client'; function cleanup(container) { const root = createRoot(container); diff --git a/codemods/replace-reactdom-render/tests/unmount-named/expected.tsx b/codemods/replace-reactdom-render/tests/unmount-named/expected.tsx index 82a0078..3af8979 100644 --- a/codemods/replace-reactdom-render/tests/unmount-named/expected.tsx +++ b/codemods/replace-reactdom-render/tests/unmount-named/expected.tsx @@ -1,5 +1,5 @@ -import { createRoot } from "react-dom/client"; import { unmountComponentAtNode } from "react-dom"; +import { createRoot } from 'react-dom/client'; function cleanup(container) { const root = createRoot(container); diff --git a/codemods/replace-use-form-state/package.json b/codemods/replace-use-form-state/package.json index a6e75e3..ecfa478 100644 --- a/codemods/replace-use-form-state/package.json +++ b/codemods/replace-use-form-state/package.json @@ -13,6 +13,6 @@ "@types/node": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/replace-use-form-state/scripts/codemod.ts b/codemods/replace-use-form-state/scripts/codemod.ts index 2fec32f..199dc3e 100644 --- a/codemods/replace-use-form-state/scripts/codemod.ts +++ b/codemods/replace-use-form-state/scripts/codemod.ts @@ -1,236 +1,228 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import { parse } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { addImport, getImport } from "@jssg/utils/javascript/imports"; +import { + addImport, + getAllImports, + getImport, + removeImport, +} from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; } - const REACT_DOM_MODULE = "react-dom"; +const REACT_MODULE = "react"; + type ModuleType = "esm" | "cjs"; +type Metric = ReturnType; -type NamedUseFormStateImport = { importNode: SgNode; specifier: SgNode; localName: string }; +function parseProgram(source: string): SgNode { + return parse("tsx", source).root(); +} -function sourceText(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) return fragment.text(); - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; +function isImportBindingNode(node: SgNode): boolean { + return node.ancestors().some((ancestor) => { + const kind = ancestor.kind(); + return ( + kind === "import_statement" || + kind === "import_specifier" || + kind === "shorthand_property_identifier_pattern" || + kind === "pair_pattern" + ); + }); } -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? sourceText(source) : null; +function useActionStateSpecifier(localName: string): { name: string; alias?: string } { + return localName === "useFormState" + ? { name: "useActionState" } + : { name: "useActionState", alias: localName }; } -function findReactDOMMemberImports(rootNode: SgNode): Map { - const names = new Map(); - const reactDomBinding = getImport(rootNode, { type: "default", from: REACT_DOM_MODULE }); - if (reactDomBinding) { - names.set(reactDomBinding.alias, reactDomBinding.moduleType); +function migrateMemberUseFormState( + initialSource: string, + metric: Metric, + filename: string, +): { source: string; changed: boolean; moduleType: ModuleType | null } { + let source = initialSource; + let program = parseProgram(source); + const reactDomBindings = getAllImports(program, { + type: "default", + from: REACT_DOM_MODULE, + }); + + if (reactDomBindings.length === 0) { + return { source, changed: false, moduleType: null }; } - for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(imp) !== REACT_DOM_MODULE) continue; + const bindingNames = new Map( + reactDomBindings.map((binding) => [binding.alias, binding.moduleType] as const), + ); + + const memberEdits: Edit[] = []; + let moduleType: ModuleType | null = null; - const importClause = imp.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); - if (defaultIdentifier) names.set(defaultIdentifier.text(), "esm"); + for (const member of program.findAll({ + rule: { + kind: "member_expression", + has: { + kind: "property_identifier", + regex: "^useFormState$", + }, + }, + })) { + const objNode = member.field("object"); + if (!objNode) continue; + const bindingModuleType = bindingNames.get(objNode.text()); + if (!bindingModuleType) continue; + + memberEdits.push(member.replace("useActionState")); + moduleType = bindingModuleType; + metric.increment({ file: metricFile(filename), pattern: "member-call" }); + } - const namespaceImport = imp.find({ rule: { kind: "namespace_import" } }); - const namespaceName = - namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }); - if (namespaceName) names.set(namespaceName.text(), "esm"); + if (memberEdits.length === 0) { + return { source, changed: false, moduleType: null }; } - return names; + source = program.commitEdits(memberEdits); + return { source, changed: true, moduleType }; } -function findNamedUseFormStateImports(rootNode: SgNode): NamedUseFormStateImport[] { - const imports: NamedUseFormStateImport[] = []; +function migrateNamedUseFormState( + initialSource: string, + metric: Metric, + filename: string, +): { source: string; changed: boolean } { + let source = initialSource; + let program = parseProgram(source); + // getAllImports falls back to namespace imports when no named match exists. + const namedImports = getAllImports(program, { + type: "named", + name: "useFormState", + from: REACT_DOM_MODULE, + }).filter((imp) => !imp.isNamespace); + + if (namedImports.length === 0) { + return { source, changed: false }; + } - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(importNode) !== REACT_DOM_MODULE) continue; + const moduleType = namedImports[0]!.moduleType; + const seenLocals = new Set(); + const specifiers: Array<{ name: string; alias?: string }> = []; + let shouldRenameUseFormState = false; - for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { - if (specifier.field("name")?.text() !== "useFormState") continue; - imports.push({ - importNode, - specifier, - localName: specifier.field("alias")?.text() ?? specifier.field("name")?.text() ?? "useFormState", - }); + for (const imp of namedImports) { + if (imp.alias === "useFormState") { + shouldRenameUseFormState = true; } + if (seenLocals.has(imp.alias)) continue; + seenLocals.add(imp.alias); + specifiers.push(useActionStateSpecifier(imp.alias)); + } + + if (shouldRenameUseFormState) { + const renameEdits: Edit[] = []; + for (const node of program.findAll({ + rule: { + any: [ + { kind: "identifier", regex: "^useFormState$" }, + { kind: "type_identifier", regex: "^useFormState$" }, + ], + }, + })) { + // Preserve prior behavior for locals/types; skip import/require bindings. + if (isImportBindingNode(node)) continue; + if (node.ancestors().some((ancestor) => ancestor.kind() === "import_statement")) continue; + renameEdits.push(node.replace("useActionState")); + } + if (renameEdits.length > 0) { + source = program.commitEdits(renameEdits); + } + } + + program = parseProgram(source); + const addEdit = addImport(program, { + type: "named", + specifiers, + from: REACT_MODULE, + moduleType, + }); + if (addEdit) { + source = program.commitEdits([addEdit]); + } + + while (true) { + program = parseProgram(source); + // Guard: removeImport can throw when only a namespace import remains, + // because getImport falls back to namespaces for missing named specs. + const remaining = getAllImports(program, { + type: "named", + name: "useFormState", + from: REACT_DOM_MODULE, + }).filter((imp) => !imp.isNamespace); + if (remaining.length === 0) break; + + const removeEdit = removeImport(program, { + type: "named", + specifiers: ["useFormState"], + from: REACT_DOM_MODULE, + }); + if (!removeEdit) break; + source = program.commitEdits([removeEdit]); } - return imports; + metric.increment({ file: metricFile(filename), pattern: "named-import" }); + return { source, changed: true }; } -const REACT_MODULE = "react"; +function ensureUseActionStateImport( + initialSource: string, + moduleType: ModuleType, +): string { + const program = parseProgram(initialSource); + if (getImport(program, { type: "named", name: "useActionState", from: REACT_MODULE })) { + return initialSource; + } + + const addEdit = addImport(program, { + type: "named", + specifiers: [{ name: "useActionState" }], + from: REACT_MODULE, + moduleType, + }); + return addEdit ? program.commitEdits([addEdit]) : initialSource; +} const transform: Transform = async (root) => { const rootNode = root.root(); - const edits: Edit[] = []; const metric = useMetricAtom("replace-use-form-state-migrations"); + const filename = root.filename(); - const reactDOMMemberImports = findReactDOMMemberImports(rootNode); - const namedUseFormStateImports = findNamedUseFormStateImports(rootNode); - - let needsReactImport = false; - let needsReactImportModuleType: ModuleType = "esm"; + let source = rootNode.text(); + let changed = false; - if (reactDOMMemberImports.size > 0) { - const memberCalls = rootNode.findAll({ - rule: { - kind: "member_expression", - has: { - kind: "property_identifier", - regex: "^useFormState$", - }, - }, - }); - - for (const member of memberCalls) { - const objNode = member.field("object"); - const moduleType = objNode ? reactDOMMemberImports.get(objNode.text()) : undefined; - if (moduleType) { - // Replace the entire member expression (ReactDOM.useFormState) with just useActionState - edits.push(member.replace("useActionState")); - needsReactImport = true; - needsReactImportModuleType = moduleType; - metric.increment({ file: metricFile(root.filename()), pattern: "member-call" }); - } - } + const memberResult = migrateMemberUseFormState(source, metric, filename); + if (memberResult.changed) { + source = memberResult.source; + changed = true; } - if (namedUseFormStateImports.length > 0) { - // Track import statements we've already edited to avoid duplicate edits - const handledImportRanges = new Set(); - - for (const entry of namedUseFormStateImports) { - const rangeKey = `${entry.importNode.range().start.index}-${entry.importNode.range().end.index}`; - if (!handledImportRanges.has(rangeKey)) { - handledImportRanges.add(rangeKey); - - // Count other specifiers in this import (non-useFormState) - const allSpecifiers = entry.importNode.findAll({ rule: { kind: "import_specifier" } }); - const otherSpecifiers = allSpecifiers.filter( - (s) => s.field("name")?.text() !== "useFormState" - ); - const importClause = entry.importNode.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); - - // Detect the quote style from the original import source - const sourceNode = entry.importNode.field("source") ?? entry.importNode.find({ rule: { kind: "string" } }); - const quoteChar = sourceNode?.text().startsWith("'") ? "'" : '"'; - - const reactImportSpecifier = entry.localName !== "useFormState" - ? `useActionState as ${entry.localName}` - : "useActionState"; - - if (otherSpecifiers.length === 0 && !defaultIdentifier) { - // useFormState is the only specifier — replace entire import with react import - edits.push(entry.importNode.replace( - `import { ${reactImportSpecifier} } from ${quoteChar}react${quoteChar};` - )); - } else if (defaultIdentifier && otherSpecifiers.length === 0) { - // Has default import + useFormState only — remove named imports part, add react import - // e.g., import ReactDOM, { useFormState } from 'react-dom' - // → import ReactDOM from 'react-dom'; - // import { useActionState } from 'react'; - const defaultName = defaultIdentifier.text(); - edits.push(entry.importNode.replace( - `import ${defaultName} from ${quoteChar}${REACT_DOM_MODULE}${quoteChar};\nimport { ${reactImportSpecifier} } from ${quoteChar}react${quoteChar};` - )); - } else { - // Other specifiers remain — rebuild import without useFormState, add react import - const otherSpecTexts = otherSpecifiers.map((s) => { - const name = s.field("name")?.text(); - const alias = s.field("alias")?.text(); - return alias && alias !== name ? `${name} as ${alias}` : name; - }); - const prefix = defaultIdentifier ? `${defaultIdentifier.text()}, ` : ""; - edits.push(entry.importNode.replace( - `import ${prefix}{ ${otherSpecTexts.join(", ")} } from ${quoteChar}${REACT_DOM_MODULE}${quoteChar};\nimport { ${reactImportSpecifier} } from ${quoteChar}react${quoteChar};` - )); - } - } - - needsReactImport = true; - - if (entry.localName === "useFormState") { - // Rename all usage sites from useFormState to useActionState - const renameTargets = rootNode.findAll({ - rule: { - any: [ - { kind: "identifier", regex: "^useFormState$" }, - { kind: "type_identifier", regex: "^useFormState$" }, - ], - }, - }); - - for (const node of renameTargets) { - if (node.ancestors().some((ancestor) => ancestor.kind() === "import_statement")) continue; - edits.push(node.replace("useActionState")); - } - } - } - - metric.increment({ file: metricFile(root.filename()), pattern: "named-import" }); + const namedResult = migrateNamedUseFormState(source, metric, filename); + if (namedResult.changed) { + source = namedResult.source; + changed = true; } - if (edits.length === 0) return null; - - // For member access cases (ReactDOM.useFormState), add import { useActionState } from "react" - // Named import cases are already handled above (import replacement includes the react import) - if (needsReactImport && namedUseFormStateImports.length === 0) { - const hasReactImport = getImport(rootNode, { - type: "named", - name: "useActionState", - from: REACT_MODULE, - }); - if (!hasReactImport) { - if (needsReactImportModuleType === "cjs") { - const addImportEdit = addImport(rootNode, { - type: "named", - from: REACT_MODULE, - specifiers: [{ name: "useActionState" }], - moduleType: "cjs", - }); - if (addImportEdit) { - const nextChar = rootNode.text()[addImportEdit.startPos]; - edits.push({ - ...addImportEdit, - insertedText: nextChar === "\n" || nextChar === "\r" - ? `\n${addImportEdit.insertedText.trimEnd()}` - : addImportEdit.insertedText, - }); - } - } else { - // Find last import statement to insert after - const allImports = rootNode.findAll({ rule: { kind: "import_statement" } }); - const lastImport = allImports[allImports.length - 1]; - if (lastImport) { - const endPos = lastImport.range().end.index; - edits.push({ - startPos: endPos, - endPos: endPos, - insertedText: '\nimport { useActionState } from "react";', - }); - } else { - edits.push({ - startPos: 0, - endPos: 0, - insertedText: 'import { useActionState } from "react";\n', - }); - } - } - } + if (memberResult.changed && memberResult.moduleType) { + // Member rewrites always need a bare useActionState binding. + source = ensureUseActionStateImport(source, memberResult.moduleType); } - return rootNode.commitEdits(edits); + return changed ? source : null; }; export default transform; diff --git a/codemods/replace-use-form-state/tests/cjs-named/expected.tsx b/codemods/replace-use-form-state/tests/cjs-named/expected.tsx new file mode 100644 index 0000000..9776f76 --- /dev/null +++ b/codemods/replace-use-form-state/tests/cjs-named/expected.tsx @@ -0,0 +1,2 @@ +const { useActionState } = require('react'); +const x = useActionState(a, 0); diff --git a/codemods/replace-use-form-state/tests/cjs-named/input.tsx b/codemods/replace-use-form-state/tests/cjs-named/input.tsx new file mode 100644 index 0000000..adc08ea --- /dev/null +++ b/codemods/replace-use-form-state/tests/cjs-named/input.tsx @@ -0,0 +1,2 @@ +const { useFormState } = require("react-dom"); +const x = useFormState(a, 0); diff --git a/codemods/replace-use-form-state/tests/cjs-named/metrics.json b/codemods/replace-use-form-state/tests/cjs-named/metrics.json new file mode 100644 index 0000000..c7a9194 --- /dev/null +++ b/codemods/replace-use-form-state/tests/cjs-named/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-use-form-state-migrations": [ + { + "cardinality": { + "file": "tests/cjs-named/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-use-form-state/tests/default-import/expected.tsx b/codemods/replace-use-form-state/tests/default-import/expected.tsx index c359d05..7ce1109 100644 --- a/codemods/replace-use-form-state/tests/default-import/expected.tsx +++ b/codemods/replace-use-form-state/tests/default-import/expected.tsx @@ -1,5 +1,5 @@ import ReactDOM from "react-dom"; -import { useActionState } from "react"; +import { useActionState } from 'react'; function StatefulForm({}) { const [state, formAction] = useActionState(increment, 0); diff --git a/codemods/replace-use-form-state/tests/merge-into-existing-react/expected.tsx b/codemods/replace-use-form-state/tests/merge-into-existing-react/expected.tsx new file mode 100644 index 0000000..8b8e763 --- /dev/null +++ b/codemods/replace-use-form-state/tests/merge-into-existing-react/expected.tsx @@ -0,0 +1,2 @@ +import { useState, useActionState } from 'react' +export const x = useActionState(a, 0) diff --git a/codemods/replace-use-form-state/tests/merge-into-existing-react/input.tsx b/codemods/replace-use-form-state/tests/merge-into-existing-react/input.tsx new file mode 100644 index 0000000..4ee59ef --- /dev/null +++ b/codemods/replace-use-form-state/tests/merge-into-existing-react/input.tsx @@ -0,0 +1,3 @@ +import { useState } from 'react' +import { useFormState } from 'react-dom' +export const x = useFormState(a, 0) diff --git a/codemods/replace-use-form-state/tests/merge-into-existing-react/metrics.json b/codemods/replace-use-form-state/tests/merge-into-existing-react/metrics.json new file mode 100644 index 0000000..e5257d3 --- /dev/null +++ b/codemods/replace-use-form-state/tests/merge-into-existing-react/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-use-form-state-migrations": [ + { + "cardinality": { + "file": "tests/merge-into-existing-react/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx index 2303201..e67610e 100644 --- a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx +++ b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx @@ -1,3 +1,2 @@ -import { useActionState } from 'react'; -import { useActionState as ufs } from 'react'; +import { useActionState, useActionState as ufs } from 'react'; function C(){ useActionState(a,0); return ufs(b,1); } diff --git a/codemods/replace-use-form-state/tests/named-import-2/expected.tsx b/codemods/replace-use-form-state/tests/named-import-2/expected.tsx index e24620d..0284e45 100644 --- a/codemods/replace-use-form-state/tests/named-import-2/expected.tsx +++ b/codemods/replace-use-form-state/tests/named-import-2/expected.tsx @@ -1,5 +1,5 @@ import { createPortal } from "react-dom"; -import { useActionState } from "react"; +import { useActionState } from 'react'; function StatefulForm({}) { const [state, formAction] = useActionState(increment, 0); diff --git a/codemods/replace-use-form-state/tests/named-import-3/expected.tsx b/codemods/replace-use-form-state/tests/named-import-3/expected.tsx index 3915af5..d073d77 100644 --- a/codemods/replace-use-form-state/tests/named-import-3/expected.tsx +++ b/codemods/replace-use-form-state/tests/named-import-3/expected.tsx @@ -1,5 +1,5 @@ import { createPortal } from "react-dom"; -import { useActionState as UFS } from "react"; +import { useActionState as UFS } from 'react'; function StatefulForm({}) { const [state, formAction] = UFS(increment, 0); diff --git a/codemods/replace-use-form-state/tests/named-import/expected.tsx b/codemods/replace-use-form-state/tests/named-import/expected.tsx index 02ae2e2..396f198 100644 --- a/codemods/replace-use-form-state/tests/named-import/expected.tsx +++ b/codemods/replace-use-form-state/tests/named-import/expected.tsx @@ -1,4 +1,4 @@ -import { useActionState } from "react"; +import { useActionState } from 'react'; async function increment(previousState, formData) { return previousState + 1; diff --git a/codemods/replace-use-form-state/tests/wildcard-import/expected.tsx b/codemods/replace-use-form-state/tests/wildcard-import/expected.tsx index 78187b0..7741fca 100644 --- a/codemods/replace-use-form-state/tests/wildcard-import/expected.tsx +++ b/codemods/replace-use-form-state/tests/wildcard-import/expected.tsx @@ -1,5 +1,5 @@ import * as ReactDOM from "react-dom"; -import { useActionState } from "react"; +import { useActionState } from 'react'; function StatefulForm({}) { const [state, formAction] = useActionState(increment, 0); diff --git a/package.json b/package.json index b48c816..fa33d37 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,6 @@ "@types/node": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e762395..ad918cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@changesets/cli': specifier: ^2.30.0 @@ -20,6 +20,10 @@ importers: version: 25.6.0 codemods/create-element-to-jsx: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -46,8 +50,8 @@ importers: codemods/find-dom-node: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -60,6 +64,10 @@ importers: version: 6.0.3 codemods/fragment-ref-wrappers: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -84,6 +92,10 @@ importers: version: 6.0.3 codemods/prop-types-typescript: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -96,6 +108,10 @@ importers: version: 6.0.3 codemods/pure-component: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -108,6 +124,10 @@ importers: version: 6.0.3 codemods/pure-render-mixin: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -126,8 +146,8 @@ importers: codemods/react-dom-to-react-dom-factories: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -142,8 +162,8 @@ importers: codemods/react-native-view-prop-types: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -158,8 +178,8 @@ importers: codemods/react-proptypes-to-prop-types: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -172,6 +192,10 @@ importers: version: 6.0.3 codemods/react-to-react-dom: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -184,6 +208,10 @@ importers: version: 6.0.3 codemods/remove-context-provider: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -198,8 +226,8 @@ importers: codemods/remove-forward-ref: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -212,6 +240,10 @@ importers: version: 6.0.3 codemods/remove-legacy-context: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -224,6 +256,10 @@ importers: version: 6.0.3 codemods/remove-memoization: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -250,8 +286,8 @@ importers: codemods/replace-act-import: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -264,6 +300,10 @@ importers: version: 6.0.3 codemods/replace-create-factory: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -302,8 +342,8 @@ importers: codemods/replace-reactdom-render: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -318,8 +358,8 @@ importers: codemods/replace-string-ref: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -334,8 +374,8 @@ importers: codemods/replace-use-form-state: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -348,6 +388,10 @@ importers: version: 6.0.3 codemods/sort-comp: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -360,6 +404,10 @@ importers: version: 6.0.3 codemods/unprefix-stable-apis: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -374,8 +422,8 @@ importers: codemods/update-react-imports: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -388,6 +436,10 @@ importers: version: 6.0.3 codemods/use-browser: + dependencies: + '@jssg/utils': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -402,8 +454,8 @@ importers: codemods/use-context-hook: dependencies: '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest @@ -491,8 +543,8 @@ packages: '@types/node': optional: true - '@jssg/utils@0.0.2': - resolution: {integrity: sha512-eCQv5Xs9yfI6OKq2PQ8SyKsxPhdiaJY/XAFJmsZiVbsK5DIBBmGBA95CsyD4JFWXDKkNv6Q7ER/Kbp6/uWzB2w==} + '@jssg/utils@0.0.10': + resolution: {integrity: sha512-2QHspAMmJsz9tucZisSJfW/4CzEhsvLZfIJ6t4wVhSeUZ/sW8AGsVcKgHJr3ES+jc16jvmWfta0yk4kTFA8IPw==} '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -966,7 +1018,7 @@ snapshots: optionalDependencies: '@types/node': 25.6.0 - '@jssg/utils@0.0.2': {} + '@jssg/utils@0.0.10': {} '@manypkg/find-root@1.1.0': dependencies: From 2c3108d770ffbc5c7f624a229531717748143d6b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Oct 2026 21:29:05 +0000 Subject: [PATCH 03/10] Adopt @jssg/utils import helpers in remaining React JSSG codemods Use getImport/getAllImports/addImport/removeImport for memoization cleanup, createFactory removal, PropTypes migration, and React alias resolution in find-dom-node/create-element-to-jsx/pure-render-mixin. Partial utils adoption in update-react-imports preserves its destructure/namespace policy. Add fixtures covering no-semicolon siblings, named import cleanup, double-quote prop-types insertion, and aliased namespace React bindings. Co-authored-by: Alex Bit --- codemods/create-element-to-jsx/package.json | 3 + .../create-element-to-jsx/scripts/codemod.ts | 31 +- .../aliased-namespace-react/expected.tsx | 4 + .../tests/aliased-namespace-react/input.tsx | 4 + .../aliased-namespace-react/metrics.json | 10 + codemods/find-dom-node/package.json | 2 +- codemods/find-dom-node/scripts/codemod.ts | 44 ++- .../aliased-namespace-react/expected.tsx | 8 + .../tests/aliased-namespace-react/input.tsx | 8 + .../aliased-namespace-react/metrics.json | 10 + codemods/pure-render-mixin/package.json | 3 + codemods/pure-render-mixin/scripts/codemod.ts | 48 ++- .../aliased-namespace-react/expected.tsx | 11 + .../tests/aliased-namespace-react/input.tsx | 11 + .../aliased-namespace-react/metrics.json | 10 + .../package.json | 2 +- .../scripts/codemod.ts | 219 ++++------- .../expected.tsx | 2 +- .../expected.tsx | 2 +- .../default-and-named-import/expected.tsx | 2 +- .../tests/default-import/expected.tsx | 2 +- .../tests/double-quote-style/expected.tsx | 9 + .../tests/double-quote-style/input.tsx | 8 + .../tests/import-alias/expected.tsx | 2 +- .../expected.tsx | 4 +- .../mixed-import-and-require-2/expected.tsx | 2 +- .../mixed-import-and-require/expected.tsx | 2 +- .../tests/module-name/expected.tsx | 2 +- .../tests/named-parameters/expected.tsx | 2 +- .../expected.tsx | 2 +- .../tests/require-alias/expected.tsx | 2 +- .../require-destructured-direct/expected.tsx | 1 + .../require-destructured-multi/expected.tsx | 2 +- .../require-destructured-only/expected.tsx | 2 +- .../tests/require/expected.tsx | 2 +- .../tests/with-top-comment/expected.tsx | 2 +- codemods/remove-memoization/package.json | 3 + .../remove-memoization/scripts/codemod.ts | 359 ++++++++---------- .../no-semicolon-keep-sibling/expected.tsx | 3 + .../tests/no-semicolon-keep-sibling/input.tsx | 3 + .../no-semicolon-keep-sibling/metrics.json | 11 + codemods/replace-create-factory/package.json | 3 + .../replace-create-factory/scripts/codemod.ts | 353 ++++++++--------- .../tests/named-with-sibling/expected.tsx | 4 + .../tests/named-with-sibling/input.tsx | 4 + .../tests/named-with-sibling/metrics.json | 11 + codemods/update-react-imports/package.json | 2 +- .../update-react-imports/scripts/codemod.ts | 59 ++- 48 files changed, 725 insertions(+), 570 deletions(-) create mode 100644 codemods/create-element-to-jsx/tests/aliased-namespace-react/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/aliased-namespace-react/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/aliased-namespace-react/metrics.json create mode 100644 codemods/find-dom-node/tests/aliased-namespace-react/expected.tsx create mode 100644 codemods/find-dom-node/tests/aliased-namespace-react/input.tsx create mode 100644 codemods/find-dom-node/tests/aliased-namespace-react/metrics.json create mode 100644 codemods/pure-render-mixin/tests/aliased-namespace-react/expected.tsx create mode 100644 codemods/pure-render-mixin/tests/aliased-namespace-react/input.tsx create mode 100644 codemods/pure-render-mixin/tests/aliased-namespace-react/metrics.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/double-quote-style/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/double-quote-style/input.tsx create mode 100644 codemods/remove-memoization/tests/no-semicolon-keep-sibling/expected.tsx create mode 100644 codemods/remove-memoization/tests/no-semicolon-keep-sibling/input.tsx create mode 100644 codemods/remove-memoization/tests/no-semicolon-keep-sibling/metrics.json create mode 100644 codemods/replace-create-factory/tests/named-with-sibling/expected.tsx create mode 100644 codemods/replace-create-factory/tests/named-with-sibling/input.tsx create mode 100644 codemods/replace-create-factory/tests/named-with-sibling/metrics.json diff --git a/codemods/create-element-to-jsx/package.json b/codemods/create-element-to-jsx/package.json index 2ca37ed..97b1602 100644 --- a/codemods/create-element-to-jsx/package.json +++ b/codemods/create-element-to-jsx/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "@types/node": "latest", "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/create-element-to-jsx/scripts/codemod.ts b/codemods/create-element-to-jsx/scripts/codemod.ts index 458ea1c..a4f2c8d 100644 --- a/codemods/create-element-to-jsx/scripts/codemod.ts +++ b/codemods/create-element-to-jsx/scripts/codemod.ts @@ -1,6 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { getImport } from "@jssg/utils/javascript/imports"; type Conversion = { code: string; @@ -33,7 +34,19 @@ type ArgumentLayout = { boundaryComments: SgNode[][]; }; -const REACT_MODULES = new Set(["React", "react", "react/addons", "react-native"]); +const REACT_MODULES = ["React", "react", "react/addons", "react-native"] as const; +const REACT_MODULE_SET = new Set(REACT_MODULES); + +/** Resolved per-transform from import/require bindings (supports `import * as R`). */ +let currentReactAlias = "React"; + +function resolveReactAlias(rootNode: SgNode): string { + for (const from of REACT_MODULES) { + const imp = getImport(rootNode, { type: "default", from }); + if (imp) return imp.alias; + } + return "React"; +} function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -92,10 +105,6 @@ function stringValue(node: SgNode): string { return evaluated; } -function isReactModuleName(value: string | null): boolean { - return value !== null && REACT_MODULES.has(value); -} - function stringNodeFrom(node: SgNode): SgNode | null { if (node.kind() === "string") { return node; @@ -144,6 +153,10 @@ function hasReact(rootNode: SgNode): boolean { return false; } +function isReactModuleName(value: string | null): boolean { + return value !== null && REACT_MODULE_SET.has(value); +} + function callArguments(call: SgNode): SgNode[] { const args = call.field("arguments"); if (!args) { @@ -167,7 +180,7 @@ function isReactCreateElementCall(node: SgNode): boolean { const property = callee.field("property"); return object?.kind() === "identifier" && - object.text() === "React" && + object.text() === currentReactAlias && property?.kind() === "property_identifier" && property.text() === "createElement"; } @@ -489,7 +502,7 @@ function propsToDescriptors(node: SgNode | null, source: string): { return { attrs: [], trailingElementComments: [], consumedBoundaryComments: [] }; } - if (isMemberCall(node, "React", "__spread") || isMemberCall(node, "Object", "assign")) { + if (isMemberCall(node, currentReactAlias, "__spread") || isMemberCall(node, "Object", "assign")) { const callee = node.field("function"); const calleeObject = callee?.field("object") ?? null; const calleeProperty = callee?.field("property") ?? null; @@ -750,7 +763,7 @@ function propsToAttributes(node: SgNode | null): string[] { return []; } - if (isMemberCall(node, "React", "__spread") || isMemberCall(node, "Object", "assign")) { + if (isMemberCall(node, currentReactAlias, "__spread") || isMemberCall(node, "Object", "assign")) { return callArguments(node).flatMap((arg) => propsToAttributes(arg)); } @@ -1214,6 +1227,8 @@ const transform: Transform = async (root, options) => { return null; } + currentReactAlias = resolveReactAlias(rootNode); + const canConvertCall = (call: SgNode): boolean => { const cached = convertibility.get(call.id()); if (cached !== undefined) { diff --git a/codemods/create-element-to-jsx/tests/aliased-namespace-react/expected.tsx b/codemods/create-element-to-jsx/tests/aliased-namespace-react/expected.tsx new file mode 100644 index 0000000..71b4dc7 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/aliased-namespace-react/expected.tsx @@ -0,0 +1,4 @@ +import * as R from 'React'; + +var a = ; +var b =
; diff --git a/codemods/create-element-to-jsx/tests/aliased-namespace-react/input.tsx b/codemods/create-element-to-jsx/tests/aliased-namespace-react/input.tsx new file mode 100644 index 0000000..fea11c7 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/aliased-namespace-react/input.tsx @@ -0,0 +1,4 @@ +import * as R from 'React'; + +var a = R.createElement(Foo, null); +var b = R.createElement('div', null); diff --git a/codemods/create-element-to-jsx/tests/aliased-namespace-react/metrics.json b/codemods/create-element-to-jsx/tests/aliased-namespace-react/metrics.json new file mode 100644 index 0000000..bd4634e --- /dev/null +++ b/codemods/create-element-to-jsx/tests/aliased-namespace-react/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/aliased-namespace-react/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/find-dom-node/package.json b/codemods/find-dom-node/package.json index bb52f08..e2e2805 100644 --- a/codemods/find-dom-node/package.json +++ b/codemods/find-dom-node/package.json @@ -13,6 +13,6 @@ "@types/node": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/find-dom-node/scripts/codemod.ts b/codemods/find-dom-node/scripts/codemod.ts index b98dc0a..50fd11c 100644 --- a/codemods/find-dom-node/scripts/codemod.ts +++ b/codemods/find-dom-node/scripts/codemod.ts @@ -1,8 +1,10 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { getImport } from "@jssg/utils/javascript/imports"; -const REACT_MODULES = new Set(["React", "react", "react/addons", "react-native"]); +const REACT_MODULES = ["React", "react", "react/addons", "react-native"] as const; +const REACT_MODULE_SET = new Set(REACT_MODULES); function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -36,26 +38,37 @@ function importSource(node: SgNode): string | null { function hasReact(rootNode: SgNode): boolean { for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (REACT_MODULES.has(importSource(importNode) ?? "")) return true; + if (REACT_MODULE_SET.has(importSource(importNode) ?? "")) return true; } for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { - if (REACT_MODULES.has(requireSource(call) ?? "")) return true; + if (REACT_MODULE_SET.has(requireSource(call) ?? "")) return true; } return false; } +function resolveReactAlias(rootNode: SgNode): string { + for (const from of REACT_MODULES) { + const imp = getImport(rootNode, { type: "default", from }); + if (imp) return imp.alias; + } + return "React"; +} + function explicitRequireDisabled(value: unknown): boolean { return value === false || value === "false"; } -function isReactCreateClassCall(node: SgNode | null): node is SgNode { +function isReactCreateClassCall( + node: SgNode | null, + reactAlias: string, +): node is SgNode { if (!node || node.kind() !== "call_expression") return false; const callee = node.field("function"); return callee?.kind() === "member_expression" && callee.field("object")?.kind() === "identifier" && - callee.field("object")?.text() === "React" && + callee.field("object")?.text() === reactAlias && callee.field("property")?.kind() === "property_identifier" && callee.field("property")?.text() === "createClass"; } @@ -68,15 +81,21 @@ function isModuleExportsMember(node: SgNode | null): boolean { node.field("property")?.text() === "exports"; } -function createClassContainers(rootNode: SgNode): SgNode[] { +function createClassContainers( + rootNode: SgNode, + reactAlias: string, +): SgNode[] { const containers: SgNode[] = []; for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - if (isReactCreateClassCall(declarator.field("value"))) containers.push(declarator); + if (isReactCreateClassCall(declarator.field("value"), reactAlias)) containers.push(declarator); } for (const assignment of rootNode.findAll({ rule: { kind: "assignment_expression" } })) { - if (isModuleExportsMember(assignment.field("left")) && isReactCreateClassCall(assignment.field("right"))) { + if ( + isModuleExportsMember(assignment.field("left")) && + isReactCreateClassCall(assignment.field("right"), reactAlias) + ) { containers.push(assignment); } } @@ -127,14 +146,15 @@ const transform: Transform = async (root, options) => { return null; } + const reactAlias = resolveReactAlias(rootNode); const metric = useMetricAtom("find-dom-node-replacements"); const edits: Edit[] = []; let replacements = 0; - for (const container of createClassContainers(rootNode)) { + for (const container of createClassContainers(rootNode, reactAlias)) { for (const call of container.findAll({ rule: { kind: "call_expression" } })) { if (isGetDOMNodeCall(call, (node) => node?.kind() === "this")) { - edits.push(call.replace("React.findDOMNode(this)")); + edits.push(call.replace(`${reactAlias}.findDOMNode(this)`)); replacements++; continue; } @@ -142,7 +162,7 @@ const transform: Transform = async (root, options) => { if (isGetDOMNodeCall(call, isThisRefsChain)) { const target = call.field("function")?.field("object"); if (!target) continue; - edits.push(call.replace(`React.findDOMNode(${target.text()})`)); + edits.push(call.replace(`${reactAlias}.findDOMNode(${target.text()})`)); replacements++; } } @@ -160,7 +180,7 @@ const transform: Transform = async (root, options) => { continue; } - edits.push(call.replace(`React.findDOMNode(${name.text()})`)); + edits.push(call.replace(`${reactAlias}.findDOMNode(${name.text()})`)); replacements++; } } diff --git a/codemods/find-dom-node/tests/aliased-namespace-react/expected.tsx b/codemods/find-dom-node/tests/aliased-namespace-react/expected.tsx new file mode 100644 index 0000000..12d538c --- /dev/null +++ b/codemods/find-dom-node/tests/aliased-namespace-react/expected.tsx @@ -0,0 +1,8 @@ +import * as R from 'react'; + +var Composer = R.createClass({ + componentWillReceiveProps: function() { + R.findDOMNode(this); + return foo(R.findDOMNode(this.refs.input)); + }, +}); diff --git a/codemods/find-dom-node/tests/aliased-namespace-react/input.tsx b/codemods/find-dom-node/tests/aliased-namespace-react/input.tsx new file mode 100644 index 0000000..9027e42 --- /dev/null +++ b/codemods/find-dom-node/tests/aliased-namespace-react/input.tsx @@ -0,0 +1,8 @@ +import * as R from 'react'; + +var Composer = R.createClass({ + componentWillReceiveProps: function() { + this.getDOMNode(); + return foo(this.refs.input.getDOMNode()); + }, +}); diff --git a/codemods/find-dom-node/tests/aliased-namespace-react/metrics.json b/codemods/find-dom-node/tests/aliased-namespace-react/metrics.json new file mode 100644 index 0000000..75cef43 --- /dev/null +++ b/codemods/find-dom-node/tests/aliased-namespace-react/metrics.json @@ -0,0 +1,10 @@ +{ + "find-dom-node-replacements": [ + { + "cardinality": { + "file": "tests/aliased-namespace-react/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-render-mixin/package.json b/codemods/pure-render-mixin/package.json index cb49a97..231c853 100644 --- a/codemods/pure-render-mixin/package.json +++ b/codemods/pure-render-mixin/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "typescript": "latest", "@types/node": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/pure-render-mixin/scripts/codemod.ts b/codemods/pure-render-mixin/scripts/codemod.ts index bf6180d..d53480c 100644 --- a/codemods/pure-render-mixin/scripts/codemod.ts +++ b/codemods/pure-render-mixin/scripts/codemod.ts @@ -1,8 +1,10 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { getImport } from "@jssg/utils/javascript/imports"; -const REACT_MODULES = new Set(["React", "react", "react/addons", "react-native"]); +const REACT_MODULES = ["React", "react", "react/addons", "react-native"] as const; +const REACT_MODULE_SET = new Set(REACT_MODULES); function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -36,38 +38,52 @@ function importSource(node: SgNode): string | null { function hasReact(rootNode: SgNode): boolean { for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (REACT_MODULES.has(importSource(importNode) ?? "")) return true; + if (REACT_MODULE_SET.has(importSource(importNode) ?? "")) return true; } for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { - if (REACT_MODULES.has(requireSource(call) ?? "")) return true; + if (REACT_MODULE_SET.has(requireSource(call) ?? "")) return true; } return false; } +function resolveReactAlias(rootNode: SgNode): string { + for (const from of REACT_MODULES) { + const imp = getImport(rootNode, { type: "default", from }); + if (imp) return imp.alias; + } + return "React"; +} + function explicitRequireDisabled(value: unknown): boolean { return value === false || value === "false"; } -function isReactCreateClassCall(node: SgNode | null): node is SgNode { +function isReactCreateClassCall( + node: SgNode | null, + reactAlias: string, +): node is SgNode { if (!node || node.kind() !== "call_expression") return false; const callee = node.field("function"); return callee?.kind() === "member_expression" && callee.field("object")?.kind() === "identifier" && - callee.field("object")?.text() === "React" && + callee.field("object")?.text() === reactAlias && callee.field("property")?.kind() === "property_identifier" && callee.field("property")?.text() === "createClass"; } -function reactCreateClassDeclarators(rootNode: SgNode): Array<{ +function reactCreateClassDeclarators( + rootNode: SgNode, + reactAlias: string, +): Array<{ declarator: SgNode; call: SgNode; }> { const result: Array<{ declarator: SgNode; call: SgNode }> = []; for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { const value = declarator.field("value"); - if (isReactCreateClassCall(value)) result.push({ declarator, call: value }); + if (isReactCreateClassCall(value, reactAlias)) result.push({ declarator, call: value }); } return result; } @@ -122,11 +138,16 @@ function declarationRemovalRange(varDecl: SgNode, source: string): { start: return { start, end }; } -function shouldUpdatePropertyText(): string { - return `${SHOULD_COMPONENT_UPDATE}: function(nextProps, nextState) {\n return React.addons.shallowCompare(this, nextProps, nextState);\n }`; +function shouldUpdatePropertyText(reactAlias: string): string { + return `${SHOULD_COMPONENT_UPDATE}: function(nextProps, nextState) {\n return ${reactAlias}.addons.shallowCompare(this, nextProps, nextState);\n }`; } -function renderConfigObject(configObj: SgNode, mixinsPair: SgNode, mixinName: string): string | null { +function renderConfigObject( + configObj: SgNode, + mixinsPair: SgNode, + mixinName: string, + reactAlias: string, +): string | null { const mixinsValue = mixinsPair.field("value"); if (!mixinsValue || mixinsValue.kind() !== "array") return null; const mixinInArray = mixinsValue.findAll(arrayIdentifierNamedRule(mixinName)); @@ -162,7 +183,7 @@ function renderConfigObject(configObj: SgNode, mixinsPair: SgNode, mix if (renderedProps.length === 0) return null; - const shouldUpdateText = shouldUpdatePropertyText(); + const shouldUpdateText = shouldUpdatePropertyText(reactAlias); if (renderIndex >= 0 && renderIndex === renderedProps.length - 1) { renderedProps.splice(renderIndex, 0, shouldUpdateText); } else { @@ -180,12 +201,13 @@ const transform: Transform = async (root, options) => { const source = rootNode.text(); const edits: Edit[] = []; + const reactAlias = resolveReactAlias(rootNode); const mixinName = (options.params?.["mixin-name"] as string) ?? DEFAULT_MIXIN_NAME; const metric = useMetricAtom("pure-render-mixin-replacements"); let transformCount = 0; - for (const { call } of reactCreateClassDeclarators(rootNode)) { + for (const { call } of reactCreateClassDeclarators(rootNode, reactAlias)) { const configObj = getConfigObject(call); if (!configObj) continue; @@ -200,7 +222,7 @@ const transform: Transform = async (root, options) => { const mixinInArray = value.findAll(arrayIdentifierNamedRule(mixinName)); if (mixinInArray.length === 0) continue; - const newConfigText = renderConfigObject(configObj, mixinsPair, mixinName); + const newConfigText = renderConfigObject(configObj, mixinsPair, mixinName, reactAlias); if (!newConfigText) continue; edits.push(configObj.replace(newConfigText)); diff --git a/codemods/pure-render-mixin/tests/aliased-namespace-react/expected.tsx b/codemods/pure-render-mixin/tests/aliased-namespace-react/expected.tsx new file mode 100644 index 0000000..c93bc39 --- /dev/null +++ b/codemods/pure-render-mixin/tests/aliased-namespace-react/expected.tsx @@ -0,0 +1,11 @@ +import * as R from 'react/addons'; + +var MyComponent = R.createClass({ + shouldComponentUpdate: function(nextProps, nextState) { + return R.addons.shallowCompare(this, nextProps, nextState); + }, + + render: function() { + return
; + }, +}); diff --git a/codemods/pure-render-mixin/tests/aliased-namespace-react/input.tsx b/codemods/pure-render-mixin/tests/aliased-namespace-react/input.tsx new file mode 100644 index 0000000..aad6564 --- /dev/null +++ b/codemods/pure-render-mixin/tests/aliased-namespace-react/input.tsx @@ -0,0 +1,11 @@ +import * as R from 'react/addons'; + +var PureRenderMixin = R.addons.PureRenderMixin; + +var MyComponent = R.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
; + }, +}); diff --git a/codemods/pure-render-mixin/tests/aliased-namespace-react/metrics.json b/codemods/pure-render-mixin/tests/aliased-namespace-react/metrics.json new file mode 100644 index 0000000..72d90ba --- /dev/null +++ b/codemods/pure-render-mixin/tests/aliased-namespace-react/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-render-mixin-replacements": [ + { + "cardinality": { + "file": "tests/aliased-namespace-react/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/package.json b/codemods/react-proptypes-to-prop-types/package.json index 83d5b9a..b94af94 100644 --- a/codemods/react-proptypes-to-prop-types/package.json +++ b/codemods/react-proptypes-to-prop-types/package.json @@ -13,6 +13,6 @@ "@types/node": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/react-proptypes-to-prop-types/scripts/codemod.ts b/codemods/react-proptypes-to-prop-types/scripts/codemod.ts index dd09868..2b56121 100644 --- a/codemods/react-proptypes-to-prop-types/scripts/codemod.ts +++ b/codemods/react-proptypes-to-prop-types/scripts/codemod.ts @@ -1,6 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { addImport, getImport, removeImport } from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -82,97 +83,41 @@ function isRequireCall(node: SgNode | null, moduleNames: Set): bool return !!source && moduleNames.has(source); } -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? sourceText(source) : null; -} - -function hasImportOrRequire(rootNode: SgNode, moduleName: string): boolean { - const modules = new Set([moduleName]); - return rootNode.findAll({ rule: { kind: "import_statement" } }) - .some((node) => importSource(node) === moduleName) || - rootNode.findAll({ rule: { kind: "call_expression" } }) - .some((node) => isRequireCall(node, modules)); -} - -function usesImportSyntax(rootNode: SgNode): boolean { - return rootNode.findAll({ rule: { kind: "import_statement" } }) - .some((node) => !node.text().startsWith("import type")); -} - -function usesVarForRequires(rootNode: SgNode): boolean { - return rootNode.find({ rule: { kind: "lexical_declaration" } }) === null; -} - -function firstSortedImportPosition( +function hasPropTypesModule( rootNode: SgNode, moduleName: string, - source: string, -): number { - const imports = rootNode.findAll({ rule: { kind: "import_statement" } }); - const lowerModule = moduleName.toLowerCase(); - let target: SgNode | null = null; - let targetName = ""; - - for (const imp of imports) { - const source = importSource(imp); - if (!source) continue; - const lowerSource = source.toLowerCase(); - if (lowerSource > lowerModule && (!target || lowerSource < targetName)) { - target = imp; - targetName = lowerSource; - } - } - - if (target) return target.range().start.index; - const last = imports[imports.length - 1]; - if (!last) return 0; - return statementRange(last, source).end; +): boolean { + return ( + getImport(rootNode, { type: "default", from: moduleName }) !== null || + getImport(rootNode, { type: "named", name: "PropTypes", from: moduleName }) !== null + ); } -function firstSortedRequirePosition(rootNode: SgNode, moduleName: string): number { - const lowerModule = moduleName.toLowerCase(); - let target: SgNode | null = null; - let targetName = ""; +function dominantQuote(rootNode: SgNode): "'" | '"' { + for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { + const source = imp.field("source"); + if (source?.text().startsWith("'")) return "'"; + if (source?.text().startsWith('"')) return '"'; + } for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { if (call.field("function")?.text() !== "require") continue; const arg = call.field("arguments")?.find({ rule: { kind: "string" } }); - const source = arg ? sourceText(arg) : null; - if (!source) continue; - const lowerSource = source.toLowerCase(); - if (lowerSource > lowerModule && (!target || lowerSource < targetName)) { - const stmt = nearestStatement(call); - if (stmt) { - target = stmt; - targetName = lowerSource; - } - } + if (arg?.text().startsWith("'")) return "'"; + if (arg?.text().startsWith('"')) return '"'; } - if (target) return target.range().start.index; - return 0; + return "'"; } -function propTypesBindingFromModule(rootNode: SgNode, moduleName: string): string | null { - for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(imp) !== moduleName) continue; - const clause = imp.find({ rule: { kind: "import_clause" } }); - const defaultName = clause?.children().find((c) => c.kind() === "identifier"); - if (defaultName) return defaultName.text(); - const named = imp.findAll({ rule: { kind: "import_specifier" } }) - .find((spec) => spec.field("name")?.text() === "PropTypes"); - if (named && !named.field("alias")) return "PropTypes"; - } - - for (const decl of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - const value = decl.field("value"); - if (!isRequireCall(value, new Set([moduleName]))) continue; - const name = decl.field("name"); - if (name?.kind() === "identifier") return name.text(); - } - - return null; +function withQuoteStyle(edit: Edit, quote: "'" | '"'): Edit { + if (quote === "'") return edit; + return { + ...edit, + insertedText: edit.insertedText + .replace(/\bfrom '([^']*)'/g, `from "$1"`) + .replace(/\brequire\('([^']*)'\)/g, `require("$1")`), + }; } function identifierNamesInPattern(pattern: SgNode): string[] { @@ -188,66 +133,50 @@ function identifierNamesInPattern(pattern: SgNode): string[] { }); } -function removeNamedImportsAfterDefault(namedImports: SgNode, source: string): Edit { - let start = namedImports.range().start.index; - let i = start - 1; - while (isWhitespace(source[i])) i--; - if (source[i] === ",") start = i; - return { startPos: start, endPos: namedImports.range().end.index, insertedText: "" }; -} - const transform: Transform = async (root, options) => { const rootNode = root.root(); const source = rootNode.text(); const edits: Edit[] = []; const metric = useMetricAtom("react-proptypes-migrations"); const moduleName = (options.params?.["module-name"] as string | undefined) ?? "prop-types"; - const reactModules = new Set(["react", "React"]); - const reactNames = new Set(["React"]); - let localPropTypesName = propTypesBindingFromModule(rootNode, moduleName) ?? "PropTypes"; - let addedPropTypesImportByReplacement = false; + const reactModules = ["react", "React"] as const; + const reactNames = new Set(); + let localPropTypesName = "PropTypes"; let changed = false; - for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { - const sourceName = importSource(imp); - if (sourceName && reactModules.has(sourceName)) { - const clause = imp.find({ rule: { kind: "import_clause" } }); - const defaultName = clause?.children().find((c) => c.kind() === "identifier"); - if (defaultName) reactNames.add(defaultName.text()); + const usesEsmImports = rootNode.findAll({ rule: { kind: "import_statement" } }) + .some((node) => !node.text().startsWith("import type")); + let moduleType: "esm" | "cjs" = usesEsmImports ? "esm" : "cjs"; + + for (const from of reactModules) { + const defaultImp = getImport(rootNode, { type: "default", from }); + if (defaultImp) { + reactNames.add(defaultImp.alias); + if (!usesEsmImports) { + moduleType = defaultImp.moduleType; + } } } - for (const decl of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - const value = decl.field("value"); - const name = decl.field("name"); - if (name?.kind() === "identifier" && isRequireCall(value, reactModules)) { - reactNames.add(name.text()); - } + if (reactNames.size === 0) { + reactNames.add("React"); } - for (const spec of rootNode.findAll({ rule: { kind: "import_specifier" } })) { - const parentImport = spec.ancestors().find((a) => a.kind() === "import_statement"); - if (!parentImport || !reactModules.has(importSource(parentImport) ?? "")) continue; - if (spec.field("name")?.text() !== "PropTypes") continue; - localPropTypesName = spec.field("alias")?.text() ?? "PropTypes"; - const namedImports = spec.ancestors().find((a) => a.kind() === "named_imports"); - const specifierCount = namedImports?.findAll({ rule: { kind: "import_specifier" } }).length ?? 0; - const importClause = parentImport.find({ rule: { kind: "import_clause" } }); - const hasDefault = importClause?.children().some((c) => c.kind() === "identifier") ?? false; - if (specifierCount === 1 && !hasDefault) { - if (!hasImportOrRequire(rootNode, moduleName)) { - edits.push(parentImport.replace(`import ${localPropTypesName} from '${moduleName}';`)); - addedPropTypesImportByReplacement = true; - } else { - const { start, end } = statementRange(parentImport, source); - edits.push({ startPos: start, endPos: end, insertedText: "" }); - } - } else if (specifierCount === 1 && hasDefault && namedImports) { - edits.push(removeNamedImportsAfterDefault(namedImports, source)); - } else { - edits.push(removeNodeWithComma(spec, source)); + for (const from of reactModules) { + const propTypesImport = getImport(rootNode, { type: "named", name: "PropTypes", from }); + if (!propTypesImport || propTypesImport.isNamespace) continue; + + localPropTypesName = propTypesImport.alias; + + const removeEdit = removeImport(rootNode, { + type: "named", + specifiers: ["PropTypes"], + from, + }); + if (removeEdit) { + edits.push(removeEdit); + changed = true; } - changed = true; } for (const pattern of rootNode.findAll({ rule: { kind: "object_pattern" } })) { @@ -257,7 +186,7 @@ const transform: Transform = async (root, options) => { if (!decl || !statement) continue; const destructuresReact = (value?.kind() === "identifier" && reactNames.has(value.text())) || - isRequireCall(value ?? null, reactModules); + isRequireCall(value ?? null, new Set(reactModules)); if (!destructuresReact) continue; const props = pattern.children().filter((child) => @@ -316,23 +245,27 @@ const transform: Transform = async (root, options) => { if (!changed) return null; - if (!addedPropTypesImportByReplacement && !hasImportOrRequire(rootNode, moduleName)) { - if (usesImportSyntax(rootNode)) { - const pos = firstSortedImportPosition(rootNode, moduleName, source); - edits.push({ - startPos: pos, - endPos: pos, - insertedText: `import ${localPropTypesName} from '${moduleName}';\n`, - - }); - } else { - const pos = firstSortedRequirePosition(rootNode, moduleName); - const keyword = usesVarForRequires(rootNode) ? "var" : "const"; - edits.push({ - startPos: pos, - endPos: pos, - insertedText: `${keyword} ${localPropTypesName} = require('${moduleName}');\n`, - }); + if (!hasPropTypesModule(rootNode, moduleName)) { + const quote = dominantQuote(rootNode); + const addEdit = addImport(rootNode, { + type: "default", + name: localPropTypesName, + from: moduleName, + moduleType, + }); + if (addEdit) { + let edit = withQuoteStyle(addEdit, quote); + // Removals that consume blank lines after the last import can overlap + // addImport's insertion point; relocate to file start in that case. + const conflicts = edits.some((e) => + e.insertedText === "" && + e.startPos <= edit.startPos && + edit.startPos < e.endPos + ); + if (conflicts) { + edit = { ...edit, startPos: 0, endPos: 0 }; + } + edits.push(edit); } } diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx index 24e0ce0..63a8824 100644 --- a/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx @@ -1,5 +1,5 @@ -const PropTypes = require('prop-types'); const React = require('react'); +const PropTypes = require('prop-types'); const ReactPropTypes = PropTypes; const foo = ReactPropTypes.string; diff --git a/codemods/react-proptypes-to-prop-types/tests/custom-react-import-insert-newline/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/custom-react-import-insert-newline/expected.tsx index d5903b5..53abbc5 100644 --- a/codemods/react-proptypes-to-prop-types/tests/custom-react-import-insert-newline/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/custom-react-import-insert-newline/expected.tsx @@ -1,7 +1,7 @@ import {React, ReactDOM} from 'nylas-exports' import {Menu} from 'nylas-component-kit' - import PropTypes from 'prop-types'; + export default class DropdownMenu extends React.Component { static propTypes = { item: PropTypes.object, diff --git a/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx index ba61a76..5e0e7e9 100644 --- a/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx @@ -1,5 +1,5 @@ -import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import PropTypes from 'prop-types'; class ClassComponent extends Component { static propTypes = { diff --git a/codemods/react-proptypes-to-prop-types/tests/default-import/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/default-import/expected.tsx index 15e1bf3..0c5ed83 100644 --- a/codemods/react-proptypes-to-prop-types/tests/default-import/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/default-import/expected.tsx @@ -1,7 +1,7 @@ import Abc from 'abc'; -import PropTypes from 'prop-types'; import React from 'React'; import Xzy from 'xyz'; +import PropTypes from 'prop-types'; class ClassComponent extends React.Component { static propTypes = { diff --git a/codemods/react-proptypes-to-prop-types/tests/double-quote-style/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/double-quote-style/expected.tsx new file mode 100644 index 0000000..b001b96 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/double-quote-style/expected.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import PropTypes from "prop-types"; + +function Component(props) { + return
{props.text}
; +} +Component.propTypes = { + text: PropTypes.string.isRequired, +}; diff --git a/codemods/react-proptypes-to-prop-types/tests/double-quote-style/input.tsx b/codemods/react-proptypes-to-prop-types/tests/double-quote-style/input.tsx new file mode 100644 index 0000000..11b7b4e --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/double-quote-style/input.tsx @@ -0,0 +1,8 @@ +import React, { PropTypes } from "react"; + +function Component(props) { + return
{props.text}
; +} +Component.propTypes = { + text: PropTypes.string.isRequired, +}; diff --git a/codemods/react-proptypes-to-prop-types/tests/import-alias/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/import-alias/expected.tsx index 317f7e5..500d6ae 100644 --- a/codemods/react-proptypes-to-prop-types/tests/import-alias/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/import-alias/expected.tsx @@ -1,5 +1,5 @@ -import PT from 'prop-types'; import React, { Component } from 'react'; +import PT from 'prop-types'; class ClassComponent extends Component { static propTypes = { diff --git a/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx index 9c5701c..5f81604 100644 --- a/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx @@ -1,4 +1,4 @@ -const PropTypes = require('prop-types'); const React = require('React'); -import type {SomeType} from 'SomeModule'; \ No newline at end of file +import type {SomeType} from 'SomeModule'; +const PropTypes = require('prop-types'); diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx index 3845188..63c7ed5 100644 --- a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx @@ -2,8 +2,8 @@ import type { Foo } from 'Foo'; -const PropTypes = require('prop-types'); const React = require('React'); +const PropTypes = require('prop-types'); class Component extends React.PureComponent { render() { diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx index 8787e82..b8db069 100644 --- a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx @@ -1,5 +1,5 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import PropTypes from 'prop-types'; require('styles.css'); diff --git a/codemods/react-proptypes-to-prop-types/tests/module-name/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/module-name/expected.tsx index e78e65e..2ab2d46 100644 --- a/codemods/react-proptypes-to-prop-types/tests/module-name/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/module-name/expected.tsx @@ -1,5 +1,5 @@ -const PropTypes = require('PropTypes'); const React = require('React'); +const PropTypes = require('PropTypes'); class ClassComponent extends React.Component { static propTypes = { diff --git a/codemods/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx index 7e1fd2f..5c8ea38 100644 --- a/codemods/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx @@ -1,6 +1,6 @@ -import PropTypes from 'prop-types'; import React from 'react'; import classnames from 'classnames'; +import PropTypes from 'prop-types'; const propTypes = { children: PropTypes.node, diff --git a/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx index bc39982..63207f5 100644 --- a/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx @@ -1,5 +1,5 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import PropTypes from 'prop-types'; const { shape, bool, number, string } = PropTypes; const { diff --git a/codemods/react-proptypes-to-prop-types/tests/require-alias/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/require-alias/expected.tsx index 0a7e190..b430bd9 100644 --- a/codemods/react-proptypes-to-prop-types/tests/require-alias/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/require-alias/expected.tsx @@ -1,5 +1,5 @@ -const PT = require('prop-types'); const React = require('react'); +const PT = require('prop-types'); const { Component } = React; class ClassComponent extends Component { diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx index b8672d9..5e89eb9 100644 --- a/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx @@ -1,5 +1,6 @@ const PropTypes = require('prop-types'); const React = require('react'); + function Foo(props) { return
{props.text}
; } diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx index 5491a87..e2cc0ec 100644 --- a/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx @@ -1,5 +1,5 @@ -const PropTypes = require('prop-types'); const React = require('react'); +const PropTypes = require('prop-types'); const { Component } = React; class ClassComponent extends Component { diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx index 3d1d184..3359861 100644 --- a/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx @@ -1,7 +1,7 @@ const Abc = require('abc'); -const PropTypes = require('prop-types'); const React = require('react'); const Xyz = require('xyz'); +const PropTypes = require('prop-types'); class ClassComponent extends React.Component { static propTypes = { diff --git a/codemods/react-proptypes-to-prop-types/tests/require/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/require/expected.tsx index 463e982..32d21c4 100644 --- a/codemods/react-proptypes-to-prop-types/tests/require/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/require/expected.tsx @@ -1,5 +1,5 @@ -const PropTypes = require('prop-types'); const React = require('React'); +const PropTypes = require('prop-types'); class ClassComponent extends React.Component { static propTypes = { diff --git a/codemods/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx index 8ab3354..67d6785 100644 --- a/codemods/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx +++ b/codemods/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx @@ -1,8 +1,8 @@ /** * This is some multiline comment */ -import PropTypes from 'prop-types'; import React from 'react'; +import PropTypes from 'prop-types'; function FunctionalComponent (props) { return
{props.text}
; diff --git a/codemods/remove-memoization/package.json b/codemods/remove-memoization/package.json index 981304e..81088a8 100644 --- a/codemods/remove-memoization/package.json +++ b/codemods/remove-memoization/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "@types/node": "latest", "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/remove-memoization/scripts/codemod.ts b/codemods/remove-memoization/scripts/codemod.ts index 3e15c0a..b19ad81 100644 --- a/codemods/remove-memoization/scripts/codemod.ts +++ b/codemods/remove-memoization/scripts/codemod.ts @@ -1,8 +1,10 @@ import type { Edit, SgNode, Transform } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { getAllImports, getImport, removeImport } from "@jssg/utils/javascript/imports"; const MEMOIZATION_IMPORTS = new Set(["useMemo", "useCallback", "memo"]); +const MEMOIZATION_NAMES = ["useMemo", "useCallback", "memo"] as const; const IDENTIFIER_KINDS = ["identifier", "type_identifier"] as const; type Range = { @@ -10,21 +12,6 @@ type Range = { end: number; }; -type ImportSpecifierInfo = { - node: SgNode; - localName: string; - importedName: string; - kind: "default" | "namespace" | "named"; -}; - -type ReactImportInfo = { - importNode: SgNode; - sourceNode: SgNode; - defaultSpecifier: ImportSpecifierInfo | null; - namespaceSpecifier: ImportSpecifierInfo | null; - namedSpecifiers: ImportSpecifierInfo[]; -}; - type MemoizationCall = { call: SgNode; importedName: string; @@ -43,21 +30,6 @@ function namedChildren(node: SgNode | null): SgNode[] { return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); } -function sourceText(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) { - return fragment.text(); - } - - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; -} - -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? sourceText(source) : null; -} - function callArguments(call: SgNode): SgNode[] { return namedChildren(call.field("arguments")); } @@ -78,75 +50,15 @@ function containsIndex(outer: Range, index: number): boolean { return outer.start <= index && index < outer.end; } -function sourceQuote(sourceNode: SgNode): string { - return sourceNode.text().startsWith("'") ? "'" : "\""; -} - -function collectReactImports(rootNode: SgNode): ReactImportInfo[] { - const imports: ReactImportInfo[] = []; - - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(importNode) !== "react") { - continue; - } - - const sourceNode = importNode.field("source") ?? importNode.find({ rule: { kind: "string" } }); - if (!sourceNode) { - continue; - } - - const importClause = importNode.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier") ?? null; - const namespaceImport = importClause?.find({ rule: { kind: "namespace_import" } }) ?? null; - const namespaceName = namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }) ?? null; - - const namedSpecifiers = importNode.findAll({ rule: { kind: "import_specifier" } }).flatMap((specifier) => { - const importedName = specifier.field("name")?.text(); - if (!importedName) { - return []; - } - - return [{ - node: specifier, - localName: specifier.field("alias")?.text() ?? importedName, - importedName, - kind: "named" as const, - }]; - }); - - imports.push({ - importNode, - sourceNode, - defaultSpecifier: defaultIdentifier - ? { - node: defaultIdentifier, - localName: defaultIdentifier.text(), - importedName: "default", - kind: "default", - } - : null, - namespaceSpecifier: namespaceName && namespaceImport - ? { - node: namespaceImport, - localName: namespaceName.text(), - importedName: "namespace", - kind: "namespace", - } - : null, - namedSpecifiers, - }); - } - - return imports; -} - -function buildNamedImportMap(reactImports: ReactImportInfo[]): Map { +function collectNamedMemoizationImports( + rootNode: SgNode, +): Map { const namedImports = new Map(); - for (const reactImport of reactImports) { - for (const specifier of reactImport.namedSpecifiers) { - if (MEMOIZATION_IMPORTS.has(specifier.importedName)) { - namedImports.set(specifier.localName, specifier.importedName); + for (const name of MEMOIZATION_NAMES) { + for (const imp of getAllImports(rootNode, { type: "named", name, from: "react" })) { + if (!imp.isNamespace) { + namedImports.set(imp.alias, name); } } } @@ -154,17 +66,11 @@ function buildNamedImportMap(reactImports: ReactImportInfo[]): Map { +function collectReactObjectNames(rootNode: SgNode): Set { const names = new Set(); - for (const reactImport of reactImports) { - if (reactImport.defaultSpecifier) { - names.add(reactImport.defaultSpecifier.localName); - } - - if (reactImport.namespaceSpecifier) { - names.add(reactImport.namespaceSpecifier.localName); - } + for (const imp of getAllImports(rootNode, { type: "default", from: "react" })) { + names.add(imp.alias); } return names; @@ -300,110 +206,175 @@ function collectUsedImportLocals( return usedNames; } -function importStatementReplacement( - reactImport: ReactImportInfo, - usedImportLocals: Set, - sourceCode: string, -): Edit { - const keptNamedSpecifiers = reactImport.namedSpecifiers.filter((specifier) => usedImportLocals.has(specifier.localName)); - const keepDefault = reactImport.defaultSpecifier ? usedImportLocals.has(reactImport.defaultSpecifier.localName) : false; - const keepNamespace = reactImport.namespaceSpecifier ? usedImportLocals.has(reactImport.namespaceSpecifier.localName) : false; +function topLevelSelectedCalls(matches: MemoizationCall[]): MemoizationCall[] { + const allRanges = matches.map((match) => ({ match, range: nodeRange(match.call) })); - const range = nodeRange(reactImport.importNode); - const allRemoved = !keepDefault && !keepNamespace && keptNamedSpecifiers.length === 0; - if (allRemoved) { - return removeImportStatementEdit(sourceCode, range); - } + return allRanges + .filter(({ match, range }) => + !allRanges.some((candidate) => candidate.match.call.id() !== match.call.id() && contains(candidate.range, range)) + ) + .map(({ match }) => match); +} - const clauses: string[] = []; - if (keepDefault && reactImport.defaultSpecifier) { - clauses.push(reactImport.defaultSpecifier.node.text()); - } +function importStatementForBinding(node: SgNode): SgNode | null { + return node.ancestors().find((ancestor) => ancestor.kind() === "import_statement") ?? null; +} + +function namedSpecifierCount(importNode: SgNode): number { + return importNode.findAll({ rule: { kind: "import_specifier" } }).length; +} - if (keepNamespace && reactImport.namespaceSpecifier) { - clauses.push(reactImport.namespaceSpecifier.node.text()); +function defaultOrNamespaceLocal(importNode: SgNode): { alias: string; isNamespace: boolean } | null { + const clause = importNode.find({ rule: { kind: "import_clause" } }); + if (!clause) { + return null; } - if (keptNamedSpecifiers.length > 0) { - clauses.push(`{ ${keptNamedSpecifiers.map((specifier) => specifier.node.text()).join(", ")} }`); + const namespace = clause.find({ rule: { kind: "namespace_import" } }); + const namespaceName = namespace?.field("name") ?? namespace?.find({ rule: { kind: "identifier" } }); + if (namespaceName) { + return { alias: namespaceName.text(), isNamespace: true }; } - const quote = sourceQuote(reactImport.sourceNode); - return { - startPos: range.start, - endPos: range.end, - insertedText: `import ${clauses.join(", ")} from ${quote}react${quote};`, - }; + const defaultIdent = clause.children().find((child) => child.kind() === "identifier"); + return defaultIdent ? { alias: defaultIdent.text(), isNamespace: false } : null; } -function lineStart(source: string, index: number): number { - let cursor = index; - while (cursor > 0 && source[cursor - 1] !== "\n" && source[cursor - 1] !== "\r") { - cursor--; +function consumeFollowingBlankLine(edit: Edit, programText: string): Edit { + if (edit.insertedText !== "" || edit.startPos !== 0) { + return edit; } - return cursor; -} -function consumeLineBreak(source: string, index: number): number { - if (source[index] === "\r" && source[index + 1] === "\n") { - return index + 2; + let cursor = edit.endPos; + while (cursor < programText.length && (programText[cursor] === " " || programText[cursor] === "\t")) { + cursor++; } - if (source[index] === "\n" || source[index] === "\r") { - return index + 1; + if (programText.slice(cursor, cursor + 2) === "\r\n") { + return { ...edit, endPos: cursor + 2 }; } - return index; + if (programText[cursor] === "\n" || programText[cursor] === "\r") { + return { ...edit, endPos: cursor + 1 }; + } + + return edit; } -function consumeBlankLine(source: string, index: number): number { - let cursor = index; - while (cursor < source.length && (source[cursor] === " " || source[cursor] === "\t")) { - cursor++; - } +function cleanupUnusedImports( + rootNode: SgNode, + usedImportLocals: Set, + edits: Edit[], +): void { + const programText = rootNode.text(); + const handledStatements = new Set(); + + for (const name of MEMOIZATION_NAMES) { + for (const imp of getAllImports(rootNode, { type: "named", name, from: "react" })) { + if (imp.isNamespace || usedImportLocals.has(imp.alias)) { + continue; + } - if (source[cursor] !== "\n" && source[cursor] !== "\r") { - return index; - } + const statement = importStatementForBinding(imp.node); + if (!statement || handledStatements.has(statement.id())) { + continue; + } - return consumeLineBreak(source, cursor); -} + const unusedInStatement: string[] = []; + for (const memoName of MEMOIZATION_NAMES) { + for (const sibling of getAllImports(rootNode, { + type: "named", + name: memoName, + from: "react", + })) { + if (sibling.isNamespace || usedImportLocals.has(sibling.alias)) { + continue; + } + if (importStatementForBinding(sibling.node)?.id() === statement.id()) { + unusedInStatement.push(memoName); + } + } + } -function removeImportStatementEdit(source: string, range: Range): Edit { - const start = lineStart(source, range.start); - let end = consumeLineBreak(source, range.end); + const removingAllNamed = unusedInStatement.length >= namedSpecifierCount(statement); + const binding = defaultOrNamespaceLocal(statement); + const keepBinding = binding ? usedImportLocals.has(binding.alias) : false; + + if (removingAllNamed && !keepBinding) { + // Drop the whole statement in one edit (avoids overlapping default+named removals). + const removeEdit = removeImport(rootNode, { + type: "named", + specifiers: unusedInStatement, + from: "react", + }) ?? (binding + ? removeImport(rootNode, { + type: binding.isNamespace ? "namespace" : "default", + from: "react", + }) + : null); + + if (removeEdit) { + edits.push(consumeFollowingBlankLine(removeEdit, programText)); + handledStatements.add(statement.id()); + } + continue; + } - if (start === 0) { - end = consumeBlankLine(source, end); + const removeEdit = removeImport(rootNode, { + type: "named", + specifiers: removingAllNamed ? unusedInStatement : [name], + from: "react", + }); + + if (removeEdit) { + edits.push(consumeFollowingBlankLine(removeEdit, programText)); + if (removingAllNamed) { + handledStatements.add(statement.id()); + } + } + } } - return { - startPos: start, - endPos: end, - insertedText: "", - }; -} + for (const imp of getAllImports(rootNode, { type: "default", from: "react" })) { + if (usedImportLocals.has(imp.alias)) { + continue; + } -function topLevelSelectedCalls(matches: MemoizationCall[]): MemoizationCall[] { - const allRanges = matches.map((match) => ({ match, range: nodeRange(match.call) })); + const statement = importStatementForBinding(imp.node); + if (!statement || handledStatements.has(statement.id())) { + continue; + } - return allRanges - .filter(({ match, range }) => - !allRanges.some((candidate) => candidate.match.call.id() !== match.call.id() && contains(candidate.range, range)) - ) - .map(({ match }) => match); + // Pure default/namespace imports only — mixed clauses with remaining named + // bindings are left to named cleanup above. + if (namedSpecifierCount(statement) > 0) { + continue; + } + + const removeEdit = removeImport(rootNode, { + type: imp.isNamespace ? "namespace" : "default", + from: "react", + }); + if (removeEdit) { + edits.push(consumeFollowingBlankLine(removeEdit, programText)); + handledStatements.add(statement.id()); + } + } } const transform: Transform = async (root) => { const rootNode = root.root(); - const reactImports = collectReactImports(rootNode); - if (reactImports.length === 0) { + + const namedImports = collectNamedMemoizationImports(rootNode); + const reactObjectNames = collectReactObjectNames(rootNode); + if (namedImports.size === 0 && reactObjectNames.size === 0) { return null; } - const namedImports = buildNamedImportMap(reactImports); - const reactObjectNames = buildReactObjectNames(reactImports); - if (namedImports.size === 0 && reactObjectNames.size === 0) { + if ( + namedImports.size === 0 && + !getImport(rootNode, { type: "default", from: "react" }) + ) { return null; } @@ -440,27 +411,19 @@ const transform: Transform = async (root) => { return null; } - const importedLocalNames = new Set(); - for (const reactImport of reactImports) { - if (reactImport.defaultSpecifier) { - importedLocalNames.add(reactImport.defaultSpecifier.localName); - } - if (reactImport.namespaceSpecifier) { - importedLocalNames.add(reactImport.namespaceSpecifier.localName); - } - for (const specifier of reactImport.namedSpecifiers) { - importedLocalNames.add(specifier.localName); - } - } + const importedLocalNames = new Set([ + ...namedImports.keys(), + ...reactObjectNames, + ]); - const usedImportLocals = collectUsedImportLocals(rootNode, importedLocalNames, selectedCalls, selectedCallIds); - const sourceCode = rootNode.text(); - for (const reactImport of reactImports) { - const edit = importStatementReplacement(reactImport, usedImportLocals, sourceCode); - if (edit.insertedText !== reactImport.importNode.text()) { - edits.push(edit); - } - } + const usedImportLocals = collectUsedImportLocals( + rootNode, + importedLocalNames, + selectedCalls, + selectedCallIds, + ); + + cleanupUnusedImports(rootNode, usedImportLocals, edits); for (const [pattern, count] of metricCounts) { metric.increment({ diff --git a/codemods/remove-memoization/tests/no-semicolon-keep-sibling/expected.tsx b/codemods/remove-memoization/tests/no-semicolon-keep-sibling/expected.tsx new file mode 100644 index 0000000..10d1606 --- /dev/null +++ b/codemods/remove-memoization/tests/no-semicolon-keep-sibling/expected.tsx @@ -0,0 +1,3 @@ +import { useState } from "react" +const v = () => 1 +const [s] = useState(0) diff --git a/codemods/remove-memoization/tests/no-semicolon-keep-sibling/input.tsx b/codemods/remove-memoization/tests/no-semicolon-keep-sibling/input.tsx new file mode 100644 index 0000000..48de5cd --- /dev/null +++ b/codemods/remove-memoization/tests/no-semicolon-keep-sibling/input.tsx @@ -0,0 +1,3 @@ +import { useMemo, useState } from "react" +const v = useMemo(() => 1, []) +const [s] = useState(0) diff --git a/codemods/remove-memoization/tests/no-semicolon-keep-sibling/metrics.json b/codemods/remove-memoization/tests/no-semicolon-keep-sibling/metrics.json new file mode 100644 index 0000000..c9f62ed --- /dev/null +++ b/codemods/remove-memoization/tests/no-semicolon-keep-sibling/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/no-semicolon-keep-sibling/input.tsx", + "pattern": "useMemo" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-create-factory/package.json b/codemods/replace-create-factory/package.json index 54182a2..e7781b4 100644 --- a/codemods/replace-create-factory/package.json +++ b/codemods/replace-create-factory/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "@types/node": "latest", "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/replace-create-factory/scripts/codemod.ts b/codemods/replace-create-factory/scripts/codemod.ts index 91ce2d5..a8b4e42 100644 --- a/codemods/replace-create-factory/scripts/codemod.ts +++ b/codemods/replace-create-factory/scripts/codemod.ts @@ -1,6 +1,7 @@ import type { Edit, SgNode, Transform } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { getAllImports, getImport, removeImport } from "@jssg/utils/javascript/imports"; const IDENTIFIER_KINDS = ["identifier", "type_identifier"] as const; @@ -9,20 +10,6 @@ type Range = { end: number; }; -type ImportSpecifierInfo = { - node: SgNode; - localName: string; - importedName: string; -}; - -type ReactImportInfo = { - importNode: SgNode; - sourceNode: SgNode; - defaultSpecifier: ImportSpecifierInfo | null; - namespaceSpecifier: ImportSpecifierInfo | null; - namedSpecifiers: ImportSpecifierInfo[]; -}; - type CreateFactoryCall = { call: SgNode; pattern: "createFactory" | "React.createFactory"; @@ -51,11 +38,6 @@ function sourceText(node: SgNode): string | null { return text.length >= 2 ? text.slice(1, -1) : null; } -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? sourceText(source) : null; -} - function callArguments(call: SgNode): SgNode[] { return namedChildren(call.field("arguments")); } @@ -72,89 +54,29 @@ function containsIndex(outer: Range, index: number): boolean { return outer.start <= index && index < outer.end; } -function sourceQuote(sourceNode: SgNode): string { - return sourceNode.text().startsWith("'") ? "'" : "\""; -} - -function collectReactImports(rootNode: SgNode): ReactImportInfo[] { - const imports: ReactImportInfo[] = []; - - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(importNode) !== "react") { - continue; - } - - const sourceNode = importNode.field("source") ?? importNode.find({ rule: { kind: "string" } }); - if (!sourceNode) { - continue; - } - - const importClause = importNode.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier") ?? null; - const namespaceImport = importClause?.find({ rule: { kind: "namespace_import" } }) ?? null; - const namespaceName = namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }) ?? null; - const namedSpecifiers = importNode.findAll({ rule: { kind: "import_specifier" } }).flatMap((specifier) => { - const importedName = specifier.field("name")?.text(); - if (!importedName) { - return []; - } - - return [{ - node: specifier, - localName: specifier.field("alias")?.text() ?? importedName, - importedName, - }]; - }); - - imports.push({ - importNode, - sourceNode, - defaultSpecifier: defaultIdentifier - ? { - node: defaultIdentifier, - localName: defaultIdentifier.text(), - importedName: "default", - } - : null, - namespaceSpecifier: namespaceName && namespaceImport - ? { - node: namespaceImport, - localName: namespaceName.text(), - importedName: "namespace", - } - : null, - namedSpecifiers, - }); - } - - return imports; -} - -function buildNamedCreateFactoryImports(reactImports: ReactImportInfo[]): Set { +function collectNamedCreateFactoryImports( + rootNode: SgNode, +): Set { const names = new Set(); - for (const reactImport of reactImports) { - for (const specifier of reactImport.namedSpecifiers) { - if (specifier.importedName === "createFactory") { - names.add(specifier.localName); - } + for (const imp of getAllImports(rootNode, { + type: "named", + name: "createFactory", + from: "react", + })) { + if (!imp.isNamespace) { + names.add(imp.alias); } } return names; } -function buildReactObjectNames(reactImports: ReactImportInfo[]): Set { +function collectReactObjectNames(rootNode: SgNode): Set { const names = new Set(); - for (const reactImport of reactImports) { - if (reactImport.defaultSpecifier) { - names.add(reactImport.defaultSpecifier.localName); - } - - if (reactImport.namespaceSpecifier) { - names.add(reactImport.namespaceSpecifier.localName); - } + for (const imp of getAllImports(rootNode, { type: "default", from: "react" })) { + names.add(imp.alias); } return names; @@ -246,99 +168,202 @@ function collectUsedImportLocals( return usedNames; } -function lineStart(source: string, index: number): number { - let cursor = index; - while (cursor > 0 && source[cursor - 1] !== "\n" && source[cursor - 1] !== "\r") { - cursor--; - } - return cursor; +function importStatementForBinding(node: SgNode): SgNode | null { + return node.ancestors().find((ancestor) => ancestor.kind() === "import_statement") ?? null; } -function consumeLineBreak(source: string, index: number): number { - if (source[index] === "\r" && source[index + 1] === "\n") { - return index + 2; +function namedSpecifierCount(importNode: SgNode): number { + return importNode.findAll({ rule: { kind: "import_specifier" } }).length; +} + +function defaultOrNamespaceLocal( + importNode: SgNode, +): { alias: string; isNamespace: boolean } | null { + const clause = importNode.find({ rule: { kind: "import_clause" } }); + if (!clause) { + return null; } - if (source[index] === "\n" || source[index] === "\r") { - return index + 1; + const namespace = clause.find({ rule: { kind: "namespace_import" } }); + const namespaceName = namespace?.field("name") ?? namespace?.find({ rule: { kind: "identifier" } }); + if (namespaceName) { + return { alias: namespaceName.text(), isNamespace: true }; } - return index; + const defaultIdent = clause.children().find((child) => child.kind() === "identifier"); + return defaultIdent ? { alias: defaultIdent.text(), isNamespace: false } : null; } -function consumeBlankLine(source: string, index: number): number { - let cursor = index; - while (cursor < source.length && (source[cursor] === " " || source[cursor] === "\t")) { +function consumeFollowingBlankLine(edit: Edit, programText: string): Edit { + if (edit.insertedText !== "" || edit.startPos !== 0) { + return edit; + } + + let cursor = edit.endPos; + while (cursor < programText.length && (programText[cursor] === " " || programText[cursor] === "\t")) { cursor++; } - if (source[cursor] !== "\n" && source[cursor] !== "\r") { - return index; + if (programText.slice(cursor, cursor + 2) === "\r\n") { + return { ...edit, endPos: cursor + 2 }; + } + + if (programText[cursor] === "\n" || programText[cursor] === "\r") { + return { ...edit, endPos: cursor + 1 }; } - return consumeLineBreak(source, cursor); + return edit; +} + +function isWhitespace(char: string | undefined): boolean { + return char === " " || char === "\t" || char === "\n" || char === "\r"; } -function removeImportStatementEdit(source: string, range: Range): Edit { - const start = lineStart(source, range.start); - let end = consumeLineBreak(source, range.end); +function removeSpecifierEdit(specifier: SgNode, programText: string): Edit { + let start = specifier.range().start.index; + let end = specifier.range().end.index; - if (start === 0) { - end = consumeBlankLine(source, end); + let i = end; + while (isWhitespace(programText[i])) i++; + if (programText[i] === ",") { + end = i + 1; + while (isWhitespace(programText[end])) end++; + } else { + let j = start - 1; + while (isWhitespace(programText[j])) j--; + if (programText[j] === ",") start = j; } - return { - startPos: start, - endPos: end, - insertedText: "", - }; + return { startPos: start, endPos: end, insertedText: "" }; } -function importStatementReplacement( - reactImport: ReactImportInfo, - usedImportLocals: Set, - sourceCode: string, -): Edit { - const keptNamedSpecifiers = reactImport.namedSpecifiers.filter((specifier) => usedImportLocals.has(specifier.localName)); - const keepDefault = reactImport.defaultSpecifier ? usedImportLocals.has(reactImport.defaultSpecifier.localName) : false; - const keepNamespace = reactImport.namespaceSpecifier ? usedImportLocals.has(reactImport.namespaceSpecifier.localName) : false; - - const range = nodeRange(reactImport.importNode); - if (!keepDefault && !keepNamespace && keptNamedSpecifiers.length === 0) { - return removeImportStatementEdit(sourceCode, range); +function findCreateFactorySpecifier( + statement: SgNode, + localAlias: string, +): SgNode | null { + for (const specifier of statement.findAll({ rule: { kind: "import_specifier" } })) { + const imported = specifier.field("name")?.text(); + if (imported !== "createFactory") { + continue; + } + const local = specifier.field("alias")?.text() ?? imported; + if (local === localAlias) { + return specifier; + } } + return null; +} - const clauses: string[] = []; - if (keepDefault && reactImport.defaultSpecifier) { - clauses.push(reactImport.defaultSpecifier.node.text()); - } +function cleanupUnusedImports( + rootNode: SgNode, + usedImportLocals: Set, + edits: Edit[], +): void { + const programText = rootNode.text(); + const unusedCreateFactory = getAllImports(rootNode, { + type: "named", + name: "createFactory", + from: "react", + }).filter((imp) => !imp.isNamespace && !usedImportLocals.has(imp.alias)); + + // removeImport only clears the first matching specifier in the file, so when + // createFactory appears in multiple react imports we remove later ones via + // their specifier nodes directly. + let usedRemoveImport = false; + + for (const imp of unusedCreateFactory) { + const statement = importStatementForBinding(imp.node); + if (!statement) { + continue; + } - if (keepNamespace && reactImport.namespaceSpecifier) { - clauses.push(reactImport.namespaceSpecifier.node.text()); - } + const binding = defaultOrNamespaceLocal(statement); + const keepBinding = binding ? usedImportLocals.has(binding.alias) : false; + const onlyCreateFactory = namedSpecifierCount(statement) === 1; + + if (onlyCreateFactory && !keepBinding) { + if (!usedRemoveImport) { + const removeEdit = removeImport(rootNode, { + type: "named", + specifiers: ["createFactory"], + from: "react", + }); + if (removeEdit) { + edits.push(consumeFollowingBlankLine(removeEdit, programText)); + usedRemoveImport = true; + continue; + } + } + + const range = statement.range(); + let end = range.end.index; + if (programText.slice(end, end + 2) === "\r\n") end += 2; + else if (programText[end] === "\n" || programText[end] === "\r") end++; + edits.push(consumeFollowingBlankLine( + { startPos: range.start.index, endPos: end, insertedText: "" }, + programText, + )); + continue; + } - if (keptNamedSpecifiers.length > 0) { - clauses.push(`{ ${keptNamedSpecifiers.map((specifier) => specifier.node.text()).join(", ")} }`); + if (!usedRemoveImport) { + const removeEdit = removeImport(rootNode, { + type: "named", + specifiers: ["createFactory"], + from: "react", + }); + if (removeEdit) { + const stmtRange = nodeRange(statement); + if ( + removeEdit.startPos >= stmtRange.start && + removeEdit.endPos <= stmtRange.end + ) { + edits.push(removeEdit); + usedRemoveImport = true; + continue; + } + } + } + + const specifier = findCreateFactorySpecifier(statement, imp.alias); + if (specifier) { + edits.push(removeSpecifierEdit(specifier, programText)); + } } - const quote = sourceQuote(reactImport.sourceNode); - return { - startPos: range.start, - endPos: range.end, - insertedText: `import ${clauses.join(", ")} from ${quote}react${quote};`, - }; + for (const imp of getAllImports(rootNode, { type: "default", from: "react" })) { + if (usedImportLocals.has(imp.alias)) { + continue; + } + + const statement = importStatementForBinding(imp.node); + if (!statement || namedSpecifierCount(statement) > 0) { + continue; + } + + const removeEdit = removeImport(rootNode, { + type: imp.isNamespace ? "namespace" : "default", + from: "react", + }); + if (removeEdit) { + edits.push(consumeFollowingBlankLine(removeEdit, programText)); + } + } } const transform: Transform = async (root) => { const rootNode = root.root(); - const reactImports = collectReactImports(rootNode); - if (reactImports.length === 0) { + + const namedImports = collectNamedCreateFactoryImports(rootNode); + const reactObjectNames = collectReactObjectNames(rootNode); + if (namedImports.size === 0 && reactObjectNames.size === 0) { return null; } - const namedImports = buildNamedCreateFactoryImports(reactImports); - const reactObjectNames = buildReactObjectNames(reactImports); - if (namedImports.size === 0 && reactObjectNames.size === 0) { + if ( + namedImports.size === 0 && + !getImport(rootNode, { type: "default", from: "react" }) + ) { return null; } @@ -373,27 +398,13 @@ const transform: Transform = async (root) => { metricCounts.set(match.pattern, (metricCounts.get(match.pattern) ?? 0) + 1); } - const importedLocalNames = new Set(); - for (const reactImport of reactImports) { - if (reactImport.defaultSpecifier) { - importedLocalNames.add(reactImport.defaultSpecifier.localName); - } - if (reactImport.namespaceSpecifier) { - importedLocalNames.add(reactImport.namespaceSpecifier.localName); - } - for (const specifier of reactImport.namedSpecifiers) { - importedLocalNames.add(specifier.localName); - } - } + const importedLocalNames = new Set([ + ...namedImports, + ...reactObjectNames, + ]); const usedImportLocals = collectUsedImportLocals(rootNode, importedLocalNames, selectedCalls); - const sourceCode = rootNode.text(); - for (const reactImport of reactImports) { - const edit = importStatementReplacement(reactImport, usedImportLocals, sourceCode); - if (edit.insertedText !== reactImport.importNode.text()) { - edits.push(edit); - } - } + cleanupUnusedImports(rootNode, usedImportLocals, edits); for (const [pattern, count] of metricCounts) { metric.increment({ diff --git a/codemods/replace-create-factory/tests/named-with-sibling/expected.tsx b/codemods/replace-create-factory/tests/named-with-sibling/expected.tsx new file mode 100644 index 0000000..942d922 --- /dev/null +++ b/codemods/replace-create-factory/tests/named-with-sibling/expected.tsx @@ -0,0 +1,4 @@ +import { useState } from "react"; + +const route = ; +const [count] = useState(0); diff --git a/codemods/replace-create-factory/tests/named-with-sibling/input.tsx b/codemods/replace-create-factory/tests/named-with-sibling/input.tsx new file mode 100644 index 0000000..0bc12fd --- /dev/null +++ b/codemods/replace-create-factory/tests/named-with-sibling/input.tsx @@ -0,0 +1,4 @@ +import { createFactory, useState } from "react"; + +const route = createFactory(Route); +const [count] = useState(0); diff --git a/codemods/replace-create-factory/tests/named-with-sibling/metrics.json b/codemods/replace-create-factory/tests/named-with-sibling/metrics.json new file mode 100644 index 0000000..7d3abfb --- /dev/null +++ b/codemods/replace-create-factory/tests/named-with-sibling/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-create-factory-replacements": [ + { + "cardinality": { + "file": "tests/named-with-sibling/input.tsx", + "pattern": "createFactory" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/package.json b/codemods/update-react-imports/package.json index 5aadad1..838f574 100644 --- a/codemods/update-react-imports/package.json +++ b/codemods/update-react-imports/package.json @@ -13,6 +13,6 @@ "@types/node": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/update-react-imports/scripts/codemod.ts b/codemods/update-react-imports/scripts/codemod.ts index d1e58da..822fb64 100644 --- a/codemods/update-react-imports/scripts/codemod.ts +++ b/codemods/update-react-imports/scripts/codemod.ts @@ -1,6 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { getImport, removeImport } from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -9,10 +10,6 @@ function metricFile(filename: string): string { type Specifier = { name: string; alias?: string; isType: boolean }; -function isWhitespace(char: string | undefined): boolean { - return char === " " || char === "\t" || char === "\n" || char === "\r"; -} - function sourceText(node: SgNode): string | null { const fragment = node.find({ rule: { kind: "string_fragment" } }); if (fragment) return fragment.text(); @@ -139,22 +136,37 @@ function removeReactImportEdits(imports: SgNode[], source: string): Edit[] }); } +function reactModuleFromImport(importNode: SgNode): "react" | "React" { + return importSource(importNode) === "React" ? "React" : "react"; +} + const transform: Transform = async (root, options) => { const rootNode = root.root(); const source = rootNode.text(); const metric = useMetricAtom("react-import-updates"); const edits: Edit[] = []; + // Discover React default/namespace bindings via utils (namespace is returned as + // the default-query fallback when no default binding exists). + const reactBinding = + getImport(rootNode, { type: "default", from: "react" }) ?? + getImport(rootNode, { type: "default", from: "React" }); + const reactImports = rootNode.findAll({ rule: { kind: "import_statement" } }).filter(isReactImport); const valueReactImports = reactImports.filter((imp) => !imp.text().startsWith("import type") && getReactSpecifier(imp) !== null - ); if (valueReactImports.length === 0) return null; + // This codemod only rewrites the canonical `React` binding. + if (reactBinding && reactBinding.alias !== "React") { + return null; + } + const primaryReactImport = valueReactImports[0]!; const reactSpecifierKind = getReactSpecifier(primaryReactImport)!; const quote = quoteFor(primaryReactImport); + const reactModule = reactModuleFromImport(primaryReactImport); const allNamedFromReactImports = reactImports.flatMap(namedSpecifiers); const allExistingNamed = valueReactImports.flatMap(namedSpecifiers); const reactIdentifiers = rootNode.findAll({ rule: { kind: "identifier", regex: "^React$" } }) @@ -212,6 +224,43 @@ const transform: Transform = async (root, options) => { regularSpecs.push(...splitExisting.regular); typeSpecs.push(...splitExisting.type); + // Unused pure default React import — removeImport preserves quote/semicolon style + // of surrounding code by deleting the statement rather than rebuilding it. + if ( + !isReactUsed && + !shouldDestructure && + allExistingNamed.length === 0 && + valueReactImports.length === 1 && + reactSpecifierKind === "default" + ) { + const removeEdit = removeImport(rootNode, { + type: "default", + from: reactModule, + }); + if (removeEdit) { + let edit = removeEdit; + // Match prior top-of-file cleanup: drop one following blank line. + if (edit.insertedText === "" && edit.startPos === 0) { + let cursor = edit.endPos; + while (cursor < source.length && (source[cursor] === " " || source[cursor] === "\t")) { + cursor++; + } + if (source.slice(cursor, cursor + 2) === "\r\n") { + edit = { ...edit, endPos: cursor + 2 }; + } else if (source[cursor] === "\n" || source[cursor] === "\r") { + edit = { ...edit, endPos: cursor + 1 }; + } + } + edits.push(edit); + metric.increment({ + action: "remove-react-import", + file: metricFile(root.filename()), + previous: reactSpecifierKind, + }); + return rootNode.commitEdits(edits); + } + } + let replacementImports = ""; if (shouldDestructure) { edits.push(...memberReplacementEdits); From bbdaba9d4211f1856e72de1748c399e23221b597 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Oct 2026 21:42:08 +0000 Subject: [PATCH 04/10] Adopt @jssg/utils import helpers in remaining React JSSG codemods Wire getImport/getAllImports/addImport/removeImport/updateImport into use-browser, remove-legacy-context, pure-component, react-native-view-prop-types, react-to-react-dom (partial), and sort-comp. Bump leftover @jssg/utils ^0.0.2 deps to ^0.0.10 and guard remove-forward-ref/replace-string-ref against getImport namespace fallbacks. Add fixtures for single-quote merge, CJS React, aliased View/findDOMNode, multi-specifier cleanup, and aliased React namespace sort-comp. Co-authored-by: Alex Bit --- codemods/pure-component/package.json | 3 + codemods/pure-component/scripts/codemod.ts | 164 ++++------ .../multi-specifier-keep-sibling/expected.tsx | 8 + .../multi-specifier-keep-sibling/input.tsx | 10 + .../multi-specifier-keep-sibling/metrics.json | 10 + .../react-native-named-import/expected.tsx | 1 - .../react-native-view-prop-types/package.json | 2 +- .../scripts/codemod.ts | 291 +++++------------- .../tests/aliased-view-import/expected.tsx | 12 + .../tests/aliased-view-import/input.tsx | 12 + .../tests/aliased-view-import/metrics.json | 10 + .../expected.tsx | 7 +- codemods/react-to-react-dom/package.json | 3 + .../react-to-react-dom/scripts/codemod.ts | 80 +++-- .../import-aliased-find-dom-node/expected.tsx | 3 + .../import-aliased-find-dom-node/input.tsx | 3 + .../import-aliased-find-dom-node/metrics.json | 12 + codemods/remove-forward-ref/package.json | 2 +- .../remove-forward-ref/scripts/codemod.ts | 4 +- codemods/remove-legacy-context/package.json | 3 + .../remove-legacy-context/scripts/codemod.ts | 83 ++--- .../tests/cjs-require-react/expected.tsx | 12 + .../tests/cjs-require-react/input.tsx | 16 + .../tests/cjs-require-react/metrics.json | 10 + codemods/replace-string-ref/package.json | 2 +- .../replace-string-ref/scripts/codemod.ts | 5 +- codemods/sort-comp/package.json | 3 + codemods/sort-comp/scripts/codemod.ts | 58 ++-- .../aliased-namespace-react/expected.tsx | 13 + .../tests/aliased-namespace-react/input.tsx | 13 + .../aliased-namespace-react/metrics.json | 11 + codemods/use-browser/package.json | 3 + codemods/use-browser/scripts/codemod.ts | 241 +++++++-------- .../arrow-component-with-fc-type/expected.tsx | 2 +- .../destructured-props-function/expected.tsx | 2 +- .../expected.tsx | 2 +- .../tests/single-quote-dom-merge/expected.tsx | 15 + .../tests/single-quote-dom-merge/input.tsx | 11 + .../tests/single-quote-dom-merge/metrics.json | 10 + pnpm-lock.yaml | 4 - 40 files changed, 583 insertions(+), 573 deletions(-) create mode 100644 codemods/pure-component/tests/multi-specifier-keep-sibling/expected.tsx create mode 100644 codemods/pure-component/tests/multi-specifier-keep-sibling/input.tsx create mode 100644 codemods/pure-component/tests/multi-specifier-keep-sibling/metrics.json create mode 100644 codemods/react-native-view-prop-types/tests/aliased-view-import/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/aliased-view-import/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/aliased-view-import/metrics.json create mode 100644 codemods/react-to-react-dom/tests/import-aliased-find-dom-node/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/import-aliased-find-dom-node/input.tsx create mode 100644 codemods/react-to-react-dom/tests/import-aliased-find-dom-node/metrics.json create mode 100644 codemods/remove-legacy-context/tests/cjs-require-react/expected.tsx create mode 100644 codemods/remove-legacy-context/tests/cjs-require-react/input.tsx create mode 100644 codemods/remove-legacy-context/tests/cjs-require-react/metrics.json create mode 100644 codemods/sort-comp/tests/aliased-namespace-react/expected.tsx create mode 100644 codemods/sort-comp/tests/aliased-namespace-react/input.tsx create mode 100644 codemods/sort-comp/tests/aliased-namespace-react/metrics.json create mode 100644 codemods/use-browser/tests/single-quote-dom-merge/expected.tsx create mode 100644 codemods/use-browser/tests/single-quote-dom-merge/input.tsx create mode 100644 codemods/use-browser/tests/single-quote-dom-merge/metrics.json diff --git a/codemods/pure-component/package.json b/codemods/pure-component/package.json index 839f620..99e9291 100644 --- a/codemods/pure-component/package.json +++ b/codemods/pure-component/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "@types/node": "latest", "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/pure-component/scripts/codemod.ts b/codemods/pure-component/scripts/codemod.ts index c698c29..a8b8895 100644 --- a/codemods/pure-component/scripts/codemod.ts +++ b/codemods/pure-component/scripts/codemod.ts @@ -1,16 +1,24 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import { parse } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { getAllImports, getImport, removeImport } from "@jssg/utils/javascript/imports"; + +type RemovableNamed = { + exportName: string; + alias: string; + from: string; +}; type ReactImportAliases = { component: Set; pureComponent: Set; - removableNamed: Set; + removableNamed: RemovableNamed[]; }; type ReactSuperclassKind = "Component" | "PureComponent"; -const REACT_SOURCES = new Set(["react", "React", "react/addons", "react-native"]); +const REACT_SOURCES = ["react", "React", "react/addons", "react-native"] as const; function truthyParam(value: unknown): boolean { return value === true || value === "true"; @@ -26,57 +34,37 @@ function namedChildren(node: SgNode | null): SgNode[] { return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); } -function sourceText(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) return fragment.text(); - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; -} - -function importSource(importStmt: SgNode): SgNode | null { - return importStmt.field("source") ?? importStmt.find({ rule: { kind: "string" } }); -} - -function isReactSource(importStmt: SgNode): boolean { - const source = importSource(importStmt); - const value = source ? sourceText(source) : null; - return value !== null && REACT_SOURCES.has(value); -} - -function reactImportAliases(rootNode: SgNode): ReactImportAliases { +function reactImportAliases(rootNode: SgNode): ReactImportAliases { const component = new Set(); const pureComponent = new Set(); - const removableNamed = new Set(); - - for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (!isReactSource(importStmt)) continue; - - for (const specifier of importStmt.findAll({ rule: { kind: "import_specifier" } })) { - const imported = specifier.field("name")?.text(); - const aliasNode = specifier.field("alias"); - const alias = aliasNode?.text() ?? imported; - if (!imported || !alias) continue; - - if (imported === "Component") { - component.add(alias); - } else if (imported === "PureComponent") { - pureComponent.add(alias); - } else { - continue; + const removableNamed: RemovableNamed[] = []; + + for (const from of REACT_SOURCES) { + for (const exportName of ["Component", "PureComponent"] as const) { + for (const imp of getAllImports(rootNode, { type: "named", name: exportName, from })) { + if (imp.isNamespace) continue; + if (exportName === "Component") component.add(imp.alias); + else pureComponent.add(imp.alias); + + // Legacy removes an import specifier by searching for its `imported` name + // in the rest of the file. For `{ Component as C }`, imported=Component but + // the class extends `C`; the search finds no `Component` identifier and the + // specifier gets kept (legacy quirk). Mirror that: only flag as removable + // when the local name equals the imported name. + if (imp.alias === exportName) { + removableNamed.push({ exportName, alias: imp.alias, from }); + } } - - // Legacy removes an import specifier by searching for its `imported` name - // in the rest of the file. For `{ Component as C }`, imported=Component but - // the class extends `C`; the search finds no `Component` identifier and the - // specifier gets kept (legacy quirk). Mirror that: only flag as removable - // when the local name equals the imported name. - if (!aliasNode) removableNamed.add(alias); } } return { component, pureComponent, removableNamed }; } +function parseProgram(source: string): SgNode { + return parse("tsx", source).root(); +} + function reactSuperclassKind( classDecl: SgNode, aliases: ReactImportAliases, @@ -429,58 +417,6 @@ function remainingNamedImportUse( }); } -function buildImportSpecifierEdits( - importStmt: SgNode, - aliasesToDrop: Set, - source: string, -): Edit[] { - const named = importStmt.find({ rule: { kind: "named_imports" } }); - if (!named) return []; - - const specifiers = named.findAll({ rule: { kind: "import_specifier" } }); - const toKeep = specifiers.filter((s) => { - const local = s.field("alias")?.text() ?? s.field("name")?.text() ?? ""; - return !aliasesToDrop.has(local); - }); - - if (toKeep.length === specifiers.length) return []; - - const clause = importStmt.find({ rule: { kind: "import_clause" } }); - const defaultImport = clause - ? namedChildren(clause).find((c) => c.kind() === "identifier") - : null; - - if (toKeep.length === 0) { - if (defaultImport) { - return [{ - startPos: clause!.range().start.index, - endPos: clause!.range().end.index, - insertedText: defaultImport.text(), - }]; - } - // Legacy uses ImportSpecifier.remove() which drops only the specifier and - // leaves `import 'source';` as a side-effect import. Match that by - // replacing the import_clause (plus the trailing `from`) with nothing. - const sourceNode = importSource(importStmt); - if (!clause || !sourceNode) { - const range = lineDeletionRange(importStmt, source); - return [{ startPos: range.start, endPos: range.end, insertedText: "" }]; - } - return [{ - startPos: clause.range().start.index, - endPos: sourceNode.range().start.index, - insertedText: "", - }]; - } - - const newSpecifiers = toKeep.map((s) => s.text()).join(", "); - return [{ - startPos: named.range().start.index, - endPos: named.range().end.index, - insertedText: `{ ${newSpecifiers} }`, - }]; -} - const transform: Transform = async (root, options) => { const rootNode = root.root(); const source = rootNode.text(); @@ -562,22 +498,40 @@ const transform: Transform = async (root, options) => { if (edits.length === 0) return null; - const aliasesToDrop = new Set(); - for (const alias of aliases.removableNamed) { - if (!remainingNamedImportUse(rootNode, alias, transformedClassIds)) { - aliasesToDrop.add(alias); + const toDropByFrom = new Map(); + for (const entry of aliases.removableNamed) { + if (!remainingNamedImportUse(rootNode, entry.alias, transformedClassIds)) { + const list = toDropByFrom.get(entry.from) ?? []; + list.push(entry.exportName); + toDropByFrom.set(entry.from, list); } } - if (aliasesToDrop.size > 0) { - for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (!isReactSource(importStmt)) continue; - edits.push(...buildImportSpecifierEdits(importStmt, aliasesToDrop, source)); + let next = rootNode.commitEdits(edits); + if (toDropByFrom.size > 0) { + let program = parseProgram(next); + for (const [from, specifiers] of toDropByFrom) { + // removeImport drops one remaining named specifier per call when siblings stay. + while (specifiers.length > 0) { + const stillPresent = specifiers.filter((name) => { + const imp = getImport(program, { type: "named", name, from }); + return !!imp && !imp.isNamespace && imp.alias === name; + }); + if (stillPresent.length === 0) break; + const removeEdit = removeImport(program, { + type: "named", + specifiers: stillPresent, + from, + }); + if (!removeEdit) break; + next = program.commitEdits([removeEdit]); + program = parseProgram(next); + } } } metric.increment({ file: metricFile(root.filename()) }, transformedClassIds.size); - return rootNode.commitEdits(edits); + return next; }; export default transform; diff --git a/codemods/pure-component/tests/multi-specifier-keep-sibling/expected.tsx b/codemods/pure-component/tests/multi-specifier-keep-sibling/expected.tsx new file mode 100644 index 0000000..cc07de8 --- /dev/null +++ b/codemods/pure-component/tests/multi-specifier-keep-sibling/expected.tsx @@ -0,0 +1,8 @@ +import React, { useState } from 'react'; + +function Pure(props) { return
; } + +export function Counter() { + const [n, setN] = useState(0); + return ; +} diff --git a/codemods/pure-component/tests/multi-specifier-keep-sibling/input.tsx b/codemods/pure-component/tests/multi-specifier-keep-sibling/input.tsx new file mode 100644 index 0000000..d14aa39 --- /dev/null +++ b/codemods/pure-component/tests/multi-specifier-keep-sibling/input.tsx @@ -0,0 +1,10 @@ +import React, { Component, useState } from 'react'; + +class Pure extends Component { + render() { return
; } +} + +export function Counter() { + const [n, setN] = useState(0); + return ; +} diff --git a/codemods/pure-component/tests/multi-specifier-keep-sibling/metrics.json b/codemods/pure-component/tests/multi-specifier-keep-sibling/metrics.json new file mode 100644 index 0000000..37d0edd --- /dev/null +++ b/codemods/pure-component/tests/multi-specifier-keep-sibling/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/multi-specifier-keep-sibling/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-component/tests/react-native-named-import/expected.tsx b/codemods/pure-component/tests/react-native-named-import/expected.tsx index 6901793..6e6273b 100644 --- a/codemods/pure-component/tests/react-native-named-import/expected.tsx +++ b/codemods/pure-component/tests/react-native-named-import/expected.tsx @@ -1,3 +1,2 @@ -import 'react-native'; function Pure(props) { return
; } diff --git a/codemods/react-native-view-prop-types/package.json b/codemods/react-native-view-prop-types/package.json index 5605cd4..4432f71 100644 --- a/codemods/react-native-view-prop-types/package.json +++ b/codemods/react-native-view-prop-types/package.json @@ -13,6 +13,6 @@ "typescript": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/react-native-view-prop-types/scripts/codemod.ts b/codemods/react-native-view-prop-types/scripts/codemod.ts index cfd3184..7b35f7c 100644 --- a/codemods/react-native-view-prop-types/scripts/codemod.ts +++ b/codemods/react-native-view-prop-types/scripts/codemod.ts @@ -1,47 +1,18 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { + addImport, + getImport, + updateImport, +} from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; } -function namedChildren(node: SgNode | null): SgNode[] { - if (!node) { - return []; - } - - return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); -} - -function callArguments(call: SgNode): SgNode[] { - return namedChildren(call.field("arguments")); -} - -function stringValue(node: SgNode): string { - const evaluated = Function(`"use strict"; return (${node.text()});`)() as unknown; - if (typeof evaluated !== "string") { - throw new Error(`Expected string literal, got "${typeof evaluated}"`); - } - - return evaluated; -} - -function isRequireCall(node: SgNode | null, source: string): boolean { - if (!node || node.kind() !== "call_expression") { - return false; - } - - const callee = node.field("function"); - const firstArg = callArguments(node)[0]; - return callee?.kind() === "identifier" && - callee.text() === "require" && - firstArg?.kind() === "string" && - stringValue(firstArg) === source; -} - -function isViewPropTypesAccess(node: SgNode): boolean { +function isViewPropTypesAccess(node: SgNode, viewAlias: string): boolean { if (node.kind() !== "member_expression") { return false; } @@ -49,13 +20,13 @@ function isViewPropTypesAccess(node: SgNode): boolean { const object = node.field("object"); const property = node.field("property"); return object?.kind() === "identifier" && - object.text() === "View" && + object.text() === viewAlias && property?.kind() === "property_identifier" && property.text() === "propTypes"; } -function isViewBindingNode(node: SgNode): boolean { - if (node.text() !== "View") { +function isViewBindingNode(node: SgNode, viewAlias: string): boolean { + if (node.text() !== viewAlias) { return false; } @@ -92,141 +63,51 @@ function isViewBindingNode(node: SgNode): boolean { return true; } -function findHasteImportStatement(rootNode: SgNode): SgNode | null { - for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { - const source = importStmt.field("source"); - if (source?.kind() === "string" && stringValue(source) === "View") { - return importStmt; - } - } - - return null; -} - -function findHasteRequireStatement(rootNode: SgNode): SgNode | null { - for (const declaration of rootNode.findAll({ - rule: { - any: [ - { pattern: "const View = require('View')" }, - { pattern: "let View = require('View')" }, - { pattern: "var View = require('View')" }, - ], - }, - })) { - if (declaration.kind() === "lexical_declaration" || declaration.kind() === "variable_declaration") { - return declaration; - } - } - - return null; -} +type ViewBinding = { + alias: string; + from: "View" | "react-native"; + moduleType: "esm" | "cjs"; + isNamespace: boolean; +}; -function findReactNativeImportStatement(rootNode: SgNode): SgNode | null { - for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { - const source = importStmt.field("source"); - if (source?.kind() === "string" && stringValue(source) === "react-native") { - const hasView = importStmt.findAll({ rule: { kind: "import_specifier" } }) - .some((specifier) => specifier.field("name")?.text() === "View"); - if (hasView) { - return importStmt; - } - } +function resolveViewBinding(rootNode: SgNode): ViewBinding | null { + const haste = getImport(rootNode, { type: "default", from: "View" }); + if (haste && !haste.isNamespace) { + return { + alias: haste.alias, + from: "View", + moduleType: haste.moduleType, + isNamespace: false, + }; } - return null; -} - -function findReactNativeRequireStatement(rootNode: SgNode): SgNode | null { - for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - const name = declarator.field("name"); - const value = declarator.field("value"); - if (name?.kind() !== "object_pattern" || !isRequireCall(value, "react-native")) { - continue; - } - - const hasView = name.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } }) - .some((property) => property.text() === "View"); - if (hasView) { - return declarator.ancestors().find((ancestor) => - ancestor.kind() === "lexical_declaration" || ancestor.kind() === "variable_declaration" - ) ?? null; - } + const rn = getImport(rootNode, { type: "named", name: "View", from: "react-native" }); + if (rn && !rn.isNamespace) { + return { + alias: rn.alias, + from: "react-native", + moduleType: rn.moduleType, + isNamespace: false, + }; } return null; } -function viewImportSpecifier(importStmt: SgNode): SgNode | null { - return importStmt.findAll({ rule: { kind: "import_specifier" } }) - .find((specifier) => specifier.field("name")?.text() === "View") ?? null; -} - -function viewRequireProperty(statement: SgNode): SgNode | null { - return statement.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } }) - .find((property) => property.text() === "View") ?? null; -} - -function hasImportSpecifier(importStmt: SgNode, name: string): boolean { - return importStmt.findAll({ rule: { kind: "import_specifier" } }) - .some((specifier) => specifier.field("name")?.text() === name); -} - -function hasRequireProperty(statement: SgNode, name: string): boolean { - return statement.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } }) - .some((property) => property.text() === name); -} - -function hasHasteViewPropTypesImport(rootNode: SgNode): boolean { - return rootNode.findAll({ rule: { kind: "import_statement" } }) - .some((importStmt) => { - const source = importStmt.field("source"); - return source?.kind() === "string" && - stringValue(source) === "ViewPropTypes"; - }); -} - -function hasHasteViewPropTypesRequire(rootNode: SgNode): boolean { - return rootNode.findAll({ - rule: { - any: [ - { pattern: "const ViewPropTypes = require('ViewPropTypes')" }, - { pattern: "let ViewPropTypes = require('ViewPropTypes')" }, - { pattern: "var ViewPropTypes = require('ViewPropTypes')" }, - ], - }, - }).length > 0; -} - -function statementInsertionIndex(source: string, node: SgNode): number { - let index = node.range().end.index; - while (index < source.length && (source[index] === ";" || source[index] === "\r" || source[index] === "\n")) { - index++; - } - return index; -} - const transform: Transform = async (root) => { const rootNode = root.root(); - const source = rootNode.text(); const edits: Edit[] = []; const metric = useMetricAtom("react-native-view-prop-types-replacements"); - const hasteImportStatement = findHasteImportStatement(rootNode); - const hasteRequireStatement = findHasteRequireStatement(rootNode); - const reactNativeImportStatement = findReactNativeImportStatement(rootNode); - const reactNativeRequireStatement = findReactNativeRequireStatement(rootNode); - const hasExistingHasteViewPropTypesImport = hasHasteViewPropTypesImport(rootNode); - const hasExistingHasteViewPropTypesRequire = hasHasteViewPropTypesRequire(rootNode); - const hasExistingReactNativeImportViewPropTypes = reactNativeImportStatement - ? hasImportSpecifier(reactNativeImportStatement, "ViewPropTypes") - : false; - const hasExistingReactNativeRequireViewPropTypes = reactNativeRequireStatement - ? hasRequireProperty(reactNativeRequireStatement, "ViewPropTypes") - : false; + const viewBinding = resolveViewBinding(rootNode); + if (!viewBinding) { + return null; + } + const viewAlias = viewBinding.alias; let replacements = 0; for (const member of rootNode.findAll({ rule: { kind: "member_expression" } })) { - if (!isViewPropTypesAccess(member)) { + if (!isViewPropTypesAccess(member, viewAlias)) { continue; } @@ -245,61 +126,55 @@ const transform: Transform = async (root) => { { kind: "property_identifier" }, ], }, - }).some(isViewBindingNode); - - if (hasteImportStatement) { - if (hasExistingHasteViewPropTypesImport) { - // Keep the existing import intact; the file already binds ViewPropTypes. + }).some((node) => isViewBindingNode(node, viewAlias)); + + const existingViewPropTypes = + getImport(rootNode, { type: "default", from: "ViewPropTypes" }) ?? + getImport(rootNode, { type: "named", name: "ViewPropTypes", from: "react-native" }); + + if (!existingViewPropTypes || existingViewPropTypes.isNamespace) { + if (viewBinding.from === "View") { + if (keepViewBinding) { + const addEdit = addImport(rootNode, { + type: "default", + name: "ViewPropTypes", + from: "ViewPropTypes", + moduleType: viewBinding.moduleType, + }); + if (addEdit) edits.push(addEdit); + } else if (viewBinding.moduleType === "esm") { + // Replace the haste View default import with ViewPropTypes. + const viewImport = getImport(rootNode, { type: "default", from: "View" }); + const statement = viewImport?.node.ancestors().find((a) => a.kind() === "import_statement"); + if (statement) { + edits.push(statement.replace("import ViewPropTypes from 'ViewPropTypes';")); + } + } else { + const viewImport = getImport(rootNode, { type: "default", from: "View" }); + const statement = viewImport?.node.ancestors().find((a) => + a.kind() === "lexical_declaration" || a.kind() === "variable_declaration" + ); + if (statement) { + edits.push(statement.replace("const ViewPropTypes = require('ViewPropTypes');")); + } + } } else if (keepViewBinding) { - edits.push({ - startPos: hasteImportStatement.range().end.index, - endPos: hasteImportStatement.range().end.index, - insertedText: "\nimport ViewPropTypes from 'ViewPropTypes';", + const addEdit = addImport(rootNode, { + type: "named", + specifiers: [{ name: "ViewPropTypes" }], + from: "react-native", + moduleType: viewBinding.moduleType, }); + if (addEdit) edits.push(addEdit); } else { - edits.push(hasteImportStatement.replace("import ViewPropTypes from 'ViewPropTypes';")); - } - } else if (hasteRequireStatement) { - if (hasExistingHasteViewPropTypesRequire) { - // Keep the existing require intact; the file already binds ViewPropTypes. - } else if (keepViewBinding) { - edits.push({ - startPos: statementInsertionIndex(source, hasteRequireStatement), - endPos: statementInsertionIndex(source, hasteRequireStatement), - insertedText: "\nconst ViewPropTypes = require('ViewPropTypes');\n", + const updateEdit = updateImport(rootNode, { + type: "named", + from: "react-native", + // Override local alias so `View as RNView` becomes `ViewPropTypes`, not + // `ViewPropTypes as RNView`, matching the call-site rewrite to ViewPropTypes. + specifiers: [{ name: "View", to: "ViewPropTypes", alias: "ViewPropTypes" }], }); - } else { - edits.push(hasteRequireStatement.replace("const ViewPropTypes = require('ViewPropTypes');")); - } - } else if (reactNativeImportStatement) { - const specifier = viewImportSpecifier(reactNativeImportStatement); - if (specifier) { - if (hasExistingReactNativeImportViewPropTypes) { - // Keep the existing import intact; the file already binds ViewPropTypes. - } else if (keepViewBinding) { - edits.push({ - startPos: specifier.range().end.index, - endPos: specifier.range().end.index, - insertedText: ", ViewPropTypes", - }); - } else { - edits.push(specifier.replace("ViewPropTypes")); - } - } - } else if (reactNativeRequireStatement) { - const property = viewRequireProperty(reactNativeRequireStatement); - if (property) { - if (hasExistingReactNativeRequireViewPropTypes) { - // Keep the existing require intact; the file already binds ViewPropTypes. - } else if (keepViewBinding) { - edits.push({ - startPos: property.range().end.index, - endPos: property.range().end.index, - insertedText: ", ViewPropTypes", - }); - } else { - edits.push(property.replace("ViewPropTypes")); - } + if (updateEdit) edits.push(updateEdit); } } diff --git a/codemods/react-native-view-prop-types/tests/aliased-view-import/expected.tsx b/codemods/react-native-view-prop-types/tests/aliased-view-import/expected.tsx new file mode 100644 index 0000000..852adb8 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/aliased-view-import/expected.tsx @@ -0,0 +1,12 @@ +import { PropTypes } from 'react'; +import { requireNativeComponent, ViewPropTypes } from 'react-native'; + +const Foo = requireNativeComponent('Foo'); + +function MyComponent(props) { + return ; +} + +MyComponent.propTypes = ViewPropTypes; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/aliased-view-import/input.tsx b/codemods/react-native-view-prop-types/tests/aliased-view-import/input.tsx new file mode 100644 index 0000000..545074f --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/aliased-view-import/input.tsx @@ -0,0 +1,12 @@ +import { PropTypes } from 'react'; +import { requireNativeComponent, View as RNView } from 'react-native'; + +const Foo = requireNativeComponent('Foo'); + +function MyComponent(props) { + return ; +} + +MyComponent.propTypes = RNView.propTypes; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/aliased-view-import/metrics.json b/codemods/react-native-view-prop-types/tests/aliased-view-import/metrics.json new file mode 100644 index 0000000..9175868 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/aliased-view-import/metrics.json @@ -0,0 +1,10 @@ +{ + "react-native-view-prop-types-replacements": [ + { + "cardinality": { + "file": "tests/aliased-view-import/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/expected.tsx b/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/expected.tsx index 82bc166..ed9c1e5 100644 --- a/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/expected.tsx +++ b/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/expected.tsx @@ -1,9 +1,6 @@ const { PropTypes } = require('react'); -const { - requireNativeComponent, - View, - ViewPropTypes -} = require('react-native'); +const { requireNativeComponent, View } = require('react-native'); +const { ViewPropTypes } = require('react-native'); const Foo = requireNativeComponent('Foo'); diff --git a/codemods/react-to-react-dom/package.json b/codemods/react-to-react-dom/package.json index cdb2b3e..0b811f9 100644 --- a/codemods/react-to-react-dom/package.json +++ b/codemods/react-to-react-dom/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "@types/node": "latest", "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/react-to-react-dom/scripts/codemod.ts b/codemods/react-to-react-dom/scripts/codemod.ts index d491ce4..2e9ae46 100644 --- a/codemods/react-to-react-dom/scripts/codemod.ts +++ b/codemods/react-to-react-dom/scripts/codemod.ts @@ -1,6 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { getAllImports, getImport } from "@jssg/utils/javascript/imports"; type NamedSpecifier = { name: string; @@ -222,7 +223,7 @@ function sortSpecs(specs: NamedSpecifier[]): NamedSpecifier[] { return [...specs].sort((left, right) => left.name.localeCompare(right.name)); } -function findModuleImports(rootNode: SgNode, moduleName: string): ImportBinding { +function findModuleImports(rootNode: SgNode, moduleName: string): ImportBinding { const statements = rootNode.findAll({ rule: { kind: "import_statement" } }) .filter((stmt) => importSource(stmt) === moduleName); @@ -232,21 +233,33 @@ function findModuleImports(rootNode: SgNode, moduleName: string): ImportBin for (const stmt of statements) { quote = quoteForImport(stmt); - const clause = stmt.find({ rule: { kind: "import_clause" } }); - if (!clause) { - continue; - } + } - const defaultIdentifier = namedChildren(clause) - .find((child) => child.kind() === "identifier"); - if (defaultIdentifier) { - alias = defaultIdentifier.text(); - } + for (const imp of getAllImports(rootNode, { type: "default", from: moduleName })) { + if (imp.moduleType !== "esm") continue; + alias = imp.alias; + } + // Collect every named specifier from ESM import statements for this module. + for (const stmt of statements) { for (const spec of stmt.findAll({ rule: { kind: "import_specifier" } })) { + const name = spec.field("name")?.text(); + if (!name) continue; + const local = spec.field("alias")?.text(); + named.push({ + name, + alias: local && local !== name ? local : undefined, + }); + } + } + + // Prefer getImport for DOM APIs so aliased named imports (findDOMNode as X) are robust. + for (const apiName of [...DOM_PROPERTIES, ...DOM_SERVER_PROPERTIES]) { + for (const imp of getAllImports(rootNode, { type: "named", name: apiName, from: moduleName })) { + if (imp.moduleType !== "esm" || imp.isNamespace) continue; named.push({ - name: spec.field("name")?.text() ?? spec.text(), - alias: spec.field("alias")?.text() ?? undefined, + name: apiName, + alias: imp.alias !== apiName ? imp.alias : undefined, }); } } @@ -259,29 +272,30 @@ function findModuleImports(rootNode: SgNode, moduleName: string): ImportBin }; } -function findRequireBinding(rootNode: SgNode, moduleName: string): RequireBinding | null { - for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - const name = declarator.field("name"); - const value = declarator.field("value"); - if (name?.kind() !== "identifier" || !isRequireOf(value, moduleName)) { - continue; - } +function findRequireBinding(rootNode: SgNode, moduleName: string): RequireBinding | null { + const imp = getImport(rootNode, { type: "default", from: moduleName }); + if (!imp || imp.moduleType !== "cjs" || imp.isNamespace) { + return null; + } - const statement = declarator.ancestors().find((ancestor) => - ancestor.kind() === "variable_declaration" || ancestor.kind() === "lexical_declaration" - ); - if (!statement) { - continue; - } + const statement = imp.node.ancestors().find((ancestor) => + ancestor.kind() === "variable_declaration" || ancestor.kind() === "lexical_declaration" + ); + if (!statement) { + return null; + } - return { - alias: name.text(), - statement, - kind: declarationKind(statement), - }; + // Skip destructured requires — those are handled via named import helpers elsewhere. + const declarator = imp.node.ancestors().find((ancestor) => ancestor.kind() === "variable_declarator"); + if (declarator?.field("name")?.kind() !== "identifier") { + return null; } - return null; + return { + alias: imp.alias, + statement, + kind: declarationKind(statement), + }; } function findAssignmentBinding(rootNode: SgNode, moduleName: string): AssignmentBinding | null { @@ -315,13 +329,13 @@ function findAssignmentBinding(rootNode: SgNode, moduleName: string): Assig return null; } -function hasCoreSource(rootNode: SgNode, moduleName: string): boolean { +function hasCoreSource(rootNode: SgNode, moduleName: string): boolean { return findModuleImports(rootNode, moduleName).statements.length > 0 || findRequireBinding(rootNode, moduleName) !== null || findAssignmentBinding(rootNode, moduleName) !== null; } -function activePair(rootNode: SgNode): ModulePair | null { +function activePair(rootNode: SgNode): ModulePair | null { for (const pair of MODULE_PAIRS) { if (hasCoreSource(rootNode, pair.core)) { return pair; diff --git a/codemods/react-to-react-dom/tests/import-aliased-find-dom-node/expected.tsx b/codemods/react-to-react-dom/tests/import-aliased-find-dom-node/expected.tsx new file mode 100644 index 0000000..daa487b --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-aliased-find-dom-node/expected.tsx @@ -0,0 +1,3 @@ +import { findDOMNode as findNode } from 'react-dom'; + +findNode(this.refs.node); diff --git a/codemods/react-to-react-dom/tests/import-aliased-find-dom-node/input.tsx b/codemods/react-to-react-dom/tests/import-aliased-find-dom-node/input.tsx new file mode 100644 index 0000000..9d87eed --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-aliased-find-dom-node/input.tsx @@ -0,0 +1,3 @@ +import { findDOMNode as findNode } from 'react'; + +findNode(this.refs.node); diff --git a/codemods/react-to-react-dom/tests/import-aliased-find-dom-node/metrics.json b/codemods/react-to-react-dom/tests/import-aliased-find-dom-node/metrics.json new file mode 100644 index 0000000..60dfec2 --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-aliased-find-dom-node/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "1", + "file": "tests/import-aliased-find-dom-node/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/package.json b/codemods/remove-forward-ref/package.json index de63753..960e288 100644 --- a/codemods/remove-forward-ref/package.json +++ b/codemods/remove-forward-ref/package.json @@ -13,6 +13,6 @@ "typescript": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/remove-forward-ref/scripts/codemod.ts b/codemods/remove-forward-ref/scripts/codemod.ts index 1e71588..8b0c574 100644 --- a/codemods/remove-forward-ref/scripts/codemod.ts +++ b/codemods/remove-forward-ref/scripts/codemod.ts @@ -275,7 +275,9 @@ const transform: Transform = async (root) => { return null; } - if (forwardRefImport) { + // getImport falls back to namespace imports for missing named specs; only + // remove a real named forwardRef binding. + if (forwardRefImport && !forwardRefImport.isNamespace) { const removeEdit = removeImport(rootNode, { type: "named", specifiers: ["forwardRef"], diff --git a/codemods/remove-legacy-context/package.json b/codemods/remove-legacy-context/package.json index b9de761..cbd1250 100644 --- a/codemods/remove-legacy-context/package.json +++ b/codemods/remove-legacy-context/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "@types/node": "latest", "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/remove-legacy-context/scripts/codemod.ts b/codemods/remove-legacy-context/scripts/codemod.ts index 4874d2c..50cc006 100644 --- a/codemods/remove-legacy-context/scripts/codemod.ts +++ b/codemods/remove-legacy-context/scripts/codemod.ts @@ -1,6 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { addImport, getAllImports } from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -19,65 +20,27 @@ function firstNamedChild(node: SgNode | null): SgNode | null { return namedChildren(node)[0] ?? null; } -function sourceText(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) { - return fragment.text(); - } - - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; -} - -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? sourceText(source) : null; -} - type ReactImportInfo = { componentNames: Set; reactObjectNames: Set; preferredReactObjectName: string | null; - quoteChar: "'" | "\""; }; function collectReactImportInfo(rootNode: SgNode): ReactImportInfo { const componentNames = new Set(); const reactObjectNames = new Set(); let preferredReactObjectName: string | null = null; - let quoteChar: "'" | "\"" = "\""; - - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (importSource(importNode) !== "react") { - continue; - } - - const sourceNode = importNode.field("source") ?? importNode.find({ rule: { kind: "string" } }); - if (sourceNode?.text().startsWith("'")) { - quoteChar = "'"; - } - - const importClause = importNode.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); - if (defaultIdentifier) { - reactObjectNames.add(defaultIdentifier.text()); - preferredReactObjectName ??= defaultIdentifier.text(); - } - const namespaceImport = importNode.find({ rule: { kind: "namespace_import" } }); - const namespaceName = namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }); - if (namespaceName) { - reactObjectNames.add(namespaceName.text()); - preferredReactObjectName ??= namespaceName.text(); - } + for (const imp of getAllImports(rootNode, { type: "default", from: "react" })) { + reactObjectNames.add(imp.alias); + preferredReactObjectName ??= imp.alias; + } - for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { - const importedName = specifier.field("name")?.text(); - if (importedName !== "Component" && importedName !== "PureComponent") { - continue; + for (const name of ["Component", "PureComponent"] as const) { + for (const imp of getAllImports(rootNode, { type: "named", name, from: "react" })) { + if (!imp.isNamespace) { + componentNames.add(imp.alias); } - - componentNames.add(specifier.field("alias")?.text() ?? importedName); } } @@ -85,7 +48,6 @@ function collectReactImportInfo(rootNode: SgNode): ReactImportIn componentNames, reactObjectNames, preferredReactObjectName, - quoteChar, }; } @@ -345,22 +307,19 @@ const transform: Transform = async (root) => { } if (needsReactHelperImport && helperReactObjectName) { - const allImports = rootNode.findAll({ rule: { kind: "import_statement" } }); - const importTarget = allImports[allImports.length - 1]; - const importText = `import * as ${helperReactObjectName} from ${reactImportInfo.quoteChar}react${reactImportInfo.quoteChar};\n`; - - if (importTarget) { - const insertPos = consumeLineBreak(source, importTarget.range().end.index); - edits.push({ - startPos: insertPos, - endPos: insertPos, - insertedText: importText, - }); - } else { + const importEdit = addImport(rootNode, { + type: "namespace", + name: helperReactObjectName, + from: "react", + }); + if (importEdit) { + const quoteMatch = source.match(/\bfrom\s+(['"])/); + const quote = quoteMatch?.[1] === "'" ? "'" : '"'; edits.push({ - startPos: 0, - endPos: 0, - insertedText: `${importText}\n`, + ...importEdit, + insertedText: importEdit.insertedText + .replace(/"([^"]+)"/g, `${quote}$1${quote}`) + .replace(/'([^']+)'/g, `${quote}$1${quote}`), }); } } diff --git a/codemods/remove-legacy-context/tests/cjs-require-react/expected.tsx b/codemods/remove-legacy-context/tests/cjs-require-react/expected.tsx new file mode 100644 index 0000000..ea2d12a --- /dev/null +++ b/codemods/remove-legacy-context/tests/cjs-require-react/expected.tsx @@ -0,0 +1,12 @@ +const PropTypes = require("prop-types"); +const React = require("react"); + +const Context = React.createContext(); + +class Parent extends React.Component { + + + render() { + return ; + } +} diff --git a/codemods/remove-legacy-context/tests/cjs-require-react/input.tsx b/codemods/remove-legacy-context/tests/cjs-require-react/input.tsx new file mode 100644 index 0000000..a49033f --- /dev/null +++ b/codemods/remove-legacy-context/tests/cjs-require-react/input.tsx @@ -0,0 +1,16 @@ +const PropTypes = require("prop-types"); +const React = require("react"); + +class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + }; + + getChildContext() { + return { foo: "bar" }; + } + + render() { + return ; + } +} diff --git a/codemods/remove-legacy-context/tests/cjs-require-react/metrics.json b/codemods/remove-legacy-context/tests/cjs-require-react/metrics.json new file mode 100644 index 0000000..9df0801 --- /dev/null +++ b/codemods/remove-legacy-context/tests/cjs-require-react/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-legacy-context-replacements": [ + { + "cardinality": { + "file": "tests/cjs-require-react/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-string-ref/package.json b/codemods/replace-string-ref/package.json index d8cb7f1..f841d15 100644 --- a/codemods/replace-string-ref/package.json +++ b/codemods/replace-string-ref/package.json @@ -13,6 +13,6 @@ "@types/node": "latest" }, "dependencies": { - "@jssg/utils": "^0.0.2" + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/replace-string-ref/scripts/codemod.ts b/codemods/replace-string-ref/scripts/codemod.ts index cb5aff4..1387537 100644 --- a/codemods/replace-string-ref/scripts/codemod.ts +++ b/codemods/replace-string-ref/scripts/codemod.ts @@ -95,8 +95,9 @@ const transform: Transform = async (root) => { const pureImport = getImport(rootNode, { type: "named", name: "PureComponent", from: "react" }); const reactName = reactDefault?.alias ?? "React"; const componentNames = new Set(); - if (componentImport) componentNames.add(componentImport.alias); - if (pureImport) componentNames.add(pureImport.alias); + // getImport falls back to namespaces for missing named specs — ignore those. + if (componentImport && !componentImport.isNamespace) componentNames.add(componentImport.alias); + if (pureImport && !pureImport.isNamespace) componentNames.add(pureImport.alias); const componentNamesList = Array.from(componentNames); const stringRefs = rootNode.findAll({ diff --git a/codemods/sort-comp/package.json b/codemods/sort-comp/package.json index 710aa77..38a89f3 100644 --- a/codemods/sort-comp/package.json +++ b/codemods/sort-comp/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "@types/node": "latest", "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/sort-comp/scripts/codemod.ts b/codemods/sort-comp/scripts/codemod.ts index 0ecab8a..ca894fc 100644 --- a/codemods/sort-comp/scripts/codemod.ts +++ b/codemods/sort-comp/scripts/codemod.ts @@ -1,6 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { getImport } from "@jssg/utils/javascript/imports"; import * as fs from "fs"; import path from "path"; import { pathToFileURL } from "url"; @@ -17,7 +18,8 @@ type ReactImportAliases = { type ReactSuperclassKind = "Component" | "PureComponent"; -const REACT_SOURCES = new Set(["react", "React", "react/addons", "react-native"]); +const REACT_SOURCES = ["react", "React", "react/addons", "react-native"] as const; +const REACT_SOURCE_SET = new Set(REACT_SOURCES); function stringValue(node: SgNode): string | null { const fragment = node.find({ rule: { kind: "string_fragment" } }); @@ -45,7 +47,7 @@ function importSource(node: SgNode): string | null { } function isReactSource(importStmt: SgNode): boolean { - return REACT_SOURCES.has(importSource(importStmt) ?? ""); + return REACT_SOURCE_SET.has(importSource(importStmt) ?? ""); } function hasReact(rootNode: SgNode): boolean { @@ -54,7 +56,7 @@ function hasReact(rootNode: SgNode): boolean { } for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { - if (REACT_SOURCES.has(requireSource(call) ?? "")) return true; + if (REACT_SOURCE_SET.has(requireSource(call) ?? "")) return true; } return false; @@ -64,28 +66,35 @@ function explicitRequireDisabled(value: unknown): boolean { return value === false || value === "false"; } -function reactImportAliases(rootNode: SgNode): ReactImportAliases { +function reactImportAliases(rootNode: SgNode): ReactImportAliases { const component = new Set(); const pureComponent = new Set(); - for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (!isReactSource(importStmt)) continue; - - for (const specifier of importStmt.findAll({ rule: { kind: "import_specifier" } })) { - const imported = specifier.field("name")?.text(); - const alias = specifier.field("alias")?.text() ?? imported; - if (!imported || !alias) continue; - if (imported === "Component") component.add(alias); - if (imported === "PureComponent") pureComponent.add(alias); + for (const from of REACT_SOURCES) { + for (const exportName of ["Component", "PureComponent"] as const) { + const imp = getImport(rootNode, { type: "named", name: exportName, from }); + if (!imp || imp.isNamespace) continue; + if (exportName === "Component") component.add(imp.alias); + else pureComponent.add(imp.alias); } } return { component, pureComponent }; } +function resolveReactObjectAliases(rootNode: SgNode): Set { + const aliases = new Set(["React"]); + for (const from of REACT_SOURCES) { + const imp = getImport(rootNode, { type: "default", from }); + if (imp) aliases.add(imp.alias); + } + return aliases; +} + function reactSuperclassKind( classDecl: SgNode, aliases: ReactImportAliases, + reactObjects: Set, ): ReactSuperclassKind | null { const heritage = classDecl.find({ rule: { kind: "class_heritage" } }); if (!heritage) return null; @@ -98,7 +107,7 @@ function reactSuperclassKind( if (directExtends.kind() === "member_expression") { const object = directExtends.field("object"); const property = directExtends.field("property")?.text() ?? ""; - if (object?.text() !== "React") return null; + if (!object || object.kind() !== "identifier" || !reactObjects.has(object.text())) return null; if (property === "Component" || property === "PureComponent") return property; return null; } @@ -109,21 +118,27 @@ function reactSuperclassKind( return null; } -function isReactCreateClassCall(node: SgNode | null): node is SgNode { +function isReactCreateClassCall( + node: SgNode | null, + reactObjects: Set, +): node is SgNode { if (!node || node.kind() !== "call_expression") return false; const callee = node.field("function"); return callee?.kind() === "member_expression" && callee.field("object")?.kind() === "identifier" && - callee.field("object")?.text() === "React" && + reactObjects.has(callee.field("object")!.text()) && callee.field("property")?.kind() === "property_identifier" && callee.field("property")?.text() === "createClass"; } -function reactCreateClassConfigs(rootNode: SgNode): SgNode[] { +function reactCreateClassConfigs( + rootNode: SgNode, + reactObjects: Set, +): SgNode[] { const configs: SgNode[] = []; for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { const value = declarator.field("value"); - if (!isReactCreateClassCall(value)) continue; + if (!isReactCreateClassCall(value, reactObjects)) continue; const args = value.field("arguments"); const config = args?.children().find((child) => child.kind() === "object"); if (config) configs.push(config); @@ -133,8 +148,9 @@ function reactCreateClassConfigs(rootNode: SgNode): SgNode[ function reactClassBodies(rootNode: SgNode): SgNode[] { const aliases = reactImportAliases(rootNode); + const reactObjects = resolveReactObjectAliases(rootNode); const reactClasses = rootNode.findAll({ rule: { kind: "class_declaration" } }) - .map((classDecl) => ({ classDecl, kind: reactSuperclassKind(classDecl, aliases) })) + .map((classDecl) => ({ classDecl, kind: reactSuperclassKind(classDecl, aliases, reactObjects) })) .filter((entry): entry is { classDecl: SgNode; kind: ReactSuperclassKind } => entry.kind !== null); const targetSuperclassKind = reactClasses.some((entry) => entry.kind === "Component") ? "Component" @@ -603,7 +619,9 @@ const transform: Transform = async (root, options) => { await getMethodsOrderFromEslint(root.filename()) ?? DEFAULT_METHODS_ORDER; - for (const config of reactCreateClassConfigs(rootNode)) { + const reactObjects = resolveReactObjectAliases(rootNode); + + for (const config of reactCreateClassConfigs(rootNode, reactObjects)) { const edit = replacementFor(config, collectChunks(config, source), methodsOrder, ",\n\n "); if (edit) { edits.push(edit); diff --git a/codemods/sort-comp/tests/aliased-namespace-react/expected.tsx b/codemods/sort-comp/tests/aliased-namespace-react/expected.tsx new file mode 100644 index 0000000..0ea0afe --- /dev/null +++ b/codemods/sort-comp/tests/aliased-namespace-react/expected.tsx @@ -0,0 +1,13 @@ +import * as R from 'react'; + +class MyComponent extends R.Component { + componentDidMount() { + } + + myOwnMethod() { + } + + render() { + return
; + } +} diff --git a/codemods/sort-comp/tests/aliased-namespace-react/input.tsx b/codemods/sort-comp/tests/aliased-namespace-react/input.tsx new file mode 100644 index 0000000..1d8307e --- /dev/null +++ b/codemods/sort-comp/tests/aliased-namespace-react/input.tsx @@ -0,0 +1,13 @@ +import * as R from 'react'; + +class MyComponent extends R.Component { + render() { + return
; + } + + componentDidMount() { + } + + myOwnMethod() { + } +} diff --git a/codemods/sort-comp/tests/aliased-namespace-react/metrics.json b/codemods/sort-comp/tests/aliased-namespace-react/metrics.json new file mode 100644 index 0000000..e9485a9 --- /dev/null +++ b/codemods/sort-comp/tests/aliased-namespace-react/metrics.json @@ -0,0 +1,11 @@ +{ + "sort-comp-reorders": [ + { + "cardinality": { + "file": "tests/aliased-namespace-react/input.tsx", + "kind": "class" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/use-browser/package.json b/codemods/use-browser/package.json index 061247b..8f6613a 100644 --- a/codemods/use-browser/package.json +++ b/codemods/use-browser/package.json @@ -11,5 +11,8 @@ "@codemod.com/jssg-types": "latest", "@types/node": "latest", "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.10" } } diff --git a/codemods/use-browser/scripts/codemod.ts b/codemods/use-browser/scripts/codemod.ts index 504b624..50dbb7a 100644 --- a/codemods/use-browser/scripts/codemod.ts +++ b/codemods/use-browser/scripts/codemod.ts @@ -1,6 +1,13 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import { parse } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import { + addImport, + getAllImports, + getImport, + removeImport, +} from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -10,19 +17,23 @@ function metricFile(filename: string): string { const REACT_MODULE = "react"; const REACT_DOM_MODULE = "react-dom"; const EFFECT_HOOKS = new Set(["useEffect", "useLayoutEffect"]); +const REACT_NAMED_EXPORTS = [ + "useState", + "useEffect", + "useLayoutEffect", + "use", + "Suspense", +] as const; type ReactBindings = { /** Local names that refer to the React module object (default or namespace import). */ namespaces: Set; /** React export name -> local name for named imports. */ named: Map; - /** The first ESM import from "react" (used to add specifiers). */ - firstImport: SgNode | null; - /** The first ESM import from "react-dom" that has named imports. */ - firstDomNamedImport: SgNode | null; /** react-dom export name -> local name for named imports. */ domNamed: Map; - lastImport: SgNode | null; + /** True when an ESM react binding exists. */ + hasReactEsm: boolean; }; type HookCall = { style: "named"; localName: string } | { style: "member"; object: string }; @@ -45,69 +56,50 @@ type ComponentMatch = { pattern: MountedPattern; }; +function parseProgram(source: string): SgNode { + return parse("tsx", source).root(); +} + function namedChildren(node: SgNode | null): SgNode[] { if (!node) return []; return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); } -function sourceText(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) return fragment.text(); - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; -} - -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? sourceText(source) : null; -} - -function isTypeOnlyImport(node: SgNode): boolean { - return node.children().some((child) => !child.isNamed() && child.text() === "type"); -} - -function quoteOf(importNode: SgNode | null): string { - const source = importNode?.field("source") ?? importNode?.find({ rule: { kind: "string" } }); - return source?.text().startsWith("'") ? "'" : '"'; -} - function collectReactBindings(rootNode: SgNode): ReactBindings { const bindings: ReactBindings = { namespaces: new Set(), named: new Map(), - firstImport: null, - firstDomNamedImport: null, domNamed: new Map(), - lastImport: null, + hasReactEsm: false, }; - for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (imp.parent()?.kind() !== "program") continue; - bindings.lastImport = imp; - const source = importSource(imp); - if (source !== REACT_MODULE && source !== REACT_DOM_MODULE) continue; - if (isTypeOnlyImport(imp)) continue; - - const named = source === REACT_MODULE ? bindings.named : bindings.domNamed; - for (const specifier of imp.findAll({ rule: { kind: "import_specifier" } })) { - const name = specifier.field("name")?.text(); - if (!name) continue; - named.set(name, specifier.field("alias")?.text() ?? name); - } + for (const imp of getAllImports(rootNode, { type: "default", from: REACT_MODULE })) { + if (imp.moduleType !== "esm") continue; + bindings.hasReactEsm = true; + bindings.namespaces.add(imp.alias); + } - if (source === REACT_MODULE) { - if (!bindings.firstImport) bindings.firstImport = imp; - const importClause = imp.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); - if (defaultIdentifier) bindings.namespaces.add(defaultIdentifier.text()); - const namespaceImport = imp.find({ rule: { kind: "namespace_import" } }); - const namespaceName = namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }); - if (namespaceName) bindings.namespaces.add(namespaceName.text()); - } else if (!bindings.firstDomNamedImport && imp.find({ rule: { kind: "named_imports" } })) { - bindings.firstDomNamedImport = imp; + for (const exportName of REACT_NAMED_EXPORTS) { + for (const imp of getAllImports(rootNode, { + type: "named", + name: exportName, + from: REACT_MODULE, + })) { + if (imp.moduleType !== "esm" || imp.isNamespace) continue; + bindings.hasReactEsm = true; + bindings.named.set(exportName, imp.alias); } } + const browserImp = getImport(rootNode, { + type: "named", + name: "browser", + from: REACT_DOM_MODULE, + }); + if (browserImp && !browserImp.isNamespace && browserImp.moduleType === "esm") { + bindings.domNamed.set("browser", browserImp.alias); + } + return bindings; } @@ -260,19 +252,6 @@ function matchGuardStatement( return { fallback }; } -/** Insert a specifier at the end of a `{ ... }` named-imports list, keeping the existing spacing. */ -function appendSpecifierEdit(namedImports: SgNode, specifier: string): Edit { - const text = namedImports.text(); - let insertPos = namedImports.range().end.index - 1; - let offset = text.length - 1; - while (offset > 0 && /\s/.test(text[offset - 1] ?? "")) { - offset--; - insertPos--; - } - const inner = text.slice(1, -1).trim(); - return { startPos: insertPos, endPos: insertPos, insertedText: inner.length > 0 ? `, ${specifier}` : ` ${specifier} ` }; -} - function countIdentifiers(scope: SgNode | SgNode, name: string, exclude: SgNode[]): number { const excludedRanges = exclude.map((node) => [node.range().start.index, node.range().end.index] as const); return scope @@ -492,28 +471,68 @@ function isNameUsed(rootNode: SgNode, name: string): boolean { }) !== null; } -function rebuildImport( - importNode: SgNode, - drop: Set, - add: string[], +function preferredQuote(source: string): '"' | "'" { + const match = source.match(/\bfrom\s+(['"])/); + return match?.[1] === "'" ? "'" : '"'; +} + +function withQuoteStyle(edit: Edit, quote: '"' | "'"): Edit { + return { + ...edit, + insertedText: edit.insertedText + .replace(/"([^"]+)"/g, `${quote}$1${quote}`) + .replace(/'([^']+)'/g, `${quote}$1${quote}`), + }; +} + +function syncImports( source: string, + dropFromReact: string[], + addToReact: string[], + needBrowser: boolean, ): string { - const quote = quoteOf(importNode); - const importClause = importNode.find({ rule: { kind: "import_clause" } }); - const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); - const namespaceImport = importNode.find({ rule: { kind: "namespace_import" } }); - const specifiers = importNode - .findAll({ rule: { kind: "import_specifier" } }) - .filter((specifier) => !drop.has(specifier.field("name")?.text() ?? "")) - .map((specifier) => specifier.text()); - const named = [...specifiers, ...add]; - - const clauses: string[] = []; - if (defaultIdentifier) clauses.push(defaultIdentifier.text()); - if (namespaceImport) clauses.push(namespaceImport.text()); - if (named.length > 0) clauses.push(`{ ${named.join(", ")} }`); - if (clauses.length === 0) return ""; - return `import ${clauses.join(", ")} from ${quote}${source}${quote};`; + let program = parseProgram(source); + let next = source; + const quote = preferredQuote(source); + + // Add before remove so merges keep existing react/react-dom statement order. + if (addToReact.length > 0) { + const addEdit = addImport(program, { + type: "named", + specifiers: addToReact.map((name) => ({ name })), + from: REACT_MODULE, + }); + if (addEdit) { + next = program.commitEdits([withQuoteStyle(addEdit, quote)]); + program = parseProgram(next); + } + } + + if (needBrowser) { + const browserEdit = addImport(program, { + type: "named", + specifiers: [{ name: "browser" }], + from: REACT_DOM_MODULE, + }); + if (browserEdit) { + next = program.commitEdits([withQuoteStyle(browserEdit, quote)]); + program = parseProgram(next); + } + } + + // removeImport drops one remaining named specifier per call when siblings stay. + while (dropFromReact.length > 0) { + const removeEdit = removeImport(program, { + type: "named", + specifiers: dropFromReact, + from: REACT_MODULE, + }); + if (!removeEdit) break; + next = program.commitEdits([removeEdit]); + program = parseProgram(next); + } + + return next; } const transform: Transform = async (root) => { @@ -522,8 +541,7 @@ const transform: Transform = async (root) => { const metric = useMetricAtom("use-browser-transformations"); const bindings = collectReactBindings(rootNode); - const reactImport = bindings.firstImport; - if (!reactImport) return null; + if (!bindings.hasReactEsm) return null; const candidates = findComponents(rootNode, bindings); if (candidates.length === 0) return null; @@ -631,53 +649,24 @@ const transform: Transform = async (root) => { // Imports: drop hooks that are no longer referenced, add use/Suspense/browser. const removedNodes = transformed.flatMap((match) => [match.pattern.stateStmt, match.pattern.effectStmt]); - const dropFromReact = new Set(); + const dropFromReact: string[] = []; for (const hook of ["useState", ...EFFECT_HOOKS]) { const localName = bindings.named.get(hook); if (!localName) continue; const remaining = countIdentifiers(rootNode, localName, removedNodes) - 1; // minus the import specifier itself - if (remaining <= 0) dropFromReact.add(hook); - } - - const quote = quoteOf(reactImport); - const browserImport = existingBrowser ? "" : `import { ${browserName} } from ${quote}${REACT_DOM_MODULE}${quote};`; - - let browserImportAdded = existingBrowser !== undefined; - if (!browserImportAdded && bindings.firstDomNamedImport) { - const namedImports = bindings.firstDomNamedImport.find({ rule: { kind: "named_imports" } }); - if (namedImports) { - edits.push(appendSpecifierEdit(namedImports, browserName)); - browserImportAdded = true; - } + if (remaining <= 0) dropFromReact.push(hook); } - const reactImportsToClean = rootNode - .findAll({ rule: { kind: "import_statement" } }) - .filter((imp) => imp.parent()?.kind() === "program" && importSource(imp) === REACT_MODULE && !isTypeOnlyImport(imp)); - - for (const imp of reactImportsToClean) { - const isFirst = imp.id() === reactImport.id(); - const specifierNames = imp - .findAll({ rule: { kind: "import_specifier" } }) - .map((specifier) => specifier.field("name")?.text() ?? ""); - const needsDrop = specifierNames.some((name) => dropFromReact.has(name)); - const adds = isFirst ? addToReact : []; - const trailing = isFirst && !browserImportAdded ? browserImport : ""; - if (!needsDrop && adds.length === 0 && trailing === "") continue; - - const rebuilt = rebuildImport(imp, dropFromReact, adds, REACT_MODULE); - if (rebuilt === "" && trailing === "") { - const [start, end] = statementLineRange(source, imp); - edits.push({ startPos: start, endPos: end, insertedText: "" }); - } else { - const text = [rebuilt, trailing].filter((part) => part !== "").join("\n"); - edits.push(imp.replace(text)); - } - if (isFirst) browserImportAdded = true; - } + const bodySource = rootNode.commitEdits(edits); + const next = syncImports( + bodySource, + dropFromReact, + addToReact, + existingBrowser === undefined, + ); metric.increment({ file: metricFile(root.filename()) }, transformed.length); - return rootNode.commitEdits(edits); + return next; }; export default transform; diff --git a/codemods/use-browser/tests/arrow-component-with-fc-type/expected.tsx b/codemods/use-browser/tests/arrow-component-with-fc-type/expected.tsx index eaecc4a..f3cacc1 100644 --- a/codemods/use-browser/tests/arrow-component-with-fc-type/expected.tsx +++ b/codemods/use-browser/tests/arrow-component-with-fc-type/expected.tsx @@ -1,6 +1,6 @@ import { type FC, Suspense, use } from 'react'; -import { browser } from 'react-dom'; import { Skeleton } from './skeleton'; +import { browser } from 'react-dom'; export const Viewport: FC<{ label: string }> = (props) => { return ( diff --git a/codemods/use-browser/tests/destructured-props-function/expected.tsx b/codemods/use-browser/tests/destructured-props-function/expected.tsx index 18dae76..4a2c63f 100644 --- a/codemods/use-browser/tests/destructured-props-function/expected.tsx +++ b/codemods/use-browser/tests/destructured-props-function/expected.tsx @@ -1,6 +1,6 @@ import { Suspense, use } from "react"; -import { browser } from "react-dom"; import { ClockSkeleton, formatNow } from "./clock"; +import { browser } from "react-dom"; export function LocalClock(props: { format: string }) { return ( diff --git a/codemods/use-browser/tests/export-default-props-identifier/expected.tsx b/codemods/use-browser/tests/export-default-props-identifier/expected.tsx index d173c5c..f3222a5 100644 --- a/codemods/use-browser/tests/export-default-props-identifier/expected.tsx +++ b/codemods/use-browser/tests/export-default-props-identifier/expected.tsx @@ -1,6 +1,6 @@ import React, { Suspense, use } from "react"; -import { browser } from "react-dom"; import { readTheme } from "./theme"; +import { browser } from "react-dom"; export default function ThemeBadge(props) { return ( diff --git a/codemods/use-browser/tests/single-quote-dom-merge/expected.tsx b/codemods/use-browser/tests/single-quote-dom-merge/expected.tsx new file mode 100644 index 0000000..2344a20 --- /dev/null +++ b/codemods/use-browser/tests/single-quote-dom-merge/expected.tsx @@ -0,0 +1,15 @@ +import { Suspense, use } from 'react'; +import { createPortal, browser } from 'react-dom'; + +export function Panel(props) { + return ( + + + + ); +} + +function PanelBrowserOnly({ children }) { + use(browser()); + return createPortal(children, document.body); +} diff --git a/codemods/use-browser/tests/single-quote-dom-merge/input.tsx b/codemods/use-browser/tests/single-quote-dom-merge/input.tsx new file mode 100644 index 0000000..27a8f8c --- /dev/null +++ b/codemods/use-browser/tests/single-quote-dom-merge/input.tsx @@ -0,0 +1,11 @@ +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; + +export function Panel({ children }) { + const [ready, setReady] = useState(false); + useEffect(() => { + setReady(true); + }, []); + if (!ready) return null; + return createPortal(children, document.body); +} diff --git a/codemods/use-browser/tests/single-quote-dom-merge/metrics.json b/codemods/use-browser/tests/single-quote-dom-merge/metrics.json new file mode 100644 index 0000000..e2ca8b2 --- /dev/null +++ b/codemods/use-browser/tests/single-quote-dom-merge/metrics.json @@ -0,0 +1,10 @@ +{ + "use-browser-transformations": [ + { + "cardinality": { + "file": "tests/single-quote-dom-merge/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad918cf..bc38c96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,10 +208,6 @@ importers: version: 6.0.3 codemods/remove-context-provider: - dependencies: - '@jssg/utils': - specifier: ^0.0.10 - version: 0.0.10 devDependencies: '@codemod.com/jssg-types': specifier: latest From dbc10a91960eb1b5a8a4561b75b818029034f95a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Oct 2026 21:43:39 +0000 Subject: [PATCH 05/10] Add changeset for @jssg/utils adoption across React codemods Co-authored-by: Alex Bit --- .changeset/leverage-jssg-utils.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .changeset/leverage-jssg-utils.md diff --git a/.changeset/leverage-jssg-utils.md b/.changeset/leverage-jssg-utils.md new file mode 100644 index 0000000..164761a --- /dev/null +++ b/.changeset/leverage-jssg-utils.md @@ -0,0 +1,27 @@ +--- +"react-create-element-to-jsx": patch +"react-find-dom-node": patch +"react-19-3-fragment-ref-wrappers": patch +"react-prop-types-typescript": patch +"react-pure-component": patch +"react-pure-render-mixin": patch +"react-dom-to-react-dom-factories": patch +"react-native-view-prop-types": patch +"react-proptypes-to-prop-types": patch +"react-to-react-dom": patch +"react-19-remove-forward-ref": patch +"react-19-remove-legacy-context": patch +"react-19-remove-memoization": patch +"react-19-replace-act-import": patch +"react-19-replace-create-factory": patch +"react-19-replace-reactdom-render": patch +"react-19-replace-string-ref": patch +"react-19-replace-use-form-state": patch +"react-sort-comp": patch +"react-19-3-unprefix-stable-apis": patch +"react-update-react-imports": patch +"react-19-3-use-browser": patch +"react-19-use-context-hook": patch +--- + +Adopt `@jssg/utils` import helpers (`getImport`, `getAllImports`, `addImport`, `removeImport`, `updateImport`) across React JSSG codemods, bump to `^0.0.10`, and add fixtures covering CJS, aliases, quote/style merges, and multi-specifier import cleanup. From ffdab779f81533d262741c78af92461fa1dce628 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Oct 2026 21:47:52 +0000 Subject: [PATCH 06/10] Bump patch versions and add utils-edge fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump changeset packages (0.1.2→0.1.3, react-19-3 0.1.1→0.1.2) and sync codemod.yaml. Add update-react-imports fixtures for single-quote removeImport and no-semicolon destructure, plus a CJS require parity case for prop-types-typescript. Co-authored-by: Alex Bit --- codemods/create-element-to-jsx/codemod.yaml | 2 +- codemods/create-element-to-jsx/package.json | 2 +- codemods/find-dom-node/codemod.yaml | 2 +- codemods/find-dom-node/package.json | 2 +- codemods/fragment-ref-wrappers/codemod.yaml | 2 +- codemods/fragment-ref-wrappers/package.json | 2 +- codemods/prop-types-typescript/codemod.yaml | 2 +- codemods/prop-types-typescript/package.json | 2 +- .../scripts/parity-cases.mjs | 26 +++++++++++++++++++ codemods/pure-component/codemod.yaml | 2 +- codemods/pure-component/package.json | 2 +- codemods/pure-render-mixin/codemod.yaml | 2 +- codemods/pure-render-mixin/package.json | 2 +- .../codemod.yaml | 2 +- .../package.json | 2 +- .../react-native-view-prop-types/codemod.yaml | 2 +- .../react-native-view-prop-types/package.json | 2 +- .../codemod.yaml | 2 +- .../package.json | 2 +- codemods/react-to-react-dom/codemod.yaml | 2 +- codemods/react-to-react-dom/package.json | 2 +- codemods/remove-forward-ref/codemod.yaml | 2 +- codemods/remove-forward-ref/package.json | 2 +- codemods/remove-legacy-context/codemod.yaml | 2 +- codemods/remove-legacy-context/package.json | 2 +- codemods/remove-memoization/codemod.yaml | 2 +- codemods/remove-memoization/package.json | 2 +- codemods/replace-act-import/codemod.yaml | 2 +- codemods/replace-act-import/package.json | 2 +- codemods/replace-create-factory/codemod.yaml | 2 +- codemods/replace-create-factory/package.json | 2 +- codemods/replace-reactdom-render/codemod.yaml | 2 +- codemods/replace-reactdom-render/package.json | 2 +- codemods/replace-string-ref/codemod.yaml | 2 +- codemods/replace-string-ref/package.json | 2 +- codemods/replace-use-form-state/codemod.yaml | 2 +- codemods/replace-use-form-state/package.json | 2 +- codemods/sort-comp/codemod.yaml | 2 +- codemods/sort-comp/package.json | 2 +- codemods/unprefix-stable-apis/codemod.yaml | 2 +- codemods/unprefix-stable-apis/package.json | 2 +- codemods/update-react-imports/codemod.yaml | 2 +- codemods/update-react-imports/package.json | 2 +- .../destructure-no-semicolon/expected.tsx | 2 ++ .../tests/destructure-no-semicolon/input.tsx | 2 ++ .../destructure-no-semicolon/metrics.json | 12 +++++++++ .../unused-default-single-quote/expected.tsx | 1 + .../unused-default-single-quote/input.tsx | 3 +++ .../unused-default-single-quote/metrics.json | 12 +++++++++ codemods/use-browser/codemod.yaml | 2 +- codemods/use-browser/package.json | 2 +- codemods/use-context-hook/codemod.yaml | 2 +- codemods/use-context-hook/package.json | 2 +- 53 files changed, 104 insertions(+), 46 deletions(-) create mode 100644 codemods/update-react-imports/tests/destructure-no-semicolon/expected.tsx create mode 100644 codemods/update-react-imports/tests/destructure-no-semicolon/input.tsx create mode 100644 codemods/update-react-imports/tests/destructure-no-semicolon/metrics.json create mode 100644 codemods/update-react-imports/tests/unused-default-single-quote/expected.tsx create mode 100644 codemods/update-react-imports/tests/unused-default-single-quote/input.tsx create mode 100644 codemods/update-react-imports/tests/unused-default-single-quote/metrics.json diff --git a/codemods/create-element-to-jsx/codemod.yaml b/codemods/create-element-to-jsx/codemod.yaml index 60e7f3f..819854c 100644 --- a/codemods/create-element-to-jsx/codemod.yaml +++ b/codemods/create-element-to-jsx/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-create-element-to-jsx" -version: "0.1.2" +version: "0.1.3" description: "Convert React.createElement calls to JSX" author: "Codemod " license: "MIT" diff --git a/codemods/create-element-to-jsx/package.json b/codemods/create-element-to-jsx/package.json index 97b1602..4bb3435 100644 --- a/codemods/create-element-to-jsx/package.json +++ b/codemods/create-element-to-jsx/package.json @@ -1,6 +1,6 @@ { "name": "react-create-element-to-jsx", - "version": "0.1.2", + "version": "0.1.3", "description": "Convert React.createElement calls to JSX", "type": "module", "scripts": { diff --git a/codemods/find-dom-node/codemod.yaml b/codemods/find-dom-node/codemod.yaml index 2917b02..b9fcbd5 100644 --- a/codemods/find-dom-node/codemod.yaml +++ b/codemods/find-dom-node/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-find-dom-node" -version: "0.1.2" +version: "0.1.3" description: "Replace getDOMNode() with React.findDOMNode()" author: "Codemod " license: "MIT" diff --git a/codemods/find-dom-node/package.json b/codemods/find-dom-node/package.json index e2e2805..293c9fd 100644 --- a/codemods/find-dom-node/package.json +++ b/codemods/find-dom-node/package.json @@ -1,6 +1,6 @@ { "name": "react-find-dom-node", - "version": "0.1.2", + "version": "0.1.3", "description": "Replace getDOMNode() with React.findDOMNode()", "type": "module", "scripts": { diff --git a/codemods/fragment-ref-wrappers/codemod.yaml b/codemods/fragment-ref-wrappers/codemod.yaml index 215700a..d8a7775 100644 --- a/codemods/fragment-ref-wrappers/codemod.yaml +++ b/codemods/fragment-ref-wrappers/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-3-fragment-ref-wrappers" -version: "0.1.1" +version: "0.1.2" description: "Replace display:contents wrapper elements that only exist to hold a ref with a Fragment ref" author: "Codemod " license: "MIT" diff --git a/codemods/fragment-ref-wrappers/package.json b/codemods/fragment-ref-wrappers/package.json index 258a4de..ce94bb2 100644 --- a/codemods/fragment-ref-wrappers/package.json +++ b/codemods/fragment-ref-wrappers/package.json @@ -1,6 +1,6 @@ { "name": "react-19-3-fragment-ref-wrappers", - "version": "0.1.1", + "version": "0.1.2", "description": "Replace display:contents wrapper elements that only exist to hold a ref with a Fragment ref", "type": "module", "scripts": { diff --git a/codemods/prop-types-typescript/codemod.yaml b/codemods/prop-types-typescript/codemod.yaml index 178dfa8..5985440 100644 --- a/codemods/prop-types-typescript/codemod.yaml +++ b/codemods/prop-types-typescript/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-prop-types-typescript" -version: "0.1.2" +version: "0.1.3" description: "Convert React PropTypes declarations into TypeScript props interfaces" author: "Codemod " license: "MIT" diff --git a/codemods/prop-types-typescript/package.json b/codemods/prop-types-typescript/package.json index 8785ef8..54f0bba 100644 --- a/codemods/prop-types-typescript/package.json +++ b/codemods/prop-types-typescript/package.json @@ -1,6 +1,6 @@ { "name": "react-prop-types-typescript", - "version": "0.1.2", + "version": "0.1.3", "description": "Convert React PropTypes declarations into TypeScript props interfaces", "type": "module", "scripts": { diff --git a/codemods/prop-types-typescript/scripts/parity-cases.mjs b/codemods/prop-types-typescript/scripts/parity-cases.mjs index deb2caf..82c61e0 100644 --- a/codemods/prop-types-typescript/scripts/parity-cases.mjs +++ b/codemods/prop-types-typescript/scripts/parity-cases.mjs @@ -1172,6 +1172,32 @@ interface MyComponentProps { foo?: number } +export const MyComponent = (props: MyComponentProps) => { + return +} +`, + }, + { + name: "cjs-prop-types-require", + input: `const PropTypes = require("prop-types"); +import React from "react" + +export const MyComponent = (props) => { + return +} + +MyComponent.propTypes = { + bar: PropTypes.string.isRequired, + foo: PropTypes.number, +} +`, + expected: `import React from "react" + +interface MyComponentProps { + bar: string + foo?: number +} + export const MyComponent = (props: MyComponentProps) => { return } diff --git a/codemods/pure-component/codemod.yaml b/codemods/pure-component/codemod.yaml index 2e58848..acb8184 100644 --- a/codemods/pure-component/codemod.yaml +++ b/codemods/pure-component/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-pure-component" -version: "0.1.2" +version: "0.1.3" description: "Convert simple React classes into pure function components" author: "Codemod " license: "MIT" diff --git a/codemods/pure-component/package.json b/codemods/pure-component/package.json index 99e9291..367b4cc 100644 --- a/codemods/pure-component/package.json +++ b/codemods/pure-component/package.json @@ -1,6 +1,6 @@ { "name": "react-pure-component", - "version": "0.1.2", + "version": "0.1.3", "description": "Convert simple React classes into pure function components", "type": "module", "scripts": { diff --git a/codemods/pure-render-mixin/codemod.yaml b/codemods/pure-render-mixin/codemod.yaml index 7b669e9..c98ba67 100644 --- a/codemods/pure-render-mixin/codemod.yaml +++ b/codemods/pure-render-mixin/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-pure-render-mixin" -version: "0.1.2" +version: "0.1.3" description: "Replace PureRenderMixin with shouldComponentUpdate using React.addons.shallowCompare" author: "Codemod " license: "MIT" diff --git a/codemods/pure-render-mixin/package.json b/codemods/pure-render-mixin/package.json index 231c853..56e0c54 100644 --- a/codemods/pure-render-mixin/package.json +++ b/codemods/pure-render-mixin/package.json @@ -1,6 +1,6 @@ { "name": "react-pure-render-mixin", - "version": "0.1.2", + "version": "0.1.3", "description": "Replace PureRenderMixin with shouldComponentUpdate using React.addons.shallowCompare", "type": "module", "scripts": { diff --git a/codemods/react-dom-to-react-dom-factories/codemod.yaml b/codemods/react-dom-to-react-dom-factories/codemod.yaml index 6af9087..49d4df9 100644 --- a/codemods/react-dom-to-react-dom-factories/codemod.yaml +++ b/codemods/react-dom-to-react-dom-factories/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-dom-to-react-dom-factories" -version: "0.1.2" +version: "0.1.3" description: "Replace React.DOM factories with createElement calls" author: "Codemod " license: "MIT" diff --git a/codemods/react-dom-to-react-dom-factories/package.json b/codemods/react-dom-to-react-dom-factories/package.json index 1464aa8..125732f 100644 --- a/codemods/react-dom-to-react-dom-factories/package.json +++ b/codemods/react-dom-to-react-dom-factories/package.json @@ -1,6 +1,6 @@ { "name": "react-dom-to-react-dom-factories", - "version": "0.1.2", + "version": "0.1.3", "description": "Replace React.DOM factories with createElement calls", "type": "module", "scripts": { diff --git a/codemods/react-native-view-prop-types/codemod.yaml b/codemods/react-native-view-prop-types/codemod.yaml index 76318f7..fe1e233 100644 --- a/codemods/react-native-view-prop-types/codemod.yaml +++ b/codemods/react-native-view-prop-types/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-native-view-prop-types" -version: "0.1.2" +version: "0.1.3" description: "Replace View.propTypes with ViewPropTypes" author: "Codemod " license: "MIT" diff --git a/codemods/react-native-view-prop-types/package.json b/codemods/react-native-view-prop-types/package.json index 4432f71..c14fc63 100644 --- a/codemods/react-native-view-prop-types/package.json +++ b/codemods/react-native-view-prop-types/package.json @@ -1,6 +1,6 @@ { "name": "react-native-view-prop-types", - "version": "0.1.2", + "version": "0.1.3", "description": "Replace View.propTypes with ViewPropTypes", "type": "module", "scripts": { diff --git a/codemods/react-proptypes-to-prop-types/codemod.yaml b/codemods/react-proptypes-to-prop-types/codemod.yaml index 3651ff6..4b50d6a 100644 --- a/codemods/react-proptypes-to-prop-types/codemod.yaml +++ b/codemods/react-proptypes-to-prop-types/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-proptypes-to-prop-types" -version: "0.1.2" +version: "0.1.3" description: "Replace React.PropTypes with prop-types package" author: "Codemod " license: "MIT" diff --git a/codemods/react-proptypes-to-prop-types/package.json b/codemods/react-proptypes-to-prop-types/package.json index b94af94..5649978 100644 --- a/codemods/react-proptypes-to-prop-types/package.json +++ b/codemods/react-proptypes-to-prop-types/package.json @@ -1,6 +1,6 @@ { "name": "react-proptypes-to-prop-types", - "version": "0.1.2", + "version": "0.1.3", "description": "Replace React.PropTypes with prop-types package", "type": "module", "scripts": { diff --git a/codemods/react-to-react-dom/codemod.yaml b/codemods/react-to-react-dom/codemod.yaml index a7bc1ea..39cd2a0 100644 --- a/codemods/react-to-react-dom/codemod.yaml +++ b/codemods/react-to-react-dom/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-to-react-dom" -version: "0.1.2" +version: "0.1.3" description: "Move DOM APIs from react to react-dom" author: "Codemod " license: "MIT" diff --git a/codemods/react-to-react-dom/package.json b/codemods/react-to-react-dom/package.json index 0b811f9..00eba43 100644 --- a/codemods/react-to-react-dom/package.json +++ b/codemods/react-to-react-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-to-react-dom", - "version": "0.1.2", + "version": "0.1.3", "description": "Move DOM APIs from react to react-dom", "type": "module", "scripts": { diff --git a/codemods/remove-forward-ref/codemod.yaml b/codemods/remove-forward-ref/codemod.yaml index a39875e..fe7bc82 100644 --- a/codemods/remove-forward-ref/codemod.yaml +++ b/codemods/remove-forward-ref/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-remove-forward-ref" -version: "0.1.2" +version: "0.1.3" description: "Inline forwardRef render functions and move ref into props" author: "Codemod " license: "MIT" diff --git a/codemods/remove-forward-ref/package.json b/codemods/remove-forward-ref/package.json index 960e288..5cc1542 100644 --- a/codemods/remove-forward-ref/package.json +++ b/codemods/remove-forward-ref/package.json @@ -1,6 +1,6 @@ { "name": "react-19-remove-forward-ref", - "version": "0.1.2", + "version": "0.1.3", "description": "Inline forwardRef render functions and move ref into props", "type": "module", "scripts": { diff --git a/codemods/remove-legacy-context/codemod.yaml b/codemods/remove-legacy-context/codemod.yaml index c24fa63..fc81d3c 100644 --- a/codemods/remove-legacy-context/codemod.yaml +++ b/codemods/remove-legacy-context/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-remove-legacy-context" -version: "0.1.2" +version: "0.1.3" description: "Remove legacy childContextTypes/getChildContext usage from React class components" author: "Codemod " license: "MIT" diff --git a/codemods/remove-legacy-context/package.json b/codemods/remove-legacy-context/package.json index cbd1250..ad5e44e 100644 --- a/codemods/remove-legacy-context/package.json +++ b/codemods/remove-legacy-context/package.json @@ -1,6 +1,6 @@ { "name": "react-19-remove-legacy-context", - "version": "0.1.2", + "version": "0.1.3", "description": "Remove legacy childContextTypes/getChildContext usage from React class components", "type": "module", "scripts": { diff --git a/codemods/remove-memoization/codemod.yaml b/codemods/remove-memoization/codemod.yaml index c45879e..b8cd2ad 100644 --- a/codemods/remove-memoization/codemod.yaml +++ b/codemods/remove-memoization/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-remove-memoization" -version: "0.1.2" +version: "0.1.3" description: "Remove manual React memoization calls for React Compiler adoption" author: "Codemod " license: "MIT" diff --git a/codemods/remove-memoization/package.json b/codemods/remove-memoization/package.json index 81088a8..d5115ad 100644 --- a/codemods/remove-memoization/package.json +++ b/codemods/remove-memoization/package.json @@ -1,6 +1,6 @@ { "name": "react-19-remove-memoization", - "version": "0.1.2", + "version": "0.1.3", "description": "Remove manual React memoization calls for React Compiler adoption", "type": "module", "scripts": { diff --git a/codemods/replace-act-import/codemod.yaml b/codemods/replace-act-import/codemod.yaml index 77507c6..6f91d2f 100644 --- a/codemods/replace-act-import/codemod.yaml +++ b/codemods/replace-act-import/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-replace-act-import" -version: "0.1.2" +version: "0.1.3" description: "Migrate act() from react-dom/test-utils to react" author: "Codemod " license: "MIT" diff --git a/codemods/replace-act-import/package.json b/codemods/replace-act-import/package.json index 9ef2598..ac948d9 100644 --- a/codemods/replace-act-import/package.json +++ b/codemods/replace-act-import/package.json @@ -1,6 +1,6 @@ { "name": "react-19-replace-act-import", - "version": "0.1.2", + "version": "0.1.3", "description": "Migrate act() from react-dom/test-utils to react", "type": "module", "scripts": { diff --git a/codemods/replace-create-factory/codemod.yaml b/codemods/replace-create-factory/codemod.yaml index a75185f..9808134 100644 --- a/codemods/replace-create-factory/codemod.yaml +++ b/codemods/replace-create-factory/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-replace-create-factory" -version: "0.1.2" +version: "0.1.3" description: "Replace deprecated React.createFactory calls with JSX" author: "Codemod " license: "MIT" diff --git a/codemods/replace-create-factory/package.json b/codemods/replace-create-factory/package.json index e7781b4..ad95bf9 100644 --- a/codemods/replace-create-factory/package.json +++ b/codemods/replace-create-factory/package.json @@ -1,6 +1,6 @@ { "name": "react-19-replace-create-factory", - "version": "0.1.2", + "version": "0.1.3", "description": "Replace deprecated React.createFactory calls with JSX", "type": "module", "scripts": { diff --git a/codemods/replace-reactdom-render/codemod.yaml b/codemods/replace-reactdom-render/codemod.yaml index 08b571a..4f3a6f9 100644 --- a/codemods/replace-reactdom-render/codemod.yaml +++ b/codemods/replace-reactdom-render/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-replace-reactdom-render" -version: "0.1.2" +version: "0.1.3" description: "Replace ReactDOM.render with createRoot for React 18" author: "Codemod " license: "MIT" diff --git a/codemods/replace-reactdom-render/package.json b/codemods/replace-reactdom-render/package.json index aa36ea3..feb4378 100644 --- a/codemods/replace-reactdom-render/package.json +++ b/codemods/replace-reactdom-render/package.json @@ -1,6 +1,6 @@ { "name": "react-19-replace-reactdom-render", - "version": "0.1.2", + "version": "0.1.3", "description": "Replace ReactDOM.render with createRoot for React 18", "type": "module", "scripts": { diff --git a/codemods/replace-string-ref/codemod.yaml b/codemods/replace-string-ref/codemod.yaml index 09f5da4..2db234d 100644 --- a/codemods/replace-string-ref/codemod.yaml +++ b/codemods/replace-string-ref/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-replace-string-ref" -version: "0.1.2" +version: "0.1.3" description: "Replace string refs with callback refs in React class components" author: "Codemod " license: "MIT" diff --git a/codemods/replace-string-ref/package.json b/codemods/replace-string-ref/package.json index f841d15..8a5dc72 100644 --- a/codemods/replace-string-ref/package.json +++ b/codemods/replace-string-ref/package.json @@ -1,6 +1,6 @@ { "name": "react-19-replace-string-ref", - "version": "0.1.2", + "version": "0.1.3", "description": "Replace string refs with callback refs in React class components", "type": "module", "scripts": { diff --git a/codemods/replace-use-form-state/codemod.yaml b/codemods/replace-use-form-state/codemod.yaml index 85784e0..071b3f5 100644 --- a/codemods/replace-use-form-state/codemod.yaml +++ b/codemods/replace-use-form-state/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-replace-use-form-state" -version: "0.1.2" +version: "0.1.3" description: "Rename useFormState to useActionState in react-dom imports and usages" author: "Codemod " license: "MIT" diff --git a/codemods/replace-use-form-state/package.json b/codemods/replace-use-form-state/package.json index ecfa478..fcf69f7 100644 --- a/codemods/replace-use-form-state/package.json +++ b/codemods/replace-use-form-state/package.json @@ -1,6 +1,6 @@ { "name": "react-19-replace-use-form-state", - "version": "0.1.2", + "version": "0.1.3", "description": "Rename useFormState to useActionState in react-dom imports and usages", "type": "module", "scripts": { diff --git a/codemods/sort-comp/codemod.yaml b/codemods/sort-comp/codemod.yaml index d70127b..a1963ca 100644 --- a/codemods/sort-comp/codemod.yaml +++ b/codemods/sort-comp/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-sort-comp" -version: "0.1.2" +version: "0.1.3" description: "Reorder React component methods to match eslint-plugin-react sort-comp rule" author: "Codemod " license: "MIT" diff --git a/codemods/sort-comp/package.json b/codemods/sort-comp/package.json index 38a89f3..1401cfa 100644 --- a/codemods/sort-comp/package.json +++ b/codemods/sort-comp/package.json @@ -1,6 +1,6 @@ { "name": "react-sort-comp", - "version": "0.1.2", + "version": "0.1.3", "description": "Reorder React component methods to match eslint-plugin-react sort-comp rule", "type": "module", "scripts": { diff --git a/codemods/unprefix-stable-apis/codemod.yaml b/codemods/unprefix-stable-apis/codemod.yaml index 46d07a9..3b864bb 100644 --- a/codemods/unprefix-stable-apis/codemod.yaml +++ b/codemods/unprefix-stable-apis/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-3-unprefix-stable-apis" -version: "0.1.1" +version: "0.1.2" description: "Rename unstable_/experimental_ React APIs that became stable in React 19.2 and 19.3 to their stable names" author: "Codemod " license: "MIT" diff --git a/codemods/unprefix-stable-apis/package.json b/codemods/unprefix-stable-apis/package.json index 1bbe8aa..eb03642 100644 --- a/codemods/unprefix-stable-apis/package.json +++ b/codemods/unprefix-stable-apis/package.json @@ -1,6 +1,6 @@ { "name": "react-19-3-unprefix-stable-apis", - "version": "0.1.1", + "version": "0.1.2", "description": "Rename unstable_/experimental_ React APIs that became stable in React 19.2 and 19.3 to their stable names", "type": "module", "scripts": { diff --git a/codemods/update-react-imports/codemod.yaml b/codemods/update-react-imports/codemod.yaml index b5907ea..359fda8 100644 --- a/codemods/update-react-imports/codemod.yaml +++ b/codemods/update-react-imports/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-update-react-imports" -version: "0.1.2" +version: "0.1.3" description: "Remove unused React default import and convert to named imports" author: "Codemod " license: "MIT" diff --git a/codemods/update-react-imports/package.json b/codemods/update-react-imports/package.json index 838f574..a3ac694 100644 --- a/codemods/update-react-imports/package.json +++ b/codemods/update-react-imports/package.json @@ -1,6 +1,6 @@ { "name": "react-update-react-imports", - "version": "0.1.2", + "version": "0.1.3", "description": "Remove unused React default import and convert to named imports", "type": "module", "scripts": { diff --git a/codemods/update-react-imports/tests/destructure-no-semicolon/expected.tsx b/codemods/update-react-imports/tests/destructure-no-semicolon/expected.tsx new file mode 100644 index 0000000..b21442b --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-no-semicolon/expected.tsx @@ -0,0 +1,2 @@ +import { useState } from "react"; +const [x] = useState(0) diff --git a/codemods/update-react-imports/tests/destructure-no-semicolon/input.tsx b/codemods/update-react-imports/tests/destructure-no-semicolon/input.tsx new file mode 100644 index 0000000..cec0c14 --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-no-semicolon/input.tsx @@ -0,0 +1,2 @@ +import React from "react" +const [x] = React.useState(0) diff --git a/codemods/update-react-imports/tests/destructure-no-semicolon/metrics.json b/codemods/update-react-imports/tests/destructure-no-semicolon/metrics.json new file mode 100644 index 0000000..2022e57 --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-no-semicolon/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-member-to-named", + "file": "tests/destructure-no-semicolon/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/unused-default-single-quote/expected.tsx b/codemods/update-react-imports/tests/unused-default-single-quote/expected.tsx new file mode 100644 index 0000000..ad1d380 --- /dev/null +++ b/codemods/update-react-imports/tests/unused-default-single-quote/expected.tsx @@ -0,0 +1 @@ +export const x = 1; diff --git a/codemods/update-react-imports/tests/unused-default-single-quote/input.tsx b/codemods/update-react-imports/tests/unused-default-single-quote/input.tsx new file mode 100644 index 0000000..a9cc41b --- /dev/null +++ b/codemods/update-react-imports/tests/unused-default-single-quote/input.tsx @@ -0,0 +1,3 @@ +import React from 'react' + +export const x = 1; diff --git a/codemods/update-react-imports/tests/unused-default-single-quote/metrics.json b/codemods/update-react-imports/tests/unused-default-single-quote/metrics.json new file mode 100644 index 0000000..f0477f0 --- /dev/null +++ b/codemods/update-react-imports/tests/unused-default-single-quote/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/unused-default-single-quote/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/use-browser/codemod.yaml b/codemods/use-browser/codemod.yaml index 238e27d..6892f4e 100644 --- a/codemods/use-browser/codemod.yaml +++ b/codemods/use-browser/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-3-use-browser" -version: "0.1.1" +version: "0.1.2" description: "Replace the mounted-state pattern for browser-only components with use(browser()) from react-dom" author: "Codemod " license: "MIT" diff --git a/codemods/use-browser/package.json b/codemods/use-browser/package.json index 8f6613a..ae8b4e8 100644 --- a/codemods/use-browser/package.json +++ b/codemods/use-browser/package.json @@ -1,6 +1,6 @@ { "name": "react-19-3-use-browser", - "version": "0.1.1", + "version": "0.1.2", "description": "Replace the mounted-state pattern for browser-only components with use(browser()) from react-dom", "type": "module", "scripts": { diff --git a/codemods/use-context-hook/codemod.yaml b/codemods/use-context-hook/codemod.yaml index 6383c70..e16e2ee 100644 --- a/codemods/use-context-hook/codemod.yaml +++ b/codemods/use-context-hook/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "react-19-use-context-hook" -version: "0.1.2" +version: "0.1.3" description: "Transform React.useContext() and useContext() to use() hook" author: "Codemod " license: "MIT" diff --git a/codemods/use-context-hook/package.json b/codemods/use-context-hook/package.json index 235ffe0..f7e9e52 100644 --- a/codemods/use-context-hook/package.json +++ b/codemods/use-context-hook/package.json @@ -1,6 +1,6 @@ { "name": "react-19-use-context-hook", - "version": "0.1.2", + "version": "0.1.3", "description": "Transform React.useContext() and useContext() to use() hook", "type": "module", "scripts": { From e86a1836bb328be28db2aa36981e131e548d834f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Oct 2026 21:54:50 +0000 Subject: [PATCH 07/10] Fix CI flake: pin codemod CLI and stop using pnpm dlx in tests Parallel `pnpm -r test` raced on the shared pnpm dlx cache (ENOENT unlink under ~/.cache/pnpm/dlx). Install codemod@1.18.6 at the workspace root and run tests through scripts/run-codemod.mjs so packages keep their cwd without concurrent dlx installs. Co-authored-by: Alex Bit --- codemods/create-element-to-jsx/package.json | 2 +- .../scripts/differential-tests.mjs | 7 +- .../scripts/error-tests.mjs | 7 +- codemods/error-boundaries/package.json | 2 +- codemods/find-dom-node/package.json | 2 +- codemods/fragment-ref-wrappers/package.json | 2 +- codemods/manual-bind-to-arrow/package.json | 2 +- .../scripts/parity-tests.mjs | 7 +- codemods/pure-component/package.json | 2 +- .../scripts/differential-tests.mjs | 7 +- .../pure-component/scripts/warning-tests.mjs | 7 +- codemods/pure-render-mixin/package.json | 2 +- .../package.json | 2 +- .../react-native-view-prop-types/package.json | 2 +- .../package.json | 2 +- codemods/react-to-react-dom/package.json | 2 +- .../scripts/error-tests.mjs | 7 +- codemods/remove-context-provider/package.json | 2 +- codemods/remove-forward-ref/package.json | 2 +- codemods/remove-legacy-context/package.json | 2 +- codemods/remove-memoization/package.json | 2 +- .../rename-unsafe-lifecycles/package.json | 2 +- codemods/replace-act-import/package.json | 2 +- codemods/replace-create-factory/package.json | 2 +- codemods/replace-default-props/package.json | 2 +- .../package.json | 2 +- codemods/replace-reactdom-render/package.json | 2 +- codemods/replace-string-ref/package.json | 2 +- codemods/replace-use-form-state/package.json | 2 +- codemods/sort-comp/package.json | 2 +- codemods/unprefix-stable-apis/package.json | 2 +- codemods/update-react-imports/package.json | 2 +- codemods/use-browser/package.json | 2 +- codemods/use-context-hook/package.json | 2 +- package.json | 3 +- pnpm-lock.yaml | 509 ++++++++++++++---- scripts/run-codemod.mjs | 33 ++ 37 files changed, 473 insertions(+), 170 deletions(-) create mode 100644 scripts/run-codemod.mjs diff --git a/codemods/create-element-to-jsx/package.json b/codemods/create-element-to-jsx/package.json index 4bb3435..0fa947b 100644 --- a/codemods/create-element-to-jsx/package.json +++ b/codemods/create-element-to-jsx/package.json @@ -4,7 +4,7 @@ "description": "Convert React.createElement calls to JSX", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/error-tests.mjs ./scripts/differential-tests.mjs", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/error-tests.mjs ./scripts/differential-tests.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/create-element-to-jsx/scripts/differential-tests.mjs b/codemods/create-element-to-jsx/scripts/differential-tests.mjs index bfd1d5b..276339f 100644 --- a/codemods/create-element-to-jsx/scripts/differential-tests.mjs +++ b/codemods/create-element-to-jsx/scripts/differential-tests.mjs @@ -8,6 +8,7 @@ import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); +const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const workflowPath = path.join(packageDir, "workflow.yaml"); const testsDir = path.join(packageDir, "tests"); @@ -26,11 +27,7 @@ function runJssg(source) { spawnSync("git", ["add", "input.tsx", ".gitignore"], { cwd: dir }); spawnSync("git", ["commit", "-qm", "init"], { cwd: dir }); - const result = spawnSync( - "pnpm", - [ - "dlx", - "codemod@latest", + const result = spawnSync("node", [codemodRunner, "workflow", "run", "-w", diff --git a/codemods/create-element-to-jsx/scripts/error-tests.mjs b/codemods/create-element-to-jsx/scripts/error-tests.mjs index d24d518..04c4e82 100644 --- a/codemods/create-element-to-jsx/scripts/error-tests.mjs +++ b/codemods/create-element-to-jsx/scripts/error-tests.mjs @@ -8,6 +8,7 @@ import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); +const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const workflowPath = path.join(packageDir, "workflow.yaml"); function runCodemod(input) { @@ -16,11 +17,7 @@ function runCodemod(input) { writeFileSync(filePath, input, "utf8"); try { - return spawnSync( - "pnpm", - [ - "dlx", - "codemod@latest", + return spawnSync("node", [codemodRunner, "workflow", "run", "-w", diff --git a/codemods/error-boundaries/package.json b/codemods/error-boundaries/package.json index 5dc2d6a..fac8d60 100644 --- a/codemods/error-boundaries/package.json +++ b/codemods/error-boundaries/package.json @@ -4,7 +4,7 @@ "description": "Rename unstable_handleError to componentDidCatch", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/find-dom-node/package.json b/codemods/find-dom-node/package.json index 293c9fd..a9b029e 100644 --- a/codemods/find-dom-node/package.json +++ b/codemods/find-dom-node/package.json @@ -4,7 +4,7 @@ "description": "Replace getDOMNode() with React.findDOMNode()", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/fragment-ref-wrappers/package.json b/codemods/fragment-ref-wrappers/package.json index ce94bb2..6c60718 100644 --- a/codemods/fragment-ref-wrappers/package.json +++ b/codemods/fragment-ref-wrappers/package.json @@ -4,7 +4,7 @@ "description": "Replace display:contents wrapper elements that only exist to hold a ref with a Fragment ref", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/manual-bind-to-arrow/package.json b/codemods/manual-bind-to-arrow/package.json index 35302ff..a6b358f 100644 --- a/codemods/manual-bind-to-arrow/package.json +++ b/codemods/manual-bind-to-arrow/package.json @@ -4,7 +4,7 @@ "description": "Convert this.method = this.method.bind(this) to arrow class property", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/prop-types-typescript/scripts/parity-tests.mjs b/codemods/prop-types-typescript/scripts/parity-tests.mjs index 6f6ec47..ff4d0bf 100644 --- a/codemods/prop-types-typescript/scripts/parity-tests.mjs +++ b/codemods/prop-types-typescript/scripts/parity-tests.mjs @@ -10,6 +10,7 @@ import { cases } from "./parity-cases.mjs"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); +const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const codemodPath = path.join(packageDir, "scripts", "codemod.ts"); const resultsCache = new Map(); @@ -50,9 +51,7 @@ function runMode(mode) { writeFileSync(path.join(dir, filename), testCase.input, "utf8"); } - const command = [ - "dlx", - "codemod@latest", + const command = [codemodRunner, "jssg", "run", "--language", @@ -67,7 +66,7 @@ function runMode(mode) { command.push("--param", `preserve-prop-types=${mode}`); } - const result = spawnSync("pnpm", command, { + const result = spawnSync("node", command, { cwd: packageDir, encoding: "utf8", timeout: 120000, diff --git a/codemods/pure-component/package.json b/codemods/pure-component/package.json index 367b4cc..160a3cb 100644 --- a/codemods/pure-component/package.json +++ b/codemods/pure-component/package.json @@ -4,7 +4,7 @@ "description": "Convert simple React classes into pure function components", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/warning-tests.mjs ./scripts/differential-tests.mjs", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/warning-tests.mjs ./scripts/differential-tests.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/pure-component/scripts/differential-tests.mjs b/codemods/pure-component/scripts/differential-tests.mjs index c1ec390..fc30ced 100644 --- a/codemods/pure-component/scripts/differential-tests.mjs +++ b/codemods/pure-component/scripts/differential-tests.mjs @@ -8,6 +8,7 @@ import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); +const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const codemodFile = path.join(packageDir, "scripts/codemod.ts"); const testsDir = path.join(packageDir, "tests"); @@ -36,11 +37,7 @@ function runJssgOutput(input, params = {}) { } try { - const result = spawnSync( - "pnpm", - [ - "dlx", - "codemod@latest", + const result = spawnSync("node", [codemodRunner, "jssg", "test", "-l", diff --git a/codemods/pure-component/scripts/warning-tests.mjs b/codemods/pure-component/scripts/warning-tests.mjs index 9940a0f..28fbc6b 100644 --- a/codemods/pure-component/scripts/warning-tests.mjs +++ b/codemods/pure-component/scripts/warning-tests.mjs @@ -8,6 +8,7 @@ import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); +const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const codemodFile = path.join(packageDir, "scripts/codemod.ts"); function runFixtureTest(input, params = {}) { @@ -21,11 +22,7 @@ function runFixtureTest(input, params = {}) { } try { - return spawnSync( - "pnpm", - [ - "dlx", - "codemod@latest", + return spawnSync("node", [codemodRunner, "jssg", "test", "-l", diff --git a/codemods/pure-render-mixin/package.json b/codemods/pure-render-mixin/package.json index 56e0c54..4dd2210 100644 --- a/codemods/pure-render-mixin/package.json +++ b/codemods/pure-render-mixin/package.json @@ -4,7 +4,7 @@ "description": "Replace PureRenderMixin with shouldComponentUpdate using React.addons.shallowCompare", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/react-dom-to-react-dom-factories/package.json b/codemods/react-dom-to-react-dom-factories/package.json index 125732f..c6ab5fa 100644 --- a/codemods/react-dom-to-react-dom-factories/package.json +++ b/codemods/react-dom-to-react-dom-factories/package.json @@ -4,7 +4,7 @@ "description": "Replace React.DOM factories with createElement calls", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts --strictness loose", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/react-native-view-prop-types/package.json b/codemods/react-native-view-prop-types/package.json index c14fc63..acde9da 100644 --- a/codemods/react-native-view-prop-types/package.json +++ b/codemods/react-native-view-prop-types/package.json @@ -4,7 +4,7 @@ "description": "Replace View.propTypes with ViewPropTypes", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts --strictness loose", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/react-proptypes-to-prop-types/package.json b/codemods/react-proptypes-to-prop-types/package.json index 5649978..0ca57b9 100644 --- a/codemods/react-proptypes-to-prop-types/package.json +++ b/codemods/react-proptypes-to-prop-types/package.json @@ -4,7 +4,7 @@ "description": "Replace React.PropTypes with prop-types package", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/react-to-react-dom/package.json b/codemods/react-to-react-dom/package.json index 00eba43..1512b97 100644 --- a/codemods/react-to-react-dom/package.json +++ b/codemods/react-to-react-dom/package.json @@ -4,7 +4,7 @@ "description": "Move DOM APIs from react to react-dom", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose && node --test ./scripts/error-tests.mjs", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts --strictness loose && node --test ./scripts/error-tests.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/react-to-react-dom/scripts/error-tests.mjs b/codemods/react-to-react-dom/scripts/error-tests.mjs index 5f589ca..5398fef 100644 --- a/codemods/react-to-react-dom/scripts/error-tests.mjs +++ b/codemods/react-to-react-dom/scripts/error-tests.mjs @@ -8,6 +8,7 @@ import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); +const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const workflowPath = path.join(packageDir, "workflow.yaml"); function runCodemod(input) { @@ -16,11 +17,7 @@ function runCodemod(input) { writeFileSync(filePath, input, "utf8"); try { - return spawnSync( - "pnpm", - [ - "dlx", - "codemod@latest", + return spawnSync("node", [codemodRunner, "workflow", "run", "-w", diff --git a/codemods/remove-context-provider/package.json b/codemods/remove-context-provider/package.json index bd87b4d..0e35954 100644 --- a/codemods/remove-context-provider/package.json +++ b/codemods/remove-context-provider/package.json @@ -4,7 +4,7 @@ "description": "Replace Context.Provider with Context in JSX", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/remove-forward-ref/package.json b/codemods/remove-forward-ref/package.json index 5cc1542..fbbc4e8 100644 --- a/codemods/remove-forward-ref/package.json +++ b/codemods/remove-forward-ref/package.json @@ -4,7 +4,7 @@ "description": "Inline forwardRef render functions and move ref into props", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts --strictness loose", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/remove-legacy-context/package.json b/codemods/remove-legacy-context/package.json index ad5e44e..85f2457 100644 --- a/codemods/remove-legacy-context/package.json +++ b/codemods/remove-legacy-context/package.json @@ -4,7 +4,7 @@ "description": "Remove legacy childContextTypes/getChildContext usage from React class components", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/remove-memoization/package.json b/codemods/remove-memoization/package.json index d5115ad..67088e7 100644 --- a/codemods/remove-memoization/package.json +++ b/codemods/remove-memoization/package.json @@ -4,7 +4,7 @@ "description": "Remove manual React memoization calls for React Compiler adoption", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/rename-unsafe-lifecycles/package.json b/codemods/rename-unsafe-lifecycles/package.json index 8ebd9ff..0933836 100644 --- a/codemods/rename-unsafe-lifecycles/package.json +++ b/codemods/rename-unsafe-lifecycles/package.json @@ -4,7 +4,7 @@ "description": "Rename deprecated lifecycle methods to UNSAFE_ prefixed versions", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/replace-act-import/package.json b/codemods/replace-act-import/package.json index ac948d9..8719363 100644 --- a/codemods/replace-act-import/package.json +++ b/codemods/replace-act-import/package.json @@ -4,7 +4,7 @@ "description": "Migrate act() from react-dom/test-utils to react", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/replace-create-factory/package.json b/codemods/replace-create-factory/package.json index ad95bf9..1a13e24 100644 --- a/codemods/replace-create-factory/package.json +++ b/codemods/replace-create-factory/package.json @@ -4,7 +4,7 @@ "description": "Replace deprecated React.createFactory calls with JSX", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/replace-default-props/package.json b/codemods/replace-default-props/package.json index fc76b56..90578a3 100644 --- a/codemods/replace-default-props/package.json +++ b/codemods/replace-default-props/package.json @@ -4,7 +4,7 @@ "description": "Replace React function component defaultProps with default parameters", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/replace-react-test-renderer-import/package.json b/codemods/replace-react-test-renderer-import/package.json index 274997c..2c4e90d 100644 --- a/codemods/replace-react-test-renderer-import/package.json +++ b/codemods/replace-react-test-renderer-import/package.json @@ -4,7 +4,7 @@ "description": "Replace react-test-renderer shallow renderer imports with react-shallow-renderer", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/replace-reactdom-render/package.json b/codemods/replace-reactdom-render/package.json index feb4378..be7bc42 100644 --- a/codemods/replace-reactdom-render/package.json +++ b/codemods/replace-reactdom-render/package.json @@ -4,7 +4,7 @@ "description": "Replace ReactDOM.render with createRoot for React 18", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/replace-string-ref/package.json b/codemods/replace-string-ref/package.json index 8a5dc72..624bceb 100644 --- a/codemods/replace-string-ref/package.json +++ b/codemods/replace-string-ref/package.json @@ -4,7 +4,7 @@ "description": "Replace string refs with callback refs in React class components", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/replace-use-form-state/package.json b/codemods/replace-use-form-state/package.json index fcf69f7..a7b5383 100644 --- a/codemods/replace-use-form-state/package.json +++ b/codemods/replace-use-form-state/package.json @@ -4,7 +4,7 @@ "description": "Rename useFormState to useActionState in react-dom imports and usages", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/sort-comp/package.json b/codemods/sort-comp/package.json index 1401cfa..d7fc257 100644 --- a/codemods/sort-comp/package.json +++ b/codemods/sort-comp/package.json @@ -4,7 +4,7 @@ "description": "Reorder React component methods to match eslint-plugin-react sort-comp rule", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/unprefix-stable-apis/package.json b/codemods/unprefix-stable-apis/package.json index eb03642..183c0fb 100644 --- a/codemods/unprefix-stable-apis/package.json +++ b/codemods/unprefix-stable-apis/package.json @@ -4,7 +4,7 @@ "description": "Rename unstable_/experimental_ React APIs that became stable in React 19.2 and 19.3 to their stable names", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/update-react-imports/package.json b/codemods/update-react-imports/package.json index a3ac694..5853627 100644 --- a/codemods/update-react-imports/package.json +++ b/codemods/update-react-imports/package.json @@ -4,7 +4,7 @@ "description": "Remove unused React default import and convert to named imports", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/use-browser/package.json b/codemods/use-browser/package.json index ae8b4e8..16cd489 100644 --- a/codemods/use-browser/package.json +++ b/codemods/use-browser/package.json @@ -4,7 +4,7 @@ "description": "Replace the mounted-state pattern for browser-only components with use(browser()) from react-dom", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/use-context-hook/package.json b/codemods/use-context-hook/package.json index f7e9e52..6d45383 100644 --- a/codemods/use-context-hook/package.json +++ b/codemods/use-context-hook/package.json @@ -4,7 +4,7 @@ "description": "Transform React.useContext() and useContext() to use() hook", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/package.json b/package.json index fa33d37..f221e22 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "packageManager": "pnpm@9.14.2", "devDependencies": { "@changesets/cli": "^2.30.0", - "@types/node": "latest" + "@types/node": "latest", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc38c96..0799e09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,10 +14,13 @@ importers: devDependencies: '@changesets/cli': specifier: ^2.30.0 - version: 2.30.0(@types/node@25.6.0) + version: 2.30.0(@types/node@26.6.6) '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 codemods/create-element-to-jsx: dependencies: @@ -27,25 +30,25 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/error-boundaries: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/find-dom-node: dependencies: @@ -55,13 +58,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/fragment-ref-wrappers: dependencies: @@ -71,25 +74,25 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/manual-bind-to-arrow: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/prop-types-typescript: dependencies: @@ -99,13 +102,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/pure-component: dependencies: @@ -115,13 +118,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/pure-render-mixin: dependencies: @@ -131,13 +134,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/react-19-3-migration-recipe: {} @@ -151,13 +154,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/react-native-view-prop-types: dependencies: @@ -167,13 +170,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/react-proptypes-to-prop-types: dependencies: @@ -183,13 +186,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/react-to-react-dom: dependencies: @@ -199,25 +202,25 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/remove-context-provider: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/remove-forward-ref: dependencies: @@ -227,13 +230,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/remove-legacy-context: dependencies: @@ -243,13 +246,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/remove-memoization: dependencies: @@ -259,25 +262,25 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/rename-unsafe-lifecycles: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/replace-act-import: dependencies: @@ -287,13 +290,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/replace-create-factory: dependencies: @@ -303,37 +306,37 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.6.0 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/replace-default-props: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.6.0 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/replace-react-test-renderer-import: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.6.0 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/replace-reactdom-render: dependencies: @@ -343,13 +346,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/replace-string-ref: dependencies: @@ -359,13 +362,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/replace-use-form-state: dependencies: @@ -375,13 +378,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/sort-comp: dependencies: @@ -391,13 +394,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/unprefix-stable-apis: dependencies: @@ -407,13 +410,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/update-react-imports: dependencies: @@ -423,13 +426,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/use-browser: dependencies: @@ -439,13 +442,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 codemods/use-context-hook: dependencies: @@ -455,13 +458,13 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.2 + version: 1.6.3 '@types/node': specifier: latest - version: 25.6.0 + version: 26.6.6 typescript: specifier: latest - version: 6.0.3 + version: 7.0.2 packages: @@ -524,11 +527,50 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@codemod.com/jssg-types@1.5.2': - resolution: {integrity: sha512-hZ+CuFL7BzE74VKSOG7xHaxgoRahEoOPdp0N9eg4KAwaYt0gc0YNMI/UvCRZKUTnnRBgTswrwgCbHZMDhjTWoQ==} - - '@codemod.com/jssg-types@1.6.0': - resolution: {integrity: sha512-9b6rsL3RZJvOPJ+gUj1W0041mHF6eYwLt+wsa4zqxJWNsxLha0JvFyaQdcw5XfQ5Ldd+H/kV/yZ4dpN+hC9OXw==} + '@codemod.com/cli-darwin-arm64@1.18.6': + resolution: {integrity: sha512-uI1rnUMWDPNSJqJWLd8Ny/QIRgu/eOUBi0WhQLYtghHwMtWBcHi/Tj2DVS06MV3oBkgnvDQ5bAjk1jAC/lj08Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@codemod.com/cli-darwin-x64@1.18.6': + resolution: {integrity: sha512-fk4O3MHRzBsO9NoSEyTsAQnAqsD9+H+9hzXkDvH/aF3ty+woHD7nODy66LfkKIgpGz9xK9VfyU2vrPrFC3jehg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@codemod.com/cli-linux-arm64-gnu@1.18.6': + resolution: {integrity: sha512-dCmspZYPDVBX8EW93AR5ZHlllxF0bSBHCdkOib9i9Z1Xj8lBXuPZNfutL/BdRzoO37L618Hror9pNbXyYoqCvg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@codemod.com/cli-linux-arm64-musl@1.18.6': + resolution: {integrity: sha512-OnKJpNEFp2MeF4zJRb8CwE1baalr16PNJHGSCsMhYY/HNRP5RLS+/w05e7nx0RMm4FMA5hFCBhHBA2D+9vk0Zg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@codemod.com/cli-linux-x64-gnu@1.18.6': + resolution: {integrity: sha512-vKlGc40CBePeweGtstNt2wmMmhUT4D1as4sPV9IKKitchG5CEFI5HIizvVC1PkPmME7edGr9gT5huHex6Y5zLQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@codemod.com/cli-linux-x64-musl@1.18.6': + resolution: {integrity: sha512-4fFToEd8p5FJ27CWVJzGFWp9ys/bwAHTLrlAh7ms6sXcHFnq17s1YmuKbmCfTbfpjkKtpkwrExhNMq0RRqJPvw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@codemod.com/cli-win32-x64-msvc@1.18.6': + resolution: {integrity: sha512-/l/na7Vg7BaJn0m6QzbnfLZKjHrdDWacE9YVvfx2gG3v7wWCNiBxLcGX1ZqldlrxSJ2yayNa3ZpHdeyJupGV+A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@codemod.com/jssg-types@1.6.3': + resolution: {integrity: sha512-b5829ixO5TdGL5xg5YcL9YfiLC3WwFZU+8Zx78NpsHlJ1iySbqfA0g0psqNXv8ss/r7hdpy2KiBXm3GE6bN5bQ==} '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} @@ -563,8 +605,128 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@25.6.0': - resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/node@26.6.6': + resolution: {integrity: sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==} + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} @@ -595,6 +757,11 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + codemod@1.18.6: + resolution: {integrity: sha512-1ZgVgBuzXGVH/G/6rupovWGCbYCZyl+BZJ6LZdpG9vMILucCtZORtqqoELwDmVbJ5Zb9De+3remZjjG8v2q0nA==} + engines: {node: '>= 16.0.0'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -603,6 +770,10 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -839,13 +1010,13 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} hasBin: true - undici-types@7.19.2: - resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} @@ -889,7 +1060,7 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.30.0(@types/node@25.6.0)': + '@changesets/cli@2.30.0(@types/node@26.6.6)': dependencies: '@changesets/apply-release-plan': 7.1.0 '@changesets/assemble-release-plan': 6.0.9 @@ -905,7 +1076,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@25.6.0) + '@inquirer/external-editor': 1.0.3(@types/node@26.6.6) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 enquirer: 2.4.1 @@ -1003,16 +1174,35 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@codemod.com/jssg-types@1.5.2': {} + '@codemod.com/cli-darwin-arm64@1.18.6': + optional: true + + '@codemod.com/cli-darwin-x64@1.18.6': + optional: true + + '@codemod.com/cli-linux-arm64-gnu@1.18.6': + optional: true + + '@codemod.com/cli-linux-arm64-musl@1.18.6': + optional: true + + '@codemod.com/cli-linux-x64-gnu@1.18.6': + optional: true + + '@codemod.com/cli-linux-x64-musl@1.18.6': + optional: true - '@codemod.com/jssg-types@1.6.0': {} + '@codemod.com/cli-win32-x64-msvc@1.18.6': + optional: true - '@inquirer/external-editor@1.0.3(@types/node@25.6.0)': + '@codemod.com/jssg-types@1.6.3': {} + + '@inquirer/external-editor@1.0.3(@types/node@26.6.6)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 25.6.0 + '@types/node': 26.6.6 '@jssg/utils@0.0.10': {} @@ -1046,9 +1236,69 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@25.6.0': + '@types/node@26.6.6': dependencies: - undici-types: 7.19.2 + undici-types: 8.9.0 + + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true ansi-colors@4.1.3: {} @@ -1072,6 +1322,18 @@ snapshots: chardet@2.1.1: {} + codemod@1.18.6: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + '@codemod.com/cli-darwin-arm64': 1.18.6 + '@codemod.com/cli-darwin-x64': 1.18.6 + '@codemod.com/cli-linux-arm64-gnu': 1.18.6 + '@codemod.com/cli-linux-arm64-musl': 1.18.6 + '@codemod.com/cli-linux-x64-gnu': 1.18.6 + '@codemod.com/cli-linux-x64-musl': 1.18.6 + '@codemod.com/cli-win32-x64-msvc': 1.18.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1080,6 +1342,8 @@ snapshots: detect-indent@6.1.0: {} + detect-libc@2.1.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -1281,9 +1545,30 @@ snapshots: dependencies: is-number: 7.0.0 - typescript@6.0.3: {} - - undici-types@7.19.2: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + + undici-types@8.9.0: {} universalify@0.1.2: {} diff --git a/scripts/run-codemod.mjs b/scripts/run-codemod.mjs new file mode 100644 index 0000000..f6861bd --- /dev/null +++ b/scripts/run-codemod.mjs @@ -0,0 +1,33 @@ +#!/usr/bin/env node +/** + * Run the workspace-installed `codemod` CLI while preserving the caller's cwd. + * + * Package tests used to call `pnpm dlx codemod@latest`, which races on the + * shared dlx cache when `pnpm -r test` fans out. Resolving the pinned root + * dependency avoids that flake and keeps relative paths (./scripts/codemod.ts) + * correct for each package. + */ +import { spawnSync } from "node:child_process"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const require = createRequire(path.join(root, "package.json")); +const codemodPkgDir = path.dirname(require.resolve("codemod/package.json")); +const binField = require(path.join(codemodPkgDir, "package.json")).bin; +const binRelative = typeof binField === "string" ? binField : binField.codemod; +const binPath = path.resolve(codemodPkgDir, binRelative); + +const result = spawnSync(binPath, process.argv.slice(2), { + stdio: "inherit", + cwd: process.cwd(), + env: process.env, +}); + +if (result.error) { + console.error(result.error); + process.exit(1); +} + +process.exit(result.status ?? 1); From a47507fb2d37b8e7d6e3dd2a8a86c3d222b4751e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Oct 2026 21:57:45 +0000 Subject: [PATCH 08/10] Pin TypeScript to 5.9 so parity tests keep the classic compiler API `typescript@latest` resolved to 7.0.2, whose package entry only exports version metadata. That made `ts.ScriptTarget` undefined in prop-types-typescript parity tests on CI. Pin ~5.9.3 via package ranges and a pnpm override. Co-authored-by: Alex Bit --- codemods/create-element-to-jsx/package.json | 2 +- codemods/error-boundaries/package.json | 2 +- codemods/find-dom-node/package.json | 2 +- codemods/fragment-ref-wrappers/package.json | 2 +- codemods/manual-bind-to-arrow/package.json | 2 +- codemods/prop-types-typescript/package.json | 2 +- .../scripts/parity-tests.mjs | 3 +- codemods/pure-component/package.json | 2 +- codemods/pure-render-mixin/package.json | 2 +- .../package.json | 2 +- .../react-native-view-prop-types/package.json | 2 +- .../package.json | 2 +- codemods/react-to-react-dom/package.json | 2 +- codemods/remove-context-provider/package.json | 2 +- codemods/remove-forward-ref/package.json | 2 +- codemods/remove-legacy-context/package.json | 2 +- codemods/remove-memoization/package.json | 2 +- .../rename-unsafe-lifecycles/package.json | 2 +- codemods/replace-act-import/package.json | 2 +- codemods/replace-create-factory/package.json | 2 +- codemods/replace-default-props/package.json | 2 +- .../package.json | 2 +- codemods/replace-reactdom-render/package.json | 2 +- codemods/replace-string-ref/package.json | 2 +- codemods/replace-use-form-state/package.json | 2 +- codemods/sort-comp/package.json | 2 +- codemods/unprefix-stable-apis/package.json | 2 +- codemods/update-react-imports/package.json | 2 +- codemods/use-browser/package.json | 2 +- codemods/use-context-hook/package.json | 2 +- package.json | 5 + pnpm-lock.yaml | 328 ++++-------------- 32 files changed, 101 insertions(+), 293 deletions(-) diff --git a/codemods/create-element-to-jsx/package.json b/codemods/create-element-to-jsx/package.json index 0fa947b..54da119 100644 --- a/codemods/create-element-to-jsx/package.json +++ b/codemods/create-element-to-jsx/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/error-boundaries/package.json b/codemods/error-boundaries/package.json index fac8d60..593d638 100644 --- a/codemods/error-boundaries/package.json +++ b/codemods/error-boundaries/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" } } diff --git a/codemods/find-dom-node/package.json b/codemods/find-dom-node/package.json index a9b029e..f2ee19d 100644 --- a/codemods/find-dom-node/package.json +++ b/codemods/find-dom-node/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" }, "dependencies": { diff --git a/codemods/fragment-ref-wrappers/package.json b/codemods/fragment-ref-wrappers/package.json index 6c60718..686982a 100644 --- a/codemods/fragment-ref-wrappers/package.json +++ b/codemods/fragment-ref-wrappers/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/manual-bind-to-arrow/package.json b/codemods/manual-bind-to-arrow/package.json index a6b358f..256513e 100644 --- a/codemods/manual-bind-to-arrow/package.json +++ b/codemods/manual-bind-to-arrow/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" } } diff --git a/codemods/prop-types-typescript/package.json b/codemods/prop-types-typescript/package.json index 54f0bba..4b2269a 100644 --- a/codemods/prop-types-typescript/package.json +++ b/codemods/prop-types-typescript/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/prop-types-typescript/scripts/parity-tests.mjs b/codemods/prop-types-typescript/scripts/parity-tests.mjs index ff4d0bf..f02db57 100644 --- a/codemods/prop-types-typescript/scripts/parity-tests.mjs +++ b/codemods/prop-types-typescript/scripts/parity-tests.mjs @@ -5,7 +5,8 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; -import ts from "typescript"; +import * as typescriptNs from "typescript"; +const ts = typescriptNs.default ?? typescriptNs; import { cases } from "./parity-cases.mjs"; const __filename = fileURLToPath(import.meta.url); diff --git a/codemods/pure-component/package.json b/codemods/pure-component/package.json index 160a3cb..5a763c0 100644 --- a/codemods/pure-component/package.json +++ b/codemods/pure-component/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/pure-render-mixin/package.json b/codemods/pure-render-mixin/package.json index 4dd2210..7516695 100644 --- a/codemods/pure-render-mixin/package.json +++ b/codemods/pure-render-mixin/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" }, "dependencies": { diff --git a/codemods/react-dom-to-react-dom-factories/package.json b/codemods/react-dom-to-react-dom-factories/package.json index c6ab5fa..745cdf8 100644 --- a/codemods/react-dom-to-react-dom-factories/package.json +++ b/codemods/react-dom-to-react-dom-factories/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/react-native-view-prop-types/package.json b/codemods/react-native-view-prop-types/package.json index acde9da..2ae56da 100644 --- a/codemods/react-native-view-prop-types/package.json +++ b/codemods/react-native-view-prop-types/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/react-proptypes-to-prop-types/package.json b/codemods/react-proptypes-to-prop-types/package.json index 0ca57b9..99acb99 100644 --- a/codemods/react-proptypes-to-prop-types/package.json +++ b/codemods/react-proptypes-to-prop-types/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" }, "dependencies": { diff --git a/codemods/react-to-react-dom/package.json b/codemods/react-to-react-dom/package.json index 1512b97..0428892 100644 --- a/codemods/react-to-react-dom/package.json +++ b/codemods/react-to-react-dom/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/remove-context-provider/package.json b/codemods/remove-context-provider/package.json index 0e35954..d44d0d3 100644 --- a/codemods/remove-context-provider/package.json +++ b/codemods/remove-context-provider/package.json @@ -10,6 +10,6 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" } } diff --git a/codemods/remove-forward-ref/package.json b/codemods/remove-forward-ref/package.json index fbbc4e8..ac570de 100644 --- a/codemods/remove-forward-ref/package.json +++ b/codemods/remove-forward-ref/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/remove-legacy-context/package.json b/codemods/remove-legacy-context/package.json index 85f2457..b576d9c 100644 --- a/codemods/remove-legacy-context/package.json +++ b/codemods/remove-legacy-context/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/remove-memoization/package.json b/codemods/remove-memoization/package.json index 67088e7..b317fec 100644 --- a/codemods/remove-memoization/package.json +++ b/codemods/remove-memoization/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/rename-unsafe-lifecycles/package.json b/codemods/rename-unsafe-lifecycles/package.json index 0933836..ada693a 100644 --- a/codemods/rename-unsafe-lifecycles/package.json +++ b/codemods/rename-unsafe-lifecycles/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" } } diff --git a/codemods/replace-act-import/package.json b/codemods/replace-act-import/package.json index 8719363..e15311a 100644 --- a/codemods/replace-act-import/package.json +++ b/codemods/replace-act-import/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" }, "dependencies": { diff --git a/codemods/replace-create-factory/package.json b/codemods/replace-create-factory/package.json index 1a13e24..cb8dcb3 100644 --- a/codemods/replace-create-factory/package.json +++ b/codemods/replace-create-factory/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/replace-default-props/package.json b/codemods/replace-default-props/package.json index 90578a3..b539485 100644 --- a/codemods/replace-default-props/package.json +++ b/codemods/replace-default-props/package.json @@ -10,6 +10,6 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" } } diff --git a/codemods/replace-react-test-renderer-import/package.json b/codemods/replace-react-test-renderer-import/package.json index 2c4e90d..6efb69d 100644 --- a/codemods/replace-react-test-renderer-import/package.json +++ b/codemods/replace-react-test-renderer-import/package.json @@ -10,6 +10,6 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" } } diff --git a/codemods/replace-reactdom-render/package.json b/codemods/replace-reactdom-render/package.json index be7bc42..05c6f5c 100644 --- a/codemods/replace-reactdom-render/package.json +++ b/codemods/replace-reactdom-render/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" }, "dependencies": { diff --git a/codemods/replace-string-ref/package.json b/codemods/replace-string-ref/package.json index 624bceb..3a2894d 100644 --- a/codemods/replace-string-ref/package.json +++ b/codemods/replace-string-ref/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" }, "dependencies": { diff --git a/codemods/replace-use-form-state/package.json b/codemods/replace-use-form-state/package.json index a7b5383..d2f2fa4 100644 --- a/codemods/replace-use-form-state/package.json +++ b/codemods/replace-use-form-state/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" }, "dependencies": { diff --git a/codemods/sort-comp/package.json b/codemods/sort-comp/package.json index d7fc257..c4a5cc5 100644 --- a/codemods/sort-comp/package.json +++ b/codemods/sort-comp/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/unprefix-stable-apis/package.json b/codemods/unprefix-stable-apis/package.json index 183c0fb..ae2612c 100644 --- a/codemods/unprefix-stable-apis/package.json +++ b/codemods/unprefix-stable-apis/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/update-react-imports/package.json b/codemods/update-react-imports/package.json index 5853627..3d906b7 100644 --- a/codemods/update-react-imports/package.json +++ b/codemods/update-react-imports/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" }, "dependencies": { diff --git a/codemods/use-browser/package.json b/codemods/use-browser/package.json index 16cd489..6b4f2b8 100644 --- a/codemods/use-browser/package.json +++ b/codemods/use-browser/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "latest" + "typescript": "~5.9.3" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/use-context-hook/package.json b/codemods/use-context-hook/package.json index 6d45383..0f836ad 100644 --- a/codemods/use-context-hook/package.json +++ b/codemods/use-context-hook/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", + "typescript": "~5.9.3", "@types/node": "latest" }, "dependencies": { diff --git a/package.json b/package.json index f221e22..2c7598e 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,10 @@ }, "dependencies": { "@jssg/utils": "^0.0.10" + }, + "pnpm": { + "overrides": { + "typescript": "~5.9.3" + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0799e09..a786c63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + typescript: ~5.9.3 + importers: .: @@ -35,8 +38,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/error-boundaries: devDependencies: @@ -47,8 +50,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/find-dom-node: dependencies: @@ -63,8 +66,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/fragment-ref-wrappers: dependencies: @@ -79,8 +82,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/manual-bind-to-arrow: devDependencies: @@ -91,8 +94,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/prop-types-typescript: dependencies: @@ -107,8 +110,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/pure-component: dependencies: @@ -123,8 +126,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/pure-render-mixin: dependencies: @@ -139,8 +142,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/react-19-3-migration-recipe: {} @@ -159,8 +162,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/react-native-view-prop-types: dependencies: @@ -175,8 +178,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/react-proptypes-to-prop-types: dependencies: @@ -191,8 +194,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/react-to-react-dom: dependencies: @@ -207,8 +210,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/remove-context-provider: devDependencies: @@ -219,8 +222,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/remove-forward-ref: dependencies: @@ -235,8 +238,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/remove-legacy-context: dependencies: @@ -251,8 +254,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/remove-memoization: dependencies: @@ -267,8 +270,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/rename-unsafe-lifecycles: devDependencies: @@ -279,8 +282,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/replace-act-import: dependencies: @@ -295,8 +298,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/replace-create-factory: dependencies: @@ -311,8 +314,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/replace-default-props: devDependencies: @@ -323,8 +326,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/replace-react-test-renderer-import: devDependencies: @@ -335,8 +338,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/replace-reactdom-render: dependencies: @@ -351,8 +354,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/replace-string-ref: dependencies: @@ -367,8 +370,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/replace-use-form-state: dependencies: @@ -383,8 +386,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/sort-comp: dependencies: @@ -399,8 +402,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/unprefix-stable-apis: dependencies: @@ -415,8 +418,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/update-react-imports: dependencies: @@ -431,8 +434,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/use-browser: dependencies: @@ -447,8 +450,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 codemods/use-context-hook: dependencies: @@ -463,8 +466,8 @@ importers: specifier: latest version: 26.6.6 typescript: - specifier: latest - version: 7.0.2 + specifier: ~5.9.3 + version: 5.9.3 packages: @@ -608,126 +611,6 @@ packages: '@types/node@26.6.6': resolution: {integrity: sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==} - '@typescript/typescript-aix-ppc64@7.0.2': - resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} - engines: {node: '>=16.20.0'} - cpu: [ppc64] - os: [aix] - - '@typescript/typescript-darwin-arm64@7.0.2': - resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [darwin] - - '@typescript/typescript-darwin-x64@7.0.2': - resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [darwin] - - '@typescript/typescript-freebsd-arm64@7.0.2': - resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [freebsd] - - '@typescript/typescript-freebsd-x64@7.0.2': - resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [freebsd] - - '@typescript/typescript-linux-arm64@7.0.2': - resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [linux] - - '@typescript/typescript-linux-arm@7.0.2': - resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} - engines: {node: '>=16.20.0'} - cpu: [arm] - os: [linux] - - '@typescript/typescript-linux-loong64@7.0.2': - resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} - engines: {node: '>=16.20.0'} - cpu: [loong64] - os: [linux] - - '@typescript/typescript-linux-mips64el@7.0.2': - resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} - engines: {node: '>=16.20.0'} - cpu: [mips64el] - os: [linux] - - '@typescript/typescript-linux-ppc64@7.0.2': - resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} - engines: {node: '>=16.20.0'} - cpu: [ppc64] - os: [linux] - - '@typescript/typescript-linux-riscv64@7.0.2': - resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} - engines: {node: '>=16.20.0'} - cpu: [riscv64] - os: [linux] - - '@typescript/typescript-linux-s390x@7.0.2': - resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} - engines: {node: '>=16.20.0'} - cpu: [s390x] - os: [linux] - - '@typescript/typescript-linux-x64@7.0.2': - resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [linux] - - '@typescript/typescript-netbsd-arm64@7.0.2': - resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [netbsd] - - '@typescript/typescript-netbsd-x64@7.0.2': - resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [netbsd] - - '@typescript/typescript-openbsd-arm64@7.0.2': - resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [openbsd] - - '@typescript/typescript-openbsd-x64@7.0.2': - resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [openbsd] - - '@typescript/typescript-sunos-x64@7.0.2': - resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [sunos] - - '@typescript/typescript-win32-arm64@7.0.2': - resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [win32] - - '@typescript/typescript-win32-x64@7.0.2': - resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [win32] - ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1010,9 +893,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - typescript@7.0.2: - resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} - engines: {node: '>=16.20.0'} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} hasBin: true undici-types@8.9.0: @@ -1240,66 +1123,6 @@ snapshots: dependencies: undici-types: 8.9.0 - '@typescript/typescript-aix-ppc64@7.0.2': - optional: true - - '@typescript/typescript-darwin-arm64@7.0.2': - optional: true - - '@typescript/typescript-darwin-x64@7.0.2': - optional: true - - '@typescript/typescript-freebsd-arm64@7.0.2': - optional: true - - '@typescript/typescript-freebsd-x64@7.0.2': - optional: true - - '@typescript/typescript-linux-arm64@7.0.2': - optional: true - - '@typescript/typescript-linux-arm@7.0.2': - optional: true - - '@typescript/typescript-linux-loong64@7.0.2': - optional: true - - '@typescript/typescript-linux-mips64el@7.0.2': - optional: true - - '@typescript/typescript-linux-ppc64@7.0.2': - optional: true - - '@typescript/typescript-linux-riscv64@7.0.2': - optional: true - - '@typescript/typescript-linux-s390x@7.0.2': - optional: true - - '@typescript/typescript-linux-x64@7.0.2': - optional: true - - '@typescript/typescript-netbsd-arm64@7.0.2': - optional: true - - '@typescript/typescript-netbsd-x64@7.0.2': - optional: true - - '@typescript/typescript-openbsd-arm64@7.0.2': - optional: true - - '@typescript/typescript-openbsd-x64@7.0.2': - optional: true - - '@typescript/typescript-sunos-x64@7.0.2': - optional: true - - '@typescript/typescript-win32-arm64@7.0.2': - optional: true - - '@typescript/typescript-win32-x64@7.0.2': - optional: true - ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -1545,28 +1368,7 @@ snapshots: dependencies: is-number: 7.0.0 - typescript@7.0.2: - optionalDependencies: - '@typescript/typescript-aix-ppc64': 7.0.2 - '@typescript/typescript-darwin-arm64': 7.0.2 - '@typescript/typescript-darwin-x64': 7.0.2 - '@typescript/typescript-freebsd-arm64': 7.0.2 - '@typescript/typescript-freebsd-x64': 7.0.2 - '@typescript/typescript-linux-arm': 7.0.2 - '@typescript/typescript-linux-arm64': 7.0.2 - '@typescript/typescript-linux-loong64': 7.0.2 - '@typescript/typescript-linux-mips64el': 7.0.2 - '@typescript/typescript-linux-ppc64': 7.0.2 - '@typescript/typescript-linux-riscv64': 7.0.2 - '@typescript/typescript-linux-s390x': 7.0.2 - '@typescript/typescript-linux-x64': 7.0.2 - '@typescript/typescript-netbsd-arm64': 7.0.2 - '@typescript/typescript-netbsd-x64': 7.0.2 - '@typescript/typescript-openbsd-arm64': 7.0.2 - '@typescript/typescript-openbsd-x64': 7.0.2 - '@typescript/typescript-sunos-x64': 7.0.2 - '@typescript/typescript-win32-arm64': 7.0.2 - '@typescript/typescript-win32-x64': 7.0.2 + typescript@5.9.3: {} undici-types@8.9.0: {} From 9e49c487995f9c01ea13042d587b51c976e0f49c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Oct 2026 22:00:19 +0000 Subject: [PATCH 09/10] Replace dlx wrapper with a real codemod dependency; move to TypeScript 7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pnpm dlx was fine conceptually — the CI failure was a parallel-cache race when many packages ran `pnpm dlx` at once. Prefer declaring `codemod` as a devDependency and invoking it on PATH. Also upgrade typescript to ^7.0.2. TS 7 dropped the classic compiler API from the main export, so prop-types-typescript parity tests import createSourceFile via @typescript/typescript6. Co-authored-by: Alex Bit --- codemods/create-element-to-jsx/package.json | 5 +- .../scripts/differential-tests.mjs | 3 +- .../scripts/error-tests.mjs | 3 +- codemods/error-boundaries/package.json | 7 +- codemods/find-dom-node/package.json | 7 +- codemods/fragment-ref-wrappers/package.json | 5 +- codemods/manual-bind-to-arrow/package.json | 7 +- codemods/prop-types-typescript/package.json | 4 +- .../scripts/parity-tests.mjs | 8 +- codemods/pure-component/package.json | 5 +- .../scripts/differential-tests.mjs | 3 +- .../pure-component/scripts/warning-tests.mjs | 3 +- codemods/pure-render-mixin/package.json | 7 +- .../react-19-3-migration-recipe/package.json | 3 +- .../react-19-migration-recipe/package.json | 3 +- .../package.json | 5 +- .../react-native-view-prop-types/package.json | 5 +- .../package.json | 7 +- codemods/react-to-react-dom/package.json | 5 +- .../scripts/error-tests.mjs | 3 +- codemods/remove-context-provider/package.json | 5 +- codemods/remove-forward-ref/package.json | 5 +- codemods/remove-legacy-context/package.json | 5 +- codemods/remove-memoization/package.json | 5 +- .../rename-unsafe-lifecycles/package.json | 7 +- codemods/replace-act-import/package.json | 7 +- codemods/replace-create-factory/package.json | 5 +- codemods/replace-default-props/package.json | 5 +- .../package.json | 5 +- codemods/replace-reactdom-render/package.json | 7 +- codemods/replace-string-ref/package.json | 7 +- codemods/replace-use-form-state/package.json | 7 +- codemods/sort-comp/package.json | 5 +- codemods/unprefix-stable-apis/package.json | 5 +- codemods/update-react-imports/package.json | 7 +- codemods/use-browser/package.json | 5 +- codemods/use-context-hook/package.json | 7 +- package.json | 5 - pnpm-lock.yaml | 431 +++++++++++++++--- scripts/run-codemod.mjs | 33 -- 40 files changed, 478 insertions(+), 188 deletions(-) delete mode 100644 scripts/run-codemod.mjs diff --git a/codemods/create-element-to-jsx/package.json b/codemods/create-element-to-jsx/package.json index 54da119..b6ec6d8 100644 --- a/codemods/create-element-to-jsx/package.json +++ b/codemods/create-element-to-jsx/package.json @@ -4,13 +4,14 @@ "description": "Convert React.createElement calls to JSX", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/error-tests.mjs ./scripts/differential-tests.mjs", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/error-tests.mjs ./scripts/differential-tests.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/create-element-to-jsx/scripts/differential-tests.mjs b/codemods/create-element-to-jsx/scripts/differential-tests.mjs index 276339f..efb6501 100644 --- a/codemods/create-element-to-jsx/scripts/differential-tests.mjs +++ b/codemods/create-element-to-jsx/scripts/differential-tests.mjs @@ -8,7 +8,6 @@ import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); -const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const workflowPath = path.join(packageDir, "workflow.yaml"); const testsDir = path.join(packageDir, "tests"); @@ -27,7 +26,7 @@ function runJssg(source) { spawnSync("git", ["add", "input.tsx", ".gitignore"], { cwd: dir }); spawnSync("git", ["commit", "-qm", "init"], { cwd: dir }); - const result = spawnSync("node", [codemodRunner, + const result = spawnSync("codemod", [ "workflow", "run", "-w", diff --git a/codemods/create-element-to-jsx/scripts/error-tests.mjs b/codemods/create-element-to-jsx/scripts/error-tests.mjs index 04c4e82..677b559 100644 --- a/codemods/create-element-to-jsx/scripts/error-tests.mjs +++ b/codemods/create-element-to-jsx/scripts/error-tests.mjs @@ -8,7 +8,6 @@ import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); -const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const workflowPath = path.join(packageDir, "workflow.yaml"); function runCodemod(input) { @@ -17,7 +16,7 @@ function runCodemod(input) { writeFileSync(filePath, input, "utf8"); try { - return spawnSync("node", [codemodRunner, + return spawnSync("codemod", [ "workflow", "run", "-w", diff --git a/codemods/error-boundaries/package.json b/codemods/error-boundaries/package.json index 593d638..fd944e6 100644 --- a/codemods/error-boundaries/package.json +++ b/codemods/error-boundaries/package.json @@ -4,12 +4,13 @@ "description": "Rename unstable_handleError to componentDidCatch", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" } } diff --git a/codemods/find-dom-node/package.json b/codemods/find-dom-node/package.json index f2ee19d..ef92424 100644 --- a/codemods/find-dom-node/package.json +++ b/codemods/find-dom-node/package.json @@ -4,13 +4,14 @@ "description": "Replace getDOMNode() with React.findDOMNode()", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/fragment-ref-wrappers/package.json b/codemods/fragment-ref-wrappers/package.json index 686982a..4031fd8 100644 --- a/codemods/fragment-ref-wrappers/package.json +++ b/codemods/fragment-ref-wrappers/package.json @@ -4,13 +4,14 @@ "description": "Replace display:contents wrapper elements that only exist to hold a ref with a Fragment ref", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/manual-bind-to-arrow/package.json b/codemods/manual-bind-to-arrow/package.json index 256513e..c64641e 100644 --- a/codemods/manual-bind-to-arrow/package.json +++ b/codemods/manual-bind-to-arrow/package.json @@ -4,12 +4,13 @@ "description": "Convert this.method = this.method.bind(this) to arrow class property", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" } } diff --git a/codemods/prop-types-typescript/package.json b/codemods/prop-types-typescript/package.json index 4b2269a..97a14d1 100644 --- a/codemods/prop-types-typescript/package.json +++ b/codemods/prop-types-typescript/package.json @@ -10,7 +10,9 @@ "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6", + "@typescript/typescript6": "^6.0.2" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/prop-types-typescript/scripts/parity-tests.mjs b/codemods/prop-types-typescript/scripts/parity-tests.mjs index f02db57..59a5a05 100644 --- a/codemods/prop-types-typescript/scripts/parity-tests.mjs +++ b/codemods/prop-types-typescript/scripts/parity-tests.mjs @@ -5,13 +5,11 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; -import * as typescriptNs from "typescript"; -const ts = typescriptNs.default ?? typescriptNs; +import ts from "@typescript/typescript6"; import { cases } from "./parity-cases.mjs"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); -const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const codemodPath = path.join(packageDir, "scripts", "codemod.ts"); const resultsCache = new Map(); @@ -52,7 +50,7 @@ function runMode(mode) { writeFileSync(path.join(dir, filename), testCase.input, "utf8"); } - const command = [codemodRunner, + const command = [ "jssg", "run", "--language", @@ -67,7 +65,7 @@ function runMode(mode) { command.push("--param", `preserve-prop-types=${mode}`); } - const result = spawnSync("node", command, { + const result = spawnSync("codemod", command, { cwd: packageDir, encoding: "utf8", timeout: 120000, diff --git a/codemods/pure-component/package.json b/codemods/pure-component/package.json index 5a763c0..7fc9f24 100644 --- a/codemods/pure-component/package.json +++ b/codemods/pure-component/package.json @@ -4,13 +4,14 @@ "description": "Convert simple React classes into pure function components", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/warning-tests.mjs ./scripts/differential-tests.mjs", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/warning-tests.mjs ./scripts/differential-tests.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/pure-component/scripts/differential-tests.mjs b/codemods/pure-component/scripts/differential-tests.mjs index fc30ced..bb5ebae 100644 --- a/codemods/pure-component/scripts/differential-tests.mjs +++ b/codemods/pure-component/scripts/differential-tests.mjs @@ -8,7 +8,6 @@ import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); -const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const codemodFile = path.join(packageDir, "scripts/codemod.ts"); const testsDir = path.join(packageDir, "tests"); @@ -37,7 +36,7 @@ function runJssgOutput(input, params = {}) { } try { - const result = spawnSync("node", [codemodRunner, + const result = spawnSync("codemod", [ "jssg", "test", "-l", diff --git a/codemods/pure-component/scripts/warning-tests.mjs b/codemods/pure-component/scripts/warning-tests.mjs index 28fbc6b..86484d7 100644 --- a/codemods/pure-component/scripts/warning-tests.mjs +++ b/codemods/pure-component/scripts/warning-tests.mjs @@ -8,7 +8,6 @@ import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); -const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const codemodFile = path.join(packageDir, "scripts/codemod.ts"); function runFixtureTest(input, params = {}) { @@ -22,7 +21,7 @@ function runFixtureTest(input, params = {}) { } try { - return spawnSync("node", [codemodRunner, + return spawnSync("codemod", [ "jssg", "test", "-l", diff --git a/codemods/pure-render-mixin/package.json b/codemods/pure-render-mixin/package.json index 7516695..40c64fd 100644 --- a/codemods/pure-render-mixin/package.json +++ b/codemods/pure-render-mixin/package.json @@ -4,13 +4,14 @@ "description": "Replace PureRenderMixin with shouldComponentUpdate using React.addons.shallowCompare", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/react-19-3-migration-recipe/package.json b/codemods/react-19-3-migration-recipe/package.json index a556fe3..32a5b35 100644 --- a/codemods/react-19-3-migration-recipe/package.json +++ b/codemods/react-19-3-migration-recipe/package.json @@ -7,5 +7,6 @@ "test": "node --test ./scripts/metadata-tests.mjs", "check-types": "node --check ./scripts/metadata-tests.mjs" }, - "license": "MIT" + "license": "MIT", + "devDependencies": {} } diff --git a/codemods/react-19-migration-recipe/package.json b/codemods/react-19-migration-recipe/package.json index d615eba..ddfa451 100644 --- a/codemods/react-19-migration-recipe/package.json +++ b/codemods/react-19-migration-recipe/package.json @@ -7,5 +7,6 @@ "test": "node --test ./scripts/metadata-tests.mjs", "check-types": "node --check ./scripts/metadata-tests.mjs" }, - "license": "MIT" + "license": "MIT", + "devDependencies": {} } diff --git a/codemods/react-dom-to-react-dom-factories/package.json b/codemods/react-dom-to-react-dom-factories/package.json index 745cdf8..9e9da6a 100644 --- a/codemods/react-dom-to-react-dom-factories/package.json +++ b/codemods/react-dom-to-react-dom-factories/package.json @@ -4,13 +4,14 @@ "description": "Replace React.DOM factories with createElement calls", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts --strictness loose", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/react-native-view-prop-types/package.json b/codemods/react-native-view-prop-types/package.json index 2ae56da..9e232ce 100644 --- a/codemods/react-native-view-prop-types/package.json +++ b/codemods/react-native-view-prop-types/package.json @@ -4,13 +4,14 @@ "description": "Replace View.propTypes with ViewPropTypes", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts --strictness loose", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/react-proptypes-to-prop-types/package.json b/codemods/react-proptypes-to-prop-types/package.json index 99acb99..89b8e61 100644 --- a/codemods/react-proptypes-to-prop-types/package.json +++ b/codemods/react-proptypes-to-prop-types/package.json @@ -4,13 +4,14 @@ "description": "Replace React.PropTypes with prop-types package", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/react-to-react-dom/package.json b/codemods/react-to-react-dom/package.json index 0428892..f78372f 100644 --- a/codemods/react-to-react-dom/package.json +++ b/codemods/react-to-react-dom/package.json @@ -4,13 +4,14 @@ "description": "Move DOM APIs from react to react-dom", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts --strictness loose && node --test ./scripts/error-tests.mjs", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts --strictness loose && node --test ./scripts/error-tests.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/react-to-react-dom/scripts/error-tests.mjs b/codemods/react-to-react-dom/scripts/error-tests.mjs index 5398fef..80d5918 100644 --- a/codemods/react-to-react-dom/scripts/error-tests.mjs +++ b/codemods/react-to-react-dom/scripts/error-tests.mjs @@ -8,7 +8,6 @@ import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); -const codemodRunner = path.resolve(packageDir, "../../scripts/run-codemod.mjs"); const workflowPath = path.join(packageDir, "workflow.yaml"); function runCodemod(input) { @@ -17,7 +16,7 @@ function runCodemod(input) { writeFileSync(filePath, input, "utf8"); try { - return spawnSync("node", [codemodRunner, + return spawnSync("codemod", [ "workflow", "run", "-w", diff --git a/codemods/remove-context-provider/package.json b/codemods/remove-context-provider/package.json index d44d0d3..d2c9150 100644 --- a/codemods/remove-context-provider/package.json +++ b/codemods/remove-context-provider/package.json @@ -4,12 +4,13 @@ "description": "Replace Context.Provider with Context in JSX", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" } } diff --git a/codemods/remove-forward-ref/package.json b/codemods/remove-forward-ref/package.json index ac570de..27a53ff 100644 --- a/codemods/remove-forward-ref/package.json +++ b/codemods/remove-forward-ref/package.json @@ -4,13 +4,14 @@ "description": "Inline forwardRef render functions and move ref into props", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts --strictness loose", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/remove-legacy-context/package.json b/codemods/remove-legacy-context/package.json index b576d9c..aa21e3f 100644 --- a/codemods/remove-legacy-context/package.json +++ b/codemods/remove-legacy-context/package.json @@ -4,13 +4,14 @@ "description": "Remove legacy childContextTypes/getChildContext usage from React class components", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/remove-memoization/package.json b/codemods/remove-memoization/package.json index b317fec..11fdc2d 100644 --- a/codemods/remove-memoization/package.json +++ b/codemods/remove-memoization/package.json @@ -4,13 +4,14 @@ "description": "Remove manual React memoization calls for React Compiler adoption", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/rename-unsafe-lifecycles/package.json b/codemods/rename-unsafe-lifecycles/package.json index ada693a..84560b0 100644 --- a/codemods/rename-unsafe-lifecycles/package.json +++ b/codemods/rename-unsafe-lifecycles/package.json @@ -4,12 +4,13 @@ "description": "Rename deprecated lifecycle methods to UNSAFE_ prefixed versions", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" } } diff --git a/codemods/replace-act-import/package.json b/codemods/replace-act-import/package.json index e15311a..f775888 100644 --- a/codemods/replace-act-import/package.json +++ b/codemods/replace-act-import/package.json @@ -4,13 +4,14 @@ "description": "Migrate act() from react-dom/test-utils to react", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/replace-create-factory/package.json b/codemods/replace-create-factory/package.json index cb8dcb3..ec26502 100644 --- a/codemods/replace-create-factory/package.json +++ b/codemods/replace-create-factory/package.json @@ -4,13 +4,14 @@ "description": "Replace deprecated React.createFactory calls with JSX", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/replace-default-props/package.json b/codemods/replace-default-props/package.json index b539485..f5768d9 100644 --- a/codemods/replace-default-props/package.json +++ b/codemods/replace-default-props/package.json @@ -4,12 +4,13 @@ "description": "Replace React function component defaultProps with default parameters", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" } } diff --git a/codemods/replace-react-test-renderer-import/package.json b/codemods/replace-react-test-renderer-import/package.json index 6efb69d..5ea2d82 100644 --- a/codemods/replace-react-test-renderer-import/package.json +++ b/codemods/replace-react-test-renderer-import/package.json @@ -4,12 +4,13 @@ "description": "Replace react-test-renderer shallow renderer imports with react-shallow-renderer", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" } } diff --git a/codemods/replace-reactdom-render/package.json b/codemods/replace-reactdom-render/package.json index 05c6f5c..9ee922b 100644 --- a/codemods/replace-reactdom-render/package.json +++ b/codemods/replace-reactdom-render/package.json @@ -4,13 +4,14 @@ "description": "Replace ReactDOM.render with createRoot for React 18", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/replace-string-ref/package.json b/codemods/replace-string-ref/package.json index 3a2894d..5fda0ae 100644 --- a/codemods/replace-string-ref/package.json +++ b/codemods/replace-string-ref/package.json @@ -4,13 +4,14 @@ "description": "Replace string refs with callback refs in React class components", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/replace-use-form-state/package.json b/codemods/replace-use-form-state/package.json index d2f2fa4..d831593 100644 --- a/codemods/replace-use-form-state/package.json +++ b/codemods/replace-use-form-state/package.json @@ -4,13 +4,14 @@ "description": "Rename useFormState to useActionState in react-dom imports and usages", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/sort-comp/package.json b/codemods/sort-comp/package.json index c4a5cc5..484080e 100644 --- a/codemods/sort-comp/package.json +++ b/codemods/sort-comp/package.json @@ -4,13 +4,14 @@ "description": "Reorder React component methods to match eslint-plugin-react sort-comp rule", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/unprefix-stable-apis/package.json b/codemods/unprefix-stable-apis/package.json index ae2612c..2c09ee2 100644 --- a/codemods/unprefix-stable-apis/package.json +++ b/codemods/unprefix-stable-apis/package.json @@ -4,13 +4,14 @@ "description": "Rename unstable_/experimental_ React APIs that became stable in React 19.2 and 19.3 to their stable names", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/update-react-imports/package.json b/codemods/update-react-imports/package.json index 3d906b7..00c2e6b 100644 --- a/codemods/update-react-imports/package.json +++ b/codemods/update-react-imports/package.json @@ -4,13 +4,14 @@ "description": "Remove unused React default import and convert to named imports", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/use-browser/package.json b/codemods/use-browser/package.json index 6b4f2b8..c06c0c0 100644 --- a/codemods/use-browser/package.json +++ b/codemods/use-browser/package.json @@ -4,13 +4,14 @@ "description": "Replace the mounted-state pattern for browser-only components with use(browser()) from react-dom", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", "@types/node": "latest", - "typescript": "~5.9.3" + "typescript": "^7.0.2", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/codemods/use-context-hook/package.json b/codemods/use-context-hook/package.json index 0f836ad..384c9f1 100644 --- a/codemods/use-context-hook/package.json +++ b/codemods/use-context-hook/package.json @@ -4,13 +4,14 @@ "description": "Transform React.useContext() and useContext() to use() hook", "type": "module", "scripts": { - "test": "node ../../scripts/run-codemod.mjs jssg test -l tsx ./scripts/codemod.ts", + "test": "codemod jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "~5.9.3", - "@types/node": "latest" + "typescript": "^7.0.2", + "@types/node": "latest", + "codemod": "1.18.6" }, "dependencies": { "@jssg/utils": "^0.0.10" diff --git a/package.json b/package.json index 2c7598e..f221e22 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,5 @@ }, "dependencies": { "@jssg/utils": "^0.0.10" - }, - "pnpm": { - "overrides": { - "typescript": "~5.9.3" - } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a786c63..41bfe52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,9 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - typescript: ~5.9.3 - importers: .: @@ -37,9 +34,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/error-boundaries: devDependencies: @@ -49,9 +49,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/find-dom-node: dependencies: @@ -65,9 +68,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/fragment-ref-wrappers: dependencies: @@ -81,9 +87,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/manual-bind-to-arrow: devDependencies: @@ -93,9 +102,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/prop-types-typescript: dependencies: @@ -109,9 +121,15 @@ importers: '@types/node': specifier: latest version: 26.6.6 + '@typescript/typescript6': + specifier: ^6.0.2 + version: 6.0.2 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/pure-component: dependencies: @@ -125,9 +143,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/pure-render-mixin: dependencies: @@ -141,9 +162,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/react-19-3-migration-recipe: {} @@ -161,9 +185,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/react-native-view-prop-types: dependencies: @@ -177,9 +204,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/react-proptypes-to-prop-types: dependencies: @@ -193,9 +223,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/react-to-react-dom: dependencies: @@ -209,9 +242,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/remove-context-provider: devDependencies: @@ -221,9 +257,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/remove-forward-ref: dependencies: @@ -237,9 +276,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/remove-legacy-context: dependencies: @@ -253,9 +295,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/remove-memoization: dependencies: @@ -269,9 +314,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/rename-unsafe-lifecycles: devDependencies: @@ -281,9 +329,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/replace-act-import: dependencies: @@ -297,9 +348,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/replace-create-factory: dependencies: @@ -313,9 +367,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/replace-default-props: devDependencies: @@ -325,9 +382,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/replace-react-test-renderer-import: devDependencies: @@ -337,9 +397,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/replace-reactdom-render: dependencies: @@ -353,9 +416,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/replace-string-ref: dependencies: @@ -369,9 +435,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/replace-use-form-state: dependencies: @@ -385,9 +454,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/sort-comp: dependencies: @@ -401,9 +473,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/unprefix-stable-apis: dependencies: @@ -417,9 +492,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/update-react-imports: dependencies: @@ -433,9 +511,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/use-browser: dependencies: @@ -449,9 +530,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 codemods/use-context-hook: dependencies: @@ -465,9 +549,12 @@ importers: '@types/node': specifier: latest version: 26.6.6 + codemod: + specifier: 1.18.6 + version: 1.18.6 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 packages: @@ -611,6 +698,130 @@ packages: '@types/node@26.6.6': resolution: {integrity: sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==} + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@typescript/typescript6@6.0.2': + resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==} + hasBin: true + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -893,11 +1104,16 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + undici-types@8.9.0: resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} @@ -1123,6 +1339,70 @@ snapshots: dependencies: undici-types: 8.9.0 + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@typescript/typescript6@6.0.2': + dependencies: + '@typescript/old': typescript@6.0.3 + ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -1368,7 +1648,30 @@ snapshots: dependencies: is-number: 7.0.0 - typescript@5.9.3: {} + typescript@6.0.3: {} + + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 undici-types@8.9.0: {} diff --git a/scripts/run-codemod.mjs b/scripts/run-codemod.mjs deleted file mode 100644 index f6861bd..0000000 --- a/scripts/run-codemod.mjs +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node -/** - * Run the workspace-installed `codemod` CLI while preserving the caller's cwd. - * - * Package tests used to call `pnpm dlx codemod@latest`, which races on the - * shared dlx cache when `pnpm -r test` fans out. Resolving the pinned root - * dependency avoids that flake and keeps relative paths (./scripts/codemod.ts) - * correct for each package. - */ -import { spawnSync } from "node:child_process"; -import { createRequire } from "node:module"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const require = createRequire(path.join(root, "package.json")); -const codemodPkgDir = path.dirname(require.resolve("codemod/package.json")); -const binField = require(path.join(codemodPkgDir, "package.json")).bin; -const binRelative = typeof binField === "string" ? binField : binField.codemod; -const binPath = path.resolve(codemodPkgDir, binRelative); - -const result = spawnSync(binPath, process.argv.slice(2), { - stdio: "inherit", - cwd: process.cwd(), - env: process.env, -}); - -if (result.error) { - console.error(result.error); - process.exit(1); -} - -process.exit(result.status ?? 1); From 396be1c7fbafc306e84977d7a7d1f814cbf712b3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 31 Oct 2026 01:20:56 +0000 Subject: [PATCH 10/10] Address Codex review: fix codemod-side import regressions - fragment-ref-wrappers: don't treat a namespace fallback as a named Fragment binding - prop-types-typescript: collect every default binding; keep imports still referenced (incl. type refs) - react-proptypes-to-prop-types: reuse the existing prop-types alias (import, namespace, require) - use-browser: stop hook cleanup before removeImport falls back to a namespace import - use-context-hook: rename every useContext reference when its import is renamed - create-element-to-jsx, find-dom-node, pure-render-mixin, sort-comp: match against all React aliases - remove-legacy-context: generate a CommonJS React binding for CommonJS sources - unprefix-stable-apis: use one collision-safe rename plan per exported name Co-authored-by: Alex Bit --- .../create-element-to-jsx/scripts/codemod.ts | 29 ++++++--- .../expected.tsx | 5 ++ .../multiple-default-react-aliases/input.tsx | 5 ++ .../metrics.json | 10 +++ codemods/find-dom-node/scripts/codemod.ts | 50 ++++++++------- .../expected.tsx | 9 +++ .../multiple-default-react-aliases/input.tsx | 9 +++ .../metrics.json | 10 +++ .../fragment-ref-wrappers/scripts/codemod.ts | 2 +- .../tests/namespace-react-import/expected.tsx | 14 +++++ .../tests/namespace-react-import/input.tsx | 14 +++++ .../tests/namespace-react-import/metrics.json | 10 +++ .../prop-types-typescript/scripts/codemod.ts | 50 ++++++++------- .../scripts/parity-cases.mjs | 63 +++++++++++++++++++ codemods/pure-render-mixin/scripts/codemod.ts | 40 +++++++----- .../expected.tsx | 12 ++++ .../multiple-default-react-aliases/input.tsx | 12 ++++ .../metrics.json | 10 +++ .../scripts/codemod.ts | 3 +- .../expected.tsx | 5 ++ .../already-migrated-import-alias/input.tsx | 5 ++ .../metrics.json | 10 +++ .../expected.tsx | 5 ++ .../input.tsx | 5 ++ .../metrics.json | 10 +++ .../expected.tsx | 5 ++ .../already-migrated-require-alias/input.tsx | 5 ++ .../metrics.json | 10 +++ .../remove-legacy-context/scripts/codemod.ts | 20 +++--- .../cjs-destructured-component/expected.tsx | 15 +++++ .../cjs-destructured-component/input.tsx | 18 ++++++ .../cjs-destructured-component/metrics.json | 10 +++ codemods/sort-comp/scripts/codemod.ts | 16 ++--- .../multiple-component-aliases/expected.tsx | 14 +++++ .../multiple-component-aliases/input.tsx | 14 +++++ .../multiple-component-aliases/metrics.json | 11 ++++ .../expected.tsx | 14 +++++ .../multiple-default-react-aliases/input.tsx | 14 +++++ .../metrics.json | 11 ++++ .../unprefix-stable-apis/scripts/codemod.ts | 25 +++++--- .../expected.tsx | 16 +++++ .../duplicate-export-name-collision/input.tsx | 16 +++++ .../metrics.json | 10 +++ codemods/use-browser/scripts/codemod.ts | 31 ++++++--- .../expected.tsx | 15 +++++ .../input.tsx | 12 ++++ .../metrics.json | 10 +++ codemods/use-context-hook/scripts/codemod.ts | 8 ++- .../expected.tsx | 7 +++ .../input.tsx | 7 +++ .../metrics.json | 11 ++++ 51 files changed, 633 insertions(+), 109 deletions(-) create mode 100644 codemods/create-element-to-jsx/tests/multiple-default-react-aliases/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/multiple-default-react-aliases/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/multiple-default-react-aliases/metrics.json create mode 100644 codemods/find-dom-node/tests/multiple-default-react-aliases/expected.tsx create mode 100644 codemods/find-dom-node/tests/multiple-default-react-aliases/input.tsx create mode 100644 codemods/find-dom-node/tests/multiple-default-react-aliases/metrics.json create mode 100644 codemods/fragment-ref-wrappers/tests/namespace-react-import/expected.tsx create mode 100644 codemods/fragment-ref-wrappers/tests/namespace-react-import/input.tsx create mode 100644 codemods/fragment-ref-wrappers/tests/namespace-react-import/metrics.json create mode 100644 codemods/pure-render-mixin/tests/multiple-default-react-aliases/expected.tsx create mode 100644 codemods/pure-render-mixin/tests/multiple-default-react-aliases/input.tsx create mode 100644 codemods/pure-render-mixin/tests/multiple-default-react-aliases/metrics.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/metrics.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/metrics.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/metrics.json create mode 100644 codemods/remove-legacy-context/tests/cjs-destructured-component/expected.tsx create mode 100644 codemods/remove-legacy-context/tests/cjs-destructured-component/input.tsx create mode 100644 codemods/remove-legacy-context/tests/cjs-destructured-component/metrics.json create mode 100644 codemods/sort-comp/tests/multiple-component-aliases/expected.tsx create mode 100644 codemods/sort-comp/tests/multiple-component-aliases/input.tsx create mode 100644 codemods/sort-comp/tests/multiple-component-aliases/metrics.json create mode 100644 codemods/sort-comp/tests/multiple-default-react-aliases/expected.tsx create mode 100644 codemods/sort-comp/tests/multiple-default-react-aliases/input.tsx create mode 100644 codemods/sort-comp/tests/multiple-default-react-aliases/metrics.json create mode 100644 codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/expected.tsx create mode 100644 codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/input.tsx create mode 100644 codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/metrics.json create mode 100644 codemods/use-browser/tests/named-hooks-with-namespace-import/expected.tsx create mode 100644 codemods/use-browser/tests/named-hooks-with-namespace-import/input.tsx create mode 100644 codemods/use-browser/tests/named-hooks-with-namespace-import/metrics.json create mode 100644 codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/expected.tsx create mode 100644 codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/input.tsx create mode 100644 codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/metrics.json diff --git a/codemods/create-element-to-jsx/scripts/codemod.ts b/codemods/create-element-to-jsx/scripts/codemod.ts index a4f2c8d..06ba3fb 100644 --- a/codemods/create-element-to-jsx/scripts/codemod.ts +++ b/codemods/create-element-to-jsx/scripts/codemod.ts @@ -1,7 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport } from "@jssg/utils/javascript/imports"; +import { getAllImports } from "@jssg/utils/javascript/imports"; type Conversion = { code: string; @@ -38,14 +38,16 @@ const REACT_MODULES = ["React", "react", "react/addons", "react-native"] as cons const REACT_MODULE_SET = new Set(REACT_MODULES); /** Resolved per-transform from import/require bindings (supports `import * as R`). */ -let currentReactAlias = "React"; +let currentReactAliases = new Set(["React"]); -function resolveReactAlias(rootNode: SgNode): string { +function resolveReactAliases(rootNode: SgNode): Set { + const aliases = new Set(); for (const from of REACT_MODULES) { - const imp = getImport(rootNode, { type: "default", from }); - if (imp) return imp.alias; + for (const imp of getAllImports(rootNode, { type: "default", from })) { + aliases.add(imp.alias); + } } - return "React"; + return aliases.size > 0 ? aliases : new Set(["React"]); } function metricFile(filename: string): string { @@ -180,7 +182,7 @@ function isReactCreateElementCall(node: SgNode): boolean { const property = callee.field("property"); return object?.kind() === "identifier" && - object.text() === currentReactAlias && + currentReactAliases.has(object.text()) && property?.kind() === "property_identifier" && property.text() === "createElement"; } @@ -502,7 +504,7 @@ function propsToDescriptors(node: SgNode | null, source: string): { return { attrs: [], trailingElementComments: [], consumedBoundaryComments: [] }; } - if (isMemberCall(node, currentReactAlias, "__spread") || isMemberCall(node, "Object", "assign")) { + if (isReactSpreadCall(node) || isMemberCall(node, "Object", "assign")) { const callee = node.field("function"); const calleeObject = callee?.field("object") ?? null; const calleeProperty = callee?.field("property") ?? null; @@ -758,12 +760,19 @@ function isMemberCall(node: SgNode, objectName: string, propertyName: strin property.text() === propertyName; } +function isReactSpreadCall(node: SgNode): boolean { + for (const alias of currentReactAliases) { + if (isMemberCall(node, alias, "__spread")) return true; + } + return false; +} + function propsToAttributes(node: SgNode | null): string[] { if (!node || node.kind() === "null") { return []; } - if (isMemberCall(node, currentReactAlias, "__spread") || isMemberCall(node, "Object", "assign")) { + if (isReactSpreadCall(node) || isMemberCall(node, "Object", "assign")) { return callArguments(node).flatMap((arg) => propsToAttributes(arg)); } @@ -1227,7 +1236,7 @@ const transform: Transform = async (root, options) => { return null; } - currentReactAlias = resolveReactAlias(rootNode); + currentReactAliases = resolveReactAliases(rootNode); const canConvertCall = (call: SgNode): boolean => { const cached = convertibility.get(call.id()); diff --git a/codemods/create-element-to-jsx/tests/multiple-default-react-aliases/expected.tsx b/codemods/create-element-to-jsx/tests/multiple-default-react-aliases/expected.tsx new file mode 100644 index 0000000..1c73ae1 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/multiple-default-react-aliases/expected.tsx @@ -0,0 +1,5 @@ +import R from 'react'; +import React from 'react'; + +var a =
; +var b = ; diff --git a/codemods/create-element-to-jsx/tests/multiple-default-react-aliases/input.tsx b/codemods/create-element-to-jsx/tests/multiple-default-react-aliases/input.tsx new file mode 100644 index 0000000..3f81492 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/multiple-default-react-aliases/input.tsx @@ -0,0 +1,5 @@ +import R from 'react'; +import React from 'react'; + +var a = React.createElement('div', null); +var b = R.createElement(Foo, null); diff --git a/codemods/create-element-to-jsx/tests/multiple-default-react-aliases/metrics.json b/codemods/create-element-to-jsx/tests/multiple-default-react-aliases/metrics.json new file mode 100644 index 0000000..3ff93bd --- /dev/null +++ b/codemods/create-element-to-jsx/tests/multiple-default-react-aliases/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/multiple-default-react-aliases/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/find-dom-node/scripts/codemod.ts b/codemods/find-dom-node/scripts/codemod.ts index 50fd11c..a7a6c67 100644 --- a/codemods/find-dom-node/scripts/codemod.ts +++ b/codemods/find-dom-node/scripts/codemod.ts @@ -1,7 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport } from "@jssg/utils/javascript/imports"; +import { getAllImports } from "@jssg/utils/javascript/imports"; const REACT_MODULES = ["React", "react", "react/addons", "react-native"] as const; const REACT_MODULE_SET = new Set(REACT_MODULES); @@ -48,29 +48,33 @@ function hasReact(rootNode: SgNode): boolean { return false; } -function resolveReactAlias(rootNode: SgNode): string { +function resolveReactAliases(rootNode: SgNode): Set { + const aliases = new Set(); for (const from of REACT_MODULES) { - const imp = getImport(rootNode, { type: "default", from }); - if (imp) return imp.alias; + for (const imp of getAllImports(rootNode, { type: "default", from })) { + aliases.add(imp.alias); + } } - return "React"; + return aliases.size > 0 ? aliases : new Set(["React"]); } function explicitRequireDisabled(value: unknown): boolean { return value === false || value === "false"; } -function isReactCreateClassCall( +function reactCreateClassAlias( node: SgNode | null, - reactAlias: string, -): node is SgNode { - if (!node || node.kind() !== "call_expression") return false; + reactAliases: Set, +): string | null { + if (!node || node.kind() !== "call_expression") return null; const callee = node.field("function"); - return callee?.kind() === "member_expression" && - callee.field("object")?.kind() === "identifier" && - callee.field("object")?.text() === reactAlias && + const object = callee?.field("object"); + const isCreateClass = callee?.kind() === "member_expression" && + object?.kind() === "identifier" && + reactAliases.has(object.text()) && callee.field("property")?.kind() === "property_identifier" && callee.field("property")?.text() === "createClass"; + return isCreateClass ? object!.text() : null; } function isModuleExportsMember(node: SgNode | null): boolean { @@ -83,21 +87,19 @@ function isModuleExportsMember(node: SgNode | null): boolean { function createClassContainers( rootNode: SgNode, - reactAlias: string, -): SgNode[] { - const containers: SgNode[] = []; + reactAliases: Set, +): Array<{ container: SgNode; reactAlias: string }> { + const containers: Array<{ container: SgNode; reactAlias: string }> = []; for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - if (isReactCreateClassCall(declarator.field("value"), reactAlias)) containers.push(declarator); + const reactAlias = reactCreateClassAlias(declarator.field("value"), reactAliases); + if (reactAlias) containers.push({ container: declarator, reactAlias }); } for (const assignment of rootNode.findAll({ rule: { kind: "assignment_expression" } })) { - if ( - isModuleExportsMember(assignment.field("left")) && - isReactCreateClassCall(assignment.field("right"), reactAlias) - ) { - containers.push(assignment); - } + if (!isModuleExportsMember(assignment.field("left"))) continue; + const reactAlias = reactCreateClassAlias(assignment.field("right"), reactAliases); + if (reactAlias) containers.push({ container: assignment, reactAlias }); } return containers; @@ -146,12 +148,12 @@ const transform: Transform = async (root, options) => { return null; } - const reactAlias = resolveReactAlias(rootNode); + const reactAliases = resolveReactAliases(rootNode); const metric = useMetricAtom("find-dom-node-replacements"); const edits: Edit[] = []; let replacements = 0; - for (const container of createClassContainers(rootNode, reactAlias)) { + for (const { container, reactAlias } of createClassContainers(rootNode, reactAliases)) { for (const call of container.findAll({ rule: { kind: "call_expression" } })) { if (isGetDOMNodeCall(call, (node) => node?.kind() === "this")) { edits.push(call.replace(`${reactAlias}.findDOMNode(this)`)); diff --git a/codemods/find-dom-node/tests/multiple-default-react-aliases/expected.tsx b/codemods/find-dom-node/tests/multiple-default-react-aliases/expected.tsx new file mode 100644 index 0000000..318e44b --- /dev/null +++ b/codemods/find-dom-node/tests/multiple-default-react-aliases/expected.tsx @@ -0,0 +1,9 @@ +import R from 'react'; +import React from 'react'; + +var Composer = React.createClass({ + componentWillReceiveProps: function() { + React.findDOMNode(this); + return foo(React.findDOMNode(this.refs.input)); + }, +}); diff --git a/codemods/find-dom-node/tests/multiple-default-react-aliases/input.tsx b/codemods/find-dom-node/tests/multiple-default-react-aliases/input.tsx new file mode 100644 index 0000000..66e37fe --- /dev/null +++ b/codemods/find-dom-node/tests/multiple-default-react-aliases/input.tsx @@ -0,0 +1,9 @@ +import R from 'react'; +import React from 'react'; + +var Composer = React.createClass({ + componentWillReceiveProps: function() { + this.getDOMNode(); + return foo(this.refs.input.getDOMNode()); + }, +}); diff --git a/codemods/find-dom-node/tests/multiple-default-react-aliases/metrics.json b/codemods/find-dom-node/tests/multiple-default-react-aliases/metrics.json new file mode 100644 index 0000000..42d4387 --- /dev/null +++ b/codemods/find-dom-node/tests/multiple-default-react-aliases/metrics.json @@ -0,0 +1,10 @@ +{ + "find-dom-node-replacements": [ + { + "cardinality": { + "file": "tests/multiple-default-react-aliases/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/fragment-ref-wrappers/scripts/codemod.ts b/codemods/fragment-ref-wrappers/scripts/codemod.ts index 43804dd..51285aa 100644 --- a/codemods/fragment-ref-wrappers/scripts/codemod.ts +++ b/codemods/fragment-ref-wrappers/scripts/codemod.ts @@ -165,7 +165,7 @@ const transform: Transform = async (root) => { const reactBinding = getImport(rootNode, { type: "default", from: REACT_MODULE }); let fragmentName: string; - if (fragmentImport) { + if (fragmentImport && !fragmentImport.isNamespace) { fragmentName = fragmentImport.alias; } else if (reactBinding) { fragmentName = `${reactBinding.alias}.Fragment`; diff --git a/codemods/fragment-ref-wrappers/tests/namespace-react-import/expected.tsx b/codemods/fragment-ref-wrappers/tests/namespace-react-import/expected.tsx new file mode 100644 index 0000000..788b9a4 --- /dev/null +++ b/codemods/fragment-ref-wrappers/tests/namespace-react-import/expected.tsx @@ -0,0 +1,14 @@ +import * as R from "react"; + +export function Toolbar({ children }) { + const ref = R.useRef(null); + const focusFirst = () => ref.current?.focus(); + return ( + <> + + + {children} + + + ); +} diff --git a/codemods/fragment-ref-wrappers/tests/namespace-react-import/input.tsx b/codemods/fragment-ref-wrappers/tests/namespace-react-import/input.tsx new file mode 100644 index 0000000..add35f3 --- /dev/null +++ b/codemods/fragment-ref-wrappers/tests/namespace-react-import/input.tsx @@ -0,0 +1,14 @@ +import * as R from "react"; + +export function Toolbar({ children }) { + const ref = R.useRef(null); + const focusFirst = () => ref.current?.focus(); + return ( + <> + +
+ {children} +
+ + ); +} diff --git a/codemods/fragment-ref-wrappers/tests/namespace-react-import/metrics.json b/codemods/fragment-ref-wrappers/tests/namespace-react-import/metrics.json new file mode 100644 index 0000000..c412c07 --- /dev/null +++ b/codemods/fragment-ref-wrappers/tests/namespace-react-import/metrics.json @@ -0,0 +1,10 @@ +{ + "fragment-ref-wrapper-replacements": [ + { + "cardinality": { + "file": "tests/namespace-react-import/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/prop-types-typescript/scripts/codemod.ts b/codemods/prop-types-typescript/scripts/codemod.ts index 1e024aa..df28ae5 100644 --- a/codemods/prop-types-typescript/scripts/codemod.ts +++ b/codemods/prop-types-typescript/scripts/codemod.ts @@ -885,8 +885,7 @@ function collectPropTypesImports(rootNode: SgNode): PropTypesImp const imports: PropTypesImport[] = []; const seen = new Set(); - const defaultImp = getImport(rootNode, { type: "default", from: "prop-types" }); - if (defaultImp) { + for (const defaultImp of getAllImports(rootNode, { type: "default", from: "prop-types" })) { const kind = defaultImp.isNamespace ? "namespace" : "default"; const key = `${kind}:${defaultImp.alias}`; if (!seen.has(key)) { @@ -954,37 +953,42 @@ function parseProgram(source: string): SgNode { return parse("tsx", source).root(); } +function isBindingReferenced( + program: SgNode, + binding: { alias: string; node: SgNode }, +): boolean { + const bindingStart = binding.node.range().start.index; + return program + .findAll({ + rule: { + any: [{ kind: "identifier" }, { kind: "type_identifier" }, { kind: "shorthand_property_identifier" }], + regex: `^${escapeRegExp(binding.alias)}$`, + not: { inside: { kind: "import_statement", stopBy: "end" } }, + }, + }) + .some((node) => node.range().start.index !== bindingStart); +} + function cleanupPropTypesImports(source: string, imports: PropTypesImport[]): string { if (imports.length === 0) { return source; } - const hasRemainingUsage = imports.some(({ localName }) => - new RegExp(`\\b${escapeRegExp(localName)}\\s*\\.`, "m").test(source) - ); - if (hasRemainingUsage) { - return source; - } - let nextSource = source; - // removeImport only clears one match per call — loop until gone. + // removeImport only targets the first default/namespace binding, so stop once that one is still referenced. while (true) { const program = parseProgram(nextSource); const defaultImp = getImport(program, { type: "default", from: "prop-types" }); - if (!defaultImp || defaultImp.isNamespace) break; - const removeEdit = removeImport(program, { type: "default", from: "prop-types" }); - if (!removeEdit) break; - nextSource = program.commitEdits([removeEdit]); - } - - while (true) { - const program = parseProgram(nextSource); - const namespaceImp = getImport(program, { type: "default", from: "prop-types" }); - if (!namespaceImp?.isNamespace) break; - const removeEdit = removeImport(program, { type: "namespace", from: "prop-types" }); + if (!defaultImp || isBindingReferenced(program, defaultImp)) break; + const removeEdit = removeImport(program, { + type: defaultImp.isNamespace ? "namespace" : "default", + from: "prop-types", + }); if (!removeEdit) break; - nextSource = program.commitEdits([removeEdit]); + const updated = program.commitEdits([removeEdit]); + if (updated === nextSource) break; + nextSource = updated; } const namedExports = [ @@ -1003,7 +1007,7 @@ function cleanupPropTypesImports(source: string, imports: PropTypesImport[]): st name: exportedName, from: "prop-types", }).filter((imp) => !imp.isNamespace); - if (remaining.length === 0) break; + if (remaining.length === 0 || remaining.some((imp) => isBindingReferenced(program, imp))) break; const removeEdit = removeImport(program, { type: "named", diff --git a/codemods/prop-types-typescript/scripts/parity-cases.mjs b/codemods/prop-types-typescript/scripts/parity-cases.mjs index 82c61e0..07d4fb8 100644 --- a/codemods/prop-types-typescript/scripts/parity-cases.mjs +++ b/codemods/prop-types-typescript/scripts/parity-cases.mjs @@ -1175,6 +1175,69 @@ interface MyComponentProps { export const MyComponent = (props: MyComponentProps) => { return } +`, + }, + { + name: "separate-default-imports-keep-used-alias", + input: `import PropTypes from "prop-types" +import PT from "prop-types" +import React from "react" + +export const MyComponent = (props) => { + return +} + +MyComponent.propTypes = { + bar: PropTypes.string.isRequired, +} + +export const shared = PT.string +`, + expected: `import PT from "prop-types" +import React from "react" + +interface MyComponentProps { + bar: string +} + +export const MyComponent = (props: MyComponentProps) => { + return +} + +export const shared = PT.string +`, + }, + { + name: "multiline-prop-types-import-keep-used-type", + input: `import PropTypes, { + Validator, +} from "prop-types" +import React from "react" + +export const MyComponent = (props) => { + return +} + +MyComponent.propTypes = { + bar: PropTypes.string.isRequired, +} + +export const validate: Validator = () => null +`, + expected: `import { + Validator, +} from "prop-types" +import React from "react" + +interface MyComponentProps { + bar: string +} + +export const MyComponent = (props: MyComponentProps) => { + return +} + +export const validate: Validator = () => null `, }, { diff --git a/codemods/pure-render-mixin/scripts/codemod.ts b/codemods/pure-render-mixin/scripts/codemod.ts index d53480c..b841c6d 100644 --- a/codemods/pure-render-mixin/scripts/codemod.ts +++ b/codemods/pure-render-mixin/scripts/codemod.ts @@ -1,7 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport } from "@jssg/utils/javascript/imports"; +import { getAllImports } from "@jssg/utils/javascript/imports"; const REACT_MODULES = ["React", "react", "react/addons", "react-native"] as const; const REACT_MODULE_SET = new Set(REACT_MODULES); @@ -48,42 +48,48 @@ function hasReact(rootNode: SgNode): boolean { return false; } -function resolveReactAlias(rootNode: SgNode): string { +function resolveReactAliases(rootNode: SgNode): Set { + const aliases = new Set(); for (const from of REACT_MODULES) { - const imp = getImport(rootNode, { type: "default", from }); - if (imp) return imp.alias; + for (const imp of getAllImports(rootNode, { type: "default", from })) { + aliases.add(imp.alias); + } } - return "React"; + return aliases.size > 0 ? aliases : new Set(["React"]); } function explicitRequireDisabled(value: unknown): boolean { return value === false || value === "false"; } -function isReactCreateClassCall( +function reactCreateClassAlias( node: SgNode | null, - reactAlias: string, -): node is SgNode { - if (!node || node.kind() !== "call_expression") return false; + reactAliases: Set, +): string | null { + if (!node || node.kind() !== "call_expression") return null; const callee = node.field("function"); - return callee?.kind() === "member_expression" && - callee.field("object")?.kind() === "identifier" && - callee.field("object")?.text() === reactAlias && + const object = callee?.field("object"); + const isCreateClass = callee?.kind() === "member_expression" && + object?.kind() === "identifier" && + reactAliases.has(object.text()) && callee.field("property")?.kind() === "property_identifier" && callee.field("property")?.text() === "createClass"; + return isCreateClass ? object!.text() : null; } function reactCreateClassDeclarators( rootNode: SgNode, - reactAlias: string, + reactAliases: Set, ): Array<{ declarator: SgNode; call: SgNode; + reactAlias: string; }> { - const result: Array<{ declarator: SgNode; call: SgNode }> = []; + const result: Array<{ declarator: SgNode; call: SgNode; reactAlias: string }> = []; for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { const value = declarator.field("value"); - if (isReactCreateClassCall(value, reactAlias)) result.push({ declarator, call: value }); + const reactAlias = reactCreateClassAlias(value, reactAliases); + if (value && reactAlias) result.push({ declarator, call: value, reactAlias }); } return result; } @@ -201,13 +207,13 @@ const transform: Transform = async (root, options) => { const source = rootNode.text(); const edits: Edit[] = []; - const reactAlias = resolveReactAlias(rootNode); + const reactAliases = resolveReactAliases(rootNode); const mixinName = (options.params?.["mixin-name"] as string) ?? DEFAULT_MIXIN_NAME; const metric = useMetricAtom("pure-render-mixin-replacements"); let transformCount = 0; - for (const { call } of reactCreateClassDeclarators(rootNode, reactAlias)) { + for (const { call, reactAlias } of reactCreateClassDeclarators(rootNode, reactAliases)) { const configObj = getConfigObject(call); if (!configObj) continue; diff --git a/codemods/pure-render-mixin/tests/multiple-default-react-aliases/expected.tsx b/codemods/pure-render-mixin/tests/multiple-default-react-aliases/expected.tsx new file mode 100644 index 0000000..85efee9 --- /dev/null +++ b/codemods/pure-render-mixin/tests/multiple-default-react-aliases/expected.tsx @@ -0,0 +1,12 @@ +import R from 'react/addons'; +import React from 'react/addons'; + +var MyComponent = React.createClass({ + shouldComponentUpdate: function(nextProps, nextState) { + return React.addons.shallowCompare(this, nextProps, nextState); + }, + + render: function() { + return
; + }, +}); diff --git a/codemods/pure-render-mixin/tests/multiple-default-react-aliases/input.tsx b/codemods/pure-render-mixin/tests/multiple-default-react-aliases/input.tsx new file mode 100644 index 0000000..77c793b --- /dev/null +++ b/codemods/pure-render-mixin/tests/multiple-default-react-aliases/input.tsx @@ -0,0 +1,12 @@ +import R from 'react/addons'; +import React from 'react/addons'; + +var PureRenderMixin = React.addons.PureRenderMixin; + +var MyComponent = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
; + }, +}); diff --git a/codemods/pure-render-mixin/tests/multiple-default-react-aliases/metrics.json b/codemods/pure-render-mixin/tests/multiple-default-react-aliases/metrics.json new file mode 100644 index 0000000..89358c7 --- /dev/null +++ b/codemods/pure-render-mixin/tests/multiple-default-react-aliases/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-render-mixin-replacements": [ + { + "cardinality": { + "file": "tests/multiple-default-react-aliases/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/scripts/codemod.ts b/codemods/react-proptypes-to-prop-types/scripts/codemod.ts index 2b56121..235eee2 100644 --- a/codemods/react-proptypes-to-prop-types/scripts/codemod.ts +++ b/codemods/react-proptypes-to-prop-types/scripts/codemod.ts @@ -141,7 +141,8 @@ const transform: Transform = async (root, options) => { const moduleName = (options.params?.["module-name"] as string | undefined) ?? "prop-types"; const reactModules = ["react", "React"] as const; const reactNames = new Set(); - let localPropTypesName = "PropTypes"; + let localPropTypesName = + getImport(rootNode, { type: "default", from: moduleName })?.alias ?? "PropTypes"; let changed = false; const usesEsmImports = rootNode.findAll({ rule: { kind: "import_statement" } }) diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/expected.tsx new file mode 100644 index 0000000..9576ab1 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/expected.tsx @@ -0,0 +1,5 @@ +import PT from 'prop-types'; +import React from 'react'; + +const object = PT.object; +const string = PT.string; diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/input.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/input.tsx new file mode 100644 index 0000000..e03a38e --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/input.tsx @@ -0,0 +1,5 @@ +import PT from 'prop-types'; +import React from 'react'; + +const object = PT.object; +const string = React.PropTypes.string; diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/metrics.json b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/metrics.json new file mode 100644 index 0000000..3c504ef --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import-alias/metrics.json @@ -0,0 +1,10 @@ +{ + "react-proptypes-migrations": [ + { + "cardinality": { + "file": "tests/already-migrated-import-alias/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/expected.tsx new file mode 100644 index 0000000..41b3613 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/expected.tsx @@ -0,0 +1,5 @@ +import * as PT from 'prop-types'; +import React from 'react'; + +const object = PT.object; +const string = PT.string; diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/input.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/input.tsx new file mode 100644 index 0000000..11b029d --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/input.tsx @@ -0,0 +1,5 @@ +import * as PT from 'prop-types'; +import React from 'react'; + +const object = PT.object; +const string = React.PropTypes.string; diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/metrics.json b/codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/metrics.json new file mode 100644 index 0000000..9c5f05c --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-namespace-import-alias/metrics.json @@ -0,0 +1,10 @@ +{ + "react-proptypes-migrations": [ + { + "cardinality": { + "file": "tests/already-migrated-namespace-import-alias/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/expected.tsx new file mode 100644 index 0000000..9ea51d2 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/expected.tsx @@ -0,0 +1,5 @@ +const PT = require('prop-types'); +const React = require('react'); + +const object = PT.object; +const string = PT.string; diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/input.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/input.tsx new file mode 100644 index 0000000..7ba87ca --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/input.tsx @@ -0,0 +1,5 @@ +const PT = require('prop-types'); +const React = require('react'); + +const object = PT.object; +const string = React.PropTypes.string; diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/metrics.json b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/metrics.json new file mode 100644 index 0000000..4c5804c --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require-alias/metrics.json @@ -0,0 +1,10 @@ +{ + "react-proptypes-migrations": [ + { + "cardinality": { + "file": "tests/already-migrated-require-alias/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-legacy-context/scripts/codemod.ts b/codemods/remove-legacy-context/scripts/codemod.ts index 50cc006..0942384 100644 --- a/codemods/remove-legacy-context/scripts/codemod.ts +++ b/codemods/remove-legacy-context/scripts/codemod.ts @@ -24,22 +24,26 @@ type ReactImportInfo = { componentNames: Set; reactObjectNames: Set; preferredReactObjectName: string | null; + moduleType: "esm" | "cjs"; }; function collectReactImportInfo(rootNode: SgNode): ReactImportInfo { const componentNames = new Set(); const reactObjectNames = new Set(); let preferredReactObjectName: string | null = null; + let moduleType: "esm" | "cjs" | null = null; for (const imp of getAllImports(rootNode, { type: "default", from: "react" })) { reactObjectNames.add(imp.alias); preferredReactObjectName ??= imp.alias; + moduleType ??= imp.moduleType; } for (const name of ["Component", "PureComponent"] as const) { for (const imp of getAllImports(rootNode, { type: "named", name, from: "react" })) { if (!imp.isNamespace) { componentNames.add(imp.alias); + moduleType ??= imp.moduleType; } } } @@ -48,6 +52,7 @@ function collectReactImportInfo(rootNode: SgNode): ReactImportIn componentNames, reactObjectNames, preferredReactObjectName, + moduleType: moduleType ?? "esm", }; } @@ -307,14 +312,15 @@ const transform: Transform = async (root) => { } if (needsReactHelperImport && helperReactObjectName) { - const importEdit = addImport(rootNode, { - type: "namespace", - name: helperReactObjectName, - from: "react", - }); + const importEdit = addImport( + rootNode, + reactImportInfo.moduleType === "cjs" + ? { type: "default", name: helperReactObjectName, from: "react", moduleType: "cjs" } + : { type: "namespace", name: helperReactObjectName, from: "react" }, + ); if (importEdit) { - const quoteMatch = source.match(/\bfrom\s+(['"])/); - const quote = quoteMatch?.[1] === "'" ? "'" : '"'; + const quoteMatch = source.match(/\bfrom\s+(['"])|\brequire\(\s*(['"])/); + const quote = (quoteMatch?.[1] ?? quoteMatch?.[2]) === "'" ? "'" : '"'; edits.push({ ...importEdit, insertedText: importEdit.insertedText diff --git a/codemods/remove-legacy-context/tests/cjs-destructured-component/expected.tsx b/codemods/remove-legacy-context/tests/cjs-destructured-component/expected.tsx new file mode 100644 index 0000000..85cf5d7 --- /dev/null +++ b/codemods/remove-legacy-context/tests/cjs-destructured-component/expected.tsx @@ -0,0 +1,15 @@ +const PropTypes = require('prop-types'); +const { Component } = require('react'); +const React = require('react'); + +const Context = React.createContext(); + +class Parent extends Component { + + + render() { + return ; + } +} + +module.exports = Parent; diff --git a/codemods/remove-legacy-context/tests/cjs-destructured-component/input.tsx b/codemods/remove-legacy-context/tests/cjs-destructured-component/input.tsx new file mode 100644 index 0000000..61d442e --- /dev/null +++ b/codemods/remove-legacy-context/tests/cjs-destructured-component/input.tsx @@ -0,0 +1,18 @@ +const PropTypes = require('prop-types'); +const { Component } = require('react'); + +class Parent extends Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + }; + + getChildContext() { + return { foo: 'bar' }; + } + + render() { + return ; + } +} + +module.exports = Parent; diff --git a/codemods/remove-legacy-context/tests/cjs-destructured-component/metrics.json b/codemods/remove-legacy-context/tests/cjs-destructured-component/metrics.json new file mode 100644 index 0000000..7c95f58 --- /dev/null +++ b/codemods/remove-legacy-context/tests/cjs-destructured-component/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-legacy-context-replacements": [ + { + "cardinality": { + "file": "tests/cjs-destructured-component/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/sort-comp/scripts/codemod.ts b/codemods/sort-comp/scripts/codemod.ts index ca894fc..d0b8683 100644 --- a/codemods/sort-comp/scripts/codemod.ts +++ b/codemods/sort-comp/scripts/codemod.ts @@ -1,7 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport } from "@jssg/utils/javascript/imports"; +import { getAllImports } from "@jssg/utils/javascript/imports"; import * as fs from "fs"; import path from "path"; import { pathToFileURL } from "url"; @@ -72,10 +72,11 @@ function reactImportAliases(rootNode: SgNode): ReactImportAliase for (const from of REACT_SOURCES) { for (const exportName of ["Component", "PureComponent"] as const) { - const imp = getImport(rootNode, { type: "named", name: exportName, from }); - if (!imp || imp.isNamespace) continue; - if (exportName === "Component") component.add(imp.alias); - else pureComponent.add(imp.alias); + for (const imp of getAllImports(rootNode, { type: "named", name: exportName, from })) { + if (imp.isNamespace) continue; + if (exportName === "Component") component.add(imp.alias); + else pureComponent.add(imp.alias); + } } } @@ -85,8 +86,9 @@ function reactImportAliases(rootNode: SgNode): ReactImportAliase function resolveReactObjectAliases(rootNode: SgNode): Set { const aliases = new Set(["React"]); for (const from of REACT_SOURCES) { - const imp = getImport(rootNode, { type: "default", from }); - if (imp) aliases.add(imp.alias); + for (const imp of getAllImports(rootNode, { type: "default", from })) { + aliases.add(imp.alias); + } } return aliases; } diff --git a/codemods/sort-comp/tests/multiple-component-aliases/expected.tsx b/codemods/sort-comp/tests/multiple-component-aliases/expected.tsx new file mode 100644 index 0000000..81ba14a --- /dev/null +++ b/codemods/sort-comp/tests/multiple-component-aliases/expected.tsx @@ -0,0 +1,14 @@ +import { Component as Base } from 'react'; +import { Component as Comp } from 'react'; + +class MyComponent extends Comp { + componentDidMount() { + } + + myOwnMethod() { + } + + render() { + return
; + } +} diff --git a/codemods/sort-comp/tests/multiple-component-aliases/input.tsx b/codemods/sort-comp/tests/multiple-component-aliases/input.tsx new file mode 100644 index 0000000..58cb760 --- /dev/null +++ b/codemods/sort-comp/tests/multiple-component-aliases/input.tsx @@ -0,0 +1,14 @@ +import { Component as Base } from 'react'; +import { Component as Comp } from 'react'; + +class MyComponent extends Comp { + render() { + return
; + } + + componentDidMount() { + } + + myOwnMethod() { + } +} diff --git a/codemods/sort-comp/tests/multiple-component-aliases/metrics.json b/codemods/sort-comp/tests/multiple-component-aliases/metrics.json new file mode 100644 index 0000000..a50e715 --- /dev/null +++ b/codemods/sort-comp/tests/multiple-component-aliases/metrics.json @@ -0,0 +1,11 @@ +{ + "sort-comp-reorders": [ + { + "cardinality": { + "file": "tests/multiple-component-aliases/input.tsx", + "kind": "class" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/sort-comp/tests/multiple-default-react-aliases/expected.tsx b/codemods/sort-comp/tests/multiple-default-react-aliases/expected.tsx new file mode 100644 index 0000000..74e0120 --- /dev/null +++ b/codemods/sort-comp/tests/multiple-default-react-aliases/expected.tsx @@ -0,0 +1,14 @@ +import R from 'react'; +import Re from 'react'; + +class MyComponent extends Re.Component { + componentDidMount() { + } + + myOwnMethod() { + } + + render() { + return
; + } +} diff --git a/codemods/sort-comp/tests/multiple-default-react-aliases/input.tsx b/codemods/sort-comp/tests/multiple-default-react-aliases/input.tsx new file mode 100644 index 0000000..9bd353f --- /dev/null +++ b/codemods/sort-comp/tests/multiple-default-react-aliases/input.tsx @@ -0,0 +1,14 @@ +import R from 'react'; +import Re from 'react'; + +class MyComponent extends Re.Component { + render() { + return
; + } + + componentDidMount() { + } + + myOwnMethod() { + } +} diff --git a/codemods/sort-comp/tests/multiple-default-react-aliases/metrics.json b/codemods/sort-comp/tests/multiple-default-react-aliases/metrics.json new file mode 100644 index 0000000..0607b41 --- /dev/null +++ b/codemods/sort-comp/tests/multiple-default-react-aliases/metrics.json @@ -0,0 +1,11 @@ +{ + "sort-comp-reorders": [ + { + "cardinality": { + "file": "tests/multiple-default-react-aliases/input.tsx", + "kind": "class" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/unprefix-stable-apis/scripts/codemod.ts b/codemods/unprefix-stable-apis/scripts/codemod.ts index b7c6cb9..488568e 100644 --- a/codemods/unprefix-stable-apis/scripts/codemod.ts +++ b/codemods/unprefix-stable-apis/scripts/codemod.ts @@ -76,15 +76,17 @@ function isNameTaken(rootNode: SgNode, name: string, ignore: SgN }); } -type NamedRename = { name: string; to: string; alias?: string }; +type NamedRename = { name: string; to: string }; function collectNamedRenames(rootNode: SgNode): { specifiers: NamedRename[]; + aliasEdits: Edit[]; localRenames: Map; renames: number; handledStarts: Set; } { const specifiers: NamedRename[] = []; + const aliasEdits: Edit[] = []; const localRenames = new Map(); const handledStarts = new Set(); let renames = 0; @@ -107,20 +109,23 @@ function collectNamedRenames(rootNode: SgNode): { const parent = binding.node.parent(); if (parent) handledStarts.add(parent.range().start.index); + // updateImport keys rename plans by exported name, so every plan for a name must be identical. + // A colliding local keeps its name through an explicit alias in the source, which updateImport preserves. + specifiers.push({ name: oldName, to: newName }); const hasExplicitAlias = binding.alias !== oldName; - if (hasExplicitAlias) { - specifiers.push({ name: oldName, to: newName }); - } else if (isNameTaken(rootNode, newName, binding.node)) { - specifiers.push({ name: oldName, to: newName, alias: oldName }); - } else { - specifiers.push({ name: oldName, to: newName }); - localRenames.set(oldName, newName); + if (!hasExplicitAlias) { + if (isNameTaken(rootNode, newName, binding.node)) { + const separator = isInsideImport(binding.node) ? " as " : ": "; + aliasEdits.push(binding.node.replace(`${oldName}${separator}${oldName}`)); + } else { + localRenames.set(oldName, newName); + } } renames += 1; } } - return { specifiers, localRenames, renames, handledStarts }; + return { specifiers, aliasEdits, localRenames, renames, handledStarts }; } function applyNamedImportRenames(source: string, specifiers: NamedRename[]): string { @@ -151,10 +156,12 @@ const transform: Transform = async (root) => { const { specifiers, + aliasEdits, localRenames, renames: importRenames, handledStarts, } = collectNamedRenames(rootNode); + edits.push(...aliasEdits); let renames = importRenames; // Member access through the module object: React.unstable_ViewTransition, . diff --git a/codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/expected.tsx b/codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/expected.tsx new file mode 100644 index 0000000..0483d40 --- /dev/null +++ b/codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/expected.tsx @@ -0,0 +1,16 @@ +import { Activity as unstable_Activity, Activity as Foo } from "react"; + +function Activity() { + return null; +} + +export function Panel({ children }) { + return ( + + + + {children} + + + ); +} diff --git a/codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/input.tsx b/codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/input.tsx new file mode 100644 index 0000000..acc94bf --- /dev/null +++ b/codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/input.tsx @@ -0,0 +1,16 @@ +import { unstable_Activity, unstable_Activity as Foo } from "react"; + +function Activity() { + return null; +} + +export function Panel({ children }) { + return ( + + + + {children} + + + ); +} diff --git a/codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/metrics.json b/codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/metrics.json new file mode 100644 index 0000000..56f44c4 --- /dev/null +++ b/codemods/unprefix-stable-apis/tests/duplicate-export-name-collision/metrics.json @@ -0,0 +1,10 @@ +{ + "unprefix-stable-apis-renames": [ + { + "cardinality": { + "file": "tests/duplicate-export-name-collision/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/use-browser/scripts/codemod.ts b/codemods/use-browser/scripts/codemod.ts index 50dbb7a..7b538dc 100644 --- a/codemods/use-browser/scripts/codemod.ts +++ b/codemods/use-browser/scripts/codemod.ts @@ -521,10 +521,17 @@ function syncImports( } // removeImport drops one remaining named specifier per call when siblings stay. - while (dropFromReact.length > 0) { + while (true) { + // getImport falls back to a namespace binding for missing named specifiers, + // and removeImport throws when only that fallback remains. + const remaining = dropFromReact.filter((name) => { + const imp = getImport(program, { type: "named", name, from: REACT_MODULE }); + return imp !== null && !imp.isNamespace; + }); + if (remaining.length === 0) break; const removeEdit = removeImport(program, { type: "named", - specifiers: dropFromReact, + specifiers: remaining, from: REACT_MODULE, }); if (!removeEdit) break; @@ -564,12 +571,20 @@ const transform: Transform = async (root) => { } else { const existingUse = bindings.named.get("use"); const existingSuspense = bindings.named.get("Suspense"); - if (!existingUse && isNameUsed(rootNode, "use")) return null; - if (!existingSuspense && isNameUsed(rootNode, "Suspense")) return null; - useName = existingUse ?? "use"; - suspenseName = existingSuspense ?? "Suspense"; - if (!existingSuspense) addToReact.push("Suspense"); - if (!existingUse) addToReact.push("use"); + // addImport skips names that getImport resolves to a namespace fallback, + // so reach missing exports through that namespace instead of importing them. + const fallbackNamespace = (name: string) => { + const imp = getImport(rootNode, { type: "named", name, from: REACT_MODULE }); + return imp?.isNamespace ? imp.alias : null; + }; + const useNamespace = existingUse ? null : fallbackNamespace("use"); + const suspenseNamespace = existingSuspense ? null : fallbackNamespace("Suspense"); + if (!existingUse && !useNamespace && isNameUsed(rootNode, "use")) return null; + if (!existingSuspense && !suspenseNamespace && isNameUsed(rootNode, "Suspense")) return null; + useName = existingUse ?? (useNamespace ? `${useNamespace}.use` : "use"); + suspenseName = existingSuspense ?? (suspenseNamespace ? `${suspenseNamespace}.Suspense` : "Suspense"); + if (!existingSuspense && !suspenseNamespace) addToReact.push("Suspense"); + if (!existingUse && !useNamespace) addToReact.push("use"); } const existingBrowser = bindings.domNamed.get("browser"); diff --git a/codemods/use-browser/tests/named-hooks-with-namespace-import/expected.tsx b/codemods/use-browser/tests/named-hooks-with-namespace-import/expected.tsx new file mode 100644 index 0000000..0357a67 --- /dev/null +++ b/codemods/use-browser/tests/named-hooks-with-namespace-import/expected.tsx @@ -0,0 +1,15 @@ +import * as React from "react"; +import { createPortal, browser } from "react-dom"; + +export function Portal(props: { children: React.ReactNode }) { + return ( + + + + ); +} + +function PortalBrowserOnly({ children }: { children: React.ReactNode }) { + React.use(browser()); + return createPortal(children, document.body); +} diff --git a/codemods/use-browser/tests/named-hooks-with-namespace-import/input.tsx b/codemods/use-browser/tests/named-hooks-with-namespace-import/input.tsx new file mode 100644 index 0000000..3a6b0f1 --- /dev/null +++ b/codemods/use-browser/tests/named-hooks-with-namespace-import/input.tsx @@ -0,0 +1,12 @@ +import { useEffect, useState } from "react"; +import * as React from "react"; +import { createPortal } from "react-dom"; + +export function Portal({ children }: { children: React.ReactNode }) { + const [ready, setReady] = useState(false); + useEffect(() => { + setReady(true); + }, []); + if (!ready) return null; + return createPortal(children, document.body); +} diff --git a/codemods/use-browser/tests/named-hooks-with-namespace-import/metrics.json b/codemods/use-browser/tests/named-hooks-with-namespace-import/metrics.json new file mode 100644 index 0000000..b8804eb --- /dev/null +++ b/codemods/use-browser/tests/named-hooks-with-namespace-import/metrics.json @@ -0,0 +1,10 @@ +{ + "use-browser-transformations": [ + { + "cardinality": { + "file": "tests/named-hooks-with-namespace-import/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/use-context-hook/scripts/codemod.ts b/codemods/use-context-hook/scripts/codemod.ts index d60620b..f2143ef 100644 --- a/codemods/use-context-hook/scripts/codemod.ts +++ b/codemods/use-context-hook/scripts/codemod.ts @@ -129,7 +129,12 @@ const transform: Transform = async (root) => { } } - if (activeLocalNames.has("useContext")) { + const needsImportRename = activeLocalNames.size > 0; + const hasUnaliasedImport = namedUseContextImports.some((entry) => entry.alias === "useContext"); + + // updateImport renames every useContext specifier, so the unaliased binding's + // references must follow whenever any alias triggers the import rename. + if (needsImportRename && hasUnaliasedImport) { const renameTargets = rootNode.findAll({ rule: { any: [ @@ -166,7 +171,6 @@ const transform: Transform = async (root) => { }, aliasedCallCount); } - const needsImportRename = activeLocalNames.size > 0; if (edits.length === 0 && !needsImportRename) { return null; } diff --git a/codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/expected.tsx b/codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/expected.tsx new file mode 100644 index 0000000..30ef7a7 --- /dev/null +++ b/codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/expected.tsx @@ -0,0 +1,7 @@ +import { use, use as read } from "react"; + +function Component() { + const theme = read(ThemeContext); + const fn = use; + return
{theme}
; +} diff --git a/codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/input.tsx b/codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/input.tsx new file mode 100644 index 0000000..68fbab3 --- /dev/null +++ b/codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/input.tsx @@ -0,0 +1,7 @@ +import { useContext, useContext as read } from "react"; + +function Component() { + const theme = read(ThemeContext); + const fn = useContext; + return
{theme}
; +} diff --git a/codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/metrics.json b/codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/metrics.json new file mode 100644 index 0000000..d3cd43c --- /dev/null +++ b/codemods/use-context-hook/tests/mixed-called-and-referenced-aliases/metrics.json @@ -0,0 +1,11 @@ +{ + "use-context-transformations": [ + { + "cardinality": { + "file": "tests/mixed-called-and-referenced-aliases/input.tsx", + "pattern": "useContext (aliased)" + }, + "count": 1 + } + ] +} \ No newline at end of file