@travetto/email-compiler 3.1.20
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/LICENSE +21 -0
- package/README.md +74 -0
- package/__index__.ts +2 -0
- package/package.json +54 -0
- package/src/compiler.ts +187 -0
- package/src/resource.ts +111 -0
- package/support/bin/config.ts +79 -0
- package/support/bin/editor.ts +108 -0
- package/support/bin/manager.ts +111 -0
- package/support/bin/send.ts +73 -0
- package/support/cli.email_compile.ts +38 -0
- package/support/cli.email_editor.ts +29 -0
- package/support/cli.email_test.ts +28 -0
- package/support/resources/email/main.scss +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 ArcSine Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!-- This file was generated by @travetto/doc and should not be modified directly -->
|
|
2
|
+
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/email-compiler/DOC.tsx and execute "npx trv doc" to rebuild -->
|
|
3
|
+
# Email Compilation Support
|
|
4
|
+
|
|
5
|
+
## Email compiling module
|
|
6
|
+
|
|
7
|
+
**Install: @travetto/email-compiler**
|
|
8
|
+
```bash
|
|
9
|
+
npm install @travetto/email-compiler
|
|
10
|
+
|
|
11
|
+
# or
|
|
12
|
+
|
|
13
|
+
yarn add @travetto/email-compiler
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This is primarily a set of command line tools for compiling and developing templates. The inputs are compiled files, generally under the `support/` folder, that represents the necessary input for the email compilation. [Email Inky Templates](https://github.com/travetto/travetto/tree/main/module/email-inky#readme "Email Inky templating module") shows this pattern by leveraging [JSX](https://en.wikipedia.org/wiki/JSX_(JavaScript)) bindings for the [inky](https://github.com/zurb/inky) framework, allowing for compile-time checked templates.
|
|
17
|
+
|
|
18
|
+
## Asset Management
|
|
19
|
+
The templating process involves loading various assets (html, css, images), and so there is provision for asset management and loading. The templating config allows for specifying asset paths, with the following paths (in order of precedence):
|
|
20
|
+
1. `%ROOT%/resources/email`
|
|
21
|
+
1. `@travetto/email-{engine}/resources/email`
|
|
22
|
+
When looking up a resources, every asset folder is consulted, in order, and the first to resolve an asset wins. This allows for overriding of default templating resources, as needed. The compilation process will convert `.email.html` files into `.compiled.html`, `.compiled.text` and `.compiled.subject` suffixes to generate the outputs respectively.
|
|
23
|
+
|
|
24
|
+
## Template Extension
|
|
25
|
+
The template extension points are defined at:
|
|
26
|
+
1. `email/main.scss` - The entry point for adding, and overriding any [sass](https://github.com/sass/dart-sass)
|
|
27
|
+
1. `email/{engine}.wrapper.html` - The html wrapper for the specific templating engine implementation.
|
|
28
|
+
|
|
29
|
+
## Template Compilation
|
|
30
|
+
The general process is as follows:
|
|
31
|
+
1. Load in the email template.
|
|
32
|
+
1. Resolve any associated stylings for said template.
|
|
33
|
+
1. Optimize styles against provided html, removing all unused stylings
|
|
34
|
+
1. Render template into html, text, and subject outputs.
|
|
35
|
+
1. Inline and optimize all images for html email transmission.
|
|
36
|
+
|
|
37
|
+
## Images
|
|
38
|
+
When referencing an image from the `resources` folder in a template, e.g.
|
|
39
|
+
|
|
40
|
+
**Code: Sample Image Reference**
|
|
41
|
+
```html
|
|
42
|
+
<img src="/email/logo.png">
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The image will be extracted out and embedded in the email as a multi part message. This allows for compression and optimization of images as well as externalizing resources that may not be immediately public. The currently supported set of image types are:
|
|
46
|
+
* jpeg
|
|
47
|
+
* png
|
|
48
|
+
|
|
49
|
+
## Template Development
|
|
50
|
+
The module provides [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework") and [VSCode plugin](https://marketplace.visualstudio.com/items?itemName=arcsine.travetto-plugin) support for email template development.
|
|
51
|
+
|
|
52
|
+
## CLI Compilation
|
|
53
|
+
The module provides [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework") support for email template compilation also. Running
|
|
54
|
+
|
|
55
|
+
**Terminal: Running template compilation**
|
|
56
|
+
```bash
|
|
57
|
+
$ trv email:compile -h
|
|
58
|
+
|
|
59
|
+
Usage: email:compile [options]
|
|
60
|
+
|
|
61
|
+
Options:
|
|
62
|
+
-w, --watch Compile in watch mode
|
|
63
|
+
-m, --module <string> Module to run for
|
|
64
|
+
-h, --help display help for command
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Will convert all `.email.html` files into the appropriate `.compiled.html`, `.compiled.text` and `.compiled.subject` files. These will be used during the running of the application. By default these files are added to the `.gitignore` as they are generally not intended to be saved but to be generated during the build process.
|
|
68
|
+
|
|
69
|
+
## VSCode Plugin
|
|
70
|
+
In addition to command line tools, the [VSCode plugin](https://marketplace.visualstudio.com/items?itemName=arcsine.travetto-plugin) also supports:
|
|
71
|
+
* automatic compilation on change
|
|
72
|
+
* real-time rendering of changes visually
|
|
73
|
+
* ability to send test emails during development
|
|
74
|
+
* ability to define custom context for template rendering
|
package/__index__.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@travetto/email-compiler",
|
|
3
|
+
"version": "3.1.20",
|
|
4
|
+
"description": "Email compiling module",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"email",
|
|
7
|
+
"email compiler",
|
|
8
|
+
"travetto",
|
|
9
|
+
"typescript"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://travetto.io",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": {
|
|
14
|
+
"email": "travetto.framework@gmail.com",
|
|
15
|
+
"name": "Travetto Framework"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"__index__.ts",
|
|
19
|
+
"src",
|
|
20
|
+
"!resources",
|
|
21
|
+
"support"
|
|
22
|
+
],
|
|
23
|
+
"main": "__index__.ts",
|
|
24
|
+
"repository": {
|
|
25
|
+
"url": "https://github.com/travetto/travetto.git",
|
|
26
|
+
"directory": "module/email-compiler"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@travetto/base": "^3.1.2",
|
|
30
|
+
"@travetto/config": "^3.1.6",
|
|
31
|
+
"@travetto/di": "^3.1.3",
|
|
32
|
+
"@travetto/email": "^3.1.12",
|
|
33
|
+
"@travetto/image": "^3.1.3",
|
|
34
|
+
"@types/inline-css": "^3.0.1",
|
|
35
|
+
"html-entities": "^2.3.3",
|
|
36
|
+
"inline-css": "^4.0.2",
|
|
37
|
+
"sass": "^1.62.1",
|
|
38
|
+
"purgecss": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@travetto/cli": "^3.1.7"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@travetto/cli": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"travetto": {
|
|
49
|
+
"displayName": "Email Compilation Support"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/compiler.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import util from 'util';
|
|
3
|
+
import { decode as decodeEntities } from 'html-entities';
|
|
4
|
+
|
|
5
|
+
import { ImageConverter } from '@travetto/image';
|
|
6
|
+
import { RootIndex, path } from '@travetto/manifest';
|
|
7
|
+
import { StreamUtil } from '@travetto/base';
|
|
8
|
+
import { MessageCompilationSource, MessageCompiled } from '@travetto/email';
|
|
9
|
+
|
|
10
|
+
import { EmailCompilerResource } from './resource';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Utilities for templating
|
|
14
|
+
*/
|
|
15
|
+
export class EmailCompiler {
|
|
16
|
+
|
|
17
|
+
static HTML_CSS_IMAGE_URLS = [
|
|
18
|
+
/(?<pre><img[^>]src=\s*["'])(?<src>[^"]+)/g,
|
|
19
|
+
/(?<pre>background(?:-image)?:\s*url[(]['"]?)(?<src>[^"')]+)/g
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
static async readText(text: Promise<string> | string): Promise<string> {
|
|
23
|
+
return decodeEntities((await text).replace(/ /g, ' '));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compile SCSS content with roots as search paths for additional assets
|
|
28
|
+
*/
|
|
29
|
+
static async compileSass(src: { data: string } | { file: string }, roots: string[]): Promise<string> {
|
|
30
|
+
const sass = await import('sass');
|
|
31
|
+
const result = await util.promisify(sass.render)({
|
|
32
|
+
...src,
|
|
33
|
+
sourceMap: false,
|
|
34
|
+
includePaths: roots
|
|
35
|
+
});
|
|
36
|
+
return result!.css.toString();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
resources: EmailCompilerResource;
|
|
40
|
+
|
|
41
|
+
constructor(resources: EmailCompilerResource) {
|
|
42
|
+
this.resources = resources;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Inline image sources
|
|
47
|
+
*/
|
|
48
|
+
async inlineImageSource(html: string): Promise<string> {
|
|
49
|
+
const { tokens, finalize } = await this.resources.tokenizeResources(html, EmailCompiler.HTML_CSS_IMAGE_URLS);
|
|
50
|
+
const pendingImages: Promise<[token: string, content: string]>[] = [];
|
|
51
|
+
|
|
52
|
+
for (const [token, src] of tokens) {
|
|
53
|
+
const ext = path.extname(src).replace(/^[.]/, '');
|
|
54
|
+
const stream = await this.resources.readStream(src);
|
|
55
|
+
|
|
56
|
+
switch (ext) {
|
|
57
|
+
case 'jpg':
|
|
58
|
+
case 'jpeg':
|
|
59
|
+
case 'png': {
|
|
60
|
+
pendingImages.push(
|
|
61
|
+
ImageConverter.optimize(ext === 'png' ? 'png' : 'jpeg', stream)
|
|
62
|
+
.then(img => StreamUtil.streamToBuffer(img))
|
|
63
|
+
.then(data => [token, `data:image/${ext};base64,${data.toString('base64')}`])
|
|
64
|
+
);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
default: {
|
|
68
|
+
pendingImages.push(
|
|
69
|
+
StreamUtil.streamToBuffer(stream)
|
|
70
|
+
.then(data => [token, `data:image/${ext};base64,${data.toString('base64')}`])
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const imageMap = new Map(await Promise.all(pendingImages));
|
|
77
|
+
|
|
78
|
+
return finalize(token => imageMap.get(token)!);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async pruneCss(html: string, css: string): Promise<string> {
|
|
82
|
+
const { PurgeCSS } = await import('purgecss');
|
|
83
|
+
const purge = new PurgeCSS();
|
|
84
|
+
const result = await purge.purge({
|
|
85
|
+
content: [{ raw: html, extension: 'html' }],
|
|
86
|
+
css: [{ raw: css }],
|
|
87
|
+
});
|
|
88
|
+
return result[0].css;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async inlineCss(html: string, css: string): Promise<string> {
|
|
92
|
+
// Inline css
|
|
93
|
+
const { default: inlineCss } = await import('inline-css');
|
|
94
|
+
html = html.replace('</head>', `<style>${css}</style></head>`);
|
|
95
|
+
// Style needs to be in head to preserve media queries
|
|
96
|
+
html = (await inlineCss(html, {
|
|
97
|
+
url: 'https://app.dev',
|
|
98
|
+
preserveMediaQueries: true,
|
|
99
|
+
removeStyleTags: true,
|
|
100
|
+
removeLinkTags: true,
|
|
101
|
+
applyStyleTags: true,
|
|
102
|
+
}));
|
|
103
|
+
return html;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Compile all
|
|
108
|
+
*/
|
|
109
|
+
async compileAll(persist = false): Promise<MessageCompiled[]> {
|
|
110
|
+
const keys = await this.resources.findAllTemplates();
|
|
111
|
+
return Promise.all(keys.map(src => this.compile(src, persist)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compile template
|
|
116
|
+
*/
|
|
117
|
+
async compile(src: string | MessageCompilationSource, persist = false): Promise<MessageCompiled> {
|
|
118
|
+
if (typeof src === 'string') {
|
|
119
|
+
src = await this.resources.loadTemplate(src);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const subject = await EmailCompiler.readText(src.subject());
|
|
123
|
+
const text = await EmailCompiler.readText(src.text());
|
|
124
|
+
|
|
125
|
+
let html = (await src.html())
|
|
126
|
+
.replace(/<(meta|img|link|hr|br)[^>]*>/g, a => a.replace('>', '/>')) // Fix self closing
|
|
127
|
+
.replace(/'/g, '''); // Fix apostrophes, as outlook hates them
|
|
128
|
+
|
|
129
|
+
if (src.inlineStyles !== false) {
|
|
130
|
+
const styles: string[] = [];
|
|
131
|
+
|
|
132
|
+
if (src.styles?.global) {
|
|
133
|
+
styles.push(src.styles.global);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const main = await this.resources.read('/email/main.scss').then(d => d, () => '');
|
|
137
|
+
if (main) {
|
|
138
|
+
styles.push(main);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (styles.length) {
|
|
142
|
+
const compiled = await EmailCompiler.compileSass(
|
|
143
|
+
{ data: styles.join('\n') },
|
|
144
|
+
[...src.styles?.search ?? [], ...this.resources.getAllPaths()]);
|
|
145
|
+
|
|
146
|
+
// Remove all unused styles
|
|
147
|
+
const finalStyles = await this.pruneCss(html, compiled);
|
|
148
|
+
|
|
149
|
+
// Apply styles
|
|
150
|
+
html = await this.inlineCss(html, finalStyles);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Fix up style behaviors
|
|
155
|
+
html = html
|
|
156
|
+
.replace(/(background(?:-color)?:\s*)([#0-9a-f]{6,8})([^>.#,]+)>/ig,
|
|
157
|
+
(all, p, col, rest) => `${p}${col}${rest} bgcolor="${col}">`) // Inline bg-color
|
|
158
|
+
.replace(/<([^>]+vertical-align:\s*(top|bottom|middle)[^>]+)>/g,
|
|
159
|
+
(a, tag, valign) => tag.indexOf('valign') ? `<${tag}>` : `<${tag} valign="${valign}">`) // Vertically align if it has the style
|
|
160
|
+
.replace(/<(table[^>]+expand[^>]+width:\s*)(100%\s+!important)([^>]+)>/g,
|
|
161
|
+
(a, left, size, right) => `<${left}100%${right}>`); // Drop important as a fix for outlook
|
|
162
|
+
|
|
163
|
+
if (src.inlineImages !== false) {
|
|
164
|
+
// Inline Images
|
|
165
|
+
html = await this.inlineImageSource(html);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Write to disk, if desired
|
|
169
|
+
if (persist) {
|
|
170
|
+
const outs = this.resources.getOutputs(src.file!, path.join(RootIndex.mainModule.sourcePath, 'resources'));
|
|
171
|
+
await Promise.all([
|
|
172
|
+
[outs.text, text],
|
|
173
|
+
[outs.html, html],
|
|
174
|
+
[outs.subject, subject]
|
|
175
|
+
].map(async ([file, content]) => {
|
|
176
|
+
if (content) {
|
|
177
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
178
|
+
await fs.writeFile(file, content, { encoding: 'utf8' });
|
|
179
|
+
} else {
|
|
180
|
+
await fs.unlink(file).catch(() => { }); // Remove file if data not provided
|
|
181
|
+
}
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { html, text, subject };
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/resource.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { FileQueryProvider } from '@travetto/base';
|
|
2
|
+
import { MessageCompilationSource, MessageCompiled } from '@travetto/email';
|
|
3
|
+
import { RootIndex, path } from '@travetto/manifest';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resource management for email templating
|
|
7
|
+
*/
|
|
8
|
+
export class EmailCompilerResource extends FileQueryProvider {
|
|
9
|
+
static PATH_PREFIX = /.*\/resources\//;
|
|
10
|
+
static EXT = /[.]email[.][jt]sx$/;
|
|
11
|
+
|
|
12
|
+
get ext(): RegExp {
|
|
13
|
+
return EmailCompilerResource.EXT;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
constructor(paths: string[] = ['@travetto/email-compiler#support/resources']) {
|
|
17
|
+
super({ paths, includeCommon: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
buildOutputPath(file: string, suffix: string, prefix?: string): string {
|
|
21
|
+
let res = file.replace(/.*(support|src)\//, '').replace(this.ext, suffix);
|
|
22
|
+
if (prefix) {
|
|
23
|
+
res = path.join(prefix, res);
|
|
24
|
+
}
|
|
25
|
+
return res;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the different parts from the file name
|
|
30
|
+
* @returns
|
|
31
|
+
*/
|
|
32
|
+
getOutputs(file: string, prefix?: string): MessageCompiled {
|
|
33
|
+
return {
|
|
34
|
+
html: this.buildOutputPath(file, '.compiled.html', prefix),
|
|
35
|
+
subject: this.buildOutputPath(file, '.compiled.subject', prefix),
|
|
36
|
+
text: this.buildOutputPath(file, '.compiled.text', prefix),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Is this a valid template file?
|
|
42
|
+
*/
|
|
43
|
+
isTemplateFile(file: string): boolean {
|
|
44
|
+
return this.ext.test(file);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the sending email key from a template file
|
|
49
|
+
* @param file
|
|
50
|
+
*/
|
|
51
|
+
async templateFileToKey(file: string): Promise<string> {
|
|
52
|
+
return this.buildOutputPath(file, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async loadTemplate(imp: string): Promise<MessageCompilationSource> {
|
|
56
|
+
const entry = RootIndex.getEntry(imp) ?? RootIndex.getFromImport(imp);
|
|
57
|
+
if (!entry) {
|
|
58
|
+
throw new Error();
|
|
59
|
+
}
|
|
60
|
+
const root = (await import(entry.outputFile)).default;
|
|
61
|
+
return { ...await root.wrap(), file: entry.sourceFile };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Grab list of all available templates
|
|
66
|
+
*/
|
|
67
|
+
async findAllTemplates(): Promise<MessageCompilationSource[]> {
|
|
68
|
+
const items = RootIndex.findSupport({
|
|
69
|
+
filter: (f) => this.ext.test(f)
|
|
70
|
+
});
|
|
71
|
+
const out: Promise<MessageCompilationSource>[] = [];
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
out.push(this.loadTemplate(item.import));
|
|
74
|
+
}
|
|
75
|
+
return Promise.all(out);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Run through text and match/resolve resource urls, producing tokens
|
|
81
|
+
*
|
|
82
|
+
* @param text
|
|
83
|
+
* @param patterns
|
|
84
|
+
* @returns
|
|
85
|
+
*/
|
|
86
|
+
async tokenizeResources(
|
|
87
|
+
text: string,
|
|
88
|
+
patterns: RegExp[]
|
|
89
|
+
): Promise<{
|
|
90
|
+
text: string;
|
|
91
|
+
tokens: Map<string, string>;
|
|
92
|
+
finalize: (onToken: (token: string) => string) => string;
|
|
93
|
+
}> {
|
|
94
|
+
let id = 0;
|
|
95
|
+
const tokens = new Map();
|
|
96
|
+
for (const pattern of patterns) {
|
|
97
|
+
for (const { [0]: all, groups: { pre, src } = { pre: '', src: '' } } of text.matchAll(pattern)) {
|
|
98
|
+
if (src.includes('://')) { // No urls
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
await this.describe(src);
|
|
102
|
+
const token = `@@${id += 1}@@`;
|
|
103
|
+
tokens.set(token, src);
|
|
104
|
+
text = text.replace(all, `${pre}${token}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const finalize = (onToken: (token: string) => string): string => text.replace(/@@[^@]+@@/g, t => onToken(t));
|
|
108
|
+
|
|
109
|
+
return { text, tokens, finalize };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
import { path } from '@travetto/manifest';
|
|
4
|
+
import { YamlUtil } from '@travetto/yaml';
|
|
5
|
+
|
|
6
|
+
interface ConfigType {
|
|
7
|
+
to: string;
|
|
8
|
+
from: string;
|
|
9
|
+
context?: Record<string, unknown>;
|
|
10
|
+
sender?: {
|
|
11
|
+
port?: number;
|
|
12
|
+
host?: string;
|
|
13
|
+
auth?: {
|
|
14
|
+
user?: string;
|
|
15
|
+
pass?: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configuration utils
|
|
22
|
+
*/
|
|
23
|
+
export class $EditorConfig {
|
|
24
|
+
|
|
25
|
+
#configFile = path.resolve('resources/email/dev.yml');
|
|
26
|
+
#defaultConfig = {
|
|
27
|
+
to: 'my-email@gmail.com',
|
|
28
|
+
from: 'from-email@gmail.com',
|
|
29
|
+
context: {
|
|
30
|
+
key: 'value'
|
|
31
|
+
},
|
|
32
|
+
sender: {
|
|
33
|
+
port: 587,
|
|
34
|
+
host: 'smtp.ethereal.email',
|
|
35
|
+
auth: {
|
|
36
|
+
user: 'email@blah.com',
|
|
37
|
+
pass: 'password'
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
*
|
|
44
|
+
*/
|
|
45
|
+
async get(): Promise<ConfigType> {
|
|
46
|
+
try {
|
|
47
|
+
const content = await fs.readFile(this.#configFile, 'utf8');
|
|
48
|
+
return YamlUtil.parse<ConfigType>(content);
|
|
49
|
+
} catch {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
51
|
+
return {} as ConfigType;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getContext(): Promise<Exclude<ConfigType['context'], undefined>> {
|
|
56
|
+
const conf = await this.get();
|
|
57
|
+
return conf.context ?? {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getSenderConfig(): Promise<Exclude<ConfigType['sender'], undefined>> {
|
|
61
|
+
const conf = await this.get();
|
|
62
|
+
return conf.sender ?? {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getDefaultConfig(): string {
|
|
66
|
+
return YamlUtil.serialize(this.#defaultConfig);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async ensureConfig(): Promise<string> {
|
|
70
|
+
const file = this.#configFile;
|
|
71
|
+
if (!(await fs.stat(file).catch(() => { }))) {
|
|
72
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
73
|
+
await fs.writeFile(file, this.getDefaultConfig(), { encoding: 'utf8' });
|
|
74
|
+
}
|
|
75
|
+
return file;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const EditorConfig = new $EditorConfig();
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { EmailCompilationManager } from './manager';
|
|
2
|
+
import { EditorSendService } from './send';
|
|
3
|
+
import { EditorConfig } from './config';
|
|
4
|
+
|
|
5
|
+
type InboundMessage =
|
|
6
|
+
{ type: 'configure' } |
|
|
7
|
+
{ type: 'redraw', file: string } |
|
|
8
|
+
{ type: 'send', file: string, from?: string, to?: string };
|
|
9
|
+
|
|
10
|
+
type OutboundMessage =
|
|
11
|
+
{ type: 'configured', file: string } |
|
|
12
|
+
{ type: 'sent', to: string, file: string, url?: string | false } |
|
|
13
|
+
{ type: 'changed', file: string, content: Record<'html' | 'subject' | 'text', string> } |
|
|
14
|
+
{ type: 'sent-failed', message: string, stack: Error['stack'], to: string, file: string } |
|
|
15
|
+
{ type: 'changed-failed', message: string, stack: Error['stack'], file: string };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Utils for interacting with editors
|
|
19
|
+
*/
|
|
20
|
+
export class EditorState {
|
|
21
|
+
|
|
22
|
+
#lastFile = '';
|
|
23
|
+
#sender: EditorSendService;
|
|
24
|
+
#template: EmailCompilationManager;
|
|
25
|
+
|
|
26
|
+
constructor(template: EmailCompilationManager) {
|
|
27
|
+
this.#template = template;
|
|
28
|
+
this.#sender = new EditorSendService();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async renderFile(file: string): Promise<void> {
|
|
32
|
+
file = this.#template.resources.isTemplateFile(file) ? file : this.#lastFile;
|
|
33
|
+
if (file) {
|
|
34
|
+
try {
|
|
35
|
+
const content = await this.#template.resolveCompiledTemplate(
|
|
36
|
+
file, await EditorConfig.getContext()
|
|
37
|
+
);
|
|
38
|
+
this.response({
|
|
39
|
+
type: 'changed',
|
|
40
|
+
file,
|
|
41
|
+
content
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err && err instanceof Error) {
|
|
45
|
+
this.response({ type: 'changed-failed', message: err.message, stack: err.stack, file });
|
|
46
|
+
} else {
|
|
47
|
+
console.error(err);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
response(response: OutboundMessage): void {
|
|
54
|
+
if (process.send) {
|
|
55
|
+
process.send(response);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async onConfigure(msg: InboundMessage & { type: 'configure' }): Promise<void> {
|
|
60
|
+
this.response({ type: 'configured', file: await EditorConfig.ensureConfig() });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async #onRedraw(msg: InboundMessage & { type: 'redraw' }): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
await this.#template.compiler.compile(msg.file, true);
|
|
66
|
+
await this.renderFile(msg.file);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err && err instanceof Error) {
|
|
69
|
+
this.response({ type: 'changed-failed', message: err.message, stack: err.stack, file: msg.file });
|
|
70
|
+
} else {
|
|
71
|
+
console.error(err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async onSend(msg: InboundMessage & { type: 'send' }): Promise<void> {
|
|
77
|
+
const cfg = await EditorConfig.get();
|
|
78
|
+
const to = msg.to || cfg.to;
|
|
79
|
+
const from = msg.from || cfg.from;
|
|
80
|
+
const key = this.#template.resources.buildOutputPath(msg.file, '');
|
|
81
|
+
try {
|
|
82
|
+
const url = await this.#sender.sendEmail(key, from, to, await EditorConfig.getContext());
|
|
83
|
+
this.response({ type: 'sent', to, file: msg.file, ...url });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err && err instanceof Error) {
|
|
86
|
+
this.response({ type: 'sent-failed', message: err.message, stack: err.stack, to, file: msg.file });
|
|
87
|
+
} else {
|
|
88
|
+
console.error(err);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Initialize context, and listeners
|
|
95
|
+
*/
|
|
96
|
+
async init(): Promise<void> {
|
|
97
|
+
process.on('message', (msg: InboundMessage) => {
|
|
98
|
+
switch (msg.type) {
|
|
99
|
+
case 'configure': this.onConfigure(msg); break;
|
|
100
|
+
case 'redraw': this.#onRedraw(msg); break;
|
|
101
|
+
case 'send': this.onSend(msg); break;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
for await (const f of this.#template.watchCompile()) {
|
|
105
|
+
await this.renderFile(f);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { MailTemplateEngine, MessageCompiled } from '@travetto/email';
|
|
2
|
+
import { TypedObject } from '@travetto/base';
|
|
3
|
+
import { DependencyRegistry } from '@travetto/di';
|
|
4
|
+
import { RootIndex, WatchEvent, WatchStream } from '@travetto/manifest';
|
|
5
|
+
|
|
6
|
+
import { MailTemplateEngineTarget } from '@travetto/email/src/internal/types';
|
|
7
|
+
import { DynamicFileLoader } from '@travetto/base/src/internal/file-loader';
|
|
8
|
+
|
|
9
|
+
import type { EmailCompiler } from '../../src/compiler';
|
|
10
|
+
import type { EmailCompilerResource } from '../../src/resource';
|
|
11
|
+
|
|
12
|
+
const VALID_FILE = (file: string): boolean => /[.](scss|css|png|jpe?g|gif|ya?ml)$/.test(file) && !/[.]compiled[.]/.test(file);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
export class EmailCompilationManager {
|
|
18
|
+
|
|
19
|
+
static async createInstance(): Promise<EmailCompilationManager> {
|
|
20
|
+
const { EmailCompiler: Compiler } = await import('../../src/compiler.js');
|
|
21
|
+
const { EmailCompilerResource: Res } = await import('../../src/resource.js');
|
|
22
|
+
|
|
23
|
+
return new EmailCompilationManager(
|
|
24
|
+
await DependencyRegistry.getInstance<MailTemplateEngine>(MailTemplateEngineTarget),
|
|
25
|
+
new Compiler(new Res())
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
compiler: EmailCompiler;
|
|
30
|
+
engine: MailTemplateEngine;
|
|
31
|
+
|
|
32
|
+
constructor(engine: MailTemplateEngine, compiler: EmailCompiler) {
|
|
33
|
+
this.engine = engine;
|
|
34
|
+
this.compiler = compiler;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get resources(): EmailCompilerResource {
|
|
38
|
+
return this.compiler.resources;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve template
|
|
43
|
+
*/
|
|
44
|
+
async resolveTemplateParts(file: string): Promise<MessageCompiled> {
|
|
45
|
+
const files = this.resources.getOutputs(file);
|
|
46
|
+
const missing = await Promise.all(Object.values(files).map(x => this.resources.describe(x).catch(() => { })));
|
|
47
|
+
|
|
48
|
+
if (missing.some(x => x === undefined)) {
|
|
49
|
+
await this.compiler.compile(file, true);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parts = await Promise.all(
|
|
53
|
+
TypedObject.entries(files).map(
|
|
54
|
+
([key, subRel]) => this.resources.read(subRel)
|
|
55
|
+
.then(content => [key, content] as const)
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
return TypedObject.fromEntries<keyof MessageCompiled, string>(parts);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Render
|
|
63
|
+
* @param rel
|
|
64
|
+
*/
|
|
65
|
+
async resolveCompiledTemplate(rel: string, context: Record<string, unknown>): Promise<MessageCompiled> {
|
|
66
|
+
const { html, text, subject } = await this.resolveTemplateParts(rel);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
html: await this.engine.template(html, context),
|
|
70
|
+
text: await this.engine.template(text, context),
|
|
71
|
+
subject: await this.engine.template(subject, context),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Watch compilation
|
|
77
|
+
*/
|
|
78
|
+
async * watchCompile(): AsyncIterable<string> {
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
80
|
+
const stream = this.resources.watchFiles() as (
|
|
81
|
+
WatchStream & {
|
|
82
|
+
add(item: WatchEvent | WatchEvent[]): void;
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
DynamicFileLoader.onLoadEvent((ev) => {
|
|
86
|
+
const src = RootIndex.getEntry(ev.file);
|
|
87
|
+
if (src && this.resources.isTemplateFile(src.sourceFile)) {
|
|
88
|
+
stream.add({ ...ev, file: src.sourceFile });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
for await (const { file, action } of stream) {
|
|
92
|
+
if (action === 'delete') {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
if (this.resources.isTemplateFile(file)) {
|
|
98
|
+
await this.compiler.compile(file, true);
|
|
99
|
+
yield file;
|
|
100
|
+
} else if (VALID_FILE(file)) {
|
|
101
|
+
await this.compiler.compileAll(true);
|
|
102
|
+
for (const el of await this.resources.findAllTemplates()) {
|
|
103
|
+
yield el.file!;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(`Error in compiling ${file}`, err && err instanceof Error ? err.message : `${err}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { MailService, SentMessage } from '@travetto/email';
|
|
2
|
+
import { MailTransportTarget } from '@travetto/email/src/internal/types';
|
|
3
|
+
import { DependencyRegistry } from '@travetto/di';
|
|
4
|
+
|
|
5
|
+
import { EditorConfig } from './config';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Util for sending emails
|
|
9
|
+
*/
|
|
10
|
+
export class EditorSendService {
|
|
11
|
+
|
|
12
|
+
#svc: MailService;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get mail service
|
|
16
|
+
*/
|
|
17
|
+
async getMailService(): Promise<MailService> {
|
|
18
|
+
if (!this.#svc) {
|
|
19
|
+
const senderConfig = await EditorConfig.getSenderConfig();
|
|
20
|
+
|
|
21
|
+
if (senderConfig?.host?.includes('ethereal.email')) {
|
|
22
|
+
const cls = class { };
|
|
23
|
+
const { NodemailerTransport } = await import('@travetto/email-nodemailer');
|
|
24
|
+
DependencyRegistry.registerFactory({
|
|
25
|
+
fn: () => new NodemailerTransport(senderConfig),
|
|
26
|
+
target: MailTransportTarget,
|
|
27
|
+
src: cls,
|
|
28
|
+
id: 'nodemailer',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
DependencyRegistry.install(cls, { curr: cls, type: 'added' });
|
|
32
|
+
} else if (!DependencyRegistry.getCandidateTypes(MailTransportTarget).length) {
|
|
33
|
+
const errorMessage = `
|
|
34
|
+
Please configure your email setup and/or credentials for testing. In the file \`email/dev.yml\`, you can specify \`sender\` configuration.
|
|
35
|
+
Email sending will not work until the above is fixed. A sample configuration would look like:
|
|
36
|
+
|
|
37
|
+
${EditorConfig.getDefaultConfig()}`.trim();
|
|
38
|
+
console.error(errorMessage);
|
|
39
|
+
throw new Error(errorMessage);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.#svc = await DependencyRegistry.getInstance(MailService);
|
|
43
|
+
}
|
|
44
|
+
return this.#svc;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve template
|
|
49
|
+
*/
|
|
50
|
+
async sendEmail(key: string, from: string, to: string, context: Record<string, unknown>): Promise<{
|
|
51
|
+
url?: string | false;
|
|
52
|
+
}> {
|
|
53
|
+
try {
|
|
54
|
+
console.log('Sending email', { to });
|
|
55
|
+
// Let the engine template
|
|
56
|
+
const svc = await this.getMailService();
|
|
57
|
+
if (!svc) {
|
|
58
|
+
throw new Error('Node mailer support is missing');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const info = await svc.send<{ host?: string } & SentMessage>({ to, from }, key, context);
|
|
62
|
+
console.log('Sent email', { to });
|
|
63
|
+
|
|
64
|
+
const senderConfig = await EditorConfig.getSenderConfig();
|
|
65
|
+
return senderConfig.host?.includes('ethereal.email') ? {
|
|
66
|
+
url: (await import('nodemailer')).getTestMessageUrl(info as any)
|
|
67
|
+
} : {};
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn('Failed to send email', { to, error: err });
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { RootRegistry } from '@travetto/registry';
|
|
2
|
+
import { CliCommandShape, CliCommand, cliTpl } from '@travetto/cli';
|
|
3
|
+
|
|
4
|
+
import { EmailCompilationManager } from './bin/manager';
|
|
5
|
+
import { GlobalEnvConfig } from '@travetto/base';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CLI Entry point for running the email server
|
|
9
|
+
*/
|
|
10
|
+
@CliCommand({ fields: ['module'] })
|
|
11
|
+
export class EmailCompileCommand implements CliCommandShape {
|
|
12
|
+
|
|
13
|
+
/** Compile in watch mode */
|
|
14
|
+
watch?: boolean;
|
|
15
|
+
|
|
16
|
+
envInit(): GlobalEnvConfig {
|
|
17
|
+
return {
|
|
18
|
+
debug: false,
|
|
19
|
+
dynamic: this.watch
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async main(): Promise<void> {
|
|
24
|
+
await RootRegistry.init();
|
|
25
|
+
|
|
26
|
+
const template = await EmailCompilationManager.createInstance();
|
|
27
|
+
|
|
28
|
+
// Let the engine template
|
|
29
|
+
const all = await template.compiler.compileAll(true);
|
|
30
|
+
console!.log(cliTpl`Successfully compiled ${{ param: `${all.length}` }} templates`);
|
|
31
|
+
|
|
32
|
+
if (this.watch) {
|
|
33
|
+
for await (const _ of template.watchCompile()) {
|
|
34
|
+
// Iterate until done
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { GlobalEnvConfig, ShutdownManager } from '@travetto/base';
|
|
2
|
+
import { CliCommand } from '@travetto/cli';
|
|
3
|
+
import { RootIndex } from '@travetto/manifest';
|
|
4
|
+
import { RootRegistry } from '@travetto/registry';
|
|
5
|
+
|
|
6
|
+
import { EditorState } from './bin/editor';
|
|
7
|
+
import { EmailCompilationManager } from './bin/manager';
|
|
8
|
+
|
|
9
|
+
/** The email editor compilation service and output serving */
|
|
10
|
+
@CliCommand()
|
|
11
|
+
export class EmailEditorCommand {
|
|
12
|
+
|
|
13
|
+
envInit(): GlobalEnvConfig {
|
|
14
|
+
return {
|
|
15
|
+
envName: 'dev',
|
|
16
|
+
resourcePaths: [`${RootIndex.getModule('@travetto/email-compiler')!.sourcePath}/resources`]
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async main(): Promise<void> {
|
|
21
|
+
await RootRegistry.init();
|
|
22
|
+
const editor = new EditorState(await EmailCompilationManager.createInstance());
|
|
23
|
+
await editor.init();
|
|
24
|
+
if (process.send) {
|
|
25
|
+
process.on('disconnect', () => ShutdownManager.execute());
|
|
26
|
+
process.send('ready');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { path } from '@travetto/manifest';
|
|
2
|
+
import { RootRegistry } from '@travetto/registry';
|
|
3
|
+
import { CliCommandShape, CliCommand } from '@travetto/cli';
|
|
4
|
+
import { GlobalEnvConfig } from '@travetto/base';
|
|
5
|
+
|
|
6
|
+
import { EmailCompilationManager } from './bin/manager';
|
|
7
|
+
import { EditorState } from './bin/editor';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CLI Entry point for running the email server
|
|
11
|
+
*/
|
|
12
|
+
@CliCommand()
|
|
13
|
+
export class EmailTestCommand implements CliCommandShape {
|
|
14
|
+
|
|
15
|
+
envInit(): GlobalEnvConfig {
|
|
16
|
+
return { envName: 'dev' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async main(file: string, to: string): Promise<void> {
|
|
20
|
+
file = path.resolve(file);
|
|
21
|
+
await RootRegistry.init();
|
|
22
|
+
const template = await EmailCompilationManager.createInstance();
|
|
23
|
+
await template.compiler.compile(file, true);
|
|
24
|
+
const editor = new EditorState(await EmailCompilationManager.createInstance());
|
|
25
|
+
await editor.onConfigure({ type: 'configure' });
|
|
26
|
+
await editor.onSend({ type: 'send', file, to });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
File without changes
|