@stati/core 1.29.2 → 2.0.1

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,484 @@
1
+ /**
2
+ * The template index: every `.eta` file under the source directory, and the name lookup that
3
+ * `stati.include()`, `stati.layout()`, front matter `layout:`, layout discovery and the Open
4
+ * Graph template share, so rendering and the ISG cache resolve every name the same way.
5
+ * @module core/template-index
6
+ */
7
+ import { posix, resolve } from 'node:path';
8
+ import { glob } from 'tinyglobby';
9
+ import { TEMPLATE_EXTENSION } from '../constants.js';
10
+ import { editDistance, findSimilarNames } from './utils/similarity.utils.js';
11
+ /**
12
+ * Normalizes `/`-separated segments, dropping `.` and empty ones.
13
+ *
14
+ * @returns The normalized path, or null when `..` climbs above its start
15
+ */
16
+ function normalizeSegments(path) {
17
+ const segments = [];
18
+ for (const segment of path.split('/')) {
19
+ if (segment === '' || segment === '.')
20
+ continue;
21
+ if (segment === '..') {
22
+ if (segments.length === 0)
23
+ return null;
24
+ segments.pop();
25
+ continue;
26
+ }
27
+ segments.push(segment);
28
+ }
29
+ return segments.join('/');
30
+ }
31
+ /**
32
+ * Parses a name passed to `stati.include()`, `stati.layout()`, `.has()` or front matter `layout:`.
33
+ * Backslashes count as `/`, and an `.eta` suffix is dropped.
34
+ *
35
+ * @param raw - The name as written
36
+ * @param callerFile - The calling template, relative to the source directory, or null for a
37
+ * template without a file (the built-in Open Graph template)
38
+ */
39
+ export function parseTemplateName(raw, callerFile) {
40
+ if (typeof raw !== 'string') {
41
+ return { kind: 'invalid', reason: `must be a string, not ${describeValue(raw)}` };
42
+ }
43
+ let name = raw.replace(/\\/g, '/');
44
+ if (name.endsWith(TEMPLATE_EXTENSION)) {
45
+ name = name.slice(0, -TEMPLATE_EXTENSION.length);
46
+ }
47
+ if (name === '') {
48
+ return { kind: 'invalid', reason: 'is empty' };
49
+ }
50
+ if (/^[a-zA-Z]:/.test(name)) {
51
+ return {
52
+ kind: 'invalid',
53
+ reason: 'is a file system path; use a path inside the source directory',
54
+ };
55
+ }
56
+ if (name.endsWith('/')) {
57
+ return { kind: 'invalid', reason: 'ends with "/"; name a template file' };
58
+ }
59
+ if (name.startsWith('/')) {
60
+ const path = normalizeSegments(name);
61
+ if (path === null) {
62
+ return { kind: 'invalid', reason: 'resolves outside the source directory' };
63
+ }
64
+ if (path === '') {
65
+ return { kind: 'invalid', reason: 'names the source directory, not a template' };
66
+ }
67
+ return { kind: 'path', path: `/${path}`, relative: false };
68
+ }
69
+ if (name === '.' || name === '..' || name.startsWith('./') || name.startsWith('../')) {
70
+ if (callerFile === null) {
71
+ return {
72
+ kind: 'invalid',
73
+ reason: 'is relative, and the built-in Open Graph template has no file to resolve it from; use a path such as "partials/card" instead',
74
+ };
75
+ }
76
+ const path = normalizeSegments(`${posix.dirname(callerFile.replace(/\\/g, '/'))}/${name}`);
77
+ if (path === null) {
78
+ return { kind: 'invalid', reason: 'resolves outside the source directory' };
79
+ }
80
+ if (path === '') {
81
+ return { kind: 'invalid', reason: 'names a folder, not a template' };
82
+ }
83
+ return { kind: 'path', path: `/${path}`, relative: true };
84
+ }
85
+ const normalized = normalizeSegments(name);
86
+ if (normalized === null) {
87
+ return { kind: 'invalid', reason: 'resolves outside the source directory' };
88
+ }
89
+ if (normalized === '') {
90
+ return { kind: 'invalid', reason: 'names a folder, not a template' };
91
+ }
92
+ return { kind: 'lookup', name: normalized };
93
+ }
94
+ function describeValue(value) {
95
+ if (value === null || value === undefined)
96
+ return String(value);
97
+ if (Array.isArray(value))
98
+ return 'an array';
99
+ return typeof value === 'object' ? 'an object' : `${typeof value} ${String(value)}`;
100
+ }
101
+ /**
102
+ * The folder of a file relative to the source directory, `''` for the source directory itself.
103
+ * Paths outside the source directory count as the source directory.
104
+ *
105
+ * @param relativeFile - A path relative to the source directory, with `/` or `\` separators
106
+ */
107
+ export function templateFolderOf(relativeFile) {
108
+ const file = relativeFile.replace(/\\/g, '/');
109
+ if (file.startsWith('../') ||
110
+ file === '..' ||
111
+ posix.isAbsolute(file) ||
112
+ /^[a-zA-Z]:/.test(file)) {
113
+ return '';
114
+ }
115
+ const dir = posix.dirname(file);
116
+ return dir === '.' ? '' : posix.normalize(dir);
117
+ }
118
+ /**
119
+ * The folders a looked-up name is searched in, nearest first: the folder itself, each parent,
120
+ * then the source directory (`''`).
121
+ */
122
+ export function lookupFolders(from) {
123
+ const folders = [];
124
+ let dir = from;
125
+ while (dir !== '') {
126
+ folders.push(dir);
127
+ const parent = posix.dirname(dir);
128
+ dir = parent === '.' ? '' : parent;
129
+ }
130
+ folders.push('');
131
+ return folders;
132
+ }
133
+ /**
134
+ * A `./` or `../` name that reaches `target` from `fromFolder`, both relative to the source
135
+ * directory. Pure string work, unlike `path.relative`, which resolves against the process's
136
+ * working directory.
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * relativeTemplateName('partials', 'partials/icon'); // './icon'
141
+ * relativeTemplateName('blog/2024', 'blog/post'); // '../post'
142
+ * relativeTemplateName('', 'layout'); // './layout'
143
+ * ```
144
+ */
145
+ export function relativeTemplateName(fromFolder, target) {
146
+ const from = fromFolder === '' ? [] : fromFolder.split('/');
147
+ const to = target.split('/');
148
+ let common = 0;
149
+ while (common < from.length && common < to.length - 1 && from[common] === to[common]) {
150
+ common++;
151
+ }
152
+ const rest = to.slice(common).join('/');
153
+ const ups = from.length - common;
154
+ return ups === 0 ? `./${rest}` : `${'../'.repeat(ups)}${rest}`;
155
+ }
156
+ /** The template file for a name in a folder. */
157
+ function candidateFile(folder, name) {
158
+ return folder === '' ? `${name}${TEMPLATE_EXTENSION}` : `${folder}/${name}${TEMPLATE_EXTENSION}`;
159
+ }
160
+ /**
161
+ * Converts an absolute path to the index's form: relative to the source directory, `/`
162
+ * separators. Returns null for files outside it and for files that are not templates.
163
+ *
164
+ * @param srcDir - Absolute source directory
165
+ * @param absolutePath - Absolute path of a file, with either separator
166
+ */
167
+ export function toTemplateFile(srcDir, absolutePath) {
168
+ const base = srcDir.replace(/\\/g, '/').replace(/\/+$/, '');
169
+ const path = absolutePath.replace(/\\/g, '/');
170
+ const caseInsensitive = /^[a-zA-Z]:/.test(base);
171
+ const comparablePath = caseInsensitive ? path.toLowerCase() : path;
172
+ const comparableBase = caseInsensitive ? base.toLowerCase() : base;
173
+ if (!comparablePath.startsWith(`${comparableBase}/`)) {
174
+ return null;
175
+ }
176
+ const relative = posix.normalize(path.slice(base.length + 1));
177
+ if (relative.startsWith('../') || !relative.endsWith(TEMPLATE_EXTENSION)) {
178
+ return null;
179
+ }
180
+ return relative;
181
+ }
182
+ /**
183
+ * Whether a value read from the cache manifest is a list of template lookups.
184
+ */
185
+ export function isTemplateLookupList(value) {
186
+ return (Array.isArray(value) &&
187
+ value.every((item) => {
188
+ if (typeof item !== 'object' || item === null)
189
+ return false;
190
+ const { name, from, file } = item;
191
+ return (typeof name === 'string' &&
192
+ (from === undefined || typeof from === 'string') &&
193
+ (file === null || typeof file === 'string'));
194
+ }));
195
+ }
196
+ /**
197
+ * Every `.eta` file under a source directory, with memoised name lookups.
198
+ * Paths are relative to the source directory, with `/` separators and the `.eta` extension.
199
+ */
200
+ export class TemplateIndex {
201
+ srcDir;
202
+ templates;
203
+ lookupCache = new Map();
204
+ sorted = null;
205
+ /**
206
+ * @param srcDir - Absolute source directory
207
+ * @param files - Template files relative to it
208
+ */
209
+ constructor(srcDir, files) {
210
+ this.srcDir = srcDir;
211
+ this.templates = new Set();
212
+ for (const file of files) {
213
+ this.templates.add(file.replace(/\\/g, '/'));
214
+ }
215
+ }
216
+ /**
217
+ * Globs the source directory once for its templates.
218
+ *
219
+ * @param srcDir - Absolute source directory
220
+ */
221
+ static async load(srcDir) {
222
+ const files = await glob(`**/*${TEMPLATE_EXTENSION}`, {
223
+ cwd: srcDir,
224
+ onlyFiles: true,
225
+ ignore: ['**/node_modules/**'],
226
+ expandDirectories: false,
227
+ });
228
+ return new TemplateIndex(srcDir, files);
229
+ }
230
+ /** Number of templates */
231
+ get size() {
232
+ return this.templates.size;
233
+ }
234
+ /** Whether a template file exists, given relative to the source directory */
235
+ has(file) {
236
+ return this.templates.has(file);
237
+ }
238
+ /** Every template file, sorted */
239
+ files() {
240
+ this.sorted ??= [...this.templates].sort();
241
+ return this.sorted;
242
+ }
243
+ /**
244
+ * Adds a template, for a file created during a dev session.
245
+ *
246
+ * @returns Whether the index changed
247
+ */
248
+ add(file) {
249
+ if (!file.endsWith(TEMPLATE_EXTENSION) || this.templates.has(file)) {
250
+ return false;
251
+ }
252
+ this.templates.add(file);
253
+ this.invalidate();
254
+ return true;
255
+ }
256
+ /**
257
+ * Removes a template, for a file deleted during a dev session.
258
+ *
259
+ * @returns Whether the index changed
260
+ */
261
+ remove(file) {
262
+ if (!this.templates.delete(file)) {
263
+ return false;
264
+ }
265
+ this.invalidate();
266
+ return true;
267
+ }
268
+ invalidate() {
269
+ this.lookupCache.clear();
270
+ this.sorted = null;
271
+ }
272
+ /**
273
+ * Finds the nearest template for a looked-up name, searching `from` and then each parent
274
+ * folder up to the source directory.
275
+ *
276
+ * @param name - A normalized looked-up name, such as `partials/header`
277
+ * @param from - The folder to start in, relative to the source directory
278
+ * @returns The template file, or null when no folder has one
279
+ */
280
+ lookup(name, from) {
281
+ const key = `${from}\0${name}`;
282
+ const cached = this.lookupCache.get(key);
283
+ if (cached !== undefined) {
284
+ return cached;
285
+ }
286
+ let found = null;
287
+ for (const folder of lookupFolders(from)) {
288
+ const file = candidateFile(folder, name);
289
+ if (this.templates.has(file)) {
290
+ found = file;
291
+ break;
292
+ }
293
+ }
294
+ this.lookupCache.set(key, found);
295
+ return found;
296
+ }
297
+ /**
298
+ * Resolves a parsed name for a page.
299
+ *
300
+ * @param name - A parsed, valid name
301
+ * @param pageFolder - The page's folder, where looked-up names start
302
+ * @returns The file (or null) and the lookup to record
303
+ */
304
+ resolve(name, pageFolder) {
305
+ if (name.kind === 'path') {
306
+ const candidate = `${name.path.slice(1)}${TEMPLATE_EXTENSION}`;
307
+ const file = this.templates.has(candidate) ? candidate : null;
308
+ return { file, lookup: { name: name.path, file } };
309
+ }
310
+ const file = this.lookup(name.name, pageFolder);
311
+ return { file, lookup: { name: name.name, from: pageFolder, file } };
312
+ }
313
+ /**
314
+ * Resolves a recorded lookup again, for the cache. Lookups with a `from` folder are searched
315
+ * like looked-up names; the others are a single path.
316
+ *
317
+ * @returns The file the lookup would resolve to now, or null
318
+ */
319
+ resolveRecorded(lookup) {
320
+ if (lookup.from === undefined) {
321
+ const candidate = `${lookup.name.replace(/^\/+/, '')}${TEMPLATE_EXTENSION}`;
322
+ return this.templates.has(candidate) ? candidate : null;
323
+ }
324
+ return this.lookup(lookup.name, lookup.from);
325
+ }
326
+ /**
327
+ * Whether any recorded lookup would now resolve to a different file than it did.
328
+ */
329
+ lookupsChanged(lookups) {
330
+ return (lookups ?? []).some((lookup) => this.resolveRecorded(lookup) !== lookup.file);
331
+ }
332
+ /**
333
+ * Names a template could have meant, for an unknown name: templates with the same base name
334
+ * first, then similar names, written the way the name was written.
335
+ *
336
+ * @param name - The unknown name, parsed
337
+ * @param pageFolder - The page's folder
338
+ * @param callerFile - The calling template, relative to the source directory
339
+ * @param limit - Maximum number of suggestions
340
+ */
341
+ suggest(name, pageFolder, callerFile, limit = 3) {
342
+ const withoutExtension = (file) => file.slice(0, -TEMPLATE_EXTENSION.length);
343
+ const candidates = new Map();
344
+ if (name.kind === 'lookup') {
345
+ // The names this page can reach: each file, relative to each folder it could be found from
346
+ const folders = lookupFolders(pageFolder);
347
+ for (const file of this.files()) {
348
+ for (const folder of folders) {
349
+ if (folder === '' || file.startsWith(`${folder}/`)) {
350
+ const reachable = withoutExtension(folder === '' ? file : file.slice(folder.length + 1));
351
+ if (!candidates.has(reachable))
352
+ candidates.set(reachable, reachable);
353
+ }
354
+ }
355
+ }
356
+ }
357
+ else {
358
+ const callerFolder = callerFile === null ? '' : templateFolderOf(callerFile);
359
+ for (const file of this.files()) {
360
+ const path = `/${withoutExtension(file)}`;
361
+ const written = name.relative
362
+ ? relativeTemplateName(callerFolder, withoutExtension(file))
363
+ : path;
364
+ candidates.set(path, written);
365
+ }
366
+ }
367
+ // A template with the same file name in another folder (a moved file, or a name without
368
+ // its folder), then a misspelt file name in the same folder; nearest first
369
+ const target = name.kind === 'lookup' ? name.name : name.path;
370
+ const targetBase = posix.basename(target);
371
+ const targetFolder = posix.dirname(target);
372
+ const others = [...candidates.keys()].filter((candidate) => candidate !== target);
373
+ const byDistance = (a, b) => editDistance(target, a) - editDistance(target, b) || a.localeCompare(b);
374
+ const sameBase = others
375
+ .filter((candidate) => posix.basename(candidate) === targetBase)
376
+ .sort(byDistance);
377
+ const sameFolder = others.filter((candidate) => posix.dirname(candidate) === targetFolder);
378
+ const similarBases = new Set(findSimilarNames(targetBase, sameFolder.map((candidate) => posix.basename(candidate)), limit));
379
+ const misspelt = sameFolder
380
+ .filter((candidate) => similarBases.has(posix.basename(candidate)))
381
+ .sort(byDistance);
382
+ return [...new Set([...sameBase, ...misspelt])]
383
+ .slice(0, limit)
384
+ .map((candidate) => candidates.get(candidate));
385
+ }
386
+ /**
387
+ * Picks a page's layout: its front matter `layout:` when that names a template, otherwise the
388
+ * nearest `layout.eta`, or for index pages the nearest `index.eta` or `layout.eta`, whichever
389
+ * is in a nearer folder (`index.eta` first within a folder).
390
+ *
391
+ * @param pageFile - The page's Markdown file, relative to the source directory
392
+ * @param frontMatterLayout - The page's front matter `layout:` value
393
+ * @param isIndexPage - Whether the page lists a collection, which enables `index.eta`
394
+ */
395
+ resolvePageLayout(pageFile, frontMatterLayout, isIndexPage) {
396
+ const pageFolder = templateFolderOf(pageFile);
397
+ const lookups = [];
398
+ let unresolvedFrontMatter;
399
+ // YAML reads `layout: 404` as a number
400
+ const layoutName = typeof frontMatterLayout === 'number' ? String(frontMatterLayout) : frontMatterLayout;
401
+ const callerFile = pageFile.replace(/\\/g, '/');
402
+ if (layoutName !== undefined && layoutName !== null && layoutName !== '') {
403
+ const parsed = parseTemplateName(layoutName, callerFile);
404
+ if (parsed.kind === 'invalid') {
405
+ unresolvedFrontMatter = { value: layoutName, problem: parsed.reason, suggestions: [] };
406
+ }
407
+ else {
408
+ const resolved = this.resolve(parsed, pageFolder);
409
+ lookups.push(resolved.lookup);
410
+ if (resolved.file !== null) {
411
+ return { file: resolved.file, lookups };
412
+ }
413
+ unresolvedFrontMatter = {
414
+ value: layoutName,
415
+ problem: 'names no template',
416
+ suggestions: this.suggest(parsed, pageFolder, callerFile),
417
+ };
418
+ }
419
+ }
420
+ const discovered = this.discoverLayout(pageFolder, isIndexPage, lookups);
421
+ return { file: discovered, lookups, ...(unresolvedFrontMatter && { unresolvedFrontMatter }) };
422
+ }
423
+ discoverLayout(pageFolder, isIndexPage, lookups) {
424
+ const layout = this.lookup('layout', pageFolder);
425
+ lookups.push({ name: 'layout', from: pageFolder, file: layout });
426
+ if (!isIndexPage) {
427
+ return layout;
428
+ }
429
+ const index = this.lookup('index', pageFolder);
430
+ lookups.push({ name: 'index', from: pageFolder, file: index });
431
+ if (index === null || layout === null) {
432
+ return index ?? layout;
433
+ }
434
+ // The nearer folder wins; within one folder index.eta comes first
435
+ const depth = (file) => {
436
+ const folder = templateFolderOf(file);
437
+ return folder === '' ? 0 : folder.split('/').length;
438
+ };
439
+ return depth(index) >= depth(layout) ? index : layout;
440
+ }
441
+ }
442
+ /**
443
+ * The index of a dev server session, which its file watcher keeps current as templates are
444
+ * added and deleted, so rebuilds reuse it. It lives outside the cached Eta engine, which every
445
+ * template change drops. Other builds read the source directory each time.
446
+ */
447
+ let session = null;
448
+ /**
449
+ * Starts a dev server session for a source directory: builds reuse one index, and the dev
450
+ * server updates it on file events.
451
+ */
452
+ export function startTemplateIndexSession(srcDir) {
453
+ session = { srcDir: resolve(srcDir), index: null };
454
+ }
455
+ /**
456
+ * Ends the dev server session for a source directory; later builds read it again. A session
457
+ * another dev server started for a different directory is left alone.
458
+ */
459
+ export function endTemplateIndexSession(srcDir) {
460
+ if (session && session.srcDir === resolve(srcDir)) {
461
+ session = null;
462
+ }
463
+ }
464
+ /**
465
+ * Returns the template index for a build: the session's while a dev server watches the
466
+ * source directory, otherwise a new one.
467
+ *
468
+ * @param srcDir - Absolute source directory
469
+ */
470
+ export async function loadTemplateIndex(srcDir) {
471
+ // The session can end while the glob runs, when the dev server stops mid-build
472
+ const current = session;
473
+ if (current && current.srcDir === resolve(srcDir)) {
474
+ current.index ??= await TemplateIndex.load(srcDir);
475
+ return current.index;
476
+ }
477
+ return TemplateIndex.load(srcDir);
478
+ }
479
+ /**
480
+ * The dev server session's index for a source directory, or null when there is none yet.
481
+ */
482
+ export function getCachedTemplateIndex(srcDir) {
483
+ return session && session.srcDir === resolve(srcDir) ? session.index : null;
484
+ }
@@ -0,0 +1,137 @@
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 { type Eta } from 'eta';
7
+ import type { Logger, PageModel, TemplateLookup } from '../types/index.js';
8
+ import { type TemplateIndex } from './template-index.js';
9
+ /**
10
+ * Templates nested deeper than this are an error. It stops a component that includes itself
11
+ * with props from recursing forever; real component trees are far shallower.
12
+ */
13
+ export declare const MAX_TEMPLATE_DEPTH = 100;
14
+ /**
15
+ * The values every template of one render shares, such as `site`, `page` and `helpers`, and an
16
+ * id: the page's layout and its Open Graph template render with different values, so includes
17
+ * cached for one must not be reused by the other.
18
+ */
19
+ export interface BaseContext {
20
+ readonly id: string;
21
+ readonly values: Record<string, unknown>;
22
+ }
23
+ /** What a render scope needs from the page being rendered. */
24
+ export interface RenderScopeOptions {
25
+ page: PageModel;
26
+ eta: Eta;
27
+ index: TemplateIndex;
28
+ logger: Logger;
29
+ /** Development renders throw errors to the error overlay; production renders log them */
30
+ development: boolean;
31
+ }
32
+ /**
33
+ * Everything one page's render shares: the templates and lookups it records, the stack of
34
+ * templates being rendered (for cycles), and the cache of includes rendered without props.
35
+ */
36
+ export declare class RenderScope {
37
+ readonly page: PageModel;
38
+ readonly eta: Eta;
39
+ readonly index: TemplateIndex;
40
+ readonly logger: Logger;
41
+ readonly development: boolean;
42
+ /** The page's Markdown file, relative to the source directory */
43
+ readonly pageFile: string;
44
+ /** The page's folder, relative to the source directory, where looked-up names start */
45
+ readonly pageFolder: string;
46
+ /** Template files the page rendered, relative to the source directory, in order of first use */
47
+ readonly templates: Set<string>;
48
+ /** Name lookups the page made, by folder and name */
49
+ readonly lookups: Map<string, TemplateLookup>;
50
+ /** Templates rendered, counting a template each time it renders */
51
+ renders: number;
52
+ /** Includes served from the page's cache instead of rendering again */
53
+ includeCacheHits: number;
54
+ private readonly stack;
55
+ private readonly includeCache;
56
+ constructor(options: RenderScopeOptions);
57
+ /** Records a lookup for the page's cache entry. */
58
+ record(lookup: TemplateLookup): void;
59
+ /**
60
+ * The rendered template files as absolute POSIX paths, the given layout first.
61
+ *
62
+ * @param layout - The page's layout, relative to the source directory
63
+ */
64
+ templateFiles(layout: string | null): string[];
65
+ /** A template file for messages: its path from the project directory */
66
+ display(file: string | null): string;
67
+ /** An absolute path for messages: its path from the project directory */
68
+ displayPath(absolutePath: string): string;
69
+ /**
70
+ * Renders a template and the parents it names with `stati.layout()`, asynchronously: the
71
+ * page's layout and the Open Graph template, which may use `await`.
72
+ *
73
+ * @param base - The values of the render
74
+ * @param template - The template file, relative to the source directory, or the source of a
75
+ * template without a file
76
+ */
77
+ renderAsync(base: BaseContext, template: {
78
+ file: string;
79
+ } | {
80
+ source: string;
81
+ }): Promise<string>;
82
+ /**
83
+ * Renders a template and the parents it names with `stati.layout()`, synchronously, for
84
+ * `stati.include()`.
85
+ */
86
+ private renderSync;
87
+ private enter;
88
+ private leave;
89
+ private createContext;
90
+ private createInclude;
91
+ private createLayout;
92
+ /** `stati.include.has()` and `stati.layout.has()` */
93
+ private has;
94
+ /**
95
+ * Resolves a name for a call, recording the lookup. Reports invalid and unknown names.
96
+ *
97
+ * @returns The template file, or null after reporting the problem
98
+ */
99
+ private resolveForCall;
100
+ private reportUnknown;
101
+ /** `stati.include(name, props?, body?)` */
102
+ private include;
103
+ /** `stati.layout(name, props?)` */
104
+ private setLayout;
105
+ private checkProps;
106
+ private checkBody;
107
+ private reportCycle;
108
+ /**
109
+ * Reports a mistake at a call: in development as an error for the overlay (Eta adds the
110
+ * calling template and line), in production as a log message.
111
+ */
112
+ private fail;
113
+ /**
114
+ * Turns a failure while rendering `frame` into the error to throw: compile errors become a
115
+ * {@link TemplateError} at the template that failed to compile, with a hint for `await` in
116
+ * a synchronous render, and every error remembers its template for production messages.
117
+ */
118
+ private describeFailure;
119
+ private describeCompileError;
120
+ /** Whether a template that failed to compile synchronously compiles as an async function */
121
+ private compilesAsync;
122
+ private logIncludeFailure;
123
+ }
124
+ /**
125
+ * The template that was rendering when an error was thrown, relative to the source directory.
126
+ * Set for errors that left {@link RenderScope.renderAsync}.
127
+ */
128
+ export declare function failingTemplateOf(error: Error): string | undefined;
129
+ /**
130
+ * Runs a page's render inside its scope.
131
+ */
132
+ export declare function runInRenderScope<T>(scope: RenderScope, render: () => Promise<T>): Promise<T>;
133
+ /**
134
+ * The render scope of the page being rendered, or undefined outside a render.
135
+ */
136
+ export declare function getRenderScope(): RenderScope | undefined;
137
+ //# sourceMappingURL=template-render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-render.d.ts","sourceRoot":"","sources":["../../src/core/template-render.ts"],"names":[],"mappings":"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"}