├── .prettierrc
├── .eslintignore
├── pnpm-workspace.yaml
├── playground
├── src
│ ├── App.vue
│ ├── main.ts
│ └── style.css
├── devtools-test-2
│ ├── main.js
│ └── index.html
├── public
│ ├── iconfont.ttf
│ └── vite.svg
├── devtools-test
│ ├── vite.config.ts
│ ├── index.html
│ └── main.js
├── .gitignore
├── index.html
├── tsconfig.json
├── package.json
└── vite.config.ts
├── renovate.json
├── .eslintrc
├── patak-tweet.png
├── .gitignore
├── test
└── index.test.ts
├── .editorconfig
├── tsconfig.json
├── src
├── node
│ └── views
│ │ ├── vite.config.ts
│ │ ├── app.js
│ │ ├── Main.vue
│ │ ├── utils.ts
│ │ ├── FrameBox.vue
│ │ └── composables.ts
├── functions
│ ├── packages.ts
│ └── assets.ts
├── types.d.ts
├── middlewares
│ └── attach.ts
├── client.ts
└── server.ts
├── .github
└── workflows
│ └── ci.yml
├── LICENSE
├── package.json
└── README.md
/.prettierrc:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - playground
3 |
--------------------------------------------------------------------------------
/playground/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 | hello
3 |
4 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["github>unjs/renovate-config"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["eslint-config-unjs"],
3 | "rules": {}
4 | }
5 |
--------------------------------------------------------------------------------
/playground/devtools-test-2/main.js:
--------------------------------------------------------------------------------
1 | console.log('second devtools (devtools-test-2)')
2 |
--------------------------------------------------------------------------------
/patak-tweet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pheno-agency/vite-plugin-devtools/HEAD/patak-tweet.png
--------------------------------------------------------------------------------
/playground/public/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pheno-agency/vite-plugin-devtools/HEAD/playground/public/iconfont.ttf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 | types
5 | .vscode
6 | .DS_Store
7 | .eslintcache
8 | *.log*
9 | *.env*
10 |
--------------------------------------------------------------------------------
/playground/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import './style.css'
3 | import App from './App.vue'
4 |
5 | const app = createApp(App)
6 |
7 |
8 | app.mount('#app')
9 |
--------------------------------------------------------------------------------
/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, it, describe } from "vitest";
2 | import {} from "../src";
3 |
4 | describe("packageName", () => {
5 | it.todo("pass", () => {
6 | expect(true).toBe(true);
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | charset = utf-8
8 |
9 | [*.js]
10 | indent_style = space
11 | indent_size = 2
12 |
13 | [{package.json,*.yml,*.cjson}]
14 | indent_style = space
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "strictNullChecks": true,
9 | "skipLibCheck": true,
10 | },
11 | "include": ["src"]
12 | }
13 |
--------------------------------------------------------------------------------
/playground/devtools-test/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { getBase } from 'vite-plugin-devtools'
2 | import { defineConfig } from 'vite'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({base: getBase('devtools-test'),
6 | build: {
7 | target: 'esnext',
8 | sourcemap: 'inline',
9 | minify: false
10 | }
11 | })
12 |
--------------------------------------------------------------------------------
/playground/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/src/node/views/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "path";
2 | import { defineConfig } from "vite";
3 | import vue from '@vitejs/plugin-vue'
4 |
5 | export default defineConfig({
6 | plugins: [vue()],
7 | build: {
8 | sourcemap: true,
9 | lib: {
10 | entry: resolve(__dirname, "./app.js"),
11 | name: "view",
12 | fileName: "view",
13 | formats: ["es"],
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + Vue + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "esModuleInterop": true,
13 | "lib": ["ESNext", "DOM"],
14 | "skipLibCheck": true,
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/playground/devtools-test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vue DevTools
8 |
9 |
10 | Vue Devtools
11 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/playground/devtools-test/main.js:
--------------------------------------------------------------------------------
1 | import { addClientFunction, clientRPC } from 'vite-plugin-devtools/client'
2 |
3 | console.log('first devtools (devtools-test)')
4 |
5 | addClientFunction('ping', () => {
6 | return 'response from client'
7 | })
8 |
9 | console.log('packages in the project', await clientRPC.here())
10 | console.log('packages in the project', await clientRPC.getPackages())
11 | console.log('static assets', await clientRPC.staticAssets())
12 |
--------------------------------------------------------------------------------
/playground/devtools-test-2/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Nuxt DevTools
8 |
9 |
10 | Nuxt DevTools
11 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/node/views/app.js:
--------------------------------------------------------------------------------
1 | import { createApp, h } from 'vue'
2 | import App from './Main.vue'
3 |
4 | function load() {
5 | const CONTAINER_ID = '__devtools-container__'
6 | const el = document.createElement('div')
7 | el.setAttribute('id', CONTAINER_ID)
8 | el.setAttribute('data-v-inspector-ignore', 'true')
9 | document.getElementsByTagName('body')[0].appendChild(el)
10 | createApp({
11 | render: () => h(App),
12 | devtools: {
13 | hide: true,
14 | },
15 | }).mount(el)
16 | }
17 | load()
18 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | ci:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | - run: corepack enable
17 | - uses: actions/setup-node@v3
18 | with:
19 | node-version: 18
20 | cache: "pnpm"
21 | - run: pnpm install
22 | - run: pnpm lint
23 | - run: pnpm build
24 | - run: pnpm vitest --coverage
25 | - uses: codecov/codecov-action@v3
26 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground",
3 | "type": "module",
4 | "version": "0.0.0",
5 | "private": true,
6 | "scripts": {
7 | "dev": "vite build devtools-test ; vite dev"
8 | },
9 | "dependencies": {
10 | "@vueuse/core": "^10.1.2",
11 | "pinia": "^2.0.35",
12 | "vue": "^3.2.47",
13 | "vue-router": "^4.1.6"
14 | },
15 | "devDependencies": {
16 | "@vitejs/plugin-vue": "^4.2.1",
17 | "serve": "^14.2.0",
18 | "typescript": "^5.0.4",
19 | "vite": "^4.3.5",
20 | "vite-plugin-inspect": "^0.7.25",
21 | "vite-plugin-devtools": "workspace:*"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/functions/packages.ts:
--------------------------------------------------------------------------------
1 | import { promises as fsp } from 'node:fs'
2 | import { resolve } from 'pathe'
3 |
4 | export async function getPackages(root: string) {
5 | // TODO: support monorepo workspace ?
6 | const pkgPath = resolve(root, 'package.json')
7 | const data: Record = JSON.parse(await fsp.readFile(pkgPath, 'utf-8').catch(() => '{}'))
8 | const categorizedPackages: Record = {}
9 | const packages: Record = {}
10 | for (const type of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
11 | if (!data[type])
12 | continue
13 | categorizedPackages[type] = data[type]
14 | }
15 | for (const type in categorizedPackages) {
16 | for (const name in categorizedPackages[type]) {
17 | const version = categorizedPackages[type][name]
18 | packages[name] = version
19 | }
20 | }
21 | return {
22 | packages,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/playground/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playground/src/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
3 | font-size: 16px;
4 | line-height: 24px;
5 | font-weight: 400;
6 |
7 | color-scheme: light dark;
8 | color: rgba(255, 255, 255, 0.87);
9 | background-color: #242424;
10 |
11 | font-synthesis: none;
12 | text-rendering: optimizeLegibility;
13 | -webkit-font-smoothing: antialiased;
14 | -moz-osx-font-smoothing: grayscale;
15 | -webkit-text-size-adjust: 100%;
16 | }
17 |
18 | a {
19 | font-weight: 500;
20 | color: #646cff;
21 | text-decoration: inherit;
22 | }
23 |
24 | a:hover {
25 | color: #535bf2;
26 | }
27 |
28 | body {
29 | margin: 0;
30 | /* display: flex; */
31 | /* place-items: center; */
32 | min-width: 320px;
33 | min-height: 100vh;
34 | }
35 |
36 | h1 {
37 | font-size: 3.2em;
38 | line-height: 1.1;
39 | }
40 |
41 | button {
42 | border-radius: 8px;
43 | border: 1px solid transparent;
44 | padding: 0.6em 1.2em;
45 | font-size: 1em;
46 | font-weight: 500;
47 | font-family: inherit;
48 | background-color: #1a1a1a;
49 | cursor: pointer;
50 | transition: border-color 0.25s;
51 | }
52 |
53 | button:hover {
54 | border-color: #646cff;
55 | }
56 |
57 | button:focus,
58 | button:focus-visible {
59 | outline: 4px auto -webkit-focus-ring-color;
60 | }
61 |
62 | .card {
63 | padding: 2em;
64 | }
65 |
66 | #app {
67 | max-width: 1280px;
68 | margin: 0 auto;
69 | padding: 2rem;
70 | text-align: center;
71 | }
72 |
73 | @media (prefers-color-scheme: light) {
74 | :root {
75 | color: #213547;
76 | background-color: #ffffff;
77 | }
78 |
79 | a:hover {
80 | color: #747bff;
81 | }
82 |
83 | button {
84 | background-color: #f9f9f9;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | import type { BirpcReturn, BirpcGroupReturn } from 'birpc'
2 |
3 | export interface DefaultServerFunctions {
4 | staticAssets(): Promise
5 | getImageMeta(filepath: string): Promise
6 | getTextAssetContent(filepath: string, limit?: number): Promise
7 | getPackages(): Promise<{ packages: Record }>
8 | }
9 |
10 | export interface ServerFunctions {
11 | }
12 |
13 | export interface ClientFunctions {
14 | }
15 |
16 | export type ServerRPC = BirpcGroupReturn
17 | export type ServerFunction = (this: ServerRPC, ...args: any) => any
18 | export type ToServerFunction any> = (this: ServerRPC, ...args: Parameters) => ReturnType
19 |
20 | export type ClientRPC = BirpcReturn
21 | export type ClientFunction = (this: ClientRPC, ...args: any) => any
22 | export type ToClientFunction any> = (this: ClientRPC, ...args: Parameters) => ReturnType
23 |
24 | export type AssetType = 'image' | 'font' | 'video' | 'audio' | 'text' | 'json' | 'other'
25 | export interface AssetInfo {
26 | path: string
27 | type: AssetType
28 | publicPath: string
29 | filePath: string
30 | size: number
31 | mtime: number
32 | }
33 | export interface ImageMeta {
34 | width: number
35 | height: number
36 | orientation?: number
37 | type?: string
38 | mimeType?: string
39 | }
40 |
41 | declare global {
42 | export interface Window {
43 | popupIframes: Record Promise>;
44 | togglePanelVisible(name: string): void
45 | toggleViewMode(viewMode: "default" | "xs"): void
46 | getViewMode(): "default" | "xs"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/middlewares/attach.ts:
--------------------------------------------------------------------------------
1 | import type { BirpcGroup, ChannelOptions } from 'birpc';
2 | import type { WebSocket } from 'ws'
3 | import { WebSocketServer } from 'ws'
4 | import type {ViteDevServer} from 'vite'
5 | import { parse, stringify } from 'flatted'
6 | import type { ClientFunctions, DefaultServerFunctions, ServerFunction, ServerFunctions, ServerRPC } from '../types';
7 |
8 | export type Config = { serverFunctions: Record, serverRPC: ServerRPC | null}
9 |
10 | const wsClients = new Map>()
11 |
12 | export function attachWebSocket(rpc: BirpcGroup, iframeSrc: string, server: ViteDevServer) {
13 | if (!wsClients.has(iframeSrc)) {
14 | wsClients.set(iframeSrc, new Set())
15 | }
16 |
17 | const route = '/__devtools__ws__/' + iframeSrc.split('/')[1]
18 | const wss = new WebSocketServer({ noServer: true })
19 |
20 | server.httpServer?.on('upgrade', (request, socket, head) => {
21 | if (!request.url)
22 | return
23 |
24 | const { pathname } = new URL(request.url, 'http://localhost')
25 | if (pathname !== route)
26 | return
27 |
28 | wss.handleUpgrade(request, socket, head, (ws) => {
29 | wss.emit('connection', ws, request)
30 |
31 | wsClients.get(iframeSrc)!.add(ws)
32 | const channel: ChannelOptions = {
33 | post: d => ws.send(d),
34 | on: fn => ws.on('message', fn),
35 | serialize: stringify,
36 | deserialize: parse,
37 | }
38 | rpc.updateChannels((c) => {
39 | c.push(channel)
40 | })
41 | ws.on('close', () => {
42 | wsClients.get(iframeSrc)!.delete(ws)
43 | rpc.updateChannels((c) => {
44 | const index = c.indexOf(channel)
45 | if (index >= 0)
46 | c.splice(index, 1)
47 | })
48 | })
49 | })
50 | })
51 | }
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-plugin-devtools",
3 | "version": "0.0.0",
4 | "description": "",
5 | "repository": "pheno-agency/vite-plugin-devtools",
6 | "license": "MIT",
7 | "sideEffects": false,
8 | "type": "module",
9 | "exports": {
10 | ".": {
11 | "types": "./dist/server.d.ts",
12 | "import": "./dist/server.mjs",
13 | "require": "./dist/server.cjs"
14 | },
15 | "./dist/client": {
16 | "types": "./dist/client.d.ts",
17 | "import": "./dist/client.mjs"
18 | },
19 | "./client": {
20 | "types": "./dist/client.d.ts",
21 | "import": "./dist/client.mjs"
22 | },
23 | "./dist/server": {
24 | "types": "./dist/server.d.ts",
25 | "import": "./dist/server.mjs"
26 | },
27 | "./server": {
28 | "types": "./dist/server.d.ts",
29 | "import": "./dist/server.mjs"
30 | }
31 | },
32 | "main": "./dist/server.mjs",
33 | "module": "./dist/server.mjs",
34 | "types": "./dist/server.d.ts",
35 | "files": [
36 | "dist",
37 | "src"
38 | ],
39 | "scripts": {
40 | "build": "vite build ./src/node/views ; unbuild",
41 | "dev": "vitest dev",
42 | "lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src test",
43 | "lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test -w",
44 | "prepack": "pnpm run build",
45 | "release": "pnpm test && changelogen --release && npm publish && git push --follow-tags",
46 | "test": "pnpm lint && vitest run --coverage"
47 | },
48 | "peerDependencies": {
49 | "vite": "^3.0.0-0 || ^4.0.0-0"
50 | },
51 | "dependencies": {
52 | "@types/ws": "^8.5.4",
53 | "birpc": "^0.2.11",
54 | "fast-glob": "^3.2.12",
55 | "flatted": "^3.2.7",
56 | "image-meta": "^0.1.1",
57 | "nanoid": "^4.0.2",
58 | "pathe": "^1.1.1",
59 | "sirv": "^2.0.3",
60 | "solid-js": "^1.7.7",
61 | "splitpanes": "^3.1.5",
62 | "tinyws": "^0.1.0",
63 | "vite-dev-rpc": "^0.1.2",
64 | "vite-hot-client": "^0.2.1",
65 | "ws": "^8.13.0"
66 | },
67 | "devDependencies": {
68 | "@vitejs/plugin-vue": "^4.2.1",
69 | "@vitest/coverage-c8": "^0.30.1",
70 | "changelogen": "^0.5.3",
71 | "esbuild": "^0.18.10",
72 | "eslint": "^8.39.0",
73 | "eslint-config-unjs": "^0.1.0",
74 | "prettier": "^2.8.8",
75 | "typescript": "^5.0.4",
76 | "unbuild": "^1.2.1",
77 | "vite": "^4.3.5",
78 | "vitest": "^0.30.1",
79 | "vue": "^3.2.47"
80 | },
81 | "packageManager": "pnpm@7.32.2"
82 | }
83 |
--------------------------------------------------------------------------------
/playground/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import vue from "@vitejs/plugin-vue";
3 | import createDevtools from "vite-plugin-devtools";
4 |
5 | declare module "vite-plugin-devtools" {
6 | export interface ServerFunctions {
7 | here(): string;
8 | }
9 | export interface ClientFunctions {
10 | ping(): string;
11 | }
12 | }
13 |
14 | const { addServerFunction, plugin, serverRPC } = createDevtools("devtools-test", {
15 | icon: `
16 |
21 | `,
22 | clientDir: "./devtools-test/dist/",
23 | });
24 |
25 | addServerFunction("here", function () {
26 | console.log(serverRPC === this)
27 | return "here";
28 | });
29 |
30 | setInterval(async () => {
31 | console.log('pinging the client, response:', await serverRPC.ping());
32 | }, 3000);
33 |
34 | // https://vitejs.dev/config/
35 | export default defineConfig({
36 | plugins: [
37 | // VueDevtools(),
38 | plugin,
39 | vue(),
40 | // multiple devtools can be used at the same time
41 | createDevtools("devtools-test-2", {
42 | icon: `
43 |
46 | `,
47 | clientDir: "./devtools-test-2",
48 | }).plugin,
49 | ],
50 | });
51 |
--------------------------------------------------------------------------------
/src/functions/assets.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises'
2 | import p from 'node:path'
3 | import fg from 'fast-glob'
4 | import { join, resolve } from 'pathe'
5 | import type { ResolvedConfig } from 'vite'
6 | import { imageMeta } from 'image-meta'
7 | import type { AssetInfo, AssetType, ImageMeta } from '../types'
8 |
9 | const _imageMetaCache = new Map()
10 |
11 | function guessType(path: string): AssetType {
12 | if (/\.(png|jpe?g|gif|svg|webp|avif|ico|bmp|tiff?)$/i.test(path))
13 | return 'image'
14 | if (/\.(mp4|webm|ogv|mov|avi|flv|wmv|mpg|mpeg|mkv|3gp|3g2|ts|mts|m2ts|vob|ogm|ogx|rm|rmvb|asf|amv|divx|m4v|svi|viv|f4v|f4p|f4a|f4b)$/i.test(path))
15 | return 'video'
16 | if (/\.(mp3|wav|ogg|flac|aac|wma|alac|ape|ac3|dts|tta|opus|amr|aiff|au|mid|midi|ra|rm|wv|weba|dss|spx|vox|tak|dsf|dff|dsd|cda)$/i.test(path))
17 | return 'audio'
18 | if (/\.(woff2?|eot|ttf|otf|ttc|pfa|pfb|pfm|afm)/i.test(path))
19 | return 'font'
20 | if (/\.(json[5c]?|te?xt|[mc]?[jt]sx?|md[cx]?|markdown)/i.test(path))
21 | return 'text'
22 | return 'other'
23 | }
24 |
25 | export async function getStaticAssets(config: ResolvedConfig): Promise {
26 | const dir = resolve(config.root)
27 | const baseURL = config.base
28 |
29 | const files = await fg([
30 | // image
31 | '**/*.(png|jpe?g|gif|svg|webp|avif|ico|bmp|tiff)',
32 | // video
33 | '**/*.(mp4|webm|ogv|mov|avi|flv|wmv|mpg|mpeg|mkv|3gp|3g2|m2ts|vob|ogm|ogx|rm|rmvb|asf|amv|divx|m4v|svi|viv|f4v|f4p|f4a|f4b)',
34 | // audio
35 | '**/*.(mp3|wav|ogg|flac|aac|wma|alac|ape|ac3|dts|tta|opus|amr|aiff|au|mid|midi|ra|rm|wv|weba|dss|spx|vox|tak|dsf|dff|dsd|cda)',
36 | // font
37 | '**/*.(woff2?|eot|ttf|otf|ttc|pfa|pfb|pfm|afm)',
38 | // text
39 | '**/*.(json[5c]?|te?xt|[mc]?[jt]sx?|md[cx]?|markdown)',
40 | ], {
41 | cwd: dir,
42 | onlyFiles: true,
43 | ignore: ['**/node_modules/**', '**/dist/**'],
44 | })
45 |
46 | return await Promise.all(files.map(async (path) => {
47 | const filePath = resolve(dir, path)
48 | const stat = await fs.lstat(filePath)
49 | const publicDirname = p.relative(config.root, config.publicDir)
50 | const normalizedPath = publicDirname === p.basename(p.dirname(path)) ? path.replace(publicDirname, '') : path
51 | return {
52 | path: normalizedPath,
53 | filePath,
54 | publicPath: join(baseURL, normalizedPath),
55 | type: guessType(path),
56 | size: stat.size,
57 | mtime: stat.mtimeMs,
58 | }
59 | }))
60 | }
61 |
62 | export async function getImageMeta(filepath: string) {
63 | if (_imageMetaCache.has(filepath))
64 | return _imageMetaCache.get(filepath)
65 | try {
66 | const meta = imageMeta(await fs.readFile(filepath)) as ImageMeta
67 | _imageMetaCache.set(filepath, meta)
68 | return meta
69 | }
70 | catch (e) {
71 | _imageMetaCache.set(filepath, undefined)
72 | console.error(e)
73 | return undefined
74 | }
75 | }
76 |
77 | export async function getTextAssetContent(filepath: string, limit = 300) {
78 | try {
79 | const content = await fs.readFile(filepath, 'utf-8')
80 | return content.slice(0, limit)
81 | }
82 | catch (e) {
83 | console.error(e)
84 | return undefined
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/client.ts:
--------------------------------------------------------------------------------
1 | import { parse, stringify } from "flatted";
2 | import {
3 | ClientFunction,
4 | ClientFunctions,
5 | ClientRPC,
6 | DefaultServerFunctions,
7 | ServerFunctions,
8 | ToClientFunction,
9 | } from "./types";
10 | export type * from "./types";
11 | import { createBirpc } from "birpc";
12 |
13 | const clientFunctions = {} as Record;
14 |
15 | const isInIframe = (function inIframe() {
16 | try {
17 | return window.self !== window.top;
18 | } catch (e) {
19 | return true;
20 | }
21 | })();
22 |
23 | const PORT = location.port;
24 | const HOST = [location.hostname, PORT].filter(Boolean).join(":");
25 | const ROUTE = "/__devtools__ws__/" + location.pathname.split("/")[1];
26 | const ENTRY_URL =
27 | `${location.protocol === "https:" ? "wss:" : "ws:"}//${HOST}` + ROUTE;
28 | const RECONNECT_INTERVAL = 2000;
29 |
30 | let connectPromise = isInIframe ? connectWS() : null;
31 | let onMessage: Function = () => {};
32 |
33 | export const clientRPC: ClientRPC = createBirpc<
34 | DefaultServerFunctions & ServerFunctions
35 | >(clientFunctions, {
36 | post: async (d) => {
37 | (await connectPromise!).send(d);
38 | },
39 | on: (fn) => {
40 | onMessage = fn;
41 | },
42 | // these are required when using WebSocket
43 | serialize: stringify,
44 | deserialize: parse,
45 | });
46 |
47 | async function connectWS() {
48 | const ws = new WebSocket(ENTRY_URL);
49 | ws.addEventListener("message", (e) => onMessage(String(e.data)));
50 | ws.addEventListener("error", (e) => {
51 | console.error(e);
52 | });
53 | ws.addEventListener("close", () => {
54 | setTimeout(async () => {
55 | connectPromise = connectWS();
56 | }, RECONNECT_INTERVAL);
57 | });
58 | if (ws.readyState !== WebSocket.OPEN)
59 | await new Promise((resolve) => ws.addEventListener("open", resolve));
60 | return ws;
61 | }
62 |
63 | export function addClientFunction(
64 | name: T,
65 | func: ToClientFunction
66 | ): void;
67 | export function addClientFunction(func: ClientFunction): void;
68 | export function addClientFunction(
69 | nameOrFunc: string | ClientFunction,
70 | func?: ToClientFunction
71 | ) {
72 | func = typeof nameOrFunc === "function" ? nameOrFunc : func;
73 | const name = typeof nameOrFunc === "string" ? nameOrFunc : func?.name;
74 | if (!func) {
75 | throw new Error("Please specify a client function");
76 | }
77 | if (!name) {
78 | throw new Error("Please specify a name for your client function");
79 | }
80 |
81 | clientFunctions[name] = func.bind(clientRPC);
82 | }
83 |
84 | export function togglePopup(name: string) {
85 | return window.popupIframes[name]?.();
86 | }
87 |
88 | export function getTheme() {
89 | return JSON.parse(localStorage.getItem("__devtools-frame-state__")!).theme;
90 | }
91 |
92 | export function toggleTheme(theme: "dark" | "auto" | "light") {
93 | localStorage.setItem("__devtools-frame-state__", JSON.stringify({
94 | ...JSON.parse(localStorage.getItem("__devtools-frame-state__")!),
95 | theme,
96 | }));
97 | }
98 |
99 | export function onThemeChange(cb: (theme: string) => any) {
100 | const listener = (e: StorageEvent) => {
101 | if (
102 | e.key === "__devtools-frame-state__" &&
103 | JSON.parse(e.newValue!).theme !== JSON.parse(e.oldValue!).theme
104 | )
105 | cb(JSON.parse(e.newValue!).theme);
106 | };
107 | window.addEventListener("storage", listener);
108 | return () => window.removeEventListener("storage", listener);
109 | }
110 |
111 | export const togglePanelVisible = window.togglePanelVisible
112 | export const toggleViewMode = window.toggleViewMode
113 | export const getViewMode = window.getViewMode
114 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vite-plugin-devtools
2 |
3 | 
4 |
5 | ## Description
6 |
7 | vite-plugin-devtools is a framework-agnostic devtools builder designed for any tool or library based on Vite. It serves as a shared foundation for other devtools plugins such as [vite-plugin-vue-devtools](https://github.com/webfansplz/vite-plugin-vue-devtools) and [nuxt-devtools](https://github.com/nuxt/devtools).
8 |
9 | Key Features:
10 |
11 | - Extensive API
12 | - Multiple devtools options
13 | - Simple and lightweight
14 | - Full control over the client and server
15 |
16 | ## Usage
17 |
18 | The following code demonstrates how to use vite-plugin-devtools:
19 |
20 | ```typescript
21 | // server (vite.config.js or plugin entry)
22 | import createDevtools from "vite-plugin-devtools";
23 |
24 | const { addServerFunction, plugin, serverRPC } = createDevtools("devtools-test", {
25 | icon: `/* svg string */`,
26 | clientDir: "./test/dist/",
27 | });
28 |
29 | // client (in `clientDir`)
30 | import { addClientFunction } from 'vite-plugin-devtools/client'
31 |
32 | addClientFunction('ping', () => {
33 | return 'response from client'
34 | })
35 | ```
36 |
37 | ## API
38 |
39 | ### createDevtools
40 |
41 | The `createDevtools` function is used to initialize the devtools.
42 |
43 | ```typescript
44 | type Options = {
45 | icon: string;
46 | clientDir: string;
47 | };
48 |
49 | declare function createDevtools(name: string, options: Options): {
50 | serverRPC: birpc.BirpcGroupReturn;
51 | addServerFunction: AddServerFunction;
52 | plugin: PluginOption;
53 | };
54 | ```
55 |
56 | #### addServerFunction
57 |
58 | The `addServerFunction` function allows adding server functions.
59 |
60 | ```typescript
61 | addServerFunction("here", function() {
62 | return "here";
63 | });
64 | ```
65 |
66 | #### plugin
67 |
68 | The `plugin` option can be passed to Vite to integrate the devtools.
69 |
70 | #### serverRPC
71 |
72 | The `serverRPC` object is used to call client functions and is bound to the server functions.
73 |
74 | ```typescript
75 | addServerFunction("here", function() {
76 | console.log(serverRPC === this);
77 | return "here";
78 | });
79 | ```
80 |
81 | ### Client
82 |
83 | ```typescript
84 | declare const clientRPC: ClientRPC;
85 | declare function addClientFunction(name: T, func: ToClientFunction): void;
86 | declare function addClientFunction(func: ClientFunction): void;
87 | declare function changePosition(position: 'bottom' | 'top' | 'left' | 'right'): void;
88 | ```
89 |
90 | #### addClientFunction
91 |
92 | The `addClientFunction` function is used to add a client function that can be called by the server using RPC.
93 |
94 | ```typescript
95 | import { addClientFunction } from 'vite-plugin-devtools/client'
96 |
97 | addClientFunction('ping', () => {
98 | return 'response from client'
99 | })
100 | ```
101 |
102 | Server example:
103 |
104 | ```typescript
105 | setInterval(async () => {
106 | console.log('pinging the client, response:', await serverRPC.ping());
107 | }, 3000);
108 | ```
109 |
110 | #### clientRPC
111 |
112 | The `clientRPC` object allows calling functions defined by the server using `addServerFunction`.
113 |
114 | #### changePosition
115 |
116 | The `changePosition` function is used to modify the position of the devtools bar and associated iframes. It affects the positions of other devtools as well.
117 |
118 | ## TypeScript
119 |
120 | To enhance autocompletion and type safety for your functions, `serverRPC`, `clientRPC`,
121 |
122 | and the `this` keyword, you can use type guarding by extending the `ServerFunctions` and `ClientFunctions` interfaces.
123 |
124 | ```typescript
125 | declare module "vite-plugin-devtools" {
126 | export interface ServerFunctions {
127 | here(): string;
128 | }
129 | export interface ClientFunctions {
130 | ping(): string;
131 | }
132 | }
133 | ```
134 |
135 | This allows for better development experience with autocompletion and type checking.
136 |
137 | ## Utilities
138 |
139 | Common utilities that may be used by devtools plugins.
140 |
141 | ```ts
142 | interface DefaultServerFunctions {
143 | staticAssets(): Promise
144 | getImageMeta(filepath: string): Promise
145 | getTextAssetContent(filepath: string, limit?: number): Promise
146 | getPackages(): Promise<{ packages: Record }>
147 | }
148 | ```
149 | These are all available by default in `clientRPC`.
150 |
151 |
--------------------------------------------------------------------------------
/src/node/views/Main.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
85 |
86 |
87 |
194 |
--------------------------------------------------------------------------------
/src/node/views/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | computed, getCurrentScope, onScopeDispose, ref, watch, watchEffect,
3 | } from 'vue'
4 | import type { Ref } from 'vue'
5 |
6 | export function tryOnScopeDispose(fn: () => void) {
7 | const scope = getCurrentScope()
8 | if (scope)
9 | onScopeDispose(fn)
10 | }
11 |
12 | // ---- storage ----
13 | export function useObjectStorage(key: string, initial: T, readonly = false): Ref {
14 | const raw = localStorage.getItem(key)
15 | const data = ref(raw ? JSON.parse(raw) : initial)
16 |
17 | for (const key in initial) {
18 | if (data.value[key] === undefined)
19 | data.value[key] = initial[key]
20 | }
21 |
22 | let updating = false
23 | let wrote = ''
24 |
25 | if (!readonly) {
26 | watch(data, (value) => {
27 | if (updating)
28 | return
29 | wrote = JSON.stringify(value)
30 | localStorage.setItem(key, wrote)
31 | }, { deep: true, flush: 'post' })
32 | }
33 |
34 | const storageChanged = (newValue: string) => {
35 | updating = true
36 | data.value = JSON.parse(newValue)
37 | updating = false
38 | }
39 |
40 | useWindowEventListener('storage', (e: StorageEvent) => {
41 | if (e.key === key && e.newValue && e.newValue !== wrote)
42 | storageChanged(e.newValue)
43 | })
44 |
45 | // @ts-expect-error custom event
46 | useWindowEventListener('vueuse-storage', (e: CustomEvent) => {
47 | const event = e.detail as StorageEvent
48 | if (event.key === key && event.newValue && event.newValue !== wrote)
49 | storageChanged(event.newValue)
50 | })
51 |
52 | return data
53 | }
54 |
55 | export function useStorage(key: string, initial: T, readonly = false) {
56 | const raw = localStorage.getItem(key)
57 | const data = ref(raw || initial)
58 |
59 | let updating = false
60 | let wrote = ''
61 |
62 | if (!readonly) {
63 | watch(data, (value) => {
64 | if (updating)
65 | return
66 | wrote = `${value}`
67 | localStorage.setItem(key, wrote)
68 | }, { deep: true, flush: 'post' })
69 | }
70 |
71 | const storageChanged = (newValue: string) => {
72 | updating = true
73 | data.value = newValue
74 | updating = false
75 | }
76 |
77 | useWindowEventListener('storage', (e: StorageEvent) => {
78 | if (e.key === key && e.newValue && e.newValue !== wrote)
79 | storageChanged(e.newValue)
80 | })
81 |
82 | // @ts-expect-error custom event
83 | useWindowEventListener('vueuse-storage', (e: CustomEvent) => {
84 | const event = e.detail as StorageEvent
85 | if (event.key === key && event.newValue && event.newValue !== wrote)
86 | storageChanged(event.newValue)
87 | })
88 |
89 | return data
90 | }
91 |
92 | // ---- index ----
93 | export const checkIsSafari = () => navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome')
94 | export function clamp(value: number, min: number, max: number) {
95 | return Math.min(Math.max(value, min), max)
96 | }
97 |
98 | // ---- event ----
99 | export function useEventListener(
100 | target: EventTarget,
101 | type: keyof WindowEventHandlersEventMap,
102 | listener: EventListenerOrEventListenerObject,
103 | options?: boolean | AddEventListenerOptions,
104 | ) {
105 | target.addEventListener(type, listener, options)
106 | tryOnScopeDispose(() => target.removeEventListener(type, listener, options))
107 | }
108 |
109 | export function useWindowEventListener(
110 | type: K,
111 | listener: (this: Window, ev: WindowEventMap[K]) => any,
112 | options?: boolean | AddEventListenerOptions,
113 | ) {
114 | useEventListener(window, type as keyof WindowEventHandlersEventMap, listener as EventListener, options)
115 | }
116 |
117 | // ---- screen ----
118 | const topVarName = '--vite-plugin-devtools-safe-area-top'
119 | const rightVarName = '--vite-plugin-devtools-devtools-safe-area-right'
120 | const bottomVarName = '--vite-plugin-devtools-safe-area-bottom'
121 | const leftVarName = '--vite-plugin-devtools-safe-area-left'
122 |
123 | /**
124 | * Reactive `env(safe-area-inset-*)`
125 | *
126 | * @see https://vueuse.org/useScreenSafeArea
127 | */
128 | export function useScreenSafeArea() {
129 | const top = ref(0)
130 | const right = ref(0)
131 | const bottom = ref(0)
132 | const left = ref(0)
133 |
134 | document.documentElement.style.setProperty(topVarName, 'env(safe-area-inset-top, 0px)')
135 | document.documentElement.style.setProperty(rightVarName, 'env(safe-area-inset-right, 0px)')
136 | document.documentElement.style.setProperty(bottomVarName, 'env(safe-area-inset-bottom, 0px)')
137 | document.documentElement.style.setProperty(leftVarName, 'env(safe-area-inset-left, 0px)')
138 |
139 | update()
140 | useWindowEventListener('resize', update)
141 |
142 | function getValue(position: string) {
143 | return Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue(position)) || 0
144 | }
145 |
146 | function update() {
147 | top.value = getValue(topVarName)
148 | right.value = getValue(rightVarName)
149 | bottom.value = getValue(bottomVarName)
150 | left.value = getValue(leftVarName)
151 | }
152 |
153 | return {
154 | top,
155 | right,
156 | bottom,
157 | left,
158 | update,
159 | }
160 | }
161 |
162 |
163 |
164 | /**
165 | * Reactive Media Query.
166 | *
167 | * @see https://vueuse.org/useMediaQuery
168 | * @param query
169 | * @param options
170 | */
171 | export function useMediaQuery(query: string) {
172 | const isSupported = () => window && 'matchMedia' in window && typeof window.matchMedia === 'function'
173 |
174 | let mediaQuery: MediaQueryList | undefined
175 | const matches = ref(false)
176 |
177 | const cleanup = () => {
178 | if (!mediaQuery)
179 | return
180 | if ('removeEventListener' in mediaQuery)
181 | // eslint-disable-next-line @typescript-eslint/no-use-before-define
182 | mediaQuery.removeEventListener('change', update)
183 | else
184 | // @ts-expect-error deprecated API
185 | // eslint-disable-next-line @typescript-eslint/no-use-before-define
186 | mediaQuery.removeListener(update)
187 | }
188 |
189 | const update = () => {
190 | if (!isSupported)
191 | return
192 |
193 | cleanup()
194 |
195 | mediaQuery = window!.matchMedia(ref(query).value)
196 | matches.value = !!mediaQuery?.matches
197 |
198 | if (!mediaQuery)
199 | return
200 |
201 | if ('addEventListener' in mediaQuery)
202 | mediaQuery.addEventListener('change', update)
203 | else
204 | // @ts-expect-error deprecated API
205 | mediaQuery.addListener(update)
206 | }
207 | watchEffect(update)
208 |
209 | tryOnScopeDispose(() => cleanup())
210 |
211 | return matches
212 | }
213 | /**
214 | * Reactive prefers-color-scheme media query.
215 | *
216 | * @see https://vueuse.org/usePreferredColorScheme
217 | * @param [options]
218 | */
219 | export function usePreferredColorScheme() {
220 | const isDark = useMediaQuery('(prefers-color-scheme: dark)')
221 |
222 | return computed(() => isDark.value ? 'dark' : 'light')
223 | }
224 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import { fileURLToPath } from "node:url";
3 | import { normalizePath } from "vite";
4 | import type {
5 | PluginOption,
6 | HtmlTagDescriptor,
7 | ViteDevServer,
8 | Plugin,
9 | } from "vite";
10 | import { createBirpcGroup } from "birpc";
11 | import sirv from "sirv";
12 | import type {
13 | ClientFunctions,
14 | DefaultServerFunctions,
15 | ServerFunction,
16 | ServerFunctions,
17 | ToServerFunction,
18 | } from "./types";
19 | import { attachWebSocket, Config } from "./middlewares/attach";
20 | import { getPackages } from "./functions/packages";
21 | import {
22 | getImageMeta,
23 | getStaticAssets,
24 | getTextAssetContent,
25 | } from "./functions/assets";
26 | export type * from "./types";
27 |
28 | type Options = {
29 | icon: string;
30 | clientDir: string;
31 | inspector?: boolean;
32 | onIframe?: string;
33 |
34 | /**
35 | * append an import to the module id ending with `appendTo` instead of adding a
36 | * script into body useful for projects that do not use html file as an entry
37 | *
38 | * WARNING: only set this if you know exactly what it does.
39 | */
40 | appendTo?: string | RegExp;
41 | };
42 |
43 | function getDevtoolsPath() {
44 | const pluginPath = normalizePath(
45 | path.dirname(fileURLToPath(import.meta.url))
46 | );
47 | return pluginPath
48 | .replace(/\/src/, "/src/node")
49 | .replace(/\/dist$/, "/src/node");
50 | }
51 |
52 | export type KeyOf = keyof T extends any ? keyof T : keyof T;
53 |
54 | function addServerFunctionToConfig(
55 | config: Config,
56 | name: T,
57 | func: ToServerFunction
58 | ): void;
59 | function addServerFunctionToConfig(config: Config, func: ServerFunction): void;
60 | function addServerFunctionToConfig(
61 | config: Config,
62 | nameOrFunc: T | ServerFunction,
63 | func?: ToServerFunction
64 | ): void {
65 | func = typeof nameOrFunc === "function" ? nameOrFunc : func;
66 | const name = typeof nameOrFunc === "string" ? nameOrFunc : func?.name;
67 | if (!func) {
68 | throw new Error("Please specify a server function");
69 | }
70 | if (!name) {
71 | throw new Error("Please specify a name for your server function");
72 | }
73 | // @ts-ignore
74 | config.serverFunctions[name] = func.bind(config.serverRPC);
75 | }
76 |
77 | interface AddServerFunction {
78 | (name: T, func: ToServerFunction): void;
79 | (func: ServerFunction): void;
80 | }
81 |
82 | // useful for detecting only one time `views/dist/view.js` run
83 | let firstPlugin: string;
84 | let appendTo: string | RegExp | undefined
85 |
86 | export function getBase(name: string) {
87 | return `/__${name}_devtools__/`;
88 | }
89 |
90 | export default function createDevtools<
91 | T extends keyof ServerFunctions = keyof ServerFunctions
92 | >(name: string, options: Options) {
93 | appendTo = options.appendTo
94 | let server: ViteDevServer;
95 | const config: Config = {
96 | serverFunctions: {
97 | staticAssets: () => getStaticAssets(server.config),
98 | getImageMeta,
99 | getTextAssetContent,
100 | getPackages: () => getPackages(server.config.root),
101 | } as DefaultServerFunctions as any,
102 | serverRPC: null,
103 | };
104 |
105 | const rpc = createBirpcGroup<
106 | ClientFunctions,
107 | DefaultServerFunctions & ServerFunctions
108 | >(config.serverFunctions as any, [], {});
109 | config.serverRPC = rpc.broadcast;
110 |
111 | const devtoolsPath = getDevtoolsPath();
112 | let iframeSrc: string;
113 |
114 | return {
115 | serverRPC: config.serverRPC,
116 | addServerFunction: addServerFunctionToConfig.bind(
117 | null,
118 | config
119 | ) as unknown as AddServerFunction,
120 | plugin: {
121 | name,
122 | enforce: "pre",
123 | apply: "serve",
124 | configureServer(_server) {
125 | server = _server;
126 | const base = server.config.base || "/";
127 | iframeSrc = `${base}__${name}_devtools__`;
128 | server.middlewares.use(
129 | iframeSrc,
130 | sirv(options.clientDir, { single: true, dev: true })
131 | );
132 | attachWebSocket(rpc, iframeSrc, server);
133 | },
134 | async resolveId(importee: string, importer) {
135 | if (importee.startsWith(`virtual:devtools:${name}:`)) {
136 | const resolvedAppPath = importee.replace(
137 | `virtual:devtools:${name}:`,
138 | `${devtoolsPath}/`
139 | );
140 | return { id: resolvedAppPath, external: "absolute" };
141 | }
142 | },
143 | transform(code, id) {
144 | if (!appendTo)
145 | return
146 |
147 | let imports: string | undefined
148 | if (!firstPlugin) {
149 | firstPlugin = name;
150 | imports = `
151 | import "virtual:devtools:${name}:views/dist/view.js"
152 | import "virtual:devtools:${name}:views/dist/style.css"
153 | `
154 | }
155 |
156 | const [filename] = id.split('?', 2)
157 | if ((typeof appendTo === 'string' && filename.endsWith(appendTo))
158 | || (appendTo instanceof RegExp && appendTo.test(filename)))
159 | return { code: `${code}\n
160 | globalThis.__devtools ??= []
161 | globalThis.__devtools.push({
162 | name: '${name}',
163 | iframeSrc: '${iframeSrc}',
164 | icon: \`${options.icon}\`,
165 | inspector: ${!!options?.inspector},
166 | onIframe: '${options.onIframe}'
167 | })
168 | ${imports}` }
169 | },
170 | transformIndexHtml(html) {
171 | if (appendTo) {
172 | return
173 | }
174 | if (!firstPlugin) {
175 | firstPlugin = name;
176 | }
177 | const tags: HtmlTagDescriptor[] = [
178 | {
179 | tag: "script",
180 | injectTo: "head-prepend",
181 | children: `
182 | globalThis.__devtools ??= []
183 | globalThis.__devtools.push({
184 | name: '${name}',
185 | iframeSrc: '${iframeSrc}',
186 | icon: \`${options.icon}\`,
187 | inspector: ${!!options?.inspector},
188 | onIframe: '${options.onIframe}'
189 | })
190 | `,
191 | attrs: {
192 | type: "module",
193 | },
194 | },
195 | ...(firstPlugin === name
196 | ? ([
197 | {
198 | tag: "script",
199 | injectTo: "head",
200 | attrs: {
201 | type: "module",
202 | src: `/@id/virtual:devtools:${name}:views/dist/view.js`,
203 | },
204 | },
205 | {
206 | tag: "link",
207 | injectTo: "head",
208 | attrs: {
209 | rel: "stylesheet",
210 | href: `/@id/virtual:devtools:${name}:views/dist/style.css`,
211 | },
212 | },
213 | ] satisfies HtmlTagDescriptor[])
214 | : []),
215 | ];
216 | return {
217 | html,
218 | tags,
219 | };
220 | },
221 | } satisfies Plugin,
222 | };
223 | }
224 |
--------------------------------------------------------------------------------
/src/node/views/FrameBox.vue:
--------------------------------------------------------------------------------
1 |
123 |
124 |
125 |