@aprovan/patchwork 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 (225) hide show
  1. package/.eslintrc.json +22 -0
  2. package/.github/workflows/publish.yml +41 -0
  3. package/.prettierignore +17 -0
  4. package/LICENSE +373 -0
  5. package/README.md +15 -0
  6. package/apps/chat/.utcp_config.json +14 -0
  7. package/apps/chat/.working/widgets/27060b91-a2a5-4272-b243-6eb904bd4070/main.tsx +107 -0
  8. package/apps/chat/index.html +17 -0
  9. package/apps/chat/node_modules/.bin/autoprefixer +17 -0
  10. package/apps/chat/node_modules/.bin/browserslist +17 -0
  11. package/apps/chat/node_modules/.bin/conc +17 -0
  12. package/apps/chat/node_modules/.bin/concurrently +17 -0
  13. package/apps/chat/node_modules/.bin/copilot-proxy +17 -0
  14. package/apps/chat/node_modules/.bin/jiti +17 -0
  15. package/apps/chat/node_modules/.bin/tailwind +17 -0
  16. package/apps/chat/node_modules/.bin/tailwindcss +17 -0
  17. package/apps/chat/node_modules/.bin/tsc +17 -0
  18. package/apps/chat/node_modules/.bin/tsserver +17 -0
  19. package/apps/chat/node_modules/.bin/tsx +17 -0
  20. package/apps/chat/node_modules/.bin/vite +17 -0
  21. package/apps/chat/package.json +55 -0
  22. package/apps/chat/postcss.config.js +6 -0
  23. package/apps/chat/src/App.tsx +7 -0
  24. package/apps/chat/src/components/ui/avatar.tsx +48 -0
  25. package/apps/chat/src/components/ui/badge.tsx +36 -0
  26. package/apps/chat/src/components/ui/button.tsx +56 -0
  27. package/apps/chat/src/components/ui/card.tsx +86 -0
  28. package/apps/chat/src/components/ui/collapsible.tsx +9 -0
  29. package/apps/chat/src/components/ui/dialog.tsx +60 -0
  30. package/apps/chat/src/components/ui/input.tsx +25 -0
  31. package/apps/chat/src/components/ui/scroll-area.tsx +46 -0
  32. package/apps/chat/src/index.css +190 -0
  33. package/apps/chat/src/lib/utils.ts +6 -0
  34. package/apps/chat/src/main.tsx +10 -0
  35. package/apps/chat/src/pages/ChatPage.tsx +460 -0
  36. package/apps/chat/tailwind.config.js +71 -0
  37. package/apps/chat/tsconfig.json +25 -0
  38. package/apps/chat/vite.config.ts +26 -0
  39. package/package.json +35 -0
  40. package/packages/bobbin/node_modules/.bin/esbuild +14 -0
  41. package/packages/bobbin/node_modules/.bin/jiti +17 -0
  42. package/packages/bobbin/node_modules/.bin/tsc +17 -0
  43. package/packages/bobbin/node_modules/.bin/tsserver +17 -0
  44. package/packages/bobbin/node_modules/.bin/tsup +17 -0
  45. package/packages/bobbin/node_modules/.bin/tsup-node +17 -0
  46. package/packages/bobbin/node_modules/.bin/tsx +17 -0
  47. package/packages/bobbin/package.json +30 -0
  48. package/packages/bobbin/src/Bobbin.tsx +89 -0
  49. package/packages/bobbin/src/components/EditPanel/EditPanel.tsx +376 -0
  50. package/packages/bobbin/src/components/EditPanel/controls/ColorPicker.tsx +138 -0
  51. package/packages/bobbin/src/components/EditPanel/controls/QuickSelectDropdown.tsx +142 -0
  52. package/packages/bobbin/src/components/EditPanel/controls/SliderInput.tsx +94 -0
  53. package/packages/bobbin/src/components/EditPanel/controls/SpacingControl.tsx +285 -0
  54. package/packages/bobbin/src/components/EditPanel/controls/ToggleGroup.tsx +37 -0
  55. package/packages/bobbin/src/components/EditPanel/controls/TokenDropdown.tsx +33 -0
  56. package/packages/bobbin/src/components/EditPanel/sections/AnnotationSection.tsx +136 -0
  57. package/packages/bobbin/src/components/EditPanel/sections/BackgroundSection.tsx +79 -0
  58. package/packages/bobbin/src/components/EditPanel/sections/EffectsSection.tsx +85 -0
  59. package/packages/bobbin/src/components/EditPanel/sections/LayoutSection.tsx +224 -0
  60. package/packages/bobbin/src/components/EditPanel/sections/SectionWrapper.tsx +57 -0
  61. package/packages/bobbin/src/components/EditPanel/sections/SizeSection.tsx +166 -0
  62. package/packages/bobbin/src/components/EditPanel/sections/SpacingSection.tsx +69 -0
  63. package/packages/bobbin/src/components/EditPanel/sections/TypographySection.tsx +148 -0
  64. package/packages/bobbin/src/components/Inspector/Inspector.tsx +221 -0
  65. package/packages/bobbin/src/components/Overlay/ControlHandles.tsx +572 -0
  66. package/packages/bobbin/src/components/Overlay/MarginPaddingOverlay.tsx +229 -0
  67. package/packages/bobbin/src/components/Overlay/SelectionOverlay.tsx +73 -0
  68. package/packages/bobbin/src/components/Pill/Pill.tsx +155 -0
  69. package/packages/bobbin/src/components/ThemeToggle/ThemeToggle.tsx +72 -0
  70. package/packages/bobbin/src/core/changeSerializer.ts +139 -0
  71. package/packages/bobbin/src/core/useBobbin.ts +399 -0
  72. package/packages/bobbin/src/core/useChangeTracker.ts +186 -0
  73. package/packages/bobbin/src/core/useClipboard.ts +21 -0
  74. package/packages/bobbin/src/core/useElementSelection.ts +146 -0
  75. package/packages/bobbin/src/index.ts +46 -0
  76. package/packages/bobbin/src/tokens/borders.ts +19 -0
  77. package/packages/bobbin/src/tokens/colors.ts +150 -0
  78. package/packages/bobbin/src/tokens/index.ts +37 -0
  79. package/packages/bobbin/src/tokens/shadows.ts +10 -0
  80. package/packages/bobbin/src/tokens/spacing.ts +37 -0
  81. package/packages/bobbin/src/tokens/typography.ts +51 -0
  82. package/packages/bobbin/src/types.ts +157 -0
  83. package/packages/bobbin/src/utils/animation.ts +40 -0
  84. package/packages/bobbin/src/utils/dom.ts +36 -0
  85. package/packages/bobbin/src/utils/selectors.ts +76 -0
  86. package/packages/bobbin/tsconfig.json +10 -0
  87. package/packages/bobbin/tsup.config.ts +10 -0
  88. package/packages/compiler/node_modules/.bin/esbuild +17 -0
  89. package/packages/compiler/node_modules/.bin/jiti +17 -0
  90. package/packages/compiler/node_modules/.bin/tsc +17 -0
  91. package/packages/compiler/node_modules/.bin/tsserver +17 -0
  92. package/packages/compiler/node_modules/.bin/tsup +17 -0
  93. package/packages/compiler/node_modules/.bin/tsup-node +17 -0
  94. package/packages/compiler/node_modules/.bin/tsx +17 -0
  95. package/packages/compiler/package.json +38 -0
  96. package/packages/compiler/src/compiler.ts +258 -0
  97. package/packages/compiler/src/images/index.ts +13 -0
  98. package/packages/compiler/src/images/loader.ts +234 -0
  99. package/packages/compiler/src/images/registry.ts +112 -0
  100. package/packages/compiler/src/index.ts +141 -0
  101. package/packages/compiler/src/mount/bridge.ts +399 -0
  102. package/packages/compiler/src/mount/embedded.ts +306 -0
  103. package/packages/compiler/src/mount/iframe.ts +433 -0
  104. package/packages/compiler/src/mount/index.ts +18 -0
  105. package/packages/compiler/src/schemas.ts +169 -0
  106. package/packages/compiler/src/transforms/cdn.ts +411 -0
  107. package/packages/compiler/src/transforms/index.ts +4 -0
  108. package/packages/compiler/src/transforms/vfs.ts +138 -0
  109. package/packages/compiler/src/types.ts +233 -0
  110. package/packages/compiler/src/vfs/backends/indexeddb.ts +66 -0
  111. package/packages/compiler/src/vfs/backends/local-fs.ts +41 -0
  112. package/packages/compiler/src/vfs/backends/s3.ts +60 -0
  113. package/packages/compiler/src/vfs/index.ts +11 -0
  114. package/packages/compiler/src/vfs/project.ts +56 -0
  115. package/packages/compiler/src/vfs/store.ts +53 -0
  116. package/packages/compiler/src/vfs/types.ts +20 -0
  117. package/packages/compiler/tsconfig.json +8 -0
  118. package/packages/compiler/tsup.config.ts +14 -0
  119. package/packages/editor/node_modules/.bin/jiti +17 -0
  120. package/packages/editor/node_modules/.bin/tsc +17 -0
  121. package/packages/editor/node_modules/.bin/tsserver +17 -0
  122. package/packages/editor/node_modules/.bin/tsup +17 -0
  123. package/packages/editor/node_modules/.bin/tsup-node +17 -0
  124. package/packages/editor/node_modules/.bin/tsx +17 -0
  125. package/packages/editor/package.json +45 -0
  126. package/packages/editor/src/components/CodeBlockExtension.tsx +190 -0
  127. package/packages/editor/src/components/CodePreview.tsx +344 -0
  128. package/packages/editor/src/components/MarkdownEditor.tsx +270 -0
  129. package/packages/editor/src/components/ServicesInspector.tsx +118 -0
  130. package/packages/editor/src/components/edit/EditHistory.tsx +89 -0
  131. package/packages/editor/src/components/edit/EditModal.tsx +236 -0
  132. package/packages/editor/src/components/edit/FileTree.tsx +144 -0
  133. package/packages/editor/src/components/edit/api.ts +100 -0
  134. package/packages/editor/src/components/edit/index.ts +6 -0
  135. package/packages/editor/src/components/edit/types.ts +53 -0
  136. package/packages/editor/src/components/edit/useEditSession.ts +164 -0
  137. package/packages/editor/src/components/index.ts +5 -0
  138. package/packages/editor/src/index.ts +72 -0
  139. package/packages/editor/src/lib/code-extractor.ts +210 -0
  140. package/packages/editor/src/lib/diff.ts +308 -0
  141. package/packages/editor/src/lib/index.ts +4 -0
  142. package/packages/editor/src/lib/utils.ts +6 -0
  143. package/packages/editor/src/lib/vfs.ts +106 -0
  144. package/packages/editor/tsconfig.json +10 -0
  145. package/packages/editor/tsup.config.ts +10 -0
  146. package/packages/images/ink/node_modules/.bin/jiti +17 -0
  147. package/packages/images/ink/node_modules/.bin/tsc +17 -0
  148. package/packages/images/ink/node_modules/.bin/tsserver +17 -0
  149. package/packages/images/ink/node_modules/.bin/tsup +17 -0
  150. package/packages/images/ink/node_modules/.bin/tsup-node +17 -0
  151. package/packages/images/ink/node_modules/.bin/tsx +17 -0
  152. package/packages/images/ink/package.json +53 -0
  153. package/packages/images/ink/src/index.ts +48 -0
  154. package/packages/images/ink/src/runner.ts +331 -0
  155. package/packages/images/ink/src/setup.ts +123 -0
  156. package/packages/images/ink/tsconfig.json +10 -0
  157. package/packages/images/ink/tsup.config.ts +11 -0
  158. package/packages/images/shadcn/node_modules/.bin/jiti +17 -0
  159. package/packages/images/shadcn/node_modules/.bin/tsc +17 -0
  160. package/packages/images/shadcn/node_modules/.bin/tsserver +17 -0
  161. package/packages/images/shadcn/node_modules/.bin/tsup +17 -0
  162. package/packages/images/shadcn/node_modules/.bin/tsup-node +17 -0
  163. package/packages/images/shadcn/node_modules/.bin/tsx +17 -0
  164. package/packages/images/shadcn/package.json +82 -0
  165. package/packages/images/shadcn/src/html.ts +341 -0
  166. package/packages/images/shadcn/src/index.ts +37 -0
  167. package/packages/images/shadcn/src/setup.ts +287 -0
  168. package/packages/images/shadcn/tsconfig.json +9 -0
  169. package/packages/images/shadcn/tsup.config.ts +13 -0
  170. package/packages/images/vanilla/node_modules/.bin/jiti +17 -0
  171. package/packages/images/vanilla/node_modules/.bin/tsc +17 -0
  172. package/packages/images/vanilla/node_modules/.bin/tsserver +17 -0
  173. package/packages/images/vanilla/node_modules/.bin/tsup +17 -0
  174. package/packages/images/vanilla/node_modules/.bin/tsup-node +17 -0
  175. package/packages/images/vanilla/node_modules/.bin/tsx +17 -0
  176. package/packages/images/vanilla/package.json +35 -0
  177. package/packages/images/vanilla/src/index.ts +7 -0
  178. package/packages/images/vanilla/src/setup.ts +6 -0
  179. package/packages/images/vanilla/tsconfig.json +9 -0
  180. package/packages/images/vanilla/tsup.config.ts +10 -0
  181. package/packages/patchwork/node_modules/.bin/jiti +17 -0
  182. package/packages/patchwork/node_modules/.bin/tsc +17 -0
  183. package/packages/patchwork/node_modules/.bin/tsserver +17 -0
  184. package/packages/patchwork/node_modules/.bin/tsup +17 -0
  185. package/packages/patchwork/node_modules/.bin/tsup-node +17 -0
  186. package/packages/patchwork/node_modules/.bin/tsx +17 -0
  187. package/packages/patchwork/package.json +27 -0
  188. package/packages/patchwork/src/index.ts +15 -0
  189. package/packages/patchwork/src/services/index.ts +11 -0
  190. package/packages/patchwork/src/services/proxy.ts +213 -0
  191. package/packages/patchwork/src/services/types.ts +28 -0
  192. package/packages/patchwork/src/types.ts +116 -0
  193. package/packages/patchwork/tsconfig.json +8 -0
  194. package/packages/patchwork/tsup.config.ts +14 -0
  195. package/packages/stitchery/node_modules/.bin/jiti +17 -0
  196. package/packages/stitchery/node_modules/.bin/tsc +17 -0
  197. package/packages/stitchery/node_modules/.bin/tsserver +17 -0
  198. package/packages/stitchery/node_modules/.bin/tsup +17 -0
  199. package/packages/stitchery/node_modules/.bin/tsup-node +17 -0
  200. package/packages/stitchery/node_modules/.bin/tsx +17 -0
  201. package/packages/stitchery/package.json +40 -0
  202. package/packages/stitchery/src/cli.ts +116 -0
  203. package/packages/stitchery/src/index.ts +16 -0
  204. package/packages/stitchery/src/prompts.ts +326 -0
  205. package/packages/stitchery/src/server/index.ts +365 -0
  206. package/packages/stitchery/src/server/local-packages.ts +91 -0
  207. package/packages/stitchery/src/server/routes.ts +122 -0
  208. package/packages/stitchery/src/server/services.ts +382 -0
  209. package/packages/stitchery/src/server/vfs-routes.ts +142 -0
  210. package/packages/stitchery/src/types.ts +59 -0
  211. package/packages/stitchery/tsconfig.json +13 -0
  212. package/packages/stitchery/tsup.config.ts +15 -0
  213. package/packages/utcp/node_modules/.bin/jiti +17 -0
  214. package/packages/utcp/node_modules/.bin/tsc +17 -0
  215. package/packages/utcp/node_modules/.bin/tsserver +17 -0
  216. package/packages/utcp/node_modules/.bin/tsup +17 -0
  217. package/packages/utcp/node_modules/.bin/tsup-node +17 -0
  218. package/packages/utcp/node_modules/.bin/tsx +17 -0
  219. package/packages/utcp/package.json +38 -0
  220. package/packages/utcp/src/index.ts +153 -0
  221. package/packages/utcp/tsconfig.json +8 -0
  222. package/packages/utcp/tsup.config.ts +12 -0
  223. package/pnpm-workspace.yaml +3 -0
  224. package/tsconfig.json +18 -0
  225. package/turbo.json +23 -0
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Service bridge - handles communication between widgets and service proxy
3
+ */
4
+
5
+ import type {
6
+ BridgeMessage,
7
+ ServiceCallPayload,
8
+ ServiceResultPayload,
9
+ ServiceProxy,
10
+ } from '../types.js';
11
+
12
+ /**
13
+ * Generate a unique message ID
14
+ */
15
+ function generateMessageId(): string {
16
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
17
+ }
18
+
19
+ /**
20
+ * Create a service proxy that calls the backend via HTTP
21
+ */
22
+ export function createHttpServiceProxy(proxyUrl: string): ServiceProxy {
23
+ return {
24
+ async call(
25
+ namespace: string,
26
+ procedure: string,
27
+ args: unknown[],
28
+ ): Promise<unknown> {
29
+ const url = `${proxyUrl}/${namespace}/${procedure}`;
30
+ const response = await fetch(url, {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify({ args: args[0] ?? {} }),
34
+ });
35
+
36
+ if (!response.ok) {
37
+ throw new Error(
38
+ `Service call failed: ${response.status} ${response.statusText}`,
39
+ );
40
+ }
41
+
42
+ const result = await response.json();
43
+ return result;
44
+ },
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Creates a proxy that enables fluent method chaining for dynamic field access.
50
+ *
51
+ * This allows arbitrary nested property access that resolves to a callable function,
52
+ * supporting patterns like `proxy.foo()`, `proxy.foo.bar()`, `proxy.bar.baz.qux()`.
53
+ *
54
+ * Used to create global namespace objects that proxy calls to a service backend.
55
+ */
56
+ export function createFieldAccessProxy<T = unknown>(
57
+ namespace: string,
58
+ handler: (
59
+ namespace: string,
60
+ methodPath: string,
61
+ ...args: T[]
62
+ ) => Promise<unknown>,
63
+ ): Record<string, (...args: T[]) => Promise<unknown>> {
64
+ function createNestedProxy(path: string): (...args: T[]) => Promise<unknown> {
65
+ const fn = (...args: T[]) => handler(namespace, path, ...args);
66
+
67
+ return new Proxy(fn, {
68
+ get(_, nestedName: string) {
69
+ if (typeof nestedName === 'symbol') return undefined;
70
+ const newPath = path ? `${path}.${nestedName}` : nestedName;
71
+ return createNestedProxy(newPath);
72
+ },
73
+ }) as (...args: T[]) => Promise<unknown>;
74
+ }
75
+
76
+ return new Proxy(
77
+ {},
78
+ {
79
+ get(_, fieldName: string) {
80
+ if (typeof fieldName === 'symbol') return undefined;
81
+ return createNestedProxy(fieldName);
82
+ },
83
+ },
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Create namespace globals that proxy calls to a service proxy
89
+ *
90
+ * Creates dynamic proxy objects for each namespace that support arbitrary
91
+ * nested method calls. This replaces the old static method registration.
92
+ *
93
+ * @param services - Array of service names (e.g., ['git', 'github'])
94
+ * @param proxy - The service proxy to forward calls to
95
+ * @returns Record of namespace names to proxy objects
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const namespaces = generateNamespaceGlobals(['git', 'github'], proxy);
100
+ * // namespaces.git.status() calls proxy.call('git', 'status', [])
101
+ * // namespaces.github.repos.list_for_user({ username: 'x' })
102
+ * // calls proxy.call('github', 'repos.list_for_user', [{ username: 'x' }])
103
+ * ```
104
+ */
105
+ export function generateNamespaceGlobals(
106
+ services: string[],
107
+ proxy: ServiceProxy,
108
+ ): Record<string, unknown> {
109
+ const namespaces: Record<string, unknown> = {};
110
+ const uniqueNamespaces = extractNamespaces(services);
111
+
112
+ for (const namespace of uniqueNamespaces) {
113
+ namespaces[namespace] = createFieldAccessProxy(
114
+ namespace,
115
+ (ns, method, ...args) => proxy.call(ns, method, args),
116
+ );
117
+ }
118
+
119
+ return namespaces;
120
+ }
121
+
122
+ /**
123
+ * Inject namespace globals into a window object
124
+ */
125
+ export function injectNamespaceGlobals(
126
+ target: Window | typeof globalThis,
127
+ namespaces: Record<string, unknown>,
128
+ ): void {
129
+ for (const [name, value] of Object.entries(namespaces)) {
130
+ (target as Record<string, unknown>)[name] = value;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Remove namespace globals from a window object
136
+ */
137
+ export function removeNamespaceGlobals(
138
+ target: Window | typeof globalThis,
139
+ namespaceNames: string[],
140
+ ): void {
141
+ for (const name of namespaceNames) {
142
+ delete (target as Record<string, unknown>)[name];
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Extract unique namespace names from services array
148
+ */
149
+ export function extractNamespaces(services: string[]): string[] {
150
+ const namespaces = new Set<string>();
151
+ for (const service of services) {
152
+ const parts = service.split('.');
153
+ if (parts[0]) {
154
+ namespaces.add(parts[0]);
155
+ }
156
+ }
157
+ return Array.from(namespaces);
158
+ }
159
+
160
+ /**
161
+ * Parent-side bridge for iframe communication
162
+ *
163
+ * Listens for postMessage events from iframes and proxies service calls.
164
+ */
165
+ export class ParentBridge {
166
+ private proxy: ServiceProxy;
167
+ private pendingCalls = new Map<
168
+ string,
169
+ { resolve: (value: unknown) => void; reject: (error: Error) => void }
170
+ >();
171
+ private iframes = new Set<HTMLIFrameElement>();
172
+ private messageHandler: (event: MessageEvent) => void;
173
+
174
+ constructor(proxy: ServiceProxy) {
175
+ this.proxy = proxy;
176
+ this.messageHandler = this.handleMessage.bind(this);
177
+ if (typeof window !== 'undefined') {
178
+ window.addEventListener('message', this.messageHandler);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Register an iframe to receive messages from
184
+ */
185
+ registerIframe(iframe: HTMLIFrameElement): void {
186
+ this.iframes.add(iframe);
187
+ }
188
+
189
+ /**
190
+ * Unregister an iframe
191
+ */
192
+ unregisterIframe(iframe: HTMLIFrameElement): void {
193
+ this.iframes.delete(iframe);
194
+ }
195
+
196
+ /**
197
+ * Handle incoming messages from iframes
198
+ */
199
+ private async handleMessage(event: MessageEvent): Promise<void> {
200
+ // Verify source is a registered iframe
201
+ const sourceIframe = Array.from(this.iframes).find(
202
+ (iframe) => iframe.contentWindow === event.source,
203
+ );
204
+
205
+ if (!sourceIframe) {
206
+ return; // Ignore messages from unknown sources
207
+ }
208
+
209
+ const message = event.data as BridgeMessage;
210
+ if (!message || typeof message !== 'object') return;
211
+
212
+ if (message.type === 'service-call') {
213
+ const payload = message.payload as ServiceCallPayload;
214
+ try {
215
+ const result = await this.proxy.call(
216
+ payload.namespace,
217
+ payload.procedure,
218
+ payload.args,
219
+ );
220
+
221
+ const response: BridgeMessage = {
222
+ type: 'service-result',
223
+ id: message.id,
224
+ payload: { result } as ServiceResultPayload,
225
+ };
226
+
227
+ sourceIframe.contentWindow?.postMessage(response, '*');
228
+ } catch (error) {
229
+ const response: BridgeMessage = {
230
+ type: 'service-result',
231
+ id: message.id,
232
+ payload: {
233
+ error: error instanceof Error ? error.message : String(error),
234
+ } as ServiceResultPayload,
235
+ };
236
+
237
+ sourceIframe.contentWindow?.postMessage(response, '*');
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Dispose the bridge
244
+ */
245
+ dispose(): void {
246
+ if (typeof window !== 'undefined') {
247
+ window.removeEventListener('message', this.messageHandler);
248
+ }
249
+ this.iframes.clear();
250
+ this.pendingCalls.clear();
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Child-side bridge for iframe communication
256
+ *
257
+ * Creates a service proxy that sends postMessage to parent.
258
+ */
259
+ export function createIframeServiceProxy(): ServiceProxy {
260
+ const pendingCalls = new Map<
261
+ string,
262
+ { resolve: (value: unknown) => void; reject: (error: Error) => void }
263
+ >();
264
+
265
+ // Listen for results from parent
266
+ if (typeof window !== 'undefined') {
267
+ window.addEventListener('message', (event: MessageEvent) => {
268
+ const message = event.data as BridgeMessage;
269
+ if (!message || typeof message !== 'object') return;
270
+
271
+ if (message.type === 'service-result') {
272
+ const pending = pendingCalls.get(message.id);
273
+ if (pending) {
274
+ pendingCalls.delete(message.id);
275
+ const payload = message.payload as ServiceResultPayload;
276
+ if (payload.error) {
277
+ pending.reject(new Error(payload.error));
278
+ } else {
279
+ pending.resolve(payload.result);
280
+ }
281
+ }
282
+ }
283
+ });
284
+ }
285
+
286
+ return {
287
+ call(
288
+ namespace: string,
289
+ procedure: string,
290
+ args: unknown[],
291
+ ): Promise<unknown> {
292
+ return new Promise((resolve, reject) => {
293
+ const id = generateMessageId();
294
+ pendingCalls.set(id, { resolve, reject });
295
+
296
+ const message: BridgeMessage = {
297
+ type: 'service-call',
298
+ id,
299
+ payload: { namespace, procedure, args } as ServiceCallPayload,
300
+ };
301
+
302
+ window.parent.postMessage(message, '*');
303
+
304
+ // Timeout after 30 seconds
305
+ setTimeout(() => {
306
+ if (pendingCalls.has(id)) {
307
+ pendingCalls.delete(id);
308
+ reject(
309
+ new Error(`Service call timeout: ${namespace}.${procedure}`),
310
+ );
311
+ }
312
+ }, 30000);
313
+ });
314
+ },
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Generate the bridge script to inject into iframes
320
+ *
321
+ * Creates a self-contained script that sets up:
322
+ * 1. Message handling for service results from parent
323
+ * 2. Dynamic proxy objects for each namespace that support arbitrary nested calls
324
+ */
325
+ export function generateIframeBridgeScript(services: string[]): string {
326
+ const uniqueNamespaces = extractNamespaces(services);
327
+ const namespaceAssignments = uniqueNamespaces
328
+ .map((ns) => `window.${ns} = createNamespaceProxy('${ns}');`)
329
+ .join('\n ');
330
+
331
+ return `
332
+ (function() {
333
+ const pendingCalls = new Map();
334
+
335
+ window.addEventListener('message', function(event) {
336
+ const message = event.data;
337
+ if (!message || typeof message !== 'object') return;
338
+
339
+ if (message.type === 'service-result') {
340
+ const pending = pendingCalls.get(message.id);
341
+ if (pending) {
342
+ pendingCalls.delete(message.id);
343
+ if (message.payload.error) {
344
+ pending.reject(new Error(message.payload.error));
345
+ } else {
346
+ pending.resolve(message.payload.result);
347
+ }
348
+ }
349
+ }
350
+ });
351
+
352
+ function proxyCall(namespace, procedure, args) {
353
+ return new Promise(function(resolve, reject) {
354
+ const id = Date.now() + '-' + Math.random().toString(36).slice(2, 11);
355
+ pendingCalls.set(id, { resolve: resolve, reject: reject });
356
+
357
+ window.parent.postMessage({
358
+ type: 'service-call',
359
+ id: id,
360
+ payload: { namespace: namespace, procedure: procedure, args: args }
361
+ }, '*');
362
+
363
+ setTimeout(function() {
364
+ if (pendingCalls.has(id)) {
365
+ pendingCalls.delete(id);
366
+ reject(new Error('Service call timeout: ' + namespace + '.' + procedure));
367
+ }
368
+ }, 30000);
369
+ });
370
+ }
371
+
372
+ // Create a dynamic proxy for a namespace that supports arbitrary nested method calls
373
+ function createNamespaceProxy(namespace) {
374
+ function createNestedProxy(path) {
375
+ var fn = function() {
376
+ return proxyCall(namespace, path, Array.prototype.slice.call(arguments));
377
+ };
378
+
379
+ return new Proxy(fn, {
380
+ get: function(_, nestedName) {
381
+ if (typeof nestedName === 'symbol') return undefined;
382
+ var newPath = path ? path + '.' + nestedName : nestedName;
383
+ return createNestedProxy(newPath);
384
+ }
385
+ });
386
+ }
387
+
388
+ return new Proxy({}, {
389
+ get: function(_, fieldName) {
390
+ if (typeof fieldName === 'symbol') return undefined;
391
+ return createNestedProxy(fieldName);
392
+ }
393
+ });
394
+ }
395
+
396
+ ${namespaceAssignments}
397
+ })();
398
+ `;
399
+ }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Embedded mount mode - mounts widgets directly in the DOM
3
+ *
4
+ * For trusted widgets that need full window access.
5
+ */
6
+
7
+ import type {
8
+ CompiledWidget,
9
+ LoadedImage,
10
+ MountedWidget,
11
+ MountOptions,
12
+ ServiceProxy,
13
+ } from '../types.js';
14
+ import {
15
+ generateNamespaceGlobals,
16
+ injectNamespaceGlobals,
17
+ removeNamespaceGlobals,
18
+ extractNamespaces,
19
+ } from './bridge.js';
20
+
21
+ let mountCounter = 0;
22
+ let importMapInjected = false;
23
+
24
+ /**
25
+ * Inject an import map for bare module specifiers.
26
+ * Maps package names to their CDN URLs so browsers can resolve them.
27
+ * Must be called before any ES module imports happen.
28
+ */
29
+ function injectImportMap(
30
+ globals: Record<string, string>,
31
+ preloadUrls: string[],
32
+ deps?: Record<string, string>,
33
+ ): void {
34
+ // Only inject once per page (browser limitation)
35
+ if (importMapInjected) return;
36
+
37
+ // Check if there's already an import map
38
+ const existingMap = document.querySelector('script[type="importmap"]');
39
+ if (existingMap) {
40
+ // Cannot modify existing import maps in standard browsers
41
+ importMapInjected = true;
42
+ return;
43
+ }
44
+
45
+ // Build import map from globals + preload URLs
46
+ // Convention: globals keys are package names, preload URLs are in matching order
47
+ const imports: Record<string, string> = {};
48
+ const packageNames = Object.keys(globals);
49
+
50
+ packageNames.forEach((pkgName, index) => {
51
+ // Use the preload URL if available, otherwise construct CDN URL
52
+ if (preloadUrls[index]) {
53
+ imports[pkgName] = preloadUrls[index];
54
+ } else if (deps?.[pkgName]) {
55
+ imports[pkgName] = `https://esm.sh/${pkgName}@${deps[pkgName]}`;
56
+ } else {
57
+ imports[pkgName] = `https://esm.sh/${pkgName}`;
58
+ }
59
+ });
60
+
61
+ // Also add common subpaths (e.g., react-dom/client)
62
+ if (imports['react-dom']) {
63
+ imports['react-dom/client'] = imports['react-dom'];
64
+ }
65
+
66
+ // Inject new import map
67
+ const script = document.createElement('script');
68
+ script.type = 'importmap';
69
+ script.textContent = JSON.stringify({ imports }, null, 2);
70
+ document.head.insertBefore(script, document.head.firstChild);
71
+
72
+ importMapInjected = true;
73
+ }
74
+
75
+ /**
76
+ * Generate a unique mount ID
77
+ */
78
+ function generateMountId(): string {
79
+ return `pw-mount-${Date.now()}-${++mountCounter}`;
80
+ }
81
+
82
+ type CreateElementFn = (...args: unknown[]) => unknown;
83
+ type CreateRootFn = (el: HTMLElement) => {
84
+ render: (el: unknown) => void;
85
+ unmount?: () => void;
86
+ };
87
+ type RenderFn = (el: unknown, container: HTMLElement) => void;
88
+
89
+ type Renderer =
90
+ | { kind: 'root'; createRoot: CreateRootFn }
91
+ | { kind: 'render'; render: RenderFn };
92
+
93
+ function pickCreateElement(
94
+ globals: Array<Record<string, unknown>>,
95
+ ): CreateElementFn | null {
96
+ for (const obj of globals) {
97
+ const ce = obj?.createElement;
98
+ if (typeof ce === 'function') return ce as CreateElementFn;
99
+ const def = obj?.default as Record<string, unknown> | undefined;
100
+ if (def && typeof def.createElement === 'function') {
101
+ return def.createElement as CreateElementFn;
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ function pickRenderer(
108
+ globals: Array<Record<string, unknown>>,
109
+ ): Renderer | null {
110
+ for (const obj of globals) {
111
+ if (obj && typeof obj.createRoot === 'function') {
112
+ return { kind: 'root', createRoot: obj.createRoot as CreateRootFn };
113
+ }
114
+ if (obj && typeof obj.render === 'function') {
115
+ return { kind: 'render', render: obj.render as RenderFn };
116
+ }
117
+ const def = obj?.default as Record<string, unknown> | undefined;
118
+ if (def && typeof def.createRoot === 'function') {
119
+ return { kind: 'root', createRoot: def.createRoot as CreateRootFn };
120
+ }
121
+ if (def && typeof def.render === 'function') {
122
+ return { kind: 'render', render: def.render as RenderFn };
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+
128
+ /**
129
+ * Mount a widget in embedded mode (direct DOM injection)
130
+ */
131
+ export async function mountEmbedded(
132
+ widget: CompiledWidget,
133
+ options: MountOptions,
134
+ image: LoadedImage | null,
135
+ proxy: ServiceProxy,
136
+ ): Promise<MountedWidget> {
137
+ const { target, inputs = {} } = options;
138
+ const mountId = generateMountId();
139
+
140
+ // Create container
141
+ const container = document.createElement('div');
142
+ container.id = mountId;
143
+ container.className = 'patchwork-widget patchwork-embedded';
144
+ target.appendChild(container);
145
+
146
+ // Run image setup if available
147
+ if (image?.setup) {
148
+ await image.setup(container);
149
+ }
150
+
151
+ // Inject CSS if available
152
+ if (image?.css) {
153
+ const style = document.createElement('style');
154
+ style.id = `${mountId}-style`;
155
+ style.textContent = image.css;
156
+ document.head.appendChild(style);
157
+ }
158
+
159
+ // Generate and inject service namespace globals
160
+ const services = widget.manifest.services || [];
161
+ const namespaceNames = extractNamespaces(services);
162
+ const namespaces = generateNamespaceGlobals(services, proxy);
163
+ injectNamespaceGlobals(window, namespaces);
164
+
165
+ // Get framework config from image
166
+ const frameworkConfig = image?.config?.framework || {};
167
+ const preloadUrls = frameworkConfig.preload || [];
168
+ const globalMapping = frameworkConfig.globals || {};
169
+ const deps = frameworkConfig.deps || {};
170
+
171
+ // Inject import map for bare module specifiers (must happen before ES module imports)
172
+ // This allows the browser to resolve imports like 'react' to CDN URLs
173
+ injectImportMap(globalMapping, preloadUrls, deps);
174
+
175
+ // Pre-load framework modules from image config
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ const preloadedModules: any[] = await Promise.all(
178
+ preloadUrls.map(
179
+ (url: string) => import(/* webpackIgnore: true */ /* @vite-ignore */ url),
180
+ ),
181
+ );
182
+
183
+ // Set framework globals on window based on image config
184
+ const win = window as unknown as Record<string, unknown>;
185
+ const globalNames = Object.values(globalMapping) as string[];
186
+
187
+ // Map preloaded modules to their global names
188
+ // Convention: preload order matches globals order (react -> React, react-dom -> ReactDOM)
189
+ preloadedModules.forEach((mod, index) => {
190
+ if (globalNames[index]) {
191
+ const name = globalNames[index];
192
+ win[name] = mod;
193
+ }
194
+ });
195
+
196
+ // Create a blob with the widget code
197
+ const blob = new Blob([widget.code], { type: 'application/javascript' });
198
+ const scriptUrl = URL.createObjectURL(blob);
199
+
200
+ // Import the module
201
+ let moduleCleanup: (() => void) | undefined;
202
+
203
+ const globalObjects = globalNames
204
+ .map((n) => win[n] as unknown)
205
+ .filter(Boolean) as Array<Record<string, unknown>>;
206
+
207
+ try {
208
+ const module = await import(/* webpackIgnore: true */ scriptUrl);
209
+
210
+ // Image-provided mount handler takes priority
211
+ if (image?.mount) {
212
+ const result = await image.mount(module, container, inputs);
213
+ if (typeof result === 'function') {
214
+ moduleCleanup = result;
215
+ }
216
+ } else if (typeof module.mount === 'function') {
217
+ // Widget exports its own mount function
218
+ const result = await module.mount(container, inputs);
219
+ if (typeof result === 'function') {
220
+ moduleCleanup = result;
221
+ }
222
+ } else if (typeof module.render === 'function') {
223
+ // Custom render function
224
+ const result = await module.render(container, inputs);
225
+ if (typeof result === 'function') {
226
+ moduleCleanup = result;
227
+ }
228
+ } else if (typeof module.default === 'function') {
229
+ // Default export component - render using framework
230
+ const Component = module.default;
231
+
232
+ const createElement = pickCreateElement(globalObjects);
233
+ const renderer = pickRenderer(globalObjects);
234
+
235
+ if (createElement && renderer?.kind === 'root') {
236
+ const root = renderer.createRoot(container);
237
+ root.render(createElement(Component, inputs));
238
+ if (typeof root.unmount === 'function') {
239
+ moduleCleanup = () => root.unmount!();
240
+ }
241
+ } else if (createElement && renderer?.kind === 'render') {
242
+ renderer.render(createElement(Component, inputs), container);
243
+ } else {
244
+ // No framework renderer - try calling as plain function
245
+ const result = Component(inputs);
246
+ if (result instanceof HTMLElement) {
247
+ container.appendChild(result);
248
+ } else if (typeof result === 'string') {
249
+ container.innerHTML = result;
250
+ }
251
+ }
252
+ }
253
+ } finally {
254
+ URL.revokeObjectURL(scriptUrl);
255
+ }
256
+
257
+ // Create unmount function
258
+ const unmount = () => {
259
+ // Call module cleanup if available
260
+ if (moduleCleanup) {
261
+ moduleCleanup();
262
+ }
263
+
264
+ // Remove namespace globals
265
+ removeNamespaceGlobals(window, namespaceNames);
266
+
267
+ // Remove style
268
+ const style = document.getElementById(`${mountId}-style`);
269
+ if (style) {
270
+ style.remove();
271
+ }
272
+
273
+ // Remove container
274
+ container.remove();
275
+ };
276
+
277
+ return {
278
+ id: mountId,
279
+ widget,
280
+ mode: 'embedded',
281
+ target,
282
+ inputs,
283
+ unmount,
284
+ };
285
+ }
286
+
287
+ /**
288
+ * Hot reload an embedded widget
289
+ */
290
+ export async function reloadEmbedded(
291
+ mounted: MountedWidget,
292
+ widget: CompiledWidget,
293
+ image: LoadedImage | null,
294
+ proxy: ServiceProxy,
295
+ ): Promise<MountedWidget> {
296
+ // Unmount existing
297
+ mounted.unmount();
298
+
299
+ // Remount with new widget
300
+ return mountEmbedded(
301
+ widget,
302
+ { target: mounted.target, mode: 'embedded', inputs: mounted.inputs },
303
+ image,
304
+ proxy,
305
+ );
306
+ }