chore: implement v5 codemods

This commit is contained in:
Alexandre Bodin 2024-03-27 19:00:00 +01:00
parent a063b749b6
commit c81b5f327c
3 changed files with 717 additions and 1 deletions

View File

@ -0,0 +1,368 @@
import type { Transform, JSCodeshift, ASTPath, ObjectExpression } from 'jscodeshift';
/*
This codemod transforms entity service calls to match the new document service interface.
It supports all kind of argument parsing, including spread elements & deeply nested objects.
Here is a list of scenarios this was tested against
const uid = "api::xxx.xxx";
const entityId = 1;
Case: basic call
strapi.entityService.findOne(uid, entityId, {
fields: ["id", "name", "description"],
populate: ["author", "comments"],
publicationState: "preview",
});
Case: using a variable declared somewhere else
const objectParam_2 = {
fields: ["id", "name", "description"],
populate: ["author", "comments"],
publicationState: "preview",
};
strapi.entityService.findOne(uid, entityId, objectParam_2);
Case: using a variable declared somewhere else with a spread element
const objectParam_3 = {
fields: ["id", "name", "description"],
populate: ["author", "comments"],
publicationState: "preview",
};
strapi.entityService.findOne(uid, entityId, {
...objectParam_3,
});
Case: using a variable declared somewhere else with a spread element and overwritten properties
const objectParam_4_1 = {
fields: ["id", "name", "description"],
populate: ["author", "comments"],
publicationState: "preview",
};
const objectParam_4 = {
publicationState: "live",
...objectParam_4_1,
};
strapi.entityService.findOne(uid, entityId, {
...objectParam_4,
});
Case: using a variable declared somewhere else with a spread array element while that need its 1st element to be moved
const objectParam_5 = [
uid,
entityId,
{
fields: ["id", "name", "description"],
populate: ["author", "comments"],
publicationState: "preview",
},
];
strapi.entityService.findOne(...objectParam_5);
Case: using a variable declared somewhere else with a partial spread array
const objectParam_6 = [
entityId,
{
fields: ["id", "name", "description"],
populate: ["author", "comments"],
publicationState: "preview",
},
];
strapi.entityService.findOne(uid, ...objectParam_6);
Case: using a variable declared somewhere else with a partial & nested spread arrays
const objectParam_7_1 = [
{
fields: ["id", "name", "description"],
populate: ["author", "comments"],
publicationState: "preview",
},
];
const objectParam_7 = [entityId, ...objectParam_7_1];
strapi.entityService.findOne(uid, ...objectParam_7);
Case: using a variable declared somewhere else with a partial & nested spread arrays & objects
const objectParam_8_1 = {
publicationState: "preview",
};
const objectParam_8 = [
entityId,
{
fields: ["id", "name", "description"],
populate: ["author", "comments"],
...objectParam_8_1,
},
];
strapi.entityService.findOne(uid, ...objectParam_8);
Case: some sort of mix of all the above
const objectParam_9_1 = {
publicationState: "preview",
};
const objectParam_9 = {
fields: ["id", "name", "description"],
populate: ["author", "comments"],
...objectParam_9_1,
};
strapi.entityService.findOne(uid, ...[entityId, [objectParam_9]]);
Case: even more complex
const objectParam_10_1 = {
publicationState: "preview",
};
const objectParam_10_2 = [uid, ...[12], ...[objectParam_10_1]];
const objectParam_10 = [...objectParam_10_2];
strapi.entityService.findOne(...[...objectParam_10]);
*/
const transformDeclaration = (path: ASTPath<any>, name: any, j: JSCodeshift) => {
const declaration = findClosesDeclaration(path, name, j);
if (!declaration) {
return;
}
transformElement(path, declaration.init, j);
};
const transformElement = (path: ASTPath<any>, element: any, j: JSCodeshift) => {
switch (true) {
case j.ObjectExpression.check(element): {
transformObjectParam(path, element, j);
break;
}
case j.Identifier.check(element): {
transformDeclaration(path, element.name, j);
break;
}
case j.SpreadElement.check(element): {
transformElement(path, element.argument, j);
break;
}
case j.ArrayExpression.check(element): {
element.elements.forEach((element) => {
transformElement(path, element, j);
});
break;
}
default: {
break;
}
}
};
const transformObjectParam = (path: ASTPath<any>, expression: ObjectExpression, j: JSCodeshift) => {
expression.properties.forEach((prop) => {
switch (true) {
case j.ObjectProperty.check(prop): {
if (!j.Identifier.check(prop.key) && !j.Literal.check(prop.key)) {
return;
}
if (j.Identifier.check(prop.key) && prop.key.name !== 'publicationState') {
return;
}
if (j.Literal.check(prop.key) && prop.key.value !== 'publicationState') {
return;
}
if (j.Identifier.check(prop.key) && prop.key.name === 'publicationState') {
if (!prop.computed && !prop.shorthand) {
prop.key.name = 'status';
}
if (prop.shorthand && !prop.computed) {
prop.shorthand = false;
prop.key = j.identifier('status');
prop.value = j.identifier('publicationState');
}
} else if (j.Literal.check(prop.key) && prop.key.value === 'publicationState') {
prop.key.value = 'status';
}
switch (true) {
case j.Literal.check(prop.value): {
prop.value = prop.value.value === 'live' ? j.literal('published') : j.literal('draft');
break;
}
case j.Identifier.check(prop.value): {
const declaration = findClosesDeclaration(path, prop.value.name, j);
if (!declaration) {
return;
}
if (j.Literal.check(declaration.init)) {
declaration.init =
declaration.init.value === 'live' ? j.literal('published') : j.literal('draft');
}
break;
}
default: {
break;
}
}
break;
}
case j.SpreadElement.check(prop): {
transformElement(path, prop.argument, j);
break;
}
default: {
break;
}
}
});
};
const findClosesDeclaration = (path: ASTPath<any>, name: string, j) => {
// find Identifier declaration
const scope = path.scope.lookup(name);
if (!scope) {
return;
}
return j(scope.path)
.find(j.VariableDeclarator, { id: { type: 'Identifier', name } })
.nodes()[0];
};
const transform: Transform = (file, api) => {
const j = api.jscodeshift;
const root = j(file.source);
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
object: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'strapi',
},
property: {
type: 'Identifier',
name: 'entityService',
},
},
},
})
.replaceWith((path) => {
if (!j.MemberExpression.check(path.value.callee)) {
return;
}
const args = path.value.arguments;
if (args.length === 0) {
// we don't know how to transform this
return;
}
type Args = typeof path.value.arguments;
function resolveArgs(args: Args): Args {
return args.flatMap((arg: Args[number]) => {
switch (true) {
case j.Identifier.check(arg):
case j.Literal.check(arg): {
return arg;
}
case j.SpreadElement.check(arg): {
switch (true) {
case j.Identifier.check(arg.argument): {
const identifier = arg.argument;
const declaration = findClosesDeclaration(path, identifier.name, j);
if (!declaration) {
return arg;
}
switch (true) {
case j.ArrayExpression.check(declaration.init): {
return resolveArgs(declaration.init.elements);
}
default:
return arg;
}
}
case j.ArrayExpression.check(arg.argument): {
return resolveArgs(arg.argument.elements as Args);
}
default: {
return arg;
}
}
}
default: {
return arg;
}
}
});
}
const resolvedArgs = resolveArgs(args);
const [docUID, ...rest] = resolvedArgs;
path.value.arguments.forEach((arg) => {
transformElement(path, arg, j);
});
return j.callExpression(
j.memberExpression(
j.callExpression(j.memberExpression(j.identifier('strapi'), j.identifier('documents')), [
docUID,
]),
path.value.callee.property
),
rest
);
});
return root.toSource();
};
export const parser = 'tsx';
export default transform;

