@rangojs/router 0.0.0-experimental.57 → 0.0.0-experimental.59
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 +15 -10
- package/dist/vite/index.js +60 -14
- package/package.json +1 -1
- package/src/browser/scroll-restoration.ts +10 -8
- package/src/vite/plugins/refresh-cmd.ts +88 -26
package/README.md
CHANGED
|
@@ -91,24 +91,29 @@ This file is a server/RSC module and should import router construction APIs from
|
|
|
91
91
|
|
|
92
92
|
```tsx
|
|
93
93
|
// src/router.tsx
|
|
94
|
-
import { createRouter
|
|
95
|
-
import { Document } from "./document";
|
|
94
|
+
import { createRouter } from "@rangojs/router";
|
|
96
95
|
|
|
97
|
-
const
|
|
98
|
-
path("/",
|
|
99
|
-
path("
|
|
96
|
+
export const router = createRouter().routes(({ path }) => [
|
|
97
|
+
path("/", HomePage, { name: "home" }),
|
|
98
|
+
path("/about", AboutPage, { name: "about" }),
|
|
100
99
|
]);
|
|
101
100
|
|
|
101
|
+
export const reverse = router.reverse;
|
|
102
|
+
// reverse("home") -> "/"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For larger apps, extract route modules with `urls()` and compose with `include()`:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { createRouter, urls } from "@rangojs/router";
|
|
109
|
+
import { blogPatterns } from "./urls/blog";
|
|
110
|
+
|
|
102
111
|
const urlpatterns = urls(({ path, include }) => [
|
|
103
112
|
path("/", HomePage, { name: "home" }),
|
|
104
113
|
include("/blog", blogPatterns, { name: "blog" }),
|
|
105
114
|
]);
|
|
106
115
|
|
|
107
|
-
export const router = createRouter(
|
|
108
|
-
|
|
109
|
-
// Export typed reverse function for URL generation by route name
|
|
110
|
-
export const reverse = router.reverse;
|
|
111
|
-
|
|
116
|
+
export const router = createRouter().routes(urlpatterns);
|
|
112
117
|
// reverse("blog.post", { slug: "hello-world" }) -> "/blog/hello-world"
|
|
113
118
|
```
|
|
114
119
|
|
package/dist/vite/index.js
CHANGED
|
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
|
|
|
1745
1745
|
// package.json
|
|
1746
1746
|
var package_default = {
|
|
1747
1747
|
name: "@rangojs/router",
|
|
1748
|
-
version: "0.0.0-experimental.
|
|
1748
|
+
version: "0.0.0-experimental.59",
|
|
1749
1749
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1750
1750
|
keywords: [
|
|
1751
1751
|
"react",
|
|
@@ -5284,29 +5284,75 @@ function poke() {
|
|
|
5284
5284
|
apply: "serve",
|
|
5285
5285
|
configureServer(server) {
|
|
5286
5286
|
const stdin = process.stdin;
|
|
5287
|
-
const
|
|
5287
|
+
const debug = process.env.RANGO_POKE_DEBUG === "1";
|
|
5288
|
+
const triggerReload = (source) => {
|
|
5289
|
+
server.hot.send({ type: "full-reload", path: "*" });
|
|
5290
|
+
server.config.logger.info(` browser reload (${source})`, {
|
|
5291
|
+
timestamp: true
|
|
5292
|
+
});
|
|
5293
|
+
};
|
|
5294
|
+
const toBuffer = (chunk) => {
|
|
5295
|
+
return typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
5296
|
+
};
|
|
5297
|
+
const formatChunk = (chunk) => {
|
|
5298
|
+
const data = toBuffer(chunk);
|
|
5299
|
+
const hex = Array.from(data).map((byte) => `0x${byte.toString(16).padStart(2, "0")}`).join(" ");
|
|
5300
|
+
const ascii = Array.from(data).map((byte) => {
|
|
5301
|
+
if (byte >= 32 && byte <= 126) return String.fromCharCode(byte);
|
|
5302
|
+
if (byte === 10) return "\\n";
|
|
5303
|
+
if (byte === 13) return "\\r";
|
|
5304
|
+
if (byte === 9) return "\\t";
|
|
5305
|
+
return ".";
|
|
5306
|
+
}).join("");
|
|
5307
|
+
return `len=${data.length} hex=[${hex}] ascii="${ascii}"`;
|
|
5308
|
+
};
|
|
5309
|
+
const readCtrlR = (chunk) => {
|
|
5310
|
+
const data = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
5311
|
+
return data.length === 1 && data[0] === 18;
|
|
5312
|
+
};
|
|
5313
|
+
const readSubmittedCommands = (chunk) => {
|
|
5314
|
+
const text = toBuffer(chunk).toString("utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
5315
|
+
if (!text.includes("\n")) return [];
|
|
5316
|
+
const lines = text.split("\n");
|
|
5317
|
+
lines.pop();
|
|
5318
|
+
return lines;
|
|
5319
|
+
};
|
|
5320
|
+
if (debug) {
|
|
5321
|
+
server.config.logger.info(
|
|
5322
|
+
` poke debug enabled (isTTY=${stdin.isTTY ? "yes" : "no"}, isRaw=${stdin.isTTY ? stdin.isRaw ? "yes" : "no" : "n/a"})`,
|
|
5323
|
+
{ timestamp: true }
|
|
5324
|
+
);
|
|
5325
|
+
}
|
|
5288
5326
|
if (stdin.isTTY) {
|
|
5289
|
-
|
|
5327
|
+
server.config.logger.info(
|
|
5328
|
+
" poke ready: press e + enter to reload browser (ctrl+r also works when available)",
|
|
5329
|
+
{ timestamp: true }
|
|
5330
|
+
);
|
|
5290
5331
|
}
|
|
5291
5332
|
const onData = (data) => {
|
|
5292
|
-
if (
|
|
5293
|
-
|
|
5294
|
-
process.emit("SIGINT", "SIGINT");
|
|
5295
|
-
return;
|
|
5296
|
-
}
|
|
5297
|
-
if (data[0] === 18) {
|
|
5298
|
-
server.hot.send({ type: "full-reload", path: "*" });
|
|
5299
|
-
server.config.logger.info(" browser reload (ctrl+r)", {
|
|
5333
|
+
if (debug) {
|
|
5334
|
+
server.config.logger.info(` poke stdin ${formatChunk(data)}`, {
|
|
5300
5335
|
timestamp: true
|
|
5301
5336
|
});
|
|
5302
5337
|
}
|
|
5338
|
+
if (readCtrlR(data)) {
|
|
5339
|
+
triggerReload("ctrl+r");
|
|
5340
|
+
return;
|
|
5341
|
+
}
|
|
5342
|
+
for (const command of readSubmittedCommands(data)) {
|
|
5343
|
+
if (command === "e") {
|
|
5344
|
+
triggerReload("e+enter");
|
|
5345
|
+
return;
|
|
5346
|
+
}
|
|
5347
|
+
if (command === "\x1Br") {
|
|
5348
|
+
triggerReload("option+r+enter");
|
|
5349
|
+
return;
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5303
5352
|
};
|
|
5304
5353
|
stdin.on("data", onData);
|
|
5305
5354
|
server.httpServer?.on("close", () => {
|
|
5306
5355
|
stdin.off("data", onData);
|
|
5307
|
-
if (stdin.isTTY && previousRawMode !== null) {
|
|
5308
|
-
stdin.setRawMode(previousRawMode);
|
|
5309
|
-
}
|
|
5310
5356
|
});
|
|
5311
5357
|
}
|
|
5312
5358
|
};
|
package/package.json
CHANGED
|
@@ -356,19 +356,18 @@ export function handleNavigationEnd(options: {
|
|
|
356
356
|
scroll?: boolean;
|
|
357
357
|
isStreaming?: () => boolean;
|
|
358
358
|
}): void {
|
|
359
|
-
if (!initialized) {
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
359
|
const { restore = false, scroll = true, isStreaming } = options;
|
|
364
360
|
|
|
365
|
-
// Don't scroll if explicitly disabled
|
|
366
|
-
if (scroll === false) {
|
|
361
|
+
// Don't scroll if explicitly disabled or not in a browser
|
|
362
|
+
if (scroll === false || typeof window === "undefined") {
|
|
367
363
|
return;
|
|
368
364
|
}
|
|
369
365
|
|
|
370
|
-
//
|
|
371
|
-
|
|
366
|
+
// Save/restore requires initialization (sessionStorage, history state).
|
|
367
|
+
// But basic scroll-to-top and hash scrolling work without it — this
|
|
368
|
+
// matters during cross-app navigation where ScrollRestoration unmounts
|
|
369
|
+
// and remounts, creating a brief window where initialized is false.
|
|
370
|
+
if (restore && initialized) {
|
|
372
371
|
if (restoreScrollPosition({ retryIfStreaming: true, isStreaming })) {
|
|
373
372
|
return;
|
|
374
373
|
}
|
|
@@ -378,6 +377,9 @@ export function handleNavigationEnd(options: {
|
|
|
378
377
|
// Defer hash and scroll-to-top to after React paints the new content,
|
|
379
378
|
// so the user doesn't see the current page jump before the new route appears.
|
|
380
379
|
deferToNextPaint(() => {
|
|
380
|
+
// Re-check: the deferred callback may fire after environment teardown
|
|
381
|
+
if (typeof window === "undefined") return;
|
|
382
|
+
|
|
381
383
|
// Try hash scrolling first
|
|
382
384
|
if (scrollToHash()) {
|
|
383
385
|
return;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Vite plugin that triggers a full browser reload
|
|
5
|
-
*
|
|
4
|
+
* Vite plugin that triggers a full browser reload from terminal input.
|
|
5
|
+
*
|
|
6
|
+
* This plugin is intentionally passive:
|
|
7
|
+
* - it never enables raw mode on stdin
|
|
8
|
+
* - it never restores terminal state
|
|
9
|
+
* - it reacts to Ctrl+R when that raw byte reaches the process
|
|
10
|
+
* - it also supports safe line-based fallbacks like "e" + Enter
|
|
6
11
|
*
|
|
7
12
|
* Usage:
|
|
8
13
|
* ```ts
|
|
@@ -20,35 +25,95 @@ export function poke(): Plugin {
|
|
|
20
25
|
|
|
21
26
|
configureServer(server) {
|
|
22
27
|
const stdin = process.stdin;
|
|
28
|
+
const debug = process.env.RANGO_POKE_DEBUG === "1";
|
|
29
|
+
|
|
30
|
+
const triggerReload = (source: string) => {
|
|
31
|
+
server.hot.send({ type: "full-reload", path: "*" });
|
|
32
|
+
server.config.logger.info(` browser reload (${source})`, {
|
|
33
|
+
timestamp: true,
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const toBuffer = (chunk: string | Buffer): Buffer => {
|
|
38
|
+
return typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const formatChunk = (chunk: string | Buffer): string => {
|
|
42
|
+
const data = toBuffer(chunk);
|
|
43
|
+
const hex = Array.from(data)
|
|
44
|
+
.map((byte) => `0x${byte.toString(16).padStart(2, "0")}`)
|
|
45
|
+
.join(" ");
|
|
46
|
+
const ascii = Array.from(data)
|
|
47
|
+
.map((byte) => {
|
|
48
|
+
if (byte >= 0x20 && byte <= 0x7e) return String.fromCharCode(byte);
|
|
49
|
+
if (byte === 0x0a) return "\\n";
|
|
50
|
+
if (byte === 0x0d) return "\\r";
|
|
51
|
+
if (byte === 0x09) return "\\t";
|
|
52
|
+
return ".";
|
|
53
|
+
})
|
|
54
|
+
.join("");
|
|
55
|
+
return `len=${data.length} hex=[${hex}] ascii="${ascii}"`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const readCtrlR = (chunk: string | Buffer): boolean => {
|
|
59
|
+
const data =
|
|
60
|
+
typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
61
|
+
return data.length === 1 && data[0] === 0x12;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const readSubmittedCommands = (chunk: string | Buffer): string[] => {
|
|
65
|
+
const text = toBuffer(chunk)
|
|
66
|
+
.toString("utf8")
|
|
67
|
+
.replace(/\r\n/g, "\n")
|
|
68
|
+
.replace(/\r/g, "\n");
|
|
69
|
+
|
|
70
|
+
if (!text.includes("\n")) return [];
|
|
71
|
+
|
|
72
|
+
const lines = text.split("\n");
|
|
73
|
+
lines.pop();
|
|
74
|
+
return lines;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (debug) {
|
|
78
|
+
server.config.logger.info(
|
|
79
|
+
` poke debug enabled (isTTY=${stdin.isTTY ? "yes" : "no"}, isRaw=${stdin.isTTY ? (stdin.isRaw ? "yes" : "no") : "n/a"})`,
|
|
80
|
+
{ timestamp: true },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
23
83
|
|
|
24
|
-
// Raw mode delivers individual keystrokes as immediate single-byte
|
|
25
|
-
// events instead of waiting for Enter (cooked/line-buffered mode).
|
|
26
|
-
// Without it, Ctrl+R (0x12) is never delivered as a discrete byte.
|
|
27
|
-
// When stdin is a pipe (CI, spawned process) setRawMode is unavailable
|
|
28
|
-
// but data already arrives unbuffered, so the isTTY guard suffices.
|
|
29
|
-
const previousRawMode = stdin.isTTY ? stdin.isRaw : null;
|
|
30
84
|
if (stdin.isTTY) {
|
|
31
|
-
|
|
85
|
+
server.config.logger.info(
|
|
86
|
+
" poke ready: press e + enter to reload browser (ctrl+r also works when available)",
|
|
87
|
+
{ timestamp: true },
|
|
88
|
+
);
|
|
32
89
|
}
|
|
33
90
|
|
|
34
|
-
const onData = (data: Buffer) => {
|
|
35
|
-
if (
|
|
91
|
+
const onData = (data: string | Buffer) => {
|
|
92
|
+
if (debug) {
|
|
93
|
+
server.config.logger.info(` poke stdin ${formatChunk(data)}`, {
|
|
94
|
+
timestamp: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
36
97
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
if (data
|
|
42
|
-
|
|
98
|
+
// Only react to the exact Ctrl+R byte when some host terminal or
|
|
99
|
+
// wrapper already delivers it to this process. We intentionally do
|
|
100
|
+
// not enable raw mode here because that can steal Vite shortcuts
|
|
101
|
+
// like "r" / "q" and interfere with terminal-level controls.
|
|
102
|
+
if (readCtrlR(data)) {
|
|
103
|
+
triggerReload("ctrl+r");
|
|
43
104
|
return;
|
|
44
105
|
}
|
|
45
106
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
107
|
+
for (const command of readSubmittedCommands(data)) {
|
|
108
|
+
if (command === "e") {
|
|
109
|
+
triggerReload("e+enter");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (command === "\u001br") {
|
|
114
|
+
triggerReload("option+r+enter");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
52
117
|
}
|
|
53
118
|
};
|
|
54
119
|
|
|
@@ -56,9 +121,6 @@ export function poke(): Plugin {
|
|
|
56
121
|
|
|
57
122
|
server.httpServer?.on("close", () => {
|
|
58
123
|
stdin.off("data", onData);
|
|
59
|
-
if (stdin.isTTY && previousRawMode !== null) {
|
|
60
|
-
stdin.setRawMode(previousRawMode);
|
|
61
|
-
}
|
|
62
124
|
});
|
|
63
125
|
},
|
|
64
126
|
};
|