@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.
- package/.eslintrc.json +22 -0
- package/.github/workflows/publish.yml +41 -0
- package/.prettierignore +17 -0
- package/LICENSE +373 -0
- package/README.md +15 -0
- package/apps/chat/.utcp_config.json +14 -0
- package/apps/chat/.working/widgets/27060b91-a2a5-4272-b243-6eb904bd4070/main.tsx +107 -0
- package/apps/chat/index.html +17 -0
- package/apps/chat/node_modules/.bin/autoprefixer +17 -0
- package/apps/chat/node_modules/.bin/browserslist +17 -0
- package/apps/chat/node_modules/.bin/conc +17 -0
- package/apps/chat/node_modules/.bin/concurrently +17 -0
- package/apps/chat/node_modules/.bin/copilot-proxy +17 -0
- package/apps/chat/node_modules/.bin/jiti +17 -0
- package/apps/chat/node_modules/.bin/tailwind +17 -0
- package/apps/chat/node_modules/.bin/tailwindcss +17 -0
- package/apps/chat/node_modules/.bin/tsc +17 -0
- package/apps/chat/node_modules/.bin/tsserver +17 -0
- package/apps/chat/node_modules/.bin/tsx +17 -0
- package/apps/chat/node_modules/.bin/vite +17 -0
- package/apps/chat/package.json +55 -0
- package/apps/chat/postcss.config.js +6 -0
- package/apps/chat/src/App.tsx +7 -0
- package/apps/chat/src/components/ui/avatar.tsx +48 -0
- package/apps/chat/src/components/ui/badge.tsx +36 -0
- package/apps/chat/src/components/ui/button.tsx +56 -0
- package/apps/chat/src/components/ui/card.tsx +86 -0
- package/apps/chat/src/components/ui/collapsible.tsx +9 -0
- package/apps/chat/src/components/ui/dialog.tsx +60 -0
- package/apps/chat/src/components/ui/input.tsx +25 -0
- package/apps/chat/src/components/ui/scroll-area.tsx +46 -0
- package/apps/chat/src/index.css +190 -0
- package/apps/chat/src/lib/utils.ts +6 -0
- package/apps/chat/src/main.tsx +10 -0
- package/apps/chat/src/pages/ChatPage.tsx +460 -0
- package/apps/chat/tailwind.config.js +71 -0
- package/apps/chat/tsconfig.json +25 -0
- package/apps/chat/vite.config.ts +26 -0
- package/package.json +35 -0
- package/packages/bobbin/node_modules/.bin/esbuild +14 -0
- package/packages/bobbin/node_modules/.bin/jiti +17 -0
- package/packages/bobbin/node_modules/.bin/tsc +17 -0
- package/packages/bobbin/node_modules/.bin/tsserver +17 -0
- package/packages/bobbin/node_modules/.bin/tsup +17 -0
- package/packages/bobbin/node_modules/.bin/tsup-node +17 -0
- package/packages/bobbin/node_modules/.bin/tsx +17 -0
- package/packages/bobbin/package.json +30 -0
- package/packages/bobbin/src/Bobbin.tsx +89 -0
- package/packages/bobbin/src/components/EditPanel/EditPanel.tsx +376 -0
- package/packages/bobbin/src/components/EditPanel/controls/ColorPicker.tsx +138 -0
- package/packages/bobbin/src/components/EditPanel/controls/QuickSelectDropdown.tsx +142 -0
- package/packages/bobbin/src/components/EditPanel/controls/SliderInput.tsx +94 -0
- package/packages/bobbin/src/components/EditPanel/controls/SpacingControl.tsx +285 -0
- package/packages/bobbin/src/components/EditPanel/controls/ToggleGroup.tsx +37 -0
- package/packages/bobbin/src/components/EditPanel/controls/TokenDropdown.tsx +33 -0
- package/packages/bobbin/src/components/EditPanel/sections/AnnotationSection.tsx +136 -0
- package/packages/bobbin/src/components/EditPanel/sections/BackgroundSection.tsx +79 -0
- package/packages/bobbin/src/components/EditPanel/sections/EffectsSection.tsx +85 -0
- package/packages/bobbin/src/components/EditPanel/sections/LayoutSection.tsx +224 -0
- package/packages/bobbin/src/components/EditPanel/sections/SectionWrapper.tsx +57 -0
- package/packages/bobbin/src/components/EditPanel/sections/SizeSection.tsx +166 -0
- package/packages/bobbin/src/components/EditPanel/sections/SpacingSection.tsx +69 -0
- package/packages/bobbin/src/components/EditPanel/sections/TypographySection.tsx +148 -0
- package/packages/bobbin/src/components/Inspector/Inspector.tsx +221 -0
- package/packages/bobbin/src/components/Overlay/ControlHandles.tsx +572 -0
- package/packages/bobbin/src/components/Overlay/MarginPaddingOverlay.tsx +229 -0
- package/packages/bobbin/src/components/Overlay/SelectionOverlay.tsx +73 -0
- package/packages/bobbin/src/components/Pill/Pill.tsx +155 -0
- package/packages/bobbin/src/components/ThemeToggle/ThemeToggle.tsx +72 -0
- package/packages/bobbin/src/core/changeSerializer.ts +139 -0
- package/packages/bobbin/src/core/useBobbin.ts +399 -0
- package/packages/bobbin/src/core/useChangeTracker.ts +186 -0
- package/packages/bobbin/src/core/useClipboard.ts +21 -0
- package/packages/bobbin/src/core/useElementSelection.ts +146 -0
- package/packages/bobbin/src/index.ts +46 -0
- package/packages/bobbin/src/tokens/borders.ts +19 -0
- package/packages/bobbin/src/tokens/colors.ts +150 -0
- package/packages/bobbin/src/tokens/index.ts +37 -0
- package/packages/bobbin/src/tokens/shadows.ts +10 -0
- package/packages/bobbin/src/tokens/spacing.ts +37 -0
- package/packages/bobbin/src/tokens/typography.ts +51 -0
- package/packages/bobbin/src/types.ts +157 -0
- package/packages/bobbin/src/utils/animation.ts +40 -0
- package/packages/bobbin/src/utils/dom.ts +36 -0
- package/packages/bobbin/src/utils/selectors.ts +76 -0
- package/packages/bobbin/tsconfig.json +10 -0
- package/packages/bobbin/tsup.config.ts +10 -0
- package/packages/compiler/node_modules/.bin/esbuild +17 -0
- package/packages/compiler/node_modules/.bin/jiti +17 -0
- package/packages/compiler/node_modules/.bin/tsc +17 -0
- package/packages/compiler/node_modules/.bin/tsserver +17 -0
- package/packages/compiler/node_modules/.bin/tsup +17 -0
- package/packages/compiler/node_modules/.bin/tsup-node +17 -0
- package/packages/compiler/node_modules/.bin/tsx +17 -0
- package/packages/compiler/package.json +38 -0
- package/packages/compiler/src/compiler.ts +258 -0
- package/packages/compiler/src/images/index.ts +13 -0
- package/packages/compiler/src/images/loader.ts +234 -0
- package/packages/compiler/src/images/registry.ts +112 -0
- package/packages/compiler/src/index.ts +141 -0
- package/packages/compiler/src/mount/bridge.ts +399 -0
- package/packages/compiler/src/mount/embedded.ts +306 -0
- package/packages/compiler/src/mount/iframe.ts +433 -0
- package/packages/compiler/src/mount/index.ts +18 -0
- package/packages/compiler/src/schemas.ts +169 -0
- package/packages/compiler/src/transforms/cdn.ts +411 -0
- package/packages/compiler/src/transforms/index.ts +4 -0
- package/packages/compiler/src/transforms/vfs.ts +138 -0
- package/packages/compiler/src/types.ts +233 -0
- package/packages/compiler/src/vfs/backends/indexeddb.ts +66 -0
- package/packages/compiler/src/vfs/backends/local-fs.ts +41 -0
- package/packages/compiler/src/vfs/backends/s3.ts +60 -0
- package/packages/compiler/src/vfs/index.ts +11 -0
- package/packages/compiler/src/vfs/project.ts +56 -0
- package/packages/compiler/src/vfs/store.ts +53 -0
- package/packages/compiler/src/vfs/types.ts +20 -0
- package/packages/compiler/tsconfig.json +8 -0
- package/packages/compiler/tsup.config.ts +14 -0
- package/packages/editor/node_modules/.bin/jiti +17 -0
- package/packages/editor/node_modules/.bin/tsc +17 -0
- package/packages/editor/node_modules/.bin/tsserver +17 -0
- package/packages/editor/node_modules/.bin/tsup +17 -0
- package/packages/editor/node_modules/.bin/tsup-node +17 -0
- package/packages/editor/node_modules/.bin/tsx +17 -0
- package/packages/editor/package.json +45 -0
- package/packages/editor/src/components/CodeBlockExtension.tsx +190 -0
- package/packages/editor/src/components/CodePreview.tsx +344 -0
- package/packages/editor/src/components/MarkdownEditor.tsx +270 -0
- package/packages/editor/src/components/ServicesInspector.tsx +118 -0
- package/packages/editor/src/components/edit/EditHistory.tsx +89 -0
- package/packages/editor/src/components/edit/EditModal.tsx +236 -0
- package/packages/editor/src/components/edit/FileTree.tsx +144 -0
- package/packages/editor/src/components/edit/api.ts +100 -0
- package/packages/editor/src/components/edit/index.ts +6 -0
- package/packages/editor/src/components/edit/types.ts +53 -0
- package/packages/editor/src/components/edit/useEditSession.ts +164 -0
- package/packages/editor/src/components/index.ts +5 -0
- package/packages/editor/src/index.ts +72 -0
- package/packages/editor/src/lib/code-extractor.ts +210 -0
- package/packages/editor/src/lib/diff.ts +308 -0
- package/packages/editor/src/lib/index.ts +4 -0
- package/packages/editor/src/lib/utils.ts +6 -0
- package/packages/editor/src/lib/vfs.ts +106 -0
- package/packages/editor/tsconfig.json +10 -0
- package/packages/editor/tsup.config.ts +10 -0
- package/packages/images/ink/node_modules/.bin/jiti +17 -0
- package/packages/images/ink/node_modules/.bin/tsc +17 -0
- package/packages/images/ink/node_modules/.bin/tsserver +17 -0
- package/packages/images/ink/node_modules/.bin/tsup +17 -0
- package/packages/images/ink/node_modules/.bin/tsup-node +17 -0
- package/packages/images/ink/node_modules/.bin/tsx +17 -0
- package/packages/images/ink/package.json +53 -0
- package/packages/images/ink/src/index.ts +48 -0
- package/packages/images/ink/src/runner.ts +331 -0
- package/packages/images/ink/src/setup.ts +123 -0
- package/packages/images/ink/tsconfig.json +10 -0
- package/packages/images/ink/tsup.config.ts +11 -0
- package/packages/images/shadcn/node_modules/.bin/jiti +17 -0
- package/packages/images/shadcn/node_modules/.bin/tsc +17 -0
- package/packages/images/shadcn/node_modules/.bin/tsserver +17 -0
- package/packages/images/shadcn/node_modules/.bin/tsup +17 -0
- package/packages/images/shadcn/node_modules/.bin/tsup-node +17 -0
- package/packages/images/shadcn/node_modules/.bin/tsx +17 -0
- package/packages/images/shadcn/package.json +82 -0
- package/packages/images/shadcn/src/html.ts +341 -0
- package/packages/images/shadcn/src/index.ts +37 -0
- package/packages/images/shadcn/src/setup.ts +287 -0
- package/packages/images/shadcn/tsconfig.json +9 -0
- package/packages/images/shadcn/tsup.config.ts +13 -0
- package/packages/images/vanilla/node_modules/.bin/jiti +17 -0
- package/packages/images/vanilla/node_modules/.bin/tsc +17 -0
- package/packages/images/vanilla/node_modules/.bin/tsserver +17 -0
- package/packages/images/vanilla/node_modules/.bin/tsup +17 -0
- package/packages/images/vanilla/node_modules/.bin/tsup-node +17 -0
- package/packages/images/vanilla/node_modules/.bin/tsx +17 -0
- package/packages/images/vanilla/package.json +35 -0
- package/packages/images/vanilla/src/index.ts +7 -0
- package/packages/images/vanilla/src/setup.ts +6 -0
- package/packages/images/vanilla/tsconfig.json +9 -0
- package/packages/images/vanilla/tsup.config.ts +10 -0
- package/packages/patchwork/node_modules/.bin/jiti +17 -0
- package/packages/patchwork/node_modules/.bin/tsc +17 -0
- package/packages/patchwork/node_modules/.bin/tsserver +17 -0
- package/packages/patchwork/node_modules/.bin/tsup +17 -0
- package/packages/patchwork/node_modules/.bin/tsup-node +17 -0
- package/packages/patchwork/node_modules/.bin/tsx +17 -0
- package/packages/patchwork/package.json +27 -0
- package/packages/patchwork/src/index.ts +15 -0
- package/packages/patchwork/src/services/index.ts +11 -0
- package/packages/patchwork/src/services/proxy.ts +213 -0
- package/packages/patchwork/src/services/types.ts +28 -0
- package/packages/patchwork/src/types.ts +116 -0
- package/packages/patchwork/tsconfig.json +8 -0
- package/packages/patchwork/tsup.config.ts +14 -0
- package/packages/stitchery/node_modules/.bin/jiti +17 -0
- package/packages/stitchery/node_modules/.bin/tsc +17 -0
- package/packages/stitchery/node_modules/.bin/tsserver +17 -0
- package/packages/stitchery/node_modules/.bin/tsup +17 -0
- package/packages/stitchery/node_modules/.bin/tsup-node +17 -0
- package/packages/stitchery/node_modules/.bin/tsx +17 -0
- package/packages/stitchery/package.json +40 -0
- package/packages/stitchery/src/cli.ts +116 -0
- package/packages/stitchery/src/index.ts +16 -0
- package/packages/stitchery/src/prompts.ts +326 -0
- package/packages/stitchery/src/server/index.ts +365 -0
- package/packages/stitchery/src/server/local-packages.ts +91 -0
- package/packages/stitchery/src/server/routes.ts +122 -0
- package/packages/stitchery/src/server/services.ts +382 -0
- package/packages/stitchery/src/server/vfs-routes.ts +142 -0
- package/packages/stitchery/src/types.ts +59 -0
- package/packages/stitchery/tsconfig.json +13 -0
- package/packages/stitchery/tsup.config.ts +15 -0
- package/packages/utcp/node_modules/.bin/jiti +17 -0
- package/packages/utcp/node_modules/.bin/tsc +17 -0
- package/packages/utcp/node_modules/.bin/tsserver +17 -0
- package/packages/utcp/node_modules/.bin/tsup +17 -0
- package/packages/utcp/node_modules/.bin/tsup-node +17 -0
- package/packages/utcp/node_modules/.bin/tsx +17 -0
- package/packages/utcp/package.json +38 -0
- package/packages/utcp/src/index.ts +153 -0
- package/packages/utcp/tsconfig.json +8 -0
- package/packages/utcp/tsup.config.ts +12 -0
- package/pnpm-workspace.yaml +3 -0
- package/tsconfig.json +18 -0
- package/turbo.json +23 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Iframe mount mode - mounts widgets in sandboxed iframes
|
|
3
|
+
*
|
|
4
|
+
* For untrusted widgets that need isolation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
CompiledWidget,
|
|
9
|
+
LoadedImage,
|
|
10
|
+
MountedWidget,
|
|
11
|
+
MountOptions,
|
|
12
|
+
ServiceProxy,
|
|
13
|
+
} from '../types.js';
|
|
14
|
+
import { ParentBridge, generateIframeBridgeScript } from './bridge.js';
|
|
15
|
+
import { generateImportMap } from '../transforms/cdn.js';
|
|
16
|
+
|
|
17
|
+
let mountCounter = 0;
|
|
18
|
+
|
|
19
|
+
// Shared bridge for all iframes
|
|
20
|
+
let sharedBridge: ParentBridge | null = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get or create the shared parent bridge
|
|
24
|
+
*/
|
|
25
|
+
function getParentBridge(proxy: ServiceProxy): ParentBridge {
|
|
26
|
+
if (!sharedBridge) {
|
|
27
|
+
sharedBridge = new ParentBridge(proxy);
|
|
28
|
+
}
|
|
29
|
+
return sharedBridge;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate a unique mount ID
|
|
34
|
+
*/
|
|
35
|
+
function generateMountId(): string {
|
|
36
|
+
return `pw-iframe-${Date.now()}-${++mountCounter}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Default sandbox attributes for iframes (production)
|
|
41
|
+
*
|
|
42
|
+
* By default, iframes are strictly sandboxed without same-origin access.
|
|
43
|
+
* This is the safest option when widgets load all dependencies from external CDNs.
|
|
44
|
+
*/
|
|
45
|
+
const DEFAULT_SANDBOX = ['allow-scripts'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Development sandbox attributes - includes allow-same-origin
|
|
49
|
+
*
|
|
50
|
+
* allow-same-origin is required when:
|
|
51
|
+
* - Fetching modules from the parent origin (e.g., /_local-packages/ in dev)
|
|
52
|
+
* - Using import maps that reference parent-relative URLs
|
|
53
|
+
* - Accessing the parent's CDN proxy
|
|
54
|
+
*
|
|
55
|
+
* Note: This does NOT allow the iframe to access parent's DOM or cookies,
|
|
56
|
+
* but it does allow same-origin network requests.
|
|
57
|
+
*
|
|
58
|
+
* WARNING: Combining allow-scripts + allow-same-origin allows the iframe to
|
|
59
|
+
* escape its sandbox. Only use in development or when hosting on a separate subdomain.
|
|
60
|
+
*/
|
|
61
|
+
export const DEV_SANDBOX = ['allow-scripts', 'allow-same-origin'];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generate the HTML content for the iframe
|
|
65
|
+
*/
|
|
66
|
+
function generateIframeContent(
|
|
67
|
+
image: LoadedImage | null,
|
|
68
|
+
inputs: Record<string, unknown>,
|
|
69
|
+
services: string[],
|
|
70
|
+
baseUrl: string,
|
|
71
|
+
): string {
|
|
72
|
+
const bridgeScript = generateIframeBridgeScript(services);
|
|
73
|
+
|
|
74
|
+
// Generate import map from image dependencies and manifest packages
|
|
75
|
+
const packages = {
|
|
76
|
+
...(image?.dependencies || {}),
|
|
77
|
+
};
|
|
78
|
+
const importMap = generateImportMap(packages);
|
|
79
|
+
|
|
80
|
+
// CSS from image
|
|
81
|
+
const css = image?.css || '';
|
|
82
|
+
|
|
83
|
+
const frameworkConfig = image?.config?.framework || {};
|
|
84
|
+
const preloadUrls = frameworkConfig.preload || [];
|
|
85
|
+
const globals = frameworkConfig.globals || {};
|
|
86
|
+
const globalNames = Object.values(globals);
|
|
87
|
+
const imageModuleUrl = image?.moduleUrl || '';
|
|
88
|
+
|
|
89
|
+
const mountScript = `
|
|
90
|
+
// Run image setup inside the iframe (styling/runtime)
|
|
91
|
+
const imageModuleUrl = ${JSON.stringify(imageModuleUrl)};
|
|
92
|
+
|
|
93
|
+
// Preload framework modules declared by the image (if any)
|
|
94
|
+
const preloadUrls = ${JSON.stringify(preloadUrls)};
|
|
95
|
+
const globalNames = ${JSON.stringify(globalNames)};
|
|
96
|
+
for (let i = 0; i < preloadUrls.length; i++) {
|
|
97
|
+
const url = preloadUrls[i];
|
|
98
|
+
const name = globalNames[i];
|
|
99
|
+
if (!url || !name) continue;
|
|
100
|
+
try {
|
|
101
|
+
const mod = await import(/* webpackIgnore: true */ url);
|
|
102
|
+
window[name] = mod;
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error('[patchwork-iframe] Failed to preload:', url, e);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const root = document.getElementById('root');
|
|
109
|
+
const inputs = window.__PATCHWORK_INPUTS__ || {};
|
|
110
|
+
|
|
111
|
+
if (imageModuleUrl && root) {
|
|
112
|
+
try {
|
|
113
|
+
const img = await import(/* webpackIgnore: true */ imageModuleUrl);
|
|
114
|
+
if (typeof img?.setup === 'function') {
|
|
115
|
+
await img.setup(root);
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error('[patchwork-iframe] Failed to run image setup:', e);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function pickCreateElement(globals) {
|
|
123
|
+
for (const obj of globals) {
|
|
124
|
+
if (obj && typeof obj.createElement === 'function') return obj.createElement.bind(obj);
|
|
125
|
+
if (obj?.default && typeof obj.default.createElement === 'function') return obj.default.createElement.bind(obj.default);
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function pickRenderer(globals) {
|
|
131
|
+
for (const obj of globals) {
|
|
132
|
+
if (obj && typeof obj.createRoot === 'function') {
|
|
133
|
+
return {
|
|
134
|
+
kind: 'root',
|
|
135
|
+
createRoot: obj.createRoot.bind(obj),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (obj && typeof obj.render === 'function') {
|
|
139
|
+
return {
|
|
140
|
+
kind: 'render',
|
|
141
|
+
render: obj.render.bind(obj),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (obj?.default && typeof obj.default.createRoot === 'function') {
|
|
145
|
+
return {
|
|
146
|
+
kind: 'root',
|
|
147
|
+
createRoot: obj.default.createRoot.bind(obj.default),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (obj?.default && typeof obj.default.render === 'function') {
|
|
151
|
+
return {
|
|
152
|
+
kind: 'render',
|
|
153
|
+
render: obj.default.render.bind(obj.default),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getGlobalsFromConfig() {
|
|
161
|
+
const names = ${JSON.stringify(globalNames)};
|
|
162
|
+
return names.map((n) => window[n]).filter(Boolean);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function mountModule(mod) {
|
|
166
|
+
if (!root) throw new Error('No #root element');
|
|
167
|
+
|
|
168
|
+
if (typeof mod?.mount === 'function') {
|
|
169
|
+
const cleanup = await mod.mount(root, inputs);
|
|
170
|
+
if (typeof cleanup === 'function') window.__PATCHWORK_CLEANUP__ = cleanup;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (typeof mod?.render === 'function') {
|
|
175
|
+
const cleanup = await mod.render(root, inputs);
|
|
176
|
+
if (typeof cleanup === 'function') window.__PATCHWORK_CLEANUP__ = cleanup;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const Component = mod?.default;
|
|
181
|
+
if (typeof Component !== 'function') {
|
|
182
|
+
root.textContent = 'Widget did not export a default component.';
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const globals = getGlobalsFromConfig();
|
|
187
|
+
const createElement = pickCreateElement(globals);
|
|
188
|
+
const renderer = pickRenderer(globals);
|
|
189
|
+
|
|
190
|
+
if (createElement && renderer?.kind === 'root') {
|
|
191
|
+
const r = renderer.createRoot(root);
|
|
192
|
+
r.render(createElement(Component, inputs));
|
|
193
|
+
if (typeof r.unmount === 'function') window.__PATCHWORK_CLEANUP__ = () => r.unmount();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (createElement && renderer?.kind === 'render') {
|
|
198
|
+
renderer.render(createElement(Component, inputs), root);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const result = Component(inputs);
|
|
203
|
+
if (result instanceof HTMLElement) {
|
|
204
|
+
root.appendChild(result);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (typeof result === 'string') {
|
|
208
|
+
root.innerHTML = result;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
root.textContent = 'No framework renderer available for this widget.';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Wait for widget code via postMessage (more efficient than inline in srcdoc)
|
|
216
|
+
// We convert relative URLs to absolute so they work inside blob URL context
|
|
217
|
+
window.addEventListener('message', async function handleWidgetCode(event) {
|
|
218
|
+
if (!event.data || event.data.type !== 'widget-code') return;
|
|
219
|
+
window.removeEventListener('message', handleWidgetCode);
|
|
220
|
+
|
|
221
|
+
const widgetCode = event.data.code;
|
|
222
|
+
const origin = event.data.origin || ''; // Parent sends the origin
|
|
223
|
+
|
|
224
|
+
// Convert relative URLs (starting with /) to absolute URLs
|
|
225
|
+
// This is necessary because blob: URLs can't resolve relative imports
|
|
226
|
+
// and srcdoc iframes have null origin
|
|
227
|
+
const absoluteCode = widgetCode.replace(
|
|
228
|
+
/from\\s*["'](\\/[^"']+)["']/g,
|
|
229
|
+
(_, path) => 'from "' + origin + path + '"'
|
|
230
|
+
).replace(
|
|
231
|
+
/import\\s*["'](\\/[^"']+)["']/g,
|
|
232
|
+
(_, path) => 'import "' + origin + path + '"'
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const blob = new Blob([absoluteCode], { type: 'application/javascript' });
|
|
236
|
+
const url = URL.createObjectURL(blob);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const mod = await import(/* webpackIgnore: true */ url);
|
|
240
|
+
await mountModule(mod);
|
|
241
|
+
window.parent.postMessage({ type: 'widget-mounted' }, '*');
|
|
242
|
+
} catch (e) {
|
|
243
|
+
console.error('[patchwork-iframe] Failed to mount widget:', e);
|
|
244
|
+
window.parent.postMessage({ type: 'widget-error', error: e.message }, '*');
|
|
245
|
+
} finally {
|
|
246
|
+
URL.revokeObjectURL(url);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Signal ready to receive widget code
|
|
251
|
+
window.parent.postMessage({ type: 'widget-ready' }, '*');
|
|
252
|
+
|
|
253
|
+
// Set up ResizeObserver to report body size changes to parent
|
|
254
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
255
|
+
for (const entry of entries) {
|
|
256
|
+
const { width, height } = entry.contentRect;
|
|
257
|
+
window.parent.postMessage({
|
|
258
|
+
type: 'widget-resize',
|
|
259
|
+
width: Math.ceil(width),
|
|
260
|
+
height: Math.ceil(height)
|
|
261
|
+
}, '*');
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
resizeObserver.observe(document.body);
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
return `<!DOCTYPE html>
|
|
268
|
+
<html>
|
|
269
|
+
<head>
|
|
270
|
+
<base href="${baseUrl}">
|
|
271
|
+
<meta charset="UTF-8">
|
|
272
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
273
|
+
<style>
|
|
274
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
275
|
+
body { font-family: system-ui, -apple-system, sans-serif; }
|
|
276
|
+
${css}
|
|
277
|
+
</style>
|
|
278
|
+
<script type="importmap">
|
|
279
|
+
${JSON.stringify({ imports: importMap }, null, 2)}
|
|
280
|
+
</script>
|
|
281
|
+
</head>
|
|
282
|
+
<body>
|
|
283
|
+
<div id="root"></div>
|
|
284
|
+
|
|
285
|
+
<!-- Service Bridge -->
|
|
286
|
+
<script>
|
|
287
|
+
${bridgeScript}
|
|
288
|
+
</script>
|
|
289
|
+
|
|
290
|
+
<!-- Widget Inputs -->
|
|
291
|
+
<script>
|
|
292
|
+
window.__PATCHWORK_INPUTS__ = ${JSON.stringify(inputs)};
|
|
293
|
+
</script>
|
|
294
|
+
|
|
295
|
+
<script type="module">
|
|
296
|
+
${mountScript}
|
|
297
|
+
</script>
|
|
298
|
+
</body>
|
|
299
|
+
</html>`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Mount a widget in iframe mode (sandboxed)
|
|
304
|
+
*/
|
|
305
|
+
export async function mountIframe(
|
|
306
|
+
widget: CompiledWidget,
|
|
307
|
+
options: MountOptions,
|
|
308
|
+
image: LoadedImage | null,
|
|
309
|
+
proxy: ServiceProxy,
|
|
310
|
+
): Promise<MountedWidget> {
|
|
311
|
+
const { target, sandbox = DEFAULT_SANDBOX, inputs = {} } = options;
|
|
312
|
+
const mountId = generateMountId();
|
|
313
|
+
|
|
314
|
+
// Create iframe
|
|
315
|
+
const iframe = document.createElement('iframe');
|
|
316
|
+
iframe.id = mountId;
|
|
317
|
+
iframe.className = 'patchwork-widget patchwork-iframe';
|
|
318
|
+
iframe.style.cssText = 'width: 100%; border: none; overflow: hidden;';
|
|
319
|
+
iframe.sandbox.add(...sandbox);
|
|
320
|
+
|
|
321
|
+
// Register with bridge before loading content
|
|
322
|
+
const bridge = getParentBridge(proxy);
|
|
323
|
+
bridge.registerIframe(iframe);
|
|
324
|
+
|
|
325
|
+
// Generate and set iframe content (without widget code)
|
|
326
|
+
// Use window.location.origin as base URL so relative paths like /_local-packages/ resolve correctly
|
|
327
|
+
const services = widget.manifest.services || [];
|
|
328
|
+
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
|
|
329
|
+
const content = generateIframeContent(image, inputs, services, baseUrl);
|
|
330
|
+
iframe.srcdoc = content;
|
|
331
|
+
|
|
332
|
+
// Append to target
|
|
333
|
+
target.appendChild(iframe);
|
|
334
|
+
|
|
335
|
+
// Handle resize messages from iframe
|
|
336
|
+
const handleResize = (event: MessageEvent) => {
|
|
337
|
+
if (event.source !== iframe.contentWindow) return;
|
|
338
|
+
if (event.data?.type === 'widget-resize') {
|
|
339
|
+
const { height } = event.data;
|
|
340
|
+
if (typeof height === 'number' && height > 0) {
|
|
341
|
+
iframe.style.height = `${height}px`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
window.addEventListener('message', handleResize);
|
|
346
|
+
|
|
347
|
+
// Wait for iframe to signal ready, then send widget code
|
|
348
|
+
await new Promise<void>((resolve, reject) => {
|
|
349
|
+
const timeout = setTimeout(() => {
|
|
350
|
+
cleanup();
|
|
351
|
+
reject(new Error('Iframe mount timeout'));
|
|
352
|
+
}, 30000);
|
|
353
|
+
|
|
354
|
+
const handleMessage = (event: MessageEvent) => {
|
|
355
|
+
if (event.source !== iframe.contentWindow) return;
|
|
356
|
+
|
|
357
|
+
if (event.data?.type === 'widget-ready') {
|
|
358
|
+
// Send widget code and origin for URL rewriting
|
|
359
|
+
iframe.contentWindow?.postMessage(
|
|
360
|
+
{ type: 'widget-code', code: widget.code, origin: baseUrl },
|
|
361
|
+
'*',
|
|
362
|
+
);
|
|
363
|
+
} else if (event.data?.type === 'widget-mounted') {
|
|
364
|
+
cleanup();
|
|
365
|
+
resolve();
|
|
366
|
+
} else if (event.data?.type === 'widget-error') {
|
|
367
|
+
cleanup();
|
|
368
|
+
reject(new Error(event.data.error || 'Widget mount failed'));
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const cleanup = () => {
|
|
373
|
+
clearTimeout(timeout);
|
|
374
|
+
window.removeEventListener('message', handleMessage);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
window.addEventListener('message', handleMessage);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Create unmount function
|
|
381
|
+
const unmount = () => {
|
|
382
|
+
window.removeEventListener('message', handleResize);
|
|
383
|
+
bridge.unregisterIframe(iframe);
|
|
384
|
+
iframe.remove();
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
id: mountId,
|
|
389
|
+
widget,
|
|
390
|
+
mode: 'iframe',
|
|
391
|
+
target,
|
|
392
|
+
iframe,
|
|
393
|
+
inputs,
|
|
394
|
+
sandbox,
|
|
395
|
+
unmount,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Hot reload an iframe widget
|
|
401
|
+
*/
|
|
402
|
+
export async function reloadIframe(
|
|
403
|
+
mounted: MountedWidget,
|
|
404
|
+
widget: CompiledWidget,
|
|
405
|
+
image: LoadedImage | null,
|
|
406
|
+
proxy: ServiceProxy,
|
|
407
|
+
): Promise<MountedWidget> {
|
|
408
|
+
// Unmount existing
|
|
409
|
+
mounted.unmount();
|
|
410
|
+
|
|
411
|
+
// Remount with new widget
|
|
412
|
+
return mountIframe(
|
|
413
|
+
widget,
|
|
414
|
+
{
|
|
415
|
+
target: mounted.target,
|
|
416
|
+
mode: 'iframe',
|
|
417
|
+
sandbox: mounted.sandbox,
|
|
418
|
+
inputs: mounted.inputs,
|
|
419
|
+
},
|
|
420
|
+
image,
|
|
421
|
+
proxy,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Dispose the shared bridge (call on app shutdown)
|
|
427
|
+
*/
|
|
428
|
+
export function disposeIframeBridge(): void {
|
|
429
|
+
if (sharedBridge) {
|
|
430
|
+
sharedBridge.dispose();
|
|
431
|
+
sharedBridge = null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { mountEmbedded, reloadEmbedded } from './embedded.js';
|
|
2
|
+
export {
|
|
3
|
+
mountIframe,
|
|
4
|
+
reloadIframe,
|
|
5
|
+
disposeIframeBridge,
|
|
6
|
+
DEV_SANDBOX,
|
|
7
|
+
} from './iframe.js';
|
|
8
|
+
export {
|
|
9
|
+
createHttpServiceProxy,
|
|
10
|
+
createFieldAccessProxy,
|
|
11
|
+
generateNamespaceGlobals,
|
|
12
|
+
injectNamespaceGlobals,
|
|
13
|
+
removeNamespaceGlobals,
|
|
14
|
+
extractNamespaces,
|
|
15
|
+
ParentBridge,
|
|
16
|
+
createIframeServiceProxy,
|
|
17
|
+
generateIframeBridgeScript,
|
|
18
|
+
} from './bridge.js';
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for Patchwork compiler types
|
|
3
|
+
*
|
|
4
|
+
* These schemas validate:
|
|
5
|
+
* - ImageConfig from package.json patchwork field
|
|
6
|
+
* - Widget manifests
|
|
7
|
+
* - Input specifications
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
|
|
12
|
+
// Platform schema
|
|
13
|
+
export const PlatformSchema = z.enum(['browser', 'cli']);
|
|
14
|
+
export type Platform = z.infer<typeof PlatformSchema>;
|
|
15
|
+
|
|
16
|
+
// esbuild configuration schema
|
|
17
|
+
export const EsbuildConfigSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
target: z.string().optional(),
|
|
20
|
+
format: z.enum(['esm', 'cjs', 'iife']).optional(),
|
|
21
|
+
jsx: z.enum(['automatic', 'transform', 'preserve']).optional(),
|
|
22
|
+
jsxFactory: z.string().optional(),
|
|
23
|
+
jsxFragment: z.string().optional(),
|
|
24
|
+
})
|
|
25
|
+
.strict()
|
|
26
|
+
.optional();
|
|
27
|
+
|
|
28
|
+
export type EsbuildConfig = z.infer<typeof EsbuildConfigSchema>;
|
|
29
|
+
|
|
30
|
+
// Framework configuration - specifies globals and CDN URLs for framework deps
|
|
31
|
+
export const FrameworkConfigSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
// Map of package names to window global names (e.g., { react: 'React' })
|
|
34
|
+
globals: z.record(z.string(), z.string()).optional(),
|
|
35
|
+
// CDN URLs to preload before widget execution
|
|
36
|
+
preload: z.array(z.string()).optional(),
|
|
37
|
+
// Dependency version overrides for CDN packages (e.g., { react: '18' })
|
|
38
|
+
deps: z.record(z.string(), z.string()).optional(),
|
|
39
|
+
})
|
|
40
|
+
.strict()
|
|
41
|
+
.optional();
|
|
42
|
+
|
|
43
|
+
export type FrameworkConfig = z.infer<typeof FrameworkConfigSchema>;
|
|
44
|
+
|
|
45
|
+
// Aliases schema - maps path patterns to target packages
|
|
46
|
+
export const AliasesSchema = z.record(z.string(), z.string()).optional();
|
|
47
|
+
|
|
48
|
+
export type Aliases = z.infer<typeof AliasesSchema>;
|
|
49
|
+
|
|
50
|
+
// Dependencies schema - maps package names to version specifiers
|
|
51
|
+
export const DependenciesSchema = z.record(z.string(), z.string()).optional();
|
|
52
|
+
|
|
53
|
+
// ImageConfig schema - validates package.json patchwork field
|
|
54
|
+
export const ImageConfigSchema = z
|
|
55
|
+
.object({
|
|
56
|
+
platform: PlatformSchema,
|
|
57
|
+
dependencies: DependenciesSchema,
|
|
58
|
+
esbuild: EsbuildConfigSchema,
|
|
59
|
+
framework: FrameworkConfigSchema,
|
|
60
|
+
aliases: AliasesSchema,
|
|
61
|
+
})
|
|
62
|
+
.strict();
|
|
63
|
+
|
|
64
|
+
export type ImageConfig = z.infer<typeof ImageConfigSchema>;
|
|
65
|
+
|
|
66
|
+
// Input specification schema
|
|
67
|
+
export const InputSpecSchema = z.object({
|
|
68
|
+
type: z.enum(['string', 'number', 'boolean', 'object', 'array']),
|
|
69
|
+
default: z.unknown().optional(),
|
|
70
|
+
required: z.boolean().optional(),
|
|
71
|
+
description: z.string().optional(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export type InputSpec = z.infer<typeof InputSpecSchema>;
|
|
75
|
+
|
|
76
|
+
// Widget manifest schema
|
|
77
|
+
export const ManifestSchema = z.object({
|
|
78
|
+
name: z.string(),
|
|
79
|
+
version: z.string(),
|
|
80
|
+
description: z.string().optional(),
|
|
81
|
+
platform: PlatformSchema,
|
|
82
|
+
image: z.string(),
|
|
83
|
+
inputs: z.record(z.string(), InputSpecSchema).optional(),
|
|
84
|
+
services: z.array(z.string()).optional(),
|
|
85
|
+
packages: z.record(z.string(), z.string()).optional(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export type Manifest = z.infer<typeof ManifestSchema>;
|
|
89
|
+
|
|
90
|
+
// Compile options schema
|
|
91
|
+
export const CompileOptionsSchema = z
|
|
92
|
+
.object({
|
|
93
|
+
typescript: z.boolean().optional(),
|
|
94
|
+
})
|
|
95
|
+
.strict()
|
|
96
|
+
.optional();
|
|
97
|
+
|
|
98
|
+
export type CompileOptions = z.infer<typeof CompileOptionsSchema>;
|
|
99
|
+
|
|
100
|
+
// Mount mode schema
|
|
101
|
+
export const MountModeSchema = z.enum(['embedded', 'iframe']);
|
|
102
|
+
export type MountMode = z.infer<typeof MountModeSchema>;
|
|
103
|
+
|
|
104
|
+
// Mount options schema
|
|
105
|
+
export const MountOptionsSchema = z.object({
|
|
106
|
+
target: z.custom<HTMLElement>((v) => v instanceof HTMLElement, {
|
|
107
|
+
message: 'Expected HTMLElement',
|
|
108
|
+
}),
|
|
109
|
+
mode: MountModeSchema,
|
|
110
|
+
sandbox: z.array(z.string()).optional(),
|
|
111
|
+
inputs: z.record(z.string(), z.unknown()).optional(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export type MountOptions = z.infer<typeof MountOptionsSchema>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse and validate ImageConfig from package.json patchwork field
|
|
118
|
+
*
|
|
119
|
+
* @param data - Raw data from package.json patchwork field
|
|
120
|
+
* @returns Validated ImageConfig
|
|
121
|
+
* @throws z.ZodError if validation fails
|
|
122
|
+
*/
|
|
123
|
+
export function parseImageConfig(data: unknown): ImageConfig {
|
|
124
|
+
return ImageConfigSchema.parse(data);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Safely parse ImageConfig, returning null on failure
|
|
129
|
+
*/
|
|
130
|
+
export function safeParseImageConfig(data: unknown): ImageConfig | null {
|
|
131
|
+
const result = ImageConfigSchema.safeParse(data);
|
|
132
|
+
return result.success ? result.data : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse and validate widget manifest
|
|
137
|
+
*/
|
|
138
|
+
export function parseManifest(data: unknown): Manifest {
|
|
139
|
+
return ManifestSchema.parse(data);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Safely parse manifest, returning null on failure
|
|
144
|
+
*/
|
|
145
|
+
export function safeParseManifest(data: unknown): Manifest | null {
|
|
146
|
+
const result = ManifestSchema.safeParse(data);
|
|
147
|
+
return result.success ? result.data : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Default ImageConfig for fallback
|
|
151
|
+
export const DEFAULT_IMAGE_CONFIG: ImageConfig = {
|
|
152
|
+
platform: 'browser',
|
|
153
|
+
esbuild: {
|
|
154
|
+
target: 'es2020',
|
|
155
|
+
format: 'esm',
|
|
156
|
+
jsx: 'automatic',
|
|
157
|
+
},
|
|
158
|
+
framework: {},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Default CLI ImageConfig for fallback
|
|
162
|
+
export const DEFAULT_CLI_IMAGE_CONFIG: ImageConfig = {
|
|
163
|
+
platform: 'cli',
|
|
164
|
+
esbuild: {
|
|
165
|
+
target: 'node20',
|
|
166
|
+
format: 'esm',
|
|
167
|
+
jsx: 'automatic',
|
|
168
|
+
},
|
|
169
|
+
};
|