@rangojs/router 0.0.0-experimental.57 → 0.0.0-experimental.58

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.
@@ -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.57",
1748
+ version: "0.0.0-experimental.58",
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 previousRawMode = stdin.isTTY ? stdin.isRaw : null;
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
- stdin.setRawMode(true);
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 (data.length !== 1) return;
5293
- if (data[0] === 3) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.57",
3
+ "version": "0.0.0-experimental.58",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -1,8 +1,13 @@
1
1
  import type { Plugin } from "vite";
2
2
 
3
3
  /**
4
- * Vite plugin that triggers a full browser reload when Ctrl+R is pressed
5
- * in the terminal running the dev server.
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
- stdin.setRawMode(true);
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 (data.length !== 1) return;
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
- // Ctrl+C (0x03) defensive fallback. This plugin enables raw mode
38
- // before Vite's internal stdin handler is registered (user plugins
39
- // run first), so there is a brief window where Ctrl+C would be
40
- // swallowed. Re-emit SIGINT so the process exits as expected.
41
- if (data[0] === 0x03) {
42
- process.emit("SIGINT", "SIGINT");
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
- // Ctrl+R = 0x12 in raw mode
47
- if (data[0] === 0x12) {
48
- server.hot.send({ type: "full-reload", path: "*" });
49
- server.config.logger.info(" browser reload (ctrl+r)", {
50
- timestamp: true,
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
  };