@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.
- package/AGENTS.md +12 -1
- package/dist/build.js +8 -1
- package/dist/editor/assets/index-BZ-5-1V0.js +31 -0
- package/dist/editor/index.html +1 -1
- package/dist/emit.js +4 -4
- package/dist/index.js +95 -41
- package/package.json +3 -3
- package/dist/editor/assets/index-Cot9JDdw.js +0 -30
package/dist/editor/index.html
CHANGED
|
@@ -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-
|
|
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.
|
|
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.
|
|
29
|
+
diagnostics: probe.error.diagnostics
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
|
-
const compiled = probe.
|
|
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
|
-
/**
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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: `
|
|
403
|
+
error: `A ${entry.content.channel} template renders no fixture.`
|
|
390
404
|
};
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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 (!
|
|
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 =
|
|
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
|
|
758
|
-
if (
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
+
"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.
|
|
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": {
|