@maizzle/framework 6.0.0-rc.25 → 6.0.0-rc.26
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/dist/build.d.ts +19 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +139 -102
- package/dist/build.js.map +1 -1
- package/dist/config/index.js +1 -1
- package/dist/index.js +2 -2
- package/dist/render/buildTemplate.d.ts +49 -0
- package/dist/render/buildTemplate.d.ts.map +1 -0
- package/dist/render/buildTemplate.js +139 -0
- package/dist/render/buildTemplate.js.map +1 -0
- package/dist/render/createRenderer.d.ts +3 -1
- package/dist/render/createRenderer.d.ts.map +1 -1
- package/dist/render/createRenderer.js +40 -3
- package/dist/render/createRenderer.js.map +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/parallel/buildWorker.d.ts +31 -0
- package/dist/render/parallel/buildWorker.d.ts.map +1 -0
- package/dist/render/parallel/buildWorker.js +66 -0
- package/dist/render/parallel/buildWorker.js.map +1 -0
- package/dist/render/parallel/worker.mjs +28 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +73 -53
- package/dist/serve.js.map +1 -1
- package/dist/server/sfc-utils.js +1 -1
- package/dist/server/ui/pages/Preview.vue +34 -11
- package/dist/server/ui/vite-env.d.ts +1 -0
- package/dist/types/config.d.ts +22 -1
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +6 -5
package/dist/build.d.ts
CHANGED
|
@@ -15,6 +15,24 @@ interface BuildResult {
|
|
|
15
15
|
* from the working directory.
|
|
16
16
|
*/
|
|
17
17
|
declare function build(configInput?: Partial<MaizzleConfig> | string): Promise<BuildResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Decide whether to build in parallel and with how many workers.
|
|
20
|
+
*
|
|
21
|
+
* `config.parallel`:
|
|
22
|
+
* - omitted → parallel when `count > 50`, min(CPU count − 1, 8) workers
|
|
23
|
+
* - `true` → always parallel (ignores threshold), default workers
|
|
24
|
+
* - `false` → always sequential
|
|
25
|
+
* - `{ workers, threshold }` → parallel when `count > threshold` (default 50),
|
|
26
|
+
* using `workers` threads (default min(CPU count − 1, 8))
|
|
27
|
+
*
|
|
28
|
+
* Workers reload the config file to recover function hooks, so parallel only
|
|
29
|
+
* applies to file-based configs (a path or the default cwd config) — an inline
|
|
30
|
+
* config object has no file to reload and always builds sequentially.
|
|
31
|
+
*/
|
|
32
|
+
declare function resolveParallel(config: MaizzleConfig, count: number, configInput: Partial<MaizzleConfig> | string | undefined): {
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
workers: number;
|
|
35
|
+
};
|
|
18
36
|
//#endregion
|
|
19
|
-
export { BuildResult, build };
|
|
37
|
+
export { BuildResult, build, resolveParallel };
|
|
20
38
|
//# sourceMappingURL=build.d.ts.map
|
package/dist/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","names":[],"sources":["../src/build.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"build.d.ts","names":[],"sources":["../src/build.ts"],"mappings":";;UAaiB,WAAA;EACf,KAAA;EACA,MAAA,EAAQ,aAAa;AAAA;;;;;;;;AAAA;AAavB;;iBAAsB,KAAA,CAAM,WAAA,GAAc,OAAA,CAAQ,aAAA,aAA0B,OAAA,CAAQ,WAAA;;;;;;;;;;;;;AAAW;AA6G/F;iBAAgB,eAAA,CACd,MAAA,EAAQ,aAAA,EACR,KAAA,UACA,WAAA,EAAa,OAAA,CAAQ,aAAA;EAClB,OAAA;EAAkB,OAAA;AAAA"}
|
package/dist/build.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { resolveConfig } from "./config/index.js";
|
|
2
2
|
import { EventManager } from "./events/index.js";
|
|
3
|
-
import { runTransformers } from "./transformers/index.js";
|
|
4
3
|
import { normalizeComponentSources } from "./utils/componentSources.js";
|
|
5
4
|
import { createRenderer } from "./render/createRenderer.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
5
|
+
import { buildTemplate, computeContentBase } from "./render/buildTemplate.js";
|
|
6
|
+
import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
7
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { availableParallelism } from "node:os";
|
|
11
10
|
import { glob } from "tinyglobby";
|
|
12
11
|
import ora from "ora";
|
|
13
|
-
import defu from "defu";
|
|
14
12
|
//#region src/build.ts
|
|
15
13
|
/**
|
|
16
14
|
* Build all SFC email templates to HTML files.
|
|
@@ -48,97 +46,56 @@ async function build(configInput) {
|
|
|
48
46
|
recursive: true,
|
|
49
47
|
force: true
|
|
50
48
|
});
|
|
51
|
-
const renderer = await createRenderer({
|
|
52
|
-
markdown: config.markdown,
|
|
53
|
-
root: config.root,
|
|
54
|
-
componentDirs: normalizeComponentSources(config.components?.source, process.cwd()),
|
|
55
|
-
vite: config.vite
|
|
56
|
-
});
|
|
57
49
|
const outputFiles = [];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Register SFC event handlers collected during render so they take
|
|
75
|
-
* part in the post-render events (afterRender / afterTransform).
|
|
76
|
-
* They're cleared at the end of the iteration so they don't
|
|
77
|
-
* leak into the next template.
|
|
78
|
-
*/
|
|
79
|
-
for (const { name, handler } of rendered.sfcEventHandlers) events.on(name, handler);
|
|
80
|
-
let html = await events.fireAfterRender({
|
|
81
|
-
config,
|
|
82
|
-
template,
|
|
83
|
-
html: rendered.html
|
|
84
|
-
});
|
|
85
|
-
/**
|
|
86
|
-
* Use the per-template merged config (from defineConfig() in the SFC) so
|
|
87
|
-
* that template-level overrides like css.safe: false are respected
|
|
88
|
-
* by transformers.
|
|
89
|
-
*/
|
|
90
|
-
const templateConfig = rendered.templateConfig;
|
|
91
|
-
const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>";
|
|
92
|
-
if (templateConfig.useTransformers !== false) html = await runTransformers(html, templateConfig, absolutePath, doctype, rendered.tailwindBlocks);
|
|
93
|
-
html = await events.fireAfterTransform({
|
|
94
|
-
config,
|
|
95
|
-
template,
|
|
96
|
-
html
|
|
97
|
-
});
|
|
98
|
-
if (doctype) html = `${doctype}\n${html}`;
|
|
99
|
-
const htmlOut = stripForHtml(html);
|
|
100
|
-
const sfcOutputPath = rendered.outputPath;
|
|
101
|
-
let outputFilePath;
|
|
102
|
-
if (sfcOutputPath) {
|
|
103
|
-
const parsed = parse(resolve(sfcOutputPath));
|
|
104
|
-
const ext = parsed.ext ? parsed.ext.slice(1) : outputExtension;
|
|
105
|
-
outputFilePath = join(parsed.dir, `${parsed.name}.${ext}`);
|
|
106
|
-
} else outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase);
|
|
107
|
-
mkdirSync(dirname(outputFilePath), { recursive: true });
|
|
108
|
-
writeFileSync(outputFilePath, htmlOut);
|
|
109
|
-
outputFiles.push(outputFilePath);
|
|
110
|
-
const globalPlaintext = templateConfig.plaintext;
|
|
111
|
-
const sfcPlaintext = rendered.plaintext;
|
|
112
|
-
if (globalPlaintext || sfcPlaintext) {
|
|
113
|
-
const globalCfg = typeof globalPlaintext === "object" ? globalPlaintext : {};
|
|
114
|
-
const stripOptions = defu(sfcPlaintext?.options, globalCfg.options);
|
|
115
|
-
const plaintext = createPlaintext(stripForPlaintext(html), stripOptions);
|
|
116
|
-
const ptExtension = sfcPlaintext?.extension ?? globalCfg.extension ?? "txt";
|
|
117
|
-
let ptOutputPath;
|
|
118
|
-
if (sfcPlaintext?.destination) {
|
|
119
|
-
const name = basename(templatePath).replace(/\.(vue|md)$/, "");
|
|
120
|
-
ptOutputPath = join(resolve(sfcPlaintext.destination), `${name}.${ptExtension}`);
|
|
121
|
-
} else if (sfcOutputPath) {
|
|
122
|
-
const parsed = parse(outputFilePath);
|
|
123
|
-
ptOutputPath = join(parsed.dir, `${parsed.name}.${ptExtension}`);
|
|
124
|
-
} else if (globalCfg.destination) ptOutputPath = resolveOutputPath(templatePath, resolve(globalCfg.destination), ptExtension, contentBase);
|
|
125
|
-
else ptOutputPath = resolveOutputPath(templatePath, outputPath, ptExtension, contentBase);
|
|
126
|
-
mkdirSync(dirname(ptOutputPath), { recursive: true });
|
|
127
|
-
writeFileSync(ptOutputPath, plaintext);
|
|
128
|
-
}
|
|
129
|
-
} finally {
|
|
130
|
-
_setCurrentTemplate(void 0);
|
|
131
|
-
events.clearSfcHandlers();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
50
|
+
let droppedAfterBuild = 0;
|
|
51
|
+
const parallel = resolveParallel(config, templateFiles.length, configInput);
|
|
52
|
+
if (parallel.enabled) {
|
|
53
|
+
spinner.text = `Building ${templateFiles.length} templates across ${parallel.workers} workers...`;
|
|
54
|
+
const result = await runParallelBuild({
|
|
55
|
+
templateFiles,
|
|
56
|
+
workers: parallel.workers,
|
|
57
|
+
config,
|
|
58
|
+
configInput,
|
|
59
|
+
outputPath,
|
|
60
|
+
outputExtension,
|
|
61
|
+
contentBase
|
|
62
|
+
});
|
|
63
|
+
outputFiles.push(...result.files);
|
|
64
|
+
droppedAfterBuild = result.sfcAfterBuildCount;
|
|
134
65
|
await copyStatic(config, outputPath);
|
|
135
66
|
await events.fireAfterBuild({
|
|
136
67
|
files: outputFiles,
|
|
137
68
|
config
|
|
138
69
|
});
|
|
139
|
-
}
|
|
140
|
-
await
|
|
70
|
+
} else {
|
|
71
|
+
const renderer = await createRenderer({
|
|
72
|
+
markdown: config.markdown,
|
|
73
|
+
root: config.root,
|
|
74
|
+
componentDirs: normalizeComponentSources(config.components?.source, process.cwd()),
|
|
75
|
+
vite: config.vite
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
for (const templatePath of templateFiles) {
|
|
79
|
+
const { files } = await buildTemplate(templatePath, {
|
|
80
|
+
config,
|
|
81
|
+
renderer,
|
|
82
|
+
events,
|
|
83
|
+
outputPath,
|
|
84
|
+
outputExtension,
|
|
85
|
+
contentBase
|
|
86
|
+
});
|
|
87
|
+
outputFiles.push(...files);
|
|
88
|
+
}
|
|
89
|
+
await copyStatic(config, outputPath);
|
|
90
|
+
await events.fireAfterBuild({
|
|
91
|
+
files: outputFiles,
|
|
92
|
+
config
|
|
93
|
+
});
|
|
94
|
+
} finally {
|
|
95
|
+
await renderer.close();
|
|
96
|
+
}
|
|
141
97
|
}
|
|
98
|
+
if (droppedAfterBuild > 0) console.warn(`[maizzle] Skipped ${droppedAfterBuild} SFC-registered afterBuild handler(s): afterBuild can't run inside a parallel build worker. Move build-completion logic to the config's afterBuild hook.`);
|
|
142
99
|
const duration = ((Date.now() - start) / 1e3).toFixed(2);
|
|
143
100
|
const count = outputFiles.length;
|
|
144
101
|
spinner.stopAndPersist({
|
|
@@ -151,20 +108,100 @@ async function build(configInput) {
|
|
|
151
108
|
};
|
|
152
109
|
}
|
|
153
110
|
/**
|
|
154
|
-
*
|
|
111
|
+
* Default template count above which parallel build turns on. Benchmarked
|
|
112
|
+
* crossover (with the worker cap below) is ~25 templates; 50 leaves margin so
|
|
113
|
+
* auto-parallel only kicks in where it's a reliable win across hardware.
|
|
114
|
+
* Override per project with `parallel: { threshold }`.
|
|
115
|
+
*/
|
|
116
|
+
const DEFAULT_PARALLEL_THRESHOLD = 50;
|
|
117
|
+
/**
|
|
118
|
+
* Default worker cap. Each worker runs a full Vite SSR renderer, so startup +
|
|
119
|
+
* contention outweighs added parallelism past ~8 — benchmarks showed 8 beating
|
|
120
|
+
* 12/16/23 at every size. Override with `parallel: { workers }`.
|
|
121
|
+
*/
|
|
122
|
+
const DEFAULT_MAX_WORKERS = 8;
|
|
123
|
+
/**
|
|
124
|
+
* Decide whether to build in parallel and with how many workers.
|
|
155
125
|
*
|
|
156
|
-
*
|
|
126
|
+
* `config.parallel`:
|
|
127
|
+
* - omitted → parallel when `count > 50`, min(CPU count − 1, 8) workers
|
|
128
|
+
* - `true` → always parallel (ignores threshold), default workers
|
|
129
|
+
* - `false` → always sequential
|
|
130
|
+
* - `{ workers, threshold }` → parallel when `count > threshold` (default 50),
|
|
131
|
+
* using `workers` threads (default min(CPU count − 1, 8))
|
|
157
132
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
133
|
+
* Workers reload the config file to recover function hooks, so parallel only
|
|
134
|
+
* applies to file-based configs (a path or the default cwd config) — an inline
|
|
135
|
+
* config object has no file to reload and always builds sequentially.
|
|
136
|
+
*/
|
|
137
|
+
function resolveParallel(config, count, configInput) {
|
|
138
|
+
const setting = config.parallel;
|
|
139
|
+
if (setting === false) return {
|
|
140
|
+
enabled: false,
|
|
141
|
+
workers: 0
|
|
142
|
+
};
|
|
143
|
+
if (!(typeof configInput === "string" || configInput == null)) return {
|
|
144
|
+
enabled: false,
|
|
145
|
+
workers: 0
|
|
146
|
+
};
|
|
147
|
+
const cpus = availableParallelism();
|
|
148
|
+
let maxWorkers = Math.min(Math.max(1, cpus - 1), DEFAULT_MAX_WORKERS);
|
|
149
|
+
let threshold = DEFAULT_PARALLEL_THRESHOLD;
|
|
150
|
+
const ignoreThreshold = setting === true;
|
|
151
|
+
if (typeof setting === "object" && setting !== null) {
|
|
152
|
+
if (typeof setting.workers === "number" && setting.workers > 0) maxWorkers = Math.floor(setting.workers);
|
|
153
|
+
if (typeof setting.threshold === "number" && setting.threshold >= 0) threshold = Math.floor(setting.threshold);
|
|
154
|
+
}
|
|
155
|
+
if (!ignoreThreshold && count <= threshold) return {
|
|
156
|
+
enabled: false,
|
|
157
|
+
workers: 0
|
|
158
|
+
};
|
|
159
|
+
const workers = Math.min(maxWorkers, count);
|
|
160
|
+
return {
|
|
161
|
+
enabled: workers >= 2 && count >= 2,
|
|
162
|
+
workers
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Run the build across worker threads. Each worker reloads the config (for its
|
|
167
|
+
* function hooks), builds its batch via the same `buildTemplate` as the
|
|
168
|
+
* sequential path, and returns the files it wrote. beforeCreate/afterBuild stay
|
|
169
|
+
* on the main thread (handled by the caller).
|
|
160
170
|
*/
|
|
161
|
-
function
|
|
162
|
-
const
|
|
163
|
-
|
|
171
|
+
async function runParallelBuild(opts) {
|
|
172
|
+
const { templateFiles, workers, config, configInput, outputPath, outputExtension, contentBase } = opts;
|
|
173
|
+
const { default: Tinypool } = await import("tinypool");
|
|
174
|
+
const workerPath = resolve(dirname(fileURLToPath(import.meta.url)), "render/parallel/worker.mjs");
|
|
175
|
+
const configPath = typeof configInput === "string" ? configInput : void 0;
|
|
176
|
+
const configData = JSON.parse(JSON.stringify(config));
|
|
177
|
+
const batches = shardEvenly(templateFiles, workers);
|
|
178
|
+
const pool = new Tinypool({
|
|
179
|
+
filename: workerPath,
|
|
180
|
+
minThreads: batches.length,
|
|
181
|
+
maxThreads: batches.length
|
|
182
|
+
});
|
|
183
|
+
try {
|
|
184
|
+
const results = await Promise.all(batches.map((templatePaths) => pool.run({
|
|
185
|
+
templatePaths,
|
|
186
|
+
configPath,
|
|
187
|
+
configData,
|
|
188
|
+
outputPath,
|
|
189
|
+
outputExtension,
|
|
190
|
+
contentBase
|
|
191
|
+
})));
|
|
192
|
+
return {
|
|
193
|
+
files: results.flatMap((r) => r.files),
|
|
194
|
+
sfcAfterBuildCount: results.reduce((n, r) => n + r.sfcAfterBuildCount, 0)
|
|
195
|
+
};
|
|
196
|
+
} finally {
|
|
197
|
+
await pool.destroy();
|
|
198
|
+
}
|
|
164
199
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
200
|
+
/** Round-robin items into up to `buckets` non-empty groups for even balance. */
|
|
201
|
+
function shardEvenly(items, buckets) {
|
|
202
|
+
const out = Array.from({ length: buckets }, () => []);
|
|
203
|
+
items.forEach((item, i) => out[i % buckets].push(item));
|
|
204
|
+
return out.filter((b) => b.length > 0);
|
|
168
205
|
}
|
|
169
206
|
async function copyStatic(config, outputPath) {
|
|
170
207
|
const sources = config.static?.source ?? ["public/**/*.*"];
|
|
@@ -178,6 +215,6 @@ async function copyStatic(config, outputPath) {
|
|
|
178
215
|
}
|
|
179
216
|
}
|
|
180
217
|
//#endregion
|
|
181
|
-
export { build };
|
|
218
|
+
export { build, resolveParallel };
|
|
182
219
|
|
|
183
220
|
//# sourceMappingURL=build.js.map
|
package/dist/build.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","names":["parsePath"],"sources":["../src/build.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, rmSync } from 'node:fs'\nimport { resolve, dirname, basename, relative, join, parse as parsePath } from 'node:path'\nimport { glob } from 'tinyglobby'\nimport ora from 'ora'\nimport { resolveConfig } from './config/index.ts'\nimport { EventManager } from './events/index.ts'\nimport { runTransformers } from './transformers/index.ts'\nimport { createRenderer } from './render/createRenderer.ts'\nimport { createPlaintext } from './plaintext.ts'\nimport { stripForHtml, stripForPlaintext } from './utils/output-markers.ts'\nimport { normalizeComponentSources } from './utils/componentSources.ts'\nimport { _setCurrentTemplate } from './composables/useCurrentTemplate.ts'\nimport defu from 'defu'\nimport type { MaizzleConfig } from './types/index.ts'\n\nexport interface BuildResult {\n files: string[]\n config: MaizzleConfig\n}\n\n/**\n * Build all SFC email templates to HTML files.\n *\n * Creates a single Renderer instance, then loops through each template\n * calling render → transformers → write to disk.\n *\n * Pass a `Partial<MaizzleConfig>` to override config inline, or a string\n * to load config from a specific file path. Omit to load `maizzle.config`\n * from the working directory.\n */\nexport async function build(configInput?: Partial<MaizzleConfig> | string): Promise<BuildResult> {\n const start = Date.now()\n const spinner = ora({ text: 'Building templates...', spinner: 'circleHalves' }).start()\n\n const config = await resolveConfig(configInput)\n\n const events = new EventManager()\n events.registerConfig(config)\n await events.fireBeforeCreate({ config })\n\n const outputPath = resolve(config.output?.path ?? 'dist')\n const outputExtension = config.output?.extension ?? 'html'\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const contentBase = computeContentBase(contentPatterns)\n const templateFiles = await glob(contentPatterns)\n\n if (templateFiles.length === 0) {\n spinner.succeed('No templates found')\n return { files: [], config }\n }\n\n // Clear the output directory before writing fresh output\n if (existsSync(outputPath)) {\n rmSync(outputPath, { recursive: true, force: true })\n }\n\n const renderer = await createRenderer({ markdown: config.markdown, root: config.root, componentDirs: normalizeComponentSources(config.components?.source, process.cwd()), vite: config.vite })\n const outputFiles: string[] = []\n\n try {\n for (const templatePath of templateFiles) {\n const absolutePath = resolve(templatePath)\n const parsedPath = parsePath(absolutePath)\n const template = { source: readFileSync(absolutePath, 'utf-8'), path: parsedPath }\n\n _setCurrentTemplate(parsedPath)\n\n try {\n await events.fireBeforeRender({ config, template })\n\n const rendered = await renderer.render(absolutePath, config)\n\n /**\n * Register SFC event handlers collected during render so they take\n * part in the post-render events (afterRender / afterTransform).\n * They're cleared at the end of the iteration so they don't\n * leak into the next template.\n */\n for (const { name, handler } of rendered.sfcEventHandlers) {\n events.on(name, handler)\n }\n\n let html = await events.fireAfterRender({ config, template, html: rendered.html })\n\n /**\n * Use the per-template merged config (from defineConfig() in the SFC) so\n * that template-level overrides like css.safe: false are respected\n * by transformers.\n */\n const templateConfig = rendered.templateConfig\n\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n\n if (templateConfig.useTransformers !== false) {\n html = await runTransformers(html, templateConfig, absolutePath, doctype, rendered.tailwindBlocks)\n }\n\n html = await events.fireAfterTransform({ config, template, html })\n if (doctype) html = `${doctype}\\n${html}`\n\n const htmlOut = stripForHtml(html)\n const sfcOutputPath = rendered.outputPath\n let outputFilePath: string\n\n if (sfcOutputPath) {\n const parsed = parsePath(resolve(sfcOutputPath))\n const ext = parsed.ext ? parsed.ext.slice(1) : outputExtension\n outputFilePath = join(parsed.dir, `${parsed.name}.${ext}`)\n } else {\n outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase)\n }\n\n mkdirSync(dirname(outputFilePath), { recursive: true })\n writeFileSync(outputFilePath, htmlOut)\n outputFiles.push(outputFilePath)\n\n // Generate plaintext version if configured\n const globalPlaintext = templateConfig.plaintext\n const sfcPlaintext = rendered.plaintext\n\n if (globalPlaintext || sfcPlaintext) {\n const globalCfg = typeof globalPlaintext === 'object' ? globalPlaintext : {}\n const stripOptions = defu(sfcPlaintext?.options, globalCfg.options)\n const plaintext = createPlaintext(stripForPlaintext(html), stripOptions)\n const ptExtension = sfcPlaintext?.extension ?? globalCfg.extension ?? 'txt'\n\n let ptOutputPath: string\n\n if (sfcPlaintext?.destination) {\n const name = basename(templatePath).replace(/\\.(vue|md)$/, '')\n ptOutputPath = join(resolve(sfcPlaintext.destination), `${name}.${ptExtension}`)\n } else if (sfcOutputPath) {\n const parsed = parsePath(outputFilePath)\n ptOutputPath = join(parsed.dir, `${parsed.name}.${ptExtension}`)\n } else if (globalCfg.destination) {\n ptOutputPath = resolveOutputPath(templatePath, resolve(globalCfg.destination), ptExtension, contentBase)\n } else {\n ptOutputPath = resolveOutputPath(templatePath, outputPath, ptExtension, contentBase)\n }\n\n mkdirSync(dirname(ptOutputPath), { recursive: true })\n writeFileSync(ptOutputPath, plaintext)\n }\n } finally {\n _setCurrentTemplate(undefined)\n events.clearSfcHandlers()\n }\n }\n\n await copyStatic(config, outputPath)\n await events.fireAfterBuild({ files: outputFiles, config })\n } finally {\n await renderer.close()\n }\n\n const duration = ((Date.now() - start) / 1000).toFixed(2)\n const count = outputFiles.length\n spinner.stopAndPersist({\n symbol: '✅',\n text: `Built ${count} template${count !== 1 ? 's' : ''} in ${duration}s`,\n })\n\n return { files: outputFiles, config }\n}\n\n/**\n * Extract the static (non-glob) prefix from content patterns.\n *\n * For example, `['/abs/path/emails/**\\/*.vue']` → `'/abs/path/emails'`\n *\n * This is used to strip the content base from template paths\n * so the output preserves only the subdirectory structure.\n */\nfunction computeContentBase(patterns: string[]): string {\n // Use the first non-negated pattern\n const pattern = patterns.find(p => !p.startsWith('!')) ?? patterns[0]\n\n // Split on first glob character (* { ? [) and take the directory part\n const staticPart = pattern.split(/[*{?[]/)[0]\n\n // Ensure we have a clean directory path (not a partial segment)\n return resolve(staticPart.endsWith('/') ? staticPart : dirname(staticPart))\n}\n\nfunction resolveOutputPath(templatePath: string, outputDir: string, extension: string, contentBase: string): string {\n const name = basename(templatePath).replace(/\\.(vue|md)$/, '')\n const absTemplate = resolve(templatePath)\n const rel = relative(contentBase, dirname(absTemplate))\n\n return join(outputDir, rel, `${name}.${extension}`)\n}\n\nasync function copyStatic(config: MaizzleConfig, outputPath: string): Promise<void> {\n const sources = config.static?.source ?? ['public/**/*.*']\n const destination = config.static?.destination ?? 'public'\n\n const files = await glob(sources)\n\n for (const file of files) {\n const destPath = join(outputPath, destination, relative(dirname(sources[0]).replace(/\\*.*$/, ''), file))\n const destDir = dirname(destPath)\n\n if (!existsSync(destDir)) {\n mkdirSync(destDir, { recursive: true })\n }\n\n cpSync(file, destPath)\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"build.js","names":[],"sources":["../src/build.ts"],"sourcesContent":["import { mkdirSync, cpSync, existsSync, rmSync } from 'node:fs'\nimport { resolve, dirname, relative, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { availableParallelism } from 'node:os'\nimport { glob } from 'tinyglobby'\nimport ora from 'ora'\nimport { resolveConfig } from './config/index.ts'\nimport { EventManager } from './events/index.ts'\nimport { createRenderer } from './render/createRenderer.ts'\nimport { normalizeComponentSources } from './utils/componentSources.ts'\nimport { buildTemplate, computeContentBase } from './render/buildTemplate.ts'\nimport type { MaizzleConfig } from './types/index.ts'\n\nexport interface BuildResult {\n files: string[]\n config: MaizzleConfig\n}\n\n/**\n * Build all SFC email templates to HTML files.\n *\n * Creates a single Renderer instance, then loops through each template\n * calling render → transformers → write to disk.\n *\n * Pass a `Partial<MaizzleConfig>` to override config inline, or a string\n * to load config from a specific file path. Omit to load `maizzle.config`\n * from the working directory.\n */\nexport async function build(configInput?: Partial<MaizzleConfig> | string): Promise<BuildResult> {\n const start = Date.now()\n const spinner = ora({ text: 'Building templates...', spinner: 'circleHalves' }).start()\n\n const config = await resolveConfig(configInput)\n\n const events = new EventManager()\n events.registerConfig(config)\n await events.fireBeforeCreate({ config })\n\n const outputPath = resolve(config.output?.path ?? 'dist')\n const outputExtension = config.output?.extension ?? 'html'\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const contentBase = computeContentBase(contentPatterns)\n const templateFiles = await glob(contentPatterns)\n\n if (templateFiles.length === 0) {\n spinner.succeed('No templates found')\n return { files: [], config }\n }\n\n // Clear the output directory before writing fresh output\n if (existsSync(outputPath)) {\n rmSync(outputPath, { recursive: true, force: true })\n }\n\n const outputFiles: string[] = []\n let droppedAfterBuild = 0\n\n const parallel = resolveParallel(config, templateFiles.length, configInput)\n\n if (parallel.enabled) {\n spinner.text = `Building ${templateFiles.length} templates across ${parallel.workers} workers...`\n\n const result = await runParallelBuild({\n templateFiles,\n workers: parallel.workers,\n config,\n configInput,\n outputPath,\n outputExtension,\n contentBase,\n })\n\n outputFiles.push(...result.files)\n droppedAfterBuild = result.sfcAfterBuildCount\n\n await copyStatic(config, outputPath)\n await events.fireAfterBuild({ files: outputFiles, config })\n } else {\n const renderer = await createRenderer({ markdown: config.markdown, root: config.root, componentDirs: normalizeComponentSources(config.components?.source, process.cwd()), vite: config.vite })\n\n try {\n for (const templatePath of templateFiles) {\n const { files } = await buildTemplate(templatePath, { config, renderer, events, outputPath, outputExtension, contentBase })\n outputFiles.push(...files)\n }\n\n await copyStatic(config, outputPath)\n await events.fireAfterBuild({ files: outputFiles, config })\n } finally {\n await renderer.close()\n }\n }\n\n if (droppedAfterBuild > 0) {\n console.warn(`[maizzle] Skipped ${droppedAfterBuild} SFC-registered afterBuild handler(s): afterBuild can't run inside a parallel build worker. Move build-completion logic to the config's afterBuild hook.`)\n }\n\n const duration = ((Date.now() - start) / 1000).toFixed(2)\n const count = outputFiles.length\n spinner.stopAndPersist({\n symbol: '✅',\n text: `Built ${count} template${count !== 1 ? 's' : ''} in ${duration}s`,\n })\n\n return { files: outputFiles, config }\n}\n\n/**\n * Default template count above which parallel build turns on. Benchmarked\n * crossover (with the worker cap below) is ~25 templates; 50 leaves margin so\n * auto-parallel only kicks in where it's a reliable win across hardware.\n * Override per project with `parallel: { threshold }`.\n */\nconst DEFAULT_PARALLEL_THRESHOLD = 50\n\n/**\n * Default worker cap. Each worker runs a full Vite SSR renderer, so startup +\n * contention outweighs added parallelism past ~8 — benchmarks showed 8 beating\n * 12/16/23 at every size. Override with `parallel: { workers }`.\n */\nconst DEFAULT_MAX_WORKERS = 8\n\n/**\n * Decide whether to build in parallel and with how many workers.\n *\n * `config.parallel`:\n * - omitted → parallel when `count > 50`, min(CPU count − 1, 8) workers\n * - `true` → always parallel (ignores threshold), default workers\n * - `false` → always sequential\n * - `{ workers, threshold }` → parallel when `count > threshold` (default 50),\n * using `workers` threads (default min(CPU count − 1, 8))\n *\n * Workers reload the config file to recover function hooks, so parallel only\n * applies to file-based configs (a path or the default cwd config) — an inline\n * config object has no file to reload and always builds sequentially.\n */\nexport function resolveParallel(\n config: MaizzleConfig,\n count: number,\n configInput: Partial<MaizzleConfig> | string | undefined,\n): { enabled: boolean; workers: number } {\n const setting = config.parallel\n if (setting === false) return { enabled: false, workers: 0 }\n\n const fileBased = typeof configInput === 'string' || configInput == null\n if (!fileBased) return { enabled: false, workers: 0 }\n\n const cpus = availableParallelism()\n const defaultWorkers = Math.min(Math.max(1, cpus - 1), DEFAULT_MAX_WORKERS)\n\n let maxWorkers = defaultWorkers\n let threshold = DEFAULT_PARALLEL_THRESHOLD\n // `true` opts in regardless of count; object/omitted stay threshold-gated.\n const ignoreThreshold = setting === true\n\n if (typeof setting === 'object' && setting !== null) {\n if (typeof setting.workers === 'number' && setting.workers > 0) maxWorkers = Math.floor(setting.workers)\n if (typeof setting.threshold === 'number' && setting.threshold >= 0) threshold = Math.floor(setting.threshold)\n }\n\n if (!ignoreThreshold && count <= threshold) return { enabled: false, workers: 0 }\n\n const workers = Math.min(maxWorkers, count)\n return { enabled: workers >= 2 && count >= 2, workers }\n}\n\n/**\n * Run the build across worker threads. Each worker reloads the config (for its\n * function hooks), builds its batch via the same `buildTemplate` as the\n * sequential path, and returns the files it wrote. beforeCreate/afterBuild stay\n * on the main thread (handled by the caller).\n */\nasync function runParallelBuild(opts: {\n templateFiles: string[]\n workers: number\n config: MaizzleConfig\n configInput: Partial<MaizzleConfig> | string | undefined\n outputPath: string\n outputExtension: string\n contentBase: string\n}): Promise<{ files: string[]; sfcAfterBuildCount: number }> {\n const { templateFiles, workers, config, configInput, outputPath, outputExtension, contentBase } = opts\n\n const { default: Tinypool } = await import('tinypool')\n const workerPath = resolve(dirname(fileURLToPath(import.meta.url)), 'render/parallel/worker.mjs')\n\n const configPath = typeof configInput === 'string' ? configInput : undefined\n // Serializable snapshot of the post-beforeCreate config (functions dropped).\n const configData = JSON.parse(JSON.stringify(config)) as Partial<MaizzleConfig>\n\n const batches = shardEvenly(templateFiles, workers)\n\n const pool = new Tinypool({ filename: workerPath, minThreads: batches.length, maxThreads: batches.length })\n\n try {\n const results = await Promise.all(\n batches.map(templatePaths => pool.run({\n templatePaths,\n configPath,\n configData,\n outputPath,\n outputExtension,\n contentBase,\n })),\n )\n\n return {\n files: results.flatMap(r => r.files),\n sfcAfterBuildCount: results.reduce((n, r) => n + r.sfcAfterBuildCount, 0),\n }\n } finally {\n await pool.destroy()\n }\n}\n\n/** Round-robin items into up to `buckets` non-empty groups for even balance. */\nfunction shardEvenly<T>(items: T[], buckets: number): T[][] {\n const out: T[][] = Array.from({ length: buckets }, () => [])\n items.forEach((item, i) => out[i % buckets].push(item))\n return out.filter(b => b.length > 0)\n}\n\nasync function copyStatic(config: MaizzleConfig, outputPath: string): Promise<void> {\n const sources = config.static?.source ?? ['public/**/*.*']\n const destination = config.static?.destination ?? 'public'\n\n const files = await glob(sources)\n\n for (const file of files) {\n const destPath = join(outputPath, destination, relative(dirname(sources[0]).replace(/\\*.*$/, ''), file))\n const destDir = dirname(destPath)\n\n if (!existsSync(destDir)) {\n mkdirSync(destDir, { recursive: true })\n }\n\n cpSync(file, destPath)\n }\n}\n"],"mappings":"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"}
|
package/dist/config/index.js
CHANGED
|
@@ -2,8 +2,8 @@ import { defaults } from "./defaults.js";
|
|
|
2
2
|
import { defineConfig } from "../composables/defineConfig.js";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
|
-
import { createJiti } from "jiti";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createJiti } from "jiti";
|
|
7
7
|
import { createDefu } from "defu";
|
|
8
8
|
//#region src/config/index.ts
|
|
9
9
|
const merge = createDefu((obj, key, value) => {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useConfig } from "./composables/useConfig.js";
|
|
2
2
|
import { defineConfig } from "./composables/defineConfig.js";
|
|
3
3
|
import { resolveConfig } from "./config/index.js";
|
|
4
|
+
import { normalizeComponentSources } from "./utils/componentSources.js";
|
|
5
|
+
import { createRenderer } from "./render/createRenderer.js";
|
|
4
6
|
import { inlineLink } from "./transformers/inlineLink.js";
|
|
5
7
|
import { safeSelectors } from "./transformers/safeSelectors.js";
|
|
6
8
|
import { attributeToStyle } from "./transformers/attributeToStyle.js";
|
|
@@ -17,8 +19,6 @@ import { purgeCss } from "./transformers/purgeCss.js";
|
|
|
17
19
|
import { replaceStrings } from "./transformers/replaceStrings.js";
|
|
18
20
|
import { format } from "./transformers/format.js";
|
|
19
21
|
import { minify } from "./transformers/minify.js";
|
|
20
|
-
import { normalizeComponentSources } from "./utils/componentSources.js";
|
|
21
|
-
import { createRenderer } from "./render/createRenderer.js";
|
|
22
22
|
import { createPlaintext } from "./plaintext.js";
|
|
23
23
|
import { useCurrentTemplate } from "./composables/useCurrentTemplate.js";
|
|
24
24
|
import { build } from "./build.js";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EventManager } from "../events/index.js";
|
|
2
|
+
import { MaizzleConfig } from "../types/config.js";
|
|
3
|
+
import { Renderer } from "./createRenderer.js";
|
|
4
|
+
|
|
5
|
+
//#region src/render/buildTemplate.d.ts
|
|
6
|
+
interface BuildTemplateContext {
|
|
7
|
+
config: MaizzleConfig;
|
|
8
|
+
renderer: Renderer;
|
|
9
|
+
events: EventManager;
|
|
10
|
+
outputPath: string;
|
|
11
|
+
outputExtension: string;
|
|
12
|
+
contentBase: string;
|
|
13
|
+
}
|
|
14
|
+
interface BuildTemplateResult {
|
|
15
|
+
/** Output files written for this template (html + optional plaintext). */
|
|
16
|
+
files: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Number of SFC-registered `afterBuild` handlers seen while rendering. They
|
|
19
|
+
* only fire once at end of build on the main thread, so a worker can't run
|
|
20
|
+
* them — the count lets the orchestrator warn instead of silently dropping.
|
|
21
|
+
*/
|
|
22
|
+
sfcAfterBuildCount: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Render a single template through the full pipeline and write its output.
|
|
26
|
+
*
|
|
27
|
+
* Shared by the sequential build loop and the parallel build worker so both
|
|
28
|
+
* paths produce byte-identical output. `events` is the manager the per-template
|
|
29
|
+
* events fire on (config handlers registered via `registerConfig`, SFC handlers
|
|
30
|
+
* registered here from the render). The caller owns build-scoped events
|
|
31
|
+
* (`beforeCreate`/`afterBuild`).
|
|
32
|
+
*/
|
|
33
|
+
declare function buildTemplate(templatePath: string, ctx: BuildTemplateContext): Promise<BuildTemplateResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Extract the static (non-glob) prefix from content patterns.
|
|
36
|
+
*
|
|
37
|
+
* For example, `['/abs/path/emails/**\/*.vue']` → `'/abs/path/emails'`
|
|
38
|
+
*
|
|
39
|
+
* Used to strip the content base from template paths so the output preserves
|
|
40
|
+
* only the subdirectory structure.
|
|
41
|
+
*
|
|
42
|
+
* With multiple positive patterns (multi-root setups), returns their common
|
|
43
|
+
* ancestor directory so templates from every root keep a clean relative path.
|
|
44
|
+
*/
|
|
45
|
+
declare function computeContentBase(patterns: string[]): string;
|
|
46
|
+
declare function resolveOutputPath(templatePath: string, outputDir: string, extension: string, contentBase: string): string;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { BuildTemplateContext, BuildTemplateResult, buildTemplate, computeContentBase, resolveOutputPath };
|
|
49
|
+
//# sourceMappingURL=buildTemplate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildTemplate.d.ts","names":[],"sources":["../../src/render/buildTemplate.ts"],"mappings":";;;;;UAWiB,oBAAA;EACf,MAAA,EAAQ,aAAA;EACR,QAAA,EAAU,QAAA;EACV,MAAA,EAAQ,YAAA;EACR,UAAA;EACA,eAAA;EACA,WAAA;AAAA;AAAA,UAGe,mBAAA;EANK;EAQpB,KAAA;EAVA;;;;;EAgBA,kBAAkB;AAAA;;;;AAXP;AAGb;;;;AAQoB;iBAYE,aAAA,CACpB,YAAA,UACA,GAAA,EAAK,oBAAA,GACJ,OAAA,CAAQ,mBAAA;;;;;;;;;;;;iBAgHK,kBAAA,CAAmB,QAAkB;AAAA,iBA4BrC,iBAAA,CAAkB,YAAA,UAAsB,SAAA,UAAmB,SAAA,UAAmB,WAAA"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { runTransformers } from "../transformers/index.js";
|
|
2
|
+
import { createPlaintext } from "../plaintext.js";
|
|
3
|
+
import { stripForHtml, stripForPlaintext } from "../utils/output-markers.js";
|
|
4
|
+
import { _setCurrentTemplate } from "../composables/useCurrentTemplate.js";
|
|
5
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { basename, dirname, join, parse, relative, resolve, sep } from "node:path";
|
|
7
|
+
import defu from "defu";
|
|
8
|
+
//#region src/render/buildTemplate.ts
|
|
9
|
+
/**
|
|
10
|
+
* Render a single template through the full pipeline and write its output.
|
|
11
|
+
*
|
|
12
|
+
* Shared by the sequential build loop and the parallel build worker so both
|
|
13
|
+
* paths produce byte-identical output. `events` is the manager the per-template
|
|
14
|
+
* events fire on (config handlers registered via `registerConfig`, SFC handlers
|
|
15
|
+
* registered here from the render). The caller owns build-scoped events
|
|
16
|
+
* (`beforeCreate`/`afterBuild`).
|
|
17
|
+
*/
|
|
18
|
+
async function buildTemplate(templatePath, ctx) {
|
|
19
|
+
const { config, renderer, events, outputPath, outputExtension, contentBase } = ctx;
|
|
20
|
+
const absolutePath = resolve(templatePath);
|
|
21
|
+
const parsedPath = parse(absolutePath);
|
|
22
|
+
const template = {
|
|
23
|
+
source: readFileSync(absolutePath, "utf-8"),
|
|
24
|
+
path: parsedPath
|
|
25
|
+
};
|
|
26
|
+
const files = [];
|
|
27
|
+
let sfcAfterBuildCount = 0;
|
|
28
|
+
_setCurrentTemplate(parsedPath);
|
|
29
|
+
try {
|
|
30
|
+
/**
|
|
31
|
+
* Clone config per template so beforeRender mutations (setting a
|
|
32
|
+
* preheader, injecting fetched data, etc.) stay scoped to this template
|
|
33
|
+
* instead of leaking into later ones through the shared config object.
|
|
34
|
+
*/
|
|
35
|
+
const renderConfig = defu({}, config);
|
|
36
|
+
const originalSource = template.source;
|
|
37
|
+
await events.fireBeforeRender({
|
|
38
|
+
config: renderConfig,
|
|
39
|
+
template
|
|
40
|
+
});
|
|
41
|
+
const rendered = await renderer.render(absolutePath, renderConfig, template.source !== originalSource ? { source: template.source } : void 0);
|
|
42
|
+
/**
|
|
43
|
+
* Register SFC event handlers collected during render so they take part in
|
|
44
|
+
* the post-render events. Cleared at the end of this call so they don't
|
|
45
|
+
* leak into the next template (afterBuild is the exception — it's never
|
|
46
|
+
* cleared by clearSfcHandlers; see the count above).
|
|
47
|
+
*/
|
|
48
|
+
for (const { name, handler } of rendered.sfcEventHandlers) {
|
|
49
|
+
if (name === "afterBuild") sfcAfterBuildCount++;
|
|
50
|
+
events.on(name, handler);
|
|
51
|
+
}
|
|
52
|
+
const templateConfig = rendered.templateConfig;
|
|
53
|
+
let html = await events.fireAfterRender({
|
|
54
|
+
config: templateConfig,
|
|
55
|
+
template,
|
|
56
|
+
html: rendered.html
|
|
57
|
+
});
|
|
58
|
+
const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>";
|
|
59
|
+
if (templateConfig.useTransformers !== false) html = await runTransformers(html, templateConfig, absolutePath, doctype, rendered.tailwindBlocks);
|
|
60
|
+
html = await events.fireAfterTransform({
|
|
61
|
+
config: templateConfig,
|
|
62
|
+
template,
|
|
63
|
+
html
|
|
64
|
+
});
|
|
65
|
+
if (doctype) html = `${doctype}\n${html}`;
|
|
66
|
+
const htmlOut = stripForHtml(html);
|
|
67
|
+
const sfcOutputPath = rendered.outputPath;
|
|
68
|
+
let outputFilePath;
|
|
69
|
+
if (sfcOutputPath) {
|
|
70
|
+
const parsed = parse(resolve(sfcOutputPath));
|
|
71
|
+
const ext = parsed.ext ? parsed.ext.slice(1) : outputExtension;
|
|
72
|
+
outputFilePath = join(parsed.dir, `${parsed.name}.${ext}`);
|
|
73
|
+
} else outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase);
|
|
74
|
+
mkdirSync(dirname(outputFilePath), { recursive: true });
|
|
75
|
+
writeFileSync(outputFilePath, htmlOut);
|
|
76
|
+
files.push(outputFilePath);
|
|
77
|
+
const globalPlaintext = templateConfig.plaintext;
|
|
78
|
+
const sfcPlaintext = rendered.plaintext;
|
|
79
|
+
if (globalPlaintext || sfcPlaintext) {
|
|
80
|
+
const globalCfg = typeof globalPlaintext === "object" ? globalPlaintext : {};
|
|
81
|
+
const stripOptions = defu(sfcPlaintext?.options, globalCfg.options);
|
|
82
|
+
const plaintext = createPlaintext(stripForPlaintext(html), stripOptions);
|
|
83
|
+
const ptExtension = sfcPlaintext?.extension ?? globalCfg.extension ?? "txt";
|
|
84
|
+
let ptOutputPath;
|
|
85
|
+
if (sfcPlaintext?.destination) ptOutputPath = resolveOutputPath(templatePath, resolve(sfcPlaintext.destination), ptExtension, contentBase);
|
|
86
|
+
else if (sfcOutputPath) {
|
|
87
|
+
const parsed = parse(outputFilePath);
|
|
88
|
+
ptOutputPath = join(parsed.dir, `${parsed.name}.${ptExtension}`);
|
|
89
|
+
} else if (globalCfg.destination) ptOutputPath = resolveOutputPath(templatePath, resolve(globalCfg.destination), ptExtension, contentBase);
|
|
90
|
+
else ptOutputPath = resolveOutputPath(templatePath, outputPath, ptExtension, contentBase);
|
|
91
|
+
mkdirSync(dirname(ptOutputPath), { recursive: true });
|
|
92
|
+
writeFileSync(ptOutputPath, plaintext);
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
_setCurrentTemplate(void 0);
|
|
96
|
+
events.clearSfcHandlers();
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
files,
|
|
100
|
+
sfcAfterBuildCount
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Extract the static (non-glob) prefix from content patterns.
|
|
105
|
+
*
|
|
106
|
+
* For example, `['/abs/path/emails/**\/*.vue']` → `'/abs/path/emails'`
|
|
107
|
+
*
|
|
108
|
+
* Used to strip the content base from template paths so the output preserves
|
|
109
|
+
* only the subdirectory structure.
|
|
110
|
+
*
|
|
111
|
+
* With multiple positive patterns (multi-root setups), returns their common
|
|
112
|
+
* ancestor directory so templates from every root keep a clean relative path.
|
|
113
|
+
*/
|
|
114
|
+
function computeContentBase(patterns) {
|
|
115
|
+
const positives = patterns.filter((p) => !p.startsWith("!"));
|
|
116
|
+
return (positives.length > 0 ? positives : patterns).map((pattern) => {
|
|
117
|
+
const staticPart = pattern.split(/[*{?[]/)[0];
|
|
118
|
+
return resolve(staticPart.endsWith("/") ? staticPart : dirname(staticPart));
|
|
119
|
+
}).reduce(commonPath);
|
|
120
|
+
}
|
|
121
|
+
/** Longest common directory path shared by two absolute paths. */
|
|
122
|
+
function commonPath(a, b) {
|
|
123
|
+
const aSegments = a.split(sep);
|
|
124
|
+
const bSegments = b.split(sep);
|
|
125
|
+
const shared = [];
|
|
126
|
+
for (let i = 0; i < Math.min(aSegments.length, bSegments.length); i++) {
|
|
127
|
+
if (aSegments[i] !== bSegments[i]) break;
|
|
128
|
+
shared.push(aSegments[i]);
|
|
129
|
+
}
|
|
130
|
+
return shared.join(sep) || sep;
|
|
131
|
+
}
|
|
132
|
+
function resolveOutputPath(templatePath, outputDir, extension, contentBase) {
|
|
133
|
+
const name = basename(templatePath).replace(/\.(vue|md)$/, "");
|
|
134
|
+
return join(outputDir, relative(contentBase, dirname(resolve(templatePath))), `${name}.${extension}`);
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { buildTemplate, computeContentBase, resolveOutputPath };
|
|
138
|
+
|
|
139
|
+
//# sourceMappingURL=buildTemplate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildTemplate.js","names":["parsePath"],"sources":["../../src/render/buildTemplate.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { resolve, dirname, basename, relative, join, sep, parse as parsePath } from 'node:path'\nimport defu from 'defu'\nimport { runTransformers } from '../transformers/index.ts'\nimport { createPlaintext } from '../plaintext.ts'\nimport { stripForHtml, stripForPlaintext } from '../utils/output-markers.ts'\nimport { _setCurrentTemplate } from '../composables/useCurrentTemplate.ts'\nimport type { EventManager } from '../events/index.ts'\nimport type { Renderer } from './createRenderer.ts'\nimport type { MaizzleConfig } from '../types/index.ts'\n\nexport interface BuildTemplateContext {\n config: MaizzleConfig\n renderer: Renderer\n events: EventManager\n outputPath: string\n outputExtension: string\n contentBase: string\n}\n\nexport interface BuildTemplateResult {\n /** Output files written for this template (html + optional plaintext). */\n files: string[]\n /**\n * Number of SFC-registered `afterBuild` handlers seen while rendering. They\n * only fire once at end of build on the main thread, so a worker can't run\n * them — the count lets the orchestrator warn instead of silently dropping.\n */\n sfcAfterBuildCount: number\n}\n\n/**\n * Render a single template through the full pipeline and write its output.\n *\n * Shared by the sequential build loop and the parallel build worker so both\n * paths produce byte-identical output. `events` is the manager the per-template\n * events fire on (config handlers registered via `registerConfig`, SFC handlers\n * registered here from the render). The caller owns build-scoped events\n * (`beforeCreate`/`afterBuild`).\n */\nexport async function buildTemplate(\n templatePath: string,\n ctx: BuildTemplateContext,\n): Promise<BuildTemplateResult> {\n const { config, renderer, events, outputPath, outputExtension, contentBase } = ctx\n const absolutePath = resolve(templatePath)\n const parsedPath = parsePath(absolutePath)\n const template = { source: readFileSync(absolutePath, 'utf-8'), path: parsedPath }\n const files: string[] = []\n let sfcAfterBuildCount = 0\n\n _setCurrentTemplate(parsedPath)\n\n try {\n /**\n * Clone config per template so beforeRender mutations (setting a\n * preheader, injecting fetched data, etc.) stay scoped to this template\n * instead of leaking into later ones through the shared config object.\n */\n const renderConfig = defu({}, config) as MaizzleConfig\n const originalSource = template.source\n\n await events.fireBeforeRender({ config: renderConfig, template })\n\n const rendered = await renderer.render(\n absolutePath,\n renderConfig,\n template.source !== originalSource ? { source: template.source } : undefined,\n )\n\n /**\n * Register SFC event handlers collected during render so they take part in\n * the post-render events. Cleared at the end of this call so they don't\n * leak into the next template (afterBuild is the exception — it's never\n * cleared by clearSfcHandlers; see the count above).\n */\n for (const { name, handler } of rendered.sfcEventHandlers) {\n if (name === 'afterBuild') sfcAfterBuildCount++\n events.on(name, handler)\n }\n\n const templateConfig = rendered.templateConfig\n\n let html = await events.fireAfterRender({ config: templateConfig, template, html: rendered.html })\n\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n\n if (templateConfig.useTransformers !== false) {\n html = await runTransformers(html, templateConfig, absolutePath, doctype, rendered.tailwindBlocks)\n }\n\n html = await events.fireAfterTransform({ config: templateConfig, template, html })\n if (doctype) html = `${doctype}\\n${html}`\n\n const htmlOut = stripForHtml(html)\n const sfcOutputPath = rendered.outputPath\n let outputFilePath: string\n\n if (sfcOutputPath) {\n const parsed = parsePath(resolve(sfcOutputPath))\n const ext = parsed.ext ? parsed.ext.slice(1) : outputExtension\n outputFilePath = join(parsed.dir, `${parsed.name}.${ext}`)\n } else {\n outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase)\n }\n\n mkdirSync(dirname(outputFilePath), { recursive: true })\n writeFileSync(outputFilePath, htmlOut)\n files.push(outputFilePath)\n\n // Generate plaintext version if configured\n const globalPlaintext = templateConfig.plaintext\n const sfcPlaintext = rendered.plaintext\n\n if (globalPlaintext || sfcPlaintext) {\n const globalCfg = typeof globalPlaintext === 'object' ? globalPlaintext : {}\n const stripOptions = defu(sfcPlaintext?.options, globalCfg.options)\n const plaintext = createPlaintext(stripForPlaintext(html), stripOptions)\n const ptExtension = sfcPlaintext?.extension ?? globalCfg.extension ?? 'txt'\n\n let ptOutputPath: string\n\n if (sfcPlaintext?.destination) {\n ptOutputPath = resolveOutputPath(templatePath, resolve(sfcPlaintext.destination), ptExtension, contentBase)\n } else if (sfcOutputPath) {\n const parsed = parsePath(outputFilePath)\n ptOutputPath = join(parsed.dir, `${parsed.name}.${ptExtension}`)\n } else if (globalCfg.destination) {\n ptOutputPath = resolveOutputPath(templatePath, resolve(globalCfg.destination), ptExtension, contentBase)\n } else {\n ptOutputPath = resolveOutputPath(templatePath, outputPath, ptExtension, contentBase)\n }\n\n mkdirSync(dirname(ptOutputPath), { recursive: true })\n writeFileSync(ptOutputPath, plaintext)\n }\n } finally {\n _setCurrentTemplate(undefined)\n events.clearSfcHandlers()\n }\n\n return { files, sfcAfterBuildCount }\n}\n\n/**\n * Extract the static (non-glob) prefix from content patterns.\n *\n * For example, `['/abs/path/emails/**\\/*.vue']` → `'/abs/path/emails'`\n *\n * Used to strip the content base from template paths so the output preserves\n * only the subdirectory structure.\n *\n * With multiple positive patterns (multi-root setups), returns their common\n * ancestor directory so templates from every root keep a clean relative path.\n */\nexport function computeContentBase(patterns: string[]): string {\n const positives = patterns.filter(p => !p.startsWith('!'))\n const sources = positives.length > 0 ? positives : patterns\n\n const bases = sources.map((pattern) => {\n // Split on first glob character (* { ? [) and take the directory part\n const staticPart = pattern.split(/[*{?[]/)[0]\n // Ensure we have a clean directory path (not a partial segment)\n return resolve(staticPart.endsWith('/') ? staticPart : dirname(staticPart))\n })\n\n return bases.reduce(commonPath)\n}\n\n/** Longest common directory path shared by two absolute paths. */\nfunction commonPath(a: string, b: string): string {\n const aSegments = a.split(sep)\n const bSegments = b.split(sep)\n const shared: string[] = []\n\n for (let i = 0; i < Math.min(aSegments.length, bSegments.length); i++) {\n if (aSegments[i] !== bSegments[i]) break\n shared.push(aSegments[i])\n }\n\n return shared.join(sep) || sep\n}\n\nexport function resolveOutputPath(templatePath: string, outputDir: string, extension: string, contentBase: string): string {\n const name = basename(templatePath).replace(/\\.(vue|md)$/, '')\n const absTemplate = resolve(templatePath)\n const rel = relative(contentBase, dirname(absTemplate))\n\n return join(outputDir, rel, `${name}.${extension}`)\n}\n"],"mappings":"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"}
|
|
@@ -15,7 +15,9 @@ interface RenderedTemplate {
|
|
|
15
15
|
tailwindBlocks?: RenderContext['tailwindBlocks'];
|
|
16
16
|
}
|
|
17
17
|
interface Renderer {
|
|
18
|
-
render(input: string | Component, config: MaizzleConfig
|
|
18
|
+
render(input: string | Component, config: MaizzleConfig, opts?: {
|
|
19
|
+
source?: string;
|
|
20
|
+
}): Promise<RenderedTemplate>;
|
|
19
21
|
invalidate(filePath: string): Promise<void>;
|
|
20
22
|
invalidateAll(): Promise<void>;
|
|
21
23
|
close(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createRenderer.d.ts","names":[],"sources":["../../src/render/createRenderer.ts"],"mappings":";;;;;;;UAmCiB,gBAAA;EACf,IAAA;EACA,OAAA;EACA,cAAA,EAAgB,aAAA;EAChB,gBAAA,EAAkB,aAAA;EAClB,SAAA,GAAY,aAAA;EACZ,UAAA,GAAa,aAAA;EACb,cAAA,GAAiB,aAAA;AAAA;AAAA,UAGF,QAAA;EACf,MAAA,CAAO,KAAA,WAAgB,SAAA,EAAW,MAAA,EAAQ,aAAA,
|
|
1
|
+
{"version":3,"file":"createRenderer.d.ts","names":[],"sources":["../../src/render/createRenderer.ts"],"mappings":";;;;;;;UAmCiB,gBAAA;EACf,IAAA;EACA,OAAA;EACA,cAAA,EAAgB,aAAA;EAChB,gBAAA,EAAkB,aAAA;EAClB,SAAA,GAAY,aAAA;EACZ,UAAA,GAAa,aAAA;EACb,cAAA,GAAiB,aAAA;AAAA;AAAA,UAGF,QAAA;EACf,MAAA,CAAO,KAAA,WAAgB,SAAA,EAAW,MAAA,EAAQ,aAAA,EAAe,IAAA;IAAS,MAAA;EAAA,IAAoB,OAAA,CAAQ,gBAAA;EAC9F,UAAA,CAAW,QAAA,WAAmB,OAAA;EAC9B,aAAA,IAAiB,OAAA;EACjB,KAAA,IAAS,OAAA;AAAA;AAAA,UAGM,qBAAA;EAbG;EAelB,GAAA;EAdY;EAgBZ,QAAA,GAAW,cAAA;EAfE;EAiBb,IAAA;EAhBiB;;AAAa;AAGhC;EAkBE,aAAA,GAAgB,yBAAA;;EAEhB,IAAA,GAAO,YAAA;AAAA;;;;;;;iBASa,cAAA,CACpB,OAAA,GAAS,qBAAA,GACR,OAAA,CAAQ,QAAA"}
|