@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/README.md CHANGED
@@ -1,31 +1,39 @@
1
1
  # @samva/vite
2
2
 
3
- The unified local editor for Samva email, SMS, and WhatsApp templates. One Vite
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.
3
+ The local editor, dev server and build for Samva templates. A template is one TSX file that
4
+ `@samva/markup` compiles without running it; this package serves the visual editor over the
5
+ compiled templates, and checks, builds, renders and exports them, so what you preview is what a
6
+ publication renders.
6
7
 
7
- ## Install
8
+ ## Start
8
9
 
9
- Keep the authoring library and editor in the project, with Vite provided as the editor's peer:
10
+ `samva templates dev` bundles the dev server with sensible defaults, so most projects configure
11
+ nothing and need no `vite.config.ts`:
10
12
 
11
13
  ```sh
12
- npm install --save-dev @samva/markup @samva/vite vite
14
+ samva templates dev
13
15
  ```
14
16
 
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.
17
+ `samva templates init` creates a project from the canonical starter. Templates live in `templates/`,
18
+ and `emails/` is also recognized. `@samva/vite` stays a public, configurable plugin for setups
19
+ those defaults cannot cover: templates in another folder, a monorepo, or a Vite server that also
20
+ runs your other plugins.
21
+
22
+ ```sh
23
+ npm install --save-dev @samva/markup @samva/vite vite
24
+ ```
17
25
 
18
26
  ```ts
19
27
  import { samvaEditor } from "@samva/vite";
20
28
  import { defineConfig } from "vite";
21
29
 
22
30
  export default defineConfig({
23
- plugins: [samvaEditor({ templatesDir: "emails" })],
31
+ plugins: [samvaEditor()],
24
32
  });
25
33
  ```
26
34
 
27
- Run `vite` and open `http://localhost:5173/`. The default assumes this dev server
28
- exists only for Samva authoring:
35
+ Run `vite` and open `http://localhost:5173/`. The default assumes this dev server exists only for
36
+ Samva authoring:
29
37
 
30
38
  - editor UI: `/`
31
39
  - editor API and event streams: `/api/*`
@@ -34,113 +42,153 @@ exists only for Samva authoring:
34
42
  To share a Vite server with another app, choose a route:
35
43
 
36
44
  ```ts
37
- samvaEditor({ templatesDir: "emails", route: "/__samva" });
45
+ samvaEditor({ route: "/__samva" });
38
46
  ```
39
47
 
40
- That mounts the UI at `/__samva/`, the API at `/__samva/api/*`, and the bundled
41
- assets at `/__samva/assets/*`. Requests outside the configured route pass through
42
- to the rest of the Vite middleware stack.
48
+ That mounts the UI at `/__samva/`, the API at `/__samva/api/*`, and the bundled assets at
49
+ `/__samva/assets/*`. Requests outside the configured route pass through to the rest of the Vite
50
+ middleware stack.
51
+
52
+ ## Options
53
+
54
+ | Option | Type | Default | Description |
55
+ | -------------- | ----------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------- |
56
+ | `templatesDir` | `string` | `templates/` and `emails/` | The one directory to scan for `.tsx` templates, relative to the project root. Unset scans whichever exist. |
57
+ | `projectRoot` | `string` | the Vite root | The folder holding `theme.css`, shared imports and `.samva/`, relative to the Vite root. Contains `templatesDir`. |
58
+ | `route` | `string` | `"/"` | Absolute editor UI route; API and assets are mounted beneath it. |
59
+ | `theme` | `string \| false` | `"theme.css"` | Project theme file relative to the project root, or `false` to compile without one. |
60
+
61
+ For templates in another folder of a monorepo, set `projectRoot` to a folder that contains both the
62
+ templates and the files they import; a templates directory outside the project root is an error.
43
63
 
44
64
  ## Authoring files
45
65
 
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.
66
+ A template default-exports `defineTemplate` from `@samva/markup` and is written in the static
67
+ profile: ordinary JSX, HTML and Tailwind that the compiler reads and never executes. The editor
68
+ previews every declared fixture, offers source-aware edits where the source intent is unambiguous,
69
+ and saves them to the file.
51
70
 
