blogger-hbs 0.0.3 → 0.0.5

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 (106) hide show
  1. package/README.md +17 -5
  2. package/dist/cli/index.js +108 -38
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/cli/stats.d.ts +58 -0
  5. package/dist/cli/stats.js +156 -0
  6. package/dist/cli/stats.js.map +1 -0
  7. package/dist/compiler/ast/index.d.ts +2 -2
  8. package/dist/compiler/ast/index.js +2 -2
  9. package/dist/compiler/ast/partials.d.ts +31 -0
  10. package/dist/compiler/ast/partials.js +82 -4
  11. package/dist/compiler/ast/partials.js.map +1 -1
  12. package/dist/compiler/ast/types.d.ts +9 -3
  13. package/dist/compiler/ast/walk.js +2 -0
  14. package/dist/compiler/ast/walk.js.map +1 -1
  15. package/dist/compiler/chunks.d.ts +3 -1
  16. package/dist/compiler/compile.d.ts +27 -11
  17. package/dist/compiler/compile.js +46 -28
  18. package/dist/compiler/compile.js.map +1 -1
  19. package/dist/compiler/dependencies.js +2 -1
  20. package/dist/compiler/dependencies.js.map +1 -1
  21. package/dist/compiler/emit.d.ts +8 -2
  22. package/dist/compiler/emit.js +24 -2
  23. package/dist/compiler/emit.js.map +1 -1
  24. package/dist/compiler/errors.d.ts +8 -1
  25. package/dist/compiler/errors.js +4 -0
  26. package/dist/compiler/errors.js.map +1 -1
  27. package/dist/compiler/index.d.ts +2 -2
  28. package/dist/compiler/options.d.ts +31 -5
  29. package/dist/compiler/options.js +56 -46
  30. package/dist/compiler/options.js.map +1 -1
  31. package/dist/compiler/plugins.d.ts +36 -8
  32. package/dist/compiler/plugins.js +215 -53
  33. package/dist/compiler/plugins.js.map +1 -1
  34. package/dist/compiler/rsbuild.d.ts +2 -2
  35. package/dist/compiler/rsbuild.js +8 -2
  36. package/dist/compiler/rsbuild.js.map +1 -1
  37. package/dist/compiler/runtimes.d.ts +30 -0
  38. package/dist/compiler/runtimes.js +140 -0
  39. package/dist/compiler/runtimes.js.map +1 -0
  40. package/dist/compiler/scan.d.ts +5 -1
  41. package/dist/compiler/scan.js +28 -12
  42. package/dist/compiler/scan.js.map +1 -1
  43. package/dist/compiler/standalone.js +0 -1
  44. package/dist/compiler/standalone.js.map +1 -1
  45. package/dist/compiler/validate.d.ts +3 -2
  46. package/dist/compiler/validate.js +16 -8
  47. package/dist/compiler/validate.js.map +1 -1
  48. package/dist/compiler/watch.d.ts +6 -3
  49. package/dist/compiler/watch.js +35 -16
  50. package/dist/compiler/watch.js.map +1 -1
  51. package/dist/config.d.ts +0 -3
  52. package/dist/config.js +28 -44
  53. package/dist/config.js.map +1 -1
  54. package/dist/constants.d.ts +6 -0
  55. package/dist/constants.js +7 -1
  56. package/dist/constants.js.map +1 -1
  57. package/dist/index.d.ts +8 -7
  58. package/dist/index.js +4 -7
  59. package/dist/plugins/index.d.ts +151 -14
  60. package/dist/plugins/index.js +40 -1
  61. package/dist/plugins/index.js.map +1 -1
  62. package/dist/runtime/chunks.d.ts +8 -0
  63. package/dist/runtime/helpers/args.d.ts +26 -0
  64. package/dist/runtime/helpers/args.js +73 -0
  65. package/dist/runtime/helpers/args.js.map +1 -0
  66. package/dist/runtime/helpers/error.d.ts +10 -4
  67. package/dist/runtime/helpers/error.js +21 -11
  68. package/dist/runtime/helpers/error.js.map +1 -1
  69. package/dist/runtime/helpers/functions.d.ts +99 -9
  70. package/dist/runtime/helpers/functions.js +446 -64
  71. package/dist/runtime/helpers/functions.js.map +1 -1
  72. package/dist/runtime/helpers/index.d.ts +2 -2
  73. package/dist/runtime/helpers/index.js +2 -2
  74. package/dist/runtime/import.d.ts +6 -1
  75. package/dist/runtime/import.js +19 -12
  76. package/dist/runtime/import.js.map +1 -1
  77. package/dist/runtime/index.d.ts +7 -3
  78. package/dist/runtime/index.js +4 -2
  79. package/dist/runtime/render.d.ts +37 -5
  80. package/dist/runtime/render.js +140 -9
  81. package/dist/runtime/render.js.map +1 -1
  82. package/dist/runtime/tags.js +7 -6
  83. package/dist/runtime/tags.js.map +1 -1
  84. package/dist/utils/schema.d.ts +55 -0
  85. package/dist/utils/schema.js +101 -0
  86. package/dist/utils/schema.js.map +1 -0
  87. package/dist/xml/expressions.js +48 -17
  88. package/dist/xml/expressions.js.map +1 -1
  89. package/dist/xml/format.d.ts +23 -16
  90. package/dist/xml/format.js +128 -144
  91. package/dist/xml/format.js.map +1 -1
  92. package/dist/xml/index.d.ts +3 -2
  93. package/dist/xml/index.js +2 -2
  94. package/dist/xml/info.d.ts +30 -0
  95. package/dist/xml/info.js +46 -0
  96. package/dist/xml/info.js.map +1 -0
  97. package/dist/xml/layout.d.ts +39 -0
  98. package/dist/xml/layout.js +245 -0
  99. package/dist/xml/layout.js.map +1 -0
  100. package/dist/xml/process.d.ts +40 -0
  101. package/dist/xml/process.js +176 -0
  102. package/dist/xml/process.js.map +1 -0
  103. package/dist/xml/scanner.d.ts +37 -1
  104. package/dist/xml/scanner.js +255 -97
  105. package/dist/xml/scanner.js.map +1 -1
  106. package/package.json +14 -5
package/README.md CHANGED
@@ -8,7 +8,8 @@ Build Blogger themes from Handlebars templates, with bundled scripts and styles.
8
8
  - Split a theme into templates with `{{import "./header.hbs"}}`, data with `{{import "./data.json"}}`.
9
9
  - Import scripts and styles, bundled with [Rsbuild](https://rsbuild.rs) and inlined: `{{import "./main.ts"}}`.
10
10
  - Every template is validated as XML, and the rendered theme is validated and formatted as Blogger saves it.
11
- - Compile plugins provide partials and data, runtime plugins provide helpers.
11
+ - Plugins provide partials, data and helpers, bundled into the theme.
12
+ - The compiled theme renders in any JavaScript runtime, a Cloudflare Worker too, with `blogger-hbs/runtime`.
12
13
 
13
14
  ## Usage
14
15
 
@@ -18,12 +19,12 @@ pnpm add -D blogger-hbs
18
19
 
19
20
  ```ts
20
21
  // blogger-hbs.config.ts
21
- import { defineConfig } from 'blogger-hbs';
22
+ import { defineConfig } from "blogger-hbs";
22
23
 
23
24
  export default defineConfig({
24
- entry: 'src/index.hbs',
25
- alias: { '~': 'src' },
26
- context: {},
25
+ entry: "src/index.hbs",
26
+ alias: { "~": "src" },
27
+ context: {},
27
28
  });
28
29
  ```
29
30
 
@@ -34,3 +35,14 @@ blogger-hbs --help # all the options
34
35
  ```
35
36
 
36
37
  The same is available from JavaScript with `compileTemplate`, `watchTemplate` and `renderTemplate`.
38
+
39
+ ## Documentation
40
+
41
+ - [Configuration](./docs/configuration.md): the options of the config and of the CLI.
42
+ - [Templates](./docs/templates.md): importing templates, data, scripts and styles.
43
+ - [Helpers](./docs/helpers.md): the built-in helpers and those of a theme.
44
+ - [Plugins](./docs/plugins.md): partials, data, hooks and runtimes of plugins.
45
+ - [Validation and formatting](./docs/validation.md): what is checked, and how the theme is written.
46
+ - [Rendering anywhere](./docs/rendering.md): `blogger-hbs/runtime` and `createRenderer`.
47
+ - [JavaScript API](./docs/api.md): `compileTemplate`, `watchTemplate` and the others.
48
+ - [Errors](./docs/errors.md): what errors say and how to throw them.
package/dist/cli/index.js CHANGED
@@ -1,29 +1,29 @@
1
1
  import { CompileError } from "../compiler/errors.js";
2
+ import { withLocation } from "../plugins/index.js";
2
3
  import { didYouMean } from "../utils/suggest.js";
3
4
  import { COMPILE_OPTION_KEYS, assertCompileOptions, assertDirectories, assertEntry, resolvePaths } from "../compiler/options.js";
4
- import { formatXml, validateXml } from "../xml/format.js";
5
- import "../xml/index.js";
5
+ import { processXml } from "../xml/process.js";
6
6
  import { compileTemplate } from "../compiler/compile.js";
7
7
  import { watchTemplate } from "../compiler/watch.js";
8
- import "../compiler/index.js";
9
8
  import { loadConfig } from "../config.js";
10
- import { renderTemplate } from "../runtime/render.js";
11
- import "../runtime/index.js";
9
+ import { createRenderer } from "../runtime/render.js";
10
+ import { collectStats, formatStats } from "./stats.js";
12
11
  import * as path from "node:path";
13
12
  import * as fs from "node:fs/promises";
14
- import { createRequire } from "node:module";
15
13
  import { watch } from "node:fs";
16
14
  import { styleText } from "node:util";
17
15
  import { cac } from "cac";
18
16
  //#region src/cli/index.ts
19
- const { version } = createRequire(import.meta.url)("../../package.json");
17
+ const { version } = JSON.parse(await fs.readFile(new URL("../../package.json", import.meta.url), "utf-8"));
20
18
  const log = {
21
19
  success: (message) => console.log(`${styleText("green", "✓")} ${message}`),
22
20
  info: (message) => console.log(`${styleText("cyan", "●")} ${message}`),
21
+ warn: (message) => console.warn(`${styleText("yellow", "⚠")} ${message}`),
23
22
  error: (error) => console.error(`${styleText("red", "✗")} ${formatError(error)}`)
24
23
  };
25
24
  const cli = cac("blogger-hbs");
26
25
  cli.command("[root]", "Compile the template and render it").alias("build").option("-c, --config <file>", "Path of the config file").option("--entry <file>", "Entry template, relative to the root").option("-o, --out-dir <dir>", "Output directory, relative to the root").option("--context <file>", "JSON file with the context to render with").option("-w, --watch", "Build again when a file changes").option("--no-clean", "Keep the files in the output directory").option("--no-validate", "Do not validate the rendered template as XML, nor format it (templates are always validated when compiling)").option("--no-format", "Do not format the rendered template").example("blogger-hbs").example("blogger-hbs --watch").example("blogger-hbs ./theme --entry src/index.hbs --context context.json").action(async (rootArg, rawFlags) => {
26
+ printHeader();
27
27
  const text = (value) => value === void 0 ? void 0 : String(value);
28
28
  const root = text(rootArg);
29
29
  const flags = {
@@ -51,7 +51,7 @@ function describeCliError(error) {
51
51
  const flags = [...cli.commands, cli.globalCommand].flatMap((command) => command.options.flatMap((option) => option.rawName.match(/--[\w-]+/g) ?? []));
52
52
  const unknown = /^Unknown option `(.+)`$/.exec(error.message)?.[1];
53
53
  const hint = unknown ? didYouMean(unknown, flags, "`") : "";
54
- return /* @__PURE__ */ new Error(`${error.message}${hint}\n Run 'blogger-hbs --help' to see the available options`);
54
+ return /* @__PURE__ */ new Error(`${error.message}${hint}\nRun 'blogger-hbs --help' to see the available options`);
55
55
  }
