@colixsystems/widget-sdk 0.54.0 → 0.56.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 +10 -1
- package/dist/contract.cjs +7 -1
- package/dist/contract.js +7 -1
- package/dist/index.d.ts +6 -0
- package/dist/linter.cjs +41 -0
- package/dist/linter.js +42 -0
- package/dist/property-schema.js +13 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -52,7 +52,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
52
52
|
|
|
53
53
|
## Status
|
|
54
54
|
|
|
55
|
-
`v0.
|
|
55
|
+
`v0.55.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**.
|
|
56
|
+
|
|
57
|
+
### What's new in 0.55.0
|
|
58
|
+
|
|
59
|
+
**Dynamic record selection for `valueRef` (sc-2327).** The `valueRef` binding gains an optional `mode` field: `"static"` (the default — pin a specific `recordId`, the only prior behaviour) or `"latest"` (resolve the most recently created row live, sorting on the host-managed `created_at` descending with `limit: 1`; `recordId` is ignored). The built-in Field Value widget reads it to offer a "Latest entry" that updates as records are added, with no per-host code — the same baked widget source and the same injected `@colixsystems/datastore-client` run on the web Player and the native Expo export. The `ValueRefBinding` type adds `mode?: "static" | "latest"`. Existing bindings carry no `mode` and read as static. `CONTRACT.version` → `1.39.0`. Additive — no existing field changed shape.
|
|
60
|
+
|
|
61
|
+
### What's new in 0.56.0
|
|
62
|
+
|
|
63
|
+
**New linter rule `react-not-imported` — widget source must be self-contained (sc-2353).** The automatic JSX runtime binds `jsx`/`jsxs` from `react/jsx-runtime` but never `React` itself, so source that reaches for the bare `React` global (`React.createElement` / `React.Fragment` / `React.memo` / `React.useMemo`) without importing it bundles cleanly, then throws `ReferenceError: React is not defined` the moment a non-initial code path hits the reference. The platform does **not** inject a `React` binding — earlier behaviour that auto-injected one left source that broke as soon as it was downloaded and re-uploaded through a path that doesn't inject. The linter now flags a bare-`React` reference with no `import React from "react"` (or `import * as React`) as an error, so the failure becomes a publish/upload finding (and an AI-agent repair-loop finding) instead of a broken shipped widget. Plain JSX, which needs no React import, is never flagged; a `React` mention inside a comment or string is masked. Author fix: add `import React from "react";` (`react` is already vetted), or prefer a JSX fragment `<>…</>` plus the SDK hooks/primitives over reaching for `React` directly. `CONTRACT` is unchanged (no new field).
|
|
56
64
|
|
|
57
65
|
### What's new in 0.54.0
|
|
58
66
|
|
|
@@ -326,6 +334,7 @@ The "split-implementation + vetted package list" pivot.
|
|
|
326
334
|
- **`fetch` and `XMLHttpRequest` come off `CONTRACT.bannedApis`.** Widgets may call third-party APIs directly. Calls to the host's own `/api/*` surface will 401 because the JWT token is never shared with widget code; the linter emits a soft `no-host-api-url` warning when it sees host-URL substrings so authors learn the rule statically. Use SDK hooks (`useDatastoreQuery`, `useUsers`, `useAsset`, …) for workspace data; use `axios` / `fetch` for third-party APIs.
|
|
327
335
|
- **`import-not-vetted` linter rule (new).** Every bare `import` specifier is validated against `CONTRACT.vettedImports`. Relative imports inside the bundle (`./shared.js`) are allowed so split-impl widgets can share helpers; `../` and absolute paths are rejected.
|
|
328
336
|
- **`import-platform-mismatch` linter rule (new).** A single-source widget that imports a native-only package while `manifest.supportedPlatforms` includes `"web"` fails the lint. The author either drops the platform from the manifest OR ships a `widget.web.jsx` + `widget.native.jsx` pair where the platform-specific import lives in the file that targets its platform.
|
|
337
|
+
- **`react-not-imported` linter rule (new).** Widget source must be self-contained: a reference to the bare `React` global (`React.createElement` / `React.Fragment` / `React.memo` / …) with no `import React from "react"` (or `import * as React`) fails the lint — it would throw `ReferenceError: React is not defined` at render. Plain JSX needs no React import and is never flagged. Add the import, or prefer a JSX fragment `<>…</>` plus the SDK hooks/primitives.
|
|
329
338
|
- **Lint findings carry `severity`.** `"error"` (default) blocks publish; `"warning"` (currently only `no-host-api-url`) surfaces to reviewers without blocking. The `lintSource(...)` return shape stays `{ ok, findings }` — `ok` is true iff no error-severity findings exist.
|
|
330
339
|
- **Four Tier A SDK additions:**
|
|
331
340
|
- `<Icon>` primitive — `<Icon name="check" size={16} color={theme.colors.primary} />`. Wraps `lucide-react-native`; works on both platforms.
|
package/dist/contract.cjs
CHANGED
|
@@ -1858,7 +1858,13 @@ const CONTRACT = deepFreeze({
|
|
|
1858
1858
|
// Player and the native export, so no browser-only PDF library is added
|
|
1859
1859
|
// to the vetted set. No existing hook, primitive, manifest field, or
|
|
1860
1860
|
// token changed shape — minor bump.
|
|
1861
|
-
|
|
1861
|
+
// 1.39.0: additive (sc-2327) — the `valueRef` propertySchema binding gains
|
|
1862
|
+
// an optional `mode` field: "static" (a pinned recordId, the default and
|
|
1863
|
+
// the only prior behaviour) or "latest" (the most recently created row,
|
|
1864
|
+
// resolved live by `created_at` desc with limit 1; recordId ignored). The
|
|
1865
|
+
// Field Value widget reads it to show a live "latest entry". Existing
|
|
1866
|
+
// bindings have no `mode` and read as static — additive, minor bump.
|
|
1867
|
+
version: "1.39.0",
|
|
1862
1868
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1863
1869
|
hooks: HOOKS,
|
|
1864
1870
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1858,7 +1858,13 @@ const CONTRACT = deepFreeze({
|
|
|
1858
1858
|
// Player and the native export, so no browser-only PDF library is added
|
|
1859
1859
|
// to the vetted set. No existing hook, primitive, manifest field, or
|
|
1860
1860
|
// token changed shape — minor bump.
|
|
1861
|
-
|
|
1861
|
+
// 1.39.0: additive (sc-2327) — the `valueRef` propertySchema binding gains
|
|
1862
|
+
// an optional `mode` field: "static" (a pinned recordId, the default and
|
|
1863
|
+
// the only prior behaviour) or "latest" (the most recently created row,
|
|
1864
|
+
// resolved live by `created_at` desc with limit 1; recordId ignored). The
|
|
1865
|
+
// Field Value widget reads it to show a live "latest entry". Existing
|
|
1866
|
+
// bindings have no `mode` and read as static — additive, minor bump.
|
|
1867
|
+
version: "1.39.0",
|
|
1862
1868
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1863
1869
|
hooks: HOOKS,
|
|
1864
1870
|
primitives: PRIMITIVES,
|
package/dist/index.d.ts
CHANGED
|
@@ -101,6 +101,12 @@ export interface ValueRefBinding {
|
|
|
101
101
|
tableId?: string;
|
|
102
102
|
recordId?: string;
|
|
103
103
|
column?: string;
|
|
104
|
+
/**
|
|
105
|
+
* How the record is chosen. "static" (default, may be omitted) pins
|
|
106
|
+
* `recordId`; "latest" resolves the most recently created row live and
|
|
107
|
+
* ignores `recordId`.
|
|
108
|
+
*/
|
|
109
|
+
mode?: "static" | "latest";
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
export interface WidgetEventDescriptor {
|
package/dist/linter.cjs
CHANGED
|
@@ -594,6 +594,46 @@ function _lucideIconRules(source) {
|
|
|
594
594
|
return findings;
|
|
595
595
|
}
|
|
596
596
|
|
|
597
|
+
// sc-2353 — widget source must be self-contained. The automatic JSX runtime
|
|
598
|
+
// binds jsx/jsxs from react/jsx-runtime but never `React` itself, so a widget
|
|
599
|
+
// that reaches for the bare `React` global (React.createElement / React.Fragment
|
|
600
|
+
// / React.memo / React.useMemo) without importing it renders fine until a
|
|
601
|
+
// non-initial code path hits the reference, then throws "React is not defined".
|
|
602
|
+
// The platform does NOT inject a React binding — require an explicit
|
|
603
|
+
// `import React from "react"` (or `import * as React`) whenever the source
|
|
604
|
+
// references React, so the bundle is self-contained on both hosts and survives
|
|
605
|
+
// a download → re-upload round-trip. Plain JSX needs no React import.
|
|
606
|
+
const _REACT_DEFAULT_IMPORT_RE = /\bimport\s+(?:React\b|\*\s+as\s+React\b)/;
|
|
607
|
+
const _REACT_MEMBER_USE_RE = /\bReact\s*\./;
|
|
608
|
+
function _reactInScopeRules(source) {
|
|
609
|
+
const findings = [];
|
|
610
|
+
const code = _stripNonCode(source);
|
|
611
|
+
if (!_REACT_MEMBER_USE_RE.test(code)) return findings;
|
|
612
|
+
if (_REACT_DEFAULT_IMPORT_RE.test(code)) return findings;
|
|
613
|
+
const codeLines = code.split(/\r?\n/);
|
|
614
|
+
const sourceLines = source.split(/\r?\n/);
|
|
615
|
+
let line = 0;
|
|
616
|
+
for (let i = 0; i < codeLines.length; i += 1) {
|
|
617
|
+
if (_REACT_MEMBER_USE_RE.test(codeLines[i])) {
|
|
618
|
+
line = i + 1;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
findings.push({
|
|
623
|
+
rule: "react-not-imported",
|
|
624
|
+
severity: "error",
|
|
625
|
+
label:
|
|
626
|
+
"source references the `React` global (e.g. React.createElement / " +
|
|
627
|
+
"React.Fragment) but never imports it — widget source must be " +
|
|
628
|
+
'self-contained. Add `import React from "react";` at the top, or drop ' +
|
|
629
|
+
"the bare `React` reference in favour of a JSX fragment `<>…</>` and the " +
|
|
630
|
+
"SDK hooks (useState / useMemo / …) and primitives (View / Text / …).",
|
|
631
|
+
line,
|
|
632
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
633
|
+
});
|
|
634
|
+
return findings;
|
|
635
|
+
}
|
|
636
|
+
|
|
597
637
|
// Narrow a split-impl widget's manifest to the platform a single bundle file
|
|
598
638
|
// ships to, so `import-platform-mismatch` lints each file against what it
|
|
599
639
|
// actually targets. Mirror of linter.js.
|
|
@@ -653,6 +693,7 @@ function lintSource(source, options) {
|
|
|
653
693
|
);
|
|
654
694
|
findings.push(..._hostApiUrlRules(source));
|
|
655
695
|
findings.push(..._lucideIconRules(source));
|
|
696
|
+
findings.push(..._reactInScopeRules(source));
|
|
656
697
|
findings.push(
|
|
657
698
|
..._scopeRules(source, options && options.manifest).map((f) => ({
|
|
658
699
|
...f,
|
package/dist/linter.js
CHANGED
|
@@ -683,6 +683,46 @@ function _lucideIconRules(source) {
|
|
|
683
683
|
return findings;
|
|
684
684
|
}
|
|
685
685
|
|
|
686
|
+
// sc-2353 — widget source must be self-contained. The automatic JSX runtime
|
|
687
|
+
// binds jsx/jsxs from react/jsx-runtime but never `React` itself, so a widget
|
|
688
|
+
// that reaches for the bare `React` global (React.createElement / React.Fragment
|
|
689
|
+
// / React.memo / React.useMemo) without importing it renders fine until a
|
|
690
|
+
// non-initial code path hits the reference, then throws "React is not defined".
|
|
691
|
+
// The platform does NOT inject a React binding — require an explicit
|
|
692
|
+
// `import React from "react"` (or `import * as React`) whenever the source
|
|
693
|
+
// references React, so the bundle is self-contained on both hosts and survives
|
|
694
|
+
// a download → re-upload round-trip. Plain JSX needs no React import.
|
|
695
|
+
const _REACT_DEFAULT_IMPORT_RE = /\bimport\s+(?:React\b|\*\s+as\s+React\b)/;
|
|
696
|
+
const _REACT_MEMBER_USE_RE = /\bReact\s*\./;
|
|
697
|
+
function _reactInScopeRules(source) {
|
|
698
|
+
const findings = [];
|
|
699
|
+
const code = _stripNonCode(source);
|
|
700
|
+
if (!_REACT_MEMBER_USE_RE.test(code)) return findings;
|
|
701
|
+
if (_REACT_DEFAULT_IMPORT_RE.test(code)) return findings;
|
|
702
|
+
const codeLines = code.split(/\r?\n/);
|
|
703
|
+
const sourceLines = source.split(/\r?\n/);
|
|
704
|
+
let line = 0;
|
|
705
|
+
for (let i = 0; i < codeLines.length; i += 1) {
|
|
706
|
+
if (_REACT_MEMBER_USE_RE.test(codeLines[i])) {
|
|
707
|
+
line = i + 1;
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
findings.push({
|
|
712
|
+
rule: "react-not-imported",
|
|
713
|
+
severity: "error",
|
|
714
|
+
label:
|
|
715
|
+
"source references the `React` global (e.g. React.createElement / " +
|
|
716
|
+
"React.Fragment) but never imports it — widget source must be " +
|
|
717
|
+
'self-contained. Add `import React from "react";` at the top, or drop ' +
|
|
718
|
+
"the bare `React` reference in favour of a JSX fragment `<>…</>` and the " +
|
|
719
|
+
"SDK hooks (useState / useMemo / …) and primitives (View / Text / …).",
|
|
720
|
+
line,
|
|
721
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
722
|
+
});
|
|
723
|
+
return findings;
|
|
724
|
+
}
|
|
725
|
+
|
|
686
726
|
/**
|
|
687
727
|
* Narrow a split-impl widget's manifest to the platform a single bundle file
|
|
688
728
|
* ships to, so `import-platform-mismatch` lints each file against what it
|
|
@@ -748,6 +788,8 @@ export function lintSource(source, options) {
|
|
|
748
788
|
// REQ-WSDK-PLATFORM §3.5: soft host-API URL warning (does not block).
|
|
749
789
|
findings.push(..._hostApiUrlRules(source));
|
|
750
790
|
findings.push(..._lucideIconRules(source));
|
|
791
|
+
// sc-2353 — widget source must be self-contained (reference React ⇒ import it).
|
|
792
|
+
findings.push(..._reactInScopeRules(source));
|
|
751
793
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — scope-aware rules. Run after the
|
|
752
794
|
// line-by-line scan so banned-identifier findings stay first in the
|
|
753
795
|
// output.
|
package/dist/property-schema.js
CHANGED
|
@@ -183,10 +183,12 @@ function coerceLeaf(def, value, path, errors) {
|
|
|
183
183
|
}
|
|
184
184
|
return value.map((item, i) => coerceLeaf(def.items, item, `${path}[${i}]`, errors));
|
|
185
185
|
case "valueRef": {
|
|
186
|
-
// REQ-WDG-VALUEREF: a `{ tableId, recordId, column }` binding.
|
|
187
|
-
// sub-field is
|
|
186
|
+
// REQ-WDG-VALUEREF: a `{ tableId, recordId, column, mode }` binding.
|
|
187
|
+
// Each string sub-field is optional (a half-configured binding is valid
|
|
188
188
|
// while the author is still picking); the bound widget treats any
|
|
189
|
-
// missing piece as "no value" and shows its fallback.
|
|
189
|
+
// missing piece as "no value" and shows its fallback. sc-2327: `mode`
|
|
190
|
+
// selects how the record is chosen — "static" (a pinned recordId, the
|
|
191
|
+
// default) or "latest" (the newest row, resolved live; recordId ignored).
|
|
190
192
|
if (!isPlainObject(value)) {
|
|
191
193
|
errors.push(`${path}: expected object`);
|
|
192
194
|
return value;
|
|
@@ -197,6 +199,14 @@ function coerceLeaf(def, value, path, errors) {
|
|
|
197
199
|
errors.push(`${path}.${sub}: expected string`);
|
|
198
200
|
}
|
|
199
201
|
}
|
|
202
|
+
if (
|
|
203
|
+
value.mode !== undefined &&
|
|
204
|
+
value.mode !== null &&
|
|
205
|
+
value.mode !== "static" &&
|
|
206
|
+
value.mode !== "latest"
|
|
207
|
+
) {
|
|
208
|
+
errors.push(`${path}.mode: expected "static" or "latest"`);
|
|
209
|
+
}
|
|
200
210
|
return value;
|
|
201
211
|
}
|
|
202
212
|
case "object": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
],
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "node scripts/build.js",
|
|
45
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js"
|
|
45
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=18"
|