Skip to content

Commit

Permalink
AG-12549 Selection API codemods (#87)
Browse files Browse the repository at this point in the history
* fix create-transform task

* add grid options codemod for v32.2

* fix simple js test

* wip

* codemods for selection properties

* add tests

* warn on unimplemented transformations

* rename scenario

* add comments, tidy, reduce type casting

* uncommit

* missing manifest

* update test

* Increment package version

* test case for deprecated colDef props

* pre-compute the value and exit early if no valid transformation
  • Loading branch information
eliasmalik authored Sep 19, 2024
1 parent 495eabc commit afdbb99
Show file tree
Hide file tree
Showing 76 changed files with 1,463 additions and 25 deletions.
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@ag-grid-devtools/cli",
"version": "32.0.7",
"version": "32.2.0",
"license": "MIT",
"description": "AG Grid developer toolkit",
"author": "AG Grid <[email protected]>",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/codemods/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, test } from 'vitest';

import * as lib from './lib';

const versions: Array<string> = ['31.0.0', '31.1.0', '31.2.0', '31.3.0', '32.0.0'];
const versions: Array<string> = ['31.0.0', '31.1.0', '31.2.0', '31.3.0', '32.0.0', '32.2.0'];

test('module exports', () => {
expect({ ...lib }).toEqual({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"name": "Transform Grid API methods",
"description": "Transform deprecated Grid API method invocations",
"template": "../../../templates/plugin-transform-grid-api-methods"
"description": "Transform deprecated Grid API method invocations"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"name": "Transform Grid options",
"description": "Transform deprecated Grid options",
"template": "../../../templates/plugin-transform-grid-options"
"description": "Transform deprecated Grid options"
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ type JSXIdentifier = Types.JSXIdentifier;
type JSXNamespacedName = Types.JSXNamespacedName;
type Literal = Types.Literal;
type MemberExpression = Types.MemberExpression;
type OptionalMemberExpression = Types.OptionalMemberExpression;
type ObjectExpression = Types.ObjectExpression;
type ObjectMethod = Types.ObjectMethod;
type ObjectProperty = Types.ObjectProperty;
Expand Down Expand Up @@ -727,6 +726,229 @@ export function migrateProperty<S extends AstTransformContext<AstCliContext>>(
return transformer;
}

/**
* Migrate a property into a nested object. For example `gridOptions.rowSelection` -> `gridOptions.selection.mode`.
*
* If the target object doesn't exist, it will be created.
*
* Note that a lot of the early returns in the transformers are to do with type narrowing; we don't expect those code paths
* to be triggered normally.
*
* @param path Ordered field names specifying the path in the target object
* @param transform Transformation to apply to the original value
* @param deprecationWarning Deprecation warning to print for unsupported transformations (e.g. Angular)
* @returns Object property transformer
*/
export function migrateDeepProperty<S extends AstTransformContext<AstCliContext>>(
path: string[],
transform: ObjectPropertyValueTransformer<S>,
deprecationWarning?: string,
): ObjectPropertyTransformer<S> {
if (path.length === 1) {
return migrateProperty(path[0], transform);
}

const transformer: ObjectPropertyTransformer<S> = {
init(node, context) {
if (node.shouldSkip) return;
node.skip();

if (!node.parentPath.isObjectExpression()) return;

// Start off at the root node, where the target object should be defined
let rootNode = node.parentPath;

const value = node.get('value');
if (Array.isArray(value) || !value.isExpression()) return;
const accessor = createStaticPropertyKey(t.identifier(path[path.length - 1]), false);
const updatedValue = transform.property(value, accessor, context);
if (updatedValue == null) {
deprecationWarning && context.opts.warn(node, deprecationWarning);
return;
}

// Step through the target path, either finding an existing field by that name,
// or creating an object property if one doesn't exist
for (let i = 0; i < path.length; i++) {
const part = path[i];
const rootAccessor = { key: t.identifier(part), computed: false };
let initializer = findSiblingPropertyInitializer(rootNode, rootAccessor);
if (!initializer) {
initializer = createSiblingPropertyInitializer(rootNode, rootAccessor);
}
if (!initializer) return;
const newObj = initializer.get('value');
if (!newObj.isObjectExpression()) return;
rootNode = newObj;

// On the final path part, apply the transformation and set the value
if (i === path.length - 1) {
rewriteObjectPropertyInitializer(initializer, rootAccessor, updatedValue);
}
}

node.remove();
},

get(node, context) {
if (node.shouldSkip) return;
node.skip();

deprecationWarning && context.opts.warn(node, deprecationWarning);
},

set(node, context) {
if (node.shouldSkip) return;
node.skip();

deprecationWarning && context.opts.warn(node, deprecationWarning);
},

angularAttribute(attributeNode, component, element, context) {
deprecationWarning && context.opts.warn(null, deprecationWarning);
},

jsxAttribute(node, element, context) {
if (node.shouldSkip) return;
node.skip();

// Parent should be the JSX element
if (!node.parentPath.isJSXOpeningElement()) return;
const root = node.parentPath;

// Compute the transformed value of the property ahead of time
let value: NodePath<Expression | t.JSXExpressionContainer | null | undefined> =
node.get('value');
// A null value for the JSXAttribute is an implicit truthy value
// (e.g. <Component foo />)
if (isNullNodePath(value)) {
const [transformed] = value.replaceWith(t.jsxExpressionContainer(t.booleanLiteral(true)));
value = transformed;
}
// When getting the value to set at the inner-most level of the object,
// we'll need to extract it from the expression container
if (value.isJSXExpressionContainer()) {
const innerExpression = value.get('expression');
// Shouldn't be possible to encounter an empty expression here
if (innerExpression.isJSXEmptyExpression()) return;
value = innerExpression as NodePath<Expression>;
}
// At this point, after the above clauses, we know `value` can only be `NodePath<Expression>`
let updatedValue = transform.jsxAttribute(
value as NodePath<Expression>,
element,
node,
context,
);
if (!updatedValue || updatedValue === true || t.isJSXEmptyExpression(updatedValue)) {
deprecationWarning && context.opts.warn(node, deprecationWarning);
return;
}

// Find or create the root attribute of the target object, injecting
// an empty object expression into the expression container
let rootSibling = root
.get('attributes')
.find(
(att): att is NodePath<JSXAttribute> =>
att.isJSXAttribute() && att.get('name').node.name === path[0],
);
if (!rootSibling) {
rootSibling = createJSXSiblingAttribute(root, path[0]);
}
if (!rootSibling) return;

// Fish out the reference to the object expression
const jsxExpressionContainer = rootSibling?.get('value');
if (!jsxExpressionContainer?.isJSXExpressionContainer()) return;
const objExp = jsxExpressionContainer.get('expression');
if (!objExp.isObjectExpression()) return;

// This loop is doing largely the same thing as the loop in the `.init` transformer:
// stepping through the path, either finding or creating the target field and setting the
// transformed value on the final step
let rootNode = objExp;
for (let i = 1; i < path.length; i++) {
const part = path[i];
const accessor = { key: t.identifier(part), computed: false };
let initializer = findSiblingPropertyInitializer(rootNode, accessor);
if (!initializer) {
initializer = createSiblingPropertyInitializer(rootNode, accessor);
}
if (!initializer) return;
const newObj = initializer.get('value');
if (!newObj.isObjectExpression()) return;
rootNode = newObj;

// On the final path part, apply the transformation and set the value
if (i === path.length - 1) {
rewriteObjectPropertyInitializer(initializer, accessor, updatedValue);
}
}

node.remove();
},

vueAttribute(templateNode, component, element, context) {
deprecationWarning && context.opts.warn(null, deprecationWarning);
},
};

return transformer;
}

function isNullNodePath<T>(x: NodePath<T | null | undefined>): x is NodePath<null | undefined> {
return x.node == null;
}

function createJSXSiblingAttribute(
root: NodePath<t.JSXOpeningElement>,
name: string,
): NodePath<JSXAttribute> | undefined {
const newAttribute = t.jsxAttribute(
t.jsxIdentifier(name),
t.jsxExpressionContainer(t.objectExpression([])),
);
const [transformed] = root.replaceWith(
t.jSXOpeningElement(root.get('name').node, root.node.attributes.concat(newAttribute), true),
);

const wrappedNewAttribute = transformed
.get('attributes')
.find(
(attr): attr is NodePath<JSXAttribute> =>
attr.isJSXAttribute() && attr.get('name').node.name === name,
);

return wrappedNewAttribute;
}

function createSiblingPropertyInitializer(
objExp: NodePath<ObjectExpression>,
accessor: PropertyAccessor,
) {
const prop = t.objectProperty(accessor.key, t.objectExpression([]));
const [newPath] = objExp.replaceWith(t.objectExpression(objExp.node.properties.concat(prop)));
return newPath
.get('properties')
.find(
(p): p is NodePath<ObjectProperty> => p.isObjectProperty() && p.node.key === accessor.key,
);
}

function findSiblingPropertyInitializer(
objExp: NodePath<ObjectExpression>,
accessor: PropertyAccessor,
): NodePath<t.ObjectProperty> | undefined {
return objExp
.get('properties')
.filter((p): p is NodePath<t.ObjectProperty> => t.isObjectProperty(p.node))
.find((p) => {
const existingAccessor = parseObjectPropertyInitializerAccessor(p);
return existingAccessor ? arePropertyAccessorsEqual(accessor, existingAccessor) : false;
});
}

export function removeProperty(
deprecationWarning: string,
): ObjectPropertyTransformer<AstTransformContext<AstCliContext>> {
Expand Down Expand Up @@ -981,8 +1203,7 @@ function getPropertyInitializerValue(
): NodePath<ObjectPropertyValueNode> | null {
if (property.isObjectProperty()) {
const value = property.get('value');
if (value.isExpression()) return value;
return null;
return value.isExpression() ? value : null;
} else if (property.isObjectMethod()) {
return property;
} else {
Expand All @@ -994,13 +1215,10 @@ function renameObjectProperty(
property: NodePath<ObjectPropertyNode>,
targetAccessor: PropertyAccessor,
): NodePath<ObjectPropertyNode> {
if (
property.node.key === targetAccessor.key &&
property.node.computed === targetAccessor.computed
) {
const { node } = property;
if (node.key === targetAccessor.key && node.computed === targetAccessor.computed) {
return property;
}
const { node } = property;
const value = t.isObjectMethod(node) ? node : t.isExpression(node.value) ? node.value : null;
if (!value) return property;
return rewriteObjectPropertyInitializer(property, targetAccessor, value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
transformOptionalValue,
transformPropertyValue,
type CodemodObjectPropertyReplacement,
getDeprecationMessage,
} from '../../plugins/transform-grid-options/transform-grid-options';

const MIGRATION_URL = 'https://ag-grid.com/javascript-data-grid/upgrading-to-ag-grid-31-2/';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# `transform-grid-options-v32-2`

> _Transform deprecated Grid options_
See the [`transform-grid-options`](../../plugins/transform-grid-options/) plugin for usage instructions.

## Common tasks

### Add a test case

Create a new unit test scenario for this transform:

```
pnpm run task:create-test --type transform --target transform-grid-options-v32-2
```

### Add a new rule

Replacement rules are specified in [`replacements.ts`](./replacements.ts)

### Add to a codemod release

Add this source code transformation to a codemod release:

```
pnpm run task:include-transform --transform transform-grid-options-v32-2
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// @ts-nocheck
import { AdvancedFilterModel, ColDef, ColGroupDef, GridReadyEvent } from '@ag-grid-community/core';
import { AgGridAngular } from '@ag-grid-community/angular';
import { HttpClient } from '@angular/common/http';
import { Component, ViewChild } from '@angular/core';
import { IOlympicData } from './interfaces';

@Component({
selector: 'my-app',
template: `<div>
<ag-grid-angular
[columnDefs]="columnDefs"
[rowData]="rowData"
[rowSelection]="single"
[suppressRowClickSelection]="true"
[suppressRowDeselection]="true"
[isRowSelectable]="true"
[rowMultiSelectWithClick]="true"
[groupSelectsChildren]="true"
[groupSelectsFiltered]="true"
[enableRangeSelection]="true"
[suppressMultiRangeSelection]="true"
[suppressClearOnFillReduction]="true"
[enableRangeHandle]="true"
[enableFillHandle]="true"
[fillHandleDirection]="true"
[fillOperation]="fillOperation($params)"
[suppressCopyRowsToClipboard]="true"
[suppressCopySingleCellRanges]="true"
(gridReady)="onGridReady($event)"
></ag-grid-angular>
</div>`,
})
export class AppComponent {
@ViewChild(AgGridAngular) private grid!: AgGridAngular;
public columnDefs: (ColDef | ColGroupDef)[] = [];
public rowData!: IOlympicData[];

constructor(private http: HttpClient) {
}

onGridReady(params: GridReadyEvent<IOlympicData>) {
this.http
.get<IOlympicData[]>('https://www.ag-grid.com/example-assets/olympic-winners.json')
.subscribe((data) => {
this.rowData = data;
console.log("Hello, world!");
});
}
}
Loading

0 comments on commit afdbb99

Please sign in to comment.