@jsenv/navi 0.29.224 → 0.29.226
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/README.md +2 -0
- package/dist/dev/jsenv_navi.js +105 -123
- package/dist/dev/jsenv_navi.js.map +8 -10
- package/dist/jsenv_navi.js +103 -121
- package/dist/jsenv_navi.js.map +8 -10
- package/docs/AI_INSTRUCTIONS.md +8 -5
- package/docs/css_architecture.md +2 -2
- package/docs/dynamic_import.md +150 -83
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -114,11 +114,14 @@ to start when unsure which export solves a problem.
|
|
|
114
114
|
a boundary. Read before displaying an error by hand or writing a boundary.
|
|
115
115
|
- `offline.md` — `setNetworkPolicy`: answer from the store, ask nothing, refuse
|
|
116
116
|
writes politely. Read before caching responses in the app.
|
|
117
|
-
- `dynamic_import.md` — a screen's code arrives like its data:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
- `dynamic_import.md` — a screen's code arrives like its data: a page's
|
|
118
|
+
import is a `routeAction` read with `loading: true` (a branch, not a
|
|
119
|
+
`<Loading>`), a component reads its own with `useAsyncData(() => import(…))`;
|
|
120
|
+
links prerun on intent what asks nothing of the address; a transition
|
|
121
|
+
photographs the pending screen; a chunk's `import.meta.css`; a chunk that
|
|
122
|
+
does not come is never a bug and never comes twice in one document — offer
|
|
123
|
+
`reload()`; why not preact's `lazy`. Read before an `import()` in a component
|
|
124
|
+
or a page, and before `prefetch={false}`.
|
|
122
125
|
|
|
123
126
|
### Controls and forms
|
|
124
127
|
|
package/docs/css_architecture.md
CHANGED
|
@@ -139,8 +139,8 @@ why they are still concatenated.
|
|
|
139
139
|
|
|
140
140
|
A module reached by `import()` carries its stylesheet in its chunk, adopted
|
|
141
141
|
when the chunk runs: a fallback rendered before it cannot use its classes, and
|
|
142
|
-
between unlayered sheets the last adopted wins. What follows for
|
|
143
|
-
|
|
142
|
+
between unlayered sheets the last adopted wins. What follows for code loaded
|
|
143
|
+
on demand is in [dynamic_import.md](./dynamic_import.md).
|
|
144
144
|
|
|
145
145
|
### Browser support: navi's css and your target
|
|
146
146
|
|
package/docs/dynamic_import.md
CHANGED
|
@@ -7,99 +7,139 @@ the network is gone or a deploy happened in between — and nothing on screen
|
|
|
7
7
|
should have to know whether what it waits for is bytes of data or bytes of
|
|
8
8
|
code.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
So navi has nothing lazy-shaped: **the import is an action**, read by the hook
|
|
11
|
+
a screen already reads its data with. Two shapes, decided by who asks.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
## A page: its code is a route action
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
The address asks for the page's data through a route action; it asks for the
|
|
16
|
+
page's code the same way, and the two are in flight together from the url
|
|
17
|
+
change. The page component in the entry reads the code like data — with
|
|
18
|
+
`loading: true` and `error: true`, drawing the page's own pending screen and
|
|
19
|
+
its own unavailable screen where the page will stand:
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
```jsx
|
|
22
|
+
const GAME_PAGE_ACTION = routeAction(GAME_ROUTE, GAME.GET, () => ({
|
|
23
|
+
gameId: GAME_ROUTE.paramsSignal.value.gameId,
|
|
24
|
+
}));
|
|
25
|
+
const GAME_PAGE_CODE = routeAction(GAME_ROUTE, () =>
|
|
26
|
+
import("./game_page.jsx").then((m) => m.GamePage),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const GamePage = () => {
|
|
30
|
+
const [Page, loading, error] = useAsyncData(GAME_PAGE_CODE, {
|
|
31
|
+
loading: true,
|
|
32
|
+
error: true,
|
|
33
|
+
});
|
|
34
|
+
if (loading) {
|
|
35
|
+
return <GameScreenPending />;
|
|
36
|
+
}
|
|
37
|
+
if (error) {
|
|
38
|
+
return <GameScreenUnavailable />;
|
|
39
|
+
}
|
|
40
|
+
return <Page />;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
<Route route={GAME_ROUTE} element={GamePage} />;
|
|
26
44
|
```
|
|
27
45
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- **
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
Why a branch rather than a `<Loading>` around it: suspending takes the
|
|
47
|
+
subtree away and puts the boundary's fallback in its place, then mounts the
|
|
48
|
+
page again — a cost worth paying when a page already on screen re-reads its
|
|
49
|
+
data (see [data_states.md](./data_states.md)), and worth nothing here, where
|
|
50
|
+
the page is not there yet and has nothing to keep. `loading: true` never
|
|
51
|
+
suspends: one component, no wrapper, the pending screen is the page's own
|
|
52
|
+
shape, and the failure sits beside it (what it offers: below, "When the code
|
|
53
|
+
does not come"). What is delegated stays delegated — the page's data, read inside
|
|
54
|
+
`game_page.jsx` with `useAsyncData(GAME_PAGE_ACTION)`, still waits in the
|
|
55
|
+
`<Loading>` written between the routes, as [navigation.md](./navigation.md)
|
|
56
|
+
says.
|
|
57
|
+
|
|
58
|
+
Everything the data layer decides then holds for the code:
|
|
59
|
+
|
|
60
|
+
- **One failure.** A rejected import fails the run like a failed read; drawn
|
|
61
|
+
by the branch above, or by the nearest `<ErrorBoundary>` when the page reads
|
|
62
|
+
its code without `error: true`.
|
|
63
|
+
- **Fetched once.** A route action left behind is aborted, not reset, and a
|
|
64
|
+
completed one is not run again: coming back to the page renders
|
|
65
|
+
synchronously, only the data is asked for per params.
|
|
66
|
+
- **Fetched ahead.** A route action that asks nothing of the address is prerun
|
|
67
|
+
on intent (below).
|
|
68
|
+
- **Counted.** The routing is busy until the code is there, like for the
|
|
69
|
+
data.
|
|
70
|
+
|
|
71
|
+
The page module itself is ordinary — it reads its data with
|
|
72
|
+
`useAsyncData(GAME_PAGE_ACTION)` like any page — and imports the routes module
|
|
73
|
+
back for it; a dynamic import is not a load-order cycle.
|
|
74
|
+
|
|
75
|
+
## A component inside a page that stays: it reads its own code
|
|
76
|
+
|
|
77
|
+
A plan drawn on demand inside a page that stays knows where it is drawn and
|
|
78
|
+
what stands there — a frame, a line, a button. `useAsyncData` takes a
|
|
79
|
+
function: the request this component owns, made into an action once on the
|
|
80
|
+
first render and kept for the life of the instance, its answer as the data.
|
|
81
|
+
The wait and the failure are then drawn where the frame is, and no boundary is
|
|
82
|
+
involved:
|
|
83
|
+
|
|
84
|
+
```jsx
|
|
85
|
+
const PlanSection = () => {
|
|
86
|
+
const [Plan, loading, error] = useAsyncData(
|
|
87
|
+
() => import("./plan.jsx").then((m) => m.Plan),
|
|
88
|
+
{ loading: true, error: true },
|
|
89
|
+
);
|
|
90
|
+
return (
|
|
91
|
+
<PlanFrame>
|
|
92
|
+
{loading ? <Text>…</Text> : null}
|
|
93
|
+
{error ? <Button action={() => reload()}>Recharger</Button> : null}
|
|
94
|
+
{Plan ? <Plan /> : null}
|
|
95
|
+
</PlanFrame>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
53
98
|
```
|
|
54
99
|
|
|
55
|
-
|
|
100
|
+
The same reasons as for a page hold: nothing stands there yet, so a
|
|
101
|
+
suspension would only swap and remount for nothing. Delegating instead
|
|
102
|
+
(`useAsyncData(fn)` alone, under a `<Loading>` around the frame) follows the
|
|
103
|
+
rule for data: **the `<Loading>` goes where the fallback should land.** A
|
|
104
|
+
boundary around the whole router takes the router away while a page loads —
|
|
105
|
+
top bar, tabs and all.
|
|
56
106
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
page's box and nothing else (see [navigation.md](./navigation.md), "Loading
|
|
63
|
-
data"). A component fetched on demand inside a page that stays gets its own
|
|
64
|
-
`<Loading>` around it, with the frame of what it stands in for as the
|
|
65
|
-
fallback.
|
|
107
|
+
This shape starts at the render and is per instance: a component fetched this
|
|
108
|
+
way in three places asks three times (the browser fetches the module once),
|
|
109
|
+
and coming back to it suspends for the microtask the import takes to answer
|
|
110
|
+
from the module map. A page wants the route action shape; a section inside a
|
|
111
|
+
page rarely notices.
|
|
66
112
|
|
|
67
113
|
Nothing has to be kept out of a subtree loaded this way: the suspension
|
|
68
114
|
happens before the module's components exist, so nothing inside it is parked
|
|
69
|
-
by the code arriving.
|
|
70
|
-
which is the same story with or without a split.
|
|
115
|
+
by the code arriving.
|
|
71
116
|
|
|
72
117
|
## Ahead of the render: intent
|
|
73
118
|
|
|
74
|
-
**A link
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
a
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
The same fetch, from code: `route.preload()` for a route something is about to
|
|
119
|
+
**A link preloads where it leads when the pointer or the focus reaches it.**
|
|
120
|
+
`<Link>` and `<Button>` with an `href` or a `route` do it by default;
|
|
121
|
+
`prefetch={false}` opts one out. The url is matched against the routes on
|
|
122
|
+
screen, a section and the page inside it alike, and what is prerun is **every
|
|
123
|
+
route action that asks nothing of the address** — a page's code, a read
|
|
124
|
+
without params. An action whose params come from the address is never
|
|
125
|
+
prefetched: a prefetch has no address, the id may still be chosen.
|
|
126
|
+
`routeAction(route, action, undefined, { prefetch: false })` keeps a
|
|
127
|
+
param-less read that is not worth a hover out of it.
|
|
128
|
+
|
|
129
|
+
A prefetch that fails is not reported: nothing on screen asked for it. The
|
|
130
|
+
failure stays on the action, where the arrival asks again — a failed action is
|
|
131
|
+
run, only a running or completed one is left alone — and shows its own.
|
|
132
|
+
|
|
133
|
+
The same from code: `route.preload()` for a route something is about to
|
|
90
134
|
navigate to, `preloadUrl(url)` for an address. A press that computes its
|
|
91
|
-
destination has nothing to preload — the navigation itself
|
|
92
|
-
|
|
93
|
-
Which routers know: a route's lazy elements register when the router holding
|
|
94
|
-
them renders. A section whose sub-router is inside its own chunk registers its
|
|
95
|
-
pages once the section is on screen; before that, intent on a link to a
|
|
96
|
-
sub-page fetches the section.
|
|
135
|
+
destination has nothing to preload — the navigation itself asks.
|
|
97
136
|
|
|
98
137
|
## What a transition photographs
|
|
99
138
|
|
|
100
|
-
A route element still waiting is
|
|
101
|
-
|
|
102
|
-
|
|
139
|
+
A route element still waiting is what the container rendered — its pending
|
|
140
|
+
screen, or the fallback of the boundary it suspended into — so a route
|
|
141
|
+
transition or a `RouteTravel` takes its picture of **that**, code and data
|
|
142
|
+
alike. Navi does not hold a navigation for a chunk: the honest answer
|
|
103
143
|
to "a chunk is a hundred milliseconds" is the prefetch above — the code is
|
|
104
144
|
there before the press — and a skeleton that is the page's own shape is what
|
|
105
145
|
gets photographed when it is not.
|
|
@@ -124,13 +164,38 @@ consequences:
|
|
|
124
164
|
in the build fixes. A rule that depends on it is a rule to make more
|
|
125
165
|
specific.
|
|
126
166
|
|
|
127
|
-
##
|
|
128
|
-
|
|
129
|
-
|
|
167
|
+
## When the code does not come
|
|
168
|
+
|
|
169
|
+
Someone has the app open, a deploy moves the chunks, and the next `import()`
|
|
170
|
+
is a 404; or the network is gone. The browser says both the same way — a
|
|
171
|
+
`TypeError` with no status — and neither is a bug of the page: there is
|
|
172
|
+
nothing in the app's code to point at.
|
|
173
|
+
|
|
174
|
+
**Asking again never fetches.** The document remembers a module whose fetch
|
|
175
|
+
failed as failed: a second `import()` of the same address is refused without a
|
|
176
|
+
request, for the life of the document. So a retry on an import has nothing to
|
|
177
|
+
do — `error.action.rerun()` fails again at once, on the same failure — and
|
|
178
|
+
the one way out is a fresh document: **`reload()`**, which the screen offers
|
|
179
|
+
in words the reader understands. Navi does not reload on its own: a document
|
|
180
|
+
reloading itself is a loop waiting to happen. It is also why a prefetch that
|
|
181
|
+
failed is not asked again on the arrival in any useful sense: the run is made,
|
|
182
|
+
refused at once, and the screen says it.
|
|
183
|
+
|
|
184
|
+
The page or the component reading its code says it where it stands, as
|
|
185
|
+
above. A failure left to a boundary — a page reading its code without
|
|
186
|
+
`error: true` — reaches it like a failed read does; a boundary that shows only
|
|
187
|
+
what came from the network and rethrows the rest as a bug recognises an import
|
|
188
|
+
by its action — `error.action` is the route action the app declared for the
|
|
189
|
+
code — rather than by the browser's message.
|
|
190
|
+
|
|
191
|
+
## Preact's `lazy()` is not this
|
|
192
|
+
|
|
193
|
+
`preact/compat` has a `lazy()`. It suspends without saying anything to
|
|
130
194
|
`<Loading>`: the boundary keeps the last reason it knows — "idle" by default —
|
|
131
195
|
and draws nothing while the module loads; a failure is not an action's, so no
|
|
132
196
|
`rerun` brings the page back. Navi warns in dev when a `<Loading>` catches a
|
|
133
|
-
suspension it cannot attribute to an action.
|
|
197
|
+
suspension it cannot attribute to an action. Read the import through an
|
|
198
|
+
action.
|
|
134
199
|
|
|
135
200
|
## The build
|
|
136
201
|
|
|
@@ -140,7 +205,9 @@ configure.
|
|
|
140
205
|
|
|
141
206
|
## Reference
|
|
142
207
|
|
|
143
|
-
- `src/nav/demos/code_splitting/code_splitting_demo.html` — two pages
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
208
|
+
- `src/nav/demos/code_splitting/code_splitting_demo.html` — two pages whose
|
|
209
|
+
code is a route action, one prefetched on hover and one opted out, and a
|
|
210
|
+
component inside a page that stays, drawing its own wait and failure; every
|
|
211
|
+
import is drawn on the backend's frontier so a wait or a failure can be
|
|
212
|
+
played by hand.
|
|
213
|
+
- `src/nav/route_action.js`, `src/nav/use_preload_on_intent.js`
|