@samva/vite 0.1.2 → 0.3.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 CHANGED
@@ -5,4 +5,73 @@ Install `@samva/markup`, `@samva/vite`, and the Vite peer in the template projec
5
5
  programmatic template builds and `@samva/vite/emit` only for module-preview integration.
6
6
 
7
7
  The CLI loads `@samva/vite/build` from the current project. Keep these authoring dependencies in the
8
- project lockfile so local preview and `samva templates push` use the same compiler version.
8
+ project lockfile so local preview, check, and build use the same compiler version as the project.
9
+
10
+ Hosted synchronization is ordinary Git. A CLI-created project has a repository-local Samva
11
+ credential helper, and `samva templates publish` accepts only a clean exact commit reachable from
12
+ the fetched `origin` default branch. Publish creates an immutable receipt; it does not push or
13
+ rewrite history.
14
+
15
+ Template discovery is `.tsx` only. The local email editor writes the canonical TSX entry with a
16
+ revision check and a fresh comparison against disk; SMS and WhatsApp stay read-only. Every discovered entry is imported once through the dev
17
+ server's own SSR graph, and a default export that is a template definition takes the email lane —
18
+ compiled and rendered through `compileEmailProject`, so a local render cannot drift from a published
19
+ one. Email requires an executable definition. Structured SMS/WhatsApp entries emit SML, and
20
+ for SMS and WhatsApp an invalid current TSX keeps and visibly labels the last successful emission
21
+ while the same file remains discovered; deleting the file removes it.
22
+
23
+ The email document is one executed render of one declared fixture: the fixture names the template
24
+ declares, the fixture being viewed, the render stamped with `data-samva-instance`, and the compile
25
+ and client findings taken from rendering every declared fixture as it will be delivered. An entry
26
+ that does not build keeps its place with a null render and the findings that stopped it. Its
27
+ revision is a digest of authored source and what the declared fixtures render, so source locations
28
+ cannot stay pinned after a formatting-only edit. It stays put when only the viewed fixture changes. The dev server is not an https origin, so a local
29
+ render points at the editor's own asset route by path — every render here is a preview or the input
30
+ to a finding, and the base a publication serves from is a build input rather than authored.
31
+
32
+ Object email projects compile through `@samva/vite/email`. It is an adapter: the whole compile step
33
+ is `@samva/markup/compiler`'s `compileEmailProject`, the same call the hosted build makes, and this
34
+ package owns only module resolution and evaluation. The asset base is an explicit input, so a
35
+ build and an export can address the same content from different origins. Both entry points evaluate
36
+ the compiler's generated wrapper rather than the authored entry: the render has to happen inside the
37
+ graph the project's stylesheets registered into.
38
+
39
+ `createEmailRenderer` keeps one evaluator across renders, as a dev server does, and gives each
40
+ project its own id namespace so their modules coexist. Anything the compiler rewrote into
41
+ JavaScript — a stylesheet, an imported asset — is served under a `.js` id; its authored extension
42
+ would be claimed by Vite's CSS or asset pipeline. A caller that reloads the same project passes a
43
+ stable `namespace` so ids survive the reload and `invalidate()` is what makes changed source take
44
+ effect; omitting it takes a fresh namespace, which is what isolates unrelated projects.
45
+
46
+ `@samva/vite/project` is the folder-of-templates surface. A project holds many definitions with
47
+ shared components and CSS between them; which `.tsx` files are templates is decided by evaluating
48
+ the compiler's generated wrapper and asking whether the default export is a template definition, so
49
+ an ordinary helper module needs no marker and no exclusion list. Identity is the id the definition
50
+ declares, never its path, and two definitions declaring one id is a reported error. Fixtures are
51
+ validated against the template's own schema before they render, so a bad fixture is a source defect
52
+ the author sees locally.
53
+
54
+ `exportEmailProject` renders in the compiler's export mode (`relativeUrls: true`). It writes one
55
+ page at `templates/<id>/<fixture>.html` and every asset once under `assets/`, so its default
56
+ `../../assets` base is the path from a page's own directory to those bytes. Delivery keeps
57
+ refusing a relative reference — an inbox has no directory to resolve one against — so an export
58
+ destined for sending names the https origin it will be served from instead. Either way the export
59
+ writes each asset under its content address and records the base, so the manifest is the whole
60
+ mapping.
61
+
62
+ A refused render reports against the one fixture rather than throwing at the caller: one template
63
+ that cannot render must not take the rest of the catalog with it.
64
+
65
+ The evaluator roots itself at this package's own directory and refuses an ambient Vite config.
66
+ Virtual ids have no directory, so bare imports resolve against the server root; rooting here is what
67
+ makes `@samva/markup` resolve under a per-package install as well as a hoisted one.
68
+
69
+ A golden-digest test in `tests/email-parity.test.ts` pins this adapter's output against the same
70
+ value the hosted build asserts in `apps/agents/tests/workerd/email-parity.test.ts`.
71
+
72
+ ## Verification
73
+
74
+ `bun run --filter @samva/vite test:browser` builds the plugin and embedded editor, then runs the
75
+ Chromium source-editing integration tests. `test:browser:cross` runs Chromium, Firefox, and WebKit.
76
+ These tests own temporary template projects and plugin servers; the editor library's browser suite
77
+ tests its standalone host separately.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @samva/vite
2
2
 
