@samva/vite 0.7.0 → 0.8.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
@@ -56,6 +56,10 @@ version your previews, checks, and builds use.
56
56
  entry's canonical `.tsx` source after checking that the file on disk is still the revision it
57
57
  read, so a change you saved elsewhere is not overwritten; reload the entry and retry. SMS and
58
58
  WhatsApp entries are read-only in the editor.
59
+ - **Committing or hand-editing `.samva/brands`.** `samva:brand` resolves live first and falls
60
+ back to the snapshot `samva brands pull` writes there, warning when it is older than the live
61
+ brand. The API owns the brand, so the directory is gitignored and refreshed with `pull`; a
62
+ project with neither credentials nor a snapshot fails with `brand-unavailable`.
59
63
 
60
64
  ## Rules
61
65
 
@@ -84,4 +88,5 @@ version your previews, checks, and builds use.
84
88
  - `vite` with `samvaEditor()` configured serves the editor at the configured `route`; every entry
85
89
  should appear with its fixtures.
86
90
  - `exportEmailProject({ dir, out })` writes browsable HTML and text for every fixture and a
87
- `catalog.json` with the asset mapping and findings, without Samva credentials.
91
+ `catalog.json` with the asset mapping and findings, without Samva credentials unless the project
92
+ imports `samva:brand` and has no snapshot.
package/README.md CHANGED
@@ -102,7 +102,9 @@ client-specific proof.
102
102
 
103
103
  ## Theme, check, and publish
104
104
 
105
- A root `theme.css` uses Tailwind v4 `@theme` syntax. Theme changes recompile
105
+ 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
106
108
  local previews while preserving the last valid theme when an edit is temporarily
107
109
  invalid.
108
110
 
@@ -118,7 +120,27 @@ per rebuild and `--json` is one-shot. A successful fixture check does not prove
118
120
  input branch.
119
121
 
120
122
  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.
