@velumo/core 0.1.0-beta.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.
Files changed (53) hide show
  1. package/dist/background-validation.d.ts +3 -0
  2. package/dist/background-validation.d.ts.map +1 -0
  3. package/dist/background-validation.js +177 -0
  4. package/dist/background-validation.js.map +1 -0
  5. package/dist/builders.d.ts +134 -0
  6. package/dist/builders.d.ts.map +1 -0
  7. package/dist/builders.js +332 -0
  8. package/dist/builders.js.map +1 -0
  9. package/dist/capabilities.d.ts +7 -0
  10. package/dist/capabilities.d.ts.map +1 -0
  11. package/dist/capabilities.js +59 -0
  12. package/dist/capabilities.js.map +1 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +7 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/layout-validation.d.ts +4 -0
  18. package/dist/layout-validation.d.ts.map +1 -0
  19. package/dist/layout-validation.js +745 -0
  20. package/dist/layout-validation.js.map +1 -0
  21. package/dist/manifest-schema.d.ts +1328 -0
  22. package/dist/manifest-schema.d.ts.map +1 -0
  23. package/dist/manifest-schema.js +774 -0
  24. package/dist/manifest-schema.js.map +1 -0
  25. package/dist/theme-validation.d.ts +3 -0
  26. package/dist/theme-validation.d.ts.map +1 -0
  27. package/dist/theme-validation.js +239 -0
  28. package/dist/theme-validation.js.map +1 -0
  29. package/dist/tsconfig.tsbuildinfo +1 -0
  30. package/dist/types.d.ts +398 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +2 -0
  33. package/dist/types.js.map +1 -0
  34. package/dist/validation-issue.d.ts +18 -0
  35. package/dist/validation-issue.d.ts.map +1 -0
  36. package/dist/validation-issue.js +30 -0
  37. package/dist/validation-issue.js.map +1 -0
  38. package/dist/validation.d.ts +4 -0
  39. package/dist/validation.d.ts.map +1 -0
  40. package/dist/validation.js +274 -0
  41. package/dist/validation.js.map +1 -0
  42. package/package.json +16 -0
  43. package/src/background-validation.ts +315 -0
  44. package/src/builders.ts +626 -0
  45. package/src/capabilities.ts +99 -0
  46. package/src/index.ts +147 -0
  47. package/src/layout-validation.ts +1385 -0
  48. package/src/manifest-schema.ts +823 -0
  49. package/src/theme-validation.ts +410 -0
  50. package/src/types.ts +560 -0
  51. package/src/validation-issue.ts +43 -0
  52. package/src/validation.ts +468 -0
  53. package/tsconfig.json +9 -0
