@uniflowed/router 0.0.0-alpha.14 → 0.0.0-alpha.15
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/client.js +51 -2
- package/internal/devtools.js +131 -0
- package/internal/diagnostics.js +3 -3
- package/package.json +3 -3
package/client.js
CHANGED
|
@@ -18,6 +18,37 @@
|
|
|
18
18
|
// Development only, and dynamically imported so a production bundle has no path
|
|
19
19
|
// to it. See ubugeeei-prod/uf#508.
|
|
20
20
|
//
|
|
21
|
+
// # And whether React DevTools can see the page at all
|
|
22
|
+
//
|
|
23
|
+
// The other question only this module is in a position to ask.
|
|
24
|
+
// `@uniflowed/vite` installs the hook DevTools attaches through, above every
|
|
25
|
+
// module in the document; whether that worked *on this page* is a fact about a
|
|
26
|
+
// running browser, and the line after hydration is where it can be read.
|
|
27
|
+
// `internal/devtools.js` has the two findings and sends them to the same
|
|
28
|
+
// terminal the hydration report goes to. See ubugeeei-prod/uf#503.
|
|
29
|
+
//
|
|
30
|
+
// # Strict Mode, in development, by default
|
|
31
|
+
//
|
|
32
|
+
// `uf dev` generates `strictMode: true` into `virtual:uf/client` and `uf build`
|
|
33
|
+
// does not, so a development render is doubled and a visitor's is not. That is
|
|
34
|
+
// React's own check for the thing it cannot check any other way: a component
|
|
35
|
+
// whose render is not pure, and an effect whose cleanup does not undo its
|
|
36
|
+
// setup, both behave correctly until the one production render that interleaves
|
|
37
|
+
// with something — and Strict Mode makes them behave incorrectly at once, on
|
|
38
|
+
// the machine of the person writing them.
|
|
39
|
+
//
|
|
40
|
+
// The wrapper is the argument to `hydrateRoot` rather than something inside
|
|
41
|
+
// `<App>`, and that is load-bearing rather than tidy. React decides whether to
|
|
42
|
+
// double-invoke a mount's effects at the *topmost fiber it is placing*: if that
|
|
43
|
+
// fiber is not itself in Strict Mode, React stops there and never looks inside
|
|
44
|
+
// it. A `<StrictMode>` further down still doubles the renders under it — that
|
|
45
|
+
// comes from the fiber's own mode — and doubles no effect at all, so it would
|
|
46
|
+
// have bought the half of the check that is easy to notice and silently lost
|
|
47
|
+
// the half that finds the bug. It renders no element, so the hydrated tree is
|
|
48
|
+
// unchanged and the markup comparison above is unaffected.
|
|
49
|
+
// `app.react.strictMode: false` in `uf.config.js` turns it off. See
|
|
50
|
+
// ubugeeei-prod/uf#516.
|
|
51
|
+
//
|
|
21
52
|
// # A route can decline to be hydrated
|
|
22
53
|
//
|
|
23
54
|
// uf's server-component analysis decides which routes have a `"use client"`
|
|
@@ -29,7 +60,7 @@
|
|
|
29
60
|
// See ubugeeei-prod/uf#350.
|
|
30
61
|
|
|
31
62
|
import * as React from "react";
|
|
32
|
-
import { startTransition } from "react";
|
|
63
|
+
import { StrictMode, startTransition } from "react";
|
|
33
64
|
import { hydrateRoot } from "react-dom/client";
|
|
34
65
|
|
|
35
66
|
import {
|
|
@@ -54,6 +85,7 @@ export async function hydrate(options: {|
|
|
|
54
85
|
readonly routes: RouteTable["routes"],
|
|
55
86
|
readonly notFound: RouteTable["notFound"],
|
|
56
87
|
readonly errors: RouteTable["errors"],
|
|
88
|
+
readonly strictMode?: boolean,
|
|
57
89
|
|}): Promise<void> {
|
|
58
90
|
const table: RouteTable = {
|
|
59
91
|
routes: options.routes,
|
|
@@ -95,11 +127,28 @@ export async function hydrate(options: {|
|
|
|
95
127
|
recovery = hydrationErrorHandler(container, captureServerMarkup(container), document);
|
|
96
128
|
}
|
|
97
129
|
|
|
130
|
+
// `<StrictMode>` renders no element of its own, so the tree React hydrates
|
|
131
|
+
// against the server's markup is the same tree either way and the flag can
|
|
132
|
+
// be a development-only difference without being a hydration difference.
|
|
133
|
+
const tree = <App url={url} initial={resolved} />;
|
|
134
|
+
|
|
98
135
|
startTransition(() => {
|
|
99
136
|
hydrateRoot(
|
|
100
137
|
container,
|
|
101
|
-
|
|
138
|
+
options.strictMode === true ? <StrictMode>{tree}</StrictMode> : tree,
|
|
102
139
|
recovery == null ? undefined : { onRecoverableError: recovery },
|
|
103
140
|
);
|
|
104
141
|
});
|
|
142
|
+
|
|
143
|
+
// And, in development only, whether the panel a developer is about to open
|
|
144
|
+
// can see any of that. `react-dom` announced itself while it was being
|
|
145
|
+
// imported — long before this line — so the answer is already settled and
|
|
146
|
+
// this only reads it. Behind the same `import.meta.hot` gate as the
|
|
147
|
+
// hydration reporter, dynamically imported for the same reason: a production
|
|
148
|
+
// bundle has no path to the module rather than merely no reason to run it.
|
|
149
|
+
// See `./internal/devtools.js` and ubugeeei-prod/uf#503.
|
|
150
|
+
if (import.meta.hot != null) {
|
|
151
|
+
const { reportDevtools } = await import("./internal/devtools.js");
|
|
152
|
+
reportDevtools(window);
|
|
153
|
+
}
|
|
105
154
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/router`: whether React DevTools can actually attach
|
|
4
|
+
// to the page this browser just hydrated.
|
|
5
|
+
//
|
|
6
|
+
// `@uniflowed/vite` installs the hook DevTools attaches through, as a classic
|
|
7
|
+
// script above every module — see `packages/vite/internal/devtools.js`, which
|
|
8
|
+
// has the argument. That is uf saying what it intends. This is the half that
|
|
9
|
+
// checks it happened, and the two are not the same claim: the preamble is
|
|
10
|
+
// injected by a `transformIndexHtml` hook, into a document a project's own Vite
|
|
11
|
+
// plugins also write to, and the thing that has to be true is an *ordering* —
|
|
12
|
+
// the hook exists before `react-dom` is evaluated — which no amount of reading
|
|
13
|
+
// the injector can establish about a particular page.
|
|
14
|
+
//
|
|
15
|
+
// It runs once, after hydration, in development only, and says nothing at all
|
|
16
|
+
// when there is nothing wrong. See ubugeeei-prod/uf#503.
|
|
17
|
+
//
|
|
18
|
+
// # Why it reports rather than throws
|
|
19
|
+
//
|
|
20
|
+
// Nothing here is a reason to stop a page. DevTools not attaching costs a
|
|
21
|
+
// developer a panel, and a framework that refused to render over it would have
|
|
22
|
+
// turned a missing convenience into an outage. So the two findings go to the
|
|
23
|
+
// terminal on the same channel as every other browser-side diagnostic — see
|
|
24
|
+
// `./diagnostics.js` — and the page carries on.
|
|
25
|
+
//
|
|
26
|
+
// # The two findings, and why they are the two
|
|
27
|
+
//
|
|
28
|
+
// A third condition — React's *development* build, which is what gives the
|
|
29
|
+
// panel props, hooks and source positions — is not checked here because a
|
|
30
|
+
// browser cannot tell the difference from the outside, and because uf owns it
|
|
31
|
+
// end to end: `mode` is `development` and `uf transform` is called with
|
|
32
|
+
// `development: true`, both asserted in `tests/library/devtools.test.js`
|
|
33
|
+
// against the plugin rather than against a page. What is left is what only a
|
|
34
|
+
// running page knows.
|
|
35
|
+
//
|
|
36
|
+
// 1. **There is no hook.** Something ran before `react-dom` and there was
|
|
37
|
+
// nothing for it to register with, or the preamble did not reach this
|
|
38
|
+
// document. Either way no renderer was announced and the panel will say
|
|
39
|
+
// the page is not using React.
|
|
40
|
+
// 2. **There is more than one renderer.** Two copies of `react-dom` each
|
|
41
|
+
// injected, and DevTools shows the tree of whichever it heard from — which
|
|
42
|
+
// is the shape of the "multiple renderers concurrently rendering the same
|
|
43
|
+
// context provider" report, and a component tree that is missing half the
|
|
44
|
+
// page for a reason nothing on screen explains.
|
|
45
|
+
|
|
46
|
+
import { reportDiagnostic } from "./diagnostics.js";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The global React registers itself with.
|
|
50
|
+
*
|
|
51
|
+
* The same string as `DEVTOOLS_HOOK` in `@uniflowed/vite`'s
|
|
52
|
+
* `internal/devtools.js`, written out again rather than imported for the reason
|
|
53
|
+
* that file's neighbour `internal/diagnostics.js` gives about the endpoint
|
|
54
|
+
* paths: `@uniflowed/vite` is loaded by Vite before any Flow transform exists
|
|
55
|
+
* and this module is Flow, so the import cannot go either way.
|
|
56
|
+
* `tests/library/devtools.test.js` asserts the two spellings agree, which is
|
|
57
|
+
* what makes a duplicated constant honest.
|
|
58
|
+
*/
|
|
59
|
+
export const DEVTOOLS_HOOK: string = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
|
|
60
|
+
|
|
61
|
+
/** The part of a page this module reads. */
|
|
62
|
+
type HookWindow = {
|
|
63
|
+
[key: string]: mixed,
|
|
64
|
+
...
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What is wrong with this page's DevTools hook, as a diagnostic, or `null`.
|
|
69
|
+
*
|
|
70
|
+
* Separated from the reporting so that a test can ask the question without a
|
|
71
|
+
* channel to answer on, and because the wording is the part worth pinning: a
|
|
72
|
+
* reader who sees this in a terminal has to be able to act on it without
|
|
73
|
+
* reading this file.
|
|
74
|
+
*/
|
|
75
|
+
export function devtoolsProblem(win: HookWindow): {|
|
|
76
|
+
readonly message: string,
|
|
77
|
+
readonly detail: $ReadOnlyArray<string>,
|
|
78
|
+
|} | null {
|
|
79
|
+
const hook = win[DEVTOOLS_HOOK];
|
|
80
|
+
if (hook == null || typeof hook !== "object") {
|
|
81
|
+
return {
|
|
82
|
+
message: `React DevTools cannot attach: nothing installed \`${DEVTOOLS_HOOK}\` before react-dom ran`,
|
|
83
|
+
detail: [
|
|
84
|
+
"React registers itself with that global while `react-dom` is evaluated, once and never again.",
|
|
85
|
+
"`uf dev` injects the hook as a classic script at the top of the head; a plugin that replaces",
|
|
86
|
+
"`transformIndexHtml`'s output, or a document that does not go through it, takes it away.",
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// `renderers` is a Map React puts its renderer in, keyed by the id `inject`
|
|
92
|
+
// handed back. Anything else there is a hook uf did not install and DevTools
|
|
93
|
+
// did not either, and guessing at its shape would report a problem that is
|
|
94
|
+
// really this module not recognising one.
|
|
95
|
+
const renderers = (hook: $FlowFixMe).renderers;
|
|
96
|
+
const count = renderers instanceof Map ? renderers.size : null;
|
|
97
|
+
if (count != null && count > 1) {
|
|
98
|
+
return {
|
|
99
|
+
message: `React DevTools has ${count} renderers on this page and will show one of them`,
|
|
100
|
+
detail: [
|
|
101
|
+
"Two copies of `react-dom` are loaded, so half the component tree is in a tree the panel cannot see.",
|
|
102
|
+
'`resolve.dedupe: ["react", "react-dom"]` is what usually prevents it; a linked package with its',
|
|
103
|
+
"own `react-dom` in `node_modules` is what usually causes it.",
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Report the problem, if there is one, to the terminal running `uf dev`.
|
|
112
|
+
*
|
|
113
|
+
* Throws nothing and returns nothing, and the guard is around the whole body
|
|
114
|
+
* rather than around the reading: this is called from the line after a
|
|
115
|
+
* successful hydration, so every failure available to it — a hook object whose
|
|
116
|
+
* property getter throws, a host with no `fetch` to report through — is a
|
|
117
|
+
* development convenience failing, and a development convenience that can take
|
|
118
|
+
* a working page down is worse than no convenience at all.
|
|
119
|
+
*/
|
|
120
|
+
export function reportDevtools(win: HookWindow): void {
|
|
121
|
+
try {
|
|
122
|
+
const problem = devtoolsProblem(win);
|
|
123
|
+
if (problem == null) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
reportDiagnostic({ severity: "warn", message: problem.message, detail: problem.detail });
|
|
127
|
+
} catch {
|
|
128
|
+
// Deliberately silent. There is no second channel to complain on, and the
|
|
129
|
+
// thing being reported was never worth interrupting anybody for.
|
|
130
|
+
}
|
|
131
|
+
}
|
package/internal/diagnostics.js
CHANGED
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
// `uf dev` serves this path and nothing else does: a built application has no
|
|
40
40
|
// `/__uf/` anything, so a call in production posts to a path that answers 404
|
|
41
41
|
// and the rejected promise is swallowed here. That is a fallback rather than a
|
|
42
|
-
// design —
|
|
43
|
-
//
|
|
44
|
-
// from it.
|
|
42
|
+
// design — both callers are behind `import.meta.hot` in `../client.js`, the
|
|
43
|
+
// hydration report and the DevTools check, so a production bundle has no path
|
|
44
|
+
// to this module rather than merely no answer from it.
|
|
45
45
|
//
|
|
46
46
|
// Nothing here opens a connection until it is called, importing it does nothing
|
|
47
47
|
// at all, and what it sends goes to the page's own origin as a path rather than
|
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.15",
|
|
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",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"react-dom": ">=19"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@uniflowed/hooks": "0.0.0-alpha.
|
|
37
|
-
"@uniflowed/server": "0.0.0-alpha.
|
|
36
|
+
"@uniflowed/hooks": "0.0.0-alpha.15",
|
|
37
|
+
"@uniflowed/server": "0.0.0-alpha.15"
|
|
38
38
|
}
|
|
39
39
|
}
|