openuispec 0.2.18 → 0.2.20

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.
Files changed (45) hide show
  1. package/README.md +2 -10
  2. package/dist/check/audit.js +392 -0
  3. package/dist/check/index.js +216 -0
  4. package/dist/cli/configure-target.js +391 -0
  5. package/dist/cli/index.js +510 -0
  6. package/dist/cli/init.js +1047 -0
  7. package/dist/drift/index.js +903 -0
  8. package/dist/mcp-server/index.js +886 -0
  9. package/dist/mcp-server/preview-render.js +1761 -0
  10. package/dist/mcp-server/preview.js +233 -0
  11. package/dist/mcp-server/screenshot-android.js +458 -0
  12. package/dist/mcp-server/screenshot-ios.js +639 -0
  13. package/dist/mcp-server/screenshot-shared.js +180 -0
  14. package/dist/mcp-server/screenshot.js +459 -0
  15. package/dist/prepare/index.js +1216 -0
  16. package/dist/runtime/package-paths.js +33 -0
  17. package/dist/schema/semantic-lint.js +564 -0
  18. package/dist/schema/validate.js +689 -0
  19. package/dist/status/index.js +194 -0
  20. package/docs/images/how-it-works.svg +56 -0
  21. package/docs/images/workflows.svg +76 -0
  22. package/package.json +12 -13
  23. package/check/audit.ts +0 -426
  24. package/check/index.ts +0 -320
  25. package/cli/configure-target.ts +0 -523
  26. package/cli/index.ts +0 -537
  27. package/cli/init.ts +0 -1253
  28. package/docs/images/how-it-works-dark.png +0 -0
  29. package/docs/images/how-it-works-light.png +0 -0
  30. package/docs/images/workflows-dark.png +0 -0
  31. package/docs/images/workflows-light.png +0 -0
  32. package/drift/index.ts +0 -1165
  33. package/mcp-server/index.ts +0 -1041
  34. package/mcp-server/preview-render.ts +0 -1922
  35. package/mcp-server/preview.ts +0 -292
  36. package/mcp-server/screenshot-android.ts +0 -621
  37. package/mcp-server/screenshot-ios.ts +0 -753
  38. package/mcp-server/screenshot-shared.ts +0 -237
  39. package/mcp-server/screenshot.ts +0 -563
  40. package/prepare/index.ts +0 -1530
  41. package/schema/semantic-lint.ts +0 -692
  42. package/schema/validate.ts +0 -870
  43. package/scripts/regenerate-previews.ts +0 -136
  44. package/scripts/take-all-screenshots.ts +0 -507
  45. package/status/index.ts +0 -275
