@technomoron/mail-magic-client 1.0.25 → 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 +35 -0
- package/README.md +73 -7
- package/dist/cjs/mail-magic-client.d.ts +19 -8
- package/dist/cjs/mail-magic-client.js +82 -40
- package/dist/cli-version.js +33 -0
- package/dist/cli.js +2 -1
- package/dist/esm/mail-magic-client.js +82 -40
- package/dist/mail-magic-client.js +82 -40
- package/dist/preprocess.js +39 -40
- package/package.json +1 -1
package/CHANGES
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
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
|
+
|
|
8
|
+
Version 1.0.29 (2026-02-11)
|
|
9
|
+
|
|
10
|
+
- Expand client coverage for mail-magic-owned endpoints:
|
|
11
|
+
- Add `storeFormRecipient()` for `POST /api/v1/form/recipient`.
|
|
12
|
+
- Add `getSwaggerSpec()` for `GET /api/swagger`.
|
|
13
|
+
- Add `fetchPublicAsset()` for public asset routes (`/asset/...` and `/api/asset/...`).
|
|
14
|
+
- Extend `storeFormTemplate()` input support for newer form fields:
|
|
15
|
+
`replyto_email`, `replyto_from_fields`, `allowed_fields`, and `captcha_required`.
|
|
16
|
+
- Add tests for the new client methods and route mappings.
|
|
17
|
+
- Refresh README usage docs for recipient upserts, swagger retrieval, and public asset fetch helpers.
|
|
18
|
+
|
|
19
|
+
Version 1.0.28 (2026-02-08)
|
|
20
|
+
|
|
21
|
+
- Refresh README/examples to match current server auth and public form submission semantics (`_mm_form_key`,
|
|
22
|
+
`_mm_locale`, `_mm_recipients`).
|
|
23
|
+
- Update `sendFormMessage()` to require `_mm_form_key` and default attachment multipart fields to `_mm_fileN`.
|
|
24
|
+
|
|
25
|
+
Version 1.0.27 (2026-02-07)
|
|
26
|
+
|
|
27
|
+
- Harden the template compiler to prevent `{% include %}` paths from escaping the
|
|
28
|
+
configured template root directory.
|
|
29
|
+
|
|
30
|
+
Version 1.0.26 (2026-02-07)
|
|
31
|
+
|
|
32
|
+
- Fix `GET` requests to omit request bodies and only set `Content-Type` when
|
|
33
|
+
sending JSON payloads.
|
|
34
|
+
- `mm-cli version` now reports the package version instead of a hardcoded string.
|
|
35
|
+
|
|
1
36
|
Version 1.0.25 (2026-02-07)
|
|
2
37
|
|
|
3
38
|
- `sendFormMessage()` now requires `domain` when sending by `formid`, and also
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# mail-magic-client
|
|
1
|
+
# @technomoron/mail-magic-client
|
|
2
2
|
|
|
3
3
|
Client library and CLI for the mail-magic server.
|
|
4
4
|
|
|
@@ -13,7 +13,13 @@ npm install @technomoron/mail-magic-client
|
|
|
13
13
|
```ts
|
|
14
14
|
import TemplateClient from '@technomoron/mail-magic-client';
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Use the server origin (no /api).
|
|
17
|
+
const baseUrl = 'http://127.0.0.1:3776';
|
|
18
|
+
|
|
19
|
+
// This is the user token from init-data.json / the admin API.
|
|
20
|
+
const token = 'example-token';
|
|
21
|
+
|
|
22
|
+
const client = new TemplateClient(baseUrl, token);
|
|
17
23
|
|
|
18
24
|
await client.storeTxTemplate({
|
|
19
25
|
domain: 'example.test',
|
|
@@ -31,6 +37,62 @@ await client.sendTxMessage({
|
|
|
31
37
|
});
|
|
32
38
|
```
|
|
33
39
|
|
|
40
|
+
## Forms
|
|
41
|
+
|
|
42
|
+
Store/update a form template (authenticated). The response includes `data.form_key`, a stable random identifier (nanoid)
|
|
43
|
+
that is preferred for public form submissions:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const res = await client.storeFormTemplate({
|
|
47
|
+
domain: 'example.test',
|
|
48
|
+
idname: 'contact',
|
|
49
|
+
sender: 'Example Forms <forms@example.test>',
|
|
50
|
+
recipient: 'owner@example.test',
|
|
51
|
+
subject: 'New contact form submission',
|
|
52
|
+
secret: 's3cret',
|
|
53
|
+
template: '<p>Hello {{ _fields_.name }}</p>'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const form_key = res.data.form_key;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Store/update form recipient mappings (authenticated):
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
await client.storeFormRecipient({
|
|
63
|
+
domain: 'example.test',
|
|
64
|
+
idname: 'support',
|
|
65
|
+
email: 'Support <support@example.test>',
|
|
66
|
+
name: 'Support Team',
|
|
67
|
+
formid: 'contact',
|
|
68
|
+
locale: 'en'
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Submit a form publicly (no auth required):
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
await fetch(`${baseUrl}/api/v1/form/message`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
_mm_form_key: form_key,
|
|
80
|
+
name: 'Sam',
|
|
81
|
+
email: 'sam@example.test',
|
|
82
|
+
message: 'Hello from the website'
|
|
83
|
+
})
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If you want to use the client helper (`sendFormMessage()`), pass `_mm_form_key` (public form key):
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
await client.sendFormMessage({
|
|
91
|
+
_mm_form_key: form_key,
|
|
92
|
+
fields: { name: 'Sam', email: 'sam@example.test', message: 'Hello' }
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
34
96
|
## CLI
|
|
35
97
|
|
|
36
98
|
The package ships `mm-cli`.
|
|
@@ -40,14 +102,14 @@ The package ships `mm-cli`.
|
|
|
40
102
|
Create `.mmcli-env` in your working directory to set defaults:
|
|
41
103
|
|
|
42
104
|
```ini
|
|
43
|
-
MMCLI_API=http://
|
|
44
|
-
MMCLI_TOKEN=
|
|
45
|
-
# or, split token:
|
|
46
|
-
MMCLI_USERNAME=username
|
|
47
|
-
MMCLI_PASSWORD=token
|
|
105
|
+
MMCLI_API=http://127.0.0.1:3776
|
|
106
|
+
MMCLI_TOKEN=example-token
|
|
48
107
|
MMCLI_DOMAIN=example.test
|
|
49
108
|
```
|
|
50
109
|
|
|
110
|
+
`MMCLI_TOKEN` is treated as the server token string. As a convenience, `MMCLI_USERNAME` + `MMCLI_PASSWORD` can be used
|
|
111
|
+
to build a combined token string (for legacy setups).
|
|
112
|
+
|
|
51
113
|
### Template Commands
|
|
52
114
|
|
|
53
115
|
Compile a template locally:
|
|
@@ -100,3 +162,7 @@ mm-cli assets --file ./hero.png --domain example.test --template-type tx --templ
|
|
|
100
162
|
|
|
101
163
|
- `push-dir` expects a `init-data.json` and domain folders that match the server config layout.
|
|
102
164
|
- Asset uploads use the server endpoint `POST /api/v1/assets`.
|
|
165
|
+
- OpenAPI spec (when enabled): `await client.getSwaggerSpec()`
|
|
166
|
+
- Public asset fetch helpers:
|
|
167
|
+
- `await client.fetchPublicAsset('example.test', 'images/logo.png')` -> `/asset/{domain}/{path}`
|
|
168
|
+
- `await client.fetchPublicAsset('example.test', 'images/logo.png', true)` -> `/api/asset/{domain}/{path}`
|
|
@@ -21,6 +21,19 @@ interface formTemplateData {
|
|
|
21
21
|
subject?: string;
|
|
22
22
|
locale?: string;
|
|
23
23
|
secret?: string;
|
|
24
|
+
replyto_email?: string;
|
|
25
|
+
replyto_from_fields?: boolean;
|
|
26
|
+
allowed_fields?: string[] | string;
|
|
27
|
+
captcha_required?: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface formRecipientData {
|
|
30
|
+
domain: string;
|
|
31
|
+
idname: string;
|
|
32
|
+
email: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
form_key?: string;
|
|
35
|
+
formid?: string;
|
|
36
|
+
locale?: string;
|
|
24
37
|
}
|
|
25
38
|
interface sendTemplateData {
|
|
26
39
|
name: string;
|
|
@@ -33,14 +46,9 @@ interface sendTemplateData {
|
|
|
33
46
|
attachments?: AttachmentInput[];
|
|
34
47
|
}
|
|
35
48
|
interface sendFormData {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
recipient?: string;
|
|
40
|
-
domain?: string;
|
|
41
|
-
locale?: string;
|
|
42
|
-
vars?: Record<string, unknown>;
|
|
43
|
-
replyTo?: string;
|
|
49
|
+
_mm_form_key: string;
|
|
50
|
+
_mm_locale?: string;
|
|
51
|
+
_mm_recipients?: string[] | string;
|
|
44
52
|
fields?: Record<string, unknown>;
|
|
45
53
|
attachments?: AttachmentInput[];
|
|
46
54
|
}
|
|
@@ -82,7 +90,10 @@ declare class templateClient {
|
|
|
82
90
|
storeTxTemplate(td: templateData): Promise<unknown>;
|
|
83
91
|
sendTxMessage(std: sendTemplateData): Promise<unknown>;
|
|
84
92
|
storeFormTemplate(data: formTemplateData): Promise<unknown>;
|
|
93
|
+
storeFormRecipient(data: formRecipientData): Promise<unknown>;
|
|
85
94
|
sendFormMessage(data: sendFormData): Promise<unknown>;
|
|
86
95
|
uploadAssets(data: uploadAssetsData): Promise<unknown>;
|
|
96
|
+
getSwaggerSpec(): Promise<unknown>;
|
|
97
|
+
fetchPublicAsset(domain: string, assetPath: string, viaApiBase?: boolean): Promise<ArrayBuffer>;
|
|
87
98
|
}
|
|
88
99
|
export default templateClient;
|
|
@@ -17,21 +17,24 @@ class templateClient {
|
|
|
17
17
|
}
|
|
18
18
|
async request(method, command, body) {
|
|
19
19
|
const url = `${this.baseURL}${command}`;
|
|
20
|
+
const headers = {
|
|
21
|
+
Accept: 'application/json',
|
|
22
|
+
Authorization: `Bearer apikey-${this.apiKey}`
|
|
23
|
+
};
|
|
20
24
|
const options = {
|
|
21
25
|
method,
|
|
22
|
-
headers
|
|
23
|
-
'Content-Type': 'application/json',
|
|
24
|
-
Authorization: `Bearer apikey-${this.apiKey}`
|
|
25
|
-
},
|
|
26
|
-
body: body ? JSON.stringify(body) : '{}'
|
|
26
|
+
headers
|
|
27
27
|
};
|
|
28
|
-
//
|
|
28
|
+
// Avoid GET bodies (they're non-standard and can break under some proxies).
|
|
29
|
+
if (method !== 'GET' && body !== undefined) {
|
|
30
|
+
headers['Content-Type'] = 'application/json';
|
|
31
|
+
options.body = JSON.stringify(body);
|
|
32
|
+
}
|
|
29
33
|
const response = await fetch(url, options);
|
|
30
34
|
const j = await response.json();
|
|
31
35
|
if (response.ok) {
|
|
32
36
|
return j;
|
|
33
37
|
}
|
|
34
|
-
// console.log(JSON.stringify(j, undefined, 2));
|
|
35
38
|
if (j && j.message) {
|
|
36
39
|
throw new Error(`FETCH FAILED: ${response.status} ${j.message}`);
|
|
37
40
|
}
|
|
@@ -70,10 +73,16 @@ class templateClient {
|
|
|
70
73
|
}
|
|
71
74
|
validateTemplate(template) {
|
|
72
75
|
try {
|
|
73
|
-
const env = new nunjucks_1.default.Environment(
|
|
74
|
-
|
|
76
|
+
const env = new nunjucks_1.default.Environment(null, { autoescape: true });
|
|
77
|
+
const compiled = nunjucks_1.default.compile(template, env);
|
|
78
|
+
compiled.render({});
|
|
75
79
|
}
|
|
76
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
|
+
}
|
|
77
86
|
if (error instanceof Error) {
|
|
78
87
|
throw new Error(`Template validation failed: ${error.message}`);
|
|
79
88
|
}
|
|
@@ -139,13 +148,7 @@ class templateClient {
|
|
|
139
148
|
throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
|
|
140
149
|
}
|
|
141
150
|
async storeTemplate(td) {
|
|
142
|
-
|
|
143
|
-
throw new Error('No template data provided');
|
|
144
|
-
}
|
|
145
|
-
this.validateTemplate(td.template);
|
|
146
|
-
if (td.sender) {
|
|
147
|
-
this.validateSender(td.sender);
|
|
148
|
-
}
|
|
151
|
+
// Backward-compatible alias for transactional template storage.
|
|
149
152
|
return this.storeTxTemplate(td);
|
|
150
153
|
}
|
|
151
154
|
async sendTemplate(std) {
|
|
@@ -172,7 +175,6 @@ class templateClient {
|
|
|
172
175
|
if (invalid.length > 0) {
|
|
173
176
|
throw new Error('Invalid email address(es): ' + invalid.join(','));
|
|
174
177
|
}
|
|
175
|
-
// this.validateTemplate(template);
|
|
176
178
|
const body = {
|
|
177
179
|
name: std.name,
|
|
178
180
|
rcpt: std.rcpt,
|
|
@@ -182,7 +184,6 @@ class templateClient {
|
|
|
182
184
|
replyTo: std.replyTo,
|
|
183
185
|
headers: std.headers
|
|
184
186
|
};
|
|
185
|
-
// console.log(JSON.stringify(body, undefined, 2));
|
|
186
187
|
if (std.attachments && std.attachments.length > 0) {
|
|
187
188
|
if (std.headers) {
|
|
188
189
|
throw new Error('Headers are not supported with attachment uploads');
|
|
@@ -217,36 +218,46 @@ class templateClient {
|
|
|
217
218
|
this.validateSender(data.sender);
|
|
218
219
|
return this.post('/api/v1/form/template', data);
|
|
219
220
|
}
|
|
220
|
-
async
|
|
221
|
-
if (!data.
|
|
222
|
-
throw new Error('
|
|
221
|
+
async storeFormRecipient(data) {
|
|
222
|
+
if (!data.domain) {
|
|
223
|
+
throw new Error('Missing domain');
|
|
224
|
+
}
|
|
225
|
+
if (!data.idname) {
|
|
226
|
+
throw new Error('Missing recipient identifier');
|
|
227
|
+
}
|
|
228
|
+
if (!data.email) {
|
|
229
|
+
throw new Error('Missing recipient email');
|
|
223
230
|
}
|
|
224
|
-
|
|
225
|
-
|
|
231
|
+
const parsed = email_addresses_1.default.parseOneAddress(data.email);
|
|
232
|
+
if (!parsed || !parsed.address) {
|
|
233
|
+
throw new Error('Invalid recipient email address');
|
|
234
|
+
}
|
|
235
|
+
return this.post('/api/v1/form/recipient', data);
|
|
236
|
+
}
|
|
237
|
+
async sendFormMessage(data) {
|
|
238
|
+
if (!data._mm_form_key) {
|
|
239
|
+
throw new Error('Invalid request body; _mm_form_key required');
|
|
226
240
|
}
|
|
227
241
|
const fields = data.fields || {};
|
|
228
242
|
const baseFields = {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
recipient: data.recipient,
|
|
233
|
-
domain: data.domain,
|
|
234
|
-
locale: data.locale,
|
|
235
|
-
vars: data.vars || {},
|
|
236
|
-
replyTo: data.replyTo,
|
|
243
|
+
_mm_form_key: data._mm_form_key,
|
|
244
|
+
_mm_locale: data._mm_locale,
|
|
245
|
+
_mm_recipients: data._mm_recipients,
|
|
237
246
|
...fields
|
|
238
247
|
};
|
|
239
248
|
if (data.attachments && data.attachments.length > 0) {
|
|
240
|
-
const
|
|
249
|
+
const normalized = data.attachments.map((attachment, idx) => {
|
|
250
|
+
const field = attachment.field || `_mm_file${idx + 1}`;
|
|
251
|
+
if (!field.startsWith('_mm_file')) {
|
|
252
|
+
throw new Error('Form attachments must use multipart field names starting with _mm_file');
|
|
253
|
+
}
|
|
254
|
+
return { ...attachment, field };
|
|
255
|
+
});
|
|
256
|
+
const { formData } = this.createAttachmentPayload(normalized);
|
|
241
257
|
this.appendFields(formData, {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
recipient: data.recipient,
|
|
246
|
-
domain: data.domain,
|
|
247
|
-
locale: data.locale,
|
|
248
|
-
vars: JSON.stringify(data.vars || {}),
|
|
249
|
-
replyTo: data.replyTo
|
|
258
|
+
_mm_form_key: data._mm_form_key,
|
|
259
|
+
_mm_locale: data._mm_locale,
|
|
260
|
+
_mm_recipients: data._mm_recipients
|
|
250
261
|
});
|
|
251
262
|
this.appendFields(formData, fields);
|
|
252
263
|
return this.postFormData('/api/v1/form/message', formData);
|
|
@@ -282,5 +293,36 @@ class templateClient {
|
|
|
282
293
|
});
|
|
283
294
|
return this.postFormData('/api/v1/assets', formData);
|
|
284
295
|
}
|
|
296
|
+
async getSwaggerSpec() {
|
|
297
|
+
return this.get('/api/swagger');
|
|
298
|
+
}
|
|
299
|
+
async fetchPublicAsset(domain, assetPath, viaApiBase = false) {
|
|
300
|
+
if (!domain) {
|
|
301
|
+
throw new Error('domain is required');
|
|
302
|
+
}
|
|
303
|
+
if (!assetPath) {
|
|
304
|
+
throw new Error('assetPath is required');
|
|
305
|
+
}
|
|
306
|
+
const cleanedPath = assetPath
|
|
307
|
+
.split('/')
|
|
308
|
+
.filter(Boolean)
|
|
309
|
+
.map((segment) => encodeURIComponent(segment))
|
|
310
|
+
.join('/');
|
|
311
|
+
if (!cleanedPath) {
|
|
312
|
+
throw new Error('assetPath is required');
|
|
313
|
+
}
|
|
314
|
+
const prefix = viaApiBase ? '/api/asset' : '/asset';
|
|
315
|
+
const url = `${this.baseURL}${prefix}/${encodeURIComponent(domain)}/${cleanedPath}`;
|
|
316
|
+
const response = await fetch(url, {
|
|
317
|
+
method: 'GET',
|
|
318
|
+
headers: {
|
|
319
|
+
Accept: '*/*'
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
|
|
324
|
+
}
|
|
325
|
+
return response.arrayBuffer();
|
|
326
|
+
}
|
|
285
327
|
}
|
|
286
328
|
exports.default = templateClient;
|
|
@@ -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
|
@@ -9,6 +9,7 @@ const readline_1 = __importDefault(require("readline"));
|
|
|
9
9
|
const commander_1 = require("commander");
|
|
10
10
|
const cli_env_1 = require("./cli-env");
|
|
11
11
|
const cli_helpers_1 = require("./cli-helpers");
|
|
12
|
+
const cli_version_1 = require("./cli-version");
|
|
12
13
|
const mail_magic_client_1 = __importDefault(require("./mail-magic-client"));
|
|
13
14
|
const preprocess_1 = require("./preprocess");
|
|
14
15
|
const program = new commander_1.Command();
|
|
@@ -144,7 +145,7 @@ program
|
|
|
144
145
|
.command('version')
|
|
145
146
|
.description('Show current client version')
|
|
146
147
|
.action(async () => {
|
|
147
|
-
console.log(
|
|
148
|
+
console.log((0, cli_version_1.resolvePackageVersion)());
|
|
148
149
|
});
|
|
149
150
|
program
|
|
150
151
|
.command('compile')
|
|
@@ -12,21 +12,24 @@ class templateClient {
|
|
|
12
12
|
}
|
|
13
13
|
async request(method, command, body) {
|
|
14
14
|
const url = `${this.baseURL}${command}`;
|
|
15
|
+
const headers = {
|
|
16
|
+
Accept: 'application/json',
|
|
17
|
+
Authorization: `Bearer apikey-${this.apiKey}`
|
|
18
|
+
};
|
|
15
19
|
const options = {
|
|
16
20
|
method,
|
|
17
|
-
headers
|
|
18
|
-
'Content-Type': 'application/json',
|
|
19
|
-
Authorization: `Bearer apikey-${this.apiKey}`
|
|
20
|
-
},
|
|
21
|
-
body: body ? JSON.stringify(body) : '{}'
|
|
21
|
+
headers
|
|
22
22
|
};
|
|
23
|
-
//
|
|
23
|
+
// Avoid GET bodies (they're non-standard and can break under some proxies).
|
|
24
|
+
if (method !== 'GET' && body !== undefined) {
|
|
25
|
+
headers['Content-Type'] = 'application/json';
|
|
26
|
+
options.body = JSON.stringify(body);
|
|
27
|
+
}
|
|
24
28
|
const response = await fetch(url, options);
|
|
25
29
|
const j = await response.json();
|
|
26
30
|
if (response.ok) {
|
|
27
31
|
return j;
|
|
28
32
|
}
|
|
29
|
-
// console.log(JSON.stringify(j, undefined, 2));
|
|
30
33
|
if (j && j.message) {
|
|
31
34
|
throw new Error(`FETCH FAILED: ${response.status} ${j.message}`);
|
|
32
35
|
}
|
|
@@ -65,10 +68,16 @@ class templateClient {
|
|
|
65
68
|
}
|
|
66
69
|
validateTemplate(template) {
|
|
67
70
|
try {
|
|
68
|
-
const env = new nunjucks.Environment(
|
|
69
|
-
|
|
71
|
+
const env = new nunjucks.Environment(null, { autoescape: true });
|
|
72
|
+
const compiled = nunjucks.compile(template, env);
|
|
73
|
+
compiled.render({});
|
|
70
74
|
}
|
|
71
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
|
+
}
|
|
72
81
|
if (error instanceof Error) {
|
|
73
82
|
throw new Error(`Template validation failed: ${error.message}`);
|
|
74
83
|
}
|
|
@@ -134,13 +143,7 @@ class templateClient {
|
|
|
134
143
|
throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
|
|
135
144
|
}
|
|
136
145
|
async storeTemplate(td) {
|
|
137
|
-
|
|
138
|
-
throw new Error('No template data provided');
|
|
139
|
-
}
|
|
140
|
-
this.validateTemplate(td.template);
|
|
141
|
-
if (td.sender) {
|
|
142
|
-
this.validateSender(td.sender);
|
|
143
|
-
}
|
|
146
|
+
// Backward-compatible alias for transactional template storage.
|
|
144
147
|
return this.storeTxTemplate(td);
|
|
145
148
|
}
|
|
146
149
|
async sendTemplate(std) {
|
|
@@ -167,7 +170,6 @@ class templateClient {
|
|
|
167
170
|
if (invalid.length > 0) {
|
|
168
171
|
throw new Error('Invalid email address(es): ' + invalid.join(','));
|
|
169
172
|
}
|
|
170
|
-
// this.validateTemplate(template);
|
|
171
173
|
const body = {
|
|
172
174
|
name: std.name,
|
|
173
175
|
rcpt: std.rcpt,
|
|
@@ -177,7 +179,6 @@ class templateClient {
|
|
|
177
179
|
replyTo: std.replyTo,
|
|
178
180
|
headers: std.headers
|
|
179
181
|
};
|
|
180
|
-
// console.log(JSON.stringify(body, undefined, 2));
|
|
181
182
|
if (std.attachments && std.attachments.length > 0) {
|
|
182
183
|
if (std.headers) {
|
|
183
184
|
throw new Error('Headers are not supported with attachment uploads');
|
|
@@ -212,36 +213,46 @@ class templateClient {
|
|
|
212
213
|
this.validateSender(data.sender);
|
|
213
214
|
return this.post('/api/v1/form/template', data);
|
|
214
215
|
}
|
|
215
|
-
async
|
|
216
|
-
if (!data.
|
|
217
|
-
throw new Error('
|
|
216
|
+
async storeFormRecipient(data) {
|
|
217
|
+
if (!data.domain) {
|
|
218
|
+
throw new Error('Missing domain');
|
|
219
|
+
}
|
|
220
|
+
if (!data.idname) {
|
|
221
|
+
throw new Error('Missing recipient identifier');
|
|
222
|
+
}
|
|
223
|
+
if (!data.email) {
|
|
224
|
+
throw new Error('Missing recipient email');
|
|
218
225
|
}
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
const parsed = emailAddresses.parseOneAddress(data.email);
|
|
227
|
+
if (!parsed || !parsed.address) {
|
|
228
|
+
throw new Error('Invalid recipient email address');
|
|
229
|
+
}
|
|
230
|
+
return this.post('/api/v1/form/recipient', data);
|
|
231
|
+
}
|
|
232
|
+
async sendFormMessage(data) {
|
|
233
|
+
if (!data._mm_form_key) {
|
|
234
|
+
throw new Error('Invalid request body; _mm_form_key required');
|
|
221
235
|
}
|
|
222
236
|
const fields = data.fields || {};
|
|
223
237
|
const baseFields = {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
recipient: data.recipient,
|
|
228
|
-
domain: data.domain,
|
|
229
|
-
locale: data.locale,
|
|
230
|
-
vars: data.vars || {},
|
|
231
|
-
replyTo: data.replyTo,
|
|
238
|
+
_mm_form_key: data._mm_form_key,
|
|
239
|
+
_mm_locale: data._mm_locale,
|
|
240
|
+
_mm_recipients: data._mm_recipients,
|
|
232
241
|
...fields
|
|
233
242
|
};
|
|
234
243
|
if (data.attachments && data.attachments.length > 0) {
|
|
235
|
-
const
|
|
244
|
+
const normalized = data.attachments.map((attachment, idx) => {
|
|
245
|
+
const field = attachment.field || `_mm_file${idx + 1}`;
|
|
246
|
+
if (!field.startsWith('_mm_file')) {
|
|
247
|
+
throw new Error('Form attachments must use multipart field names starting with _mm_file');
|
|
248
|
+
}
|
|
249
|
+
return { ...attachment, field };
|
|
250
|
+
});
|
|
251
|
+
const { formData } = this.createAttachmentPayload(normalized);
|
|
236
252
|
this.appendFields(formData, {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
recipient: data.recipient,
|
|
241
|
-
domain: data.domain,
|
|
242
|
-
locale: data.locale,
|
|
243
|
-
vars: JSON.stringify(data.vars || {}),
|
|
244
|
-
replyTo: data.replyTo
|
|
253
|
+
_mm_form_key: data._mm_form_key,
|
|
254
|
+
_mm_locale: data._mm_locale,
|
|
255
|
+
_mm_recipients: data._mm_recipients
|
|
245
256
|
});
|
|
246
257
|
this.appendFields(formData, fields);
|
|
247
258
|
return this.postFormData('/api/v1/form/message', formData);
|
|
@@ -277,5 +288,36 @@ class templateClient {
|
|
|
277
288
|
});
|
|
278
289
|
return this.postFormData('/api/v1/assets', formData);
|
|
279
290
|
}
|
|
291
|
+
async getSwaggerSpec() {
|
|
292
|
+
return this.get('/api/swagger');
|
|
293
|
+
}
|
|
294
|
+
async fetchPublicAsset(domain, assetPath, viaApiBase = false) {
|
|
295
|
+
if (!domain) {
|
|
296
|
+
throw new Error('domain is required');
|
|
297
|
+
}
|
|
298
|
+
if (!assetPath) {
|
|
299
|
+
throw new Error('assetPath is required');
|
|
300
|
+
}
|
|
301
|
+
const cleanedPath = assetPath
|
|
302
|
+
.split('/')
|
|
303
|
+
.filter(Boolean)
|
|
304
|
+
.map((segment) => encodeURIComponent(segment))
|
|
305
|
+
.join('/');
|
|
306
|
+
if (!cleanedPath) {
|
|
307
|
+
throw new Error('assetPath is required');
|
|
308
|
+
}
|
|
309
|
+
const prefix = viaApiBase ? '/api/asset' : '/asset';
|
|
310
|
+
const url = `${this.baseURL}${prefix}/${encodeURIComponent(domain)}/${cleanedPath}`;
|
|
311
|
+
const response = await fetch(url, {
|
|
312
|
+
method: 'GET',
|
|
313
|
+
headers: {
|
|
314
|
+
Accept: '*/*'
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
if (!response.ok) {
|
|
318
|
+
throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
|
|
319
|
+
}
|
|
320
|
+
return response.arrayBuffer();
|
|
321
|
+
}
|
|
280
322
|
}
|
|
281
323
|
export default templateClient;
|
|
@@ -17,21 +17,24 @@ class templateClient {
|
|
|
17
17
|
}
|
|
18
18
|
async request(method, command, body) {
|
|
19
19
|
const url = `${this.baseURL}${command}`;
|
|
20
|
+
const headers = {
|
|
21
|
+
Accept: 'application/json',
|
|
22
|
+
Authorization: `Bearer apikey-${this.apiKey}`
|
|
23
|
+
};
|
|
20
24
|
const options = {
|
|
21
25
|
method,
|
|
22
|
-
headers
|
|
23
|
-
'Content-Type': 'application/json',
|
|
24
|
-
Authorization: `Bearer apikey-${this.apiKey}`
|
|
25
|
-
},
|
|
26
|
-
body: body ? JSON.stringify(body) : '{}'
|
|
26
|
+
headers
|
|
27
27
|
};
|
|
28
|
-
//
|
|
28
|
+
// Avoid GET bodies (they're non-standard and can break under some proxies).
|
|
29
|
+
if (method !== 'GET' && body !== undefined) {
|
|
30
|
+
headers['Content-Type'] = 'application/json';
|
|
31
|
+
options.body = JSON.stringify(body);
|
|
32
|
+
}
|
|
29
33
|
const response = await fetch(url, options);
|
|
30
34
|
const j = await response.json();
|
|
31
35
|
if (response.ok) {
|
|
32
36
|
return j;
|
|
33
37
|
}
|
|
34
|
-
// console.log(JSON.stringify(j, undefined, 2));
|
|
35
38
|
if (j && j.message) {
|
|
36
39
|
throw new Error(`FETCH FAILED: ${response.status} ${j.message}`);
|
|
37
40
|
}
|
|
@@ -70,10 +73,16 @@ class templateClient {
|
|
|
70
73
|
}
|
|
71
74
|
validateTemplate(template) {
|
|
72
75
|
try {
|
|
73
|
-
const env = new nunjucks_1.default.Environment(
|
|
74
|
-
|
|
76
|
+
const env = new nunjucks_1.default.Environment(null, { autoescape: true });
|
|
77
|
+
const compiled = nunjucks_1.default.compile(template, env);
|
|
78
|
+
compiled.render({});
|
|
75
79
|
}
|
|
76
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
|
+
}
|
|
77
86
|
if (error instanceof Error) {
|
|
78
87
|
throw new Error(`Template validation failed: ${error.message}`);
|
|
79
88
|
}
|
|
@@ -139,13 +148,7 @@ class templateClient {
|
|
|
139
148
|
throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
|
|
140
149
|
}
|
|
141
150
|
async storeTemplate(td) {
|
|
142
|
-
|
|
143
|
-
throw new Error('No template data provided');
|
|
144
|
-
}
|
|
145
|
-
this.validateTemplate(td.template);
|
|
146
|
-
if (td.sender) {
|
|
147
|
-
this.validateSender(td.sender);
|
|
148
|
-
}
|
|
151
|
+
// Backward-compatible alias for transactional template storage.
|
|
149
152
|
return this.storeTxTemplate(td);
|
|
150
153
|
}
|
|
151
154
|
async sendTemplate(std) {
|
|
@@ -172,7 +175,6 @@ class templateClient {
|
|
|
172
175
|
if (invalid.length > 0) {
|
|
173
176
|
throw new Error('Invalid email address(es): ' + invalid.join(','));
|
|
174
177
|
}
|
|
175
|
-
// this.validateTemplate(template);
|
|
176
178
|
const body = {
|
|
177
179
|
name: std.name,
|
|
178
180
|
rcpt: std.rcpt,
|
|
@@ -182,7 +184,6 @@ class templateClient {
|
|
|
182
184
|
replyTo: std.replyTo,
|
|
183
185
|
headers: std.headers
|
|
184
186
|
};
|
|
185
|
-
// console.log(JSON.stringify(body, undefined, 2));
|
|
186
187
|
if (std.attachments && std.attachments.length > 0) {
|
|
187
188
|
if (std.headers) {
|
|
188
189
|
throw new Error('Headers are not supported with attachment uploads');
|
|
@@ -217,36 +218,46 @@ class templateClient {
|
|
|
217
218
|
this.validateSender(data.sender);
|
|
218
219
|
return this.post('/api/v1/form/template', data);
|
|
219
220
|
}
|
|
220
|
-
async
|
|
221
|
-
if (!data.
|
|
222
|
-
throw new Error('
|
|
221
|
+
async storeFormRecipient(data) {
|
|
222
|
+
if (!data.domain) {
|
|
223
|
+
throw new Error('Missing domain');
|
|
224
|
+
}
|
|
225
|
+
if (!data.idname) {
|
|
226
|
+
throw new Error('Missing recipient identifier');
|
|
227
|
+
}
|
|
228
|
+
if (!data.email) {
|
|
229
|
+
throw new Error('Missing recipient email');
|
|
223
230
|
}
|
|
224
|
-
|
|
225
|
-
|
|
231
|
+
const parsed = email_addresses_1.default.parseOneAddress(data.email);
|
|
232
|
+
if (!parsed || !parsed.address) {
|
|
233
|
+
throw new Error('Invalid recipient email address');
|
|
234
|
+
}
|
|
235
|
+
return this.post('/api/v1/form/recipient', data);
|
|
236
|
+
}
|
|
237
|
+
async sendFormMessage(data) {
|
|
238
|
+
if (!data._mm_form_key) {
|
|
239
|
+
throw new Error('Invalid request body; _mm_form_key required');
|
|
226
240
|
}
|
|
227
241
|
const fields = data.fields || {};
|
|
228
242
|
const baseFields = {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
recipient: data.recipient,
|
|
233
|
-
domain: data.domain,
|
|
234
|
-
locale: data.locale,
|
|
235
|
-
vars: data.vars || {},
|
|
236
|
-
replyTo: data.replyTo,
|
|
243
|
+
_mm_form_key: data._mm_form_key,
|
|
244
|
+
_mm_locale: data._mm_locale,
|
|
245
|
+
_mm_recipients: data._mm_recipients,
|
|
237
246
|
...fields
|
|
238
247
|
};
|
|
239
248
|
if (data.attachments && data.attachments.length > 0) {
|
|
240
|
-
const
|
|
249
|
+
const normalized = data.attachments.map((attachment, idx) => {
|
|
250
|
+
const field = attachment.field || `_mm_file${idx + 1}`;
|
|
251
|
+
if (!field.startsWith('_mm_file')) {
|
|
252
|
+
throw new Error('Form attachments must use multipart field names starting with _mm_file');
|
|
253
|
+
}
|
|
254
|
+
return { ...attachment, field };
|
|
255
|
+
});
|
|
256
|
+
const { formData } = this.createAttachmentPayload(normalized);
|
|
241
257
|
this.appendFields(formData, {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
recipient: data.recipient,
|
|
246
|
-
domain: data.domain,
|
|
247
|
-
locale: data.locale,
|
|
248
|
-
vars: JSON.stringify(data.vars || {}),
|
|
249
|
-
replyTo: data.replyTo
|
|
258
|
+
_mm_form_key: data._mm_form_key,
|
|
259
|
+
_mm_locale: data._mm_locale,
|
|
260
|
+
_mm_recipients: data._mm_recipients
|
|
250
261
|
});
|
|
251
262
|
this.appendFields(formData, fields);
|
|
252
263
|
return this.postFormData('/api/v1/form/message', formData);
|
|
@@ -282,5 +293,36 @@ class templateClient {
|
|
|
282
293
|
});
|
|
283
294
|
return this.postFormData('/api/v1/assets', formData);
|
|
284
295
|
}
|
|
296
|
+
async getSwaggerSpec() {
|
|
297
|
+
return this.get('/api/swagger');
|
|
298
|
+
}
|
|
299
|
+
async fetchPublicAsset(domain, assetPath, viaApiBase = false) {
|
|
300
|
+
if (!domain) {
|
|
301
|
+
throw new Error('domain is required');
|
|
302
|
+
}
|
|
303
|
+
if (!assetPath) {
|
|
304
|
+
throw new Error('assetPath is required');
|
|
305
|
+
}
|
|
306
|
+
const cleanedPath = assetPath
|
|
307
|
+
.split('/')
|
|
308
|
+
.filter(Boolean)
|
|
309
|
+
.map((segment) => encodeURIComponent(segment))
|
|
310
|
+
.join('/');
|
|
311
|
+
if (!cleanedPath) {
|
|
312
|
+
throw new Error('assetPath is required');
|
|
313
|
+
}
|
|
314
|
+
const prefix = viaApiBase ? '/api/asset' : '/asset';
|
|
315
|
+
const url = `${this.baseURL}${prefix}/${encodeURIComponent(domain)}/${cleanedPath}`;
|
|
316
|
+
const response = await fetch(url, {
|
|
317
|
+
method: 'GET',
|
|
318
|
+
headers: {
|
|
319
|
+
Accept: '*/*'
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
|
|
324
|
+
}
|
|
325
|
+
return response.arrayBuffer();
|
|
326
|
+
}
|
|
285
327
|
}
|
|
286
328
|
exports.default = templateClient;
|
package/dist/preprocess.js
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
}
|
|
@@ -31,7 +33,7 @@ function resolveCssPath(cssPath) {
|
|
|
31
33
|
}
|
|
32
34
|
return node_path_1.default.isAbsolute(cssPath) ? cssPath : node_path_1.default.join(process.cwd(), cssPath);
|
|
33
35
|
}
|
|
34
|
-
function inlineIncludes(content, baseDir, srcRoot, stack) {
|
|
36
|
+
function inlineIncludes(content, baseDir, srcRoot, normalizedSrcRoot, stack) {
|
|
35
37
|
const includeExp = /\{%\s*include\s+['"]([^'"]+)['"][^%]*%\}/g;
|
|
36
38
|
return content.replace(includeExp, (_match, includePath) => {
|
|
37
39
|
const cleaned = includePath.replace(/^\/+/, '');
|
|
@@ -41,19 +43,26 @@ function inlineIncludes(content, baseDir, srcRoot, stack) {
|
|
|
41
43
|
throw new Error(`Include not found: ${includePath}`);
|
|
42
44
|
}
|
|
43
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
|
+
}
|
|
44
52
|
if (stack.has(resolved)) {
|
|
45
53
|
throw new Error(`Circular include detected for ${includePath}`);
|
|
46
54
|
}
|
|
47
55
|
stack.add(resolved);
|
|
48
56
|
const raw = node_fs_1.default.readFileSync(resolved, 'utf8');
|
|
49
|
-
const inlined = inlineIncludes(raw, node_path_1.default.dirname(resolved), srcRoot, stack);
|
|
57
|
+
const inlined = inlineIncludes(raw, node_path_1.default.dirname(resolved), srcRoot, normalizedSrcRoot, stack);
|
|
50
58
|
stack.delete(resolved);
|
|
51
59
|
return inlined;
|
|
52
60
|
});
|
|
53
61
|
}
|
|
54
62
|
class PreprocessExtension {
|
|
55
|
-
constructor() {
|
|
63
|
+
constructor(cfg) {
|
|
56
64
|
this.tags = ['process_layout'];
|
|
65
|
+
this.cfg = cfg;
|
|
57
66
|
}
|
|
58
67
|
parse(parser, nodes) {
|
|
59
68
|
const token = parser.nextToken();
|
|
@@ -62,13 +71,13 @@ class PreprocessExtension {
|
|
|
62
71
|
return new nodes.CallExtension(this, 'run', args);
|
|
63
72
|
}
|
|
64
73
|
run(_context, tplname) {
|
|
65
|
-
const template = cfg.env.getTemplate(tplname);
|
|
74
|
+
const template = this.cfg.env.getTemplate(tplname);
|
|
66
75
|
const src = template.tmplStr;
|
|
67
76
|
const extmatch = src.match(/\{%\s*extends\s+['"]([^'"]+)['"]\s*%\}/);
|
|
68
77
|
if (!extmatch)
|
|
69
78
|
return src;
|
|
70
79
|
const layoutName = extmatch[1];
|
|
71
|
-
const layoutTemplate = cfg.env.getTemplate(layoutName);
|
|
80
|
+
const layoutTemplate = this.cfg.env.getTemplate(layoutName);
|
|
72
81
|
const layoutSrc = layoutTemplate.tmplStr;
|
|
73
82
|
const blocks = {};
|
|
74
83
|
const blockexp = /\{%\s*block\s+([a-zA-Z0-9_]+)\s*%\}([\s\S]*?)\{%\s*endblock\s*%\}/g;
|
|
@@ -91,16 +100,18 @@ class PreprocessExtension {
|
|
|
91
100
|
return merged;
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
|
-
function process_template(tplname, writeOutput = true) {
|
|
103
|
+
function process_template(cfg, tplname, writeOutput = true) {
|
|
95
104
|
console.log(`Processing template: ${tplname}`);
|
|
96
105
|
try {
|
|
97
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;
|
|
98
109
|
const templateFile = node_path_1.default.join(srcRoot, `${tplname}.njk`);
|
|
99
110
|
// 1) Resolve template inheritance
|
|
100
111
|
const mergedTemplate = cfg.env.renderString(`{% process_layout "${tplname}.njk" %}`, {});
|
|
101
112
|
// 1.5) Inline partials/includes so the server doesn't need a loader
|
|
102
113
|
const mergedWithPartials = cfg.inline_includes
|
|
103
|
-
? inlineIncludes(mergedTemplate, node_path_1.default.dirname(templateFile), srcRoot, new Set())
|
|
114
|
+
? inlineIncludes(mergedTemplate, node_path_1.default.dirname(templateFile), srcRoot, normalizedSrcRoot, new Set())
|
|
104
115
|
: mergedTemplate;
|
|
105
116
|
// 2) Protect variables/flow
|
|
106
117
|
const protectedTemplate = cfg.env.filters.protect_variables(mergedWithPartials);
|
|
@@ -216,7 +227,7 @@ function get_all_files(dir, filelist = []) {
|
|
|
216
227
|
});
|
|
217
228
|
return filelist;
|
|
218
229
|
}
|
|
219
|
-
function find_templates() {
|
|
230
|
+
function find_templates(cfg) {
|
|
220
231
|
const srcRoot = resolvePathRoot(cfg.src_dir);
|
|
221
232
|
const all = get_all_files(srcRoot);
|
|
222
233
|
return all
|
|
@@ -234,16 +245,16 @@ function find_templates() {
|
|
|
234
245
|
return name.substring(0, name.length - 4);
|
|
235
246
|
});
|
|
236
247
|
}
|
|
237
|
-
async function process_all_templates() {
|
|
248
|
+
async function process_all_templates(cfg) {
|
|
238
249
|
const distRoot = resolvePathRoot(cfg.dist_dir);
|
|
239
250
|
if (!node_fs_1.default.existsSync(distRoot)) {
|
|
240
251
|
node_fs_1.default.mkdirSync(distRoot, { recursive: true });
|
|
241
252
|
}
|
|
242
|
-
const templates = find_templates();
|
|
253
|
+
const templates = find_templates(cfg);
|
|
243
254
|
console.log(`Found ${templates.length} templates to process: ${templates.join(', ')}`);
|
|
244
255
|
for (const template of templates) {
|
|
245
256
|
try {
|
|
246
|
-
process_template(template);
|
|
257
|
+
process_template(cfg, template);
|
|
247
258
|
}
|
|
248
259
|
catch (error) {
|
|
249
260
|
console.error(`Failed to process ${template}:`, error);
|
|
@@ -251,7 +262,7 @@ async function process_all_templates() {
|
|
|
251
262
|
}
|
|
252
263
|
console.log('All templates processed!');
|
|
253
264
|
}
|
|
254
|
-
function init_env() {
|
|
265
|
+
function init_env(cfg) {
|
|
255
266
|
const loader = new nunjucks_1.default.FileSystemLoader(resolvePathRoot(cfg.src_dir));
|
|
256
267
|
cfg.env = new nunjucks_1.default.Environment(loader, { autoescape: false });
|
|
257
268
|
if (!cfg.env)
|
|
@@ -265,7 +276,7 @@ function init_env() {
|
|
|
265
276
|
cfg.css_content = null;
|
|
266
277
|
}
|
|
267
278
|
// Extension
|
|
268
|
-
cfg.env.addExtension('PreprocessExtension', new PreprocessExtension());
|
|
279
|
+
cfg.env.addExtension('PreprocessExtension', new PreprocessExtension(cfg));
|
|
269
280
|
// Filters
|
|
270
281
|
cfg.env.addFilter('protect_variables', function (content) {
|
|
271
282
|
return content
|
|
@@ -281,29 +292,17 @@ function init_env() {
|
|
|
281
292
|
});
|
|
282
293
|
}
|
|
283
294
|
async function do_the_template_thing(options = {}) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (options.dist_dir)
|
|
287
|
-
cfg.dist_dir = options.dist_dir;
|
|
288
|
-
if (options.css_path)
|
|
289
|
-
cfg.css_path = options.css_path;
|
|
290
|
-
if (options.inline_includes !== undefined)
|
|
291
|
-
cfg.inline_includes = options.inline_includes;
|
|
292
|
-
init_env();
|
|
295
|
+
const cfg = createCompileCfg(options);
|
|
296
|
+
init_env(cfg);
|
|
293
297
|
if (options.tplname) {
|
|
294
|
-
process_template(options.tplname);
|
|
298
|
+
process_template(cfg, options.tplname);
|
|
295
299
|
}
|
|
296
300
|
else {
|
|
297
|
-
await process_all_templates();
|
|
301
|
+
await process_all_templates(cfg);
|
|
298
302
|
}
|
|
299
303
|
}
|
|
300
304
|
async function compileTemplate(options) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
cfg.css_path = options.css_path;
|
|
305
|
-
if (options.inline_includes !== undefined)
|
|
306
|
-
cfg.inline_includes = options.inline_includes;
|
|
307
|
-
init_env();
|
|
308
|
-
return process_template(options.tplname, false);
|
|
305
|
+
const cfg = createCompileCfg(options);
|
|
306
|
+
init_env(cfg);
|
|
307
|
+
return process_template(cfg, options.tplname, false);
|
|
309
308
|
}
|