@evanion/astro-widget 0.1.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 +17 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/define-blocks.d.ts +7 -0
- package/dist/define-blocks.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/validate-blocks.d.ts +10 -0
- package/dist/validate-blocks.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/components/Widgets.astro +42 -0
- package/src/define-blocks.ts +9 -0
- package/src/index.ts +3 -0
- package/src/types.ts +30 -0
- package/src/validate-blocks.ts +45 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## 0.1.0 (2026-08-30)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- **astro-widget:** Add Widgets renderer ([e10b840](https://github.com/Evanion/libraries/commit/e10b840))
|
|
6
|
+
- **astro-widget:** Add validateBlocks ([8280820](https://github.com/Evanion/libraries/commit/8280820))
|
|
7
|
+
- **astro-widget:** Add block types and defineBlocks ([8cceba9](https://github.com/Evanion/libraries/commit/8cceba9))
|
|
8
|
+
|
|
9
|
+
### 🩹 Fixes
|
|
10
|
+
|
|
11
|
+
- **astro-widget:** Fix six pre-publish packaging and packaging-adjacent findings ([09be4c2](https://github.com/Evanion/libraries/commit/09be4c2))
|
|
12
|
+
- **astro-widget:** Correct children and chrome.item docs ([5ffd5e6](https://github.com/Evanion/libraries/commit/5ffd5e6))
|
|
13
|
+
- **astro-widget:** Move key-union assertion into a type-checked test-d file ([1e300d2](https://github.com/Evanion/libraries/commit/1e300d2))
|
|
14
|
+
|
|
15
|
+
### ❤️ Thank You
|
|
16
|
+
|
|
17
|
+
- Mikael Pettersson @Evanion
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mikael Pettersson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# @evanion/astro-widget
|
|
2
|
+
|
|
3
|
+
Render CMS-driven Astro sections from structured block data. Build-time only —
|
|
4
|
+
no runtime, no hydration, nothing shipped to the browser.
|
|
5
|
+
|
|
6
|
+
The Astro counterpart to [`@evanion/react-widget`](../widget).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @evanion/astro-widget
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Astro `>=5` is a peer dependency.
|
|
15
|
+
|
|
16
|
+
## Use
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// src/registry.ts
|
|
20
|
+
import { defineBlocks } from '@evanion/astro-widget';
|
|
21
|
+
import Hero from './blocks/Hero.astro';
|
|
22
|
+
import Cards from './blocks/Cards.astro';
|
|
23
|
+
|
|
24
|
+
export const registry = defineBlocks({ hero: Hero, cards: Cards });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```astro
|
|
28
|
+
---
|
|
29
|
+
import Widgets from '@evanion/astro-widget/components/Widgets.astro';
|
|
30
|
+
import { registry } from '../registry';
|
|
31
|
+
import page from '../data/page.json';
|
|
32
|
+
---
|
|
33
|
+
<Widgets items={page.sections} registry={registry} ctx={{ site: 'example.com' }} />
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Where `page.json` is whatever your CMS writes:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{ "sections": [{ "type": "hero", "heading": "Hello" }] }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Data shape
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
interface BlockItem {
|
|
46
|
+
type: string; // must be a key in the registry
|
|
47
|
+
id?: string; // optional, useful as a DOM anchor
|
|
48
|
+
children?: BlockItem[];
|
|
49
|
+
[key: string]: unknown; // everything else becomes props
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Validation
|
|
54
|
+
|
|
55
|
+
`Widgets` **skips unknown block types silently** so a bad CMS save can never
|
|
56
|
+
break a render. Catch them loudly at build time instead:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { validateBlocks } from '@evanion/astro-widget';
|
|
60
|
+
|
|
61
|
+
const problems = validateBlocks(page.sections, registry, { hero: ['heading'] });
|
|
62
|
+
if (problems.length) {
|
|
63
|
+
for (const p of problems) console.error(`section ${p.index} (${p.type}): ${p.message}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`index` is scoped to whatever level of the tree it was found at: a problem in
|
|
69
|
+
a top-level section and a problem in one of its `children` can both report
|
|
70
|
+
`index: 0`, meaning different things. Use `type` and `message` together with
|
|
71
|
+
`index` to disambiguate, or walk `children` yourself if you need an
|
|
72
|
+
unambiguous path to the offending block.
|
|
73
|
+
|
|
74
|
+
## Chrome
|
|
75
|
+
|
|
76
|
+
Wrap every block without each block reimplementing section markup:
|
|
77
|
+
|
|
78
|
+
```astro
|
|
79
|
+
<Widgets items={items} registry={registry} chrome={{ item: Section }} />
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`Section` receives the block's `type` and props, and **must render
|
|
83
|
+
`<slot />`**. If it doesn't, Astro silently drops the wrapped block — no
|
|
84
|
+
error, no warning, the section just vanishes from the page.
|
|
85
|
+
|
|
86
|
+
## Nesting
|
|
87
|
+
|
|
88
|
+
The renderer does **not** recurse. `children` on a `BlockItem` is forwarded
|
|
89
|
+
to its block as ordinary prop data, nothing more — Astro projects child
|
|
90
|
+
content through `<slot />`, never through a `children` prop. A block that
|
|
91
|
+
wants to render its own nested sections must do so itself:
|
|
92
|
+
|
|
93
|
+
```astro
|
|
94
|
+
---
|
|
95
|
+
// Cards.astro
|
|
96
|
+
import Widgets from '@evanion/astro-widget/components/Widgets.astro';
|
|
97
|
+
import { registry } from '../registry';
|
|
98
|
+
const { children } = Astro.props;
|
|
99
|
+
---
|
|
100
|
+
<Widgets items={children} registry={registry} />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Differences from @evanion/react-widget
|
|
104
|
+
|
|
105
|
+
| | react-widget | astro-widget |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| Provider / `useWidgets` | yes | **no** — build-time rendering has nothing to provide; use `ctx` |
|
|
108
|
+
| Prop type inference | inferred from the component map | **no** — Astro components are opaque at the type level. Use `validateBlocks` |
|
|
109
|
+
| Nested `children` | `<Output />` outlet | **no recursion** — a block must render `<Widgets items={children} registry={registry} />` itself |
|
|
110
|
+
|
|
111
|
+
## Licence
|
|
112
|
+
|
|
113
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BlockRegistry } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Identity function that pins the registry's key union for editors and for
|
|
4
|
+
* `validateBlocks`. It does no work at runtime by design.
|
|
5
|
+
*/
|
|
6
|
+
export declare function defineBlocks<M extends BlockRegistry>(map: M): M;
|
|
7
|
+
//# sourceMappingURL=define-blocks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-blocks.d.ts","sourceRoot":"","sources":["../src/define-blocks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,aAAa,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAE/D"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/define-blocks.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
return e;
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/validate-blocks.ts
|
|
7
|
+
function t(e, n, r = {}) {
|
|
8
|
+
if (!Array.isArray(e)) return [{
|
|
9
|
+
index: -1,
|
|
10
|
+
type: "-",
|
|
11
|
+
message: "sektionslistan är inte en lista"
|
|
12
|
+
}];
|
|
13
|
+
let i = [];
|
|
14
|
+
return e.forEach((e, a) => {
|
|
15
|
+
let o = typeof e?.type == "string" ? e.type : "-";
|
|
16
|
+
if (!(o in n)) i.push({
|
|
17
|
+
index: a,
|
|
18
|
+
type: o,
|
|
19
|
+
message: "okänd blocktyp"
|
|
20
|
+
});
|
|
21
|
+
else for (let t of r[o] ?? []) {
|
|
22
|
+
let n = e[t];
|
|
23
|
+
(n == null || typeof n == "string" && n.trim() === "") && i.push({
|
|
24
|
+
index: a,
|
|
25
|
+
type: o,
|
|
26
|
+
message: `saknar fältet ${t}`
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
Array.isArray(e?.children) && i.push(...t(e.children, n, r));
|
|
30
|
+
}), i;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { e as defineBlocks, t as validateBlocks };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** One section, as written by the CMS. */
|
|
2
|
+
export interface BlockItem {
|
|
3
|
+
/** Discriminator; must be a key in the registry. */
|
|
4
|
+
type: string;
|
|
5
|
+
/** Optional stable id, useful as a DOM anchor. */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Nested blocks, forwarded to the component as ordinary prop data.
|
|
8
|
+
The renderer does NOT recurse — Astro projects child content through
|
|
9
|
+
<slot />, not through a `children` prop. A block that wants nesting
|
|
10
|
+
renders <Widgets items={children} registry={registry} /> itself. */
|
|
11
|
+
children?: BlockItem[];
|
|
12
|
+
/** Everything else is passed to the component as props. */
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Maps a block type to an Astro component.
|
|
17
|
+
*
|
|
18
|
+
* Values are deliberately `unknown`: Astro components are opaque at the type
|
|
19
|
+
* level, so unlike @evanion/react-widget we cannot infer each block's props
|
|
20
|
+
* from the component. `validateBlocks` covers that ground at build time.
|
|
21
|
+
*/
|
|
22
|
+
export type BlockRegistry = Record<string, unknown>;
|
|
23
|
+
/** A problem found by `validateBlocks`. */
|
|
24
|
+
export interface BlockProblem {
|
|
25
|
+
index: number;
|
|
26
|
+
type: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,MAAM,WAAW,SAAS;IACxB,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;2EAGuE;IACvE,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;IACvB,2DAA2D;IAC3D,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BlockItem, BlockProblem, BlockRegistry } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Checks a block list against a registry. Returns problems rather than
|
|
4
|
+
* throwing, so a caller can print all of them at once.
|
|
5
|
+
*
|
|
6
|
+
* `required` maps a block type to the field names that must be present and
|
|
7
|
+
* non-blank.
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateBlocks(items: BlockItem[], registry: BlockRegistry, required?: Record<string, string[]>): BlockProblem[];
|
|
10
|
+
//# sourceMappingURL=validate-blocks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-blocks.d.ts","sourceRoot":"","sources":["../src/validate-blocks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEtE;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,EAAE,EAClB,QAAQ,EAAE,aAAa,EACvB,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAM,GACtC,YAAY,EAAE,CA+BhB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@evanion/astro-widget",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Render CMS-driven Astro sections from structured block data. Build-time, zero runtime.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"astro",
|
|
7
|
+
"widget",
|
|
8
|
+
"cms",
|
|
9
|
+
"blocks",
|
|
10
|
+
"sections",
|
|
11
|
+
"registry",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/Evanion/libraries/tree/main/libs/astro-widget#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/Evanion/libraries/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Evanion/libraries.git",
|
|
21
|
+
"directory": "libs/astro-widget"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Mikael Pettersson",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
"./package.json": "./package.json",
|
|
35
|
+
".": {
|
|
36
|
+
"@evanion/source": "./src/index.ts",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"import": "./dist/index.js",
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./components/*": "./src/components/*"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"!dist/**/*.tsbuildinfo",
|
|
46
|
+
"src",
|
|
47
|
+
"!src/**/*.test.*",
|
|
48
|
+
"!src/**/*.spec.*",
|
|
49
|
+
"!src/**/*.test-d.*",
|
|
50
|
+
"!src/test-setup.ts",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE",
|
|
53
|
+
"CHANGELOG.md"
|
|
54
|
+
],
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"astro": ">=5"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Renders a list of CMS blocks.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately defensive: an unknown type is skipped rather than thrown on, so
|
|
6
|
+
* a gap in build-time validation can never take a page down mid-render.
|
|
7
|
+
* `validateBlocks` is the loud gate; this is the safety net.
|
|
8
|
+
*/
|
|
9
|
+
import type { BlockItem, BlockRegistry } from '../types';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
items: BlockItem[];
|
|
13
|
+
registry: BlockRegistry;
|
|
14
|
+
/** Page-level data passed to every block. Astro renders at build time, so
|
|
15
|
+
there is no context provider to do this for us — and none is needed. */
|
|
16
|
+
ctx?: Record<string, unknown>;
|
|
17
|
+
/** Optional wrapper rendered around every block, e.g. for CMS edit affordances.
|
|
18
|
+
`chrome.item` MUST render <slot /> — if it doesn't, Astro silently drops
|
|
19
|
+
the wrapped block and it will not appear on the page. No error is thrown. */
|
|
20
|
+
chrome?: { item?: unknown };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { items, registry, ctx, chrome } = Astro.props;
|
|
24
|
+
const list = Array.isArray(items) ? items : [];
|
|
25
|
+
const Item = chrome?.item as never;
|
|
26
|
+
---
|
|
27
|
+
{
|
|
28
|
+
list.map((item) => {
|
|
29
|
+
const Component = registry[item?.type] as never;
|
|
30
|
+
if (!Component) return null;
|
|
31
|
+
|
|
32
|
+
const { type, id, children, ...props } = item;
|
|
33
|
+
|
|
34
|
+
return Item ? (
|
|
35
|
+
<Item type={type} id={id} {...props}>
|
|
36
|
+
<Component {...props} id={id} ctx={ctx} children={children} />
|
|
37
|
+
</Item>
|
|
38
|
+
) : (
|
|
39
|
+
<Component {...props} id={id} ctx={ctx} children={children} />
|
|
40
|
+
);
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BlockRegistry } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Identity function that pins the registry's key union for editors and for
|
|
5
|
+
* `validateBlocks`. It does no work at runtime by design.
|
|
6
|
+
*/
|
|
7
|
+
export function defineBlocks<M extends BlockRegistry>(map: M): M {
|
|
8
|
+
return map;
|
|
9
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** One section, as written by the CMS. */
|
|
2
|
+
export interface BlockItem {
|
|
3
|
+
/** Discriminator; must be a key in the registry. */
|
|
4
|
+
type: string;
|
|
5
|
+
/** Optional stable id, useful as a DOM anchor. */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Nested blocks, forwarded to the component as ordinary prop data.
|
|
8
|
+
The renderer does NOT recurse — Astro projects child content through
|
|
9
|
+
<slot />, not through a `children` prop. A block that wants nesting
|
|
10
|
+
renders <Widgets items={children} registry={registry} /> itself. */
|
|
11
|
+
children?: BlockItem[];
|
|
12
|
+
/** Everything else is passed to the component as props. */
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maps a block type to an Astro component.
|
|
18
|
+
*
|
|
19
|
+
* Values are deliberately `unknown`: Astro components are opaque at the type
|
|
20
|
+
* level, so unlike @evanion/react-widget we cannot infer each block's props
|
|
21
|
+
* from the component. `validateBlocks` covers that ground at build time.
|
|
22
|
+
*/
|
|
23
|
+
export type BlockRegistry = Record<string, unknown>;
|
|
24
|
+
|
|
25
|
+
/** A problem found by `validateBlocks`. */
|
|
26
|
+
export interface BlockProblem {
|
|
27
|
+
index: number;
|
|
28
|
+
type: string;
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { BlockItem, BlockProblem, BlockRegistry } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks a block list against a registry. Returns problems rather than
|
|
5
|
+
* throwing, so a caller can print all of them at once.
|
|
6
|
+
*
|
|
7
|
+
* `required` maps a block type to the field names that must be present and
|
|
8
|
+
* non-blank.
|
|
9
|
+
*/
|
|
10
|
+
export function validateBlocks(
|
|
11
|
+
items: BlockItem[],
|
|
12
|
+
registry: BlockRegistry,
|
|
13
|
+
required: Record<string, string[]> = {}
|
|
14
|
+
): BlockProblem[] {
|
|
15
|
+
if (!Array.isArray(items)) {
|
|
16
|
+
return [{ index: -1, type: '-', message: 'sektionslistan är inte en lista' }];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const problems: BlockProblem[] = [];
|
|
20
|
+
|
|
21
|
+
items.forEach((item, index) => {
|
|
22
|
+
const type = typeof item?.type === 'string' ? item.type : '-';
|
|
23
|
+
|
|
24
|
+
if (!(type in registry)) {
|
|
25
|
+
problems.push({ index, type, message: 'okänd blocktyp' });
|
|
26
|
+
} else {
|
|
27
|
+
for (const field of required[type] ?? []) {
|
|
28
|
+
const value = item[field];
|
|
29
|
+
const blank =
|
|
30
|
+
value === undefined ||
|
|
31
|
+
value === null ||
|
|
32
|
+
(typeof value === 'string' && value.trim() === '');
|
|
33
|
+
if (blank) {
|
|
34
|
+
problems.push({ index, type, message: `saknar fältet ${field}` });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(item?.children)) {
|
|
40
|
+
problems.push(...validateBlocks(item.children, registry, required));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return problems;
|
|
45
|
+
}
|