Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(cli): Setup command for mailer #9335

Merged
merged 8 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/docs/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -1904,6 +1904,19 @@ In order to use [Netlify Dev](https://www.netlify.com/products/dev/) you need to

> Note: To detect the RedwoodJS framework, please use netlify-cli v3.34.0 or greater.

### setup mailer

This command adds the necessary packages and files to get started using the RedwoodJS mailer. By default it also creates an example mail template which can be skipped with the `--skip-examples` flag.

```
yarn redwood setup mailer
```

| Arguments & Options | Description |
| :---------------------- | :----------------------------- |
| `--force, -f` | Overwrite existing files |
| `--skip-examples` | Do not include example content, such as a React email template |

### setup package

This command takes a published npm package that you specify, performs some compatibility checks, and then executes its bin script. This allows you to use third-party packages that can provide you with an easy-to-use setup command for the particular functionality they provide.
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/src/commands/setup/mailer/mailer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'

export const command = 'mailer'

export const description =
'Setup the redwood mailer. This will install the required packages and add the required initial configuration to your redwood app.'

export const builder = (yargs) => {
yargs
.option('force', {
alias: 'f',
default: false,
description: 'Overwrite existing configuration',
type: 'boolean',
})
.option('skip-examples', {
default: false,
description: 'Only include required files and exclude any examples',
type: 'boolean',
})
}

export const handler = async (options) => {
recordTelemetryAttributes({
command: 'setup mailer',
force: options.force,
skipExamples: options.skipExamples,
})
const { handler } = await import('./mailerHandler.js')
return handler(options)
}
119 changes: 119 additions & 0 deletions packages/cli/src/commands/setup/mailer/mailerHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import fs from 'fs'
import path from 'path'

import { Listr } from 'listr2'

import { addApiPackages } from '@redwoodjs/cli-helpers'
import { errorTelemetry } from '@redwoodjs/telemetry'

import { getPaths, transformTSToJS, writeFile } from '../../../lib'
import c from '../../../lib/colors'
import { isTypeScriptProject } from '../../../lib/project'

export const handler = async ({ force, skipExamples }) => {
const projectIsTypescript = isTypeScriptProject()
const redwoodVersion =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like many setup needs, getting the RW version feels like it could be a utility function rather than repeated the logic here. I know I had to have it for realtime setup and I think I checked a different package inside the setup/internal package rather than the project package -- which seems more sensible and can be consistent across all setups.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'll note down that this should be extracted out into a new utility. I'll do that in a separate PR

require(path.join(getPaths().base, 'package.json')).devDependencies[
'@redwoodjs/core'
] ?? 'latest'

const tasks = new Listr(
[
{
title: `Adding api/src/lib/mailer.${
projectIsTypescript ? 'ts' : 'js'
}...`,
task: () => {
const templatePath = path.resolve(
__dirname,
'templates',
'mailer.ts.template'
)
const templateContent = fs.readFileSync(templatePath, {
encoding: 'utf8',
flag: 'r',
})

const mailerPath = path.join(
getPaths().api.lib,
`mailer.${projectIsTypescript ? 'ts' : 'js'}`
)
const mailerContent = projectIsTypescript
? templateContent
: transformTSToJS(mailerPath, templateContent)

return writeFile(mailerPath, mailerContent, {
overwriteExisting: force,
})
},
},
{
title: 'Adding api/src/mail directory...',
task: () => {
const mailDir = path.join(getPaths().api.mail)
if (!fs.existsSync(mailDir)) {
fs.mkdirSync(mailDir)
}
},
},
{
title: `Adding example ReactEmail mail template`,
skip: () => skipExamples,
task: () => {
const templatePath = path.resolve(
__dirname,
'templates',
're-example.tsx.template'
)
const templateContent = fs.readFileSync(templatePath, {
encoding: 'utf8',
flag: 'r',
})

const exampleTemplatePath = path.join(
getPaths().api.mail,
'Example',
`Example.${projectIsTypescript ? 'tsx' : 'jsx'}`
)
const exampleTemplateContent = projectIsTypescript
? templateContent
: transformTSToJS(exampleTemplatePath, templateContent)

return writeFile(exampleTemplatePath, exampleTemplateContent, {
overwriteExisting: force,
})
},
},
{
// Add production dependencies
...addApiPackages([
`@redwoodjs/mailer-core@${redwoodVersion}`,
`@redwoodjs/mailer-handler-nodemailer@${redwoodVersion}`,
`@redwoodjs/mailer-renderer-react-email@${redwoodVersion}`,
`@react-email/components`, // NOTE: Unpinned dependency here
]),
title: 'Adding production dependencies to your api side...',
},
{
// Add development dependencies
...addApiPackages([
'-D',
`@redwoodjs/mailer-handler-in-memory@${redwoodVersion}`,
`@redwoodjs/mailer-handler-studio@${redwoodVersion}`,
]),
title: 'Adding development dependencies to your api side...',
},
],
{
rendererOptions: { collapseSubtasks: false },
}
)

try {
await tasks.run()
} catch (e) {
errorTelemetry(process.argv, e.message)
console.error(c.error(e.message))
process.exit(e?.exitCode || 1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Mailer } from '@redwoodjs/mailer-core'
import { NodemailerMailHandler } from '@redwoodjs/mailer-handler-nodemailer'
import { ReactEmailRenderer } from '@redwoodjs/mailer-renderer-react-email'

import { logger } from 'src/lib/logger'

export const mailer = new Mailer({
handling: {
handlers: {
// TODO: Update this handler config or switch it out for a different handler completely
nodemailer: new NodemailerMailHandler({
transport: {
host: 'localhost',
port: 4319,
secure: false,
},
}),
},
default: 'nodemailer',
},

rendering: {
renderers: {
reactEmail: new ReactEmailRenderer(),
},
default: 'reactEmail',
},

logger,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react'

import {
Html,
Text,
Hr,
Body,
Head,
Tailwind,
Preview,
Container,
Heading,
} from '@react-email/components'

export function ExampleEmail(
{ when }: { when: string } = { when: new Date().toLocaleString() }
) {
return (
<Html lang="en">
<Head />
<Preview>An example email</Preview>
<Tailwind>
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-[40px] rounded border border-solid border-gray-200 p-[20px]">
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-black">
Example Email
</Heading>
<Text className="text-[14px] leading-[24px] text-black">
This is an example email which you can customise to your needs.
</Text>
<Hr className="mx-0 my-[26px] w-full border border-solid border-[#eaeaea]" />
<Text className="text-[12px] leading-[24px] text-[#666666]">
Message was sent on {when}
</Text>
</Container>
</Body>
</Tailwind>
</Html>
)
}
40 changes: 27 additions & 13 deletions packages/mailer/core/src/__tests__/mailer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,41 +325,53 @@ describe('Uses the correct modes', () => {
expect(console.warn).toBeCalledWith(
'The test handler is null, this will prevent mail from being processed in test mode'
)
})

test('development', () => {
console.warn.mockClear()
const _mailer2 = new Mailer({
const _mailer1 = new Mailer({
...baseConfig,
test: {
development: {
when: true,
handler: undefined,
handler: null,
},
})
expect(console.warn).toBeCalledWith(
'The test handler is null, this will prevent mail from being processed in test mode'
'The development handler is null, this will prevent mail from being processed in development mode'
)
})
})

test('development', () => {
describe('attempts to use fallback handlers', () => {
beforeAll(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {})
})

test('test', () => {
console.warn.mockClear()
const _mailer1 = new Mailer({
const _mailer = new Mailer({
...baseConfig,
development: {
test: {
when: true,
handler: null,
handler: undefined,
},
})
expect(console.warn).toBeCalledWith(
'The development handler is null, this will prevent mail from being processed in development mode'
"Automatically loaded the '@redwoodjs/mailer-handler-in-memory' handler, this will be used to process mail in test mode"
)
})

test('development', () => {
console.warn.mockClear()
const _mailer2 = new Mailer({
const _mailer = new Mailer({
...baseConfig,
development: {
when: true,
handler: undefined,
},
})
expect(console.warn).toBeCalledWith(
'The development handler is null, this will prevent mail from being processed in development mode'
"Automatically loaded the '@redwoodjs/mailer-handler-studio' handler, this will be used to process mail in development mode"
)
})
})
Expand Down Expand Up @@ -804,7 +816,7 @@ describe('Uses the correct modes', () => {
expect(mailerExplicitlyNullTestHandler.getTestHandler()).toBeNull()

const mailerNoTestHandlerDefined = new Mailer(baseConfig)
expect(mailerNoTestHandlerDefined.getTestHandler()).toBeNull()
expect(mailerNoTestHandlerDefined.getTestHandler()).not.toBeNull()
})

test('getDevelopmentHandler', () => {
Expand Down Expand Up @@ -835,7 +847,9 @@ describe('Uses the correct modes', () => {
).toBeNull()

const mailerNoDevelopmentHandlerDefined = new Mailer(baseConfig)
expect(mailerNoDevelopmentHandlerDefined.getDevelopmentHandler()).toBeNull()
expect(
mailerNoDevelopmentHandlerDefined.getDevelopmentHandler()
).not.toBeNull()
})

test('getDefaultProductionHandler', () => {
Expand Down
Loading
Loading