@glasshome/widget-sdk 0.2.1 → 0.2.4

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.
@@ -0,0 +1,92 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Runtime validation schemas for widget data structures.
4
+ *
5
+ * These mirror the TypeScript interfaces in types.ts but work at runtime —
6
+ * use them at HTTP and database boundaries to catch type mismatches early
7
+ * instead of letting them surface as cryptic SQLite or JSON errors.
8
+ */
9
+ export declare const GridSizeSchema: z.ZodObject<{
10
+ w: z.ZodNumber;
11
+ h: z.ZodNumber;
12
+ }, z.core.$strip>;
13
+ export declare const WidgetManifestSchema: z.ZodObject<{
14
+ name: z.ZodString;
15
+ description: z.ZodOptional<z.ZodString>;
16
+ minSize: z.ZodObject<{
17
+ w: z.ZodNumber;
18
+ h: z.ZodNumber;
19
+ }, z.core.$strip>;
20
+ maxSize: z.ZodObject<{
21
+ w: z.ZodNumber;
22
+ h: z.ZodNumber;
23
+ }, z.core.$strip>;
24
+ sdkVersion: z.ZodString;
25
+ icon: z.ZodOptional<z.ZodString>;
26
+ schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
27
+ defaultConfig: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
28
+ }, z.core.$strip>;
29
+ export declare const PublishRequestSchema: z.ZodObject<{
30
+ action: z.ZodLiteral<"request">;
31
+ scope: z.ZodString;
32
+ name: z.ZodString;
33
+ displayName: z.ZodString;
34
+ description: z.ZodOptional<z.ZodString>;
35
+ icon: z.ZodOptional<z.ZodString>;
36
+ minSize: z.ZodObject<{
37
+ w: z.ZodNumber;
38
+ h: z.ZodNumber;
39
+ }, z.core.$strip>;
40
+ maxSize: z.ZodObject<{
41
+ w: z.ZodNumber;
42
+ h: z.ZodNumber;
43
+ }, z.core.$strip>;
44
+ sdkVersion: z.ZodString;
45
+ version: z.ZodString;
46
+ bundleSize: z.ZodNumber;
47
+ sha256Hash: z.ZodString;
48
+ manifestJson: z.ZodString;
49
+ }, z.core.$strip>;
50
+ export declare const PublishConfirmSchema: z.ZodObject<{
51
+ action: z.ZodLiteral<"confirm">;
52
+ versionId: z.ZodString;
53
+ }, z.core.$strip>;
54
+ export declare const PublishBodySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
55
+ action: z.ZodLiteral<"request">;
56
+ scope: z.ZodString;
57
+ name: z.ZodString;
58
+ displayName: z.ZodString;
59
+ description: z.ZodOptional<z.ZodString>;
60
+ icon: z.ZodOptional<z.ZodString>;
61
+ minSize: z.ZodObject<{
62
+ w: z.ZodNumber;
63
+ h: z.ZodNumber;
64
+ }, z.core.$strip>;
65
+ maxSize: z.ZodObject<{
66
+ w: z.ZodNumber;
67
+ h: z.ZodNumber;
68
+ }, z.core.$strip>;
69
+ sdkVersion: z.ZodString;
70
+ version: z.ZodString;
71
+ bundleSize: z.ZodNumber;
72
+ sha256Hash: z.ZodString;
73
+ manifestJson: z.ZodString;
74
+ }, z.core.$strip>, z.ZodObject<{
75
+ action: z.ZodLiteral<"confirm">;
76
+ versionId: z.ZodString;
77
+ }, z.core.$strip>], "action">;
78
+ /**
79
+ * Serialize a GridSize to a JSON string for database storage.
80
+ */
81
+ export declare function serializeGridSize(size: z.infer<typeof GridSizeSchema>): string;
82
+ /**
83
+ * Parse a GridSize from a database text column or API response.
84
+ * Accepts a JSON string, an already-parsed object, or garbage.
85
+ * Returns the fallback for any value that doesn't validate.
86
+ */
87
+ export declare function parseGridSize(raw: unknown, fallback?: z.infer<typeof GridSizeSchema>): z.infer<typeof GridSizeSchema>;
88
+ /**
89
+ * Format a zod error into a single user-friendly string.
90
+ * Groups issues by path so the output reads like a bulleted list.
91
+ */
92
+ export declare function formatSchemaError(error: z.ZodError): string;
@@ -0,0 +1,63 @@
1
+ import { z as n } from "zod";
2
+ const t = n.object({
3
+ w: n.number().int().min(1),
4
+ h: n.number().int().min(1)
5
+ }), u = n.object({
6
+ name: n.string().min(1),
7
+ description: n.string().optional(),
8
+ minSize: t,
9
+ maxSize: t,
10
+ sdkVersion: n.string().min(1),
11
+ icon: n.string().optional(),
12
+ schema: n.record(n.string(), n.unknown()).optional(),
13
+ defaultConfig: n.record(n.string(), n.unknown()).optional()
14
+ }), o = /^[a-z0-9][a-z0-9-]*$/, a = n.object({
15
+ action: n.literal("request"),
16
+ scope: n.string().regex(o, "Must be lowercase alphanumeric with hyphens"),
17
+ name: n.string().min(1),
18
+ displayName: n.string().min(1),
19
+ description: n.string().optional(),
20
+ icon: n.string().optional(),
21
+ minSize: t,
22
+ maxSize: t,
23
+ sdkVersion: n.string().min(1),
24
+ version: n.string().min(1),
25
+ bundleSize: n.number().int().positive(),
26
+ sha256Hash: n.string().min(1),
27
+ manifestJson: n.string().min(1)
28
+ }), c = n.object({
29
+ action: n.literal("confirm"),
30
+ versionId: n.string().min(1)
31
+ }), p = n.discriminatedUnion("action", [
32
+ a,
33
+ c
34
+ ]);
35
+ function h(i) {
36
+ return JSON.stringify(i);
37
+ }
38
+ function l(i, e = { w: 1, h: 1 }) {
39
+ if (i != null && typeof i == "object") {
40
+ const s = t.safeParse(i);
41
+ if (s.success) return s.data;
42
+ }
43
+ if (typeof i == "string")
44
+ try {
45
+ const s = JSON.parse(i), r = t.safeParse(s);
46
+ if (r.success) return r.data;
47
+ } catch {
48
+ }
49
+ return e;
50
+ }
51
+ function g(i) {
52
+ return i.issues.map((e) => `${e.path.length > 0 ? e.path.join(".") : "(root)"}: ${e.message}`).join("; ");
53
+ }
54
+ export {
55
+ t as GridSizeSchema,
56
+ p as PublishBodySchema,
57
+ c as PublishConfirmSchema,
58
+ a as PublishRequestSchema,
59
+ u as WidgetManifestSchema,
60
+ g as formatSchemaError,
61
+ l as parseGridSize,
62
+ h as serializeGridSize
63
+ };
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ZodType } from "zod";
1
2
  /**
2
3
  * Grid size constraint — number of columns (w) and rows (h).
3
4
  */
