UNPKG

@maizzle/framework

Version:

Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.

141 lines (140 loc) 6.13 kB
import { runTransformers } from "../transformers/index.js"; import { createPlaintext } from "../plaintext.js"; import { stripForHtml, stripForPlaintext } from "../utils/output-markers.js"; import { _setCurrentTemplate } from "../composables/useCurrentTemplate.js"; import { cloneConfig } from "../utils/cloneConfig.js"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { basename, dirname, join, parse, relative, resolve, sep } from "node:path"; import defu from "defu"; //#region src/render/buildTemplate.ts /** * Render a single template through the full pipeline and write its output. * * Shared by the sequential build loop and the parallel build worker so both * paths produce byte-identical output. `events` is the manager the per-template * events fire on (config handlers registered via `registerConfig`, SFC handlers * registered here from the render). The caller owns build-scoped events * (`beforeCreate`/`afterBuild`). */ async function buildTemplate(templatePath, ctx) { const { config, renderer, events, outputPath, outputExtension, contentBase } = ctx; const absolutePath = resolve(templatePath); const parsedPath = parse(absolutePath); const template = { source: readFileSync(absolutePath, "utf-8"), path: parsedPath }; const files = []; let sfcAfterBuildCount = 0; _setCurrentTemplate(parsedPath); try { /** * Clone config per template so beforeRender mutations (setting a * preheader, injecting fetched data, etc.) stay scoped to this template * instead of leaking into later ones through the shared config object. */ const renderConfig = cloneConfig(config); const originalSource = template.source; await events.fireBeforeRender({ config: renderConfig, template }); const rendered = await renderer.render(absolutePath, renderConfig, template.source !== originalSource ? { source: template.source } : void 0); /** * Register SFC event handlers collected during render so they take part in * the post-render events. Cleared at the end of this call so they don't * leak into the next template (afterBuild is the exception — it's never * cleared by clearSfcHandlers; see the count above). */ for (const { name, handler } of rendered.sfcEventHandlers) { if (name === "afterBuild") sfcAfterBuildCount++; events.on(name, handler); } const templateConfig = rendered.templateConfig; let html = await events.fireAfterRender({ config: templateConfig, template, html: rendered.html }); const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>"; if (templateConfig.useTransformers !== false) html = await runTransformers(html, templateConfig, absolutePath, doctype, rendered.tailwindBlocks); html = await events.fireAfterTransform({ config: templateConfig, template, html }); if (doctype) html = `${doctype}\n${html}`; const htmlOut = stripForHtml(html); const sfcOutputPath = rendered.outputPath; let outputFilePath; if (sfcOutputPath) { const parsed = parse(resolve(sfcOutputPath)); const ext = parsed.ext ? parsed.ext.slice(1) : outputExtension; outputFilePath = join(parsed.dir, `${parsed.name}.${ext}`); } else outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase); mkdirSync(dirname(outputFilePath), { recursive: true }); writeFileSync(outputFilePath, htmlOut); files.push(outputFilePath); const globalPlaintext = templateConfig.plaintext; const sfcPlaintext = rendered.plaintext; if (globalPlaintext || sfcPlaintext) { const globalCfg = typeof globalPlaintext === "object" ? globalPlaintext : {}; const stripOptions = defu(sfcPlaintext?.options, globalCfg.options); const plaintext = createPlaintext(stripForPlaintext(html), stripOptions); const ptExtension = sfcPlaintext?.extension ?? globalCfg.extension ?? "txt"; let ptOutputPath; if (sfcPlaintext?.destination) ptOutputPath = resolveOutputPath(templatePath, resolve(sfcPlaintext.destination), ptExtension, contentBase); else if (sfcOutputPath) { const parsed = parse(outputFilePath); ptOutputPath = join(parsed.dir, `${parsed.name}.${ptExtension}`); } else if (globalCfg.destination) ptOutputPath = resolveOutputPath(templatePath, resolve(globalCfg.destination), ptExtension, contentBase); else ptOutputPath = resolveOutputPath(templatePath, outputPath, ptExtension, contentBase); mkdirSync(dirname(ptOutputPath), { recursive: true }); writeFileSync(ptOutputPath, plaintext); files.push(ptOutputPath); } } finally { _setCurrentTemplate(void 0); events.clearSfcHandlers(); } return { files, sfcAfterBuildCount }; } /** * Extract the static (non-glob) prefix from content patterns. * * For example, `['/abs/path/emails/**\/*.vue']` → `'/abs/path/emails'` * * Used to strip the content base from template paths so the output preserves * only the subdirectory structure. * * With multiple positive patterns (multi-root setups), returns their common * ancestor directory so templates from every root keep a clean relative path. */ function computeContentBase(patterns) { const positives = patterns.filter((p) => !p.startsWith("!")); return (positives.length > 0 ? positives : patterns).map((pattern) => { const staticPart = pattern.split(/[*{?[]/)[0]; return resolve(staticPart.endsWith("/") ? staticPart : dirname(staticPart)); }).reduce(commonPath); } /** Longest common directory path shared by two absolute paths. */ function commonPath(a, b) { const aSegments = a.split(sep); const bSegments = b.split(sep); const shared = []; for (let i = 0; i < Math.min(aSegments.length, bSegments.length); i++) { if (aSegments[i] !== bSegments[i]) break; shared.push(aSegments[i]); } return shared.join(sep) || sep; } function resolveOutputPath(templatePath, outputDir, extension, contentBase) { const name = basename(templatePath).replace(/\.(vue|md)$/, ""); return join(outputDir, relative(contentBase, dirname(resolve(templatePath))), `${name}.${extension}`); } //#endregion export { buildTemplate, computeContentBase, resolveOutputPath }; //# sourceMappingURL=buildTemplate.js.map