Skip to content
Closed
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
23 changes: 15 additions & 8 deletions packages/jsx-email/src/cli/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type IssueGroup, caniemail, groupIssues, sortIssues } from 'caniemail';
import chalk from 'chalk';
import chalkTmpl from 'chalk-template';
import stripAnsi from 'strip-ansi';
import { type InferOutput as Infer, parse as assert, object } from 'valibot';
import { type InferOutput as Infer, parse as assert, boolean, object, optional } from 'valibot';

import { formatBytes, gmailByteLimit, gmailBytesSafe } from '../helpers.js';

Expand All @@ -13,7 +13,9 @@ import { type CommandFn } from './types.js';

const { error, log } = console;

const CheckOptionsStruct = object({});
const CheckOptionsStruct = object({
usePreviewProps: optional(boolean())
});

type CheckOptions = Infer<typeof CheckOptionsStruct>;

Expand All @@ -34,18 +36,23 @@ Check jsx-email templates for client compatibility
{underline Usage}
$ email check <template file name>

{underline Options}
--use-preview-props
When set, use the \`previewProps\` exported by the template file (if present).

{underline Examples}
$ email check ./emails/Batman.tsx
`;

const formatNotes = (notes: string[], indent: string) => {
export const formatNotes = (notes: string[], indent: string) => {
if (!notes.length) return '';
const noteLines = (notes as string[]).join(`\n${'.'.repeat(indent.length)}**`);
console.log({ noteLines });
return chalkTmpl`\n${indent}{cyan Notes}:\n${'.'.repeat(indent.length)}asshole\n`;
const noteIndent = `${indent} `;
const noteLines = notes.join(`\n${noteIndent}`);

return chalkTmpl`\n${indent}{cyan Notes}:\n${noteIndent}{dim ${noteLines}}`;
};

const formatIssue = (group: IssueGroup): string => {
export const formatIssue = (group: IssueGroup): string => {
const { issue, clients } = group;
const { position, support, title } = issue;
const positionTuple = chalkTmpl`{dim ${position!.start.line}:${position!.start.column}}`;
Expand Down Expand Up @@ -129,7 +136,7 @@ export const command: CommandFn = async (argv: CheckOptions, input) => {
log(chalkTmpl`{blue Checking email template for Client Compatibility...}\n`);

const [file] = await buildTemplates({
buildOptions: { showStats: false, writeToFile: false },
buildOptions: { showStats: false, usePreviewProps: argv.usePreviewProps, writeToFile: false },
targetPath: input[0]
});

Expand Down
120 changes: 120 additions & 0 deletions packages/jsx-email/test/cli/check.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import { join } from 'node:path';

import stripAnsi from 'strip-ansi';

import { build } from '../../src/cli/commands/build.js';
import { command, formatIssue, formatNotes, help } from '../../src/cli/commands/check.js';

vi.mock('caniemail', () => ({
caniemail: vi.fn(() => ({ success: true, issues: {} })),
groupIssues: vi.fn(() => []),
sortIssues: vi.fn(() => [])
}));

vi.mock('../../src/cli/commands/build.js', async () => {
const actual = (await vi.importActual('../../src/cli/commands/build.js')) as Record<
string,
unknown
>;

return {
...actual,
buildTemplates: vi.fn(async ({ targetPath, buildOptions }: any) => [
{
fileName: targetPath,
html: '<html></html>'
}
])
};
});

describe('email check', async () => {
test('help includes --use-preview-props', async () => {
expect(stripAnsi(help)).toContain('--use-preview-props');
});

test('formats notes and issues without placeholder/debug output', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

const notesText = stripAnsi(formatNotes(['1. First', '2. Second'], ' '));
expect(notesText).toContain('Notes:');
expect(notesText).toContain('1. First');
expect(notesText).toContain('2. Second');
expect(notesText).not.toContain('asshole');
expect(logSpy).not.toHaveBeenCalled();

const formatted = stripAnsi(
formatIssue({
issue: {
notes: ['First', 'Second'],
position: { start: { line: 3, column: 5 } },
support: 'partial',
title: 'Some issue'
},
clients: ['gmail.ios', 'outlook.windows']
} as any)
);

expect(formatted).toContain('warn');
expect(formatted).toContain('3:5');
expect(formatted).toContain('Some issue:');
expect(formatted).toContain('Notes:');
expect(formatted).toContain('1. First');
expect(formatted).toContain('2. Second');
expect(formatted).toContain('gmail.ios');
expect(formatted).toContain('outlook.windows');

logSpy.mockRestore();
});

test('passes --use-preview-props through to buildTemplates', async () => {
const dir = await mkdtemp(join(os.tmpdir(), 'jsx-email-check-'));
const templatePath = join(dir, 'template.tsx');
await writeFile(templatePath, 'export const Template = () => null;', 'utf8');

const { buildTemplates } = await import('../../src/cli/commands/build.js');

await command({ usePreviewProps: true } as any, [templatePath]);

expect(buildTemplates).toHaveBeenCalledWith({
buildOptions: { showStats: false, usePreviewProps: true, writeToFile: false },
targetPath: templatePath
});
});

test('build uses previewProps when usePreviewProps is enabled', async () => {
const dir = await mkdtemp(join(process.cwd(), '.tmp-jsx-email-preview-props-'));
const out = join(dir, 'out');
const modulePath = join(dir, 'template.mjs');

await writeFile(
modulePath,
[
"import React from 'react';",
"export const previewProps = { name: 'Bruce' };",
"export const Template = (props) => React.createElement('p', null, props.name ?? 'missing');",
''
].join('\n'),
'utf8'
);

const withPreviewProps = await build({
argv: { out, usePreviewProps: true, writeToFile: false } as any,
path: modulePath,
sourceFile: modulePath
});

const withoutPreviewProps = await build({
argv: { out, props: '{"name":"Clark"}', writeToFile: false } as any,
path: modulePath,
sourceFile: modulePath
});

expect(withPreviewProps.html).toContain('Bruce');
expect(withoutPreviewProps.html).toContain('Clark');

await rm(dir, { force: true, recursive: true });
});
Comment on lines +72 to +119

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The new test suite creates temp directories, but cleanup is not guaranteed if an assertion fails (or if build() throws). This can leave .tmp-jsx-email-preview-props-* directories behind in local runs/CI workspaces.

Also, this test mixes os.tmpdir() (good) and process.cwd() (riskier) for temp dirs; prefer one consistent approach (ideally os.tmpdir()).

Suggestion

Wrap temp-dir lifecycle in try/finally (or use afterEach) and use os.tmpdir() consistently.

test('build uses previewProps when usePreviewProps is enabled', async () => {
  const dir = await mkdtemp(join(os.tmpdir(), 'jsx-email-preview-props-'));
  try {
    const out = join(dir, 'out');
    const modulePath = join(dir, 'template.mjs');

    await writeFile(
      modulePath,
      [
        "import React from 'react';",
        "export const previewProps = { name: 'Bruce' };",
        "export const Template = (props) => React.createElement('p', null, props.name ?? 'missing');",
        ''
      ].join('\n'),
      'utf8'
    );

    const withPreviewProps = await build({
      argv: { out, usePreviewProps: true, writeToFile: false } as any,
      path: modulePath,
      sourceFile: modulePath
    });

    const withoutPreviewProps = await build({
      argv: { out, props: '{"name":"Clark"}', writeToFile: false } as any,
      path: modulePath,
      sourceFile: modulePath
    });

    expect(withPreviewProps.html).toContain('Bruce');
    expect(withoutPreviewProps.html).toContain('Clark');
  } finally {
    await rm(dir, { force: true, recursive: true });
  }
});

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.

});
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ exports[`defineConfig > basic set 1`] = `
"reject": [Function],
"resolve": [Function],
},
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"time": [Function],
"transports": undefined,
},
Expand All @@ -40,7 +40,7 @@ exports[`defineConfig > basic set 1`] = `
"WARN": 3,
},
},
"name": "dot-log:∵ root/minify",
"name": "dot-log:∵ root/minify",
"options": {
"factory": LogFactory {
"options": {
Expand All @@ -51,7 +51,7 @@ exports[`defineConfig > basic set 1`] = `
"reject": [Function],
"resolve": [Function],
},
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"time": [Function],
"transports": undefined,
},
Expand All @@ -71,9 +71,9 @@ exports[`defineConfig > basic set 1`] = `
"WARN": 3,
},
},
"id": "dot-log:∵ root/minify",
"id": "dot-log:∵ root/minify",
"level": "info",
"name": "dot-log:∵ root/minify",
"name": "dot-log:∵ root/minify",
"prefix": undefined,
},
"trace": [Function],
Expand Down Expand Up @@ -116,7 +116,7 @@ exports[`defineConfig > de-dupe plugins 1`] = `
"reject": [Function],
"resolve": [Function],
},
"template": "[{{time}}] jsx-email ∵ batman {{level}} ",
"template": "[{{time}}] jsx-email ∵ batman {{level}} ",
"time": [Function],
"transports": undefined,
},
Expand All @@ -136,7 +136,7 @@ exports[`defineConfig > de-dupe plugins 1`] = `
"WARN": 3,
},
},
"name": "dot-log:∵ batman",
"name": "dot-log:∵ batman",
"options": {
"factory": LogFactory {
"options": {
Expand All @@ -147,7 +147,7 @@ exports[`defineConfig > de-dupe plugins 1`] = `
"reject": [Function],
"resolve": [Function],
},
"template": "[{{time}}] jsx-email ∵ batman {{level}} ",
"template": "[{{time}}] jsx-email ∵ batman {{level}} ",
"time": [Function],
"transports": undefined,
},
Expand All @@ -167,9 +167,9 @@ exports[`defineConfig > de-dupe plugins 1`] = `
"WARN": 3,
},
},
"id": "dot-log:∵ batman",
"id": "dot-log:∵ batman",
"level": "info",
"name": "dot-log:∵ batman",
"name": "dot-log:∵ batman",
"prefix": undefined,
},
"trace": [Function],
Expand Down Expand Up @@ -226,7 +226,7 @@ exports[`defineConfig > minify and pretty 1`] = `
"reject": [Function],
"resolve": [Function],
},
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"time": [Function],
"transports": undefined,
},
Expand All @@ -246,7 +246,7 @@ exports[`defineConfig > minify and pretty 1`] = `
"WARN": 3,
},
},
"name": "dot-log:∵ root/minify",
"name": "dot-log:∵ root/minify",
"options": {
"factory": LogFactory {
"options": {
Expand All @@ -257,7 +257,7 @@ exports[`defineConfig > minify and pretty 1`] = `
"reject": [Function],
"resolve": [Function],
},
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"time": [Function],
"transports": undefined,
},
Expand All @@ -277,9 +277,9 @@ exports[`defineConfig > minify and pretty 1`] = `
"WARN": 3,
},
},
"id": "dot-log:∵ root/minify",
"id": "dot-log:∵ root/minify",
"level": "info",
"name": "dot-log:∵ root/minify",
"name": "dot-log:∵ root/minify",
"prefix": undefined,
},
"trace": [Function],
Expand Down Expand Up @@ -307,7 +307,7 @@ exports[`defineConfig > minify and pretty 1`] = `
"reject": [Function],
"resolve": [Function],
},
"template": "[{{time}}] jsx-email ∵ root/pretty {{level}} ",
"template": "[{{time}}] jsx-email ∵ root/pretty {{level}} ",
"time": [Function],
"transports": undefined,
},
Expand All @@ -327,7 +327,7 @@ exports[`defineConfig > minify and pretty 1`] = `
"WARN": 3,
},
},
"name": "dot-log:∵ root/pretty",
"name": "dot-log:∵ root/pretty",
"options": {
"factory": LogFactory {
"options": {
Expand All @@ -338,7 +338,7 @@ exports[`defineConfig > minify and pretty 1`] = `
"reject": [Function],
"resolve": [Function],
},
"template": "[{{time}}] jsx-email ∵ root/pretty {{level}} ",
"template": "[{{time}}] jsx-email ∵ root/pretty {{level}} ",
"time": [Function],
"transports": undefined,
},
Expand All @@ -358,9 +358,9 @@ exports[`defineConfig > minify and pretty 1`] = `
"WARN": 3,
},
},
"id": "dot-log:∵ root/pretty",
"id": "dot-log:∵ root/pretty",
"level": "info",
"name": "dot-log:∵ root/pretty",
"name": "dot-log:∵ root/pretty",
"prefix": undefined,
},
"trace": [Function],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ exports[`loadConfig → dotdir > loadConfig 1`] = `
"reject": [Function],
"resolve": [Function],
},
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"time": [Function],
"transports": undefined,
},
Expand All @@ -40,7 +40,7 @@ exports[`loadConfig → dotdir > loadConfig 1`] = `
"WARN": 3,
},
},
"name": "dot-log:∵ root/minify",
"name": "dot-log:∵ root/minify",
"options": {
"factory": LogFactory {
"options": {
Expand All @@ -51,7 +51,7 @@ exports[`loadConfig → dotdir > loadConfig 1`] = `
"reject": [Function],
"resolve": [Function],
},
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"template": "[{{time}}] jsx-email ∵ root/minify {{level}} ",
"time": [Function],
"transports": undefined,
},
Expand All @@ -71,9 +71,9 @@ exports[`loadConfig → dotdir > loadConfig 1`] = `
"WARN": 3,
},
},
"id": "dot-log:∵ root/minify",
"id": "dot-log:∵ root/minify",
"level": "info",
"name": "dot-log:∵ root/minify",
"name": "dot-log:∵ root/minify",
"prefix": undefined,
},
"trace": [Function],
Expand Down
Loading
Loading