@technomoron/mail-magic-client 1.0.23
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/CHANGES +4 -0
- package/README.md +102 -0
- package/dist/cjs/mail-magic-client.d.ts +82 -0
- package/dist/cjs/mail-magic-client.js +281 -0
- package/dist/cli-env.js +55 -0
- package/dist/cli-helpers.js +405 -0
- package/dist/cli.js +307 -0
- package/dist/esm/mail-magic-client.js +276 -0
- package/dist/mail-magic-client.js +281 -0
- package/dist/preprocess.js +310 -0
- package/package.json +69 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const email_addresses_1 = __importDefault(require("email-addresses"));
|
|
9
|
+
const nunjucks_1 = __importDefault(require("nunjucks"));
|
|
10
|
+
class templateClient {
|
|
11
|
+
constructor(baseURL, apiKey) {
|
|
12
|
+
this.baseURL = baseURL;
|
|
13
|
+
this.apiKey = apiKey;
|
|
14
|
+
if (!apiKey || !baseURL) {
|
|
15
|
+
throw new Error('Apikey/api-url required');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async request(method, command, body) {
|
|
19
|
+
const url = `${this.baseURL}${command}`;
|
|
20
|
+
const options = {
|
|
21
|
+
method,
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
Authorization: `Bearer apikey-${this.apiKey}`
|
|
25
|
+
},
|
|
26
|
+
body: body ? JSON.stringify(body) : '{}'
|
|
27
|
+
};
|
|
28
|
+
// console.log(JSON.stringify({ options, url }));
|
|
29
|
+
const response = await fetch(url, options);
|
|
30
|
+
const j = await response.json();
|
|
31
|
+
if (response.ok) {
|
|
32
|
+
return j;
|
|
33
|
+
}
|
|
34
|
+
// console.log(JSON.stringify(j, undefined, 2));
|
|
35
|
+
if (j && j.message) {
|
|
36
|
+
throw new Error(`FETCH FAILED: ${response.status} ${j.message}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async get(command) {
|
|
43
|
+
return this.request('GET', command);
|
|
44
|
+
}
|
|
45
|
+
async post(command, body) {
|
|
46
|
+
return this.request('POST', command, body);
|
|
47
|
+
}
|
|
48
|
+
async put(command, body) {
|
|
49
|
+
return this.request('PUT', command, body);
|
|
50
|
+
}
|
|
51
|
+
async delete(command, body) {
|
|
52
|
+
return this.request('DELETE', command, body);
|
|
53
|
+
}
|
|
54
|
+
validateEmails(list) {
|
|
55
|
+
const valid = [], invalid = [];
|
|
56
|
+
const emails = list
|
|
57
|
+
.split(',')
|
|
58
|
+
.map((email) => email.trim())
|
|
59
|
+
.filter((email) => email !== '');
|
|
60
|
+
emails.forEach((email) => {
|
|
61
|
+
const parsed = email_addresses_1.default.parseOneAddress(email);
|
|
62
|
+
if (parsed && parsed.address) {
|
|
63
|
+
valid.push(parsed.address);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
invalid.push(email);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return { valid, invalid };
|
|
70
|
+
}
|
|
71
|
+
validateTemplate(template) {
|
|
72
|
+
try {
|
|
73
|
+
const env = new nunjucks_1.default.Environment(new nunjucks_1.default.FileSystemLoader(['./templates']));
|
|
74
|
+
const t = env.renderString(template, {});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof Error) {
|
|
78
|
+
throw new Error(`Template validation failed: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
throw new Error('Template validation failed with an unknown error');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
validateSender(sender) {
|
|
86
|
+
const exp = /^[^<>]+<[^<>]+@[^<>]+\.[^<>]+>$/;
|
|
87
|
+
if (!exp.test(sender)) {
|
|
88
|
+
throw new Error('Invalid sender format. Expected "Name <email@example.com>"');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
createAttachmentPayload(attachments) {
|
|
92
|
+
const formData = new FormData();
|
|
93
|
+
const usedFields = [];
|
|
94
|
+
for (const attachment of attachments) {
|
|
95
|
+
if (!attachment?.path) {
|
|
96
|
+
throw new Error('Attachment path is required');
|
|
97
|
+
}
|
|
98
|
+
const raw = fs_1.default.readFileSync(attachment.path);
|
|
99
|
+
const filename = attachment.filename || path_1.default.basename(attachment.path);
|
|
100
|
+
const blob = new Blob([raw], attachment.contentType ? { type: attachment.contentType } : undefined);
|
|
101
|
+
const field = attachment.field || 'attachment';
|
|
102
|
+
formData.append(field, blob, filename);
|
|
103
|
+
usedFields.push(field);
|
|
104
|
+
}
|
|
105
|
+
return { formData, usedFields };
|
|
106
|
+
}
|
|
107
|
+
appendFields(formData, fields) {
|
|
108
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
109
|
+
if (value === undefined || value === null) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (typeof value === 'string') {
|
|
113
|
+
formData.append(key, value);
|
|
114
|
+
}
|
|
115
|
+
else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
116
|
+
formData.append(key, String(value));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
formData.append(key, JSON.stringify(value));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async postFormData(command, formData) {
|
|
124
|
+
const url = `${this.baseURL}${command}`;
|
|
125
|
+
const response = await fetch(url, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
Authorization: `Bearer apikey-${this.apiKey}`
|
|
129
|
+
},
|
|
130
|
+
body: formData
|
|
131
|
+
});
|
|
132
|
+
const j = await response.json();
|
|
133
|
+
if (response.ok) {
|
|
134
|
+
return j;
|
|
135
|
+
}
|
|
136
|
+
if (j && j.message) {
|
|
137
|
+
throw new Error(`FETCH FAILED: ${response.status} ${j.message}`);
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
|
|
140
|
+
}
|
|
141
|
+
async storeTemplate(td) {
|
|
142
|
+
if (!td.template) {
|
|
143
|
+
throw new Error('No template data provided');
|
|
144
|
+
}
|
|
145
|
+
this.validateTemplate(td.template);
|
|
146
|
+
if (td.sender) {
|
|
147
|
+
this.validateSender(td.sender);
|
|
148
|
+
}
|
|
149
|
+
return this.storeTxTemplate(td);
|
|
150
|
+
}
|
|
151
|
+
async sendTemplate(std) {
|
|
152
|
+
if (!std.name || !std.rcpt) {
|
|
153
|
+
throw new Error('Invalid request body; name/rcpt required');
|
|
154
|
+
}
|
|
155
|
+
return this.sendTxMessage(std);
|
|
156
|
+
}
|
|
157
|
+
async storeTxTemplate(td) {
|
|
158
|
+
if (!td.template) {
|
|
159
|
+
throw new Error('No template data provided');
|
|
160
|
+
}
|
|
161
|
+
this.validateTemplate(td.template);
|
|
162
|
+
if (td.sender) {
|
|
163
|
+
this.validateSender(td.sender);
|
|
164
|
+
}
|
|
165
|
+
return this.post('/api/v1/tx/template', td);
|
|
166
|
+
}
|
|
167
|
+
async sendTxMessage(std) {
|
|
168
|
+
if (!std.name || !std.rcpt) {
|
|
169
|
+
throw new Error('Invalid request body; name/rcpt required');
|
|
170
|
+
}
|
|
171
|
+
const { valid, invalid } = this.validateEmails(std.rcpt);
|
|
172
|
+
if (invalid.length > 0) {
|
|
173
|
+
throw new Error('Invalid email address(es): ' + invalid.join(','));
|
|
174
|
+
}
|
|
175
|
+
// this.validateTemplate(template);
|
|
176
|
+
const body = {
|
|
177
|
+
name: std.name,
|
|
178
|
+
rcpt: std.rcpt,
|
|
179
|
+
domain: std.domain || '',
|
|
180
|
+
locale: std.locale || '',
|
|
181
|
+
vars: std.vars || {},
|
|
182
|
+
replyTo: std.replyTo,
|
|
183
|
+
headers: std.headers
|
|
184
|
+
};
|
|
185
|
+
// console.log(JSON.stringify(body, undefined, 2));
|
|
186
|
+
if (std.attachments && std.attachments.length > 0) {
|
|
187
|
+
if (std.headers) {
|
|
188
|
+
throw new Error('Headers are not supported with attachment uploads');
|
|
189
|
+
}
|
|
190
|
+
const { formData } = this.createAttachmentPayload(std.attachments);
|
|
191
|
+
this.appendFields(formData, {
|
|
192
|
+
name: std.name,
|
|
193
|
+
rcpt: std.rcpt,
|
|
194
|
+
domain: std.domain || '',
|
|
195
|
+
locale: std.locale || '',
|
|
196
|
+
vars: JSON.stringify(std.vars || {}),
|
|
197
|
+
replyTo: std.replyTo
|
|
198
|
+
});
|
|
199
|
+
return this.postFormData('/api/v1/tx/message', formData);
|
|
200
|
+
}
|
|
201
|
+
return this.post('/api/v1/tx/message', body);
|
|
202
|
+
}
|
|
203
|
+
async storeFormTemplate(data) {
|
|
204
|
+
if (!data.template) {
|
|
205
|
+
throw new Error('No template data provided');
|
|
206
|
+
}
|
|
207
|
+
if (!data.idname) {
|
|
208
|
+
throw new Error('Missing form identifier');
|
|
209
|
+
}
|
|
210
|
+
if (!data.sender) {
|
|
211
|
+
throw new Error('Missing sender address');
|
|
212
|
+
}
|
|
213
|
+
if (!data.recipient) {
|
|
214
|
+
throw new Error('Missing recipient address');
|
|
215
|
+
}
|
|
216
|
+
this.validateTemplate(data.template);
|
|
217
|
+
this.validateSender(data.sender);
|
|
218
|
+
return this.post('/api/v1/form/template', data);
|
|
219
|
+
}
|
|
220
|
+
async sendFormMessage(data) {
|
|
221
|
+
if (!data.formid) {
|
|
222
|
+
throw new Error('Invalid request body; formid required');
|
|
223
|
+
}
|
|
224
|
+
const fields = data.fields || {};
|
|
225
|
+
const baseFields = {
|
|
226
|
+
formid: data.formid,
|
|
227
|
+
secret: data.secret,
|
|
228
|
+
recipient: data.recipient,
|
|
229
|
+
domain: data.domain,
|
|
230
|
+
locale: data.locale,
|
|
231
|
+
vars: data.vars || {},
|
|
232
|
+
replyTo: data.replyTo,
|
|
233
|
+
...fields
|
|
234
|
+
};
|
|
235
|
+
if (data.attachments && data.attachments.length > 0) {
|
|
236
|
+
const { formData } = this.createAttachmentPayload(data.attachments);
|
|
237
|
+
this.appendFields(formData, {
|
|
238
|
+
formid: data.formid,
|
|
239
|
+
secret: data.secret,
|
|
240
|
+
recipient: data.recipient,
|
|
241
|
+
domain: data.domain,
|
|
242
|
+
locale: data.locale,
|
|
243
|
+
vars: JSON.stringify(data.vars || {}),
|
|
244
|
+
replyTo: data.replyTo
|
|
245
|
+
});
|
|
246
|
+
this.appendFields(formData, fields);
|
|
247
|
+
return this.postFormData('/api/v1/form/message', formData);
|
|
248
|
+
}
|
|
249
|
+
return this.post('/api/v1/form/message', baseFields);
|
|
250
|
+
}
|
|
251
|
+
async uploadAssets(data) {
|
|
252
|
+
if (!data.domain) {
|
|
253
|
+
throw new Error('domain is required');
|
|
254
|
+
}
|
|
255
|
+
if (!data.files || data.files.length === 0) {
|
|
256
|
+
throw new Error('At least one asset file is required');
|
|
257
|
+
}
|
|
258
|
+
if (data.templateType && !data.template) {
|
|
259
|
+
throw new Error('template is required when templateType is provided');
|
|
260
|
+
}
|
|
261
|
+
if (data.template && !data.templateType) {
|
|
262
|
+
throw new Error('templateType is required when template is provided');
|
|
263
|
+
}
|
|
264
|
+
const attachments = data.files.map((input) => {
|
|
265
|
+
if (typeof input === 'string') {
|
|
266
|
+
return { path: input, field: 'asset' };
|
|
267
|
+
}
|
|
268
|
+
return { ...input, field: input.field || 'asset' };
|
|
269
|
+
});
|
|
270
|
+
const { formData } = this.createAttachmentPayload(attachments);
|
|
271
|
+
this.appendFields(formData, {
|
|
272
|
+
domain: data.domain,
|
|
273
|
+
templateType: data.templateType,
|
|
274
|
+
template: data.template,
|
|
275
|
+
locale: data.locale,
|
|
276
|
+
path: data.path
|
|
277
|
+
});
|
|
278
|
+
return this.postFormData('/api/v1/assets', formData);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
exports.default = templateClient;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Merge templates in source dir using nunjucks blocks and layouts.
|
|
4
|
+
* Preserve flow control and variable expansion for later dynamic data.
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.do_the_template_thing = do_the_template_thing;
|
|
11
|
+
exports.compileTemplate = compileTemplate;
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const cheerio_1 = require("cheerio");
|
|
15
|
+
const juice_1 = __importDefault(require("juice"));
|
|
16
|
+
const nunjucks_1 = __importDefault(require("nunjucks"));
|
|
17
|
+
const cfg = {
|
|
18
|
+
env: null,
|
|
19
|
+
src_dir: 'templates',
|
|
20
|
+
dist_dir: 'templates-dist',
|
|
21
|
+
css_path: node_path_1.default.join(process.cwd(), 'templates', 'foundation-emails.css'),
|
|
22
|
+
css_content: null,
|
|
23
|
+
inline_includes: true
|
|
24
|
+
};
|
|
25
|
+
function resolvePathRoot(dir) {
|
|
26
|
+
return node_path_1.default.isAbsolute(dir) ? dir : node_path_1.default.join(process.cwd(), dir);
|
|
27
|
+
}
|
|
28
|
+
function resolveCssPath(cssPath) {
|
|
29
|
+
if (!cssPath) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
return node_path_1.default.isAbsolute(cssPath) ? cssPath : node_path_1.default.join(process.cwd(), cssPath);
|
|
33
|
+
}
|
|
34
|
+
function inlineIncludes(content, baseDir, srcRoot, stack) {
|
|
35
|
+
const includeExp = /\{%\s*include\s+['"]([^'"]+)['"][^%]*%\}/g;
|
|
36
|
+
return content.replace(includeExp, (_match, includePath) => {
|
|
37
|
+
const cleaned = includePath.replace(/^\/+/, '');
|
|
38
|
+
const candidates = [node_path_1.default.resolve(baseDir, cleaned), node_path_1.default.resolve(srcRoot, cleaned)];
|
|
39
|
+
const found = candidates.find((candidate) => node_fs_1.default.existsSync(candidate));
|
|
40
|
+
if (!found) {
|
|
41
|
+
throw new Error(`Include not found: ${includePath}`);
|
|
42
|
+
}
|
|
43
|
+
const resolved = node_fs_1.default.realpathSync(found);
|
|
44
|
+
if (stack.has(resolved)) {
|
|
45
|
+
throw new Error(`Circular include detected for ${includePath}`);
|
|
46
|
+
}
|
|
47
|
+
stack.add(resolved);
|
|
48
|
+
const raw = node_fs_1.default.readFileSync(resolved, 'utf8');
|
|
49
|
+
const inlined = inlineIncludes(raw, node_path_1.default.dirname(resolved), srcRoot, stack);
|
|
50
|
+
stack.delete(resolved);
|
|
51
|
+
return inlined;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
class PreprocessExtension {
|
|
55
|
+
constructor() {
|
|
56
|
+
this.tags = ['process_layout'];
|
|
57
|
+
}
|
|
58
|
+
// types from nunjucks are not exported for parser/nodes; use any
|
|
59
|
+
parse(parser, nodes) {
|
|
60
|
+
const token = parser.nextToken();
|
|
61
|
+
const args = parser.parseSignature(null, true);
|
|
62
|
+
parser.advanceAfterBlockEnd(token.value);
|
|
63
|
+
return new nodes.CallExtension(this, 'run', args);
|
|
64
|
+
}
|
|
65
|
+
run(_context, tplname) {
|
|
66
|
+
const template = cfg.env.getTemplate(tplname);
|
|
67
|
+
const src = template.tmplStr;
|
|
68
|
+
const extmatch = src.match(/\{%\s*extends\s+['"]([^'"]+)['"]\s*%\}/);
|
|
69
|
+
if (!extmatch)
|
|
70
|
+
return src;
|
|
71
|
+
const layoutName = extmatch[1];
|
|
72
|
+
const layoutTemplate = cfg.env.getTemplate(layoutName);
|
|
73
|
+
const layoutSrc = layoutTemplate.tmplStr;
|
|
74
|
+
const blocks = {};
|
|
75
|
+
const blockexp = /\{%\s*block\s+([a-zA-Z0-9_]+)\s*%\}([\s\S]*?)\{%\s*endblock\s*%\}/g;
|
|
76
|
+
let match;
|
|
77
|
+
while ((match = blockexp.exec(src)) !== null) {
|
|
78
|
+
const bname = match[1];
|
|
79
|
+
const bcontent = match[2];
|
|
80
|
+
blocks[bname] = bcontent.trim();
|
|
81
|
+
}
|
|
82
|
+
let merged = layoutSrc;
|
|
83
|
+
for (const [bname, bcontent] of Object.entries(blocks)) {
|
|
84
|
+
const lbexpt = new RegExp(`\\{%\\s*block\\s+${bname}\\s*%\\}[\\s\\S]*?\\{%\\s*endblock\\s*%\\}`, 'g');
|
|
85
|
+
merged = merged.replace(lbexpt, bcontent);
|
|
86
|
+
}
|
|
87
|
+
merged = merged.replace(/\{%\s*extends\s+['"][^'"]+['"]\s*%\}/, '');
|
|
88
|
+
if (merged.match(/\{%\s*extends\s+['"]([^'"]+)['"]\s*%\}/)) {
|
|
89
|
+
return this.run(_context, layoutName);
|
|
90
|
+
}
|
|
91
|
+
merged = merged.replace(/\{%\s*block\s+([a-zA-Z0-9_]+)\s*%\}\s*\{%\s*endblock\s*%\}/g, '');
|
|
92
|
+
return merged;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function process_template(tplname, writeOutput = true) {
|
|
96
|
+
console.log(`Processing template: ${tplname}`);
|
|
97
|
+
try {
|
|
98
|
+
const srcRoot = resolvePathRoot(cfg.src_dir);
|
|
99
|
+
const templateFile = node_path_1.default.join(srcRoot, `${tplname}.njk`);
|
|
100
|
+
// 1) Resolve template inheritance
|
|
101
|
+
const mergedTemplate = cfg.env.renderString(`{% process_layout "${tplname}.njk" %}`, {});
|
|
102
|
+
// 1.5) Inline partials/includes so the server doesn't need a loader
|
|
103
|
+
const mergedWithPartials = cfg.inline_includes
|
|
104
|
+
? inlineIncludes(mergedTemplate, node_path_1.default.dirname(templateFile), srcRoot, new Set())
|
|
105
|
+
: mergedTemplate;
|
|
106
|
+
// 2) Protect variables/flow
|
|
107
|
+
const protectedTemplate = cfg.env.filters.protect_variables(mergedWithPartials);
|
|
108
|
+
// 3) Light HTML transforms for email compatibility
|
|
109
|
+
console.log('Processing HTML for email compatibility');
|
|
110
|
+
let processedHtml = protectedTemplate;
|
|
111
|
+
try {
|
|
112
|
+
const $ = (0, cheerio_1.load)(protectedTemplate, {
|
|
113
|
+
xmlMode: false
|
|
114
|
+
// decodeEntities: false
|
|
115
|
+
});
|
|
116
|
+
// <container> -> <table>
|
|
117
|
+
$('container').each(function () {
|
|
118
|
+
const $container = $(this);
|
|
119
|
+
const $table = $('<table/>').attr({
|
|
120
|
+
align: 'center',
|
|
121
|
+
class: $container.attr('class') || '',
|
|
122
|
+
width: '100%',
|
|
123
|
+
cellpadding: '0',
|
|
124
|
+
cellspacing: '0',
|
|
125
|
+
border: '0'
|
|
126
|
+
});
|
|
127
|
+
const $tbody = $('<tbody/>');
|
|
128
|
+
$table.append($tbody);
|
|
129
|
+
$tbody.append($container.contents());
|
|
130
|
+
$container.replaceWith($table);
|
|
131
|
+
});
|
|
132
|
+
// <row> -> <tr>
|
|
133
|
+
$('row').each(function () {
|
|
134
|
+
const $row = $(this);
|
|
135
|
+
const background = $row.attr('background') || '';
|
|
136
|
+
const $tr = $('<tr/>').attr({ class: $row.attr('class') || '' });
|
|
137
|
+
if (background)
|
|
138
|
+
$tr.css('background', background);
|
|
139
|
+
$tr.append($row.contents());
|
|
140
|
+
$row.replaceWith($tr);
|
|
141
|
+
});
|
|
142
|
+
// <columns> -> <td>
|
|
143
|
+
$('columns').each(function () {
|
|
144
|
+
const $columns = $(this);
|
|
145
|
+
const padding = $columns.attr('padding') || '0';
|
|
146
|
+
const $td = $('<td/>').attr({
|
|
147
|
+
class: $columns.attr('class') || '',
|
|
148
|
+
style: `padding: ${padding};`
|
|
149
|
+
});
|
|
150
|
+
$td.append($columns.contents());
|
|
151
|
+
$columns.replaceWith($td);
|
|
152
|
+
});
|
|
153
|
+
// <button> -> <a>
|
|
154
|
+
$('button').each(function () {
|
|
155
|
+
const $button = $(this);
|
|
156
|
+
const href = $button.attr('href') || '#';
|
|
157
|
+
const buttonClass = $button.attr('class') || '';
|
|
158
|
+
const $a = $('<a/>').attr({
|
|
159
|
+
href,
|
|
160
|
+
class: buttonClass,
|
|
161
|
+
style: $button.attr('style') ||
|
|
162
|
+
'display: inline-block; padding: 8px 16px; border-radius: 3px; text-decoration: none;'
|
|
163
|
+
});
|
|
164
|
+
$a.append($button.contents());
|
|
165
|
+
$button.replaceWith($a);
|
|
166
|
+
});
|
|
167
|
+
processedHtml = $.html();
|
|
168
|
+
console.log('HTML processing complete');
|
|
169
|
+
}
|
|
170
|
+
catch (htmlError) {
|
|
171
|
+
console.error('HTML processing error:', htmlError);
|
|
172
|
+
processedHtml = protectedTemplate;
|
|
173
|
+
}
|
|
174
|
+
// 4) Inline CSS
|
|
175
|
+
let inlinedHtml;
|
|
176
|
+
try {
|
|
177
|
+
inlinedHtml = (0, juice_1.default)(processedHtml, {
|
|
178
|
+
extraCss: cfg.css_content ?? undefined,
|
|
179
|
+
removeStyleTags: false,
|
|
180
|
+
preserveMediaQueries: true,
|
|
181
|
+
preserveFontFaces: true
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (juiceError) {
|
|
185
|
+
console.error('CSS inlining error:', juiceError);
|
|
186
|
+
inlinedHtml = processedHtml;
|
|
187
|
+
}
|
|
188
|
+
// 5) Restore variables/flow
|
|
189
|
+
const finalHtml = cfg.env.filters.restore_variables(inlinedHtml);
|
|
190
|
+
// Write
|
|
191
|
+
if (writeOutput) {
|
|
192
|
+
const distRoot = resolvePathRoot(cfg.dist_dir);
|
|
193
|
+
const outputPath = node_path_1.default.join(distRoot, `${tplname}.njk`);
|
|
194
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(outputPath), { recursive: true });
|
|
195
|
+
node_fs_1.default.writeFileSync(outputPath, finalHtml);
|
|
196
|
+
}
|
|
197
|
+
if (writeOutput) {
|
|
198
|
+
console.log(`Created ${tplname}.njk`);
|
|
199
|
+
}
|
|
200
|
+
return finalHtml;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
console.error(`Error processing ${tplname}:`, error);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function get_all_files(dir, filelist = []) {
|
|
208
|
+
const files = node_fs_1.default.readdirSync(dir);
|
|
209
|
+
files.forEach((file) => {
|
|
210
|
+
const file_path = node_path_1.default.join(dir, file);
|
|
211
|
+
if (node_fs_1.default.statSync(file_path).isDirectory()) {
|
|
212
|
+
get_all_files(file_path, filelist);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
filelist.push(file_path);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return filelist;
|
|
219
|
+
}
|
|
220
|
+
function find_templates() {
|
|
221
|
+
const srcRoot = resolvePathRoot(cfg.src_dir);
|
|
222
|
+
const all = get_all_files(srcRoot);
|
|
223
|
+
return all
|
|
224
|
+
.filter((file) => file.endsWith('.njk'))
|
|
225
|
+
.filter((file) => {
|
|
226
|
+
const basename = node_path_1.default.basename(file);
|
|
227
|
+
const content = node_fs_1.default.readFileSync(file, 'utf8');
|
|
228
|
+
return (!basename.startsWith('_') &&
|
|
229
|
+
!basename.includes('layout') &&
|
|
230
|
+
!basename.includes('part') &&
|
|
231
|
+
content.includes('{% extends'));
|
|
232
|
+
})
|
|
233
|
+
.map((file) => {
|
|
234
|
+
const name = node_path_1.default.relative(srcRoot, file);
|
|
235
|
+
return name.substring(0, name.length - 4);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async function process_all_templates() {
|
|
239
|
+
const distRoot = resolvePathRoot(cfg.dist_dir);
|
|
240
|
+
if (!node_fs_1.default.existsSync(distRoot)) {
|
|
241
|
+
node_fs_1.default.mkdirSync(distRoot, { recursive: true });
|
|
242
|
+
}
|
|
243
|
+
const templates = find_templates();
|
|
244
|
+
console.log(`Found ${templates.length} templates to process: ${templates.join(', ')}`);
|
|
245
|
+
for (const template of templates) {
|
|
246
|
+
try {
|
|
247
|
+
process_template(template);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
console.error(`Failed to process ${template}:`, error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
console.log('All templates processed!');
|
|
254
|
+
}
|
|
255
|
+
function init_env() {
|
|
256
|
+
const loader = new nunjucks_1.default.FileSystemLoader(resolvePathRoot(cfg.src_dir));
|
|
257
|
+
cfg.env = new nunjucks_1.default.Environment(loader, { autoescape: false });
|
|
258
|
+
if (!cfg.env)
|
|
259
|
+
throw Error('Unable to init nunjucks environment');
|
|
260
|
+
// Load CSS if present
|
|
261
|
+
const cssPath = resolveCssPath(cfg.css_path);
|
|
262
|
+
if (cssPath && node_fs_1.default.existsSync(cssPath)) {
|
|
263
|
+
cfg.css_content = node_fs_1.default.readFileSync(cssPath, 'utf8');
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
cfg.css_content = null;
|
|
267
|
+
}
|
|
268
|
+
// Extension
|
|
269
|
+
cfg.env.addExtension('PreprocessExtension', new PreprocessExtension());
|
|
270
|
+
// Filters
|
|
271
|
+
cfg.env.addFilter('protect_variables', function (content) {
|
|
272
|
+
return content
|
|
273
|
+
.replace(/(\{\{[\s\S]*?\}\})/g, (m) => `<!--VAR:${Buffer.from(m).toString('base64')}-->`)
|
|
274
|
+
.replace(/(\{%(?!\s*block|\s*endblock|\s*extends)[\s\S]*?%\})/g, (m) => {
|
|
275
|
+
return `<!--FLOW:${Buffer.from(m).toString('base64')}-->`;
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
cfg.env.addFilter('restore_variables', function (content) {
|
|
279
|
+
return content
|
|
280
|
+
.replace(/<!--VAR:(.*?)-->/g, (_m, enc) => Buffer.from(enc, 'base64').toString('utf8'))
|
|
281
|
+
.replace(/<!--FLOW:(.*?)-->/g, (_m, enc) => Buffer.from(enc, 'base64').toString('utf8'));
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async function do_the_template_thing(options = {}) {
|
|
285
|
+
if (options.src_dir)
|
|
286
|
+
cfg.src_dir = options.src_dir;
|
|
287
|
+
if (options.dist_dir)
|
|
288
|
+
cfg.dist_dir = options.dist_dir;
|
|
289
|
+
if (options.css_path)
|
|
290
|
+
cfg.css_path = options.css_path;
|
|
291
|
+
if (options.inline_includes !== undefined)
|
|
292
|
+
cfg.inline_includes = options.inline_includes;
|
|
293
|
+
init_env();
|
|
294
|
+
if (options.tplname) {
|
|
295
|
+
process_template(options.tplname);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
await process_all_templates();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function compileTemplate(options) {
|
|
302
|
+
if (options.src_dir)
|
|
303
|
+
cfg.src_dir = options.src_dir;
|
|
304
|
+
if (options.css_path)
|
|
305
|
+
cfg.css_path = options.css_path;
|
|
306
|
+
if (options.inline_includes !== undefined)
|
|
307
|
+
cfg.inline_includes = options.inline_includes;
|
|
308
|
+
init_env();
|
|
309
|
+
return process_template(options.tplname, false);
|
|
310
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@technomoron/mail-magic-client",
|
|
3
|
+
"version": "1.0.23",
|
|
4
|
+
"description": "Client library for mail-magic",
|
|
5
|
+
"main": "dist/cjs/mail-magic-client.js",
|
|
6
|
+
"types": "dist/cjs/mail-magic-client.d.ts",
|
|
7
|
+
"module": "dist/esm/mail-magic-client.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mm-cli": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"prepare": "npm run build",
|
|
13
|
+
"scrub": "rm -rf node_modules pnpm-lock.yaml lib/ dist/",
|
|
14
|
+
"build:cjs": "tsc --project tsconfig/tsconfig.cjs.json",
|
|
15
|
+
"build:esm": "tsc --project tsconfig/tsconfig.esm.json",
|
|
16
|
+
"build:cli": "tsc --project tsconfig/tsconfig.cli.json",
|
|
17
|
+
"build": "npm run build:cjs && npm run build:esm && npm run build:cli",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"lint": "eslint --ext .js,.ts,.vue ./src",
|
|
21
|
+
"lintfix": "eslint --fix --ext .js,.ts,.vue,.json ./src",
|
|
22
|
+
"format": "npm run lintfix && npm run pretty",
|
|
23
|
+
"pretty": "prettier --write \"**/*.{js,ts,vue,json,css,scss,md}\"",
|
|
24
|
+
"cleanbuild": "rm -rf ./dist/ && npm run lintfix && npm run format && npm run build"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/technomoron/mail-magic.git",
|
|
29
|
+
"directory": "packages/mail-magic-client"
|
|
30
|
+
},
|
|
31
|
+
"author": "Bjørn Erik Jacobsen",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist/**/*.js",
|
|
35
|
+
"dist/**/*.d.ts",
|
|
36
|
+
"package.json",
|
|
37
|
+
"CHANGES"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"cheerio": "^1.1.2",
|
|
41
|
+
"commander": "^10.0.1",
|
|
42
|
+
"email-addresses": "^5.0.0",
|
|
43
|
+
"foundation-emails": "^2.4.0",
|
|
44
|
+
"inky": "^0.1.0",
|
|
45
|
+
"juice": "^11.0.1",
|
|
46
|
+
"node-fetch": "^3.3.2",
|
|
47
|
+
"nunjucks": "^3.2.4"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/cheerio": "^1.0.0",
|
|
51
|
+
"@types/node": "^20.19.11",
|
|
52
|
+
"@types/nunjucks": "^3.2.6",
|
|
53
|
+
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
|
54
|
+
"@typescript-eslint/parser": "^8.30.1",
|
|
55
|
+
"@vue/eslint-config-prettier": "^10.2.0",
|
|
56
|
+
"@vue/eslint-config-typescript": "^14.5.0",
|
|
57
|
+
"eslint": "^9.34.0",
|
|
58
|
+
"eslint-config-prettier": "^10.1.5",
|
|
59
|
+
"eslint-import-resolver-alias": "^1.1.2",
|
|
60
|
+
"eslint-plugin-import": "^2.31.0",
|
|
61
|
+
"eslint-plugin-nuxt": "^4.0.0",
|
|
62
|
+
"eslint-plugin-prettier": "^5.4.1",
|
|
63
|
+
"eslint-plugin-vue": "^10.0.0",
|
|
64
|
+
"prettier": "^3.5.3",
|
|
65
|
+
"typescript": "^5.9.2",
|
|
66
|
+
"vitest": "^4.0.16",
|
|
67
|
+
"vue-eslint-parser": "^10.1.3"
|
|
68
|
+
}
|
|
69
|
+
}
|