@jsenv/navi 0.29.223 → 0.29.224
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 +204 -49
- package/dist/dev/jsenv_navi.js.map +15 -11
- package/dist/jsenv_navi.js +197 -49
- package/dist/jsenv_navi.js.map +15 -11
- package/docs/AI_INSTRUCTIONS.md +5 -0
- package/docs/css_architecture.md +7 -0
- package/docs/dynamic_import.md +146 -0
- package/docs/navigation.md +3 -1
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -114,6 +114,11 @@ 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: `lazy()` makes
|
|
118
|
+
the import an action, so `<Loading>`, `<ErrorBoundary>` and `rerun` cover it;
|
|
119
|
+
links fetch code on intent, never data; a transition photographs the
|
|
120
|
+
fallback; a chunk's `import.meta.css`; why not preact's `lazy`. Read before
|
|
121
|
+
an `import()` in a component or a page, and before `prefetch={false}`.
|
|
117
122
|
|
|
118
123
|
### Controls and forms
|
|
119
124
|
|
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 a lazily
|
|
143
|
+
loaded component 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,146 @@
|
|
|
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
|
+
## The import is an action
|
|
11
|
+
|
|
12
|
+
`lazy()` turns a loader into a component whose code is an action:
|
|
13
|
+
|
|
14
|
+
```jsx
|
|
15
|
+
import { lazy } from "@jsenv/navi";
|
|
16
|
+
|
|
17
|
+
const GamePage = lazy(() => import("./game_page.jsx"));
|
|
18
|
+
|
|
19
|
+
<Route>
|
|
20
|
+
<ErrorBoundary fallback={PageError}>
|
|
21
|
+
<Loading fallback={<PageSkeleton />}>
|
|
22
|
+
<Route route={GAME_ROUTE} element={GamePage} />
|
|
23
|
+
</Loading>
|
|
24
|
+
</ErrorBoundary>
|
|
25
|
+
</Route>;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Everything the data layer already decides then holds for the code, with
|
|
29
|
+
nothing more to write:
|
|
30
|
+
|
|
31
|
+
- **It starts with the data.** The route action runs on the url change; the
|
|
32
|
+
import starts from the render the match triggers, a microtask later — or
|
|
33
|
+
earlier, on intent (below). The two are in flight together, and the page
|
|
34
|
+
suspends on whichever arrives last.
|
|
35
|
+
- **One wait.** The import suspends into the nearest `<Loading>` like a
|
|
36
|
+
`useAsyncData` does — the boundary is told it is loading, and draws its
|
|
37
|
+
fallback. It counts in the document's busy state like a route action.
|
|
38
|
+
- **One failure.** A rejected import fails the run: the nearest
|
|
39
|
+
`<ErrorBoundary>` shows it, `error.action.rerun()` is the way back, and going
|
|
40
|
+
somewhere else leaves it behind. A module missing after a deploy is an error
|
|
41
|
+
the screen already knows how to show.
|
|
42
|
+
- **Fetched once.** A completed run is not run again: coming back to the page
|
|
43
|
+
renders synchronously, only the data is asked for per params.
|
|
44
|
+
|
|
45
|
+
The loader resolves to the component itself or to the module: its `default`
|
|
46
|
+
export, or its only exported function. A module exporting several says which
|
|
47
|
+
by returning it from the loader — and two `lazy()` on the same module cost one
|
|
48
|
+
fetch, the browser holds a module once:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
const PlanThumbnail = lazy(() => import("./plan.jsx").then((m) => m.PlanThumbnail));
|
|
52
|
+
const PlanDialog = lazy(() => import("./plan.jsx").then((m) => m.PlanDialog));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Where the boundary goes
|
|
56
|
+
|
|
57
|
+
The rule of the data layer applies unchanged: **the `<Loading>` goes where the
|
|
58
|
+
fallback should land**, and a `lazy()` element needs one above it exactly as a
|
|
59
|
+
page reading its data does. A boundary around the whole router takes the
|
|
60
|
+
router away while a page loads — top bar, tabs and all — and puts the fallback
|
|
61
|
+
in its place. Written between the container and its branches, it holds the
|
|
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.
|
|
66
|
+
|
|
67
|
+
Nothing has to be kept out of a subtree loaded this way: the suspension
|
|
68
|
+
happens before the module's components exist, so nothing inside it is parked
|
|
69
|
+
by the code arriving. A subtree parked later is parked by a data suspension,
|
|
70
|
+
which is the same story with or without a split.
|
|
71
|
+
|
|
72
|
+
## Ahead of the render: intent
|
|
73
|
+
|
|
74
|
+
**A link fetches the code of where it leads when the pointer or the focus
|
|
75
|
+
reaches it.** `<Link>` and `<Button>` with an `href` or a `route` do it by
|
|
76
|
+
default; `prefetch={false}` opts one out — a destination not worth fetching on
|
|
77
|
+
a hover. The url is matched against the routes on screen, a section and the
|
|
78
|
+
page inside it alike, and each fetches what its router registered for it.
|
|
79
|
+
|
|
80
|
+
Two things are deliberately NOT part of it:
|
|
81
|
+
|
|
82
|
+
- **Only code is fetched, never data.** The data is the route action's, asked
|
|
83
|
+
for on arrival with the params the address holds. A prefetch has no such
|
|
84
|
+
address yet — the id may still be chosen — and a read ahead of need is a
|
|
85
|
+
decision the app takes itself, with `prerun()` on the binding it wants warm.
|
|
86
|
+
- **A prefetch that fails is forgotten**, not reported: nothing on screen asked
|
|
87
|
+
for it. The visit that needs the code asks again and shows its own failure.
|
|
88
|
+
|
|
89
|
+
The same fetch, from code: `route.preload()` for a route something is about to
|
|
90
|
+
navigate to, `preloadUrl(url)` for an address. A press that computes its
|
|
91
|
+
destination has nothing to preload — the navigation itself fetches the code.
|
|
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.
|
|
97
|
+
|
|
98
|
+
## What a transition photographs
|
|
99
|
+
|
|
100
|
+
A route element still waiting is announced as rendered by its boundary, so a
|
|
101
|
+
route transition or a `RouteTravel` takes its picture of the **fallback**, code
|
|
102
|
+
and data alike. Navi does not hold a navigation for a chunk: the honest answer
|
|
103
|
+
to "a chunk is a hundred milliseconds" is the prefetch above — the code is
|
|
104
|
+
there before the press — and a skeleton that is the page's own shape is what
|
|
105
|
+
gets photographed when it is not.
|
|
106
|
+
|
|
107
|
+
## `import.meta.css` on the far side of a split
|
|
108
|
+
|
|
109
|
+
A module's stylesheet ships **inside its chunk** and is adopted when the
|
|
110
|
+
assignment runs (see [css_architecture.md](./css_architecture.md)). Three
|
|
111
|
+
consequences:
|
|
112
|
+
|
|
113
|
+
- **The fallback cannot use a class the lazy module defines.** It is rendered
|
|
114
|
+
by the eager side, before the chunk exists. The frame of what it stands in
|
|
115
|
+
for — background, border, aspect ratio — belongs to the eager module, the
|
|
116
|
+
lazy one owning only the inside; duplicating the frame into the eager side
|
|
117
|
+
is the same answer.
|
|
118
|
+
- **Layers are what the text says.** `@layer navi` holds navi's defaults
|
|
119
|
+
whether the sheet came with the entry or with a chunk, and an app's unlayered
|
|
120
|
+
rule wins over it from either side.
|
|
121
|
+
- **Between unlayered sheets, the last adopted wins at equal specificity.** A
|
|
122
|
+
chunk's sheet lands after everything already adopted, so a chunk beats the
|
|
123
|
+
entry; between two chunks the order is the order they ran in, which nothing
|
|
124
|
+
in the build fixes. A rule that depends on it is a rule to make more
|
|
125
|
+
specific.
|
|
126
|
+
|
|
127
|
+
## Preact's `lazy()` is not this one
|
|
128
|
+
|
|
129
|
+
`preact/compat` has a `lazy()` too. It suspends without saying anything to
|
|
130
|
+
`<Loading>`: the boundary keeps the last reason it knows — "idle" by default —
|
|
131
|
+
and draws nothing while the module loads; a failure is not an action's, so no
|
|
132
|
+
`rerun` brings the page back. Navi warns in dev when a `<Loading>` catches a
|
|
133
|
+
suspension it cannot attribute to an action. Import `lazy` from navi.
|
|
134
|
+
|
|
135
|
+
## The build
|
|
136
|
+
|
|
137
|
+
Every `import()` becomes a chunk of its own under `js/`, versioned with the
|
|
138
|
+
rest, and a target without dynamic import gets the fallback. Nothing to
|
|
139
|
+
configure.
|
|
140
|
+
|
|
141
|
+
## Reference
|
|
142
|
+
|
|
143
|
+
- `src/nav/demos/code_splitting/code_splitting_demo.html` — two pages fetched
|
|
144
|
+
on demand next to a route action, one prefetched on hover and one opted out,
|
|
145
|
+
with the import drawn on the backend's frontier so both waits can be watched.
|
|
146
|
+
- `src/state/async/lazy.jsx`, `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
|