Skip to content

Commit 0c0aaca

Browse files
committed
Migrate to ReScript 12 Rewatch
1 parent 0d8fb0a commit 0c0aaca

96 files changed

Lines changed: 309 additions & 4901 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
yarn install --immutable
3535
- name: Format ReScript
3636
run: |
37-
yarn workspaces foreach run rescript format -all
37+
yarn rescript format
3838
- name: Build all projects
3939
run: |
4040
yarn build

WIP.md

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Plans
22

3-
- [x] SSR Relay support, streaming
43
- [x] Preload data
54
- [x] Preload code
65
- [x] Type safe URL makers
@@ -13,7 +12,7 @@
1312
- [x] Watch routes and rebuild
1413
- [x] Only write if has changes
1514
- [x] Ensure @live is added appropriately to enable dead code analysis
16-
- [x] Explore continously streaming code splitting preload tags
15+
- [x] Explore code splitting preload tags
1716
- [x] Unify router approach (react-router, rescript react router, hand rolled, require shallow push, etc)
1817
- [x] Error handling in vite plugin and CLI commands
1918
- [x] Allow multiple JSON files to make it easier to split up definitions?
@@ -41,8 +40,6 @@
4140
- [ ] [later] Validate and harden routes JSON definition, including JSON schema?
4241
- [ ] [later] Figure out Rollup plugin for linking imports to chunks so we can do proper preloads via modulepreload
4342
- [ ] [later] Extension integration
44-
- [ ] [ssr] Validate streaming SSR with defer/stream
45-
- [ ] [ssr] Package up the streaming etc
4643
- [ ] [later] CLI for checking whether there are unapplied changes (for use with CI, so process.exit)
4744
- [ ] [later] Investigate language server features (go to definition of renderer, find route, etc)
4845
- [ ] [later] Write resi files for routes? (might be cumbersome for some files, as we rely a fair bit on inference)
@@ -54,8 +51,3 @@
5451
- [ ] [wontfix] Inline React router matchers (it's fine to just depend on RR directly, it seems well suited for tree shaking)
5552
- [ ] [wontfix] Default values for query params? Not right now, easy enough to implement when needed
5653
- [ ] [wontfix] Figure out reasonable way to use IDs as polyvariants for tab ids etc (no need for now)
57-
58-
## TODO
59-
60-
- Apply errors to stream
61-
- Figure out a way to test all of this

docs/router-optimization-opportunities.md

Lines changed: 22 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
# Router Optimization Opportunities
22

3-
This document captures a holistic pass over `rescript-relay-router` with a focus on runtime performance, bundle shape, code splitting, SSR asset loading, and maintainability of the hot paths.
3+
This document captures a holistic pass over `rescript-relay-router` with a focus on runtime performance, bundle shape, code splitting, and maintainability of the hot paths.
44

55
The intent is that each section can be picked up as an independent work item. Some items are direct optimizations with low semantic risk. Others are architectural directions that should start with measurement or a narrow proof of concept.
66

77
## Priority Overview
88

99
| Priority | Area | Expected impact | Risk | Suggested owner |
1010
| --- | --- | --- | --- | --- |
11-
| P0 | Centralize manifest-backed asset preloading | High for SSR, hydration, and route transitions | Medium | SSR/build integration |
1211
| P1 | Reuse unchanged prepared matches with explicit freshness policy | High for nested routes with parent queries | High | Relay/runtime |
1312
| P1 | Centralize location subscriptions | Medium to high in nav-heavy UIs | Medium | React runtime |
1413
| P2 | Split route declaration payload from route prepare modules | High in very large apps | High | Codegen/build |
@@ -39,13 +38,12 @@ Before changing behavior, add a repeatable way to measure the current costs:
3938
- Measure number of `prepare` calls per navigation.
4039
- Measure number of history listeners on a page with many active route hooks.
4140
- Measure client bundle size and chunk graph for a realistic app.
42-
- Measure SSR output for preload tag duplication, preload ordering, and hydration timing.
4341

4442
Suggested test fixtures:
4543

4644
- Extend `packages/rescript-relay-router/test/RouterUtils.test.res` for pure route matching and route-key behavior.
4745
- Add focused tests around generated output in `packages/rescript-relay-router/test/RescriptRelayRouterCli.test.res`.
48-
- Add a larger example or benchmark fixture under `examples/` only if it exercises Vite chunk output or SSR manifest behavior.
46+
- Add a larger example or benchmark fixture under `examples/` only if it exercises realistic route trees and Vite chunk output.
4947

5048
Use this baseline to prevent a common failure mode: making the router theoretically cleaner while moving cost from one phase to another without observing the final app behavior.
5149

@@ -94,103 +92,7 @@ uses the compiled matcher for initial matching, history updates, and route prelo
9492

9593
- Future vendored React Router changes still need parity tests because this helper is modified.
9694

97-
## 2. Centralize Manifest-Backed Asset Preloading
98-
99-
### Current State
100-
101-
The router has a preload abstraction:
102-
103-
- `Component`
104-
- `Image`
105-
- `Style`
106-
107-
Client-side preloading currently de-dupes asset identifiers, but only component assets actually do anything:
108-
109-
- `packages/rescript-relay-router/src/RelayRouter__AssetPreloader.res`
110-
111-
SSR preloading is implemented in the Express example:
112-
113-
- `examples/express/src/EntryServer.res`
114-
115-
The example:
116-
117-
- emits `<script type="module" src="...">` for component chunks
118-
- recursively walks direct imports
119-
- emits CSS preload tags
120-
- treats all manifest assets as images, with a TODO noting this can be wrong
121-
- has a TODO for duplicate chunk loads
122-
123-
The Vite manifest transform already creates a router-specific manifest:
124-
125-
- `packages/rescript-relay-router/vite-plugins/RescriptRelayVitePlugin__ManifestTransform.res`
126-
- `packages/rescript-relay-router/src/RelayRouter__Manifest.res`
127-
128-
### Opportunity
129-
130-
Move the default manifest-aware preloading logic into the router package. Make examples consume that default instead of implementing their own.
131-
132-
The router should provide two default preload implementations:
133-
134-
- Client: de-duped `import()` for component chunks, DOM `<link>` insertion for CSS/images/modules when useful.
135-
- Server: de-duped HTML tag emission through `PreloadInsertingStream`.
136-
137-
The core design should make asset preloading explicit and safe:
138-
139-
- Component chunks should generally use `rel="modulepreload"` on the server, not an eager script execution tag, unless there is a deliberate reason to execute.
140-
- CSS should use `rel="stylesheet"` or `rel="preload" as="style"` with clear tradeoffs.
141-
- Images should use `rel="preload" as="image"` only when the asset type is known.
142-
- Unknown assets should either not be preloaded or should carry metadata from the manifest transform.
143-
144-
### Implementation Steps
145-
146-
1. Extend `RelayRouter__Manifest.file` with enough metadata to avoid guessing asset types.
147-
- Option A: store assets as records with `url` and `kind`.
148-
- Option B: split manifest fields into `images`, `fonts`, `assets`, etc.
149-
- Option C: keep raw assets but infer from extension in one central helper.
150-
2. Add a shared `AssetPreloader` helper that can expand a component chunk into:
151-
- the chunk itself
152-
- recursive static imports
153-
- associated CSS
154-
- associated known assets
155-
3. Ensure recursion is cycle-safe and de-duped by URL.
156-
4. Add a server preloader constructor, for example:
157-
158-
```rescript
159-
let makeServerAssetPreloader: (
160-
~manifest: RelayRouter__Manifest.t,
161-
~emit: string => unit,
162-
) => RelayRouter__Types.preloadAssetFn
163-
```
164-
165-
5. Add a client preloader constructor that optionally accepts the manifest:
166-
167-
```rescript
168-
let makeClientAssetPreloader: (
169-
preparedAssetsMap,
170-
~manifest: option<RelayRouter__Manifest.t>=?,
171-
) => RelayRouter__Types.preloadAssetFn
172-
```
173-
174-
6. Replace the custom SSR implementation in `examples/express/src/EntryServer.res` with the router-provided default.
175-
7. Preserve the current user extension point: callers can still pass a custom `preloadAsset`.
176-
8. Add tests for de-dupe and manifest expansion.
177-
178-
### Validation
179-
180-
- SSR output includes no duplicate preload tags for the same URL.
181-
- Component routes emit module preload tags for the exact chunks needed.
182-
- CSS needed by matched chunks is emitted.
183-
- Unknown asset types are not mislabeled as images.
184-
- Existing examples still hydrate.
185-
- Client-side `Image` and `Style` preloads are no longer no-ops.
186-
187-
### Risks
188-
189-
- Browser preload behavior is sensitive. `modulepreload`, `preload`, and actual script execution are not interchangeable.
190-
- Over-preloading can hurt performance. The implementation should respect priority and make aggressive behavior opt-in where possible.
191-
- Vite manifest shape can differ across versions and config options.
192-
193-
## 3. Reuse Unchanged Prepared Matches With an Explicit Freshness Policy
95+
## 2. Reuse Unchanged Prepared Matches With an Explicit Freshness Policy
19496

19597
### Current State
19698

@@ -258,7 +160,7 @@ Defaulting this is the important design decision:
258160
- The policy needs clear docs. Users should understand when reuse is safe.
259161
- A custom policy can become an API surface that is hard to evolve.
260162

261-
## 4. Split the Route Declaration Payload From Lazy Route Prepare Modules
163+
## 3. Split the Route Declaration Payload From Lazy Route Prepare Modules
262164

263165
### Current State
264166

@@ -284,7 +186,6 @@ Split the generated route output into:
284186
1. A small always-loaded match manifest:
285187
- route path
286188
- route name
287-
- route chunk identifier
288189
- children
289190
2. Lazy per-route modules:
290191
- prepare prop decoding
@@ -298,7 +199,7 @@ The runtime matcher only needs the match manifest. The router only needs the laz
298199

299200
1. Define a smaller route matching type, separate from the executable `route` type.
300201
2. Update the matcher binding to operate on this smaller type if needed.
301-
3. Generate a manifest-only `RouteDeclarations.makeManifest()`.
202+
3. Generate a match-only `RouteDeclarations.makeRouteTree()`.
302203
4. Generate per-route loader modules that can produce the executable route behavior when matched.
303204
5. Update `Router.make` to accept either:
304205
- current eager route objects
@@ -311,15 +212,15 @@ The runtime matcher only needs the match manifest. The router only needs the laz
311212
- Initial client bundle shrinks for a large generated route tree.
312213
- Route matching still works without loading unmatched route prepare modules.
313214
- Preloading a link loads only the modules for the matched branch.
314-
- SSR still has enough manifest data to emit correct preload tags.
215+
- The server can still construct the match tree without browser APIs.
315216

316217
### Risks
317218

318219
- This is a larger architectural shift.
319220
- It may complicate type-safe generated route module access.
320221
- Dynamic import boundaries from generated code need careful Vite/Rollup testing.
321222

322-
## 5. Verify and Improve Renderer Chunk Boundaries
223+
## 4. Verify and Improve Renderer Chunk Boundaries
323224

324225
### Current State
325226

@@ -367,7 +268,7 @@ This may produce cleaner and smaller async chunks.
367268
- ReScript or Vite may already tree-shake this well enough, making the change unnecessary.
368269
- The `Routes` access path is more ergonomic. Direct module references are less discoverable.
369270

370-
## 6. Centralize Location Subscriptions
271+
## 5. Centralize Location Subscriptions
371272

372273
### Current State
373274

@@ -419,7 +320,7 @@ The key goal is one history listener per router instance, not one per hook.
419320
- Current router route-entry updates skip shallow navigations. Location subscribers must not accidentally inherit that behavior if query param hooks should update.
420321
- React transition behavior should be reviewed. Some location updates may want transition scheduling, others may not.
421322

422-
## 7. Make Route Keys Collision-Safe
323+
## 6. Make Route Keys Collision-Safe
423324

424325
### Landed Shape
425326

@@ -450,7 +351,7 @@ query param arrays. It distinguishes:
450351

451352
- If current accidental collisions masked bugs, this can expose them.
452353

453-
## 8. Route Declaration Entrypoints
354+
## 7. Route Declaration Entrypoints
454355

455356
### Use Case
456357

@@ -565,7 +466,7 @@ This behavior is preferable to render-time filtering because it scopes matching,
565466
- Generating per-root files can complicate the current generated module structure and LSP helpers.
566467
- If top-level modules share `loadedRouteRenderers`, code loading can be shared across contexts. If they do not, duplicate dynamic import state may be tracked. Decide explicitly.
567468

568-
## 9. Pool Link Intersection Observers
469+
## 8. Pool Link Intersection Observers
569470

570471
### Current State
571472

@@ -618,7 +519,7 @@ The default should be conservative, but the current threshold may be too late to
618519
- Root element identity is awkward as a dictionary key. A JS helper may be cleaner than doing all registry logic in ReScript.
619520
- Early preloading can increase network usage. Defaults should be measured.
620521

621-
## 10. Replace Reflective Disposable Extraction
522+
## 9. Replace Reflective Disposable Extraction
622523

623524
### Current State
624525

@@ -678,7 +579,7 @@ The second option avoids wrapping existing prepare returns but still gives expli
678579
- The API may become noisier for users.
679580
- Having both explicit and reflective behavior can be confusing. Docs should state precedence clearly.
680581

681-
## 11. Reduce Repeated Query Decoding
582+
## 10. Reduce Repeated Query Decoding
682583

683584
### Current State
684585

@@ -730,48 +631,16 @@ The simpler near-term improvement is to reuse parsed `QueryParams.t` across rout
730631
- The cache may need to return fresh copies for mutation-heavy call sites.
731632
- Over-caching can introduce subtle stale-state bugs.
732633

733-
## 12. SSR Stream Logging and Cleanup
734-
735-
### Current State
736-
737-
`RelaySSRUtils` logs a lot of debug information directly:
738-
739-
- `packages/rescript-relay-router/src/RelaySSRUtils.res`
740-
741-
This includes streamed entry events, boot events, and replay subject cleanup. It is not the largest performance issue, but it can add noise and overhead in production SSR/hydration flows.
742-
743-
### Opportunity
744-
745-
Gate debug logs behind a configuration flag or remove them from the default path.
746-
747-
This belongs in the same family as SSR asset preloading because both affect response streaming and hydration behavior.
748-
749-
### Implementation Steps
750-
751-
1. Add a debug flag or logging callback.
752-
2. Default it to disabled.
753-
3. Replace direct `Console.log` calls with the gated logger.
754-
4. Add a test or example check that production usage does not log by default.
755-
756-
### Validation
757-
758-
- No debug logs in normal example app boot.
759-
- Logs can still be enabled for stream debugging.
760-
761-
### Risks
762-
763-
- Debugging SSR streaming is hard. Keep an easy way to re-enable logs.
764-
765634
## Suggested Work Slicing
766635

767636
These work items can be picked up independently:
768637

769-
1. Server/client asset preloader:
770-
- practical SSR and code-splitting payoff
771-
- likely needs manifest type changes
772-
2. Location subscription store:
638+
1. Location subscription store:
773639
- contained React runtime change
774640
- needs care around shallow navigation
641+
2. Named root-tree rendering:
642+
- codegen and runtime scoping change
643+
- useful for multi-entry apps, admin surfaces, and embedded route renderers
775644
3. Link observer pooling:
776645
- contained link component change
777646
- needs browser behavior validation
@@ -789,20 +658,18 @@ These work items can be picked up independently:
789658
- may become a small scaffold generation change
790659
8. Query decoding cache:
791660
- useful after route-key and location-store work clarify ownership of `search`
792-
9. SSR stream logging cleanup:
793-
- small server-runtime cleanup
794-
- should preserve opt-in debugging
795661

796662
## Recommended Starting Order
797663

798664
Start with:
799665

800-
1. Centralized asset preloading.
666+
1. Named root-tree rendering, if multi-entry apps or embedded surfaces are a near-term target.
801667
2. Centralized location subscriptions.
802668
3. Reduce repeated query decoding.
803669
4. Prepare freshness policy, behind an explicit opt-in.
804670
5. Renderer chunk boundary verification before any larger lazy route declaration split.
805671

806-
That order focuses on the remaining SSR/code-splitting payoff first, then reduces router subscription
807-
and query parsing churn before touching Relay freshness semantics. The larger lazy declaration split
808-
should wait for bundle analysis so the architectural work is justified by measured chunk output.
672+
That order improves route-tree boundaries for multi-entry rendering, then reduces router
673+
subscription and query parsing churn before touching Relay freshness semantics. The larger lazy
674+
declaration split should wait for bundle analysis so the architectural work is justified by
675+
measured chunk output.

0 commit comments

Comments
 (0)