From 91fc153a7359485d1c4baf1131abf02625c91a34 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Tue, 2 Jun 2026 05:11:45 +0800 Subject: [PATCH 1/2] fix(spec): escape unmatched `<`/`{` in generated MDX docs (unblock apps/docs build) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `build-docs.ts`'s `escapeMdxDescription` only backtick-wrapped *matched* `<…>` / `{…}` pairs. A lone `<` with no closing partner — e.g. a SemVer range in a `.describe()` like `">=4.0 <5"` — leaked into the generated MDX, where Turbopack/fumadocs read the `<` as the start of a JSX tag and failed the docs build: ./content/docs/references/kernel/manifest.mdx 82:97: Unexpected character `5` (U+0035) before name Now an unmatched `<` / `{` is replaced with its HTML entity (`<` / `{`) so it renders literally and never opens a JSX/expression node. Also routes union-variant descriptions through the same escaper (previously emitted raw). Verified: `pnpm --filter @objectstack/docs build` (gen:schema + gen:docs + next build) now completes; no remaining unmatched `<` outside code spans across the generated references. Co-Authored-By: Claude Opus 4.8 --- packages/spec/scripts/build-docs.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/spec/scripts/build-docs.ts b/packages/spec/scripts/build-docs.ts index 61e484d91..ed532d056 100644 --- a/packages/spec/scripts/build-docs.ts +++ b/packages/spec/scripts/build-docs.ts @@ -167,6 +167,12 @@ function generateMarkdown(schemaName: string, schema: any, category: string, zod // span are left untouched. A naive two-pass replace double-wraps nested cases // like `{}` into `` `{``}` `` — the inner backticks close the span // early and leak `` as raw JSX (MDX: "Expected a closing tag for ``"). + // + // A matched `{…}` / `<…>` pair is wrapped in an inline-code span so it renders + // literally. A *lone* `<` or `{` with no closing partner (e.g. a SemVer range + // `">=4.0 <5"`, or prose like `count < 5`) can't be wrapped, so it is replaced + // with its HTML entity — otherwise MDX reads the `<` as the start of a JSX tag + // and the build dies ("Unexpected character `5` before name"). const escapeMdxDescription = (raw: string): string => { let out = ''; let inCode = false; @@ -185,6 +191,9 @@ function generateMarkdown(schemaName: string, schema: any, category: string, zod i = end; continue; } + // Unmatched: escape so MDX doesn't treat it as a JSX/expression opener. + out += ch === '<' ? '<' : '{'; + continue; } out += ch; } @@ -223,7 +232,7 @@ function generateMarkdown(schemaName: string, schema: any, category: string, zod variants.forEach((variant: any, index: number) => { const variantTitle = variant.title || `Option ${index + 1}`; md += `#### ${variantTitle}\n\n`; - if (variant.description) md += `${variant.description}\n\n`; + if (variant.description) md += `${escapeMdxDescription(variant.description)}\n\n`; if (variant.type === 'object' && variant.properties) { if (variant.properties.type && variant.properties.type.const) { From 3bdfb54b0bc7235f7c61d8fed1351c404682d74a Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Tue, 2 Jun 2026 05:11:57 +0800 Subject: [PATCH 2/2] chore: changeset for docs MDX escape fix Co-Authored-By: Claude Opus 4.8 --- .changeset/fix-docs-mdx-escape.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-docs-mdx-escape.md diff --git a/.changeset/fix-docs-mdx-escape.md b/.changeset/fix-docs-mdx-escape.md new file mode 100644 index 000000000..82dab30b3 --- /dev/null +++ b/.changeset/fix-docs-mdx-escape.md @@ -0,0 +1,5 @@ +--- +"@objectstack/spec": patch +--- + +Fix the docs generator (`build-docs.ts`) leaking an unmatched `<` / `{` into generated MDX, which broke the `apps/docs` Turbopack build (e.g. a SemVer range `">=4.0 <5"` in a `.describe()` string was read as the start of a JSX tag). Unmatched openers are now emitted as HTML entities (`<` / `{`); union-variant descriptions also go through the escaper.