52
71
  ```tsx
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";
72
+ import { defineTemplate } from "@samva/markup";
73
+ import { Email, Section } from "@samva/markup/email";
74
+ import { jsonSchema } from "@samva/markup/input-schema";
57
75
 
58
- export default defineEmail({
76
+ export default defineTemplate({
59
77
  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: (
78
+ schema: jsonSchema<{ firstName: string }>({
79
+ type: "object",
80
+ required: ["firstName"],
81
+ properties: { firstName: { type: "string" } },
82
+ additionalProperties: false,
83
+ }),
84
+ fixtures: { basic: { firstName: "Ada" } },
85
+ email: {
86
+ subject: (input) => `Welcome, ${input.firstName}`,
87
+ preheader: () => "Your account is ready",
88
+ body: (input) => (
68
89
  <Email>
69
90
  <Section>
70
91
  <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
92
  </Section>
79
93
  </Email>
80
94
  ),
81
- }),
95
+ },
82
96
  });
83
97
  ```
84
98
 
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.
94
-
95
- ## Options
99
+ Any other `.tsx` module in the templates directory (a partial, a component) is an ordinary module
100
+ that templates import; a module is a template only if its default export is `defineTemplate`.
101
+ Identity is the `id` the definition declares, unique within the project. A template that declares
102
+ `sms` or `whatsapp` beside `email` shows one editor document per channel over the same file.
96
103
 
97
- | Option | Type | Default | Description |
98
- | -------------- | ----------------- | ------------- | ---------------------------------------------------------------- |
99
- | `templatesDir` | `string` | `"emails"` | Recursive `.tsx` template directory. |
100
- | `route` | `string` | `"/"` | Absolute editor UI route; API and assets are mounted beneath it. |
101
- | `theme` | `string \| false` | `"theme.css"` | Project theme file, or `false` to disable discovery. |
104
+ The browser canvas is a structural preview of Samva's modern and Outlook-safe compiler output. It
105
+ does not emulate the exact Gmail, Outlook, Apple Mail, or Yahoo rendering engines. Use
106
+ compatibility findings and inbox tests for client-specific proof.
102
107
 
103
108
  ## Theme, check, and publish
104
109
 
105
110
  A root `theme.css` uses Tailwind v4 `@theme` syntax and may `@import` other project `.css` files
106
- and `samva:brand`; any other import is a compile error. Changes to the theme or a stylesheet it
107
- imports recompile
108
- local previews while preserving the last valid theme when an edit is temporarily
109
- invalid.
111
+ and `samva:brand`; any other import is a compile error. A change to the theme, a partial, or an
112
+ imported stylesheet recompiles every template that reads it.
113
+
114
+ `samva templates check` and `buildTemplates()` compile every template, render every declared
115
+ fixture through each channel the template declares, and check the email output against the client
116
+ matrix. `buildTemplates()` returns `files`, one per template entry, with `ok`, the template's IR,
117
+ input schema, each fixture's rendered output, and structured diagnostics; helper modules have no
118
+ entry. A template requires at least one fixture.
119
+
120
+ ```ts
121
+ import { buildTemplates } from "@samva/vite/build";
122
+
123
+ const result = await buildTemplates({ root: ".", assetBase: "https://assets.example.com/p" });
124
+ ```
125
+
126
+ `buildTemplates(options)` and `createTemplateBuildSession(options)` take `root`, `dir`, `theme`,
127
+ `assetBase` and `offline`. Imported email assets require an HTTPS `assetBase`, which names where
128
+ their bytes are served in delivered messages. `createTemplateBuildSession` also reports file
129
+ changes for `check --watch`.
130
+
131
+ `samva templates build --out dist/templates.json` writes the CLI's local artifact. Check writes no
132
+ files. A successful fixture check does not prove every possible input branch.
133
+
134
+ ## Render one fixture
135
+
136
+ `samva templates render` runs this package's `render` entry from your project, so it uses the
137
+ compiler version your lockfile pins. The results are plain JSON.
138
+
139
+ ```ts
140
+ import { renderChannel, renderTemplate } from "@samva/vite/render";
141
+
142
+ const email = await renderTemplate({ root: ".", template: "welcome", fixture: "basic" });
143
+ // { ok: true, subject, preheader?, html, text, diagnostics } or { ok: false, diagnostics }
144
+
145
+ const sms = await renderChannel({ root: ".", template: "order", fixture: "first", channel: "sms" });
146
+ ```
147
+
148
+ `template` is a template id or the project-relative path of its entry. `locale` and `timeZone`
149
+ override the formatters' defaults.
110
150
 
