@stati/core 1.29.2 → 2.0.0

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 (104) hide show
  1. package/README.md +10 -13
  2. package/dist/config/loader.d.ts +9 -0
  3. package/dist/config/loader.d.ts.map +1 -1
  4. package/dist/config/loader.js +77 -1
  5. package/dist/core/build.d.ts.map +1 -1
  6. package/dist/core/build.js +39 -29
  7. package/dist/core/content.d.ts.map +1 -1
  8. package/dist/core/content.js +9 -7
  9. package/dist/core/dev.d.ts.map +1 -1
  10. package/dist/core/dev.js +156 -197
  11. package/dist/core/eta-plugin.d.ts +60 -0
  12. package/dist/core/eta-plugin.d.ts.map +1 -0
  13. package/dist/core/eta-plugin.js +143 -0
  14. package/dist/core/isg/builder.d.ts +37 -19
  15. package/dist/core/isg/builder.d.ts.map +1 -1
  16. package/dist/core/isg/builder.js +70 -153
  17. package/dist/core/isg/deps.d.ts +8 -70
  18. package/dist/core/isg/deps.d.ts.map +1 -1
  19. package/dist/core/isg/deps.js +10 -401
  20. package/dist/core/isg/hash.d.ts.map +1 -1
  21. package/dist/core/isg/hash.js +2 -1
  22. package/dist/core/isg/index.d.ts +2 -2
  23. package/dist/core/isg/index.d.ts.map +1 -1
  24. package/dist/core/isg/index.js +3 -3
  25. package/dist/core/isg/manifest.d.ts.map +1 -1
  26. package/dist/core/isg/manifest.js +6 -1
  27. package/dist/core/markdown.d.ts +1 -1
  28. package/dist/core/markdown.d.ts.map +1 -1
  29. package/dist/core/markdown.js +3 -2
  30. package/dist/core/template-index.d.ts +209 -0
  31. package/dist/core/template-index.d.ts.map +1 -0
  32. package/dist/core/template-index.js +484 -0
  33. package/dist/core/template-render.d.ts +137 -0
  34. package/dist/core/template-render.d.ts.map +1 -0
  35. package/dist/core/template-render.js +444 -0
  36. package/dist/core/templates.d.ts +29 -11
  37. package/dist/core/templates.d.ts.map +1 -1
  38. package/dist/core/templates.js +115 -196
  39. package/dist/core/utils/context-validation.utils.d.ts +10 -6
  40. package/dist/core/utils/context-validation.utils.d.ts.map +1 -1
  41. package/dist/core/utils/context-validation.utils.js +39 -16
  42. package/dist/core/utils/date.utils.d.ts +1 -1
  43. package/dist/core/utils/date.utils.js +1 -1
  44. package/dist/core/utils/dev-css-watcher.utils.d.ts.map +1 -1
  45. package/dist/core/utils/dev-css-watcher.utils.js +3 -2
  46. package/dist/core/utils/error-overlay.utils.d.ts.map +1 -1
  47. package/dist/core/utils/error-overlay.utils.js +7 -45
  48. package/dist/core/utils/front-matter.utils.d.ts +50 -0
  49. package/dist/core/utils/front-matter.utils.d.ts.map +1 -0
  50. package/dist/core/utils/front-matter.utils.js +157 -0
  51. package/dist/core/utils/index.d.ts +4 -5
  52. package/dist/core/utils/index.d.ts.map +1 -1
  53. package/dist/core/utils/index.js +5 -7
  54. package/dist/core/utils/paths.utils.d.ts +3 -3
  55. package/dist/core/utils/paths.utils.js +6 -6
  56. package/dist/core/utils/tailwind-inventory.utils.d.ts +2 -2
  57. package/dist/core/utils/tailwind-inventory.utils.d.ts.map +1 -1
  58. package/dist/core/utils/tailwind-inventory.utils.js +15 -6
  59. package/dist/core/utils/template-discovery.utils.d.ts +3 -14
  60. package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
  61. package/dist/core/utils/template-discovery.utils.js +2 -62
  62. package/dist/core/utils/template-errors.utils.d.ts +14 -1
  63. package/dist/core/utils/template-errors.utils.d.ts.map +1 -1
  64. package/dist/core/utils/template-errors.utils.js +45 -4
  65. package/dist/core/utils/theme.utils.d.ts +17 -0
  66. package/dist/core/utils/theme.utils.d.ts.map +1 -0
  67. package/dist/core/utils/theme.utils.js +59 -0
  68. package/dist/images/index.d.ts +1 -2
  69. package/dist/images/index.d.ts.map +1 -1
  70. package/dist/images/index.js +0 -1
  71. package/dist/images/markdown-rule.d.ts +1 -2
  72. package/dist/images/markdown-rule.d.ts.map +1 -1
  73. package/dist/images/og.d.ts +2 -15
  74. package/dist/images/og.d.ts.map +1 -1
  75. package/dist/images/og.js +4 -47
  76. package/dist/images/runtime.d.ts +24 -5
  77. package/dist/images/runtime.d.ts.map +1 -1
  78. package/dist/images/runtime.js +14 -12
  79. package/dist/index.d.ts +2 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/metrics/noop.d.ts.map +1 -1
  82. package/dist/metrics/noop.js +4 -1
  83. package/dist/metrics/recorder.d.ts.map +1 -1
  84. package/dist/metrics/recorder.js +11 -8
  85. package/dist/metrics/types.d.ts +31 -5
  86. package/dist/metrics/types.d.ts.map +1 -1
  87. package/dist/metrics/utils/html-report.utils.d.ts +2 -1
  88. package/dist/metrics/utils/html-report.utils.d.ts.map +1 -1
  89. package/dist/metrics/utils/html-report.utils.js +1115 -1136
  90. package/dist/types/config.d.ts +18 -6
  91. package/dist/types/config.d.ts.map +1 -1
  92. package/dist/types/content.d.ts +83 -16
  93. package/dist/types/content.d.ts.map +1 -1
  94. package/dist/types/index.d.ts +4 -3
  95. package/dist/types/index.d.ts.map +1 -1
  96. package/dist/types/isg.d.ts +48 -2
  97. package/dist/types/isg.d.ts.map +1 -1
  98. package/package.json +6 -6
  99. package/dist/core/utils/callable-partials.utils.d.ts +0 -60
  100. package/dist/core/utils/callable-partials.utils.d.ts.map +0 -1
  101. package/dist/core/utils/callable-partials.utils.js +0 -127
  102. package/dist/core/utils/partial-validation.utils.d.ts +0 -9
  103. package/dist/core/utils/partial-validation.utils.d.ts.map +0 -1
  104. package/dist/core/utils/partial-validation.utils.js +0 -107
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Template composition: `stati.include()`, `stati.layout()` and `stati.body`, rendered on demand
3
+ * inside a per-page render scope that records every template and name lookup for the ISG cache.
4
+ * @module core/template-render
5
+ */
6
+ import { AsyncLocalStorage } from 'node:async_hooks';
7
+ import { readFileSync } from 'node:fs';
8
+ import { join, posix, relative } from 'node:path';
9
+ import { EtaParseError } from 'eta';
10
+ import { lookupFolders, parseTemplateName, templateFolderOf, } from './template-index.js';
11
+ import { createValidatingContextProxy, parseEtaError, TemplateError, TemplateUsageError, } from './utils/index.js';
12
+ /**
13
+ * Templates nested deeper than this are an error. It stops a component that includes itself
14
+ * with props from recursing forever; real component trees are far shallower.
15
+ */
16
+ export const MAX_TEMPLATE_DEPTH = 100;
17
+ /** Marks a render whose context has no `body` key: the page's layout and the Open Graph template */
18
+ const NO_BODY = Symbol('no body');
19
+ /**
20
+ * The template that failed while rendering, per error, for production log messages: Eta adds
21
+ * the template path to messages only in its debug mode.
22
+ */
23
+ const failingTemplates = new WeakMap();
24
+ /** Errors for `await` in a template that renders synchronously; such an include renders empty */
25
+ const awaitErrors = new WeakSet();
26
+ /**
27
+ * Everything one page's render shares: the templates and lookups it records, the stack of
28
+ * templates being rendered (for cycles), and the cache of includes rendered without props.
29
+ */
30
+ export class RenderScope {
31
+ page;
32
+ eta;
33
+ index;
34
+ logger;
35
+ development;
36
+ /** The page's Markdown file, relative to the source directory */
37
+ pageFile;
38
+ /** The page's folder, relative to the source directory, where looked-up names start */
39
+ pageFolder;
40
+ /** Template files the page rendered, relative to the source directory, in order of first use */
41
+ templates = new Set();
42
+ /** Name lookups the page made, by folder and name */
43
+ lookups = new Map();
44
+ /** Templates rendered, counting a template each time it renders */
45
+ renders = 0;
46
+ /** Includes served from the page's cache instead of rendering again */
47
+ includeCacheHits = 0;
48
+ stack = [];
49
+ includeCache = new Map();
50
+ constructor(options) {
51
+ this.page = options.page;
52
+ this.eta = options.eta;
53
+ this.index = options.index;
54
+ this.logger = options.logger;
55
+ this.development = options.development;
56
+ this.pageFile = relative(options.index.srcDir, options.page.sourcePath).replace(/\\/g, '/');
57
+ this.pageFolder = templateFolderOf(this.pageFile);
58
+ }
59
+ /** Records a lookup for the page's cache entry. */
60
+ record(lookup) {
61
+ this.lookups.set(`${lookup.from ?? ''}\0${lookup.name}`, lookup);
62
+ }
63
+ /**
64
+ * The rendered template files as absolute POSIX paths, the given layout first.
65
+ *
66
+ * @param layout - The page's layout, relative to the source directory
67
+ */
68
+ templateFiles(layout) {
69
+ const files = [...this.templates];
70
+ if (layout !== null && this.templates.has(layout)) {
71
+ files.splice(files.indexOf(layout), 1);
72
+ files.unshift(layout);
73
+ }
74
+ const srcDir = this.index.srcDir.replace(/\\/g, '/');
75
+ return files.map((file) => posix.join(srcDir, file));
76
+ }
77
+ /** A template file for messages: its path from the project directory */
78
+ display(file) {
79
+ if (file === null) {
80
+ return 'the built-in Open Graph template';
81
+ }
82
+ return this.displayPath(join(this.index.srcDir, file)) || file;
83
+ }
84
+ /** An absolute path for messages: its path from the project directory */
85
+ displayPath(absolutePath) {
86
+ return relative(process.cwd(), absolutePath).replace(/\\/g, '/');
87
+ }
88
+ /**
89
+ * Renders a template and the parents it names with `stati.layout()`, asynchronously: the
90
+ * page's layout and the Open Graph template, which may use `await`.
91
+ *
92
+ * @param base - The values of the render
93
+ * @param template - The template file, relative to the source directory, or the source of a
94
+ * template without a file
95
+ */
96
+ async renderAsync(base, template) {
97
+ const chain = [];
98
+ let frame = 'file' in template
99
+ ? { file: template.file, props: undefined, body: NO_BODY }
100
+ : { file: null, source: template.source, props: undefined, body: NO_BODY };
101
+ let output = '';
102
+ try {
103
+ while (frame) {
104
+ this.enter(frame.file, chain);
105
+ const slot = { target: null, calls: 0, closed: false };
106
+ const context = this.createContext(base, frame, slot);
107
+ try {
108
+ const result = frame.file !== null
109
+ ? await this.eta.renderAsync(frame.file, context)
110
+ : await this.eta.renderStringAsync(frame.source, context);
111
+ output = toOutput(result);
112
+ }
113
+ catch (error) {
114
+ throw this.describeFailure(error, frame, false);
115
+ }
116
+ finally {
117
+ slot.closed = true;
118
+ }
119
+ frame = slot.target
120
+ ? { file: slot.target.file, props: slot.target.props, body: output }
121
+ : null;
122
+ }
123
+ }
124
+ finally {
125
+ this.leave(chain);
126
+ }
127
+ return output;
128
+ }
129
+ /**
130
+ * Renders a template and the parents it names with `stati.layout()`, synchronously, for
131
+ * `stati.include()`.
132
+ */
133
+ renderSync(base, first) {
134
+ const chain = [];
135
+ let frame = first;
136
+ let output = '';
137
+ try {
138
+ while (frame) {
139
+ this.enter(frame.file, chain);
140
+ const slot = { target: null, calls: 0, closed: false };
141
+ const context = this.createContext(base, frame, slot);
142
+ try {
143
+ output = toOutput(this.eta.render(frame.file, context));
144
+ }
145
+ catch (error) {
146
+ throw this.describeFailure(error, frame, true);
147
+ }
148
+ finally {
149
+ slot.closed = true;
150
+ }
151
+ frame = slot.target
152
+ ? { file: slot.target.file, props: slot.target.props, body: output }
153
+ : null;
154
+ }
155
+ }
156
+ finally {
157
+ this.leave(chain);
158
+ }
159
+ return output;
160
+ }
161
+ enter(file, chain) {
162
+ this.renders++;
163
+ if (file === null) {
164
+ return;
165
+ }
166
+ this.templates.add(file);
167
+ this.stack.push(file);
168
+ chain.push(file);
169
+ }
170
+ leave(chain) {
171
+ this.stack.splice(this.stack.length - chain.length, chain.length);
172
+ }
173
+ createContext(base, frame, slot) {
174
+ const context = {
175
+ ...base.values,
176
+ props: frame.props,
177
+ include: this.createInclude(base, frame.file),
178
+ layout: this.createLayout(slot, frame.file),
179
+ };
180
+ if (frame.body !== NO_BODY) {
181
+ context.body = frame.body;
182
+ }
183
+ return createValidatingContextProxy(context);
184
+ }
185
+ createInclude(base, callerFile) {
186
+ const include = ((name, props, body) => this.include(base, callerFile, name, props, body));
187
+ include.has = (name) => this.has(callerFile, name);
188
+ return include;
189
+ }
190
+ createLayout(slot, callerFile) {
191
+ const layout = ((name, props) => {
192
+ this.setLayout(slot, callerFile, name, props);
193
+ return '';
194
+ });
195
+ layout.has = (name) => this.has(callerFile, name);
196
+ return layout;
197
+ }
198
+ /** `stati.include.has()` and `stati.layout.has()` */
199
+ has(callerFile, name) {
200
+ const parsed = parseTemplateName(name, callerFile);
201
+ if (parsed.kind === 'invalid') {
202
+ return false;
203
+ }
204
+ const { file, lookup } = this.index.resolve(parsed, this.pageFolder);
205
+ this.record(lookup);
206
+ return file !== null;
207
+ }
208
+ /**
209
+ * Resolves a name for a call, recording the lookup. Reports invalid and unknown names.
210
+ *
211
+ * @returns The template file, or null after reporting the problem
212
+ */
213
+ resolveForCall(call, callerFile, name) {
214
+ const parsed = parseTemplateName(name, callerFile);
215
+ if (parsed.kind === 'invalid') {
216
+ this.fail('error', `${call}(${describeName(name)}): the name ${parsed.reason}.`, []);
217
+ return null;
218
+ }
219
+ const { file, lookup } = this.index.resolve(parsed, this.pageFolder);
220
+ this.record(lookup);
221
+ if (file === null) {
222
+ this.reportUnknown(call, name, parsed, callerFile);
223
+ }
224
+ return file;
225
+ }
226
+ reportUnknown(call, name, parsed, callerFile) {
227
+ const suggestions = this.index.suggest(parsed, this.pageFolder, callerFile);
228
+ const tried = parsed.kind === 'lookup'
229
+ ? lookupFolders(this.pageFolder).map((folder) => this.display(folder === '' ? `${parsed.name}.eta` : `${folder}/${parsed.name}.eta`))
230
+ : [this.display(`${parsed.path.slice(1)}.eta`)];
231
+ const didYouMean = suggestions.length > 0
232
+ ? ` Did you mean ${suggestions.map((suggestion) => `"${suggestion}"`).join(', ')}?`
233
+ : '';
234
+ this.fail('error', `${call}(${describeName(name)}): no template at ${tried.join(' or ')}.${didYouMean}`, suggestions.map((suggestion) => `Did you mean "${suggestion}"?`));
235
+ }
236
+ /** `stati.include(name, props?, body?)` */
237
+ include(base, callerFile, name, props, body) {
238
+ const file = this.resolveForCall('stati.include', callerFile, name);
239
+ if (file === null) {
240
+ return '';
241
+ }
242
+ const checkedProps = this.checkProps('stati.include', name, props);
243
+ const checkedBody = this.checkBody(name, body);
244
+ const cacheable = checkedProps === undefined && checkedBody === undefined;
245
+ const cacheKey = `${base.id}\0${file}`;
246
+ if (cacheable) {
247
+ const cached = this.includeCache.get(cacheKey);
248
+ if (cached !== undefined) {
249
+ this.includeCacheHits++;
250
+ return cached;
251
+ }
252
+ }
253
+ // A template that reaches itself without props renders the same way each time, forever.
254
+ // With props it may be a recursive component, which only the depth limit stops.
255
+ if ((cacheable && this.stack.includes(file)) || this.stack.length >= MAX_TEMPLATE_DEPTH) {
256
+ this.reportCycle('stati.include', file);
257
+ return '';
258
+ }
259
+ let html;
260
+ try {
261
+ html = this.renderSync(base, { file, props: checkedProps, body: checkedBody });
262
+ }
263
+ catch (error) {
264
+ if (this.development) {
265
+ throw error;
266
+ }
267
+ return this.logIncludeFailure(file, error);
268
+ }
269
+ if (cacheable) {
270
+ this.includeCache.set(cacheKey, html);
271
+ }
272
+ return html;
273
+ }
274
+ /** `stati.layout(name, props?)` */
275
+ setLayout(slot, callerFile, name, props) {
276
+ const caller = this.display(callerFile);
277
+ if (slot.closed) {
278
+ this.fail('warning', `stati.layout(${describeName(name)}) was called after ${caller} finished rendering, so it cannot wrap it. Call stati.layout() while the template renders.`, []);
279
+ return;
280
+ }
281
+ slot.calls++;
282
+ if (slot.calls > 1) {
283
+ this.fail('warning', `stati.layout(${describeName(name)}): ${caller} already called stati.layout(), and a template can name one layout. The last call is used.`, ['Call stati.layout() once per template; choose the name with a condition instead']);
284
+ }
285
+ const file = this.resolveForCall('stati.layout', callerFile, name);
286
+ if (file === null) {
287
+ slot.target = null;
288
+ return;
289
+ }
290
+ if (this.stack.includes(file)) {
291
+ this.reportCycle('stati.layout', file);
292
+ slot.target = null;
293
+ return;
294
+ }
295
+ slot.target = { file, props: this.checkProps('stati.layout', name, props) };
296
+ }
297
+ checkProps(call, name, props) {
298
+ if (props === undefined || props === null) {
299
+ return undefined;
300
+ }
301
+ if (typeof props !== 'object' || Array.isArray(props)) {
302
+ this.fail('warning', `${call}(${describeName(name)}, props): props must be an object, not ${describeType(props)}. The template renders without them.`, [`Pass the values as an object, such as ${call}(${describeName(name)}, { value })`]);
303
+ return undefined;
304
+ }
305
+ return props;
306
+ }
307
+ checkBody(name, body) {
308
+ if (body === undefined || body === null) {
309
+ return undefined;
310
+ }
311
+ if (typeof body !== 'string') {
312
+ this.fail('warning', `stati.include(${describeName(name)}, props, body): body must be a string of HTML, not ${describeType(body)}. The template renders without it.`, ['Pass HTML as a string, such as the output of another stati.include()']);
313
+ return undefined;
314
+ }
315
+ return body;
316
+ }
317
+ reportCycle(call, file) {
318
+ const chain = [...this.stack, file].map((template) => this.display(template)).join(' → ');
319
+ const reason = this.stack.length >= MAX_TEMPLATE_DEPTH
320
+ ? `templates are nested more than ${MAX_TEMPLATE_DEPTH} deep`
321
+ : `${this.display(file)} is already rendering`;
322
+ this.fail('error', `${call}(): ${reason}, so the call would never finish: ${chain}. The repeated template is skipped.`, ['Remove the call that leads back to a template that is already rendering']);
323
+ }
324
+ /**
325
+ * Reports a mistake at a call: in development as an error for the overlay (Eta adds the
326
+ * calling template and line), in production as a log message.
327
+ */
328
+ fail(level, message, suggestions) {
329
+ if (this.development) {
330
+ throw new TemplateUsageError(message, suggestions);
331
+ }
332
+ const text = `${message} (page ${this.page.url})`;
333
+ if (level === 'error') {
334
+ this.logger.error(text);
335
+ }
336
+ else {
337
+ this.logger.warning(text);
338
+ }
339
+ }
340
+ /**
341
+ * Turns a failure while rendering `frame` into the error to throw: compile errors become a
342
+ * {@link TemplateError} at the template that failed to compile, with a hint for `await` in
343
+ * a synchronous render, and every error remembers its template for production messages.
344
+ */
345
+ describeFailure(error, frame, sync) {
346
+ const failure = error instanceof Error ? error : new Error(String(error));
347
+ // Eta throws EtaParseError when the template itself fails to compile. Errors from templates
348
+ // it renders reach here wrapped (EtaRuntimeError) or already described.
349
+ if (failure instanceof EtaParseError) {
350
+ const described = this.describeCompileError(failure, frame, sync);
351
+ if (frame.file !== null)
352
+ failingTemplates.set(described, frame.file);
353
+ return described;
354
+ }
355
+ if (frame.file !== null && !failingTemplates.has(failure)) {
356
+ failingTemplates.set(failure, frame.file);
357
+ }
358
+ return failure;
359
+ }
360
+ describeCompileError(failure, frame, sync) {
361
+ const filePath = frame.file !== null ? join(this.index.srcDir, frame.file) : undefined;
362
+ // Eta appends the whole compiled function to syntax errors; the first part is the reason
363
+ const [reason] = failure.message.split(/\n=+\n/);
364
+ // An include renders synchronously: a template that compiles as an async function uses await
365
+ const source = sync && filePath !== undefined ? readTemplate(filePath) : null;
366
+ if (filePath !== undefined && source !== null && this.compilesAsync(source, filePath)) {
367
+ const awaitError = new TemplateError(`${this.display(frame.file)} uses await, but templates rendered by stati.include() render synchronously. Move the awaited work into a build hook or into data a helper prepares.`, filePath, lineOf(source, /\bawait\b/), undefined, undefined, ['Templates rendered by stati.include() cannot use await']);
368
+ awaitErrors.add(awaitError);
369
+ return awaitError;
370
+ }
371
+ const parsed = parseEtaError(new Error(reason ?? failure.message), filePath);
372
+ return new TemplateError(frame.file !== null ? `${this.display(frame.file)}: ${parsed.message}` : parsed.message, filePath, parsed.line, parsed.column, undefined, parsed.suggestions);
373
+ }
374
+ /** Whether a template that failed to compile synchronously compiles as an async function */
375
+ compilesAsync(source, filePath) {
376
+ try {
377
+ this.eta.compile(source, { async: true, filepath: filePath });
378
+ return true;
379
+ }
380
+ catch {
381
+ return false;
382
+ }
383
+ }
384
+ logIncludeFailure(file, error) {
385
+ const failure = error instanceof Error ? error : new Error(String(error));
386
+ const failed = failingTemplates.get(failure) ?? file;
387
+ const message = failure.message;
388
+ this.logger.error(`Error rendering ${this.display(failed)} for ${this.page.url}: ${message}`);
389
+ if (awaitErrors.has(failure)) {
390
+ return '';
391
+ }
392
+ // A comment cannot contain "--", which would let the message end it early
393
+ return `<!-- Error rendering ${this.display(failed)}: ${message.replace(/-(?=-)/g, '- ')} -->`;
394
+ }
395
+ }
396
+ /**
397
+ * The template that was rendering when an error was thrown, relative to the source directory.
398
+ * Set for errors that left {@link RenderScope.renderAsync}.
399
+ */
400
+ export function failingTemplateOf(error) {
401
+ return failingTemplates.get(error);
402
+ }
403
+ /** The render scope of the page being rendered, for code that the template engine calls back. */
404
+ const renderScopeStorage = new AsyncLocalStorage();
405
+ /**
406
+ * Runs a page's render inside its scope.
407
+ */
408
+ export function runInRenderScope(scope, render) {
409
+ return renderScopeStorage.run(scope, render);
410
+ }
411
+ /**
412
+ * The render scope of the page being rendered, or undefined outside a render.
413
+ */
414
+ export function getRenderScope() {
415
+ return renderScopeStorage.getStore();
416
+ }
417
+ function toOutput(result) {
418
+ if (typeof result === 'string')
419
+ return result;
420
+ return result === undefined || result === null ? '' : String(result);
421
+ }
422
+ function describeName(name) {
423
+ return typeof name === 'string' ? `'${name}'` : describeType(name);
424
+ }
425
+ function describeType(value) {
426
+ if (value === null)
427
+ return 'null';
428
+ if (Array.isArray(value))
429
+ return 'an array';
430
+ return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
431
+ }
432
+ function readTemplate(filePath) {
433
+ try {
434
+ return readFileSync(filePath, 'utf-8');
435
+ }
436
+ catch {
437
+ return null;
438
+ }
439
+ }
440
+ /** The 1-based line of the first match, or undefined */
441
+ function lineOf(source, pattern) {
442
+ const match = pattern.exec(source);
443
+ return match ? source.slice(0, match.index).split('\n').length : undefined;
444
+ }
@@ -1,20 +1,33 @@
1
1
  import { Eta } from 'eta';
