@uniflowed/vite 0.0.0-alpha.31 → 0.0.0-alpha.33

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/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));
@@ -56,6 +56,7 @@ export const UNSUPPORTED_SEGMENTS = Object.freeze([
56
56
  "(..)photo",
57
57
  "(...)photo",
58
58
  "(..)(..)photo",
59
+ "(..)(..)(..)photo",
59
60
  ]);
60
61
 
61
62
  /**
@@ -77,6 +78,16 @@ export const LAYOUT_PROP_NAMES = Object.freeze(["children", "params"]);
77
78
  /** Template spellings that look conventional elsewhere but uf will not open. */
78
79
  const UNSUPPORTED_TEMPLATE_FILES = Object.freeze(["template.js", "_uf.template.js"]);
79
80
 
81
+ /** Boundary spellings a slot might look for, but uf does not open. */
82
+ const UNSUPPORTED_SLOT_BOUNDARY_FILES = Object.freeze({
83
+ "error.js": "error",
84
+ "loading.js": "loading",
85
+ "not-found.js": "not-found",
86
+ "_uf.error.js": "error",
87
+ "_uf.loading.js": "loading",
88
+ "_uf.not-found.js": "not-found",
89
+ });
90
+
80
91
  /** Extensions a page or layout may use; `.mdx` is a page written as content. */
81
92
  const PAGE_EXTENSIONS = [".js", ".jsx", ".mdx"];
82
93
  const MODULE_EXTENSIONS = [".js", ".jsx"];
@@ -177,18 +188,21 @@ const MAX_DEPTH = 32;
177
188
  * @property {number} above how many of the route's layouts are outside it
178
189
  * @property {?string} defaultPage absolute path of `$default.*`, or `null`
179
190
  * @property {boolean} defaultMdx whether that default is MDX content
191
+ * @property {?{above: number, module: string}} defaultErrorBoundary the
192
+ * `$error.js` boundary that catches the default page in the browser
180
193
  * @property {ReadonlyArray<SlotRoute>} routes what the slot may render, by URL
181
194
  */
182
195
 
183
196
  /**
184
197
  * One page inside a slot.
185
198
  *
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
199
+ * A `Route` without the request-level parts a slot does not have: no handler
200
+ * and no not-found boundary of its own. Loading boundaries, templates and
201
+ * browser render error boundaries are the pieces that compose like layouts, so
202
+ * they are carried below. Per-slot not-found boundaries are the part of
203
+ * parallel routes uf has not built — see
190
204
  * https://github.com/ubugeeei-prod/uf/issues/267 — and {@link scanRoutes}
191
- * refuses the files rather than leaving them unopened.
205
+ * refuses those files rather than leaving them unopened.
192
206
  *
193
207
  * `layouts` are the layouts *inside* the slot, root first; the ones above it
194
208
  * are already rendering, since the slot renders into one of them.
@@ -198,6 +212,12 @@ const MAX_DEPTH = 32;
198
212
  * @property {ReadonlyArray<{name: string, catchAll: boolean}>} params
199
213
  * @property {string} page absolute path of the page module
200
214
  * @property {ReadonlyArray<string>} layouts absolute paths, slot root first
215
+ * @property {ReadonlyArray<{above: number, module: string}>} loading the
216
+ * `$loading.js` fallbacks inside the slot, root first
217
+ * @property {ReadonlyArray<{above: number, module: string}>} templates the
218
+ * `$template.js` wrappers inside the slot, root first
219
+ * @property {?{above: number, module: string}} errorBoundary the `$error.js`
220
+ * boundary inside the slot, if one is in scope
201
221
  * @property {ReadonlyArray<Slot>} slots slots declared inside this slot
202
222
  * @property {boolean} mdx whether the page is MDX content
203
223
  */
@@ -298,9 +318,9 @@ const MAX_DEPTH = 32;
298
318
  * A `@slot` directory is a parallel route and is scanned; see {@link Slot}. It
299
319
  * throws for the three ways one can be written without being renderable: a
300
320
  * 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.
321
+ * directly inside a slot, a not-found boundary or handler inside a slot, and
322
+ * boundary-like files with names uf does not open. Each is a file the router
323
+ * would otherwise never open, which is the failure #267 is about.
304
324
  *
305
325
  * @param {string} appRoot absolute path of the router root (`app/`)
306
326
  * @param {{target?: "web" | "native" | "ios" | "android"}} [options]
@@ -581,13 +601,27 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, targe
581
601
 
582
602
  const routes = [];
583
603
  const defaultPage = findModule(directory, RESERVED.default, PAGE_EXTENSIONS, target);
