@ankhorage/devtools 1.10.12 → 1.10.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/dist/tools/skills/assets/zora-designer/SKILL.md +2 -2
- package/dist/tools/skills/assets/zora-designer/references/artifact.md +1 -1
- package/dist/tools/skills/assets/zora-designer/references/audit.md +2 -2
- package/dist/tools/skills/assets/zora-designer/references/screens.md +1 -1
- package/dist/tools/skills/assets/zora-designer/references/workflow.md +2 -2
- package/dist/tools/skills/assets/zora-designer/scripts/{audit.mjs → audit.ts} +280 -49
- package/dist/tools/skills/assets/zora-designer/scripts/{generate-template-catalog.mjs → generate-template-catalog.ts} +15 -7
- package/dist/tools/skills/assets/zora-designer/scripts/{owner-api.mjs → owner-api.ts} +359 -52
- package/dist/tools/skills/assets/zora-designer/scripts/{scaffold-template.mjs → scaffold-template.ts} +27 -21
- package/package.json +1 -1
|
@@ -4,6 +4,117 @@ import { readFile } from 'node:fs/promises';
|
|
|
4
4
|
import { dirname, join, parse, resolve } from 'node:path';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
|
|
7
|
+
interface OwnerRequirement {
|
|
8
|
+
exports: readonly string[];
|
|
9
|
+
minimumVersion: string;
|
|
10
|
+
packageName: string;
|
|
11
|
+
specifier: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Diagnostic extends Record<string, unknown> {
|
|
15
|
+
severity?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ComputedTheme extends Record<string, unknown> {
|
|
19
|
+
diagnostics: Diagnostic[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface DesignCompilation extends Record<string, unknown> {
|
|
23
|
+
computedTheme: ComputedTheme;
|
|
24
|
+
diagnostics: Diagnostic[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ManifestComposition extends Record<string, unknown> {
|
|
28
|
+
diagnostics: Diagnostic[];
|
|
29
|
+
status: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ColorTheoryApi extends Record<string, unknown> {
|
|
33
|
+
COLOR_HARMONIES: readonly string[];
|
|
34
|
+
COLOR_HARMONY_CATALOG: readonly Record<string, unknown>[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ContractsApi extends Record<string, unknown> {
|
|
38
|
+
APP_CATEGORIES: readonly string[];
|
|
39
|
+
NAVIGATOR_TYPES: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TemplatesApi extends Record<string, unknown> {
|
|
43
|
+
CATEGORY_PRESETS: Record<string, Record<string, unknown>>;
|
|
44
|
+
TONE_PAIR_CATALOG: readonly Record<string, unknown>[];
|
|
45
|
+
assertTemplateManifestReady: (composition: ManifestComposition) => Record<string, unknown>;
|
|
46
|
+
compileCategoryDesign: (category: string, theme: Record<string, unknown>) => DesignCompilation;
|
|
47
|
+
composeCategoryAppManifest: (input: Record<string, unknown>) => ManifestComposition;
|
|
48
|
+
resolveCategoryDesignPreset: (...arguments_: unknown[]) => unknown;
|
|
49
|
+
resolveTonePair: (...arguments_: unknown[]) => unknown;
|
|
50
|
+
validateTemplateManifest: (
|
|
51
|
+
manifest: Record<string, unknown>,
|
|
52
|
+
authoringState: string,
|
|
53
|
+
) => ManifestComposition;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ZoraThemeApi extends Record<string, unknown> {
|
|
57
|
+
compileZoraTheme: (...arguments_: unknown[]) => unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface EventMetadata extends Record<string, unknown> {
|
|
61
|
+
description: string;
|
|
62
|
+
eventType: string;
|
|
63
|
+
label: string;
|
|
64
|
+
payloadFields?: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ComponentMetadata extends Record<string, unknown> {
|
|
68
|
+
allowedChildren: readonly string[];
|
|
69
|
+
directManifestNode: boolean;
|
|
70
|
+
events?: Record<string, EventMetadata>;
|
|
71
|
+
manifestPolicy?: { kind?: unknown };
|
|
72
|
+
name: string;
|
|
73
|
+
props: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface RecipeFieldMetadata extends Record<string, unknown> {
|
|
77
|
+
options?: readonly string[];
|
|
78
|
+
type: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface RecipeMetadata extends Record<string, unknown> {
|
|
82
|
+
fields: Record<string, RecipeFieldMetadata | undefined>;
|
|
83
|
+
kind: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ZoraMetadataApi extends Record<string, unknown> {
|
|
87
|
+
ZORA_COMPONENT_META: Record<string, ComponentMetadata | undefined>;
|
|
88
|
+
ZORA_THEME_RECIPE_META: Record<string, RecipeMetadata | undefined>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ManifestNode extends Record<string, unknown> {
|
|
92
|
+
children?: ManifestNode[];
|
|
93
|
+
id: string;
|
|
94
|
+
props?: Record<string, unknown>;
|
|
95
|
+
type: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface ManifestScreen extends Record<string, unknown> {
|
|
99
|
+
root: ManifestNode;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface Region extends Record<string, unknown> {
|
|
103
|
+
component?: unknown;
|
|
104
|
+
evidenceId?: unknown;
|
|
105
|
+
id: string;
|
|
106
|
+
parentNodeId?: unknown;
|
|
107
|
+
props?: unknown;
|
|
108
|
+
reason?: unknown;
|
|
109
|
+
requestedCapability: string;
|
|
110
|
+
screenId: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface LoadedOwnerModule {
|
|
114
|
+
module: Record<string, unknown>;
|
|
115
|
+
version: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
7
118
|
const OWNER_RELEASES = {
|
|
8
119
|
colorTheory: { packageName: '@ankhorage/color-theory', minimumVersion: '0.3.0' },
|
|
9
120
|
contracts: { packageName: '@ankhorage/contracts', minimumVersion: '8.2.0' },
|
|
@@ -48,33 +159,52 @@ const OWNER_REQUIREMENTS = {
|
|
|
48
159
|
},
|
|
49
160
|
};
|
|
50
161
|
|
|
162
|
+
/*** Expose the runtime owner gates so tests and callers never duplicate managed versions. */
|
|
163
|
+
export function inspectOwnerRequirements() {
|
|
164
|
+
return structuredClone(OWNER_RELEASES);
|
|
165
|
+
}
|
|
166
|
+
|
|
51
167
|
/*** Load and validate every released owner API from one target repository. */
|
|
52
168
|
export async function loadOwnerApis(targetDirectory = process.cwd()) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
169
|
+
const colorTheory = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.colorTheory);
|
|
170
|
+
const contracts = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.contracts);
|
|
171
|
+
const templates = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.templates);
|
|
172
|
+
const zoraTheme = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.zoraTheme);
|
|
173
|
+
const zoraMetadata = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.zoraMetadata);
|
|
174
|
+
assertColorTheoryApi(colorTheory.module);
|
|
175
|
+
assertContractsApi(contracts.module);
|
|
176
|
+
assertTemplatesApi(templates.module);
|
|
177
|
+
assertZoraThemeApi(zoraTheme.module);
|
|
178
|
+
assertZoraMetadataApi(zoraMetadata.module);
|
|
58
179
|
|
|
59
180
|
return {
|
|
60
|
-
colorTheory:
|
|
61
|
-
contracts:
|
|
62
|
-
templates:
|
|
63
|
-
zoraTheme:
|
|
64
|
-
zoraMetadata:
|
|
181
|
+
colorTheory: colorTheory.module,
|
|
182
|
+
contracts: contracts.module,
|
|
183
|
+
templates: templates.module,
|
|
184
|
+
zoraTheme: zoraTheme.module,
|
|
185
|
+
zoraMetadata: zoraMetadata.module,
|
|
65
186
|
versions: {
|
|
66
|
-
colorTheory:
|
|
67
|
-
contracts:
|
|
68
|
-
templates:
|
|
69
|
-
zora:
|
|
187
|
+
colorTheory: colorTheory.version,
|
|
188
|
+
contracts: contracts.version,
|
|
189
|
+
templates: templates.version,
|
|
190
|
+
zora: zoraTheme.version,
|
|
70
191
|
},
|
|
71
192
|
};
|
|
72
193
|
}
|
|
73
194
|
|
|
195
|
+
/*** Load only Contracts for tooling that runs before another owner package has been built. */
|
|
196
|
+
export async function loadContractsApi(targetDirectory = process.cwd()): Promise<ContractsApi> {
|
|
197
|
+
const loaded = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.contracts);
|
|
198
|
+
assertContractsApi(loaded.module);
|
|
199
|
+
return loaded.module;
|
|
200
|
+
}
|
|
201
|
+
|
|
74
202
|
/*** Return installed catalogs and metadata names without copying owner definitions. */
|
|
75
203
|
export async function inspectOwnerApis(targetDirectory = process.cwd()) {
|
|
76
204
|
const owners = await loadOwnerApis(targetDirectory);
|
|
77
|
-
const componentMetadata = Object.values(owners.zoraMetadata.ZORA_COMPONENT_META)
|
|
205
|
+
const componentMetadata = Object.values(owners.zoraMetadata.ZORA_COMPONENT_META).filter(
|
|
206
|
+
(metadata) => metadata !== undefined,
|
|
207
|
+
);
|
|
78
208
|
return {
|
|
79
209
|
versions: owners.versions,
|
|
80
210
|
appCategories: owners.contracts.APP_CATEGORIES,
|
|
@@ -104,20 +234,22 @@ export async function inspectOwnerApis(targetDirectory = process.cwd()) {
|
|
|
104
234
|
}
|
|
105
235
|
|
|
106
236
|
/*** Compose one design without turning missing runtime/UI capabilities into a design blocker. */
|
|
107
|
-
export async function composeDesign(input, targetDirectory = process.cwd()) {
|
|
237
|
+
export async function composeDesign(input: unknown, targetDirectory = process.cwd()) {
|
|
108
238
|
const owners = await loadOwnerApis(targetDirectory);
|
|
109
239
|
assertRecord(input, 'Design input');
|
|
110
240
|
assertNonEmptyString(input.category, 'category');
|
|
111
241
|
assertRecord(input.navigator, 'navigator');
|
|
112
|
-
|
|
113
|
-
|
|
242
|
+
const screens = readManifestScreens(input.screens);
|
|
243
|
+
const theme = input.theme === undefined ? {} : input.theme;
|
|
244
|
+
assertRecord(theme, 'theme');
|
|
245
|
+
assertSupportedThemeRecipes(theme.recipes, owners.zoraMetadata.ZORA_THEME_RECIPE_META);
|
|
114
246
|
|
|
115
247
|
const regionResult = resolveRegionNodes(
|
|
116
|
-
|
|
248
|
+
screens,
|
|
117
249
|
Array.isArray(input.regions) ? input.regions : [],
|
|
118
250
|
owners.zoraMetadata.ZORA_COMPONENT_META,
|
|
119
251
|
);
|
|
120
|
-
const design = owners.templates.compileCategoryDesign(input.category,
|
|
252
|
+
const design = owners.templates.compileCategoryDesign(input.category, theme);
|
|
121
253
|
const { computedTheme, ...resolvedDesign } = design;
|
|
122
254
|
const requestedAuthoringState = input.authoringState === 'release' ? 'release' : 'draft';
|
|
123
255
|
const composition = owners.templates.composeCategoryAppManifest({
|
|
@@ -156,7 +288,11 @@ export async function composeDesign(input, targetDirectory = process.cwd()) {
|
|
|
156
288
|
}
|
|
157
289
|
|
|
158
290
|
/*** Resolve explicit region decisions using exact metadata or a visible non-blocking Box placeholder. */
|
|
159
|
-
export function resolveRegionNodes(
|
|
291
|
+
export function resolveRegionNodes(
|
|
292
|
+
screens: Record<string, ManifestScreen | undefined>,
|
|
293
|
+
regions: readonly unknown[],
|
|
294
|
+
componentMeta: Record<string, ComponentMetadata | undefined>,
|
|
295
|
+
) {
|
|
160
296
|
const resolvedScreens = structuredClone(screens);
|
|
161
297
|
const diagnostics = [];
|
|
162
298
|
const gaps = [];
|
|
@@ -166,6 +302,7 @@ export function resolveRegionNodes(screens, regions, componentMeta) {
|
|
|
166
302
|
assertNonEmptyString(region.id, 'region.id');
|
|
167
303
|
assertNonEmptyString(region.screenId, 'region.screenId');
|
|
168
304
|
assertNonEmptyString(region.requestedCapability, 'region.requestedCapability');
|
|
305
|
+
assertRegion(region);
|
|
169
306
|
const screen = resolvedScreens[region.screenId];
|
|
170
307
|
if (!screen) {
|
|
171
308
|
throw new Error(`Region "${region.id}" targets unknown screen "${region.screenId}".`);
|
|
@@ -182,7 +319,10 @@ export function resolveRegionNodes(screens, regions, componentMeta) {
|
|
|
182
319
|
return { screens: resolvedScreens, diagnostics, gaps };
|
|
183
320
|
}
|
|
184
321
|
|
|
185
|
-
function resolveRegionNode(
|
|
322
|
+
function resolveRegionNode(
|
|
323
|
+
region: Region,
|
|
324
|
+
componentMeta: Record<string, ComponentMetadata | undefined>,
|
|
325
|
+
) {
|
|
186
326
|
const component = typeof region.component === 'string' ? region.component : null;
|
|
187
327
|
const meta = component === null ? null : componentMeta[component];
|
|
188
328
|
if (meta && meta.directManifestNode && meta.manifestPolicy?.kind !== 'unresolved-element') {
|
|
@@ -218,7 +358,6 @@ function resolveRegionNode(region, componentMeta) {
|
|
|
218
358
|
return {
|
|
219
359
|
node: { id: region.id, type: placeholderMeta.name, props: {} },
|
|
220
360
|
diagnostic: {
|
|
221
|
-
regionId: region.id,
|
|
222
361
|
status: 'placeholder',
|
|
223
362
|
component: placeholderMeta.name,
|
|
224
363
|
...gap,
|
|
@@ -228,7 +367,11 @@ function resolveRegionNode(region, componentMeta) {
|
|
|
228
367
|
}
|
|
229
368
|
|
|
230
369
|
/*** Validate that manifest props are declared by the selected component metadata. */
|
|
231
|
-
function assertSupportedProps(
|
|
370
|
+
function assertSupportedProps(
|
|
371
|
+
props: unknown,
|
|
372
|
+
meta: ComponentMetadata,
|
|
373
|
+
regionId: string,
|
|
374
|
+
): Record<string, unknown> {
|
|
232
375
|
assertRecord(props, `props for region "${regionId}"`);
|
|
233
376
|
const unsupported = Object.keys(props).filter((name) => !(name in meta.props));
|
|
234
377
|
if (unsupported.length > 0) {
|
|
@@ -240,7 +383,12 @@ function assertSupportedProps(props, meta, regionId) {
|
|
|
240
383
|
}
|
|
241
384
|
|
|
242
385
|
/*** Insert a region node under the declared parent or the target screen root. */
|
|
243
|
-
function insertRegionNode(
|
|
386
|
+
function insertRegionNode(
|
|
387
|
+
screen: ManifestScreen,
|
|
388
|
+
parentNodeId: unknown,
|
|
389
|
+
node: ManifestNode,
|
|
390
|
+
componentMeta: Record<string, ComponentMetadata | undefined>,
|
|
391
|
+
): void {
|
|
244
392
|
const parent =
|
|
245
393
|
typeof parentNodeId === 'string' && parentNodeId !== ''
|
|
246
394
|
? findNode(screen.root, parentNodeId)
|
|
@@ -249,7 +397,7 @@ function insertRegionNode(screen, parentNodeId, node, componentMeta) {
|
|
|
249
397
|
throw new Error(`Region parent node not found: ${String(parentNodeId)}`);
|
|
250
398
|
}
|
|
251
399
|
const parentMeta = componentMeta[parent.type];
|
|
252
|
-
if (!parentMeta
|
|
400
|
+
if (!parentMeta?.directManifestNode) {
|
|
253
401
|
throw new Error(`Region parent "${parent.id}" is absent from direct ZORA manifest metadata.`);
|
|
254
402
|
}
|
|
255
403
|
if (!parentMeta.allowedChildren.includes(node.type)) {
|
|
@@ -261,7 +409,10 @@ function insertRegionNode(screen, parentNodeId, node, componentMeta) {
|
|
|
261
409
|
}
|
|
262
410
|
|
|
263
411
|
/*** Validate persisted component and pattern recipe overrides against exact ZORA metadata fields. */
|
|
264
|
-
function assertSupportedThemeRecipes(
|
|
412
|
+
function assertSupportedThemeRecipes(
|
|
413
|
+
recipes: unknown,
|
|
414
|
+
recipeMeta: Record<string, RecipeMetadata | undefined>,
|
|
415
|
+
): void {
|
|
265
416
|
if (recipes === undefined) return;
|
|
266
417
|
assertRecord(recipes, 'theme.recipes');
|
|
267
418
|
for (const [group, kind] of [
|
|
@@ -273,13 +424,16 @@ function assertSupportedThemeRecipes(recipes, recipeMeta) {
|
|
|
273
424
|
assertRecord(overrides, `theme.recipes.${group}`);
|
|
274
425
|
for (const [recipeName, fields] of Object.entries(overrides)) {
|
|
275
426
|
const meta = recipeMeta[recipeName];
|
|
276
|
-
if (
|
|
427
|
+
if (meta?.kind !== kind) {
|
|
277
428
|
throw new Error(`Unknown ZORA ${kind} theme recipe: ${recipeName}.`);
|
|
278
429
|
}
|
|
279
430
|
assertRecord(fields, `theme recipe ${recipeName}`);
|
|
280
431
|
for (const [fieldName, value] of Object.entries(fields)) {
|
|
281
432
|
const fieldMeta = meta.fields[fieldName];
|
|
282
|
-
if (
|
|
433
|
+
if (fieldMeta === undefined) {
|
|
434
|
+
throw new Error(`Unsupported ZORA theme recipe value: ${recipeName}.${fieldName}.`);
|
|
435
|
+
}
|
|
436
|
+
if (!isRecipeValueSupported(value, fieldMeta)) {
|
|
283
437
|
throw new Error(`Unsupported ZORA theme recipe value: ${recipeName}.${fieldName}.`);
|
|
284
438
|
}
|
|
285
439
|
}
|
|
@@ -288,14 +442,14 @@ function assertSupportedThemeRecipes(recipes, recipeMeta) {
|
|
|
288
442
|
}
|
|
289
443
|
|
|
290
444
|
/*** Validate one recipe value using its owner-defined boolean, choice, or token field metadata. */
|
|
291
|
-
function isRecipeValueSupported(value, fieldMeta) {
|
|
445
|
+
function isRecipeValueSupported(value: unknown, fieldMeta: RecipeFieldMetadata): boolean {
|
|
292
446
|
if (fieldMeta.type === 'boolean') return typeof value === 'boolean';
|
|
293
447
|
if (typeof value !== 'string') return false;
|
|
294
|
-
return fieldMeta.type !== 'choice' || fieldMeta.options
|
|
448
|
+
return fieldMeta.type !== 'choice' || fieldMeta.options?.includes(value) === true;
|
|
295
449
|
}
|
|
296
450
|
|
|
297
451
|
/*** Find a manifest node recursively by stable node ID. */
|
|
298
|
-
function findNode(node, nodeId) {
|
|
452
|
+
function findNode(node: ManifestNode, nodeId: string): ManifestNode | null {
|
|
299
453
|
if (node.id === nodeId) return node;
|
|
300
454
|
for (const child of Array.isArray(node.children) ? node.children : []) {
|
|
301
455
|
const found = findNode(child, nodeId);
|
|
@@ -305,8 +459,11 @@ function findNode(node, nodeId) {
|
|
|
305
459
|
}
|
|
306
460
|
|
|
307
461
|
/*** Resolve, version-check, and import one public owner module from the target repository. */
|
|
308
|
-
async function loadOwnerModule(
|
|
309
|
-
|
|
462
|
+
async function loadOwnerModule(
|
|
463
|
+
targetDirectory: string,
|
|
464
|
+
requirement: OwnerRequirement,
|
|
465
|
+
): Promise<LoadedOwnerModule> {
|
|
466
|
+
let packageManifestPath: string;
|
|
310
467
|
try {
|
|
311
468
|
packageManifestPath = await findInstalledPackageManifest(
|
|
312
469
|
targetDirectory,
|
|
@@ -320,8 +477,9 @@ async function loadOwnerModule(targetDirectory, requirement) {
|
|
|
320
477
|
);
|
|
321
478
|
}
|
|
322
479
|
|
|
323
|
-
const packageManifest = JSON.parse(await readFile(packageManifestPath, 'utf8'));
|
|
324
|
-
|
|
480
|
+
const packageManifest: unknown = JSON.parse(await readFile(packageManifestPath, 'utf8'));
|
|
481
|
+
assertRecord(packageManifest, `${requirement.packageName} package manifest`);
|
|
482
|
+
const { version } = packageManifest;
|
|
325
483
|
if (typeof version !== 'string' || compareVersions(version, requirement.minimumVersion) < 0) {
|
|
326
484
|
throw ownerError(
|
|
327
485
|
requirement,
|
|
@@ -334,7 +492,8 @@ async function loadOwnerModule(targetDirectory, requirement) {
|
|
|
334
492
|
requirement.specifier,
|
|
335
493
|
requirement,
|
|
336
494
|
);
|
|
337
|
-
const ownerModule = await import(pathToFileURL(modulePath).href);
|
|
495
|
+
const ownerModule: unknown = await import(pathToFileURL(modulePath).href);
|
|
496
|
+
assertRecord(ownerModule, `${requirement.packageName} public module`);
|
|
338
497
|
const missingExports = requirement.exports.filter((name) => !(name in ownerModule));
|
|
339
498
|
if (missingExports.length > 0) {
|
|
340
499
|
throw ownerError(requirement, `is missing public exports: ${missingExports.join(', ')}`);
|
|
@@ -343,11 +502,15 @@ async function loadOwnerModule(targetDirectory, requirement) {
|
|
|
343
502
|
}
|
|
344
503
|
|
|
345
504
|
/*** Find the nearest installed package manifest without falling back to the skill's own tree. */
|
|
346
|
-
async function findInstalledPackageManifest(
|
|
505
|
+
async function findInstalledPackageManifest(
|
|
506
|
+
targetDirectory: string,
|
|
507
|
+
packageName: string,
|
|
508
|
+
): Promise<string> {
|
|
347
509
|
const resolvedTarget = resolve(targetDirectory);
|
|
348
510
|
const selfManifestPath = join(resolvedTarget, 'package.json');
|
|
349
511
|
try {
|
|
350
|
-
const selfManifest = JSON.parse(await readFile(selfManifestPath, 'utf8'));
|
|
512
|
+
const selfManifest: unknown = JSON.parse(await readFile(selfManifestPath, 'utf8'));
|
|
513
|
+
assertRecord(selfManifest, 'Target package manifest');
|
|
351
514
|
if (selfManifest.name === packageName) return selfManifestPath;
|
|
352
515
|
} catch (error) {
|
|
353
516
|
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
|
|
@@ -355,7 +518,7 @@ async function findInstalledPackageManifest(targetDirectory, packageName) {
|
|
|
355
518
|
|
|
356
519
|
const filesystemRoot = parse(resolvedTarget).root;
|
|
357
520
|
let directory = resolvedTarget;
|
|
358
|
-
|
|
521
|
+
for (;;) {
|
|
359
522
|
const candidate = join(directory, 'node_modules', ...packageName.split('/'), 'package.json');
|
|
360
523
|
try {
|
|
361
524
|
await readFile(candidate, 'utf8');
|
|
@@ -370,7 +533,12 @@ async function findInstalledPackageManifest(targetDirectory, packageName) {
|
|
|
370
533
|
}
|
|
371
534
|
|
|
372
535
|
/*** Resolve one import-condition public export from an installed package manifest. */
|
|
373
|
-
function resolvePublicExportPath(
|
|
536
|
+
function resolvePublicExportPath(
|
|
537
|
+
packageManifest: Record<string, unknown>,
|
|
538
|
+
packageManifestPath: string,
|
|
539
|
+
specifier: string,
|
|
540
|
+
requirement: OwnerRequirement,
|
|
541
|
+
): string {
|
|
374
542
|
const exportKey =
|
|
375
543
|
specifier === requirement.packageName
|
|
376
544
|
? '.'
|
|
@@ -378,14 +546,14 @@ function resolvePublicExportPath(packageManifest, packageManifestPath, specifier
|
|
|
378
546
|
const exportsField = packageManifest.exports;
|
|
379
547
|
const rawExport = isRecord(exportsField) ? exportsField[exportKey] : null;
|
|
380
548
|
const exportTarget = selectImportExportTarget(rawExport);
|
|
381
|
-
if (
|
|
549
|
+
if (!exportTarget?.startsWith('./')) {
|
|
382
550
|
throw ownerError(requirement, `does not expose the import target for ${specifier}`);
|
|
383
551
|
}
|
|
384
552
|
return resolve(dirname(packageManifestPath), exportTarget);
|
|
385
553
|
}
|
|
386
554
|
|
|
387
555
|
/*** Select the canonical ESM import target without resolving a CommonJS compatibility condition. */
|
|
388
|
-
function selectImportExportTarget(rawExport) {
|
|
556
|
+
function selectImportExportTarget(rawExport: unknown): string | null {
|
|
389
557
|
if (typeof rawExport === 'string') return rawExport;
|
|
390
558
|
if (!isRecord(rawExport)) return null;
|
|
391
559
|
for (const condition of ['import', 'default', 'bun', 'browser', 'react-native']) {
|
|
@@ -395,7 +563,7 @@ function selectImportExportTarget(rawExport) {
|
|
|
395
563
|
}
|
|
396
564
|
|
|
397
565
|
/*** Create an actionable released-owner diagnostic without offering a compatibility fallback. */
|
|
398
|
-
function ownerError(requirement, detail, cause) {
|
|
566
|
+
function ownerError(requirement: OwnerRequirement, detail: string, cause?: unknown): Error {
|
|
399
567
|
return new Error(
|
|
400
568
|
`zora-designer requires ${requirement.packageName} >=${requirement.minimumVersion}; ${detail}. ` +
|
|
401
569
|
`Update the target dependency through its normal Renovate/release workflow and rerun inspection.`,
|
|
@@ -404,7 +572,7 @@ function ownerError(requirement, detail, cause) {
|
|
|
404
572
|
}
|
|
405
573
|
|
|
406
574
|
/*** Compare stable semantic versions needed by the released public API gates. */
|
|
407
|
-
function compareVersions(left, right) {
|
|
575
|
+
function compareVersions(left: string, right: string): number {
|
|
408
576
|
const leftParts = parseVersion(left);
|
|
409
577
|
const rightParts = parseVersion(right);
|
|
410
578
|
for (let index = 0; index < 3; index += 1) {
|
|
@@ -415,26 +583,165 @@ function compareVersions(left, right) {
|
|
|
415
583
|
}
|
|
416
584
|
|
|
417
585
|
/*** Parse the numeric major, minor, and patch tuple from a semantic version. */
|
|
418
|
-
function parseVersion(version) {
|
|
586
|
+
function parseVersion(version: string): [number, number, number] {
|
|
419
587
|
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/u.exec(version);
|
|
420
588
|
if (!match) return [-1, -1, -1];
|
|
421
|
-
return match
|
|
589
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/*** Read and validate the screen tree boundary supplied by portable JSON input. */
|
|
593
|
+
function readManifestScreens(value: unknown): Record<string, ManifestScreen | undefined> {
|
|
594
|
+
assertRecord(value, 'screens');
|
|
595
|
+
const screens: Record<string, ManifestScreen> = {};
|
|
596
|
+
for (const [screenId, screen] of Object.entries(value)) {
|
|
597
|
+
assertRecord(screen, `screen "${screenId}"`);
|
|
598
|
+
assertManifestNode(screen.root, `screen "${screenId}" root`);
|
|
599
|
+
screens[screenId] = { ...screen, root: screen.root };
|
|
600
|
+
}
|
|
601
|
+
return screens;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/*** Validate one manifest node recursively before region insertion. */
|
|
605
|
+
function assertManifestNode(value: unknown, label: string): asserts value is ManifestNode {
|
|
606
|
+
assertRecord(value, label);
|
|
607
|
+
assertNonEmptyString(value.id, `${label}.id`);
|
|
608
|
+
assertNonEmptyString(value.type, `${label}.type`);
|
|
609
|
+
if (value.props !== undefined) assertRecord(value.props, `${label}.props`);
|
|
610
|
+
if (value.children !== undefined) {
|
|
611
|
+
if (!Array.isArray(value.children)) throw new Error(`${label}.children must be an array.`);
|
|
612
|
+
value.children.forEach((child, index) =>
|
|
613
|
+
assertManifestNode(child, `${label}.children[${index}]`),
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/*** Finish narrowing a region after its required string fields have been checked. */
|
|
619
|
+
function assertRegion(value: Record<string, unknown>): asserts value is Region {
|
|
620
|
+
if (value.props !== undefined) {
|
|
621
|
+
assertRecord(value.props, `props for region "${String(value.id)}"`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/*** Narrow the released Color Theory capability surface used by this script. */
|
|
626
|
+
function assertColorTheoryApi(value: Record<string, unknown>): asserts value is ColorTheoryApi {
|
|
627
|
+
assertStringArray(value.COLOR_HARMONIES, 'COLOR_HARMONIES');
|
|
628
|
+
assertRecordArray(value.COLOR_HARMONY_CATALOG, 'COLOR_HARMONY_CATALOG');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/*** Narrow the released Contracts capability surface used by this script. */
|
|
632
|
+
function assertContractsApi(value: Record<string, unknown>): asserts value is ContractsApi {
|
|
633
|
+
assertStringArray(value.APP_CATEGORIES, 'APP_CATEGORIES');
|
|
634
|
+
assertStringArray(value.NAVIGATOR_TYPES, 'NAVIGATOR_TYPES');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/*** Narrow the released Templates capability surface used by this script. */
|
|
638
|
+
function assertTemplatesApi(value: Record<string, unknown>): asserts value is TemplatesApi {
|
|
639
|
+
assertRecord(value.CATEGORY_PRESETS, 'CATEGORY_PRESETS');
|
|
640
|
+
assertRecordArray(value.TONE_PAIR_CATALOG, 'TONE_PAIR_CATALOG');
|
|
641
|
+
for (const exportName of [
|
|
642
|
+
'assertTemplateManifestReady',
|
|
643
|
+
'compileCategoryDesign',
|
|
644
|
+
'composeCategoryAppManifest',
|
|
645
|
+
'resolveCategoryDesignPreset',
|
|
646
|
+
'resolveTonePair',
|
|
647
|
+
'validateTemplateManifest',
|
|
648
|
+
]) {
|
|
649
|
+
if (typeof value[exportName] !== 'function') {
|
|
650
|
+
throw new Error(`${exportName} must be a function.`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/*** Narrow the released ZORA theme compiler capability. */
|
|
656
|
+
function assertZoraThemeApi(value: Record<string, unknown>): asserts value is ZoraThemeApi {
|
|
657
|
+
if (typeof value.compileZoraTheme !== 'function') {
|
|
658
|
+
throw new Error('compileZoraTheme must be a function.');
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/*** Narrow released ZORA metadata into only the fields the orchestration needs. */
|
|
663
|
+
function assertZoraMetadataApi(value: Record<string, unknown>): asserts value is ZoraMetadataApi {
|
|
664
|
+
assertRecord(value.ZORA_COMPONENT_META, 'ZORA_COMPONENT_META');
|
|
665
|
+
for (const [name, metadata] of Object.entries(value.ZORA_COMPONENT_META)) {
|
|
666
|
+
assertComponentMetadata(metadata, name);
|
|
667
|
+
}
|
|
668
|
+
assertRecord(value.ZORA_THEME_RECIPE_META, 'ZORA_THEME_RECIPE_META');
|
|
669
|
+
for (const [name, metadata] of Object.entries(value.ZORA_THEME_RECIPE_META)) {
|
|
670
|
+
assertRecipeMetadata(metadata, name);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/*** Validate one component metadata entry obtained from the released owner. */
|
|
675
|
+
function assertComponentMetadata(value: unknown, name: string): asserts value is ComponentMetadata {
|
|
676
|
+
assertRecord(value, `ZORA component metadata ${name}`);
|
|
677
|
+
assertNonEmptyString(value.name, `ZORA component metadata ${name}.name`);
|
|
678
|
+
if (typeof value.directManifestNode !== 'boolean') {
|
|
679
|
+
throw new Error(`ZORA component metadata ${name}.directManifestNode must be a boolean.`);
|
|
680
|
+
}
|
|
681
|
+
assertStringArray(value.allowedChildren, `ZORA component metadata ${name}.allowedChildren`);
|
|
682
|
+
assertRecord(value.props, `ZORA component metadata ${name}.props`);
|
|
683
|
+
if (value.events !== undefined) {
|
|
684
|
+
assertRecord(value.events, `ZORA component metadata ${name}.events`);
|
|
685
|
+
for (const [eventName, event] of Object.entries(value.events)) {
|
|
686
|
+
assertEventMetadata(event, `${name}.${eventName}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (value.manifestPolicy !== undefined) {
|
|
690
|
+
assertRecord(value.manifestPolicy, `ZORA component metadata ${name}.manifestPolicy`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/*** Validate one event metadata entry used by interactive discovery. */
|
|
695
|
+
function assertEventMetadata(value: unknown, name: string): asserts value is EventMetadata {
|
|
696
|
+
assertRecord(value, `ZORA event metadata ${name}`);
|
|
697
|
+
assertNonEmptyString(value.eventType, `ZORA event metadata ${name}.eventType`);
|
|
698
|
+
assertNonEmptyString(value.label, `ZORA event metadata ${name}.label`);
|
|
699
|
+
assertNonEmptyString(value.description, `ZORA event metadata ${name}.description`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/*** Validate one theme recipe metadata entry and its supported fields. */
|
|
703
|
+
function assertRecipeMetadata(value: unknown, name: string): asserts value is RecipeMetadata {
|
|
704
|
+
assertRecord(value, `ZORA theme recipe metadata ${name}`);
|
|
705
|
+
assertNonEmptyString(value.kind, `ZORA theme recipe metadata ${name}.kind`);
|
|
706
|
+
assertRecord(value.fields, `ZORA theme recipe metadata ${name}.fields`);
|
|
707
|
+
for (const [fieldName, field] of Object.entries(value.fields)) {
|
|
708
|
+
assertRecord(field, `ZORA theme recipe field ${name}.${fieldName}`);
|
|
709
|
+
assertNonEmptyString(field.type, `ZORA theme recipe field ${name}.${fieldName}.type`);
|
|
710
|
+
if (field.options !== undefined) {
|
|
711
|
+
assertStringArray(field.options, `ZORA theme recipe field ${name}.${fieldName}.options`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/*** Require an array containing only non-empty strings. */
|
|
717
|
+
function assertStringArray(value: unknown, label: string): asserts value is string[] {
|
|
718
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
719
|
+
value.forEach((entry, index) => assertNonEmptyString(entry, `${label}[${index}]`));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/*** Require an array containing only records. */
|
|
723
|
+
function assertRecordArray(
|
|
724
|
+
value: unknown,
|
|
725
|
+
label: string,
|
|
726
|
+
): asserts value is Record<string, unknown>[] {
|
|
727
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
728
|
+
value.forEach((entry, index) => assertRecord(entry, `${label}[${index}]`));
|
|
422
729
|
}
|
|
423
730
|
|
|
424
731
|
/*** Require an object-shaped input value. */
|
|
425
|
-
function assertRecord(value, label) {
|
|
732
|
+
function assertRecord(value: unknown, label: string): asserts value is Record<string, unknown> {
|
|
426
733
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
427
734
|
throw new Error(`${label} must be an object.`);
|
|
428
735
|
}
|
|
429
736
|
}
|
|
430
737
|
|
|
431
738
|
/*** Narrow unknown package export metadata to a record. */
|
|
432
|
-
function isRecord(value) {
|
|
739
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
433
740
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
434
741
|
}
|
|
435
742
|
|
|
436
743
|
/*** Require a non-empty string input field. */
|
|
437
|
-
function assertNonEmptyString(value, label) {
|
|
744
|
+
function assertNonEmptyString(value: unknown, label: string): asserts value is string {
|
|
438
745
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
439
746
|
throw new Error(`${label} must be a non-empty string.`);
|
|
440
747
|
}
|
|
@@ -448,11 +755,11 @@ async function main() {
|
|
|
448
755
|
return;
|
|
449
756
|
}
|
|
450
757
|
if (command === 'compose' && inputPath) {
|
|
451
|
-
const input = JSON.parse(await readFile(inputPath, 'utf8'));
|
|
758
|
+
const input: unknown = JSON.parse(await readFile(inputPath, 'utf8'));
|
|
452
759
|
console.log(JSON.stringify(await composeDesign(input), null, 2));
|
|
453
760
|
return;
|
|
454
761
|
}
|
|
455
|
-
throw new Error('Usage: owner-api.
|
|
762
|
+
throw new Error('Usage: owner-api.ts inspect | owner-api.ts compose <input.json>');
|
|
456
763
|
}
|
|
457
764
|
|
|
458
765
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|