@glasshome/widget-cli 0.4.14 → 0.5.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/package.json +3 -2
- package/src/commands/connect.ts +28 -1
- package/src/commands/publish.ts +32 -1
- package/src/commands/validate.ts +17 -10
- package/src/utils/hub-api.ts +11 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/widget-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI for creating and managing GlassHome widgets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@clack/prompts": "^0.10.0",
|
|
24
|
-
"@glasshome/widget-
|
|
24
|
+
"@glasshome/widget-contract": "0.1.0",
|
|
25
|
+
"@glasshome/widget-sdk": "1.0.0",
|
|
25
26
|
"open": "^11.0.0",
|
|
26
27
|
"semver": "^7.6.0",
|
|
27
28
|
"vite-plugin-solid": "^2.11.0"
|
package/src/commands/connect.ts
CHANGED
|
@@ -52,8 +52,28 @@ async function uploadAndRegister(
|
|
|
52
52
|
throw new Error(`Failed to upload bundle for ${slug}: HTTP ${uploadRes.status}`);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Widgets render in shadow roots and need their own stylesheet uploaded
|
|
56
|
+
// alongside the bundle.
|
|
57
|
+
const cssPath = resolve(distDir, `${slug}.css`);
|
|
58
|
+
let cssUrl: string | undefined;
|
|
59
|
+
if (existsSync(cssPath)) {
|
|
60
|
+
const cssRes = await fetch(`${api}/bundles/local/local/${slug}`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
"Content-Type": "text/css",
|
|
64
|
+
Authorization: `Bearer ${token}`,
|
|
65
|
+
},
|
|
66
|
+
body: readFileSync(cssPath, "utf-8"),
|
|
67
|
+
});
|
|
68
|
+
if (!cssRes.ok) {
|
|
69
|
+
throw new Error(`Failed to upload stylesheet for ${slug}: HTTP ${cssRes.status}`);
|
|
70
|
+
}
|
|
71
|
+
cssUrl = `/bundles/local/local/${slug}/bundle.css`;
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
const manifest = { ...widget };
|
|
56
75
|
delete (manifest as Record<string, unknown>).bundleUrl;
|
|
76
|
+
delete (manifest as Record<string, unknown>).cssUrl;
|
|
57
77
|
|
|
58
78
|
await trpcMutate({
|
|
59
79
|
apiUrl: api,
|
|
@@ -64,6 +84,7 @@ async function uploadAndRegister(
|
|
|
64
84
|
name: slug,
|
|
65
85
|
version: widget.version,
|
|
66
86
|
bundleUrl: `/bundles/local/local/${slug}/bundle.js`,
|
|
87
|
+
...(cssUrl ? { cssUrl } : {}),
|
|
67
88
|
manifestJson: JSON.stringify(manifest),
|
|
68
89
|
},
|
|
69
90
|
});
|
|
@@ -94,7 +115,13 @@ export async function runConnect(
|
|
|
94
115
|
): Promise<void> {
|
|
95
116
|
const distDir = resolve(cwd, "dist");
|
|
96
117
|
const solid = (await import("vite-plugin-solid")).default;
|
|
97
|
-
|
|
118
|
+
// delegateEvents: false — widgets run in closed shadow roots where Solid's
|
|
119
|
+
// document-level event delegation cannot see the target.
|
|
120
|
+
const buildOpts = {
|
|
121
|
+
srcDir: "src",
|
|
122
|
+
outDir: "dist",
|
|
123
|
+
plugins: [solid({ solid: { delegateEvents: false } })],
|
|
124
|
+
};
|
|
98
125
|
|
|
99
126
|
// Step 1: Initial build
|
|
100
127
|
const s = spinner();
|
package/src/commands/publish.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { cancel, isCancel, log, select, spinner } from "@clack/prompts";
|
|
5
5
|
import semver from "semver";
|
|
@@ -192,6 +192,17 @@ export async function runPublish(
|
|
|
192
192
|
|
|
193
193
|
const sha256Hash = createHash("sha256").update(bundleBuffer).digest("hex");
|
|
194
194
|
|
|
195
|
+
// Optional stylesheet emitted next to the bundle; published alongside it
|
|
196
|
+
// so the Hub can serve and integrity-check both.
|
|
197
|
+
const cssPath = resolve(cwd, "dist", `${widgetName}.css`);
|
|
198
|
+
let cssBuffer: Buffer | null = null;
|
|
199
|
+
if (existsSync(cssPath)) {
|
|
200
|
+
cssBuffer = Buffer.from(readFileSync(cssPath));
|
|
201
|
+
}
|
|
202
|
+
const cssSha256Hash = cssBuffer
|
|
203
|
+
? createHash("sha256").update(cssBuffer).digest("hex")
|
|
204
|
+
: undefined;
|
|
205
|
+
|
|
195
206
|
s.start(`Publishing ${manifest.name}@${version}...`);
|
|
196
207
|
|
|
197
208
|
let publishData: Awaited<ReturnType<typeof requestPublish>>;
|
|
@@ -208,6 +219,9 @@ export async function runPublish(
|
|
|
208
219
|
version,
|
|
209
220
|
bundleSize: bundleBuffer.byteLength,
|
|
210
221
|
sha256Hash,
|
|
222
|
+
...(cssBuffer && cssSha256Hash
|
|
223
|
+
? { cssSize: cssBuffer.byteLength, cssSha256Hash }
|
|
224
|
+
: {}),
|
|
211
225
|
manifestJson: JSON.stringify(manifest),
|
|
212
226
|
});
|
|
213
227
|
} catch (err: any) {
|
|
@@ -227,11 +241,28 @@ export async function runPublish(
|
|
|
227
241
|
process.exit(1);
|
|
228
242
|
}
|
|
229
243
|
|
|
244
|
+
if (cssBuffer) {
|
|
245
|
+
if (!publishData.cssUploadUrl) {
|
|
246
|
+
s.stop(
|
|
247
|
+
`dist/${widgetName}.css exists but the Hub did not return a css upload URL — update the Hub or remove the stylesheet`,
|
|
248
|
+
);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
s.message(`Uploading ${manifest.name} styles to CDN...`);
|
|
252
|
+
try {
|
|
253
|
+
await uploadToR2(publishData.cssUploadUrl, cssBuffer, "text/css");
|
|
254
|
+
} catch (err: any) {
|
|
255
|
+
s.stop(`CSS upload failed: ${err.message}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
230
260
|
s.message(`Confirming ${manifest.name}...`);
|
|
231
261
|
try {
|
|
232
262
|
const result = await confirmPublish(hubUrl, token!, publishData.versionId);
|
|
233
263
|
s.stop(`Published ${manifest.name}@${version}`);
|
|
234
264
|
log.info(`CDN: ${result.bundleUrl}`);
|
|
265
|
+
if (result.cssUrl) log.info(`CSS: ${result.cssUrl}`);
|
|
235
266
|
} catch (err: any) {
|
|
236
267
|
s.stop(`Confirmation failed: ${err.message}`);
|
|
237
268
|
process.exit(1);
|
package/src/commands/validate.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { log, spinner } from "@clack/prompts";
|
|
4
|
+
import {
|
|
5
|
+
formatSchemaError,
|
|
6
|
+
publishManifestSchema,
|
|
7
|
+
requiresCapabilities,
|
|
8
|
+
} from "@glasshome/widget-contract";
|
|
4
9
|
import {
|
|
5
10
|
discoverWidgets,
|
|
6
11
|
formatBytes,
|
|
@@ -28,9 +33,17 @@ function validateManifest(manifest: WidgetManifest): ValidationResult {
|
|
|
28
33
|
typeof manifest.maxSize.w === "number" &&
|
|
29
34
|
typeof manifest.maxSize.h === "number";
|
|
30
35
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
if (!
|
|
36
|
+
// Canonical publish-time validation shared with the Hub.
|
|
37
|
+
const schemaResult = publishManifestSchema.safeParse(manifest);
|
|
38
|
+
if (!schemaResult.success) {
|
|
39
|
+
errors.push(`Manifest schema: ${formatSchemaError(schemaResult.error)}`);
|
|
40
|
+
const capabilities = (manifest as { capabilities?: unknown }).capabilities;
|
|
41
|
+
if (manifest.sdkVersion && requiresCapabilities(manifest.sdkVersion) && capabilities === undefined) {
|
|
42
|
+
errors.push(
|
|
43
|
+
`sdkVersion "${manifest.sdkVersion}" only admits SDK >= 1.0.0, so manifest.json must declare "capabilities" — use [] if the widget never reads or controls Home Assistant`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
34
47
|
|
|
35
48
|
// INS-05: "*" sdkVersion is rejected at publish time — authors must declare
|
|
36
49
|
// a real range (e.g. "^0.3.0"). Installed widgets that already ship "*" are
|
|
@@ -42,13 +55,7 @@ function validateManifest(manifest: WidgetManifest): ValidationResult {
|
|
|
42
55
|
);
|
|
43
56
|
}
|
|
44
57
|
|
|
45
|
-
// Size validation
|
|
46
|
-
if (!hasValidMinSize) {
|
|
47
|
-
errors.push("minSize must be an object with numeric w and h properties");
|
|
48
|
-
}
|
|
49
|
-
if (!hasValidMaxSize) {
|
|
50
|
-
errors.push("maxSize must be an object with numeric w and h properties");
|
|
51
|
-
}
|
|
58
|
+
// Size validation (cross-field checks the schema does not cover)
|
|
52
59
|
if (
|
|
53
60
|
manifest.defaultSize &&
|
|
54
61
|
(typeof manifest.defaultSize.w !== "number" || typeof manifest.defaultSize.h !== "number")
|
package/src/utils/hub-api.ts
CHANGED
|
@@ -29,6 +29,8 @@ interface PublishRequestParams {
|
|
|
29
29
|
version: string;
|
|
30
30
|
bundleSize: number;
|
|
31
31
|
sha256Hash: string;
|
|
32
|
+
cssSize?: number;
|
|
33
|
+
cssSha256Hash?: string;
|
|
32
34
|
manifestJson: string;
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -37,11 +39,14 @@ interface PublishRequestResponse {
|
|
|
37
39
|
cdnUrl: string;
|
|
38
40
|
widgetId: string;
|
|
39
41
|
versionId: string;
|
|
42
|
+
cssUploadUrl?: string;
|
|
43
|
+
cssUrl?: string;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
interface PublishConfirmResponse {
|
|
43
47
|
success: boolean;
|
|
44
48
|
bundleUrl: string;
|
|
49
|
+
cssUrl?: string;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
export async function requestPublish(
|
|
@@ -107,10 +112,14 @@ export async function confirmPublish(
|
|
|
107
112
|
return res.json() as Promise<PublishConfirmResponse>;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
export async function uploadToR2(
|
|
115
|
+
export async function uploadToR2(
|
|
116
|
+
uploadUrl: string,
|
|
117
|
+
bundleBuffer: Uint8Array,
|
|
118
|
+
contentType: string = "application/javascript",
|
|
119
|
+
): Promise<void> {
|
|
111
120
|
const res = await fetch(uploadUrl, {
|
|
112
121
|
method: "PUT",
|
|
113
|
-
headers: { "Content-Type":
|
|
122
|
+
headers: { "Content-Type": contentType },
|
|
114
123
|
body: bundleBuffer as unknown as BodyInit,
|
|
115
124
|
});
|
|
116
125
|
|