diff --git a/packages/utils/upgrade/resources/codemods/5.0.0/entity-service-document-service.code.ts b/packages/utils/upgrade/resources/codemods/5.0.0/entity-service-document-service.code.ts new file mode 100644 index 0000000000..a4f5120167 --- /dev/null +++ b/packages/utils/upgrade/resources/codemods/5.0.0/entity-service-document-service.code.ts @@ -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, name: any, j: JSCodeshift) => { + const declaration = findClosesDeclaration(path, name, j); + + if (!declaration) { + return; + } + + transformElement(path, declaration.init, j); +}; + +const transformElement = (path: ASTPath, 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, 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, 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; diff --git a/packages/utils/upgrade/resources/codemods/5.0.0/strapi-public-interface.code.ts b/packages/utils/upgrade/resources/codemods/5.0.0/strapi-public-interface.code.ts index 315d3202a1..2c85893545 100644 --- a/packages/utils/upgrade/resources/codemods/5.0.0/strapi-public-interface.code.ts +++ b/packages/utils/upgrade/resources/codemods/5.0.0/strapi-public-interface.code.ts @@ -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') ) ); diff --git a/packages/utils/upgrade/resources/codemods/5.0.0/utils-public-interface.code.ts b/packages/utils/upgrade/resources/codemods/5.0.0/utils-public-interface.code.ts new file mode 100644 index 0000000000..7c77037f97 --- /dev/null +++ b/packages/utils/upgrade/resources/codemods/5.0.0/utils-public-interface.code.ts @@ -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;