@uniflowed/router 0.0.0-alpha.4 → 0.0.0-alpha.6
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/internal/runtime.js +100 -15
- package/package.json +2 -2
package/internal/runtime.js
CHANGED
|
@@ -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?:
|
|
36
|
-
readonly Page?:
|
|
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?:
|
|
50
|
-
readonly Layout?:
|
|
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
|
-
/**
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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<
|
|
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<
|
|
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.
|
|
3
|
+
"version": "0.0.0-alpha.6",
|
|
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.
|
|
32
|
+
"@uniflowed/server": "0.0.0-alpha.6"
|
|
33
33
|
}
|
|
34
34
|
}
|