@epicdm/flowstate-extension-api 1.0.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.
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +272 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +231 -0
- package/dist/index.mjs.map +1 -0
- package/dist/testing/index.d.mts +31 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.js +172 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/index.mjs +141 -0
- package/dist/testing/index.mjs.map +1 -0
- package/package.json +55 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ActivationEvent, AuthService, CommandHandler, Disposable, DisposableStore, EventBusService, ExtensionContext, ExtensionHost, ExtensionInfo, ExtensionModule, ExtensionRegistry, ExtensionServices, ExtensionState, ExtensionStorage, ExtensionStorageAPI, ExtensionType, InMemoryStorageBackend, McpToolDefinition, NetworkService, PermissionEnforcer, Platform, PlatformEvent, SettingsTabConfig, StatusBarConfig, StorageBackend, SyncProviderFactory, UpdateInfo, ViewContribution } from '@epicdm/flowstate-app-framework/extensions';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ActivationEvent, AuthService, CommandHandler, Disposable, DisposableStore, EventBusService, ExtensionContext, ExtensionHost, ExtensionInfo, ExtensionModule, ExtensionRegistry, ExtensionServices, ExtensionState, ExtensionStorage, ExtensionStorageAPI, ExtensionType, InMemoryStorageBackend, McpToolDefinition, NetworkService, PermissionEnforcer, Platform, PlatformEvent, SettingsTabConfig, StatusBarConfig, StorageBackend, SyncProviderFactory, UpdateInfo, ViewContribution } from '@epicdm/flowstate-app-framework/extensions';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
DisposableStore: () => DisposableStore,
|
|
34
|
+
ExtensionRegistry: () => ExtensionRegistry,
|
|
35
|
+
ExtensionStorage: () => ExtensionStorage,
|
|
36
|
+
InMemoryStorageBackend: () => InMemoryStorageBackend,
|
|
37
|
+
PermissionEnforcer: () => PermissionEnforcer
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(src_exports);
|
|
40
|
+
|
|
41
|
+
// ../flowstate-app-framework/src/extensions/DisposableStore.ts
|
|
42
|
+
var DisposableStore = class {
|
|
43
|
+
constructor() {
|
|
44
|
+
this.disposables = [];
|
|
45
|
+
}
|
|
46
|
+
get size() {
|
|
47
|
+
return this.disposables.length;
|
|
48
|
+
}
|
|
49
|
+
add(disposable) {
|
|
50
|
+
this.disposables.push(disposable);
|
|
51
|
+
return disposable;
|
|
52
|
+
}
|
|
53
|
+
disposeAll() {
|
|
54
|
+
const toDispose = this.disposables.splice(0);
|
|
55
|
+
for (const d of toDispose) {
|
|
56
|
+
try {
|
|
57
|
+
d.dispose();
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ../flowstate-app-framework/src/extensions/PermissionEnforcer.ts
|
|
65
|
+
var PermissionEnforcer = class {
|
|
66
|
+
constructor(permissions) {
|
|
67
|
+
this.permissions = new Set(permissions);
|
|
68
|
+
this.networkDomains = new Set(
|
|
69
|
+
permissions.filter((p) => p.startsWith("network:")).map((p) => p.slice("network:".length))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
hasPermission(permission) {
|
|
73
|
+
return this.permissions.has(permission);
|
|
74
|
+
}
|
|
75
|
+
assertPermission(permission) {
|
|
76
|
+
if (!this.hasPermission(permission)) {
|
|
77
|
+
throw new Error(`Extension lacks permission: ${permission}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
canAccessNetwork(url) {
|
|
81
|
+
if (this.networkDomains.size === 0) return false;
|
|
82
|
+
try {
|
|
83
|
+
const parsed = new URL(url);
|
|
84
|
+
const host = parsed.host;
|
|
85
|
+
return this.networkDomains.has(host) || this.networkDomains.has(parsed.hostname);
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
assertNetworkAccess(url) {
|
|
91
|
+
if (!this.canAccessNetwork(url)) {
|
|
92
|
+
let domain = url;
|
|
93
|
+
try {
|
|
94
|
+
domain = new URL(url).host;
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`Network access denied for domain: ${domain}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ../flowstate-app-framework/src/extensions/ExtensionStorage.ts
|
|
103
|
+
var InMemoryStorageBackend = class {
|
|
104
|
+
constructor() {
|
|
105
|
+
this.store = /* @__PURE__ */ new Map();
|
|
106
|
+
}
|
|
107
|
+
async get(key) {
|
|
108
|
+
return this.store.get(key);
|
|
109
|
+
}
|
|
110
|
+
async set(key, value) {
|
|
111
|
+
this.store.set(key, value);
|
|
112
|
+
}
|
|
113
|
+
async delete(key) {
|
|
114
|
+
this.store.delete(key);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var ExtensionStorage = class {
|
|
118
|
+
constructor(extensionId, backend) {
|
|
119
|
+
this.backend = backend;
|
|
120
|
+
this.prefix = `ext:${extensionId}:`;
|
|
121
|
+
}
|
|
122
|
+
async get(key) {
|
|
123
|
+
const raw = await this.backend.get(this.prefix + key);
|
|
124
|
+
if (raw === void 0) return void 0;
|
|
125
|
+
return JSON.parse(raw);
|
|
126
|
+
}
|
|
127
|
+
async set(key, value) {
|
|
128
|
+
await this.backend.set(this.prefix + key, JSON.stringify(value));
|
|
129
|
+
}
|
|
130
|
+
async delete(key) {
|
|
131
|
+
await this.backend.delete(this.prefix + key);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// ../flowstate-app-framework/src/extensions/ExtensionRegistry.ts
|
|
136
|
+
var ExtensionRegistry = class {
|
|
137
|
+
constructor() {
|
|
138
|
+
this.extensions = /* @__PURE__ */ new Map();
|
|
139
|
+
this.views = /* @__PURE__ */ new Map();
|
|
140
|
+
// slot -> contributions
|
|
141
|
+
this.viewSnapshots = /* @__PURE__ */ new Map();
|
|
142
|
+
// memoized for useSyncExternalStore
|
|
143
|
+
this.extensionsSnapshot = null;
|
|
144
|
+
// memoized for useSyncExternalStore
|
|
145
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
146
|
+
}
|
|
147
|
+
register(info) {
|
|
148
|
+
this.extensions.set(info.id, { ...info });
|
|
149
|
+
this.notify();
|
|
150
|
+
}
|
|
151
|
+
get(id) {
|
|
152
|
+
const info = this.extensions.get(id);
|
|
153
|
+
return info ? { ...info } : void 0;
|
|
154
|
+
}
|
|
155
|
+
getState(id) {
|
|
156
|
+
return this.extensions.get(id)?.state;
|
|
157
|
+
}
|
|
158
|
+
getAll() {
|
|
159
|
+
return Array.from(this.extensions.values()).map((e) => ({ ...e }));
|
|
160
|
+
}
|
|
161
|
+
setState(id, state, error) {
|
|
162
|
+
const ext = this.extensions.get(id);
|
|
163
|
+
if (!ext) return;
|
|
164
|
+
ext.state = state;
|
|
165
|
+
ext.error = state === "failed" ? error : void 0;
|
|
166
|
+
this.notify();
|
|
167
|
+
}
|
|
168
|
+
remove(id) {
|
|
169
|
+
this.extensions.delete(id);
|
|
170
|
+
this.removeViewsForExtension(id);
|
|
171
|
+
}
|
|
172
|
+
// View contribution management
|
|
173
|
+
registerView(contribution) {
|
|
174
|
+
const { slot } = contribution;
|
|
175
|
+
if (!this.views.has(slot)) this.views.set(slot, []);
|
|
176
|
+
this.views.get(slot).push(contribution);
|
|
177
|
+
this.notify();
|
|
178
|
+
return () => {
|
|
179
|
+
const list = this.views.get(slot);
|
|
180
|
+
if (!list) return;
|
|
181
|
+
const idx = list.indexOf(contribution);
|
|
182
|
+
if (idx >= 0) list.splice(idx, 1);
|
|
183
|
+
this.notify();
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// Returns a stable memoized snapshot of all extensions for useSyncExternalStore.
|
|
187
|
+
// useSyncExternalStore compares snapshot references with Object.is, so returning
|
|
188
|
+
// a new array on every call would cause infinite re-renders.
|
|
189
|
+
// The cache is cleared on every notify() call so stale data is never served.
|
|
190
|
+
getExtensionsSnapshot() {
|
|
191
|
+
if (this.extensionsSnapshot === null) {
|
|
192
|
+
this.extensionsSnapshot = Array.from(this.extensions.values()).map((e) => ({ ...e }));
|
|
193
|
+
}
|
|
194
|
+
return this.extensionsSnapshot;
|
|
195
|
+
}
|
|
196
|
+
// Returns a memoized snapshot for useSyncExternalStore compatibility.
|
|
197
|
+
// useSyncExternalStore compares snapshot references with Object.is, so
|
|
198
|
+
// returning a new array on every call would cause infinite re-renders.
|
|
199
|
+
// The cache is cleared on every notify() call so stale data is never served.
|
|
200
|
+
getViewsForSlot(slot) {
|
|
201
|
+
if (!this.viewSnapshots.has(slot)) {
|
|
202
|
+
const sorted = (this.views.get(slot) ?? []).slice().sort((a, b) => a.order - b.order);
|
|
203
|
+
this.viewSnapshots.set(slot, sorted);
|
|
204
|
+
}
|
|
205
|
+
return this.viewSnapshots.get(slot);
|
|
206
|
+
}
|
|
207
|
+
removeViewsForExtension(extensionId) {
|
|
208
|
+
for (const [slot, contributions] of this.views) {
|
|
209
|
+
this.views.set(
|
|
210
|
+
slot,
|
|
211
|
+
contributions.filter((c) => c.extensionId !== extensionId)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
this.notify();
|
|
215
|
+
}
|
|
216
|
+
// Change notification
|
|
217
|
+
onChange(listener) {
|
|
218
|
+
this.listeners.add(listener);
|
|
219
|
+
return () => {
|
|
220
|
+
this.listeners.delete(listener);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
notify() {
|
|
224
|
+
this.viewSnapshots.clear();
|
|
225
|
+
this.extensionsSnapshot = null;
|
|
226
|
+
for (const listener of this.listeners) {
|
|
227
|
+
listener();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
clear() {
|
|
231
|
+
this.extensions.clear();
|
|
232
|
+
this.views.clear();
|
|
233
|
+
this.viewSnapshots.clear();
|
|
234
|
+
this.extensionsSnapshot = null;
|
|
235
|
+
this.notify();
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// ../flowstate-app-framework/src/extensions/ExtensionSlot.tsx
|
|
240
|
+
var import_react = __toESM(require("react"));
|
|
241
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
242
|
+
|
|
243
|
+
// ../flowstate-app-framework/src/extensions/ExtensionHostContext.tsx
|
|
244
|
+
var import_react2 = __toESM(require("react"));
|
|
245
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
246
|
+
var ExtensionHostCtx = (0, import_react2.createContext)(null);
|
|
247
|
+
var McpToolRegistryContext = (0, import_react2.createContext)(null);
|
|
248
|
+
var ThemeRegistryContext = (0, import_react2.createContext)(null);
|
|
249
|
+
|
|
250
|
+
// ../flowstate-app-framework/src/extensions/hooks/useExtensions.ts
|
|
251
|
+
var import_react3 = require("react");
|
|
252
|
+
|
|
253
|
+
// ../flowstate-app-framework/src/extensions/hooks/useMarketplaceSearch.ts
|
|
254
|
+
var import_react4 = require("react");
|
|
255
|
+
|
|
256
|
+
// ../flowstate-app-framework/src/extensions/hooks/useExtensionUpdates.ts
|
|
257
|
+
var import_react5 = require("react");
|
|
258
|
+
|
|
259
|
+
// ../flowstate-app-framework/src/extensions/hooks/useMcpTools.ts
|
|
260
|
+
var import_react6 = require("react");
|
|
261
|
+
|
|
262
|
+
// ../flowstate-app-framework/src/extensions/hooks/useActiveTheme.ts
|
|
263
|
+
var import_react7 = require("react");
|
|
264
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
265
|
+
0 && (module.exports = {
|
|
266
|
+
DisposableStore,
|
|
267
|
+
ExtensionRegistry,
|
|
268
|
+
ExtensionStorage,
|
|
269
|
+
InMemoryStorageBackend,
|
|
270
|
+
PermissionEnforcer
|
|
271
|
+
});
|
|
272
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../flowstate-app-framework/src/extensions/DisposableStore.ts","../../flowstate-app-framework/src/extensions/PermissionEnforcer.ts","../../flowstate-app-framework/src/extensions/ExtensionStorage.ts","../../flowstate-app-framework/src/extensions/ExtensionRegistry.ts","../../flowstate-app-framework/src/extensions/ExtensionSlot.tsx","../../flowstate-app-framework/src/extensions/ExtensionHostContext.tsx","../../flowstate-app-framework/src/extensions/hooks/useExtensions.ts","../../flowstate-app-framework/src/extensions/hooks/useMarketplaceSearch.ts","../../flowstate-app-framework/src/extensions/hooks/useExtensionUpdates.ts","../../flowstate-app-framework/src/extensions/hooks/useMcpTools.ts","../../flowstate-app-framework/src/extensions/hooks/useActiveTheme.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\n// Core types — the public API surface for extension authors.\n// Imported from the framework's dedicated extensions sub-path so we pull\n// only the extension surface, not the full framework bundle.\nexport type {\n Disposable,\n ExtensionState,\n ExtensionInfo,\n ExtensionType,\n Platform,\n ExtensionModule,\n ExtensionContext,\n ExtensionStorageAPI,\n CommandHandler,\n SettingsTabConfig,\n StatusBarConfig,\n SyncProviderFactory,\n McpToolDefinition,\n ExtensionServices,\n AuthService,\n NetworkService,\n EventBusService,\n PlatformEvent,\n ExtensionHost,\n UpdateInfo,\n ActivationEvent,\n ViewContribution,\n} from '@epicdm/flowstate-app-framework/extensions'\n\n// Runtime classes — for advanced usage (building custom hosts, testing)\nexport {\n DisposableStore,\n PermissionEnforcer,\n ExtensionStorage,\n InMemoryStorageBackend,\n ExtensionRegistry,\n} from '@epicdm/flowstate-app-framework/extensions'\n\nexport type { StorageBackend } from '@epicdm/flowstate-app-framework/extensions'\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport type { Disposable } from './types'\n\nexport class DisposableStore {\n private disposables: Disposable[] = []\n\n get size(): number {\n return this.disposables.length\n }\n\n add<T extends Disposable>(disposable: T): T {\n this.disposables.push(disposable)\n return disposable\n }\n\n disposeAll(): void {\n const toDispose = this.disposables.splice(0)\n for (const d of toDispose) {\n try {\n d.dispose()\n } catch {\n // Swallow errors — continue disposing remaining items\n }\n }\n }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nexport class PermissionEnforcer {\n private readonly permissions: Set<string>\n private readonly networkDomains: Set<string>\n\n constructor(permissions: string[]) {\n this.permissions = new Set(permissions)\n this.networkDomains = new Set(\n permissions.filter(p => p.startsWith('network:')).map(p => p.slice('network:'.length))\n )\n }\n\n hasPermission(permission: string): boolean {\n return this.permissions.has(permission)\n }\n\n assertPermission(permission: string): void {\n if (!this.hasPermission(permission)) {\n throw new Error(`Extension lacks permission: ${permission}`)\n }\n }\n\n canAccessNetwork(url: string): boolean {\n if (this.networkDomains.size === 0) return false\n try {\n const parsed = new URL(url)\n const host = parsed.host // includes port if present\n return this.networkDomains.has(host) || this.networkDomains.has(parsed.hostname)\n } catch {\n return false\n }\n }\n\n assertNetworkAccess(url: string): void {\n if (!this.canAccessNetwork(url)) {\n let domain = url\n try {\n domain = new URL(url).host\n } catch {\n // Use raw URL if parsing fails\n }\n throw new Error(`Network access denied for domain: ${domain}`)\n }\n }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport type { ExtensionStorageAPI } from './types'\n\nexport interface StorageBackend {\n get(key: string): Promise<string | undefined>\n set(key: string, value: string): Promise<void>\n delete(key: string): Promise<void>\n}\n\nexport class InMemoryStorageBackend implements StorageBackend {\n private store = new Map<string, string>()\n\n async get(key: string): Promise<string | undefined> {\n return this.store.get(key)\n }\n\n async set(key: string, value: string): Promise<void> {\n this.store.set(key, value)\n }\n\n async delete(key: string): Promise<void> {\n this.store.delete(key)\n }\n}\n\nexport class ExtensionStorage implements ExtensionStorageAPI {\n private readonly prefix: string\n\n constructor(\n extensionId: string,\n private readonly backend: StorageBackend\n ) {\n this.prefix = `ext:${extensionId}:`\n }\n\n async get<T>(key: string): Promise<T | undefined> {\n const raw = await this.backend.get(this.prefix + key)\n if (raw === undefined) return undefined\n return JSON.parse(raw) as T\n }\n\n async set<T>(key: string, value: T): Promise<void> {\n await this.backend.set(this.prefix + key, JSON.stringify(value))\n }\n\n async delete(key: string): Promise<void> {\n await this.backend.delete(this.prefix + key)\n }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport type { ExtensionInfo, ExtensionState, ViewContribution } from './types'\n\nexport class ExtensionRegistry {\n private extensions = new Map<string, ExtensionInfo>()\n private views = new Map<string, ViewContribution[]>() // slot -> contributions\n private viewSnapshots = new Map<string, ViewContribution[]>() // memoized for useSyncExternalStore\n private extensionsSnapshot: ExtensionInfo[] | null = null // memoized for useSyncExternalStore\n private listeners = new Set<() => void>()\n\n register(info: ExtensionInfo): void {\n this.extensions.set(info.id, { ...info })\n this.notify()\n }\n\n get(id: string): ExtensionInfo | undefined {\n const info = this.extensions.get(id)\n return info ? { ...info } : undefined\n }\n\n getState(id: string): ExtensionState | undefined {\n return this.extensions.get(id)?.state\n }\n\n getAll(): ExtensionInfo[] {\n return Array.from(this.extensions.values()).map(e => ({ ...e }))\n }\n\n setState(id: string, state: ExtensionState, error?: string): void {\n const ext = this.extensions.get(id)\n if (!ext) return\n ext.state = state\n ext.error = state === 'failed' ? error : undefined\n this.notify()\n }\n\n remove(id: string): void {\n this.extensions.delete(id)\n this.removeViewsForExtension(id) // calls notify() internally\n }\n\n // View contribution management\n\n registerView(contribution: ViewContribution): () => void {\n const { slot } = contribution\n if (!this.views.has(slot)) this.views.set(slot, [])\n this.views.get(slot)!.push(contribution)\n this.notify()\n return () => {\n const list = this.views.get(slot)\n if (!list) return\n const idx = list.indexOf(contribution)\n if (idx >= 0) list.splice(idx, 1)\n this.notify()\n }\n }\n\n // Returns a stable memoized snapshot of all extensions for useSyncExternalStore.\n // useSyncExternalStore compares snapshot references with Object.is, so returning\n // a new array on every call would cause infinite re-renders.\n // The cache is cleared on every notify() call so stale data is never served.\n getExtensionsSnapshot(): ExtensionInfo[] {\n if (this.extensionsSnapshot === null) {\n this.extensionsSnapshot = Array.from(this.extensions.values()).map(e => ({ ...e }))\n }\n return this.extensionsSnapshot\n }\n\n // Returns a memoized snapshot for useSyncExternalStore compatibility.\n // useSyncExternalStore compares snapshot references with Object.is, so\n // returning a new array on every call would cause infinite re-renders.\n // The cache is cleared on every notify() call so stale data is never served.\n getViewsForSlot(slot: string): ViewContribution[] {\n if (!this.viewSnapshots.has(slot)) {\n const sorted = (this.views.get(slot) ?? []).slice().sort((a, b) => a.order - b.order)\n this.viewSnapshots.set(slot, sorted)\n }\n return this.viewSnapshots.get(slot)!\n }\n\n removeViewsForExtension(extensionId: string): void {\n for (const [slot, contributions] of this.views) {\n this.views.set(\n slot,\n contributions.filter(c => c.extensionId !== extensionId)\n )\n }\n this.notify()\n }\n\n // Change notification\n\n onChange(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n private notify(): void {\n this.viewSnapshots.clear() // invalidate memoized snapshots\n this.extensionsSnapshot = null // invalidate extensions snapshot\n for (const listener of this.listeners) {\n listener()\n }\n }\n\n clear(): void {\n this.extensions.clear()\n this.views.clear()\n this.viewSnapshots.clear()\n this.extensionsSnapshot = null\n this.notify()\n }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport React, { Component, useSyncExternalStore, type ReactNode } from 'react'\nimport type { ExtensionRegistry } from './ExtensionRegistry'\n\n// Error boundary per contribution\ninterface SlotErrorBoundaryProps {\n extensionId: string\n children: ReactNode\n}\n\ninterface SlotErrorBoundaryState {\n hasError: boolean\n}\n\nclass SlotErrorBoundary extends Component<SlotErrorBoundaryProps, SlotErrorBoundaryState> {\n state: SlotErrorBoundaryState = { hasError: false }\n\n static getDerivedStateFromError(): SlotErrorBoundaryState {\n return { hasError: true }\n }\n\n componentDidCatch(error: Error) {\n console.error(`[ExtensionSlot] Extension \"${this.props.extensionId}\" threw:`, error)\n }\n\n render() {\n if (this.state.hasError) return null\n return this.props.children\n }\n}\n\n// ExtensionSlot component\nexport interface ExtensionSlotProps {\n name: string\n context: Record<string, unknown>\n registry: ExtensionRegistry\n activationEngine?: { triggerEvent(event: string): Promise<void> }\n className?: string\n}\n\nexport function ExtensionSlot({\n name,\n context,\n registry,\n activationEngine,\n className,\n}: ExtensionSlotProps) {\n // Trigger lazy activation for extensions listening to onView:<slot>\n React.useEffect(() => {\n activationEngine?.triggerEvent(`onView:${name}`)\n }, [activationEngine, name])\n\n const views = useSyncExternalStore(\n onStoreChange => registry.onChange(onStoreChange),\n () => registry.getViewsForSlot(name)\n )\n\n if (views.length === 0) return null\n\n return (\n <div className={className} data-extension-slot={name}>\n {views.map((view, i) => (\n <SlotErrorBoundary key={`${view.extensionId}-${i}`} extensionId={view.extensionId}>\n <view.component {...context} />\n </SlotErrorBoundary>\n ))}\n </div>\n )\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport React, { createContext, useContext } from 'react'\nimport type { ExtensionHost } from './types'\nimport type { ExtensionRegistry } from './ExtensionRegistry'\nimport type { ActivationEngine } from './ActivationEngine'\nimport type { McpToolRegistry } from './McpToolRegistry'\nimport type { ThemeRegistry } from './ThemeRegistry'\n\ninterface ExtensionHostContextValue {\n registry: ExtensionRegistry\n host: ExtensionHost | null\n engine: ActivationEngine | null\n}\n\nconst ExtensionHostCtx = createContext<ExtensionHostContextValue | null>(null)\nconst McpToolRegistryContext = createContext<McpToolRegistry | null>(null)\nconst ThemeRegistryContext = createContext<ThemeRegistry | null>(null)\n\nexport interface ExtensionHostProviderProps {\n registry: ExtensionRegistry\n host: ExtensionHost | null\n engine: ActivationEngine | null\n mcpToolRegistry?: McpToolRegistry | null\n themeRegistry?: ThemeRegistry | null\n children: React.ReactNode\n}\n\nexport function ExtensionHostProvider({\n registry,\n host,\n engine,\n mcpToolRegistry,\n themeRegistry,\n children,\n}: ExtensionHostProviderProps) {\n const value = React.useMemo(() => ({ registry, host, engine }), [registry, host, engine])\n return (\n <ThemeRegistryContext.Provider value={themeRegistry ?? null}>\n <McpToolRegistryContext.Provider value={mcpToolRegistry ?? null}>\n <ExtensionHostCtx.Provider value={value}>{children}</ExtensionHostCtx.Provider>\n </McpToolRegistryContext.Provider>\n </ThemeRegistryContext.Provider>\n )\n}\n\nfunction useExtensionHostContext(): ExtensionHostContextValue {\n const ctx = useContext(ExtensionHostCtx)\n if (!ctx) throw new Error('Extension hooks must be used within ExtensionHostProvider')\n return ctx\n}\n\nexport function useExtensionRegistry(): ExtensionRegistry {\n return useExtensionHostContext().registry\n}\n\nexport function useExtensionHost(): ExtensionHost | null {\n return useExtensionHostContext().host\n}\n\nexport function useActivationEngine(): ActivationEngine | null {\n return useExtensionHostContext().engine\n}\n\nexport function useMcpToolRegistry(): McpToolRegistry | null {\n const ctx = useContext(McpToolRegistryContext)\n return ctx\n}\n\nexport function useThemeRegistry(): ThemeRegistry | null {\n return useContext(ThemeRegistryContext)\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport { useSyncExternalStore } from 'react'\nimport type { ExtensionInfo, ExtensionState } from '../types'\nimport { useExtensionRegistry } from '../ExtensionHostContext'\n\nexport function useExtensions(): ExtensionInfo[] {\n const registry = useExtensionRegistry()\n return useSyncExternalStore(\n cb => registry.onChange(cb),\n () => registry.getExtensionsSnapshot()\n )\n}\n\nexport function useExtensionState(id: string): ExtensionState | undefined {\n const registry = useExtensionRegistry()\n return useSyncExternalStore(\n cb => registry.onChange(cb),\n () => registry.getState(id)\n )\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport { useEffect, useState } from 'react'\n\nexport interface MarketplaceSearchOptions {\n baseUrl: string\n query?: string\n category?: string\n type?: string\n limit?: number\n}\n\nexport interface MarketplaceListingResult {\n id: string\n label: string\n description: string\n category: string\n type: string\n tags: string[]\n iconUrl: string | null\n author: string | null\n downloadCount: number | null\n pricing: string | null\n priceUsdc: string | null\n}\n\nexport interface MarketplaceSearchResult {\n results: MarketplaceListingResult[]\n loading: boolean\n error: string | null\n}\n\nexport function useMarketplaceSearch(options: MarketplaceSearchOptions): MarketplaceSearchResult {\n const { baseUrl, query, category, type, limit } = options\n\n const hasSearchParams = Boolean(query || category || type)\n\n const [results, setResults] = useState<MarketplaceListingResult[]>([])\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n useEffect(() => {\n if (!hasSearchParams) {\n setResults([])\n setLoading(false)\n setError(null)\n return\n }\n\n setLoading(true)\n setError(null)\n\n const controller = new AbortController()\n\n const timer = setTimeout(async () => {\n const params = new URLSearchParams()\n if (query) params.set('q', query)\n if (category) params.set('category', category)\n if (type) params.set('type', type)\n params.set('limit', String(limit ?? 20))\n\n const url = `${baseUrl}/api/listings?${params.toString()}`\n\n try {\n const response = await fetch(url, { signal: controller.signal })\n\n if (!response.ok) {\n setError(`Request failed with status ${response.status}`)\n setResults([])\n setLoading(false)\n return\n }\n\n const data = await response.json()\n setResults(data.listings ?? [])\n setError(null)\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n // Fetch was aborted by cleanup — do not update state\n return\n }\n setError(err instanceof Error ? err.message : 'Unknown error')\n setResults([])\n } finally {\n // Only clear loading if the fetch was not aborted\n if (!controller.signal.aborted) {\n setLoading(false)\n }\n }\n }, 300)\n\n return () => {\n clearTimeout(timer)\n controller.abort()\n }\n }, [baseUrl, query, category, type, limit, hasSearchParams])\n\n return { results, loading, error }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport { useEffect, useState } from 'react'\nimport { useExtensionHost } from '../ExtensionHostContext'\nimport type { UpdateInfo } from '../types'\n\nexport function useExtensionUpdates(): { updates: UpdateInfo[]; loading: boolean } {\n const host = useExtensionHost()\n const [updates, setUpdates] = useState<UpdateInfo[]>([])\n const [loading, setLoading] = useState(false)\n\n useEffect(() => {\n if (!host) return\n let cancelled = false\n setLoading(true)\n host\n .checkForUpdates()\n .then(result => {\n if (!cancelled) {\n setUpdates(result)\n setLoading(false)\n }\n })\n .catch(() => {\n if (!cancelled) setLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [host])\n\n return { updates, loading }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport { useSyncExternalStore } from 'react'\nimport type { McpToolRegistry } from '../McpToolRegistry'\nimport type { RegisteredMcpTool } from '../types'\n\n/**\n * React hook that subscribes to the McpToolRegistry and returns\n * the current list of registered MCP tools. Re-renders when tools\n * are added or removed.\n */\nexport function useMcpTools(registry: McpToolRegistry): RegisteredMcpTool[] {\n return useSyncExternalStore(\n onStoreChange => registry.onChange(onStoreChange),\n () => registry.getToolsSnapshot()\n )\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport { useSyncExternalStore } from 'react'\nimport type { ThemeRegistry, RegisteredTheme } from '../ThemeRegistry'\n\nexport function useActiveTheme(registry: ThemeRegistry): RegisteredTheme | null {\n return useSyncExternalStore(\n onStoreChange => registry.onChange(onStoreChange),\n () => registry.getActiveThemeSnapshot()\n )\n}\n"],"mappings":"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","names":["import_react","import_jsx_runtime","import_react","import_react","import_react","import_react","import_react"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// ../flowstate-app-framework/src/extensions/DisposableStore.ts
|
|
2
|
+
var DisposableStore = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.disposables = [];
|
|
5
|
+
}
|
|
6
|
+
get size() {
|
|
7
|
+
return this.disposables.length;
|
|
8
|
+
}
|
|
9
|
+
add(disposable) {
|
|
10
|
+
this.disposables.push(disposable);
|
|
11
|
+
return disposable;
|
|
12
|
+
}
|
|
13
|
+
disposeAll() {
|
|
14
|
+
const toDispose = this.disposables.splice(0);
|
|
15
|
+
for (const d of toDispose) {
|
|
16
|
+
try {
|
|
17
|
+
d.dispose();
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ../flowstate-app-framework/src/extensions/PermissionEnforcer.ts
|
|
25
|
+
var PermissionEnforcer = class {
|
|
26
|
+
constructor(permissions) {
|
|
27
|
+
this.permissions = new Set(permissions);
|
|
28
|
+
this.networkDomains = new Set(
|
|
29
|
+
permissions.filter((p) => p.startsWith("network:")).map((p) => p.slice("network:".length))
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
hasPermission(permission) {
|
|
33
|
+
return this.permissions.has(permission);
|
|
34
|
+
}
|
|
35
|
+
assertPermission(permission) {
|
|
36
|
+
if (!this.hasPermission(permission)) {
|
|
37
|
+
throw new Error(`Extension lacks permission: ${permission}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
canAccessNetwork(url) {
|
|
41
|
+
if (this.networkDomains.size === 0) return false;
|
|
42
|
+
try {
|
|
43
|
+
const parsed = new URL(url);
|
|
44
|
+
const host = parsed.host;
|
|
45
|
+
return this.networkDomains.has(host) || this.networkDomains.has(parsed.hostname);
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
assertNetworkAccess(url) {
|
|
51
|
+
if (!this.canAccessNetwork(url)) {
|
|
52
|
+
let domain = url;
|
|
53
|
+
try {
|
|
54
|
+
domain = new URL(url).host;
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Network access denied for domain: ${domain}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ../flowstate-app-framework/src/extensions/ExtensionStorage.ts
|
|
63
|
+
var InMemoryStorageBackend = class {
|
|
64
|
+
constructor() {
|
|
65
|
+
this.store = /* @__PURE__ */ new Map();
|
|
66
|
+
}
|
|
67
|
+
async get(key) {
|
|
68
|
+
return this.store.get(key);
|
|
69
|
+
}
|
|
70
|
+
async set(key, value) {
|
|
71
|
+
this.store.set(key, value);
|
|
72
|
+
}
|
|
73
|
+
async delete(key) {
|
|
74
|
+
this.store.delete(key);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var ExtensionStorage = class {
|
|
78
|
+
constructor(extensionId, backend) {
|
|
79
|
+
this.backend = backend;
|
|
80
|
+
this.prefix = `ext:${extensionId}:`;
|
|
81
|
+
}
|
|
82
|
+
async get(key) {
|
|
83
|
+
const raw = await this.backend.get(this.prefix + key);
|
|
84
|
+
if (raw === void 0) return void 0;
|
|
85
|
+
return JSON.parse(raw);
|
|
86
|
+
}
|
|
87
|
+
async set(key, value) {
|
|
88
|
+
await this.backend.set(this.prefix + key, JSON.stringify(value));
|
|
89
|
+
}
|
|
90
|
+
async delete(key) {
|
|
91
|
+
await this.backend.delete(this.prefix + key);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ../flowstate-app-framework/src/extensions/ExtensionRegistry.ts
|
|
96
|
+
var ExtensionRegistry = class {
|
|
97
|
+
constructor() {
|
|
98
|
+
this.extensions = /* @__PURE__ */ new Map();
|
|
99
|
+
this.views = /* @__PURE__ */ new Map();
|
|
100
|
+
// slot -> contributions
|
|
101
|
+
this.viewSnapshots = /* @__PURE__ */ new Map();
|
|
102
|
+
// memoized for useSyncExternalStore
|
|
103
|
+
this.extensionsSnapshot = null;
|
|
104
|
+
// memoized for useSyncExternalStore
|
|
105
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
106
|
+
}
|
|
107
|
+
register(info) {
|
|
108
|
+
this.extensions.set(info.id, { ...info });
|
|
109
|
+
this.notify();
|
|
110
|
+
}
|
|
111
|
+
get(id) {
|
|
112
|
+
const info = this.extensions.get(id);
|
|
113
|
+
return info ? { ...info } : void 0;
|
|
114
|
+
}
|
|
115
|
+
getState(id) {
|
|
116
|
+
return this.extensions.get(id)?.state;
|
|
117
|
+
}
|
|
118
|
+
getAll() {
|
|
119
|
+
return Array.from(this.extensions.values()).map((e) => ({ ...e }));
|
|
120
|
+
}
|
|
121
|
+
setState(id, state, error) {
|
|
122
|
+
const ext = this.extensions.get(id);
|
|
123
|
+
if (!ext) return;
|
|
124
|
+
ext.state = state;
|
|
125
|
+
ext.error = state === "failed" ? error : void 0;
|
|
126
|
+
this.notify();
|
|
127
|
+
}
|
|
128
|
+
remove(id) {
|
|
129
|
+
this.extensions.delete(id);
|
|
130
|
+
this.removeViewsForExtension(id);
|
|
131
|
+
}
|
|
132
|
+
// View contribution management
|
|
133
|
+
registerView(contribution) {
|
|
134
|
+
const { slot } = contribution;
|
|
135
|
+
if (!this.views.has(slot)) this.views.set(slot, []);
|
|
136
|
+
this.views.get(slot).push(contribution);
|
|
137
|
+
this.notify();
|
|
138
|
+
return () => {
|
|
139
|
+
const list = this.views.get(slot);
|
|
140
|
+
if (!list) return;
|
|
141
|
+
const idx = list.indexOf(contribution);
|
|
142
|
+
if (idx >= 0) list.splice(idx, 1);
|
|
143
|
+
this.notify();
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Returns a stable memoized snapshot of all extensions for useSyncExternalStore.
|
|
147
|
+
// useSyncExternalStore compares snapshot references with Object.is, so returning
|
|
148
|
+
// a new array on every call would cause infinite re-renders.
|
|
149
|
+
// The cache is cleared on every notify() call so stale data is never served.
|
|
150
|
+
getExtensionsSnapshot() {
|
|
151
|
+
if (this.extensionsSnapshot === null) {
|
|
152
|
+
this.extensionsSnapshot = Array.from(this.extensions.values()).map((e) => ({ ...e }));
|
|
153
|
+
}
|
|
154
|
+
return this.extensionsSnapshot;
|
|
155
|
+
}
|
|
156
|
+
// Returns a memoized snapshot for useSyncExternalStore compatibility.
|
|
157
|
+
// useSyncExternalStore compares snapshot references with Object.is, so
|
|
158
|
+
// returning a new array on every call would cause infinite re-renders.
|
|
159
|
+
// The cache is cleared on every notify() call so stale data is never served.
|
|
160
|
+
getViewsForSlot(slot) {
|
|
161
|
+
if (!this.viewSnapshots.has(slot)) {
|
|
162
|
+
const sorted = (this.views.get(slot) ?? []).slice().sort((a, b) => a.order - b.order);
|
|
163
|
+
this.viewSnapshots.set(slot, sorted);
|
|
164
|
+
}
|
|
165
|
+
return this.viewSnapshots.get(slot);
|
|
166
|
+
}
|
|
167
|
+
removeViewsForExtension(extensionId) {
|
|
168
|
+
for (const [slot, contributions] of this.views) {
|
|
169
|
+
this.views.set(
|
|
170
|
+
slot,
|
|
171
|
+
contributions.filter((c) => c.extensionId !== extensionId)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
this.notify();
|
|
175
|
+
}
|
|
176
|
+
// Change notification
|
|
177
|
+
onChange(listener) {
|
|
178
|
+
this.listeners.add(listener);
|
|
179
|
+
return () => {
|
|
180
|
+
this.listeners.delete(listener);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
notify() {
|
|
184
|
+
this.viewSnapshots.clear();
|
|
185
|
+
this.extensionsSnapshot = null;
|
|
186
|
+
for (const listener of this.listeners) {
|
|
187
|
+
listener();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
clear() {
|
|
191
|
+
this.extensions.clear();
|
|
192
|
+
this.views.clear();
|
|
193
|
+
this.viewSnapshots.clear();
|
|
194
|
+
this.extensionsSnapshot = null;
|
|
195
|
+
this.notify();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// ../flowstate-app-framework/src/extensions/ExtensionSlot.tsx
|
|
200
|
+
import React, { Component, useSyncExternalStore } from "react";
|
|
201
|
+
import { jsx } from "react/jsx-runtime";
|
|
202
|
+
|
|
203
|
+
// ../flowstate-app-framework/src/extensions/ExtensionHostContext.tsx
|
|
204
|
+
import React2, { createContext, useContext } from "react";
|
|
205
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
206
|
+
var ExtensionHostCtx = createContext(null);
|
|
207
|
+
var McpToolRegistryContext = createContext(null);
|
|
208
|
+
var ThemeRegistryContext = createContext(null);
|
|
209
|
+
|
|
210
|
+
// ../flowstate-app-framework/src/extensions/hooks/useExtensions.ts
|
|
211
|
+
import { useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
212
|
+
|
|
213
|
+
// ../flowstate-app-framework/src/extensions/hooks/useMarketplaceSearch.ts
|
|
214
|
+
import { useEffect, useState } from "react";
|
|
215
|
+
|
|
216
|
+
// ../flowstate-app-framework/src/extensions/hooks/useExtensionUpdates.ts
|
|
217
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
218
|
+
|
|
219
|
+
// ../flowstate-app-framework/src/extensions/hooks/useMcpTools.ts
|
|
220
|
+
import { useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
221
|
+
|
|
222
|
+
// ../flowstate-app-framework/src/extensions/hooks/useActiveTheme.ts
|
|
223
|
+
import { useSyncExternalStore as useSyncExternalStore4 } from "react";
|
|
224
|
+
export {
|
|
225
|
+
DisposableStore,
|
|
226
|
+
ExtensionRegistry,
|
|
227
|
+
ExtensionStorage,
|
|
228
|
+
InMemoryStorageBackend,
|
|
229
|
+
PermissionEnforcer
|
|
230
|
+
};
|
|
231
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../flowstate-app-framework/src/extensions/DisposableStore.ts","../../flowstate-app-framework/src/extensions/PermissionEnforcer.ts","../../flowstate-app-framework/src/extensions/ExtensionStorage.ts","../../flowstate-app-framework/src/extensions/ExtensionRegistry.ts","../../flowstate-app-framework/src/extensions/ExtensionSlot.tsx","../../flowstate-app-framework/src/extensions/ExtensionHostContext.tsx","../../flowstate-app-framework/src/extensions/hooks/useExtensions.ts","../../flowstate-app-framework/src/extensions/hooks/useMarketplaceSearch.ts","../../flowstate-app-framework/src/extensions/hooks/useExtensionUpdates.ts","../../flowstate-app-framework/src/extensions/hooks/useMcpTools.ts","../../flowstate-app-framework/src/extensions/hooks/useActiveTheme.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport type { Disposable } from './types'\n\nexport class DisposableStore {\n private disposables: Disposable[] = []\n\n get size(): number {\n return this.disposables.length\n }\n\n add<T extends Disposable>(disposable: T): T {\n this.disposables.push(disposable)\n return disposable\n }\n\n disposeAll(): void {\n const toDispose = this.disposables.splice(0)\n for (const d of toDispose) {\n try {\n d.dispose()\n } catch {\n // Swallow errors — continue disposing remaining items\n }\n }\n }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nexport class PermissionEnforcer {\n private readonly permissions: Set<string>\n private readonly networkDomains: Set<string>\n\n constructor(permissions: string[]) {\n this.permissions = new Set(permissions)\n this.networkDomains = new Set(\n permissions.filter(p => p.startsWith('network:')).map(p => p.slice('network:'.length))\n )\n }\n\n hasPermission(permission: string): boolean {\n return this.permissions.has(permission)\n }\n\n assertPermission(permission: string): void {\n if (!this.hasPermission(permission)) {\n throw new Error(`Extension lacks permission: ${permission}`)\n }\n }\n\n canAccessNetwork(url: string): boolean {\n if (this.networkDomains.size === 0) return false\n try {\n const parsed = new URL(url)\n const host = parsed.host // includes port if present\n return this.networkDomains.has(host) || this.networkDomains.has(parsed.hostname)\n } catch {\n return false\n }\n }\n\n assertNetworkAccess(url: string): void {\n if (!this.canAccessNetwork(url)) {\n let domain = url\n try {\n domain = new URL(url).host\n } catch {\n // Use raw URL if parsing fails\n }\n throw new Error(`Network access denied for domain: ${domain}`)\n }\n }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport type { ExtensionStorageAPI } from './types'\n\nexport interface StorageBackend {\n get(key: string): Promise<string | undefined>\n set(key: string, value: string): Promise<void>\n delete(key: string): Promise<void>\n}\n\nexport class InMemoryStorageBackend implements StorageBackend {\n private store = new Map<string, string>()\n\n async get(key: string): Promise<string | undefined> {\n return this.store.get(key)\n }\n\n async set(key: string, value: string): Promise<void> {\n this.store.set(key, value)\n }\n\n async delete(key: string): Promise<void> {\n this.store.delete(key)\n }\n}\n\nexport class ExtensionStorage implements ExtensionStorageAPI {\n private readonly prefix: string\n\n constructor(\n extensionId: string,\n private readonly backend: StorageBackend\n ) {\n this.prefix = `ext:${extensionId}:`\n }\n\n async get<T>(key: string): Promise<T | undefined> {\n const raw = await this.backend.get(this.prefix + key)\n if (raw === undefined) return undefined\n return JSON.parse(raw) as T\n }\n\n async set<T>(key: string, value: T): Promise<void> {\n await this.backend.set(this.prefix + key, JSON.stringify(value))\n }\n\n async delete(key: string): Promise<void> {\n await this.backend.delete(this.prefix + key)\n }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport type { ExtensionInfo, ExtensionState, ViewContribution } from './types'\n\nexport class ExtensionRegistry {\n private extensions = new Map<string, ExtensionInfo>()\n private views = new Map<string, ViewContribution[]>() // slot -> contributions\n private viewSnapshots = new Map<string, ViewContribution[]>() // memoized for useSyncExternalStore\n private extensionsSnapshot: ExtensionInfo[] | null = null // memoized for useSyncExternalStore\n private listeners = new Set<() => void>()\n\n register(info: ExtensionInfo): void {\n this.extensions.set(info.id, { ...info })\n this.notify()\n }\n\n get(id: string): ExtensionInfo | undefined {\n const info = this.extensions.get(id)\n return info ? { ...info } : undefined\n }\n\n getState(id: string): ExtensionState | undefined {\n return this.extensions.get(id)?.state\n }\n\n getAll(): ExtensionInfo[] {\n return Array.from(this.extensions.values()).map(e => ({ ...e }))\n }\n\n setState(id: string, state: ExtensionState, error?: string): void {\n const ext = this.extensions.get(id)\n if (!ext) return\n ext.state = state\n ext.error = state === 'failed' ? error : undefined\n this.notify()\n }\n\n remove(id: string): void {\n this.extensions.delete(id)\n this.removeViewsForExtension(id) // calls notify() internally\n }\n\n // View contribution management\n\n registerView(contribution: ViewContribution): () => void {\n const { slot } = contribution\n if (!this.views.has(slot)) this.views.set(slot, [])\n this.views.get(slot)!.push(contribution)\n this.notify()\n return () => {\n const list = this.views.get(slot)\n if (!list) return\n const idx = list.indexOf(contribution)\n if (idx >= 0) list.splice(idx, 1)\n this.notify()\n }\n }\n\n // Returns a stable memoized snapshot of all extensions for useSyncExternalStore.\n // useSyncExternalStore compares snapshot references with Object.is, so returning\n // a new array on every call would cause infinite re-renders.\n // The cache is cleared on every notify() call so stale data is never served.\n getExtensionsSnapshot(): ExtensionInfo[] {\n if (this.extensionsSnapshot === null) {\n this.extensionsSnapshot = Array.from(this.extensions.values()).map(e => ({ ...e }))\n }\n return this.extensionsSnapshot\n }\n\n // Returns a memoized snapshot for useSyncExternalStore compatibility.\n // useSyncExternalStore compares snapshot references with Object.is, so\n // returning a new array on every call would cause infinite re-renders.\n // The cache is cleared on every notify() call so stale data is never served.\n getViewsForSlot(slot: string): ViewContribution[] {\n if (!this.viewSnapshots.has(slot)) {\n const sorted = (this.views.get(slot) ?? []).slice().sort((a, b) => a.order - b.order)\n this.viewSnapshots.set(slot, sorted)\n }\n return this.viewSnapshots.get(slot)!\n }\n\n removeViewsForExtension(extensionId: string): void {\n for (const [slot, contributions] of this.views) {\n this.views.set(\n slot,\n contributions.filter(c => c.extensionId !== extensionId)\n )\n }\n this.notify()\n }\n\n // Change notification\n\n onChange(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n private notify(): void {\n this.viewSnapshots.clear() // invalidate memoized snapshots\n this.extensionsSnapshot = null // invalidate extensions snapshot\n for (const listener of this.listeners) {\n listener()\n }\n }\n\n clear(): void {\n this.extensions.clear()\n this.views.clear()\n this.viewSnapshots.clear()\n this.extensionsSnapshot = null\n this.notify()\n }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport React, { Component, useSyncExternalStore, type ReactNode } from 'react'\nimport type { ExtensionRegistry } from './ExtensionRegistry'\n\n// Error boundary per contribution\ninterface SlotErrorBoundaryProps {\n extensionId: string\n children: ReactNode\n}\n\ninterface SlotErrorBoundaryState {\n hasError: boolean\n}\n\nclass SlotErrorBoundary extends Component<SlotErrorBoundaryProps, SlotErrorBoundaryState> {\n state: SlotErrorBoundaryState = { hasError: false }\n\n static getDerivedStateFromError(): SlotErrorBoundaryState {\n return { hasError: true }\n }\n\n componentDidCatch(error: Error) {\n console.error(`[ExtensionSlot] Extension \"${this.props.extensionId}\" threw:`, error)\n }\n\n render() {\n if (this.state.hasError) return null\n return this.props.children\n }\n}\n\n// ExtensionSlot component\nexport interface ExtensionSlotProps {\n name: string\n context: Record<string, unknown>\n registry: ExtensionRegistry\n activationEngine?: { triggerEvent(event: string): Promise<void> }\n className?: string\n}\n\nexport function ExtensionSlot({\n name,\n context,\n registry,\n activationEngine,\n className,\n}: ExtensionSlotProps) {\n // Trigger lazy activation for extensions listening to onView:<slot>\n React.useEffect(() => {\n activationEngine?.triggerEvent(`onView:${name}`)\n }, [activationEngine, name])\n\n const views = useSyncExternalStore(\n onStoreChange => registry.onChange(onStoreChange),\n () => registry.getViewsForSlot(name)\n )\n\n if (views.length === 0) return null\n\n return (\n <div className={className} data-extension-slot={name}>\n {views.map((view, i) => (\n <SlotErrorBoundary key={`${view.extensionId}-${i}`} extensionId={view.extensionId}>\n <view.component {...context} />\n </SlotErrorBoundary>\n ))}\n </div>\n )\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport React, { createContext, useContext } from 'react'\nimport type { ExtensionHost } from './types'\nimport type { ExtensionRegistry } from './ExtensionRegistry'\nimport type { ActivationEngine } from './ActivationEngine'\nimport type { McpToolRegistry } from './McpToolRegistry'\nimport type { ThemeRegistry } from './ThemeRegistry'\n\ninterface ExtensionHostContextValue {\n registry: ExtensionRegistry\n host: ExtensionHost | null\n engine: ActivationEngine | null\n}\n\nconst ExtensionHostCtx = createContext<ExtensionHostContextValue | null>(null)\nconst McpToolRegistryContext = createContext<McpToolRegistry | null>(null)\nconst ThemeRegistryContext = createContext<ThemeRegistry | null>(null)\n\nexport interface ExtensionHostProviderProps {\n registry: ExtensionRegistry\n host: ExtensionHost | null\n engine: ActivationEngine | null\n mcpToolRegistry?: McpToolRegistry | null\n themeRegistry?: ThemeRegistry | null\n children: React.ReactNode\n}\n\nexport function ExtensionHostProvider({\n registry,\n host,\n engine,\n mcpToolRegistry,\n themeRegistry,\n children,\n}: ExtensionHostProviderProps) {\n const value = React.useMemo(() => ({ registry, host, engine }), [registry, host, engine])\n return (\n <ThemeRegistryContext.Provider value={themeRegistry ?? null}>\n <McpToolRegistryContext.Provider value={mcpToolRegistry ?? null}>\n <ExtensionHostCtx.Provider value={value}>{children}</ExtensionHostCtx.Provider>\n </McpToolRegistryContext.Provider>\n </ThemeRegistryContext.Provider>\n )\n}\n\nfunction useExtensionHostContext(): ExtensionHostContextValue {\n const ctx = useContext(ExtensionHostCtx)\n if (!ctx) throw new Error('Extension hooks must be used within ExtensionHostProvider')\n return ctx\n}\n\nexport function useExtensionRegistry(): ExtensionRegistry {\n return useExtensionHostContext().registry\n}\n\nexport function useExtensionHost(): ExtensionHost | null {\n return useExtensionHostContext().host\n}\n\nexport function useActivationEngine(): ActivationEngine | null {\n return useExtensionHostContext().engine\n}\n\nexport function useMcpToolRegistry(): McpToolRegistry | null {\n const ctx = useContext(McpToolRegistryContext)\n return ctx\n}\n\nexport function useThemeRegistry(): ThemeRegistry | null {\n return useContext(ThemeRegistryContext)\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport { useSyncExternalStore } from 'react'\nimport type { ExtensionInfo, ExtensionState } from '../types'\nimport { useExtensionRegistry } from '../ExtensionHostContext'\n\nexport function useExtensions(): ExtensionInfo[] {\n const registry = useExtensionRegistry()\n return useSyncExternalStore(\n cb => registry.onChange(cb),\n () => registry.getExtensionsSnapshot()\n )\n}\n\nexport function useExtensionState(id: string): ExtensionState | undefined {\n const registry = useExtensionRegistry()\n return useSyncExternalStore(\n cb => registry.onChange(cb),\n () => registry.getState(id)\n )\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport { useEffect, useState } from 'react'\n\nexport interface MarketplaceSearchOptions {\n baseUrl: string\n query?: string\n category?: string\n type?: string\n limit?: number\n}\n\nexport interface MarketplaceListingResult {\n id: string\n label: string\n description: string\n category: string\n type: string\n tags: string[]\n iconUrl: string | null\n author: string | null\n downloadCount: number | null\n pricing: string | null\n priceUsdc: string | null\n}\n\nexport interface MarketplaceSearchResult {\n results: MarketplaceListingResult[]\n loading: boolean\n error: string | null\n}\n\nexport function useMarketplaceSearch(options: MarketplaceSearchOptions): MarketplaceSearchResult {\n const { baseUrl, query, category, type, limit } = options\n\n const hasSearchParams = Boolean(query || category || type)\n\n const [results, setResults] = useState<MarketplaceListingResult[]>([])\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n useEffect(() => {\n if (!hasSearchParams) {\n setResults([])\n setLoading(false)\n setError(null)\n return\n }\n\n setLoading(true)\n setError(null)\n\n const controller = new AbortController()\n\n const timer = setTimeout(async () => {\n const params = new URLSearchParams()\n if (query) params.set('q', query)\n if (category) params.set('category', category)\n if (type) params.set('type', type)\n params.set('limit', String(limit ?? 20))\n\n const url = `${baseUrl}/api/listings?${params.toString()}`\n\n try {\n const response = await fetch(url, { signal: controller.signal })\n\n if (!response.ok) {\n setError(`Request failed with status ${response.status}`)\n setResults([])\n setLoading(false)\n return\n }\n\n const data = await response.json()\n setResults(data.listings ?? [])\n setError(null)\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n // Fetch was aborted by cleanup — do not update state\n return\n }\n setError(err instanceof Error ? err.message : 'Unknown error')\n setResults([])\n } finally {\n // Only clear loading if the fetch was not aborted\n if (!controller.signal.aborted) {\n setLoading(false)\n }\n }\n }, 300)\n\n return () => {\n clearTimeout(timer)\n controller.abort()\n }\n }, [baseUrl, query, category, type, limit, hasSearchParams])\n\n return { results, loading, error }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport { useEffect, useState } from 'react'\nimport { useExtensionHost } from '../ExtensionHostContext'\nimport type { UpdateInfo } from '../types'\n\nexport function useExtensionUpdates(): { updates: UpdateInfo[]; loading: boolean } {\n const host = useExtensionHost()\n const [updates, setUpdates] = useState<UpdateInfo[]>([])\n const [loading, setLoading] = useState(false)\n\n useEffect(() => {\n if (!host) return\n let cancelled = false\n setLoading(true)\n host\n .checkForUpdates()\n .then(result => {\n if (!cancelled) {\n setUpdates(result)\n setLoading(false)\n }\n })\n .catch(() => {\n if (!cancelled) setLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [host])\n\n return { updates, loading }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport { useSyncExternalStore } from 'react'\nimport type { McpToolRegistry } from '../McpToolRegistry'\nimport type { RegisteredMcpTool } from '../types'\n\n/**\n * React hook that subscribes to the McpToolRegistry and returns\n * the current list of registered MCP tools. Re-renders when tools\n * are added or removed.\n */\nexport function useMcpTools(registry: McpToolRegistry): RegisteredMcpTool[] {\n return useSyncExternalStore(\n onStoreChange => registry.onChange(onStoreChange),\n () => registry.getToolsSnapshot()\n )\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport { useSyncExternalStore } from 'react'\nimport type { ThemeRegistry, RegisteredTheme } from '../ThemeRegistry'\n\nexport function useActiveTheme(registry: ThemeRegistry): RegisteredTheme | null {\n return useSyncExternalStore(\n onStoreChange => registry.onChange(onStoreChange),\n () => registry.getActiveThemeSnapshot()\n )\n}\n"],"mappings":"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","names":["React","jsx","useSyncExternalStore","useEffect","useState","useSyncExternalStore","useSyncExternalStore"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AuthService, NetworkService, EventBusService, PlatformEvent, ExtensionStorageAPI, ExtensionContext, ViewContribution } from '@epicdm/flowstate-app-framework/extensions';
|
|
2
|
+
|
|
3
|
+
interface MockUser {
|
|
4
|
+
id: string;
|
|
5
|
+
email: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
declare function createMockAuthService(user?: MockUser | null): AuthService;
|
|
9
|
+
declare function createMockNetworkService(fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>): NetworkService;
|
|
10
|
+
declare function createMockEventBusService(): EventBusService & {
|
|
11
|
+
emit: (event: PlatformEvent, ...args: unknown[]) => void;
|
|
12
|
+
};
|
|
13
|
+
declare function createMockStorage(): ExtensionStorageAPI;
|
|
14
|
+
|
|
15
|
+
interface MockContextOptions {
|
|
16
|
+
extensionId?: string;
|
|
17
|
+
extensionVersion?: string;
|
|
18
|
+
/** Pass null to simulate an unauthenticated session. */
|
|
19
|
+
user?: MockUser | null;
|
|
20
|
+
fetch?: (url: string, init?: RequestInit) => Promise<Response>;
|
|
21
|
+
permissions?: string[];
|
|
22
|
+
}
|
|
23
|
+
interface MockExtensionContext extends ExtensionContext {
|
|
24
|
+
/** Returns all views contributed to a named slot — use in test assertions. */
|
|
25
|
+
getContributedViews(slot: string): ViewContribution[];
|
|
26
|
+
/** Calls dispose() on every entry in subscriptions and clears the array. */
|
|
27
|
+
cleanup(): void;
|
|
28
|
+
}
|
|
29
|
+
declare function createMockContext(options?: MockContextOptions): MockExtensionContext;
|
|
30
|
+
|
|
31
|
+
export { type MockContextOptions, type MockExtensionContext, type MockUser, createMockAuthService, createMockContext, createMockEventBusService, createMockNetworkService, createMockStorage };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AuthService, NetworkService, EventBusService, PlatformEvent, ExtensionStorageAPI, ExtensionContext, ViewContribution } from '@epicdm/flowstate-app-framework/extensions';
|
|
2
|
+
|
|
3
|
+
interface MockUser {
|
|
4
|
+
id: string;
|
|
5
|
+
email: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
declare function createMockAuthService(user?: MockUser | null): AuthService;
|
|
9
|
+
declare function createMockNetworkService(fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>): NetworkService;
|
|
10
|
+
declare function createMockEventBusService(): EventBusService & {
|
|
11
|
+
emit: (event: PlatformEvent, ...args: unknown[]) => void;
|
|
12
|
+
};
|
|
13
|
+
declare function createMockStorage(): ExtensionStorageAPI;
|
|
14
|
+
|
|
15
|
+
interface MockContextOptions {
|
|
16
|
+
extensionId?: string;
|
|
17
|
+
extensionVersion?: string;
|
|
18
|
+
/** Pass null to simulate an unauthenticated session. */
|
|
19
|
+
user?: MockUser | null;
|
|
20
|
+
fetch?: (url: string, init?: RequestInit) => Promise<Response>;
|
|
21
|
+
permissions?: string[];
|
|
22
|
+
}
|
|
23
|
+
interface MockExtensionContext extends ExtensionContext {
|
|
24
|
+
/** Returns all views contributed to a named slot — use in test assertions. */
|
|
25
|
+
getContributedViews(slot: string): ViewContribution[];
|
|
26
|
+
/** Calls dispose() on every entry in subscriptions and clears the array. */
|
|
27
|
+
cleanup(): void;
|
|
28
|
+
}
|
|
29
|
+
declare function createMockContext(options?: MockContextOptions): MockExtensionContext;
|
|
30
|
+
|
|
31
|
+
export { type MockContextOptions, type MockExtensionContext, type MockUser, createMockAuthService, createMockContext, createMockEventBusService, createMockNetworkService, createMockStorage };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/testing/index.ts
|
|
21
|
+
var testing_exports = {};
|
|
22
|
+
__export(testing_exports, {
|
|
23
|
+
createMockAuthService: () => createMockAuthService,
|
|
24
|
+
createMockContext: () => createMockContext,
|
|
25
|
+
createMockEventBusService: () => createMockEventBusService,
|
|
26
|
+
createMockNetworkService: () => createMockNetworkService,
|
|
27
|
+
createMockStorage: () => createMockStorage
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(testing_exports);
|
|
30
|
+
|
|
31
|
+
// src/testing/mocks.ts
|
|
32
|
+
var DEFAULT_USER = {
|
|
33
|
+
id: "test-user-id",
|
|
34
|
+
email: "test@example.com",
|
|
35
|
+
name: "Test User"
|
|
36
|
+
};
|
|
37
|
+
function createMockAuthService(user) {
|
|
38
|
+
const resolvedUser = user === null ? null : user ?? DEFAULT_USER;
|
|
39
|
+
return {
|
|
40
|
+
getCurrentUser: () => resolvedUser,
|
|
41
|
+
getActiveOrgId: () => resolvedUser ? "test-org-id" : null,
|
|
42
|
+
getAccessToken: async () => resolvedUser ? "mock-access-token" : null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function createMockNetworkService(fetchImpl) {
|
|
46
|
+
const defaultFetch = async (_url, _init) => new Response(JSON.stringify({}), {
|
|
47
|
+
status: 200,
|
|
48
|
+
headers: { "Content-Type": "application/json" }
|
|
49
|
+
});
|
|
50
|
+
const resolved = fetchImpl ?? defaultFetch;
|
|
51
|
+
return {
|
|
52
|
+
// Always forward init explicitly so mock assertions on argument count are predictable
|
|
53
|
+
fetch: (url, init) => resolved(url, init)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function createMockEventBusService() {
|
|
57
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
58
|
+
return {
|
|
59
|
+
on(event, handler) {
|
|
60
|
+
if (!handlers.has(event)) handlers.set(event, /* @__PURE__ */ new Set());
|
|
61
|
+
handlers.get(event).add(handler);
|
|
62
|
+
return {
|
|
63
|
+
dispose: () => {
|
|
64
|
+
handlers.get(event)?.delete(handler);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
emit(event, ...args) {
|
|
69
|
+
handlers.get(event)?.forEach((h) => h(...args));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function createMockStorage() {
|
|
74
|
+
const store = /* @__PURE__ */ new Map();
|
|
75
|
+
return {
|
|
76
|
+
async get(key) {
|
|
77
|
+
const raw = store.get(key);
|
|
78
|
+
return raw ? JSON.parse(raw) : void 0;
|
|
79
|
+
},
|
|
80
|
+
async set(key, value) {
|
|
81
|
+
store.set(key, JSON.stringify(value));
|
|
82
|
+
},
|
|
83
|
+
async delete(key) {
|
|
84
|
+
store.delete(key);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/testing/createMockContext.ts
|
|
90
|
+
function createMockContext(options = {}) {
|
|
91
|
+
const {
|
|
92
|
+
extensionId = "test-extension",
|
|
93
|
+
extensionVersion = "1.0.0",
|
|
94
|
+
user,
|
|
95
|
+
fetch: fetchImpl
|
|
96
|
+
} = options;
|
|
97
|
+
const storage = createMockStorage();
|
|
98
|
+
const eventBus = createMockEventBusService();
|
|
99
|
+
const subscriptions = [];
|
|
100
|
+
const views = /* @__PURE__ */ new Map();
|
|
101
|
+
const ctx = {
|
|
102
|
+
extensionId,
|
|
103
|
+
extensionVersion,
|
|
104
|
+
storage,
|
|
105
|
+
services: {
|
|
106
|
+
auth: createMockAuthService(user),
|
|
107
|
+
network: createMockNetworkService(fetchImpl),
|
|
108
|
+
events: eventBus
|
|
109
|
+
},
|
|
110
|
+
subscriptions,
|
|
111
|
+
contributeView(slot, component) {
|
|
112
|
+
const contribution = {
|
|
113
|
+
extensionId,
|
|
114
|
+
slot,
|
|
115
|
+
component,
|
|
116
|
+
order: views.get(slot)?.length ?? 0
|
|
117
|
+
};
|
|
118
|
+
if (!views.has(slot)) views.set(slot, []);
|
|
119
|
+
views.get(slot).push(contribution);
|
|
120
|
+
const disposable = {
|
|
121
|
+
dispose: () => {
|
|
122
|
+
const list = views.get(slot);
|
|
123
|
+
if (list) {
|
|
124
|
+
const idx = list.indexOf(contribution);
|
|
125
|
+
if (idx >= 0) list.splice(idx, 1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
subscriptions.push(disposable);
|
|
130
|
+
return disposable;
|
|
131
|
+
},
|
|
132
|
+
contributeCommand(_id, _handler) {
|
|
133
|
+
return { dispose: () => {
|
|
134
|
+
} };
|
|
135
|
+
},
|
|
136
|
+
contributeSettingsTab(_config) {
|
|
137
|
+
return { dispose: () => {
|
|
138
|
+
} };
|
|
139
|
+
},
|
|
140
|
+
contributeStatusBarItem(_config) {
|
|
141
|
+
return { dispose: () => {
|
|
142
|
+
} };
|
|
143
|
+
},
|
|
144
|
+
registerSyncProvider(_factory) {
|
|
145
|
+
return { dispose: () => {
|
|
146
|
+
} };
|
|
147
|
+
},
|
|
148
|
+
registerMcpTool(_tool) {
|
|
149
|
+
return { dispose: () => {
|
|
150
|
+
} };
|
|
151
|
+
},
|
|
152
|
+
getContributedViews(slot) {
|
|
153
|
+
return views.get(slot) ?? [];
|
|
154
|
+
},
|
|
155
|
+
cleanup() {
|
|
156
|
+
for (const sub of subscriptions) {
|
|
157
|
+
sub.dispose();
|
|
158
|
+
}
|
|
159
|
+
subscriptions.length = 0;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
return ctx;
|
|
163
|
+
}
|
|
164
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
165
|
+
0 && (module.exports = {
|
|
166
|
+
createMockAuthService,
|
|
167
|
+
createMockContext,
|
|
168
|
+
createMockEventBusService,
|
|
169
|
+
createMockNetworkService,
|
|
170
|
+
createMockStorage
|
|
171
|
+
});
|
|
172
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/testing/index.ts","../../src/testing/mocks.ts","../../src/testing/createMockContext.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nexport {\n createMockContext,\n type MockContextOptions,\n type MockExtensionContext,\n} from './createMockContext'\nexport {\n createMockAuthService,\n createMockNetworkService,\n createMockEventBusService,\n createMockStorage,\n type MockUser,\n} from './mocks'\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport type {\n AuthService,\n NetworkService,\n EventBusService,\n ExtensionStorageAPI,\n Disposable,\n PlatformEvent,\n} from '../index'\n\nexport interface MockUser {\n id: string\n email: string\n name: string\n}\n\nconst DEFAULT_USER: MockUser = {\n id: 'test-user-id',\n email: 'test@example.com',\n name: 'Test User',\n}\n\nexport function createMockAuthService(user?: MockUser | null): AuthService {\n const resolvedUser = user === null ? null : (user ?? DEFAULT_USER)\n return {\n getCurrentUser: () => resolvedUser,\n getActiveOrgId: () => (resolvedUser ? 'test-org-id' : null),\n getAccessToken: async () => (resolvedUser ? 'mock-access-token' : null),\n }\n}\n\nexport function createMockNetworkService(\n fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>\n): NetworkService {\n const defaultFetch = async (_url: string, _init?: RequestInit) =>\n new Response(JSON.stringify({}), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n const resolved = fetchImpl ?? defaultFetch\n return {\n // Always forward init explicitly so mock assertions on argument count are predictable\n fetch: (url: string, init?: RequestInit) => resolved(url, init),\n }\n}\n\nexport function createMockEventBusService(): EventBusService & {\n emit: (event: PlatformEvent, ...args: unknown[]) => void\n} {\n const handlers = new Map<string, Set<(...args: unknown[]) => void>>()\n\n return {\n on(event: PlatformEvent, handler: (...args: unknown[]) => void): Disposable {\n if (!handlers.has(event)) handlers.set(event, new Set())\n // Non-null assertion is safe: we just set the key above\n handlers.get(event)!.add(handler)\n return {\n dispose: () => {\n handlers.get(event)?.delete(handler)\n },\n }\n },\n emit(event: PlatformEvent, ...args: unknown[]) {\n handlers.get(event)?.forEach(h => h(...args))\n },\n }\n}\n\nexport function createMockStorage(): ExtensionStorageAPI {\n const store = new Map<string, string>()\n return {\n async get<T>(key: string): Promise<T | undefined> {\n const raw = store.get(key)\n return raw ? (JSON.parse(raw) as T) : undefined\n },\n async set<T>(key: string, value: T): Promise<void> {\n store.set(key, JSON.stringify(value))\n },\n async delete(key: string): Promise<void> {\n store.delete(key)\n },\n }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport type { ComponentType } from 'react'\nimport type {\n ExtensionContext,\n Disposable,\n ViewContribution,\n CommandHandler,\n SettingsTabConfig,\n StatusBarConfig,\n SyncProviderFactory,\n McpToolDefinition,\n} from '../index'\nimport {\n createMockAuthService,\n createMockNetworkService,\n createMockEventBusService,\n createMockStorage,\n type MockUser,\n} from './mocks'\n\nexport interface MockContextOptions {\n extensionId?: string\n extensionVersion?: string\n /** Pass null to simulate an unauthenticated session. */\n user?: MockUser | null\n fetch?: (url: string, init?: RequestInit) => Promise<Response>\n permissions?: string[]\n}\n\nexport interface MockExtensionContext extends ExtensionContext {\n /** Returns all views contributed to a named slot — use in test assertions. */\n getContributedViews(slot: string): ViewContribution[]\n /** Calls dispose() on every entry in subscriptions and clears the array. */\n cleanup(): void\n}\n\nexport function createMockContext(options: MockContextOptions = {}): MockExtensionContext {\n const {\n extensionId = 'test-extension',\n extensionVersion = '1.0.0',\n user,\n fetch: fetchImpl,\n } = options\n\n const storage = createMockStorage()\n const eventBus = createMockEventBusService()\n const subscriptions: Disposable[] = []\n const views = new Map<string, ViewContribution[]>()\n\n const ctx: MockExtensionContext = {\n extensionId,\n extensionVersion,\n storage,\n services: {\n auth: createMockAuthService(user),\n network: createMockNetworkService(fetchImpl),\n events: eventBus,\n },\n subscriptions,\n\n contributeView(slot: string, component: ComponentType<unknown>): Disposable {\n const contribution: ViewContribution = {\n extensionId,\n slot,\n component,\n order: views.get(slot)?.length ?? 0,\n }\n if (!views.has(slot)) views.set(slot, [])\n views.get(slot)!.push(contribution)\n const disposable: Disposable = {\n dispose: () => {\n const list = views.get(slot)\n if (list) {\n const idx = list.indexOf(contribution)\n if (idx >= 0) list.splice(idx, 1)\n }\n },\n }\n subscriptions.push(disposable)\n return disposable\n },\n\n contributeCommand(_id: string, _handler: CommandHandler): Disposable {\n return { dispose: () => {} }\n },\n\n contributeSettingsTab(_config: SettingsTabConfig): Disposable {\n return { dispose: () => {} }\n },\n\n contributeStatusBarItem(_config: StatusBarConfig): Disposable {\n return { dispose: () => {} }\n },\n\n registerSyncProvider(_factory: SyncProviderFactory): Disposable {\n return { dispose: () => {} }\n },\n\n registerMcpTool(_tool: McpToolDefinition): Disposable {\n return { dispose: () => {} }\n },\n\n getContributedViews(slot: string): ViewContribution[] {\n return views.get(slot) ?? []\n },\n\n cleanup() {\n for (const sub of subscriptions) {\n sub.dispose()\n }\n // Clear in place so any reference held by the caller sees the empty array\n subscriptions.length = 0\n },\n }\n\n return ctx\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/testing/mocks.ts
|
|
2
|
+
var DEFAULT_USER = {
|
|
3
|
+
id: "test-user-id",
|
|
4
|
+
email: "test@example.com",
|
|
5
|
+
name: "Test User"
|
|
6
|
+
};
|
|
7
|
+
function createMockAuthService(user) {
|
|
8
|
+
const resolvedUser = user === null ? null : user ?? DEFAULT_USER;
|
|
9
|
+
return {
|
|
10
|
+
getCurrentUser: () => resolvedUser,
|
|
11
|
+
getActiveOrgId: () => resolvedUser ? "test-org-id" : null,
|
|
12
|
+
getAccessToken: async () => resolvedUser ? "mock-access-token" : null
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function createMockNetworkService(fetchImpl) {
|
|
16
|
+
const defaultFetch = async (_url, _init) => new Response(JSON.stringify({}), {
|
|
17
|
+
status: 200,
|
|
18
|
+
headers: { "Content-Type": "application/json" }
|
|
19
|
+
});
|
|
20
|
+
const resolved = fetchImpl ?? defaultFetch;
|
|
21
|
+
return {
|
|
22
|
+
// Always forward init explicitly so mock assertions on argument count are predictable
|
|
23
|
+
fetch: (url, init) => resolved(url, init)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function createMockEventBusService() {
|
|
27
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
28
|
+
return {
|
|
29
|
+
on(event, handler) {
|
|
30
|
+
if (!handlers.has(event)) handlers.set(event, /* @__PURE__ */ new Set());
|
|
31
|
+
handlers.get(event).add(handler);
|
|
32
|
+
return {
|
|
33
|
+
dispose: () => {
|
|
34
|
+
handlers.get(event)?.delete(handler);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
emit(event, ...args) {
|
|
39
|
+
handlers.get(event)?.forEach((h) => h(...args));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function createMockStorage() {
|
|
44
|
+
const store = /* @__PURE__ */ new Map();
|
|
45
|
+
return {
|
|
46
|
+
async get(key) {
|
|
47
|
+
const raw = store.get(key);
|
|
48
|
+
return raw ? JSON.parse(raw) : void 0;
|
|
49
|
+
},
|
|
50
|
+
async set(key, value) {
|
|
51
|
+
store.set(key, JSON.stringify(value));
|
|
52
|
+
},
|
|
53
|
+
async delete(key) {
|
|
54
|
+
store.delete(key);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/testing/createMockContext.ts
|
|
60
|
+
function createMockContext(options = {}) {
|
|
61
|
+
const {
|
|
62
|
+
extensionId = "test-extension",
|
|
63
|
+
extensionVersion = "1.0.0",
|
|
64
|
+
user,
|
|
65
|
+
fetch: fetchImpl
|
|
66
|
+
} = options;
|
|
67
|
+
const storage = createMockStorage();
|
|
68
|
+
const eventBus = createMockEventBusService();
|
|
69
|
+
const subscriptions = [];
|
|
70
|
+
const views = /* @__PURE__ */ new Map();
|
|
71
|
+
const ctx = {
|
|
72
|
+
extensionId,
|
|
73
|
+
extensionVersion,
|
|
74
|
+
storage,
|
|
75
|
+
services: {
|
|
76
|
+
auth: createMockAuthService(user),
|
|
77
|
+
network: createMockNetworkService(fetchImpl),
|
|
78
|
+
events: eventBus
|
|
79
|
+
},
|
|
80
|
+
subscriptions,
|
|
81
|
+
contributeView(slot, component) {
|
|
82
|
+
const contribution = {
|
|
83
|
+
extensionId,
|
|
84
|
+
slot,
|
|
85
|
+
component,
|
|
86
|
+
order: views.get(slot)?.length ?? 0
|
|
87
|
+
};
|
|
88
|
+
if (!views.has(slot)) views.set(slot, []);
|
|
89
|
+
views.get(slot).push(contribution);
|
|
90
|
+
const disposable = {
|
|
91
|
+
dispose: () => {
|
|
92
|
+
const list = views.get(slot);
|
|
93
|
+
if (list) {
|
|
94
|
+
const idx = list.indexOf(contribution);
|
|
95
|
+
if (idx >= 0) list.splice(idx, 1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
subscriptions.push(disposable);
|
|
100
|
+
return disposable;
|
|
101
|
+
},
|
|
102
|
+
contributeCommand(_id, _handler) {
|
|
103
|
+
return { dispose: () => {
|
|
104
|
+
} };
|
|
105
|
+
},
|
|
106
|
+
contributeSettingsTab(_config) {
|
|
107
|
+
return { dispose: () => {
|
|
108
|
+
} };
|
|
109
|
+
},
|
|
110
|
+
contributeStatusBarItem(_config) {
|
|
111
|
+
return { dispose: () => {
|
|
112
|
+
} };
|
|
113
|
+
},
|
|
114
|
+
registerSyncProvider(_factory) {
|
|
115
|
+
return { dispose: () => {
|
|
116
|
+
} };
|
|
117
|
+
},
|
|
118
|
+
registerMcpTool(_tool) {
|
|
119
|
+
return { dispose: () => {
|
|
120
|
+
} };
|
|
121
|
+
},
|
|
122
|
+
getContributedViews(slot) {
|
|
123
|
+
return views.get(slot) ?? [];
|
|
124
|
+
},
|
|
125
|
+
cleanup() {
|
|
126
|
+
for (const sub of subscriptions) {
|
|
127
|
+
sub.dispose();
|
|
128
|
+
}
|
|
129
|
+
subscriptions.length = 0;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
return ctx;
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
createMockAuthService,
|
|
136
|
+
createMockContext,
|
|
137
|
+
createMockEventBusService,
|
|
138
|
+
createMockNetworkService,
|
|
139
|
+
createMockStorage
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/testing/mocks.ts","../../src/testing/createMockContext.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport type {\n AuthService,\n NetworkService,\n EventBusService,\n ExtensionStorageAPI,\n Disposable,\n PlatformEvent,\n} from '../index'\n\nexport interface MockUser {\n id: string\n email: string\n name: string\n}\n\nconst DEFAULT_USER: MockUser = {\n id: 'test-user-id',\n email: 'test@example.com',\n name: 'Test User',\n}\n\nexport function createMockAuthService(user?: MockUser | null): AuthService {\n const resolvedUser = user === null ? null : (user ?? DEFAULT_USER)\n return {\n getCurrentUser: () => resolvedUser,\n getActiveOrgId: () => (resolvedUser ? 'test-org-id' : null),\n getAccessToken: async () => (resolvedUser ? 'mock-access-token' : null),\n }\n}\n\nexport function createMockNetworkService(\n fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>\n): NetworkService {\n const defaultFetch = async (_url: string, _init?: RequestInit) =>\n new Response(JSON.stringify({}), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n const resolved = fetchImpl ?? defaultFetch\n return {\n // Always forward init explicitly so mock assertions on argument count are predictable\n fetch: (url: string, init?: RequestInit) => resolved(url, init),\n }\n}\n\nexport function createMockEventBusService(): EventBusService & {\n emit: (event: PlatformEvent, ...args: unknown[]) => void\n} {\n const handlers = new Map<string, Set<(...args: unknown[]) => void>>()\n\n return {\n on(event: PlatformEvent, handler: (...args: unknown[]) => void): Disposable {\n if (!handlers.has(event)) handlers.set(event, new Set())\n // Non-null assertion is safe: we just set the key above\n handlers.get(event)!.add(handler)\n return {\n dispose: () => {\n handlers.get(event)?.delete(handler)\n },\n }\n },\n emit(event: PlatformEvent, ...args: unknown[]) {\n handlers.get(event)?.forEach(h => h(...args))\n },\n }\n}\n\nexport function createMockStorage(): ExtensionStorageAPI {\n const store = new Map<string, string>()\n return {\n async get<T>(key: string): Promise<T | undefined> {\n const raw = store.get(key)\n return raw ? (JSON.parse(raw) as T) : undefined\n },\n async set<T>(key: string, value: T): Promise<void> {\n store.set(key, JSON.stringify(value))\n },\n async delete(key: string): Promise<void> {\n store.delete(key)\n },\n }\n}\n","// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Epic Digital Interactive Media LLC\n\nimport type { ComponentType } from 'react'\nimport type {\n ExtensionContext,\n Disposable,\n ViewContribution,\n CommandHandler,\n SettingsTabConfig,\n StatusBarConfig,\n SyncProviderFactory,\n McpToolDefinition,\n} from '../index'\nimport {\n createMockAuthService,\n createMockNetworkService,\n createMockEventBusService,\n createMockStorage,\n type MockUser,\n} from './mocks'\n\nexport interface MockContextOptions {\n extensionId?: string\n extensionVersion?: string\n /** Pass null to simulate an unauthenticated session. */\n user?: MockUser | null\n fetch?: (url: string, init?: RequestInit) => Promise<Response>\n permissions?: string[]\n}\n\nexport interface MockExtensionContext extends ExtensionContext {\n /** Returns all views contributed to a named slot — use in test assertions. */\n getContributedViews(slot: string): ViewContribution[]\n /** Calls dispose() on every entry in subscriptions and clears the array. */\n cleanup(): void\n}\n\nexport function createMockContext(options: MockContextOptions = {}): MockExtensionContext {\n const {\n extensionId = 'test-extension',\n extensionVersion = '1.0.0',\n user,\n fetch: fetchImpl,\n } = options\n\n const storage = createMockStorage()\n const eventBus = createMockEventBusService()\n const subscriptions: Disposable[] = []\n const views = new Map<string, ViewContribution[]>()\n\n const ctx: MockExtensionContext = {\n extensionId,\n extensionVersion,\n storage,\n services: {\n auth: createMockAuthService(user),\n network: createMockNetworkService(fetchImpl),\n events: eventBus,\n },\n subscriptions,\n\n contributeView(slot: string, component: ComponentType<unknown>): Disposable {\n const contribution: ViewContribution = {\n extensionId,\n slot,\n component,\n order: views.get(slot)?.length ?? 0,\n }\n if (!views.has(slot)) views.set(slot, [])\n views.get(slot)!.push(contribution)\n const disposable: Disposable = {\n dispose: () => {\n const list = views.get(slot)\n if (list) {\n const idx = list.indexOf(contribution)\n if (idx >= 0) list.splice(idx, 1)\n }\n },\n }\n subscriptions.push(disposable)\n return disposable\n },\n\n contributeCommand(_id: string, _handler: CommandHandler): Disposable {\n return { dispose: () => {} }\n },\n\n contributeSettingsTab(_config: SettingsTabConfig): Disposable {\n return { dispose: () => {} }\n },\n\n contributeStatusBarItem(_config: StatusBarConfig): Disposable {\n return { dispose: () => {} }\n },\n\n registerSyncProvider(_factory: SyncProviderFactory): Disposable {\n return { dispose: () => {} }\n },\n\n registerMcpTool(_tool: McpToolDefinition): Disposable {\n return { dispose: () => {} }\n },\n\n getContributedViews(slot: string): ViewContribution[] {\n return views.get(slot) ?? []\n },\n\n cleanup() {\n for (const sub of subscriptions) {\n sub.dispose()\n }\n // Clear in place so any reference held by the caller sees the empty array\n subscriptions.length = 0\n },\n }\n\n return ctx\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@epicdm/flowstate-extension-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "FlowState Extension SDK — types and testing utilities for extension developers",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./testing": {
|
|
15
|
+
"types": "./dist/testing/index.d.ts",
|
|
16
|
+
"import": "./dist/testing/index.mjs",
|
|
17
|
+
"require": "./dist/testing/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "tsup --watch",
|
|
23
|
+
"test": "jest",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@epicdm/flowstate-app-framework": "workspace:*"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/jest": "^30.0.0",
|
|
34
|
+
"@types/react": "^19.0.0",
|
|
35
|
+
"dts-bundle-generator": "^9.5.1",
|
|
36
|
+
"jest": "^30.0.0",
|
|
37
|
+
"react": "19.2.0",
|
|
38
|
+
"ts-jest": "^29.0.0",
|
|
39
|
+
"tsup": "^8.3.0",
|
|
40
|
+
"typescript": "^5.5.0"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"author": "Epic Digital Interactive Media LLC",
|
|
46
|
+
"license": "Apache-2.0",
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/epicdm/epic-flowstate.git",
|
|
53
|
+
"directory": "packages/extension-sdk"
|
|
54
|
+
}
|
|
55
|
+
}
|