3
3
  The unified local editor for Samva email, SMS, and WhatsApp templates. One Vite
4
- plugin discovers `.samva` and `.tsx` files, evaluates TSX through the Samva JSX
5
- runtime, applies the project theme, and serves the full visual editor with HMR.
4
+ plugin discovers `.tsx` files, evaluates them through the Samva JSX runtime,
5
+ applies the project theme, and serves the full visual editor with HMR.
6
6
 
7
7
  ## Install
8
8
 
@@ -12,9 +12,8 @@ Keep the authoring library and editor in the project, with Vite provided as the
12
12
  npm install --save-dev @samva/markup @samva/vite vite
13
13
  ```
14
14
 
15
- `samva templates init` writes these dependencies, an equivalent `samva.vite.config.ts`, and
16
- collision-resistant `samva:dev` / `samva:push` package scripts. Existing project scripts remain
17
- untouched.
15
+ `samva templates init` creates a project from the canonical starter, which contains these
16
+ dependencies and an equivalent `vite.config.ts`. Existing project scripts remain untouched.
18
17
 
19
18
  ```ts
20
19
  import { samvaEditor } from "@samva/vite";
@@ -44,65 +43,114 @@ to the rest of the Vite middleware stack.
44
43
 
45
44
  ## Authoring files
46
45
 
47
- - `.samva` files are editable for every channel.
48
- - `.tsx` files are code-authored and always read-only in the editor. The source
49
- view shows authored TSX beside the emitted SML used for rendering.
50
- - A `.tsx` file contains at most one template, and that template must be its
51
- `default` export. Named exports are ignored, so they remain available for
52
- helpers and constants. A file without a default export is excluded with a
53
- warning; an invalid default export is a diagnostic.
46
+ Email `.tsx` entries default-export `defineEmail({ id, schema, fixtures, render })` from
47
+ `@samva/markup/template`. Use the `@samva/markup/email` JSX runtime and primitives from
48
+ `@samva/markup/email/components`. Ordinary synchronous helpers, conditions and loops execute
49
+ against actual validated JSON. Shared helper modules are excluded from the template catalog;
50
+ ids are stable independently of paths and must be unique within the project.
54
51
 
