Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"files": {
"includes": ["."],
"experimentalScannerIgnores": ["codemods/**/tests/**/fixtures/**"]
Expand Down
33 changes: 33 additions & 0 deletions codemods/pos-api-smartgrid-to-action/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Build artifacts
target/
dist/
build/

# Temporary files
*.tmp
*.temp
.cache/

# Environment files
.env
.env.local

# IDE files
.vscode/
.idea/
*.swp
*.swo

# OS files
.DS_Store
Thumbs.db

# Package bundles
*.tar.gz
*.tgz
39 changes: 39 additions & 0 deletions codemods/pos-api-smartgrid-to-action/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# testing

Transform legacy code patterns

## Installation

```bash
# Install from registry
codemod run testing

# Or run locally
codemod run -w workflow.yaml
```

## Usage

This codemod transforms typescript code by:

- Converting `var` declarations to `const`/`let`
- Removing debug statements
- Modernizing syntax patterns

## Development

```bash
# Test the transformation
npm test

# Validate the workflow
codemod validate -w workflow.yaml

# Publish to registry
codemod login
codemod publish
```

## License

MIT
18 changes: 18 additions & 0 deletions codemods/pos-api-smartgrid-to-action/codemod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
schema_version: "1.0"

name: "testing"
version: "0.1.0"
description: "Transform legacy code patterns"
author: "Shadi <[email protected]>"
license: "MIT"
workflow: "workflow.yaml"
category: "migration"

targets:
languages: ["typescript"]

keywords: ["transformation", "migration"]

registry:
access: "private"
visibility: "private"
14 changes: 14 additions & 0 deletions codemods/pos-api-smartgrid-to-action/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "testing",
"version": "0.1.0",
"description": "Transform legacy code patterns",
"type": "module",
"devDependencies": {
"@codemod.com/jssg-types": "^1.0.7",
"typescript": "^5.8.3"
},
"scripts": {
"test": "npx codemod@latest jssg test --language typescript ./scripts/codemod.ts",
"check-types": "npx tsc --noEmit"
}
}
144 changes: 144 additions & 0 deletions codemods/pos-api-smartgrid-to-action/scripts/codemod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import type { SgRoot, Edit } from "codemod:ast-grep";
import type TS from "codemod:ast-grep/langs/typescript";

// Utilities for API alias detection and validation
function getApiAliases(rootNode: any): Set<string> {
return getVariableAliases(rootNode, "api", ["smartGrid"]);
}

function getVariableAliases(
rootNode: any,
sourceVar: string,
destructuredProps: string[] = []
): Set<string> {
const aliases = new Set<string>([sourceVar]);

// Pattern 1: const myVar = sourceVar
const directAliases = rootNode.findAll({
rule: { pattern: `const $VAR = ${sourceVar}` },
});

for (const alias of directAliases) {
const varName = alias.getMatch("VAR")?.text();
if (varName) {
aliases.add(varName);
}
}

// Pattern 2: const { prop } = sourceVar (for specified destructured properties)
for (const prop of destructuredProps) {
const destructuring = rootNode.findAll({
rule: { pattern: `const { ${prop} } = ${sourceVar}` },
});

if (destructuring.length > 0) {
aliases.add(prop);
}
}

return aliases;
}

function isValidObjectReference(
objectText: string,
aliases: Set<string>
): boolean {
// Direct alias match
if (aliases.has(objectText)) return true;

// Optional chaining pattern (api?)
const optionalBase = objectText.replace(/\?$/, "");
if (aliases.has(optionalBase)) return true;

// Function call patterns: getApi(), api(), getApi()?, etc.
const functionCallBase = objectText.replace(/\(\)\??$/, "");
if (aliases.has(functionCallBase)) return true;

// Check if function call returns an API (any function ending with 'api' or known patterns)
if (objectText.endsWith("()") || objectText.endsWith("()?")) {
const funcName = objectText.replace(/\(\)\??$/, "");
// Common patterns like getApi, fetchApi, etc.
if (funcName.toLowerCase().includes("api")) return true;
}

// this.alias pattern (including optional chaining)
for (const alias of aliases) {
if (
objectText === `this.${alias}` ||
objectText.startsWith(`this.${alias}.`)
) {
return true;
}
// Handle this?.alias? patterns
if (objectText === `this?.${alias}?` || objectText === `this?.${alias}`) {
return true;
}
}

return false;
}

