@glasshome/widget-cli 0.8.4 → 0.10.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/bin/cli.ts +13 -0
- package/package.json +22 -3
- package/src/commands/add.ts +13 -2
- package/src/commands/build.ts +7 -0
- package/src/commands/create.ts +4 -0
- package/src/commands/preview.ts +100 -0
- package/src/commands/publish.ts +21 -7
- package/src/commands/upgrade.ts +25 -10
- package/src/commands/validate.ts +22 -18
- package/src/preview/capture.ts +259 -0
- package/src/preview/constraints.ts +240 -0
- package/src/preview/harness/harness.tsx +169 -0
- package/src/preview/harness/index.html +28 -0
- package/src/preview/nix-ld.ts +49 -0
- package/src/utils/resolution.ts +22 -0
- package/src/utils/sdk-version.test.ts +85 -0
- package/src/utils/sdk-version.ts +76 -0
- package/src/utils/typecheck.test.ts +128 -0
- package/src/utils/typecheck.ts +84 -0
- package/template/.gitignore.template +2 -0
- package/template/README.md.template +6 -1
- package/template/package.json.template +3 -2
- package/template/src/index.tsx.template +1 -1
- package/src/commands/dev-registry.ts +0 -86
package/bin/cli.ts
CHANGED
|
@@ -27,6 +27,8 @@ Commands:
|
|
|
27
27
|
build Build all widgets (per-widget, self-contained bundles)
|
|
28
28
|
connect <url> Connect to a running dashboard for live testing
|
|
29
29
|
--re-auth discards stored credentials and re-runs device approval
|
|
30
|
+
preview [name...] Screenshot widgets' examples (light + dark) into preview/
|
|
31
|
+
--isolate gives every render its own browser process
|
|
30
32
|
validate [name] Validate all widgets or a specific one
|
|
31
33
|
publish [hub-url] Build and publish a widget to Hub
|
|
32
34
|
login [hub-url] Authenticate with GlassHome Hub
|
|
@@ -42,6 +44,8 @@ Examples:
|
|
|
42
44
|
glasshome-widget add
|
|
43
45
|
glasshome-widget build
|
|
44
46
|
glasshome-widget connect http://localhost:3333
|
|
47
|
+
glasshome-widget preview
|
|
48
|
+
glasshome-widget preview clock
|
|
45
49
|
glasshome-widget validate
|
|
46
50
|
glasshome-widget validate clock
|
|
47
51
|
glasshome-widget info
|
|
@@ -65,6 +69,7 @@ const { values: flags, positionals } = parseArgs({
|
|
|
65
69
|
bump: { type: "string" },
|
|
66
70
|
scope: { type: "string" },
|
|
67
71
|
"re-auth": { type: "boolean" },
|
|
72
|
+
isolate: { type: "boolean" },
|
|
68
73
|
dry: { type: "boolean" },
|
|
69
74
|
help: { type: "boolean", short: "h" },
|
|
70
75
|
},
|
|
@@ -134,6 +139,14 @@ switch (effectiveCommand) {
|
|
|
134
139
|
break;
|
|
135
140
|
}
|
|
136
141
|
|
|
142
|
+
case "preview": {
|
|
143
|
+
const widgetDir = resolveWidgetDir();
|
|
144
|
+
await notifySdkUpdate(widgetDir);
|
|
145
|
+
const { runPreview } = await import("../src/commands/preview");
|
|
146
|
+
await runPreview(widgetDir, args, flags.isolate === true);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
|
|
137
150
|
case "validate": {
|
|
138
151
|
const widgetName = args[0];
|
|
139
152
|
const widgetDir = resolveWidgetDir();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/widget-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "CLI for creating and managing GlassHome widgets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"cli": "bun bin/cli.ts",
|
|
11
|
+
"test": "bun test",
|
|
11
12
|
"typecheck": "tsc --noEmit"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
@@ -16,19 +17,37 @@
|
|
|
16
17
|
"template"
|
|
17
18
|
],
|
|
18
19
|
"devDependencies": {
|
|
20
|
+
"@iconify/types": "^2.0.0",
|
|
19
21
|
"@types/bun": "latest",
|
|
20
|
-
"@types/semver": "^7.0.0"
|
|
22
|
+
"@types/semver": "^7.0.0",
|
|
23
|
+
"playwright": "^1.40.0",
|
|
24
|
+
"typescript": "^5"
|
|
21
25
|
},
|
|
22
26
|
"dependencies": {
|
|
23
27
|
"@clack/prompts": "^1.6.0",
|
|
28
|
+
"@fontsource-variable/caveat": "^5.2.8",
|
|
29
|
+
"@fontsource-variable/geist": "^5.2.9",
|
|
30
|
+
"@fontsource-variable/geist-mono": "^5.2.8",
|
|
31
|
+
"@glasshome/sync-layer": "0.4.0",
|
|
24
32
|
"@glasshome/widget-contract": "0.2.0",
|
|
25
|
-
"@glasshome/widget-sdk": "1.
|
|
33
|
+
"@glasshome/widget-sdk": "1.8.1",
|
|
34
|
+
"@iconify-json/lucide": "^1.2.0",
|
|
35
|
+
"@iconify-json/mdi": "^1.2.3",
|
|
36
|
+
"@tailwindcss/vite": "^4.1.13",
|
|
26
37
|
"open": "^11.0.0",
|
|
27
38
|
"picocolors": "^1.1.1",
|
|
28
39
|
"semver": "^7.6.0",
|
|
29
40
|
"ts-morph": "^28.0.0",
|
|
30
41
|
"vite-plugin-solid": "^2.11.0"
|
|
31
42
|
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"playwright": "^1.40.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"playwright": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
32
51
|
"repository": {
|
|
33
52
|
"type": "git",
|
|
34
53
|
"url": "https://github.com/glasshome/widget-cli.git"
|
package/src/commands/add.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { cancel, isCancel, log, text } from "@clack/prompts";
|
|
4
|
+
import { defaultSdkRange, FALLBACK_SDK_RANGE } from "../utils/sdk-version";
|
|
4
5
|
|
|
5
6
|
/** Capitalize kebab-case into PascalCase for component names. */
|
|
6
7
|
function capitalize(str: string): string {
|
|
@@ -30,20 +31,30 @@ export function scaffoldWidget(
|
|
|
30
31
|
const displayName = capitalize(widgetName);
|
|
31
32
|
const widgetDescription = description || `A GlassHome dashboard widget: ${widgetName}`;
|
|
32
33
|
|
|
34
|
+
// The range the host checks before mounting, derived from the SDK this
|
|
35
|
+
// project actually builds against. Hardcoding it produced widgets that
|
|
36
|
+
// declared a 0.2-era range while running on 1.x, which reads as "pre-1.0" to
|
|
37
|
+
// `requiresCapabilities` and quietly excused them from declaring capabilities.
|
|
38
|
+
const sdkRange = defaultSdkRange(cwd) ?? FALLBACK_SDK_RANGE;
|
|
39
|
+
|
|
33
40
|
// Read index.tsx template from the template directory
|
|
34
41
|
const templateDir = resolve(import.meta.dir, "../../template");
|
|
35
42
|
let srcContent = readFileSync(resolve(templateDir, "src/index.tsx.template"), "utf-8");
|
|
36
43
|
srcContent = srcContent.replace(/WIDGET_NAME/g, displayName);
|
|
44
|
+
srcContent = srcContent.replace(/SDK_RANGE/g, sdkRange);
|
|
37
45
|
writeFileSync(resolve(widgetDir, "index.tsx"), srcContent);
|
|
38
46
|
|
|
39
|
-
// Generate manifest.json
|
|
47
|
+
// Generate manifest.json. `capabilities` is required for any SDK >= 1.0.0
|
|
48
|
+
// range and an empty array is the honest default: a fresh widget reads
|
|
49
|
+
// nothing from Home Assistant, and the declaration is what consent shows.
|
|
40
50
|
const manifest = {
|
|
41
51
|
name: displayName,
|
|
42
52
|
description: widgetDescription,
|
|
43
53
|
minSize: { w: 1, h: 1 },
|
|
44
54
|
maxSize: { w: 4, h: 4 },
|
|
45
55
|
defaultSize: { w: 2, h: 2 },
|
|
46
|
-
sdkVersion:
|
|
56
|
+
sdkVersion: sdkRange,
|
|
57
|
+
capabilities: [],
|
|
47
58
|
version: "0.1.0",
|
|
48
59
|
};
|
|
49
60
|
writeFileSync(resolve(widgetDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
package/src/commands/build.ts
CHANGED
|
@@ -2,8 +2,15 @@ import { log, spinner } from "@clack/prompts";
|
|
|
2
2
|
import { buildWidgets } from "@glasshome/widget-sdk/vite";
|
|
3
3
|
import { lintAndReport } from "../utils/lint-source";
|
|
4
4
|
import { withQuietStdout } from "../utils/quiet";
|
|
5
|
+
import { typecheckAndReport } from "../utils/typecheck";
|
|
5
6
|
|
|
6
7
|
export async function runBuild(cwd: string): Promise<void> {
|
|
8
|
+
const check = spinner();
|
|
9
|
+
check.start("Checking types...");
|
|
10
|
+
const typesOk = typecheckAndReport(cwd);
|
|
11
|
+
check.stop(typesOk ? "Types checked" : "Typecheck failed");
|
|
12
|
+
if (!typesOk) process.exit(1);
|
|
13
|
+
|
|
7
14
|
const s = spinner();
|
|
8
15
|
s.start("Building widgets...");
|
|
9
16
|
|
package/src/commands/create.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { dirname, join, resolve } from "node:path";
|
|
12
12
|
import { cancel, confirm, isCancel, log, note, spinner, text } from "@clack/prompts";
|
|
13
13
|
import color from "picocolors";
|
|
14
|
+
import { defaultSdkRange, FALLBACK_SDK_RANGE } from "../utils/sdk-version";
|
|
14
15
|
import { getCliVersion } from "../utils/version";
|
|
15
16
|
import { promptWidgetDetails, scaffoldWidget } from "./add";
|
|
16
17
|
|
|
@@ -125,6 +126,9 @@ export async function runCreate() {
|
|
|
125
126
|
pkgContent = pkgContent.replace(/PROJECT_NAME/g, projectName as string);
|
|
126
127
|
pkgContent = pkgContent.replace(/PROJECT_DESCRIPTION/g, projectDesc);
|
|
127
128
|
pkgContent = pkgContent.replace(/CLI_VERSION/g, `^${getCliVersion()}`);
|
|
129
|
+
// Nothing is installed yet, so this comes from the SDK the CLI ships
|
|
130
|
+
// against. `bun widget upgrade` re-syncs it and every manifest later.
|
|
131
|
+
pkgContent = pkgContent.replace(/SDK_RANGE/g, defaultSdkRange() ?? FALLBACK_SDK_RANGE);
|
|
128
132
|
writeFileSync(pkgPath, pkgContent);
|
|
129
133
|
|
|
130
134
|
// Replace project-level placeholders in README.md
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { log } from "@clack/prompts";
|
|
2
|
+
import color from "picocolors";
|
|
3
|
+
import { runPreview as capturePreview } from "../preview/capture";
|
|
4
|
+
import { withQuietStdout } from "../utils/quiet";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Screenshot every widget's authored examples (light + dark) into
|
|
8
|
+
* `<project>/preview/`, rendered through the same constraints as the hub's
|
|
9
|
+
* render worker: frozen clock, DNS blackhole, per-render timeout, hash pin.
|
|
10
|
+
*
|
|
11
|
+
* Playwright is an OPTIONAL peer — the CLI stays Chromium-free by default — so
|
|
12
|
+
* detect it first and give an actionable install hint rather than a raw
|
|
13
|
+
* module-not-found when it is absent.
|
|
14
|
+
*/
|
|
15
|
+
export async function runPreview(cwd: string, names: string[], isolate: boolean): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
await import("playwright");
|
|
18
|
+
} catch {
|
|
19
|
+
log.error("Preview needs Playwright + a Chromium build, which are not installed.");
|
|
20
|
+
log.info(`Install them with:\n ${color.bold("bun add -d playwright && bunx playwright install chromium")}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Capture the REAL stdout writer before withQuietStdout no-ops
|
|
25
|
+
// process.stdout.write. A clack spinner writes through that same override and
|
|
26
|
+
// would freeze inside the quiet block, so progress goes out this bound
|
|
27
|
+
// reference (which the override cannot intercept) as plain lines. Build/vite
|
|
28
|
+
// noise stays silenced; only these phase lines get through.
|
|
29
|
+
const realWrite = process.stdout.write.bind(process.stdout);
|
|
30
|
+
const progress = (m: string) => {
|
|
31
|
+
realWrite(`${color.gray("│")} ${color.dim(m)}\n`);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
log.info(names.length ? `Previewing ${names.join(", ")}` : "Previewing all widgets");
|
|
35
|
+
|
|
36
|
+
let summary: Awaited<ReturnType<typeof capturePreview>>;
|
|
37
|
+
try {
|
|
38
|
+
summary = await withQuietStdout(() =>
|
|
39
|
+
capturePreview({ projectDir: cwd, only: names, isolate, onProgress: progress }),
|
|
40
|
+
);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const hangs = summary.failures.filter((f) => f.kind === "hang");
|
|
47
|
+
const integrity = summary.failures.filter((f) => f.kind === "integrity");
|
|
48
|
+
const networkWidgets = [
|
|
49
|
+
...new Set(summary.failures.filter((f) => f.kind === "network").map((f) => f.widget)),
|
|
50
|
+
];
|
|
51
|
+
const attempted = summary.shots + hangs.length;
|
|
52
|
+
|
|
53
|
+
// Headline: plain count of what landed in preview/. A miss only matters if a
|
|
54
|
+
// render could not complete (hangs) or a bundle changed under us (integrity).
|
|
55
|
+
const out = color.cyan("preview/");
|
|
56
|
+
if (hangs.length || integrity.length) {
|
|
57
|
+
log.warn(`Rendered ${summary.shots} of ${attempted} previews into ${out}`);
|
|
58
|
+
} else {
|
|
59
|
+
log.success(`Rendered ${summary.shots} previews (light + dark) into ${out}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (summary.skipped.length) {
|
|
63
|
+
log.message(color.dim(`No examples to render: ${summary.skipped.join(", ")}`));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Real miss: a render still too slow after one retry. Its slot is left without
|
|
67
|
+
// a PNG; everything else rendered.
|
|
68
|
+
if (hangs.length) {
|
|
69
|
+
log.warn(
|
|
70
|
+
`Too slow to render, even after a retry (no image written):\n` +
|
|
71
|
+
hangs.map((f) => ` · ${f.widget} ${f.detail}`).join("\n"),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Should never happen: a bundle's bytes changed mid-run. This is a real
|
|
76
|
+
// problem, not a slow render.
|
|
77
|
+
if (integrity.length) {
|
|
78
|
+
log.error(
|
|
79
|
+
`A bundle changed while rendering (report this):\n` +
|
|
80
|
+
integrity.map((f) => ` · ${f.widget}`).join("\n"),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Informational, not a failure: some widgets have no offline data and reach
|
|
85
|
+
// for the network. The request is blocked and nothing leaves the machine; the
|
|
86
|
+
// widget renders a placeholder. Expected for camera; worth a glance if another
|
|
87
|
+
// widget shows up here.
|
|
88
|
+
if (networkWidgets.length) {
|
|
89
|
+
log.message(
|
|
90
|
+
color.dim(
|
|
91
|
+
`Used a placeholder (no offline data; network blocked, nothing left the machine): ${networkWidgets.join(", ")}`,
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Vite's dev server leaves live handles behind (file watchers, keep-alive
|
|
97
|
+
// sockets from browsers that crashed mid-render), so the process would sit
|
|
98
|
+
// idle forever after the verdict instead of exiting. Leave deliberately.
|
|
99
|
+
process.exit(hangs.length || integrity.length ? 1 : 0);
|
|
100
|
+
}
|
package/src/commands/publish.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
readManifest,
|
|
13
13
|
writeManifest,
|
|
14
14
|
} from "../utils/manifest";
|
|
15
|
+
import { typecheckAndReport } from "../utils/typecheck";
|
|
15
16
|
import { enforceCliVersion } from "../utils/version";
|
|
16
17
|
import { runLogin } from "./login";
|
|
17
18
|
|
|
@@ -28,13 +29,7 @@ export async function runPublish(
|
|
|
28
29
|
): Promise<void> {
|
|
29
30
|
const s = spinner();
|
|
30
31
|
|
|
31
|
-
// Step 1: Validate (quiet)
|
|
32
32
|
const { runValidate } = await import("./validate");
|
|
33
|
-
const valid = await runValidate(cwd, undefined, { quiet: true });
|
|
34
|
-
if (!valid) {
|
|
35
|
-
log.error("Fix validation errors before publishing.");
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
33
|
|
|
39
34
|
// Step 2: Authenticate
|
|
40
35
|
const hubUrl = hubUrlOverride ?? getHubUrl();
|
|
@@ -167,7 +162,17 @@ export async function runPublish(
|
|
|
167
162
|
writeManifest(cwd, widgetName, manifest);
|
|
168
163
|
}
|
|
169
164
|
|
|
170
|
-
// Step 6:
|
|
165
|
+
// Step 6: Typecheck, then build (async so spinner animates).
|
|
166
|
+
//
|
|
167
|
+
// Checked here rather than left to the project's own build script: a project
|
|
168
|
+
// scaffolded before the template ran tsc builds with a bare `vite build`, and
|
|
169
|
+
// publishing is the irreversible step. A project whose build already
|
|
170
|
+
// typechecks just pays it twice, which is cheap next to a bad publish.
|
|
171
|
+
s.start("Checking types...");
|
|
172
|
+
const typesOk = typecheckAndReport(cwd);
|
|
173
|
+
s.stop(typesOk ? "Types checked" : "Typecheck failed");
|
|
174
|
+
if (!typesOk) process.exit(1);
|
|
175
|
+
|
|
171
176
|
s.start("Building widget...");
|
|
172
177
|
const buildProc = Bun.spawn(["bun", "run", "build"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
173
178
|
const buildExit = await buildProc.exited;
|
|
@@ -179,6 +184,15 @@ export async function runPublish(
|
|
|
179
184
|
}
|
|
180
185
|
s.stop("Build complete");
|
|
181
186
|
|
|
187
|
+
// Validate AFTER the build, not before: the build is what produces the
|
|
188
|
+
// manifest and bundle that get published, so validating beforehand checks a
|
|
189
|
+
// different document than the one that ships.
|
|
190
|
+
const valid = await runValidate(cwd, undefined, { quiet: true });
|
|
191
|
+
if (!valid) {
|
|
192
|
+
log.error("Fix validation errors before publishing.");
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
182
196
|
// Step 7: Publish
|
|
183
197
|
const distPath = resolve(cwd, "dist", `${widgetName}.js`);
|
|
184
198
|
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { log, note, spinner } from "@clack/prompts";
|
|
4
4
|
import { discoverWidgets, readManifest, writeManifest } from "../utils/manifest";
|
|
5
|
+
import { getInstalledSdkVersion } from "../utils/sdk-version";
|
|
5
6
|
import { runValidate } from "./validate";
|
|
6
7
|
|
|
7
8
|
/** Walk up from cwd looking for a package.json with a `workspaces` field. */
|
|
@@ -121,16 +122,30 @@ export async function runUpgrade(cwd: string): Promise<void> {
|
|
|
121
122
|
} else {
|
|
122
123
|
log.warn(`Synced to ${sdkVersion} but validation has warnings/errors. Check above.`);
|
|
123
124
|
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Standalone project. Bumping the dependency is still the author's call, but
|
|
129
|
+
// syncing the manifests to whatever is installed is not: a manifest range
|
|
130
|
+
// that excludes the installed SDK fails validation, and this is the command
|
|
131
|
+
// that error tells people to run.
|
|
132
|
+
const installed = getInstalledSdkVersion(cwd);
|
|
133
|
+
if (installed) {
|
|
134
|
+
syncManifestSdkVersions(cwd, installed);
|
|
135
|
+
const valid = await runValidate(cwd);
|
|
136
|
+
if (!valid) log.warn(`Synced manifests to ${installed}, but validation found problems above.`);
|
|
124
137
|
} else {
|
|
125
|
-
log.
|
|
126
|
-
note(
|
|
127
|
-
[
|
|
128
|
-
"1. Bump @glasshome/widget-sdk in package.json peerDependencies",
|
|
129
|
-
"2. bun install",
|
|
130
|
-
"3. bun widget upgrade (sync manifest files)",
|
|
131
|
-
"4. bun widget validate (check compatibility)",
|
|
132
|
-
].join("\n"),
|
|
133
|
-
"To upgrade",
|
|
134
|
-
);
|
|
138
|
+
log.warn("No @glasshome/widget-sdk installed here, so manifests were left alone.");
|
|
135
139
|
}
|
|
140
|
+
|
|
141
|
+
log.info("To move to a different SDK version:");
|
|
142
|
+
note(
|
|
143
|
+
[
|
|
144
|
+
"1. Bump @glasshome/widget-sdk in package.json peerDependencies",
|
|
145
|
+
"2. bun install",
|
|
146
|
+
"3. bun widget upgrade (sync manifest files)",
|
|
147
|
+
"4. bun widget validate (check compatibility)",
|
|
148
|
+
].join("\n"),
|
|
149
|
+
"To upgrade",
|
|
150
|
+
);
|
|
136
151
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
formatSchemaError,
|
|
6
6
|
publishManifestSchema,
|
|
7
7
|
requiresCapabilities,
|
|
8
|
+
satisfiesSdk,
|
|
8
9
|
} from "@glasshome/widget-contract";
|
|
9
10
|
import semver from "semver";
|
|
10
11
|
import {
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
readRegistry,
|
|
16
17
|
type WidgetManifest,
|
|
17
18
|
} from "../utils/manifest";
|
|
18
|
-
import {
|
|
19
|
+
import { getInstalledSdkVersion } from "../utils/sdk-version";
|
|
19
20
|
|
|
20
21
|
interface ValidationResult {
|
|
21
22
|
passed: boolean;
|
|
@@ -23,7 +24,7 @@ interface ValidationResult {
|
|
|
23
24
|
warnings: string[];
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function validateManifest(manifest: WidgetManifest): ValidationResult {
|
|
27
|
+
function validateManifest(manifest: WidgetManifest, installedSdk: string | null): ValidationResult {
|
|
27
28
|
const errors: string[] = [];
|
|
28
29
|
const warnings: string[] = [];
|
|
29
30
|
const hasValidMinSize =
|
|
@@ -47,6 +48,23 @@ function validateManifest(manifest: WidgetManifest): ValidationResult {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// The range must admit the SDK actually resolved in node_modules. Comparing
|
|
52
|
+
// it against the range declared in package.json only compares a claim with
|
|
53
|
+
// itself: both said "^0.2.0" while the build ran on 1.7.0, and it passed.
|
|
54
|
+
// An error, not a warning: a pre-1.0 range also excuses the widget from
|
|
55
|
+
// declaring capabilities, so a stale one silently leaves the 1.x contract.
|
|
56
|
+
if (
|
|
57
|
+
installedSdk &&
|
|
58
|
+
manifest.sdkVersion &&
|
|
59
|
+
manifest.sdkVersion !== "*" &&
|
|
60
|
+
semver.validRange(manifest.sdkVersion) &&
|
|
61
|
+
!satisfiesSdk(installedSdk, manifest.sdkVersion)
|
|
62
|
+
) {
|
|
63
|
+
errors.push(
|
|
64
|
+
`sdkVersion "${manifest.sdkVersion}" excludes the installed SDK ${installedSdk}, so it claims a compatibility it was not built for; run \`bun widget upgrade\``,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
// INS-05: "*" sdkVersion is rejected at publish time, authors must declare
|
|
51
69
|
// a real range (e.g. "^0.3.0"). Installed widgets that already ship "*" are
|
|
52
70
|
// still tolerated by the loader with a warn-and-mount, but new publishes
|
|
@@ -157,8 +175,7 @@ export async function runValidate(
|
|
|
157
175
|
|
|
158
176
|
const failed: string[] = [];
|
|
159
177
|
|
|
160
|
-
|
|
161
|
-
const projectSdk = getProjectSdkVersion(cwd);
|
|
178
|
+
const installedSdk = getInstalledSdkVersion(cwd);
|
|
162
179
|
|
|
163
180
|
for (const name of toValidate) {
|
|
164
181
|
if (!quiet) log.step(name);
|
|
@@ -175,7 +192,7 @@ export async function runValidate(
|
|
|
175
192
|
continue;
|
|
176
193
|
}
|
|
177
194
|
|
|
178
|
-
const result = validateManifest(manifest);
|
|
195
|
+
const result = validateManifest(manifest, installedSdk);
|
|
179
196
|
|
|
180
197
|
if (result.errors.length > 0) {
|
|
181
198
|
for (const err of result.errors) {
|
|
@@ -189,19 +206,6 @@ export async function runValidate(
|
|
|
189
206
|
}
|
|
190
207
|
}
|
|
191
208
|
|
|
192
|
-
if (
|
|
193
|
-
!quiet &&
|
|
194
|
-
projectSdk &&
|
|
195
|
-
manifest.sdkVersion &&
|
|
196
|
-
manifest.sdkVersion !== "*" &&
|
|
197
|
-
semver.validRange(manifest.sdkVersion) &&
|
|
198
|
-
!semver.satisfies(projectSdk, manifest.sdkVersion)
|
|
199
|
-
) {
|
|
200
|
-
log.warn(
|
|
201
|
-
` - manifest sdkVersion "${manifest.sdkVersion}" excludes the installed SDK ${projectSdk}; run \`bun widget upgrade\``,
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
209
|
// Check bundle exists
|
|
206
210
|
const bundlePath = resolve(cwd, "dist", `${name}.js`);
|
|
207
211
|
if (!existsSync(bundlePath)) {
|