55
52
  ```tsx
56
- import { Email, Text } from "@samva/markup/components";
57
- import type { Variables } from "@samva/markup/variables";
58
-
59
- export const subjectPrefix = "Hello"; // helper, ignored by discovery
60
-
61
- export default function Welcome(v: Variables<{ firstName: string }>) {
62
- return (
63
- <Email>
64
- <Text>
65
- {subjectPrefix}, {v.firstName}
66
- </Text>
67
- </Email>
68
- );
69
- }
53
+ /** @jsxImportSource @samva/markup/email */
54
+ import { Schema } from "effect";
55
+ import { Email, Section } from "@samva/markup/email/components";
56
+ import { defineEmail } from "@samva/markup/template";
57
+
58
+ export default defineEmail({
59
+ id: "welcome",
60
+ schema: Schema.toStandardJSONSchemaV1(
61
+ Schema.Struct({ firstName: Schema.String, items: Schema.Array(Schema.String) }),
62
+ ),
63
+ fixtures: { basic: { firstName: "Ada", items: ["Your first project"] } },
64
+ render: (input) => ({
65
+ subject: `Welcome, ${input.firstName}`,
66
+ preheader: "Your account is ready",
67
+ body: (
68
+ <Email>
69
+ <Section>
70
+ <h1>Welcome, {input.firstName}</h1>
71
+ {input.items.length > 0 && (
72
+ <ul>
73
+ {input.items.map((item) => (
74
+ <li key={item}>{item}</li>
75
+ ))}
76
+ </ul>
77
+ )}
78
+ </Section>
79
+ </Email>
80
+ ),
81
+ }),
82
+ });
70
83
  ```
71
84
 
72
- The editor keeps preview, SML, compiled HTML/plain text or channel payload,
73
- light/dark email rendering, responsive widths, diagnostics, and variable samples
74
- in one surface. Editing a template, imported dependency, or `theme.css` refreshes
75
- the relevant catalog and open document.
85
+ The email editor previews declared fixtures and offers source-aware edits where the source intent
86
+ is unambiguous. Source is canonical. Failed email entries remain visible with diagnostics and a
87
+ null render. SMS and WhatsApp remain read-only structured previews; an invalid edit retains their
88
+ last successful emission and labels it stale. Deleting an entry removes its preview.
89
+
90
+ The browser canvas is a structural preview of Samva's modern and Outlook-safe
91
+ compiler output. It does not emulate the exact Gmail, Outlook, Apple Mail, or
92
+ Yahoo rendering engines. Use compatibility findings and inbox tests for
93
+ client-specific proof.
76
94
 
77
95
  ## Options
78
96
 
79
97
  | Option | Type | Default | Description |
80
98
  | -------------- | ----------------- | ------------- | ---------------------------------------------------------------- |
81
- | `templatesDir` | `string` | `"emails"` | Recursive `.samva` / `.tsx` template directory. |
99
+ | `templatesDir` | `string` | `"emails"` | Recursive `.tsx` template directory. |
82
100
  | `route` | `string` | `"/"` | Absolute editor UI route; API and assets are mounted beneath it. |
83
101
  | `theme` | `string \| false` | `"theme.css"` | Project theme file, or `false` to disable discovery. |
84
- | `compile` | `CompileOptions` | — | Extra compile options applied to local rendering and validation. |
85
102
 
86
- ## Theme and push
103
+ ## Theme, check, and publish
87
104
 
88
105
  A root `theme.css` uses Tailwind v4 `@theme` syntax. Theme changes recompile
89
106
  local previews while preserving the last valid theme when an edit is temporarily
90
107
  invalid.
91
108
 
92
- `samva templates push --dir emails` and `buildTemplates()` run the same default-export
93
- TSX evaluation and emit pipeline. They send or return SML, never compiled HTML;
94
- the platform recompiles the SML with the organization theme.
109
+ `samva templates check` and `buildTemplates()` execute every email fixture and validate its
110
+ actual input. `buildTemplates()` returns `files`, each with a `preview.kind` discriminator:
111
+ `email` contains input schema, compiled module graph, actual fixture subject/HTML/text and
112
+ structured diagnostics; `structured` contains the SMS/WhatsApp preview. Helper modules have no
113
+ file entry. An email requires at least one fixture.
114
+
115
+ `samva templates build --dir emails --out dist/templates.json` writes the CLI's version 2 local
116
+ artifact. Check writes no files. `check --watch` observes changes; `--jsonl` emits one observation
117
+ per rebuild and `--json` is one-shot. A successful fixture check does not prove every possible
118
+ input branch.
95
119
 