View File

@ -1,11 +1,44 @@
import { Transform, JSCodeshift, Collection } from 'jscodeshift';
/*
This codemod transforms @strapi/strapi imports to use the new public interface.
ESM
Before:
import strapi from '@strapi/strapi';
strapi();
After:
import { createStrapi } from '@strapi/strapi'; // keeps the default import
createStrapi();
---
Common JS
Before:
const strapi = require('@strapi/strapi');
strapi();
After:
const strapi = require('@strapi/strapi');
strapi.createStrapi();
*/
const transformStrapiImport = (root: Collection, j: JSCodeshift) => {
root.find(j.ImportDefaultSpecifier).forEach((path) => {
if (path.parent.value.source.value === '@strapi/strapi') {
const newSpecifiers = path.parent.value.specifiers.filter(
(specifier) => specifier.type !== 'ImportDefaultSpecifier'
);
j(path.parent).replaceWith(
j.importDeclaration(
[...path.parent.value.specifiers, j.importSpecifier(j.identifier('createStrapi'))],
[...newSpecifiers, j.importSpecifier(j.identifier('createStrapi'))],
j.literal('@strapi/strapi')
)
);

View File

@ -0,0 +1,315 @@
import { Transform, JSCodeshift, Collection } from 'jscodeshift';
/*
This codemod transforms @strapi/utils imports to change method calls to math the new public interface.
It will also warn about removed functions to avoid breaking user code.
ESM
Before:
import * as utils from '@strapi/utils';
utils.nameToSlug();
After:
import { strings } from '@strapi/utils';
strings.nameToSlug();
---
ESM
Before:
import { nameToSlug } from '@strapi/utils';
nameToSlug();
After:
import { strings } from '@strapi/utils';
strings.nameToSlug();
---
Common JS
Before:
const utils = require('@strapi/utils');
utils.nameToSlug();
After:
const { strings } = require('@strapi/utils');
strings.nameToSlug();
---
Common JS
Before:
const { nameToSlug } = require('@strapi/utils');
nameToSlug();
After:
const { strings } = require('@strapi/utils');
strings.nameToSlug();
*/
const changes = {
strings: {
nameToSlug: 'nameToSlug',
nameToCollectionName: 'nameToCollectionName',
stringEquals: 'isEqual',
isCamelCase: 'isCamelCase',
isKebabCase: 'isKebabCase',
toKebabCase: 'toKebabCase',
toRegressedEnumValue: 'toRegressedEnumValue',
startsWithANumber: 'startsWithANumber',
joinBy: 'joinBy',
},
arrays: {
stringIncludes: 'includesString',
},
objects: {
keysDeep: 'keysDeep',
},
dates: {
generateTimestampCode: 'timestampCode',
},
async: {
pipeAsync: 'pipe',
mapAsync: 'map',
reduceAsync: 'reduce',
},
};
const removed = [
'getCommonBeginning',
'templateConfiguration',
'removeUndefined',
'getConfigUrls',
'getAbsoluteAdminUrl',
'getAbsoluteServerUrl',
'forEachAsync',
];
const transformImports = (root: Collection, j: JSCodeshift) => {
root
.find(j.ImportDeclaration, {
source: { value: '@strapi/utils' },
})
.forEach((path) => {
path.value.specifiers?.forEach((specifier) => {
if (!j.ImportSpecifier.check(specifier)) {
return false;
}
if (removed.includes(specifier.imported.name)) {
console.warn(
`Function "${specifier.imported.name}" as removed. You will have to remove it from your code.`
);
return false;
}
});
for (const primitive of Object.keys(changes)) {
const functions = Object.keys(changes[primitive]);
const specifiersToRefactor = path.value.specifiers?.filter((specifier) => {
return j.ImportSpecifier.check(specifier) && functions.includes(specifier.imported.name);
});
if (specifiersToRefactor?.length > 0) {
path.value.specifiers?.unshift(j.importSpecifier(j.identifier(primitive)));
specifiersToRefactor.forEach((specifier) => {
const index = path.value.specifiers?.indexOf(specifier);
path.value.specifiers?.splice(index, 1);
});
}
}
if (path.value.specifiers?.length === 0) {
j(path).remove();
}
});
root.find(j.ImportNamespaceSpecifier).forEach((specifierPath) => {
if (specifierPath.parent.value.source.value === '@strapi/utils') {
for (const primitive of Object.keys(changes)) {
const functions = Object.keys(changes[primitive]);
functions.forEach((funcName) => {
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
property: {
type: 'Identifier',
name: funcName,
},
object: {
type: 'Identifier',
name: specifierPath.value.local.name,
},
},
})
.replaceWith((path) => {
return j.callExpression(
j.memberExpression(
j.memberExpression(
j.identifier(specifierPath.value.local.name),
j.identifier(primitive)
),
j.identifier(changes[primitive][funcName])
),
path.value.arguments
);
});
});
}
}
});
root
.find(j.VariableDeclarator, {
init: {
callee: {
name: 'require',
},
arguments: [{ value: '@strapi/utils' }],
},
})
.forEach((path) => {
// destrucured require
if (j.ObjectPattern.check(path.value.id)) {
const properties = path.value.id.properties;
properties?.forEach((property) => {
if (!j.ObjectProperty.check(property) || !j.Identifier.check(property.value)) {
return false;
}
if (removed.includes(property.value.name)) {
console.warn(
`Function "${property.value.name}" as removed. You will have to remove it from your code.`
);
return false;
}
});
for (const primitive of Object.keys(changes)) {
const functions = Object.keys(changes[primitive]);
const propertiesToRefactor = properties?.filter((property) => {
return (
j.ObjectProperty.check(property) &&
j.Identifier.check(property.value) &&
functions.includes(property.value.name)
);
});
if (propertiesToRefactor?.length > 0) {
const identifier = j.identifier(primitive);
properties?.unshift(
j.objectProperty.from({
key: identifier,
value: identifier,
shorthand: true,
})
);
propertiesToRefactor.forEach((property) => {
const index = properties?.indexOf(property);
properties?.splice(index, 1);
});
}
}
if (path.value.id.properties?.length === 0) {
j(path).remove();
}
}
// namespace require
if (path.value.id.type === 'Identifier') {
const identifier = path.value.id.name;
for (const primitive of Object.keys(changes)) {
const functions = Object.keys(changes[primitive]);
functions.forEach((funcName) => {
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
property: {
type: 'Identifier',
name: funcName,
},
object: {
type: 'Identifier',
name: identifier,
},
},
})
.replaceWith((path) => {
return j.callExpression(
j.memberExpression(
j.memberExpression(j.identifier(identifier), j.identifier(primitive)),
j.identifier(changes[primitive][funcName])
),
path.value.arguments
);
});
});
}
}
});
for (const primitive of Object.keys(changes)) {
const functions = Object.keys(changes[primitive]);
functions.forEach((funcName) => {
root
.find(j.CallExpression, {
callee: {
type: 'Identifier',
name: funcName,
},
})
.replaceWith((path) => {
if (j.Identifier.check(path.value.callee)) {
path.value.callee.name = changes[primitive][funcName];
return j.memberExpression(j.identifier(primitive), path.value);
}
});
});
}
};
const transform: Transform = (file, api) => {
const j = api.jscodeshift;
const root = j(file.source);
transformImports(root, j);
return root.toSource();
};
export const parser = 'tsx';
export default transform;