Skip to content

Commit cfd3f1d

Browse files
committed
feat(config): migrate frontmatter schemas to Zod
1 parent f7bb785 commit cfd3f1d

22 files changed

Lines changed: 396 additions & 480 deletions

README.md

Lines changed: 102 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,75 @@
11
# @amamo/mdx
22

3-
Compile trusted MDX into JavaScript modules for a configurable JSX runtime (React by default),
4-
collection metadata, and JSON manifests. A Rust native binding handles parsing, validation, media
5-
rewriting, manifest projection, and persistent cache records; Shiki runs in JavaScript and feeds
6-
highlighted HAST back into the same compile pipeline.
3+
Build MDX collections for Vite 8, Next 16, or a custom Node.js build. Define each collection with
4+
the package's `z` schema builder, then import MDX as application modules or consume the generated
5+
collection registry and JSON manifests.
76

8-
The package exposes three import paths:
7+
`@amamo/mdx` provides:
98

10-
| Import | Purpose |
11-
| ----------------- | -------------------------------------------------- |
12-
| `@amamo/mdx` | Configure and drive the compiler directly. |
13-
| `@amamo/mdx/vite` | Compile MDX through Vite 8. |
14-
| `@amamo/mdx/next` | Compile MDX for Next 16 with Turbopack or Webpack. |
9+
- frontmatter validation and defaults;
10+
- JavaScript modules for a configurable JSX runtime, with React as the default;
11+
- fenced-code highlighting and Markdown media imports;
12+
- collection metadata with companion TypeScript declarations;
13+
- configurable JSON manifests and a persistent build cache.
1514

