@stati/core 1.26.2 → 1.27.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.
- package/README.md +58 -40
- package/dist/core/content.d.ts.map +1 -1
- package/dist/core/content.js +19 -3
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +2 -20
- package/dist/core/isg/ttl.d.ts.map +1 -1
- package/dist/core/isg/ttl.js +3 -25
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +11 -5
- package/dist/core/utils/callable-partials.utils.d.ts.map +1 -1
- package/dist/core/utils/callable-partials.utils.js +14 -5
- package/dist/core/utils/context-validation.utils.d.ts +45 -0
- package/dist/core/utils/context-validation.utils.d.ts.map +1 -0
- package/dist/core/utils/context-validation.utils.js +175 -0
- package/dist/core/utils/date.utils.d.ts +31 -0
- package/dist/core/utils/date.utils.d.ts.map +1 -0
- package/dist/core/utils/date.utils.js +52 -0
- package/dist/core/utils/error-overlay.utils.d.ts.map +1 -1
- package/dist/core/utils/error-overlay.utils.js +747 -312
- package/dist/core/utils/index.d.ts +3 -0
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +6 -0
- package/dist/core/utils/partial-validation.utils.d.ts.map +1 -1
- package/dist/core/utils/partial-validation.utils.js +30 -81
- package/dist/core/utils/similarity.utils.d.ts +34 -0
- package/dist/core/utils/similarity.utils.d.ts.map +1 -0
- package/dist/core/utils/similarity.utils.js +73 -0
- package/dist/core/utils/template-errors.utils.d.ts +3 -1
- package/dist/core/utils/template-errors.utils.d.ts.map +1 -1
- package/dist/core/utils/template-errors.utils.js +68 -8
- package/dist/rss/generator.d.ts.map +1 -1
- package/dist/rss/generator.js +6 -4
- package/dist/types/content.d.ts +8 -5
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/rss.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { isDevelopment } from '../../env.js';
|
|
2
|
+
import { findSimilarNames } from './similarity.utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Properties that must resolve normally even when they are not defined on the
|
|
5
|
+
* object, because the runtime, serializers, or test tooling probe for them.
|
|
6
|
+
*/
|
|
7
|
+
const PASSTHROUGH_PROPERTIES = new Set([
|
|
8
|
+
'length',
|
|
9
|
+
'constructor',
|
|
10
|
+
'then', // Promise detection
|
|
11
|
+
'$$typeof', // React inspection
|
|
12
|
+
'nodeType', // DOM node detection
|
|
13
|
+
'asymmetricMatch', // Jest/Vitest matcher
|
|
14
|
+
'toJSON', // JSON serialization
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Checks whether a property access should bypass validation.
|
|
18
|
+
*/
|
|
19
|
+
export function isPassthroughProperty(prop) {
|
|
20
|
+
return typeof prop === 'symbol' || prop in Object.prototype || PASSTHROUGH_PROPERTIES.has(prop);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Every `SiteConfig` field. Optional fields that a site leaves unset are absent at
|
|
24
|
+
* runtime, so they are listed here to keep `stati.site.description` from being
|
|
25
|
+
* reported as unknown. Typed as a Record so a new `SiteConfig` field fails to compile
|
|
26
|
+
* until it is added.
|
|
27
|
+
*/
|
|
28
|
+
const SITE_CONFIG_KEYS = {
|
|
29
|
+
title: true,
|
|
30
|
+
baseUrl: true,
|
|
31
|
+
defaultLocale: true,
|
|
32
|
+
description: true,
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Context properties whose own keys form a closed set and are validated one level
|
|
36
|
+
* down, with keys that may be legitimately absent at runtime.
|
|
37
|
+
* `page` is left out because frontmatter keys differ from page to page, and
|
|
38
|
+
* `partials` already has its own validating proxy.
|
|
39
|
+
*/
|
|
40
|
+
const VALIDATED_NESTED_PROPERTIES = {
|
|
41
|
+
site: Object.keys(SITE_CONFIG_KEYS),
|
|
42
|
+
nav: [],
|
|
43
|
+
collection: [],
|
|
44
|
+
generator: [],
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Context properties searched for an exact match when a key is missing from the
|
|
48
|
+
* root, so that `stati.title` can suggest `stati.page.title` and `stati.header`
|
|
49
|
+
* can suggest `stati.partials.header`.
|
|
50
|
+
*/
|
|
51
|
+
const CROSS_LEVEL_PROPERTIES = ['page', 'site', 'nav', 'partials'];
|
|
52
|
+
/**
|
|
53
|
+
* Thrown in development when a template reads a property that does not exist
|
|
54
|
+
* on the template context.
|
|
55
|
+
*
|
|
56
|
+
* The suggestions are encoded in the message because Eta's debug mode rethrows
|
|
57
|
+
* runtime errors as a new error that only keeps the original message and name.
|
|
58
|
+
* Use {@link parseUnknownPropertyMessage} to read them back.
|
|
59
|
+
*/
|
|
60
|
+
export class UnknownTemplatePropertyError extends Error {
|
|
61
|
+
propertyPath;
|
|
62
|
+
suggestions;
|
|
63
|
+
constructor(propertyPath, suggestions, availableKeys) {
|
|
64
|
+
super(formatUnknownPropertyMessage(propertyPath, suggestions, availableKeys));
|
|
65
|
+
this.name = 'UnknownTemplatePropertyError';
|
|
66
|
+
this.propertyPath = propertyPath;
|
|
67
|
+
this.suggestions = suggestions;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function formatUnknownPropertyMessage(propertyPath, suggestions, availableKeys) {
|
|
71
|
+
const base = `Unknown property "${propertyPath}".`;
|
|
72
|
+
if (suggestions.length > 0) {
|
|
73
|
+
const quoted = suggestions.map((s) => `"${s}"`);
|
|
74
|
+
const list = quoted.length === 1 ? quoted[0] : `${quoted.slice(0, -1).join(', ')} or ${quoted.at(-1)}`;
|
|
75
|
+
return `${base} Did you mean ${list}?`;
|
|
76
|
+
}
|
|
77
|
+
return `${base} Available properties: ${availableKeys.join(', ')}.`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reads an {@link UnknownTemplatePropertyError} message back into a short message
|
|
81
|
+
* and a list of overlay suggestions. Returns `undefined` for any other message.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* parseUnknownPropertyMessage('Unknown property "stati.paeg". Did you mean "stati.page"?');
|
|
86
|
+
* // { message: 'Unknown property "stati.paeg"', suggestions: ['Did you mean "stati.page"?'] }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function parseUnknownPropertyMessage(message) {
|
|
90
|
+
const match = message.match(/^Unknown property "([^"]+)"\.(?: Did you mean (.+)\?| Available properties: (.*)\.)?$/);
|
|
91
|
+
if (!match) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const [, propertyPath, didYouMean, available] = match;
|
|
95
|
+
const suggestions = [];
|
|
96
|
+
if (didYouMean) {
|
|
97
|
+
for (const [, name] of didYouMean.matchAll(/"([^"]+)"/g)) {
|
|
98
|
+
suggestions.push(`Did you mean "${name}"?`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (available !== undefined) {
|
|
102
|
+
const parentPath = propertyPath.slice(0, propertyPath.lastIndexOf('.'));
|
|
103
|
+
suggestions.push(`Available properties on ${parentPath}: ${available || '(none)'}`);
|
|
104
|
+
}
|
|
105
|
+
return { message: `Unknown property "${propertyPath}"`, suggestions };
|
|
106
|
+
}
|
|
107
|
+
function listKeys(target, extraKeys) {
|
|
108
|
+
return [...new Set([...Object.keys(target), ...extraKeys])].sort();
|
|
109
|
+
}
|
|
110
|
+
function createValidatingObjectProxy(target, path, extraKeys) {
|
|
111
|
+
return new Proxy(target, {
|
|
112
|
+
get(obj, prop, receiver) {
|
|
113
|
+
if (prop in obj || isPassthroughProperty(prop) || extraKeys.includes(prop)) {
|
|
114
|
+
return Reflect.get(obj, prop, receiver);
|
|
115
|
+
}
|
|
116
|
+
const name = String(prop);
|
|
117
|
+
const availableKeys = listKeys(obj, extraKeys);
|
|
118
|
+
const suggestions = findSimilarNames(name, availableKeys).map((key) => `${path}.${key}`);
|
|
119
|
+
throw new UnknownTemplatePropertyError(`${path}.${name}`, suggestions, availableKeys);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Wraps the template context so that reading a property that does not exist
|
|
125
|
+
* throws an {@link UnknownTemplatePropertyError} with "did you mean" suggestions
|
|
126
|
+
* instead of silently rendering `undefined`. The error reaches the dev server's
|
|
127
|
+
* error overlay.
|
|
128
|
+
*
|
|
129
|
+
* Validates the top-level `stati.*` keys (including custom filters, which are
|
|
130
|
+
* spread onto the context) and one level down for `site`, `nav`, `collection`,
|
|
131
|
+
* and `generator`. Frontmatter under `stati.page` is not validated.
|
|
132
|
+
*
|
|
133
|
+
* Only active in development; other environments get the context back unchanged.
|
|
134
|
+
*/
|
|
135
|
+
export function createValidatingContextProxy(context) {
|
|
136
|
+
if (!isDevelopment()) {
|
|
137
|
+
return context;
|
|
138
|
+
}
|
|
139
|
+
const nestedProxies = new Map();
|
|
140
|
+
return new Proxy(context, {
|
|
141
|
+
get(obj, prop, receiver) {
|
|
142
|
+
if (prop in obj || isPassthroughProperty(prop)) {
|
|
143
|
+
const value = Reflect.get(obj, prop, receiver);
|
|
144
|
+
const nestedExtraKeys = typeof prop === 'string' && Object.hasOwn(VALIDATED_NESTED_PROPERTIES, prop)
|
|
145
|
+
? VALIDATED_NESTED_PROPERTIES[prop]
|
|
146
|
+
: undefined;
|
|
147
|
+
if (nestedExtraKeys && typeof value === 'object' && value !== null) {
|
|
148
|
+
// Cache so that repeated reads return the same proxy (stati.nav === stati.nav)
|
|
149
|
+
let nested = nestedProxies.get(prop);
|
|
150
|
+
if (!nested) {
|
|
151
|
+
nested = createValidatingObjectProxy(value, `stati.${String(prop)}`, nestedExtraKeys);
|
|
152
|
+
nestedProxies.set(prop, nested);
|
|
153
|
+
}
|
|
154
|
+
return nested;
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
const name = String(prop);
|
|
159
|
+
const availableKeys = listKeys(obj, []);
|
|
160
|
+
// An exact match one level down (stati.title -> stati.page.title) is a
|
|
161
|
+
// stronger signal than a spelling match, so it goes first
|
|
162
|
+
const suggestions = [];
|
|
163
|
+
for (const parent of CROSS_LEVEL_PROPERTIES) {
|
|
164
|
+
const parentValue = obj[parent];
|
|
165
|
+
if (typeof parentValue === 'object' &&
|
|
166
|
+
parentValue !== null &&
|
|
167
|
+
Object.hasOwn(parentValue, name)) {
|
|
168
|
+
suggestions.push(`stati.${parent}.${name}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
suggestions.push(...findSimilarNames(name, availableKeys).map((key) => `stati.${key}`));
|
|
172
|
+
throw new UnknownTemplatePropertyError(`stati.${name}`, suggestions.slice(0, 3), availableKeys);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date utilities for reading publish dates from front matter.
|
|
3
|
+
* @module core/utils/date
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Front matter fields checked for a page's publish date, in priority order.
|
|
7
|
+
* Collections, navigation, ISG aging and RSS all use this order.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PUBLISHED_DATE_FIELDS: readonly ["publishedAt", "published", "date", "createdAt"];
|
|
10
|
+
/**
|
|
11
|
+
* Returns the publish date from front matter, or `undefined` if none is usable.
|
|
12
|
+
*
|
|
13
|
+
* Checks {@link PUBLISHED_DATE_FIELDS} in order and returns the first valid value.
|
|
14
|
+
* Accepts strings (quoted YAML dates) and `Date` objects (unquoted YAML dates,
|
|
15
|
+
* which gray-matter parses into `Date`). Missing, `null` and empty values are skipped;
|
|
16
|
+
* other invalid values are reported through `onInvalid` and skipped, so a bad
|
|
17
|
+
* `publishedAt` does not hide a valid `date`.
|
|
18
|
+
*
|
|
19
|
+
* @param frontMatter - Parsed front matter of a page; `null`/`undefined` returns `undefined`
|
|
20
|
+
* @param onInvalid - Called for each present field whose value is not a valid date
|
|
21
|
+
* @returns A new `Date` instance, or `undefined`
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* getFrontMatterDate({ date: new Date('2026-01-01') }); // 2026-01-01T00:00:00.000Z
|
|
26
|
+
* getFrontMatterDate({ publishedAt: 'soon', date: '2026-06-01' }); // 2026-06-01T00:00:00.000Z
|
|
27
|
+
* getFrontMatterDate({ title: 'Undated' }); // undefined
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function getFrontMatterDate(frontMatter: Record<string, unknown> | null | undefined, onInvalid?: (field: string, value: unknown) => void): Date | undefined;
|
|
31
|
+
//# sourceMappingURL=date.utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/date.utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;GAGG;AACH,eAAO,MAAM,qBAAqB,4DAA6D,CAAC;AAEhG;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EACvD,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,GAClD,IAAI,GAAG,SAAS,CA0BlB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date utilities for reading publish dates from front matter.
|
|
3
|
+
* @module core/utils/date
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Front matter fields checked for a page's publish date, in priority order.
|
|
7
|
+
* Collections, navigation, ISG aging and RSS all use this order.
|
|
8
|
+
*/
|
|
9
|
+
export const PUBLISHED_DATE_FIELDS = ['publishedAt', 'published', 'date', 'createdAt'];
|
|
10
|
+
/**
|
|
11
|
+
* Returns the publish date from front matter, or `undefined` if none is usable.
|
|
12
|
+
*
|
|
13
|
+
* Checks {@link PUBLISHED_DATE_FIELDS} in order and returns the first valid value.
|
|
14
|
+
* Accepts strings (quoted YAML dates) and `Date` objects (unquoted YAML dates,
|
|
15
|
+
* which gray-matter parses into `Date`). Missing, `null` and empty values are skipped;
|
|
16
|
+
* other invalid values are reported through `onInvalid` and skipped, so a bad
|
|
17
|
+
* `publishedAt` does not hide a valid `date`.
|
|
18
|
+
*
|
|
19
|
+
* @param frontMatter - Parsed front matter of a page; `null`/`undefined` returns `undefined`
|
|
20
|
+
* @param onInvalid - Called for each present field whose value is not a valid date
|
|
21
|
+
* @returns A new `Date` instance, or `undefined`
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* getFrontMatterDate({ date: new Date('2026-01-01') }); // 2026-01-01T00:00:00.000Z
|
|
26
|
+
* getFrontMatterDate({ publishedAt: 'soon', date: '2026-06-01' }); // 2026-06-01T00:00:00.000Z
|
|
27
|
+
* getFrontMatterDate({ title: 'Undated' }); // undefined
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function getFrontMatterDate(frontMatter, onInvalid) {
|
|
31
|
+
if (!frontMatter) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
for (const field of PUBLISHED_DATE_FIELDS) {
|
|
35
|
+
const value = frontMatter[field];
|
|
36
|
+
if (value === undefined || value === null || (typeof value === 'string' && !value.trim())) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
let date;
|
|
40
|
+
if (typeof value === 'string') {
|
|
41
|
+
date = new Date(value);
|
|
42
|
+
}
|
|
43
|
+
else if (value instanceof Date) {
|
|
44
|
+
date = new Date(value.getTime());
|
|
45
|
+
}
|
|
46
|
+
if (date && !isNaN(date.getTime())) {
|
|
47
|
+
return date;
|
|
48
|
+
}
|
|
49
|
+
onInvalid?.(field, value);
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-overlay.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/error-overlay.utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;
|
|
1
|
+
{"version":3,"file":"error-overlay.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/error-overlay.utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AA0sBD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAiJnF;AAsFD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,EAC1B,QAAQ,CAAC,EAAE,MAAM,GAChB,YAAY,CAkCd"}
|