111
- `samva templates check` and `buildTemplates()` execute every email fixture and validate its
112
- actual input. `buildTemplates()` returns `files`, each with a `preview.kind` discriminator:
113
- `email` contains input schema, compiled module graph, actual fixture subject/HTML/text and
114
- structured diagnostics; `structured` contains the SMS/WhatsApp preview. Helper modules have no
115
- file entry. An email requires at least one fixture.
151
+ ## Dev server without a config file
116
152
 
117
- `samva templates build --dir emails --out dist/templates.json` writes the CLI's version 2 local
118
- artifact. Check writes no files. `check --watch` observes changes; `--jsonl` emits one observation
119
- per rebuild and `--json` is one-shot. A successful fixture check does not prove every possible
120
- input branch.
153
+ ```ts
154
+ import { startDevServer } from "@samva/vite/dev";
155
+
156
+ const server = await startDevServer({ root: ".", port: 5173, open: true });
157
+ console.log(server.url);
158
+ await server.close();
159
+ ```
121
160
 
122
- Imported email assets require `assetBase` in `buildTemplates()` or `--asset-base` in CLI checks
123
- and builds, naming their actual HTTPS serving base.
161
+ `startDevServer` creates a Vite server with `configFile: false` and `samvaEditor()`, which is what
162
+ `samva templates dev` does.
124
163
 
125
164
  ## The organization brand
126
165
 
127
166
  A project that imports `samva:brand` (the default brand) or `samva:brand/<slug>` compiles against
128
- its organization's brand. The editor, `buildTemplates()`, and `exportEmailProject()` resolve it in
129
- this order:
167
+ its organization's brand. The platform brand record is the source of truth; each publication pins
168
+ the brand version it was built with. The editor, `buildTemplates()`, and `exportProject()` resolve
169
+ it in this order:
130
170
 
131
171
  1. **Live.** With `SAMVA_API_KEY` in the environment or the project's `.env` (and `SAMVA_API_URL`
132
172
  for another API), the brand is fetched directly. Without one, the `samva` CLI on `PATH` is asked
133
173
  (`samva brands get --json`) and answers with your `samva login` session, which stays in the
134
- operating-system keyring the CLI owns.
135
- 2. **Snapshot.** `.samva/brands/<slug>.json`, written by `samva brands pull [slug]`, with a
136
- `brand-offline` warning.
174
+ operating-system keyring the CLI owns. A live answer refreshes the cached copy.
175
+ 2. **Cached copy.** `.samva/brands/<slug>.json`, written by every live fetch and by
176
+ `samva brands pull [slug]`, with a `brand-offline` warning.
137
177
  3. **Neither.** The build fails with `brand-unavailable`, naming both fixes.
138
178
 
139
- When the live brand's digest differs from the snapshot's, the build warns with
140
- `brand-snapshot-stale`; run `samva brands pull` again. Warnings are build diagnostics and editor
141
- log lines; they never fail a build. The editor and `check --watch` reuse a resolution briefly,
142
- so a brand edited elsewhere arrives within seconds rather than on the next save. Keep `.samva` in
143
- `.gitignore`. A project that imports no brand needs no credentials for local work.
179
+ When the live brand's digest differs from the cached copy's, the build warns with
180
+ `brand-snapshot-stale` and refreshes the copy. Warnings are build diagnostics and editor log lines;
181
+ they never fail a build. The editor and `check --watch` reuse a resolution briefly, so a brand
182
+ edited elsewhere arrives within seconds rather than on the next save. A project that imports no
183
+ brand needs no credentials for local work.
184
+
185
+ ## The `.samva/` folder
186
+
187
+ `.samva/` holds files this package generates in your project, the way `.next/` and `.tanstack/`
188
+ do. Keep it in `.gitignore`. It is not part of the project's source, so changes inside it never
189
+ recompile templates.
190
+
191
+ ## Publish
144
192
 
