@colixsystems/widget-sdk 0.2.0 → 0.4.1
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 +28 -1
- package/dist/_theme-tokens.js +10 -0
- package/dist/contract.cjs +404 -0
- package/dist/contract.js +17 -0
- package/dist/hooks.js +242 -20
- package/dist/index.d.ts +159 -1
- package/dist/index.js +3 -1
- package/dist/index.native.js +3 -1
- package/dist/linter.js +72 -24
- package/dist/manifest.cjs +144 -0
- package/dist/manifest.js +10 -128
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -6,7 +6,34 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
`v0.
|
|
9
|
+
`v0.4.1` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
10
|
+
|
|
11
|
+
### What's new in 0.4.1
|
|
12
|
+
|
|
13
|
+
- **`useDatastoreQuery` returns a stable `refetch` identity.** The hook no longer rebinds the underlying callback when the host's `WidgetContext` value (a fresh object identity on every render in Studio + PageRenderer) changes. Widgets that put `refetch` in a `useEffect` dep array no longer loop.
|
|
14
|
+
|
|
15
|
+
### What's new in 0.4.0
|
|
16
|
+
|
|
17
|
+
- **`CONTRACT` is now a named export.** A frozen object literal describing the SDK surface every consumer (LLM system prompt, static analyzer, host builder, contract tests) derives from instead of declaring its own copy. See `docs/design/ai-widget-contract.md` for the full design and `src/contract.js` for the source. The contract carries:
|
|
18
|
+
|
|
19
|
+
- `hooks` — name, signature, return shape, required `WidgetContext` slices, manifest scopes.
|
|
20
|
+
- `primitives` — `Text` / `View` / `Pressable` / `Image` / `ScrollView` props.
|
|
21
|
+
- `manifestSchema` — the authoritative `WidgetManifest` field list (with `id` and `minAppStudioVersion`, not the legacy `manifestId` / `minAppStudioVer`).
|
|
22
|
+
- `themeTokens` — the default `useTheme()` payload.
|
|
23
|
+
- `widgetContextShape` — what the host must populate.
|
|
24
|
+
- `bundleExportContract` — the two default-export shapes the loader accepts.
|
|
25
|
+
- `bannedApis` — the global allowlist gate.
|
|
26
|
+
- `allowedBareImports` — `react`, `@colixsystems/widget-sdk`.
|
|
27
|
+
|
|
28
|
+
- **`useDatastoreQuery` is now stateful.** Returns `{ data, loading, error, refetch }`. Previously the hook returned the raw `list(...)` promise; widgets that called `.map(...)` synchronously on the result threw on first render. Migration: replace `const rows = useDatastoreQuery(...)` with `const { data, loading, error } = useDatastoreQuery(...)`. `data` is always an array (empty when the table is unbound or loading).
|
|
29
|
+
|
|
30
|
+
- **`DatastoreError` is now a named export.** Mutations throw a structured `DatastoreError` with `.code` (`VALIDATION` / `CONSTRAINT_VIOLATION` / `FORBIDDEN` / `NOT_FOUND` / `INTERNAL`) and an optional `.fieldErrors` map populated from the host's 422 payload. Widgets can branch on `err.code` without parsing axios messages.
|
|
31
|
+
|
|
32
|
+
- **`useI18n().t` now honours `fallback`.** `t(key, fallback)` returns `fallback` when the host's translation table has no entry for `key`.
|
|
33
|
+
|
|
34
|
+
### What was in 0.3.0
|
|
35
|
+
|
|
36
|
+
Additive: `WidgetManifest` carries an optional `datastoreTemplate` field. When a tenant installs a widget that declares one, the table set is seeded into their workspace alongside the install. Tables follow the existing built-in template semantics (auto-suffixed naming, REQ-ACL-05 creator-grants, REQ-TEMPLATES-ACL public grants, REQ-ACL-RELINHERIT cross-relation inheritance) and persist when the widget is later uninstalled. See `WidgetDatastoreTemplate` in `src/index.d.ts` for the structural constraints — at most 8 tables per widget, 24 columns per table, RELATION columns address siblings by `suffix`.
|
|
10
37
|
|
|
11
38
|
## Public API
|
|
12
39
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Re-export of the default theme tokens from the canonical contract data.
|
|
2
|
+
// Exists so the frontend's `widgetTheme.js` can keep its existing
|
|
3
|
+
// `DEFAULT_THEME_TOKENS` named export without reaching into the contract
|
|
4
|
+
// path itself. The values are the same Object.freeze'd reference as
|
|
5
|
+
// CONTRACT.themeTokens — assertion #6 in the contract test relies on
|
|
6
|
+
// referential identity.
|
|
7
|
+
|
|
8
|
+
import { CONTRACT } from "./contract.js";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_THEME_TOKENS = CONTRACT.themeTokens;
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// CommonJS-flavoured copy of the contract data so backend services (CJS
|
|
2
|
+
// runtime) can `require()` it directly without dynamic import gymnastics.
|
|
3
|
+
//
|
|
4
|
+
// The ESM `contract.js` re-exports the SAME object as the `CONTRACT` named
|
|
5
|
+
// export. The contract test asserts the two are identical (deep-equal) so
|
|
6
|
+
// drift between this file and the ESM source-of-truth is caught in CI.
|
|
7
|
+
//
|
|
8
|
+
// Keep edits in lockstep between `contract.cjs` and `contract.js`. The
|
|
9
|
+
// SDK build script copies both into `dist/`.
|
|
10
|
+
|
|
11
|
+
const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
12
|
+
colors: Object.freeze({
|
|
13
|
+
primary: "#ff6b5b",
|
|
14
|
+
onPrimary: "#ffffff",
|
|
15
|
+
surface: "#ffffff",
|
|
16
|
+
onSurface: "#111827",
|
|
17
|
+
surfaceMuted: "#f8fafc",
|
|
18
|
+
onSurfaceMuted: "#475569",
|
|
19
|
+
border: "#e2e8f0",
|
|
20
|
+
danger: "#dc2626",
|
|
21
|
+
success: "#059669",
|
|
22
|
+
warning: "#d97706",
|
|
23
|
+
info: "#0284c7",
|
|
24
|
+
}),
|
|
25
|
+
spacing: Object.freeze({ xs: 4, sm: 8, md: 16, lg: 24, xl: 32 }),
|
|
26
|
+
radii: Object.freeze({ sm: 4, md: 8, lg: 16, pill: 9999 }),
|
|
27
|
+
typography: Object.freeze({
|
|
28
|
+
fontFamily:
|
|
29
|
+
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
30
|
+
sizes: Object.freeze({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, xxl: 32 }),
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const HOOKS = [
|
|
35
|
+
{
|
|
36
|
+
name: "useTheme",
|
|
37
|
+
signature: "useTheme()",
|
|
38
|
+
returnShape: {
|
|
39
|
+
colors:
|
|
40
|
+
"{ primary, onPrimary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }",
|
|
41
|
+
spacing: "{ xs, sm, md, lg, xl }",
|
|
42
|
+
radii: "{ sm, md, lg, pill }",
|
|
43
|
+
typography: "{ fontFamily, sizes: { xs, sm, md, lg, xl, xxl } }",
|
|
44
|
+
},
|
|
45
|
+
requiredContextSlice: ["workspace.theme"],
|
|
46
|
+
scopes: null,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "useI18n",
|
|
50
|
+
signature: "useI18n()",
|
|
51
|
+
returnShape: {
|
|
52
|
+
t: "(key: string, fallback?: string) => string",
|
|
53
|
+
locale: "string",
|
|
54
|
+
},
|
|
55
|
+
requiredContextSlice: ["i18n.t", "i18n.locale"],
|
|
56
|
+
scopes: null,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "useDatastoreQuery",
|
|
60
|
+
signature: "useDatastoreQuery(tableId, options?)",
|
|
61
|
+
returnShape: {
|
|
62
|
+
data: "Record[]",
|
|
63
|
+
loading: "boolean",
|
|
64
|
+
error: "DatastoreError | null",
|
|
65
|
+
refetch: "() => Promise<void>",
|
|
66
|
+
},
|
|
67
|
+
requiredContextSlice: ["datastore.records"],
|
|
68
|
+
scopes: ["datastore.read:*"],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "useDatastoreMutation",
|
|
72
|
+
signature: "useDatastoreMutation(tableId)",
|
|
73
|
+
returnShape: {
|
|
74
|
+
create: "(record) => Promise<Record> // rejects with DatastoreError",
|
|
75
|
+
update: "(id, partial) => Promise<Record> // rejects with DatastoreError",
|
|
76
|
+
delete: "(id) => Promise<void> // rejects with DatastoreError",
|
|
77
|
+
},
|
|
78
|
+
requiredContextSlice: ["datastore.records"],
|
|
79
|
+
scopes: ["datastore.write:*"],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "useWidgetEvent",
|
|
83
|
+
signature: "useWidgetEvent(eventName)",
|
|
84
|
+
returnShape: { emit: "(payload?) => void" },
|
|
85
|
+
requiredContextSlice: ["events.emit"],
|
|
86
|
+
scopes: null,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const PRIMITIVES = [
|
|
91
|
+
{
|
|
92
|
+
name: "Text",
|
|
93
|
+
description:
|
|
94
|
+
"Text node. Web: <span>. Native: react-native Text. Use `style` for typography.",
|
|
95
|
+
props: {
|
|
96
|
+
children: { type: "ReactNode", description: "Text content." },
|
|
97
|
+
style: { type: "CSSObject | StyleObject", description: "Inline style." },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "View",
|
|
102
|
+
description:
|
|
103
|
+
"Block container. Web: <div>. Native: react-native View. Use for layout boxes.",
|
|
104
|
+
props: {
|
|
105
|
+
children: { type: "ReactNode" },
|
|
106
|
+
style: { type: "CSSObject | StyleObject" },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "Pressable",
|
|
111
|
+
description:
|
|
112
|
+
"Clickable wrapper. Use instead of raw <button> so cross-platform styling stays consistent.",
|
|
113
|
+
props: {
|
|
114
|
+
children: { type: "ReactNode" },
|
|
115
|
+
style: { type: "CSSObject | StyleObject" },
|
|
116
|
+
onPress: {
|
|
117
|
+
type: "(event) => void",
|
|
118
|
+
description: "Fires on click/tap. Web preventDefault is auto-applied.",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "Image",
|
|
124
|
+
description:
|
|
125
|
+
"Image. Pass an object with a `uri` (or a bare string on web). Falls back to `alt`.",
|
|
126
|
+
props: {
|
|
127
|
+
source: {
|
|
128
|
+
type: "{ uri: string } | string",
|
|
129
|
+
required: true,
|
|
130
|
+
description: "Source object or bare URL string.",
|
|
131
|
+
},
|
|
132
|
+
alt: { type: "string", description: "Alt text for accessibility." },
|
|
133
|
+
style: { type: "CSSObject | StyleObject" },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "ScrollView",
|
|
138
|
+
description:
|
|
139
|
+
"Scrollable container. Pass `horizontal` to scroll on the x-axis.",
|
|
140
|
+
props: {
|
|
141
|
+
children: { type: "ReactNode" },
|
|
142
|
+
horizontal: { type: "boolean" },
|
|
143
|
+
style: { type: "CSSObject | StyleObject" },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const CATEGORIES = [
|
|
149
|
+
"INPUT",
|
|
150
|
+
"DISPLAY",
|
|
151
|
+
"LAYOUT",
|
|
152
|
+
"DATA",
|
|
153
|
+
"MEDIA",
|
|
154
|
+
"COMMUNICATION",
|
|
155
|
+
"CUSTOM",
|
|
156
|
+
];
|
|
157
|
+
const PLATFORMS = ["web", "native"];
|
|
158
|
+
|
|
159
|
+
// Reverse-DNS-ish manifest id, e.g. "com.acme.charts.barchart". Two or
|
|
160
|
+
// more labels, lowercase alnum + hyphen, label starts with a letter. The
|
|
161
|
+
// analyzer + the SDK validator both read this from the contract so a
|
|
162
|
+
// rename / relaxation can only happen in one place.
|
|
163
|
+
const MANIFEST_ID_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/;
|
|
164
|
+
|
|
165
|
+
const MANIFEST_SCHEMA = {
|
|
166
|
+
id: {
|
|
167
|
+
type: "string",
|
|
168
|
+
required: true,
|
|
169
|
+
description: "Reverse-DNS identifier, e.g. com.acme.hello.",
|
|
170
|
+
example: "com.acme.hello",
|
|
171
|
+
pattern: MANIFEST_ID_PATTERN,
|
|
172
|
+
},
|
|
173
|
+
name: {
|
|
174
|
+
type: "string",
|
|
175
|
+
required: true,
|
|
176
|
+
description: "Human-readable widget name.",
|
|
177
|
+
},
|
|
178
|
+
version: {
|
|
179
|
+
type: "string",
|
|
180
|
+
required: true,
|
|
181
|
+
description: "Semver. Patch bump per publish unless author overrides.",
|
|
182
|
+
example: "1.0.0",
|
|
183
|
+
},
|
|
184
|
+
category: {
|
|
185
|
+
type: "enum",
|
|
186
|
+
required: true,
|
|
187
|
+
description:
|
|
188
|
+
"One of " + CATEGORIES.join(", ") + ". Drives the palette section.",
|
|
189
|
+
values: CATEGORIES,
|
|
190
|
+
},
|
|
191
|
+
icon: {
|
|
192
|
+
type: "string",
|
|
193
|
+
required: true,
|
|
194
|
+
description: "Icon identifier, e.g. lucide:sparkles.",
|
|
195
|
+
default: "lucide:sparkles",
|
|
196
|
+
},
|
|
197
|
+
description: {
|
|
198
|
+
type: "string",
|
|
199
|
+
required: true,
|
|
200
|
+
description: "1-2 sentence storefront description.",
|
|
201
|
+
},
|
|
202
|
+
author: {
|
|
203
|
+
type: "object",
|
|
204
|
+
required: true,
|
|
205
|
+
description: "{ name: string, url?: string, email?: string }",
|
|
206
|
+
},
|
|
207
|
+
supportedPlatforms: {
|
|
208
|
+
type: "string[]",
|
|
209
|
+
required: true,
|
|
210
|
+
description: "Subset of " + PLATFORMS.join(", ") + ".",
|
|
211
|
+
default: ["web"],
|
|
212
|
+
},
|
|
213
|
+
minAppStudioVersion: {
|
|
214
|
+
type: "string",
|
|
215
|
+
required: true,
|
|
216
|
+
description: "Semver range, e.g. >=2.4.0.",
|
|
217
|
+
default: ">=0.1.0",
|
|
218
|
+
},
|
|
219
|
+
requestedScopes: {
|
|
220
|
+
type: "string[]",
|
|
221
|
+
required: true,
|
|
222
|
+
description:
|
|
223
|
+
"Permission scopes; use [] unless the widget reads/writes data.",
|
|
224
|
+
default: [],
|
|
225
|
+
},
|
|
226
|
+
propertySchema: {
|
|
227
|
+
type: "object",
|
|
228
|
+
required: true,
|
|
229
|
+
description: "Map of prop name to property definition.",
|
|
230
|
+
default: {},
|
|
231
|
+
},
|
|
232
|
+
events: {
|
|
233
|
+
type: "object[]",
|
|
234
|
+
required: true,
|
|
235
|
+
description: "Declared events. Each entry { name, payloadSchema? }.",
|
|
236
|
+
default: [],
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const WIDGET_CONTEXT_SHAPE = {
|
|
241
|
+
props: {
|
|
242
|
+
description:
|
|
243
|
+
"Resolved property values, shape per manifest.propertySchema.",
|
|
244
|
+
required: true,
|
|
245
|
+
fields: {},
|
|
246
|
+
},
|
|
247
|
+
widget: {
|
|
248
|
+
description:
|
|
249
|
+
"Identity of the currently rendered widget instance ({ id, version }).",
|
|
250
|
+
required: true,
|
|
251
|
+
fields: { id: "manifest.id", version: "manifest.version" },
|
|
252
|
+
},
|
|
253
|
+
user: {
|
|
254
|
+
description: "Signed-in user ({ id, email, displayName }).",
|
|
255
|
+
required: true,
|
|
256
|
+
fields: { id: "string", email: "string", displayName: "string" },
|
|
257
|
+
},
|
|
258
|
+
workspace: {
|
|
259
|
+
description:
|
|
260
|
+
"Workspace identity + resolved theme ({ id, slug, theme, locale }).",
|
|
261
|
+
required: true,
|
|
262
|
+
fields: {
|
|
263
|
+
id: "string",
|
|
264
|
+
slug: "string",
|
|
265
|
+
theme: "ThemeTokens",
|
|
266
|
+
locale: "string",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
navigation: {
|
|
270
|
+
description: "{ push(path), replace(path), back() }.",
|
|
271
|
+
required: true,
|
|
272
|
+
fields: { push: "function", replace: "function", back: "function" },
|
|
273
|
+
},
|
|
274
|
+
datastore: {
|
|
275
|
+
description:
|
|
276
|
+
"{ records(table) -> { list(query?), get(id), create(values), update(id, values), delete(id) } }.",
|
|
277
|
+
required: true,
|
|
278
|
+
fields: { records: "function" },
|
|
279
|
+
},
|
|
280
|
+
events: {
|
|
281
|
+
description: "{ emit(name, payload) }.",
|
|
282
|
+
required: true,
|
|
283
|
+
fields: { emit: "function" },
|
|
284
|
+
},
|
|
285
|
+
i18n: {
|
|
286
|
+
description: "{ t(key, fallback?), locale }.",
|
|
287
|
+
required: true,
|
|
288
|
+
fields: { t: "function", locale: "string" },
|
|
289
|
+
},
|
|
290
|
+
logger: {
|
|
291
|
+
description:
|
|
292
|
+
"{ debug, info, warn, error } — host-routed; never use console.*.",
|
|
293
|
+
required: true,
|
|
294
|
+
fields: {
|
|
295
|
+
debug: "function",
|
|
296
|
+
info: "function",
|
|
297
|
+
warn: "function",
|
|
298
|
+
error: "function",
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const BUNDLE_EXPORT_CONTRACT = [
|
|
304
|
+
{
|
|
305
|
+
name: "wrapped",
|
|
306
|
+
description:
|
|
307
|
+
"default export = defineWidget({ manifest, component }) → resolves to { manifest, component, _kind: 'widget' }. Public marketplace widgets.",
|
|
308
|
+
predicate:
|
|
309
|
+
"typeof mod.default === 'object' && typeof mod.default.component === 'function' && typeof mod.default.manifest === 'object'",
|
|
310
|
+
manifestSource: "mod.default.manifest",
|
|
311
|
+
audience: "public-marketplace",
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "bare-component",
|
|
315
|
+
description:
|
|
316
|
+
"default export = function MyWidget(props) {...} → host pairs it with the installation's pinnedVersion.manifestJson. AI-agent widgets.",
|
|
317
|
+
predicate: "typeof mod.default === 'function'",
|
|
318
|
+
manifestSource: "installation.pinnedVersion.manifestJson",
|
|
319
|
+
audience: "ai-agent",
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
const BANNED_APIS = [
|
|
324
|
+
{ identifier: "eval", reason: "Arbitrary code evaluation." },
|
|
325
|
+
{
|
|
326
|
+
identifier: "Function",
|
|
327
|
+
reason: "Function() constructor evaluates strings.",
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
identifier: "new Function",
|
|
331
|
+
reason: "Function() constructor evaluates strings.",
|
|
332
|
+
},
|
|
333
|
+
{ identifier: "window", reason: "Host environment escape." },
|
|
334
|
+
{
|
|
335
|
+
identifier: "document",
|
|
336
|
+
reason: "Host environment escape; use SDK primitives.",
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
identifier: "process",
|
|
340
|
+
reason: "Node global; unavailable in the browser, banned for parity.",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
identifier: "localStorage",
|
|
344
|
+
reason: "Bypasses host data model; not portable to native.",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
identifier: "sessionStorage",
|
|
348
|
+
reason: "Same reason as localStorage.",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
identifier: "fetch",
|
|
352
|
+
reason:
|
|
353
|
+
"Direct network calls bypass the datastore client + tenant auth.",
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
identifier: "XMLHttpRequest",
|
|
357
|
+
reason:
|
|
358
|
+
"Direct network calls bypass the datastore client + tenant auth.",
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
identifier: "import(",
|
|
362
|
+
reason: "Dynamic import bypasses the loader's allowlist.",
|
|
363
|
+
},
|
|
364
|
+
{ identifier: "globalThis", reason: "Host environment escape." },
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
const ALLOWED_BARE_IMPORTS = ["react", "@colixsystems/widget-sdk"];
|
|
368
|
+
|
|
369
|
+
function deepFreeze(value) {
|
|
370
|
+
if (value === null || typeof value !== "object") return value;
|
|
371
|
+
if (Object.isFrozen(value)) return value;
|
|
372
|
+
for (const key of Object.keys(value)) deepFreeze(value[key]);
|
|
373
|
+
return Object.freeze(value);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const CONTRACT = deepFreeze({
|
|
377
|
+
version: "1.0.0",
|
|
378
|
+
hooks: HOOKS,
|
|
379
|
+
primitives: PRIMITIVES,
|
|
380
|
+
manifestSchema: MANIFEST_SCHEMA,
|
|
381
|
+
manifestCategories: CATEGORIES,
|
|
382
|
+
manifestPlatforms: PLATFORMS,
|
|
383
|
+
themeTokens: DEFAULT_THEME_TOKENS,
|
|
384
|
+
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
385
|
+
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
|
386
|
+
bannedApis: BANNED_APIS,
|
|
387
|
+
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
function isHookAllowed(name) {
|
|
391
|
+
return CONTRACT.hooks.some((h) => h.name === name);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function requiredContextKeys() {
|
|
395
|
+
const keys = new Set();
|
|
396
|
+
for (const hook of CONTRACT.hooks) {
|
|
397
|
+
for (const dotPath of hook.requiredContextSlice) {
|
|
398
|
+
keys.add(dotPath.split(".")[0]);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return [...keys];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = { CONTRACT, isHookAllowed, requiredContextKeys };
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Single-source-of-truth contract for the AI Widget Agent and every layer
|
|
2
|
+
// that derives from it. See docs/design/ai-widget-contract.md for the full
|
|
3
|
+
// rationale.
|
|
4
|
+
//
|
|
5
|
+
// The canonical data lives in `contract.cjs` so backend services (CJS
|
|
6
|
+
// runtime) can `require()` it without dynamic import gymnastics. This file
|
|
7
|
+
// is the ESM mirror — both modules export the exact same frozen
|
|
8
|
+
// `CONTRACT` object, and the contract test asserts they stay in lockstep.
|
|
9
|
+
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const cjs = require("./contract.cjs");
|
|
14
|
+
|
|
15
|
+
export const CONTRACT = cjs.CONTRACT;
|
|
16
|
+
export const isHookAllowed = cjs.isHookAllowed;
|
|
17
|
+
export const requiredContextKeys = cjs.requiredContextKeys;
|