feat(runtime): author rich Markdown blocks as JSX via rsc-markdown-stream - #344
Conversation
…ream Re-export renderToMarkdown/renderToMarkdownStream and add the async MarkdownContent component, which renders JSX children (GFM tables, task lists, nested async components, escaped text) to one Markdown string lowered into Agent.Markdown. The audiobook-curator example authors its DataList/FileList primitives as JSX with byte-identical output and adds a JSX-authored measured-files table to the duplicate analysis.
🦋 Changeset detectedLatest commit: 3e4b52d The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
commit: |
|
REVIEW ONLY — do not merge. This PR is intentionally experimental and stays open by owner decision; nothing below is a merge signal. Findings from a full review of the diff (merge-sweep lane, reviewed against the runtime's element/lowering architecture): Sound:
Worth watching before any future merge:
Local gates were started but intentionally aborted when the PR was marked experimental; no gate verdict is claimed here. |
# Conflicts: # packages/rsc-runtime/package.json
…to end Bumps the commit pin to eba2ea0, which fixes output a GFM parser read differently from the authored tree (edge <br> as a literal backslash, sibling lists merging, entity-like text decoding, task checkboxes glued to labels, trailing # in headings, dl/dt/dd gluing, dropped captions, leaked style/script bodies, em-in-em becoming strong, Activity throwing). Tests now describe what a reader sees rather than which characters were emitted: the MarkdownContent suite renders every supported element and round-trips the result through micromark + GFM (the parser under the Workbench's react-markdown/remark-gfm), and a new integration test runs MarkdownContent through a real react-server Flight render of the built package, decoding the wire back into an Agent Document.
`rsc-markdown-stream` is not published to npm, and npm 12 refuses git dependencies by default (`allow-git=none` -> EALLOWGIT), so a git-pinned entry in `@agent-bundle/runtime`'s `dependencies` would fail every consumer's install -- the AB7015 class the prepack gate rejects. Move the pin to `devDependencies` so Rslib's `autoExternal` inlines it into `dist/index.js` (react stays external), and declare the public Markdown types in `markdown-content.ts` so no emitted `.d.ts` imports a module consumers never install; the typed assignments of `renderToMarkdown` / `renderToMarkdownStream` stop compiling if the pinned upstream drifts. The changeset is now a `patch` (pre-1.0: features are patches) and ends with the PR reference.
Add "Rich Markdown blocks as JSX" to the MCP authoring guide (en + zh): what `MarkdownContent` serializes, escaping, the no-trailing-newline contract, custom `components` serializers, and the exported `renderToMarkdown` / `renderToMarkdownStream`. The audiobook-curator example page notes that its report primitives author Markdown this way.
Copy ScriptedAlchemy/rsc-markdown-stream at eba2ea0b930493b80b9f4f9bb2c582041b0a3f47 into packages/rsc-markdown-stream: the upstream ESM sources and hand-written declarations under src/, built with Rslib to dist/ like the other publishable packages, with the node:test suite ported to rstest (57 tests; the react-server child-process test runs in the integration pool). The package is unpublished on npm, so it is a publishable package of this repository: Apache-2.0 like its siblings, the upstream MIT notice preserved as UPSTREAM-LICENSE and listed in NOTICE. @agent-bundle/runtime depends on it as ^0.1.0, satisfied from the workspace through the pnpm override (a published manifest never carries workspace:), and its re-exports resolve to the package again. Root build, typecheck, preview:publish, and the license sync enumerate the new package.
declaredDependencyRoots whitelisted only the project's direct dependencies' real paths. A workspace package linked into the project that itself links another workspace package (@agent-bundle/runtime -> rsc-markdown-stream) surfaced that second package's modules at a real path with no node_modules segment, and `agent-bundle build` rejected them as sources outside the output root (AB5000). Walk each linked root's runtime dependencies too; registry packages resolve beneath node_modules and are never walked.
The runtime tarball now depends on rsc-markdown-stream by version, which the registry cannot serve until this repository publishes it. Pack it with the other run-level tarballs and offer it in the same npm install as the runtime tarball (npm dedupes the runtime's edge onto it); scaffolded projects take it with --no-save so their manifests stay exactly as the scaffolder wrote them.
# Conflicts: # website/docs/en/guide/authoring/mcp.mdx # website/docs/zh/guide/authoring/mcp.mdx
The docs build compiles packages/rsc-runtime/src without pnpm build, and the renderer publishes its types from dist, so TypeDoc could not find the module in CI. Map it to the hand-written src/index.d.ts like the runtime entries.
…e the project root The provenance walk probed only <packageRoot>/node_modules/<name>, so a transitive workspace dependency hoisted to an ancestor node_modules (npm, Yarn, pnpm with a hoist pattern) was missed and AB5000 fired. It also added a resolved root before checking whether it was the project itself, so a dependency cycle back onto the project (A -> B -> A) silently dropped every authored module from sourceInputs. Resolve each dependency's manifest with createRequire(...).resolve(), fall back to an ancestor node_modules walk when an exports map hides package.json, canonicalize the project root once, and never register it or anything inside it as an ignored root.
isInsideOrEqual(projectRoot, root) also rejected every linked dependency that lives beneath the project (a root package linking <project>/packages/dep, or file:./vendor/dep), so those dependency files stayed attributed as project sources. The contract is equality: compare the realpath'ed dependency root with the canonical project root, so nested roots are excluded again while the project root itself never is. docs/preview-packages.md and the website preview pages now count four publishable packages, show the rsc-markdown-stream install URL, document the full preview:publish command, and state that pkg-pr-new rewrites the runtime's rsc-markdown-stream dependency to the same-sha preview tarball.
Summary
packages/rsc-markdown-stream, copied from upstream commiteba2ea0b930493b80b9f4f9bb2c582041b0a3f47: the upstream ESM sources and hand-written declarations undersrc/, built with Rslib todist/like the other publishable packages, thenode:testsuite ported to rstest (57/57 tests kept; the react-server child-process test runs in the integration pool). The name is unpublished on npm, so it is a publishable package of this repo (Apache-2.0 like its siblings,publishConfig.access: public; the upstream MIT notice is preserved asUPSTREAM-LICENSEand listed inNOTICE).@agent-bundle/runtimedepends on it as"rsc-markdown-stream": "^0.1.0"— satisfied from the workspace through a pnpmoverridesentry, the same formagent-bundleuses for its runtime peer, so the packed manifest carries a plain semver range and noworkspace:or git specifier — and re-exportsrenderToMarkdown/renderToMarkdownStreamand the asyncMarkdownContentcomponent, which renders JSX children — headings, lists, GFM tables, task lists, nested async components, escaped text — to one Markdown string lowered intoAgent.Markdown. Rootbuild/typecheck/preview:publish, the license sync,AGENTS.md, and the changeset README enumerate the new package.agent-bundle build: bundle provenance whitelisted only the project's direct dependencies' real paths, so a workspace package linked into a project that itself links another workspace package (@agent-bundle/runtime→rsc-markdown-stream) failed withAB5000("outside output root").declaredDependencyRootsnow follows linked roots' runtime dependencies transitively (registry packages resolve beneathnode_modulesand are never walked); regression test inbuild.test.tsreproduces the failure on the old code.test:packed): the runtime tarball now depends onrsc-markdown-streamby version, which the registry cannot serve until this repo publishes it, so the shared pack also packsrsc-markdown-streamand offers it in the samenpm installas the runtime tarball (npm dedupes the runtime's edge onto it); scaffolded projects take it with--no-saveso their manifests stay exactly as the scaffolder wrote them.DataList/FileListprimitives as JSX (byte-identical output — the exact-document pin incli-dispatch.test.tspasses unchanged) and adds a JSX-authored measured-files GFM table to the duplicate analysis, something the hand-concatenated string approach never supported.755c5ee): the react-server condition's React build only exports__SERVER_INTERNALS_*, and rspack's strict ESM linking failed the artifact flight build on the static client-internals namespace access. The internals slot is now located with computed access across client / react-server / legacy keys, with a child-process regression test under--conditions react-server(nowpackages/rsc-markdown-stream/tests/react-server.test.ts).eba2ea0(the vendored commit). Each case was confirmed by rendering the Markdown back through micromark + GFM before the fix:<br>at a paragraph edge emitted a literal trailing backslash; entity-looking text (<,©) was decoded by parsers; two sibling<ul>s (or<ol>s) merged into one loose list; a task checkbox glued to its label wasn't a task item;<ol start={0}>started at 1; a heading ending in#lost it to the ATX closing-sequence rule;<dl>/<dt>/<dd>glued into one paragraph;<caption>was dropped;<style>/<script>/head metadata leaked as paragraphs;<em>inside<em>became strong;<Activity>/<ViewTransition>threw. Intraword_(snake_case) is no longer escaped since it can never open emphasis.MarkdownContent/renderToMarkdown/renderToMarkdownStreamdocumented in the MCP authoring guide and the audiobook-curator example page (en + zh); the preview-packages page lists the new package.website/tsconfig.typedoc.jsonmapsrsc-markdown-streamto its source declarations, since the docs job compiles the runtime sources withoutpnpm build(this was the one red check on the first push).Publishing note
rsc-markdown-stream@0.1.0does not exist on npm yet. Until the first release from this repo publishes it, installing an@agent-bundle/runtimetarball outside this workspace resolvesrsc-markdown-stream@^0.1.0from the registry and 404s — which is why the packed proofs pack it alongside. pkg.pr.new previews are unaffected (preview:publishnow includes the package and rewrites the range to the paired preview).Piped CLI output (real artifact)
Test plan
packages/rsc-markdown-stream/tests/**: the upstream suite ported to rstest — 57/57 (render, components, fidelity, robustness in the unit pool;react-server.test.tsspawns Node with--conditions react-serverin the integration pool);tests/types.tscompile-only check under the packagetypecheckpackages/rsc-runtime/tests/markdown-content.test.ts: renders every supported element (h1–h6, emphasis/strike/code/link/image, nested + ordered + task lists, fenced code, blockquote, aligned table with caption and escaped pipes, hr, container passthrough) throughMarkdownContent, pins the exact Markdown, and round-trips it through micromark + GFM (the parser under the Workbench'sreact-markdown/remark-gfm) asserting the parsed structure; hostile text (*stars*,[x],<tag>,&,~~, line-start-/#/1./>/---) parses back as literal text; sibling lists stay separate, edge<br>drops, trailing#survives; async components +React.use; custom serializers; streaming parity — 10/10packages/rsc-runtime/tests/markdown-content-flight.test.ts(integration pool): spawns the built package under--conditions=react-server, rendersMarkdownContent(with async row components) inside a real Flight request withrsc-markdown-streamresolved as an external dependency, decodes the wire into an Agent Document and asserts the loweredmarkdownnode textpackages/agent-bundle/tests/build.test.ts› "bundles symlinked workspace dependencies, transitively, without attributing them as project sources", "excludes a transitive workspace dependency hoisted to an ancestor node_modules", "keeps the project sources when a linked dependency depends back on the project", "excludes linked dependencies that live inside the project directory": the first two fail on the previousdeclaredDependencyRootswith the exactAB5000message, the third silently losessrc/local.tsfromsourceInputs, and the fourth (on c599f78) wrongly reportspackages/linked-a/index.jsandpackages/linked-b/index.jsas project sources; all pass with the fix (30/30 in the file)scripts/run-packed-tests.mjs:packed-stdio-projection.test.ts,scaffold-packed.e2e.test.ts, and (--release)scaffold-packed-matrix.e2e.test.ts— 5/5, installing the runtime tarball together with thersc-markdown-streamtarballpnpm pack/npm packofpackages/rsc-runtime: packeddependenciesshow"rsc-markdown-stream": "^0.1.0"— noworkspace:, no git URL;pnpm-lock.yamlhas nogithub.com/...rsc-markdown-streamtarball entry (link:../rsc-markdown-streamonly)examples/audiobook-curatorpnpm check: validate,build --output artifact(flight bundle compiles the workspace renderer under the react-server condition), typecheck, 40 unit + 31 route-unit tests including the byte-exact piped-Markdown pin and the GFM table assertionspnpm build(four publint passes,rsc-markdown-streamfirst),pnpm typecheck(now includes the new package's tsconfig),pnpm lint,pnpm test:unit(238 files, 3420 passed),pnpm docs:site:build(language parity clean); branch merged up toorigin/mainSelf-review
Reviewer:
gpt-5.6-sol-medium(change-risk-reviewer), against the diff vsorigin/main.declaredDependencyRootsprobed only<packageRoot>/node_modules/<name>, so a dependency B hoisted to an ancestornode_modulesby npm/Yarn/hoisting pnpm was resolved by Node and Rspack but skipped by the walk, andAB5000fired again): fixed in c599f78 — each dependency's manifest is now resolved withcreateRequire(join(packageRoot, 'package.json')).resolve(${name}/package.json), with an ancestornode_moduleswalk (up to the filesystem root) as the fallback when anexportsmap hidespackage.json; the resolved package directory is thenrealpathed. Regression test:build.test.ts› "excludes a transitive workspace dependency hoisted to an ancestor node_modules".roots, andcollectBundledOutputEvidencethen dropped every authored module from provenance without an error): fixed in c599f78 — the project root is canonicalized once and neither it nor any path inside it is ever inserted intoroots; the visited set still bounds recursion. Regression test:build.test.ts› "keeps the project sources when a linked dependency depends back on the project" (asserts the implicitsrc/local.tsimport stays insourceInputs).rsc-markdown-streampublishes before@agent-bundle/runtimeconsumes^0.1.0);bumpVersionsWithWorkspaceProtocolOnlywith the^0.1.0range; pkg.pr.new preview;react-serverexport maps; vendored layout assumptions; docs/changeset; Apache/MITNOTICEhandling.Second pass (reviewer
gpt-5.6-sol-medium,change-risk-reviewer, against c599f78):isInsideOrEqual(projectRoot, root)rejected not only the project root but every linked dependency resolved to a directory beneath it (a root package linking<project>/packages/dep, orfile:./vendor/dep), so those dependency modules stayed attributed as projectsourceInputs. Fixed in 3ed48c6 — the check is now canonical-path equality,root === projectRoot, both sidesrealpathed (the project root keeps its ENOENT fallback); nested roots are inserted again, the project root itself never is. Regression test:build.test.ts› "excludes linked dependencies that live inside the project directory" (fails on c599f78 with the two nested dependency files present insourceInputs).docs/preview-packages.mdstill said three packages: fixed in 3ed48c6 — the count is four, both install blocks show thersc-markdown-streamURL, the documentedpreview:publishcommand lists./packages/rsc-markdown-stream, and the text records that pkg-pr-new rewrites@agent-bundle/runtime'srsc-markdown-streamdependency to the same-sha preview tarball (pkg-pr-newhijackDepsrewrites anydependenciesentry naming a co-published package). The workflow needed no change:.github/workflows/package-preview.ymlrunspnpm preview:publish, whose script already included the renderer.website/docs/{en,zh}/guide/distribution/preview-packages.mdxhad the same "three" and were corrected together;pnpm docs:site:buildpasses with language parity.