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/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/codemod.yaml b/codemods/pos-api-smartgrid-to-action/codemod.yaml new file mode 100644 index 0000000..637df09 --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/codemod.yaml @@ -0,0 +1,18 @@ +schema_version: "1.0" + +name: "pos-api-smartgrid-to-action" +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..61d85ef --- /dev/null +++ b/codemods/pos-api-smartgrid-to-action/scripts/codemod.ts @@ -0,0 +1,81 @@ +import type { SgRoot, Edit, SgNode } from "codemod:ast-grep"; +import type TS from "codemod:ast-grep/langs/typescript"; +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[] = []; + + const apiAliases = getApiAliases(rootNode as unknown as SgNode); + + const smartGridProps = rootNode + .findAll({ + rule: { + kind: "property_identifier", + }, + }) + .filter((prop) => prop.text() === "smartGrid"); + + for (const prop of smartGridProps) { + if (!shouldTransformProperty(prop as unknown as SgNode, apiAliases)) { + continue; + } + + 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 + * @param prop - AST node + * @param apiAliases - Set of valid API aliases + */ +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; + + // 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" : false; +} + +export default transform; 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; 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" 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..37e1299 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 @@ -17,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'} @@ -29,6 +36,8 @@ packages: snapshots: + '@codemod.com/jssg-types@1.0.9': {} + husky@9.1.7: {} typescript@5.9.2: {} diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts new file mode 100644 index 0000000..1ab886f --- /dev/null +++ b/utils/ast-utils.ts @@ -0,0 +1,594 @@ +import type { SgNode } from "@codemod.com/jssg-types/main"; + +export interface ImportInfo { + source: string; + specifier: string; + node: SgNode; +} + +/** + * 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 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; let altPolaris = Polaris; + * const polarisAliases = getVariableAliases(rootNode, "Polaris"); + */ +export function getVariableAliases( + rootNode: SgNode, + sourceVar: string, + destructuredProps: string[] = [] +): Set { + const aliases = new Set([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); + } + } + + // 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|let|var] { prop } = sourceVar (for specified destructured properties) + for (const prop of destructuredProps) { + for (const declType of declarationTypes) { + // Simple destructuring: { prop } = sourceVar + const simpleDestructuring = rootNode.findAll({ + rule: { pattern: `${declType} { ${prop} } = ${sourceVar}` }, + }); + + 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); + } + } + } + } + } + + return aliases; +} + +/** + * 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") + * @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: SgNode, + objectAliases: Set, + property: string, + method?: string +): SgNode[] { + const usages: SgNode[] = []; + + // Find all member expressions and call expressions + const memberExpressions = rootNode.findAll({ + rule: { kind: "member_expression" }, + }); + + const callExpressions = rootNode.findAll({ + rule: { kind: "call_expression" }, + }); + + // 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; +} + +/** + * 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: SgNode, + 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; +} + +/** + * 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: SgNode, + packageName: string, + importName: string +): SgNode[] { + const nodes: SgNode[] = []; + + // 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; +} + +/** + * 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"); +}