@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,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stati's Eta plugin. Eta compiles every template with functions of its own: `include()`,
|
|
3
|
+
* `includeAsync()`, `layout()`, `block()` and `blockAsync()`. They resolve names their own way,
|
|
4
|
+
* skip validation and the template index, and a template reached through Eta's `layout()`
|
|
5
|
+
* inherits its child's `stati`. Stati templates compose with `stati.include()` and
|
|
6
|
+
* `stati.layout()`, so the plugin replaces Eta's functions in each compiled template with ones
|
|
7
|
+
* that report the Stati call to use.
|
|
8
|
+
* @module core/eta-plugin
|
|
9
|
+
*/
|
|
10
|
+
import { isDevelopment } from '../env.js';
|
|
11
|
+
import { TemplateUsageError } from './utils/index.js';
|
|
12
|
+
import { getRenderScope } from './template-render.js';
|
|
13
|
+
/**
|
|
14
|
+
* Eta's declaration of its `layout()` function. The plugin checks for it: when Eta generates
|
|
15
|
+
* different code, creating the engine fails instead of templates silently getting Eta's
|
|
16
|
+
* functions back.
|
|
17
|
+
*/
|
|
18
|
+
export const ETA_LAYOUT_DECLARATION = 'function layout(path, data) {\n __eta.layout = path;\n __eta.layoutData = data;\n}';
|
|
19
|
+
/** Eta's template functions that Stati replaces, and the code that declares each one. */
|
|
20
|
+
const ETA_FUNCTIONS = [
|
|
21
|
+
{ name: 'include', declaration: 'let include = ' },
|
|
22
|
+
{ name: 'includeAsync', declaration: 'let includeAsync = ' },
|
|
23
|
+
{ name: 'layout', declaration: ETA_LAYOUT_DECLARATION },
|
|
24
|
+
{ name: 'block', declaration: 'function block(' },
|
|
25
|
+
{ name: 'blockAsync', declaration: 'async function blockAsync(' },
|
|
26
|
+
];
|
|
27
|
+
/** Marks the start of the template's own code in the compiled function. */
|
|
28
|
+
const BODY_START = '/*@stati:template-body@*/';
|
|
29
|
+
/** Property of the engine through which compiled templates report a blocked call. */
|
|
30
|
+
const BLOCKED_CALL_HANDLER = '__statiBlockedEtaCall';
|
|
31
|
+
/**
|
|
32
|
+
* Creates the plugin. The replacements go at the start of the template's code rather than
|
|
33
|
+
* next to Eta's declarations, because with `debug` on Eta declares `block()` inside a `try`
|
|
34
|
+
* block, where a replacement made outside it would be shadowed. The template's code then runs
|
|
35
|
+
* in a block of its own, so it can still declare names such as `block` or `output`, which
|
|
36
|
+
* would otherwise clash with Eta's declarations.
|
|
37
|
+
*/
|
|
38
|
+
export function createStatiEtaPlugin() {
|
|
39
|
+
return {
|
|
40
|
+
processAST(ast) {
|
|
41
|
+
const lineNo = ast.reduce((line, node) => typeof node === 'object' && node.lineNo !== undefined ? node.lineNo : line, 1);
|
|
42
|
+
const start = { t: 'e', val: BODY_START, lineNo: 1 };
|
|
43
|
+
const end = { t: 'e', val: '}', lineNo };
|
|
44
|
+
return [start, ...ast, end];
|
|
45
|
+
},
|
|
46
|
+
processFnString(fnString) {
|
|
47
|
+
if (!fnString.includes(ETA_LAYOUT_DECLARATION) || !fnString.includes(BODY_START)) {
|
|
48
|
+
throw new Error("Stati could not find Eta's layout() function in a compiled template, so it cannot block Eta's own include(), layout() and block(). This Eta version is not supported; install the version @stati/core depends on.");
|
|
49
|
+
}
|
|
50
|
+
// The names are the identifiers listed in ETA_FUNCTIONS, so they need no escaping
|
|
51
|
+
const replacements = ETA_FUNCTIONS.filter(({ declaration }) => fnString.includes(declaration))
|
|
52
|
+
.map(({ name }) => `${name} = (...__statiArgs) => this.${BLOCKED_CALL_HANDLER}('${name}', __statiArgs, options && options.filepath);`)
|
|
53
|
+
.join(' ');
|
|
54
|
+
// With debug on, Eta marks each tag's line with `__eta.line=N` and no semicolon, so a tag
|
|
55
|
+
// that starts with `(`, `[` or a template literal would continue that statement
|
|
56
|
+
return fnString
|
|
57
|
+
.replace(BODY_START, `${replacements} {`)
|
|
58
|
+
.replace(/__eta\.line=(\d+)\n/g, '__eta.line=$1;\n');
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Installs the handler that the replaced functions call, and compiles an empty template so
|
|
64
|
+
* that an Eta version the plugin does not match fails here rather than on the first page.
|
|
65
|
+
*
|
|
66
|
+
* @throws {Error} When Eta's generated code lacks the declarations the plugin replaces
|
|
67
|
+
*/
|
|
68
|
+
export function installEtaPlugin(eta) {
|
|
69
|
+
eta[BLOCKED_CALL_HANDLER] = handleBlockedCall;
|
|
70
|
+
eta.compileToString('', { async: false });
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The Stati call to suggest for a name passed to one of Eta's functions. It keeps Eta's
|
|
74
|
+
* resolution: a name without a leading `/`, `./` or `../` is relative to the calling template.
|
|
75
|
+
*/
|
|
76
|
+
function suggestedName(name) {
|
|
77
|
+
if (typeof name !== 'string') {
|
|
78
|
+
return 'path';
|
|
79
|
+
}
|
|
80
|
+
let path = name.replace(/\\/g, '/');
|
|
81
|
+
if (path.endsWith('.eta')) {
|
|
82
|
+
path = path.slice(0, -'.eta'.length);
|
|
83
|
+
}
|
|
84
|
+
if (!path.startsWith('/') && !path.startsWith('./') && !path.startsWith('../')) {
|
|
85
|
+
path = `./${path}`;
|
|
86
|
+
}
|
|
87
|
+
// Escaped for the single-quoted string of the suggested call
|
|
88
|
+
return path.replace(/[\\']/g, '\\$&');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The message for a blocked call, and the replacement to suggest.
|
|
92
|
+
*/
|
|
93
|
+
export function describeBlockedCall(name, args) {
|
|
94
|
+
const withData = args.length > 1 && args[1] !== undefined;
|
|
95
|
+
const target = suggestedName(args[0]);
|
|
96
|
+
switch (name) {
|
|
97
|
+
case 'include':
|
|
98
|
+
case 'includeAsync': {
|
|
99
|
+
const suggestion = `stati.include('${target}'${withData ? ', data' : ''})`;
|
|
100
|
+
const sync = name === 'includeAsync'
|
|
101
|
+
? '; templates rendered by stati.include() render synchronously'
|
|
102
|
+
: '';
|
|
103
|
+
return {
|
|
104
|
+
message: `Eta's ${name}() is not available in Stati templates. Use ${suggestion} instead${sync}.`,
|
|
105
|
+
suggestion: `Use ${suggestion}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
case 'layout': {
|
|
109
|
+
const suggestion = `stati.layout('${target}'${withData ? ', data' : ''})`;
|
|
110
|
+
return {
|
|
111
|
+
message: `Eta's layout() is not available in Stati templates. Use ${suggestion} instead; the data arrives as stati.props.`,
|
|
112
|
+
suggestion: `Use ${suggestion}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
case 'block':
|
|
116
|
+
case 'blockAsync':
|
|
117
|
+
return {
|
|
118
|
+
message: `Eta's ${name}() is not available in Stati templates. Pass the content to the layout as a prop instead, such as stati.layout('base', { sidebar: html }).`,
|
|
119
|
+
suggestion: 'Pass the content to the layout as a prop',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Called by a compiled template for one of Eta's blocked functions. Development throws, so the
|
|
125
|
+
* error overlay points at the call; production logs the error and the call renders nothing.
|
|
126
|
+
*/
|
|
127
|
+
function handleBlockedCall(name, args, filepath) {
|
|
128
|
+
const { message, suggestion } = describeBlockedCall(name, args);
|
|
129
|
+
if (isDevelopment()) {
|
|
130
|
+
throw new TemplateUsageError(message, [suggestion]);
|
|
131
|
+
}
|
|
132
|
+
const scope = getRenderScope();
|
|
133
|
+
const where = filepath ? `${scope ? scope.displayPath(filepath) : filepath}: ` : '';
|
|
134
|
+
const page = scope ? ` (page ${scope.page.url})` : '';
|
|
135
|
+
const text = `${where}${message}${page}`;
|
|
136
|
+
if (scope) {
|
|
137
|
+
scope.logger.error(text);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.error(text);
|
|
141
|
+
}
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { PageModel, CacheEntry, StatiConfig } from '../../types/index.js';
|
|
1
|
+
import type { PageModel, CacheEntry, StatiConfig, TemplateLookup } from '../../types/index.js';
|
|
2
2
|
import type { PageReads } from '../collections.js';
|
|
3
|
+
import { TemplateIndex } from '../template-index.js';
|
|
3
4
|
/**
|
|
4
|
-
* What a render discovered: the
|
|
5
|
-
* images
|
|
5
|
+
* What a render discovered: the templates it used (`templates`, `templateLookups`), the source
|
|
6
|
+
* images a page displays (`assetDeps`), the generated images it references (`images`) and the
|
|
7
|
+
* shared data it read (`reads`).
|
|
6
8
|
*/
|
|
7
9
|
export interface RenderedAssets {
|
|
8
10
|
/** Absolute paths of files whose content affects the page, such as source images */
|
|
@@ -11,6 +13,10 @@ export interface RenderedAssets {
|
|
|
11
13
|
images: readonly string[];
|
|
12
14
|
/** Whether the render read the collection and the navigation tree */
|
|
13
15
|
reads?: PageReads | undefined;
|
|
16
|
+
/** Template files the render used, absolute POSIX paths, the page's layout first */
|
|
17
|
+
templates?: readonly string[] | undefined;
|
|
18
|
+
/** Template names the render looked up, with what each resolved to */
|
|
19
|
+
templateLookups?: readonly TemplateLookup[] | undefined;
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
16
22
|
* Whether a cache entry records what its page read while rendering. Entries written before
|
|
@@ -22,10 +28,23 @@ export interface RenderedAssets {
|
|
|
22
28
|
* @returns True when `readsCollection` and `readsNav` are both recorded
|
|
23
29
|
*/
|
|
24
30
|
export declare function hasRecordedReads(entry: CacheEntry): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether a cache entry's templates and name lookups were recorded while its page rendered.
|
|
33
|
+
* An entry without them cannot tell when a template change affects its page, so the page is
|
|
34
|
+
* rebuilt once to record them.
|
|
35
|
+
*
|
|
36
|
+
* @param entry - The cache entry
|
|
37
|
+
* @returns True when `templatesRecorded` is set
|
|
38
|
+
*/
|
|
39
|
+
export declare function hasRecordedTemplates(entry: CacheEntry): boolean;
|
|
25
40
|
/**
|
|
26
41
|
* Determines if a page should be rebuilt based on ISG logic.
|
|
27
|
-
* Checks content changes,
|
|
28
|
-
* Handles edge cases like missing dates
|
|
42
|
+
* Checks content changes, the templates the last render recorded, the page's layout, TTL
|
|
43
|
+
* expiration, and freeze status. Handles edge cases like missing dates and corrupted entries.
|
|
44
|
+
*
|
|
45
|
+
* A cached page is stale when a template it rendered changed or no longer exists, when its
|
|
46
|
+
* layout would now be a different file, or when a template name it looked up would now
|
|
47
|
+
* resolve differently (checked against the template index, without reading files).
|
|
29
48
|
*
|
|
30
49
|
* @param page - The page model to check
|
|
31
50
|
* @param entry - Existing cache entry, or undefined if not cached
|
|
@@ -33,6 +52,7 @@ export declare function hasRecordedReads(entry: CacheEntry): boolean;
|
|
|
33
52
|
* @param allPages - All pages in the site, used to resolve the same layout as rendering
|
|
34
53
|
* @param now - Current date/time
|
|
35
54
|
* @param renderInputsHash - Optional hash of asset URLs and injection settings
|
|
55
|
+
* @param templateIndex - The build's template index; read from the source directory when omitted
|
|
36
56
|
* @returns True if the page should be rebuilt
|
|
37
57
|
*
|
|
38
58
|
* @example
|
|
@@ -43,43 +63,41 @@ export declare function hasRecordedReads(entry: CacheEntry): boolean;
|
|
|
43
63
|
* }
|
|
44
64
|
* ```
|
|
45
65
|
*/
|
|
46
|
-
export declare function shouldRebuildPage(page: PageModel, entry: CacheEntry | undefined, config: StatiConfig, allPages: PageModel[], now: Date, renderInputsHash?: string): Promise<boolean>;
|
|
66
|
+
export declare function shouldRebuildPage(page: PageModel, entry: CacheEntry | undefined, config: StatiConfig, allPages: PageModel[], now: Date, renderInputsHash?: string, templateIndex?: TemplateIndex): Promise<boolean>;
|
|
47
67
|
/**
|
|
48
|
-
* Creates a new cache entry for a page after it has been rendered
|
|
68
|
+
* Creates a new cache entry for a page after it has been rendered, with the templates and name
|
|
69
|
+
* lookups the render recorded (`rendered`).
|
|
49
70
|
*
|
|
50
71
|
* @param page - The page model
|
|
51
72
|
* @param config - Stati configuration
|
|
52
|
-
* @param allPages - All pages in the site, used to resolve the same layout as rendering
|
|
53
73
|
* @param renderedAt - When the page was rendered
|
|
54
74
|
* @param renderInputsHash - Optional hash of asset URLs and injection settings
|
|
55
|
-
* @param rendered -
|
|
75
|
+
* @param rendered - What the render used: templates, lookups, images and shared data. Without
|
|
76
|
+
* recorded templates the entry is not marked `templatesRecorded` and the page rebuilds next time.
|
|
56
77
|
* @returns New cache entry
|
|
57
78
|
*
|
|
58
79
|
* @example
|
|
59
80
|
* ```typescript
|
|
60
|
-
* const entry = await createCacheEntry(page, config,
|
|
81
|
+
* const entry = await createCacheEntry(page, config, new Date(), renderInputsHash, rendered);
|
|
61
82
|
* ```
|
|
62
83
|
*/
|
|
63
|
-
export declare function createCacheEntry(page: PageModel, config: StatiConfig,
|
|
84
|
+
export declare function createCacheEntry(page: PageModel, config: StatiConfig, renderedAt: Date, renderInputsHash?: string, rendered?: RenderedAssets): Promise<CacheEntry>;
|
|
64
85
|
/**
|
|
65
|
-
* Updates an existing cache entry
|
|
66
|
-
*
|
|
67
|
-
* template structure rarely changes between incremental rebuilds.
|
|
86
|
+
* Updates an existing cache entry after its page rendered again: a new entry from this
|
|
87
|
+
* render, keeping the original publish date when the page no longer sets one.
|
|
68
88
|
*
|
|
69
89
|
* @param entry - Existing cache entry
|
|
70
90
|
* @param page - The page model
|
|
71
91
|
* @param config - Stati configuration
|
|
72
|
-
* @param allPages - All pages in the site, used to resolve the same layout as rendering
|
|
73
92
|
* @param renderedAt - When the page was rendered
|
|
74
|
-
* @param fastUpdate - If true, skip expensive dep tracking (dev mode optimization)
|
|
75
93
|
* @param renderInputsHash - Optional hash of asset URLs and injection settings
|
|
76
|
-
* @param rendered -
|
|
94
|
+
* @param rendered - What the render used: templates, lookups, images and shared data
|
|
77
95
|
* @returns Updated cache entry
|
|
78
96
|
*
|
|
79
97
|
* @example
|
|
80
98
|
* ```typescript
|
|
81
|
-
* const updatedEntry = await updateCacheEntry(existingEntry, page, config,
|
|
99
|
+
* const updatedEntry = await updateCacheEntry(existingEntry, page, config, new Date(), hash, rendered);
|
|
82
100
|
* ```
|
|
83
101
|
*/
|
|
84
|
-
export declare function updateCacheEntry(entry: CacheEntry, page: PageModel, config: StatiConfig,
|
|
102
|
+
export declare function updateCacheEntry(entry: CacheEntry, page: PageModel, config: StatiConfig, renderedAt: Date, renderInputsHash?: string, rendered?: RenderedAssets): Promise<CacheEntry>;
|
|
85
103
|
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/core/isg/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC/
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/core/isg/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC/F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAWnD,OAAO,EAAE,aAAa,EAAwB,MAAM,sBAAsB,CAAC;AA+E3E;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,oFAAoF;IACpF,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,kFAAkF;IAClF,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,qEAAqE;IACrE,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC9B,oFAAoF;IACpF,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAC1C,sEAAsE;IACtE,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,GAAG,SAAS,CAAC;CACzD;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAE3D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAE/D;AAuDD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,EACrB,GAAG,EAAE,IAAI,EACT,gBAAgB,CAAC,EAAE,MAAM,EACzB,aAAa,CAAC,EAAE,aAAa,GAC5B,OAAO,CAAC,OAAO,CAAC,CA+JlB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,IAAI,EAChB,gBAAgB,CAAC,EAAE,MAAM,EACzB,QAAQ,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,UAAU,CAAC,CA0ErB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,IAAI,EAChB,gBAAgB,CAAC,EAAE,MAAM,EACzB,QAAQ,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,UAAU,CAAC,CASrB"}
|
package/dist/core/isg/builder.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { computeContentHash, computeFileHash, computeBinaryFileHash, computeInputsHash, } from './hash.js';
|
|
2
|
-
import {
|
|
2
|
+
import { resolveLayoutDependency } from './deps.js';
|
|
3
3
|
import { computeEffectiveTTL, computeNextRebuildAt, isPageFrozen } from './ttl.js';
|
|
4
4
|
import { validatePageISGOverrides, extractNumericOverride } from './validation.js';
|
|
5
|
-
import { getFrontMatterDate } from '../utils/index.js';
|
|
6
|
-
import {
|
|
5
|
+
import { getFrontMatterDate, normalizePathForComparison, resolveSrcDir } from '../utils/index.js';
|
|
6
|
+
import { TemplateIndex, isTemplateLookupList } from '../template-index.js';
|
|
7
7
|
/**
|
|
8
8
|
* Determines the output path for a page.
|
|
9
9
|
*/
|
|
@@ -60,11 +60,14 @@ function isValidCacheEntry(entry) {
|
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
for (const flag of [entry.readsCollection, entry.readsNav]) {
|
|
63
|
+
for (const flag of [entry.readsCollection, entry.readsNav, entry.templatesRecorded]) {
|
|
64
64
|
if (flag !== undefined && typeof flag !== 'boolean') {
|
|
65
65
|
return false;
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
if (entry.templateLookups !== undefined && !isTemplateLookupList(entry.templateLookups)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
68
71
|
return true;
|
|
69
72
|
}
|
|
70
73
|
/**
|
|
@@ -79,6 +82,17 @@ function isValidCacheEntry(entry) {
|
|
|
79
82
|
export function hasRecordedReads(entry) {
|
|
80
83
|
return typeof entry.readsCollection === 'boolean' && typeof entry.readsNav === 'boolean';
|
|
81
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether a cache entry's templates and name lookups were recorded while its page rendered.
|
|
87
|
+
* An entry without them cannot tell when a template change affects its page, so the page is
|
|
88
|
+
* rebuilt once to record them.
|
|
89
|
+
*
|
|
90
|
+
* @param entry - The cache entry
|
|
91
|
+
* @returns True when `templatesRecorded` is set
|
|
92
|
+
*/
|
|
93
|
+
export function hasRecordedTemplates(entry) {
|
|
94
|
+
return entry.templatesRecorded === true;
|
|
95
|
+
}
|
|
82
96
|
/**
|
|
83
97
|
* Tags a page's cache entry carries for `stati invalidate "tag:…"`: its front matter tags,
|
|
84
98
|
* plus the key and label of a generated page, so `tag:css` also refreshes `/tags/css`.
|
|
@@ -125,8 +139,12 @@ function withRenderedAssets(entry, rendered) {
|
|
|
125
139
|
}
|
|
126
140
|
/**
|
|
127
141
|
* Determines if a page should be rebuilt based on ISG logic.
|
|
128
|
-
* Checks content changes,
|
|
129
|
-
* Handles edge cases like missing dates
|
|
142
|
+
* Checks content changes, the templates the last render recorded, the page's layout, TTL
|
|
143
|
+
* expiration, and freeze status. Handles edge cases like missing dates and corrupted entries.
|
|
144
|
+
*
|
|
145
|
+
* A cached page is stale when a template it rendered changed or no longer exists, when its
|
|
146
|
+
* layout would now be a different file, or when a template name it looked up would now
|
|
147
|
+
* resolve differently (checked against the template index, without reading files).
|
|
130
148
|
*
|
|
131
149
|
* @param page - The page model to check
|
|
132
150
|
* @param entry - Existing cache entry, or undefined if not cached
|
|
@@ -134,6 +152,7 @@ function withRenderedAssets(entry, rendered) {
|
|
|
134
152
|
* @param allPages - All pages in the site, used to resolve the same layout as rendering
|
|
135
153
|
* @param now - Current date/time
|
|
136
154
|
* @param renderInputsHash - Optional hash of asset URLs and injection settings
|
|
155
|
+
* @param templateIndex - The build's template index; read from the source directory when omitted
|
|
137
156
|
* @returns True if the page should be rebuilt
|
|
138
157
|
*
|
|
139
158
|
* @example
|
|
@@ -144,7 +163,7 @@ function withRenderedAssets(entry, rendered) {
|
|
|
144
163
|
* }
|
|
145
164
|
* ```
|
|
146
165
|
*/
|
|
147
|
-
export async function shouldRebuildPage(page, entry, config, allPages, now, renderInputsHash) {
|
|
166
|
+
export async function shouldRebuildPage(page, entry, config, allPages, now, renderInputsHash, templateIndex) {
|
|
148
167
|
if (config.isg?.enabled === false) {
|
|
149
168
|
return true;
|
|
150
169
|
}
|
|
@@ -158,46 +177,37 @@ export async function shouldRebuildPage(page, entry, config, allPages, now, rend
|
|
|
158
177
|
console.warn(`Invalid cache entry for ${page.url}, forcing rebuild`);
|
|
159
178
|
return true;
|
|
160
179
|
}
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
let deps;
|
|
165
|
-
try {
|
|
166
|
-
deps = await trackTemplateDependencies(page, config, allPages);
|
|
180
|
+
// Entries from before templates were recorded while rendering rebuild once to record them
|
|
181
|
+
if (!hasRecordedTemplates(entry)) {
|
|
182
|
+
return true;
|
|
167
183
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
const index = templateIndex ?? (await TemplateIndex.load(resolveSrcDir(config)));
|
|
185
|
+
// The page can switch layouts without a template file changing, for example when a
|
|
186
|
+
// collection gains its first child page. deps[0] is the layout. A page without a layout
|
|
187
|
+
// renders Stati's fallback HTML; it always rebuilds, so a new layout.eta takes effect.
|
|
188
|
+
// Compared in normalized form: the drive letter's case follows the shell that ran the build.
|
|
189
|
+
const layout = resolveLayoutDependency(page, allPages, index);
|
|
190
|
+
const recordedLayout = entry.deps[0];
|
|
191
|
+
if (layout === null ||
|
|
192
|
+
recordedLayout === undefined ||
|
|
193
|
+
normalizePathForComparison(layout) !== normalizePathForComparison(recordedLayout)) {
|
|
176
194
|
return true;
|
|
177
195
|
}
|
|
178
|
-
//
|
|
196
|
+
// A name the page looked up would now find another template, such as a section override
|
|
197
|
+
// added at the same path, or a file that stati.include.has() did not find
|
|
198
|
+
if (index.lookupsChanged(entry.templateLookups)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
// Check if inputs (content + dependencies) have changed
|
|
202
|
+
const currentContentHash = computeContentHash(page.content, page.frontMatter);
|
|
203
|
+
// Hash every template the page rendered; one that no longer exists rebuilds the page
|
|
179
204
|
const depsHashes = [];
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (depHash) {
|
|
185
|
-
depsHashes.push(depHash);
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
dependencyErrors++;
|
|
189
|
-
console.warn(`Missing dependency file: ${dep} (used by ${page.url})`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
dependencyErrors++;
|
|
194
|
-
console.warn(`Failed to hash dependency ${dep}: ${error instanceof Error ? error.message : String(error)}`);
|
|
205
|
+
for (const dep of entry.deps) {
|
|
206
|
+
const depHash = await computeFileHash(dep);
|
|
207
|
+
if (!depHash) {
|
|
208
|
+
return true;
|
|
195
209
|
}
|
|
196
|
-
|
|
197
|
-
// If we had dependency errors, force rebuild to be safe
|
|
198
|
-
if (dependencyErrors > 0) {
|
|
199
|
-
console.warn(`${dependencyErrors} dependency errors for ${page.url}, forcing rebuild`);
|
|
200
|
-
return true;
|
|
210
|
+
depsHashes.push(depHash);
|
|
201
211
|
}
|
|
202
212
|
// Files found during the last render, such as images, are hashed by content
|
|
203
213
|
depsHashes.push(...(await hashAssetDeps(entry.assetDeps)));
|
|
@@ -276,37 +286,28 @@ export async function shouldRebuildPage(page, entry, config, allPages, now, rend
|
|
|
276
286
|
}
|
|
277
287
|
}
|
|
278
288
|
/**
|
|
279
|
-
* Creates a new cache entry for a page after it has been rendered
|
|
289
|
+
* Creates a new cache entry for a page after it has been rendered, with the templates and name
|
|
290
|
+
* lookups the render recorded (`rendered`).
|
|
280
291
|
*
|
|
281
292
|
* @param page - The page model
|
|
282
293
|
* @param config - Stati configuration
|
|
283
|
-
* @param allPages - All pages in the site, used to resolve the same layout as rendering
|
|
284
294
|
* @param renderedAt - When the page was rendered
|
|
285
295
|
* @param renderInputsHash - Optional hash of asset URLs and injection settings
|
|
286
|
-
* @param rendered -
|
|
296
|
+
* @param rendered - What the render used: templates, lookups, images and shared data. Without
|
|
297
|
+
* recorded templates the entry is not marked `templatesRecorded` and the page rebuilds next time.
|
|
287
298
|
* @returns New cache entry
|
|
288
299
|
*
|
|
289
300
|
* @example
|
|
290
301
|
* ```typescript
|
|
291
|
-
* const entry = await createCacheEntry(page, config,
|
|
302
|
+
* const entry = await createCacheEntry(page, config, new Date(), renderInputsHash, rendered);
|
|
292
303
|
* ```
|
|
293
304
|
*/
|
|
294
|
-
export async function createCacheEntry(page, config,
|
|
295
|
-
const timings = {};
|
|
296
|
-
let start = performance.now();
|
|
305
|
+
export async function createCacheEntry(page, config, renderedAt, renderInputsHash, rendered) {
|
|
297
306
|
// Validate page-level ISG overrides first
|
|
298
307
|
validatePageISGOverrides(page.frontMatter, page.sourcePath);
|
|
299
|
-
timings.validate = performance.now() - start;
|
|
300
|
-
// Compute content hash
|
|
301
|
-
start = performance.now();
|
|
302
308
|
const contentHash = computeContentHash(page.content, page.frontMatter);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
start = performance.now();
|
|
306
|
-
const deps = await trackTemplateDependencies(page, config, allPages);
|
|
307
|
-
timings.trackDeps = performance.now() - start;
|
|
308
|
-
// Compute hashes for all dependencies
|
|
309
|
-
start = performance.now();
|
|
309
|
+
// The templates the render used, the layout first
|
|
310
|
+
const deps = [...(rendered?.templates ?? [])];
|
|
310
311
|
const depsHashes = [];
|
|
311
312
|
for (const dep of deps) {
|
|
312
313
|
const depHash = await computeFileHash(dep);
|
|
@@ -315,7 +316,6 @@ export async function createCacheEntry(page, config, allPages, renderedAt, rende
|
|
|
315
316
|
}
|
|
316
317
|
}
|
|
317
318
|
depsHashes.push(...(await hashAssetDeps(rendered?.assetDeps)));
|
|
318
|
-
timings.depsHashes = performance.now() - start;
|
|
319
319
|
const inputsHash = computeInputsHash(contentHash, depsHashes, renderInputsHash);
|
|
320
320
|
// Extract tags from front matter
|
|
321
321
|
const tags = getCacheTags(page);
|
|
@@ -343,6 +343,10 @@ export async function createCacheEntry(page, config, allPages, renderedAt, rende
|
|
|
343
343
|
path: outputPath,
|
|
344
344
|
inputsHash,
|
|
345
345
|
deps,
|
|
346
|
+
...(rendered?.templates && {
|
|
347
|
+
templateLookups: [...(rendered.templateLookups ?? [])],
|
|
348
|
+
templatesRecorded: true,
|
|
349
|
+
}),
|
|
346
350
|
tags,
|
|
347
351
|
renderedAt: renderedAt.toISOString(),
|
|
348
352
|
ttlSeconds,
|
|
@@ -357,111 +361,24 @@ export async function createCacheEntry(page, config, allPages, renderedAt, rende
|
|
|
357
361
|
return withRenderedAssets(cacheEntry, rendered);
|
|
358
362
|
}
|
|
359
363
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* This detects when new includes/extends have been added to templates.
|
|
363
|
-
*
|
|
364
|
-
* @param entry - The existing cache entry
|
|
365
|
-
* @returns True if template structure may have changed, false if structure is unchanged
|
|
366
|
-
*/
|
|
367
|
-
async function hasTemplateStructureChanged(entry) {
|
|
368
|
-
try {
|
|
369
|
-
// If the entry has a layout in its deps, check if that layout file has been modified
|
|
370
|
-
// since the last render. Layout files are typically the first dep or contain the
|
|
371
|
-
// includes/extends declarations.
|
|
372
|
-
if (entry.deps.length === 0) {
|
|
373
|
-
return false;
|
|
374
|
-
}
|
|
375
|
-
// Get the timestamp of the last render
|
|
376
|
-
const lastRendered = new Date(entry.renderedAt).getTime();
|
|
377
|
-
// Import stat from node:fs/promises at runtime to avoid circular dependency issues
|
|
378
|
-
const { stat } = await import('node:fs/promises');
|
|
379
|
-
// Check if any template files (layout or partials) have been modified
|
|
380
|
-
// We only need to check the first few deps (usually layout and immediate includes)
|
|
381
|
-
// as a heuristic to detect structural changes without expensive full tracking
|
|
382
|
-
const depsToCheck = entry.deps.slice(0, 3);
|
|
383
|
-
for (const depPath of depsToCheck) {
|
|
384
|
-
try {
|
|
385
|
-
const stats = await stat(depPath);
|
|
386
|
-
if (stats.mtimeMs > lastRendered) {
|
|
387
|
-
// Template file was modified after last render - structure may have changed
|
|
388
|
-
return true;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
catch (_error) {
|
|
392
|
-
// If we can't stat the file, assume it might have changed
|
|
393
|
-
return true;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
catch (_error) {
|
|
399
|
-
// On any error, be conservative and assume structure changed
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Updates an existing cache entry with new information after rebuilding.
|
|
405
|
-
* In dev mode, this is optimized to reuse the existing deps array since
|
|
406
|
-
* template structure rarely changes between incremental rebuilds.
|
|
364
|
+
* Updates an existing cache entry after its page rendered again: a new entry from this
|
|
365
|
+
* render, keeping the original publish date when the page no longer sets one.
|
|
407
366
|
*
|
|
408
367
|
* @param entry - Existing cache entry
|
|
409
368
|
* @param page - The page model
|
|
410
369
|
* @param config - Stati configuration
|
|
411
|
-
* @param allPages - All pages in the site, used to resolve the same layout as rendering
|
|
412
370
|
* @param renderedAt - When the page was rendered
|
|
413
|
-
* @param fastUpdate - If true, skip expensive dep tracking (dev mode optimization)
|
|
414
371
|
* @param renderInputsHash - Optional hash of asset URLs and injection settings
|
|
415
|
-
* @param rendered -
|
|
372
|
+
* @param rendered - What the render used: templates, lookups, images and shared data
|
|
416
373
|
* @returns Updated cache entry
|
|
417
374
|
*
|
|
418
375
|
* @example
|
|
419
376
|
* ```typescript
|
|
420
|
-
* const updatedEntry = await updateCacheEntry(existingEntry, page, config,
|
|
377
|
+
* const updatedEntry = await updateCacheEntry(existingEntry, page, config, new Date(), hash, rendered);
|
|
421
378
|
* ```
|
|
422
379
|
*/
|
|
423
|
-
export async function updateCacheEntry(entry, page, config,
|
|
424
|
-
|
|
425
|
-
if (fastUpdate && entry.deps.length > 0) {
|
|
426
|
-
// Detect if template structure has changed (new includes/extends added)
|
|
427
|
-
// by checking if the layout file has been modified or if template content
|
|
428
|
-
// contains different dependency patterns
|
|
429
|
-
const structureChanged = await hasTemplateStructureChanged(entry);
|
|
430
|
-
// The page can also switch layouts without any template file changing, for example
|
|
431
|
-
// when a collection gains its first child page or a front matter layout is edited.
|
|
432
|
-
// deps[0] is always the layout, so compare it against the layout used now.
|
|
433
|
-
const layoutChanged = !structureChanged &&
|
|
434
|
-
(await resolveLayoutDependency(page, config, allPages)) !== entry.deps[0];
|
|
435
|
-
if (structureChanged || layoutChanged) {
|
|
436
|
-
// Template structure or layout changed - fall through to full dependency tracking
|
|
437
|
-
// This ensures we pick up new includes/extends and the newly selected layout
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
// Structure unchanged - safe to reuse existing deps
|
|
441
|
-
// Just update the content hash and timestamp, keep existing deps
|
|
442
|
-
const contentHash = computeContentHash(page.content, page.frontMatter);
|
|
443
|
-
// Compute hashes for existing dependencies (fast - files are cached)
|
|
444
|
-
const depsHashes = [];
|
|
445
|
-
for (const dep of entry.deps) {
|
|
446
|
-
const depHash = await computeFileHash(dep);
|
|
447
|
-
if (depHash) {
|
|
448
|
-
depsHashes.push(depHash);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
// Asset deps come from this render, not from the previous entry
|
|
452
|
-
depsHashes.push(...(await hashAssetDeps(rendered?.assetDeps)));
|
|
453
|
-
const inputsHash = computeInputsHash(contentHash, depsHashes, renderInputsHash);
|
|
454
|
-
return withRenderedAssets({
|
|
455
|
-
...entry,
|
|
456
|
-
inputsHash,
|
|
457
|
-
// Front matter tags, and a generated page's label, can change without a layout change
|
|
458
|
-
tags: getCacheTags(page),
|
|
459
|
-
renderedAt: renderedAt.toISOString(),
|
|
460
|
-
}, rendered);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
// Full update - create new entry and preserve original publishedAt
|
|
464
|
-
const newEntry = await createCacheEntry(page, config, allPages, renderedAt, renderInputsHash, rendered);
|
|
380
|
+
export async function updateCacheEntry(entry, page, config, renderedAt, renderInputsHash, rendered) {
|
|
381
|
+
const newEntry = await createCacheEntry(page, config, renderedAt, renderInputsHash, rendered);
|
|
465
382
|
// Preserve original publishedAt if no new one is specified
|
|
466
383
|
if (!newEntry.publishedAt && entry.publishedAt) {
|
|
467
384
|
newEntry.publishedAt = entry.publishedAt;
|