584
-
585
- const walkSlot = (current, currentSegments, layouts, atSlotRoot, currentDepth) => {
604
+ const defaultError = findModule(directory, RESERVED.error, MODULE_EXTENSIONS, target);
605
+
606
+ const walkSlot = (
607
+ current,
608
+ currentSegments,
609
+ layouts,
610
+ loading,
611
+ templates,
612
+ errorBoundary,
613
+ atSlotRoot,
614
+ currentDepth,
615
+ ) => {
586
616
  if (currentDepth > MAX_DEPTH) return;
617
+ const entries = readdirSync(current, { withFileTypes: true }).sort((a, b) =>
618
+ a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
619
+ );
620
+
587
621
  // 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]) {
622
+ // read it rather than by never opening it. Loading and error boundaries
623
+ // compose like layouts, so they are carried below instead of refused here.
624
+ for (const role of [RESERVED.notFound]) {
591
625
  const found =
592
626
  findModule(current, role, MODULE_EXTENSIONS, target) ??
593
627
  findModule(current, role, PAGE_EXTENSIONS, target);
@@ -595,12 +629,24 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, targe
595
629
  throw new Error(
596
630
  `${found}: a \`@slot\` renders a page and the layouts inside the slot, and has no ` +
597
631
  `\`${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 " +
632
+ "per-slot not-found boundaries yet, so this file would never be opened. Put it outside " +
599
633
  `\`${directoryName}\`, where it covers the whole segment. ` +
600
634
  "https://github.com/ubugeeei-prod/uf/issues/267",
601
635
  );
602
636
  }
603
637
  }
638
+ for (const entry of entries) {
639
+ if (!entry.isFile()) continue;
640
+ const role = unsupportedSlotBoundaryRole(entry.name);
641
+ if (role == null) continue;
642
+ const file = path.join(current, entry.name);
643
+ throw new Error(
644
+ `${file}: \`${entry.name}\` looks like a \`${role}\` boundary for a \`@slot\`, but it ` +
645
+ "is not a uf route file there. Use `$loading.js` for slot loading and `$error.js` " +
646
+ "for slot errors; per-slot not-found boundaries are still not implemented. " +
647
+ "https://github.com/ubugeeei-prod/uf/issues/267",
648
+ );
649
+ }
604
650
  for (const role of [RESERVED.route, RESERVED.middleware]) {
605
651
  const found = findModule(current, role, MODULE_EXTENSIONS, target);
606
652
  if (found != null) {
@@ -626,10 +672,18 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, targe
626
672
 
627
673
  const layoutHere = findModule(current, RESERVED.layout, MODULE_EXTENSIONS, target);
628
674
  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
- );
675
+ const loadingHere = findModule(current, RESERVED.loading, MODULE_EXTENSIONS, target);
676
+ const nextLoading = loadingHere
677
+ ? [...loading, { above: nextLayouts.length, module: loadingHere }]
678
+ : loading;
679
+ const templateHere = findModule(current, RESERVED.template, MODULE_EXTENSIONS, target);
680
+ const nextTemplates = templateHere
681
+ ? [...templates, { above: nextLayouts.length, module: templateHere }]
682
+ : templates;
683
+ const errorHere = findModule(current, RESERVED.error, MODULE_EXTENSIONS, target);
684
+ const nextErrorBoundary = errorHere
685
+ ? { above: nextLayouts.length, module: errorHere }
686
+ : errorBoundary;
633
687
 
634
688
  refuseUnsupportedTemplateFiles(current, entries);
635
689
 
@@ -662,6 +716,9 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, targe
662
716
  params,
663
717
  page,
664
718
  layouts: nextLayouts,
719
+ loading: nextLoading,
720
+ templates: nextTemplates,
721
+ errorBoundary: nextErrorBoundary,
665
722
  slots: nestedSlots,
666
723
  mdx: page.endsWith(".mdx"),
667
724
  });
@@ -680,13 +737,25 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, targe
680
737
  path.join(current, entry.name),
681
738
  [...currentSegments, entry.name],
682
739
  nextLayouts,
740
+ nextLoading,
741
+ nextTemplates,
742
+ nextErrorBoundary,
683
743
  false,
684
744
  currentDepth + 1,
685
745
  );
686
746
  }
687
747
  };
688
748
 
689
- walkSlot(directory, segments, [], true, depth + 1);
749
+ walkSlot(
750
+ directory,
751
+ segments,
752
+ [],
753
+ [],
754
+ [],
755
+ defaultError == null ? null : { above: 0, module: defaultError },
756
+ true,
757
+ depth + 1,
758
+ );
690
759
 
