@ankhorage/contracts 7.1.0 → 7.3.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 +12 -0
- package/README.md +1 -1
- package/dist/appManifest/bindings.d.ts +4 -0
- package/dist/appManifest/bindings.d.ts.map +1 -0
- package/dist/appManifest/bindings.js +96 -0
- package/dist/appManifest/bindings.js.map +1 -0
- package/dist/appManifest/dataSources.d.ts +2 -0
- package/dist/appManifest/dataSources.d.ts.map +1 -0
- package/dist/appManifest/dataSources.js +133 -0
- package/dist/appManifest/dataSources.js.map +1 -0
- package/dist/appManifest/generatedApis.d.ts +2 -0
- package/dist/appManifest/generatedApis.d.ts.map +1 -0
- package/dist/appManifest/generatedApis.js +87 -0
- package/dist/appManifest/generatedApis.js.map +1 -0
- package/dist/appManifest/infra.d.ts +2 -0
- package/dist/appManifest/infra.d.ts.map +1 -0
- package/dist/appManifest/infra.js +136 -0
- package/dist/appManifest/infra.js.map +1 -0
- package/dist/appManifest/media.d.ts +3 -0
- package/dist/appManifest/media.d.ts.map +1 -0
- package/dist/appManifest/media.js +75 -0
- package/dist/appManifest/media.js.map +1 -0
- package/dist/appManifest/screens.d.ts +6 -0
- package/dist/appManifest/screens.d.ts.map +1 -0
- package/dist/appManifest/screens.js +119 -0
- package/dist/appManifest/screens.js.map +1 -0
- package/dist/appManifest/shared.d.ts +8 -0
- package/dist/appManifest/shared.d.ts.map +1 -0
- package/dist/appManifest/shared.js +31 -0
- package/dist/appManifest/shared.js.map +1 -0
- package/dist/appManifest.d.ts +19 -0
- package/dist/appManifest.d.ts.map +1 -0
- package/dist/appManifest.js +67 -0
- package/dist/appManifest.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/media.d.ts +48 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +11 -0
- package/dist/media.js.map +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +8 -2
- package/src/appManifest/bindings.ts +135 -0
- package/src/appManifest/dataSources.ts +175 -0
- package/src/appManifest/generatedApis.ts +122 -0
- package/src/appManifest/infra.ts +199 -0
- package/src/appManifest/media.ts +101 -0
- package/src/appManifest/screens.ts +171 -0
- package/src/appManifest/shared.ts +40 -0
- package/src/appManifest.test.ts +203 -0
- package/src/appManifest.ts +89 -0
- package/src/index.ts +2 -0
- package/src/media.test.ts +70 -0
- package/src/media.ts +68 -0
- package/src/types.ts +3 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { isAppManifest, parseAppManifest } from './appManifest';
|
|
4
|
+
|
|
5
|
+
function createManifest(): Record<string, unknown> {
|
|
6
|
+
return {
|
|
7
|
+
metadata: {
|
|
8
|
+
name: 'Example',
|
|
9
|
+
slug: 'example',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
category: 'developer_tools',
|
|
12
|
+
themeId: 'default',
|
|
13
|
+
},
|
|
14
|
+
themes: [
|
|
15
|
+
{
|
|
16
|
+
id: 'default',
|
|
17
|
+
name: 'Default',
|
|
18
|
+
light: { primaryColor: '#3366ff', harmony: 'analogous' },
|
|
19
|
+
dark: { primaryColor: '#6699ff', harmony: 'analogous' },
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
activeThemeId: 'default',
|
|
23
|
+
activeThemeMode: 'light',
|
|
24
|
+
splashScreen: {
|
|
25
|
+
image: './assets/splash.png',
|
|
26
|
+
resizeMode: 'contain',
|
|
27
|
+
backgroundColor: '#ffffff',
|
|
28
|
+
dark: { backgroundColor: '#000000' },
|
|
29
|
+
},
|
|
30
|
+
media: {
|
|
31
|
+
assets: {
|
|
32
|
+
hero: {
|
|
33
|
+
id: 'hero',
|
|
34
|
+
name: 'Hero image',
|
|
35
|
+
kind: 'image',
|
|
36
|
+
source: { kind: 'storage', bucket: 'media', path: 'studio/hero.webp' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
infra: {
|
|
41
|
+
deployment: { target: 'minikube', monitoring: true },
|
|
42
|
+
database: { provider: 'supabase', tier: 'dev' },
|
|
43
|
+
storage: { provider: 'auto', buckets: ['media'] },
|
|
44
|
+
state: { provider: 'legend', persistence: 'local' },
|
|
45
|
+
networking: { domain: 'example.test', cdn: false },
|
|
46
|
+
modules: ['expo-localization'],
|
|
47
|
+
modulesConfig: { localization: { defaultLocale: 'en' } },
|
|
48
|
+
},
|
|
49
|
+
navigator: {
|
|
50
|
+
type: 'stack',
|
|
51
|
+
initialRouteName: 'home',
|
|
52
|
+
routes: [
|
|
53
|
+
{
|
|
54
|
+
name: 'home',
|
|
55
|
+
path: '/',
|
|
56
|
+
screenId: 'home',
|
|
57
|
+
showInPrimaryNavigation: true,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
screens: {
|
|
62
|
+
home: {
|
|
63
|
+
id: 'home',
|
|
64
|
+
name: 'Home',
|
|
65
|
+
root: {
|
|
66
|
+
id: 'root',
|
|
67
|
+
type: 'Stack',
|
|
68
|
+
children: [
|
|
69
|
+
{
|
|
70
|
+
id: 'title',
|
|
71
|
+
type: 'Text',
|
|
72
|
+
repeat: { source: { kind: 'state', path: 'items' }, itemAlias: 'item' },
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
dataLoaders: [
|
|
77
|
+
{
|
|
78
|
+
kind: 'operation',
|
|
79
|
+
id: 'load-users',
|
|
80
|
+
operation: { dataSourceId: 'external', endpointId: 'main', operationId: 'list' },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
requires: {
|
|
84
|
+
permissions: [{ permission: 'camera' }],
|
|
85
|
+
capabilities: [{ capability: 'barcodeScanner' }],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
generatedApis: {
|
|
90
|
+
users: {
|
|
91
|
+
id: 'users',
|
|
92
|
+
protocol: 'rest',
|
|
93
|
+
basePath: '/users',
|
|
94
|
+
database: { id: 'primary', kind: 'database' },
|
|
95
|
+
resources: [
|
|
96
|
+
{
|
|
97
|
+
id: 'users',
|
|
98
|
+
path: '/users',
|
|
99
|
+
collection: {
|
|
100
|
+
name: 'users',
|
|
101
|
+
fields: [{ name: 'id', type: 'uuid', required: true }],
|
|
102
|
+
primaryKey: 'id',
|
|
103
|
+
},
|
|
104
|
+
operations: ['list', 'read'],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
dataSources: {
|
|
110
|
+
external: {
|
|
111
|
+
id: 'external',
|
|
112
|
+
kind: 'api',
|
|
113
|
+
origin: 'external',
|
|
114
|
+
protocol: 'rest',
|
|
115
|
+
baseUrl: 'https://example.test',
|
|
116
|
+
endpoints: {
|
|
117
|
+
main: {
|
|
118
|
+
id: 'main',
|
|
119
|
+
kind: 'http',
|
|
120
|
+
operations: {
|
|
121
|
+
list: { id: 'list', protocol: 'rest', intent: 'read', path: '/users' },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
dataBindings: {
|
|
128
|
+
title: {
|
|
129
|
+
componentId: 'title',
|
|
130
|
+
props: { text: { source: { kind: 'state', path: 'title' } } },
|
|
131
|
+
events: {
|
|
132
|
+
press: [{ target: { kind: 'action', type: 'console' } }],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
settings: {
|
|
137
|
+
apiBaseUrl: 'https://example.test/api',
|
|
138
|
+
localization: { defaultLocale: 'en', locales: ['en', 'de'] },
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
describe('AppManifest runtime parsing', () => {
|
|
144
|
+
it('accepts the canonical manifest including optional nested sections', () => {
|
|
145
|
+
const manifest = createManifest();
|
|
146
|
+
|
|
147
|
+
expect(isAppManifest(manifest)).toBe(true);
|
|
148
|
+
expect(parseAppManifest(manifest)).toEqual({ ok: true, manifest });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('rejects missing required top-level sections', () => {
|
|
152
|
+
const manifest = createManifest();
|
|
153
|
+
delete manifest.settings;
|
|
154
|
+
|
|
155
|
+
expect(isAppManifest(manifest)).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('rejects malformed nested canonical structures', () => {
|
|
159
|
+
const manifest = createManifest();
|
|
160
|
+
const themes = manifest.themes as Record<string, unknown>[];
|
|
161
|
+
const light = themes[0]?.light as Record<string, unknown>;
|
|
162
|
+
light.harmony = 'not-a-harmony';
|
|
163
|
+
|
|
164
|
+
expect(parseAppManifest(manifest)).toEqual({
|
|
165
|
+
ok: false,
|
|
166
|
+
message: 'Value is not a canonical AppManifest.',
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('rejects transient media URLs at the manifest boundary', () => {
|
|
171
|
+
const manifest = createManifest();
|
|
172
|
+
const media = manifest.media as Record<string, Record<string, Record<string, unknown>>>;
|
|
173
|
+
media.assets.hero.source = { kind: 'url', url: 'blob:https://example.test/transient' };
|
|
174
|
+
|
|
175
|
+
expect(isAppManifest(manifest)).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('rejects legacy infra plugin state', () => {
|
|
179
|
+
const manifest = createManifest();
|
|
180
|
+
const infra = manifest.infra as Record<string, unknown>;
|
|
181
|
+
infra.plugins = ['legacy'];
|
|
182
|
+
|
|
183
|
+
expect(isAppManifest(manifest)).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('rejects screen registries whose key disagrees with screen id', () => {
|
|
187
|
+
const manifest = createManifest();
|
|
188
|
+
const screens = manifest.screens as Record<string, unknown>;
|
|
189
|
+
screens.other = screens.home;
|
|
190
|
+
delete screens.home;
|
|
191
|
+
|
|
192
|
+
expect(isAppManifest(manifest)).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('rejects invalid data-binding source kinds', () => {
|
|
196
|
+
const manifest = createManifest();
|
|
197
|
+
const bindings = manifest.dataBindings as Record<string, Record<string, unknown>>;
|
|
198
|
+
const props = bindings.title?.props as Record<string, Record<string, unknown>>;
|
|
199
|
+
props.text = { source: { kind: 'provider-specific', path: 'title' } };
|
|
200
|
+
|
|
201
|
+
expect(isAppManifest(manifest)).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { isComponentDataBindingRegistry } from './appManifest/bindings';
|
|
2
|
+
import { isDataSourceRegistry } from './appManifest/dataSources';
|
|
3
|
+
import { isGeneratedApiRegistry } from './appManifest/generatedApis';
|
|
4
|
+
import { isInfraManifest } from './appManifest/infra';
|
|
5
|
+
import { isMediaManifest } from './appManifest/media';
|
|
6
|
+
import {
|
|
7
|
+
isManifestMetadata,
|
|
8
|
+
isNavigatorSpec,
|
|
9
|
+
isScreenRegistry,
|
|
10
|
+
isSplashScreenSpec,
|
|
11
|
+
isThemeConfig,
|
|
12
|
+
} from './appManifest/screens';
|
|
13
|
+
import { isOptionalString, isRecord, isStringArray } from './appManifest/shared';
|
|
14
|
+
import type { AppManifest } from './types';
|
|
15
|
+
|
|
16
|
+
export type AppManifestParseResult =
|
|
17
|
+
| { readonly ok: true; readonly manifest: AppManifest }
|
|
18
|
+
| { readonly ok: false; readonly message: string };
|
|
19
|
+
|
|
20
|
+
const APP_MANIFEST_KEY_POLICY = {
|
|
21
|
+
metadata: 'required',
|
|
22
|
+
themes: 'required',
|
|
23
|
+
activeThemeId: 'required',
|
|
24
|
+
activeThemeMode: 'optional',
|
|
25
|
+
splashScreen: 'optional',
|
|
26
|
+
media: 'optional',
|
|
27
|
+
infra: 'required',
|
|
28
|
+
navigator: 'required',
|
|
29
|
+
screens: 'required',
|
|
30
|
+
generatedApis: 'optional',
|
|
31
|
+
dataSources: 'optional',
|
|
32
|
+
dataBindings: 'optional',
|
|
33
|
+
settings: 'required',
|
|
34
|
+
} as const satisfies Record<keyof AppManifest, 'optional' | 'required'>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse unknown JSON-compatible input at the canonical AppManifest boundary.
|
|
38
|
+
*
|
|
39
|
+
* Contracts owns structural manifest validation. Consumers may add semantic
|
|
40
|
+
* diagnostics after this parser succeeds, but should not reconstruct the
|
|
41
|
+
* AppManifest shape in their own packages.
|
|
42
|
+
*/
|
|
43
|
+
export function parseAppManifest(value: unknown): AppManifestParseResult {
|
|
44
|
+
return isAppManifest(value)
|
|
45
|
+
? { ok: true, manifest: value }
|
|
46
|
+
: { ok: false, message: 'Value is not a canonical AppManifest.' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Return whether an unknown value satisfies the canonical AppManifest shape. */
|
|
50
|
+
export function isAppManifest(value: unknown): value is AppManifest {
|
|
51
|
+
return (
|
|
52
|
+
isRecord(value) &&
|
|
53
|
+
hasRequiredManifestKeys(value) &&
|
|
54
|
+
isManifestMetadata(value.metadata) &&
|
|
55
|
+
Array.isArray(value.themes) &&
|
|
56
|
+
value.themes.every(isThemeConfig) &&
|
|
57
|
+
typeof value.activeThemeId === 'string' &&
|
|
58
|
+
isActiveThemeMode(value.activeThemeMode) &&
|
|
59
|
+
(value.splashScreen === undefined || isSplashScreenSpec(value.splashScreen)) &&
|
|
60
|
+
(value.media === undefined || isMediaManifest(value.media)) &&
|
|
61
|
+
isInfraManifest(value.infra) &&
|
|
62
|
+
isNavigatorSpec(value.navigator) &&
|
|
63
|
+
isScreenRegistry(value.screens) &&
|
|
64
|
+
(value.generatedApis === undefined || isGeneratedApiRegistry(value.generatedApis)) &&
|
|
65
|
+
(value.dataSources === undefined || isDataSourceRegistry(value.dataSources)) &&
|
|
66
|
+
(value.dataBindings === undefined || isComponentDataBindingRegistry(value.dataBindings)) &&
|
|
67
|
+
isAppSettings(value.settings)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasRequiredManifestKeys(value: Record<string, unknown>): boolean {
|
|
72
|
+
return Object.entries(APP_MANIFEST_KEY_POLICY).every(
|
|
73
|
+
([key, policy]) => policy === 'optional' || key in value,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isActiveThemeMode(value: unknown): boolean {
|
|
78
|
+
return value === undefined || value === 'dark' || value === 'light';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isAppSettings(value: unknown): boolean {
|
|
82
|
+
return (
|
|
83
|
+
isRecord(value) &&
|
|
84
|
+
isRecord(value.localization) &&
|
|
85
|
+
typeof value.localization.defaultLocale === 'string' &&
|
|
86
|
+
isStringArray(value.localization.locales) &&
|
|
87
|
+
isOptionalString(value.apiBaseUrl)
|
|
88
|
+
);
|
|
89
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
export * from './appManifest';
|
|
1
2
|
export * from './auth';
|
|
2
3
|
export * from './bindings';
|
|
3
4
|
export * from './cli';
|
|
4
5
|
export * from './data';
|
|
5
6
|
export * from './db';
|
|
7
|
+
export * from './media';
|
|
6
8
|
export * from './requirements';
|
|
7
9
|
export * from './runtimeCallbacks';
|
|
8
10
|
export * from './secretManifest';
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { isMediaManifest } from './appManifest/media';
|
|
4
|
+
import { isMediaAssetReference } from './media';
|
|
5
|
+
|
|
6
|
+
function createMediaManifest() {
|
|
7
|
+
return {
|
|
8
|
+
assets: {
|
|
9
|
+
hero: {
|
|
10
|
+
id: 'hero',
|
|
11
|
+
name: 'Hero image',
|
|
12
|
+
kind: 'image',
|
|
13
|
+
source: { kind: 'storage', bucket: 'media', path: 'studio/hero.webp' },
|
|
14
|
+
contentType: 'image/webp',
|
|
15
|
+
metadata: { width: 1600, height: 900, sizeBytes: 42000 },
|
|
16
|
+
},
|
|
17
|
+
logo: {
|
|
18
|
+
id: 'logo',
|
|
19
|
+
name: 'Logo',
|
|
20
|
+
kind: 'image',
|
|
21
|
+
source: { kind: 'bundled', path: 'assets/logo.png' },
|
|
22
|
+
},
|
|
23
|
+
remote: {
|
|
24
|
+
id: 'remote',
|
|
25
|
+
name: 'Remote image',
|
|
26
|
+
kind: 'image',
|
|
27
|
+
source: { kind: 'url', url: 'https://example.test/image.png' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('media contracts', () => {
|
|
34
|
+
it('accepts storage, bundled, and stable URL authoring media', () => {
|
|
35
|
+
expect(isMediaManifest(createMediaManifest())).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('requires registry keys to match media asset ids', () => {
|
|
39
|
+
const media = createMediaManifest();
|
|
40
|
+
media.assets.hero.id = 'other';
|
|
41
|
+
|
|
42
|
+
expect(isMediaManifest(media)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('rejects transient or local URL schemes', () => {
|
|
46
|
+
for (const url of [
|
|
47
|
+
'blob:https://example.test/id',
|
|
48
|
+
'file:///tmp/image.png',
|
|
49
|
+
'data:image/png;base64,x',
|
|
50
|
+
]) {
|
|
51
|
+
const media = createMediaManifest();
|
|
52
|
+
media.assets.remote.source = { kind: 'url', url };
|
|
53
|
+
expect(isMediaManifest(media)).toBe(false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('rejects denormalized public URLs on managed storage sources', () => {
|
|
58
|
+
const media = createMediaManifest();
|
|
59
|
+
const source = media.assets.hero.source as Record<string, unknown>;
|
|
60
|
+
source.publicUrl = 'https://example.test/public/hero.webp';
|
|
61
|
+
|
|
62
|
+
expect(isMediaManifest(media)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('validates stable media references', () => {
|
|
66
|
+
expect(isMediaAssetReference({ mediaId: 'hero' })).toBe(true);
|
|
67
|
+
expect(isMediaAssetReference({ mediaId: 'hero', url: 'blob:local' })).toBe(false);
|
|
68
|
+
expect(isMediaAssetReference({ mediaId: '' })).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
package/src/media.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const MEDIA_ASSET_KINDS = ['image', 'audio', 'video', 'font', 'file'] as const;
|
|
2
|
+
|
|
3
|
+
export type MediaAssetKind = (typeof MEDIA_ASSET_KINDS)[number];
|
|
4
|
+
|
|
5
|
+
export interface MediaStorageSource {
|
|
6
|
+
readonly kind: 'storage';
|
|
7
|
+
/** Optional logical storage connection identifier for future multi-storage apps. */
|
|
8
|
+
readonly storageId?: string;
|
|
9
|
+
readonly bucket: string;
|
|
10
|
+
readonly path: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MediaUrlSource {
|
|
14
|
+
readonly kind: 'url';
|
|
15
|
+
/** Stable remote URL. Transient/local URL schemes are not canonical media sources. */
|
|
16
|
+
readonly url: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MediaBundledSource {
|
|
20
|
+
readonly kind: 'bundled';
|
|
21
|
+
/** App-relative bundled asset path resolved by the generated/runtime host. */
|
|
22
|
+
readonly path: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type MediaAssetSource = MediaStorageSource | MediaUrlSource | MediaBundledSource;
|
|
26
|
+
|
|
27
|
+
export interface MediaAssetMetadata {
|
|
28
|
+
readonly originalFileName?: string;
|
|
29
|
+
readonly sizeBytes?: number;
|
|
30
|
+
readonly createdAt?: string;
|
|
31
|
+
readonly width?: number;
|
|
32
|
+
readonly height?: number;
|
|
33
|
+
readonly durationMs?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Canonical Studio-managed authoring media entry. */
|
|
37
|
+
export interface MediaAsset {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly kind: MediaAssetKind;
|
|
41
|
+
readonly source: MediaAssetSource;
|
|
42
|
+
readonly contentType?: string;
|
|
43
|
+
readonly metadata?: MediaAssetMetadata;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type MediaAssetRegistry = Readonly<Record<string, MediaAsset>>;
|
|
47
|
+
|
|
48
|
+
/** App-authoring media pool. Runtime/user-generated uploads do not belong here. */
|
|
49
|
+
export interface MediaManifest {
|
|
50
|
+
readonly assets: MediaAssetRegistry;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Stable component/property reference to one entry in `AppManifest.media.assets`. */
|
|
54
|
+
export interface MediaAssetReference {
|
|
55
|
+
readonly mediaId: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isMediaAssetReference(value: unknown): value is MediaAssetReference {
|
|
59
|
+
return (
|
|
60
|
+
typeof value === 'object' &&
|
|
61
|
+
value !== null &&
|
|
62
|
+
!Array.isArray(value) &&
|
|
63
|
+
Object.keys(value).length === 1 &&
|
|
64
|
+
'mediaId' in value &&
|
|
65
|
+
typeof value.mediaId === 'string' &&
|
|
66
|
+
value.mediaId.trim().length > 0
|
|
67
|
+
);
|
|
68
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ScreenDataLoaderDefinition,
|
|
8
8
|
} from './bindings';
|
|
9
9
|
import type { DataSourceRegistry, GeneratedApiRegistry } from './data';
|
|
10
|
+
import type { MediaManifest } from './media';
|
|
10
11
|
import type { ScreenRequirements } from './requirements';
|
|
11
12
|
import type { ThemeGlobalTokenOverrides, ThemeRecipeOverrides } from './theme';
|
|
12
13
|
|
|
@@ -398,6 +399,8 @@ export interface AppManifest {
|
|
|
398
399
|
activeThemeId: string;
|
|
399
400
|
activeThemeMode?: 'dark' | 'light';
|
|
400
401
|
splashScreen?: SplashScreenSpec;
|
|
402
|
+
/** Studio-managed authoring media. Runtime/user uploads are intentionally separate. */
|
|
403
|
+
media?: MediaManifest;
|
|
401
404
|
infra: InfraManifest;
|
|
402
405
|
navigator: NavigatorSpec;
|
|
403
406
|
screens: Record<string, ScreenSpec>;
|