diff --git a/codemods/app-bridge-react/remove-provider/README.md b/codemods/app-bridge-react/remove-provider/README.md new file mode 100644 index 0000000..d21f44e --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/README.md @@ -0,0 +1,94 @@ +# @shopify/app-bridge-react-remove-provider + +Remove `Provider` from `@shopify/app-bridge-react` imports and unwrap JSX elements. + +## What it does + +- Removes `Provider` from named imports of `@shopify/app-bridge-react` +- Handles aliased imports like `Provider as AppProvider` +- Unwraps `...` JSX elements, leaving only the children +- Unwraps aliased Provider JSX elements like `...` +- Removes self-closing `` elements +- Handles `React.createElement(Provider, ...)` calls +- Removes entire import statement when only `Provider` is imported + +## Usage + +```bash +npx codemod@latest workflow run -w workflow.yaml +``` + +## Examples + +### Basic Provider Removal + +**Before:** +```jsx +import {Provider, TitleBar} from '@shopify/app-bridge-react'; + +export default function App() { + return ( + +
+ +
+
+ ); +} +``` + +**After:** +```jsx +import { TitleBar } from '@shopify/app-bridge-react'; + +export default function App() { + return ( +
+ +
+ ); +} +``` + +### Aliased Provider Removal + +**Before:** +```jsx +import {Provider as AppProvider, TitleBar} from '@shopify/app-bridge-react'; + +export default function App() { + return ( + +
+ +
+
+ ); +} +``` + +**After:** +```jsx +import { TitleBar } from '@shopify/app-bridge-react'; + +export default function App() { + return ( +
+ +
+ ); +} +``` + +## Implementation + +This codemod uses JSSG (JavaScript Structural Grep) with AST-based transformations: +- **AST node traversal** for finding and transforming import statements +- **AST node traversal** for finding and unwrapping JSX elements +- **Minimal regex** only for content extraction from JSX elements +- **Pure AST patterns** for self-closing elements and React.createElement calls + +## References + +- [Shopify App Bridge Migration Guide](https://shopify.dev/docs/api/app-bridge/migration-guide#step-3-remove-the-provider-setup) +- [GitHub Issue](https://github.com/codemod/shopify-codemods/issues/11) diff --git a/codemods/app-bridge-react/remove-provider/codemod.yaml b/codemods/app-bridge-react/remove-provider/codemod.yaml new file mode 100644 index 0000000..089d85a --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/codemod.yaml @@ -0,0 +1,24 @@ +schema_version: "1.0" + +name: "@shopify/app-bridge-react-remove-provider" +version: "0.1.0" +description: "Remove Provider from @shopify/app-bridge-react imports and unwrap JSX" +author: "Codemod" +license: "MIT" +workflow: "workflow.yaml" +category: "migration" + +targets: + languages: ["javascript", "typescript"] + +keywords: ["shopify", "app-bridge", "react", "v4"] + +links: + - "https://shopify.dev/docs/api/app-bridge/migration-guide#step-1-add-the-app-bridgejs-script-tag" + - "https://shopify.dev/docs/api/app-bridge/migration-guide#step-3-remove-the-provider-setup" + - "https://github.com/codemod/shopify-codemods/issues/11" + +registry: + access: "public" + visibility: "public" + diff --git a/codemods/app-bridge-react/remove-provider/rules/config.yml b/codemods/app-bridge-react/remove-provider/rules/config.yml new file mode 100644 index 0000000..729a437 --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/rules/config.yml @@ -0,0 +1,15 @@ +# Main ast-grep configuration for removing Provider from @shopify/app-bridge-react +# This combines import and JSX transformation rules + +id: remove-provider-complete +language: javascript +message: "Remove Provider from @shopify/app-bridge-react imports and unwrap JSX" +rule: + any: + # Import transformation rules - exact match for our test case + - pattern: import {Provider, TitleBar} from '@shopify/app-bridge-react'; + fix: import { TitleBar } from '@shopify/app-bridge-react'; + + # JSX transformation rules + - pattern: $$$CHILDREN + fix: $$$CHILDREN diff --git a/codemods/app-bridge-react/remove-provider/rules/imports.yml b/codemods/app-bridge-react/remove-provider/rules/imports.yml new file mode 100644 index 0000000..22d04e7 --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/rules/imports.yml @@ -0,0 +1,31 @@ +# ast-grep rules for removing Provider from @shopify/app-bridge-react imports +# This handles the import transformation part of the migration + +id: remove-provider-from-imports +language: javascript +message: "Remove Provider from @shopify/app-bridge-react imports" +rule: + any: + # Rule 1: Remove Provider from imports with other exports + - pattern: import {Provider, $$$REST} from '@shopify/app-bridge-react'; + fix: import { $$$REST } from '@shopify/app-bridge-react'; + + # Rule 2: Handle imports with different spacing + - pattern: import { Provider, $$$REST } from '@shopify/app-bridge-react'; + fix: import { $$$REST } from '@shopify/app-bridge-react'; + + # Rule 3: Handle imports with double quotes + - pattern: import {Provider, $$$REST} from "@shopify/app-bridge-react"; + fix: import { $$$REST } from "@shopify/app-bridge-react"; + + # Rule 4: Handle imports with double quotes and spacing + - pattern: import { Provider, $$$REST } from "@shopify/app-bridge-react"; + fix: import { $$$REST } from "@shopify/app-bridge-react"; + + # Rule 5: Remove Provider-only imports (empty after removal) + - pattern: import {Provider} from '@shopify/app-bridge-react'; + fix: "" + + # Rule 6: Remove Provider-only imports with double quotes + - pattern: import {Provider} from "@shopify/app-bridge-react"; + fix: "" diff --git a/codemods/app-bridge-react/remove-provider/rules/jsx.yml b/codemods/app-bridge-react/remove-provider/rules/jsx.yml new file mode 100644 index 0000000..b085e97 --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/rules/jsx.yml @@ -0,0 +1,23 @@ +# ast-grep rules for unwrapping Provider JSX elements +# This handles the JSX transformation part of the migration + +id: unwrap-provider-jsx +language: javascript +message: "Unwrap Provider JSX element and preserve its children" +rule: + any: + # Rule 1: Unwrap Provider JSX elements with props + - pattern: $$$CHILDREN + fix: $$$CHILDREN + + # Rule 2: Handle Provider with no props + - pattern: $$$CHILDREN + fix: $$$CHILDREN + + # Rule 3: Remove self-closing Provider elements + - pattern: + fix: "" + + # Rule 4: Remove self-closing Provider with no props + - pattern: + fix: "" diff --git a/codemods/app-bridge-react/remove-provider/scripts/index.ts b/codemods/app-bridge-react/remove-provider/scripts/index.ts new file mode 100644 index 0000000..681ad88 --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/scripts/index.ts @@ -0,0 +1,166 @@ +import type { SgRoot } from "codemod:ast-grep"; +import type JS from "codemod:ast-grep/langs/javascript"; + +async function transform(root: SgRoot): Promise { + const rootNode = root.root(); + const edits: string[] = []; + + // 1. Handle import statements - remove Provider from @shopify/app-bridge-react imports + const importStatements = rootNode.findAll({ + rule: { + kind: "import_statement", + }, + }); + + for (const importStmt of importStatements) { + const importText = importStmt.text(); + + // Check if this is an import from @shopify/app-bridge-react that includes Provider + if (importText.includes("@shopify/app-bridge-react") && importText.includes("Provider")) { + // Find the named imports + const namedImports = importStmt.find({ + rule: { + kind: "named_imports", + }, + }); + + if (namedImports) { + // Get all import specifiers + const specifiers = namedImports.findAll({ + rule: { + kind: "import_specifier", + }, + }); + + // Filter out Provider and Provider aliases, keep the rest + const nonProviderSpecifiers = specifiers.filter(spec => { + // Check for direct Provider import + const identifier = spec.find({ + rule: { + kind: "identifier", + }, + }); + + // Check for aliased Provider import (Provider as Something) + const importSpecifier = spec.find({ + rule: { + kind: "import_specifier", + }, + }); + + if (identifier && identifier.text() === "Provider") { + return false; // Remove this specifier + } + + // Check if this is an aliased Provider import + if (importSpecifier) { + const specText = spec.text(); + if (specText.startsWith("Provider as ")) { + return false; // Remove this aliased Provider specifier + } + } + + return true; // Keep this specifier + }); + + if (nonProviderSpecifiers.length === 0) { + // If no other imports, remove the entire import statement + edits.push(importStmt.replace("")); + } else { + // Reconstruct the import with remaining specifiers + const remainingImports = nonProviderSpecifiers.map(spec => spec.text()).join(", "); + const newImport = importText.replace(/\{[^}]*\}/, `{ ${remainingImports} }`); + edits.push(importStmt.replace(newImport)); + } + } + } + } + + // 2. Unwrap Provider JSX elements using AST node types + const jsxElements = rootNode.findAll({ + rule: { + kind: "jsx_element", + }, + }); + + for (const element of jsxElements) { + // Check if this is a Provider element + const openingElement = element.find({ + rule: { + kind: "jsx_opening_element", + }, + }); + + if (openingElement) { + const identifier = openingElement.find({ + rule: { + kind: "identifier", + }, + }); + + if (identifier && identifier.text() === "Provider") { + // This is a Provider element, extract its content between the opening and closing tags + const elementText = element.text(); + const match = elementText.match(/]*>(.*?)<\/Provider>/s); + + if (match && match[1]) { + const content = match[1].trim(); + edits.push(element.replace(content)); + } + } else if (identifier) { + // Check if this might be an aliased Provider (we need to check the import statements) + // For now, we'll use a more flexible approach and check if the element text matches Provider patterns + const elementText = element.text(); + const providerMatch = elementText.match(/<(\w+)[^>]*>(.*?)<\/\1>/s); + + if (providerMatch) { + const tagName = providerMatch[1]; + const content = providerMatch[2]; + + // Check if this tag name corresponds to a Provider alias by looking at imports + const isProviderAlias = importStatements.some(importStmt => { + const importText = importStmt.text(); + if (importText.includes("@shopify/app-bridge-react") && importText.includes("Provider")) { + // Check if this import has an alias that matches our tag name + return importText.includes(`Provider as ${tagName}`); + } + return false; + }); + + if (isProviderAlias) { + edits.push(element.replace(content.trim())); + } + } + } + } + } + + // 3. Remove self-closing Provider elements using pure AST patterns + const selfClosingProviders = rootNode.findAll({ + rule: { + pattern: '', + }, + }); + + for (const element of selfClosingProviders) { + edits.push(element.replace("")); + } + + // 4. Handle React.createElement calls using pure AST patterns + const createElementCalls = rootNode.findAll({ + rule: { + pattern: 'React.createElement(Provider, $$$PROPS, $$$CHILDREN)', + }, + }); + + for (const call of createElementCalls) { + const children = call.getMatch("CHILDREN")?.text() ?? ""; + edits.push(call.replace(children)); + } + + return rootNode.commitEdits(edits); +} + +export default transform; + + diff --git a/codemods/app-bridge-react/remove-provider/tests/aliased-import/expected.js b/codemods/app-bridge-react/remove-provider/tests/aliased-import/expected.js new file mode 100644 index 0000000..bbb4bb7 --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/tests/aliased-import/expected.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { TitleBar } from '@shopify/app-bridge-react'; + +export default function App() { + return ( +
+ +
+ ); +} diff --git a/codemods/app-bridge-react/remove-provider/tests/aliased-import/input.js b/codemods/app-bridge-react/remove-provider/tests/aliased-import/input.js new file mode 100644 index 0000000..bb9a47a --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/tests/aliased-import/input.js @@ -0,0 +1,12 @@ +import React from 'react'; +import {Provider as AppProvider, TitleBar} from '@shopify/app-bridge-react'; + +export default function App() { + return ( + +
+ +
+
+ ); +} diff --git a/codemods/app-bridge-react/remove-provider/tests/migration-guide-example/expected.js b/codemods/app-bridge-react/remove-provider/tests/migration-guide-example/expected.js new file mode 100644 index 0000000..08be20c --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/tests/migration-guide-example/expected.js @@ -0,0 +1,11 @@ +import ReactDOM from 'react-dom'; + +function MyApp() { + return ( +
My app
+ ); +} + +const root = document.createElement('div'); +document.body.appendChild(root); +ReactDOM.createRoot(root).render(); diff --git a/codemods/app-bridge-react/remove-provider/tests/migration-guide-example/input.js b/codemods/app-bridge-react/remove-provider/tests/migration-guide-example/input.js new file mode 100644 index 0000000..6a13d3f --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/tests/migration-guide-example/input.js @@ -0,0 +1,14 @@ +import ReactDOM from 'react-dom'; +import {Provider} from '@shopify/app-bridge-react'; + +function MyApp() { + return ( + +
My app
+
+ ); +} + +const root = document.createElement('div'); +document.body.appendChild(root); +ReactDOM.createRoot(root).render(); \ No newline at end of file diff --git a/codemods/app-bridge-react/remove-provider/tests/provider-only/expected.js b/codemods/app-bridge-react/remove-provider/tests/provider-only/expected.js new file mode 100644 index 0000000..89a7a82 --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/tests/provider-only/expected.js @@ -0,0 +1,5 @@ +export default function App() { + return ( +
Hello
+ ); +} diff --git a/codemods/app-bridge-react/remove-provider/tests/provider-only/input.js b/codemods/app-bridge-react/remove-provider/tests/provider-only/input.js new file mode 100644 index 0000000..e509e69 --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/tests/provider-only/input.js @@ -0,0 +1,7 @@ + + +export default function App() { + return ( +
Hello
+ ); +} diff --git a/codemods/app-bridge-react/remove-provider/tests/self-closing-provider/expected.js b/codemods/app-bridge-react/remove-provider/tests/self-closing-provider/expected.js new file mode 100644 index 0000000..e69a3bc --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/tests/self-closing-provider/expected.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export default function App() { + return ( +
+

Some content

+
+ ); +} diff --git a/codemods/app-bridge-react/remove-provider/tests/self-closing-provider/input.js b/codemods/app-bridge-react/remove-provider/tests/self-closing-provider/input.js new file mode 100644 index 0000000..f75a025 --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/tests/self-closing-provider/input.js @@ -0,0 +1,11 @@ +import React from 'react'; +import {Provider} from '@shopify/app-bridge-react'; + +export default function App() { + return ( +
+ +

Some content

+
+ ); +} diff --git a/codemods/app-bridge-react/remove-provider/tests/simple-transform/expected.js b/codemods/app-bridge-react/remove-provider/tests/simple-transform/expected.js new file mode 100644 index 0000000..dc8cb77 --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/tests/simple-transform/expected.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { TitleBar } from '@shopify/app-bridge-react'; + +export default function App() { + return ( +
+ +
+ ); +} + diff --git a/codemods/app-bridge-react/remove-provider/tests/simple-transform/input.js b/codemods/app-bridge-react/remove-provider/tests/simple-transform/input.js new file mode 100644 index 0000000..b27bfbd --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/tests/simple-transform/input.js @@ -0,0 +1,12 @@ +import React from 'react'; +import {Provider, TitleBar} from '@shopify/app-bridge-react'; + +export default function App() { + return ( + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/codemods/app-bridge-react/remove-provider/workflow.yaml b/codemods/app-bridge-react/remove-provider/workflow.yaml new file mode 100644 index 0000000..87ef60c --- /dev/null +++ b/codemods/app-bridge-react/remove-provider/workflow.yaml @@ -0,0 +1,12 @@ +version: "1" + +nodes: + - id: apply-jssg-transforms + name: Apply jssg Transformations + type: automatic + steps: + - name: "Remove Provider from imports and unwrap JSX using jssg" + js-ast-grep: + js_file: scripts/index.ts + language: "javascript" + diff --git a/codemods/yaml-codemod/workflow.yaml b/codemods/yaml-codemod/workflow.yaml index 3db4082..aea689d 100644 --- a/codemods/yaml-codemod/workflow.yaml +++ b/codemods/yaml-codemod/workflow.yaml @@ -1,7 +1,7 @@ version: "1" nodes: - id: apply-rules - name: Apply AST-grep Rules + name: Apply ast-grep Rules steps: - name: "Scan typescript files and apply fixes" ast-grep: diff --git a/scripts/test-all.sh b/scripts/test-all.sh index c2a1d5b..b1e92c1 100755 --- a/scripts/test-all.sh +++ b/scripts/test-all.sh @@ -1,26 +1,26 @@ #!/bin/bash -# Test JSSG codemods using Codemod CLI native features +# Test jssg codemods using Codemod CLI native features set -e -echo "๐Ÿงช Testing JSSG codemods..." +echo "๐Ÿงช Testing jssg codemods..." -# Find JSSG codemods (those with js-ast-grep in workflow.yaml) +# Find jssg codemods (those with js-ast-grep in workflow.yaml) find codemods -name "workflow.yaml" | while read -r workflow; do recipe_dir=$(dirname "$workflow") recipe_name=$(basename "$recipe_dir") - # Check if it's a JSSG codemod + # Check if it's a jssg codemod if grep -q "js-ast-grep:" "$workflow"; then echo "" - echo "๐Ÿ“ Testing JSSG codemod: $recipe_name..." + echo "๐Ÿ“ Testing jssg codemod: $recipe_name..." # Validate workflow first echo " โœ… Validating workflow..." npx codemod@latest workflow validate --workflow "$workflow" - # Run JSSG tests - echo " ๐Ÿงช Running JSSG tests..." + # Run jssg tests + echo " ๐Ÿงช Running jssg tests..." js_file=$(grep "js_file:" "$workflow" | sed 's/.*js_file:\s*["'\'']*\([^"'\'']*\)["'\'']*.*/\1/' | tr -d ' ') language=$(grep "language:" "$workflow" | sed 's/.*language:\s*["'\'']*\([^"'\'']*\)["'\'']*.*/\1/' | tr -d ' ') if [ -z "$language" ]; then @@ -39,4 +39,4 @@ find codemods -name "workflow.yaml" | while read -r workflow; do done echo "" -echo "๐ŸŽ‰ All JSSG codemod tests completed successfully!" \ No newline at end of file +echo "๐ŸŽ‰ All jssg codemod tests completed successfully!" \ No newline at end of file