"添加前端模板和运行代码模块"
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { UpdateState } from './types';
|
||||
|
||||
/**
|
||||
* History Manager
|
||||
* Manages the history of updates for undo/redo functionality
|
||||
*/
|
||||
export class HistoryManager {
|
||||
private history: UpdateState[] = [];
|
||||
|
||||
/**
|
||||
* Add an update to history
|
||||
*/
|
||||
public add(update: UpdateState) {
|
||||
this.history.push(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all history
|
||||
*/
|
||||
public getHistory(): UpdateState[] {
|
||||
return [...this.history];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear history
|
||||
*/
|
||||
public clear() {
|
||||
this.history = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last update
|
||||
* Moves the update from history to redo stack
|
||||
*/
|
||||
public undo(): UpdateState | null {
|
||||
const update = this.history.pop();
|
||||
if (update) {
|
||||
update.status = 'reverted';
|
||||
this.redoStack.push(update);
|
||||
}
|
||||
return update || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last reverted update for redo
|
||||
* Note: Since we are changing how undo works (popping), we need to handle redo differently.
|
||||
* If we want to support redo, we should store the undone updates.
|
||||
*/
|
||||
private redoStack: UpdateState[] = [];
|
||||
|
||||
/**
|
||||
* Redo the last undone update
|
||||
* Moves the update from redo stack to history
|
||||
*/
|
||||
public redo(): UpdateState | null {
|
||||
const update = this.redoStack.pop();
|
||||
if (update) {
|
||||
update.status = 'completed';
|
||||
this.history.push(update);
|
||||
}
|
||||
return update || null;
|
||||
}
|
||||
|
||||
// To maintain backward compatibility with the "broken" logic (if it was indeed broken),
|
||||
// or to fix it. The user asked to refactor, which implies improving maintainability.
|
||||
// Fixing a bug is also good.
|
||||
// However, I should be careful not to change behavior if the user relies on it (even if buggy).
|
||||
// But "redo not working" is hardly a feature.
|
||||
|
||||
// I'll implement `undo` to return the update, and let the caller handle the DOM restoration.
|
||||
// The manager just manages the state.
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { UpdateState, UpdateResult, UpdateManagerConfig } from './types';
|
||||
|
||||
/**
|
||||
* UpdateService
|
||||
* Handles update processing, validation, and server synchronization
|
||||
*/
|
||||
export class UpdateService {
|
||||
private processingUpdates = new Set<string>();
|
||||
|
||||
constructor(
|
||||
private config: UpdateManagerConfig,
|
||||
private onComplete: (update: UpdateState) => void,
|
||||
private onFail: (update: UpdateState) => void
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Process a single update
|
||||
*/
|
||||
public async processUpdate(update: UpdateState): Promise<UpdateResult> {
|
||||
// Validate update
|
||||
if (!this.validateUpdate(update)) {
|
||||
return {
|
||||
success: false,
|
||||
element: update.element,
|
||||
updateId: update.id,
|
||||
error: 'Update validation failed',
|
||||
};
|
||||
}
|
||||
|
||||
// Mark as processing
|
||||
update.status = 'processing';
|
||||
this.processingUpdates.add(update.id);
|
||||
|
||||
try {
|
||||
// Apply DOM update
|
||||
this.applyUpdateToDOM(update);
|
||||
|
||||
let serverResponse;
|
||||
// Save to source via API (only if persist is true)
|
||||
if (update.persist) {
|
||||
serverResponse = await this.saveToSource(update);
|
||||
} else {
|
||||
serverResponse = { success: true, preview: true };
|
||||
}
|
||||
|
||||
// Mark as completed
|
||||
update.status = 'completed';
|
||||
this.onComplete(update);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
element: update.element,
|
||||
updateId: update.id,
|
||||
serverResponse,
|
||||
};
|
||||
} catch (error) {
|
||||
update.status = 'failed';
|
||||
update.error = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
||||
// Retry mechanism
|
||||
if (update.retryCount < this.config.maxRetries) {
|
||||
update.retryCount++;
|
||||
return this.processUpdate(update);
|
||||
}
|
||||
|
||||
this.onFail(update);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
element: update.element,
|
||||
updateId: update.id,
|
||||
error: update.error,
|
||||
};
|
||||
} finally {
|
||||
this.processingUpdates.delete(update.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process batch update
|
||||
*/
|
||||
public async processBatchUpdate(
|
||||
updates: UpdateState[]
|
||||
): Promise<UpdateResult[]> {
|
||||
try {
|
||||
// Build batch API request
|
||||
const batchRequest = {
|
||||
updates: updates.map(update => ({
|
||||
filePath: update.sourceInfo.fileName,
|
||||
line: update.sourceInfo.lineNumber,
|
||||
column: update.sourceInfo.columnNumber,
|
||||
type: (update.operation === 'style_update' || update.operation === 'class_update') ? 'style' : 'content',
|
||||
newValue: update.newValue,
|
||||
originalValue: update.oldValue,
|
||||
})),
|
||||
};
|
||||
|
||||
// Call batch API
|
||||
const response = await fetch('/__appdev_design_mode/batch-update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(batchRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Batch update failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const results = await response.json();
|
||||
|
||||
// Mark all updates as completed
|
||||
updates.forEach(update => {
|
||||
update.status = 'completed';
|
||||
this.onComplete(update);
|
||||
});
|
||||
|
||||
return results.map((result: any, index: number) => ({
|
||||
success: result.success,
|
||||
element: updates[index].element,
|
||||
updateId: updates[index].id,
|
||||
serverResponse: result,
|
||||
}));
|
||||
} catch (error) {
|
||||
// Mark all updates as failed
|
||||
updates.forEach(update => {
|
||||
update.status = 'failed';
|
||||
update.error = error instanceof Error ? error.message : 'Unknown error';
|
||||
this.onFail(update);
|
||||
});
|
||||
|
||||
return updates.map(update => ({
|
||||
success: false,
|
||||
element: update.element,
|
||||
updateId: update.id,
|
||||
error: update.error,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply update to DOM
|
||||
*/
|
||||
private applyUpdateToDOM(update: UpdateState) {
|
||||
const { element, operation, newValue } = update;
|
||||
|
||||
switch (operation) {
|
||||
case 'style_update':
|
||||
case 'class_update':
|
||||
element.className = newValue;
|
||||
break;
|
||||
case 'content_update':
|
||||
element.innerText = newValue;
|
||||
break;
|
||||
case 'attribute_update':
|
||||
// This should get attribute name from sourceInfo
|
||||
// Currently simplified to set data-attribute
|
||||
element.setAttribute('data-updated-value', newValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save to source file via API
|
||||
*/
|
||||
private async saveToSource(update: UpdateState): Promise<any> {
|
||||
const response = await fetch('/__appdev_design_mode/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filePath: update.sourceInfo.fileName,
|
||||
line: update.sourceInfo.lineNumber,
|
||||
column: update.sourceInfo.columnNumber,
|
||||
newValue: update.newValue,
|
||||
originalValue: update.oldValue,
|
||||
type: (update.operation === 'style_update' || update.operation === 'class_update') ? 'style' : 'content',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to save source: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate update before processing
|
||||
*/
|
||||
private validateUpdate(update: UpdateState): boolean {
|
||||
// Validate source mapping
|
||||
if (this.config.validation.validateSource) {
|
||||
if (
|
||||
!update.sourceInfo.fileName ||
|
||||
update.sourceInfo.lineNumber === null ||
|
||||
update.sourceInfo.columnNumber === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate value
|
||||
if (this.config.validation.validateValue) {
|
||||
if (update.newValue.length > this.config.validation.maxLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if update is processing
|
||||
*/
|
||||
public isProcessing(updateId: string): boolean {
|
||||
return this.processingUpdates.has(updateId);
|
||||
}
|
||||
}
|
||||
37
qiming-vite-plugin-design-mode/packages/client-shared/src/attributeNames.d.ts
vendored
Normal file
37
qiming-vite-plugin-design-mode/packages/client-shared/src/attributeNames.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Resolved `data-*` names for source mapping (prefix from global config or meta tag).
|
||||
*/
|
||||
/** e.g. `data-xagi` */
|
||||
export declare function getPrefix(): string;
|
||||
/** Call after changing `window.__APPDEV_DESIGN_MODE_CONFIG__` in tests. */
|
||||
export declare function resetPrefixCache(): void;
|
||||
/** `{prefix}-{suffix}` */
|
||||
export declare function getAttributeName(suffix: string): string;
|
||||
/** Lazy getters so prefix changes propagate */
|
||||
export declare const AttributeNames: {
|
||||
readonly file: string;
|
||||
readonly line: string;
|
||||
readonly column: string;
|
||||
readonly info: string;
|
||||
readonly elementId: string;
|
||||
readonly component: string;
|
||||
readonly function: string;
|
||||
readonly position: string;
|
||||
readonly staticContent: string;
|
||||
/**
|
||||
* Get the children source attribute name (for tracking where static children come from)
|
||||
* Example: data-xagi-children-source
|
||||
*/
|
||||
readonly childrenSource: string;
|
||||
readonly contextMenu: string;
|
||||
readonly contextMenuHover: string;
|
||||
readonly import: string;
|
||||
/**
|
||||
* Get the static-class attribute name (for tracking if className is a pure static string)
|
||||
* Example: data-xagi-static-class
|
||||
*/
|
||||
readonly staticClass: string;
|
||||
};
|
||||
/** True for any `{prefix}-*` */
|
||||
export declare function isSourceMappingAttribute(attributeName: string): boolean;
|
||||
//# sourceMappingURL=attributeNames.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"attributeNames.d.ts","sourceRoot":"","sources":["attributeNames.ts"],"names":[],"mappings":"AAAA;;GAEG;AA0BH,uBAAuB;AACvB,wBAAgB,SAAS,IAAI,MAAM,CAKlC;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED,0BAA0B;AAC1B,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,+CAA+C;AAC/C,eAAO,MAAM,cAAc;;;;;;;;;;IA4BzB;;;OAGG;;;;;IAaH;;;OAGG;;CAIK,CAAC;AAEX,gCAAgC;AAChC,wBAAgB,wBAAwB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAGvE"}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Resolved `data-*` names for source mapping (prefix from global config or meta tag).
|
||||
*/
|
||||
const DEFAULT_PREFIX = 'data-xagi';
|
||||
function getAttributePrefix() {
|
||||
if (typeof window !== 'undefined') {
|
||||
const globalConfig = window.__APPDEV_DESIGN_MODE_CONFIG__;
|
||||
if (globalConfig?.attributePrefix) {
|
||||
return globalConfig.attributePrefix;
|
||||
}
|
||||
const metaTag = document.querySelector('meta[name="appdev-design-mode:attribute-prefix"]');
|
||||
if (metaTag) {
|
||||
const prefix = metaTag.getAttribute('content');
|
||||
if (prefix) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
return DEFAULT_PREFIX;
|
||||
}
|
||||
/** Cached prefix */
|
||||
let cachedPrefix = null;
|
||||
/** e.g. `data-xagi` */
|
||||
export function getPrefix() {
|
||||
if (cachedPrefix === null) {
|
||||
cachedPrefix = getAttributePrefix();
|
||||
}
|
||||
return cachedPrefix;
|
||||
}
|
||||
/** Call after changing `window.__APPDEV_DESIGN_MODE_CONFIG__` in tests. */
|
||||
export function resetPrefixCache() {
|
||||
cachedPrefix = null;
|
||||
}
|
||||
/** `{prefix}-{suffix}` */
|
||||
export function getAttributeName(suffix) {
|
||||
const prefix = getPrefix();
|
||||
return `${prefix}-${suffix}`;
|
||||
}
|
||||
/** Lazy getters so prefix changes propagate */
|
||||
export const AttributeNames = {
|
||||
get file() {
|
||||
return getAttributeName('file');
|
||||
},
|
||||
get line() {
|
||||
return getAttributeName('line');
|
||||
},
|
||||
get column() {
|
||||
return getAttributeName('column');
|
||||
},
|
||||
get info() {
|
||||
return getAttributeName('info');
|
||||
},
|
||||
get elementId() {
|
||||
return getAttributeName('element-id');
|
||||
},
|
||||
get component() {
|
||||
return getAttributeName('component');
|
||||
},
|
||||
get function() {
|
||||
return getAttributeName('function');
|
||||
},
|
||||
get position() {
|
||||
return getAttributeName('position');
|
||||
},
|
||||
get staticContent() {
|
||||
return getAttributeName('static-content');
|
||||
},
|
||||
/**
|
||||
* Get the children source attribute name (for tracking where static children come from)
|
||||
* Example: data-xagi-children-source
|
||||
*/
|
||||
get childrenSource() {
|
||||
return getAttributeName('children-source');
|
||||
},
|
||||
get contextMenu() {
|
||||
return getAttributeName('context-menu');
|
||||
},
|
||||
get contextMenuHover() {
|
||||
return getAttributeName('context-menu-hover');
|
||||
},
|
||||
get import() {
|
||||
return getAttributeName('import');
|
||||
},
|
||||
/**
|
||||
* Get the static-class attribute name (for tracking if className is a pure static string)
|
||||
* Example: data-xagi-static-class
|
||||
*/
|
||||
get staticClass() {
|
||||
return getAttributeName('static-class');
|
||||
},
|
||||
};
|
||||
/** True for any `{prefix}-*` */
|
||||
export function isSourceMappingAttribute(attributeName) {
|
||||
const prefix = getPrefix();
|
||||
return attributeName.startsWith(`${prefix}-`);
|
||||
}
|
||||
//# sourceMappingURL=attributeNames.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"attributeNames.js","sourceRoot":"","sources":["attributeNames.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,cAAc,GAAG,WAAW,CAAC;AAEnC,SAAS,kBAAkB;IACzB,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,MAAM,YAAY,GAAI,MAAc,CAAC,6BAA6B,CAAC;QACnE,IAAI,YAAY,EAAE,eAAe,EAAE,CAAC;YAClC,OAAO,YAAY,CAAC,eAAe,CAAC;QACtC,CAAC;QAED,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,kDAAkD,CAAC,CAAC;QAC3F,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YAC/C,IAAI,MAAM,EAAE,CAAC;gBACX,OAAO,MAAM,CAAC;YAChB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,oBAAoB;AACpB,IAAI,YAAY,GAAkB,IAAI,CAAC;AAEvC,uBAAuB;AACvB,MAAM,UAAU,SAAS;IACvB,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC1B,YAAY,GAAG,kBAAkB,EAAE,CAAC;IACtC,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,gBAAgB;IAC9B,YAAY,GAAG,IAAI,CAAC;AACtB,CAAC;AAED,0BAA0B;AAC1B,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,OAAO,GAAG,MAAM,IAAI,MAAM,EAAE,CAAC;AAC/B,CAAC;AAED,+CAA+C;AAC/C,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,IAAI,IAAI;QACN,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,IAAI;QACN,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,MAAM;QACR,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,IAAI;QACN,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,SAAS;QACX,OAAO,gBAAgB,CAAC,YAAY,CAAC,CAAC;IACxC,CAAC;IACD,IAAI,SAAS;QACX,OAAO,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACvC,CAAC;IACD,IAAI,QAAQ;QACV,OAAO,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACtC,CAAC;IACD,IAAI,QAAQ;QACV,OAAO,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACtC,CAAC;IACD,IAAI,aAAa;QACf,OAAO,gBAAgB,CAAC,gBAAgB,CAAC,CAAC;IAC5C,CAAC;IACD;;;OAGG;IACH,IAAI,cAAc;QAChB,OAAO,gBAAgB,CAAC,iBAAiB,CAAC,CAAC;IAC7C,CAAC;IACD,IAAI,WAAW;QACb,OAAO,gBAAgB,CAAC,cAAc,CAAC,CAAC;IAC1C,CAAC;IACD,IAAI,gBAAgB;QAClB,OAAO,gBAAgB,CAAC,oBAAoB,CAAC,CAAC;IAChD,CAAC;IACD,IAAI,MAAM;QACR,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;IACD;;;OAGG;IACH,IAAI,WAAW;QACb,OAAO,gBAAgB,CAAC,cAAc,CAAC,CAAC;IAC1C,CAAC;CACO,CAAC;AAEX,gCAAgC;AAChC,MAAM,UAAU,wBAAwB,CAAC,aAAqB;IAC5D,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,OAAO,aAAa,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC;AAChD,CAAC"}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Resolved `data-*` names for source mapping (prefix from global config or meta tag).
|
||||
*/
|
||||
|
||||
const DEFAULT_PREFIX = 'data-xagi';
|
||||
|
||||
function getAttributePrefix(): string {
|
||||
if (typeof window !== 'undefined') {
|
||||
const globalConfig = (window as any).__APPDEV_DESIGN_MODE_CONFIG__;
|
||||
if (globalConfig?.attributePrefix) {
|
||||
return globalConfig.attributePrefix;
|
||||
}
|
||||
|
||||
const metaTag = document.querySelector('meta[name="appdev-design-mode:attribute-prefix"]');
|
||||
if (metaTag) {
|
||||
const prefix = metaTag.getAttribute('content');
|
||||
if (prefix) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_PREFIX;
|
||||
}
|
||||
|
||||
/** Cached prefix */
|
||||
let cachedPrefix: string | null = null;
|
||||
|
||||
/** e.g. `data-xagi` */
|
||||
export function getPrefix(): string {
|
||||
if (cachedPrefix === null) {
|
||||
cachedPrefix = getAttributePrefix();
|
||||
}
|
||||
return cachedPrefix;
|
||||
}
|
||||
|
||||
/** Call after changing `window.__APPDEV_DESIGN_MODE_CONFIG__` in tests. */
|
||||
export function resetPrefixCache(): void {
|
||||
cachedPrefix = null;
|
||||
}
|
||||
|
||||
/** `{prefix}-{suffix}` */
|
||||
export function getAttributeName(suffix: string): string {
|
||||
const prefix = getPrefix();
|
||||
return `${prefix}-${suffix}`;
|
||||
}
|
||||
|
||||
/** Lazy getters so prefix changes propagate */
|
||||
export const AttributeNames = {
|
||||
get file() {
|
||||
return getAttributeName('file');
|
||||
},
|
||||
get line() {
|
||||
return getAttributeName('line');
|
||||
},
|
||||
get column() {
|
||||
return getAttributeName('column');
|
||||
},
|
||||
get info() {
|
||||
return getAttributeName('info');
|
||||
},
|
||||
get elementId() {
|
||||
return getAttributeName('element-id');
|
||||
},
|
||||
get component() {
|
||||
return getAttributeName('component');
|
||||
},
|
||||
get function() {
|
||||
return getAttributeName('function');
|
||||
},
|
||||
get position() {
|
||||
return getAttributeName('position');
|
||||
},
|
||||
get staticContent() {
|
||||
return getAttributeName('static-content');
|
||||
},
|
||||
/**
|
||||
* Get the children source attribute name (for tracking where static children come from)
|
||||
* Example: data-xagi-children-source
|
||||
*/
|
||||
get childrenSource() {
|
||||
return getAttributeName('children-source');
|
||||
},
|
||||
get contextMenu() {
|
||||
return getAttributeName('context-menu');
|
||||
},
|
||||
get contextMenuHover() {
|
||||
return getAttributeName('context-menu-hover');
|
||||
},
|
||||
get import() {
|
||||
return getAttributeName('import');
|
||||
},
|
||||
/**
|
||||
* Get the static-class attribute name (for tracking if className is a pure static string)
|
||||
* Example: data-xagi-static-class
|
||||
*/
|
||||
get staticClass() {
|
||||
return getAttributeName('static-class');
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** True for any `{prefix}-*` */
|
||||
export function isSourceMappingAttribute(attributeName: string): boolean {
|
||||
const prefix = getPrefix();
|
||||
return attributeName.startsWith(`${prefix}-`);
|
||||
}
|
||||
|
||||
97
qiming-vite-plugin-design-mode/packages/client-shared/src/bridge.d.ts
vendored
Normal file
97
qiming-vite-plugin-design-mode/packages/client-shared/src/bridge.d.ts
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
import { DesignModeMessage, MessageValidator, BridgeInterface, BridgeConfig, MessageUtilsInterface, MessageValidationResult } from './messages';
|
||||
/**
|
||||
* postMessage bridge: optional request/response, validation hooks, heartbeats.
|
||||
*/
|
||||
export declare class EnhancedBridge implements BridgeInterface {
|
||||
private listeners;
|
||||
private pendingRequests;
|
||||
private config;
|
||||
private _isConnected;
|
||||
private lastHeartbeat;
|
||||
private heartbeatTimer?;
|
||||
private connectionCheckTimer?;
|
||||
constructor(config?: Partial<BridgeConfig>);
|
||||
/** Listen for `message` and mark connected after a short delay (iframe vs top). */
|
||||
private initializeMessageHandling;
|
||||
/** Notify parent that the child iframe bridge is ready. */
|
||||
private sendReadyMessage;
|
||||
/**
|
||||
* Periodic HEARTBEAT posts when running inside an iframe.
|
||||
*/
|
||||
private initializeHeartbeat;
|
||||
/** Periodically infer disconnect from stale heartbeats. */
|
||||
private initializeConnectionCheck;
|
||||
/** True when `window.self !== window.top`. */
|
||||
private isIframeEnvironment;
|
||||
/** Snapshot for diagnostics / health. */
|
||||
getEnvironmentInfo(): {
|
||||
isIframe: boolean;
|
||||
isConnected: boolean;
|
||||
origin: string;
|
||||
userAgent: string;
|
||||
location: string;
|
||||
};
|
||||
/** Log bridge state to the console (dev aid). */
|
||||
diagnose(): void;
|
||||
/** Parent when iframe; `window` when top-level. */
|
||||
private getTargetWindow;
|
||||
/** Route incoming postMessage payloads. */
|
||||
private handleMessage;
|
||||
/** Resolve or reject pendingRequests entry. */
|
||||
private handleResponseMessage;
|
||||
/** Shape check before dispatch. */
|
||||
private isValidMessage;
|
||||
/** Known DesignModeMessage union tags. */
|
||||
private isSupportedMessageType;
|
||||
/** ACK or carries requestId. */
|
||||
private isResponseMessage;
|
||||
/**
|
||||
* Fire-and-forget postMessage (may no-op if not connected).
|
||||
*/
|
||||
send<T extends DesignModeMessage>(message: T): Promise<void>;
|
||||
/** RPC-style: wait until matching response or timeout. */
|
||||
sendWithResponse<T extends DesignModeMessage, R extends DesignModeMessage>(message: T, responseType: R['type']): Promise<R>;
|
||||
/** Ensure requestId + timestamp exist. */
|
||||
private enhanceMessage;
|
||||
/** Unique id for correlating responses. */
|
||||
private generateRequestId;
|
||||
/** ms since epoch */
|
||||
private createTimestamp;
|
||||
/** Best-effort ACK (messageType not wired through yet). */
|
||||
private sendAcknowledgement;
|
||||
/** HEARTBEAT post to parent. */
|
||||
private sendHeartbeat;
|
||||
/** Mark disconnected if no heartbeats for 3× interval. */
|
||||
private checkConnection;
|
||||
/** Subscribe; returns unsubscribe. */
|
||||
on<T extends DesignModeMessage>(type: T['type'], handler: (message: T) => void): () => void;
|
||||
/** Remove one handler for `type`. */
|
||||
off(type: string, handler: Function): void;
|
||||
/** Invoke all listeners for message.type */
|
||||
private dispatchMessage;
|
||||
/** `_isConnected` flag */
|
||||
isConnected(): boolean;
|
||||
/** Same as `isConnected` (legacy name). */
|
||||
isConnectedToTarget(): boolean;
|
||||
/** Clear timers, reject pendings, remove listener. */
|
||||
disconnect(): void;
|
||||
/** Summarize connectivity; may issue HEALTH_CHECK RPC in iframe. */
|
||||
healthCheck(): Promise<{
|
||||
status: string;
|
||||
details: any;
|
||||
}>;
|
||||
/** Guarded by config.debug */
|
||||
private log;
|
||||
/** Alias for disconnect(). */
|
||||
destroy(): void;
|
||||
}
|
||||
/** Small helpers for request ids / timestamps. */
|
||||
export declare const MessageUtils: MessageUtilsInterface;
|
||||
/** Singleton used by the design-mode client bundle. */
|
||||
export declare const bridge: EnhancedBridge;
|
||||
/** Structural validation for a subset of message types. */
|
||||
export declare class MessageValidatorImpl implements MessageValidator {
|
||||
validate(message: any): MessageValidationResult;
|
||||
}
|
||||
export declare const messageValidator: MessageValidatorImpl;
|
||||
//# sourceMappingURL=bridge.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["bridge.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,503 @@
|
||||
/**
|
||||
* postMessage bridge: optional request/response, validation hooks, heartbeats.
|
||||
*/
|
||||
export class EnhancedBridge {
|
||||
constructor(config = {}) {
|
||||
this.listeners = new Map();
|
||||
this.pendingRequests = new Map();
|
||||
this._isConnected = false;
|
||||
this.lastHeartbeat = 0;
|
||||
this.config = {
|
||||
timeout: 10000,
|
||||
retryAttempts: 3,
|
||||
retryDelay: 1000,
|
||||
heartbeatInterval: 30000,
|
||||
debug: false,
|
||||
...config
|
||||
};
|
||||
this.initializeMessageHandling();
|
||||
this.initializeHeartbeat();
|
||||
this.initializeConnectionCheck();
|
||||
}
|
||||
/** Listen for `message` and mark connected after a short delay (iframe vs top). */
|
||||
initializeMessageHandling() {
|
||||
if (typeof window === 'undefined')
|
||||
return;
|
||||
window.addEventListener('message', this.handleMessage.bind(this));
|
||||
if (this.isIframeEnvironment()) {
|
||||
setTimeout(() => {
|
||||
this._isConnected = true;
|
||||
this.log('Bridge initialized and connected (iframe mode)');
|
||||
this.sendReadyMessage();
|
||||
}, 200);
|
||||
}
|
||||
else {
|
||||
setTimeout(() => {
|
||||
this._isConnected = true;
|
||||
this.log('Bridge initialized and connected (main window mode)');
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
/** Notify parent that the child iframe bridge is ready. */
|
||||
sendReadyMessage() {
|
||||
try {
|
||||
const readyMessage = {
|
||||
type: 'BRIDGE_READY',
|
||||
payload: {
|
||||
timestamp: this.createTimestamp(),
|
||||
environment: 'iframe'
|
||||
},
|
||||
timestamp: this.createTimestamp()
|
||||
};
|
||||
this.getTargetWindow().postMessage(readyMessage, '*');
|
||||
this.log('Sent ready message to parent');
|
||||
}
|
||||
catch (error) {
|
||||
this.log('Failed to send ready message:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Periodic HEARTBEAT posts when running inside an iframe.
|
||||
*/
|
||||
initializeHeartbeat() {
|
||||
if (typeof window === 'undefined')
|
||||
return;
|
||||
if (this.isIframeEnvironment()) {
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
this.sendHeartbeat();
|
||||
}, this.config.heartbeatInterval);
|
||||
}
|
||||
}
|
||||
/** Periodically infer disconnect from stale heartbeats. */
|
||||
initializeConnectionCheck() {
|
||||
if (typeof window === 'undefined')
|
||||
return;
|
||||
this.connectionCheckTimer = setInterval(() => {
|
||||
this.checkConnection();
|
||||
}, this.config.heartbeatInterval * 2);
|
||||
}
|
||||
/** True when `window.self !== window.top`. */
|
||||
isIframeEnvironment() {
|
||||
return typeof window !== 'undefined' && window.self !== window.top;
|
||||
}
|
||||
/** Snapshot for diagnostics / health. */
|
||||
getEnvironmentInfo() {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
isIframe: false,
|
||||
isConnected: false,
|
||||
origin: '',
|
||||
userAgent: '',
|
||||
location: ''
|
||||
};
|
||||
}
|
||||
return {
|
||||
isIframe: this.isIframeEnvironment(),
|
||||
isConnected: this._isConnected,
|
||||
origin: window.location.origin,
|
||||
userAgent: navigator.userAgent.substring(0, 100),
|
||||
location: window.location.href
|
||||
};
|
||||
}
|
||||
/** Log bridge state to the console (dev aid). */
|
||||
diagnose() {
|
||||
const env = this.getEnvironmentInfo();
|
||||
console.group('[EnhancedBridge] Bridge Diagnosis');
|
||||
console.log('Environment:', env);
|
||||
console.log('Connection Status:', this._isConnected);
|
||||
console.log('Pending Requests:', this.pendingRequests.size);
|
||||
console.log('Message Listeners:', Array.from(this.listeners.keys()));
|
||||
console.log('Config:', this.config);
|
||||
console.groupEnd();
|
||||
}
|
||||
/** Parent when iframe; `window` when top-level. */
|
||||
getTargetWindow() {
|
||||
return this.isIframeEnvironment() ? window.parent : window;
|
||||
}
|
||||
/** Route incoming postMessage payloads. */
|
||||
handleMessage(event) {
|
||||
// Optional origin filter (disabled by default):
|
||||
// if (event.origin !== window.location.origin && window.location.origin !== 'null') {
|
||||
// if (!event.origin.startsWith('http') && !event.origin.startsWith('https')) {
|
||||
// this.log('Received message from different origin, allowing:', event.origin);
|
||||
// } else {
|
||||
// this.log('Skipping message from different origin:', event.origin);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
const message = event.data;
|
||||
// Drop malformed payloads
|
||||
if (!this.isValidMessage(message)) {
|
||||
this.log('Invalid message received:', message);
|
||||
return;
|
||||
}
|
||||
// Child handshake
|
||||
if (message.type === 'BRIDGE_READY') {
|
||||
this.log('Received ready message from child');
|
||||
this._isConnected = true;
|
||||
return;
|
||||
}
|
||||
// Completes a pending sendWithResponse
|
||||
if (this.isResponseMessage(message)) {
|
||||
this.handleResponseMessage(message);
|
||||
return;
|
||||
}
|
||||
// Fan-out to on(type) handlers
|
||||
this.dispatchMessage(message);
|
||||
}
|
||||
/** Resolve or reject pendingRequests entry. */
|
||||
handleResponseMessage(message) {
|
||||
const { requestId } = message;
|
||||
if (this.pendingRequests.has(requestId)) {
|
||||
const request = this.pendingRequests.get(requestId);
|
||||
clearTimeout(request.timeout);
|
||||
this.pendingRequests.delete(requestId);
|
||||
if (message.type === 'ACKNOWLEDGEMENT') {
|
||||
request.resolve({ success: true, acknowledged: true });
|
||||
}
|
||||
else {
|
||||
request.resolve(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Shape check before dispatch. */
|
||||
isValidMessage(message) {
|
||||
return (message &&
|
||||
typeof message.type === 'string' &&
|
||||
this.isSupportedMessageType(message.type));
|
||||
}
|
||||
/** Known DesignModeMessage union tags. */
|
||||
isSupportedMessageType(type) {
|
||||
const supportedTypes = [
|
||||
// Iframe to Parent
|
||||
'ELEMENT_SELECTED', 'ELEMENT_DESELECTED', 'CONTENT_UPDATED', 'STYLE_UPDATED',
|
||||
'DESIGN_MODE_CHANGED', 'ELEMENT_STATE_RESPONSE', 'ERROR', 'ACKNOWLEDGEMENT',
|
||||
'HEARTBEAT', 'HEALTH_CHECK_RESPONSE', 'BRIDGE_READY', 'ADD_TO_CHAT',
|
||||
// Parent to Iframe
|
||||
'TOGGLE_DESIGN_MODE', 'UPDATE_STYLE', 'UPDATE_CONTENT', 'BATCH_UPDATE',
|
||||
'HEALTH_CHECK'
|
||||
];
|
||||
return supportedTypes.includes(type);
|
||||
}
|
||||
/** ACK or carries requestId. */
|
||||
isResponseMessage(message) {
|
||||
return message &&
|
||||
(message.type === 'ACKNOWLEDGEMENT' ||
|
||||
message.requestId !== undefined) &&
|
||||
(message.timestamp !== undefined);
|
||||
}
|
||||
/**
|
||||
* Fire-and-forget postMessage (may no-op if not connected).
|
||||
*/
|
||||
async send(message) {
|
||||
if (!this._isConnected && this.isIframeEnvironment()) {
|
||||
this.log('Bridge not connected, attempting to reconnect...');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
if (!this._isConnected) {
|
||||
console.warn('[EnhancedBridge] Bridge still not connected, message will be queued');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!this._isConnected && !this.isIframeEnvironment()) {
|
||||
this.log('Running in main window, bridge connection not applicable');
|
||||
return;
|
||||
}
|
||||
const enhancedMessage = this.enhanceMessage(message);
|
||||
try {
|
||||
this.log('Sending message:', enhancedMessage);
|
||||
this.getTargetWindow().postMessage(enhancedMessage, '*');
|
||||
if (this.isIframeEnvironment() && enhancedMessage.requestId) {
|
||||
this.sendAcknowledgement(enhancedMessage.requestId);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
this.log('Error sending message:', error);
|
||||
// Dev: surface send failures
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
/** RPC-style: wait until matching response or timeout. */
|
||||
async sendWithResponse(message, responseType) {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('Bridge is not connected');
|
||||
}
|
||||
const enhancedMessage = this.enhanceMessage(message);
|
||||
const requestId = enhancedMessage.requestId;
|
||||
return new Promise((resolve, reject) => {
|
||||
// Timeout
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingRequests.delete(requestId);
|
||||
reject(new Error(`Request timeout after ${this.config.timeout}ms`));
|
||||
}, this.config.timeout);
|
||||
// Pending request map
|
||||
this.pendingRequests.set(requestId, {
|
||||
resolve,
|
||||
reject,
|
||||
timeout,
|
||||
responseType
|
||||
});
|
||||
try {
|
||||
this.log('Sending request with response:', enhancedMessage);
|
||||
this.getTargetWindow().postMessage(enhancedMessage, '*');
|
||||
}
|
||||
catch (error) {
|
||||
this.pendingRequests.delete(requestId);
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
/** Ensure requestId + timestamp exist. */
|
||||
enhanceMessage(message) {
|
||||
const requestId = message.requestId || this.generateRequestId();
|
||||
const timestamp = message.timestamp || this.createTimestamp();
|
||||
return {
|
||||
...message,
|
||||
requestId,
|
||||
timestamp
|
||||
};
|
||||
}
|
||||
/** Unique id for correlating responses. */
|
||||
generateRequestId() {
|
||||
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
/** ms since epoch */
|
||||
createTimestamp() {
|
||||
return Date.now();
|
||||
}
|
||||
/** Best-effort ACK (messageType not wired through yet). */
|
||||
sendAcknowledgement(requestId) {
|
||||
const acknowledgement = {
|
||||
type: 'ACKNOWLEDGEMENT',
|
||||
payload: {
|
||||
messageType: 'UNKNOWN', // TODO: propagate originating type
|
||||
},
|
||||
requestId,
|
||||
timestamp: this.createTimestamp()
|
||||
};
|
||||
this.getTargetWindow().postMessage(acknowledgement, '*');
|
||||
}
|
||||
/** HEARTBEAT post to parent. */
|
||||
sendHeartbeat() {
|
||||
const heartbeat = {
|
||||
type: 'HEARTBEAT',
|
||||
payload: {
|
||||
timestamp: this.createTimestamp()
|
||||
},
|
||||
timestamp: this.createTimestamp()
|
||||
};
|
||||
this.getTargetWindow().postMessage(heartbeat, '*');
|
||||
this.lastHeartbeat = Date.now();
|
||||
}
|
||||
/** Mark disconnected if no heartbeats for 3× interval. */
|
||||
checkConnection() {
|
||||
const now = Date.now();
|
||||
const timeSinceLastHeartbeat = now - this.lastHeartbeat;
|
||||
if (timeSinceLastHeartbeat > this.config.heartbeatInterval * 3) {
|
||||
this.log('Connection appears to be lost');
|
||||
this._isConnected = false;
|
||||
// Optimistic reconnect
|
||||
setTimeout(() => {
|
||||
this._isConnected = true;
|
||||
this.log('Connection restored');
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
/** Subscribe; returns unsubscribe. */
|
||||
on(type, handler) {
|
||||
if (!this.listeners.has(type)) {
|
||||
this.listeners.set(type, new Set());
|
||||
}
|
||||
this.listeners.get(type).add(handler);
|
||||
// Unsubscribe
|
||||
return () => {
|
||||
this.listeners.get(type)?.delete(handler);
|
||||
};
|
||||
}
|
||||
/** Remove one handler for `type`. */
|
||||
off(type, handler) {
|
||||
this.listeners.get(type)?.delete(handler);
|
||||
}
|
||||
/** Invoke all listeners for message.type */
|
||||
dispatchMessage(message) {
|
||||
const handlers = this.listeners.get(message.type);
|
||||
if (handlers) {
|
||||
handlers.forEach(handler => {
|
||||
try {
|
||||
handler(message);
|
||||
}
|
||||
catch (error) {
|
||||
this.log('Error in message handler:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
/** `_isConnected` flag */
|
||||
isConnected() {
|
||||
return this._isConnected;
|
||||
}
|
||||
/** Same as `isConnected` (legacy name). */
|
||||
isConnectedToTarget() {
|
||||
return this._isConnected;
|
||||
}
|
||||
/** Clear timers, reject pendings, remove listener. */
|
||||
disconnect() {
|
||||
this._isConnected = false;
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
}
|
||||
if (this.connectionCheckTimer) {
|
||||
clearInterval(this.connectionCheckTimer);
|
||||
}
|
||||
// Reject pending RPCs
|
||||
this.pendingRequests.forEach(request => {
|
||||
clearTimeout(request.timeout);
|
||||
request.reject(new Error('Connection disconnected'));
|
||||
});
|
||||
this.pendingRequests.clear();
|
||||
// Remove global listener
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('message', this.handleMessage.bind(this));
|
||||
}
|
||||
this.listeners.clear();
|
||||
this.log('Bridge disconnected');
|
||||
}
|
||||
/** Summarize connectivity; may issue HEALTH_CHECK RPC in iframe. */
|
||||
async healthCheck() {
|
||||
try {
|
||||
const env = this.getEnvironmentInfo();
|
||||
// Top-level window
|
||||
if (!env.isIframe && this._isConnected) {
|
||||
return {
|
||||
status: 'healthy',
|
||||
details: {
|
||||
...env,
|
||||
message: 'Bridge is healthy and running in main window'
|
||||
}
|
||||
};
|
||||
}
|
||||
if (env.isIframe && this._isConnected) {
|
||||
// Iframe: ping parent
|
||||
try {
|
||||
const response = await this.sendWithResponse({
|
||||
type: 'HEALTH_CHECK',
|
||||
requestId: '',
|
||||
timestamp: this.createTimestamp()
|
||||
}, 'HEALTH_CHECK_RESPONSE');
|
||||
return {
|
||||
status: 'healthy',
|
||||
details: {
|
||||
...env,
|
||||
response: response.payload,
|
||||
message: 'Bridge is healthy and responsive'
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
status: 'degraded',
|
||||
details: {
|
||||
...env,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: 'Bridge is connected but not responding to health checks'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: env.isIframe ? 'connecting' : 'unnecessary',
|
||||
details: {
|
||||
...env,
|
||||
message: env.isIframe ? 'Bridge is initializing' : 'Bridge not needed in main window'
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
details: {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
environment: this.getEnvironmentInfo()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
/** Guarded by config.debug */
|
||||
log(...args) {
|
||||
if (this.config.debug) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[${timestamp}] [EnhancedBridge]`, ...args);
|
||||
}
|
||||
}
|
||||
/** Alias for disconnect(). */
|
||||
destroy() {
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
/** Small helpers for request ids / timestamps. */
|
||||
export const MessageUtils = {
|
||||
generateRequestId: () => {
|
||||
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
},
|
||||
createTimestamp: () => {
|
||||
return Date.now();
|
||||
},
|
||||
isValidMessage: (message) => {
|
||||
return message &&
|
||||
typeof message.type === 'string' &&
|
||||
typeof message.timestamp === 'number';
|
||||
},
|
||||
createResponse: function (originalMessage, payload, success = true, error) {
|
||||
return {
|
||||
payload,
|
||||
type: payload?.type,
|
||||
requestId: originalMessage.requestId || '',
|
||||
timestamp: this.createTimestamp()
|
||||
};
|
||||
}
|
||||
};
|
||||
/** Singleton used by the design-mode client bundle. */
|
||||
export const bridge = new EnhancedBridge();
|
||||
/** Structural validation for a subset of message types. */
|
||||
export class MessageValidatorImpl {
|
||||
validate(message) {
|
||||
const errors = [];
|
||||
if (!message) {
|
||||
errors.push('Message is null or undefined');
|
||||
return { isValid: false, errors };
|
||||
}
|
||||
if (typeof message.type !== 'string') {
|
||||
errors.push('Message type must be a string');
|
||||
}
|
||||
if (typeof message.timestamp !== 'number') {
|
||||
errors.push('Message timestamp must be a number');
|
||||
}
|
||||
// Per-type payload checks
|
||||
switch (message.type) {
|
||||
case 'ELEMENT_SELECTED':
|
||||
if (!message.payload?.elementInfo) {
|
||||
errors.push('ELEMENT_SELECTED must have elementInfo in payload');
|
||||
}
|
||||
break;
|
||||
case 'UPDATE_STYLE':
|
||||
case 'UPDATE_CONTENT':
|
||||
if (!message.payload?.sourceInfo) {
|
||||
errors.push(`${message.type} must have sourceInfo in payload`);
|
||||
}
|
||||
break;
|
||||
case 'BATCH_UPDATE':
|
||||
if (!Array.isArray(message.payload?.updates)) {
|
||||
errors.push('BATCH_UPDATE must have updates array in payload');
|
||||
}
|
||||
break;
|
||||
}
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
export const messageValidator = new MessageValidatorImpl();
|
||||
//# sourceMappingURL=bridge.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,651 @@
|
||||
import {
|
||||
DesignModeMessage,
|
||||
IframeToParentMessage,
|
||||
ParentToIframeMessage,
|
||||
RequestResponseMessage,
|
||||
MessageValidator,
|
||||
BridgeInterface,
|
||||
BridgeConfig,
|
||||
MessageUtilsInterface,
|
||||
MessageValidationResult,
|
||||
AcknowledgementMessage,
|
||||
HealthCheckMessage,
|
||||
HealthCheckResponseMessage,
|
||||
HeartbeatMessage
|
||||
} from './messages';
|
||||
|
||||
/**
|
||||
* postMessage bridge: optional request/response, validation hooks, heartbeats.
|
||||
*/
|
||||
export class EnhancedBridge implements BridgeInterface {
|
||||
private listeners: Map<string, Set<Function>> = new Map();
|
||||
private pendingRequests: Map<string, {
|
||||
resolve: Function;
|
||||
reject: Function;
|
||||
timeout: NodeJS.Timeout;
|
||||
responseType: string;
|
||||
}> = new Map();
|
||||
|
||||
private config: BridgeConfig;
|
||||
private _isConnected = false;
|
||||
private lastHeartbeat = 0;
|
||||
private heartbeatTimer?: NodeJS.Timeout;
|
||||
private connectionCheckTimer?: NodeJS.Timeout;
|
||||
|
||||
constructor(config: Partial<BridgeConfig> = {}) {
|
||||
this.config = {
|
||||
timeout: 10000,
|
||||
retryAttempts: 3,
|
||||
retryDelay: 1000,
|
||||
heartbeatInterval: 30000,
|
||||
debug: false,
|
||||
...config
|
||||
};
|
||||
|
||||
this.initializeMessageHandling();
|
||||
this.initializeHeartbeat();
|
||||
this.initializeConnectionCheck();
|
||||
}
|
||||
|
||||
/** Listen for `message` and mark connected after a short delay (iframe vs top). */
|
||||
private initializeMessageHandling() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
window.addEventListener('message', this.handleMessage.bind(this));
|
||||
if (this.isIframeEnvironment()) {
|
||||
setTimeout(() => {
|
||||
this._isConnected = true;
|
||||
this.log('Bridge initialized and connected (iframe mode)');
|
||||
|
||||
this.sendReadyMessage();
|
||||
}, 200);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this._isConnected = true;
|
||||
this.log('Bridge initialized and connected (main window mode)');
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
/** Notify parent that the child iframe bridge is ready. */
|
||||
private sendReadyMessage() {
|
||||
try {
|
||||
const readyMessage = {
|
||||
type: 'BRIDGE_READY',
|
||||
payload: {
|
||||
timestamp: this.createTimestamp(),
|
||||
environment: 'iframe'
|
||||
},
|
||||
timestamp: this.createTimestamp()
|
||||
};
|
||||
this.getTargetWindow().postMessage(readyMessage, '*');
|
||||
this.log('Sent ready message to parent');
|
||||
} catch (error) {
|
||||
this.log('Failed to send ready message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic HEARTBEAT posts when running inside an iframe.
|
||||
*/
|
||||
private initializeHeartbeat() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
if (this.isIframeEnvironment()) {
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
this.sendHeartbeat();
|
||||
}, this.config.heartbeatInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/** Periodically infer disconnect from stale heartbeats. */
|
||||
private initializeConnectionCheck() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
this.connectionCheckTimer = setInterval(() => {
|
||||
this.checkConnection();
|
||||
}, this.config.heartbeatInterval * 2);
|
||||
}
|
||||
|
||||
/** True when `window.self !== window.top`. */
|
||||
private isIframeEnvironment(): boolean {
|
||||
return typeof window !== 'undefined' && window.self !== window.top;
|
||||
}
|
||||
|
||||
/** Snapshot for diagnostics / health. */
|
||||
public getEnvironmentInfo(): {
|
||||
isIframe: boolean;
|
||||
isConnected: boolean;
|
||||
origin: string;
|
||||
userAgent: string;
|
||||
location: string;
|
||||
} {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
isIframe: false,
|
||||
isConnected: false,
|
||||
origin: '',
|
||||
userAgent: '',
|
||||
location: ''
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isIframe: this.isIframeEnvironment(),
|
||||
isConnected: this._isConnected,
|
||||
origin: window.location.origin,
|
||||
userAgent: navigator.userAgent.substring(0, 100),
|
||||
location: window.location.href
|
||||
};
|
||||
}
|
||||
|
||||
/** Log bridge state to the console (dev aid). */
|
||||
public diagnose(): void {
|
||||
// Diagnose output is explicitly gated by debug mode to avoid noisy runtime logs.
|
||||
if (!this.config.debug) return;
|
||||
|
||||
const env = this.getEnvironmentInfo();
|
||||
|
||||
console.group('[EnhancedBridge] Bridge Diagnosis');
|
||||
console.log('Environment:', env);
|
||||
console.log('Connection Status:', this._isConnected);
|
||||
console.log('Pending Requests:', this.pendingRequests.size);
|
||||
console.log('Message Listeners:', Array.from(this.listeners.keys()));
|
||||
console.log('Config:', this.config);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
/** Parent when iframe; `window` when top-level. */
|
||||
private getTargetWindow(): Window {
|
||||
return this.isIframeEnvironment() ? window.parent : window;
|
||||
}
|
||||
|
||||
/** Route incoming postMessage payloads. */
|
||||
private handleMessage(event: MessageEvent) {
|
||||
// Optional origin filter (disabled by default):
|
||||
// if (event.origin !== window.location.origin && window.location.origin !== 'null') {
|
||||
// if (!event.origin.startsWith('http') && !event.origin.startsWith('https')) {
|
||||
// this.log('Received message from different origin, allowing:', event.origin);
|
||||
// } else {
|
||||
// this.log('Skipping message from different origin:', event.origin);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
const message = event.data;
|
||||
// Drop malformed payloads
|
||||
if (!this.isValidMessage(message)) {
|
||||
this.log('Invalid message received:', message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Child handshake
|
||||
if (message.type === 'BRIDGE_READY') {
|
||||
this.log('Received ready message from child');
|
||||
this._isConnected = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 父窗口 -> iframe 的控制类消息:必须优先走业务分发,不能误判为 RPC response。
|
||||
// 否则像 TOGGLE_DESIGN_MODE 这类同样带 requestId/timestamp 的消息会被 isResponseMessage
|
||||
// 吞掉,导致 bridge.on('TOGGLE_DESIGN_MODE') 永远不触发,父页面一直等不到 DESIGN_MODE_CHANGED。
|
||||
if (this.isIframeEnvironment() && this.isParentToIframeCommand(message)) {
|
||||
this.dispatchMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completes a pending sendWithResponse(仅处理真正的响应类型,避免与父到子命令冲突)
|
||||
if (this.isResponseMessage(message)) {
|
||||
this.handleResponseMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fan-out to on(type) handlers
|
||||
this.dispatchMessage(message);
|
||||
}
|
||||
|
||||
/** Resolve or reject pendingRequests entry. */
|
||||
private handleResponseMessage(message: RequestResponseMessage) {
|
||||
const { requestId } = message;
|
||||
|
||||
if (this.pendingRequests.has(requestId)) {
|
||||
const request = this.pendingRequests.get(requestId)!;
|
||||
clearTimeout(request.timeout);
|
||||
this.pendingRequests.delete(requestId);
|
||||
|
||||
if (message.type === 'ACKNOWLEDGEMENT') {
|
||||
request.resolve({ success: true, acknowledged: true });
|
||||
} else {
|
||||
request.resolve(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shape check before dispatch. */
|
||||
private isValidMessage(message: any): message is DesignModeMessage {
|
||||
return (
|
||||
message &&
|
||||
typeof message.type === 'string' &&
|
||||
this.isSupportedMessageType(message.type)
|
||||
);
|
||||
}
|
||||
|
||||
/** Known DesignModeMessage union tags. */
|
||||
private isSupportedMessageType(type: string): boolean {
|
||||
const supportedTypes = [
|
||||
// Iframe to Parent
|
||||
'ELEMENT_SELECTED', 'ELEMENT_DESELECTED', 'CONTENT_UPDATED', 'STYLE_UPDATED',
|
||||
'DESIGN_MODE_CHANGED', 'ELEMENT_STATE_RESPONSE', 'ERROR', 'ACKNOWLEDGEMENT',
|
||||
'HEARTBEAT', 'HEALTH_CHECK_RESPONSE', 'BRIDGE_READY', 'ADD_TO_CHAT',
|
||||
// Parent to Iframe
|
||||
'TOGGLE_DESIGN_MODE', 'UPDATE_STYLE', 'UPDATE_CONTENT', 'BATCH_UPDATE',
|
||||
'HEALTH_CHECK'
|
||||
];
|
||||
return supportedTypes.includes(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为 sendWithResponse / ACK 通道的响应消息。
|
||||
* 注意:不能仅凭 requestId + timestamp 判断,否则父页面发来的 TOGGLE_DESIGN_MODE
|
||||
*(同样带 requestId)会被误判为 response,从而跳过 dispatchMessage。
|
||||
*/
|
||||
private isResponseMessage(message: any): message is RequestResponseMessage {
|
||||
if (!message || typeof message.type !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (message.requestId === undefined || message.timestamp === undefined) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
message.type === 'ACKNOWLEDGEMENT' ||
|
||||
message.type === 'HEALTH_CHECK_RESPONSE'
|
||||
);
|
||||
}
|
||||
|
||||
/** 父窗口发往 iframe 的控制命令(在 iframe 内必须由业务 handler 处理) */
|
||||
private isParentToIframeCommand(message: DesignModeMessage): boolean {
|
||||
const t = (message as { type?: string }).type;
|
||||
return (
|
||||
t === 'TOGGLE_DESIGN_MODE' ||
|
||||
t === 'UPDATE_STYLE' ||
|
||||
t === 'UPDATE_CONTENT' ||
|
||||
t === 'BATCH_UPDATE' ||
|
||||
t === 'HEALTH_CHECK'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire-and-forget postMessage (may no-op if not connected).
|
||||
*/
|
||||
public async send<T extends DesignModeMessage>(message: T): Promise<void> {
|
||||
if (!this._isConnected && this.isIframeEnvironment()) {
|
||||
this.log('Bridge not connected, attempting to reconnect...');
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
if (!this._isConnected) return;
|
||||
}
|
||||
|
||||
if (!this._isConnected && !this.isIframeEnvironment()) {
|
||||
this.log('Running in main window, bridge connection not applicable');
|
||||
return;
|
||||
}
|
||||
|
||||
const enhancedMessage = this.enhanceMessage(message);
|
||||
try {
|
||||
this.log('Sending message:', enhancedMessage);
|
||||
|
||||
this.getTargetWindow().postMessage(enhancedMessage, '*');
|
||||
|
||||
if (this.isIframeEnvironment() && enhancedMessage.requestId) {
|
||||
this.sendAcknowledgement(enhancedMessage.requestId);
|
||||
}
|
||||
} catch (error) {
|
||||
this.log('Error sending message:', error);
|
||||
|
||||
// Dev: surface send failures
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** RPC-style: wait until matching response or timeout. */
|
||||
public async sendWithResponse<T extends DesignModeMessage, R extends DesignModeMessage>(
|
||||
message: T,
|
||||
responseType: R['type']
|
||||
): Promise<R> {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('Bridge is not connected');
|
||||
}
|
||||
|
||||
const enhancedMessage = this.enhanceMessage(message);
|
||||
const requestId = enhancedMessage.requestId!;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Timeout
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingRequests.delete(requestId);
|
||||
reject(new Error(`Request timeout after ${this.config.timeout}ms`));
|
||||
}, this.config.timeout);
|
||||
|
||||
// Pending request map
|
||||
this.pendingRequests.set(requestId, {
|
||||
resolve,
|
||||
reject,
|
||||
timeout,
|
||||
responseType
|
||||
});
|
||||
|
||||
try {
|
||||
this.log('Sending request with response:', enhancedMessage);
|
||||
this.getTargetWindow().postMessage(enhancedMessage, '*');
|
||||
} catch (error) {
|
||||
this.pendingRequests.delete(requestId);
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Ensure requestId + timestamp exist. */
|
||||
private enhanceMessage<T extends DesignModeMessage>(message: T): T & { requestId?: string; timestamp: number } {
|
||||
const requestId = (message as any).requestId || this.generateRequestId();
|
||||
const timestamp = (message as any).timestamp || this.createTimestamp();
|
||||
|
||||
return {
|
||||
...message,
|
||||
requestId,
|
||||
timestamp
|
||||
};
|
||||
}
|
||||
|
||||
/** Unique id for correlating responses. */
|
||||
private generateRequestId(): string {
|
||||
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/** ms since epoch */
|
||||
private createTimestamp(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
/** Best-effort ACK (messageType not wired through yet). */
|
||||
private sendAcknowledgement(requestId: string) {
|
||||
const acknowledgement: AcknowledgementMessage = {
|
||||
type: 'ACKNOWLEDGEMENT',
|
||||
payload: {
|
||||
messageType: 'UNKNOWN', // TODO: propagate originating type
|
||||
},
|
||||
requestId,
|
||||
timestamp: this.createTimestamp()
|
||||
};
|
||||
|
||||
this.getTargetWindow().postMessage(acknowledgement, '*');
|
||||
}
|
||||
|
||||
/** HEARTBEAT post to parent. */
|
||||
private sendHeartbeat() {
|
||||
const heartbeat: HeartbeatMessage = {
|
||||
type: 'HEARTBEAT',
|
||||
payload: {
|
||||
timestamp: this.createTimestamp()
|
||||
},
|
||||
timestamp: this.createTimestamp()
|
||||
};
|
||||
|
||||
this.getTargetWindow().postMessage(heartbeat, '*');
|
||||
this.lastHeartbeat = Date.now();
|
||||
}
|
||||
|
||||
/** Mark disconnected if no heartbeats for 3× interval. */
|
||||
private checkConnection() {
|
||||
const now = Date.now();
|
||||
const timeSinceLastHeartbeat = now - this.lastHeartbeat;
|
||||
|
||||
if (timeSinceLastHeartbeat > this.config.heartbeatInterval * 3) {
|
||||
this.log('Connection appears to be lost');
|
||||
this._isConnected = false;
|
||||
|
||||
// Optimistic reconnect
|
||||
setTimeout(() => {
|
||||
this._isConnected = true;
|
||||
this.log('Connection restored');
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/** Subscribe; returns unsubscribe. */
|
||||
public on<T extends DesignModeMessage>(type: T['type'], handler: (message: T) => void): () => void {
|
||||
if (!this.listeners.has(type)) {
|
||||
this.listeners.set(type, new Set());
|
||||
}
|
||||
|
||||
this.listeners.get(type)!.add(handler);
|
||||
|
||||
// Unsubscribe
|
||||
return () => {
|
||||
this.listeners.get(type)?.delete(handler);
|
||||
};
|
||||
}
|
||||
|
||||
/** Remove one handler for `type`. */
|
||||
public off(type: string, handler: Function): void {
|
||||
this.listeners.get(type)?.delete(handler);
|
||||
}
|
||||
|
||||
/** Invoke all listeners for message.type */
|
||||
private dispatchMessage(message: DesignModeMessage) {
|
||||
const handlers = this.listeners.get(message.type);
|
||||
if (handlers) {
|
||||
handlers.forEach(handler => {
|
||||
try {
|
||||
handler(message);
|
||||
} catch (error) {
|
||||
this.log('Error in message handler:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** `_isConnected` flag */
|
||||
public isConnected(): boolean {
|
||||
return this._isConnected;
|
||||
}
|
||||
|
||||
/** Same as `isConnected` (legacy name). */
|
||||
public isConnectedToTarget(): boolean {
|
||||
return this._isConnected;
|
||||
}
|
||||
|
||||
/** Clear timers, reject pendings, remove listener. */
|
||||
public disconnect(): void {
|
||||
this._isConnected = false;
|
||||
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
}
|
||||
|
||||
if (this.connectionCheckTimer) {
|
||||
clearInterval(this.connectionCheckTimer);
|
||||
}
|
||||
|
||||
// Reject pending RPCs
|
||||
this.pendingRequests.forEach(request => {
|
||||
clearTimeout(request.timeout);
|
||||
request.reject(new Error('Connection disconnected'));
|
||||
});
|
||||
this.pendingRequests.clear();
|
||||
|
||||
// Remove global listener
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('message', this.handleMessage.bind(this));
|
||||
}
|
||||
this.listeners.clear();
|
||||
|
||||
this.log('Bridge disconnected');
|
||||
}
|
||||
|
||||
/** Summarize connectivity; may issue HEALTH_CHECK RPC in iframe. */
|
||||
public async healthCheck(): Promise<{ status: string; details: any }> {
|
||||
try {
|
||||
const env = this.getEnvironmentInfo();
|
||||
|
||||
// Top-level window
|
||||
if (!env.isIframe && this._isConnected) {
|
||||
return {
|
||||
status: 'healthy',
|
||||
details: {
|
||||
...env,
|
||||
message: 'Bridge is healthy and running in main window'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (env.isIframe && this._isConnected) {
|
||||
// Iframe: ping parent
|
||||
try {
|
||||
const response = await this.sendWithResponse<HealthCheckMessage, HealthCheckResponseMessage>(
|
||||
{
|
||||
type: 'HEALTH_CHECK',
|
||||
requestId: '',
|
||||
timestamp: this.createTimestamp()
|
||||
},
|
||||
'HEALTH_CHECK_RESPONSE'
|
||||
);
|
||||
|
||||
return {
|
||||
status: 'healthy',
|
||||
details: {
|
||||
...env,
|
||||
response: response.payload,
|
||||
message: 'Bridge is healthy and responsive'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'degraded',
|
||||
details: {
|
||||
...env,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: 'Bridge is connected but not responding to health checks'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: env.isIframe ? 'connecting' : 'unnecessary',
|
||||
details: {
|
||||
...env,
|
||||
message: env.isIframe ? 'Bridge is initializing' : 'Bridge not needed in main window'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
details: {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
environment: this.getEnvironmentInfo()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Guarded by config.debug */
|
||||
private log(...args: any[]) {
|
||||
// Keep debug logging capability for explicit troubleshooting sessions.
|
||||
if (this.config.debug) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[${timestamp}] [EnhancedBridge]`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/** Alias for disconnect(). */
|
||||
public destroy(): void {
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/** Small helpers for request ids / timestamps. */
|
||||
export const MessageUtils: MessageUtilsInterface = {
|
||||
generateRequestId: (): string => {
|
||||
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
},
|
||||
|
||||
createTimestamp: (): number => {
|
||||
return Date.now();
|
||||
},
|
||||
|
||||
isValidMessage: (message: any): boolean => {
|
||||
return message &&
|
||||
typeof message.type === 'string' &&
|
||||
typeof message.timestamp === 'number';
|
||||
},
|
||||
|
||||
createResponse: function<T extends IframeToParentMessage>(
|
||||
originalMessage: ParentToIframeMessage,
|
||||
payload: T extends { payload: infer P } ? P : never,
|
||||
success: boolean = true,
|
||||
error?: string
|
||||
): T {
|
||||
return {
|
||||
payload,
|
||||
type: (payload as any)?.type,
|
||||
requestId: (originalMessage as any).requestId || '',
|
||||
timestamp: this.createTimestamp()
|
||||
} as unknown as T;
|
||||
}
|
||||
};
|
||||
|
||||
/** Singleton used by the design-mode client bundle. */
|
||||
export const bridge = new EnhancedBridge();
|
||||
|
||||
/** Structural validation for a subset of message types. */
|
||||
export class MessageValidatorImpl implements MessageValidator {
|
||||
validate(message: any): MessageValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!message) {
|
||||
errors.push('Message is null or undefined');
|
||||
return { isValid: false, errors };
|
||||
}
|
||||
|
||||
if (typeof message.type !== 'string') {
|
||||
errors.push('Message type must be a string');
|
||||
}
|
||||
|
||||
if (typeof message.timestamp !== 'number') {
|
||||
errors.push('Message timestamp must be a number');
|
||||
}
|
||||
|
||||
// Per-type payload checks
|
||||
switch (message.type) {
|
||||
case 'ELEMENT_SELECTED':
|
||||
if (!message.payload?.elementInfo) {
|
||||
errors.push('ELEMENT_SELECTED must have elementInfo in payload');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'UPDATE_STYLE':
|
||||
case 'UPDATE_CONTENT':
|
||||
if (!message.payload?.sourceInfo) {
|
||||
errors.push(`${message.type} must have sourceInfo in payload`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'BATCH_UPDATE':
|
||||
if (!Array.isArray(message.payload?.updates)) {
|
||||
errors.push('BATCH_UPDATE must have updates array in payload');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const messageValidator = new MessageValidatorImpl();
|
||||
5
qiming-vite-plugin-design-mode/packages/client-shared/src/elementUtils.d.ts
vendored
Normal file
5
qiming-vite-plugin-design-mode/packages/client-shared/src/elementUtils.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* True when the element has only text child nodes (no element children).
|
||||
*/
|
||||
export declare function isPureStaticText(element: HTMLElement): boolean;
|
||||
//# sourceMappingURL=elementUtils.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"elementUtils.d.ts","sourceRoot":"","sources":["elementUtils.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAa9D"}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* True when the element has only text child nodes (no element children).
|
||||
*/
|
||||
export function isPureStaticText(element) {
|
||||
if (element.children.length > 0) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < element.childNodes.length; i++) {
|
||||
const node = element.childNodes[i];
|
||||
if (node.nodeType !== Node.TEXT_NODE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
//# sourceMappingURL=elementUtils.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"elementUtils.js","sourceRoot":"","sources":["elementUtils.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAoB;IACnD,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnD,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;YACrC,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* True when the element has only text child nodes (no element children).
|
||||
*/
|
||||
export function isPureStaticText(element: HTMLElement): boolean {
|
||||
if (element.children.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < element.childNodes.length; i++) {
|
||||
const node = element.childNodes[i];
|
||||
if (node.nodeType !== Node.TEXT_NODE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Export all types
|
||||
export * from './types';
|
||||
export * from './messages';
|
||||
|
||||
// Export attribute names utilities
|
||||
export * from './attributeNames';
|
||||
|
||||
// Export bridge
|
||||
export * from './bridge';
|
||||
|
||||
// Export source info utilities
|
||||
export * from './sourceInfo';
|
||||
export * from './sourceInfoResolver';
|
||||
|
||||
// Export element utilities
|
||||
export * from './elementUtils';
|
||||
|
||||
// Export services
|
||||
export * from './UpdateService';
|
||||
export * from './HistoryManager';
|
||||
265
qiming-vite-plugin-design-mode/packages/client-shared/src/messages.d.ts
vendored
Normal file
265
qiming-vite-plugin-design-mode/packages/client-shared/src/messages.d.ts
vendored
Normal file
@@ -0,0 +1,265 @@
|
||||
export interface SourceInfo {
|
||||
fileName: string;
|
||||
lineNumber: number;
|
||||
columnNumber: number;
|
||||
elementType?: string;
|
||||
componentName?: string;
|
||||
functionName?: string;
|
||||
elementId?: string;
|
||||
importPath?: string;
|
||||
isUIComponent?: boolean;
|
||||
}
|
||||
export interface ElementInfo {
|
||||
tagName: string;
|
||||
className: string;
|
||||
textContent: string;
|
||||
sourceInfo: SourceInfo;
|
||||
isStaticText: boolean;
|
||||
isStaticClass?: boolean;
|
||||
componentName?: string;
|
||||
componentPath?: string;
|
||||
props?: Record<string, string>;
|
||||
hierarchy?: {
|
||||
tagName: string;
|
||||
componentName?: string;
|
||||
fileName?: string;
|
||||
}[];
|
||||
}
|
||||
export interface MessageValidationResult {
|
||||
isValid: boolean;
|
||||
errors?: string[];
|
||||
}
|
||||
export interface MessageValidator {
|
||||
validate: (message: any) => MessageValidationResult;
|
||||
}
|
||||
export interface BridgeReadyMessage {
|
||||
type: 'BRIDGE_READY';
|
||||
payload: {
|
||||
timestamp: number;
|
||||
environment: 'iframe' | 'main';
|
||||
};
|
||||
timestamp?: number;
|
||||
requestId?: string;
|
||||
}
|
||||
export interface ElementSelectedMessage {
|
||||
type: 'ELEMENT_SELECTED';
|
||||
payload: {
|
||||
elementInfo: ElementInfo;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface ElementDeselectedMessage {
|
||||
type: 'ELEMENT_DESELECTED';
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
payload?: null;
|
||||
}
|
||||
export interface ContentUpdatedMessage {
|
||||
type: 'CONTENT_UPDATED';
|
||||
payload: {
|
||||
sourceInfo: SourceInfo;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
realtime?: boolean;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface ContentUpdatedCallbackMessage {
|
||||
type: 'CONTENT_UPDATED_CALLBACK';
|
||||
payload: {
|
||||
sourceInfo: SourceInfo;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
realtime?: boolean;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface StyleUpdatedMessage {
|
||||
type: 'STYLE_UPDATED';
|
||||
payload: {
|
||||
sourceInfo: SourceInfo;
|
||||
oldClass: string;
|
||||
newClass: string;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface DesignModeChangedMessage {
|
||||
type: 'DESIGN_MODE_CHANGED';
|
||||
enabled: boolean;
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface ToggleDesignModeMessage {
|
||||
type: 'TOGGLE_DESIGN_MODE';
|
||||
enabled: boolean;
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface UpdateStyleMessage {
|
||||
type: 'UPDATE_STYLE';
|
||||
payload: {
|
||||
sourceInfo: SourceInfo;
|
||||
newClass: string;
|
||||
persist?: boolean;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
export interface UpdateContentMessage {
|
||||
type: 'UPDATE_CONTENT';
|
||||
payload: {
|
||||
sourceInfo: SourceInfo;
|
||||
newContent: string;
|
||||
persist?: boolean;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface BatchUpdateItem {
|
||||
type: 'style' | 'content';
|
||||
sourceInfo: SourceInfo;
|
||||
newValue: string;
|
||||
originalValue?: string;
|
||||
}
|
||||
export interface BatchUpdateMessage {
|
||||
type: 'BATCH_UPDATE';
|
||||
payload: {
|
||||
updates: BatchUpdateItem[];
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface AddToChatMessage {
|
||||
type: 'ADD_TO_CHAT';
|
||||
payload: {
|
||||
content: string;
|
||||
context?: {
|
||||
elementInfo?: ElementInfo;
|
||||
sourceInfo?: SourceInfo;
|
||||
};
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface CopyElementMessage {
|
||||
type: 'COPY_ELEMENT';
|
||||
payload: {
|
||||
elementInfo: {
|
||||
tagName: string;
|
||||
className: string;
|
||||
content: string;
|
||||
sourceInfo?: SourceInfo;
|
||||
};
|
||||
textContent: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface ErrorMessage {
|
||||
type: 'ERROR';
|
||||
payload: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: any;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface AcknowledgementMessage {
|
||||
type: 'ACKNOWLEDGEMENT';
|
||||
payload: {
|
||||
messageType: string;
|
||||
};
|
||||
requestId: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface HeartbeatMessage {
|
||||
type: 'HEARTBEAT';
|
||||
payload?: {
|
||||
timestamp: number;
|
||||
};
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface HealthCheckMessage {
|
||||
type: 'HEALTH_CHECK';
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export interface HealthCheckResponseMessage {
|
||||
type: 'HEALTH_CHECK_RESPONSE';
|
||||
payload: {
|
||||
status: 'healthy' | 'unhealthy' | 'degraded' | 'connecting' | 'unnecessary';
|
||||
version?: string;
|
||||
uptime?: number;
|
||||
message?: string;
|
||||
response?: any;
|
||||
error?: string;
|
||||
isIframe?: boolean;
|
||||
isConnected?: boolean;
|
||||
origin?: string;
|
||||
userAgent?: string;
|
||||
location?: string;
|
||||
};
|
||||
requestId: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
export type IframeToParentMessage = ElementSelectedMessage | ElementDeselectedMessage | ContentUpdatedMessage | StyleUpdatedMessage | DesignModeChangedMessage | ErrorMessage | AcknowledgementMessage | HeartbeatMessage | HealthCheckResponseMessage | BridgeReadyMessage | AddToChatMessage | CopyElementMessage | ContentUpdatedCallbackMessage;
|
||||
export type ParentToIframeMessage = ToggleDesignModeMessage | UpdateStyleMessage | UpdateContentMessage | BatchUpdateMessage | HealthCheckMessage | HeartbeatMessage;
|
||||
export type DesignModeMessage = IframeToParentMessage | ParentToIframeMessage;
|
||||
export type RequestMessage = ParentToIframeMessage & {
|
||||
requestId: string;
|
||||
timestamp: number;
|
||||
};
|
||||
export type ResponseMessage = IframeToParentMessage & {
|
||||
requestId: string;
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
timestamp: number;
|
||||
};
|
||||
export type RequestResponseMessage = RequestMessage | ResponseMessage | AcknowledgementMessage;
|
||||
export interface MessageHandler<T extends DesignModeMessage = DesignModeMessage> {
|
||||
type: T['type'];
|
||||
handler: (message: T) => Promise<any> | any;
|
||||
validator?: MessageValidator;
|
||||
}
|
||||
export interface IframeModeConfig {
|
||||
enabled: boolean;
|
||||
hideUI: boolean;
|
||||
enableSelection: boolean;
|
||||
enableDirectEdit: boolean;
|
||||
}
|
||||
export interface BatchUpdateConfig {
|
||||
enabled: boolean;
|
||||
debounceMs: number;
|
||||
}
|
||||
export interface BridgeConfig {
|
||||
timeout: number;
|
||||
retryAttempts: number;
|
||||
retryDelay: number;
|
||||
heartbeatInterval: number;
|
||||
debug: boolean;
|
||||
}
|
||||
export interface MessageUtilsInterface {
|
||||
generateRequestId: () => string;
|
||||
createTimestamp: () => number;
|
||||
isValidMessage: (message: any) => boolean;
|
||||
createResponse: <T extends IframeToParentMessage>(originalMessage: ParentToIframeMessage, payload: T extends {
|
||||
payload: infer P;
|
||||
} ? P : never, success?: boolean, error?: string) => T;
|
||||
}
|
||||
export interface BridgeInterface {
|
||||
send: <T extends DesignModeMessage>(message: T) => Promise<void>;
|
||||
sendWithResponse: <T extends DesignModeMessage, R extends DesignModeMessage>(message: T, responseType: R['type']) => Promise<R>;
|
||||
on: <T extends DesignModeMessage>(type: T['type'], handler: (message: T) => void) => void;
|
||||
off: (type: string, handler: Function) => void;
|
||||
isConnected: () => boolean;
|
||||
disconnect: () => void;
|
||||
}
|
||||
//# sourceMappingURL=messages.d.ts.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
// Message types for iframe ↔ parent window communication
|
||||
export {};
|
||||
//# sourceMappingURL=messages.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"messages.js","sourceRoot":"","sources":["messages.ts"],"names":[],"mappings":"AAAA,yDAAyD"}
|
||||
@@ -0,0 +1,340 @@
|
||||
// Message types for iframe ↔ parent window communication
|
||||
|
||||
export interface SourceInfo {
|
||||
fileName: string;
|
||||
lineNumber: number;
|
||||
columnNumber: number;
|
||||
elementType?: string;
|
||||
componentName?: string;
|
||||
functionName?: string;
|
||||
elementId?: string;
|
||||
importPath?: string;
|
||||
isUIComponent?: boolean; // Whether this is a UI-kit component (e.g. under components/ui)
|
||||
}
|
||||
|
||||
export interface ElementInfo {
|
||||
tagName: string;
|
||||
className: string;
|
||||
textContent: string;
|
||||
sourceInfo: SourceInfo;
|
||||
isStaticText: boolean;
|
||||
isStaticClass?: boolean; // Whether className is a pure static string (editable)
|
||||
componentName?: string;
|
||||
componentPath?: string;
|
||||
props?: Record<string, string>;
|
||||
hierarchy?: {
|
||||
tagName: string;
|
||||
componentName?: string;
|
||||
fileName?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// Message validation types
|
||||
export interface MessageValidationResult {
|
||||
isValid: boolean;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface MessageValidator {
|
||||
validate: (message: any) => MessageValidationResult;
|
||||
}
|
||||
|
||||
// Bridge Ready Message
|
||||
export interface BridgeReadyMessage {
|
||||
type: 'BRIDGE_READY';
|
||||
payload: {
|
||||
timestamp: number;
|
||||
environment: 'iframe' | 'main';
|
||||
};
|
||||
timestamp?: number;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
// iframe → Parent messages
|
||||
export interface ElementSelectedMessage {
|
||||
type: 'ELEMENT_SELECTED';
|
||||
payload: {
|
||||
elementInfo: ElementInfo;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface ElementDeselectedMessage {
|
||||
type: 'ELEMENT_DESELECTED';
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
payload?: null; // Added optional payload to match usage
|
||||
}
|
||||
|
||||
export interface ContentUpdatedMessage {
|
||||
type: 'CONTENT_UPDATED';
|
||||
payload: {
|
||||
sourceInfo: SourceInfo;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
realtime?: boolean; // Whether this is a realtime (throttled) update
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface ContentUpdatedCallbackMessage {
|
||||
type: 'CONTENT_UPDATED_CALLBACK';
|
||||
payload: {
|
||||
sourceInfo: SourceInfo;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
realtime?: boolean; // Whether this is a realtime (throttled) update
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface StyleUpdatedMessage {
|
||||
type: 'STYLE_UPDATED';
|
||||
payload: {
|
||||
sourceInfo: SourceInfo;
|
||||
oldClass: string;
|
||||
newClass: string;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface DesignModeChangedMessage {
|
||||
type: 'DESIGN_MODE_CHANGED';
|
||||
enabled: boolean;
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// Parent → iframe messages
|
||||
export interface ToggleDesignModeMessage {
|
||||
type: 'TOGGLE_DESIGN_MODE';
|
||||
enabled: boolean;
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface UpdateStyleMessage {
|
||||
type: 'UPDATE_STYLE';
|
||||
payload: {
|
||||
sourceInfo: SourceInfo;
|
||||
newClass: string;
|
||||
persist?: boolean;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
enabled?: boolean; // Added to fix TS error where enabled was accessed on this type (though likely a bug in usage, adding optional property avoids strict error if discriminated union fails)
|
||||
}
|
||||
|
||||
export interface UpdateContentMessage {
|
||||
type: 'UPDATE_CONTENT';
|
||||
payload: {
|
||||
sourceInfo: SourceInfo;
|
||||
newContent: string;
|
||||
persist?: boolean;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface BatchUpdateItem {
|
||||
type: 'style' | 'content';
|
||||
sourceInfo: SourceInfo;
|
||||
newValue: string;
|
||||
originalValue?: string;
|
||||
}
|
||||
|
||||
export interface BatchUpdateMessage {
|
||||
type: 'BATCH_UPDATE';
|
||||
payload: {
|
||||
updates: BatchUpdateItem[];
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// Add to Chat Message
|
||||
export interface AddToChatMessage {
|
||||
type: 'ADD_TO_CHAT';
|
||||
payload: {
|
||||
content: string;
|
||||
context?: {
|
||||
elementInfo?: ElementInfo;
|
||||
sourceInfo?: SourceInfo;
|
||||
};
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// Copy Element Message
|
||||
export interface CopyElementMessage {
|
||||
type: 'COPY_ELEMENT';
|
||||
payload: {
|
||||
elementInfo: {
|
||||
tagName: string;
|
||||
className: string;
|
||||
content: string;
|
||||
sourceInfo?: SourceInfo;
|
||||
};
|
||||
textContent: string; // JSON string for clipboard
|
||||
success: boolean; // Whether copy succeeded
|
||||
error?: string; // Error message when copy failed
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// Error message
|
||||
export interface ErrorMessage {
|
||||
type: 'ERROR';
|
||||
payload: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: any;
|
||||
};
|
||||
requestId?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// Acknowledgement message
|
||||
export interface AcknowledgementMessage {
|
||||
type: 'ACKNOWLEDGEMENT';
|
||||
payload: {
|
||||
messageType: string;
|
||||
};
|
||||
requestId: string; // Response MUST have requestId
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// Heartbeat and health-check messages
|
||||
export interface HeartbeatMessage {
|
||||
type: 'HEARTBEAT';
|
||||
payload?: {
|
||||
timestamp: number;
|
||||
};
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface HealthCheckMessage {
|
||||
type: 'HEALTH_CHECK';
|
||||
requestId?: string; // Made optional for sender
|
||||
timestamp?: number; // Made optional for sender
|
||||
}
|
||||
|
||||
export interface HealthCheckResponseMessage {
|
||||
type: 'HEALTH_CHECK_RESPONSE';
|
||||
payload: {
|
||||
status: 'healthy' | 'unhealthy' | 'degraded' | 'connecting' | 'unnecessary';
|
||||
version?: string;
|
||||
uptime?: number;
|
||||
message?: string;
|
||||
response?: any;
|
||||
error?: string;
|
||||
isIframe?: boolean;
|
||||
isConnected?: boolean;
|
||||
origin?: string;
|
||||
userAgent?: string;
|
||||
location?: string;
|
||||
};
|
||||
requestId: string; // Response MUST have requestId
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// Union types
|
||||
export type IframeToParentMessage =
|
||||
| ElementSelectedMessage
|
||||
| ElementDeselectedMessage
|
||||
| ContentUpdatedMessage
|
||||
| StyleUpdatedMessage
|
||||
| DesignModeChangedMessage
|
||||
| ErrorMessage
|
||||
| AcknowledgementMessage
|
||||
| HeartbeatMessage
|
||||
| HealthCheckResponseMessage
|
||||
| BridgeReadyMessage
|
||||
| AddToChatMessage
|
||||
| CopyElementMessage
|
||||
| ContentUpdatedCallbackMessage;
|
||||
|
||||
export type ParentToIframeMessage =
|
||||
| ToggleDesignModeMessage
|
||||
| UpdateStyleMessage
|
||||
| UpdateContentMessage
|
||||
| BatchUpdateMessage
|
||||
| HealthCheckMessage
|
||||
| HeartbeatMessage; // Added HeartbeatMessage here too for bidirectional support
|
||||
|
||||
export type DesignModeMessage = IframeToParentMessage | ParentToIframeMessage;
|
||||
|
||||
// Promise-based request/response types
|
||||
export type RequestMessage = ParentToIframeMessage & {
|
||||
requestId: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type ResponseMessage = IframeToParentMessage & {
|
||||
requestId: string;
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type RequestResponseMessage = RequestMessage | ResponseMessage | AcknowledgementMessage;
|
||||
|
||||
// Message handler type
|
||||
export interface MessageHandler<T extends DesignModeMessage = DesignModeMessage> {
|
||||
type: T['type'];
|
||||
handler: (message: T) => Promise<any> | any;
|
||||
validator?: MessageValidator;
|
||||
}
|
||||
|
||||
// Config-related types
|
||||
export interface IframeModeConfig {
|
||||
enabled: boolean;
|
||||
hideUI: boolean;
|
||||
enableSelection: boolean;
|
||||
enableDirectEdit: boolean;
|
||||
}
|
||||
|
||||
export interface BatchUpdateConfig {
|
||||
enabled: boolean;
|
||||
debounceMs: number;
|
||||
}
|
||||
|
||||
export interface BridgeConfig {
|
||||
timeout: number;
|
||||
retryAttempts: number;
|
||||
retryDelay: number;
|
||||
heartbeatInterval: number;
|
||||
debug: boolean;
|
||||
}
|
||||
|
||||
// Message utility helpers
|
||||
export interface MessageUtilsInterface {
|
||||
generateRequestId: () => string;
|
||||
createTimestamp: () => number;
|
||||
isValidMessage: (message: any) => boolean;
|
||||
createResponse: <T extends IframeToParentMessage>(
|
||||
originalMessage: ParentToIframeMessage,
|
||||
payload: T extends { payload: infer P } ? P : never,
|
||||
success?: boolean,
|
||||
error?: string
|
||||
) => T;
|
||||
}
|
||||
|
||||
// Bridge interface
|
||||
export interface BridgeInterface {
|
||||
send: <T extends DesignModeMessage>(message: T) => Promise<void>;
|
||||
sendWithResponse: <T extends DesignModeMessage, R extends DesignModeMessage>(
|
||||
message: T,
|
||||
responseType: R['type']
|
||||
) => Promise<R>;
|
||||
on: <T extends DesignModeMessage>(type: T['type'], handler: (message: T) => void) => void;
|
||||
off: (type: string, handler: Function) => void;
|
||||
isConnected: () => boolean;
|
||||
disconnect: () => void;
|
||||
}
|
||||
10
qiming-vite-plugin-design-mode/packages/client-shared/src/sourceInfo.d.ts
vendored
Normal file
10
qiming-vite-plugin-design-mode/packages/client-shared/src/sourceInfo.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { SourceInfo } from './messages';
|
||||
/**
|
||||
* Whether the element carries the compact JSON `{prefix}-info` attribute.
|
||||
*/
|
||||
export declare function hasSourceMapping(element: HTMLElement): boolean;
|
||||
/**
|
||||
* Parse `{prefix}-info` JSON into `SourceInfo`, or null when missing/invalid.
|
||||
*/
|
||||
export declare function extractSourceInfo(element: HTMLElement): SourceInfo | null;
|
||||
//# sourceMappingURL=sourceInfo.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sourceInfo.d.ts","sourceRoot":"","sources":["sourceInfo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAGxC;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAE9D;AAqCD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,GAAG,IAAI,CA4BzE"}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { AttributeNames } from './attributeNames';
|
||||
/**
|
||||
* Whether the element carries the compact JSON `{prefix}-info` attribute.
|
||||
*/
|
||||
export function hasSourceMapping(element) {
|
||||
return element.hasAttribute(AttributeNames.info);
|
||||
}
|
||||
function toNumber(value) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value.trim() !== '') {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function normalizeSourceInfo(raw) {
|
||||
const fileName = typeof raw.fileName === 'string' ? raw.fileName : null;
|
||||
const lineNumber = toNumber(raw.lineNumber ?? raw.line);
|
||||
const columnNumber = toNumber(raw.columnNumber ?? raw.column);
|
||||
if (!fileName || lineNumber === null || columnNumber === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
fileName,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
elementType: typeof raw.elementType === 'string' ? raw.elementType : undefined,
|
||||
componentName: typeof raw.componentName === 'string' ? raw.componentName : undefined,
|
||||
functionName: typeof raw.functionName === 'string' ? raw.functionName : undefined,
|
||||
elementId: typeof raw.elementId === 'string' ? raw.elementId : undefined,
|
||||
importPath: typeof raw.importPath === 'string' ? raw.importPath : undefined,
|
||||
isUIComponent: typeof raw.isUIComponent === 'boolean' ? raw.isUIComponent : undefined,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Parse `{prefix}-info` JSON into `SourceInfo`, or null when missing/invalid.
|
||||
*/
|
||||
export function extractSourceInfo(element) {
|
||||
const sourceInfoStr = element.getAttribute(AttributeNames.info);
|
||||
if (sourceInfoStr) {
|
||||
try {
|
||||
const parsed = JSON.parse(sourceInfoStr);
|
||||
const normalized = normalizeSourceInfo(parsed);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.warn(`[sourceInfo] Failed to parse ${AttributeNames.info}:`, e);
|
||||
}
|
||||
}
|
||||
// Legacy per-field attrs.
|
||||
const fileName = element.getAttribute(AttributeNames.file);
|
||||
const line = toNumber(element.getAttribute(AttributeNames.line));
|
||||
const column = toNumber(element.getAttribute(AttributeNames.column));
|
||||
if (fileName && line !== null && column !== null) {
|
||||
return {
|
||||
fileName,
|
||||
lineNumber: line,
|
||||
columnNumber: column,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//# sourceMappingURL=sourceInfo.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sourceInfo.js","sourceRoot":"","sources":["sourceInfo.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { SourceInfo } from './messages';
|
||||
import { AttributeNames } from './attributeNames';
|
||||
|
||||
/**
|
||||
* Whether the element carries the compact JSON `{prefix}-info` attribute.
|
||||
*/
|
||||
export function hasSourceMapping(element: HTMLElement): boolean {
|
||||
return element.hasAttribute(AttributeNames.info);
|
||||
}
|
||||
|
||||
function toNumber(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value.trim() !== '') {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeSourceInfo(raw: Record<string, unknown>): SourceInfo | null {
|
||||
const fileName = typeof raw.fileName === 'string' ? raw.fileName : null;
|
||||
const lineNumber = toNumber(raw.lineNumber ?? raw.line);
|
||||
const columnNumber = toNumber(raw.columnNumber ?? raw.column);
|
||||
|
||||
if (!fileName || lineNumber === null || columnNumber === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
fileName,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
elementType: typeof raw.elementType === 'string' ? raw.elementType : undefined,
|
||||
componentName: typeof raw.componentName === 'string' ? raw.componentName : undefined,
|
||||
functionName: typeof raw.functionName === 'string' ? raw.functionName : undefined,
|
||||
elementId: typeof raw.elementId === 'string' ? raw.elementId : undefined,
|
||||
importPath: typeof raw.importPath === 'string' ? raw.importPath : undefined,
|
||||
isUIComponent: typeof raw.isUIComponent === 'boolean' ? raw.isUIComponent : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `{prefix}-info` JSON into `SourceInfo`, or null when missing/invalid.
|
||||
*/
|
||||
export function extractSourceInfo(element: HTMLElement): SourceInfo | null {
|
||||
const sourceInfoStr = element.getAttribute(AttributeNames.info);
|
||||
if (sourceInfoStr) {
|
||||
try {
|
||||
const parsed = JSON.parse(sourceInfoStr) as Record<string, unknown>;
|
||||
const normalized = normalizeSourceInfo(parsed);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[sourceInfo] Failed to parse ${AttributeNames.info}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy per-field attrs.
|
||||
const fileName = element.getAttribute(AttributeNames.file);
|
||||
const line = toNumber(element.getAttribute(AttributeNames.line));
|
||||
const column = toNumber(element.getAttribute(AttributeNames.column));
|
||||
|
||||
if (fileName && line !== null && column !== null) {
|
||||
return {
|
||||
fileName,
|
||||
lineNumber: line,
|
||||
columnNumber: column,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
13
qiming-vite-plugin-design-mode/packages/client-shared/src/sourceInfoResolver.d.ts
vendored
Normal file
13
qiming-vite-plugin-design-mode/packages/client-shared/src/sourceInfoResolver.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { SourceInfo } from './types';
|
||||
/**
|
||||
* Resolve the **usage-site** `SourceInfo` for an element.
|
||||
*
|
||||
* Pass-through/static-content nodes may be compiled inside a UI-kit file; this walks
|
||||
* parents / `children-source` / cross-file children so selections map to the caller file.
|
||||
*/
|
||||
export declare function resolveSourceInfo(element: HTMLElement): SourceInfo | null;
|
||||
/**
|
||||
* Heuristic: node lives in a component **definition** (both parent/child static, same file).
|
||||
*/
|
||||
export declare function isInComponentDefinition(element: HTMLElement): boolean;
|
||||
//# sourceMappingURL=sourceInfoResolver.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sourceInfoResolver.d.ts","sourceRoot":"","sources":["sourceInfoResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAkDrC;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,GAAG,IAAI,CAuGzE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAmBrE"}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { AttributeNames } from './attributeNames';
|
||||
import { extractSourceInfo } from './sourceInfo';
|
||||
/**
|
||||
* BFS for the first descendant whose `{prefix}-info` points at a different file than `currentFile`.
|
||||
* Used to detect wrapper components (e.g. `<button>` inside a design-system `Button`).
|
||||
*
|
||||
* @param element Root to search under
|
||||
* @param currentFile File path of the current element’s mapping
|
||||
*/
|
||||
function findChildFromDifferentFile(element, currentFile) {
|
||||
const queue = [];
|
||||
for (let i = 0; i < element.children.length; i++) {
|
||||
const child = element.children[i];
|
||||
if (child instanceof HTMLElement) {
|
||||
queue.push(child);
|
||||
}
|
||||
}
|
||||
let depth = 0;
|
||||
const maxDepth = 5;
|
||||
while (queue.length > 0 && depth < maxDepth) {
|
||||
const levelSize = queue.length;
|
||||
for (let i = 0; i < levelSize; i++) {
|
||||
const child = queue.shift();
|
||||
if (!child)
|
||||
continue;
|
||||
const childInfo = extractSourceInfo(child);
|
||||
if (childInfo && childInfo.fileName !== currentFile) {
|
||||
return child;
|
||||
}
|
||||
for (let j = 0; j < child.children.length; j++) {
|
||||
const grandChild = child.children[j];
|
||||
if (grandChild instanceof HTMLElement) {
|
||||
queue.push(grandChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
depth++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Resolve the **usage-site** `SourceInfo` for an element.
|
||||
*
|
||||
* Pass-through/static-content nodes may be compiled inside a UI-kit file; this walks
|
||||
* parents / `children-source` / cross-file children so selections map to the caller file.
|
||||
*/
|
||||
export function resolveSourceInfo(element) {
|
||||
const hasStaticContent = element.hasAttribute(AttributeNames.staticContent);
|
||||
const currentInfo = extractSourceInfo(element);
|
||||
const currentFile = currentInfo?.fileName;
|
||||
if (currentInfo?.isUIComponent) {
|
||||
let parent = element.parentElement;
|
||||
let depth = 0;
|
||||
while (parent && depth < 20) {
|
||||
const parentInfo = extractSourceInfo(parent);
|
||||
if (parentInfo?.fileName && !parentInfo.isUIComponent) {
|
||||
return {
|
||||
fileName: parentInfo.fileName,
|
||||
lineNumber: parentInfo.lineNumber,
|
||||
columnNumber: parentInfo.columnNumber,
|
||||
elementType: element.tagName?.toLowerCase() || 'unknown',
|
||||
componentName: currentInfo?.componentName,
|
||||
functionName: currentInfo?.functionName
|
||||
};
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
const candidates = [
|
||||
'data-xagi-children-source',
|
||||
'data-source-children-source',
|
||||
AttributeNames.childrenSource
|
||||
];
|
||||
let childrenSource = null;
|
||||
for (const attr of candidates) {
|
||||
const val = element.getAttribute(attr);
|
||||
if (val) {
|
||||
childrenSource = val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (childrenSource) {
|
||||
const parts = childrenSource.split(':');
|
||||
if (parts.length >= 3) {
|
||||
const fileName = parts.slice(0, -2).join(':'); // Paths may contain `:`
|
||||
const lineNumber = parseInt(parts[parts.length - 2], 10);
|
||||
const columnNumber = parseInt(parts[parts.length - 1], 10);
|
||||
if (!isNaN(lineNumber) && !isNaN(columnNumber)) {
|
||||
return {
|
||||
fileName,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
elementType: element.tagName?.toLowerCase() || 'unknown',
|
||||
componentName: currentInfo?.componentName,
|
||||
functionName: currentInfo?.functionName
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentFile) {
|
||||
const childWithDifferentSource = findChildFromDifferentFile(element, currentFile);
|
||||
if (childWithDifferentSource) {
|
||||
const childInfo = extractSourceInfo(childWithDifferentSource);
|
||||
if (childInfo && childInfo.fileName !== currentFile) {
|
||||
return {
|
||||
fileName: childInfo.fileName,
|
||||
lineNumber: childInfo.lineNumber,
|
||||
columnNumber: childInfo.columnNumber,
|
||||
elementType: element.tagName?.toLowerCase() || 'unknown',
|
||||
componentName: currentInfo?.componentName,
|
||||
functionName: currentInfo?.functionName
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasStaticContent) {
|
||||
return extractSourceInfo(element);
|
||||
}
|
||||
let currentElement = element;
|
||||
let depth = 0;
|
||||
while (currentElement && depth < 20) {
|
||||
const parent = currentElement.parentElement;
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
const parentInfo = extractSourceInfo(parent);
|
||||
const parentFile = parentInfo?.fileName;
|
||||
const parentHasStaticContent = parent.hasAttribute(AttributeNames.staticContent);
|
||||
if (parentFile && (!parentHasStaticContent || parentFile !== currentFile)) {
|
||||
return parentInfo;
|
||||
}
|
||||
currentElement = parent;
|
||||
depth++;
|
||||
}
|
||||
return extractSourceInfo(element);
|
||||
}
|
||||
/**
|
||||
* Heuristic: node lives in a component **definition** (both parent/child static, same file).
|
||||
*/
|
||||
export function isInComponentDefinition(element) {
|
||||
const hasStaticContent = element.hasAttribute(AttributeNames.staticContent);
|
||||
const sourceInfo = extractSourceInfo(element);
|
||||
const sourceFile = sourceInfo?.fileName;
|
||||
if (!hasStaticContent || !sourceFile) {
|
||||
return false;
|
||||
}
|
||||
const parent = element.parentElement;
|
||||
if (!parent) {
|
||||
return false;
|
||||
}
|
||||
const parentInfo = extractSourceInfo(parent);
|
||||
const parentFile = parentInfo?.fileName;
|
||||
const parentHasStaticContent = parent.hasAttribute(AttributeNames.staticContent);
|
||||
return parentHasStaticContent && parentFile === sourceFile;
|
||||
}
|
||||
//# sourceMappingURL=sourceInfoResolver.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sourceInfoResolver.js","sourceRoot":"","sources":["sourceInfoResolver.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { SourceInfo } from './types';
|
||||
import { AttributeNames } from './attributeNames';
|
||||
import { extractSourceInfo } from './sourceInfo';
|
||||
|
||||
/**
|
||||
* BFS for the first descendant whose `{prefix}-info` points at a different file than `currentFile`.
|
||||
* Used to detect wrapper components (e.g. `<button>` inside a design-system `Button`).
|
||||
*
|
||||
* @param element Root to search under
|
||||
* @param currentFile File path of the current element’s mapping
|
||||
*/
|
||||
function findChildFromDifferentFile(element: HTMLElement, currentFile: string): HTMLElement | null {
|
||||
const queue: HTMLElement[] = [];
|
||||
|
||||
for (let i = 0; i < element.children.length; i++) {
|
||||
const child = element.children[i];
|
||||
if (child instanceof HTMLElement) {
|
||||
queue.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
const maxDepth = 5;
|
||||
|
||||
while (queue.length > 0 && depth < maxDepth) {
|
||||
const levelSize = queue.length;
|
||||
|
||||
for (let i = 0; i < levelSize; i++) {
|
||||
const child = queue.shift();
|
||||
if (!child) continue;
|
||||
|
||||
const childInfo = extractSourceInfo(child);
|
||||
if (childInfo && childInfo.fileName !== currentFile) {
|
||||
return child;
|
||||
}
|
||||
|
||||
for (let j = 0; j < child.children.length; j++) {
|
||||
const grandChild = child.children[j];
|
||||
if (grandChild instanceof HTMLElement) {
|
||||
queue.push(grandChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
depth++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the **usage-site** `SourceInfo` for an element.
|
||||
*
|
||||
* Pass-through/static-content nodes may be compiled inside a UI-kit file; this walks
|
||||
* parents / `children-source` / cross-file children so selections map to the caller file.
|
||||
*/
|
||||
export function resolveSourceInfo(element: HTMLElement): SourceInfo | null {
|
||||
const hasStaticContent = element.hasAttribute(AttributeNames.staticContent);
|
||||
const currentInfo = extractSourceInfo(element);
|
||||
const currentFile = currentInfo?.fileName;
|
||||
|
||||
if (currentInfo?.isUIComponent) {
|
||||
let parent = element.parentElement;
|
||||
let depth = 0;
|
||||
while (parent && depth < 20) {
|
||||
const parentInfo = extractSourceInfo(parent);
|
||||
if (parentInfo?.fileName && !parentInfo.isUIComponent) {
|
||||
return {
|
||||
fileName: parentInfo.fileName,
|
||||
lineNumber: parentInfo.lineNumber,
|
||||
columnNumber: parentInfo.columnNumber,
|
||||
elementType: element.tagName?.toLowerCase() || 'unknown',
|
||||
componentName: currentInfo?.componentName,
|
||||
functionName: currentInfo?.functionName
|
||||
};
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
'data-xagi-children-source',
|
||||
'data-source-children-source',
|
||||
AttributeNames.childrenSource
|
||||
];
|
||||
|
||||
let childrenSource: string | null = null;
|
||||
for (const attr of candidates) {
|
||||
const val = element.getAttribute(attr);
|
||||
if (val) {
|
||||
childrenSource = val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (childrenSource) {
|
||||
const parts = childrenSource.split(':');
|
||||
if (parts.length >= 3) {
|
||||
const fileName = parts.slice(0, -2).join(':'); // Paths may contain `:`
|
||||
const lineNumber = parseInt(parts[parts.length - 2], 10);
|
||||
const columnNumber = parseInt(parts[parts.length - 1], 10);
|
||||
|
||||
if (!isNaN(lineNumber) && !isNaN(columnNumber)) {
|
||||
return {
|
||||
fileName,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
elementType: element.tagName?.toLowerCase() || 'unknown',
|
||||
componentName: currentInfo?.componentName,
|
||||
functionName: currentInfo?.functionName
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentFile) {
|
||||
const childWithDifferentSource = findChildFromDifferentFile(element, currentFile);
|
||||
if (childWithDifferentSource) {
|
||||
const childInfo = extractSourceInfo(childWithDifferentSource);
|
||||
if (childInfo && childInfo.fileName !== currentFile) {
|
||||
return {
|
||||
fileName: childInfo.fileName,
|
||||
lineNumber: childInfo.lineNumber,
|
||||
columnNumber: childInfo.columnNumber,
|
||||
elementType: element.tagName?.toLowerCase() || 'unknown',
|
||||
componentName: currentInfo?.componentName,
|
||||
functionName: currentInfo?.functionName
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasStaticContent) {
|
||||
return extractSourceInfo(element);
|
||||
}
|
||||
|
||||
// Only walk up for elements inside component definitions (both parent and child
|
||||
// have static-content in the same file). Regular nested HTML should use its own sourceInfo.
|
||||
if (!isInComponentDefinition(element)) {
|
||||
return extractSourceInfo(element);
|
||||
}
|
||||
|
||||
let currentElement: HTMLElement | null = element;
|
||||
let depth = 0;
|
||||
|
||||
while (currentElement && depth < 20) {
|
||||
const parent: HTMLElement | null = currentElement.parentElement;
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
|
||||
const parentInfo = extractSourceInfo(parent);
|
||||
const parentFile = parentInfo?.fileName;
|
||||
const parentHasStaticContent = parent.hasAttribute(AttributeNames.staticContent);
|
||||
|
||||
if (parentFile && (!parentHasStaticContent || parentFile !== currentFile)) {
|
||||
return parentInfo;
|
||||
}
|
||||
|
||||
currentElement = parent;
|
||||
depth++;
|
||||
}
|
||||
|
||||
return extractSourceInfo(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic: node lives in a component **definition** (both parent/child static, same file).
|
||||
*/
|
||||
export function isInComponentDefinition(element: HTMLElement): boolean {
|
||||
const hasStaticContent = element.hasAttribute(AttributeNames.staticContent);
|
||||
const sourceInfo = extractSourceInfo(element);
|
||||
const sourceFile = sourceInfo?.fileName;
|
||||
|
||||
if (!hasStaticContent || !sourceFile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parent = element.parentElement;
|
||||
if (!parent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentInfo = extractSourceInfo(parent);
|
||||
const parentFile = parentInfo?.fileName;
|
||||
const parentHasStaticContent = parent.hasAttribute(AttributeNames.staticContent);
|
||||
|
||||
return parentHasStaticContent && parentFile === sourceFile;
|
||||
}
|
||||
50
qiming-vite-plugin-design-mode/packages/client-shared/src/types.d.ts
vendored
Normal file
50
qiming-vite-plugin-design-mode/packages/client-shared/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { SourceInfo, ElementInfo } from './messages';
|
||||
export type { SourceInfo, ElementInfo };
|
||||
/** High-level update kind tracked by `UpdateManager`. */
|
||||
export type UpdateOperation = 'style_update' | 'content_update' | 'attribute_update' | 'class_update' | 'batch_update';
|
||||
/** One in-flight or historical mutation. */
|
||||
export interface UpdateState {
|
||||
id: string;
|
||||
operation: UpdateOperation;
|
||||
sourceInfo: SourceInfo;
|
||||
element: HTMLElement;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed' | 'reverted';
|
||||
timestamp: number;
|
||||
error?: string;
|
||||
retryCount: number;
|
||||
persist?: boolean;
|
||||
}
|
||||
/** Result of persisting / previewing an update. */
|
||||
export interface UpdateResult {
|
||||
success: boolean;
|
||||
element: HTMLElement;
|
||||
updateId: string;
|
||||
error?: string;
|
||||
serverResponse?: any;
|
||||
}
|
||||
/** One row in a batch POST body for UpdateManager. */
|
||||
export interface UpdateManagerBatchItem {
|
||||
element: HTMLElement;
|
||||
type: 'style' | 'content' | 'attribute';
|
||||
sourceInfo: SourceInfo;
|
||||
newValue: string;
|
||||
originalValue?: string;
|
||||
selector?: string;
|
||||
}
|
||||
/** `UpdateManager` runtime options. */
|
||||
export interface UpdateManagerConfig {
|
||||
enableDirectEdit: boolean;
|
||||
enableBatching: boolean;
|
||||
batchDebounceMs: number;
|
||||
maxRetries: number;
|
||||
autoSave: boolean;
|
||||
saveDelay: number;
|
||||
validation: {
|
||||
validateSource: boolean;
|
||||
validateValue: boolean;
|
||||
maxLength: number;
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG1D,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC;AAExC,yDAAyD;AACzD,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,gBAAgB,GAChB,kBAAkB,GAClB,cAAc,GACd,cAAc,CAAC;AAEnB,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,eAAe,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,mDAAmD;AACnD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,GAAG,CAAC;CACtB;AAED,sDAAsD;AACtD,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,uCAAuC;AACvC,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE;QACV,cAAc,EAAE,OAAO,CAAC;QACxB,aAAa,EAAE,OAAO,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH"}
|
||||
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":""}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Import shared types from messages
|
||||
import type { SourceInfo, ElementInfo } from './messages';
|
||||
|
||||
// Re-export for convenience
|
||||
export type { SourceInfo, ElementInfo };
|
||||
|
||||
/** High-level update kind tracked by `UpdateManager`. */
|
||||
export type UpdateOperation =
|
||||
| 'style_update'
|
||||
| 'content_update'
|
||||
| 'attribute_update'
|
||||
| 'class_update'
|
||||
| 'batch_update';
|
||||
|
||||
/** One in-flight or historical mutation. */
|
||||
export interface UpdateState {
|
||||
id: string;
|
||||
operation: UpdateOperation;
|
||||
sourceInfo: SourceInfo;
|
||||
element: HTMLElement;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed' | 'reverted';
|
||||
timestamp: number;
|
||||
error?: string;
|
||||
retryCount: number;
|
||||
persist?: boolean;
|
||||
}
|
||||
|
||||
/** Result of persisting / previewing an update. */
|
||||
export interface UpdateResult {
|
||||
success: boolean;
|
||||
element: HTMLElement;
|
||||
updateId: string;
|
||||
error?: string;
|
||||
serverResponse?: any;
|
||||
}
|
||||
|
||||
/** One row in a batch POST body for UpdateManager. */
|
||||
export interface UpdateManagerBatchItem {
|
||||
element: HTMLElement;
|
||||
type: 'style' | 'content' | 'attribute';
|
||||
sourceInfo: SourceInfo;
|
||||
newValue: string;
|
||||
originalValue?: string;
|
||||
selector?: string;
|
||||
}
|
||||
|
||||
/** `UpdateManager` runtime options. */
|
||||
export interface UpdateManagerConfig {
|
||||
enableDirectEdit: boolean;
|
||||
enableBatching: boolean;
|
||||
batchDebounceMs: number;
|
||||
maxRetries: number;
|
||||
autoSave: boolean;
|
||||
saveDelay: number;
|
||||
validation: {
|
||||
validateSource: boolean;
|
||||
validateValue: boolean;
|
||||
maxLength: number;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user