From a9552a70bf44abf3e4aa8173f6e36c62c3063170 Mon Sep 17 00:00:00 2001 From: Jamie Howard <48524071+jhoward1994@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:10:45 +0100 Subject: [PATCH] [Email] Migrate to typescript (#18136) Co-authored-by: Josh <37798644+joshuaellis@users.noreply.github.com> --- .../core/admin/admin/src/utils/createRoute.js | 5 +- packages/core/email/.editorconfig | 12 +- packages/core/email/.eslintignore | 3 +- packages/core/email/.eslintrc.js | 2 +- packages/core/email/admin/.eslintrc.js | 4 + .../admin/src/{constants.js => constants.ts} | 0 .../email/admin/src/{index.js => index.ts} | 18 +- .../core/email/admin/src/pages/Settings.tsx | 324 ++++++++++++++++++ .../email/admin/src/pages/Settings/index.js | 314 ----------------- .../tests/{plural.test.js => plural.test.ts} | 8 +- .../admin/src/utils/{schema.js => schema.ts} | 4 +- packages/core/email/admin/tsconfig.json | 8 + packages/core/email/package.json | 36 +- packages/core/email/packup.config.ts | 27 ++ packages/core/email/server/.eslintrc.js | 7 + packages/core/email/server/bootstrap.js | 43 --- .../core/email/server/controllers/email.js | 68 ---- .../core/email/server/controllers/index.js | 7 - packages/core/email/server/routes/index.js | 6 - packages/core/email/server/services/index.js | 7 - packages/core/email/server/src/bootstrap.ts | 57 +++ .../email/server/{config.js => src/config.ts} | 4 +- .../email/server/src/controllers/email.ts | 77 +++++ .../email/server/src/controllers/index.ts | 3 + packages/core/email/server/src/index.ts | 13 + .../{routes/admin.js => src/routes/admin.ts} | 4 +- .../routes/content-api.ts} | 4 +- .../core/email/server/src/routes/index.ts | 7 + .../email.js => src/services/email.ts} | 38 +- .../core/email/server/src/services/index.ts | 3 + packages/core/email/server/src/types.ts | 43 +++ .../core/email/server/tsconfig.build.json | 9 + .../email/server}/tsconfig.eslint.json | 3 + packages/core/email/server/tsconfig.json | 8 + packages/core/email/shared/types.ts | 11 + packages/core/email/strapi-admin.js | 3 - packages/core/email/strapi-server.js | 16 +- packages/core/email/tsconfig.build.json | 8 + .../src/components/CheckPagePermissions.tsx | 9 +- .../src/components/CheckPermissions.tsx | 6 +- .../strapi/src/core/loaders/plugins/index.ts | 20 +- .../core/plugins/config/strapi-admin/index.ts | 2 +- .../plugins/config/strapi-server/config.ts | 6 +- .../types/src/types/core/plugins/index.ts | 21 +- yarn.lock | 6 + 45 files changed, 752 insertions(+), 532 deletions(-) create mode 100644 packages/core/email/admin/.eslintrc.js rename packages/core/email/admin/src/{constants.js => constants.ts} (100%) rename packages/core/email/admin/src/{index.js => index.ts} (76%) create mode 100644 packages/core/email/admin/src/pages/Settings.tsx delete mode 100644 packages/core/email/admin/src/pages/Settings/index.js rename packages/core/email/admin/src/translations/tests/{plural.test.js => plural.test.ts} (68%) rename packages/core/email/admin/src/utils/{schema.js => schema.ts} (75%) create mode 100644 packages/core/email/admin/tsconfig.json create mode 100644 packages/core/email/packup.config.ts create mode 100644 packages/core/email/server/.eslintrc.js delete mode 100644 packages/core/email/server/bootstrap.js delete mode 100644 packages/core/email/server/controllers/email.js delete mode 100644 packages/core/email/server/controllers/index.js delete mode 100644 packages/core/email/server/routes/index.js delete mode 100644 packages/core/email/server/services/index.js create mode 100644 packages/core/email/server/src/bootstrap.ts rename packages/core/email/server/{config.js => src/config.ts} (66%) create mode 100644 packages/core/email/server/src/controllers/email.ts create mode 100644 packages/core/email/server/src/controllers/index.ts create mode 100644 packages/core/email/server/src/index.ts rename packages/core/email/server/{routes/admin.js => src/routes/admin.ts} (95%) rename packages/core/email/server/{routes/content-api.js => src/routes/content-api.ts} (78%) create mode 100644 packages/core/email/server/src/routes/index.ts rename packages/core/email/server/{services/email.js => src/services/email.ts} (66%) create mode 100644 packages/core/email/server/src/services/index.ts create mode 100644 packages/core/email/server/src/types.ts create mode 100644 packages/core/email/server/tsconfig.build.json rename packages/{plugins/color-picker/admin => core/email/server}/tsconfig.eslint.json (65%) create mode 100644 packages/core/email/server/tsconfig.json create mode 100644 packages/core/email/shared/types.ts delete mode 100644 packages/core/email/strapi-admin.js create mode 100644 packages/core/email/tsconfig.build.json diff --git a/packages/core/admin/admin/src/utils/createRoute.js b/packages/core/admin/admin/src/utils/createRoute.js index 3481a7e456..c8818946b1 100644 --- a/packages/core/admin/admin/src/utils/createRoute.js +++ b/packages/core/admin/admin/src/utils/createRoute.js @@ -12,7 +12,10 @@ const LazyCompo = ({ loadComponent }) => { try { const loadedCompo = await loadComponent(); - setCompo(() => loadedCompo.default); + // TODO the loaded component provided can currently come from a default or named export + // We will move the entire codebase to use named exports only + // Until then we support both cases with priority given to the existing default exports + setCompo(() => loadedCompo?.default ?? loadedCompo); } catch (err) { // TODO return the error component console.log(err); diff --git a/packages/core/email/.editorconfig b/packages/core/email/.editorconfig index d4eed8406b..d452b351df 100644 --- a/packages/core/email/.editorconfig +++ b/packages/core/email/.editorconfig @@ -2,6 +2,16 @@ root = true [*] end_of_line = lf -insert_final_newline = false indent_style = space indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + + +[{package.json,*.yml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/packages/core/email/.eslintignore b/packages/core/email/.eslintignore index 24c11109a2..728eb86d30 100644 --- a/packages/core/email/.eslintignore +++ b/packages/core/email/.eslintignore @@ -1,3 +1,4 @@ node_modules/ .eslintrc.js -index.d.ts \ No newline at end of file +index.d.ts +dist/ diff --git a/packages/core/email/.eslintrc.js b/packages/core/email/.eslintrc.js index a6c2c1e76d..6f2604d0b1 100644 --- a/packages/core/email/.eslintrc.js +++ b/packages/core/email/.eslintrc.js @@ -7,7 +7,7 @@ module.exports = { }, { files: ['**/*'], - excludedFiles: ['admin/**/*'], + excludedFiles: ['admin/**/*', 'server/**/*'], extends: ['custom/back'], }, ], diff --git a/packages/core/email/admin/.eslintrc.js b/packages/core/email/admin/.eslintrc.js new file mode 100644 index 0000000000..5b585ac0ad --- /dev/null +++ b/packages/core/email/admin/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['custom/front/typescript'], +}; diff --git a/packages/core/email/admin/src/constants.js b/packages/core/email/admin/src/constants.ts similarity index 100% rename from packages/core/email/admin/src/constants.js rename to packages/core/email/admin/src/constants.ts diff --git a/packages/core/email/admin/src/index.js b/packages/core/email/admin/src/index.ts similarity index 76% rename from packages/core/email/admin/src/index.js rename to packages/core/email/admin/src/index.ts index f861c89c73..8649f0b98d 100644 --- a/packages/core/email/admin/src/index.js +++ b/packages/core/email/admin/src/index.ts @@ -9,8 +9,12 @@ import { prefixPluginTranslations } from '@strapi/helper-plugin'; import { PERMISSIONS } from './constants'; -export default { - register(app) { +import type { Plugin } from '@strapi/types'; + +const admin: Plugin.Config.AdminInput = { + // TODO typing app in strapi/types as every plugin needs it + // eslint-disable-next-line @typescript-eslint/no-explicit-any + register(app: any) { // Create the email settings section app.createSettingSection( { @@ -26,11 +30,11 @@ export default { id: 'settings', to: `/settings/email`, async Component() { - const component = await import( + const { ProtectedSettingsPage } = await import( /* webpackChunkName: "email-settings-page" */ './pages/Settings' ); - return component; + return ProtectedSettingsPage; }, permissions: PERMISSIONS.settings, }, @@ -41,8 +45,9 @@ export default { name: 'email', }); }, + // eslint-disable-next-line @typescript-eslint/no-empty-function bootstrap() {}, - async registerTrads({ locales }) { + async registerTrads({ locales }: { locales: string[] }) { const importedTrads = await Promise.all( locales.map((locale) => { return import( @@ -66,3 +71,6 @@ export default { return Promise.resolve(importedTrads); }, }; + +// eslint-disable-next-line import/no-default-export +export default admin; diff --git a/packages/core/email/admin/src/pages/Settings.tsx b/packages/core/email/admin/src/pages/Settings.tsx new file mode 100644 index 0000000000..7c05ea58b7 --- /dev/null +++ b/packages/core/email/admin/src/pages/Settings.tsx @@ -0,0 +1,324 @@ +import * as React from 'react'; + +import { + Box, + Button, + ContentLayout, + Flex, + Grid, + GridItem, + HeaderLayout, + Main, + Option, + Select, + TextInput, + Typography, +} from '@strapi/design-system'; +import { + CheckPagePermissions, + getYupInnerErrors, + LoadingIndicatorPage, + SettingsPageTitle, + useFetchClient, + useFocusWhenNavigate, + useNotification, + useOverlayBlocker, +} from '@strapi/helper-plugin'; +import { Envelop } from '@strapi/icons'; +import { useIntl } from 'react-intl'; +import { useQuery, useMutation } from 'react-query'; +import styled from 'styled-components'; +import { ValidationError } from 'yup'; + +import { PERMISSIONS } from '../constants'; +import { schema } from '../utils/schema'; + +import type { EmailSettings } from '../../../shared/types'; + +const DocumentationLink = styled.a` + color: ${({ theme }) => theme.colors.primary600}; +`; + +interface MutationBody { + to: string; +} + +export const ProtectedSettingsPage = () => ( + + + +); + +const SettingsPage = () => { + const toggleNotification = useNotification(); + const { formatMessage } = useIntl(); + const { lockApp, unlockApp } = useOverlayBlocker(); + const { get, post } = useFetchClient(); + + const [testAddress, setTestAddress] = React.useState(''); + const [isTestAddressValid, setIsTestAddressValid] = React.useState(false); + + // TODO: I'm not sure how to type this. I think it should be Record but that type is defined in the helper-plugin + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [formErrors, setFormErrors] = React.useState>({}); + + const { data, isLoading } = useQuery(['email', 'settings'], async () => { + const res = await get('/email/settings'); + const { + data: { config }, + } = res; + + return config; + }); + + const mutation = useMutation( + async (body) => { + await post('/email/test', body); + }, + { + onError() { + toggleNotification!({ + type: 'warning', + message: formatMessage( + { + id: 'email.Settings.email.plugin.notification.test.error', + defaultMessage: 'Failed to send a test mail to {to}', + }, + { to: testAddress } + ), + }); + }, + onSuccess() { + toggleNotification!({ + type: 'success', + message: formatMessage( + { + id: 'email.Settings.email.plugin.notification.test.success', + defaultMessage: 'Email test succeeded, check the {to} mailbox', + }, + { to: testAddress } + ), + }); + }, + retry: false, + } + ); + + useFocusWhenNavigate(); + + React.useEffect(() => { + schema + .validate({ email: testAddress }, { abortEarly: false }) + .then(() => setIsTestAddressValid(true)) + .catch(() => setIsTestAddressValid(false)); + }, [testAddress]); + + const handleChange = (event: React.ChangeEvent) => { + setTestAddress(() => event.target.value); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + try { + await schema.validate({ email: testAddress }, { abortEarly: false }); + } catch (error) { + if (error instanceof ValidationError) { + setFormErrors(getYupInnerErrors(error)); + } + } + + lockApp!(); + + mutation.mutate({ to: testAddress }); + + unlockApp!(); + }; + + return ( +
+ + + + + + {isLoading ? ( + + ) : ( + data && ( +
+ + + + + + {formatMessage({ + id: 'email.Settings.email.plugin.title.config', + defaultMessage: 'Configuration', + })} + + + {formatMessage( + { + id: 'email.Settings.email.plugin.text.configuration', + defaultMessage: + 'The plugin is configured through the {file} file, checkout this {link} for the documentation.', + }, + { + file: './config/plugins.js', + link: ( + + {formatMessage({ + id: 'email.link', + defaultMessage: 'Link', + })} + + ), + } + )} + + + + + + '", + })} + disabled + value={data.settings.defaultFrom} + /> + + + + '`, + })} + disabled + value={data.settings.defaultReplyTo} + /> + + + + + + + + + + + + {formatMessage({ + id: 'email.Settings.email.plugin.title.test', + defaultMessage: 'Test email delivery', + })} + + + + + + + + + + + + +
+ ) + )} +
+
+ ); +}; diff --git a/packages/core/email/admin/src/pages/Settings/index.js b/packages/core/email/admin/src/pages/Settings/index.js deleted file mode 100644 index df66bb164b..0000000000 --- a/packages/core/email/admin/src/pages/Settings/index.js +++ /dev/null @@ -1,314 +0,0 @@ -import * as React from 'react'; - -import { - Box, - Button, - ContentLayout, - Flex, - Grid, - GridItem, - HeaderLayout, - Main, - Option, - Select, - TextInput, - Typography, -} from '@strapi/design-system'; -import { - CheckPagePermissions, - getYupInnerErrors, - LoadingIndicatorPage, - SettingsPageTitle, - useFetchClient, - useFocusWhenNavigate, - useNotification, - useOverlayBlocker, -} from '@strapi/helper-plugin'; -import { Envelop } from '@strapi/icons'; -import { useIntl } from 'react-intl'; -import { useQuery, useMutation } from 'react-query'; -import styled from 'styled-components'; - -import { PERMISSIONS } from '../../constants'; -import schema from '../../utils/schema'; - -const DocumentationLink = styled.a` - color: ${({ theme }) => theme.colors.primary600}; -`; - -const ProtectedSettingsPage = () => ( - - - -); - -const SettingsPage = () => { - const toggleNotification = useNotification(); - const { formatMessage } = useIntl(); - const { lockApp, unlockApp } = useOverlayBlocker(); - const { get, post } = useFetchClient(); - const { data, isLoading } = useQuery(['email', 'settings'], async () => { - const { - data: { config }, - } = await get('/email/settings'); - - return config; - }); - - const mutation = useMutation( - (body) => post('/email/test', body), - { - onError() { - toggleNotification({ - type: 'warning', - message: formatMessage( - { - id: 'email.Settings.email.plugin.notification.test.error', - defaultMessage: 'Failed to send a test mail to {to}', - }, - { to: testAddress } - ), - }); - }, - - onSuccess() { - toggleNotification({ - type: 'success', - message: formatMessage( - { - id: 'email.Settings.email.plugin.notification.test.success', - defaultMessage: 'Email test succeeded, check the {to} mailbox', - }, - { to: testAddress } - ), - }); - }, - }, - { - retry: false, - } - ); - - useFocusWhenNavigate(); - - const [formErrors, setFormErrors] = React.useState({}); - const [testAddress, setTestAddress] = React.useState(''); - const [isTestAddressValid, setIsTestAddressValid] = React.useState(false); - - React.useEffect(() => { - schema - .validate({ email: testAddress }, { abortEarly: false }) - .then(() => setIsTestAddressValid(true)) - .catch(() => setIsTestAddressValid(false)); - }, [testAddress]); - - const handleChange = (e) => { - setTestAddress(() => e.target.value); - }; - - const handleSubmit = async (event) => { - event.preventDefault(); - - try { - await schema.validate({ email: testAddress }, { abortEarly: false }); - } catch (error) { - setFormErrors(getYupInnerErrors(error)); - } - - lockApp(); - - mutation.mutate({ to: testAddress }); - - unlockApp(); - }; - - return ( -
- - - - - - {isLoading ? ( - - ) : ( -
- - - - - - {formatMessage({ - id: 'email.Settings.email.plugin.title.config', - defaultMessage: 'Configuration', - })} - - - {formatMessage( - { - id: 'email.Settings.email.plugin.text.configuration', - defaultMessage: - 'The plugin is configured through the {file} file, checkout this {link} for the documentation.', - }, - { - file: './config/plugins.js', - link: ( - - {formatMessage({ - id: 'email.link', - defaultMessage: 'Link', - })} - - ), - } - )} - - - - - - '", - })} - disabled - onChange={() => {}} - value={data.settings.defaultFrom} - /> - - - - '`, - })} - disabled - onChange={() => {}} - value={data.settings.defaultReplyTo} - /> - - - - - - - - - - - - {formatMessage({ - id: 'email.Settings.email.plugin.title.test', - defaultMessage: 'Test email delivery', - })} - - - - - - - - - - - - -
- )} -
-
- ); -}; - -export default ProtectedSettingsPage; diff --git a/packages/core/email/admin/src/translations/tests/plural.test.js b/packages/core/email/admin/src/translations/tests/plural.test.ts similarity index 68% rename from packages/core/email/admin/src/translations/tests/plural.test.js rename to packages/core/email/admin/src/translations/tests/plural.test.ts index a02247e0f5..5ee59d6b17 100644 --- a/packages/core/email/admin/src/translations/tests/plural.test.js +++ b/packages/core/email/admin/src/translations/tests/plural.test.ts @@ -1,9 +1,11 @@ -const translations = require('../en.json'); +import translations from '../en.json'; + +const typedTranslations: Record = translations; describe('translations', () => { describe('plural syntax', () => { it('should avoid .plural/.singular syntax', () => { - Object.keys(translations).forEach((translationKey) => { + Object.keys(typedTranslations).forEach((translationKey) => { const keyParts = translationKey.split('.'); const lastKeyPart = keyParts.pop(); @@ -13,7 +15,7 @@ describe('translations', () => { keyParts.push('plural'); const pluralKey = keyParts.join('.'); - expect(translations[pluralKey]).toBeUndefined(); + expect(typedTranslations[pluralKey]).toBeUndefined(); } }); }); diff --git a/packages/core/email/admin/src/utils/schema.js b/packages/core/email/admin/src/utils/schema.ts similarity index 75% rename from packages/core/email/admin/src/utils/schema.js rename to packages/core/email/admin/src/utils/schema.ts index c2b870cd2f..e67dcff644 100644 --- a/packages/core/email/admin/src/utils/schema.js +++ b/packages/core/email/admin/src/utils/schema.ts @@ -1,8 +1,6 @@ import { translatedErrors } from '@strapi/helper-plugin'; import * as yup from 'yup'; -const schema = yup.object().shape({ +export const schema = yup.object().shape({ email: yup.string().email(translatedErrors.email).required(translatedErrors.required), }); - -export default schema; diff --git a/packages/core/email/admin/tsconfig.json b/packages/core/email/admin/tsconfig.json new file mode 100644 index 0000000000..fdbb2a4fb5 --- /dev/null +++ b/packages/core/email/admin/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "tsconfig/client.json", + "include": ["./src", "../shared/types.ts"], + "compilerOptions": { + "rootDir": "../", + } +} + \ No newline at end of file diff --git a/packages/core/email/package.json b/packages/core/email/package.json index 5641871201..78de8f0a13 100644 --- a/packages/core/email/package.json +++ b/packages/core/email/package.json @@ -19,15 +19,42 @@ "url": "https://strapi.io" } ], + "exports": { + "./strapi-admin": { + "types": "./dist/admin/src/index.d.ts", + "source": "./admin/src/index.ts", + "import": "./dist/admin/index.mjs", + "require": "./dist/admin/index.js", + "default": "./dist/admin/index.js" + }, + "./strapi-server": { + "types": "./dist/server/src/index.d.ts", + "source": "./server/src/index.ts", + "import": "./dist/server/index.mjs", + "require": "./dist/server/index.js", + "default": "./dist/server/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "./dist", + "strapi-server.js" + ], "scripts": { + "build": "pack-up build", + "clean": "run -T rimraf ./dist", "lint": "run -T eslint .", + "prepublishOnly": "yarn clean && yarn build", "test:front": "run -T cross-env IS_EE=true jest --config ./jest.config.front.js", "test:front:ce": "run -T cross-env IS_EE=false jest --config ./jest.config.front.js", "test:front:watch": "run -T cross-env IS_EE=true jest --config ./jest.config.front.js --watchAll", - "test:front:watch:ce": "run -T cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll" + "test:front:watch:ce": "run -T cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll", + "test:ts:front": "run -T tsc -p admin/tsconfig.json", + "watch": "pack-up watch" }, "dependencies": { "@strapi/design-system": "1.12.0", + "@strapi/helper-plugin": "4.14.4", "@strapi/icons": "1.12.0", "@strapi/provider-email-sendmail": "4.14.4", "@strapi/utils": "4.14.4", @@ -38,8 +65,12 @@ "yup": "0.32.9" }, "devDependencies": { - "@strapi/helper-plugin": "4.14.4", + "@strapi/pack-up": "workspace:*", + "@strapi/types": "workspace:*", "@testing-library/react": "14.0.0", + "@types/koa": "2.13.4", + "@types/lodash": "^4.14.191", + "koa": "2.13.4", "msw": "1.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -47,6 +78,7 @@ "styled-components": "5.3.3" }, "peerDependencies": { + "koa": "2.13.4", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0", "react-router-dom": "5.3.4", diff --git a/packages/core/email/packup.config.ts b/packages/core/email/packup.config.ts new file mode 100644 index 0000000000..bc1f2e55a6 --- /dev/null +++ b/packages/core/email/packup.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from '@strapi/pack-up'; + +export default defineConfig({ + bundles: [ + { + source: './admin/src/index.ts', + import: './dist/admin/index.mjs', + require: './dist/admin/index.js', + types: './dist/admin/src/index.d.ts', + runtime: 'web', + }, + { + source: './server/src/index.ts', + import: './dist/server/index.mjs', + require: './dist/server/index.js', + types: './dist/server/src/index.d.ts', + runtime: 'node', + }, + ], + dist: './dist', + /** + * Because we're exporting a server & client package + * which have different runtimes we want to ignore + * what they look like in the package.json + */ + exports: {}, +}); diff --git a/packages/core/email/server/.eslintrc.js b/packages/core/email/server/.eslintrc.js new file mode 100644 index 0000000000..81357fb6e7 --- /dev/null +++ b/packages/core/email/server/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ['custom/back/typescript'], + parserOptions: { + project: ['./server/tsconfig.eslint.json'], + }, +}; diff --git a/packages/core/email/server/bootstrap.js b/packages/core/email/server/bootstrap.js deleted file mode 100644 index cb635d26bd..0000000000 --- a/packages/core/email/server/bootstrap.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const createProvider = (emailConfig) => { - const providerName = emailConfig.provider?.toLowerCase(); - let provider; - - let modulePath; - try { - modulePath = require.resolve(`@strapi/provider-email-${providerName}`); - } catch (error) { - if (error.code === 'MODULE_NOT_FOUND') { - modulePath = providerName; - } else { - throw error; - } - } - - try { - provider = require(modulePath); - } catch (err) { - throw new Error(`Could not load email provider "${providerName}".`); - } - - return provider.init(emailConfig.providerOptions, emailConfig.settings); -}; - -module.exports = async ({ strapi }) => { - const emailConfig = strapi.config.get('plugin.email'); - strapi.plugin('email').provider = createProvider(emailConfig); - - // Add permissions - const actions = [ - { - section: 'settings', - category: 'email', - displayName: 'Access the Email Settings page', - uid: 'settings.read', - pluginName: 'email', - }, - ]; - - await strapi.admin.services.permission.actionProvider.registerMany(actions); -}; diff --git a/packages/core/email/server/controllers/email.js b/packages/core/email/server/controllers/email.js deleted file mode 100644 index 3aebf30a5d..0000000000 --- a/packages/core/email/server/controllers/email.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -const { pick } = require('lodash/fp'); -const { ApplicationError } = require('@strapi/utils').errors; - -/** - * Email.js controller - * - * @description: A set of functions called "actions" of the `email` plugin. - */ -module.exports = { - async send(ctx) { - const options = ctx.request.body; - - try { - await strapi.plugin('email').service('email').send(options); - } catch (e) { - if (e.statusCode === 400) { - throw new ApplicationError(e.message); - } else { - throw new Error(`Couldn't send email: ${e.message}.`); - } - } - - // Send 200 `ok` - ctx.send({}); - }, - - async test(ctx) { - const { to } = ctx.request.body; - - if (!to) { - throw new ApplicationError('No recipient(s) are given'); - } - - const email = { - to, - subject: `Strapi test mail to: ${to}`, - text: `Great! You have correctly configured the Strapi email plugin with the ${strapi.config.get( - 'plugin.email.provider' - )} provider. \r\nFor documentation on how to use the email plugin checkout: https://docs.strapi.io/developer-docs/latest/plugins/email.html`, - }; - - try { - await strapi.plugin('email').service('email').send(email); - } catch (e) { - if (e.statusCode === 400) { - throw new ApplicationError(e.message); - } else { - throw new Error(`Couldn't send test email: ${e.message}.`); - } - } - - // Send 200 `ok` - ctx.send({}); - }, - - async getSettings(ctx) { - const config = strapi.plugin('email').service('email').getProviderSettings(); - - ctx.send({ - config: pick( - ['provider', 'settings.defaultFrom', 'settings.defaultReplyTo', 'settings.testAddress'], - config - ), - }); - }, -}; diff --git a/packages/core/email/server/controllers/index.js b/packages/core/email/server/controllers/index.js deleted file mode 100644 index 913aea1c3b..0000000000 --- a/packages/core/email/server/controllers/index.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const email = require('./email'); - -module.exports = { - email, -}; diff --git a/packages/core/email/server/routes/index.js b/packages/core/email/server/routes/index.js deleted file mode 100644 index 6939f2d012..0000000000 --- a/packages/core/email/server/routes/index.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports = { - admin: require('./admin'), - 'content-api': require('./content-api'), -}; diff --git a/packages/core/email/server/services/index.js b/packages/core/email/server/services/index.js deleted file mode 100644 index 913aea1c3b..0000000000 --- a/packages/core/email/server/services/index.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const email = require('./email'); - -module.exports = { - email, -}; diff --git a/packages/core/email/server/src/bootstrap.ts b/packages/core/email/server/src/bootstrap.ts new file mode 100644 index 0000000000..508743f807 --- /dev/null +++ b/packages/core/email/server/src/bootstrap.ts @@ -0,0 +1,57 @@ +import type { Strapi } from '@strapi/types'; +import type { EmailConfig, SendOptions } from './types'; + +interface EmailProvider { + send: (options: SendOptions) => Promise; +} + +interface EmailProviderModule { + init: ( + options: EmailConfig['providerOptions'], + settings: EmailConfig['settings'] + ) => EmailProvider; + name?: string; + provider?: string; +} + +const createProvider = (emailConfig: EmailConfig) => { + const providerName = emailConfig.provider.toLowerCase(); + let provider: EmailProviderModule; + + let modulePath: string; + try { + modulePath = require.resolve(`@strapi/provider-email-${providerName}`); + } catch (error) { + if (error instanceof Error && 'code' in error && error.code === 'MODULE_NOT_FOUND') { + modulePath = providerName; + } else { + throw error; + } + } + + try { + provider = require(modulePath); + } catch (err) { + throw new Error(`Could not load email provider "${providerName}".`); + } + + return provider.init(emailConfig.providerOptions, emailConfig.settings); +}; + +export const bootstrap = async ({ strapi }: { strapi: Strapi }) => { + const emailConfig: EmailConfig = strapi.config.get('plugin.email'); + strapi.plugin('email').provider = createProvider(emailConfig); + + // Add permissions + const actions = [ + { + section: 'settings', + category: 'email', + displayName: 'Access the Email Settings page', + uid: 'settings.read', + pluginName: 'email', + }, + ]; + + await strapi.admin!.services.permission.actionProvider.registerMany(actions); +}; diff --git a/packages/core/email/server/config.js b/packages/core/email/server/src/config.ts similarity index 66% rename from packages/core/email/server/config.js rename to packages/core/email/server/src/config.ts index 655340321c..81518d1bc5 100644 --- a/packages/core/email/server/config.js +++ b/packages/core/email/server/src/config.ts @@ -1,6 +1,6 @@ -'use strict'; +import type { StrapiConfig } from './types'; -module.exports = { +export const config: StrapiConfig = { default: { provider: 'sendmail', providerOptions: {}, diff --git a/packages/core/email/server/src/controllers/email.ts b/packages/core/email/server/src/controllers/email.ts new file mode 100644 index 0000000000..cd1a2d45a4 --- /dev/null +++ b/packages/core/email/server/src/controllers/email.ts @@ -0,0 +1,77 @@ +import { pick } from 'lodash/fp'; +import { errors } from '@strapi/utils'; + +import type Koa from 'koa'; +import type { EmailConfig, SendOptions } from '../types'; + +const { ApplicationError } = errors; + +/** + * Email.js controller + * + * @description: A set of functions called "actions" of the `email` plugin. + */ +const emailController = { + async send(ctx: Koa.Context) { + const options: SendOptions = ctx.request.body; + + try { + await strapi.plugin('email').service('email').send(options); + } catch (error) { + if (error instanceof Error) { + if ('statusCode' in error && error.statusCode === 400) { + throw new ApplicationError(error.message); + } else { + throw new Error(`Couldn't send email: ${error.message}.`); + } + } + } + + // Send 200 `ok` + ctx.send({}); + }, + + async test(ctx: Koa.Context) { + const { to } = ctx.request.body; + + if (!to) { + throw new ApplicationError('No recipient(s) are given'); + } + + const email: SendOptions = { + to, + subject: `Strapi test mail to: ${to}`, + text: `Great! You have correctly configured the Strapi email plugin with the ${strapi.config.get( + 'plugin.email.provider' + )} provider. \r\nFor documentation on how to use the email plugin checkout: https://docs.strapi.io/developer-docs/latest/plugins/email.html`, + }; + + try { + await strapi.plugin('email').service('email').send(email); + } catch (error) { + if (error instanceof Error) { + if ('statusCode' in error && error.statusCode === 400) { + throw new ApplicationError(error.message); + } else { + throw new Error(`Couldn't send test email: ${error.message}.`); + } + } + } + + // Send 200 `ok` + ctx.send({}); + }, + + async getSettings(ctx: Koa.Context) { + const config: EmailConfig = strapi.plugin('email').service('email').getProviderSettings(); + + ctx.send({ + config: pick( + ['provider', 'settings.defaultFrom', 'settings.defaultReplyTo', 'settings.testAddress'], + config + ), + }); + }, +}; + +export default emailController; diff --git a/packages/core/email/server/src/controllers/index.ts b/packages/core/email/server/src/controllers/index.ts new file mode 100644 index 0000000000..755626a727 --- /dev/null +++ b/packages/core/email/server/src/controllers/index.ts @@ -0,0 +1,3 @@ +import email from './email'; + +export const controllers = { email }; diff --git a/packages/core/email/server/src/index.ts b/packages/core/email/server/src/index.ts new file mode 100644 index 0000000000..68f5164189 --- /dev/null +++ b/packages/core/email/server/src/index.ts @@ -0,0 +1,13 @@ +import { bootstrap } from './bootstrap'; +import { services } from './services'; +import { routes } from './routes'; +import { controllers } from './controllers'; +import { config } from './config'; + +export default { + bootstrap, + services, + routes, + controllers, + config, +}; diff --git a/packages/core/email/server/routes/admin.js b/packages/core/email/server/src/routes/admin.ts similarity index 95% rename from packages/core/email/server/routes/admin.js rename to packages/core/email/server/src/routes/admin.ts index 8b14aefed6..3d5362d83a 100644 --- a/packages/core/email/server/routes/admin.js +++ b/packages/core/email/server/src/routes/admin.ts @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = { +export default { type: 'admin', routes: [ { diff --git a/packages/core/email/server/routes/content-api.js b/packages/core/email/server/src/routes/content-api.ts similarity index 78% rename from packages/core/email/server/routes/content-api.js rename to packages/core/email/server/src/routes/content-api.ts index 23f1764864..ee0ecf5eb8 100644 --- a/packages/core/email/server/routes/content-api.js +++ b/packages/core/email/server/src/routes/content-api.ts @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = { +export default { type: 'content-api', routes: [ { diff --git a/packages/core/email/server/src/routes/index.ts b/packages/core/email/server/src/routes/index.ts new file mode 100644 index 0000000000..d6b08414d6 --- /dev/null +++ b/packages/core/email/server/src/routes/index.ts @@ -0,0 +1,7 @@ +import admin from './admin'; +import contentApi from './content-api'; + +export const routes = { + admin, + 'content-api': contentApi, +}; diff --git a/packages/core/email/server/services/email.js b/packages/core/email/server/src/services/email.ts similarity index 66% rename from packages/core/email/server/services/email.js rename to packages/core/email/server/src/services/email.ts index bff1c7f867..a054f1d21e 100644 --- a/packages/core/email/server/services/email.js +++ b/packages/core/email/server/src/services/email.ts @@ -1,18 +1,19 @@ -'use strict'; +import * as _ from 'lodash'; +import { keysDeep, template } from '@strapi/utils'; -const _ = require('lodash'); -const { - template: { createStrictInterpolationRegExp }, - keysDeep, -} = require('@strapi/utils'); +import type { + EmailConfig, + EmailOptions, + EmailTemplate, + EmailTemplateData, + SendOptions, +} from '../types'; -const getProviderSettings = () => { - return strapi.config.get('plugin.email'); -}; +const { createStrictInterpolationRegExp } = template; -const send = async (options) => { - return strapi.plugin('email').provider.send(options); -}; +const getProviderSettings = (): EmailConfig => strapi.config.get('plugin.email'); + +const send = async (options: SendOptions) => strapi.plugin('email').provider.send(options); /** * fill subject, text and html using lodash template @@ -21,9 +22,14 @@ const send = async (options) => { * @param {object} data - data used to fill the template * @returns {{ subject, text, subject }} */ -const sendTemplatedEmail = (emailOptions = {}, emailTemplate = {}, data = {}) => { +const sendTemplatedEmail = ( + emailOptions: EmailOptions, + emailTemplate: EmailTemplate, + data: EmailTemplateData +) => { const attributes = ['subject', 'text', 'html']; const missingAttributes = _.difference(attributes, Object.keys(emailTemplate)); + if (missingAttributes.length > 0) { throw new Error( `Following attributes are missing from your email template : ${missingAttributes.join(', ')}` @@ -39,8 +45,6 @@ const sendTemplatedEmail = (emailOptions = {}, emailTemplate = {}, data = {}) => ? Object.assign(compiled, { [attribute]: _.template(emailTemplate[attribute], { interpolate, - evaluate: false, - escape: false, })(data), }) : compiled, @@ -50,8 +54,10 @@ const sendTemplatedEmail = (emailOptions = {}, emailTemplate = {}, data = {}) => return strapi.plugin('email').provider.send({ ...emailOptions, ...templatedAttributes }); }; -module.exports = () => ({ +const emailService = () => ({ getProviderSettings, send, sendTemplatedEmail, }); + +export default emailService; diff --git a/packages/core/email/server/src/services/index.ts b/packages/core/email/server/src/services/index.ts new file mode 100644 index 0000000000..535c46e0a2 --- /dev/null +++ b/packages/core/email/server/src/services/index.ts @@ -0,0 +1,3 @@ +import email from './email'; + +export const services = { email }; diff --git a/packages/core/email/server/src/types.ts b/packages/core/email/server/src/types.ts new file mode 100644 index 0000000000..c00cddf2b6 --- /dev/null +++ b/packages/core/email/server/src/types.ts @@ -0,0 +1,43 @@ +import type { Plugin } from '@strapi/types'; + +export interface EmailConfig extends Record { + provider: string; + providerOptions?: object; + settings?: { + defaultFrom?: string; + }; +} + +type LoadedPluginConfig = Plugin.LoadedPlugin['config']; + +export interface StrapiConfig extends LoadedPluginConfig { + default: EmailConfig; +} + +export interface EmailTemplateData { + url?: string; + user?: { + email: string; + firstname: string; + lastname: string; + username: string; + }; +} + +export interface EmailOptions { + from?: string; + to: string; + cc?: string; + bcc?: string; + replyTo?: string; + [key: string]: string | undefined; // to allow additional template attributes if needed +} + +export interface EmailTemplate { + subject: string; + text: string; + html?: string; + [key: string]: string | undefined; // to allow additional template attributes if needed +} + +export type SendOptions = EmailOptions & EmailTemplate; diff --git a/packages/core/email/server/tsconfig.build.json b/packages/core/email/server/tsconfig.build.json new file mode 100644 index 0000000000..49d94a869f --- /dev/null +++ b/packages/core/email/server/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": ["./src"], + "exclude": ["./src/**/*.test.ts"], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/server" + } +} diff --git a/packages/plugins/color-picker/admin/tsconfig.eslint.json b/packages/core/email/server/tsconfig.eslint.json similarity index 65% rename from packages/plugins/color-picker/admin/tsconfig.eslint.json rename to packages/core/email/server/tsconfig.eslint.json index 9b62409191..b531808514 100644 --- a/packages/plugins/color-picker/admin/tsconfig.eslint.json +++ b/packages/core/email/server/tsconfig.eslint.json @@ -1,5 +1,8 @@ { "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, "include": ["src"], "exclude": ["node_modules"] } diff --git a/packages/core/email/server/tsconfig.json b/packages/core/email/server/tsconfig.json new file mode 100644 index 0000000000..9cf449b4cd --- /dev/null +++ b/packages/core/email/server/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "tsconfig/base.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "esModuleInterop": true + }, +} diff --git a/packages/core/email/shared/types.ts b/packages/core/email/shared/types.ts new file mode 100644 index 0000000000..2beb7e1111 --- /dev/null +++ b/packages/core/email/shared/types.ts @@ -0,0 +1,11 @@ +export interface EmailSettings { + config: ConfigSettings; +} + +export interface ConfigSettings { + provider: string; + settings: { + defaultFrom: string; + defaultReplyTo: string; + }; +} diff --git a/packages/core/email/strapi-admin.js b/packages/core/email/strapi-admin.js deleted file mode 100644 index 2d1a3d93ac..0000000000 --- a/packages/core/email/strapi-admin.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -module.exports = require('./admin/src').default; diff --git a/packages/core/email/strapi-server.js b/packages/core/email/strapi-server.js index 08262d070a..bf55958861 100644 --- a/packages/core/email/strapi-server.js +++ b/packages/core/email/strapi-server.js @@ -1,17 +1,3 @@ 'use strict'; -const bootstrap = require('./server/bootstrap'); -const services = require('./server/services'); -const routes = require('./server/routes'); -const controllers = require('./server/controllers'); -const config = require('./server/config'); - -module.exports = () => { - return { - bootstrap, - config, - routes, - controllers, - services, - }; -}; +module.exports = require('./dist/server'); diff --git a/packages/core/email/tsconfig.build.json b/packages/core/email/tsconfig.build.json new file mode 100644 index 0000000000..e1a56f65d3 --- /dev/null +++ b/packages/core/email/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "tsconfig/client.json", + "include": ["./admin", "./shared", "./server"], + "compilerOptions": { + "declarationDir": "./dist", + "outDir": "./dist" + } +} diff --git a/packages/core/helper-plugin/src/components/CheckPagePermissions.tsx b/packages/core/helper-plugin/src/components/CheckPagePermissions.tsx index 6802ecfc73..4c0f7ea417 100644 --- a/packages/core/helper-plugin/src/components/CheckPagePermissions.tsx +++ b/packages/core/helper-plugin/src/components/CheckPagePermissions.tsx @@ -12,12 +12,15 @@ import type { domain } from '@strapi/permissions'; type Permission = domain.permission.Permission; -export interface CheckPagePermissions { +export interface CheckPagePermissionsProps { children: React.ReactNode; permissions?: Permission[]; } -const CheckPagePermissions = ({ permissions = [], children }: CheckPagePermissions) => { +const CheckPagePermissions = ({ + permissions = [], + children, +}: CheckPagePermissionsProps): React.JSX.Element => { const abortController = new AbortController(); const { signal } = abortController; const { allPermissions } = useRBACProvider(); @@ -73,7 +76,7 @@ const CheckPagePermissions = ({ permissions = [], children }: CheckPagePermissio return ; } - return children; + return <>{children}; }; export { CheckPagePermissions }; diff --git a/packages/core/helper-plugin/src/components/CheckPermissions.tsx b/packages/core/helper-plugin/src/components/CheckPermissions.tsx index 8fb9bf1cde..f2d294237d 100644 --- a/packages/core/helper-plugin/src/components/CheckPermissions.tsx +++ b/packages/core/helper-plugin/src/components/CheckPermissions.tsx @@ -11,12 +11,12 @@ type Permission = domain.permission.Permission; // NOTE: this component is very similar to the CheckPagePermissions // except that it does not handle redirections nor loading state -export interface CheckPagePermissions { +export interface CheckPermissionsProps { children: React.ReactNode; permissions?: Permission[]; } -const CheckPermissions = ({ permissions = [], children }: CheckPagePermissions) => { +const CheckPermissions = ({ permissions = [], children }: CheckPermissionsProps) => { const { allPermissions } = useRBACProvider(); const toggleNotification = useNotification(); const [state, setState] = React.useState({ isLoading: true, canAccess: false }); @@ -69,7 +69,7 @@ const CheckPermissions = ({ permissions = [], children }: CheckPagePermissions) return null; } - return children; + return <>{children}; }; export { CheckPermissions }; diff --git a/packages/core/strapi/src/core/loaders/plugins/index.ts b/packages/core/strapi/src/core/loaders/plugins/index.ts index 6bed582b54..e380045ca7 100644 --- a/packages/core/strapi/src/core/loaders/plugins/index.ts +++ b/packages/core/strapi/src/core/loaders/plugins/index.ts @@ -2,30 +2,14 @@ import { join } from 'path'; import fse from 'fs-extra'; import { defaultsDeep, defaults, getOr, get } from 'lodash/fp'; import { env } from '@strapi/utils'; -import type { Strapi, Common, Schema } from '@strapi/types'; +import type { Strapi, Plugin } from '@strapi/types'; import { loadFile } from '../../app-configuration/load-config-file'; import loadFiles from '../../../load/load-files'; import { getEnabledPlugins } from './get-enabled-plugins'; import { getUserPluginsConfig } from './get-user-plugins-config'; -type LoadedPlugin = { - config: { - default: Record | ((opts: { env: typeof env }) => Record); - validator: (config: Record) => void; - }; - bootstrap: ({ strapi }: { strapi: Strapi }) => void | Promise; - destroy: ({ strapi }: { strapi: Strapi }) => void | Promise; - register: ({ strapi }: { strapi: Strapi }) => void | Promise; - routes: Record; - controllers: Record; - services: Record; - policies: Record; - middlewares: Record; - contentTypes: Record; -}; - interface Plugins { - [key: string]: LoadedPlugin; + [key: string]: Plugin.LoadedPlugin; } const defaultPlugin = { diff --git a/packages/core/types/src/types/core/plugins/config/strapi-admin/index.ts b/packages/core/types/src/types/core/plugins/config/strapi-admin/index.ts index 95eabab9ff..38953eb3b0 100644 --- a/packages/core/types/src/types/core/plugins/config/strapi-admin/index.ts +++ b/packages/core/types/src/types/core/plugins/config/strapi-admin/index.ts @@ -3,5 +3,5 @@ export interface AdminInput { register: unknown; bootstrap: unknown; - registerTrads: unknown; + registerTrads: ({ locales }: { locales: string[] }) => Promise; } diff --git a/packages/core/types/src/types/core/plugins/config/strapi-server/config.ts b/packages/core/types/src/types/core/plugins/config/strapi-server/config.ts index f7f8ae9075..027c0bc48b 100644 --- a/packages/core/types/src/types/core/plugins/config/strapi-server/config.ts +++ b/packages/core/types/src/types/core/plugins/config/strapi-server/config.ts @@ -1,4 +1,6 @@ +import { env } from '@strapi/utils'; + export interface Config { - validator: () => unknown; - default: object | (() => object); + validator: (config: Record) => void; + default: Record | ((opts: { env: typeof env }) => Record); } diff --git a/packages/core/types/src/types/core/plugins/index.ts b/packages/core/types/src/types/core/plugins/index.ts index cb1ab1deb7..03151d13cb 100644 --- a/packages/core/types/src/types/core/plugins/index.ts +++ b/packages/core/types/src/types/core/plugins/index.ts @@ -1,4 +1,7 @@ -import type { Common, Shared, Utils } from '../..'; +import { env } from '@strapi/utils'; + +import type { Common, Shared, Utils, Schema } from '../..'; +import type { Strapi } from '../../..'; export type IsEnabled< TName extends keyof any, @@ -15,4 +18,20 @@ export type IsEnabled< : false : false; +export type LoadedPlugin = { + config: { + default: Record | ((opts: { env: typeof env }) => Record); + validator: (config: Record) => void; + }; + bootstrap: ({ strapi }: { strapi: Strapi }) => void | Promise; + destroy: ({ strapi }: { strapi: Strapi }) => void | Promise; + register: ({ strapi }: { strapi: Strapi }) => void | Promise; + routes: Record; + controllers: Record; + services: Record; + policies: Record; + middlewares: Record; + contentTypes: Record; +}; + export * as Config from './config'; diff --git a/yarn.lock b/yarn.lock index 7daa6b73f3..5013280f4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7934,9 +7934,14 @@ __metadata: "@strapi/design-system": 1.12.0 "@strapi/helper-plugin": 4.14.4 "@strapi/icons": 1.12.0 + "@strapi/pack-up": "workspace:*" "@strapi/provider-email-sendmail": 4.14.4 + "@strapi/types": "workspace:*" "@strapi/utils": 4.14.4 "@testing-library/react": 14.0.0 + "@types/koa": 2.13.4 + "@types/lodash": ^4.14.191 + koa: 2.13.4 lodash: 4.17.21 msw: 1.3.0 prop-types: ^15.8.1 @@ -7948,6 +7953,7 @@ __metadata: styled-components: 5.3.3 yup: 0.32.9 peerDependencies: + koa: 2.13.4 react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 react-router-dom: 5.3.4