145
193
  For a hosted project, commit and push the checked TSX tree with ordinary Git, then publish one exact
146
194
  commit:
@@ -156,25 +204,23 @@ The CLI's repository-local credential helper obtains a short-lived Artifact cred
156
204
  Samva's credential protocol when Git needs one. It validates the repository host/path and does not
157
205
  store the token in Git config or the remote URL. `templates publish` requires a clean worktree and
158
206
  an exact commit reachable from the fetched `origin` default branch; it creates an immutable
159
- publication receipt and does not push or rewrite history.
160
-
161
- Hosted builds consume the committed Bun text lockfile version 2 or npm lockfile version 3.
162
- Keep dependencies and the supported lockfile in the project. Sending selects an exact immutable
207
+ publication receipt and does not push or rewrite history. Sending selects an exact immutable
163
208
  publication by default; later source edits do not change that publication.
164
209
 
165
210
  ## Local export
166
211
 
167
212
  ```ts
168
- import { exportEmailProject } from "@samva/vite/project";
213
+ import { exportProject } from "@samva/vite/project";
169
214
 
170
- const receipt = await exportEmailProject({ dir: "emails", root: ".", out: "dist/email" });
215
+ const receipt = await exportProject({ root: ".", out: "dist/email" });
171
216
  ```
172
217
 
173
- `dir` holds the templates and `root` (default `dir`) is the project: its `theme.css` themes the
174
- export, as in `buildTemplates()`, `theme` names another file or `false` disables it, and
175
- `samva:brand` resolves as described above.
218
+ `root` is the project: its `theme.css` themes the export, as in `buildTemplates()`; `dir` narrows
219
+ the templates directory, `theme` names another file or `false` disables it, and `samva:brand`
220
+ resolves as described above.
176
221
 
177
- Export writes `templates/<id>/<fixture>.html` and `.txt`, content-addressed files under `assets/`,
178
- and `catalog.json` with the asset mapping and findings. The default `../../assets` base resolves
179
- from each exported page. For delivery, supply the actual HTTPS `assetBase` and serve those bytes
180
- there; relative URLs are for local export. External HTTPS images stay external references.
222
+ Export writes `templates/<id>/<fixture>.html` and `.txt`, plus `.sms.txt` and `.whatsapp.json` for
223
+ those channels, content-addressed files under `assets/`, and `catalog.json` with the asset mapping
224
+ and findings. The default `../../assets` base resolves from each exported page. For delivery,
225
+ supply the actual HTTPS `assetBase` and serve those bytes there; relative URLs are for local
226
+ export. External HTTPS images stay external references.
package/dist/build.d.ts CHANGED
@@ -1,61 +1,66 @@
1
- import { ModulePreview } from "./emit.js";
2
- import { CompiledEmailProject } from "@samva/markup/compiler";
3
- import { EmailDiagnostic } from "@samva/markup/email/diagnostics";
1
+ import { _ as TemplateChannel, a as ProjectOptions } from "./project-CRqjs-wV.js";
2
+ import { RenderedIrEmail, RenderedIrWhatsApp, TemplateIr } from "@samva/markup/render";
3
+ import { EmailDiagnostic } from "@samva/markup/diagnostics";
4
4
  //#region src/build.d.ts