@@ -1,292 +0,0 @@
1
- /**
2
- * preview.ts — Orchestrates spec loading, mock data, and HTML rendering
3
- * for the openuispec_preview tool.
4
- */
5
-
6
- import { readFileSync, existsSync, readdirSync } from "node:fs";
7
- import { join, resolve } from "node:path";
8
- import YAML from "yaml";
9
- import { findProjectDir } from "../drift/index.js";
10
- import { getBrowser, type ScreenshotResult } from "./screenshot-shared.js";
11
- import { renderPage, type PreviewContext } from "./preview-render.js";
12
-
13
- // ── types ───────────────────────────────────────────────────────────
14
-
15
- export interface PreviewOptions {
16
- screen: string;
17
- size_class?: "compact" | "regular" | "expanded";
18
- theme?: "light" | "dark";
19
- locale?: string;
20
- viewport?: { width: number; height: number };
21
- include_html?: boolean;
22
- }
23
-
24
- // ── viewport defaults per size class ────────────────────────────────
25
-
26
- const SIZE_CLASS_VIEWPORTS: Record<string, { width: number; height: number }> = {
27
- compact: { width: 390, height: 844 },
28
- regular: { width: 820, height: 1180 },
29
- expanded: { width: 1280, height: 800 },
30
- };
31
-
32
- // ── spec loading helpers ────────────────────────────────────────────
33
-
34
- function loadManifest(projectDir: string): any {
35
- const manifestPath = join(projectDir, "openuispec.yaml");
36
- if (!existsSync(manifestPath)) {
37
- throw new Error(`Manifest not found at ${manifestPath}`);
38
- }
39
- return YAML.parse(readFileSync(manifestPath, "utf-8"));
40
- }
41
-
42
- function loadScreen(projectDir: string, manifest: any, screenName: string): any {
43
- const screensDir = resolve(projectDir, manifest.includes?.screens ?? "./screens/");
44
-
45
- // Try exact filename first
46
- const candidates = [
47
- join(screensDir, `${screenName}.yaml`),
48
- join(screensDir, `${screenName}.yml`),
49
- ];
50
-
51
- for (const candidate of candidates) {
52
- if (existsSync(candidate)) {
53
- return YAML.parse(readFileSync(candidate, "utf-8"));
54
- }
55
- }
56
-
57
- // Scan all screen files for matching screen name key
58
- if (existsSync(screensDir)) {
59
- for (const file of readdirSync(screensDir)) {
60
- if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
61
- const content = YAML.parse(readFileSync(join(screensDir, file), "utf-8"));
62
- if (content && typeof content === "object" && screenName in content) {
63
- return content;
64
- }
65
- }
66
- }
67
-
68
- throw new Error(
69
- `Screen "${screenName}" not found in ${screensDir}. ` +
70
- `Available: ${existsSync(screensDir) ? readdirSync(screensDir).filter(f => f.endsWith(".yaml")).map(f => f.replace(".yaml", "")).join(", ") : "none"}`,
71
- );
72
- }
73
-
74
- function loadAllTokens(projectDir: string, manifest: any): Record<string, any> {
75
- const tokensDir = resolve(projectDir, manifest.includes?.tokens ?? "./tokens/");
76
- const tokens: Record<string, any> = {};
77
-
78
- if (!existsSync(tokensDir)) return tokens;
79
-
80
- for (const file of readdirSync(tokensDir)) {
81
- if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
82
- const category = file.replace(/\.ya?ml$/, "");
83
- try {
84
- tokens[category] = YAML.parse(readFileSync(join(tokensDir, file), "utf-8"));
85
- } catch {
86
- // Skip malformed token files
87
- }
88
- }
89
-
90
- return tokens;
91
- }
92
-
93
- function loadLocale(projectDir: string, manifest: any, localeName: string): Record<string, string> {
94
- const localesDir = resolve(projectDir, manifest.includes?.locales ?? "./locales/");
95
-
96
- const candidates = [
97
- join(localesDir, `${localeName}.json`),
98
- join(localesDir, `${localeName}.yaml`),
99
- join(localesDir, `${localeName}.yml`),
100
- ];
101
-
102
- for (const candidate of candidates) {
103
- if (existsSync(candidate)) {
104
- const content = readFileSync(candidate, "utf-8");
105
- if (candidate.endsWith(".json")) {
106
- return JSON.parse(content);
107
- }
108
- return YAML.parse(content) ?? {};
109
- }
110
- }
111
-
112
- // Fallback to default locale
113
- const defaultLocale = manifest.i18n?.default_locale ?? "en";
114
- if (localeName !== defaultLocale) {
115
- return loadLocale(projectDir, manifest, defaultLocale);
116
- }
117
-
118
- return {};
119
- }
120
-
121
- function loadContractDefs(projectDir: string, manifest: any): Record<string, any> {
122
- const contractsDir = resolve(projectDir, manifest.includes?.contracts ?? "./contracts/");
123
- const defs: Record<string, any> = {};
124
-
125
- if (!existsSync(contractsDir)) return defs;
126
-
127
- for (const file of readdirSync(contractsDir)) {
128
- if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
129
- try {
130
- const content = YAML.parse(readFileSync(join(contractsDir, file), "utf-8"));
131
- if (content && typeof content === "object") {
132
- const name = file.replace(/\.ya?ml$/, "");
133
- defs[name] = content;
134
- }
135
- } catch {
136
- // Skip malformed contract files
137
- }
138
- }
139
-
140
- return defs;
141
- }
142
-
143
- function loadComponentDefs(projectDir: string, manifest: any): Record<string, any> {
144
- const componentsDir = resolve(projectDir, manifest.includes?.components ?? "./components/");
145
- const defs: Record<string, any> = {};
146
-
147
- if (!existsSync(componentsDir)) return defs;
148
-
149
- for (const file of readdirSync(componentsDir)) {
150
- if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
151
- try {
152
- const content = YAML.parse(readFileSync(join(componentsDir, file), "utf-8"));
153
- if (content && typeof content === "object") {
154
- for (const [name, def] of Object.entries(content)) {
155
- defs[name] = def;
156
- }
157
- }
158
- } catch {
159
- // Skip malformed component files
160
- }
161
- }
162
-
163
- return defs;
164
- }
165
-
166
- function loadMockData(projectDir: string, screenName: string): { data: Record<string, any>; params: Record<string, any> } {
167
- const mockDir = join(projectDir, "mock");
168
-
169
- const candidates = [
170
- join(mockDir, `${screenName}.yaml`),
171
- join(mockDir, `${screenName}.yml`),
172
- join(mockDir, `${screenName}.json`),
173
- ];
174
-
175
- for (const candidate of candidates) {
176
- if (existsSync(candidate)) {
177
- const content = readFileSync(candidate, "utf-8");
178
- let parsed: any;
179
- if (candidate.endsWith(".json")) {
180
- parsed = JSON.parse(content);
181
- } else {
182
- parsed = YAML.parse(content);
183
- }
184
- return {
185
- data: parsed?.data ?? parsed ?? {},
186
- params: parsed?.params ?? {},
187
- };
188
- }
189
- }
190
-
191
- return { data: {}, params: {} };
192
- }
193
-
194
- // ── main preview function ───────────────────────────────────────────
195
-
196
- export async function renderPreview(
197
- projectCwd: string,
198
- options: PreviewOptions,
199
- ): Promise<ScreenshotResult> {
200
- const {
201
- screen,
202
- size_class = "compact",
203
- theme = "light",
204
- locale: localeName = "en",
205
- viewport,
206
- include_html = false,
207
- } = options;
208
-
209
- // 1. Find project directory
210
- const projectDir = findProjectDir(projectCwd);
211
-
212
- // 2. Load specs
213
- const manifest = loadManifest(projectDir);
214
- const screenSpec = loadScreen(projectDir, manifest, screen);
215
- const tokens = loadAllTokens(projectDir, manifest);
216
- const locale = loadLocale(projectDir, manifest, localeName);
217
-
218
- // 3. Load contract definitions (project extensions) and component definitions
219
- const contractDefs = loadContractDefs(projectDir, manifest);
220
- manifest._contractDefs = contractDefs;
221
- const componentDefs = loadComponentDefs(projectDir, manifest);
222
- manifest._componentDefs = componentDefs;
223
-
224
- // 4. Load mock data
225
- const { data: mockData, params: mockParams } = loadMockData(projectDir, screen);
226
-
227
- // 5. Build render context
228
- const ctx: PreviewContext = {
229
- manifest,
230
- screen: screenSpec,
231
- screenName: screen,
232
- tokens,
233
- locale,
234
- mockData,
235
- mockParams,
236
- sizeClass: size_class,
237
- theme,
238
- };
239
-
240
- // 6. Render HTML
241
- const html = renderPage(ctx);
242
-
243
- // 7. Screenshot with Puppeteer
244
- const vp = viewport ?? SIZE_CLASS_VIEWPORTS[size_class];
245
- const browser = await getBrowser();
246
- const page = await browser.newPage();
247
-
248
- try {
249
- await page.setViewport({
250
- width: vp.width,
251
- height: vp.height,
252
- deviceScaleFactor: 2,
253
- });
254
-
255
- if (theme === "dark") {
256
- await page.emulateMediaFeatures([
257
- { name: "prefers-color-scheme", value: "dark" },
258
- ]);
259
- }
260
-
261
- await page.setContent(html, { waitUntil: "load", timeout: 10_000 });
262
-
263
- // Small delay for CSS to settle
264
- await new Promise((r) => setTimeout(r, 200));
265
-
266
- const buffer = await page.screenshot({ type: "png", fullPage: true });
267
- const base64 = buffer.toString("base64");
268
-
269
- const content: ScreenshotResult["content"] = [
270
- { type: "image" as const, data: base64, mimeType: "image/png" },
271
- {
272
- type: "text" as const,
273
- text: JSON.stringify({
274
- screen,
275
- size_class,
276
- theme,
277
- locale: localeName,
278
- viewport: vp,
279
- mock_data_loaded: Object.keys(mockData).length > 0,
280
- }, null, 2),
281
- },
282
- ];
283
-
284
- if (include_html) {
285
- content.push({ type: "text" as const, text: html });
286
- }
287
-
288
- return { content };
289
- } finally {
290
- await page.close();
291
- }
292
- }