@uniflowed/router 0.0.0-alpha.4 → 0.0.0-alpha.5

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.
Files changed (2) hide show
  1. package/internal/runtime.js +100 -15
  2. package/package.json +2 -2
@@ -30,10 +30,57 @@ export type RouteParams = { readonly [string]: string | $ReadOnlyArray<string> }
30
30
  /** The query string, as a read-only map. */
31
31
  export type SearchParams = { readonly [string]: string };
32
32
 
33
+ /**
34
+ * A component found in a route module.
35
+ *
36
+ * `React.ComponentType<empty>` is "some React component", and it is a claim
37
+ * rather than a shrug. `ComponentType` is contravariant in its props — Flow's
38
+ * library definition writes it `component(...P)` with `in P` — so `empty` is
39
+ * the *top* of the component types: every component is one, and nothing may be
40
+ * passed to one until a caller has said which props it is passing. That is
41
+ * exactly what is known here. The router finds these by dynamic import, and
42
+ * nobody has told it what a page's props are.
43
+ *
44
+ * It cannot be `React.ComponentType<PageRenderProps>`, the props the router
45
+ * actually passes, because Flow's `component` syntax gives a component *exact*
46
+ * props and a page is free to want none of them. This repository's own pages
47
+ * and layouts are `component NotFound()` and
48
+ * `component Layout(children: React.Node)`, and against the props the router
49
+ * hands them that reads:
50
+ *
51
+ * error[incompatible-type]: property `data`, property `params`, and
52
+ * property `searchParams` are extra in `PageRenderProps` but missing in
53
+ * `props of component NotFound`. Exact objects do not accept extra props.
54
+ *
55
+ * React passing a component a prop it did not declare is allowed and always
56
+ * has been. `renderable` is the one line that says so.
57
+ */
58
+ type RouteComponent = React.ComponentType<empty>;
59
+
60
+ /**
61
+ * The props `RouteView` gives the page it renders.
62
+ *
63
+ * The same three as the public `PageProps` in `../index.js`, at the arguments
64
+ * the runtime instantiates it with: the runtime knows the parameters as
65
+ * strings and the loader's data as `mixed`, and a page narrows both by
66
+ * annotating its own props.
67
+ */
68
+ type PageRenderProps = {|
69
+ readonly params: RouteParams,
70
+ readonly searchParams: SearchParams,
71
+ readonly data: mixed,
72
+ |};
73
+
74
+ /** The props `RouteView` gives each layout, outermost first. */
75
+ type LayoutRenderProps = {|
76
+ readonly params: RouteParams,
77
+ readonly children: React.Node,
78
+ |};
79
+
33
80
  /** What a page module may export. The component is `default` or `Page`. */