2
- import type { StatiConfig, PageModel, NavNode, TocEntry, Logger } from '../types/index.js';
2
+ import type { StatiConfig, PageModel, NavNode, TocEntry, Logger, TemplateLookup } from '../types/index.js';
3
3
  import type { PageReads } from './collections.js';
4
4
  import type { PageImages } from '../images/runtime.js';
5
5
  import type { GeneratedOgImage } from '../types/index.js';
6
+ import { TemplateIndex } from './template-index.js';
6
7
  /**
7
8
  * Result of rendering a page template.
8
9
  */
9
10
  export interface RenderResult {
10
11
  /** The rendered HTML content */
11
12
  html: string;
12
- /** Number of templates loaded (layout + partials) */
13
+ /** Number of distinct template files the page rendered */
13
14
  templatesLoaded: number;
15
+ /** Templates rendered, counting a template each time it renders */
16
+ templateRenders: number;
17
+ /** Includes served from the page's cache instead of rendering again */
18
+ includeCacheHits: number;
14
19
  /** Open Graph image generated for the page, if any */
15
20
  ogImage?: GeneratedOgImage | undefined;
16
21
  /** Shared data the render read, which the cache entry records */
17
22
  reads: PageReads;
23
+ /**
24
+ * Every template file the render used, as absolute POSIX paths, the page's layout first:
25
+ * the layout, its parents, the templates it included and the Open Graph template. A failed
26
+ * render lists the templates up to the one that failed.
27
+ */
28
+ templates: string[];
29
+ /** Template names the render looked up, with what each resolved to */
30
+ templateLookups: TemplateLookup[];
18
31
  }
