@colixsystems/widget-sdk 0.1.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/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/cli.js +41 -0
- package/dist/define-widget.js +29 -0
- package/dist/hooks.js +76 -0
- package/dist/index.d.ts +207 -0
- package/dist/index.js +17 -0
- package/dist/index.native.js +17 -0
- package/dist/linter.js +68 -0
- package/dist/manifest.js +133 -0
- package/dist/primitives.js +51 -0
- package/dist/primitives.native.js +11 -0
- package/dist/property-schema.js +163 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AppStudio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @colixsystems/widget-sdk
|
|
2
|
+
|
|
3
|
+
Common widget interface for [AppStudio](https://github.com/appstudio). This package implements the contract that every widget — built-in or third-party, web or native — speaks: a `WidgetManifest`, a `WidgetContext`, a property schema, the helper hooks, and the static linter that gates submissions.
|
|
4
|
+
|
|
5
|
+
See the design reference for the full architecture: [`docs/architecture/widget-marketplace.md`](../../docs/architecture/widget-marketplace.md), specifically section 3.1.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
`v0.1.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**.
|
|
10
|
+
|
|
11
|
+
## Public API
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@colixsystems/widget-sdk";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
18
|
+
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
19
|
+
- `useDatastoreQuery`, `useDatastoreMutation`, `useWidgetEvent`, `useTheme`, `useI18n` — hooks that read from the host-provided `WidgetContext`.
|
|
20
|
+
- `Text`, `View`, `Pressable`, `Image`, `ScrollView` — platform-aware primitives. Web entry maps them to DOM elements; the `react-native` entry maps them to React Native components.
|
|
21
|
+
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
|
22
|
+
|
|
23
|
+
## Linter
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx appstudio-widget lint path/to/widget.js
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Scans for banned patterns (`eval`, `new Function`, dynamic `import()`, direct imports of host stores, raw axios). Exits 1 on findings.
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { lintSource } from "@colixsystems/widget-sdk/linter";
|
|
33
|
+
const report = lintSource(source);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Why no TypeScript dependency?
|
|
37
|
+
|
|
38
|
+
The package is plain ESM JavaScript with hand-written `.d.ts` ambient types. Consumers using TypeScript get full IntelliSense; consumers using JavaScript pay nothing.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CLI entry for `appstudio-widget lint <path>`.
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { argv, exit, stderr, stdout } from "node:process";
|
|
7
|
+
import { lintSource } from "./linter.js";
|
|
8
|
+
|
|
9
|
+
function usage() {
|
|
10
|
+
stderr.write("Usage: appstudio-widget lint <path>\n");
|
|
11
|
+
exit(2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const args = argv.slice(2);
|
|
15
|
+
if (args.length === 0) usage();
|
|
16
|
+
const [cmd, ...rest] = args;
|
|
17
|
+
|
|
18
|
+
if (cmd === "lint") {
|
|
19
|
+
if (rest.length === 0) usage();
|
|
20
|
+
const filePath = resolve(rest[0]);
|
|
21
|
+
let source;
|
|
22
|
+
try {
|
|
23
|
+
source = readFileSync(filePath, "utf8");
|
|
24
|
+
} catch (err) {
|
|
25
|
+
stderr.write(`Could not read ${filePath}: ${err.message}\n`);
|
|
26
|
+
exit(1);
|
|
27
|
+
}
|
|
28
|
+
const { ok, findings } = lintSource(source);
|
|
29
|
+
if (ok) {
|
|
30
|
+
stdout.write(`${filePath}: clean\n`);
|
|
31
|
+
exit(0);
|
|
32
|
+
}
|
|
33
|
+
stderr.write(`${filePath}: ${findings.length} finding(s)\n`);
|
|
34
|
+
for (const f of findings) {
|
|
35
|
+
stderr.write(` [${f.rule}] line ${f.line}: ${f.label}\n ${f.snippet}\n`);
|
|
36
|
+
}
|
|
37
|
+
exit(1);
|
|
38
|
+
} else {
|
|
39
|
+
stderr.write(`Unknown command "${cmd}"\n`);
|
|
40
|
+
usage();
|
|
41
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// defineWidget per docs/architecture/widget-marketplace.md §3.1.
|
|
2
|
+
// Runs manifest shape validation synchronously and throws on failure.
|
|
3
|
+
|
|
4
|
+
import { validateManifest } from "./manifest.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Define an AppStudio widget module.
|
|
8
|
+
* @param {{ manifest: object, component: Function }} opts
|
|
9
|
+
* @returns {{ manifest: object, component: Function, _kind: 'appstudio-widget-module' }}
|
|
10
|
+
*/
|
|
11
|
+
export function defineWidget(opts) {
|
|
12
|
+
if (opts === null || typeof opts !== "object") {
|
|
13
|
+
throw new TypeError("defineWidget: opts must be an object");
|
|
14
|
+
}
|
|
15
|
+
const { manifest, component } = opts;
|
|
16
|
+
if (typeof component !== "function") {
|
|
17
|
+
throw new TypeError("defineWidget: component must be a function");
|
|
18
|
+
}
|
|
19
|
+
const result = validateManifest(manifest);
|
|
20
|
+
if (!result.ok) {
|
|
21
|
+
const msg = `Invalid widget manifest:\n - ${result.errors.join("\n - ")}`;
|
|
22
|
+
throw new Error(msg);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
manifest,
|
|
26
|
+
component,
|
|
27
|
+
_kind: "appstudio-widget-module",
|
|
28
|
+
};
|
|
29
|
+
}
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Widget hooks per docs/architecture/widget-marketplace.md §3.1.
|
|
2
|
+
// Each hook reads from the host-provided WidgetContext via React context, then
|
|
3
|
+
// delegates to the host implementation. Bodies are intentionally slim — the
|
|
4
|
+
// hosts (Studio, Player, exported app) own the runtime semantics.
|
|
5
|
+
//
|
|
6
|
+
// This file avoids JSX so it can ship as a plain .js without a transform.
|
|
7
|
+
|
|
8
|
+
import React, { createContext, useContext, useCallback } from "react";
|
|
9
|
+
|
|
10
|
+
/** @internal — host-injected context value of shape WidgetContext (see index.d.ts). */
|
|
11
|
+
const HostWidgetContext = createContext(null);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wraps children with the host-provided WidgetContext.
|
|
15
|
+
* The host (Studio/Player/native shell) builds the value and renders this provider.
|
|
16
|
+
*/
|
|
17
|
+
export function WidgetContextProvider({ value, children }) {
|
|
18
|
+
return React.createElement(HostWidgetContext.Provider, { value }, children);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function useWidgetContextOrThrow(hookName) {
|
|
22
|
+
const ctx = useContext(HostWidgetContext);
|
|
23
|
+
if (ctx == null) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`${hookName} must be used inside a WidgetContextProvider. ` +
|
|
26
|
+
`The host (Studio, Player, or exported app) is responsible for mounting it.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return ctx;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// v0.1 scope: returns the host's datastore-backed query result.
|
|
33
|
+
// The host owns suspense/caching; this hook is a passthrough to ctx.datastore.
|
|
34
|
+
export function useDatastoreQuery(table, query) {
|
|
35
|
+
const ctx = useWidgetContextOrThrow("useDatastoreQuery");
|
|
36
|
+
if (!ctx.datastore || typeof ctx.datastore.records !== "function") {
|
|
37
|
+
throw new Error("useDatastoreQuery: host did not inject a datastore client");
|
|
38
|
+
}
|
|
39
|
+
// The host-injected datastore is expected to expose a `useRecords(table, query)` hook;
|
|
40
|
+
// v0.1 stub returns the raw promise interface and lets the host wire React Query.
|
|
41
|
+
return ctx.datastore.records(table).list(query);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// v0.1 scope: returns { create, update, delete } from the injected datastore client.
|
|
45
|
+
export function useDatastoreMutation(table) {
|
|
46
|
+
const ctx = useWidgetContextOrThrow("useDatastoreMutation");
|
|
47
|
+
if (!ctx.datastore || typeof ctx.datastore.records !== "function") {
|
|
48
|
+
throw new Error("useDatastoreMutation: host did not inject a datastore client");
|
|
49
|
+
}
|
|
50
|
+
const ns = ctx.datastore.records(table);
|
|
51
|
+
return {
|
|
52
|
+
create: (values) => ns.create(values),
|
|
53
|
+
update: (id, values) => ns.update(id, values),
|
|
54
|
+
delete: (id) => ns.delete(id),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// v0.1 scope: emits a named event through ctx.events.emit. Page-level event
|
|
59
|
+
// bindings (subscribed by the host) decide what happens next.
|
|
60
|
+
export function useWidgetEvent(name) {
|
|
61
|
+
const ctx = useWidgetContextOrThrow("useWidgetEvent");
|
|
62
|
+
return useCallback((payload) => ctx.events.emit(name, payload), [ctx, name]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// v0.1 scope: returns ThemeTokens. Host owns theme resolution per workspace.
|
|
66
|
+
export function useTheme() {
|
|
67
|
+
const ctx = useWidgetContextOrThrow("useTheme");
|
|
68
|
+
return ctx.workspace.theme;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// v0.1 scope: returns { locale, t }. Host owns translation tables;
|
|
72
|
+
// the SDK does not bundle a translation engine.
|
|
73
|
+
export function useI18n() {
|
|
74
|
+
const ctx = useWidgetContextOrThrow("useI18n");
|
|
75
|
+
return ctx.i18n;
|
|
76
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// Hand-written ambient types for @colixsystems/widget-sdk.
|
|
2
|
+
// The package itself is plain ESM JavaScript; this file is shipped through the
|
|
3
|
+
// `types` field for IDE IntelliSense without forcing consumers to install
|
|
4
|
+
// TypeScript. Keep in sync with src/index.js when adding exports.
|
|
5
|
+
|
|
6
|
+
import type { ReactNode, JSX } from "react";
|
|
7
|
+
|
|
8
|
+
export type WidgetCategory =
|
|
9
|
+
| "input"
|
|
10
|
+
| "display"
|
|
11
|
+
| "layout"
|
|
12
|
+
| "data"
|
|
13
|
+
| "media"
|
|
14
|
+
| "communication"
|
|
15
|
+
| "custom";
|
|
16
|
+
|
|
17
|
+
export type WidgetScope = string; // e.g. "datastore.read:orders"
|
|
18
|
+
|
|
19
|
+
export type WidgetPropertyType =
|
|
20
|
+
| "string"
|
|
21
|
+
| "number"
|
|
22
|
+
| "boolean"
|
|
23
|
+
| "color"
|
|
24
|
+
| "icon"
|
|
25
|
+
| "image"
|
|
26
|
+
| "select"
|
|
27
|
+
| "multiselect"
|
|
28
|
+
| "tableRef"
|
|
29
|
+
| "columnRef"
|
|
30
|
+
| "recordBinding"
|
|
31
|
+
| "expression"
|
|
32
|
+
| "eventBinding"
|
|
33
|
+
| "object"
|
|
34
|
+
| "array";
|
|
35
|
+
|
|
36
|
+
export interface WidgetPropertyDef {
|
|
37
|
+
type: WidgetPropertyType;
|
|
38
|
+
label: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
default?: unknown;
|
|
41
|
+
required?: boolean;
|
|
42
|
+
enum?: Array<{ value: unknown; label: string }>;
|
|
43
|
+
items?: WidgetPropertyDef;
|
|
44
|
+
properties?: Record<string, WidgetPropertyDef>;
|
|
45
|
+
ui?: { widget?: "textarea" | "slider" | "code"; group?: string; order?: number };
|
|
46
|
+
validation?: { min?: number; max?: number; pattern?: string };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type WidgetPropertySchema = Record<string, WidgetPropertyDef>;
|
|
50
|
+
|
|
51
|
+
export interface WidgetEventDescriptor {
|
|
52
|
+
name: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
payloadSchema?: WidgetPropertySchema;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WidgetManifest {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
version: string;
|
|
61
|
+
category: WidgetCategory;
|
|
62
|
+
icon: string;
|
|
63
|
+
description: string;
|
|
64
|
+
author: { name: string; url?: string; email?: string };
|
|
65
|
+
supportedPlatforms: Array<"web" | "native">;
|
|
66
|
+
minAppStudioVersion: string;
|
|
67
|
+
requestedScopes: WidgetScope[];
|
|
68
|
+
propertySchema: WidgetPropertySchema;
|
|
69
|
+
events: WidgetEventDescriptor[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ThemeTokens {
|
|
73
|
+
colors: {
|
|
74
|
+
primary: string;
|
|
75
|
+
surface: string;
|
|
76
|
+
onSurface: string;
|
|
77
|
+
danger: string;
|
|
78
|
+
[k: string]: string;
|
|
79
|
+
};
|
|
80
|
+
spacing: { xs: number; sm: number; md: number; lg: number; xl: number };
|
|
81
|
+
radii: { sm: number; md: number; lg: number; pill: number };
|
|
82
|
+
typography: {
|
|
83
|
+
fontFamily: string;
|
|
84
|
+
sizes: { xs: number; sm: number; md: number; lg: number; xl: number };
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface WidgetContext<TProps = unknown> {
|
|
89
|
+
props: TProps;
|
|
90
|
+
widget: { id: string; instanceId: string; version: string };
|
|
91
|
+
user: {
|
|
92
|
+
id: string | null;
|
|
93
|
+
email: string | null;
|
|
94
|
+
displayName: string | null;
|
|
95
|
+
roles: string[];
|
|
96
|
+
groupIds: string[];
|
|
97
|
+
};
|
|
98
|
+
workspace: {
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
locale: string;
|
|
102
|
+
theme: ThemeTokens;
|
|
103
|
+
};
|
|
104
|
+
navigation: {
|
|
105
|
+
goTo(pageId: string, params?: Record<string, string>): void;
|
|
106
|
+
goBack(): void;
|
|
107
|
+
currentRoute: { pageId: string; params: Record<string, string> };
|
|
108
|
+
};
|
|
109
|
+
datastore: unknown; // typed by @colixsystems/datastore-client
|
|
110
|
+
events: { emit(eventName: string, payload?: unknown): void };
|
|
111
|
+
i18n: { locale: string; t(key: string, vars?: Record<string, unknown>): string };
|
|
112
|
+
platform: "web" | "native";
|
|
113
|
+
logger: {
|
|
114
|
+
debug: (...args: unknown[]) => void;
|
|
115
|
+
info: (...args: unknown[]) => void;
|
|
116
|
+
warn: (...args: unknown[]) => void;
|
|
117
|
+
error: (...args: unknown[]) => void;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface WidgetModule<TProps = unknown> {
|
|
122
|
+
manifest: WidgetManifest;
|
|
123
|
+
component: (ctx: WidgetContext<TProps>) => JSX.Element;
|
|
124
|
+
_kind: "appstudio-widget-module";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function defineWidget<TProps = unknown>(opts: {
|
|
128
|
+
manifest: WidgetManifest;
|
|
129
|
+
component: (ctx: WidgetContext<TProps>) => JSX.Element;
|
|
130
|
+
}): WidgetModule<TProps>;
|
|
131
|
+
|
|
132
|
+
export function validateManifest(
|
|
133
|
+
manifest: unknown
|
|
134
|
+
): { ok: true } | { ok: false; errors: string[] };
|
|
135
|
+
|
|
136
|
+
export function validatePropertySchema(
|
|
137
|
+
schema: unknown
|
|
138
|
+
): { ok: true } | { ok: false; errors: string[] };
|
|
139
|
+
|
|
140
|
+
export function validateProps<T = Record<string, unknown>>(
|
|
141
|
+
schema: WidgetPropertySchema,
|
|
142
|
+
props: unknown
|
|
143
|
+
): { ok: true; value: T } | { ok: false; errors: string[] };
|
|
144
|
+
|
|
145
|
+
export interface Query {
|
|
146
|
+
filter?: Record<string, unknown>;
|
|
147
|
+
sort?: Array<{ field: string; dir: "asc" | "desc" }>;
|
|
148
|
+
limit?: number;
|
|
149
|
+
cursor?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface QueryResult<T> {
|
|
153
|
+
data: T[];
|
|
154
|
+
loading: boolean;
|
|
155
|
+
error: Error | null;
|
|
156
|
+
refetch(): Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface MutationApi<T> {
|
|
160
|
+
create(values: Partial<T>): Promise<T>;
|
|
161
|
+
update(id: string, values: Partial<T>): Promise<T>;
|
|
162
|
+
delete(id: string): Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function useDatastoreQuery<T = unknown>(
|
|
166
|
+
table: string,
|
|
167
|
+
query?: Query
|
|
168
|
+
): QueryResult<T>;
|
|
169
|
+
|
|
170
|
+
export function useDatastoreMutation<T = unknown>(
|
|
171
|
+
table: string
|
|
172
|
+
): MutationApi<T>;
|
|
173
|
+
|
|
174
|
+
export function useWidgetEvent(
|
|
175
|
+
name: string
|
|
176
|
+
): (payload?: unknown) => void;
|
|
177
|
+
|
|
178
|
+
export function useTheme(): ThemeTokens;
|
|
179
|
+
|
|
180
|
+
export function useI18n(): {
|
|
181
|
+
locale: string;
|
|
182
|
+
t(key: string, vars?: Record<string, unknown>): string;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export function WidgetContextProvider(props: {
|
|
186
|
+
value: WidgetContext;
|
|
187
|
+
children?: ReactNode;
|
|
188
|
+
}): JSX.Element;
|
|
189
|
+
|
|
190
|
+
// Primitives — platform-aware. Type as opaque components.
|
|
191
|
+
export const Text: any;
|
|
192
|
+
export const View: any;
|
|
193
|
+
export const Pressable: any;
|
|
194
|
+
export const Image: any;
|
|
195
|
+
export const ScrollView: any;
|
|
196
|
+
|
|
197
|
+
// Linter
|
|
198
|
+
export interface LintFinding {
|
|
199
|
+
rule: string;
|
|
200
|
+
label: string;
|
|
201
|
+
line: number;
|
|
202
|
+
snippet: string;
|
|
203
|
+
}
|
|
204
|
+
export function lintSource(source: string): {
|
|
205
|
+
ok: boolean;
|
|
206
|
+
findings: LintFinding[];
|
|
207
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Web entry. Re-exports the public surface of @colixsystems/widget-sdk.
|
|
2
|
+
// The `react-native` field in package.json points Metro at index.native.js
|
|
3
|
+
// instead — keep these two files synchronised when adding exports.
|
|
4
|
+
|
|
5
|
+
export { defineWidget } from "./define-widget.js";
|
|
6
|
+
export { validateManifest, canonicalCategory } from "./manifest.js";
|
|
7
|
+
export { validatePropertySchema, validateProps } from "./property-schema.js";
|
|
8
|
+
export {
|
|
9
|
+
WidgetContextProvider,
|
|
10
|
+
useDatastoreQuery,
|
|
11
|
+
useDatastoreMutation,
|
|
12
|
+
useWidgetEvent,
|
|
13
|
+
useTheme,
|
|
14
|
+
useI18n,
|
|
15
|
+
} from "./hooks.js";
|
|
16
|
+
export { Text, View, Pressable, Image, ScrollView } from "./primitives.js";
|
|
17
|
+
export { lintSource } from "./linter.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// React Native entry. Identical to index.js except primitives are sourced from
|
|
2
|
+
// react-native instead of the DOM-backed implementation. Metro selects this
|
|
3
|
+
// file via the `react-native` field in package.json.
|
|
4
|
+
|
|
5
|
+
export { defineWidget } from "./define-widget.js";
|
|
6
|
+
export { validateManifest, canonicalCategory } from "./manifest.js";
|
|
7
|
+
export { validatePropertySchema, validateProps } from "./property-schema.js";
|
|
8
|
+
export {
|
|
9
|
+
WidgetContextProvider,
|
|
10
|
+
useDatastoreQuery,
|
|
11
|
+
useDatastoreMutation,
|
|
12
|
+
useWidgetEvent,
|
|
13
|
+
useTheme,
|
|
14
|
+
useI18n,
|
|
15
|
+
} from "./hooks.js";
|
|
16
|
+
export { Text, View, Pressable, Image, ScrollView } from "./primitives.native.js";
|
|
17
|
+
export { lintSource } from "./linter.js";
|
package/dist/linter.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Static-analysis linter per docs/architecture/widget-marketplace.md §3.1.
|
|
2
|
+
// Scans widget source for banned patterns. Pure text scanning — no AST parsing
|
|
3
|
+
// dep — which is sufficient to gate v0.1 submissions and gives clear pointers
|
|
4
|
+
// to humans during review.
|
|
5
|
+
|
|
6
|
+
const RULES = [
|
|
7
|
+
{
|
|
8
|
+
id: "no-eval",
|
|
9
|
+
label: "eval() is forbidden",
|
|
10
|
+
pattern: /\beval\s*\(/,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "no-new-function",
|
|
14
|
+
label: "new Function() is forbidden",
|
|
15
|
+
pattern: /\bnew\s+Function\s*\(/,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "no-function-constructor",
|
|
19
|
+
label: "Function() constructor is forbidden",
|
|
20
|
+
// Bare Function( call not preceded by identifier/property char.
|
|
21
|
+
pattern: /(^|[^A-Za-z0-9_$.])Function\s*\(/,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "no-dynamic-import",
|
|
25
|
+
label: "dynamic import() is forbidden",
|
|
26
|
+
pattern: /(^|[^A-Za-z0-9_$.])import\s*\(/,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "no-auth-store-import",
|
|
30
|
+
label: "widgets must not import the host's auth store",
|
|
31
|
+
pattern: /useAuthStore/,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "no-axios-import",
|
|
35
|
+
label: "widgets must not import axios directly; use the injected datastore client",
|
|
36
|
+
pattern: /from\s+['"]axios['"]|require\s*\(\s*['"]axios['"]\s*\)/,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Lint a JavaScript source string.
|
|
42
|
+
* @param {string} source
|
|
43
|
+
* @returns {{ ok: boolean, findings: Array<{ rule: string, label: string, line: number, snippet: string }> }}
|
|
44
|
+
*/
|
|
45
|
+
export function lintSource(source) {
|
|
46
|
+
if (typeof source !== "string") {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
findings: [{ rule: "input", label: "source must be a string", line: 0, snippet: "" }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const findings = [];
|
|
53
|
+
const lines = source.split(/\r?\n/);
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
const line = lines[i];
|
|
56
|
+
for (const rule of RULES) {
|
|
57
|
+
if (rule.pattern.test(line)) {
|
|
58
|
+
findings.push({
|
|
59
|
+
rule: rule.id,
|
|
60
|
+
label: rule.label,
|
|
61
|
+
line: i + 1,
|
|
62
|
+
snippet: line.trim().slice(0, 200),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { ok: findings.length === 0, findings };
|
|
68
|
+
}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Shape validation for WidgetManifest per docs/architecture/widget-marketplace.md §2.1.
|
|
2
|
+
// Pure JS — no third-party deps. Banned-identifier scanning lives in linter.js.
|
|
3
|
+
|
|
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
|
+
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"]);
|
|
13
|
+
|
|
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) {
|
|
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-]+)+$/;
|
|
28
|
+
|
|
29
|
+
function isNonEmptyString(v) {
|
|
30
|
+
return typeof v === "string" && v.length > 0;
|
|
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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Web primitive wrappers. Map to plain DOM elements via React.createElement.
|
|
2
|
+
// API surface only — host CSS / theme tokens drive styling.
|
|
3
|
+
|
|
4
|
+
import React from "react";
|
|
5
|
+
|
|
6
|
+
const el = React.createElement;
|
|
7
|
+
|
|
8
|
+
function asStyle(style) {
|
|
9
|
+
return style && typeof style === "object" ? style : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Text({ children, style, ...rest }) {
|
|
13
|
+
return el("span", { style: asStyle(style), ...rest }, children);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function View({ children, style, ...rest }) {
|
|
17
|
+
return el("div", { style: asStyle(style), ...rest }, children);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Pressable({ children, onPress, style, ...rest }) {
|
|
21
|
+
const handle = onPress
|
|
22
|
+
? (ev) => {
|
|
23
|
+
ev.preventDefault?.();
|
|
24
|
+
onPress(ev);
|
|
25
|
+
}
|
|
26
|
+
: undefined;
|
|
27
|
+
return el(
|
|
28
|
+
"button",
|
|
29
|
+
{
|
|
30
|
+
type: "button",
|
|
31
|
+
onClick: handle,
|
|
32
|
+
style: asStyle(style),
|
|
33
|
+
...rest,
|
|
34
|
+
},
|
|
35
|
+
children
|
|
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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// React Native primitive wrappers. Metro picks this entry through the
|
|
2
|
+
// package.json `react-native` field. `react-native` is an optional peer
|
|
3
|
+
// dependency — web-only consumers never import this file.
|
|
4
|
+
|
|
5
|
+
import { Text as RNText, View as RNView, Pressable as RNPressable, Image as RNImage, ScrollView as RNScrollView } from "react-native";
|
|
6
|
+
|
|
7
|
+
export const Text = RNText;
|
|
8
|
+
export const View = RNView;
|
|
9
|
+
export const Pressable = RNPressable;
|
|
10
|
+
export const Image = RNImage;
|
|
11
|
+
export const ScrollView = RNScrollView;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Property schema validation per docs/architecture/widget-marketplace.md §2.2.
|
|
2
|
+
// Drives the schema-driven Properties Panel and validates persisted page JSON.
|
|
3
|
+
|
|
4
|
+
const VALID_TYPES = new Set([
|
|
5
|
+
"string", "number", "boolean",
|
|
6
|
+
"color", "icon", "image",
|
|
7
|
+
"select", "multiselect",
|
|
8
|
+
"tableRef", "columnRef", "recordBinding",
|
|
9
|
+
"expression", "eventBinding",
|
|
10
|
+
"object", "array",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function isPlainObject(v) {
|
|
14
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validateDef(def, path, errors) {
|
|
18
|
+
if (!isPlainObject(def)) {
|
|
19
|
+
errors.push(`${path}: must be an object`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!VALID_TYPES.has(def.type)) {
|
|
23
|
+
errors.push(`${path}.type: invalid type "${def.type}"`);
|
|
24
|
+
}
|
|
25
|
+
if (typeof def.label !== "string" || def.label.length === 0) {
|
|
26
|
+
errors.push(`${path}.label: must be a non-empty string`);
|
|
27
|
+
}
|
|
28
|
+
if (def.description !== undefined && typeof def.description !== "string") {
|
|
29
|
+
errors.push(`${path}.description: must be a string when present`);
|
|
30
|
+
}
|
|
31
|
+
if (def.enum !== undefined) {
|
|
32
|
+
if (!Array.isArray(def.enum)) {
|
|
33
|
+
errors.push(`${path}.enum: must be an array`);
|
|
34
|
+
} else {
|
|
35
|
+
def.enum.forEach((entry, i) => {
|
|
36
|
+
if (!isPlainObject(entry) || typeof entry.label !== "string") {
|
|
37
|
+
errors.push(`${path}.enum[${i}]: must be { value, label }`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (def.type === "array") {
|
|
43
|
+
if (!isPlainObject(def.items)) {
|
|
44
|
+
errors.push(`${path}.items: array type requires nested items definition`);
|
|
45
|
+
} else {
|
|
46
|
+
validateDef(def.items, `${path}.items`, errors);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (def.type === "object") {
|
|
50
|
+
if (!isPlainObject(def.properties)) {
|
|
51
|
+
errors.push(`${path}.properties: object type requires nested properties`);
|
|
52
|
+
} else {
|
|
53
|
+
for (const [k, child] of Object.entries(def.properties)) {
|
|
54
|
+
validateDef(child, `${path}.properties.${k}`, errors);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validates a WidgetPropertySchema (top-level shape: Record<string, WidgetPropertyDef>).
|
|
62
|
+
* @param {unknown} schema
|
|
63
|
+
* @returns {{ ok: true } | { ok: false, errors: string[] }}
|
|
64
|
+
*/
|
|
65
|
+
export function validatePropertySchema(schema) {
|
|
66
|
+
const errors = [];
|
|
67
|
+
if (!isPlainObject(schema)) {
|
|
68
|
+
return { ok: false, errors: ["propertySchema must be an object"] };
|
|
69
|
+
}
|
|
70
|
+
for (const [k, def] of Object.entries(schema)) {
|
|
71
|
+
validateDef(def, `propertySchema.${k}`, errors);
|
|
72
|
+
}
|
|
73
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function coerceLeaf(def, value, path, errors) {
|
|
77
|
+
const t = def.type;
|
|
78
|
+
if (value === undefined || value === null) {
|
|
79
|
+
if (def.required) errors.push(`${path}: required`);
|
|
80
|
+
return def.default !== undefined ? def.default : value;
|
|
81
|
+
}
|
|
82
|
+
switch (t) {
|
|
83
|
+
case "string":
|
|
84
|
+
case "color":
|
|
85
|
+
case "icon":
|
|
86
|
+
case "image":
|
|
87
|
+
case "tableRef":
|
|
88
|
+
case "columnRef":
|
|
89
|
+
case "recordBinding":
|
|
90
|
+
case "expression":
|
|
91
|
+
case "eventBinding":
|
|
92
|
+
if (typeof value !== "string") errors.push(`${path}: expected string`);
|
|
93
|
+
return value;
|
|
94
|
+
case "number":
|
|
95
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
96
|
+
errors.push(`${path}: expected number`);
|
|
97
|
+
} else {
|
|
98
|
+
if (def.validation?.min !== undefined && value < def.validation.min) {
|
|
99
|
+
errors.push(`${path}: must be >= ${def.validation.min}`);
|
|
100
|
+
}
|
|
101
|
+
if (def.validation?.max !== undefined && value > def.validation.max) {
|
|
102
|
+
errors.push(`${path}: must be <= ${def.validation.max}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
case "boolean":
|
|
107
|
+
if (typeof value !== "boolean") errors.push(`${path}: expected boolean`);
|
|
108
|
+
return value;
|
|
109
|
+
case "select":
|
|
110
|
+
if (Array.isArray(def.enum) && !def.enum.some((e) => e.value === value)) {
|
|
111
|
+
errors.push(`${path}: value not in enum`);
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
case "multiselect":
|
|
115
|
+
if (!Array.isArray(value)) {
|
|
116
|
+
errors.push(`${path}: expected array`);
|
|
117
|
+
} else if (Array.isArray(def.enum)) {
|
|
118
|
+
const allowed = new Set(def.enum.map((e) => e.value));
|
|
119
|
+
value.forEach((v, i) => {
|
|
120
|
+
if (!allowed.has(v)) errors.push(`${path}[${i}]: value not in enum`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
case "array":
|
|
125
|
+
if (!Array.isArray(value)) {
|
|
126
|
+
errors.push(`${path}: expected array`);
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
return value.map((item, i) => coerceLeaf(def.items, item, `${path}[${i}]`, errors));
|
|
130
|
+
case "object": {
|
|
131
|
+
if (!isPlainObject(value)) {
|
|
132
|
+
errors.push(`${path}: expected object`);
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
const out = {};
|
|
136
|
+
for (const [k, child] of Object.entries(def.properties || {})) {
|
|
137
|
+
out[k] = coerceLeaf(child, value[k], `${path}.${k}`, errors);
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
default:
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validates `props` against a property schema. Applies defaults for missing leaves.
|
|
148
|
+
* @param {Record<string, any>} schema
|
|
149
|
+
* @param {unknown} props
|
|
150
|
+
* @returns {{ ok: true, value: Record<string, unknown> } | { ok: false, errors: string[] }}
|
|
151
|
+
*/
|
|
152
|
+
export function validateProps(schema, props) {
|
|
153
|
+
const errors = [];
|
|
154
|
+
if (!isPlainObject(schema)) {
|
|
155
|
+
return { ok: false, errors: ["schema must be an object"] };
|
|
156
|
+
}
|
|
157
|
+
const input = isPlainObject(props) ? props : {};
|
|
158
|
+
const out = {};
|
|
159
|
+
for (const [k, def] of Object.entries(schema)) {
|
|
160
|
+
out[k] = coerceLeaf(def, input[k], k, errors);
|
|
161
|
+
}
|
|
162
|
+
return errors.length === 0 ? { ok: true, value: out } : { ok: false, errors };
|
|
163
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@colixsystems/widget-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"react-native": "./dist/index.native.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"react-native": "./dist/index.native.js",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./linter": {
|
|
18
|
+
"import": "./dist/linter.js",
|
|
19
|
+
"default": "./dist/linter.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"appstudio-widget": "./dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "node scripts/build.js",
|
|
32
|
+
"test": "node --test src"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/colixwestin/appstudio.git",
|
|
44
|
+
"directory": "packages/widget-sdk"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": ">=18.0.0",
|
|
48
|
+
"react-native": "*"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"react": {
|
|
52
|
+
"optional": false
|
|
53
|
+
},
|
|
54
|
+
"react-native": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"appstudio",
|
|
60
|
+
"widget",
|
|
61
|
+
"sdk",
|
|
62
|
+
"low-code"
|
|
63
|
+
],
|
|
64
|
+
"license": "MIT"
|
|
65
|
+
}
|