@colixsystems/widget-sdk 0.14.1 → 0.15.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 +20 -4
- package/dist/clipboard.js +88 -0
- package/dist/clipboard.native.js +64 -0
- package/dist/contract.cjs +216 -11
- package/dist/contract.js +178 -9
- package/dist/datetimepicker.js +102 -0
- package/dist/hooks.js +3 -1
- package/dist/icon.js +29 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +7 -0
- package/dist/index.native.js +5 -0
- package/dist/linter.cjs +148 -9
- package/dist/linter.js +193 -13
- package/dist/primitives.js +8 -0
- package/dist/primitives.native.js +9 -0
- package/dist/toast.js +73 -0
- package/dist/toast.native.js +46 -0
- package/package.json +1 -1
package/dist/linter.js
CHANGED
|
@@ -57,18 +57,19 @@ const CONTRACT_RULES = CONTRACT.bannedApis.map((b) =>
|
|
|
57
57
|
|
|
58
58
|
// Extra rules that don't map 1:1 to a banned identifier in the contract:
|
|
59
59
|
// host-internal imports that widgets must never touch.
|
|
60
|
+
//
|
|
61
|
+
// REQ-WSDK-PLATFORM: `no-axios-import` is GONE. axios is on the vetted
|
|
62
|
+
// import list now (`CONTRACT.vettedImports`) — widgets may call third-party
|
|
63
|
+
// APIs directly. Calls to the host's own /api/* surface are blocked at
|
|
64
|
+
// runtime by the WidgetContextProvider's network gate; the soft
|
|
65
|
+
// `no-host-api-url` rule below flags obvious host-URL substrings so
|
|
66
|
+
// authors learn the rule statically.
|
|
60
67
|
const EXTRA_RULES = [
|
|
61
68
|
{
|
|
62
69
|
id: "no-auth-store-import",
|
|
63
70
|
label: "widgets must not import the host's auth store",
|
|
64
71
|
pattern: /useAuthStore/,
|
|
65
72
|
},
|
|
66
|
-
{
|
|
67
|
-
id: "no-axios-import",
|
|
68
|
-
label:
|
|
69
|
-
"widgets must not import axios directly; use the injected datastore client",
|
|
70
|
-
pattern: /from\s+['"]axios['"]|require\s*\(\s*['"]axios['"]\s*\)/,
|
|
71
|
-
},
|
|
72
73
|
];
|
|
73
74
|
|
|
74
75
|
const RULES = [...CONTRACT_RULES, ...EXTRA_RULES];
|
|
@@ -83,6 +84,158 @@ export function bannedIdentifiers() {
|
|
|
83
84
|
return CONTRACT.bannedApis.map((b) => b.identifier);
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// REQ-WSDK-PLATFORM §3.4, §8: scan the source for every bare import
|
|
88
|
+
// specifier and validate it against `CONTRACT.vettedImports`.
|
|
89
|
+
//
|
|
90
|
+
// Recognised forms (string-level, no AST):
|
|
91
|
+
// import X from "spec"
|
|
92
|
+
// import { … } from "spec"
|
|
93
|
+
// import * as X from "spec"
|
|
94
|
+
// import "spec"
|
|
95
|
+
// const X = require("spec") // CJS — for completeness
|
|
96
|
+
//
|
|
97
|
+
// Relative imports within the bundle (`./foo.js`, `./shared/util.js`) are
|
|
98
|
+
// allowed — REQ-WSDK-PLATFORM §4.2 lets a split-impl widget share helpers
|
|
99
|
+
// between `widget.web.jsx` and `widget.native.jsx`. Parent-directory
|
|
100
|
+
// (`../`) and absolute (`/`, `http(s):`) paths are rejected to keep the
|
|
101
|
+
// bundle scope sealed.
|
|
102
|
+
const IMPORT_SPECIFIER_RE =
|
|
103
|
+
/(?:^|[\s;])(?:import(?:\s+[^"';]+from)?|require)\s*\(?\s*['"]([^'"]+)['"]/g;
|
|
104
|
+
|
|
105
|
+
function _classifySpecifier(spec) {
|
|
106
|
+
if (
|
|
107
|
+
spec.startsWith("./") ||
|
|
108
|
+
(spec.startsWith(".") && !spec.startsWith(".."))
|
|
109
|
+
) {
|
|
110
|
+
return "relative-ok";
|
|
111
|
+
}
|
|
112
|
+
if (spec.startsWith("../")) return "relative-escape";
|
|
113
|
+
if (
|
|
114
|
+
spec.startsWith("/") ||
|
|
115
|
+
spec.startsWith("http:") ||
|
|
116
|
+
spec.startsWith("https:") ||
|
|
117
|
+
spec.startsWith("blob:") ||
|
|
118
|
+
spec.startsWith("data:")
|
|
119
|
+
) {
|
|
120
|
+
return "absolute";
|
|
121
|
+
}
|
|
122
|
+
return "bare";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function _importRules(source, manifest) {
|
|
126
|
+
const findings = [];
|
|
127
|
+
const allowed = new Map(
|
|
128
|
+
CONTRACT.vettedImports.map((v) => [v.specifier, v]),
|
|
129
|
+
);
|
|
130
|
+
// Track declared `supportedPlatforms` so a widget that claims "web only"
|
|
131
|
+
// doesn't import a native-only package (and vice versa) without the
|
|
132
|
+
// marketplace listing being honest about which platforms ship.
|
|
133
|
+
const declaredPlatforms =
|
|
134
|
+
manifest &&
|
|
135
|
+
Array.isArray(manifest.supportedPlatforms) &&
|
|
136
|
+
manifest.supportedPlatforms.length > 0
|
|
137
|
+
? new Set(manifest.supportedPlatforms)
|
|
138
|
+
: null;
|
|
139
|
+
|
|
140
|
+
const lines = source.split(/\r?\n/);
|
|
141
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
142
|
+
const line = lines[i];
|
|
143
|
+
// Reset lastIndex — the regex is /g and shared across lines.
|
|
144
|
+
IMPORT_SPECIFIER_RE.lastIndex = 0;
|
|
145
|
+
let m;
|
|
146
|
+
while ((m = IMPORT_SPECIFIER_RE.exec(line))) {
|
|
147
|
+
const spec = m[1];
|
|
148
|
+
const kind = _classifySpecifier(spec);
|
|
149
|
+
if (kind === "relative-ok") continue;
|
|
150
|
+
if (kind === "relative-escape") {
|
|
151
|
+
findings.push({
|
|
152
|
+
rule: "no-parent-relative-import",
|
|
153
|
+
label: `relative import "${spec}" escapes the bundle (\`../\`) — only sibling-file imports (\`./foo.js\`) are allowed`,
|
|
154
|
+
line: i + 1,
|
|
155
|
+
snippet: line.trim().slice(0, 200),
|
|
156
|
+
});
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (kind === "absolute") {
|
|
160
|
+
findings.push({
|
|
161
|
+
rule: "no-absolute-import",
|
|
162
|
+
label: `absolute import "${spec}" is not allowed — use a vetted bare specifier or a sibling-file (\`./foo.js\`) import`,
|
|
163
|
+
line: i + 1,
|
|
164
|
+
snippet: line.trim().slice(0, 200),
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// Bare specifier — validate against the vetted list.
|
|
169
|
+
const entry = allowed.get(spec);
|
|
170
|
+
if (!entry) {
|
|
171
|
+
findings.push({
|
|
172
|
+
rule: "import-not-vetted",
|
|
173
|
+
label:
|
|
174
|
+
`import "${spec}" is not on the vetted package list — see ` +
|
|
175
|
+
`CONTRACT.vettedImports for the current set or open a request to add it`,
|
|
176
|
+
line: i + 1,
|
|
177
|
+
snippet: line.trim().slice(0, 200),
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
// If the manifest declares supportedPlatforms, every imported
|
|
182
|
+
// specifier must support every declared platform. A widget that
|
|
183
|
+
// imports react-native-maps (native-only) cannot honestly claim
|
|
184
|
+
// supportedPlatforms: ["web", "native"] from a single source file —
|
|
185
|
+
// it must either drop "web" from the manifest, OR move that import
|
|
186
|
+
// into widget.native.jsx so the web bundle doesn't see it.
|
|
187
|
+
if (declaredPlatforms) {
|
|
188
|
+
for (const wanted of declaredPlatforms) {
|
|
189
|
+
if (!entry.platforms.includes(wanted)) {
|
|
190
|
+
findings.push({
|
|
191
|
+
rule: "import-platform-mismatch",
|
|
192
|
+
label:
|
|
193
|
+
`"${spec}" supports [${entry.platforms.join(", ")}] but the ` +
|
|
194
|
+
`manifest claims supportedPlatforms includes "${wanted}". ` +
|
|
195
|
+
`Move this import into widget.${wanted === "web" ? "native" : "web"}.jsx, ` +
|
|
196
|
+
`or drop "${wanted}" from manifest.supportedPlatforms.`,
|
|
197
|
+
line: i + 1,
|
|
198
|
+
snippet: line.trim().slice(0, 200),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return findings;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// REQ-WSDK-PLATFORM §3.5: soft warning when source contains host-API URL
|
|
209
|
+
// substrings. NOT a hard block — false positives are possible (a widget
|
|
210
|
+
// that happens to call a third-party API also located at `/api`). The
|
|
211
|
+
// marketplace review queue surfaces the warning for a human pass.
|
|
212
|
+
function _hostApiUrlRules(source) {
|
|
213
|
+
const findings = [];
|
|
214
|
+
const patterns = CONTRACT.hostApiUrlPatterns || [];
|
|
215
|
+
const lines = source.split(/\r?\n/);
|
|
216
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
217
|
+
const line = lines[i];
|
|
218
|
+
for (const needle of patterns) {
|
|
219
|
+
if (line.includes(needle)) {
|
|
220
|
+
findings.push({
|
|
221
|
+
rule: "no-host-api-url",
|
|
222
|
+
severity: "warning",
|
|
223
|
+
label:
|
|
224
|
+
`source contains "${needle}" — calls to the AppStudio host API ` +
|
|
225
|
+
`are blocked at runtime (widgets get no JWT token). Use SDK ` +
|
|
226
|
+
`hooks for workspace data; \`axios\`/\`fetch\` for third-party APIs.`,
|
|
227
|
+
line: i + 1,
|
|
228
|
+
snippet: line.trim().slice(0, 200),
|
|
229
|
+
});
|
|
230
|
+
// Only fire once per line — repeated needle hits on the same line
|
|
231
|
+
// are noise.
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return findings;
|
|
237
|
+
}
|
|
238
|
+
|
|
86
239
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — scope-required-for-user-mutation.
|
|
87
240
|
//
|
|
88
241
|
// A widget that calls `useUsers().invite()` / `.deactivate()` /
|
|
@@ -197,18 +350,29 @@ function _scopeRules(source, manifest) {
|
|
|
197
350
|
/**
|
|
198
351
|
* Lint a JavaScript source string.
|
|
199
352
|
*
|
|
353
|
+
* REQ-WSDK-PLATFORM update: findings can now carry a `severity` field
|
|
354
|
+
* (`"error"` | `"warning"`). The `ok` flag is true iff NO `severity:
|
|
355
|
+
* "error"` finding exists — warnings (currently just `no-host-api-url`)
|
|
356
|
+
* surface to the reviewer but do not block publish. Findings without an
|
|
357
|
+
* explicit `severity` are treated as errors for back-compat with existing
|
|
358
|
+
* rules.
|
|
359
|
+
*
|
|
200
360
|
* @param {string} source
|
|
201
|
-
* @param {{ manifest?: { requestedScopes?: string[] } }} [options]
|
|
202
|
-
*
|
|
203
|
-
* `scope-required-for-user-mutation`).
|
|
204
|
-
* @returns {{ ok: boolean, findings: Array<{ rule: string, label: string, line: number, snippet: string }> }}
|
|
361
|
+
* @param {{ manifest?: { requestedScopes?: string[], supportedPlatforms?: string[] } }} [options]
|
|
362
|
+
* @returns {{ ok: boolean, findings: Array<{ rule: string, severity?: "error" | "warning", label: string, line: number, snippet: string }> }}
|
|
205
363
|
*/
|
|
206
364
|
export function lintSource(source, options) {
|
|
207
365
|
if (typeof source !== "string") {
|
|
208
366
|
return {
|
|
209
367
|
ok: false,
|
|
210
368
|
findings: [
|
|
211
|
-
{
|
|
369
|
+
{
|
|
370
|
+
rule: "input",
|
|
371
|
+
severity: "error",
|
|
372
|
+
label: "source must be a string",
|
|
373
|
+
line: 0,
|
|
374
|
+
snippet: "",
|
|
375
|
+
},
|
|
212
376
|
],
|
|
213
377
|
};
|
|
214
378
|
}
|
|
@@ -220,6 +384,7 @@ export function lintSource(source, options) {
|
|
|
220
384
|
if (rule.pattern.test(line)) {
|
|
221
385
|
findings.push({
|
|
222
386
|
rule: rule.id,
|
|
387
|
+
severity: "error",
|
|
223
388
|
label: rule.label,
|
|
224
389
|
line: i + 1,
|
|
225
390
|
snippet: line.trim().slice(0, 200),
|
|
@@ -227,9 +392,24 @@ export function lintSource(source, options) {
|
|
|
227
392
|
}
|
|
228
393
|
}
|
|
229
394
|
}
|
|
395
|
+
// REQ-WSDK-PLATFORM §3.4: import-specifier validation.
|
|
396
|
+
findings.push(
|
|
397
|
+
..._importRules(source, options && options.manifest).map((f) => ({
|
|
398
|
+
...f,
|
|
399
|
+
severity: f.severity || "error",
|
|
400
|
+
})),
|
|
401
|
+
);
|
|
402
|
+
// REQ-WSDK-PLATFORM §3.5: soft host-API URL warning (does not block).
|
|
403
|
+
findings.push(..._hostApiUrlRules(source));
|
|
230
404
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — scope-aware rules. Run after the
|
|
231
405
|
// line-by-line scan so banned-identifier findings stay first in the
|
|
232
406
|
// output.
|
|
233
|
-
findings.push(
|
|
234
|
-
|
|
407
|
+
findings.push(
|
|
408
|
+
..._scopeRules(source, options && options.manifest).map((f) => ({
|
|
409
|
+
...f,
|
|
410
|
+
severity: f.severity || "error",
|
|
411
|
+
})),
|
|
412
|
+
);
|
|
413
|
+
const hasErrors = findings.some((f) => f.severity !== "warning");
|
|
414
|
+
return { ok: !hasErrors, findings };
|
|
235
415
|
}
|
package/dist/primitives.js
CHANGED
|
@@ -60,3 +60,11 @@ export const StyleSheet = ReactNative.StyleSheet;
|
|
|
60
60
|
// the OS hands it off to the system handler. `canOpenURL` reports whether
|
|
61
61
|
// the URL scheme is registered.
|
|
62
62
|
export const Linking = ReactNative.Linking;
|
|
63
|
+
// REQ-WSDK-PLATFORM §6 — `<Icon>` wraps lucide-react-native. Same source
|
|
64
|
+
// runs on both platforms; see ./icon.js.
|
|
65
|
+
export { Icon } from "./icon.js";
|
|
66
|
+
// REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` wraps
|
|
67
|
+
// @react-native-community/datetimepicker and normalizes its value to
|
|
68
|
+
// ISO 8601 strings. Same source runs on both platforms; see
|
|
69
|
+
// ./datetimepicker.js.
|
|
70
|
+
export { DateTimePicker } from "./datetimepicker.js";
|
|
@@ -20,3 +20,12 @@ export {
|
|
|
20
20
|
StyleSheet,
|
|
21
21
|
Linking,
|
|
22
22
|
} from "react-native";
|
|
23
|
+
|
|
24
|
+
// REQ-WSDK-PLATFORM §6 — `<Icon>` is implemented in one file that runs on
|
|
25
|
+
// both platforms (lucide-react-native ships a working build for both).
|
|
26
|
+
export { Icon } from "./icon.js";
|
|
27
|
+
// REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` ditto. The underlying
|
|
28
|
+
// @react-native-community/datetimepicker handles its own
|
|
29
|
+
// per-platform rendering; the SDK wrapper just normalizes the value
|
|
30
|
+
// to ISO strings.
|
|
31
|
+
export { DateTimePicker } from "./datetimepicker.js";
|
package/dist/toast.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// REQ-WSDK-PLATFORM §6 — `useToast()` hook (web implementation).
|
|
2
|
+
//
|
|
3
|
+
// Surfaces a short auto-dismissing notification. Widget API:
|
|
4
|
+
//
|
|
5
|
+
// const { showToast } = useToast();
|
|
6
|
+
// showToast({ kind: "success" | "error" | "info" | "warning", message: "Saved" });
|
|
7
|
+
//
|
|
8
|
+
// Renderer resolution order:
|
|
9
|
+
// 1. If the host provided `ctx.toast.showToast`, call that. The host
|
|
10
|
+
// owns positioning + theming.
|
|
11
|
+
// 2. Otherwise: dispatch a CustomEvent named "appstudio:widget-toast"
|
|
12
|
+
// on the window with detail `{ kind, message }`. Hosts that want a
|
|
13
|
+
// central toast renderer install a `window.addEventListener` for it.
|
|
14
|
+
// 3. As a last resort, `console.log` the toast so dev work surfaces
|
|
15
|
+
// something even before either of the above is wired.
|
|
16
|
+
//
|
|
17
|
+
// The SDK does NOT render any DOM itself — that responsibility belongs
|
|
18
|
+
// to the host so a single workspace-themed toast component shows the
|
|
19
|
+
// notifications across all widgets.
|
|
20
|
+
|
|
21
|
+
import { useCallback } from "react";
|
|
22
|
+
import { useWidgetContextOrThrow } from "./hooks.js";
|
|
23
|
+
|
|
24
|
+
function _normalizeKind(kind) {
|
|
25
|
+
return kind === "success" || kind === "error" || kind === "warning"
|
|
26
|
+
? kind
|
|
27
|
+
: "info";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useToast() {
|
|
31
|
+
const ctx = useWidgetContextOrThrow("useToast");
|
|
32
|
+
const hostToast =
|
|
33
|
+
ctx && ctx.toast && typeof ctx.toast.showToast === "function"
|
|
34
|
+
? ctx.toast.showToast
|
|
35
|
+
: null;
|
|
36
|
+
const showToast = useCallback(
|
|
37
|
+
(payload) => {
|
|
38
|
+
const opts = payload && typeof payload === "object" ? payload : {};
|
|
39
|
+
const kind = _normalizeKind(opts.kind);
|
|
40
|
+
const message = typeof opts.message === "string" ? opts.message : "";
|
|
41
|
+
if (!message) return;
|
|
42
|
+
if (hostToast) {
|
|
43
|
+
try {
|
|
44
|
+
hostToast({ kind, message });
|
|
45
|
+
return;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// Fall through to the event + console fallback rather than
|
|
48
|
+
// crashing the widget.
|
|
49
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
50
|
+
console.warn("[useToast] host toast handler threw:", err);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (
|
|
55
|
+
typeof window !== "undefined" &&
|
|
56
|
+
typeof window.dispatchEvent === "function" &&
|
|
57
|
+
typeof CustomEvent === "function"
|
|
58
|
+
) {
|
|
59
|
+
window.dispatchEvent(
|
|
60
|
+
new CustomEvent("appstudio:widget-toast", {
|
|
61
|
+
detail: { kind, message },
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (typeof console !== "undefined" && console.log) {
|
|
67
|
+
console.log(`[toast:${kind}] ${message}`);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[hostToast],
|
|
71
|
+
);
|
|
72
|
+
return { showToast };
|
|
73
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// REQ-WSDK-PLATFORM §6 — `useToast()` hook (native implementation).
|
|
2
|
+
//
|
|
3
|
+
// Same API as the web counterpart (./toast.js). Native has no
|
|
4
|
+
// `window.dispatchEvent` fallback — if the host doesn't provide
|
|
5
|
+
// `ctx.toast.showToast`, the hook falls back to `console.log` so the
|
|
6
|
+
// widget at least surfaces something during dev.
|
|
7
|
+
|
|
8
|
+
import { useCallback } from "react";
|
|
9
|
+
import { useWidgetContextOrThrow } from "./hooks.js";
|
|
10
|
+
|
|
11
|
+
function _normalizeKind(kind) {
|
|
12
|
+
return kind === "success" || kind === "error" || kind === "warning"
|
|
13
|
+
? kind
|
|
14
|
+
: "info";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useToast() {
|
|
18
|
+
const ctx = useWidgetContextOrThrow("useToast");
|
|
19
|
+
const hostToast =
|
|
20
|
+
ctx && ctx.toast && typeof ctx.toast.showToast === "function"
|
|
21
|
+
? ctx.toast.showToast
|
|
22
|
+
: null;
|
|
23
|
+
const showToast = useCallback(
|
|
24
|
+
(payload) => {
|
|
25
|
+
const opts = payload && typeof payload === "object" ? payload : {};
|
|
26
|
+
const kind = _normalizeKind(opts.kind);
|
|
27
|
+
const message = typeof opts.message === "string" ? opts.message : "";
|
|
28
|
+
if (!message) return;
|
|
29
|
+
if (hostToast) {
|
|
30
|
+
try {
|
|
31
|
+
hostToast({ kind, message });
|
|
32
|
+
return;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
35
|
+
console.warn("[useToast] host toast handler threw:", err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (typeof console !== "undefined" && console.log) {
|
|
40
|
+
console.log(`[toast:${kind}] ${message}`);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[hostToast],
|
|
44
|
+
);
|
|
45
|
+
return { showToast };
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|