@colixsystems/widget-sdk 0.44.0 → 0.45.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/README.md +55 -1
- package/dist/contract.cjs +11 -2
- package/dist/contract.js +11 -2
- package/dist/dev-shims.js +6 -0
- package/dist/linter.cjs +59 -0
- package/dist/linter.js +59 -0
- package/dist/lucideIconNames.cjs +1462 -0
- package/dist/lucideIconNames.js +1460 -0
- package/dist/webbundle.js +23 -0
- package/package.json +7 -4
- package/src/dev-shims.js +261 -0
- package/src/webbundle.js +148 -0
package/dist/webbundle.js
CHANGED
|
@@ -108,7 +108,30 @@ export async function bundleWebEntry({
|
|
|
108
108
|
jsx: "automatic",
|
|
109
109
|
// Leave the host-resolved set as bare `import`s; the Studio loader rewrites
|
|
110
110
|
// them to blob shims at runtime (and Metro resolves them in the export).
|
|
111
|
+
// `react-native` is in that set (hostExternalSpecifiers): a vetted RN
|
|
112
|
+
// package (react-native-gesture-handler, react-native-reanimated, …) is
|
|
113
|
+
// INLINED, but its internal `import "react-native"` is left bare and
|
|
114
|
+
// resolves to the host's single react-native-web instance — the same
|
|
115
|
+
// shared-instance model as react/react-dom, far smaller than inlining a
|
|
116
|
+
// second copy of react-native-web into every bundle.
|
|
111
117
|
external,
|
|
118
|
+
// sc-1265: resolve the WEB build of cross-platform packages. RN packages
|
|
119
|
+
// ship `.web.js` platform variants and lean on the `browser` field /
|
|
120
|
+
// export condition; without these esbuild would pull their native entry and
|
|
121
|
+
// bundle React Native internals that break in the browser.
|
|
122
|
+
resolveExtensions: [
|
|
123
|
+
".web.js",
|
|
124
|
+
".web.jsx",
|
|
125
|
+
".web.ts",
|
|
126
|
+
".web.tsx",
|
|
127
|
+
".js",
|
|
128
|
+
".jsx",
|
|
129
|
+
".ts",
|
|
130
|
+
".tsx",
|
|
131
|
+
".json",
|
|
132
|
+
],
|
|
133
|
+
mainFields: ["browser", "module", "main"],
|
|
134
|
+
conditions: ["browser"],
|
|
112
135
|
loader: { ".png": "dataurl", ".jpg": "dataurl", ".gif": "dataurl" },
|
|
113
136
|
plugins: [cssInjectPlugin()],
|
|
114
137
|
// esbuild walks up from the entry for node_modules by default; `nodePaths`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"require": "./dist/contract.cjs",
|
|
28
28
|
"import": "./dist/contract.js",
|
|
29
29
|
"default": "./dist/contract.cjs"
|
|
30
|
-
}
|
|
30
|
+
},
|
|
31
|
+
"./src/webbundle.js": "./src/webbundle.js"
|
|
31
32
|
},
|
|
32
33
|
"bin": {
|
|
33
34
|
"appstudio-widget": "./dist/cli.js"
|
|
@@ -35,11 +36,13 @@
|
|
|
35
36
|
"files": [
|
|
36
37
|
"dist",
|
|
37
38
|
"README.md",
|
|
38
|
-
"LICENSE"
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"src/webbundle.js",
|
|
41
|
+
"src/dev-shims.js"
|
|
39
42
|
],
|
|
40
43
|
"scripts": {
|
|
41
44
|
"build": "node scripts/build.js",
|
|
42
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js"
|
|
45
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js"
|
|
43
46
|
},
|
|
44
47
|
"engines": {
|
|
45
48
|
"node": ">=18"
|
package/src/dev-shims.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
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
|
+
// sc-1265: a bundled widget (and any vetted RN package it inlines, e.g.
|
|
111
|
+
// react-native-gesture-handler) imports `react-native`; the host resolves it
|
|
112
|
+
// to its OWN single react-native-web instance through a blob shim, so the
|
|
113
|
+
// bundle shares one RN-web (StyleSheet/context) instead of inlining a second
|
|
114
|
+
// copy. On native, Metro resolves the real react-native in the export.
|
|
115
|
+
"react-native",
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The canonical "host-resolved at runtime" bare-specifier set — the externals
|
|
120
|
+
* the web-entry bundler must NOT inline. Returns a fresh array each call so a
|
|
121
|
+
* caller can't mutate the source of truth.
|
|
122
|
+
*
|
|
123
|
+
* @returns {string[]}
|
|
124
|
+
*/
|
|
125
|
+
export function hostExternalSpecifiers() {
|
|
126
|
+
return [...HOST_EXTERNAL_SPECIFIERS];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function reactShimSrc() {
|
|
130
|
+
const host = "globalThis.__appstudioWidgetHost__.React";
|
|
131
|
+
const named = REACT_NAMED_EXPORTS.map(
|
|
132
|
+
(n) => `export const ${n} = (${host})[${JSON.stringify(n)}];`,
|
|
133
|
+
).join("\n");
|
|
134
|
+
return `const __host = ${host};\nexport default __host;\n${named}\n`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function reactJsxRuntimeShimSrc() {
|
|
138
|
+
const host = "globalThis.__appstudioWidgetHost__.ReactJSXRuntime";
|
|
139
|
+
const named = REACT_JSX_RUNTIME_EXPORTS.map(
|
|
140
|
+
(n) => `export const ${n} = (${host})[${JSON.stringify(n)}];`,
|
|
141
|
+
).join("\n");
|
|
142
|
+
return named;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// react-dom named exports a bundled widget might reach through the host shim.
|
|
146
|
+
// `react-leaflet`'s Popup/Pane/SVGOverlay use `createPortal`; `flushSync`
|
|
147
|
+
// rounds out the surface react-leaflet may touch. These are the names that
|
|
148
|
+
// genuinely EXIST on the `react-dom` namespace in React 19 — `createRoot` /
|
|
149
|
+
// `hydrateRoot` moved to `react-dom/client`, and `render` /
|
|
150
|
+
// `unmountComponentAtNode` / `findDOMNode` were removed entirely, so listing
|
|
151
|
+
// them here would re-export `undefined` (sc-1064). `version` is always present.
|
|
152
|
+
// Mirrors REACT_NAMED_EXPORTS' hand-list-of-the-named-surface approach (a blob
|
|
153
|
+
// module can't `export *`).
|
|
154
|
+
export const REACT_DOM_NAMED_EXPORTS = ["createPortal", "flushSync", "version"];
|
|
155
|
+
|
|
156
|
+
// React 19: `createRoot` / `hydrateRoot` live ONLY on `react-dom/client`. The
|
|
157
|
+
// `react-dom/client` shim sources these from the host's REAL `react-dom/client`
|
|
158
|
+
// namespace (stashed as `ReactDOMClient`), NOT from the `react-dom` namespace
|
|
159
|
+
// where they are `undefined` on v19. `version` is present on both.
|
|
160
|
+
export const REACT_DOM_CLIENT_NAMED_EXPORTS = [
|
|
161
|
+
"createRoot",
|
|
162
|
+
"hydrateRoot",
|
|
163
|
+
"version",
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
function reactDomShimSrc() {
|
|
167
|
+
const host = "globalThis.__appstudioWidgetHost__.ReactDOM";
|
|
168
|
+
const named = REACT_DOM_NAMED_EXPORTS.map(
|
|
169
|
+
(n) => `export const ${n} = (${host})[${JSON.stringify(n)}];`,
|
|
170
|
+
).join("\n");
|
|
171
|
+
return `const __host = ${host};\nexport default __host;\n${named}\n`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function reactDomClientShimSrc() {
|
|
175
|
+
const host = "globalThis.__appstudioWidgetHost__.ReactDOMClient";
|
|
176
|
+
const named = REACT_DOM_CLIENT_NAMED_EXPORTS.map(
|
|
177
|
+
(n) => `export const ${n} = (${host})[${JSON.stringify(n)}];`,
|
|
178
|
+
).join("\n");
|
|
179
|
+
return `const __host = ${host};\nexport default __host;\n${named}\n`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sdkShimSrc(sdkNamedExports) {
|
|
183
|
+
const host = "globalThis.__appstudioWidgetHost__.sdk";
|
|
184
|
+
const names = (Array.isArray(sdkNamedExports) ? sdkNamedExports : []).filter(
|
|
185
|
+
(n) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(n),
|
|
186
|
+
);
|
|
187
|
+
const named = names
|
|
188
|
+
.map((n) => `export const ${n} = (${host})[${JSON.stringify(n)}];`)
|
|
189
|
+
.join("\n");
|
|
190
|
+
return `const __host = ${host};\nexport default __host;\n${named}\n`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Build a shim ESM source for a vetted bare specifier with a statically-known
|
|
195
|
+
* named-export shape. Returns null when the specifier isn't shimmable from a
|
|
196
|
+
* sibling file — the caller surfaces this as a clear error.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} specifier bare import specifier (e.g. "react")
|
|
199
|
+
* @param {object} [opts]
|
|
200
|
+
* @param {string[]} [opts.sdkExports] named-export list for
|
|
201
|
+
* "@colixsystems/widget-sdk" — caller passes this since the dev server
|
|
202
|
+
* doesn't statically load the SDK.
|
|
203
|
+
* @returns {string | null}
|
|
204
|
+
*/
|
|
205
|
+
export function getShimSource(specifier, opts = {}) {
|
|
206
|
+
switch (specifier) {
|
|
207
|
+
case "react":
|
|
208
|
+
return reactShimSrc();
|
|
209
|
+
case "react/jsx-runtime":
|
|
210
|
+
case "react/jsx-dev-runtime":
|
|
211
|
+
return reactJsxRuntimeShimSrc();
|
|
212
|
+
case "react-dom":
|
|
213
|
+
return reactDomShimSrc();
|
|
214
|
+
case "react-dom/client":
|
|
215
|
+
return reactDomClientShimSrc();
|
|
216
|
+
case "@colixsystems/widget-sdk":
|
|
217
|
+
return sdkShimSrc(opts.sdkExports);
|
|
218
|
+
default:
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The set of bare specifiers a SIBLING file may import — i.e. the keys
|
|
225
|
+
* `getShimSource` recognises. Anything outside this set must move to the
|
|
226
|
+
* entry, where the Studio's runtime blob-shim path enumerates the full
|
|
227
|
+
* `CONTRACT.allowedBareImports` surface (REQ-WSDK-PLATFORM §3.4).
|
|
228
|
+
*
|
|
229
|
+
* @returns {string[]}
|
|
230
|
+
*/
|
|
231
|
+
export function shimmableSpecifiersForSiblings() {
|
|
232
|
+
return [
|
|
233
|
+
"react",
|
|
234
|
+
"react/jsx-runtime",
|
|
235
|
+
"react/jsx-dev-runtime",
|
|
236
|
+
"@colixsystems/widget-sdk",
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Stable URL-safe slug for a bare specifier — used as the path segment under
|
|
242
|
+
* `/shim/` so a specifier like `react/jsx-runtime` doesn't need URL encoding
|
|
243
|
+
* on the wire. Reversed by `shimSpecifierFromSlug` on the server.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} specifier
|
|
246
|
+
* @returns {string}
|
|
247
|
+
*/
|
|
248
|
+
export function shimSlugFor(specifier) {
|
|
249
|
+
return specifier.replace(/\//g, "__").replace(/@/g, "_at_");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Reverse of `shimSlugFor`. Returns the bare specifier; the caller validates
|
|
254
|
+
* it against `shimmableSpecifiersForSiblings()` before serving.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} slug
|
|
257
|
+
* @returns {string}
|
|
258
|
+
*/
|
|
259
|
+
export function shimSpecifierFromSlug(slug) {
|
|
260
|
+
return slug.replace(/_at_/g, "@").replace(/__/g, "/");
|
|
261
|
+
}
|
package/src/webbundle.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// REQ-WSDK-PLATFORM bundle (sc-1064): the SINGLE web-entry bundler shared by the
|
|
2
|
+
// publish packer (`scripts/pack-first-party-widget.mjs`) and the dev server
|
|
3
|
+
// (`devserver.js`). Both paths feed a widget's WEB entry through this so what
|
|
4
|
+
// you `dev` is byte-shape-identical to what ships (CLAUDE.md §3, one source).
|
|
5
|
+
//
|
|
6
|
+
// Why a bundler at all (vs. the sucrase transpile the native entry still uses):
|
|
7
|
+
// the Studio's runtime loader resolves bare specifiers only for the host's OWN
|
|
8
|
+
// shimmed set (`dev-shims.hostExternalSpecifiers()` — react family, react-dom,
|
|
9
|
+
// the SDK, and a few vetted packages). A transpile-only web entry leaves
|
|
10
|
+
// `import … from "react-leaflet"` / `"leaflet"` / `"leaflet/dist/leaflet.css"`
|
|
11
|
+
// / the marker PNGs bare, and the loader's `findUnresolvedBareImports` guard
|
|
12
|
+
// rejects the bundle ("unresolvable bare imports: react-leaflet, leaflet, …").
|
|
13
|
+
// esbuild inlines all of those while leaving the host-resolved set external.
|
|
14
|
+
//
|
|
15
|
+
// What's externalised vs. inlined:
|
|
16
|
+
// * external = `hostExternalSpecifiers()` — the host injects its OWN React,
|
|
17
|
+
// react-dom, SDK, and vetted packages at runtime; inlining a second copy of
|
|
18
|
+
// React/react-dom would break the shared-instance invariant (mismatched
|
|
19
|
+
// renderer). These stay as bare `import`s the loader rewrites to blob shims.
|
|
20
|
+
// * inlined = everything else: `react-leaflet`, `leaflet`, the marker PNGs
|
|
21
|
+
// (→ data URLs), the `leaflet/dist/leaflet.css` side-effect import (→ a JS
|
|
22
|
+
// module that injects a <style> at runtime).
|
|
23
|
+
//
|
|
24
|
+
// esbuild is a LAZY, optional dependency — imported only when this module's
|
|
25
|
+
// `bundleWebEntry` runs (the `dev`/pack paths), never by the published SDK
|
|
26
|
+
// runtime. Same pattern as `loadTransform()`'s `sucrase` import in devserver.js.
|
|
27
|
+
|
|
28
|
+
import { hostExternalSpecifiers } from "./dev-shims.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Lazily resolve esbuild's `build`. Kept out of the module's static imports so
|
|
32
|
+
* the published SDK runtime never forces the dependency — only the dev/pack
|
|
33
|
+
* bundle paths reach it. Throws a friendly install hint when it's absent
|
|
34
|
+
* (mirrors `loadTransform`'s sucrase handling).
|
|
35
|
+
*
|
|
36
|
+
* @returns {Promise<typeof import("esbuild")>}
|
|
37
|
+
*/
|
|
38
|
+
export async function loadEsbuild() {
|
|
39
|
+
try {
|
|
40
|
+
return await import("esbuild");
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Bundling a widget's web entry needs the optional `esbuild` dependency.\n" +
|
|
44
|
+
"Install it in your widget project:\n" +
|
|
45
|
+
" npm install --save-dev esbuild",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// esbuild plugin: resolve `import "….css"` (a side-effect import) to a tiny JS
|
|
51
|
+
// module that injects the CSS through a runtime <style> element. The output
|
|
52
|
+
// bundle then carries NO `.css` import, and the styling still applies the
|
|
53
|
+
// moment the module is evaluated in the browser. Guarded by
|
|
54
|
+
// `typeof document !== "undefined"` so the same module is a no-op under SSR /
|
|
55
|
+
// the native runtime (which never loads the web entry, but belt-and-braces).
|
|
56
|
+
function cssInjectPlugin() {
|
|
57
|
+
return {
|
|
58
|
+
name: "appstudio-css-inject",
|
|
59
|
+
setup(build) {
|
|
60
|
+
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
|
61
|
+
const { readFile } = await import("node:fs/promises");
|
|
62
|
+
const css = await readFile(args.path, "utf8");
|
|
63
|
+
const contents =
|
|
64
|
+
"if (typeof document !== \"undefined\") {\n" +
|
|
65
|
+
" const __s = document.createElement(\"style\");\n" +
|
|
66
|
+
" __s.setAttribute(\"data-appstudio-widget-css\", \"1\");\n" +
|
|
67
|
+
` __s.textContent = ${JSON.stringify(css)};\n` +
|
|
68
|
+
" document.head.appendChild(__s);\n" +
|
|
69
|
+
"}\n" +
|
|
70
|
+
"export default undefined;\n";
|
|
71
|
+
return { contents, loader: "js" };
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Bundle a widget's WEB entry into a single ESM string with the host-resolved
|
|
79
|
+
* specifiers left external. JSX is transformed with the automatic runtime (so
|
|
80
|
+
* the loader's react/jsx-runtime shims resolve it), `.png` assets become data
|
|
81
|
+
* URLs, and `.css` side-effect imports become runtime <style> injectors.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} opts
|
|
84
|
+
* @param {string} opts.entryPath absolute path to the web entry (e.g. the
|
|
85
|
+
* resolved `MapWidget.web.jsx`).
|
|
86
|
+
* @param {string[]} [opts.external] bare specifiers to leave external. Defaults
|
|
87
|
+
* to `hostExternalSpecifiers()` — the canonical host-resolved set.
|
|
88
|
+
* @param {string[]} [opts.nodePaths] extra `node_modules` roots esbuild should
|
|
89
|
+
* search when resolving the inlined deps (`react-leaflet`, `leaflet`, …). The
|
|
90
|
+
* packer passes the frontend's `node_modules` (where the vetted web packages
|
|
91
|
+
* are installed per the adding-vetted-packages skill) so the bundle resolves
|
|
92
|
+
* them no matter the invoking CWD.
|
|
93
|
+
* @returns {Promise<string>} the bundled ESM source.
|
|
94
|
+
*/
|
|
95
|
+
export async function bundleWebEntry({
|
|
96
|
+
entryPath,
|
|
97
|
+
external = hostExternalSpecifiers(),
|
|
98
|
+
nodePaths = [],
|
|
99
|
+
}) {
|
|
100
|
+
const esbuild = await loadEsbuild();
|
|
101
|
+
const result = await esbuild.build({
|
|
102
|
+
entryPoints: [entryPath],
|
|
103
|
+
bundle: true,
|
|
104
|
+
write: false,
|
|
105
|
+
format: "esm",
|
|
106
|
+
platform: "browser",
|
|
107
|
+
target: ["es2020"],
|
|
108
|
+
jsx: "automatic",
|
|
109
|
+
// Leave the host-resolved set as bare `import`s; the Studio loader rewrites
|
|
110
|
+
// them to blob shims at runtime (and Metro resolves them in the export).
|
|
111
|
+
// `react-native` is in that set (hostExternalSpecifiers): a vetted RN
|
|
112
|
+
// package (react-native-gesture-handler, react-native-reanimated, …) is
|
|
113
|
+
// INLINED, but its internal `import "react-native"` is left bare and
|
|
114
|
+
// resolves to the host's single react-native-web instance — the same
|
|
115
|
+
// shared-instance model as react/react-dom, far smaller than inlining a
|
|
116
|
+
// second copy of react-native-web into every bundle.
|
|
117
|
+
external,
|
|
118
|
+
// sc-1265: resolve the WEB build of cross-platform packages. RN packages
|
|
119
|
+
// ship `.web.js` platform variants and lean on the `browser` field /
|
|
120
|
+
// export condition; without these esbuild would pull their native entry and
|
|
121
|
+
// bundle React Native internals that break in the browser.
|
|
122
|
+
resolveExtensions: [
|
|
123
|
+
".web.js",
|
|
124
|
+
".web.jsx",
|
|
125
|
+
".web.ts",
|
|
126
|
+
".web.tsx",
|
|
127
|
+
".js",
|
|
128
|
+
".jsx",
|
|
129
|
+
".ts",
|
|
130
|
+
".tsx",
|
|
131
|
+
".json",
|
|
132
|
+
],
|
|
133
|
+
mainFields: ["browser", "module", "main"],
|
|
134
|
+
conditions: ["browser"],
|
|
135
|
+
loader: { ".png": "dataurl", ".jpg": "dataurl", ".gif": "dataurl" },
|
|
136
|
+
plugins: [cssInjectPlugin()],
|
|
137
|
+
// esbuild walks up from the entry for node_modules by default; `nodePaths`
|
|
138
|
+
// adds extra roots so a packer invoked from the repo root still resolves the
|
|
139
|
+
// vetted web packages installed under frontend/node_modules.
|
|
140
|
+
nodePaths,
|
|
141
|
+
logLevel: "silent",
|
|
142
|
+
});
|
|
143
|
+
const out = result.outputFiles?.[0];
|
|
144
|
+
if (!out) {
|
|
145
|
+
throw new Error(`esbuild produced no output for ${entryPath}`);
|
|
146
|
+
}
|
|
147
|
+
return out.text;
|
|
148
|
+
}
|