├── lib ├── v4 │ ├── utils │ │ ├── strapi-server.js │ │ ├── module-exports.js │ │ ├── strapi-admin.js │ │ ├── run-jscodeshift.js │ │ └── strapi-packages.js │ ├── migration-helpers │ │ ├── templates │ │ │ ├── config-plugins.liquid │ │ │ ├── config-api.liquid │ │ │ ├── config-admin.liquid │ │ │ ├── config-server.liquid │ │ │ ├── config-middlewares.liquid │ │ │ ├── src-webpack.liquid │ │ │ ├── config-database-sqlite.liquid │ │ │ ├── config-database-mysql.liquid │ │ │ ├── config-database-postgres.liquid │ │ │ ├── src-index.liquid │ │ │ ├── src-app.liquid │ │ │ └── app-env.liquid │ │ ├── update-api-folder-structure │ │ │ ├── templates │ │ │ │ ├── core-router.liquid │ │ │ │ ├── core-service.liquid │ │ │ │ └── core-controller.liquid │ │ │ ├── utils │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── update-plugin-folder-structure │ │ │ ├── utils │ │ │ │ ├── index.js │ │ │ │ ├── create-server-index.js │ │ │ │ ├── move-bootstrap-function.js │ │ │ │ ├── move-to-server.js │ │ │ │ ├── create-directory-index.js │ │ │ │ └── create-content-type-index.js │ │ │ ├── transforms │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── __mocks__ │ │ │ ├── mock-package-json-v3.json │ │ │ ├── mock-routes-v4.js │ │ │ └── mock-routes-v3.json │ │ ├── __tests__ │ │ │ ├── rename-api-files-to-singular.test.js │ │ │ ├── update-plugin-folder-structure │ │ │ │ ├── move-bootstrap-function.test.js │ │ │ │ ├── create-server-index.test.js │ │ │ │ ├── move-to-server.test.js │ │ │ │ ├── create-directory-index.test.js │ │ │ │ ├── create-content-type-index.test.js │ │ │ │ └── index.test.js │ │ │ ├── update-package-dependencies.test.js │ │ │ ├── update-routes.test.js │ │ │ ├── update-api-policies.test.js │ │ │ ├── update-api-folder-structure.test.js │ │ │ ├── convert-models-to-content-types.test.js │ │ │ └── get-relation-object.test.js │ │ ├── get-relation-object.js │ │ ├── update-application-services.js │ │ ├── update-api-policies.js │ │ ├── update-application-controllers.js │ │ ├── update-application-routes.js │ │ ├── rename-api-files-to-singular.js │ │ ├── update-routes.js │ │ ├── generate-application-config.js │ │ ├── update-package-dependencies.js │ │ ├── convert-components.js │ │ ├── convert-models-to-content-types.js │ │ └── update-application-folder-structure.js │ ├── transforms │ │ ├── __testfixtures__ │ │ │ ├── add-strapi-to-bootstrap-params.input.js │ │ │ ├── change-find-to-findMany.input.js │ │ │ ├── change-find-to-findMany.output.js │ │ │ ├── update-strapi-scoped-imports.input.js │ │ │ ├── update-strapi-scoped-imports.output.js │ │ │ ├── add-strapi-to-bootstrap-params.output.js │ │ │ ├── update-top-level-plugin-getter.input.js │ │ │ ├── update-top-level-plugin-getter.output.js │ │ │ ├── change-model-getters-to-content-types.input.js │ │ │ ├── change-model-getters-to-content-types.output.js │ │ │ ├── use-plugin-getters.input.js │ │ │ ├── use-plugin-getters.output.js │ │ │ ├── convert-object-export-to-function.input.js │ │ │ └── convert-object-export-to-function.output.js │ │ ├── __tests__ │ │ │ ├── use-plugin-getters.test.js │ │ │ ├── change-find-to-findMany.test.js │ │ │ ├── update-strapi-scoped-imports.test.js │ │ │ ├── add-strapi-to-bootstrap-params.test.js │ │ │ ├── update-top-level-plugin-getter.test.js │ │ │ ├── convert-object-export-to-function.test.js │ │ │ └── change-model-getter-to-content-types.test.js │ │ ├── change-model-getters-to-content-types.js │ │ ├── update-strapi-scoped-imports.js │ │ ├── add-strapi-to-bootstrap-params.js │ │ ├── change-find-to-findMany.js │ │ ├── update-top-level-plugin-getter.js │ │ ├── use-plugin-getters.js │ │ └── convert-object-export-to-function.js │ └── index.js ├── index.js └── global │ ├── index.js │ └── utils │ ├── index.js │ ├── logger.js │ ├── is-path-strapi-app.js │ ├── prompt-user.js │ ├── is-clean-git-repo.js │ └── format-code.js ├── preview.png ├── .prettierrc.js ├── __mocks__ └── fs-extra.js ├── .github └── workflows │ └── tests.yml ├── .eslintrc.js ├── package.json ├── bin ├── commands │ ├── default-commands.js │ ├── migrate.js │ └── transform.js └── cli.js ├── .gitignore └── README.md /lib/v4/utils/strapi-server.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./server'); 2 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi/codemods/HEAD/preview.png -------------------------------------------------------------------------------- /lib/v4/utils/module-exports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /lib/v4/utils/strapi-admin.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./admin/src').default; 2 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const v4 = require('./v4'); 2 | 3 | module.exports = { 4 | v4, 5 | }; 6 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/config-plugins.liquid: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({}); -------------------------------------------------------------------------------- /lib/global/index.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils'); 2 | 3 | module.exports = { 4 | utils, 5 | }; 6 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/add-strapi-to-bootstrap-params.input.js: -------------------------------------------------------------------------------- 1 | module.exports = () => {}; 2 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/change-find-to-findMany.input.js: -------------------------------------------------------------------------------- 1 | strapi.query('something').find(); 2 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/change-find-to-findMany.output.js: -------------------------------------------------------------------------------- 1 | strapi.query('something').findMany(); -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/update-strapi-scoped-imports.input.js: -------------------------------------------------------------------------------- 1 | require('strapi-something-or-another'); 2 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/update-strapi-scoped-imports.output.js: -------------------------------------------------------------------------------- 1 | require('@strapi/something-or-another'); 2 | -------------------------------------------------------------------------------- /lib/v4/index.js: -------------------------------------------------------------------------------- 1 | const migrationHelpers = require('./migration-helpers'); 2 | 3 | module.exports = { 4 | migrationHelpers, 5 | }; 6 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/add-strapi-to-bootstrap-params.output.js: -------------------------------------------------------------------------------- 1 | module.exports = ( 2 | { 3 | strapi 4 | } 5 | ) => {}; 6 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/update-top-level-plugin-getter.input.js: -------------------------------------------------------------------------------- 1 | strapi.plugins['test-one']; 2 | strapi.plugins.testTwo; 3 | strapi.plugins['test-three'].services; 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | endOfLine: 'lf', 3 | semi: true, 4 | singleQuote: true, 5 | tabWidth: 2, 6 | trailingComma: 'es5', 7 | printWidth: 100, 8 | }; 9 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/config-api.liquid: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rest: { 3 | defaultLimit: 25, 4 | maxLimit: 100, 5 | withCount: true, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/update-top-level-plugin-getter.output.js: -------------------------------------------------------------------------------- 1 | strapi.plugin('test-one'); 2 | strapi.plugin('test-two'); 3 | strapi.plugin('test-three').services; 4 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/config-admin.liquid: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | auth: { 3 | secret: env('ADMIN_JWT_SECRET'), 4 | }, 5 | apiToken: { 6 | salt: env('API_TOKEN_SALT'), 7 | }, 8 | }); -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/config-server.liquid: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | host: env('HOST', '0.0.0.0'), 3 | port: env.int('PORT', 1337), 4 | app: { 5 | keys: env.array('APP_KEYS'), 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/change-model-getters-to-content-types.input.js: -------------------------------------------------------------------------------- 1 | strapi.models; 2 | strapi.models['some-model']; 3 | strapi.plugins['some-plugin'].models; 4 | strapi.plugins['some-plugin'].models['some-model']; 5 | -------------------------------------------------------------------------------- /lib/v4/transforms/__tests__/use-plugin-getters.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.autoMockOff(); 4 | 5 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 6 | 7 | defineTest(__dirname, 'use-plugin-getters'); 8 | -------------------------------------------------------------------------------- /lib/v4/transforms/__tests__/change-find-to-findMany.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.autoMockOff(); 4 | 5 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 6 | 7 | defineTest(__dirname, 'change-find-to-findMany'); 8 | -------------------------------------------------------------------------------- /lib/v4/transforms/__tests__/update-strapi-scoped-imports.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.autoMockOff(); 4 | 5 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 6 | 7 | defineTest(__dirname, 'update-strapi-scoped-imports'); 8 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/change-model-getters-to-content-types.output.js: -------------------------------------------------------------------------------- 1 | strapi.contentTypes; 2 | strapi.contentTypes['some-model']; 3 | strapi.plugins['some-plugin'].contentTypes; 4 | strapi.plugins['some-plugin'].contentTypes['some-model']; 5 | -------------------------------------------------------------------------------- /lib/v4/transforms/__tests__/add-strapi-to-bootstrap-params.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.autoMockOff(); 4 | 5 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 6 | 7 | defineTest(__dirname, 'add-strapi-to-bootstrap-params'); 8 | -------------------------------------------------------------------------------- /lib/v4/transforms/__tests__/update-top-level-plugin-getter.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.autoMockOff(); 4 | 5 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 6 | 7 | defineTest(__dirname, 'update-top-level-plugin-getter'); 8 | -------------------------------------------------------------------------------- /lib/v4/transforms/__tests__/convert-object-export-to-function.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.autoMockOff(); 4 | 5 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 6 | 7 | defineTest(__dirname, 'convert-object-export-to-function'); 8 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-api-folder-structure/templates/core-router.liquid: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * {{ id }} router 5 | */ 6 | 7 | const { createCoreRouter } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreRouter('{{ uid }}'); -------------------------------------------------------------------------------- /lib/v4/transforms/__tests__/change-model-getter-to-content-types.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.autoMockOff(); 4 | 5 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 6 | 7 | defineTest(__dirname, 'change-model-getters-to-content-types'); 8 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-api-folder-structure/templates/core-service.liquid: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * {{ id }} service 5 | */ 6 | 7 | const { createCoreService } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreService('{{ uid }}'); -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-api-folder-structure/templates/core-controller.liquid: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * {{ id }} controller 5 | */ 6 | 7 | const { createCoreController } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreController('{{ uid }}'); -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/use-plugin-getters.input.js: -------------------------------------------------------------------------------- 1 | strapi.plugin('test-one').services.serviceA; 2 | strapi.plugin('test-one').services['serviceB']; 3 | strapi.plugin('test-three').services['serviceC'].stuff; 4 | strapi.plugin('test-two').controllers.controllerA; 5 | strapi.plugin('test-two').controllers['controllerB']; 6 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/use-plugin-getters.output.js: -------------------------------------------------------------------------------- 1 | strapi.plugin('test-one').service('serviceA'); 2 | strapi.plugin('test-one').service('serviceB'); 3 | strapi.plugin('test-three').service('serviceC').stuff; 4 | strapi.plugin('test-two').controller('controllerA'); 5 | strapi.plugin('test-two').controller('controllerB'); 6 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/config-middlewares.liquid: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'strapi::errors', 3 | 'strapi::security', 4 | 'strapi::cors', 5 | 'strapi::poweredBy', 6 | 'strapi::logger', 7 | 'strapi::query', 8 | 'strapi::body', 9 | 'strapi::session', 10 | 'strapi::favicon', 11 | 'strapi::public', 12 | ]; 13 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/src-webpack.liquid: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-unused-vars */ 4 | module.exports = (config, webpack) => { 5 | // Note: we provide webpack above so you should not `require` it 6 | // Perform customizations to webpack config 7 | // Important: return the modified config 8 | return config; 9 | }; 10 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/config-database-sqlite.liquid: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = ({ env }) => ({ 4 | connection: { 5 | client: 'sqlite', 6 | connection: { 7 | filename: path.join(__dirname, '..', env('DATABASE_FILENAME', '.tmp/data.db')), 8 | }, 9 | useNullAsDefault: true, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /lib/global/utils/index.js: -------------------------------------------------------------------------------- 1 | const formatCode = require('./format-code'); 2 | const isPathStrapiApp = require('./is-path-strapi-app'); 3 | const logger = require('./logger'); 4 | const isCleanGitRepo = require('./is-clean-git-repo'); 5 | const promptUser = require('./prompt-user'); 6 | 7 | module.exports = { 8 | formatCode, 9 | isPathStrapiApp, 10 | logger, 11 | isCleanGitRepo, 12 | promptUser, 13 | }; 14 | -------------------------------------------------------------------------------- /lib/v4/transforms/change-model-getters-to-content-types.js: -------------------------------------------------------------------------------- 1 | module.exports = function changeModelGettersToContentTypes(file, api) { 2 | const j = api.jscodeshift; 3 | const root = j(file.source); 4 | const models = root.find(j.Identifier, { name: 'models' }); 5 | 6 | models.forEach(({ node }) => { 7 | node.name = 'contentTypes'; 8 | }); 9 | 10 | return root.toSource({ quote: 'single' }); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/convert-object-export-to-function.input.js: -------------------------------------------------------------------------------- 1 | const file = require('file'); 2 | 3 | const someVar = { name: 'paul' }; 4 | 5 | const foo = () => { 6 | console.log(strapi); 7 | }; 8 | 9 | function bar() { 10 | console.log(strapi); 11 | } 12 | 13 | module.exports = { 14 | foo, 15 | bar, 16 | plop() { 17 | console.log(strapi); 18 | }, 19 | plunk() { 20 | console.log(strapi); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/index.js: -------------------------------------------------------------------------------- 1 | const migrateApiFolder = require('./update-api-folder-structure'); 2 | const migrateDependencies = require('./update-package-dependencies'); 3 | const migratePlugin = require('./update-plugin-folder-structure'); 4 | const migrateApplicationFolderStructure = require('./update-application-folder-structure'); 5 | 6 | module.exports = { 7 | migrateApiFolder, 8 | migrateDependencies, 9 | migratePlugin, 10 | migrateApplicationFolderStructure, 11 | }; 12 | -------------------------------------------------------------------------------- /lib/global/utils/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | 5 | module.exports = { 6 | error(message) { 7 | console.error(`${chalk.red('error')}: ${message}`); 8 | }, 9 | 10 | warn(message) { 11 | console.log(`${chalk.yellow('warning')}: ${message}`); 12 | }, 13 | 14 | info(message) { 15 | console.log(`${chalk.blue('info')}: ${message}`); 16 | }, 17 | 18 | success(message) { 19 | console.log(`${chalk.green('success')}: ${message}`); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/config-database-mysql.liquid: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | connection: { 3 | client: 'mysql', 4 | connection: { 5 | host: env('DATABASE_HOST', '127.0.0.1'), 6 | port: env.int('DATABASE_PORT', 3306), 7 | database: env('DATABASE_NAME', 'strapi'), 8 | user: env('DATABASE_USERNAME', 'strapi'), 9 | password: env('DATABASE_PASSWORD', 'strapi'), 10 | ssl: env.bool('DATABASE_SSL', false), 11 | }, 12 | debug: false, 13 | }, 14 | }); -------------------------------------------------------------------------------- /lib/global/utils/is-path-strapi-app.js: -------------------------------------------------------------------------------- 1 | const { resolve, join } = require('path'); 2 | const logger = require('./logger'); 3 | 4 | const isPathStrapiApp = (path) => { 5 | try { 6 | const pkgJSON = require(resolve(join(path, 'package.json'))); 7 | return 'strapi' in pkgJSON; 8 | } catch (error) { 9 | logger.error( 10 | 'The specified path is not a Strapi project. Please check the path and try again.' 11 | ); 12 | process.exit(1) 13 | } 14 | }; 15 | 16 | module.exports = isPathStrapiApp; 17 | -------------------------------------------------------------------------------- /lib/v4/transforms/__testfixtures__/convert-object-export-to-function.output.js: -------------------------------------------------------------------------------- 1 | const file = require('file'); 2 | 3 | const someVar = { name: 'paul' }; 4 | 5 | module.exports = ( 6 | { 7 | strapi 8 | } 9 | ) => { 10 | const foo = () => { 11 | console.log(strapi); 12 | }; 13 | 14 | function bar() { 15 | console.log(strapi); 16 | } 17 | 18 | return { 19 | foo, 20 | bar, 21 | 22 | plop() { 23 | console.log(strapi); 24 | }, 25 | 26 | plunk() { 27 | console.log(strapi); 28 | } 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-plugin-folder-structure/utils/index.js: -------------------------------------------------------------------------------- 1 | const moveToServer = require('./move-to-server'); 2 | const moveBootstrapFunction = require('./move-bootstrap-function'); 3 | const createDirectoryIndex = require('./create-directory-index'); 4 | const createServerIndex = require('./create-server-index'); 5 | const createContentTypeIndex = require('./create-content-type-index'); 6 | 7 | module.exports = { 8 | createDirectoryIndex, 9 | createServerIndex, 10 | createContentTypeIndex, 11 | moveToServer, 12 | moveBootstrapFunction, 13 | }; 14 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/config-database-postgres.liquid: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | connection: { 3 | client: 'postgres', 4 | connection: { 5 | host: env('DATABASE_HOST', '127.0.0.1'), 6 | port: env.int('DATABASE_PORT', 5432), 7 | database: env('DATABASE_NAME', 'strapi'), 8 | user: env('DATABASE_USERNAME', 'strapi'), 9 | password: env('DATABASE_PASSWORD', 'strapi'), 10 | schema: env('DATABASE_SCHEMA', 'public'), // Not required 11 | ssl: env.bool('DATABASE_SSL', false), 12 | }, 13 | debug: false, 14 | }, 15 | }); -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/src-index.liquid: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | /** 5 | * An asynchronous register function that runs before 6 | * your application is initialized. 7 | * 8 | * This gives you an opportunity to extend code. 9 | */ 10 | register(/*{ strapi }*/) {}, 11 | 12 | /** 13 | * An asynchronous bootstrap function that runs before 14 | * your application gets started. 15 | * 16 | * This gives you an opportunity to set up your data model, 17 | * run jobs, or perform some special logic. 18 | */ 19 | bootstrap(/*{ strapi }*/) {}, 20 | }; -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__mocks__/mock-package-json-v3.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "strapi": "3.6.8", 4 | "strapi-admin": "3.6.8", 5 | "strapi-utils": "3.6.8", 6 | "strapi-plugin-content-type-builder": "3.6.8", 7 | "strapi-plugin-content-manager": "3.6.8", 8 | "strapi-plugin-users-permissions": "3.6.8", 9 | "strapi-plugin-email": "3.6.8", 10 | "strapi-plugin-upload": "3.6.8", 11 | "strapi-plugin-i18n": "3.6.8", 12 | "strapi-connector-bookshelf": "3.6.8" 13 | }, 14 | "engines": { 15 | "node": ">=10.16.0 <=14.x.x", 16 | "npm": ">=10.16.0 <=14.x.x" 17 | } 18 | } -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__mocks__/mock-routes-v4.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | routes: [ 3 | { 4 | method: 'GET', 5 | path: '/test', 6 | handler: 'test.index', 7 | config: { 8 | policies: [], 9 | }, 10 | }, 11 | { 12 | method: 'POST', 13 | path: '/test/create', 14 | handler: 'test.create', 15 | config: { 16 | policies: [], 17 | }, 18 | }, 19 | { 20 | method: 'PUT', 21 | path: '/test/:id', 22 | handler: 'test.update', 23 | config: { 24 | policies: [], 25 | }, 26 | }, 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /lib/v4/transforms/update-strapi-scoped-imports.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Replace strapi-some-package with @strapi/some-package 3 | */ 4 | module.exports = function updateStrapiScopedImports(file, api) { 5 | const j = api.jscodeshift; 6 | const root = j(file.source); 7 | const imports = root.find(j.CallExpression, { 8 | callee: { 9 | name: 'require', 10 | }, 11 | }); 12 | 13 | imports.forEach(({ node }) => { 14 | const arg = node.arguments[0].value; 15 | const update = arg && arg.replace('strapi-', '@strapi/'); 16 | node.arguments[0].value = update; 17 | }); 18 | 19 | return root.toSource({ quote: 'single' }); 20 | }; 21 | -------------------------------------------------------------------------------- /__mocks__/fs-extra.js: -------------------------------------------------------------------------------- 1 | const fsExtra = require('fs-extra'); 2 | 3 | jest.mock('fs-extra', () => ({ 4 | exists: jest.fn(() => Promise.resolve()), 5 | pathExists: jest.fn(() => Promise.resolve()), 6 | readdir: jest.fn(() => Promise.resolve()), 7 | readJSON: jest.fn(() => Promise.resolve()), 8 | remove: jest.fn(() => Promise.resolve()), 9 | ensureFile: jest.fn(() => Promise.resolve()), 10 | writeJSON: jest.fn(() => Promise.resolve()), 11 | copy: jest.fn(() => Promise.resolve()), 12 | move: jest.fn(() => Promise.resolve()), 13 | createWriteStream: jest.fn(), 14 | rename: jest.fn(() => Promise.resolve()) 15 | })); 16 | 17 | module.exports = fsExtra; 18 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/src-app.liquid: -------------------------------------------------------------------------------- 1 | const config = { 2 | locales: [ 3 | // 'ar', 4 | // 'fr', 5 | // 'cs', 6 | // 'de', 7 | // 'dk', 8 | // 'es', 9 | // 'he', 10 | // 'id', 11 | // 'it', 12 | // 'ja', 13 | // 'ko', 14 | // 'ms', 15 | // 'nl', 16 | // 'no', 17 | // 'pl', 18 | // 'pt-BR', 19 | // 'pt', 20 | // 'ru', 21 | // 'sk', 22 | // 'sv', 23 | // 'th', 24 | // 'tr', 25 | // 'uk', 26 | // 'vi', 27 | // 'zh-Hans', 28 | // 'zh', 29 | ], 30 | }; 31 | 32 | const bootstrap = (app) => { 33 | console.log(app); 34 | }; 35 | 36 | export default { 37 | config, 38 | bootstrap, 39 | }; 40 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-plugin-folder-structure/utils/create-server-index.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const { importFilesToIndex, addModulesToExport } = require('../transforms'); 5 | 6 | async function createServerIndex(serverDir) { 7 | const indexPath = join(serverDir, 'index.js'); 8 | await fs.copy(join(__dirname, '..', '..', '..', 'utils', 'module-exports.js'), indexPath); 9 | 10 | const dirContent = await fs.readdir(serverDir); 11 | const filesToImport = dirContent.filter((file) => file !== 'index.js'); 12 | 13 | await importFilesToIndex(indexPath, filesToImport); 14 | await addModulesToExport(indexPath, filesToImport); 15 | } 16 | 17 | module.exports = createServerIndex; 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | paths-ignore: 9 | - '**.mdx?' 10 | - '**.md?' 11 | 12 | jobs: 13 | unit_tests: 14 | name: 'unit_tests (node: ${{ matrix.node }})' 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node: [14, 16] 19 | steps: 20 | - uses: actions/checkout@v2 21 | with: 22 | repository: 'strapi/codemods' 23 | persist-credentials: false 24 | - uses: actions/setup-node@v2-beta 25 | with: 26 | node-version: ${{ matrix.node }} 27 | - name: 'Run install' 28 | run: yarn install 29 | - name: Run tests 30 | run: yarn test 31 | -------------------------------------------------------------------------------- /lib/v4/transforms/add-strapi-to-bootstrap-params.js: -------------------------------------------------------------------------------- 1 | module.exports = function addStrapiToBootStrapParams(file, api) { 2 | const j = api.jscodeshift; 3 | const root = j(file.source); 4 | 5 | const moduleExports = root.find(j.AssignmentExpression, { 6 | left: { 7 | object: { 8 | name: 'module', 9 | }, 10 | property: { 11 | name: 'exports', 12 | }, 13 | }, 14 | right: { 15 | type: 'ArrowFunctionExpression', 16 | }, 17 | }); 18 | 19 | const strapiArg = j.property('init', j.identifier('strapi'), j.identifier('strapi')); 20 | moduleExports.get().value.right.params = [ 21 | j.objectPattern([{ ...strapiArg, shorthand: true, loc: { indent: 0 } }]), 22 | ]; 23 | 24 | return root.toSource({ quote: 'single' }); 25 | }; 26 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__mocks__/mock-routes-v3.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "method": "GET", 5 | "path": "/test", 6 | "handler": "test.index", 7 | "config": { 8 | "policies": [] 9 | } 10 | }, 11 | { 12 | "method": "GET", 13 | "path": "/test/count/", 14 | "handler": "test.count", 15 | "config": { 16 | "policies": [] 17 | } 18 | }, 19 | { 20 | "method": "POST", 21 | "path": "/test/create", 22 | "handler": "test.create", 23 | "config": { 24 | "policies": [] 25 | } 26 | }, 27 | { 28 | "method": "PUT", 29 | "path": "/test/:id", 30 | "handler": "test.update", 31 | "config": { 32 | "policies": [] 33 | } 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-plugin-folder-structure/utils/move-bootstrap-function.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const runJscodeshift = require('../../../utils/run-jscodeshift'); 5 | const moveToServer = require('./move-to-server'); 6 | 7 | async function moveBootstrapFunction(pluginPath) { 8 | await moveToServer(pluginPath, join('config', 'functions'), 'bootstrap.js'); 9 | 10 | const functionsDir = join(pluginPath, 'config', 'functions'); 11 | const dirContent = await fs.readdir(functionsDir); 12 | 13 | await runJscodeshift(join(pluginPath, 'server', 'bootstrap.js'), 'add-strapi-to-bootstrap-params'); 14 | 15 | if (!dirContent.length) { 16 | await fs.remove(functionsDir); 17 | } 18 | } 19 | 20 | module.exports = moveBootstrapFunction; 21 | -------------------------------------------------------------------------------- /lib/v4/transforms/change-find-to-findMany.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Replaces .query().find() with .query().findMany() 3 | * 4 | */ 5 | module.exports = function changeFindToFindMany(file, api) { 6 | const j = api.jscodeshift; 7 | const root = j(file.source); 8 | const strapiQueries = root.find(j.CallExpression, { 9 | callee: { 10 | object: { 11 | callee: { 12 | object: { 13 | name: 'strapi', 14 | }, 15 | property: { 16 | name: 'query', 17 | }, 18 | }, 19 | }, 20 | property: { 21 | name: 'find', 22 | }, 23 | }, 24 | }); 25 | 26 | strapiQueries.forEach(({ node }) => { 27 | return (node.callee.property.name = 'findMany'); 28 | }); 29 | 30 | return root.toSource({ quote: 'single' }); 31 | }; 32 | -------------------------------------------------------------------------------- /lib/v4/utils/run-jscodeshift.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | const jscodeshiftExecutable = require.resolve('.bin/jscodeshift'); 4 | const execa = require('execa'); 5 | const { logger } = require('../../global/utils'); 6 | 7 | /** 8 | * @description Executes jscodeshift 9 | * 10 | * @param {string} path - the path where the transform should run 11 | * @param {string} transform - the name of the transform file 12 | * @param {object} options - execa options 13 | */ 14 | module.exports = async (path, transform, options) => { 15 | try { 16 | return execa( 17 | jscodeshiftExecutable, 18 | ['-t', join(__dirname, '..', 'transforms', `${transform}.js`), path], 19 | options 20 | ); 21 | } catch (error) { 22 | logger.error(error.message); 23 | process.exit(1) 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/rename-api-files-to-singular.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | 3 | const renameApiFilesToSingular = require('../rename-api-files-to-singular'); 4 | 5 | describe('Rename collections types to singular name', () => { 6 | it('renames plural paths to singular paths', async () => { 7 | await renameApiFilesToSingular('test', 'plurals', 'plural'); 8 | expect(fs.rename.mock.calls).toEqual([ 9 | ['test/plurals', 'test/plural'], 10 | ['test/plural/controllers/plurals.js', 'test/plural/controllers/plural.js'], 11 | ['test/plural/services/plurals.js', 'test/plural/services/plural.js'], 12 | ['test/plural/models/plurals.js', 'test/plural/models/plural.js'], 13 | ['test/plural/models/plurals.settings.json', 'test/plural/models/plural.settings.json'], 14 | ]); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-plugin-folder-structure/utils/move-to-server.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | const chalk = require('chalk'); 4 | 5 | const { logger } = require('../../../../global/utils'); 6 | 7 | async function moveToServer(v4Plugin, originDir, serverDir) { 8 | const exists = await fs.pathExists(join(v4Plugin, originDir, serverDir)); 9 | if (!exists) return; 10 | 11 | const origin = join(v4Plugin, originDir, serverDir); 12 | const destination = join(v4Plugin, 'server', serverDir); 13 | await fs.move(origin, destination); 14 | 15 | const destinationLog = 16 | serverDir === 'models' ? join(v4Plugin, 'server', 'content-types') : destination; 17 | logger.info(`moved ${chalk.yellow(serverDir)} to ${chalk.yellow(destinationLog)}`); 18 | } 19 | 20 | module.exports = moveToServer; 21 | -------------------------------------------------------------------------------- /lib/global/utils/prompt-user.js: -------------------------------------------------------------------------------- 1 | // Inquirer engine. 2 | const { prompt } = require('inquirer'); 3 | 4 | // Prompt's configuration 5 | const defaultPromptOptions = [ 6 | { 7 | type: 'list', 8 | name: 'type', 9 | message: 'What do you want to migrate?', 10 | choices: [ 11 | { name: 'Application', value: 'application' }, 12 | { name: 'Plugin', value: 'plugin' }, 13 | { name: 'Only Dependencies', value: 'dependencies' }, 14 | ], 15 | }, 16 | { 17 | type: 'input', 18 | name: 'path', 19 | message(answer) { 20 | return answer.type === 'plugin' 21 | ? 'Enter the path to your Strapi plugin' 22 | : 'Enter the path to your Strapi application'; 23 | }, 24 | }, 25 | ]; 26 | 27 | module.exports = async (options) => { 28 | const promptOptions = options || defaultPromptOptions; 29 | return prompt(promptOptions); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-plugin-folder-structure/utils/create-directory-index.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const { importFilesToIndex, addModulesToExport } = require('../transforms'); 5 | 6 | async function createDirectoryIndex(dir) { 7 | const hasDir = await fs.pathExists(dir); 8 | if (!hasDir) return; 9 | 10 | const indexPath = join(dir, 'index.js'); 11 | 12 | await fs.copy(join(__dirname, '..', '..', '..', 'utils', 'module-exports.js'), indexPath); 13 | 14 | const dirContent = await fs.readdir(dir, { withFileTypes: true }); 15 | const filesToImport = dirContent 16 | .filter((fd) => fd.isFile() && fd.name !== 'index.js') 17 | .map((file) => file.name); 18 | 19 | await importFilesToIndex(indexPath, filesToImport); 20 | await addModulesToExport(indexPath, filesToImport); 21 | } 22 | 23 | module.exports = createDirectoryIndex; 24 | -------------------------------------------------------------------------------- /lib/v4/transforms/update-top-level-plugin-getter.js: -------------------------------------------------------------------------------- 1 | // Update plugin getters 2 | // strapi.plugins['plugin-name'] => strapi.plugin("plugin-name") 3 | // strapi.plugins.pluginName => strapi.plugin("plugin-name") 4 | const _ = require('lodash'); 5 | 6 | module.exports = function updateTopLevelGetter(file, api) { 7 | const j = api.jscodeshift; 8 | const root = j(file.source); 9 | const foundPlugin = root.find(j.MemberExpression, { 10 | object: { 11 | object: { 12 | name: 'strapi', 13 | }, 14 | property: { 15 | name: 'plugins', 16 | }, 17 | }, 18 | }); 19 | 20 | foundPlugin.replaceWith(({ node }) => { 21 | const name = node.property.name ? node.property.name : node.property.value; 22 | 23 | return j.callExpression(j.memberExpression(j.identifier('strapi'), j.identifier('plugin')), [ 24 | j.literal(_.kebabCase(name)), 25 | ]); 26 | }); 27 | 28 | return root.toSource({ quote: 'single' }); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/global/utils/is-clean-git-repo.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const execa = require('execa'); 3 | const chalk = require('chalk'); 4 | const logger = require('./logger'); 5 | 6 | const isCleanGitRepo = async (path) => { 7 | try { 8 | await execa('git', ['-C', resolve(path), 'rev-parse']); 9 | } catch (error) { 10 | logger.error( 11 | `A ${chalk.yellow('git')} directory was not found at ${chalk.yellow(resolve(path))}.` 12 | ); 13 | logger.error(`You should not run this command without ${chalk.yellow('git')}.`); 14 | process.exit(1); 15 | } 16 | 17 | try { 18 | const { stdout } = await execa('git', [`--git-dir=${path}/.git`, `--work-tree=${path}`, 'status', '--porcelain']); 19 | if (stdout.length) 20 | throw Error( 21 | `The ${chalk.yellow('git')} directory at ${chalk.yellow(resolve(path))} is not clean` 22 | ); 23 | } catch (err) { 24 | logger.error(err.message); 25 | process.exit(1); 26 | } 27 | }; 28 | 29 | module.exports = isCleanGitRepo; 30 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/templates/app-env.liquid: -------------------------------------------------------------------------------- 1 | ## Server Settings 2 | 3 | HOST={{ host }} 4 | PORT={{ port }} 5 | APP_KEYS={{ appKeys }} 6 | 7 | ## Admin Settings 8 | 9 | API_TOKEN_SALT={{ apiTokenSalt }} 10 | ADMIN_JWT_SECRET={{ adminJwtSecret }} 11 | 12 | ## Users-Permissions Plugin Settings 13 | 14 | JWT_SECRET={{ jwtSecret }} 15 | 16 | ## Database Settings 17 | {% if databaseType == 'mysql' %} 18 | DATABASE_HOST={{ databaseHost }} 19 | DATABASE_PORT={{ databasePort }} 20 | DATABASE_NAME={{ databaseName }} 21 | DATABASE_USERNAME={{ databaseUsername }} 22 | DATABASE_PASSWORD={{ databasePassword }} 23 | DATABASE_SSL={{ databaseSsl }} 24 | {% elsif databaseType == 'postgres' %} 25 | DATABASE_HOST={{ databaseHost }} 26 | DATABASE_PORT={{ databasePort }} 27 | DATABASE_NAME={{ databaseName }} 28 | DATABASE_USERNAME={{ databaseUsername }} 29 | DATABASE_PASSWORD={{ databasePassword }} 30 | DATABASE_SSL={{ databaseSsl }} 31 | DATABASE_SCHEMA={{ databaseSchema }} 32 | {% else %} 33 | DATABASE_FILENAME={{ databaseFilename }} 34 | {% endif %} -------------------------------------------------------------------------------- /lib/v4/migration-helpers/get-relation-object.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const pluralize = require('pluralize'); 3 | 4 | const getMapped = (mapped, inversed) => { 5 | if (!mapped) return {}; 6 | 7 | if (inversed) { 8 | return { 9 | inversedBy: mapped, 10 | }; 11 | } 12 | 13 | return { 14 | mappedBy: mapped, 15 | }; 16 | }; 17 | 18 | /** 19 | * 20 | * @param {string} relation The type of relation 21 | * @param {object} attribute The attribute object 22 | * @returns 23 | */ 24 | module.exports = (relation, attribute) => { 25 | // Parse the target 26 | const targetType = attribute.collection || attribute.model; 27 | const target = attribute.plugin 28 | ? `plugin::${attribute.plugin}.${targetType}` 29 | : `api::${_.kebabCase(pluralize.singular(targetType))}.${_.kebabCase( 30 | pluralize.singular(targetType) 31 | )}`; 32 | 33 | return { 34 | type: 'relation', 35 | relation, 36 | target, 37 | ...getMapped(attribute.via, attribute.inversed), 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-plugin-folder-structure/utils/create-content-type-index.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const { importFilesToIndex, addModulesToExport } = require('../transforms'); 5 | 6 | const createDirectoryIndex = require('./create-directory-index'); 7 | 8 | async function createContentTypeIndex(v4PluginPath, dir) { 9 | const hasDir = await fs.pathExists(dir); 10 | if (!hasDir) return; 11 | 12 | const indexPath = join(dir, 'index.js'); 13 | 14 | await fs.copy(join(__dirname, '..', '..', '..', 'utils', 'module-exports.js'), indexPath); 15 | const dirContent = await fs.readdir(dir, { withFileTypes: true }); 16 | const directoriesToImport = dirContent.filter((fd) => fd.isDirectory()).map((fd) => fd.name); 17 | 18 | for (const directory of directoriesToImport) { 19 | createDirectoryIndex(join(v4PluginPath, 'server', 'content-types', directory)); 20 | } 21 | 22 | await importFilesToIndex(indexPath, directoriesToImport); 23 | await addModulesToExport(indexPath, directoriesToImport); 24 | } 25 | 26 | module.exports = createContentTypeIndex; 27 | -------------------------------------------------------------------------------- /lib/v4/transforms/use-plugin-getters.js: -------------------------------------------------------------------------------- 1 | const pluralize = require('pluralize'); 2 | 3 | module.exports = function usePluginGetters(file, api) { 4 | const j = api.jscodeshift; 5 | const root = j(file.source); 6 | 7 | const getters = ['services', 'controllers', 'models', 'policy', 'middleware']; 8 | 9 | getters.forEach((getter) => { 10 | const foundGetter = root.find(j.MemberExpression, { 11 | object: { 12 | object: { 13 | callee: { 14 | object: { 15 | name: 'strapi', 16 | }, 17 | property: { 18 | name: 'plugin', 19 | }, 20 | }, 21 | }, 22 | property: { 23 | name: getter, 24 | }, 25 | }, 26 | }); 27 | 28 | foundGetter.replaceWith(({ node }) => { 29 | const name = node.property.name || node.property.value; 30 | const property = j.callExpression(j.identifier(pluralize.singular(getter)), [ 31 | j.literal(name), 32 | ]); 33 | 34 | return { ...node.object, property }; 35 | }); 36 | }); 37 | 38 | return root.toSource({ quote: 'single' }); 39 | }; 40 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: [ 5 | 'eslint:recommended', 6 | 'prettier', 7 | 'plugin:import/errors', 8 | 'plugin:import/warnings', 9 | 'plugin:node/recommended', 10 | ], 11 | env: { 12 | es6: true, 13 | node: true, 14 | jest: true, 15 | }, 16 | globals: { 17 | strapi: false, 18 | }, 19 | ignorePatterns: ['/lib/v4/utils/', '/lib/v4/transforms/__testFixtures__'], 20 | rules: { 21 | 'node/no-unpublished-require': 'off', 22 | 'require-atomic-updates': 'off', 23 | 'no-process-exit': 'off', 24 | 'no-return-await': 'error', 25 | 'object-shorthand': ['error', 'always', { avoidExplicitReturnArrows: true }], 26 | 'import/order': 'error', 27 | 'import/no-cycle': 'error', 28 | 'import/no-useless-path-segments': 'error', 29 | 'import/first': 'error', 30 | 'import/extensions': ['error', 'never'], 31 | 'import/newline-after-import': 'error', 32 | 'node/exports-style': ['error', 'module.exports'], 33 | 'node/no-new-require': 'error', 34 | 'node/no-path-concat': 'error', 35 | 'node/no-callback-literal': 'error', 36 | 'node/handle-callback-err': 'error', 37 | 'one-var': ['error', 'never'], 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@strapi/codemods", 3 | "version": "1.3.0", 4 | "main": "bin/cli.js", 5 | "license": "MIT", 6 | "files": [ 7 | "bin/", 8 | "lib/" 9 | ], 10 | "bin": { 11 | "codemods": "bin/cli.js" 12 | }, 13 | "scripts": { 14 | "test": "jest --verbose", 15 | "lint": "eslint .", 16 | "lint:fix": "eslint --fix ." 17 | }, 18 | "dependencies": { 19 | "@babel/preset-env": "^7.1.6", 20 | "axios": "^0.21.2", 21 | "chalk": "^4.1.2", 22 | "commander": "^8.2.0", 23 | "dotenv": "^16.0.2", 24 | "eslint": "7.25.0", 25 | "eslint-config-airbnb": "18.2.1", 26 | "eslint-config-airbnb-base": "14.2.1", 27 | "eslint-config-prettier": "6.15.0", 28 | "eslint-plugin-import": "2.22.1", 29 | "eslint-plugin-node": "11.1.0", 30 | "execa": "^5.1.1", 31 | "fs-extra": "^10.0.0", 32 | "inquirer": "^8.2.0", 33 | "inquirer-fuzzy-path": "^2.3.0", 34 | "jscodeshift": "^0.13.0", 35 | "liquidjs": "^9.40.0", 36 | "lodash": "^4.17.21", 37 | "pluralize": "^8.0.0", 38 | "prettier": "^2.4.1" 39 | }, 40 | "engines": { 41 | "node": ">=12.x.x <=20.x.x", 42 | "npm": ">=6.0.0" 43 | }, 44 | "devDependencies": { 45 | "jest": "^27.0.6" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-application-services.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const logger = require('../../global/utils/logger'); 5 | 6 | /** 7 | * @description Migrates core services to v4 factories 8 | * 9 | * @param {string} apiPath Path to the current api 10 | * @param {string} apiName Name of the API 11 | * @param {function} liquidEngine Liquid engine to use for templating 12 | */ 13 | module.exports = async (apiPath, apiName, liquidEngine) => { 14 | const v4ServicePath = join(apiPath, 'services', `${apiName}.js`); 15 | 16 | try { 17 | // Compile the template 18 | const template = liquidEngine.renderFileSync('core-service', { 19 | id: apiName, 20 | uid: `api::${apiName}.${apiName}`, 21 | }); 22 | 23 | // Create the js file 24 | await fs.ensureFile(v4ServicePath); 25 | 26 | // Create write stream for new js file 27 | const file = fs.createWriteStream(v4ServicePath); 28 | 29 | // Export core controllers from liquid template file 30 | file.write(template); 31 | 32 | // Close the write stream 33 | file.end(); 34 | } catch (error) { 35 | logger.error(`an error occurred when creating factory service file for ${v4ServicePath}`); 36 | console.log(error); 37 | } 38 | }; -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/update-plugin-folder-structure/move-bootstrap-function.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../update-plugin-folder-structure/utils/move-to-server', () => jest.fn()); 2 | 3 | const { join } = require('path'); 4 | const fs = require('fs-extra'); 5 | 6 | const { moveBootstrapFunction } = require('../../update-plugin-folder-structure/utils'); 7 | const moveToServer = require('../../update-plugin-folder-structure/utils/move-to-server'); 8 | 9 | describe('moveBootstrapFunction', () => { 10 | beforeEach(() => { 11 | jest.spyOn(console, 'error').mockImplementation(() => {}); 12 | jest.spyOn(console, 'log').mockImplementation(() => {}); 13 | fs.readdir.mockReturnValueOnce([]); 14 | }); 15 | 16 | afterEach(() => { 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | it('moves the bootstrap function to the v4 server root', async () => { 21 | const v4Plugin = './plugin-dir'; 22 | 23 | await moveBootstrapFunction(v4Plugin); 24 | 25 | expect(moveToServer).toHaveBeenCalled(); 26 | }); 27 | 28 | it('removes the v3 bootstrap functions directory', async () => { 29 | const v4Plugin = './plugin-dir'; 30 | 31 | await moveBootstrapFunction(v4Plugin); 32 | 33 | expect(fs.remove).toHaveBeenCalledWith(join(v4Plugin, 'config', 'functions')); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-api-policies.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const chalk = require('chalk'); 5 | 6 | const { logger } = require('../../global/utils'); 7 | 8 | /** 9 | * 10 | * @param {string} apiPath Path to the current api 11 | */ 12 | const updatePolicies = async (apiPath) => { 13 | const v3PoliciesPath = join(apiPath, 'config', 'policies'); 14 | 15 | const exists = await fs.exists(v3PoliciesPath); 16 | if (!exists) return; 17 | 18 | const v3Policies = await fs.readdir(v3PoliciesPath, { withFileTypes: true }); 19 | const policyFiles = v3Policies.filter((fd) => fd.isFile()); 20 | 21 | if (!policyFiles.length) { 22 | await fs.remove(v3PoliciesPath); 23 | } 24 | 25 | const v4PoliciesPath = join(apiPath, 'policies'); 26 | try { 27 | for (const policy of policyFiles) { 28 | await fs.copy(join(v3PoliciesPath, policy.name), join(v4PoliciesPath, policy.name)); 29 | } 30 | // delete the v3 policy folder 31 | await fs.remove(v3PoliciesPath); 32 | } catch (error) { 33 | logger.error( 34 | `an error occured when migrating a policy from ${chalk.yellow( 35 | v3PoliciesPath 36 | )} to ${chalk.yellow(v4PoliciesPath)}` 37 | ); 38 | } 39 | }; 40 | 41 | module.exports = updatePolicies; 42 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-application-controllers.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const logger = require('../../global/utils/logger'); 5 | 6 | /** 7 | * @description Migrates core controllers to v4 factories 8 | * 9 | * @param {string} apiPath Path to the current api 10 | * @param {string} apiName Name of the API 11 | * @param {function} liquidEngine Liquid engine to use for templating 12 | */ 13 | module.exports = async (apiPath, apiName, liquidEngine) => { 14 | const v4ControllerPath = join(apiPath, 'controllers', `${apiName}.js`); 15 | 16 | try { 17 | // Compile the template 18 | const template = liquidEngine.renderFileSync('core-controller', { 19 | id: apiName, 20 | uid: `api::${apiName}.${apiName}`, 21 | }); 22 | 23 | // Create the js file 24 | await fs.ensureFile(v4ControllerPath); 25 | 26 | // Create write stream for new js file 27 | const file = fs.createWriteStream(v4ControllerPath); 28 | 29 | // Export core controllers from liquid template file 30 | file.write(template); 31 | 32 | // Close the write stream 33 | file.end(); 34 | } catch (error) { 35 | logger.error(`an error occurred when creating factory controller file for ${v4ControllerPath}`); 36 | console.log(error); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /bin/commands/default-commands.js: -------------------------------------------------------------------------------- 1 | // Inquirer engine. 2 | const { isCleanGitRepo, promptUser } = require('../../lib/global/utils'); 3 | const migrate = require('./migrate'); 4 | const transform = require('./transform'); 5 | 6 | const defaultTransform = async (type, path) => { 7 | await isCleanGitRepo(process.cwd()); 8 | await transform(type, path); 9 | }; 10 | 11 | const defaultMigrate = async () => { 12 | const { type, path } = await promptUser(); 13 | await migrate(type, path); 14 | }; 15 | 16 | // Prompt's configuration 17 | const promptOptions = [ 18 | { 19 | type: 'list', 20 | name: 'type', 21 | message: 'What would you like to do?', 22 | choices: [ 23 | { name: 'Migrate', value: 'migrate' }, 24 | { name: 'Transform', value: 'transform' }, 25 | ], 26 | }, 27 | ]; 28 | 29 | const defaultCommand = async () => { 30 | try { 31 | const options = await promptUser(promptOptions); 32 | 33 | switch (options.type) { 34 | case 'migrate': 35 | await defaultMigrate(); 36 | break; 37 | case 'transform': 38 | await defaultTransform(); 39 | break; 40 | } 41 | 42 | process.exit(0); 43 | } catch (error) { 44 | console.error(error); 45 | process.exit(1); 46 | } 47 | }; 48 | 49 | module.exports = { 50 | defaultCommand, 51 | defaultMigrate, 52 | defaultTransform, 53 | }; 54 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-application-routes.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const logger = require('../../global/utils/logger'); 5 | 6 | /** 7 | * @description Migrates core routers to v4 factories 8 | * 9 | * @param {string} apiPath Path to the current api 10 | * @param {string} apiName Name of the API 11 | * @param {function} liquidEngine Liquid engine to use for templating 12 | */ 13 | module.exports = async (apiPath, apiName, liquidEngine) => { 14 | const v4RouterPath = join(apiPath, 'routes', `${apiName}.js`); 15 | 16 | try { 17 | // Compile the template 18 | const template = liquidEngine.renderFileSync('core-router', { 19 | id: apiName, 20 | uid: `api::${apiName}.${apiName}`, 21 | }); 22 | 23 | // Create the js file 24 | await fs.ensureFile(v4RouterPath); 25 | 26 | // Create write stream for new js file 27 | const file = fs.createWriteStream(v4RouterPath); 28 | 29 | // Export core controllers from liquid template file 30 | file.write(template); 31 | 32 | // Close the write stream 33 | file.end(); 34 | 35 | // Delete the v3 config/routes.json 36 | await fs.remove(join(apiPath, 'config', 'routes.json')); 37 | 38 | } catch (error) { 39 | logger.error(`an error occurred when creating factory router file for ${v4RouterPath}`); 40 | console.log(error); 41 | } 42 | }; -------------------------------------------------------------------------------- /lib/global/utils/format-code.js: -------------------------------------------------------------------------------- 1 | const { join, extname } = require('path'); 2 | const prettier = require('prettier'); 3 | const { readFile, writeFile, readdir, lstat } = require('fs-extra'); 4 | const logger = require('./logger'); 5 | 6 | const formatFile = async (path) => { 7 | try { 8 | const fileContent = await readFile(path, 'utf-8'); 9 | // Format the code with prettier 10 | return writeFile( 11 | path, 12 | prettier.format(fileContent, { 13 | filepath: path, 14 | }) 15 | ); 16 | } catch (error) { 17 | logger.warn(`Failed to format code, check ${path}`); 18 | } 19 | }; 20 | 21 | /** 22 | * @description Recursively walks a directory to format code 23 | * @param {path} path - path to file or directory 24 | */ 25 | const formatCode = async (path) => { 26 | // Determine if path is a file 27 | const pathStats = await lstat(path); 28 | if (pathStats.isFile() && extname(path) === '.js') { 29 | return formatFile(path); 30 | } 31 | 32 | // Get content of directory 33 | const dirContent = await readdir(path, { withFileTypes: true }); 34 | for (const item of dirContent) { 35 | if (item.isFile() && extname(item.name) === '.js') { 36 | const filePath = join(path, item.name); 37 | return formatFile(filePath); 38 | } 39 | 40 | if (item.isDirectory()) { 41 | await formatCode(join(path, item.name)); 42 | } 43 | } 44 | }; 45 | 46 | module.exports = formatCode; 47 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/update-plugin-folder-structure/create-server-index.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../update-plugin-folder-structure/transforms', () => ({ 2 | importFilesToIndex: jest.fn(), 3 | addModulesToExport: jest.fn(), 4 | })); 5 | 6 | const { join } = require('path'); 7 | const fs = require('fs-extra'); 8 | 9 | const { createServerIndex } = require('../../update-plugin-folder-structure/utils'); 10 | const { 11 | importFilesToIndex, 12 | addModulesToExport, 13 | } = require('../../update-plugin-folder-structure/transforms'); 14 | 15 | describe('create-server-index', () => { 16 | beforeEach(() => { 17 | jest.spyOn(console, 'error').mockImplementation(() => {}); 18 | jest.spyOn(console, 'log').mockImplementation(() => {}); 19 | fs.readdir.mockReturnValueOnce(['test.js', 'test-two.js', 'index.js']); 20 | }); 21 | 22 | afterEach(() => { 23 | jest.clearAllMocks(); 24 | }); 25 | 26 | it('creates an index file for the server directory', async () => { 27 | const v4PluginPath = 'test-plugin-v4/server'; 28 | 29 | await createServerIndex(v4PluginPath); 30 | 31 | expect(fs.copy.mock.calls[0]).toEqual([ 32 | join(__dirname, '..', '..', '..', 'utils', 'module-exports.js'), 33 | join(v4PluginPath, 'index.js'), 34 | ]); 35 | expect(importFilesToIndex).toHaveBeenCalledWith(join(v4PluginPath, 'index.js'), [ 36 | 'test.js', 37 | 'test-two.js', 38 | ]); 39 | expect(addModulesToExport).toHaveBeenCalledWith(join(v4PluginPath, 'index.js'), [ 40 | 'test.js', 41 | 'test-two.js', 42 | ]); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/rename-api-files-to-singular.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | /** 5 | * Renames files in an api to use a singularize name 6 | * 7 | * @param {string} path - The original path 8 | * @param {string} oldName - The original name 9 | * @param {string} newName - The new name to use 10 | */ 11 | const renameApiFilesToSingular = async (path, oldName, newName) => { 12 | const oldPath = join(path, oldName); 13 | const newPath = join(path, newName); 14 | await fs.rename(oldPath, newPath); 15 | await fs.rename( 16 | join(newPath, 'controllers', `${oldName}.js`), 17 | join(newPath, 'controllers', `${newName}.js`) 18 | ); 19 | const documentationDir = join(newPath, 'documentation'); 20 | const documentationExists = await fs.exists(documentationDir); 21 | if (documentationExists) { 22 | const documentationDirs = await fs.readdir(documentationDir); 23 | for (const dir of documentationDirs) { 24 | await fs.rename( 25 | join(documentationDir, dir, `${oldName}.json`), 26 | join(documentationDir, dir, `${newName}.json`) 27 | ); 28 | } 29 | } 30 | await fs.rename( 31 | join(newPath, 'services', `${oldName}.js`), 32 | join(newPath, 'services', `${newName}.js`) 33 | ); 34 | await fs.rename( 35 | join(newPath, 'models', `${oldName}.js`), 36 | join(newPath, 'models', `${newName}.js`) 37 | ); 38 | await fs.rename( 39 | join(newPath, 'models', `${oldName}.settings.json`), 40 | join(newPath, 'models', `${newName}.settings.json`) 41 | ); 42 | }; 43 | 44 | module.exports = renameApiFilesToSingular 45 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-api-folder-structure/utils/index.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | const { logger } = require('../../../../global/utils'); 4 | 5 | /** 6 | * @description Get's directory entries from a given path 7 | * 8 | * @param {string} path The path to the directory 9 | * @returns array of of directory entries 10 | */ 11 | const getDirsAtPath = async (path) => { 12 | const dir = await fs.readdir(path, { withFileTypes: true }); 13 | return dir.filter((fd) => fd.isDirectory()); 14 | }; 15 | 16 | const getFilesAtPath = async (path) => { 17 | return fs.readdir(path, { withFileTypes: true }); 18 | }; 19 | 20 | /** 21 | * 22 | * @description Recursively removes empty directories 23 | * 24 | * @param {array} dirs Directory entries 25 | * @param {string} baseDir The path to check for empty directories 26 | */ 27 | const cleanEmptyDirectories = async (dirs, baseDir) => { 28 | for (const dir of dirs) { 29 | const currentDirPath = join(baseDir, dir.name); 30 | try { 31 | const currentDirContent = await fs.readdir(currentDirPath); 32 | 33 | if (!currentDirContent.length) { 34 | // Remove empty directory 35 | await fs.remove(currentDirPath); 36 | } else { 37 | // Otherwise get the directories of the current directory 38 | const currentDirs = await getDirsAtPath(currentDirPath); 39 | await cleanEmptyDirectories(currentDirs, currentDirPath); 40 | } 41 | } catch (error) { 42 | logger.warn(`Failed to remove ${currentDirPath}`); 43 | } 44 | } 45 | }; 46 | 47 | module.exports = { cleanEmptyDirectories, getDirsAtPath, getFilesAtPath }; 48 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/update-plugin-folder-structure/move-to-server.test.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const { moveToServer } = require('../../update-plugin-folder-structure/utils'); 5 | 6 | describe('moveToServer', () => { 7 | beforeEach(() => { 8 | jest.spyOn(console, 'error').mockImplementation(() => {}); 9 | jest.spyOn(console, 'log').mockImplementation(() => {}); 10 | }); 11 | 12 | afterEach(() => { 13 | jest.clearAllMocks(); 14 | }); 15 | 16 | it('checks if the origin directory exists', async () => { 17 | fs.pathExists.mockReturnValueOnce(true); 18 | const v4PluginPath = 'test-plugin-v4'; 19 | 20 | await moveToServer(v4PluginPath, 'origin', 'destination'); 21 | 22 | expect(fs.pathExists).toHaveBeenCalledWith(join(v4PluginPath, 'origin', 'destination')); 23 | }); 24 | 25 | it('exits when the origin directory does not exists', async () => { 26 | fs.pathExists.mockReturnValueOnce(false); 27 | const v4PluginPath = 'test-plugin-v4'; 28 | 29 | await moveToServer(v4PluginPath, 'origin', 'destination'); 30 | 31 | expect(fs.pathExists).toHaveBeenCalledWith(join(v4PluginPath, 'origin', 'destination')); 32 | expect(fs.move).not.toHaveBeenCalled(); 33 | }); 34 | 35 | it('moves to the correct v4 server path', async () => { 36 | fs.pathExists.mockReturnValueOnce(true); 37 | const v4PluginPath = 'test-plugin-v4'; 38 | 39 | await moveToServer(v4PluginPath, 'origin', 'destination'); 40 | 41 | expect(fs.move).toHaveBeenCalledWith( 42 | join(v4PluginPath, 'origin', 'destination'), 43 | join(v4PluginPath, 'server', 'destination') 44 | ); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-routes.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { inspect } = require('util'); 3 | const fs = require('fs-extra'); 4 | 5 | const logger = require('../../global/utils/logger'); 6 | 7 | /** 8 | * @description Migrates settings.json to schema.json 9 | * 10 | * @param {string} apiPath Path to the current api 11 | * @param {string} apiName Name of the API 12 | */ 13 | module.exports = async (apiPath, apiName, renamedFrom) => { 14 | const v3RoutePath = join(apiPath, 'config', 'routes.json'); 15 | const v4RoutePath = join(apiPath, 'routes', `${apiName}.js`); 16 | try { 17 | // Create the js file 18 | await fs.ensureFile(v4RoutePath); 19 | 20 | // Create write stream for new js file 21 | const file = fs.createWriteStream(v4RoutePath); 22 | 23 | // Get the existing JSON routes file 24 | const routesJson = await fs.readJSON(v3RoutePath); 25 | 26 | const { routes } = routesJson; 27 | 28 | // Remove count 29 | const updatedRoutes = routes.filter( 30 | (route) => !route.handler.includes('count') || !route.path.includes('count') 31 | ); 32 | 33 | const renamedRoutes = updatedRoutes.map((route) => { 34 | route.handler = route.handler.replace(renamedFrom, apiName); 35 | return route; 36 | }); 37 | 38 | // Transform objects to strings 39 | const routesToString = inspect({ routes: renamedRoutes }, { depth: Infinity }); 40 | 41 | // Export routes from create js file 42 | file.write(`module.exports = ${routesToString}`); 43 | 44 | // Close the write stream 45 | file.end(); 46 | 47 | // Delete the v3 config/routes.json 48 | await fs.remove(join(apiPath, 'config', 'routes.json')); 49 | } catch (error) { 50 | logger.error(`an error occured when migrating routes from ${v3RoutePath} to ${v4RoutePath}`); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /lib/v4/transforms/convert-object-export-to-function.js: -------------------------------------------------------------------------------- 1 | module.exports = function convertObjectExportToFunction(file, api) { 2 | const j = api.jscodeshift; 3 | const root = j(file.source); 4 | 5 | const remainingBody = []; 6 | const functionDeclarations = []; 7 | 8 | for (const node of root.get().node.program.body) { 9 | let isArrowFunction = false; 10 | if (node.type === 'VariableDeclaration') { 11 | isArrowFunction = 12 | node.declarations.filter((dec) => dec.init.type === 'ArrowFunctionExpression').length > 0; 13 | } 14 | 15 | if (node.type === 'FunctionDeclaration' || isArrowFunction) { 16 | functionDeclarations.push(node); 17 | } else { 18 | remainingBody.push(node); 19 | } 20 | } 21 | 22 | root.get().node.program.body = remainingBody; 23 | 24 | const moduleExports = root.find(j.AssignmentExpression, { 25 | left: { 26 | object: { 27 | name: 'module', 28 | }, 29 | property: { 30 | name: 'exports', 31 | }, 32 | }, 33 | right: { 34 | type: 'ObjectExpression', 35 | }, 36 | }); 37 | 38 | const strapiArg = j.property('init', j.identifier('strapi'), j.identifier('strapi')); 39 | const objectExpression = moduleExports.length && moduleExports.get().value.right; 40 | 41 | if (!objectExpression) { 42 | console.log('This file does not need to be transformed'); 43 | process.exit(1); 44 | } 45 | 46 | const arrowFunctionExpression = j.arrowFunctionExpression( 47 | [j.objectPattern([{ ...strapiArg, shorthand: true, loc: { indent: 0 } }])], 48 | j.blockStatement([ 49 | ...functionDeclarations, 50 | j.returnStatement(j.objectExpression(objectExpression.properties)), 51 | ]) 52 | ); 53 | 54 | moduleExports.get().value.right = arrowFunctionExpression; 55 | 56 | return root.toSource({ quote: 'single' }); 57 | }; 58 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/generate-application-config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const logger = require('../../global/utils/logger'); 5 | 6 | /** 7 | * @description Migrates v3 config structure to v4 structure 8 | * 9 | * @param {string} configPath Path to the config folder 10 | * @param {string} database Database Type 11 | * @param {function} liquidEngine Liquid engine to use for templating 12 | */ 13 | module.exports = async (configPath, database, liquidEngine) => { 14 | const files = ['admin', 'api', 'database', 'middlewares', 'plugins', 'server']; 15 | 16 | let paths = {}; 17 | 18 | files.forEach((file) => { 19 | paths[file] = join(configPath, file); 20 | }); 21 | 22 | try { 23 | // create config path 24 | await fs.ensureDir(configPath); 25 | } catch (error) { 26 | logger.error(`an error occurred when creating ${configPath} folder`); 27 | console.log(error); 28 | } 29 | 30 | for (const jsFile in files) { 31 | let template; 32 | 33 | const fileName = files[jsFile]; 34 | 35 | try { 36 | // Compile the template 37 | if (fileName !== 'database') { 38 | template = await liquidEngine.renderFile(`config-${fileName}`); 39 | } else { 40 | template = await liquidEngine.renderFile(`config-${fileName}-${database}`); 41 | } 42 | 43 | // Create the js file 44 | await fs.ensureFile(`${paths[fileName]}.js`); 45 | 46 | // Create write stream for new js file 47 | const file = fs.createWriteStream(`${paths[fileName]}.js`); 48 | 49 | // Export core controllers from liquid template file 50 | file.write(template); 51 | 52 | // Close the write stream 53 | file.end(); 54 | } catch (error) { 55 | logger.error(`an error occurred when migrating ${fileName} config file`); 56 | console.log(error); 57 | } 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/update-package-dependencies.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('axios', () => ({ 2 | get: jest.fn(() => Promise.resolve()), 3 | })); 4 | 5 | const path = require('path'); 6 | const fs = require('fs-extra'); 7 | const axios = require('axios'); 8 | 9 | const updatePackageDependencies = require('../update-package-dependencies'); 10 | 11 | const packageJsonPath = path.resolve( 12 | path.join(__dirname, '..', '__mocks__', 'mock-package-json-v3.json') 13 | ); 14 | 15 | describe('update api folder structure', () => { 16 | beforeEach(() => { 17 | jest.spyOn(console, 'error').mockImplementation(() => {}); 18 | jest.spyOn(console, 'log').mockImplementation(() => {}); 19 | jest.spyOn(process, 'exit').mockImplementation(() => {}); 20 | jest.spyOn(path, 'resolve').mockReturnValueOnce(packageJsonPath); 21 | axios.get.mockReturnValueOnce({ 22 | data: { 23 | 'dist-tags': { 24 | next: '4.0.0-next.20', 25 | latest: '4.0.0', 26 | beta: '4.0.0-beta.22', 27 | }, 28 | }, 29 | }); 30 | }); 31 | 32 | afterEach(() => { 33 | jest.clearAllMocks(); 34 | }); 35 | 36 | it('gets the latest strapi version', async () => { 37 | const testDir = 'test-dir'; 38 | 39 | await updatePackageDependencies(testDir); 40 | 41 | expect(axios.get).toHaveBeenCalled(); 42 | }); 43 | 44 | it('writes the correct v4 dependencies', async () => { 45 | const testDir = 'test-dir'; 46 | const v4PackageJSON = { 47 | dependencies: { 48 | '@strapi/strapi': '4.0.0', 49 | '@strapi/plugin-users-permissions': '4.0.0', 50 | '@strapi/plugin-i18n': '4.0.0', 51 | }, 52 | engines: { 53 | node: '>=14.19.1 <=16.x.x', 54 | npm: '>=6.0.0', 55 | }, 56 | }; 57 | 58 | await updatePackageDependencies(testDir); 59 | 60 | expect(fs.writeJSON).toHaveBeenCalledWith(packageJsonPath, v4PackageJSON, { spaces: 2 }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /lib/v4/utils/strapi-packages.js: -------------------------------------------------------------------------------- 1 | // Match old Strapi packages to their new names on npm 2 | const toBeDeleted = Symbol(); 3 | const toBeRemoved = Symbol(); 4 | 5 | const strapiPackages = { 6 | strapi: '@strapi/strapi', 7 | 'strapi-admin': toBeRemoved, 8 | 'strapi-connector-bookshelf': toBeDeleted, 9 | 'strapi-database': toBeRemoved, 10 | 'strapi-generate': toBeRemoved, 11 | 'strapi-generate-api': toBeRemoved, 12 | 'strapi-generate-controller': toBeRemoved, 13 | 'strapi-generate-model': toBeRemoved, 14 | 'strapi-generate-new': toBeRemoved, 15 | 'strapi-generate-plugin': toBeRemoved, 16 | 'strapi-generate-policy': toBeRemoved, 17 | 'strapi-generate-service': toBeRemoved, 18 | 'strapi-helper-plugin': '@strapi/helper-plugin', 19 | 'strapi-hook-ejs': toBeDeleted, 20 | 'strapi-hook-redis': toBeDeleted, 21 | 'strapi-middleware-views': toBeDeleted, 22 | 'strapi-plugin-content-manager': toBeRemoved, 23 | 'strapi-plugin-content-type-builder': toBeRemoved, 24 | 'strapi-plugin-documentation': '@strapi/plugin-documentation', 25 | 'strapi-plugin-email': toBeRemoved, 26 | 'strapi-plugin-graphql': '@strapi/plugin-graphql', 27 | 'strapi-plugin-i18n': '@strapi/plugin-i18n', 28 | 'strapi-plugin-sentry': '@strapi/plugin-sentry', 29 | 'strapi-plugin-upload': toBeRemoved, 30 | 'strapi-plugin-users-permissions': '@strapi/plugin-users-permissions', 31 | 'strapi-provider-amazon-ses': '@strapi/provider-email-amazon-ses', 32 | 'strapi-provider-email-mailgun': '@strapi/provider-email-mailgun', 33 | 'strapi-provider-email-nodemailer': '@strapi/provider-email-nodemailer', 34 | 'strapi-provider-email-sendgrid': '@strapi/provider-email-sendgrid', 35 | 'strapi-provider-email-sendmail': toBeRemoved, 36 | 'strapi-provider-upload-aws-s3': '@strapi/provider-upload-aws-s3', 37 | 'strapi-provider-upload-cloudinary': '@strapi/provider-upload-cloudinary', 38 | 'strapi-provider-upload-local': toBeRemoved, 39 | 'strapi-provider-upload-rackspace': toBeDeleted, 40 | 'strapi-utils': toBeRemoved, 41 | }; 42 | 43 | module.exports = { strapiPackages, toBeDeleted, toBeRemoved }; 44 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/update-routes.test.js: -------------------------------------------------------------------------------- 1 | const { resolve, join } = require('path'); 2 | const { inspect } = require('util'); 3 | const fs = require('fs-extra'); 4 | 5 | const updateRoutes = require('../update-routes'); 6 | const mockRoutesV3 = require('../__mocks__/mock-routes-v3'); 7 | const mockRoutesV4 = require('../__mocks__/mock-routes-v4'); 8 | 9 | describe('migrate routes to v4', () => { 10 | beforeEach(() => { 11 | jest.spyOn(console, 'error').mockImplementation(() => {}); 12 | fs.readJSON.mockReturnValueOnce(mockRoutesV3); 13 | }); 14 | 15 | afterEach(() => { 16 | jest.clearAllMocks(); 17 | }); 18 | 19 | it('creates a v4 routes file', async () => { 20 | const dirPath = resolve('./test-dir'); 21 | const v4RoutesPath = join('routes', 'index.js'); 22 | 23 | await updateRoutes(dirPath, 'index'); 24 | 25 | const expectedPath = join(dirPath, v4RoutesPath); 26 | expect(fs.ensureFile).toHaveBeenCalledWith(expectedPath); 27 | }); 28 | 29 | it('creates a write stream on the v4 routes file', async () => { 30 | const dirPath = resolve('./test-dir'); 31 | const v4RoutesPath = join('routes', 'index.js'); 32 | 33 | await updateRoutes(dirPath, 'index'); 34 | 35 | const expectedPath = join(dirPath, v4RoutesPath); 36 | expect(fs.createWriteStream).toHaveBeenCalledWith(expectedPath); 37 | }); 38 | 39 | it('gets the v3 routes', async () => { 40 | const dirPath = resolve('./test-dir'); 41 | 42 | await updateRoutes(dirPath, 'index'); 43 | 44 | const expectedV3RoutePath = join(dirPath, 'config', 'routes.json'); 45 | expect(fs.readJSON).toHaveBeenCalledWith(expectedV3RoutePath); 46 | }); 47 | 48 | it('writes valid v4 routes', async () => { 49 | const dirPath = resolve('./test-dir'); 50 | 51 | const writeMock = jest.fn(); 52 | fs.createWriteStream.mockReturnValueOnce({ 53 | write: writeMock, 54 | }); 55 | 56 | await updateRoutes(dirPath, 'index'); 57 | 58 | expect(writeMock).toHaveBeenCalledWith( 59 | `module.exports = ${inspect(mockRoutesV4, { depth: Infinity })}` 60 | ); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-plugin-folder-structure/transforms/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const j = require('jscodeshift'); 3 | const { camelCase } = require('lodash'); 4 | 5 | const { statement } = j.template; 6 | /** 7 | * 8 | * @param {string} filePath 9 | * @param {array} imports 10 | */ 11 | async function importFilesToIndex(filePath, imports) { 12 | const fileContent = await fs.readFile(filePath); 13 | const file = fileContent.toString(); 14 | const root = j(file); 15 | const body = root.find(j.Program).get('body'); 16 | 17 | imports.forEach((fileImport) => { 18 | // Remove extension 19 | const filename = fileImport.replace(/\.[^/.]+$/, ''); 20 | 21 | const declaration = statement`const ${camelCase(filename)} = require(${j.literal( 22 | './' + filename 23 | )});\n`; 24 | 25 | const hasUseStrict = body.get(0).value.directive === 'use strict'; 26 | if (hasUseStrict) { 27 | // When use strict is present add imports after 28 | body.get(0).insertAfter(declaration); 29 | } else { 30 | // Otherwise add them to the top of the file 31 | body.unshift(declaration); 32 | } 33 | }); 34 | 35 | await fs.writeFile(filePath, root.toSource({ quote: 'single' })); 36 | } 37 | 38 | async function addModulesToExport(filePath, modules) { 39 | const fileContent = await fs.readFile(filePath); 40 | const file = fileContent.toString(); 41 | const root = j(file); 42 | const moduleExports = root.find(j.AssignmentExpression, { 43 | left: { 44 | object: { 45 | name: 'module', 46 | }, 47 | property: { 48 | name: 'exports', 49 | }, 50 | }, 51 | }); 52 | 53 | modules.forEach((mod) => { 54 | // Remove extension 55 | const moduleName = mod.replace(/\.[^/.]+$/, ''); 56 | const property = j.property( 57 | 'init', 58 | j.identifier(camelCase(moduleName)), 59 | j.identifier(camelCase(moduleName)) 60 | ); 61 | 62 | moduleExports.get().value.right.properties.push({ ...property, shorthand: true }); 63 | }); 64 | 65 | await fs.writeFile(filePath, root.toSource({ quote: 'single' })); 66 | } 67 | 68 | module.exports = { importFilesToIndex, addModulesToExport }; 69 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/update-api-policies.test.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const updateApiPolicies = require('../update-api-policies'); 5 | 6 | describe('update api policies', () => { 7 | beforeEach(() => { 8 | jest.spyOn(console, 'error').mockImplementation(() => {}); 9 | fs.readdir.mockReturnValueOnce([ 10 | { name: 'test.js', isFile: jest.fn(() => true) }, 11 | { name: 'test-two.js', isFile: jest.fn(() => true) }, 12 | { name: 'test', isFile: jest.fn(() => false) }, 13 | ]); 14 | }); 15 | 16 | afterEach(() => { 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | it('checks for a path to v3 policies', async () => { 21 | fs.exists.mockReturnValueOnce(true); 22 | const dirPath = './test-dir'; 23 | 24 | await updateApiPolicies(dirPath); 25 | 26 | const expectedPath = join(dirPath, 'config', 'policies'); 27 | expect(fs.exists).toHaveBeenCalledWith(expectedPath); 28 | }); 29 | 30 | it('exits when path is not v3 policies', async () => { 31 | fs.exists.mockReturnValueOnce(false); 32 | const dirPath = './test-dir'; 33 | 34 | await updateApiPolicies(dirPath); 35 | 36 | const expectedPath = join(dirPath, 'config', 'policies'); 37 | expect(fs.exists).toHaveBeenCalledWith(expectedPath); 38 | expect(fs.readdir).not.toHaveBeenCalled(); 39 | }); 40 | 41 | it('gets the v3 policies', async () => { 42 | fs.exists.mockReturnValueOnce(true); 43 | const dirPath = './test-dir'; 44 | 45 | await updateApiPolicies(dirPath); 46 | 47 | const expectedPath = join(dirPath, 'config', 'policies'); 48 | expect(fs.readdir).toHaveBeenCalledWith(expectedPath, { 49 | withFileTypes: true, 50 | }); 51 | }); 52 | 53 | it('copies the v3 policies to the correct v4 path', async () => { 54 | fs.exists.mockReturnValueOnce(true); 55 | const dirPath = './test-dir'; 56 | 57 | await updateApiPolicies(dirPath); 58 | 59 | const expectedV3Path = join(dirPath, 'config', 'policies'); 60 | const expectedV4Path = join(dirPath, 'policies'); 61 | expect(fs.copy.mock.calls).toEqual([ 62 | [join(expectedV3Path, 'test.js'), join(expectedV4Path, 'test.js')], 63 | [join(expectedV3Path, 'test-two.js'), join(expectedV4Path, 'test-two.js')], 64 | ]); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const { resolve } = require('path'); 5 | const { Command } = require('commander'); 6 | 7 | const { version } = require('../package.json'); 8 | 9 | const { defaultMigrate, defaultTransform, defaultCommand } = require('./commands/default-commands'); 10 | const migrate = require('./commands/migrate'); 11 | 12 | // Initial program setup 13 | const program = new Command(); 14 | 15 | // `$ strapi-codemods version || strapi-codemods -v || strapi-codemods --version` 16 | program.version(version, '-v, --version', 'Output your version of @strapi/codemods'); 17 | program 18 | .command('version') 19 | .description('Output your version of @strapi/codemods') 20 | .action(() => { 21 | process.stdout.write(version + '\n'); 22 | process.exit(0); 23 | }); 24 | 25 | // `$ strapi-codemods || strapi-codemods default` 26 | program 27 | .command('default', { isDefault: true }) 28 | .description(false) 29 | .action(async () => { 30 | await defaultCommand(); 31 | process.exit(0); 32 | }); 33 | 34 | // `$ strapi-codemods migrate` 35 | program 36 | .command('migrate') 37 | .description('Migrate a v3 Strapi application or plugin to v4') 38 | .action(async () => { 39 | await defaultMigrate(); 40 | }); 41 | 42 | // `$ strapi-codemods migrate:application` 43 | program 44 | .command('migrate:application [path]') 45 | .description('Migrate a v3 Strapi application to v4') 46 | .action(async (path) => { 47 | await migrate('application', path); 48 | }); 49 | 50 | // `$ strapi-codemods migrate:plugin` 51 | program 52 | .command('migrate:plugin [path] [pathForV4]') 53 | .description('Migrate a v3 dependencies to v4') 54 | .action(async (path, pathForV4) => { 55 | const pathForV4Plugin = pathForV4 ? resolve(pathForV4) : `${resolve(path)}-v4`; 56 | 57 | await migrate('plugin', path, pathForV4Plugin); 58 | }); 59 | 60 | // `$ strapi-codemods migrate:dependencies` 61 | program 62 | .command('migrate:dependencies [path]') 63 | .description('Migrate a v3 Strapi plugin to v4') 64 | .action(async (path) => { 65 | await migrate('dependencies', path); 66 | }); 67 | 68 | // `$ strapi-codemods transform` 69 | program 70 | .command('transform [type] [path]') 71 | .description('Transform v3 code in your v4 project') 72 | .action(async (type, path) => { 73 | await defaultTransform(type, path); 74 | }); 75 | 76 | program.parse(process.argv); 77 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/update-plugin-folder-structure/create-directory-index.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../update-plugin-folder-structure/transforms', () => ({ 2 | importFilesToIndex: jest.fn(), 3 | addModulesToExport: jest.fn(), 4 | })); 5 | 6 | const { join } = require('path'); 7 | const fs = require('fs-extra'); 8 | 9 | const { createDirectoryIndex } = require('../../update-plugin-folder-structure/utils'); 10 | const { 11 | importFilesToIndex, 12 | addModulesToExport, 13 | } = require('../../update-plugin-folder-structure/transforms'); 14 | 15 | describe('create-directory-index', () => { 16 | beforeEach(() => { 17 | jest.spyOn(console, 'error').mockImplementation(() => {}); 18 | jest.spyOn(console, 'log').mockImplementation(() => {}); 19 | fs.readdir.mockReturnValueOnce([ 20 | { name: 'test.js', isFile: jest.fn(() => true) }, 21 | { name: 'test-two.js', isFile: jest.fn(() => true) }, 22 | { name: 'index.js', isFile: jest.fn(() => true) }, 23 | ]); 24 | }); 25 | 26 | afterEach(() => { 27 | jest.clearAllMocks(); 28 | }); 29 | 30 | it('checks if the directory exists', async () => { 31 | fs.pathExists.mockReturnValueOnce(true); 32 | const v4PluginPath = 'test-plugin-v4/server/some-api'; 33 | 34 | await createDirectoryIndex(v4PluginPath); 35 | 36 | expect(fs.pathExists).toHaveBeenCalledWith(v4PluginPath); 37 | }); 38 | 39 | it('exits when the directory does not exists', async () => { 40 | fs.pathExists.mockReturnValueOnce(false); 41 | const v4PluginPath = 'test-plugin-v4/server/some-api'; 42 | 43 | await createDirectoryIndex(v4PluginPath); 44 | 45 | expect(fs.pathExists).toHaveBeenCalledWith(v4PluginPath); 46 | expect(fs.copy).not.toHaveBeenCalled(); 47 | }); 48 | 49 | it('creates an index file with exported modules', async () => { 50 | fs.pathExists.mockReturnValueOnce(true); 51 | const v4PluginPath = 'test-plugin-v4'; 52 | 53 | await createDirectoryIndex(v4PluginPath); 54 | 55 | 56 | expect(fs.copy.mock.calls[0]).toEqual([ 57 | join(__dirname, '..', '..', '..', 'utils', 'module-exports.js'), 58 | join(v4PluginPath, 'index.js'), 59 | ]); 60 | expect(importFilesToIndex).toHaveBeenCalledWith(join(v4PluginPath, 'index.js'), [ 61 | 'test.js', 62 | 'test-two.js', 63 | ]); 64 | expect(addModulesToExport).toHaveBeenCalledWith(join(v4PluginPath, 'index.js'), [ 65 | 'test.js', 66 | 'test-two.js', 67 | ]); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | .node_modules 121 | .DS_Store 122 | .vscode 123 | 124 | # migration related 125 | v3/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![@strapi/codemods](./preview.png) 2 | 3 | # @strapi/codemods 4 | 5 | > CLI to help you migrate your Strapi applications & plugins from v3 to v4. 6 | 7 | ## Features 8 | 9 | - Migrate a Strapi application to v4 10 | - Migrate a Strapi plugin to v4 11 | - Migrate a Strapi application or a plugin's dependecies to v4 12 | 13 | ## Getting started 14 | 15 | ### 🖐 Requirements 16 | 17 | Before running any commands, be sure you have initialized a git repository, the working tree is clean, you've pushed your code to GitHub, and you are on a new branch. 18 | 19 | ### 🕹 Usage 20 | 21 | #### Migrate 22 | 23 | _Usage with prompt_ 24 | 25 | ```bash 26 | npx @strapi/codemods migrate 27 | ``` 28 | 29 | The prompt will ask you: 30 | 31 | - What do you want to migrate? 32 | - `Application` (migrate folder structure + dependencies) 33 | - `Plugin` (migrate folder structure + dependencies) 34 | - `Dependencies` (on migrate dependencies) 35 | - Where is the project located? (default: `./`). 36 | - _(plugin only)_ Where do you want to create the v4 plugin 37 | 38 | _Bypass the prompt_ 39 | 40 | To bypass the prompts use one of the following commands: 41 | 42 | - `Application` migration 43 | 44 | ```bash 45 | npx @strapi/codemods migrate:application 46 | ``` 47 | 48 | - `Plugin` migration 49 | 50 | ```bash 51 | npx @strapi/codemods migrate:plugin [pathForV4Plugin] 52 | ``` 53 | 54 | > Note: if no `pathForV4Plugin` is provided it will be created at `-v4` 55 | 56 | - `Dependencies` migration 57 | 58 | ```bash 59 | npx @strapi/codemods migrate:dependencies 60 | ``` 61 | 62 | #### Transform 63 | 64 | :warning: _This command will modify your source code. Be sure you have initialized a git repository, the working tree is clean, you've pushed your code to GitHub, and you are on a new branch._ 65 | 66 | ```bash 67 | npx @strapi/codemods transform 68 | ``` 69 | 70 | The prompt will ask two questions: 71 | 72 | - What kind of transformation you want to perform: 73 | 74 | - `find` -> `findMany`: Change `find` method to `findMany` 75 | 76 | - `strapi-some-package` -> `@strapi/some-package`: Update strapi scoped imports 77 | 78 | - `.models` -> `.contentTypes`: Change model getters to content types 79 | 80 | - `strapi.plugins['some-plugin']` -> `strapi.plugin('some-plugin')`: Update top level plugin getters 81 | 82 | - `strapi.plugin('some-plugin').controllers['some-controller']` -> `strapi.plugin('some-plugin').controller('some-controller')`: Use plugin getters 83 | 84 | - Add arrow function for service export 85 | 86 | - Add strapi to bootstrap function params 87 | 88 | - Where is the file(s) or folder to transform 89 | 90 | Enjoy 🎉 91 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/update-plugin-folder-structure/create-content-type-index.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../update-plugin-folder-structure/transforms', () => ({ 2 | importFilesToIndex: jest.fn(), 3 | addModulesToExport: jest.fn(), 4 | })); 5 | 6 | jest.mock('../../update-plugin-folder-structure/utils/create-directory-index', () => jest.fn()); 7 | 8 | const { join } = require('path'); 9 | const fs = require('fs-extra'); 10 | 11 | const { createContentTypeIndex } = require('../../update-plugin-folder-structure/utils'); 12 | const createDirectoryIndex = require('../../update-plugin-folder-structure/utils/create-directory-index'); 13 | 14 | const { 15 | importFilesToIndex, 16 | addModulesToExport, 17 | } = require('../../update-plugin-folder-structure/transforms'); 18 | 19 | describe('create-content-type-index', () => { 20 | beforeEach(() => { 21 | jest.spyOn(console, 'error').mockImplementation(() => {}); 22 | jest.spyOn(console, 'log').mockImplementation(() => {}); 23 | fs.readdir.mockReturnValueOnce([ 24 | { name: 'test', isDirectory: jest.fn(() => true) }, 25 | { name: 'test-two', isDirectory: jest.fn(() => true) }, 26 | { name: 'index.js', isDirectory: jest.fn(() => false) }, 27 | ]); 28 | }); 29 | 30 | afterEach(() => { 31 | jest.clearAllMocks(); 32 | }); 33 | 34 | it('checks if the directory exists', async () => { 35 | fs.pathExists.mockReturnValueOnce(true); 36 | const v4PluginPath = 'test-plugin-v4'; 37 | 38 | await createContentTypeIndex(v4PluginPath, 'test-dir'); 39 | 40 | expect(fs.pathExists).toHaveBeenCalledWith('test-dir'); 41 | }); 42 | 43 | it('exits when the directory does not exists', async () => { 44 | fs.pathExists.mockReturnValueOnce(false); 45 | const v4PluginPath = 'test-plugin-v4'; 46 | 47 | await createContentTypeIndex(v4PluginPath, 'test-dir'); 48 | 49 | expect(fs.pathExists).toHaveBeenCalledWith('test-dir'); 50 | expect(fs.copy).not.toHaveBeenCalled(); 51 | }); 52 | 53 | it('creates an index file for each content type directory', async () => { 54 | fs.pathExists.mockReturnValueOnce(true); 55 | const v4PluginPath = 'test-plugin-v4'; 56 | 57 | await createContentTypeIndex(v4PluginPath, 'test-dir'); 58 | 59 | expect(fs.copy.mock.calls[0]).toEqual([ 60 | join(__dirname, '..', '..', '..', 'utils', 'module-exports.js'), 61 | join('test-dir', 'index.js'), 62 | ]); 63 | expect(createDirectoryIndex.mock.calls).toEqual([ 64 | [join(v4PluginPath, 'server', 'content-types', 'test')], 65 | [join(v4PluginPath, 'server', 'content-types', 'test-two')], 66 | ]); 67 | expect(importFilesToIndex).toHaveBeenCalledWith(join('test-dir', 'index.js'), ['test', 'test-two']) 68 | expect(addModulesToExport).toHaveBeenCalledWith(join('test-dir', 'index.js'), ['test', 'test-two']) 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-package-dependencies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fse = require('fs-extra'); 5 | const _ = require('lodash'); 6 | const axios = require('axios'); 7 | const chalk = require('chalk'); 8 | const { strapiPackages, toBeDeleted, toBeRemoved } = require('../utils/strapi-packages'); 9 | const { logger } = require('../../global/utils'); 10 | 11 | async function getLatestStrapiVersion() { 12 | try { 13 | const { data } = await axios.get( 14 | `https://registry.npmjs.org/${encodeURIComponent('@strapi/strapi')}` 15 | ); 16 | 17 | return data['dist-tags'].latest; 18 | } catch (error) { 19 | logger.error('Failed to fetch the latest version of Strapi'); 20 | } 21 | } 22 | 23 | const updatePackageDependencies = async (appPath) => { 24 | // Import the app's package.json as an object 25 | const packageJSONPath = path.resolve(appPath, 'package.json'); 26 | let packageJSON; 27 | try { 28 | packageJSON = require(packageJSONPath); 29 | } catch (error) { 30 | logger.error('Could not find a package.json. Are you sure this is a Strapi app?'); 31 | process.exit(1); 32 | } 33 | 34 | if (_.isEmpty(packageJSON.dependencies)) { 35 | logger.error(`${chalk.yellow(appPath)} does not have dependencies`); 36 | process.exit(1); 37 | } 38 | 39 | // Get the latest Strapi release version 40 | const latestStrapiVersion = await getLatestStrapiVersion(); 41 | 42 | // Write all the package JSON changes in a new object 43 | const v4PackageJSON = _.cloneDeep(packageJSON); 44 | Object.keys(packageJSON.dependencies).forEach((depName) => { 45 | const newStrapiDependency = strapiPackages[depName]; 46 | if (newStrapiDependency) { 47 | // The dependency is a v3 Strapi package, remove it 48 | delete v4PackageJSON.dependencies[depName]; 49 | if (newStrapiDependency === toBeDeleted) { 50 | // Warn user if the dependency doesn't exist anymore 51 | logger.warn(`${depName} does not exist anymore in Strapi v4`); 52 | } else if (newStrapiDependency === toBeRemoved) { 53 | // Warn user if the dependency no longer needs to be declared in package.json 54 | logger.info(`${depName} does not need to be defined in package.json`); 55 | } else { 56 | // Replace dependency if there's a matching v4 package 57 | v4PackageJSON.dependencies[newStrapiDependency] = latestStrapiVersion; 58 | } 59 | } else if (depName === 'sqlite3') { 60 | // The dependency is sqlite, switch to new package 61 | delete v4PackageJSON.dependencies[depName]; 62 | v4PackageJSON.dependencies['better-sqlite3'] = '^7.6.2'; 63 | } 64 | }); 65 | 66 | // Update node and npm compatibility 67 | v4PackageJSON.engines.node = '>=14.19.1 <=16.x.x'; 68 | v4PackageJSON.engines.npm = '>=6.0.0'; 69 | 70 | try { 71 | await fse.writeJSON(packageJSONPath, v4PackageJSON, { spaces: 2 }); 72 | } catch (error) { 73 | logger.error(`Failed to update ${chalk.yellow(packageJSONPath)}`); 74 | } 75 | }; 76 | 77 | module.exports = updatePackageDependencies; 78 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-plugin-folder-structure/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { resolve, join } = require('path'); 4 | const fs = require('fs-extra'); 5 | const chalk = require('chalk'); 6 | 7 | const { logger } = require('../../../global/utils'); 8 | const convertModelToContentType = require('../convert-models-to-content-types'); 9 | const updateRoutes = require('../update-routes'); 10 | const runJscodeshift = require('../../utils/run-jscodeshift'); 11 | const { 12 | createDirectoryIndex, 13 | createServerIndex, 14 | createContentTypeIndex, 15 | moveToServer, 16 | moveBootstrapFunction, 17 | } = require('./utils'); 18 | 19 | const SERVER_DIRECTORIES = ['controllers', 'models', 'middlewares', 'services']; 20 | 21 | const migratePlugin = async (v3PluginPath, v4DestinationPath) => { 22 | const v4Plugin = v4DestinationPath ? resolve(v4DestinationPath) : resolve(`${v3PluginPath}-v4`); 23 | 24 | const exists = await fs.pathExists(v4Plugin); 25 | if (exists) { 26 | logger.error(`${chalk.yellow(v4Plugin)} already exists`); 27 | return; 28 | } 29 | 30 | try { 31 | // Create plugin copy 32 | await fs.copy(resolve(v3PluginPath), v4Plugin); 33 | logger.info(`copied v3 plugin to ${chalk.yellow(v4Plugin)}`); 34 | 35 | // Create root strapi-admin 36 | const strapiAdmin = join(v4Plugin, `strapi-admin.js`); 37 | await fs.copy(join(__dirname, '..', '..', 'utils', 'strapi-admin.js'), strapiAdmin); 38 | 39 | logger.info(`created ${chalk.yellow(strapiAdmin)}`); 40 | 41 | // Create root strapi-server 42 | const strapiServer = join(v4Plugin, `strapi-server.js`); 43 | await fs.copy(join(__dirname, '..', '..', 'utils', 'strapi-server.js'), strapiServer); 44 | 45 | logger.info(`created ${chalk.yellow(strapiServer)}`); 46 | 47 | // Move all server files to /server 48 | for (const directory of SERVER_DIRECTORIES) { 49 | await moveToServer(v4Plugin, '.', directory); 50 | // Convert services to function export before creating index file 51 | if (directory === 'services') { 52 | await runJscodeshift( 53 | join(v4Plugin, 'server', 'services'), 54 | 'convert-object-export-to-function' 55 | ); 56 | } 57 | 58 | // Create index file for directory 59 | if (directory === 'models') { 60 | await convertModelToContentType(join(v4Plugin, 'server')); 61 | await createContentTypeIndex(v4Plugin, join(v4Plugin, 'server', 'content-types')); 62 | } else { 63 | await createDirectoryIndex(join(v4Plugin, 'server', directory)); 64 | } 65 | } 66 | 67 | // Move bootstrap to /server/bootstrap.js 68 | await moveBootstrapFunction(v4Plugin); 69 | // Move routes 70 | await updateRoutes(v4Plugin, 'index'); 71 | await moveToServer(v4Plugin, '.', 'routes'); 72 | // Move policies 73 | await moveToServer(v4Plugin, 'config', 'policies'); 74 | await createDirectoryIndex(join(v4Plugin, 'server', 'policies')); 75 | 76 | // Create src/server index 77 | await createServerIndex(join(v4Plugin, 'server')); 78 | logger.success(`finished migrating v3 plugin to v4 at ${chalk.green(v4Plugin)}`); 79 | } catch (error) { 80 | logger.error(error.message); 81 | } 82 | }; 83 | 84 | module.exports = migratePlugin; 85 | -------------------------------------------------------------------------------- /bin/commands/migrate.js: -------------------------------------------------------------------------------- 1 | // Node.js core 2 | const { resolve } = require('path'); 3 | 4 | const fs = require('fs-extra'); 5 | const chalk = require('chalk'); 6 | 7 | // Migration Helpers 8 | const { v4 } = require('../../lib'); 9 | 10 | const { migratePlugin, migrateApiFolder, migrateDependencies, migrateApplicationFolderStructure } = 11 | v4.migrationHelpers; 12 | 13 | // Global utils 14 | const { isPathStrapiApp, logger, isCleanGitRepo, promptUser } = require('../../lib/global/utils'); 15 | 16 | const migrate = async (type, path, pathForV4Plugin) => { 17 | // Check the path exists 18 | const exists = await fs.pathExists(resolve(path)); 19 | if (!exists) { 20 | logger.error(`${chalk.yellow(resolve(path))} does not exist`); 21 | process.exit(1); 22 | } 23 | 24 | try { 25 | switch (type) { 26 | case 'application': 27 | await migrateApplicationToV4(path); 28 | break; 29 | case 'dependencies': 30 | await migrateDependenciesToV4(path); 31 | break; 32 | case 'plugin': 33 | await migratePluginToV4(path, pathForV4Plugin); 34 | break; 35 | } 36 | } catch (error) { 37 | logger.error(error.message); 38 | process.exit(1); 39 | } 40 | }; 41 | 42 | // `strapi-codemods migrate:application` 43 | const migrateApplicationToV4 = async (path) => { 44 | const promptOptions = { 45 | type: 'input', 46 | name: 'path', 47 | message: 'Enter the path to your Strapi application', 48 | when: !path, 49 | }; 50 | 51 | const options = await promptUser(promptOptions); 52 | const projectPath = path || options.path; 53 | 54 | await isCleanGitRepo(projectPath); 55 | await isPathStrapiApp(projectPath); 56 | await migrateDependencies(projectPath); 57 | await migrateApplicationFolderStructure(projectPath); 58 | await migrateApiFolder(projectPath); 59 | }; 60 | 61 | // `strapi-codemods migrate:plugin` 62 | const migratePluginToV4 = async (pathToV3, pathForV4Plugin) => { 63 | const promptOptions = [ 64 | { 65 | type: 'input', 66 | name: 'path', 67 | message: 'Enter the path to your v3 Strapi plugin', 68 | when: !pathToV3, 69 | }, 70 | { 71 | type: 'input', 72 | name: 'pathForV4', 73 | message: 'Where would you like to create your v4 plugin?', 74 | default(answers) { 75 | const path = pathToV3 || answers.pathToV3; 76 | return `${resolve(path)}-v4`; 77 | }, 78 | when: !pathForV4Plugin, 79 | }, 80 | ]; 81 | 82 | const response = await promptUser(promptOptions); 83 | const path = pathToV3 || response.path; 84 | const pathForV4 = pathForV4Plugin || response.pathForV4; 85 | 86 | await isPathStrapiApp(path); 87 | await migratePlugin(path, resolve(pathForV4)); 88 | }; 89 | 90 | // `strapi-codemods migrate:dependencies` 91 | const migrateDependenciesToV4 = async (path) => { 92 | const promptOptions = { 93 | type: 'input', 94 | name: 'path', 95 | message: 'Enter the path to your Strapi application or plugin', 96 | when: !path, 97 | }; 98 | 99 | const response = await promptUser(promptOptions); 100 | const projectPath = path || response.path; 101 | 102 | await isPathStrapiApp(projectPath); 103 | await migrateDependencies(projectPath); 104 | }; 105 | 106 | module.exports = migrate; 107 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/update-api-folder-structure.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../convert-models-to-content-types', () => jest.fn()); 2 | jest.mock('../update-routes', () => jest.fn()); 3 | jest.mock('../update-api-policies', () => jest.fn()); 4 | jest.mock('../../utils/run-jscodeshift', () => jest.fn()); 5 | jest.mock('../update-api-folder-structure/utils', () => ({ 6 | cleanEmptyDirectories: jest.fn(() => Promise.resolve()), 7 | getDirsAtPath: jest.fn(() => [ 8 | { name: 'test', isDirectory: jest.fn(() => true) }, 9 | { name: 'test-two', isDirectory: jest.fn(() => true) }, 10 | ]), 11 | })); 12 | 13 | const { join, resolve } = require('path'); 14 | const fs = require('fs-extra'); 15 | 16 | const updateApiFolderStructure = require('../update-api-folder-structure'); 17 | const updateContentTypes = require('../convert-models-to-content-types'); 18 | const updatePolicies = require('../update-api-policies'); 19 | const runJscodeshift = require('../../utils/run-jscodeshift'); 20 | 21 | describe('update api folder structure', () => { 22 | beforeEach(() => { 23 | jest.spyOn(console, 'error').mockImplementation(() => {}); 24 | jest.spyOn(console, 'log').mockImplementation(() => {}); 25 | }); 26 | 27 | afterEach(() => { 28 | jest.clearAllMocks(); 29 | }); 30 | 31 | it('copies the v3 api to a v3 directory for safe keeping', async () => { 32 | const appPath = 'test-dir'; 33 | 34 | await updateApiFolderStructure(appPath); 35 | 36 | const expectedv3CopyPath = join(resolve(appPath), 'v3', 'api'); 37 | expect(fs.copy).toHaveBeenCalledWith(join(resolve(appPath), 'api'), expectedv3CopyPath); 38 | }); 39 | 40 | it('moves the v3 api to v4 src directory', async () => { 41 | const appPath = 'test-dir'; 42 | 43 | await updateApiFolderStructure(appPath); 44 | 45 | const expectedV4Path = join(resolve(appPath), 'src', 'api'); 46 | 47 | expect(fs.move).toHaveBeenCalledWith(join(resolve(appPath), 'api'), expectedV4Path); 48 | }); 49 | 50 | it('converts models to content types', async () => { 51 | const appPath = 'test-dir'; 52 | 53 | await updateApiFolderStructure(appPath); 54 | 55 | const expectedV4Path = join(resolve(appPath), 'src', 'api'); 56 | const expectedv4ExtensionsPath = join(resolve(appPath), 'src', 'extensions'); 57 | expect(updateContentTypes).toBeCalled(); 58 | expect(updateContentTypes.mock.calls).toEqual([ 59 | [join(expectedv4ExtensionsPath, 'test')], 60 | [join(expectedv4ExtensionsPath, 'test-two')], 61 | [join(expectedV4Path, 'test')], 62 | [join(expectedV4Path, 'test-two')], 63 | ]); 64 | }); 65 | 66 | it('updates policies', async () => { 67 | const appPath = 'test-dir'; 68 | 69 | await updateApiFolderStructure(appPath); 70 | 71 | const expectedV4Path = join(resolve(appPath), 'src', 'api'); 72 | expect(updatePolicies.mock.calls).toEqual([ 73 | [join(expectedV4Path, 'test')], 74 | [join(expectedV4Path, 'test-two')], 75 | ]); 76 | }); 77 | 78 | it('updates services', async () => { 79 | const appPath = 'test-dir'; 80 | 81 | await updateApiFolderStructure(appPath); 82 | 83 | const expectedV4Path = join(resolve(appPath), 'src', 'api'); 84 | expect(runJscodeshift.mock.calls).toEqual([ 85 | [join(expectedV4Path, 'test', 'services'), 'convert-object-export-to-function'], 86 | [join(expectedV4Path, 'test-two', 'services'), 'convert-object-export-to-function'], 87 | ]); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /bin/commands/transform.js: -------------------------------------------------------------------------------- 1 | // jscodeshift executable 2 | const { prompt, registerPrompt } = require('inquirer'); 3 | const runJscodeshift = require('../../lib/v4/utils/run-jscodeshift'); 4 | 5 | // Inquirer engine. 6 | registerPrompt('fuzzypath', require('inquirer-fuzzy-path')); 7 | 8 | // global utils 9 | const { utils } = require('../../lib/global'); 10 | const { logger } = require('../../lib/global/utils'); 11 | 12 | const { formatCode } = utils; 13 | 14 | const fuzzyPathOptions = { 15 | type: 'fuzzypath', 16 | excludePath: (nodePath) => 17 | nodePath.includes('node_modules') || 18 | nodePath.includes('build') || 19 | // Exclude all dot files 20 | nodePath.match(/^\/?(?:\w+\/)*(\.\w+)/), 21 | excludeFilter: (nodePath) => 22 | nodePath.includes('node_modules') || 23 | nodePath.includes('build') || 24 | // Exclude all dot files 25 | nodePath.match(/^\/?(?:\w+\/)*(\.\w+)/), 26 | suggestOnly: false, 27 | }; 28 | 29 | const fuzzyPromptOptions = { 30 | ...fuzzyPathOptions, 31 | name: 'path', 32 | message: 'Enter the path to a file or folder', 33 | itemType: 'any', 34 | }; 35 | 36 | /** 37 | * Prompt's configuration 38 | * choices array value have to be the name of transform file 39 | */ 40 | const promptOptions = [ 41 | { 42 | type: 'list', 43 | name: 'type', 44 | message: 'What kind of transformation do you want to perform?', 45 | choices: [ 46 | { name: 'find -> findMany', value: 'change-find-to-findMany' }, 47 | { 48 | name: 'strapi-some-package -> @strapi/some-package', 49 | value: 'update-strapi-scoped-imports', 50 | }, 51 | { 52 | name: '.models -> .contentTypes', 53 | value: 'change-model-getters-to-content-types', 54 | }, 55 | { 56 | name: "strapi.plugins['some-plugin'] -> strapi.plugin('some-plugin')", 57 | value: 'use-plugin-getters', 58 | }, 59 | { 60 | name: "strapi.plugin('some-plugin').controllers['some-controller'] -> strapi.plugin('some-plugin').controller('some-controller')", 61 | value: 'update-top-level-plugin-getter', 62 | }, 63 | { 64 | name: 'Convert object export to function export', 65 | value: 'convert-object-export-to-function', 66 | }, 67 | { 68 | name: 'Add strapi to bootstrap function params', 69 | value: 'add-strapi-to-bootstrap-params', 70 | }, 71 | ], 72 | }, 73 | fuzzyPromptOptions, 74 | ]; 75 | 76 | // `strapi-codemods transform` 77 | const transform = async (transform, path) => { 78 | try { 79 | let args; 80 | if (transform && path) { 81 | // Use provided arguments 82 | args = { path, type: transform }; 83 | } else if (transform && !path) { 84 | // Use provided transform and ask for path 85 | const response = await prompt(fuzzyPromptOptions); 86 | args = { path: response.path, type: transform }; 87 | } else if (!transform && !path) { 88 | // Ask for everything 89 | args = await prompt(promptOptions); 90 | } 91 | 92 | // execute jscodeshift's Runner 93 | await runJscodeshift(args.path, args.type, { stdio: 'inherit', cwd: process.cwd() }); 94 | 95 | // format code with prettier 96 | await formatCode(args.path); 97 | } catch (error) { 98 | logger.error(error.message); 99 | process.exit(1); 100 | } 101 | }; 102 | 103 | module.exports = transform; 104 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/convert-models-to-content-types.test.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const convertModelsToContentTypes = require('../convert-models-to-content-types'); 5 | 6 | describe('convert models to content types', () => { 7 | beforeEach(() => { 8 | jest.spyOn(console, 'error').mockImplementation(() => {}); 9 | jest.spyOn(console, 'log').mockImplementation(() => {}); 10 | fs.readdir.mockReturnValueOnce([ 11 | { name: 'test.settings.json', isFile: jest.fn(() => true) }, 12 | { name: 'test.js', isFile: jest.fn(() => true) }, 13 | { name: 'test', isFile: jest.fn(() => false) }, 14 | ]); 15 | }); 16 | 17 | afterEach(() => { 18 | jest.clearAllMocks(); 19 | }); 20 | 21 | it('checks for a path to v3 models', async () => { 22 | fs.exists.mockReturnValueOnce(true); 23 | const dirPath = './test-dir'; 24 | 25 | await convertModelsToContentTypes(dirPath); 26 | 27 | expect(fs.exists).toHaveBeenCalledWith(join(dirPath, 'models')); 28 | }); 29 | 30 | it('exits when path is not v3 models', async () => { 31 | fs.exists.mockReturnValueOnce(false); 32 | const dirPath = './test-dir'; 33 | 34 | await convertModelsToContentTypes(dirPath); 35 | 36 | expect(fs.exists).toHaveBeenCalled(); 37 | expect(fs.readdir).not.toHaveBeenCalled(); 38 | }); 39 | 40 | it('gets the v3 models', async () => { 41 | fs.exists.mockReturnValueOnce(true); 42 | const dirPath = './test-dir'; 43 | 44 | await convertModelsToContentTypes(dirPath); 45 | 46 | expect(fs.readdir).toHaveBeenCalledWith(join(dirPath, 'models'), { 47 | withFileTypes: true, 48 | }); 49 | }); 50 | 51 | it('gets the v3 settings.json', async () => { 52 | fs.exists.mockReturnValueOnce(true).mockReturnValueOnce(true); 53 | 54 | const dirPath = './test-dir'; 55 | 56 | await convertModelsToContentTypes(dirPath); 57 | 58 | const expectedPath = join(dirPath, 'models', 'test.settings.json'); 59 | expect(fs.readJSON).toHaveBeenCalledWith(expectedPath); 60 | }); 61 | 62 | it('creates the v4 schema.json', async () => { 63 | fs.exists.mockReturnValueOnce(true).mockReturnValueOnce(true); 64 | 65 | const dirPath = './test-dir'; 66 | 67 | await convertModelsToContentTypes(dirPath); 68 | 69 | const expectedPath = join(dirPath, 'content-types', 'test', 'schema.json'); 70 | expect(fs.ensureFile).toHaveBeenCalledWith(expectedPath); 71 | }); 72 | 73 | it('writes the json with correct info object', async () => { 74 | fs.exists.mockReturnValueOnce(true).mockReturnValueOnce(true); 75 | 76 | const dirPath = './test-dir'; 77 | 78 | await convertModelsToContentTypes(dirPath); 79 | 80 | const expectedPath = join(dirPath, 'content-types', 'test', 'schema.json'); 81 | const expectedInfoObject = { 82 | singularName: 'test', 83 | pluralName: 'tests', 84 | displayName: 'Test', 85 | name: 'test', 86 | }; 87 | expect(fs.writeJSON).toHaveBeenCalledWith( 88 | expectedPath, 89 | { info: expectedInfoObject }, 90 | { 91 | spaces: 2, 92 | } 93 | ); 94 | }); 95 | 96 | it('creates the v4 lifecycles.js', async () => { 97 | const dirPath = './test-dir'; 98 | const v3LifecyclesPath = join(dirPath, 'models', 'test.js'); 99 | 100 | fs.exists 101 | .mockReturnValueOnce(true) 102 | .mockReturnValueOnce(true) 103 | .mockImplementationOnce((path) => path === v3LifecyclesPath); 104 | 105 | await convertModelsToContentTypes(dirPath); 106 | 107 | const expectedV4LifecyclesPath = join(dirPath, 'content-types', 'test', 'lifecycles.js'); 108 | expect(fs.move).toHaveBeenCalledWith(v3LifecyclesPath, expectedV4LifecyclesPath); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/get-relation-object.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('lodash', () => ({ 2 | kebabCase: jest.fn(() => 'destination'), 3 | })); 4 | 5 | jest.mock('pluralize', () => ({ 6 | singular: jest.fn(), 7 | })); 8 | 9 | const getRelationObject = require('../get-relation-object'); 10 | 11 | describe('migrate relations from v3 settings.json to v4 schema.json', () => { 12 | it('migrates oneToOne (oneWay)', () => { 13 | const v3HasOneDestination = { 14 | model: 'destination', 15 | }; 16 | 17 | const v4Migration = getRelationObject('oneToOne', { ...v3HasOneDestination, inversed: false }); 18 | 19 | const v4ExpectedHasOneDestination = { 20 | type: 'relation', 21 | relation: 'oneToOne', 22 | target: 'api::destination.destination', 23 | }; 24 | 25 | expect(v4Migration).toEqual(v4ExpectedHasOneDestination); 26 | }); 27 | 28 | it('migrates oneToOne', () => { 29 | const v3hasAndBelongsToOneDestination = { 30 | model: 'destination', 31 | via: 'origin', 32 | }; 33 | 34 | const v4Migration = getRelationObject('oneToOne', { 35 | ...v3hasAndBelongsToOneDestination, 36 | inversed: true, 37 | }); 38 | 39 | const v4hasAndBelongsToOneDestination = { 40 | type: 'relation', 41 | relation: 'oneToOne', 42 | target: 'api::destination.destination', 43 | inversedBy: 'origin', 44 | }; 45 | 46 | expect(v4Migration).toEqual(v4hasAndBelongsToOneDestination); 47 | }); 48 | 49 | it('migrates oneToMany', () => { 50 | const v3BelongsToManyDestinations = { 51 | collection: 'destination', 52 | via: 'origin', 53 | }; 54 | 55 | const v4Migration = getRelationObject('oneToMany', { 56 | ...v3BelongsToManyDestinations, 57 | inversed: false, 58 | }); 59 | 60 | const v4ExpectedBelongsToManyDestinations = { 61 | type: 'relation', 62 | relation: 'oneToMany', 63 | target: 'api::destination.destination', 64 | mappedBy: 'origin', 65 | }; 66 | 67 | expect(v4Migration).toEqual(v4ExpectedBelongsToManyDestinations); 68 | }); 69 | 70 | it('migrates manyToOne', () => { 71 | const v3HasManyOrigins = { 72 | model: 'destination', 73 | via: 'origins', 74 | }; 75 | 76 | const v4Migration = getRelationObject('manyToOne', { ...v3HasManyOrigins, inversed: true }); 77 | 78 | const v4ExpectedHasManyOrigins = { 79 | type: 'relation', 80 | relation: 'manyToOne', 81 | target: 'api::destination.destination', 82 | inversedBy: 'origins', 83 | }; 84 | 85 | expect(v4Migration).toEqual(v4ExpectedHasManyOrigins); 86 | }); 87 | 88 | it('migrates manyToMany', () => { 89 | const v3HasAndBelongsToManyDestinations = { 90 | collection: 'destination', 91 | via: 'origins', 92 | dominant: true, 93 | }; 94 | 95 | const v4Migration = getRelationObject('manyToMany', { 96 | ...v3HasAndBelongsToManyDestinations, 97 | inversed: v3HasAndBelongsToManyDestinations.dominant, 98 | }); 99 | 100 | const v4ExpectedHasAndBelongsToManyDestinations = { 101 | type: 'relation', 102 | relation: 'manyToMany', 103 | target: 'api::destination.destination', 104 | inversedBy: 'origins', 105 | }; 106 | 107 | expect(v4Migration).toEqual(v4ExpectedHasAndBelongsToManyDestinations); 108 | }); 109 | 110 | it('migrates oneToMany', () => { 111 | const v3HasManyDestinations = { 112 | collection: 'destination', 113 | }; 114 | 115 | const v4Migration = getRelationObject('oneToMany', { 116 | ...v3HasManyDestinations.collection, 117 | inversed: false, 118 | }); 119 | 120 | const v4ExpectedHasManyDestinations = { 121 | type: 'relation', 122 | relation: 'oneToMany', 123 | target: 'api::destination.destination', 124 | }; 125 | 126 | expect(v4Migration).toEqual(v4ExpectedHasManyDestinations); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/__tests__/update-plugin-folder-structure/index.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../update-plugin-folder-structure/utils', () => ({ 2 | createDirectoryIndex: jest.fn(() => Promise.resolve()), 3 | createServerIndex: jest.fn(() => Promise.resolve()), 4 | createContentTypeIndex: jest.fn(() => Promise.resolve()), 5 | moveToServer: jest.fn(() => Promise.resolve()), 6 | moveBootstrapFunction: jest.fn(() => Promise.resolve()), 7 | })); 8 | 9 | jest.mock('../../convert-models-to-content-types', () => jest.fn()); 10 | jest.mock('../../../utils/run-jscodeshift', () => jest.fn()); 11 | 12 | const { join, resolve } = require('path'); 13 | const fs = require('fs-extra'); 14 | 15 | const updatePluginFolderStructure = require('../../update-plugin-folder-structure'); 16 | const convertModelsToContentTypes = require('../../convert-models-to-content-types'); 17 | const utils = require('../../update-plugin-folder-structure/utils'); 18 | const runJscodeshift = require('../../../utils/run-jscodeshift'); 19 | 20 | describe('update plugin folder structure', () => { 21 | beforeEach(() => { 22 | jest.spyOn(console, 'error').mockImplementation(() => {}); 23 | jest.spyOn(console, 'log').mockImplementation(() => {}); 24 | }); 25 | 26 | afterEach(() => { 27 | jest.clearAllMocks(); 28 | }); 29 | 30 | it('checks if the path for v4 plugin already exists', async () => { 31 | fs.pathExists.mockReturnValueOnce(false); 32 | const dirPath = resolve('./test-dir'); 33 | 34 | await updatePluginFolderStructure(dirPath); 35 | 36 | expect(fs.pathExists).toHaveBeenCalledWith(`${dirPath}-v4`); 37 | }); 38 | 39 | it('exits when the v4 plugin path already exists', async () => { 40 | fs.pathExists.mockReturnValueOnce(true); 41 | const dirPath = resolve('./test-dir'); 42 | 43 | await updatePluginFolderStructure(dirPath); 44 | 45 | expect(fs.pathExists).toHaveBeenCalledWith(`${dirPath}-v4`); 46 | expect(fs.readdir).not.toHaveBeenCalled(); 47 | }); 48 | 49 | it('creates strapi-admin.js and strapi-server.js', async () => { 50 | fs.pathExists.mockReturnValueOnce(false); 51 | const dirPath = resolve('./test-dir'); 52 | 53 | await updatePluginFolderStructure(dirPath); 54 | 55 | expect(fs.copy.mock.calls[1]).toEqual([ 56 | join(__dirname, '..', '..', '..', 'utils', 'strapi-admin.js'), 57 | join(`${dirPath}-v4`, 'strapi-admin.js'), 58 | ]); 59 | expect(fs.copy.mock.calls[2]).toEqual([ 60 | join(__dirname, '..', '..', '..', 'utils', 'strapi-server.js'), 61 | join(`${dirPath}-v4`, 'strapi-server.js'), 62 | ]); 63 | }); 64 | 65 | it.each(['controllers', 'models', 'middlewares', 'services', 'policies', 'routes', 'bootstrap'])( 66 | 'moves %s to server directory', 67 | async (directory) => { 68 | fs.pathExists.mockReturnValueOnce(false).mockReturnValue(true); 69 | const dirPath = resolve('./test-dir'); 70 | 71 | await updatePluginFolderStructure(dirPath); 72 | 73 | switch (directory) { 74 | case 'routes': { 75 | expect(utils.moveToServer).toHaveBeenCalledWith(`${dirPath}-v4`, '.', directory); 76 | break; 77 | } 78 | case 'policies': { 79 | expect(utils.moveToServer).toHaveBeenCalledWith(`${dirPath}-v4`, 'config', directory); 80 | break; 81 | } 82 | case 'bootstrap': { 83 | expect(utils.moveBootstrapFunction).toHaveBeenCalledWith(`${dirPath}-v4`); 84 | break; 85 | } 86 | case 'services': { 87 | expect(utils.moveToServer).toHaveBeenCalledWith(join(`${dirPath}-v4`), '.', directory); 88 | expect(runJscodeshift).toHaveBeenCalledWith( 89 | join(`${dirPath}-v4`, 'server', 'services'), 90 | 'convert-object-export-to-function' 91 | ); 92 | break; 93 | } 94 | case 'models': { 95 | expect(utils.moveToServer).toHaveBeenCalledWith(join(`${dirPath}-v4`), '.', directory); 96 | expect(convertModelsToContentTypes).toHaveBeenCalledWith(join(`${dirPath}-v4`, 'server')); 97 | break; 98 | } 99 | default: { 100 | expect(utils.moveToServer).toHaveBeenCalledWith(join(`${dirPath}-v4`), '.', directory); 101 | } 102 | } 103 | } 104 | ); 105 | }); 106 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/convert-components.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Migrate components to v4 structure 3 | */ 4 | const fs = require('fs-extra'); 5 | const _ = require('lodash'); 6 | const pluralize = require('pluralize'); 7 | 8 | const { isPlural, isSingular } = pluralize; 9 | const logger = require('../../global/utils/logger'); 10 | const getRelationObject = require('./get-relation-object'); 11 | 12 | /** 13 | * @description Migrates component json to v4 structure with nested relations 14 | * 15 | * @param {string} componentsPath Path to the components folder 16 | * @param {string} componentCategory Name of the current component's category 17 | * @param {string} componentName Name of the current component 18 | */ 19 | const convertComponent = async (componentsPath, componentCategory, componentName) => { 20 | const componentExists = await fs.exists(componentsPath); 21 | if (!componentExists) { 22 | logger.error(`${componentCategory}/${componentName}.json does not exist`); 23 | return; 24 | } 25 | 26 | try { 27 | // Read the component.json file 28 | const componentJson = await fs.readJSON(componentsPath); 29 | if (componentJson.info.name) { 30 | componentJson.info.displayName = componentJson.info.name; 31 | delete componentJson.info.name; 32 | } 33 | 34 | if (componentJson.attributes) { 35 | Object.entries(componentJson.attributes).forEach(([key, attribute]) => { 36 | // Not a relation, return early 37 | if (!attribute.via && !attribute.collection && !attribute.model) return; 38 | 39 | if ( 40 | attribute.plugin === 'upload' && 41 | (attribute.model === 'file' || attribute.collection === 'file') 42 | ) { 43 | // Handle the Media Plugin 44 | attribute = { 45 | type: 'media', 46 | allowedTypes: attribute.allowedTypes, 47 | multiple: _.has(attribute, 'collection'), 48 | required: attribute.required, 49 | private: attribute.private, 50 | }; 51 | } else if ( 52 | attribute.plugin === 'admin' && 53 | (attribute.model === 'user' || attribute.collection === 'user') 54 | ) { 55 | // Handle admin user relation 56 | attribute = { 57 | type: 'relation', 58 | target: 'admin::user', 59 | relation: _.has(attribute, 'collection') ? 'oneToMany' : 'oneToOne', 60 | }; 61 | 62 | if (attribute.private) { 63 | attribute.private = true; 64 | } 65 | } else if (attribute.via && attribute.model && isSingular(attribute.via)) { 66 | // One-To-One 67 | attribute = getRelationObject('oneToOne', { ...attribute, inversed: true }); 68 | } else if (attribute.model && !attribute.via && !attribute.collection) { 69 | // One-To-One (One-Way) 70 | attribute = getRelationObject('oneToOne', { ...attribute, inversed: false }); 71 | } else if (attribute.via && attribute.model && isPlural(attribute.via)) { 72 | // Many-To-One 73 | attribute = getRelationObject('manyToOne', { ...attribute, inversed: true }); 74 | } else if (attribute.via && attribute.collection && isPlural(attribute.via)) { 75 | // Many-To-Many 76 | attribute = getRelationObject('manyToMany', { 77 | ...attribute, 78 | inversed: attribute.dominant, 79 | }); 80 | } else if (attribute.collection && !attribute.via && !attribute.model) { 81 | // Many-Way 82 | attribute = getRelationObject('oneToMany', attribute); 83 | } else if (attribute.via && attribute.collection) { 84 | // One-To-Many 85 | attribute = getRelationObject('oneToMany', { ...attribute, inversed: false }); 86 | } else { 87 | logger.warn(`unknown relation type, please fix manually: ${key}`); 88 | } 89 | 90 | _.set(componentJson, `attributes.${key}`, attribute); 91 | }); 92 | } 93 | 94 | // Ensure the component.json file exists 95 | await fs.ensureFile(componentsPath); 96 | // Write modified JSON to component.sjon 97 | await fs.writeJSON(componentsPath, componentJson, { 98 | spaces: 2, 99 | }); 100 | } catch (error) { 101 | logger.error( 102 | `an error occurred when trying to migrate ${componentCategory}/${componentName}.json` 103 | ); 104 | } 105 | }; 106 | 107 | module.exports = convertComponent; -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-api-folder-structure/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Migrate API folder structure to v4 3 | */ 4 | 5 | const { resolve, join, basename } = require('path'); 6 | const fs = require('fs-extra'); 7 | const _ = require('lodash'); 8 | const chalk = require('chalk'); 9 | const { Liquid } = require('liquidjs'); 10 | 11 | const pluralize = require('pluralize'); 12 | const runJscodeshift = require('../../utils/run-jscodeshift'); 13 | const { logger } = require('../../../global/utils'); 14 | const updateComponents = require('../convert-components'); 15 | const updateContentTypes = require('../convert-models-to-content-types'); 16 | const updateRoutes = require('../update-application-routes'); 17 | const updateControllers = require('../update-application-controllers'); 18 | const updateServices = require('../update-application-services'); 19 | const updatePolicies = require('../update-api-policies'); 20 | const renameApiFilesToSingular = require('../rename-api-files-to-singular'); 21 | const { getDirsAtPath, getFilesAtPath, cleanEmptyDirectories } = require('./utils'); 22 | 23 | const liquidEngine = new Liquid({ 24 | root: resolve(__dirname, 'templates'), 25 | extname: '.liquid', 26 | }); 27 | 28 | const updateApiFolderStructure = async (appPath) => { 29 | const strapiAppPath = resolve(appPath); 30 | const apiDirCopyPath = join(strapiAppPath, 'src', 'api'); 31 | const componentDirCopyPath = join(strapiAppPath, 'src', 'components'); 32 | 33 | try { 34 | // Copy the api folder to the v3 folder for safe keeping 35 | await fs.copy(join(strapiAppPath, 'api'), join(strapiAppPath, 'v3', 'api')); 36 | // Move the original api folder to src/api 37 | await fs.move(join(strapiAppPath, 'api'), apiDirCopyPath); 38 | } catch (error) { 39 | logger.error( 40 | `${basename(strapiAppPath)}/api or ${basename( 41 | strapiAppPath 42 | )}/config not found, are you sure this is a Strapi app?` 43 | ); 44 | process.exit(1); 45 | } 46 | 47 | try { 48 | // Copy the components folder to the v3 folder for safe keeping 49 | await fs.copy(join(strapiAppPath, 'components'), join(strapiAppPath, 'v3', 'components')); 50 | // Move the original components folder to src/components 51 | await fs.move(join(strapiAppPath, 'components'), componentDirCopyPath); 52 | } catch (error) { 53 | logger.warn(`${basename(strapiAppPath)}/components not found, skipping`); 54 | } 55 | 56 | // Migrate extensions 57 | const extensionPath = join(strapiAppPath, 'src', 'extensions'); 58 | const extensionDirs = await getDirsAtPath(extensionPath); 59 | for (const extension of extensionDirs) { 60 | await updateContentTypes(join(extensionPath, extension.name)); 61 | } 62 | 63 | // Migrate Components 64 | if (await fs.exists(componentDirCopyPath)) { 65 | const componentCategoryDirs = await getDirsAtPath(componentDirCopyPath); 66 | for (const category of componentCategoryDirs) { 67 | const componentFiles = await getFilesAtPath(join(componentDirCopyPath, category.name)); 68 | for (const component of componentFiles) { 69 | let componentName = component.name.replace('.json', ''); 70 | let componentPath = join(componentDirCopyPath, category.name, component.name); 71 | 72 | await updateComponents(componentPath, category.name, componentName); 73 | } 74 | } 75 | } else { 76 | logger.warn('No components found, skipping'); 77 | } 78 | 79 | // Migrate Content Types 80 | const apiDirs = await getDirsAtPath(apiDirCopyPath); 81 | for (const api of apiDirs) { 82 | let apiSingularName = pluralize.singular(_.kebabCase(api.name)); 83 | if (apiSingularName !== api.name) { 84 | await renameApiFilesToSingular(apiDirCopyPath, api.name, apiSingularName); 85 | } 86 | const apiPath = join(apiDirCopyPath, apiSingularName); 87 | await updateContentTypes(apiPath); 88 | await updateRoutes(apiPath, apiSingularName, liquidEngine); 89 | await updateControllers(apiPath, apiSingularName, liquidEngine); 90 | await updateServices(apiPath, apiSingularName, liquidEngine); 91 | await updatePolicies(apiPath); 92 | // Update services using jscodeshift transform 93 | await runJscodeshift( 94 | join(apiDirCopyPath, apiSingularName, 'services'), 95 | 'convert-object-export-to-function' 96 | ); 97 | } 98 | logger.info(`migrated ${chalk.yellow(basename(strapiAppPath))} to Strapi v4 🚀`); 99 | logger.warn('Custom config, controllers, services, and routes were not migrated'); 100 | logger.warn('These will need to be updated manually'); 101 | logger.info(`to see changes: Run ${chalk.yellow('git add . && git diff --cached')}`); 102 | logger.info( 103 | `to revert: ${chalk.green('git')} reset HEAD --hard && ${chalk.green('git')} clean -xdf` 104 | ); 105 | logger.info(`to accept: ${chalk.green('git')} commit -am "migrate API to v4 structure"`); 106 | 107 | const dirsWithSingularNames = await getDirsAtPath(apiDirCopyPath); 108 | await cleanEmptyDirectories(dirsWithSingularNames, apiDirCopyPath); 109 | }; 110 | 111 | module.exports = updateApiFolderStructure; 112 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/convert-models-to-content-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Migrate API folder structure to v4 3 | */ 4 | const { join } = require('path'); 5 | const fs = require('fs-extra'); 6 | const _ = require('lodash'); 7 | const pluralize = require('pluralize'); 8 | 9 | const { isPlural, isSingular } = pluralize; 10 | const logger = require('../../global/utils/logger'); 11 | const getRelationObject = require('./get-relation-object'); 12 | 13 | /** 14 | * @description Migrates settings.json to schema.json 15 | * 16 | * @param {string} apiPath Path to the current api 17 | * @param {string} contentTypeName Name of the current contentType 18 | */ 19 | const convertModelToContentType = async (apiPath, contentTypeName) => { 20 | const settingsJsonPath = join(apiPath, 'models', `${contentTypeName}.settings.json`); 21 | 22 | const settingsExists = await fs.exists(settingsJsonPath); 23 | if (!settingsExists) { 24 | logger.error(`${contentTypeName}.settings.json does not exist`); 25 | return; 26 | } 27 | 28 | const v4SchemaJsonPath = join(apiPath, 'content-types', contentTypeName, 'schema.json'); 29 | 30 | try { 31 | // Read the settings.json file 32 | const settingsJson = await fs.readJSON(settingsJsonPath); 33 | // Create a copy 34 | const schemaJson = { ...settingsJson }; 35 | const infoUpdate = { 36 | singularName: _.kebabCase(pluralize.singular(contentTypeName)), 37 | pluralName: _.kebabCase(pluralize(contentTypeName)), 38 | displayName: _.upperFirst(contentTypeName), 39 | name: contentTypeName, 40 | }; 41 | 42 | if ( 43 | schemaJson.collectionName === 'users-permissions_user' || 44 | schemaJson.collectionName === 'users-permissions_permission' || 45 | schemaJson.collectionName === 'users-permissions_role' 46 | ) { 47 | let newPrefix = schemaJson.collectionName.replace('users-permissions_', 'up_'); 48 | schemaJson.collectionName = pluralize(newPrefix); 49 | } 50 | 51 | // Modify the JSON 52 | _.set(schemaJson, 'info', infoUpdate); 53 | 54 | if (schemaJson.attributes) { 55 | Object.entries(schemaJson.attributes).forEach(([key, attribute]) => { 56 | // Not a relation, return early 57 | if (!attribute.via && !attribute.collection && !attribute.model) return; 58 | 59 | if ( 60 | attribute.plugin === 'upload' && 61 | (attribute.model === 'file' || attribute.collection === 'file') 62 | ) { 63 | // Handle the Media Plugin 64 | attribute = { 65 | type: 'media', 66 | allowedTypes: attribute.allowedTypes, 67 | multiple: _.has(attribute, 'collection'), 68 | required: attribute.required, 69 | private: attribute.private, 70 | }; 71 | } else if ( 72 | attribute.plugin === 'admin' && 73 | (attribute.model === 'user' || attribute.collection === 'user') 74 | ) { 75 | // Handle admin user relation 76 | attribute = { 77 | type: 'relation', 78 | target: 'admin::user', 79 | relation: _.has(attribute, 'collection') ? 'oneToMany' : 'oneToOne', 80 | }; 81 | 82 | if (attribute.private) { 83 | attribute.private = true; 84 | } 85 | } else if (attribute.via && attribute.model && isSingular(attribute.via)) { 86 | // One-To-One 87 | attribute = getRelationObject('oneToOne', { ...attribute, inversed: true }); 88 | } else if (attribute.model && !attribute.via && !attribute.collection) { 89 | // One-To-One (One-Way) 90 | attribute = getRelationObject('oneToOne', { ...attribute, inversed: false }); 91 | } else if (attribute.via && attribute.model && isPlural(attribute.via)) { 92 | // Many-To-One 93 | attribute = getRelationObject('manyToOne', { ...attribute, inversed: true }); 94 | } else if (attribute.via && attribute.collection && isPlural(attribute.via)) { 95 | // Many-To-Many 96 | attribute = getRelationObject('manyToMany', { 97 | ...attribute, 98 | inversed: attribute.dominant, 99 | }); 100 | } else if (attribute.collection && !attribute.via && !attribute.model) { 101 | // Many-Way 102 | attribute = getRelationObject('oneToMany', attribute); 103 | } else if (attribute.via && attribute.collection) { 104 | // One-To-Many 105 | attribute = getRelationObject('oneToMany', { ...attribute, inversed: false }); 106 | } else { 107 | logger.warn(`unknown relation type, please fix manually: ${key}`); 108 | } 109 | 110 | _.set(schemaJson, `attributes.${key}`, attribute); 111 | }); 112 | } 113 | 114 | // Create the new content-types/api/schema.json file 115 | await fs.ensureFile(v4SchemaJsonPath); 116 | // Write modified JSON to schema.json 117 | await fs.writeJSON(v4SchemaJsonPath, schemaJson, { 118 | spaces: 2, 119 | }); 120 | } catch (error) { 121 | logger.error( 122 | `an error occured when migrating the model at ${settingsJsonPath} to a contentType at ${v4SchemaJsonPath} ` 123 | ); 124 | } 125 | 126 | const lifecyclePath = join(apiPath, 'models', `${contentTypeName}.js`); 127 | const lifecyclesExist = await fs.exists(lifecyclePath); 128 | 129 | const v4LifecyclesPath = join(apiPath, 'content-types', contentTypeName, 'lifecycles.js'); 130 | 131 | if (lifecyclesExist) { 132 | try { 133 | await fs.move(lifecyclePath, v4LifecyclesPath); 134 | } catch (error) { 135 | logger.error(`failed to migrate lifecycles from ${lifecyclePath} to ${v4LifecyclesPath}`); 136 | } 137 | } else { 138 | logger.info(`will not create lifecycles since ${contentTypeName}.js was not found`); 139 | } 140 | }; 141 | 142 | /** 143 | * 144 | * @param {string} apiPath Path to the current API 145 | */ 146 | const updateContentTypes = async (apiPath) => { 147 | const exists = await fs.exists(join(apiPath, 'models')); 148 | 149 | if (!exists) return; 150 | 151 | const allModels = await fs.readdir(join(apiPath, 'models'), { 152 | withFileTypes: true, 153 | }); 154 | 155 | const allModelFiles = allModels.filter((f) => f.isFile() && f.name.includes('settings')); 156 | 157 | if (!allModelFiles.length) { 158 | await fs.remove(join(apiPath, 'models')); 159 | } 160 | 161 | for (const model of allModelFiles) { 162 | const [contentTypeName] = model.name.split('.'); 163 | await convertModelToContentType(apiPath, contentTypeName); 164 | } 165 | 166 | // all models have been deleted, remove the directory 167 | await fs.remove(join(apiPath, 'models')); 168 | }; 169 | 170 | module.exports = updateContentTypes; 171 | -------------------------------------------------------------------------------- /lib/v4/migration-helpers/update-application-folder-structure.js: -------------------------------------------------------------------------------- 1 | const { resolve, join, basename } = require('path'); 2 | const crypto = require('crypto'); 3 | const dotenv = require('dotenv'); 4 | const fs = require('fs-extra'); 5 | const { Liquid } = require('liquidjs'); 6 | const { logger } = require('../../global/utils'); 7 | 8 | const generateConfig = require('./generate-application-config'); 9 | 10 | const liquidEngine = new Liquid({ 11 | root: resolve(__dirname, 'templates'), 12 | extname: '.liquid', 13 | }); 14 | 15 | const moveDir = async (appPath, folder, srcDir) => { 16 | const destination = 17 | folder === 'admin' ? `${join(srcDir, folder)}/extensions` : join(srcDir, folder); 18 | 19 | try { 20 | await fs.copy(join(appPath, folder), join('v3', folder)); 21 | await fs.move(join(appPath, folder), destination); 22 | } catch (error) { 23 | logger.warn(`${basename(appPath)}/${folder} not found, skipping...`); 24 | } 25 | }; 26 | 27 | // Generate secrets 28 | const generateASecret = () => crypto.randomBytes(16).toString('base64'); 29 | 30 | module.exports = async (appPath) => { 31 | const strapiAppPath = resolve(appPath); 32 | const srcDir = join(strapiAppPath, 'src'); 33 | const adminPath = join(srcDir, 'admin'); 34 | const configPath = join(strapiAppPath, 'config'); 35 | const envPath = join(strapiAppPath, '.env'); 36 | let databaseType; 37 | let databasePort; 38 | 39 | // Do some cleanup on the application folder 40 | moveDir(strapiAppPath, 'admin', srcDir); 41 | moveDir(strapiAppPath, 'extensions', srcDir); 42 | moveDir(strapiAppPath, 'middlewares', srcDir); 43 | moveDir(strapiAppPath, 'plugins', srcDir); 44 | moveDir(join(strapiAppPath, 'config'), 'policies', srcDir); 45 | 46 | // Check and generate admin example files if needed 47 | try { 48 | // File paths 49 | const appExamplePath = join(adminPath, 'app.example.js'); 50 | const webpackExamplePath = join(adminPath, 'webpack.config.example.js'); 51 | 52 | // Check if folder exists and create it if it doesn't 53 | await fs.ensureDir(adminPath); 54 | 55 | // Load the templates 56 | const adminAppExample = await liquidEngine.renderFile(`src-app`); 57 | const adminWebpackExample = await liquidEngine.renderFile(`src-webpack`); 58 | const indexExample = await liquidEngine.renderFile(`src-index`); 59 | 60 | // Create the js file 61 | await fs.ensureFile(appExamplePath); 62 | await fs.ensureFile(webpackExamplePath); 63 | await fs.ensureFile(join(srcDir, 'index.js')); 64 | 65 | // Create write stream for new js file 66 | const appExampleFile = fs.createWriteStream(appExamplePath); 67 | const webpackExampleFile = fs.createWriteStream(webpackExamplePath); 68 | const indexExampleFile = fs.createWriteStream(join(srcDir, 'index.js')); 69 | 70 | // Export core controllers from liquid template file 71 | appExampleFile.write(adminAppExample); 72 | webpackExampleFile.write(adminWebpackExample); 73 | indexExampleFile.write(indexExample); 74 | 75 | // Close the write stream 76 | appExampleFile.end(); 77 | webpackExampleFile.end(); 78 | indexExampleFile.end(); 79 | } catch (error) { 80 | logger.error(`an error occurred when creating ./src/admin or ./src/index.js example files`); 81 | console.log(error); 82 | } 83 | 84 | // Check if config exists 85 | const configExists = await fs.pathExists(configPath); 86 | 87 | // If config exists, determine database type and move config to v3 folder 88 | if (configExists) { 89 | // Determine database type 90 | const v3DatabaseConfig = require(join(strapiAppPath, 'config', 'database.js')).toString(); 91 | 92 | // Set database type based on existing config 93 | if (v3DatabaseConfig.search(/sqlite/) > -1) { 94 | databaseType = 'sqlite'; 95 | } else if (v3DatabaseConfig.search(/mysql/) > -1) { 96 | databaseType = 'mysql'; 97 | databasePort = 3306; 98 | } else if (v3DatabaseConfig.search(/postgres/) > -1) { 99 | databaseType = 'postgres'; 100 | databasePort = 5432; 101 | } else { 102 | logger.error(`unable to determine database type, please update config/database.js`); 103 | process.exit(1); 104 | } 105 | 106 | // Move existing config to v3 folder for safe keeping 107 | await fs.move(configPath, join(strapiAppPath, 'v3', 'config')); 108 | } else { 109 | // Set default migration database type 110 | databaseType = 'sqlite'; 111 | } 112 | 113 | // Ensure config folder exists 114 | await fs.ensureDir(configPath); 115 | 116 | // Generate new config based on v4 structure 117 | await generateConfig(configPath, databaseType, liquidEngine); 118 | 119 | // Check if .env exists 120 | const envExists = await fs.pathExists(envPath); 121 | 122 | // Load v3 .env if exists or create new .env 123 | if (envExists) { 124 | try { 125 | dotenv.config({ path: envPath }); 126 | await fs.move(envPath, join(strapiAppPath, 'v3', '.env')); 127 | await fs.ensureFile(envPath); 128 | } catch (e) { 129 | // Do nothing 130 | } 131 | } else { 132 | logger.warn('No existing .env detected, skipping...'); 133 | await fs.ensureFile(envPath); 134 | } 135 | 136 | // Create write stream for new .env file 137 | const envFile = fs.createWriteStream(envPath); 138 | 139 | // Write new .env file 140 | try { 141 | envFile.write( 142 | await liquidEngine.renderFile(`app-env`, { 143 | databaseType, 144 | host: process.env.HOST || '0.0.0.0', 145 | port: process.env.PORT || 1337, 146 | appKeys: process.env.APP_KEYS || new Array(4).fill().map(generateASecret).join(','), 147 | apiTokenSalt: process.env.API_TOKEN_SALT || generateASecret(), 148 | adminJwtSecret: process.env.ADMIN_JWT_SECRET || generateASecret(), 149 | jwtSecret: process.env.JWT_SECRET || generateASecret(), 150 | databaseHost: process.env.DATABASE_HOST || 'localhost', 151 | databasePort, 152 | databaseName: process.env.DATABASE_NAME || 'strapi', 153 | databaseUsername: process.env.DATABASE_USERNAME || 'strapi', 154 | databasePassword: process.env.DATABASE_PASSWORD || 'strapi', 155 | databaseSsl: process.env.DATABASE_SSL || 'false', 156 | databaseSchema: process.env.DATABASE_SCHEMA || 'public', 157 | databaseFilename: process.env.DATABASE_FILENAME || '.tmp/data.db', 158 | }) 159 | ); 160 | logger.warn('======================'); 161 | logger.warn( 162 | 'Please update your .env file with the new variables and do not attempt to run Strapi until this is complete or you may have data loss.' 163 | ); 164 | logger.warn('======================'); 165 | } catch (error) { 166 | logger.error(`an error occurred when creating .env file`); 167 | console.log(error); 168 | } 169 | 170 | // Close the write stream 171 | envFile.end(); 172 | }; 173 | --------------------------------------------------------------------------------