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,21 @@
1
+ const path = require("path");
2
+ const wrapperPath = module.filename;
3
+ const originalPath = (globalThis.__HOVERSOURCE_ORIGINAL_CJS_RUNTIMES__ && globalThis.__HOVERSOURCE_ORIGINAL_CJS_RUNTIMES__[wrapperPath]) || "react/jsx-dev-runtime";
4
+ const original = require(originalPath);
5
+
6
+ exports.Fragment = original.Fragment;
7
+
8
+ exports.jsxDEV = function(type, props, key, isStaticChildren, source, self) {
9
+ if (typeof window === "undefined" && source && props && typeof type === "string") {
10
+ let filePath = source.fileName || "";
11
+ try {
12
+ const relative = path.relative(process.cwd(), filePath).replace(/\\/g, "/");
13
+ filePath = relative;
14
+ } catch {}
15
+
16
+ props["data-hoversource-loc"] = `${filePath}:${source.lineNumber}:${source.columnNumber}`;
17
+ }
18
+ return original.jsxDEV(type, props, key, isStaticChildren, source, self);
19
+ };
20
+
21
+ exports.jsxsDEV = original.jsxsDEV || exports.jsxDEV;
@@ -0,0 +1,3 @@
1
+ export declare const Fragment: any;
2
+ export declare function jsxDEV(type: any, props: any, key: any, isStaticChildren: any, source: any, self: any): any;
3
+ export declare const jsxsDEV: any;
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+ const urlParams = new URL(import.meta.url).searchParams;
3
+ const originalUrl = urlParams.get("original");
4
+ // Dynamically import the original module using top-level await
5
+ // @ts-ignore
6
+ const original = originalUrl ? await import(originalUrl) : await import("react/jsx-dev-runtime");
7
+ export const Fragment = original.Fragment;
8
+ export function jsxDEV(type, props, key, isStaticChildren, source, self) {
9
+ if (globalThis.window === undefined && source && props && typeof type === "string") {
10
+ let filePath = source.fileName || "";
11
+ try {
12
+ // Normalize absolute path to relative path based on process CWD
13
+ const relative = path.relative(process.cwd(), filePath).replaceAll("\\", "/");
14
+ filePath = relative;
15
+ }
16
+ catch { }
17
+ props["data-hoversource-loc"] = `${filePath}:${source.lineNumber}:${source.columnNumber}`;
18
+ }
19
+ return original.jsxDEV(type, props, key, isStaticChildren, source, self);
20
+ }
21
+ export const jsxsDEV = original.jsxsDEV || jsxDEV;
@@ -0,0 +1,76 @@
1
+ const path = require("path");
2
+ const wrapperPath = module.filename;
3
+ const originalPath = (globalThis.__HOVERSOURCE_ORIGINAL_CJS_RUNTIMES__ && globalThis.__HOVERSOURCE_ORIGINAL_CJS_RUNTIMES__[wrapperPath]) || "react/jsx-runtime";
4
+ const original = require(originalPath);
5
+
6
+ exports.Fragment = original.Fragment;
7
+
8
+ let ssrLogCount = 0;
9
+ function getJSXSourceLocation(type) {
10
+ const err = new Error();
11
+ const stack = err.stack || "";
12
+
13
+ if (typeof window === "undefined" && ssrLogCount < 200) {
14
+ ssrLogCount++;
15
+ try {
16
+ const lines = stack.split("\n");
17
+ for (let i = 1; i < lines.length; i++) {
18
+ const line = lines[i];
19
+ if (!line) continue;
20
+ const normalizedLine = line.replace(/\\\\/g, "/");
21
+ const isFramework =
22
+ line.includes("jsxDEV") ||
23
+ line.includes("jsxsDEV") ||
24
+ line.includes("node:internal") ||
25
+ line.includes("Module._compile") ||
26
+ line.includes("next-dev-server") ||
27
+ /node_modules[\\/_]react[\\/_]/i.test(normalizedLine) ||
28
+ /node_modules[\\/_]react-dom[\\/_]/i.test(normalizedLine) ||
29
+ /node_modules[\\/_]next[\\/_]/i.test(normalizedLine) ||
30
+ /node_modules[\\/_]scheduler[\\/_]/i.test(normalizedLine) ||
31
+ line.includes("react-jsx-dev-runtime") ||
32
+ line.includes("react-jsx-runtime") ||
33
+ line.includes("custom-jsx-runtime") ||
34
+ line.includes("custom-jsx-dev-runtime");
35
+
36
+ if (isFramework) {
37
+ continue;
38
+ }
39
+ const match = line.match(/(?:at\\s+.*?\\s+\\(|at\\s+)(.*?):(\\d+):(\\d+)\\)?$/);
40
+ if (match) {
41
+ return {
42
+ fileName: match[1],
43
+ lineNumber: parseInt(match[2], 10),
44
+ columnNumber: parseInt(match[3], 10)
45
+ };
46
+ }
47
+ }
48
+ } catch {}
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function injectSource(type, config) {
54
+ if (config && typeof type === "string") {
55
+ const source = getJSXSourceLocation(type);
56
+ if (source) {
57
+ let filePath = source.fileName || "";
58
+ try {
59
+ if (path.isAbsolute(filePath)) {
60
+ filePath = path.relative(process.cwd(), filePath).replace(/\\/g, "/");
61
+ }
62
+ } catch {}
63
+ config["data-hoversource-loc"] = `${filePath}:${source.lineNumber}:${source.columnNumber}`;
64
+ }
65
+ }
66
+ }
67
+
68
+ exports.jsx = function(type, config, maybeKey, ...args) {
69
+ injectSource(type, config);
70
+ return original.jsx.call(this, type, config, maybeKey, ...args);
71
+ };
72
+
73
+ exports.jsxs = function(type, config, maybeKey, ...args) {
74
+ injectSource(type, config);
75
+ return original.jsxs.call(this, type, config, maybeKey, ...args);
76
+ };
@@ -0,0 +1,3 @@
1
+ export declare const Fragment: any;
2
+ export declare function jsx(type: any, config: any, maybeKey: any, ...args: any[]): any;
3
+ export declare function jsxs(type: any, config: any, maybeKey: any, ...args: any[]): any;
@@ -0,0 +1,72 @@
1
+ import path from "node:path";
2
+ const urlParams = new URL(import.meta.url).searchParams;
3
+ const originalUrl = urlParams.get("original");
4
+ // @ts-ignore
5
+ const original = originalUrl ? await import(originalUrl) : await import("react/jsx-runtime");
6
+ export const Fragment = original.Fragment;
7
+ let ssrLogCount = 0;
8
+ function getJSXSourceLocation(type) {
9
+ const err = new Error();
10
+ const stack = err.stack || "";
11
+ if (typeof window === "undefined" && ssrLogCount < 200) {
12
+ ssrLogCount++;
13
+ try {
14
+ const lines = stack.split("\n");
15
+ for (let i = 1; i < lines.length; i++) {
16
+ const line = lines[i];
17
+ if (!line)
18
+ continue;
19
+ const normalizedLine = line.replace(/\\/g, "/");
20
+ const isFramework = line.includes("jsxDEV") ||
21
+ line.includes("jsxsDEV") ||
22
+ line.includes("node:internal") ||
23
+ line.includes("Module._compile") ||
24
+ line.includes("next-dev-server") ||
25
+ /node_modules[\/_]react[\/_]/i.test(normalizedLine) ||
26
+ /node_modules[\/_]react-dom[\/_]/i.test(normalizedLine) ||
27
+ /node_modules[\/_]next[\/_]/i.test(normalizedLine) ||
28
+ /node_modules[\/_]scheduler[\/_]/i.test(normalizedLine) ||
29
+ line.includes("react-jsx-dev-runtime") ||
30
+ line.includes("react-jsx-runtime") ||
31
+ line.includes("custom-jsx-runtime") ||
32
+ line.includes("custom-jsx-dev-runtime");
33
+ if (isFramework) {
34
+ continue;
35
+ }
36
+ const match = line.match(/(?:at\s+.*?\s+\(|at\s+)(.*?):(\d+):(\d+)\)?$/);
37
+ if (match) {
38
+ return {
39
+ fileName: match[1],
40
+ lineNumber: parseInt(match[2], 10),
41
+ columnNumber: parseInt(match[3], 10)
42
+ };
43
+ }
44
+ }
45
+ }
46
+ catch { }
47
+ }
48
+ return null;
49
+ }
50
+ function injectSource(type, config) {
51
+ if (config && typeof type === "string") {
52
+ const source = getJSXSourceLocation(type);
53
+ if (source) {
54
+ let filePath = source.fileName || "";
55
+ try {
56
+ if (path.isAbsolute(filePath)) {
57
+ filePath = path.relative(process.cwd(), filePath).replace(/\\/g, "/");
58
+ }
59
+ }
60
+ catch { }
61
+ config["data-hoversource-loc"] = `${filePath}:${source.lineNumber}:${source.columnNumber}`;
62
+ }
63
+ }
64
+ }
65
+ export function jsx(type, config, maybeKey, ...args) {
66
+ injectSource(type, config);
67
+ return original.jsx(type, config, maybeKey, ...args);
68
+ }
69
+ export function jsxs(type, config, maybeKey, ...args) {
70
+ injectSource(type, config);
71
+ return original.jsxs(type, config, maybeKey, ...args);
72
+ }
@@ -0,0 +1,4 @@
1
+ import { AppLauncher, LaunchConfig } from "./types.js";
2
+ export declare class ElectronCdpLauncher implements AppLauncher {
3
+ launch(config: LaunchConfig): Promise<void>;
4
+ }
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import { parseCommand, resolveWindowsCommand, validateSafeCommand, validateSafePath, findScriptPath, startCdpInjectionWatch, resolveDebugPortConflicts, detectDevServerPort, } from "../cli.js";
4
+ import { resolveDevServerPort } from "../port.js";
5
+ export class ElectronCdpLauncher {
6
+ async launch(config) {
7
+ const { execCommand, projectRoot, serverPort, debugPort, args } = config;
8
+ const { command, args: cmdArgs } = parseCommand(execCommand);
9
+ const resolvedCmd = resolveWindowsCommand(validateSafeCommand(command));
10
+ const autoResolve = args["auto-resolve"] === true;
11
+ // Check if the dev server port is occupied and resolve it
12
+ const devPort = detectDevServerPort(projectRoot, execCommand);
13
+ const devResult = await resolveDevServerPort({
14
+ projectRoot,
15
+ execCommand,
16
+ expectedPort: devPort,
17
+ mode: "electron",
18
+ autoResolve,
19
+ excludePorts: [serverPort, debugPort]
20
+ });
21
+ const runWithCdp = (portToUse, patchRestorer) => {
22
+ console.log(`[HoverSource] Launching target command with remote debugging: ${[resolvedCmd, ...cmdArgs].join(" ")}`);
23
+ const env = {
24
+ ...process.env,
25
+ ...devResult.env,
26
+ ELECTRON_EXTRA_LAUNCH_ARGS: `--remote-debugging-port=${portToUse}`
27
+ };
28
+ const useShell = process.platform === "win32";
29
+ const child = useShell
30
+ ? spawn([resolvedCmd, ...cmdArgs].join(" "), {
31
+ shell: true,
32
+ env,
33
+ cwd: validateSafePath(projectRoot),
34
+ stdio: "inherit",
35
+ })
36
+ : spawn(resolvedCmd, cmdArgs, {
37
+ shell: false,
38
+ env,
39
+ cwd: validateSafePath(projectRoot),
40
+ stdio: "inherit",
41
+ detached: true,
42
+ });
43
+ child.on("error", (err) => {
44
+ console.error(`[HoverSource] Exec command failed to start: ${err.message}`);
45
+ });
46
+ const scriptPath = findScriptPath(projectRoot);
47
+ const scriptContent = fs.readFileSync(validateSafePath(scriptPath), "utf-8");
48
+ const portBootstrap = `globalThis.__HOVERSOURCE_PORT__ = ${serverPort};\n`;
49
+ const scriptWithPort = portBootstrap + scriptContent;
50
+ startCdpInjectionWatch(portToUse, scriptWithPort);
51
+ const cleanup = () => {
52
+ if (patchRestorer)
53
+ patchRestorer();
54
+ process.exit();
55
+ };
56
+ process.once("SIGINT", cleanup);
57
+ process.once("SIGTERM", cleanup);
58
+ child.on("exit", cleanup);
59
+ };
60
+ const resolved = await resolveDebugPortConflicts(debugPort, projectRoot, autoResolve, args);
61
+ runWithCdp(resolved.resolvedDebugPort, resolved.patchRestorer);
62
+ }
63
+ }
@@ -0,0 +1,5 @@
1
+ import { AppLauncher, LaunchConfig } from "./types.js";
2
+ export declare class WebProxyLauncher implements AppLauncher {
3
+ private readonly patcher;
4
+ launch(config: LaunchConfig): Promise<void>;
5
+ }
@@ -0,0 +1,114 @@
1
+ import path from "node:path";
2
+ import { pathToFileURL, fileURLToPath } from "node:url";
3
+ import { spawn } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import { ReactRuntimePatcher } from "../patcher/ReactRuntimePatcher.js";
6
+ import { loadMergedConfig } from "@hoversource/companion-server";
7
+ import { detectDevServerPort, parseCommand, resolveWindowsCommand, validateSafeCommand, validateSafePath, waitForServer, runProxyMode } from "../cli.js";
8
+ import { resolveDevServerPort } from "../port.js";
9
+ export class WebProxyLauncher {
10
+ patcher = new ReactRuntimePatcher();
11
+ async launch(config) {
12
+ const { execCommand, projectRoot, serverPort, args } = config;
13
+ // Apply React runtime patching
14
+ this.patcher.patch(projectRoot);
15
+ const mergedConfig = loadMergedConfig(projectRoot);
16
+ const timeoutSec = mergedConfig.webAppDevServerTimeout ?? 120;
17
+ const timeoutMs = timeoutSec * 1000;
18
+ const expectedDevPort = detectDevServerPort(projectRoot, execCommand);
19
+ console.log(`[HoverSource] Web app detected. Dev server expected on port ${expectedDevPort}.`);
20
+ const autoResolve = args["auto-resolve"] === true;
21
+ const devResult = await resolveDevServerPort({
22
+ projectRoot,
23
+ execCommand,
24
+ expectedPort: expectedDevPort,
25
+ mode: "web",
26
+ autoResolve,
27
+ excludePorts: [serverPort]
28
+ });
29
+ const targetDevPort = devResult.port;
30
+ if (targetDevPort !== expectedDevPort) {
31
+ console.log(`[HoverSource] Dev server will use port ${targetDevPort} instead of ${expectedDevPort}.`);
32
+ }
33
+ // Set NODE_OPTIONS to preload our bootstrap script in all Node processes spawned by the dev server
34
+ const __filename = fileURLToPath(import.meta.url);
35
+ const __dirname = path.dirname(__filename);
36
+ const localBootstrap = path.resolve(__dirname, "../bootstrap.js");
37
+ const projectBootstrap = path.resolve(projectRoot, "node_modules/@hoversource/cli/dist/bootstrap.js");
38
+ const bootstrapPath = fs.existsSync(projectBootstrap) ? projectBootstrap : localBootstrap;
39
+ const bootstrapUrl = pathToFileURL(bootstrapPath).href;
40
+ const env = { ...process.env, ...devResult.env };
41
+ const currentOptions = env.NODE_OPTIONS || "";
42
+ env.NODE_OPTIONS = `${currentOptions} --import "${bootstrapUrl}"`.trim();
43
+ // Spawn the dev server
44
+ const { command, args: cmdArgs } = parseCommand(execCommand);
45
+ const finalArgs = [...cmdArgs, ...devResult.extraArgs];
46
+ const resolvedCmd = resolveWindowsCommand(validateSafeCommand(command));
47
+ const useShell = process.platform === "win32";
48
+ const child = useShell
49
+ ? spawn([resolvedCmd, ...finalArgs].join(" "), {
50
+ shell: true,
51
+ env,
52
+ cwd: validateSafePath(projectRoot),
53
+ stdio: "inherit",
54
+ })
55
+ : spawn(resolvedCmd, finalArgs, {
56
+ shell: false,
57
+ env,
58
+ cwd: validateSafePath(projectRoot),
59
+ stdio: "inherit",
60
+ detached: true,
61
+ });
62
+ child.on("error", (err) => {
63
+ console.error(`[HoverSource] Dev server failed to start:`, err.message);
64
+ });
65
+ let hasExited = false;
66
+ let exitCode = null;
67
+ child.on("exit", (code) => {
68
+ hasExited = true;
69
+ exitCode = code;
70
+ });
71
+ // Wait for the dev server to be ready
72
+ console.log(`[HoverSource] Waiting for dev server on port ${targetDevPort} (timeout: ${timeoutSec}s)...`);
73
+ const ready = await waitForServer(targetDevPort, timeoutMs, () => hasExited);
74
+ if (!ready) {
75
+ if (hasExited) {
76
+ console.error(`\n\x1b[31m[HoverSource] Error: Dev server process exited early with code ${exitCode}.\x1b[0m`);
77
+ console.error(`[HoverSource] Please make sure your dev server is built and can run successfully.`);
78
+ console.error(`[HoverSource] If it's a production server, ensure you have built it first (e.g. npm run build)`);
79
+ console.error(`[HoverSource] or run the development server instead (e.g. hs dev).`);
80
+ return;
81
+ }
82
+ console.error(`[HoverSource] Dev server did not respond on port ${targetDevPort} within timeout.`);
83
+ console.error(`[HoverSource] If your dev server uses a different port, run it separately and use:`);
84
+ console.error(`\x1b[36m[HoverSource] hs -t http://localhost:<your-port>\x1b[0m`);
85
+ return;
86
+ }
87
+ console.log(`[HoverSource] Dev server is ready on port ${targetDevPort}.`);
88
+ // Register cleanup to restore runtimes on exit
89
+ const cleanup = () => {
90
+ this.patcher.restore();
91
+ if (child.pid) {
92
+ try {
93
+ if (process.platform === "win32") {
94
+ const taskkillPath = process.env.SystemRoot
95
+ ? path.join(process.env.SystemRoot, "System32", "taskkill.exe")
96
+ : "taskkill";
97
+ spawn(taskkillPath, ["/F", "/T", "/PID", String(child.pid)]);
98
+ }
99
+ else {
100
+ process.kill(-child.pid, "SIGKILL");
101
+ }
102
+ }
103
+ catch {
104
+ child.kill();
105
+ }
106
+ }
107
+ process.exit();
108
+ };
109
+ process.once("SIGINT", cleanup);
110
+ process.once("SIGTERM", cleanup);
111
+ child.on("exit", cleanup);
112
+ await runProxyMode(`http://localhost:${targetDevPort}`, serverPort, args);
113
+ }
114
+ }
@@ -0,0 +1,10 @@
1
+ export interface LaunchConfig {
2
+ execCommand: string;
3
+ projectRoot: string;
4
+ serverPort: number;
5
+ debugPort: number;
6
+ args: any;
7
+ }
8
+ export interface AppLauncher {
9
+ launch(config: LaunchConfig): Promise<void>;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function resolve(specifier: string, context: any, nextResolve: any): Promise<any>;
package/dist/loader.js ADDED
@@ -0,0 +1,31 @@
1
+ import { pathToFileURL, fileURLToPath } from "node:url";
2
+ import path from "node:path";
3
+ const __filename = fileURLToPath(import.meta.url);
4
+ const __dirname = path.dirname(__filename);
5
+ const wrapperUrl = pathToFileURL(path.resolve(__dirname, "./custom-jsx-dev-runtime.js")).href;
6
+ export async function resolve(specifier, context, nextResolve) {
7
+ const parentUrl = context.parentURL || "";
8
+ // Recursion guard: if parent is our wrapper, do not redirect
9
+ if (parentUrl.startsWith(wrapperUrl)) {
10
+ return nextResolve(specifier, context);
11
+ }
12
+ const isTarget = specifier === "react/jsx-dev-runtime" ||
13
+ specifier.endsWith("/react/jsx-dev-runtime") ||
14
+ specifier.replaceAll("\\", "/").includes("react/jsx-dev-runtime") ||
15
+ specifier.includes("next/dist/compiled/react/jsx-dev-runtime");
16
+ if (isTarget) {
17
+ try {
18
+ const originalResult = await nextResolve(specifier, context);
19
+ const originalUrl = originalResult.url;
20
+ // Append the original module URL as a query parameter to the wrapper URL
21
+ const redirectUrl = `${wrapperUrl}?original=${encodeURIComponent(originalUrl)}`;
22
+ return {
23
+ format: "module",
24
+ shortCircuit: true,
25
+ url: redirectUrl
26
+ };
27
+ }
28
+ catch { }
29
+ }
30
+ return nextResolve(specifier, context);
31
+ }
@@ -0,0 +1,5 @@
1
+ import { RuntimePatcher } from "./types.js";
2
+ export declare class ReactRuntimePatcher implements RuntimePatcher {
3
+ patch(projectRoot: string): void;
4
+ restore(): void;
5
+ }