@colixsystems/widget-sdk 0.3.0 → 0.6.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 +40 -4
- package/dist/_theme-tokens.js +10 -0
- package/dist/contract.cjs +433 -0
- package/dist/contract.js +425 -0
- package/dist/hooks.js +259 -23
- package/dist/index.d.ts +122 -12
- package/dist/index.js +16 -2
- package/dist/index.native.js +16 -2
- package/dist/linter.cjs +104 -0
- package/dist/linter.js +72 -24
- package/dist/manifest.cjs +144 -0
- package/dist/manifest.js +47 -36
- package/dist/primitives.js +35 -51
- package/dist/primitives.native.js +20 -10
- package/package.json +13 -3
package/dist/linter.js
CHANGED
|
@@ -2,29 +2,62 @@
|
|
|
2
2
|
// Scans widget source for banned patterns. Pure text scanning — no AST parsing
|
|
3
3
|
// dep — which is sufficient to gate v0.1 submissions and gives clear pointers
|
|
4
4
|
// to humans during review.
|
|
5
|
+
//
|
|
6
|
+
// The banned-identifier list is derived from `CONTRACT.bannedApis` so the
|
|
7
|
+
// system prompt, the linter, and the runtime allowlist agree.
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
9
|
+
import { CONTRACT } from "./contract.js";
|
|
10
|
+
|
|
11
|
+
// Per-identifier match rule. Most banned identifiers compile to a
|
|
12
|
+
// whole-word match; a few have special syntax (`Function(`, `new Function`,
|
|
13
|
+
// `import(`) that needs a tighter regex.
|
|
14
|
+
function _ruleForIdentifier(identifier, reason) {
|
|
15
|
+
const id = identifier;
|
|
16
|
+
if (id === "Function") {
|
|
17
|
+
return {
|
|
18
|
+
id: "no-function-constructor",
|
|
19
|
+
label: `${reason} (banned: Function() constructor)`,
|
|
20
|
+
pattern: /(^|[^A-Za-z0-9_$.])Function\s*\(/,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (id === "new Function") {
|
|
24
|
+
return {
|
|
25
|
+
id: "no-new-function",
|
|
26
|
+
label: `${reason} (banned: new Function())`,
|
|
27
|
+
pattern: /\bnew\s+Function\s*\(/,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (id === "import(") {
|
|
31
|
+
return {
|
|
32
|
+
id: "no-dynamic-import",
|
|
33
|
+
label: `${reason} (banned: dynamic import())`,
|
|
34
|
+
pattern: /(^|[^A-Za-z0-9_$.])import\s*\(/,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (id === "eval") {
|
|
38
|
+
return {
|
|
39
|
+
id: "no-eval",
|
|
40
|
+
label: `${reason} (banned: eval)`,
|
|
41
|
+
pattern: /\beval\s*\(/,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Default: word-boundary anchored. Used for the simple identifier
|
|
45
|
+
// forms (`window`, `document`, `process`, ...).
|
|
46
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
|
+
return {
|
|
48
|
+
id: `no-${id.toLowerCase()}`,
|
|
49
|
+
label: `${reason} (banned: ${id})`,
|
|
50
|
+
pattern: new RegExp(`\\b${escaped}\\b`),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const CONTRACT_RULES = CONTRACT.bannedApis.map((b) =>
|
|
55
|
+
_ruleForIdentifier(b.identifier, b.reason),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Extra rules that don't map 1:1 to a banned identifier in the contract:
|
|
59
|
+
// host-internal imports that widgets must never touch.
|
|
60
|
+
const EXTRA_RULES = [
|
|
28
61
|
{
|
|
29
62
|
id: "no-auth-store-import",
|
|
30
63
|
label: "widgets must not import the host's auth store",
|
|
@@ -32,11 +65,24 @@ const RULES = [
|
|
|
32
65
|
},
|
|
33
66
|
{
|
|
34
67
|
id: "no-axios-import",
|
|
35
|
-
label:
|
|
68
|
+
label:
|
|
69
|
+
"widgets must not import axios directly; use the injected datastore client",
|
|
36
70
|
pattern: /from\s+['"]axios['"]|require\s*\(\s*['"]axios['"]\s*\)/,
|
|
37
71
|
},
|
|
38
72
|
];
|
|
39
73
|
|
|
74
|
+
const RULES = [...CONTRACT_RULES, ...EXTRA_RULES];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns the contract-derived list of banned identifiers. Exposed so the
|
|
78
|
+
* SDK contract test (and any host build step that wants to surface the
|
|
79
|
+
* list to humans) can read the canonical set without re-parsing the
|
|
80
|
+
* source.
|
|
81
|
+
*/
|
|
82
|
+
export function bannedIdentifiers() {
|
|
83
|
+
return CONTRACT.bannedApis.map((b) => b.identifier);
|
|
84
|
+
}
|
|
85
|
+
|
|
40
86
|
/**
|
|
41
87
|
* Lint a JavaScript source string.
|
|
42
88
|
* @param {string} source
|
|
@@ -46,7 +92,9 @@ export function lintSource(source) {
|
|
|
46
92
|
if (typeof source !== "string") {
|
|
47
93
|
return {
|
|
48
94
|
ok: false,
|
|
49
|
-
findings: [
|
|
95
|
+
findings: [
|
|
96
|
+
{ rule: "input", label: "source must be a string", line: 0, snippet: "" },
|
|
97
|
+
],
|
|
50
98
|
};
|
|
51
99
|
}
|
|
52
100
|
const findings = [];
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// CommonJS mirror of manifest.js. Lets backend services (CJS) call
|
|
2
|
+
// `validateManifest(...)` without a dynamic-import detour. The two files
|
|
3
|
+
// share their constants by mutual import via createRequire — manifest.js
|
|
4
|
+
// re-exports the same functions defined here so there is exactly one
|
|
5
|
+
// implementation.
|
|
6
|
+
|
|
7
|
+
const VALID_CATEGORIES = new Set([
|
|
8
|
+
"input", "display", "layout", "data", "media", "communication", "custom",
|
|
9
|
+
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "CUSTOM",
|
|
10
|
+
]);
|
|
11
|
+
const VALID_PLATFORMS = new Set(["web", "native"]);
|
|
12
|
+
|
|
13
|
+
function canonicalCategory(c) {
|
|
14
|
+
if (typeof c !== "string") return null;
|
|
15
|
+
const upper = c.toUpperCase();
|
|
16
|
+
return [
|
|
17
|
+
"INPUT",
|
|
18
|
+
"DISPLAY",
|
|
19
|
+
"LAYOUT",
|
|
20
|
+
"DATA",
|
|
21
|
+
"MEDIA",
|
|
22
|
+
"COMMUNICATION",
|
|
23
|
+
"CUSTOM",
|
|
24
|
+
].includes(upper)
|
|
25
|
+
? upper
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
30
|
+
const SEMVER_RANGE_RE = /^[\^~>=<]*\s*\d+\.\d+\.\d+/;
|
|
31
|
+
const ID_RE = /^[a-z0-9]+(?:\.[a-z0-9-]+)+$/;
|
|
32
|
+
|
|
33
|
+
function isNonEmptyString(v) {
|
|
34
|
+
return typeof v === "string" && v.length > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pushIf(errors, cond, msg) {
|
|
38
|
+
if (!cond) errors.push(msg);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validateManifest(m) {
|
|
42
|
+
const errors = [];
|
|
43
|
+
if (m === null || typeof m !== "object") {
|
|
44
|
+
return { ok: false, errors: ["manifest must be an object"] };
|
|
45
|
+
}
|
|
46
|
+
const manifest = m;
|
|
47
|
+
|
|
48
|
+
pushIf(errors, isNonEmptyString(manifest.id), "manifest.id must be a non-empty string");
|
|
49
|
+
if (isNonEmptyString(manifest.id)) {
|
|
50
|
+
pushIf(
|
|
51
|
+
errors,
|
|
52
|
+
ID_RE.test(manifest.id),
|
|
53
|
+
"manifest.id must be reverse-DNS, e.g. com.acme.charts.barchart",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pushIf(errors, isNonEmptyString(manifest.name), "manifest.name must be a non-empty string");
|
|
58
|
+
|
|
59
|
+
pushIf(errors, isNonEmptyString(manifest.version), "manifest.version must be a non-empty string");
|
|
60
|
+
if (isNonEmptyString(manifest.version)) {
|
|
61
|
+
pushIf(errors, SEMVER_RE.test(manifest.version), "manifest.version must be a valid semver");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pushIf(
|
|
65
|
+
errors,
|
|
66
|
+
typeof manifest.category === "string" && VALID_CATEGORIES.has(manifest.category),
|
|
67
|
+
`manifest.category must be one of ${[...VALID_CATEGORIES].join(", ")}`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
pushIf(errors, isNonEmptyString(manifest.icon), "manifest.icon must be a non-empty string");
|
|
71
|
+
pushIf(errors, isNonEmptyString(manifest.description), "manifest.description must be a non-empty string");
|
|
72
|
+
|
|
73
|
+
if (manifest.author === null || typeof manifest.author !== "object") {
|
|
74
|
+
errors.push("manifest.author must be an object with a name");
|
|
75
|
+
} else {
|
|
76
|
+
const author = manifest.author;
|
|
77
|
+
pushIf(errors, isNonEmptyString(author.name), "manifest.author.name must be a non-empty string");
|
|
78
|
+
if (author.url !== undefined) {
|
|
79
|
+
pushIf(errors, isNonEmptyString(author.url), "manifest.author.url must be a string");
|
|
80
|
+
}
|
|
81
|
+
if (author.email !== undefined) {
|
|
82
|
+
pushIf(errors, isNonEmptyString(author.email), "manifest.author.email must be a string");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!Array.isArray(manifest.supportedPlatforms) || manifest.supportedPlatforms.length === 0) {
|
|
87
|
+
errors.push("manifest.supportedPlatforms must be a non-empty array");
|
|
88
|
+
} else {
|
|
89
|
+
for (const p of manifest.supportedPlatforms) {
|
|
90
|
+
if (!VALID_PLATFORMS.has(p)) {
|
|
91
|
+
errors.push(`manifest.supportedPlatforms contains invalid value "${p}"`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pushIf(
|
|
97
|
+
errors,
|
|
98
|
+
isNonEmptyString(manifest.minAppStudioVersion),
|
|
99
|
+
"manifest.minAppStudioVersion must be a non-empty string",
|
|
100
|
+
);
|
|
101
|
+
if (isNonEmptyString(manifest.minAppStudioVersion)) {
|
|
102
|
+
pushIf(
|
|
103
|
+
errors,
|
|
104
|
+
SEMVER_RANGE_RE.test(manifest.minAppStudioVersion),
|
|
105
|
+
"manifest.minAppStudioVersion must be a semver range, e.g. >=2.4.0",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!Array.isArray(manifest.requestedScopes)) {
|
|
110
|
+
errors.push("manifest.requestedScopes must be an array (use [] for none)");
|
|
111
|
+
} else {
|
|
112
|
+
for (const s of manifest.requestedScopes) {
|
|
113
|
+
if (!isNonEmptyString(s)) {
|
|
114
|
+
errors.push("manifest.requestedScopes entries must be non-empty strings");
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (manifest.propertySchema === null || typeof manifest.propertySchema !== "object") {
|
|
121
|
+
errors.push("manifest.propertySchema must be an object");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!Array.isArray(manifest.events)) {
|
|
125
|
+
errors.push("manifest.events must be an array (use [] for none)");
|
|
126
|
+
} else {
|
|
127
|
+
for (const e of manifest.events) {
|
|
128
|
+
if (e === null || typeof e !== "object") {
|
|
129
|
+
errors.push("manifest.events entries must be objects");
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (!isNonEmptyString(e.name)) {
|
|
133
|
+
errors.push("manifest.events[].name must be a non-empty string");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
validateManifest,
|
|
143
|
+
canonicalCategory,
|
|
144
|
+
};
|
package/dist/manifest.js
CHANGED
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// CommonJS mirror of manifest.js. Lets backend services (CJS) call
|
|
2
|
+
// `validateManifest(...)` without a dynamic-import detour. The two files
|
|
3
|
+
// share their constants by mutual import via createRequire — manifest.js
|
|
4
|
+
// re-exports the same functions defined here so there is exactly one
|
|
5
|
+
// implementation.
|
|
3
6
|
|
|
4
|
-
// REQ-MKT canonical category values are uppercase (matching the Prisma
|
|
5
|
-
// `WidgetCategory` enum used to persist them server-side). We accept both
|
|
6
|
-
// cases on input — lowercase is the form spelled out in design doc §2.2
|
|
7
|
-
// and the TS types — and canonicalize internally via `canonicalCategory`.
|
|
8
7
|
const VALID_CATEGORIES = new Set([
|
|
9
8
|
"input", "display", "layout", "data", "media", "communication", "custom",
|
|
10
9
|
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "CUSTOM",
|
|
11
10
|
]);
|
|
12
11
|
const VALID_PLATFORMS = new Set(["web", "native"]);
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
* Normalize a manifest category to its canonical (uppercase) form.
|
|
16
|
-
* Returns `null` if the input is not a recognised category.
|
|
17
|
-
* @param {unknown} c
|
|
18
|
-
* @returns {string | null}
|
|
19
|
-
*/
|
|
20
|
-
export function canonicalCategory(c) {
|
|
13
|
+
function canonicalCategory(c) {
|
|
21
14
|
if (typeof c !== "string") return null;
|
|
22
15
|
const upper = c.toUpperCase();
|
|
23
|
-
return [
|
|
16
|
+
return [
|
|
17
|
+
"INPUT",
|
|
18
|
+
"DISPLAY",
|
|
19
|
+
"LAYOUT",
|
|
20
|
+
"DATA",
|
|
21
|
+
"MEDIA",
|
|
22
|
+
"COMMUNICATION",
|
|
23
|
+
"CUSTOM",
|
|
24
|
+
].includes(upper)
|
|
25
|
+
? upper
|
|
26
|
+
: null;
|
|
24
27
|
}
|
|
28
|
+
|
|
25
29
|
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
26
30
|
const SEMVER_RANGE_RE = /^[\^~>=<]*\s*\d+\.\d+\.\d+/;
|
|
27
31
|
const ID_RE = /^[a-z0-9]+(?:\.[a-z0-9-]+)+$/;
|
|
@@ -34,37 +38,34 @@ function pushIf(errors, cond, msg) {
|
|
|
34
38
|
if (!cond) errors.push(msg);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
* Validates a WidgetManifest shape.
|
|
39
|
-
* @param {unknown} m
|
|
40
|
-
* @returns {{ ok: true } | { ok: false, errors: string[] }}
|
|
41
|
-
*/
|
|
42
|
-
export function validateManifest(m) {
|
|
41
|
+
function validateManifest(m) {
|
|
43
42
|
const errors = [];
|
|
44
|
-
|
|
45
43
|
if (m === null || typeof m !== "object") {
|
|
46
44
|
return { ok: false, errors: ["manifest must be an object"] };
|
|
47
45
|
}
|
|
48
|
-
|
|
49
|
-
const manifest = /** @type {Record<string, unknown>} */ (m);
|
|
46
|
+
const manifest = m;
|
|
50
47
|
|
|
51
48
|
pushIf(errors, isNonEmptyString(manifest.id), "manifest.id must be a non-empty string");
|
|
52
49
|
if (isNonEmptyString(manifest.id)) {
|
|
53
|
-
pushIf(
|
|
54
|
-
|
|
50
|
+
pushIf(
|
|
51
|
+
errors,
|
|
52
|
+
ID_RE.test(manifest.id),
|
|
53
|
+
"manifest.id must be reverse-DNS, e.g. com.acme.charts.barchart",
|
|
54
|
+
);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
pushIf(errors, isNonEmptyString(manifest.name), "manifest.name must be a non-empty string");
|
|
58
58
|
|
|
59
59
|
pushIf(errors, isNonEmptyString(manifest.version), "manifest.version must be a non-empty string");
|
|
60
60
|
if (isNonEmptyString(manifest.version)) {
|
|
61
|
-
pushIf(errors, SEMVER_RE.test(
|
|
62
|
-
"manifest.version must be a valid semver");
|
|
61
|
+
pushIf(errors, SEMVER_RE.test(manifest.version), "manifest.version must be a valid semver");
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
pushIf(
|
|
64
|
+
pushIf(
|
|
65
|
+
errors,
|
|
66
66
|
typeof manifest.category === "string" && VALID_CATEGORIES.has(manifest.category),
|
|
67
|
-
`manifest.category must be one of ${[...VALID_CATEGORIES].join(", ")}
|
|
67
|
+
`manifest.category must be one of ${[...VALID_CATEGORIES].join(", ")}`,
|
|
68
|
+
);
|
|
68
69
|
|
|
69
70
|
pushIf(errors, isNonEmptyString(manifest.icon), "manifest.icon must be a non-empty string");
|
|
70
71
|
pushIf(errors, isNonEmptyString(manifest.description), "manifest.description must be a non-empty string");
|
|
@@ -72,7 +73,7 @@ export function validateManifest(m) {
|
|
|
72
73
|
if (manifest.author === null || typeof manifest.author !== "object") {
|
|
73
74
|
errors.push("manifest.author must be an object with a name");
|
|
74
75
|
} else {
|
|
75
|
-
const author =
|
|
76
|
+
const author = manifest.author;
|
|
76
77
|
pushIf(errors, isNonEmptyString(author.name), "manifest.author.name must be a non-empty string");
|
|
77
78
|
if (author.url !== undefined) {
|
|
78
79
|
pushIf(errors, isNonEmptyString(author.url), "manifest.author.url must be a string");
|
|
@@ -92,11 +93,17 @@ export function validateManifest(m) {
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
pushIf(
|
|
96
|
-
|
|
96
|
+
pushIf(
|
|
97
|
+
errors,
|
|
98
|
+
isNonEmptyString(manifest.minAppStudioVersion),
|
|
99
|
+
"manifest.minAppStudioVersion must be a non-empty string",
|
|
100
|
+
);
|
|
97
101
|
if (isNonEmptyString(manifest.minAppStudioVersion)) {
|
|
98
|
-
pushIf(
|
|
99
|
-
|
|
102
|
+
pushIf(
|
|
103
|
+
errors,
|
|
104
|
+
SEMVER_RANGE_RE.test(manifest.minAppStudioVersion),
|
|
105
|
+
"manifest.minAppStudioVersion must be a semver range, e.g. >=2.4.0",
|
|
106
|
+
);
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
if (!Array.isArray(manifest.requestedScopes)) {
|
|
@@ -122,8 +129,7 @@ export function validateManifest(m) {
|
|
|
122
129
|
errors.push("manifest.events entries must be objects");
|
|
123
130
|
break;
|
|
124
131
|
}
|
|
125
|
-
|
|
126
|
-
if (!isNonEmptyString(ev.name)) {
|
|
132
|
+
if (!isNonEmptyString(e.name)) {
|
|
127
133
|
errors.push("manifest.events[].name must be a non-empty string");
|
|
128
134
|
}
|
|
129
135
|
}
|
|
@@ -131,3 +137,8 @@ export function validateManifest(m) {
|
|
|
131
137
|
|
|
132
138
|
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
133
139
|
}
|
|
140
|
+
|
|
141
|
+
export {
|
|
142
|
+
validateManifest,
|
|
143
|
+
canonicalCategory,
|
|
144
|
+
};
|
package/dist/primitives.js
CHANGED
|
@@ -1,51 +1,35 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function Image({ source, style, alt, ...rest }) {
|
|
40
|
-
const src = typeof source === "string" ? source : source?.uri;
|
|
41
|
-
return el("img", { src, alt: alt ?? "", style: asStyle(style), ...rest });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function ScrollView({ children, style, horizontal, ...rest }) {
|
|
45
|
-
const merged = {
|
|
46
|
-
overflowX: horizontal ? "auto" : "hidden",
|
|
47
|
-
overflowY: horizontal ? "hidden" : "auto",
|
|
48
|
-
...(asStyle(style) || {}),
|
|
49
|
-
};
|
|
50
|
-
return el("div", { style: merged, ...rest }, children);
|
|
51
|
-
}
|
|
1
|
+
// REQ-WSDK-RN-WEB: the SDK ships ONE primitive surface. On the web
|
|
2
|
+
// (Studio + Player) the host's bundler aliases `react-native` to
|
|
3
|
+
// `react-native-web` (see `frontend/vite.config.js`), which renders the
|
|
4
|
+
// same React Native API as DOM elements. The exported Expo app's Metro
|
|
5
|
+
// bundler resolves `react-native` to the real library via the SDK
|
|
6
|
+
// package.json `react-native` field — pointing at
|
|
7
|
+
// `primitives.native.js`, which mirrors this file.
|
|
8
|
+
//
|
|
9
|
+
// The previous hand-written DOM wrappers (one function per primitive,
|
|
10
|
+
// two implementations to keep in sync) are gone: the React Native API
|
|
11
|
+
// is the contract, react-native-web does the platform mapping for us,
|
|
12
|
+
// and adding a new primitive (FlatList, ActivityIndicator, …) becomes
|
|
13
|
+
// a one-line re-export rather than a paired implementation.
|
|
14
|
+
//
|
|
15
|
+
// Authors continue to import these from `@colixsystems/widget-sdk` — the
|
|
16
|
+
// SDK's index re-exports them under the names we document in CONTRACT.
|
|
17
|
+
// The static analyzer's allowedBareImports still rejects direct
|
|
18
|
+
// `react-native` imports; the SDK is the single entry point.
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
Text,
|
|
22
|
+
View,
|
|
23
|
+
Pressable,
|
|
24
|
+
Image,
|
|
25
|
+
ScrollView,
|
|
26
|
+
TextInput,
|
|
27
|
+
// Additional cross-platform primitives the AI agent + handwritten
|
|
28
|
+
// widgets commonly reach for. All work in both react-native-web and
|
|
29
|
+
// native react-native without per-platform code.
|
|
30
|
+
FlatList,
|
|
31
|
+
SectionList,
|
|
32
|
+
ActivityIndicator,
|
|
33
|
+
Switch,
|
|
34
|
+
StyleSheet,
|
|
35
|
+
} from "react-native";
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// REQ-WSDK-RN-WEB: native primitive re-exports. Metro picks this entry
|
|
2
|
+
// via the `react-native` field in package.json. Web mirror in
|
|
3
|
+
// `./primitives.js` re-exports from the SAME `react-native` module —
|
|
4
|
+
// the frontend's Vite alias rewrites it to `react-native-web` for
|
|
5
|
+
// browser builds, so the two files only differ in which `react-native`
|
|
6
|
+
// resolves at bundle time. Keep the export list in lockstep with
|
|
7
|
+
// `./primitives.js`.
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
export {
|
|
10
|
+
Text,
|
|
11
|
+
View,
|
|
12
|
+
Pressable,
|
|
13
|
+
Image,
|
|
14
|
+
ScrollView,
|
|
15
|
+
TextInput,
|
|
16
|
+
FlatList,
|
|
17
|
+
SectionList,
|
|
18
|
+
ActivityIndicator,
|
|
19
|
+
Switch,
|
|
20
|
+
StyleSheet,
|
|
21
|
+
} from "react-native";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
"./linter": {
|
|
18
18
|
"import": "./dist/linter.js",
|
|
19
19
|
"default": "./dist/linter.js"
|
|
20
|
+
},
|
|
21
|
+
"./contract": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"require": "./dist/contract.cjs",
|
|
24
|
+
"import": "./dist/contract.js",
|
|
25
|
+
"default": "./dist/contract.cjs"
|
|
20
26
|
}
|
|
21
27
|
},
|
|
22
28
|
"bin": {
|
|
@@ -29,7 +35,7 @@
|
|
|
29
35
|
],
|
|
30
36
|
"scripts": {
|
|
31
37
|
"build": "node scripts/build.js",
|
|
32
|
-
"test": "node --test src"
|
|
38
|
+
"test": "node --test src/__tests__/contract.test.js"
|
|
33
39
|
},
|
|
34
40
|
"engines": {
|
|
35
41
|
"node": ">=18"
|
|
@@ -45,7 +51,8 @@
|
|
|
45
51
|
},
|
|
46
52
|
"peerDependencies": {
|
|
47
53
|
"react": ">=18.0.0",
|
|
48
|
-
"react-native": "*"
|
|
54
|
+
"react-native": "*",
|
|
55
|
+
"react-native-web": ">=0.19.0"
|
|
49
56
|
},
|
|
50
57
|
"peerDependenciesMeta": {
|
|
51
58
|
"react": {
|
|
@@ -53,6 +60,9 @@
|
|
|
53
60
|
},
|
|
54
61
|
"react-native": {
|
|
55
62
|
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"react-native-web": {
|
|
65
|
+
"optional": true
|
|
56
66
|
}
|
|
57
67
|
},
|
|
58
68
|
"keywords": [
|