@uniflowed/vite 0.0.0-alpha.32 → 0.0.0-alpha.34

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/driver.js CHANGED
@@ -1356,9 +1356,9 @@ async function deploy() {
1356
1356
  * out — `server.js` below does exactly that through
1357
1357
  * `@uniflowed/server/node`, and a worker hands `settle` to `ctx.waitUntil`.
1358
1358
  * It comes from the bundle rather than from the host's own
1359
- * `@uniflowed/server`, because the request lives in an `AsyncLocalStorage`
1360
- * belonging to a module instance and the instance the application reads is the
1361
- * one inlined here. See ubugeeei-prod/uf#389.
1359
+ * `@uniflowed/server`, because the request store is shared only by copies of
1360
+ * one release of that package and the release the application reads is the one
1361
+ * inlined here. See ubugeeei-prod/uf#389.
1362
1362
  *
1363
1363
  * The document's script and stylesheet URLs are baked in here because they
1364
1364
  * come from the client manifest, which exists at this moment and not in the
package/index.js CHANGED
@@ -291,6 +291,12 @@ function flowPlugin({
291
291
  const forgetActions = () => {
292
292
  actionMemo = null;
293
293
  };
294
+ const invalidateActionModules = (moduleGraph, modules) => {
295
+ for (const id of modules.keys()) {
296
+ const module = moduleGraph.getModuleById(id);
297
+ if (module) moduleGraph.invalidateModule(module);
298
+ }
299
+ };
294
300
  const actionTables = () => {
295
301
  const file = process.env[RSC_MANIFEST_ENV];
296
302
  const key = rscManifestKey(file);
@@ -581,11 +587,15 @@ function flowPlugin({
581
587
  devServer.watcher.add(manifestPath);
582
588
  const onManifest = (file) => {
583
589
  if (path.resolve(file) !== manifestPath) return;
590
+ const previousActions = actionTables().modules;
584
591
  // The action tables are read from the same file and are memoised on
585
592
  // its size and modification time, which is a pair two writes inside
586
593
  // one millisecond can share. This is the answer that does not
587
594
  // depend on a clock.
588
595
  forgetActions();
596
+ const nextActions = actionTables().modules;
597
+ invalidateActionModules(devServer.moduleGraph, previousActions);
598
+ invalidateActionModules(devServer.moduleGraph, nextActions);
589
599
  const routes = devServer.moduleGraph.getModuleById(resolved(VIRTUAL.routes));
590
600
  if (routes) devServer.moduleGraph.invalidateModule(routes);
591
601
  const actions = devServer.moduleGraph.getModuleById(resolved(VIRTUAL.actions));
@@ -39,23 +39,44 @@ export const RESERVED = Object.freeze({
39
39
  * spelling one router refuses and the other serves as a URL is exactly the
40
40
  * disagreement that made this necessary.
41
41
  *
42
- * `(.)photo` is Next.js's intercepting route. `@team`, its parallel-route
43
- * slot, was on this list too: until #267 both fell through to "a literal URL
44
- * segment", so `@team` became `/@team`, `(.)photo` became `/(.)photo` — the
45
- * test for a `(group)` is that the segment *ends* in `)` — and the generated
46
- * `RoutePath` union contained them, so `route("/@team", …)` type checked. A
42
+ * Until #267 neither `@team` nor `(.)photo` meant anything to this scan, so
43
+ * both fell through to "a literal URL segment": `@team` became `/@team`,
44
+ * `(.)photo` became `/(.)photo` — the test for a `(group)` is that the segment
45
+ * *ends* in `)` — and the generated `RoutePath` union contained them. A
47
46
  * convention served as nonsense is worse than one that is refused, because the
48
47
  * project looks like it works.
49
48
  *
50
- * A slot is a route this router serves now; see {@link scanRoutes}. An
51
- * interception is not: it needs a navigation to carry where it came from,
52
- * which is a change to what a navigation is rather than to this scan.
49
+ * Both are routes this router serves now: a slot everywhere, and an
50
+ * interception inside a slot; see {@link INTERCEPTION_SEGMENTS}. What is left
51
+ * here is what is spelled like an interception and cannot be one — a marker
52
+ * that climbs nowhere, or a marker with no URL segment after it.
53
53
  */
54
54
  export const UNSUPPORTED_SEGMENTS = Object.freeze([
55
+ "(.)(.)photo",
56
+ "(.)(..)photo",
57
+ "(...)(..)photo",
58
+ "(....)photo",
59
+ "(.)(gallery)",
60
+ "(.)@photo",
61
+ ]);
62
+
63
+ /**
64
+ * Directory names this router serves inside a `@slot` and refuses outside one:
65
+ * intercepting routes.
66
+ *
67
+ * The same list as `uf_router::RouteSegment::SLOT_ONLY_EXAMPLES`, held to it by
68
+ * `crates/uf_router/tests/reserved_names.rs`. A second list rather than more
69
+ * entries on {@link UNSUPPORTED_SEGMENTS}, because the refusal is a different
70
+ * sentence: these are spelled correctly and are in the wrong place, and telling
71
+ * somebody to rename a correct directory is the worse of the two mistakes.
72
+ */
73
+ export const INTERCEPTION_SEGMENTS = Object.freeze([
55
74
  "(.)photo",
56
75
  "(..)photo",
57
76
  "(...)photo",
58
77
  "(..)(..)photo",
78
+ "(..)(..)(..)photo",
79
+ "(.)[id]",
59
80
  ]);
60
81
 
61
82
  /**
@@ -77,6 +98,16 @@ export const LAYOUT_PROP_NAMES = Object.freeze(["children", "params"]);
77
98
  /** Template spellings that look conventional elsewhere but uf will not open. */
78
99
  const UNSUPPORTED_TEMPLATE_FILES = Object.freeze(["template.js", "_uf.template.js"]);
79
100
 
101
+ /** Boundary spellings a slot might look for, but uf does not open. */
102
+ const UNSUPPORTED_SLOT_BOUNDARY_FILES = Object.freeze({
103
+ "error.js": "error",
104
+ "loading.js": "loading",
105
+ "not-found.js": "not-found",
106
+ "_uf.error.js": "error",
107
+ "_uf.loading.js": "loading",
108
+ "_uf.not-found.js": "not-found",
109
+ });
110
+
80
111
  /** Extensions a page or layout may use; `.mdx` is a page written as content. */
81
112
  const PAGE_EXTENSIONS = [".js", ".jsx", ".mdx"];
82
113
  const MODULE_EXTENSIONS = [".js", ".jsx"];
@@ -172,23 +203,34 @@ const MAX_DEPTH = 32;
172
203
  * renders nothing, which is what an unaddressed slot on a soft navigation does
173
204
  * in Next.js too.
174
205
  *
206
+ * `intercepts` is what the slot renders for a client navigation that starts on
207
+ * a page it is on and reaches the URL each entry names — the pages under an
208
+ * interception directory such as `@modal/(.)photo/[id]/`, each at the URL it
209
+ * stands in for. A list of its own, because nothing that matches `routes`
210
+ * may reach one: the server renders the ordinary page for that URL, always.
211
+ *
175
212
  * @typedef {object} Slot
176
213
  * @property {string} name the slot's name, without the `@`
177
214
  * @property {number} above how many of the route's layouts are outside it
178
215
  * @property {?string} defaultPage absolute path of `$default.*`, or `null`
179
216
  * @property {boolean} defaultMdx whether that default is MDX content
217
+ * @property {?{above: number, module: string}} defaultErrorBoundary the
218
+ * `$error.js` boundary that catches the default page in the browser
180
219
  * @property {ReadonlyArray<SlotRoute>} routes what the slot may render, by URL
220
+ * @property {ReadonlyArray<SlotRoute>} intercepts what the slot renders when a
221
+ * client navigation is intercepted, by the URL it stands in for
181
222
  */
182
223
 
183
224
  /**
184
225
  * One page inside a slot.
185
226
  *
186
- * A `Route` without the parts a slot does not have: no `loading`, no
187
- * `templates`, and no boundary of its own. Those are the segment's, and they
188
- * already wrap the layout the slot renders into. Per-slot boundaries are the
189
- * part of parallel routes uf has not built — see
227
+ * A `Route` without the request-level parts a slot does not have: no handler
228
+ * and no not-found boundary of its own. Loading boundaries, templates and
229
+ * browser render error boundaries are the pieces that compose like layouts, so
230
+ * they are carried below. Per-slot not-found boundaries are the part of
231
+ * parallel routes uf has not built — see
190
232
  * https://github.com/ubugeeei-prod/uf/issues/267 — and {@link scanRoutes}
191
- * refuses the files rather than leaving them unopened.
233
+ * refuses those files rather than leaving them unopened.
192
234
  *
193
235
  * `layouts` are the layouts *inside* the slot, root first; the ones above it
194
236
  * are already rendering, since the slot renders into one of them.
@@ -198,6 +240,12 @@ const MAX_DEPTH = 32;
198
240
  * @property {ReadonlyArray<{name: string, catchAll: boolean}>} params
199
241
  * @property {string} page absolute path of the page module
200
242
  * @property {ReadonlyArray<string>} layouts absolute paths, slot root first
243
+ * @property {ReadonlyArray<{above: number, module: string}>} loading the
244
+ * `$loading.js` fallbacks inside the slot, root first
245
+ * @property {ReadonlyArray<{above: number, module: string}>} templates the
246
+ * `$template.js` wrappers inside the slot, root first
247
+ * @property {?{above: number, module: string}} errorBoundary the `$error.js`
248
+ * boundary inside the slot, if one is in scope
201
249
  * @property {ReadonlyArray<Slot>} slots slots declared inside this slot
202
250
  * @property {boolean} mdx whether the page is MDX content
203
251
  */
@@ -291,16 +339,17 @@ const MAX_DEPTH = 32;
291
339
  * Directories that do not exist yield an empty table rather than an error: a
292
340
  * library project has no router root, and that is not a mistake.
293
341
  *
294
- * Throws for a directory named the way an intercepting route is spelled: uf
295
- * does not have interception, and the spelling used to become a literal URL
296
- * segment. See {@link UNSUPPORTED_SEGMENTS}.
297
- *
298
342
  * A `@slot` directory is a parallel route and is scanned; see {@link Slot}. It
299
- * throws for the three ways one can be written without being renderable: a
300
- * slot on a segment with no layout of its own, a `$default.js` that is not
301
- * directly inside a slot, and a boundary or a handler inside a slot. Each is a
302
- * file the router would otherwise never open, which is the failure #267 is
303
- * about.
343
+ * throws for the ways one can be written without being renderable: a slot on a
344
+ * segment with no layout of its own, a `$default.js` that is not directly
345
+ * inside a slot, a not-found boundary or handler inside a slot, and
346
+ * boundary-like files with names uf does not open. Each is a file the router
347
+ * would otherwise never open, which is the failure #267 is about.
348
+ *
349
+ * An interception directory — `(.)photo` — is scanned inside a slot, into that
350
+ * slot's `intercepts`, and throws everywhere it cannot be one: outside a slot,
351
+ * spelled so nothing reads it ({@link UNSUPPORTED_SEGMENTS}), climbing past the
352
+ * router root, or standing in for a URL no page serves.
304
353
  *
305
354
  * @param {string} appRoot absolute path of the router root (`app/`)
306
355
  * @param {{target?: "web" | "native" | "ios" | "android"}} [options]
@@ -464,8 +513,10 @@ export function scanRoutes(appRoot, options = {}) {
464
513
  if (classifyRouteSegment(entry.name).kind === "slot") continue;
465
514
  // Checked before descending, and after the private-directory test for
466
515
  // the same reason `uf_router` prunes them: `app/_drafts/(.)photo/` is not
467
- // a route uf would have served, so it is not one to refuse.
468
- const refused = unsupportedSegmentReason(entry.name);
516
+ // a route uf would have served, so it is not one to refuse. This walk is
517
+ // everywhere a slot is not, so a correctly spelled interception is
518
+ // refused here too — for where it is rather than how it is written.
519
+ const refused = unsupportedSegmentReason(entry.name) ?? outsideSlotReason(entry.name);
469
520
  if (refused != null) {
470
521
  throw new Error(`${path.join(directory, entry.name)}: ${refused}`);
471
522
  }
@@ -526,9 +577,88 @@ export function scanRoutes(appRoot, options = {}) {
526
577
  // parallel-route trees uf does not have yet; see ubugeeei-prod/uf#267.
527
578
  notFound.sort(byPath);
528
579
  errors.sort(byPath);
580
+ // Last, because it is about the table rather than a directory: an
581
+ // intercepting page is only as good as the ordinary page that serves its URL
582
+ // to everybody the interception does not.
583
+ refuseInterceptionsWithoutPages(appRoot, routes);
529
584
  return { routes, handlers, middleware, notFound, errors };
530
585
  }
531
586
 
587
+ /**
588
+ * Refuse an intercepting route whose URL no page serves.
589
+ *
590
+ * An interception renders in its slot only for a client navigation that starts
591
+ * on a page the slot is on. Everybody else who arrives at the URL — a reload, a
592
+ * shared link, a crawler, the prerender — is given the page the URL names, and
593
+ * with none the photo a reader opened in a modal is a 404 the moment they reload
594
+ * it or send it to somebody. Mirrors `uf_router`'s `check_interceptions`.
595
+ *
596
+ * Each slot record is visited once, because a record is shared by every route
597
+ * under the segment that declares it.
598
+ */
599
+ function refuseInterceptionsWithoutPages(appRoot, routes) {
600
+ const seen = new Set();
601
+ const visit = (slots) => {
602
+ for (const slot of slots) {
603
+ if (seen.has(slot)) continue;
604
+ seen.add(slot);
605
+ for (const intercepting of slot.intercepts ?? []) {
606
+ if (!routes.some((route) => servesEveryUrlOf(route.path, intercepting.path))) {
607
+ const directories = intercepting.path
608
+ .split("/")
609
+ .filter((part) => part !== "")
610
+ .map((part) =>
611
+ part.startsWith(":") && part.endsWith("*")
612
+ ? `[...${part.slice(1, -1)}]`
613
+ : part.startsWith(":")
614
+ ? `[${part.slice(1)}]`
615
+ : part,
616
+ );
617
+ const ordinary = path.join(appRoot, ...directories, `${RESERVED.page}.js`);
618
+ throw new Error(
619
+ `${intercepting.page}: this intercepting route stands in for \`${intercepting.path}\` ` +
620
+ "when a client navigation reaches it, and no page serves " +
621
+ `\`${intercepting.path}\`, so a reload of that URL, a link to it and the prerender ` +
622
+ `would all be a 404. Add \`${ordinary}\`, the page everybody who does not arrive by ` +
623
+ "that navigation gets, or remove the interception.",
624
+ );
625
+ }
626
+ visit(intercepting.slots);
627
+ }
628
+ for (const route of slot.routes) {
629
+ visit(route.slots);
630
+ }
631
+ }
632
+ };
633
+ for (const route of routes) {
634
+ visit(route.slots ?? []);
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Whether every URL the route path `intercepted` matches is one `ordinary`
640
+ * serves: segment by segment, the way the runtime's matcher reads both. A
641
+ * static segment serves only itself, a parameter any one segment but not a
642
+ * catch-all's many, and a catch-all whatever is left as long as something is.
643
+ * Mirrors `uf_router`'s `serves_every_url_of`.
644
+ */
645
+ function servesEveryUrlOf(ordinary, intercepted) {
646
+ const theirs = ordinary.split("/").filter((part) => part !== "");
647
+ const ours = intercepted.split("/").filter((part) => part !== "");
648
+ for (let index = 0; index < theirs.length; index += 1) {
649
+ const segment = theirs[index];
650
+ if (segment.startsWith(":") && segment.endsWith("*")) return ours.length > index;
651
+ const other = ours[index];
652
+ if (other === undefined) return false;
653
+ const otherIsCatchAll = other.startsWith(":") && other.endsWith("*");
654
+ const serves = segment.startsWith(":")
655
+ ? !otherIsCatchAll
656
+ : !other.startsWith(":") && other === segment;
657
+ if (!serves) return false;
658
+ }
659
+ return ours.length === theirs.length;
660
+ }
661
+
532
662
  /**
533
663
  * One `@slot` directory, scanned into a {@link Slot}.
534
664
  *
@@ -580,14 +710,35 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, targe
580
710
  }
581
711
 
582
712
  const routes = [];
713
+ // What the slot renders *instead of* the page a client navigation reaches:
714
+ // the pages under an interception directory. A list of its own rather than
715
+ // more `routes`, because `routes` is matched against every URL the segment
716
+ // renders — by the server as much as the browser — and nothing but a
717
+ // navigation that starts on a page this slot is on may render one of these.
718
+ const intercepts = [];
583
719
  const defaultPage = findModule(directory, RESERVED.default, PAGE_EXTENSIONS, target);
584
-
585
- const walkSlot = (current, currentSegments, layouts, atSlotRoot, currentDepth) => {
720
+ const defaultError = findModule(directory, RESERVED.error, MODULE_EXTENSIONS, target);
721
+
722
+ const walkSlot = (
723
+ current,
724
+ currentSegments,
725
+ layouts,
726
+ loading,
727
+ templates,
728
+ errorBoundary,
729
+ atSlotRoot,
730
+ intercepting,
731
+ currentDepth,
732
+ ) => {
586
733
  if (currentDepth > MAX_DEPTH) return;
734
+ const entries = readdirSync(current, { withFileTypes: true }).sort((a, b) =>
735
+ a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
736
+ );
737
+
587
738
  // What a slot does not have, said where somebody writing the file will
588
- // read it rather than by never opening it. This is also the list of what
589
- // is left of parallel routes; see the issue.
590
- for (const role of [RESERVED.notFound, RESERVED.error, RESERVED.loading, RESERVED.template]) {
739
+ // read it rather than by never opening it. Loading and error boundaries
740
+ // compose like layouts, so they are carried below instead of refused here.
741
+ for (const role of [RESERVED.notFound]) {
591
742
  const found =
592
743
  findModule(current, role, MODULE_EXTENSIONS, target) ??
593
744
  findModule(current, role, PAGE_EXTENSIONS, target);
@@ -595,12 +746,24 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, targe
595
746
  throw new Error(
596
747
  `${found}: a \`@slot\` renders a page and the layouts inside the slot, and has no ` +
597
748
  `\`${role.slice("$".length)}\` of its own — uf's parallel routes do not carry ` +
598
- "per-slot boundaries yet, so this file would never be opened. Put it outside " +
749
+ "per-slot not-found boundaries yet, so this file would never be opened. Put it outside " +
599
750
  `\`${directoryName}\`, where it covers the whole segment. ` +
600
751
  "https://github.com/ubugeeei-prod/uf/issues/267",
601
752
  );
602
753
  }
603
754
  }
755
+ for (const entry of entries) {
756
+ if (!entry.isFile()) continue;
757
+ const role = unsupportedSlotBoundaryRole(entry.name);
758
+ if (role == null) continue;
759
+ const file = path.join(current, entry.name);
760
+ throw new Error(
761
+ `${file}: \`${entry.name}\` looks like a \`${role}\` boundary for a \`@slot\`, but it ` +
762
+ "is not a uf route file there. Use `$loading.js` for slot loading and `$error.js` " +
763
+ "for slot errors; per-slot not-found boundaries are still not implemented. " +
764
+ "https://github.com/ubugeeei-prod/uf/issues/267",
765
+ );
766
+ }
604
767
  for (const role of [RESERVED.route, RESERVED.middleware]) {
605
768
  const found = findModule(current, role, MODULE_EXTENSIONS, target);
606
769
  if (found != null) {
@@ -626,10 +789,18 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, targe
626
789
 
627
790
  const layoutHere = findModule(current, RESERVED.layout, MODULE_EXTENSIONS, target);
628
791
  const nextLayouts = layoutHere ? [...layouts, layoutHere] : layouts;
629
-
630
- const entries = readdirSync(current, { withFileTypes: true }).sort((a, b) =>
631
- a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
632
- );
792
+ const loadingHere = findModule(current, RESERVED.loading, MODULE_EXTENSIONS, target);
793
+ const nextLoading = loadingHere
794
+ ? [...loading, { above: nextLayouts.length, module: loadingHere }]
795
+ : loading;
796
+ const templateHere = findModule(current, RESERVED.template, MODULE_EXTENSIONS, target);
797
+ const nextTemplates = templateHere
798
+ ? [...templates, { above: nextLayouts.length, module: templateHere }]
799
+ : templates;
800
+ const errorHere = findModule(current, RESERVED.error, MODULE_EXTENSIONS, target);
801
+ const nextErrorBoundary = errorHere
802
+ ? { above: nextLayouts.length, module: errorHere }
803
+ : errorBoundary;
633
804
 
634
805
  refuseUnsupportedTemplateFiles(current, entries);
635
806
 
@@ -656,12 +827,18 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, targe
656
827
 
657
828
  const page = findModule(current, RESERVED.page, PAGE_EXTENSIONS, target);
658
829
  if (page) {
830
+ // Under an interception directory the path is the URL the page stands in
831
+ // for — `routeFromSegments` applies the climb — and the page goes in the
832
+ // slot's other list.
659
833
  const { path: routePath, params } = routeFromSegments(currentSegments);
660
- routes.push({
834
+ (intercepting ? intercepts : routes).push({
661
835
  path: routePath,
662
836
  params,
663
837
  page,
664
838
  layouts: nextLayouts,
839
+ loading: nextLoading,
840
+ templates: nextTemplates,
841
+ errorBoundary: nextErrorBoundary,
665
842
  slots: nestedSlots,
666
843
  mdx: page.endsWith(".mdx"),
667
844
  });
@@ -672,30 +849,59 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, targe
672
849
  if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
673
850
  const classified = classifyRouteSegment(entry.name);
674
851
  if (classified.kind === "slot") continue;
675
- const refused = unsupportedSegmentReason(entry.name);
676
- if (refused != null) {
677
- throw new Error(`${path.join(current, entry.name)}: ${refused}`);
852
+ if (classified.kind === "interception") {
853
+ // Inside a slot, so the place is right. What is left to refuse is a
854
+ // spelling nothing reads and a climb past the router root, and the
855
+ // depth is what the directories above it really contribute, climbs
856
+ // applied.
857
+ const depthHere = routeFromSegments(currentSegments)
858
+ .path.split("/")
859
+ .filter((part) => part !== "").length;
860
+ const refused = unsupportedSegmentReason(entry.name) ?? climbReason(entry.name, depthHere);
861
+ if (refused != null) {
862
+ throw new Error(`${path.join(current, entry.name)}: ${refused}`);
863
+ }
678
864
  }
679
865
  walkSlot(
680
866
  path.join(current, entry.name),
681
867
  [...currentSegments, entry.name],
682
868
  nextLayouts,
869
+ nextLoading,
870
+ nextTemplates,
871
+ nextErrorBoundary,
683
872
  false,
873
+ intercepting || classified.kind === "interception",
684
874
  currentDepth + 1,
685
875
  );
686
876
  }
687
877
  };
688
878
 
689
- walkSlot(directory, segments, [], true, depth + 1);
879
+ // The slot's own directory is in the segments from here down. It adds nothing
880
+ // to a path, and it is how `routeFromSegments` knows that an interception
881
+ // below it is inside a slot.
882
+ walkSlot(
883
+ directory,
884
+ [...segments, directoryName],
885
+ [],
886
+ [],
887
+ [],
888
+ defaultError == null ? null : { above: 0, module: defaultError },
889
+ true,
890
+ false,
891
+ depth + 1,
892
+ );
690
893
 
691
894
  const byPath = (a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
692
895
  routes.sort(byPath);
896
+ intercepts.sort(byPath);
693
897
  return {
694
898
  name,
695
899
  above,
696
900
  defaultPage,
697
901
  defaultMdx: defaultPage != null && defaultPage.endsWith(".mdx"),
902
+ defaultErrorBoundary: defaultError == null ? null : { above: 0, module: defaultError },
698
903
  routes,
904
+ intercepts,
699
905
  };
700
906
  }
701
907
 
@@ -740,6 +946,10 @@ function unsupportedTemplateFileReason(fileName) {
740
946
  );
741
947
  }
742
948
 
949
+ function unsupportedSlotBoundaryRole(fileName) {
950
+ return UNSUPPORTED_SLOT_BOUNDARY_FILES[fileName] ?? null;
951
+ }
952
+
743
953
  /**
744
954
  * What one directory name means to the route path.
745
955
  *
@@ -774,10 +984,12 @@ export function classifyRouteSegment(segment) {
774
984
  /**
775
985
  * The `(.)`-style prefix of `segment` and the route after it, or `null`.
776
986
  *
777
- * One or more of `(.)`, `(..)` and `(...)` — every marker Next.js defines;
778
- * `(..)(..)` is two of them rather than a fourth — followed by something for
779
- * them to intercept. A marker with nothing after it names no route and is the
780
- * `(group)` it has always been.
987
+ * One or more parenthesised runs of dots, followed by something for them to
988
+ * intercept. Which runs *mean* anything is {@link interceptionClimb}'s question
989
+ * and deliberately not this one: `(....)photo` is the shape of an interception
990
+ * written by somebody who miscounted, and reading it as a literal URL segment is
991
+ * how the miscount becomes a page at `/(....)photo`. A marker with nothing after
992
+ * it names no route and is the `(group)` it has always been.
781
993
  */
782
994
  function interceptionMarker(segment) {
783
995
  let consumed = 0;
@@ -785,7 +997,7 @@ function interceptionMarker(segment) {
785
997
  const close = segment.indexOf(")", consumed);
786
998
  if (close === -1) break;
787
999
  const inner = segment.slice(consumed + 1, close);
788
- if (inner.length === 0 || inner.length > 3 || /[^.]/.test(inner)) break;
1000
+ if (inner.length === 0 || /[^.]/.test(inner)) break;
789
1001
  consumed = close + 1;
790
1002
  }
791
1003
  if (consumed === 0 || consumed === segment.length) return null;
@@ -793,28 +1005,124 @@ function interceptionMarker(segment) {
793
1005
  }
794
1006
 
795
1007
  /**
796
- * Why uf refuses a directory named `segment`, or `null` when it serves it.
1008
+ * How far a marker climbs, in URL segments: a number for `(.)` and `(..)`
1009
+ * repeated, `"root"` for `(...)`, and `null` for a marker uf does not read.
1010
+ *
1011
+ * Mirrors `uf_router::interception_climb`. `(.)` and `(...)` only as the whole
1012
+ * marker, because each already says where the climb ends; `(..)` as many times
1013
+ * as there are levels to climb.
1014
+ *
1015
+ * @param {string} marker
1016
+ * @returns {number | "root" | null}
1017
+ */
1018
+ export function interceptionClimb(marker) {
1019
+ const runs = marker.match(/\(\.+\)/g) ?? [];
1020
+ if (runs.length === 0 || runs.join("") !== marker) return null;
1021
+ if (runs.length === 1 && runs[0] === "(.)") return 0;
1022
+ if (runs.length === 1 && runs[0] === "(...)") return "root";
1023
+ return runs.every((run) => run === "(..)") ? runs.length : null;
1024
+ }
1025
+
1026
+ /**
1027
+ * The interception a classified directory name is, when it is one uf reads: a
1028
+ * marker that climbs, with a URL segment after it to stand in for.
1029
+ *
1030
+ * @returns {{climb: number | "root", route: {kind: string, name: string}} | null}
1031
+ */
1032
+ function readInterception(classified) {
1033
+ if (classified.kind !== "interception") return null;
1034
+ const climb = interceptionClimb(classified.marker);
1035
+ const route = classifyRouteSegment(classified.route);
1036
+ if (climb == null) return null;
1037
+ if (route.kind !== "literal" && route.kind !== "param" && route.kind !== "catchAll") {
1038
+ return null;
1039
+ }
1040
+ return { climb, route };
1041
+ }
1042
+
1043
+ /**
1044
+ * Why uf refuses a directory named `segment` wherever it is, or `null` when it
1045
+ * serves it somewhere.
797
1046
  *
798
1047
  * The message is this router's own rather than `uf_router`'s, because the two
799
1048
  * are reached differently: the Rust one fails `uf build` and `uf dev` through
800
1049
  * the route manifest, and this one fails a project driving Vite itself. Both
801
- * say the same two things — which feature the spelling belongs to, and that it
802
- * is refused rather than served as a URL.
1050
+ * say the same things — what is wrong with the spelling, and that it is refused
1051
+ * rather than served as a URL.
1052
+ *
1053
+ * A correctly spelled interception has two more refusals, about where it is
1054
+ * rather than how it is written: {@link outsideSlotReason} and `climbReason`.
803
1055
  */
804
1056
  export function unsupportedSegmentReason(segment) {
805
1057
  const classified = classifyRouteSegment(segment);
806
- if (classified.kind === "interception") {
1058
+ if (classified.kind !== "interception") return null;
1059
+ const { marker, route } = classified;
1060
+ if (interceptionClimb(marker) == null) {
1061
+ return (
1062
+ `\`${segment}\` is spelled like an intercepting route and \`${marker}\` is not a marker uf ` +
1063
+ "reads. The markers are `(.)` for the level the directory is at, `(..)` for one above it — " +
1064
+ "repeated for each further level — and `(...)` for the router root. It is refused rather " +
1065
+ `than served as the URL segment \`/${segment}\`, which is what it used to become. Spell the ` +
1066
+ "marker as one of those and put the directory inside a `@slot`, or rename it to the literal " +
1067
+ `segment \`${route}\`. https://github.com/ubugeeei-prod/uf/issues/267`
1068
+ );
1069
+ }
1070
+ if (readInterception(classified) == null) {
807
1071
  return (
808
- `\`${segment}\` is an intercepting route, and uf does not have interception — a navigation ` +
809
- "carries where it is going and not where it came from, so nothing here could match " +
810
- `\`${classified.route}\`. It is refused rather than served as the URL segment ` +
811
- `\`/${segment}\`, which is what it used to become. Move the route to the path it belongs ` +
812
- "at, or rename the directory. https://github.com/ubugeeei-prod/uf/issues/267"
1072
+ `\`${segment}\` is spelled like an intercepting route, and \`${route}\` after the marker is ` +
1073
+ "not a URL segment, so there is no path for it to intercept: an interception names the " +
1074
+ `segment it stands in for, the way \`${marker}photo\` and \`${marker}[id]\` do. It is ` +
1075
+ `refused rather than served as the URL segment \`/${segment}\`, which is what it used to ` +
1076
+ "become. Put a segment name after the marker, or rename the directory. " +
1077
+ "https://github.com/ubugeeei-prod/uf/issues/267"
813
1078
  );
814
1079
  }
815
1080
  return null;
816
1081
  }
817
1082
 
1083
+ /**
1084
+ * Why uf refuses the intercepting route `segment` outside a `@slot`, where it
1085
+ * would serve it inside one; `null` for any other directory name.
1086
+ *
1087
+ * A sentence of its own rather than another case of
1088
+ * {@link unsupportedSegmentReason}, because this one is spelled correctly and
1089
+ * placed wrongly, and telling its author to rename it would be wrong.
1090
+ */
1091
+ export function outsideSlotReason(segment) {
1092
+ const classified = classifyRouteSegment(segment);
1093
+ if (readInterception(classified) == null) return null;
1094
+ return (
1095
+ `\`${segment}\` is an intercepting route, and an intercepting route renders into a \`@slot\`: ` +
1096
+ "it is what a client navigation shows in a named place instead of the page its URL names, " +
1097
+ "and outside a slot there is no named place for it to show in. It is refused rather than " +
1098
+ `served as the URL segment \`/${segment}\`, which is what it used to become. Move it inside a ` +
1099
+ "slot directory beside the layout that renders the slot, or rename the directory to the " +
1100
+ `literal segment \`${classified.route}\`. https://github.com/ubugeeei-prod/uf/issues/267`
1101
+ );
1102
+ }
1103
+
1104
+ /**
1105
+ * Why an interception `depth` URL segments below the router root climbs past
1106
+ * it, or `null` when it does not. Mirrors `RouteSegment::climb_reason`.
1107
+ */
1108
+ function climbReason(segment, depth) {
1109
+ const read = readInterception(classifyRouteSegment(segment));
1110
+ if (read == null || read.climb === "root" || read.climb <= depth) return null;
1111
+ const climbs = read.climb === 1 ? "one level" : `${read.climb} levels`;
1112
+ const sits =
1113
+ depth === 0
1114
+ ? "at the router root"
1115
+ : depth === 1
1116
+ ? "one level below it"
1117
+ : `${depth} levels below it`;
1118
+ return (
1119
+ `\`${segment}\` climbs ${climbs} from the directory it is in, which is ${sits}, so the URL it ` +
1120
+ "intercepts would be above the router root, and there is no such URL. It is refused rather " +
1121
+ "than read as a climb to the root. Remove a `(..)`, or write `(...)` to intercept from the " +
1122
+ "router root. https://github.com/ubugeeei-prod/uf/issues/267"
1123
+ );
1124
+ }
1125
+
818
1126
  /**
819
1127
  * Turn directory segments into a route path and its parameters.
820
1128
  *
@@ -822,29 +1130,48 @@ export function unsupportedSegmentReason(segment) {
822
1130
  * captures one segment, and `[...name]` captures the rest of the path. A
823
1131
  * `@slot` contributes nothing either — it is a named place a route renders
824
1132
  * into, matched against the URL of the segment that declares it — so a slot's
825
- * pages are matched against ordinary paths and add none of their own. An
826
- * interception never reaches here: {@link scanRoutes} refuses the directory
827
- * before it walks into it.
1133
+ * pages are matched against ordinary paths and add none of their own.
1134
+ *
1135
+ * An intercepting route is where "one directory, one segment" stops holding.
1136
+ * Inside a slot, `(..)photo` takes a segment *away* before it adds its own, so
1137
+ * `["feed", "@modal", "(..)photo", "[id]"]` is `/photo/:id`: the URL the
1138
+ * interception stands in for. That is `uf_router`'s `path_segments`, spelled
1139
+ * again. Wherever an interception cannot be — outside a slot, climbing past the
1140
+ * root, written so nothing reads it — this throws the refusal
1141
+ * {@link scanRoutes} would give the directory rather than build a URL from it.
828
1142
  */
829
1143
  export function routeFromSegments(segments) {
830
- const params = [];
831
- const out = [];
1144
+ let out = [];
1145
+ let insideSlot = false;
832
1146
  for (const segment of segments) {
833
1147
  const classified = classifyRouteSegment(segment);
834
- if (classified.kind === "group" || classified.kind === "slot") continue;
835
- if (classified.kind === "catchAll") {
836
- params.push({ name: classified.name, catchAll: true });
837
- out.push(`:${classified.name}*`);
1148
+ if (classified.kind === "group") continue;
1149
+ if (classified.kind === "slot") {
1150
+ insideSlot = true;
838
1151
  continue;
839
1152
  }
840
- if (classified.kind === "param") {
841
- params.push({ name: classified.name, catchAll: false });
842
- out.push(`:${classified.name}`);
843
- continue;
1153
+ let named = classified;
1154
+ if (classified.kind === "interception") {
1155
+ const refused =
1156
+ unsupportedSegmentReason(segment) ??
1157
+ (insideSlot ? climbReason(segment, out.length) : outsideSlotReason(segment));
1158
+ if (refused != null) {
1159
+ throw new Error(refused);
1160
+ }
1161
+ const read = readInterception(classified);
1162
+ out = read.climb === "root" ? [] : out.slice(0, out.length - read.climb);
1163
+ named = read.route;
1164
+ }
1165
+ if (named.kind === "catchAll") {
1166
+ out.push({ spelling: `:${named.name}*`, param: { name: named.name, catchAll: true } });
1167
+ } else if (named.kind === "param") {
1168
+ out.push({ spelling: `:${named.name}`, param: { name: named.name, catchAll: false } });
1169
+ } else {
1170
+ out.push({ spelling: named.name, param: null });
844
1171
  }
845
- out.push(segment);
846
1172
  }
847
- const routePath = out.length === 0 ? "/" : `/${out.join("/")}`;
1173
+ const routePath = out.length === 0 ? "/" : `/${out.map((entry) => entry.spelling).join("/")}`;
1174
+ const params = out.flatMap((entry) => (entry.param == null ? [] : [entry.param]));
848
1175
  return { path: routePath, pattern: routePath.replace(/:(\w+)\*/g, "*$1"), params };
849
1176
  }
850
1177
 
@@ -1020,44 +1347,75 @@ export function routesModuleSource(table, options = {}) {
1020
1347
  const slotIds = new Map();
1021
1348
  const slotDefinitions = [];
1022
1349
  const slotFiles = new Set();
1023
- const slotId = (slot) => {
1024
- let id = slotIds.get(slot);
1025
- if (id !== undefined) {
1026
- return id;
1027
- }
1028
- const routes = slot.routes.map((route) => {
1029
- slotFiles.add(route.page);
1030
- for (const file of route.layouts) slotFiles.add(file);
1031
- const nested = route.slots.map(slotId);
1032
- return ` {
1350
+ const slotErrorBoundary = (boundary) =>
1351
+ boundary == null
1352
+ ? "null"
1353
+ : `{ above: ${boundary.above}, module: () => import(${JSON.stringify(boundary.module)}) }`;
1354
+ // One route a slot may render, as source. The same shape for a slot's own
1355
+ // routes and for its interceptions, because an intercepting page is composed
1356
+ // exactly the way every other page in the slot is.
1357
+ const slotRoute = (route) => {
1358
+ slotFiles.add(route.page);
1359
+ for (const file of route.layouts) slotFiles.add(file);
1360
+ for (const entry of route.loading ?? []) slotFiles.add(entry.module);
1361
+ const nested = route.slots.map(slotId);
1362
+ if (route.errorBoundary != null) slotFiles.add(route.errorBoundary.module);
1363
+ return ` {
1033
1364
  path: ${JSON.stringify(route.path)},
1034
1365
  params: ${JSON.stringify(route.params)},
1035
1366
  mdx: ${route.mdx},
1036
1367
  file: ${JSON.stringify(displayFile(route.page))},
1037
1368
  page: () => import(${JSON.stringify(route.page)}),
1038
1369
  layouts: [${route.layouts.map(layoutId).join(", ")}],
1370
+ loading: [${(route.loading ?? [])
1371
+ .map((boundary) => `{ above: ${boundary.above}, module: ${loadingId(boundary.module)} }`)
1372
+ .join(", ")}],
1373
+ templates: [${(route.templates ?? [])
1374
+ .map((entry) => `{ above: ${entry.above}, module: ${templateId(entry.module)} }`)
1375
+ .join(", ")}],
1376
+ errorBoundary: ${slotErrorBoundary(route.errorBoundary ?? null)},
1039
1377
  slots: [${nested.join(", ")}],
1040
1378
  }`;
1041
- });
1379
+ };
1380
+ const slotId = (slot) => {
1381
+ let id = slotIds.get(slot);
1382
+ if (id !== undefined) {
1383
+ return id;
1384
+ }
1385
+ const routes = slot.routes.map(slotRoute);
1386
+ const intercepts = (slot.intercepts ?? []).map(slotRoute);
1042
1387
  // After the routes, so a nested slot's `const` is already emitted.
1043
1388
  id = `slot${slotIds.size}`;
1044
1389
  slotIds.set(slot, id);
1045
1390
  if (slot.defaultPage != null) {
1046
1391
  slotFiles.add(slot.defaultPage);
1047
1392
  }
1393
+ if (slot.defaultErrorBoundary != null) {
1394
+ slotFiles.add(slot.defaultErrorBoundary.module);
1395
+ }
1048
1396
  const fallback =
1049
1397
  slot.defaultPage == null
1050
1398
  ? " defaultPage: null,"
1051
1399
  : ` defaultPage: () => import(${JSON.stringify(slot.defaultPage)}),
1052
1400
  defaultFile: ${JSON.stringify(displayFile(slot.defaultPage))},`;
1401
+ // Only when there is one, so a slot that intercepts nothing is emitted byte
1402
+ // for byte the way it was before interception existed.
1403
+ const intercepting =
1404
+ intercepts.length === 0
1405
+ ? ""
1406
+ : `
1407
+ intercepts: [
1408
+ ${intercepts.join(",\n")}
1409
+ ],`;
1053
1410
  slotDefinitions.push(`const ${id} = {
1054
1411
  name: ${JSON.stringify(slot.name)},
1055
1412
  above: ${slot.above},
1056
1413
  ${fallback}
1057
1414
  defaultMdx: ${slot.defaultMdx},
1415
+ defaultErrorBoundary: ${slotErrorBoundary(slot.defaultErrorBoundary ?? null)},
1058
1416
  routes: [
1059
1417
  ${routes.join(",\n")}
1060
- ],
1418
+ ],${intercepting}
1061
1419
  };`);
1062
1420
  return id;
1063
1421
  };
@@ -1221,8 +1579,18 @@ function slotModuleFiles(slots) {
1221
1579
  const files = [];
1222
1580
  for (const slot of slots) {
1223
1581
  if (slot.defaultPage != null) files.push(slot.defaultPage);
1224
- for (const route of slot.routes) {
1225
- files.push(route.page, ...route.layouts, ...slotModuleFiles(route.slots));
1582
+ if (slot.defaultErrorBoundary != null) files.push(slot.defaultErrorBoundary.module);
1583
+ // An interception's modules with the slot's own: its stylesheet is part of
1584
+ // what the page looks like when a navigation opens it over this route.
1585
+ for (const route of [...slot.routes, ...(slot.intercepts ?? [])]) {
1586
+ files.push(
1587
+ route.page,
1588
+ ...route.layouts,
1589
+ ...(route.loading ?? []).map((it) => it.module),
1590
+ ...(route.templates ?? []).map((it) => it.module),
1591
+ ...(route.errorBoundary == null ? [] : [route.errorBoundary.module]),
1592
+ ...slotModuleFiles(route.slots),
1593
+ );
1226
1594
  }
1227
1595
  }
1228
1596
  return files;
@@ -1330,14 +1698,15 @@ ${mount}({ App, routes, notFound, errors${strictMode}${navigation} });
1330
1698
  * has nothing to do with a document that is no route's.
1331
1699
  *
1332
1700
  * `beginRequest` is the fourth, and it is re-exported rather than imported by
1333
- * the host for a reason that is easy to get wrong: `@uniflowed/server` keeps
1334
- * the request in an `AsyncLocalStorage` held by *its module*, and a bundled
1335
- * application has its own copy of that module inlined. A host that imported
1336
- * `beginRequest` from its own `node_modules` would establish a request in a
1337
- * second storage, and every `cookies()` in the application would still be
1338
- * outside one. So the bundle hands the host the entry point that belongs to
1339
- * the bundle. `uf preview`, `uf start`, `uf dev` and the compiled binary all
1340
- * take it from here; see ubugeeei-prod/uf#389.
1701
+ * the host for a reason that is easy to get wrong: `@uniflowed/server` shares
1702
+ * its request store between copies of one *release* of itself, and a bundled
1703
+ * application has its own copy inlined. A host that imported `beginRequest`
1704
+ * from its own `node_modules` could be holding another release, would
1705
+ * establish a request in a store the application never reads, and every
1706
+ * `cookies()` in the application would still be outside one. So the bundle
1707
+ * hands the host the entry point that belongs to the bundle. `uf preview`,
1708
+ * `uf start`, `uf dev` and the compiled binary all take it from here; see
1709
+ * ubugeeei-prod/uf#389.
1341
1710
  *
1342
1711
  * Through `@uniflowed/router/server` rather than `@uniflowed/server/host`,
1343
1712
  * because this source is resolved from the *project's* directory and a project
package/internal/rsc.js CHANGED
@@ -187,7 +187,13 @@ export function clientRouteFilter(manifest, root, boundaries = {}) {
187
187
  *
188
188
  * @param {ReadonlyArray<{
189
189
  * defaultPage: ?string,
190
- * routes: ReadonlyArray<{page: string, layouts: ReadonlyArray<string>, slots: ReadonlyArray<*>}>,
190
+ * routes: ReadonlyArray<{
191
+ * page: string,
192
+ * layouts: ReadonlyArray<string>,
193
+ * loading?: ReadonlyArray<{module: string}>,
194
+ * templates?: ReadonlyArray<{module: string}>,
195
+ * slots: ReadonlyArray<*>,
196
+ * }>,
191
197
  * @param {(file: ?string) => boolean} needed
192
198
  * @returns {(slot: *) => boolean}
193
199
  */
@@ -196,13 +202,25 @@ function slotPredicate(needed) {
196
202
  const needsSlot = (slot) => {
197
203
  const cached = cache.get(slot);
198
204
  if (cached !== undefined) return cached;
199
- let answer = false;
205
+ // A slot that intercepts anything needs the client router on every page it
206
+ // is rendered on, whatever its modules are. An intercepting route renders
207
+ // only for a client navigation that starts on such a page, and a page
208
+ // served as a document with no router hydrated on it is a page no
209
+ // navigation starts from — so dropping it would leave the interception a
210
+ // file nothing ever renders, which is the silence #267 is about.
211
+ let answer = (slot.intercepts ?? []).length > 0;
200
212
  if (slot.defaultPage != null && needed(slot.defaultPage)) {
201
213
  answer = true;
202
214
  }
203
215
  for (const route of slot.routes) {
204
216
  if (answer) break;
205
- if (needed(route.page) || route.layouts.some(needed) || route.slots.some(needsSlot)) {
217
+ if (
218
+ needed(route.page) ||
219
+ route.layouts.some(needed) ||
220
+ (route.loading ?? []).some((entry) => needed(entry.module)) ||
221
+ (route.templates ?? []).some((entry) => needed(entry.module)) ||
222
+ route.slots.some(needsSlot)
223
+ ) {
206
224
  answer = true;
207
225
  }
208
226
  }
package/internal/serve.js CHANGED
@@ -64,12 +64,12 @@
64
64
  // settles it on the line after the last byte.
65
65
  //
66
66
  // It has to be the *entry's* `beginRequest` rather than one imported here: the
67
- // request lives in an `AsyncLocalStorage` belonging to one copy of
68
- // `@uniflowed/server`, and the copy that matters is the one inside the
69
- // application bundle. A host that resolved its own would begin a request the
70
- // application cannot see, and nothing would fail loudly — the guard would run,
71
- // the page would render, and every `cookies()` in it would throw as though no
72
- // host had run at all. See ubugeeei-prod/uf#389.
67
+ // request store is shared by every copy of one release of `@uniflowed/server`,
68
+ // and the release that matters is the one inside the application bundle. A
69
+ // host that resolved its own could be holding another release, would begin a
70
+ // request the application cannot see, and nothing would fail loudly — the
71
+ // guard would run, the page would render, and every `cookies()` in it would
72
+ // throw as though no host had run at all. See ubugeeei-prod/uf#389.
73
73
 
74
74
  import { readFile, stat } from "node:fs/promises";
75
75
  import path from "node:path";
@@ -348,10 +348,10 @@ export function assetsFromManifest(manifest) {
348
348
  * its callback whether the render threw or not, and `drainDeferred` already
349
349
  * reports a failing task rather than propagating it.
350
350
  *
351
- * `entry.beginRequest` and not an import: the request lives in an
352
- * `AsyncLocalStorage` belonging to one copy of `@uniflowed/server`, and the
353
- * copy that matters is the one inside the application bundle. See
354
- * `serverModuleSource` in `./routes.js`.
351
+ * `entry.beginRequest` and not an import: the request store is shared by every
352
+ * copy of one release of `@uniflowed/server`, and the release that matters is
353
+ * the one inside the application bundle. See `serverModuleSource` in
354
+ * `./routes.js`.
355
355
  *
356
356
  * The one case this cannot be exact about is a request uf hands back rather
357
357
  * than answers: a caller whose `catch` is `next(error)` gives the response to
@@ -385,10 +385,10 @@ export async function withRequest(entry, request, body) {
385
385
  * `finally` here. A caller that always answers should use [`withRequest`] and
386
386
  * not think about it.
387
387
  *
388
- * `entry.beginRequest` and not an import: the request lives in an
389
- * `AsyncLocalStorage` belonging to one copy of `@uniflowed/server`, and the
390
- * copy that matters is the one inside the application bundle. See
391
- * `serverModuleSource` in `./routes.js`.
388
+ * `entry.beginRequest` and not an import: the request store is shared by every
389
+ * copy of one release of `@uniflowed/server`, and the release that matters is
390
+ * the one inside the application bundle. See `serverModuleSource` in
391
+ * `./routes.js`.
392
392
  *
393
393
  * What this host can do is put on the request the way `createFetchHandler`
394
394
  * puts it on the one it owns. `uf dev` and `uf build --compile` reach a route
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/vite",
3
- "version": "0.0.0-alpha.32",
3
+ "version": "0.0.0-alpha.34",
4
4
  "description": "Vite, driven by uf.config.js: every Flow module through `uf transform`, MDX, the file-system router and static rendering as Vite plugins.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,10 +34,10 @@
34
34
  "dependencies": {
35
35
  "@mdx-js/rollup": "^3.1.1",
36
36
  "@shikijs/rehype": "^3.23.0",
37
- "@uniflowed/host": "0.0.0-alpha.32",
38
- "@uniflowed/router": "0.0.0-alpha.32",
39
- "@uniflowed/server": "0.0.0-alpha.32",
40
- "@uniflowed/validator": "0.0.0-alpha.32",
37
+ "@uniflowed/host": "0.0.0-alpha.34",
38
+ "@uniflowed/router": "0.0.0-alpha.34",
39
+ "@uniflowed/server": "0.0.0-alpha.34",
40
+ "@uniflowed/validator": "0.0.0-alpha.34",
41
41
  "rehype-slug": "^6.0.0",
42
42
  "remark-frontmatter": "^5.0.0",
43
43
  "remark-gfm": "^4.0.1",