16-
## Quick start
15+
## Requirements
16+
17+
- Node.js 20.19 or newer.
18+
- A [supported native target](https://jikkai.github.io/mdx/native-targets/). There is no JavaScript
19+
or WASI fallback for MDX compilation.
20+
- React 19 when using the default JSX runtime.
21+
22+
MDX can contain imports, expressions, and JSX. Compile content from authors who are allowed to add
23+
application code.
24+
25+
## Install
1726

1827
```sh
1928
pnpm add @amamo/mdx
2029
```
2130

22-
`@amamo/mdx` requires Node.js 20.19 or newer and a [supported native
23-
target](https://jikkai.github.io/mdx/native-targets/). It has no JavaScript or WASI fallback.
31+
The package manager installs the platform package for the current operating system and CPU. Install
32+
dependencies again after moving the project to a different platform instead of copying
33+
`node_modules`.
34+
35+
## Define a collection
2436

25-
Create a serializable config:
37+
Create `amamo.config.mjs`:
2638

2739
```js
28-
// amamo.config.mjs
29-
import { defineConfig } from '@amamo/mdx'
40+
import { defineConfig, z } from '@amamo/mdx'
3041

3142
export default defineConfig({
3243
root: import.meta.dirname,
3344
collections: {
3445
posts: {
3546
directory: 'content/posts',
36-
schema: {
37-
$schema: 'https://json-schema.org/draft/2020-12/schema',
38-
type: 'object',
39-
properties: { title: { type: 'string' } },
40-
required: ['title'],
41-
},
47+
schema: z.object({
48+
title: z.string(),
49+
publishedAt: z.string().optional(),
50+
}),
4251
},
4352
},
4453
})
4554
```
4655

47-
Then choose the integration that owns the build.
56+
Then add `content/posts/hello.mdx`:
57+
58+
```mdx
59+
---
60+
title: Hello
61+
publishedAt: 2026-08-15
62+
---
63+
64+
# Hello
65+
66+
This document is compiled by @amamo/mdx.
67+
```
68+
69+
The collection directory must exist before the first full build. Relative collection, cache,
70+
generated, and manifest paths are resolved from `root`.
71+
72+
## Choose an integration
4873

4974
### Vite
5075

@@ -55,7 +80,9 @@ import { defineConfig } from 'vite'
5580

5681
import amamo from './amamo.config.mjs'
5782

58-
export default defineConfig({ plugins: [amamoMdx(amamo)] })
83+
export default defineConfig({
84+
plugins: [amamoMdx(amamo)],
85+
})
5986
```
6087

6188
### Next
@@ -66,17 +93,21 @@ import { withAmamoMdx } from '@amamo/mdx/next'
6693

6794
import amamo from './amamo.config.mjs'
6895

69-
export default withAmamoMdx(amamo)({ reactStrictMode: true })
96+
export default withAmamoMdx(amamo)({
97+
reactStrictMode: true,
98+
})
7099
```
71100

72101
### Direct compiler API
73102

74-
```ts
103+
```js
104+
// build-content.mjs
75105
import { createCompiler } from '@amamo/mdx'
76106

77107
import amamo from './amamo.config.mjs'
78108

79109
const compiler = await createCompiler(amamo)
110+
80111
try {
81112
const result = await compiler.build()
82113
console.log(result)
@@ -85,26 +116,57 @@ try {
85116
}
86117
```
87118

88-
The first build writes these compiler-owned files under `generatedDirectory` (default
89-
`.amamo-mdx`):
119+
Run the script with `node build-content.mjs`.
90120

91-
- `collections.mjs` — collection metadata with lazy imports of the source MDX files.
92-
- `collections.d.ts` — a companion declaration output for the collection registry.
93-
- `index.json` — the private index used by the Next loader.
121+
## Use compiled content
94122

95-
Cache and manifest paths are configured separately and are resolved from `root`.
123+
With the Vite plugin or Next wrapper configured, import an MDX file like an application module:
96124

97-
## Security boundary
125+
```tsx
126+
import Post, { frontmatter } from './content/posts/hello.mdx'
98127

99-
MDX modules can execute JavaScript when the host imports or renders them. Compile only content from
100-
trusted authors; schema validation is not a sandbox. Frontmatter fields are ordinary data and may
101-
appear in compiled modules, cache records, and configured manifests. Store protected values in an
102-
encrypted form and decrypt them in the consumer, or keep them outside frontmatter. See the
103-
[security model](https://jikkai.github.io/mdx/security/) before handling protected data.
128+
export function Page() {
129+
return (
130+
<main>
131+
<h1>{frontmatter.title}</h1>
132+
<Post />
133+
</main>
134+
)
135+
}
136+
```
104137

105-
## Documentation
138+
Or load a document from the generated registry:
139+
140+
```ts
141+
import { collections } from './.amamo-mdx/collections.mjs'
142+
143+
const hello = collections.posts.find((document) => document.slug === 'hello')
144+
const module = await hello?.load()
145+
```
146+
147+
Registry `load()` functions import the source MDX file, so they must run through the configured Vite
148+
plugin or Next loader.
106149

107-
The complete English and Simplified Chinese documentation covers:
150+
## Generated files
151+
152+
The first build writes these files under `generatedDirectory`, which defaults to `.amamo-mdx`:
153+
154+
- `collections.mjs` — sorted collection metadata and lazy source imports;
155+
- `collections.d.ts` — TypeScript declarations for the registry;
156+
- `index.json` — the source-to-cache index used by the Next loader.
157+
158+
Cache and manifest paths are configured separately from `generatedDirectory`. Add `.amamo-mdx/` to
159+
the host repository's ignore file unless the application deliberately tracks generated output.
160+
161+
## Package entry points
162+
163+
| Import | Use it for |
164+
| ----------------- | ------------------------------------------ |
165+
| `@amamo/mdx` | Configuration and the direct compiler API. |
166+
| `@amamo/mdx/vite` | Vite 8 development and production builds. |
167+
| `@amamo/mdx/next` | Next 16 development and production builds. |
168+
169+
## Documentation
108170

109171
- [Getting started](https://jikkai.github.io/mdx/getting-started/)
110172
- [Configuration reference](https://jikkai.github.io/mdx/configuration/)

apps/docs/docs/compiler-api.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ remove call rejects with `AMAMO_COMPILER_DISPOSED`.
121121

122122
Different boundaries fail differently:
123123

124-
- Invalid plain-data config throws `TypeError` with an `AMAMO_CONFIG_*` code in its message.
124+
- Invalid object schemas or non-schema configuration data throw `TypeError` with an
125+
`AMAMO_CONFIG_*` code in the message.
125126
- Native parsing, schema, media, cache, and manifest failures become an error named
126127
`AmamoMdxError` with a `diagnostics` array.
127128
- Shiki setup and unknown-language failures are ordinary `Error` values.

apps/docs/docs/compiler-api.zh-CN.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ await compiler.dispose()
118118

119119
不同边界的失败形式不同:
120120

121-
- 非法纯数据配置会抛出 `TypeError`,消息中带 `AMAMO_CONFIG_*` code。
121+
- 非法 object schema 或其它配置数据会抛出 `TypeError`,消息中带 `AMAMO_CONFIG_*` code。
122122
- 原生解析、schema、媒体、缓存和 manifest 失败会变成名为 `AmamoMdxError` 的错误,并带
123123
`diagnostics` 数组。
124124
- Shiki 初始化和未知语言失败是普通 `Error`

apps/docs/docs/configuration.mdx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ description: Exact defaults and behavior for collections, MDX, math, highlightin
44

55
# Configuration
66

7-
`defineConfig` returns its argument unchanged. `normalizeConfig` is the runtime boundary: it rejects
8-
non-plain data, applies defaults, resolves paths, and returns the `IAmamoMDXConfig` used by every
9-
compiler and adapter.
7+
`defineConfig` returns its argument unchanged. `normalizeConfig` is the runtime boundary: it converts
8+
Zod collection schemas to JSON Schema, rejects other non-plain data, applies defaults, resolves
9+
paths, and returns the `IAmamoMDXConfig` used by every compiler and adapter.
1010

1111
```ts
12-
import { defineConfig } from '@amamo/mdx'
12+
import { defineConfig, z } from '@amamo/mdx'
1313

1414
export default defineConfig({
1515
collections: {
1616
posts: {
1717
directory: 'content/posts',
18-
schema: { type: 'object' },
18+
schema: z.object({}),
1919
},
2020
},
2121
})
@@ -37,27 +37,24 @@ At least one collection is required.
3737
| `cache` | enabled | Persistent compiled records. Use `false` to disable. |
3838
| `generatedDirectory` | `.amamo-mdx` | Registry, declaration, and private Next index directory. |
3939

40-
Functions, symbols, accessors, class instances, cycles, `undefined`, `bigint`, and non-finite numbers
41-
are rejected. This check runs after your configuration module itself has been imported; it does not
42-
sandbox that module.
40+
Apart from collection Zod schemas, functions, symbols, accessors, class instances, cycles,
41+
`undefined`, `bigint`, and non-finite numbers are rejected. This check runs after your configuration
42+
module itself has been imported; it does not sandbox that module.
4343

4444
## Collections
4545

4646
```ts
47+
import { z } from '@amamo/mdx'
48+
4749
collections: {
4850
posts: {
4951
directory: 'content/posts',
5052
extensions: ['.mdx'],
5153
locales: { default: 'en', names: ['en', 'zh-CN'] },
52-
schema: {
53-
$schema: 'https://json-schema.org/draft/2020-12/schema',
54-
type: 'object',
55-
properties: {
56-
title: { type: 'string' },
57-
draft: { type: 'boolean', default: false },
58-
},
59-
required: ['title'],
60-
},
54+
schema: z.object({
55+
title: z.string(),
56+
draft: z.boolean().default(false),
57+
}),
6158
slug: { indexNames: ['index', 'page'] },
6259
},
6360
}
@@ -68,12 +65,15 @@ collections: {
6865
| `directory` | required | Resolved from `root`; the directory must exist before `build()`. |
6966
| `extensions` | `['.mdx']` | Included suffixes. Every value must begin with `.`. |
7067
| `locales` | none | Optional filename-based locale mapping. |
71-
| `schema` | required | JSON Schema Draft 2020-12 for YAML frontmatter. |
68+
| `schema` | required | Zod object schema for YAML frontmatter. |
7269
| `slug.indexNames` | `['index', 'page']` | Basenames omitted from the derived slug. |
7370

74-
Schema defaults declared under `properties`, `items`, and `allOf` are applied before validation.
75-
Values are never coerced. Validation diagnostics may contain submitted values; do not expose raw
76-
build errors to untrusted users.
71+
The package keeps Zod internal and exposes its compatible `z` builder. The config boundary is the
72+
structural `IFrontmatterSchema` interface, so another compatible object schema can also be passed.
73+
The object schema is converted to JSON Schema Draft 2020-12; raw JSON Schema objects are rejected.
74+
Rust applies `.default()` values before validation, but it does not run Zod parsing, coercion,
75+
transforms, or custom refinements. Use JSON-Schema-representable types and built-in checks. Validation
76+
diagnostics may contain submitted values; do not expose raw build errors to untrusted users.
7777

7878
When locales are configured, an unsuffixed file uses the default locale. A recognized suffix such as
7979
`page.zh-CN.mdx` selects that locale. The default locale must appear in `names`. Document identity is

apps/docs/docs/configuration.zh-CN.mdx

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ description: 集合、MDX、数学公式、高亮、媒体、派生字段、mani
44

55
# 配置
66

7-
`defineConfig` 会原样返回参数。`normalizeConfig` 才是运行时边界:它拒绝非纯数据、应用默认值、
8-
解析路径,并返回所有编译器和适配器共用的 `IAmamoMDXConfig`
7+
`defineConfig` 会原样返回参数。`normalizeConfig` 才是运行时边界:它把集合的 Zod schema 转成
8+
JSON Schema、拒绝其它非纯数据、应用默认值、解析路径,并返回所有编译器和适配器共用的
9+
`IAmamoMDXConfig`
910

1011
```ts
11-
import { defineConfig } from '@amamo/mdx'
12+
import { defineConfig, z } from '@amamo/mdx'
1213

1314
export default defineConfig({
1415
collections: {
1516
posts: {
1617
directory: 'content/posts',
17-
schema: { type: 'object' },
18+
schema: z.object({}),
1819
},
1920
},
2021
})
@@ -36,40 +37,40 @@ export default defineConfig({
3637
| `cache` | 启用 | 持久化编译记录;设为 `false` 可禁用。 |
3738
| `generatedDirectory` | `.amamo-mdx` | 注册表、声明文件和 Next 私有索引目录。 |
3839

39-
函数、symbol、访问器、类实例、循环引用、`undefined``bigint` 和非有限数字都会被拒绝。检查
40-
发生在配置模块本身已经 import 之后,因此不会沙箱化那个模块。
40+
除集合的 Zod schema 外,函数、symbol、访问器、类实例、循环引用、`undefined``bigint`
41+
非有限数字都会被拒绝。检查发生在配置模块本身已经 import 之后,因此不会沙箱化那个模块。
4142

4243
## 集合
4344

4445
```ts
46+
import { z } from '@amamo/mdx'
47+
4548
collections: {
4649
posts: {
4750
directory: 'content/posts',
4851
extensions: ['.mdx'],
4952
locales: { default: 'en', names: ['en', 'zh-CN'] },
50-
schema: {
51-
$schema: 'https://json-schema.org/draft/2020-12/schema',
52-
type: 'object',
53-
properties: {
54-
title: { type: 'string' },
55-
draft: { type: 'boolean', default: false },
56-
},
57-
required: ['title'],
58-
},
53+
schema: z.object({
54+
title: z.string(),
55+
draft: z.boolean().default(false),
56+
}),
5957
slug: { indexNames: ['index', 'page'] },
6058
},
6159
}
6260
```
6361

64-
|| 默认值 | 行为 |
65-
| ----------------- | ------------------- | ---------------------------------------------------- |
66-
| `directory` | 必填 |`root` 解析;调用 `build()` 前目录必须存在。 |
67-
| `extensions` | `['.mdx']` | 纳入集合的后缀;每项必须以 `.` 开头。 |
68-
| `locales` || 可选的基于文件名的 locale 映射。 |
69-
| `schema` | 必填 | 校验 YAML frontmatter 的 JSON Schema Draft 2020-12。 |
70-
| `slug.indexNames` | `['index', 'page']` | 推导 slug 时省略的 basename。 |
71-
72-
校验前会应用 `properties``items``allOf` 中声明的 schema 默认值,但绝不做类型转换。
62+
|| 默认值 | 行为 |
63+
| ----------------- | ------------------- | ----------------------------------------------- |
64+
| `directory` | 必填 |`root` 解析;调用 `build()` 前目录必须存在。 |
65+
| `extensions` | `['.mdx']` | 纳入集合的后缀;每项必须以 `.` 开头。 |
66+
| `locales` || 可选的基于文件名的 locale 映射。 |
67+
| `schema` | 必填 | 校验 YAML frontmatter 的 Zod object schema。 |
68+
| `slug.indexNames` | `['index', 'page']` | 推导 slug 时省略的 basename。 |
69+
70+
包内维护 Zod,并对外提供兼容的 `z` builder。配置边界是结构化的 `IFrontmatterSchema` 接口,
71+
因此也可以传入其它兼容的 object schema。Object schema 会转换为 JSON Schema Draft 2020-12;
72+
原始 JSON Schema 对象会被拒绝。Rust 会在校验前应用 `.default()` 值,但不会执行 Zod parse、
73+
类型转换、transform 或自定义 refinement。请只使用可表示为 JSON Schema 的类型和内置检查。
7374
诊断可能包含用户提交的值;不要把原始构建错误暴露给不可信用户。
7475

7576
配置 locale 后,无后缀文件使用默认 locale;`page.zh-CN.mdx` 这样的已知后缀会选择对应 locale。

0 commit comments

Comments
 (0)