@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 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 promises = [match.route.component()];
462
- if (match.route.layout) {
463
- promises.push(match.route.layout());
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 promises = [match.route.component()];
418
- if (match.route.layout) {
419
- promises.push(match.route.layout());
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.0",
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.