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 (
+
+ );
+}
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 (
+
+ );
+}
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