@stati/core 1.26.2 → 1.27.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.
@@ -9,6 +9,8 @@ export { propValue } from './template.utils.js';
9
9
  export { slugify } from './slugify.utils.js';
10
10
  export { trackTailwindClass, enableInventoryTracking, disableInventoryTracking, clearInventory, getInventory, getInventorySize, isTrackingEnabled, writeTailwindClassInventory, isTailwindUsed, resetTailwindDetection, loadPreviousInventory, } from './tailwind-inventory.utils.js';
11
11
  export { createValidatingPartialsProxy } from './partial-validation.utils.js';
12
+ export { createValidatingContextProxy, isPassthroughProperty, parseUnknownPropertyMessage, UnknownTemplatePropertyError, } from './context-validation.utils.js';
13
+ export { editDistance, findSimilarNames } from './similarity.utils.js';
12
14
  export { makeCallablePartial, wrapPartialsAsCallable } from './callable-partials.utils.js';
13
15
  export type { CallablePartial } from './callable-partials.utils.js';
14
16
  export { TemplateError, parseEtaError, createTemplateError } from './template-errors.utils.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,GACL,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAG9E,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3F,YAAY,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC7F,YAAY,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,GACL,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAG9E,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACrB,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGvE,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3F,YAAY,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC7F,YAAY,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC"}
@@ -16,6 +16,10 @@ export { slugify } from './slugify.utils.js';
16
16
  export { trackTailwindClass, enableInventoryTracking, disableInventoryTracking, clearInventory, getInventory, getInventorySize, isTrackingEnabled, writeTailwindClassInventory, isTailwindUsed, resetTailwindDetection, loadPreviousInventory, } from './tailwind-inventory.utils.js';
17
17
  // Partial validation utilities
18
18
  export { createValidatingPartialsProxy } from './partial-validation.utils.js';
19
+ // Template context validation utilities
20
+ export { createValidatingContextProxy, isPassthroughProperty, parseUnknownPropertyMessage, UnknownTemplatePropertyError, } from './context-validation.utils.js';
21
+ // String similarity utilities
22
+ export { editDistance, findSimilarNames } from './similarity.utils.js';
19
23
  // Callable partial utilities
20
24
  export { makeCallablePartial, wrapPartialsAsCallable } from './callable-partials.utils.js';
21
25
  // Template error utilities
@@ -1 +1 @@
1
- {"version":3,"file":"partial-validation.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/partial-validation.utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,8BAA8B,CAAC;AA8FpE;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,CAAC,SAAS,MAAM,GAAG,eAAe,EAC9E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAC1B,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CA+EnB"}
1
+ {"version":3,"file":"partial-validation.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/partial-validation.utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAqDpE;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,CAAC,SAAS,MAAM,GAAG,eAAe,EAC9E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAC1B,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CA4DnB"}
@@ -1,4 +1,6 @@
1
1
  import { isProduction } from '../../env.js';
2
+ import { isPassthroughProperty } from './context-validation.utils.js';
3
+ import { findSimilarNames } from './similarity.utils.js';
2
4
  /**
3
5
  * Creates inline error overlay HTML for missing partials
4
6
  */
