openuispec 0.1.12 → 0.1.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/cli/index.ts +1 -1
- package/cli/init.ts +40 -26
- package/package.json +1 -1
- package/schema/locale.schema.json +19 -3
- package/schema/validate.ts +60 -18
package/cli/index.ts
CHANGED
package/cli/init.ts
CHANGED
|
@@ -137,6 +137,12 @@ OpenUISpec is a YAML-based format that describes your app's UI semantically —
|
|
|
137
137
|
| \`platform/\` | Platform overrides — per-target (iOS, Android, Web) behaviors |
|
|
138
138
|
| \`locales/\` | Localization — i18n strings (JSON, ICU MessageFormat) |
|
|
139
139
|
|
|
140
|
+
All directory paths are configured in \`openuispec.yaml\` under \`includes:\` and support relative paths. For example, to share locales across projects:
|
|
141
|
+
\`\`\`yaml
|
|
142
|
+
includes:
|
|
143
|
+
locales: "../../shared/locales" # resolved relative to openuispec.yaml
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
140
146
|
## Getting started
|
|
141
147
|
|
|
142
148
|
**Start here:** read \`openuispec.yaml\` — it's the root manifest that defines the project structure, data model, API endpoints, and generation targets.
|
|
@@ -170,16 +176,19 @@ OpenUISpec is a YAML-based format that describes your app's UI semantically —
|
|
|
170
176
|
- \`draft\` — actively being specced. Tracked by drift.
|
|
171
177
|
- \`ready\` — fully specified (default if omitted). Tracked by drift.
|
|
172
178
|
|
|
173
|
-
## Learning OpenUISpec —
|
|
179
|
+
## Learning OpenUISpec — where to find the docs
|
|
174
180
|
|
|
175
|
-
All documentation is included in the installed package
|
|
176
|
-
1. **
|
|
177
|
-
2. **
|
|
178
|
-
3. **
|
|
181
|
+
All documentation is included in the installed \`openuispec\` package. Search for it in this order:
|
|
182
|
+
1. **Local:** \`node_modules/openuispec/\` (if installed as a project dependency)
|
|
183
|
+
2. **Global:** run \`npm root -g\` to find the global prefix, then look in \`<prefix>/openuispec/\`
|
|
184
|
+
3. **Online fallback:** if the package is not installed at all, fetch from:
|
|
185
|
+
- \`https://openuispec.rsteam.uz/llms-full.txt\` — complete spec + all JSON schemas in one file
|
|
186
|
+
- \`https://openuispec.rsteam.uz/llms.txt\` — concise summary with links
|
|
179
187
|
|
|
180
|
-
|
|
181
|
-
- \`
|
|
182
|
-
- \`
|
|
188
|
+
Inside the package:
|
|
189
|
+
- **Full specification:** \`spec/openuispec-v0.1.md\`
|
|
190
|
+
- **Example app:** \`examples/taskflow/\`
|
|
191
|
+
- **JSON Schemas:** \`schema/\`
|
|
183
192
|
|
|
184
193
|
## Token file structure — root wrapper key required
|
|
185
194
|
|
|
@@ -219,9 +228,9 @@ Root keys: \`color\`, \`typography\`, \`spacing\`, \`elevation\`, \`motion\`, \`
|
|
|
219
228
|
| \`tokens/themes.yaml\` | \`tokens/themes.schema.json\` | \`themes\` |
|
|
220
229
|
| \`tokens/icons.yaml\` | \`tokens/icons.schema.json\` | \`icons\` |
|
|
221
230
|
|
|
222
|
-
All schemas are in \`
|
|
231
|
+
All schemas are in \`schema/\` inside the installed package. Shared type definitions (actions, data-binding, adaptive, validation, common) are in \`schema/defs/\`.
|
|
223
232
|
|
|
224
|
-
**Workflow:** read the schema → read an example from \`
|
|
233
|
+
**Workflow:** read the schema → read an example from \`examples/taskflow/\` → create the YAML → run \`openuispec validate\`.
|
|
225
234
|
|
|
226
235
|
## Spec format quick reference
|
|
227
236
|
|
|
@@ -247,12 +256,12 @@ This project generates native code for: **${targetList}**
|
|
|
247
256
|
|
|
248
257
|
## Learn more
|
|
249
258
|
|
|
250
|
-
All docs and examples are
|
|
259
|
+
All docs and examples are in the installed \`openuispec\` package — check \`node_modules/openuispec/\` or run \`npm root -g\` for the global install path.
|
|
251
260
|
|
|
252
|
-
- Full spec: \`
|
|
253
|
-
- Example app: \`
|
|
254
|
-
- JSON Schemas: \`
|
|
255
|
-
-
|
|
261
|
+
- Full spec: \`spec/openuispec-v0.1.md\`
|
|
262
|
+
- Example app: \`examples/taskflow/\`
|
|
263
|
+
- JSON Schemas: \`schema/\`
|
|
264
|
+
- Online reference: \`https://openuispec.rsteam.uz/llms-full.txt\`
|
|
256
265
|
`;
|
|
257
266
|
}
|
|
258
267
|
|
|
@@ -278,6 +287,8 @@ OpenUISpec is a YAML-based spec format that describes an app's UI semantically
|
|
|
278
287
|
- Platform: \`${specDir}/platform/\` — per-target overrides (iOS, Android, Web)
|
|
279
288
|
- Locales: \`${specDir}/locales/\` — i18n strings (JSON, ICU MessageFormat)
|
|
280
289
|
|
|
290
|
+
**Note:** These are the default paths. Actual paths are in \`includes:\` in \`openuispec.yaml\` and may use relative paths (e.g. \`../../shared/locales\`). Always read \`openuispec.yaml\` to find the real directories.
|
|
291
|
+
|
|
281
292
|
## If spec directories are empty (first-time setup)
|
|
282
293
|
This means the project has existing UI code but hasn't been specced yet. Your job:
|
|
283
294
|
|
|
@@ -311,15 +322,18 @@ This means the project has existing UI code but hasn't been specced yet. Your jo
|
|
|
311
322
|
2. Run \`openuispec drift --snapshot --target <target>\` for each affected platform.
|
|
312
323
|
3. Run \`openuispec drift\` to verify no untracked drift remains.
|
|
313
324
|
|
|
314
|
-
## Learning OpenUISpec —
|
|
315
|
-
All documentation is
|
|
316
|
-
1. **
|
|
317
|
-
2. **
|
|
318
|
-
3. **
|
|
325
|
+
## Learning OpenUISpec — where to find the docs
|
|
326
|
+
All documentation is in the installed \`openuispec\` package. Search in this order:
|
|
327
|
+
1. **Local:** \`node_modules/openuispec/\` (project dependency)
|
|
328
|
+
2. **Global:** run \`npm root -g\` to get the global prefix, then look in \`<prefix>/openuispec/\`
|
|
329
|
+
3. **Online fallback:** if not installed, fetch from:
|
|
330
|
+
- \`https://openuispec.rsteam.uz/llms-full.txt\` — complete spec + all JSON schemas
|
|
331
|
+
- \`https://openuispec.rsteam.uz/llms.txt\` — concise summary with links
|
|
319
332
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
333
|
+
Inside the package:
|
|
334
|
+
1. **Full specification:** \`spec/openuispec-v0.1.md\` — the complete spec (read this to understand the format)
|
|
335
|
+
2. **Example app:** \`examples/taskflow/\` — a complete working app with all file types
|
|
336
|
+
3. **JSON Schemas:** \`schema/\` — validation schemas that define the exact structure of every file type
|
|
323
337
|
|
|
324
338
|
## Token file structure — root wrapper key required
|
|
325
339
|
Every token file must have a single root key matching the token type. Do NOT put properties at the top level.
|
|
@@ -335,7 +349,7 @@ Every token file must have a single root key matching the token type. Do NOT put
|
|
|
335
349
|
## File formats and schemas — read before creating spec files
|
|
336
350
|
Before creating or editing any spec file, read the corresponding JSON Schema. Do not guess the file format.
|
|
337
351
|
|
|
338
|
-
| File | Schema (in \`
|
|
352
|
+
| File | Schema (in \`schema/\` inside the installed package) | Root key |
|
|
339
353
|
|------|--------|----------|
|
|
340
354
|
| \`openuispec.yaml\` | \`openuispec.schema.json\` | \`spec_version\` |
|
|
341
355
|
| \`screens/*.yaml\` | \`screen.schema.json\` | \`<screen_id>\` |
|
|
@@ -354,7 +368,7 @@ Before creating or editing any spec file, read the corresponding JSON Schema. Do
|
|
|
354
368
|
|
|
355
369
|
Shared type definitions (actions, data-binding, adaptive, validation, common) are in \`schema/defs/\`.
|
|
356
370
|
|
|
357
|
-
Workflow: read the schema → read an example from \`
|
|
371
|
+
Workflow: read the schema → read an example from \`examples/taskflow/\` → create the YAML → run \`openuispec validate\`.
|
|
358
372
|
|
|
359
373
|
## Spec format reference
|
|
360
374
|
- 7 contract families: nav_container, surface, action_trigger, input_field, data_display, collection, feedback
|
|
@@ -499,7 +513,7 @@ Commands:
|
|
|
499
513
|
|
|
500
514
|
AI rules have been added to CLAUDE.md and AGENTS.md.
|
|
501
515
|
|
|
502
|
-
Docs:
|
|
516
|
+
Docs: https://openuispec.rsteam.uz
|
|
503
517
|
`);
|
|
504
518
|
} catch (err) {
|
|
505
519
|
rl.close();
|
package/package.json
CHANGED
|
@@ -18,9 +18,25 @@
|
|
|
18
18
|
},
|
|
19
19
|
"patternProperties": {
|
|
20
20
|
"^[a-zA-Z_][a-zA-Z0-9_.]*$": {
|
|
21
|
-
"
|
|
22
|
-
"description": "Translation string (ICU MessageFormat)"
|
|
21
|
+
"$ref": "#/$defs/message_value"
|
|
23
22
|
}
|
|
24
23
|
},
|
|
25
|
-
"additionalProperties": false
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"$defs": {
|
|
26
|
+
"message_value": {
|
|
27
|
+
"description": "Translation string (ICU MessageFormat) or nested group of translations",
|
|
28
|
+
"oneOf": [
|
|
29
|
+
{ "type": "string" },
|
|
30
|
+
{
|
|
31
|
+
"type": "object",
|
|
32
|
+
"patternProperties": {
|
|
33
|
+
"^[a-zA-Z_][a-zA-Z0-9_.]*$": {
|
|
34
|
+
"$ref": "#/$defs/message_value"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"additionalProperties": false
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
26
42
|
}
|
package/schema/validate.ts
CHANGED
|
@@ -97,10 +97,8 @@ function validateFile(
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// Convert schema URL to a local path for display
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
"node_modules/openuispec/schema/",
|
|
103
|
-
);
|
|
100
|
+
const schemaRelPath = schemaId.replace(BASE, "");
|
|
101
|
+
const schemaLocalPath = resolve(SCHEMA_DIR, schemaRelPath);
|
|
104
102
|
|
|
105
103
|
const errors: ErrorObject[] = validate.errors ?? [];
|
|
106
104
|
console.log(` FAIL ${name} (${errors.length} error(s))`);
|
|
@@ -150,9 +148,46 @@ function validateFile(
|
|
|
150
148
|
|
|
151
149
|
// ── validation groups ────────────────────────────────────────────────
|
|
152
150
|
|
|
151
|
+
// ── includes resolution ──────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
interface Includes {
|
|
154
|
+
tokens: string;
|
|
155
|
+
contracts: string;
|
|
156
|
+
screens: string;
|
|
157
|
+
flows: string;
|
|
158
|
+
platform: string;
|
|
159
|
+
locales: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const DEFAULT_INCLUDES: Includes = {
|
|
163
|
+
tokens: "./tokens/",
|
|
164
|
+
contracts: "./contracts/",
|
|
165
|
+
screens: "./screens/",
|
|
166
|
+
flows: "./flows/",
|
|
167
|
+
platform: "./platform/",
|
|
168
|
+
locales: "./locales/",
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
function readIncludes(projectDir: string): Includes {
|
|
172
|
+
const manifestPath = join(projectDir, "openuispec.yaml");
|
|
173
|
+
try {
|
|
174
|
+
const manifest = loadYaml(manifestPath) as Record<string, unknown>;
|
|
175
|
+
const inc = manifest?.includes as Partial<Includes> | undefined;
|
|
176
|
+
return { ...DEFAULT_INCLUDES, ...inc };
|
|
177
|
+
} catch {
|
|
178
|
+
return DEFAULT_INCLUDES;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function resolveInclude(projectDir: string, includePath: string): string {
|
|
183
|
+
return resolve(projectDir, includePath);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── validation groups ────────────────────────────────────────────────
|
|
187
|
+
|
|
153
188
|
interface ValidationGroup {
|
|
154
189
|
label: string;
|
|
155
|
-
run(ajv: AjvInstance, projectDir: string): number;
|
|
190
|
+
run(ajv: AjvInstance, projectDir: string, includes: Includes): number;
|
|
156
191
|
}
|
|
157
192
|
|
|
158
193
|
const GROUPS: Record<string, ValidationGroup> = {
|
|
@@ -169,8 +204,9 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
169
204
|
|
|
170
205
|
tokens: {
|
|
171
206
|
label: "Tokens",
|
|
172
|
-
run(ajv, projectDir) {
|
|
207
|
+
run(ajv, projectDir, includes) {
|
|
173
208
|
let errors = 0;
|
|
209
|
+
const tokensDir = resolveInclude(projectDir, includes.tokens);
|
|
174
210
|
const tokenMap: Record<string, string> = {
|
|
175
211
|
"color.yaml": "color.schema.json",
|
|
176
212
|
"typography.yaml": "typography.schema.json",
|
|
@@ -182,7 +218,7 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
182
218
|
"icons.yaml": "icons.schema.json",
|
|
183
219
|
};
|
|
184
220
|
for (const [data, schema] of Object.entries(tokenMap)) {
|
|
185
|
-
const filePath = join(
|
|
221
|
+
const filePath = join(tokensDir, data);
|
|
186
222
|
if (existsSync(filePath)) {
|
|
187
223
|
errors += validateFile(ajv, filePath, `${BASE}tokens/${schema}`);
|
|
188
224
|
}
|
|
@@ -193,9 +229,10 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
193
229
|
|
|
194
230
|
screens: {
|
|
195
231
|
label: "Screens",
|
|
196
|
-
run(ajv, projectDir) {
|
|
232
|
+
run(ajv, projectDir, includes) {
|
|
197
233
|
let errors = 0;
|
|
198
|
-
|
|
234
|
+
const dir = resolveInclude(projectDir, includes.screens);
|
|
235
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
199
236
|
errors += validateFile(ajv, f, `${BASE}screen.schema.json`);
|
|
200
237
|
}
|
|
201
238
|
return errors;
|
|
@@ -204,9 +241,10 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
204
241
|
|
|
205
242
|
flows: {
|
|
206
243
|
label: "Flows",
|
|
207
|
-
run(ajv, projectDir) {
|
|
244
|
+
run(ajv, projectDir, includes) {
|
|
208
245
|
let errors = 0;
|
|
209
|
-
|
|
246
|
+
const dir = resolveInclude(projectDir, includes.flows);
|
|
247
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
210
248
|
errors += validateFile(ajv, f, `${BASE}flow.schema.json`);
|
|
211
249
|
}
|
|
212
250
|
return errors;
|
|
@@ -215,9 +253,10 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
215
253
|
|
|
216
254
|
platform: {
|
|
217
255
|
label: "Platform",
|
|
218
|
-
run(ajv, projectDir) {
|
|
256
|
+
run(ajv, projectDir, includes) {
|
|
219
257
|
let errors = 0;
|
|
220
|
-
|
|
258
|
+
const dir = resolveInclude(projectDir, includes.platform);
|
|
259
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
221
260
|
errors += validateFile(ajv, f, `${BASE}platform.schema.json`);
|
|
222
261
|
}
|
|
223
262
|
return errors;
|
|
@@ -226,9 +265,10 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
226
265
|
|
|
227
266
|
locales: {
|
|
228
267
|
label: "Locales",
|
|
229
|
-
run(ajv, projectDir) {
|
|
268
|
+
run(ajv, projectDir, includes) {
|
|
230
269
|
let errors = 0;
|
|
231
|
-
|
|
270
|
+
const dir = resolveInclude(projectDir, includes.locales);
|
|
271
|
+
for (const f of listFiles(dir, ".json")) {
|
|
232
272
|
errors += validateFile(ajv, f, `${BASE}locale.schema.json`);
|
|
233
273
|
}
|
|
234
274
|
return errors;
|
|
@@ -237,9 +277,10 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
237
277
|
|
|
238
278
|
custom_contracts: {
|
|
239
279
|
label: "Custom contracts",
|
|
240
|
-
run(ajv, projectDir) {
|
|
280
|
+
run(ajv, projectDir, includes) {
|
|
241
281
|
let errors = 0;
|
|
242
|
-
|
|
282
|
+
const dir = resolveInclude(projectDir, includes.contracts);
|
|
283
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
243
284
|
if (basename(f).startsWith("x_")) {
|
|
244
285
|
errors += validateFile(
|
|
245
286
|
ajv,
|
|
@@ -293,13 +334,14 @@ export function runValidate(argv: string[]): void {
|
|
|
293
334
|
}
|
|
294
335
|
|
|
295
336
|
const projectDir = findProjectDir(process.cwd());
|
|
337
|
+
const includes = readIncludes(projectDir);
|
|
296
338
|
const ajv = buildAjv();
|
|
297
339
|
let totalErrors = 0;
|
|
298
340
|
|
|
299
341
|
for (const key of selected) {
|
|
300
342
|
const group = GROUPS[key];
|
|
301
343
|
console.log(`\n${group.label}:`);
|
|
302
|
-
totalErrors += group.run(ajv, projectDir);
|
|
344
|
+
totalErrors += group.run(ajv, projectDir, includes);
|
|
303
345
|
}
|
|
304
346
|
|
|
305
347
|
console.log(`\n${"=".repeat(50)}`);
|