@samva/vite 0.7.0 → 0.8.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.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 { a as loadEmailCatalog, d as BRAND_SNAPSHOT_DIR, h as resolveProjectBrand, i as isTemplateDefinition, m as loadBrandEnv, n as createEmailProjectSession, o as readEmailProject, r as exportEmailProject, s as renderEmailFixtures, t as DEFAULT_EXPORT_ASSET_BASE } from "./project-RON2Ei7d.js";
2
+ export { BRAND_SNAPSHOT_DIR, DEFAULT_EXPORT_ASSET_BASE, createEmailProjectSession, exportEmailProject, isTemplateDefinition, loadBrandEnv, loadEmailCatalog, readEmailProject, renderEmailFixtures, resolveProjectBrand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samva/vite",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Unified Vite editor for Samva email, SMS, and WhatsApp templates",
5
5
  "keywords": [
6
6
  "email",
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@babel/parser": "^8.0.4",
73
- "@samva/markup": "^0.7.0",
73
+ "@samva/markup": "^0.8.0",
74
74
  "better-result": "3.0.1",
75
75
  "caniemail": "2.0.2",
76
76
  "css-tree": "3.2.1",