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. 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) {