@colixsystems/widget-sdk 0.37.0 → 0.39.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.
@@ -0,0 +1,255 @@
1
+ // REQ-WSDK-DEVKIT v2 (sc-1027): shared shim-source generator used by BOTH
2
+ // `widgetLoader.buildShims()` (Studio runtime, blob URLs) AND the dev server's
3
+ // `/shim/<spec>` endpoint (when serving siblings of a multi-file widget).
4
+ //
5
+ // Both code paths produce ESM that re-exports the matching key out of
6
+ // `globalThis.__appstudioWidgetHost__` — populated by the Studio's `buildShims`
7
+ // before any widget bundle is evaluated. Keeping the shim sources in one place
8
+ // means a vetted-package addition lands once and both paths see it.
9
+ //
10
+ // Pure JS, no runtime deps — safe to import from the SDK dev server (Node)
11
+ // and from the Studio loader (browser).
12
+ //
13
+ // LIMITATION (v1): for SIBLING files served by the dev server's `/file/<rel>`
14
+ // endpoint, only the specifiers with a statically-known named-export list are
15
+ // shimmable through `/shim/<spec>`: `react`, `react/jsx-runtime`,
16
+ // `react/jsx-dev-runtime`, and `@colixsystems/widget-sdk`. Any other vetted
17
+ // package (`lucide-react-native`, `react-native-svg`, `date-fns`, ...) must be
18
+ // imported from the widget's ENTRY (where the Studio's blob-shim path
19
+ // runtime-enumerates the host's namespace). Imports of these packages from a
20
+ // sibling .jsx file return `422` from the dev server with a clear message.
21
+
22
+ // React named exports the Studio re-exposes through the shim. Mirrors the
23
+ // browser's `react` named-export surface. Adding a new hook in a React minor
24
+ // release means appending it here.
25
+ export const REACT_NAMED_EXPORTS = [
26
+ "Children",
27
+ "Component",
28
+ "Fragment",
29
+ "Profiler",
30
+ "PureComponent",
31
+ "StrictMode",
32
+ "Suspense",
33
+ "cloneElement",
34
+ "createContext",
35
+ "createElement",
36
+ "createFactory",
37
+ "createRef",
38
+ "forwardRef",
39
+ "isValidElement",
40
+ "lazy",
41
+ "memo",
42
+ "startTransition",
43
+ "useCallback",
44
+ "useContext",
45
+ "useDebugValue",
46
+ "useDeferredValue",
47
+ "useEffect",
48
+ "useId",
49
+ "useImperativeHandle",
50
+ "useInsertionEffect",
51
+ "useLayoutEffect",
52
+ "useMemo",
53
+ "useReducer",
54
+ "useRef",
55
+ "useState",
56
+ "useSyncExternalStore",
57
+ "useTransition",
58
+ "version",
59
+ ];
60
+
61
+ // React's automatic JSX runtime: jsx / jsxs / Fragment, plus the dev variant.
62
+ export const REACT_JSX_RUNTIME_EXPORTS = ["jsx", "jsxs", "Fragment", "jsxDEV"];
63
+
64
+ // REQ-WSDK-PLATFORM bundle (sc-1064): the canonical list of bare specifiers the
65
+ // runtime web host RESOLVES for a loaded widget — i.e. the ones the Studio's
66
+ // `widgetLoader.buildShims()` re-exports from `globalThis.__appstudioWidgetHost__`
67
+ // rather than letting a bundle inline a second copy. The web-entry bundler
68
+ // (`webbundle.bundleWebEntry`, used by both the publish packer and the dev
69
+ // server) marks EXACTLY these `external`, so everything else the entry imports
70
+ // (`react-leaflet`, `leaflet`, CSS, PNG assets, …) gets inlined.
71
+ //
72
+ // This is the SINGLE source of truth (CLAUDE.md §3). It MUST stay in lockstep
73
+ // with the frontend's `widgetLoader._hostVettedModules()` + the core React/SDK
74
+ // shims it builds. Two backstops enforce the equality: (1) this list's
75
+ // contents are pinned by the SDK-side importable test
76
+ // (`packages/widget-sdk/src/__tests__/host-externals.test.js`), and (2) the
77
+ // loader's own module-load guard in `frontend/src/services/widgetLoader.js`
78
+ // throws at import time if `hostShimmedSpecifiers()` drifts from
79
+ // `hostExternalSpecifiers()` — the loader's shimmed set can't be asserted from
80
+ // a Node test (it imports `react-dom` etc.), so that equality is enforced by
81
+ // the load-time guard rather than a separate frontend test.
82
+ //
83
+ // Why each entry is host-resolved (NOT bundled):
84
+ // * `react`, `react/jsx-runtime`, `react/jsx-dev-runtime` — the widget MUST
85
+ // share the host's ONE React instance (hooks + context); a bundled second
86
+ // React breaks both.
87
+ // * `react-dom`, `react-dom/client` — `react-leaflet`'s Popup/Pane render
88
+ // through `createPortal` from `react-dom`. A bundled second react-dom
89
+ // paired with the host's external React is a mismatched renderer and
90
+ // crashes. The Studio is a React-DOM app, so it shims its own react-dom.
91
+ // On React 19 the two specifiers expose DIFFERENT surfaces — createPortal /
92
+ // flushSync / version live on `react-dom`, createRoot / hydrateRoot on
93
+ // `react-dom/client` — so the loader stashes + shims each from its own real
94
+ // host namespace (sc-1064).
95
+ // * `@colixsystems/widget-sdk` — the widget must route hooks through the SAME
96
+ // SDK instance the host built (datastore/theme/navigation live there).
97
+ // * `lucide-react-native`, `react-native-svg`, `date-fns` — vetted packages
98
+ // the host already bundles + shims (`_hostVettedModules`); sharing the
99
+ // host copy keeps icons/SVG on the host's react-native-web instance.
100
+ const HOST_EXTERNAL_SPECIFIERS = [
101
+ "react",
102
+ "react/jsx-runtime",
103
+ "react/jsx-dev-runtime",
104
+ "react-dom",
105
+ "react-dom/client",
106
+ "@colixsystems/widget-sdk",
107
+ "lucide-react-native",
108
+ "react-native-svg",
109
+ "date-fns",
110
+ ];
111
+
112
+ /**
113
+ * The canonical "host-resolved at runtime" bare-specifier set — the externals
114
+ * the web-entry bundler must NOT inline. Returns a fresh array each call so a
115
+ * caller can't mutate the source of truth.
116
+ *
117
+ * @returns {string[]}
118
+ */
119
+ export function hostExternalSpecifiers() {
120
+ return [...HOST_EXTERNAL_SPECIFIERS];
121
+ }
122
+
123
+ function reactShimSrc() {
124
+ const host = "globalThis.__appstudioWidgetHost__.React";
125
+ const named = REACT_NAMED_EXPORTS.map(
126
+ (n) => `export const ${n} = (${host})[${JSON.stringify(n)}];`,
127
+ ).join("\n");
128
+ return `const __host = ${host};\nexport default __host;\n${named}\n`;
129
+ }
130
+
131
+ function reactJsxRuntimeShimSrc() {
132
+ const host = "globalThis.__appstudioWidgetHost__.ReactJSXRuntime";
133
+ const named = REACT_JSX_RUNTIME_EXPORTS.map(
134
+ (n) => `export const ${n} = (${host})[${JSON.stringify(n)}];`,
135
+ ).join("\n");
136
+ return named;
137
+ }
138
+
139
+ // react-dom named exports a bundled widget might reach through the host shim.
140
+ // `react-leaflet`'s Popup/Pane/SVGOverlay use `createPortal`; `flushSync`
141
+ // rounds out the surface react-leaflet may touch. These are the names that
142
+ // genuinely EXIST on the `react-dom` namespace in React 19 — `createRoot` /
143
+ // `hydrateRoot` moved to `react-dom/client`, and `render` /
144
+ // `unmountComponentAtNode` / `findDOMNode` were removed entirely, so listing
145
+ // them here would re-export `undefined` (sc-1064). `version` is always present.
146
+ // Mirrors REACT_NAMED_EXPORTS' hand-list-of-the-named-surface approach (a blob
147
+ // module can't `export *`).
148
+ export const REACT_DOM_NAMED_EXPORTS = ["createPortal", "flushSync", "version"];
149
+
150
+ // React 19: `createRoot` / `hydrateRoot` live ONLY on `react-dom/client`. The
151
+ // `react-dom/client` shim sources these from the host's REAL `react-dom/client`
152
+ // namespace (stashed as `ReactDOMClient`), NOT from the `react-dom` namespace
153
+ // where they are `undefined` on v19. `version` is present on both.
154
+ export const REACT_DOM_CLIENT_NAMED_EXPORTS = [
155
+ "createRoot",
156
+ "hydrateRoot",
157
+ "version",
158
+ ];
159
+
160
+ function reactDomShimSrc() {
161
+ const host = "globalThis.__appstudioWidgetHost__.ReactDOM";
162
+ const named = REACT_DOM_NAMED_EXPORTS.map(
163
+ (n) => `export const ${n} = (${host})[${JSON.stringify(n)}];`,
164
+ ).join("\n");
165
+ return `const __host = ${host};\nexport default __host;\n${named}\n`;
166
+ }
167
+
168
+ function reactDomClientShimSrc() {
169
+ const host = "globalThis.__appstudioWidgetHost__.ReactDOMClient";
170
+ const named = REACT_DOM_CLIENT_NAMED_EXPORTS.map(
171
+ (n) => `export const ${n} = (${host})[${JSON.stringify(n)}];`,
172
+ ).join("\n");
173
+ return `const __host = ${host};\nexport default __host;\n${named}\n`;
174
+ }
175
+
176
+ function sdkShimSrc(sdkNamedExports) {
177
+ const host = "globalThis.__appstudioWidgetHost__.sdk";
178
+ const names = (Array.isArray(sdkNamedExports) ? sdkNamedExports : []).filter(
179
+ (n) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(n),
180
+ );
181
+ const named = names
182
+ .map((n) => `export const ${n} = (${host})[${JSON.stringify(n)}];`)
183
+ .join("\n");
184
+ return `const __host = ${host};\nexport default __host;\n${named}\n`;
185
+ }
186
+
187
+ /**
188
+ * Build a shim ESM source for a vetted bare specifier with a statically-known
189
+ * named-export shape. Returns null when the specifier isn't shimmable from a
190
+ * sibling file — the caller surfaces this as a clear error.
191
+ *
192
+ * @param {string} specifier bare import specifier (e.g. "react")
193
+ * @param {object} [opts]
194
+ * @param {string[]} [opts.sdkExports] named-export list for
195
+ * "@colixsystems/widget-sdk" — caller passes this since the dev server
196
+ * doesn't statically load the SDK.
197
+ * @returns {string | null}
198
+ */
199
+ export function getShimSource(specifier, opts = {}) {
200
+ switch (specifier) {
201
+ case "react":
202
+ return reactShimSrc();
203
+ case "react/jsx-runtime":
204
+ case "react/jsx-dev-runtime":
205
+ return reactJsxRuntimeShimSrc();
206
+ case "react-dom":
207
+ return reactDomShimSrc();
208
+ case "react-dom/client":
209
+ return reactDomClientShimSrc();
210
+ case "@colixsystems/widget-sdk":
211
+ return sdkShimSrc(opts.sdkExports);
212
+ default:
213
+ return null;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * The set of bare specifiers a SIBLING file may import — i.e. the keys
219
+ * `getShimSource` recognises. Anything outside this set must move to the
220
+ * entry, where the Studio's runtime blob-shim path enumerates the full
221
+ * `CONTRACT.allowedBareImports` surface (REQ-WSDK-PLATFORM §3.4).
222
+ *
223
+ * @returns {string[]}
224
+ */
225
+ export function shimmableSpecifiersForSiblings() {
226
+ return [
227
+ "react",
228
+ "react/jsx-runtime",
229
+ "react/jsx-dev-runtime",
230
+ "@colixsystems/widget-sdk",
231
+ ];
232
+ }
233
+
234
+ /**
235
+ * Stable URL-safe slug for a bare specifier — used as the path segment under
236
+ * `/shim/` so a specifier like `react/jsx-runtime` doesn't need URL encoding
237
+ * on the wire. Reversed by `shimSpecifierFromSlug` on the server.
238
+ *
239
+ * @param {string} specifier
240
+ * @returns {string}
241
+ */
242
+ export function shimSlugFor(specifier) {
243
+ return specifier.replace(/\//g, "__").replace(/@/g, "_at_");
244
+ }
245
+
246
+ /**
247
+ * Reverse of `shimSlugFor`. Returns the bare specifier; the caller validates
248
+ * it against `shimmableSpecifiersForSiblings()` before serving.
249
+ *
250
+ * @param {string} slug
251
+ * @returns {string}
252
+ */
253
+ export function shimSpecifierFromSlug(slug) {
254
+ return slug.replace(/_at_/g, "@").replace(/__/g, "/");
255
+ }