openuispec 0.2.12 → 0.2.14
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 +8 -7
- package/cli/index.ts +18 -12
- package/cli/init.ts +78 -13
- package/docs/cli.md +81 -27
- package/docs/file-formats.md +52 -2
- package/drift/index.ts +7 -2
- package/examples/social-app/openuispec/README.md +2 -1
- package/examples/social-app/openuispec/mock/chat_detail.yaml +25 -0
- package/examples/social-app/openuispec/mock/discover.yaml +17 -0
- package/examples/social-app/openuispec/mock/edit_profile.yaml +9 -0
- package/examples/social-app/openuispec/mock/home_feed.yaml +32 -0
- package/examples/social-app/openuispec/mock/messages_inbox.yaml +15 -0
- package/examples/social-app/openuispec/mock/notifications.yaml +30 -0
- package/examples/social-app/openuispec/mock/post_detail.yaml +26 -0
- package/examples/social-app/openuispec/mock/profile_self.yaml +28 -0
- package/examples/social-app/openuispec/mock/profile_user.yaml +32 -0
- package/examples/social-app/openuispec/mock/search_results.yaml +17 -0
- package/examples/social-app/openuispec/mock/settings.yaml +7 -0
- package/examples/social-app/openuispec/openuispec.yaml +3 -2
- package/examples/taskflow/README.md +5 -3
- package/examples/taskflow/openuispec/README.md +2 -1
- package/examples/taskflow/openuispec/components/media_player.yaml +92 -0
- package/examples/taskflow/openuispec/contracts/README.md +2 -2
- package/examples/taskflow/openuispec/locales/en.json +1 -0
- package/examples/taskflow/openuispec/mock/home.yaml +64 -0
- package/examples/taskflow/openuispec/mock/profile_edit.yaml +6 -0
- package/examples/taskflow/openuispec/mock/project_detail.yaml +33 -0
- package/examples/taskflow/openuispec/mock/settings.yaml +13 -0
- package/examples/taskflow/openuispec/mock/task_detail.yaml +18 -0
- package/examples/taskflow/openuispec/openuispec.yaml +3 -4
- package/examples/taskflow/openuispec/platform/ios.yaml +0 -4
- package/examples/taskflow/openuispec/screens/task_detail.yaml +5 -8
- package/examples/taskflow/openuispec/tokens/icons.yaml +16 -0
- package/examples/todo-orbit/README.md +3 -2
- package/examples/todo-orbit/openuispec/README.md +2 -1
- package/examples/todo-orbit/openuispec/components/task_trend_chart.yaml +85 -0
- package/examples/todo-orbit/openuispec/locales/en.json +3 -0
- package/examples/todo-orbit/openuispec/locales/ru.json +3 -0
- package/examples/todo-orbit/openuispec/mock/analytics.yaml +26 -0
- package/examples/todo-orbit/openuispec/mock/home.yaml +33 -0
- package/examples/todo-orbit/openuispec/mock/settings.yaml +7 -0
- package/examples/todo-orbit/openuispec/mock/task_detail.yaml +14 -0
- package/examples/todo-orbit/openuispec/openuispec.yaml +3 -3
- package/examples/todo-orbit/openuispec/platform/android.yaml +0 -3
- package/examples/todo-orbit/openuispec/platform/ios.yaml +0 -3
- package/examples/todo-orbit/openuispec/platform/web.yaml +0 -3
- package/examples/todo-orbit/openuispec/screens/analytics.yaml +1 -4
- package/mcp-server/index.ts +80 -3
- package/mcp-server/preview-render.ts +1922 -0
- package/mcp-server/preview.ts +292 -0
- package/mcp-server/screenshot-shared.ts +38 -0
- package/mcp-server/screenshot.ts +3 -32
- package/package.json +1 -1
- package/prepare/index.ts +1 -1
- package/schema/component.schema.json +278 -0
- package/schema/custom-contract.schema.json +2 -2
- package/schema/openuispec.schema.json +18 -8
- package/schema/screen.schema.json +12 -1
- package/schema/semantic-lint.ts +24 -2
- package/schema/validate.ts +21 -0
- package/scripts/regenerate-previews.ts +136 -0
- package/spec/{openuispec-v0.1.md → openuispec-v0.2.md} +275 -17
- package/examples/taskflow/openuispec/contracts/x_media_player.yaml +0 -185
- package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +0 -139
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"states": {
|
|
32
32
|
"type": "object",
|
|
33
|
-
"description": "
|
|
33
|
+
"description": "UI interaction states (e.g. idle, active, disabled, loading, error) — each key is a state name. These are visual/interaction states, not business logic states.",
|
|
34
34
|
"additionalProperties": {
|
|
35
35
|
"$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/state_def"
|
|
36
36
|
}
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
},
|
|
166
166
|
"state_def": {
|
|
167
167
|
"type": "object",
|
|
168
|
-
"description": "A single
|
|
168
|
+
"description": "A single UI interaction state",
|
|
169
169
|
"properties": {
|
|
170
170
|
"semantic": {
|
|
171
171
|
"type": "string"
|
|
@@ -38,22 +38,32 @@
|
|
|
38
38
|
"description": "Paths to spec component directories",
|
|
39
39
|
"properties": {
|
|
40
40
|
"tokens": {
|
|
41
|
-
"type": "string"
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Visual design tokens — colors, typography, spacing, elevation, motion. Affects UI rendering only, not business logic."
|
|
42
43
|
},
|
|
43
44
|
"contracts": {
|
|
44
|
-
"type": "string"
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Reusable UI component definitions — buttons, inputs, lists, surfaces. Each contract specifies visual props, UI interaction states (pressed, disabled, loading), and accessibility. These are UI rendering specifications, not business logic or domain state machines."
|
|
45
47
|
},
|
|
46
48
|
"screens": {
|
|
47
|
-
"type": "string"
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Screen layouts composed from contracts — defines what UI components appear on each screen, their data bindings, and visual arrangement."
|
|
48
51
|
},
|
|
49
52
|
"flows": {
|
|
50
|
-
"type": "string"
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Multi-screen navigation journeys — defines screen sequences, transitions, and navigation triggers. UI navigation structure, not business workflow logic."
|
|
51
55
|
},
|
|
52
56
|
"platform": {
|
|
53
|
-
"type": "string"
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "Per-target platform overrides — native UI behavior, framework-specific generation config. One file per target (ios.yaml, android.yaml, web.yaml)."
|
|
54
59
|
},
|
|
55
60
|
"locales": {
|
|
56
|
-
"type": "string"
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Localization string files — one JSON file per supported locale containing translated UI text."
|
|
63
|
+
},
|
|
64
|
+
"components": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Reusable component compositions — each YAML file defines a component as a composition of contracts with named slots, states, and variants."
|
|
57
67
|
}
|
|
58
68
|
},
|
|
59
69
|
"required": [
|
|
@@ -175,10 +185,10 @@
|
|
|
175
185
|
},
|
|
176
186
|
"tracks": {
|
|
177
187
|
"type": "array",
|
|
178
|
-
"description": "Spec categories this shared layer tracks for drift.
|
|
188
|
+
"description": "Spec categories this shared layer tracks for hash-based drift detection. Optional — when omitted, the layer relies on scope alone. Categories: manifest (project config, data models, API endpoints), tokens (visual design tokens — UI only), contracts (reusable UI component definitions — UI only, not business logic), screens (screen layouts — UI only), flows (navigation journeys — UI only), platform (per-target overrides — UI only), locales (translated UI strings).",
|
|
179
189
|
"items": {
|
|
180
190
|
"type": "string",
|
|
181
|
-
"enum": ["manifest", "tokens", "contracts", "screens", "flows", "platform", "locales"]
|
|
191
|
+
"enum": ["manifest", "tokens", "contracts", "components", "screens", "flows", "platform", "locales"]
|
|
182
192
|
}
|
|
183
193
|
},
|
|
184
194
|
"scope": {
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"additionalProperties": false
|
|
172
172
|
},
|
|
173
173
|
"section_item": {
|
|
174
|
-
"description": "A section item \u2014
|
|
174
|
+
"description": "A section item \u2014 a contract instance, component instance, or section group with children",
|
|
175
175
|
"type": "object",
|
|
176
176
|
"properties": {
|
|
177
177
|
"id": {
|
|
@@ -180,6 +180,17 @@
|
|
|
180
180
|
"contract": {
|
|
181
181
|
"type": "string"
|
|
182
182
|
},
|
|
183
|
+
"component": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"description": "Component name (alternative to contract). References a component defined in components/*.yaml."
|
|
186
|
+
},
|
|
187
|
+
"slots": {
|
|
188
|
+
"type": "object",
|
|
189
|
+
"description": "Slot overrides when using a component. Each key is a slot name.",
|
|
190
|
+
"additionalProperties": {
|
|
191
|
+
"$ref": "https://openuispec.rsteam.uz/schema/component.schema.json#/$defs/slot_override"
|
|
192
|
+
}
|
|
193
|
+
},
|
|
183
194
|
"variant": {
|
|
184
195
|
"type": "string"
|
|
185
196
|
},
|
package/schema/semantic-lint.ts
CHANGED
|
@@ -25,6 +25,7 @@ function collectLocaleKeys(data: unknown, prefix = ""): string[] {
|
|
|
25
25
|
export interface Includes {
|
|
26
26
|
tokens: string;
|
|
27
27
|
contracts: string;
|
|
28
|
+
components: string;
|
|
28
29
|
screens: string;
|
|
29
30
|
flows: string;
|
|
30
31
|
platform: string;
|
|
@@ -43,6 +44,7 @@ interface SemanticContext {
|
|
|
43
44
|
formatterNames: Set<string>;
|
|
44
45
|
mapperNames: Set<string>;
|
|
45
46
|
contractNames: Set<string>;
|
|
47
|
+
componentNames: Set<string>;
|
|
46
48
|
tokenRefs: Set<string>;
|
|
47
49
|
iconRefs: Set<string>;
|
|
48
50
|
iconVariantSuffixes: string[];
|
|
@@ -253,6 +255,15 @@ function buildContext(projectDir: string, includes: Includes, manifest: UnknownR
|
|
|
253
255
|
}
|
|
254
256
|
}
|
|
255
257
|
|
|
258
|
+
// Components are also valid references in screen sections (via "component:" key)
|
|
259
|
+
const componentNames = new Set<string>();
|
|
260
|
+
const componentsDir = resolve(projectDir, includes.components);
|
|
261
|
+
for (const filePath of listFiles(componentsDir, ".yaml")) {
|
|
262
|
+
for (const key of rootKeys(filePath)) {
|
|
263
|
+
componentNames.add(key);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
256
267
|
const tokenRefs = new Set<string>();
|
|
257
268
|
const tokensDir = resolve(projectDir, includes.tokens);
|
|
258
269
|
for (const filePath of listFiles(tokensDir, ".yaml")) {
|
|
@@ -300,6 +311,7 @@ function buildContext(projectDir: string, includes: Includes, manifest: UnknownR
|
|
|
300
311
|
formatterNames,
|
|
301
312
|
mapperNames,
|
|
302
313
|
contractNames,
|
|
314
|
+
componentNames,
|
|
303
315
|
tokenRefs,
|
|
304
316
|
iconRefs: iconData.refs,
|
|
305
317
|
iconVariantSuffixes: iconData.suffixes,
|
|
@@ -429,6 +441,10 @@ function validateStringValue(
|
|
|
429
441
|
errors.push({ path, message: `unknown contract "${value}"` });
|
|
430
442
|
}
|
|
431
443
|
|
|
444
|
+
if (key === "component" && !path.includes("platform_mapping") && !context.componentNames.has(value)) {
|
|
445
|
+
errors.push({ path, message: `unknown component "${value}"` });
|
|
446
|
+
}
|
|
447
|
+
|
|
432
448
|
if (
|
|
433
449
|
(key === "icon" || key === "icon_active") &&
|
|
434
450
|
!isDynamicReference(value) &&
|
|
@@ -617,6 +633,7 @@ export function collectSemanticLint(projectDir: string, includes: Includes): Usa
|
|
|
617
633
|
const manifest = readManifest(projectDir) as UnknownRecord;
|
|
618
634
|
const context = buildContext(projectDir, includes, manifest);
|
|
619
635
|
const contractsDir = resolve(projectDir, includes.contracts);
|
|
636
|
+
const componentsDir = resolve(projectDir, includes.components);
|
|
620
637
|
|
|
621
638
|
const allErrors: UsageLint[] = [
|
|
622
639
|
...lintLocaleCoverage(context),
|
|
@@ -629,11 +646,13 @@ export function collectSemanticLint(projectDir: string, includes: Includes): Usa
|
|
|
629
646
|
...listFiles(resolve(projectDir, includes.flows), ".yaml"),
|
|
630
647
|
...listFiles(resolve(projectDir, includes.platform), ".yaml"),
|
|
631
648
|
...listFiles(resolve(projectDir, includes.contracts), ".yaml"),
|
|
649
|
+
...listFiles(componentsDir, ".yaml"),
|
|
632
650
|
];
|
|
633
651
|
|
|
634
652
|
for (const filePath of files) {
|
|
653
|
+
const isContractOrComponent = filePath.startsWith(contractsDir) || filePath.startsWith(componentsDir);
|
|
635
654
|
allErrors.push(
|
|
636
|
-
...lintFile(filePath, context, { validateTokens: !
|
|
655
|
+
...lintFile(filePath, context, { validateTokens: !isContractOrComponent })
|
|
637
656
|
);
|
|
638
657
|
}
|
|
639
658
|
|
|
@@ -645,6 +664,7 @@ export function runSemanticLint(projectDir: string, includes: Includes): number
|
|
|
645
664
|
const context = buildContext(projectDir, includes, manifest);
|
|
646
665
|
let total = 0;
|
|
647
666
|
const contractsDir = resolve(projectDir, includes.contracts);
|
|
667
|
+
const componentsDir = resolve(projectDir, includes.components);
|
|
648
668
|
|
|
649
669
|
total += printSemanticErrors("locales", lintLocaleCoverage(context));
|
|
650
670
|
total += printSemanticErrors("openuispec.yaml", lintManifestGenerationContext(projectDir, context.manifest));
|
|
@@ -655,12 +675,14 @@ export function runSemanticLint(projectDir: string, includes: Includes): number
|
|
|
655
675
|
...listFiles(resolve(projectDir, includes.flows), ".yaml"),
|
|
656
676
|
...listFiles(resolve(projectDir, includes.platform), ".yaml"),
|
|
657
677
|
...listFiles(resolve(projectDir, includes.contracts), ".yaml"),
|
|
678
|
+
...listFiles(componentsDir, ".yaml"),
|
|
658
679
|
];
|
|
659
680
|
|
|
660
681
|
for (const filePath of files) {
|
|
682
|
+
const isContractOrComponent = filePath.startsWith(contractsDir) || filePath.startsWith(componentsDir);
|
|
661
683
|
total += printSemanticErrors(
|
|
662
684
|
basename(filePath),
|
|
663
|
-
lintFile(filePath, context, { validateTokens: !
|
|
685
|
+
lintFile(filePath, context, { validateTokens: !isContractOrComponent })
|
|
664
686
|
);
|
|
665
687
|
}
|
|
666
688
|
|
package/schema/validate.ts
CHANGED
|
@@ -478,6 +478,7 @@ function validateFile(
|
|
|
478
478
|
const DEFAULT_INCLUDES: Includes = {
|
|
479
479
|
tokens: "./tokens/",
|
|
480
480
|
contracts: "./contracts/",
|
|
481
|
+
components: "./components/",
|
|
481
482
|
screens: "./screens/",
|
|
482
483
|
flows: "./flows/",
|
|
483
484
|
platform: "./platform/",
|
|
@@ -702,6 +703,26 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
702
703
|
},
|
|
703
704
|
},
|
|
704
705
|
|
|
706
|
+
components: {
|
|
707
|
+
label: "Components",
|
|
708
|
+
run(ajv, projectDir, includes) {
|
|
709
|
+
let errors = 0;
|
|
710
|
+
const dir = resolveInclude(projectDir, includes.components);
|
|
711
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
712
|
+
errors += validateFile(ajv, f, `${BASE}component.schema.json`);
|
|
713
|
+
}
|
|
714
|
+
return errors;
|
|
715
|
+
},
|
|
716
|
+
collectJson(ajv, projectDir, includes, groupKey) {
|
|
717
|
+
const errors: JsonError[] = [];
|
|
718
|
+
const dir = resolveInclude(projectDir, includes.components);
|
|
719
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
720
|
+
errors.push(...collectValidateFile(ajv, f, `${BASE}component.schema.json`));
|
|
721
|
+
}
|
|
722
|
+
return { group: groupKey, errors };
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
|
|
705
726
|
semantic: {
|
|
706
727
|
label: "Semantic",
|
|
707
728
|
run(_ajv, projectDir, includes) {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Regenerates all preview PNGs for the 3 example projects using the
|
|
4
|
+
* preview renderer (preview-render.ts → preview.ts → Puppeteer).
|
|
5
|
+
*
|
|
6
|
+
* Outputs to examples/<project>/previews/<screen>_<sizeClass>[_<theme>].png
|
|
7
|
+
*
|
|
8
|
+
* Usage: npx tsx scripts/regenerate-previews.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
import { renderPreview } from "../mcp-server/preview.js";
|
|
14
|
+
import { closeBrowser } from "../mcp-server/screenshot-shared.js";
|
|
15
|
+
|
|
16
|
+
const ROOT = resolve(import.meta.dirname!, "..");
|
|
17
|
+
|
|
18
|
+
interface Capture {
|
|
19
|
+
screen: string;
|
|
20
|
+
sizeClass: "compact" | "regular" | "expanded";
|
|
21
|
+
theme?: "light" | "dark";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ProjectDef {
|
|
25
|
+
name: string;
|
|
26
|
+
captures: Capture[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function allSizes(screen: string): Capture[] {
|
|
30
|
+
return [
|
|
31
|
+
{ screen, sizeClass: "compact" },
|
|
32
|
+
{ screen, sizeClass: "expanded" },
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function allSizesWithThemes(screen: string): Capture[] {
|
|
37
|
+
return [
|
|
38
|
+
{ screen, sizeClass: "compact" },
|
|
39
|
+
{ screen, sizeClass: "expanded" },
|
|
40
|
+
{ screen, sizeClass: "compact", theme: "light" },
|
|
41
|
+
{ screen, sizeClass: "compact", theme: "dark" },
|
|
42
|
+
{ screen, sizeClass: "expanded", theme: "light" },
|
|
43
|
+
{ screen, sizeClass: "expanded", theme: "dark" },
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PROJECTS: ProjectDef[] = [
|
|
48
|
+
{
|
|
49
|
+
name: "social-app",
|
|
50
|
+
captures: [
|
|
51
|
+
...allSizes("home_feed"),
|
|
52
|
+
...allSizes("discover"),
|
|
53
|
+
...allSizes("notifications"),
|
|
54
|
+
...allSizes("messages_inbox"),
|
|
55
|
+
...allSizes("profile_self"),
|
|
56
|
+
...allSizes("profile_user"),
|
|
57
|
+
...allSizes("settings"),
|
|
58
|
+
...allSizes("post_detail"),
|
|
59
|
+
...allSizes("chat_detail"),
|
|
60
|
+
...allSizes("search_results"),
|
|
61
|
+
...allSizes("edit_profile"),
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "taskflow",
|
|
66
|
+
captures: [
|
|
67
|
+
...allSizesWithThemes("home"),
|
|
68
|
+
...allSizes("projects"),
|
|
69
|
+
...allSizes("calendar"),
|
|
70
|
+
...allSizesWithThemes("settings"),
|
|
71
|
+
...allSizesWithThemes("task_detail"),
|
|
72
|
+
...allSizes("project_detail"),
|
|
73
|
+
...allSizes("profile_edit"),
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "todo-orbit",
|
|
78
|
+
captures: [
|
|
79
|
+
...allSizes("home"),
|
|
80
|
+
...allSizes("analytics"),
|
|
81
|
+
...allSizes("settings"),
|
|
82
|
+
...allSizes("task_detail"),
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function log(msg: string) { console.log(`\x1b[36m▸\x1b[0m ${msg}`); }
|
|
88
|
+
function logOk(msg: string) { console.log(`\x1b[32m✔\x1b[0m ${msg}`); }
|
|
89
|
+
function logErr(msg: string) { console.error(`\x1b[31m✖\x1b[0m ${msg}`); }
|
|
90
|
+
|
|
91
|
+
async function main() {
|
|
92
|
+
let total = 0;
|
|
93
|
+
let failures = 0;
|
|
94
|
+
|
|
95
|
+
for (const project of PROJECTS) {
|
|
96
|
+
console.log(`\n\x1b[1m=== ${project.name} ===\x1b[0m\n`);
|
|
97
|
+
const projectCwd = join(ROOT, "examples", project.name);
|
|
98
|
+
const outDir = join(projectCwd, "previews");
|
|
99
|
+
mkdirSync(outDir, { recursive: true });
|
|
100
|
+
|
|
101
|
+
for (const cap of project.captures) {
|
|
102
|
+
const theme = cap.theme ?? "light";
|
|
103
|
+
const suffix = cap.theme ? `_${cap.theme}` : "";
|
|
104
|
+
const filename = `${cap.screen}_${cap.sizeClass}${suffix}.png`;
|
|
105
|
+
log(`${filename}...`);
|
|
106
|
+
total++;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const result = await renderPreview(projectCwd, {
|
|
110
|
+
screen: cap.screen,
|
|
111
|
+
size_class: cap.sizeClass,
|
|
112
|
+
theme,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
for (const item of result.content) {
|
|
116
|
+
if (item.type === "image" && "data" in item) {
|
|
117
|
+
writeFileSync(join(outDir, filename), Buffer.from(item.data, "base64"));
|
|
118
|
+
logOk(filename);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (err: any) {
|
|
122
|
+
logErr(`${filename}: ${err.message}`);
|
|
123
|
+
failures++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await closeBrowser();
|
|
129
|
+
console.log(`\n\x1b[${failures ? "31" : "32"}m${total - failures}/${total} previews generated, ${failures} failures\x1b[0m\n`);
|
|
130
|
+
process.exit(failures > 0 ? 1 : 0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main().catch((err) => {
|
|
134
|
+
console.error(err);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
});
|