@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/AGENTS.md +6 -1
- package/README.md +29 -3
- package/dist/build.js +31 -8
- package/dist/{theme-CBF6Czjm.js → discovery-D2eR-rzi.js} +4 -19
- package/dist/editor/assets/index-6Qlbkdhz.js +36 -0
- package/dist/editor/assets/index-Ed_V5wEW.css +2 -0
- package/dist/editor/index.html +2 -2
- package/dist/index.js +60 -21
- package/dist/project-RON2Ei7d.js +603 -0
- package/dist/project.d.ts +62 -3
- package/dist/project.js +2 -294
- package/package.json +2 -2
- package/dist/editor/assets/index-BeVA-Ur2.js +0 -32
- package/dist/editor/assets/index-Dz2CE64u.css +0 -2
package/dist/project.js
CHANGED
|
@@ -1,294 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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.
|
|
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.
|
|
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",
|