19
32
  /**
20
33
  * Page-specific state for {@link renderPage}.
@@ -22,21 +35,26 @@ export interface RenderResult {
22
35
  export interface RenderPageExtras {
23
36
  /** Image state of the page; without it `stati.image()` emits a plain `<img>` */
24
37
  images?: PageImages | undefined;
38
+ /** The build's template index; without it the source directory is read for this page */
39
+ templateIndex?: TemplateIndex | undefined;
25
40
  }
26
- /**
27
- * Discovers partials in the hierarchy for a given page path.
28
- * Scans all parent directories for folders starting with underscore.
29
- *
30
- * @param pagePath - The path to the page (relative to srcDir), with `/` or `\` separators
31
- * @param config - Stati configuration
32
- * @returns Object mapping partial names to their file paths
33
- */
34
- export declare function discoverPartials(pagePath: string, config: StatiConfig): Promise<Record<string, string>>;
35
41
  /**
36
42
  * Clear the cached template engine. Call this when templates change in dev mode
37
43
  * so the next build recompiles them, or when shutting down a dev session.
38
44
  */
39
45
  export declare function clearTemplateEngineCache(): void;
46
+ /**
47
+ * Creates the template engine: Eta with `stati` as its data variable and Stati's plugin,
48
+ * which replaces Eta's own `include()`, `layout()` and `block()` in templates.
49
+ *
50
+ * @throws {Error} When the installed Eta generates code the plugin does not recognize
51
+ */
40
52
  export declare function createTemplateEngine(config: StatiConfig): Eta;