@@ -10,15 +11,16 @@ export interface GridSize {
10
11
  * Framework-agnostic: no SolidJS imports.
11
12
  */
12
13
  export interface WidgetManifest {
13
- tag: string;
14
14
  name: string;
15
15
  description?: string;
16
16
  minSize: GridSize;
17
17
  maxSize: GridSize;
18
+ defaultSize?: GridSize;
18
19
  sdkVersion: string;
19
20
  icon?: string;
20
21
  schema?: object;
21
22
  defaultConfig?: Record<string, unknown>;
23
+ configVersion?: number;
22
24
  }
23
25
  /**
24
26
  * Runtime context provided to widgets by the host.
@@ -45,6 +47,8 @@ export interface WidgetContext {
45
47
  */
46
48
  export interface WidgetDefinition<C = Record<string, unknown>> {
47
49
  manifest: WidgetManifest;
50
+ configSchema?: ZodType<C, unknown>;
51
+ migrate?: (config: Record<string, unknown>, fromConfigVersion: number) => Record<string, unknown>;
48
52
  component: (props: {
49
53
  config: C;
50
54
  }) => any;
@@ -1,86 +1,91 @@
1
- import { existsSync as a, rmSync as x, mkdirSync as w, readdirSync as v, statSync as b, readFileSync as D, writeFileSync as S } from "node:fs";
2
- import { resolve as l, join as g, sep as h, dirname as W } from "node:path";
3
- import { fileURLToPath as I } from "node:url";
4
- const j = "virtual:glasshome-widget", p = "\0virtual:glasshome-widget", E = "/@glasshome/preview";
5
- function y(e) {
6
- return h === "\\" ? e.split(h).join("/") : e;
1
+ import { createHash as I } from "node:crypto";
2
+ import { existsSync as l, rmSync as N, mkdirSync as P, readdirSync as x, statSync as O, readFileSync as h, writeFileSync as b } from "node:fs";
3
+ import { resolve as a, join as g, sep as v, dirname as E } from "node:path";
4
+ import { fileURLToPath as _ } from "node:url";
5
+ function R(e) {
6
+ const n = JSON.stringify(e, Object.keys(e).sort());
7
+ return I("sha256").update(n).digest("hex").slice(0, 16);
7
8
  }
8
- function f() {
9
- const e = I(import.meta.url);
10
- return l(W(e), "../../preview");
9
+ const k = "virtual:glasshome-widget", S = "\0virtual:glasshome-widget", T = "/@glasshome/preview";
10
+ function D(e) {
11
+ return v === "\\" ? e.split(v).join("/") : e;
11
12
  }
12
- function P(e) {
13
+ function y() {
14
+ const e = _(import.meta.url);
15
+ return a(E(e), "../../preview");
16
+ }
17
+ function j(e) {
13
18
  return e === "solid-js" || e.startsWith("solid-js/") || e === "@glasshome/widget-sdk" || e.startsWith("@glasshome/widget-sdk/") || e === "@glasshome/ui" || e.startsWith("@glasshome/ui/") || e === "@glasshome/sync-layer" || e.startsWith("@glasshome/sync-layer/");
14
19
  }
15
- function _(e) {
16
- const i = [];
17
- if (!a(e)) return i;
18
- for (const r of v(e)) {
19
- const n = l(e, r);
20
- if (!b(n).isDirectory()) continue;
21
- const t = l(n, "index.tsx"), s = l(n, "manifest.json");
22
- a(t) && a(s) && i.push({ name: r, entry: t });
20
+ function F(e) {
21
+ const n = [];
22
+ if (!l(e)) return n;
23
+ for (const r of x(e)) {
24
+ const o = a(e, r);
25
+ if (!O(o).isDirectory()) continue;
26
+ const i = a(o, "index.tsx"), t = a(o, "manifest.json");
27
+ l(i) && l(t) && n.push({ name: r, entry: i });
23
28
  }
24
- return i;
29
+ return n;
25
30
  }
26
- function O(e, i) {
31
+ function J(e, n) {
27
32
  const r = [];
28
- if (a(e))
29
- for (const t of v(e)) {
30
- if (!b(g(e, t)).isDirectory()) continue;
31
- const s = g(e, t, "manifest.json");
33
+ if (l(e))
34
+ for (const i of x(e)) {
35
+ if (!O(g(e, i)).isDirectory()) continue;
36
+ const t = g(e, i, "manifest.json");
32
37
  try {
33
- const o = JSON.parse(D(s, "utf-8"));
34
- r.push({ ...o, bundleUrl: `./${t}.js` });
38
+ const s = JSON.parse(h(t, "utf-8"));
39
+ r.push({ ...s, bundleUrl: `./${i}.js` });
35
40
  } catch {
36
41
  }
37
42
  }
38
- const n = {
43
+ const o = {
39
44
  version: 1,
40
45
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
41
46
  baseUrl: "./",
42
47
  widgets: r
43
48
  };
44
- a(i) || w(i, { recursive: !0 }), S(g(i, "registry.json"), JSON.stringify(n, null, 2)), console.log(`[registry] Generated registry.json with ${r.length} widget(s)`);
49
+ l(n) || P(n, { recursive: !0 }), b(g(n, "registry.json"), JSON.stringify(o, null, 2)), console.log(`[registry] Generated registry.json with ${r.length} widget(s)`);
45
50
  }
46
- async function R(e) {
47
- const { build: i } = await import("vite"), r = process.cwd(), n = l(r, e?.srcDir ?? "src"), t = l(r, e?.outDir ?? "dist");
48
- let s = _(n);
49
- if (s.length === 0) {
50
- console.warn("[glasshome-widgets] No widgets found in", n);
51
+ async function U(e) {
52
+ const { build: n } = await import("vite"), r = process.cwd(), o = a(r, e?.srcDir ?? "src"), i = a(r, e?.outDir ?? "dist");
53
+ let t = F(o);
54
+ if (t.length === 0) {
55
+ console.warn("[glasshome-widgets] No widgets found in", o);
51
56
  return;
52
57
  }
53
58
  if (e?.only) {
54
- const o = new Set(e.only);
55
- s = s.filter((u) => o.has(u.name));
59
+ const s = new Set(e.only);
60
+ t = t.filter((c) => s.has(c.name));
56
61
  }
57
- e?.only || a(t) && x(t, { recursive: !0 }), a(t) || w(t, { recursive: !0 });
58
- for (const o of s)
59
- await i({
62
+ e?.only || l(i) && N(i, { recursive: !0 }), l(i) || P(i, { recursive: !0 });
63
+ for (const s of t)
64
+ await n({
60
65
  configFile: !1,
61
66
  root: r,
62
67
  plugins: e?.plugins ?? [],
63
68
  ...e?.viteConfig,
64
69
  build: {
65
70
  lib: {
66
- entry: o.entry,
71
+ entry: s.entry,
67
72
  formats: ["es"],
68
- fileName: o.name
73
+ fileName: s.name
69
74
  },
70
75
  rollupOptions: {
71
- external: P
76
+ external: j
72
77
  },
73
- outDir: t,
78
+ outDir: i,
74
79
  emptyOutDir: !1,
75
80
  copyPublicDir: !1,
76
81
  ...e?.viteConfig?.build
77
82
  },
78
83
  logLevel: "warn"
79
84
  });
80
- O(n, t);
85
+ J(o, i);
81
86
  }
82
- function L(e) {
83
- const i = e?.entry ?? "src/index.tsx";
87
+ function C(e) {
88
+ const n = e?.entry ?? "src/index.tsx";
84
89
  return [{
85
90
  name: "glasshome-widget:build",
86
91
  apply: "build",
@@ -88,67 +93,92 @@ function L(e) {
88
93
  return {
89
94
  build: {
90
95
  lib: {
91
- entry: i,
96
+ entry: n,
92
97
  formats: ["es"],
93
98
  fileName: "index"
94
99
  },
95
100
  rollupOptions: {
96
- external: P
101
+ external: j
97
102
  }
98
103
  }
99
104
  };
100
105
  }
106
+ }, {
107
+ name: "glasshome-widget:schema",
108
+ apply: "build",
109
+ async closeBundle() {
110
+ const t = a(process.cwd(), "dist", "index.js");
111
+ if (l(t))
112
+ try {
113
+ const c = (await import(`${t}?t=${Date.now()}`)).default;
114
+ if (!c?.configSchema) return;
115
+ const { z: m } = await import("zod"), f = m.toJSONSchema(c.configSchema, { unrepresentable: "any" }), d = a(process.cwd(), "manifest.json");
116
+ if (l(d)) {
117
+ const w = JSON.parse(h(d, "utf-8"));
118
+ w.schema = f, b(d, JSON.stringify(w, null, 2));
119
+ }
120
+ const u = R(f), p = a(process.cwd(), ".schema-hash");
121
+ if (l(p) && h(p, "utf-8").trim() !== u) {
122
+ const W = c.manifest?.name ?? "unknown";
123
+ console.warn(
124
+ `[widget-sdk] Schema shape changed for "${W}" — verify configVersion was bumped`
125
+ );
126
+ }
127
+ b(p, u);
128
+ } catch {
129
+ }
130
+ }
101
131
  }, {
102
132
  name: "glasshome-widget:dev",
103
133
  apply: "serve",
104
134
  enforce: "pre",
105
135
  config() {
106
- const t = f(), s = process.cwd(), o = l(s, "..", "..");
136
+ const t = y(), s = process.cwd(), c = a(s, "..", "..");
107
137
  return {
108
138
  server: {
109
139
  fs: {
110
- allow: [t, s, o]
140
+ allow: [t, s, c]
111
141
  }
112
142
  }
113
143
  };
114
144
  },
115
145
  configureServer(t) {
116
- const s = f(), o = g(s, "preview.html");
117
- t.middlewares.use(async (u, d, m) => {
118
- if (u.url === "/" || u.url === "/index.html")
146
+ const s = y(), c = g(s, "preview.html");
147
+ t.middlewares.use(async (m, f, d) => {
148
+ if (m.url === "/" || m.url === "/index.html")
119
149
  try {
120
- let c = D(o, "utf-8");
121
- c = await t.transformIndexHtml(u.url, c), d.setHeader("Content-Type", "text/html"), d.statusCode = 200, d.end(c);
122
- } catch (c) {
123
- m(c);
150
+ let u = h(c, "utf-8");
151
+ u = await t.transformIndexHtml(m.url, u), f.setHeader("Content-Type", "text/html"), f.statusCode = 200, f.end(u);
152
+ } catch (u) {
153
+ d(u);
124
154
  }
125
155
  else
126
- m();
156
+ d();
127
157
  });
128
158
  },
129
159
  resolveId(t) {
130
- if (t === E) {
131
- const s = f();
132
- return y(g(s, "host.tsx"));
160
+ if (t === T) {
161
+ const s = y();
162
+ return D(g(s, "host.tsx"));
133
163
  }
134
- if (t === j)
135
- return p;
164
+ if (t === k)
165
+ return S;
136
166
  },
137
167
  load(t) {
138
- if (t === p)
139
- return `export { default } from "${y(l(process.cwd(), i))}";`;
168
+ if (t === S)
169
+ return `export { default } from "${D(a(process.cwd(), n))}";`;
140
170
  }
141
171
  }];
142
172
  }
143
- function k(e) {
144
- const i = e?.srcDir ?? "src", r = e?.outDir ?? "dist";
145
- let n = [];
173
+ function A(e) {
174
+ const n = e?.srcDir ?? "src", r = e?.outDir ?? "dist";
175
+ let o = [];
146
176
  return [{
147
177
  name: "glasshome-widgets:build",
148
178
  apply: "build",
149
- config(s) {
150
- return n = (s.plugins ?? []).flat().filter(Boolean).filter(
151
- (o) => !o.name?.startsWith("glasshome-widgets:")
179
+ config(t) {
180
+ return o = (t.plugins ?? []).flat().filter(Boolean).filter(
181
+ (s) => !s.name?.startsWith("glasshome-widgets:")
152
182
  ), {
153
183
  build: {
154
184
  rollupOptions: {
@@ -159,26 +189,26 @@ function k(e) {
159
189
  }
160
190
  };
161
191
  },
162
- resolveId(s) {
163
- if (s === "\0glasshome-noop") return s;
192
+ resolveId(t) {
193
+ if (t === "\0glasshome-noop") return t;
164
194
  },
165
- load(s) {
166
- if (s === "\0glasshome-noop") return "export {}";
195
+ load(t) {
196
+ if (t === "\0glasshome-noop") return "export {}";
167
197
  },
168
198
  async closeBundle() {
169
- await R({
170
- srcDir: i,
199
+ await U({
200
+ srcDir: n,
171
201
  outDir: r,
172
- plugins: n
202
+ plugins: o
173
203
  });
174
204
  }
175
205
  }];
176
206
  }
177
207
  export {
178
- R as buildWidgets,
179
- _ as discoverWidgets,
180
- O as generateRegistry,
181
- L as glasshomeWidget,
182
- k as glasshomeWidgets,
183
- P as isWidgetExternal
208
+ U as buildWidgets,
209
+ F as discoverWidgets,
210
+ J as generateRegistry,
211
+ C as glasshomeWidget,
212
+ A as glasshomeWidgets,
213
+ j as isWidgetExternal
184
214
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/widget-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.4",
4
4
  "description": "SDK for building GlassHome dashboard widgets with SolidJS",
5
5
  "keywords": [
6
6
  "glasshome",
package/preview/host.tsx CHANGED
@@ -41,7 +41,7 @@ function PreviewHost() {
41
41
  "margin-top": "4px",
42
42
  }}
43
43
  >
44
- &lt;{widget.manifest.tag}&gt; | {widget.manifest.type} | {widget.manifest.size}
44
+ &lt;{widget.manifest.name}&gt; | {widget.manifest.type} | {widget.manifest.size}
45
45
  </p>
46
46
  </div>
47
47
  <button