Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AG-12549 Selection API codemods #87

Merged
merged 16 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 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