@colixsystems/widget-sdk 0.65.0 → 0.66.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/cli.js +14 -4
- package/dist/linter.cjs +59 -0
- package/dist/linter.js +78 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -53,7 +53,16 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
53
53
|
|
|
54
54
|
## Status
|
|
55
55
|
|
|
56
|
-
`v0.
|
|
56
|
+
`v0.66.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**.
|
|
57
|
+
|
|
58
|
+
### What's new in 0.66.0
|
|
59
|
+
|
|
60
|
+
**New linter rule `image-percent-height`, and `appstudio-widget lint` finally prints warnings (sc-3493).**
|
|
61
|
+
|
|
62
|
+
- **`image-percent-height` (severity `warning`, non-blocking).** An `<Image>` / `<ImageBackground>` sized with a literal percentage `height` — `style={{ width: "100%", height: "47%" }}` — is flagged. React Native / Yoga resolves a percentage height against the **parent's** height, so under a content-sized parent it collapses to 0: the `uri` still fetches, but the image is invisible on both the web Player and the native Expo export, with nothing in the console to trace. Author fix: size it with `aspectRatio` (`{ width: "100%", aspectRatio: 1 }`) or a numeric pixel height. It is a **warning**, not an error, precisely because `height: "100%"` *is* correct inside a parent with a definite height (a fixed-height hero) and a text scan cannot tell the two apart — so the rule informs without rejecting a valid widget. Scope is the literal inline form only; a height threaded through a variable or a `StyleSheet` object is beyond an AST-free scan, and the guidance in the `useFilestoreFile` note below remains the primary guard. Comments are not scanned, so documenting the anti-pattern is safe.
|
|
63
|
+
- **The CLI no longer swallows warnings.** `runLint` reported `clean` and dropped every `severity: "warning"` finding whenever there were no errors, which made the existing `no-host-api-url` warning (and this new one) invisible to anyone using `appstudio-widget lint`. It now prints an `N error(s), M warning(s)` header and one line per finding tagged `error` / `warning`. **Exit codes are unchanged:** `0` when there are no error-severity findings (warnings included), `1` otherwise — so a warning still never blocks a build. `clean` is printed only when there are genuinely zero findings.
|
|
64
|
+
|
|
65
|
+
`CONTRACT` is unchanged (no new field), and no export changed signature.
|
|
57
66
|
|
|
58
67
|
### What's new in 0.65.0
|
|
59
68
|
|
package/dist/cli.js
CHANGED
|
@@ -60,15 +60,25 @@ function runLint(rest) {
|
|
|
60
60
|
exit(1);
|
|
61
61
|
}
|
|
62
62
|
const { ok, findings } = lintSource(source);
|
|
63
|
-
if (
|
|
63
|
+
if (findings.length === 0) {
|
|
64
64
|
stdout.write(`${filePath}: clean\n`);
|
|
65
65
|
exit(0);
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
// sc-3493 — a warning-severity finding used to be swallowed: `ok` stays true
|
|
68
|
+
// for warnings, so the CLI printed "clean" and dropped them. A warning nobody
|
|
69
|
+
// sees is pointless. Report every finding; only errors change the exit code.
|
|
70
|
+
const errors = findings.filter((f) => f.severity !== "warning").length;
|
|
71
|
+
const stream = ok ? stdout : stderr;
|
|
72
|
+
stream.write(
|
|
73
|
+
`${filePath}: ${errors} error(s), ${findings.length - errors} warning(s)\n`,
|
|
74
|
+
);
|
|
68
75
|
for (const f of findings) {
|
|
69
|
-
|
|
76
|
+
const severity = f.severity === "warning" ? "warning" : "error";
|
|
77
|
+
stream.write(
|
|
78
|
+
` ${severity} [${f.rule}] line ${f.line}: ${f.label}\n ${f.snippet}\n`,
|
|
79
|
+
);
|
|
70
80
|
}
|
|
71
|
-
exit(1);
|
|
81
|
+
exit(ok ? 0 : 1);
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
async function runDev(rest) {
|
package/dist/linter.cjs
CHANGED
|
@@ -669,6 +669,64 @@ function _reactInScopeRules(source) {
|
|
|
669
669
|
return findings;
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
+
// sc-3466 / sc-3493 — percentage height on an <Image> collapses to 0 against a
|
|
673
|
+
// content-sized parent, so the image loads but renders invisible on both hosts.
|
|
674
|
+
// `severity: "warning"`: the same value is correct under a definite-height
|
|
675
|
+
// parent, which this AST-free scan cannot see. Mirror of linter.js.
|
|
676
|
+
const _IMAGE_TAG_RE = /<(Image|ImageBackground)\b/g;
|
|
677
|
+
const _PERCENT_HEIGHT_RE =
|
|
678
|
+
/(^|[{,;\s])height\s*:\s*(["'])\s*\d+(?:\.\d+)?\s*%\s*\2/g;
|
|
679
|
+
|
|
680
|
+
function _jsxOpenTagEnd(source, from) {
|
|
681
|
+
let depth = 0;
|
|
682
|
+
let quote = "";
|
|
683
|
+
for (let i = from; i < source.length; i += 1) {
|
|
684
|
+
const ch = source[i];
|
|
685
|
+
if (quote) {
|
|
686
|
+
if (ch === "\\") i += 1;
|
|
687
|
+
else if (ch === quote) quote = "";
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
691
|
+
else if (ch === "{") depth += 1;
|
|
692
|
+
else if (ch === "}") depth -= 1;
|
|
693
|
+
else if (ch === ">" && depth <= 0) return i;
|
|
694
|
+
}
|
|
695
|
+
return source.length;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function _imagePercentHeightRules(source) {
|
|
699
|
+
const findings = [];
|
|
700
|
+
const code = _stripNonCode(source, { keepStrings: true });
|
|
701
|
+
const sourceLines = source.split(/\r?\n/);
|
|
702
|
+
_IMAGE_TAG_RE.lastIndex = 0;
|
|
703
|
+
let tag;
|
|
704
|
+
while ((tag = _IMAGE_TAG_RE.exec(code))) {
|
|
705
|
+
const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
|
|
706
|
+
const attrs = code.slice(tag.index, end);
|
|
707
|
+
_PERCENT_HEIGHT_RE.lastIndex = 0;
|
|
708
|
+
const hit = _PERCENT_HEIGHT_RE.exec(attrs);
|
|
709
|
+
if (!hit) continue;
|
|
710
|
+
const line = code.slice(0, tag.index + hit.index).split(/\r?\n/).length;
|
|
711
|
+
findings.push({
|
|
712
|
+
rule: "image-percent-height",
|
|
713
|
+
severity: "warning",
|
|
714
|
+
label:
|
|
715
|
+
`<${tag[1]}> sizes its height with a percentage — React Native ` +
|
|
716
|
+
`resolves that against the PARENT's height, and a content-sized ` +
|
|
717
|
+
`parent has none, so it collapses to 0: the image loads but is ` +
|
|
718
|
+
`invisible on BOTH the web Player and the native Expo export. Size ` +
|
|
719
|
+
`it with aspectRatio (e.g. { width: "100%", aspectRatio: 1 }) or a ` +
|
|
720
|
+
`numeric pixel height. Warning only — a percentage height is correct ` +
|
|
721
|
+
`when the parent has a definite height (e.g. a fixed-height hero).`,
|
|
722
|
+
line,
|
|
723
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
724
|
+
});
|
|
725
|
+
_IMAGE_TAG_RE.lastIndex = end;
|
|
726
|
+
}
|
|
727
|
+
return findings;
|
|
728
|
+
}
|
|
729
|
+
|
|
672
730
|
// Narrow a split-impl widget's manifest to the platform a single bundle file
|
|
673
731
|
// ships to, so `import-platform-mismatch` lints each file against what it
|
|
674
732
|
// actually targets. Mirror of linter.js.
|
|
@@ -729,6 +787,7 @@ function lintSource(source, options) {
|
|
|
729
787
|
findings.push(..._hostApiUrlRules(source));
|
|
730
788
|
findings.push(..._lucideIconRules(source));
|
|
731
789
|
findings.push(..._reactInScopeRules(source));
|
|
790
|
+
findings.push(..._imagePercentHeightRules(source));
|
|
732
791
|
findings.push(
|
|
733
792
|
..._scopeRules(source, options && options.manifest).map((f) => ({
|
|
734
793
|
...f,
|
package/dist/linter.js
CHANGED
|
@@ -761,6 +761,82 @@ function _reactInScopeRules(source) {
|
|
|
761
761
|
return findings;
|
|
762
762
|
}
|
|
763
763
|
|
|
764
|
+
// sc-3466 / sc-3493 — percentage height on an <Image> collapses to 0.
|
|
765
|
+
// React Native / Yoga resolves a percentage `height` against the PARENT's
|
|
766
|
+
// height; a content-sized parent has none, so the value resolves to 0 and the
|
|
767
|
+
// image fetches its uri but renders invisible on BOTH the web Player and the
|
|
768
|
+
// native Expo export. sc-3466 taught the rule to the AI widget agent's
|
|
769
|
+
// DEFAULT_SYSTEM_PROMPT, which remains the primary guard — this is the
|
|
770
|
+
// mechanical belt-and-braces catch for a model (or a human author) that
|
|
771
|
+
// ignores it.
|
|
772
|
+
//
|
|
773
|
+
// `severity: "warning"` deliberately: `height: "100%"` IS correct inside a
|
|
774
|
+
// parent with a definite height (a fixed-height hero), which the AST-free scan
|
|
775
|
+
// cannot see, so a blocking rule would reject valid widgets.
|
|
776
|
+
//
|
|
777
|
+
// Scope is the literal inline form only. A height threaded through a variable
|
|
778
|
+
// or a StyleSheet object stays out of reach of a text scan.
|
|
779
|
+
const _IMAGE_TAG_RE = /<(Image|ImageBackground)\b/g;
|
|
780
|
+
const _PERCENT_HEIGHT_RE =
|
|
781
|
+
/(^|[{,;\s])height\s*:\s*(["'])\s*\d+(?:\.\d+)?\s*%\s*\2/g;
|
|
782
|
+
|
|
783
|
+
// Index of the `>` closing the JSX opening tag that starts at `from`. Braces
|
|
784
|
+
// and string literals are skipped so a `>` inside `onPress={() => …}` or an
|
|
785
|
+
// attribute string can't end the tag early.
|
|
786
|
+
function _jsxOpenTagEnd(source, from) {
|
|
787
|
+
let depth = 0;
|
|
788
|
+
let quote = "";
|
|
789
|
+
for (let i = from; i < source.length; i += 1) {
|
|
790
|
+
const ch = source[i];
|
|
791
|
+
if (quote) {
|
|
792
|
+
if (ch === "\\") i += 1;
|
|
793
|
+
else if (ch === quote) quote = "";
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
797
|
+
else if (ch === "{") depth += 1;
|
|
798
|
+
else if (ch === "}") depth -= 1;
|
|
799
|
+
else if (ch === ">" && depth <= 0) return i;
|
|
800
|
+
}
|
|
801
|
+
return source.length;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function _imagePercentHeightRules(source) {
|
|
805
|
+
const findings = [];
|
|
806
|
+
// Comments are blanked (string contents kept) so a commented-out example —
|
|
807
|
+
// including the one in this rule's own docs — is never flagged.
|
|
808
|
+
const code = _stripNonCode(source, { keepStrings: true });
|
|
809
|
+
const sourceLines = source.split(/\r?\n/);
|
|
810
|
+
_IMAGE_TAG_RE.lastIndex = 0;
|
|
811
|
+
let tag;
|
|
812
|
+
while ((tag = _IMAGE_TAG_RE.exec(code))) {
|
|
813
|
+
const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
|
|
814
|
+
const attrs = code.slice(tag.index, end);
|
|
815
|
+
_PERCENT_HEIGHT_RE.lastIndex = 0;
|
|
816
|
+
const hit = _PERCENT_HEIGHT_RE.exec(attrs);
|
|
817
|
+
if (!hit) continue;
|
|
818
|
+
const line = code.slice(0, tag.index + hit.index).split(/\r?\n/).length;
|
|
819
|
+
findings.push({
|
|
820
|
+
rule: "image-percent-height",
|
|
821
|
+
severity: "warning",
|
|
822
|
+
label:
|
|
823
|
+
`<${tag[1]}> sizes its height with a percentage — React Native ` +
|
|
824
|
+
`resolves that against the PARENT's height, and a content-sized ` +
|
|
825
|
+
`parent has none, so it collapses to 0: the image loads but is ` +
|
|
826
|
+
`invisible on BOTH the web Player and the native Expo export. Size ` +
|
|
827
|
+
`it with aspectRatio (e.g. { width: "100%", aspectRatio: 1 }) or a ` +
|
|
828
|
+
`numeric pixel height. Warning only — a percentage height is correct ` +
|
|
829
|
+
`when the parent has a definite height (e.g. a fixed-height hero).`,
|
|
830
|
+
line,
|
|
831
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
832
|
+
});
|
|
833
|
+
// One finding per <Image>; a second percentage height on the same tag is
|
|
834
|
+
// the same defect.
|
|
835
|
+
_IMAGE_TAG_RE.lastIndex = end;
|
|
836
|
+
}
|
|
837
|
+
return findings;
|
|
838
|
+
}
|
|
839
|
+
|
|
764
840
|
/**
|
|
765
841
|
* Narrow a split-impl widget's manifest to the platform a single bundle file
|
|
766
842
|
* ships to, so `import-platform-mismatch` lints each file against what it
|
|
@@ -828,6 +904,8 @@ export function lintSource(source, options) {
|
|
|
828
904
|
findings.push(..._lucideIconRules(source));
|
|
829
905
|
// sc-2353 — widget source must be self-contained (reference React ⇒ import it).
|
|
830
906
|
findings.push(..._reactInScopeRules(source));
|
|
907
|
+
// sc-3493 — soft warning: percentage height on an <Image> collapses to 0.
|
|
908
|
+
findings.push(..._imagePercentHeightRules(source));
|
|
831
909
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — scope-aware rules. Run after the
|
|
832
910
|
// line-by-line scan so banned-identifier findings stay first in the
|
|
833
911
|
// output.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.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",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/build.js",
|
|
51
|
-
"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-filestore-file.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 src/__tests__/property-schema-resolve.test.js"
|
|
51
|
+
"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-filestore-file.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-image-height.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 src/__tests__/property-schema-resolve.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|