@u-devtools/core 0.1.5 → 0.2.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 @@
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"?"js":"cjs"}`:`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" ? "js" : "cjs"}` : `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.5",
3
+ "version": "0.2.0",
4
4
  "description": "Core types and interfaces for Universal DevTools",
5
5
  "keywords": [
6
6
  "devtools",
@@ -22,32 +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
- "vite"
44
+ "vite",
45
+ "README.md",
46
+ "LICENSE"
29
47
  ],
30
48
  "devDependencies": {
31
49
  "@types/node": "^20.19.27",
32
- "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"
33
56
  },
34
57
  "scripts": {
35
58
  "build": "vite build",
36
59
  "typecheck": "tsc --noEmit"
37
- },
38
- "exports": {
39
- ".": {
40
- "types": "./dist/index.d.ts",
41
- "import": "./dist/index.es.js",
42
- "require": "./dist/index.cjs.js"
43
- },
44
- "./vite.config.base": {
45
- "types": "./vite/vite.config.base.ts",
46
- "import": "./vite/vite.config.base.ts"
47
- },
48
- "./clean-timestamp-plugin": {
49
- "types": "./vite/clean-timestamp-plugin.ts",
50
- "import": "./vite/clean-timestamp-plugin.ts"
51
- }
52
60
  }
53
61
  }
package/src/bridge-app.ts CHANGED
@@ -1,67 +1,214 @@
1
+ import { BroadcastTransport } from './transports/broadcast-transport';
2
+
1
3
  /**
2
- * AppBridge provides a standardized way for plugins to communicate
3
- * between the application runtime (window) and the DevTools iframe.
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 AppBridge {
6
- private channel: BroadcastChannel;
7
- private listeners = new Map<string, Set<(data: unknown) => void>>();
8
-
9
- constructor(public namespace: string) {
10
- // Автоматическое пространство имен
11
- this.channel = new BroadcastChannel(`u-devtools:${namespace}`);
12
-
13
- this.channel.onmessage = (e) => {
14
- const { event, data } = e.data as { event: string; data: unknown };
15
- const handlers = this.listeners.get(event);
16
- if (handlers) {
17
- handlers.forEach((fn) => {
18
- fn(data);
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
- on<T = unknown>(event: string, cb: (data: T) => void): () => void {
47
- const eventStr = String(event);
48
- if (!this.listeners.has(eventStr)) {
49
- this.listeners.set(eventStr, new Set());
50
- }
51
- const handlers = this.listeners.get(eventStr);
52
- const wrappedCb = cb as (data: unknown) => void;
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.get(eventStr)?.delete(wrappedCb);
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.channel.close();
65
- this.listeners.clear();
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
  }