@ankhorage/devtools 1.10.13 → 1.10.15
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} +14 -5
- package/dist/tools/skills/assets/zora-designer/scripts/{owner-api.mjs → owner-api.ts} +356 -54
- package/dist/tools/skills/assets/zora-designer/scripts/{scaffold-template.mjs → scaffold-template.ts} +27 -21
- package/dist/tools/workflows/files/release.yml +35 -0
- package/dist/tools/workflows/files/renovate.yml +2 -2
- 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,38 +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
|
|
|
74
195
|
/*** Load only Contracts for tooling that runs before another owner package has been built. */
|
|
75
|
-
export async function loadContractsApi(targetDirectory = process.cwd()) {
|
|
76
|
-
|
|
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;
|
|
77
200
|
}
|
|
78
201
|
|
|
79
202
|
/*** Return installed catalogs and metadata names without copying owner definitions. */
|
|
80
203
|
export async function inspectOwnerApis(targetDirectory = process.cwd()) {
|
|
81
204
|
const owners = await loadOwnerApis(targetDirectory);
|
|
82
|
-
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
|
+
);
|
|
83
208
|
return {
|
|
84
209
|
versions: owners.versions,
|
|
85
210
|
appCategories: owners.contracts.APP_CATEGORIES,
|
|
@@ -109,20 +234,22 @@ export async function inspectOwnerApis(targetDirectory = process.cwd()) {
|
|
|
109
234
|
}
|
|
110
235
|
|
|
111
236
|
/*** Compose one design without turning missing runtime/UI capabilities into a design blocker. */
|
|
112
|
-
export async function composeDesign(input, targetDirectory = process.cwd()) {
|
|
237
|
+
export async function composeDesign(input: unknown, targetDirectory = process.cwd()) {
|
|
113
238
|
const owners = await loadOwnerApis(targetDirectory);
|
|
114
239
|
assertRecord(input, 'Design input');
|
|
115
240
|
assertNonEmptyString(input.category, 'category');
|
|
116
241
|
assertRecord(input.navigator, 'navigator');
|
|
117
|
-
|
|
118
|
-
|
|
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);
|
|
119
246
|
|
|
120
247
|
const regionResult = resolveRegionNodes(
|
|
121
|
-
|
|
248
|
+
screens,
|
|
122
249
|
Array.isArray(input.regions) ? input.regions : [],
|
|
123
250
|
owners.zoraMetadata.ZORA_COMPONENT_META,
|
|
124
251
|
);
|
|
125
|
-
const design = owners.templates.compileCategoryDesign(input.category,
|
|
252
|
+
const design = owners.templates.compileCategoryDesign(input.category, theme);
|
|
126
253
|
const { computedTheme, ...resolvedDesign } = design;
|
|
127
254
|
const requestedAuthoringState = input.authoringState === 'release' ? 'release' : 'draft';
|
|
128
255
|
const composition = owners.templates.composeCategoryAppManifest({
|
|
@@ -161,7 +288,11 @@ export async function composeDesign(input, targetDirectory = process.cwd()) {
|
|
|
161
288
|
}
|
|
162
289
|
|
|
163
290
|
/*** Resolve explicit region decisions using exact metadata or a visible non-blocking Box placeholder. */
|
|
164
|
-
export function resolveRegionNodes(
|
|
291
|
+
export function resolveRegionNodes(
|
|
292
|
+
screens: Record<string, ManifestScreen | undefined>,
|
|
293
|
+
regions: readonly unknown[],
|
|
294
|
+
componentMeta: Record<string, ComponentMetadata | undefined>,
|
|
295
|
+
) {
|
|
165
296
|
const resolvedScreens = structuredClone(screens);
|
|
166
297
|
const diagnostics = [];
|
|
167
298
|
const gaps = [];
|
|
@@ -171,6 +302,7 @@ export function resolveRegionNodes(screens, regions, componentMeta) {
|
|
|
171
302
|
assertNonEmptyString(region.id, 'region.id');
|
|
172
303
|
assertNonEmptyString(region.screenId, 'region.screenId');
|
|
173
304
|
assertNonEmptyString(region.requestedCapability, 'region.requestedCapability');
|
|
305
|
+
assertRegion(region);
|
|
174
306
|
const screen = resolvedScreens[region.screenId];
|
|
175
307
|
if (!screen) {
|
|
176
308
|
throw new Error(`Region "${region.id}" targets unknown screen "${region.screenId}".`);
|
|
@@ -187,7 +319,10 @@ export function resolveRegionNodes(screens, regions, componentMeta) {
|
|
|
187
319
|
return { screens: resolvedScreens, diagnostics, gaps };
|
|
188
320
|
}
|
|
189
321
|
|
|
190
|
-
function resolveRegionNode(
|
|
322
|
+
function resolveRegionNode(
|
|
323
|
+
region: Region,
|
|
324
|
+
componentMeta: Record<string, ComponentMetadata | undefined>,
|
|
325
|
+
) {
|
|
191
326
|
const component = typeof region.component === 'string' ? region.component : null;
|
|
192
327
|
const meta = component === null ? null : componentMeta[component];
|
|
193
328
|
if (meta && meta.directManifestNode && meta.manifestPolicy?.kind !== 'unresolved-element') {
|
|
@@ -223,7 +358,6 @@ function resolveRegionNode(region, componentMeta) {
|
|
|
223
358
|
return {
|
|
224
359
|
node: { id: region.id, type: placeholderMeta.name, props: {} },
|
|
225
360
|
diagnostic: {
|
|
226
|
-
regionId: region.id,
|
|
227
361
|
status: 'placeholder',
|
|
228
362
|
component: placeholderMeta.name,
|
|
229
363
|
...gap,
|
|
@@ -233,7 +367,11 @@ function resolveRegionNode(region, componentMeta) {
|
|
|
233
367
|
}
|
|
234
368
|
|
|
235
369
|
/*** Validate that manifest props are declared by the selected component metadata. */
|
|
236
|
-
function assertSupportedProps(
|
|
370
|
+
function assertSupportedProps(
|
|
371
|
+
props: unknown,
|
|
372
|
+
meta: ComponentMetadata,
|
|
373
|
+
regionId: string,
|
|
374
|
+
): Record<string, unknown> {
|
|
237
375
|
assertRecord(props, `props for region "${regionId}"`);
|
|
238
376
|
const unsupported = Object.keys(props).filter((name) => !(name in meta.props));
|
|
239
377
|
if (unsupported.length > 0) {
|
|
@@ -245,7 +383,12 @@ function assertSupportedProps(props, meta, regionId) {
|
|
|
245
383
|
}
|
|
246
384
|
|
|
247
385
|
/*** Insert a region node under the declared parent or the target screen root. */
|
|
248
|
-
function insertRegionNode(
|
|
386
|
+
function insertRegionNode(
|
|
387
|
+
screen: ManifestScreen,
|
|
388
|
+
parentNodeId: unknown,
|
|
389
|
+
node: ManifestNode,
|
|
390
|
+
componentMeta: Record<string, ComponentMetadata | undefined>,
|
|
391
|
+
): void {
|
|
249
392
|
const parent =
|
|
250
393
|
typeof parentNodeId === 'string' && parentNodeId !== ''
|
|
251
394
|
? findNode(screen.root, parentNodeId)
|
|
@@ -254,7 +397,7 @@ function insertRegionNode(screen, parentNodeId, node, componentMeta) {
|
|
|
254
397
|
throw new Error(`Region parent node not found: ${String(parentNodeId)}`);
|
|
255
398
|
}
|
|
256
399
|
const parentMeta = componentMeta[parent.type];
|
|
257
|
-
if (!parentMeta
|
|
400
|
+
if (!parentMeta?.directManifestNode) {
|
|
258
401
|
throw new Error(`Region parent "${parent.id}" is absent from direct ZORA manifest metadata.`);
|
|
259
402
|
}
|
|
260
403
|
if (!parentMeta.allowedChildren.includes(node.type)) {
|
|
@@ -266,7 +409,10 @@ function insertRegionNode(screen, parentNodeId, node, componentMeta) {
|
|
|
266
409
|
}
|
|
267
410
|
|
|
268
411
|
/*** Validate persisted component and pattern recipe overrides against exact ZORA metadata fields. */
|
|
269
|
-
function assertSupportedThemeRecipes(
|
|
412
|
+
function assertSupportedThemeRecipes(
|
|
413
|
+
recipes: unknown,
|
|
414
|
+
recipeMeta: Record<string, RecipeMetadata | undefined>,
|
|
415
|
+
): void {
|
|
270
416
|
if (recipes === undefined) return;
|
|
271
417
|
assertRecord(recipes, 'theme.recipes');
|
|
272
418
|
for (const [group, kind] of [
|
|
@@ -278,13 +424,16 @@ function assertSupportedThemeRecipes(recipes, recipeMeta) {
|
|
|
278
424
|
assertRecord(overrides, `theme.recipes.${group}`);
|
|
279
425
|
for (const [recipeName, fields] of Object.entries(overrides)) {
|
|
280
426
|
const meta = recipeMeta[recipeName];
|
|
281
|
-
if (
|
|
427
|
+
if (meta?.kind !== kind) {
|
|
282
428
|
throw new Error(`Unknown ZORA ${kind} theme recipe: ${recipeName}.`);
|
|
283
429
|
}
|
|
284
430
|
assertRecord(fields, `theme recipe ${recipeName}`);
|
|
285
431
|
for (const [fieldName, value] of Object.entries(fields)) {
|
|
286
432
|
const fieldMeta = meta.fields[fieldName];
|
|
287
|
-
if (
|
|
433
|
+
if (fieldMeta === undefined) {
|
|
434
|
+
throw new Error(`Unsupported ZORA theme recipe value: ${recipeName}.${fieldName}.`);
|
|
435
|
+
}
|
|
436
|
+
if (!isRecipeValueSupported(value, fieldMeta)) {
|
|
288
437
|
throw new Error(`Unsupported ZORA theme recipe value: ${recipeName}.${fieldName}.`);
|
|
289
438
|
}
|
|
290
439
|
}
|
|
@@ -293,14 +442,14 @@ function assertSupportedThemeRecipes(recipes, recipeMeta) {
|
|
|
293
442
|
}
|
|
294
443
|
|
|
295
444
|
/*** Validate one recipe value using its owner-defined boolean, choice, or token field metadata. */
|
|
296
|
-
function isRecipeValueSupported(value, fieldMeta) {
|
|
445
|
+
function isRecipeValueSupported(value: unknown, fieldMeta: RecipeFieldMetadata): boolean {
|
|
297
446
|
if (fieldMeta.type === 'boolean') return typeof value === 'boolean';
|
|
298
447
|
if (typeof value !== 'string') return false;
|
|
299
|
-
return fieldMeta.type !== 'choice' || fieldMeta.options
|
|
448
|
+
return fieldMeta.type !== 'choice' || fieldMeta.options?.includes(value) === true;
|
|
300
449
|
}
|
|
301
450
|
|
|
302
451
|
/*** Find a manifest node recursively by stable node ID. */
|
|
303
|
-
function findNode(node, nodeId) {
|
|
452
|
+
function findNode(node: ManifestNode, nodeId: string): ManifestNode | null {
|
|
304
453
|
if (node.id === nodeId) return node;
|
|
305
454
|
for (const child of Array.isArray(node.children) ? node.children : []) {
|
|
306
455
|
const found = findNode(child, nodeId);
|
|
@@ -310,8 +459,11 @@ function findNode(node, nodeId) {
|
|
|
310
459
|
}
|
|
311
460
|
|
|
312
461
|
/*** Resolve, version-check, and import one public owner module from the target repository. */
|
|
313
|
-
async function loadOwnerModule(
|
|
314
|
-
|
|
462
|
+
async function loadOwnerModule(
|
|
463
|
+
targetDirectory: string,
|
|
464
|
+
requirement: OwnerRequirement,
|
|
465
|
+
): Promise<LoadedOwnerModule> {
|
|
466
|
+
let packageManifestPath: string;
|
|
315
467
|
try {
|
|
316
468
|
packageManifestPath = await findInstalledPackageManifest(
|
|
317
469
|
targetDirectory,
|
|
@@ -325,8 +477,9 @@ async function loadOwnerModule(targetDirectory, requirement) {
|
|
|
325
477
|
);
|
|
326
478
|
}
|
|
327
479
|
|
|
328
|
-
const packageManifest = JSON.parse(await readFile(packageManifestPath, 'utf8'));
|
|
329
|
-
|
|
480
|
+
const packageManifest: unknown = JSON.parse(await readFile(packageManifestPath, 'utf8'));
|
|
481
|
+
assertRecord(packageManifest, `${requirement.packageName} package manifest`);
|
|
482
|
+
const { version } = packageManifest;
|
|
330
483
|
if (typeof version !== 'string' || compareVersions(version, requirement.minimumVersion) < 0) {
|
|
331
484
|
throw ownerError(
|
|
332
485
|
requirement,
|
|
@@ -339,7 +492,8 @@ async function loadOwnerModule(targetDirectory, requirement) {
|
|
|
339
492
|
requirement.specifier,
|
|
340
493
|
requirement,
|
|
341
494
|
);
|
|
342
|
-
const ownerModule = await import(pathToFileURL(modulePath).href);
|
|
495
|
+
const ownerModule: unknown = await import(pathToFileURL(modulePath).href);
|
|
496
|
+
assertRecord(ownerModule, `${requirement.packageName} public module`);
|
|
343
497
|
const missingExports = requirement.exports.filter((name) => !(name in ownerModule));
|
|
344
498
|
if (missingExports.length > 0) {
|
|
345
499
|
throw ownerError(requirement, `is missing public exports: ${missingExports.join(', ')}`);
|
|
@@ -348,11 +502,15 @@ async function loadOwnerModule(targetDirectory, requirement) {
|
|
|
348
502
|
}
|
|
349
503
|
|
|
350
504
|
/*** Find the nearest installed package manifest without falling back to the skill's own tree. */
|
|
351
|
-
async function findInstalledPackageManifest(
|
|
505
|
+
async function findInstalledPackageManifest(
|
|
506
|
+
targetDirectory: string,
|
|
507
|
+
packageName: string,
|
|
508
|
+
): Promise<string> {
|
|
352
509
|
const resolvedTarget = resolve(targetDirectory);
|
|
353
510
|
const selfManifestPath = join(resolvedTarget, 'package.json');
|
|
354
511
|
try {
|
|
355
|
-
const selfManifest = JSON.parse(await readFile(selfManifestPath, 'utf8'));
|
|
512
|
+
const selfManifest: unknown = JSON.parse(await readFile(selfManifestPath, 'utf8'));
|
|
513
|
+
assertRecord(selfManifest, 'Target package manifest');
|
|
356
514
|
if (selfManifest.name === packageName) return selfManifestPath;
|
|
357
515
|
} catch (error) {
|
|
358
516
|
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
|
|
@@ -360,7 +518,7 @@ async function findInstalledPackageManifest(targetDirectory, packageName) {
|
|
|
360
518
|
|
|
361
519
|
const filesystemRoot = parse(resolvedTarget).root;
|
|
362
520
|
let directory = resolvedTarget;
|
|
363
|
-
|
|
521
|
+
for (;;) {
|
|
364
522
|
const candidate = join(directory, 'node_modules', ...packageName.split('/'), 'package.json');
|
|
365
523
|
try {
|
|
366
524
|
await readFile(candidate, 'utf8');
|
|
@@ -375,7 +533,12 @@ async function findInstalledPackageManifest(targetDirectory, packageName) {
|
|
|
375
533
|
}
|
|
376
534
|
|
|
377
535
|
/*** Resolve one import-condition public export from an installed package manifest. */
|
|
378
|
-
function resolvePublicExportPath(
|
|
536
|
+
function resolvePublicExportPath(
|
|
537
|
+
packageManifest: Record<string, unknown>,
|
|
538
|
+
packageManifestPath: string,
|
|
539
|
+
specifier: string,
|
|
540
|
+
requirement: OwnerRequirement,
|
|
541
|
+
): string {
|
|
379
542
|
const exportKey =
|
|
380
543
|
specifier === requirement.packageName
|
|
381
544
|
? '.'
|
|
@@ -383,14 +546,14 @@ function resolvePublicExportPath(packageManifest, packageManifestPath, specifier
|
|
|
383
546
|
const exportsField = packageManifest.exports;
|
|
384
547
|
const rawExport = isRecord(exportsField) ? exportsField[exportKey] : null;
|
|
385
548
|
const exportTarget = selectImportExportTarget(rawExport);
|
|
386
|
-
if (
|
|
549
|
+
if (!exportTarget?.startsWith('./')) {
|
|
387
550
|
throw ownerError(requirement, `does not expose the import target for ${specifier}`);
|
|
388
551
|
}
|
|
389
552
|
return resolve(dirname(packageManifestPath), exportTarget);
|
|
390
553
|
}
|
|
391
554
|
|
|
392
555
|
/*** Select the canonical ESM import target without resolving a CommonJS compatibility condition. */
|
|
393
|
-
function selectImportExportTarget(rawExport) {
|
|
556
|
+
function selectImportExportTarget(rawExport: unknown): string | null {
|
|
394
557
|
if (typeof rawExport === 'string') return rawExport;
|
|
395
558
|
if (!isRecord(rawExport)) return null;
|
|
396
559
|
for (const condition of ['import', 'default', 'bun', 'browser', 'react-native']) {
|
|
@@ -400,7 +563,7 @@ function selectImportExportTarget(rawExport) {
|
|
|
400
563
|
}
|
|
401
564
|
|
|
402
565
|
/*** Create an actionable released-owner diagnostic without offering a compatibility fallback. */
|
|
403
|
-
function ownerError(requirement, detail, cause) {
|
|
566
|
+
function ownerError(requirement: OwnerRequirement, detail: string, cause?: unknown): Error {
|
|
404
567
|
return new Error(
|
|
405
568
|
`zora-designer requires ${requirement.packageName} >=${requirement.minimumVersion}; ${detail}. ` +
|
|
406
569
|
`Update the target dependency through its normal Renovate/release workflow and rerun inspection.`,
|
|
@@ -409,7 +572,7 @@ function ownerError(requirement, detail, cause) {
|
|
|
409
572
|
}
|
|
410
573
|
|
|
411
574
|
/*** Compare stable semantic versions needed by the released public API gates. */
|
|
412
|
-
function compareVersions(left, right) {
|
|
575
|
+
function compareVersions(left: string, right: string): number {
|
|
413
576
|
const leftParts = parseVersion(left);
|
|
414
577
|
const rightParts = parseVersion(right);
|
|
415
578
|
for (let index = 0; index < 3; index += 1) {
|
|
@@ -420,26 +583,165 @@ function compareVersions(left, right) {
|
|
|
420
583
|
}
|
|
421
584
|
|
|
422
585
|
/*** Parse the numeric major, minor, and patch tuple from a semantic version. */
|
|
423
|
-
function parseVersion(version) {
|
|
586
|
+
function parseVersion(version: string): [number, number, number] {
|
|
424
587
|
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/u.exec(version);
|
|
425
588
|
if (!match) return [-1, -1, -1];
|
|
426
|
-
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}]`));
|
|
427
729
|
}
|
|
428
730
|
|
|
429
731
|
/*** Require an object-shaped input value. */
|
|
430
|
-
function assertRecord(value, label) {
|
|
732
|
+
function assertRecord(value: unknown, label: string): asserts value is Record<string, unknown> {
|
|
431
733
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
432
734
|
throw new Error(`${label} must be an object.`);
|
|
433
735
|
}
|
|
434
736
|
}
|
|
435
737
|
|
|
436
738
|
/*** Narrow unknown package export metadata to a record. */
|
|
437
|
-
function isRecord(value) {
|
|
739
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
438
740
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
439
741
|
}
|
|
440
742
|
|
|
441
743
|
/*** Require a non-empty string input field. */
|
|
442
|
-
function assertNonEmptyString(value, label) {
|
|
744
|
+
function assertNonEmptyString(value: unknown, label: string): asserts value is string {
|
|
443
745
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
444
746
|
throw new Error(`${label} must be a non-empty string.`);
|
|
445
747
|
}
|
|
@@ -453,11 +755,11 @@ async function main() {
|
|
|
453
755
|
return;
|
|
454
756
|
}
|
|
455
757
|
if (command === 'compose' && inputPath) {
|
|
456
|
-
const input = JSON.parse(await readFile(inputPath, 'utf8'));
|
|
758
|
+
const input: unknown = JSON.parse(await readFile(inputPath, 'utf8'));
|
|
457
759
|
console.log(JSON.stringify(await composeDesign(input), null, 2));
|
|
458
760
|
return;
|
|
459
761
|
}
|
|
460
|
-
throw new Error('Usage: owner-api.
|
|
762
|
+
throw new Error('Usage: owner-api.ts inspect | owner-api.ts compose <input.json>');
|
|
461
763
|
}
|
|
462
764
|
|
|
463
765
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|