From 67f35efebc134f4b14be1fdebe51c18a91674811 Mon Sep 17 00:00:00 2001 From: Shadi Date: Wed, 17 Sep 2025 20:33:51 -0700 Subject: [PATCH 01/12] feat: initialize POS API smartGrid migration codemod project - Set up basic codemod structure with workflow and package configuration - Add TypeScript configuration for development - Implement AST-based smartGrid to action transformation - Add basic test fixtures for validation --- biome.jsonc | 2 +- .../pos-api-smartgrid-to-action/.gitignore | 33 +++++++++ .../pos-api-smartgrid-to-action/codemod.yaml | 18 +++++ .../pos-api-smartgrid-to-action/package.json | 14 ++++ .../scripts/codemod.ts | 70 +++++++++++++++++++ .../pos-api-smartgrid-to-action/tsconfig.json | 17 +++++ .../pos-api-smartgrid-to-action/workflow.yaml | 11 +++ 7 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 codemods/pos-api-smartgrid-to-action/.gitignore create mode 100644 codemods/pos-api-smartgrid-to-action/codemod.yaml create mode 100644 codemods/pos-api-smartgrid-to-action/package.json create mode 100644 codemods/pos-api-smartgrid-to-action/scripts/codemod.ts create mode 100644 codemods/pos-api-smartgrid-to-action/tsconfig.json create mode 100644 codemods/pos-api-smartgrid-to-action/workflow.yaml diff --git a/biome.jsonc b/biome.jsonc index 29e9ef5..74a3877 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -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/**"] diff --git a/codemods/pos-api-smartgrid-to-action/.gitignore b/codemods/pos-api-smartgrid-to-action/.gitignore new file mode 100644 index 0000000..78174f4 --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/.gitignore @@ -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 \ No newline at end of file diff --git a/codemods/pos-api-smartgrid-to-action/codemod.yaml b/codemods/pos-api-smartgrid-to-action/codemod.yaml new file mode 100644 index 0000000..fce2cab --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/codemod.yaml @@ -0,0 +1,18 @@ +schema_version: "1.0" + +name: "testing" +version: "0.1.0" +description: "Transform legacy code patterns" +author: "Shadi " +license: "MIT" +workflow: "workflow.yaml" +category: "migration" + +targets: + languages: ["typescript"] + +keywords: ["transformation", "migration"] + +registry: + access: "private" + visibility: "private" diff --git a/codemods/pos-api-smartgrid-to-action/package.json b/codemods/pos-api-smartgrid-to-action/package.json new file mode 100644 index 0000000..80a62cd --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/package.json @@ -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" + } +} diff --git a/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts b/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts new file mode 100644 index 0000000..45ad3fe --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts @@ -0,0 +1,70 @@ +import type { SgRoot, Edit } from "codemod:ast-grep"; +import type TS from "codemod:ast-grep/langs/typescript"; + +async function transform(root: SgRoot): Promise { + const rootNode = root.root(); + const edits: Edit[] = []; + + const smartGridProperties = rootNode + .findAll({ + rule: { + kind: "property_identifier", + }, + }) + .filter((prop) => prop.text() === "smartGrid"); + + const apiAliases = new Set(); + apiAliases.add("api"); // Always include 'api' + + const aliasAssignments = rootNode.findAll({ + rule: { pattern: "const $VAR = api" }, + }); + + for (const assignment of aliasAssignments) { + const varName = assignment.getMatch("VAR")?.text(); + if (varName) { + apiAliases.add(varName); + } + } + + for (const smartGridProp of smartGridProperties) { + // Get the parent member expression (e.g., "api.smartGrid") + const memberExpr = smartGridProp.parent(); + if (!memberExpr || memberExpr.kind() !== "member_expression") continue; + + // Check if the object of this member expression is 'api' or an alias + const objectNode = memberExpr.field("object"); + if (!objectNode) continue; + + const objectText = objectNode.text(); + const isApiOrAlias = + apiAliases.has(objectText) || + objectText === "api?" || + objectText.endsWith("()") || + objectText.endsWith("()?"); + + if (!isApiOrAlias) continue; + + // Get the grandparent which should be the outer member expression (e.g., "api.smartGrid.presentModal") + const outerMemberExpr = memberExpr.parent(); + if (!outerMemberExpr || outerMemberExpr.kind() !== "member_expression") + continue; + + const property = outerMemberExpr.field("property"); + if (property && property.text() === "presentModal") { + // Only transform if this is a function call (has call_expression as parent) + const callExpr = outerMemberExpr.parent(); + if (callExpr && callExpr.kind() === "call_expression") { + edits.push(smartGridProp.replace("action")); + } + } + } + + if (edits.length === 0) { + return null; + } + + return rootNode.commitEdits(edits); +} + +export default transform; diff --git a/codemods/pos-api-smartgrid-to-action/tsconfig.json b/codemods/pos-api-smartgrid-to-action/tsconfig.json new file mode 100644 index 0000000..469fc5a --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["@codemod.com/jssg-types"], + "allowImportingTsExtensions": true, + "noEmit": true, + "verbatimModuleSyntax": true, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": ["tests"] +} diff --git a/codemods/pos-api-smartgrid-to-action/workflow.yaml b/codemods/pos-api-smartgrid-to-action/workflow.yaml new file mode 100644 index 0000000..50a4933 --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/workflow.yaml @@ -0,0 +1,11 @@ +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: "Scan typescript files and apply fixes" + js-ast-grep: + js_file: scripts/codemod.ts + language: "typescript" From 8879a5eef8222a81a88ef9bf9b9285af7f5d5da1 Mon Sep 17 00:00:00 2001 From: Shadi Date: Wed, 17 Sep 2025 20:34:07 -0700 Subject: [PATCH 02/12] feat(utils): add comprehensive AST utility library - Add getImports() with support for all import patterns - Add getNamedImports() for specific import extraction - Add getVariableAliases() for dynamic alias detection - Add isValidObjectReference() with edge case handling - Provide foundation for reusable codemod utilities --- utils/ast-utils.ts | 313 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 utils/ast-utils.ts diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts new file mode 100644 index 0000000..75b513d --- /dev/null +++ b/utils/ast-utils.ts @@ -0,0 +1,313 @@ +// Type definitions for AST-grep nodes (avoiding import issues) +export interface SgNode { + text(): string; + kind(): string; + parent(): SgNode | null; + field(name: string): SgNode | null; + getMatch(name: string): SgNode | null; + replace(text: string): any; + findAll(config: { rule: any }): SgNode[]; +} + +export interface ImportInfo { + source: string; + specifier: string; + node: any; +} + +/** + * Generic utility to get variable aliases for any source variable + * @param rootNode - The root AST node to search in + * @param sourceVar - The source variable name (e.g., "api", "Polaris", "React") + * @param destructuredProps - Optional array of properties that create direct aliases when destructured + * @returns Set of all aliases for the source variable + * + * @example + * // For API aliases: const myApi = api; const { smartGrid } = api; + * const apiAliases = getVariableAliases(rootNode, "api", ["smartGrid"]); + * + * // For Polaris aliases: const MyPolaris = Polaris; + * const polarisAliases = getVariableAliases(rootNode, "Polaris"); + */ +export function getVariableAliases( + rootNode: any, + sourceVar: string, + destructuredProps: string[] = [] +): Set { + const aliases = new Set([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; +} + +/** + * Legacy API-specific wrapper for backward compatibility + * @deprecated Use getVariableAliases(rootNode, "api", ["smartGrid"]) instead + */ +export function getApiAliases(rootNode: any): Set { + return getVariableAliases(rootNode, "api", ["smartGrid"]); +} + +/** + * Generic utility to find member expressions with specific object and property + * @param rootNode - The root AST node to search in + * @param objectAliases - Set of valid object aliases + * @param property - The property name to find (e.g., "smartGrid", "Button", "Modal") + * @param method - Optional method name that must follow the property (e.g., "presentModal") + * @returns Array of matching AST nodes + * + * @example + * // Find api.smartGrid.presentModal() calls + * const smartGridUsages = findMemberExpressions(rootNode, apiAliases, "smartGrid", "presentModal"); + * + * // Find Polaris.Button usages + * const buttonUsages = findMemberExpressions(rootNode, polarisAliases, "Button"); + */ +export function findMemberExpressions( + rootNode: any, + objectAliases: Set, + property: string, + method?: string +): any[] { + const usages: any[] = []; + const methodPattern = method ? `.${method}($$$ARGS)` : ""; + + for (const alias of objectAliases) { + if (alias === property) { + // Special case: direct property usage (from destructuring) + const directUsages = rootNode.findAll({ + rule: { pattern: `${alias}${methodPattern}` }, + }); + usages.push(...directUsages); + } else { + // Normal case: alias.property.method() + const memberUsages = rootNode.findAll({ + rule: { pattern: `${alias}.${property}${methodPattern}` }, + }); + usages.push(...memberUsages); + } + } + + // Handle this.alias patterns + const thisUsages = rootNode.findAll({ + rule: { + pattern: `this.${ + Array.from(objectAliases)[0] + }.${property}${methodPattern}`, + }, + }); + usages.push(...thisUsages); + + // Handle optional chaining + const optionalUsages = rootNode.findAll({ + rule: { + pattern: `${Array.from(objectAliases)[0]}?.${property}${methodPattern}`, + }, + }); + usages.push(...optionalUsages); + + // Handle function call patterns + const functionCallUsages = rootNode.findAll({ + rule: { pattern: `$FUNC().${property}${methodPattern}` }, + }); + usages.push(...functionCallUsages); + + return usages; +} + +/** + * Get all imports from a specific package/module + * @param rootNode - The root AST node to search in + * @param packageName - The package name to search for (e.g., "@shopify/polaris", "react") + * @returns Array of import information + * + * @example + * // Find all Polaris imports + * const polarisImports = getImports(rootNode, "@shopify/polaris"); + */ +export function getImports(rootNode: any, packageName: string): ImportInfo[] { + const imports: ImportInfo[] = []; + + // Pattern 1: import specifier from "package" + const defaultImports = rootNode.findAll({ + rule: { pattern: `import $SPEC from "${packageName}"` }, + }); + + for (const importNode of defaultImports) { + const spec = importNode.getMatch("SPEC"); + if (spec) { + imports.push({ + source: packageName, + specifier: spec.text(), + node: importNode, + }); + } + } + + // Pattern 2: import { namedImports } from "package" + const namedImports = rootNode.findAll({ + rule: { pattern: `import { $$$SPECS } from "${packageName}"` }, + }); + + for (const importNode of namedImports) { + const specs = importNode.getMatch("SPECS"); + if (specs) { + imports.push({ + source: packageName, + specifier: `{ ${specs.text()} }`, + node: importNode, + }); + } + } + + // Pattern 3: import * as alias from "package" + const namespaceImports = rootNode.findAll({ + rule: { pattern: `import * as $ALIAS from "${packageName}"` }, + }); + + for (const importNode of namespaceImports) { + const alias = importNode.getMatch("ALIAS"); + if (alias) { + imports.push({ + source: packageName, + specifier: `* as ${alias.text()}`, + node: importNode, + }); + } + } + + // Pattern 4: import "package" (side-effect only) + const sideEffectImports = rootNode.findAll({ + rule: { pattern: `import "${packageName}"` }, + }); + + for (const importNode of sideEffectImports) { + imports.push({ + source: packageName, + specifier: "", + node: importNode, + }); + } + + return imports; +} + +/** + * Legacy function for backward compatibility + * @deprecated Use getImports(rootNode, packageName) instead + */ +export function getImportSources( + rootNode: any, + packageName: string +): ImportInfo[] { + return getImports(rootNode, packageName); +} + +/** + * Get specific named imports from a package + * @param rootNode - The root AST node to search in + * @param packageName - The package name to search for + * @param importName - The specific import name to find + * @returns Array of nodes that import the specific name + * + * @example + * // Find all imports of 'Button' from '@shopify/polaris' + * const buttonImports = getNamedImports(rootNode, "@shopify/polaris", "Button"); + */ +export function getNamedImports( + rootNode: any, + packageName: string, + importName: string +): any[] { + const nodes: any[] = []; + + // Pattern: import { importName } from "package" or import { importName, ... } from "package" + const imports = rootNode.findAll({ + rule: { pattern: `import { $$$SPECS } from "${packageName}"` }, + }); + + for (const importNode of imports) { + const specs = importNode.getMatch("SPECS"); + if (specs && specs.text().includes(importName)) { + nodes.push(importNode); + } + } + + return nodes; +} + +/** + * Check if an object reference is valid based on a set of aliases + * Handles complex patterns like this.api, api(), api?.method, getApi(), etc. + * @param objectText - The text of the object being checked + * @param aliases - Set of valid aliases + * @returns boolean indicating if the object is valid + * + * @example + * const apiAliases = new Set(['api', 'myApi']); + * isValidObjectReference('api', apiAliases); // true + * isValidObjectReference('this.api', apiAliases); // true + * isValidObjectReference('getApi()', apiAliases); // true + * isValidObjectReference('getApi()?.', apiAliases); // true + * isValidObjectReference('someOther', apiAliases); // false + */ +export function isValidObjectReference( + objectText: string, + aliases: Set +): 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; +} From 7b0599c67d25305a910a91af3e746fdda0740377 Mon Sep 17 00:00:00 2001 From: Shadi Date: Wed, 17 Sep 2025 20:34:24 -0700 Subject: [PATCH 03/12] refactor(pos-api): modularize and improve transformation logic - Extract utility functions (getApiAliases, isValidObjectReference) - Add shouldTransformProperty() helper for cleaner code - Support complex patterns: this?.api?, getApi()?, function calls - Improve alias detection with proper destructuring support - Add comprehensive documentation and comments --- .../scripts/codemod.ts | 160 +++++++++++++----- 1 file changed, 117 insertions(+), 43 deletions(-) diff --git a/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts b/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts index 45ad3fe..6b0f978 100644 --- a/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts +++ b/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts @@ -1,11 +1,96 @@ 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 { + return getVariableAliases(rootNode, "api", ["smartGrid"]); +} + +function getVariableAliases( + rootNode: any, + sourceVar: string, + destructuredProps: string[] = [] +): Set { + const aliases = new Set([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 +): 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): Promise { const rootNode = root.root(); const edits: Edit[] = []; - const smartGridProperties = rootNode + // 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", @@ -13,58 +98,47 @@ async function transform(root: SgRoot): Promise { }) .filter((prop) => prop.text() === "smartGrid"); - const apiAliases = new Set(); - apiAliases.add("api"); // Always include 'api' - - const aliasAssignments = rootNode.findAll({ - rule: { pattern: "const $VAR = api" }, - }); - - for (const assignment of aliasAssignments) { - const varName = assignment.getMatch("VAR")?.text(); - if (varName) { - apiAliases.add(varName); + for (const prop of smartGridProps) { + if (!shouldTransformProperty(prop, apiAliases)) { + continue; } - } - for (const smartGridProp of smartGridProperties) { - // Get the parent member expression (e.g., "api.smartGrid") - const memberExpr = smartGridProp.parent(); - if (!memberExpr || memberExpr.kind() !== "member_expression") continue; + // Replace "smartGrid" with "action" + edits.push(prop.replace("action")); + } - // Check if the object of this member expression is 'api' or an alias - const objectNode = memberExpr.field("object"); - if (!objectNode) continue; + return edits.length === 0 ? null : rootNode.commitEdits(edits); +} - const objectText = objectNode.text(); - const isApiOrAlias = - apiAliases.has(objectText) || - objectText === "api?" || - objectText.endsWith("()") || - objectText.endsWith("()?"); +/** + * Determine if a smartGrid property should be transformed + * Only transforms api.smartGrid.presentModal() patterns + */ +function shouldTransformProperty(prop: any, apiAliases: Set): boolean { + // Get the parent member expression (e.g., "api.smartGrid") + const memberExpr = prop.parent(); + if (!memberExpr || memberExpr.kind() !== "member_expression") return false; - if (!isApiOrAlias) continue; + // Check if the object is a valid API reference + const objectNode = memberExpr.field("object"); + if (!objectNode) return false; - // Get the grandparent which should be the outer member expression (e.g., "api.smartGrid.presentModal") - const outerMemberExpr = memberExpr.parent(); - if (!outerMemberExpr || outerMemberExpr.kind() !== "member_expression") - continue; + const objectText = objectNode.text(); + if (!isValidObjectReference(objectText, apiAliases)) return false; - const property = outerMemberExpr.field("property"); - if (property && property.text() === "presentModal") { - // Only transform if this is a function call (has call_expression as parent) - const callExpr = outerMemberExpr.parent(); - if (callExpr && callExpr.kind() === "call_expression") { - edits.push(smartGridProp.replace("action")); - } - } + // Get the outer member expression (e.g., "api.smartGrid.presentModal") + const outerMemberExpr = memberExpr.parent(); + if (!outerMemberExpr || outerMemberExpr.kind() !== "member_expression") { + return false; } - if (edits.length === 0) { - return null; - } + // Only transform if the method is "presentModal" + const property = outerMemberExpr.field("property"); + if (!property || property.text() !== "presentModal") return false; - return rootNode.commitEdits(edits); + // 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; From 2c2209791f6bee3304fab8d82ef46ba10268a452 Mon Sep 17 00:00:00 2001 From: Shadi Date: Wed, 17 Sep 2025 20:34:39 -0700 Subject: [PATCH 04/12] test: add comprehensive test samples for pos-api codemod - Add shopify-pos-extension.ts with realistic POS patterns - Add legacy-patterns.ts with class-based and complex patterns - Add pos-checkout-extension.js with JavaScript patterns - Add README documentation for testing --- .../pos-api-smartgrid-to-action/README.md | 39 +++++++++ .../test-samples/legacy-patterns.ts | 80 ++++++++++++++++++ .../test-samples/pos-checkout-extension.js | 70 ++++++++++++++++ .../test-samples/shopify-pos-extension.ts | 83 +++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 codemods/pos-api-smartgrid-to-action/README.md create mode 100644 codemods/pos-api-smartgrid-to-action/test-samples/legacy-patterns.ts create mode 100644 codemods/pos-api-smartgrid-to-action/test-samples/pos-checkout-extension.js create mode 100644 codemods/pos-api-smartgrid-to-action/test-samples/shopify-pos-extension.ts diff --git a/codemods/pos-api-smartgrid-to-action/README.md b/codemods/pos-api-smartgrid-to-action/README.md new file mode 100644 index 0000000..352099c --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/README.md @@ -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 \ No newline at end of file diff --git a/codemods/pos-api-smartgrid-to-action/test-samples/legacy-patterns.ts b/codemods/pos-api-smartgrid-to-action/test-samples/legacy-patterns.ts new file mode 100644 index 0000000..e7387b6 --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/test-samples/legacy-patterns.ts @@ -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 +} diff --git a/codemods/pos-api-smartgrid-to-action/test-samples/pos-checkout-extension.js b/codemods/pos-api-smartgrid-to-action/test-samples/pos-checkout-extension.js new file mode 100644 index 0000000..f6c7f79 --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/test-samples/pos-checkout-extension.js @@ -0,0 +1,70 @@ +// JavaScript POS extension patterns +import { extend } from "@shopify/pos-ui-extensions"; + +export default extend( + "pos.checkout.customer.smartGrid", + (root, { api, ui }) => { + const customerModal = ui.createComponent("Button", { + title: "Customer Info", + onPress: () => { + // Pattern: Arrow function with implicit return + api.smartGrid.presentModal({ + kind: "form", + title: "Customer Details", + fields: [ + { type: "email", label: "Email" }, + { type: "phone", label: "Phone" }, + ], + }); + }, + }); + + // Pattern: Object method + const modalHelpers = { + async showConfirmation(message) { + const result = await api.smartGrid.presentModal({ + kind: "confirmation", + message: message, + }); + return result.reason === "confirm"; + }, + + showAlert: (title, message) => { + // Pattern: Arrow function assignment + return api.smartGrid.presentModal({ + kind: "alert", + title, + message, + }); + }, + }; + + // Pattern: Destructured API + const { smartGrid } = api; + const showModal = () => + smartGrid.presentModal({ + kind: "info", + title: "Information", + }); + + // Additional edge case patterns + const myApi = api; + myApi.smartGrid.presentModal({ kind: "alias" }); + + // Optional chaining pattern + api?.smartGrid.presentModal({ kind: "optional" }); + + // Function call pattern + function getApi() { + return api; + } + getApi().smartGrid.presentModal({ kind: "function" }); + + root.appendChild(customerModal); + } +); +api.smartGrid.presentModal({ kind: "test" }); // Should transform +const myApi = api; +myApi.smartGrid.presentModal(); // Should transform +api?.smartGrid.presentModal(); // Should transform +getApi().smartGrid.presentModal(); // Should transform diff --git a/codemods/pos-api-smartgrid-to-action/test-samples/shopify-pos-extension.ts b/codemods/pos-api-smartgrid-to-action/test-samples/shopify-pos-extension.ts new file mode 100644 index 0000000..00d48c3 --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/test-samples/shopify-pos-extension.ts @@ -0,0 +1,83 @@ +//Shopify POS UI Extension patterns +// Note: @shopify/pos-ui-extensions types not available in this environment +declare const extend: any; +declare const Modal: any; + +export default extend( + "pos.cart.line-item.action", + (root: any, { api, ui }: any) => { + const button = ui.createComponent("Button", { + title: "Custom Action", + onPress: async () => { + // Pattern 1: Basic modal usage + const result = await api.smartGrid.presentModal({ + kind: "confirmation", + title: "Confirm Action", + message: "Are you sure you want to proceed?", + }); + + if (result.reason === "confirm") { + // Pattern 2: Nested modal calls + const detailsResult = await api.smartGrid.presentModal({ + kind: "form", + title: "Enter Details", + fields: [ + { type: "text", label: "Name", required: true }, + { type: "number", label: "Quantity", required: true }, + ], + }); + + if (detailsResult.reason === "confirm") { + console.log("Details submitted:", detailsResult.data); + } + } + }, + }); + + root.appendChild(button); + } +); + +// Pattern 3: Modal in utility function +async function showCustomModal(api: any, product: any) { + return await api.smartGrid.presentModal({ + kind: "custom", + title: `Product: ${product.title}`, + content: product.description, + }); +} + +// Pattern 4: Conditional modal +function maybeShowModal(api: any, condition: boolean) { + if (condition) { + return api.smartGrid.presentModal({ + kind: "alert", + message: "Something happened!", + }); + } + return Promise.resolve(null); +} + +// Pattern 5: Modal with promise chain +export function handleProductAction(api: any, productId: string) { + return api.smartGrid + .presentModal({ + kind: "confirmation", + title: "Delete Product", + message: "This action cannot be undone.", + }) + .then((result: any) => { + if (result.reason === "confirm") { + return deleteProduct(productId); + } + throw new Error("Action cancelled"); + }) + .catch((error: any) => { + console.error("Product action failed:", error); + }); +} + +function deleteProduct(id: string) { + // Implementation + return Promise.resolve(); +} From 095eaae61df9ff83c2fddb23543c5ee06d872be6 Mon Sep 17 00:00:00 2001 From: Shadi Date: Wed, 17 Sep 2025 20:48:23 -0700 Subject: [PATCH 05/12] test: add test fixtures for jssg test runner --- .../tests/fixtures/expected.js | 36 +++++++++++++++++++ .../tests/fixtures/input.js | 36 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 codemods/pos-api-smartgrid-to-action/tests/fixtures/expected.js create mode 100644 codemods/pos-api-smartgrid-to-action/tests/fixtures/input.js diff --git a/codemods/pos-api-smartgrid-to-action/tests/fixtures/expected.js b/codemods/pos-api-smartgrid-to-action/tests/fixtures/expected.js new file mode 100644 index 0000000..905f0cc --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/tests/fixtures/expected.js @@ -0,0 +1,36 @@ +// Basic case +api.action.presentModal(); + +// With arguments +api.action.presentModal(modalData, options); + +// Assigned to variable +const result = api.action.presentModal(); + +// In method chain (promise) +const response = await api.action.presentModal().then(handleResponse); + +// Alias receiver +const x = api; +x.action.presentModal({ title: "Hi" }); + +// Optional chaining +api?.action.presentModal(); +const maybe = getApi()?.action.presentModal(cfg); + +// Spacing and comments should be preserved +api /*A*/.action /*B*/ + .presentModal /*C*/ + (/*D*/); + +// Already migrated (should be NO-OP) +api.action.presentModal(existing); + +// Negative: different API object (should NOT transform) +someOtherApi.smartGrid.presentModal(); + +// Negative: different method (should NOT transform) +api.smartGrid.someOtherMethod(); + +// Negative: property access only (no call) — should NOT transform +const fn = api.smartGrid.presentModal; diff --git a/codemods/pos-api-smartgrid-to-action/tests/fixtures/input.js b/codemods/pos-api-smartgrid-to-action/tests/fixtures/input.js new file mode 100644 index 0000000..7257a2e --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/tests/fixtures/input.js @@ -0,0 +1,36 @@ +// Basic case +api.smartGrid.presentModal(); + +// With arguments +api.smartGrid.presentModal(modalData, options); + +// Assigned to variable +const result = api.smartGrid.presentModal(); + +// In method chain (promise) +const response = await api.smartGrid.presentModal().then(handleResponse); + +// Alias receiver +const x = api; +x.smartGrid.presentModal({ title: "Hi" }); + +// Optional chaining +api?.smartGrid.presentModal(); +const maybe = getApi()?.smartGrid.presentModal(cfg); + +// Spacing and comments should be preserved +api /*A*/.smartGrid /*B*/ + .presentModal /*C*/ + (/*D*/); + +// Already migrated (should be NO-OP) +api.action.presentModal(existing); + +// Negative: different API object (should NOT transform) +someOtherApi.smartGrid.presentModal(); + +// Negative: different method (should NOT transform) +api.smartGrid.someOtherMethod(); + +// Negative: property access only (no call) — should NOT transform +const fn = api.smartGrid.presentModal; From fd75e066bbe0ef9081fc45d8c959aa9782f036ae Mon Sep 17 00:00:00 2001 From: Shadi Date: Thu, 18 Sep 2025 19:04:24 -0700 Subject: [PATCH 06/12] refactor(pos-api-smartgrid): removed extra test files --- .../test-samples/legacy-patterns.ts | 80 ------------------ .../test-samples/pos-checkout-extension.js | 70 ---------------- .../test-samples/shopify-pos-extension.ts | 83 ------------------- 3 files changed, 233 deletions(-) delete mode 100644 codemods/pos-api-smartgrid-to-action/test-samples/legacy-patterns.ts delete mode 100644 codemods/pos-api-smartgrid-to-action/test-samples/pos-checkout-extension.js delete mode 100644 codemods/pos-api-smartgrid-to-action/test-samples/shopify-pos-extension.ts diff --git a/codemods/pos-api-smartgrid-to-action/test-samples/legacy-patterns.ts b/codemods/pos-api-smartgrid-to-action/test-samples/legacy-patterns.ts deleted file mode 100644 index e7387b6..0000000 --- a/codemods/pos-api-smartgrid-to-action/test-samples/legacy-patterns.ts +++ /dev/null @@ -1,80 +0,0 @@ -// 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 -} diff --git a/codemods/pos-api-smartgrid-to-action/test-samples/pos-checkout-extension.js b/codemods/pos-api-smartgrid-to-action/test-samples/pos-checkout-extension.js deleted file mode 100644 index f6c7f79..0000000 --- a/codemods/pos-api-smartgrid-to-action/test-samples/pos-checkout-extension.js +++ /dev/null @@ -1,70 +0,0 @@ -// JavaScript POS extension patterns -import { extend } from "@shopify/pos-ui-extensions"; - -export default extend( - "pos.checkout.customer.smartGrid", - (root, { api, ui }) => { - const customerModal = ui.createComponent("Button", { - title: "Customer Info", - onPress: () => { - // Pattern: Arrow function with implicit return - api.smartGrid.presentModal({ - kind: "form", - title: "Customer Details", - fields: [ - { type: "email", label: "Email" }, - { type: "phone", label: "Phone" }, - ], - }); - }, - }); - - // Pattern: Object method - const modalHelpers = { - async showConfirmation(message) { - const result = await api.smartGrid.presentModal({ - kind: "confirmation", - message: message, - }); - return result.reason === "confirm"; - }, - - showAlert: (title, message) => { - // Pattern: Arrow function assignment - return api.smartGrid.presentModal({ - kind: "alert", - title, - message, - }); - }, - }; - - // Pattern: Destructured API - const { smartGrid } = api; - const showModal = () => - smartGrid.presentModal({ - kind: "info", - title: "Information", - }); - - // Additional edge case patterns - const myApi = api; - myApi.smartGrid.presentModal({ kind: "alias" }); - - // Optional chaining pattern - api?.smartGrid.presentModal({ kind: "optional" }); - - // Function call pattern - function getApi() { - return api; - } - getApi().smartGrid.presentModal({ kind: "function" }); - - root.appendChild(customerModal); - } -); -api.smartGrid.presentModal({ kind: "test" }); // Should transform -const myApi = api; -myApi.smartGrid.presentModal(); // Should transform -api?.smartGrid.presentModal(); // Should transform -getApi().smartGrid.presentModal(); // Should transform diff --git a/codemods/pos-api-smartgrid-to-action/test-samples/shopify-pos-extension.ts b/codemods/pos-api-smartgrid-to-action/test-samples/shopify-pos-extension.ts deleted file mode 100644 index 00d48c3..0000000 --- a/codemods/pos-api-smartgrid-to-action/test-samples/shopify-pos-extension.ts +++ /dev/null @@ -1,83 +0,0 @@ -//Shopify POS UI Extension patterns -// Note: @shopify/pos-ui-extensions types not available in this environment -declare const extend: any; -declare const Modal: any; - -export default extend( - "pos.cart.line-item.action", - (root: any, { api, ui }: any) => { - const button = ui.createComponent("Button", { - title: "Custom Action", - onPress: async () => { - // Pattern 1: Basic modal usage - const result = await api.smartGrid.presentModal({ - kind: "confirmation", - title: "Confirm Action", - message: "Are you sure you want to proceed?", - }); - - if (result.reason === "confirm") { - // Pattern 2: Nested modal calls - const detailsResult = await api.smartGrid.presentModal({ - kind: "form", - title: "Enter Details", - fields: [ - { type: "text", label: "Name", required: true }, - { type: "number", label: "Quantity", required: true }, - ], - }); - - if (detailsResult.reason === "confirm") { - console.log("Details submitted:", detailsResult.data); - } - } - }, - }); - - root.appendChild(button); - } -); - -// Pattern 3: Modal in utility function -async function showCustomModal(api: any, product: any) { - return await api.smartGrid.presentModal({ - kind: "custom", - title: `Product: ${product.title}`, - content: product.description, - }); -} - -// Pattern 4: Conditional modal -function maybeShowModal(api: any, condition: boolean) { - if (condition) { - return api.smartGrid.presentModal({ - kind: "alert", - message: "Something happened!", - }); - } - return Promise.resolve(null); -} - -// Pattern 5: Modal with promise chain -export function handleProductAction(api: any, productId: string) { - return api.smartGrid - .presentModal({ - kind: "confirmation", - title: "Delete Product", - message: "This action cannot be undone.", - }) - .then((result: any) => { - if (result.reason === "confirm") { - return deleteProduct(productId); - } - throw new Error("Action cancelled"); - }) - .catch((error: any) => { - console.error("Product action failed:", error); - }); -} - -function deleteProduct(id: string) { - // Implementation - return Promise.resolve(); -} From 479dce70f13124d9b536fdc04d6f9a304c2d4946 Mon Sep 17 00:00:00 2001 From: Shadi Date: Thu, 18 Sep 2025 19:06:03 -0700 Subject: [PATCH 07/12] chore: updfate project dependencies and minor config updates --- codemods/pos-api-smartgrid-to-action/codemod.yaml | 2 +- package.json | 3 +++ pnpm-lock.yaml | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/codemods/pos-api-smartgrid-to-action/codemod.yaml b/codemods/pos-api-smartgrid-to-action/codemod.yaml index fce2cab..637df09 100644 --- a/codemods/pos-api-smartgrid-to-action/codemod.yaml +++ b/codemods/pos-api-smartgrid-to-action/codemod.yaml @@ -1,6 +1,6 @@ schema_version: "1.0" -name: "testing" +name: "pos-api-smartgrid-to-action" version: "0.1.0" description: "Transform legacy code patterns" author: "Shadi " diff --git a/package.json b/package.json index 5a18b94..3b82716 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ "devDependencies": { "husky": "^9.0.0", "typescript": "^5.5.4" + }, + "dependencies": { + "@codemod.com/jssg-types": "^1.0.9" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 575fa87..8f6c719 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + '@codemod.com/jssg-types': + specifier: ^1.0.9 + version: 1.0.9 devDependencies: husky: specifier: ^9.0.0 From 3e495a6aa94959d2d49d4330a9fd49512086008f Mon Sep 17 00:00:00 2001 From: Shadi Date: Thu, 18 Sep 2025 19:16:14 -0700 Subject: [PATCH 08/12] refactor(utils): replace all 'any' types and improve type safety --- .../scripts/codemod.ts | 11 ++++--- utils/ast-utils.ts | 30 ++++++++----------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts b/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts index 6b0f978..38c7abf 100644 --- a/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts +++ b/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts @@ -1,4 +1,4 @@ -import type { SgRoot, Edit } from "codemod:ast-grep"; +import type { SgRoot, Edit, SgNode } from "codemod:ast-grep"; import type TS from "codemod:ast-grep/langs/typescript"; // Utilities for API alias detection and validation @@ -87,7 +87,7 @@ async function transform(root: SgRoot): Promise { const edits: Edit[] = []; // Get all possible aliases for 'api' including destructured 'smartGrid' - const apiAliases = getApiAliases(rootNode); + const apiAliases = getApiAliases(rootNode as unknown as SgNode); // Find all 'smartGrid' property identifiers in the code const smartGridProps = rootNode @@ -99,7 +99,7 @@ async function transform(root: SgRoot): Promise { .filter((prop) => prop.text() === "smartGrid"); for (const prop of smartGridProps) { - if (!shouldTransformProperty(prop, apiAliases)) { + if (!shouldTransformProperty(prop as unknown as SgNode, apiAliases)) { continue; } @@ -114,7 +114,10 @@ async function transform(root: SgRoot): Promise { * Determine if a smartGrid property should be transformed * Only transforms api.smartGrid.presentModal() patterns */ -function shouldTransformProperty(prop: any, apiAliases: Set): boolean { +function shouldTransformProperty( + prop: SgNode, + apiAliases: Set +): boolean { // Get the parent member expression (e.g., "api.smartGrid") const memberExpr = prop.parent(); if (!memberExpr || memberExpr.kind() !== "member_expression") return false; diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts index 75b513d..a4c0656 100644 --- a/utils/ast-utils.ts +++ b/utils/ast-utils.ts @@ -1,18 +1,9 @@ -// Type definitions for AST-grep nodes (avoiding import issues) -export interface SgNode { - text(): string; - kind(): string; - parent(): SgNode | null; - field(name: string): SgNode | null; - getMatch(name: string): SgNode | null; - replace(text: string): any; - findAll(config: { rule: any }): SgNode[]; -} +import type { SgNode } from "@codemod.com/jssg-types/main"; export interface ImportInfo { source: string; specifier: string; - node: any; + node: SgNode; } /** @@ -30,7 +21,7 @@ export interface ImportInfo { * const polarisAliases = getVariableAliases(rootNode, "Polaris"); */ export function getVariableAliases( - rootNode: any, + rootNode: SgNode, sourceVar: string, destructuredProps: string[] = [] ): Set { @@ -66,7 +57,7 @@ export function getVariableAliases( * Legacy API-specific wrapper for backward compatibility * @deprecated Use getVariableAliases(rootNode, "api", ["smartGrid"]) instead */ -export function getApiAliases(rootNode: any): Set { +export function getApiAliases(rootNode: SgNode): Set { return getVariableAliases(rootNode, "api", ["smartGrid"]); } @@ -86,7 +77,7 @@ export function getApiAliases(rootNode: any): Set { * const buttonUsages = findMemberExpressions(rootNode, polarisAliases, "Button"); */ export function findMemberExpressions( - rootNode: any, + rootNode: SgNode, objectAliases: Set, property: string, method?: string @@ -147,7 +138,10 @@ export function findMemberExpressions( * // Find all Polaris imports * const polarisImports = getImports(rootNode, "@shopify/polaris"); */ -export function getImports(rootNode: any, packageName: string): ImportInfo[] { +export function getImports( + rootNode: SgNode, + packageName: string +): ImportInfo[] { const imports: ImportInfo[] = []; // Pattern 1: import specifier from "package" @@ -237,11 +231,11 @@ export function getImportSources( * const buttonImports = getNamedImports(rootNode, "@shopify/polaris", "Button"); */ export function getNamedImports( - rootNode: any, + rootNode: SgNode, packageName: string, importName: string -): any[] { - const nodes: any[] = []; +): SgNode[] { + const nodes: SgNode[] = []; // Pattern: import { importName } from "package" or import { importName, ... } from "package" const imports = rootNode.findAll({ From 12322e52baff08e12b8284db28c8f64bb09fd4f8 Mon Sep 17 00:00:00 2001 From: Shadi Date: Thu, 18 Sep 2025 19:46:18 -0700 Subject: [PATCH 09/12] refactor(utils): replace string patterns with AST linked-list traversal - Add getMemberExpressionChain() for robust member expression parsing - Add matchesMemberPattern() for clean pattern validation - Replace findMemberExpressions() with proper AST traversal approach --- utils/ast-utils.ts | 214 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 163 insertions(+), 51 deletions(-) diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts index a4c0656..1058b41 100644 --- a/utils/ast-utils.ts +++ b/utils/ast-utils.ts @@ -54,15 +54,131 @@ export function getVariableAliases( } /** - * Legacy API-specific wrapper for backward compatibility - * @deprecated Use getVariableAliases(rootNode, "api", ["smartGrid"]) instead + * Get all possible aliases for the 'api' variable including destructured 'smartGrid' + * This is a convenience wrapper for common Shopify POS API usage patterns + * @param rootNode - The root AST node to search in + * @returns Set of all aliases for the api variable + * + * @example + * const apiAliases = getApiAliases(rootNode); + * // Returns Set(["api", "smartGrid"]) if code has: const { smartGrid } = api; */ export function getApiAliases(rootNode: SgNode): Set { return getVariableAliases(rootNode, "api", ["smartGrid"]); } +/** + * Helper function to traverse a member expression chain like a linked list + * and extract the full property path + * @param node - The member expression node to traverse + * @returns Array of property names from base to tip (e.g., ["api", "smartGrid", "presentModal"]) + */ +function getMemberExpressionChain(node: SgNode): string[] { + const chain: string[] = []; + let current = node; + + // Traverse the member expression chain + while (current && current.kind() === "member_expression") { + // Get the property (right side of the dot) + const property = current.field("property"); + if (property) { + chain.unshift(property.text()); // Add to front since we're traversing backwards + } + + // Move to the object (left side of the dot) + current = current.field("object")!; + } + + // Add the base object (identifier, this, function call, etc.) + if (current) { + const baseText = current.text(); + // Handle different base types + if (baseText.endsWith("()")) { + // Function call: getApi() -> "getApi" + chain.unshift(baseText.slice(0, -2)); + } else if (baseText.endsWith("()?")) { + // Optional function call: getApi()? -> "getApi" + chain.unshift(baseText.slice(0, -3)); + } else if (baseText === "this") { + // this.api.method -> ["this", "api", "method"] + chain.unshift("this"); + } else { + // Regular identifier: api -> "api" + chain.unshift(baseText); + } + } + + return chain; +} + +/** + * Check if a member expression chain matches the expected pattern + * @param chain - Array of property names from getMemberExpressionChain + * @param objectAliases - Set of valid object aliases + * @param property - The property name to find + * @param method - Optional method name that must follow the property + * @returns boolean indicating if the chain matches + */ +function matchesMemberPattern( + chain: string[], + objectAliases: Set, + property: string, + method?: string +): boolean { + if (chain.length < 1) return false; + + // Handle direct property usage (from destructuring) + if (chain.length === 1) { + const base = chain[0]; + return objectAliases.has(base) && base === property && !method; + } + + if (chain.length === 2 && method) { + // Direct method call: smartGrid.presentModal + const [base, methodName] = chain; + return ( + objectAliases.has(base) && base === property && methodName === method + ); + } + + // Handle normal patterns: alias.property[.method] + const expectedLength = method ? 3 : 2; + if (chain.length !== expectedLength) return false; + + const [base, prop, methodName] = chain; + + // Check base object (handle this.alias pattern) + let validBase = false; + if (objectAliases.has(base)) { + validBase = true; + } else if ( + base === "this" && + chain.length > 1 && + objectAliases.has(chain[1]) + ) { + // this.alias.property pattern + validBase = true; + // Adjust indices for this.alias pattern + const adjustedProp = chain[2]; + const adjustedMethod = chain[3]; + return adjustedProp === property && (!method || adjustedMethod === method); + } else { + // Check for function calls that might return the object + const funcCallBase = base.replace(/\?\?$/, ""); // Remove optional chaining + if ( + funcCallBase.toLowerCase().includes("api") || + objectAliases.has(funcCallBase) + ) { + validBase = true; + } + } + + return validBase && prop === property && (!method || methodName === method); +} + /** * Generic utility to find member expressions with specific object and property + * Uses proper AST traversal instead of string pattern matching * @param rootNode - The root AST node to search in * @param objectAliases - Set of valid object aliases * @param property - The property name to find (e.g., "smartGrid", "Button", "Modal") @@ -81,49 +197,56 @@ export function findMemberExpressions( objectAliases: Set, property: string, method?: string -): any[] { - const usages: any[] = []; - const methodPattern = method ? `.${method}($$$ARGS)` : ""; - - for (const alias of objectAliases) { - if (alias === property) { - // Special case: direct property usage (from destructuring) - const directUsages = rootNode.findAll({ - rule: { pattern: `${alias}${methodPattern}` }, - }); - usages.push(...directUsages); - } else { - // Normal case: alias.property.method() - const memberUsages = rootNode.findAll({ - rule: { pattern: `${alias}.${property}${methodPattern}` }, - }); - usages.push(...memberUsages); - } - } +): SgNode[] { + const usages: SgNode[] = []; - // Handle this.alias patterns - const thisUsages = rootNode.findAll({ - rule: { - pattern: `this.${ - Array.from(objectAliases)[0] - }.${property}${methodPattern}`, - }, + // Find all member expressions and call expressions + const memberExpressions = rootNode.findAll({ + rule: { kind: "member_expression" }, }); - usages.push(...thisUsages); - // Handle optional chaining - const optionalUsages = rootNode.findAll({ - rule: { - pattern: `${Array.from(objectAliases)[0]}?.${property}${methodPattern}`, - }, + const callExpressions = rootNode.findAll({ + rule: { kind: "call_expression" }, }); - usages.push(...optionalUsages); - // Handle function call patterns - const functionCallUsages = rootNode.findAll({ - rule: { pattern: `$FUNC().${property}${methodPattern}` }, - }); - usages.push(...functionCallUsages); + // Check member expressions + for (const node of memberExpressions) { + const chain = getMemberExpressionChain(node); + if (matchesMemberPattern(chain, objectAliases, property, method)) { + usages.push(node); + } + } + + // Check call expressions (for method calls) + if (method) { + for (const node of callExpressions) { + const callee = node.field("function"); + if (callee && callee.kind() === "member_expression") { + const chain = getMemberExpressionChain(callee); + if (matchesMemberPattern(chain, objectAliases, property, method)) { + usages.push(node); // Return the call expression, not just the member expression + } + } + } + } + + // Handle direct identifier usage (from destructuring) + if (!method) { + const identifiers = rootNode.findAll({ + rule: { kind: "identifier" }, + }); + + for (const identifier of identifiers) { + const text = identifier.text(); + if (objectAliases.has(text) && text === property) { + // Make sure it's not part of a larger member expression + const parent = identifier.parent(); + if (parent && parent.kind() !== "member_expression") { + usages.push(identifier); + } + } + } + } return usages; } @@ -208,17 +331,6 @@ export function getImports( return imports; } -/** - * Legacy function for backward compatibility - * @deprecated Use getImports(rootNode, packageName) instead - */ -export function getImportSources( - rootNode: any, - packageName: string -): ImportInfo[] { - return getImports(rootNode, packageName); -} - /** * Get specific named imports from a package * @param rootNode - The root AST node to search in From c77f41e64e796383d16085adb037c1a47bfd826b Mon Sep 17 00:00:00 2001 From: Shadi Date: Thu, 18 Sep 2025 19:48:08 -0700 Subject: [PATCH 10/12] refactor(utils): take into account 'var' and 'let' patterns too --- utils/ast-utils.ts | 100 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 16 deletions(-) diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts index 1058b41..8c3c803 100644 --- a/utils/ast-utils.ts +++ b/utils/ast-utils.ts @@ -8,16 +8,20 @@ export interface ImportInfo { /** * Generic utility to get variable aliases for any source variable + * Supports const, let, and var declarations with various patterns * @param rootNode - The root AST node to search in * @param sourceVar - The source variable name (e.g., "api", "Polaris", "React") * @param destructuredProps - Optional array of properties that create direct aliases when destructured * @returns Set of all aliases for the source variable * * @example - * // For API aliases: const myApi = api; const { smartGrid } = api; + * // For API aliases with various declaration types: + * // const myApi = api; let anotherApi = api; var thirdApi = api; + * // const { smartGrid } = api; let { smartGrid: grid } = api; var { smartGrid: varGrid } = api; + * // const a = 1, aliasedApi = api, b = 2; * const apiAliases = getVariableAliases(rootNode, "api", ["smartGrid"]); * - * // For Polaris aliases: const MyPolaris = Polaris; + * // For Polaris aliases: const MyPolaris = Polaris; let altPolaris = Polaris; * const polarisAliases = getVariableAliases(rootNode, "Polaris"); */ export function getVariableAliases( @@ -27,26 +31,90 @@ export function getVariableAliases( ): Set { const aliases = new Set([sourceVar]); - // Pattern 1: const myVar = sourceVar - const directAliases = rootNode.findAll({ - rule: { pattern: `const $VAR = ${sourceVar}` }, - }); + // All declaration types to search for + const declarationTypes = ["const", "let", "var"]; + + // Pattern 1: [const|let|var] myVar = sourceVar + for (const declType of declarationTypes) { + const directAliases = rootNode.findAll({ + rule: { pattern: `${declType} $VAR = ${sourceVar}` }, + }); + + for (const alias of directAliases) { + const varName = alias.getMatch("VAR")?.text(); + if (varName) { + aliases.add(varName); + } + } - for (const alias of directAliases) { - const varName = alias.getMatch("VAR")?.text(); - if (varName) { - aliases.add(varName); + // Also handle multiple declarations: [const|let|var] a = x, myVar = sourceVar, c = z + const multipleDeclarations = rootNode.findAll({ + rule: { pattern: `${declType} $$$VARS` }, + }); + + for (const decl of multipleDeclarations) { + const varsText = decl.getMatch("VARS")?.text(); + if (varsText && varsText.includes(`= ${sourceVar}`)) { + // Parse multiple variable declarations + const assignments = varsText.split(","); + for (const assignment of assignments) { + const trimmed = assignment.trim(); + if (trimmed.includes(`= ${sourceVar}`)) { + const varMatch = trimmed.match(/^(\w+)\s*=/); + if (varMatch) { + aliases.add(varMatch[1]); + } + } + } + } } } - // Pattern 2: const { prop } = sourceVar (for specified destructured properties) + // Pattern 2: [const|let|var] { prop } = sourceVar (for specified destructured properties) for (const prop of destructuredProps) { - const destructuring = rootNode.findAll({ - rule: { pattern: `const { ${prop} } = ${sourceVar}` }, - }); + for (const declType of declarationTypes) { + // Simple destructuring: { prop } = sourceVar + const simpleDestructuring = rootNode.findAll({ + rule: { pattern: `${declType} { ${prop} } = ${sourceVar}` }, + }); - if (destructuring.length > 0) { - aliases.add(prop); + if (simpleDestructuring.length > 0) { + aliases.add(prop); + } + + // Renamed destructuring: { prop: alias } = sourceVar + const renamedDestructuring = rootNode.findAll({ + rule: { pattern: `${declType} { ${prop}: $ALIAS } = ${sourceVar}` }, + }); + + for (const destructure of renamedDestructuring) { + const aliasName = destructure.getMatch("ALIAS")?.text(); + if (aliasName) { + aliases.add(aliasName); + } + } + + // Mixed destructuring: { prop, prop2: alias, ... } = sourceVar + const mixedDestructuring = rootNode.findAll({ + rule: { pattern: `${declType} { $$$PROPS } = ${sourceVar}` }, + }); + + for (const destructure of mixedDestructuring) { + const propsText = destructure.getMatch("PROPS")?.text(); + if (propsText && propsText.includes(prop)) { + // Parse the destructuring to find aliases + const propPattern = new RegExp(`\\b${prop}:\\s*(\\w+)`, "g"); + let match; + while ((match = propPattern.exec(propsText)) !== null) { + aliases.add(match[1]); + } + + // Also check for simple property name + if (new RegExp(`\\b${prop}\\b(?!:)`).test(propsText)) { + aliases.add(prop); + } + } + } } } From 6c06c7231f01c9e559731763531539e7c28dc0e6 Mon Sep 17 00:00:00 2001 From: Shadi Date: Thu, 18 Sep 2025 19:51:50 -0700 Subject: [PATCH 11/12] refactor: add robust import checking system and eliminate code duplication - Add generic hasImportsMatching() function with flexible pattern matching (includes/startsWith/exact) - Add isPOSUIExtensionsFile() for comprehensive POS UI Extensions detection (old and new packages) - Remove duplicate utility functions from pos-api-smartgrid codemod - Import reusable functions from utils/ast-utils.ts instead of local duplicates --- .../scripts/codemod.ts | 96 +++------------- utils/ast-utils.ts | 107 ++++++++++++++++++ 2 files changed, 122 insertions(+), 81 deletions(-) diff --git a/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts b/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts index 38c7abf..61d85ef 100644 --- a/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts +++ b/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts @@ -1,95 +1,28 @@ import type { SgRoot, Edit, SgNode } 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 { - return getVariableAliases(rootNode, "api", ["smartGrid"]); -} - -function getVariableAliases( - rootNode: any, - sourceVar: string, - destructuredProps: string[] = [] -): Set { - const aliases = new Set([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 -): 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; -} +import { + isPOSUIExtensionsFile, + getApiAliases, + isValidObjectReference, +} from "../../../utils/ast-utils.js"; /** * Transform api.smartGrid.presentModal() calls to api.action.presentModal() * Handles various patterns including aliases, destructuring, optional chaining, etc. + * Only applies to files that import from POS UI Extensions packages. */ async function transform(root: SgRoot): Promise { const rootNode = root.root(); + + // Only transform files that import from POS UI Extensions (old or new packages) + if (!isPOSUIExtensionsFile(rootNode as unknown as SgNode)) { + return null; + } + const edits: Edit[] = []; - // Get all possible aliases for 'api' including destructured 'smartGrid' const apiAliases = getApiAliases(rootNode as unknown as SgNode); - // Find all 'smartGrid' property identifiers in the code const smartGridProps = rootNode .findAll({ rule: { @@ -103,7 +36,6 @@ async function transform(root: SgRoot): Promise { continue; } - // Replace "smartGrid" with "action" edits.push(prop.replace("action")); } @@ -113,6 +45,8 @@ async function transform(root: SgRoot): Promise { /** * Determine if a smartGrid property should be transformed * Only transforms api.smartGrid.presentModal() patterns + * @param prop - AST node + * @param apiAliases - Set of valid API aliases */ function shouldTransformProperty( prop: SgNode, @@ -141,7 +75,7 @@ function shouldTransformProperty( // Only transform if this is a function call (not just property access) const callExpr = outerMemberExpr.parent(); - return callExpr && callExpr.kind() === "call_expression"; + return callExpr ? callExpr.kind() === "call_expression" : false; } export default transform; diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts index 8c3c803..1ab886f 100644 --- a/utils/ast-utils.ts +++ b/utils/ast-utils.ts @@ -485,3 +485,110 @@ export function isValidObjectReference( return false; } + +/** + * Generic function to check if a file imports from packages matching specific patterns + * This is the foundation for all codemod package filtering + * @param rootNode - The root AST node to search in + * @param patterns - Array of package name patterns to match + * @param matchType - How to match patterns: 'includes' (default), 'startsWith', or 'exact' + * @returns boolean indicating if any matching imports are found + * + * @example + * // Check for Shopify POS imports + * const isShopifyFile = hasImportsMatching(rootNode, [ + * "@shopify/pos", + * "@shopify/point-of-sale", + * "pos-ui-extensions" + * ]); + * + * // Check for React imports with exact matching + * const isReactFile = hasImportsMatching(rootNode, ["react"], "exact"); + * + * // Check for any GraphQL-related imports + * const hasGraphQL = hasImportsMatching(rootNode, ["graphql", "apollo"], "includes"); + */ +export function hasImportsMatching( + rootNode: SgNode, + patterns: string[], + matchType: "includes" | "startsWith" | "exact" = "includes" +): boolean { + const importStatements = rootNode.findAll({ + rule: { pattern: `import $IMPORT from "$SOURCE"` }, + }); + + const namedImportStatements = rootNode.findAll({ + rule: { pattern: `import { $$$SPECS } from "$SOURCE"` }, + }); + + const namespaceImportStatements = rootNode.findAll({ + rule: { pattern: `import * as $ALIAS from "$SOURCE"` }, + }); + + const sideEffectImportStatements = rootNode.findAll({ + rule: { pattern: `import "$SOURCE"` }, + }); + + const allImportNodes = [ + ...importStatements, + ...namedImportStatements, + ...namespaceImportStatements, + ...sideEffectImportStatements, + ]; + + for (const importNode of allImportNodes) { + const source = importNode.getMatch("SOURCE"); + if (source) { + const sourcePath = source.text(); + + for (const pattern of patterns) { + let matches = false; + + switch (matchType) { + case "exact": + matches = sourcePath === pattern; + break; + case "startsWith": + matches = sourcePath.startsWith(pattern); + break; + case "includes": + default: + matches = + sourcePath.includes(pattern) || sourcePath.startsWith(pattern); + break; + } + + if (matches) { + return true; + } + } + } + } + + return false; +} + +/** + * Check if a file imports from POS UI Extensions packages (old or new) + * @param rootNode - The root AST node to search in + * @returns boolean indicating if this file uses POS UI Extensions + * + * @example + * if (isPOSUIExtensionsFile(rootNode)) { + * // Apply POS UI Extensions transformations + * } + */ +export function isPOSUIExtensionsFile(rootNode: SgNode): boolean { + const posUIExtensionPatterns = [ + // New packages (post-migration) + "@shopify/ui-extensions/point-of-sale", + "@shopify/ui-extensions-react/point-of-sale", + // Old packages (pre-migration) + "@shopify/pos-ui-extensions", + "@shopify/retail-ui-extensions", + "@shopify/retail-ui-extensions-react", + "pos-ui-extensions", + ]; + + return hasImportsMatching(rootNode, posUIExtensionPatterns, "startsWith"); +} From 10b07093234410b5c41c95310628f92fd38d4994 Mon Sep 17 00:00:00 2001 From: Shadi Date: Thu, 18 Sep 2025 19:54:06 -0700 Subject: [PATCH 12/12] chore: update pnpm lockfile --- pnpm-lock.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f6c719..37e1299 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: packages: + '@codemod.com/jssg-types@1.0.9': + resolution: {integrity: sha512-j+O2nvYnBcmBy0mG5bSmBD7Cn7q3fgp4tI6aqIuF7pVu7j8Dgs45Ohgkpzx9mwqcmAE7vC9CEc8zQZOfwfICyw==} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -33,6 +36,8 @@ packages: snapshots: + '@codemod.com/jssg-types@1.0.9': {} + husky@9.1.7: {} typescript@5.9.2: {}