@ankhorage/zora 4.2.2 → 4.4.0
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/CHANGELOG.md +24 -0
- package/README.md +2 -1
- package/dist/components/button/resolveButtonThemeRecipe.d.ts +1 -1
- package/dist/components/radio/RadioGroup.d.ts.map +1 -1
- package/dist/components/radio/RadioGroup.js +27 -7
- package/dist/components/radio/RadioGroup.js.map +1 -1
- package/dist/components/radio/meta.d.ts +203 -4
- package/dist/components/radio/meta.d.ts.map +1 -1
- package/dist/components/radio/meta.js +168 -4
- package/dist/components/radio/meta.js.map +1 -1
- package/dist/components/radio/resolveRadioGroupThemeRecipe.d.ts +20 -0
- package/dist/components/radio/resolveRadioGroupThemeRecipe.d.ts.map +1 -0
- package/dist/components/radio/resolveRadioGroupThemeRecipe.js +34 -0
- package/dist/components/radio/resolveRadioGroupThemeRecipe.js.map +1 -0
- package/dist/components/radio/themeRecipeMeta.d.ts +26 -0
- package/dist/components/radio/themeRecipeMeta.d.ts.map +1 -0
- package/dist/components/radio/themeRecipeMeta.js +27 -0
- package/dist/components/radio/themeRecipeMeta.js.map +1 -0
- package/dist/components/radio/types.d.ts +1 -0
- package/dist/components/radio/types.d.ts.map +1 -1
- package/dist/components/radio/types.js.map +1 -1
- package/dist/corePlugin.d.ts +4 -0
- package/dist/corePlugin.d.ts.map +1 -0
- package/dist/corePlugin.js +8 -0
- package/dist/corePlugin.js.map +1 -0
- package/dist/corePluginMetadata.d.ts +3 -0
- package/dist/corePluginMetadata.d.ts.map +1 -0
- package/dist/corePluginMetadata.js +31 -0
- package/dist/corePluginMetadata.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/metadata/index.d.ts +3 -0
- package/dist/metadata/index.d.ts.map +1 -1
- package/dist/metadata/index.js +2 -0
- package/dist/metadata/index.js.map +1 -1
- package/dist/metadata/themeRecipeMeta.d.ts.map +1 -1
- package/dist/metadata/themeRecipeMeta.js +2 -0
- package/dist/metadata/themeRecipeMeta.js.map +1 -1
- package/dist/metadata/types.d.ts +2 -1
- package/dist/metadata/types.d.ts.map +1 -1
- package/dist/metadata/types.js.map +1 -1
- package/dist/pluginComposition.d.ts +45 -0
- package/dist/pluginComposition.d.ts.map +1 -0
- package/dist/pluginComposition.js +153 -0
- package/dist/pluginComposition.js.map +1 -0
- package/package.json +6 -6
- package/src/components/radio/RadioGroup.test.ts +31 -0
- package/src/components/radio/RadioGroup.tsx +45 -8
- package/src/components/radio/meta.ts +170 -4
- package/src/components/radio/resolveRadioGroupThemeRecipe.ts +45 -0
- package/src/components/radio/themeRecipeMeta.ts +28 -0
- package/src/components/radio/types.ts +1 -0
- package/src/corePlugin.ts +9 -0
- package/src/corePluginMetadata.ts +44 -0
- package/src/index.ts +15 -0
- package/src/metadata/componentMeta.test.ts +21 -0
- package/src/metadata/index.ts +7 -0
- package/src/metadata/themeRecipeMeta.test.ts +4 -0
- package/src/metadata/themeRecipeMeta.ts +2 -0
- package/src/metadata/types.ts +2 -0
- package/src/pluginComposition.test.ts +141 -0
- package/src/pluginComposition.ts +278 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ZORA_CORE_PLUGIN_METADATA } from './corePluginMetadata';
|
|
2
|
+
import type { ZoraPluginDescriptor } from './pluginComposition';
|
|
3
|
+
import { ZORA_COMPONENT_REGISTRY } from './registry';
|
|
4
|
+
|
|
5
|
+
/*** Describe the complete ZORA core runtime and authoring surface through the public plugin contract. */
|
|
6
|
+
export const ZORA_CORE_PLUGIN: ZoraPluginDescriptor = {
|
|
7
|
+
...ZORA_CORE_PLUGIN_METADATA,
|
|
8
|
+
componentRegistry: ZORA_COMPONENT_REGISTRY,
|
|
9
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { UiComponentMetaRegistry } from '@ankhorage/contracts';
|
|
2
|
+
|
|
3
|
+
import { ZORA_BINDABLE_COMPONENT_META } from './metadata/bindableComponentMeta';
|
|
4
|
+
import { ZORA_COMPONENT_META } from './metadata/componentMeta';
|
|
5
|
+
import type { ZoraComponentMetaRegistry } from './metadata/types';
|
|
6
|
+
import type { ZoraPluginMetadata } from './pluginComposition';
|
|
7
|
+
|
|
8
|
+
const CORE_EXTENSION_HOSTS = [
|
|
9
|
+
'Box',
|
|
10
|
+
'Card',
|
|
11
|
+
'Container',
|
|
12
|
+
'Grid',
|
|
13
|
+
'MessageBubble',
|
|
14
|
+
'Notice',
|
|
15
|
+
'Panel',
|
|
16
|
+
'PostCard',
|
|
17
|
+
'Screen',
|
|
18
|
+
'ScreenSection',
|
|
19
|
+
'Stack',
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export const ZORA_CORE_PLUGIN_METADATA: ZoraPluginMetadata = {
|
|
23
|
+
packageName: '@ankhorage/zora',
|
|
24
|
+
displayName: 'ZORA',
|
|
25
|
+
componentMeta: mergeBindableMeta(ZORA_COMPONENT_META, ZORA_BINDABLE_COMPONENT_META),
|
|
26
|
+
extensionHosts: CORE_EXTENSION_HOSTS,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function mergeBindableMeta(
|
|
30
|
+
componentMeta: ZoraComponentMetaRegistry,
|
|
31
|
+
bindableMeta: UiComponentMetaRegistry,
|
|
32
|
+
): ZoraComponentMetaRegistry {
|
|
33
|
+
const bindingsByComponent = new Map(
|
|
34
|
+
Object.entries(bindableMeta).map(([componentName, meta]) => [componentName, meta.bindings]),
|
|
35
|
+
);
|
|
36
|
+
return Object.fromEntries(
|
|
37
|
+
Object.entries(componentMeta).map(([componentName, meta]) => [
|
|
38
|
+
componentName,
|
|
39
|
+
bindingsByComponent.get(componentName)
|
|
40
|
+
? { ...meta, bindings: bindingsByComponent.get(componentName) }
|
|
41
|
+
: meta,
|
|
42
|
+
]),
|
|
43
|
+
);
|
|
44
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -156,6 +156,8 @@ export type { ToastOptions, ToastProps, ToastProviderProps, ToastStatus } from '
|
|
|
156
156
|
export { Toast, ToastProvider, useToast } from './components/toast';
|
|
157
157
|
export type { ToolbarActionProps, ToolbarPosition, ToolbarProps } from './components/toolbar';
|
|
158
158
|
export { Toolbar, ToolbarAction } from './components/toolbar';
|
|
159
|
+
export { ZORA_CORE_PLUGIN } from './corePlugin';
|
|
160
|
+
export { ZORA_CORE_PLUGIN_METADATA } from './corePluginMetadata';
|
|
159
161
|
export type {
|
|
160
162
|
BoxProps,
|
|
161
163
|
CenterProps,
|
|
@@ -398,6 +400,19 @@ export type { ZoraDrawerContentProps } from './patterns/zora-drawer-content';
|
|
|
398
400
|
export { ZoraDrawerContent } from './patterns/zora-drawer-content';
|
|
399
401
|
export type { ZoraTabBarProps } from './patterns/zora-tab-bar';
|
|
400
402
|
export { ZoraTabBar } from './patterns/zora-tab-bar';
|
|
403
|
+
export type {
|
|
404
|
+
ComposedZoraPluginCatalog,
|
|
405
|
+
ComposedZoraPluginMetadataCatalog,
|
|
406
|
+
ZoraPluginCompositionErrorCode,
|
|
407
|
+
ZoraPluginDescriptor,
|
|
408
|
+
ZoraPluginMetadata,
|
|
409
|
+
ZoraPluginPlacement,
|
|
410
|
+
} from './pluginComposition';
|
|
411
|
+
export {
|
|
412
|
+
composeZoraPluginMetadata,
|
|
413
|
+
composeZoraPlugins,
|
|
414
|
+
ZoraPluginCompositionError,
|
|
415
|
+
} from './pluginComposition';
|
|
401
416
|
export type { ZoraComponentRegistry } from './registry';
|
|
402
417
|
export { ZORA_COMPONENT_REGISTRY } from './registry';
|
|
403
418
|
export * from './theme';
|
|
@@ -126,6 +126,7 @@ describe('ZORA_COMPONENT_META invariants', () => {
|
|
|
126
126
|
'EmptyState',
|
|
127
127
|
'Hero',
|
|
128
128
|
'Button',
|
|
129
|
+
'RadioGroup',
|
|
129
130
|
'ThemeModeToggle',
|
|
130
131
|
'Input',
|
|
131
132
|
'Textarea',
|
|
@@ -198,6 +199,26 @@ describe('ZORA_COMPONENT_META invariants', () => {
|
|
|
198
199
|
expect(ZORA_COMPONENT_META.Image.props.alt?.authoring).toEqual({ authority: 'instance' });
|
|
199
200
|
});
|
|
200
201
|
|
|
202
|
+
test('RadioGroup is a direct manifest leaf with card presentation and normalized value events', () => {
|
|
203
|
+
const radioGroup = ZORA_COMPONENT_META.RadioGroup;
|
|
204
|
+
|
|
205
|
+
expect(radioGroup.directManifestNode).toBe(true);
|
|
206
|
+
expect(radioGroup.allowedChildren).toEqual([]);
|
|
207
|
+
expect(radioGroup.props.presentation?.enum).toEqual(['inline', 'card']);
|
|
208
|
+
expect(radioGroup.props.options?.type).toBe('array');
|
|
209
|
+
expect(radioGroup.props.options?.itemSchema?.map((item) => item.key)).toEqual([
|
|
210
|
+
'value',
|
|
211
|
+
'label',
|
|
212
|
+
'description',
|
|
213
|
+
'disabled',
|
|
214
|
+
]);
|
|
215
|
+
expect(radioGroup.bindings?.props?.value?.value).toEqual({ type: 'string' });
|
|
216
|
+
expect(radioGroup.events?.valueChange?.eventType).toBe('radioGroup.valueChange');
|
|
217
|
+
expect(radioGroup.events?.valueChange?.payloadFields).toEqual([
|
|
218
|
+
{ path: 'value', type: 'string', label: 'Value' },
|
|
219
|
+
]);
|
|
220
|
+
});
|
|
221
|
+
|
|
201
222
|
test('ThemeModeToggle is a direct manifest leaf with serializable defaults', () => {
|
|
202
223
|
expect(ZORA_COMPONENT_META.ThemeModeToggle.directManifestNode).toBe(true);
|
|
203
224
|
expect(ZORA_COMPONENT_META.ThemeModeToggle.allowedChildren).toEqual([]);
|
package/src/metadata/index.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export { ZORA_CORE_PLUGIN_METADATA } from '../corePluginMetadata';
|
|
2
|
+
export type {
|
|
3
|
+
ComposedZoraPluginMetadataCatalog,
|
|
4
|
+
ZoraPluginMetadata,
|
|
5
|
+
ZoraPluginPlacement,
|
|
6
|
+
} from '../pluginComposition';
|
|
7
|
+
export { composeZoraPluginMetadata, ZoraPluginCompositionError } from '../pluginComposition';
|
|
1
8
|
export type { ZoraBindableComponentType } from './bindableComponentMeta';
|
|
2
9
|
export { ZORA_BINDABLE_COMPONENT_META } from './bindableComponentMeta';
|
|
3
10
|
export { ZORA_COMPONENT_META } from './componentMeta';
|
|
@@ -11,6 +11,7 @@ describe('ZORA_THEME_RECIPE_META', () => {
|
|
|
11
11
|
'Button',
|
|
12
12
|
'Card',
|
|
13
13
|
'Heading',
|
|
14
|
+
'RadioGroup',
|
|
14
15
|
'Text',
|
|
15
16
|
'Panel',
|
|
16
17
|
]);
|
|
@@ -50,6 +51,9 @@ describe('ZORA_THEME_RECIPE_META', () => {
|
|
|
50
51
|
test('keeps recipe defaults aligned with existing runtime behavior', () => {
|
|
51
52
|
expect(ZORA_THEME_RECIPE_META.Button?.fields.size?.default).toBe('l');
|
|
52
53
|
expect(ZORA_THEME_RECIPE_META.Card?.fields.padding?.default).toBeUndefined();
|
|
54
|
+
expect(ZORA_THEME_RECIPE_META.RadioGroup?.fields.gap?.default).toBe('s');
|
|
55
|
+
expect(ZORA_THEME_RECIPE_META.RadioGroup?.fields.color?.default).toBe('primary');
|
|
56
|
+
expect(ZORA_THEME_RECIPE_META.RadioGroup?.fields.size?.default).toBe('m');
|
|
53
57
|
expect(ZORA_THEME_RECIPE_META.Text?.fields.weight?.default).toBeUndefined();
|
|
54
58
|
expect(ZORA_THEME_RECIPE_META.Heading?.fields.size?.default).toBeUndefined();
|
|
55
59
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buttonThemeRecipeMeta } from '../components/button/themeRecipeMeta';
|
|
2
2
|
import { cardThemeRecipeMeta } from '../components/card/themeRecipeMeta';
|
|
3
3
|
import { headingThemeRecipeMeta } from '../components/heading/themeRecipeMeta';
|
|
4
|
+
import { radioGroupThemeRecipeMeta } from '../components/radio/themeRecipeMeta';
|
|
4
5
|
import { textThemeRecipeMeta } from '../components/text/themeRecipeMeta';
|
|
5
6
|
import { panelThemeRecipeMeta } from '../patterns/panel/themeRecipeMeta';
|
|
6
7
|
import type { ZoraThemeRecipeMetaRegistry } from './themeRecipeTypes';
|
|
@@ -9,6 +10,7 @@ export const ZORA_THEME_RECIPE_META: ZoraThemeRecipeMetaRegistry = {
|
|
|
9
10
|
Button: buttonThemeRecipeMeta,
|
|
10
11
|
Card: cardThemeRecipeMeta,
|
|
11
12
|
Heading: headingThemeRecipeMeta,
|
|
13
|
+
RadioGroup: radioGroupThemeRecipeMeta,
|
|
12
14
|
Text: textThemeRecipeMeta,
|
|
13
15
|
Panel: panelThemeRecipeMeta,
|
|
14
16
|
};
|
package/src/metadata/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
ComponentEventDtoKind,
|
|
3
3
|
ComponentRequirements,
|
|
4
4
|
MediaAssetKind,
|
|
5
|
+
UiComponentBindingMeta,
|
|
5
6
|
UiComponentPropType,
|
|
6
7
|
} from '@ankhorage/contracts';
|
|
7
8
|
|
|
@@ -86,6 +87,7 @@ export interface ZoraComponentMeta {
|
|
|
86
87
|
description?: string;
|
|
87
88
|
directManifestNode: boolean;
|
|
88
89
|
allowedChildren: readonly string[];
|
|
90
|
+
bindings?: UiComponentBindingMeta;
|
|
89
91
|
manifestPolicy?: ZoraComponentManifestPolicy;
|
|
90
92
|
requirements?: ComponentRequirements;
|
|
91
93
|
blueprint?: ZoraComponentBlueprint;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import type { ZoraComponentMeta } from './metadata';
|
|
4
|
+
import {
|
|
5
|
+
composeZoraPlugins,
|
|
6
|
+
ZoraPluginCompositionError,
|
|
7
|
+
type ZoraPluginDescriptor,
|
|
8
|
+
} from './pluginComposition';
|
|
9
|
+
|
|
10
|
+
const pluginLeafMeta: ZoraComponentMeta = {
|
|
11
|
+
name: 'PluginLeaf',
|
|
12
|
+
category: 'component',
|
|
13
|
+
directManifestNode: true,
|
|
14
|
+
allowedChildren: [],
|
|
15
|
+
blueprint: { label: 'Plugin leaf' },
|
|
16
|
+
bindings: {
|
|
17
|
+
props: {
|
|
18
|
+
value: { value: { type: 'string' } },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
props: {
|
|
22
|
+
value: { type: 'string', category: 'Content', authoring: { authority: 'instance' } },
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const PluginLeaf = () => null;
|
|
27
|
+
|
|
28
|
+
const Host = () => null;
|
|
29
|
+
|
|
30
|
+
const core = {
|
|
31
|
+
packageName: '@example/core',
|
|
32
|
+
componentRegistry: { Host },
|
|
33
|
+
componentMeta: {
|
|
34
|
+
Host: {
|
|
35
|
+
name: 'Host',
|
|
36
|
+
category: 'layout',
|
|
37
|
+
directManifestNode: true,
|
|
38
|
+
allowedChildren: [],
|
|
39
|
+
props: {},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
extensionHosts: ['Host'],
|
|
43
|
+
} satisfies ZoraPluginDescriptor;
|
|
44
|
+
|
|
45
|
+
const plugin = {
|
|
46
|
+
packageName: '@example/zora-plugin',
|
|
47
|
+
displayName: 'Example plugin',
|
|
48
|
+
componentRegistry: { PluginLeaf },
|
|
49
|
+
componentMeta: { PluginLeaf: pluginLeafMeta },
|
|
50
|
+
placements: [{ child: 'PluginLeaf', parents: ['Host'] }],
|
|
51
|
+
} satisfies ZoraPluginDescriptor;
|
|
52
|
+
|
|
53
|
+
describe('composeZoraPlugins', () => {
|
|
54
|
+
test('composes runtime, authoring, binding, package, and placement projections', () => {
|
|
55
|
+
const catalog = composeZoraPlugins([core, plugin]);
|
|
56
|
+
|
|
57
|
+
expect(catalog.componentRegistry.PluginLeaf).toBe(PluginLeaf);
|
|
58
|
+
expect(catalog.componentMeta.PluginLeaf).toBe(pluginLeafMeta);
|
|
59
|
+
expect(catalog.bindableComponentMeta.PluginLeaf?.bindings).toEqual(pluginLeafMeta.bindings);
|
|
60
|
+
expect(catalog.componentMeta.Host.allowedChildren).toContain('PluginLeaf');
|
|
61
|
+
expect(catalog.packageManifests.map((entry) => entry.packageName)).toEqual([
|
|
62
|
+
'@example/core',
|
|
63
|
+
'@example/zora-plugin',
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('is deterministic regardless of plugin input order', () => {
|
|
68
|
+
const Alpha = () => null;
|
|
69
|
+
const alpha = createPlugin('@example/alpha', 'Alpha', Alpha);
|
|
70
|
+
const beta = createPlugin('@example/beta', 'Beta', () => null);
|
|
71
|
+
|
|
72
|
+
expect(Object.keys(composeZoraPlugins([core, beta, alpha]).componentMeta)).toEqual(
|
|
73
|
+
Object.keys(composeZoraPlugins([alpha, core, beta]).componentMeta),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('rejects duplicate component ownership with a stable diagnostic code', () => {
|
|
78
|
+
const duplicate = createPlugin('@example/duplicate', 'Host', () => null);
|
|
79
|
+
|
|
80
|
+
expect(() => composeZoraPlugins([core, duplicate])).toThrow(ZoraPluginCompositionError);
|
|
81
|
+
try {
|
|
82
|
+
composeZoraPlugins([core, duplicate]);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
expect(error).toMatchObject({
|
|
85
|
+
code: 'duplicate-component',
|
|
86
|
+
packageName: '@example/duplicate',
|
|
87
|
+
componentName: 'Host',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('rejects runtime and metadata mismatches', () => {
|
|
93
|
+
const invalid = {
|
|
94
|
+
...plugin,
|
|
95
|
+
packageName: '@example/invalid',
|
|
96
|
+
componentMeta: {},
|
|
97
|
+
} satisfies ZoraPluginDescriptor;
|
|
98
|
+
|
|
99
|
+
expectCompositionError(invalid, 'missing-component-meta');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('rejects placement into components that did not opt in as extension hosts', () => {
|
|
103
|
+
const invalid = {
|
|
104
|
+
...plugin,
|
|
105
|
+
packageName: '@example/invalid-placement',
|
|
106
|
+
placements: [{ child: 'PluginLeaf', parents: ['PluginLeaf'] }],
|
|
107
|
+
} satisfies ZoraPluginDescriptor;
|
|
108
|
+
|
|
109
|
+
expectCompositionError(invalid, 'invalid-placement-parent');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function createPlugin(
|
|
114
|
+
packageName: string,
|
|
115
|
+
componentName: string,
|
|
116
|
+
component: ZoraPluginDescriptor['componentRegistry'][string],
|
|
117
|
+
): ZoraPluginDescriptor {
|
|
118
|
+
return {
|
|
119
|
+
packageName,
|
|
120
|
+
componentRegistry: { [componentName]: component },
|
|
121
|
+
componentMeta: {
|
|
122
|
+
[componentName]: {
|
|
123
|
+
...pluginLeafMeta,
|
|
124
|
+
name: componentName,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function expectCompositionError(
|
|
131
|
+
descriptor: ZoraPluginDescriptor,
|
|
132
|
+
code: ZoraPluginCompositionError['code'],
|
|
133
|
+
): void {
|
|
134
|
+
try {
|
|
135
|
+
composeZoraPlugins([core, descriptor]);
|
|
136
|
+
throw new Error('Expected plugin composition to fail.');
|
|
137
|
+
} catch (error) {
|
|
138
|
+
expect(error).toBeInstanceOf(ZoraPluginCompositionError);
|
|
139
|
+
expect(error).toMatchObject({ code });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type { UiComponentMetaRegistry, UiComponentPackageManifest } from '@ankhorage/contracts';
|
|
2
|
+
|
|
3
|
+
import type { ZoraComponentMeta, ZoraComponentMetaRegistry } from './metadata/types';
|
|
4
|
+
import type { ZoraComponentRegistry } from './registry';
|
|
5
|
+
|
|
6
|
+
export type ZoraPluginCompositionErrorCode =
|
|
7
|
+
| 'duplicate-component'
|
|
8
|
+
| 'duplicate-package'
|
|
9
|
+
| 'invalid-component-meta'
|
|
10
|
+
| 'invalid-interaction-policy-component'
|
|
11
|
+
| 'invalid-placement-child'
|
|
12
|
+
| 'invalid-placement-parent'
|
|
13
|
+
| 'missing-component-meta'
|
|
14
|
+
| 'missing-runtime-component';
|
|
15
|
+
|
|
16
|
+
export interface ZoraPluginPlacement {
|
|
17
|
+
readonly child: string;
|
|
18
|
+
readonly parents: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ZoraPluginMetadata {
|
|
22
|
+
readonly packageName: string;
|
|
23
|
+
readonly displayName?: string;
|
|
24
|
+
readonly componentMeta: ZoraComponentMetaRegistry;
|
|
25
|
+
readonly extensionHosts?: readonly string[];
|
|
26
|
+
readonly placements?: readonly ZoraPluginPlacement[];
|
|
27
|
+
readonly interactionPolicySupportedComponents?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ZoraPluginDescriptor extends ZoraPluginMetadata {
|
|
31
|
+
readonly componentRegistry: ZoraComponentRegistry;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ComposedZoraPluginMetadataCatalog {
|
|
35
|
+
readonly componentMeta: ZoraComponentMetaRegistry;
|
|
36
|
+
readonly bindableComponentMeta: UiComponentMetaRegistry;
|
|
37
|
+
readonly packageManifests: readonly UiComponentPackageManifest[];
|
|
38
|
+
readonly interactionPolicySupportedComponents: Readonly<Record<string, true>>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ComposedZoraPluginCatalog extends ComposedZoraPluginMetadataCatalog {
|
|
42
|
+
readonly componentRegistry: ZoraComponentRegistry;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/*** Report one deterministic plugin composition contract violation. */
|
|
46
|
+
export class ZoraPluginCompositionError extends Error {
|
|
47
|
+
readonly code: ZoraPluginCompositionErrorCode;
|
|
48
|
+
readonly packageName: string;
|
|
49
|
+
readonly componentName?: string;
|
|
50
|
+
|
|
51
|
+
constructor(args: {
|
|
52
|
+
readonly code: ZoraPluginCompositionErrorCode;
|
|
53
|
+
readonly packageName: string;
|
|
54
|
+
readonly message: string;
|
|
55
|
+
readonly componentName?: string;
|
|
56
|
+
}) {
|
|
57
|
+
super(args.message);
|
|
58
|
+
this.name = 'ZoraPluginCompositionError';
|
|
59
|
+
this.code = args.code;
|
|
60
|
+
this.packageName = args.packageName;
|
|
61
|
+
this.componentName = args.componentName;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/*** Compose a selected ZORA metadata set for authoring tools that must not load React Native runtime modules. */
|
|
66
|
+
export function composeZoraPluginMetadata(
|
|
67
|
+
plugins: readonly ZoraPluginMetadata[],
|
|
68
|
+
): ComposedZoraPluginMetadataCatalog {
|
|
69
|
+
const descriptors = sortPlugins(plugins);
|
|
70
|
+
const packages = new Set<string>();
|
|
71
|
+
const componentOwners = new Map<string, string>();
|
|
72
|
+
const componentMeta = new Map<string, ZoraComponentMeta>();
|
|
73
|
+
const extensionHosts = new Set<string>();
|
|
74
|
+
const interactionPolicySupportedComponents = new Set<string>();
|
|
75
|
+
|
|
76
|
+
for (const descriptor of descriptors) {
|
|
77
|
+
validateUniquePackage(descriptor, packages);
|
|
78
|
+
validateMetadata(descriptor);
|
|
79
|
+
for (const [componentName, meta] of sortedEntries(descriptor.componentMeta)) {
|
|
80
|
+
claimComponent(componentName, descriptor.packageName, componentOwners);
|
|
81
|
+
componentMeta.set(componentName, meta);
|
|
82
|
+
}
|
|
83
|
+
for (const componentName of descriptor.extensionHosts ?? []) extensionHosts.add(componentName);
|
|
84
|
+
for (const componentName of descriptor.interactionPolicySupportedComponents ?? []) {
|
|
85
|
+
interactionPolicySupportedComponents.add(componentName);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const descriptor of descriptors) {
|
|
90
|
+
for (const placement of descriptor.placements ?? []) {
|
|
91
|
+
applyPlacement({ descriptor, placement, componentMeta, extensionHosts });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
componentMeta: Object.fromEntries(componentMeta),
|
|
97
|
+
bindableComponentMeta: Object.fromEntries(componentMeta),
|
|
98
|
+
packageManifests: descriptors.map(({ packageName, displayName, componentMeta }) => ({
|
|
99
|
+
packageName,
|
|
100
|
+
...(displayName === undefined ? {} : { displayName }),
|
|
101
|
+
components: componentMeta,
|
|
102
|
+
})),
|
|
103
|
+
interactionPolicySupportedComponents: Object.fromEntries(
|
|
104
|
+
[...interactionPolicySupportedComponents]
|
|
105
|
+
.sort()
|
|
106
|
+
.map((componentName) => [componentName, true]),
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/*** Compose a selected ZORA core/plugin descriptor set into one validated authoring/runtime catalog. */
|
|
112
|
+
export function composeZoraPlugins(
|
|
113
|
+
plugins: readonly ZoraPluginDescriptor[],
|
|
114
|
+
): ComposedZoraPluginCatalog {
|
|
115
|
+
const descriptors = sortPlugins(plugins);
|
|
116
|
+
const componentRegistry = new Map<string, ZoraComponentRegistry[string]>();
|
|
117
|
+
|
|
118
|
+
for (const descriptor of descriptors) {
|
|
119
|
+
validateRuntime(descriptor);
|
|
120
|
+
for (const [componentName, component] of sortedEntries(descriptor.componentRegistry)) {
|
|
121
|
+
componentRegistry.set(componentName, component);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const metadataCatalog = composeZoraPluginMetadata(descriptors);
|
|
125
|
+
|
|
126
|
+
return { ...metadataCatalog, componentRegistry: Object.fromEntries(componentRegistry) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function validateUniquePackage(descriptor: ZoraPluginMetadata, packages: Set<string>): void {
|
|
130
|
+
if (packages.has(descriptor.packageName)) {
|
|
131
|
+
throw compositionError(
|
|
132
|
+
'duplicate-package',
|
|
133
|
+
descriptor.packageName,
|
|
134
|
+
`Duplicate ZORA plugin package '${descriptor.packageName}'.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
packages.add(descriptor.packageName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validateMetadata(descriptor: ZoraPluginMetadata): void {
|
|
141
|
+
for (const [componentName, meta] of sortedEntries(descriptor.componentMeta)) {
|
|
142
|
+
if (meta.name !== componentName) {
|
|
143
|
+
throw compositionError(
|
|
144
|
+
'invalid-component-meta',
|
|
145
|
+
descriptor.packageName,
|
|
146
|
+
`ZORA plugin '${descriptor.packageName}' metadata key '${componentName}' does not match meta.name '${meta.name}'.`,
|
|
147
|
+
componentName,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function validateRuntime(descriptor: ZoraPluginDescriptor): void {
|
|
154
|
+
const componentMetaNames = new Set(Object.keys(descriptor.componentMeta));
|
|
155
|
+
const runtimeComponentNames = new Set(Object.keys(descriptor.componentRegistry));
|
|
156
|
+
for (const componentName of sortedKeys(descriptor.componentRegistry)) {
|
|
157
|
+
if (!componentMetaNames.has(componentName)) {
|
|
158
|
+
throw compositionError(
|
|
159
|
+
'missing-component-meta',
|
|
160
|
+
descriptor.packageName,
|
|
161
|
+
`ZORA plugin '${descriptor.packageName}' registers runtime component '${componentName}' without component metadata.`,
|
|
162
|
+
componentName,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const [componentName, meta] of sortedEntries(descriptor.componentMeta)) {
|
|
167
|
+
if (meta.directManifestNode && !runtimeComponentNames.has(componentName)) {
|
|
168
|
+
throw compositionError(
|
|
169
|
+
'missing-runtime-component',
|
|
170
|
+
descriptor.packageName,
|
|
171
|
+
`ZORA plugin '${descriptor.packageName}' exposes direct manifest node '${componentName}' without a runtime component.`,
|
|
172
|
+
componentName,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const componentName of descriptor.interactionPolicySupportedComponents ?? []) {
|
|
177
|
+
if (!runtimeComponentNames.has(componentName)) {
|
|
178
|
+
throw compositionError(
|
|
179
|
+
'invalid-interaction-policy-component',
|
|
180
|
+
descriptor.packageName,
|
|
181
|
+
`ZORA plugin '${descriptor.packageName}' declares interaction-policy support for unregistered component '${componentName}'.`,
|
|
182
|
+
componentName,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function claimComponent(
|
|
189
|
+
componentName: string,
|
|
190
|
+
packageName: string,
|
|
191
|
+
owners: Map<string, string>,
|
|
192
|
+
): void {
|
|
193
|
+
const existingOwner = owners.get(componentName);
|
|
194
|
+
if (existingOwner) {
|
|
195
|
+
throw compositionError(
|
|
196
|
+
'duplicate-component',
|
|
197
|
+
packageName,
|
|
198
|
+
`ZORA component '${componentName}' is owned by both '${existingOwner}' and '${packageName}'.`,
|
|
199
|
+
componentName,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
owners.set(componentName, packageName);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function applyPlacement(args: {
|
|
206
|
+
descriptor: ZoraPluginMetadata;
|
|
207
|
+
placement: ZoraPluginPlacement;
|
|
208
|
+
componentMeta: Map<string, ZoraComponentMeta>;
|
|
209
|
+
extensionHosts: ReadonlySet<string>;
|
|
210
|
+
}): void {
|
|
211
|
+
const { descriptor, placement, componentMeta, extensionHosts } = args;
|
|
212
|
+
const childMeta = descriptor.componentMeta[placement.child];
|
|
213
|
+
if (!childMeta?.directManifestNode) {
|
|
214
|
+
throw compositionError(
|
|
215
|
+
'invalid-placement-child',
|
|
216
|
+
descriptor.packageName,
|
|
217
|
+
`ZORA plugin '${descriptor.packageName}' placement child '${placement.child}' is not an owned direct manifest node.`,
|
|
218
|
+
placement.child,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
for (const parentName of [...placement.parents].sort()) {
|
|
222
|
+
const parentMeta = componentMeta.get(parentName);
|
|
223
|
+
if (!parentMeta || !extensionHosts.has(parentName)) {
|
|
224
|
+
throw compositionError(
|
|
225
|
+
'invalid-placement-parent',
|
|
226
|
+
descriptor.packageName,
|
|
227
|
+
`ZORA plugin '${descriptor.packageName}' placement parent '${parentName}' is not a declared extension host.`,
|
|
228
|
+
placement.child,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
componentMeta.set(parentName, addAllowedChild(parentMeta, placement.child));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function addAllowedChild(meta: ZoraComponentMeta, child: string): ZoraComponentMeta {
|
|
236
|
+
const allowedChildren = appendUnique(meta.allowedChildren, child);
|
|
237
|
+
const childrenSlot = meta.slots?.children;
|
|
238
|
+
return {
|
|
239
|
+
...meta,
|
|
240
|
+
allowedChildren,
|
|
241
|
+
...(childrenSlot
|
|
242
|
+
? {
|
|
243
|
+
slots: {
|
|
244
|
+
...meta.slots,
|
|
245
|
+
children: {
|
|
246
|
+
...childrenSlot,
|
|
247
|
+
allowedChildren: appendUnique(childrenSlot.allowedChildren ?? [], child),
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
: {}),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function appendUnique(values: readonly string[], value: string): readonly string[] {
|
|
256
|
+
return values.includes(value) ? values : [...values, value];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function sortedKeys(value: Readonly<Record<string, unknown>>): string[] {
|
|
260
|
+
return Object.keys(value).sort();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function sortedEntries<T>(value: Readonly<Record<string, T>>): [string, T][] {
|
|
264
|
+
return Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function sortPlugins<T extends ZoraPluginMetadata>(plugins: readonly T[]): T[] {
|
|
268
|
+
return [...plugins].sort((left, right) => left.packageName.localeCompare(right.packageName));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function compositionError(
|
|
272
|
+
code: ZoraPluginCompositionErrorCode,
|
|
273
|
+
packageName: string,
|
|
274
|
+
message: string,
|
|
275
|
+
componentName?: string,
|
|
276
|
+
): ZoraPluginCompositionError {
|
|
277
|
+
return new ZoraPluginCompositionError({ code, packageName, message, componentName });
|
|
278
|
+
}
|