@samva/vite 0.7.0 → 0.9.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.
package/dist/project.d.ts CHANGED
@@ -1,116 +1,2 @@
1
- import { EmailProjectSource, EmailRenderer, LoadedEmailEntry } from "./email.js";
2
- import { AssetEntry, RenderedEmail, TransformOptions } from "@samva/markup/compiler";
3
- import { TemplateDefinition, isTemplateDefinition } from "@samva/markup/template";
4
- import { EmailDiagnostic } from "@samva/markup/email/diagnostics";
5
- import { SerializeEmailOptions } from "@samva/markup/email/html";
6
- //#region src/project.d.ts
7
- /**
8
- * Read a project directory into the source map the compiler consumes. Text
9
- * modules arrive as strings and imported files as bytes, which is the same
10
- * shape the hosted build hands `compileEmailProject`.
11
- */
12
- declare const readEmailProject: (dir: string) => Promise<EmailProjectSource>;
13
- /** One discovered template: its declared identity and the entry that declares it. */
14
- interface EmailCatalogEntry {
15
- /** The identity the definition declares for itself. */
16
- readonly id: string;
17
- /** Project-relative path of the authored entry. */
18
- readonly entryPath: string;
19
- readonly definition: TemplateDefinition<unknown>;
20
- readonly loaded: LoadedEmailEntry;
21
- }
22
- interface EmailCatalog {
23
- readonly templates: readonly EmailCatalogEntry[];
24
- /** Candidate entries that turned out to be ordinary modules. */
25
- readonly helpers: readonly string[];
26
- readonly diagnostics: readonly EmailDiagnostic[];
27
- /** Every asset the discovered templates import, deduplicated by content. */
28
- readonly assets: readonly AssetEntry[];
29
- }
30
- /**
31
- * Compile and evaluate every candidate entry, keeping the ones that declare a
32
- * template. Compilation is per entry, exactly as a publication is per template,
33
- * so shared components and CSS reach each template the same way they reach the
34
- * hosted build.
35
- */
36
- declare const loadEmailCatalog: (input: {
37
- readonly renderer: EmailRenderer;
38
- readonly files: EmailProjectSource;
39
- readonly entryPaths?: readonly string[] | undefined;
40
- /**
41
- * Reuse one id namespace per entry so a reload replaces its modules instead
42
- * of accumulating a new namespace per pass. A watch session sets this; a
43
- * one-shot load leaves each entry isolated.
44
- */
45
- readonly stableNamespaces?: boolean | undefined;
46
- }) => Promise<EmailCatalog>;
47
- /** One fixture rendered through the template it belongs to. */
48
- interface RenderedFixture {
49
- readonly templateId: string;
50
- readonly fixture: string;
51
- readonly rendered: RenderedEmail | undefined;
52
- readonly diagnostics: readonly EmailDiagnostic[];
53
- }
54
- /**
55
- * Validate and render every fixture a template declares. A fixture that its own
56
- * schema rejects is a source defect the author sees here rather than at publish.
57
- */
58
- declare const renderEmailFixtures: (entry: EmailCatalogEntry, options?: SerializeEmailOptions) => readonly RenderedFixture[];
59
- /** A live catalog over a project directory, reloaded when its source changes. */
60
- interface EmailProjectSession {
61
- readonly reload: () => Promise<EmailCatalog>;
62
- readonly current: () => EmailCatalog;
63
- readonly subscribe: (listener: (path: string) => void) => () => void;
64
- readonly close: () => Promise<void>;
65
- }
66
- /**
67
- * The authoring loop: one evaluator, one catalog, reloaded on change. Content
68
- * semantics come from `compileEmailProject` and `renderEmail`, the same two
69
- * calls the hosted preview makes, so what an author iterates on locally is what
70
- * a publication renders.
71
- */
72
- declare const createEmailProjectSession: (dir: string, options: TransformOptions) => Promise<EmailProjectSession>;
73
- interface EmailExportReceipt {
74
- readonly templates: readonly {
75
- readonly id: string;
76
- readonly entryPath: string;
77
- readonly fixtures: readonly {
78
- readonly fixture: string;
79
- readonly html: string;
80
- readonly text: string;
81
- }[];
82
- }[];
83
- readonly assets: {
84
- readonly base: string;
85
- readonly assets: readonly AssetEntry[];
86
- };
87
- readonly diagnostics: readonly EmailDiagnostic[];
88
- }
89
- /**
90
- * Where an export serves its assets from when the caller names no origin.
91
- *
92
- * An export writes one page at `templates/<id>/<fixture>.html` and every asset
93
- * once at `assets/<content address>`, so the base a page resolves against is
94
- * the path from its own directory back to that one. A template id is one path
95
- * segment, so every page sits at the same depth and one base serves them all.
96
- */
97
- declare const DEFAULT_EXPORT_ASSET_BASE = "../../assets";
98
- /**
99
- * Render every template's fixtures to HTML and text beside the bytes of every
100
- * asset they reference, with the manifest that maps each address to its file.
101
- *
102
- * The export renders in the compiler's export mode, so a relative `assetBase`
103
- * is accepted and the written page resolves its images from the directory it
104
- * sits in. Delivery keeps refusing relative references — an inbox has no
105
- * directory to resolve one against — so an export destined for sending names
106
- * the https origin it will be served from instead.
107
- */
108
- declare const exportEmailProject: (input: {
109
- readonly dir: string;
110
- readonly out: string;
111
- readonly assetBase?: string | undefined;
112
- readonly tailwind?: TransformOptions["tailwind"] | undefined;
113
- }) => Promise<EmailExportReceipt>;
114
- //#endregion
115
- export { DEFAULT_EXPORT_ASSET_BASE, EmailCatalog, EmailCatalogEntry, EmailExportReceipt, EmailProjectSession, RenderedFixture, createEmailProjectSession, exportEmailProject, isTemplateDefinition, loadEmailCatalog, readEmailProject, renderEmailFixtures };
116
- //# sourceMappingURL=project.d.ts.map
1
+ import { A as loadBrandEnv, C as renderChannelFixture, D as BrandSnapshot, E as BrandResolution, F as discoverEntries, I as readProjectFiles, L as templateDirectories, M as DEFAULT_TEMPLATE_DIRS, N as ProjectFiles, O as BrandWarning, P as SAMVA_DIR, S as hasErrors, T as BRAND_SNAPSHOT_DIR, _ as TemplateChannel, a as ProjectOptions, b as emailCompatibility, c as exportProject, d as CompileProjectInput, f as CompiledEntry, g as RenderedFixture, h as RenderedChannel, i as ProjectExportReceipt, j as resolveProjectBrand, k as ResolvedBrand, l as loadProject, m as FailedEntry, n as ExportedFixture, o as ProjectSession, p as CompiledProject, r as LoadedProject, s as createProjectSession, t as DEFAULT_EXPORT_ASSET_BASE, u as projectRoot, v as channelsOf, w as renderEmailFixtures, x as finding, y as compileProject } from "./project-CiUuzKsC.js";
2
+ export { BRAND_SNAPSHOT_DIR, type BrandResolution, type BrandSnapshot, type BrandWarning, CompileProjectInput, CompiledEntry, CompiledProject, DEFAULT_EXPORT_ASSET_BASE, DEFAULT_TEMPLATE_DIRS, ExportedFixture, FailedEntry, LoadedProject, ProjectExportReceipt, type ProjectFiles, ProjectOptions, ProjectSession, RenderedChannel, RenderedFixture, type ResolvedBrand, SAMVA_DIR, TemplateChannel, channelsOf, compileProject, createProjectSession, discoverEntries, emailCompatibility, exportProject, finding, hasErrors, loadBrandEnv, loadProject, projectRoot, readProjectFiles, renderChannelFixture, renderEmailFixtures, resolveProjectBrand, templateDirectories };
package/dist/project.js CHANGED
@@ -1,294 +1,2 @@
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 } from "@samva/markup/compiler";
6
- import { TEMPLATE_NAME_PATTERN, isTemplateDefinition, isTemplateDefinition as isTemplateDefinition$1 } from "@samva/markup/template";
7
- //#region src/project.ts
8
- const toPosix = (value) => sep === "/" ? value : value.split(sep).join("/");
9
- const isCandidateEntry = (path) => path.endsWith(".tsx");
10
- const diagnostic = (code, severity, message) => ({
11
- code,
12
- severity,
13
- message,
14
- origins: []
15
- });
16
- /**
17
- * Read a project directory into the source map the compiler consumes. Text
18
- * modules arrive as strings and imported files as bytes, which is the same
19
- * shape the hosted build hands `compileEmailProject`.
20
- */
21
- const readEmailProject = async (dir) => {
22
- const root = resolve(dir);
23
- const files = {};
24
- const walk = async (current) => {
25
- const entries = await readdir(current, { withFileTypes: true });
26
- await Promise.all(entries.filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules").map(async (entry) => {
27
- const full = join(current, entry.name);
28
- if (entry.isDirectory()) return walk(full);
29
- if (!entry.isFile()) return;
30
- const path = toPosix(relative(root, full));
31
- files[path] = isAssetPath(path) ? new Uint8Array(await readFile(full)) : await readFile(full, "utf8");
32
- }));
33
- };
34
- await walk(root);
35
- return files;
36
- };
37
- /**
38
- * Compile and evaluate every candidate entry, keeping the ones that declare a
39
- * template. Compilation is per entry, exactly as a publication is per template,
40
- * so shared components and CSS reach each template the same way they reach the
41
- * hosted build.
42
- */
43
- const loadEmailCatalog = async (input) => {
44
- const candidates = (input.entryPaths ?? Object.keys(input.files).filter(isCandidateEntry)).slice();
45
- candidates.sort();
46
- const templates = [];
47
- const helpers = [];
48
- const diagnostics = [];
49
- const assets = /* @__PURE__ */ new Map();
50
- const declaredBy = /* @__PURE__ */ new Map();
51
- const loaded = await Promise.all(candidates.map((entryPath) => loadEntry(input.renderer, input.files, entryPath, input.stableNamespaces === true ? entryPath : void 0)));
52
- for (const [index, entryPath] of candidates.entries()) {
53
- const result = loaded[index];
54
- if (!result.ok) {
55
- diagnostics.push(diagnostic("entry-load-error", "error", `${entryPath}: ${result.message}`));
56
- continue;
57
- }
58
- const template = result.entry.template;
59
- if (template === void 0) {
60
- helpers.push(entryPath);
61
- continue;
62
- }
63
- if (!isTemplateDefinition$1(template)) {
64
- helpers.push(entryPath);
65
- 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.`));
66
- continue;
67
- }
68
- const previous = declaredBy.get(template.id);
69
- if (previous !== void 0) {
70
- 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.`));
71
- continue;
72
- }
73
- declaredBy.set(template.id, entryPath);
74
- for (const asset of result.entry.compiled.assets) assets.set(asset.digest, asset);
75
- diagnostics.push(...result.entry.compiled.diagnostics);
76
- templates.push({
77
- id: template.id,
78
- entryPath,
79
- definition: template,
80
- loaded: result.entry
81
- });
82
- }
83
- return {
84
- templates: templates.sort((left, right) => left.id < right.id ? -1 : 1),
85
- helpers,
86
- diagnostics,
87
- assets: [...assets.values()].sort((left, right) => left.fileName < right.fileName ? -1 : 1)
88
- };
89
- };
90
- const loadEntry = async (renderer, files, entryPath, namespace) => renderer.load(files, entryPath, namespace).then((entry) => ({
91
- ok: true,
92
- entry
93
- }), (cause) => ({
94
- ok: false,
95
- message: cause instanceof Error ? cause.message : String(cause)
96
- }));
97
- /**
98
- * Validate and render every fixture a template declares. A fixture that its own
99
- * schema rejects is a source defect the author sees here rather than at publish.
100
- */
101
- const renderEmailFixtures = (entry, options = {}) => Object.entries(entry.definition.fixtures).map(([fixture, data]) => {
102
- if (!entry.definition.schema.validate(data)) return {
103
- templateId: entry.id,
104
- fixture,
105
- rendered: void 0,
106
- 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)}`)]
107
- };
108
- try {
109
- const rendered = entry.loaded.renderEmail(entry.definition, data, options);
110
- return {
111
- templateId: entry.id,
112
- fixture,
113
- rendered,
114
- diagnostics: rendered.diagnostics
115
- };
116
- } catch (cause) {
117
- return {
118
- templateId: entry.id,
119
- fixture,
120
- rendered: void 0,
121
- diagnostics: findingsOf(cause, entry.id, fixture)
122
- };
123
- }
124
- });
125
- const findingsOf = (cause, templateId, fixture) => {
126
- const carried = cause.diagnostics;
127
- if (Array.isArray(carried) && carried.length > 0) return carried;
128
- return [diagnostic("render-failed", "error", `${templateId} could not render fixture ${JSON.stringify(fixture)}: ${cause instanceof Error ? cause.message : String(cause)}`)];
129
- };
130
- /**
131
- * The authoring loop: one evaluator, one catalog, reloaded on change. Content
132
- * semantics come from `compileEmailProject` and `renderEmail`, the same two
133
- * calls the hosted preview makes, so what an author iterates on locally is what
134
- * a publication renders.
135
- */
136
- const createEmailProjectSession = async (dir, options) => {
137
- const root = resolve(dir);
138
- const renderer = await createEmailRenderer(options);
139
- let catalog = {
140
- templates: [],
141
- helpers: [],
142
- diagnostics: [],
143
- assets: []
144
- };
145
- const listeners = /* @__PURE__ */ new Set();
146
- const observer = watch(root, { recursive: true }, (_event, name) => {
147
- const changed = name === null ? root : toPosix(name.toString());
148
- for (const listener of listeners) listener(changed);
149
- });
150
- const reload = async () => {
151
- renderer.invalidate();
152
- catalog = await loadEmailCatalog({
153
- renderer,
154
- files: await readEmailProject(root),
155
- stableNamespaces: true
156
- });
157
- return catalog;
158
- };
159
- return {
160
- reload,
161
- current: () => catalog,
162
- subscribe: (listener) => {
163
- listeners.add(listener);
164
- return () => void listeners.delete(listener);
165
- },
166
- close: async () => {
167
- observer.close();
168
- listeners.clear();
169
- await renderer.close();
170
- }
171
- };
172
- };
173
- /**
174
- * Where an export serves its assets from when the caller names no origin.
175
- *
176
- * An export writes one page at `templates/<id>/<fixture>.html` and every asset
177
- * once at `assets/<content address>`, so the base a page resolves against is
178
- * the path from its own directory back to that one. A template id is one path
179
- * segment, so every page sits at the same depth and one base serves them all.
180
- */
181
- const DEFAULT_EXPORT_ASSET_BASE = "../../assets";
182
- /**
183
- * The path an export writes, or `undefined` when a name would leave the root.
184
- *
185
- * `defineEmail` already refuses an id or a fixture key that is not one path
186
- * segment, so this is the guard rather than the contract: the same pattern,
187
- * plus a containment check, so a name that reaches `join` can never address a
188
- * file the export does not own.
189
- */
190
- const exportPath = (out, ...segments) => {
191
- if (!segments.every((segment) => TEMPLATE_NAME_PATTERN.test(segment))) return void 0;
192
- const path = join(out, ...segments);
193
- const inside = relative(out, path);
194
- return inside === "" || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside) ? void 0 : path;
195
- };
196
- 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 "_".`);
197
- /**
198
- * Render every template's fixtures to HTML and text beside the bytes of every
199
- * asset they reference, with the manifest that maps each address to its file.
200
- *
201
- * The export renders in the compiler's export mode, so a relative `assetBase`
202
- * is accepted and the written page resolves its images from the directory it
203
- * sits in. Delivery keeps refusing relative references — an inbox has no
204
- * directory to resolve one against — so an export destined for sending names
205
- * the https origin it will be served from instead.
206
- */
207
- const exportEmailProject = async (input) => {
208
- const out = resolve(input.out);
209
- const files = await readEmailProject(input.dir);
210
- const base = input.assetBase ?? "../../assets";
211
- const renderer = await createEmailRenderer({
212
- assetBase: base,
213
- ...input.tailwind === void 0 ? {} : { tailwind: input.tailwind }
214
- });
215
- try {
216
- const catalog = await loadEmailCatalog({
217
- renderer,
218
- files
219
- });
220
- const diagnostics = [...catalog.diagnostics];
221
- const written = await Promise.all(catalog.templates.map(async (entry) => {
222
- const directory = exportPath(out, "templates", entry.id);
223
- if (directory === void 0) return {
224
- entry: void 0,
225
- diagnostics: [unsafeExportName("Template id", entry.id)]
226
- };
227
- await mkdir(directory, { recursive: true });
228
- const rendered = renderEmailFixtures(entry, { relativeUrls: true });
229
- const pages = await Promise.all(rendered.map(async (result) => {
230
- if (result.rendered === void 0) return {
231
- fixture: void 0,
232
- diagnostics: []
233
- };
234
- const htmlPath = exportPath(out, "templates", entry.id, `${result.fixture}.html`);
235
- const textPath = exportPath(out, "templates", entry.id, `${result.fixture}.txt`);
236
- if (htmlPath === void 0 || textPath === void 0) return {
237
- fixture: void 0,
238
- diagnostics: [unsafeExportName("Fixture name", result.fixture)]
239
- };
240
- await Promise.all([writeFile(htmlPath, result.rendered.html, "utf8"), writeFile(textPath, result.rendered.text, "utf8")]);
241
- return {
242
- fixture: {
243
- fixture: result.fixture,
244
- html: toPosix(relative(out, htmlPath)),
245
- text: toPosix(relative(out, textPath))
246
- },
247
- diagnostics: []
248
- };
249
- }));
250
- return {
251
- entry: {
252
- id: entry.id,
253
- entryPath: entry.entryPath,
254
- fixtures: pages.map((result) => result.fixture).filter((fixture) => fixture !== void 0)
255
- },
256
- diagnostics: [...rendered.flatMap((result) => result.diagnostics), ...pages.flatMap((result) => result.diagnostics)]
257
- };
258
- }));
259
- const templates = written.map((result) => result.entry).filter((entry) => entry !== void 0);
260
- diagnostics.push(...written.flatMap((result) => result.diagnostics));
261
- const assetDirectory = join(out, "assets");
262
- if (catalog.assets.length > 0) await mkdir(assetDirectory, { recursive: true });
263
- await Promise.all(catalog.assets.map((asset) => {
264
- const bytes = files[asset.path];
265
- return bytes === void 0 || typeof bytes === "string" ? Promise.resolve() : writeFile(join(assetDirectory, asset.fileName), bytes);
266
- }));
267
- const receipt = {
268
- templates,
269
- assets: {
270
- base,
271
- assets: catalog.assets
272
- },
273
- diagnostics
274
- };
275
- await mkdir(out, { recursive: true });
276
- await writeFile(join(out, "catalog.json"), `${JSON.stringify({
277
- ...receipt,
278
- assets: {
279
- base,
280
- assets: catalog.assets.map((asset) => ({
281
- ...asset,
282
- url: assetUrl(asset, base)
283
- }))
284
- }
285
- }, null, 2)}\n`, "utf8");
286
- return receipt;
287
- } finally {
288
- await renderer.close();
289
- }
290
- };
291
- //#endregion
292
- export { DEFAULT_EXPORT_ASSET_BASE, createEmailProjectSession, exportEmailProject, isTemplateDefinition, loadEmailCatalog, readEmailProject, renderEmailFixtures };
293
-
294
- //# sourceMappingURL=project.js.map
1
+ import { C as discoverEntries, D as readProjectFiles, O as templateDirectories, S as SAMVA_DIR, a as projectRoot, b as resolveProjectBrand, d as emailCompatibility, f as finding, g as BRAND_SNAPSHOT_DIR, h as renderEmailFixtures, i as loadProject, l as channelsOf, m as renderChannelFixture, n as createProjectSession, p as hasErrors, r as exportProject, t as DEFAULT_EXPORT_ASSET_BASE, u as compileProject, x as DEFAULT_TEMPLATE_DIRS, y as loadBrandEnv } from "./project-Cp8qb3s0.js";
2
+ export { BRAND_SNAPSHOT_DIR, DEFAULT_EXPORT_ASSET_BASE, DEFAULT_TEMPLATE_DIRS, SAMVA_DIR, channelsOf, compileProject, createProjectSession, discoverEntries, emailCompatibility, exportProject, finding, hasErrors, loadBrandEnv, loadProject, projectRoot, readProjectFiles, renderChannelFixture, renderEmailFixtures, resolveProjectBrand, templateDirectories };
@@ -0,0 +1,54 @@
1
+ import { a as ProjectOptions } from "./project-CiUuzKsC.js";
2
+ import { EmailDiagnostic } from "@samva/markup/email/diagnostics";
3
+ import { RenderedIrWhatsApp } from "@samva/markup/ir";
4
+ //#region src/render.d.ts
5
+ interface RenderTemplateInput extends ProjectOptions {
6
+ /** A template id, or the project-relative path of its entry. */
7
+ readonly template: string;
8
+ readonly fixture: string;
9
+ /** Overrides the template's locale for formatters. */
10
+ readonly locale?: string | undefined;
11
+ /** IANA time zone for date and time formatters; defaults to UTC. */
12
+ readonly timeZone?: string | undefined;
13
+ }
14
+ type RenderTemplateResult = {
15
+ readonly ok: true;
16
+ readonly template: string;
17
+ readonly fixture: string;
18
+ readonly subject: string;
19
+ readonly preheader?: string | undefined;
20
+ readonly html: string;
21
+ readonly text: string;
22
+ readonly diagnostics: readonly EmailDiagnostic[];
23
+ } | {
24
+ readonly ok: false;
25
+ readonly diagnostics: readonly EmailDiagnostic[];
26
+ };
27
+ type RenderChannelInput = RenderTemplateInput & {
28
+ readonly channel: "sms" | "whatsapp";
29
+ };
30
+ type RenderChannelResult = {
31
+ readonly ok: true;
32
+ readonly channel: "sms";
33
+ readonly template: string;
34
+ readonly fixture: string;
35
+ readonly text: string;
36
+ readonly diagnostics: readonly EmailDiagnostic[];
37
+ } | {
38
+ readonly ok: true;
39
+ readonly channel: "whatsapp";
40
+ readonly template: string;
41
+ readonly fixture: string;
42
+ readonly message: RenderedIrWhatsApp;
43
+ readonly diagnostics: readonly EmailDiagnostic[];
44
+ } | {
45
+ readonly ok: false;
46
+ readonly diagnostics: readonly EmailDiagnostic[];
47
+ };
48
+ /** Compile the project and render one fixture of one template through its email channel. */
49
+ declare const renderTemplate: (input: RenderTemplateInput) => Promise<RenderTemplateResult>;
50
+ /** Compile the project and render one fixture of one template through its SMS or WhatsApp channel. */
51
+ declare const renderChannel: (input: RenderChannelInput) => Promise<RenderChannelResult>;
52
+ //#endregion
53
+ export { RenderChannelInput, RenderChannelResult, RenderTemplateInput, RenderTemplateResult, renderChannel, renderTemplate };
54
+ //# sourceMappingURL=render.d.ts.map
package/dist/render.js ADDED
@@ -0,0 +1,98 @@
1
+ import { _ as brandDiagnostics, f as finding, h as renderEmailFixtures, i as loadProject, m as renderChannelFixture, p as hasErrors } from "./project-Cp8qb3s0.js";
2
+ //#region src/render.ts
3
+ /** Compile just enough of the project to find and build the named template. */
4
+ const locate = async (input) => {
5
+ const byPath = input.template.endsWith(".tsx");
6
+ const project = await loadProject(input, void 0, byPath ? [input.template.replace(/^\.\//, "")] : void 0);
7
+ const { catalog } = project;
8
+ const entry = catalog.templates.find((candidate) => byPath ? candidate.entryPath === input.template.replace(/^\.\//, "") : candidate.id === input.template);
9
+ const brand = brandDiagnostics(project.brand);
10
+ const ambiguous = entry === void 0 ? [] : catalog.diagnostics.filter((item) => item.code === "duplicate-template-id" && item.message.startsWith(`Template id ${JSON.stringify(entry.id)} `));
11
+ if (entry !== void 0 && ambiguous.length === 0) return {
12
+ ok: true,
13
+ entry,
14
+ diagnostics: [...brand, ...entry.diagnostics]
15
+ };
16
+ const failed = byPath ? catalog.failures.filter((candidate) => candidate.entryPath === input.template.replace(/^\.\//, "")) : catalog.failures;
17
+ return {
18
+ ok: false,
19
+ diagnostics: [
20
+ ...brand,
21
+ ...catalog.diagnostics,
22
+ ...failed.flatMap((candidate) => candidate.diagnostics),
23
+ ...entry !== void 0 || byPath && failed.length > 0 ? [] : [finding("template-not-found", "error", `No template ${JSON.stringify(input.template)} in the project${failed.length > 0 ? ", and some entries did not compile (their findings are listed)" : ""}. Known templates: ${catalog.templates.map((candidate) => candidate.id).join(", ") || "none"}.`)]
24
+ ]
25
+ };
26
+ };
27
+ /** Compile the project and render one fixture of one template through its email channel. */
28
+ const renderTemplate = async (input) => {
29
+ const located = await locate(input);
30
+ if (!located.ok) return located;
31
+ const { entry } = located;
32
+ if (!entry.channels.includes("email")) return {
33
+ ok: false,
34
+ diagnostics: [finding("missing-channel", "error", `Template ${entry.id} declares no email channel.`)]
35
+ };
36
+ const [rendered] = renderEmailFixtures({
37
+ ...entry,
38
+ fixtures: pick(entry.fixtures, input.fixture)
39
+ }, renderOptions(input));
40
+ if (rendered === void 0) return {
41
+ ok: false,
42
+ diagnostics: [...located.diagnostics, unknownFixture(entry, input.fixture)]
43
+ };
44
+ if (rendered.rendered === void 0 || hasErrors(located.diagnostics)) return {
45
+ ok: false,
46
+ diagnostics: [...located.diagnostics, ...rendered.diagnostics]
47
+ };
48
+ return {
49
+ ok: true,
50
+ template: entry.id,
51
+ fixture: input.fixture,
52
+ subject: rendered.rendered.subject,
53
+ ...rendered.rendered.preheader === void 0 ? {} : { preheader: rendered.rendered.preheader },
54
+ html: rendered.rendered.html,
55
+ text: rendered.rendered.text,
56
+ diagnostics: located.diagnostics
57
+ };
58
+ };
59
+ /** Compile the project and render one fixture of one template through its SMS or WhatsApp channel. */
60
+ const renderChannel = async (input) => {
61
+ const located = await locate(input);
62
+ if (!located.ok) return located;
63
+ const { entry } = located;
64
+ if (!entry.channels.includes(input.channel)) return {
65
+ ok: false,
66
+ diagnostics: [finding("missing-channel", "error", `Template ${entry.id} declares no ${input.channel} channel.`)]
67
+ };
68
+ const rendered = renderChannelFixture(entry, input.channel, input.fixture, renderOptions(input));
69
+ if (!rendered.ok || hasErrors(located.diagnostics)) return {
70
+ ok: false,
71
+ diagnostics: [...located.diagnostics, ...rendered.ok ? [] : rendered.diagnostics]
72
+ };
73
+ return rendered.channel === "sms" ? {
74
+ ok: true,
75
+ channel: "sms",
76
+ template: entry.id,
77
+ fixture: input.fixture,
78
+ text: rendered.text,
79
+ diagnostics: located.diagnostics
80
+ } : {
81
+ ok: true,
82
+ channel: "whatsapp",
83
+ template: entry.id,
84
+ fixture: input.fixture,
85
+ message: rendered.message,
86
+ diagnostics: located.diagnostics
87
+ };
88
+ };
89
+ const renderOptions = (input) => ({
90
+ ...input.locale === void 0 ? {} : { locale: input.locale },
91
+ ...input.timeZone === void 0 ? {} : { timeZone: input.timeZone }
92
+ });
93
+ const pick = (fixtures, name) => Object.hasOwn(fixtures, name) ? { [name]: fixtures[name] } : {};
94
+ const unknownFixture = (entry, fixture) => finding("unknown-fixture", "error", `Template ${entry.id} declares no fixture named ${JSON.stringify(fixture)}. Declared: ${Object.keys(entry.fixtures).join(", ")}.`);
95
+ //#endregion
96
+ export { renderChannel, renderTemplate };
97
+
98
+ //# sourceMappingURL=render.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@samva/vite",
3
- "version": "0.7.0",
4
- "description": "Unified Vite editor for Samva email, SMS, and WhatsApp templates",
3
+ "version": "0.9.0",
4
+ "description": "Local editor, dev server and build for Samva email, SMS and WhatsApp templates",
5
5
  "keywords": [
6
6
  "email",
7
7
  "samva",
@@ -38,10 +38,6 @@
38
38
  "types": "./dist/index.d.ts",
39
39
  "import": "./dist/index.js"
40
40
  },
41
- "./emit": {
42
- "types": "./dist/emit.d.ts",
43
- "import": "./dist/emit.js"
44
- },
45
41
  "./build": {
46
42
  "types": "./dist/build.d.ts",
47
43
  "import": "./dist/build.js"
@@ -50,9 +46,13 @@
50
46
  "types": "./dist/project.d.ts",
51
47
  "import": "./dist/project.js"
52
48
  },
53
- "./email": {
54
- "types": "./dist/email.d.ts",
55
- "import": "./dist/email.js"
49
+ "./dev": {
50
+ "types": "./dist/dev.d.ts",
51
+ "import": "./dist/dev.js"
52
+ },
53
+ "./render": {
54
+ "types": "./dist/render.d.ts",
55
+ "import": "./dist/render.js"
56
56
  }
57
57
  },
58
58
  "publishConfig": {
@@ -64,14 +64,13 @@
64
64
  "dev:editor": "bun run --filter @samva/vite-editor-app dev",
65
65
  "prepack": "bun run build",
66
66
  "typecheck": "tsc --noEmit",
67
- "test": "vitest run --root ../.. --project packages packages/vite/tests",
67
+ "test": "vitest run --root ../.. --project packages --project serial-vite packages/vite/tests",
68
68
  "test:browser": "turbo run build --filter=@samva/vite && playwright test --config playwright.config.ts --project chromium",
69
69
  "test:browser:cross": "turbo run build --filter=@samva/vite && playwright test --config playwright.config.ts"
70
70
  },
71
71
  "dependencies": {
72
72
  "@babel/parser": "^8.0.4",
73
- "@samva/markup": "^0.7.0",
74
- "better-result": "3.0.1",
73
+ "@samva/markup": "^0.9.0",
75
74
  "caniemail": "2.0.2",
76
75
  "css-tree": "3.2.1",
77
76
  "tailwindcss": "^4.3.3"