@colixsystems/widget-sdk 0.55.0 → 0.57.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 CHANGED
@@ -52,11 +52,19 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
52
52
 
53
53
  ## Status
54
54
 
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**.
55
+ `v0.57.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.57.0
58
+
59
+ **A `required: true` `tableRef` must ship a matching `datastoreTemplate` table (sc-2791).** A widget property that the author cannot skip — a `tableRef` you mark `required: true` — must have a table to bind on install, or the end user has nothing to pick and the widget can't load its data. The marketplace analyzer now enforces this at publish: the check `manifest.requiredTableRefsHaveTemplate` rejects a manifest whose `datastoreTemplate.tables` count is fewer than its `required` `tableRef` property count, naming the offending properties. Two ways to clear it: seed a `datastoreTemplate` table for each required `tableRef` (the default — the widget owns its data), OR make the property **optional** when it binds a table the app already owns (shared data the user names). This tightens the existing "always ship a `datastoreTemplate` for a data widget" guidance into an enforced rule for the `required` case. No export, type, or hook changed shape — additive publish-gate + documentation. `CONTRACT` is unchanged (no new field).
56
60
 
57
61
  ### What's new in 0.55.0
58
62
 
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.
63
+ **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 Data 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.
64
+
65
+ ### What's new in 0.56.0
66
+
67
+ **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).
60
68
 
61
69
  ### What's new in 0.54.0
62
70
 
@@ -123,7 +131,7 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
123
131
 
124
132
  ### What's new in 0.42.0
125
133
 
126
- **RELATION columns hydrate with a display label (sc-1181).** Record reads now return `{ id, label }` for ONE_TO_ONE / ONE_TO_MANY and `[{ id, label }, ...]` for MANY_TO_MANY (empty array when no links) — `label` is the value of the column pointed at by the new optional `display_column_id` on `DatastoreSchemaColumn`, or, when unset, the first STRING/TEXT column on the target table. Widgets should render `record.<rel>.label` (or `record.<rel>.map(r => r.label).join(", ")` for M:M) directly; `.id` is still there for the foreign-key case. The cell-formatting helpers in the built-in `DataList` and `FieldValue` widgets already walk arrays and prefer `label` over `name` / `id` — author widgets that need the same can copy that pattern. `CONTRACT.version` is unchanged.
134
+ **RELATION columns hydrate with a display label (sc-1181).** Record reads now return `{ id, label }` for ONE_TO_ONE / ONE_TO_MANY and `[{ id, label }, ...]` for MANY_TO_MANY (empty array when no links) — `label` is the value of the column pointed at by the new optional `display_column_id` on `DatastoreSchemaColumn`, or, when unset, the first STRING/TEXT column on the target table. Widgets should render `record.<rel>.label` (or `record.<rel>.map(r => r.label).join(", ")` for M:M) directly; `.id` is still there for the foreign-key case. The cell-formatting helpers in the built-in `DataList` and `DataValue` widgets already walk arrays and prefer `label` over `name` / `id` — author widgets that need the same can copy that pattern. `CONTRACT.version` is unchanged.
127
135
 
128
136
  ### What's new in 0.41.0
129
137
 
@@ -250,10 +258,10 @@ Also: `useFileSignatures(fileIds)` is now **self-scoped** (the caller's own sign
250
258
 
251
259
  **New `valueRef` propertySchema type — bind a widget to a single value in the datastore (REQ-WDG-VALUEREF).**
252
260
 
253
- - **`valueRef`** is a composite picker: the Studio Properties Panel renders three cascading dropdowns — pick a **table**, then a **record**, then a **column** — and the bound widget resolves the one cell. It is the discoverable replacement for hand-typing a `tableRef` + a raw record-id `string` + a `columnRef` separately (the old Field Value shape).
261
+ - **`valueRef`** is a composite picker: the Studio Properties Panel renders three cascading dropdowns — pick a **table**, then a **record**, then a **column** — and the bound widget resolves the one cell. It is the discoverable replacement for hand-typing a `tableRef` + a raw record-id `string` + a `columnRef` separately (the old Data Value shape).
254
262
  - **Persisted value is an object** `{ tableId, recordId, column }` (new `ValueRefBinding` type), unlike every other ref type, which is a bare string. A widget reads it with `useDatastoreRecord(value.tableId, value.recordId)` then `record[value.column]`; any missing piece means "no value".
255
263
  - **tenant-copy** remaps `tableId` to the copied table and **nulls `recordId`** (records are business data and are never copied), so a copied workspace shows the widget's fallback until the new operator re-picks a record. `column` (a name) is preserved verbatim.
256
- - The built-in **Field Value** widget now uses `valueRef` (back-compatible: already-saved Field Value widgets that stored `tableId`/`recordId`/`column` as separate props keep rendering).
264
+ - The built-in **Data Value** widget now uses `valueRef` (back-compatible: already-saved Data Value widgets that stored `tableId`/`recordId`/`column` as separate props keep rendering).
257
265
  - **`CONTRACT.version` → `1.12.0`** (additive: one new optional propertySchema type). No existing export or type changed signature.
258
266
 
259
267
  ### What's new in 0.21.1
@@ -330,6 +338,7 @@ The "split-implementation + vetted package list" pivot.
330
338
  - **`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.
331
339
  - **`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.
332
340
  - **`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.
341
+ - **`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.
333
342
  - **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.
334
343
  - **Four Tier A SDK additions:**
335
344
  - `<Icon>` primitive — `<Icon name="check" size={16} color={theme.colors.primary} />`. Wraps `lucide-react-native`; works on both platforms.
package/dist/contract.cjs CHANGED
@@ -1862,7 +1862,7 @@ const CONTRACT = deepFreeze({
1862
1862
  // an optional `mode` field: "static" (a pinned recordId, the default and
1863
1863
  // the only prior behaviour) or "latest" (the most recently created row,
1864
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
1865
+ // Data Value widget reads it to show a live "latest entry". Existing
1866
1866
  // bindings have no `mode` and read as static — additive, minor bump.
1867
1867
  version: "1.39.0",
1868
1868
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
package/dist/contract.js CHANGED
@@ -1862,7 +1862,7 @@ const CONTRACT = deepFreeze({
1862
1862
  // an optional `mode` field: "static" (a pinned recordId, the default and
1863
1863
  // the only prior behaviour) or "latest" (the most recently created row,
1864
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
1865
+ // Data Value widget reads it to show a live "latest entry". Existing
1866
1866
  // bindings have no `mode` and read as static — additive, minor bump.
1867
1867
  version: "1.39.0",
1868
1868
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.55.0",
3
+ "version": "0.57.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"