@jxsuite/server 0.0.1
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/package.json +19 -0
- package/src/build.js +67 -0
- package/src/code-api.js +155 -0
- package/src/resolve.js +206 -0
- package/src/server.js +253 -0
- package/src/studio-api.js +689 -0
- package/src/watch.js +134 -0
package/src/watch.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/** Watch.js — File watcher + SSE live reload */
|
|
2
|
+
|
|
3
|
+
import chokidar from "chokidar";
|
|
4
|
+
import { relative } from "node:path";
|
|
5
|
+
import { rebuild } from "./build.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_IGNORE = [
|
|
8
|
+
"**/node_modules/**",
|
|
9
|
+
"**/dist/**",
|
|
10
|
+
"**/.git/**",
|
|
11
|
+
"**/.devenv/**",
|
|
12
|
+
"**/.direnv/**",
|
|
13
|
+
"**/bun.lockb",
|
|
14
|
+
"**/bun.lock",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/** @param {string} value */
|
|
18
|
+
function normalizePath(value) {
|
|
19
|
+
return value.replaceAll("\\", "/");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} pathname
|
|
24
|
+
* @param {string[]} ignore
|
|
25
|
+
*/
|
|
26
|
+
function shouldIgnore(pathname, ignore) {
|
|
27
|
+
const normalizedPath = normalizePath(pathname);
|
|
28
|
+
return ignore.some((pattern) => {
|
|
29
|
+
const normalizedPattern = normalizePath(pattern);
|
|
30
|
+
if (normalizedPattern.startsWith("**/") && normalizedPattern.endsWith("/**")) {
|
|
31
|
+
const segment = normalizedPattern.slice(3, -3);
|
|
32
|
+
return normalizedPath.includes(`/${segment}/`) || normalizedPath.endsWith(`/${segment}`);
|
|
33
|
+
}
|
|
34
|
+
if (normalizedPattern.startsWith("**/")) {
|
|
35
|
+
const suffix = normalizedPattern.slice(3);
|
|
36
|
+
return normalizedPath.endsWith(`/${suffix}`) || normalizedPath === suffix;
|
|
37
|
+
}
|
|
38
|
+
return normalizedPath.includes(normalizedPattern);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const SSE_SCRIPT = `\n<script>new EventSource('/__reload').onmessage=()=>location.reload()</script>`;
|
|
43
|
+
|
|
44
|
+
/** @param {string} html */
|
|
45
|
+
export function injectSSE(html) {
|
|
46
|
+
return html.includes("</body>")
|
|
47
|
+
? html.replace("</body>", SSE_SCRIPT + "\n</body>")
|
|
48
|
+
: html + SSE_SCRIPT;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create the file watcher + SSE system.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} root - Absolute path to watch
|
|
55
|
+
* @param {any[]} builds - Build entries (for selective rebuild)
|
|
56
|
+
* @param {{ ignore?: string[]; debounce?: number }} [opts]
|
|
57
|
+
* @returns {{ broadcast: () => void; handleSSE: () => Response }}
|
|
58
|
+
*/
|
|
59
|
+
export function createWatcher(root, builds, opts = {}) {
|
|
60
|
+
const ignore = opts.ignore ?? DEFAULT_IGNORE;
|
|
61
|
+
const debounceMs = opts.debounce ?? 50;
|
|
62
|
+
|
|
63
|
+
/** @type {Set<(msg: string) => void>} */
|
|
64
|
+
const clients = new Set();
|
|
65
|
+
const encoder = new TextEncoder();
|
|
66
|
+
|
|
67
|
+
function broadcast() {
|
|
68
|
+
for (const send of clients) send("data: reload\n\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleSSE() {
|
|
72
|
+
/** @type {any} */
|
|
73
|
+
let send;
|
|
74
|
+
const stream = new ReadableStream({
|
|
75
|
+
start(c) {
|
|
76
|
+
send = (/** @type {string} */ msg) => {
|
|
77
|
+
try {
|
|
78
|
+
c.enqueue(encoder.encode(msg));
|
|
79
|
+
} catch {}
|
|
80
|
+
};
|
|
81
|
+
clients.add(send);
|
|
82
|
+
const hb = setInterval(() => {
|
|
83
|
+
try {
|
|
84
|
+
c.enqueue(encoder.encode(": heartbeat\n\n"));
|
|
85
|
+
} catch {
|
|
86
|
+
clearInterval(hb);
|
|
87
|
+
}
|
|
88
|
+
}, 15_000);
|
|
89
|
+
},
|
|
90
|
+
cancel() {
|
|
91
|
+
clients.delete(send);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
return new Response(stream, {
|
|
95
|
+
headers: {
|
|
96
|
+
"Content-Type": "text/event-stream",
|
|
97
|
+
"Cache-Control": "no-cache",
|
|
98
|
+
Connection: "keep-alive",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @type {any} */
|
|
104
|
+
let timer = null;
|
|
105
|
+
const watcher = chokidar.watch(root, {
|
|
106
|
+
ignored: (watchedPath) => shouldIgnore(watchedPath, ignore),
|
|
107
|
+
ignoreInitial: true,
|
|
108
|
+
ignorePermissionErrors: true,
|
|
109
|
+
awaitWriteFinish: {
|
|
110
|
+
stabilityThreshold: debounceMs,
|
|
111
|
+
pollInterval: 10,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
watcher.on("all", (_, changedPath) => {
|
|
116
|
+
const filename = relative(root, changedPath);
|
|
117
|
+
if (!filename || filename.startsWith("..")) return;
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
timer = setTimeout(async () => {
|
|
120
|
+
if (builds.length > 0) {
|
|
121
|
+
const result = await rebuild(builds, filename);
|
|
122
|
+
if (!result.success) return;
|
|
123
|
+
if (result.rebuilt.length > 0) {
|
|
124
|
+
broadcast();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
console.log(`Changed → ${filename}`);
|
|
129
|
+
broadcast();
|
|
130
|
+
}, debounceMs);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return { broadcast, handleSSE };
|
|
134
|
+
}
|