@samva/vite 0.8.0 → 0.10.0

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.
@@ -1,603 +0,0 @@
1
- import { createEmailRenderer } from "./email.js";
2
- import { watch } from "node:fs";
3
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
- import { assetUrl, isAssetPath, parseBrandSpecifier, projectBrandSpecifier } from "@samva/markup/compiler";
6
- import { execFile } from "node:child_process";
7
- import { TEMPLATE_NAME_PATTERN, isTemplateDefinition, isTemplateDefinition as isTemplateDefinition$1 } from "@samva/markup/template";
8
- //#region src/internal/guards.ts
9
- /** A non-null, non-array object, narrowed for property access. */
10
- const isObject = (input) => typeof input === "object" && input !== null && !Array.isArray(input);
11
- //#endregion
12
- //#region src/brand.ts
13
- /** Where `samva brands pull` writes snapshots, relative to the project root. */
14
- const BRAND_SNAPSHOT_DIR = ".samva/brands";
15
- const SNAPSHOT_VERSION = 1;
16
- const DEFAULT_API_URL = "https://api.samva.dev";
17
- const LIVE_TIMEOUT_MS = 15e3;
18
- /** How long a resolution is reused, so a watch loop does not ask the API on every save. */
19
- const RESOLUTION_TTL_MS = 3e4;
20
- /** Accept only the fields a build needs, from an API body or a snapshot file. */
21
- const asBrand = (value) => {
22
- if (!isObject(value)) return void 0;
23
- const { slug, name, isDefault, css, assets, footer, digest } = value;
24
- if (typeof slug !== "string" || typeof name !== "string" || typeof isDefault !== "boolean" || typeof css !== "string" || typeof digest !== "string" || assets !== void 0 && !isObject(assets) || footer !== void 0 && !isObject(footer)) return void 0;
25
- return {
26
- slug,
27
- name,
28
- isDefault,
29
- css,
30
- digest,
31
- ...assets === void 0 ? {} : { assets },
32
- ...footer === void 0 ? {} : { footer }
33
- };
34
- };
35
- const liveFromBody = (body, when) => {
36
- const brand = asBrand(body);
37
- return brand === void 0 ? {
38
- ok: false,
39
- reason: `${when} answered with something that is not a brand`
40
- } : {
41
- ok: true,
42
- brand: {
43
- ...brand,
44
- version: SNAPSHOT_VERSION,
45
- pulledAt: ""
46
- }
47
- };
48
- };
49
- const fetchJson = async (url, apiKey) => {
50
- const response = await fetch(url, {
51
- headers: {
52
- "x-api-key": apiKey,
53
- accept: "application/json"
54
- },
55
- signal: AbortSignal.timeout(LIVE_TIMEOUT_MS)
56
- });
57
- if (!response.ok) throw new Error(`${url} answered ${response.status}`);
58
- return response.json();
59
- };
60
- const liveWithApiKey = async (slug, env, apiKey) => {
61
- const base = (env["SAMVA_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
62
- try {
63
- if (slug !== void 0) return liveFromBody(await fetchJson(`${base}/v1/brands/${slug}`, apiKey), "The Samva API");
64
- const body = await fetchJson(`${base}/v1/brands`, apiKey);
65
- const found = (isObject(body) && Array.isArray(body["items"]) ? body["items"] : []).find((item) => isObject(item) && item["isDefault"] === true);
66
- return found === void 0 ? {
67
- ok: false,
68
- reason: "the organization has no default brand"
69
- } : liveFromBody(found, "The Samva API");
70
- } catch (cause) {
71
- return {
72
- ok: false,
73
- reason: `the Samva API did not answer (${cause instanceof Error ? cause.message : String(cause)})`
74
- };
75
- }
76
- };
77
- /** The CLI's error envelope message, or its raw stderr. */
78
- const cliFailure = (stderr) => {
79
- const parsed = (() => {
80
- try {
81
- return JSON.parse(stderr);
82
- } catch {
83
- return;
84
- }
85
- })();
86
- return isObject(parsed) && typeof parsed["message"] === "string" ? parsed["message"] : stderr.trim() || "it exited without a message";
87
- };
88
- const liveWithCli = (slug, env) => new Promise((resolve) => {
89
- execFile("samva", [
90
- "brands",
91
- "get",
92
- ...slug === void 0 ? [] : [slug],
93
- "--json"
94
- ], {
95
- env: { ...env },
96
- timeout: LIVE_TIMEOUT_MS,
97
- maxBuffer: 1048576
98
- }, (error, stdout, stderr) => {
99
- if (error !== null) {
100
- const code = error.code;
101
- resolve({
102
- ok: false,
103
- reason: code === "ENOENT" ? "there is no SAMVA_API_KEY and no `samva` CLI on PATH" : `\`samva brands get\` failed: ${cliFailure(stderr)}`
104
- });
105
- return;
106
- }
107
- const envelope = (() => {
108
- try {
109
- return JSON.parse(stdout);
110
- } catch {
111
- return;
112
- }
113
- })();
114
- resolve(liveFromBody(isObject(envelope) ? envelope["data"] : void 0, "`samva brands get`"));
115
- });
116
- });
117
- const fetchLiveBrand = (slug, env) => {
118
- const apiKey = env["SAMVA_API_KEY"];
119
- return apiKey !== void 0 && apiKey.length > 0 ? liveWithApiKey(slug, env, apiKey) : liveWithCli(slug, env);
120
- };
121
- const readSnapshotFile = async (path) => {
122
- const raw = await readFile(path, "utf8").catch((cause) => {
123
- if (cause.code === "ENOENT") return void 0;
124
- throw cause;
125
- });
126
- if (raw === void 0) return {
127
- ok: true,
128
- snapshot: void 0,
129
- path
130
- };
131
- const parsed = (() => {
132
- try {
133
- return JSON.parse(raw);
134
- } catch {
135
- return;
136
- }
137
- })();
138
- const brand = asBrand(parsed);
139
- if (brand === void 0 || !isObject(parsed) || parsed["version"] !== SNAPSHOT_VERSION || typeof parsed["pulledAt"] !== "string") return {
140
- ok: false,
141
- message: `${path} is not a brand snapshot this version of @samva/vite reads. Run \`samva brands pull\` to rewrite it.`
142
- };
143
- return {
144
- ok: true,
145
- snapshot: {
146
- ...brand,
147
- version: SNAPSHOT_VERSION,
148
- pulledAt: parsed["pulledAt"]
149
- },
150
- path
151
- };
152
- };
153
- /**
154
- * The snapshot for a named brand, or for the default brand the most recently
155
- * pulled snapshot that was the default when it was pulled.
156
- */
157
- const readSnapshot = async (root, slug) => {
158
- const directory = join(root, BRAND_SNAPSHOT_DIR);
159
- if (slug !== void 0) return readSnapshotFile(join(directory, `${slug}.json`));
160
- const names = await readdir(directory).catch((cause) => {
161
- if (cause.code === "ENOENT") return [];
162
- throw cause;
163
- });
164
- const reads = await Promise.all(names.filter((name) => name.endsWith(".json")).sort().map((name) => readSnapshotFile(join(directory, name))));
165
- const failed = reads.find((read) => !read.ok);
166
- if (failed !== void 0) return failed;
167
- const defaults = reads.flatMap((read) => read.ok && read.snapshot?.isDefault === true ? [{
168
- ...read,
169
- snapshot: read.snapshot
170
- }] : []);
171
- defaults.sort((a, b) => b.snapshot.pulledAt.localeCompare(a.snapshot.pulledAt));
172
- return defaults[0] ?? {
173
- ok: true,
174
- snapshot: void 0,
175
- path: directory
176
- };
177
- };
178
- const toResolved = (brand) => ({
179
- slug: brand.slug,
180
- css: brand.css,
181
- digest: brand.digest,
182
- ...brand.assets === void 0 ? {} : { assets: brand.assets },
183
- ...brand.footer === void 0 ? {} : { footer: brand.footer }
184
- });
185
- const relativeSnapshot = (root, path) => relative(root, path).split(sep).join("/");
186
- /**
187
- * Resolve the brand a project's `samva:brand` import names: live when the
188
- * environment's credentials reach the API, else the pulled snapshot with a
189
- * warning, else a failure naming both fixes.
190
- */
191
- const resolveProjectBrand = async (input) => {
192
- const specifier = projectBrandSpecifier(input.files, input.themePath ?? "");
193
- if (specifier === void 0) return {
194
- ok: true,
195
- brand: void 0,
196
- source: "none",
197
- warnings: []
198
- };
199
- const slug = parseBrandSpecifier(specifier)?.slug;
200
- const pullCommand = `samva brands pull${slug === void 0 ? "" : ` ${slug}`}`;
201
- const [live, local] = await Promise.all([fetchLiveBrand(slug, input.env), readSnapshot(input.root, slug)]);
202
- if (live.ok) {
203
- const stale = local.ok && local.snapshot !== void 0 && local.snapshot.digest !== live.brand.digest;
204
- return {
205
- ok: true,
206
- brand: toResolved(live.brand),
207
- source: "live",
208
- warnings: stale ? [{
209
- code: "brand-snapshot-stale",
210
- message: `${relativeSnapshot(input.root, local.path)} is older than the live brand ${live.brand.slug}. Run \`${pullCommand}\` so offline builds match it.`
211
- }] : []
212
- };
213
- }
214
- if (!local.ok) return {
215
- ok: false,
216
- message: local.message
217
- };
218
- if (local.snapshot === void 0) return {
219
- ok: false,
220
- message: `The project imports ${specifier}, but ${live.reason}, and there is no snapshot in ${BRAND_SNAPSHOT_DIR}. Run \`samva login\` or set SAMVA_API_KEY, or run \`${pullCommand}\` while online.`
221
- };
222
- return {
223
- ok: true,
224
- brand: toResolved(local.snapshot),
225
- source: "snapshot",
226
- warnings: [{
227
- code: "brand-offline",
228
- message: `Using ${relativeSnapshot(input.root, local.path)} (pulled ${local.snapshot.pulledAt}) for ${specifier}: ${live.reason}.`
229
- }]
230
- };
231
- };
232
- /**
233
- * The environment a local build resolves the brand with: the process
234
- * environment plus the project's `.env` files, as the editor's dev server
235
- * reads them, so `SAMVA_API_KEY` in `.env` reaches `check` and `export` too.
236
- */
237
- const loadBrandEnv = async (root) => {
238
- const vite = await import("vite").catch(() => void 0);
239
- if (vite === void 0) throw new Error("`vite` is required to resolve samva:brand locally.");
240
- return vite.loadEnv("development", root, "");
241
- };
242
- /** The resolution as build findings: warnings stay warnings, a failure is an error. */
243
- const brandDiagnostics = (resolution) => resolution.ok ? resolution.warnings.map((warning) => ({
244
- code: warning.code,
245
- severity: "warning",
246
- message: warning.message,
247
- origins: []
248
- })) : [{
249
- code: "brand-unavailable",
250
- severity: "error",
251
- message: resolution.message,
252
- origins: []
253
- }];
254
- /**
255
- * One resolver per watch session. A resolution is reused for a short while per
256
- * specifier, so saving a file does not ask the API each time while a brand
257
- * edited elsewhere still arrives within the window.
258
- */
259
- const createBrandResolver = (options) => {
260
- const cache = /* @__PURE__ */ new Map();
261
- return { resolve: async (files, themePath) => {
262
- const key = projectBrandSpecifier(files, themePath ?? "") ?? "";
263
- const cached = cache.get(key);
264
- if (cached !== void 0 && Date.now() - cached.at < RESOLUTION_TTL_MS) return cached.resolution;
265
- const resolution = await resolveProjectBrand({
266
- root: options.root,
267
- files,
268
- themePath,
269
- env: options.env
270
- });
271
- cache.set(key, {
272
- at: Date.now(),
273
- resolution
274
- });
275
- return resolution;
276
- } };
277
- };
278
- /**
279
- * Resolves the `theme` plugin option to an absolute path, or `undefined` when
280
- * theming is explicitly disabled (`theme: false`). A string is taken relative to
281
- * `root` (absolute strings pass through); the default is `<root>/theme.css`.
282
- */
283
- const resolveThemePath = (root, option) => {
284
- if (option === false) return void 0;
285
- const file = option ?? "theme.css";
286
- return isAbsolute(file) ? file : resolve(root, file);
287
- };
288
- /** Read authored Tailwind CSS verbatim; the email compiler validates its syntax. */
289
- const readProjectThemeCss = async (path) => path === void 0 ? void 0 : readFile(path, "utf8").catch((cause) => {
290
- if (cause.code === "ENOENT") return void 0;
291
- throw cause;
292
- });
293
- /** The theme's project-relative path, from which its relative `@import`s resolve. */
294
- const projectThemePath = (root, themePath) => relative(root, themePath).split(sep).join("/");
295
- //#endregion
296
- //#region src/project.ts
297
- const toPosix = (value) => sep === "/" ? value : value.split(sep).join("/");
298
- const isCandidateEntry = (path) => path.endsWith(".tsx");
299
- const diagnostic = (code, severity, message) => ({
300
- code,
301
- severity,
302
- message,
303
- origins: []
304
- });
305
- /**
306
- * Read a project directory into the source map the compiler consumes. Text
307
- * modules arrive as strings and imported files as bytes, which is the same
308
- * shape the hosted build hands `compileEmailProject`.
309
- */
310
- const readEmailProject = async (dir) => {
311
- const root = resolve(dir);
312
- const files = {};
313
- const walk = async (current) => {
314
- const entries = await readdir(current, { withFileTypes: true });
315
- await Promise.all(entries.filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules").map(async (entry) => {
316
- const full = join(current, entry.name);
317
- if (entry.isDirectory()) return walk(full);
318
- if (!entry.isFile()) return;
319
- const path = toPosix(relative(root, full));
320
- files[path] = isAssetPath(path) ? new Uint8Array(await readFile(full)) : await readFile(full, "utf8");
321
- }));
322
- };
323
- await walk(root);
324
- return files;
325
- };
326
- /**
327
- * Compile and evaluate every candidate entry, keeping the ones that declare a
328
- * template. Compilation is per entry, exactly as a publication is per template,
329
- * so shared components and CSS reach each template the same way they reach the
330
- * hosted build.
331
- */
332
- const loadEmailCatalog = async (input) => {
333
- const candidates = (input.entryPaths ?? Object.keys(input.files).filter(isCandidateEntry)).slice();
334
- candidates.sort();
335
- const templates = [];
336
- const helpers = [];
337
- const diagnostics = [];
338
- const assets = /* @__PURE__ */ new Map();
339
- const declaredBy = /* @__PURE__ */ new Map();
340
- const loaded = await Promise.all(candidates.map((entryPath) => loadEntry(input.renderer, input.files, entryPath, input.stableNamespaces === true ? entryPath : void 0)));
341
- for (const [index, entryPath] of candidates.entries()) {
342
- const result = loaded[index];
343
- if (!result.ok) {
344
- diagnostics.push(diagnostic("entry-load-error", "error", `${entryPath}: ${result.message}`));
345
- continue;
346
- }
347
- const template = result.entry.template;
348
- if (template === void 0) {
349
- helpers.push(entryPath);
350
- continue;
351
- }
352
- if (!isTemplateDefinition(template)) {
353
- helpers.push(entryPath);
354
- diagnostics.push(diagnostic("not-a-template", "warning", `${entryPath} default-exports something that is not a template definition. Wrap it with defineEmail, or drop the default export so it reads as a helper module.`));
355
- continue;
356
- }
357
- const previous = declaredBy.get(template.id);
358
- if (previous !== void 0) {
359
- diagnostics.push(diagnostic("duplicate-template-id", "error", `Template id ${JSON.stringify(template.id)} is declared by both ${previous} and ${entryPath}. An id is a publication's identity, so it must name exactly one template.`));
360
- continue;
361
- }
362
- declaredBy.set(template.id, entryPath);
363
- for (const asset of result.entry.compiled.assets) assets.set(asset.digest, asset);
364
- diagnostics.push(...result.entry.compiled.diagnostics);
365
- templates.push({
366
- id: template.id,
367
- entryPath,
368
- definition: template,
369
- loaded: result.entry
370
- });
371
- }
372
- return {
373
- templates: templates.sort((left, right) => left.id < right.id ? -1 : 1),
374
- helpers,
375
- diagnostics,
376
- assets: [...assets.values()].sort((left, right) => left.fileName < right.fileName ? -1 : 1)
377
- };
378
- };
379
- const loadEntry = async (renderer, files, entryPath, namespace) => renderer.load(files, entryPath, namespace).then((entry) => ({
380
- ok: true,
381
- entry
382
- }), (cause) => ({
383
- ok: false,
384
- message: cause instanceof Error ? cause.message : String(cause)
385
- }));
386
- /**
387
- * Validate and render every fixture a template declares. A fixture that its own
388
- * schema rejects is a source defect the author sees here rather than at publish.
389
- */
390
- const renderEmailFixtures = (entry, options = {}) => Object.entries(entry.definition.fixtures).map(([fixture, data]) => {
391
- if (!entry.definition.schema.validate(data)) return {
392
- templateId: entry.id,
393
- fixture,
394
- rendered: void 0,
395
- diagnostics: [diagnostic("invalid-fixture", "error", `Fixture ${JSON.stringify(fixture)} does not satisfy ${entry.id}'s own input schema: ${JSON.stringify(entry.definition.schema.validate.errors)}`)]
396
- };
397
- try {
398
- const rendered = entry.loaded.renderEmail(entry.definition, data, options);
399
- return {
400
- templateId: entry.id,
401
- fixture,
402
- rendered,
403
- diagnostics: rendered.diagnostics
404
- };
405
- } catch (cause) {
406
- return {
407
- templateId: entry.id,
408
- fixture,
409
- rendered: void 0,
410
- diagnostics: findingsOf(cause, entry.id, fixture)
411
- };
412
- }
413
- });
414
- const findingsOf = (cause, templateId, fixture) => {
415
- const carried = cause.diagnostics;
416
- if (Array.isArray(carried) && carried.length > 0) return carried;
417
- return [diagnostic("render-failed", "error", `${templateId} could not render fixture ${JSON.stringify(fixture)}: ${cause instanceof Error ? cause.message : String(cause)}`)];
418
- };
419
- /**
420
- * The authoring loop: one evaluator, one catalog, reloaded on change. Content
421
- * semantics come from `compileEmailProject` and `renderEmail`, the same two
422
- * calls the hosted preview makes, so what an author iterates on locally is what
423
- * a publication renders.
424
- */
425
- const createEmailProjectSession = async (dir, options) => {
426
- const root = resolve(dir);
427
- const renderer = await createEmailRenderer(options);
428
- let catalog = {
429
- templates: [],
430
- helpers: [],
431
- diagnostics: [],
432
- assets: []
433
- };
434
- const listeners = /* @__PURE__ */ new Set();
435
- const observer = watch(root, { recursive: true }, (_event, name) => {
436
- const changed = name === null ? root : toPosix(name.toString());
437
- for (const listener of listeners) listener(changed);
438
- });
439
- const reload = async () => {
440
- renderer.invalidate();
441
- catalog = await loadEmailCatalog({
442
- renderer,
443
- files: await readEmailProject(root),
444
- stableNamespaces: true
445
- });
446
- return catalog;
447
- };
448
- return {
449
- reload,
450
- current: () => catalog,
451
- subscribe: (listener) => {
452
- listeners.add(listener);
453
- return () => void listeners.delete(listener);
454
- },
455
- close: async () => {
456
- observer.close();
457
- listeners.clear();
458
- await renderer.close();
459
- }
460
- };
461
- };
462
- /**
463
- * Where an export serves its assets from when the caller names no origin.
464
- *
465
- * An export writes one page at `templates/<id>/<fixture>.html` and every asset
466
- * once at `assets/<content address>`, so the base a page resolves against is
467
- * the path from its own directory back to that one. A template id is one path
468
- * segment, so every page sits at the same depth and one base serves them all.
469
- */
470
- const DEFAULT_EXPORT_ASSET_BASE = "../../assets";
471
- /**
472
- * The path an export writes, or `undefined` when a name would leave the root.
473
- *
474
- * `defineEmail` already refuses an id or a fixture key that is not one path
475
- * segment, so this is the guard rather than the contract: the same pattern,
476
- * plus a containment check, so a name that reaches `join` can never address a
477
- * file the export does not own.
478
- */
479
- const exportPath = (out, ...segments) => {
480
- if (!segments.every((segment) => TEMPLATE_NAME_PATTERN.test(segment))) return void 0;
481
- const path = join(out, ...segments);
482
- const inside = relative(out, path);
483
- return inside === "" || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside) ? void 0 : path;
484
- };
485
- const unsafeExportName = (kind, value) => diagnostic("unsafe-export-name", "error", `${kind} ${JSON.stringify(value)} is not one path segment, so an export cannot write a file for it. Use letters, digits, ".", "_" and "-", starting with a letter, digit or "_".`);
486
- /**
487
- * Render every template's fixtures to HTML and text beside the bytes of every
488
- * asset they reference, with the manifest that maps each address to its file.
489
- *
490
- * The export renders in the compiler's export mode, so a relative `assetBase`
491
- * is accepted and the written page resolves its images from the directory it
492
- * sits in. Delivery keeps refusing relative references — an inbox has no
493
- * directory to resolve one against — so an export destined for sending names
494
- * the https origin it will be served from instead.
495
- */
496
- const exportEmailProject = async (input) => {
497
- const out = resolve(input.out);
498
- const root = resolve(input.root ?? input.dir);
499
- const files = await readEmailProject(root);
500
- const templatesPrefix = toPosix(relative(root, resolve(input.dir)));
501
- const entryPaths = templatesPrefix === "" ? void 0 : Object.keys(files).filter((path) => isCandidateEntry(path) && path.startsWith(`${templatesPrefix}/`));
502
- const base = input.assetBase ?? "../../assets";
503
- const themePath = resolveThemePath(root, input.theme);
504
- const themeCss = await readProjectThemeCss(themePath);
505
- const themeFile = themeCss === void 0 || themePath === void 0 ? void 0 : projectThemePath(root, themePath);
506
- const brandResolution = await resolveProjectBrand({
507
- root,
508
- files: themeFile === void 0 || themeCss === void 0 ? files : {
509
- ...files,
510
- [themeFile]: themeCss
511
- },
512
- themePath: themeFile,
513
- env: await loadBrandEnv(root)
514
- });
515
- const renderer = await createEmailRenderer({
516
- assetBase: base,
517
- ...themeCss === void 0 || themeFile === void 0 ? {} : { tailwind: {
518
- css: themeCss,
519
- cssPath: themeFile
520
- } },
521
- ...brandResolution.ok && brandResolution.brand !== void 0 ? { brand: brandResolution.brand } : {}
522
- });
523
- try {
524
- const catalog = await loadEmailCatalog({
525
- renderer,
526
- files,
527
- entryPaths
528
- });
529
- const diagnostics = [...brandDiagnostics(brandResolution), ...catalog.diagnostics];
530
- const written = await Promise.all(catalog.templates.map(async (entry) => {
531
- const directory = exportPath(out, "templates", entry.id);
532
- if (directory === void 0) return {
533
- entry: void 0,
534
- diagnostics: [unsafeExportName("Template id", entry.id)]
535
- };
536
- await mkdir(directory, { recursive: true });
537
- const rendered = renderEmailFixtures(entry, { relativeUrls: true });
538
- const pages = await Promise.all(rendered.map(async (result) => {
539
- if (result.rendered === void 0) return {
540
- fixture: void 0,
541
- diagnostics: []
542
- };
543
- const htmlPath = exportPath(out, "templates", entry.id, `${result.fixture}.html`);
544
- const textPath = exportPath(out, "templates", entry.id, `${result.fixture}.txt`);
545
- if (htmlPath === void 0 || textPath === void 0) return {
546
- fixture: void 0,
547
- diagnostics: [unsafeExportName("Fixture name", result.fixture)]
548
- };
549
- await Promise.all([writeFile(htmlPath, result.rendered.html, "utf8"), writeFile(textPath, result.rendered.text, "utf8")]);
550
- return {
551
- fixture: {
552
- fixture: result.fixture,
553
- html: toPosix(relative(out, htmlPath)),
554
- text: toPosix(relative(out, textPath))
555
- },
556
- diagnostics: []
557
- };
558
- }));
559
- return {
560
- entry: {
561
- id: entry.id,
562
- entryPath: entry.entryPath,
563
- fixtures: pages.map((result) => result.fixture).filter((fixture) => fixture !== void 0)
564
- },
565
- diagnostics: [...rendered.flatMap((result) => result.diagnostics), ...pages.flatMap((result) => result.diagnostics)]
566
- };
567
- }));
568
- const templates = written.map((result) => result.entry).filter((entry) => entry !== void 0);
569
- diagnostics.push(...written.flatMap((result) => result.diagnostics));
570
- const assetDirectory = join(out, "assets");
571
- if (catalog.assets.length > 0) await mkdir(assetDirectory, { recursive: true });
572
- await Promise.all(catalog.assets.map((asset) => {
573
- const bytes = files[asset.path];
574
- return bytes === void 0 || typeof bytes === "string" ? Promise.resolve() : writeFile(join(assetDirectory, asset.fileName), bytes);
575
- }));
576
- const receipt = {
577
- templates,
578
- assets: {
579
- base,
580
- assets: catalog.assets
581
- },
582
- diagnostics
583
- };
584
- await mkdir(out, { recursive: true });
585
- await writeFile(join(out, "catalog.json"), `${JSON.stringify({
586
- ...receipt,
587
- assets: {
588
- base,
589
- assets: catalog.assets.map((asset) => ({
590
- ...asset,
591
- url: assetUrl(asset, base)
592
- }))
593
- }
594
- }, null, 2)}\n`, "utf8");
595
- return receipt;
596
- } finally {
597
- await renderer.close();
598
- }
599
- };
600
- //#endregion
601
- export { loadEmailCatalog as a, projectThemePath as c, BRAND_SNAPSHOT_DIR as d, brandDiagnostics as f, isObject as g, resolveProjectBrand as h, isTemplateDefinition$1 as i, readProjectThemeCss as l, loadBrandEnv as m, createEmailProjectSession as n, readEmailProject as o, createBrandResolver as p, exportEmailProject as r, renderEmailFixtures as s, DEFAULT_EXPORT_ASSET_BASE as t, resolveThemePath as u };
602
-
603
- //# sourceMappingURL=project-RON2Ei7d.js.map