@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.
- package/README.md +10 -13
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +77 -1
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +39 -29
- package/dist/core/content.d.ts.map +1 -1
- package/dist/core/content.js +9 -7
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +156 -197
- package/dist/core/eta-plugin.d.ts +60 -0
- package/dist/core/eta-plugin.d.ts.map +1 -0
- package/dist/core/eta-plugin.js +143 -0
- package/dist/core/isg/builder.d.ts +37 -19
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +70 -153
- package/dist/core/isg/deps.d.ts +8 -70
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +10 -401
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +2 -1
- package/dist/core/isg/index.d.ts +2 -2
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +3 -3
- package/dist/core/isg/manifest.d.ts.map +1 -1
- package/dist/core/isg/manifest.js +6 -1
- package/dist/core/markdown.d.ts +1 -1
- package/dist/core/markdown.d.ts.map +1 -1
- package/dist/core/markdown.js +3 -2
- package/dist/core/template-index.d.ts +209 -0
- package/dist/core/template-index.d.ts.map +1 -0
- package/dist/core/template-index.js +484 -0
- package/dist/core/template-render.d.ts +137 -0
- package/dist/core/template-render.d.ts.map +1 -0
- package/dist/core/template-render.js +444 -0
- package/dist/core/templates.d.ts +29 -11
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +115 -196
- package/dist/core/utils/context-validation.utils.d.ts +10 -6
- package/dist/core/utils/context-validation.utils.d.ts.map +1 -1
- package/dist/core/utils/context-validation.utils.js +39 -16
- package/dist/core/utils/date.utils.d.ts +1 -1
- package/dist/core/utils/date.utils.js +1 -1
- package/dist/core/utils/dev-css-watcher.utils.d.ts.map +1 -1
- package/dist/core/utils/dev-css-watcher.utils.js +3 -2
- package/dist/core/utils/error-overlay.utils.d.ts.map +1 -1
- package/dist/core/utils/error-overlay.utils.js +7 -45
- package/dist/core/utils/front-matter.utils.d.ts +50 -0
- package/dist/core/utils/front-matter.utils.d.ts.map +1 -0
- package/dist/core/utils/front-matter.utils.js +157 -0
- package/dist/core/utils/index.d.ts +4 -5
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +5 -7
- package/dist/core/utils/paths.utils.d.ts +3 -3
- package/dist/core/utils/paths.utils.js +6 -6
- package/dist/core/utils/tailwind-inventory.utils.d.ts +2 -2
- package/dist/core/utils/tailwind-inventory.utils.d.ts.map +1 -1
- package/dist/core/utils/tailwind-inventory.utils.js +15 -6
- package/dist/core/utils/template-discovery.utils.d.ts +3 -14
- package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
- package/dist/core/utils/template-discovery.utils.js +2 -62
- package/dist/core/utils/template-errors.utils.d.ts +14 -1
- package/dist/core/utils/template-errors.utils.d.ts.map +1 -1
- package/dist/core/utils/template-errors.utils.js +45 -4
- package/dist/core/utils/theme.utils.d.ts +17 -0
- package/dist/core/utils/theme.utils.d.ts.map +1 -0
- package/dist/core/utils/theme.utils.js +59 -0
- package/dist/images/index.d.ts +1 -2
- package/dist/images/index.d.ts.map +1 -1
- package/dist/images/index.js +0 -1
- package/dist/images/markdown-rule.d.ts +1 -2
- package/dist/images/markdown-rule.d.ts.map +1 -1
- package/dist/images/og.d.ts +2 -15
- package/dist/images/og.d.ts.map +1 -1
- package/dist/images/og.js +4 -47
- package/dist/images/runtime.d.ts +24 -5
- package/dist/images/runtime.d.ts.map +1 -1
- package/dist/images/runtime.js +14 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/metrics/noop.d.ts.map +1 -1
- package/dist/metrics/noop.js +4 -1
- package/dist/metrics/recorder.d.ts.map +1 -1
- package/dist/metrics/recorder.js +11 -8
- package/dist/metrics/types.d.ts +31 -5
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/metrics/utils/html-report.utils.d.ts +2 -1
- package/dist/metrics/utils/html-report.utils.d.ts.map +1 -1
- package/dist/metrics/utils/html-report.utils.js +1115 -1136
- package/dist/types/config.d.ts +18 -6
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +83 -16
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/isg.d.ts +48 -2
- package/dist/types/isg.d.ts.map +1 -1
- package/package.json +6 -6
- package/dist/core/utils/callable-partials.utils.d.ts +0 -60
- package/dist/core/utils/callable-partials.utils.d.ts.map +0 -1
- package/dist/core/utils/callable-partials.utils.js +0 -127
- package/dist/core/utils/partial-validation.utils.d.ts +0 -9
- package/dist/core/utils/partial-validation.utils.d.ts.map +0 -1
- 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
|
+
}
|
package/dist/core/templates.d.ts
CHANGED
|
@@ -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
|
|
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;
|
|
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"}
|