vintasend-react-email 0.9.1 → 0.10.0
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/README.md +61 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/react-email-inline-template-renderer.d.ts +24 -0
- package/dist/react-email-inline-template-renderer.d.ts.map +1 -0
- package/dist/react-email-inline-template-renderer.js +123 -0
- package/dist/scripts/compile-react-email-templates.d.ts +10 -0
- package/dist/scripts/compile-react-email-templates.d.ts.map +1 -0
- package/dist/scripts/compile-react-email-templates.js +102 -0
- package/package.json +10 -4
- package/src/scripts/compile-react-email-templates.ts +140 -0
package/README.md
CHANGED
|
@@ -19,6 +19,40 @@ npm install vintasend-react-email
|
|
|
19
19
|
|
|
20
20
|
- `ReactEmailTemplateRenderer`
|
|
21
21
|
- `ReactEmailTemplateRendererFactory`
|
|
22
|
+
- `ReactEmailInlineTemplateRenderer`
|
|
23
|
+
- `ReactEmailInlineTemplateRendererFactory`
|
|
24
|
+
|
|
25
|
+
## Template compilation script
|
|
26
|
+
|
|
27
|
+
For environments that can only deploy a single source file (e.g. Medplum bots), you can pre-compile template source files into a JSON map and then feed that map to an inline renderer.
|
|
28
|
+
|
|
29
|
+
This package provides a CLI:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
compile-react-email-templates [input-directory] [output-file]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Supported template extensions:
|
|
36
|
+
|
|
37
|
+
- `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.mjs`, `.cjs`
|
|
38
|
+
|
|
39
|
+
### Add to `package.json`
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"scripts": {
|
|
44
|
+
"compile-templates": "compile-react-email-templates ./notification-templates ./compiled-react-email-templates.json"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Run
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm run compile-templates
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The output JSON uses template paths (relative to `input-directory`) as keys and raw template source as values.
|
|
22
56
|
|
|
23
57
|
## How it works
|
|
24
58
|
|
|
@@ -63,6 +97,27 @@ import { ReactEmailTemplateRendererFactory } from 'vintasend-react-email';
|
|
|
63
97
|
const templateRenderer = new ReactEmailTemplateRendererFactory<MyConfig>().create();
|
|
64
98
|
```
|
|
65
99
|
|
|
100
|
+
### 1.1) Create inline renderer instance (for pre-compiled templates)
|
|
101
|
+
|
|
102
|
+
Use this when your runtime cannot read template files directly and you want to load templates from a JSON map.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import compiledTemplates from './compiled-react-email-templates.json';
|
|
106
|
+
import { ReactEmailInlineTemplateRendererFactory } from 'vintasend-react-email';
|
|
107
|
+
|
|
108
|
+
const inlineTemplateRenderer = new ReactEmailInlineTemplateRendererFactory<MyConfig>().create(
|
|
109
|
+
compiledTemplates,
|
|
110
|
+
);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Expected map shape:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
type CompiledTemplates = Record<string, string>;
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Where keys match the values you pass in `notification.bodyTemplate` and `notification.subjectTemplate`.
|
|
120
|
+
|
|
66
121
|
### 2) Use with your adapter (example)
|
|
67
122
|
|
|
68
123
|
```ts
|
|
@@ -70,6 +125,12 @@ const templateRenderer = new ReactEmailTemplateRendererFactory<MyConfig>().creat
|
|
|
70
125
|
const adapter = new SomeEmailAdapterFactory<MyConfig>().create(templateRenderer);
|
|
71
126
|
```
|
|
72
127
|
|
|
128
|
+
Inline renderer with adapter:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const adapter = new SomeEmailAdapterFactory<MyConfig>().create(inlineTemplateRenderer);
|
|
132
|
+
```
|
|
133
|
+
|
|
73
134
|
### 3) Template files
|
|
74
135
|
|
|
75
136
|
`subject-template.ts`:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { ReactEmailTemplateRenderer, ReactEmailTemplateRendererFactory } from './react-email-template-renderer';
|
|
2
|
+
export { ReactEmailInlineTemplateRenderer, ReactEmailInlineTemplateRendererFactory, } from './react-email-inline-template-renderer';
|
|
2
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,iCAAiC,EAAE,MAAM,iCAAiC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,iCAAiC,EAAE,MAAM,iCAAiC,CAAC;AAChH,OAAO,EACN,gCAAgC,EAChC,uCAAuC,GACvC,MAAM,wCAAwC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BaseEmailTemplateRenderer, EmailTemplate, EmailTemplateContent } from 'vintasend/dist/services/notification-template-renderers/base-email-template-renderer';
|
|
2
|
+
import type { JsonObject } from 'vintasend/dist/types/json-values';
|
|
3
|
+
import type { BaseLogger } from 'vintasend/dist/services/loggers/base-logger';
|
|
4
|
+
import type { AnyNotification, DatabaseNotification } from 'vintasend/dist/types/notification';
|
|
5
|
+
import type { BaseNotificationTypeConfig } from 'vintasend/dist/types/notification-type-config';
|
|
6
|
+
export declare class ReactEmailInlineTemplateRenderer<Config extends BaseNotificationTypeConfig> implements BaseEmailTemplateRenderer<Config> {
|
|
7
|
+
private readonly templates;
|
|
8
|
+
private logger;
|
|
9
|
+
constructor(generatedTemplates: Record<string, string>);
|
|
10
|
+
injectLogger(logger: BaseLogger): void;
|
|
11
|
+
render(notification: DatabaseNotification<Config>, context: JsonObject): Promise<EmailTemplate>;
|
|
12
|
+
renderFromTemplateContent(notification: AnyNotification<Config>, templateContent: EmailTemplateContent, context: JsonObject): Promise<EmailTemplate>;
|
|
13
|
+
private compileSourceToCacheFile;
|
|
14
|
+
private loadTemplateFactoryFromContent;
|
|
15
|
+
private buildContentModuleSource;
|
|
16
|
+
private looksLikeTemplateModule;
|
|
17
|
+
private looksLikeFunctionBody;
|
|
18
|
+
private renderBodyOutput;
|
|
19
|
+
private renderSubjectOutput;
|
|
20
|
+
}
|
|
21
|
+
export declare class ReactEmailInlineTemplateRendererFactory<Config extends BaseNotificationTypeConfig> {
|
|
22
|
+
create(generatedTemplates: Record<string, string>): ReactEmailInlineTemplateRenderer<Config>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=react-email-inline-template-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-email-inline-template-renderer.d.ts","sourceRoot":"","sources":["../src/react-email-inline-template-renderer.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,yBAAyB,EACzB,aAAa,EACb,oBAAoB,EACrB,MAAM,sFAAsF,CAAC;AAC9F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6CAA6C,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC/F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+CAA+C,CAAC;AAWhG,qBAAa,gCAAgC,CAAC,MAAM,SAAS,0BAA0B,CACrF,YAAW,yBAAyB,CAAC,MAAM,CAAC;IAE5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,MAAM,CAA2B;gBAE7B,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAItD,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAIhC,MAAM,CACV,YAAY,EAAE,oBAAoB,CAAC,MAAM,CAAC,EAC1C,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,aAAa,CAAC;IAsCnB,yBAAyB,CAC7B,YAAY,EAAE,eAAe,CAAC,MAAM,CAAC,EACrC,eAAe,EAAE,oBAAoB,EACrC,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,aAAa,CAAC;YAwBX,wBAAwB;YAwBxB,8BAA8B;IAiB5C,OAAO,CAAC,wBAAwB;IAmBhC,OAAO,CAAC,uBAAuB;IAI/B,OAAO,CAAC,qBAAqB;YAMf,gBAAgB;IAQ9B,OAAO,CAAC,mBAAmB;CAO5B;AAED,qBAAa,uCAAuC,CAAC,MAAM,SAAS,0BAA0B;IAC5F,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CAGlD"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { render as renderReactEmail } from '@react-email/render';
|
|
7
|
+
import { transform } from 'esbuild';
|
|
8
|
+
const COMPILED_TEMPLATE_CACHE_DIR = join(tmpdir(), 'vintasend-react-email-inline-templates');
|
|
9
|
+
export class ReactEmailInlineTemplateRenderer {
|
|
10
|
+
constructor(generatedTemplates) {
|
|
11
|
+
this.logger = null;
|
|
12
|
+
this.templates = generatedTemplates;
|
|
13
|
+
}
|
|
14
|
+
injectLogger(logger) {
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
}
|
|
17
|
+
async render(notification, context) {
|
|
18
|
+
this.logger?.info(`Rendering inline email template for notification ${notification.id}`);
|
|
19
|
+
const bodyTemplateKey = notification.bodyTemplate;
|
|
20
|
+
if (!bodyTemplateKey) {
|
|
21
|
+
throw new Error('Body template is required');
|
|
22
|
+
}
|
|
23
|
+
const bodyTemplateContent = this.templates[bodyTemplateKey];
|
|
24
|
+
if (bodyTemplateContent === undefined) {
|
|
25
|
+
throw new Error(`Body template "${bodyTemplateKey}" not found in templates`);
|
|
26
|
+
}
|
|
27
|
+
const subjectTemplateKey = notification.subjectTemplate;
|
|
28
|
+
if (!subjectTemplateKey) {
|
|
29
|
+
throw new Error('Subject template is required');
|
|
30
|
+
}
|
|
31
|
+
const subjectTemplateContent = this.templates[subjectTemplateKey];
|
|
32
|
+
if (subjectTemplateContent === undefined) {
|
|
33
|
+
throw new Error(`Subject template "${subjectTemplateKey}" not found in templates`);
|
|
34
|
+
}
|
|
35
|
+
const bodyTemplateFactory = await this.loadTemplateFactoryFromContent(bodyTemplateContent, 'body');
|
|
36
|
+
const subjectTemplateFactory = await this.loadTemplateFactoryFromContent(subjectTemplateContent, 'subject');
|
|
37
|
+
const bodyOutput = await bodyTemplateFactory(context);
|
|
38
|
+
const subjectOutput = await subjectTemplateFactory(context);
|
|
39
|
+
return {
|
|
40
|
+
subject: this.renderSubjectOutput(subjectOutput),
|
|
41
|
+
body: await this.renderBodyOutput(bodyOutput),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async renderFromTemplateContent(notification, templateContent, context) {
|
|
45
|
+
this.logger?.info(`Rendering inline email template from content for notification ${notification.id}`);
|
|
46
|
+
if (!templateContent.subject) {
|
|
47
|
+
throw new Error('Subject template is required');
|
|
48
|
+
}
|
|
49
|
+
const subjectTemplateFactory = await this.loadTemplateFactoryFromContent(templateContent.subject, 'subject');
|
|
50
|
+
const bodyTemplateFactory = await this.loadTemplateFactoryFromContent(templateContent.body, 'body');
|
|
51
|
+
const subjectOutput = await subjectTemplateFactory(context);
|
|
52
|
+
const bodyOutput = await bodyTemplateFactory(context);
|
|
53
|
+
return {
|
|
54
|
+
subject: this.renderSubjectOutput(subjectOutput),
|
|
55
|
+
body: await this.renderBodyOutput(bodyOutput),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async compileSourceToCacheFile(source, sourceIdentifier, loader) {
|
|
59
|
+
const sourceHash = createHash('sha1').update(`${sourceIdentifier}:${source}`).digest('hex');
|
|
60
|
+
const cacheFileName = `${sourceHash}-${loader}.mjs`;
|
|
61
|
+
const compiledPath = join(COMPILED_TEMPLATE_CACHE_DIR, cacheFileName);
|
|
62
|
+
await mkdir(dirname(compiledPath), { recursive: true });
|
|
63
|
+
const { code } = await transform(source, {
|
|
64
|
+
loader,
|
|
65
|
+
format: 'esm',
|
|
66
|
+
target: 'es2021',
|
|
67
|
+
jsx: 'automatic',
|
|
68
|
+
sourcefile: sourceIdentifier,
|
|
69
|
+
sourcemap: 'inline',
|
|
70
|
+
});
|
|
71
|
+
await writeFile(compiledPath, code, 'utf8');
|
|
72
|
+
return compiledPath;
|
|
73
|
+
}
|
|
74
|
+
async loadTemplateFactoryFromContent(templateContent, templateKind) {
|
|
75
|
+
const moduleSource = this.buildContentModuleSource(templateContent);
|
|
76
|
+
const contentIdentifier = `inline-${templateKind}-template.tsx`;
|
|
77
|
+
const compiledPath = await this.compileSourceToCacheFile(moduleSource, contentIdentifier, 'tsx');
|
|
78
|
+
const templateModule = (await import(pathToFileURL(compiledPath).href));
|
|
79
|
+
const templateExport = templateModule.default ?? templateModule.render;
|
|
80
|
+
if (typeof templateExport !== 'function') {
|
|
81
|
+
throw new Error(`${templateKind} template content must export a function`);
|
|
82
|
+
}
|
|
83
|
+
return templateExport;
|
|
84
|
+
}
|
|
85
|
+
buildContentModuleSource(templateContent) {
|
|
86
|
+
const trimmedContent = templateContent.trim();
|
|
87
|
+
if (this.looksLikeTemplateModule(trimmedContent)) {
|
|
88
|
+
return trimmedContent;
|
|
89
|
+
}
|
|
90
|
+
const functionBody = this.looksLikeFunctionBody(trimmedContent)
|
|
91
|
+
? trimmedContent
|
|
92
|
+
: `return (${trimmedContent});`;
|
|
93
|
+
return [
|
|
94
|
+
'import React from "react";',
|
|
95
|
+
'export default function VintaSendTemplate(context) {',
|
|
96
|
+
functionBody,
|
|
97
|
+
'}',
|
|
98
|
+
].join('\n');
|
|
99
|
+
}
|
|
100
|
+
looksLikeTemplateModule(templateContent) {
|
|
101
|
+
return /\bexport\s+default\b|\bexport\s+(const|function)\s+render\b/.test(templateContent);
|
|
102
|
+
}
|
|
103
|
+
looksLikeFunctionBody(templateContent) {
|
|
104
|
+
return /(^|\n)\s*(return\b|if\b|for\b|while\b|switch\b|const\b|let\b|var\b|throw\b|try\b)/.test(templateContent);
|
|
105
|
+
}
|
|
106
|
+
async renderBodyOutput(output) {
|
|
107
|
+
if (typeof output === 'string') {
|
|
108
|
+
return output;
|
|
109
|
+
}
|
|
110
|
+
return renderReactEmail(output);
|
|
111
|
+
}
|
|
112
|
+
renderSubjectOutput(output) {
|
|
113
|
+
if (typeof output === 'string') {
|
|
114
|
+
return output;
|
|
115
|
+
}
|
|
116
|
+
return String(output ?? '');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export class ReactEmailInlineTemplateRendererFactory {
|
|
120
|
+
create(generatedTemplates) {
|
|
121
|
+
return new ReactEmailInlineTemplateRenderer(generatedTemplates);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Recursively finds all supported template files in a directory
|
|
4
|
+
*/
|
|
5
|
+
export declare function findTemplateFiles(dir: string, baseDir: string, files?: Map<string, string>): Map<string, string>;
|
|
6
|
+
/**
|
|
7
|
+
* Main function to compile react email templates to JSON
|
|
8
|
+
*/
|
|
9
|
+
export declare function compileReactEmailTemplates(inputDir?: string, outputFile?: string): void;
|
|
10
|
+
//# sourceMappingURL=compile-react-email-templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile-react-email-templates.d.ts","sourceRoot":"","sources":["../../src/scripts/compile-react-email-templates.ts"],"names":[],"mappings":";AAoBA;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAa,GACrC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA0BrB;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,GAAE,MAAsB,EAChC,UAAU,GAAE,MAA8C,GACzD,IAAI,CAyBN"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
const SUPPORTED_TEMPLATE_EXTENSIONS = new Set([
|
|
6
|
+
'.ts',
|
|
7
|
+
'.tsx',
|
|
8
|
+
'.js',
|
|
9
|
+
'.jsx',
|
|
10
|
+
'.mts',
|
|
11
|
+
'.cts',
|
|
12
|
+
'.mjs',
|
|
13
|
+
'.cjs',
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* Recursively finds all supported template files in a directory
|
|
17
|
+
*/
|
|
18
|
+
export function findTemplateFiles(dir, baseDir, files = new Map()) {
|
|
19
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const fullPath = path.join(dir, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
findTemplateFiles(fullPath, baseDir, files);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!entry.isFile()) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
30
|
+
if (!SUPPORTED_TEMPLATE_EXTENSIONS.has(extension)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
34
|
+
const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
35
|
+
files.set(relativePath, content);
|
|
36
|
+
}
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Main function to compile react email templates to JSON
|
|
41
|
+
*/
|
|
42
|
+
export function compileReactEmailTemplates(inputDir = './templates', outputFile = 'compiled-react-email-templates.json') {
|
|
43
|
+
if (!fs.existsSync(inputDir)) {
|
|
44
|
+
throw new Error(`Directory "${inputDir}" does not exist.`);
|
|
45
|
+
}
|
|
46
|
+
if (!fs.statSync(inputDir).isDirectory()) {
|
|
47
|
+
throw new Error(`"${inputDir}" is not a directory.`);
|
|
48
|
+
}
|
|
49
|
+
console.log(`Searching for template files in: ${inputDir}`);
|
|
50
|
+
const templateFiles = findTemplateFiles(inputDir, inputDir);
|
|
51
|
+
console.log(`Found ${templateFiles.size} template file(s)`);
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const [filePath, content] of templateFiles.entries()) {
|
|
54
|
+
result[filePath] = content;
|
|
55
|
+
console.log(` - ${filePath}`);
|
|
56
|
+
}
|
|
57
|
+
const outputContent = JSON.stringify(result, null, 2);
|
|
58
|
+
fs.writeFileSync(outputFile, outputContent, 'utf-8');
|
|
59
|
+
console.log(`\nSuccessfully compiled templates to: ${outputFile}`);
|
|
60
|
+
}
|
|
61
|
+
function runFromCli() {
|
|
62
|
+
const args = process.argv.slice(2);
|
|
63
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
64
|
+
console.log('Usage: compile-react-email-templates [input-directory] [output-file]');
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log('Arguments:');
|
|
67
|
+
console.log(' input-directory Directory containing React Email templates (default: ./templates)');
|
|
68
|
+
console.log(' output-file Output JSON file path (default: compiled-react-email-templates.json)');
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log('Supported extensions: .ts, .tsx, .js, .jsx, .mts, .cts, .mjs, .cjs');
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log('Examples:');
|
|
73
|
+
console.log(' compile-react-email-templates');
|
|
74
|
+
console.log(' compile-react-email-templates ./notification-templates');
|
|
75
|
+
console.log(' compile-react-email-templates ./notification-templates ./compiled-notification-templates.json');
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
const [inputDir, outputFile] = args;
|
|
79
|
+
try {
|
|
80
|
+
compileReactEmailTemplates(inputDir, outputFile);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
+
console.error(`Error: ${message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const isCliEntryPoint = (() => {
|
|
89
|
+
const entry = process.argv[1];
|
|
90
|
+
if (!entry) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return pathToFileURL(entry).href === import.meta.url;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
if (isCliEntryPoint) {
|
|
101
|
+
runFromCli();
|
|
102
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vintasend-react-email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "VintaSend template renderer implementation for React Email",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
"prepublishOnly": "npm run build",
|
|
9
9
|
"test": "vitest run",
|
|
10
10
|
"test:watch": "vitest",
|
|
11
|
-
"test:coverage": "vitest run --coverage"
|
|
11
|
+
"test:coverage": "vitest run --coverage",
|
|
12
|
+
"compile-templates": "compile-react-email-templates ./templates ./compiled-react-email-templates.json"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
|
-
"dist"
|
|
15
|
+
"dist",
|
|
16
|
+
"src/scripts/compile-react-email-templates.ts"
|
|
15
17
|
],
|
|
18
|
+
"bin": {
|
|
19
|
+
"compile-react-email-templates": "dist/scripts/compile-react-email-templates.js"
|
|
20
|
+
},
|
|
16
21
|
"author": "Hugo Bessa",
|
|
17
22
|
"license": "MIT",
|
|
18
23
|
"dependencies": {
|
|
@@ -20,9 +25,10 @@
|
|
|
20
25
|
"esbuild": "^0.25.11",
|
|
21
26
|
"react": "^19.2.0",
|
|
22
27
|
"react-dom": "^19.2.0",
|
|
23
|
-
"vintasend": "^0.
|
|
28
|
+
"vintasend": "^0.10.0"
|
|
24
29
|
},
|
|
25
30
|
"devDependencies": {
|
|
31
|
+
"@types/node": "^25.3.3",
|
|
26
32
|
"@vitest/coverage-v8": "4.0.18",
|
|
27
33
|
"typescript": "^5.9.3",
|
|
28
34
|
"vitest": "4.0.18"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
interface ReactEmailTemplatesMap {
|
|
7
|
+
[key: string]: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SUPPORTED_TEMPLATE_EXTENSIONS = new Set([
|
|
11
|
+
'.ts',
|
|
12
|
+
'.tsx',
|
|
13
|
+
'.js',
|
|
14
|
+
'.jsx',
|
|
15
|
+
'.mts',
|
|
16
|
+
'.cts',
|
|
17
|
+
'.mjs',
|
|
18
|
+
'.cjs',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Recursively finds all supported template files in a directory
|
|
23
|
+
*/
|
|
24
|
+
export function findTemplateFiles(
|
|
25
|
+
dir: string,
|
|
26
|
+
baseDir: string,
|
|
27
|
+
files: Map<string, string> = new Map(),
|
|
28
|
+
): Map<string, string> {
|
|
29
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const fullPath = path.join(dir, entry.name);
|
|
33
|
+
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
findTemplateFiles(fullPath, baseDir, files);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!entry.isFile()) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
44
|
+
if (!SUPPORTED_TEMPLATE_EXTENSIONS.has(extension)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
49
|
+
const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
50
|
+
files.set(relativePath, content);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return files;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Main function to compile react email templates to JSON
|
|
58
|
+
*/
|
|
59
|
+
export function compileReactEmailTemplates(
|
|
60
|
+
inputDir: string = './templates',
|
|
61
|
+
outputFile: string = 'compiled-react-email-templates.json',
|
|
62
|
+
): void {
|
|
63
|
+
if (!fs.existsSync(inputDir)) {
|
|
64
|
+
throw new Error(`Directory "${inputDir}" does not exist.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!fs.statSync(inputDir).isDirectory()) {
|
|
68
|
+
throw new Error(`"${inputDir}" is not a directory.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`Searching for template files in: ${inputDir}`);
|
|
72
|
+
|
|
73
|
+
const templateFiles = findTemplateFiles(inputDir, inputDir);
|
|
74
|
+
|
|
75
|
+
console.log(`Found ${templateFiles.size} template file(s)`);
|
|
76
|
+
|
|
77
|
+
const result: ReactEmailTemplatesMap = {};
|
|
78
|
+
for (const [filePath, content] of templateFiles.entries()) {
|
|
79
|
+
result[filePath] = content;
|
|
80
|
+
console.log(` - ${filePath}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const outputContent = JSON.stringify(result, null, 2);
|
|
84
|
+
fs.writeFileSync(outputFile, outputContent, 'utf-8');
|
|
85
|
+
|
|
86
|
+
console.log(`\nSuccessfully compiled templates to: ${outputFile}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function runFromCli(): void {
|
|
90
|
+
const args = process.argv.slice(2);
|
|
91
|
+
|
|
92
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
93
|
+
console.log('Usage: compile-react-email-templates [input-directory] [output-file]');
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log('Arguments:');
|
|
96
|
+
console.log(
|
|
97
|
+
' input-directory Directory containing React Email templates (default: ./templates)',
|
|
98
|
+
);
|
|
99
|
+
console.log(
|
|
100
|
+
' output-file Output JSON file path (default: compiled-react-email-templates.json)',
|
|
101
|
+
);
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log('Supported extensions: .ts, .tsx, .js, .jsx, .mts, .cts, .mjs, .cjs');
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log('Examples:');
|
|
106
|
+
console.log(' compile-react-email-templates');
|
|
107
|
+
console.log(' compile-react-email-templates ./notification-templates');
|
|
108
|
+
console.log(
|
|
109
|
+
' compile-react-email-templates ./notification-templates ./compiled-notification-templates.json',
|
|
110
|
+
);
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const [inputDir, outputFile] = args;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
compileReactEmailTemplates(inputDir, outputFile);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
120
|
+
console.error(`Error: ${message}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isCliEntryPoint = (() => {
|
|
126
|
+
const entry = process.argv[1];
|
|
127
|
+
if (!entry) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
return pathToFileURL(entry).href === import.meta.url;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
|
|
138
|
+
if (isCliEntryPoint) {
|
|
139
|
+
runFromCli();
|
|
140
|
+
}
|