@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.
Files changed (36) hide show
  1. package/README.md +58 -40
  2. package/dist/core/content.d.ts.map +1 -1
  3. package/dist/core/content.js +19 -3
  4. package/dist/core/isg/builder.d.ts.map +1 -1
  5. package/dist/core/isg/builder.js +2 -20
  6. package/dist/core/isg/ttl.d.ts.map +1 -1
  7. package/dist/core/isg/ttl.js +3 -25
  8. package/dist/core/templates.d.ts.map +1 -1
  9. package/dist/core/templates.js +11 -5
  10. package/dist/core/utils/callable-partials.utils.d.ts.map +1 -1
  11. package/dist/core/utils/callable-partials.utils.js +14 -5
  12. package/dist/core/utils/context-validation.utils.d.ts +45 -0
  13. package/dist/core/utils/context-validation.utils.d.ts.map +1 -0
  14. package/dist/core/utils/context-validation.utils.js +175 -0
  15. package/dist/core/utils/date.utils.d.ts +31 -0
  16. package/dist/core/utils/date.utils.d.ts.map +1 -0
  17. package/dist/core/utils/date.utils.js +52 -0
  18. package/dist/core/utils/error-overlay.utils.d.ts.map +1 -1
  19. package/dist/core/utils/error-overlay.utils.js +747 -312
  20. package/dist/core/utils/index.d.ts +3 -0
  21. package/dist/core/utils/index.d.ts.map +1 -1
  22. package/dist/core/utils/index.js +6 -0
  23. package/dist/core/utils/partial-validation.utils.d.ts.map +1 -1
  24. package/dist/core/utils/partial-validation.utils.js +30 -81
  25. package/dist/core/utils/similarity.utils.d.ts +34 -0
  26. package/dist/core/utils/similarity.utils.d.ts.map +1 -0
  27. package/dist/core/utils/similarity.utils.js +73 -0
  28. package/dist/core/utils/template-errors.utils.d.ts +3 -1
  29. package/dist/core/utils/template-errors.utils.d.ts.map +1 -1
  30. package/dist/core/utils/template-errors.utils.js +68 -8
  31. package/dist/rss/generator.d.ts.map +1 -1
  32. package/dist/rss/generator.js +6 -4
  33. package/dist/types/content.d.ts +8 -5
  34. package/dist/types/content.d.ts.map +1 -1
  35. package/dist/types/rss.d.ts +1 -1
  36. 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;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAwbnF;AA0DD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,EAC1B,QAAQ,CAAC,EAAE,MAAM,GAChB,YAAY,CAkCd"}
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"}