stack/packages/template/src/dev-tool/index.ts
Konsti Wohlwend e3202a331c
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
Publish npm packages / publish (push) Has been cancelled
Publish Swift SDK to prerelease repo / publish (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
feat: devtool indicator auto-visibility based on NODE_ENV (#1624)
2026-06-19 16:47:02 -07:00

156 lines
4.8 KiB
TypeScript

// IF_PLATFORM js-like
import type { StackClientApp } from "../lib/hexclave-app";
import { captureError } from "@hexclave/shared/dist/utils/errors";
import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
import { isLocalhost } from "@hexclave/shared/dist/utils/urls";
import { canMountIntoDom } from "../in-page-ui/dom";
import { envVars } from "../generated/env";
import type { createDevTool as CreateDevToolFn } from "./dev-tool-core";
// Hexclave rebrand: UI-only local pref — straight rename (one-time reset is harmless)
const OVERRIDE_KEY = '__hexclave-dev-tool-override';
function getOverride(): boolean | null {
try {
const val = localStorage.getItem(OVERRIDE_KEY);
if (val === 'true') return true;
if (val === 'false') return false;
} catch {}
return null;
}
let activeDevToolOption: true | "auto" | undefined = undefined;
function shouldShow(): boolean {
const override = getOverride();
if (override !== null) return override;
if (!canMountIntoDom()) return false;
// If devTool was explicitly set to true, always show
if (activeDevToolOption === true) return true;
// "auto" behavior (the default):
const nodeEnv = envVars.NODE_ENV;
if (nodeEnv !== undefined) {
// NODE_ENV is available (bundler/process env exists) — only show in development
return nodeEnv === "development";
}
// NODE_ENV not found (no process.env/import.meta) — show on localhost or file: protocol
try {
const url = new URL(window.location.href);
if (url.protocol === "file:") return true;
} catch {}
return isLocalhost(window.location.href);
}
let activeCleanup: (() => void) | null = null;
let activeApp: StackClientApp<true> | null = null;
let mountGeneration = 0;
let createDevToolPromise: Promise<typeof CreateDevToolFn> | null = null;
function loadCreateDevTool(): Promise<typeof CreateDevToolFn> {
if (!createDevToolPromise) {
createDevToolPromise = import("./dev-tool-core").then(m => m.createDevTool).catch((err) => {
createDevToolPromise = null;
throw err;
});
}
return createDevToolPromise;
}
function tryMount() {
if (activeCleanup) {
activeCleanup();
activeCleanup = null;
}
if (!shouldShow() || !activeApp || !canMountIntoDom()) return;
const generation = ++mountGeneration;
const app = activeApp;
runAsynchronously(async () => {
const createDevTool = await loadCreateDevTool();
if (generation !== mountGeneration) return;
if (!shouldShow() || activeApp !== app || !canMountIntoDom()) return;
activeCleanup = createDevTool(app);
}, {
noErrorLogging: true,
onError: (error) => {
captureError("dev-tool-mount", error);
},
});
}
/**
* Mounts the Hexclave dev tool on the page.
*
* Visibility is determined by the `devTool` option:
* - `true`: always show
* - `false`: never show (caller gates this — mountDevTool won't be called)
* - `"auto"` / `undefined`: show based on NODE_ENV or localhost/file: heuristics
*
* Console commands (also work in production):
* HexclaveDevTool.enable() — force-show the dev tool
* HexclaveDevTool.disable() — force-hide the dev tool
* HexclaveDevTool.reset() — revert to auto behavior
*/
export function mountDevTool(app: StackClientApp<true>, devToolOption?: boolean | "auto"): () => void {
activeApp = app;
activeDevToolOption = devToolOption === false ? undefined : devToolOption ?? undefined;
tryMount();
// Capture the cleanup created by THIS specific mount call so that React
// StrictMode's double-invoke doesn't let the first effect's cleanup tear
// down the second mount (which would cause the tool to disappear silently).
const myCleanup = activeCleanup;
return () => {
activeApp = null;
if (activeCleanup === myCleanup && myCleanup != null) {
activeCleanup = null;
myCleanup();
}
};
}
// Expose console commands: HexclaveDevTool.enable() / .disable() / .reset()
if (typeof window !== 'undefined') {
// Hexclave rebrand: expose under both the legacy and new global names.
(window as any).HexclaveDevTool = (window as any).HexclaveDevTool = {
enable() {
try {
localStorage.setItem(OVERRIDE_KEY, 'true');
} catch {}
tryMount();
console.log('[Stack DevTool] Enabled. Refresh if the panel does not appear.');
},
disable() {
try {
localStorage.setItem(OVERRIDE_KEY, 'false');
} catch {}
if (activeCleanup) {
activeCleanup();
activeCleanup = null;
}
console.log('[Stack DevTool] Disabled.');
},
reset() {
try {
localStorage.removeItem(OVERRIDE_KEY);
} catch {}
if (shouldShow()) {
tryMount();
} else if (activeCleanup) {
activeCleanup();
activeCleanup = null;
}
console.log('[Stack DevTool] Reset to default (auto-detect based on NODE_ENV or localhost/file: origin).');
},
};
}
// END_PLATFORM