@@ -6,82 +8,45 @@ function createInlineErrorOverlay(partialName, suggestions) {
6
8
  const suggestionText = suggestions.length > 0
7
9
  ? `Did you mean: ${suggestions.map((s) => `"${s}"`).join(', ')}?`
8
10
  : 'No similar partials found';
11
+ // Inline styles only: this toast is injected into the user's own page, so it
12
+ // carries the Stati dark slate palette rather than relying on page styles.
9
13
  return `
10
14
  <!-- Stati Development Error Overlay -->
11
- <div style="
15
+ <div role="alert" style="
12
16
  position: fixed;
13
- top: 20px;
14
- right: 20px;
15
- z-index: 999999;
16
- background: #dc2626;
17
- color: white;
18
- padding: 16px;
17
+ top: 16px;
18
+ right: 16px;
19
+ z-index: 2147483647;
20
+ box-sizing: border-box;
21
+ width: min(24rem, calc(100vw - 32px));
22
+ margin: 0;
23
+ padding: 14px 16px;
24
+ background: #0f172a;
25
+ color: #f1f5f9;
26
+ border: 1px solid #475569;
19
27
  border-radius: 8px;
20
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
21
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
28
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
22
29
  font-size: 14px;
23
- line-height: 1.4;
24
- max-width: 400px;
25
- border: 2px solid #fca5a5;
30
+ font-weight: 400;
31
+ line-height: 1.5;
32
+ text-align: left;
26
33
  ">
27
- <div style="font-weight: 600; margin-bottom: 8px; font-size: 16px;">
28
- ⚠️ Template Error
34
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 13px; font-weight: 600; color: #f87171;">
35
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: none;"><path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>
36
+ Template Error
29
37
  </div>
30
- <div style="margin-bottom: 8px;">
31
- <strong>Partial "${partialName}" not found</strong>
38
+ <div style="font-weight: 600; overflow-wrap: anywhere;">
39
+ Partial "${partialName}" not found
32
40
  </div>
33
- <div style="color: #fca5a5; font-size: 13px;">
41
+ <div style="margin-top: 2px; font-size: 13px; color: #94a3b8; overflow-wrap: anywhere;">
34
42
  ${suggestionText}
35
43
  </div>
36
- <div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid #fca5a5; font-size: 12px; opacity: 0.8;">
44
+ <div style="margin-top: 12px; padding-top: 10px; border-top: 1px solid #1e293b; font-size: 12px; color: #8391a7;">
37
45
  Fix the partial name in your template to dismiss this error
38
46
  </div>
39
47
  </div>
40
48
  <!-- End Stati Error Overlay -->`;
41
49
  }
