toiljs 0.0.45 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +12 -2
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/email-preview.d.ts +12 -0
- package/build/compiler/email-preview.js +253 -0
- package/build/compiler/emails.d.ts +6 -3
- package/build/compiler/emails.js +52 -12
- package/build/compiler/index.js +14 -0
- package/build/compiler/plugin.js +64 -2
- package/build/compiler/vite.js +1 -0
- package/docs/email.md +35 -19
- package/package.json +2 -2
- package/src/cli/create.ts +2 -2
- package/src/cli/doctor.ts +15 -0
- package/src/compiler/email-preview.ts +305 -0
- package/src/compiler/emails.ts +82 -12
- package/src/compiler/index.ts +19 -0
- package/src/compiler/plugin.ts +88 -4
- package/src/compiler/vite.ts +4 -0
- package/test/email-preview.test.ts +68 -0
- package/test/emails.test.ts +58 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { createServer } from 'vite';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { loadConfig } from '../src/compiler/config';
|
|
7
|
+
import {
|
|
8
|
+
emailsVersion,
|
|
9
|
+
listEmails,
|
|
10
|
+
previewShellHtml,
|
|
11
|
+
renderEmailByName,
|
|
12
|
+
} from '../src/compiler/email-preview';
|
|
13
|
+
import { createViteConfig } from '../src/compiler/vite';
|
|
14
|
+
|
|
15
|
+
const EXAMPLE = path.resolve(__dirname, '../examples/basic');
|
|
16
|
+
|
|
17
|
+
describe('email preview end-to-end (examples/basic)', () => {
|
|
18
|
+
it('lists Welcome and inlines its emails/styles/email.css; client/* alias resolves', async () => {
|
|
19
|
+
const cfg = await loadConfig({ root: EXAMPLE });
|
|
20
|
+
const items = listEmails(cfg);
|
|
21
|
+
expect(items.map((i) => i.name)).toContain('Welcome');
|
|
22
|
+
|
|
23
|
+
const server = await createServer({
|
|
24
|
+
...(await createViteConfig(cfg)),
|
|
25
|
+
server: { middlewareMode: true, hmr: false },
|
|
26
|
+
appType: 'custom',
|
|
27
|
+
logLevel: 'silent',
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
const r = await renderEmailByName(server, cfg, 'Welcome');
|
|
31
|
+
if (!r) throw new Error('Welcome did not render');
|
|
32
|
+
// tokens discovered from props
|
|
33
|
+
expect(r.tokens).toEqual(['code', 'name']);
|
|
34
|
+
// subject token template
|
|
35
|
+
expect(r.subject).toBe('Welcome, {{name}}!');
|
|
36
|
+
// .email-title { color: #111827 } from emails/styles/email.css inlined onto the <h1>
|
|
37
|
+
expect(r.html).toMatch(/<h1[^>]*style="[^"]*color:\s*#111827/i);
|
|
38
|
+
// .email-card backgroundColor inlined onto the <table>
|
|
39
|
+
expect(r.html).toMatch(/<table[^>]*style="[^"]*background-color:\s*#f6f7f9/i);
|
|
40
|
+
|
|
41
|
+
// The `client/*` reuse alias still resolves project CSS (the documented
|
|
42
|
+
// `import 'client/styles/…'` path), independent of where the demo keeps its styles.
|
|
43
|
+
const aliased = (await server.ssrLoadModule('client/styles/main.css?inline')) as {
|
|
44
|
+
default?: unknown;
|
|
45
|
+
};
|
|
46
|
+
expect(typeof aliased.default).toBe('string');
|
|
47
|
+
} finally {
|
|
48
|
+
await server.close();
|
|
49
|
+
}
|
|
50
|
+
}, 30000);
|
|
51
|
+
|
|
52
|
+
it('emailsVersion is a non-empty mtime:count fingerprint', async () => {
|
|
53
|
+
const cfg = await loadConfig({ root: EXAMPLE });
|
|
54
|
+
const v = emailsVersion(cfg);
|
|
55
|
+
expect(v).toMatch(/^\d+(\.\d+)?:\d+$/);
|
|
56
|
+
// at least Welcome.tsx + the client CSS files were counted
|
|
57
|
+
expect(Number(v.split(':')[1])).toBeGreaterThan(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('the preview shell wires the dev endpoints', () => {
|
|
61
|
+
const html = previewShellHtml();
|
|
62
|
+
expect(html).toContain("var BASE = '/__toil/emails'");
|
|
63
|
+
for (const frag of ["BASE + '/list'", "BASE + '/render?name='", "BASE + '/version'"]) {
|
|
64
|
+
expect(html).toContain(frag);
|
|
65
|
+
}
|
|
66
|
+
expect(html).toContain('/__toil/open?file='); // open-in-editor
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createElement, type ReactElement } from 'react';
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { __test } from '../src/compiler/emails';
|
|
6
|
+
|
|
7
|
+
const render = (el: unknown): string => renderToStaticMarkup(el as ReactElement);
|
|
8
|
+
|
|
9
|
+
describe('renderModule', () => {
|
|
10
|
+
it('discovers props as {{tokens}} and renders placeholders (alpha-sorted, deduped)', async () => {
|
|
11
|
+
const mod = {
|
|
12
|
+
default: (p: { name: string; code: string }) =>
|
|
13
|
+
createElement('p', null, `Hi ${p.name}, code ${p.code}`),
|
|
14
|
+
};
|
|
15
|
+
const r = await __test.renderModule('Welcome', mod, render);
|
|
16
|
+
if (!r) throw new Error('expected a rendered email');
|
|
17
|
+
expect(r.tokens).toEqual(['code', 'name']);
|
|
18
|
+
expect(r.html).toContain('{{name}}');
|
|
19
|
+
expect(r.html).toContain('{{code}}');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns null when there is no default-exported component', async () => {
|
|
23
|
+
expect(await __test.renderModule('X', { default: 'nope' }, render)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('inlines imported CSS into element style="" (the reuse path)', async () => {
|
|
27
|
+
const mod = { default: () => createElement('h1', { className: 'email-title' }, 'Hello') };
|
|
28
|
+
const css = '.email-title { color: #111827; font-size: 22px; }';
|
|
29
|
+
const r = await __test.renderModule('Styled', mod, render, css);
|
|
30
|
+
if (!r) throw new Error('expected a rendered email');
|
|
31
|
+
// The class rule is moved onto the element as an inline style by the inliner.
|
|
32
|
+
expect(r.html).toMatch(/<h1[^>]*style="[^"]*color:\s*#111827/i);
|
|
33
|
+
expect(r.html).toMatch(/font-size:\s*22px/i);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('renderModuleSource', () => {
|
|
38
|
+
it('generates a typed Emails.<Name>.send with alpha-sorted token params', () => {
|
|
39
|
+
const src = __test.renderModuleSource([
|
|
40
|
+
{
|
|
41
|
+
name: 'Welcome',
|
|
42
|
+
subject: 'Welcome, {{name}}!',
|
|
43
|
+
html: '<p>{{code}}</p>',
|
|
44
|
+
text: 'code {{code}}',
|
|
45
|
+
tokens: ['code', 'name'],
|
|
46
|
+
purpose: 'welcome',
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
expect(src).toContain('export namespace Emails {');
|
|
50
|
+
expect(src).toContain('export namespace Welcome {');
|
|
51
|
+
expect(src).toContain(
|
|
52
|
+
'export function send(to: string, code: string, name: string, purpose: string = "welcome")',
|
|
53
|
+
);
|
|
54
|
+
expect(src).toContain(
|
|
55
|
+
'return new EmailTemplate(SUBJECT, TEXT, HTML).send(to, __v, purpose);',
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
});
|