mancha 0.15.0 → 0.16.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 +9 -2
- package/dist/browser.js +1 -1
- package/dist/cli.js +102 -13
- package/dist/cli.js.map +1 -1
- package/dist/mancha.js +1 -1
- package/dist/plugins.js +10 -0
- package/dist/plugins.js.map +1 -1
- package/dist/renderer.js +2 -0
- package/dist/renderer.js.map +1 -1
- package/dist/safe_browser.js +1 -1
- package/dist/test_types/api.d.ts +11 -0
- package/dist/test_types/api.js +3 -0
- package/dist/test_types/api.js.map +1 -0
- package/dist/test_types/nested/child.d.ts +12 -0
- package/dist/test_types/nested/child.js +2 -0
- package/dist/test_types/nested/child.js.map +1 -0
- package/dist/test_types/product.d.ts +11 -0
- package/dist/test_types/product.js +3 -0
- package/dist/test_types/product.js.map +1 -0
- package/dist/test_types/user.d.ts +10 -0
- package/dist/test_types/user.js +3 -0
- package/dist/test_types/user.js.map +1 -0
- package/dist/type_checker.d.ts +6 -0
- package/dist/type_checker.js +311 -0
- package/dist/type_checker.js.map +1 -0
- package/docs/quickstart.md +281 -2
- package/package.json +11 -8
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import { JSDOM } from "jsdom";
|
|
6
|
+
import { getAttributeOrDataset } from "./dome.js";
|
|
7
|
+
async function getDiagnostics(content, options) {
|
|
8
|
+
// Use OS temp directory to avoid interfering with project structure
|
|
9
|
+
// but configure baseUrl for correct module resolution if HTML file path is provided
|
|
10
|
+
const baseDir = options.filePath ? path.dirname(path.resolve(options.filePath)) : process.cwd();
|
|
11
|
+
const tempFilePath = path.join(os.tmpdir(), `temp_type_check_${Math.random().toString(36).substring(2, 15)}.ts`);
|
|
12
|
+
await fs.writeFile(tempFilePath, content);
|
|
13
|
+
const host = ts.createCompilerHost({
|
|
14
|
+
noEmit: true,
|
|
15
|
+
strict: options.strict,
|
|
16
|
+
strictNullChecks: options.strict,
|
|
17
|
+
target: ts.ScriptTarget.ES2022,
|
|
18
|
+
module: ts.ModuleKind.ESNext,
|
|
19
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
20
|
+
baseUrl: baseDir,
|
|
21
|
+
skipLibCheck: true,
|
|
22
|
+
skipDefaultLibCheck: false,
|
|
23
|
+
allowImportingTsExtensions: true,
|
|
24
|
+
resolveJsonModule: true,
|
|
25
|
+
allowSyntheticDefaultImports: true,
|
|
26
|
+
});
|
|
27
|
+
const program = ts.createProgram([tempFilePath], {
|
|
28
|
+
noEmit: true,
|
|
29
|
+
strict: options.strict,
|
|
30
|
+
strictNullChecks: options.strict,
|
|
31
|
+
target: ts.ScriptTarget.ES2022,
|
|
32
|
+
module: ts.ModuleKind.ESNext,
|
|
33
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
34
|
+
baseUrl: baseDir,
|
|
35
|
+
skipLibCheck: true,
|
|
36
|
+
skipDefaultLibCheck: false,
|
|
37
|
+
allowImportingTsExtensions: true,
|
|
38
|
+
resolveJsonModule: true,
|
|
39
|
+
allowSyntheticDefaultImports: true,
|
|
40
|
+
}, host);
|
|
41
|
+
const allDiagnostics = ts.getPreEmitDiagnostics(program);
|
|
42
|
+
await fs.unlink(tempFilePath);
|
|
43
|
+
// Filter out irrelevant diagnostics (keep semantic errors 2000-2999 and strict mode errors 18000-18999)
|
|
44
|
+
return allDiagnostics.filter((d) => (d.code >= 2000 && d.code < 3000) || (d.code >= 18000 && d.code < 19000));
|
|
45
|
+
}
|
|
46
|
+
// Replace @import:MODULE_PATH:TYPE_NAME with import("MODULE_PATH").TYPE_NAME
|
|
47
|
+
// and resolve relative paths to absolute paths
|
|
48
|
+
function replaceImportSyntax(typeString, baseDir) {
|
|
49
|
+
return typeString.replace(/@import:([^:]+):([A-Za-z_][A-Za-z0-9_]*)/g, (match, modulePath, typeName) => {
|
|
50
|
+
// If baseDir is provided and the path is relative, resolve it to an absolute path
|
|
51
|
+
if (baseDir && (modulePath.startsWith('./') || modulePath.startsWith('../'))) {
|
|
52
|
+
const absolutePath = path.resolve(baseDir, modulePath);
|
|
53
|
+
return `import("${absolutePath}").${typeName}`;
|
|
54
|
+
}
|
|
55
|
+
return `import("${modulePath}").${typeName}`;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function buildTypeScriptSource(types, scope, baseDir) {
|
|
59
|
+
const namespace = `M${Math.random().toString(36).substring(2, 15)}`;
|
|
60
|
+
// Apply import syntax transformation to all type values
|
|
61
|
+
const declarations = Array.from(types.entries())
|
|
62
|
+
.map(([key, value]) => {
|
|
63
|
+
const resolvedType = replaceImportSyntax(value, baseDir);
|
|
64
|
+
return `declare let ${key}: ${resolvedType};`;
|
|
65
|
+
})
|
|
66
|
+
.join("\n");
|
|
67
|
+
// Recursively build checks from scope
|
|
68
|
+
const buildChecks = (currentScope, indent = "") => {
|
|
69
|
+
let result = "";
|
|
70
|
+
// Add expressions in current scope
|
|
71
|
+
for (const expr of currentScope.expressions) {
|
|
72
|
+
result += `${indent}(${expr});\n`;
|
|
73
|
+
}
|
|
74
|
+
// Add for loops with their nested scopes
|
|
75
|
+
for (const forLoop of currentScope.forLoops) {
|
|
76
|
+
result += `${indent}for (const ${forLoop.itemName} of ${forLoop.itemsExpression}) {\n`;
|
|
77
|
+
result += buildChecks(forLoop.scope, indent + " ");
|
|
78
|
+
result += `${indent}}\n`;
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
const checks = buildChecks(scope);
|
|
83
|
+
return `namespace ${namespace} {\n${declarations}\n${checks}}`;
|
|
84
|
+
}
|
|
85
|
+
// Helper to check if an element is nested within another :types element
|
|
86
|
+
function hasTypesAncestor(element) {
|
|
87
|
+
let parent = element.parentElement;
|
|
88
|
+
while (parent) {
|
|
89
|
+
if (getAttributeOrDataset(parent, "types", ":")) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
parent = parent.parentElement;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
// Compute type of for-loop variable based on the items expression
|
|
97
|
+
function computeForLoopVariableType(itemsType) {
|
|
98
|
+
// Handle array types: string[] -> string, Array<T> -> T
|
|
99
|
+
if (itemsType.endsWith("[]")) {
|
|
100
|
+
return itemsType.slice(0, -2);
|
|
101
|
+
}
|
|
102
|
+
if (itemsType.match(/^Array<(.+)>$/)) {
|
|
103
|
+
return itemsType.match(/^Array<(.+)>$/)[1];
|
|
104
|
+
}
|
|
105
|
+
// For complex types, try to infer element type
|
|
106
|
+
// This is a simple heuristic - for more complex cases, TypeScript would need to resolve the type
|
|
107
|
+
return "any";
|
|
108
|
+
}
|
|
109
|
+
// Find for-loop ancestors of an element and compute their variable types
|
|
110
|
+
function getForLoopContext(element, typesMap) {
|
|
111
|
+
const forLoopTypes = new Map();
|
|
112
|
+
let current = element.parentElement;
|
|
113
|
+
while (current) {
|
|
114
|
+
const forAttr = getAttributeOrDataset(current, "for", ":");
|
|
115
|
+
if (forAttr) {
|
|
116
|
+
const parts = forAttr.split(" in ");
|
|
117
|
+
const itemName = parts[0].trim();
|
|
118
|
+
const itemsExpression = parts[1].trim();
|
|
119
|
+
// Try to determine the type of the items expression
|
|
120
|
+
// First check if it's a simple variable name in our types map
|
|
121
|
+
if (typesMap.has(itemsExpression)) {
|
|
122
|
+
const itemsType = typesMap.get(itemsExpression);
|
|
123
|
+
const itemType = computeForLoopVariableType(itemsType);
|
|
124
|
+
forLoopTypes.set(itemName, itemType);
|
|
125
|
+
}
|
|
126
|
+
else if (forLoopTypes.has(itemsExpression)) {
|
|
127
|
+
// Check if it's a for-loop variable from an outer loop (for shadowing)
|
|
128
|
+
const itemsType = forLoopTypes.get(itemsExpression);
|
|
129
|
+
const itemType = computeForLoopVariableType(itemsType);
|
|
130
|
+
forLoopTypes.set(itemName, itemType);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Try to infer from more complex expressions like user.scores
|
|
134
|
+
// For now, use a simple heuristic
|
|
135
|
+
const match = itemsExpression.match(/^(\w+)\.(\w+)$/);
|
|
136
|
+
if (match && forLoopTypes.has(match[1])) {
|
|
137
|
+
// e.g., user.scores where user is already in forLoopTypes
|
|
138
|
+
const parentType = forLoopTypes.get(match[1]);
|
|
139
|
+
const propertyName = match[2];
|
|
140
|
+
// Extract property type from parent type (simple heuristic)
|
|
141
|
+
const propMatch = parentType.match(new RegExp(`${propertyName}:\\s*([^,}]+)`));
|
|
142
|
+
if (propMatch) {
|
|
143
|
+
const propType = propMatch[1].trim();
|
|
144
|
+
const itemType = computeForLoopVariableType(propType);
|
|
145
|
+
forLoopTypes.set(itemName, itemType);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Stop if we hit a :types boundary (don't cross into parent :types scopes)
|
|
151
|
+
if (current !== element && getAttributeOrDataset(current, "types", ":")) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
current = current.parentElement;
|
|
155
|
+
}
|
|
156
|
+
return forLoopTypes;
|
|
157
|
+
}
|
|
158
|
+
// Recursively process a :types element and its nested :types descendants
|
|
159
|
+
async function processTypesElement(element, parentTypes, options, processedElements) {
|
|
160
|
+
// Skip if already processed
|
|
161
|
+
if (processedElements.has(element)) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
processedElements.add(element);
|
|
165
|
+
const allDiagnostics = [];
|
|
166
|
+
const typesAttr = getAttributeOrDataset(element, "types", ":");
|
|
167
|
+
if (!typesAttr)
|
|
168
|
+
return allDiagnostics;
|
|
169
|
+
try {
|
|
170
|
+
// Parse types for this element
|
|
171
|
+
const elementTypes = new Map(Object.entries(JSON.parse(typesAttr)));
|
|
172
|
+
// Merge with parent types (element types override parent types)
|
|
173
|
+
const mergedTypes = new Map([...parentTypes, ...elementTypes]);
|
|
174
|
+
// Get expressions for this element (excluding nested :types descendants)
|
|
175
|
+
const expressions = getExpressionsExcludingNestedTypes(element);
|
|
176
|
+
// Type check expressions in this scope
|
|
177
|
+
const baseDir = options.filePath ? path.dirname(path.resolve(options.filePath)) : undefined;
|
|
178
|
+
const source = buildTypeScriptSource(mergedTypes, expressions, baseDir);
|
|
179
|
+
const diagnostics = await getDiagnostics(source, options);
|
|
180
|
+
allDiagnostics.push(...diagnostics);
|
|
181
|
+
// Find and process nested :types elements
|
|
182
|
+
const nestedTypesElements = findDirectNestedTypesElements(element);
|
|
183
|
+
for (const nestedElement of nestedTypesElements) {
|
|
184
|
+
// Compute for-loop context for the nested element
|
|
185
|
+
const forLoopContext = getForLoopContext(nestedElement, mergedTypes);
|
|
186
|
+
// Merge for-loop variables with parent types (for-loop variables take precedence)
|
|
187
|
+
const nestedParentTypes = new Map([...mergedTypes, ...forLoopContext]);
|
|
188
|
+
const nestedDiagnostics = await processTypesElement(nestedElement, nestedParentTypes, options, processedElements);
|
|
189
|
+
allDiagnostics.push(...nestedDiagnostics);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
console.error("Error parsing :types attribute:", e);
|
|
194
|
+
}
|
|
195
|
+
return allDiagnostics;
|
|
196
|
+
}
|
|
197
|
+
// Find nested :types elements that are direct descendants (not grandchildren through other :types)
|
|
198
|
+
function findDirectNestedTypesElements(element) {
|
|
199
|
+
const result = [];
|
|
200
|
+
const walker = element.ownerDocument.createTreeWalker(element, 1); // 1 = SHOW_ELEMENT
|
|
201
|
+
while (walker.nextNode()) {
|
|
202
|
+
const node = walker.currentNode;
|
|
203
|
+
if (node === element)
|
|
204
|
+
continue;
|
|
205
|
+
const typesAttr = getAttributeOrDataset(node, "types", ":");
|
|
206
|
+
if (typesAttr) {
|
|
207
|
+
// Check if this is a direct nested :types (not nested within another :types first)
|
|
208
|
+
let parent = node.parentElement;
|
|
209
|
+
let foundIntermediateTypes = false;
|
|
210
|
+
while (parent && parent !== element) {
|
|
211
|
+
if (getAttributeOrDataset(parent, "types", ":")) {
|
|
212
|
+
foundIntermediateTypes = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
parent = parent.parentElement;
|
|
216
|
+
}
|
|
217
|
+
if (!foundIntermediateTypes) {
|
|
218
|
+
result.push(node);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
// Get expressions excluding those in nested :types elements
|
|
225
|
+
function getExpressionsExcludingNestedTypes(root) {
|
|
226
|
+
const scope = { expressions: [], forLoops: [] };
|
|
227
|
+
const processedForElements = new Set();
|
|
228
|
+
const processElement = (element, currentScope) => {
|
|
229
|
+
// Stop if we encounter a nested :types element
|
|
230
|
+
if (element !== root && getAttributeOrDataset(element, "types", ":")) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Check if this element has a :for attribute
|
|
234
|
+
const forAttr = getAttributeOrDataset(element, "for", ":");
|
|
235
|
+
if (forAttr) {
|
|
236
|
+
if (processedForElements.has(element))
|
|
237
|
+
return;
|
|
238
|
+
processedForElements.add(element);
|
|
239
|
+
const parts = forAttr.split(" in ");
|
|
240
|
+
const itemName = parts[0].trim();
|
|
241
|
+
const itemsExpression = parts[1].trim();
|
|
242
|
+
currentScope.expressions.push(itemsExpression);
|
|
243
|
+
const forScope = { expressions: [], forLoops: [] };
|
|
244
|
+
processDescendants(element, forScope);
|
|
245
|
+
currentScope.forLoops.push({ itemName, itemsExpression, scope: forScope });
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// Process attributes
|
|
249
|
+
for (const attr of Array.from(element.attributes)) {
|
|
250
|
+
if ((attr.name.startsWith(":") || attr.name.startsWith("data-")) &&
|
|
251
|
+
attr.name !== ":types" &&
|
|
252
|
+
attr.name !== "data-types") {
|
|
253
|
+
currentScope.expressions.push(attr.value);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Process children
|
|
257
|
+
for (const child of Array.from(element.childNodes)) {
|
|
258
|
+
if (child.nodeType === 1) {
|
|
259
|
+
processElement(child, currentScope);
|
|
260
|
+
}
|
|
261
|
+
else if (child.nodeType === 3) {
|
|
262
|
+
processTextNode(child, currentScope);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const processDescendants = (element, currentScope) => {
|
|
268
|
+
for (const child of Array.from(element.childNodes)) {
|
|
269
|
+
if (child.nodeType === 1) {
|
|
270
|
+
processElement(child, currentScope);
|
|
271
|
+
}
|
|
272
|
+
else if (child.nodeType === 3) {
|
|
273
|
+
processTextNode(child, currentScope);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const processTextNode = (textNode, currentScope) => {
|
|
278
|
+
const text = textNode.nodeValue;
|
|
279
|
+
if (text) {
|
|
280
|
+
const matches = text.matchAll(/{{(.*?)}}/g);
|
|
281
|
+
for (const match of matches) {
|
|
282
|
+
currentScope.expressions.push(match[1].trim());
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
// Start processing from root's children
|
|
287
|
+
for (const child of Array.from(root.childNodes)) {
|
|
288
|
+
if (child.nodeType === 1) {
|
|
289
|
+
processElement(child, scope);
|
|
290
|
+
}
|
|
291
|
+
else if (child.nodeType === 3) {
|
|
292
|
+
processTextNode(child, scope);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return scope;
|
|
296
|
+
}
|
|
297
|
+
export async function typeCheck(html, options) {
|
|
298
|
+
const dom = new JSDOM(html);
|
|
299
|
+
const allTypeNodes = dom.window.document.querySelectorAll("[\\:types], [data-types]");
|
|
300
|
+
const allDiagnostics = [];
|
|
301
|
+
const processedElements = new Set();
|
|
302
|
+
// Find top-level :types elements (not nested within other :types)
|
|
303
|
+
const topLevelTypeNodes = Array.from(allTypeNodes).filter((node) => !hasTypesAncestor(node));
|
|
304
|
+
// Process each top-level :types element and its nested descendants
|
|
305
|
+
for (const node of topLevelTypeNodes) {
|
|
306
|
+
const diagnostics = await processTypesElement(node, new Map(), options, processedElements);
|
|
307
|
+
allDiagnostics.push(...diagnostics);
|
|
308
|
+
}
|
|
309
|
+
return allDiagnostics;
|
|
310
|
+
}
|
|
311
|
+
//# sourceMappingURL=type_checker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type_checker.js","sourceRoot":"","sources":["../src/type_checker.ts"],"names":[],"mappings":"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"}
|
package/docs/quickstart.md
CHANGED
|
@@ -41,8 +41,8 @@ For example, a basic form might look like this
|
|
|
41
41
|
|
|
42
42
|
In the code above, the `:on:submit` tag simply prints 'submitted' to the console. Note that `Mancha`
|
|
43
43
|
automatically prevents the form submission from refreshing the page by calling
|
|
44
|
-
`event.
|
|
45
|
-
|
|
44
|
+
`event.preventDefault()`. You can also trigger this behavior with other events by adding a `.prevent`
|
|
45
|
+
modifier to the event attribute, for example `:on:click.prevent`. To provide more complex handlers, you can define callbacks as a function:
|
|
46
46
|
|
|
47
47
|
```html
|
|
48
48
|
<body :data="{ name: null, message: null }">
|
|
@@ -279,3 +279,282 @@ describe("My Component", () => {
|
|
|
279
279
|
});
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
+
## Type Checking (Experimental)
|
|
283
|
+
|
|
284
|
+
**⚠️ This feature is experimental and may change in future versions.**
|
|
285
|
+
|
|
286
|
+
`mancha` includes an experimental type checker that can validate your template expressions using TypeScript. This helps catch type errors during development before they become runtime errors.
|
|
287
|
+
|
|
288
|
+
### Basic Type Checking
|
|
289
|
+
|
|
290
|
+
Use the `:types` attribute to declare types for variables in your templates:
|
|
291
|
+
|
|
292
|
+
```html
|
|
293
|
+
<div :types='{"name": "string", "age": "number"}'>
|
|
294
|
+
<span>{{ name.toUpperCase() }}</span>
|
|
295
|
+
<span>{{ age.toFixed(0) }}</span>
|
|
296
|
+
</div>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The type checker will validate that:
|
|
300
|
+
- `name.toUpperCase()` is valid (string has toUpperCase method)
|
|
301
|
+
- `age.toFixed(0)` is valid (number has toFixed method)
|
|
302
|
+
- Using `name.toFixed()` would be an error (string doesn't have toFixed)
|
|
303
|
+
|
|
304
|
+
### Running the Type Checker
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
# Check a single file
|
|
308
|
+
npx mancha check src/index.html
|
|
309
|
+
|
|
310
|
+
# Check with strict mode
|
|
311
|
+
npx mancha check src/index.html --strict
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Stripping Types in Production
|
|
315
|
+
|
|
316
|
+
The `:types` attributes are only used for static analysis and have no runtime behavior. However, you may want to remove them from your production HTML to reduce file size and avoid exposing type information:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Render with types stripped
|
|
320
|
+
npx mancha render src/index.html --output public/index.html --strip-types
|
|
321
|
+
|
|
322
|
+
# Render without stripping (default)
|
|
323
|
+
npx mancha render src/index.html --output public/index.html
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The `--strip-types` flag removes all `:types` and `data-types` attributes from the rendered output.
|
|
327
|
+
|
|
328
|
+
### Type Checking with For-Loops
|
|
329
|
+
|
|
330
|
+
The type checker understands `:for` loops and infers the item type from the array:
|
|
331
|
+
|
|
332
|
+
```html
|
|
333
|
+
<div :types='{"users": "{ name: string, age: number }[]"}'>
|
|
334
|
+
<ul :for="user in users">
|
|
335
|
+
<!-- 'user' is automatically typed as { name: string, age: number } -->
|
|
336
|
+
<li>{{ user.name.toUpperCase() }}</li>
|
|
337
|
+
<li>{{ user.age.toFixed(0) }}</li>
|
|
338
|
+
</ul>
|
|
339
|
+
</div>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Nested Scopes
|
|
343
|
+
|
|
344
|
+
Child scopes inherit types from parent scopes:
|
|
345
|
+
|
|
346
|
+
```html
|
|
347
|
+
<div :types='{"name": "string", "age": "number"}'>
|
|
348
|
+
<span>{{ name.toUpperCase() }}</span>
|
|
349
|
+
|
|
350
|
+
<div :types='{"city": "string"}'>
|
|
351
|
+
<!-- This scope has access to: name, age, and city -->
|
|
352
|
+
<span>{{ name.toLowerCase() }}</span>
|
|
353
|
+
<span>{{ city.toUpperCase() }}</span>
|
|
354
|
+
<span>{{ age.toFixed(0) }}</span>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Child scopes can override parent types:
|
|
360
|
+
|
|
361
|
+
```html
|
|
362
|
+
<div :types='{"value": "string"}'>
|
|
363
|
+
<span>{{ value.toUpperCase() }}</span>
|
|
364
|
+
|
|
365
|
+
<div :types='{"value": "number"}'>
|
|
366
|
+
<!-- 'value' is now number, not string -->
|
|
367
|
+
<span>{{ value.toFixed(2) }}</span>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<!-- Back to string scope -->
|
|
371
|
+
<span>{{ value.toLowerCase() }}</span>
|
|
372
|
+
</div>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Importing Types (Experimental)
|
|
376
|
+
|
|
377
|
+
**⚠️ This feature is highly experimental and the syntax may change.**
|
|
378
|
+
|
|
379
|
+
You can import TypeScript types from external files using the `@import:` syntax:
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// types/user.ts
|
|
383
|
+
export interface User {
|
|
384
|
+
id: number;
|
|
385
|
+
name: string;
|
|
386
|
+
email: string;
|
|
387
|
+
isAdmin: boolean;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface Product {
|
|
391
|
+
id: number;
|
|
392
|
+
name: string;
|
|
393
|
+
price: number;
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
```html
|
|
398
|
+
<!-- Import a single type -->
|
|
399
|
+
<div :types='{"user": "@import:./types/user.ts:User"}'>
|
|
400
|
+
<span>{{ user.name.toUpperCase() }}</span>
|
|
401
|
+
<span>{{ user.email.toLowerCase() }}</span>
|
|
402
|
+
<span :show="user.isAdmin">Admin Badge</span>
|
|
403
|
+
</div>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
#### Import Syntax
|
|
407
|
+
|
|
408
|
+
The format is: `@import:MODULE_PATH:TYPE_NAME`
|
|
409
|
+
|
|
410
|
+
- **MODULE_PATH**:
|
|
411
|
+
- Starts with `.` → relative path (e.g., `./types/user.ts`)
|
|
412
|
+
- No `.` → library import (e.g., `moment`)
|
|
413
|
+
- **TYPE_NAME**: The exported type/interface name
|
|
414
|
+
|
|
415
|
+
#### Arrays of Imported Types
|
|
416
|
+
|
|
417
|
+
```html
|
|
418
|
+
<div :types='{"users": "@import:./types/user.ts:User[]"}'>
|
|
419
|
+
<ul :for="user in users">
|
|
420
|
+
<li>{{ user.name }} - {{ user.email }}</li>
|
|
421
|
+
</ul>
|
|
422
|
+
</div>
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
#### Multiple Imports
|
|
426
|
+
|
|
427
|
+
```html
|
|
428
|
+
<div :types='{
|
|
429
|
+
"user": "@import:./types/user.ts:User",
|
|
430
|
+
"product": "@import:./types/user.ts:Product",
|
|
431
|
+
"count": "number"
|
|
432
|
+
}'>
|
|
433
|
+
<span>{{ user.name }}</span>
|
|
434
|
+
<span>{{ product.name }} - ${{ product.price.toFixed(2) }}</span>
|
|
435
|
+
<span>Total: {{ count }}</span>
|
|
436
|
+
</div>
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
#### Imports in Complex Types
|
|
440
|
+
|
|
441
|
+
Use imports anywhere you'd use a type:
|
|
442
|
+
|
|
443
|
+
```html
|
|
444
|
+
<!-- In object types -->
|
|
445
|
+
<div :types='{"response": "{ data: @import:./types/user.ts:User[], total: number }"}'>
|
|
446
|
+
<span>Total users: {{ response.total }}</span>
|
|
447
|
+
<ul :for="user in response.data">
|
|
448
|
+
<li>{{ user.name }}</li>
|
|
449
|
+
</ul>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<!-- With generics -->
|
|
453
|
+
<div :types='{"response": "@import:./api.ts:ApiResponse<@import:./types/user.ts:User>"}'>
|
|
454
|
+
<span>{{ response.data.name }}</span>
|
|
455
|
+
<span>Status: {{ response.status }}</span>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
<!-- With unions -->
|
|
459
|
+
<div :types='{"user": "@import:./types/user.ts:User | null"}'>
|
|
460
|
+
<span :show="user !== null">{{ user.name }}</span>
|
|
461
|
+
<span :show="user === null">Not logged in</span>
|
|
462
|
+
</div>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
#### Nested Scopes with Imports
|
|
466
|
+
|
|
467
|
+
Imports are inherited by nested scopes:
|
|
468
|
+
|
|
469
|
+
```html
|
|
470
|
+
<div :types='{"user": "@import:./types/user.ts:User"}'>
|
|
471
|
+
<span>{{ user.name }}</span>
|
|
472
|
+
|
|
473
|
+
<div :types='{"product": "@import:./types/user.ts:Product"}'>
|
|
474
|
+
<!-- Has access to both User and Product types -->
|
|
475
|
+
<span>{{ user.name }} bought {{ product.name }}</span>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
#### Complex Example
|
|
481
|
+
|
|
482
|
+
```html
|
|
483
|
+
<div :types='{"orders": "@import:./types/orders.ts:Order[]"}'>
|
|
484
|
+
<div :for="order in orders">
|
|
485
|
+
<h2>Order #{{ order.id }}</h2>
|
|
486
|
+
<p>Customer: {{ order.customer.name }}</p>
|
|
487
|
+
|
|
488
|
+
<div :types='{"selectedProduct": "@import:./types/products.ts:Product"}'>
|
|
489
|
+
<ul :for="item in order.items">
|
|
490
|
+
<li>
|
|
491
|
+
{{ item.product.name }} x {{ item.quantity }}
|
|
492
|
+
= ${{ (item.product.price * item.quantity).toFixed(2) }}
|
|
493
|
+
</li>
|
|
494
|
+
</ul>
|
|
495
|
+
|
|
496
|
+
<div :show="selectedProduct">
|
|
497
|
+
<h3>Selected: {{ selectedProduct.name }}</h3>
|
|
498
|
+
<p>${{ selectedProduct.price.toFixed(2) }}</p>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
<p>Total: ${{ order.total.toFixed(2) }}</p>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Best Practices
|
|
508
|
+
|
|
509
|
+
1. **Start Simple**: Add types gradually, starting with the most critical paths
|
|
510
|
+
2. **Use Strict Mode**: Enable strict mode in your TypeScript config for better type safety
|
|
511
|
+
3. **Import Shared Types**: Keep commonly used types in separate files and import them
|
|
512
|
+
4. **Document Complex Types**: Add comments for complex object structures
|
|
513
|
+
5. **Test Your Types**: Run the type checker in your CI/CD pipeline
|
|
514
|
+
|
|
515
|
+
### Limitations
|
|
516
|
+
|
|
517
|
+
- The type checker analyzes templates statically, not at runtime
|
|
518
|
+
- Some dynamic JavaScript patterns may not be fully supported
|
|
519
|
+
- Import paths must be resolvable by TypeScript
|
|
520
|
+
- The import syntax is experimental and may change
|
|
521
|
+
|
|
522
|
+
### Examples
|
|
523
|
+
|
|
524
|
+
#### Form Validation
|
|
525
|
+
|
|
526
|
+
```html
|
|
527
|
+
<div :types='{
|
|
528
|
+
"formData": "{ name: string, email: string, age: number }",
|
|
529
|
+
"errors": "{ name?: string, email?: string, age?: string }"
|
|
530
|
+
}'>
|
|
531
|
+
<form>
|
|
532
|
+
<input type="text" :bind="formData.name" />
|
|
533
|
+
<span :show="errors.name" class="error">{{ errors.name }}</span>
|
|
534
|
+
|
|
535
|
+
<input type="email" :bind="formData.email" />
|
|
536
|
+
<span :show="errors.email" class="error">{{ errors.email }}</span>
|
|
537
|
+
|
|
538
|
+
<input type="number" :bind="formData.age" />
|
|
539
|
+
<span :show="errors.age" class="error">{{ errors.age }}</span>
|
|
540
|
+
</form>
|
|
541
|
+
</div>
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
#### API Response Handling
|
|
545
|
+
|
|
546
|
+
```html
|
|
547
|
+
<div :types='{
|
|
548
|
+
"response": "@import:./api.ts:ApiResponse<@import:./types/user.ts:User>",
|
|
549
|
+
"loading": "boolean",
|
|
550
|
+
"error": "string | null"
|
|
551
|
+
}'>
|
|
552
|
+
<div :show="loading">Loading...</div>
|
|
553
|
+
<div :show="error">Error: {{ error }}</div>
|
|
554
|
+
<div :show="!loading && !error && response">
|
|
555
|
+
<h1>{{ response.data.name }}</h1>
|
|
556
|
+
<p>{{ response.data.email }}</p>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
```
|
|
560
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mancha",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Javscript HTML rendering engine",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
"clean": "rm -rf dist/",
|
|
11
11
|
"build": "npx gulp build",
|
|
12
12
|
"prepare": "npm run build",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"test": "
|
|
13
|
+
"pretest": "npm run build",
|
|
14
|
+
"test:node": "mocha dist/*.test.js",
|
|
15
|
+
"test:browser": "web-test-runner dist/*browser.test.js --node-resolve --compatibility all --playwright --browsers chromium --no-sandbox",
|
|
16
|
+
"test": "npm run test:node && npm run test:browser",
|
|
16
17
|
"check_size": "brotli -c dist/mancha.js | wc -c",
|
|
17
18
|
"cli": "node dist/cli.js"
|
|
18
19
|
},
|
|
@@ -35,20 +36,22 @@
|
|
|
35
36
|
"dom-serializer": "^2.0.0",
|
|
36
37
|
"htmlparser2": "^9.1.0",
|
|
37
38
|
"jexpr": "^1.0.0-pre.9",
|
|
38
|
-
"jsdom": "^
|
|
39
|
+
"jsdom": "^27.0.1",
|
|
40
|
+
"mancha": "file:",
|
|
39
41
|
"safevalues": "^0.6.0"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
|
-
"@types/chai": "^5.
|
|
44
|
+
"@types/chai": "^5.2.3",
|
|
43
45
|
"@types/chai-as-promised": "^8.0.1",
|
|
44
46
|
"@types/jsdom": "^21.1.6",
|
|
45
47
|
"@types/mocha": "^10.0.10",
|
|
46
48
|
"@types/node": "^20.12.11",
|
|
47
49
|
"@types/path-browserify": "^1.0.1",
|
|
50
|
+
"@types/trusted-types": "^2.0.7",
|
|
48
51
|
"@types/yargs": "^17.0.29",
|
|
49
52
|
"@web/test-runner": "^0.19.0",
|
|
50
53
|
"@web/test-runner-playwright": "^0.11.1",
|
|
51
|
-
"chai": "^5.
|
|
54
|
+
"chai": "^5.3.3",
|
|
52
55
|
"chai-as-promised": "^8.0.1",
|
|
53
56
|
"css-loader": "^7.1.2",
|
|
54
57
|
"csso": "^5.0.5",
|
|
@@ -60,7 +63,7 @@
|
|
|
60
63
|
"terser-webpack-plugin": "^5.3.10",
|
|
61
64
|
"ts-node": "^10.9.2",
|
|
62
65
|
"tsec": "^0.2.8",
|
|
63
|
-
"typescript": "^5.
|
|
66
|
+
"typescript": "^5.9.3",
|
|
64
67
|
"webpack": "^5.97.1",
|
|
65
68
|
"webpack-cli": "^5.1.4",
|
|
66
69
|
"yargs": "^17.7.2"
|