@@ -0,0 +1,468 @@
1
+ import type { ManifestValidationResult, ValidationIssue } from "./types.js";
2
+ import { validateBackground } from "./background-validation.js";
3
+ import { validateLayoutNode } from "./layout-validation.js";
4
+ import { validateTheme } from "./theme-validation.js";
5
+ import { describeType, issue } from "./validation-issue.js";
6
+
7
+ type IndexedRecord = Record<string, unknown>;
8
+
9
+ function isRecord(value: unknown): value is IndexedRecord {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+
13
+ function isNonEmptyString(value: unknown): value is string {
14
+ return typeof value === "string" && value.trim().length > 0;
15
+ }
16
+
17
+ function typeIssue(
18
+ path: string,
19
+ message: string,
20
+ expected: string,
21
+ received: unknown,
22
+ ): ValidationIssue {
23
+ return issue(path, "wrong_type", message, {
24
+ expected,
25
+ received: describeType(received),
26
+ });
27
+ }
28
+
29
+ function requiredIssue(path: string, message: string): ValidationIssue {
30
+ return issue(path, "required", message, {
31
+ suggestion: `Provide a value for \`${path}\`.`,
32
+ });
33
+ }
34
+
35
+ const CAMERA_EASINGS = [
36
+ "linear",
37
+ "ease",
38
+ "ease-in",
39
+ "ease-out",
40
+ "ease-in-out",
41
+ ] as const;
42
+
43
+ const CAMERA_VIEW_KEYS = new Set(["x", "y", "zoom"]);
44
+ const CAMERA_TRANSITION_KEYS = new Set(["duration", "easing"]);
45
+ const CAMERA_STOP_KEYS = new Set(["camera", "transition", "cues", "hold"]);
46
+
47
+ function isPercent(value: unknown): value is number {
48
+ return (
49
+ typeof value === "number" &&
50
+ Number.isFinite(value) &&
51
+ value >= 0 &&
52
+ value <= 100
53
+ );
54
+ }
55
+
56
+ function valueIssue(
57
+ path: string,
58
+ message: string,
59
+ received: unknown,
60
+ ): ValidationIssue {
61
+ return issue(path, "invalid_value", message, {
62
+ received: describeType(received),
63
+ });
64
+ }
65
+
66
+ function unknownFieldIssues(
67
+ record: IndexedRecord,
68
+ allowed: ReadonlySet<string>,
69
+ path: string,
70
+ ): readonly ValidationIssue[] {
71
+ return Object.keys(record)
72
+ .filter((key) => !allowed.has(key))
73
+ .map((key) =>
74
+ issue(
75
+ `${path}.${key}`,
76
+ "unknown_field",
77
+ `${path}.${key} is not allowed`,
78
+ {
79
+ expected: [...allowed].join(", "),
80
+ suggestion: `Remove \`${key}\`; it is not a recognized field here.`,
81
+ },
82
+ ),
83
+ );
84
+ }
85
+
86
+ function validateCameraView(
87
+ view: unknown,
88
+ path: string,
89
+ ): readonly ValidationIssue[] {
90
+ if (!isRecord(view)) {
91
+ return [typeIssue(path, `${path} must be an object`, "object", view)];
92
+ }
93
+ const issues: ValidationIssue[] = [];
94
+ for (const axis of ["x", "y"] as const) {
95
+ if (view[axis] === undefined) {
96
+ issues.push(
97
+ requiredIssue(`${path}.${axis}`, `${path}.${axis} is required`),
98
+ );
99
+ } else if (!isPercent(view[axis])) {
100
+ issues.push(
101
+ valueIssue(
102
+ `${path}.${axis}`,
103
+ `${path}.${axis} must be a percentage 0-100`,
104
+ view[axis],
105
+ ),
106
+ );
107
+ }
108
+ }
109
+ if (view.zoom !== undefined) {
110
+ if (
111
+ typeof view.zoom !== "number" ||
112
+ !Number.isFinite(view.zoom) ||
113
+ view.zoom <= 0
114
+ ) {
115
+ issues.push(
116
+ valueIssue(
117
+ `${path}.zoom`,
118
+ `${path}.zoom must be a number > 0`,
119
+ view.zoom,
120
+ ),
121
+ );
122
+ }
123
+ }
124
+ issues.push(...unknownFieldIssues(view, CAMERA_VIEW_KEYS, path));
125
+ return issues;
126
+ }
127
+
128
+ function validateCameraTransition(
129
+ transition: unknown,
130
+ path: string,
131
+ ): readonly ValidationIssue[] {
132
+ if (!isRecord(transition)) {
133
+ return [typeIssue(path, `${path} must be an object`, "object", transition)];
134
+ }
135
+ const issues: ValidationIssue[] = [];
136
+ if (transition.duration !== undefined) {
137
+ if (
138
+ typeof transition.duration !== "number" ||
139
+ !Number.isFinite(transition.duration) ||
140
+ transition.duration < 0
141
+ ) {
142
+ issues.push(
143
+ valueIssue(
144
+ `${path}.duration`,
145
+ `${path}.duration must be a number >= 0`,
146
+ transition.duration,
147
+ ),
148
+ );
149
+ }
150
+ }
151
+ if (
152
+ transition.easing !== undefined &&
153
+ !CAMERA_EASINGS.includes(
154
+ transition.easing as (typeof CAMERA_EASINGS)[number],
155
+ )
156
+ ) {
157
+ issues.push(
158
+ issue(
159
+ `${path}.easing`,
160
+ "invalid_enum",
161
+ `${path}.easing must be a known easing`,
162
+ {
163
+ expected: CAMERA_EASINGS.join(", "),
164
+ received:
165
+ typeof transition.easing === "string"
166
+ ? transition.easing
167
+ : describeType(transition.easing),
168
+ suggestion: `Use one of: ${CAMERA_EASINGS.join(", ")}.`,
169
+ },
170
+ ),
171
+ );
172
+ }
173
+ issues.push(...unknownFieldIssues(transition, CAMERA_TRANSITION_KEYS, path));
174
+ return issues;
175
+ }
176
+
177
+ function validateCameraStop(
178
+ stop: unknown,
179
+ path: string,
180
+ ): readonly ValidationIssue[] {
181
+ if (!isRecord(stop)) {
182
+ return [typeIssue(path, `${path} must be an object`, "object", stop)];
183
+ }
184
+ const issues: ValidationIssue[] = [];
185
+ if (stop.camera === undefined) {
186
+ issues.push(requiredIssue(`${path}.camera`, `${path}.camera is required`));
187
+ } else {
188
+ issues.push(...validateCameraView(stop.camera, `${path}.camera`));
189
+ }
190
+ if (stop.transition !== undefined) {
191
+ issues.push(
192
+ ...validateCameraTransition(stop.transition, `${path}.transition`),
193
+ );
194
+ }
195
+ if (stop.cues !== undefined) {
196
+ if (!Array.isArray(stop.cues)) {
197
+ issues.push(
198
+ typeIssue(
199
+ `${path}.cues`,
200
+ `${path}.cues must be an array`,
201
+ "array",
202
+ stop.cues,
203
+ ),
204
+ );
205
+ } else {
206
+ stop.cues.forEach((cue, cueIndex) => {
207
+ const cuePath = `${path}.cues[${cueIndex}]`;
208
+ if (!isRecord(cue)) {
209
+ issues.push(
210
+ typeIssue(cuePath, `${cuePath} must be an object`, "object", cue),
211
+ );
212
+ return;
213
+ }
214
+ if (typeof cue.time !== "number") {
215
+ issues.push(
216
+ requiredIssue(`${cuePath}.time`, `${cuePath}.time is required`),
217
+ );
218
+ } else if (!Number.isFinite(cue.time) || cue.time < 0) {
219
+ issues.push(
220
+ valueIssue(
221
+ `${cuePath}.time`,
222
+ `${cuePath}.time must be a finite number >= 0`,
223
+ cue.time,
224
+ ),
225
+ );
226
+ }
227
+ if (!isRecord(cue.action) || typeof cue.action.type !== "string") {
228
+ issues.push(
229
+ requiredIssue(
230
+ `${cuePath}.action`,
231
+ `${cuePath}.action.type is required`,
232
+ ),
233
+ );
234
+ }
235
+ });
236
+ }
237
+ }
238
+ if (stop.hold !== undefined) {
239
+ if (
240
+ typeof stop.hold !== "number" ||
241
+ !Number.isFinite(stop.hold) ||
242
+ stop.hold < 0
243
+ ) {
244
+ issues.push(
245
+ valueIssue(
246
+ `${path}.hold`,
247
+ `${path}.hold must be a number >= 0`,
248
+ stop.hold,
249
+ ),
250
+ );
251
+ }
252
+ }
253
+ issues.push(...unknownFieldIssues(stop, CAMERA_STOP_KEYS, path));
254
+ return issues;
255
+ }
256
+
257
+ function validateCameraPath(
258
+ value: unknown,
259
+ path: string,
260
+ ): readonly ValidationIssue[] {
261
+ if (!Array.isArray(value)) {
262
+ return [typeIssue(path, `${path} must be an array`, "array", value)];
263
+ }
264
+ const issues: ValidationIssue[] = [];
265
+ if (value.length === 0) {
266
+ issues.push(
267
+ issue(path, "constraint", `${path} must contain at least one stop`, {
268
+ suggestion: "Add a cameraStop, or remove the empty cameraPath.",
269
+ }),
270
+ );
271
+ return issues;
272
+ }
273
+ value.forEach((stop, index) => {
274
+ issues.push(...validateCameraStop(stop, `${path}[${index}]`));
275
+ });
276
+ return issues;
277
+ }
278
+
279
+ function validateSlideEntry(
280
+ entry: unknown,
281
+ index: number,
282
+ ): readonly ValidationIssue[] {
283
+ const issues: ValidationIssue[] = [];
284
+ const path = `slides[${index}]`;
285
+
286
+ if (!isRecord(entry)) {
287
+ return [typeIssue(path, `${path} must be an object`, "object", entry)];
288
+ }
289
+
290
+ if (!isNonEmptyString(entry.id)) {
291
+ issues.push(requiredIssue(`${path}.id`, `${path}.id is required`));
292
+ }
293
+
294
+ if (entry.layout !== undefined) {
295
+ issues.push(...validateLayoutNode(entry.layout, `${path}.layout`));
296
+ }
297
+
298
+ if (Array.isArray(entry.layers)) {
299
+ entry.layers.forEach((layer, layerIndex) => {
300
+ const layerPath = `${path}.layers[${layerIndex}]`;
301
+ if (!isRecord(layer)) {
302
+ issues.push(
303
+ typeIssue(
304
+ layerPath,
305
+ `${layerPath} must be an object`,
306
+ "object",
307
+ layer,
308
+ ),
309
+ );
310
+ return;
311
+ }
312
+
313
+ if (!isNonEmptyString(layer.type)) {
314
+ issues.push(
315
+ requiredIssue(`${layerPath}.type`, `${layerPath}.type is required`),
316
+ );
317
+ }
318
+ });
319
+ }
320
+
321
+ if (Array.isArray(entry.cues)) {
322
+ entry.cues.forEach((cue, cueIndex) => {
323
+ const cuePath = `${path}.cues[${cueIndex}]`;
324
+ if (!isRecord(cue)) {
325
+ issues.push(
326
+ typeIssue(cuePath, `${cuePath} must be an object`, "object", cue),
327
+ );
328
+ return;
329
+ }
330
+
331
+ if (typeof cue.time !== "number") {
332
+ issues.push(
333
+ requiredIssue(`${cuePath}.time`, `${cuePath}.time is required`),
334
+ );
335
+ } else if (!Number.isFinite(cue.time) || cue.time < 0) {
336
+ issues.push(
337
+ valueIssue(
338
+ `${cuePath}.time`,
339
+ `${cuePath}.time must be a finite number >= 0`,
340
+ cue.time,
341
+ ),
342
+ );
343
+ }
344
+ });
345
+ }
346
+
347
+ if (Array.isArray(entry.fragments)) {
348
+ entry.fragments.forEach((fragment, fragmentIndex) => {
349
+ const fragmentPath = `${path}.fragments[${fragmentIndex}]`;
350
+ if (!isRecord(fragment)) {
351
+ issues.push(
352
+ typeIssue(
353
+ fragmentPath,
354
+ `${fragmentPath} must be an object`,
355
+ "object",
356
+ fragment,
357
+ ),
358
+ );
359
+ return;
360
+ }
361
+
362
+ if (!isNonEmptyString(fragment.id)) {
363
+ issues.push(
364
+ requiredIssue(`${fragmentPath}.id`, `${fragmentPath}.id is required`),
365
+ );
366
+ }
367
+ });
368
+ }
369
+
370
+ if (entry.background !== undefined) {
371
+ issues.push(...validateBackground(entry.background, `${path}.background`));
372
+ }
373
+
374
+ if (entry.cameraPath !== undefined) {
375
+ if (entry.kind !== "scene") {
376
+ issues.push(
377
+ issue(
378
+ `${path}.cameraPath`,
379
+ "unknown_field",
380
+ `${path}.cameraPath is only allowed on a scene (kind: "scene")`,
381
+ {
382
+ suggestion: `Change this entry's kind to "scene", or remove cameraPath.`,
383
+ },
384
+ ),
385
+ );
386
+ } else {
387
+ issues.push(
388
+ ...validateCameraPath(entry.cameraPath, `${path}.cameraPath`),
389
+ );
390
+ }
391
+ }
392
+
393
+ if (entry.chrome !== undefined && typeof entry.chrome !== "boolean") {
394
+ issues.push(
395
+ typeIssue(
396
+ `${path}.chrome`,
397
+ `${path}.chrome must be a boolean`,
398
+ "boolean",
399
+ entry.chrome,
400
+ ),
401
+ );
402
+ }
403
+
404
+ return issues;
405
+ }
406
+
407
+ function collectIssues(manifest: unknown): readonly ValidationIssue[] {
408
+ if (!isRecord(manifest)) {
409
+ return [
410
+ typeIssue("manifest", "manifest must be an object", "object", manifest),
411
+ ];
412
+ }
413
+
414
+ const issues: ValidationIssue[] = [];
415
+
416
+ if (!isRecord(manifest.deck)) {
417
+ issues.push(
418
+ typeIssue("deck", "deck must be an object", "object", manifest.deck),
419
+ );
420
+ } else {
421
+ if (!isNonEmptyString(manifest.deck.title)) {
422
+ issues.push(requiredIssue("deck.title", "deck.title is required"));
423
+ }
424
+ if (
425
+ manifest.deck.watermark !== undefined &&
426
+ typeof manifest.deck.watermark !== "string"
427
+ ) {
428
+ issues.push(
429
+ typeIssue(
430
+ "deck.watermark",
431
+ "deck.watermark must be a string",
432
+ "string",
433
+ manifest.deck.watermark,
434
+ ),
435
+ );
436
+ }
437
+ }
438
+
439
+ if (!Array.isArray(manifest.slides)) {
440
+ issues.push(
441
+ typeIssue("slides", "slides must be an array", "array", manifest.slides),
442
+ );
443
+ } else {
444
+ manifest.slides.forEach((slide, index) => {
445
+ issues.push(...validateSlideEntry(slide, index));
446
+ });
447
+ }
448
+
449
+ if (manifest.theme !== undefined) {
450
+ issues.push(...validateTheme(manifest.theme));
451
+ }
452
+
453
+ return issues;
454
+ }
455
+
456
+ export function validateManifest(manifest: unknown): ManifestValidationResult {
457
+ const issues = collectIssues(manifest);
458
+
459
+ return {
460
+ valid: issues.length === 0,
461
+ errors: issues.map((current) => current.message),
462
+ issues,
463
+ };
464
+ }
465
+
466
+ export function isValidManifest(manifest: unknown): boolean {
467
+ return validateManifest(manifest).valid;
468
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
7
+ },
8
+ "include": ["src/**/*.ts"]
9
+ }