@micacms/astro 0.5.0 → 0.6.0-rc.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/README.md +16 -4
- package/dist/images.d.ts +12 -0
- package/dist/images.d.ts.map +1 -0
- package/dist/images.js +41 -0
- package/dist/images.js.map +1 -0
- package/dist/index.d.ts +22 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -17
- package/dist/index.js.map +1 -1
- package/package.json +20 -7
- package/setup/content-collections.md +59 -45
- package/setup/finish.md +50 -15
- package/setup/new-project.md +6 -2
- package/setup/reference.md +19 -33
- package/src/images.ts +49 -0
- package/src/index.ts +495 -0
- package/src/manifest.ts +18 -0
- package/src/richtext.ts +62 -0
- package/src/root.ts +37 -0
|
@@ -13,12 +13,15 @@ entries are converted once. The website then reads them through
|
|
|
13
13
|
- The Git working tree must be clean (`git status`). If it is not, ask the
|
|
14
14
|
user to commit or stash first.
|
|
15
15
|
- Ask the user whether to work on a new branch, and make it if they agree.
|
|
16
|
-
- Build the website once, and copy
|
|
17
|
-
outside the project
|
|
16
|
+
- Build the website once, and copy two results to a temporary directory
|
|
17
|
+
outside the project: the website, which step 8 compares with, and Astro's
|
|
18
|
+
data store, which `mica import` reads in step 6. Astro writes the store
|
|
19
|
+
again when the collections change, so this copy is the only one.
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
22
|
npx astro build
|
|
21
23
|
cp -r dist <temporary directory>/dist-before
|
|
24
|
+
cp node_modules/.astro/data-store.json <temporary directory>/data-store-before.json
|
|
22
25
|
```
|
|
23
26
|
|
|
24
27
|
## 2. Make an inventory
|
|
@@ -64,7 +67,16 @@ Show the inventory as a short table, and ask:
|
|
|
64
67
|
- Headings lose their `id` and code blocks lose their highlighting, unless
|
|
65
68
|
the website adds them. See
|
|
66
69
|
[reference.md](./reference.md#what-markdoc-renders-differently-from-astros-markdown).
|
|
67
|
-
4. Should editors put a collection in a manual order?
|
|
70
|
+
4. Should editors put a collection in a manual order? If a collection has a
|
|
71
|
+
number field that only sorts it, such as `order`, give the user the choice
|
|
72
|
+
and the reason:
|
|
73
|
+
- Keep it as a `number()` field. No page changes. Editors type numbers,
|
|
74
|
+
and two entries can get the same one.
|
|
75
|
+
- Make the collection `sortable: true`. Editors drag the entries into
|
|
76
|
+
order, which is easier and cannot give two entries the same place. The
|
|
77
|
+
import turns the numbers into `_order.json` (`--order`), the field goes, and each
|
|
78
|
+
page that sorts by it sorts by `entry.data._mica.position` instead: one
|
|
79
|
+
changed line per page.
|
|
68
80
|
|
|
69
81
|
Wait for the answers.
|
|
70
82
|
|
|
@@ -79,7 +91,7 @@ Wait for the answers.
|
|
|
79
91
|
| `z.date()`, `z.coerce.date()` | `date()` (stored as `"YYYY-MM-DD"`) |
|
|
80
92
|
| `z.enum([...])` | `select({ options })` |
|
|
81
93
|
| `z.array(z.enum([...]))` | `select({ multiple: true, options })` |
|
|
82
|
-
| `image()` | `media({ accept: "image" })` |
|
|
94
|
+
| `image()` | `media({ accept: "image", alt: "required" })`, or `media({ accept: "image" })` for a decorative image |
|
|
83
95
|
| `reference("authors")` | `relation({ collection: "authors" })` |
|
|
84
96
|
| `z.object({...})` | `object({ fields })` |
|
|
85
97
|
| `z.array(z.object({...}))` | `repeater({ fields })` |
|
|
@@ -100,51 +112,49 @@ lowerCamelCase.
|
|
|
100
112
|
3. In `src/content.config.ts`, remove the definitions of the collections that
|
|
101
113
|
Mica now manages, and add `...micaCollections(site)`. Keep the others.
|
|
102
114
|
4. Run `npx astro sync`. It writes `content/.mica/schema.json`, which the
|
|
103
|
-
|
|
115
|
+
import reads. The pages can fail now; step 7 fixes them.
|
|
104
116
|
|
|
105
117
|
## 6. Convert the entries
|
|
106
118
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
Keep the source files. Do not delete anything in this step.
|
|
135
|
-
|
|
136
|
-
Run the script, then:
|
|
119
|
+
`mica import` reads the copy of the data store from step 1, where Astro keeps
|
|
120
|
+
every entry after its own loader and schema. Each field of the Mica model takes
|
|
121
|
+
the value with the same name. The command converts it to the stored form: a
|
|
122
|
+
date to `"YYYY-MM-DD"` in UTC, a reference to a slug, an `image()` path to a
|
|
123
|
+
copy in the entry's `media/` folder, and the Markdown body to the rich-text
|
|
124
|
+
field `body`. It makes each slug from the entry id, and writes the files the
|
|
125
|
+
way the editor does.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx mica import --from <temporary directory>/data-store-before.json \
|
|
129
|
+
--map blog.cover.alt=coverAlt --order team=order
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- `--map <collection>.<field>=<key>` takes a field from another key. Use it
|
|
133
|
+
when a name changed, for the alt text of an image (`blog.cover.alt=coverAlt`),
|
|
134
|
+
and for a body that goes to a rich-text field with another name
|
|
135
|
+
(`blog.content=@body`). `--map` does not reach into the rows of a repeater.
|
|
136
|
+
- `--order <collection>=<field>` writes the manual order of a `sortable`
|
|
137
|
+
collection from a number field, if the user chose that in step 3.
|
|
138
|
+
- Names after the options import only those collections.
|
|
139
|
+
|
|
140
|
+
The command lists every key it did not import, every id that gives the same
|
|
141
|
+
slug as another, and every image it did not find. Fix the model or add a
|
|
142
|
+
`--map`, delete the collection's directory in `content/`, and run it again.
|
|
143
|
+
Then make the changes to the bodies that the user decided in step 3 directly
|
|
144
|
+
in the `.mdoc` files, and run:
|
|
137
145
|
|
|
138
146
|
```bash
|
|
139
147
|
npx mica check
|
|
140
148
|
```
|
|
141
149
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
Read every warning: "Not read" means that an entry or a file is lost, and a
|
|
151
|
+
Markdoc warning means that some text does not render as before. If you cannot
|
|
152
|
+
fix a warning, list it in the report with the reason. Keep the source files.
|
|
153
|
+
Do not delete anything in this step.
|
|
154
|
+
|
|
155
|
+
If the import cannot express a conversion, for example a value that the model
|
|
156
|
+
splits into two fields, write a script for that part only, with `writeEntry`
|
|
157
|
+
from [reference.md](./reference.md#write-entries-with-a-script).
|
|
148
158
|
|
|
149
159
|
## 7. Change the pages
|
|
150
160
|
|
|
@@ -161,9 +171,12 @@ endpoints such as `rss.xml.js`. See
|
|
|
161
171
|
strings gives `NaN`, and the list is then not sorted, with no error.
|
|
162
172
|
- Where an API needs a `Date`, such as the RSS `pubDate`, pass
|
|
163
173
|
`new Date(entry.data.pubDate)`.
|
|
164
|
-
- An image
|
|
165
|
-
|
|
166
|
-
`
|
|
174
|
+
- An `image()` field is still `ImageMetadata` with `media({ accept: "image" })`,
|
|
175
|
+
so its pages do not change. With `alt`, the value is `{ src, alt }`: use
|
|
176
|
+
`entry.data.cover.src` where the page used `entry.data.cover`, and
|
|
177
|
+
`entry.data.cover.alt` in place of the old alt-text field, or spread it:
|
|
178
|
+
`<Image {...entry.data.cover} />`. See
|
|
179
|
+
[reference.md](./reference.md#images).
|
|
167
180
|
- A reference was `{ collection, id }` and is now a slug:
|
|
168
181
|
`getEntry("authors", entry.data.author)`.
|
|
169
182
|
- `entry.id` is the slug. If ids had folders, update the links, and add
|
|
@@ -200,7 +213,8 @@ Ask the user before each of these:
|
|
|
200
213
|
such as `@astrojs/mdx` when no MDX is left, and those that only the
|
|
201
214
|
conversion needed. Change text in the content that describes what you
|
|
202
215
|
removed.
|
|
203
|
-
- Delete the
|
|
216
|
+
- Delete the temporary directory with `dist-before` and the data store copy,
|
|
217
|
+
and a conversion script if you wrote one.
|
|
204
218
|
|
|
205
219
|
Then run `npx mica check` and `npx astro build` once more.
|
|
206
220
|
|
package/setup/finish.md
CHANGED
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
# Finish the setup
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1. Add scripts
|
|
4
|
+
|
|
5
|
+
Add two scripts to `package.json`, so that the user has short commands:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
"mica": "mica start",
|
|
9
|
+
"check": "mica check && astro check"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- If the project has no `@astrojs/check`, use `"check": "mica check"`. Do not
|
|
13
|
+
install it only for this: without it, `astro check` asks to install it, and a
|
|
14
|
+
script cannot answer. Tell the user that nothing checks the types in
|
|
15
|
+
`.astro` files.
|
|
16
|
+
- If the project already has a `mica` script, keep it. If it has a `check`
|
|
17
|
+
script without `mica check`, ask the user whether to add `mica check &&` in
|
|
18
|
+
front of it. If the user says no, use `npx mica check` in place of
|
|
19
|
+
`npm run check` in the steps below and in the notes.
|
|
20
|
+
|
|
21
|
+
## 2. Check
|
|
4
22
|
|
|
5
23
|
All three must pass:
|
|
6
24
|
|
|
7
25
|
```bash
|
|
8
26
|
npx astro sync
|
|
9
|
-
|
|
27
|
+
npm run check # mica check: 0 errors; then astro check, if it is there
|
|
10
28
|
npx astro build
|
|
11
29
|
```
|
|
12
30
|
|
|
13
|
-
If
|
|
14
|
-
|
|
15
|
-
the user.
|
|
31
|
+
If `astro check` reports errors that were there before you started, tell the
|
|
32
|
+
user. Do not fix them unless the user asks.
|
|
16
33
|
|
|
17
34
|
Explain each warning of `mica check` to the user, or fix it.
|
|
18
35
|
|
|
19
|
-
##
|
|
36
|
+
## 3. Add notes for the next agent
|
|
20
37
|
|
|
21
38
|
Add this section to the project's agent instructions: `AGENTS.md`,
|
|
22
39
|
`CLAUDE.md` or the file the project already uses. If `CLAUDE.md` is a link to
|
|
@@ -27,23 +44,41 @@ paths if the project uses others.
|
|
|
27
44
|
## Content (Mica)
|
|
28
45
|
|
|
29
46
|
- The content model is `src/mica.ts`. The content is JSON and Markdoc in
|
|
30
|
-
`content/`; editors change it with `
|
|
31
|
-
- After a change to `src/mica.ts`, run `npx astro sync` and `
|
|
47
|
+
`content/`; editors change it with `npm run mica`.
|
|
48
|
+
- After a change to `src/mica.ts`, run `npx astro sync` and `npm run check`,
|
|
32
49
|
and commit `content/.mica/schema.json` with the change.
|
|
33
50
|
- Renaming or removing a field or a collection does not move the stored
|
|
34
51
|
content. Change the content files in the same commit, or ask first.
|
|
35
|
-
- Before a commit that changes `content/`, `
|
|
52
|
+
- Before a commit that changes `content/`, `npm run check` must pass.
|
|
36
53
|
```
|
|
37
54
|
|
|
38
|
-
##
|
|
55
|
+
## 4. Tell the user
|
|
39
56
|
|
|
40
57
|
- What you added and changed, file by file.
|
|
41
58
|
- The decisions they made, and what was not converted, if anything.
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
- How to commit: give the commands, and run them only if the user asks. With
|
|
60
|
+
converted content, use two commits, so that the history keeps the setup
|
|
61
|
+
apart from the content. Name the files of the first commit from
|
|
62
|
+
`git status`:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 1. The setup: packages, model, collections config, agent notes, scripts.
|
|
66
|
+
git add package.json package-lock.json src/mica.ts src/content.config.ts \
|
|
67
|
+
content/.mica/schema.json AGENTS.md
|
|
68
|
+
git commit -m "Add Mica"
|
|
69
|
+
# 2. The content and the page changes: everything else that git status shows.
|
|
70
|
+
git add -A
|
|
71
|
+
git commit -m "Move the content to Mica"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Use the project's lock file (`pnpm-lock.yaml`, `yarn.lock`). For a new
|
|
75
|
+
project without converted content, one commit is enough.
|
|
76
|
+
- How to publish the branch: on a branch that is not on the remote yet,
|
|
77
|
+
`git push -u origin <branch>`. The editor's Publish does this too, the first
|
|
78
|
+
time.
|
|
79
|
+
- How to edit: `npm run mica` opens the editor on this computer. A save writes
|
|
80
|
+
files in `content/`; Publish commits them. `npm run check` checks the content
|
|
81
|
+
(and the types, if `@astrojs/check` is there). `@micacms/editor` is a dev
|
|
47
82
|
dependency, so on another clone, run `npm install` first.
|
|
48
83
|
- That a self-hosted editor for other people is described at
|
|
49
84
|
https://micacms.com/docs/self-hosting/.
|
package/setup/new-project.md
CHANGED
|
@@ -17,8 +17,12 @@ Ask these questions, and wait for the answers:
|
|
|
17
17
|
3. Do entries refer to other entries? For example, an article has an author
|
|
18
18
|
from the team.
|
|
19
19
|
4. Should editors put a collection in a manual order?
|
|
20
|
-
5.
|
|
21
|
-
|
|
20
|
+
5. Which images carry meaning, and which are decorative? An image that
|
|
21
|
+
carries meaning gets `media({ accept: "image", alt: "required" })`, so that
|
|
22
|
+
editors write its alt text. A decorative image gets
|
|
23
|
+
`media({ accept: "image" })`. `accept: "image"` makes the field an Astro
|
|
24
|
+
image on the pages; without it, the field is a path, for a PDF or another
|
|
25
|
+
file.
|
|
22
26
|
6. Do they want example content, so that the pages show something at once?
|
|
23
27
|
|
|
24
28
|
Suggest a model from the answers, as a short list of collections, singletons
|
package/setup/reference.md
CHANGED
|
@@ -9,7 +9,9 @@ The guides link here. Read the section that a step names.
|
|
|
9
9
|
| `npx mica init` | Writes a starter `src/mica.ts` and `src/content.config.ts`. Never overwrites a file |
|
|
10
10
|
| `npx astro sync` | Runs the Mica loaders, which write the model to `content/.mica/schema.json` |
|
|
11
11
|
| `npx mica check` | Checks every entry against `content/.mica/schema.json`. Exit code 1 on an error |
|
|
12
|
-
| `npx mica` |
|
|
12
|
+
| `npx mica import` | Moves Astro collections into Mica, from Astro's data store. See [content-collections.md](./content-collections.md#6-convert-the-entries) |
|
|
13
|
+
| `npx mica migrate-layout` | Moves entries into the layout of the model, after the first or the last rich-text or media field of a collection was added or removed |
|
|
14
|
+
| `npx mica start` | Opens the editor in the browser. Only for the user; do not run it yourself |
|
|
13
15
|
|
|
14
16
|
`mica check` reads the model from `content/.mica/schema.json`. After every
|
|
15
17
|
change to `src/mica.ts`, run `npx astro sync` before `npx mica check`.
|
|
@@ -61,7 +63,7 @@ in the entry list), `readonly`, `hidden`, `group`, `width` (`"full"`, `"half"`,
|
|
|
61
63
|
|
|
62
64
|
| Field | Stored as | Other options |
|
|
63
65
|
| --- | --- | --- |
|
|
64
|
-
| `text()` | string | `minLength`, `maxLength`, `pattern
|
|
66
|
+
| `text()` | string | `minLength`, `maxLength`, `pattern` (must match the whole value), `placeholder`, `default` |
|
|
65
67
|
| `textarea()` | string | `minLength`, `maxLength`, `rows`, `placeholder`, `default` |
|
|
66
68
|
| `richText()` | Markdoc in its own `.mdoc` file | `toolbar`, `maxLength`, `default` |
|
|
67
69
|
| `number()` | number | `min`, `max`, `step`, `integer`, `unit`, `default` |
|
|
@@ -74,7 +76,7 @@ in the entry list), `readonly`, `hidden`, `group`, `width` (`"full"`, `"half"`,
|
|
|
74
76
|
| `object()` | object | `fields` (required), `collapsible`, `collapsed` |
|
|
75
77
|
| `repeater()` | array of objects | `fields` (required), `min`, `max`, `itemLabel`, `addLabel` |
|
|
76
78
|
| `relation()` | the slug of an entry | `collection` (required), `multiple` |
|
|
77
|
-
| `media()` | a file path | `accept: "image" \| "document" \| "any"`, `multiple`, `max` |
|
|
79
|
+
| `media()` | a file path, or `{ path, alt }` with `alt` | `accept: "image" \| "document" \| "any"`, `multiple`, `max`, `alt: "required" \| "optional"` |
|
|
78
80
|
| `color()` | `"#rrggbb"` | `presets`, `default` |
|
|
79
81
|
| `coordinates()` | `{ "lat": 47.37, "lng": 8.54 }` | `defaultCenter`, `defaultZoom` |
|
|
80
82
|
|
|
@@ -152,12 +154,13 @@ export async function writeOrder(collection, slugs) {
|
|
|
152
154
|
await writeFile(join(ROOT, collection, "_order.json"), `${JSON.stringify(slugs, null, 2)}\n`);
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
// Example: `data` has every field, rich text as a Markdoc string, media as
|
|
157
|
+
// Example: `data` has every field, rich text as a Markdoc string, and media as
|
|
158
|
+
// `media/<file>`, or `{ path, alt }` for a `media({ alt })` field.
|
|
156
159
|
// await writeEntry("news", "hello-world", {
|
|
157
160
|
// title: "Hello, world",
|
|
158
161
|
// slug: "hello-world",
|
|
159
162
|
// publishedOn: "2026-01-15",
|
|
160
|
-
// cover: "media/cover.jpg",
|
|
163
|
+
// cover: { path: "media/cover.jpg", alt: "The team at the opening" },
|
|
161
164
|
// body: "The first article.",
|
|
162
165
|
// });
|
|
163
166
|
```
|
|
@@ -206,41 +209,24 @@ const settings = await getEntry("settings", "settings");
|
|
|
206
209
|
|
|
207
210
|
### Images
|
|
208
211
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
```ts
|
|
214
|
-
// src/lib/media.ts — the glob path is relative to this file
|
|
215
|
-
import type { ImageMetadata } from "astro";
|
|
216
|
-
|
|
217
|
-
const images = import.meta.glob<{ default: ImageMetadata }>(
|
|
218
|
-
"../../content/**/*.{jpeg,jpg,png,webp,avif,gif}",
|
|
219
|
-
{ eager: true },
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
/** The image for a Mica media path, or undefined when the file is not there. */
|
|
223
|
-
export function mediaImage(path: string | null | undefined): ImageMetadata | undefined {
|
|
224
|
-
return path ? images[`../../content/${path}`]?.default : undefined;
|
|
225
|
-
}
|
|
226
|
-
```
|
|
212
|
+
A `media({ accept: "image" })` field reaches Astro as an image that Astro
|
|
213
|
+
optimizes, the same as `image()` in an Astro collection: `ImageMetadata`. With
|
|
214
|
+
`alt`, it is `{ src, alt }`. No helper is needed:
|
|
227
215
|
|
|
228
216
|
```astro
|
|
229
217
|
---
|
|
230
218
|
import { Image } from "astro:assets";
|
|
231
|
-
|
|
232
|
-
const cover = mediaImage(article.data.cover);
|
|
219
|
+
const { cover } = article.data; // media({ accept: "image", alt: "required" })
|
|
233
220
|
---
|
|
234
|
-
{cover && <Image
|
|
221
|
+
{cover && <Image {...cover} />}
|
|
235
222
|
```
|
|
236
223
|
|
|
237
|
-
|
|
238
|
-
`
|
|
239
|
-
|
|
240
|
-
A media
|
|
241
|
-
|
|
242
|
-
`import.meta.glob("../../content/**/*.pdf", { query: "?url", import: "default", eager: true })
|
|
243
|
-
and link to the URL.
|
|
224
|
+
- Without `alt`, the field is the image itself: `<Image src={logo} alt="" />`.
|
|
225
|
+
- A `media({ accept: "image", multiple: true })` field is an array of them.
|
|
226
|
+
- On disk, the field still holds `media/cover.jpg`, or `{ path, alt }`.
|
|
227
|
+
- A `media()` without `accept: "image"`, for example a PDF, is a path from the
|
|
228
|
+
content root: `news/hello-world/media/plan.pdf`. To link to it, use
|
|
229
|
+
`import.meta.glob("../../content/**/*.pdf", { query: "?url", import: "default", eager: true })`.
|
|
244
230
|
|
|
245
231
|
### What Markdoc renders differently from Astro's Markdown
|
|
246
232
|
|
package/src/images.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image fields reach Astro through its own `image()`, which resolves a path
|
|
3
|
+
* against the entry's file, optimizes the image and gives `ImageMetadata`. A
|
|
4
|
+
* site then needs no helper: `<Image {...entry.data.cover} />`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { posix } from "node:path";
|
|
8
|
+
|
|
9
|
+
import type { FieldMap } from "@micacms/core";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Rewrites the paths of `media({ accept: "image" })` fields, which the reader
|
|
13
|
+
* gives from the content root, to paths from the entry's data file.
|
|
14
|
+
*/
|
|
15
|
+
export function imagePathsFromEntry(
|
|
16
|
+
fields: FieldMap,
|
|
17
|
+
data: Record<string, unknown>,
|
|
18
|
+
dataFile: string,
|
|
19
|
+
): Record<string, unknown> {
|
|
20
|
+
const from = posix.dirname(dataFile);
|
|
21
|
+
const relative = (path: unknown): unknown => {
|
|
22
|
+
if (typeof path !== "string" || path === "") return path;
|
|
23
|
+
const result = posix.relative(from, path);
|
|
24
|
+
return result.startsWith(".") ? result : `./${result}`;
|
|
25
|
+
};
|
|
26
|
+
const one = (value: unknown): unknown =>
|
|
27
|
+
isRecord(value) ? { ...value, path: relative(value["path"]) } : relative(value);
|
|
28
|
+
|
|
29
|
+
const visit = (map: FieldMap, record: Record<string, unknown>): Record<string, unknown> => {
|
|
30
|
+
const result = { ...record };
|
|
31
|
+
for (const [key, field] of Object.entries(map)) {
|
|
32
|
+
const value = record[key];
|
|
33
|
+
if (field.kind === "media" && field.accept === "image") {
|
|
34
|
+
result[key] = Array.isArray(value) ? value.map(one) : one(value);
|
|
35
|
+
} else if (field.kind === "object" && isRecord(value)) {
|
|
36
|
+
result[key] = visit(field.fields, value);
|
|
37
|
+
} else if (field.kind === "repeater" && Array.isArray(value)) {
|
|
38
|
+
result[key] = value.map((row) => (isRecord(row) ? visit(field.fields, row) : row));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return visit(fields, data);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
48
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
49
|
+
}
|