@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.
package/dist/build.js CHANGED
@@ -1,228 +1,103 @@
1
- import { c as projectThemePath, f as brandDiagnostics, i as isTemplateDefinition, l as readProjectThemeCss, m as loadBrandEnv, o as readEmailProject, p as createBrandResolver, s as renderEmailFixtures, u as resolveThemePath } from "./project-RON2Ei7d.js";
2
- import { t as discoverTemplateFiles } from "./discovery-D2eR-rzi.js";
3
- import { COMPILER_VERSION, emitModulePreview } from "./emit.js";
4
- import { createEmailRenderer } from "./email.js";
5
- import { dirname, parse, relative, resolve, sep } from "node:path";
1
+ import { d as emailCompatibility, f as finding, h as renderEmailFixtures, i as loadProject, m as renderChannelFixture, n as createProjectSession, p as hasErrors, s as isDeliverableBase } from "./project-Ut6wra0r.js";
2
+ import markupPackage from "@samva/markup/package.json" with { type: "json" };
6
3
  //#region src/build.ts
7
- /** Execute each email definition's fixtures; compile structured SMS/WhatsApp through their channel compiler. */
8
- const buildTemplates = async (dir, options = {}) => {
9
- const session = await createTemplateBuildSession(dir, options);
10
- try {
11
- return await session.build();
12
- } finally {
13
- await session.close();
14
- }
15
- };
16
- const emailFinding = (code, message) => ({
17
- code,
18
- severity: "error",
19
- message,
20
- origins: []
21
- });
22
- const emailFailure = (cause, code = "email-build-error") => ({
23
- kind: "email",
24
- ok: false,
25
- compilerVersion: COMPILER_VERSION,
26
- templates: [],
27
- diagnostics: Array.isArray(cause?.diagnostics) ? cause.diagnostics : [emailFinding(code, cause instanceof Error ? cause.message : String(cause))]
4
+ /** The compiler a build ran, as a publication records it. */
5
+ const COMPILER = `@samva/markup@${markupPackage.version}`;
6
+ /**
7
+ * A build needs an https base for imported assets and reports one finding when it has none, so a
8
+ * template that imports an asset still compiles and renders and the finding is the only noise.
9
+ */
10
+ const PLACEHOLDER_ORIGIN = "https://assets.invalid";
11
+ const withCompileBase = (options) => ({
12
+ ...options,
13
+ assetBase: isDeliverableBase(options.assetBase ?? "") ? options.assetBase : PLACEHOLDER_ORIGIN
28
14
  });
29
- /** A watcher and module evaluator over the project's installed authoring toolchain. */
30
- const createTemplateBuildSession = async (dir, options = {}) => {
31
- const templatesDir = resolve(dir);
32
- const root = resolve(options.root ?? dirname(templatesDir));
33
- const themePath = resolveThemePath(root, options.theme);
34
- await discoverTemplateFiles(templatesDir);
35
- const server = await createEvaluator(templatesDir);
36
- server.watcher.add(root);
37
- if (themePath !== void 0) server.watcher.add(themePath);
38
- let closed = false;
39
- let emailRenderer;
40
- let previousTheme;
41
- let previousBrand;
42
- const brands = createBrandResolver({
43
- root,
44
- env: await loadBrandEnv(root)
15
+ const assetFindings = (entry, assetBase) => {
16
+ if (entry.assets.length === 0) return [];
17
+ if (assetBase === void 0 || assetBase === "") return [finding("missing-asset-base", "error", "Imported email assets require --asset-base with their HTTPS serving origin.")];
18
+ return /^https:\/\//i.test(assetBase) ? [] : [finding("invalid-asset-base", "error", "Email check and build require an HTTPS --asset-base; relative URLs belong to templates export.")];
19
+ };
20
+ const buildEntry = (entry, assetBase) => {
21
+ const email = entry.channels.includes("email") ? renderEmailFixtures(entry) : [];
22
+ const fixtures = Object.entries(entry.fixtures).map(([name, input]) => {
23
+ const rendered = email.find((result) => result.fixture === name);
24
+ const sms = entry.channels.includes("sms") ? renderChannelFixture(entry, "sms", name) : void 0;
25
+ const whatsapp = entry.channels.includes("whatsapp") ? renderChannelFixture(entry, "whatsapp", name) : void 0;
26
+ const diagnostics = [...rendered?.diagnostics ?? [], ...[sms, whatsapp].flatMap((result) => result?.ok === false ? result.diagnostics : [])];
27
+ return {
28
+ name,
29
+ input,
30
+ ok: !hasErrors(diagnostics),
31
+ ...rendered?.rendered === void 0 ? {} : { email: {
32
+ subject: rendered.rendered.subject,
33
+ preheader: rendered.rendered.preheader,
34
+ html: rendered.rendered.html,
35
+ text: rendered.rendered.text
36
+ } },
37
+ ...sms?.ok === true && sms.channel === "sms" ? { sms: sms.text } : {},
38
+ ...whatsapp?.ok === true && whatsapp.channel === "whatsapp" ? { whatsapp: whatsapp.message } : {},
39
+ diagnostics
40
+ };
45
41
  });
46
- const build = async () => {
47
- server.moduleGraph.invalidateAll();
48
- const themeCss = await readProjectThemeCss(themePath);
49
- const files = await discoverTemplateFiles(templatesDir);
50
- const sourceFiles = await readEmailProject(root);
51
- const themeFile = themePath === void 0 ? void 0 : projectThemePath(root, themePath);
52
- const brandResolution = await brands.resolve(themeFile === void 0 || themeCss === void 0 ? sourceFiles : {
53
- ...sourceFiles,
54
- [themeFile]: themeCss
55
- }, themeFile);
56
- const brand = brandResolution.ok ? brandResolution.brand : void 0;
57
- const brandFindings = brandDiagnostics(brandResolution);
58
- if (emailRenderer !== void 0 && (themeCss !== previousTheme || brand?.digest !== previousBrand)) {
59
- await emailRenderer.close();
60
- emailRenderer = void 0;
61
- }
62
- previousTheme = themeCss;
63
- previousBrand = brand?.digest;
64
- emailRenderer?.invalidate();
65
- const entries = [];
66
- const emailIds = /* @__PURE__ */ new Map();
67
- for (const file of files) {
68
- const evaluated = await server.ssrLoadModule(file.path).then((moduleExports) => ({
69
- ok: true,
70
- moduleExports
71
- }), (cause) => ({
72
- ok: false,
73
- cause
74
- }));
75
- if (!evaluated.ok) {
76
- entries.push({
77
- id: file.id,
78
- kind: file.kind,
79
- preview: emailFailure(evaluated.cause)
80
- });
81
- continue;
82
- }
83
- const definition = evaluated.moduleExports.default;
84
- if (definition === void 0) continue;
85
- if (isTemplateDefinition(definition)) {
86
- const entryPath = relative(root, file.path).split(sep).join("/");
87
- const previous = emailIds.get(definition.id);
88
- if (previous !== void 0) {
89
- entries.push({
90
- id: file.id,
91
- kind: file.kind,
92
- preview: emailFailure(`Duplicate template id ${definition.id}: ${previous} and ${file.id}`, "duplicate-template-id")
93
- });
94
- continue;
95
- }
96
- emailIds.set(definition.id, file.id);
97
- const preview = await (async () => {
98
- emailRenderer ??= await createEmailRenderer({
99
- assetBase: options.assetBase ?? "",
100
- ...themeCss === void 0 || themeFile === void 0 ? {} : { tailwind: {
101
- css: themeCss,
102
- cssPath: themeFile
103
- } },
104
- ...brand === void 0 ? {} : { brand }
105
- });
106
- const loaded = await emailRenderer.load(sourceFiles, entryPath, entryPath);
107
- const template = loaded.template;
108
- if (!isTemplateDefinition(template)) return emailFailure(`${file.id} does not export defineEmail`);
109
- const fixtures = renderEmailFixtures({
110
- id: template.id,
111
- entryPath,
112
- definition: template,
113
- loaded
114
- }).map((fixture) => ({
115
- name: fixture.fixture,
116
- input: template.fixtures[fixture.fixture],
117
- ok: fixture.rendered !== void 0 && !fixture.diagnostics.some((finding) => finding.severity === "error"),
118
- ...fixture.rendered === void 0 ? {} : {
119
- subject: fixture.rendered.subject,
120
- preheader: fixture.rendered.preheader,
121
- html: fixture.rendered.html,
122
- text: fixture.rendered.text
123
- },
124
- diagnostics: fixture.diagnostics
125
- }));
126
- const diagnostics = [...brandFindings, ...loaded.compiled.diagnostics];
127
- if (fixtures.length === 0) diagnostics.push(emailFinding("missing-fixtures", `${template.id} must declare at least one fixture for check and build.`));
128
- if (loaded.compiled.assets.length > 0 && options.assetBase === void 0) diagnostics.push(emailFinding("missing-asset-base", "Imported email assets require --asset-base with their HTTPS serving origin."));
129
- if (loaded.compiled.assets.length > 0 && options.assetBase !== void 0 && !/^https:\/\//i.test(options.assetBase)) diagnostics.push(emailFinding("invalid-asset-base", "Email check and build require an HTTPS --asset-base; relative URLs belong to templates export."));
130
- const ok = !diagnostics.some((finding) => finding.severity === "error") && fixtures.every((fixture) => fixture.ok);
131
- return {
132
- kind: "email",
133
- ok,
134
- compilerVersion: COMPILER_VERSION,
135
- diagnostics,
136
- templates: [{
137
- name: template.id,
138
- channel: "email",
139
- ok,
140
- source: String(sourceFiles[entryPath]),
141
- inputSchema: template.schema.schema,
142
- compiled: loaded.compiled,
143
- fixtures
144
- }]
145
- };
146
- })().catch((cause) => {
147
- const failure = emailFailure(cause);
148
- return {
149
- ...failure,
150
- diagnostics: [...brandFindings, ...failure.diagnostics]
151
- };
152
- });
153
- entries.push({
154
- id: file.id,
155
- kind: file.kind,
156
- preview
157
- });
158
- continue;
159
- }
160
- const preview = emitModulePreview(parse(file.id).name, evaluated.moduleExports);
161
- if (preview.templates.length === 0) continue;
162
- if (preview.templates.some((template) => /^\s*<Email(?:\s|>)/.test(template.source ?? ""))) entries.push({
163
- id: file.id,
164
- kind: file.kind,
165
- preview: emailFailure(`${file.id}: email templates must default-export defineEmail with an input schema and fixtures.`, "email-requires-object-template")
166
- });
167
- else entries.push({
168
- id: file.id,
169
- kind: file.kind,
170
- preview: {
171
- ...preview,
172
- kind: "structured"
173
- }
174
- });
175
- }
176
- return { files: entries };
42
+ const compatibility = emailCompatibility(entry, email);
43
+ const diagnostics = [...entry.diagnostics, ...assetFindings(entry, assetBase)];
44
+ const ok = !hasErrors(diagnostics) && !hasErrors(compatibility) && fixtures.every((fixture) => fixture.ok);
45
+ const template = {
46
+ template: entry.id,
47
+ channels: entry.channels,
48
+ ok,
49
+ source: entry.entryPath,
50
+ inputSchema: entry.ir.schema,
51
+ ir: entry.ir,
52
+ fixtures,
53
+ diagnostics,
54
+ compatibility
177
55
  };
178
- const fileEvents = /* @__PURE__ */ new Set([
179
- "add",
180
- "change",
181
- "unlink"
182
- ]);
183
- const subscribe = (listener) => {
184
- const onChange = (event, path) => {
185
- if (fileEvents.has(event)) listener(path);
186
- };
187
- server.watcher.on("all", onChange);
188
- return () => server.watcher.off("all", onChange);
56
+ return {
57
+ id: entry.entryPath,
58
+ kind: "tsx",
59
+ ok,
60
+ template,
61
+ diagnostics
189
62
  };
63
+ };
64
+ const summarize = (project, assetBase) => {
65
+ const built = project.catalog.templates.map((entry) => buildEntry(entry, assetBase));
66
+ const failed = project.catalog.failures.map((entry) => ({
67
+ id: entry.entryPath,
68
+ kind: "tsx",
69
+ ok: false,
70
+ diagnostics: entry.diagnostics
71
+ }));
72
+ const files = [...built, ...failed].sort((left, right) => left.id < right.id ? -1 : 1);
73
+ return {
74
+ compilerVersion: COMPILER,
75
+ files,
76
+ diagnostics: project.diagnostics,
77
+ ok: !hasErrors(project.diagnostics) && files.every((file) => file.ok)
78
+ };
79
+ };
80
+ /**
81
+ * Compile every template in a project and render every fixture it declares
82
+ * through each of its channels, then check the email fixtures against the
83
+ * client matrix. Nothing is evaluated.
84
+ */
85
+ const buildTemplates = async (options = {}) => summarize(await loadProject(withCompileBase(options)), options.assetBase);
86
+ /** A watcher and compiler over a project, for `check --watch`. */
87
+ const createTemplateBuildSession = (options = {}) => {
88
+ const session = createProjectSession(withCompileBase(options));
89
+ let closed = false;
190
90
  return {
191
- build,
192
- subscribe,
91
+ build: async () => summarize(await session.load(), options.assetBase),
92
+ subscribe: session.subscribe,
193
93
  close: async () => {
194
94
  if (closed) return;
195
95
  closed = true;
196
- try {
197
- await emailRenderer?.close();
198
- } finally {
199
- await server.close();
200
- }
96
+ await session.close();
201
97
  }
202
98
  };
203
99
  };
204
- const createEvaluator = async (root) => {
205
- const vite = await import("vite").catch(() => void 0);
206
- if (vite === void 0) throw new Error("`vite` is required for template authoring. Install it as a dev dependency.");
207
- return vite.createServer({
208
- root,
209
- configFile: false,
210
- logLevel: "silent",
211
- server: {
212
- middlewareMode: true,
213
- hmr: false
214
- },
215
- oxc: { jsx: {
216
- runtime: "automatic",
217
- importSource: "@samva/markup"
218
- } },
219
- optimizeDeps: {
220
- noDiscovery: true,
221
- include: []
222
- }
223
- });
224
- };
225
100
  //#endregion
226
- export { buildTemplates, createTemplateBuildSession };
101
+ export { COMPILER, buildTemplates, createTemplateBuildSession };
227
102
 
228
103
  //# sourceMappingURL=build.js.map
package/dist/dev.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ //#region src/dev.d.ts
2
+ interface DevServerOptions {
3
+ /** The project root. Default: the working directory. */
4
+ readonly root?: string | undefined;
5
+ /** The templates directory, relative to `root`. Default: `templates/` and `emails/`, whichever exist. */
6
+ readonly dir?: string | undefined;
7
+ readonly port?: number | undefined;
8
+ /** The interface to listen on. Default: loopback only. */
9
+ readonly host?: string | undefined;
10
+ /** Open the editor in a browser once the server is listening. */
11
+ readonly open?: boolean | undefined;
12
+ }
13
+ interface DevServer {
14
+ /** Where the editor is served. */
15
+ readonly url: string;
16
+ readonly close: () => Promise<void>;
17
+ }
18
+ /** Start the local editor for a template project. */
19
+ declare const startDevServer: (options?: DevServerOptions) => Promise<DevServer>;
20
+ //#endregion
21
+ export { DevServer, DevServerOptions, startDevServer };
22
+ //# sourceMappingURL=dev.d.ts.map
package/dist/dev.js ADDED
@@ -0,0 +1,43 @@
1
+ import { t as samvaEditor } from "./editor-B_2rlC_U.js";
2
+ import { resolve } from "node:path";
3
+ //#region src/dev.ts
4
+ /** Start the local editor for a template project. */
5
+ const startDevServer = async (options = {}) => {
6
+ const vite = await import("vite").catch(() => void 0);
7
+ if (vite === void 0) throw new Error("`vite` is required for the template dev server. Install it as a dev dependency.");
8
+ const root = resolve(options.root ?? process.cwd());
9
+ const server = await vite.createServer({
10
+ root,
11
+ configFile: false,
12
+ appType: "custom",
13
+ optimizeDeps: {
14
+ noDiscovery: true,
15
+ include: []
16
+ },
17
+ server: {
18
+ ...options.port === void 0 ? {} : { port: options.port },
19
+ ...options.host === void 0 ? {} : { host: options.host },
20
+ open: options.open === true
21
+ },
22
+ plugins: [samvaEditor({ ...options.dir === void 0 ? {} : { templatesDir: options.dir } })]
23
+ });
24
+ try {
25
+ await server.listen();
26
+ } catch (cause) {
27
+ await server.close();
28
+ throw cause;
29
+ }
30
+ const url = server.resolvedUrls?.local[0] ?? server.resolvedUrls?.network[0];
31
+ if (url === void 0) {
32
+ await server.close();
33
+ throw new Error("The template dev server started without a URL.");
34
+ }
35
+ return {
36
+ url,
37
+ close: () => server.close()
38
+ };
39
+ };
40
+ //#endregion
41
+ export { startDevServer };
42
+
43
+ //# sourceMappingURL=dev.js.map