123
+ and builds, naming their actual HTTPS serving base.
124
+
125
+ ## The organization brand
126
+
127
+ 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:
130
+
131
+ 1. **Live.** With `SAMVA_API_KEY` in the environment or the project's `.env` (and `SAMVA_API_URL`
132
+ for another API), the brand is fetched directly. Without one, the `samva` CLI on `PATH` is asked
133
+ (`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.
137
+ 3. **Neither.** The build fails with `brand-unavailable`, naming both fixes.
138
+
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.
122
144
 
123
145
  For a hosted project, commit and push the checked TSX tree with ordinary Git, then publish one exact
124
146
  commit:
@@ -145,9 +167,13 @@ publication by default; later source edits do not change that publication.
145
167
  ```ts
146
168
  import { exportEmailProject } from "@samva/vite/project";
147
169
 
148
- const receipt = await exportEmailProject({ dir: ".", out: "dist/email" });
170
+ const receipt = await exportEmailProject({ dir: "emails", root: ".", out: "dist/email" });
149
171
  ```
150
172
 
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.
176
+
151
177
  Export writes `templates/<id>/<fixture>.html` and `.txt`, content-addressed files under `assets/`,
152
178
  and `catalog.json` with the asset mapping and findings. The default `../../assets` base resolves
153
179
  from each exported page. For delivery, supply the actual HTTPS `assetBase` and serve those bytes
package/dist/build.js CHANGED
@@ -1,7 +1,7 @@
1
- import { n as resolveThemePath, r as discoverTemplateFiles, t as readProjectThemeCss } from "./theme-CBF6Czjm.js";
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";
2
3
  import { COMPILER_VERSION, emitModulePreview } from "./emit.js";
3
4
  import { createEmailRenderer } from "./email.js";
4
- import { isTemplateDefinition, readEmailProject, renderEmailFixtures } from "./project.js";
5
5
  import { dirname, parse, relative, resolve, sep } from "node:path";
6
6
  //#region src/build.ts
7
7
  /** Execute each email definition's fixtures; compile structured SMS/WhatsApp through their channel compiler. */
@@ -38,17 +38,30 @@ const createTemplateBuildSession = async (dir, options = {}) => {
38
38
  let closed = false;
39
39
  let emailRenderer;
40
40
  let previousTheme;
41
+ let previousBrand;
42
+ const brands = createBrandResolver({
43
+ root,
44
+ env: await loadBrandEnv(root)
45
+ });
41
46
  const build = async () => {
42
47
  server.moduleGraph.invalidateAll();
43
48
  const themeCss = await readProjectThemeCss(themePath);
44
- if (emailRenderer !== void 0 && themeCss !== previousTheme) {
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)) {
45
59
  await emailRenderer.close();
46
60
  emailRenderer = void 0;
47
61
  }
48
62
  previousTheme = themeCss;
63
+ previousBrand = brand?.digest;
49
64
  emailRenderer?.invalidate();
50
- const files = await discoverTemplateFiles(templatesDir);
51
- const sourceFiles = await readEmailProject(root);
52
65
  const entries = [];
53
66
  const emailIds = /* @__PURE__ */ new Map();
54
67
  for (const file of files) {
@@ -84,7 +97,11 @@ const createTemplateBuildSession = async (dir, options = {}) => {
84
97
  const preview = await (async () => {
85
98
  emailRenderer ??= await createEmailRenderer({
86
99
  assetBase: options.assetBase ?? "",
87
- ...themeCss === void 0 ? {} : { tailwind: { css: themeCss } }
100
+ ...themeCss === void 0 || themeFile === void 0 ? {} : { tailwind: {
101
+ css: themeCss,
102
+ cssPath: themeFile
103
+ } },
104
+ ...brand === void 0 ? {} : { brand }
88
105
  });
89
106
  const loaded = await emailRenderer.load(sourceFiles, entryPath, entryPath);
90
107
  const template = loaded.template;
@@ -106,7 +123,7 @@ const createTemplateBuildSession = async (dir, options = {}) => {
106
123
  },
107
124
  diagnostics: fixture.diagnostics
108
125
  }));
109
- const diagnostics = [...loaded.compiled.diagnostics];
126
+ const diagnostics = [...brandFindings, ...loaded.compiled.diagnostics];
110
127
  if (fixtures.length === 0) diagnostics.push(emailFinding("missing-fixtures", `${template.id} must declare at least one fixture for check and build.`));
111
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."));
112
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."));
@@ -126,7 +143,13 @@ const createTemplateBuildSession = async (dir, options = {}) => {
126
143
  fixtures
127
144
  }]
128
145
  };
129
- })().catch(emailFailure);
146
+ })().catch((cause) => {
147
+ const failure = emailFailure(cause);
148
+ return {
149
+ ...failure,
150
+ diagnostics: [...brandFindings, ...failure.diagnostics]
151
+ };
152
+ });
130
153
  entries.push({
131
154
  id: file.id,
132
155
  kind: file.kind,
@@ -1,5 +1,5 @@
1
- import { readFile, readdir } from "node:fs/promises";
2
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
1
+ import { readdir } from "node:fs/promises";
2
+ import { join, relative, sep } from "node:path";
3
3
  //#region src/discovery.ts
4
4
  const isTemplateFile = (name) => name.endsWith(".tsx");
5
5
  const toPosix = (value) => sep === "/" ? value : value.split(sep).join("/");
@@ -29,22 +29,7 @@ const discoverTemplateFiles = async (dir) => {
29
29
  await walk(dir);
30
30
  return found.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
31
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
32
  //#endregion
48
- export { resolveThemePath as n, discoverTemplateFiles as r, readProjectThemeCss as t };
33
+ export { discoverTemplateFiles as t };
49
34
 
50
- //# sourceMappingURL=theme-CBF6Czjm.js.map
35
+ //# sourceMappingURL=discovery-D2eR-rzi.js.map