@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/build.js CHANGED
@@ -1,205 +1,103 @@
1
- import { n as resolveThemePath, r as discoverTemplateFiles, t as readProjectThemeCss } from "./theme-CBF6Czjm.js";
2
- import { COMPILER_VERSION, emitModulePreview } from "./emit.js";
3
- import { createEmailRenderer } from "./email.js";
4
- import { isTemplateDefinition, readEmailProject, renderEmailFixtures } from "./project.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-Cp8qb3s0.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
- const build = async () => {
42
- server.moduleGraph.invalidateAll();
43
- const themeCss = await readProjectThemeCss(themePath);
44
- if (emailRenderer !== void 0 && themeCss !== previousTheme) {
45
- await emailRenderer.close();
46
- emailRenderer = void 0;
47
- }
48
- previousTheme = themeCss;
49
- emailRenderer?.invalidate();
50
- const files = await discoverTemplateFiles(templatesDir);
51
- const sourceFiles = await readEmailProject(root);
52
- const entries = [];
53
- const emailIds = /* @__PURE__ */ new Map();
54
- for (const file of files) {
55
- const evaluated = await server.ssrLoadModule(file.path).then((moduleExports) => ({
56
- ok: true,
57
- moduleExports
58
- }), (cause) => ({
59
- ok: false,
60
- cause
61
- }));
62
- if (!evaluated.ok) {
63
- entries.push({
64
- id: file.id,
65
- kind: file.kind,
66
- preview: emailFailure(evaluated.cause)
67
- });
68
- continue;
69
- }
70
- const definition = evaluated.moduleExports.default;
71
- if (definition === void 0) continue;
72
- if (isTemplateDefinition(definition)) {
73
- const entryPath = relative(root, file.path).split(sep).join("/");
74
- const previous = emailIds.get(definition.id);
75
- if (previous !== void 0) {
76
- entries.push({
77
- id: file.id,
78
- kind: file.kind,
79
- preview: emailFailure(`Duplicate template id ${definition.id}: ${previous} and ${file.id}`, "duplicate-template-id")
80
- });
81
- continue;
82
- }
83
- emailIds.set(definition.id, file.id);
84
- const preview = await (async () => {
85
- emailRenderer ??= await createEmailRenderer({
86
- assetBase: options.assetBase ?? "",
87
- ...themeCss === void 0 ? {} : { tailwind: { css: themeCss } }
88
- });
89
- const loaded = await emailRenderer.load(sourceFiles, entryPath, entryPath);
90
- const template = loaded.template;
91
- if (!isTemplateDefinition(template)) return emailFailure(`${file.id} does not export defineEmail`);
92
- const fixtures = renderEmailFixtures({
93
- id: template.id,
94
- entryPath,
95
- definition: template,
96
- loaded
97
- }).map((fixture) => ({
98
- name: fixture.fixture,
99
- input: template.fixtures[fixture.fixture],
100
- ok: fixture.rendered !== void 0 && !fixture.diagnostics.some((finding) => finding.severity === "error"),
101
- ...fixture.rendered === void 0 ? {} : {
102
- subject: fixture.rendered.subject,
103
- preheader: fixture.rendered.preheader,
104
- html: fixture.rendered.html,
105
- text: fixture.rendered.text
106
- },
107
- diagnostics: fixture.diagnostics
108
- }));
109
- const diagnostics = [...loaded.compiled.diagnostics];
110
- if (fixtures.length === 0) diagnostics.push(emailFinding("missing-fixtures", `${template.id} must declare at least one fixture for check and build.`));
111
- 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."));
112
- 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."));
113
- const ok = !diagnostics.some((finding) => finding.severity === "error") && fixtures.every((fixture) => fixture.ok);
114
- return {
115
- kind: "email",
116
- ok,
117
- compilerVersion: COMPILER_VERSION,
118
- diagnostics,
119
- templates: [{
120
- name: template.id,
121
- channel: "email",
122
- ok,
123
- source: String(sourceFiles[entryPath]),
124
- inputSchema: template.schema.schema,
125
- compiled: loaded.compiled,
126
- fixtures
127
- }]
128
- };
129
- })().catch(emailFailure);
130
- entries.push({
131
- id: file.id,
132
- kind: file.kind,
133
- preview
134
- });
135
- continue;
136
- }
137
- const preview = emitModulePreview(parse(file.id).name, evaluated.moduleExports);
138
- if (preview.templates.length === 0) continue;
139
- if (preview.templates.some((template) => /^\s*<Email(?:\s|>)/.test(template.source ?? ""))) entries.push({
140
- id: file.id,
141
- kind: file.kind,
142
- preview: emailFailure(`${file.id}: email templates must default-export defineEmail with an input schema and fixtures.`, "email-requires-object-template")
143
- });
144
- else entries.push({
145
- id: file.id,
146
- kind: file.kind,
147
- preview: {
148
- ...preview,
149
- kind: "structured"
150
- }
151
- });
152
- }
153
- return { files: entries };
154
- };
155
- const fileEvents = /* @__PURE__ */ new Set([
156
- "add",
157
- "change",
158
- "unlink"
159
- ]);
160
- const subscribe = (listener) => {
161
- const onChange = (event, path) => {
162
- if (fileEvents.has(event)) listener(path);
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
163
40
  };
164
- server.watcher.on("all", onChange);
165
- return () => server.watcher.off("all", onChange);
41
+ });
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
55
+ };
56
+ return {
57
+ id: entry.entryPath,
58
+ kind: "tsx",
59
+ ok,
60
+ template,
61
+ diagnostics
166
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);
167
73
  return {
168
- build,
169
- subscribe,
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;
90
+ return {
91
+ build: async () => summarize(await session.load(), options.assetBase),
92
+ subscribe: session.subscribe,
170
93
  close: async () => {
171
94
  if (closed) return;
172
95
  closed = true;
173
- try {
174
- await emailRenderer?.close();
175
- } finally {
176
- await server.close();
177
- }
96
+ await session.close();
178
97
  }
179
98
  };
180
99
  };
181
- const createEvaluator = async (root) => {
182
- const vite = await import("vite").catch(() => void 0);
183
- if (vite === void 0) throw new Error("`vite` is required for template authoring. Install it as a dev dependency.");
184
- return vite.createServer({
185
- root,
186
- configFile: false,
187
- logLevel: "silent",
188
- server: {
189
- middlewareMode: true,
190
- hmr: false
191
- },
192
- oxc: { jsx: {
193
- runtime: "automatic",
194
- importSource: "@samva/markup"
195
- } },
196
- optimizeDeps: {
197
- noDiscovery: true,
198
- include: []
199
- }
200
- });
201
- };
202
100
  //#endregion
203
- export { buildTemplates, createTemplateBuildSession };
101
+ export { COMPILER, buildTemplates, createTemplateBuildSession };
204
102
 
205
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-Cow7zZ_9.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