dalila 1.9.2 → 1.9.3
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 +3 -2
- package/dist/cli/check.d.ts +3 -0
- package/dist/cli/check.js +902 -0
- package/dist/cli/index.js +49 -7
- package/dist/cli/routes-generator.d.ts +25 -0
- package/dist/cli/routes-generator.js +5 -5
- package/dist/router/route-tables.d.ts +23 -6
- package/dist/router/route-tables.js +19 -0
- package/dist/runtime/bind.d.ts +14 -2
- package/dist/runtime/bind.js +42 -11
- package/dist/runtime/fromHtml.d.ts +3 -2
- package/dist/runtime/fromHtml.js +0 -15
- package/dist/runtime/index.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Dalila is a reactive framework built on signals. No virtual DOM, no JSX required
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm create dalila my-app
|
|
10
|
+
npm create dalila@latest my-app
|
|
11
11
|
cd my-app
|
|
12
12
|
npm install
|
|
13
13
|
npm run dev
|
|
@@ -64,6 +64,7 @@ bind(document.getElementById('app')!, ctx);
|
|
|
64
64
|
### Routing
|
|
65
65
|
|
|
66
66
|
- [Router](./docs/router.md) — Client-side routing with nested layouts, preloading, and file-based route generation
|
|
67
|
+
- [Template Check CLI](./docs/cli/check.md) — `dalila check` static analysis for template/context consistency
|
|
67
68
|
|
|
68
69
|
### UI Components
|
|
69
70
|
|
|
@@ -214,7 +215,7 @@ src/app/
|
|
|
214
215
|
```
|
|
215
216
|
|
|
216
217
|
```bash
|
|
217
|
-
dalila routes generate
|
|
218
|
+
npx dalila routes generate
|
|
218
219
|
```
|
|
219
220
|
|
|
220
221
|
```ts
|
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { buildRouteTree, injectHtmlPathTemplates, findFile, findProjectRoot, extractParamKeys, } from './routes-generator.js';
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// TypeScript Compiler API (dynamic import)
|
|
6
|
+
// ============================================================================
|
|
7
|
+
async function loadTypeScript() {
|
|
8
|
+
try {
|
|
9
|
+
return await import('typescript');
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
console.error('❌ TypeScript is required for `dalila check`.\n' +
|
|
13
|
+
' Install it with: npm install -D typescript');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Loader return type extraction via TS Compiler API
|
|
19
|
+
// ============================================================================
|
|
20
|
+
function extractLoaderReturnKeys(ts, checker, loaderSymbol, sourceFile) {
|
|
21
|
+
function isPlainObjectLikeType(type) {
|
|
22
|
+
if (type.isUnion()) {
|
|
23
|
+
return type.types.length > 0 && type.types.every(isPlainObjectLikeType);
|
|
24
|
+
}
|
|
25
|
+
if (type.isIntersection()) {
|
|
26
|
+
return type.types.length > 0 && type.types.every(isPlainObjectLikeType);
|
|
27
|
+
}
|
|
28
|
+
if ((type.flags & ts.TypeFlags.Object) === 0)
|
|
29
|
+
return false;
|
|
30
|
+
const objectType = type;
|
|
31
|
+
if (objectType.getCallSignatures().length > 0)
|
|
32
|
+
return false;
|
|
33
|
+
if (checker.isArrayType(type) || checker.isTupleType(type))
|
|
34
|
+
return false;
|
|
35
|
+
const objectFlags = objectType.objectFlags ?? 0;
|
|
36
|
+
if ((objectFlags & ts.ObjectFlags.Reference) !== 0) {
|
|
37
|
+
const target = objectType.target;
|
|
38
|
+
const targetName = target?.symbol?.getName();
|
|
39
|
+
if (targetName === 'Array' || targetName === 'ReadonlyArray')
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
const resolvedLoaderSymbol = loaderSymbol.flags & ts.SymbolFlags.Alias
|
|
45
|
+
? checker.getAliasedSymbol(loaderSymbol)
|
|
46
|
+
: loaderSymbol;
|
|
47
|
+
const loaderLocation = resolvedLoaderSymbol.valueDeclaration ??
|
|
48
|
+
resolvedLoaderSymbol.declarations?.[0] ??
|
|
49
|
+
sourceFile;
|
|
50
|
+
const loaderType = checker.getTypeOfSymbolAtLocation(resolvedLoaderSymbol, loaderLocation);
|
|
51
|
+
const callSignatures = loaderType.getCallSignatures();
|
|
52
|
+
if (callSignatures.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
let returnType = checker.getReturnTypeOfSignature(callSignatures[0]);
|
|
55
|
+
// Unwrap Promise<T>
|
|
56
|
+
const typeSymbol = returnType.getSymbol();
|
|
57
|
+
const targetSymbol = returnType.target?.symbol;
|
|
58
|
+
if (typeSymbol?.getName() === 'Promise' ||
|
|
59
|
+
targetSymbol?.getName() === 'Promise') {
|
|
60
|
+
const typeArgs = checker.getTypeArguments(returnType);
|
|
61
|
+
if (typeArgs && typeArgs.length > 0) {
|
|
62
|
+
returnType = typeArgs[0];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!isPlainObjectLikeType(returnType))
|
|
66
|
+
return null;
|
|
67
|
+
return returnType.getProperties().map(p => p.getName());
|
|
68
|
+
}
|
|
69
|
+
function getLoaderExportSymbol(ts, checker, sourceFile) {
|
|
70
|
+
const symbol = checker.getSymbolAtLocation(sourceFile);
|
|
71
|
+
if (!symbol)
|
|
72
|
+
return null;
|
|
73
|
+
const moduleExports = checker.getExportsOfModule(symbol);
|
|
74
|
+
const loaderSymbol = moduleExports.find(s => s.getName() === 'loader');
|
|
75
|
+
if (!loaderSymbol)
|
|
76
|
+
return null;
|
|
77
|
+
const runtimeFlags = ts.SymbolFlags.Function |
|
|
78
|
+
ts.SymbolFlags.Variable |
|
|
79
|
+
ts.SymbolFlags.Property |
|
|
80
|
+
ts.SymbolFlags.Method |
|
|
81
|
+
ts.SymbolFlags.GetAccessor |
|
|
82
|
+
ts.SymbolFlags.Alias;
|
|
83
|
+
return (loaderSymbol.flags & runtimeFlags) !== 0 ? loaderSymbol : null;
|
|
84
|
+
}
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Template identifier extraction (regex-based)
|
|
87
|
+
// ============================================================================
|
|
88
|
+
const JS_KEYWORDS = new Set([
|
|
89
|
+
'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
|
|
90
|
+
'typeof', 'instanceof', 'void', 'delete', 'in', 'of',
|
|
91
|
+
'new', 'this', 'if', 'else', 'return', 'switch', 'case',
|
|
92
|
+
'break', 'continue', 'for', 'while', 'do', 'try', 'catch',
|
|
93
|
+
'finally', 'throw', 'const', 'let', 'var', 'function', 'class',
|
|
94
|
+
]);
|
|
95
|
+
function extractParamNames(segment) {
|
|
96
|
+
const result = [];
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
const IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
99
|
+
function splitTopLevel(input) {
|
|
100
|
+
const parts = [];
|
|
101
|
+
let start = 0;
|
|
102
|
+
let depthParen = 0;
|
|
103
|
+
let depthBracket = 0;
|
|
104
|
+
let depthBrace = 0;
|
|
105
|
+
let inString = null;
|
|
106
|
+
let escaped = false;
|
|
107
|
+
for (let i = 0; i < input.length; i++) {
|
|
108
|
+
const ch = input[i];
|
|
109
|
+
if (inString) {
|
|
110
|
+
if (escaped) {
|
|
111
|
+
escaped = false;
|
|
112
|
+
}
|
|
113
|
+
else if (ch === '\\') {
|
|
114
|
+
escaped = true;
|
|
115
|
+
}
|
|
116
|
+
else if (ch === inString) {
|
|
117
|
+
inString = null;
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
122
|
+
inString = ch;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (ch === '(')
|
|
126
|
+
depthParen++;
|
|
127
|
+
else if (ch === ')')
|
|
128
|
+
depthParen = Math.max(0, depthParen - 1);
|
|
129
|
+
else if (ch === '[')
|
|
130
|
+
depthBracket++;
|
|
131
|
+
else if (ch === ']')
|
|
132
|
+
depthBracket = Math.max(0, depthBracket - 1);
|
|
133
|
+
else if (ch === '{')
|
|
134
|
+
depthBrace++;
|
|
135
|
+
else if (ch === '}')
|
|
136
|
+
depthBrace = Math.max(0, depthBrace - 1);
|
|
137
|
+
else if (ch === ',' &&
|
|
138
|
+
depthParen === 0 &&
|
|
139
|
+
depthBracket === 0 &&
|
|
140
|
+
depthBrace === 0) {
|
|
141
|
+
parts.push(input.slice(start, i));
|
|
142
|
+
start = i + 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
parts.push(input.slice(start));
|
|
146
|
+
return parts;
|
|
147
|
+
}
|
|
148
|
+
function topLevelIndexOf(input, target) {
|
|
149
|
+
let depthParen = 0;
|
|
150
|
+
let depthBracket = 0;
|
|
151
|
+
let depthBrace = 0;
|
|
152
|
+
let inString = null;
|
|
153
|
+
let escaped = false;
|
|
154
|
+
for (let i = 0; i < input.length; i++) {
|
|
155
|
+
const ch = input[i];
|
|
156
|
+
if (inString) {
|
|
157
|
+
if (escaped) {
|
|
158
|
+
escaped = false;
|
|
159
|
+
}
|
|
160
|
+
else if (ch === '\\') {
|
|
161
|
+
escaped = true;
|
|
162
|
+
}
|
|
163
|
+
else if (ch === inString) {
|
|
164
|
+
inString = null;
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
169
|
+
inString = ch;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (ch === '(')
|
|
173
|
+
depthParen++;
|
|
174
|
+
else if (ch === ')')
|
|
175
|
+
depthParen = Math.max(0, depthParen - 1);
|
|
176
|
+
else if (ch === '[')
|
|
177
|
+
depthBracket++;
|
|
178
|
+
else if (ch === ']')
|
|
179
|
+
depthBracket = Math.max(0, depthBracket - 1);
|
|
180
|
+
else if (ch === '{')
|
|
181
|
+
depthBrace++;
|
|
182
|
+
else if (ch === '}')
|
|
183
|
+
depthBrace = Math.max(0, depthBrace - 1);
|
|
184
|
+
else if (ch === target &&
|
|
185
|
+
depthParen === 0 &&
|
|
186
|
+
depthBracket === 0 &&
|
|
187
|
+
depthBrace === 0) {
|
|
188
|
+
return i;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return -1;
|
|
192
|
+
}
|
|
193
|
+
function stripDefaultValue(input) {
|
|
194
|
+
const eqIdx = topLevelIndexOf(input, '=');
|
|
195
|
+
if (eqIdx >= 0)
|
|
196
|
+
return input.slice(0, eqIdx).trim();
|
|
197
|
+
return input.trim();
|
|
198
|
+
}
|
|
199
|
+
function collectFromPattern(pattern) {
|
|
200
|
+
let param = pattern.trim();
|
|
201
|
+
if (!param)
|
|
202
|
+
return;
|
|
203
|
+
param = param.replace(/^\.\.\./, '').trim();
|
|
204
|
+
param = stripDefaultValue(param);
|
|
205
|
+
if (!param)
|
|
206
|
+
return;
|
|
207
|
+
if (IDENT_RE.test(param)) {
|
|
208
|
+
if (!JS_KEYWORDS.has(param) && !seen.has(param)) {
|
|
209
|
+
seen.add(param);
|
|
210
|
+
result.push(param);
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (param.startsWith('{') && param.endsWith('}')) {
|
|
215
|
+
const inner = param.slice(1, -1);
|
|
216
|
+
for (const rawEntry of splitTopLevel(inner)) {
|
|
217
|
+
const entry = rawEntry.trim();
|
|
218
|
+
if (!entry)
|
|
219
|
+
continue;
|
|
220
|
+
if (entry.startsWith('...')) {
|
|
221
|
+
collectFromPattern(entry.slice(3));
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const colonIdx = topLevelIndexOf(entry, ':');
|
|
225
|
+
if (colonIdx >= 0) {
|
|
226
|
+
collectFromPattern(entry.slice(colonIdx + 1));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
collectFromPattern(entry);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (param.startsWith('[') && param.endsWith(']')) {
|
|
235
|
+
const inner = param.slice(1, -1);
|
|
236
|
+
for (const rawEntry of splitTopLevel(inner)) {
|
|
237
|
+
collectFromPattern(rawEntry);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const chunk of splitTopLevel(segment)) {
|
|
242
|
+
collectFromPattern(chunk);
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
function collectLocalIdentifiers(expr) {
|
|
247
|
+
const locals = new Set();
|
|
248
|
+
const ARROW_SINGLE_PARAM_RE = /(?<![\w$.])([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/g;
|
|
249
|
+
let match;
|
|
250
|
+
while ((match = ARROW_SINGLE_PARAM_RE.exec(expr))) {
|
|
251
|
+
locals.add(match[1]);
|
|
252
|
+
}
|
|
253
|
+
const ARROW_PARAMS_RE = /\(([^)]*)\)\s*=>/g;
|
|
254
|
+
while ((match = ARROW_PARAMS_RE.exec(expr))) {
|
|
255
|
+
for (const name of extractParamNames(match[1])) {
|
|
256
|
+
locals.add(name);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const FUNCTION_PARAMS_RE = /function(?:\s+[a-zA-Z_$][a-zA-Z0-9_$]*)?\s*\(([^)]*)\)/g;
|
|
260
|
+
while ((match = FUNCTION_PARAMS_RE.exec(expr))) {
|
|
261
|
+
for (const name of extractParamNames(match[1])) {
|
|
262
|
+
locals.add(name);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const CATCH_PARAM_RE = /catch\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\)/g;
|
|
266
|
+
while ((match = CATCH_PARAM_RE.exec(expr))) {
|
|
267
|
+
locals.add(match[1]);
|
|
268
|
+
}
|
|
269
|
+
return locals;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Extract root identifiers from a template expression.
|
|
273
|
+
* Given `items.length`, returns `['items']`.
|
|
274
|
+
* Given `count`, returns `['count']`.
|
|
275
|
+
* Ignores string literals, numbers, and JS keywords.
|
|
276
|
+
*/
|
|
277
|
+
function extractRootIdentifiers(expr) {
|
|
278
|
+
// Remove plain string literal contents while preserving template
|
|
279
|
+
// expression bodies inside `${...}` so identifiers are still validated.
|
|
280
|
+
const stripStringsPreserveTemplateExpressions = (input) => {
|
|
281
|
+
let i = 0;
|
|
282
|
+
let out = '';
|
|
283
|
+
const consumeQuoted = (quote) => {
|
|
284
|
+
out += ' ';
|
|
285
|
+
i++;
|
|
286
|
+
let escaped = false;
|
|
287
|
+
while (i < input.length) {
|
|
288
|
+
const ch = input[i];
|
|
289
|
+
out += ' ';
|
|
290
|
+
i++;
|
|
291
|
+
if (escaped) {
|
|
292
|
+
escaped = false;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (ch === '\\') {
|
|
296
|
+
escaped = true;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (ch === quote)
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const consumeTemplate = () => {
|
|
304
|
+
out += ' ';
|
|
305
|
+
i++;
|
|
306
|
+
let escaped = false;
|
|
307
|
+
while (i < input.length) {
|
|
308
|
+
const ch = input[i];
|
|
309
|
+
if (escaped) {
|
|
310
|
+
out += ' ';
|
|
311
|
+
escaped = false;
|
|
312
|
+
i++;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (ch === '\\') {
|
|
316
|
+
out += ' ';
|
|
317
|
+
escaped = true;
|
|
318
|
+
i++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (ch === '`') {
|
|
322
|
+
out += ' ';
|
|
323
|
+
i++;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
if (ch === '$' && input[i + 1] === '{') {
|
|
327
|
+
out += '${';
|
|
328
|
+
i += 2;
|
|
329
|
+
let depth = 1;
|
|
330
|
+
while (i < input.length && depth > 0) {
|
|
331
|
+
const inner = input[i];
|
|
332
|
+
if (inner === '"' || inner === "'") {
|
|
333
|
+
consumeQuoted(inner);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (inner === '`') {
|
|
337
|
+
consumeTemplate();
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (inner === '{') {
|
|
341
|
+
depth++;
|
|
342
|
+
out += inner;
|
|
343
|
+
i++;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (inner === '}') {
|
|
347
|
+
depth--;
|
|
348
|
+
out += inner;
|
|
349
|
+
i++;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
out += inner;
|
|
353
|
+
i++;
|
|
354
|
+
}
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
out += ' ';
|
|
358
|
+
i++;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
while (i < input.length) {
|
|
362
|
+
const ch = input[i];
|
|
363
|
+
if (ch === '"' || ch === "'") {
|
|
364
|
+
consumeQuoted(ch);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (ch === '`') {
|
|
368
|
+
consumeTemplate();
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
out += ch;
|
|
372
|
+
i++;
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
};
|
|
376
|
+
const cleaned = stripStringsPreserveTemplateExpressions(expr);
|
|
377
|
+
const localIdentifiers = collectLocalIdentifiers(cleaned);
|
|
378
|
+
// Match identifiers NOT preceded by a dot (member access)
|
|
379
|
+
const regex = /(?<![.\w$])([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
|
380
|
+
const seen = new Set();
|
|
381
|
+
const result = [];
|
|
382
|
+
const prevNonWs = (index) => {
|
|
383
|
+
for (let i = index; i >= 0; i--) {
|
|
384
|
+
if (!/\s/.test(cleaned[i]))
|
|
385
|
+
return cleaned[i];
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
};
|
|
389
|
+
const nextNonWs = (index) => {
|
|
390
|
+
for (let i = index; i < cleaned.length; i++) {
|
|
391
|
+
if (!/\s/.test(cleaned[i]))
|
|
392
|
+
return cleaned[i];
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
};
|
|
396
|
+
let match;
|
|
397
|
+
while ((match = regex.exec(cleaned))) {
|
|
398
|
+
const name = match[1];
|
|
399
|
+
const nextToken = nextNonWs(regex.lastIndex);
|
|
400
|
+
const prevToken = prevNonWs(match.index - 1);
|
|
401
|
+
const isObjectLiteralKey = nextToken === ':' && (prevToken === '{' || prevToken === ',');
|
|
402
|
+
if (!isObjectLiteralKey && !JS_KEYWORDS.has(name) && !seen.has(name) && !localIdentifiers.has(name)) {
|
|
403
|
+
seen.add(name);
|
|
404
|
+
result.push(name);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Extract all template identifiers from HTML content.
|
|
411
|
+
*
|
|
412
|
+
* Scans for:
|
|
413
|
+
* 1. Text interpolation `{expr}` — only outside HTML tags
|
|
414
|
+
* 2. Context-binding directives `d-*="value"` — specific set
|
|
415
|
+
*/
|
|
416
|
+
function extractTemplateIdentifiers(html) {
|
|
417
|
+
const identifiers = [];
|
|
418
|
+
const lines = html.split('\n');
|
|
419
|
+
const lineOffsets = [];
|
|
420
|
+
let runningOffset = 0;
|
|
421
|
+
for (const line of lines) {
|
|
422
|
+
lineOffsets.push(runningOffset);
|
|
423
|
+
runningOffset += line.length + 1;
|
|
424
|
+
}
|
|
425
|
+
function offsetToLineCol(offset) {
|
|
426
|
+
let lineIdx = 0;
|
|
427
|
+
for (let i = 0; i < lineOffsets.length; i++) {
|
|
428
|
+
if (lineOffsets[i] <= offset) {
|
|
429
|
+
lineIdx = i;
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return { line: lineIdx + 1, col: offset - lineOffsets[lineIdx] + 1 };
|
|
436
|
+
}
|
|
437
|
+
// --- 1. Text interpolation {expr} with state machine (supports multiline) ---
|
|
438
|
+
let inTag = false;
|
|
439
|
+
let tagQuote = null;
|
|
440
|
+
let i = 0;
|
|
441
|
+
while (i < html.length) {
|
|
442
|
+
const ch = html[i];
|
|
443
|
+
if (!inTag && ch === '<') {
|
|
444
|
+
inTag = true;
|
|
445
|
+
tagQuote = null;
|
|
446
|
+
i++;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (inTag) {
|
|
450
|
+
if (tagQuote) {
|
|
451
|
+
if (ch === tagQuote) {
|
|
452
|
+
tagQuote = null;
|
|
453
|
+
}
|
|
454
|
+
i++;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (ch === '"' || ch === "'") {
|
|
458
|
+
tagQuote = ch;
|
|
459
|
+
i++;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (ch === '>') {
|
|
463
|
+
inTag = false;
|
|
464
|
+
tagQuote = null;
|
|
465
|
+
i++;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
i++;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (ch === '{') {
|
|
472
|
+
const start = i;
|
|
473
|
+
let depth = 1;
|
|
474
|
+
let j = i + 1;
|
|
475
|
+
let inString = null;
|
|
476
|
+
let escaped = false;
|
|
477
|
+
while (j < html.length && depth > 0) {
|
|
478
|
+
const ch = html[j];
|
|
479
|
+
if (inString) {
|
|
480
|
+
if (escaped) {
|
|
481
|
+
escaped = false;
|
|
482
|
+
}
|
|
483
|
+
else if (ch === '\\') {
|
|
484
|
+
escaped = true;
|
|
485
|
+
}
|
|
486
|
+
else if (ch === inString) {
|
|
487
|
+
inString = null;
|
|
488
|
+
}
|
|
489
|
+
j++;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
493
|
+
inString = ch;
|
|
494
|
+
j++;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (ch === '{')
|
|
498
|
+
depth++;
|
|
499
|
+
if (ch === '}')
|
|
500
|
+
depth--;
|
|
501
|
+
j++;
|
|
502
|
+
}
|
|
503
|
+
if (depth === 0) {
|
|
504
|
+
const expr = html.slice(start + 1, j - 1);
|
|
505
|
+
const roots = extractRootIdentifiers(expr);
|
|
506
|
+
const loc = offsetToLineCol(start);
|
|
507
|
+
for (const name of roots) {
|
|
508
|
+
identifiers.push({
|
|
509
|
+
name,
|
|
510
|
+
line: loc.line,
|
|
511
|
+
col: loc.col,
|
|
512
|
+
offset: start,
|
|
513
|
+
source: 'interpolation',
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
i = j;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
i++;
|
|
521
|
+
}
|
|
522
|
+
// --- 2. Directive scanning (supports single and double quotes) ---
|
|
523
|
+
const DIRECTIVE_RE = /\b(d-each|d-virtual-each|d-virtual-height|d-virtual-item-height|d-virtual-overscan|d-if|d-when|d-match|d-html|d-attr-[a-zA-Z][\w-]*|d-on-[a-zA-Z][\w-]*|d-form-error|d-form|d-array)\s*=\s*(['"])([\s\S]*?)\2/g;
|
|
524
|
+
DIRECTIVE_RE.lastIndex = 0;
|
|
525
|
+
let match;
|
|
526
|
+
while ((match = DIRECTIVE_RE.exec(html))) {
|
|
527
|
+
const directive = match[1];
|
|
528
|
+
const value = match[3].trim();
|
|
529
|
+
if (!value)
|
|
530
|
+
continue;
|
|
531
|
+
const roots = extractRootIdentifiers(value);
|
|
532
|
+
const loc = offsetToLineCol(match.index);
|
|
533
|
+
for (const name of roots) {
|
|
534
|
+
identifiers.push({
|
|
535
|
+
name,
|
|
536
|
+
line: loc.line,
|
|
537
|
+
col: loc.col,
|
|
538
|
+
offset: match.index,
|
|
539
|
+
source: directive,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return identifiers;
|
|
544
|
+
}
|
|
545
|
+
function extractLoopRanges(html) {
|
|
546
|
+
const ranges = [];
|
|
547
|
+
const stack = [];
|
|
548
|
+
let i = 0;
|
|
549
|
+
while (i < html.length) {
|
|
550
|
+
if (html[i] !== '<') {
|
|
551
|
+
i++;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
let j = i + 1;
|
|
555
|
+
let inString = null;
|
|
556
|
+
let escaped = false;
|
|
557
|
+
while (j < html.length) {
|
|
558
|
+
const ch = html[j];
|
|
559
|
+
if (inString) {
|
|
560
|
+
if (escaped) {
|
|
561
|
+
escaped = false;
|
|
562
|
+
}
|
|
563
|
+
else if (ch === '\\') {
|
|
564
|
+
escaped = true;
|
|
565
|
+
}
|
|
566
|
+
else if (ch === inString) {
|
|
567
|
+
inString = null;
|
|
568
|
+
}
|
|
569
|
+
j++;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (ch === '"' || ch === "'") {
|
|
573
|
+
inString = ch;
|
|
574
|
+
j++;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (ch === '>')
|
|
578
|
+
break;
|
|
579
|
+
j++;
|
|
580
|
+
}
|
|
581
|
+
if (j >= html.length)
|
|
582
|
+
break;
|
|
583
|
+
const fullTag = html.slice(i, j + 1);
|
|
584
|
+
const inner = html.slice(i + 1, j).trim();
|
|
585
|
+
const isClosingTag = inner.startsWith('/');
|
|
586
|
+
const normalized = isClosingTag ? inner.slice(1).trim() : inner;
|
|
587
|
+
const nameMatch = /^([a-zA-Z][\w:-]*)/.exec(normalized);
|
|
588
|
+
if (!nameMatch) {
|
|
589
|
+
i = j + 1;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const tagName = nameMatch[1];
|
|
593
|
+
const attrs = normalized.slice(tagName.length);
|
|
594
|
+
const isSelfClosingTag = !isClosingTag && /\/\s*$/.test(normalized);
|
|
595
|
+
if (isClosingTag) {
|
|
596
|
+
for (let stackIdx = stack.length - 1; stackIdx >= 0; stackIdx--) {
|
|
597
|
+
if (stack[stackIdx].tagName !== tagName)
|
|
598
|
+
continue;
|
|
599
|
+
const entry = stack.splice(stackIdx, 1)[0];
|
|
600
|
+
if (entry.isLoop) {
|
|
601
|
+
ranges.push({ start: entry.start, end: i + fullTag.length });
|
|
602
|
+
}
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
i = j + 1;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const isLoopTag = /\bd-(?:virtual-)?each\s*=\s*(['"])([\s\S]*?)\1/.test(attrs);
|
|
609
|
+
if (isSelfClosingTag) {
|
|
610
|
+
if (isLoopTag) {
|
|
611
|
+
ranges.push({ start: i, end: i + fullTag.length });
|
|
612
|
+
}
|
|
613
|
+
i = j + 1;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
stack.push({ tagName, isLoop: isLoopTag, start: i });
|
|
617
|
+
i = j + 1;
|
|
618
|
+
}
|
|
619
|
+
for (const entry of stack) {
|
|
620
|
+
if (entry.isLoop) {
|
|
621
|
+
ranges.push({ start: entry.start, end: html.length });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return ranges;
|
|
625
|
+
}
|
|
626
|
+
function isInsideLoopRange(offset, ranges) {
|
|
627
|
+
for (const range of ranges) {
|
|
628
|
+
if (offset >= range.start && offset < range.end)
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
// ============================================================================
|
|
634
|
+
// Levenshtein did-you-mean
|
|
635
|
+
// ============================================================================
|
|
636
|
+
function levenshtein(a, b) {
|
|
637
|
+
const m = a.length;
|
|
638
|
+
const n = b.length;
|
|
639
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
640
|
+
for (let i = 0; i <= m; i++)
|
|
641
|
+
dp[i][0] = i;
|
|
642
|
+
for (let j = 0; j <= n; j++)
|
|
643
|
+
dp[0][j] = j;
|
|
644
|
+
for (let i = 1; i <= m; i++) {
|
|
645
|
+
for (let j = 1; j <= n; j++) {
|
|
646
|
+
if (a[i - 1] === b[j - 1]) {
|
|
647
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
dp[i][j] =
|
|
651
|
+
1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return dp[m][n];
|
|
656
|
+
}
|
|
657
|
+
function findSuggestion(identifier, validNames) {
|
|
658
|
+
let best;
|
|
659
|
+
let bestDist = Infinity;
|
|
660
|
+
const maxDist = Math.max(2, Math.floor(identifier.length * 0.4));
|
|
661
|
+
for (const name of validNames) {
|
|
662
|
+
const dist = levenshtein(identifier, name);
|
|
663
|
+
if (dist < bestDist && dist <= maxDist) {
|
|
664
|
+
bestDist = dist;
|
|
665
|
+
best = name;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return best;
|
|
669
|
+
}
|
|
670
|
+
// ============================================================================
|
|
671
|
+
// Diagnostic check
|
|
672
|
+
// ============================================================================
|
|
673
|
+
const BUILTIN_IDENTIFIERS = new Set([
|
|
674
|
+
'params',
|
|
675
|
+
'query',
|
|
676
|
+
'path',
|
|
677
|
+
'fullPath',
|
|
678
|
+
]);
|
|
679
|
+
const LOOP_VARS = new Set([
|
|
680
|
+
'$index',
|
|
681
|
+
'$count',
|
|
682
|
+
'$first',
|
|
683
|
+
'$last',
|
|
684
|
+
'$odd',
|
|
685
|
+
'$even',
|
|
686
|
+
'item',
|
|
687
|
+
'key',
|
|
688
|
+
]);
|
|
689
|
+
const LOOP_FORCED_CHECK_SOURCES = new Set([
|
|
690
|
+
'd-each',
|
|
691
|
+
'd-virtual-each',
|
|
692
|
+
'd-virtual-height',
|
|
693
|
+
'd-virtual-item-height',
|
|
694
|
+
'd-virtual-overscan',
|
|
695
|
+
]);
|
|
696
|
+
function checkHtmlContent(html, filePath, validIdentifiers, diagnostics) {
|
|
697
|
+
const ids = extractTemplateIdentifiers(html);
|
|
698
|
+
const loopRanges = extractLoopRanges(html);
|
|
699
|
+
for (const id of ids) {
|
|
700
|
+
if (validIdentifiers.has(id.name))
|
|
701
|
+
continue;
|
|
702
|
+
const insideLoop = isInsideLoopRange(id.offset, loopRanges);
|
|
703
|
+
if (insideLoop) {
|
|
704
|
+
if (LOOP_VARS.has(id.name))
|
|
705
|
+
continue;
|
|
706
|
+
// Loop runtime injects item fields into local scope, so unknown
|
|
707
|
+
// identifiers may be valid inside loop bodies.
|
|
708
|
+
if (!LOOP_FORCED_CHECK_SOURCES.has(id.source))
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const suggestion = findSuggestion(id.name, [...validIdentifiers]);
|
|
712
|
+
diagnostics.push({
|
|
713
|
+
filePath,
|
|
714
|
+
line: id.line,
|
|
715
|
+
col: id.col,
|
|
716
|
+
identifier: id.name,
|
|
717
|
+
source: id.source,
|
|
718
|
+
suggestion,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// ============================================================================
|
|
723
|
+
// Route tree helpers
|
|
724
|
+
// ============================================================================
|
|
725
|
+
function collectRouteTsPaths(node, routesDir, out) {
|
|
726
|
+
for (const file of node.files) {
|
|
727
|
+
if (file.isHtml)
|
|
728
|
+
continue;
|
|
729
|
+
out.push(path.join(routesDir, file.path));
|
|
730
|
+
}
|
|
731
|
+
for (const child of node.children) {
|
|
732
|
+
collectRouteTsPaths(child, routesDir, out);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function computeFullPattern(node, parentPattern) {
|
|
736
|
+
if (!node.segment)
|
|
737
|
+
return parentPattern || '/';
|
|
738
|
+
const base = parentPattern === '/' ? '' : parentPattern;
|
|
739
|
+
return `${base}/${node.routePath}`;
|
|
740
|
+
}
|
|
741
|
+
function traverseAndCheck(node, routesDir, parentPattern, loaderKeysMap, uninferableLoaderPaths, diagnostics) {
|
|
742
|
+
const currentPattern = computeFullPattern(node, parentPattern);
|
|
743
|
+
const paramKeys = extractParamKeys(currentPattern);
|
|
744
|
+
// --- Check page.html ---
|
|
745
|
+
const pageHtml = findFile(node, 'page', true);
|
|
746
|
+
const pageTs = findFile(node, 'page', false);
|
|
747
|
+
if (pageHtml && pageHtml.htmlContent) {
|
|
748
|
+
const pageTsPath = pageTs ? path.join(routesDir, pageTs.path) : null;
|
|
749
|
+
const skipPageCheck = pageTsPath && uninferableLoaderPaths.has(pageTsPath);
|
|
750
|
+
if (!skipPageCheck) {
|
|
751
|
+
const validIds = new Set(BUILTIN_IDENTIFIERS);
|
|
752
|
+
for (const k of paramKeys)
|
|
753
|
+
validIds.add(k);
|
|
754
|
+
if (pageTs) {
|
|
755
|
+
const keys = loaderKeysMap.get(path.join(routesDir, pageTs.path));
|
|
756
|
+
if (keys)
|
|
757
|
+
for (const k of keys)
|
|
758
|
+
validIds.add(k);
|
|
759
|
+
}
|
|
760
|
+
checkHtmlContent(pageHtml.htmlContent, pageHtml.path, validIds, diagnostics);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
// --- Check layout.html ---
|
|
764
|
+
const layoutHtml = findFile(node, 'layout', true);
|
|
765
|
+
const layoutTs = findFile(node, 'layout', false);
|
|
766
|
+
if (layoutHtml && layoutHtml.htmlContent) {
|
|
767
|
+
const layoutTsPath = layoutTs ? path.join(routesDir, layoutTs.path) : null;
|
|
768
|
+
const skipLayoutCheck = layoutTsPath && uninferableLoaderPaths.has(layoutTsPath);
|
|
769
|
+
if (!skipLayoutCheck) {
|
|
770
|
+
const validIds = new Set(BUILTIN_IDENTIFIERS);
|
|
771
|
+
for (const k of paramKeys)
|
|
772
|
+
validIds.add(k);
|
|
773
|
+
if (layoutTs) {
|
|
774
|
+
const keys = loaderKeysMap.get(path.join(routesDir, layoutTs.path));
|
|
775
|
+
if (keys)
|
|
776
|
+
for (const k of keys)
|
|
777
|
+
validIds.add(k);
|
|
778
|
+
}
|
|
779
|
+
checkHtmlContent(layoutHtml.htmlContent, layoutHtml.path, validIds, diagnostics);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// --- Check error.html, loading.html, not-found.html ---
|
|
783
|
+
const stateTypes = ['error', 'pending', 'notFound'];
|
|
784
|
+
for (const type of stateTypes) {
|
|
785
|
+
const html = findFile(node, type, true);
|
|
786
|
+
if (!html || !html.htmlContent)
|
|
787
|
+
continue;
|
|
788
|
+
const validIds = new Set(BUILTIN_IDENTIFIERS);
|
|
789
|
+
for (const k of paramKeys)
|
|
790
|
+
validIds.add(k);
|
|
791
|
+
if (type === 'error')
|
|
792
|
+
validIds.add('errorMessage');
|
|
793
|
+
checkHtmlContent(html.htmlContent, html.path, validIds, diagnostics);
|
|
794
|
+
}
|
|
795
|
+
// Recurse into children
|
|
796
|
+
for (const child of node.children) {
|
|
797
|
+
traverseAndCheck(child, routesDir, currentPattern, loaderKeysMap, uninferableLoaderPaths, diagnostics);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// ============================================================================
|
|
801
|
+
// Main entry point
|
|
802
|
+
// ============================================================================
|
|
803
|
+
export async function runCheck(appDir, options = {}) {
|
|
804
|
+
const ts = await loadTypeScript();
|
|
805
|
+
const strictMode = Boolean(options.strict);
|
|
806
|
+
console.log('');
|
|
807
|
+
console.log('🔍 Dalila Check');
|
|
808
|
+
console.log('');
|
|
809
|
+
const routesDir = path.resolve(appDir);
|
|
810
|
+
if (!fs.existsSync(routesDir)) {
|
|
811
|
+
console.error(`❌ App directory not found: ${routesDir}`);
|
|
812
|
+
return 1;
|
|
813
|
+
}
|
|
814
|
+
// 1. Build route tree (reuses routes-generator internals)
|
|
815
|
+
const tree = await buildRouteTree(routesDir, '', '');
|
|
816
|
+
const projectRoot = (await findProjectRoot(routesDir)) ?? process.cwd();
|
|
817
|
+
await injectHtmlPathTemplates(tree, routesDir, projectRoot);
|
|
818
|
+
// 2. Collect route .ts files, create shared TS Program, infer loader keys by symbols
|
|
819
|
+
const routeTsPaths = [];
|
|
820
|
+
collectRouteTsPaths(tree, routesDir, routeTsPaths);
|
|
821
|
+
const loaderKeysMap = new Map();
|
|
822
|
+
const uninferableLoaderPaths = new Set();
|
|
823
|
+
const strictIssues = [];
|
|
824
|
+
if (routeTsPaths.length > 0) {
|
|
825
|
+
const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
|
|
826
|
+
let compilerOptions = {
|
|
827
|
+
target: ts.ScriptTarget.ES2020,
|
|
828
|
+
module: ts.ModuleKind.ESNext,
|
|
829
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
830
|
+
strict: true,
|
|
831
|
+
esModuleInterop: true,
|
|
832
|
+
skipLibCheck: true,
|
|
833
|
+
};
|
|
834
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
835
|
+
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
836
|
+
if (configFile.config) {
|
|
837
|
+
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, projectRoot);
|
|
838
|
+
compilerOptions = parsed.options;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const program = ts.createProgram(routeTsPaths, compilerOptions);
|
|
842
|
+
const checker = program.getTypeChecker();
|
|
843
|
+
for (const filePath of routeTsPaths) {
|
|
844
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
845
|
+
if (!sourceFile)
|
|
846
|
+
continue;
|
|
847
|
+
const loaderSymbol = getLoaderExportSymbol(ts, checker, sourceFile);
|
|
848
|
+
if (!loaderSymbol)
|
|
849
|
+
continue;
|
|
850
|
+
const keys = extractLoaderReturnKeys(ts, checker, loaderSymbol, sourceFile);
|
|
851
|
+
if (keys) {
|
|
852
|
+
loaderKeysMap.set(filePath, keys);
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
uninferableLoaderPaths.add(filePath);
|
|
856
|
+
if (strictMode) {
|
|
857
|
+
strictIssues.push(`${path.relative(process.cwd(), filePath)} exports "loader", but its return type could not be inferred`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
// 3. Traverse tree and check all HTML templates
|
|
863
|
+
const diagnostics = [];
|
|
864
|
+
traverseAndCheck(tree, routesDir, '/', loaderKeysMap, uninferableLoaderPaths, diagnostics);
|
|
865
|
+
// 4. Report results
|
|
866
|
+
if (strictIssues.length === 0 && diagnostics.length === 0) {
|
|
867
|
+
console.log('✅ No errors found');
|
|
868
|
+
console.log('');
|
|
869
|
+
return 0;
|
|
870
|
+
}
|
|
871
|
+
if (strictIssues.length > 0) {
|
|
872
|
+
console.log(' Strict mode');
|
|
873
|
+
for (const issue of strictIssues) {
|
|
874
|
+
console.log(` ❌ ${issue}`);
|
|
875
|
+
}
|
|
876
|
+
console.log('');
|
|
877
|
+
}
|
|
878
|
+
// Group by file
|
|
879
|
+
const grouped = new Map();
|
|
880
|
+
for (const d of diagnostics) {
|
|
881
|
+
const list = grouped.get(d.filePath) ?? [];
|
|
882
|
+
list.push(d);
|
|
883
|
+
grouped.set(d.filePath, list);
|
|
884
|
+
}
|
|
885
|
+
for (const [file, diags] of grouped) {
|
|
886
|
+
console.log(` ${file}`);
|
|
887
|
+
for (const d of diags) {
|
|
888
|
+
const loc = `${d.line}:${d.col}`;
|
|
889
|
+
let msg = `"${d.identifier}" is not defined in template context (${d.source})`;
|
|
890
|
+
if (d.suggestion) {
|
|
891
|
+
msg += `. Did you mean "${d.suggestion}"?`;
|
|
892
|
+
}
|
|
893
|
+
console.log(` ${loc.padEnd(8)} ❌ ${msg}`);
|
|
894
|
+
}
|
|
895
|
+
console.log('');
|
|
896
|
+
}
|
|
897
|
+
const totalErrors = diagnostics.length + strictIssues.length;
|
|
898
|
+
const fileCount = grouped.size + strictIssues.length;
|
|
899
|
+
console.log(`❌ Found ${totalErrors} error${totalErrors === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`);
|
|
900
|
+
console.log('');
|
|
901
|
+
return 1;
|
|
902
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -16,15 +16,18 @@ Usage:
|
|
|
16
16
|
dalila routes init Initialize app and generate routes outputs
|
|
17
17
|
dalila routes watch [options] Watch routes and regenerate outputs on changes
|
|
18
18
|
dalila routes --help Show routes command help
|
|
19
|
+
dalila check [path] [--strict] Static analysis of HTML templates against loaders
|
|
19
20
|
dalila help Show this help message
|
|
20
21
|
|
|
21
22
|
Options:
|
|
22
23
|
--output <path> Output file (default: ./routes.generated.ts)
|
|
23
24
|
|
|
24
25
|
Examples:
|
|
25
|
-
dalila routes generate
|
|
26
|
-
dalila routes generate --output src/routes.generated.ts
|
|
27
|
-
dalila routes init
|
|
26
|
+
npx dalila routes generate
|
|
27
|
+
npx dalila routes generate --output src/routes.generated.ts
|
|
28
|
+
npx dalila routes init
|
|
29
|
+
npx dalila check
|
|
30
|
+
npx dalila check src/app --strict
|
|
28
31
|
`);
|
|
29
32
|
}
|
|
30
33
|
function showRoutesHelp() {
|
|
@@ -41,10 +44,33 @@ Options:
|
|
|
41
44
|
--output <path> Output file (default: ./routes.generated.ts)
|
|
42
45
|
|
|
43
46
|
Examples:
|
|
44
|
-
dalila routes generate
|
|
45
|
-
dalila routes generate --output src/routes.generated.ts
|
|
46
|
-
dalila routes watch
|
|
47
|
-
dalila routes init
|
|
47
|
+
npx dalila routes generate
|
|
48
|
+
npx dalila routes generate --output src/routes.generated.ts
|
|
49
|
+
npx dalila routes watch
|
|
50
|
+
npx dalila routes init
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
function showCheckHelp() {
|
|
54
|
+
console.log(`
|
|
55
|
+
Dalila CLI - Check
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
dalila check [path] [options] Static analysis of HTML templates
|
|
59
|
+
|
|
60
|
+
Validates that identifiers used in HTML templates ({expr}, d-* directives)
|
|
61
|
+
match the return type of the corresponding loader() in TypeScript.
|
|
62
|
+
|
|
63
|
+
Arguments:
|
|
64
|
+
[path] App directory to check (default: src/app)
|
|
65
|
+
|
|
66
|
+
Options:
|
|
67
|
+
--strict Fail when exported loader return keys cannot be inferred
|
|
68
|
+
--help, -h Show this help message
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
npx dalila check
|
|
72
|
+
npx dalila check src/app
|
|
73
|
+
npx dalila check --strict
|
|
48
74
|
`);
|
|
49
75
|
}
|
|
50
76
|
function hasHelpFlag(list) {
|
|
@@ -331,6 +357,22 @@ async function main() {
|
|
|
331
357
|
process.exit(1);
|
|
332
358
|
}
|
|
333
359
|
}
|
|
360
|
+
else if (command === 'check') {
|
|
361
|
+
const checkArgs = args.slice(1);
|
|
362
|
+
if (hasHelpFlag(checkArgs)) {
|
|
363
|
+
showCheckHelp();
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const strict = checkArgs.includes('--strict');
|
|
367
|
+
const positional = checkArgs.filter(a => !a.startsWith('--'));
|
|
368
|
+
const appDir = positional[0]
|
|
369
|
+
? path.resolve(positional[0])
|
|
370
|
+
: resolveDefaultAppDir(process.cwd());
|
|
371
|
+
const { runCheck } = await import('./check.js');
|
|
372
|
+
const exitCode = await runCheck(appDir, { strict });
|
|
373
|
+
process.exit(exitCode);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
334
376
|
else if (command === '--help' || command === '-h') {
|
|
335
377
|
showHelp();
|
|
336
378
|
}
|
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
export type RouteFileType = 'middleware' | 'layout' | 'page' | 'error' | 'pending' | 'notFound';
|
|
2
|
+
export interface RouteFile {
|
|
3
|
+
path: string;
|
|
4
|
+
type: RouteFileType;
|
|
5
|
+
importName: string;
|
|
6
|
+
isHtml: boolean;
|
|
7
|
+
htmlContent?: string;
|
|
8
|
+
htmlPath?: string;
|
|
9
|
+
sourceContent?: string;
|
|
10
|
+
namedExports?: string[];
|
|
11
|
+
tags?: string[];
|
|
12
|
+
lazy?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface RouteNode {
|
|
15
|
+
fsPath: string;
|
|
16
|
+
segment: string;
|
|
17
|
+
routePath: string;
|
|
18
|
+
files: RouteFile[];
|
|
19
|
+
children: RouteNode[];
|
|
20
|
+
}
|
|
21
|
+
export declare function extractParamKeys(routePattern: string): string[];
|
|
22
|
+
export declare function injectHtmlPathTemplates(node: RouteNode, routesDir: string, projectRoot: string): Promise<void>;
|
|
23
|
+
export declare function findProjectRoot(startDir: string): Promise<string | null>;
|
|
24
|
+
export declare function findFile(node: RouteNode, type: RouteFileType, isHtml?: boolean): RouteFile | undefined;
|
|
25
|
+
export declare function buildRouteTree(routesDir: string, currentPath?: string, currentSegment?: string): Promise<RouteNode>;
|
|
1
26
|
export declare function collectHtmlPathDependencyDirs(routesDir: string): string[];
|
|
2
27
|
/**
|
|
3
28
|
* Generate route files from the app directory.
|
|
@@ -128,7 +128,7 @@ function parseRouteParamSegment(segment) {
|
|
|
128
128
|
}
|
|
129
129
|
return { key: raw, isCatchAll: false, isOptionalCatchAll: false };
|
|
130
130
|
}
|
|
131
|
-
function extractParamKeys(routePattern) {
|
|
131
|
+
export function extractParamKeys(routePattern) {
|
|
132
132
|
const keys = [];
|
|
133
133
|
const segments = normalizeRoutePath(routePattern).split('/').filter(Boolean);
|
|
134
134
|
for (const segment of segments) {
|
|
@@ -257,7 +257,7 @@ function resolveHtmlPath(htmlPath, routesDir, filePath, projectRoot) {
|
|
|
257
257
|
}
|
|
258
258
|
return path.resolve(routeFileDir, htmlPath);
|
|
259
259
|
}
|
|
260
|
-
async function injectHtmlPathTemplates(node, routesDir, projectRoot) {
|
|
260
|
+
export async function injectHtmlPathTemplates(node, routesDir, projectRoot) {
|
|
261
261
|
const syntheticHtmlFiles = [];
|
|
262
262
|
for (const file of node.files) {
|
|
263
263
|
if (file.isHtml || !file.htmlPath)
|
|
@@ -299,7 +299,7 @@ const DEFAULT_ROUTE_TAG_POLICY = {
|
|
|
299
299
|
],
|
|
300
300
|
priority: ['auth', 'public']
|
|
301
301
|
};
|
|
302
|
-
async function findProjectRoot(startDir) {
|
|
302
|
+
export async function findProjectRoot(startDir) {
|
|
303
303
|
let current = path.resolve(startDir);
|
|
304
304
|
while (true) {
|
|
305
305
|
if (await pathExists(path.join(current, 'package.json'))) {
|
|
@@ -409,7 +409,7 @@ function validateManifestTags(entries, policy) {
|
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
|
-
function findFile(node, type, isHtml) {
|
|
412
|
+
export function findFile(node, type, isHtml) {
|
|
413
413
|
return node.files.find((file) => {
|
|
414
414
|
if (file.type !== type)
|
|
415
415
|
return false;
|
|
@@ -591,7 +591,7 @@ function buildRouteTreeSync(routesDir, currentPath = '', currentSegment = '') {
|
|
|
591
591
|
}
|
|
592
592
|
return node;
|
|
593
593
|
}
|
|
594
|
-
async function buildRouteTree(routesDir, currentPath = '', currentSegment = '') {
|
|
594
|
+
export async function buildRouteTree(routesDir, currentPath = '', currentSegment = '') {
|
|
595
595
|
const node = {
|
|
596
596
|
fsPath: currentPath.replace(/\\/g, '/'),
|
|
597
597
|
segment: currentSegment,
|
|
@@ -40,19 +40,19 @@ export type RouteMiddlewareResolver = RouteMiddleware[] | ((ctx: RouteCtx) => Ro
|
|
|
40
40
|
* data loading, state views, children, guards, middleware, redirects,
|
|
41
41
|
* and params/query validation.
|
|
42
42
|
*/
|
|
43
|
-
export interface RouteTable {
|
|
43
|
+
export interface RouteTable<T = any> {
|
|
44
44
|
path: string;
|
|
45
45
|
id?: string;
|
|
46
46
|
score?: number;
|
|
47
47
|
paramKeys?: string[];
|
|
48
48
|
tags?: string[];
|
|
49
|
-
view?: (ctx: RouteCtx, data:
|
|
50
|
-
layout?: (ctx: RouteCtx, child: Node | DocumentFragment | Node[], data:
|
|
51
|
-
loader?: (ctx: RouteCtx) => Promise<
|
|
52
|
-
preload?: (ctx: RouteCtx) => Promise<
|
|
49
|
+
view?: (ctx: RouteCtx, data: T) => Node | DocumentFragment | Node[];
|
|
50
|
+
layout?: (ctx: RouteCtx, child: Node | DocumentFragment | Node[], data: T) => Node | DocumentFragment | Node[];
|
|
51
|
+
loader?: (ctx: RouteCtx) => Promise<T>;
|
|
52
|
+
preload?: (ctx: RouteCtx) => Promise<T>;
|
|
53
53
|
onMount?: (root: HTMLElement) => void;
|
|
54
54
|
pending?: (ctx: RouteCtx) => Node | DocumentFragment | Node[];
|
|
55
|
-
error?: (ctx: RouteCtx, error: unknown, data?:
|
|
55
|
+
error?: (ctx: RouteCtx, error: unknown, data?: T) => Node | DocumentFragment | Node[];
|
|
56
56
|
notFound?: (ctx: RouteCtx) => Node | DocumentFragment | Node[];
|
|
57
57
|
children?: RouteTable[];
|
|
58
58
|
middleware?: RouteMiddlewareResolver;
|
|
@@ -60,6 +60,23 @@ export interface RouteTable {
|
|
|
60
60
|
redirect?: string | ((ctx: RouteCtx) => RouteRedirectResult);
|
|
61
61
|
validation?: RouteValidationResolver;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Helper to define a single route with full type inference between
|
|
65
|
+
* `loader` return type and the `view` / `layout` / `error` `data` parameter.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* const route = defineRoute({
|
|
70
|
+
* path: '/users',
|
|
71
|
+
* loader: async () => ({ users: await fetchUsers() }),
|
|
72
|
+
* view: (ctx, data) => {
|
|
73
|
+
* // data is inferred as { users: User[] }
|
|
74
|
+
* return fromHtml(tpl, { data });
|
|
75
|
+
* },
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function defineRoute<T = any>(route: RouteTable<T>): RouteTable<T>;
|
|
63
80
|
/** Immutable snapshot of the current navigation state. */
|
|
64
81
|
export interface RouteState {
|
|
65
82
|
path: string;
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper to define a single route with full type inference between
|
|
3
|
+
* `loader` return type and the `view` / `layout` / `error` `data` parameter.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const route = defineRoute({
|
|
8
|
+
* path: '/users',
|
|
9
|
+
* loader: async () => ({ users: await fetchUsers() }),
|
|
10
|
+
* view: (ctx, data) => {
|
|
11
|
+
* // data is inferred as { users: User[] }
|
|
12
|
+
* return fromHtml(tpl, { data });
|
|
13
|
+
* },
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function defineRoute(route) {
|
|
18
|
+
return route;
|
|
19
|
+
}
|
|
1
20
|
/** Normalize a path: ensure leading slash, collapse duplicates, strip trailing slash. */
|
|
2
21
|
export function normalizePath(path) {
|
|
3
22
|
if (!path)
|
package/dist/runtime/bind.d.ts
CHANGED
|
@@ -41,7 +41,19 @@ export interface BindOptions {
|
|
|
41
41
|
export interface BindContext {
|
|
42
42
|
[key: string]: unknown;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Convenience alias: any object whose values are `unknown`.
|
|
46
|
+
* Use the generic parameter on `bind<T>()` / `autoBind<T>()` / `fromHtml<T>()`
|
|
47
|
+
* to preserve the concrete type at call sites while still satisfying internal
|
|
48
|
+
* look-ups that index by string key.
|
|
49
|
+
*/
|
|
50
|
+
export type BindData<T extends Record<string, unknown> = Record<string, unknown>> = T;
|
|
44
51
|
export type DisposeFunction = () => void;
|
|
52
|
+
export interface BindHandle {
|
|
53
|
+
(): void;
|
|
54
|
+
getRef(name: string): Element | null;
|
|
55
|
+
getRefs(): Readonly<Record<string, Element>>;
|
|
56
|
+
}
|
|
45
57
|
/**
|
|
46
58
|
* Bind a DOM tree to a reactive context.
|
|
47
59
|
*
|
|
@@ -65,7 +77,7 @@ export type DisposeFunction = () => void;
|
|
|
65
77
|
* dispose();
|
|
66
78
|
* ```
|
|
67
79
|
*/
|
|
68
|
-
export declare function bind(root: Element, ctx:
|
|
80
|
+
export declare function bind<T extends Record<string, unknown> = BindContext>(root: Element, ctx: T, options?: BindOptions): BindHandle;
|
|
69
81
|
/**
|
|
70
82
|
* Automatically bind when DOM is ready.
|
|
71
83
|
* Useful for simple pages without a build step.
|
|
@@ -78,4 +90,4 @@ export declare function bind(root: Element, ctx: BindContext, options?: BindOpti
|
|
|
78
90
|
* </script>
|
|
79
91
|
* ```
|
|
80
92
|
*/
|
|
81
|
-
export declare function autoBind(selector: string, ctx:
|
|
93
|
+
export declare function autoBind<T extends Record<string, unknown> = BindContext>(selector: string, ctx: T, options?: BindOptions): Promise<BindHandle>;
|
package/dist/runtime/bind.js
CHANGED
|
@@ -2445,6 +2445,24 @@ function bindArrayOperations(container, fieldArray, cleanups) {
|
|
|
2445
2445
|
}
|
|
2446
2446
|
}
|
|
2447
2447
|
// ============================================================================
|
|
2448
|
+
// d-ref — declarative element references
|
|
2449
|
+
// ============================================================================
|
|
2450
|
+
function bindRef(root, refs) {
|
|
2451
|
+
const elements = qsaIncludingRoot(root, '[d-ref]');
|
|
2452
|
+
for (const el of elements) {
|
|
2453
|
+
const name = el.getAttribute('d-ref');
|
|
2454
|
+
if (!name || !name.trim()) {
|
|
2455
|
+
warn('d-ref: empty ref name ignored');
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
const trimmed = name.trim();
|
|
2459
|
+
if (refs.has(trimmed)) {
|
|
2460
|
+
warn(`d-ref: duplicate ref name "${trimmed}" in the same scope`);
|
|
2461
|
+
}
|
|
2462
|
+
refs.set(trimmed, el);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
// ============================================================================
|
|
2448
2466
|
// Main bind() Function
|
|
2449
2467
|
// ============================================================================
|
|
2450
2468
|
/**
|
|
@@ -2484,6 +2502,7 @@ export function bind(root, ctx, options = {}) {
|
|
|
2484
2502
|
// Create a scope for this template binding
|
|
2485
2503
|
const templateScope = createScope();
|
|
2486
2504
|
const cleanups = [];
|
|
2505
|
+
const refs = new Map();
|
|
2487
2506
|
linkScopeToDom(templateScope, root, describeBindRoot(root));
|
|
2488
2507
|
// Run all bindings within the template scope
|
|
2489
2508
|
withScope(templateScope, () => {
|
|
@@ -2495,24 +2514,26 @@ export function bind(root, ctx, options = {}) {
|
|
|
2495
2514
|
bindVirtualEach(root, ctx, cleanups);
|
|
2496
2515
|
// 4. d-each — must run early: removes templates before TreeWalker visits them
|
|
2497
2516
|
bindEach(root, ctx, cleanups);
|
|
2498
|
-
// 5.
|
|
2517
|
+
// 5. d-ref — collect element references (after d-each removes templates)
|
|
2518
|
+
bindRef(root, refs);
|
|
2519
|
+
// 6. Text interpolation (template plan cache + lazy parser fallback)
|
|
2499
2520
|
bindTextInterpolation(root, ctx, rawTextSelectors, templatePlanCacheConfig, benchSession);
|
|
2500
|
-
//
|
|
2521
|
+
// 7. d-attr bindings
|
|
2501
2522
|
bindAttrs(root, ctx, cleanups);
|
|
2502
|
-
//
|
|
2523
|
+
// 8. d-html bindings
|
|
2503
2524
|
bindHtml(root, ctx, cleanups);
|
|
2504
|
-
//
|
|
2525
|
+
// 9. Form fields — register fields with form instances
|
|
2505
2526
|
bindField(root, ctx, cleanups);
|
|
2506
|
-
//
|
|
2527
|
+
// 10. Event bindings
|
|
2507
2528
|
bindEvents(root, ctx, events, cleanups);
|
|
2508
|
-
//
|
|
2529
|
+
// 11. d-when directive
|
|
2509
2530
|
bindWhen(root, ctx, cleanups);
|
|
2510
|
-
//
|
|
2531
|
+
// 12. d-match directive
|
|
2511
2532
|
bindMatch(root, ctx, cleanups);
|
|
2512
|
-
//
|
|
2533
|
+
// 13. Form error displays — BEFORE d-if to bind errors in conditionally rendered sections
|
|
2513
2534
|
bindError(root, ctx, cleanups);
|
|
2514
2535
|
bindFormError(root, ctx, cleanups);
|
|
2515
|
-
//
|
|
2536
|
+
// 14. d-if — must run last: elements are fully bound before conditional removal
|
|
2516
2537
|
bindIf(root, ctx, cleanups);
|
|
2517
2538
|
});
|
|
2518
2539
|
// Bindings complete: remove loading state and mark as ready.
|
|
@@ -2524,8 +2545,8 @@ export function bind(root, ctx, options = {}) {
|
|
|
2524
2545
|
});
|
|
2525
2546
|
}
|
|
2526
2547
|
flushBindBenchSession(benchSession);
|
|
2527
|
-
// Return dispose
|
|
2528
|
-
|
|
2548
|
+
// Return BindHandle (callable dispose + ref accessors)
|
|
2549
|
+
const dispose = () => {
|
|
2529
2550
|
// Run manual cleanups (event listeners)
|
|
2530
2551
|
for (const cleanup of cleanups) {
|
|
2531
2552
|
if (typeof cleanup === 'function') {
|
|
@@ -2540,6 +2561,7 @@ export function bind(root, ctx, options = {}) {
|
|
|
2540
2561
|
}
|
|
2541
2562
|
}
|
|
2542
2563
|
cleanups.length = 0;
|
|
2564
|
+
refs.clear();
|
|
2543
2565
|
// Dispose template scope (stops all effects)
|
|
2544
2566
|
try {
|
|
2545
2567
|
templateScope.dispose();
|
|
@@ -2550,6 +2572,15 @@ export function bind(root, ctx, options = {}) {
|
|
|
2550
2572
|
}
|
|
2551
2573
|
}
|
|
2552
2574
|
};
|
|
2575
|
+
const handle = Object.assign(dispose, {
|
|
2576
|
+
getRef(name) {
|
|
2577
|
+
return refs.get(name) ?? null;
|
|
2578
|
+
},
|
|
2579
|
+
getRefs() {
|
|
2580
|
+
return Object.freeze(Object.fromEntries(refs));
|
|
2581
|
+
},
|
|
2582
|
+
});
|
|
2583
|
+
return handle;
|
|
2553
2584
|
}
|
|
2554
2585
|
// ============================================================================
|
|
2555
2586
|
// Convenience: Auto-bind on DOMContentLoaded
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* @module dalila/runtime
|
|
10
10
|
*/
|
|
11
11
|
import type { Scope } from '../core/scope.js';
|
|
12
|
-
export interface FromHtmlOptions {
|
|
12
|
+
export interface FromHtmlOptions<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
13
13
|
/** Bind context — keys map to {placeholder} tokens in the HTML */
|
|
14
|
-
data?:
|
|
14
|
+
data?: T;
|
|
15
15
|
/** Child nodes to inject into [data-slot="children"] */
|
|
16
16
|
children?: Node | DocumentFragment | Node[];
|
|
17
17
|
/** Route scope — registers bind cleanup automatically */
|
|
@@ -32,4 +32,5 @@ export interface FromHtmlOptions {
|
|
|
32
32
|
* const el = fromHtml('<div><div data-slot="children"></div></div>', { children });
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
|
+
export declare function fromHtml<T extends Record<string, unknown>>(html: string, options: FromHtmlOptions<T>): HTMLElement;
|
|
35
36
|
export declare function fromHtml(html: string, options?: FromHtmlOptions): HTMLElement;
|
package/dist/runtime/fromHtml.js
CHANGED
|
@@ -9,21 +9,6 @@
|
|
|
9
9
|
* @module dalila/runtime
|
|
10
10
|
*/
|
|
11
11
|
import { bind } from './bind.js';
|
|
12
|
-
/**
|
|
13
|
-
* Parse an HTML string into a bound DOM element.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* // Static HTML
|
|
18
|
-
* const el = fromHtml('<div><h1>Hello</h1></div>');
|
|
19
|
-
*
|
|
20
|
-
* // With data binding
|
|
21
|
-
* const el = fromHtml('<div>{name}</div>', { data: { name: 'Dalila' } });
|
|
22
|
-
*
|
|
23
|
-
* // Layout with children slot
|
|
24
|
-
* const el = fromHtml('<div><div data-slot="children"></div></div>', { children });
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
12
|
export function fromHtml(html, options = {}) {
|
|
28
13
|
const { data, children, scope } = options;
|
|
29
14
|
const template = document.createElement('template');
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* @module dalila/runtime
|
|
8
8
|
*/
|
|
9
9
|
export { bind, autoBind } from './bind.js';
|
|
10
|
-
export type { BindOptions, BindContext, DisposeFunction } from './bind.js';
|
|
10
|
+
export type { BindOptions, BindContext, BindData, DisposeFunction, BindHandle } from './bind.js';
|
|
11
11
|
export { fromHtml } from './fromHtml.js';
|
|
12
12
|
export type { FromHtmlOptions } from './fromHtml.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dalila",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3",
|
|
4
4
|
"description": "DOM-first reactive framework based on signals",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"build": "tsc && node scripts/ensure-cli-executable.cjs",
|
|
101
101
|
"dev": "tsc --watch",
|
|
102
102
|
"serve": "node scripts/dev-server.cjs",
|
|
103
|
-
"test": "npm run build && node --test",
|
|
103
|
+
"test": "npm run build && node --test --test-concurrency=1 test/*.test.js",
|
|
104
104
|
"test:e2e": "npm run build && playwright test",
|
|
105
105
|
"test:watch": "jest --watch",
|
|
106
106
|
"clean": "rm -rf dist",
|