@matthesketh/utopia-router 0.2.0 → 0.3.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
@@ -401,33 +401,41 @@ function createRouterView() {
401
401
  container.setAttribute("data-utopia-router-view", "");
402
402
  let currentCleanup = null;
403
403
  let currentMatch = null;
404
+ let loadId = 0;
404
405
  (0, import_utopia_core2.effect)(() => {
405
406
  const match = currentRoute();
406
407
  if (match === currentMatch) {
407
408
  return;
408
409
  }
409
410
  currentMatch = match;
410
- if (currentCleanup) {
411
- currentCleanup();
412
- currentCleanup = null;
413
- }
414
- while (container.firstChild) {
415
- container.removeChild(container.firstChild);
416
- }
411
+ const thisLoadId = ++loadId;
417
412
  if (!match) {
413
+ if (currentCleanup) {
414
+ currentCleanup();
415
+ currentCleanup = null;
416
+ }
417
+ clearContainer(container);
418
418
  const notFound = document.createElement("div");
419
419
  notFound.setAttribute("data-utopia-not-found", "");
420
420
  notFound.textContent = "Page not found";
421
421
  container.appendChild(notFound);
422
422
  return;
423
423
  }
424
- loadRouteComponent(match, container).then((cleanupFn) => {
425
- currentCleanup = cleanupFn;
424
+ loadRouteComponent(match).then((result) => {
425
+ if (thisLoadId !== loadId) return;
426
+ if (!result) return;
427
+ if (currentCleanup) {
428
+ currentCleanup();
429
+ currentCleanup = null;
430
+ }
431
+ clearContainer(container);
432
+ container.appendChild(result.node);
433
+ currentCleanup = result.cleanup;
426
434
  });
427
435
  });
428
436
  return container;
429
437
  }
430
- async function loadRouteComponent(match, container) {
438
+ async function loadRouteComponent(match) {
431
439
  try {
432
440
  const promises = [match.route.component()];
433
441
  if (match.route.layout) {
@@ -441,26 +449,26 @@ async function loadRouteComponent(match, container) {
441
449
  }
442
450
  const PageComponent = pageModule.default ?? pageModule;
443
451
  const LayoutComponent = layoutModule ? layoutModule.default ?? layoutModule : null;
444
- while (container.firstChild) {
445
- container.removeChild(container.firstChild);
446
- }
447
452
  const pageNode = renderComponent(PageComponent, {
448
453
  params: match.params,
449
454
  url: match.url
450
455
  });
456
+ let node;
451
457
  if (LayoutComponent) {
452
- const layoutNode = renderComponent(LayoutComponent, {
458
+ node = renderComponent(LayoutComponent, {
453
459
  params: match.params,
454
460
  url: match.url,
455
461
  children: pageNode
456
462
  });
457
- container.appendChild(layoutNode);
458
463
  } else {
459
- container.appendChild(pageNode);
464
+ node = pageNode;
460
465
  }
461
- return () => {
462
- while (container.firstChild) {
463
- container.removeChild(container.firstChild);
466
+ return {
467
+ node,
468
+ cleanup: () => {
469
+ if (node.parentNode) {
470
+ node.parentNode.removeChild(node);
471
+ }
464
472
  }
465
473
  };
466
474
  } catch (err) {
@@ -468,26 +476,33 @@ async function loadRouteComponent(match, container) {
468
476
  try {
469
477
  const errorModule = await match.route.error();
470
478
  const ErrorComponent = errorModule.default ?? errorModule;
471
- while (container.firstChild) {
472
- container.removeChild(container.firstChild);
473
- }
474
479
  const errorNode = renderComponent(ErrorComponent, {
475
480
  error: err,
476
481
  params: match.params,
477
482
  url: match.url
478
483
  });
479
- container.appendChild(errorNode);
484
+ return {
485
+ node: errorNode,
486
+ cleanup: () => {
487
+ if (errorNode.parentNode) {
488
+ errorNode.parentNode.removeChild(errorNode);
489
+ }
490
+ }
491
+ };
480
492
  } catch {
481
- renderFallbackError(container, err);
493
+ return {
494
+ node: createFallbackErrorNode(err),
495
+ cleanup: () => {
496
+ }
497
+ };
482
498
  }
483
499
  } else {
484
- renderFallbackError(container, err);
500
+ return {
501
+ node: createFallbackErrorNode(err),
502
+ cleanup: () => {
503
+ }
504
+ };
485
505
  }
486
- return () => {
487
- while (container.firstChild) {
488
- container.removeChild(container.firstChild);
489
- }
490
- };
491
506
  }
492
507
  }
493
508
  function renderComponent(component, props) {
@@ -510,10 +525,7 @@ function renderComponent(component, props) {
510
525
  div.textContent = "[Component render error]";
511
526
  return div;
512
527
  }
513
- function renderFallbackError(container, error) {
514
- while (container.firstChild) {
515
- container.removeChild(container.firstChild);
516
- }
528
+ function createFallbackErrorNode(error) {
517
529
  const errorDiv = document.createElement("div");
518
530
  errorDiv.setAttribute("data-utopia-error", "");
519
531
  errorDiv.style.cssText = "padding:2rem;color:#dc2626;font-family:monospace;";
@@ -523,7 +535,7 @@ function renderFallbackError(container, error) {
523
535
  error instanceof Error ? error.message : String(error)
524
536
  )}</pre>
525
537
  `;
526
- container.appendChild(errorDiv);
538
+ return errorDiv;
527
539
  }
528
540
  function createLink(props) {
529
541
  const anchor = document.createElement("a");
@@ -550,6 +562,11 @@ function createLink(props) {
550
562
  }
551
563
  return anchor;
552
564
  }
565
+ function clearContainer(container) {
566
+ while (container.firstChild) {
567
+ container.removeChild(container.firstChild);
568
+ }
569
+ }
553
570
  function escapeHtml(str) {
554
571
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
555
572
  }
package/dist/index.d.cts CHANGED
@@ -171,8 +171,8 @@ declare function destroy(): void;
171
171
  * Creates a DOM node that renders the current route's component.
172
172
  *
173
173
  * When the route changes:
174
- * 1. The old component is unmounted (removed from DOM)
175
- * 2. The new component is lazily loaded
174
+ * 1. The new component is lazily loaded (old content stays visible)
175
+ * 2. Once loaded, the old component is swapped out atomically
176
176
  * 3. If the route has a layout, the page is wrapped in the layout
177
177
  * 4. If loading fails and an error component exists, it is shown instead
178
178
  *
package/dist/index.d.ts CHANGED
@@ -171,8 +171,8 @@ declare function destroy(): void;
171
171
  * Creates a DOM node that renders the current route's component.
172
172
  *
173
173
  * When the route changes:
174
- * 1. The old component is unmounted (removed from DOM)
175
- * 2. The new component is lazily loaded
174
+ * 1. The new component is lazily loaded (old content stays visible)
175
+ * 2. Once loaded, the old component is swapped out atomically
176
176
  * 3. If the route has a layout, the page is wrapped in the layout
177
177
  * 4. If loading fails and an error component exists, it is shown instead
178
178
  *
package/dist/index.js CHANGED
@@ -362,33 +362,41 @@ function createRouterView() {
362
362
  container.setAttribute("data-utopia-router-view", "");
363
363
  let currentCleanup = null;
364
364
  let currentMatch = null;
365
+ let loadId = 0;
365
366
  effect(() => {
366
367
  const match = currentRoute();
367
368
  if (match === currentMatch) {
368
369
  return;
369
370
  }
370
371
  currentMatch = match;
371
- if (currentCleanup) {
372
- currentCleanup();
373
- currentCleanup = null;
374
- }
375
- while (container.firstChild) {
376
- container.removeChild(container.firstChild);
377
- }
372
+ const thisLoadId = ++loadId;
378
373
  if (!match) {
374
+ if (currentCleanup) {
375
+ currentCleanup();
376
+ currentCleanup = null;
377
+ }
378
+ clearContainer(container);
379
379
  const notFound = document.createElement("div");
380
380
  notFound.setAttribute("data-utopia-not-found", "");
381
381
  notFound.textContent = "Page not found";
382
382
  container.appendChild(notFound);
383
383
  return;
384
384
  }
385
- loadRouteComponent(match, container).then((cleanupFn) => {
386
- currentCleanup = cleanupFn;
385
+ loadRouteComponent(match).then((result) => {
386
+ if (thisLoadId !== loadId) return;
387
+ if (!result) return;
388
+ if (currentCleanup) {
389
+ currentCleanup();
390
+ currentCleanup = null;
391
+ }
392
+ clearContainer(container);
393
+ container.appendChild(result.node);
394
+ currentCleanup = result.cleanup;
387
395
  });
388
396
  });
389
397
  return container;
390
398
  }
391
- async function loadRouteComponent(match, container) {
399
+ async function loadRouteComponent(match) {
392
400
  try {
393
401
  const promises = [match.route.component()];
394
402
  if (match.route.layout) {
@@ -402,26 +410,26 @@ async function loadRouteComponent(match, container) {
402
410
  }
403
411
  const PageComponent = pageModule.default ?? pageModule;
404
412
  const LayoutComponent = layoutModule ? layoutModule.default ?? layoutModule : null;
405
- while (container.firstChild) {
406
- container.removeChild(container.firstChild);
407
- }
408
413
  const pageNode = renderComponent(PageComponent, {
409
414
  params: match.params,
410
415
  url: match.url
411
416
  });
417
+ let node;
412
418
  if (LayoutComponent) {
413
- const layoutNode = renderComponent(LayoutComponent, {
419
+ node = renderComponent(LayoutComponent, {
414
420
  params: match.params,
415
421
  url: match.url,
416
422
  children: pageNode
417
423
  });
418
- container.appendChild(layoutNode);
419
424
  } else {
420
- container.appendChild(pageNode);
425
+ node = pageNode;
421
426
  }
422
- return () => {
423
- while (container.firstChild) {
424
- container.removeChild(container.firstChild);
427
+ return {
428
+ node,
429
+ cleanup: () => {
430
+ if (node.parentNode) {
431
+ node.parentNode.removeChild(node);
432
+ }
425
433
  }
426
434
  };
427
435
  } catch (err) {
@@ -429,26 +437,33 @@ async function loadRouteComponent(match, container) {
429
437
  try {
430
438
  const errorModule = await match.route.error();
431
439
  const ErrorComponent = errorModule.default ?? errorModule;
432
- while (container.firstChild) {
433
- container.removeChild(container.firstChild);
434
- }
435
440
  const errorNode = renderComponent(ErrorComponent, {
436
441
  error: err,
437
442
  params: match.params,
438
443
  url: match.url
439
444
  });
440
- container.appendChild(errorNode);
445
+ return {
446
+ node: errorNode,
447
+ cleanup: () => {
448
+ if (errorNode.parentNode) {
449
+ errorNode.parentNode.removeChild(errorNode);
450
+ }
451
+ }
452
+ };
441
453
  } catch {
442
- renderFallbackError(container, err);
454
+ return {
455
+ node: createFallbackErrorNode(err),
456
+ cleanup: () => {
457
+ }
458
+ };
443
459
  }
444
460
  } else {
445
- renderFallbackError(container, err);
461
+ return {
462
+ node: createFallbackErrorNode(err),
463
+ cleanup: () => {
464
+ }
465
+ };
446
466
  }
447
- return () => {
448
- while (container.firstChild) {
449
- container.removeChild(container.firstChild);
450
- }
451
- };
452
467
  }
453
468
  }
454
469
  function renderComponent(component, props) {
@@ -471,10 +486,7 @@ function renderComponent(component, props) {
471
486
  div.textContent = "[Component render error]";
472
487
  return div;
473
488
  }
474
- function renderFallbackError(container, error) {
475
- while (container.firstChild) {
476
- container.removeChild(container.firstChild);
477
- }
489
+ function createFallbackErrorNode(error) {
478
490
  const errorDiv = document.createElement("div");
479
491
  errorDiv.setAttribute("data-utopia-error", "");
480
492
  errorDiv.style.cssText = "padding:2rem;color:#dc2626;font-family:monospace;";
@@ -484,7 +496,7 @@ function renderFallbackError(container, error) {
484
496
  error instanceof Error ? error.message : String(error)
485
497
  )}</pre>
486
498
  `;
487
- container.appendChild(errorDiv);
499
+ return errorDiv;
488
500
  }
489
501
  function createLink(props) {
490
502
  const anchor = document.createElement("a");
@@ -511,6 +523,11 @@ function createLink(props) {
511
523
  }
512
524
  return anchor;
513
525
  }
526
+ function clearContainer(container) {
527
+ while (container.firstChild) {
528
+ container.removeChild(container.firstChild);
529
+ }
530
+ }
514
531
  function escapeHtml(str) {
515
532
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
516
533
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthesketh/utopia-router",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "File-based routing for UtopiaJS",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,8 +40,8 @@
40
40
  "dist"
41
41
  ],
42
42
  "dependencies": {
43
- "@matthesketh/utopia-core": "0.2.0",
44
- "@matthesketh/utopia-runtime": "0.2.0"
43
+ "@matthesketh/utopia-core": "0.3.1",
44
+ "@matthesketh/utopia-runtime": "0.3.1"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsup src/index.ts --format esm,cjs --dts",