@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.
@@ -0,0 +1,405 @@
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.pushTemplate = pushTemplate;
7
+ exports.pushTemplateDir = pushTemplateDir;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const mail_magic_client_1 = __importDefault(require("./mail-magic-client"));
11
+ const preprocess_1 = require("./preprocess");
12
+ function resolveTemplateName(template, inputDir) {
13
+ const cleaned = template.replace(/\\/g, '/');
14
+ const inputRoot = node_path_1.default.resolve(inputDir);
15
+ if (node_path_1.default.isAbsolute(template)) {
16
+ const templateAbs = node_path_1.default.resolve(template);
17
+ const relative = node_path_1.default.relative(inputRoot, templateAbs);
18
+ if (relative.startsWith('..') || node_path_1.default.isAbsolute(relative)) {
19
+ throw new Error(`Template must be under input directory: ${template}`);
20
+ }
21
+ const withoutExt = relative.endsWith('.njk') ? relative.slice(0, -4) : relative;
22
+ return withoutExt.replace(/\\/g, '/');
23
+ }
24
+ return cleaned.endsWith('.njk') ? cleaned.slice(0, -4) : cleaned;
25
+ }
26
+ function normalizeSlug(input) {
27
+ if (!input) {
28
+ return '';
29
+ }
30
+ return input
31
+ .trim()
32
+ .toLowerCase()
33
+ .replace(/[^a-z0-9-_\.]/g, '-')
34
+ .replace(/--+/g, '-')
35
+ .replace(/^-+|-+$/g, '');
36
+ }
37
+ function loadInitData(rootDir) {
38
+ const initPath = node_path_1.default.join(rootDir, 'init-data.json');
39
+ if (!node_fs_1.default.existsSync(initPath)) {
40
+ throw new Error(`init-data.json not found in ${rootDir}`);
41
+ }
42
+ const raw = node_fs_1.default.readFileSync(initPath, 'utf8');
43
+ const data = JSON.parse(raw);
44
+ return {
45
+ domain: data.domain ?? [],
46
+ template: data.template ?? [],
47
+ form: data.form ?? []
48
+ };
49
+ }
50
+ function collectFiles(dir) {
51
+ if (!node_fs_1.default.existsSync(dir)) {
52
+ return [];
53
+ }
54
+ const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
55
+ const files = [];
56
+ for (const entry of entries) {
57
+ const fullPath = node_path_1.default.join(dir, entry.name);
58
+ if (entry.isDirectory()) {
59
+ files.push(...collectFiles(fullPath));
60
+ }
61
+ else if (entry.isFile()) {
62
+ files.push(fullPath);
63
+ }
64
+ }
65
+ return files;
66
+ }
67
+ function resolveTemplateFile(typeRoot, domainName, type, nameSlug, localeSlug, filename) {
68
+ let tplname = '';
69
+ if (filename) {
70
+ const cleaned = filename.replace(/\\/g, '/');
71
+ const prefix = `${domainName}/${type}/`;
72
+ if (cleaned.startsWith(prefix)) {
73
+ tplname = cleaned.slice(prefix.length);
74
+ }
75
+ else if (cleaned.startsWith(`${type}/`)) {
76
+ tplname = cleaned.slice(type.length + 1);
77
+ }
78
+ else {
79
+ tplname = cleaned;
80
+ }
81
+ if (tplname.endsWith('.njk')) {
82
+ tplname = tplname.slice(0, -4);
83
+ }
84
+ }
85
+ else {
86
+ tplname = localeSlug ? node_path_1.default.join(localeSlug, nameSlug) : nameSlug;
87
+ }
88
+ const filePath = node_path_1.default.join(typeRoot, `${tplname}.njk`);
89
+ return { tplname: tplname.replace(/\\/g, '/'), filePath };
90
+ }
91
+ function resolveCssPath(rootDir, domainDir, typeRoot, override) {
92
+ if (override) {
93
+ return override;
94
+ }
95
+ const candidates = [
96
+ node_path_1.default.join(typeRoot, 'foundation-emails.css'),
97
+ node_path_1.default.join(domainDir, 'foundation-emails.css'),
98
+ node_path_1.default.join(rootDir, 'foundation-emails.css')
99
+ ];
100
+ for (const candidate of candidates) {
101
+ if (node_fs_1.default.existsSync(candidate)) {
102
+ return candidate;
103
+ }
104
+ }
105
+ return '';
106
+ }
107
+ function selectTemplateForAsset(templates, localeSlug, templateSlug) {
108
+ if (!templates.length) {
109
+ return null;
110
+ }
111
+ const byLocale = localeSlug
112
+ ? templates.filter((template) => template.localeSlug === localeSlug)
113
+ : templates.filter((template) => !template.localeSlug);
114
+ const candidates = byLocale.length ? byLocale : templates;
115
+ if (templateSlug) {
116
+ const exact = candidates.find((template) => template.nameSlug === templateSlug);
117
+ if (exact) {
118
+ return exact;
119
+ }
120
+ }
121
+ return candidates[0] ?? null;
122
+ }
123
+ async function uploadDomainAssets(uploader, domainName, assetsRoot, options) {
124
+ const files = collectFiles(assetsRoot);
125
+ const grouped = new Map();
126
+ for (const file of files) {
127
+ const relDir = node_path_1.default.relative(assetsRoot, node_path_1.default.dirname(file));
128
+ const key = relDir === '.' ? '' : relDir.split(node_path_1.default.sep).join('/');
129
+ const current = grouped.get(key) ?? [];
130
+ current.push(file);
131
+ grouped.set(key, current);
132
+ }
133
+ for (const [subdir, fileList] of grouped.entries()) {
134
+ if (options.dryRun) {
135
+ options.actions.push({
136
+ kind: 'domain-assets',
137
+ domain: domainName,
138
+ path: subdir || undefined,
139
+ files: fileList.map((file) => node_path_1.default.relative(options.inputRoot, file))
140
+ });
141
+ continue;
142
+ }
143
+ await uploader.uploadAssets({
144
+ domain: domainName,
145
+ files: fileList,
146
+ path: subdir || undefined
147
+ });
148
+ }
149
+ }
150
+ async function uploadTemplateAssets(uploader, domainName, type, typeRoot, templates, options) {
151
+ const files = collectFiles(typeRoot).filter((file) => {
152
+ if (file.endsWith('.njk')) {
153
+ return false;
154
+ }
155
+ return node_path_1.default.basename(file) !== 'foundation-emails.css';
156
+ });
157
+ if (!files.length) {
158
+ return;
159
+ }
160
+ const grouped = new Map();
161
+ for (const file of files) {
162
+ const rel = node_path_1.default.relative(typeRoot, file);
163
+ const parts = rel.split(node_path_1.default.sep).filter(Boolean);
164
+ if (!parts.length) {
165
+ continue;
166
+ }
167
+ let localeSlug = '';
168
+ let templateSlug = null;
169
+ if (parts.length && templates.some((template) => template.localeSlug === parts[0])) {
170
+ localeSlug = parts.shift() || '';
171
+ }
172
+ if (parts.length && templates.some((template) => template.nameSlug === parts[0])) {
173
+ templateSlug = parts.shift() || null;
174
+ }
175
+ const template = selectTemplateForAsset(templates, localeSlug, templateSlug);
176
+ if (!template) {
177
+ continue;
178
+ }
179
+ const relDirParts = parts.slice(0, -1);
180
+ const relDir = relDirParts.length ? relDirParts.join('/') : '';
181
+ const key = `${template.name}|${template.locale}|${relDir}`;
182
+ const entry = grouped.get(key) ?? { template, path: relDir, files: [] };
183
+ entry.files.push(file);
184
+ grouped.set(key, entry);
185
+ }
186
+ for (const entry of grouped.values()) {
187
+ if (options.dryRun) {
188
+ options.actions.push({
189
+ kind: 'template-assets',
190
+ domain: domainName,
191
+ template: entry.template.name,
192
+ locale: entry.template.locale,
193
+ path: entry.path || undefined,
194
+ files: entry.files.map((file) => node_path_1.default.relative(options.inputRoot, file))
195
+ });
196
+ continue;
197
+ }
198
+ await uploader.uploadAssets({
199
+ domain: domainName,
200
+ files: entry.files,
201
+ templateType: type,
202
+ template: entry.template.name,
203
+ locale: entry.template.locale,
204
+ path: entry.path || undefined
205
+ });
206
+ }
207
+ }
208
+ async function pushTemplate(options, client) {
209
+ if (!options.template) {
210
+ throw new Error('Template name is required');
211
+ }
212
+ if (!options.domain) {
213
+ throw new Error('Domain is required');
214
+ }
215
+ if (!options.api || !options.token) {
216
+ throw new Error('API URL and token are required');
217
+ }
218
+ const inputDir = options.input ?? './templates';
219
+ const cssPath = options.css ?? node_path_1.default.join(inputDir, 'foundation-emails.css');
220
+ const tplname = resolveTemplateName(options.template, inputDir);
221
+ const inputRoot = node_path_1.default.resolve(inputDir);
222
+ const templateFile = node_path_1.default.join(inputRoot, `${tplname}.njk`);
223
+ if (!node_fs_1.default.existsSync(templateFile)) {
224
+ throw new Error(`Template file not found: ${templateFile}`);
225
+ }
226
+ const compiled = await (0, preprocess_1.compileTemplate)({
227
+ src_dir: inputDir,
228
+ css_path: cssPath,
229
+ tplname
230
+ });
231
+ const name = options.name ?? node_path_1.default.basename(tplname);
232
+ const summary = {
233
+ domain: options.domain,
234
+ name,
235
+ locale: options.locale,
236
+ sender: options.sender,
237
+ subject: options.subject,
238
+ filePath: templateFile
239
+ };
240
+ if (!options.dryRun) {
241
+ const uploader = client ?? new mail_magic_client_1.default(options.api, options.token);
242
+ await uploader.storeTxTemplate({
243
+ template: compiled,
244
+ domain: options.domain,
245
+ name,
246
+ locale: options.locale,
247
+ sender: options.sender,
248
+ subject: options.subject
249
+ });
250
+ }
251
+ return summary;
252
+ }
253
+ async function pushTemplateDir(options, client) {
254
+ if (!options.api || !options.token) {
255
+ throw new Error('API URL and token are required');
256
+ }
257
+ const inputRoot = node_path_1.default.resolve(options.input ?? './data');
258
+ const initData = loadInitData(inputRoot);
259
+ const domains = initData.domain ?? [];
260
+ if (!domains.length) {
261
+ throw new Error('No domains found in init-data.json');
262
+ }
263
+ const requestedDomain = options.domain;
264
+ const domainNames = requestedDomain ? [requestedDomain] : domains.length === 1 ? [domains[0].name] : [];
265
+ if (!domainNames.length) {
266
+ throw new Error('Domain is required when init-data.json contains multiple domains');
267
+ }
268
+ const includeTx = options.includeTx ?? true;
269
+ const includeForms = options.includeForms ?? true;
270
+ const includeAssets = options.includeAssets ?? true;
271
+ const dryRun = options.dryRun ?? false;
272
+ const uploader = client ?? new mail_magic_client_1.default(options.api, options.token);
273
+ const actions = [];
274
+ for (const domainName of domainNames) {
275
+ const domainRecord = domains.find((domain) => domain.name === domainName);
276
+ if (!domainRecord) {
277
+ throw new Error(`Domain "${domainName}" not found in init-data.json`);
278
+ }
279
+ const domainDir = node_path_1.default.join(inputRoot, domainName);
280
+ if (!node_fs_1.default.existsSync(domainDir)) {
281
+ throw new Error(`Domain directory not found: ${domainDir}`);
282
+ }
283
+ const domainLocale = domainRecord.locale || '';
284
+ const txTemplates = (initData.template ?? []).filter((template) => template.domain_id === domainRecord.domain_id);
285
+ const formTemplates = (initData.form ?? []).filter((form) => form.domain_id === domainRecord.domain_id);
286
+ const normalizedTx = txTemplates.map((template) => {
287
+ const localeValue = template.locale || domainLocale || '';
288
+ return {
289
+ name: template.name,
290
+ nameSlug: normalizeSlug(template.name),
291
+ locale: localeValue,
292
+ localeSlug: normalizeSlug(localeValue)
293
+ };
294
+ });
295
+ const normalizedForms = formTemplates.map((form) => {
296
+ const localeValue = form.locale || domainLocale || '';
297
+ return {
298
+ name: form.idname,
299
+ nameSlug: normalizeSlug(form.idname),
300
+ locale: localeValue,
301
+ localeSlug: normalizeSlug(localeValue)
302
+ };
303
+ });
304
+ if (includeTx) {
305
+ const txRoot = node_path_1.default.join(domainDir, 'tx-template');
306
+ const cssPath = resolveCssPath(inputRoot, domainDir, txRoot, options.css);
307
+ for (const template of txTemplates) {
308
+ const localeValue = template.locale || domainLocale || '';
309
+ const localeSlug = normalizeSlug(localeValue);
310
+ const nameSlug = normalizeSlug(template.name);
311
+ const resolved = resolveTemplateFile(txRoot, domainName, 'tx-template', nameSlug, localeSlug, template.filename);
312
+ if (!node_fs_1.default.existsSync(resolved.filePath)) {
313
+ throw new Error(`Template file not found: ${resolved.filePath}`);
314
+ }
315
+ const compiled = await (0, preprocess_1.compileTemplate)({
316
+ src_dir: txRoot,
317
+ css_path: cssPath,
318
+ tplname: resolved.tplname
319
+ });
320
+ actions.push({
321
+ kind: 'tx-template',
322
+ domain: domainName,
323
+ template: template.name,
324
+ locale: localeValue
325
+ });
326
+ if (!dryRun) {
327
+ await uploader.storeTxTemplate({
328
+ template: compiled,
329
+ domain: domainName,
330
+ name: template.name,
331
+ locale: localeValue,
332
+ sender: template.sender,
333
+ subject: template.subject
334
+ });
335
+ }
336
+ }
337
+ }
338
+ if (includeForms) {
339
+ const formRoot = node_path_1.default.join(domainDir, 'form-template');
340
+ const cssPath = resolveCssPath(inputRoot, domainDir, formRoot, options.css);
341
+ for (const form of formTemplates) {
342
+ const localeValue = form.locale || domainLocale || '';
343
+ const localeSlug = normalizeSlug(localeValue);
344
+ const nameSlug = normalizeSlug(form.idname);
345
+ const resolved = resolveTemplateFile(formRoot, domainName, 'form-template', nameSlug, localeSlug, form.filename);
346
+ if (!node_fs_1.default.existsSync(resolved.filePath)) {
347
+ throw new Error(`Form template file not found: ${resolved.filePath}`);
348
+ }
349
+ const compiled = await (0, preprocess_1.compileTemplate)({
350
+ src_dir: formRoot,
351
+ css_path: cssPath,
352
+ tplname: resolved.tplname
353
+ });
354
+ actions.push({
355
+ kind: 'form-template',
356
+ domain: domainName,
357
+ template: form.idname,
358
+ locale: localeValue
359
+ });
360
+ if (!dryRun) {
361
+ await uploader.storeFormTemplate({
362
+ idname: form.idname,
363
+ domain: domainName,
364
+ template: compiled,
365
+ sender: form.sender,
366
+ recipient: form.recipient,
367
+ subject: form.subject,
368
+ locale: localeValue,
369
+ secret: form.secret
370
+ });
371
+ }
372
+ }
373
+ }
374
+ if (includeAssets) {
375
+ const assetsRoot = node_path_1.default.join(domainDir, 'assets');
376
+ if (node_fs_1.default.existsSync(assetsRoot)) {
377
+ await uploadDomainAssets(uploader, domainName, assetsRoot, {
378
+ dryRun,
379
+ inputRoot,
380
+ actions
381
+ });
382
+ }
383
+ const txRoot = node_path_1.default.join(domainDir, 'tx-template');
384
+ if (node_fs_1.default.existsSync(txRoot)) {
385
+ await uploadTemplateAssets(uploader, domainName, 'tx', txRoot, normalizedTx, {
386
+ dryRun,
387
+ inputRoot,
388
+ actions
389
+ });
390
+ }
391
+ const formRoot = node_path_1.default.join(domainDir, 'form-template');
392
+ if (node_fs_1.default.existsSync(formRoot)) {
393
+ await uploadTemplateAssets(uploader, domainName, 'form', formRoot, normalizedForms, {
394
+ dryRun,
395
+ inputRoot,
396
+ actions
397
+ });
398
+ }
399
+ }
400
+ }
401
+ const templates = actions.filter((action) => action.kind === 'tx-template').length;
402
+ const forms = actions.filter((action) => action.kind === 'form-template').length;
403
+ const assetBatches = actions.filter((action) => action.kind.includes('assets')).length;
404
+ return { templates, forms, assetBatches, actions };
405
+ }