Skip to content

Commit

Permalink
feat(cli): Setup command for mailer (#9335)
Browse files Browse the repository at this point in the history
**Description**
Mailer will be available from a setup command: `yarn rw setup mailer`.
This PR adds that command.

Like all good PRs this also does a second thing. This adds functionality
to the mailer which enables it to conditionally use an in-memory handler
for test and the studio handler for development if those dependencies
are available. This is essentially adding automatic defaults but
respecting if the dependencies are available.

**Changes**
1. Adds a setup command: `yarn rw setup mailer`
2. Implements fallback/default dev & test handlers for when the user has
not set specific configuration.

**Notes**
1. I'm not overly thrilled with the typing/implementation of these
automatic/fallback test/dev handlers but we can improve that going
forward.

**Future work**
1. We should improve the logging output.
2. I NEED to write the docs now.

---------

Co-authored-by: David Thyresson <[email protected]>
  • Loading branch information
2 people authored and jtoar committed Nov 1, 2023
1 parent 3d22dc7 commit 032d793
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 51 deletions.
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 =
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

0 comments on commit 032d793

Please sign in to comment.