@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.
- package/README.md +58 -40
- 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/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 +2 -0
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +4 -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/package.json +1 -1
|
@@ -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"}
|
package/dist/core/utils/index.js
CHANGED
|
@@ -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;
|
|
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:
|
|
14
|
-
right:
|
|
15
|
-
z-index:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
font-weight: 400;
|
|
31
|
+
line-height: 1.5;
|
|
32
|
+
text-align: left;
|
|
26
33
|
">
|
|
27
|
-
<div style="
|
|
28
|
-
|
|
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="
|
|
31
|
-
|
|
38
|
+
<div style="font-weight: 600; overflow-wrap: anywhere;">
|
|
39
|
+
Partial "${partialName}" not found
|
|
32
40
|
</div>
|
|
33
|
-
<div style="
|
|
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:
|
|
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
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
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 =
|
|
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;
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
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.
|