@colixsystems/widget-sdk 0.103.0 → 0.105.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 +23 -1
- package/dist/contract.cjs +16 -1
- package/dist/contract.js +16 -1
- package/dist/corner-radius.js +95 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +8 -0
- package/dist/index.native.js +8 -0
- package/dist/linter.cjs +120 -34
- package/dist/linter.js +122 -35
- package/dist/property-schema.js +40 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -68,7 +68,29 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
68
68
|
|
|
69
69
|
## Status
|
|
70
70
|
|
|
71
|
-
`v0.
|
|
71
|
+
`v0.105.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
|
+
|
|
73
|
+
### What's new in 0.105.0 (contract unchanged)
|
|
74
|
+
|
|
75
|
+
**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.
|
|
76
|
+
|
|
77
|
+
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).
|
|
78
|
+
### What's new in 0.104.0 (contract 1.79.0)
|
|
79
|
+
|
|
80
|
+
**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.
|
|
81
|
+
|
|
82
|
+
Declare `{ type: "cornerRadius", label: "Corner radius", validation: { min: 0, max: 48 } }` in your `propertySchema` or `styleSchema`. The Studio renders a slider with a typeable number that sets all four corners, plus a disclosure for setting each one. The authored value is `number | { topLeft, topRight, bottomRight, bottomLeft }` — the scalar form is unchanged, so every value stored before is still valid.
|
|
83
|
+
|
|
84
|
+
Resolve it with the two new exports rather than reading the raw value, because they hide the shape and spell the style props the way BOTH hosts accept:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
import { normaliseCornerRadius, cornerRadiusStyle } from "@colixsystems/widget-sdk";
|
|
88
|
+
|
|
89
|
+
const radius = normaliseCornerRadius(style.radius, 0, 48);
|
|
90
|
+
return <View style={[styles.card, cornerRadiusStyle(radius)]} />;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`normaliseCornerRadius(value, fallback, max)` returns all four corners resolved and clamped; `cornerRadiusStyle(radius, format)` emits the `borderRadius` shorthand when they agree and the four long-hand props when they differ (pass `n => `+"`${n}px`"+`` for the DOM). `isUniformCornerRadius` and `hasCornerRadius` round out the set. `CONTRACT.version` → `1.79.0`. Additive: every value accepted before is accepted now.
|
|
72
94
|
|
|
73
95
|
### What's new in 0.103.0 (contract 1.78.0)
|
|
74
96
|
|
package/dist/contract.cjs
CHANGED
|
@@ -239,6 +239,15 @@ const FORM_SUBMIT_FIELDS = Object.freeze({
|
|
|
239
239
|
gradient: "submitGradient",
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
+
// The Link card's action button -- its own `button*` names, kept distinct from
|
|
243
|
+
// the card surface the button sits on.
|
|
244
|
+
const LINK_ACTION_FIELDS = Object.freeze({
|
|
245
|
+
background: "buttonBackground",
|
|
246
|
+
textColor: "buttonTextColor",
|
|
247
|
+
gradient: "buttonGradient",
|
|
248
|
+
radius: "buttonRadius",
|
|
249
|
+
});
|
|
250
|
+
|
|
242
251
|
// REQ-THEME-WIDGET: the card fields whose NAMES are unambiguous, so they bind to
|
|
243
252
|
// ANY widget that reads them -- including a Mason-generated one, whose id can
|
|
244
253
|
// never appear in a hand-maintained allowlist. That allowlist is why "make the
|
|
@@ -308,6 +317,8 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
308
317
|
}),
|
|
309
318
|
"appstudio.form-input": FORM_SUBMIT_FIELDS,
|
|
310
319
|
"appstudio.form-builder": FORM_SUBMIT_FIELDS,
|
|
320
|
+
// sc-5757: the Link card's action button, so the button scope reaches it.
|
|
321
|
+
"appstudio.link": LINK_ACTION_FIELDS,
|
|
311
322
|
}),
|
|
312
323
|
}),
|
|
313
324
|
card: Object.freeze({
|
|
@@ -331,6 +342,7 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
331
342
|
"appstudio.form-input": CARD_SURFACE_FIELDS,
|
|
332
343
|
"appstudio.form-builder": CARD_SURFACE_FIELDS,
|
|
333
344
|
"appstudio.user-management": CARD_SURFACE_FIELDS,
|
|
345
|
+
"appstudio.link": CARD_SURFACE_FIELDS,
|
|
334
346
|
}),
|
|
335
347
|
}),
|
|
336
348
|
text: Object.freeze({
|
|
@@ -3275,6 +3287,9 @@ const CONTRACT = deepFreeze({
|
|
|
3275
3287
|
// PAGE_ROUTES maps both to the screen name), but the contract said
|
|
3276
3288
|
// `pageId` only, so data-driven widgets — whose rows carry slugs —
|
|
3277
3289
|
// invented params patterns that navigate nowhere. No code changed shape.
|
|
3290
|
+
// 1.79.0: additive (sc-5890) — the `cornerRadius` property type: a radius
|
|
3291
|
+
// value is `number | { topLeft, topRight, bottomRight, bottomLeft }`, resolved
|
|
3292
|
+
// by `normaliseCornerRadius` and emitted by `cornerRadiusStyle`.
|
|
3278
3293
|
// 1.78.0: additive (sc-5646) — `widgetStyles` carries every value shape a
|
|
3279
3294
|
// `styleSchema` field can produce, not just scalars and gradients. The
|
|
3280
3295
|
// Studio offers the SAME field in the widget editor and in Theme
|
|
@@ -3286,7 +3301,7 @@ const CONTRACT = deepFreeze({
|
|
|
3286
3301
|
// per-scalar limits never stated, so the worst-case unauthenticated
|
|
3287
3302
|
// payload is SMALLER than before. `__proto__`-style keys are refused at
|
|
3288
3303
|
// every level. Additive: every value accepted before is accepted now.
|
|
3289
|
-
version: "1.
|
|
3304
|
+
version: "1.79.0",
|
|
3290
3305
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3291
3306
|
hooks: HOOKS,
|
|
3292
3307
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -239,6 +239,15 @@ const FORM_SUBMIT_FIELDS = Object.freeze({
|
|
|
239
239
|
gradient: "submitGradient",
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
+
// The Link card's action button -- its own `button*` names, kept distinct from
|
|
243
|
+
// the card surface the button sits on.
|
|
244
|
+
const LINK_ACTION_FIELDS = Object.freeze({
|
|
245
|
+
background: "buttonBackground",
|
|
246
|
+
textColor: "buttonTextColor",
|
|
247
|
+
gradient: "buttonGradient",
|
|
248
|
+
radius: "buttonRadius",
|
|
249
|
+
});
|
|
250
|
+
|
|
242
251
|
// REQ-THEME-WIDGET: the card fields whose NAMES are unambiguous, so they bind to
|
|
243
252
|
// ANY widget that reads them -- including a Mason-generated one, whose id can
|
|
244
253
|
// never appear in a hand-maintained allowlist. That allowlist is why "make the
|
|
@@ -308,6 +317,8 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
308
317
|
}),
|
|
309
318
|
"appstudio.form-input": FORM_SUBMIT_FIELDS,
|
|
310
319
|
"appstudio.form-builder": FORM_SUBMIT_FIELDS,
|
|
320
|
+
// sc-5757: the Link card's action button, so the button scope reaches it.
|
|
321
|
+
"appstudio.link": LINK_ACTION_FIELDS,
|
|
311
322
|
}),
|
|
312
323
|
}),
|
|
313
324
|
card: Object.freeze({
|
|
@@ -331,6 +342,7 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
331
342
|
"appstudio.form-input": CARD_SURFACE_FIELDS,
|
|
332
343
|
"appstudio.form-builder": CARD_SURFACE_FIELDS,
|
|
333
344
|
"appstudio.user-management": CARD_SURFACE_FIELDS,
|
|
345
|
+
"appstudio.link": CARD_SURFACE_FIELDS,
|
|
334
346
|
}),
|
|
335
347
|
}),
|
|
336
348
|
text: Object.freeze({
|
|
@@ -3275,6 +3287,9 @@ const CONTRACT = deepFreeze({
|
|
|
3275
3287
|
// PAGE_ROUTES maps both to the screen name), but the contract said
|
|
3276
3288
|
// `pageId` only, so data-driven widgets — whose rows carry slugs —
|
|
3277
3289
|
// invented params patterns that navigate nowhere. No code changed shape.
|
|
3290
|
+
// 1.79.0: additive (sc-5890) — the `cornerRadius` property type: a radius
|
|
3291
|
+
// value is `number | { topLeft, topRight, bottomRight, bottomLeft }`, resolved
|
|
3292
|
+
// by `normaliseCornerRadius` and emitted by `cornerRadiusStyle`.
|
|
3278
3293
|
// 1.78.0: additive (sc-5646) — `widgetStyles` carries every value shape a
|
|
3279
3294
|
// `styleSchema` field can produce, not just scalars and gradients. The
|
|
3280
3295
|
// Studio offers the SAME field in the widget editor and in Theme
|
|
@@ -3286,7 +3301,7 @@ const CONTRACT = deepFreeze({
|
|
|
3286
3301
|
// per-scalar limits never stated, so the worst-case unauthenticated
|
|
3287
3302
|
// payload is SMALLER than before. `__proto__`-style keys are refused at
|
|
3288
3303
|
// every level. Additive: every value accepted before is accepted now.
|
|
3289
|
-
version: "1.
|
|
3304
|
+
version: "1.79.0",
|
|
3290
3305
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3291
3306
|
hooks: HOOKS,
|
|
3292
3307
|
primitives: PRIMITIVES,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// REQ-LAY-16 (sc-5890): the per-corner radius vocabulary. ONE normaliser and
|
|
2
|
+
// ONE style emitter for the web Player, the Builder canvas, the exported Expo
|
|
3
|
+
// app, and any custom widget declaring a `cornerRadius` field — so the four can
|
|
4
|
+
// never disagree about what a radius value means.
|
|
5
|
+
//
|
|
6
|
+
// The authored value is `number | { topLeft, topRight, bottomRight, bottomLeft }`.
|
|
7
|
+
// The scalar form is the one sc-3797 shipped and is still what most styles hold,
|
|
8
|
+
// so it stays first-class rather than being migrated away: widening the shape
|
|
9
|
+
// beats adding a second key beside it (CLAUDE.md §3).
|
|
10
|
+
//
|
|
11
|
+
// Both hosts spell the long-hand props identically (`borderTopLeftRadius` &co in
|
|
12
|
+
// React inline style AND in React Native), which is why one emitter serves both.
|
|
13
|
+
|
|
14
|
+
export const CORNER_RADIUS_KEYS = Object.freeze([
|
|
15
|
+
"topLeft",
|
|
16
|
+
"topRight",
|
|
17
|
+
"bottomRight",
|
|
18
|
+
"bottomLeft",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const LONGHAND_PROP = Object.freeze({
|
|
22
|
+
topLeft: "borderTopLeftRadius",
|
|
23
|
+
topRight: "borderTopRightRadius",
|
|
24
|
+
bottomRight: "borderBottomRightRadius",
|
|
25
|
+
bottomLeft: "borderBottomLeftRadius",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function clampCorner(value, fallback, max) {
|
|
29
|
+
// Unset (undefined/null/"") falls back; an explicit 0 is honoured — that
|
|
30
|
+
// distinction is what makes "square just this corner" expressible.
|
|
31
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
32
|
+
const n = Number(value);
|
|
33
|
+
if (!Number.isFinite(n)) return fallback;
|
|
34
|
+
return Math.min(Math.max(Math.round(n), 0), max);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve an authored radius to its four corners.
|
|
39
|
+
*
|
|
40
|
+
* @param {number|object|null|undefined} value the authored `number | {corners}`
|
|
41
|
+
* @param {number} [fallback] the value each unset corner takes
|
|
42
|
+
* @param {number} [max] the upper clamp, matching the field's declared max
|
|
43
|
+
* @returns {{topLeft:number, topRight:number, bottomRight:number, bottomLeft:number}}
|
|
44
|
+
*/
|
|
45
|
+
export function normaliseCornerRadius(value, fallback = 0, max = 48) {
|
|
46
|
+
const base = clampCorner(
|
|
47
|
+
typeof value === "number" || typeof value === "string" ? value : undefined,
|
|
48
|
+
clampCorner(fallback, 0, max),
|
|
49
|
+
max,
|
|
50
|
+
);
|
|
51
|
+
const corners = value && typeof value === "object" ? value : null;
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const key of CORNER_RADIUS_KEYS) {
|
|
54
|
+
out[key] = clampCorner(corners ? corners[key] : undefined, base, max);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True when all four corners resolve to the same number. */
|
|
60
|
+
export function isUniformCornerRadius(radius) {
|
|
61
|
+
if (!radius) return true;
|
|
62
|
+
const { topLeft } = radius;
|
|
63
|
+
return CORNER_RADIUS_KEYS.every((key) => radius[key] === topLeft);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The style props for a resolved radius, in the spelling BOTH hosts accept.
|
|
68
|
+
* A uniform radius emits the `borderRadius` shorthand so an untouched style is
|
|
69
|
+
* byte-identical to what sc-3797 produced; only a genuinely mixed radius pays
|
|
70
|
+
* for the four long-hand props. A radius of all-zero emits nothing at all.
|
|
71
|
+
*
|
|
72
|
+
* @param {object|null} radius output of {@link normaliseCornerRadius}
|
|
73
|
+
* @param {(n: number) => any} [format] wraps each number — the DOM needs "12px"
|
|
74
|
+
* @returns {object|null}
|
|
75
|
+
*/
|
|
76
|
+
export function cornerRadiusStyle(radius, format) {
|
|
77
|
+
if (!radius) return null;
|
|
78
|
+
const wrap = format || ((n) => n);
|
|
79
|
+
if (isUniformCornerRadius(radius)) {
|
|
80
|
+
return radius.topLeft > 0 ? { borderRadius: wrap(radius.topLeft) } : null;
|
|
81
|
+
}
|
|
82
|
+
const out = {};
|
|
83
|
+
for (const key of CORNER_RADIUS_KEYS) {
|
|
84
|
+
out[LONGHAND_PROP[key]] = wrap(radius[key]);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* True when the authored value asks for anything rounded. Callers use it to
|
|
91
|
+
* decide whether a background layer needs clipping at all.
|
|
92
|
+
*/
|
|
93
|
+
export function hasCornerRadius(radius) {
|
|
94
|
+
return Boolean(radius) && CORNER_RADIUS_KEYS.some((key) => radius[key] > 0);
|
|
95
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export type WidgetPropertyType =
|
|
|
53
53
|
// label, required, optionsSource, inlineOptions, optionsTableId,
|
|
54
54
|
// optionsValueColumn, optionsLabelColumn }.
|
|
55
55
|
| "fieldList"
|
|
56
|
+
// REQ-LAY-16 (sc-5890): corner-radius picker. Value is
|
|
57
|
+
// `number | { topLeft, topRight, bottomRight, bottomLeft }` — a scalar rounds
|
|
58
|
+
// all four corners, the object rounds each independently.
|
|
59
|
+
| "cornerRadius"
|
|
56
60
|
| "expression"
|
|
57
61
|
| "eventBinding"
|
|
58
62
|
| "object"
|
|
@@ -692,6 +696,44 @@ export function validateProps<T = Record<string, unknown>>(
|
|
|
692
696
|
props: unknown,
|
|
693
697
|
): { ok: true; value: T } | { ok: false; errors: string[] };
|
|
694
698
|
|
|
699
|
+
/**
|
|
700
|
+
* REQ-LAY-16 (sc-5890): the per-corner radius vocabulary. A `cornerRadius`
|
|
701
|
+
* field's authored value is a scalar or a per-corner object; resolve it with
|
|
702
|
+
* `normaliseCornerRadius` and turn it into style props with `cornerRadiusStyle`
|
|
703
|
+
* — both hosts spell the long-hand props identically, so one call serves the
|
|
704
|
+
* web Player and the Expo export.
|
|
705
|
+
*/
|
|
706
|
+
export type CornerRadiusKey =
|
|
707
|
+
| "topLeft"
|
|
708
|
+
| "topRight"
|
|
709
|
+
| "bottomRight"
|
|
710
|
+
| "bottomLeft";
|
|
711
|
+
|
|
712
|
+
export type CornerRadiusValue = number | Partial<Record<CornerRadiusKey, number>>;
|
|
713
|
+
|
|
714
|
+
export type ResolvedCornerRadius = Record<CornerRadiusKey, number>;
|
|
715
|
+
|
|
716
|
+
export const CORNER_RADIUS_KEYS: readonly CornerRadiusKey[];
|
|
717
|
+
|
|
718
|
+
export function normaliseCornerRadius(
|
|
719
|
+
value: CornerRadiusValue | null | undefined,
|
|
720
|
+
fallback?: number,
|
|
721
|
+
max?: number,
|
|
722
|
+
): ResolvedCornerRadius;
|
|
723
|
+
|
|
724
|
+
export function isUniformCornerRadius(
|
|
725
|
+
radius: ResolvedCornerRadius | null | undefined,
|
|
726
|
+
): boolean;
|
|
727
|
+
|
|
728
|
+
export function cornerRadiusStyle<T = number>(
|
|
729
|
+
radius: ResolvedCornerRadius | null | undefined,
|
|
730
|
+
format?: (n: number) => T,
|
|
731
|
+
): Record<string, T> | null;
|
|
732
|
+
|
|
733
|
+
export function hasCornerRadius(
|
|
734
|
+
radius: ResolvedCornerRadius | null | undefined,
|
|
735
|
+
): boolean;
|
|
736
|
+
|
|
695
737
|
export interface Query {
|
|
696
738
|
filter?: Record<string, unknown>;
|
|
697
739
|
sort?: Array<{ field: string; dir: "asc" | "desc" }>;
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
export { defineWidget } from "./define-widget.js";
|
|
6
6
|
export { validateManifest, canonicalCategory } from "./manifest.js";
|
|
7
7
|
export { validatePropertySchema, validateProps } from "./property-schema.js";
|
|
8
|
+
// REQ-LAY-16 (sc-5890): per-corner radius — one normaliser/emitter for both hosts.
|
|
9
|
+
export {
|
|
10
|
+
CORNER_RADIUS_KEYS,
|
|
11
|
+
normaliseCornerRadius,
|
|
12
|
+
isUniformCornerRadius,
|
|
13
|
+
cornerRadiusStyle,
|
|
14
|
+
hasCornerRadius,
|
|
15
|
+
} from "./corner-radius.js";
|
|
8
16
|
export {
|
|
9
17
|
WidgetContextProvider,
|
|
10
18
|
DatastoreError,
|
package/dist/index.native.js
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
export { defineWidget } from "./define-widget.js";
|
|
6
6
|
export { validateManifest, canonicalCategory } from "./manifest.js";
|
|
7
7
|
export { validatePropertySchema, validateProps } from "./property-schema.js";
|
|
8
|
+
// REQ-LAY-16 (sc-5890): per-corner radius — one normaliser/emitter for both hosts.
|
|
9
|
+
export {
|
|
10
|
+
CORNER_RADIUS_KEYS,
|
|
11
|
+
normaliseCornerRadius,
|
|
12
|
+
isUniformCornerRadius,
|
|
13
|
+
cornerRadiusStyle,
|
|
14
|
+
hasCornerRadius,
|
|
15
|
+
} from "./corner-radius.js";
|
|
8
16
|
export {
|
|
9
17
|
WidgetContextProvider,
|
|
10
18
|
DatastoreError,
|
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/dist/property-schema.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Property schema validation per docs/architecture/widget-marketplace.md §2.2.
|
|
2
2
|
// Drives the schema-driven Properties Panel and validates persisted page JSON.
|
|
3
3
|
|
|
4
|
+
import { CORNER_RADIUS_KEYS } from "./corner-radius.js";
|
|
5
|
+
|
|
4
6
|
const VALID_TYPES = new Set([
|
|
5
7
|
"string", "number", "boolean",
|
|
6
8
|
"color", "icon", "image",
|
|
@@ -47,6 +49,14 @@ const VALID_TYPES = new Set([
|
|
|
47
49
|
// NOT copied, so tenant-copy leaves the id as-is (it resolves to the space
|
|
48
50
|
// root if absent in the target).
|
|
49
51
|
"folderRef",
|
|
52
|
+
// REQ-LAY-16 (sc-5890): `cornerRadius` is a corner-radius picker. Its value
|
|
53
|
+
// is `number | { topLeft, topRight, bottomRight, bottomLeft }` — a scalar
|
|
54
|
+
// rounds all four, the object rounds each independently. The Studio renders
|
|
55
|
+
// a slider + typeable number with a per-corner disclosure; a widget turns
|
|
56
|
+
// the value into style props with `cornerRadiusStyle(normaliseCornerRadius(v))`,
|
|
57
|
+
// which both hosts spell identically. Plain numbers, so tenant-copy needs
|
|
58
|
+
// no remap.
|
|
59
|
+
"cornerRadius",
|
|
50
60
|
"expression", "eventBinding",
|
|
51
61
|
"object", "array",
|
|
52
62
|
]);
|
|
@@ -152,6 +162,36 @@ function coerceLeaf(def, value, path, errors) {
|
|
|
152
162
|
case "boolean":
|
|
153
163
|
if (typeof value !== "boolean") errors.push(`${path}: expected boolean`);
|
|
154
164
|
return value;
|
|
165
|
+
case "cornerRadius": {
|
|
166
|
+
// REQ-LAY-16: a scalar rounds all four corners; an object rounds each.
|
|
167
|
+
// Every corner is optional so a half-set object stays valid while the
|
|
168
|
+
// author is still adjusting — an unset corner falls back to the scalar.
|
|
169
|
+
const { min = 0, max } = def.validation || {};
|
|
170
|
+
const checkCorner = (n, at) => {
|
|
171
|
+
if (typeof n !== "number" || Number.isNaN(n)) {
|
|
172
|
+
errors.push(`${at}: expected number`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (n < min) errors.push(`${at}: must be >= ${min}`);
|
|
176
|
+
if (max !== undefined && n > max) errors.push(`${at}: must be <= ${max}`);
|
|
177
|
+
};
|
|
178
|
+
if (typeof value === "number") {
|
|
179
|
+
checkCorner(value, path);
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
if (!isPlainObject(value)) {
|
|
183
|
+
errors.push(`${path}: expected number or per-corner object`);
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
for (const [k, n] of Object.entries(value)) {
|
|
187
|
+
if (!CORNER_RADIUS_KEYS.includes(k)) {
|
|
188
|
+
errors.push(`${path}.${k}: unknown corner`);
|
|
189
|
+
} else if (n !== undefined && n !== null) {
|
|
190
|
+
checkCorner(n, `${path}.${k}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
155
195
|
case "select":
|
|
156
196
|
if (Array.isArray(def.enum) && !def.enum.some((e) => e.value === value)) {
|
|
157
197
|
errors.push(`${path}: value not in enum`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.105.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-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-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__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/toast-host.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-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.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-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-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__/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__/toast-host.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-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|