@samva/vite 0.1.1 → 0.2.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,8 +4,8 @@
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-C1g6_O2Y.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/index-j-Fc0EML.css">
7
+ <script type="module" crossorigin src="./assets/index-Cot9JDdw.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-Cg4x6ewh.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -0,0 +1,58 @@
1
+ import { CompiledEmailProject, TransformOptions, compileEmailProject } from "@samva/markup/compiler";
2
+ import { TemplateDefinition } from "@samva/markup/template";
3
+ import { SerializeEmailOptions } from "@samva/markup/email/html";
4
+ import { renderEmail } from "@samva/markup/email/render";
5
+ //#region src/email.d.ts
6
+ interface EmailProjectSource {
7
+ /** Project-relative path to text content, or to bytes for an imported file. */
8
+ readonly [path: string]: string | Uint8Array;
9
+ }
10
+ interface RenderedEmailProject extends ReturnType<typeof renderEmail> {
11
+ readonly compiled: CompiledEmailProject;
12
+ }
13
+ /**
14
+ * One compiled entry, evaluated. `template` is `undefined` when the entry has no
15
+ * default export, which is how a helper module is told apart from a template
16
+ * without inspecting authored source.
17
+ */
18
+ interface LoadedEmailEntry {
19
+ readonly template: TemplateDefinition<unknown> | undefined;
20
+ readonly renderEmail: typeof renderEmail;
21
+ readonly compiled: CompiledEmailProject;
22
+ }
23
+ /** A long-lived evaluator. Every render shares its module graph, as a dev server does. */
24
+ interface EmailRenderer {
25
+ /**
26
+ * Compile one entry and evaluate the compiler's generated wrapper. Callers
27
+ * that render several inputs against one template load once and render many
28
+ * times; nothing here renders on its own.
29
+ *
30
+ * `namespace` names the id space the entry's modules occupy. Reusing one
31
+ * keeps a watch session's ids stable across reloads — so `invalidate` is what
32
+ * makes changed source take effect — while a fresh one isolates an unrelated
33
+ * project. Omitting it takes a new namespace each time.
34
+ */
35
+ readonly load: (source: EmailProjectSource, entryPoint: string, namespace?: string) => Promise<LoadedEmailEntry>;
36
+ /**
37
+ * Compile, evaluate and render one input in a single call. `serialize` reaches
38
+ * the serializer, so a caller that renders for an editor asks for
39
+ * `instancePaths` here; a render that will be delivered passes nothing.
40
+ */
41
+ readonly render: (source: EmailProjectSource, entryPoint: string, input: unknown, serialize?: SerializeEmailOptions) => Promise<RenderedEmailProject>;
42
+ /** Drop every evaluated module so the next load re-reads changed source. */
43
+ readonly invalidate: () => void;
44
+ readonly close: () => Promise<void>;
45
+ }
46
+ /**
47
+ * One Vite server, many renders. Each project gets its own id namespace so their
48
+ * modules coexist in one graph — which also means they share one instance of
49
+ * `@samva/markup`, exactly as a published consumer's install does. That shared
50
+ * instance holds the stylesheet registry, which is why the compiler's generated
51
+ * reset module runs first in every project's graph.
52
+ */
53
+ declare const createEmailRenderer: (options: TransformOptions, ssrExternal?: readonly string[]) => Promise<EmailRenderer>;
54
+ /** Compile and render one project, then release the evaluator. */
55
+ declare const renderEmailProject: (source: EmailProjectSource, entryPoint: string, options: TransformOptions, input: unknown) => Promise<RenderedEmailProject>;
56
+ //#endregion
57
+ export { type CompiledEmailProject, EmailProjectSource, EmailRenderer, LoadedEmailEntry, RenderedEmailProject, compileEmailProject, createEmailRenderer, renderEmailProject };
58
+ //# sourceMappingURL=email.d.ts.map
package/dist/email.js ADDED
@@ -0,0 +1,116 @@
1
+ import { dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { compileEmailProject } from "@samva/markup/compiler";
4
+ //#region src/email.ts
5
+ const VIRTUAL = "/@samva-email/";
6
+ /** This package's own directory: `src` in the repository, `dist` when published. */
7
+ const RESOLUTION_ROOT = dirname(fileURLToPath(import.meta.url));
8
+ const SCRIPT = /\.[cm]?[jt]sx?$/;
9
+ const qualify = (path) => SCRIPT.test(path) ? path : `${path}.js`;
10
+ const unqualify = (id) => id.endsWith(".js") && !SCRIPT.test(id.slice(0, -3)) ? id.slice(0, -3) : id;
11
+ const resolveWithin = (graph, id, importer) => {
12
+ const own = id.startsWith(graph.prefix) ? unqualify(id.slice(graph.prefix.length)) : id;
13
+ if (graph.files[own] !== void 0) return `${graph.prefix}${qualify(own)}`;
14
+ if (!own.startsWith(".")) return void 0;
15
+ const segments = [...(importer?.startsWith(graph.prefix) === true ? unqualify(importer.slice(graph.prefix.length)) : "").split("/").slice(0, -1), ...own.split("/")];
16
+ const parts = [];
17
+ for (const segment of segments) {
18
+ if (segment === "." || segment === "") continue;
19
+ if (segment === "..") parts.pop();
20
+ else parts.push(segment);
21
+ }
22
+ const path = parts.join("/");
23
+ for (const candidate of [
24
+ path,
25
+ `${path}.ts`,
26
+ `${path}.tsx`,
27
+ `${path}.js`,
28
+ `${path}/index.ts`
29
+ ]) if (graph.files[candidate] !== void 0) return `${graph.prefix}${qualify(candidate)}`;
30
+ };
31
+ /**
32
+ * One Vite server, many renders. Each project gets its own id namespace so their
33
+ * modules coexist in one graph — which also means they share one instance of
34
+ * `@samva/markup`, exactly as a published consumer's install does. That shared
35
+ * instance holds the stylesheet registry, which is why the compiler's generated
36
+ * reset module runs first in every project's graph.
37
+ */
38
+ const createEmailRenderer = async (options, ssrExternal = []) => {
39
+ const vite = await import("vite").catch(() => void 0);
40
+ if (vite === void 0) throw new Error("`vite` is required to render an email project locally.");
41
+ const graphs = /* @__PURE__ */ new Map();
42
+ let anonymous = 0;
43
+ const graphFor = (id) => id === void 0 ? void 0 : [...graphs.values()].find((graph) => id.startsWith(graph.prefix));
44
+ const server = await vite.createServer({
45
+ root: RESOLUTION_ROOT,
46
+ configFile: false,
47
+ logLevel: "silent",
48
+ server: {
49
+ middlewareMode: true,
50
+ hmr: false
51
+ },
52
+ optimizeDeps: {
53
+ noDiscovery: true,
54
+ include: []
55
+ },
56
+ oxc: { jsx: {
57
+ runtime: "automatic",
58
+ importSource: "@samva/markup/email"
59
+ } },
60
+ ...ssrExternal.length === 0 ? {} : { ssr: { external: [...ssrExternal] } },
61
+ plugins: [{
62
+ name: "samva:email-project",
63
+ enforce: "pre",
64
+ resolveId: (id, importer) => {
65
+ const graph = graphFor(id) ?? graphFor(importer);
66
+ return graph === void 0 ? void 0 : resolveWithin(graph, id, importer);
67
+ },
68
+ load: (id) => {
69
+ const graph = graphFor(id);
70
+ return graph === void 0 ? void 0 : graph.files[unqualify(id.slice(graph.prefix.length))];
71
+ }
72
+ }]
73
+ });
74
+ const load = async (source, entryPoint, namespace) => {
75
+ const key = namespace ?? `#${anonymous += 1}`;
76
+ const graph = graphs.get(key) ?? {
77
+ files: {},
78
+ prefix: `${VIRTUAL}${graphs.size}/`
79
+ };
80
+ graphs.set(key, graph);
81
+ const compiled = await compileEmailProject(source, entryPoint, options);
82
+ graph.files = compiled.files;
83
+ const module = await server.ssrLoadModule(`${graph.prefix}${qualify(compiled.entryPoint)}`);
84
+ return {
85
+ template: module.template,
86
+ renderEmail: module.renderEmail,
87
+ compiled
88
+ };
89
+ };
90
+ return {
91
+ load,
92
+ render: async (source, entryPoint, input, serialize) => {
93
+ const entry = await load(source, entryPoint);
94
+ if (entry.template === void 0) throw new TypeError(`${entryPoint} does not default-export a template definition.`);
95
+ return {
96
+ ...entry.renderEmail(entry.template, input, serialize),
97
+ compiled: entry.compiled
98
+ };
99
+ },
100
+ invalidate: () => server.moduleGraph.invalidateAll(),
101
+ close: () => server.close()
102
+ };
103
+ };
104
+ /** Compile and render one project, then release the evaluator. */
105
+ const renderEmailProject = async (source, entryPoint, options, input) => {
106
+ const renderer = await createEmailRenderer(options);
107
+ try {
108
+ return await renderer.render(source, entryPoint, input);
109
+ } finally {
110
+ await renderer.close();
111
+ }
112
+ };
113
+ //#endregion
114
+ export { compileEmailProject, createEmailRenderer, renderEmailProject };
115
+
116
+ //# sourceMappingURL=email.js.map
package/dist/emit.d.ts CHANGED
@@ -1,19 +1,9 @@
1
- import { COMPILER_VERSION, CompileBase, CompileOptions } from "@samva/markup/compile";
1
+ import { COMPILER_VERSION } from "@samva/markup/compile";
2
2
  import { Diagnostic } from "@samva/markup/diagnostics";
3
3
  import { TemplateVariable } from "@samva/markup/render-template";
4
4
  //#region src/emit.d.ts
5
- type PreviewChannel = "email" | "sms" | "whatsapp";
6
- /** Both email compile bases the preview surfaces and a build materializes. */
7
- declare const COMPILE_BASES: readonly ["outlook-safe", "modern"];
8
- /** One email base's compiled artifact, or its compile diagnostics if it failed. */
9
- interface EmailBaseArtifact {
10
- readonly base: CompileBase;
11
- readonly ok: boolean;
12
- readonly html?: string | undefined;
13
- readonly text?: string | undefined;
14
- readonly warnings?: readonly Diagnostic[] | undefined;
15
- readonly diagnostics?: readonly Diagnostic[] | undefined;
16
- }
5
+ /** Structured channel preview emission, independent of the email execution runtime. */
6
+ type PreviewChannel = "unknown" | "sms" | "whatsapp";
17
7
  type WhatsAppButtonPreview = {
18
8
  readonly type: "QUICK_REPLY";
19
9
  readonly text: string;
@@ -36,19 +26,13 @@ interface AuthoringValidationSummary {
36
26
  readonly repairableDiagnostics: number;
37
27
  readonly placeholders: readonly string[];
38
28
  readonly darkClassCount: number;
39
- readonly emailBases?: readonly {
40
- readonly base: CompileBase;
41
- readonly ok: boolean;
42
- readonly htmlBytes?: number | undefined;
43
- readonly warnings: number;
44
- readonly diagnostics: number;
45
- readonly repairableDiagnostics: number;
46
- }[] | undefined;
47
29
  }
48
30
  /** The compiled payload for one template, discriminated by channel. */
49
31
  type CompiledPreview = {
50
- readonly channel: "email";
51
- readonly bases: readonly EmailBaseArtifact[];
32
+ readonly channel: "unknown";
33
+ readonly ok: false;
34
+ readonly diagnostics: readonly Diagnostic[];
35
+ readonly warnings?: readonly Diagnostic[];
52
36
  } | {
53
37
  readonly channel: "sms";
54
38
  readonly ok: boolean;
@@ -103,14 +87,13 @@ interface ModulePreview {
103
87
  * changes the implementation shape of the default export. Direct callers of
104
88
  * `@samva/vite/emit` must pass that file-backed name as the first argument.
105
89
  */
106
- declare const emitModulePreview: (name: string, moduleExports: Record<string, unknown>, options?: CompileOptions) => ModulePreview;
90
+ declare const emitModulePreview: (name: string, moduleExports: Record<string, unknown>) => ModulePreview;
107
91
  /**
108
- * Compiles a raw `.samva` source string directly (no TSX). Used for `.samva`
109
- * files the plugin discovers alongside `.tsx` templates and for `samva
110
- * templates pull` round-trips. Wraps the single source in the same preview
111
- * shape so the client renders `.samva` and `.tsx` templates identically.
92
+ * Compiles a raw SML source string directly (no TSX). Used by tests and any
93
+ * caller that already holds emitted SML. Wraps the single source in the same
94
+ * preview shape so the client renders compiled SML identically to TSX emission.
112
95
  */
113
- declare const compileSourcePreview: (name: string, source: string, options?: CompileOptions) => EmittedTemplatePreview;
96
+ declare const compileSourcePreview: (name: string, source: string) => EmittedTemplatePreview;
114
97
  //#endregion
115
- export { AuthoringValidationSummary, COMPILER_VERSION, COMPILE_BASES, CompiledPreview, EmailBaseArtifact, EmittedTemplatePreview, ModulePreview, PreviewChannel, WhatsAppButtonPreview, compileSourcePreview, emitModulePreview };
98
+ export { AuthoringValidationSummary, COMPILER_VERSION, CompiledPreview, EmittedTemplatePreview, ModulePreview, PreviewChannel, WhatsAppButtonPreview, compileSourcePreview, emitModulePreview };
116
99
  //# sourceMappingURL=emit.d.ts.map
package/dist/emit.js CHANGED
@@ -2,37 +2,10 @@ import { Result } from "effect";
2
2
  import { COMPILER_VERSION, compileSource } from "@samva/markup/compile";
3
3
  import { emitModule } from "@samva/markup/emit";
4
4
  //#region src/emit.ts
5
- /** Both email compile bases the preview surfaces and a build materializes. */
6
- const COMPILE_BASES = ["outlook-safe", "modern"];
7
5
  const countRepairable = (diagnostics) => diagnostics.filter((diagnostic) => diagnostic.repair !== void 0).length;
8
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();
9
7
  const darkClassCount = (source) => source === void 0 ? 0 : (source.match(/\bdark:/g) ?? []).length;
10
8
  const validationSummary = (source, emitDiagnostics, compiled) => {
11
- if (compiled.channel === "email") {
12
- const baseSummaries = compiled.bases.map((artifact) => {
13
- const warnings = artifact.warnings ?? [];
14
- const diagnostics = artifact.diagnostics ?? [];
15
- return {
16
- base: artifact.base,
17
- ok: artifact.ok,
18
- htmlBytes: artifact.html === void 0 ? void 0 : new TextEncoder().encode(artifact.html).length,
19
- warnings: warnings.length,
20
- diagnostics: diagnostics.length,
21
- repairableDiagnostics: countRepairable(diagnostics)
22
- };
23
- });
24
- const warnings = baseSummaries.reduce((sum, base) => sum + base.warnings, 0);
25
- const diagnostics = emitDiagnostics.length + baseSummaries.reduce((sum, base) => sum + base.diagnostics, 0);
26
- return {
27
- ok: baseSummaries.length > 0 && baseSummaries.every((base) => base.ok) && emitDiagnostics.length === 0,
28
- diagnostics,
29
- warnings,
30
- repairableDiagnostics: countRepairable(emitDiagnostics) + baseSummaries.reduce((sum, base) => sum + base.repairableDiagnostics, 0),
31
- placeholders: uniquePlaceholders(source),
32
- darkClassCount: darkClassCount(source),
33
- emailBases: baseSummaries
34
- };
35
- }
36
9
  const warnings = compiled.warnings ?? [];
37
10
  const diagnostics = compiled.diagnostics ?? [];
38
11
  return {
@@ -44,52 +17,16 @@ const validationSummary = (source, emitDiagnostics, compiled) => {
44
17
  darkClassCount: darkClassCount(source)
45
18
  };
46
19
  };
47
- const emailBaseArtifact = (source, base, options) => {
48
- const result = compileSource(source, {
49
- ...options,
50
- base
51
- });
52
- if (Result.isFailure(result)) return {
53
- base,
54
- ok: false,
55
- diagnostics: result.failure.diagnostics
56
- };
57
- if (result.success.channel !== "email") return {
58
- base,
59
- ok: false,
60
- diagnostics: [{
61
- code: "channel-changed",
62
- message: "template no longer compiles to email",
63
- span: {
64
- start: 0,
65
- end: 0
66
- }
67
- }]
68
- };
69
- return {
70
- base,
71
- ok: true,
72
- html: result.success.html,
73
- text: result.success.text,
74
- warnings: result.success.warnings
75
- };
76
- };
77
20
  /** Compile one source into its channel-polymorphic preview. */
78
- const compiledPreview = (source, options) => {
79
- const probe = compileSource(source, {
80
- ...options,
81
- base: "outlook-safe"
82
- });
21
+ const compiledPreview = (source) => {
22
+ const probe = compileSource(source);
83
23
  if (Result.isFailure(probe)) return {
84
- channel: "email",
24
+ channel: "unknown",
85
25
  compileOk: false,
86
26
  compiled: {
87
- channel: "email",
88
- bases: [{
89
- base: "outlook-safe",
90
- ok: false,
91
- diagnostics: probe.failure.diagnostics
92
- }]
27
+ channel: "unknown",
28
+ ok: false,
29
+ diagnostics: probe.failure.diagnostics
93
30
  }
94
31
  };
95
32
  const compiled = probe.success;
@@ -106,7 +43,7 @@ const compiledPreview = (source, options) => {
106
43
  warnings: compiled.warnings
107
44
  }
108
45
  };
109
- if (compiled.channel === "whatsapp") return {
46
+ return {
110
47
  channel: "whatsapp",
111
48
  compileOk: true,
112
49
  compiled: {
@@ -120,40 +57,27 @@ const compiledPreview = (source, options) => {
120
57
  warnings: compiled.warnings
121
58
  }
122
59
  };
123
- const bases = [{
124
- base: "outlook-safe",
125
- ok: true,
126
- html: compiled.html,
127
- text: compiled.text,
128
- warnings: compiled.warnings
129
- }, emailBaseArtifact(source, "modern", options)];
130
- return {
131
- channel: "email",
132
- compileOk: bases.every((artifact) => artifact.ok),
133
- compiled: {
134
- channel: "email",
135
- bases
136
- }
137
- };
138
60
  };
139
- const previewTemplate = (emitted, options) => {
61
+ const previewTemplate = (emitted) => {
140
62
  if (emitted.source === void 0) return {
141
63
  name: emitted.name,
142
64
  ok: false,
143
- channel: "email",
65
+ channel: "unknown",
144
66
  source: void 0,
145
67
  variables: emitted.variables,
146
68
  diagnostics: emitted.diagnostics,
147
69
  compiled: {
148
- channel: "email",
149
- bases: []
70
+ channel: "unknown",
71
+ ok: false,
72
+ diagnostics: []
150
73
  },
151
74
  validation: validationSummary(void 0, emitted.diagnostics, {
152
- channel: "email",
153
- bases: []
75
+ channel: "unknown",
76
+ ok: false,
77
+ diagnostics: []
154
78
  })
155
79
  };
156
- const { channel, compileOk, compiled } = compiledPreview(emitted.source, options);
80
+ const { channel, compileOk, compiled } = compiledPreview(emitted.source);
157
81
  return {
158
82
  name: emitted.name,
159
83
  ok: emitted.ok && compileOk,
@@ -175,35 +99,18 @@ const previewTemplate = (emitted, options) => {
175
99
  * changes the implementation shape of the default export. Direct callers of
176
100
  * `@samva/vite/emit` must pass that file-backed name as the first argument.
177
101
  */
178
- const emitModulePreview = (name, moduleExports, options) => {
179
- if (!Object.hasOwn(moduleExports, "default")) return {
180
- ok: false,
181
- compilerVersion: COMPILER_VERSION,
182
- templates: [],
183
- diagnostics: [{
184
- code: "missing-default-export",
185
- message: "TSX template files must default-export exactly one template; named exports are ignored.",
186
- span: {
187
- start: 0,
188
- end: 0
189
- }
190
- }]
191
- };
192
- const emitted = emitModule({ [name]: moduleExports.default });
102
+ const emitModulePreview = (name, moduleExports) => {
103
+ const emitted = emitModule(moduleExports);
193
104
  if (emitted.templates.length === 0) return {
194
105
  ok: false,
195
106
  compilerVersion: COMPILER_VERSION,
196
107
  templates: [],
197
- diagnostics: [{
198
- code: "invalid-default-export",
199
- message: "The default export is not a Samva template returning <Email>, <Sms>, or <WhatsApp>.",
200
- span: {
201
- start: 0,
202
- end: 0
203
- }
204
- }]
108
+ diagnostics: emitted.diagnostics
205
109
  };
206
- const templates = emitted.templates.map((template) => previewTemplate(template, options));
110
+ const templates = emitted.templates.map((template) => previewTemplate({
111
+ ...template,
112
+ name
113
+ }));
207
114
  return {
208
115
  ok: emitted.ok && templates.every((template) => template.ok),
209
116
  compilerVersion: COMPILER_VERSION,
@@ -212,13 +119,12 @@ const emitModulePreview = (name, moduleExports, options) => {
212
119
  };
213
120
  };
214
121
  /**
215
- * Compiles a raw `.samva` source string directly (no TSX). Used for `.samva`
216
- * files the plugin discovers alongside `.tsx` templates and for `samva
217
- * templates pull` round-trips. Wraps the single source in the same preview
218
- * shape so the client renders `.samva` and `.tsx` templates identically.
122
+ * Compiles a raw SML source string directly (no TSX). Used by tests and any
123
+ * caller that already holds emitted SML. Wraps the single source in the same
124
+ * preview shape so the client renders compiled SML identically to TSX emission.
219
125
  */
220
- const compileSourcePreview = (name, source, options) => {
221
- const { channel, compileOk, compiled } = compiledPreview(source, options);
126
+ const compileSourcePreview = (name, source) => {
127
+ const { channel, compileOk, compiled } = compiledPreview(source);
222
128
  return {
223
129
  name,
224
130
  ok: compileOk,
@@ -231,6 +137,6 @@ const compileSourcePreview = (name, source, options) => {
231
137
  };
232
138
  };
233
139
  //#endregion
234
- export { COMPILER_VERSION, COMPILE_BASES, compileSourcePreview, emitModulePreview };
140
+ export { COMPILER_VERSION, compileSourcePreview, emitModulePreview };
235
141
 
236
142
  //# sourceMappingURL=emit.js.map
package/dist/index.d.ts CHANGED
@@ -1,13 +1,10 @@
1
1
  import { Plugin } from "vite";
2
- import { CompileOptions } from "@samva/markup/compile";
3
2
  //#region src/editor.d.ts
4
3
  interface SamvaEditorPluginOptions {
5
- /** Directory containing `.samva` / `.tsx` templates, relative to project root. Default `emails`. */
4
+ /** Directory containing `.tsx` templates, relative to project root. Default `emails`. */
6
5
  readonly templatesDir?: string | undefined;
7
6
  /** Editor UI route. Default `/`; its API and assets are mounted as siblings below it. */
8
7
  readonly route?: string | undefined;
9
- /** Compile options applied to catalog validation and browser previews. */
10
- readonly compile?: CompileOptions | undefined;
11
8
  /** Project theme file relative to root. Default `theme.css`; false disables discovery. */
12
9
  readonly theme?: string | false | undefined;
13
10
  }