blogger-hbs 0.0.0 → 0.0.2

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 (125) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +36 -0
  3. package/bin/blogger-hbs.js +2 -0
  4. package/dist/_virtual/_rolldown/runtime.js +13 -0
  5. package/dist/cli/index.d.ts +1 -0
  6. package/dist/cli/index.js +315 -0
  7. package/dist/cli/index.js.map +1 -0
  8. package/dist/compiler/ast/hash.d.ts +5 -0
  9. package/dist/compiler/ast/hash.js +24 -0
  10. package/dist/compiler/ast/hash.js.map +1 -0
  11. package/dist/compiler/ast/imports.d.ts +25 -0
  12. package/dist/compiler/ast/imports.js +144 -0
  13. package/dist/compiler/ast/imports.js.map +1 -0
  14. package/dist/compiler/ast/index.d.ts +6 -0
  15. package/dist/compiler/ast/index.js +5 -0
  16. package/dist/compiler/ast/partials.d.ts +6 -0
  17. package/dist/compiler/ast/partials.js +14 -0
  18. package/dist/compiler/ast/partials.js.map +1 -0
  19. package/dist/compiler/ast/types.d.ts +122 -0
  20. package/dist/compiler/ast/types.js +0 -0
  21. package/dist/compiler/ast/walk.d.ts +6 -0
  22. package/dist/compiler/ast/walk.js +45 -0
  23. package/dist/compiler/ast/walk.js.map +1 -0
  24. package/dist/compiler/bundle.d.ts +27 -0
  25. package/dist/compiler/bundle.js +70 -0
  26. package/dist/compiler/bundle.js.map +1 -0
  27. package/dist/compiler/chunks.d.ts +37 -0
  28. package/dist/compiler/chunks.js +0 -0
  29. package/dist/compiler/compile.d.ts +48 -0
  30. package/dist/compiler/compile.js +80 -0
  31. package/dist/compiler/compile.js.map +1 -0
  32. package/dist/compiler/dependencies.d.ts +15 -0
  33. package/dist/compiler/dependencies.js +37 -0
  34. package/dist/compiler/dependencies.js.map +1 -0
  35. package/dist/compiler/emit.d.ts +13 -0
  36. package/dist/compiler/emit.js +66 -0
  37. package/dist/compiler/emit.js.map +1 -0
  38. package/dist/compiler/errors.d.ts +6 -0
  39. package/dist/compiler/errors.js +11 -0
  40. package/dist/compiler/errors.js.map +1 -0
  41. package/dist/compiler/index.d.ts +5 -0
  42. package/dist/compiler/index.js +4 -0
  43. package/dist/compiler/options.d.ts +21 -0
  44. package/dist/compiler/options.js +130 -0
  45. package/dist/compiler/options.js.map +1 -0
  46. package/dist/compiler/plugins.d.ts +29 -0
  47. package/dist/compiler/plugins.js +164 -0
  48. package/dist/compiler/plugins.js.map +1 -0
  49. package/dist/compiler/precompile.d.ts +6 -0
  50. package/dist/compiler/precompile.js +20 -0
  51. package/dist/compiler/precompile.js.map +1 -0
  52. package/dist/compiler/rsbuild.d.ts +53 -0
  53. package/dist/compiler/rsbuild.js +273 -0
  54. package/dist/compiler/rsbuild.js.map +1 -0
  55. package/dist/compiler/scan.d.ts +33 -0
  56. package/dist/compiler/scan.js +145 -0
  57. package/dist/compiler/scan.js.map +1 -0
  58. package/dist/compiler/standalone.d.ts +15 -0
  59. package/dist/compiler/standalone.js +64 -0
  60. package/dist/compiler/standalone.js.map +1 -0
  61. package/dist/compiler/validate.d.ts +24 -0
  62. package/dist/compiler/validate.js +147 -0
  63. package/dist/compiler/validate.js.map +1 -0
  64. package/dist/compiler/watch.d.ts +35 -0
  65. package/dist/compiler/watch.js +154 -0
  66. package/dist/compiler/watch.js.map +1 -0
  67. package/dist/config.d.ts +83 -0
  68. package/dist/config.js +96 -0
  69. package/dist/config.js.map +1 -0
  70. package/dist/constants.d.ts +22 -0
  71. package/dist/constants.js +66 -0
  72. package/dist/constants.js.map +1 -0
  73. package/dist/index.d.ts +14 -0
  74. package/dist/index.js +12 -0
  75. package/dist/plugins/index.d.ts +90 -0
  76. package/dist/plugins/index.js +26 -0
  77. package/dist/plugins/index.js.map +1 -0
  78. package/dist/runtime/chunks.d.ts +33 -0
  79. package/dist/runtime/chunks.js +0 -0
  80. package/dist/runtime/helpers/error.d.ts +18 -0
  81. package/dist/runtime/helpers/error.js +22 -0
  82. package/dist/runtime/helpers/error.js.map +1 -0
  83. package/dist/runtime/helpers/functions.d.ts +13 -0
  84. package/dist/runtime/helpers/functions.js +86 -0
  85. package/dist/runtime/helpers/functions.js.map +1 -0
  86. package/dist/runtime/helpers/index.d.ts +3 -0
  87. package/dist/runtime/helpers/index.js +3 -0
  88. package/dist/runtime/import.d.ts +17 -0
  89. package/dist/runtime/import.js +75 -0
  90. package/dist/runtime/import.js.map +1 -0
  91. package/dist/runtime/index.d.ts +5 -0
  92. package/dist/runtime/index.js +3 -0
  93. package/dist/runtime/linebreaks.d.ts +6 -0
  94. package/dist/runtime/linebreaks.js +19 -0
  95. package/dist/runtime/linebreaks.js.map +1 -0
  96. package/dist/runtime/render.d.ts +16 -0
  97. package/dist/runtime/render.js +46 -0
  98. package/dist/runtime/render.js.map +1 -0
  99. package/dist/runtime/tags.d.ts +25 -0
  100. package/dist/runtime/tags.js +77 -0
  101. package/dist/runtime/tags.js.map +1 -0
  102. package/dist/utils/fs.d.ts +10 -0
  103. package/dist/utils/fs.js +15 -0
  104. package/dist/utils/fs.js.map +1 -0
  105. package/dist/utils/path.d.ts +11 -0
  106. package/dist/utils/path.js +19 -0
  107. package/dist/utils/path.js.map +1 -0
  108. package/dist/utils/resolver.d.ts +18 -0
  109. package/dist/utils/resolver.js +81 -0
  110. package/dist/utils/resolver.js.map +1 -0
  111. package/dist/utils/suggest.d.ts +10 -0
  112. package/dist/utils/suggest.js +45 -0
  113. package/dist/utils/suggest.js.map +1 -0
  114. package/dist/xml/expressions.d.ts +28 -0
  115. package/dist/xml/expressions.js +129 -0
  116. package/dist/xml/expressions.js.map +1 -0
  117. package/dist/xml/format.d.ts +34 -0
  118. package/dist/xml/format.js +210 -0
  119. package/dist/xml/format.js.map +1 -0
  120. package/dist/xml/index.d.ts +4 -0
  121. package/dist/xml/index.js +4 -0
  122. package/dist/xml/scanner.d.ts +135 -0
  123. package/dist/xml/scanner.js +415 -0
  124. package/dist/xml/scanner.js.map +1 -0
  125. package/package.json +67 -2
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present Deo Kumar <https://github.com/kumardeo>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # blogger-hbs
2
+
3
+ Build Blogger themes from Handlebars templates, with bundled scripts and styles.
4
+
5
+ > [!WARNING]
6
+ > `blogger-hbs` is experimental. Its API, options and output can change in any release, without a major version bump, and it can have bugs. Pin an exact version and check the output before relying on it.
7
+
8
+ - Split a theme into templates with `{{import "./header.hbs"}}`, data with `{{import "./data.json"}}`.
9
+ - Import scripts and styles, bundled with [Rsbuild](https://rsbuild.rs) and inlined: `{{import "./main.ts"}}`.
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.
12
+
13
+ ## Usage
14
+
15
+ ```sh
16
+ pnpm add -D blogger-hbs
17
+ ```
18
+
19
+ ```ts
20
+ // blogger-hbs.config.ts
21
+ import { defineConfig } from 'blogger-hbs';
22
+
23
+ export default defineConfig({
24
+ entry: 'src/index.hbs',
25
+ alias: { '~': 'src' },
26
+ context: {},
27
+ });
28
+ ```
29
+
30
+ ```sh
31
+ blogger-hbs # build to dist/theme.xml
32
+ blogger-hbs --watch # build again on changes
33
+ blogger-hbs --help # all the options
34
+ ```
35
+
36
+ The same is available from JavaScript with `compileTemplate`, `watchTemplate` and `renderTemplate`.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli/index.js';
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll };
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,315 @@
1
+ import { CompileError } from "../compiler/errors.js";
2
+ import { didYouMean } from "../utils/suggest.js";
3
+ 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";
6
+ import { compileTemplate } from "../compiler/compile.js";
7
+ import { watchTemplate } from "../compiler/watch.js";
8
+ import "../compiler/index.js";
9
+ import { loadConfig } from "../config.js";
10
+ import { renderTemplate } from "../runtime/render.js";
11
+ import "../runtime/index.js";
12
+ import * as path from "node:path";
13
+ import * as fs from "node:fs/promises";
14
+ import { createRequire } from "node:module";
15
+ import { watch } from "node:fs";
16
+ import { styleText } from "node:util";
17
+ import { cac } from "cac";
18
+ //#region src/cli/index.ts
19
+ const { version } = createRequire(import.meta.url)("../../package.json");
20
+ const log = {
21
+ success: (message) => console.log(`${styleText("green", "✓")} ${message}`),
22
+ info: (message) => console.log(`${styleText("cyan", "●")} ${message}`),
23
+ error: (error) => console.error(`${styleText("red", "✗")} ${formatError(error)}`)
24
+ };
25
+ const cli = cac("blogger-hbs");
26
+ 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) => {
27
+ const text = (value) => value === void 0 ? void 0 : String(value);
28
+ const root = text(rootArg);
29
+ const flags = {
30
+ ...rawFlags,
31
+ config: text(rawFlags.config),
32
+ entry: text(rawFlags.entry),
33
+ outDir: text(rawFlags.outDir),
34
+ context: text(rawFlags.context)
35
+ };
36
+ if (flags.watch) await watchBuild(root, flags);
37
+ else await build(root, flags);
38
+ });
39
+ cli.help();
40
+ cli.version(version);
41
+ try {
42
+ cli.parse(process.argv, { run: false });
43
+ await cli.runMatchedCommand();
44
+ } catch (error) {
45
+ log.error(describeCliError(error));
46
+ process.exitCode = 1;
47
+ }
48
+ /** Adds a suggestion and a pointer to `--help` to the errors of the arguments. */
49
+ function describeCliError(error) {
50
+ if (!(error instanceof Error) || error.name !== "CACError") return error;
51
+ const flags = [...cli.commands, cli.globalCommand].flatMap((command) => command.options.flatMap((option) => option.rawName.match(/--[\w-]+/g) ?? []));
52
+ const unknown = /^Unknown option `(.+)`$/.exec(error.message)?.[1];
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`);
55
+ }
56
+ async function setup(root, flags) {
57
+ const cwd = process.cwd();
58
+ const rootFlag = root === void 0 ? void 0 : path.resolve(cwd, root);
59
+ if (rootFlag !== void 0 && !await isDirectory(rootFlag)) throw new CompileError(await exists(rootFlag) ? `Root '${root}' is not a directory` : `Root directory '${root}' does not exist`);
60
+ const loaded = await loadConfig({
61
+ cwd: rootFlag ?? cwd,
62
+ configFile: flags.config && path.resolve(cwd, flags.config),
63
+ watch: flags.watch
64
+ });
65
+ const { config } = loaded;
66
+ const resolvedRoot = rootFlag ?? config.root ?? cwd;
67
+ const output = config.output ?? {};
68
+ const options = {};
69
+ for (const key of COMPILE_OPTION_KEYS) if (key in config) Object.assign(options, { [key]: config[key] });
70
+ options.root = resolvedRoot;
71
+ options.entry = flags.entry ?? config.entry;
72
+ assertCompileOptions(options);
73
+ await assertDirectories(options);
74
+ const outDir = path.resolve(resolvedRoot, flags.outDir ?? output.dir ?? "dist");
75
+ const clean = flags.clean === false ? false : output.clean ?? true;
76
+ if (clean) assertCleanable(outDir, options);
77
+ return {
78
+ options,
79
+ loaded,
80
+ outDir,
81
+ moduleFile: output.module ?? "compiled.js",
82
+ templateFile: output.template ?? "theme.xml",
83
+ validate: flags.validate === false ? false : output.validate ?? true,
84
+ format: flags.format === false ? false : output.format ?? true,
85
+ clean,
86
+ context: flags.context ? await readContext(path.resolve(cwd, flags.context), flags.context) : config.context ?? {},
87
+ publicDir: await resolvePublicDir(resolvedRoot, config.publicDir, outDir)
88
+ };
89
+ }
90
+ /** The public directory, `public` when it exists unless set otherwise. */
91
+ async function resolvePublicDir(root, value, outDir) {
92
+ if (value === false) return;
93
+ const directory = path.resolve(root, value ?? "public");
94
+ const shown = path.relative(process.cwd(), directory) || ".";
95
+ if (!await isDirectory(directory)) {
96
+ if (value === void 0) return;
97
+ throw new CompileError(await exists(directory) ? `Public directory '${shown}' is not a directory` : `Public directory '${shown}' does not exist, set 'publicDir: false' to not copy anything`);
98
+ }
99
+ if (isInside(outDir, directory) || isInside(directory, outDir)) throw new CompileError(`Public directory '${shown}' and output directory '${path.relative(process.cwd(), outDir) || "."}' overlap, they must be separate`);
100
+ return directory;
101
+ }
102
+ /** Copies the public directory into the output directory. */
103
+ async function copyPublic({ publicDir, outDir, moduleFile, templateFile }) {
104
+ if (publicDir === void 0) return;
105
+ for (const name of await fs.readdir(publicDir)) if (name === moduleFile || name === templateFile) throw new CompileError(`'${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"}'`);
106
+ await fs.cp(publicDir, outDir, {
107
+ recursive: true,
108
+ force: true
109
+ });
110
+ }
111
+ /**
112
+ * Cleaning removes everything in the output directory, which must not hold the
113
+ * project: the root, the working directory, the templates or an alias target.
114
+ */
115
+ function assertCleanable(outDir, options) {
116
+ const { root, entry } = resolvePaths(options);
117
+ const shown = path.relative(process.cwd(), outDir) || ".";
118
+ const kept = [
119
+ [root, "the root"],
120
+ [process.cwd(), "the working directory"],
121
+ [path.dirname(entry), "the entry template"],
122
+ ...Object.entries(options.alias ?? {}).map(([name, target]) => [path.resolve(root, target), `the target of the alias '${name}'`])
123
+ ];
124
+ for (const [directory, what] of kept) if (isInside(directory, outDir)) throw new CompileError(`Refusing to clean the output directory '${shown}', it contains ${what}. Use another output directory, or --no-clean / 'output.clean: false' to keep its files`);
125
+ }
126
+ async function readContext(file, shown) {
127
+ let text;
128
+ try {
129
+ text = await fs.readFile(file, "utf-8");
130
+ } catch (cause) {
131
+ if (cause.code === "ENOENT") throw new CompileError(`Context file '${shown}' not found`);
132
+ throw new CompileError(`Failed to read the context file '${shown}'`, { cause });
133
+ }
134
+ try {
135
+ return JSON.parse(text);
136
+ } catch (cause) {
137
+ throw new CompileError(`Context file '${shown}' is not valid JSON: ${cause.message}`);
138
+ }
139
+ }
140
+ /** Whether `child` is `parent` or inside of it. */
141
+ function isInside(child, parent) {
142
+ const relative = path.relative(parent, child);
143
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
144
+ }
145
+ async function exists(file) {
146
+ return fs.stat(file).then(() => true, () => false);
147
+ }
148
+ async function isDirectory(file) {
149
+ return fs.stat(file).then((stats) => stats.isDirectory(), () => false);
150
+ }
151
+ async function build(root, flags) {
152
+ const current = await setup(root, flags);
153
+ await assertEntry(current.options);
154
+ await prepareOutDir(current);
155
+ await copyPublic(current);
156
+ const start = performance.now();
157
+ const compiled = await compileTemplate(current.options);
158
+ log.success(`Compiled in ${elapsed(start)}`);
159
+ await write(current, compiled);
160
+ }
161
+ /** Watches the template and restarts when the config file changes. */
162
+ async function watchBuild(root, flags) {
163
+ let current = await setup(root, flags);
164
+ await prepareOutDir(current);
165
+ let watcher;
166
+ let watchers = [];
167
+ let restarting = false;
168
+ const start = async () => {
169
+ let begin = performance.now();
170
+ let first = true;
171
+ try {
172
+ await copyPublic(current);
173
+ } catch (error) {
174
+ log.error(error);
175
+ }
176
+ watcher = await watchTemplate({
177
+ ...current.options,
178
+ onStart() {
179
+ begin = performance.now();
180
+ },
181
+ async onCompile(compiled) {
182
+ log.success(`${first ? "Compiled" : "Rebuilt"} in ${elapsed(begin)}`);
183
+ first = false;
184
+ await write(current, compiled);
185
+ },
186
+ onError: log.error
187
+ });
188
+ for (const file of current.loaded.dependencies) try {
189
+ const configWatcher = watch(file, () => {
190
+ if (!restarting) {
191
+ restarting = true;
192
+ restart().finally(() => {
193
+ restarting = false;
194
+ });
195
+ }
196
+ });
197
+ configWatcher.on("error", () => configWatcher.close());
198
+ watchers.push(configWatcher);
199
+ } catch {}
200
+ if (current.publicDir !== void 0) watchers.push(watchPublic(current));
201
+ };
202
+ const stopWatchers = () => {
203
+ for (const fileWatcher of watchers) fileWatcher.close();
204
+ watchers = [];
205
+ };
206
+ const restart = async () => {
207
+ log.info(`Config changed, restarting ${styleText("dim", path.relative(process.cwd(), current.loaded.file ?? ""))}`);
208
+ stopWatchers();
209
+ await watcher?.close();
210
+ try {
211
+ current = await setup(root, flags);
212
+ } catch (error) {
213
+ log.error(error);
214
+ }
215
+ await start();
216
+ };
217
+ await start();
218
+ log.info(`Watching for changes ${styleText("dim", "(press Ctrl+C to stop)")}`);
219
+ const stop = async () => {
220
+ stopWatchers();
221
+ await watcher?.close();
222
+ process.exit(0);
223
+ };
224
+ process.once("SIGINT", stop);
225
+ process.once("SIGTERM", stop);
226
+ }
227
+ /** Copies the public directory again when something in it changes. */
228
+ function watchPublic(current) {
229
+ const publicDir = current.publicDir;
230
+ const changed = /* @__PURE__ */ new Set();
231
+ let timer;
232
+ const sync = async () => {
233
+ const names = [...changed];
234
+ changed.clear();
235
+ try {
236
+ for (const name of names) if (!await exists(path.join(publicDir, name))) await fs.rm(path.join(current.outDir, name), {
237
+ recursive: true,
238
+ force: true
239
+ });
240
+ await copyPublic(current);
241
+ log.success(`Copied ${styleText("dim", path.relative(process.cwd(), publicDir))}`);
242
+ } catch (error) {
243
+ log.error(error);
244
+ }
245
+ };
246
+ const publicWatcher = watch(publicDir, { recursive: true }, (_event, name) => {
247
+ if (name) changed.add(name);
248
+ clearTimeout(timer);
249
+ timer = setTimeout(sync, 50);
250
+ });
251
+ publicWatcher.on("error", () => publicWatcher.close());
252
+ publicWatcher.on("close", () => clearTimeout(timer));
253
+ return publicWatcher;
254
+ }
255
+ async function prepareOutDir({ outDir, clean }) {
256
+ if (clean) await fs.rm(outDir, {
257
+ recursive: true,
258
+ force: true
259
+ });
260
+ await fs.mkdir(outDir, { recursive: true });
261
+ }
262
+ /**
263
+ * Writes the compiled module and the rendered template, validated and
264
+ * formatted after rendering when enabled.
265
+ */
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;
270
+ const file = path.join(outDir, templateFile);
271
+ const shown = path.relative(process.cwd(), file);
272
+ let start = performance.now();
273
+ const rendered = renderTemplate({
274
+ ...compiled,
275
+ context,
276
+ plugins: loaded.config.runtimePlugins
277
+ });
278
+ log.success(`Rendered in ${elapsed(start)}`);
279
+ start = performance.now();
280
+ let output;
281
+ try {
282
+ output = finalizeXml(current, rendered);
283
+ } catch (error) {
284
+ await fs.writeFile(file, rendered);
285
+ throw new CompileError(`The rendered template ${shown} is not valid XML: ${error.message}`);
286
+ }
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)}`);
290
+ }
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);
299
+ }
300
+ function elapsed(start) {
301
+ return styleText("yellow", `${Math.round(performance.now() - start)}ms`);
302
+ }
303
+ /** The message of the error and of everything that caused it. */
304
+ function formatError(error) {
305
+ const lines = [];
306
+ let current = error;
307
+ while (current !== void 0) {
308
+ lines.push(current instanceof Error ? `${lines.length === 0 ? "" : "Caused by: "}${current.message}` : String(current));
309
+ current = current instanceof Error ? current.cause : void 0;
310
+ }
311
+ return lines.join("\n ");
312
+ }
313
+ //#endregion
314
+
315
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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\tprocess.exit(0);\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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"}
@@ -0,0 +1,5 @@
1
+ import { BlockStatement, MustacheStatement, SubExpression } from "./types.js";
2
+ //#region src/compiler/ast/hash.d.ts
3
+ export declare function setHashString(node: MustacheStatement | BlockStatement | SubExpression, key: string, value: string): void;
4
+ //#endregion
5
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1,24 @@
1
+ //#region src/compiler/ast/hash.ts
2
+ function setHashString(node, key, value) {
3
+ const { loc } = node;
4
+ node.hash ??= {
5
+ type: "Hash",
6
+ pairs: [],
7
+ loc
8
+ };
9
+ node.hash.pairs.push({
10
+ type: "HashPair",
11
+ key,
12
+ value: {
13
+ type: "StringLiteral",
14
+ value,
15
+ original: value,
16
+ loc
17
+ },
18
+ loc
19
+ });
20
+ }
21
+ //#endregion
22
+ export { setHashString };
23
+
24
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","names":[],"sources":["../../../src/compiler/ast/hash.ts"],"sourcesContent":["import type { BlockStatement, MustacheStatement, SubExpression } from './types';\n\nexport function setHashString(\n\tnode: MustacheStatement | BlockStatement | SubExpression,\n\tkey: string,\n\tvalue: string,\n): void {\n\tconst { loc } = node;\n\tnode.hash ??= { type: 'Hash', pairs: [], loc };\n\tnode.hash.pairs.push({\n\t\ttype: 'HashPair',\n\t\tkey,\n\t\tvalue: { type: 'StringLiteral', value, original: value, loc },\n\t\tloc,\n\t});\n}\n"],"mappings":";AAEA,SAAgB,cACf,MACA,KACA,OACO;CACP,MAAM,EAAE,QAAQ;CAChB,KAAK,SAAS;EAAE,MAAM;EAAQ,OAAO,CAAC;EAAG;CAAI;CAC7C,KAAK,KAAK,MAAM,KAAK;EACpB,MAAM;EACN;EACA,OAAO;GAAE,MAAM;GAAiB;GAAO,UAAU;GAAO;EAAI;EAC5D;CACD,CAAC;AACF"}
@@ -0,0 +1,25 @@
1
+ import { BlockStatement, MustacheStatement, Node, PathExpression, SubExpression } from "./types.js";
2
+ //#region src/compiler/ast/imports.d.ts
3
+ interface Location {
4
+ start: [number, number];
5
+ end: [number, number];
6
+ node: ImportNode;
7
+ }
8
+ export interface ValidImport extends Location {
9
+ valid: true;
10
+ specifier: string;
11
+ dependsOn: string[];
12
+ hashKeys: string[];
13
+ }
14
+ export interface InvalidImport extends Location {
15
+ valid: false;
16
+ reason: string;
17
+ }
18
+ export type Import = ValidImport | InvalidImport;
19
+ export declare function extractImports(node: Node): Import[];
20
+ export type ImportNode = (MustacheStatement | BlockStatement | SubExpression) & {
21
+ path: PathExpression;
22
+ };
23
+ export declare function isImportNode(node: Node): node is ImportNode;
24
+ //#endregion
25
+ //# sourceMappingURL=imports.d.ts.map
@@ -0,0 +1,144 @@
1
+ import { ATTRIBUTE_NAME_REGEX } from "../../constants.js";
2
+ import { walk } from "./walk.js";
3
+ //#region src/compiler/ast/imports.ts
4
+ const ASSET_KEYS = /* @__PURE__ */ new Set(["script", "style"]);
5
+ const ASSET_ATTRIBUTE_REGEX = /^(?:script|style):(.+)$/s;
6
+ /** Options rendering only the script or the styles of an import, raw without a tag. */
7
+ const FLAG_KEYS = /* @__PURE__ */ new Map([
8
+ ["script:raw", {
9
+ tag: "script",
10
+ raw: true
11
+ }],
12
+ ["style:raw", {
13
+ tag: "style",
14
+ raw: true
15
+ }],
16
+ ["script:only", {
17
+ tag: "script",
18
+ raw: false
19
+ }],
20
+ ["style:only", {
21
+ tag: "style",
22
+ raw: false
23
+ }]
24
+ ]);
25
+ function extractImports(node) {
26
+ const imports = [];
27
+ for (const importNode of extractImportNodes(node)) {
28
+ const { params, loc, hash } = importNode;
29
+ const location = {
30
+ start: [loc.start.line, loc.start.column],
31
+ end: [loc.end.line, loc.end.column],
32
+ node: importNode
33
+ };
34
+ const invalid = (reason) => {
35
+ imports.push({
36
+ ...location,
37
+ valid: false,
38
+ reason
39
+ });
40
+ };
41
+ if (params.length === 0) {
42
+ invalid("'import' expects a string literal argument");
43
+ continue;
44
+ }
45
+ if (params.length > 1) {
46
+ invalid("'import' accepts only one argument");
47
+ continue;
48
+ }
49
+ const specifier = params[0];
50
+ if (specifier.type !== "StringLiteral") {
51
+ invalid("First argument to 'import' must be a string literal");
52
+ continue;
53
+ }
54
+ let dependsOn = [];
55
+ const hashKeys = [];
56
+ const seenKeys = /* @__PURE__ */ new Set();
57
+ let reason;
58
+ for (const { key, value } of hash?.pairs || []) {
59
+ if (seenKeys.has(key)) {
60
+ reason = `'import' option '${key}' was specified more than once`;
61
+ break;
62
+ }
63
+ seenKeys.add(key);
64
+ if (key === "depends-on") {
65
+ if (value.type !== "StringLiteral") {
66
+ reason = "'import' 'depends-on' must be a string literal";
67
+ break;
68
+ }
69
+ dependsOn = value.value.split(",").map((item) => item.trim()).filter(Boolean);
70
+ if (dependsOn.length === 0) {
71
+ reason = "'import' 'depends-on' must list at least one script or style";
72
+ break;
73
+ }
74
+ continue;
75
+ }
76
+ if (FLAG_KEYS.has(key)) {
77
+ if (!isFlagValue(value)) {
78
+ reason = `'import' option '${key}' must be true, 'true' or '', got: ${describeValue(value)}`;
79
+ break;
80
+ }
81
+ hashKeys.push(key);
82
+ continue;
83
+ }
84
+ if (ASSET_KEYS.has(key) || isAssetAttribute(key)) {
85
+ hashKeys.push(key);
86
+ continue;
87
+ }
88
+ reason = `'import' received an invalid option '${key}'`;
89
+ break;
90
+ }
91
+ const flag = hashKeys.find((key) => FLAG_KEYS.has(key));
92
+ const mode = flag === void 0 ? void 0 : FLAG_KEYS.get(flag);
93
+ if (reason === void 0 && flag !== void 0 && mode !== void 0) {
94
+ const omitted = mode.tag === "script" ? "style" : "script";
95
+ const conflict = hashKeys.find((key) => key !== flag && (mode.raw || FLAG_KEYS.has(key) || key === omitted || key.startsWith(`${omitted}:`)));
96
+ if (conflict !== void 0) reason = `'import' option '${flag}' cannot be combined with '${conflict}'`;
97
+ }
98
+ if (reason !== void 0) {
99
+ invalid(reason);
100
+ continue;
101
+ }
102
+ imports.push({
103
+ ...location,
104
+ valid: true,
105
+ specifier: specifier.value,
106
+ dependsOn,
107
+ hashKeys
108
+ });
109
+ }
110
+ return imports;
111
+ }
112
+ function extractImportNodes(node) {
113
+ const nodes = [];
114
+ walk(node, (node) => {
115
+ if (isImportNode(node)) nodes.push(node);
116
+ });
117
+ return nodes;
118
+ }
119
+ function isImportNode(node) {
120
+ return (node.type === "MustacheStatement" || node.type === "BlockStatement" || node.type === "SubExpression") && node.path.type === "PathExpression" && node.path.parts.length === 1 && node.path.parts[0] === "import";
121
+ }
122
+ /** `script:raw` / `style:raw` / `script:only` / `style:only` take `true`, `'true'` or an empty string. */
123
+ function isFlagValue(value) {
124
+ return value.type === "BooleanLiteral" && value.value || value.type === "StringLiteral" && (value.value === "true" || value.value === "");
125
+ }
126
+ function describeValue(value) {
127
+ switch (value.type) {
128
+ case "StringLiteral": return `'${value.value}'`;
129
+ case "BooleanLiteral":
130
+ case "NumberLiteral": return String(value.value);
131
+ case "NullLiteral": return "null";
132
+ case "UndefinedLiteral": return "undefined";
133
+ case "PathExpression": return `variable '${value.original}'`;
134
+ case "SubExpression": return `subexpression '${value.path.original}'`;
135
+ }
136
+ }
137
+ function isAssetAttribute(key) {
138
+ const name = ASSET_ATTRIBUTE_REGEX.exec(key)?.[1];
139
+ return name !== void 0 && ATTRIBUTE_NAME_REGEX.test(name);
140
+ }
141
+ //#endregion
142
+ export { extractImports, isImportNode };
143
+
144
+ //# sourceMappingURL=imports.js.map