@nuxt/devtools
Version:
<a href="https://devtools.nuxt.com"><img width="1200" alt="Nuxt DevTools" src="https://github-production-user-asset-6210df.s3.amazonaws.com/904724/261577617-a10567bd-ad33-48cc-9bda-9e37dbe1929f.png"></a> <br>
333 lines (332 loc) • 9.88 kB
JavaScript
import { setIframeServerContext } from "@vue/devtools-kit";
import { createHooks } from "hookable";
import { debounce } from "perfect-debounce";
import { computed, createApp, h, markRaw, ref, shallowReactive, shallowRef, watch } from "vue";
import { initTimelineMetrics } from "../../function-metrics-helpers.js";
import Main from "./Main.vue";
import { popupWindow, state } from "./state.js";
import { useAppConfig, useRuntimeConfig } from "#imports";
const clientRef = shallowRef();
export { clientRef as client };
export async function setupDevToolsClient({
nuxt,
clientHooks,
timeMetric,
router
}) {
const isInspecting = ref(false);
const colorMode = useClientColorMode();
const timeline = initTimelineMetrics();
const client = shallowReactive({
nuxt: markRaw(nuxt),
hooks: createHooks(),
inspector: getInspectorInstance(),
getIframe,
syncClient,
devtools: {
toggle() {
if (state.value.open)
client.devtools.close();
else
client.devtools.open();
},
close() {
if (!state.value.open)
return;
state.value.open = false;
if (popupWindow.value) {
try {
popupWindow.value.close();
} catch {
}
popupWindow.value = null;
}
},
open() {
if (state.value.open)
return;
state.value.open = true;
},
async navigate(path) {
if (!state.value.open)
await client.devtools.open();
await client.hooks.callHook("host:action:navigate", path);
},
async reload() {
await client.hooks.callHook("host:action:reload");
}
},
app: {
appConfig: useAppConfig(),
reload() {
location.reload();
},
navigate(path, hard = false) {
if (hard)
location.href = path;
else
router.push(path);
},
colorMode,
frameState: state,
$fetch: globalThis.$fetch
},
metrics: {
clientPlugins: () => window.__NUXT_DEVTOOLS_PLUGINS_METRIC__,
clientHooks: () => Object.values(clientHooks),
clientTimeline: () => timeline,
loading: () => timeMetric
},
revision: ref(0)
});
window.__NUXT_DEVTOOLS_HOST__ = client;
let iframe;
function syncClient() {
if (!client.inspector)
client.inspector = getInspectorInstance();
try {
iframe?.contentWindow?.__NUXT_DEVTOOLS_VIEW__?.setClient(client);
} catch {
}
return client;
}
function getIframe() {
if (!iframe) {
const runtimeConfig = useRuntimeConfig();
const CLIENT_BASE = "/__nuxt_devtools__/client";
const CLIENT_PATH = `${runtimeConfig.app.baseURL.replace(CLIENT_BASE, "/")}${CLIENT_BASE}`.replace(/\/+/g, "/");
const initialUrl = CLIENT_PATH + state.value.route;
iframe = document.createElement("iframe");
for (const [key, value] of Object.entries(runtimeConfig.app.devtools?.iframeProps || {}))
iframe.setAttribute(key, String(value));
iframe.id = "nuxt-devtools-iframe";
iframe.src = initialUrl;
iframe.onload = async () => {
try {
setIframeServerContext(iframe);
await waitForClientInjection();
client.syncClient();
} catch (e) {
console.error("Nuxt DevTools client injection failed");
console.error(e);
}
};
}
return iframe;
}
function waitForClientInjection(retry = 20, timeout = 300) {
let lastError;
const test = () => {
try {
return !!iframe?.contentWindow?.__NUXT_DEVTOOLS_VIEW__;
} catch (e) {
lastError = e;
}
return false;
};
if (test())
return;
return new Promise((resolve, reject) => {
const interval = setInterval(() => {
if (test()) {
clearInterval(interval);
resolve();
} else if (retry-- <= 0) {
clearInterval(interval);
reject(lastError);
}
}, timeout);
});
}
function enableComponentInspector() {
window.__VUE_INSPECTOR__?.enable();
isInspecting.value = true;
}
function disableComponentInspector() {
if (!window.__VUE_INSPECTOR__?.enabled)
return;
window.__VUE_INSPECTOR__?.disable();
client?.hooks.callHook("host:inspector:close");
isInspecting.value = false;
}
function getInspectorInstance() {
const componentInspector = window.__VUE_INSPECTOR__;
if (componentInspector) {
componentInspector.openInEditor = async (url) => {
disableComponentInspector();
await client.hooks.callHook("host:inspector:click", url);
};
componentInspector.onUpdated = () => {
client.hooks.callHook("host:inspector:update", {
...componentInspector.linkParams,
...componentInspector.position
});
};
}
return markRaw({
isEnabled: isInspecting,
enable: enableComponentInspector,
disable: disableComponentInspector,
toggle: () => {
if (!state.value.open)
client.devtools.open();
if (window.__VUE_INSPECTOR__?.enabled)
disableComponentInspector();
else
enableComponentInspector();
},
instance: componentInspector
});
}
setupRouteTracking(timeline, router);
setupReactivity(client, router, timeline);
clientRef.value = client;
const documentPictureInPicture = window.documentPictureInPicture;
if (documentPictureInPicture?.requestWindow) {
client.devtools.popup = async () => {
const iframe2 = getIframe();
if (!iframe2)
return;
const pip = popupWindow.value = await documentPictureInPicture.requestWindow({
width: Math.round(window.innerWidth * state.value.width / 100),
height: Math.round(window.innerHeight * state.value.height / 100)
});
const style = pip.document.createElement("style");
style.innerHTML = `
body {
margin: 0;
padding: 0;
}
iframe {
width: 100vw;
height: 100vh;
border: none;
outline: none;
}
`;
pip.__NUXT_DEVTOOLS_DISABLE__ = true;
pip.__NUXT_DEVTOOLS_IS_POPUP__ = true;
pip.__NUXT__ = window.parent?.__NUXT__ || window.__NUXT__;
pip.document.title = "Nuxt DevTools";
pip.document.head.appendChild(style);
pip.document.body.appendChild(iframe2);
pip.addEventListener("resize", () => {
state.value.width = Math.round(pip.innerWidth / window.innerWidth * 100);
state.value.height = Math.round(pip.innerHeight / window.innerHeight * 100);
});
pip.addEventListener("pagehide", () => {
popupWindow.value = null;
pip.close();
});
};
}
const holder = document.createElement("div");
holder.id = "nuxt-devtools-container";
holder.setAttribute("data-v-inspector-ignore", "true");
document.body.appendChild(holder);
window.addEventListener("keydown", (e) => {
if (e.code === "KeyD" && e.altKey && e.shiftKey)
client.devtools.toggle();
});
const app = createApp({
render: () => h(Main, { client }),
devtools: {
hide: true
}
});
app.mount(holder);
}
export function useClientColorMode() {
const explicitColor = ref();
const systemColor = ref();
const elements = [
document.documentElement,
document.body
];
const ob = new MutationObserver(getExplicitColor);
elements.forEach((el) => {
ob.observe(el, {
attributes: true,
attributeFilter: ["class"]
});
});
const preferDarkQuery = window.matchMedia("(prefers-color-scheme: dark)");
const preferLightQuery = window.matchMedia("(prefers-color-scheme: light)");
preferDarkQuery.addEventListener("change", getSystemColor);
preferLightQuery.addEventListener("change", getSystemColor);
function getExplicitColor() {
let color;
for (const el of elements) {
if (el.classList.contains("dark")) {
color = "dark";
break;
}
if (el.classList.contains("light")) {
color = "light";
break;
}
}
explicitColor.value = color;
}
function getSystemColor() {
if (preferDarkQuery.matches)
systemColor.value = "dark";
else if (preferLightQuery.matches)
systemColor.value = "light";
else
systemColor.value = void 0;
}
getExplicitColor();
getSystemColor();
return computed(() => explicitColor.value || systemColor.value || "light");
}
function setupRouteTracking(timeline, router) {
if (timeline.options.enabled && router?.currentRoute?.value?.path) {
const start = timeline.events[0]?.start || Date.now();
timeline.events.unshift({
type: "route",
from: router.currentRoute.value.path,
to: router.currentRoute.value.path,
start,
end: start
});
}
let lastRouteEvent;
router?.afterEach(() => {
if (lastRouteEvent && !lastRouteEvent?.end)
lastRouteEvent.end = Date.now();
});
router?.beforeEach((to, from) => {
if (!timeline.options.enabled)
return;
lastRouteEvent = {
type: "route",
from: from.path,
to: to.path,
start: Date.now()
};
timeline.events.push(lastRouteEvent);
});
}
function setupReactivity(client, router, timeMetric) {
const refreshReactivity = debounce(() => {
client.hooks.callHook("host:update:reactivity");
}, 100, { trailing: true });
watch(() => [
client.nuxt.payload,
client.app.colorMode.value,
client.metrics.loading(),
timeMetric
], () => {
refreshReactivity();
}, { deep: true });
router?.afterEach(() => {
refreshReactivity();
});
client.nuxt.hook("app:mounted", () => {
refreshReactivity();
});
client.hooks.hook("devtools:navigate", (path) => {
state.value.route = path;
});
}