@colixsystems/widget-sdk 0.139.0 → 0.141.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 +30 -3
- package/dist/cli.js +12 -3
- package/dist/contract.cjs +17 -23
- package/dist/contract.js +17 -23
- package/dist/host.d.ts +12 -14
- package/dist/host.js +8 -9
- package/dist/linter.cjs +262 -5
- package/dist/linter.js +261 -5
- package/dist/navigation.cjs +24 -36
- package/dist/navigation.js +24 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -70,7 +70,27 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
70
70
|
|
|
71
71
|
## Status
|
|
72
72
|
|
|
73
|
-
`v0.
|
|
73
|
+
`v0.141.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.141.0 (contract 1.109.0)
|
|
76
|
+
|
|
77
|
+
**The app's global navigation is the sidebar alone — `top-bar` and `bottom-tabs` leave `CONTRACT.themeMenuTypes` (sc-7352, REQ-NAV-STRUCTURE).** A top bar is now a PAGE's local bar drawing a saved menu (REQ-NAV-LOCAL, sc-7103): the same tab row the `top-bar` shape used to draw, themed by the same `topBar` tokens (`resolveTopBarTokens` is unchanged), worn by one page under the sidebar. Two "top bars" competing for one word and one token set was the reason.
|
|
78
|
+
|
|
79
|
+
- `normaliseNavigation` resolves a stored `top-bar` or `bottom-tabs` to `sidebar`, so a host never has to guard the value. `menuItemCap` / `quickBarCap` keep their signatures (`quickBarCap("sidebar")` is still 5).
|
|
80
|
+
- The global menu never lived in the shape (it is the pages' `show_in_menu` flags), so a workspace that stored a retired shape already draws the sidebar with the same menu. The sc-7352 backfill deletes the stored key BEFORE the shapes disappear; a bottom-tabs app's phone strip becomes the sidebar's quick bar.
|
|
81
|
+
- Host-integration surface only; no author-facing hook, prop or manifest field changed. `CONTRACT.version` → `1.109.0`.
|
|
82
|
+
|
|
83
|
+
### What's new in 0.140.0 (contract 1.108.0)
|
|
84
|
+
|
|
85
|
+
**Three rules make a generated widget's EDITABILITY checkable, and `lintEditability` joins the linter export (sc-7799).** The rules that already had a deterministic gate — the three basic style fields, `no-hardcoded-design`, `style-field-unread` — held every time. The ones left to prose drifted: `ui.group`, per-element coverage and author-editable copy were guidance only, so some widgets shipped a grouped Style section covering every element while others shipped the three basics and nothing else, and wording was routinely pinned in the source where no author could reach it.
|
|
86
|
+
|
|
87
|
+
- **`style-group-missing`** — a `styleSchema` field with no `ui.group`, or one filed under a catch-all name (`Advanced`, `Other`, `Misc`, `Style`, `Styles`, `General`) that rebuilds the flat list grouping exists to split. The three basics belong under `"Basics"`; each per-element field under the Title Case singular name of the element it styles (`"Card"`, `"Title"`, `"Row"`).
|
|
88
|
+
- **`style-elements-uncovered`** — a widget that renders several visual elements while its `styleSchema` carries nothing beyond `background`/`textColor`/`align`. A genuinely single-element widget is exempt by construction.
|
|
89
|
+
- **`text-literal-unexposed`** — a string literal sitting directly in a `<Text>`: copy that reaches neither the author nor the translation pipeline.
|
|
90
|
+
|
|
91
|
+
All three are `warning`, so `appstudio-widget lint` reports them and still exits 0 — the human author decides when to act. The AI widget agent publishes with no human in the loop and `_lintFindingsToPublishChecks` turns every finding into a publish check regardless of severity, so the same three **block** an agent publish and drive a repair turn, with no new severity plumbing.
|
|
92
|
+
|
|
93
|
+
`lintEditability(manifest, files)` is bundle-level like `lintStyleWiring`, and needs `--manifest` for the same reason. It counts rendered elements PER FILE, though: joining `widget.web.jsx` and `widget.native.jsx` would double every tag and call a single-`<Text>` split-impl widget multi-element.
|
|
74
94
|
|
|
75
95
|
### What's new in 0.139.0 (contract 1.108.0)
|
|
76
96
|
|
|
@@ -1762,15 +1782,22 @@ npx appstudio-widget lint widget.web.jsx widget.native.jsx --manifest manifest.j
|
|
|
1762
1782
|
Scans for banned patterns (`eval`, `new Function`, dynamic `import()`, direct imports of host stores, raw axios) and for the styling rules below. **Only error-severity findings change the exit code**; warnings print and exit 0.
|
|
1763
1783
|
|
|
1764
1784
|
```js
|
|
1765
|
-
import {
|
|
1785
|
+
import {
|
|
1786
|
+
lintSource,
|
|
1787
|
+
lintStyleWiring,
|
|
1788
|
+
lintEditability,
|
|
1789
|
+
} from "@colixsystems/widget-sdk/linter";
|
|
1766
1790
|
const report = lintSource(source, { manifest });
|
|
1767
1791
|
// Bundle-level — pass every file the widget ships:
|
|
1768
1792
|
const wiring = lintStyleWiring(manifest, { "widget.jsx": source });
|
|
1793
|
+
const editability = lintEditability(manifest, { "widget.jsx": source });
|
|
1769
1794
|
```
|
|
1770
1795
|
|
|
1771
1796
|
Two rules keep a widget's look reachable from the Studio (sc-6455). `no-hardcoded-design` flags a colour literal, a `fontFamily` string, or a numeric `fontSize` — values that outrank the theme permanently and that no control can move. `style-field-unread` flags a `styleSchema` field the source never reads, which renders a control that does nothing. Both are warnings for a human author and blocking for the AI widget agent. License a genuinely un-tokenizable value with a preceding `// appstudio-design-ok: <reason>` (the reason is required); see *What's new in 0.110.0*.
|
|
1772
1797
|
|
|
1773
|
-
|
|
1798
|
+
Three more rules keep a widget EDITABLE once it ships (sc-7799), via the bundle-level `lintEditability(manifest, files)`. `style-group-missing` flags a `styleSchema` field with no `ui.group` — or one under a catch-all name — so a long Style section reads as labelled per-element fieldsets. `style-elements-uncovered` flags a multi-element widget whose Style section still only restyles the widget as a whole. `text-literal-unexposed` flags copy pinned in a `<Text>` — wording no control can change and no locale can translate. All three are warnings here and blocking for the AI widget agent; see *What's new in 0.140.0*.
|
|
1799
|
+
|
|
1800
|
+
Pass `--manifest` to enable `style-field-unread` and the three editability rules — they need the manifest, and every source at once so a split-impl widget's per-host reads are seen together.
|
|
1774
1801
|
|
|
1775
1802
|
`flex-basis-percent` flags a literal percentage `flexBasis` (sc-7274) — `"100%"`, `"50%"`, or the ternary a responsive cell writes. `flex-basis` sizes the MAIN axis, so in a column parent it asks for the parent's HEIGHT: under an ancestor that makes that height definite (a Grid cell stretches its child on the web Player) the child fills the container and, since a `View` never shrinks, overflows over everything below it. Say full-width with `width: "100%"` — correct in a column AND in a wrap row — and keep a NUMBER for a wrap threshold. A warning, because the percentage is right in a row and a text scan cannot see the parent.
|
|
1776
1803
|
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { readFileSync } from "node:fs";
|
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { argv, exit, stderr, stdout } from "node:process";
|
|
11
|
-
import { lintSource, lintStyleWiring } from "./linter.js";
|
|
11
|
+
import { lintSource, lintStyleWiring, lintEditability } from "./linter.js";
|
|
12
12
|
import { startDevServer } from "./devserver.js";
|
|
13
13
|
|
|
14
14
|
function usage() {
|
|
@@ -106,9 +106,14 @@ async function runLint(rest) {
|
|
|
106
106
|
const wiring = manifest
|
|
107
107
|
? lintStyleWiring(manifest, files)
|
|
108
108
|
: { ok: true, findings: [] };
|
|
109
|
+
// sc-7799 — same manifest+bundle shape as the dead-control gate above.
|
|
110
|
+
const editability = manifest
|
|
111
|
+
? lintEditability(manifest, files)
|
|
112
|
+
: { ok: true, findings: [] };
|
|
109
113
|
const all = perFile
|
|
110
114
|
.flatMap(([, r]) => r.findings)
|
|
111
|
-
.concat(wiring.findings)
|
|
115
|
+
.concat(wiring.findings)
|
|
116
|
+
.concat(editability.findings);
|
|
112
117
|
if (all.length === 0) {
|
|
113
118
|
stdout.write(`${label}: clean\n`);
|
|
114
119
|
exit(0);
|
|
@@ -116,7 +121,7 @@ async function runLint(rest) {
|
|
|
116
121
|
// sc-3493 — a warning-severity finding used to be swallowed: `ok` stays true
|
|
117
122
|
// for warnings, so the CLI printed "clean" and dropped them. A warning nobody
|
|
118
123
|
// sees is pointless. Report every finding; only errors change the exit code.
|
|
119
|
-
const ok = perFile.every(([, r]) => r.ok) && wiring.ok;
|
|
124
|
+
const ok = perFile.every(([, r]) => r.ok) && wiring.ok && editability.ok;
|
|
120
125
|
const errors = all.filter((f) => f.severity !== "warning").length;
|
|
121
126
|
const stream = ok ? stdout : stderr;
|
|
122
127
|
stream.write(
|
|
@@ -131,6 +136,10 @@ async function runLint(rest) {
|
|
|
131
136
|
stream.write(`manifest.styleSchema:\n`);
|
|
132
137
|
writeFindings(wiring.findings, stream);
|
|
133
138
|
}
|
|
139
|
+
if (editability.findings.length > 0) {
|
|
140
|
+
stream.write(`manifest.editability:\n`);
|
|
141
|
+
writeFindings(editability.findings, stream);
|
|
142
|
+
}
|
|
134
143
|
exit(ok ? 0 : 1);
|
|
135
144
|
}
|
|
136
145
|
|
package/dist/contract.cjs
CHANGED
|
@@ -217,36 +217,20 @@ const THEME_WIDGET_STYLES = Object.freeze({
|
|
|
217
217
|
// PlayerChrome and the compiler's navigator -- so a type cannot be offered to an
|
|
218
218
|
// author without every host actually drawing it.
|
|
219
219
|
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
220
|
+
// REQ-NAV-STRUCTURE (sc-7352): the app's GLOBAL navigation has exactly one
|
|
221
|
+
// shape, the sidebar. A top bar is a PAGE's local bar drawing a saved menu
|
|
222
|
+
// (REQ-NAV-LOCAL), themed by the `topBar` tokens, never an app-wide menu shape.
|
|
223
|
+
// `maxItems` is the menu cap (none for a rail); `quickBarMaxItems` caps the
|
|
224
|
+
// SECONDARY mobile quick bar, which may drop a page because the rail and the
|
|
225
|
+
// drawer still list every one.
|
|
225
226
|
const THEME_MENU_TYPES = Object.freeze({
|
|
226
227
|
sidebar: Object.freeze({
|
|
227
228
|
name: "Sidebar",
|
|
228
229
|
summary:
|
|
229
230
|
"A persistent left rail on desktop; a hamburger drawer plus the optional bottom quick bar on mobile.",
|
|
230
231
|
maxItems: null,
|
|
231
|
-
// The SECONDARY mobile quick bar's cap. It may drop a page past it, because
|
|
232
|
-
// the rail and the drawer still list every one -- the opposite of
|
|
233
|
-
// `bottom-tabs`' `maxItems`, where the strip IS the menu.
|
|
234
232
|
quickBarMaxItems: 5,
|
|
235
233
|
}),
|
|
236
|
-
"top-bar": Object.freeze({
|
|
237
|
-
name: "Top bar",
|
|
238
|
-
summary:
|
|
239
|
-
"A tab row beneath the app header at every width -- icon and label per page, scrolling sideways when it runs out of room. No rail at any width.",
|
|
240
|
-
maxItems: null,
|
|
241
|
-
quickBarMaxItems: null,
|
|
242
|
-
}),
|
|
243
|
-
"bottom-tabs": Object.freeze({
|
|
244
|
-
name: "Bottom tabs",
|
|
245
|
-
summary:
|
|
246
|
-
"A sticky bottom strip at every width -- the phone-native shape. Pages past the cap move behind a More sheet.",
|
|
247
|
-
maxItems: 4,
|
|
248
|
-
quickBarMaxItems: null,
|
|
249
|
-
}),
|
|
250
234
|
});
|
|
251
235
|
const THEME_SPACING_SCALE = Object.freeze({
|
|
252
236
|
min: 0.5,
|
|
@@ -3976,7 +3960,17 @@ const CONTRACT = deepFreeze({
|
|
|
3976
3960
|
// a stored hex is untouched and still renders byte-identically, and an
|
|
3977
3961
|
// unresolvable binding falls back to the field's `themeDefault` rather
|
|
3978
3962
|
// than painting blank.
|
|
3979
|
-
|
|
3963
|
+
// 1.109.0: BREAKING on the pre-1.0 channel (sc-7352) — `themeMenuTypes`
|
|
3964
|
+
// loses `top-bar` and `bottom-tabs`: the app's global navigation is the
|
|
3965
|
+
// sidebar alone, and a top bar is a page's LOCAL bar drawing a saved menu
|
|
3966
|
+
// (REQ-NAV-LOCAL), themed by the same `topBar` tokens. A stored
|
|
3967
|
+
// `navigation.menuType` of a retired shape resolves to `sidebar` through
|
|
3968
|
+
// `normaliseNavigation`, so no host has to guard the value; the global
|
|
3969
|
+
// menu never lived in the shape, so such apps already draw the sidebar,
|
|
3970
|
+
// and the sc-7352 backfill deletes the stored key (a bottom-tabs app's
|
|
3971
|
+
// strip becomes the sidebar's quick bar) BEFORE this ships.
|
|
3972
|
+
// `menuItemCap` / `quickBarCap` keep their signatures.
|
|
3973
|
+
version: "1.109.0",
|
|
3980
3974
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3981
3975
|
hooks: HOOKS,
|
|
3982
3976
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -217,36 +217,20 @@ const THEME_WIDGET_STYLES = Object.freeze({
|
|
|
217
217
|
// PlayerChrome and the compiler's navigator -- so a type cannot be offered to an
|
|
218
218
|
// author without every host actually drawing it.
|
|
219
219
|
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
220
|
+
// REQ-NAV-STRUCTURE (sc-7352): the app's GLOBAL navigation has exactly one
|
|
221
|
+
// shape, the sidebar. A top bar is a PAGE's local bar drawing a saved menu
|
|
222
|
+
// (REQ-NAV-LOCAL), themed by the `topBar` tokens, never an app-wide menu shape.
|
|
223
|
+
// `maxItems` is the menu cap (none for a rail); `quickBarMaxItems` caps the
|
|
224
|
+
// SECONDARY mobile quick bar, which may drop a page because the rail and the
|
|
225
|
+
// drawer still list every one.
|
|
225
226
|
const THEME_MENU_TYPES = Object.freeze({
|
|
226
227
|
sidebar: Object.freeze({
|
|
227
228
|
name: "Sidebar",
|
|
228
229
|
summary:
|
|
229
230
|
"A persistent left rail on desktop; a hamburger drawer plus the optional bottom quick bar on mobile.",
|
|
230
231
|
maxItems: null,
|
|
231
|
-
// The SECONDARY mobile quick bar's cap. It may drop a page past it, because
|
|
232
|
-
// the rail and the drawer still list every one -- the opposite of
|
|
233
|
-
// `bottom-tabs`' `maxItems`, where the strip IS the menu.
|
|
234
232
|
quickBarMaxItems: 5,
|
|
235
233
|
}),
|
|
236
|
-
"top-bar": Object.freeze({
|
|
237
|
-
name: "Top bar",
|
|
238
|
-
summary:
|
|
239
|
-
"A tab row beneath the app header at every width -- icon and label per page, scrolling sideways when it runs out of room. No rail at any width.",
|
|
240
|
-
maxItems: null,
|
|
241
|
-
quickBarMaxItems: null,
|
|
242
|
-
}),
|
|
243
|
-
"bottom-tabs": Object.freeze({
|
|
244
|
-
name: "Bottom tabs",
|
|
245
|
-
summary:
|
|
246
|
-
"A sticky bottom strip at every width -- the phone-native shape. Pages past the cap move behind a More sheet.",
|
|
247
|
-
maxItems: 4,
|
|
248
|
-
quickBarMaxItems: null,
|
|
249
|
-
}),
|
|
250
234
|
});
|
|
251
235
|
const THEME_SPACING_SCALE = Object.freeze({
|
|
252
236
|
min: 0.5,
|
|
@@ -3976,7 +3960,17 @@ const CONTRACT = deepFreeze({
|
|
|
3976
3960
|
// a stored hex is untouched and still renders byte-identically, and an
|
|
3977
3961
|
// unresolvable binding falls back to the field's `themeDefault` rather
|
|
3978
3962
|
// than painting blank.
|
|
3979
|
-
|
|
3963
|
+
// 1.109.0: BREAKING on the pre-1.0 channel (sc-7352) — `themeMenuTypes`
|
|
3964
|
+
// loses `top-bar` and `bottom-tabs`: the app's global navigation is the
|
|
3965
|
+
// sidebar alone, and a top bar is a page's LOCAL bar drawing a saved menu
|
|
3966
|
+
// (REQ-NAV-LOCAL), themed by the same `topBar` tokens. A stored
|
|
3967
|
+
// `navigation.menuType` of a retired shape resolves to `sidebar` through
|
|
3968
|
+
// `normaliseNavigation`, so no host has to guard the value; the global
|
|
3969
|
+
// menu never lived in the shape, so such apps already draw the sidebar,
|
|
3970
|
+
// and the sc-7352 backfill deletes the stored key (a bottom-tabs app's
|
|
3971
|
+
// strip becomes the sidebar's quick bar) BEFORE this ships.
|
|
3972
|
+
// `menuItemCap` / `quickBarCap` keep their signatures.
|
|
3973
|
+
version: "1.109.0",
|
|
3980
3974
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3981
3975
|
hooks: HOOKS,
|
|
3982
3976
|
primitives: PRIMITIVES,
|
package/dist/host.d.ts
CHANGED
|
@@ -150,10 +150,11 @@ export function createToastController(
|
|
|
150
150
|
): ToastController;
|
|
151
151
|
|
|
152
152
|
// REQ-NAV-STRUCTURE — the navigation SHAPE a host draws its chrome in. The
|
|
153
|
-
// vocabulary is closed by `CONTRACT.themeMenuTypes`;
|
|
154
|
-
//
|
|
153
|
+
// vocabulary is closed by `CONTRACT.themeMenuTypes`; since sc-7352 it holds
|
|
154
|
+
// the sidebar alone (a top bar is a page's local bar, REQ-NAV-LOCAL), and any
|
|
155
|
+
// stored value outside it resolves to `sidebar`.
|
|
155
156
|
|
|
156
|
-
export type ThemeMenuType = "sidebar"
|
|
157
|
+
export type ThemeMenuType = "sidebar";
|
|
157
158
|
|
|
158
159
|
export interface ResolvedNavigation {
|
|
159
160
|
menuType: ThemeMenuType;
|
|
@@ -168,8 +169,7 @@ export function normaliseNavigation(navigation: unknown): ResolvedNavigation;
|
|
|
168
169
|
|
|
169
170
|
/**
|
|
170
171
|
* How many menu pages this shape may draw at once, or `null` for no cap. The
|
|
171
|
-
*
|
|
172
|
-
* it, a bottom-tabs strip must move its surplus behind a More sheet.
|
|
172
|
+
* sidebar has none — its rail and drawer list every menu page.
|
|
173
173
|
*/
|
|
174
174
|
export function menuItemCap(menuType: string): number | null;
|
|
175
175
|
|
|
@@ -232,16 +232,14 @@ export interface TopBarTokens {
|
|
|
232
232
|
* label plus its indicator. Falls back to the RAIL's `activeColor` and then
|
|
233
233
|
* to the brand, so an app states its navigation colour once. */
|
|
234
234
|
activeColor: string;
|
|
235
|
-
/** How a
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* under the `links` style, which draws no tabs. */
|
|
235
|
+
/** How a local bar marks its current page. `underline` keeps the tab in the
|
|
236
|
+
* bar's surface behind an indicator; `attached` cuts the bar away under the
|
|
237
|
+
* current tab so the page shows through it. */
|
|
239
238
|
tabStyle: "underline" | "attached";
|
|
240
|
-
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
|
|
244
|
-
contentSurface: string;
|
|
239
|
+
/** Under `attached`, the colour the PANEL behind the row paints — the
|
|
240
|
+
* authored `tabActiveBackgroundColor`, alpha kept — or null for the page's
|
|
241
|
+
* own wash. The current tab paints nothing itself. */
|
|
242
|
+
contentSurface: string | null;
|
|
245
243
|
/** A tab's own surface, authored or null. Null paints none and the bar shows
|
|
246
244
|
* through. */
|
|
247
245
|
tabBackgroundColor: string | null;
|
package/dist/host.js
CHANGED
|
@@ -52,15 +52,14 @@ export {
|
|
|
52
52
|
TOAST_DEFAULTS,
|
|
53
53
|
} from "./toast-host.js";
|
|
54
54
|
|
|
55
|
-
// REQ-NAV-STRUCTURE: the navigation
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// the sidebar's so an untouched workspace
|
|
62
|
-
//
|
|
63
|
-
// panel is hidden and those tokens can no longer be set at all.
|
|
55
|
+
// REQ-NAV-STRUCTURE: the navigation resolver both hosts draw their chrome from.
|
|
56
|
+
// `normaliseNavigation` turns a stored `theme_config.navigation` block into the
|
|
57
|
+
// resolved `{ menuType }` — always `sidebar` since sc-7352 retired the other
|
|
58
|
+
// shapes, so a stored value never needs guarding; `menuItemCap` / `quickBarCap`
|
|
59
|
+
// state the caps. One implementation, so the chrome cannot mean one thing in
|
|
60
|
+
// the Player and another in the Expo export. `resolveFooterTokens` is the quick
|
|
61
|
+
// bar's own token set, falling back to the sidebar's so an untouched workspace
|
|
62
|
+
// keeps the appearance it has.
|
|
64
63
|
// REQ-THEME-14: `resolveSidebarTokens` and `resolveTopBarTokens` are the same
|
|
65
64
|
// idea for the rail and the app bar, whose defaults used to live inline in each
|
|
66
65
|
// host and had already drifted apart.
|
package/dist/linter.cjs
CHANGED
|
@@ -1438,11 +1438,7 @@ function lintStyleWiring(manifest, files) {
|
|
|
1438
1438
|
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
1439
1439
|
return empty;
|
|
1440
1440
|
}
|
|
1441
|
-
|
|
1442
|
-
if (typeof files === "string") sources = [files];
|
|
1443
|
-
else if (Array.isArray(files)) sources = files;
|
|
1444
|
-
else if (files && typeof files === "object") sources = Object.values(files);
|
|
1445
|
-
sources = sources.filter((s) => typeof s === "string" && s.length > 0);
|
|
1441
|
+
const sources = _bundleSources(files);
|
|
1446
1442
|
if (sources.length === 0) return empty;
|
|
1447
1443
|
const code = sources
|
|
1448
1444
|
.map((s) => _stripNonCode(s, { keepStrings: true }))
|
|
@@ -1471,6 +1467,266 @@ function lintStyleWiring(manifest, files) {
|
|
|
1471
1467
|
return { ok: !hasErrors, findings };
|
|
1472
1468
|
}
|
|
1473
1469
|
|
|
1470
|
+
// sc-7799 — a group name that groups everything defeats the rule: one
|
|
1471
|
+
// catch-all fieldset reads exactly like the flat list it replaced.
|
|
1472
|
+
const _GENERIC_STYLE_GROUPS = new Set([
|
|
1473
|
+
"advanced",
|
|
1474
|
+
"other",
|
|
1475
|
+
"misc",
|
|
1476
|
+
"style",
|
|
1477
|
+
"styles",
|
|
1478
|
+
"general",
|
|
1479
|
+
]);
|
|
1480
|
+
|
|
1481
|
+
// The three the basic-style gate mandates; they restyle the widget as a whole.
|
|
1482
|
+
const _WHOLE_WIDGET_STYLE_FIELDS = new Set([
|
|
1483
|
+
"background",
|
|
1484
|
+
"textColor",
|
|
1485
|
+
"align",
|
|
1486
|
+
]);
|
|
1487
|
+
|
|
1488
|
+
// A second element an author would style apart from the text. Pressable and
|
|
1489
|
+
// Button are deliberately absent: a touch target wrapping the only label is
|
|
1490
|
+
// one element, not two.
|
|
1491
|
+
const _SECOND_ELEMENT_TAGS = ["Image", "TextInput", "FlatList", "SectionList"];
|
|
1492
|
+
|
|
1493
|
+
const _SEPARATOR_GLYPHS = new Set(["-", "–", "—", "•", "·", "…", "|", "/"]);
|
|
1494
|
+
|
|
1495
|
+
// Names ride the finding; the remainder is counted, so one finding stays
|
|
1496
|
+
// inside the agent's per-message budget however many fields are wrong.
|
|
1497
|
+
const _EDITABILITY_LIST_CAP = 3;
|
|
1498
|
+
|
|
1499
|
+
// Tighter for copy: each entry carries the text AND its line, so six of them
|
|
1500
|
+
// push the message past the report's 300-char truncation.
|
|
1501
|
+
const _LITERAL_LIST_CAP = 2;
|
|
1502
|
+
|
|
1503
|
+
// Shared by both bundle-level gates — one normaliser, not two.
|
|
1504
|
+
function _bundleSources(files) {
|
|
1505
|
+
let sources = [];
|
|
1506
|
+
if (typeof files === "string") sources = [files];
|
|
1507
|
+
else if (Array.isArray(files)) sources = files;
|
|
1508
|
+
else if (files && typeof files === "object") sources = Object.values(files);
|
|
1509
|
+
return sources.filter((s) => typeof s === "string" && s.length > 0);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function _plainObject(value) {
|
|
1513
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Both the count and each entry are bounded: the backend wraps the label in
|
|
1517
|
+
// `[manifest.editability] … (line N: snippet)` and truncates at 300, so an
|
|
1518
|
+
// unbounded name list loses the tail of the advice that follows it.
|
|
1519
|
+
function _clip(text, max) {
|
|
1520
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function _quoteList(names) {
|
|
1524
|
+
const shown = names.slice(0, _EDITABILITY_LIST_CAP).map((n) => _clip(n, 16));
|
|
1525
|
+
const more = names.length - shown.length;
|
|
1526
|
+
return shown.join(", ") + (more > 0 ? `, …(+${more} more)` : "");
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// Copy worth a control: a separator glyph or a bare number is chrome.
|
|
1530
|
+
function _isAuthoredCopy(text) {
|
|
1531
|
+
if (text.length < 2) return false;
|
|
1532
|
+
if (!/[A-Za-z]/.test(text)) return false;
|
|
1533
|
+
return !_SEPARATOR_GLYPHS.has(text);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// The `>` that CLOSES a JSX tag, skipping quoted attribute values and any `>`
|
|
1537
|
+
// inside a `{…}` expression — `onPress={() => x}` carries one, and treating it
|
|
1538
|
+
// as the tag end reports the handler body as copy.
|
|
1539
|
+
function _tagEnd(code, from) {
|
|
1540
|
+
let depth = 0;
|
|
1541
|
+
let quote = null;
|
|
1542
|
+
for (let i = from; i < code.length; i++) {
|
|
1543
|
+
const ch = code[i];
|
|
1544
|
+
if (quote) {
|
|
1545
|
+
if (ch === "\\") i++;
|
|
1546
|
+
else if (ch === quote) quote = null;
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
1550
|
+
else if (ch === "{") depth++;
|
|
1551
|
+
else if (ch === "}") depth--;
|
|
1552
|
+
else if (ch === ">" && depth <= 0) return i;
|
|
1553
|
+
}
|
|
1554
|
+
return -1;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// The balanced `{…}` starting at `from`, or null.
|
|
1558
|
+
function _bracedSpan(code, from) {
|
|
1559
|
+
if (code[from] !== "{") return null;
|
|
1560
|
+
let depth = 0;
|
|
1561
|
+
let quote = null;
|
|
1562
|
+
for (let i = from; i < code.length; i++) {
|
|
1563
|
+
const ch = code[i];
|
|
1564
|
+
if (quote) {
|
|
1565
|
+
if (ch === "\\") i++;
|
|
1566
|
+
else if (ch === quote) quote = null;
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
1570
|
+
else if (ch === "{") depth++;
|
|
1571
|
+
else if (ch === "}") {
|
|
1572
|
+
depth--;
|
|
1573
|
+
if (depth === 0) return code.slice(from + 1, i);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Copy pinned in a `<Text>`: a bare literal child, or a braced child that is
|
|
1580
|
+
// NOTHING BUT a string literal (`{"Hello"}`) — the one-pair-of-braces evasion.
|
|
1581
|
+
// A braced expression carrying an identifier is left alone: it may already be
|
|
1582
|
+
// a prop, and under-firing is right for a gate that blocks a publish.
|
|
1583
|
+
function _textLiteralOccurrences(codes, masks) {
|
|
1584
|
+
const byText = new Map();
|
|
1585
|
+
for (let f = 0; f < codes.length; f++) {
|
|
1586
|
+
const code = codes[f];
|
|
1587
|
+
// Locate tags on the string-blanked mask, so markup HELD IN a string is
|
|
1588
|
+
// not read as markup. Both masks preserve offsets, so the child is read
|
|
1589
|
+
// back from the kept-strings source at the very same index.
|
|
1590
|
+
const mask = masks[f];
|
|
1591
|
+
const re = /<Text\b/g;
|
|
1592
|
+
let m;
|
|
1593
|
+
while ((m = re.exec(mask)) !== null) {
|
|
1594
|
+
const end = _tagEnd(mask, m.index);
|
|
1595
|
+
if (end === -1) continue;
|
|
1596
|
+
let i = end + 1;
|
|
1597
|
+
while (i < code.length && /\s/.test(code[i])) i++;
|
|
1598
|
+
let text = null;
|
|
1599
|
+
if (code[i] === "{") {
|
|
1600
|
+
const inner = _bracedSpan(code, i);
|
|
1601
|
+
const lit =
|
|
1602
|
+
inner && inner.trim().match(/^(["'`])((?:\\.|(?!\1).)*)\1$/);
|
|
1603
|
+
// A template carrying `${…}` interpolates a value, so it is not a literal.
|
|
1604
|
+
if (lit && !(lit[1] === "`" && lit[2].includes("${"))) text = lit[2];
|
|
1605
|
+
} else {
|
|
1606
|
+
const child = code.slice(i).match(/^([^<>{}\n]+)/);
|
|
1607
|
+
if (child) text = child[1];
|
|
1608
|
+
}
|
|
1609
|
+
if (text === null) continue;
|
|
1610
|
+
text = text.trim();
|
|
1611
|
+
if (!_isAuthoredCopy(text) || byText.has(text)) continue;
|
|
1612
|
+
// Split-impl carries the same copy twice; report the string, not the file.
|
|
1613
|
+
byText.set(text, code.slice(0, i).split("\n").length);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return [...byText.entries()].map(([text, line]) => ({ text, line }));
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// Per FILE: joining widget.web.jsx and widget.native.jsx doubles every tag.
|
|
1620
|
+
// A second element must be a DISTINCT visual kind beside the text — two
|
|
1621
|
+
// `<Text>` tags are as often a loading branch and its content as a title and
|
|
1622
|
+
// a body, and a blocking gate may not guess.
|
|
1623
|
+
function _rendersSeveralElements(code) {
|
|
1624
|
+
if (!/<Text[\s>/]/.test(code)) return false;
|
|
1625
|
+
return _SECOND_ELEMENT_TAGS.some((tag) =>
|
|
1626
|
+
new RegExp(`<${tag}[\\s>/]`).test(code),
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function _styleGroupFindings(manifest) {
|
|
1631
|
+
const schema = _plainObject(manifest) ? manifest.styleSchema : null;
|
|
1632
|
+
if (!_plainObject(schema)) return [];
|
|
1633
|
+
const bad = Object.keys(schema).filter((field) => {
|
|
1634
|
+
const def = schema[field];
|
|
1635
|
+
const group = _plainObject(def) && _plainObject(def.ui) ? def.ui.group : "";
|
|
1636
|
+
if (typeof group !== "string" || group.trim().length === 0) return true;
|
|
1637
|
+
return _GENERIC_STYLE_GROUPS.has(group.trim().toLowerCase());
|
|
1638
|
+
});
|
|
1639
|
+
if (bad.length === 0) return [];
|
|
1640
|
+
return [
|
|
1641
|
+
{
|
|
1642
|
+
rule: "style-group-missing",
|
|
1643
|
+
severity: "warning",
|
|
1644
|
+
label:
|
|
1645
|
+
`styleSchema fields with no usable ui.group: ${_quoteList(
|
|
1646
|
+
bad.map((f) => `"${f}"`),
|
|
1647
|
+
)}. Group EVERY field — basics under "Basics", each per-element ` +
|
|
1648
|
+
`field under the element it styles. A catch-all name is not a group.`,
|
|
1649
|
+
line: 0,
|
|
1650
|
+
snippet: _clip(bad[0], 12),
|
|
1651
|
+
},
|
|
1652
|
+
];
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function _styleCoverageFindings(manifest, codes) {
|
|
1656
|
+
const schema = _plainObject(manifest) ? manifest.styleSchema : null;
|
|
1657
|
+
if (!_plainObject(schema)) return [];
|
|
1658
|
+
const fields = Object.keys(schema);
|
|
1659
|
+
if (fields.length === 0) return [];
|
|
1660
|
+
if (fields.some((f) => !_WHOLE_WIDGET_STYLE_FIELDS.has(f))) return [];
|
|
1661
|
+
if (!codes.some(_rendersSeveralElements)) return [];
|
|
1662
|
+
return [
|
|
1663
|
+
{
|
|
1664
|
+
rule: "style-elements-uncovered",
|
|
1665
|
+
severity: "warning",
|
|
1666
|
+
label:
|
|
1667
|
+
`The widget renders more than one visual element but its styleSchema ` +
|
|
1668
|
+
`stops at background/textColor/align. Add 2-4 grouped per-element ` +
|
|
1669
|
+
`fields (cardBackground, titleColor, rowBackground, imageRadius), ` +
|
|
1670
|
+
`each applied ONLY when set.`,
|
|
1671
|
+
line: 0,
|
|
1672
|
+
snippet: "",
|
|
1673
|
+
},
|
|
1674
|
+
];
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function _textLiteralFindings(occurrences) {
|
|
1678
|
+
if (occurrences.length === 0) return [];
|
|
1679
|
+
// Each entry is bounded so a long line of copy cannot push the whole
|
|
1680
|
+
// message past the report's truncation and take the line numbers with it.
|
|
1681
|
+
const listed = occurrences
|
|
1682
|
+
.slice(0, _LITERAL_LIST_CAP)
|
|
1683
|
+
.map((o) => `"${_clip(o.text, 16)}" (line ${o.line})`);
|
|
1684
|
+
const more = occurrences.length - listed.length;
|
|
1685
|
+
return [
|
|
1686
|
+
{
|
|
1687
|
+
rule: "text-literal-unexposed",
|
|
1688
|
+
severity: "warning",
|
|
1689
|
+
label:
|
|
1690
|
+
`Copy pinned in a <Text> reaches neither author nor translation: ` +
|
|
1691
|
+
`${listed.join(", ")}${more > 0 ? `, …(+${more} more)` : ""}. ` +
|
|
1692
|
+
`Declare each as a propertySchema string defaulting to that text ` +
|
|
1693
|
+
`and render the prop; fixed chrome uses t().`,
|
|
1694
|
+
line: occurrences[0].line,
|
|
1695
|
+
snippet: _clip(occurrences[0].text, 12),
|
|
1696
|
+
},
|
|
1697
|
+
];
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* sc-7799 — the editability gates: what an author can still change once the
|
|
1702
|
+
* widget ships. Bundle-level like `lintStyleWiring`, and for the same reason
|
|
1703
|
+
* — one manifest serves both halves of a split-impl widget.
|
|
1704
|
+
*
|
|
1705
|
+
* Each rule emits ONE consolidated finding, because the agent's draft-findings
|
|
1706
|
+
* report keeps only the first dozen checks and truncates each message: a
|
|
1707
|
+
* per-field finding on a 14-field manifest would evict the render-time crash
|
|
1708
|
+
* sitting behind it.
|
|
1709
|
+
*
|
|
1710
|
+
* @param {object} manifest widget manifest (`styleSchema` only)
|
|
1711
|
+
* @param {object|string[]|string} files bundle sources — a `{ name: source }`
|
|
1712
|
+
* map, an array of sources, or one source string
|
|
1713
|
+
* @returns {{ ok: boolean, findings: Array<{ rule: string, severity: string, label: string, line: number, snippet: string }> }}
|
|
1714
|
+
*/
|
|
1715
|
+
function lintEditability(manifest, files) {
|
|
1716
|
+
const sources = _bundleSources(files);
|
|
1717
|
+
if (sources.length === 0) return { ok: true, findings: [] };
|
|
1718
|
+
const codes = sources.map((s) => _stripNonCode(s, { keepStrings: true }));
|
|
1719
|
+
const masks = sources.map((s) => _stripNonCode(s, { keepStrings: false }));
|
|
1720
|
+
const findings = [
|
|
1721
|
+
..._styleGroupFindings(manifest),
|
|
1722
|
+
..._styleCoverageFindings(manifest, codes),
|
|
1723
|
+
..._textLiteralFindings(_textLiteralOccurrences(codes, masks)),
|
|
1724
|
+
];
|
|
1725
|
+
// Every rule here is a warning by construction: the CLI must stay exit-0 for
|
|
1726
|
+
// a human author, while the agent blocks on any finding regardless.
|
|
1727
|
+
return { ok: true, findings };
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1474
1730
|
function lintSource(source, options) {
|
|
1475
1731
|
if (typeof source !== "string") {
|
|
1476
1732
|
return {
|
|
@@ -1553,6 +1809,7 @@ function lintSource(source, options) {
|
|
|
1553
1809
|
module.exports = {
|
|
1554
1810
|
lintSource,
|
|
1555
1811
|
lintStyleWiring,
|
|
1812
|
+
lintEditability,
|
|
1556
1813
|
bannedIdentifiers,
|
|
1557
1814
|
narrowManifestForFile,
|
|
1558
1815
|
};
|
package/dist/linter.js
CHANGED
|
@@ -1509,11 +1509,7 @@ export function lintStyleWiring(manifest, files) {
|
|
|
1509
1509
|
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
1510
1510
|
return empty;
|
|
1511
1511
|
}
|
|
1512
|
-
|
|
1513
|
-
if (typeof files === "string") sources = [files];
|
|
1514
|
-
else if (Array.isArray(files)) sources = files;
|
|
1515
|
-
else if (files && typeof files === "object") sources = Object.values(files);
|
|
1516
|
-
sources = sources.filter((s) => typeof s === "string" && s.length > 0);
|
|
1512
|
+
const sources = _bundleSources(files);
|
|
1517
1513
|
if (sources.length === 0) return empty;
|
|
1518
1514
|
const code = sources
|
|
1519
1515
|
.map((s) => _stripNonCode(s, { keepStrings: true }))
|
|
@@ -1542,6 +1538,266 @@ export function lintStyleWiring(manifest, files) {
|
|
|
1542
1538
|
return { ok: !hasErrors, findings };
|
|
1543
1539
|
}
|
|
1544
1540
|
|
|
1541
|
+
// sc-7799 — a group name that groups everything defeats the rule: one
|
|
1542
|
+
// catch-all fieldset reads exactly like the flat list it replaced.
|
|
1543
|
+
const _GENERIC_STYLE_GROUPS = new Set([
|
|
1544
|
+
"advanced",
|
|
1545
|
+
"other",
|
|
1546
|
+
"misc",
|
|
1547
|
+
"style",
|
|
1548
|
+
"styles",
|
|
1549
|
+
"general",
|
|
1550
|
+
]);
|
|
1551
|
+
|
|
1552
|
+
// The three the basic-style gate mandates; they restyle the widget as a whole.
|
|
1553
|
+
const _WHOLE_WIDGET_STYLE_FIELDS = new Set([
|
|
1554
|
+
"background",
|
|
1555
|
+
"textColor",
|
|
1556
|
+
"align",
|
|
1557
|
+
]);
|
|
1558
|
+
|
|
1559
|
+
// A second element an author would style apart from the text. Pressable and
|
|
1560
|
+
// Button are deliberately absent: a touch target wrapping the only label is
|
|
1561
|
+
// one element, not two.
|
|
1562
|
+
const _SECOND_ELEMENT_TAGS = ["Image", "TextInput", "FlatList", "SectionList"];
|
|
1563
|
+
|
|
1564
|
+
const _SEPARATOR_GLYPHS = new Set(["-", "–", "—", "•", "·", "…", "|", "/"]);
|
|
1565
|
+
|
|
1566
|
+
// Names ride the finding; the remainder is counted, so one finding stays
|
|
1567
|
+
// inside the agent's per-message budget however many fields are wrong.
|
|
1568
|
+
const _EDITABILITY_LIST_CAP = 3;
|
|
1569
|
+
|
|
1570
|
+
// Tighter for copy: each entry carries the text AND its line, so six of them
|
|
1571
|
+
// push the message past the report's 300-char truncation.
|
|
1572
|
+
const _LITERAL_LIST_CAP = 2;
|
|
1573
|
+
|
|
1574
|
+
// Shared by both bundle-level gates — one normaliser, not two.
|
|
1575
|
+
function _bundleSources(files) {
|
|
1576
|
+
let sources = [];
|
|
1577
|
+
if (typeof files === "string") sources = [files];
|
|
1578
|
+
else if (Array.isArray(files)) sources = files;
|
|
1579
|
+
else if (files && typeof files === "object") sources = Object.values(files);
|
|
1580
|
+
return sources.filter((s) => typeof s === "string" && s.length > 0);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function _plainObject(value) {
|
|
1584
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Both the count and each entry are bounded: the backend wraps the label in
|
|
1588
|
+
// `[manifest.editability] … (line N: snippet)` and truncates at 300, so an
|
|
1589
|
+
// unbounded name list loses the tail of the advice that follows it.
|
|
1590
|
+
function _clip(text, max) {
|
|
1591
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function _quoteList(names) {
|
|
1595
|
+
const shown = names.slice(0, _EDITABILITY_LIST_CAP).map((n) => _clip(n, 16));
|
|
1596
|
+
const more = names.length - shown.length;
|
|
1597
|
+
return shown.join(", ") + (more > 0 ? `, …(+${more} more)` : "");
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// Copy worth a control: a separator glyph or a bare number is chrome.
|
|
1601
|
+
function _isAuthoredCopy(text) {
|
|
1602
|
+
if (text.length < 2) return false;
|
|
1603
|
+
if (!/[A-Za-z]/.test(text)) return false;
|
|
1604
|
+
return !_SEPARATOR_GLYPHS.has(text);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// The `>` that CLOSES a JSX tag, skipping quoted attribute values and any `>`
|
|
1608
|
+
// inside a `{…}` expression — `onPress={() => x}` carries one, and treating it
|
|
1609
|
+
// as the tag end reports the handler body as copy.
|
|
1610
|
+
function _tagEnd(code, from) {
|
|
1611
|
+
let depth = 0;
|
|
1612
|
+
let quote = null;
|
|
1613
|
+
for (let i = from; i < code.length; i++) {
|
|
1614
|
+
const ch = code[i];
|
|
1615
|
+
if (quote) {
|
|
1616
|
+
if (ch === "\\") i++;
|
|
1617
|
+
else if (ch === quote) quote = null;
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
1621
|
+
else if (ch === "{") depth++;
|
|
1622
|
+
else if (ch === "}") depth--;
|
|
1623
|
+
else if (ch === ">" && depth <= 0) return i;
|
|
1624
|
+
}
|
|
1625
|
+
return -1;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// The balanced `{…}` starting at `from`, or null.
|
|
1629
|
+
function _bracedSpan(code, from) {
|
|
1630
|
+
if (code[from] !== "{") return null;
|
|
1631
|
+
let depth = 0;
|
|
1632
|
+
let quote = null;
|
|
1633
|
+
for (let i = from; i < code.length; i++) {
|
|
1634
|
+
const ch = code[i];
|
|
1635
|
+
if (quote) {
|
|
1636
|
+
if (ch === "\\") i++;
|
|
1637
|
+
else if (ch === quote) quote = null;
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
1641
|
+
else if (ch === "{") depth++;
|
|
1642
|
+
else if (ch === "}") {
|
|
1643
|
+
depth--;
|
|
1644
|
+
if (depth === 0) return code.slice(from + 1, i);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return null;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// Copy pinned in a `<Text>`: a bare literal child, or a braced child that is
|
|
1651
|
+
// NOTHING BUT a string literal (`{"Hello"}`) — the one-pair-of-braces evasion.
|
|
1652
|
+
// A braced expression carrying an identifier is left alone: it may already be
|
|
1653
|
+
// a prop, and under-firing is right for a gate that blocks a publish.
|
|
1654
|
+
function _textLiteralOccurrences(codes, masks) {
|
|
1655
|
+
const byText = new Map();
|
|
1656
|
+
for (let f = 0; f < codes.length; f++) {
|
|
1657
|
+
const code = codes[f];
|
|
1658
|
+
// Locate tags on the string-blanked mask, so markup HELD IN a string is
|
|
1659
|
+
// not read as markup. Both masks preserve offsets, so the child is read
|
|
1660
|
+
// back from the kept-strings source at the very same index.
|
|
1661
|
+
const mask = masks[f];
|
|
1662
|
+
const re = /<Text\b/g;
|
|
1663
|
+
let m;
|
|
1664
|
+
while ((m = re.exec(mask)) !== null) {
|
|
1665
|
+
const end = _tagEnd(mask, m.index);
|
|
1666
|
+
if (end === -1) continue;
|
|
1667
|
+
let i = end + 1;
|
|
1668
|
+
while (i < code.length && /\s/.test(code[i])) i++;
|
|
1669
|
+
let text = null;
|
|
1670
|
+
if (code[i] === "{") {
|
|
1671
|
+
const inner = _bracedSpan(code, i);
|
|
1672
|
+
const lit =
|
|
1673
|
+
inner && inner.trim().match(/^(["'`])((?:\\.|(?!\1).)*)\1$/);
|
|
1674
|
+
// A template carrying `${…}` interpolates a value, so it is not a literal.
|
|
1675
|
+
if (lit && !(lit[1] === "`" && lit[2].includes("${"))) text = lit[2];
|
|
1676
|
+
} else {
|
|
1677
|
+
const child = code.slice(i).match(/^([^<>{}\n]+)/);
|
|
1678
|
+
if (child) text = child[1];
|
|
1679
|
+
}
|
|
1680
|
+
if (text === null) continue;
|
|
1681
|
+
text = text.trim();
|
|
1682
|
+
if (!_isAuthoredCopy(text) || byText.has(text)) continue;
|
|
1683
|
+
// Split-impl carries the same copy twice; report the string, not the file.
|
|
1684
|
+
byText.set(text, code.slice(0, i).split("\n").length);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
return [...byText.entries()].map(([text, line]) => ({ text, line }));
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// Per FILE: joining widget.web.jsx and widget.native.jsx doubles every tag.
|
|
1691
|
+
// A second element must be a DISTINCT visual kind beside the text — two
|
|
1692
|
+
// `<Text>` tags are as often a loading branch and its content as a title and
|
|
1693
|
+
// a body, and a blocking gate may not guess.
|
|
1694
|
+
function _rendersSeveralElements(code) {
|
|
1695
|
+
if (!/<Text[\s>/]/.test(code)) return false;
|
|
1696
|
+
return _SECOND_ELEMENT_TAGS.some((tag) =>
|
|
1697
|
+
new RegExp(`<${tag}[\\s>/]`).test(code),
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function _styleGroupFindings(manifest) {
|
|
1702
|
+
const schema = _plainObject(manifest) ? manifest.styleSchema : null;
|
|
1703
|
+
if (!_plainObject(schema)) return [];
|
|
1704
|
+
const bad = Object.keys(schema).filter((field) => {
|
|
1705
|
+
const def = schema[field];
|
|
1706
|
+
const group = _plainObject(def) && _plainObject(def.ui) ? def.ui.group : "";
|
|
1707
|
+
if (typeof group !== "string" || group.trim().length === 0) return true;
|
|
1708
|
+
return _GENERIC_STYLE_GROUPS.has(group.trim().toLowerCase());
|
|
1709
|
+
});
|
|
1710
|
+
if (bad.length === 0) return [];
|
|
1711
|
+
return [
|
|
1712
|
+
{
|
|
1713
|
+
rule: "style-group-missing",
|
|
1714
|
+
severity: "warning",
|
|
1715
|
+
label:
|
|
1716
|
+
`styleSchema fields with no usable ui.group: ${_quoteList(
|
|
1717
|
+
bad.map((f) => `"${f}"`),
|
|
1718
|
+
)}. Group EVERY field — basics under "Basics", each per-element ` +
|
|
1719
|
+
`field under the element it styles. A catch-all name is not a group.`,
|
|
1720
|
+
line: 0,
|
|
1721
|
+
snippet: _clip(bad[0], 12),
|
|
1722
|
+
},
|
|
1723
|
+
];
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function _styleCoverageFindings(manifest, codes) {
|
|
1727
|
+
const schema = _plainObject(manifest) ? manifest.styleSchema : null;
|
|
1728
|
+
if (!_plainObject(schema)) return [];
|
|
1729
|
+
const fields = Object.keys(schema);
|
|
1730
|
+
if (fields.length === 0) return [];
|
|
1731
|
+
if (fields.some((f) => !_WHOLE_WIDGET_STYLE_FIELDS.has(f))) return [];
|
|
1732
|
+
if (!codes.some(_rendersSeveralElements)) return [];
|
|
1733
|
+
return [
|
|
1734
|
+
{
|
|
1735
|
+
rule: "style-elements-uncovered",
|
|
1736
|
+
severity: "warning",
|
|
1737
|
+
label:
|
|
1738
|
+
`The widget renders more than one visual element but its styleSchema ` +
|
|
1739
|
+
`stops at background/textColor/align. Add 2-4 grouped per-element ` +
|
|
1740
|
+
`fields (cardBackground, titleColor, rowBackground, imageRadius), ` +
|
|
1741
|
+
`each applied ONLY when set.`,
|
|
1742
|
+
line: 0,
|
|
1743
|
+
snippet: "",
|
|
1744
|
+
},
|
|
1745
|
+
];
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function _textLiteralFindings(occurrences) {
|
|
1749
|
+
if (occurrences.length === 0) return [];
|
|
1750
|
+
// Each entry is bounded so a long line of copy cannot push the whole
|
|
1751
|
+
// message past the report's truncation and take the line numbers with it.
|
|
1752
|
+
const listed = occurrences
|
|
1753
|
+
.slice(0, _LITERAL_LIST_CAP)
|
|
1754
|
+
.map((o) => `"${_clip(o.text, 16)}" (line ${o.line})`);
|
|
1755
|
+
const more = occurrences.length - listed.length;
|
|
1756
|
+
return [
|
|
1757
|
+
{
|
|
1758
|
+
rule: "text-literal-unexposed",
|
|
1759
|
+
severity: "warning",
|
|
1760
|
+
label:
|
|
1761
|
+
`Copy pinned in a <Text> reaches neither author nor translation: ` +
|
|
1762
|
+
`${listed.join(", ")}${more > 0 ? `, …(+${more} more)` : ""}. ` +
|
|
1763
|
+
`Declare each as a propertySchema string defaulting to that text ` +
|
|
1764
|
+
`and render the prop; fixed chrome uses t().`,
|
|
1765
|
+
line: occurrences[0].line,
|
|
1766
|
+
snippet: _clip(occurrences[0].text, 12),
|
|
1767
|
+
},
|
|
1768
|
+
];
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* sc-7799 — the editability gates: what an author can still change once the
|
|
1773
|
+
* widget ships. Bundle-level like `lintStyleWiring`, and for the same reason
|
|
1774
|
+
* — one manifest serves both halves of a split-impl widget.
|
|
1775
|
+
*
|
|
1776
|
+
* Each rule emits ONE consolidated finding, because the agent's draft-findings
|
|
1777
|
+
* report keeps only the first dozen checks and truncates each message: a
|
|
1778
|
+
* per-field finding on a 14-field manifest would evict the render-time crash
|
|
1779
|
+
* sitting behind it.
|
|
1780
|
+
*
|
|
1781
|
+
* @param {object} manifest widget manifest (`styleSchema` only)
|
|
1782
|
+
* @param {object|string[]|string} files bundle sources — a `{ name: source }`
|
|
1783
|
+
* map, an array of sources, or one source string
|
|
1784
|
+
* @returns {{ ok: boolean, findings: Array<{ rule: string, severity: string, label: string, line: number, snippet: string }> }}
|
|
1785
|
+
*/
|
|
1786
|
+
export function lintEditability(manifest, files) {
|
|
1787
|
+
const sources = _bundleSources(files);
|
|
1788
|
+
if (sources.length === 0) return { ok: true, findings: [] };
|
|
1789
|
+
const codes = sources.map((s) => _stripNonCode(s, { keepStrings: true }));
|
|
1790
|
+
const masks = sources.map((s) => _stripNonCode(s, { keepStrings: false }));
|
|
1791
|
+
const findings = [
|
|
1792
|
+
..._styleGroupFindings(manifest),
|
|
1793
|
+
..._styleCoverageFindings(manifest, codes),
|
|
1794
|
+
..._textLiteralFindings(_textLiteralOccurrences(codes, masks)),
|
|
1795
|
+
];
|
|
1796
|
+
// Every rule here is a warning by construction: the CLI must stay exit-0 for
|
|
1797
|
+
// a human author, while the agent blocks on any finding regardless.
|
|
1798
|
+
return { ok: true, findings };
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1545
1801
|
export function lintSource(source, options) {
|
|
1546
1802
|
if (typeof source !== "string") {
|
|
1547
1803
|
return {
|
package/dist/navigation.cjs
CHANGED
|
@@ -59,10 +59,8 @@ function normaliseNavigation(navigation) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* How many MENU pages the chrome may draw at once, or `null` for no cap.
|
|
63
|
-
*
|
|
64
|
-
* Where the chrome IS the menu (`bottom-tabs`) this cap must never drop a page:
|
|
65
|
-
* the surplus moves behind a More sheet. Contrast `quickBarCap` below.
|
|
62
|
+
* How many MENU pages the chrome may draw at once, or `null` for no cap. The
|
|
63
|
+
* sidebar has none. Contrast `quickBarCap` below.
|
|
66
64
|
*
|
|
67
65
|
* @param {string} menuType — a resolved menu type.
|
|
68
66
|
* @returns {number|null}
|
|
@@ -90,13 +88,17 @@ function quickBarCap(menuType) {
|
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
// The strip's surface was hard-coded white on BOTH hosts and its items read the
|
|
93
|
-
// sidebar's tokens
|
|
94
|
-
//
|
|
95
|
-
//
|
|
91
|
+
// sidebar's tokens. `footer` gives the quick bar tokens of its own.
|
|
92
|
+
//
|
|
93
|
+
// Every colour falls back to the sidebar's, so a workspace that never opens the
|
|
94
|
+
// panel keeps exactly the appearance it has today; only an explicit `footer`
|
|
95
|
+
// value moves anything.
|
|
96
96
|
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
97
|
+
// sc-7695: this block is the BOTTOM EDGE's, not the quick bar's alone. A page's
|
|
98
|
+
// own bottom bar (REQ-NAV-LOCAL) is the same strip worn by one page and reads
|
|
99
|
+
// exactly these values — the Design panel that edits them is named for that
|
|
100
|
+
// bar, since the quick bar is on its way out (sc-7753) and the look outlives
|
|
101
|
+
// it. No key is the bar's alone: two strips, one look.
|
|
100
102
|
function resolveFooterTokens(theme) {
|
|
101
103
|
const config = isPlainObject(theme) ? theme : {};
|
|
102
104
|
const footer = isPlainObject(config.footer) ? config.footer : {};
|
|
@@ -176,11 +178,6 @@ function pageColor(config, alphaRule) {
|
|
|
176
178
|
);
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
// An `attached` tab: opaque, or the bar shows through the page it joins.
|
|
180
|
-
function pageSurface(config) {
|
|
181
|
-
return pageColor(config, opaqueOr);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
181
|
// sc-6596: what an UNSET rail or bar is painted. Read at full strength — an
|
|
185
182
|
// alpha on the bottom layer would mean nothing to chrome above it.
|
|
186
183
|
function chromeSurface(config) {
|
|
@@ -223,12 +220,6 @@ function tabIndicatorOr(value) {
|
|
|
223
220
|
return Math.min(Math.max(Math.round(value), 0), TAB_INDICATOR_MAX);
|
|
224
221
|
}
|
|
225
222
|
|
|
226
|
-
function opaqueOr(value) {
|
|
227
|
-
return isHexColor(value) && value.length !== 9 && value.length !== 5
|
|
228
|
-
? value
|
|
229
|
-
: null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
223
|
// A gradient stop as the page readers admit one: flat hex, never alpha.
|
|
233
224
|
function flatHexOrNull(value) {
|
|
234
225
|
return isHexColor(value) && value.length !== 9 ? value : null;
|
|
@@ -330,18 +321,15 @@ function resolveTopBarTokens(theme) {
|
|
|
330
321
|
// surface and meets the page beneath it, so the two read as one plane. Meaningless on a shape that draws no tab row — a host
|
|
331
322
|
// reads it only where it has tabs to draw.
|
|
332
323
|
tabStyle: topBar.tabStyle === "attached" ? "attached" : "underline",
|
|
333
|
-
// The surface an `attached`
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
contentSurface:
|
|
342
|
-
opaqueOr(topBar.tabActiveBackgroundColor),
|
|
343
|
-
pageSurface(config),
|
|
344
|
-
),
|
|
324
|
+
// The surface the PANEL behind an `attached` row paints — the authored
|
|
325
|
+
// active colour, alpha and all, or nothing. Under `attached` the current
|
|
326
|
+
// tab itself paints NOTHING: it is a hole in the bar, and what shows
|
|
327
|
+
// through it is this panel, or the page's own wash when this is null. So
|
|
328
|
+
// the tab is exactly as see-through as the page, a gradient stays one
|
|
329
|
+
// gradient, and a translucent page never doubles up under a tab (sc-7352 —
|
|
330
|
+
// painting the tab a copy of the page could never do any of that). One
|
|
331
|
+
// value, resolved HERE, so both hosts paint the panel the same colour.
|
|
332
|
+
contentSurface: hexOrNull(topBar.tabActiveBackgroundColor),
|
|
345
333
|
// REQ-NAV-STRUCTURE: the underline that marks the current tab, in px. Zero
|
|
346
334
|
// turns it OFF and leaves the tab marked by its label colour alone, which
|
|
347
335
|
// is a real choice rather than an unstyled state — so unlike the chrome
|
|
@@ -360,9 +348,9 @@ function resolveTopBarTokens(theme) {
|
|
|
360
348
|
// and the bar shows through it, which is what the row did before the keys
|
|
361
349
|
// existed — so an app that never set them is unchanged.
|
|
362
350
|
tabBackgroundColor: hexOrNull(topBar.tabBackgroundColor),
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
351
|
+
// Under `underline` the current tab's own surface, or null for none. Under
|
|
352
|
+
// `attached` the tab paints nothing and this colour is the PANEL's — see
|
|
353
|
+
// `contentSurface` above.
|
|
366
354
|
tabActiveBackgroundColor: hexOrNull(topBar.tabActiveBackgroundColor),
|
|
367
355
|
};
|
|
368
356
|
}
|
package/dist/navigation.js
CHANGED
|
@@ -50,10 +50,8 @@ export function normaliseNavigation(navigation) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* How many MENU pages the chrome may draw at once, or `null` for no cap.
|
|
54
|
-
*
|
|
55
|
-
* Where the chrome IS the menu (`bottom-tabs`) this cap must never drop a page:
|
|
56
|
-
* the surplus moves behind a More sheet. Contrast `quickBarCap` below.
|
|
53
|
+
* How many MENU pages the chrome may draw at once, or `null` for no cap. The
|
|
54
|
+
* sidebar has none. Contrast `quickBarCap` below.
|
|
57
55
|
*
|
|
58
56
|
* @param {string} menuType — a resolved menu type.
|
|
59
57
|
* @returns {number|null}
|
|
@@ -81,13 +79,17 @@ export function quickBarCap(menuType) {
|
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
// The strip's surface was hard-coded white on BOTH hosts and its items read the
|
|
84
|
-
// sidebar's tokens
|
|
85
|
-
//
|
|
86
|
-
//
|
|
82
|
+
// sidebar's tokens. `footer` gives the quick bar tokens of its own.
|
|
83
|
+
//
|
|
84
|
+
// Every colour falls back to the sidebar's, so a workspace that never opens the
|
|
85
|
+
// panel keeps exactly the appearance it has today; only an explicit `footer`
|
|
86
|
+
// value moves anything.
|
|
87
87
|
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
88
|
+
// sc-7695: this block is the BOTTOM EDGE's, not the quick bar's alone. A page's
|
|
89
|
+
// own bottom bar (REQ-NAV-LOCAL) is the same strip worn by one page and reads
|
|
90
|
+
// exactly these values — the Design panel that edits them is named for that
|
|
91
|
+
// bar, since the quick bar is on its way out (sc-7753) and the look outlives
|
|
92
|
+
// it. No key is the bar's alone: two strips, one look.
|
|
91
93
|
export function resolveFooterTokens(theme) {
|
|
92
94
|
const config = isPlainObject(theme) ? theme : {};
|
|
93
95
|
const footer = isPlainObject(config.footer) ? config.footer : {};
|
|
@@ -167,11 +169,6 @@ function pageColor(config, alphaRule) {
|
|
|
167
169
|
);
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
// An `attached` tab: opaque, or the bar shows through the page it joins.
|
|
171
|
-
function pageSurface(config) {
|
|
172
|
-
return pageColor(config, opaqueOr);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
172
|
// sc-6596: what an UNSET rail or bar is painted. Read at full strength — an
|
|
176
173
|
// alpha on the bottom layer would mean nothing to chrome above it.
|
|
177
174
|
function chromeSurface(config) {
|
|
@@ -214,12 +211,6 @@ function tabIndicatorOr(value) {
|
|
|
214
211
|
return Math.min(Math.max(Math.round(value), 0), TAB_INDICATOR_MAX);
|
|
215
212
|
}
|
|
216
213
|
|
|
217
|
-
function opaqueOr(value) {
|
|
218
|
-
return isHexColor(value) && value.length !== 9 && value.length !== 5
|
|
219
|
-
? value
|
|
220
|
-
: null;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
214
|
// A gradient stop as the page readers admit one: flat hex, never alpha.
|
|
224
215
|
function flatHexOrNull(value) {
|
|
225
216
|
return isHexColor(value) && value.length !== 9 ? value : null;
|
|
@@ -321,18 +312,15 @@ export function resolveTopBarTokens(theme) {
|
|
|
321
312
|
// surface and meets the page beneath it, so the two read as one plane. Meaningless on a shape that draws no tab row — a host
|
|
322
313
|
// reads it only where it has tabs to draw.
|
|
323
314
|
tabStyle: topBar.tabStyle === "attached" ? "attached" : "underline",
|
|
324
|
-
// The surface an `attached`
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
contentSurface:
|
|
333
|
-
opaqueOr(topBar.tabActiveBackgroundColor),
|
|
334
|
-
pageSurface(config),
|
|
335
|
-
),
|
|
315
|
+
// The surface the PANEL behind an `attached` row paints — the authored
|
|
316
|
+
// active colour, alpha and all, or nothing. Under `attached` the current
|
|
317
|
+
// tab itself paints NOTHING: it is a hole in the bar, and what shows
|
|
318
|
+
// through it is this panel, or the page's own wash when this is null. So
|
|
319
|
+
// the tab is exactly as see-through as the page, a gradient stays one
|
|
320
|
+
// gradient, and a translucent page never doubles up under a tab (sc-7352 —
|
|
321
|
+
// painting the tab a copy of the page could never do any of that). One
|
|
322
|
+
// value, resolved HERE, so both hosts paint the panel the same colour.
|
|
323
|
+
contentSurface: hexOrNull(topBar.tabActiveBackgroundColor),
|
|
336
324
|
// REQ-NAV-STRUCTURE: the underline that marks the current tab, in px. Zero
|
|
337
325
|
// turns it OFF and leaves the tab marked by its label colour alone, which
|
|
338
326
|
// is a real choice rather than an unstyled state — so unlike the chrome
|
|
@@ -351,9 +339,9 @@ export function resolveTopBarTokens(theme) {
|
|
|
351
339
|
// and the bar shows through it, which is what the row did before the keys
|
|
352
340
|
// existed — so an app that never set them is unchanged.
|
|
353
341
|
tabBackgroundColor: hexOrNull(topBar.tabBackgroundColor),
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
342
|
+
// Under `underline` the current tab's own surface, or null for none. Under
|
|
343
|
+
// `attached` the tab paints nothing and this colour is the PANEL's — see
|
|
344
|
+
// `contentSurface` above.
|
|
357
345
|
tabActiveBackgroundColor: hexOrNull(topBar.tabActiveBackgroundColor),
|
|
358
346
|
};
|
|
359
347
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.141.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "node scripts/build.js",
|
|
52
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/theme-token-refs.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.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-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-flex-basis-percent.test.js src/__tests__/linter-hardcoded-design.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.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__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-barcode.test.js src/__tests__/hooks-image-editor.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js src/__tests__/style-group.test.js"
|
|
52
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/theme-token-refs.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.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-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-flex-basis-percent.test.js src/__tests__/linter-hardcoded-design.test.js src/__tests__/linter-editability.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.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__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-barcode.test.js src/__tests__/hooks-image-editor.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js src/__tests__/style-group.test.js"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
55
|
"node": ">=18"
|