96
- The runnable `examples/email-starter` workspace exercises this exact setup.
120
+ Imported email assets require `assetBase` in `buildTemplates()` or `--asset-base` in CLI checks
121
+ and builds, naming their actual HTTPS serving base. No credentials are needed for local work.
97
122
 
98
- ## Template identity
123
+ For a hosted project, commit and push the checked TSX tree with ordinary Git, then publish one exact
124
+ commit:
125
+
126
+ ```sh
127
+ git add .
128
+ git commit -m "feat: update welcome email"
129
+ git push origin main
130
+ samva templates publish --commit HEAD
131
+ ```
99
132
 
100
- Template identity comes from the file name, and only the default export is
101
- evaluated — `welcome-email.tsx` owns the platform slug `welcome-email`.
102
- Renaming a file changes template identity; the CLI treats the new name as a
103
- different template and `--force` does not remap identities. Direct
104
- `@samva/vite/emit` callers pass the file-backed template name:
133
+ The CLI's repository-local credential helper obtains a short-lived Artifact credential through
134
+ Samva's credential protocol when Git needs one. It validates the repository host/path and does not
135
+ store the token in Git config or the remote URL. `templates publish` requires a clean worktree and
136
+ an exact commit reachable from the fetched `origin` default branch; it creates an immutable
137
+ publication receipt and does not push or rewrite history.
138
+
139
+ Hosted builds consume the committed Bun text lockfile version 2 or npm lockfile version 3.
140
+ Keep dependencies and the supported lockfile in the project. Sending selects an exact immutable
141
+ publication by default; later source edits do not change that publication.
142
+
143
+ ## Local export
105
144
 
106
145
  ```ts
107
- emitModulePreview("welcome-email", moduleExports, compileOptions);
146
+ import { exportEmailProject } from "@samva/vite/project";
147
+
148
+ const receipt = await exportEmailProject({ dir: ".", out: "dist/email" });
108
149
  ```
150
+
151
+ Export writes `templates/<id>/<fixture>.html` and `.txt`, content-addressed files under `assets/`,
152
+ and `catalog.json` with the asset mapping and findings. The default `../../assets` base resolves
153
+ from each exported page. For delivery, supply the actual HTTPS `assetBase` and serve those bytes
154
+ there; relative URLs are for local export. External HTTPS images stay external references.
155
+
156
+ The runnable `examples/email-starter` workspace exercises the authoring setup.
package/dist/build.d.ts CHANGED
@@ -1,42 +1,61 @@
1
1
  import { ModulePreview } from "./emit.js";
2
- import { CompileOptions } from "@samva/markup/compile";
3
- import { Diagnostic } from "@samva/markup/diagnostics";
2
+ import { CompiledEmailProject } from "@samva/markup/compiler";
3
+ import { EmailDiagnostic } from "@samva/markup/email/diagnostics";
4
4
  //#region src/build.d.ts
5
+ /** One declared input rendered through executable email source. */
6
+ interface EmailFixtureBuild {
7
+ readonly name: string;
8
+ readonly input: unknown;
9
+ readonly ok: boolean;
10
+ readonly subject?: string | undefined;
11
+ readonly preheader?: string | undefined;
12
+ readonly html?: string | undefined;
13
+ readonly text?: string | undefined;
14
+ readonly diagnostics: readonly EmailDiagnostic[];
15
+ }
16
+ /** Executable email artifacts retain their input contract, module graph and actual fixture output. */
17
+ interface EmailBuildPreview {
18
+ readonly kind: "email";
19
+ readonly ok: boolean;
20
+ readonly compilerVersion: string;
21
+ readonly diagnostics: readonly EmailDiagnostic[];
22
+ readonly templates: readonly {
23
+ readonly name: string;
24
+ readonly channel: "email";
25
+ readonly ok: boolean;
26
+ readonly source: string;
27
+ readonly inputSchema: Readonly<Record<string, unknown>>;
28
+ readonly compiled: CompiledEmailProject;
29
+ readonly fixtures: readonly EmailFixtureBuild[];
30
+ }[];
31
+ }
32
+ type TemplateBuildPreview = EmailBuildPreview | (ModulePreview & {
33
+ readonly kind: "structured";
34
+ });
5
35
  interface BuildTemplatesResult {
6
- /** One entry per discovered template file, in discovery (sorted) order. */
36
+ /** Authored entry paths, sorted; helper modules have no entry here. */
7
37
  readonly files: ReadonlyArray<{
8
38
  readonly id: string;
9
- readonly kind: "tsx" | "samva";
10
- readonly preview: ModulePreview;
39
+ readonly kind: "tsx";
40
+ readonly preview: TemplateBuildPreview;
11
41
  }>;
12
- /**
13
- * Project theme.css parse diagnostics (spans into theme.css). `buildTemplates`
14
- * always sets it (empty when the theme parsed or there is no theme file);
15
- * optional so callers constructing a result by hand needn't supply it.
16
- */
17
- readonly themeDiagnostics?: ReadonlyArray<Diagnostic> | undefined;
18
42
  }
