@travetto/email-compiler 7.0.0-rc.0 → 7.0.0-rc.2
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/package.json +8 -8
- package/src/compiler.ts +18 -18
- package/src/util.ts +34 -32
- package/support/bin/editor.ts +25 -24
- package/support/bin/send.ts +7 -7
- package/support/cli.email_compile.ts +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/email-compiler",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.2",
|
|
4
4
|
"description": "Email compiling module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"directory": "module/email-compiler"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^7.0.0-rc.
|
|
30
|
-
"@travetto/di": "^7.0.0-rc.
|
|
31
|
-
"@travetto/email": "^7.0.0-rc.
|
|
32
|
-
"@travetto/image": "^7.0.0-rc.
|
|
33
|
-
"@travetto/runtime": "^7.0.0-rc.
|
|
34
|
-
"@travetto/worker": "^7.0.0-rc.
|
|
29
|
+
"@travetto/config": "^7.0.0-rc.2",
|
|
30
|
+
"@travetto/di": "^7.0.0-rc.2",
|
|
31
|
+
"@travetto/email": "^7.0.0-rc.2",
|
|
32
|
+
"@travetto/image": "^7.0.0-rc.2",
|
|
33
|
+
"@travetto/runtime": "^7.0.0-rc.2",
|
|
34
|
+
"@travetto/worker": "^7.0.0-rc.2",
|
|
35
35
|
"@types/inline-css": "^3.0.4",
|
|
36
36
|
"html-entities": "^2.6.0",
|
|
37
37
|
"inline-css": "^4.0.3",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"sass": "^1.94.2"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@travetto/cli": "^7.0.0-rc.
|
|
42
|
+
"@travetto/cli": "^7.0.0-rc.2"
|
|
43
43
|
},
|
|
44
44
|
"peerDependenciesMeta": {
|
|
45
45
|
"@travetto/cli": {
|
package/src/compiler.ts
CHANGED
|
@@ -23,14 +23,14 @@ export class EmailCompiler {
|
|
|
23
23
|
/**
|
|
24
24
|
* Grab list of all available templates
|
|
25
25
|
*/
|
|
26
|
-
static findAllTemplates(
|
|
26
|
+
static findAllTemplates(moduleName?: string): string[] {
|
|
27
27
|
return RuntimeIndex
|
|
28
28
|
.find({
|
|
29
|
-
module:
|
|
30
|
-
folder:
|
|
31
|
-
file:
|
|
29
|
+
module: mod => !moduleName ? mod.roles.includes('std') : moduleName === mod.name,
|
|
30
|
+
folder: folder => folder === 'support',
|
|
31
|
+
file: file => EmailCompileUtil.isTemplateFile(file.sourceFile)
|
|
32
32
|
})
|
|
33
|
-
.map(
|
|
33
|
+
.map(file => file.sourceFile);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -52,14 +52,14 @@ export class EmailCompiler {
|
|
|
52
52
|
/**
|
|
53
53
|
* Write template to file
|
|
54
54
|
*/
|
|
55
|
-
static async writeTemplate(file: string,
|
|
55
|
+
static async writeTemplate(file: string, message: EmailCompiled): Promise<void> {
|
|
56
56
|
const outs = this.getOutputFiles(file);
|
|
57
|
-
await Promise.all(TypedObject.keys(outs).map(async
|
|
58
|
-
if (
|
|
59
|
-
const content = MailUtil.buildBrand(file,
|
|
60
|
-
await BinaryUtil.bufferedFileWrite(outs[
|
|
57
|
+
await Promise.all(TypedObject.keys(outs).map(async key => {
|
|
58
|
+
if (message[key]) {
|
|
59
|
+
const content = MailUtil.buildBrand(file, message[key], 'trv email:compile');
|
|
60
|
+
await BinaryUtil.bufferedFileWrite(outs[key], content);
|
|
61
61
|
} else {
|
|
62
|
-
await fs.rm(outs[
|
|
62
|
+
await fs.rm(outs[key], { force: true }); // Remove file if data not provided
|
|
63
63
|
}
|
|
64
64
|
}));
|
|
65
65
|
}
|
|
@@ -68,8 +68,8 @@ export class EmailCompiler {
|
|
|
68
68
|
* Compile a file given a resource provider
|
|
69
69
|
*/
|
|
70
70
|
static async compile(file: string): Promise<EmailCompiled> {
|
|
71
|
-
const
|
|
72
|
-
const compiled = await EmailCompileUtil.compile(
|
|
71
|
+
const template = await this.loadTemplate(file);
|
|
72
|
+
const compiled = await EmailCompileUtil.compile(template);
|
|
73
73
|
await this.writeTemplate(file, compiled);
|
|
74
74
|
return compiled;
|
|
75
75
|
}
|
|
@@ -79,7 +79,7 @@ export class EmailCompiler {
|
|
|
79
79
|
*/
|
|
80
80
|
static async compileAll(mod?: string): Promise<string[]> {
|
|
81
81
|
const keys = this.findAllTemplates(mod);
|
|
82
|
-
await Promise.all(keys.map(
|
|
82
|
+
await Promise.all(keys.map(key => this.compile(key)));
|
|
83
83
|
return keys;
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -89,16 +89,16 @@ export class EmailCompiler {
|
|
|
89
89
|
static async * watchCompile(signal?: AbortSignal): AsyncIterable<string> {
|
|
90
90
|
// Watch template files
|
|
91
91
|
for await (const { file, action } of watchCompiler({ signal })) {
|
|
92
|
-
const
|
|
93
|
-
if (!
|
|
92
|
+
const entry = RuntimeIndex.getEntry(file);
|
|
93
|
+
if (!entry || !EmailCompileUtil.isTemplateFile(entry.sourceFile) || action === 'delete') {
|
|
94
94
|
continue;
|
|
95
95
|
}
|
|
96
96
|
try {
|
|
97
97
|
await this.compile(file);
|
|
98
98
|
console.log('Successfully compiled template', { changed: [file] });
|
|
99
99
|
yield file;
|
|
100
|
-
} catch (
|
|
101
|
-
console.error(`Error in compiling ${file}`,
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error(`Error in compiling ${file}`, error && error instanceof Error ? error.message : `${error}`);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
}
|
package/src/util.ts
CHANGED
|
@@ -11,11 +11,11 @@ type Tokenized = {
|
|
|
11
11
|
finalize: (onToken: (token: string) => string) => string;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const SUPPORT_SOURCE = /(?:support|src)\//;
|
|
15
15
|
|
|
16
16
|
const HTML_CSS_IMAGE_URLS = [
|
|
17
|
-
/(?<
|
|
18
|
-
/(?<
|
|
17
|
+
/(?<prefix><img[^>]src=\s{0,10}["'])(?<source>[^"{}]{1,1000})/g,
|
|
18
|
+
/(?<prefix>background(?:-image)?:\s{0,10}url[(]['"]?)(?<source>[^"'){}]{1,1000})/g
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
const EXT = /[.]email[.]tsx$/;
|
|
@@ -36,8 +36,8 @@ export class EmailCompileUtil {
|
|
|
36
36
|
* Generate singular output path given a file
|
|
37
37
|
*/
|
|
38
38
|
static buildOutputPath(file: string, suffix: string, prefix?: string): string {
|
|
39
|
-
const
|
|
40
|
-
return prefix ? path.join(prefix,
|
|
39
|
+
const location = (SUPPORT_SOURCE.test(file) ? file.split(SUPPORT_SOURCE)[1] : file).replace(EXT, suffix);
|
|
40
|
+
return prefix ? path.join(prefix, location) : location;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -62,16 +62,16 @@ export class EmailCompileUtil {
|
|
|
62
62
|
let id = 0;
|
|
63
63
|
const tokens = new Map();
|
|
64
64
|
for (const pattern of patterns) {
|
|
65
|
-
for (const { [0]: all, groups: {
|
|
66
|
-
if (
|
|
65
|
+
for (const { [0]: all, groups: { prefix, source } = { prefix: '', source: '' } } of text.matchAll(pattern)) {
|
|
66
|
+
if (source.includes('://')) { // No urls
|
|
67
67
|
continue;
|
|
68
68
|
}
|
|
69
69
|
const token = `@@${id += 1}@@`;
|
|
70
|
-
tokens.set(token,
|
|
71
|
-
text = text.replace(all, `${
|
|
70
|
+
tokens.set(token, source);
|
|
71
|
+
text = text.replace(all, `${prefix}${token}`);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
const finalize = (onToken: (token: string) => string): string => text.replace(/@@[^@]{1,100}@@/g,
|
|
74
|
+
const finalize = (onToken: (token: string) => string): string => text.replace(/@@[^@]{1,100}@@/g, token => onToken(token));
|
|
75
75
|
|
|
76
76
|
return { text, tokens, finalize };
|
|
77
77
|
}
|
|
@@ -79,12 +79,12 @@ export class EmailCompileUtil {
|
|
|
79
79
|
/**
|
|
80
80
|
* Compile SCSS content with roots as search paths for additional assets
|
|
81
81
|
*/
|
|
82
|
-
static async compileSass(
|
|
82
|
+
static async compileSass(input: { data: string } | { file: string }, options: EmailTemplateResource): Promise<string> {
|
|
83
83
|
const sass = await import('sass');
|
|
84
84
|
const result = await util.promisify(sass.render)({
|
|
85
|
-
...
|
|
85
|
+
...input,
|
|
86
86
|
sourceMap: false,
|
|
87
|
-
includePaths:
|
|
87
|
+
includePaths: options.loader.searchPaths.slice(0)
|
|
88
88
|
});
|
|
89
89
|
return result!.css.toString();
|
|
90
90
|
}
|
|
@@ -133,21 +133,21 @@ export class EmailCompileUtil {
|
|
|
133
133
|
/**
|
|
134
134
|
* Inline image sources
|
|
135
135
|
*/
|
|
136
|
-
static async inlineImages(html: string,
|
|
136
|
+
static async inlineImages(html: string, options: EmailTemplateResource): Promise<string> {
|
|
137
137
|
const { tokens, finalize } = await this.tokenizeResources(html, HTML_CSS_IMAGE_URLS);
|
|
138
138
|
const pendingImages: [token: string, ext: string, stream: Buffer | Promise<Buffer>][] = [];
|
|
139
139
|
|
|
140
|
-
for (const [token,
|
|
141
|
-
const ext = path.extname(
|
|
140
|
+
for (const [token, source] of tokens) {
|
|
141
|
+
const ext = path.extname(source);
|
|
142
142
|
if (/^[.](jpe?g|png)$/.test(ext)) {
|
|
143
143
|
const output = await ImageUtil.convert(
|
|
144
|
-
await
|
|
144
|
+
await options.loader.readStream(source),
|
|
145
145
|
{ format: ext === '.png' ? 'png' : 'jpeg' }
|
|
146
146
|
);
|
|
147
147
|
const buffer = await toBuffer(output);
|
|
148
148
|
pendingImages.push([token, ext, buffer]);
|
|
149
149
|
} else {
|
|
150
|
-
pendingImages.push([token, ext,
|
|
150
|
+
pendingImages.push([token, ext, options.loader.read(source, true)]);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -167,7 +167,7 @@ export class EmailCompileUtil {
|
|
|
167
167
|
.replace(/<(meta|img|link|hr|br)[^>]{0,200}>/g, a => a.replace(/>/g, '/>')) // Fix self closing
|
|
168
168
|
.replace(/'/g, ''') // Fix apostrophes, as outlook hates them
|
|
169
169
|
.replace(/(background(?:-color)?:\s*)([#0-9a-f]{6,8})([^>.#,]+)>/ig,
|
|
170
|
-
(all,
|
|
170
|
+
(all, property, color, rest) => `${property}${color}${rest} bgcolor="${color}">`) // Inline bg-color
|
|
171
171
|
.replace(/<([^>]+vertical-align:\s*(top|bottom|middle)[^>]+)>/g,
|
|
172
172
|
(a, tag, valign) => tag.indexOf('valign') ? `<${tag}>` : `<${tag} valign="${valign}">`) // Vertically align if it has the style
|
|
173
173
|
.replace(/<(table[^>]+expand[^>]+width:\s*)(100%\s+!important)([^>]+)>/g,
|
|
@@ -179,14 +179,16 @@ export class EmailCompileUtil {
|
|
|
179
179
|
/**
|
|
180
180
|
* Apply styles into a given html document
|
|
181
181
|
*/
|
|
182
|
-
static async applyStyles(html: string,
|
|
182
|
+
static async applyStyles(html: string, options: EmailTemplateResource): Promise<string> {
|
|
183
183
|
const styles = [
|
|
184
|
-
|
|
185
|
-
await
|
|
186
|
-
]
|
|
184
|
+
options.globalStyles ?? '',
|
|
185
|
+
await options.loader.read('/email/main.scss').catch(() => '')
|
|
186
|
+
]
|
|
187
|
+
.filter(line => !!line)
|
|
188
|
+
.join('\n');
|
|
187
189
|
|
|
188
190
|
if (styles.length) {
|
|
189
|
-
const compiled = await this.compileSass({ data: styles },
|
|
191
|
+
const compiled = await this.compileSass({ data: styles }, options);
|
|
190
192
|
|
|
191
193
|
// Remove all unused styles
|
|
192
194
|
const finalStyles = await this.pruneCss(html, compiled);
|
|
@@ -198,21 +200,21 @@ export class EmailCompileUtil {
|
|
|
198
200
|
return html;
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
static async compile(
|
|
202
|
-
const subject = await this.simplifiedText(await
|
|
203
|
-
const text = await this.simplifiedText(await
|
|
203
|
+
static async compile(input: EmailTemplateModule): Promise<EmailCompiled> {
|
|
204
|
+
const subject = await this.simplifiedText(await input.subject());
|
|
205
|
+
const text = await this.simplifiedText(await input.text());
|
|
204
206
|
|
|
205
|
-
let html = await
|
|
207
|
+
let html = await input.html();
|
|
206
208
|
|
|
207
|
-
if (
|
|
208
|
-
html = await this.applyStyles(html,
|
|
209
|
+
if (input.inlineStyle !== false) {
|
|
210
|
+
html = await this.applyStyles(html, input);
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
// Fix up html edge cases
|
|
212
214
|
html = this.handleHtmlEdgeCases(html);
|
|
213
215
|
|
|
214
|
-
if (
|
|
215
|
-
html = await this.inlineImages(html,
|
|
216
|
+
if (input.inlineImages !== false) {
|
|
217
|
+
html = await this.inlineImages(html, input);
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
return { html, subject, text };
|
package/support/bin/editor.ts
CHANGED
|
@@ -24,10 +24,11 @@ export class EditorService {
|
|
|
24
24
|
return Promise.resolve(this.engine.render(text, context)).then(MailUtil.purgeBrand);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
async #renderTemplate(
|
|
28
|
-
const
|
|
27
|
+
async #renderTemplate(templateFile: string, context: Record<string, unknown>): Promise<EmailCompiled> {
|
|
28
|
+
const email = await EmailCompiler.compile(templateFile);
|
|
29
29
|
return TypedObject.fromEntries(
|
|
30
|
-
await Promise.all(TypedObject.entries(
|
|
30
|
+
await Promise.all(TypedObject.entries(email).map(([key, value]) =>
|
|
31
|
+
this.#interpolate(value, context).then((result) => [key, result])))
|
|
31
32
|
);
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -36,24 +37,24 @@ export class EditorService {
|
|
|
36
37
|
return { content, file };
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
async #response<T>(
|
|
40
|
+
async #response<T>(operation: Promise<T>, success: (value: T) => EditorResponse, fail?: (error: Error) => EditorResponse): Promise<void> {
|
|
40
41
|
try {
|
|
41
|
-
const
|
|
42
|
-
if (process.connected) { process.send?.(success(
|
|
43
|
-
} catch (
|
|
44
|
-
if (fail && process.connected &&
|
|
45
|
-
process.send?.(fail(
|
|
42
|
+
const response = await operation;
|
|
43
|
+
if (process.connected) { process.send?.(success(response)); }
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (fail && process.connected && error && error instanceof Error) {
|
|
46
|
+
process.send?.(fail(error));
|
|
46
47
|
} else {
|
|
47
|
-
console.error(
|
|
48
|
+
console.error(error);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
async sendFile(file: string, to?: string): Promise<{ to: string, file: string, url?: string | false | undefined }> {
|
|
53
|
-
const
|
|
54
|
-
to ||=
|
|
55
|
-
const content = await this.#renderTemplate(file,
|
|
56
|
-
return { to, file, ...await this.sender.send({ from:
|
|
54
|
+
const config = await EditorConfig.get();
|
|
55
|
+
to ||= config.to;
|
|
56
|
+
const content = await this.#renderTemplate(file, config.context ?? {});
|
|
57
|
+
return { to, file, ...await this.sender.send({ from: config.from, to, ...content, }) };
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
@@ -63,22 +64,22 @@ export class EditorService {
|
|
|
63
64
|
if (!process.connected || !process.send) {
|
|
64
65
|
throw new AppError('Unable to run email editor, missing ipc channel');
|
|
65
66
|
}
|
|
66
|
-
process.on('message', async (
|
|
67
|
-
switch (
|
|
67
|
+
process.on('message', async (request: EditorRequest) => {
|
|
68
|
+
switch (request.type) {
|
|
68
69
|
case 'configure': {
|
|
69
70
|
return await this.#response(EditorConfig.ensureConfig(), file => ({ type: 'configured', file }));
|
|
70
71
|
}
|
|
71
72
|
case 'compile': {
|
|
72
|
-
return await this.#response(this.#renderFile(
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
return await this.#response(this.#renderFile(request.file),
|
|
74
|
+
result => ({ type: 'compiled', ...result }),
|
|
75
|
+
error => ({ type: 'compiled-failed', message: error.message, stack: error.stack, file: request.file })
|
|
75
76
|
);
|
|
76
77
|
}
|
|
77
78
|
case 'send': {
|
|
78
79
|
return await this.#response(
|
|
79
|
-
this.sendFile(
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
this.sendFile(request.file, request.to),
|
|
81
|
+
result => ({ type: 'sent', ...result }),
|
|
82
|
+
error => ({ type: 'sent-failed', message: error.message, stack: error.stack, to: request.to!, file: request.file })
|
|
82
83
|
);
|
|
83
84
|
}
|
|
84
85
|
}
|
|
@@ -88,8 +89,8 @@ export class EditorService {
|
|
|
88
89
|
|
|
89
90
|
for await (const file of EmailCompiler.watchCompile()) {
|
|
90
91
|
await this.#response(this.#renderFile(file),
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
result => ({ type: 'compiled', ...result }),
|
|
93
|
+
error => ({ type: 'compiled-failed', message: error.message, stack: error.stack, file })
|
|
93
94
|
);
|
|
94
95
|
}
|
|
95
96
|
}
|
package/support/bin/send.ts
CHANGED
|
@@ -33,7 +33,7 @@ export class EditorSendService {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
|
-
Registry.process([{ type: 'added',
|
|
36
|
+
Registry.process([{ type: 'added', current: cls }]);
|
|
37
37
|
|
|
38
38
|
this.ethereal = !!senderConfig.host?.includes('ethereal.email');
|
|
39
39
|
} catch {
|
|
@@ -51,23 +51,23 @@ export class EditorSendService {
|
|
|
51
51
|
const to = message.to!;
|
|
52
52
|
try {
|
|
53
53
|
console.log('Sending email', { to });
|
|
54
|
-
const
|
|
54
|
+
const service = await this.service();
|
|
55
55
|
if (this.ethereal) {
|
|
56
56
|
const { getTestMessageUrl } = await import('nodemailer');
|
|
57
57
|
const { default: _smtp } = await import('nodemailer/lib/smtp-transport/index');
|
|
58
58
|
type SendMessage = Parameters<Parameters<(typeof _smtp)['prototype']['send']>[1]>[1];
|
|
59
|
-
const info = await
|
|
59
|
+
const info = await service.send<SendMessage>(message);
|
|
60
60
|
const url = getTestMessageUrl(info);
|
|
61
61
|
console.log('Sent email', { to, url });
|
|
62
62
|
return { url };
|
|
63
63
|
} else {
|
|
64
|
-
await
|
|
64
|
+
await service.send(message);
|
|
65
65
|
console.log('Sent email', { to });
|
|
66
66
|
return {};
|
|
67
67
|
}
|
|
68
|
-
} catch (
|
|
69
|
-
console.warn('Failed to send email', { to, error
|
|
70
|
-
throw
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn('Failed to send email', { to, error });
|
|
70
|
+
throw error;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -23,10 +23,10 @@ export class EmailCompileCommand implements CliCommandShape {
|
|
|
23
23
|
await Registry.init();
|
|
24
24
|
|
|
25
25
|
// Let the engine template
|
|
26
|
-
const
|
|
27
|
-
console!.log(cliTpl`Successfully compiled ${{ param: `${
|
|
28
|
-
for (const
|
|
29
|
-
console!.log(cliTpl` * ${{ param: Runtime.stripWorkspacePath(
|
|
26
|
+
const locations = await EmailCompiler.compileAll();
|
|
27
|
+
console!.log(cliTpl`Successfully compiled ${{ param: `${locations.length}` }} templates`);
|
|
28
|
+
for (const location of locations) {
|
|
29
|
+
console!.log(cliTpl` * ${{ param: Runtime.stripWorkspacePath(location) }}`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (this.watch) {
|