691
760
  const byPath = (a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
692
761
  routes.sort(byPath);
@@ -695,6 +764,7 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, targe
695
764
  above,
696
765
  defaultPage,
697
766
  defaultMdx: defaultPage != null && defaultPage.endsWith(".mdx"),
767
+ defaultErrorBoundary: defaultError == null ? null : { above: 0, module: defaultError },
698
768
  routes,
699
769
  };
700
770
  }
@@ -740,6 +810,10 @@ function unsupportedTemplateFileReason(fileName) {
740
810
  );
741
811
  }
742
812
 
813
+ function unsupportedSlotBoundaryRole(fileName) {
814
+ return UNSUPPORTED_SLOT_BOUNDARY_FILES[fileName] ?? null;
815
+ }
816
+
743
817
  /**
744
818
  * What one directory name means to the route path.
745
819
  *
@@ -842,6 +916,9 @@ export function routeFromSegments(segments) {
842
916
  out.push(`:${classified.name}`);
843
917
  continue;
844
918
  }
919
+ if (classified.kind === "interception") {
920
+ throw new Error(unsupportedSegmentReason(segment));
921
+ }
845
922
  out.push(segment);
846
923
  }
847
924
  const routePath = out.length === 0 ? "/" : `/${out.join("/")}`;
@@ -1020,6 +1097,10 @@ export function routesModuleSource(table, options = {}) {
1020
1097
  const slotIds = new Map();
1021
1098
  const slotDefinitions = [];
1022
1099
  const slotFiles = new Set();
1100
+ const slotErrorBoundary = (boundary) =>
1101
+ boundary == null
1102
+ ? "null"
1103
+ : `{ above: ${boundary.above}, module: () => import(${JSON.stringify(boundary.module)}) }`;
1023
1104
  const slotId = (slot) => {
1024
1105
  let id = slotIds.get(slot);
1025
1106
  if (id !== undefined) {
@@ -1028,7 +1109,9 @@ export function routesModuleSource(table, options = {}) {
1028
1109
  const routes = slot.routes.map((route) => {
1029
1110
  slotFiles.add(route.page);
1030
1111
  for (const file of route.layouts) slotFiles.add(file);
1112
+ for (const entry of route.loading ?? []) slotFiles.add(entry.module);
1031
1113
  const nested = route.slots.map(slotId);
1114
+ if (route.errorBoundary != null) slotFiles.add(route.errorBoundary.module);
1032
1115
  return ` {
1033
1116
  path: ${JSON.stringify(route.path)},
1034
1117
  params: ${JSON.stringify(route.params)},
@@ -1036,6 +1119,13 @@ export function routesModuleSource(table, options = {}) {
1036
1119
  file: ${JSON.stringify(displayFile(route.page))},
1037
1120
  page: () => import(${JSON.stringify(route.page)}),
1038
1121
  layouts: [${route.layouts.map(layoutId).join(", ")}],
1122
+ loading: [${(route.loading ?? [])
1123
+ .map((boundary) => `{ above: ${boundary.above}, module: ${loadingId(boundary.module)} }`)
1124
+ .join(", ")}],
1125
+ templates: [${(route.templates ?? [])
1126
+ .map((entry) => `{ above: ${entry.above}, module: ${templateId(entry.module)} }`)
1127
+ .join(", ")}],
1128
+ errorBoundary: ${slotErrorBoundary(route.errorBoundary ?? null)},
1039
1129
  slots: [${nested.join(", ")}],
1040
1130
  }`;
1041
1131
  });
@@ -1045,6 +1135,9 @@ export function routesModuleSource(table, options = {}) {
1045
1135
  if (slot.defaultPage != null) {
1046
1136
  slotFiles.add(slot.defaultPage);
1047
1137
  }
1138
+ if (slot.defaultErrorBoundary != null) {
1139
+ slotFiles.add(slot.defaultErrorBoundary.module);
1140
+ }
1048
1141
  const fallback =
1049
1142
  slot.defaultPage == null
1050
1143
  ? " defaultPage: null,"
@@ -1055,6 +1148,7 @@ export function routesModuleSource(table, options = {}) {
1055
1148
  above: ${slot.above},
1056
1149
  ${fallback}
1057
1150
  defaultMdx: ${slot.defaultMdx},
1151
+ defaultErrorBoundary: ${slotErrorBoundary(slot.defaultErrorBoundary ?? null)},
1058
1152
  routes: [
1059
1153
  ${routes.join(",\n")}
1060
1154
  ],
@@ -1221,8 +1315,16 @@ function slotModuleFiles(slots) {
1221
1315
  const files = [];
1222
1316
  for (const slot of slots) {
1223
1317
  if (slot.defaultPage != null) files.push(slot.defaultPage);
1318
+ if (slot.defaultErrorBoundary != null) files.push(slot.defaultErrorBoundary.module);
1224
1319
  for (const route of slot.routes) {
1225
- files.push(route.page, ...route.layouts, ...slotModuleFiles(route.slots));
1320
+ files.push(
1321
+ route.page,
1322
+ ...route.layouts,
1323
+ ...(route.loading ?? []).map((it) => it.module),
1324
+ ...(route.templates ?? []).map((it) => it.module),
1325
+ ...(route.errorBoundary == null ? [] : [route.errorBoundary.module]),
1326
+ ...slotModuleFiles(route.slots),
1327
+ );
1226
1328
  }
1227
1329
  }
1228
1330
  return files;
package/internal/rsc.js CHANGED
@@ -145,6 +145,7 @@ export function clientRouteFilter(manifest, root, boundaries = {}) {
145
145
 
146
146
  const notFound = boundaries.notFound ?? [];
147
147
  const errors = boundaries.errors ?? [];
148
+ const needsSlot = slotPredicate(needed);
148
149
 
149
150
  return (route) => {
150
151
  if (needed(route.page)) return true;
@@ -156,7 +157,7 @@ export function clientRouteFilter(manifest, root, boundaries = {}) {
156
157
  // interactive part is in a slot would be dropped from the client bundle
157
158
  // and served as a document — rendered correctly and never hydrated, which
158
159
  // is the quietest way a feature can be half-implemented.
159
- if (slotNeeds(route.slots ?? [], needed)) return true;
160
+ if ((route.slots ?? []).some(needsSlot)) return true;
160
161
  // A boundary with no module of its own is the record the scan synthesises
161
162
  // at the router root, and what renders there is the framework's own page —
162
163
  // already in `@uniflowed/router`, reaching nothing this project wrote. It
@@ -186,20 +187,41 @@ export function clientRouteFilter(manifest, root, boundaries = {}) {
186
187
  *
187
188
  * @param {ReadonlyArray<{
188
189
  * defaultPage: ?string,
189
- * routes: ReadonlyArray<{page: string, layouts: ReadonlyArray<string>, slots: ReadonlyArray<*>}>,
190
- * }>} slots
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
198
+ * @returns {(slot: *) => boolean}
192
199
  */
