@colixsystems/widget-sdk 0.188.0 → 0.190.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 +27 -1
- package/dist/cli.js +14 -3
- package/dist/contract.cjs +93 -1
- package/dist/contract.js +93 -1
- package/dist/dev-shims.js +6 -69
- package/dist/hooks.js +65 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/linter.cjs +129 -57
- package/dist/linter.js +130 -67
- package/package.json +3 -2
- package/src/contract.js +5355 -0
- package/src/dev-shims.js +6 -69
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
53
53
|
| **AGENTS** | `useAgent(agentRef)` | `{ send, cancel, reset, messages, sending, resuming, error, available }` | `ctx.agents` — `ai.invoke:*`. Hold a conversation with one of the WORKSPACE's own AI agents (an author-written name + system prompt). Sending is IMPERATIVE: call `send(text)` from an event handler, never on mount — the first send opens the conversation, so a widget nobody types into costs nothing. On mount the hook DOES reopen the caller's most recent thread with this agent, so a refresh or a navigation keeps the conversation instead of starting a blank one; that resume only READS and spends no credit. `resuming` is true while it runs — render a loading state, not an empty thread. `messages` is the running transcript, oldest first. You **cannot** set or read the system prompt, replay an edited transcript, or pick a model: the server assembles every turn from the stored prompt plus the stored history, and `content` is the only thing that reaches the wire. `reset()` drops the thread (a "new chat" affordance), and the new thread is the one a later mount resumes. **Every send spends the workspace's AI credits** and is rate-limited per caller AND per workspace. Rejects with `AgentError`; branch on `.code` — `AI_QUOTA_EXCEEDED` (429) is the workspace's credit ceiling and will NOT clear on a retry, unlike `RATE_LIMITED`, so never offer "try again" for it, and `AGENT_DISABLED` (409) means the author turned it off, so hide the affordance. Never surface a raw error or the workspace's billing state to an app user. `available` is false when no agent is bound or the host brokers none — render a visible unbound state, never a blank box. An assistant message that used a tool also carries **`activity`** — what the agent DID with the user's data: the table it read, or the table and columns it CHANGED (never the values, and never a row the permissions refused). **Render a write.** A reply saying "done!" is not evidence a row was written, and if the agent got it wrong the user has no other way to notice. A talk-only turn omits the key entirely. |
|
|
54
54
|
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*`. **To CLEAR a cell, pass it empty** — `update(id, { when: null })` (also `""` or `[]`) empties that cell and it reads back as `null` (`[]` on an array column). There is no separate clear call, so never write a sentinel such as `0` or a placeholder date to mean "no value". `false` and `0` are values, not clears, and a REQUIRED column cannot be cleared. |
|
|
55
55
|
| **DATASTORE** | `useDatastoreSubscription(table, handlers, options)` (optional: options) | `{ status }` — `"connecting" \| "live" \| "reconnecting" \| "fallback"` | `records(table).subscribe` — `datastore.read:<table>`. Live `onCreated` / `onUpdated` / `onDeleted` off the REQ-RT-07 socket; never throws, resolving to `{ status: "fallback" }` so the widget polls instead. A whole-table subscribe is gated on read-EVERY-row, because one envelope reaches every subscriber of the table — so for a table governed by per-record grants pass `options.scope`: `{ kind: "record", record_id }` for one row, or `{ kind: "parent", relation_column, record_id }` for the rows whose RELATION column points at that parent (the column must carry `inheritAcl`, else the subscribe reports `"fallback"`). Re-subscribes on the scope's VALUES, so a fresh object literal each render is fine. |
|
|
56
|
+
| **DATASTORE** | `useTyping(tableId, recordId)` | `{ typingUserIds, setTyping, status }` — status `"connecting" \| "live" \| "unavailable"` | `records(table).typing` — `datastore.read:<table>`. Who else is typing in one record, over the same socket as `useDatastoreSubscription`. Call `setTyping(true)` on every keystroke (throttled to one signal per 3 s; `false` is sent after 5 s idle) and `setTyping(false)` on send, clear or blur. `typingUserIds` lists the OTHER users' ids; each drops out 6 s after their last signal. Needs a signed-in user who can read the record. Never throws: an anonymous visitor, a refused subscription or a host whose client lacks `typing` reads `{ typingUserIds: [], status: "unavailable" }`. Re-subscribes when either id changes. |
|
|
56
57
|
| **DATASTORE** | `useRecordPermissions(tableId, recordId)` | `{ permissions, loading, error, grant, revoke, update, refetch }` | `records(table).permissions(record).{ list, grant, update, revoke }` — `acl.write:records` (+ `can_grant` on the record) |
|
|
57
58
|
| **DATASTORE** | `useCanWrite(tableId, options)` (optional: options) | `{ canWrite, loading, error, refetch }` | `myPermissions(tableId, { recordId? })` — scope `datastore.read:<table>`. A FLOOR, not a full replacement for domain-specific write rules: answers "may this caller write", reading the same table-ACL answer the write endpoint enforces — so a table granting Create to Everyone answers `true` for a logged-out visitor, and this hook alone is the right gate for a widget meant to work without signing in. Pass `{ recordId }` for a per-row check. A widget whose own rule is MORE SPECIFIC than the table ACL (e.g. "only the assigned user may edit this row") must still hand-check that in addition. Pair with `useUser()` to also tell "not signed in" apart from "signed in but forbidden" — both resolve `canWrite: false` here. Falsy `tableId`, or a host that hasn't injected `myPermissions` (an older host), collapses to `{ canWrite: false, loading: false, error: null, refetch: async () => undefined }` rather than throwing. |
|
|
58
59
|
| **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
|
|
@@ -75,7 +76,32 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
75
76
|
|
|
76
77
|
## Status
|
|
77
78
|
|
|
78
|
-
`v0.
|
|
79
|
+
`v0.190.0` — 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**.
|
|
80
|
+
|
|
81
|
+
### What's new in 0.190.0 (contract 1.151.0)
|
|
82
|
+
|
|
83
|
+
**New `useTyping(tableId, recordId)` hook: a typing indicator for one record.** It returns `{ typingUserIds, setTyping, status }`. Call `setTyping(true)` on every keystroke in your composer and `setTyping(false)` when the message is sent, the field is cleared or it loses focus. The hook throttles for you: it sends at most one signal every 3 s, and sends `false` on its own after 5 s without a keystroke. `typingUserIds` lists the other users typing in that record (never yourself); each one drops out 6 s after their last signal, so a closed tab never leaves a stale indicator. Signals carry no message content and are never stored.
|
|
84
|
+
|
|
85
|
+
Key it on the record the conversation lives in (a thread or channel row), which the user must be able to read. It rides the same realtime connection as `useDatastoreSubscription`, so a widget holds one socket for both. `status` is `"connecting"`, `"live"` or `"unavailable"`; it is `"unavailable"` for an anonymous visitor, a refused subscription, a preview with no backend, and a host whose datastore client predates typing. The hook never throws, so render the indicator only when `typingUserIds` is non-empty:
|
|
86
|
+
|
|
87
|
+
```jsx
|
|
88
|
+
const { typingUserIds, setTyping } = useTyping(threadsTableId, threadId);
|
|
89
|
+
// <TextInput onChangeText={(t) => { setDraft(t); setTyping(t.length > 0); }} onBlur={() => setTyping(false)} />
|
|
90
|
+
// {typingUserIds.length > 0 && <Text>{typingUserIds.length === 1 ? "Someone is typing…" : "Several people are typing…"}</Text>}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Declare `datastore.read` for the table. Additive; works identically in the web Player and the native export.
|
|
94
|
+
|
|
95
|
+
### What's new in 0.189.0 (contract 1.150.0)
|
|
96
|
+
|
|
97
|
+
**The import check now catches two shapes that passed lint and then failed to load in the web Player.** This narrows `import-not-vetted`:
|
|
98
|
+
|
|
99
|
+
- **Imports wrapped over several lines.** The check read one line at a time, so a Prettier-wrapped import of a package that is not on the vetted list was never seen. It now reads the whole file and reports the finding on the `from` line. `export { … } from "pkg"` and `export * from "pkg"` are checked too, and so are forms written without spaces (`import{a}from"pkg"`) or inside an expression (`x = require("pkg")`).
|
|
100
|
+
- **Subpaths of packages the web Player provides.** The web Player supplies some packages itself and loads them by their exact name only. `CONTRACT.hostExternalSpecifiers` lists them; today they are `react` (and its JSX runtimes), `react-dom`, `react-native`, `@colixsystems/widget-sdk`, `lucide-react-native`, `react-native-svg`, `react-native-qrcode-svg`, `date-fns`, `decimal.js`, `d3-scale`, `d3-shape`, `d3-array` and `simple-statistics`. A subpath of one of them, such as `date-fns/format` or `react-native/Libraries/…`, cannot load on web. It is now `import-not-vetted` in any file that ships to web. Import from the package itself: `import { format } from "date-fns"`.
|
|
101
|
+
|
|
102
|
+
A subpath of a package your bundle includes is still allowed, for example `leaflet/dist/leaflet.css`, a Leaflet marker image or `react-native-vector-icons/MaterialIcons`. A file that ships only to native (`widget.native.jsx`, or a manifest whose `supportedPlatforms` is `["native"]`) keeps every vetted subpath, because the native build resolves them.
|
|
103
|
+
|
|
104
|
+
`CONTRACT.subpathImportRoots` lists the vetted packages whose subpaths load on web, and the new `subpathImportRootsFor(manifest)` export of `@colixsystems/widget-sdk/linter` returns the roots that load for a file with that manifest. `appstudio-widget lint --manifest` now checks `widget.web.jsx` and `widget.native.jsx` against the platform each one ships to, as the submission check already did.
|
|
79
105
|
|
|
80
106
|
### What's new in 0.188.0 (contract 1.149.0)
|
|
81
107
|
|
package/dist/cli.js
CHANGED
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
// — REQ-WSDK-DEVKIT local dev server
|
|
6
6
|
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
|
-
import { resolve } from "node:path";
|
|
8
|
+
import { basename, resolve } from "node:path";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { argv, exit, stderr, stdout } from "node:process";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
lintSource,
|
|
13
|
+
lintStyleWiring,
|
|
14
|
+
lintEditability,
|
|
15
|
+
narrowManifestForFile,
|
|
16
|
+
} from "./linter.js";
|
|
12
17
|
import { startDevServer } from "./devserver.js";
|
|
13
18
|
|
|
14
19
|
function usage() {
|
|
@@ -99,9 +104,15 @@ async function runLint(rest) {
|
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
const label = positionals.join(", ");
|
|
107
|
+
// Narrow per split-impl file, as the submission gate does.
|
|
102
108
|
const perFile = Object.entries(files).map(([filePath, source]) => [
|
|
103
109
|
filePath,
|
|
104
|
-
lintSource(
|
|
110
|
+
lintSource(
|
|
111
|
+
source,
|
|
112
|
+
manifest
|
|
113
|
+
? { manifest: narrowManifestForFile(manifest, basename(filePath)) }
|
|
114
|
+
: undefined,
|
|
115
|
+
),
|
|
105
116
|
]);
|
|
106
117
|
const wiring = manifest
|
|
107
118
|
? lintStyleWiring(manifest, files)
|
package/dist/contract.cjs
CHANGED
|
@@ -2043,6 +2043,40 @@ const HOOKS = [
|
|
|
2043
2043
|
requiredContextSlice: ["datastore.records"],
|
|
2044
2044
|
scopes: ["datastore.read:<table>"],
|
|
2045
2045
|
},
|
|
2046
|
+
// REQ-RT-08 — typing signals for one record.
|
|
2047
|
+
{
|
|
2048
|
+
name: "useTyping",
|
|
2049
|
+
signature: "useTyping(tableId, recordId)",
|
|
2050
|
+
description:
|
|
2051
|
+
"Who else is typing in one record, and announcing your own typing, via " +
|
|
2052
|
+
"ctx.datastore.records(tableId).typing(recordId, ...). It shares the " +
|
|
2053
|
+
"datastore client's one realtime socket with useDatastoreSubscription. " +
|
|
2054
|
+
"Call setTyping(true) on every keystroke (throttled inside: one signal " +
|
|
2055
|
+
"per 3 s at most, and false is sent after 5 s without a keystroke) and " +
|
|
2056
|
+
"setTyping(false) on send, clear or blur. typingUserIds lists the OTHER " +
|
|
2057
|
+
"users typing (user ids, never yourself); each drops out 6 s after their " +
|
|
2058
|
+
"last signal. Signals carry no message content and are never stored. " +
|
|
2059
|
+
"Typing needs a signed-in user who can read the record; key it on the " +
|
|
2060
|
+
"record the conversation lives in (a thread or channel row). Gate the " +
|
|
2061
|
+
"composer that calls setTyping on useCanWrite as usual. status is " +
|
|
2062
|
+
"'connecting' | 'live' | 'unavailable'. " +
|
|
2063
|
+
"Never throws: with a falsy id, an anonymous visitor, a refused " +
|
|
2064
|
+
"subscription, a host whose datastore client predates typing, or a " +
|
|
2065
|
+
"preview with no backend, it returns { typingUserIds: [], setTyping: no-op, " +
|
|
2066
|
+
"status: 'unavailable' }, so render the indicator only when " +
|
|
2067
|
+
"typingUserIds is non-empty. Re-subscribes when tableId or recordId " +
|
|
2068
|
+
"changes. Declare datastore.read for the table.",
|
|
2069
|
+
returnShape: {
|
|
2070
|
+
typingUserIds:
|
|
2071
|
+
"string[] // other users typing in this record; empty when unavailable",
|
|
2072
|
+
setTyping:
|
|
2073
|
+
"(active: boolean) => void // true per keystroke, false on send/clear/blur",
|
|
2074
|
+
status:
|
|
2075
|
+
"'connecting' | 'live' | 'unavailable' // 'unavailable' → hide the indicator",
|
|
2076
|
+
},
|
|
2077
|
+
requiredContextSlice: ["datastore.records"],
|
|
2078
|
+
scopes: ["datastore.read:<table>"],
|
|
2079
|
+
},
|
|
2046
2080
|
// REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
|
|
2047
2081
|
{
|
|
2048
2082
|
name: "useClipboard",
|
|
@@ -3505,6 +3539,49 @@ const ALLOWED_BARE_IMPORTS = [
|
|
|
3505
3539
|
...CORE_INFRA_BARE_IMPORTS,
|
|
3506
3540
|
];
|
|
3507
3541
|
|
|
3542
|
+
// sc-10502: the bare specifiers the web host RESOLVES for a loaded widget
|
|
3543
|
+
// instead of letting its bundle inline a copy. The web-entry bundler marks
|
|
3544
|
+
// exactly these `external` and the Studio loader shims each by EXACT name, so
|
|
3545
|
+
// a subpath of one (`date-fns/format`) cannot load in the web Player.
|
|
3546
|
+
// `dev-shims.hostExternalSpecifiers()` returns this list; the loader's
|
|
3547
|
+
// module-load guard (frontend widgetLoader.js) throws if its shims drift.
|
|
3548
|
+
//
|
|
3549
|
+
// Why each entry is host-resolved, not bundled:
|
|
3550
|
+
// * react + its JSX runtimes: one React instance (hooks, context).
|
|
3551
|
+
// * react-dom, react-dom/client: react-leaflet portals through react-dom;
|
|
3552
|
+
// a second renderer beside the host's React crashes (sc-1064).
|
|
3553
|
+
// * @colixsystems/widget-sdk: the host-built SDK carries datastore, theme
|
|
3554
|
+
// and navigation.
|
|
3555
|
+
// * react-native, lucide-react-native, react-native-svg,
|
|
3556
|
+
// react-native-qrcode-svg: share the host's react-native-web instance
|
|
3557
|
+
// (sc-1265, sc-6105).
|
|
3558
|
+
// * date-fns and the maths set: an agent widget is transpiled, not
|
|
3559
|
+
// bundled, so the host must resolve its bare imports (sc-7191).
|
|
3560
|
+
const HOST_EXTERNAL_SPECIFIERS = [
|
|
3561
|
+
"react",
|
|
3562
|
+
"react/jsx-runtime",
|
|
3563
|
+
"react/jsx-dev-runtime",
|
|
3564
|
+
"react-dom",
|
|
3565
|
+
"react-dom/client",
|
|
3566
|
+
"@colixsystems/widget-sdk",
|
|
3567
|
+
"lucide-react-native",
|
|
3568
|
+
"react-native-svg",
|
|
3569
|
+
"date-fns",
|
|
3570
|
+
"react-native-qrcode-svg",
|
|
3571
|
+
"react-native",
|
|
3572
|
+
"decimal.js",
|
|
3573
|
+
"d3-scale",
|
|
3574
|
+
"d3-shape",
|
|
3575
|
+
"d3-array",
|
|
3576
|
+
"simple-statistics",
|
|
3577
|
+
];
|
|
3578
|
+
|
|
3579
|
+
// Vetted roots whose subpaths load on web (`leaflet/dist/leaflet.css`): the
|
|
3580
|
+
// bundle inlines them, so no exact-name host shim stands in the way.
|
|
3581
|
+
const SUBPATH_IMPORT_ROOTS = ALLOWED_BARE_IMPORTS.filter(
|
|
3582
|
+
(spec) => !HOST_EXTERNAL_SPECIFIERS.includes(spec),
|
|
3583
|
+
);
|
|
3584
|
+
|
|
3508
3585
|
// REQ-WSDK-PLATFORM §3.5: host-API URL patterns the linter scans for as a
|
|
3509
3586
|
// soft warning. None of these block the lint by themselves — they prompt
|
|
3510
3587
|
// the marketplace reviewer to spot-check, and they help authors learn the
|
|
@@ -4871,7 +4948,20 @@ const CONTRACT = deepFreeze({
|
|
|
4871
4948
|
// (error): a file importing leaflet / react-leaflet must import
|
|
4872
4949
|
// "leaflet/dist/leaflet.css". `leaflet-default-marker-icon` (warning)
|
|
4873
4950
|
// flags a react-leaflet <Marker> with no `icon`.
|
|
4874
|
-
|
|
4951
|
+
// 1.150.0: narrowing (sc-10502) — the linter scans imports across the whole
|
|
4952
|
+
// file, so a wrapped `import { … } from` (or an `export … from`) of an
|
|
4953
|
+
// unvetted package is `import-not-vetted` on its `from` line. A file
|
|
4954
|
+
// that ships to web may import a SUBPATH only of a bundled vetted
|
|
4955
|
+
// package (`subpathImportRoots`, e.g. "leaflet/dist/leaflet.css"); a
|
|
4956
|
+
// host-provided package's subpath ("date-fns/format") is flagged. A
|
|
4957
|
+
// native-only file keeps every vetted subpath (Metro resolves them).
|
|
4958
|
+
// New `hostExternalSpecifiers` / `subpathImportRoots` lists and the
|
|
4959
|
+
// linter export `subpathImportRootsFor(manifest)`.
|
|
4960
|
+
// 1.151.0: additive (sc-10538) — new `useTyping(tableId, recordId)` returns
|
|
4961
|
+
// `{ typingUserIds, setTyping, status }`: the other users typing in one
|
|
4962
|
+
// record, over the datastore client's single realtime socket. A host
|
|
4963
|
+
// whose client lacks `records(t).typing` reads `status: "unavailable"`.
|
|
4964
|
+
version: "1.151.0",
|
|
4875
4965
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4876
4966
|
hooks: HOOKS,
|
|
4877
4967
|
primitives: PRIMITIVES,
|
|
@@ -4898,6 +4988,8 @@ const CONTRACT = deepFreeze({
|
|
|
4898
4988
|
bannedApis: BANNED_APIS,
|
|
4899
4989
|
vettedImports: VETTED_IMPORTS,
|
|
4900
4990
|
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
4991
|
+
hostExternalSpecifiers: HOST_EXTERNAL_SPECIFIERS,
|
|
4992
|
+
subpathImportRoots: SUBPATH_IMPORT_ROOTS,
|
|
4901
4993
|
hostApiUrlPatterns: HOST_API_URL_PATTERNS,
|
|
4902
4994
|
payloadValueTypes: PAYLOAD_VALUE_TYPES,
|
|
4903
4995
|
chargeCurrency: CHARGE_CURRENCY,
|
package/dist/contract.js
CHANGED
|
@@ -2043,6 +2043,40 @@ const HOOKS = [
|
|
|
2043
2043
|
requiredContextSlice: ["datastore.records"],
|
|
2044
2044
|
scopes: ["datastore.read:<table>"],
|
|
2045
2045
|
},
|
|
2046
|
+
// REQ-RT-08 — typing signals for one record.
|
|
2047
|
+
{
|
|
2048
|
+
name: "useTyping",
|
|
2049
|
+
signature: "useTyping(tableId, recordId)",
|
|
2050
|
+
description:
|
|
2051
|
+
"Who else is typing in one record, and announcing your own typing, via " +
|
|
2052
|
+
"ctx.datastore.records(tableId).typing(recordId, ...). It shares the " +
|
|
2053
|
+
"datastore client's one realtime socket with useDatastoreSubscription. " +
|
|
2054
|
+
"Call setTyping(true) on every keystroke (throttled inside: one signal " +
|
|
2055
|
+
"per 3 s at most, and false is sent after 5 s without a keystroke) and " +
|
|
2056
|
+
"setTyping(false) on send, clear or blur. typingUserIds lists the OTHER " +
|
|
2057
|
+
"users typing (user ids, never yourself); each drops out 6 s after their " +
|
|
2058
|
+
"last signal. Signals carry no message content and are never stored. " +
|
|
2059
|
+
"Typing needs a signed-in user who can read the record; key it on the " +
|
|
2060
|
+
"record the conversation lives in (a thread or channel row). Gate the " +
|
|
2061
|
+
"composer that calls setTyping on useCanWrite as usual. status is " +
|
|
2062
|
+
"'connecting' | 'live' | 'unavailable'. " +
|
|
2063
|
+
"Never throws: with a falsy id, an anonymous visitor, a refused " +
|
|
2064
|
+
"subscription, a host whose datastore client predates typing, or a " +
|
|
2065
|
+
"preview with no backend, it returns { typingUserIds: [], setTyping: no-op, " +
|
|
2066
|
+
"status: 'unavailable' }, so render the indicator only when " +
|
|
2067
|
+
"typingUserIds is non-empty. Re-subscribes when tableId or recordId " +
|
|
2068
|
+
"changes. Declare datastore.read for the table.",
|
|
2069
|
+
returnShape: {
|
|
2070
|
+
typingUserIds:
|
|
2071
|
+
"string[] // other users typing in this record; empty when unavailable",
|
|
2072
|
+
setTyping:
|
|
2073
|
+
"(active: boolean) => void // true per keystroke, false on send/clear/blur",
|
|
2074
|
+
status:
|
|
2075
|
+
"'connecting' | 'live' | 'unavailable' // 'unavailable' → hide the indicator",
|
|
2076
|
+
},
|
|
2077
|
+
requiredContextSlice: ["datastore.records"],
|
|
2078
|
+
scopes: ["datastore.read:<table>"],
|
|
2079
|
+
},
|
|
2046
2080
|
// REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
|
|
2047
2081
|
{
|
|
2048
2082
|
name: "useClipboard",
|
|
@@ -3501,6 +3535,49 @@ const ALLOWED_BARE_IMPORTS = [
|
|
|
3501
3535
|
...CORE_INFRA_BARE_IMPORTS,
|
|
3502
3536
|
];
|
|
3503
3537
|
|
|
3538
|
+
// sc-10502: the bare specifiers the web host RESOLVES for a loaded widget
|
|
3539
|
+
// instead of letting its bundle inline a copy. The web-entry bundler marks
|
|
3540
|
+
// exactly these `external` and the Studio loader shims each by EXACT name, so
|
|
3541
|
+
// a subpath of one (`date-fns/format`) cannot load in the web Player.
|
|
3542
|
+
// `dev-shims.hostExternalSpecifiers()` returns this list; the loader's
|
|
3543
|
+
// module-load guard (frontend widgetLoader.js) throws if its shims drift.
|
|
3544
|
+
//
|
|
3545
|
+
// Why each entry is host-resolved, not bundled:
|
|
3546
|
+
// * react + its JSX runtimes: one React instance (hooks, context).
|
|
3547
|
+
// * react-dom, react-dom/client: react-leaflet portals through react-dom;
|
|
3548
|
+
// a second renderer beside the host's React crashes (sc-1064).
|
|
3549
|
+
// * @colixsystems/widget-sdk: the host-built SDK carries datastore, theme
|
|
3550
|
+
// and navigation.
|
|
3551
|
+
// * react-native, lucide-react-native, react-native-svg,
|
|
3552
|
+
// react-native-qrcode-svg: share the host's react-native-web instance
|
|
3553
|
+
// (sc-1265, sc-6105).
|
|
3554
|
+
// * date-fns and the maths set: an agent widget is transpiled, not
|
|
3555
|
+
// bundled, so the host must resolve its bare imports (sc-7191).
|
|
3556
|
+
const HOST_EXTERNAL_SPECIFIERS = [
|
|
3557
|
+
"react",
|
|
3558
|
+
"react/jsx-runtime",
|
|
3559
|
+
"react/jsx-dev-runtime",
|
|
3560
|
+
"react-dom",
|
|
3561
|
+
"react-dom/client",
|
|
3562
|
+
"@colixsystems/widget-sdk",
|
|
3563
|
+
"lucide-react-native",
|
|
3564
|
+
"react-native-svg",
|
|
3565
|
+
"date-fns",
|
|
3566
|
+
"react-native-qrcode-svg",
|
|
3567
|
+
"react-native",
|
|
3568
|
+
"decimal.js",
|
|
3569
|
+
"d3-scale",
|
|
3570
|
+
"d3-shape",
|
|
3571
|
+
"d3-array",
|
|
3572
|
+
"simple-statistics",
|
|
3573
|
+
];
|
|
3574
|
+
|
|
3575
|
+
// Vetted roots whose subpaths load on web (`leaflet/dist/leaflet.css`): the
|
|
3576
|
+
// bundle inlines them, so no exact-name host shim stands in the way.
|
|
3577
|
+
const SUBPATH_IMPORT_ROOTS = ALLOWED_BARE_IMPORTS.filter(
|
|
3578
|
+
(spec) => !HOST_EXTERNAL_SPECIFIERS.includes(spec),
|
|
3579
|
+
);
|
|
3580
|
+
|
|
3504
3581
|
// REQ-WSDK-PLATFORM §3.5: host-API URL patterns the linter scans for as a
|
|
3505
3582
|
// soft warning. None of these block the lint by themselves — they prompt
|
|
3506
3583
|
// the marketplace reviewer to spot-check, and they help authors learn the
|
|
@@ -4867,7 +4944,20 @@ const CONTRACT = deepFreeze({
|
|
|
4867
4944
|
// (error): a file importing leaflet / react-leaflet must import
|
|
4868
4945
|
// "leaflet/dist/leaflet.css". `leaflet-default-marker-icon` (warning)
|
|
4869
4946
|
// flags a react-leaflet <Marker> with no `icon`.
|
|
4870
|
-
|
|
4947
|
+
// 1.150.0: narrowing (sc-10502) — the linter scans imports across the whole
|
|
4948
|
+
// file, so a wrapped `import { … } from` (or an `export … from`) of an
|
|
4949
|
+
// unvetted package is `import-not-vetted` on its `from` line. A file
|
|
4950
|
+
// that ships to web may import a SUBPATH only of a bundled vetted
|
|
4951
|
+
// package (`subpathImportRoots`, e.g. "leaflet/dist/leaflet.css"); a
|
|
4952
|
+
// host-provided package's subpath ("date-fns/format") is flagged. A
|
|
4953
|
+
// native-only file keeps every vetted subpath (Metro resolves them).
|
|
4954
|
+
// New `hostExternalSpecifiers` / `subpathImportRoots` lists and the
|
|
4955
|
+
// linter export `subpathImportRootsFor(manifest)`.
|
|
4956
|
+
// 1.151.0: additive (sc-10538) — new `useTyping(tableId, recordId)` returns
|
|
4957
|
+
// `{ typingUserIds, setTyping, status }`: the other users typing in one
|
|
4958
|
+
// record, over the datastore client's single realtime socket. A host
|
|
4959
|
+
// whose client lacks `records(t).typing` reads `status: "unavailable"`.
|
|
4960
|
+
version: "1.151.0",
|
|
4871
4961
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4872
4962
|
hooks: HOOKS,
|
|
4873
4963
|
primitives: PRIMITIVES,
|
|
@@ -4894,6 +4984,8 @@ const CONTRACT = deepFreeze({
|
|
|
4894
4984
|
bannedApis: BANNED_APIS,
|
|
4895
4985
|
vettedImports: VETTED_IMPORTS,
|
|
4896
4986
|
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
4987
|
+
hostExternalSpecifiers: HOST_EXTERNAL_SPECIFIERS,
|
|
4988
|
+
subpathImportRoots: SUBPATH_IMPORT_ROOTS,
|
|
4897
4989
|
hostApiUrlPatterns: HOST_API_URL_PATTERNS,
|
|
4898
4990
|
payloadValueTypes: PAYLOAD_VALUE_TYPES,
|
|
4899
4991
|
chargeCurrency: CHARGE_CURRENCY,
|
package/dist/dev-shims.js
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
// runtime-enumerates the host's namespace). Imports of these packages from a
|
|
20
20
|
// sibling .jsx file return `422` from the dev server with a clear message.
|
|
21
21
|
|
|
22
|
+
import { CONTRACT } from "./contract.js";
|
|
23
|
+
|
|
22
24
|
// React named exports the Studio re-exposes through the shim. Mirrors the
|
|
23
25
|
// browser's `react` named-export surface. Adding a new hook in a React minor
|
|
24
26
|
// release means appending it here.
|
|
@@ -61,81 +63,16 @@ export const REACT_NAMED_EXPORTS = [
|
|
|
61
63
|
// React's automatic JSX runtime: jsx / jsxs / Fragment, plus the dev variant.
|
|
62
64
|
export const REACT_JSX_RUNTIME_EXPORTS = ["jsx", "jsxs", "Fragment", "jsxDEV"];
|
|
63
65
|
|
|
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`,
|
|
98
|
-
// `react-native-qrcode-svg` — vetted packages the host already bundles +
|
|
99
|
-
// shims (`_hostVettedModules`); sharing the host copy keeps icons/SVG on
|
|
100
|
-
// the host's react-native-web instance. qrcode-svg draws through
|
|
101
|
-
// react-native-svg, so externalising it keeps the QR on that same
|
|
102
|
-
// instance instead of a bundle inlining a second one (sc-6105).
|
|
103
|
-
const HOST_EXTERNAL_SPECIFIERS = [
|
|
104
|
-
"react",
|
|
105
|
-
"react/jsx-runtime",
|
|
106
|
-
"react/jsx-dev-runtime",
|
|
107
|
-
"react-dom",
|
|
108
|
-
"react-dom/client",
|
|
109
|
-
"@colixsystems/widget-sdk",
|
|
110
|
-
"lucide-react-native",
|
|
111
|
-
"react-native-svg",
|
|
112
|
-
"date-fns",
|
|
113
|
-
"react-native-qrcode-svg",
|
|
114
|
-
// sc-1265: a bundled widget (and any vetted RN package it inlines, e.g.
|
|
115
|
-
// react-native-gesture-handler) imports `react-native`; the host resolves it
|
|
116
|
-
// to its OWN single react-native-web instance through a blob shim, so the
|
|
117
|
-
// bundle shares one RN-web (StyleSheet/context) instead of inlining a second
|
|
118
|
-
// copy. On native, Metro resolves the real react-native in the export.
|
|
119
|
-
"react-native",
|
|
120
|
-
// sc-7191: the vetted pure-JS maths set. Externalised for the same reason as
|
|
121
|
-
// date-fns — an AI-agent widget is transpiled, never bundled, so the host
|
|
122
|
-
// must resolve the bare import at runtime.
|
|
123
|
-
"decimal.js",
|
|
124
|
-
"d3-scale",
|
|
125
|
-
"d3-shape",
|
|
126
|
-
"d3-array",
|
|
127
|
-
"simple-statistics",
|
|
128
|
-
];
|
|
129
|
-
|
|
130
66
|
/**
|
|
131
67
|
* The canonical "host-resolved at runtime" bare-specifier set — the externals
|
|
132
|
-
* the web-entry bundler must NOT inline.
|
|
133
|
-
*
|
|
68
|
+
* the web-entry bundler must NOT inline. Sourced from
|
|
69
|
+
* `CONTRACT.hostExternalSpecifiers`, which documents each entry. Returns a
|
|
70
|
+
* fresh array each call so a caller can't mutate the source of truth.
|
|
134
71
|
*
|
|
135
72
|
* @returns {string[]}
|
|
136
73
|
*/
|
|
137
74
|
export function hostExternalSpecifiers() {
|
|
138
|
-
return [...
|
|
75
|
+
return [...CONTRACT.hostExternalSpecifiers];
|
|
139
76
|
}
|
|
140
77
|
|
|
141
78
|
function reactShimSrc() {
|
package/dist/hooks.js
CHANGED
|
@@ -3834,6 +3834,71 @@ export function useDatastoreSubscription(table, handlers, options) {
|
|
|
3834
3834
|
return { status };
|
|
3835
3835
|
}
|
|
3836
3836
|
|
|
3837
|
+
const _NO_TYPERS = Object.freeze([]);
|
|
3838
|
+
|
|
3839
|
+
/**
|
|
3840
|
+
* Who else is typing in one record; call `setTyping` per keystroke.
|
|
3841
|
+
* Never throws: a client without typing reports status "unavailable".
|
|
3842
|
+
*
|
|
3843
|
+
* @param {string} tableId
|
|
3844
|
+
* @param {string} recordId
|
|
3845
|
+
* @returns {{ typingUserIds: string[], setTyping: (active: boolean) => void, status: "connecting" | "live" | "unavailable" }}
|
|
3846
|
+
*/
|
|
3847
|
+
export function useTyping(tableId, recordId) {
|
|
3848
|
+
const ctx = useWidgetContextOrThrow("useTyping");
|
|
3849
|
+
const [typingUserIds, setTypingUserIds] = useState(_NO_TYPERS);
|
|
3850
|
+
const [status, setStatus] = useState("connecting");
|
|
3851
|
+
const handleRef = useRef(null);
|
|
3852
|
+
// `ctx` is a fresh identity per host render; the effect keys on the ids.
|
|
3853
|
+
const recordsRef = useRef(null);
|
|
3854
|
+
recordsRef.current =
|
|
3855
|
+
ctx.datastore && typeof ctx.datastore.records === "function"
|
|
3856
|
+
? ctx.datastore.records
|
|
3857
|
+
: null;
|
|
3858
|
+
|
|
3859
|
+
useEffect(() => {
|
|
3860
|
+
setTypingUserIds(_NO_TYPERS);
|
|
3861
|
+
setStatus("connecting");
|
|
3862
|
+
const unavailable = () => {
|
|
3863
|
+
setStatus("unavailable");
|
|
3864
|
+
return undefined;
|
|
3865
|
+
};
|
|
3866
|
+
if (!tableId || !recordId || typeof recordsRef.current !== "function") {
|
|
3867
|
+
return unavailable();
|
|
3868
|
+
}
|
|
3869
|
+
try {
|
|
3870
|
+
const ns = recordsRef.current(tableId);
|
|
3871
|
+
if (!ns || typeof ns.typing !== "function") return unavailable();
|
|
3872
|
+
let current = true;
|
|
3873
|
+
const handle = ns.typing(recordId, {
|
|
3874
|
+
onChange: (ids) => {
|
|
3875
|
+
if (current) setTypingUserIds(ids);
|
|
3876
|
+
},
|
|
3877
|
+
onStatus: (s) => {
|
|
3878
|
+
if (current) setStatus(s);
|
|
3879
|
+
},
|
|
3880
|
+
});
|
|
3881
|
+
handleRef.current = handle;
|
|
3882
|
+
return () => {
|
|
3883
|
+
current = false;
|
|
3884
|
+
handleRef.current = null;
|
|
3885
|
+
if (handle && typeof handle.stop === "function") handle.stop();
|
|
3886
|
+
};
|
|
3887
|
+
} catch (_) {
|
|
3888
|
+
return unavailable();
|
|
3889
|
+
}
|
|
3890
|
+
}, [tableId, recordId]);
|
|
3891
|
+
|
|
3892
|
+
const setTyping = useCallback((active) => {
|
|
3893
|
+
const handle = handleRef.current;
|
|
3894
|
+
if (handle && typeof handle.setTyping === "function") {
|
|
3895
|
+
handle.setTyping(active === true);
|
|
3896
|
+
}
|
|
3897
|
+
}, []);
|
|
3898
|
+
|
|
3899
|
+
return { typingUserIds, setTyping, status };
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3837
3902
|
/* ============================================================================
|
|
3838
3903
|
* ASSETS CLIENT — ctx.assets (@colixsystems/assets-client)
|
|
3839
3904
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -496,6 +496,14 @@ export interface DatastoreClient {
|
|
|
496
496
|
): Promise<unknown>;
|
|
497
497
|
revoke(permissionId: string): Promise<void>;
|
|
498
498
|
};
|
|
499
|
+
/** REQ-RT-08 — backs `useTyping`; optional, the hook degrades without it. */
|
|
500
|
+
typing?(
|
|
501
|
+
recordId: string,
|
|
502
|
+
handlers: {
|
|
503
|
+
onChange?(typingUserIds: string[]): void;
|
|
504
|
+
onStatus?(status: TypingStatus): void;
|
|
505
|
+
},
|
|
506
|
+
): { setTyping(active: boolean): void; stop(): void };
|
|
499
507
|
};
|
|
500
508
|
}
|
|
501
509
|
|
|
@@ -1410,6 +1418,27 @@ export function useDatastoreSubscription(
|
|
|
1410
1418
|
options?: DatastoreSubscriptionOptions,
|
|
1411
1419
|
): { status: DatastoreSubscriptionStatus };
|
|
1412
1420
|
|
|
1421
|
+
export type TypingStatus = "connecting" | "live" | "unavailable";
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* REQ-RT-08: who else is typing in one record, and announcing your own typing,
|
|
1425
|
+
* via `ctx.datastore.records(tableId).typing(recordId, ...)`. Call
|
|
1426
|
+
* `setTyping(true)` on every keystroke (throttled inside; `false` is sent
|
|
1427
|
+
* after 5 s idle) and `setTyping(false)` on send, clear or blur.
|
|
1428
|
+
* `typingUserIds` lists the other users typing; each drops out 6 s after
|
|
1429
|
+
* their last signal. Needs a signed-in user who can read the record.
|
|
1430
|
+
* Re-subscribes when `tableId` or `recordId` changes. Never throws — a host
|
|
1431
|
+
* whose datastore client lacks typing reports `status: "unavailable"`.
|
|
1432
|
+
*/
|
|
1433
|
+
export function useTyping(
|
|
1434
|
+
tableId: string | null | undefined,
|
|
1435
|
+
recordId: string | null | undefined,
|
|
1436
|
+
): {
|
|
1437
|
+
typingUserIds: string[];
|
|
1438
|
+
setTyping(active: boolean): void;
|
|
1439
|
+
status: TypingStatus;
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1413
1442
|
/**
|
|
1414
1443
|
* Stateful file-asset resolver hook. Returns `{ url, file, loading, error,
|
|
1415
1444
|
* refetch }`. The `url` is an absolute URL composed against the host's API
|
|
@@ -3380,6 +3409,10 @@ export interface AiWidgetContract {
|
|
|
3380
3409
|
readonly bundleExportContract: ReadonlyArray<ContractBundleShape>;
|
|
3381
3410
|
readonly bannedApis: ReadonlyArray<ContractBannedApi>;
|
|
3382
3411
|
readonly allowedBareImports: ReadonlyArray<string>;
|
|
3412
|
+
/** Bare specifiers the web host provides by exact name; a widget bundle never inlines them. */
|
|
3413
|
+
readonly hostExternalSpecifiers: ReadonlyArray<string>;
|
|
3414
|
+
/** Bundled vetted package roots: the only roots whose subpaths load in the web Player. */
|
|
3415
|
+
readonly subpathImportRoots: ReadonlyArray<string>;
|
|
3383
3416
|
/** sc-4686 — the render-time fallback currency when the host supplies none. */
|
|
3384
3417
|
readonly chargeCurrency: string;
|
|
3385
3418
|
/** sc-4686 — how each currency is written; read by `formatMoney`, never Intl. */
|
package/dist/index.js
CHANGED