34
81
  export type PageModule = {
35
- readonly default?: React.ComponentType<any>,
36
- readonly Page?: React.ComponentType<any>,
82
+ readonly default?: RouteComponent,
83
+ readonly Page?: RouteComponent,
37
84
  readonly loader?: (args: LoaderArgs) => mixed | Promise<mixed>,
38
85
  readonly metadata?: Metadata,
39
86
  readonly generateMetadata?: (args: MetadataArgs) => Metadata | Promise<Metadata>,
@@ -46,8 +93,8 @@ export type PageModule = {
46
93
 
47
94
  /** What a layout module may export. The component is `default` or `Layout`. */
48
95
  export type LayoutModule = {
49
- readonly default?: React.ComponentType<any>,
50
- readonly Layout?: React.ComponentType<any>,
96
+ readonly default?: RouteComponent,
97
+ readonly Layout?: RouteComponent,
51
98
  readonly metadata?: Metadata,
52
99
  ...
53
100
  };
@@ -631,10 +678,28 @@ export hook useRouter(): Router {
631
678
  return useRouterState().router;
632
679
  }
633
680
 
634
- /** The current page's loader data. */
635
- export hook useLoaderData<T>(): T {
636
- // $FlowFixMe[unclear-type] loader data is typed by the page that declares the loader.
637
- return useRouterState().resolved.data as any;
681
+ /**
682
+ * The current page's loader data.
683
+ *
684
+ * `mixed`, so the page that reads it says what it is and the checker watches
685
+ * it do so. This was `useLoaderData<T>(): T`, which looks like inference and
686
+ * is a cast a caller writes at a distance: `useLoaderData<Post>()` asserted
687
+ * that a loader three files away returned a `Post` and nothing anywhere
688
+ * checked it, so a loader that changed shape produced a `Post`-shaped
689
+ * `undefined` at the first property read rather than an error where the shape
690
+ * was decided.
691
+ *
692
+ * Narrowing is a line at the top of the page — `if (typeof data !== "object"
693
+ * || data == null) { … }`, or the page's own validator schema, which is what
694
+ * `@uniflowed/validator` is for at exactly this boundary.
695
+ *
696
+ * The type that would need no narrowing is a *generated* one: the route table
697
+ * already produces `RoutePath` and `RouteParams` from the `app/` directory
698
+ * (`crates/uf_router/src/lib.rs`), and a loader's return type belongs in the
699
+ * same file, keyed by route. Until it is there, this says what is true.
700
+ */
701
+ export hook useLoaderData(): mixed {
702
+ return useRouterState().resolved.data;
638
703
  }
639
704
 
640
705
  /**
@@ -663,25 +728,47 @@ export component RouteView() {
663
728
  * The component a page module renders: its default export, or the named
664
729
  * `Page` that `uf create` scaffolds. An MDX page always has a default export.
665
730
  */
666
- function pageComponent(module: PageModule): React.ComponentType<any> {
731
+ function pageComponent(module: PageModule): React.ComponentType<PageRenderProps> {
667
732
  const component = module.default ?? module.Page;
668
733
  if (component == null) {
669
734
  throw new Error(
670
735
  "@uniflowed/router: a page module must export a component as `default` or `Page`",
671
736
  );
672
737
  }
673
- return component;
738
+ return renderable(component);
674
739
  }
675
740
 
676
741
  /** The component a layout module renders: `default`, or the named `Layout`. */
677
- function layoutComponent(module: LayoutModule): React.ComponentType<any> {
742
+ function layoutComponent(module: LayoutModule): React.ComponentType<LayoutRenderProps> {
678
743
  const component = module.default ?? module.Layout;
679
744
  if (component == null) {
680
745
  throw new Error(
681
746
  "@uniflowed/router: a layout module must export a component as `default` or `Layout`",
682
747
  );
683
748
  }
684
- return component;
749
+ return renderable(component);
750
+ }
751
+
752
+ /**
753
+ * A route module's component, as the router is about to render it.
754
+ *
755
+ * # The one cast in this file, and why it is here rather than in six places
756
+ *
757
+ * A `RouteComponent` is a component about whose props nothing was claimed, and
758
+ * `RouteView` is about to pass it three. React allows that — a component
759
+ * receives the props its parent wrote and ignores the ones it did not declare
760
+ * — but Flow cannot be told it: a page's props are exact, so no props type but
761
+ * that page's own is assignable, and the router does not know which page it
762
+ * has. `React.ComponentType<any>` on the module types was this same
763
+ * unsoundness spread over six declarations, where it also stopped anyone from
764
+ * checking that `RouteView` passes the props a page is documented to receive.
765
+ * Here it is one line, and everything on either side of it is checked: what a
766
+ * module may export, and what a page is handed.
767
+ */
768
+ function renderable<TProps extends { ... }>(
769
+ component: RouteComponent,
770
+ ): React.ComponentType<TProps> {
771
+ return component as any;
685
772
  }
686
773
 
687
774
  component Head(metadata: Metadata) {
@@ -695,9 +782,7 @@ component Head(metadata: Metadata) {
695
782
  <meta property="og:description" content={openGraph.description} />
696
783
  ) : null}
697
784
  {openGraph?.images != null
698
- ? openGraph.images.map((image) => (
699
- <meta key={image} property="og:image" content={image} />
700
- ))
785
+ ? openGraph.images.map((image) => <meta key={image} property="og:image" content={image} />)
701
786
  : null}
702
787
  </>
703
788
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/router",
3
- "version": "0.0.0-alpha.4",
3
+ "version": "0.0.0-alpha.5",
4
4
  "description": "The file-system router for Flow React applications: matching, layouts, loaders, navigation, server rendering and hydration.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,6 +29,6 @@
29
29
  "react-dom": ">=19"
30
30
  },
31
31
  "dependencies": {
32
- "@uniflowed/server": "0.0.0-alpha.4"
32
+ "@uniflowed/server": "0.0.0-alpha.5"
33
33
  }
34
34
  }