@jsenv/navi 0.29.223 → 0.29.225
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/dist/dev/jsenv_navi.js +191 -55
- package/dist/dev/jsenv_navi.js.map +13 -11
- package/dist/jsenv_navi.js +184 -55
- package/dist/jsenv_navi.js.map +13 -11
- package/docs/AI_INSTRUCTIONS.md +7 -0
- package/docs/css_architecture.md +7 -0
- package/docs/dynamic_import.md +189 -0
- package/docs/navigation.md +3 -1
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -114,6 +114,13 @@ 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: a page's
|
|
118
|
+
import is a `routeAction`, a component reads its own with
|
|
119
|
+
`useAsyncData(() => import(…))`, so `<Loading>`, `<ErrorBoundary>` and
|
|
120
|
+
`rerun` cover it; links prerun on intent what asks nothing of the address; a
|
|
121
|
+
transition photographs the fallback; a chunk's `import.meta.css`; a chunk
|
|
122
|
+
that does not come is never a bug; why not preact's `lazy`. Read before an
|
|
123
|
+
`import()` in a component or a page, and before `prefetch={false}`.
|
|
117
124
|
|
|
118
125
|
### Controls and forms
|
|
119
126
|
|
package/docs/css_architecture.md
CHANGED
|
@@ -135,6 +135,13 @@ before splitting. In navi, `popup_css.js` and `surface_text_css.js` overlap
|
|
|
135
135
|
`.navi_dialog` and `.navi_popover` with dialog's and popover's own css, which is
|
|
136
136
|
why they are still concatenated.
|
|
137
137
|
|
|
138
|
+
### A sheet on the far side of a split
|
|
139
|
+
|
|
140
|
+
A module reached by `import()` carries its stylesheet in its chunk, adopted
|
|
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 code loaded
|
|
143
|
+
on demand is in [dynamic_import.md](./dynamic_import.md).
|
|
144
|
+
|
|
138
145
|
### Browser support: navi's css and your target
|
|
139
146
|
|
|
140
147
|
**navi does not require recent browsers.** Its css is written with modern
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Code loaded on demand
|
|
2
|
+
|
|
3
|
+
What an app wants from an `import()` is that **a screen's code arrives the way
|
|
4
|
+
its data does**: one wait, one failure, one place that shows both. A module
|
|
5
|
+
fetched on demand is a request like any other — it takes time, it fails when
|
|
6
|
+
the network is gone or a deploy happened in between — and nothing on screen
|
|
7
|
+
should have to know whether what it waits for is bytes of data or bytes of
|
|
8
|
+
code.
|
|
9
|
+
|
|
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.
|
|
12
|
+
|
|
13
|
+
## A page: its code is a route action
|
|
14
|
+
|
|
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:
|
|
18
|
+
|
|
19
|
+
```jsx
|
|
20
|
+
const GAME_PAGE_ACTION = routeAction(GAME_ROUTE, GAME.GET, () => ({
|
|
21
|
+
gameId: GAME_ROUTE.paramsSignal.value.gameId,
|
|
22
|
+
}));
|
|
23
|
+
const GAME_PAGE_CODE = routeAction(GAME_ROUTE, () =>
|
|
24
|
+
import("./game_page.jsx").then((m) => m.GamePage),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const GamePage = () => {
|
|
28
|
+
const [Page] = useAsyncData(GAME_PAGE_CODE);
|
|
29
|
+
return <Page />;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
<Route>
|
|
33
|
+
<ErrorBoundary fallback={PageError}>
|
|
34
|
+
<Loading fallback={<PageSkeleton />}>
|
|
35
|
+
<Route route={GAME_ROUTE} element={GamePage} />
|
|
36
|
+
</Loading>
|
|
37
|
+
</ErrorBoundary>
|
|
38
|
+
</Route>;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Everything the data layer decides then holds for the code, with nothing more
|
|
42
|
+
to write:
|
|
43
|
+
|
|
44
|
+
- **One wait.** The page suspends into the nearest `<Loading>` on whichever of
|
|
45
|
+
the two arrives last; the boundary is told it is loading and draws its
|
|
46
|
+
fallback; the routing is busy until both are there.
|
|
47
|
+
- **One failure.** A rejected import fails the run: the nearest
|
|
48
|
+
`<ErrorBoundary>` shows it, `error.action.rerun()` is the way back, going
|
|
49
|
+
somewhere else leaves it behind.
|
|
50
|
+
- **Fetched once.** A route action left behind is aborted, not reset, and a
|
|
51
|
+
completed one is not run again: coming back to the page renders
|
|
52
|
+
synchronously, only the data is asked for per params.
|
|
53
|
+
- **Fetched ahead.** A route action that asks nothing of the address is prerun
|
|
54
|
+
on intent (below).
|
|
55
|
+
|
|
56
|
+
The page module itself is ordinary — it reads its data with
|
|
57
|
+
`useAsyncData(GAME_PAGE_ACTION)` like any page — and imports the routes module
|
|
58
|
+
back for it; a dynamic import is not a load-order cycle.
|
|
59
|
+
|
|
60
|
+
## A component inside a page that stays: it reads its own code
|
|
61
|
+
|
|
62
|
+
A plan drawn on demand inside a page that stays knows where it is drawn and
|
|
63
|
+
what stands there — a frame, a line, a button. `useAsyncData` takes a
|
|
64
|
+
function: the request this component owns, made into an action once on the
|
|
65
|
+
first render and kept for the life of the instance, its answer as the data.
|
|
66
|
+
The wait and the failure are then drawn where the frame is, and no boundary is
|
|
67
|
+
involved:
|
|
68
|
+
|
|
69
|
+
```jsx
|
|
70
|
+
const PlanSection = () => {
|
|
71
|
+
const [Plan, loading, error] = useAsyncData(
|
|
72
|
+
() => import("./plan.jsx").then((m) => m.Plan),
|
|
73
|
+
{ loading: true, error: true },
|
|
74
|
+
);
|
|
75
|
+
return (
|
|
76
|
+
<PlanFrame>
|
|
77
|
+
{loading ? <Text>…</Text> : null}
|
|
78
|
+
{error ? (
|
|
79
|
+
<Button action={() => error.action.rerun()}>Réessayer</Button>
|
|
80
|
+
) : null}
|
|
81
|
+
{Plan ? <Plan /> : null}
|
|
82
|
+
</PlanFrame>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Delegating instead (`useAsyncData(fn)` alone, under a `<Loading>` around the
|
|
88
|
+
frame) is the same hook with the same rule as for data: **the `<Loading>` goes
|
|
89
|
+
where the fallback should land.** A boundary around the whole router takes the
|
|
90
|
+
router away while a page loads — top bar, tabs and all.
|
|
91
|
+
|
|
92
|
+
This shape starts at the render and is per instance: a component fetched this
|
|
93
|
+
way in three places asks three times (the browser fetches the module once),
|
|
94
|
+
and coming back to it suspends for the microtask the import takes to answer
|
|
95
|
+
from the module map. A page wants the route action shape; a section inside a
|
|
96
|
+
page rarely notices.
|
|
97
|
+
|
|
98
|
+
Nothing has to be kept out of a subtree loaded this way: the suspension
|
|
99
|
+
happens before the module's components exist, so nothing inside it is parked
|
|
100
|
+
by the code arriving.
|
|
101
|
+
|
|
102
|
+
## Ahead of the render: intent
|
|
103
|
+
|
|
104
|
+
**A link preloads where it leads when the pointer or the focus reaches it.**
|
|
105
|
+
`<Link>` and `<Button>` with an `href` or a `route` do it by default;
|
|
106
|
+
`prefetch={false}` opts one out. The url is matched against the routes on
|
|
107
|
+
screen, a section and the page inside it alike, and what is prerun is **every
|
|
108
|
+
route action that asks nothing of the address** — a page's code, a read
|
|
109
|
+
without params. An action whose params come from the address is never
|
|
110
|
+
prefetched: a prefetch has no address, the id may still be chosen.
|
|
111
|
+
`routeAction(route, action, undefined, { prefetch: false })` keeps a
|
|
112
|
+
param-less read that is not worth a hover out of it.
|
|
113
|
+
|
|
114
|
+
A prefetch that fails is forgotten, not reported: nothing on screen asked for
|
|
115
|
+
it. The arrival asks again and shows its own failure.
|
|
116
|
+
|
|
117
|
+
The same from code: `route.preload()` for a route something is about to
|
|
118
|
+
navigate to, `preloadUrl(url)` for an address. A press that computes its
|
|
119
|
+
destination has nothing to preload — the navigation itself asks.
|
|
120
|
+
|
|
121
|
+
## What a transition photographs
|
|
122
|
+
|
|
123
|
+
A route element still waiting is announced as rendered by its boundary, so a
|
|
124
|
+
route transition or a `RouteTravel` takes its picture of the **fallback**, code
|
|
125
|
+
and data alike. Navi does not hold a navigation for a chunk: the honest answer
|
|
126
|
+
to "a chunk is a hundred milliseconds" is the prefetch above — the code is
|
|
127
|
+
there before the press — and a skeleton that is the page's own shape is what
|
|
128
|
+
gets photographed when it is not.
|
|
129
|
+
|
|
130
|
+
## `import.meta.css` on the far side of a split
|
|
131
|
+
|
|
132
|
+
A module's stylesheet ships **inside its chunk** and is adopted when the
|
|
133
|
+
assignment runs (see [css_architecture.md](./css_architecture.md)). Three
|
|
134
|
+
consequences:
|
|
135
|
+
|
|
136
|
+
- **The fallback cannot use a class the lazy module defines.** It is rendered
|
|
137
|
+
by the eager side, before the chunk exists. The frame of what it stands in
|
|
138
|
+
for — background, border, aspect ratio — belongs to the eager module, the
|
|
139
|
+
lazy one owning only the inside; duplicating the frame into the eager side
|
|
140
|
+
is the same answer.
|
|
141
|
+
- **Layers are what the text says.** `@layer navi` holds navi's defaults
|
|
142
|
+
whether the sheet came with the entry or with a chunk, and an app's unlayered
|
|
143
|
+
rule wins over it from either side.
|
|
144
|
+
- **Between unlayered sheets, the last adopted wins at equal specificity.** A
|
|
145
|
+
chunk's sheet lands after everything already adopted, so a chunk beats the
|
|
146
|
+
entry; between two chunks the order is the order they ran in, which nothing
|
|
147
|
+
in the build fixes. A rule that depends on it is a rule to make more
|
|
148
|
+
specific.
|
|
149
|
+
|
|
150
|
+
## When the code does not come
|
|
151
|
+
|
|
152
|
+
Someone has the app open, a deploy moves the chunks, and the next `import()`
|
|
153
|
+
is a 404; or the network is gone. The browser says both the same way — a
|
|
154
|
+
`TypeError` with no status — and neither is a bug of the page: there is
|
|
155
|
+
nothing in the app's code to point at.
|
|
156
|
+
|
|
157
|
+
The component reading its own code says it where it stands, as above. For a
|
|
158
|
+
page, the failure reaches the boundary like a failed read does; a boundary
|
|
159
|
+
that shows only what came from the network and rethrows the rest as a bug
|
|
160
|
+
recognises an import by its action — `error.action` is the route action the
|
|
161
|
+
app declared for the code — rather than by the browser's message. Either way
|
|
162
|
+
`error.action.rerun()` retries; a retry failing the same way says the document
|
|
163
|
+
is stale, and **reloading it** is what brings the new code. Navi does not do
|
|
164
|
+
it on its own — a document reloading itself is a loop waiting to happen — the
|
|
165
|
+
screen says it.
|
|
166
|
+
|
|
167
|
+
## Preact's `lazy()` is not this
|
|
168
|
+
|
|
169
|
+
`preact/compat` has a `lazy()`. It suspends without saying anything to
|
|
170
|
+
`<Loading>`: the boundary keeps the last reason it knows — "idle" by default —
|
|
171
|
+
and draws nothing while the module loads; a failure is not an action's, so no
|
|
172
|
+
`rerun` brings the page back. Navi warns in dev when a `<Loading>` catches a
|
|
173
|
+
suspension it cannot attribute to an action. Read the import through an
|
|
174
|
+
action.
|
|
175
|
+
|
|
176
|
+
## The build
|
|
177
|
+
|
|
178
|
+
Every `import()` becomes a chunk of its own under `js/`, versioned with the
|
|
179
|
+
rest, and a target without dynamic import gets the fallback. Nothing to
|
|
180
|
+
configure.
|
|
181
|
+
|
|
182
|
+
## Reference
|
|
183
|
+
|
|
184
|
+
- `src/nav/demos/code_splitting/code_splitting_demo.html` — two pages whose
|
|
185
|
+
code is a route action, one prefetched on hover and one opted out, and a
|
|
186
|
+
component inside a page that stays, drawing its own wait and failure; every
|
|
187
|
+
import is drawn on the backend's frontier so a wait or a failure can be
|
|
188
|
+
played by hand.
|
|
189
|
+
- `src/nav/route_action.js`, `src/nav/use_preload_on_intent.js`
|
package/docs/navigation.md
CHANGED
|
@@ -385,7 +385,9 @@ no boundary takes: [error_handling.md](./error_handling.md).
|
|
|
385
385
|
## Links and tab rows
|
|
386
386
|
|
|
387
387
|
`<Link route={…}>` builds its href from the route and knows on its own whether it
|
|
388
|
-
is the current one — that is what draws the current-tab state.
|
|
388
|
+
is the current one — that is what draws the current-tab state. It also fetches
|
|
389
|
+
the code of where it leads when the pointer or the focus reaches it, for a page
|
|
390
|
+
loaded on demand (see [dynamic_import.md](./dynamic_import.md)). `<Nav>` says once,
|
|
389
391
|
for the whole row, where the bar that marks the current tab goes:
|
|
390
392
|
|
|
391
393
|
```jsx
|