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 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
@@ -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
@@ -1 +1,2 @@
1
1
  export { ReactEmailTemplateRenderer, ReactEmailTemplateRendererFactory } from './react-email-template-renderer';
2
+ export { ReactEmailInlineTemplateRenderer, ReactEmailInlineTemplateRendererFactory, } from './react-email-inline-template-renderer';
@@ -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.9.1",
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.9.1"
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
+ }