Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions codemods/app-bridge-react/remove-provider/README.md
Original file line number Diff line number Diff line change
@@ -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 `<Provider>...</Provider>` JSX elements, leaving only the children
- Unwraps aliased Provider JSX elements like `<AppProvider>...</AppProvider>`
- Removes self-closing `<Provider />` 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 (
<Provider config={{apiKey: 'old', host: 'host'}}>
<div>
<TitleBar title="Hello" />
</div>
</Provider>
);
}
```

**After:**
```jsx
import { TitleBar } from '@shopify/app-bridge-react';

export default function App() {
return (
<div>
<TitleBar title="Hello" />
</div>
);
}
```

### Aliased Provider Removal

**Before:**
```jsx
import {Provider as AppProvider, TitleBar} from '@shopify/app-bridge-react';

export default function App() {
return (
<AppProvider config={{apiKey: 'old', host: 'host'}}>
<div>
<TitleBar title="Hello" />
</div>
</AppProvider>
);
}
```

**After:**
```jsx
import { TitleBar } from '@shopify/app-bridge-react';

export default function App() {
return (
<div>
<TitleBar title="Hello" />
</div>
);
}
```

## 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)
24 changes: 24 additions & 0 deletions codemods/app-bridge-react/remove-provider/codemod.yaml
Original file line number Diff line number Diff line change
@@ -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"

15 changes: 15 additions & 0 deletions codemods/app-bridge-react/remove-provider/rules/config.yml
Original file line number Diff line number Diff line change
@@ -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: <Provider $$$PROPS>$$$CHILDREN</Provider>
fix: $$$CHILDREN
31 changes: 31 additions & 0 deletions codemods/app-bridge-react/remove-provider/rules/imports.yml
Original file line number Diff line number Diff line change
@@ -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: ""
23 changes: 23 additions & 0 deletions codemods/app-bridge-react/remove-provider/rules/jsx.yml
Original file line number Diff line number Diff line change
@@ -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: <Provider $$$PROPS>$$$CHILDREN</Provider>
fix: $$$CHILDREN

# Rule 2: Handle Provider with no props
- pattern: <Provider>$$$CHILDREN</Provider>
fix: $$$CHILDREN

# Rule 3: Remove self-closing Provider elements
- pattern: <Provider $$$PROPS />
fix: ""

# Rule 4: Remove self-closing Provider with no props
- pattern: <Provider />
fix: ""
166 changes: 166 additions & 0 deletions codemods/app-bridge-react/remove-provider/scripts/index.ts
Original file line number Diff line number Diff line change
@@ -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<JS>): Promise<string> {
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[^>]*>(.*?)<\/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: '<Provider $$$PROPS />',
},
});

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;


Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { TitleBar } from '@shopify/app-bridge-react';

export default function App() {
return (
<div>
<TitleBar title="Hello" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import {Provider as AppProvider, TitleBar} from '@shopify/app-bridge-react';

export default function App() {
return (
<AppProvider config={{apiKey: 'old', host: 'host'}}>
<div>
<TitleBar title="Hello" />
</div>
</AppProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ReactDOM from 'react-dom';

function MyApp() {
return (
<div>My app</div>
);
}

const root = document.createElement('div');
document.body.appendChild(root);
ReactDOM.createRoot(root).render(<MyApp />);
Loading
Loading