openuispec 0.1.13 → 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/init.ts +8 -0
- package/package.json +1 -1
- package/schema/locale.schema.json +19 -3
- package/schema/validate.ts +58 -14
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.
|
|
@@ -281,6 +287,8 @@ OpenUISpec is a YAML-based spec format that describes an app's UI semantically
|
|
|
281
287
|
- Platform: \`${specDir}/platform/\` — per-target overrides (iOS, Android, Web)
|
|
282
288
|
- Locales: \`${specDir}/locales/\` — i18n strings (JSON, ICU MessageFormat)
|
|
283
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
|
+
|
|
284
292
|
## If spec directories are empty (first-time setup)
|
|
285
293
|
This means the project has existing UI code but hasn't been specced yet. Your job:
|
|
286
294
|
|
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
|
@@ -148,9 +148,46 @@ function validateFile(
|
|
|
148
148
|
|
|
149
149
|
// ── validation groups ────────────────────────────────────────────────
|
|
150
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
|
+
|
|
151
188
|
interface ValidationGroup {
|
|
152
189
|
label: string;
|
|
153
|
-
run(ajv: AjvInstance, projectDir: string): number;
|
|
190
|
+
run(ajv: AjvInstance, projectDir: string, includes: Includes): number;
|
|
154
191
|
}
|
|
155
192
|
|
|
156
193
|
const GROUPS: Record<string, ValidationGroup> = {
|
|
@@ -167,8 +204,9 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
167
204
|
|
|
168
205
|
tokens: {
|
|
169
206
|
label: "Tokens",
|
|
170
|
-
run(ajv, projectDir) {
|
|
207
|
+
run(ajv, projectDir, includes) {
|
|
171
208
|
let errors = 0;
|
|
209
|
+
const tokensDir = resolveInclude(projectDir, includes.tokens);
|
|
172
210
|
const tokenMap: Record<string, string> = {
|
|
173
211
|
"color.yaml": "color.schema.json",
|
|
174
212
|
"typography.yaml": "typography.schema.json",
|
|
@@ -180,7 +218,7 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
180
218
|
"icons.yaml": "icons.schema.json",
|
|
181
219
|
};
|
|
182
220
|
for (const [data, schema] of Object.entries(tokenMap)) {
|
|
183
|
-
const filePath = join(
|
|
221
|
+
const filePath = join(tokensDir, data);
|
|
184
222
|
if (existsSync(filePath)) {
|
|
185
223
|
errors += validateFile(ajv, filePath, `${BASE}tokens/${schema}`);
|
|
186
224
|
}
|
|
@@ -191,9 +229,10 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
191
229
|
|
|
192
230
|
screens: {
|
|
193
231
|
label: "Screens",
|
|
194
|
-
run(ajv, projectDir) {
|
|
232
|
+
run(ajv, projectDir, includes) {
|
|
195
233
|
let errors = 0;
|
|
196
|
-
|
|
234
|
+
const dir = resolveInclude(projectDir, includes.screens);
|
|
235
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
197
236
|
errors += validateFile(ajv, f, `${BASE}screen.schema.json`);
|
|
198
237
|
}
|
|
199
238
|
return errors;
|
|
@@ -202,9 +241,10 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
202
241
|
|
|
203
242
|
flows: {
|
|
204
243
|
label: "Flows",
|
|
205
|
-
run(ajv, projectDir) {
|
|
244
|
+
run(ajv, projectDir, includes) {
|
|
206
245
|
let errors = 0;
|
|
207
|
-
|
|
246
|
+
const dir = resolveInclude(projectDir, includes.flows);
|
|
247
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
208
248
|
errors += validateFile(ajv, f, `${BASE}flow.schema.json`);
|
|
209
249
|
}
|
|
210
250
|
return errors;
|
|
@@ -213,9 +253,10 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
213
253
|
|
|
214
254
|
platform: {
|
|
215
255
|
label: "Platform",
|
|
216
|
-
run(ajv, projectDir) {
|
|
256
|
+
run(ajv, projectDir, includes) {
|
|
217
257
|
let errors = 0;
|
|
218
|
-
|
|
258
|
+
const dir = resolveInclude(projectDir, includes.platform);
|
|
259
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
219
260
|
errors += validateFile(ajv, f, `${BASE}platform.schema.json`);
|
|
220
261
|
}
|
|
221
262
|
return errors;
|
|
@@ -224,9 +265,10 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
224
265
|
|
|
225
266
|
locales: {
|
|
226
267
|
label: "Locales",
|
|
227
|
-
run(ajv, projectDir) {
|
|
268
|
+
run(ajv, projectDir, includes) {
|
|
228
269
|
let errors = 0;
|
|
229
|
-
|
|
270
|
+
const dir = resolveInclude(projectDir, includes.locales);
|
|
271
|
+
for (const f of listFiles(dir, ".json")) {
|
|
230
272
|
errors += validateFile(ajv, f, `${BASE}locale.schema.json`);
|
|
231
273
|
}
|
|
232
274
|
return errors;
|
|
@@ -235,9 +277,10 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
235
277
|
|
|
236
278
|
custom_contracts: {
|
|
237
279
|
label: "Custom contracts",
|
|
238
|
-
run(ajv, projectDir) {
|
|
280
|
+
run(ajv, projectDir, includes) {
|
|
239
281
|
let errors = 0;
|
|
240
|
-
|
|
282
|
+
const dir = resolveInclude(projectDir, includes.contracts);
|
|
283
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
241
284
|
if (basename(f).startsWith("x_")) {
|
|
242
285
|
errors += validateFile(
|
|
243
286
|
ajv,
|
|
@@ -291,13 +334,14 @@ export function runValidate(argv: string[]): void {
|
|
|
291
334
|
}
|
|
292
335
|
|
|
293
336
|
const projectDir = findProjectDir(process.cwd());
|
|
337
|
+
const includes = readIncludes(projectDir);
|
|
294
338
|
const ajv = buildAjv();
|
|
295
339
|
let totalErrors = 0;
|
|
296
340
|
|
|
297
341
|
for (const key of selected) {
|
|
298
342
|
const group = GROUPS[key];
|
|
299
343
|
console.log(`\n${group.label}:`);
|
|
300
|
-
totalErrors += group.run(ajv, projectDir);
|
|
344
|
+
totalErrors += group.run(ajv, projectDir, includes);
|
|
301
345
|
}
|
|
302
346
|
|
|
303
347
|
console.log(`\n${"=".repeat(50)}`);
|