-
Notifications
You must be signed in to change notification settings - Fork 1k
feat(react-email): add email config support #3411
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
base: canary
Are you sure you want to change the base?
Changes from 3 commits
fc01648
0ecdc4a
2fa8dd5
30c0182
e84e8f9
a74524b
a9f3fe3
9e16701
3c80930
53a5ff1
4c418dd
78396db
7babd59
4aab66c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import fs from 'node:fs'; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this test is quite useful, as the code is quite simple and just reading it makes it clear if it's right or not. The only effect that this has is making it harder to change the code.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed the test as per your feedback |
||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { getEnvVariablesForPreviewApp } from './get-env-variables-for-preview-app'; | ||
|
|
||
| describe('getEnvVariablesForPreviewApp()', () => { | ||
| let temporaryDirectory = ''; | ||
|
|
||
| beforeEach(() => { | ||
| temporaryDirectory = fs.mkdtempSync( | ||
| path.join(os.tmpdir(), 'react-email-preview-env-'), | ||
| ); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| fs.rmSync(temporaryDirectory, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| it('includes the discovered email config path', () => { | ||
| const emailConfigPath = path.join(temporaryDirectory, 'email.config.cts'); | ||
| fs.writeFileSync(emailConfigPath, 'export default {};\n', 'utf8'); | ||
|
|
||
| expect( | ||
| getEnvVariablesForPreviewApp('emails', '/preview', temporaryDirectory), | ||
| ).toMatchObject({ | ||
| REACT_EMAIL_INTERNAL_EMAIL_CONFIG_PATH: emailConfigPath, | ||
| REACT_EMAIL_INTERNAL_EMAILS_DIR_RELATIVE_PATH: 'emails', | ||
| REACT_EMAIL_INTERNAL_EMAILS_DIR_ABSOLUTE_PATH: path.join( | ||
| temporaryDirectory, | ||
| 'emails', | ||
| ), | ||
| REACT_EMAIL_INTERNAL_PREVIEW_SERVER_LOCATION: '/preview', | ||
| REACT_EMAIL_INTERNAL_USER_PROJECT_LOCATION: temporaryDirectory, | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import fs from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { | ||
| getEmailConfigPath, | ||
| supportedEmailConfigFilenames, | ||
| } from './get-email-config-path'; | ||
|
|
||
| describe('getEmailConfigPath()', () => { | ||
| let temporaryDirectory = ''; | ||
|
|
||
| beforeEach(() => { | ||
| temporaryDirectory = fs.mkdtempSync( | ||
| path.join(os.tmpdir(), 'react-email-config-path-'), | ||
| ); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| fs.rmSync(temporaryDirectory, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| it('returns undefined when there is no config file', () => { | ||
| expect(getEmailConfigPath(temporaryDirectory)).toBeUndefined(); | ||
| }); | ||
|
|
||
| it.each( | ||
| supportedEmailConfigFilenames, | ||
| )('detects a %s config file', (filename) => { | ||
| const emailConfigPath = path.join(temporaryDirectory, filename); | ||
| fs.writeFileSync(emailConfigPath, 'export default {};\n', 'utf8'); | ||
|
|
||
| expect(getEmailConfigPath(temporaryDirectory)).toBe(emailConfigPath); | ||
| }); | ||
|
|
||
| it('prefers the first supported filename when multiple configs exist', () => { | ||
| for (const filename of supportedEmailConfigFilenames) { | ||
| fs.writeFileSync( | ||
| path.join(temporaryDirectory, filename), | ||
| 'export default {};\n', | ||
| 'utf8', | ||
| ); | ||
| } | ||
|
|
||
| expect(getEmailConfigPath(temporaryDirectory)).toBe( | ||
| path.join(temporaryDirectory, supportedEmailConfigFilenames[0]!), | ||
| ); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
|
|
||
| export const supportedEmailConfigFilenames = [ | ||
| 'email.config.ts', | ||
| 'email.config.mts', | ||
| 'email.config.cts', | ||
| 'email.config.js', | ||
| 'email.config.mjs', | ||
| 'email.config.cjs', | ||
| ]; | ||
|
|
||
| export const getEmailConfigPath = (userProjectLocation: string) => { | ||
| for (const filename of supportedEmailConfigFilenames) { | ||
| const emailConfigPath = path.join(userProjectLocation, filename); | ||
|
|
||
| if (fs.existsSync(emailConfigPath)) { | ||
| return emailConfigPath; | ||
| } | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { createJiti } from 'jiti'; | ||
| import { getEmailConfig } from './get-email-config'; | ||
|
|
||
| vi.mock('jiti', () => ({ | ||
| createJiti: vi.fn(), | ||
| })); | ||
|
|
||
| describe('getEmailConfig()', () => { | ||
| const mockedCreateJiti = vi.mocked(createJiti); | ||
|
|
||
| beforeEach(() => { | ||
| mockedCreateJiti.mockReset(); | ||
| }); | ||
|
|
||
| it('returns an empty config when no path is provided', async () => { | ||
| await expect(getEmailConfig()).resolves.toEqual({}); | ||
| expect(mockedCreateJiti).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('loads a config object from disk', async () => { | ||
| const importMock = vi.fn().mockResolvedValue({ | ||
| esbuild: { | ||
| plugins: [{ name: 'test-plugin', setup: vi.fn() }], | ||
| }, | ||
| }); | ||
| mockedCreateJiti.mockReturnValue({ | ||
| import: importMock, | ||
| } as unknown as ReturnType<typeof createJiti>); | ||
|
|
||
| const config = await getEmailConfig('/tmp/email.config.ts'); | ||
|
|
||
| expect(config).toMatchObject({ | ||
| esbuild: { | ||
| plugins: [{ name: 'test-plugin' }], | ||
| }, | ||
| }); | ||
|
|
||
| expect(mockedCreateJiti).toHaveBeenCalledWith('/tmp/email.config.ts'); | ||
| expect(importMock).toHaveBeenCalledWith('/tmp/email.config.ts', { | ||
| default: true, | ||
| }); | ||
| }); | ||
|
|
||
| it('rejects configs that do not export an object', async () => { | ||
| mockedCreateJiti.mockReturnValue({ | ||
| import: vi.fn().mockResolvedValue(null), | ||
| } as unknown as ReturnType<typeof createJiti>); | ||
|
|
||
| await expect(getEmailConfig('/tmp/email.config.ts')).rejects.toThrow( | ||
| 'Expected React Email config at /tmp/email.config.ts to export an object.', | ||
| ); | ||
| }); | ||
|
|
||
| it('rejects configs with a non-array esbuild.plugins value', async () => { | ||
| mockedCreateJiti.mockReturnValue({ | ||
| import: vi.fn().mockResolvedValue({ | ||
| esbuild: { | ||
| plugins: {}, | ||
| }, | ||
| }), | ||
| } as unknown as ReturnType<typeof createJiti>); | ||
|
|
||
| await expect(getEmailConfig('/tmp/email.config.ts')).rejects.toThrow( | ||
| 'Expected "esbuild.plugins" in React Email config at /tmp/email.config.ts to be an array.', | ||
| ); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.