@glasshome/widget-sdk 1.10.3 → 1.11.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 CHANGED
@@ -86,6 +86,12 @@ The `area` widget renders quick-glance metric pills (CO2 traffic-light, humidity
86
86
 
87
87
  If your widget needs full visual control (a chart, a video stream, a custom animated surface), follow these patterns: keep the `<Widget>` shell neutral (no `tone`/`color`), place your custom DOM inside `<Widget.Content>`, and document the choice in the widget's README.
88
88
 
89
+ ## Troubleshooting
90
+
91
+ | Variable | Effect |
92
+ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
93
+ | `GLASSHOME_WIDGET_NO_WORKER` | Set to `1` to make `bun widget connect` check each rebuild in a fresh process again, instead of reusing one worker for the session. |
94
+
89
95
  ## Peer Dependencies
90
96
 
91
97
  | Package | Required | Notes |
package/dist/index.js CHANGED
@@ -1451,7 +1451,7 @@ function Un() {
1451
1451
  //#region src/version.ts
1452
1452
  var Wn = {
1453
1453
  name: "@glasshome/widget-sdk",
1454
- version: "1.10.3",
1454
+ version: "1.11.0",
1455
1455
  description: "SDK for building GlassHome dashboard widgets with SolidJS",
1456
1456
  keywords: [
1457
1457
  "glasshome",
@@ -0,0 +1,72 @@
1
+ import { copyFileSync as e, rmSync as t } from "node:fs";
2
+ import { pathToFileURL as n } from "node:url";
3
+ //#region src/vite/introspect-core.ts
4
+ function r(e) {
5
+ return typeof e?.safeParse == "function";
6
+ }
7
+ function i(e, t) {
8
+ if (!r(e) || !Array.isArray(t)) return [];
9
+ let n = [];
10
+ for (let [r, i] of t.entries()) {
11
+ let t = i?.config;
12
+ if (typeof t != "object" || !t) continue;
13
+ let a = e.safeParse(t);
14
+ a.success || n.push({
15
+ index: r,
16
+ label: i.label,
17
+ problems: (a.error?.issues ?? []).map((e) => {
18
+ let t = e.path.map(String).join(".");
19
+ return t ? `${t}: ${e.message}` : e.message;
20
+ })
21
+ });
22
+ }
23
+ return n;
24
+ }
25
+ var a = [
26
+ "window",
27
+ "document",
28
+ "navigator",
29
+ "location",
30
+ "customElements",
31
+ "HTMLElement",
32
+ "Element",
33
+ "Node",
34
+ "CSS",
35
+ "getComputedStyle",
36
+ "requestAnimationFrame",
37
+ "cancelAnimationFrame",
38
+ "matchMedia"
39
+ ];
40
+ async function o() {
41
+ let { Window: e } = await import("happy-dom"), t = new e({ url: "http://localhost" });
42
+ for (let e of a) try {
43
+ Object.defineProperty(globalThis, e, {
44
+ value: t[e],
45
+ configurable: !0,
46
+ writable: !0
47
+ });
48
+ } catch {}
49
+ }
50
+ async function s(r, i) {
51
+ let a = `${r}.introspect-${i}.mjs`;
52
+ e(r, a);
53
+ try {
54
+ return await import(n(a).href);
55
+ } finally {
56
+ t(a, { force: !0 });
57
+ }
58
+ }
59
+ async function c(e, t) {
60
+ let r = (t ? await s(e, t) : await import(n(e).href)).default, a = null;
61
+ if (r?.configSchema) {
62
+ let { z: e } = await import("zod");
63
+ a = e.toJSONSchema(r.configSchema, { unrepresentable: "any" });
64
+ }
65
+ return {
66
+ manifest: r?.manifest ?? null,
67
+ jsonSchema: a,
68
+ exampleConfigIssues: i(r?.configSchema, r?.manifest?.examples)
69
+ };
70
+ }
71
+ //#endregion
72
+ export { c as n, o as t };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,8 @@
1
1
  import type { InlineConfig, Plugin } from "vite";
2
+ import type { ExampleConfigIssue, WidgetIntrospection } from "./introspect-core";
3
+ import { createIntrospectSession, type IntrospectSession, type IntrospectSessionOptions, introspectOnce } from "./introspect-session";
4
+ export { createIntrospectSession, introspectOnce };
5
+ export type { ExampleConfigIssue, IntrospectSession, IntrospectSessionOptions, WidgetIntrospection, };
2
6
  export interface GlasshomeWidgetOptions {
3
7
  /** Entry file for the widget (default: "src/index.tsx") */
4
8
  entry?: string;
@@ -16,6 +20,8 @@ export declare function runSchemaGuard(args: {
16
20
  hashFile: string;
17
21
  widgetName: string;
18
22
  manifestPath?: string;
23
+ /** Persistent worker to reuse. Only `bun widget connect`'s watch loop passes one. */
24
+ session?: IntrospectSession;
19
25
  }): Promise<void>;
20
26
  /**
21
27
  * Packages provided by the host import map — widgets must not bundle these.
@@ -74,6 +80,8 @@ export interface BuildWidgetsOptions {
74
80
  viteConfig?: InlineConfig;
75
81
  /** Build only these widget names (subdirectory names). Skips full clean. */
76
82
  only?: string[];
83
+ /** Persistent introspection worker to reuse across rebuilds. */
84
+ session?: IntrospectSession;
77
85
  }
78
86
  /**
79
87
  * Build each widget as a separate Vite invocation so shared code is inlined
@@ -103,4 +111,3 @@ export interface GlasshomeWidgetsOptions {
103
111
  * default build, and `closeBundle()` runs the actual per-widget builds.
104
112
  */
105
113
  export declare function glasshomeWidgets(options?: GlasshomeWidgetsOptions): Plugin[];
106
- export {};
@@ -1,19 +1,193 @@
1
1
  import { deprecations as e, formatDeprecation as t } from "../deprecations.js";
2
2
  import { formatSchemaError as n, isHostProvidedModule as r, widgetManifestSchema as i } from "@glasshome/widget-contract";
3
- import { spawnSync as a } from "node:child_process";
4
- import { createHash as o } from "node:crypto";
5
- import { existsSync as s, mkdirSync as c, readFileSync as l, readdirSync as u, rmSync as d, statSync as f, writeFileSync as p } from "node:fs";
6
- import { createRequire as m } from "node:module";
7
- import { dirname as h, join as g, relative as _, resolve as v, sep as y } from "node:path";
8
- import { fileURLToPath as b } from "node:url";
9
- import x from "@tailwindcss/vite";
3
+ import { createHash as a } from "node:crypto";
4
+ import { existsSync as o, mkdirSync as s, readFileSync as c, readdirSync as l, rmSync as u, statSync as d, writeFileSync as f } from "node:fs";
5
+ import { createRequire as p } from "node:module";
6
+ import { dirname as m, join as h, relative as g, resolve as _, sep as v } from "node:path";
7
+ import { fileURLToPath as y } from "node:url";
8
+ import ee from "@tailwindcss/vite";
9
+ import { spawn as b, spawnSync as x } from "node:child_process";
10
+ //#region src/vite/introspect-session.ts
11
+ var te = 262144e3, S = 3e4, C = 3e3;
12
+ function w(e) {
13
+ let t = m(y(import.meta.url));
14
+ return [h(t, `${e}.js`), h(t, `${e}.ts`)].find((e) => o(e)) ?? null;
15
+ }
16
+ function T(e) {
17
+ let t = w("introspect");
18
+ if (!t) return {
19
+ ok: !1,
20
+ reason: "introspect not found next to the sdk"
21
+ };
22
+ let n = x(process.execPath, [
23
+ "--conditions",
24
+ "browser",
25
+ t,
26
+ e
27
+ ], {
28
+ encoding: "utf-8",
29
+ timeout: S
30
+ });
31
+ if (n.error) return {
32
+ ok: !1,
33
+ reason: n.error.message
34
+ };
35
+ if (n.status !== 0) return {
36
+ ok: !1,
37
+ reason: (n.stderr || "").trim().split("\n")[0] || `exit ${n.status}`
38
+ };
39
+ try {
40
+ return {
41
+ ok: !0,
42
+ value: JSON.parse(n.stdout)
43
+ };
44
+ } catch {
45
+ return {
46
+ ok: !1,
47
+ reason: "introspect returned unparseable output"
48
+ };
49
+ }
50
+ }
51
+ function ne() {
52
+ return {
53
+ introspect: async (e) => T(e),
54
+ dispose: async () => {},
55
+ childPid: null
56
+ };
57
+ }
58
+ function re(e) {
59
+ if (process.env.GLASSHOME_WIDGET_NO_WORKER === "1") return ne();
60
+ let t = e?.rssLimitBytes ?? te, n = e?.timeoutMs ?? S, r = e?.slowMs ?? C, i = null, a = null, o = !1, s = !1, c = 0, l = Promise.resolve(), u = /* @__PURE__ */ new Map();
61
+ function d(e) {
62
+ for (let t of u.values()) t({
63
+ ok: !1,
64
+ reason: e
65
+ });
66
+ u.clear();
67
+ }
68
+ function f(e) {
69
+ let t = i;
70
+ i = null, a = null, o = !1, d(e), t?.kill();
71
+ }
72
+ function p(e) {
73
+ let n;
74
+ try {
75
+ n = JSON.parse(e);
76
+ } catch {
77
+ return;
78
+ }
79
+ if (typeof n.id != "number") return;
80
+ let i = u.get(n.id);
81
+ i && (u.delete(n.id), (n.rss ?? 0) > t && (o = !0), (n.importMs ?? 0) > r && console.warn(`[widget-sdk] introspection took ${n.importMs}ms`), i(n.ok ? {
82
+ ok: !0,
83
+ value: n.value
84
+ } : {
85
+ ok: !1,
86
+ reason: n.reason ?? "introspection failed"
87
+ }));
88
+ }
89
+ function m() {
90
+ if (i && a && !o) return a;
91
+ (o || i) && f("worker was replaced");
92
+ let e = w("introspect-worker");
93
+ if (!e) return Promise.reject(/* @__PURE__ */ Error("introspect-worker not found next to the sdk"));
94
+ let t = Date.now(), n = b(process.execPath, [
95
+ "--conditions",
96
+ "browser",
97
+ e
98
+ ], { stdio: [
99
+ "pipe",
100
+ "pipe",
101
+ "inherit"
102
+ ] });
103
+ i = n;
104
+ let s = new Promise((e, o) => {
105
+ let s = "", c = !1;
106
+ n.stdout?.setEncoding("utf-8"), n.stdout?.on("data", (n) => {
107
+ s += n;
108
+ let i = s.indexOf("\n");
109
+ for (; i >= 0;) {
110
+ let n = s.slice(0, i).trim();
111
+ if (s = s.slice(i + 1), n) {
112
+ if (!c && n.includes("\"ready\"")) {
113
+ c = !0;
114
+ let n = Date.now() - t;
115
+ n > r && console.warn(`[widget-sdk] introspect worker took ${n}ms to boot`), e();
116
+ } else p(n);
117
+ }
118
+ i = s.indexOf("\n");
119
+ }
120
+ });
121
+ let l = (e) => {
122
+ i === n && (i = null, a = null), d(e), c || o(Error(e));
123
+ };
124
+ n.on("error", (e) => l(e.message)), n.on("exit", (e, t) => l(`introspect worker exited (${t ?? `code ${e}`})`));
125
+ });
126
+ return a = s, s;
127
+ }
128
+ async function h(e) {
129
+ if (s) return {
130
+ ok: !1,
131
+ reason: "introspect session disposed"
132
+ };
133
+ try {
134
+ await m();
135
+ } catch (e) {
136
+ return {
137
+ ok: !1,
138
+ reason: e instanceof Error ? e.message : String(e)
139
+ };
140
+ }
141
+ let t = i;
142
+ if (!t?.stdin?.writable) return {
143
+ ok: !1,
144
+ reason: "introspect worker is not writable"
145
+ };
146
+ let r = ++c;
147
+ return new Promise((i) => {
148
+ let a = setTimeout(() => {
149
+ u.delete(r), f(`introspection timed out after ${n}ms`), i({
150
+ ok: !1,
151
+ reason: `introspection timed out after ${n}ms`
152
+ });
153
+ }, n);
154
+ u.set(r, (e) => {
155
+ clearTimeout(a), i(e);
156
+ });
157
+ try {
158
+ t.stdin?.write(`${JSON.stringify({
159
+ id: r,
160
+ outFile: e
161
+ })}\n`);
162
+ } catch (e) {
163
+ clearTimeout(a), u.delete(r), o = !0, i({
164
+ ok: !1,
165
+ reason: e instanceof Error ? e.message : String(e)
166
+ });
167
+ }
168
+ });
169
+ }
170
+ return {
171
+ introspect(e) {
172
+ let t = l.then(() => h(e));
173
+ return l = t.catch(() => void 0), t;
174
+ },
175
+ async dispose() {
176
+ s = !0, f("introspect session disposed");
177
+ },
178
+ get childPid() {
179
+ return i?.pid ?? null;
180
+ }
181
+ };
182
+ }
183
+ //#endregion
10
184
  //#region src/vite/index.ts
11
- function S(e) {
185
+ function E(e) {
12
186
  let t = JSON.stringify(e, Object.keys(e).sort());
13
- return o("sha256").update(t).digest("hex").slice(0, 16);
187
+ return a("sha256").update(t).digest("hex").slice(0, 16);
14
188
  }
15
- var C = 20;
16
- async function w(e, t) {
189
+ var D = 20;
190
+ async function O(e, t) {
17
191
  if (e === void 0) return;
18
192
  let n;
19
193
  try {
@@ -33,7 +207,7 @@ async function w(e, t) {
33
207
  let a = i.error.issues.map((e) => ` examples${e.path.length ? `[${e.path.join("][")}]` : ""}: ${e.message}`).join("\n");
34
208
  throw Error(`[widget-sdk] Invalid \`examples\` for "${t}":\n${a}\nEach example needs a \`config\` object and a \`size\` of whole positive tiles, and at most 20 are rendered.`);
35
209
  }
36
- function T(e, t) {
210
+ function k(e, t) {
37
211
  if (!e?.length) return;
38
212
  let n = e.map((e) => {
39
213
  let t = e.label ? `"${e.label}"` : `#${e.index}`;
@@ -41,42 +215,7 @@ function T(e, t) {
41
215
  }).join("\n");
42
216
  throw Error(`[widget-sdk] Example config rejected by "${t}"'s own configSchema:\n${n}\nEach example's \`config\` must be a config the widget would actually accept, or its preview renders empty.`);
43
217
  }
44
- function E(e) {
45
- let t = h(b(import.meta.url)), n = [g(t, "introspect.js"), g(t, "introspect.ts")].find((e) => s(e));
46
- if (!n) return {
47
- ok: !1,
48
- reason: `introspect not found next to ${t}`
49
- };
50
- let r = a(process.execPath, [
51
- "--conditions",
52
- "browser",
53
- n,
54
- e
55
- ], {
56
- encoding: "utf-8",
57
- timeout: 3e4
58
- });
59
- if (r.error) return {
60
- ok: !1,
61
- reason: r.error.message
62
- };
63
- if (r.status !== 0) return {
64
- ok: !1,
65
- reason: (r.stderr || "").trim().split("\n")[0] || `exit ${r.status}`
66
- };
67
- try {
68
- return {
69
- ok: !0,
70
- value: JSON.parse(r.stdout)
71
- };
72
- } catch {
73
- return {
74
- ok: !1,
75
- reason: "introspect returned unparseable output"
76
- };
77
- }
78
- }
79
- var D = [
218
+ var A = [
80
219
  "schema",
81
220
  "defaultConfig",
82
221
  "examples",
@@ -89,80 +228,80 @@ var D = [
89
228
  "minSize",
90
229
  "maxSize"
91
230
  ];
92
- function O(e, t, n) {
93
- let r = l(e, "utf-8"), i = JSON.parse(r), a = {
231
+ function j(e, t, n) {
232
+ let r = c(e, "utf-8"), i = JSON.parse(r), a = {
94
233
  ...t.manifest ?? {},
95
234
  schema: n
96
235
  };
97
- for (let e of D) a[e] !== void 0 && (i[e] = a[e]);
236
+ for (let e of A) a[e] !== void 0 && (i[e] = a[e]);
98
237
  let o = `${JSON.stringify(i, null, 2)}\n`;
99
- o !== r && p(e, o);
238
+ o !== r && f(e, o);
100
239
  }
101
- async function k(e) {
102
- if (!s(e.outFile)) return;
103
- let t = E(e.outFile);
240
+ async function M(e) {
241
+ if (!o(e.outFile)) return;
242
+ let t = e.session ? await e.session.introspect(e.outFile) : T(e.outFile);
104
243
  if (!t.ok) throw Error(`[widget-sdk] Could not read "${e.widgetName}" to check it: ${t.reason}\n Its examples and config schema could not be validated, so the build cannot continue.
105
244
  This usually means the bundle throws while being imported.`);
106
245
  let n = t.value, r = n.manifest?.name ?? e.widgetName;
107
- await w(n.manifest?.examples, r), T(n.exampleConfigIssues, r);
246
+ await O(n.manifest?.examples, r), k(n.exampleConfigIssues, r);
108
247
  let i = n.jsonSchema;
109
248
  if (!i) return;
110
- e.manifestPath && s(e.manifestPath) && O(e.manifestPath, n, i);
111
- let a = S(i), o = n.manifest?.configVersion ?? null, c = n.manifest?.name ?? e.widgetName;
112
- if (s(e.hashFile)) {
113
- let t = l(e.hashFile, "utf-8").trim(), n = null;
249
+ e.manifestPath && o(e.manifestPath) && j(e.manifestPath, n, i);
250
+ let a = E(i), s = n.manifest?.configVersion ?? null, l = n.manifest?.name ?? e.widgetName;
251
+ if (o(e.hashFile)) {
252
+ let t = c(e.hashFile, "utf-8").trim(), n = null;
114
253
  if (t.startsWith("{")) try {
115
254
  n = JSON.parse(t);
116
255
  } catch {
117
256
  n = null;
118
257
  }
119
258
  if (n) {
120
- if (n.hash !== a && n.configVersion === o) throw Error(`[widget-sdk] Config schema shape changed for "${c}" without a configVersion bump. Bump configVersion in defineWidget (currently ${o ?? "unset"}) or revert the schema change, then rebuild. Recorded in ${e.hashFile}`);
121
- } else t !== a && console.warn(`[widget-sdk] Schema shape changed for "${c}" — verify configVersion was bumped`);
259
+ if (n.hash !== a && n.configVersion === s) throw Error(`[widget-sdk] Config schema shape changed for "${l}" without a configVersion bump. Bump configVersion in defineWidget (currently ${s ?? "unset"}) or revert the schema change, then rebuild. Recorded in ${e.hashFile}`);
260
+ } else t !== a && console.warn(`[widget-sdk] Schema shape changed for "${l}" — verify configVersion was bumped`);
122
261
  }
123
- p(e.hashFile, `${JSON.stringify({
262
+ f(e.hashFile, `${JSON.stringify({
124
263
  hash: a,
125
- configVersion: o
264
+ configVersion: s
126
265
  })}\n`);
127
266
  }
128
- var A = "virtual:glasshome-widget", j = "\0virtual:glasshome-widget", M = "/@glasshome/preview";
129
- function N(e) {
130
- return y === "\\" ? e.split(y).join("/") : e;
267
+ var ie = "virtual:glasshome-widget", N = "\0virtual:glasshome-widget", P = "/@glasshome/preview";
268
+ function F(e) {
269
+ return v === "\\" ? e.split(v).join("/") : e;
131
270
  }
132
- function P() {
133
- let e = b(import.meta.url);
134
- return v(h(e), "../../preview");
271
+ function I() {
272
+ let e = y(import.meta.url);
273
+ return _(m(e), "../../preview");
135
274
  }
136
- function F(e) {
275
+ function L(e) {
137
276
  return r(e);
138
277
  }
139
- var I = /\.(ts|tsx|js|jsx)$/, L = /from\s+["']@glasshome\/sync-layer(\/[^"']*)?["']/;
140
- function R(e, t) {
141
- let n = N(e).split("?")[0] ?? "";
142
- return n.includes("/node_modules/") || !I.test(n) ? !1 : L.test(t);
278
+ var R = /\.(ts|tsx|js|jsx)$/, z = /from\s+["']@glasshome\/sync-layer(\/[^"']*)?["']/;
279
+ function B(e, t) {
280
+ let n = F(e).split("?")[0] ?? "";
281
+ return n.includes("/node_modules/") || !R.test(n) ? !1 : z.test(t);
143
282
  }
144
- function z() {
283
+ function V() {
145
284
  return {
146
285
  name: "glasshome-widget:sync-layer-guard",
147
286
  apply: "build",
148
287
  enforce: "pre",
149
288
  transform(e, t) {
150
- if (R(t, e)) throw Error(`Widgets must not import "@glasshome/sync-layer" directly (imported by ${N(_(process.cwd(), t.split("?")[0] ?? t))}). Import the equivalent hook from "@glasshome/widget-sdk" instead (e.g. useEntity, useEntities, useService).`);
289
+ if (B(t, e)) throw Error(`Widgets must not import "@glasshome/sync-layer" directly (imported by ${F(g(process.cwd(), t.split("?")[0] ?? t))}). Import the equivalent hook from "@glasshome/widget-sdk" instead (e.g. useEntity, useEntities, useService).`);
151
290
  return null;
152
291
  }
153
292
  };
154
293
  }
155
- function B(t, n) {
156
- let r = N(t).split("?")[0] ?? "";
157
- if (r.includes("/node_modules/") || !I.test(r)) return !1;
294
+ function H(t, n) {
295
+ let r = F(t).split("?")[0] ?? "";
296
+ if (r.includes("/node_modules/") || !R.test(r)) return !1;
158
297
  let i = e.find((e) => e.id === "direct-ui-import");
159
298
  return i?.sourcePattern ? new RegExp(i.sourcePattern).test(n) : !1;
160
299
  }
161
- function V(e = /* @__PURE__ */ new Set()) {
300
+ function U(e = /* @__PURE__ */ new Set()) {
162
301
  let t = /* @__PURE__ */ new Set();
163
302
  return {
164
303
  scan(e, n) {
165
- B(e, n) && t.add(N(_(process.cwd(), e.split("?")[0] ?? e)));
304
+ H(e, n) && t.add(F(g(process.cwd(), e.split("?")[0] ?? e)));
166
305
  },
167
306
  take() {
168
307
  let n = [...t].filter((t) => !e.has(t));
@@ -171,8 +310,8 @@ function V(e = /* @__PURE__ */ new Set()) {
171
310
  }
172
311
  };
173
312
  }
174
- function H(n) {
175
- let r = V(n);
313
+ function W(n) {
314
+ let r = U(n);
176
315
  return {
177
316
  name: "glasshome-widget:ui-import-guard",
178
317
  apply: "build",
@@ -188,15 +327,15 @@ function H(n) {
188
327
  }
189
328
  };
190
329
  }
191
- var U = "node_modules/.cache/glasshome-widgets";
192
- function W(e) {
330
+ var G = "node_modules/.cache/glasshome-widgets";
331
+ function K(e) {
193
332
  try {
194
- return m(g(process.cwd(), "package.json")).resolve(e);
333
+ return p(h(process.cwd(), "package.json")).resolve(e);
195
334
  } catch {
196
335
  return null;
197
336
  }
198
337
  }
199
- var G = [
338
+ var q = [
200
339
  "--radius",
201
340
  "--background",
202
341
  "--foreground",
@@ -206,74 +345,74 @@ var G = [
206
345
  "--muted",
207
346
  "--accent"
208
347
  ];
209
- function K(e, t) {
210
- let n = g(e, `${t}.css`);
211
- if (!s(n)) return;
212
- let r = l(n, "utf-8");
348
+ function J(e, t) {
349
+ let n = h(e, `${t}.css`);
350
+ if (!o(n)) return;
351
+ let r = c(n, "utf-8");
213
352
  for (let e of r.matchAll(/:host[^{]*\{([^}]*)\}/g)) {
214
- let n = e[1] ?? "", r = G.find((e) => n.includes(`${e}:`));
353
+ let n = e[1] ?? "", r = q.find((e) => n.includes(`${e}:`));
215
354
  if (r) throw Error(`[glasshome-widget] "${t}.css" defines theme variable "${r}" on :host. Theme variables must inherit from the document so widgets follow the household theme — never re-scope them into the widget's shadow root.`);
216
355
  }
217
356
  }
218
- function q(e, t, n) {
219
- let r = v(e, U);
220
- c(r, { recursive: !0 });
221
- let i = W("@glasshome/ui/styles");
357
+ function Y(e, t, n) {
358
+ let r = _(e, G);
359
+ s(r, { recursive: !0 });
360
+ let i = K("@glasshome/ui/styles");
222
361
  if (!i) throw Error("[widget-sdk] @glasshome/ui is required to build widgets (it provides the theme every GlassHome widget ships with). Add it to your widget's dependencies.");
223
- let a = v(h(i), "../.."), o = W("@glasshome/widget-sdk/tailwind-sources"), l = o ? h(o) : null, u = [
224
- g(a, "src"),
225
- g(a, "dist"),
226
- ...l ? [g(l, "src"), g(l, "dist")] : [],
227
- h(n)
228
- ].filter((e) => s(e)), d = v(r, `${t}.tailwind.css`);
229
- p(d, [
362
+ let a = _(m(i), "../.."), c = K("@glasshome/widget-sdk/tailwind-sources"), l = c ? m(c) : null, u = [
363
+ h(a, "src"),
364
+ h(a, "dist"),
365
+ ...l ? [h(l, "src"), h(l, "dist")] : [],
366
+ m(n)
367
+ ].filter((e) => o(e)), d = _(r, `${t}.tailwind.css`);
368
+ f(d, [
230
369
  "@import \"tailwindcss\" source(none);",
231
370
  "@import \"tw-animate-css\";",
232
371
  "@import \"@glasshome/ui/styles/theme\";",
233
- ...u.map((e) => `@source "${N(e)}";`),
372
+ ...u.map((e) => `@source "${F(e)}";`),
234
373
  "@custom-variant dark {",
235
374
  " &:is(.dark *) { @slot }",
236
375
  " :host(.dark) & { @slot }",
237
376
  "}",
238
377
  ""
239
378
  ].join("\n"));
240
- let f = v(r, `${t}.entry.ts`);
241
- return p(f, [
242
- `import "${N(d)}";`,
243
- `export { default } from "${N(n)}";`,
379
+ let p = _(r, `${t}.entry.ts`);
380
+ return f(p, [
381
+ `import "${F(d)}";`,
382
+ `export { default } from "${F(n)}";`,
244
383
  ""
245
- ].join("\n")), f;
384
+ ].join("\n")), p;
246
385
  }
247
- function J() {
248
- let e = x();
386
+ function X() {
387
+ let e = ee();
249
388
  for (let t of e) t.apply ??= "build";
250
389
  return e;
251
390
  }
252
- function Y(e) {
391
+ function Z(e) {
253
392
  let t = [];
254
- if (!s(e)) return t;
255
- for (let n of u(e)) {
256
- let r = v(e, n);
257
- if (!f(r).isDirectory()) continue;
258
- let i = v(r, "index.tsx"), a = v(r, "manifest.json");
259
- s(i) && s(a) && t.push({
393
+ if (!o(e)) return t;
394
+ for (let n of l(e)) {
395
+ let r = _(e, n);
396
+ if (!d(r).isDirectory()) continue;
397
+ let i = _(r, "index.tsx"), a = _(r, "manifest.json");
398
+ o(i) && o(a) && t.push({
260
399
  name: n,
261
400
  entry: i
262
401
  });
263
402
  }
264
403
  return t;
265
404
  }
266
- function X(e, t) {
405
+ function Q(e, t) {
267
406
  let r = [];
268
- if (s(e)) for (let a of u(e)) {
269
- if (!f(g(e, a)).isDirectory()) continue;
270
- let o = g(e, a, "manifest.json");
271
- if (!s(o)) continue;
272
- let c = i.safeParse(JSON.parse(l(o, "utf-8")));
273
- if (!c.success) throw Error(`[registry] Invalid manifest for "${a}": ${n(c.error)}`);
274
- let u = s(g(t, `${a}.css`));
407
+ if (o(e)) for (let a of l(e)) {
408
+ if (!d(h(e, a)).isDirectory()) continue;
409
+ let s = h(e, a, "manifest.json");
410
+ if (!o(s)) continue;
411
+ let l = i.safeParse(JSON.parse(c(s, "utf-8")));
412
+ if (!l.success) throw Error(`[registry] Invalid manifest for "${a}": ${n(l.error)}`);
413
+ let u = o(h(t, `${a}.css`));
275
414
  r.push({
276
- ...c.data,
415
+ ...l.data,
277
416
  bundleUrl: `./${a}.js`,
278
417
  ...u ? { cssUrl: `./${a}.css` } : {}
279
418
  });
@@ -284,10 +423,10 @@ function X(e, t) {
284
423
  baseUrl: "./",
285
424
  widgets: r
286
425
  };
287
- s(t) || c(t, { recursive: !0 }), p(g(t, "registry.json"), JSON.stringify(a, null, 2)), console.log(`[registry] Generated registry.json with ${r.length} widget(s)`);
426
+ o(t) || s(t, { recursive: !0 }), f(h(t, "registry.json"), JSON.stringify(a, null, 2)), console.log(`[registry] Generated registry.json with ${r.length} widget(s)`);
288
427
  }
289
- async function Z(e) {
290
- let { build: t } = await import("vite"), n = process.cwd(), r = v(n, e?.srcDir ?? "src"), i = v(n, e?.outDir ?? "dist"), a = Y(r);
428
+ async function $(e) {
429
+ let { build: t } = await import("vite"), n = process.cwd(), r = _(n, e?.srcDir ?? "src"), i = _(n, e?.outDir ?? "dist"), a = Z(r);
291
430
  if (a.length === 0) {
292
431
  console.warn("[glasshome-widgets] No widgets found in", r);
293
432
  return;
@@ -296,41 +435,42 @@ async function Z(e) {
296
435
  let t = new Set(e.only);
297
436
  a = a.filter((e) => t.has(e.name));
298
437
  }
299
- e?.only || s(i) && d(i, { recursive: !0 }), s(i) || c(i, { recursive: !0 });
300
- let o = /* @__PURE__ */ new Set();
438
+ e?.only || o(i) && u(i, { recursive: !0 }), o(i) || s(i, { recursive: !0 });
439
+ let c = /* @__PURE__ */ new Set();
301
440
  for (let r of a) await t({
302
441
  configFile: !1,
303
442
  root: n,
304
443
  plugins: [
305
- ...J(),
444
+ ...X(),
306
445
  ...e?.plugins ?? [],
307
- z(),
308
- H(o)
446
+ V(),
447
+ W(c)
309
448
  ],
310
449
  ...e?.viteConfig,
311
450
  build: {
312
451
  lib: {
313
- entry: q(n, r.name, r.entry),
452
+ entry: Y(n, r.name, r.entry),
314
453
  formats: ["es"],
315
454
  fileName: r.name,
316
455
  cssFileName: r.name
317
456
  },
318
- rollupOptions: { external: F },
457
+ rollupOptions: { external: L },
319
458
  outDir: i,
320
459
  emptyOutDir: !1,
321
460
  copyPublicDir: !1,
322
461
  ...e?.viteConfig?.build
323
462
  },
324
463
  logLevel: "warn"
325
- }), K(i, r.name), await k({
326
- outFile: g(i, `${r.name}.js`),
327
- hashFile: g(h(r.entry), ".schema-hash"),
464
+ }), J(i, r.name), await M({
465
+ outFile: h(i, `${r.name}.js`),
466
+ hashFile: h(m(r.entry), ".schema-hash"),
328
467
  widgetName: r.name,
329
- manifestPath: g(h(r.entry), "manifest.json")
468
+ manifestPath: h(m(r.entry), "manifest.json"),
469
+ session: e?.session
330
470
  });
331
- X(r, i);
471
+ Q(r, i);
332
472
  }
333
- function Q(e) {
473
+ function ae(e) {
334
474
  let t = e?.entry ?? "src/index.tsx", n = {
335
475
  name: "glasshome-widget:build",
336
476
  apply: "build",
@@ -338,12 +478,12 @@ function Q(e) {
338
478
  let e = process.cwd();
339
479
  return { build: {
340
480
  lib: {
341
- entry: q(e, "index", v(e, t)),
481
+ entry: Y(e, "index", _(e, t)),
342
482
  formats: ["es"],
343
483
  fileName: "index",
344
484
  cssFileName: "index"
345
485
  },
346
- rollupOptions: { external: F }
486
+ rollupOptions: { external: L }
347
487
  } };
348
488
  }
349
489
  }, r = {
@@ -351,18 +491,18 @@ function Q(e) {
351
491
  apply: "serve",
352
492
  enforce: "pre",
353
493
  config() {
354
- let e = P(), t = process.cwd();
494
+ let e = I(), t = process.cwd();
355
495
  return { server: { fs: { allow: [
356
496
  e,
357
497
  t,
358
- v(t, "..", "..")
498
+ _(t, "..", "..")
359
499
  ] } } };
360
500
  },
361
501
  configureServer(e) {
362
- let t = P(), n = g(t, "preview.html");
502
+ let t = I(), n = h(t, "preview.html");
363
503
  e.middlewares.use(async (t, r, i) => {
364
504
  if (t.url === "/" || t.url === "/index.html") try {
365
- let i = l(n, "utf-8");
505
+ let i = c(n, "utf-8");
366
506
  i = await e.transformIndexHtml(t.url, i), r.setHeader("Content-Type", "text/html"), r.statusCode = 200, r.end(i);
367
507
  } catch (e) {
368
508
  i(e);
@@ -371,37 +511,37 @@ function Q(e) {
371
511
  });
372
512
  },
373
513
  resolveId(e) {
374
- if (e === M) {
375
- let e = P();
376
- return N(g(e, "host.tsx"));
514
+ if (e === P) {
515
+ let e = I();
516
+ return F(h(e, "host.tsx"));
377
517
  }
378
- if (e === A) return j;
518
+ if (e === ie) return N;
379
519
  },
380
520
  load(e) {
381
- if (e === j) return `export { default } from "${N(v(process.cwd(), t))}";`;
521
+ if (e === N) return `export { default } from "${F(_(process.cwd(), t))}";`;
382
522
  }
383
523
  }, i = {
384
524
  name: "glasshome-widget:schema",
385
525
  apply: "build",
386
526
  async closeBundle() {
387
- K(v(process.cwd(), "dist"), "index"), await k({
388
- outFile: v(process.cwd(), "dist", "index.js"),
389
- hashFile: v(process.cwd(), ".schema-hash"),
527
+ J(_(process.cwd(), "dist"), "index"), await M({
528
+ outFile: _(process.cwd(), "dist", "index.js"),
529
+ hashFile: _(process.cwd(), ".schema-hash"),
390
530
  widgetName: "index",
391
- manifestPath: v(process.cwd(), "manifest.json")
531
+ manifestPath: _(process.cwd(), "manifest.json")
392
532
  });
393
533
  }
394
534
  };
395
535
  return [
396
- ...J(),
536
+ ...X(),
397
537
  n,
398
- z(),
399
- H(),
538
+ V(),
539
+ W(),
400
540
  i,
401
541
  r
402
542
  ];
403
543
  }
404
- function $(e) {
544
+ function oe(e) {
405
545
  let t = e?.srcDir ?? "src", n = e?.outDir ?? "dist", r = [];
406
546
  return [{
407
547
  name: "glasshome-widgets:build",
@@ -420,7 +560,7 @@ function $(e) {
420
560
  if (e === "\0glasshome-noop") return "export {}";
421
561
  },
422
562
  async closeBundle() {
423
- await Z({
563
+ await $({
424
564
  srcDir: t,
425
565
  outDir: n,
426
566
  plugins: r
@@ -429,4 +569,4 @@ function $(e) {
429
569
  }];
430
570
  }
431
571
  //#endregion
432
- export { C as MAX_EXAMPLES, Z as buildWidgets, V as createUiImportTracker, Y as discoverWidgets, X as generateRegistry, Q as glasshomeWidget, $ as glasshomeWidgets, R as isDirectSyncLayerImportSource, B as isDirectUiImportSource, F as isWidgetExternal, k as runSchemaGuard };
572
+ export { D as MAX_EXAMPLES, $ as buildWidgets, re as createIntrospectSession, U as createUiImportTracker, Z as discoverWidgets, Q as generateRegistry, ae as glasshomeWidget, oe as glasshomeWidgets, T as introspectOnce, B as isDirectSyncLayerImportSource, H as isDirectUiImportSource, L as isWidgetExternal, M as runSchemaGuard };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Reads a built widget bundle's definition under a fake DOM.
3
+ *
4
+ * A widget bundle cannot be imported by the build process: `solid-js/web`
5
+ * resolves to its server build under Node's conditions and throws the moment
6
+ * the module initialises, and browser conditions then need a DOM for the
7
+ * templates Solid creates at import time. Both callers (the one-shot
8
+ * `introspect.ts` CLI and the persistent `introspect-worker.ts`) run under
9
+ * `--conditions browser` and install a DOM before importing.
10
+ */
11
+ export interface ExampleConfigIssue {
12
+ index: number;
13
+ label?: string;
14
+ problems: string[];
15
+ }
16
+ export interface WidgetIntrospection {
17
+ manifest: ({
18
+ name?: string;
19
+ configVersion?: number;
20
+ examples?: unknown;
21
+ } & Record<string, unknown>) | null;
22
+ /** JSON Schema derived from the widget's configSchema, when it has one. */
23
+ jsonSchema: object | null;
24
+ /** Examples whose config the widget's own configSchema rejects. */
25
+ exampleConfigIssues: ExampleConfigIssue[];
26
+ }
27
+ /**
28
+ * Install a fresh DOM. Unconditional: the persistent worker calls this per
29
+ * request, and skipping already-set keys would leak the previous widget's
30
+ * mutated window into the next one.
31
+ */
32
+ export declare function installDom(): Promise<void>;
33
+ export declare function introspectBundle(bundlePath: string, cacheBust?: string): Promise<WidgetIntrospection>;
@@ -0,0 +1,30 @@
1
+ import type { WidgetIntrospection } from "./introspect-core";
2
+ export type IntrospectResult = {
3
+ ok: true;
4
+ value: WidgetIntrospection;
5
+ } | {
6
+ ok: false;
7
+ reason: string;
8
+ };
9
+ export interface IntrospectSessionOptions {
10
+ /** Respawn once the worker's RSS crosses this, between requests only. */
11
+ rssLimitBytes?: number;
12
+ /** Per-request ceiling; a module-eval hang can only be cured by a kill. */
13
+ timeoutMs?: number;
14
+ /** Log spawn and import durations above this. */
15
+ slowMs?: number;
16
+ }
17
+ export interface IntrospectSession {
18
+ introspect(outFile: string): Promise<IntrospectResult>;
19
+ dispose(): Promise<void>;
20
+ readonly childPid: number | null;
21
+ }
22
+ /** One process per check. The path `build`, `capture` and the fallback take. */
23
+ export declare function introspectOnce(outFile: string): IntrospectResult;
24
+ /**
25
+ * A worker reused across rebuilds, for callers that own a session.
26
+ *
27
+ * Only `bun widget connect`'s watch loop does: it is the one call site that
28
+ * re-pays the per-check process boot tax on every save.
29
+ */
30
+ export declare function createIntrospectSession(options?: IntrospectSessionOptions): IntrospectSession;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Long-lived introspection worker for `bun widget connect`.
3
+ *
4
+ * Newline-delimited JSON over stdio. Every request carries an id: a widget
5
+ * bundle can print at module-eval time, and without id pairing one stray line
6
+ * offsets every later response.
7
+ *
8
+ * Usage: bun --conditions browser introspect-worker.js
9
+ */
10
+ export {};
@@ -0,0 +1,76 @@
1
+ import { n as e, t } from "../introspect-core-DYv9Uufi.js";
2
+ //#region src/vite/introspect-worker.ts
3
+ var n = process.stdout.write.bind(process.stdout);
4
+ process.stdout.write = ((e, ...t) => process.stderr.write(e, ...t));
5
+ function r(e) {
6
+ n(`${JSON.stringify(e)}\n`);
7
+ }
8
+ function i(e) {
9
+ return e instanceof Error ? e.message : String(e);
10
+ }
11
+ var a = null, o = 0, s = Promise.resolve();
12
+ async function c(n, s) {
13
+ a = n;
14
+ let c = performance.now();
15
+ try {
16
+ await t(), r({
17
+ id: n,
18
+ ok: !0,
19
+ value: await e(s, String(++o)),
20
+ rss: process.memoryUsage().rss,
21
+ importMs: Math.round(performance.now() - c)
22
+ });
23
+ } catch (e) {
24
+ r({
25
+ id: n,
26
+ ok: !1,
27
+ reason: i(e),
28
+ rss: process.memoryUsage().rss,
29
+ importMs: Math.round(performance.now() - c)
30
+ });
31
+ } finally {
32
+ a = null;
33
+ }
34
+ }
35
+ function l(e) {
36
+ let t;
37
+ try {
38
+ t = JSON.parse(e);
39
+ } catch {
40
+ return;
41
+ }
42
+ if (typeof t.id != "number" || typeof t.outFile != "string") return;
43
+ let { id: n, outFile: r } = t;
44
+ s = s.then(() => c(n, r));
45
+ }
46
+ function u(e) {
47
+ a !== null && r({
48
+ id: a,
49
+ ok: !1,
50
+ reason: i(e),
51
+ rss: process.memoryUsage().rss,
52
+ importMs: 0
53
+ }), process.exit(1);
54
+ }
55
+ process.on("uncaughtException", u), process.on("unhandledRejection", u);
56
+ var d = "";
57
+ process.stdin.setEncoding("utf-8"), process.stdin.on("data", (e) => {
58
+ d += e;
59
+ let t = d.indexOf("\n");
60
+ for (; t >= 0;) {
61
+ let e = d.slice(0, t).trim();
62
+ d = d.slice(t + 1), e && l(e), t = d.indexOf("\n");
63
+ }
64
+ }), process.stdin.on("end", () => process.exit(0)), process.stdin.on("close", () => process.exit(0)), process.stdin.resume();
65
+ var f = process.ppid;
66
+ setInterval(() => {
67
+ try {
68
+ process.kill(f, 0);
69
+ } catch {
70
+ process.exit(0);
71
+ }
72
+ }, 5e3), r({
73
+ id: 0,
74
+ ready: !0
75
+ });
76
+ //#endregion
@@ -1,21 +1,12 @@
1
1
  /**
2
2
  * Reads a built widget bundle's definition and prints it as JSON.
3
3
  *
4
- * Runs as its own process because a widget bundle cannot be imported by the
5
- * build: `solid-js/web` resolves to its server build under Node's conditions
6
- * and throws "Client-only API called on the server side" the moment the module
7
- * initialises, and forcing browser conditions then needs a DOM for the
8
- * templates Solid creates at import time. The build spawns this with
9
- * `--conditions browser` and a DOM in place, so the import succeeds.
10
- *
11
- * Until this existed the guard swallowed that import failure and silently did
12
- * nothing — for every widget, on every build.
4
+ * One-shot counterpart to introspect-worker.ts: a fresh process per check, used
5
+ * by `bun widget build`, the render worker's capture, and as the
6
+ * GLASSHOME_WIDGET_NO_WORKER fallback.
13
7
  *
14
8
  * Usage: bun --conditions browser introspect.js <bundle.js>
15
- * Prints: { manifest, jsonSchema } on stdout, or exits non-zero with a reason.
9
+ * Prints: { manifest, jsonSchema, exampleConfigIssues } on stdout, or exits
10
+ * non-zero with a reason.
16
11
  */
17
- export interface ExampleConfigIssue {
18
- index: number;
19
- label?: string;
20
- problems: string[];
21
- }
12
+ export {};
@@ -1,60 +1,10 @@
1
- import { pathToFileURL as e } from "node:url";
1
+ import { n as e, t } from "../introspect-core-DYv9Uufi.js";
2
2
  //#region src/vite/introspect.ts
3
- function t(e) {
4
- return typeof e?.safeParse == "function";
3
+ async function n() {
4
+ let n = process.argv[2];
5
+ n || (process.stderr.write("introspect: no bundle path given\n"), process.exit(2)), await t(), process.stdout.write(JSON.stringify(await e(n)));
5
6
  }
6
- function n(e, n) {
7
- if (!t(e) || !Array.isArray(n)) return [];
8
- let r = [];
9
- for (let [t, i] of n.entries()) {
10
- let n = i?.config;
11
- if (typeof n != "object" || !n) continue;
12
- let a = e.safeParse(n);
13
- a.success || r.push({
14
- index: t,
15
- label: i.label,
16
- problems: (a.error?.issues ?? []).map((e) => {
17
- let t = e.path.map(String).join(".");
18
- return t ? `${t}: ${e.message}` : e.message;
19
- })
20
- });
21
- }
22
- return r;
23
- }
24
- async function r() {
25
- let { Window: e } = await import("happy-dom"), t = new e({ url: "http://localhost" });
26
- for (let e of [
27
- "window",
28
- "document",
29
- "navigator",
30
- "location",
31
- "customElements",
32
- "HTMLElement",
33
- "Element",
34
- "Node",
35
- "CSS",
36
- "getComputedStyle",
37
- "requestAnimationFrame",
38
- "cancelAnimationFrame",
39
- "matchMedia"
40
- ]) globalThis[e] === void 0 && (globalThis[e] = t[e]);
41
- }
42
- async function i() {
43
- let t = process.argv[2];
44
- t || (process.stderr.write("introspect: no bundle path given\n"), process.exit(2)), await r();
45
- let i = (await import(e(t).href)).default, a = null;
46
- if (i?.configSchema) {
47
- let { z: e } = await import("zod");
48
- a = e.toJSONSchema(i.configSchema, { unrepresentable: "any" });
49
- }
50
- let o = {
51
- manifest: i?.manifest ?? null,
52
- jsonSchema: a,
53
- exampleConfigIssues: n(i?.configSchema, i?.manifest?.examples)
54
- };
55
- process.stdout.write(JSON.stringify(o));
56
- }
57
- i().catch((e) => {
7
+ n().catch((e) => {
58
8
  process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`), process.exit(1);
59
9
  });
60
10
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/widget-sdk",
3
- "version": "1.10.3",
3
+ "version": "1.11.0",
4
4
  "description": "SDK for building GlassHome dashboard widgets with SolidJS",
5
5
  "keywords": [
6
6
  "glasshome",