5
- /** One declared input rendered through executable email source. */
6
- interface EmailFixtureBuild {
5
+ /** The compiler a build ran, as a publication records it. */
6
+ declare const COMPILER: string;
7
+ /** One declared input rendered through every channel the template declares. */
8
+ interface FixtureBuild {
7
9
  readonly name: string;
8
10
  readonly input: unknown;
9
11
  readonly ok: boolean;
10
- readonly subject?: string | undefined;
11
- readonly preheader?: string | undefined;
12
- readonly html?: string | undefined;
13
- readonly text?: string | undefined;
12
+ readonly email?: Pick<RenderedIrEmail, "subject" | "preheader" | "html" | "text"> | undefined;
13
+ readonly sms?: string | undefined;
14
+ readonly whatsapp?: RenderedIrWhatsApp | undefined;
14
15
  readonly diagnostics: readonly EmailDiagnostic[];
15
16
  }
16
- /** Executable email artifacts retain their input contract, module graph and actual fixture output. */
17
- interface EmailBuildPreview {
18
- readonly kind: "email";
17
+ /** A template's IR, its input contract, and what each declared fixture rendered. */
18
+ interface TemplateBuild {
19
+ /** The id the definition declares. */
20
+ readonly template: string;
21
+ readonly channels: readonly TemplateChannel[];
19
22
  readonly ok: boolean;
20
- readonly compilerVersion: string;
23
+ /** Project-relative source of the entry. */
24
+ readonly source: string;
25
+ readonly inputSchema: Readonly<Record<string, unknown>>;
26
+ readonly ir: TemplateIr;
27
+ readonly fixtures: readonly FixtureBuild[];
28
+ /** Compile findings, brand findings and asset-base findings. */
21
29
  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
- }[];
30
+ /** Target-client findings across the email fixtures. */
31
+ readonly compatibility: readonly EmailDiagnostic[];
31
32
  }
32
- type TemplateBuildPreview = EmailBuildPreview | (ModulePreview & {
33
- readonly kind: "structured";
34
- });
35
- interface BuildTemplatesResult {
36
- /** Authored entry paths, sorted; helper modules have no entry here. */
37
- readonly files: ReadonlyArray<{
38
- readonly id: string;
39
- readonly kind: "tsx";
40
- readonly preview: TemplateBuildPreview;
41
- }>;
33
+ /** One template entry of the project. `template` is absent when the entry did not compile. */
34
+ interface BuildFile {
35
+ /** Project-relative path of the entry. */
36
+ readonly id: string;
37
+ readonly kind: "tsx";
38
+ readonly ok: boolean;
39
+ readonly template?: TemplateBuild | undefined;
40
+ readonly diagnostics: readonly EmailDiagnostic[];
42
41
  }
43
- interface BuildTemplatesOptions {
44
- /** Project root containing shared imports and theme.css; defaults to the parent of dir. */
45
- readonly root?: string | undefined;
46
- readonly theme?: string | false | undefined;
47
- /** Required for imported email assets. Must be their actual HTTPS serving base. */
48
- readonly assetBase?: string | undefined;
42
+ interface BuildTemplatesResult {
43
+ readonly compilerVersion: string;
44
+ /** Template entries, sorted by path; helper modules have no entry here. */
45
+ readonly files: readonly BuildFile[];
46
+ /** Findings about the project as a whole: the brand, a duplicate id. */
47
+ readonly diagnostics: readonly EmailDiagnostic[];
48
+ readonly ok: boolean;
49
49
  }
50
+ interface BuildTemplatesOptions extends ProjectOptions {}
50
51
  interface TemplateBuildSession {
51
52
  readonly build: () => Promise<BuildTemplatesResult>;
52
53
  readonly subscribe: (listener: (path: string) => void) => () => void;
53
54
  readonly close: () => Promise<void>;
54
55
  }
55
- /** Execute each email definition's fixtures; compile structured SMS/WhatsApp through their channel compiler. */
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>;
56
+ /**
57
+ * Compile every template in a project and render every fixture it declares
58
+ * through each of its channels, then check the email fixtures against the
59
+ * client matrix. Nothing is evaluated.
60
+ */
61
+ declare const buildTemplates: (options?: BuildTemplatesOptions) => Promise<BuildTemplatesResult>;
62
+ /** A watcher and compiler over a project, for `check --watch`. */
63
+ declare const createTemplateBuildSession: (options?: BuildTemplatesOptions) => TemplateBuildSession;
59
64
  //#endregion
60
- export { BuildTemplatesOptions, BuildTemplatesResult, EmailBuildPreview, EmailFixtureBuild, TemplateBuildPreview, TemplateBuildSession, buildTemplates, createTemplateBuildSession };
65
+ export { BuildFile, BuildTemplatesOptions, BuildTemplatesResult, COMPILER, FixtureBuild, TemplateBuild, TemplateBuildSession, buildTemplates, createTemplateBuildSession };
61
66
  //# sourceMappingURL=build.d.ts.map