blogger-hbs 0.0.4 → 0.0.6
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/README.md +20 -126
- package/dist/cli/index.js +261 -36
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/stats.d.ts +58 -0
- package/dist/cli/stats.js +156 -0
- package/dist/cli/stats.js.map +1 -0
- package/dist/compiler/chunks.d.ts +3 -1
- package/dist/compiler/compile.d.ts +1 -1
- package/dist/compiler/compile.js +3 -3
- package/dist/compiler/compile.js.map +1 -1
- package/dist/compiler/dependencies.js +2 -1
- package/dist/compiler/dependencies.js.map +1 -1
- package/dist/compiler/errors.d.ts +8 -1
- package/dist/compiler/errors.js +4 -0
- package/dist/compiler/errors.js.map +1 -1
- package/dist/compiler/plugins.js +44 -24
- package/dist/compiler/plugins.js.map +1 -1
- package/dist/compiler/rsbuild.d.ts +2 -2
- package/dist/compiler/rsbuild.js +8 -2
- package/dist/compiler/rsbuild.js.map +1 -1
- package/dist/compiler/scan.d.ts +5 -1
- package/dist/compiler/scan.js +28 -11
- package/dist/compiler/scan.js.map +1 -1
- package/dist/compiler/validate.d.ts +3 -2
- package/dist/compiler/validate.js +16 -7
- package/dist/compiler/validate.js.map +1 -1
- package/dist/compiler/watch.d.ts +6 -3
- package/dist/compiler/watch.js +8 -2
- package/dist/compiler/watch.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/plugins/index.d.ts +19 -4
- package/dist/plugins/index.js +25 -4
- package/dist/plugins/index.js.map +1 -1
- package/dist/runtime/helpers/args.d.ts +26 -0
- package/dist/runtime/helpers/args.js +73 -0
- package/dist/runtime/helpers/args.js.map +1 -0
- package/dist/runtime/helpers/error.d.ts +10 -4
- package/dist/runtime/helpers/error.js +21 -11
- package/dist/runtime/helpers/error.js.map +1 -1
- package/dist/runtime/helpers/functions.d.ts +99 -9
- package/dist/runtime/helpers/functions.js +446 -64
- package/dist/runtime/helpers/functions.js.map +1 -1
- package/dist/runtime/helpers/index.d.ts +2 -2
- package/dist/runtime/helpers/index.js +2 -2
- package/dist/runtime/import.js +11 -9
- package/dist/runtime/import.js.map +1 -1
- package/dist/runtime/index.d.ts +3 -2
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/render.d.ts +1 -1
- package/dist/runtime/render.js +43 -9
- package/dist/runtime/render.js.map +1 -1
- package/dist/runtime/tags.js +7 -6
- package/dist/runtime/tags.js.map +1 -1
- package/dist/xml/expressions.js +48 -17
- package/dist/xml/expressions.js.map +1 -1
- package/dist/xml/format.d.ts +23 -16
- package/dist/xml/format.js +128 -144
- package/dist/xml/format.js.map +1 -1
- package/dist/xml/index.d.ts +3 -2
- package/dist/xml/index.js +2 -2
- package/dist/xml/info.d.ts +30 -0
- package/dist/xml/info.js +46 -0
- package/dist/xml/info.js.map +1 -0
- package/dist/xml/layout.d.ts +39 -0
- package/dist/xml/layout.js +245 -0
- package/dist/xml/layout.js.map +1 -0
- package/dist/xml/process.d.ts +40 -0
- package/dist/xml/process.js +176 -0
- package/dist/xml/process.js.map +1 -0
- package/dist/xml/scanner.d.ts +37 -1
- package/dist/xml/scanner.js +255 -97
- package/dist/xml/scanner.js.map +1 -1
- package/package.json +2 -2
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
|
-
-
|
|
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,139 +19,32 @@ pnpm add -D blogger-hbs
|
|
|
18
19
|
|
|
19
20
|
```ts
|
|
20
21
|
// blogger-hbs.config.ts
|
|
21
|
-
import { defineConfig } from
|
|
22
|
+
import { defineConfig } from "blogger-hbs";
|
|
22
23
|
|
|
23
24
|
export default defineConfig({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
entry: "src/index.hbs",
|
|
26
|
+
alias: { "~": "src" },
|
|
27
|
+
context: {},
|
|
27
28
|
});
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
```sh
|
|
31
|
-
blogger-hbs
|
|
32
|
-
blogger-hbs --watch
|
|
33
|
-
blogger-hbs
|
|
32
|
+
blogger-hbs # build to dist/theme.xml
|
|
33
|
+
blogger-hbs --watch # build again on changes
|
|
34
|
+
blogger-hbs validate theme.xml # validate a theme
|
|
35
|
+
blogger-hbs format theme.xml -o out.xml # format it
|
|
36
|
+
blogger-hbs --help # all the options
|
|
34
37
|
```
|
|
35
38
|
|
|
36
39
|
The same is available from JavaScript with `compileTemplate`, `watchTemplate` and `renderTemplate`.
|
|
37
40
|
|
|
38
|
-
##
|
|
41
|
+
## Documentation
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const renderer = createRenderer(theme);
|
|
49
|
-
|
|
50
|
-
export default {
|
|
51
|
-
async fetch() {
|
|
52
|
-
const html = await renderer.render({ title: 'Blog' });
|
|
53
|
-
return new Response(html, { headers: { 'content-type': 'text/html' } });
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
`createRenderer(theme, { plugins })` takes runtime plugins of the host rendering the theme, for example with data of the request in `prepare`. The helpers of the theme itself are in `compiled.js`, see [Helpers of the theme](#helpers-of-the-theme). `formatXml` and `validateXml` are in `blogger-hbs/runtime` as well. A compiled module is rendered by the version of `blogger-hbs` it is compiled with, `createRenderer` checks it.
|
|
59
|
-
|
|
60
|
-
## Helpers of the theme
|
|
61
|
-
|
|
62
|
-
Helpers are given by the runtime of a plugin, which is bundled into `compiled.js`: the theme renders with them wherever it is rendered. A plugin of the theme itself only needs a name and its runtime.
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
// blogger-hbs.config.ts
|
|
66
|
-
export default defineConfig({
|
|
67
|
-
plugins: [{ name: 'theme', runtime: { entry: './src/runtime.ts' } }],
|
|
68
|
-
});
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
// src/runtime.ts
|
|
73
|
-
import { defineRuntimePlugin } from 'blogger-hbs/runtime';
|
|
74
|
-
|
|
75
|
-
export default defineRuntimePlugin((api) => ({
|
|
76
|
-
helpers: {
|
|
77
|
-
upper: (text: string) => text.toUpperCase(),
|
|
78
|
-
},
|
|
79
|
-
}));
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Plugins
|
|
83
|
-
|
|
84
|
-
A compile plugin provides partials (`{{> name.partial}}`) and data (`{{@name.value}}`) when compiling, and can come with a runtime half for rendering.
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
import { fileURLToPath } from 'node:url';
|
|
88
|
-
import type { CompilePlugin } from 'blogger-hbs';
|
|
89
|
-
|
|
90
|
-
export function myPlugin(options: { prefix: string }): CompilePlugin {
|
|
91
|
-
return {
|
|
92
|
-
name: 'my',
|
|
93
|
-
provide({ requests }, plugin) {
|
|
94
|
-
// every partial used, with where and how: `{{> my.badge size=16}}`
|
|
95
|
-
for (const { name, usages } of requests) {
|
|
96
|
-
for (const usage of usages) {
|
|
97
|
-
if ('color' in usage.hash) {
|
|
98
|
-
plugin.error(`'color' is not a prop of ${name}`, usage);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return Object.fromEntries(
|
|
103
|
-
requests.map(({ name }) => [name, `<span class='${name}'/>`]),
|
|
104
|
-
);
|
|
105
|
-
},
|
|
106
|
-
// bundled into compiled.js, it can not use Node.js modules
|
|
107
|
-
runtime: {
|
|
108
|
-
entry: fileURLToPath(new URL('./runtime.js', import.meta.url)),
|
|
109
|
-
options: { prefix: options.prefix },
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
// runtime.ts, published as runtime.js
|
|
117
|
-
import { defineRuntimePlugin } from 'blogger-hbs/runtime';
|
|
118
|
-
|
|
119
|
-
export default defineRuntimePlugin((api, options: { prefix: string }) => ({
|
|
120
|
-
helpers: {
|
|
121
|
-
greet: (name: string) =>
|
|
122
|
-
new api.SafeString(`${options.prefix} ${api.escapeExpression(name)}`),
|
|
123
|
-
},
|
|
124
|
-
// before every render, can be async, read as `{{@my.posts}}`
|
|
125
|
-
async prepare(context) {
|
|
126
|
-
return { posts: [] };
|
|
127
|
-
},
|
|
128
|
-
// after every render
|
|
129
|
-
transform: (html) => html,
|
|
130
|
-
}));
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
| Hook | When |
|
|
134
|
-
| --- | --- |
|
|
135
|
-
| `buildStart(plugin)` | first, in every compilation, also each time when watching |
|
|
136
|
-
| `preprocess(templates, plugin)` | with every template, their ASTs can be changed |
|
|
137
|
-
| `provide({ partials, requests, templates }, plugin)` | with the partials used, again for those the provided ones use |
|
|
138
|
-
| `finalize({ provided, missing, requests, available }, plugin)` | once, for partials that need all the others |
|
|
139
|
-
| `data({ provided, available, templates }, plugin)` | once, the values are JSON |
|
|
140
|
-
| `buildEnd(plugin, error?)` | last, when the templates are compiled |
|
|
141
|
-
|
|
142
|
-
### Partials of a plugin
|
|
143
|
-
|
|
144
|
-
A plugin `my` provides its main partial and any names separated by single dots:
|
|
145
|
-
|
|
146
|
-
| In a template | Key provided |
|
|
147
|
-
| --- | --- |
|
|
148
|
-
| `{{> my}}`, `{{#> my}}...{{/my}}` | `.` |
|
|
149
|
-
| `{{> my.badge}}` | `badge` |
|
|
150
|
-
| `{{> my.badge.small}}` | `badge.small` |
|
|
151
|
-
|
|
152
|
-
A name is checked as it is written, `{{> my.}}`, `{{> my..badge}}`, `{{> my.badge.}}` and `{{> my/badge}}` are errors where they are, and a theme can not define an inline partial with a name of a plugin, `{{#*inline "my.badge"}}`. Partials of no plugin are left to Handlebars.
|
|
153
|
-
|
|
154
|
-
Every hook gets the plugin context: `root`, `entry`, `mode`, `watch`, `resolve(specifier, importer?)` with the aliases, `addWatchFile(file)`, `warn(message, loc?)` and `error(message, loc?)`. `enforce: 'pre'` or `'post'` runs a plugin before or after the others.
|
|
155
|
-
|
|
156
|
-
The runtime halves of all the plugins are bundled together, a module they share (a dependency of two plugins) is in `compiled.js` once and is the same instance for both. The runtime half is set up with `api` (`SafeString`, `escapeExpression`, `createFrame`, `HelperError`) and the options, which are JSON. Its helpers also work with `renderTemplate`, `prepare` and `transform` need `createRenderer`.
|
|
43
|
+
- [Configuration](./docs/configuration.md): the options of the config and of the CLI.
|
|
44
|
+
- [Templates](./docs/templates.md): importing templates, data, scripts and styles.
|
|
45
|
+
- [Helpers](./docs/helpers.md): the built-in helpers and those of a theme.
|
|
46
|
+
- [Plugins](./docs/plugins.md): partials, data, hooks and runtimes of plugins.
|
|
47
|
+
- [Validation and formatting](./docs/validation.md): what is checked, and how the theme is written.
|
|
48
|
+
- [Rendering anywhere](./docs/rendering.md): `blogger-hbs/runtime` and `createRenderer`.
|
|
49
|
+
- [JavaScript API](./docs/api.md): `compileTemplate`, `watchTemplate` and the others.
|
|
50
|
+
- [Errors](./docs/errors.md): what errors say and how to throw them.
|
package/dist/cli/index.js
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
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 {
|
|
5
|
+
import { XmlError, processXml } from "../xml/process.js";
|
|
5
6
|
import { compileTemplate } from "../compiler/compile.js";
|
|
6
7
|
import { watchTemplate } from "../compiler/watch.js";
|
|
7
8
|
import { loadConfig } from "../config.js";
|
|
8
9
|
import { createRenderer } from "../runtime/render.js";
|
|
10
|
+
import { collectStats, formatStats } from "./stats.js";
|
|
9
11
|
import * as path from "node:path";
|
|
10
12
|
import * as fs from "node:fs/promises";
|
|
11
|
-
import { createRequire } from "node:module";
|
|
12
13
|
import { watch } from "node:fs";
|
|
13
14
|
import { styleText } from "node:util";
|
|
14
15
|
import { cac } from "cac";
|
|
15
16
|
//#region src/cli/index.ts
|
|
16
|
-
const { version } =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
17
|
+
const { version, license, repository } = JSON.parse(await fs.readFile(new URL("../../package.json", import.meta.url), "utf-8"));
|
|
18
|
+
/** What the CLI prints, with `write`, warnings and errors on stderr. */
|
|
19
|
+
function logTo(write) {
|
|
20
|
+
return {
|
|
21
|
+
write,
|
|
22
|
+
success: (message) => write(`${styleText("green", "✓")} ${message}`),
|
|
23
|
+
info: (message) => write(`${styleText("cyan", "●")} ${message}`),
|
|
24
|
+
warn: (message) => console.warn(`${styleText("yellow", "⚠")} ${message}`),
|
|
25
|
+
error: (error) => console.error(`${styleText("red", "✗")} ${formatError(error)}`)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const log = logTo(console.log);
|
|
23
29
|
const cli = cac("blogger-hbs");
|
|
24
30
|
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) => {
|
|
31
|
+
printHeader();
|
|
25
32
|
const text = (value) => value === void 0 ? void 0 : String(value);
|
|
26
33
|
const root = text(rootArg);
|
|
27
34
|
const flags = {
|
|
@@ -34,22 +41,45 @@ cli.command("[root]", "Compile the template and render it").alias("build").optio
|
|
|
34
41
|
if (flags.watch) await watchBuild(root, flags);
|
|
35
42
|
else await build(root, flags);
|
|
36
43
|
});
|
|
44
|
+
cli.command("validate <file>", "Validate a theme as Blogger reads it").option("--json", "Print the result as JSON").example("blogger-hbs validate dist/theme.xml").example("blogger-hbs validate dist/theme.xml --json").action(async (file, flags) => {
|
|
45
|
+
await processFile(String(file), {
|
|
46
|
+
format: false,
|
|
47
|
+
json: flags.json
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
cli.command("format <file>", "Format a theme as Blogger saves it, written to stdout, or to --out").option("-o, --out <file>", "Write it to this file instead of stdout").option("--json", "Print the result as JSON, the theme in it without --out").example("blogger-hbs format theme.xml").example("blogger-hbs format theme.xml -o theme.formatted.xml").action(async (file, flags) => {
|
|
51
|
+
const out = flags.out === void 0 ? void 0 : String(flags.out);
|
|
52
|
+
await processFile(String(file), {
|
|
53
|
+
format: true,
|
|
54
|
+
out,
|
|
55
|
+
json: flags.json
|
|
56
|
+
});
|
|
57
|
+
});
|
|
37
58
|
cli.help();
|
|
38
|
-
cli.version
|
|
59
|
+
cli.globalCommand.versionNumber = version;
|
|
60
|
+
cli.option("-v, --version", "Display the version, the copyright and the license");
|
|
61
|
+
/** Whether the header is printed, so that an error before a command runs prints it too. */
|
|
62
|
+
let headerPrinted = false;
|
|
39
63
|
try {
|
|
40
64
|
cli.parse(process.argv, { run: false });
|
|
41
|
-
|
|
65
|
+
if (cli.options.version) await printVersion();
|
|
66
|
+
else await cli.runMatchedCommand();
|
|
42
67
|
} catch (error) {
|
|
68
|
+
if (!headerPrinted && !cli.options.json) printHeader(formatsToStdout() ? console.error : console.log);
|
|
43
69
|
log.error(describeCliError(error));
|
|
44
70
|
process.exitCode = 1;
|
|
45
71
|
}
|
|
72
|
+
/** Whether the command formats a theme to stdout, which has nothing else on it. */
|
|
73
|
+
function formatsToStdout() {
|
|
74
|
+
return cli.matchedCommandName === "format" && cli.options.out === void 0;
|
|
75
|
+
}
|
|
46
76
|
/** Adds a suggestion and a pointer to `--help` to the errors of the arguments. */
|
|
47
77
|
function describeCliError(error) {
|
|
48
78
|
if (!(error instanceof Error) || error.name !== "CACError") return error;
|
|
49
79
|
const flags = [...cli.commands, cli.globalCommand].flatMap((command) => command.options.flatMap((option) => option.rawName.match(/--[\w-]+/g) ?? []));
|
|
50
80
|
const unknown = /^Unknown option `(.+)`$/.exec(error.message)?.[1];
|
|
51
81
|
const hint = unknown ? didYouMean(unknown, flags, "`") : "";
|
|
52
|
-
return /* @__PURE__ */ new Error(`${error.message}${hint}\
|
|
82
|
+
return /* @__PURE__ */ new Error(`${error.message}${hint}\nRun 'blogger-hbs ${cli.matchedCommandName ? `${cli.matchedCommandName} ` : ""}--help' to see the available options`);
|
|
53
83
|
}
|
|
54
84
|
async function setup(root, flags) {
|
|
55
85
|
const cwd = process.cwd();
|
|
@@ -83,7 +113,8 @@ async function setup(root, flags) {
|
|
|
83
113
|
format: flags.format === false ? false : output.format ?? true,
|
|
84
114
|
clean,
|
|
85
115
|
context: flags.context ? await readContext(path.resolve(cwd, flags.context), flags.context) : config.context ?? {},
|
|
86
|
-
publicDir: await resolvePublicDir(resolvedRoot, config.publicDir, outDir)
|
|
116
|
+
publicDir: await resolvePublicDir(resolvedRoot, config.publicDir, outDir),
|
|
117
|
+
sizes: /* @__PURE__ */ new Map()
|
|
87
118
|
};
|
|
88
119
|
}
|
|
89
120
|
/** The public directory, `public` when it exists unless set otherwise. */
|
|
@@ -155,7 +186,7 @@ async function build(root, flags) {
|
|
|
155
186
|
const start = performance.now();
|
|
156
187
|
const compiled = await compileTemplate(current.options);
|
|
157
188
|
log.success(`Compiled in ${elapsed(start)}`);
|
|
158
|
-
await write(current, compiled);
|
|
189
|
+
await write(current, compiled, start);
|
|
159
190
|
}
|
|
160
191
|
/** Watches the template and restarts when the config file changes. */
|
|
161
192
|
async function watchBuild(root, flags) {
|
|
@@ -167,6 +198,15 @@ async function watchBuild(root, flags) {
|
|
|
167
198
|
const start = async () => {
|
|
168
199
|
let begin = performance.now();
|
|
169
200
|
let first = true;
|
|
201
|
+
/** Shown before what a rebuild prints, after the first build. */
|
|
202
|
+
let divider = "";
|
|
203
|
+
/** Once, before the first thing a rebuild prints, its output can be the same. */
|
|
204
|
+
const printDivider = () => {
|
|
205
|
+
if (divider) {
|
|
206
|
+
console.log(divider);
|
|
207
|
+
divider = "";
|
|
208
|
+
}
|
|
209
|
+
};
|
|
170
210
|
try {
|
|
171
211
|
await copyPublic(current);
|
|
172
212
|
} catch (error) {
|
|
@@ -174,15 +214,20 @@ async function watchBuild(root, flags) {
|
|
|
174
214
|
}
|
|
175
215
|
watcher = await watchTemplate({
|
|
176
216
|
...current.options,
|
|
177
|
-
onStart() {
|
|
217
|
+
onStart({ files }) {
|
|
178
218
|
begin = performance.now();
|
|
219
|
+
divider = first ? "" : formatDivider(files);
|
|
179
220
|
},
|
|
180
221
|
async onCompile(compiled) {
|
|
222
|
+
printDivider();
|
|
181
223
|
log.success(`${first ? "Compiled" : "Rebuilt"} in ${elapsed(begin)}`);
|
|
224
|
+
await write(current, compiled, begin);
|
|
182
225
|
first = false;
|
|
183
|
-
await write(current, compiled);
|
|
184
226
|
},
|
|
185
|
-
onError
|
|
227
|
+
onError(error) {
|
|
228
|
+
printDivider();
|
|
229
|
+
log.error(error);
|
|
230
|
+
}
|
|
186
231
|
});
|
|
187
232
|
for (const file of current.loaded.dependencies) try {
|
|
188
233
|
const configWatcher = watch(file, () => {
|
|
@@ -262,45 +307,225 @@ async function prepareOutDir({ outDir, clean }) {
|
|
|
262
307
|
* Writes the compiled module and the rendered template, validated and
|
|
263
308
|
* formatted after rendering when enabled.
|
|
264
309
|
*/
|
|
265
|
-
async function write(current, compiled) {
|
|
266
|
-
const { outDir, moduleFile, templateFile
|
|
267
|
-
|
|
268
|
-
|
|
310
|
+
async function write(current, compiled, begin) {
|
|
311
|
+
const { outDir, moduleFile, templateFile } = current;
|
|
312
|
+
const written = [];
|
|
313
|
+
let theme;
|
|
314
|
+
if (moduleFile) {
|
|
315
|
+
const file = path.join(outDir, moduleFile);
|
|
316
|
+
await fs.writeFile(file, compiled.source);
|
|
317
|
+
written.push({
|
|
318
|
+
path: path.relative(process.cwd(), file),
|
|
319
|
+
content: compiled.source
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
if (templateFile) {
|
|
323
|
+
theme = await writeTheme(current, compiled, templateFile);
|
|
324
|
+
written.unshift(theme.file);
|
|
325
|
+
}
|
|
326
|
+
const stats = await collectStats(compiled, written, theme?.info && {
|
|
327
|
+
...theme.file,
|
|
328
|
+
info: theme.info
|
|
329
|
+
});
|
|
330
|
+
console.log(formatStats(stats, current.sizes, { previousTheme: current.theme }));
|
|
331
|
+
current.sizes = new Map(stats.files.map(({ path, size }) => [path, size]));
|
|
332
|
+
current.theme = stats.theme;
|
|
333
|
+
log.success(`Built in ${elapsed(begin)}`);
|
|
334
|
+
}
|
|
335
|
+
/** `─── 14:32:05 · src/header.hbs +2 ───────`, dim. */
|
|
336
|
+
function formatDivider(files) {
|
|
337
|
+
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false });
|
|
338
|
+
const [file, ...others] = files.map((item) => path.relative(process.cwd(), item));
|
|
339
|
+
const text = `─── ${time}${file ? ` · ${file}${others.length > 0 ? ` +${others.length}` : ""}` : ""} `;
|
|
340
|
+
return `\n${styleText("dim", text + "─".repeat(Math.max(3, 50 - text.length)))}`;
|
|
341
|
+
}
|
|
342
|
+
/** Renders, validates and formats the theme, as enabled, and writes it. */
|
|
343
|
+
async function writeTheme(current, compiled, templateFile) {
|
|
344
|
+
const { outDir, context } = current;
|
|
269
345
|
const file = path.join(outDir, templateFile);
|
|
270
346
|
const shown = path.relative(process.cwd(), file);
|
|
271
347
|
let start = performance.now();
|
|
272
348
|
const rendered = await createRenderer(compiled).render(context);
|
|
273
349
|
log.success(`Rendered in ${elapsed(start)}`);
|
|
350
|
+
if (!current.validate) {
|
|
351
|
+
await fs.writeFile(file, rendered);
|
|
352
|
+
return { file: {
|
|
353
|
+
path: shown,
|
|
354
|
+
content: rendered
|
|
355
|
+
} };
|
|
356
|
+
}
|
|
274
357
|
start = performance.now();
|
|
275
|
-
let
|
|
358
|
+
let result;
|
|
276
359
|
try {
|
|
277
|
-
|
|
360
|
+
result = processXml(rendered, {
|
|
361
|
+
path: shown,
|
|
362
|
+
format: current.format
|
|
363
|
+
});
|
|
278
364
|
} catch (error) {
|
|
279
365
|
await fs.writeFile(file, rendered);
|
|
280
|
-
throw new CompileError(
|
|
366
|
+
throw new CompileError("The rendered template is not valid XML", {
|
|
367
|
+
cause: error,
|
|
368
|
+
loc: error.loc
|
|
369
|
+
});
|
|
281
370
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
371
|
+
for (const { message, loc } of result.warnings) log.warn(withLocation(message, loc));
|
|
372
|
+
log.success(`${current.format ? "Formatted" : "Validated"} in ${elapsed(start)}`);
|
|
373
|
+
await fs.writeFile(file, result.xml);
|
|
374
|
+
return {
|
|
375
|
+
file: {
|
|
376
|
+
path: shown,
|
|
377
|
+
content: result.xml
|
|
378
|
+
},
|
|
379
|
+
info: result.info
|
|
380
|
+
};
|
|
285
381
|
}
|
|
286
|
-
/**
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Validates the theme in `file`, and with `format` formats it, written to
|
|
384
|
+
* `out` or to stdout. Warnings are placed in what is written, errors in
|
|
385
|
+
* `file`, and nothing is written when it is not valid.
|
|
386
|
+
*/
|
|
387
|
+
async function processFile(file, { format, out, json }) {
|
|
388
|
+
if (json) {
|
|
389
|
+
const report = await reportOf(file, {
|
|
390
|
+
format,
|
|
391
|
+
out
|
|
392
|
+
});
|
|
393
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
394
|
+
if (!report.valid) process.exitCode = 1;
|
|
395
|
+
return;
|
|
292
396
|
}
|
|
293
|
-
|
|
397
|
+
const toStdout = format && out === void 0;
|
|
398
|
+
const print = toStdout ? logTo(console.error) : log;
|
|
399
|
+
if (!toStdout) printHeader(print.write);
|
|
400
|
+
const start = performance.now();
|
|
401
|
+
const { result, written } = await processTheme(file, {
|
|
402
|
+
format,
|
|
403
|
+
out
|
|
404
|
+
});
|
|
405
|
+
for (const { message, loc } of result.warnings) print.warn(withLocation(message, loc));
|
|
406
|
+
if (!format) print.success(`Validated in ${elapsed(start)}`);
|
|
407
|
+
else if (out === void 0) process.stdout.write(result.xml);
|
|
408
|
+
else print.success(`Formatted in ${elapsed(start)}, written to ${written}`);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Reads, processes and, with `out`, writes the theme in `file`. Its
|
|
412
|
+
* warnings and info are placed in what is written, and an error throws.
|
|
413
|
+
*/
|
|
414
|
+
async function processTheme(file, { format, out }) {
|
|
415
|
+
const shown = path.relative(process.cwd(), path.resolve(file));
|
|
416
|
+
const xml = await readTheme(file, shown);
|
|
417
|
+
let result;
|
|
418
|
+
try {
|
|
419
|
+
result = processXml(xml, {
|
|
420
|
+
path: shown,
|
|
421
|
+
format
|
|
422
|
+
});
|
|
423
|
+
} catch (error) {
|
|
424
|
+
throw new Error(`${shown} is not valid XML`, { cause: error });
|
|
425
|
+
}
|
|
426
|
+
const written = !format ? shown : out === void 0 ? "<stdout>" : path.relative(process.cwd(), path.resolve(out));
|
|
427
|
+
const at = (item) => ({
|
|
428
|
+
...item,
|
|
429
|
+
loc: {
|
|
430
|
+
...item.loc,
|
|
431
|
+
path: written
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
result = {
|
|
435
|
+
...result,
|
|
436
|
+
warnings: result.warnings.map(at),
|
|
437
|
+
info: {
|
|
438
|
+
...result.info,
|
|
439
|
+
sections: result.info.sections.map(at),
|
|
440
|
+
widgets: result.info.widgets.map(at)
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
if (format && out !== void 0) {
|
|
444
|
+
await fs.mkdir(path.dirname(path.resolve(out)), { recursive: true });
|
|
445
|
+
await fs.writeFile(out, result.xml);
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
result,
|
|
449
|
+
written
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
async function reportOf(file, { format, out }) {
|
|
453
|
+
try {
|
|
454
|
+
const { result, written } = await processTheme(file, {
|
|
455
|
+
format,
|
|
456
|
+
out
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
valid: true,
|
|
460
|
+
warnings: result.warnings,
|
|
461
|
+
info: result.info,
|
|
462
|
+
...format && out === void 0 && { xml: result.xml },
|
|
463
|
+
...format && out !== void 0 && { written }
|
|
464
|
+
};
|
|
465
|
+
} catch (error) {
|
|
466
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
467
|
+
return {
|
|
468
|
+
valid: false,
|
|
469
|
+
errors: cause instanceof XmlError ? [...cause.errors] : [{ message: messageOf(error) }]
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/** The message of an error and of what caused it, on one line. */
|
|
474
|
+
function messageOf(error) {
|
|
475
|
+
const messages = [];
|
|
476
|
+
for (let current = error; current !== void 0;) {
|
|
477
|
+
messages.push(current instanceof Error ? current.message : String(current));
|
|
478
|
+
current = current instanceof Error ? current.cause : void 0;
|
|
479
|
+
}
|
|
480
|
+
return messages.join(": ");
|
|
481
|
+
}
|
|
482
|
+
/** The text of the theme in `file`, an error saying so when it can not be read. */
|
|
483
|
+
async function readTheme(file, shown) {
|
|
484
|
+
try {
|
|
485
|
+
return await fs.readFile(file, "utf-8");
|
|
486
|
+
} catch (error) {
|
|
487
|
+
throw new Error(`Can not read ${shown}`, { cause: error });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/** The version, the copyright and the license, where the project is, and the runtime. */
|
|
491
|
+
async function printVersion() {
|
|
492
|
+
printHeader();
|
|
493
|
+
const notice = await fs.readFile(new URL("../../LICENSE.md", import.meta.url), "utf-8").catch(() => "");
|
|
494
|
+
const copyright = /^Copyright .*$/m.exec(notice)?.[0];
|
|
495
|
+
console.log([
|
|
496
|
+
copyright,
|
|
497
|
+
`Released under the ${license} License, provided as is, without warranty.`,
|
|
498
|
+
repository.url,
|
|
499
|
+
"",
|
|
500
|
+
`Node.js ${process.version} on ${process.platform}-${process.arch}`
|
|
501
|
+
].filter((line) => line !== void 0).join("\n"));
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* A blank line, `blogger-hbs v0.0.4` right-aligned on a cyan line under it,
|
|
505
|
+
* before what a command prints, with `write`.
|
|
506
|
+
*/
|
|
507
|
+
function printHeader(write = console.log) {
|
|
508
|
+
headerPrinted = true;
|
|
509
|
+
const text = `blogger-hbs v${version}`;
|
|
510
|
+
const width = text.length + 2;
|
|
511
|
+
write(`\n${" ".repeat(width - text.length)}${styleText("bold", "blogger-hbs")} ${styleText("dim", `v${version}`)}\n${styleText("cyan", "─".repeat(width))}`);
|
|
294
512
|
}
|
|
295
513
|
function elapsed(start) {
|
|
296
514
|
return styleText("yellow", `${Math.round(performance.now() - start)}ms`);
|
|
297
515
|
}
|
|
298
|
-
/**
|
|
516
|
+
/**
|
|
517
|
+
* The message of the error and of everything that caused it, a cause whose
|
|
518
|
+
* message is already in the one before is not repeated. The lines of a
|
|
519
|
+
* message are indented under its first.
|
|
520
|
+
*/
|
|
299
521
|
function formatError(error) {
|
|
300
522
|
const lines = [];
|
|
523
|
+
let previous = "";
|
|
301
524
|
let current = error;
|
|
302
525
|
while (current !== void 0) {
|
|
303
|
-
|
|
526
|
+
const message = current instanceof Error ? current.message : String(current);
|
|
527
|
+
if (lines.length === 0 || !previous.includes(message)) lines.push(`${lines.length === 0 ? "" : "Caused by: "}${message.replaceAll("\n", "\n ")}`);
|
|
528
|
+
previous = message;
|
|
304
529
|
current = current instanceof Error ? current.cause : void 0;
|
|
305
530
|
}
|
|
306
531
|
return lines.join("\n ");
|