@travetto/email-compiler 3.1.20 → 3.1.22
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/__index__.ts +2 -2
- package/package.json +7 -7
- package/src/compiler.ts +105 -147
- package/src/util.ts +221 -0
- package/support/bin/config.ts +17 -14
- package/support/bin/editor.ts +21 -11
- package/support/bin/manager.ts +9 -59
- package/support/bin/send.ts +16 -11
- package/support/cli.email_compile.ts +9 -7
- package/support/cli.email_editor.ts +3 -12
- package/support/cli.email_test.ts +11 -6
- package/src/resource.ts +0 -111
- package/support/resources/email/main.scss +0 -0
package/__index__.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './src/
|
|
2
|
-
export * from './src/
|
|
1
|
+
export * from './src/compiler';
|
|
2
|
+
export * from './src/util';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/email-compiler",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.22",
|
|
4
4
|
"description": "Email compiling module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"directory": "module/email-compiler"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/base": "^3.1.
|
|
30
|
-
"@travetto/config": "^3.1.
|
|
31
|
-
"@travetto/di": "^3.1.
|
|
32
|
-
"@travetto/email": "^3.1.
|
|
33
|
-
"@travetto/image": "^3.1.
|
|
29
|
+
"@travetto/base": "^3.1.3",
|
|
30
|
+
"@travetto/config": "^3.1.7",
|
|
31
|
+
"@travetto/di": "^3.1.4",
|
|
32
|
+
"@travetto/email": "^3.1.13",
|
|
33
|
+
"@travetto/image": "^3.1.4",
|
|
34
34
|
"@types/inline-css": "^3.0.1",
|
|
35
35
|
"html-entities": "^2.3.3",
|
|
36
36
|
"inline-css": "^4.0.2",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"purgecss": "^5.0.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@travetto/cli": "^3.1.
|
|
41
|
+
"@travetto/cli": "^3.1.8"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"@travetto/cli": {
|
package/src/compiler.ts
CHANGED
|
@@ -1,187 +1,145 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
-
import util from 'util';
|
|
3
|
-
import { decode as decodeEntities } from 'html-entities';
|
|
4
2
|
|
|
5
|
-
import {
|
|
6
|
-
import { RootIndex, path } from '@travetto/manifest';
|
|
7
|
-
import { StreamUtil } from '@travetto/base';
|
|
3
|
+
import { FileQueryProvider, TypedObject } from '@travetto/base';
|
|
8
4
|
import { MessageCompilationSource, MessageCompiled } from '@travetto/email';
|
|
5
|
+
import { RootIndex, path } from '@travetto/manifest';
|
|
6
|
+
import { DynamicFileLoader } from '@travetto/base/src/internal/file-loader';
|
|
7
|
+
|
|
8
|
+
import { EmailCompileUtil } from './util';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const VALID_FILE = (file: string): boolean => /[.](scss|css|png|jpe?g|gif|ya?ml)$/.test(file) && !/[.]compiled[.]/.test(file);
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Email compilation support
|
|
14
14
|
*/
|
|
15
15
|
export class EmailCompiler {
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
/** Load Template */
|
|
18
|
+
static async loadTemplate(file: string): Promise<MessageCompilationSource> {
|
|
19
|
+
const entry = RootIndex.getEntry(file);
|
|
20
|
+
if (!entry) {
|
|
21
|
+
throw new Error(`Unable to find template for ${file}`);
|
|
22
|
+
}
|
|
23
|
+
const root = (await import(entry.outputFile)).default;
|
|
24
|
+
const res: MessageCompilationSource = { ...await root.wrap(), file: entry.sourceFile };
|
|
25
|
+
const mod = RootIndex.getModule(entry.module)!;
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
const resourcePaths = [
|
|
28
|
+
path.resolve(mod.sourcePath, 'resources'),
|
|
29
|
+
path.resolve(RootIndex.manifest.workspacePath, 'resources')
|
|
30
|
+
];
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
}
|
|
32
|
+
const styles = res.styles ??= {};
|
|
33
|
+
(styles.search ??= []).push(...resourcePaths);
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
const images = res.images ??= {};
|
|
36
|
+
(images.search ??= []).push(...resourcePaths);
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
this.resources = resources;
|
|
38
|
+
return res;
|
|
43
39
|
}
|
|
44
40
|
|
|
45
41
|
/**
|
|
46
|
-
*
|
|
42
|
+
* Grab list of all available templates
|
|
47
43
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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)!);
|
|
44
|
+
static findAllTemplates(mod?: string): string[] {
|
|
45
|
+
return RootIndex
|
|
46
|
+
.findSupport({ filter: f => EmailCompileUtil.isTemplateFile(f) })
|
|
47
|
+
.filter(x => !mod || x.module === mod)
|
|
48
|
+
.map(x => x.sourceFile);
|
|
79
49
|
}
|
|
80
50
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return result[0].css;
|
|
51
|
+
/**
|
|
52
|
+
* Get output files
|
|
53
|
+
*/
|
|
54
|
+
static getOutputFiles(file: string): MessageCompiled {
|
|
55
|
+
const entry = RootIndex.getEntry(file)!;
|
|
56
|
+
const mod = RootIndex.getModule(entry.module)!;
|
|
57
|
+
return EmailCompileUtil.getOutputs(file, path.join(mod.sourcePath, 'resources'));
|
|
89
58
|
}
|
|
90
59
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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;
|
|
60
|
+
/**
|
|
61
|
+
* Get the sending email key from a template file
|
|
62
|
+
*/
|
|
63
|
+
static async templateFileToKey(file: string): Promise<string> {
|
|
64
|
+
return EmailCompileUtil.buildOutputPath(file, '');
|
|
104
65
|
}
|
|
105
66
|
|
|
106
67
|
/**
|
|
107
|
-
*
|
|
68
|
+
* Write template to file
|
|
108
69
|
*/
|
|
109
|
-
async
|
|
110
|
-
const
|
|
111
|
-
|
|
70
|
+
static async writeTemplate(file: string, msg: MessageCompiled): Promise<void> {
|
|
71
|
+
const outs = this.getOutputFiles(file);
|
|
72
|
+
await Promise.all(TypedObject.keys(outs).map(async k => {
|
|
73
|
+
if (msg[k]) {
|
|
74
|
+
await fs.mkdir(path.dirname(outs[k]), { recursive: true });
|
|
75
|
+
await fs.writeFile(outs[k], msg[k], { encoding: 'utf8' });
|
|
76
|
+
} else {
|
|
77
|
+
await fs.unlink(outs[k]).catch(() => { }); // Remove file if data not provided
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
112
80
|
}
|
|
113
81
|
|
|
114
82
|
/**
|
|
115
|
-
* Compile
|
|
83
|
+
* Compile a file given a resource provider
|
|
116
84
|
*/
|
|
117
|
-
async compile(
|
|
118
|
-
|
|
119
|
-
|
|
85
|
+
static async compile(file: string, persist: boolean = false): Promise<MessageCompiled> {
|
|
86
|
+
const src = await this.loadTemplate(file);
|
|
87
|
+
const compiled = await EmailCompileUtil.compile(src);
|
|
88
|
+
if (persist) {
|
|
89
|
+
await this.writeTemplate(file, compiled);
|
|
120
90
|
}
|
|
91
|
+
return compiled;
|
|
92
|
+
}
|
|
121
93
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const styles: string[] = [];
|
|
94
|
+
/**
|
|
95
|
+
* Compile all
|
|
96
|
+
*/
|
|
97
|
+
static async compileAll(mod?: string): Promise<string[]> {
|
|
98
|
+
const keys = this.findAllTemplates(mod);
|
|
99
|
+
await Promise.all(keys.map(src => this.compile(src, true)));
|
|
100
|
+
return keys;
|
|
101
|
+
}
|
|
131
102
|
|
|
132
|
-
if (src.styles?.global) {
|
|
133
|
-
styles.push(src.styles.global);
|
|
134
|
-
}
|
|
135
103
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Watch compilation
|
|
106
|
+
*/
|
|
107
|
+
static async * watchCompile(): AsyncIterable<string> {
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
109
|
+
const all = new FileQueryProvider(
|
|
110
|
+
[...new Set(this.findAllTemplates()
|
|
111
|
+
.map(x => RootIndex.getEntry(x)!.module)
|
|
112
|
+
)].map(x => path.resolve(RootIndex.getModule(x)!.sourcePath, 'resources'))
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const stream = all.watchFiles();
|
|
116
|
+
|
|
117
|
+
DynamicFileLoader.onLoadEvent((ev) => {
|
|
118
|
+
const src = RootIndex.getEntry(ev.file);
|
|
119
|
+
if (src && EmailCompileUtil.isTemplateFile(src.sourceFile)) {
|
|
120
|
+
stream.add({ ...ev, file: src.sourceFile });
|
|
139
121
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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);
|
|
122
|
+
});
|
|
123
|
+
for await (const { file, action } of stream) {
|
|
124
|
+
if (action === 'delete') {
|
|
125
|
+
continue;
|
|
151
126
|
}
|
|
152
|
-
}
|
|
153
127
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
128
|
+
try {
|
|
129
|
+
if (EmailCompileUtil.isTemplateFile(file)) {
|
|
130
|
+
await this.compile(file, true);
|
|
131
|
+
console.log(`Successfully compiled ${1} templates`, { changed: [file] });
|
|
132
|
+
yield file;
|
|
133
|
+
} else if (VALID_FILE(file)) {
|
|
134
|
+
const rootFile = file.replace(/\/resources.*/, '/package.json');
|
|
135
|
+
const mod = RootIndex.getFromSource(rootFile)!.module;
|
|
136
|
+
const changed = await this.compileAll(mod);
|
|
137
|
+
console.log(`Successfully compiled ${changed.length} templates`, { changed, file });
|
|
138
|
+
yield* changed;
|
|
181
139
|
}
|
|
182
|
-
})
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error(`Error in compiling ${file}`, err && err instanceof Error ? err.message : `${err}`);
|
|
142
|
+
}
|
|
183
143
|
}
|
|
184
|
-
|
|
185
|
-
return { html, text, subject };
|
|
186
144
|
}
|
|
187
145
|
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import util from 'util';
|
|
2
|
+
import { Readable } from 'stream';
|
|
3
|
+
|
|
4
|
+
import { FileResourceProvider, StreamUtil } from '@travetto/base';
|
|
5
|
+
import { MessageCompilationImages, MessageCompilationSource, MessageCompilationStyles, MessageCompiled } from '@travetto/email';
|
|
6
|
+
import { ImageConverter } from '@travetto/image';
|
|
7
|
+
import { path } from '@travetto/manifest';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
type Tokenized = {
|
|
11
|
+
text: string;
|
|
12
|
+
tokens: Map<string, string>;
|
|
13
|
+
finalize: (onToken: (token: string) => string) => string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Email compile tools
|
|
18
|
+
*/
|
|
19
|
+
export class EmailCompileUtil {
|
|
20
|
+
static #HTML_CSS_IMAGE_URLS = [
|
|
21
|
+
/(?<pre><img[^>]src=\s*["'])(?<src>[^"]+)/g,
|
|
22
|
+
/(?<pre>background(?:-image)?:\s*url[(]['"]?)(?<src>[^"')]+)/g
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
static #EXT = /[.]email[.]tsx$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Is file a template?
|
|
29
|
+
*/
|
|
30
|
+
static isTemplateFile(file: string): boolean {
|
|
31
|
+
return this.#EXT.test(file);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate singular output path given a file
|
|
36
|
+
*/
|
|
37
|
+
static buildOutputPath(file: string, suffix: string, prefix?: string): string {
|
|
38
|
+
const res = file.replace(/.*(support|src)\//, '').replace(this.#EXT, suffix);
|
|
39
|
+
return prefix ? path.join(prefix, res) : res;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the different parts from the file name
|
|
44
|
+
*/
|
|
45
|
+
static getOutputs(file: string, prefix?: string): MessageCompiled {
|
|
46
|
+
return {
|
|
47
|
+
html: this.buildOutputPath(file, '.compiled.html', prefix),
|
|
48
|
+
subject: this.buildOutputPath(file, '.compiled.subject', prefix),
|
|
49
|
+
text: this.buildOutputPath(file, '.compiled.text', prefix),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run through text and match/resolve resource urls, producing tokens
|
|
55
|
+
*
|
|
56
|
+
* @param text
|
|
57
|
+
* @param patterns
|
|
58
|
+
* @returns
|
|
59
|
+
*/
|
|
60
|
+
static async tokenizeResources(text: string, patterns: RegExp[]): Promise<Tokenized> {
|
|
61
|
+
let id = 0;
|
|
62
|
+
const tokens = new Map();
|
|
63
|
+
for (const pattern of patterns) {
|
|
64
|
+
for (const { [0]: all, groups: { pre, src } = { pre: '', src: '' } } of text.matchAll(pattern)) {
|
|
65
|
+
if (src.includes('://')) { // No urls
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const token = `@@${id += 1}@@`;
|
|
69
|
+
tokens.set(token, src);
|
|
70
|
+
text = text.replace(all, `${pre}${token}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const finalize = (onToken: (token: string) => string): string => text.replace(/@@[^@]+@@/g, t => onToken(t));
|
|
74
|
+
|
|
75
|
+
return { text, tokens, finalize };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compile SCSS content with roots as search paths for additional assets
|
|
80
|
+
*/
|
|
81
|
+
static async compileSass(src: { data: string } | { file: string }, roots: string[]): Promise<string> {
|
|
82
|
+
const sass = await import('sass');
|
|
83
|
+
const result = await util.promisify(sass.render)({
|
|
84
|
+
...src,
|
|
85
|
+
sourceMap: false,
|
|
86
|
+
includePaths: roots
|
|
87
|
+
});
|
|
88
|
+
return result!.css.toString();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Prunes unused css given html document
|
|
93
|
+
*/
|
|
94
|
+
static async pruneCss(html: string, css: string): Promise<string> {
|
|
95
|
+
const { PurgeCSS } = await import('purgecss');
|
|
96
|
+
const purge = new PurgeCSS();
|
|
97
|
+
const [result] = await purge.purge({
|
|
98
|
+
content: [{ raw: html, extension: 'html' }],
|
|
99
|
+
css: [{ raw: css }],
|
|
100
|
+
});
|
|
101
|
+
return result.css;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Moves CSS inline into html output, minus media queries
|
|
106
|
+
*/
|
|
107
|
+
static async inlineCss(html: string, css: string): Promise<string> {
|
|
108
|
+
// Inline css
|
|
109
|
+
const { default: inlineCss } = await import('inline-css');
|
|
110
|
+
return inlineCss(
|
|
111
|
+
// Style needs to be in head to preserve media queries
|
|
112
|
+
html.replace('</head>', `<style>${css}</style></head>`),
|
|
113
|
+
{
|
|
114
|
+
url: 'https://app.dev',
|
|
115
|
+
preserveMediaQueries: true,
|
|
116
|
+
removeStyleTags: true,
|
|
117
|
+
removeLinkTags: true,
|
|
118
|
+
applyStyleTags: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Simplifies text by decoding all entities
|
|
124
|
+
*
|
|
125
|
+
* @param text
|
|
126
|
+
*/
|
|
127
|
+
static async simplifiedText(text: string): Promise<string> {
|
|
128
|
+
const { decode } = await import('html-entities');
|
|
129
|
+
return decode(text.replace(/ /g, ' '));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Inline image sources
|
|
134
|
+
*/
|
|
135
|
+
static async inlineImages(html: string, opts: MessageCompilationImages): Promise<string> {
|
|
136
|
+
const { tokens, finalize } = await this.tokenizeResources(html, this.#HTML_CSS_IMAGE_URLS);
|
|
137
|
+
const pendingImages: [token: string, ext: string, stream: Readable | Promise<Readable>][] = [];
|
|
138
|
+
const resources = new FileResourceProvider(opts.search ?? []);
|
|
139
|
+
|
|
140
|
+
for (const [token, src] of tokens) {
|
|
141
|
+
const ext = path.extname(src);
|
|
142
|
+
const stream = await resources.readStream(src);
|
|
143
|
+
pendingImages.push([token, ext, /^[.](jpe?g|png)$/.test(ext) ?
|
|
144
|
+
ImageConverter.optimize(ext === '.png' ? 'png' : 'jpeg', stream) : stream]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const imageMap = new Map(await Promise.all(pendingImages.map(async ([token, ext, stream]) => {
|
|
148
|
+
const data = await StreamUtil.streamToBuffer(await stream);
|
|
149
|
+
return [token, `data:image/${ext};base64,${data.toString('base64')}`] as const;
|
|
150
|
+
})));
|
|
151
|
+
|
|
152
|
+
return finalize(token => imageMap.get(token)!);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Handle various edge cases
|
|
157
|
+
*/
|
|
158
|
+
static handleHtmlEdgeCases(html: string): string {
|
|
159
|
+
return html
|
|
160
|
+
.replace(/<(meta|img|link|hr|br)[^>]*>/g, a => a.replace('>', '/>')) // Fix self closing
|
|
161
|
+
.replace(/'/g, ''') // Fix apostrophes, as outlook hates them
|
|
162
|
+
.replace(/(background(?:-color)?:\s*)([#0-9a-f]{6,8})([^>.#,]+)>/ig,
|
|
163
|
+
(all, p, col, rest) => `${p}${col}${rest} bgcolor="${col}">`) // Inline bg-color
|
|
164
|
+
.replace(/<([^>]+vertical-align:\s*(top|bottom|middle)[^>]+)>/g,
|
|
165
|
+
(a, tag, valign) => tag.indexOf('valign') ? `<${tag}>` : `<${tag} valign="${valign}">`) // Vertically align if it has the style
|
|
166
|
+
.replace(/<(table[^>]+expand[^>]+width:\s*)(100%\s+!important)([^>]+)>/g,
|
|
167
|
+
(a, left, size, right) => `<${left}100%${right}>`); // Drop important as a fix for outlook
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Apply styles into a given html document
|
|
173
|
+
*/
|
|
174
|
+
static async applyStyles(html: string, opts: MessageCompilationStyles): Promise<string> {
|
|
175
|
+
const styles: string[] = [];
|
|
176
|
+
|
|
177
|
+
if (opts.global) {
|
|
178
|
+
styles.push(opts.global);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const resource = new FileResourceProvider(opts.search ?? []);
|
|
182
|
+
const main = await resource.read('/email/main.scss').then(d => d, () => '');
|
|
183
|
+
|
|
184
|
+
if (main) {
|
|
185
|
+
styles.push(main);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (styles.length) {
|
|
189
|
+
const compiled = await this.compileSass(
|
|
190
|
+
{ data: styles.join('\n') },
|
|
191
|
+
[...opts.search ?? []]);
|
|
192
|
+
|
|
193
|
+
// Remove all unused styles
|
|
194
|
+
const finalStyles = await this.pruneCss(html, compiled);
|
|
195
|
+
|
|
196
|
+
// Apply styles
|
|
197
|
+
html = await this.inlineCss(html, finalStyles);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return html;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static async compile(src: MessageCompilationSource): Promise<MessageCompiled> {
|
|
204
|
+
const subject = await this.simplifiedText(await src.subject());
|
|
205
|
+
const text = await this.simplifiedText(await src.text());
|
|
206
|
+
|
|
207
|
+
let html = await src.html();
|
|
208
|
+
|
|
209
|
+
if (src.styles?.inline !== false) {
|
|
210
|
+
html = await this.applyStyles(html, src.styles!);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Fix up html edge cases
|
|
214
|
+
html = this.handleHtmlEdgeCases(html);
|
|
215
|
+
|
|
216
|
+
if (src.images?.inline !== false) {
|
|
217
|
+
html = await this.inlineImages(html, src.images!);
|
|
218
|
+
}
|
|
219
|
+
return { html, subject, text };
|
|
220
|
+
}
|
|
221
|
+
}
|
package/support/bin/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
|
|
3
|
-
import { path } from '@travetto/manifest';
|
|
3
|
+
import { RootIndex, path } from '@travetto/manifest';
|
|
4
4
|
import { YamlUtil } from '@travetto/yaml';
|
|
5
5
|
|
|
6
6
|
interface ConfigType {
|
|
@@ -22,7 +22,7 @@ interface ConfigType {
|
|
|
22
22
|
*/
|
|
23
23
|
export class $EditorConfig {
|
|
24
24
|
|
|
25
|
-
#configFile =
|
|
25
|
+
#configFile: Record<string, string> = {};
|
|
26
26
|
#defaultConfig = {
|
|
27
27
|
to: 'my-email@gmail.com',
|
|
28
28
|
from: 'from-email@gmail.com',
|
|
@@ -42,9 +42,10 @@ export class $EditorConfig {
|
|
|
42
42
|
/**
|
|
43
43
|
*
|
|
44
44
|
*/
|
|
45
|
-
async get(): Promise<ConfigType> {
|
|
45
|
+
async get(file: string): Promise<ConfigType> {
|
|
46
46
|
try {
|
|
47
|
-
const
|
|
47
|
+
const mod = RootIndex.getModuleFromSource(file)!.name;
|
|
48
|
+
const content = await fs.readFile(this.#configFile[mod], 'utf8');
|
|
48
49
|
return YamlUtil.parse<ConfigType>(content);
|
|
49
50
|
} catch {
|
|
50
51
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
@@ -52,13 +53,13 @@ export class $EditorConfig {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
async getContext(): Promise<Exclude<ConfigType['context'], undefined>> {
|
|
56
|
-
const conf = await this.get();
|
|
56
|
+
async getContext(file: string): Promise<Exclude<ConfigType['context'], undefined>> {
|
|
57
|
+
const conf = await this.get(file);
|
|
57
58
|
return conf.context ?? {};
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
async getSenderConfig(): Promise<Exclude<ConfigType['sender'], undefined>> {
|
|
61
|
-
const conf = await this.get();
|
|
61
|
+
async getSenderConfig(file: string): Promise<Exclude<ConfigType['sender'], undefined>> {
|
|
62
|
+
const conf = await this.get(file);
|
|
62
63
|
return conf.sender ?? {};
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -66,13 +67,15 @@ export class $EditorConfig {
|
|
|
66
67
|
return YamlUtil.serialize(this.#defaultConfig);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
async ensureConfig(): Promise<string> {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
async ensureConfig(file: string): Promise<string> {
|
|
71
|
+
console.log('Ensuring config', file);
|
|
72
|
+
const mod = RootIndex.getModuleFromSource(file)!;
|
|
73
|
+
const resolved = this.#configFile[mod.name] ??= path.resolve(mod.sourcePath, 'resources/email/dev.yml');
|
|
74
|
+
if (!(await fs.stat(resolved).catch(() => { }))) {
|
|
75
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
76
|
+
await fs.writeFile(resolved, this.getDefaultConfig(), { encoding: 'utf8' });
|
|
74
77
|
}
|
|
75
|
-
return
|
|
78
|
+
return resolved;
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
|
package/support/bin/editor.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { ShutdownManager } from '@travetto/base';
|
|
2
|
+
|
|
1
3
|
import { EmailCompilationManager } from './manager';
|
|
2
4
|
import { EditorSendService } from './send';
|
|
3
5
|
import { EditorConfig } from './config';
|
|
4
6
|
|
|
7
|
+
import { EmailCompiler } from '../../src/compiler';
|
|
8
|
+
import { EmailCompileUtil } from '../../src/util';
|
|
9
|
+
|
|
5
10
|
type InboundMessage =
|
|
6
|
-
{ type: 'configure' } |
|
|
11
|
+
{ type: 'configure', file: string } |
|
|
7
12
|
{ type: 'redraw', file: string } |
|
|
8
13
|
{ type: 'send', file: string, from?: string, to?: string };
|
|
9
14
|
|
|
@@ -20,20 +25,18 @@ type OutboundMessage =
|
|
|
20
25
|
export class EditorState {
|
|
21
26
|
|
|
22
27
|
#lastFile = '';
|
|
23
|
-
#sender: EditorSendService;
|
|
24
28
|
#template: EmailCompilationManager;
|
|
25
29
|
|
|
26
30
|
constructor(template: EmailCompilationManager) {
|
|
27
31
|
this.#template = template;
|
|
28
|
-
this.#sender = new EditorSendService();
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
async renderFile(file: string): Promise<void> {
|
|
32
|
-
file =
|
|
35
|
+
file = EmailCompileUtil.isTemplateFile(file) ? file : this.#lastFile;
|
|
33
36
|
if (file) {
|
|
34
37
|
try {
|
|
35
38
|
const content = await this.#template.resolveCompiledTemplate(
|
|
36
|
-
file, await EditorConfig.getContext()
|
|
39
|
+
file, await EditorConfig.getContext(file)
|
|
37
40
|
);
|
|
38
41
|
this.response({
|
|
39
42
|
type: 'changed',
|
|
@@ -57,12 +60,12 @@ export class EditorState {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
async onConfigure(msg: InboundMessage & { type: 'configure' }): Promise<void> {
|
|
60
|
-
this.response({ type: 'configured', file: await EditorConfig.ensureConfig() });
|
|
63
|
+
this.response({ type: 'configured', file: await EditorConfig.ensureConfig(msg.file) });
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
async #onRedraw(msg: InboundMessage & { type: 'redraw' }): Promise<void> {
|
|
64
67
|
try {
|
|
65
|
-
await
|
|
68
|
+
await EmailCompiler.compile(msg.file, true);
|
|
66
69
|
await this.renderFile(msg.file);
|
|
67
70
|
} catch (err) {
|
|
68
71
|
if (err && err instanceof Error) {
|
|
@@ -74,12 +77,15 @@ export class EditorState {
|
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
async onSend(msg: InboundMessage & { type: 'send' }): Promise<void> {
|
|
77
|
-
const cfg = await EditorConfig.get();
|
|
80
|
+
const cfg = await EditorConfig.get(msg.file);
|
|
78
81
|
const to = msg.to || cfg.to;
|
|
79
82
|
const from = msg.from || cfg.from;
|
|
80
|
-
const
|
|
83
|
+
const content = await this.#template.resolveCompiledTemplate(
|
|
84
|
+
msg.file, await EditorConfig.getContext(msg.file)
|
|
85
|
+
);
|
|
86
|
+
|
|
81
87
|
try {
|
|
82
|
-
const url = await
|
|
88
|
+
const url = await EditorSendService.sendEmail(msg.file, { from, to, ...content, });
|
|
83
89
|
this.response({ type: 'sent', to, file: msg.file, ...url });
|
|
84
90
|
} catch (err) {
|
|
85
91
|
if (err && err instanceof Error) {
|
|
@@ -101,7 +107,11 @@ export class EditorState {
|
|
|
101
107
|
case 'send': this.onSend(msg); break;
|
|
102
108
|
}
|
|
103
109
|
});
|
|
104
|
-
|
|
110
|
+
|
|
111
|
+
process.on('disconnect', () => ShutdownManager.execute());
|
|
112
|
+
process.send?.('ready');
|
|
113
|
+
|
|
114
|
+
for await (const f of EmailCompiler.watchCompile()) {
|
|
105
115
|
await this.renderFile(f);
|
|
106
116
|
}
|
|
107
117
|
}
|
package/support/bin/manager.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
|
|
1
3
|
import type { MailTemplateEngine, MessageCompiled } from '@travetto/email';
|
|
2
|
-
import { TypedObject } from '@travetto/base';
|
|
3
4
|
import { DependencyRegistry } from '@travetto/di';
|
|
4
|
-
import {
|
|
5
|
-
|
|
5
|
+
import { TypedObject } from '@travetto/base';
|
|
6
6
|
import { MailTemplateEngineTarget } from '@travetto/email/src/internal/types';
|
|
7
|
-
import { DynamicFileLoader } from '@travetto/base/src/internal/file-loader';
|
|
8
7
|
|
|
9
|
-
import
|
|
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);
|
|
8
|
+
import { EmailCompiler } from '../../src/compiler';
|
|
13
9
|
|
|
14
10
|
/**
|
|
15
11
|
*
|
|
@@ -17,41 +13,31 @@ const VALID_FILE = (file: string): boolean => /[.](scss|css|png|jpe?g|gif|ya?ml)
|
|
|
17
13
|
export class EmailCompilationManager {
|
|
18
14
|
|
|
19
15
|
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
16
|
return new EmailCompilationManager(
|
|
24
17
|
await DependencyRegistry.getInstance<MailTemplateEngine>(MailTemplateEngineTarget),
|
|
25
|
-
new Compiler(new Res())
|
|
26
18
|
);
|
|
27
19
|
}
|
|
28
20
|
|
|
29
|
-
compiler: EmailCompiler;
|
|
30
21
|
engine: MailTemplateEngine;
|
|
31
22
|
|
|
32
|
-
constructor(engine: MailTemplateEngine
|
|
23
|
+
constructor(engine: MailTemplateEngine) {
|
|
33
24
|
this.engine = engine;
|
|
34
|
-
this.compiler = compiler;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
get resources(): EmailCompilerResource {
|
|
38
|
-
return this.compiler.resources;
|
|
39
25
|
}
|
|
40
26
|
|
|
41
27
|
/**
|
|
42
28
|
* Resolve template
|
|
43
29
|
*/
|
|
44
30
|
async resolveTemplateParts(file: string): Promise<MessageCompiled> {
|
|
45
|
-
const files =
|
|
46
|
-
const missing = await Promise.all(Object.values(files).map(x =>
|
|
31
|
+
const files = EmailCompiler.getOutputFiles(file);
|
|
32
|
+
const missing = await Promise.all(Object.values(files).map(x => fs.stat(file).catch(() => { })));
|
|
47
33
|
|
|
48
34
|
if (missing.some(x => x === undefined)) {
|
|
49
|
-
await
|
|
35
|
+
await EmailCompiler.compile(file, true);
|
|
50
36
|
}
|
|
51
37
|
|
|
52
38
|
const parts = await Promise.all(
|
|
53
39
|
TypedObject.entries(files).map(
|
|
54
|
-
([key,
|
|
40
|
+
([key, partFile]) => fs.readFile(partFile, 'utf8')
|
|
55
41
|
.then(content => [key, content] as const)
|
|
56
42
|
)
|
|
57
43
|
);
|
|
@@ -72,40 +58,4 @@ export class EmailCompilationManager {
|
|
|
72
58
|
};
|
|
73
59
|
}
|
|
74
60
|
|
|
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
61
|
}
|
package/support/bin/send.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import { MailService, SentMessage } from '@travetto/email';
|
|
1
|
+
import { MailService, MessageOptions, SentMessage } from '@travetto/email';
|
|
2
2
|
import { MailTransportTarget } from '@travetto/email/src/internal/types';
|
|
3
3
|
import { DependencyRegistry } from '@travetto/di';
|
|
4
4
|
|
|
5
5
|
import { EditorConfig } from './config';
|
|
6
|
+
import { RootIndex } from '@travetto/manifest';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Util for sending emails
|
|
9
10
|
*/
|
|
10
11
|
export class EditorSendService {
|
|
11
12
|
|
|
12
|
-
#svc: MailService;
|
|
13
|
+
static #svc: Record<string, MailService> = {};
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Get mail service
|
|
16
17
|
*/
|
|
17
|
-
async getMailService(): Promise<MailService> {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
static async getMailService(file: string): Promise<MailService> {
|
|
19
|
+
const mod = RootIndex.getModuleFromSource(file)!.name;
|
|
20
|
+
|
|
21
|
+
if (!this.#svc[mod]) {
|
|
22
|
+
const senderConfig = await EditorConfig.getSenderConfig(file);
|
|
20
23
|
|
|
21
24
|
if (senderConfig?.host?.includes('ethereal.email')) {
|
|
22
25
|
const cls = class { };
|
|
@@ -39,30 +42,32 @@ ${EditorConfig.getDefaultConfig()}`.trim();
|
|
|
39
42
|
throw new Error(errorMessage);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
this.#svc = await DependencyRegistry.getInstance(MailService);
|
|
45
|
+
this.#svc[mod] = await DependencyRegistry.getInstance(MailService);
|
|
43
46
|
}
|
|
44
|
-
return this.#svc;
|
|
47
|
+
return this.#svc[mod];
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
/**
|
|
48
51
|
* Resolve template
|
|
49
52
|
*/
|
|
50
|
-
async sendEmail(
|
|
53
|
+
static async sendEmail(file: string, message: MessageOptions): Promise<{
|
|
51
54
|
url?: string | false;
|
|
52
55
|
}> {
|
|
56
|
+
const to = message.to!;
|
|
53
57
|
try {
|
|
54
58
|
console.log('Sending email', { to });
|
|
55
59
|
// Let the engine template
|
|
56
|
-
const svc = await this.getMailService();
|
|
60
|
+
const svc = await this.getMailService(file);
|
|
57
61
|
if (!svc) {
|
|
58
62
|
throw new Error('Node mailer support is missing');
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
const info = await svc.send<{ host?: string } & SentMessage>(
|
|
65
|
+
const info = await svc.send<{ host?: string } & SentMessage>(message);
|
|
62
66
|
console.log('Sent email', { to });
|
|
63
67
|
|
|
64
|
-
const senderConfig = await EditorConfig.getSenderConfig();
|
|
68
|
+
const senderConfig = await EditorConfig.getSenderConfig(file);
|
|
65
69
|
return senderConfig.host?.includes('ethereal.email') ? {
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
|
|
66
71
|
url: (await import('nodemailer')).getTestMessageUrl(info as any)
|
|
67
72
|
} : {};
|
|
68
73
|
} catch (err) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { RootRegistry } from '@travetto/registry';
|
|
2
2
|
import { CliCommandShape, CliCommand, cliTpl } from '@travetto/cli';
|
|
3
|
-
|
|
4
|
-
import { EmailCompilationManager } from './bin/manager';
|
|
5
3
|
import { GlobalEnvConfig } from '@travetto/base';
|
|
6
4
|
|
|
5
|
+
import { EmailCompiler } from '../src/compiler';
|
|
6
|
+
|
|
7
7
|
/**
|
|
8
8
|
* CLI Entry point for running the email server
|
|
9
9
|
*/
|
|
10
|
-
@CliCommand(
|
|
10
|
+
@CliCommand()
|
|
11
11
|
export class EmailCompileCommand implements CliCommandShape {
|
|
12
12
|
|
|
13
13
|
/** Compile in watch mode */
|
|
@@ -23,14 +23,16 @@ export class EmailCompileCommand implements CliCommandShape {
|
|
|
23
23
|
async main(): Promise<void> {
|
|
24
24
|
await RootRegistry.init();
|
|
25
25
|
|
|
26
|
-
const template = await EmailCompilationManager.createInstance();
|
|
27
|
-
|
|
28
26
|
// Let the engine template
|
|
29
|
-
const all = await
|
|
27
|
+
const all = await EmailCompiler.compileAll();
|
|
30
28
|
console!.log(cliTpl`Successfully compiled ${{ param: `${all.length}` }} templates`);
|
|
29
|
+
for (const el of all) {
|
|
30
|
+
console!.log(cliTpl` * ${{ param: el }}`);
|
|
31
|
+
}
|
|
31
32
|
|
|
32
33
|
if (this.watch) {
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
for await (const _ of EmailCompiler.watchCompile()) {
|
|
34
36
|
// Iterate until done
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { GlobalEnvConfig
|
|
1
|
+
import { GlobalEnvConfig } from '@travetto/base';
|
|
2
2
|
import { CliCommand } from '@travetto/cli';
|
|
3
|
-
import { RootIndex } from '@travetto/manifest';
|
|
4
3
|
import { RootRegistry } from '@travetto/registry';
|
|
5
4
|
|
|
6
5
|
import { EditorState } from './bin/editor';
|
|
@@ -11,19 +10,11 @@ import { EmailCompilationManager } from './bin/manager';
|
|
|
11
10
|
export class EmailEditorCommand {
|
|
12
11
|
|
|
13
12
|
envInit(): GlobalEnvConfig {
|
|
14
|
-
return {
|
|
15
|
-
envName: 'dev',
|
|
16
|
-
resourcePaths: [`${RootIndex.getModule('@travetto/email-compiler')!.sourcePath}/resources`]
|
|
17
|
-
};
|
|
13
|
+
return { envName: 'dev', dynamic: true };
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
async main(): Promise<void> {
|
|
21
17
|
await RootRegistry.init();
|
|
22
|
-
|
|
23
|
-
await editor.init();
|
|
24
|
-
if (process.send) {
|
|
25
|
-
process.on('disconnect', () => ShutdownManager.execute());
|
|
26
|
-
process.send('ready');
|
|
27
|
-
}
|
|
18
|
+
await new EditorState(await EmailCompilationManager.createInstance()).init();
|
|
28
19
|
}
|
|
29
20
|
}
|
|
@@ -4,7 +4,10 @@ import { CliCommandShape, CliCommand } from '@travetto/cli';
|
|
|
4
4
|
import { GlobalEnvConfig } from '@travetto/base';
|
|
5
5
|
|
|
6
6
|
import { EmailCompilationManager } from './bin/manager';
|
|
7
|
-
import {
|
|
7
|
+
import { EditorConfig } from './bin/config';
|
|
8
|
+
import { EditorSendService } from './bin/send';
|
|
9
|
+
|
|
10
|
+
import { EmailCompiler } from '../src/compiler';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* CLI Entry point for running the email server
|
|
@@ -19,10 +22,12 @@ export class EmailTestCommand implements CliCommandShape {
|
|
|
19
22
|
async main(file: string, to: string): Promise<void> {
|
|
20
23
|
file = path.resolve(file);
|
|
21
24
|
await RootRegistry.init();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
await
|
|
26
|
-
await
|
|
25
|
+
await EmailCompiler.compile(file, true);
|
|
26
|
+
|
|
27
|
+
const mgr = await EmailCompilationManager.createInstance();
|
|
28
|
+
const cfg = await EditorConfig.get(file);
|
|
29
|
+
const content = await mgr.resolveCompiledTemplate(file, await EditorConfig.getContext(file));
|
|
30
|
+
|
|
31
|
+
await EditorSendService.sendEmail(file, { from: cfg.from, to, ...content, });
|
|
27
32
|
}
|
|
28
33
|
}
|
package/src/resource.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
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
|
-
}
|
|
File without changes
|