@samva/vite 0.1.2 → 0.2.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 +70 -1
- package/README.md +92 -44
- package/dist/build.d.ts +47 -28
- package/dist/build.js +166 -67
- package/dist/editor/assets/index-Cg4x6ewh.css +2 -0
- package/dist/editor/assets/index-Cot9JDdw.js +30 -0
- package/dist/editor/index.html +2 -2
- package/dist/email.d.ts +58 -0
- package/dist/email.js +116 -0
- package/dist/emit.d.ts +13 -30
- package/dist/emit.js +29 -123
- package/dist/index.d.ts +1 -4
- package/dist/index.js +480 -305
- package/dist/project.d.ts +116 -0
- package/dist/project.js +294 -0
- package/dist/theme-CBF6Czjm.js +50 -0
- package/package.json +28 -20
- package/dist/editor/assets/authoring-editor-ClA5aqFP-CO-FhsWG.js +0 -145
- package/dist/editor/assets/index-CI_j2PFe.css +0 -2
- package/dist/editor/assets/index-DbdWiln3.js +0 -84
- package/dist/theme-CfJe8s3k.js +0 -138
|
@@ -0,0 +1,116 @@
|
|
|
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
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
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
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
//#region src/discovery.ts
|
|
4
|
+
const isTemplateFile = (name) => name.endsWith(".tsx");
|
|
5
|
+
const toPosix = (value) => sep === "/" ? value : value.split(sep).join("/");
|
|
6
|
+
/**
|
|
7
|
+
* Recursively lists template files under `dir`. Hidden entries and
|
|
8
|
+
* `node_modules` are skipped. Results are sorted by id for deterministic
|
|
9
|
+
* ordering (the preview list and build output must be stable).
|
|
10
|
+
*/
|
|
11
|
+
const discoverTemplateFiles = async (dir) => {
|
|
12
|
+
const found = [];
|
|
13
|
+
const walk = async (current) => {
|
|
14
|
+
const visible = (await readdir(current, { withFileTypes: true })).filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules");
|
|
15
|
+
await Promise.all(visible.map(async (entry) => {
|
|
16
|
+
const full = join(current, entry.name);
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
await walk(full);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!isTemplateFile(entry.name)) return;
|
|
22
|
+
found.push({
|
|
23
|
+
path: full,
|
|
24
|
+
id: toPosix(relative(dir, full)),
|
|
25
|
+
kind: "tsx"
|
|
26
|
+
});
|
|
27
|
+
}));
|
|
28
|
+
};
|
|
29
|
+
await walk(dir);
|
|
30
|
+
return found.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Resolves the `theme` plugin option to an absolute path, or `undefined` when
|
|
34
|
+
* theming is explicitly disabled (`theme: false`). A string is taken relative to
|
|
35
|
+
* `root` (absolute strings pass through); the default is `<root>/theme.css`.
|
|
36
|
+
*/
|
|
37
|
+
const resolveThemePath = (root, option) => {
|
|
38
|
+
if (option === false) return void 0;
|
|
39
|
+
const file = option ?? "theme.css";
|
|
40
|
+
return isAbsolute(file) ? file : resolve(root, file);
|
|
41
|
+
};
|
|
42
|
+
/** Read authored Tailwind CSS verbatim; the email compiler validates its syntax. */
|
|
43
|
+
const readProjectThemeCss = async (path) => path === void 0 ? void 0 : readFile(path, "utf8").catch((cause) => {
|
|
44
|
+
if (cause.code === "ENOENT") return void 0;
|
|
45
|
+
throw cause;
|
|
46
|
+
});
|
|
47
|
+
//#endregion
|
|
48
|
+
export { resolveThemePath as n, discoverTemplateFiles as r, readProjectThemeCss as t };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=theme-CBF6Czjm.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@samva/vite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Unified Vite editor for Samva email, SMS, and WhatsApp templates",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -45,39 +45,47 @@
|
|
|
45
45
|
"./build": {
|
|
46
46
|
"types": "./dist/build.d.ts",
|
|
47
47
|
"import": "./dist/build.js"
|
|
48
|
+
},
|
|
49
|
+
"./project": {
|
|
50
|
+
"types": "./dist/project.d.ts",
|
|
51
|
+
"import": "./dist/project.js"
|
|
52
|
+
},
|
|
53
|
+
"./email": {
|
|
54
|
+
"types": "./dist/email.d.ts",
|
|
55
|
+
"import": "./dist/email.js"
|
|
48
56
|
}
|
|
49
57
|
},
|
|
50
58
|
"publishConfig": {
|
|
51
59
|
"access": "public"
|
|
52
60
|
},
|
|
53
61
|
"scripts": {
|
|
54
|
-
"build": "NODE_ENV=production tsdown &&
|
|
55
|
-
"build:editor": "
|
|
56
|
-
"dev:editor": "
|
|
62
|
+
"build": "NODE_ENV=production tsdown && bun run build:editor",
|
|
63
|
+
"build:editor": "bun run --filter @samva/vite-editor-app build",
|
|
64
|
+
"dev:editor": "bun run --filter @samva/vite-editor-app dev",
|
|
57
65
|
"prepack": "bun run build",
|
|
58
|
-
"typecheck": "
|
|
59
|
-
"test": "vitest run --root ../.. --project packages packages/vite/tests"
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"test": "vitest run --root ../.. --project packages packages/vite/tests",
|
|
68
|
+
"test:browser": "turbo run build --filter=@samva/vite && playwright test --config playwright.config.ts --project chromium",
|
|
69
|
+
"test:browser:cross": "turbo run build --filter=@samva/vite && playwright test --config playwright.config.ts"
|
|
60
70
|
},
|
|
61
71
|
"dependencies": {
|
|
62
|
-
"@
|
|
63
|
-
"
|
|
72
|
+
"@babel/parser": "^8.0.4",
|
|
73
|
+
"@samva/markup": "^0.2.0",
|
|
74
|
+
"caniemail": "2.0.2",
|
|
75
|
+
"css-tree": "3.2.1",
|
|
76
|
+
"effect": "4.0.0-rc.112",
|
|
77
|
+
"tailwindcss": "^4.3.3"
|
|
64
78
|
},
|
|
65
79
|
"devDependencies": {
|
|
66
|
-
"@effect/vitest": "4.0.0-rc.
|
|
67
|
-
"@
|
|
68
|
-
"@types/react": "19.2.18",
|
|
69
|
-
"@types/react-dom": "19.2.4",
|
|
70
|
-
"@vitejs/plugin-react": "^6.0.5",
|
|
71
|
-
"react": "19.2.8",
|
|
72
|
-
"react-dom": "19.2.8",
|
|
73
|
-
"tailwindcss": "^4.3.3",
|
|
80
|
+
"@effect/vitest": "4.0.0-rc.112",
|
|
81
|
+
"@playwright/test": "^1.62.1",
|
|
74
82
|
"tsdown": "^0.22.13",
|
|
75
|
-
"typescript": "^
|
|
76
|
-
"vite": "~8.2.
|
|
77
|
-
"vitest": "^4.1.
|
|
83
|
+
"typescript": "^7.0.2",
|
|
84
|
+
"vite": "~8.2.2",
|
|
85
|
+
"vitest": "^4.1.11"
|
|
78
86
|
},
|
|
79
87
|
"peerDependencies": {
|
|
80
|
-
"vite": "^
|
|
88
|
+
"vite": "^8.2.2"
|
|
81
89
|
},
|
|
82
90
|
"engines": {
|
|
83
91
|
"node": ">=22"
|