193
- function slotNeeds(slots, needed) {
194
- for (const slot of slots) {
195
- if (slot.defaultPage != null && needed(slot.defaultPage)) return true;
200
+ function slotPredicate(needed) {
201
+ const cache = new WeakMap();
202
+ const needsSlot = (slot) => {
203
+ const cached = cache.get(slot);
204
+ if (cached !== undefined) return cached;
205
+ let answer = false;
206
+ if (slot.defaultPage != null && needed(slot.defaultPage)) {
207
+ answer = true;
208
+ }
196
209
  for (const route of slot.routes) {
197
- if (needed(route.page)) return true;
198
- if (route.layouts.some(needed)) return true;
199
- if (slotNeeds(route.slots, needed)) return true;
210
+ if (answer) break;
211
+ if (
212
+ needed(route.page) ||
213
+ route.layouts.some(needed) ||
214
+ (route.loading ?? []).some((entry) => needed(entry.module)) ||
215
+ (route.templates ?? []).some((entry) => needed(entry.module)) ||
216
+ route.slots.some(needsSlot)
217
+ ) {
218
+ answer = true;
219
+ }
200
220
  }
201
- }
202
- return false;
221
+ cache.set(slot, answer);
222
+ return answer;
223
+ };
224
+ return needsSlot;
203
225
  }
204
226
 
205
227
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/vite",
3
- "version": "0.0.0-alpha.31",
3
+ "version": "0.0.0-alpha.33",
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.31",
38
- "@uniflowed/router": "0.0.0-alpha.31",
39
- "@uniflowed/server": "0.0.0-alpha.31",
40
- "@uniflowed/validator": "0.0.0-alpha.31",
37
+ "@uniflowed/host": "0.0.0-alpha.33",
38
+ "@uniflowed/router": "0.0.0-alpha.33",
39
+ "@uniflowed/server": "0.0.0-alpha.33",
40
+ "@uniflowed/validator": "0.0.0-alpha.33",
41
41
  "rehype-slug": "^6.0.0",
42
42
  "remark-frontmatter": "^5.0.0",
43
43
  "remark-gfm": "^4.0.1",