@matthesketh/utopia-router 0.5.0 → 0.5.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/dist/index.cjs +74 -6
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +73 -6
- package/package.json +6 -6
- package/LICENSE +0 -21
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
isNavigating: () => isNavigating,
|
|
37
37
|
matchRoute: () => matchRoute,
|
|
38
38
|
navigate: () => navigate,
|
|
39
|
+
preloadRoute: () => preloadRoute,
|
|
39
40
|
queryParams: () => queryParams,
|
|
40
41
|
setQueryParam: () => setQueryParam,
|
|
41
42
|
setQueryParams: () => setQueryParams
|
|
@@ -417,12 +418,35 @@ function capScrollPositions() {
|
|
|
417
418
|
|
|
418
419
|
// src/components.ts
|
|
419
420
|
var import_utopia_core2 = require("@matthesketh/utopia-core");
|
|
421
|
+
var moduleCache = /* @__PURE__ */ new Map();
|
|
422
|
+
async function preloadRoute() {
|
|
423
|
+
const match = currentRoute.peek();
|
|
424
|
+
if (!match) return;
|
|
425
|
+
const promises = [match.route.component()];
|
|
426
|
+
if (match.route.layout) {
|
|
427
|
+
promises.push(match.route.layout());
|
|
428
|
+
}
|
|
429
|
+
const modules = await Promise.all(promises);
|
|
430
|
+
moduleCache.set(match.route.component, modules[0]);
|
|
431
|
+
if (match.route.layout && modules.length > 1) {
|
|
432
|
+
moduleCache.set(match.route.layout, modules[1]);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
420
435
|
function createRouterView() {
|
|
421
436
|
const container = document.createElement("div");
|
|
422
437
|
container.setAttribute("data-utopia-router-view", "");
|
|
423
438
|
let currentCleanup = null;
|
|
424
439
|
let currentMatch = null;
|
|
425
440
|
let loadId = 0;
|
|
441
|
+
const initialMatch = currentRoute.peek();
|
|
442
|
+
if (initialMatch) {
|
|
443
|
+
const syncResult = tryRenderFromCache(initialMatch);
|
|
444
|
+
if (syncResult) {
|
|
445
|
+
container.appendChild(syncResult.node);
|
|
446
|
+
currentCleanup = syncResult.cleanup;
|
|
447
|
+
currentMatch = initialMatch;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
426
450
|
(0, import_utopia_core2.effect)(() => {
|
|
427
451
|
const match = currentRoute();
|
|
428
452
|
if (match === currentMatch) {
|
|
@@ -456,15 +480,58 @@ function createRouterView() {
|
|
|
456
480
|
});
|
|
457
481
|
return container;
|
|
458
482
|
}
|
|
483
|
+
function tryRenderFromCache(match) {
|
|
484
|
+
const cachedPage = moduleCache.get(match.route.component);
|
|
485
|
+
if (!cachedPage) return null;
|
|
486
|
+
if (match.route.layout && !moduleCache.has(match.route.layout)) return null;
|
|
487
|
+
const cachedLayout = match.route.layout ? moduleCache.get(match.route.layout) : null;
|
|
488
|
+
moduleCache.delete(match.route.component);
|
|
489
|
+
if (match.route.layout) moduleCache.delete(match.route.layout);
|
|
490
|
+
const PageComponent = cachedPage.default ?? cachedPage;
|
|
491
|
+
const LayoutComponent = cachedLayout ? cachedLayout.default ?? cachedLayout : null;
|
|
492
|
+
const pageNode = renderComponent(PageComponent, {
|
|
493
|
+
params: match.params,
|
|
494
|
+
url: match.url
|
|
495
|
+
});
|
|
496
|
+
let node;
|
|
497
|
+
if (LayoutComponent) {
|
|
498
|
+
node = renderComponent(LayoutComponent, {
|
|
499
|
+
params: match.params,
|
|
500
|
+
url: match.url,
|
|
501
|
+
children: pageNode
|
|
502
|
+
});
|
|
503
|
+
} else {
|
|
504
|
+
node = pageNode;
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
node,
|
|
508
|
+
cleanup: () => {
|
|
509
|
+
if (node.parentNode) {
|
|
510
|
+
node.parentNode.removeChild(node);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
459
515
|
async function loadRouteComponent(match) {
|
|
460
516
|
try {
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
517
|
+
const cachedPage = moduleCache.get(match.route.component);
|
|
518
|
+
const cachedLayout = match.route.layout ? moduleCache.get(match.route.layout) : void 0;
|
|
519
|
+
let pageModule;
|
|
520
|
+
let layoutModule = null;
|
|
521
|
+
if (cachedPage) {
|
|
522
|
+
pageModule = cachedPage;
|
|
523
|
+
layoutModule = cachedLayout ?? null;
|
|
524
|
+
moduleCache.delete(match.route.component);
|
|
525
|
+
if (match.route.layout) moduleCache.delete(match.route.layout);
|
|
526
|
+
} else {
|
|
527
|
+
const promises = [match.route.component()];
|
|
528
|
+
if (match.route.layout) {
|
|
529
|
+
promises.push(match.route.layout());
|
|
530
|
+
}
|
|
531
|
+
const modules = await Promise.all(promises);
|
|
532
|
+
pageModule = modules[0];
|
|
533
|
+
layoutModule = modules.length > 1 ? modules[1] : null;
|
|
464
534
|
}
|
|
465
|
-
const modules = await Promise.all(promises);
|
|
466
|
-
const pageModule = modules[0];
|
|
467
|
-
const layoutModule = modules.length > 1 ? modules[1] : null;
|
|
468
535
|
if (currentRoute.peek() !== match) {
|
|
469
536
|
return null;
|
|
470
537
|
}
|
|
@@ -662,6 +729,7 @@ function getRouteParam(name) {
|
|
|
662
729
|
isNavigating,
|
|
663
730
|
matchRoute,
|
|
664
731
|
navigate,
|
|
732
|
+
preloadRoute,
|
|
665
733
|
queryParams,
|
|
666
734
|
setQueryParam,
|
|
667
735
|
setQueryParams
|
package/dist/index.d.cts
CHANGED
|
@@ -167,6 +167,20 @@ declare function beforeNavigate(hook: BeforeNavigateHook): () => void;
|
|
|
167
167
|
*/
|
|
168
168
|
declare function destroy(): void;
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Pre-load the current route's component (and layout) so that
|
|
172
|
+
* `createRouterView()` can render it synchronously on first paint.
|
|
173
|
+
*
|
|
174
|
+
* Call this **after** `createRouter()` and **before** `mount()`.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* createRouter(routes)
|
|
179
|
+
* await preloadRoute()
|
|
180
|
+
* mount(App, '#app')
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
declare function preloadRoute(): Promise<void>;
|
|
170
184
|
/**
|
|
171
185
|
* Creates a DOM node that renders the current route's component.
|
|
172
186
|
*
|
|
@@ -239,4 +253,4 @@ declare function setQueryParams(params: Record<string, string | null>): void;
|
|
|
239
253
|
*/
|
|
240
254
|
declare function getRouteParam(name: string): _matthesketh_utopia_core.ReadonlySignal<string | null>;
|
|
241
255
|
|
|
242
|
-
export { type BeforeNavigateHook, type Route, type RouteMatch, type RouterState, back, beforeNavigate, buildRouteTable, compilePattern, createLink, createRouter, createRouterView, currentRoute, destroy, filePathToRoute, forward, getQueryParam, getRouteParam, isNavigating, matchRoute, navigate, queryParams, setQueryParam, setQueryParams };
|
|
256
|
+
export { type BeforeNavigateHook, type Route, type RouteMatch, type RouterState, back, beforeNavigate, buildRouteTable, compilePattern, createLink, createRouter, createRouterView, currentRoute, destroy, filePathToRoute, forward, getQueryParam, getRouteParam, isNavigating, matchRoute, navigate, preloadRoute, queryParams, setQueryParam, setQueryParams };
|
package/dist/index.d.ts
CHANGED
|
@@ -167,6 +167,20 @@ declare function beforeNavigate(hook: BeforeNavigateHook): () => void;
|
|
|
167
167
|
*/
|
|
168
168
|
declare function destroy(): void;
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Pre-load the current route's component (and layout) so that
|
|
172
|
+
* `createRouterView()` can render it synchronously on first paint.
|
|
173
|
+
*
|
|
174
|
+
* Call this **after** `createRouter()` and **before** `mount()`.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* createRouter(routes)
|
|
179
|
+
* await preloadRoute()
|
|
180
|
+
* mount(App, '#app')
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
declare function preloadRoute(): Promise<void>;
|
|
170
184
|
/**
|
|
171
185
|
* Creates a DOM node that renders the current route's component.
|
|
172
186
|
*
|
|
@@ -239,4 +253,4 @@ declare function setQueryParams(params: Record<string, string | null>): void;
|
|
|
239
253
|
*/
|
|
240
254
|
declare function getRouteParam(name: string): _matthesketh_utopia_core.ReadonlySignal<string | null>;
|
|
241
255
|
|
|
242
|
-
export { type BeforeNavigateHook, type Route, type RouteMatch, type RouterState, back, beforeNavigate, buildRouteTable, compilePattern, createLink, createRouter, createRouterView, currentRoute, destroy, filePathToRoute, forward, getQueryParam, getRouteParam, isNavigating, matchRoute, navigate, queryParams, setQueryParam, setQueryParams };
|
|
256
|
+
export { type BeforeNavigateHook, type Route, type RouteMatch, type RouterState, back, beforeNavigate, buildRouteTable, compilePattern, createLink, createRouter, createRouterView, currentRoute, destroy, filePathToRoute, forward, getQueryParam, getRouteParam, isNavigating, matchRoute, navigate, preloadRoute, queryParams, setQueryParam, setQueryParams };
|
package/dist/index.js
CHANGED
|
@@ -373,12 +373,35 @@ function capScrollPositions() {
|
|
|
373
373
|
|
|
374
374
|
// src/components.ts
|
|
375
375
|
import { effect } from "@matthesketh/utopia-core";
|
|
376
|
+
var moduleCache = /* @__PURE__ */ new Map();
|
|
377
|
+
async function preloadRoute() {
|
|
378
|
+
const match = currentRoute.peek();
|
|
379
|
+
if (!match) return;
|
|
380
|
+
const promises = [match.route.component()];
|
|
381
|
+
if (match.route.layout) {
|
|
382
|
+
promises.push(match.route.layout());
|
|
383
|
+
}
|
|
384
|
+
const modules = await Promise.all(promises);
|
|
385
|
+
moduleCache.set(match.route.component, modules[0]);
|
|
386
|
+
if (match.route.layout && modules.length > 1) {
|
|
387
|
+
moduleCache.set(match.route.layout, modules[1]);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
376
390
|
function createRouterView() {
|
|
377
391
|
const container = document.createElement("div");
|
|
378
392
|
container.setAttribute("data-utopia-router-view", "");
|
|
379
393
|
let currentCleanup = null;
|
|
380
394
|
let currentMatch = null;
|
|
381
395
|
let loadId = 0;
|
|
396
|
+
const initialMatch = currentRoute.peek();
|
|
397
|
+
if (initialMatch) {
|
|
398
|
+
const syncResult = tryRenderFromCache(initialMatch);
|
|
399
|
+
if (syncResult) {
|
|
400
|
+
container.appendChild(syncResult.node);
|
|
401
|
+
currentCleanup = syncResult.cleanup;
|
|
402
|
+
currentMatch = initialMatch;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
382
405
|
effect(() => {
|
|
383
406
|
const match = currentRoute();
|
|
384
407
|
if (match === currentMatch) {
|
|
@@ -412,15 +435,58 @@ function createRouterView() {
|
|
|
412
435
|
});
|
|
413
436
|
return container;
|
|
414
437
|
}
|
|
438
|
+
function tryRenderFromCache(match) {
|
|
439
|
+
const cachedPage = moduleCache.get(match.route.component);
|
|
440
|
+
if (!cachedPage) return null;
|
|
441
|
+
if (match.route.layout && !moduleCache.has(match.route.layout)) return null;
|
|
442
|
+
const cachedLayout = match.route.layout ? moduleCache.get(match.route.layout) : null;
|
|
443
|
+
moduleCache.delete(match.route.component);
|
|
444
|
+
if (match.route.layout) moduleCache.delete(match.route.layout);
|
|
445
|
+
const PageComponent = cachedPage.default ?? cachedPage;
|
|
446
|
+
const LayoutComponent = cachedLayout ? cachedLayout.default ?? cachedLayout : null;
|
|
447
|
+
const pageNode = renderComponent(PageComponent, {
|
|
448
|
+
params: match.params,
|
|
449
|
+
url: match.url
|
|
450
|
+
});
|
|
451
|
+
let node;
|
|
452
|
+
if (LayoutComponent) {
|
|
453
|
+
node = renderComponent(LayoutComponent, {
|
|
454
|
+
params: match.params,
|
|
455
|
+
url: match.url,
|
|
456
|
+
children: pageNode
|
|
457
|
+
});
|
|
458
|
+
} else {
|
|
459
|
+
node = pageNode;
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
node,
|
|
463
|
+
cleanup: () => {
|
|
464
|
+
if (node.parentNode) {
|
|
465
|
+
node.parentNode.removeChild(node);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
415
470
|
async function loadRouteComponent(match) {
|
|
416
471
|
try {
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
472
|
+
const cachedPage = moduleCache.get(match.route.component);
|
|
473
|
+
const cachedLayout = match.route.layout ? moduleCache.get(match.route.layout) : void 0;
|
|
474
|
+
let pageModule;
|
|
475
|
+
let layoutModule = null;
|
|
476
|
+
if (cachedPage) {
|
|
477
|
+
pageModule = cachedPage;
|
|
478
|
+
layoutModule = cachedLayout ?? null;
|
|
479
|
+
moduleCache.delete(match.route.component);
|
|
480
|
+
if (match.route.layout) moduleCache.delete(match.route.layout);
|
|
481
|
+
} else {
|
|
482
|
+
const promises = [match.route.component()];
|
|
483
|
+
if (match.route.layout) {
|
|
484
|
+
promises.push(match.route.layout());
|
|
485
|
+
}
|
|
486
|
+
const modules = await Promise.all(promises);
|
|
487
|
+
pageModule = modules[0];
|
|
488
|
+
layoutModule = modules.length > 1 ? modules[1] : null;
|
|
420
489
|
}
|
|
421
|
-
const modules = await Promise.all(promises);
|
|
422
|
-
const pageModule = modules[0];
|
|
423
|
-
const layoutModule = modules.length > 1 ? modules[1] : null;
|
|
424
490
|
if (currentRoute.peek() !== match) {
|
|
425
491
|
return null;
|
|
426
492
|
}
|
|
@@ -617,6 +683,7 @@ export {
|
|
|
617
683
|
isNavigating,
|
|
618
684
|
matchRoute,
|
|
619
685
|
navigate,
|
|
686
|
+
preloadRoute,
|
|
620
687
|
queryParams,
|
|
621
688
|
setQueryParam,
|
|
622
689
|
setQueryParams
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthesketh/utopia-router",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "File-based routing for UtopiaJS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"files": [
|
|
40
40
|
"dist"
|
|
41
41
|
],
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"@matthesketh/utopia-core": "0.5.0",
|
|
44
|
-
"@matthesketh/utopia-runtime": "0.5.0"
|
|
45
|
-
},
|
|
46
42
|
"scripts": {
|
|
47
43
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
48
44
|
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@matthesketh/utopia-core": "workspace:*",
|
|
48
|
+
"@matthesketh/utopia-runtime": "workspace:*"
|
|
49
49
|
}
|
|
50
|
-
}
|
|
50
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Matt Hesketh
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|