42
- /**
43
- * Finds similar partial names to suggest when a partial is not found
44
- */
45
- function findSimilarPartialNames(targetName, availableNames) {
46
- if (availableNames.length === 0)
47
- return [];
48
- // Simple similarity check based on common prefixes and substrings
49
- const target = targetName.toLowerCase();
50
- const suggestions = availableNames
51
- .map((name) => {
52
- const candidate = name.toLowerCase();
53
- let score = 0;
54
- // Exact match gets highest score
55
- if (candidate === target)
56
- return { name, score: 1000 };
57
- // Check if one is contained in the other
58
- if (candidate.includes(target) || target.includes(candidate)) {
59
- score += 100;
60
- }
61
- // Check common prefix length
62
- let prefixLen = 0;
63
- for (let i = 0; i < Math.min(target.length, candidate.length); i++) {
64
- if (target[i] === candidate[i]) {
65
- prefixLen++;
66
- }
67
- else {
68
- break;
69
- }
70
- }
71
- score += prefixLen * 10;
72
- // Check common characters
73
- const targetChars = new Set(target);
74
- const candidateChars = new Set(candidate);
75
- const commonChars = [...targetChars].filter((char) => candidateChars.has(char));
76
- score += commonChars.length;
77
- return { name, score };
78
- })
79
- .filter((item) => item.score > 0) // Only include items with some similarity
80
- .sort((a, b) => b.score - a.score) // Sort by score descending
81
- .slice(0, 3) // Top 3 suggestions
82
- .map((item) => item.name);
83
- return suggestions;
84
- }
85
50
  /**
86
51
  * Creates a development-mode Proxy for the partials object that throws errors
87
52
  * when accessing non-existent partials instead of returning undefined.
@@ -101,31 +66,15 @@ export function createValidatingPartialsProxy(partials) {
101
66
  }
102
67
  return new Proxy(partials, {
103
68
  get(target, prop, receiver) {
104
- // Allow normal object operations
105
- if (typeof prop === 'symbol') {
106
- return Reflect.get(target, prop, receiver);
107
- }
108
69
  const propName = String(prop);
109
- // Check if the property exists
110
- if (propName in target) {
111
- return target[propName];
112
- }
113
- // Special case: allow accessing length, toString, etc.
114
- // Also handle test framework inspection properties
115
- if (propName in Object.prototype ||
116
- propName === 'length' ||
117
- propName === 'constructor' ||
118
- propName === 'then' || // Promise detection
119
- propName === '$$typeof' || // React inspection
120
- propName === 'nodeType' || // DOM node detection
121
- propName === 'asymmetricMatch' || // Jest/Vitest matcher
122
- propName === 'toJSON' // JSON serialization
123
- ) {
70
+ // Return existing partials, and let symbols, prototype methods, and
71
+ // serializer/test-tooling probes resolve normally
72
+ if (propName in target || isPassthroughProperty(prop)) {
124
73
  return Reflect.get(target, prop, receiver);
125
74
  }
126
75
  // Property doesn't exist - return error overlay HTML instead of throwing
127
76
  const availablePartials = Object.keys(target);
128
- const suggestions = findSimilarPartialNames(propName, availablePartials);
77
+ const suggestions = findSimilarNames(propName, availablePartials);
129
78
  // In development, render an inline error overlay
130
79
  const errorHtml = createInlineErrorOverlay(propName, suggestions);
131
80
  // Check if we're dealing with CallablePartials by testing a known partial
@@ -0,0 +1,34 @@
1
+ /**
2
+ * String similarity helpers used to build "did you mean" suggestions.
3
+ */
4
+ /**
5
+ * Computes the edit distance between two strings, counting insertions,
6
+ * deletions, substitutions, and transpositions of adjacent characters
7
+ * (optimal string alignment distance).
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * editDistance('paeg', 'page'); // 1 (transposition)
12
+ * editDistance('titel', 'title'); // 1
13
+ * ```
14
+ */
15
+ export declare function editDistance(a: string, b: string): number;
16
+ /**
17
+ * Finds the candidates that look like a misspelling of `target`, best match first.
18
+ *
19
+ * A candidate matches when its case-insensitive edit distance to the target is at
20
+ * most a third of the target's length (minimum 1), or when one is a prefix of the
21
+ * other and both are at least 3 characters long.
22
+ *
23
+ * @param target - The name that was not found
24
+ * @param candidates - The names that do exist
25
+ * @param limit - Maximum number of suggestions to return
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * findSimilarNames('paeg', ['page', 'partials', 'site']); // ['page']
30
+ * findSimilarNames('head', ['header', 'hero', 'footer']); // ['header']
31
+ * ```
32
+ */
33
+ export declare function findSimilarNames(target: string, candidates: Iterable<string>, limit?: number): string[];
34
+ //# sourceMappingURL=similarity.utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"similarity.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/similarity.utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAwBzD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC5B,KAAK,SAAI,GACR,MAAM,EAAE,CAsBV"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * String similarity helpers used to build "did you mean" suggestions.
3
+ */
4
+ /**
5
+ * Computes the edit distance between two strings, counting insertions,
6
+ * deletions, substitutions, and transpositions of adjacent characters
7
+ * (optimal string alignment distance).
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * editDistance('paeg', 'page'); // 1 (transposition)
12
+ * editDistance('titel', 'title'); // 1
13
+ * ```
14
+ */
15
+ export function editDistance(a, b) {
16
+ const rows = a.length + 1;
17
+ const cols = b.length + 1;
18
+ const d = Array.from({ length: rows }, (_, i) => {
19
+ const row = new Array(cols).fill(0);
20
+ row[0] = i;
21
+ return row;
22
+ });
23
+ for (let j = 0; j < cols; j++) {
24
+ d[0][j] = j;
25
+ }
26
+ for (let i = 1; i < rows; i++) {
27
+ for (let j = 1; j < cols; j++) {
28
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
29
+ let value = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
30
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
31
+ value = Math.min(value, d[i - 2][j - 2] + 1);
32
+ }
33
+ d[i][j] = value;
34
+ }
35
+ }
36
+ return d[a.length][b.length];
37
+ }
38
+ /**
39
+ * Finds the candidates that look like a misspelling of `target`, best match first.
40
+ *
41
+ * A candidate matches when its case-insensitive edit distance to the target is at
42
+ * most a third of the target's length (minimum 1), or when one is a prefix of the
43
+ * other and both are at least 3 characters long.
44
+ *
45
+ * @param target - The name that was not found
46
+ * @param candidates - The names that do exist
47
+ * @param limit - Maximum number of suggestions to return
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * findSimilarNames('paeg', ['page', 'partials', 'site']); // ['page']
52
+ * findSimilarNames('head', ['header', 'hero', 'footer']); // ['header']
53
+ * ```
54
+ */
55
+ export function findSimilarNames(target, candidates, limit = 3) {
56
+ const normalizedTarget = target.toLowerCase();
57
+ const maxDistance = Math.max(1, Math.floor(normalizedTarget.length / 3));
58
+ const matches = [];
59
+ for (const name of new Set(candidates)) {
60
+ const candidate = name.toLowerCase();
61
+ const distance = editDistance(normalizedTarget, candidate);
62
+ const isPrefix = Math.min(candidate.length, normalizedTarget.length) >= 3 &&
63
+ (candidate.startsWith(normalizedTarget) || normalizedTarget.startsWith(candidate));
64
+ if (distance <= maxDistance || isPrefix) {
65
+ // Prefix matches win ties against plain edit-distance matches
66
+ matches.push({ name, score: distance - (isPrefix ? 0.5 : 0) });
67
+ }
68
+ }
69
+ return matches
70
+ .sort((a, b) => a.score - b.score || a.name.localeCompare(b.name))
71
+ .slice(0, limit)
72
+ .map((match) => match.name);
73
+ }
@@ -7,11 +7,13 @@ export declare class TemplateError extends Error {
7
7
  readonly line?: number;
8
8
  readonly column?: number;
9
9
  readonly template?: string;
10
+ /** Error-specific suggestions shown before the generic ones in the overlay */
11
+ readonly suggestions?: string[];
10
12
  readonly context?: {
11
13
  before?: string[];
12
14
  after?: string[];
13
15
  };
14
- constructor(message: string, filePath?: string, line?: number, column?: number, template?: string);
16
+ constructor(message: string, filePath?: string, line?: number, column?: number, template?: string, suggestions?: string[]);
15
17
  /**
16
18
  * Converts this template error to ErrorDetails for the overlay.
17
19
  */
@@ -1 +1 @@
1
- {"version":3,"file":"template-errors.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/template-errors.utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,SAAgB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClC,SAAgB,IAAI,CAAC,EAAE,MAAM,CAAC;IAC9B,SAAgB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChC,SAAgB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClC,SAAgB,OAAO,CAAC,EAAE;QACxB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;gBAGA,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM;IAUnB;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CAoE9C;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,aAAa,CA8BhF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,KAAK,EACZ,YAAY,CAAC,EAAE,MAAM,EACrB,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,GACf,aAAa,CAMf"}
1
+ {"version":3,"file":"template-errors.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/template-errors.utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,SAAgB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClC,SAAgB,IAAI,CAAC,EAAE,MAAM,CAAC;IAC9B,SAAgB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChC,SAAgB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClC,8EAA8E;IAC9E,SAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvC,SAAgB,OAAO,CAAC,EAAE;QACxB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;gBAGA,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,EAAE;IAWxB;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CAoE9C;AAsCD;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,aAAa,CAoDhF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,KAAK,EACZ,YAAY,CAAC,EAAE,MAAM,EACrB,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,GACf,aAAa,CAMf"}
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'fs/promises';
2
2
  import { pathExists } from './fs.utils.js';
3
+ import { parseUnknownPropertyMessage } from './context-validation.utils.js';
3
4
  /**
4
5
  * Enhanced template error that includes file context and suggestions.
5
6
  */
@@ -8,8 +9,10 @@ export class TemplateError extends Error {
8
9
  line;
9
10
  column;
10
11
  template;
12
+ /** Error-specific suggestions shown before the generic ones in the overlay */
13
+ suggestions;
11
14
  context;
12
- constructor(message, filePath, line, column, template) {
15
+ constructor(message, filePath, line, column, template, suggestions) {
13
16
  super(message);
14
17
  this.name = 'TemplateError';
15
18
  if (filePath !== undefined)
@@ -20,12 +23,14 @@ export class TemplateError extends Error {
20
23
  this.column = column;
21
24
  if (template !== undefined)
22
25
  this.template = template;
26
+ if (suggestions !== undefined)
27
+ this.suggestions = suggestions;
23
28
  }
24
29
  /**
25
30
  * Converts this template error to ErrorDetails for the overlay.
26
31
  */
27
32
  async toErrorDetails() {
28
- const suggestions = [];
33
+ const suggestions = [...(this.suggestions ?? [])];
29
34
  const message = this.message.toLowerCase();
30
35
  // Add specific suggestions based on error type
31
36
  if (message.includes('not found') || message.includes('cannot resolve')) {
@@ -87,6 +92,39 @@ export class TemplateError extends Error {
87
92
  return errorDetails;
88
93
  }
89
94
  }
95
+ /**
96
+ * Header that Eta's debug mode prepends to runtime errors:
97
+ * `<path>:<line>` (or `line <line>` without a path), a numbered excerpt of the
98
+ * template, then a blank line and the original message.
99
+ */
100
+ const ETA_RUNTIME_ERROR_HEADER = /^(?:(.+):(\d+)|line (\d+))\n(?:(?: >> | {4})\d+\|[^\n]*\n)+\n/;
101
+ /**
102
+ * Root template context properties, used to suggest `stati.<name>` when a
103
+ * template references one without the `stati.` prefix.
104
+ */
105
+ const CONTEXT_ROOT_NAMES = new Set([
106
+ 'site',
107
+ 'page',
108
+ 'content',
109
+ 'nav',
110
+ 'partials',
111
+ 'collection',
112
+ 'props',
113
+ 'assets',
114
+ 'generator',
115
+ 'generateSEO',
116
+ 'propValue',
117
+ ]);
118
+ function getReferenceErrorSuggestions(message) {
119
+ const name = message.match(/^(\S+) is not defined$/)?.[1];
120
+ if (name === 'it') {
121
+ return ['Stati templates use "stati" instead of Eta\'s default "it", e.g. stati.page.title'];
122
+ }
123
+ if (name && CONTEXT_ROOT_NAMES.has(name)) {
124
+ return [`Did you mean "stati.${name}"?`];
125
+ }
126
+ return [];
127
+ }
90
128
  /**
91
129
  * Parses Eta template errors to extract file location information.
92
130
  */
@@ -94,16 +132,38 @@ export function parseEtaError(error, templatePath) {
94
132
  let filePath = templatePath;
95
133
  let line;
96
134
  let column;
97
- // Try to parse error location from Eta error messages
98
- const message = error.message;
99
- // Eta syntax errors often include line/column info
100
- const locationMatch = message.match(/at line (\d+), column (\d+)/i);
135
+ let message = error.message;
136
+ // Eta's debug mode wraps runtime errors in a header with the template path and
137
+ // line. A partial rendered inside another template is wrapped once per level,
138
+ // so unwrap them all and keep the innermost location.
139
+ let header = message.match(ETA_RUNTIME_ERROR_HEADER);
140
+ while (header) {
141
+ if (header[1] !== undefined) {
142
+ filePath = header[1];
143
+ }
144
+ // The regex captures the line in group 2 (with a path) or group 3 (without)
145
+ line = parseInt(header[2] ?? header[3], 10);
146
+ column = undefined;
147
+ message = message.slice(header[0].length);
148
+ header = message.match(ETA_RUNTIME_ERROR_HEADER);
149
+ }
150
+ // Eta syntax errors report "at line N col N"
151
+ const locationMatch = message.match(/at line (\d+),? col(?:umn)? (\d+)/i);
101
152
  if (locationMatch && locationMatch[1] && locationMatch[2]) {
102
153
  line = parseInt(locationMatch[1], 10);
103
154
  column = parseInt(locationMatch[2], 10);
104
155
  }
156
+ let suggestions = [];
157
+ const unknownProperty = parseUnknownPropertyMessage(message);
158
+ if (unknownProperty) {
159
+ message = unknownProperty.message;
160
+ suggestions = unknownProperty.suggestions;
161
+ }
162
+ else if (error.name === 'ReferenceError') {
163
+ suggestions = getReferenceErrorSuggestions(message);
164
+ }
105
165
  // Try to extract file path from stack trace if not provided
106
- if (!filePath && error.stack) {
166
+ if (!filePath && line === undefined && error.stack) {
107
167
  const stackLines = error.stack.split('\n');
108
168
  for (const stackLine of stackLines) {
109
169
  const fileMatch = stackLine.match(/at.*\(([^)]+\.eta):(\d+):(\d+)\)/);
@@ -115,7 +175,7 @@ export function parseEtaError(error, templatePath) {
115
175
  }
116
176
  }
117
177
  }
118
- return new TemplateError(message, filePath, line, column);
178
+ return new TemplateError(message, filePath, line, column, undefined, suggestions);
119
179
  }
120
180
  /**
121
181
  * Enhances a generic error with template-specific context.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stati/core",
3
- "version": "1.26.2",
3
+ "version": "1.27.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",