19
43
  interface BuildTemplatesOptions {
20
- readonly compile?: CompileOptions | undefined;
21
- /**
22
- * Project root to discover `theme.css` from (design brief §7e), themed into
23
- * every compiled preview as the org-token layer. Defaults to the parent of
24
- * `dir` (templates live under the project root, e.g. `emails/`).
25
- */
44
+ /** Project root containing shared imports and theme.css; defaults to the parent of dir. */
26
45
  readonly root?: string | undefined;
27
- /**
28
- * Theme file override, relative to `root`. `false` disables theme discovery.
29
- * Default: `theme.css`.
30
- */
31
46
  readonly theme?: string | false | undefined;
47
+ /** Required for imported email assets. Must be their actual HTTPS serving base. */
48
+ readonly assetBase?: string | undefined;
49
+ }
50
+ interface TemplateBuildSession {
51
+ readonly build: () => Promise<BuildTemplatesResult>;
52
+ readonly subscribe: (listener: (path: string) => void) => () => void;
53
+ readonly close: () => Promise<void>;
32
54
  }
33
- /**
34
- * Discovers and evaluates every `.tsx`/`.samva` template under `dir`, returning
35
- * emitted SML + compiled artifacts per template — themed by the project's
36
- * `theme.css` if present. Throws if `vite` is not installed (the optional peer) —
37
- * the caller surfaces that as a user error.
38
- */
55
+ /** Execute each email definition's fixtures; compile structured SMS/WhatsApp through their channel compiler. */
39
56
  declare const buildTemplates: (dir: string, options?: BuildTemplatesOptions) => Promise<BuildTemplatesResult>;
57
+ /** A watcher and module evaluator over the project's installed authoring toolchain. */
58
+ declare const createTemplateBuildSession: (dir: string, options?: BuildTemplatesOptions) => Promise<TemplateBuildSession>;
40
59
  //#endregion
41
- export { BuildTemplatesOptions, BuildTemplatesResult, buildTemplates };
60
+ export { BuildTemplatesOptions, BuildTemplatesResult, EmailBuildPreview, EmailFixtureBuild, TemplateBuildPreview, TemplateBuildSession, buildTemplates, createTemplateBuildSession };
42
61
  //# sourceMappingURL=build.d.ts.map
package/dist/build.js CHANGED
@@ -1,87 +1,186 @@
1
- import { a as discoverTemplateFiles, i as withThemeLayer, n as loadProjectTheme, r as resolveThemePath } from "./theme-CfJe8s3k.js";
2
- import { COMPILER_VERSION, compileSourcePreview, emitModulePreview } from "./emit.js";
3
- import { dirname, parse, resolve } from "node:path";
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";
4
6
  //#region src/build.ts
