@technomoron/mail-magic-client 1.0.30 → 1.0.32
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 +22 -0
- package/README.md +2 -65
- package/dist/cjs/mail-magic-client.d.ts +24 -18
- package/dist/cjs/mail-magic-client.js +6 -5
- package/dist/esm/mail-magic-client.js +6 -5
- package/dist/mail-magic-client.js +322 -325
- package/package.json +46 -51
- package/dist/cli-env.js +0 -55
- package/dist/cli-helpers.js +0 -405
- package/dist/cli-version.js +0 -33
- package/dist/cli.js +0 -325
- package/dist/preprocess.js +0 -308
package/dist/cli.js
DELETED
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
-
};
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const readline_1 = __importDefault(require("readline"));
|
|
9
|
-
const commander_1 = require("commander");
|
|
10
|
-
const cli_env_1 = require("./cli-env");
|
|
11
|
-
const cli_helpers_1 = require("./cli-helpers");
|
|
12
|
-
const cli_version_1 = require("./cli-version");
|
|
13
|
-
const mail_magic_client_1 = __importDefault(require("./mail-magic-client"));
|
|
14
|
-
const preprocess_1 = require("./preprocess");
|
|
15
|
-
const program = new commander_1.Command();
|
|
16
|
-
const envDefaults = (0, cli_env_1.loadCliEnv)();
|
|
17
|
-
const defaultToken = (0, cli_env_1.resolveToken)(envDefaults);
|
|
18
|
-
const apiDefault = envDefaults.api || 'http://localhost:3000';
|
|
19
|
-
program.option('-a, --api <api>', 'Base API endpoint', apiDefault);
|
|
20
|
-
if (defaultToken) {
|
|
21
|
-
program.option('-t, --token <token>', 'Authentication token in the format "username:token"', defaultToken);
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
program.option('-t, --token <token>', 'Authentication token in the format "username:token"');
|
|
25
|
-
}
|
|
26
|
-
program
|
|
27
|
-
.option('-f, --file <file>', 'Path to the file containing the template data (Nunjucks with MJML)')
|
|
28
|
-
.option('-s, --sender <sender>', 'Sender email address')
|
|
29
|
-
.option('-r, --rcpt <rcpt>', 'Recipient email addresses (comma-separated)')
|
|
30
|
-
.option('-n, --name <name>', 'Template name')
|
|
31
|
-
.option('-b, --subject <subject>', 'Email subject')
|
|
32
|
-
.option('-l, --locale <locale>', 'Locale')
|
|
33
|
-
.option('-d, --domain <domain>', 'Domain', envDefaults.domain)
|
|
34
|
-
.option('-p, --part <true|false>', 'Part')
|
|
35
|
-
.option('-v, --vars <vars>', 'Template parameters (JSON string)');
|
|
36
|
-
const readStdin = async () => {
|
|
37
|
-
if (process.stdin.isTTY) {
|
|
38
|
-
return '';
|
|
39
|
-
}
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
const rl = readline_1.default.createInterface({
|
|
42
|
-
input: process.stdin,
|
|
43
|
-
output: process.stdout,
|
|
44
|
-
terminal: false
|
|
45
|
-
});
|
|
46
|
-
let data = '';
|
|
47
|
-
rl.on('line', (line) => {
|
|
48
|
-
data += line + '\n';
|
|
49
|
-
});
|
|
50
|
-
rl.on('close', () => {
|
|
51
|
-
resolve(data.trim());
|
|
52
|
-
});
|
|
53
|
-
rl.on('error', (err) => {
|
|
54
|
-
reject(err);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
};
|
|
58
|
-
const getTemplateData = async () => {
|
|
59
|
-
if (program.opts().file) {
|
|
60
|
-
const filePath = program.opts().file;
|
|
61
|
-
if (!fs_1.default.existsSync(filePath)) {
|
|
62
|
-
throw new Error(`File not found: ${filePath}`);
|
|
63
|
-
}
|
|
64
|
-
return fs_1.default.readFileSync(filePath, 'utf-8');
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
return await readStdin();
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
71
|
-
const parseVarsOption = (input) => {
|
|
72
|
-
if (!input) {
|
|
73
|
-
return {};
|
|
74
|
-
}
|
|
75
|
-
let parsed;
|
|
76
|
-
try {
|
|
77
|
-
parsed = JSON.parse(input);
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
-
throw new Error(`Invalid JSON for --vars: ${message}`);
|
|
82
|
-
}
|
|
83
|
-
if (!isPlainObject(parsed)) {
|
|
84
|
-
throw new Error('--vars must be a JSON object');
|
|
85
|
-
}
|
|
86
|
-
return parsed;
|
|
87
|
-
};
|
|
88
|
-
program
|
|
89
|
-
.command('template')
|
|
90
|
-
.description('Store a template on the server')
|
|
91
|
-
.action(async () => {
|
|
92
|
-
const client = new mail_magic_client_1.default(program.opts().api, program.opts().token);
|
|
93
|
-
try {
|
|
94
|
-
const template = await getTemplateData();
|
|
95
|
-
const templateData = {
|
|
96
|
-
template,
|
|
97
|
-
sender: program.opts().sender,
|
|
98
|
-
name: program.opts().name,
|
|
99
|
-
subject: program.opts().subject,
|
|
100
|
-
locale: program.opts().locale,
|
|
101
|
-
domain: program.opts().domain,
|
|
102
|
-
part: !!program.opts().part
|
|
103
|
-
};
|
|
104
|
-
await client.storeTemplate(templateData);
|
|
105
|
-
console.log('Template updated');
|
|
106
|
-
}
|
|
107
|
-
catch (error) {
|
|
108
|
-
if (error instanceof Error) {
|
|
109
|
-
console.error('Error:', error.message);
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
console.error('An unknown error occurred.');
|
|
113
|
-
}
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
program
|
|
118
|
-
.command('send')
|
|
119
|
-
.description('Send a template to recipients')
|
|
120
|
-
.action(async () => {
|
|
121
|
-
const client = new mail_magic_client_1.default(program.opts().api, program.opts().token);
|
|
122
|
-
try {
|
|
123
|
-
const vars = parseVarsOption(program.opts().vars);
|
|
124
|
-
const templateData = {
|
|
125
|
-
name: program.opts().name,
|
|
126
|
-
rcpt: program.opts().rcpt,
|
|
127
|
-
domain: program.opts().domain,
|
|
128
|
-
locale: program.opts().locale,
|
|
129
|
-
vars
|
|
130
|
-
};
|
|
131
|
-
await client.sendTemplate(templateData);
|
|
132
|
-
console.log('Template sent');
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
if (error instanceof Error) {
|
|
136
|
-
console.error('Error:', error.message);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
console.error('An unknown error occurred.');
|
|
140
|
-
}
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
program
|
|
145
|
-
.command('version')
|
|
146
|
-
.description('Show current client version')
|
|
147
|
-
.action(async () => {
|
|
148
|
-
console.log((0, cli_version_1.resolvePackageVersion)());
|
|
149
|
-
});
|
|
150
|
-
program
|
|
151
|
-
.command('compile')
|
|
152
|
-
.description('Compile templates by resolving inheritance and processing with FFE')
|
|
153
|
-
.option('-i, --input <input>', 'Input directory', './templates')
|
|
154
|
-
.option('-o, --output <output>', 'Output directory', './templates-dist')
|
|
155
|
-
.option('-c, --css <css>', 'Path to Foundation for Emails CSS', './templates/foundation-emails.css')
|
|
156
|
-
.option('-t, --template <template>', 'Process a specific template only')
|
|
157
|
-
.action(async (cmdOptions) => {
|
|
158
|
-
try {
|
|
159
|
-
await (0, preprocess_1.do_the_template_thing)({
|
|
160
|
-
src_dir: cmdOptions.input,
|
|
161
|
-
dist_dir: cmdOptions.output,
|
|
162
|
-
css_path: cmdOptions.css,
|
|
163
|
-
tplname: cmdOptions.template // Pass undefined if not specified
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
catch (error) {
|
|
167
|
-
if (error instanceof Error) {
|
|
168
|
-
console.error('Error:', error.message);
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
console.error('An unknown error occurred.');
|
|
172
|
-
}
|
|
173
|
-
process.exit(1);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
program
|
|
177
|
-
.command('push')
|
|
178
|
-
.description('Compile a template with partials and store it on the server')
|
|
179
|
-
.option('-i, --input <input>', 'Input directory', './templates')
|
|
180
|
-
.option('-c, --css <css>', 'Path to Foundation for Emails CSS', './templates/foundation-emails.css')
|
|
181
|
-
.option('-t, --template <template>', 'Template path relative to input (without .njk)')
|
|
182
|
-
.option('-n, --name <name>', 'Template name (defaults to template basename)')
|
|
183
|
-
.option('-s, --sender <sender>', 'Sender email address')
|
|
184
|
-
.option('-b, --subject <subject>', 'Email subject')
|
|
185
|
-
.option('-l, --locale <locale>', 'Locale')
|
|
186
|
-
.option('-d, --domain <domain>', 'Domain')
|
|
187
|
-
.option('--dry-run', 'Show what would be uploaded without sending anything')
|
|
188
|
-
.action(async (cmdOptions) => {
|
|
189
|
-
try {
|
|
190
|
-
const summary = await (0, cli_helpers_1.pushTemplate)({
|
|
191
|
-
api: program.opts().api,
|
|
192
|
-
token: program.opts().token,
|
|
193
|
-
domain: cmdOptions.domain,
|
|
194
|
-
template: cmdOptions.template,
|
|
195
|
-
name: cmdOptions.name,
|
|
196
|
-
locale: cmdOptions.locale,
|
|
197
|
-
sender: cmdOptions.sender,
|
|
198
|
-
subject: cmdOptions.subject,
|
|
199
|
-
input: cmdOptions.input,
|
|
200
|
-
css: cmdOptions.css,
|
|
201
|
-
dryRun: !!cmdOptions.dryRun
|
|
202
|
-
});
|
|
203
|
-
if (cmdOptions.dryRun) {
|
|
204
|
-
console.log(`Dry run - template: ${summary.domain} ${summary.locale || ''} ${summary.name}`);
|
|
205
|
-
console.log(`Source: ${summary.filePath}`);
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
console.log('Template compiled and uploaded');
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
if (error instanceof Error) {
|
|
213
|
-
console.error('Error:', error.message);
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
console.error('An unknown error occurred.');
|
|
217
|
-
}
|
|
218
|
-
process.exit(1);
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
program
|
|
222
|
-
.command('push-dir')
|
|
223
|
-
.description('Upload templates and assets from a config-style directory')
|
|
224
|
-
.option('-i, --input <input>', 'Config directory (contains init-data.json)', './data')
|
|
225
|
-
.option('-c, --css <css>', 'Path to Foundation for Emails CSS (optional)')
|
|
226
|
-
.option('--dry-run', 'Show what would be uploaded without sending anything')
|
|
227
|
-
.option('--skip-assets', 'Skip asset uploads')
|
|
228
|
-
.option('--skip-tx', 'Skip transactional templates')
|
|
229
|
-
.option('--skip-forms', 'Skip form templates')
|
|
230
|
-
.option('-d, --domain <domain>', 'Domain to upload (overrides global)')
|
|
231
|
-
.action(async (cmdOptions) => {
|
|
232
|
-
try {
|
|
233
|
-
const summary = await (0, cli_helpers_1.pushTemplateDir)({
|
|
234
|
-
api: program.opts().api,
|
|
235
|
-
token: program.opts().token,
|
|
236
|
-
input: cmdOptions.input,
|
|
237
|
-
domain: cmdOptions.domain || program.opts().domain,
|
|
238
|
-
css: cmdOptions.css,
|
|
239
|
-
includeAssets: !cmdOptions.skipAssets,
|
|
240
|
-
includeTx: !cmdOptions.skipTx,
|
|
241
|
-
includeForms: !cmdOptions.skipForms,
|
|
242
|
-
dryRun: !!cmdOptions.dryRun
|
|
243
|
-
});
|
|
244
|
-
if (cmdOptions.dryRun) {
|
|
245
|
-
console.log('Dry run - planned uploads:');
|
|
246
|
-
for (const action of summary.actions) {
|
|
247
|
-
if (action.kind === 'tx-template') {
|
|
248
|
-
console.log(`tx-template: ${action.domain} ${action.locale || ''} ${action.template}`);
|
|
249
|
-
}
|
|
250
|
-
else if (action.kind === 'form-template') {
|
|
251
|
-
console.log(`form-template: ${action.domain} ${action.locale || ''} ${action.template}`);
|
|
252
|
-
}
|
|
253
|
-
else if (action.kind === 'domain-assets') {
|
|
254
|
-
const files = action.files?.join(', ') || '';
|
|
255
|
-
console.log(`domain-assets: ${action.domain} ${action.path || '.'} ${files}`);
|
|
256
|
-
}
|
|
257
|
-
else if (action.kind === 'template-assets') {
|
|
258
|
-
const files = action.files?.join(', ') || '';
|
|
259
|
-
console.log(`template-assets: ${action.domain} ${action.locale || ''} ${action.template} ${action.path || '.'} ${files}`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
console.log(`Summary: ${summary.templates} tx template(s), ${summary.forms} form template(s), ${summary.assetBatches} asset batch(es)`);
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
console.log('Templates and assets uploaded');
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
catch (error) {
|
|
269
|
-
if (error instanceof Error) {
|
|
270
|
-
console.error('Error:', error.message);
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
console.error('An unknown error occurred.');
|
|
274
|
-
}
|
|
275
|
-
process.exit(1);
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
program
|
|
279
|
-
.command('assets')
|
|
280
|
-
.description('Upload asset files to the server')
|
|
281
|
-
.option('-f, --file <file...>', 'Asset file path(s)')
|
|
282
|
-
.option('--template-type <type>', 'Template type (tx or form)')
|
|
283
|
-
.option('--template <template>', 'Template name/idname')
|
|
284
|
-
.option('--path <path>', 'Destination subdirectory under assets or template')
|
|
285
|
-
.option('-l, --locale <locale>', 'Locale')
|
|
286
|
-
.option('--dry-run', 'Show what would be uploaded without sending anything')
|
|
287
|
-
.action(async (cmdOptions) => {
|
|
288
|
-
const client = new mail_magic_client_1.default(program.opts().api, program.opts().token);
|
|
289
|
-
try {
|
|
290
|
-
const files = cmdOptions.file;
|
|
291
|
-
if (!files || files.length === 0) {
|
|
292
|
-
throw new Error('At least one --file is required');
|
|
293
|
-
}
|
|
294
|
-
if (cmdOptions.dryRun) {
|
|
295
|
-
for (const file of files) {
|
|
296
|
-
if (!fs_1.default.existsSync(file)) {
|
|
297
|
-
throw new Error(`File not found: ${file}`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
console.log('Dry run - assets:');
|
|
301
|
-
console.log(`domain=${program.opts().domain} templateType=${cmdOptions.templateType || ''} template=${cmdOptions.template || ''} locale=${cmdOptions.locale || ''} path=${cmdOptions.path || ''}`);
|
|
302
|
-
console.log(`files=${files.join(', ')}`);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
await client.uploadAssets({
|
|
306
|
-
domain: program.opts().domain,
|
|
307
|
-
files,
|
|
308
|
-
templateType: cmdOptions.templateType,
|
|
309
|
-
template: cmdOptions.template,
|
|
310
|
-
locale: cmdOptions.locale,
|
|
311
|
-
path: cmdOptions.path
|
|
312
|
-
});
|
|
313
|
-
console.log('Assets uploaded');
|
|
314
|
-
}
|
|
315
|
-
catch (error) {
|
|
316
|
-
if (error instanceof Error) {
|
|
317
|
-
console.error('Error:', error.message);
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
console.error('An unknown error occurred.');
|
|
321
|
-
}
|
|
322
|
-
process.exit(1);
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
program.parse(process.argv);
|
package/dist/preprocess.js
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
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
|
-
function createCompileCfg(options) {
|
|
18
|
-
return {
|
|
19
|
-
env: null,
|
|
20
|
-
src_dir: options.src_dir ?? 'templates',
|
|
21
|
-
dist_dir: options.dist_dir ?? 'templates-dist',
|
|
22
|
-
css_path: options.css_path ?? node_path_1.default.join(process.cwd(), 'templates', 'foundation-emails.css'),
|
|
23
|
-
css_content: null,
|
|
24
|
-
inline_includes: options.inline_includes ?? true
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
function resolvePathRoot(dir) {
|
|
28
|
-
return node_path_1.default.isAbsolute(dir) ? dir : node_path_1.default.join(process.cwd(), dir);
|
|
29
|
-
}
|
|
30
|
-
function resolveCssPath(cssPath) {
|
|
31
|
-
if (!cssPath) {
|
|
32
|
-
return '';
|
|
33
|
-
}
|
|
34
|
-
return node_path_1.default.isAbsolute(cssPath) ? cssPath : node_path_1.default.join(process.cwd(), cssPath);
|
|
35
|
-
}
|
|
36
|
-
function inlineIncludes(content, baseDir, srcRoot, normalizedSrcRoot, stack) {
|
|
37
|
-
const includeExp = /\{%\s*include\s+['"]([^'"]+)['"][^%]*%\}/g;
|
|
38
|
-
return content.replace(includeExp, (_match, includePath) => {
|
|
39
|
-
const cleaned = includePath.replace(/^\/+/, '');
|
|
40
|
-
const candidates = [node_path_1.default.resolve(baseDir, cleaned), node_path_1.default.resolve(srcRoot, cleaned)];
|
|
41
|
-
const found = candidates.find((candidate) => node_fs_1.default.existsSync(candidate));
|
|
42
|
-
if (!found) {
|
|
43
|
-
throw new Error(`Include not found: ${includePath}`);
|
|
44
|
-
}
|
|
45
|
-
const resolved = node_fs_1.default.realpathSync(found);
|
|
46
|
-
if (!resolved.startsWith(normalizedSrcRoot)) {
|
|
47
|
-
throw new Error(`Include path escapes template root: ${includePath}`);
|
|
48
|
-
}
|
|
49
|
-
if (!node_fs_1.default.statSync(resolved).isFile()) {
|
|
50
|
-
throw new Error(`Include is not a file: ${includePath}`);
|
|
51
|
-
}
|
|
52
|
-
if (stack.has(resolved)) {
|
|
53
|
-
throw new Error(`Circular include detected for ${includePath}`);
|
|
54
|
-
}
|
|
55
|
-
stack.add(resolved);
|
|
56
|
-
const raw = node_fs_1.default.readFileSync(resolved, 'utf8');
|
|
57
|
-
const inlined = inlineIncludes(raw, node_path_1.default.dirname(resolved), srcRoot, normalizedSrcRoot, stack);
|
|
58
|
-
stack.delete(resolved);
|
|
59
|
-
return inlined;
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
class PreprocessExtension {
|
|
63
|
-
constructor(cfg) {
|
|
64
|
-
this.tags = ['process_layout'];
|
|
65
|
-
this.cfg = cfg;
|
|
66
|
-
}
|
|
67
|
-
parse(parser, nodes) {
|
|
68
|
-
const token = parser.nextToken();
|
|
69
|
-
const args = parser.parseSignature(null, true);
|
|
70
|
-
parser.advanceAfterBlockEnd(token.value);
|
|
71
|
-
return new nodes.CallExtension(this, 'run', args);
|
|
72
|
-
}
|
|
73
|
-
run(_context, tplname) {
|
|
74
|
-
const template = this.cfg.env.getTemplate(tplname);
|
|
75
|
-
const src = template.tmplStr;
|
|
76
|
-
const extmatch = src.match(/\{%\s*extends\s+['"]([^'"]+)['"]\s*%\}/);
|
|
77
|
-
if (!extmatch)
|
|
78
|
-
return src;
|
|
79
|
-
const layoutName = extmatch[1];
|
|
80
|
-
const layoutTemplate = this.cfg.env.getTemplate(layoutName);
|
|
81
|
-
const layoutSrc = layoutTemplate.tmplStr;
|
|
82
|
-
const blocks = {};
|
|
83
|
-
const blockexp = /\{%\s*block\s+([a-zA-Z0-9_]+)\s*%\}([\s\S]*?)\{%\s*endblock\s*%\}/g;
|
|
84
|
-
let match;
|
|
85
|
-
while ((match = blockexp.exec(src)) !== null) {
|
|
86
|
-
const bname = match[1];
|
|
87
|
-
const bcontent = match[2];
|
|
88
|
-
blocks[bname] = bcontent.trim();
|
|
89
|
-
}
|
|
90
|
-
let merged = layoutSrc;
|
|
91
|
-
for (const [bname, bcontent] of Object.entries(blocks)) {
|
|
92
|
-
const lbexpt = new RegExp(`\\{%\\s*block\\s+${bname}\\s*%\\}[\\s\\S]*?\\{%\\s*endblock\\s*%\\}`, 'g');
|
|
93
|
-
merged = merged.replace(lbexpt, bcontent);
|
|
94
|
-
}
|
|
95
|
-
merged = merged.replace(/\{%\s*extends\s+['"][^'"]+['"]\s*%\}/, '');
|
|
96
|
-
if (merged.match(/\{%\s*extends\s+['"]([^'"]+)['"]\s*%\}/)) {
|
|
97
|
-
return this.run(_context, layoutName);
|
|
98
|
-
}
|
|
99
|
-
merged = merged.replace(/\{%\s*block\s+([a-zA-Z0-9_]+)\s*%\}\s*\{%\s*endblock\s*%\}/g, '');
|
|
100
|
-
return merged;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function process_template(cfg, tplname, writeOutput = true) {
|
|
104
|
-
console.log(`Processing template: ${tplname}`);
|
|
105
|
-
try {
|
|
106
|
-
const srcRoot = resolvePathRoot(cfg.src_dir);
|
|
107
|
-
const resolvedSrcRoot = node_fs_1.default.realpathSync(srcRoot);
|
|
108
|
-
const normalizedSrcRoot = resolvedSrcRoot.endsWith(node_path_1.default.sep) ? resolvedSrcRoot : resolvedSrcRoot + node_path_1.default.sep;
|
|
109
|
-
const templateFile = node_path_1.default.join(srcRoot, `${tplname}.njk`);
|
|
110
|
-
// 1) Resolve template inheritance
|
|
111
|
-
const mergedTemplate = cfg.env.renderString(`{% process_layout "${tplname}.njk" %}`, {});
|
|
112
|
-
// 1.5) Inline partials/includes so the server doesn't need a loader
|
|
113
|
-
const mergedWithPartials = cfg.inline_includes
|
|
114
|
-
? inlineIncludes(mergedTemplate, node_path_1.default.dirname(templateFile), srcRoot, normalizedSrcRoot, new Set())
|
|
115
|
-
: mergedTemplate;
|
|
116
|
-
// 2) Protect variables/flow
|
|
117
|
-
const protectedTemplate = cfg.env.filters.protect_variables(mergedWithPartials);
|
|
118
|
-
// 3) Light HTML transforms for email compatibility
|
|
119
|
-
console.log('Processing HTML for email compatibility');
|
|
120
|
-
let processedHtml = protectedTemplate;
|
|
121
|
-
try {
|
|
122
|
-
const $ = (0, cheerio_1.load)(protectedTemplate, {
|
|
123
|
-
xmlMode: false
|
|
124
|
-
// decodeEntities: false
|
|
125
|
-
});
|
|
126
|
-
// <container> -> <table>
|
|
127
|
-
$('container').each((_index, element) => {
|
|
128
|
-
const $container = $(element);
|
|
129
|
-
const $table = $('<table/>').attr({
|
|
130
|
-
align: 'center',
|
|
131
|
-
class: $container.attr('class') || '',
|
|
132
|
-
width: '100%',
|
|
133
|
-
cellpadding: '0',
|
|
134
|
-
cellspacing: '0',
|
|
135
|
-
border: '0'
|
|
136
|
-
});
|
|
137
|
-
const $tbody = $('<tbody/>');
|
|
138
|
-
$table.append($tbody);
|
|
139
|
-
$tbody.append($container.contents());
|
|
140
|
-
$container.replaceWith($table);
|
|
141
|
-
});
|
|
142
|
-
// <row> -> <tr>
|
|
143
|
-
$('row').each((_index, element) => {
|
|
144
|
-
const $row = $(element);
|
|
145
|
-
const background = $row.attr('background') || '';
|
|
146
|
-
const $tr = $('<tr/>').attr({ class: $row.attr('class') || '' });
|
|
147
|
-
if (background)
|
|
148
|
-
$tr.css('background', background);
|
|
149
|
-
$tr.append($row.contents());
|
|
150
|
-
$row.replaceWith($tr);
|
|
151
|
-
});
|
|
152
|
-
// <columns> -> <td>
|
|
153
|
-
$('columns').each((_index, element) => {
|
|
154
|
-
const $columns = $(element);
|
|
155
|
-
const padding = $columns.attr('padding') || '0';
|
|
156
|
-
const $td = $('<td/>').attr({
|
|
157
|
-
class: $columns.attr('class') || '',
|
|
158
|
-
style: `padding: ${padding};`
|
|
159
|
-
});
|
|
160
|
-
$td.append($columns.contents());
|
|
161
|
-
$columns.replaceWith($td);
|
|
162
|
-
});
|
|
163
|
-
// <button> -> <a>
|
|
164
|
-
$('button').each((_index, element) => {
|
|
165
|
-
const $button = $(element);
|
|
166
|
-
const href = $button.attr('href') || '#';
|
|
167
|
-
const buttonClass = $button.attr('class') || '';
|
|
168
|
-
const $a = $('<a/>').attr({
|
|
169
|
-
href,
|
|
170
|
-
class: buttonClass,
|
|
171
|
-
style: $button.attr('style') ||
|
|
172
|
-
'display: inline-block; padding: 8px 16px; border-radius: 3px; text-decoration: none;'
|
|
173
|
-
});
|
|
174
|
-
$a.append($button.contents());
|
|
175
|
-
$button.replaceWith($a);
|
|
176
|
-
});
|
|
177
|
-
processedHtml = $.html();
|
|
178
|
-
console.log('HTML processing complete');
|
|
179
|
-
}
|
|
180
|
-
catch (htmlError) {
|
|
181
|
-
console.error('HTML processing error:', htmlError);
|
|
182
|
-
processedHtml = protectedTemplate;
|
|
183
|
-
}
|
|
184
|
-
// 4) Inline CSS
|
|
185
|
-
let inlinedHtml;
|
|
186
|
-
try {
|
|
187
|
-
inlinedHtml = (0, juice_1.default)(processedHtml, {
|
|
188
|
-
extraCss: cfg.css_content ?? undefined,
|
|
189
|
-
removeStyleTags: false,
|
|
190
|
-
preserveMediaQueries: true,
|
|
191
|
-
preserveFontFaces: true
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
catch (juiceError) {
|
|
195
|
-
console.error('CSS inlining error:', juiceError);
|
|
196
|
-
inlinedHtml = processedHtml;
|
|
197
|
-
}
|
|
198
|
-
// 5) Restore variables/flow
|
|
199
|
-
const finalHtml = cfg.env.filters.restore_variables(inlinedHtml);
|
|
200
|
-
// Write
|
|
201
|
-
if (writeOutput) {
|
|
202
|
-
const distRoot = resolvePathRoot(cfg.dist_dir);
|
|
203
|
-
const outputPath = node_path_1.default.join(distRoot, `${tplname}.njk`);
|
|
204
|
-
node_fs_1.default.mkdirSync(node_path_1.default.dirname(outputPath), { recursive: true });
|
|
205
|
-
node_fs_1.default.writeFileSync(outputPath, finalHtml);
|
|
206
|
-
}
|
|
207
|
-
if (writeOutput) {
|
|
208
|
-
console.log(`Created ${tplname}.njk`);
|
|
209
|
-
}
|
|
210
|
-
return finalHtml;
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
console.error(`Error processing ${tplname}:`, error);
|
|
214
|
-
throw error;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
function get_all_files(dir, filelist = []) {
|
|
218
|
-
const files = node_fs_1.default.readdirSync(dir);
|
|
219
|
-
files.forEach((file) => {
|
|
220
|
-
const file_path = node_path_1.default.join(dir, file);
|
|
221
|
-
if (node_fs_1.default.statSync(file_path).isDirectory()) {
|
|
222
|
-
get_all_files(file_path, filelist);
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
filelist.push(file_path);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
return filelist;
|
|
229
|
-
}
|
|
230
|
-
function find_templates(cfg) {
|
|
231
|
-
const srcRoot = resolvePathRoot(cfg.src_dir);
|
|
232
|
-
const all = get_all_files(srcRoot);
|
|
233
|
-
return all
|
|
234
|
-
.filter((file) => file.endsWith('.njk'))
|
|
235
|
-
.filter((file) => {
|
|
236
|
-
const basename = node_path_1.default.basename(file);
|
|
237
|
-
const content = node_fs_1.default.readFileSync(file, 'utf8');
|
|
238
|
-
return (!basename.startsWith('_') &&
|
|
239
|
-
!basename.includes('layout') &&
|
|
240
|
-
!basename.includes('part') &&
|
|
241
|
-
content.includes('{% extends'));
|
|
242
|
-
})
|
|
243
|
-
.map((file) => {
|
|
244
|
-
const name = node_path_1.default.relative(srcRoot, file);
|
|
245
|
-
return name.substring(0, name.length - 4);
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
async function process_all_templates(cfg) {
|
|
249
|
-
const distRoot = resolvePathRoot(cfg.dist_dir);
|
|
250
|
-
if (!node_fs_1.default.existsSync(distRoot)) {
|
|
251
|
-
node_fs_1.default.mkdirSync(distRoot, { recursive: true });
|
|
252
|
-
}
|
|
253
|
-
const templates = find_templates(cfg);
|
|
254
|
-
console.log(`Found ${templates.length} templates to process: ${templates.join(', ')}`);
|
|
255
|
-
for (const template of templates) {
|
|
256
|
-
try {
|
|
257
|
-
process_template(cfg, template);
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
console.error(`Failed to process ${template}:`, error);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
console.log('All templates processed!');
|
|
264
|
-
}
|
|
265
|
-
function init_env(cfg) {
|
|
266
|
-
const loader = new nunjucks_1.default.FileSystemLoader(resolvePathRoot(cfg.src_dir));
|
|
267
|
-
cfg.env = new nunjucks_1.default.Environment(loader, { autoescape: false });
|
|
268
|
-
if (!cfg.env)
|
|
269
|
-
throw Error('Unable to init nunjucks environment');
|
|
270
|
-
// Load CSS if present
|
|
271
|
-
const cssPath = resolveCssPath(cfg.css_path);
|
|
272
|
-
if (cssPath && node_fs_1.default.existsSync(cssPath)) {
|
|
273
|
-
cfg.css_content = node_fs_1.default.readFileSync(cssPath, 'utf8');
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
cfg.css_content = null;
|
|
277
|
-
}
|
|
278
|
-
// Extension
|
|
279
|
-
cfg.env.addExtension('PreprocessExtension', new PreprocessExtension(cfg));
|
|
280
|
-
// Filters
|
|
281
|
-
cfg.env.addFilter('protect_variables', function (content) {
|
|
282
|
-
return content
|
|
283
|
-
.replace(/(\{\{[\s\S]*?\}\})/g, (m) => `<!--VAR:${Buffer.from(m).toString('base64')}-->`)
|
|
284
|
-
.replace(/(\{%(?!\s*block|\s*endblock|\s*extends)[\s\S]*?%\})/g, (m) => {
|
|
285
|
-
return `<!--FLOW:${Buffer.from(m).toString('base64')}-->`;
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
cfg.env.addFilter('restore_variables', function (content) {
|
|
289
|
-
return content
|
|
290
|
-
.replace(/<!--VAR:(.*?)-->/g, (_m, enc) => Buffer.from(enc, 'base64').toString('utf8'))
|
|
291
|
-
.replace(/<!--FLOW:(.*?)-->/g, (_m, enc) => Buffer.from(enc, 'base64').toString('utf8'));
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
async function do_the_template_thing(options = {}) {
|
|
295
|
-
const cfg = createCompileCfg(options);
|
|
296
|
-
init_env(cfg);
|
|
297
|
-
if (options.tplname) {
|
|
298
|
-
process_template(cfg, options.tplname);
|
|
299
|
-
}
|
|
300
|
-
else {
|
|
301
|
-
await process_all_templates(cfg);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
async function compileTemplate(options) {
|
|
305
|
-
const cfg = createCompileCfg(options);
|
|
306
|
-
init_env(cfg);
|
|
307
|
-
return process_template(cfg, options.tplname, false);
|
|
308
|
-
}
|