hoversource 1.0.0

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.
@@ -0,0 +1,263 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { recordPatchState, removePatchState } from "../utils/patchState.js";
4
+ const patchedReactRuntimesList = [];
5
+ function patchVendoredReactRuntime(content, relPath, projectRoot) {
6
+ const exportsIdx = content.indexOf("module.exports");
7
+ if (exportsIdx === -1) {
8
+ return null;
9
+ }
10
+ const equalsIdx = content.indexOf("=", exportsIdx);
11
+ const endIdx = content.indexOf(";", exportsIdx);
12
+ if (equalsIdx === -1 || endIdx === -1 || equalsIdx >= endIdx) {
13
+ return null;
14
+ }
15
+ const originalExpr = content.slice(equalsIdx + 1, endIdx).trim();
16
+ const isDev = relPath.includes("dev");
17
+ let wrappedContent = "";
18
+ if (isDev) {
19
+ wrappedContent = `
20
+ const original = ${originalExpr};
21
+ exports.Fragment = original.Fragment;
22
+ exports.jsxDEV = function(type, config, maybeKey, isStaticChildren, source, self) {
23
+ globalThis.__HOVERSOURCE_INJECT_SOURCE__(type, config, source);
24
+ return original.jsxDEV(type, config, maybeKey, isStaticChildren, source, self);
25
+ };
26
+ `;
27
+ }
28
+ else {
29
+ wrappedContent = `
30
+ const original = ${originalExpr};
31
+ exports.Fragment = original.Fragment;
32
+ exports.jsx = function(type, config, maybeKey, ...args) {
33
+ globalThis.__HOVERSOURCE_INJECT_SOURCE__(type, config);
34
+ return original.jsx.call(this, type, config, maybeKey, ...args);
35
+ };
36
+ exports.jsxs = function(type, config, maybeKey, ...args) {
37
+ globalThis.__HOVERSOURCE_INJECT_SOURCE__(type, config);
38
+ return original.jsxs.call(this, type, config, maybeKey, ...args);
39
+ };
40
+ `;
41
+ }
42
+ return (content.includes('"use strict";') ? '"use strict";\n' : "") + `
43
+ // HoverSource Injection Patch
44
+ if (!globalThis.__HOVERSOURCE_INJECT_SOURCE__) {
45
+ globalThis.__HOVERSOURCE_INJECT_SOURCE__ = (function() {
46
+ const path = require("path");
47
+ let ssrLogCount = 0;
48
+
49
+ function getJSXSourceLocation(type) {
50
+ const err = new Error();
51
+ const stack = err.stack || "";
52
+
53
+ if (typeof window === "undefined" && ssrLogCount < 200) {
54
+ ssrLogCount++;
55
+ try {
56
+ const lines = stack.split("\\n");
57
+ for (let i = 1; i < lines.length; i++) {
58
+ const line = lines[i];
59
+ if (!line) continue;
60
+ const normalizedLine = line.replace(/\\\\/g, "/");
61
+ const isFramework =
62
+ line.includes("jsxDEV") ||
63
+ line.includes("jsxsDEV") ||
64
+ line.includes("node:internal") ||
65
+ line.includes("Module._compile") ||
66
+ line.includes("next-dev-server") ||
67
+ /node_modules[\\/_]react[\\/_]/i.test(normalizedLine) ||
68
+ /node_modules[\\/_]react-dom[\\/_]/i.test(normalizedLine) ||
69
+ /node_modules[\\/_]next[\\/_]/i.test(normalizedLine) ||
70
+ /node_modules[\\/_]scheduler[\\/_]/i.test(normalizedLine) ||
71
+ line.includes("react-jsx-dev-runtime") ||
72
+ line.includes("react-jsx-runtime");
73
+
74
+ if (isFramework) {
75
+ continue;
76
+ }
77
+ const match = line.match(/(?:at\\\\s+.*?\\\\s+\\\\(|at\\\\s+)(.*?):(\\\\d+):(\\\\d+)\\\\)?$/);
78
+ if (match) {
79
+ return {
80
+ fileName: match[1],
81
+ lineNumber: parseInt(match[2], 10),
82
+ columnNumber: parseInt(match[3], 10)
83
+ };
84
+ }
85
+ }
86
+ } catch {}
87
+ }
88
+ return null;
89
+ }
90
+
91
+ return function(type, config, source) {
92
+ if (!config || typeof type !== "string") return;
93
+ if (config["data-hoversource-loc"]) return;
94
+
95
+ const loc = source || getJSXSourceLocation(type);
96
+ if (loc) {
97
+ let filePath = loc.fileName || "";
98
+ try {
99
+ if (path.isAbsolute(filePath)) {
100
+ filePath = path.relative(process.cwd(), filePath).replace(/\\\\/g, "/");
101
+ }
102
+ } catch {}
103
+ config["data-hoversource-loc"] = \`\${filePath}:\${loc.lineNumber}:\${loc.columnNumber}\`;
104
+ }
105
+ };
106
+ })();
107
+ }
108
+ ${wrappedContent}
109
+ `;
110
+ }
111
+ function patchSingleReactRuntime(fullPath, relPath, projectRoot) {
112
+ try {
113
+ const content = fs.readFileSync(fullPath, "utf-8");
114
+ if (content.includes("HoverSource Injection Patch")) {
115
+ return;
116
+ }
117
+ let newContent = content;
118
+ if (relPath.includes("vendored")) {
119
+ const patched = patchVendoredReactRuntime(content, relPath, projectRoot);
120
+ if (patched) {
121
+ newContent = patched;
122
+ }
123
+ }
124
+ else {
125
+ // 1. For anonymous function exports, prepend injection as the first statement in the body
126
+ newContent = newContent.replace(/exports\.jsxDEV\s*=\s*function\s*\(\s*type,\s*config,\s*maybeKey,\s*isStaticChildren\s*\)\s*\{/g, "exports.jsxDEV = function(type, config, maybeKey, isStaticChildren) { globalThis.__HOVERSOURCE_INJECT_SOURCE__(type, config, arguments[4]);");
127
+ newContent = newContent.replace(/exports\.jsx\s*=\s*function\s*\(\s*type,\s*config,\s*maybeKey\s*\)\s*\{/g, "exports.jsx = function(type, config, maybeKey) { globalThis.__HOVERSOURCE_INJECT_SOURCE__(type, config);");
128
+ newContent = newContent.replace(/exports\.jsxs\s*=\s*function\s*\(\s*type,\s*config,\s*maybeKey\s*\)\s*\{/g, "exports.jsxs = function(type, config, maybeKey) { globalThis.__HOVERSOURCE_INJECT_SOURCE__(type, config);");
129
+ // 2. For variable assignments, replace with top-level wrappers at module scope
130
+ newContent = newContent.replace("exports.jsxDEV = jsxDEV$1;", "exports.jsxDEV = function(t, c, k, s, ...args) { globalThis.__HOVERSOURCE_INJECT_SOURCE__(t, c, s); return jsxDEV$1(t, c, k, s, ...args); };");
131
+ newContent = newContent.replace("exports.jsxDEV = jsxDEV;", "exports.jsxDEV = function(t, c, k, s, ...args) { globalThis.__HOVERSOURCE_INJECT_SOURCE__(t, c, s); return jsxDEV(t, c, k, s, ...args); };");
132
+ newContent = newContent.replace("exports.jsx = jsx;", "exports.jsx = function(t, c, k, ...args) { globalThis.__HOVERSOURCE_INJECT_SOURCE__(t, c); return jsx(t, c, k, ...args); };");
133
+ newContent = newContent.replace("exports.jsx = jsxWithValidationDynamic;", "exports.jsx = function(t, c, k, ...args) { globalThis.__HOVERSOURCE_INJECT_SOURCE__(t, c); return jsxWithValidationDynamic(t, c, k, ...args); };");
134
+ newContent = newContent.replace("exports.jsxs = jsxs;", "exports.jsxs = function(t, c, k, ...args) { globalThis.__HOVERSOURCE_INJECT_SOURCE__(t, c); return jsxs(t, c, k, ...args); };");
135
+ // 3. Append the global helper block to the end of the file
136
+ newContent = newContent + `
137
+ // HoverSource Injection Patch
138
+ globalThis.__HOVERSOURCE_INJECT_SOURCE__ = (function() {
139
+ const path = require("path");
140
+ let ssrLogCount = 0;
141
+
142
+ function getJSXSourceLocation(type) {
143
+ const err = new Error();
144
+ const stack = err.stack || "";
145
+
146
+ if (typeof window === "undefined" && ssrLogCount < 200) {
147
+ ssrLogCount++;
148
+ try {
149
+ const lines = stack.split("\\n");
150
+ for (let i = 1; i < lines.length; i++) {
151
+ const line = lines[i];
152
+ if (!line) continue;
153
+ const normalizedLine = line.replace(/\\\\/g, "/");
154
+ const isFramework =
155
+ line.includes("jsxDEV") ||
156
+ line.includes("jsxsDEV") ||
157
+ line.includes("node:internal") ||
158
+ line.includes("Module._compile") ||
159
+ line.includes("next-dev-server") ||
160
+ /node_modules[\\/_]react[\\/_]/i.test(normalizedLine) ||
161
+ /node_modules[\\/_]react-dom[\\/_]/i.test(normalizedLine) ||
162
+ /node_modules[\\/_]next[\\/_]/i.test(normalizedLine) ||
163
+ /node_modules[\\/_]scheduler[\\/_]/i.test(normalizedLine) ||
164
+ line.includes("react-jsx-dev-runtime") ||
165
+ line.includes("react-jsx-runtime");
166
+
167
+ if (isFramework) {
168
+ continue;
169
+ }
170
+ const match = line.match(/(?:at\\\\s+.*?\\\\s+\\\\(|at\\\\s+)(.*?):(\\\\d+):(\\\\d+)\\\\)?$/);
171
+ if (match) {
172
+ return {
173
+ fileName: match[1],
174
+ lineNumber: parseInt(match[2], 10),
175
+ columnNumber: parseInt(match[3], 10)
176
+ };
177
+ }
178
+ }
179
+ } catch {}
180
+ }
181
+ return null;
182
+ }
183
+
184
+ return function(type, config, source) {
185
+ if (!config || typeof type !== "string") return;
186
+ if (config["data-hoversource-loc"]) return;
187
+
188
+ const loc = source || getJSXSourceLocation(type);
189
+ if (loc) {
190
+ let filePath = loc.fileName || "";
191
+ try {
192
+ if (path.isAbsolute(filePath)) {
193
+ filePath = path.relative(process.cwd(), filePath).replace(/\\\\/g, "/");
194
+ }
195
+ } catch {}
196
+ config["data-hoversource-loc"] = \`\${filePath}:\${loc.lineNumber}:\${loc.columnNumber}\`;
197
+ }
198
+ };
199
+ })();
200
+ `;
201
+ }
202
+ if (newContent !== content) {
203
+ recordPatchState(fullPath, content);
204
+ patchedReactRuntimesList.push({ path: fullPath, originalContent: content });
205
+ fs.writeFileSync(fullPath, newContent, "utf-8");
206
+ console.log(`[HoverSource] Temporarily patched React development runtime on disk: ${path.relative(projectRoot, fullPath)}`);
207
+ }
208
+ }
209
+ catch (err) {
210
+ console.warn(`[HoverSource] Failed to patch React runtime in ${path.relative(projectRoot, fullPath)}:`, err.message);
211
+ }
212
+ }
213
+ export class ReactRuntimePatcher {
214
+ patch(projectRoot) {
215
+ process.once("exit", () => this.restore());
216
+ const searchDirs = [
217
+ projectRoot,
218
+ path.join(projectRoot, "apps/web")
219
+ ];
220
+ const relativePaths = [
221
+ "node_modules/react/cjs/react-jsx-dev-runtime.development.js",
222
+ "node_modules/next/dist/compiled/react/cjs/react-jsx-dev-runtime.development.js",
223
+ "node_modules/next/dist/compiled/react-experimental/cjs/react-jsx-dev-runtime.development.js",
224
+ "node_modules/next/dist/compiled/react/cjs/react-jsx-dev-runtime.react-server.development.js",
225
+ "node_modules/next/dist/compiled/react-experimental/cjs/react-jsx-dev-runtime.react-server.development.js",
226
+ "node_modules/react/cjs/react-jsx-runtime.development.js",
227
+ "node_modules/next/dist/compiled/react/cjs/react-jsx-runtime.development.js",
228
+ "node_modules/next/dist/compiled/react-experimental/cjs/react-jsx-runtime.development.js",
229
+ "node_modules/next/dist/compiled/react/cjs/react-jsx-runtime.react-server.development.js",
230
+ "node_modules/next/dist/compiled/react-experimental/cjs/react-jsx-runtime.react-server.development.js",
231
+ // Next.js App Router vendored runtimes
232
+ "node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime.js",
233
+ "node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-runtime.js",
234
+ "node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.js",
235
+ "node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-jsx-runtime.js",
236
+ "node_modules/next/dist/esm/server/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime.js",
237
+ "node_modules/next/dist/esm/server/route-modules/app-page/vendored/rsc/react-jsx-runtime.js",
238
+ "node_modules/next/dist/esm/server/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.js",
239
+ "node_modules/next/dist/esm/server/route-modules/app-page/vendored/ssr/react-jsx-runtime.js"
240
+ ];
241
+ for (const dir of searchDirs) {
242
+ for (const relPath of relativePaths) {
243
+ const fullPath = path.resolve(dir, relPath);
244
+ if (fs.existsSync(fullPath)) {
245
+ patchSingleReactRuntime(fullPath, relPath, projectRoot);
246
+ }
247
+ }
248
+ }
249
+ }
250
+ restore() {
251
+ for (const file of patchedReactRuntimesList) {
252
+ try {
253
+ fs.writeFileSync(file.path, file.originalContent, "utf-8");
254
+ removePatchState(file.path);
255
+ console.log(`[HoverSource] Restored React development runtime: ${file.path}`);
256
+ }
257
+ catch (err) {
258
+ console.error(`[HoverSource] Failed to restore React runtime ${file.path}:`, err.message);
259
+ }
260
+ }
261
+ patchedReactRuntimesList.length = 0;
262
+ }
263
+ }
@@ -0,0 +1,4 @@
1
+ export interface RuntimePatcher {
2
+ patch(projectRoot: string): void;
3
+ restore(): void;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ export interface ProxyPipelineOptions {
2
+ overlayScriptUrl: string;
3
+ }
4
+ export declare class ProxyResponsePipeline {
5
+ /**
6
+ * Checks if the response content type needs to be transformed (i.e. is HTML).
7
+ */
8
+ shouldTransform(contentType: string): boolean;
9
+ /**
10
+ * Decompresses the response body based on contentEncoding, injects the overlay script,
11
+ * and returns the transformed uncompressed Buffer.
12
+ */
13
+ transform(body: Buffer, contentEncoding: string, options: ProxyPipelineOptions): Buffer;
14
+ }
@@ -0,0 +1,45 @@
1
+ import zlib from "node:zlib";
2
+ export class ProxyResponsePipeline {
3
+ /**
4
+ * Checks if the response content type needs to be transformed (i.e. is HTML).
5
+ */
6
+ shouldTransform(contentType) {
7
+ return contentType.toLowerCase().includes("text/html");
8
+ }
9
+ /**
10
+ * Decompresses the response body based on contentEncoding, injects the overlay script,
11
+ * and returns the transformed uncompressed Buffer.
12
+ */
13
+ transform(body, contentEncoding, options) {
14
+ if (body.length === 0) {
15
+ return body;
16
+ }
17
+ let buffer = body;
18
+ const encoding = (contentEncoding || "").toLowerCase().trim();
19
+ try {
20
+ if (encoding.includes("gzip")) {
21
+ buffer = zlib.gunzipSync(buffer);
22
+ }
23
+ else if (encoding.includes("deflate")) {
24
+ buffer = zlib.inflateSync(buffer);
25
+ }
26
+ else if (encoding.includes("br")) {
27
+ buffer = zlib.brotliDecompressSync(buffer);
28
+ }
29
+ }
30
+ catch (err) {
31
+ console.error(`[HoverSource Proxy] Failed to decompress body:`, err.message);
32
+ }
33
+ let html = buffer.toString("utf-8");
34
+ // Strip integrity attributes to avoid SRI mismatches when injecting or modifying scripts/styles
35
+ html = html.replace(/\sintegrity\s*=\s*(?:"[^"]*"|'[^']*'|[^'"\s>]+)/gi, "");
36
+ const injection = `<script src="${options.overlayScriptUrl}"></script>`;
37
+ if (html.includes("</body>")) {
38
+ html = html.replace("</body>", `${injection}\n</body>`);
39
+ }
40
+ else {
41
+ html += `\n${injection}`;
42
+ }
43
+ return Buffer.from(html, "utf-8");
44
+ }
45
+ }
package/dist/port.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Port resolution module for HoverSource CLI.
3
+ *
4
+ * Consolidates all port-checking, port-finding, and conflict-resolution logic.
5
+ * The two main entry points are:
6
+ * - `resolveDevServerPort()` — ensures the dev port is free and returns env/args to inject
7
+ * - `resolveAllPorts()` — one-shot collision-free resolution of all ports HS needs
8
+ */
9
+ export interface DevServerPortResult {
10
+ /** The port the dev server will actually listen on. */
11
+ port: number;
12
+ /** Env vars to inject into the spawned process (e.g. { PORT: "5174" }). */
13
+ env: Record<string, string>;
14
+ /** Extra CLI args for frameworks that prefer --port (appended to spawn args). */
15
+ extraArgs: string[];
16
+ }
17
+ export interface PortPlan {
18
+ companionPort: number;
19
+ devServerPort: number;
20
+ debugPort: number;
21
+ proxyPort: number;
22
+ /** Env vars to inject into the spawned dev server process. */
23
+ devServerEnv: Record<string, string>;
24
+ /** Extra CLI args to append to the spawned dev server command. */
25
+ devServerExtraArgs: string[];
26
+ }
27
+ export declare function probeHostPort(port: number, host: string): Promise<boolean>;
28
+ export declare function isPortFree(port: number): Promise<boolean>;
29
+ export declare function findFreePort(startPort: number, excludePorts?: number | number[], maxPort?: number): Promise<number>;
30
+ export declare function getPidUsingPort(port: number): Promise<number | undefined>;
31
+ export declare function getProcessName(pid: number): Promise<string | undefined>;
32
+ export declare function killProcess(pid: number, port: number): Promise<boolean>;
33
+ export declare function askQuestion(query: string): Promise<string>;
34
+ export declare function isZombieOfProject(pid: number, projectRoot: string): Promise<boolean>;
35
+ /**
36
+ * If the requested port is occupied by another HoverSource instance, tell it
37
+ * to shut down and wait for the port to free up so we can reuse it.
38
+ * Returns the port we should actually bind to.
39
+ */
40
+ export declare function resolveCompanionPort(requestedPort: number, targetPort?: number): Promise<number>;
41
+ /**
42
+ * Ensures the dev server port is free before the app launches.
43
+ *
44
+ * Two distinct strategies based on mode:
45
+ * - **web**: Shift to next free port + inject PORT/VITE_PORT env vars. Works because
46
+ * web frameworks read these env vars to determine which port to bind.
47
+ * - **electron**: MUST free the original port. Port shifting won't work because Electron
48
+ * projects embed the dev server inside concurrently/scripts with hardcoded port references
49
+ * (e.g. VITE_DEV_SERVER_URL=http://localhost:5173). For Electron, the function will:
50
+ * 1. Auto-kill zombie processes from the same project
51
+ * 2. Prompt to kill any other blocker (default: YES, since there's no alternative)
52
+ * 3. In non-interactive mode, auto-kill the blocker
53
+ */
54
+ export declare function resolveDevServerPort(opts: {
55
+ projectRoot: string;
56
+ execCommand: string;
57
+ expectedPort: number;
58
+ mode: "web" | "electron";
59
+ autoResolve: boolean;
60
+ /** Ports already claimed by other HS subsystems — will be excluded from candidates. */
61
+ excludePorts?: number[];
62
+ }): Promise<DevServerPortResult>;
63
+ /**
64
+ * Resolves all ports HS needs in one call. Guarantees no collisions between
65
+ * companion, dev server, debug, and proxy ports. Returns a PortPlan.
66
+ */
67
+ export declare function resolveAllPorts(opts: {
68
+ projectRoot: string;
69
+ execCommand?: string;
70
+ requestedCompanionPort: number;
71
+ expectedDevPort: number;
72
+ debugPort: number;
73
+ requestedProxyPort?: number;
74
+ mode: "web" | "electron";
75
+ autoResolve: boolean;
76
+ }): Promise<PortPlan>;