@nimbalyst/extension-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +151 -0
  3. package/dist/MaterialSymbol.d.ts +2 -0
  4. package/dist/MaterialSymbol.d.ts.map +1 -0
  5. package/dist/MaterialSymbol.js +1 -0
  6. package/dist/clipboard.d.ts +14 -0
  7. package/dist/clipboard.d.ts.map +1 -0
  8. package/dist/clipboard.js +27 -0
  9. package/dist/createReadOnlyHost.d.ts +45 -0
  10. package/dist/createReadOnlyHost.d.ts.map +1 -0
  11. package/dist/createReadOnlyHost.js +81 -0
  12. package/dist/documentPath.d.ts +6 -0
  13. package/dist/documentPath.d.ts.map +1 -0
  14. package/dist/documentPath.js +4 -0
  15. package/dist/externals.d.ts +23 -0
  16. package/dist/externals.d.ts.map +1 -0
  17. package/dist/externals.js +42 -0
  18. package/dist/externalsPlugin.d.ts +39 -0
  19. package/dist/externalsPlugin.d.ts.map +1 -0
  20. package/dist/externalsPlugin.js +251 -0
  21. package/dist/index.d.ts +33 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +39 -0
  24. package/dist/tailwind.d.ts +112 -0
  25. package/dist/tailwind.d.ts.map +1 -0
  26. package/dist/tailwind.js +129 -0
  27. package/dist/testing.d.ts +122 -0
  28. package/dist/testing.d.ts.map +1 -0
  29. package/dist/testing.js +199 -0
  30. package/dist/types/editor.d.ts +385 -0
  31. package/dist/types/editor.d.ts.map +1 -0
  32. package/dist/types/editor.js +15 -0
  33. package/dist/types/editors.d.ts +56 -0
  34. package/dist/types/editors.d.ts.map +1 -0
  35. package/dist/types/editors.js +19 -0
  36. package/dist/types/extension.d.ts +744 -0
  37. package/dist/types/extension.d.ts.map +1 -0
  38. package/dist/types/extension.js +4 -0
  39. package/dist/types/index.d.ts +9 -0
  40. package/dist/types/index.d.ts.map +1 -0
  41. package/dist/types/index.js +8 -0
  42. package/dist/types/panel.d.ts +575 -0
  43. package/dist/types/panel.d.ts.map +1 -0
  44. package/dist/types/panel.js +14 -0
  45. package/dist/types/theme.d.ts +148 -0
  46. package/dist/types/theme.d.ts.map +1 -0
  47. package/dist/types/theme.js +7 -0
  48. package/dist/useEditorLifecycle.d.ts +166 -0
  49. package/dist/useEditorLifecycle.d.ts.map +1 -0
  50. package/dist/useEditorLifecycle.js +327 -0
  51. package/dist/validate.d.ts +31 -0
  52. package/dist/validate.d.ts.map +1 -0
  53. package/dist/validate.js +128 -0
  54. package/dist/vite.d.ts +92 -0
  55. package/dist/vite.d.ts.map +1 -0
  56. package/dist/vite.js +179 -0
  57. package/package.json +91 -0
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Core types for Nimbalyst theme system.
3
+ *
4
+ * Themes are separate from extensions - they provide styling only, no code.
5
+ * This module defines the schema for theme.json manifests and theme metadata.
6
+ */
7
+ /**
8
+ * Available theme color keys that can be overridden.
9
+ * These map to CSS variables (--nim-*) used throughout the UI.
10
+ */
11
+ export type ThemeColorKey = 'bg' | 'bg-secondary' | 'bg-tertiary' | 'bg-hover' | 'bg-selected' | 'bg-active' | 'text' | 'text-muted' | 'text-faint' | 'text-disabled' | 'border' | 'border-focus' | 'primary' | 'primary-hover' | 'link' | 'link-hover' | 'success' | 'warning' | 'error' | 'info';
12
+ /**
13
+ * Theme color values.
14
+ * All colors are optional - missing colors will fall back to base theme.
15
+ */
16
+ export type ThemeColors = Partial<Record<ThemeColorKey, string>>;
17
+ /**
18
+ * Theme manifest schema (theme.json).
19
+ * This is the schema for standalone theme packages.
20
+ */
21
+ export interface ThemeManifest {
22
+ /** Unique theme identifier (e.g., 'solarized-dark') */
23
+ id: string;
24
+ /** Display name shown in UI */
25
+ name: string;
26
+ /** Semantic version */
27
+ version: string;
28
+ /** Theme author name or organization */
29
+ author?: string;
30
+ /** Brief description */
31
+ description?: string;
32
+ /** Whether this is a dark theme (determines base theme for fallbacks) */
33
+ isDark: boolean;
34
+ /**
35
+ * Theme color overrides.
36
+ * Only include colors you want to override from the base theme.
37
+ * All color values must be valid hex codes or CSS color names.
38
+ */
39
+ colors: ThemeColors;
40
+ /** Path to preview image (relative to theme.json) */
41
+ preview?: string;
42
+ /** Tags for discovery and filtering */
43
+ tags?: string[];
44
+ /** License identifier (SPDX) */
45
+ license?: string;
46
+ /** Homepage or repository URL */
47
+ homepage?: string;
48
+ }
49
+ /**
50
+ * Full theme object with metadata and resolved colors.
51
+ * This is what's used at runtime after loading a theme.
52
+ */
53
+ export interface Theme {
54
+ /** Theme ID (unique across all themes) */
55
+ id: string;
56
+ /** Display name */
57
+ name: string;
58
+ /** Semantic version */
59
+ version: string;
60
+ /** Author */
61
+ author?: string;
62
+ /** Description */
63
+ description?: string;
64
+ /** Is dark theme */
65
+ isDark: boolean;
66
+ /** Resolved color values (merged with base theme) */
67
+ colors: ThemeColors;
68
+ /** Tags */
69
+ tags?: string[];
70
+ /** Preview image path (absolute) */
71
+ previewPath?: string;
72
+ /** Source of the theme */
73
+ source: ThemeSource;
74
+ }
75
+ /**
76
+ * Where the theme came from.
77
+ */
78
+ export type ThemeSource = {
79
+ type: 'builtin';
80
+ } | {
81
+ type: 'user';
82
+ installPath: string;
83
+ } | {
84
+ type: 'extension';
85
+ extensionId: string;
86
+ };
87
+ /**
88
+ * Marketplace theme metadata.
89
+ * Used for browsing and discovering themes before installation.
90
+ */
91
+ export interface MarketplaceTheme {
92
+ /** Theme ID */
93
+ id: string;
94
+ /** Display name */
95
+ name: string;
96
+ /** Author */
97
+ author: string;
98
+ /** Version */
99
+ version: string;
100
+ /** Description */
101
+ description: string;
102
+ /** Download count */
103
+ downloads: number;
104
+ /** Average rating (0-5) */
105
+ rating: number;
106
+ /** Tags */
107
+ tags: string[];
108
+ /** URL to preview image */
109
+ preview: string;
110
+ /** URL to download .nimtheme file */
111
+ downloadUrl: string;
112
+ /** Last update timestamp */
113
+ lastUpdated: string;
114
+ /** License */
115
+ license?: string;
116
+ /** Homepage URL */
117
+ homepage?: string;
118
+ }
119
+ /**
120
+ * Theme validation result.
121
+ */
122
+ export interface ThemeValidationResult {
123
+ /** Whether the theme is valid */
124
+ valid: boolean;
125
+ /** Validation errors (if any) */
126
+ errors: string[];
127
+ /** Validation warnings (non-fatal) */
128
+ warnings: string[];
129
+ }
130
+ /**
131
+ * Theme installation options.
132
+ */
133
+ export interface ThemeInstallOptions {
134
+ /** Path to .nimtheme file or directory */
135
+ source: string;
136
+ /** Whether to overwrite existing theme with same ID */
137
+ overwrite?: boolean;
138
+ }
139
+ /**
140
+ * Theme uninstall options.
141
+ */
142
+ export interface ThemeUninstallOptions {
143
+ /** Theme ID to uninstall */
144
+ themeId: string;
145
+ /** Whether to keep theme files (just disable) */
146
+ keepFiles?: boolean;
147
+ }
148
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/types/theme.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Core types for Nimbalyst theme system.
3
+ *
4
+ * Themes are separate from extensions - they provide styling only, no code.
5
+ * This module defines the schema for theme.json manifests and theme metadata.
6
+ */
7
+ export {};
@@ -0,0 +1,166 @@
1
+ /**
2
+ * useEditorLifecycle Hook
3
+ *
4
+ * The single hook for custom editors to integrate with the EditorHost API.
5
+ * Handles all lifecycle concerns: loading, saving, echo detection, dirty state,
6
+ * file change handling, diff mode, theme changes.
7
+ *
8
+ * Content state NEVER lives in this hook. The hook interacts with the editor's
9
+ * state through pull/push callbacks:
10
+ * - applyContent: push new content INTO the editor (on load, external file change)
11
+ * - getCurrentContent: pull current content FROM the editor (on save)
12
+ *
13
+ * This design works for all editor types:
14
+ * - Library-managed (Excalidraw, Three.js): callbacks talk to the library's imperative API
15
+ * - Store-managed (Mindmap, DatamodelLM): callbacks talk to a Zustand store
16
+ * - Read-only (PDF, SQLite): only applyContent, no getCurrentContent
17
+ *
18
+ * @example Library-managed editor (Excalidraw)
19
+ * ```tsx
20
+ * function ExcalidrawEditor({ host }: EditorHostProps) {
21
+ * const apiRef = useRef<ExcalidrawImperativeAPI>(null);
22
+ *
23
+ * const { markDirty, isLoading, theme } = useEditorLifecycle(host, {
24
+ * applyContent: (elements) => apiRef.current?.updateScene({ elements }),
25
+ * getCurrentContent: () => apiRef.current?.getSceneElements() ?? [],
26
+ * parse: (raw) => JSON.parse(raw).elements,
27
+ * serialize: (elements) => JSON.stringify({ elements }),
28
+ * });
29
+ *
30
+ * return <Excalidraw ref={apiRef} onChange={(el) => { if (changed(el)) markDirty(); }} />;
31
+ * }
32
+ * ```
33
+ *
34
+ * @example Store-managed editor (Mindmap)
35
+ * ```tsx
36
+ * function MindmapEditor({ host }: EditorHostProps) {
37
+ * const storeRef = useRef(createMindmapStore());
38
+ * const store = storeRef.current;
39
+ *
40
+ * const { markDirty } = useEditorLifecycle(host, {
41
+ * applyContent: (doc) => store.getState().loadDocument(doc),
42
+ * getCurrentContent: () => store.getState().document,
43
+ * parse: parseDocument,
44
+ * serialize: serializeDocument,
45
+ * });
46
+ *
47
+ * return <MindmapCanvas store={store} markDirty={markDirty} />;
48
+ * }
49
+ * ```
50
+ *
51
+ * @example Read-only viewer (PDF)
52
+ * ```tsx
53
+ * function PDFViewer({ host }: EditorHostProps) {
54
+ * const dataRef = useRef<ArrayBuffer | null>(null);
55
+ * const [, forceRender] = useReducer((x) => x + 1, 0);
56
+ *
57
+ * const { isLoading } = useEditorLifecycle(host, {
58
+ * applyContent: (data) => { dataRef.current = data; forceRender(); },
59
+ * binary: true,
60
+ * });
61
+ *
62
+ * return isLoading ? <Loading /> : <PDFRenderer data={dataRef.current} />;
63
+ * }
64
+ * ```
65
+ */
66
+ import type { EditorHost, DiffConfig } from './types/editor.js';
67
+ export interface UseEditorLifecycleOptions<T> {
68
+ /**
69
+ * Push new content into the editor.
70
+ * Called on initial load and on external file changes.
71
+ * This should update the editor's internal state WITHOUT triggering
72
+ * a React re-render of the entire editor tree.
73
+ */
74
+ applyContent: (content: T) => void;
75
+ /**
76
+ * Pull current content from the editor for saving.
77
+ * Omit for read-only editors (PDF viewer, SQLite browser, etc.)
78
+ */
79
+ getCurrentContent?: () => T;
80
+ /**
81
+ * Parse raw file content (string) into the editor's internal format.
82
+ * If omitted, raw string is passed through as-is.
83
+ */
84
+ parse?: (raw: string) => T;
85
+ /**
86
+ * Serialize editor's internal format back to string for saving.
87
+ * If omitted, String(content) is used.
88
+ */
89
+ serialize?: (content: T) => string;
90
+ /**
91
+ * Use loadBinaryContent() instead of loadContent().
92
+ * For binary file formats (PDF, SQLite, images, etc.)
93
+ * When true, parse/serialize are ignored -- applyContent receives the raw
94
+ * ArrayBuffer and getCurrentContent should return an ArrayBuffer.
95
+ */
96
+ binary?: boolean;
97
+ /**
98
+ * Called after initial content is loaded and applied.
99
+ * Use for post-load setup (e.g., fitting viewport to content).
100
+ */
101
+ onLoaded?: () => void;
102
+ /**
103
+ * Called when an external file change is detected and applied.
104
+ * NOT called for echoes from our own saves.
105
+ * Use for editor-specific reactions to external changes (e.g., re-layout).
106
+ */
107
+ onExternalChange?: (content: T) => void;
108
+ /**
109
+ * Custom save handler. When provided, replaces the default
110
+ * getCurrentContent -> serialize -> saveContent flow.
111
+ * Use for editors that require async content extraction (e.g., RevoGrid).
112
+ * The hook still handles dirty state clearing after onSave resolves.
113
+ */
114
+ onSave?: () => Promise<void>;
115
+ /**
116
+ * Custom diff request handler. When provided, replaces the default
117
+ * diff parsing and state management. The editor manages its own diff state.
118
+ * Use for editors with specialized diff rendering (e.g., cell-level CSV diff).
119
+ */
120
+ onDiffRequested?: (config: DiffConfig) => void;
121
+ /**
122
+ * Custom diff cleared handler. When provided, replaces the default
123
+ * diff cleared behavior (reload from disk). Paired with onDiffRequested.
124
+ */
125
+ onDiffCleared?: () => Promise<void>;
126
+ }
127
+ export interface DiffState<T> {
128
+ /** Content before the AI edit */
129
+ original: T;
130
+ /** Content after the AI edit (what's on disk now) */
131
+ modified: T;
132
+ /** History tag ID for this diff */
133
+ tagId: string;
134
+ /** AI session ID that made the edit */
135
+ sessionId: string;
136
+ /** Accept the AI's changes */
137
+ accept: () => void;
138
+ /** Reject the AI's changes (revert to original) */
139
+ reject: () => void;
140
+ }
141
+ export interface UseEditorLifecycleResult<T> {
142
+ /**
143
+ * Call when the user makes an edit. Marks the document dirty.
144
+ * No-op for read-only editors (when getCurrentContent is not provided).
145
+ */
146
+ markDirty: () => void;
147
+ /** Whether initial content is still loading. */
148
+ isLoading: boolean;
149
+ /** Error from initial load, or null. */
150
+ error: Error | null;
151
+ /** Current theme (reactive -- updates when user changes theme). */
152
+ theme: string;
153
+ /** Whether the editor has unsaved changes. */
154
+ isDirty: boolean;
155
+ /**
156
+ * Diff state when AI proposes changes. null when not in diff mode.
157
+ * Editors should render a diff view when this is non-null.
158
+ */
159
+ diffState: DiffState<T> | null;
160
+ /** Toggle source mode (switches to Monaco). Only present if host supports it. */
161
+ toggleSourceMode: (() => void) | undefined;
162
+ /** Whether source mode is currently active. */
163
+ isSourceMode: boolean;
164
+ }
165
+ export declare function useEditorLifecycle<T = string>(host: EditorHost, options: UseEditorLifecycleOptions<T>): UseEditorLifecycleResult<T>;
166
+ //# sourceMappingURL=useEditorLifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEditorLifecycle.d.ts","sourceRoot":"","sources":["../src/useEditorLifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAMhE,MAAM,WAAW,yBAAyB,CAAC,CAAC;IAC1C;;;;;OAKG;IACH,YAAY,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;IAEnC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAC;IAE5B;;;OAGG;IACH,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,MAAM,CAAC;IAEnC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;IAExC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAE/C;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,iCAAiC;IACjC,QAAQ,EAAE,CAAC,CAAC;IAEZ,qDAAqD;IACrD,QAAQ,EAAE,CAAC,CAAC;IAEZ,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IAEd,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAElB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,IAAI,CAAC;IAEnB,mDAAmD;IACnD,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC;;;OAGG;IACH,SAAS,EAAE,MAAM,IAAI,CAAC;IAEtB,gDAAgD;IAChD,SAAS,EAAE,OAAO,CAAC;IAEnB,wCAAwC;IACxC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAEpB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IAEd,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAE/B,iFAAiF;IACjF,gBAAgB,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAE3C,+CAA+C;IAC/C,YAAY,EAAE,OAAO,CAAC;CACvB;AAMD,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,MAAM,EAC3C,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,yBAAyB,CAAC,CAAC,CAAC,GACpC,wBAAwB,CAAC,CAAC,CAAC,CA+R7B"}
@@ -0,0 +1,327 @@
1
+ /**
2
+ * useEditorLifecycle Hook
3
+ *
4
+ * The single hook for custom editors to integrate with the EditorHost API.
5
+ * Handles all lifecycle concerns: loading, saving, echo detection, dirty state,
6
+ * file change handling, diff mode, theme changes.
7
+ *
8
+ * Content state NEVER lives in this hook. The hook interacts with the editor's
9
+ * state through pull/push callbacks:
10
+ * - applyContent: push new content INTO the editor (on load, external file change)
11
+ * - getCurrentContent: pull current content FROM the editor (on save)
12
+ *
13
+ * This design works for all editor types:
14
+ * - Library-managed (Excalidraw, Three.js): callbacks talk to the library's imperative API
15
+ * - Store-managed (Mindmap, DatamodelLM): callbacks talk to a Zustand store
16
+ * - Read-only (PDF, SQLite): only applyContent, no getCurrentContent
17
+ *
18
+ * @example Library-managed editor (Excalidraw)
19
+ * ```tsx
20
+ * function ExcalidrawEditor({ host }: EditorHostProps) {
21
+ * const apiRef = useRef<ExcalidrawImperativeAPI>(null);
22
+ *
23
+ * const { markDirty, isLoading, theme } = useEditorLifecycle(host, {
24
+ * applyContent: (elements) => apiRef.current?.updateScene({ elements }),
25
+ * getCurrentContent: () => apiRef.current?.getSceneElements() ?? [],
26
+ * parse: (raw) => JSON.parse(raw).elements,
27
+ * serialize: (elements) => JSON.stringify({ elements }),
28
+ * });
29
+ *
30
+ * return <Excalidraw ref={apiRef} onChange={(el) => { if (changed(el)) markDirty(); }} />;
31
+ * }
32
+ * ```
33
+ *
34
+ * @example Store-managed editor (Mindmap)
35
+ * ```tsx
36
+ * function MindmapEditor({ host }: EditorHostProps) {
37
+ * const storeRef = useRef(createMindmapStore());
38
+ * const store = storeRef.current;
39
+ *
40
+ * const { markDirty } = useEditorLifecycle(host, {
41
+ * applyContent: (doc) => store.getState().loadDocument(doc),
42
+ * getCurrentContent: () => store.getState().document,
43
+ * parse: parseDocument,
44
+ * serialize: serializeDocument,
45
+ * });
46
+ *
47
+ * return <MindmapCanvas store={store} markDirty={markDirty} />;
48
+ * }
49
+ * ```
50
+ *
51
+ * @example Read-only viewer (PDF)
52
+ * ```tsx
53
+ * function PDFViewer({ host }: EditorHostProps) {
54
+ * const dataRef = useRef<ArrayBuffer | null>(null);
55
+ * const [, forceRender] = useReducer((x) => x + 1, 0);
56
+ *
57
+ * const { isLoading } = useEditorLifecycle(host, {
58
+ * applyContent: (data) => { dataRef.current = data; forceRender(); },
59
+ * binary: true,
60
+ * });
61
+ *
62
+ * return isLoading ? <Loading /> : <PDFRenderer data={dataRef.current} />;
63
+ * }
64
+ * ```
65
+ */
66
+ import { useState, useEffect, useRef, useCallback } from 'react';
67
+ // ============================================================================
68
+ // Hook Implementation
69
+ // ============================================================================
70
+ export function useEditorLifecycle(host, options) {
71
+ // -- React state: only for things that SHOULD cause re-renders --
72
+ const [isLoading, setIsLoading] = useState(true);
73
+ const [error, setError] = useState(null);
74
+ const [isDirty, setIsDirty] = useState(false);
75
+ const [theme, setTheme] = useState(host.theme);
76
+ const [diffState, setDiffState] = useState(null);
77
+ const [isSourceMode, setIsSourceMode] = useState(host.isSourceModeActive?.() ?? false);
78
+ // -- Refs: for everything that should NOT cause re-renders --
79
+ // Options ref -- updated every render so callbacks are never stale
80
+ const optionsRef = useRef(options);
81
+ optionsRef.current = options;
82
+ // Echo detection: stores the last serialized string we saved
83
+ const lastSavedContentRef = useRef('');
84
+ // Prevent double-loading
85
+ const loadedRef = useRef(false);
86
+ // Track dirty state in a ref too (for use in callbacks without stale closure)
87
+ const isDirtyRef = useRef(false);
88
+ // ---- Parse / Serialize helpers ----
89
+ const parseContent = useCallback((raw) => {
90
+ const { parse } = optionsRef.current;
91
+ if (parse)
92
+ return parse(raw);
93
+ return raw;
94
+ }, [] // stable -- reads from ref
95
+ );
96
+ const serializeContent = useCallback((content) => {
97
+ const { serialize } = optionsRef.current;
98
+ if (serialize)
99
+ return serialize(content);
100
+ return String(content);
101
+ }, [] // stable -- reads from ref
102
+ );
103
+ // ---- markDirty ----
104
+ const markDirty = useCallback(() => {
105
+ if (!optionsRef.current.getCurrentContent)
106
+ return; // read-only
107
+ if (!isDirtyRef.current) {
108
+ isDirtyRef.current = true;
109
+ setIsDirty(true);
110
+ host.setDirty(true);
111
+ }
112
+ }, [host]);
113
+ const clearDirty = useCallback(() => {
114
+ if (isDirtyRef.current) {
115
+ isDirtyRef.current = false;
116
+ setIsDirty(false);
117
+ host.setDirty(false);
118
+ }
119
+ }, [host]);
120
+ // ---- Initial load ----
121
+ useEffect(() => {
122
+ if (loadedRef.current)
123
+ return;
124
+ loadedRef.current = true;
125
+ let mounted = true;
126
+ const doLoad = async () => {
127
+ try {
128
+ const opts = optionsRef.current;
129
+ if (opts.binary) {
130
+ const data = await host.loadBinaryContent();
131
+ if (!mounted)
132
+ return;
133
+ opts.applyContent(data);
134
+ }
135
+ else {
136
+ const raw = await host.loadContent();
137
+ if (!mounted)
138
+ return;
139
+ const parsed = parseContent(raw);
140
+ lastSavedContentRef.current = raw;
141
+ opts.applyContent(parsed);
142
+ }
143
+ if (!mounted)
144
+ return;
145
+ setIsLoading(false);
146
+ optionsRef.current.onLoaded?.();
147
+ }
148
+ catch (err) {
149
+ if (!mounted)
150
+ return;
151
+ const loadError = err instanceof Error ? err : new Error(String(err));
152
+ setError(loadError);
153
+ setIsLoading(false);
154
+ }
155
+ };
156
+ doLoad();
157
+ return () => {
158
+ mounted = false;
159
+ };
160
+ }, [host, parseContent]);
161
+ // ---- Save handling ----
162
+ useEffect(() => {
163
+ return host.onSaveRequested(async () => {
164
+ const opts = optionsRef.current;
165
+ try {
166
+ if (opts.onSave) {
167
+ // Custom save handler (e.g., async content extraction from RevoGrid)
168
+ await opts.onSave();
169
+ }
170
+ else {
171
+ if (!opts.getCurrentContent)
172
+ return; // read-only
173
+ const content = opts.getCurrentContent();
174
+ if (opts.binary) {
175
+ await host.saveContent(content);
176
+ }
177
+ else {
178
+ const serialized = serializeContent(content);
179
+ // Update echo detection BEFORE saving so the subsequent onFileChanged is ignored
180
+ lastSavedContentRef.current = serialized;
181
+ await host.saveContent(serialized);
182
+ }
183
+ }
184
+ clearDirty();
185
+ }
186
+ catch (err) {
187
+ // Save failed -- dirty state remains
188
+ }
189
+ });
190
+ }, [host, serializeContent, clearDirty]);
191
+ // ---- External file changes ----
192
+ useEffect(() => {
193
+ return host.onFileChanged((newRawContent) => {
194
+ // Echo detection: ignore if this is our own save echoing back
195
+ if (newRawContent === lastSavedContentRef.current) {
196
+ return;
197
+ }
198
+ const opts = optionsRef.current;
199
+ try {
200
+ const parsed = parseContent(newRawContent);
201
+ lastSavedContentRef.current = newRawContent;
202
+ opts.applyContent(parsed);
203
+ clearDirty();
204
+ opts.onExternalChange?.(parsed);
205
+ }
206
+ catch (err) {
207
+ // Failed to parse external change
208
+ }
209
+ });
210
+ }, [host, parseContent, clearDirty]);
211
+ // ---- Theme changes ----
212
+ useEffect(() => {
213
+ return host.onThemeChanged((newTheme) => {
214
+ setTheme(newTheme);
215
+ });
216
+ }, [host]);
217
+ // ---- Diff mode ----
218
+ useEffect(() => {
219
+ if (!host.onDiffRequested)
220
+ return;
221
+ const unsubDiff = host.onDiffRequested((config) => {
222
+ const opts = optionsRef.current;
223
+ // Custom diff handler -- editor manages its own diff state
224
+ if (opts.onDiffRequested) {
225
+ opts.onDiffRequested(config);
226
+ return;
227
+ }
228
+ let original;
229
+ let modified;
230
+ try {
231
+ original = opts.binary
232
+ ? config.originalContent
233
+ : parseContent(config.originalContent);
234
+ modified = opts.binary
235
+ ? config.modifiedContent
236
+ : parseContent(config.modifiedContent);
237
+ }
238
+ catch (err) {
239
+ return;
240
+ }
241
+ setDiffState({
242
+ original,
243
+ modified,
244
+ tagId: config.tagId,
245
+ sessionId: config.sessionId,
246
+ accept: () => {
247
+ // Accept: apply modified content, report result
248
+ optionsRef.current.applyContent(modified);
249
+ if (!opts.binary) {
250
+ lastSavedContentRef.current =
251
+ serializeContent(modified);
252
+ }
253
+ host.reportDiffResult?.({
254
+ content: opts.binary
255
+ ? ''
256
+ : serializeContent(modified),
257
+ action: 'accept',
258
+ });
259
+ clearDirty();
260
+ setDiffState(null);
261
+ },
262
+ reject: () => {
263
+ // Reject: apply original content, report result
264
+ optionsRef.current.applyContent(original);
265
+ if (!opts.binary) {
266
+ lastSavedContentRef.current =
267
+ serializeContent(original);
268
+ }
269
+ host.reportDiffResult?.({
270
+ content: opts.binary
271
+ ? ''
272
+ : serializeContent(original),
273
+ action: 'reject',
274
+ });
275
+ clearDirty();
276
+ setDiffState(null);
277
+ },
278
+ });
279
+ });
280
+ return unsubDiff;
281
+ }, [host, parseContent, serializeContent, clearDirty]);
282
+ // Handle diff cleared externally (via unified diff header)
283
+ useEffect(() => {
284
+ if (!host.onDiffCleared)
285
+ return;
286
+ return host.onDiffCleared(async () => {
287
+ const opts = optionsRef.current;
288
+ // Custom diff cleared handler
289
+ if (opts.onDiffCleared) {
290
+ await opts.onDiffCleared();
291
+ return;
292
+ }
293
+ setDiffState(null);
294
+ // Reload content since the file may have changed
295
+ if (!opts.binary) {
296
+ host.loadContent().then((raw) => {
297
+ const parsed = parseContent(raw);
298
+ lastSavedContentRef.current = raw;
299
+ optionsRef.current.applyContent(parsed);
300
+ clearDirty();
301
+ });
302
+ }
303
+ });
304
+ }, [host, parseContent, clearDirty]);
305
+ // ---- Source mode ----
306
+ useEffect(() => {
307
+ if (!host.onSourceModeChanged)
308
+ return;
309
+ return host.onSourceModeChanged((isActive) => {
310
+ setIsSourceMode(isActive);
311
+ });
312
+ }, [host]);
313
+ const toggleSourceMode = host.toggleSourceMode
314
+ ? useCallback(() => host.toggleSourceMode?.(), [host])
315
+ : undefined;
316
+ // ---- Return ----
317
+ return {
318
+ markDirty,
319
+ isLoading,
320
+ error,
321
+ theme,
322
+ isDirty,
323
+ diffState,
324
+ toggleSourceMode,
325
+ isSourceMode,
326
+ };
327
+ }
@@ -0,0 +1,31 @@
1
+ export interface ValidationResult {
2
+ /** Whether the bundle passed validation */
3
+ valid: boolean;
4
+ /** Critical errors that will cause runtime failures */
5
+ errors: string[];
6
+ /** Warnings that may cause issues */
7
+ warnings: string[];
8
+ }
9
+ /**
10
+ * Validates an extension bundle for common issues.
11
+ *
12
+ * Run this after building to catch configuration mistakes before runtime.
13
+ *
14
+ * @param distPath - Path to the dist directory containing the bundle
15
+ * @param bundleName - Name of the bundle file (default: 'index.js')
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const result = await validateExtensionBundle('./dist');
20
+ * if (!result.valid) {
21
+ * console.error('Build failed:', result.errors);
22
+ * process.exit(1);
23
+ * }
24
+ * ```
25
+ */
26
+ export declare function validateExtensionBundle(distPath: string, bundleName?: string): Promise<ValidationResult>;
27
+ /**
28
+ * Prints validation results to console with formatting.
29
+ */
30
+ export declare function printValidationResult(result: ValidationResult): void;
31
+ //# sourceMappingURL=validate.d.ts.map