@colixsystems/widget-sdk 0.104.0 → 0.106.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 +24 -2
- package/dist/contract.cjs +12 -2
- package/dist/contract.js +12 -2
- package/dist/linter.cjs +120 -34
- package/dist/linter.js +122 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,8 +68,30 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
68
68
|
|
|
69
69
|
## Status
|
|
70
70
|
|
|
71
|
-
`v0.
|
|
71
|
+
`v0.106.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**.
|
|
72
72
|
|
|
73
|
+
### What's new in 0.106.0 (contract 1.80.0)
|
|
74
|
+
|
|
75
|
+
**A `required` `tableRef` can now say the table is the APP'S, not the widget's — `sharedTable: true` (sc-5982).** sc-2791's publish gate `manifest.requiredTableRefsHaveTemplate` counted only two kinds of satisfying table: one the widget seeds via `datastoreTemplate`, and one the host hands in when the in-Studio agent delegates a build. A widget that legitimately reads a table it does not own — a chart, a metric, a map, a manager view over another widget's table — could satisfy neither when publishing on its own, so the only way through the gate was to drop `required`. That trades a publish error for a worse runtime one: an unbound widget renders an empty tile and the author gets no warning. (In this repo it also took down every first-party publish for twelve days.)
|
|
76
|
+
|
|
77
|
+
`sharedTable: true` on a `tableRef` property says "required, but the author supplies the table; I seed nothing for it". It exempts the property from the seeding duty only — the property stays `required`, the author must still bind a table, and the host still refuses to place the widget unbound. Pick between the two shapes by asking who owns the data:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
propertySchema: {
|
|
81
|
+
// The widget owns its data → seed it.
|
|
82
|
+
messagesTableId: { type: "tableRef", label: "Messages", required: true },
|
|
83
|
+
// The app owns the data, the author points at it → declare it shared.
|
|
84
|
+
sourceTableId: { type: "tableRef", label: "Source table", required: true, sharedTable: true },
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Leaving a `required` `tableRef` with neither a seeded table nor `sharedTable` is still rejected at publish, and the rejection still names the offending properties. Additive: every manifest that passed before passes now.
|
|
89
|
+
|
|
90
|
+
### What's new in 0.105.0 (contract unchanged)
|
|
91
|
+
|
|
92
|
+
**New linter rule `sdk-export-not-imported` — self-containment now covers the SDK surface, not just `React` (sc-5897).** `react-not-imported` (0.62.0) enforced that a referenced `React` global must be imported, but there was no equivalent rule for the SDK's own exports: source that called `useFilestoreFile(id)` — or rendered `<View>` — without naming it in `import { … } from "@colixsystems/widget-sdk"` linted clean, bundled clean, and threw `ReferenceError: useFilestoreFile is not defined` at render. The two failures are one bug, so they now share one detector, keyed off the names `CONTRACT.hooks` and `CONTRACT.primitives` already enumerate — a hook added to the contract is covered with no second list to maintain.
|
|
93
|
+
|
|
94
|
+
The scan is AST-free, so it flags only the three reference forms text can decide with certainty: a call `NAME(`, a member read `NAME.` (`StyleSheet.create`), and a JSX element `<NAME`. Any other occurrence of the word — an import specifier, a local declaration or destructure, a property key, JSX text, a comment or string — means a binding the scan cannot see may exist, and the name is abandoned rather than flagged. `error` severity with no opt-out, matching `react-not-imported`. Author fix: add the name to your SDK import. `CONTRACT` is unchanged (no new field).
|
|
73
95
|
### What's new in 0.104.0 (contract 1.79.0)
|
|
74
96
|
|
|
75
97
|
**Each corner can be rounded on its own — the `cornerRadius` property type.** A radius was a single number, so every rounded surface was rounded on all four corners: a card that meets the screen edge, a tab rounded only on top, or a bubble with one squared corner had no expression.
|
|
@@ -542,7 +564,7 @@ useEffect(() => {
|
|
|
542
564
|
|
|
543
565
|
### What's new in 0.57.0
|
|
544
566
|
|
|
545
|
-
**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
|
|
567
|
+
**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 mark the property `sharedTable: true` when it binds a table the app already owns (0.106.0 — it stays `required`, so the author must still bind one). Dropping `required` also clears the gate but is the wrong fix: an optional table prop lets the widget ship unbound and render an empty tile. 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).
|
|
546
568
|
|
|
547
569
|
### What's new in 0.55.0
|
|
548
570
|
|
package/dist/contract.cjs
CHANGED
|
@@ -1903,7 +1903,7 @@ const MANIFEST_SCHEMA = {
|
|
|
1903
1903
|
type: "object",
|
|
1904
1904
|
required: false,
|
|
1905
1905
|
description:
|
|
1906
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (must be declared earlier in the array). Tables persist across uninstalls.",
|
|
1906
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (must be declared earlier in the array). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for.",
|
|
1907
1907
|
},
|
|
1908
1908
|
translations: {
|
|
1909
1909
|
type: "object",
|
|
@@ -3301,7 +3301,17 @@ const CONTRACT = deepFreeze({
|
|
|
3301
3301
|
// per-scalar limits never stated, so the worst-case unauthenticated
|
|
3302
3302
|
// payload is SMALLER than before. `__proto__`-style keys are refused at
|
|
3303
3303
|
// every level. Additive: every value accepted before is accepted now.
|
|
3304
|
-
|
|
3304
|
+
// 1.80.0: additive (sc-5982) — a `tableRef` property may declare
|
|
3305
|
+
// `sharedTable: true`: "required, but the author supplies the table; the
|
|
3306
|
+
// widget seeds nothing." The publish gate
|
|
3307
|
+
// `manifest.requiredTableRefsHaveTemplate` counted only seeded
|
|
3308
|
+
// `datastoreTemplate` tables and Mason-supplied bound tables, so a widget
|
|
3309
|
+
// that legitimately reads an app-owned table (Chart, Metric, Map, a
|
|
3310
|
+
// manager view) could never satisfy it when publishing standalone — which
|
|
3311
|
+
// blocked every first-party publish for twelve days. The flag separates
|
|
3312
|
+
// who PROVIDES the table from whether the author may skip it; `required`
|
|
3313
|
+
// still means required.
|
|
3314
|
+
version: "1.80.0",
|
|
3305
3315
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3306
3316
|
hooks: HOOKS,
|
|
3307
3317
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1903,7 +1903,7 @@ const MANIFEST_SCHEMA = {
|
|
|
1903
1903
|
type: "object",
|
|
1904
1904
|
required: false,
|
|
1905
1905
|
description:
|
|
1906
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (must be declared earlier in the array). Tables persist across uninstalls.",
|
|
1906
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (must be declared earlier in the array). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for.",
|
|
1907
1907
|
},
|
|
1908
1908
|
translations: {
|
|
1909
1909
|
type: "object",
|
|
@@ -3301,7 +3301,17 @@ const CONTRACT = deepFreeze({
|
|
|
3301
3301
|
// per-scalar limits never stated, so the worst-case unauthenticated
|
|
3302
3302
|
// payload is SMALLER than before. `__proto__`-style keys are refused at
|
|
3303
3303
|
// every level. Additive: every value accepted before is accepted now.
|
|
3304
|
-
|
|
3304
|
+
// 1.80.0: additive (sc-5982) — a `tableRef` property may declare
|
|
3305
|
+
// `sharedTable: true`: "required, but the author supplies the table; the
|
|
3306
|
+
// widget seeds nothing." The publish gate
|
|
3307
|
+
// `manifest.requiredTableRefsHaveTemplate` counted only seeded
|
|
3308
|
+
// `datastoreTemplate` tables and Mason-supplied bound tables, so a widget
|
|
3309
|
+
// that legitimately reads an app-owned table (Chart, Metric, Map, a
|
|
3310
|
+
// manager view) could never satisfy it when publishing standalone — which
|
|
3311
|
+
// blocked every first-party publish for twelve days. The flag separates
|
|
3312
|
+
// who PROVIDES the table from whether the author may skip it; `required`
|
|
3313
|
+
// still means required.
|
|
3314
|
+
version: "1.80.0",
|
|
3305
3315
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3306
3316
|
hooks: HOOKS,
|
|
3307
3317
|
primitives: PRIMITIVES,
|
package/dist/linter.cjs
CHANGED
|
@@ -714,43 +714,129 @@ function _lucideIconRules(source) {
|
|
|
714
714
|
return findings;
|
|
715
715
|
}
|
|
716
716
|
|
|
717
|
-
// sc-2353 — widget source must be
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
717
|
+
// sc-2353 / sc-5897 — widget source must be SELF-CONTAINED: every identifier it
|
|
718
|
+
// references needs a binding in that file. Two rules share ONE detector here
|
|
719
|
+
// because they are one bug with one cause (CLAUDE.md §3):
|
|
720
|
+
//
|
|
721
|
+
// * `react-not-imported` (sc-2353) — the automatic JSX runtime binds
|
|
722
|
+
// jsx/jsxs from react/jsx-runtime but never `React` itself, so source that
|
|
723
|
+
// reaches for the bare `React` global (React.createElement / React.Fragment
|
|
724
|
+
// / React.memo) renders fine until a non-initial code path hits the
|
|
725
|
+
// reference, then throws "React is not defined". The platform does NOT
|
|
726
|
+
// inject a React binding. Plain JSX needs no React import.
|
|
727
|
+
// * `sdk-export-not-imported` (sc-5897) — an SDK hook or primitive referenced
|
|
728
|
+
// as a bare identifier (`useFilestoreFile(id)`, `<View>`) with no import
|
|
729
|
+
// throws the same ReferenceError. It used to lint clean and surface only at
|
|
730
|
+
// the render.smoke gate, where "<name> is not defined" is softenable — so
|
|
731
|
+
// it was demoted to an advisory warning and burned repair turns instead of
|
|
732
|
+
// being rejected deterministically here.
|
|
733
|
+
//
|
|
734
|
+
// The scan is AST-free, so it flags ONLY the three reference forms text can
|
|
735
|
+
// decide with certainty — a call `NAME(`, a member read `NAME.`, and a JSX
|
|
736
|
+
// element `<NAME`. Any OTHER occurrence of the word (an import specifier, a
|
|
737
|
+
// declaration, a destructured local, a property key, JSX text) means a binding
|
|
738
|
+
// this scan cannot see may exist, so the name is abandoned rather than flagged:
|
|
739
|
+
// missing a real bug is recoverable, a false `error` with no opt-out blocks a
|
|
740
|
+
// legitimate publish.
|
|
741
|
+
const _IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
742
|
+
|
|
743
|
+
// CONTRACT enumerates the SDK's public hooks and primitives, so a name added
|
|
744
|
+
// there is covered here without a second hand-maintained list.
|
|
745
|
+
const _SDK_EXPORT_NAMES = Array.from(
|
|
746
|
+
new Set(
|
|
747
|
+
[...CONTRACT.hooks, ...CONTRACT.primitives]
|
|
748
|
+
.map((entry) => entry && entry.name)
|
|
749
|
+
.filter((name) => typeof name === "string" && _IDENTIFIER_RE.test(name)),
|
|
750
|
+
),
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
// The only words that may legally precede an expression. A DECLARATION keyword
|
|
754
|
+
// (`function`/`const`/`import`/`new`/`as`/…) is absent by design, and so is
|
|
755
|
+
// every other identifier: `Call useTheme() now` is not valid JS, so a word lead
|
|
756
|
+
// outside this set means the match sits in JSX text, not in code.
|
|
757
|
+
const _EXPRESSION_LEAD_WORDS = new Set([
|
|
758
|
+
"return",
|
|
759
|
+
"await",
|
|
760
|
+
"typeof",
|
|
761
|
+
"void",
|
|
762
|
+
"delete",
|
|
763
|
+
"yield",
|
|
764
|
+
"throw",
|
|
765
|
+
"case",
|
|
766
|
+
"in",
|
|
767
|
+
"of",
|
|
768
|
+
"do",
|
|
769
|
+
"else",
|
|
770
|
+
"instanceof",
|
|
771
|
+
]);
|
|
772
|
+
|
|
773
|
+
// 1-based line of the first unbound reference to `name`, or 0 when the name is
|
|
774
|
+
// bound, never referenced, or referenced in a form this scan cannot decide.
|
|
775
|
+
function _unboundReferenceLine(code, name) {
|
|
776
|
+
if (!code.includes(name)) return 0;
|
|
777
|
+
// `NAME(…) {` is a function / method DEFINITION, not a call.
|
|
778
|
+
if (new RegExp(`\\b${name}\\s*\\([^()]*\\)\\s*\\{`).test(code)) return 0;
|
|
779
|
+
const word = new RegExp(`\\b${name}\\b`, "g");
|
|
780
|
+
let firstIndex = -1;
|
|
781
|
+
let match;
|
|
782
|
+
while ((match = word.exec(code))) {
|
|
783
|
+
const before = code.slice(0, match.index).replace(/\s+$/, "");
|
|
784
|
+
// `obj.NAME` / `obj?.NAME` / `{...NAME}` — never this binding.
|
|
785
|
+
if (before.endsWith(".")) continue;
|
|
786
|
+
const lead = before.slice(-1);
|
|
787
|
+
if (/[A-Za-z0-9_$]/.test(lead)) {
|
|
788
|
+
const leadWord = (before.match(/[A-Za-z_$][A-Za-z0-9_$]*$/) || [""])[0];
|
|
789
|
+
if (!_EXPRESSION_LEAD_WORDS.has(leadWord)) return 0;
|
|
790
|
+
} else if (lead === ">" && !before.endsWith("=>")) {
|
|
791
|
+
// A `>` that is not an arrow closes a JSX tag, so what follows is text.
|
|
792
|
+
return 0;
|
|
793
|
+
}
|
|
794
|
+
const after = code.slice(match.index + name.length).replace(/^\s+/, "");
|
|
795
|
+
const isReference =
|
|
796
|
+
/<\s*\/?$/.test(before) || after.startsWith("(") || after.startsWith(".");
|
|
797
|
+
if (!isReference) return 0;
|
|
798
|
+
if (firstIndex < 0) firstIndex = match.index;
|
|
799
|
+
}
|
|
800
|
+
if (firstIndex < 0) return 0;
|
|
801
|
+
return code.slice(0, firstIndex).split(/\r?\n/).length;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function _selfContainedReferenceRules(source) {
|
|
729
805
|
const findings = [];
|
|
730
806
|
const code = _stripNonCode(source);
|
|
731
|
-
if (!_REACT_MEMBER_USE_RE.test(code)) return findings;
|
|
732
|
-
if (_REACT_DEFAULT_IMPORT_RE.test(code)) return findings;
|
|
733
|
-
const codeLines = code.split(/\r?\n/);
|
|
734
807
|
const sourceLines = source.split(/\r?\n/);
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
808
|
+
const snippetAt = (line) => (sourceLines[line - 1] || "").trim().slice(0, 200);
|
|
809
|
+
|
|
810
|
+
const reactLine = _unboundReferenceLine(code, "React");
|
|
811
|
+
if (reactLine) {
|
|
812
|
+
findings.push({
|
|
813
|
+
rule: "react-not-imported",
|
|
814
|
+
severity: "error",
|
|
815
|
+
label:
|
|
816
|
+
"source references the `React` global (e.g. React.createElement / " +
|
|
817
|
+
"React.Fragment) but never imports it — widget source must be " +
|
|
818
|
+
'self-contained. Add `import React from "react";` at the top, or drop ' +
|
|
819
|
+
"the bare `React` reference in favour of a JSX fragment `<>…</>` and the " +
|
|
820
|
+
"SDK hooks (useState / useMemo / …) and primitives (View / Text / …).",
|
|
821
|
+
line: reactLine,
|
|
822
|
+
snippet: snippetAt(reactLine),
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
for (const name of _SDK_EXPORT_NAMES) {
|
|
827
|
+
const line = _unboundReferenceLine(code, name);
|
|
828
|
+
if (!line) continue;
|
|
829
|
+
findings.push({
|
|
830
|
+
rule: "sdk-export-not-imported",
|
|
831
|
+
severity: "error",
|
|
832
|
+
label:
|
|
833
|
+
`\`${name}\` is used but never imported — it throws ` +
|
|
834
|
+
`"${name} is not defined" at render. Import it from ` +
|
|
835
|
+
'"@colixsystems/widget-sdk"; widget source must be self-contained.',
|
|
836
|
+
line,
|
|
837
|
+
snippet: snippetAt(line),
|
|
838
|
+
});
|
|
741
839
|
}
|
|
742
|
-
findings.push({
|
|
743
|
-
rule: "react-not-imported",
|
|
744
|
-
severity: "error",
|
|
745
|
-
label:
|
|
746
|
-
"source references the `React` global (e.g. React.createElement / " +
|
|
747
|
-
"React.Fragment) but never imports it — widget source must be " +
|
|
748
|
-
'self-contained. Add `import React from "react";` at the top, or drop ' +
|
|
749
|
-
"the bare `React` reference in favour of a JSX fragment `<>…</>` and the " +
|
|
750
|
-
"SDK hooks (useState / useMemo / …) and primitives (View / Text / …).",
|
|
751
|
-
line,
|
|
752
|
-
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
753
|
-
});
|
|
754
840
|
return findings;
|
|
755
841
|
}
|
|
756
842
|
|
|
@@ -1195,7 +1281,7 @@ function lintSource(source, options) {
|
|
|
1195
1281
|
findings.push(..._translationApiRules(source));
|
|
1196
1282
|
findings.push(..._handBuiltPageUrlRules(source));
|
|
1197
1283
|
findings.push(..._lucideIconRules(source));
|
|
1198
|
-
findings.push(...
|
|
1284
|
+
findings.push(..._selfContainedReferenceRules(source));
|
|
1199
1285
|
findings.push(..._imagePercentHeightRules(source));
|
|
1200
1286
|
// sc-4913 — soft warning: a measured width that includes the widget's own
|
|
1201
1287
|
// padding wraps the last grid column into an empty one.
|
package/dist/linter.js
CHANGED
|
@@ -835,43 +835,129 @@ function _lucideIconRules(source) {
|
|
|
835
835
|
return findings;
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
-
// sc-2353 — widget source must be
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
838
|
+
// sc-2353 / sc-5897 — widget source must be SELF-CONTAINED: every identifier it
|
|
839
|
+
// references needs a binding in that file. Two rules share ONE detector here
|
|
840
|
+
// because they are one bug with one cause (CLAUDE.md §3):
|
|
841
|
+
//
|
|
842
|
+
// * `react-not-imported` (sc-2353) — the automatic JSX runtime binds
|
|
843
|
+
// jsx/jsxs from react/jsx-runtime but never `React` itself, so source that
|
|
844
|
+
// reaches for the bare `React` global (React.createElement / React.Fragment
|
|
845
|
+
// / React.memo) renders fine until a non-initial code path hits the
|
|
846
|
+
// reference, then throws "React is not defined". The platform does NOT
|
|
847
|
+
// inject a React binding. Plain JSX needs no React import.
|
|
848
|
+
// * `sdk-export-not-imported` (sc-5897) — an SDK hook or primitive referenced
|
|
849
|
+
// as a bare identifier (`useFilestoreFile(id)`, `<View>`) with no import
|
|
850
|
+
// throws the same ReferenceError. It used to lint clean and surface only at
|
|
851
|
+
// the render.smoke gate, where "<name> is not defined" is softenable — so
|
|
852
|
+
// it was demoted to an advisory warning and burned repair turns instead of
|
|
853
|
+
// being rejected deterministically here.
|
|
854
|
+
//
|
|
855
|
+
// The scan is AST-free, so it flags ONLY the three reference forms text can
|
|
856
|
+
// decide with certainty — a call `NAME(`, a member read `NAME.`, and a JSX
|
|
857
|
+
// element `<NAME`. Any OTHER occurrence of the word (an import specifier, a
|
|
858
|
+
// declaration, a destructured local, a property key, JSX text) means a binding
|
|
859
|
+
// this scan cannot see may exist, so the name is abandoned rather than flagged:
|
|
860
|
+
// missing a real bug is recoverable, a false `error` with no opt-out blocks a
|
|
861
|
+
// legitimate publish.
|
|
862
|
+
const _IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
863
|
+
|
|
864
|
+
// CONTRACT enumerates the SDK's public hooks and primitives, so a name added
|
|
865
|
+
// there is covered here without a second hand-maintained list.
|
|
866
|
+
const _SDK_EXPORT_NAMES = Array.from(
|
|
867
|
+
new Set(
|
|
868
|
+
[...CONTRACT.hooks, ...CONTRACT.primitives]
|
|
869
|
+
.map((entry) => entry && entry.name)
|
|
870
|
+
.filter((name) => typeof name === "string" && _IDENTIFIER_RE.test(name)),
|
|
871
|
+
),
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
// The only words that may legally precede an expression. A DECLARATION keyword
|
|
875
|
+
// (`function`/`const`/`import`/`new`/`as`/…) is absent by design, and so is
|
|
876
|
+
// every other identifier: `Call useTheme() now` is not valid JS, so a word lead
|
|
877
|
+
// outside this set means the match sits in JSX text, not in code.
|
|
878
|
+
const _EXPRESSION_LEAD_WORDS = new Set([
|
|
879
|
+
"return",
|
|
880
|
+
"await",
|
|
881
|
+
"typeof",
|
|
882
|
+
"void",
|
|
883
|
+
"delete",
|
|
884
|
+
"yield",
|
|
885
|
+
"throw",
|
|
886
|
+
"case",
|
|
887
|
+
"in",
|
|
888
|
+
"of",
|
|
889
|
+
"do",
|
|
890
|
+
"else",
|
|
891
|
+
"instanceof",
|
|
892
|
+
]);
|
|
893
|
+
|
|
894
|
+
// 1-based line of the first unbound reference to `name`, or 0 when the name is
|
|
895
|
+
// bound, never referenced, or referenced in a form this scan cannot decide.
|
|
896
|
+
function _unboundReferenceLine(code, name) {
|
|
897
|
+
if (!code.includes(name)) return 0;
|
|
898
|
+
// `NAME(…) {` is a function / method DEFINITION, not a call.
|
|
899
|
+
if (new RegExp(`\\b${name}\\s*\\([^()]*\\)\\s*\\{`).test(code)) return 0;
|
|
900
|
+
const word = new RegExp(`\\b${name}\\b`, "g");
|
|
901
|
+
let firstIndex = -1;
|
|
902
|
+
let match;
|
|
903
|
+
while ((match = word.exec(code))) {
|
|
904
|
+
const before = code.slice(0, match.index).replace(/\s+$/, "");
|
|
905
|
+
// `obj.NAME` / `obj?.NAME` / `{...NAME}` — never this binding.
|
|
906
|
+
if (before.endsWith(".")) continue;
|
|
907
|
+
const lead = before.slice(-1);
|
|
908
|
+
if (/[A-Za-z0-9_$]/.test(lead)) {
|
|
909
|
+
const leadWord = (before.match(/[A-Za-z_$][A-Za-z0-9_$]*$/) || [""])[0];
|
|
910
|
+
if (!_EXPRESSION_LEAD_WORDS.has(leadWord)) return 0;
|
|
911
|
+
} else if (lead === ">" && !before.endsWith("=>")) {
|
|
912
|
+
// A `>` that is not an arrow closes a JSX tag, so what follows is text.
|
|
913
|
+
return 0;
|
|
914
|
+
}
|
|
915
|
+
const after = code.slice(match.index + name.length).replace(/^\s+/, "");
|
|
916
|
+
const isReference =
|
|
917
|
+
/<\s*\/?$/.test(before) || after.startsWith("(") || after.startsWith(".");
|
|
918
|
+
if (!isReference) return 0;
|
|
919
|
+
if (firstIndex < 0) firstIndex = match.index;
|
|
920
|
+
}
|
|
921
|
+
if (firstIndex < 0) return 0;
|
|
922
|
+
return code.slice(0, firstIndex).split(/\r?\n/).length;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function _selfContainedReferenceRules(source) {
|
|
850
926
|
const findings = [];
|
|
851
927
|
const code = _stripNonCode(source);
|
|
852
|
-
if (!_REACT_MEMBER_USE_RE.test(code)) return findings;
|
|
853
|
-
if (_REACT_DEFAULT_IMPORT_RE.test(code)) return findings;
|
|
854
|
-
const codeLines = code.split(/\r?\n/);
|
|
855
928
|
const sourceLines = source.split(/\r?\n/);
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
929
|
+
const snippetAt = (line) => (sourceLines[line - 1] || "").trim().slice(0, 200);
|
|
930
|
+
|
|
931
|
+
const reactLine = _unboundReferenceLine(code, "React");
|
|
932
|
+
if (reactLine) {
|
|
933
|
+
findings.push({
|
|
934
|
+
rule: "react-not-imported",
|
|
935
|
+
severity: "error",
|
|
936
|
+
label:
|
|
937
|
+
"source references the `React` global (e.g. React.createElement / " +
|
|
938
|
+
"React.Fragment) but never imports it — widget source must be " +
|
|
939
|
+
'self-contained. Add `import React from "react";` at the top, or drop ' +
|
|
940
|
+
"the bare `React` reference in favour of a JSX fragment `<>…</>` and the " +
|
|
941
|
+
"SDK hooks (useState / useMemo / …) and primitives (View / Text / …).",
|
|
942
|
+
line: reactLine,
|
|
943
|
+
snippet: snippetAt(reactLine),
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
for (const name of _SDK_EXPORT_NAMES) {
|
|
948
|
+
const line = _unboundReferenceLine(code, name);
|
|
949
|
+
if (!line) continue;
|
|
950
|
+
findings.push({
|
|
951
|
+
rule: "sdk-export-not-imported",
|
|
952
|
+
severity: "error",
|
|
953
|
+
label:
|
|
954
|
+
`\`${name}\` is used but never imported — it throws ` +
|
|
955
|
+
`"${name} is not defined" at render. Import it from ` +
|
|
956
|
+
'"@colixsystems/widget-sdk"; widget source must be self-contained.',
|
|
957
|
+
line,
|
|
958
|
+
snippet: snippetAt(line),
|
|
959
|
+
});
|
|
862
960
|
}
|
|
863
|
-
findings.push({
|
|
864
|
-
rule: "react-not-imported",
|
|
865
|
-
severity: "error",
|
|
866
|
-
label:
|
|
867
|
-
"source references the `React` global (e.g. React.createElement / " +
|
|
868
|
-
"React.Fragment) but never imports it — widget source must be " +
|
|
869
|
-
'self-contained. Add `import React from "react";` at the top, or drop ' +
|
|
870
|
-
"the bare `React` reference in favour of a JSX fragment `<>…</>` and the " +
|
|
871
|
-
"SDK hooks (useState / useMemo / …) and primitives (View / Text / …).",
|
|
872
|
-
line,
|
|
873
|
-
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
874
|
-
});
|
|
875
961
|
return findings;
|
|
876
962
|
}
|
|
877
963
|
|
|
@@ -1372,8 +1458,9 @@ export function lintSource(source, options) {
|
|
|
1372
1458
|
findings.push(..._translationApiRules(source));
|
|
1373
1459
|
findings.push(..._handBuiltPageUrlRules(source));
|
|
1374
1460
|
findings.push(..._lucideIconRules(source));
|
|
1375
|
-
// sc-2353 — widget source must be self-contained
|
|
1376
|
-
|
|
1461
|
+
// sc-2353 / sc-5897 — widget source must be self-contained: a referenced
|
|
1462
|
+
// `React` global or SDK export with no import throws at render.
|
|
1463
|
+
findings.push(..._selfContainedReferenceRules(source));
|
|
1377
1464
|
// sc-3493 — soft warning: percentage height on an <Image> collapses to 0.
|
|
1378
1465
|
findings.push(..._imagePercentHeightRules(source));
|
|
1379
1466
|
// sc-4913 — soft warning: a measured width that includes the widget's own
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.106.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",
|