dalila 1.9.2 → 1.9.4
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 +35 -23
- package/dist/cli/check.d.ts +3 -0
- package/dist/cli/check.js +902 -0
- package/dist/cli/index.js +53 -11
- package/dist/cli/routes-generator.d.ts +25 -0
- package/dist/cli/routes-generator.js +28 -7
- package/dist/router/route-tables.d.ts +28 -7
- package/dist/router/route-tables.js +19 -0
- package/dist/router/router.js +87 -3
- package/dist/routes.generated.d.ts +2 -0
- package/dist/routes.generated.js +76 -0
- package/dist/routes.generated.manifest.d.ts +4 -0
- package/dist/routes.generated.manifest.js +32 -0
- package/dist/routes.generated.types.d.ts +11 -0
- package/dist/routes.generated.types.js +37 -0
- package/dist/runtime/bind.d.ts +47 -2
- package/dist/runtime/bind.js +702 -26
- package/dist/runtime/component.d.ts +74 -0
- package/dist/runtime/component.js +40 -0
- package/dist/runtime/fromHtml.d.ts +3 -2
- package/dist/runtime/fromHtml.js +0 -15
- package/dist/runtime/index.d.ts +4 -2
- package/dist/runtime/index.js +2 -1
- package/package.json +2 -2
- package/scripts/dev-server.cjs +47 -7
package/dist/cli/index.js
CHANGED
|
@@ -9,27 +9,30 @@ const routeArgs = args.slice(2);
|
|
|
9
9
|
const WATCH_DEBOUNCE_MS = 120;
|
|
10
10
|
function showHelp() {
|
|
11
11
|
console.log(`
|
|
12
|
-
Dalila CLI
|
|
12
|
+
🐰 ✂️ Dalila CLI
|
|
13
13
|
|
|
14
14
|
Usage:
|
|
15
15
|
dalila routes generate [options] Generate routes + manifest from app file structure
|
|
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() {
|
|
31
34
|
console.log(`
|
|
32
|
-
Dalila CLI - Routes
|
|
35
|
+
🐰 ✂️ Dalila CLI - Routes
|
|
33
36
|
|
|
34
37
|
Usage:
|
|
35
38
|
dalila routes generate [options] Generate routes + manifest from app file structure
|
|
@@ -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) {
|
|
@@ -156,7 +182,7 @@ function resolveGenerateConfig(cliArgs, cwd = process.cwd()) {
|
|
|
156
182
|
async function generateRoutes(cliArgs) {
|
|
157
183
|
const { appDir, outputPath } = resolveGenerateConfig(cliArgs);
|
|
158
184
|
console.log('');
|
|
159
|
-
console.log('
|
|
185
|
+
console.log('🐰 ✂️ Dalila Routes Generator');
|
|
160
186
|
console.log('');
|
|
161
187
|
try {
|
|
162
188
|
await generateRoutesFile(appDir, outputPath);
|
|
@@ -217,7 +243,7 @@ function watchRoutes(cliArgs) {
|
|
|
217
243
|
process.exit(1);
|
|
218
244
|
}
|
|
219
245
|
console.log('');
|
|
220
|
-
console.log('
|
|
246
|
+
console.log('🐰 ✂️ Dalila Routes Watch');
|
|
221
247
|
console.log(` app: ${appDir}`);
|
|
222
248
|
console.log(` output: ${outputPath}`);
|
|
223
249
|
console.log('');
|
|
@@ -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;
|
|
@@ -467,16 +467,31 @@ function pageProps(pageHtml, pageTs) {
|
|
|
467
467
|
if (hasNamedExport(pageTs, 'onMount')) {
|
|
468
468
|
if (pageTs.lazy) {
|
|
469
469
|
const lazyLoader = `${pageTs.importName}_lazy`;
|
|
470
|
-
props.push(`onMount: (root: HTMLElement) => ${lazyLoader}().then(mod => {
|
|
470
|
+
props.push(`onMount: (root: HTMLElement, data: unknown, ctx: unknown) => ${lazyLoader}().then(mod => {
|
|
471
471
|
if (typeof (mod as any).onMount === 'function') {
|
|
472
|
-
(mod as any).onMount(root);
|
|
472
|
+
return (mod as any).onMount(root, data, ctx);
|
|
473
473
|
}
|
|
474
|
+
return undefined;
|
|
474
475
|
})`);
|
|
475
476
|
}
|
|
476
477
|
else {
|
|
477
478
|
props.push(`onMount: ${moduleExport(pageTs, 'onMount')}`);
|
|
478
479
|
}
|
|
479
480
|
}
|
|
481
|
+
if (hasNamedExport(pageTs, 'onUnmount')) {
|
|
482
|
+
if (pageTs.lazy) {
|
|
483
|
+
const lazyLoader = `${pageTs.importName}_lazy`;
|
|
484
|
+
props.push(`onUnmount: (root: HTMLElement, data: unknown, ctx: unknown) => ${lazyLoader}().then(mod => {
|
|
485
|
+
if (typeof (mod as any).onUnmount === 'function') {
|
|
486
|
+
return (mod as any).onUnmount(root, data, ctx);
|
|
487
|
+
}
|
|
488
|
+
return undefined;
|
|
489
|
+
})`);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
props.push(`onUnmount: ${moduleExport(pageTs, 'onUnmount')}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
480
495
|
}
|
|
481
496
|
else {
|
|
482
497
|
props.push(`view: (ctx) => fromHtml(${viewConst}, { data: ${routeDataExpr()}, scope: ctx.scope })`);
|
|
@@ -501,6 +516,12 @@ function pageProps(pageHtml, pageTs) {
|
|
|
501
516
|
if (hasNamedExport(pageTs, 'validation')) {
|
|
502
517
|
props.push(`validation: ${moduleExport(pageTs, 'validation', { allowValue: true })}`);
|
|
503
518
|
}
|
|
519
|
+
if (hasNamedExport(pageTs, 'onMount')) {
|
|
520
|
+
props.push(`onMount: ${moduleExport(pageTs, 'onMount')}`);
|
|
521
|
+
}
|
|
522
|
+
if (hasNamedExport(pageTs, 'onUnmount')) {
|
|
523
|
+
props.push(`onUnmount: ${moduleExport(pageTs, 'onUnmount')}`);
|
|
524
|
+
}
|
|
504
525
|
return props;
|
|
505
526
|
}
|
|
506
527
|
function layoutProps(indent, layoutHtml, layoutTs) {
|
|
@@ -591,7 +612,7 @@ function buildRouteTreeSync(routesDir, currentPath = '', currentSegment = '') {
|
|
|
591
612
|
}
|
|
592
613
|
return node;
|
|
593
614
|
}
|
|
594
|
-
async function buildRouteTree(routesDir, currentPath = '', currentSegment = '') {
|
|
615
|
+
export async function buildRouteTree(routesDir, currentPath = '', currentSegment = '') {
|
|
595
616
|
const node = {
|
|
596
617
|
fsPath: currentPath.replace(/\\/g, '/'),
|
|
597
618
|
segment: currentSegment,
|
|
@@ -33,6 +33,9 @@ export type RouteRedirectResult = string | null | undefined | Promise<string | n
|
|
|
33
33
|
export type RouteMiddlewareResult = RouteGuardResult;
|
|
34
34
|
export type RouteMiddleware = (ctx: RouteCtx) => RouteMiddlewareResult;
|
|
35
35
|
export type RouteMiddlewareResolver = RouteMiddleware[] | ((ctx: RouteCtx) => RouteMiddleware[] | null | undefined | Promise<RouteMiddleware[] | null | undefined>);
|
|
36
|
+
export type RouteMountCleanup = () => void;
|
|
37
|
+
export type RouteMountResult = void | RouteMountCleanup | Promise<void | RouteMountCleanup>;
|
|
38
|
+
export type RouteUnmountResult = void | Promise<void>;
|
|
36
39
|
/**
|
|
37
40
|
* Route definition.
|
|
38
41
|
*
|
|
@@ -40,19 +43,20 @@ export type RouteMiddlewareResolver = RouteMiddleware[] | ((ctx: RouteCtx) => Ro
|
|
|
40
43
|
* data loading, state views, children, guards, middleware, redirects,
|
|
41
44
|
* and params/query validation.
|
|
42
45
|
*/
|
|
43
|
-
export interface RouteTable {
|
|
46
|
+
export interface RouteTable<T = any> {
|
|
44
47
|
path: string;
|
|
45
48
|
id?: string;
|
|
46
49
|
score?: number;
|
|
47
50
|
paramKeys?: string[];
|
|
48
51
|
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<
|
|
53
|
-
onMount?: (root: HTMLElement) =>
|
|
52
|
+
view?: (ctx: RouteCtx, data: T) => Node | DocumentFragment | Node[];
|
|
53
|
+
layout?: (ctx: RouteCtx, child: Node | DocumentFragment | Node[], data: T) => Node | DocumentFragment | Node[];
|
|
54
|
+
loader?: (ctx: RouteCtx) => Promise<T>;
|
|
55
|
+
preload?: (ctx: RouteCtx) => Promise<T>;
|
|
56
|
+
onMount?: (root: HTMLElement, data: T, ctx: RouteCtx) => RouteMountResult;
|
|
57
|
+
onUnmount?: (root: HTMLElement, data: T, ctx: RouteCtx) => RouteUnmountResult;
|
|
54
58
|
pending?: (ctx: RouteCtx) => Node | DocumentFragment | Node[];
|
|
55
|
-
error?: (ctx: RouteCtx, error: unknown, data?:
|
|
59
|
+
error?: (ctx: RouteCtx, error: unknown, data?: T) => Node | DocumentFragment | Node[];
|
|
56
60
|
notFound?: (ctx: RouteCtx) => Node | DocumentFragment | Node[];
|
|
57
61
|
children?: RouteTable[];
|
|
58
62
|
middleware?: RouteMiddlewareResolver;
|
|
@@ -60,6 +64,23 @@ export interface RouteTable {
|
|
|
60
64
|
redirect?: string | ((ctx: RouteCtx) => RouteRedirectResult);
|
|
61
65
|
validation?: RouteValidationResolver;
|
|
62
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Helper to define a single route with full type inference between
|
|
69
|
+
* `loader` return type and the `view` / `layout` / `error` `data` parameter.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const route = defineRoute({
|
|
74
|
+
* path: '/users',
|
|
75
|
+
* loader: async () => ({ users: await fetchUsers() }),
|
|
76
|
+
* view: (ctx, data) => {
|
|
77
|
+
* // data is inferred as { users: User[] }
|
|
78
|
+
* return fromHtml(tpl, { data });
|
|
79
|
+
* },
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function defineRoute<T = any>(route: RouteTable<T>): RouteTable<T>;
|
|
63
84
|
/** Immutable snapshot of the current navigation state. */
|
|
64
85
|
export interface RouteState {
|
|
65
86
|
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/router/router.js
CHANGED
|
@@ -109,6 +109,11 @@ export function createRouter(config) {
|
|
|
109
109
|
let currentLoaderController = null;
|
|
110
110
|
let started = false;
|
|
111
111
|
let navigationToken = 0;
|
|
112
|
+
let activeLeafRoute = null;
|
|
113
|
+
let activeRouteMountCleanup = null;
|
|
114
|
+
let activeRouteUnmountHook = null;
|
|
115
|
+
let activeLeafData = undefined;
|
|
116
|
+
let activeLeafCtx = null;
|
|
112
117
|
const transitionCoalescing = new Map();
|
|
113
118
|
const scrollPositions = new LRUCache(config.scrollPositionsCacheSize ?? 100);
|
|
114
119
|
const preloadTagsByKey = new Map();
|
|
@@ -582,10 +587,43 @@ export function createRouter(config) {
|
|
|
582
587
|
replace: true
|
|
583
588
|
};
|
|
584
589
|
}
|
|
590
|
+
function runRouteUnmountLifecycle() {
|
|
591
|
+
const cleanup = activeRouteMountCleanup;
|
|
592
|
+
const onUnmount = activeRouteUnmountHook;
|
|
593
|
+
const data = activeLeafData;
|
|
594
|
+
const ctx = activeLeafCtx;
|
|
595
|
+
activeRouteMountCleanup = null;
|
|
596
|
+
activeRouteUnmountHook = null;
|
|
597
|
+
activeLeafRoute = null;
|
|
598
|
+
activeLeafData = undefined;
|
|
599
|
+
activeLeafCtx = null;
|
|
600
|
+
if (cleanup) {
|
|
601
|
+
try {
|
|
602
|
+
cleanup();
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
console.error('[Dalila] Error in onMount cleanup lifecycle hook:', error);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (onUnmount) {
|
|
609
|
+
try {
|
|
610
|
+
const result = onUnmount(outletElement, data, ctx);
|
|
611
|
+
if (result && typeof result.then === 'function') {
|
|
612
|
+
void result.catch((error) => {
|
|
613
|
+
console.error('[Dalila] Error in onUnmount lifecycle hook:', error);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
console.error('[Dalila] Error in onUnmount lifecycle hook:', error);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
585
622
|
/**
|
|
586
623
|
* Mount nodes into outlet and remove loading state
|
|
587
624
|
*/
|
|
588
625
|
function mountToOutlet(...nodes) {
|
|
626
|
+
runRouteUnmountLifecycle();
|
|
589
627
|
outletElement.replaceChildren(...nodes);
|
|
590
628
|
queueMicrotask(() => {
|
|
591
629
|
outletElement.removeAttribute('d-loading');
|
|
@@ -950,7 +988,7 @@ export function createRouter(config) {
|
|
|
950
988
|
}
|
|
951
989
|
commitRouteState();
|
|
952
990
|
try {
|
|
953
|
-
mountViewStack(matchStack, ctx, dataStack);
|
|
991
|
+
mountViewStack(matchStack, ctx, dataStack, token);
|
|
954
992
|
await finishSuccessfulTransition();
|
|
955
993
|
}
|
|
956
994
|
catch (error) {
|
|
@@ -986,16 +1024,18 @@ export function createRouter(config) {
|
|
|
986
1024
|
return promise;
|
|
987
1025
|
}
|
|
988
1026
|
/** Compose and mount the view stack (leaf view wrapped by parent layouts). */
|
|
989
|
-
function mountViewStack(matchStack, ctx, dataStack) {
|
|
1027
|
+
function mountViewStack(matchStack, ctx, dataStack, navToken) {
|
|
990
1028
|
try {
|
|
991
1029
|
let content = null;
|
|
992
1030
|
let leafRoute = null;
|
|
1031
|
+
let leafData = undefined;
|
|
993
1032
|
for (let i = matchStack.length - 1; i >= 0; i--) {
|
|
994
1033
|
const match = matchStack[i];
|
|
995
1034
|
const data = dataStack[i];
|
|
996
1035
|
const route = match.route;
|
|
997
1036
|
if (i === matchStack.length - 1) {
|
|
998
1037
|
leafRoute = route;
|
|
1038
|
+
leafData = data;
|
|
999
1039
|
if (!route.view) {
|
|
1000
1040
|
console.warn(`[Dalila] Leaf route ${match.path} has no view function`);
|
|
1001
1041
|
return;
|
|
@@ -1027,11 +1067,54 @@ export function createRouter(config) {
|
|
|
1027
1067
|
if (content) {
|
|
1028
1068
|
const nodes = Array.isArray(content) ? content : [content];
|
|
1029
1069
|
mountToOutlet(...nodes);
|
|
1070
|
+
activeLeafRoute = leafRoute ?? null;
|
|
1071
|
+
activeRouteUnmountHook = leafRoute?.onUnmount ?? null;
|
|
1072
|
+
activeLeafData = leafData;
|
|
1073
|
+
activeLeafCtx = ctx;
|
|
1030
1074
|
// Call onMount lifecycle hook if present
|
|
1031
1075
|
if (leafRoute?.onMount) {
|
|
1032
1076
|
queueMicrotask(() => {
|
|
1077
|
+
if (navigationToken !== navToken)
|
|
1078
|
+
return;
|
|
1079
|
+
if (activeLeafRoute !== leafRoute)
|
|
1080
|
+
return;
|
|
1033
1081
|
try {
|
|
1034
|
-
leafRoute.onMount(outletElement);
|
|
1082
|
+
const result = leafRoute.onMount(outletElement, leafData, ctx);
|
|
1083
|
+
if (typeof result === 'function') {
|
|
1084
|
+
if (navigationToken === navToken && activeLeafRoute === leafRoute) {
|
|
1085
|
+
activeRouteMountCleanup = result;
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
try {
|
|
1089
|
+
result();
|
|
1090
|
+
}
|
|
1091
|
+
catch (cleanupError) {
|
|
1092
|
+
console.error('[Dalila] Error in stale onMount cleanup lifecycle hook:', cleanupError);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (result && typeof result.then === 'function') {
|
|
1098
|
+
void result
|
|
1099
|
+
.then((resolved) => {
|
|
1100
|
+
if (typeof resolved !== 'function')
|
|
1101
|
+
return;
|
|
1102
|
+
if (navigationToken === navToken && activeLeafRoute === leafRoute) {
|
|
1103
|
+
activeRouteMountCleanup = resolved;
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
try {
|
|
1107
|
+
resolved();
|
|
1108
|
+
}
|
|
1109
|
+
catch (cleanupError) {
|
|
1110
|
+
console.error('[Dalila] Error in stale async onMount cleanup lifecycle hook:', cleanupError);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
})
|
|
1114
|
+
.catch((error) => {
|
|
1115
|
+
console.error('[Dalila] Error in onMount lifecycle hook:', error);
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1035
1118
|
}
|
|
1036
1119
|
catch (error) {
|
|
1037
1120
|
console.error('[Dalila] Error in onMount lifecycle hook:', error);
|
|
@@ -1413,6 +1496,7 @@ export function createRouter(config) {
|
|
|
1413
1496
|
document.removeEventListener('click', handleLink);
|
|
1414
1497
|
document.removeEventListener('pointerover', handleLinkIntent);
|
|
1415
1498
|
document.removeEventListener('focusin', handleLinkIntent);
|
|
1499
|
+
runRouteUnmountLifecycle();
|
|
1416
1500
|
if (currentLoaderController) {
|
|
1417
1501
|
currentLoaderController.abort();
|
|
1418
1502
|
currentLoaderController = null;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// This file is auto-generated by 'dalila routes generate'
|
|
2
|
+
// Do not edit manually - your changes will be overwritten
|
|
3
|
+
import { fromHtml } from 'dalila';
|
|
4
|
+
const layout_html = `<div class="app-shell">
|
|
5
|
+
<h1>Router + PageVM + Componente</h1>
|
|
6
|
+
<nav>
|
|
7
|
+
<a href="/examples/router-simple/" d-link>Home</a>
|
|
8
|
+
<a href="/examples/router-simple/about" d-link>Sobre</a>
|
|
9
|
+
</nav>
|
|
10
|
+
<main id="outlet-slot" data-slot="children"></main>
|
|
11
|
+
</div>
|
|
12
|
+
`;
|
|
13
|
+
const page_html = `<section>
|
|
14
|
+
<h2>Home</h2>
|
|
15
|
+
<p>PageVM da rota: <strong>HomePageVM</strong></p>
|
|
16
|
+
<status-badge d-props-text="status"></status-badge>
|
|
17
|
+
<div style="display:flex; gap:.5rem; margin-top:.75rem;">
|
|
18
|
+
<button d-on-click="increment">Incrementar</button>
|
|
19
|
+
<button d-on-click="openModal">Abrir modal</button>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<d-dialog d-ui="infoDialog" class="d-dialog">
|
|
23
|
+
<d-dialog-header>
|
|
24
|
+
<d-dialog-title>Modal Dalila</d-dialog-title>
|
|
25
|
+
<d-dialog-close d-on-click="closeModal">×</d-dialog-close>
|
|
26
|
+
</d-dialog-header>
|
|
27
|
+
<d-dialog-body>
|
|
28
|
+
<p>This modal is mounted with <code>mountUI()</code> in the route <code>onMount</code>.</p>
|
|
29
|
+
<p>Status atual: <strong d-text="status"></strong></p>
|
|
30
|
+
</d-dialog-body>
|
|
31
|
+
<d-dialog-footer>
|
|
32
|
+
<button d-on-click="closeModal">Fechar</button>
|
|
33
|
+
</d-dialog-footer>
|
|
34
|
+
</d-dialog>
|
|
35
|
+
</section>
|
|
36
|
+
`;
|
|
37
|
+
const about_page_html = `<section>
|
|
38
|
+
<h2>Sobre</h2>
|
|
39
|
+
<p>PageVM da rota: <strong>AboutPageVM</strong></p>
|
|
40
|
+
<p>Nome atual: <strong d-text="nomeUpper"></strong></p>
|
|
41
|
+
<input d-bind-value="nome" placeholder="Digite um nome" />
|
|
42
|
+
</section>
|
|
43
|
+
`;
|
|
44
|
+
const page_lazy = () => import('./app/page.js');
|
|
45
|
+
const about_page_lazy = () => import('./app/about/page.js');
|
|
46
|
+
export const routes = [
|
|
47
|
+
{
|
|
48
|
+
path: '/',
|
|
49
|
+
layout: (ctx, children) => fromHtml(layout_html, { data: { ...ctx.params, params: ctx.params, query: ctx.query.toString(), path: ctx.path, fullPath: ctx.fullPath }, children, scope: ctx.scope }),
|
|
50
|
+
children: [
|
|
51
|
+
{ path: '', view: (ctx, data) => fromHtml(page_html, { data: { ...ctx.params, ...(data ?? {}), params: ctx.params, query: ctx.query.toString(), path: ctx.path, fullPath: ctx.fullPath }, scope: ctx.scope }), loader: (...args) => page_lazy().then(mod => {
|
|
52
|
+
const exported = mod.loader;
|
|
53
|
+
if (typeof exported === 'function') {
|
|
54
|
+
return exported(...args);
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}), onMount: (root, data, ctx) => page_lazy().then(mod => {
|
|
58
|
+
if (typeof mod.onMount === 'function') {
|
|
59
|
+
return mod.onMount(root, data, ctx);
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}) },
|
|
63
|
+
{
|
|
64
|
+
path: 'about',
|
|
65
|
+
view: (ctx, data) => fromHtml(about_page_html, { data: { ...ctx.params, ...(data ?? {}), params: ctx.params, query: ctx.query.toString(), path: ctx.path, fullPath: ctx.fullPath }, scope: ctx.scope }),
|
|
66
|
+
loader: (...args) => about_page_lazy().then(mod => {
|
|
67
|
+
const exported = mod.loader;
|
|
68
|
+
if (typeof exported === 'function') {
|
|
69
|
+
return exported(...args);
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}),
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RouteManifestEntry } from 'dalila/router';
|
|
2
|
+
export declare const routeManifest: RouteManifestEntry[];
|
|
3
|
+
export declare function getRouteManifestEntry(id: string): RouteManifestEntry | undefined;
|
|
4
|
+
export declare function prefetchRouteById(id: string): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// This file is auto-generated by 'dalila routes generate'
|
|
2
|
+
// Do not edit manually - your changes will be overwritten
|
|
3
|
+
export const routeManifest = [
|
|
4
|
+
{
|
|
5
|
+
id: 'root',
|
|
6
|
+
pattern: '/',
|
|
7
|
+
score: 1000,
|
|
8
|
+
paramKeys: [],
|
|
9
|
+
tags: [],
|
|
10
|
+
modules: ["./app/page.js"],
|
|
11
|
+
load: () => import('./app/page.js').then(() => undefined)
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'about',
|
|
15
|
+
pattern: '/about',
|
|
16
|
+
score: 301,
|
|
17
|
+
paramKeys: [],
|
|
18
|
+
tags: [],
|
|
19
|
+
modules: ["./app/page.js", "./app/about/page.js"],
|
|
20
|
+
load: () => Promise.all([import('./app/page.js'), import('./app/about/page.js')]).then(() => undefined)
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
const manifestById = new Map(routeManifest.map(route => [route.id, route]));
|
|
24
|
+
export function getRouteManifestEntry(id) {
|
|
25
|
+
return manifestById.get(id);
|
|
26
|
+
}
|
|
27
|
+
export async function prefetchRouteById(id) {
|
|
28
|
+
const entry = manifestById.get(id);
|
|
29
|
+
if (!entry)
|
|
30
|
+
return;
|
|
31
|
+
await entry.load();
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RoutePattern = '/' | '/about';
|
|
2
|
+
export type RouteParamsByPattern = {
|
|
3
|
+
'/': {};
|
|
4
|
+
'/about': {};
|
|
5
|
+
};
|
|
6
|
+
export type RouteSearchByPattern = {
|
|
7
|
+
[P in RoutePattern]: Record<string, string | string[]>;
|
|
8
|
+
};
|
|
9
|
+
export type RouteParams<P extends RoutePattern> = RouteParamsByPattern[P];
|
|
10
|
+
export type RouteSearch<P extends RoutePattern> = RouteSearchByPattern[P];
|
|
11
|
+
export declare function buildRoutePath<P extends RoutePattern>(pattern: P, params: RouteParams<P>): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// This file is auto-generated by 'dalila routes generate'
|
|
2
|
+
// Do not edit manually - your changes will be overwritten
|
|
3
|
+
export function buildRoutePath(pattern, params) {
|
|
4
|
+
const out = [];
|
|
5
|
+
for (const segment of pattern.split('/').filter(Boolean)) {
|
|
6
|
+
if (!segment.startsWith(':')) {
|
|
7
|
+
out.push(segment);
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const isOptionalCatchAll = segment.endsWith('*?');
|
|
11
|
+
const isCatchAll = isOptionalCatchAll || segment.endsWith('*');
|
|
12
|
+
const key = segment.slice(1, isCatchAll ? (isOptionalCatchAll ? -2 : -1) : undefined);
|
|
13
|
+
const value = params[key];
|
|
14
|
+
if (isCatchAll) {
|
|
15
|
+
if (value === undefined || value === null) {
|
|
16
|
+
if (isOptionalCatchAll)
|
|
17
|
+
continue;
|
|
18
|
+
throw new Error(`Missing route param: ${key}`);
|
|
19
|
+
}
|
|
20
|
+
if (!Array.isArray(value)) {
|
|
21
|
+
throw new Error(`Route param "${key}" must be an array`);
|
|
22
|
+
}
|
|
23
|
+
if (value.length === 0) {
|
|
24
|
+
if (isOptionalCatchAll)
|
|
25
|
+
continue;
|
|
26
|
+
throw new Error(`Route param "${key}" cannot be empty`);
|
|
27
|
+
}
|
|
28
|
+
out.push(...value.map(v => encodeURIComponent(String(v))));
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (value === undefined || value === null) {
|
|
32
|
+
throw new Error(`Missing route param: ${key}`);
|
|
33
|
+
}
|
|
34
|
+
out.push(encodeURIComponent(String(value)));
|
|
35
|
+
}
|
|
36
|
+
return out.length === 0 ? '/' : `/${out.join('/')}`;
|
|
37
|
+
}
|