5
- /**
6
- * Discovers and evaluates every `.tsx`/`.samva` template under `dir`, returning
7
- * emitted SML + compiled artifacts per template — themed by the project's
8
- * `theme.css` if present. Throws if `vite` is not installed (the optional peer) —
9
- * the caller surfaces that as a user error.
10
- */
7
+ /** Execute each email definition's fixtures; compile structured SMS/WhatsApp through their channel compiler. */
11
8
  const buildTemplates = async (dir, options = {}) => {
12
- const templatesDir = resolve(dir);
13
- const root = options.root ?? dirname(templatesDir);
14
- const theme = await loadProjectTheme(resolveThemePath(root, options.theme));
15
- const compile = withThemeLayer(options.compile, theme.tokens);
16
- const files = await discoverTemplateFiles(templatesDir);
17
- const server = files.filter((file) => file.kind === "tsx").length > 0 ? await createEvaluator(templatesDir) : void 0;
9
+ const session = await createTemplateBuildSession(dir, options);
18
10
  try {
19
- return {
20
- files: await Promise.all(files.map(async (file) => {
21
- if (file.kind === "samva") {
22
- const { readFile } = await import("node:fs/promises");
23
- const source = await readFile(file.path, "utf8");
24
- const name = file.id.replace(/\.samva$/, "");
25
- const template = compileSourcePreview(name, source, compile);
26
- const preview = {
27
- ok: template.ok,
28
- compilerVersion: COMPILER_VERSION,
29
- templates: [template],
30
- diagnostics: []
31
- };
32
- return {
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))]
28
+ });
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({
33
77
  id: file.id,
34
78
  kind: file.kind,
35
- preview
36
- };
79
+ preview: emailFailure(`Duplicate template id ${definition.id}: ${previous} and ${file.id}`, "duplicate-template-id")
80
+ });
81
+ continue;
37
82
  }
38
- const name = parse(file.id).name;
39
- const evaluated = await server.ssrLoadModule(file.path).then((value) => ({
40
- ok: true,
41
- moduleExports: value
42
- })).catch((cause) => ({
43
- ok: false,
44
- message: cause instanceof Error ? cause.message : String(cause)
45
- }));
46
- if (!evaluated.ok) return {
47
- id: file.id,
48
- kind: file.kind,
49
- preview: {
50
- ok: false,
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,
51
117
  compilerVersion: COMPILER_VERSION,
52
- templates: [],
53
- diagnostics: [{
54
- code: "module-load-error",
55
- message: evaluated.message,
56
- span: {
57
- start: 0,
58
- end: 0
59
- }
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
60
127
  }]
61
- }
62
- };
63
- return {
128
+ };
129
+ })().catch(emailFailure);
130
+ entries.push({
64
131
  id: file.id,
65
132
  kind: file.kind,
66
- preview: emitModulePreview(name, evaluated.moduleExports, compile)
67
- };
68
- })),
69
- themeDiagnostics: theme.diagnostics
70
- };
71
- } finally {
72
- await server?.close();
73
- }
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 subscribe = (listener) => {
156
+ const onChange = (_event, path) => listener(path);
157
+ server.watcher.on("all", onChange);
158
+ return () => server.watcher.off("all", onChange);
159
+ };
160
+ return {
161
+ build,
162
+ subscribe,
163
+ close: async () => {
164
+ if (closed) return;
165
+ closed = true;
166
+ try {
167
+ await emailRenderer?.close();
168
+ } finally {
169
+ await server.close();
170
+ }
171
+ }
172
+ };
74
173
  };
75
174
  const createEvaluator = async (root) => {
76
175
  const vite = await import("vite").catch(() => void 0);
77
- if (vite === void 0) throw new Error("`vite` is required for `samva templates push`. Install it as a dev dependency (it ships as an optional peer of @samva/vite).");
176
+ if (vite === void 0) throw new Error("`vite` is required for template authoring. Install it as a dev dependency.");
78
177
  return vite.createServer({
79
178
  root,
179
+ configFile: false,
80
180
  logLevel: "silent",
81
181
  server: {
82
182
  middlewareMode: true,
83
- hmr: false,
84
- watch: null
183
+ hmr: false
85
184
  },
86
185
  oxc: { jsx: {
87
186
  runtime: "automatic",
@@ -94,6 +193,6 @@ const createEvaluator = async (root) => {
94
193
  });
95
194
  };
96
195
  //#endregion
97
- export { buildTemplates };
196
+ export { buildTemplates, createTemplateBuildSession };
98
197
 
99
198
  //# sourceMappingURL=build.js.map