/**
* Transform api.smartGrid.presentModal() calls to api.action.presentModal()
* Handles various patterns including aliases, destructuring, optional chaining, etc.
*/
async function transform(root: SgRoot<TS>): Promise<string | null> {
const rootNode = root.root();
const edits: Edit[] = [];

// Get all possible aliases for 'api' including destructured 'smartGrid'
const apiAliases = getApiAliases(rootNode);

// Find all 'smartGrid' property identifiers in the code
const smartGridProps = rootNode
.findAll({
rule: {
kind: "property_identifier",
},
})
.filter((prop) => prop.text() === "smartGrid");

for (const prop of smartGridProps) {
if (!shouldTransformProperty(prop, apiAliases)) {
continue;
}

// Replace "smartGrid" with "action"
edits.push(prop.replace("action"));
}

return edits.length === 0 ? null : rootNode.commitEdits(edits);
}

/**
* Determine if a smartGrid property should be transformed
* Only transforms api.smartGrid.presentModal() patterns
*/
function shouldTransformProperty(prop: any, apiAliases: Set<string>): boolean {
// Get the parent member expression (e.g., "api.smartGrid")
const memberExpr = prop.parent();
if (!memberExpr || memberExpr.kind() !== "member_expression") return false;

// Check if the object is a valid API reference
const objectNode = memberExpr.field("object");
if (!objectNode) return false;

const objectText = objectNode.text();
if (!isValidObjectReference(objectText, apiAliases)) return false;

// Get the outer member expression (e.g., "api.smartGrid.presentModal")
const outerMemberExpr = memberExpr.parent();
if (!outerMemberExpr || outerMemberExpr.kind() !== "member_expression") {
return false;
}

// Only transform if the method is "presentModal"
const property = outerMemberExpr.field("property");
if (!property || property.text() !== "presentModal") return false;

// Only transform if this is a function call (not just property access)
const callExpr = outerMemberExpr.parent();
return callExpr && callExpr.kind() === "call_expression";
}

export default transform;
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Legacy patterns that might exist in older Shopify codebases
declare const api: any;

// Pattern 1: Class-based components
class ModalManager {
private api: any;

constructor(apiInstance: any) {
this.api = apiInstance;
}

async showDialog(options: any) {
// Should be transformed
return await this.api.smartGrid.presentModal(options);
}

async confirmAction(message: string) {
// Should be transformed
const result = await this.api.smartGrid.presentModal({
kind: "confirmation",
message: message,
});

return result.reason === "confirm";
}

// Should NOT be transformed - different method
hideModal() {
return this.api.smartGrid.closeModal();
}
}

// Pattern 2: Complex expressions
const modalActions = {
bulk: {
confirm: (items: any[]) =>
api.smartGrid.presentModal({
kind: "confirmation",
title: `Process ${items.length} items?`,
message: "This will update all selected items.",
}),
},
};

// Pattern 3: Higher-order function
function withModal(api: any) {
return function (options: any) {
return api.smartGrid.presentModal(options);
};
}

const showModal = withModal(api);

// Pattern 4: Async/await with error handling
async function safeShowModal(api: any, options: any) {
try {
const result = await api.smartGrid.presentModal(options);
return { success: true, result };
} catch (error) {
console.error("Modal failed:", error);
return { success: false, error };
}
}

// Pattern 5: Template literals and dynamic content
function showProductModal(api: any, product: any) {
return api.smartGrid.presentModal({
kind: "info",
title: `Product: ${product.title}`,
message: `
Price: ${product.price}
Stock: ${product.inventory}
`.trim(),
});
}

// Additional edge case test patterns
function testThisApiPattern(this: any, api: any, options: any) {
return this?.api?.smartGrid.presentModal(options); // Should transform
}
Loading
Loading