Skip to content

Conversation

ShadiBitaraf
Copy link
Contributor

Overview

Adds a codemod to migrate Shopify POS extensions from the deprecated
api.smartGrid.presentModal()api.action.presentModal().

Key Features

  • Transforms only presentModal calls, preserving other smartGrid methods
  • Handles aliases, destructuring, optional chaining, and class-based usage
  • AST-based, syntax-aware transformations with validation
  • Backed by new utils library (utils/ast-utils.ts)

Impact

  • Automates migration of POS extensions with safe, tested transformations
  • Reduces manual effort and risk of migration errors
  • Provides reusable utilities for future codemods

Files Added

  • utils/ast-utils.ts
  • codemods/pos-api-smartgrid-to-action/ (implementation, tests, docs)

- 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
- 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
- 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
- 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
Comment on lines 2 to 10
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[];
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can import SgNode from @codemod.com/jssg-types/main

export interface ImportInfo {
source: string;
specifier: string;
node: any;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should refrain from using any types. This should probably be a SgNode<TS, "import_statement">?

Comment on lines 32 to 63
export function getVariableAliases(
rootNode: any,
sourceVar: string,
destructuredProps: string[] = []
): Set<string> {
const aliases = new Set<string>([sourceVar]);

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

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

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

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

return aliases;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not taking into account var and let kinds i think

property: string,
method?: string
): any[] {
const usages: any[] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no explicit anys

Comment on lines 95 to 137
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good util function to have, but I'd just traverse the member expression like a linked list instead of using string ops. The current code is not readable and hard to follow (and honestly I can't really trust if it covers all the edge cases as it's heavily relying on ast-grep pattern matchers)

* // Find all Polaris imports
* const polarisImports = getImports(rootNode, "@shopify/polaris");
*/
export function getImports(rootNode: any, packageName: string): ImportInfo[] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no explicit any

packageName: string,
importName: string
): any[] {
const nodes: any[] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explicit any ❌

* const buttonImports = getNamedImports(rootNode, "@shopify/polaris", "Button");
*/
export function getNamedImports(
rootNode: any,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any

* @deprecated Use getImports(rootNode, packageName) instead
*/
export function getImportSources(
rootNode: any,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any

- Add getMemberExpressionChain() for robust member expression parsing
- Add matchesMemberPattern() for clean pattern validation
- Replace findMemberExpressions() with proper AST traversal approach
…ation

- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants