@siteable/core 0.1.1 → 0.1.2
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/index.d.ts +1 -1
- package/dist/index.js +874 -822
- package/package.json +1 -1
- package/src/blocks/faq/FaqBlock.tsx +4 -6
- package/src/blocks/footer/FooterBlock.tsx +4 -6
- package/src/blocks/gallery/GalleryBlock.tsx +5 -6
- package/src/blocks/image/ImageBlock.tsx +5 -1
- package/src/blocks/logocloud/LogoCloudBlock.tsx +3 -2
- package/src/blocks/pricing/PricingBlock.tsx +4 -27
- package/src/blocks/stats/StatsBlock.tsx +4 -6
- package/src/blocks/team/TeamBlock.tsx +4 -6
- package/src/blocks/testimonials/TestimonialsBlock.tsx +4 -6
- package/src/editor/AgentPanel.tsx +3 -1
- package/src/editor/LayersPanel.tsx +3 -1
- package/src/editor/LeftSidebar.tsx +3 -1
- package/src/lib/block-default-content.ts +110 -0
- package/src/lib/block-metadata.ts +28 -11
- package/src/lib/generate-site.ts +15 -2
- package/src/lib/generation-prompt.ts +4 -3
- package/src/lib/prop-normalization.ts +121 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISS-005 — prop normalization at the generation chokepoint.
|
|
3
|
+
*
|
|
4
|
+
* AI generators (Gemini, host /api/generate) return block props that violate the
|
|
5
|
+
* declared contracts in block-metadata (e.g. navbar `links` as [{label,href}]
|
|
6
|
+
* where the block declares string[]). Blocks render those props unguarded →
|
|
7
|
+
* React "Objects are not valid as a React child" crash. validateBlock was the
|
|
8
|
+
* hole: `{ ...defaultProps, ...raw.props }` never checked raw VALUES.
|
|
9
|
+
*
|
|
10
|
+
* This module coerces raw prop values against the SHAPE of the block's
|
|
11
|
+
* defaultProps — fully generic, zero block-name hardcoding.
|
|
12
|
+
*
|
|
13
|
+
* POLICY (review item 2): an empty raw array (e.g. `links: []`) is replaced by
|
|
14
|
+
* the DEFAULT array — DELIBERATE, not an oversight. At this layer the config
|
|
15
|
+
* is unvalidated AI/host output, so "empty" is indistinguishable from
|
|
16
|
+
* "generator emitted nothing meaningful"; rendering a navbar with zero links
|
|
17
|
+
* is a silent broken page, defaults are recoverable. Codified by:
|
|
18
|
+
* tests/prop-normalization.test.ts — 'empty raw array falls back to non-empty defaultProps'
|
|
19
|
+
* tests/validate-site-config.test.ts — footer columns 'links: []' entry case
|
|
20
|
+
* A user who genuinely wants zero links sets them via the editor AFTER this
|
|
21
|
+
* chokepoint (normalize only runs on generation validation, never on
|
|
22
|
+
* editor/store round-trips), so user intent is expressible elsewhere.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// For a string contract, an object value is best-effort rescued by taking the
|
|
26
|
+
// first string field among these display-ish keys (order = priority).
|
|
27
|
+
const STRING_FALLBACK_FIELDS = ['label', 'text', 'title', 'name'] as const
|
|
28
|
+
|
|
29
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
30
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Coerce any value to a string for a `string`/`string[]` contract; null = unresolvable. */
|
|
34
|
+
function toStringOrNull(value: unknown): string | null {
|
|
35
|
+
if (typeof value === 'string') return value
|
|
36
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
|
|
37
|
+
if (typeof value === 'boolean') return String(value)
|
|
38
|
+
if (isPlainObject(value)) {
|
|
39
|
+
for (const field of STRING_FALLBACK_FIELDS) {
|
|
40
|
+
if (typeof value[field] === 'string') return value[field] as string
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeValue(raw: unknown, def: unknown): unknown {
|
|
47
|
+
// string contract
|
|
48
|
+
if (typeof def === 'string') {
|
|
49
|
+
return toStringOrNull(raw) ?? def
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// number contract: keep finite numbers, accept numeric strings, else default
|
|
53
|
+
if (typeof def === 'number') {
|
|
54
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) return raw
|
|
55
|
+
if (typeof raw === 'string' && raw.trim() !== '' && Number.isFinite(Number(raw))) return Number(raw)
|
|
56
|
+
return def
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// boolean contract
|
|
60
|
+
if (typeof def === 'boolean') {
|
|
61
|
+
if (typeof raw === 'boolean') return raw
|
|
62
|
+
if (raw === 'true') return true
|
|
63
|
+
if (raw === 'false') return false
|
|
64
|
+
return def
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// array contracts
|
|
68
|
+
if (Array.isArray(def)) {
|
|
69
|
+
if (!Array.isArray(raw)) return def
|
|
70
|
+
if (def.length === 0) return raw // no element shape declared → pass through
|
|
71
|
+
|
|
72
|
+
const elemDef = def[0]
|
|
73
|
+
const out: unknown[] = []
|
|
74
|
+
for (const entry of raw) {
|
|
75
|
+
if (typeof elemDef === 'string') {
|
|
76
|
+
const s = toStringOrNull(entry) // string entries kept, objects rescued, garbage dropped
|
|
77
|
+
if (s !== null) out.push(s)
|
|
78
|
+
} else if (isPlainObject(elemDef)) {
|
|
79
|
+
// array-of-objects: first default entry is the shape; non-object entries dropped
|
|
80
|
+
if (isPlainObject(entry)) out.push(normalizeBlockProps(entry, elemDef))
|
|
81
|
+
} else if (typeof entry === typeof elemDef) {
|
|
82
|
+
// primitive (number/boolean) elements: keep type-matching entries as-is
|
|
83
|
+
out.push(entry)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// all-garbage / emptied array → show defaults rather than render nothing
|
|
87
|
+
if (out.length === 0) return def
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// object contract: recurse per key
|
|
92
|
+
if (isPlainObject(def)) {
|
|
93
|
+
if (isPlainObject(raw)) return normalizeBlockProps(raw, def)
|
|
94
|
+
return def
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// no contract declared for this default (null/undefined) → keep raw
|
|
98
|
+
return raw
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Coerce `rawProps` values against the shape of `defaultProps`.
|
|
103
|
+
* - Keys missing from rawProps are NOT injected — the caller merges defaultProps.
|
|
104
|
+
* - Keys present in rawProps but absent from defaultProps are kept as-is
|
|
105
|
+
* (editor JSON patches may legitimately add keys; never discard data).
|
|
106
|
+
* - Non-object rawProps (null, string, array) normalize to {}.
|
|
107
|
+
*/
|
|
108
|
+
export function normalizeBlockProps(
|
|
109
|
+
rawProps: unknown,
|
|
110
|
+
defaultProps: Record<string, unknown>,
|
|
111
|
+
): Record<string, unknown> {
|
|
112
|
+
if (!isPlainObject(rawProps)) return {}
|
|
113
|
+
|
|
114
|
+
const out: Record<string, unknown> = { ...rawProps }
|
|
115
|
+
for (const [key, def] of Object.entries(defaultProps)) {
|
|
116
|
+
if (key in out) {
|
|
117
|
+
out[key] = normalizeValue(out[key], def)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out
|
|
121
|
+
}
|