desen-core 1.0.0-draft
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/.turbo/turbo-build.log +5 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +40 -0
- package/dist/schemas/action.d.ts +3 -0
- package/dist/schemas/action.js +14 -0
- package/dist/schemas/binding.d.ts +91 -0
- package/dist/schemas/binding.js +31 -0
- package/dist/schemas/composition.d.ts +51 -0
- package/dist/schemas/composition.js +99 -0
- package/dist/schemas/element.d.ts +759 -0
- package/dist/schemas/element.js +29 -0
- package/dist/schemas/style.d.ts +192 -0
- package/dist/schemas/style.js +69 -0
- package/dist/schemas/surface.d.ts +39 -0
- package/dist/schemas/surface.js +19 -0
- package/dist/schemas/telemetry.d.ts +32 -0
- package/dist/schemas/telemetry.js +20 -0
- package/dist/schemas/token.d.ts +24 -0
- package/dist/schemas/token.js +36 -0
- package/package.json +17 -0
- package/src/index.ts +63 -0
- package/src/schemas/action.ts +17 -0
- package/src/schemas/binding.ts +38 -0
- package/src/schemas/composition.ts +131 -0
- package/src/schemas/element.ts +36 -0
- package/src/schemas/style.ts +77 -0
- package/src/schemas/surface.ts +20 -0
- package/src/schemas/telemetry.ts +23 -0
- package/src/schemas/token.ts +48 -0
- package/test-schemas.js +62 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { modifiersSpec, bindingSpec } from "./binding";
|
|
3
|
+
import { telemetrySpec } from "./telemetry";
|
|
4
|
+
|
|
5
|
+
// ─── Forward-declare recursive types ─────────────────────────────
|
|
6
|
+
// TypeScript needs explicit type annotations for circular schemas.
|
|
7
|
+
// z.lazy() defers evaluation to runtime, solving circular references.
|
|
8
|
+
|
|
9
|
+
export type NodeSpec = {
|
|
10
|
+
type: string;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// ─── Positioning ─────────────────────────────────────────────────
|
|
15
|
+
// SPEC.md §3.5: relative, sticky, absolute
|
|
16
|
+
export const positioningSpec = z.object({
|
|
17
|
+
type: z.enum(["relative", "sticky", "absolute"]),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// ─── layoutSpec ──────────────────────────────────────────────────
|
|
21
|
+
// SPEC.md §2.3: kind REQUIRED, rest optional
|
|
22
|
+
export const layoutSpec = z.object({
|
|
23
|
+
kind: z.enum(["stack", "grid"]),
|
|
24
|
+
direction: z.enum(["vertical", "horizontal"]).optional(),
|
|
25
|
+
gap: z.number().min(0).optional(),
|
|
26
|
+
columns: z.number().int().min(1).optional(),
|
|
27
|
+
align: z.enum(["start", "center", "end", "stretch"]).optional(),
|
|
28
|
+
positioning: positioningSpec.optional(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ─── Composition types ───────────────────────────────────────────
|
|
32
|
+
const compositionTypeEnum = z.enum(["Card", "Stack", "Grid", "Header", "Footer"]);
|
|
33
|
+
const compositionTypes = new Set(["Card", "Stack", "Grid", "Header", "Footer"]);
|
|
34
|
+
|
|
35
|
+
// ─── Constraints (shared) ────────────────────────────────────────
|
|
36
|
+
const constraintsSpec = z.object({
|
|
37
|
+
a11y: z.object({
|
|
38
|
+
aria_label: z.string().optional(),
|
|
39
|
+
aria_role: z.string().optional(),
|
|
40
|
+
min_contrast_ratio: z.number().min(1).optional(),
|
|
41
|
+
focusable: z.boolean().optional(),
|
|
42
|
+
}).optional(),
|
|
43
|
+
}).passthrough().optional();
|
|
44
|
+
|
|
45
|
+
// ─── nodeSpec (discriminated by type field) ───────────────────────
|
|
46
|
+
// SPEC.md §1.3: Node = Element | Composition | Repeater
|
|
47
|
+
//
|
|
48
|
+
// PERFORMANCE FIX: Instead of z.union() which tries all branches,
|
|
49
|
+
// we use a custom z.lazy() that checks the `type` field to
|
|
50
|
+
// dispatch to the correct schema — avoiding exponential backtracking.
|
|
51
|
+
export const nodeSpec: z.ZodType<NodeSpec> = z.lazy(() =>
|
|
52
|
+
z.any().superRefine((val, ctx) => {
|
|
53
|
+
if (typeof val !== "object" || val === null || typeof val.type !== "string") {
|
|
54
|
+
ctx.addIssue({
|
|
55
|
+
code: z.ZodIssueCode.custom,
|
|
56
|
+
message: "Node must be an object with a 'type' string property.",
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let result;
|
|
62
|
+
if (val.type === "Repeater") {
|
|
63
|
+
result = repeaterSpec.safeParse(val);
|
|
64
|
+
} else if (compositionTypes.has(val.type)) {
|
|
65
|
+
result = compositionSpec.safeParse(val);
|
|
66
|
+
} else {
|
|
67
|
+
// Any other type is treated as an ElementSpec
|
|
68
|
+
result = elementSpecInternal.safeParse(val);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!result.success) {
|
|
72
|
+
for (const issue of result.error.issues) {
|
|
73
|
+
ctx.addIssue(issue);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// ─── Internal ElementSpec (no refine to avoid double-checking in nodeSpec) ──
|
|
80
|
+
const elementSpecInternal = z.object({
|
|
81
|
+
type: z.string().min(1),
|
|
82
|
+
id: z.string().min(1),
|
|
83
|
+
props: z.record(z.any()),
|
|
84
|
+
constraints: constraintsSpec,
|
|
85
|
+
modifiers: modifiersSpec.optional(),
|
|
86
|
+
telemetry: telemetrySpec.optional(),
|
|
87
|
+
}).passthrough()
|
|
88
|
+
.refine(
|
|
89
|
+
(data) => !("children" in data),
|
|
90
|
+
{ message: "ElementSpec MUST NOT contain 'children'. Use CompositionSpec for containers." }
|
|
91
|
+
)
|
|
92
|
+
.refine(
|
|
93
|
+
(data) => !("layout" in data),
|
|
94
|
+
{ message: "ElementSpec MUST NOT contain 'layout'. Use CompositionSpec for layout." }
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// ─── CompositionSpec ─────────────────────────────────────────────
|
|
98
|
+
// SPEC.md §2.3: type (enum), id, layout, children, telemetry REQUIRED.
|
|
99
|
+
export const compositionSpec: z.ZodType<any> = z.lazy(() =>
|
|
100
|
+
z.object({
|
|
101
|
+
type: compositionTypeEnum,
|
|
102
|
+
id: z.string().min(1).regex(/^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/),
|
|
103
|
+
layout: layoutSpec,
|
|
104
|
+
children: z.array(nodeSpec).min(0),
|
|
105
|
+
constraints: constraintsSpec,
|
|
106
|
+
modifiers: modifiersSpec.optional(),
|
|
107
|
+
telemetry: telemetrySpec,
|
|
108
|
+
}).passthrough()
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// ─── RepeaterSpec ────────────────────────────────────────────────
|
|
112
|
+
// SPEC.md §2.5: type MUST be "Repeater", data.bind REQUIRED,
|
|
113
|
+
// template MUST be a valid nodeSpec, telemetry REQUIRED.
|
|
114
|
+
export const repeaterSpec: z.ZodType<any> = z.lazy(() =>
|
|
115
|
+
z.object({
|
|
116
|
+
type: z.literal("Repeater"),
|
|
117
|
+
id: z.string().min(1),
|
|
118
|
+
data: z.object({
|
|
119
|
+
bind: bindingSpec,
|
|
120
|
+
}),
|
|
121
|
+
template: nodeSpec,
|
|
122
|
+
modifiers: modifiersSpec.optional(),
|
|
123
|
+
telemetry: telemetrySpec,
|
|
124
|
+
}).passthrough()
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
128
|
+
export type PositioningSpec = z.infer<typeof positioningSpec>;
|
|
129
|
+
export type LayoutSpec = z.infer<typeof layoutSpec>;
|
|
130
|
+
export type CompositionSpec = z.infer<typeof compositionSpec>;
|
|
131
|
+
export type RepeaterSpec = z.infer<typeof repeaterSpec>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { modifiersSpec } from "./binding";
|
|
3
|
+
import { telemetrySpec } from "./telemetry";
|
|
4
|
+
|
|
5
|
+
// ─── ElementSpec ─────────────────────────────────────────────────
|
|
6
|
+
// SPEC.md §2.2: type, id, props REQUIRED.
|
|
7
|
+
//
|
|
8
|
+
// CRITICAL RULE: ElementSpec MUST NOT contain `children` or `layout`.
|
|
9
|
+
// These belong exclusively to CompositionSpec (§2.3).
|
|
10
|
+
// Enforced via .refine() to reject any payload with these fields.
|
|
11
|
+
export const elementSpec = z.object({
|
|
12
|
+
type: z.string().min(1),
|
|
13
|
+
id: z.string().min(1),
|
|
14
|
+
props: z.record(z.any()),
|
|
15
|
+
constraints: z.object({
|
|
16
|
+
a11y: z.object({
|
|
17
|
+
aria_label: z.string().optional(),
|
|
18
|
+
aria_role: z.string().optional(),
|
|
19
|
+
min_contrast_ratio: z.number().min(1).optional(),
|
|
20
|
+
focusable: z.boolean().optional(),
|
|
21
|
+
}).optional(),
|
|
22
|
+
}).passthrough().optional(),
|
|
23
|
+
modifiers: modifiersSpec.optional(),
|
|
24
|
+
telemetry: telemetrySpec.optional(),
|
|
25
|
+
}).passthrough()
|
|
26
|
+
.refine(
|
|
27
|
+
(data) => !("children" in data),
|
|
28
|
+
{ message: "ElementSpec MUST NOT contain 'children'. Use CompositionSpec for containers." }
|
|
29
|
+
)
|
|
30
|
+
.refine(
|
|
31
|
+
(data) => !("layout" in data),
|
|
32
|
+
{ message: "ElementSpec MUST NOT contain 'layout'. Use CompositionSpec for layout." }
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
36
|
+
export type ElementSpec = z.infer<typeof elementSpec>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// ─── Token-Aware Style Schema ────────────────────────────────────
|
|
4
|
+
// SPEC.md §2.6: Properties support both literal and token-based config.
|
|
5
|
+
// Convention: literal `bg_color` + parallel `bg_color_token` (DTCG path).
|
|
6
|
+
//
|
|
7
|
+
// When both are present, runtimes MUST attempt token resolution first.
|
|
8
|
+
// If token resolution fails and literal is type-compatible, runtimes
|
|
9
|
+
// MAY use literal as graceful degradation.
|
|
10
|
+
|
|
11
|
+
// DTCG token path: e.g. "color.bg.primary"
|
|
12
|
+
const dtcgTokenPath = z.string().min(1);
|
|
13
|
+
|
|
14
|
+
export const styleSpec = z.object({
|
|
15
|
+
// ── Color Properties ──
|
|
16
|
+
bg_color: z.string().optional(),
|
|
17
|
+
bg_color_token: dtcgTokenPath.optional(),
|
|
18
|
+
text_color: z.string().optional(),
|
|
19
|
+
text_color_token: dtcgTokenPath.optional(),
|
|
20
|
+
border_color: z.string().optional(),
|
|
21
|
+
border_color_token: dtcgTokenPath.optional(),
|
|
22
|
+
|
|
23
|
+
// ── Typography Properties ──
|
|
24
|
+
font_family: z.string().optional(),
|
|
25
|
+
font_family_token: dtcgTokenPath.optional(),
|
|
26
|
+
font_size: z.number().optional(),
|
|
27
|
+
font_size_token: dtcgTokenPath.optional(),
|
|
28
|
+
font_weight: z.number().optional(),
|
|
29
|
+
font_weight_token: dtcgTokenPath.optional(),
|
|
30
|
+
line_height: z.number().optional(),
|
|
31
|
+
line_height_token: dtcgTokenPath.optional(),
|
|
32
|
+
|
|
33
|
+
// ── Spacing Properties ──
|
|
34
|
+
padding: z.union([
|
|
35
|
+
z.number(),
|
|
36
|
+
z.object({
|
|
37
|
+
top: z.number().optional(),
|
|
38
|
+
right: z.number().optional(),
|
|
39
|
+
bottom: z.number().optional(),
|
|
40
|
+
left: z.number().optional(),
|
|
41
|
+
}),
|
|
42
|
+
]).optional(),
|
|
43
|
+
padding_token: dtcgTokenPath.optional(),
|
|
44
|
+
margin: z.union([
|
|
45
|
+
z.number(),
|
|
46
|
+
z.object({
|
|
47
|
+
top: z.number().optional(),
|
|
48
|
+
right: z.number().optional(),
|
|
49
|
+
bottom: z.number().optional(),
|
|
50
|
+
left: z.number().optional(),
|
|
51
|
+
}),
|
|
52
|
+
]).optional(),
|
|
53
|
+
margin_token: dtcgTokenPath.optional(),
|
|
54
|
+
|
|
55
|
+
// ── Border Properties ──
|
|
56
|
+
border_radius: z.number().optional(),
|
|
57
|
+
border_radius_token: dtcgTokenPath.optional(),
|
|
58
|
+
border_width: z.number().optional(),
|
|
59
|
+
border_width_token: dtcgTokenPath.optional(),
|
|
60
|
+
|
|
61
|
+
// ── Dimension Properties ──
|
|
62
|
+
min_height: z.number().optional(),
|
|
63
|
+
min_height_token: dtcgTokenPath.optional(),
|
|
64
|
+
min_width: z.number().optional(),
|
|
65
|
+
min_width_token: dtcgTokenPath.optional(),
|
|
66
|
+
width: z.union([z.number(), z.string()]).optional(),
|
|
67
|
+
width_token: dtcgTokenPath.optional(),
|
|
68
|
+
height: z.union([z.number(), z.string()]).optional(),
|
|
69
|
+
height_token: dtcgTokenPath.optional(),
|
|
70
|
+
|
|
71
|
+
// ── Opacity ──
|
|
72
|
+
opacity: z.number().min(0).max(1).optional(),
|
|
73
|
+
opacity_token: dtcgTokenPath.optional(),
|
|
74
|
+
}).passthrough(); // Allow vendor extensions (§2.9)
|
|
75
|
+
|
|
76
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
77
|
+
export type StyleSpec = z.infer<typeof styleSpec>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { compositionSpec } from "./composition";
|
|
3
|
+
import { telemetrySpec } from "./telemetry";
|
|
4
|
+
|
|
5
|
+
// ─── SurfaceSpec ─────────────────────────────────────────────────
|
|
6
|
+
// SPEC.md §2.8: id, root, telemetry REQUIRED.
|
|
7
|
+
//
|
|
8
|
+
// CRITICAL RULE: root MUST be a CompositionSpec.
|
|
9
|
+
// It MUST NOT be an ElementSpec. An Element cannot exist at the
|
|
10
|
+
// root level; it must be contained within a Composition.
|
|
11
|
+
export const surfaceSpec = z.object({
|
|
12
|
+
id: z.string().min(1).regex(/^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/),
|
|
13
|
+
root: compositionSpec,
|
|
14
|
+
telemetry: telemetrySpec,
|
|
15
|
+
metadata: z.record(z.any()).optional(),
|
|
16
|
+
environment: z.record(z.any()).optional(),
|
|
17
|
+
}).passthrough();
|
|
18
|
+
|
|
19
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
20
|
+
export type SurfaceSpec = z.infer<typeof surfaceSpec>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// ─── telemetrySpec ───────────────────────────────────────────────
|
|
4
|
+
// SPEC.md §2.3/$defs/telemetrySpec: object with emit string array
|
|
5
|
+
export const telemetrySpec = z.object({
|
|
6
|
+
emit: z.array(z.string()),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// ─── TelemetryEnvelope ───────────────────────────────────────────
|
|
10
|
+
// SPEC.md §4.5: All telemetry events MUST share this common envelope.
|
|
11
|
+
// All fields are REQUIRED per the normative schema.
|
|
12
|
+
export const telemetryEnvelopeSpec = z.object({
|
|
13
|
+
session_id: z.string().min(1),
|
|
14
|
+
element_id: z.string().min(1),
|
|
15
|
+
timestamp: z.string(), // ISO 8601 UTC
|
|
16
|
+
revision_id: z.string().min(1),
|
|
17
|
+
event_type: z.string().min(1),
|
|
18
|
+
payload: z.record(z.any()),
|
|
19
|
+
}).passthrough(); // additionalProperties: true per §4.5
|
|
20
|
+
|
|
21
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
22
|
+
export type TelemetrySpec = z.infer<typeof telemetrySpec>;
|
|
23
|
+
export type TelemetryEnvelope = z.infer<typeof telemetryEnvelopeSpec>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// ─── DTCG Token Format Schema ────────────────────────────────────
|
|
4
|
+
// SPEC.md §2.6: DESEN MUST reuse the Design Tokens Community Group
|
|
5
|
+
// (DTCG) Design Tokens Format Module.
|
|
6
|
+
//
|
|
7
|
+
// A token leaf node has { $type, $value } (required) and
|
|
8
|
+
// optional $description. Non-leaf nodes are groups that contain
|
|
9
|
+
// other token nodes or groups (recursive).
|
|
10
|
+
//
|
|
11
|
+
// Example:
|
|
12
|
+
// {
|
|
13
|
+
// "color": {
|
|
14
|
+
// "bg": {
|
|
15
|
+
// "primary": { "$type": "color", "$value": "#0B5FFF" }
|
|
16
|
+
// }
|
|
17
|
+
// }
|
|
18
|
+
// }
|
|
19
|
+
|
|
20
|
+
// Leaf token node — the actual design token
|
|
21
|
+
export const dtcgTokenLeaf = z.object({
|
|
22
|
+
$type: z.string().min(1),
|
|
23
|
+
$value: z.any(),
|
|
24
|
+
$description: z.string().optional(),
|
|
25
|
+
}).passthrough();
|
|
26
|
+
|
|
27
|
+
// Recursive token group — can contain leaves or nested groups
|
|
28
|
+
export type DTCGTokenGroup = {
|
|
29
|
+
[key: string]: DTCGTokenGroup | { $type: string; $value: any; $description?: string };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// The full token theme is a recursive structure.
|
|
33
|
+
// We use z.record + z.lazy for recursive validation.
|
|
34
|
+
const dtcgTokenNode: z.ZodType<any> = z.lazy(() =>
|
|
35
|
+
z.union([
|
|
36
|
+
dtcgTokenLeaf,
|
|
37
|
+
z.record(dtcgTokenNode),
|
|
38
|
+
])
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// ─── TokenThemeSchema ────────────────────────────────────────────
|
|
42
|
+
// Top-level theme file: a record of token groups
|
|
43
|
+
// e.g. { "color": { "bg": { "primary": { "$type": "color", "$value": "#0B5FFF" } } } }
|
|
44
|
+
export const tokenThemeSchema = z.record(dtcgTokenNode);
|
|
45
|
+
|
|
46
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
47
|
+
export type DTCGTokenLeaf = z.infer<typeof dtcgTokenLeaf>;
|
|
48
|
+
export type TokenTheme = z.infer<typeof tokenThemeSchema>;
|
package/test-schemas.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { surfaceSpec, elementSpec, tokenThemeSchema } = require('./dist/index');
|
|
2
|
+
|
|
3
|
+
// TEST 1: Valid SurfaceSpec
|
|
4
|
+
const validSurface = {
|
|
5
|
+
id: 'promo.surface',
|
|
6
|
+
root: {
|
|
7
|
+
type: 'Stack',
|
|
8
|
+
id: 'promo.root',
|
|
9
|
+
layout: { kind: 'stack', direction: 'vertical' },
|
|
10
|
+
children: [
|
|
11
|
+
{ type: 'Text', id: 'promo.title', props: { text: 'Hello DESEN' } }
|
|
12
|
+
],
|
|
13
|
+
telemetry: { emit: ['VIEW_ELEMENT'] }
|
|
14
|
+
},
|
|
15
|
+
telemetry: { emit: ['VIEW_ELEMENT'] }
|
|
16
|
+
};
|
|
17
|
+
const r1 = surfaceSpec.safeParse(validSurface);
|
|
18
|
+
console.log('TEST 1 (Valid Surface):', r1.success ? 'PASS ✅' : 'FAIL ❌', r1.error?.issues?.map(i => i.message).join('; ') || '');
|
|
19
|
+
|
|
20
|
+
// TEST 2: Element with children → MUST FAIL
|
|
21
|
+
const badElement = { type: 'Button', id: 'bad', props: {}, children: [] };
|
|
22
|
+
const r2 = elementSpec.safeParse(badElement);
|
|
23
|
+
console.log('TEST 2 (Element+children rejected):', !r2.success ? 'PASS ✅' : 'FAIL ❌', r2.error?.issues?.map(i => i.message).join('; ') || '');
|
|
24
|
+
|
|
25
|
+
// TEST 3: Element with layout → MUST FAIL
|
|
26
|
+
const badElement2 = { type: 'Button', id: 'bad2', props: {}, layout: { kind: 'stack' } };
|
|
27
|
+
const r3 = elementSpec.safeParse(badElement2);
|
|
28
|
+
console.log('TEST 3 (Element+layout rejected):', !r3.success ? 'PASS ✅' : 'FAIL ❌', r3.error?.issues?.map(i => i.message).join('; ') || '');
|
|
29
|
+
|
|
30
|
+
// TEST 4: DTCG Token validation
|
|
31
|
+
const validTheme = { color: { bg: { primary: { $type: 'color', $value: '#0B5FFF' } } } };
|
|
32
|
+
const r4 = tokenThemeSchema.safeParse(validTheme);
|
|
33
|
+
console.log('TEST 4 (DTCG Theme valid):', r4.success ? 'PASS ✅' : 'FAIL ❌', r4.error?.issues?.map(i => i.message).join('; ') || '');
|
|
34
|
+
|
|
35
|
+
// TEST 5: Surface with Element root → MUST FAIL
|
|
36
|
+
const badSurface = {
|
|
37
|
+
id: 'bad.surface',
|
|
38
|
+
root: { type: 'Button', id: 'btn', props: {} },
|
|
39
|
+
telemetry: { emit: ['VIEW_ELEMENT'] }
|
|
40
|
+
};
|
|
41
|
+
const r5 = surfaceSpec.safeParse(badSurface);
|
|
42
|
+
console.log('TEST 5 (Element root rejected):', !r5.success ? 'PASS ✅' : 'FAIL ❌');
|
|
43
|
+
|
|
44
|
+
// TEST 6: Valid Element with modifiers binding
|
|
45
|
+
const validElement = {
|
|
46
|
+
type: 'Button',
|
|
47
|
+
id: 'admin.delete',
|
|
48
|
+
props: { text: 'Delete' },
|
|
49
|
+
modifiers: {
|
|
50
|
+
visible: { bind: { source: 'session', path: 'user.is_admin', fallback: false } }
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const r6 = elementSpec.safeParse(validElement);
|
|
54
|
+
console.log('TEST 6 (Element+modifiers valid):', r6.success ? 'PASS ✅' : 'FAIL ❌', r6.error?.issues?.map(i => i.message).join('; ') || '');
|
|
55
|
+
|
|
56
|
+
// TEST 7: Binding with invalid source → MUST FAIL
|
|
57
|
+
const { bindingSpec } = require('./dist/index');
|
|
58
|
+
const badBinding = { source: 'unknown_source', path: 'user.name' };
|
|
59
|
+
const r7 = bindingSpec.safeParse(badBinding);
|
|
60
|
+
console.log('TEST 7 (Invalid binding source rejected):', !r7.success ? 'PASS ✅' : 'FAIL ❌');
|
|
61
|
+
|
|
62
|
+
console.log('\n--- All tests complete ---');
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true
|
|
11
|
+
},
|
|
12
|
+
"include": [
|
|
13
|
+
"src/**/*"
|
|
14
|
+
]
|
|
15
|
+
}
|