@u-devtools/core 0.1.6 → 0.2.1
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.
- package/LICENSE +1 -2
- package/README.md +50 -8
- package/dist/index.cjs.js +46 -1
- package/dist/index.d.ts +871 -64
- package/dist/index.es.js +3113 -56
- package/dist/vite/vite.config.base.cjs.js +1 -0
- package/dist/vite/vite.config.base.d.ts +59 -0
- package/dist/vite/vite.config.base.js +91 -0
- package/dist/vite.config.base.d.ts +42 -0
- package/package.json +25 -18
- package/src/bridge-app.ts +198 -51
- package/src/control.ts +23 -52
- package/src/event-bus.ts +126 -0
- package/src/index.ts +476 -44
- package/src/schemas/rpc.ts +36 -0
- package/src/schemas/settings.ts +125 -0
- package/src/transport.ts +172 -0
- package/src/transports/broadcast-transport.ts +174 -0
- package/src/transports/hmr-transport.ts +82 -0
- package/src/transports/websocket-transport.ts +158 -0
- package/vite/vite.config.base.ts +82 -15
- package/vite/clean-timestamp-plugin.ts +0 -28
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=require("vite"),d=require("@vitejs/plugin-vue"),x=require("vite-plugin-dts"),n=require("node:path"),f=require("node:fs");function T(r){return{name:"clean-timestamp-files",buildStart(){try{f.readdirSync(r).forEach(t=>{if(t.includes(".timestamp-")&&t.endsWith(".mjs"))try{f.unlinkSync(n.join(r,t))}catch{}})}catch{}}}}function E({entry:r,name:u,dir:t,external:c=[],clearScreen:p=!1,useVue:l=!0,formats:m=["es","cjs"],fileName:j,dtsOptions:i={},additionalPlugins:v=[],resolveAlias:a,cssCodeSplit:y}){const o=[];l&&o.push(d()),o.push(x({rollupTypes:i.rollupTypes??!(i.insertTypesEntry??!1),insertTypesEntry:i.insertTypesEntry??!1,exclude:i.exclude,copyDtsFiles:i.copyDtsFiles,tsconfigPath:n.resolve(t,"tsconfig.json"),outDir:n.resolve(t,"dist"),compilerOptions:{removeComments:!1}})),o.push(T(t)),o.push(...v);const h=typeof r=="string"?n.resolve(t,r):Object.fromEntries(Object.entries(r).map(([e,s])=>[e,n.resolve(t,s)])),b=(e,s)=>s&&s!=="index"?`${s}.${e==="es"?"es":"cjs"}.js`:`index.${e==="es"?"es":"cjs"}.js`;return g.defineConfig({clearScreen:p,plugins:o,define:{"import.meta.hot":"import.meta.hot"},resolve:{extensions:[".mjs",".js",".mts",".ts",".jsx",".tsx",".json",".vue"],...a?{alias:Object.fromEntries(Object.entries(a).map(([e,s])=>[e,n.resolve(t,s)]))}:{}},build:{lib:{entry:h,name:u,fileName:j??b,formats:m},cssCodeSplit:y,rollupOptions:{external:e=>!!(e==="vite"||l&&e==="vue"||e.startsWith("@u-devtools/")||e.startsWith("node:")||c.includes(e)),output:{globals:l?{vue:"Vue"}:{}}}}})}exports.createViteConfig=E;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type PluginOption } from 'vite';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration options for createViteConfig
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export interface ConfigOptions {
|
|
7
|
+
entry: string | Record<string, string>;
|
|
8
|
+
name: string;
|
|
9
|
+
dir: string;
|
|
10
|
+
external?: string[];
|
|
11
|
+
clearScreen?: boolean;
|
|
12
|
+
useVue?: boolean;
|
|
13
|
+
formats?: ('es' | 'cjs')[];
|
|
14
|
+
fileName?: string | ((format: string, entryName?: string) => string);
|
|
15
|
+
dtsOptions?: {
|
|
16
|
+
insertTypesEntry?: boolean;
|
|
17
|
+
exclude?: string[];
|
|
18
|
+
rollupTypes?: boolean;
|
|
19
|
+
copyDtsFiles?: boolean;
|
|
20
|
+
};
|
|
21
|
+
additionalPlugins?: PluginOption[];
|
|
22
|
+
resolveAlias?: Record<string, string>;
|
|
23
|
+
cssCodeSplit?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates a Vite configuration optimized for building DevTools packages.
|
|
27
|
+
*
|
|
28
|
+
* This function generates a Vite config with proper TypeScript declaration file generation,
|
|
29
|
+
* preserving JSDoc comments in .d.ts files for better IDE autocomplete.
|
|
30
|
+
*
|
|
31
|
+
* @param options - Configuration options for the Vite build
|
|
32
|
+
* @param options.entry - Entry point(s) for the library (string or record of entry points)
|
|
33
|
+
* @param options.name - Library name (used for UMD builds)
|
|
34
|
+
* @param options.dir - Package directory (usually __dirname)
|
|
35
|
+
* @param options.external - Array of module IDs to externalize
|
|
36
|
+
* @param options.clearScreen - Whether to clear screen on build (default: false)
|
|
37
|
+
* @param options.useVue - Whether to include Vue plugin (default: true)
|
|
38
|
+
* @param options.formats - Output formats: 'es' and/or 'cjs' (default: ['es', 'cjs'])
|
|
39
|
+
* @param options.fileName - Custom file naming function or string
|
|
40
|
+
* @param options.dtsOptions - Options for TypeScript declaration file generation
|
|
41
|
+
* @param options.additionalPlugins - Additional Vite plugins to include
|
|
42
|
+
* @param options.resolveAlias - Path aliases for module resolution
|
|
43
|
+
* @param options.cssCodeSplit - Whether to enable CSS code splitting
|
|
44
|
+
* @returns Vite configuration object
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { createViteConfig } from '@u-devtools/core/vite.config.base';
|
|
49
|
+
*
|
|
50
|
+
* export default createViteConfig({
|
|
51
|
+
* name: 'MyPackage',
|
|
52
|
+
* entry: 'src/index.ts',
|
|
53
|
+
* dir: __dirname,
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
export declare function createViteConfig({ entry, name, dir, external, clearScreen, useVue, formats, fileName, dtsOptions, additionalPlugins, resolveAlias, cssCodeSplit, }: ConfigOptions): import("vite").UserConfig;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { defineConfig as x } from "vite";
|
|
2
|
+
import b from "@vitejs/plugin-vue";
|
|
3
|
+
import g from "vite-plugin-dts";
|
|
4
|
+
import { resolve as o, join as E } from "node:path";
|
|
5
|
+
import { readdirSync as T, unlinkSync as d } from "node:fs";
|
|
6
|
+
function F(r) {
|
|
7
|
+
return {
|
|
8
|
+
name: "clean-timestamp-files",
|
|
9
|
+
buildStart() {
|
|
10
|
+
try {
|
|
11
|
+
T(r).forEach((t) => {
|
|
12
|
+
if (t.includes(".timestamp-") && t.endsWith(".mjs"))
|
|
13
|
+
try {
|
|
14
|
+
d(E(r, t));
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function O({
|
|
24
|
+
entry: r,
|
|
25
|
+
name: u,
|
|
26
|
+
dir: t,
|
|
27
|
+
external: c = [],
|
|
28
|
+
clearScreen: m = !1,
|
|
29
|
+
useVue: l = !0,
|
|
30
|
+
formats: p = ["es", "cjs"],
|
|
31
|
+
fileName: a,
|
|
32
|
+
dtsOptions: n = {},
|
|
33
|
+
additionalPlugins: j = [],
|
|
34
|
+
resolveAlias: f,
|
|
35
|
+
cssCodeSplit: h
|
|
36
|
+
}) {
|
|
37
|
+
const i = [];
|
|
38
|
+
l && i.push(b()), i.push(
|
|
39
|
+
g({
|
|
40
|
+
rollupTypes: n.rollupTypes ?? !(n.insertTypesEntry ?? !1),
|
|
41
|
+
insertTypesEntry: n.insertTypesEntry ?? !1,
|
|
42
|
+
exclude: n.exclude,
|
|
43
|
+
copyDtsFiles: n.copyDtsFiles,
|
|
44
|
+
tsconfigPath: o(t, "tsconfig.json"),
|
|
45
|
+
outDir: o(t, "dist"),
|
|
46
|
+
compilerOptions: {
|
|
47
|
+
removeComments: !1
|
|
48
|
+
// Explicitly preserve JSDoc comments
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
), i.push(F(t)), i.push(...j);
|
|
52
|
+
const y = typeof r == "string" ? o(t, r) : Object.fromEntries(Object.entries(r).map(([e, s]) => [e, o(t, s)])), v = (e, s) => s && s !== "index" ? `${s}.${e === "es" ? "es" : "cjs"}.js` : `index.${e === "es" ? "es" : "cjs"}.js`;
|
|
53
|
+
return x({
|
|
54
|
+
clearScreen: m,
|
|
55
|
+
plugins: i,
|
|
56
|
+
// IMPORTANT: This prevents replacing import.meta.hot with false during build
|
|
57
|
+
// Now code in dist will contain check if (import.meta.hot)
|
|
58
|
+
// and HMR will work even in built version
|
|
59
|
+
define: {
|
|
60
|
+
"import.meta.hot": "import.meta.hot"
|
|
61
|
+
},
|
|
62
|
+
resolve: {
|
|
63
|
+
extensions: [".mjs", ".js", ".mts", ".ts", ".jsx", ".tsx", ".json", ".vue"],
|
|
64
|
+
...f ? {
|
|
65
|
+
alias: Object.fromEntries(
|
|
66
|
+
Object.entries(f).map(([e, s]) => [e, o(t, s)])
|
|
67
|
+
)
|
|
68
|
+
} : {}
|
|
69
|
+
},
|
|
70
|
+
build: {
|
|
71
|
+
lib: {
|
|
72
|
+
entry: y,
|
|
73
|
+
name: u,
|
|
74
|
+
fileName: a ?? v,
|
|
75
|
+
formats: p
|
|
76
|
+
},
|
|
77
|
+
cssCodeSplit: h,
|
|
78
|
+
rollupOptions: {
|
|
79
|
+
external: (e) => !!(e === "vite" || l && e === "vue" || e.startsWith("@u-devtools/") || e.startsWith("node:") || c.includes(e)),
|
|
80
|
+
output: {
|
|
81
|
+
globals: l ? {
|
|
82
|
+
vue: "Vue"
|
|
83
|
+
} : {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
O as createViteConfig
|
|
91
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { PluginOption } from 'vite';
|
|
2
|
+
import { UserConfig } from 'vite';
|
|
3
|
+
|
|
4
|
+
/* Excluded from this release type: ConfigOptions */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Vite configuration optimized for building DevTools packages.
|
|
8
|
+
*
|
|
9
|
+
* This function generates a Vite config with proper TypeScript declaration file generation,
|
|
10
|
+
* preserving JSDoc comments in .d.ts files for better IDE autocomplete.
|
|
11
|
+
*
|
|
12
|
+
* @param options - Configuration options for the Vite build
|
|
13
|
+
* @param options.entry - Entry point(s) for the library (string or record of entry points)
|
|
14
|
+
* @param options.name - Library name (used for UMD builds)
|
|
15
|
+
* @param options.dir - Package directory (usually __dirname)
|
|
16
|
+
* @param options.external - Array of module IDs to externalize
|
|
17
|
+
* @param options.clearScreen - Whether to clear screen on build (default: false)
|
|
18
|
+
* @param options.useVue - Whether to include Vue plugin (default: true)
|
|
19
|
+
* @param options.formats - Output formats: 'es' and/or 'cjs' (default: ['es', 'cjs'])
|
|
20
|
+
* @param options.fileName - Custom file naming function or string
|
|
21
|
+
* @param options.dtsOptions - Options for TypeScript declaration file generation
|
|
22
|
+
* @param options.additionalPlugins - Additional Vite plugins to include
|
|
23
|
+
* @param options.resolveAlias - Path aliases for module resolution
|
|
24
|
+
* @param options.cssCodeSplit - Whether to enable CSS code splitting
|
|
25
|
+
* @returns Vite configuration object
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { createViteConfig } from '@u-devtools/core/vite.config.base';
|
|
30
|
+
*
|
|
31
|
+
* export default createViteConfig({
|
|
32
|
+
* name: 'MyPackage',
|
|
33
|
+
* entry: 'src/index.ts',
|
|
34
|
+
* dir: __dirname,
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
export declare function createViteConfig({ entry, name, dir, external, clearScreen, useVue, formats, fileName, dtsOptions, additionalPlugins, resolveAlias, cssCodeSplit, }: ConfigOptions): UserConfig;
|
|
41
|
+
|
|
42
|
+
export { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@u-devtools/core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Core types and interfaces for Universal DevTools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"devtools",
|
|
@@ -22,33 +22,40 @@
|
|
|
22
22
|
"main": "./dist/index.cjs.js",
|
|
23
23
|
"module": "./dist/index.es.js",
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.es.js",
|
|
29
|
+
"require": "./dist/index.cjs.js"
|
|
30
|
+
},
|
|
31
|
+
"./vite/vite.config.base": {
|
|
32
|
+
"types": "./dist/vite/vite.config.base.d.ts",
|
|
33
|
+
"import": "./dist/vite/vite.config.base.js"
|
|
34
|
+
},
|
|
35
|
+
"./vite.config.base": {
|
|
36
|
+
"types": "./dist/vite/vite.config.base.d.ts",
|
|
37
|
+
"import": "./dist/vite/vite.config.base.js"
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
25
41
|
"files": [
|
|
26
42
|
"dist",
|
|
27
43
|
"src",
|
|
28
44
|
"vite",
|
|
29
|
-
"README.md"
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
30
47
|
],
|
|
31
48
|
"devDependencies": {
|
|
32
49
|
"@types/node": "^20.19.27",
|
|
33
|
-
"typescript": "^5.9.3"
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"vite": "^7.3.0"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"zod": "^4.3.5",
|
|
55
|
+
"@u-devtools/utils": "^0.2.0"
|
|
34
56
|
},
|
|
35
57
|
"scripts": {
|
|
36
58
|
"build": "vite build",
|
|
37
59
|
"typecheck": "tsc --noEmit"
|
|
38
|
-
},
|
|
39
|
-
"exports": {
|
|
40
|
-
".": {
|
|
41
|
-
"types": "./dist/index.d.ts",
|
|
42
|
-
"import": "./dist/index.es.js",
|
|
43
|
-
"require": "./dist/index.cjs.js"
|
|
44
|
-
},
|
|
45
|
-
"./vite.config.base": {
|
|
46
|
-
"types": "./vite/vite.config.base.ts",
|
|
47
|
-
"import": "./vite/vite.config.base.ts"
|
|
48
|
-
},
|
|
49
|
-
"./clean-timestamp-plugin": {
|
|
50
|
-
"types": "./vite/clean-timestamp-plugin.ts",
|
|
51
|
-
"import": "./vite/clean-timestamp-plugin.ts"
|
|
52
|
-
}
|
|
53
60
|
}
|
|
54
61
|
}
|
package/src/bridge-app.ts
CHANGED
|
@@ -1,67 +1,214 @@
|
|
|
1
|
+
import { BroadcastTransport } from './transports/broadcast-transport';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
+
* Universal state class with automatic synchronization between App and Client contexts.
|
|
5
|
+
* Implements "Handshake" protocol for getting current data on initialization.
|
|
6
|
+
*
|
|
7
|
+
* Use this for state that needs to be shared between App context (main window)
|
|
8
|
+
* and Client context (DevTools iframe). Changes are automatically synchronized.
|
|
9
|
+
*
|
|
10
|
+
* @template T - Type of the state value
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { AppBridge } from '@u-devtools/core';
|
|
15
|
+
*
|
|
16
|
+
* // Create bridge
|
|
17
|
+
* const bridge = new AppBridge('my-plugin');
|
|
18
|
+
*
|
|
19
|
+
* // Create synced state
|
|
20
|
+
* const isOpen = bridge.state('isOpen', false);
|
|
21
|
+
* const count = bridge.state('count', 0);
|
|
22
|
+
*
|
|
23
|
+
* // Update value (automatically syncs to Client)
|
|
24
|
+
* isOpen.value = true;
|
|
25
|
+
* count.value = 42;
|
|
26
|
+
*
|
|
27
|
+
* // Subscribe to changes
|
|
28
|
+
* const unsubscribe = isOpen.subscribe((value) => {
|
|
29
|
+
* console.log('State changed:', value);
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Cleanup
|
|
33
|
+
* unsubscribe();
|
|
34
|
+
* ```
|
|
4
35
|
*/
|
|
5
|
-
export class
|
|
6
|
-
private
|
|
7
|
-
private listeners = new
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
36
|
+
export class SyncedState<T> {
|
|
37
|
+
private _value: T;
|
|
38
|
+
private listeners = new Set<(val: T) => void>();
|
|
39
|
+
private isUpdating = false;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
private bridge: AppBridge<any>,
|
|
43
|
+
private key: string,
|
|
44
|
+
initialValue: T
|
|
45
|
+
) {
|
|
46
|
+
this._value = initialValue;
|
|
47
|
+
|
|
48
|
+
const syncEvent = `sync:${key}`;
|
|
49
|
+
const requestEvent = `request:${key}`; // Event for requesting current state
|
|
50
|
+
|
|
51
|
+
// 1. Listen for updates (SYNC)
|
|
52
|
+
this.bridge.on(syncEvent, ((data: unknown) => {
|
|
53
|
+
// Ignore echo (if we sent it ourselves)
|
|
54
|
+
if (this.isUpdating) return;
|
|
55
|
+
|
|
56
|
+
const newValue = data as T;
|
|
57
|
+
if (this._value !== newValue) {
|
|
58
|
+
this._value = newValue;
|
|
59
|
+
this.isUpdating = true; // Block sending back to prevent loop
|
|
60
|
+
this.notify();
|
|
61
|
+
this.isUpdating = false;
|
|
20
62
|
}
|
|
21
|
-
};
|
|
63
|
+
}) as (data: unknown) => void);
|
|
64
|
+
|
|
65
|
+
// 2. Listen for state requests (HANDSHAKE RESPONSE)
|
|
66
|
+
// If the other side (e.g., panel) just opened, it will ask for current value.
|
|
67
|
+
// We must respond with our current value.
|
|
68
|
+
this.bridge.on(requestEvent, () => {
|
|
69
|
+
// Send current value to all who asked
|
|
70
|
+
this.bridge.send(syncEvent, this._value);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// 3. Request current state (HANDSHAKE REQUEST)
|
|
74
|
+
// Right after creation, ask: "Hey, what's the current value?"
|
|
75
|
+
// This is critical if we loaded later than the other side.
|
|
76
|
+
this.bridge.send(requestEvent, {});
|
|
22
77
|
}
|
|
23
78
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*/
|
|
27
|
-
send(event: string, data?: unknown): void {
|
|
28
|
-
try {
|
|
29
|
-
this.channel.postMessage({ event, data });
|
|
30
|
-
} catch (e) {
|
|
31
|
-
// Ignore errors if channel is closed
|
|
32
|
-
if (
|
|
33
|
-
e instanceof DOMException &&
|
|
34
|
-
(e.name === 'InvalidStateError' || e.message?.includes('closed'))
|
|
35
|
-
) {
|
|
36
|
-
console.warn(`[AppBridge] Cannot send event "${event}": channel is closed`);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
throw e;
|
|
40
|
-
}
|
|
79
|
+
get value(): T {
|
|
80
|
+
return this._value;
|
|
41
81
|
}
|
|
42
82
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (handlers) {
|
|
54
|
-
handlers.add(wrappedCb);
|
|
83
|
+
set value(newValue: T) {
|
|
84
|
+
if (this._value !== newValue) {
|
|
85
|
+
this._value = newValue;
|
|
86
|
+
// Notify local subscribers
|
|
87
|
+
this.notify();
|
|
88
|
+
|
|
89
|
+
// Send to bridge if this is not an "incoming" change
|
|
90
|
+
if (!this.isUpdating) {
|
|
91
|
+
this.bridge.send(`sync:${this.key}`, newValue);
|
|
92
|
+
}
|
|
55
93
|
}
|
|
94
|
+
}
|
|
56
95
|
|
|
57
|
-
|
|
96
|
+
subscribe = (fn: (val: T) => void): () => void => {
|
|
97
|
+
this.listeners.add(fn);
|
|
98
|
+
fn(this._value);
|
|
58
99
|
return () => {
|
|
59
|
-
this.listeners.
|
|
100
|
+
this.listeners.delete(fn);
|
|
60
101
|
};
|
|
61
102
|
}
|
|
62
103
|
|
|
104
|
+
getSnapshot = (): T => {
|
|
105
|
+
return this._value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private notify() {
|
|
109
|
+
this.listeners.forEach((fn) => {
|
|
110
|
+
fn(this._value);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* AppBridge - Typed communication bridge between App context (main window) and Client context (DevTools iframe).
|
|
117
|
+
*
|
|
118
|
+
* Communication: App ↔ Client via BroadcastChannel API
|
|
119
|
+
*
|
|
120
|
+
* Provides type-safe event-based communication with automatic state synchronization.
|
|
121
|
+
*
|
|
122
|
+
* @template Protocol - Type definition for events and their handlers
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* import { AppBridge } from '@u-devtools/core';
|
|
127
|
+
*
|
|
128
|
+
* // Define protocol for type-safe communication
|
|
129
|
+
* interface MyPluginProtocol {
|
|
130
|
+
* 'element-selected': (data: { id: string; html: string }) => void;
|
|
131
|
+
* 'toggle-inspector': (data: { state: boolean }) => void;
|
|
132
|
+
* }
|
|
133
|
+
*
|
|
134
|
+
* const typedBridge = new AppBridge<MyPluginProtocol>('my-plugin');
|
|
135
|
+
*
|
|
136
|
+
* // Send events (type-safe)
|
|
137
|
+
* typedBridge.send('element-selected', { id: 'el-1', html: '<div>...</div>' });
|
|
138
|
+
*
|
|
139
|
+
* // Listen to events (type-safe)
|
|
140
|
+
* typedBridge.on('toggle-inspector', ({ state }) => {
|
|
141
|
+
* // state is automatically typed as { state: boolean }
|
|
142
|
+
* console.log('Inspector toggled:', state);
|
|
143
|
+
* });
|
|
144
|
+
*
|
|
145
|
+
* // Create synced state
|
|
146
|
+
* const selectedElement = typedBridge.state<HTMLElement | null>('selectedElement', null);
|
|
147
|
+
* selectedElement.value = document.getElementById('my-element');
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export class AppBridge<Protocol = Record<string, (...args: any[]) => any>> {
|
|
151
|
+
private transport: BroadcastTransport;
|
|
152
|
+
public namespace: string; // Normalized name for bridge (lowercase)
|
|
153
|
+
public displayName: string; // Original name for UI
|
|
154
|
+
|
|
155
|
+
constructor(namespace: string) {
|
|
156
|
+
// Normalize namespace to lowercase and replace spaces with dashes for BroadcastChannel compatibility
|
|
157
|
+
// but keep original name for UI display
|
|
158
|
+
this.namespace = namespace.toLowerCase().replace(/\s+/g, '-');
|
|
159
|
+
this.displayName = namespace;
|
|
160
|
+
this.transport = new BroadcastTransport(this.namespace);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
send<K extends keyof Protocol>(
|
|
164
|
+
event: K,
|
|
165
|
+
...args: Protocol[K] extends (...args: infer P) => any ? P : []
|
|
166
|
+
): void {
|
|
167
|
+
const payload = args.length === 1 ? args[0] : args.length > 1 ? args : undefined;
|
|
168
|
+
this.transport.send(event as string, payload);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
on<K extends keyof Protocol>(
|
|
172
|
+
event: K,
|
|
173
|
+
cb: Protocol[K] extends (...args: any[]) => any
|
|
174
|
+
? (data: Parameters<Protocol[K]>[0]) => void
|
|
175
|
+
: never
|
|
176
|
+
): () => void {
|
|
177
|
+
return this.transport.on(event as string, cb as (data: unknown) => void);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
request<RequestData, ResponseData>(
|
|
181
|
+
requestEvent: string,
|
|
182
|
+
requestData: RequestData,
|
|
183
|
+
responseEvent: string,
|
|
184
|
+
timeout = 5000,
|
|
185
|
+
responseFilter?: (request: RequestData, response: ResponseData) => boolean
|
|
186
|
+
): Promise<ResponseData> {
|
|
187
|
+
return new Promise<ResponseData>((resolve, reject) => {
|
|
188
|
+
const timeoutId = setTimeout(() => {
|
|
189
|
+
unsubscribe();
|
|
190
|
+
reject(new Error(`Request timeout: ${requestEvent} -> ${responseEvent}`));
|
|
191
|
+
}, timeout);
|
|
192
|
+
|
|
193
|
+
const unsubscribe = this.transport.on(responseEvent, (data: unknown) => {
|
|
194
|
+
const response = data as ResponseData;
|
|
195
|
+
if (responseFilter && !responseFilter(requestData, response)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
clearTimeout(timeoutId);
|
|
199
|
+
unsubscribe();
|
|
200
|
+
resolve(response);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
this.transport.send(requestEvent, requestData);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
63
207
|
close() {
|
|
64
|
-
this.
|
|
65
|
-
|
|
208
|
+
this.transport.close();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
state<T>(key: string, initialValue: T): SyncedState<T> {
|
|
212
|
+
return new SyncedState(this, key, initialValue);
|
|
66
213
|
}
|
|
67
214
|
}
|