@samva/vite 0.3.0 → 0.5.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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Samva editor</title>
7
- <script type="module" crossorigin src="./assets/index-Cot9JDdw.js"></script>
7
+ <script type="module" crossorigin src="./assets/index-BZ-5-1V0.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="./assets/index-Cg4x6ewh.css">
9
9
  </head>
10
10
  <body>
package/dist/emit.js CHANGED
@@ -1,6 +1,6 @@
1
- import { Result } from "effect";
2
1
  import { COMPILER_VERSION, compileSource } from "@samva/markup/compile";
3
2
  import { emitModule } from "@samva/markup/emit";
3
+ import { Result } from "better-result";
4
4
  //#region src/emit.ts
5
5
  const countRepairable = (diagnostics) => diagnostics.filter((diagnostic) => diagnostic.repair !== void 0).length;
6
6
  const uniquePlaceholders = (source) => source === void 0 ? [] : [...new Set([...source.matchAll(/{{\s*([A-Za-z_][\w.-]*)\s*}}/g)].map((match) => match[1] ?? ""))].filter(Boolean).sort();
@@ -20,16 +20,16 @@ const validationSummary = (source, emitDiagnostics, compiled) => {
20
20
  /** Compile one source into its channel-polymorphic preview. */
21
21
  const compiledPreview = (source) => {
22
22
  const probe = compileSource(source);
23
- if (Result.isFailure(probe)) return {
23
+ if (Result.isError(probe)) return {
24
24
  channel: "unknown",
25
25
  compileOk: false,
26
26
  compiled: {
27
27
  channel: "unknown",
28
28
  ok: false,
29
- diagnostics: probe.failure.diagnostics
29
+ diagnostics: probe.error.diagnostics
30
30
  }
31
31
  };
32
- const compiled = probe.success;
32
+ const compiled = probe.value;
33
33
  if (compiled.channel === "sms") return {
34
34
  channel: "sms",
35
35
  compileOk: true,
package/dist/index.js CHANGED
@@ -7,7 +7,6 @@ import { open, readFile, realpath, rename, rm, stat } from "node:fs/promises";
7
7
  import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { checkEmailCompatibility, emailSelections } from "@samva/markup/compiler";
10
- import { Predicate } from "effect";
11
10
  import { createServerModuleRunner, loadEnv } from "vite";
12
11
  import { createHash, randomUUID } from "node:crypto";
13
12
  const hashSource = (source) => createHash("sha256").update(source, "utf8").digest("hex").slice(0, 16);
@@ -248,12 +247,39 @@ var EditorFileStore = class {
248
247
  async catalog() {
249
248
  return this.ensureLoaded();
250
249
  }
251
- /** Re-evaluate discovery and every TSX entry after template/dependency/theme HMR. */
250
+ /**
251
+ * Re-evaluate discovery and every TSX entry after template/dependency/theme HMR.
252
+ * `changed` names the file whose change asked for this refresh.
253
+ *
254
+ * This store writes template files itself, so an editor save comes back as a
255
+ * change event indistinguishable from an author's own edit in another program.
256
+ * Re-running the project for it costs a second full evaluation — the price of
257
+ * one save is the whole catalog, twice — and republishes the document with no
258
+ * client attached, which is exactly how a foreign edit reaches the editor. A
259
+ * changed file that still holds the bytes its current entry was built from is
260
+ * already evaluated, so there is nothing to publish and nothing to rebuild.
261
+ * Every other path, and every file whose bytes have moved, is a real change.
262
+ */
252
263
  async refreshAll(options = {}) {
253
264
  const loading = this.initialLoad;
254
265
  if (loading !== void 0) await loading;
255
266
  else if (!this.loaded) return this.ensureLoaded();
256
- return this.serialize(() => this.rebuild(options.documents ?? true));
267
+ return this.serialize(async () => await this.alreadyBuilt(options.changed) ? this.catalogSnapshot() : this.rebuild(options.documents ?? true));
268
+ }
269
+ /**
270
+ * True when `path` is a discovered entry whose file still holds the exact bytes
271
+ * that entry was evaluated from. Read inside the mutation queue, so a save's own
272
+ * rebuild has already landed by the time its change event asks.
273
+ */
274
+ async alreadyBuilt(path) {
275
+ if (path === void 0) return false;
276
+ const id = this.idFor(path);
277
+ if (id === void 0) return false;
278
+ const entry = this.entries.get(id);
279
+ if (entry === void 0 || entry.template.stale || entry.authoredSource === void 0) return false;
280
+ const resolved = await this.resolvePath(id);
281
+ if (resolved === void 0) return false;
282
+ return await readFile(resolved, "utf8").catch(() => void 0) === entry.authoredSource;
257
283
  }
258
284
  async list() {
259
285
  return (await this.catalog()).templates;
@@ -355,43 +381,51 @@ var EditorFileStore = class {
355
381
  * Show another declared fixture of an email document. It re-renders the same
356
382
  * build, so the revision does not move; the resulting document reaches every
357
383
  * watcher on the change stream, attributed to the client that asked.
384
+ *
385
+ * It runs in the mutation queue for the same reason a save does: the viewed
386
+ * fixture is part of the entry, and a rebuild that straddled the switch would
387
+ * take its own snapshot from before it and its evaluation from after it, then
388
+ * publish the difference with no client — the reader's own fixture switch,
389
+ * returned to them as somebody else's edit.
358
390
  */
359
391
  async viewFixture(id, fixture, client) {
360
392
  if (!this.loaded) await this.ensureLoaded();
361
- const entry = this.entries.get(id);
362
- if (entry === void 0) return {
363
- ok: false,
364
- kind: "missing",
365
- error: "template not found"
366
- };
367
- if (entry.content.channel !== "email") return {
368
- ok: false,
369
- kind: "refused",
370
- error: `A ${entry.content.channel} template renders no fixture.`
371
- };
372
- if (!entry.content.fixtures.includes(fixture)) return {
373
- ok: false,
374
- kind: "refused",
375
- error: `Template declares no fixture named ${JSON.stringify(fixture)}.`
376
- };
377
- const meta = this.session.get(id);
378
- this.session.set(id, {
379
- ...meta,
380
- metadata: meta?.metadata ?? {},
381
- fixture
382
- });
383
- const loaded = await this.load(id, entry);
384
- if (loaded.entry === void 0) {
385
- if (loaded.diagnostics.length > 0) this.diagnostics.set(id, loaded.diagnostics);
386
- return {
393
+ return this.serialize(async () => {
394
+ const entry = this.entries.get(id);
395
+ if (entry === void 0) return {
396
+ ok: false,
397
+ kind: "missing",
398
+ error: "template not found"
399
+ };
400
+ if (entry.content.channel !== "email") return {
387
401
  ok: false,
388
402
  kind: "refused",
389
- error: `Template ${id} could not be re-rendered.`
403
+ error: `A ${entry.content.channel} template renders no fixture.`
390
404
  };
391
- }
392
- this.entries.set(id, loaded.entry);
393
- this.publishEntry(id, loaded.entry, client);
394
- return { ok: true };
405
+ if (!entry.content.fixtures.includes(fixture)) return {
406
+ ok: false,
407
+ kind: "refused",
408
+ error: `Template declares no fixture named ${JSON.stringify(fixture)}.`
409
+ };
410
+ const meta = this.session.get(id);
411
+ this.session.set(id, {
412
+ ...meta,
413
+ metadata: meta?.metadata ?? {},
414
+ fixture
415
+ });
416
+ const loaded = await this.load(id, entry);
417
+ if (loaded.entry === void 0) {
418
+ if (loaded.diagnostics.length > 0) this.diagnostics.set(id, loaded.diagnostics);
419
+ return {
420
+ ok: false,
421
+ kind: "refused",
422
+ error: `Template ${id} could not be re-rendered.`
423
+ };
424
+ }
425
+ this.entries.set(id, loaded.entry);
426
+ this.publishEntry(id, loaded.entry, client);
427
+ return { ok: true };
428
+ });
395
429
  }
396
430
  publishEntry(id, entry, client) {
397
431
  const change = {
@@ -413,6 +447,10 @@ var EditorFileStore = class {
413
447
  }
414
448
  };
415
449
  //#endregion
450
+ //#region src/internal/guards.ts
451
+ /** A non-null, non-array object, narrowed for property access. */
452
+ const isObject = (input) => typeof input === "object" && input !== null && !Array.isArray(input);
453
+ //#endregion
416
454
  //#region src/editor.ts
417
455
  const DEFAULT_ROUTE = "/";
418
456
  const MODULE_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -536,11 +574,11 @@ const LOCAL_RENDER = { relativeUrls: true };
536
574
  */
537
575
  const variablesOf = (definition) => {
538
576
  const properties = definition.schema.schema["properties"];
539
- if (!Predicate.isObject(properties)) return [];
577
+ if (!isObject(properties)) return [];
540
578
  const required = definition.schema.schema["required"];
541
579
  const names = Array.isArray(required) ? required : [];
542
580
  return Object.entries(properties).map(([name, property]) => {
543
- const examples = Predicate.isObject(property) ? property["examples"] : void 0;
581
+ const examples = isObject(property) ? property["examples"] : void 0;
544
582
  const example = Array.isArray(examples) ? examples[0] : void 0;
545
583
  return {
546
584
  name,
@@ -740,6 +778,9 @@ const samvaEditor = (options = {}) => {
740
778
  const refreshTheme = () => {
741
779
  queueStoreRefresh(server, () => store.refreshAll({ documents: true }));
742
780
  };
781
+ let burst = /* @__PURE__ */ new Set();
782
+ let burstShape = false;
783
+ let burstQueued = false;
743
784
  const refreshTsxDependents = (path, shapeChange) => {
744
785
  const ssr = server.environments.ssr;
745
786
  if (ssr === void 0) return;
@@ -747,18 +788,31 @@ const samvaEditor = (options = {}) => {
747
788
  const directTemplate = path.endsWith(".tsx") && store.idFor(path) !== void 0;
748
789
  const potentialModuleShapeChange = shapeChange && changedModules.size === 0 && MODULE_EXTENSIONS.has(extname(path));
749
790
  if (!directTemplate && changedModules.size === 0 && !potentialModuleShapeChange) return;
791
+ burst.add(path);
792
+ burstShape ||= shapeChange;
793
+ if (burstQueued) return;
794
+ burstQueued = true;
750
795
  queueStoreRefresh(server, async () => {
796
+ const paths = burst;
797
+ const shape = burstShape;
798
+ burst = /* @__PURE__ */ new Set();
799
+ burstShape = false;
800
+ burstQueued = false;
751
801
  const invalidated = /* @__PURE__ */ new Set();
752
802
  const collectImporters = (module) => {
753
803
  if (invalidated.has(module)) return;
754
804
  invalidated.add(module);
755
805
  for (const importer of module.importers) collectImporters(importer);
756
806
  };
757
- for (const module of changedModules) collectImporters(module);
758
- if (shapeChange) ssr.moduleGraph.invalidateAll();
807
+ for (const changed of paths) for (const module of ssr.moduleGraph.getModulesByFile(changed) ?? []) collectImporters(module);
808
+ if (shape) ssr.moduleGraph.invalidateAll();
759
809
  else for (const module of invalidated) ssr.moduleGraph.invalidateModule(module);
760
810
  moduleRunner.evaluatedModules.clear();
761
- return store.refreshAll({ documents: true });
811
+ const only = paths.size === 1 ? [...paths][0] : void 0;
812
+ return store.refreshAll({
813
+ documents: true,
814
+ changed: only
815
+ });
762
816
  });
763
817
  };
764
818
  const onChange = (path) => {
@@ -850,7 +904,7 @@ const samvaEditor = (options = {}) => {
850
904
  if (error instanceof EditorBodyTooLarge) return sendJson(res, 413, { error: error.message });
851
905
  return sendJson(res, 400, { error: "malformed JSON body" });
852
906
  }
853
- if (!Predicate.isObject(body) || body["kind"] !== "authoredSource" || typeof body["baseRev"] !== "string" || typeof body["authoredSource"] !== "string") return sendJson(res, 400, { error: "Expected a revision-bound authored source update." });
907
+ if (!isObject(body) || body["kind"] !== "authoredSource" || typeof body["baseRev"] !== "string" || typeof body["authoredSource"] !== "string") return sendJson(res, 400, { error: "Expected a revision-bound authored source update." });
854
908
  if (Buffer.byteLength(body["authoredSource"], "utf8") > 1048576) return sendJson(res, 413, { error: "Authored source exceeds the 1 MiB limit." });
855
909
  const outcome = await store.saveSource(id, body["baseRev"], body["authoredSource"], query.get("client") ?? void 0);
856
910
  if (outcome.ok) return sendJson(res, 200, { rev: outcome.rev });
@@ -872,7 +926,7 @@ const samvaEditor = (options = {}) => {
872
926
  } catch {
873
927
  return sendJson(res, 400, { error: "malformed JSON body" });
874
928
  }
875
- if (!Predicate.isObject(body) || typeof body["fixture"] !== "string") return sendJson(res, 400, { error: "malformed JSON body" });
929
+ if (!isObject(body) || typeof body["fixture"] !== "string") return sendJson(res, 400, { error: "malformed JSON body" });
876
930
  const outcome = await store.viewFixture(id, body["fixture"], query.get("client") ?? void 0);
877
931
  if (outcome.ok) return sendJson(res, 200, {});
878
932
  return sendJson(res, outcome.kind === "missing" ? 404 : 422, { error: outcome.error });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samva/vite",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Unified Vite editor for Samva email, SMS, and WhatsApp templates",
5
5
  "keywords": [
6
6
  "email",
@@ -70,10 +70,10 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@babel/parser": "^8.0.4",
73
- "@samva/markup": "^0.3.0",
73
+ "@samva/markup": "^0.5.0",
74
+ "better-result": "3.0.1",
74
75
  "caniemail": "2.0.2",
75
76
  "css-tree": "3.2.1",
76
- "effect": "4.0.0-rc.112",
77
77
  "tailwindcss": "^4.3.3"
78
78
  },
79
79
  "devDependencies": {