@maizzle/framework 6.0.0-rc.6 → 6.0.0-rc.7

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.
Files changed (63) hide show
  1. package/dist/components/CodeBlock.vue +12 -19
  2. package/dist/components/Markdown.vue +70 -0
  3. package/dist/plugins/postcss/tailwindCleanup.mjs +22 -13
  4. package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
  5. package/dist/render/createRenderer.d.mts +2 -3
  6. package/dist/render/createRenderer.d.mts.map +1 -1
  7. package/dist/render/createRenderer.mjs +55 -4
  8. package/dist/render/createRenderer.mjs.map +1 -1
  9. package/dist/serve.d.mts.map +1 -1
  10. package/dist/serve.mjs +83 -3
  11. package/dist/serve.mjs.map +1 -1
  12. package/dist/server/compatibility.d.mts +1 -2
  13. package/dist/server/compatibility.d.mts.map +1 -1
  14. package/dist/server/compatibility.mjs +15 -15
  15. package/dist/server/compatibility.mjs.map +1 -1
  16. package/dist/server/email.d.mts +17 -0
  17. package/dist/server/email.d.mts.map +1 -0
  18. package/dist/server/email.mjs +40 -0
  19. package/dist/server/email.mjs.map +1 -0
  20. package/dist/server/ui/App.vue +204 -68
  21. package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
  22. package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
  23. package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
  24. package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
  25. package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
  26. package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
  27. package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
  28. package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
  29. package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  30. package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
  31. package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
  32. package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
  33. package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
  34. package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
  35. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
  36. package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
  37. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
  38. package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
  39. package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
  40. package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
  41. package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
  42. package/dist/server/ui/components/ui/toggle/index.ts +3 -3
  43. package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
  44. package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
  45. package/dist/server/ui/main.css +20 -20
  46. package/dist/server/ui/pages/Home.vue +12 -5
  47. package/dist/server/ui/pages/Preview.vue +369 -150
  48. package/dist/transformers/inlineCSS.mjs +9 -0
  49. package/dist/transformers/inlineCSS.mjs.map +1 -1
  50. package/dist/transformers/purgeCSS.d.mts.map +1 -1
  51. package/dist/transformers/purgeCSS.mjs +67 -1
  52. package/dist/transformers/purgeCSS.mjs.map +1 -1
  53. package/dist/transformers/tailwindcss.mjs +3 -7
  54. package/dist/transformers/tailwindcss.mjs.map +1 -1
  55. package/dist/types/config.d.mts +38 -4
  56. package/dist/types/config.d.mts.map +1 -1
  57. package/dist/types/index.d.mts +2 -2
  58. package/package.json +7 -3
  59. package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
  60. package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
  61. package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
  62. package/dist/server/ui/components/ui/resizable/index.ts +0 -3
  63. /package/dist/components/{Preview.vue → Preheader.vue} +0 -0
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
- import { createStaticVNode } from 'vue'
3
- import { codeToHtml, getSingletonHighlighter } from 'shiki'
2
+ import { createStaticVNode, type PropType } from 'vue'
3
+ import { codeToHtml, getSingletonHighlighter, type BundledLanguage, type BundledTheme } from 'shiki'
4
4
 
