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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",
@@ -18,9 +18,25 @@
18
18
  },
19
19
  "patternProperties": {
20
20
  "^[a-zA-Z_][a-zA-Z0-9_.]*$": {
21
- "type": "string",
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
  }
@@ -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(projectDir, "tokens", data);
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
- for (const f of listFiles(join(projectDir, "screens"), ".yaml")) {
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
- for (const f of listFiles(join(projectDir, "flows"), ".yaml")) {
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
- for (const f of listFiles(join(projectDir, "platform"), ".yaml")) {
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
- for (const f of listFiles(join(projectDir, "locales"), ".json")) {
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
- for (const f of listFiles(join(projectDir, "contracts"), ".yaml")) {
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)}`);