@colixsystems/widget-sdk 0.117.0 → 0.119.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 +56 -2
- package/dist/contract.cjs +59 -6
- package/dist/contract.js +59 -6
- package/dist/devserver.js +92 -40
- package/dist/flatten-entry.js +817 -0
- package/dist/hooks.js +148 -9
- package/dist/host.d.ts +8 -4
- package/dist/index.d.ts +55 -0
- package/dist/linter.js +3 -146
- package/dist/source-mask.js +162 -0
- package/dist/theme-components.cjs +70 -10
- package/dist/theme-components.js +70 -10
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
35
35
|
| **CORE** | `useRefresh(handler)` | `void` | `ctx.refresh.subscribe` — no scope. Subscribes the handler to the page-level refresh tick (pull-to-refresh on mobile). Handler may return a Promise — the host waits for `allSettled` before clearing the spinner. The three datastore hooks auto-subscribe their own `refetch`; widgets only call this directly to re-run non-datastore work. No-op on a host that doesn't implement refresh. |
|
|
36
36
|
| **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
|
|
37
37
|
| **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` — wired by the Player and the Expo export; an authoring preview omits it and the call is a no-op — no scope |
|
|
38
|
-
| **CORE** | `useGeolocation(options?)` | `{ latitude, longitude, accuracy, loading, error, getCurrentPosition }` | `ctx.device.geolocation` — no scope. Capture is IMPERATIVE: call `getCurrentPosition()` from a user gesture (a tap), never on mount. Resolves to `{ latitude, longitude, accuracy }`; rejects with `GeolocationError` (`.code` in `PERMISSION_DENIED \| UNAVAILABLE \| TIMEOUT \| UNSUPPORTED \| INTERNAL`). Identical on web (`navigator.geolocation`) and the Expo export (`expo-location`). |
|
|
38
|
+
| **CORE** | `useGeolocation(options?)` | `{ latitude, longitude, accuracy, loading, error, getCurrentPosition, backgroundSupported, backgroundWatching, startBackgroundWatch, stopBackgroundWatch }` | `ctx.device.geolocation` — no scope. Capture is IMPERATIVE: call `getCurrentPosition()` from a user gesture (a tap), never on mount. Resolves to `{ latitude, longitude, accuracy }`; rejects with `GeolocationError` (`.code` in `PERMISSION_DENIED \| UNAVAILABLE \| TIMEOUT \| UNSUPPORTED \| INTERNAL`). Identical on web (`navigator.geolocation`) and the Expo export (`expo-location`). **Background watch (sc-6450)** — `startBackgroundWatch({ enableHighAccuracy, distanceIntervalMeters, timeIntervalMs })` keeps positions arriving while the app is backgrounded; `stopBackgroundWatch()` releases it. NATIVE-ONLY and opt-in per app: gate the control on `backgroundSupported` (false on web, and in an export whose workspace did not opt in). The watch outlives the widget's mount, and its positions land in the same `latitude`/`longitude`/`accuracy` slots. |
|
|
39
39
|
| **CORE** | `useSpeechToText(options?)` | `{ transcript, partial, listening, supported, error, start, stop, abort, reset }` | `ctx.device.speech` — no scope. Dictation with the device's **on-device** recogniser: no audio is uploaded and no AI credit is spent. Capture is IMPERATIVE: call `start()` from a user gesture (a tap), never on mount. `transcript` accumulates finalised speech, `partial` holds the uncommitted guess (needs `options.interimResults`); `stop()` keeps it, `abort()` discards it. Rejects with `SpeechToTextError` (`.code` in `PERMISSION_DENIED \| NO_SPEECH \| LANGUAGE_UNSUPPORTED \| NETWORK \| ABORTED \| UNSUPPORTED \| INTERNAL`). **Gate your mic button on `supported`** — Firefox ships no `SpeechRecognition`. Identical on web (`SpeechRecognition`) and the Expo export (`expo-speech-recognition`). |
|
|
40
40
|
| **CORE** | `useCamera(options?)` | `{ asset, loading, error, supported, capture, pick, reset }` | `ctx.device.camera` — no scope. Take a photo (`capture()`) or choose one (`pick()`). Capture is IMPERATIVE: call from a user gesture (a tap), never on mount. Both resolve a normalised `{ uri, name, mimeType, width, height, size, file }`, or **`null` when the user dismisses the picker** — dismissal is not an error, so no `try/catch` is needed on the happy path. Rejects with `CameraError` (`.code` in `PERMISSION_DENIED \| UNSUPPORTED \| INTERNAL`). `asset.file` is already the right upload part for the host, so `fd.append("file", asset.file)` → `ctx.assets.upload(fd)` is ONE code path on both. **Gate your camera button on `supported`.** Identical on web (file input) and the Expo export (`expo-image-picker`). |
|
|
41
41
|
| **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) first, then a **predefined shared key** (`shared.<key>`) when `key` is one of the standard strings (`submit`, `cancel`, `save`, `loading`, …), then the raw key. Use a shared key for an identical default string so it translates once and any per-instance `widget.<id>.<key>` override still wins. |
|
|
@@ -70,7 +70,61 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
70
70
|
|
|
71
71
|
## Status
|
|
72
72
|
|
|
73
|
-
`v0.
|
|
73
|
+
`v0.119.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**.
|
|
74
|
+
|
|
75
|
+
### What's new in 0.119.0 (contract 1.91.0)
|
|
76
|
+
|
|
77
|
+
**`useGeolocation()` can now track location while the app is BACKGROUNDED (sc-6450).** The hook only ever read a position while the app was in the foreground, so the whole class of field-work apps — delivery tracking, site visits, mileage and timesheet logging — could not be built. Its result gains four members; the existing foreground API is untouched:
|
|
78
|
+
|
|
79
|
+
| Member | Shape | Notes |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `backgroundSupported` | `boolean` | **Check this before rendering the control.** |
|
|
82
|
+
| `backgroundWatching` | `boolean` | Whether a watch is running on this device. |
|
|
83
|
+
| `startBackgroundWatch` | `(options?) => Promise<void>` | `options`: `{ enableHighAccuracy, distanceIntervalMeters, timeIntervalMs }`. |
|
|
84
|
+
| `stopBackgroundWatch` | `() => Promise<void>` | Releases the OS subscription. |
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
const { latitude, longitude, backgroundSupported, backgroundWatching, startBackgroundWatch, stopBackgroundWatch } = useGeolocation();
|
|
88
|
+
|
|
89
|
+
{backgroundSupported && (
|
|
90
|
+
<Button
|
|
91
|
+
label={backgroundWatching ? "Stop trip" : "Start trip"}
|
|
92
|
+
onPress={() => (backgroundWatching ? stopBackgroundWatch() : startBackgroundWatch({ distanceIntervalMeters: 50 }))}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The watch is **native-only and opt-in per app**. `backgroundSupported` is `false` on the web Player — a browser tab genuinely cannot track in the background, the same capability-gated honesty `useSpeechToText` applies on Firefox — and it is also `false` in an exported app whose workspace has not enabled background location in **Publishing Settings**. That opt-in exists so an app that never uses the capability declares no background mode and stays clear of the extra store review.
|
|
98
|
+
|
|
99
|
+
Three behaviours to design around. Tracking covers the app **running in the background**; it does not survive the OS terminating the app, so don't promise an unattended log. The watch **outlives the widget's mount** — that is the point — so it is released only by `stopBackgroundWatch()`, never on unmount; `backgroundWatching` is seeded from the host so a remounted widget reports a running watch honestly. And background positions land in the **same** `latitude` / `longitude` / `accuracy` slots as the foreground read, so a widget renders one position regardless of how it arrived.
|
|
100
|
+
|
|
101
|
+
`startBackgroundWatch()` rejects with the existing `GeolocationError`: `.code` `UNSUPPORTED` when the host or build does not offer the capability, `PERMISSION_DENIED` when the user refuses always-on location. On Android 11+ the always-on grant cannot be made from a runtime dialog — the user has to pick "Allow all the time" in system Settings — so treat `PERMISSION_DENIED` as a prompt to explain that, not as a dead end.
|
|
102
|
+
|
|
103
|
+
`backgroundWatching` is a mirror of the host, not of your calls: the OS can end the watch on its own (a permission downgrade, a killed foreground service) and a sibling widget can start or stop it, so render from the flag rather than from whether you called `start`.
|
|
104
|
+
|
|
105
|
+
**Store review:** Apple and Google both require a *visible user benefit* plus a justification for background location. Your app must show the user that tracking is running and let them stop it, and your store listing must explain why the app needs it. A build that turns the opt-in on without that is rejected at review.
|
|
106
|
+
|
|
107
|
+
`CONTRACT.version` → `1.91.0`. Additive — four new result members and six new optional `ctx.device.geolocation` broker members; no existing export changed signature.
|
|
108
|
+
### What's new in 0.118.0 (contract 1.90.0)
|
|
109
|
+
|
|
110
|
+
**A `styleSchema` field's `default` now actually applies.** Declaring `default` on a style field wrote it into the manifest and nothing ever read it back, so a widget's own styling baseline — and any design saved from the Widget Builder preview — was silently dropped on the next render. The host now resolves it onto `props.style`.
|
|
111
|
+
|
|
112
|
+
It is the **weakest** layer, deliberately: it applies only when nothing above it sets that field.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
styleSchema `default` -> palette / components.<scope> -> widgetStyles[manifestId] -> per-instance props.style
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
So a workspace theme still outranks a widget's own baseline, and a field you leave undefaulted keeps following the theme exactly as before. Nothing changes for a widget that declares no style defaults.
|
|
119
|
+
|
|
120
|
+
The host still does **not** apply style to elements — your widget owns placement and keeps reading `props.style.<field>` (or `useWidgetStyle()`) and applying each value where it chooses:
|
|
121
|
+
|
|
122
|
+
```jsx
|
|
123
|
+
const style = useWidgetStyle();
|
|
124
|
+
<View style={[styles.card, style.cardBackground && { backgroundColor: style.cardBackground }]}>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Keep `themeDefault` for a fallback that IS a theme token: it stays display-only (a greyed placeholder in the Style panel) so the field tracks the workspace theme. Use `default` for a literal constant your code genuinely falls back to — that value now reaches `props.style`, so it must match what your code applies.
|
|
74
128
|
|
|
75
129
|
### What's new in 0.117.0 (contract 1.89.0)
|
|
76
130
|
|
package/dist/contract.cjs
CHANGED
|
@@ -1570,7 +1570,17 @@ const HOOKS = [
|
|
|
1570
1570
|
"permission prompt on a gesture, so it NEVER fires on mount. The promise resolves to { latitude, longitude, accuracy } " +
|
|
1571
1571
|
"and stores the same values on the hook; it rejects with a GeolocationError whose .code is one of PERMISSION_DENIED | " +
|
|
1572
1572
|
"UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL. options pass through to the host ({ enableHighAccuracy, timeout, " +
|
|
1573
|
-
"maximumAge }). Identical on web (navigator.geolocation) and the Expo export (expo-location)."
|
|
1573
|
+
"maximumAge }). Identical on web (navigator.geolocation) and the Expo export (expo-location). " +
|
|
1574
|
+
"BACKGROUND WATCH (sc-6450): startBackgroundWatch(options?) keeps positions arriving while the app is BACKGROUNDED — the " +
|
|
1575
|
+
"field-work case (delivery tracking, site visits, mileage logging) — and stopBackgroundWatch() releases the OS " +
|
|
1576
|
+
"subscription. It is NATIVE-ONLY and opt-in per app: ALWAYS check `backgroundSupported` before rendering the control, " +
|
|
1577
|
+
"because the web Player reports false (a browser tab cannot track in the background) and so does an exported app whose " +
|
|
1578
|
+
"workspace has not enabled background location in Publishing Settings. The watch OUTLIVES the widget mount by design, so " +
|
|
1579
|
+
"it stops ONLY on stopBackgroundWatch(); `backgroundWatching` is seeded from the host so a remounted widget reports it " +
|
|
1580
|
+
"honestly. Background positions land in the SAME latitude/longitude/accuracy slots. startBackgroundWatch rejects with a " +
|
|
1581
|
+
"GeolocationError (.code UNSUPPORTED when the host or build does not offer it, PERMISSION_DENIED when the user refuses " +
|
|
1582
|
+
"always-on location). options: { enableHighAccuracy, distanceIntervalMeters, timeIntervalMs }. Tracking continues while the " +
|
|
1583
|
+
"app is running in the BACKGROUND; it does not survive the OS terminating the app, so do not promise an unattended log.",
|
|
1574
1584
|
returnShape: {
|
|
1575
1585
|
latitude: "number | null",
|
|
1576
1586
|
longitude: "number | null",
|
|
@@ -1579,6 +1589,13 @@ const HOOKS = [
|
|
|
1579
1589
|
error: "GeolocationError | null",
|
|
1580
1590
|
getCurrentPosition:
|
|
1581
1591
|
"() => Promise<{ latitude, longitude, accuracy }> // rejects with GeolocationError",
|
|
1592
|
+
backgroundSupported:
|
|
1593
|
+
"boolean // GATE THE CONTROL ON THIS: false on the web Player and in an export that did not opt in",
|
|
1594
|
+
backgroundWatching: "boolean // a background watch is running on this device",
|
|
1595
|
+
startBackgroundWatch:
|
|
1596
|
+
"(options?) => Promise<void> // NATIVE-ONLY; { enableHighAccuracy, distanceIntervalMeters, timeIntervalMs }; rejects with GeolocationError",
|
|
1597
|
+
stopBackgroundWatch:
|
|
1598
|
+
"() => Promise<void> // the ONLY release - the watch outlives the widget's mount",
|
|
1582
1599
|
},
|
|
1583
1600
|
requiredContextSlice: [],
|
|
1584
1601
|
scopes: null,
|
|
@@ -1967,8 +1984,11 @@ const MANIFEST_SCHEMA = {
|
|
|
1967
1984
|
"exposes; the Studio Properties Panel renders a \"Style\" section from " +
|
|
1968
1985
|
"it. The author's resolved values are delivered to the widget under " +
|
|
1969
1986
|
"props.style (one object keyed by style-field name); the widget reads " +
|
|
1970
|
-
"props.style.<field> and applies each wherever it chooses.
|
|
1971
|
-
"
|
|
1987
|
+
"props.style.<field> and applies each wherever it chooses. A field's " +
|
|
1988
|
+
"own `default` is resolved onto props.style as the WEAKEST layer " +
|
|
1989
|
+
"(sc-6750): it applies only when no theme layer and no per-instance " +
|
|
1990
|
+
"value sets that field. The host never applies style to elements " +
|
|
1991
|
+
"— the widget owns placement.",
|
|
1972
1992
|
default: {},
|
|
1973
1993
|
},
|
|
1974
1994
|
rendersOwnChrome: {
|
|
@@ -2256,7 +2276,10 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2256
2276
|
device: {
|
|
2257
2277
|
description:
|
|
2258
2278
|
"Optional host-brokered device capabilities. " +
|
|
2259
|
-
"{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }
|
|
2279
|
+
"{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }>, " +
|
|
2280
|
+
"isBackgroundSupported() -> boolean, startBackgroundWatch(options?) -> Promise<void>, stopBackgroundWatch() -> Promise<void>, " +
|
|
2281
|
+
"isBackgroundWatching() -> boolean, subscribeBackgroundPositions(cb) -> unsubscribe, " +
|
|
2282
|
+
"subscribeBackgroundWatchState(cb) -> unsubscribe }, " +
|
|
2260
2283
|
"speech: { isSupported() -> boolean, start(options, { onResult, onError, onEnd }) -> Promise<{ stop(), abort() }> }, " +
|
|
2261
2284
|
"camera: { isSupported() -> boolean, capture(options?) -> Promise<asset | null>, pick(options?) -> Promise<asset | null> } }. " +
|
|
2262
2285
|
"Backs useGeolocation(), useSpeechToText() and useCamera(). The web Player brokers them via navigator.geolocation, " +
|
|
@@ -2266,7 +2289,14 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2266
2289
|
"speech.start streams { transcript, isFinal } to onResult and runs ON DEVICE — it uploads no audio and spends no AI credit; " +
|
|
2267
2290
|
"its onError carries the Web Speech error vocabulary (not-allowed | no-speech | language-not-supported | network | aborted). " +
|
|
2268
2291
|
"camera.capture/pick resolve a normalised { uri, name, mimeType, width, height, size, file, release? } or NULL when the " +
|
|
2269
|
-
"user dismisses the picker, and reject with a CameraError (.code PERMISSION_DENIED | UNSUPPORTED | INTERNAL)."
|
|
2292
|
+
"user dismisses the picker, and reject with a CameraError (.code PERMISSION_DENIED | UNSUPPORTED | INTERNAL). " +
|
|
2293
|
+
"sc-6450 — the geolocation background-watch members are NATIVE-ONLY and opt-in per app: the web Player and an export that " +
|
|
2294
|
+
"did not opt in both report isBackgroundSupported() false, and the Expo export backs it with expo-location + " +
|
|
2295
|
+
"expo-task-manager. subscribeBackgroundPositions and subscribeBackgroundWatchState are plain subscriptions — they start no " +
|
|
2296
|
+
"sensor and prompt for nothing, so the watch can outlive any one widget mount. The HOST owns whether a watch is running: " +
|
|
2297
|
+
"it is primed asynchronously after a cold relaunch, the OS can end it on its own (a permission downgrade, a killed " +
|
|
2298
|
+
"foreground service), and a sibling widget may start or stop it — so subscribeBackgroundWatchState is how every mounted " +
|
|
2299
|
+
"widget stays truthful, and isBackgroundWatching() is only the synchronous first read.",
|
|
2270
2300
|
required: false,
|
|
2271
2301
|
fields: { geolocation: "object", speech: "object", camera: "object" },
|
|
2272
2302
|
},
|
|
@@ -3515,7 +3545,30 @@ const CONTRACT = deepFreeze({
|
|
|
3515
3545
|
// web build reads deviceorientation angles rather than real acceleration —
|
|
3516
3546
|
// the web half is a widget.web.jsx over window.DeviceMotionEvent. Pinned in
|
|
3517
3547
|
// the compiler's export dependencies, like every other native-module member.
|
|
3518
|
-
|
|
3548
|
+
// 1.90.0: additive (sc-6750) — a `styleSchema` field's declared `default`
|
|
3549
|
+
// now RESOLVES onto `props.style`, as the weakest style layer. It was
|
|
3550
|
+
// written into the manifest and then read by nothing, so an author's saved
|
|
3551
|
+
// colour was silently dropped on the next render. Precedence is unchanged
|
|
3552
|
+
// above it: styleSchema `default` -> palette/`components.<scope>` ->
|
|
3553
|
+
// `widgetStyles[manifestId]` -> per-instance `props.style`, so a workspace
|
|
3554
|
+
// theme still outranks a widget's own baseline and a field the author never
|
|
3555
|
+
// defaulted follows the theme exactly as before. The host still never
|
|
3556
|
+
// applies style to elements — the widget owns placement.
|
|
3557
|
+
// 1.91.0: additive (sc-6450) — the geolocation BACKGROUND watch:
|
|
3558
|
+
// `useGeolocation()` gains backgroundSupported / backgroundWatching /
|
|
3559
|
+
// startBackgroundWatch / stopBackgroundWatch, and the optional
|
|
3560
|
+
// `device.geolocation` broker gains isBackgroundSupported,
|
|
3561
|
+
// startBackgroundWatch, stopBackgroundWatch, isBackgroundWatching,
|
|
3562
|
+
// subscribeBackgroundPositions and subscribeBackgroundWatchState. Field-work
|
|
3563
|
+
// apps — delivery tracking, site visits, mileage — could not be built at all
|
|
3564
|
+
// while a position could only be read in the foreground. Native-only and
|
|
3565
|
+
// opt-in per app: the web Player reports backgroundSupported false because a
|
|
3566
|
+
// tab is suspended once backgrounded, and so does an export whose workspace
|
|
3567
|
+
// did not opt in. The HOST owns whether a watch is running (it is primed
|
|
3568
|
+
// asynchronously after an OS relaunch, the OS can end it, and a sibling
|
|
3569
|
+
// widget can start or stop it), so subscribeBackgroundWatchState is how every
|
|
3570
|
+
// mounted widget stays truthful.
|
|
3571
|
+
version: "1.91.0",
|
|
3519
3572
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3520
3573
|
hooks: HOOKS,
|
|
3521
3574
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1570,7 +1570,17 @@ const HOOKS = [
|
|
|
1570
1570
|
"permission prompt on a gesture, so it NEVER fires on mount. The promise resolves to { latitude, longitude, accuracy } " +
|
|
1571
1571
|
"and stores the same values on the hook; it rejects with a GeolocationError whose .code is one of PERMISSION_DENIED | " +
|
|
1572
1572
|
"UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL. options pass through to the host ({ enableHighAccuracy, timeout, " +
|
|
1573
|
-
"maximumAge }). Identical on web (navigator.geolocation) and the Expo export (expo-location)."
|
|
1573
|
+
"maximumAge }). Identical on web (navigator.geolocation) and the Expo export (expo-location). " +
|
|
1574
|
+
"BACKGROUND WATCH (sc-6450): startBackgroundWatch(options?) keeps positions arriving while the app is BACKGROUNDED — the " +
|
|
1575
|
+
"field-work case (delivery tracking, site visits, mileage logging) — and stopBackgroundWatch() releases the OS " +
|
|
1576
|
+
"subscription. It is NATIVE-ONLY and opt-in per app: ALWAYS check `backgroundSupported` before rendering the control, " +
|
|
1577
|
+
"because the web Player reports false (a browser tab cannot track in the background) and so does an exported app whose " +
|
|
1578
|
+
"workspace has not enabled background location in Publishing Settings. The watch OUTLIVES the widget mount by design, so " +
|
|
1579
|
+
"it stops ONLY on stopBackgroundWatch(); `backgroundWatching` is seeded from the host so a remounted widget reports it " +
|
|
1580
|
+
"honestly. Background positions land in the SAME latitude/longitude/accuracy slots. startBackgroundWatch rejects with a " +
|
|
1581
|
+
"GeolocationError (.code UNSUPPORTED when the host or build does not offer it, PERMISSION_DENIED when the user refuses " +
|
|
1582
|
+
"always-on location). options: { enableHighAccuracy, distanceIntervalMeters, timeIntervalMs }. Tracking continues while the " +
|
|
1583
|
+
"app is running in the BACKGROUND; it does not survive the OS terminating the app, so do not promise an unattended log.",
|
|
1574
1584
|
returnShape: {
|
|
1575
1585
|
latitude: "number | null",
|
|
1576
1586
|
longitude: "number | null",
|
|
@@ -1579,6 +1589,13 @@ const HOOKS = [
|
|
|
1579
1589
|
error: "GeolocationError | null",
|
|
1580
1590
|
getCurrentPosition:
|
|
1581
1591
|
"() => Promise<{ latitude, longitude, accuracy }> // rejects with GeolocationError",
|
|
1592
|
+
backgroundSupported:
|
|
1593
|
+
"boolean // GATE THE CONTROL ON THIS: false on the web Player and in an export that did not opt in",
|
|
1594
|
+
backgroundWatching: "boolean // a background watch is running on this device",
|
|
1595
|
+
startBackgroundWatch:
|
|
1596
|
+
"(options?) => Promise<void> // NATIVE-ONLY; { enableHighAccuracy, distanceIntervalMeters, timeIntervalMs }; rejects with GeolocationError",
|
|
1597
|
+
stopBackgroundWatch:
|
|
1598
|
+
"() => Promise<void> // the ONLY release - the watch outlives the widget's mount",
|
|
1582
1599
|
},
|
|
1583
1600
|
requiredContextSlice: [],
|
|
1584
1601
|
scopes: null,
|
|
@@ -1967,8 +1984,11 @@ const MANIFEST_SCHEMA = {
|
|
|
1967
1984
|
"exposes; the Studio Properties Panel renders a \"Style\" section from " +
|
|
1968
1985
|
"it. The author's resolved values are delivered to the widget under " +
|
|
1969
1986
|
"props.style (one object keyed by style-field name); the widget reads " +
|
|
1970
|
-
"props.style.<field> and applies each wherever it chooses.
|
|
1971
|
-
"
|
|
1987
|
+
"props.style.<field> and applies each wherever it chooses. A field's " +
|
|
1988
|
+
"own `default` is resolved onto props.style as the WEAKEST layer " +
|
|
1989
|
+
"(sc-6750): it applies only when no theme layer and no per-instance " +
|
|
1990
|
+
"value sets that field. The host never applies style to elements " +
|
|
1991
|
+
"— the widget owns placement.",
|
|
1972
1992
|
default: {},
|
|
1973
1993
|
},
|
|
1974
1994
|
rendersOwnChrome: {
|
|
@@ -2256,7 +2276,10 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2256
2276
|
device: {
|
|
2257
2277
|
description:
|
|
2258
2278
|
"Optional host-brokered device capabilities. " +
|
|
2259
|
-
"{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }
|
|
2279
|
+
"{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }>, " +
|
|
2280
|
+
"isBackgroundSupported() -> boolean, startBackgroundWatch(options?) -> Promise<void>, stopBackgroundWatch() -> Promise<void>, " +
|
|
2281
|
+
"isBackgroundWatching() -> boolean, subscribeBackgroundPositions(cb) -> unsubscribe, " +
|
|
2282
|
+
"subscribeBackgroundWatchState(cb) -> unsubscribe }, " +
|
|
2260
2283
|
"speech: { isSupported() -> boolean, start(options, { onResult, onError, onEnd }) -> Promise<{ stop(), abort() }> }, " +
|
|
2261
2284
|
"camera: { isSupported() -> boolean, capture(options?) -> Promise<asset | null>, pick(options?) -> Promise<asset | null> } }. " +
|
|
2262
2285
|
"Backs useGeolocation(), useSpeechToText() and useCamera(). The web Player brokers them via navigator.geolocation, " +
|
|
@@ -2266,7 +2289,14 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2266
2289
|
"speech.start streams { transcript, isFinal } to onResult and runs ON DEVICE — it uploads no audio and spends no AI credit; " +
|
|
2267
2290
|
"its onError carries the Web Speech error vocabulary (not-allowed | no-speech | language-not-supported | network | aborted). " +
|
|
2268
2291
|
"camera.capture/pick resolve a normalised { uri, name, mimeType, width, height, size, file, release? } or NULL when the " +
|
|
2269
|
-
"user dismisses the picker, and reject with a CameraError (.code PERMISSION_DENIED | UNSUPPORTED | INTERNAL)."
|
|
2292
|
+
"user dismisses the picker, and reject with a CameraError (.code PERMISSION_DENIED | UNSUPPORTED | INTERNAL). " +
|
|
2293
|
+
"sc-6450 — the geolocation background-watch members are NATIVE-ONLY and opt-in per app: the web Player and an export that " +
|
|
2294
|
+
"did not opt in both report isBackgroundSupported() false, and the Expo export backs it with expo-location + " +
|
|
2295
|
+
"expo-task-manager. subscribeBackgroundPositions and subscribeBackgroundWatchState are plain subscriptions — they start no " +
|
|
2296
|
+
"sensor and prompt for nothing, so the watch can outlive any one widget mount. The HOST owns whether a watch is running: " +
|
|
2297
|
+
"it is primed asynchronously after a cold relaunch, the OS can end it on its own (a permission downgrade, a killed " +
|
|
2298
|
+
"foreground service), and a sibling widget may start or stop it — so subscribeBackgroundWatchState is how every mounted " +
|
|
2299
|
+
"widget stays truthful, and isBackgroundWatching() is only the synchronous first read.",
|
|
2270
2300
|
required: false,
|
|
2271
2301
|
fields: { geolocation: "object", speech: "object", camera: "object" },
|
|
2272
2302
|
},
|
|
@@ -3515,7 +3545,30 @@ const CONTRACT = deepFreeze({
|
|
|
3515
3545
|
// web build reads deviceorientation angles rather than real acceleration —
|
|
3516
3546
|
// the web half is a widget.web.jsx over window.DeviceMotionEvent. Pinned in
|
|
3517
3547
|
// the compiler's export dependencies, like every other native-module member.
|
|
3518
|
-
|
|
3548
|
+
// 1.90.0: additive (sc-6750) — a `styleSchema` field's declared `default`
|
|
3549
|
+
// now RESOLVES onto `props.style`, as the weakest style layer. It was
|
|
3550
|
+
// written into the manifest and then read by nothing, so an author's saved
|
|
3551
|
+
// colour was silently dropped on the next render. Precedence is unchanged
|
|
3552
|
+
// above it: styleSchema `default` -> palette/`components.<scope>` ->
|
|
3553
|
+
// `widgetStyles[manifestId]` -> per-instance `props.style`, so a workspace
|
|
3554
|
+
// theme still outranks a widget's own baseline and a field the author never
|
|
3555
|
+
// defaulted follows the theme exactly as before. The host still never
|
|
3556
|
+
// applies style to elements — the widget owns placement.
|
|
3557
|
+
// 1.91.0: additive (sc-6450) — the geolocation BACKGROUND watch:
|
|
3558
|
+
// `useGeolocation()` gains backgroundSupported / backgroundWatching /
|
|
3559
|
+
// startBackgroundWatch / stopBackgroundWatch, and the optional
|
|
3560
|
+
// `device.geolocation` broker gains isBackgroundSupported,
|
|
3561
|
+
// startBackgroundWatch, stopBackgroundWatch, isBackgroundWatching,
|
|
3562
|
+
// subscribeBackgroundPositions and subscribeBackgroundWatchState. Field-work
|
|
3563
|
+
// apps — delivery tracking, site visits, mileage — could not be built at all
|
|
3564
|
+
// while a position could only be read in the foreground. Native-only and
|
|
3565
|
+
// opt-in per app: the web Player reports backgroundSupported false because a
|
|
3566
|
+
// tab is suspended once backgrounded, and so does an export whose workspace
|
|
3567
|
+
// did not opt in. The HOST owns whether a watch is running (it is primed
|
|
3568
|
+
// asynchronously after an OS relaunch, the OS can end it, and a sibling
|
|
3569
|
+
// widget can start or stop it), so subscribeBackgroundWatchState is how every
|
|
3570
|
+
// mounted widget stays truthful.
|
|
3571
|
+
version: "1.91.0",
|
|
3519
3572
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3520
3573
|
hooks: HOOKS,
|
|
3521
3574
|
primitives: PRIMITIVES,
|
package/dist/devserver.js
CHANGED
|
@@ -67,32 +67,38 @@ import {
|
|
|
67
67
|
shimSpecifierFromSlug,
|
|
68
68
|
} from "./dev-shims.js";
|
|
69
69
|
import { bundleWebEntry } from "./webbundle.js";
|
|
70
|
+
import {
|
|
71
|
+
findImportStatements,
|
|
72
|
+
findRelativeImportStatements,
|
|
73
|
+
} from "./flatten-entry.js";
|
|
70
74
|
|
|
71
75
|
// Bare-import / relative-import detection. The loader rewrites bare specifiers
|
|
72
76
|
// in the ENTRY to client-side blob shims, so they resolve fine. Relative
|
|
73
77
|
// imports in a SINGLE-FILE entry have nowhere to resolve — single-file mode
|
|
74
78
|
// refuses them with guidance. In directory mode, relative imports in the entry
|
|
75
79
|
// AND in siblings are rewritten to absolute dev-server URLs.
|
|
76
|
-
const RELATIVE_IMPORT_RE =
|
|
77
|
-
/(?:^|\n)\s*(?:import|export)[^;\n]*?from\s*['"](\.\.?\/[^'"]+)['"]/g;
|
|
78
|
-
|
|
79
|
-
// Static-import / static-export-from anchored at a statement boundary
|
|
80
|
-
// (start-of-line, `;`, or newline before `import`/`export`) so the rewriter
|
|
81
|
-
// doesn't touch occurrences inside string literals or comment bodies. The
|
|
82
|
-
// dynamic-import form (`import("...")`) is matched separately by
|
|
83
|
-
// `DYNAMIC_IMPORT_RE` below — it's safe to anchor in the same way because
|
|
84
|
-
// it's also valid only as an expression.
|
|
85
80
|
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
81
|
+
// STATIC imports are found by the packer's scanner (`findImportStatements`),
|
|
82
|
+
// never by a local regex. sc-6086 removed RELATIVE_IMPORT_RE and sc-6670 the
|
|
83
|
+
// rewriter's STATIC_IMPORT_RE for the same defect: a `[^'";\n]` clause body
|
|
84
|
+
// cannot span newlines, so a multi-line `import {\n a,\n} from "react"` — the
|
|
85
|
+
// norm in this codebase — read as no import at all and was served verbatim.
|
|
86
|
+
// The scanner walks the clause character by character, so line breaks are
|
|
87
|
+
// nothing special, and it returns the specifier's quote offsets so the
|
|
88
|
+
// rewriter splices exactly the specifier and can never bleed into an adjacent
|
|
89
|
+
// statement. One scanner for the guard, the packer, and the rewriter (§3).
|
|
90
|
+
|
|
91
|
+
// Dynamic import (`import("…")`) is an EXPRESSION, valid at any brace depth,
|
|
92
|
+
// so the statement scanner does not see it — it keeps its own regex. That
|
|
93
|
+
// regex separates the specifier from `import` with `\s*`, which does match
|
|
94
|
+
// newlines, so it has no multi-line gap (sc-6670 verified).
|
|
95
|
+
//
|
|
96
|
+
// Capture groups:
|
|
97
|
+
// 1 = `import` + whitespace + `(` (and the leading boundary char, preserved
|
|
98
|
+
// verbatim during replace so the call isn't glued onto what precedes it)
|
|
90
99
|
// 2 = opening quote
|
|
91
100
|
// 3 = specifier
|
|
92
|
-
const
|
|
93
|
-
/((?:^|[\n;])\s*(?:import|export)\s+(?:[^'";\n]*?\s+from\s*)?)(['"])([^'"]+)\2/gm;
|
|
94
|
-
const DYNAMIC_IMPORT_RE =
|
|
95
|
-
/((?:^|[\s;\(,!?:=])import\s*\(\s*)(['"])([^'"]+)\2/g;
|
|
101
|
+
const DYNAMIC_IMPORT_RE = /((?:^|[\s;\(,!?:=])import\s*\(\s*)(['"])([^'"]+)\2/g;
|
|
96
102
|
|
|
97
103
|
/**
|
|
98
104
|
* Lazily resolve `sucrase`'s `transform`. Kept out of the module's static
|
|
@@ -137,15 +143,21 @@ export function transpile(transform, source) {
|
|
|
137
143
|
* single-file mode to reject split-impl bundles with a clear message instead
|
|
138
144
|
* of serving a module the browser can't resolve.
|
|
139
145
|
*
|
|
146
|
+
* sc-6086: this delegates to the packer's statement scanner rather than
|
|
147
|
+
* `RELATIVE_IMPORT_RE`, whose `[^;\n]` body cannot span newlines — so a
|
|
148
|
+
* MULTI-LINE `import {\n a,\n b,\n} from "./lib/x.js"` was invisible to it.
|
|
149
|
+
* That blind spot is why `worktime-employer` passed every local check and
|
|
150
|
+
* still shipped an unresolvable import to the marketplace. One scanner, so
|
|
151
|
+
* the dev guard and the packer agree on what a relative import is
|
|
152
|
+
* (CLAUDE.md §3).
|
|
153
|
+
*
|
|
140
154
|
* @param {string} source
|
|
141
155
|
* @returns {string[]} unique relative specifiers
|
|
142
156
|
*/
|
|
143
157
|
export function findRelativeImports(source) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
while ((m = RELATIVE_IMPORT_RE.exec(source))) out.add(m[1]);
|
|
148
|
-
return Array.from(out);
|
|
158
|
+
return Array.from(
|
|
159
|
+
new Set(findRelativeImportStatements(source).map((s) => s.specifier)),
|
|
160
|
+
);
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
/**
|
|
@@ -201,15 +213,16 @@ export function resolveRelativeImport(fromRel, specifier) {
|
|
|
201
213
|
export function rewriteImportsForDirectoryMode(source, ctx) {
|
|
202
214
|
const { fromRel, baseUrl, shimmable } = ctx;
|
|
203
215
|
const unresolved = new Set();
|
|
204
|
-
|
|
216
|
+
// The URL a specifier should become, or null to leave it exactly as written.
|
|
217
|
+
const resolveSpecifier = (spec) => {
|
|
205
218
|
// Relative — resolve under the widget dir.
|
|
206
219
|
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
207
220
|
const rel = resolveRelativeImport(fromRel, spec);
|
|
208
221
|
if (!rel) {
|
|
209
222
|
unresolved.add(spec);
|
|
210
|
-
return
|
|
223
|
+
return null;
|
|
211
224
|
}
|
|
212
|
-
return `${
|
|
225
|
+
return `${baseUrl}/file/${rel}`;
|
|
213
226
|
}
|
|
214
227
|
// Absolute or already-resolved — leave alone.
|
|
215
228
|
if (
|
|
@@ -219,21 +232,36 @@ export function rewriteImportsForDirectoryMode(source, ctx) {
|
|
|
219
232
|
spec.startsWith("blob:") ||
|
|
220
233
|
spec.startsWith("data:")
|
|
221
234
|
) {
|
|
222
|
-
return
|
|
235
|
+
return null;
|
|
223
236
|
}
|
|
224
237
|
// Bare — rewrite if shimmable, else surface as unresolved.
|
|
225
238
|
if (shimmable.includes(spec)) {
|
|
226
|
-
return `${
|
|
239
|
+
return `${baseUrl}/shim/${shimSlugFor(spec)}`;
|
|
227
240
|
}
|
|
228
241
|
unresolved.add(spec);
|
|
229
|
-
return
|
|
242
|
+
return null;
|
|
230
243
|
};
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
244
|
+
|
|
245
|
+
// Pass 1 — static imports, spliced by the scanner's quote offsets. Splicing
|
|
246
|
+
// only what sits BETWEEN the quotes is what makes span bleed impossible:
|
|
247
|
+
// the clause, the trailing `;`, and any adjacent statement are never part of
|
|
248
|
+
// the replaced range. Walk back-to-front so earlier offsets stay valid.
|
|
249
|
+
const statements = findImportStatements(source);
|
|
250
|
+
let out = source;
|
|
251
|
+
for (let i = statements.length - 1; i >= 0; i -= 1) {
|
|
252
|
+
const { specifier, quoteStart, quoteEnd } = statements[i];
|
|
253
|
+
const url = resolveSpecifier(specifier);
|
|
254
|
+
if (url === null) continue;
|
|
255
|
+
out = out.slice(0, quoteStart + 1) + url + out.slice(quoteEnd);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Pass 2 — dynamic `import("…")`, an expression the statement scanner does
|
|
259
|
+
// not report. Its own anchor keeps it off substrings inside string literals
|
|
260
|
+
// like `const s = "import('react')"`.
|
|
261
|
+
out = out.replace(DYNAMIC_IMPORT_RE, (match, prefix, quote, spec) => {
|
|
262
|
+
const url = resolveSpecifier(spec);
|
|
263
|
+
return url === null ? match : `${prefix}${quote}${url}${quote}`;
|
|
264
|
+
});
|
|
237
265
|
return { code: out, unresolved: Array.from(unresolved) };
|
|
238
266
|
}
|
|
239
267
|
|
|
@@ -315,7 +343,10 @@ export function loadWidgetJson(widgetDir) {
|
|
|
315
343
|
const candidates = [
|
|
316
344
|
resolve(widgetDir, p),
|
|
317
345
|
p.startsWith(`first-party-widgets/${widgetSlug}/`)
|
|
318
|
-
? resolve(
|
|
346
|
+
? resolve(
|
|
347
|
+
widgetDir,
|
|
348
|
+
p.slice(`first-party-widgets/${widgetSlug}/`.length),
|
|
349
|
+
)
|
|
319
350
|
: null,
|
|
320
351
|
resolve(widgetDir, "..", "..", p),
|
|
321
352
|
].filter(Boolean);
|
|
@@ -326,7 +357,10 @@ export function loadWidgetJson(widgetDir) {
|
|
|
326
357
|
`${configPath}: ${label} must point at a file under ${widgetDir} (got "${p}")`,
|
|
327
358
|
);
|
|
328
359
|
}
|
|
329
|
-
const manifestAbs = _resolveUnderWidget(
|
|
360
|
+
const manifestAbs = _resolveUnderWidget(
|
|
361
|
+
"manifestSource",
|
|
362
|
+
config.manifestSource,
|
|
363
|
+
);
|
|
330
364
|
|
|
331
365
|
let entryAbs;
|
|
332
366
|
if (config.componentSources && typeof config.componentSources === "object") {
|
|
@@ -353,7 +387,12 @@ export function loadWidgetJson(widgetDir) {
|
|
|
353
387
|
);
|
|
354
388
|
}
|
|
355
389
|
const entryRel = relative(widgetDir, entryAbs).split(sep).join("/");
|
|
356
|
-
return {
|
|
390
|
+
return {
|
|
391
|
+
widgetDir,
|
|
392
|
+
manifestPath: manifestAbs,
|
|
393
|
+
entryPath: entryAbs,
|
|
394
|
+
entryRel,
|
|
395
|
+
};
|
|
357
396
|
}
|
|
358
397
|
|
|
359
398
|
function _isUnder(parentAbs, childAbs) {
|
|
@@ -444,7 +483,13 @@ export function createDevServer({
|
|
|
444
483
|
// entry's own ancestor node_modules by default, so a widget that vendored
|
|
445
484
|
// its deps locally still resolves.
|
|
446
485
|
const bundleNodePaths = [];
|
|
447
|
-
const _frontendNm = resolve(
|
|
486
|
+
const _frontendNm = resolve(
|
|
487
|
+
watchRoot,
|
|
488
|
+
"..",
|
|
489
|
+
"..",
|
|
490
|
+
"frontend",
|
|
491
|
+
"node_modules",
|
|
492
|
+
);
|
|
448
493
|
if (existsSync(_frontendNm)) bundleNodePaths.push(_frontendNm);
|
|
449
494
|
|
|
450
495
|
const sseClients = new Set();
|
|
@@ -700,7 +745,13 @@ export function createDevServer({
|
|
|
700
745
|
if (url === "/__dev/events") return serveEvents(req, res);
|
|
701
746
|
if (url === "/__dev/health") {
|
|
702
747
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
703
|
-
res.end(
|
|
748
|
+
res.end(
|
|
749
|
+
JSON.stringify({
|
|
750
|
+
ok: true,
|
|
751
|
+
manifestId,
|
|
752
|
+
mode: directoryMode ? "directory" : "single-file",
|
|
753
|
+
}),
|
|
754
|
+
);
|
|
704
755
|
return;
|
|
705
756
|
}
|
|
706
757
|
if (directoryMode && url.startsWith("/file/")) {
|
|
@@ -736,7 +787,8 @@ export function createDevServer({
|
|
|
736
787
|
if (ok) onLint("lint: clean");
|
|
737
788
|
else {
|
|
738
789
|
onLint(`lint: ${findings.length} finding(s)`);
|
|
739
|
-
for (const f of findings)
|
|
790
|
+
for (const f of findings)
|
|
791
|
+
onLint(` [${f.rule}] line ${f.line}: ${f.label}`);
|
|
740
792
|
}
|
|
741
793
|
}
|
|
742
794
|
}
|