5
5
  export default {
6
6
  props: {
@@ -9,33 +9,26 @@ export default {
9
9
  type: String,
10
10
  default: ''
11
11
  },
12
- /** Base64-encoded code, set by the Vite transform for slot content. */
13
- encodedCode: {
14
- type: String,
15
- default: ''
16
- },
17
- /** The language for syntax highlighting. @default 'html' */
18
- lang: {
19
- type: String,
12
+ /** The language for syntax highlighting. @default 'html' */
13
+ language: {
14
+ type: String as PropType<BundledLanguage>,
20
15
  default: 'html'
21
16
  },
22
17
  /** The shiki theme to use. @default 'github-light' */
23
18
  theme: {
24
- type: String,
19
+ type: String as PropType<BundledTheme>,
25
20
  default: 'github-light'
26
21
  },
27
- /** CSS class for the wrapping table cell. @default 'max-w-0 mso-padding-alt-6' */
22
+ /** CSS class for the wrapping table cell. @default 'max-w-0 mso-padding-alt-4' */
28
23
  tdClass: {
29
24
  type: String,
30
- default: 'max-w-0 mso-padding-alt-6'
25
+ default: 'max-w-0 mso-padding-alt-4'
31
26
  }
32
27
  },
33
28
  inheritAttrs: false,
34
29
  async setup(props, { slots, attrs }) {
35
- // Prefer encodedCode (from Vite transform) → code prop → slot text
36
- let source = props.encodedCode
37
- ? Buffer.from(props.encodedCode, 'base64').toString('utf-8')
38
- : props.code
30
+ // Prefer code prop → slot text
31
+ let source = props.code
39
32
 
40
33
  if (!source) {
41
34
  const slotContent = slots.default?.()
@@ -51,7 +44,7 @@ export default {
51
44
  }
52
45
 
53
46
  const highlighted = await codeToHtml(source, {
54
- lang: props.lang,
47
+ lang: props.language,
55
48
  theme: props.theme,
56
49
  })
57
50
 
@@ -64,7 +57,7 @@ export default {
64
57
  .replace(/<\/code><\/pre>$/, '')
65
58
 
66
59
  const classes = ['font-mono', attrs.class].filter(Boolean).join(' ')
67
- const baseStyles = `background-color:${bg};padding:24px;overflow:auto;white-space:pre;word-wrap:normal;word-break:normal;word-spacing:normal`
60
+ const baseStyles = `background-color:${bg};padding:16px;overflow:auto;white-space:pre;word-wrap:normal;word-break:normal;word-spacing:normal`
68
61
  const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
69
62
 
70
63
  const html = `<table class="w-full"><tr><td class="${props.tdClass}"><pre class="${classes}" style="${styles}"><code>${codeContent}</code></pre></td></tr></table>`
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import { createStaticVNode, type PropType } from 'vue'
3
+ import { createMarkdownExit } from 'markdown-exit'
4
+ import { codeToHtml, type BundledTheme } from 'shiki'
5
+
6
+ export default {
7
+ props: {
8
+ /** Markdown content string. Overrides slot content. */
9
+ content: {
10
+ type: String,
11
+ default: ''
12
+ },
13
+ /** Path to a markdown file to render. Resolved at build time. */
14
+ src: {
15
+ type: String,
16
+ default: ''
17
+ },
18
+ /** Shiki theme for fenced code blocks. @default 'github-dark-high-contrast' */
19
+ shikiTheme: {
20
+ type: String as PropType<BundledTheme>,
21
+ default: 'github-dark-high-contrast'
22
+ },
23
+ /** Wrap output in a div element. @default true */
24
+ wrapper: {
25
+ type: Boolean,
26
+ default: true
27
+ },
28
+ },
29
+ inheritAttrs: false,
30
+ async setup(props, { slots, attrs }) {
31
+ let source = props.content
32
+
33
+ if (!source) {
34
+ const slotContent = slots.default?.()
35
+ source = slotContent
36
+ ?.map((vnode: any) => (typeof vnode.children === 'string' ? vnode.children : ''))
37
+ .join('') ?? ''
38
+ }
39
+
40
+ source = source.trim()
41
+
42
+ if (!source) {
43
+ return () => createStaticVNode('', 0)
44
+ }
45
+
46
+ const md = createMarkdownExit({
47
+ html: true,
48
+ linkify: true,
49
+ typographer: true,
50
+ highlight: async (code, lang) => {
51
+ try {
52
+ return await codeToHtml(code, { lang, theme: props.shikiTheme })
53
+ } catch {
54
+ return ''
55
+ }
56
+ },
57
+ })
58
+
59
+ let html = await md.renderAsync(source)
60
+
61
+ if (props.wrapper) {
62
+ const classes = attrs.class ? ` class="${attrs.class}"` : ''
63
+ const style = attrs.style ? ` style="${attrs.style}"` : ''
64
+ html = `<div${classes}${style}>${html}</div>`
65
+ }
66
+
67
+ return () => createStaticVNode(html, 1)
68
+ }
69
+ }
70
+ </script>
@@ -14,20 +14,29 @@ const DEFAULT_AT_RULES = ["layer", "property"];
14
14
  function tailwindCleanup(config) {
15
15
  const selectors = config.postcss?.removeSelectors ?? DEFAULT_SELECTORS;
16
16
  const atRules = config.postcss?.removeAtRules ?? DEFAULT_AT_RULES;
17
- return [{
18
- postcssPlugin: "tailwind-cleanup-selectors",
19
- Rule(rule) {
20
- const parts = rule.selector.split(",").map((s) => s.trim());
21
- const kept = parts.filter((p) => !selectors.some((s) => p === s || p.startsWith(`${s}(`)));
22
- if (kept.length === 0) rule.remove();
23
- else if (kept.length < parts.length) rule.selector = kept.join(", ");
17
+ return [
18
+ {
19
+ postcssPlugin: "tailwind-cleanup-selectors",
20
+ Rule(rule) {
21
+ const parts = rule.selector.split(",").map((s) => s.trim());
22
+ const kept = parts.filter((p) => !selectors.some((s) => p === s || p.startsWith(`${s}(`)));
23
+ if (kept.length === 0) rule.remove();
24
+ else if (kept.length < parts.length) rule.selector = kept.join(", ");
25
+ }
26
+ },
27
+ {
28
+ postcssPlugin: "tailwind-cleanup-at-rules",
29
+ AtRule(rule) {
30
+ if (atRules.includes(rule.name)) rule.remove();
31
+ }
32
+ },
33
+ {
34
+ postcssPlugin: "tailwind-cleanup-text-decoration",
35
+ Declaration(decl) {
36
+ if (decl.prop === "text-decoration-line") decl.prop = "text-decoration";
37
+ }
24
38
  }
25
- }, {
26
- postcssPlugin: "tailwind-cleanup-at-rules",
27
- AtRule(rule) {
28
- if (atRules.includes(rule.name)) rule.remove();
29
- }
30
- }];
39
+ ];
31
40
  }
32
41
 
33
42
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"tailwindCleanup.mjs","names":[],"sources":["../../../src/plugins/postcss/tailwindCleanup.ts"],"sourcesContent":["import postcss from 'postcss'\nimport type { MaizzleConfig } from '../../types/config.ts'\n\nconst DEFAULT_SELECTORS = [':host', ':lang']\nconst DEFAULT_AT_RULES = ['layer', 'property']\n\n/**\n * Removes CSS rules whose every comma-separated selector part starts with\n * one of the configured prefixes (e.g. ':host', ':lang'). Rules with mixed\n * selectors have the unwanted parts stripped.\n *\n * Also removes entire at-rules by name (e.g. '@layer', '@property').\n *\n * Intended to clean up Tailwind's compiled output after lightningcss has\n * flattened all modern CSS syntax.\n */\nexport function tailwindCleanup(config: MaizzleConfig): postcss.Plugin[] {\n const selectors: string[] = config.postcss?.removeSelectors ?? DEFAULT_SELECTORS\n const atRules: string[] = config.postcss?.removeAtRules ?? DEFAULT_AT_RULES\n\n return [\n {\n postcssPlugin: 'tailwind-cleanup-selectors',\n Rule(rule) {\n const parts = rule.selector.split(',').map(s => s.trim())\n const kept = parts.filter(p => !selectors.some(s => p === s || p.startsWith(`${s}(`)))\n if (kept.length === 0) {\n rule.remove()\n } else if (kept.length < parts.length) {\n rule.selector = kept.join(', ')\n }\n },\n },\n {\n postcssPlugin: 'tailwind-cleanup-at-rules',\n AtRule(rule) {\n if (atRules.includes(rule.name)) {\n rule.remove()\n }\n },\n },\n ]\n}\n"],"mappings":";AAGA,MAAM,oBAAoB,CAAC,SAAS,QAAQ;AAC5C,MAAM,mBAAmB,CAAC,SAAS,WAAW;;;;;;;;;;;AAY9C,SAAgB,gBAAgB,QAAyC;CACvE,MAAM,YAAsB,OAAO,SAAS,mBAAmB;CAC/D,MAAM,UAAoB,OAAO,SAAS,iBAAiB;AAE3D,QAAO,CACL;EACE,eAAe;EACf,KAAK,MAAM;GACT,MAAM,QAAQ,KAAK,SAAS,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC;GACzD,MAAM,OAAO,MAAM,QAAO,MAAK,CAAC,UAAU,MAAK,MAAK,MAAM,KAAK,EAAE,WAAW,GAAG,EAAE,GAAG,CAAC,CAAC;AACtF,OAAI,KAAK,WAAW,EAClB,MAAK,QAAQ;YACJ,KAAK,SAAS,MAAM,OAC7B,MAAK,WAAW,KAAK,KAAK,KAAK;;EAGpC,EACD;EACE,eAAe;EACf,OAAO,MAAM;AACX,OAAI,QAAQ,SAAS,KAAK,KAAK,CAC7B,MAAK,QAAQ;;EAGlB,CACF"}
1
+ {"version":3,"file":"tailwindCleanup.mjs","names":[],"sources":["../../../src/plugins/postcss/tailwindCleanup.ts"],"sourcesContent":["import postcss from 'postcss'\nimport type { MaizzleConfig } from '../../types/config.ts'\n\nconst DEFAULT_SELECTORS = [':host', ':lang']\nconst DEFAULT_AT_RULES = ['layer', 'property']\n\n/**\n * Removes CSS rules whose every comma-separated selector part starts with\n * one of the configured prefixes (e.g. ':host', ':lang'). Rules with mixed\n * selectors have the unwanted parts stripped.\n *\n * Also removes entire at-rules by name (e.g. '@layer', '@property').\n *\n * Intended to clean up Tailwind's compiled output after lightningcss has\n * flattened all modern CSS syntax.\n */\nexport function tailwindCleanup(config: MaizzleConfig): postcss.Plugin[] {\n const selectors: string[] = config.postcss?.removeSelectors ?? DEFAULT_SELECTORS\n const atRules: string[] = config.postcss?.removeAtRules ?? DEFAULT_AT_RULES\n\n return [\n {\n postcssPlugin: 'tailwind-cleanup-selectors',\n Rule(rule) {\n const parts = rule.selector.split(',').map(s => s.trim())\n const kept = parts.filter(p => !selectors.some(s => p === s || p.startsWith(`${s}(`)))\n if (kept.length === 0) {\n rule.remove()\n } else if (kept.length < parts.length) {\n rule.selector = kept.join(', ')\n }\n },\n },\n {\n postcssPlugin: 'tailwind-cleanup-at-rules',\n AtRule(rule) {\n if (atRules.includes(rule.name)) {\n rule.remove()\n }\n },\n },\n {\n postcssPlugin: 'tailwind-cleanup-text-decoration',\n Declaration(decl) {\n if (decl.prop === 'text-decoration-line') {\n decl.prop = 'text-decoration'\n }\n },\n },\n ]\n}\n"],"mappings":";AAGA,MAAM,oBAAoB,CAAC,SAAS,QAAQ;AAC5C,MAAM,mBAAmB,CAAC,SAAS,WAAW;;;;;;;;;;;AAY9C,SAAgB,gBAAgB,QAAyC;CACvE,MAAM,YAAsB,OAAO,SAAS,mBAAmB;CAC/D,MAAM,UAAoB,OAAO,SAAS,iBAAiB;AAE3D,QAAO;EACL;GACE,eAAe;GACf,KAAK,MAAM;IACT,MAAM,QAAQ,KAAK,SAAS,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC;IACzD,MAAM,OAAO,MAAM,QAAO,MAAK,CAAC,UAAU,MAAK,MAAK,MAAM,KAAK,EAAE,WAAW,GAAG,EAAE,GAAG,CAAC,CAAC;AACtF,QAAI,KAAK,WAAW,EAClB,MAAK,QAAQ;aACJ,KAAK,SAAS,MAAM,OAC7B,MAAK,WAAW,KAAK,KAAK,KAAK;;GAGpC;EACD;GACE,eAAe;GACf,OAAO,MAAM;AACX,QAAI,QAAQ,SAAS,KAAK,KAAK,CAC7B,MAAK,QAAQ;;GAGlB;EACD;GACE,eAAe;GACf,YAAY,MAAM;AAChB,QAAI,KAAK,SAAS,uBAChB,MAAK,OAAO;;GAGjB;EACF"}
@@ -1,7 +1,6 @@
1
- import { MaizzleConfig } from "../types/config.mjs";
1
+ import { MaizzleConfig, MarkdownConfig } from "../types/config.mjs";
2
2
  import { RenderContext } from "../composables/renderContext.mjs";
3
3
  import { Component } from "vue";
4
- import { Options } from "unplugin-vue-markdown/types";
5
4
 
6
5
  //#region src/render/createRenderer.d.ts
7
6
  interface RenderedTemplate {
@@ -21,7 +20,7 @@ interface CreateRendererOptions {
21
20
  /** Generate .d.ts files for auto-imports and components (default: false) */
22
21
  dts?: boolean;
23
22
  /** Options passed to unplugin-vue-markdown */
24
- markdown?: Options;
23
+ markdown?: MarkdownConfig;
25
24
  /** Root directory for resolving user component dirs and .d.ts output */
26
25
  root?: string;
27
26
  /** Additional component directories to register for auto-import */
@@ -1 +1 @@
1
- {"version":3,"file":"createRenderer.d.mts","names":[],"sources":["../../src/render/createRenderer.ts"],"mappings":";;;;;;UA2EiB,gBAAA;EACf,IAAA;EACA,OAAA;EACA,cAAA,EAAgB,aAAA;EAChB,gBAAA,EAAkB,aAAA;EAClB,SAAA,GAAY,aAAA;AAAA;AAAA,UAGG,QAAA;EACf,MAAA,CAAO,KAAA,WAAgB,SAAA,EAAW,MAAA,EAAQ,aAAA,GAAgB,OAAA,CAAQ,gBAAA;EAClE,UAAA,CAAW,QAAA,WAAmB,OAAA;EAC9B,aAAA,IAAiB,OAAA;EACjB,KAAA,IAAS,OAAA;AAAA;AAAA,UAGM,qBAAA;EAZC;EAchB,GAAA;EAbkB;EAelB,QAAA,GAAW,OAAA;EAdC;EAgBZ,IAAA;EAhByB;EAkBzB,aAAA;AAAA;;;;;;;iBASoB,cAAA,CACpB,OAAA,GAAS,qBAAA,GACR,OAAA,CAAQ,QAAA"}
1
+ {"version":3,"file":"createRenderer.d.mts","names":[],"sources":["../../src/render/createRenderer.ts"],"mappings":";;;;;UAwJiB,gBAAA;EACf,IAAA;EACA,OAAA;EACA,cAAA,EAAgB,aAAA;EAChB,gBAAA,EAAkB,aAAA;EAClB,SAAA,GAAY,aAAA;AAAA;AAAA,UAGG,QAAA;EACf,MAAA,CAAO,KAAA,WAAgB,SAAA,EAAW,MAAA,EAAQ,aAAA,GAAgB,OAAA,CAAQ,gBAAA;EAClE,UAAA,CAAW,QAAA,WAAmB,OAAA;EAC9B,aAAA,IAAiB,OAAA;EACjB,KAAA,IAAS,OAAA;AAAA;AAAA,UAGM,qBAAA;EAZC;EAchB,GAAA;EAbkB;EAelB,QAAA,GAAW,cAAA;EAdC;EAgBZ,IAAA;EAhByB;EAkBzB,aAAA;AAAA;;;;;;;iBASoB,cAAA,CACpB,OAAA,GAAS,qBAAA,GACR,OAAA,CAAQ,QAAA"}
@@ -1,6 +1,7 @@
1
1
  import { MaizzleConfigKey } from "../composables/useConfig.mjs";
2
2
  import { RenderContextKey } from "../composables/renderContext.mjs";
3
3
  import { isLaravel } from "../utils/detect.mjs";
4
+ import { readFileSync } from "node:fs";
4
5
  import { dirname, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { defu } from "defu";
@@ -36,8 +37,49 @@ function codeBlockExtract() {
36
37
  const stripped = content.replace(/^\n+/, "").replace(/\s+$/, "");
37
38
  if (!stripped) return _match;
38
39
  const minIndent = stripped.match(/^[ \t]*(?=\S)/gm)?.reduce((min, ws) => Math.min(min, ws.length), Infinity) ?? 0;
39
- const dedented = minIndent > 0 ? stripped.replace(new RegExp(`^[ \\t]{${minIndent}}`, "gm"), "") : stripped;
40
- return `<${tag}${attrs} encoded-code="${Buffer.from(dedented).toString("base64")}" />`;
40
+ return `<${tag}${attrs} code="${(minIndent > 0 ? stripped.replace(new RegExp(`^[ \\t]{${minIndent}}`, "gm"), "") : stripped).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}" />`;
41
+ });
42
+ if (transformed !== code) return {
43
+ code: transformed,
44
+ map: null
45
+ };
46
+ }
47
+ };
48
+ }
49
+ /**
50
+ * Vite plugin that pre-processes <Markdown> tags:
51
+ * - Extracts slot content, dedents it, and passes as :content prop
52
+ * - Resolves `src` prop to read file contents at build time
53
+ */
54
+ function markdownExtract() {
55
+ const re = /<(Markdown|markdown)((?:\s[^>]*?)?)>([\s\S]*?)<\/\1>/g;
56
+ const selfClosingRe = /<(Markdown|markdown)((?:\s[^>]*?\bsrc\s*=\s*"[^"]*"[^>]*?))\/>/g;
57
+ return {
58
+ name: "maizzle:markdown-extract",
59
+ enforce: "pre",
60
+ transform(code, id) {
61
+ if (!id.endsWith(".vue") && !id.endsWith(".md")) return;
62
+ if (!code.includes("Markdown") && !code.includes("markdown")) return;
63
+ let transformed = code;
64
+ transformed = transformed.replace(re, (_match, tag, attrs, content) => {
65
+ if (/(?:^|\s):content\b/.test(attrs) || /v-bind:content\b/.test(attrs)) return _match;
66
+ const stripped = content.replace(/^\n+/, "").replace(/\s+$/, "");
67
+ if (!stripped) return _match;
68
+ const minIndent = stripped.match(/^[ \t]*(?=\S)/gm)?.reduce((min, ws) => Math.min(min, ws.length), Infinity) ?? 0;
69
+ return `<${tag}${attrs} content="${(minIndent > 0 ? stripped.replace(new RegExp(`^[ \\t]{${minIndent}}`, "gm"), "") : stripped).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}" />`;
70
+ });
71
+ transformed = transformed.replace(selfClosingRe, (_match, tag, attrs) => {
72
+ const srcMatch = attrs.match(/\bsrc\s*=\s*"([^"]*)"/);
73
+ if (!srcMatch) return _match;
74
+ const srcPath = srcMatch[1];
75
+ const resolvedPath = resolve(dirname(id), srcPath);
76
+ let fileContent;
77
+ try {
78
+ fileContent = readFileSync(resolvedPath, "utf-8").trim();
79
+ } catch {
80
+ return _match;
81
+ }
82
+ return `<${tag}${attrs.replace(/\s*\bsrc\s*=\s*"[^"]*"/, "")} content="${fileContent.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}" />`;
41
83
  });
42
84
  if (transformed !== code) return {
43
85
  code: transformed,
@@ -57,7 +99,8 @@ const vueRouterPkgDir = dirname(fileURLToPath(import.meta.resolve("vue-router/pa
57
99
  * Tailwind CSS compilation is handled by the transformer pipeline.
58
100
  */
59
101
  async function createRenderer(options = {}) {
60
- const { dts = false, markdown: markdownOptions, root = process.cwd(), componentDirs = [] } = options;
102
+ const { dts = false, markdown: markdownOptionsRaw, root = process.cwd(), componentDirs = [] } = options;
103
+ const { shikiTheme = "github-light", ...markdownOptions } = markdownOptionsRaw ?? {};
61
104
  const dtsDir = isLaravel() ? resolve(process.cwd(), "resources/js/types/maizzle") : resolve(root, ".maizzle");
62
105
  const VIRTUAL_SFC_ID = "virtual:maizzle-sfc.vue";
63
106
  let virtualSfcSource = "";
@@ -65,6 +108,7 @@ async function createRenderer(options = {}) {
65
108
  configFile: false,
66
109
  plugins: [
67
110
  codeBlockExtract(),
111
+ markdownExtract(),
68
112
  {
69
113
  name: "maizzle:virtual-sfc",
70
114
  resolveId(id) {
@@ -80,7 +124,14 @@ async function createRenderer(options = {}) {
80
124
  }),
81
125
  Markdown(defu(markdownOptions ?? {}, {
82
126
  headEnabled: true,
83
- wrapperDiv: false
127
+ wrapperDiv: false,
128
+ markdownOptions: { async highlight(code, lang) {
129
+ const { codeToHtml } = await import("shiki");
130
+ return codeToHtml(code, {
131
+ lang,
132
+ theme: shikiTheme
133
+ });
134
+ } }
84
135
  })),
85
136
  AutoImport({
86
137
  dirs: [resolve(__dirname, "../composables"), resolve(__dirname, "../filters")],
@@ -1 +1 @@
1
- {"version":3,"file":"createRenderer.mjs","names":["merge"],"sources":["../../src/render/createRenderer.ts"],"sourcesContent":["import { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { isLaravel } from '../utils/detect.ts'\nimport { createServer } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport Markdown from 'unplugin-vue-markdown/vite'\nimport AutoImport from 'unplugin-auto-import/vite'\nimport Components from 'unplugin-vue-components/vite'\nimport { unheadVueComposablesImports } from '@unhead/vue'\nimport { defu as merge } from 'defu'\nimport { createSSRApp } from 'vue'\nimport { renderToString } from 'vue/server-renderer'\nimport { createHead, renderSSRHead } from '@unhead/vue/server'\nimport { MaizzleConfigKey } from '../composables/useConfig.ts'\nimport { RenderContextKey } from '../composables/renderContext.ts'\nimport type { Component, InjectionKey } from 'vue'\nimport type { MaizzleConfig } from '../types/index.ts'\nimport type { Options as MarkdownOptions } from 'unplugin-vue-markdown/types'\nimport type { RenderContext } from '../composables/renderContext.ts'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n/**\n * Vite plugin that extracts raw slot content from <CodeBlock> tags\n * and passes it as a :code prop before Vue compiles the template.\n *\n * This lets users write HTML naturally inside CodeBlock slots without\n * Vue attempting to compile it as template syntax.\n */\nfunction codeBlockExtract() {\n // Matches <CodeBlock ...>content</CodeBlock> (and kebab-case <code-block>)\n const re = /<(CodeBlock|code-block)((?:\\s[^>]*?)?)>([\\s\\S]*?)<\\/\\1>/g\n\n return {\n name: 'maizzle:code-block-extract',\n enforce: 'pre' as const,\n transform(code: string, id: string) {\n if (!id.endsWith('.vue') && !id.endsWith('.md')) return\n if (!code.includes('CodeBlock') && !code.includes('code-block')) return\n\n const transformed = code.replace(re, (_match, tag, attrs, content) => {\n // Skip if already has a :code or v-bind:code prop\n if (/(?:^|\\s):code\\b/.test(attrs) || /v-bind:code\\b/.test(attrs)) return _match\n\n // Strip leading/trailing blank lines, then dedent based on\n // the minimum indent of non-empty lines (à la min-indent)\n const stripped = content.replace(/^\\n+/, '').replace(/\\s+$/, '')\n if (!stripped) return _match\n\n const minIndent = stripped.match(/^[ \\t]*(?=\\S)/gm)\n ?.reduce((min, ws) => Math.min(min, ws.length), Infinity) ?? 0\n\n const dedented = minIndent > 0\n ? stripped.replace(new RegExp(`^[ \\\\t]{${minIndent}}`, 'gm'), '')\n : stripped\n\n // Base64-encode so no characters can interfere with Vue's HTML parser.\n // The component decodes it back via Buffer.\n const encoded = Buffer.from(dedented).toString('base64')\n\n return `<${tag}${attrs} encoded-code=\"${encoded}\" />`\n })\n\n if (transformed !== code) {\n return { code: transformed, map: null }\n }\n },\n }\n}\n\nconst vuePkgDir = dirname(fileURLToPath(import.meta.resolve('vue/package.json')))\nconst vueServerRendererPkgDir = dirname(fileURLToPath(import.meta.resolve('@vue/server-renderer/package.json')))\nconst unheadVuePkgDir = resolve(dirname(fileURLToPath(import.meta.resolve('@unhead/vue'))), '..')\nconst vueRouterPkgDir = dirname(fileURLToPath(import.meta.resolve('vue-router/package.json')))\n\nexport interface RenderedTemplate {\n html: string\n doctype?: string\n templateConfig: MaizzleConfig\n sfcEventHandlers: RenderContext['sfcEventHandlers']\n plaintext?: RenderContext['plaintext']\n}\n\nexport interface Renderer {\n render(input: string | Component, config: MaizzleConfig): Promise<RenderedTemplate>\n invalidate(filePath: string): Promise<void>\n invalidateAll(): Promise<void>\n close(): Promise<void>\n}\n\nexport interface CreateRendererOptions {\n /** Generate .d.ts files for auto-imports and components (default: false) */\n dts?: boolean\n /** Options passed to unplugin-vue-markdown */\n markdown?: MarkdownOptions\n /** Root directory for resolving user component dirs and .d.ts output */\n root?: string\n /** Additional component directories to register for auto-import */\n componentDirs?: string[]\n}\n\n/**\n * Lightweight Vite SSR loader for rendering Vue SFC email templates.\n *\n * Uses only Vue + unplugin for component/auto-import resolution.\n * Tailwind CSS compilation is handled by the transformer pipeline.\n */\nexport async function createRenderer(\n options: CreateRendererOptions = {},\n): Promise<Renderer> {\n const { dts = false, markdown: markdownOptions, root = process.cwd(), componentDirs = [] } = options\n\n const dtsDir = isLaravel()\n ? resolve(process.cwd(), 'resources/js/types/maizzle')\n : resolve(root, '.maizzle')\n\n const VIRTUAL_SFC_ID = 'virtual:maizzle-sfc.vue'\n let virtualSfcSource = ''\n\n const server = await createServer({\n configFile: false,\n plugins: [\n codeBlockExtract(),\n {\n name: 'maizzle:virtual-sfc',\n resolveId(id) {\n if (id === VIRTUAL_SFC_ID) return id\n },\n load(id) {\n if (id === VIRTUAL_SFC_ID) return virtualSfcSource\n },\n },\n vue({\n include: [/\\.vue$/, /\\.md$/],\n template: {\n transformAssetUrls: false,\n },\n }),\n Markdown(merge(markdownOptions ?? {}, {\n headEnabled: true,\n wrapperDiv: false,\n })),\n AutoImport({\n dirs: [\n resolve(__dirname, '../composables'),\n resolve(__dirname, '../filters'),\n ],\n imports: ['vue', unheadVueComposablesImports],\n dts: dts ? resolve(dtsDir, 'auto-imports.d.ts') : false,\n }),\n Components({\n extensions: ['vue', 'md'],\n include: [/\\.vue$/, /\\.vue\\?vue/, /\\.md$/],\n dirs: [\n resolve(__dirname, '../components'),\n resolve(root, 'components'),\n ...componentDirs,\n ],\n dts: dts ? resolve(dtsDir, 'components.d.ts') : false,\n }),\n ],\n resolve: {\n alias: {\n 'vue/server-renderer': resolve(vueServerRendererPkgDir, 'dist/server-renderer.esm-bundler.js'),\n 'vue': resolve(vuePkgDir, 'dist/vue.runtime.esm-bundler.js'),\n 'vue-router': vueRouterPkgDir,\n '@unhead/vue/server': resolve(unheadVuePkgDir, 'dist/server.mjs'),\n '@unhead/vue': resolve(unheadVuePkgDir, 'dist/index.mjs'),\n },\n },\n server: {\n middlewareMode: true,\n hmr: false,\n watch: null,\n fs: {\n allow: [process.cwd(), root, ...componentDirs, vuePkgDir, vueServerRendererPkgDir, unheadVuePkgDir, vueRouterPkgDir],\n },\n },\n appType: 'custom',\n logLevel: 'silent',\n optimizeDeps: {\n noDiscovery: true,\n },\n })\n\n return {\n async render(input: string | Component, config: MaizzleConfig): Promise<RenderedTemplate> {\n let component: Component\n let configKey: InjectionKey<MaizzleConfig>\n let contextKey: InjectionKey<RenderContext>\n\n if (typeof input === 'string') {\n // String input goes through Vite — must use ssrLoadModule for injection keys\n // so they share the same module instance as the SFC\n const configModule = await server.ssrLoadModule(resolve(__dirname, '../composables/useConfig'))\n const contextModule = await server.ssrLoadModule(resolve(__dirname, '../composables/renderContext'))\n configKey = configModule.MaizzleConfigKey\n contextKey = contextModule.RenderContextKey\n\n if (input.includes('<template') || input.includes('<script')) {\n virtualSfcSource = input\n const mod = server.moduleGraph.getModuleById(VIRTUAL_SFC_ID)\n if (mod) server.moduleGraph.invalidateModule(mod)\n component = (await server.ssrLoadModule(VIRTUAL_SFC_ID)).default\n } else {\n component = (await server.ssrLoadModule(input)).default\n }\n } else {\n // Pre-compiled component — use directly imported keys\n component = input\n configKey = MaizzleConfigKey\n contextKey = RenderContextKey\n }\n\n const renderContext: RenderContext = {\n doctype: undefined,\n sfcConfig: undefined,\n sfcEventHandlers: [],\n }\n\n const head = createHead({ disableDefaults: true })\n const app = createSSRApp(component)\n app.use(head)\n app.provide(configKey, config)\n app.provide(contextKey, renderContext)\n\n const ssrContext: Record<string, any> = {}\n let html: string = await renderToString(app, ssrContext)\n\n const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(head)\n\n // Inject head entries into the rendered HTML\n if (htmlAttrs) {\n html = html.replace(/<html([^>]*)>/, `<html$1 ${htmlAttrs}>`)\n }\n if (headTags) {\n html = html.replace('</head>', `${headTags}\\n</head>`)\n }\n if (bodyAttrs) {\n html = html.replace(/<body([^>]*)>/, `<body$1 ${bodyAttrs}>`)\n }\n if (bodyTagsOpen) {\n html = html.replace(/<body([^>]*)>/, `<body$1>\\n${bodyTagsOpen}`)\n }\n if (bodyTags) {\n html = html.replace('</body>', `${bodyTags}\\n</body>`)\n }\n\n // Inject SSR teleport content into their target elements\n if (ssrContext.teleports) {\n const { parse: parseDom, serialize: serializeDom, walk } = await import('../utils/ast/index.ts')\n let dom = parseDom(html)\n\n for (const [rawTarget, content] of Object.entries(ssrContext.teleports) as [string, string][]) {\n if (!content) continue\n\n const prepend = rawTarget.endsWith(':start')\n const target = prepend ? rawTarget.slice(0, -6) : rawTarget\n const targetChildren = parseDom(content)\n\n walk(dom, (node) => {\n const el = node as import('domhandler').Element\n\n if (!el.name) return\n\n const matched\n = target === el.name\n || (target.startsWith('#') && el.attribs?.id === target.slice(1))\n || (target.startsWith('.') && el.attribs?.class?.split(/\\s+/).includes(target.slice(1)))\n\n if (matched) {\n for (const child of targetChildren) {\n child.parent = el as any\n }\n\n el.children = prepend\n ? [...targetChildren, ...(el.children || [])] as any\n : [...(el.children || []), ...targetChildren] as any\n }\n })\n }\n\n html = serializeDom(dom)\n }\n\n // Inject preview/preheader text from usePreviewText() composable\n if (renderContext.previewText) {\n const { text, fillerCount, shyCount } = renderContext.previewText\n const filler = '\\u2007\\u034F '.repeat(fillerCount)\n const shys = '\\u00AD '.repeat(shyCount)\n const previewHtml = `<div style=\"display:none\">${text}${filler}${shys}\\u00A0</div>`\n html = html.replace(/<body([^>]*)>/, `<body$1>${previewHtml}`)\n }\n\n return {\n html,\n doctype: renderContext.doctype,\n templateConfig: renderContext.sfcConfig ?? config,\n sfcEventHandlers: renderContext.sfcEventHandlers,\n plaintext: renderContext.plaintext,\n }\n },\n\n async invalidate(filePath: string): Promise<void> {\n const mod = await server.moduleGraph.getModuleByUrl(filePath)\n if (mod) {\n server.moduleGraph.invalidateModule(mod)\n }\n },\n\n async invalidateAll(): Promise<void> {\n for (const mod of server.moduleGraph.idToModuleMap.values()) {\n server.moduleGraph.invalidateModule(mod)\n }\n },\n\n async close(): Promise<void> {\n await server.close()\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"createRenderer.mjs","names":["merge"],"sources":["../../src/render/createRenderer.ts"],"sourcesContent":["import { dirname, resolve } from 'node:path'\nimport { readFileSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { isLaravel } from '../utils/detect.ts'\nimport { createServer } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport Markdown from 'unplugin-vue-markdown/vite'\nimport AutoImport from 'unplugin-auto-import/vite'\nimport Components from 'unplugin-vue-components/vite'\nimport { unheadVueComposablesImports } from '@unhead/vue'\nimport { defu as merge } from 'defu'\nimport { createSSRApp } from 'vue'\nimport { renderToString } from 'vue/server-renderer'\nimport { createHead, renderSSRHead } from '@unhead/vue/server'\nimport { MaizzleConfigKey } from '../composables/useConfig.ts'\nimport { RenderContextKey } from '../composables/renderContext.ts'\nimport type { Component, InjectionKey } from 'vue'\nimport type { MaizzleConfig, MarkdownConfig } from '../types/index.ts'\nimport type { RenderContext } from '../composables/renderContext.ts'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n/**\n * Vite plugin that extracts raw slot content from <CodeBlock> tags\n * and passes it as a :code prop before Vue compiles the template.\n *\n * This lets users write HTML naturally inside CodeBlock slots without\n * Vue attempting to compile it as template syntax.\n */\nfunction codeBlockExtract() {\n // Matches <CodeBlock ...>content</CodeBlock> (and kebab-case <code-block>)\n const re = /<(CodeBlock|code-block)((?:\\s[^>]*?)?)>([\\s\\S]*?)<\\/\\1>/g\n\n return {\n name: 'maizzle:code-block-extract',\n enforce: 'pre' as const,\n transform(code: string, id: string) {\n if (!id.endsWith('.vue') && !id.endsWith('.md')) return\n if (!code.includes('CodeBlock') && !code.includes('code-block')) return\n\n const transformed = code.replace(re, (_match, tag, attrs, content) => {\n // Skip if already has a :code or v-bind:code prop\n if (/(?:^|\\s):code\\b/.test(attrs) || /v-bind:code\\b/.test(attrs)) return _match\n\n // Strip leading/trailing blank lines, then dedent based on\n // the minimum indent of non-empty lines (à la min-indent)\n const stripped = content.replace(/^\\n+/, '').replace(/\\s+$/, '')\n if (!stripped) return _match\n\n const minIndent = stripped.match(/^[ \\t]*(?=\\S)/gm)\n ?.reduce((min, ws) => Math.min(min, ws.length), Infinity) ?? 0\n\n const dedented = minIndent > 0\n ? stripped.replace(new RegExp(`^[ \\\\t]{${minIndent}}`, 'gm'), '')\n : stripped\n\n // HTML-escape for safe embedding in a static attribute value.\n const escaped = dedented\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n\n return `<${tag}${attrs} code=\"${escaped}\" />`\n })\n\n if (transformed !== code) {\n return { code: transformed, map: null }\n }\n },\n }\n}\n\n/**\n * Vite plugin that pre-processes <Markdown> tags:\n * - Extracts slot content, dedents it, and passes as :content prop\n * - Resolves `src` prop to read file contents at build time\n */\nfunction markdownExtract() {\n const re = /<(Markdown|markdown)((?:\\s[^>]*?)?)>([\\s\\S]*?)<\\/\\1>/g\n const selfClosingRe = /<(Markdown|markdown)((?:\\s[^>]*?\\bsrc\\s*=\\s*\"[^\"]*\"[^>]*?))\\/>/g\n\n return {\n name: 'maizzle:markdown-extract',\n enforce: 'pre' as const,\n transform(code: string, id: string) {\n if (!id.endsWith('.vue') && !id.endsWith('.md')) return\n if (!code.includes('Markdown') && !code.includes('markdown')) return\n\n let transformed = code\n\n // Handle <Markdown>content</Markdown>\n transformed = transformed.replace(re, (_match, tag, attrs, content) => {\n if (/(?:^|\\s):content\\b/.test(attrs) || /v-bind:content\\b/.test(attrs)) return _match\n\n const stripped = content.replace(/^\\n+/, '').replace(/\\s+$/, '')\n if (!stripped) return _match\n\n const minIndent = stripped.match(/^[ \\t]*(?=\\S)/gm)\n ?.reduce((min: number, ws: string) => Math.min(min, ws.length), Infinity) ?? 0\n\n const dedented = minIndent > 0\n ? stripped.replace(new RegExp(`^[ \\\\t]{${minIndent}}`, 'gm'), '')\n : stripped\n\n const escaped = dedented\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n\n return `<${tag}${attrs} content=\"${escaped}\" />`\n })\n\n // Handle <Markdown src=\"./file.md\" /> — resolve and inline file content\n transformed = transformed.replace(selfClosingRe, (_match, tag, attrs) => {\n const srcMatch = attrs.match(/\\bsrc\\s*=\\s*\"([^\"]*)\"/)\n if (!srcMatch) return _match\n\n const srcPath = srcMatch[1]\n const resolvedPath = resolve(dirname(id), srcPath)\n\n let fileContent: string\n try {\n fileContent = readFileSync(resolvedPath, 'utf-8').trim()\n } catch {\n return _match\n }\n\n // Remove src prop, add content prop\n const cleanAttrs = attrs.replace(/\\s*\\bsrc\\s*=\\s*\"[^\"]*\"/, '')\n const escaped = fileContent\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n\n return `<${tag}${cleanAttrs} content=\"${escaped}\" />`\n })\n\n if (transformed !== code) {\n return { code: transformed, map: null }\n }\n },\n }\n}\n\nconst vuePkgDir = dirname(fileURLToPath(import.meta.resolve('vue/package.json')))\nconst vueServerRendererPkgDir = dirname(fileURLToPath(import.meta.resolve('@vue/server-renderer/package.json')))\nconst unheadVuePkgDir = resolve(dirname(fileURLToPath(import.meta.resolve('@unhead/vue'))), '..')\nconst vueRouterPkgDir = dirname(fileURLToPath(import.meta.resolve('vue-router/package.json')))\n\nexport interface RenderedTemplate {\n html: string\n doctype?: string\n templateConfig: MaizzleConfig\n sfcEventHandlers: RenderContext['sfcEventHandlers']\n plaintext?: RenderContext['plaintext']\n}\n\nexport interface Renderer {\n render(input: string | Component, config: MaizzleConfig): Promise<RenderedTemplate>\n invalidate(filePath: string): Promise<void>\n invalidateAll(): Promise<void>\n close(): Promise<void>\n}\n\nexport interface CreateRendererOptions {\n /** Generate .d.ts files for auto-imports and components (default: false) */\n dts?: boolean\n /** Options passed to unplugin-vue-markdown */\n markdown?: MarkdownConfig\n /** Root directory for resolving user component dirs and .d.ts output */\n root?: string\n /** Additional component directories to register for auto-import */\n componentDirs?: string[]\n}\n\n/**\n * Lightweight Vite SSR loader for rendering Vue SFC email templates.\n *\n * Uses only Vue + unplugin for component/auto-import resolution.\n * Tailwind CSS compilation is handled by the transformer pipeline.\n */\nexport async function createRenderer(\n options: CreateRendererOptions = {},\n): Promise<Renderer> {\n const { dts = false, markdown: markdownOptionsRaw, root = process.cwd(), componentDirs = [] } = options\n const { shikiTheme = 'github-light', ...markdownOptions } = markdownOptionsRaw ?? {}\n\n const dtsDir = isLaravel()\n ? resolve(process.cwd(), 'resources/js/types/maizzle')\n : resolve(root, '.maizzle')\n\n const VIRTUAL_SFC_ID = 'virtual:maizzle-sfc.vue'\n let virtualSfcSource = ''\n\n const server = await createServer({\n configFile: false,\n plugins: [\n codeBlockExtract(),\n markdownExtract(),\n {\n name: 'maizzle:virtual-sfc',\n resolveId(id) {\n if (id === VIRTUAL_SFC_ID) return id\n },\n load(id) {\n if (id === VIRTUAL_SFC_ID) return virtualSfcSource\n },\n },\n vue({\n include: [/\\.vue$/, /\\.md$/],\n template: {\n transformAssetUrls: false,\n },\n }),\n Markdown(merge(markdownOptions ?? {}, {\n headEnabled: true,\n wrapperDiv: false,\n markdownOptions: {\n async highlight(code: string, lang: string) {\n const { codeToHtml } = await import('shiki')\n return codeToHtml(code, { lang, theme: shikiTheme })\n },\n },\n })),\n AutoImport({\n dirs: [\n resolve(__dirname, '../composables'),\n resolve(__dirname, '../filters'),\n ],\n imports: ['vue', unheadVueComposablesImports],\n dts: dts ? resolve(dtsDir, 'auto-imports.d.ts') : false,\n }),\n Components({\n extensions: ['vue', 'md'],\n include: [/\\.vue$/, /\\.vue\\?vue/, /\\.md$/],\n dirs: [\n resolve(__dirname, '../components'),\n resolve(root, 'components'),\n ...componentDirs,\n ],\n dts: dts ? resolve(dtsDir, 'components.d.ts') : false,\n }),\n ],\n resolve: {\n alias: {\n 'vue/server-renderer': resolve(vueServerRendererPkgDir, 'dist/server-renderer.esm-bundler.js'),\n 'vue': resolve(vuePkgDir, 'dist/vue.runtime.esm-bundler.js'),\n 'vue-router': vueRouterPkgDir,\n '@unhead/vue/server': resolve(unheadVuePkgDir, 'dist/server.mjs'),\n '@unhead/vue': resolve(unheadVuePkgDir, 'dist/index.mjs'),\n },\n },\n server: {\n middlewareMode: true,\n hmr: false,\n watch: null,\n fs: {\n allow: [process.cwd(), root, ...componentDirs, vuePkgDir, vueServerRendererPkgDir, unheadVuePkgDir, vueRouterPkgDir],\n },\n },\n appType: 'custom',\n logLevel: 'silent',\n optimizeDeps: {\n noDiscovery: true,\n },\n })\n\n return {\n async render(input: string | Component, config: MaizzleConfig): Promise<RenderedTemplate> {\n let component: Component\n let configKey: InjectionKey<MaizzleConfig>\n let contextKey: InjectionKey<RenderContext>\n\n if (typeof input === 'string') {\n // String input goes through Vite — must use ssrLoadModule for injection keys\n // so they share the same module instance as the SFC\n const configModule = await server.ssrLoadModule(resolve(__dirname, '../composables/useConfig'))\n const contextModule = await server.ssrLoadModule(resolve(__dirname, '../composables/renderContext'))\n configKey = configModule.MaizzleConfigKey\n contextKey = contextModule.RenderContextKey\n\n if (input.includes('<template') || input.includes('<script')) {\n virtualSfcSource = input\n const mod = server.moduleGraph.getModuleById(VIRTUAL_SFC_ID)\n if (mod) server.moduleGraph.invalidateModule(mod)\n component = (await server.ssrLoadModule(VIRTUAL_SFC_ID)).default\n } else {\n component = (await server.ssrLoadModule(input)).default\n }\n } else {\n // Pre-compiled component — use directly imported keys\n component = input\n configKey = MaizzleConfigKey\n contextKey = RenderContextKey\n }\n\n const renderContext: RenderContext = {\n doctype: undefined,\n sfcConfig: undefined,\n sfcEventHandlers: [],\n }\n\n const head = createHead({ disableDefaults: true })\n const app = createSSRApp(component)\n app.use(head)\n app.provide(configKey, config)\n app.provide(contextKey, renderContext)\n\n const ssrContext: Record<string, any> = {}\n let html: string = await renderToString(app, ssrContext)\n\n const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(head)\n\n // Inject head entries into the rendered HTML\n if (htmlAttrs) {\n html = html.replace(/<html([^>]*)>/, `<html$1 ${htmlAttrs}>`)\n }\n if (headTags) {\n html = html.replace('</head>', `${headTags}\\n</head>`)\n }\n if (bodyAttrs) {\n html = html.replace(/<body([^>]*)>/, `<body$1 ${bodyAttrs}>`)\n }\n if (bodyTagsOpen) {\n html = html.replace(/<body([^>]*)>/, `<body$1>\\n${bodyTagsOpen}`)\n }\n if (bodyTags) {\n html = html.replace('</body>', `${bodyTags}\\n</body>`)\n }\n\n // Inject SSR teleport content into their target elements\n if (ssrContext.teleports) {\n const { parse: parseDom, serialize: serializeDom, walk } = await import('../utils/ast/index.ts')\n let dom = parseDom(html)\n\n for (const [rawTarget, content] of Object.entries(ssrContext.teleports) as [string, string][]) {\n if (!content) continue\n\n const prepend = rawTarget.endsWith(':start')\n const target = prepend ? rawTarget.slice(0, -6) : rawTarget\n const targetChildren = parseDom(content)\n\n walk(dom, (node) => {\n const el = node as import('domhandler').Element\n\n if (!el.name) return\n\n const matched\n = target === el.name\n || (target.startsWith('#') && el.attribs?.id === target.slice(1))\n || (target.startsWith('.') && el.attribs?.class?.split(/\\s+/).includes(target.slice(1)))\n\n if (matched) {\n for (const child of targetChildren) {\n child.parent = el as any\n }\n\n el.children = prepend\n ? [...targetChildren, ...(el.children || [])] as any\n : [...(el.children || []), ...targetChildren] as any\n }\n })\n }\n\n html = serializeDom(dom)\n }\n\n // Inject preview/preheader text from usePreviewText() composable\n if (renderContext.previewText) {\n const { text, fillerCount, shyCount } = renderContext.previewText\n const filler = '\\u2007\\u034F '.repeat(fillerCount)\n const shys = '\\u00AD '.repeat(shyCount)\n const previewHtml = `<div style=\"display:none\">${text}${filler}${shys}\\u00A0</div>`\n html = html.replace(/<body([^>]*)>/, `<body$1>${previewHtml}`)\n }\n\n return {\n html,\n doctype: renderContext.doctype,\n templateConfig: renderContext.sfcConfig ?? config,\n sfcEventHandlers: renderContext.sfcEventHandlers,\n plaintext: renderContext.plaintext,\n }\n },\n\n async invalidate(filePath: string): Promise<void> {\n const mod = await server.moduleGraph.getModuleByUrl(filePath)\n if (mod) {\n server.moduleGraph.invalidateModule(mod)\n }\n },\n\n async invalidateAll(): Promise<void> {\n for (const mod of server.moduleGraph.idToModuleMap.values()) {\n server.moduleGraph.invalidateModule(mod)\n }\n },\n\n async close(): Promise<void> {\n await server.close()\n },\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.mts","names":[],"sources":["../src/serve.ts"],"mappings":";;;;UAwBiB,YAAA;EACf,MAAA,GAAS,OAAA,CAAQ,aAAA;;EAEjB,IAAA;EAH2B;EAK3B,MAAA;AAAA;;;;;;;;AAYF;;iBAAsB,KAAA,CAAM,OAAA,GAAS,YAAA,GAAiB,OAAA,CAAA,aAAA;AAAA,iBAqdtC,WAAA,CAAY,MAAA,EAAQ,aAAA,EAAe,WAAA"}
1
+ {"version":3,"file":"serve.d.mts","names":[],"sources":["../src/serve.ts"],"mappings":";;;;UAyBiB,YAAA;EACf,MAAA,GAAS,OAAA,CAAQ,aAAA;;EAEjB,IAAA;EAH2B;EAK3B,MAAA;AAAA;;;;;;;;AAYF;;iBAAsB,KAAA,CAAM,OAAA,GAAS,YAAA,GAAiB,OAAA,CAAA,aAAA;AAAA,iBAuiBtC,WAAA,CAAY,MAAA,EAAQ,aAAA,EAAe,WAAA"}
package/dist/serve.mjs CHANGED
@@ -4,6 +4,7 @@ import { createRenderer } from "./render/createRenderer.mjs";
4
4
  import { createPlaintext } from "./plaintext.mjs";
5
5
  import { serveCompatibility } from "./server/compatibility.mjs";
6
6
  import { serveLint } from "./server/linter.mjs";
7
+ import { sendEmail } from "./server/email.mjs";
7
8
  import { createRequire } from "node:module";
8
9
  import { readFileSync } from "node:fs";
9
10
  import { basename, dirname, resolve } from "node:path";
@@ -32,7 +33,7 @@ async function serve(options = {}) {
32
33
  const start = performance.now();
33
34
  let config = await resolveConfig(options.config);
34
35
  const port = config.server?.port ?? 3e3;
35
- const renderer = await createRenderer({
36
+ let renderer = await createRenderer({
36
37
  dts: true,
37
38
  markdown: config.markdown,
38
39
  root: config.root,
@@ -170,7 +171,16 @@ function maizzleDevPlugin(config, renderer, configInput) {
170
171
  }
171
172
  });
172
173
  server.watcher.on("change", async (file) => {
173
- if (watchPaths.some((p) => file.endsWith(p))) config = await resolveConfig(configInput);
174
+ if (watchPaths.some((p) => file.endsWith(p))) {
175
+ config = await resolveConfig(configInput);
176
+ await renderer.close();
177
+ renderer = await createRenderer({
178
+ dts: true,
179
+ markdown: config.markdown,
180
+ root: config.root,
181
+ componentDirs: [config.components?.source ?? []].flat()
182
+ });
183
+ }
174
184
  await renderer.invalidateAll();
175
185
  if (isTemplateFile(file) || watchPaths.some((p) => file.endsWith(p))) server.ws.send({
176
186
  type: "custom",
@@ -183,11 +193,13 @@ function maizzleDevPlugin(config, renderer, configInput) {
183
193
  if (url === "/__maizzle/templates") return serveTemplateList(config, res);
184
194
  if (url.startsWith("/__maizzle/render/")) return await serveRenderedTemplate(url, config, renderer, res);
185
195
  if (url.startsWith("/__maizzle/source/")) return await serveHighlightedSource(url, config, renderer, res);
186
- if (url.startsWith("/__maizzle/compatibility/")) return await serveCompatibility(url, config, res);
196
+ if (url === "/__maizzle/compatibility" && req.method === "POST") return await serveCompatibility(req, res);
187
197
  if (url.startsWith("/__maizzle/lint/")) return await serveLint(url, config, res);
188
198
  if (url.startsWith("/__maizzle/vue-source/")) return await serveVueSource(url, config, res);
189
199
  if (url.startsWith("/__maizzle/plaintext/")) return await servePlaintext(url, config, renderer, res);
190
200
  if (url.startsWith("/__maizzle/stats/")) return await serveStats(url, config, renderer, res);
201
+ if (url.startsWith("/__maizzle/email/") && req.method === "POST") return await serveEmailEndpoint(url, req, res, config, renderer);
202
+ if (url === "/__maizzle/email-config") return serveEmailConfig(config, res);
191
203
  next();
192
204
  });
193
205
  return () => {
@@ -392,6 +404,74 @@ async function serveStats(url, config, renderer, res) {
392
404
  res.end(JSON.stringify({ error: error.message }));
393
405
  }
394
406
  }
407
+ async function serveEmailEndpoint(url, req, res, config, renderer) {
408
+ const templateSlug = url.replace("/__maizzle/email/", "").replace(/\?.*$/, "");
409
+ const match = (await glob(config.content ?? ["emails/**/*.vue"])).find((t) => t.replace(/\.(vue|md)$/, "") === templateSlug);
410
+ if (!match) {
411
+ res.statusCode = 404;
412
+ res.end(JSON.stringify({
413
+ success: false,
414
+ message: "Template not found"
415
+ }));
416
+ return;
417
+ }
418
+ let body = "";
419
+ for await (const chunk of req) body += chunk;
420
+ let payload;
421
+ try {
422
+ payload = JSON.parse(body);
423
+ } catch {
424
+ res.statusCode = 400;
425
+ res.end(JSON.stringify({
426
+ success: false,
427
+ message: "Invalid JSON"
428
+ }));
429
+ return;
430
+ }
431
+ if (!payload.to?.length) {
432
+ res.statusCode = 400;
433
+ res.end(JSON.stringify({
434
+ success: false,
435
+ message: "Missing recipients"
436
+ }));
437
+ return;
438
+ }
439
+ try {
440
+ const absolutePath = resolve(match);
441
+ await renderer.invalidateAll();
442
+ const rendered = await renderer.render(absolutePath, config);
443
+ let html = rendered.html;
444
+ const templateConfig = rendered.templateConfig;
445
+ const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>";
446
+ html = await runTransformers(html, templateConfig, absolutePath, doctype);
447
+ html = `${doctype}\n${html}`;
448
+ const text = createPlaintext(html);
449
+ const result = await sendEmail({
450
+ to: payload.to,
451
+ subject: payload.subject,
452
+ html,
453
+ text
454
+ }, config, templateConfig);
455
+ res.setHeader("Content-Type", "application/json");
456
+ res.end(JSON.stringify(result));
457
+ } catch (error) {
458
+ res.statusCode = 500;
459
+ res.end(JSON.stringify({
460
+ success: false,
461
+ message: error.message
462
+ }));
463
+ }
464
+ }
465
+ function serveEmailConfig(config, res) {
466
+ const emailConfig = config.server?.email;
467
+ res.setHeader("Content-Type", "application/json");
468
+ res.end(JSON.stringify({
469
+ to: emailConfig?.to ? Array.isArray(emailConfig.to) ? emailConfig.to : [emailConfig.to] : [],
470
+ from: emailConfig?.from ?? "",
471
+ subject: emailConfig?.subject ?? "",
472
+ hasTransport: !!emailConfig?.transport
473
+ }));
474
+ }
395
475
  function printBanner(server, startupTime) {
396
476
  const info = server.config.logger.info;
397
477
  const time = startupTime ?? server._maizzleStartupTime;