@technomoron/mail-magic-client 1.0.28 → 1.0.30

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 CHANGED
@@ -1,3 +1,10 @@
1
+ Version 1.0.30 (2026-02-17)
2
+
3
+ - Refactor template preprocess compilation to use per-invocation configuration
4
+ (remove module-level mutable config state).
5
+ - Add regression coverage to ensure preprocess options (such as
6
+ `inline_includes`) do not leak between compile calls.
7
+
1
8
  Version 1.0.29 (2026-02-11)
2
9
 
3
10
  - Expand client coverage for mail-magic-owned endpoints:
@@ -30,13 +30,11 @@ class templateClient {
30
30
  headers['Content-Type'] = 'application/json';
31
31
  options.body = JSON.stringify(body);
32
32
  }
33
- // console.log(JSON.stringify({ options, url }));
34
33
  const response = await fetch(url, options);
35
34
  const j = await response.json();
36
35
  if (response.ok) {
37
36
  return j;
38
37
  }
39
- // console.log(JSON.stringify(j, undefined, 2));
40
38
  if (j && j.message) {
41
39
  throw new Error(`FETCH FAILED: ${response.status} ${j.message}`);
42
40
  }
@@ -75,10 +73,16 @@ class templateClient {
75
73
  }
76
74
  validateTemplate(template) {
77
75
  try {
78
- const env = new nunjucks_1.default.Environment(new nunjucks_1.default.FileSystemLoader(['./templates']));
79
- env.renderString(template, {});
76
+ const env = new nunjucks_1.default.Environment(null, { autoescape: true });
77
+ const compiled = nunjucks_1.default.compile(template, env);
78
+ compiled.render({});
80
79
  }
81
80
  catch (error) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ // Syntax validation should not require local template loaders.
83
+ if (/template not found|no loader|unable to find template/i.test(message)) {
84
+ return;
85
+ }
82
86
  if (error instanceof Error) {
83
87
  throw new Error(`Template validation failed: ${error.message}`);
84
88
  }
@@ -144,13 +148,7 @@ class templateClient {
144
148
  throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
145
149
  }
146
150
  async storeTemplate(td) {
147
- if (!td.template) {
148
- throw new Error('No template data provided');
149
- }
150
- this.validateTemplate(td.template);
151
- if (td.sender) {
152
- this.validateSender(td.sender);
153
- }
151
+ // Backward-compatible alias for transactional template storage.
154
152
  return this.storeTxTemplate(td);
155
153
  }
156
154
  async sendTemplate(std) {
@@ -177,7 +175,6 @@ class templateClient {
177
175
  if (invalid.length > 0) {
178
176
  throw new Error('Invalid email address(es): ' + invalid.join(','));
179
177
  }
180
- // this.validateTemplate(template);
181
178
  const body = {
182
179
  name: std.name,
183
180
  rcpt: std.rcpt,
@@ -187,7 +184,6 @@ class templateClient {
187
184
  replyTo: std.replyTo,
188
185
  headers: std.headers
189
186
  };
190
- // console.log(JSON.stringify(body, undefined, 2));
191
187
  if (std.attachments && std.attachments.length > 0) {
192
188
  if (std.headers) {
193
189
  throw new Error('Headers are not supported with attachment uploads');
@@ -0,0 +1,33 @@
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
+ exports.resolvePackageVersion = resolvePackageVersion;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function resolvePackageVersion(options = {}) {
10
+ const argv1 = options.argv1 ?? process.argv[1] ?? '';
11
+ const cwd = options.cwd ?? process.cwd();
12
+ const candidates = [
13
+ argv1 ? path_1.default.resolve(path_1.default.dirname(argv1), '../package.json') : '',
14
+ path_1.default.resolve(cwd, 'package.json'),
15
+ path_1.default.resolve(cwd, 'packages/mail-magic-client/package.json')
16
+ ].filter(Boolean);
17
+ for (const candidate of candidates) {
18
+ if (!fs_1.default.existsSync(candidate)) {
19
+ continue;
20
+ }
21
+ try {
22
+ const raw = fs_1.default.readFileSync(candidate, 'utf8');
23
+ const data = JSON.parse(raw);
24
+ if (data.name === '@technomoron/mail-magic-client') {
25
+ return typeof data.version === 'string' && data.version ? data.version : 'unknown';
26
+ }
27
+ }
28
+ catch {
29
+ // Try next candidate.
30
+ }
31
+ }
32
+ return 'unknown';
33
+ }
package/dist/cli.js CHANGED
@@ -5,27 +5,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const fs_1 = __importDefault(require("fs"));
8
- const path_1 = __importDefault(require("path"));
9
8
  const readline_1 = __importDefault(require("readline"));
10
9
  const commander_1 = require("commander");
11
10
  const cli_env_1 = require("./cli-env");
12
11
  const cli_helpers_1 = require("./cli-helpers");
12
+ const cli_version_1 = require("./cli-version");
13
13
  const mail_magic_client_1 = __importDefault(require("./mail-magic-client"));
14
14
  const preprocess_1 = require("./preprocess");
15
15
  const program = new commander_1.Command();
16
16
  const envDefaults = (0, cli_env_1.loadCliEnv)();
17
17
  const defaultToken = (0, cli_env_1.resolveToken)(envDefaults);
18
18
  const apiDefault = envDefaults.api || 'http://localhost:3000';
19
- function resolvePackageVersion() {
20
- try {
21
- const raw = fs_1.default.readFileSync(path_1.default.join(__dirname, '../package.json'), 'utf8');
22
- const data = JSON.parse(raw);
23
- return typeof data.version === 'string' && data.version ? data.version : 'unknown';
24
- }
25
- catch {
26
- return 'unknown';
27
- }
28
- }
29
19
  program.option('-a, --api <api>', 'Base API endpoint', apiDefault);
30
20
  if (defaultToken) {
31
21
  program.option('-t, --token <token>', 'Authentication token in the format "username:token"', defaultToken);
@@ -155,7 +145,7 @@ program
155
145
  .command('version')
156
146
  .description('Show current client version')
157
147
  .action(async () => {
158
- console.log(resolvePackageVersion());
148
+ console.log((0, cli_version_1.resolvePackageVersion)());
159
149
  });
160
150
  program
161
151
  .command('compile')
@@ -25,13 +25,11 @@ class templateClient {
25
25
  headers['Content-Type'] = 'application/json';
26
26
  options.body = JSON.stringify(body);
27
27
  }
28
- // console.log(JSON.stringify({ options, url }));
29
28
  const response = await fetch(url, options);
30
29
  const j = await response.json();
31
30
  if (response.ok) {
32
31
  return j;
33
32
  }
34
- // console.log(JSON.stringify(j, undefined, 2));
35
33
  if (j && j.message) {
36
34
  throw new Error(`FETCH FAILED: ${response.status} ${j.message}`);
37
35
  }
@@ -70,10 +68,16 @@ class templateClient {
70
68
  }
71
69
  validateTemplate(template) {
72
70
  try {
73
- const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(['./templates']));
74
- env.renderString(template, {});
71
+ const env = new nunjucks.Environment(null, { autoescape: true });
72
+ const compiled = nunjucks.compile(template, env);
73
+ compiled.render({});
75
74
  }
76
75
  catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ // Syntax validation should not require local template loaders.
78
+ if (/template not found|no loader|unable to find template/i.test(message)) {
79
+ return;
80
+ }
77
81
  if (error instanceof Error) {
78
82
  throw new Error(`Template validation failed: ${error.message}`);
79
83
  }
@@ -139,13 +143,7 @@ class templateClient {
139
143
  throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
140
144
  }
141
145
  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
- }
146
+ // Backward-compatible alias for transactional template storage.
149
147
  return this.storeTxTemplate(td);
150
148
  }
151
149
  async sendTemplate(std) {
@@ -172,7 +170,6 @@ class templateClient {
172
170
  if (invalid.length > 0) {
173
171
  throw new Error('Invalid email address(es): ' + invalid.join(','));
174
172
  }
175
- // this.validateTemplate(template);
176
173
  const body = {
177
174
  name: std.name,
178
175
  rcpt: std.rcpt,
@@ -182,7 +179,6 @@ class templateClient {
182
179
  replyTo: std.replyTo,
183
180
  headers: std.headers
184
181
  };
185
- // console.log(JSON.stringify(body, undefined, 2));
186
182
  if (std.attachments && std.attachments.length > 0) {
187
183
  if (std.headers) {
188
184
  throw new Error('Headers are not supported with attachment uploads');
@@ -30,13 +30,11 @@ class templateClient {
30
30
  headers['Content-Type'] = 'application/json';
31
31
  options.body = JSON.stringify(body);
32
32
  }
33
- // console.log(JSON.stringify({ options, url }));
34
33
  const response = await fetch(url, options);
35
34
  const j = await response.json();
36
35
  if (response.ok) {
37
36
  return j;
38
37
  }
39
- // console.log(JSON.stringify(j, undefined, 2));
40
38
  if (j && j.message) {
41
39
  throw new Error(`FETCH FAILED: ${response.status} ${j.message}`);
42
40
  }
@@ -75,10 +73,16 @@ class templateClient {
75
73
  }
76
74
  validateTemplate(template) {
77
75
  try {
78
- const env = new nunjucks_1.default.Environment(new nunjucks_1.default.FileSystemLoader(['./templates']));
79
- env.renderString(template, {});
76
+ const env = new nunjucks_1.default.Environment(null, { autoescape: true });
77
+ const compiled = nunjucks_1.default.compile(template, env);
78
+ compiled.render({});
80
79
  }
81
80
  catch (error) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ // Syntax validation should not require local template loaders.
83
+ if (/template not found|no loader|unable to find template/i.test(message)) {
84
+ return;
85
+ }
82
86
  if (error instanceof Error) {
83
87
  throw new Error(`Template validation failed: ${error.message}`);
84
88
  }
@@ -144,13 +148,7 @@ class templateClient {
144
148
  throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
145
149
  }
146
150
  async storeTemplate(td) {
147
- if (!td.template) {
148
- throw new Error('No template data provided');
149
- }
150
- this.validateTemplate(td.template);
151
- if (td.sender) {
152
- this.validateSender(td.sender);
153
- }
151
+ // Backward-compatible alias for transactional template storage.
154
152
  return this.storeTxTemplate(td);
155
153
  }
156
154
  async sendTemplate(std) {
@@ -177,7 +175,6 @@ class templateClient {
177
175
  if (invalid.length > 0) {
178
176
  throw new Error('Invalid email address(es): ' + invalid.join(','));
179
177
  }
180
- // this.validateTemplate(template);
181
178
  const body = {
182
179
  name: std.name,
183
180
  rcpt: std.rcpt,
@@ -187,7 +184,6 @@ class templateClient {
187
184
  replyTo: std.replyTo,
188
185
  headers: std.headers
189
186
  };
190
- // console.log(JSON.stringify(body, undefined, 2));
191
187
  if (std.attachments && std.attachments.length > 0) {
192
188
  if (std.headers) {
193
189
  throw new Error('Headers are not supported with attachment uploads');
@@ -14,14 +14,16 @@ const node_path_1 = __importDefault(require("node:path"));
14
14
  const cheerio_1 = require("cheerio");
15
15
  const juice_1 = __importDefault(require("juice"));
16
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
- };
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
+ }
25
27
  function resolvePathRoot(dir) {
26
28
  return node_path_1.default.isAbsolute(dir) ? dir : node_path_1.default.join(process.cwd(), dir);
27
29
  }
@@ -58,8 +60,9 @@ function inlineIncludes(content, baseDir, srcRoot, normalizedSrcRoot, stack) {
58
60
  });
59
61
  }
60
62
  class PreprocessExtension {
61
- constructor() {
63
+ constructor(cfg) {
62
64
  this.tags = ['process_layout'];
65
+ this.cfg = cfg;
63
66
  }
64
67
  parse(parser, nodes) {
65
68
  const token = parser.nextToken();
@@ -68,13 +71,13 @@ class PreprocessExtension {
68
71
  return new nodes.CallExtension(this, 'run', args);
69
72
  }
70
73
  run(_context, tplname) {
71
- const template = cfg.env.getTemplate(tplname);
74
+ const template = this.cfg.env.getTemplate(tplname);
72
75
  const src = template.tmplStr;
73
76
  const extmatch = src.match(/\{%\s*extends\s+['"]([^'"]+)['"]\s*%\}/);
74
77
  if (!extmatch)
75
78
  return src;
76
79
  const layoutName = extmatch[1];
77
- const layoutTemplate = cfg.env.getTemplate(layoutName);
80
+ const layoutTemplate = this.cfg.env.getTemplate(layoutName);
78
81
  const layoutSrc = layoutTemplate.tmplStr;
79
82
  const blocks = {};
80
83
  const blockexp = /\{%\s*block\s+([a-zA-Z0-9_]+)\s*%\}([\s\S]*?)\{%\s*endblock\s*%\}/g;
@@ -97,7 +100,7 @@ class PreprocessExtension {
97
100
  return merged;
98
101
  }
99
102
  }
100
- function process_template(tplname, writeOutput = true) {
103
+ function process_template(cfg, tplname, writeOutput = true) {
101
104
  console.log(`Processing template: ${tplname}`);
102
105
  try {
103
106
  const srcRoot = resolvePathRoot(cfg.src_dir);
@@ -224,7 +227,7 @@ function get_all_files(dir, filelist = []) {
224
227
  });
225
228
  return filelist;
226
229
  }
227
- function find_templates() {
230
+ function find_templates(cfg) {
228
231
  const srcRoot = resolvePathRoot(cfg.src_dir);
229
232
  const all = get_all_files(srcRoot);
230
233
  return all
@@ -242,16 +245,16 @@ function find_templates() {
242
245
  return name.substring(0, name.length - 4);
243
246
  });
244
247
  }
245
- async function process_all_templates() {
248
+ async function process_all_templates(cfg) {
246
249
  const distRoot = resolvePathRoot(cfg.dist_dir);
247
250
  if (!node_fs_1.default.existsSync(distRoot)) {
248
251
  node_fs_1.default.mkdirSync(distRoot, { recursive: true });
249
252
  }
250
- const templates = find_templates();
253
+ const templates = find_templates(cfg);
251
254
  console.log(`Found ${templates.length} templates to process: ${templates.join(', ')}`);
252
255
  for (const template of templates) {
253
256
  try {
254
- process_template(template);
257
+ process_template(cfg, template);
255
258
  }
256
259
  catch (error) {
257
260
  console.error(`Failed to process ${template}:`, error);
@@ -259,7 +262,7 @@ async function process_all_templates() {
259
262
  }
260
263
  console.log('All templates processed!');
261
264
  }
262
- function init_env() {
265
+ function init_env(cfg) {
263
266
  const loader = new nunjucks_1.default.FileSystemLoader(resolvePathRoot(cfg.src_dir));
264
267
  cfg.env = new nunjucks_1.default.Environment(loader, { autoescape: false });
265
268
  if (!cfg.env)
@@ -273,7 +276,7 @@ function init_env() {
273
276
  cfg.css_content = null;
274
277
  }
275
278
  // Extension
276
- cfg.env.addExtension('PreprocessExtension', new PreprocessExtension());
279
+ cfg.env.addExtension('PreprocessExtension', new PreprocessExtension(cfg));
277
280
  // Filters
278
281
  cfg.env.addFilter('protect_variables', function (content) {
279
282
  return content
@@ -289,29 +292,17 @@ function init_env() {
289
292
  });
290
293
  }
291
294
  async function do_the_template_thing(options = {}) {
292
- if (options.src_dir)
293
- cfg.src_dir = options.src_dir;
294
- if (options.dist_dir)
295
- cfg.dist_dir = options.dist_dir;
296
- if (options.css_path)
297
- cfg.css_path = options.css_path;
298
- if (options.inline_includes !== undefined)
299
- cfg.inline_includes = options.inline_includes;
300
- init_env();
295
+ const cfg = createCompileCfg(options);
296
+ init_env(cfg);
301
297
  if (options.tplname) {
302
- process_template(options.tplname);
298
+ process_template(cfg, options.tplname);
303
299
  }
304
300
  else {
305
- await process_all_templates();
301
+ await process_all_templates(cfg);
306
302
  }
307
303
  }
308
304
  async function compileTemplate(options) {
309
- if (options.src_dir)
310
- cfg.src_dir = options.src_dir;
311
- if (options.css_path)
312
- cfg.css_path = options.css_path;
313
- if (options.inline_includes !== undefined)
314
- cfg.inline_includes = options.inline_includes;
315
- init_env();
316
- return process_template(options.tplname, false);
305
+ const cfg = createCompileCfg(options);
306
+ init_env(cfg);
307
+ return process_template(cfg, options.tplname, false);
317
308
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/mail-magic-client",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "Client library for mail-magic",
5
5
  "main": "dist/cjs/mail-magic-client.js",
6
6
  "types": "dist/cjs/mail-magic-client.d.ts",