56
56
  async function setup(root, flags) {
57
57
  const cwd = process.cwd();
@@ -69,6 +69,7 @@ async function setup(root, flags) {
69
69
  for (const key of COMPILE_OPTION_KEYS) if (key in config) Object.assign(options, { [key]: config[key] });
70
70
  options.root = resolvedRoot;
71
71
  options.entry = flags.entry ?? config.entry;
72
+ options.onWarn ??= log.warn;
72
73
  assertCompileOptions(options);
73
74
  await assertDirectories(options);
74
75
  const outDir = path.resolve(resolvedRoot, flags.outDir ?? output.dir ?? "dist");
@@ -84,7 +85,8 @@ async function setup(root, flags) {
84
85
  format: flags.format === false ? false : output.format ?? true,
85
86
  clean,
86
87
  context: flags.context ? await readContext(path.resolve(cwd, flags.context), flags.context) : config.context ?? {},
87
- publicDir: await resolvePublicDir(resolvedRoot, config.publicDir, outDir)
88
+ publicDir: await resolvePublicDir(resolvedRoot, config.publicDir, outDir),
89
+ sizes: /* @__PURE__ */ new Map()
88
90
  };
89
91
  }
90
92
  /** The public directory, `public` when it exists unless set otherwise. */
@@ -156,7 +158,7 @@ async function build(root, flags) {
156
158
  const start = performance.now();
157
159
  const compiled = await compileTemplate(current.options);
158
160
  log.success(`Compiled in ${elapsed(start)}`);
159
- await write(current, compiled);
161
+ await write(current, compiled, start);
160
162
  }
161
163
  /** Watches the template and restarts when the config file changes. */
162
164
  async function watchBuild(root, flags) {
@@ -168,6 +170,15 @@ async function watchBuild(root, flags) {
168
170
  const start = async () => {
169
171
  let begin = performance.now();
170
172
  let first = true;
173
+ /** Shown before what a rebuild prints, after the first build. */
174
+ let divider = "";
175
+ /** Once, before the first thing a rebuild prints, its output can be the same. */
176
+ const printDivider = () => {
177
+ if (divider) {
178
+ console.log(divider);
179
+ divider = "";
180
+ }
181
+ };
171
182
  try {
172
183
  await copyPublic(current);
173
184
  } catch (error) {
@@ -175,15 +186,20 @@ async function watchBuild(root, flags) {
175
186
  }
176
187
  watcher = await watchTemplate({
177
188
  ...current.options,
178
- onStart() {
189
+ onStart({ files }) {
179
190
  begin = performance.now();
191
+ divider = first ? "" : formatDivider(files);
180
192
  },
181
193
  async onCompile(compiled) {
194
+ printDivider();
182
195
  log.success(`${first ? "Compiled" : "Rebuilt"} in ${elapsed(begin)}`);
196
+ await write(current, compiled, begin);
183
197
  first = false;
184
- await write(current, compiled);
185
198
  },
186
- onError: log.error
199
+ onError(error) {
200
+ printDivider();
201
+ log.error(error);
202
+ }
187
203
  });
188
204
  for (const file of current.loaded.dependencies) try {
189
205
  const configWatcher = watch(file, () => {
@@ -263,49 +279,103 @@ async function prepareOutDir({ outDir, clean }) {
263
279
  * Writes the compiled module and the rendered template, validated and
264
280
  * formatted after rendering when enabled.
265
281
  */
266
- async function write(current, compiled) {
267
- const { outDir, moduleFile, templateFile, context, loaded } = current;
268
- if (moduleFile) await fs.writeFile(path.join(outDir, moduleFile), compiled.source);
269
- if (!templateFile) return;
282
+ async function write(current, compiled, begin) {
283
+ const { outDir, moduleFile, templateFile } = current;
284
+ const written = [];
285
+ let theme;
286
+ if (moduleFile) {
287
+ const file = path.join(outDir, moduleFile);
288
+ await fs.writeFile(file, compiled.source);
289
+ written.push({
290
+ path: path.relative(process.cwd(), file),
291
+ content: compiled.source
292
+ });
293
+ }
294
+ if (templateFile) {
295
+ theme = await writeTheme(current, compiled, templateFile);
296
+ written.unshift(theme.file);
297
+ }
298
+ const stats = await collectStats(compiled, written, theme?.info && {
299
+ ...theme.file,
300
+ info: theme.info
301
+ });
302
+ console.log(formatStats(stats, current.sizes, { previousTheme: current.theme }));
303
+ current.sizes = new Map(stats.files.map(({ path, size }) => [path, size]));
304
+ current.theme = stats.theme;
305
+ log.success(`Built in ${elapsed(begin)}`);
306
+ }
307
+ /** `─── 14:32:05 · src/header.hbs +2 ───────`, dim. */
308
+ function formatDivider(files) {
309
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false });
310
+ const [file, ...others] = files.map((item) => path.relative(process.cwd(), item));
311
+ const text = `─── ${time}${file ? ` · ${file}${others.length > 0 ? ` +${others.length}` : ""}` : ""} `;
312
+ return `\n${styleText("dim", text + "─".repeat(Math.max(3, 50 - text.length)))}`;
313
+ }
314
+ /** Renders, validates and formats the theme, as enabled, and writes it. */
315
+ async function writeTheme(current, compiled, templateFile) {
316
+ const { outDir, context } = current;
270
317
  const file = path.join(outDir, templateFile);
271
318
  const shown = path.relative(process.cwd(), file);
272
319
  let start = performance.now();
273
- const rendered = renderTemplate({
274
- ...compiled,
275
- context,
276
- plugins: loaded.config.runtimePlugins
277
- });
320
+ const rendered = await createRenderer(compiled).render(context);
278
321
  log.success(`Rendered in ${elapsed(start)}`);
322
+ if (!current.validate) {
323
+ await fs.writeFile(file, rendered);
324
+ return { file: {
325
+ path: shown,
326
+ content: rendered
327
+ } };
328
+ }
279
329
  start = performance.now();
280
- let output;
330
+ let result;
281
331
  try {
282
- output = finalizeXml(current, rendered);
332
+ result = processXml(rendered, {
333
+ path: shown,
334
+ format: current.format
335
+ });
283
336
  } catch (error) {
284
337
  await fs.writeFile(file, rendered);
285
- throw new CompileError(`The rendered template ${shown} is not valid XML: ${error.message}`);
338
+ throw new CompileError("The rendered template is not valid XML", {
339
+ cause: error,
340
+ loc: error.loc
341
+ });
286
342
  }
287
- if (current.validate) log.success(`${current.format ? "Formatted" : "Validated"} in ${elapsed(start)}`);
288
- await fs.writeFile(file, output);
289
- log.success(`Written ${styleText("dim", shown)}`);
343
+ for (const { message, loc } of result.warnings) log.warn(withLocation(message, loc));
344
+ log.success(`${current.format ? "Formatted" : "Validated"} in ${elapsed(start)}`);
345
+ await fs.writeFile(file, result.xml);
346
+ return {
347
+ file: {
348
+ path: shown,
349
+ content: result.xml
350
+ },
351
+ info: result.info
352
+ };
290
353
  }
291
- /** Validates and formats the rendered template, as enabled. */
292
- function finalizeXml({ validate, format }, xml) {
293
- if (!validate) return xml;
294
- if (!format) {
295
- validateXml(xml);
296
- return xml;
297
- }
298
- return formatXml(xml);
354
+ /**
355
+ * A blank line, `blogger-hbs v0.0.4` right-aligned on a cyan line under it,
356
+ * before what a build prints.
357
+ */
358
+ function printHeader() {
359
+ const text = `blogger-hbs v${version}`;
360
+ const width = text.length + 2;
361
+ console.log(`\n${" ".repeat(width - text.length)}${styleText("bold", "blogger-hbs")} ${styleText("dim", `v${version}`)}\n${styleText("cyan", "─".repeat(width))}`);
299
362
  }
300
363
  function elapsed(start) {
301
364
  return styleText("yellow", `${Math.round(performance.now() - start)}ms`);
302
365
  }
303
- /** The message of the error and of everything that caused it. */
366
+ /**
367
+ * The message of the error and of everything that caused it, a cause whose
368
+ * message is already in the one before is not repeated. The lines of a
369
+ * message are indented under its first.
370
+ */
304
371
  function formatError(error) {
305
372
  const lines = [];
373
+ let previous = "";
306
374
  let current = error;
307
375
  while (current !== void 0) {
308
- lines.push(current instanceof Error ? `${lines.length === 0 ? "" : "Caused by: "}${current.message}` : String(current));
376
+ const message = current instanceof Error ? current.message : String(current);
377
+ if (lines.length === 0 || !previous.includes(message)) lines.push(`${lines.length === 0 ? "" : "Caused by: "}${message.replaceAll("\n", "\n ")}`);
378
+ previous = message;
309
379
  current = current instanceof Error ? current.cause : void 0;
310
380
  }
311
381
  return lines.join("\n ");
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["import { type FSWatcher, watch } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport * as path from 'node:path';\nimport { styleText } from 'node:util';\nimport { cac } from 'cac';\nimport {\n\ttype CompiledTemplate,\n\tCompileError,\n\ttype CompileTemplateOptions,\n\tcompileTemplate,\n\ttype TemplateWatcher,\n\twatchTemplate,\n} from '../compiler';\nimport {\n\tassertCompileOptions,\n\tassertDirectories,\n\tassertEntry,\n\tCOMPILE_OPTION_KEYS,\n\tresolvePaths,\n} from '../compiler/options';\nimport { type LoadedConfig, loadConfig } from '../config';\nimport { renderTemplate } from '../runtime';\nimport { didYouMean } from '../utils/suggest';\nimport { formatXml, validateXml } from '../xml';\n\nconst { version } = createRequire(import.meta.url)('../../package.json') as {\n\tversion: string;\n};\n\ninterface BuildFlags {\n\tconfig?: string;\n\tentry?: string;\n\toutDir?: string;\n\tcontext?: string;\n\twatch?: boolean;\n\tclean?: boolean;\n\tvalidate?: boolean;\n\tformat?: boolean;\n}\n\n/** Everything a build needs, from the config file and the flags. */\ninterface BuildSetup {\n\toptions: CompileTemplateOptions;\n\tloaded: LoadedConfig;\n\toutDir: string;\n\tmoduleFile: string | false;\n\ttemplateFile: string | false;\n\tclean: boolean;\n\tcontext: unknown;\n\t/** Whether the rendered template is validated. */\n\tvalidate: boolean;\n\t/** Whether the rendered template is formatted, only when it is validated. */\n\tformat: boolean;\n\t/** Absolute path of the public directory, undefined when nothing is copied. */\n\tpublicDir?: string;\n}\n\nconst log = {\n\tsuccess: (message: string) =>\n\t\tconsole.log(`${styleText('green', '✓')} ${message}`),\n\tinfo: (message: string) =>\n\t\tconsole.log(`${styleText('cyan', '●')} ${message}`),\n\terror: (error: unknown) =>\n\t\tconsole.error(`${styleText('red', '✗')} ${formatError(error)}`),\n};\n\nconst cli = cac('blogger-hbs');\n\ncli\n\t.command('[root]', 'Compile the template and render it')\n\t.alias('build')\n\t.option('-c, --config <file>', 'Path of the config file')\n\t.option('--entry <file>', 'Entry template, relative to the root')\n\t.option('-o, --out-dir <dir>', 'Output directory, relative to the root')\n\t.option('--context <file>', 'JSON file with the context to render with')\n\t.option('-w, --watch', 'Build again when a file changes')\n\t.option('--no-clean', 'Keep the files in the output directory')\n\t.option(\n\t\t'--no-validate',\n\t\t'Do not validate the rendered template as XML, nor format it (templates are always validated when compiling)',\n\t)\n\t.option('--no-format', 'Do not format the rendered template')\n\t.example('blogger-hbs')\n\t.example('blogger-hbs --watch')\n\t.example('blogger-hbs ./theme --entry src/index.hbs --context context.json')\n\t.action(async (rootArg: unknown, rawFlags: BuildFlags) => {\n\t\t// cac turns values that look like numbers into numbers, `-o 2024`\n\t\tconst text = (value: unknown) =>\n\t\t\tvalue === undefined ? undefined : String(value);\n\t\tconst root = text(rootArg);\n\t\tconst flags: BuildFlags = {\n\t\t\t...rawFlags,\n\t\t\tconfig: text(rawFlags.config),\n\t\t\tentry: text(rawFlags.entry),\n\t\t\toutDir: text(rawFlags.outDir),\n\t\t\tcontext: text(rawFlags.context),\n\t\t};\n\n\t\tif (flags.watch) {\n\t\t\tawait watchBuild(root, flags);\n\t\t} else {\n\t\t\tawait build(root, flags);\n\t\t}\n\t});\n\ncli.help();\ncli.version(version);\n\ntry {\n\tcli.parse(process.argv, { run: false });\n\tawait cli.runMatchedCommand();\n} catch (error) {\n\tlog.error(describeCliError(error));\n\tprocess.exitCode = 1;\n}\n\n/** Adds a suggestion and a pointer to `--help` to the errors of the arguments. */\nfunction describeCliError(error: unknown): unknown {\n\tif (!(error instanceof Error) || error.name !== 'CACError') {\n\t\treturn error;\n\t}\n\n\tconst flags = [...cli.commands, cli.globalCommand].flatMap((command) =>\n\t\tcommand.options.flatMap(\n\t\t\t(option) => option.rawName.match(/--[\\w-]+/g) ?? [],\n\t\t),\n\t);\n\tconst unknown = /^Unknown option `(.+)`$/.exec(error.message)?.[1];\n\tconst hint = unknown ? didYouMean(unknown, flags, '`') : '';\n\n\treturn new Error(\n\t\t`${error.message}${hint}\\n Run 'blogger-hbs --help' to see the available options`,\n\t);\n}\n\nasync function setup(\n\troot: string | undefined,\n\tflags: BuildFlags,\n): Promise<BuildSetup> {\n\tconst cwd = process.cwd();\n\tconst rootFlag = root === undefined ? undefined : path.resolve(cwd, root);\n\n\t// the config is looked for in the root, which has to exist for that\n\tif (rootFlag !== undefined && !(await isDirectory(rootFlag))) {\n\t\tthrow new CompileError(\n\t\t\t(await exists(rootFlag))\n\t\t\t\t? `Root '${root}' is not a directory`\n\t\t\t\t: `Root directory '${root}' does not exist`,\n\t\t);\n\t}\n\n\tconst loaded = await loadConfig({\n\t\tcwd: rootFlag ?? cwd,\n\t\tconfigFile: flags.config && path.resolve(cwd, flags.config),\n\t\twatch: flags.watch,\n\t});\n\tconst { config } = loaded;\n\n\tconst resolvedRoot = rootFlag ?? config.root ?? cwd;\n\tconst output = config.output ?? {};\n\n\t// the config has more than the compile options\n\tconst options: CompileTemplateOptions = {};\n\tfor (const key of COMPILE_OPTION_KEYS) {\n\t\tif (key in config) {\n\t\t\tObject.assign(options, { [key]: config[key as keyof typeof config] });\n\t\t}\n\t}\n\toptions.root = resolvedRoot;\n\toptions.entry = flags.entry ?? config.entry;\n\n\tassertCompileOptions(options);\n\tawait assertDirectories(options);\n\n\tconst outDir = path.resolve(\n\t\tresolvedRoot,\n\t\tflags.outDir ?? output.dir ?? 'dist',\n\t);\n\tconst clean = flags.clean === false ? false : (output.clean ?? true);\n\tif (clean) {\n\t\tassertCleanable(outDir, options);\n\t}\n\n\treturn {\n\t\toptions,\n\t\tloaded,\n\t\toutDir,\n\t\tmoduleFile: output.module ?? 'compiled.js',\n\t\ttemplateFile: output.template ?? 'theme.xml',\n\t\tvalidate: flags.validate === false ? false : (output.validate ?? true),\n\t\tformat: flags.format === false ? false : (output.format ?? true),\n\t\tclean,\n\t\tcontext: flags.context\n\t\t\t? await readContext(path.resolve(cwd, flags.context), flags.context)\n\t\t\t: (config.context ?? {}),\n\t\tpublicDir: await resolvePublicDir(resolvedRoot, config.publicDir, outDir),\n\t};\n}\n\n/** The public directory, `public` when it exists unless set otherwise. */\nasync function resolvePublicDir(\n\troot: string,\n\tvalue: string | false | undefined,\n\toutDir: string,\n): Promise<string | undefined> {\n\tif (value === false) {\n\t\treturn;\n\t}\n\n\tconst directory = path.resolve(root, value ?? 'public');\n\tconst shown = path.relative(process.cwd(), directory) || '.';\n\n\tif (!(await isDirectory(directory))) {\n\t\tif (value === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tthrow new CompileError(\n\t\t\t(await exists(directory))\n\t\t\t\t? `Public directory '${shown}' is not a directory`\n\t\t\t\t: `Public directory '${shown}' does not exist, set 'publicDir: false' to not copy anything`,\n\t\t);\n\t}\n\n\tif (isInside(outDir, directory) || isInside(directory, outDir)) {\n\t\tthrow new CompileError(\n\t\t\t`Public directory '${shown}' and output directory '${path.relative(process.cwd(), outDir) || '.'}' overlap, they must be separate`,\n\t\t);\n\t}\n\n\treturn directory;\n}\n\n/** Copies the public directory into the output directory. */\nasync function copyPublic({\n\tpublicDir,\n\toutDir,\n\tmoduleFile,\n\ttemplateFile,\n}: BuildSetup): Promise<void> {\n\tif (publicDir === undefined) {\n\t\treturn;\n\t}\n\n\tfor (const name of await fs.readdir(publicDir)) {\n\t\tif (name === moduleFile || name === templateFile) {\n\t\t\tthrow new CompileError(\n\t\t\t\t`'${name}' in the public directory has the same name as the ${name === moduleFile ? 'compiled module' : 'rendered template'} and would be overwritten, rename it or set 'output.${name === moduleFile ? 'module' : 'template'}'`,\n\t\t\t);\n\t\t}\n\t}\n\n\tawait fs.cp(publicDir, outDir, { recursive: true, force: true });\n}\n\n/**\n * Cleaning removes everything in the output directory, which must not hold the\n * project: the root, the working directory, the templates or an alias target.\n */\nfunction assertCleanable(outDir: string, options: CompileTemplateOptions) {\n\tconst { root, entry } = resolvePaths(options);\n\tconst shown = path.relative(process.cwd(), outDir) || '.';\n\tconst kept: [string, string][] = [\n\t\t[root, 'the root'],\n\t\t[process.cwd(), 'the working directory'],\n\t\t[path.dirname(entry), 'the entry template'],\n\t\t...Object.entries(options.alias ?? {}).map(\n\t\t\t([name, target]): [string, string] => [\n\t\t\t\tpath.resolve(root, target),\n\t\t\t\t`the target of the alias '${name}'`,\n\t\t\t],\n\t\t),\n\t];\n\n\tfor (const [directory, what] of kept) {\n\t\tif (isInside(directory, outDir)) {\n\t\t\tthrow new CompileError(\n\t\t\t\t`Refusing to clean the output directory '${shown}', it contains ${what}. Use another output directory, or --no-clean / 'output.clean: false' to keep its files`,\n\t\t\t);\n\t\t}\n\t}\n}\n\nasync function readContext(file: string, shown: string): Promise<unknown> {\n\tlet text: string;\n\ttry {\n\t\ttext = await fs.readFile(file, 'utf-8');\n\t} catch (cause) {\n\t\tif ((cause as NodeJS.ErrnoException).code === 'ENOENT') {\n\t\t\tthrow new CompileError(`Context file '${shown}' not found`);\n\t\t}\n\t\tthrow new CompileError(`Failed to read the context file '${shown}'`, {\n\t\t\tcause,\n\t\t});\n\t}\n\n\ttry {\n\t\treturn JSON.parse(text);\n\t} catch (cause) {\n\t\tthrow new CompileError(\n\t\t\t`Context file '${shown}' is not valid JSON: ${(cause as Error).message}`,\n\t\t);\n\t}\n}\n\n/** Whether `child` is `parent` or inside of it. */\nfunction isInside(child: string, parent: string): boolean {\n\tconst relative = path.relative(parent, child);\n\treturn (\n\t\trelative === '' ||\n\t\t(!relative.startsWith('..') && !path.isAbsolute(relative))\n\t);\n}\n\nasync function exists(file: string): Promise<boolean> {\n\treturn fs.stat(file).then(\n\t\t() => true,\n\t\t() => false,\n\t);\n}\n\nasync function isDirectory(file: string): Promise<boolean> {\n\treturn fs.stat(file).then(\n\t\t(stats) => stats.isDirectory(),\n\t\t() => false,\n\t);\n}\n\nasync function build(root: string | undefined, flags: BuildFlags) {\n\tconst current = await setup(root, flags);\n\t// nothing is written when there is nothing to build, the watcher waits\n\t// for the entry instead\n\tawait assertEntry(current.options);\n\tawait prepareOutDir(current);\n\tawait copyPublic(current);\n\n\tconst start = performance.now();\n\tconst compiled = await compileTemplate(current.options);\n\tlog.success(`Compiled in ${elapsed(start)}`);\n\n\tawait write(current, compiled);\n}\n\n/** Watches the template and restarts when the config file changes. */\nasync function watchBuild(root: string | undefined, flags: BuildFlags) {\n\tlet current = await setup(root, flags);\n\tawait prepareOutDir(current);\n\n\tlet watcher: TemplateWatcher | undefined;\n\tlet watchers: FSWatcher[] = [];\n\tlet restarting = false;\n\n\tconst start = async () => {\n\t\tlet begin = performance.now();\n\t\tlet first = true;\n\n\t\ttry {\n\t\t\tawait copyPublic(current);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\n\t\twatcher = await watchTemplate({\n\t\t\t...current.options,\n\t\t\tonStart() {\n\t\t\t\tbegin = performance.now();\n\t\t\t},\n\t\t\tasync onCompile(compiled) {\n\t\t\t\tlog.success(`${first ? 'Compiled' : 'Rebuilt'} in ${elapsed(begin)}`);\n\t\t\t\tfirst = false;\n\t\t\t\tawait write(current, compiled);\n\t\t\t},\n\t\t\tonError: log.error,\n\t\t});\n\n\t\tfor (const file of current.loaded.dependencies) {\n\t\t\ttry {\n\t\t\t\tconst configWatcher = watch(file, () => {\n\t\t\t\t\t// saving fires more than once, a restart is enough\n\t\t\t\t\tif (!restarting) {\n\t\t\t\t\t\trestarting = true;\n\t\t\t\t\t\trestart().finally(() => {\n\t\t\t\t\t\t\trestarting = false;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tconfigWatcher.on('error', () => configWatcher.close());\n\t\t\t\twatchers.push(configWatcher);\n\t\t\t} catch {\n\t\t\t\t// a dependency that can not be watched does not restart\n\t\t\t}\n\t\t}\n\n\t\tif (current.publicDir !== undefined) {\n\t\t\twatchers.push(watchPublic(current));\n\t\t}\n\t};\n\n\tconst stopWatchers = () => {\n\t\tfor (const fileWatcher of watchers) {\n\t\t\tfileWatcher.close();\n\t\t}\n\t\twatchers = [];\n\t};\n\n\tconst restart = async () => {\n\t\tlog.info(\n\t\t\t`Config changed, restarting ${styleText('dim', path.relative(process.cwd(), current.loaded.file ?? ''))}`,\n\t\t);\n\t\tstopWatchers();\n\t\tawait watcher?.close();\n\t\ttry {\n\t\t\tcurrent = await setup(root, flags);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\t\tawait start();\n\t};\n\n\tawait start();\n\tlog.info(\n\t\t`Watching for changes ${styleText('dim', '(press Ctrl+C to stop)')}`,\n\t);\n\n\tconst stop = async () => {\n\t\tstopWatchers();\n\t\tawait watcher?.close();\n\t\t// the process ends once nothing is left running: `process.exit` would skip\n\t\t// the cleanup of rspack's native bindings, which then warn about leaks,\n\t\t// so it is only a fallback for something left running, a plugin's timer\n\t\tsetTimeout(() => process.exit(0), 3000).unref();\n\t};\n\tprocess.once('SIGINT', stop);\n\tprocess.once('SIGTERM', stop);\n}\n\n/** Copies the public directory again when something in it changes. */\nfunction watchPublic(current: BuildSetup): FSWatcher {\n\tconst publicDir = current.publicDir as string;\n\tconst changed = new Set<string>();\n\tlet timer: NodeJS.Timeout | undefined;\n\n\tconst sync = async () => {\n\t\tconst names = [...changed];\n\t\tchanged.clear();\n\t\ttry {\n\t\t\t// what was deleted from the public directory goes from the output too\n\t\t\tfor (const name of names) {\n\t\t\t\tif (!(await exists(path.join(publicDir, name)))) {\n\t\t\t\t\tawait fs.rm(path.join(current.outDir, name), {\n\t\t\t\t\t\trecursive: true,\n\t\t\t\t\t\tforce: true,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tawait copyPublic(current);\n\t\t\tlog.success(\n\t\t\t\t`Copied ${styleText('dim', path.relative(process.cwd(), publicDir))}`,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\t};\n\n\tconst publicWatcher = watch(\n\t\tpublicDir,\n\t\t{ recursive: true },\n\t\t(_event, name) => {\n\t\t\tif (name) {\n\t\t\t\tchanged.add(name);\n\t\t\t}\n\t\t\tclearTimeout(timer);\n\t\t\ttimer = setTimeout(sync, 50);\n\t\t},\n\t);\n\tpublicWatcher.on('error', () => publicWatcher.close());\n\tpublicWatcher.on('close', () => clearTimeout(timer));\n\treturn publicWatcher;\n}\n\nasync function prepareOutDir({ outDir, clean }: BuildSetup) {\n\tif (clean) {\n\t\tawait fs.rm(outDir, { recursive: true, force: true });\n\t}\n\tawait fs.mkdir(outDir, { recursive: true });\n}\n\n/**\n * Writes the compiled module and the rendered template, validated and\n * formatted after rendering when enabled.\n */\nasync function write(current: BuildSetup, compiled: CompiledTemplate) {\n\tconst { outDir, moduleFile, templateFile, context, loaded } = current;\n\n\tif (moduleFile) {\n\t\tawait fs.writeFile(path.join(outDir, moduleFile), compiled.source);\n\t}\n\tif (!templateFile) {\n\t\treturn;\n\t}\n\n\tconst file = path.join(outDir, templateFile);\n\tconst shown = path.relative(process.cwd(), file);\n\n\tlet start = performance.now();\n\tconst rendered = renderTemplate({\n\t\t...compiled,\n\t\tcontext,\n\t\tplugins: loaded.config.runtimePlugins,\n\t});\n\tlog.success(`Rendered in ${elapsed(start)}`);\n\n\tstart = performance.now();\n\tlet output: string;\n\ttry {\n\t\toutput = finalizeXml(current, rendered);\n\t} catch (error) {\n\t\t// written anyway, to look at where it is invalid\n\t\tawait fs.writeFile(file, rendered);\n\t\tthrow new CompileError(\n\t\t\t`The rendered template ${shown} is not valid XML: ${(error as Error).message}`,\n\t\t);\n\t}\n\tif (current.validate) {\n\t\tlog.success(\n\t\t\t`${current.format ? 'Formatted' : 'Validated'} in ${elapsed(start)}`,\n\t\t);\n\t}\n\n\tawait fs.writeFile(file, output);\n\tlog.success(`Written ${styleText('dim', shown)}`);\n}\n\n/** Validates and formats the rendered template, as enabled. */\nfunction finalizeXml({ validate, format }: BuildSetup, xml: string): string {\n\tif (!validate) {\n\t\treturn xml;\n\t}\n\tif (!format) {\n\t\tvalidateXml(xml);\n\t\treturn xml;\n\t}\n\treturn formatXml(xml);\n}\n\nfunction elapsed(start: number): string {\n\treturn styleText('yellow', `${Math.round(performance.now() - start)}ms`);\n}\n\n/** The message of the error and of everything that caused it. */\nfunction formatError(error: unknown): string {\n\tconst lines: string[] = [];\n\tlet current: unknown = error;\n\n\twhile (current !== undefined) {\n\t\tlines.push(\n\t\t\tcurrent instanceof Error\n\t\t\t\t? `${lines.length === 0 ? '' : 'Caused by: '}${current.message}`\n\t\t\t\t: String(current),\n\t\t);\n\t\tcurrent = current instanceof Error ? current.cause : undefined;\n\t}\n\n\treturn lines.join('\\n ');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["import { type FSWatcher, watch } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { styleText } from 'node:util';\nimport { cac } from 'cac';\nimport {\n\ttype CompiledTemplate,\n\tCompileError,\n\ttype CompileTemplateOptions,\n\tcompileTemplate,\n\ttype TemplateWatcher,\n\twatchTemplate,\n} from '../compiler';\nimport {\n\tassertCompileOptions,\n\tassertDirectories,\n\tassertEntry,\n\tCOMPILE_OPTION_KEYS,\n\tresolvePaths,\n} from '../compiler/options';\nimport { type LoadedConfig, loadConfig } from '../config';\nimport { withLocation } from '../plugins';\nimport { createRenderer } from '../runtime';\nimport { didYouMean } from '../utils/suggest';\nimport {\n\tprocessXml,\n\ttype XmlError,\n\ttype XmlInfo,\n\ttype XmlResult,\n} from '../xml';\nimport {\n\ttype BuildStats,\n\tcollectStats,\n\tformatStats,\n\ttype WrittenFile,\n} from './stats';\n\nconst { version } = JSON.parse(\n\tawait fs.readFile(new URL('../../package.json', import.meta.url), 'utf-8'),\n) as { version: string };\n\ninterface BuildFlags {\n\tconfig?: string;\n\tentry?: string;\n\toutDir?: string;\n\tcontext?: string;\n\twatch?: boolean;\n\tclean?: boolean;\n\tvalidate?: boolean;\n\tformat?: boolean;\n}\n\n/** Everything a build needs, from the config file and the flags. */\ninterface BuildSetup {\n\toptions: CompileTemplateOptions;\n\tloaded: LoadedConfig;\n\toutDir: string;\n\tmoduleFile: string | false;\n\ttemplateFile: string | false;\n\tclean: boolean;\n\tcontext: unknown;\n\t/** Whether the rendered template is validated. */\n\tvalidate: boolean;\n\t/** Whether the rendered template is formatted, only when it is validated. */\n\tformat: boolean;\n\t/** Absolute path of the public directory, undefined when nothing is copied. */\n\tpublicDir?: string;\n\t/** Sizes of the files written last, by path, to show their changes when watching. */\n\tsizes: Map<string, number>;\n\t/** What the theme was made of at the last build, when watching. */\n\ttheme?: BuildStats['theme'];\n}\n\nconst log = {\n\tsuccess: (message: string) =>\n\t\tconsole.log(`${styleText('green', '✓')} ${message}`),\n\tinfo: (message: string) =>\n\t\tconsole.log(`${styleText('cyan', '●')} ${message}`),\n\twarn: (message: string) =>\n\t\tconsole.warn(`${styleText('yellow', '⚠')} ${message}`),\n\terror: (error: unknown) =>\n\t\tconsole.error(`${styleText('red', '✗')} ${formatError(error)}`),\n};\n\nconst cli = cac('blogger-hbs');\n\ncli\n\t.command('[root]', 'Compile the template and render it')\n\t.alias('build')\n\t.option('-c, --config <file>', 'Path of the config file')\n\t.option('--entry <file>', 'Entry template, relative to the root')\n\t.option('-o, --out-dir <dir>', 'Output directory, relative to the root')\n\t.option('--context <file>', 'JSON file with the context to render with')\n\t.option('-w, --watch', 'Build again when a file changes')\n\t.option('--no-clean', 'Keep the files in the output directory')\n\t.option(\n\t\t'--no-validate',\n\t\t'Do not validate the rendered template as XML, nor format it (templates are always validated when compiling)',\n\t)\n\t.option('--no-format', 'Do not format the rendered template')\n\t.example('blogger-hbs')\n\t.example('blogger-hbs --watch')\n\t.example('blogger-hbs ./theme --entry src/index.hbs --context context.json')\n\t.action(async (rootArg: unknown, rawFlags: BuildFlags) => {\n\t\tprintHeader();\n\t\t// cac turns values that look like numbers into numbers, `-o 2024`\n\t\tconst text = (value: unknown) =>\n\t\t\tvalue === undefined ? undefined : String(value);\n\t\tconst root = text(rootArg);\n\t\tconst flags: BuildFlags = {\n\t\t\t...rawFlags,\n\t\t\tconfig: text(rawFlags.config),\n\t\t\tentry: text(rawFlags.entry),\n\t\t\toutDir: text(rawFlags.outDir),\n\t\t\tcontext: text(rawFlags.context),\n\t\t};\n\n\t\tif (flags.watch) {\n\t\t\tawait watchBuild(root, flags);\n\t\t} else {\n\t\t\tawait build(root, flags);\n\t\t}\n\t});\n\ncli.help();\ncli.version(version);\n\ntry {\n\tcli.parse(process.argv, { run: false });\n\tawait cli.runMatchedCommand();\n} catch (error) {\n\tlog.error(describeCliError(error));\n\tprocess.exitCode = 1;\n}\n\n/** Adds a suggestion and a pointer to `--help` to the errors of the arguments. */\nfunction describeCliError(error: unknown): unknown {\n\tif (!(error instanceof Error) || error.name !== 'CACError') {\n\t\treturn error;\n\t}\n\n\tconst flags = [...cli.commands, cli.globalCommand].flatMap((command) =>\n\t\tcommand.options.flatMap(\n\t\t\t(option) => option.rawName.match(/--[\\w-]+/g) ?? [],\n\t\t),\n\t);\n\tconst unknown = /^Unknown option `(.+)`$/.exec(error.message)?.[1];\n\tconst hint = unknown ? didYouMean(unknown, flags, '`') : '';\n\n\treturn new Error(\n\t\t`${error.message}${hint}\\nRun 'blogger-hbs --help' to see the available options`,\n\t);\n}\n\nasync function setup(\n\troot: string | undefined,\n\tflags: BuildFlags,\n): Promise<BuildSetup> {\n\tconst cwd = process.cwd();\n\tconst rootFlag = root === undefined ? undefined : path.resolve(cwd, root);\n\n\t// the config is looked for in the root, which has to exist for that\n\tif (rootFlag !== undefined && !(await isDirectory(rootFlag))) {\n\t\tthrow new CompileError(\n\t\t\t(await exists(rootFlag))\n\t\t\t\t? `Root '${root}' is not a directory`\n\t\t\t\t: `Root directory '${root}' does not exist`,\n\t\t);\n\t}\n\n\tconst loaded = await loadConfig({\n\t\tcwd: rootFlag ?? cwd,\n\t\tconfigFile: flags.config && path.resolve(cwd, flags.config),\n\t\twatch: flags.watch,\n\t});\n\tconst { config } = loaded;\n\n\tconst resolvedRoot = rootFlag ?? config.root ?? cwd;\n\tconst output = config.output ?? {};\n\n\t// the config has more than the compile options\n\tconst options: CompileTemplateOptions = {};\n\tfor (const key of COMPILE_OPTION_KEYS) {\n\t\tif (key in config) {\n\t\t\tObject.assign(options, { [key]: config[key as keyof typeof config] });\n\t\t}\n\t}\n\toptions.root = resolvedRoot;\n\toptions.entry = flags.entry ?? config.entry;\n\toptions.onWarn ??= log.warn;\n\n\tassertCompileOptions(options);\n\tawait assertDirectories(options);\n\n\tconst outDir = path.resolve(\n\t\tresolvedRoot,\n\t\tflags.outDir ?? output.dir ?? 'dist',\n\t);\n\tconst clean = flags.clean === false ? false : (output.clean ?? true);\n\tif (clean) {\n\t\tassertCleanable(outDir, options);\n\t}\n\n\treturn {\n\t\toptions,\n\t\tloaded,\n\t\toutDir,\n\t\tmoduleFile: output.module ?? 'compiled.js',\n\t\ttemplateFile: output.template ?? 'theme.xml',\n\t\tvalidate: flags.validate === false ? false : (output.validate ?? true),\n\t\tformat: flags.format === false ? false : (output.format ?? true),\n\t\tclean,\n\t\tcontext: flags.context\n\t\t\t? await readContext(path.resolve(cwd, flags.context), flags.context)\n\t\t\t: (config.context ?? {}),\n\t\tpublicDir: await resolvePublicDir(resolvedRoot, config.publicDir, outDir),\n\t\tsizes: new Map(),\n\t};\n}\n\n/** The public directory, `public` when it exists unless set otherwise. */\nasync function resolvePublicDir(\n\troot: string,\n\tvalue: string | false | undefined,\n\toutDir: string,\n): Promise<string | undefined> {\n\tif (value === false) {\n\t\treturn;\n\t}\n\n\tconst directory = path.resolve(root, value ?? 'public');\n\tconst shown = path.relative(process.cwd(), directory) || '.';\n\n\tif (!(await isDirectory(directory))) {\n\t\tif (value === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tthrow new CompileError(\n\t\t\t(await exists(directory))\n\t\t\t\t? `Public directory '${shown}' is not a directory`\n\t\t\t\t: `Public directory '${shown}' does not exist, set 'publicDir: false' to not copy anything`,\n\t\t);\n\t}\n\n\tif (isInside(outDir, directory) || isInside(directory, outDir)) {\n\t\tthrow new CompileError(\n\t\t\t`Public directory '${shown}' and output directory '${path.relative(process.cwd(), outDir) || '.'}' overlap, they must be separate`,\n\t\t);\n\t}\n\n\treturn directory;\n}\n\n/** Copies the public directory into the output directory. */\nasync function copyPublic({\n\tpublicDir,\n\toutDir,\n\tmoduleFile,\n\ttemplateFile,\n}: BuildSetup): Promise<void> {\n\tif (publicDir === undefined) {\n\t\treturn;\n\t}\n\n\tfor (const name of await fs.readdir(publicDir)) {\n\t\tif (name === moduleFile || name === templateFile) {\n\t\t\tthrow new CompileError(\n\t\t\t\t`'${name}' in the public directory has the same name as the ${name === moduleFile ? 'compiled module' : 'rendered template'} and would be overwritten, rename it or set 'output.${name === moduleFile ? 'module' : 'template'}'`,\n\t\t\t);\n\t\t}\n\t}\n\n\tawait fs.cp(publicDir, outDir, { recursive: true, force: true });\n}\n\n/**\n * Cleaning removes everything in the output directory, which must not hold the\n * project: the root, the working directory, the templates or an alias target.\n */\nfunction assertCleanable(outDir: string, options: CompileTemplateOptions) {\n\tconst { root, entry } = resolvePaths(options);\n\tconst shown = path.relative(process.cwd(), outDir) || '.';\n\tconst kept: [string, string][] = [\n\t\t[root, 'the root'],\n\t\t[process.cwd(), 'the working directory'],\n\t\t[path.dirname(entry), 'the entry template'],\n\t\t...Object.entries(options.alias ?? {}).map(\n\t\t\t([name, target]): [string, string] => [\n\t\t\t\tpath.resolve(root, target),\n\t\t\t\t`the target of the alias '${name}'`,\n\t\t\t],\n\t\t),\n\t];\n\n\tfor (const [directory, what] of kept) {\n\t\tif (isInside(directory, outDir)) {\n\t\t\tthrow new CompileError(\n\t\t\t\t`Refusing to clean the output directory '${shown}', it contains ${what}. Use another output directory, or --no-clean / 'output.clean: false' to keep its files`,\n\t\t\t);\n\t\t}\n\t}\n}\n\nasync function readContext(file: string, shown: string): Promise<unknown> {\n\tlet text: string;\n\ttry {\n\t\ttext = await fs.readFile(file, 'utf-8');\n\t} catch (cause) {\n\t\tif ((cause as NodeJS.ErrnoException).code === 'ENOENT') {\n\t\t\tthrow new CompileError(`Context file '${shown}' not found`);\n\t\t}\n\t\tthrow new CompileError(`Failed to read the context file '${shown}'`, {\n\t\t\tcause,\n\t\t});\n\t}\n\n\ttry {\n\t\treturn JSON.parse(text);\n\t} catch (cause) {\n\t\tthrow new CompileError(\n\t\t\t`Context file '${shown}' is not valid JSON: ${(cause as Error).message}`,\n\t\t);\n\t}\n}\n\n/** Whether `child` is `parent` or inside of it. */\nfunction isInside(child: string, parent: string): boolean {\n\tconst relative = path.relative(parent, child);\n\treturn (\n\t\trelative === '' ||\n\t\t(!relative.startsWith('..') && !path.isAbsolute(relative))\n\t);\n}\n\nasync function exists(file: string): Promise<boolean> {\n\treturn fs.stat(file).then(\n\t\t() => true,\n\t\t() => false,\n\t);\n}\n\nasync function isDirectory(file: string): Promise<boolean> {\n\treturn fs.stat(file).then(\n\t\t(stats) => stats.isDirectory(),\n\t\t() => false,\n\t);\n}\n\nasync function build(root: string | undefined, flags: BuildFlags) {\n\tconst current = await setup(root, flags);\n\t// nothing is written when there is nothing to build, the watcher waits\n\t// for the entry instead\n\tawait assertEntry(current.options);\n\tawait prepareOutDir(current);\n\tawait copyPublic(current);\n\n\tconst start = performance.now();\n\tconst compiled = await compileTemplate(current.options);\n\tlog.success(`Compiled in ${elapsed(start)}`);\n\n\tawait write(current, compiled, start);\n}\n\n/** Watches the template and restarts when the config file changes. */\nasync function watchBuild(root: string | undefined, flags: BuildFlags) {\n\tlet current = await setup(root, flags);\n\tawait prepareOutDir(current);\n\n\tlet watcher: TemplateWatcher | undefined;\n\tlet watchers: FSWatcher[] = [];\n\tlet restarting = false;\n\n\tconst start = async () => {\n\t\tlet begin = performance.now();\n\t\tlet first = true;\n\t\t/** Shown before what a rebuild prints, after the first build. */\n\t\tlet divider = '';\n\n\t\t/** Once, before the first thing a rebuild prints, its output can be the same. */\n\t\tconst printDivider = () => {\n\t\t\tif (divider) {\n\t\t\t\tconsole.log(divider);\n\t\t\t\tdivider = '';\n\t\t\t}\n\t\t};\n\n\t\ttry {\n\t\t\tawait copyPublic(current);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\n\t\twatcher = await watchTemplate({\n\t\t\t...current.options,\n\t\t\tonStart({ files }) {\n\t\t\t\tbegin = performance.now();\n\t\t\t\tdivider = first ? '' : formatDivider(files);\n\t\t\t},\n\t\t\tasync onCompile(compiled) {\n\t\t\t\tprintDivider();\n\t\t\t\tlog.success(`${first ? 'Compiled' : 'Rebuilt'} in ${elapsed(begin)}`);\n\t\t\t\tawait write(current, compiled, begin);\n\t\t\t\tfirst = false;\n\t\t\t},\n\t\t\tonError(error) {\n\t\t\t\tprintDivider();\n\t\t\t\tlog.error(error);\n\t\t\t},\n\t\t});\n\n\t\tfor (const file of current.loaded.dependencies) {\n\t\t\ttry {\n\t\t\t\tconst configWatcher = watch(file, () => {\n\t\t\t\t\t// saving fires more than once, a restart is enough\n\t\t\t\t\tif (!restarting) {\n\t\t\t\t\t\trestarting = true;\n\t\t\t\t\t\trestart().finally(() => {\n\t\t\t\t\t\t\trestarting = false;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tconfigWatcher.on('error', () => configWatcher.close());\n\t\t\t\twatchers.push(configWatcher);\n\t\t\t} catch {\n\t\t\t\t// a dependency that can not be watched does not restart\n\t\t\t}\n\t\t}\n\n\t\tif (current.publicDir !== undefined) {\n\t\t\twatchers.push(watchPublic(current));\n\t\t}\n\t};\n\n\tconst stopWatchers = () => {\n\t\tfor (const fileWatcher of watchers) {\n\t\t\tfileWatcher.close();\n\t\t}\n\t\twatchers = [];\n\t};\n\n\tconst restart = async () => {\n\t\tlog.info(\n\t\t\t`Config changed, restarting ${styleText('dim', path.relative(process.cwd(), current.loaded.file ?? ''))}`,\n\t\t);\n\t\tstopWatchers();\n\t\tawait watcher?.close();\n\t\ttry {\n\t\t\tcurrent = await setup(root, flags);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\t\tawait start();\n\t};\n\n\tawait start();\n\tlog.info(\n\t\t`Watching for changes ${styleText('dim', '(press Ctrl+C to stop)')}`,\n\t);\n\n\tconst stop = async () => {\n\t\tstopWatchers();\n\t\tawait watcher?.close();\n\t\t// the process ends once nothing is left running: `process.exit` would skip\n\t\t// the cleanup of rspack's native bindings, which then warn about leaks,\n\t\t// so it is only a fallback for something left running, a plugin's timer\n\t\tsetTimeout(() => process.exit(0), 3000).unref();\n\t};\n\tprocess.once('SIGINT', stop);\n\tprocess.once('SIGTERM', stop);\n}\n\n/** Copies the public directory again when something in it changes. */\nfunction watchPublic(current: BuildSetup): FSWatcher {\n\tconst publicDir = current.publicDir as string;\n\tconst changed = new Set<string>();\n\tlet timer: NodeJS.Timeout | undefined;\n\n\tconst sync = async () => {\n\t\tconst names = [...changed];\n\t\tchanged.clear();\n\t\ttry {\n\t\t\t// what was deleted from the public directory goes from the output too\n\t\t\tfor (const name of names) {\n\t\t\t\tif (!(await exists(path.join(publicDir, name)))) {\n\t\t\t\t\tawait fs.rm(path.join(current.outDir, name), {\n\t\t\t\t\t\trecursive: true,\n\t\t\t\t\t\tforce: true,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tawait copyPublic(current);\n\t\t\tlog.success(\n\t\t\t\t`Copied ${styleText('dim', path.relative(process.cwd(), publicDir))}`,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\t};\n\n\tconst publicWatcher = watch(\n\t\tpublicDir,\n\t\t{ recursive: true },\n\t\t(_event, name) => {\n\t\t\tif (name) {\n\t\t\t\tchanged.add(name);\n\t\t\t}\n\t\t\tclearTimeout(timer);\n\t\t\ttimer = setTimeout(sync, 50);\n\t\t},\n\t);\n\tpublicWatcher.on('error', () => publicWatcher.close());\n\tpublicWatcher.on('close', () => clearTimeout(timer));\n\treturn publicWatcher;\n}\n\nasync function prepareOutDir({ outDir, clean }: BuildSetup) {\n\tif (clean) {\n\t\tawait fs.rm(outDir, { recursive: true, force: true });\n\t}\n\tawait fs.mkdir(outDir, { recursive: true });\n}\n\n/**\n * Writes the compiled module and the rendered template, validated and\n * formatted after rendering when enabled.\n */\nasync function write(\n\tcurrent: BuildSetup,\n\tcompiled: CompiledTemplate,\n\tbegin: number,\n) {\n\tconst { outDir, moduleFile, templateFile } = current;\n\tconst written: WrittenFile[] = [];\n\tlet theme: WrittenTheme | undefined;\n\n\tif (moduleFile) {\n\t\tconst file = path.join(outDir, moduleFile);\n\t\tawait fs.writeFile(file, compiled.source);\n\t\twritten.push({\n\t\t\tpath: path.relative(process.cwd(), file),\n\t\t\tcontent: compiled.source,\n\t\t});\n\t}\n\tif (templateFile) {\n\t\ttheme = await writeTheme(current, compiled, templateFile);\n\t\twritten.unshift(theme.file);\n\t}\n\n\tconst stats = await collectStats(\n\t\tcompiled,\n\t\twritten,\n\t\ttheme?.info && { ...theme.file, info: theme.info },\n\t);\n\tconsole.log(\n\t\tformatStats(stats, current.sizes, { previousTheme: current.theme }),\n\t);\n\tcurrent.sizes = new Map(stats.files.map(({ path, size }) => [path, size]));\n\tcurrent.theme = stats.theme;\n\tlog.success(`Built in ${elapsed(begin)}`);\n}\n\n/** `─── 14:32:05 · src/header.hbs +2 ───────`, dim. */\nfunction formatDivider(files: readonly string[]): string {\n\tconst time = new Date().toLocaleTimeString('en-GB', { hour12: false });\n\tconst [file, ...others] = files.map((item) =>\n\t\tpath.relative(process.cwd(), item),\n\t);\n\tconst what = file\n\t\t? ` · ${file}${others.length > 0 ? ` +${others.length}` : ''}`\n\t\t: '';\n\tconst text = `─── ${time}${what} `;\n\treturn `\\n${styleText('dim', text + '─'.repeat(Math.max(3, 50 - text.length)))}`;\n}\n\ninterface WrittenTheme {\n\tfile: WrittenFile;\n\t/** What it is made of, when it is validated. */\n\tinfo?: XmlInfo;\n}\n\n/** Renders, validates and formats the theme, as enabled, and writes it. */\nasync function writeTheme(\n\tcurrent: BuildSetup,\n\tcompiled: CompiledTemplate,\n\ttemplateFile: string,\n): Promise<WrittenTheme> {\n\tconst { outDir, context } = current;\n\tconst file = path.join(outDir, templateFile);\n\tconst shown = path.relative(process.cwd(), file);\n\n\t// the same as anywhere else the compiled module is rendered\n\tlet start = performance.now();\n\tconst renderer = createRenderer(compiled);\n\tconst rendered = await renderer.render(context);\n\tlog.success(`Rendered in ${elapsed(start)}`);\n\n\tif (!current.validate) {\n\t\tawait fs.writeFile(file, rendered);\n\t\treturn { file: { path: shown, content: rendered } };\n\t}\n\n\tstart = performance.now();\n\tlet result: XmlResult;\n\ttry {\n\t\tresult = processXml(rendered, { path: shown, format: current.format });\n\t} catch (error) {\n\t\t// written anyway, to look at where it is invalid\n\t\tawait fs.writeFile(file, rendered);\n\t\tthrow new CompileError('The rendered template is not valid XML', {\n\t\t\tcause: error,\n\t\t\tloc: (error as XmlError).loc,\n\t\t});\n\t}\n\tfor (const { message, loc } of result.warnings) {\n\t\tlog.warn(withLocation(message, loc));\n\t}\n\tlog.success(\n\t\t`${current.format ? 'Formatted' : 'Validated'} in ${elapsed(start)}`,\n\t);\n\n\tawait fs.writeFile(file, result.xml);\n\treturn { file: { path: shown, content: result.xml }, info: result.info };\n}\n\n/**\n * A blank line, `blogger-hbs v0.0.4` right-aligned on a cyan line under it,\n * before what a build prints.\n */\nfunction printHeader(): void {\n\tconst text = `blogger-hbs v${version}`;\n\tconst width = text.length + 2;\n\tconsole.log(\n\t\t`\\n${' '.repeat(width - text.length)}${styleText('bold', 'blogger-hbs')} ${styleText('dim', `v${version}`)}\\n${styleText('cyan', '─'.repeat(width))}`,\n\t);\n}\n\nfunction elapsed(start: number): string {\n\treturn styleText('yellow', `${Math.round(performance.now() - start)}ms`);\n}\n\n/**\n * The message of the error and of everything that caused it, a cause whose\n * message is already in the one before is not repeated. The lines of a\n * message are indented under its first.\n */\nfunction formatError(error: unknown): string {\n\tconst lines: string[] = [];\n\tlet previous = '';\n\tlet current: unknown = error;\n\n\twhile (current !== undefined) {\n\t\tconst message =\n\t\t\tcurrent instanceof Error ? current.message : String(current);\n\t\tif (lines.length === 0 || !previous.includes(message)) {\n\t\t\tlines.push(\n\t\t\t\t`${lines.length === 0 ? '' : 'Caused by: '}${message.replaceAll('\\n', '\\n ')}`,\n\t\t\t);\n\t\t}\n\t\tprevious = message;\n\t\tcurrent = current instanceof Error ? current.cause : undefined;\n\t}\n\n\treturn lines.join('\\n ');\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAqCA,MAAM,EAAE,YAAY,KAAK,MACxB,MAAM,GAAG,SAAS,IAAI,IAAI,sBAAsB,YAAY,GAAG,GAAG,OAAO,CAC1E;AAkCA,MAAM,MAAM;CACX,UAAU,YACT,QAAQ,IAAI,GAAG,UAAU,SAAS,GAAG,EAAE,GAAG,SAAS;CACpD,OAAO,YACN,QAAQ,IAAI,GAAG,UAAU,QAAQ,GAAG,EAAE,GAAG,SAAS;CACnD,OAAO,YACN,QAAQ,KAAK,GAAG,UAAU,UAAU,GAAG,EAAE,GAAG,SAAS;CACtD,QAAQ,UACP,QAAQ,MAAM,GAAG,UAAU,OAAO,GAAG,EAAE,GAAG,YAAY,KAAK,GAAG;AAChE;AAEA,MAAM,MAAM,IAAI,aAAa;AAE7B,IACE,QAAQ,UAAU,oCAAoC,CAAC,CACvD,MAAM,OAAO,CAAC,CACd,OAAO,uBAAuB,yBAAyB,CAAC,CACxD,OAAO,kBAAkB,sCAAsC,CAAC,CAChE,OAAO,uBAAuB,wCAAwC,CAAC,CACvE,OAAO,oBAAoB,2CAA2C,CAAC,CACvE,OAAO,eAAe,iCAAiC,CAAC,CACxD,OAAO,cAAc,wCAAwC,CAAC,CAC9D,OACA,iBACA,6GACD,CAAC,CACA,OAAO,eAAe,qCAAqC,CAAC,CAC5D,QAAQ,aAAa,CAAC,CACtB,QAAQ,qBAAqB,CAAC,CAC9B,QAAQ,kEAAkE,CAAC,CAC3E,OAAO,OAAO,SAAkB,aAAyB;CACzD,YAAY;CAEZ,MAAM,QAAQ,UACb,UAAU,KAAA,IAAY,KAAA,IAAY,OAAO,KAAK;CAC/C,MAAM,OAAO,KAAK,OAAO;CACzB,MAAM,QAAoB;EACzB,GAAG;EACH,QAAQ,KAAK,SAAS,MAAM;EAC5B,OAAO,KAAK,SAAS,KAAK;EAC1B,QAAQ,KAAK,SAAS,MAAM;EAC5B,SAAS,KAAK,SAAS,OAAO;CAC/B;CAEA,IAAI,MAAM,OACT,MAAM,WAAW,MAAM,KAAK;MAE5B,MAAM,MAAM,MAAM,KAAK;AAEzB,CAAC;AAEF,IAAI,KAAK;AACT,IAAI,QAAQ,OAAO;AAEnB,IAAI;CACH,IAAI,MAAM,QAAQ,MAAM,EAAE,KAAK,MAAM,CAAC;CACtC,MAAM,IAAI,kBAAkB;AAC7B,SAAS,OAAO;CACf,IAAI,MAAM,iBAAiB,KAAK,CAAC;CACjC,QAAQ,WAAW;AACpB;;AAGA,SAAS,iBAAiB,OAAyB;CAClD,IAAI,EAAE,iBAAiB,UAAU,MAAM,SAAS,YAC/C,OAAO;CAGR,MAAM,QAAQ,CAAC,GAAG,IAAI,UAAU,IAAI,aAAa,CAAC,CAAC,SAAS,YAC3D,QAAQ,QAAQ,SACd,WAAW,OAAO,QAAQ,MAAM,WAAW,KAAK,CAAC,CACnD,CACD;CACA,MAAM,UAAU,0BAA0B,KAAK,MAAM,OAAO,CAAC,GAAG;CAChE,MAAM,OAAO,UAAU,WAAW,SAAS,OAAO,GAAG,IAAI;CAEzD,uBAAO,IAAI,MACV,GAAG,MAAM,UAAU,KAAK,wDACzB;AACD;AAEA,eAAe,MACd,MACA,OACsB;CACtB,MAAM,MAAM,QAAQ,IAAI;CACxB,MAAM,WAAW,SAAS,KAAA,IAAY,KAAA,IAAY,KAAK,QAAQ,KAAK,IAAI;CAGxE,IAAI,aAAa,KAAA,KAAa,CAAE,MAAM,YAAY,QAAQ,GACzD,MAAM,IAAI,aACR,MAAM,OAAO,QAAQ,IACnB,SAAS,KAAK,wBACd,mBAAmB,KAAK,iBAC5B;CAGD,MAAM,SAAS,MAAM,WAAW;EAC/B,KAAK,YAAY;EACjB,YAAY,MAAM,UAAU,KAAK,QAAQ,KAAK,MAAM,MAAM;EAC1D,OAAO,MAAM;CACd,CAAC;CACD,MAAM,EAAE,WAAW;CAEnB,MAAM,eAAe,YAAY,OAAO,QAAQ;CAChD,MAAM,SAAS,OAAO,UAAU,CAAC;CAGjC,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,OAAO,qBACjB,IAAI,OAAO,QACV,OAAO,OAAO,SAAS,GAAG,MAAM,OAAO,KAA4B,CAAC;CAGtE,QAAQ,OAAO;CACf,QAAQ,QAAQ,MAAM,SAAS,OAAO;CACtC,QAAQ,WAAW,IAAI;CAEvB,qBAAqB,OAAO;CAC5B,MAAM,kBAAkB,OAAO;CAE/B,MAAM,SAAS,KAAK,QACnB,cACA,MAAM,UAAU,OAAO,OAAO,MAC/B;CACA,MAAM,QAAQ,MAAM,UAAU,QAAQ,QAAS,OAAO,SAAS;CAC/D,IAAI,OACH,gBAAgB,QAAQ,OAAO;CAGhC,OAAO;EACN;EACA;EACA;EACA,YAAY,OAAO,UAAU;EAC7B,cAAc,OAAO,YAAY;EACjC,UAAU,MAAM,aAAa,QAAQ,QAAS,OAAO,YAAY;EACjE,QAAQ,MAAM,WAAW,QAAQ,QAAS,OAAO,UAAU;EAC3D;EACA,SAAS,MAAM,UACZ,MAAM,YAAY,KAAK,QAAQ,KAAK,MAAM,OAAO,GAAG,MAAM,OAAO,IAChE,OAAO,WAAW,CAAC;EACvB,WAAW,MAAM,iBAAiB,cAAc,OAAO,WAAW,MAAM;EACxE,uBAAO,IAAI,IAAI;CAChB;AACD;;AAGA,eAAe,iBACd,MACA,OACA,QAC8B;CAC9B,IAAI,UAAU,OACb;CAGD,MAAM,YAAY,KAAK,QAAQ,MAAM,SAAS,QAAQ;CACtD,MAAM,QAAQ,KAAK,SAAS,QAAQ,IAAI,GAAG,SAAS,KAAK;CAEzD,IAAI,CAAE,MAAM,YAAY,SAAS,GAAI;EACpC,IAAI,UAAU,KAAA,GACb;EAED,MAAM,IAAI,aACR,MAAM,OAAO,SAAS,IACpB,qBAAqB,MAAM,wBAC3B,qBAAqB,MAAM,8DAC/B;CACD;CAEA,IAAI,SAAS,QAAQ,SAAS,KAAK,SAAS,WAAW,MAAM,GAC5D,MAAM,IAAI,aACT,qBAAqB,MAAM,0BAA0B,KAAK,SAAS,QAAQ,IAAI,GAAG,MAAM,KAAK,IAAI,iCAClG;CAGD,OAAO;AACR;;AAGA,eAAe,WAAW,EACzB,WACA,QACA,YACA,gBAC6B;CAC7B,IAAI,cAAc,KAAA,GACjB;CAGD,KAAK,MAAM,QAAQ,MAAM,GAAG,QAAQ,SAAS,GAC5C,IAAI,SAAS,cAAc,SAAS,cACnC,MAAM,IAAI,aACT,IAAI,KAAK,qDAAqD,SAAS,aAAa,oBAAoB,oBAAoB,sDAAsD,SAAS,aAAa,WAAW,WAAW,EAC/N;CAIF,MAAM,GAAG,GAAG,WAAW,QAAQ;EAAE,WAAW;EAAM,OAAO;CAAK,CAAC;AAChE;;;;;AAMA,SAAS,gBAAgB,QAAgB,SAAiC;CACzE,MAAM,EAAE,MAAM,UAAU,aAAa,OAAO;CAC5C,MAAM,QAAQ,KAAK,SAAS,QAAQ,IAAI,GAAG,MAAM,KAAK;CACtD,MAAM,OAA2B;EAChC,CAAC,MAAM,UAAU;EACjB,CAAC,QAAQ,IAAI,GAAG,uBAAuB;EACvC,CAAC,KAAK,QAAQ,KAAK,GAAG,oBAAoB;EAC1C,GAAG,OAAO,QAAQ,QAAQ,SAAS,CAAC,CAAC,CAAC,CAAC,KACrC,CAAC,MAAM,YAA8B,CACrC,KAAK,QAAQ,MAAM,MAAM,GACzB,4BAA4B,KAAK,EAClC,CACD;CACD;CAEA,KAAK,MAAM,CAAC,WAAW,SAAS,MAC/B,IAAI,SAAS,WAAW,MAAM,GAC7B,MAAM,IAAI,aACT,2CAA2C,MAAM,iBAAiB,KAAK,wFACxE;AAGH;AAEA,eAAe,YAAY,MAAc,OAAiC;CACzE,IAAI;CACJ,IAAI;EACH,OAAO,MAAM,GAAG,SAAS,MAAM,OAAO;CACvC,SAAS,OAAO;EACf,IAAK,MAAgC,SAAS,UAC7C,MAAM,IAAI,aAAa,iBAAiB,MAAM,YAAY;EAE3D,MAAM,IAAI,aAAa,oCAAoC,MAAM,IAAI,EACpE,MACD,CAAC;CACF;CAEA,IAAI;EACH,OAAO,KAAK,MAAM,IAAI;CACvB,SAAS,OAAO;EACf,MAAM,IAAI,aACT,iBAAiB,MAAM,uBAAwB,MAAgB,SAChE;CACD;AACD;;AAGA,SAAS,SAAS,OAAe,QAAyB;CACzD,MAAM,WAAW,KAAK,SAAS,QAAQ,KAAK;CAC5C,OACC,aAAa,MACZ,CAAC,SAAS,WAAW,IAAI,KAAK,CAAC,KAAK,WAAW,QAAQ;AAE1D;AAEA,eAAe,OAAO,MAAgC;CACrD,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,WACd,YACA,KACP;AACD;AAEA,eAAe,YAAY,MAAgC;CAC1D,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,MACnB,UAAU,MAAM,YAAY,SACvB,KACP;AACD;AAEA,eAAe,MAAM,MAA0B,OAAmB;CACjE,MAAM,UAAU,MAAM,MAAM,MAAM,KAAK;CAGvC,MAAM,YAAY,QAAQ,OAAO;CACjC,MAAM,cAAc,OAAO;CAC3B,MAAM,WAAW,OAAO;CAExB,MAAM,QAAQ,YAAY,IAAI;CAC9B,MAAM,WAAW,MAAM,gBAAgB,QAAQ,OAAO;CACtD,IAAI,QAAQ,eAAe,QAAQ,KAAK,GAAG;CAE3C,MAAM,MAAM,SAAS,UAAU,KAAK;AACrC;;AAGA,eAAe,WAAW,MAA0B,OAAmB;CACtE,IAAI,UAAU,MAAM,MAAM,MAAM,KAAK;CACrC,MAAM,cAAc,OAAO;CAE3B,IAAI;CACJ,IAAI,WAAwB,CAAC;CAC7B,IAAI,aAAa;CAEjB,MAAM,QAAQ,YAAY;EACzB,IAAI,QAAQ,YAAY,IAAI;EAC5B,IAAI,QAAQ;;EAEZ,IAAI,UAAU;;EAGd,MAAM,qBAAqB;GAC1B,IAAI,SAAS;IACZ,QAAQ,IAAI,OAAO;IACnB,UAAU;GACX;EACD;EAEA,IAAI;GACH,MAAM,WAAW,OAAO;EACzB,SAAS,OAAO;GACf,IAAI,MAAM,KAAK;EAChB;EAEA,UAAU,MAAM,cAAc;GAC7B,GAAG,QAAQ;GACX,QAAQ,EAAE,SAAS;IAClB,QAAQ,YAAY,IAAI;IACxB,UAAU,QAAQ,KAAK,cAAc,KAAK;GAC3C;GACA,MAAM,UAAU,UAAU;IACzB,aAAa;IACb,IAAI,QAAQ,GAAG,QAAQ,aAAa,UAAU,MAAM,QAAQ,KAAK,GAAG;IACpE,MAAM,MAAM,SAAS,UAAU,KAAK;IACpC,QAAQ;GACT;GACA,QAAQ,OAAO;IACd,aAAa;IACb,IAAI,MAAM,KAAK;GAChB;EACD,CAAC;EAED,KAAK,MAAM,QAAQ,QAAQ,OAAO,cACjC,IAAI;GACH,MAAM,gBAAgB,MAAM,YAAY;IAEvC,IAAI,CAAC,YAAY;KAChB,aAAa;KACb,QAAQ,CAAC,CAAC,cAAc;MACvB,aAAa;KACd,CAAC;IACF;GACD,CAAC;GACD,cAAc,GAAG,eAAe,cAAc,MAAM,CAAC;GACrD,SAAS,KAAK,aAAa;EAC5B,QAAQ,CAER;EAGD,IAAI,QAAQ,cAAc,KAAA,GACzB,SAAS,KAAK,YAAY,OAAO,CAAC;CAEpC;CAEA,MAAM,qBAAqB;EAC1B,KAAK,MAAM,eAAe,UACzB,YAAY,MAAM;EAEnB,WAAW,CAAC;CACb;CAEA,MAAM,UAAU,YAAY;EAC3B,IAAI,KACH,8BAA8B,UAAU,OAAO,KAAK,SAAS,QAAQ,IAAI,GAAG,QAAQ,OAAO,QAAQ,EAAE,CAAC,GACvG;EACA,aAAa;EACb,MAAM,SAAS,MAAM;EACrB,IAAI;GACH,UAAU,MAAM,MAAM,MAAM,KAAK;EAClC,SAAS,OAAO;GACf,IAAI,MAAM,KAAK;EAChB;EACA,MAAM,MAAM;CACb;CAEA,MAAM,MAAM;CACZ,IAAI,KACH,wBAAwB,UAAU,OAAO,wBAAwB,GAClE;CAEA,MAAM,OAAO,YAAY;EACxB,aAAa;EACb,MAAM,SAAS,MAAM;EAIrB,iBAAiB,QAAQ,KAAK,CAAC,GAAG,GAAI,CAAC,CAAC,MAAM;CAC/C;CACA,QAAQ,KAAK,UAAU,IAAI;CAC3B,QAAQ,KAAK,WAAW,IAAI;AAC7B;;AAGA,SAAS,YAAY,SAAgC;CACpD,MAAM,YAAY,QAAQ;CAC1B,MAAM,0BAAU,IAAI,IAAY;CAChC,IAAI;CAEJ,MAAM,OAAO,YAAY;EACxB,MAAM,QAAQ,CAAC,GAAG,OAAO;EACzB,QAAQ,MAAM;EACd,IAAI;GAEH,KAAK,MAAM,QAAQ,OAClB,IAAI,CAAE,MAAM,OAAO,KAAK,KAAK,WAAW,IAAI,CAAC,GAC5C,MAAM,GAAG,GAAG,KAAK,KAAK,QAAQ,QAAQ,IAAI,GAAG;IAC5C,WAAW;IACX,OAAO;GACR,CAAC;GAGH,MAAM,WAAW,OAAO;GACxB,IAAI,QACH,UAAU,UAAU,OAAO,KAAK,SAAS,QAAQ,IAAI,GAAG,SAAS,CAAC,GACnE;EACD,SAAS,OAAO;GACf,IAAI,MAAM,KAAK;EAChB;CACD;CAEA,MAAM,gBAAgB,MACrB,WACA,EAAE,WAAW,KAAK,IACjB,QAAQ,SAAS;EACjB,IAAI,MACH,QAAQ,IAAI,IAAI;EAEjB,aAAa,KAAK;EAClB,QAAQ,WAAW,MAAM,EAAE;CAC5B,CACD;CACA,cAAc,GAAG,eAAe,cAAc,MAAM,CAAC;CACrD,cAAc,GAAG,eAAe,aAAa,KAAK,CAAC;CACnD,OAAO;AACR;AAEA,eAAe,cAAc,EAAE,QAAQ,SAAqB;CAC3D,IAAI,OACH,MAAM,GAAG,GAAG,QAAQ;EAAE,WAAW;EAAM,OAAO;CAAK,CAAC;CAErD,MAAM,GAAG,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;AAC3C;;;;;AAMA,eAAe,MACd,SACA,UACA,OACC;CACD,MAAM,EAAE,QAAQ,YAAY,iBAAiB;CAC7C,MAAM,UAAyB,CAAC;CAChC,IAAI;CAEJ,IAAI,YAAY;EACf,MAAM,OAAO,KAAK,KAAK,QAAQ,UAAU;EACzC,MAAM,GAAG,UAAU,MAAM,SAAS,MAAM;EACxC,QAAQ,KAAK;GACZ,MAAM,KAAK,SAAS,QAAQ,IAAI,GAAG,IAAI;GACvC,SAAS,SAAS;EACnB,CAAC;CACF;CACA,IAAI,cAAc;EACjB,QAAQ,MAAM,WAAW,SAAS,UAAU,YAAY;EACxD,QAAQ,QAAQ,MAAM,IAAI;CAC3B;CAEA,MAAM,QAAQ,MAAM,aACnB,UACA,SACA,OAAO,QAAQ;EAAE,GAAG,MAAM;EAAM,MAAM,MAAM;CAAK,CAClD;CACA,QAAQ,IACP,YAAY,OAAO,QAAQ,OAAO,EAAE,eAAe,QAAQ,MAAM,CAAC,CACnE;CACA,QAAQ,QAAQ,IAAI,IAAI,MAAM,MAAM,KAAK,EAAE,MAAM,WAAW,CAAC,MAAM,IAAI,CAAC,CAAC;CACzE,QAAQ,QAAQ,MAAM;CACtB,IAAI,QAAQ,YAAY,QAAQ,KAAK,GAAG;AACzC;;AAGA,SAAS,cAAc,OAAkC;CACxD,MAAM,wBAAO,IAAI,KAAK,EAAA,CAAE,mBAAmB,SAAS,EAAE,QAAQ,MAAM,CAAC;CACrE,MAAM,CAAC,MAAM,GAAG,UAAU,MAAM,KAAK,SACpC,KAAK,SAAS,QAAQ,IAAI,GAAG,IAAI,CAClC;CAIA,MAAM,OAAO,OAAO,OAHP,OACV,MAAM,OAAO,OAAO,SAAS,IAAI,KAAK,OAAO,WAAW,OACxD,GAC6B;CAChC,OAAO,KAAK,UAAU,OAAO,OAAO,IAAI,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,MAAM,CAAC,CAAC;AAC9E;;AASA,eAAe,WACd,SACA,UACA,cACwB;CACxB,MAAM,EAAE,QAAQ,YAAY;CAC5B,MAAM,OAAO,KAAK,KAAK,QAAQ,YAAY;CAC3C,MAAM,QAAQ,KAAK,SAAS,QAAQ,IAAI,GAAG,IAAI;CAG/C,IAAI,QAAQ,YAAY,IAAI;CAE5B,MAAM,WAAW,MADA,eAAe,QACF,CAAC,CAAC,OAAO,OAAO;CAC9C,IAAI,QAAQ,eAAe,QAAQ,KAAK,GAAG;CAE3C,IAAI,CAAC,QAAQ,UAAU;EACtB,MAAM,GAAG,UAAU,MAAM,QAAQ;EACjC,OAAO,EAAE,MAAM;GAAE,MAAM;GAAO,SAAS;EAAS,EAAE;CACnD;CAEA,QAAQ,YAAY,IAAI;CACxB,IAAI;CACJ,IAAI;EACH,SAAS,WAAW,UAAU;GAAE,MAAM;GAAO,QAAQ,QAAQ;EAAO,CAAC;CACtE,SAAS,OAAO;EAEf,MAAM,GAAG,UAAU,MAAM,QAAQ;EACjC,MAAM,IAAI,aAAa,0CAA0C;GAChE,OAAO;GACP,KAAM,MAAmB;EAC1B,CAAC;CACF;CACA,KAAK,MAAM,EAAE,SAAS,SAAS,OAAO,UACrC,IAAI,KAAK,aAAa,SAAS,GAAG,CAAC;CAEpC,IAAI,QACH,GAAG,QAAQ,SAAS,cAAc,YAAY,MAAM,QAAQ,KAAK,GAClE;CAEA,MAAM,GAAG,UAAU,MAAM,OAAO,GAAG;CACnC,OAAO;EAAE,MAAM;GAAE,MAAM;GAAO,SAAS,OAAO;EAAI;EAAG,MAAM,OAAO;CAAK;AACxE;;;;;AAMA,SAAS,cAAoB;CAC5B,MAAM,OAAO,gBAAgB;CAC7B,MAAM,QAAQ,KAAK,SAAS;CAC5B,QAAQ,IACP,KAAK,IAAI,OAAO,QAAQ,KAAK,MAAM,IAAI,UAAU,QAAQ,aAAa,EAAE,GAAG,UAAU,OAAO,IAAI,SAAS,EAAE,IAAI,UAAU,QAAQ,IAAI,OAAO,KAAK,CAAC,GACnJ;AACD;AAEA,SAAS,QAAQ,OAAuB;CACvC,OAAO,UAAU,UAAU,GAAG,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK,EAAE,GAAG;AACxE;;;;;;AAOA,SAAS,YAAY,OAAwB;CAC5C,MAAM,QAAkB,CAAC;CACzB,IAAI,WAAW;CACf,IAAI,UAAmB;CAEvB,OAAO,YAAY,KAAA,GAAW;EAC7B,MAAM,UACL,mBAAmB,QAAQ,QAAQ,UAAU,OAAO,OAAO;EAC5D,IAAI,MAAM,WAAW,KAAK,CAAC,SAAS,SAAS,OAAO,GACnD,MAAM,KACL,GAAG,MAAM,WAAW,IAAI,KAAK,gBAAgB,QAAQ,WAAW,MAAM,MAAM,GAC7E;EAED,WAAW;EACX,UAAU,mBAAmB,QAAQ,QAAQ,QAAQ,KAAA;CACtD;CAEA,OAAO,MAAM,KAAK,MAAM;AACzB"}
@@ -0,0 +1,58 @@
1
+ import { CompiledTemplate } from "../compiler/compile.js";
2
+ import "../compiler/index.js";
3
+ import { XmlInfo } from "../xml/info.js";
4
+ import "../xml/index.js";
5
+ //#region src/cli/stats.d.ts
6
+ export interface WrittenFile {
7
+ /** As shown, relative to the working directory. */
8
+ path: string;
9
+ content: string;
10
+ }
11
+ export interface BuildStats {
12
+ files: {
13
+ path: string;
14
+ size: number;
15
+ gzip: number;
16
+ }[];
17
+ /** What the rendered theme is made of, when it is validated. */
18
+ theme?: {
19
+ path: string;
20
+ scripts: {
21
+ count: number;
22
+ size: number;
23
+ };
24
+ styles: {
25
+ count: number;
26
+ size: number;
27
+ };
28
+ markup: number;
29
+ templates: number;
30
+ partials: number;
31
+ sections: number;
32
+ widgets: number;
33
+ };
34
+ }
35
+ /**
36
+ * The sizes of the files written, and what the theme is made of when it is
37
+ * validated: its scripts and styles from the chunks, its sections and
38
+ * widgets from `processXml`.
39
+ */
40
+ export declare function collectStats(compiled: CompiledTemplate, files: readonly WrittenFile[], theme?: WrittenFile & {
41
+ info: XmlInfo;
42
+ }): Promise<BuildStats>;
43
+ /** `950 B`, `82.4 kB`, `1.20 MB`, in units of 1000 bytes. */
44
+ export declare function formatSize(size: number): string;
45
+ /**
46
+ * The tables of the files written and of what the theme is made of. A size
47
+ * is followed by its change since `previous`, by path.
48
+ */
49
+ export declare function formatStats({ files, theme }: BuildStats, previous?: ReadonlyMap<string, number>,
50
+ /**
51
+ * What the theme was made of at the last build, for the change of every
52
+ * count and size since.
53
+ */
54
+ { previousTheme }?: {
55
+ previousTheme?: BuildStats['theme'];
56
+ }): string;
57
+ //#endregion
58
+ //# sourceMappingURL=stats.d.ts.map
@@ -0,0 +1,156 @@
1
+ import { promisify, styleText } from "node:util";
2
+ import { gzip } from "node:zlib";
3
+ //#region src/cli/stats.ts
4
+ const gzip$1 = promisify(gzip);
5
+ const bytes = (text) => Buffer.byteLength(text);
6
+ /** How much of `theme` the pieces of code take, each piece as often as it is in it. */
7
+ function inlineSize(theme, codes) {
8
+ let size = 0;
9
+ for (const code of new Set([...codes].map((item) => item?.trim()))) if (code) size += (theme.split(code).length - 1) * bytes(code);
10
+ return size;
11
+ }
12
+ /**
13
+ * The sizes of the files written, and what the theme is made of when it is
14
+ * validated: its scripts and styles from the chunks, its sections and
15
+ * widgets from `processXml`.
16
+ */
17
+ async function collectStats(compiled, files, theme) {
18
+ const stats = { files: await Promise.all(files.map(async ({ path, content }) => ({
19
+ path,
20
+ size: bytes(content),
21
+ gzip: (await gzip$1(content)).length
22
+ }))) };
23
+ if (theme === void 0) return stats;
24
+ const chunks = [...compiled.chunks.values()];
25
+ const scripts = chunks.filter((chunk) => chunk.type === "script");
26
+ const styles = chunks.filter((chunk) => chunk.type === "style");
27
+ const runtimes = chunks.filter((chunk) => chunk.type === "runtime");
28
+ const xml = theme.content;
29
+ const scriptSize = inlineSize(xml, [...scripts, ...runtimes].map((chunk) => chunk.code));
30
+ const styleSize = inlineSize(xml, [...styles.map((chunk) => chunk.code), ...scripts.map((chunk) => chunk.css)]);
31
+ stats.theme = {
32
+ path: theme.path,
33
+ scripts: {
34
+ count: scripts.length,
35
+ size: scriptSize
36
+ },
37
+ styles: {
38
+ count: styles.length,
39
+ size: styleSize
40
+ },
41
+ markup: Math.max(0, bytes(xml) - scriptSize - styleSize),
42
+ templates: chunks.filter((chunk) => chunk.type === "template").length,
43
+ partials: compiled.pluginPartials.size,
44
+ sections: theme.info.sections.length,
45
+ widgets: theme.info.widgets.length
46
+ };
47
+ return stats;
48
+ }
49
+ /** `950 B`, `82.4 kB`, `1.20 MB`, in units of 1000 bytes. */
50
+ function formatSize(size) {
51
+ if (size < 1e3) return `${size} B`;
52
+ return size < 1e6 ? `${(size / 1e3).toFixed(1)} kB` : `${(size / 1e6).toFixed(2)} MB`;
53
+ }
54
+ /** `+1.2 kB`, `-12 B`, nothing when it is the same, colored by the table. */
55
+ function formatSizeChange(size, previous) {
56
+ if (previous === void 0 || size === previous) return "";
57
+ const change = size - previous;
58
+ return `${change > 0 ? "+" : "-"}${formatSize(Math.abs(change))}`;
59
+ }
60
+ /** `+1`, `-2`, nothing when it is the same. */
61
+ function formatCountChange(count, previous) {
62
+ if (previous === void 0 || count === previous) return "";
63
+ return `${count > previous ? "+" : "-"}${Math.abs(count - previous)}`;
64
+ }
65
+ /** The cells of a row, its count and size with their changes, empty when it has none. */
66
+ function row(label, count, previousCount, size, previousSize) {
67
+ return [
68
+ label,
69
+ count === void 0 ? "" : String(count),
70
+ count === void 0 ? "" : formatCountChange(count, previousCount),
71
+ size === void 0 ? "" : formatSize(size),
72
+ size === void 0 ? "" : formatSizeChange(size, previousSize)
73
+ ];
74
+ }
75
+ /** `dist/` dim and `theme.xml` cyan, the padding after it as it is. */
76
+ function colorPath(text) {
77
+ const slash = text.lastIndexOf("/") + 1;
78
+ const name = text.slice(slash).trimEnd();
79
+ return styleText("dim", text.slice(0, slash)) + styleText("cyan", name) + text.slice(slash + name.length);
80
+ }
81
+ /**
82
+ * Rows of cells padded to columns, colored once padded. A row ends at its
83
+ * last cell with something in it, so no line ends in spaces, colored or not.
84
+ */
85
+ function table(rows, align, color = (_row, _column, text) => text) {
86
+ const widths = align.map((_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
87
+ return rows.map((row, i) => {
88
+ const last = row.findLastIndex((cell) => cell !== "");
89
+ return ` ${row.slice(0, last + 1).map((cell, column) => {
90
+ if (widths[column] === 0) return;
91
+ const left = align[column] === "left";
92
+ const width = left && column === last ? 0 : widths[column];
93
+ return color(i, column, left ? cell.padEnd(width) : cell.padStart(width));
94
+ }).filter((cell) => cell !== void 0).join(" ")}`;
95
+ });
96
+ }
97
+ /**
98
+ * The tables of the files written and of what the theme is made of. A size
99
+ * is followed by its change since `previous`, by path.
100
+ */
101
+ function formatStats({ files, theme }, previous = /* @__PURE__ */ new Map(), { previousTheme } = {}) {
102
+ const dim = (text) => styleText("dim", text);
103
+ const bold = (text) => styleText("bold", text);
104
+ const change = (text) => styleText(text.trimStart().startsWith("+") ? "yellow" : "green", text);
105
+ const lines = ["", ...table([[
106
+ "Written",
107
+ "Size",
108
+ "Gzip",
109
+ ""
110
+ ], ...files.map(({ path, size, gzip }) => [
111
+ path,
112
+ formatSize(size),
113
+ formatSize(gzip),
114
+ formatSizeChange(size, previous.get(path))
115
+ ])], [
116
+ "left",
117
+ "right",
118
+ "right",
119
+ "left"
120
+ ], (row, column, text) => row === 0 ? dim(text) : [
121
+ colorPath,
122
+ bold,
123
+ dim,
124
+ change
125
+ ][column](text))];
126
+ if (theme) {
127
+ const was = previousTheme;
128
+ const rows = [
129
+ row("Scripts", theme.scripts.count, was?.scripts.count, theme.scripts.size, was?.scripts.size),
130
+ row("Styles", theme.styles.count, was?.styles.count, theme.styles.size, was?.styles.size),
131
+ row("Markup", void 0, void 0, theme.markup, was?.markup),
132
+ row("Templates", theme.templates, was?.templates),
133
+ row("Plugin partials", theme.partials, was?.partials),
134
+ row("Sections", theme.sections, was?.sections),
135
+ row("Widgets", theme.widgets, was?.widgets)
136
+ ];
137
+ lines.push("", ...table([[
138
+ `In ${theme.path}`,
139
+ "Count",
140
+ "",
141
+ "Size",
142
+ ""
143
+ ], ...rows], [
144
+ "left",
145
+ "right",
146
+ "left",
147
+ "right",
148
+ "left"
149
+ ], (row, column, text) => row === 0 ? dim(text) : column === 1 ? bold(text) : column === 2 || column === 4 ? change(text) : text));
150
+ }
151
+ return [...lines, ""].join("\n");
152
+ }
153
+ //#endregion
154
+ export { collectStats, formatSize, formatStats };
155
+
156
+ //# sourceMappingURL=stats.js.map