@colixsystems/widget-sdk 0.2.0 → 0.4.1
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 +28 -1
- package/dist/_theme-tokens.js +10 -0
- package/dist/contract.cjs +404 -0
- package/dist/contract.js +17 -0
- package/dist/hooks.js +242 -20
- package/dist/index.d.ts +159 -1
- package/dist/index.js +3 -1
- package/dist/index.native.js +3 -1
- package/dist/linter.js +72 -24
- package/dist/manifest.cjs +144 -0
- package/dist/manifest.js +10 -128
- package/package.json +8 -2
|
@@ -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,133 +1,15 @@
|
|
|
1
1
|
// Shape validation for WidgetManifest per docs/architecture/widget-marketplace.md §2.1.
|
|
2
2
|
// Pure JS — no third-party deps. Banned-identifier scanning lives in linter.js.
|
|
3
|
+
//
|
|
4
|
+
// The implementation lives in `manifest.cjs` so backend services (CJS
|
|
5
|
+
// runtime) can `require` the same validator the SDK ships to ESM
|
|
6
|
+
// consumers. This file is a thin re-export so ESM imports keep working
|
|
7
|
+
// unchanged.
|
|
3
8
|
|
|
4
|
-
|
|
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
|
-
const VALID_CATEGORIES = new Set([
|
|
9
|
-
"input", "display", "layout", "data", "media", "communication", "custom",
|
|
10
|
-
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "CUSTOM",
|
|
11
|
-
]);
|
|
12
|
-
const VALID_PLATFORMS = new Set(["web", "native"]);
|
|
9
|
+
import { createRequire } from "module";
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
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) {
|
|
21
|
-
if (typeof c !== "string") return null;
|
|
22
|
-
const upper = c.toUpperCase();
|
|
23
|
-
return ["INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "CUSTOM"].includes(upper) ? upper : null;
|
|
24
|
-
}
|
|
25
|
-
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
26
|
-
const SEMVER_RANGE_RE = /^[\^~>=<]*\s*\d+\.\d+\.\d+/;
|
|
27
|
-
const ID_RE = /^[a-z0-9]+(?:\.[a-z0-9-]+)+$/;
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const cjs = require("./manifest.cjs");
|
|
28
13
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function pushIf(errors, cond, msg) {
|
|
34
|
-
if (!cond) errors.push(msg);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Validates a WidgetManifest shape.
|
|
39
|
-
* @param {unknown} m
|
|
40
|
-
* @returns {{ ok: true } | { ok: false, errors: string[] }}
|
|
41
|
-
*/
|
|
42
|
-
export function validateManifest(m) {
|
|
43
|
-
const errors = [];
|
|
44
|
-
|
|
45
|
-
if (m === null || typeof m !== "object") {
|
|
46
|
-
return { ok: false, errors: ["manifest must be an object"] };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const manifest = /** @type {Record<string, unknown>} */ (m);
|
|
50
|
-
|
|
51
|
-
pushIf(errors, isNonEmptyString(manifest.id), "manifest.id must be a non-empty string");
|
|
52
|
-
if (isNonEmptyString(manifest.id)) {
|
|
53
|
-
pushIf(errors, ID_RE.test(/** @type {string} */ (manifest.id)),
|
|
54
|
-
"manifest.id must be reverse-DNS, e.g. com.acme.charts.barchart");
|
|
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(/** @type {string} */ (manifest.version)),
|
|
62
|
-
"manifest.version must be a valid semver");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
pushIf(errors,
|
|
66
|
-
typeof manifest.category === "string" && VALID_CATEGORIES.has(manifest.category),
|
|
67
|
-
`manifest.category must be one of ${[...VALID_CATEGORIES].join(", ")}`);
|
|
68
|
-
|
|
69
|
-
pushIf(errors, isNonEmptyString(manifest.icon), "manifest.icon must be a non-empty string");
|
|
70
|
-
pushIf(errors, isNonEmptyString(manifest.description), "manifest.description must be a non-empty string");
|
|
71
|
-
|
|
72
|
-
if (manifest.author === null || typeof manifest.author !== "object") {
|
|
73
|
-
errors.push("manifest.author must be an object with a name");
|
|
74
|
-
} else {
|
|
75
|
-
const author = /** @type {Record<string, unknown>} */ (manifest.author);
|
|
76
|
-
pushIf(errors, isNonEmptyString(author.name), "manifest.author.name must be a non-empty string");
|
|
77
|
-
if (author.url !== undefined) {
|
|
78
|
-
pushIf(errors, isNonEmptyString(author.url), "manifest.author.url must be a string");
|
|
79
|
-
}
|
|
80
|
-
if (author.email !== undefined) {
|
|
81
|
-
pushIf(errors, isNonEmptyString(author.email), "manifest.author.email must be a string");
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!Array.isArray(manifest.supportedPlatforms) || manifest.supportedPlatforms.length === 0) {
|
|
86
|
-
errors.push("manifest.supportedPlatforms must be a non-empty array");
|
|
87
|
-
} else {
|
|
88
|
-
for (const p of manifest.supportedPlatforms) {
|
|
89
|
-
if (!VALID_PLATFORMS.has(p)) {
|
|
90
|
-
errors.push(`manifest.supportedPlatforms contains invalid value "${p}"`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
pushIf(errors, isNonEmptyString(manifest.minAppStudioVersion),
|
|
96
|
-
"manifest.minAppStudioVersion must be a non-empty string");
|
|
97
|
-
if (isNonEmptyString(manifest.minAppStudioVersion)) {
|
|
98
|
-
pushIf(errors, SEMVER_RANGE_RE.test(/** @type {string} */ (manifest.minAppStudioVersion)),
|
|
99
|
-
"manifest.minAppStudioVersion must be a semver range, e.g. >=2.4.0");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!Array.isArray(manifest.requestedScopes)) {
|
|
103
|
-
errors.push("manifest.requestedScopes must be an array (use [] for none)");
|
|
104
|
-
} else {
|
|
105
|
-
for (const s of manifest.requestedScopes) {
|
|
106
|
-
if (!isNonEmptyString(s)) {
|
|
107
|
-
errors.push("manifest.requestedScopes entries must be non-empty strings");
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (manifest.propertySchema === null || typeof manifest.propertySchema !== "object") {
|
|
114
|
-
errors.push("manifest.propertySchema must be an object");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (!Array.isArray(manifest.events)) {
|
|
118
|
-
errors.push("manifest.events must be an array (use [] for none)");
|
|
119
|
-
} else {
|
|
120
|
-
for (const e of manifest.events) {
|
|
121
|
-
if (e === null || typeof e !== "object") {
|
|
122
|
-
errors.push("manifest.events entries must be objects");
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
const ev = /** @type {Record<string, unknown>} */ (e);
|
|
126
|
-
if (!isNonEmptyString(ev.name)) {
|
|
127
|
-
errors.push("manifest.events[].name must be a non-empty string");
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
133
|
-
}
|
|
14
|
+
export const validateManifest = cjs.validateManifest;
|
|
15
|
+
export const canonicalCategory = cjs.canonicalCategory;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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"
|