53
+ /**
54
+ * Renders a page: its layout (chosen by front matter `layout:` or discovery), the parents the
55
+ * layout names with `stati.layout()`, and every template they include, plus the page's Open
56
+ * Graph template when it gets a generated image. Records every template and name lookup for
57
+ * the ISG cache.
58
+ */
41
59
  export declare function renderPage(page: PageModel, body: string, config: StatiConfig, eta: Eta, navigation?: NavNode[], allPages?: PageModel[], assets?: import('../types/index.js').StatiAssets, toc?: TocEntry[], logger?: Logger, extras?: RenderPageExtras): Promise<RenderResult>;
42
60
  //# sourceMappingURL=templates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,OAAO,EAEP,QAAQ,EACR,MAAM,EACP,MAAM,mBAAmB,CAAC;AAuB3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACvC,iEAAiE;IACjE,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AA2CD;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAoDjC;AAcD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CA2B7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,EAChD,GAAG,CAAC,EAAE,QAAQ,EAAE,EAChB,MAAM,CAAC,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,YAAY,CAAC,CA8RvB"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,OAAO,EAEP,QAAQ,EACR,MAAM,EACN,cAAc,EACf,MAAM,mBAAmB,CAAC;AAkB3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,KAAK,EAAsB,UAAU,EAAqB,MAAM,sBAAsB,CAAC;AAC9F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAA6B,MAAM,qBAAqB,CAAC;AAS/E;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,eAAe,EAAE,MAAM,CAAC;IACxB,mEAAmE;IACnE,eAAe,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,gBAAgB,EAAE,MAAM,CAAC;IACzB,sDAAsD;IACtD,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACvC,iEAAiE;IACjE,KAAK,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,sEAAsE;IACtE,eAAe,EAAE,cAAc,EAAE,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAChC,wFAAwF;IACxF,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAC3C;AAuDD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CA6B7D;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,EAChD,GAAG,CAAC,EAAE,QAAQ,EAAE,EAChB,MAAM,CAAC,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,YAAY,CAAC,CAOvB"}