Skip to content

fix(npm): make ext-content-mdx an optional peer, not a runtime dependency - #3783

Merged
kwakayama merged 5 commits into
mainfrom
fix/issue-3725
Aug 16, 2026
Merged

kwakayama merged 5 commits into
mainfrom
fix/issue-3725

Conversation

@kwakayama

@kwakayama kwakayama commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

fix(npm): make ext-content-mdx an optional peer, not a runtime dependency

npm install veryfront broke a previously-clean tsc --noEmit in any
consumer project without skipLibCheck. The root package listed
@veryfront/ext-content-mdx under runtime dependencies, which drags
@mdx-js/mdx -> @types/mdx@2.0.14. That file references the global JSX
namespace @types/react@19 no longer declares, and tsc auto-includes
everything under node_modules/@types, so four TS2503 errors appeared in a
package the developer never imported.

The issue's suggested optionalDependencies does not fix it: npm installs
optional dependencies by default and only tolerates their installation
failure. Verified with npm 11.12.1 in the published package shape --
optionalDependencies installs 117 packages, keeps node_modules/@types/mdx
and still exits 2; an optional peer installs 4 packages, has no @types/mdx
and exits 0. So the move lands on peerDependencies +
peerDependenciesMeta.optional, reusing the mechanism
ROOT_OPTIONAL_RUNTIME_PEERS already uses.

Three consequences handled:

  • Server startup calls ensureBuiltinContentProcessor unconditionally, so
    a missing package there would break npx veryfront dev for every
    project, including ones with no .mdx file. It now tolerates a
    missing-module failure and leaves the contract unregistered, deferring
    the report to the compile path, which already throws the typed
    MISSING_EXTENSION_ERROR naming @veryfront/ext-content-mdx. A real load
    failure inside an installed extension still propagates.

  • The minimal starter ships app/about/page.mdx, so it now declares the
    extension via firstPartyExtensions like docs-agent does for
    ext-document-kreuzberg.

  • update_package_version only rewrote first-party extension pins under
    dependencies. An RC publish would have left the optional peer pointing
    at a version that was never published; it now covers optionalDependencies
    and peerDependencies too.

The compiled binary is unchanged: compile-binary.ts embeds
extensions/ext-content-mdx/src/index.ts as a compile-time include, with no
reference to npm dependency metadata.

Refs #3725


The headline: the issue's own suggested fix does not work

#3725 proposes optionalDependencies as the first step. That was measured and it does not solve the problem. npm installs optional dependencies by default — optional means "tolerate installation failure", not "skip". Measured with npm 11.12.1 against the published package shape:

packages installed node_modules/@types/mdx tsc --noEmit
optionalDependencies 117 present exit 2
optional peer 4 absent exit 0

So the change lands on peerDependencies + peerDependenciesMeta.optional, reusing the ROOT_OPTIONAL_RUNTIME_PEERS mechanism the repo already has. Worth correcting in the issue as well as here.

Three consequences that fall out of it

These are the parts most likely to bite, and each is handled:

  1. Server startup calls ensureBuiltinContentProcessor unconditionally. A missing package would have broken npx veryfront dev for every project, including ones with no .mdx at all. It now tolerates a missing-module failure and defers to the compile path, which already throws the typed MISSING_EXTENSION_ERROR naming the package. A real load failure inside an installed extension still propagates — the tolerance is narrow.
  2. The minimal starter ships app/about/page.mdx, so it declares the extension via firstPartyExtensions, matching what docs-agent already does for ext-document-kreuzberg.
  3. update_package_version only rewrote first-party pins under dependencies. An RC publish would have shipped an optional peer pointing at a version that was never published. Now covers optionalDependencies and peerDependencies too. This one would have been a silent publish break.

Acceptance against the issue

  • A plain strict TS project typechecks clean after npm install veryfront with no skipLibCheck — measured, exit 0
  • npx veryfront dev still renders .mdx, or fails with an actionable install message — the typed MISSING_EXTENSION_ERROR names the package
  • Compiled binary unchanged — compile-binary.ts embeds extensions/ext-content-mdx/src/index.ts as a compile-time include and never reads npm dependency metadata
  • Regression test asserting a bare consumer typechecks
  • docs/getting-started/add-to-existing-project.md updated to drop the skipLibCheck requirement

Closes #3725

Summary by CodeRabbit

  • New Features

    • Added automatic MDX extension setup for generated projects and templates containing MDX files.
    • Added warnings when MDX files are generated without the required extension installed.
    • Improved handling of optional MDX support during content compilation.
  • Documentation

    • Clarified TypeScript configuration guidance and MDX installation behavior.
    • Updated scaffold API reference links.
  • Bug Fixes

    • Improved package metadata and version handling for first-party extensions.

…ency

`npm install veryfront` broke a previously-clean `tsc --noEmit` in any
consumer project without skipLibCheck. The root package listed
@veryfront/ext-content-mdx under runtime `dependencies`, which drags
@mdx-js/mdx -> @types/mdx@2.0.14. That file references the *global* JSX
namespace @types/react@19 no longer declares, and tsc auto-includes
everything under node_modules/@types, so four TS2503 errors appeared in a
package the developer never imported.

The issue's suggested `optionalDependencies` does not fix it: npm installs
optional dependencies by default and only tolerates their installation
*failure*. Verified with npm 11.12.1 in the published package shape --
optionalDependencies installs 117 packages, keeps node_modules/@types/mdx
and still exits 2; an optional peer installs 4 packages, has no @types/mdx
and exits 0. So the move lands on peerDependencies +
peerDependenciesMeta.optional, reusing the mechanism
ROOT_OPTIONAL_RUNTIME_PEERS already uses.

Three consequences handled:

- Server startup calls `ensureBuiltinContentProcessor` unconditionally, so
  a missing package there would break `npx veryfront dev` for every
  project, including ones with no .mdx file. It now tolerates a
  missing-module failure and leaves the contract unregistered, deferring
  the report to the compile path, which already throws the typed
  MISSING_EXTENSION_ERROR naming @veryfront/ext-content-mdx. A real load
  failure inside an installed extension still propagates.

- The `minimal` starter ships app/about/page.mdx, so it now declares the
  extension via `firstPartyExtensions` like docs-agent does for
  ext-document-kreuzberg.

- `update_package_version` only rewrote first-party extension pins under
  `dependencies`. An RC publish would have left the optional peer pointing
  at a version that was never published; it now covers optionalDependencies
  and peerDependencies too.

The compiled binary is unchanged: compile-binary.ts embeds
extensions/ext-content-mdx/src/index.ts as a compile-time include, with no
reference to npm dependency metadata.

Refs #3725
@coderabbitai

coderabbitai Bot commented Aug 16, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kwakayama, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Limit details: You’ve used all 3 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a1d7c245-122b-4d71-b770-4c02a3da8258

📥 Commits

Reviewing files that changed from the base of the PR and between cfc2807 and 0bea8bd.

📒 Files selected for processing (1)
  • cli/commands/generate/command.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 43fa6d25-56be-415b-8bfa-e19920ae4464

📥 Commits

Reviewing files that changed from the base of the PR and between 7b42a75 and cfc2807.

📒 Files selected for processing (12)
  • cli/commands/generate/command.ts
  • cli/shared/ensure-content-processor.test.ts
  • cli/shared/ensure-content-processor.ts
  • cli/shared/project-creation.test.ts
  • cli/shared/project-creation.ts
  • docs/api-reference/veryfront/scaffold.md
  • docs/getting-started/add-to-existing-project.md
  • scripts/build/npm-package-metadata.test.ts
  • scripts/build/npm-package-metadata.ts
  • scripts/ci/publish-npm-packages.sh
  • templates/index.test.ts
  • templates/index.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The CLI now treats the MDX extension as optional, declares it in MDX scaffolds, normalizes it as an optional peer during publishing, and documents the related installation and TypeScript behavior. Tests cover loading, scaffolding, templates, and package metadata.

Changes

MDX extension integration

Layer / File(s) Summary
Optional MDX loading and guidance
cli/shared/ensure-content-processor.ts, cli/shared/ensure-content-processor.test.ts, cli/commands/generate/command.ts, docs/getting-started/add-to-existing-project.md
Missing optional extension errors are deferred, unrelated loader errors are rethrown, generated MDX projects receive an installation warning, and MDX TypeScript guidance is documented.
Scaffold MDX declarations
cli/shared/project-creation.ts, cli/shared/project-creation.test.ts, templates/index.ts, templates/index.test.ts, docs/api-reference/veryfront/scaffold.md
Scaffolds add @veryfront/ext-content-mdx when MDX features or files are present. Templates and source references are updated and tested.
Package publish normalization
scripts/build/npm-package-metadata.ts, scripts/build/npm-package-metadata.test.ts, scripts/ci/publish-npm-packages.sh
The MDX extension is normalized as an optional peer, and first-party extension versions are updated across dependency sections.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to cfc28

This change makes the MDX extension optional while preserving explicit setup for projects that use it and updates related scaffolding and publishing behavior; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant generateCommand
  participant packageJson
  participant ensureBuiltinContentProcessor
  participant MdxContentProcessor
  generateCommand->>packageJson: Check generated .mdx files and extension declarations
  generateCommand->>ensureBuiltinContentProcessor: Load optional MDX extension
  ensureBuiltinContentProcessor->>MdxContentProcessor: Register processor when loading succeeds
  ensureBuiltinContentProcessor-->>generateCommand: Defer missing-extension failure to compilation
Loading

Possibly related PRs

Suggested reviewers: kojiwakayama

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The changes address the optional peer, startup fallback, actionable MDX error, binary behavior, declarations, versioning, and documentation, but no bare consumer typecheck regression test is shown [#3725]. Add a regression test that installs the package in a bare strict TypeScript project and runs tsc --noEmit without skipLibCheck.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: making ext-content-mdx an optional peer dependency instead of a runtime dependency.
Out of Scope Changes check ✅ Passed The template, generation, publishing, documentation, and test changes directly support the linked issue objectives and the dependency migration.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-3725

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

📦 Client bundle boundary

Entrypoint Modules Source size Server leaks
src/index.client.ts 321 1908 KiB ✅ 0

A server module in a client graph aborts hydration in the browser. New leaks fail CI; known leaks are tracked in scripts/lint/client-bundle-baseline.json to burn down.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c915dcb7d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread templates/index.ts
Comment on lines +66 to +68
minimal: {
firstPartyExtensions: ["@veryfront/ext-content-mdx"],
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install the optional peer when adding the MDX feature

When callers use materializeScaffold({ template: "ai-agent", features: ["mdx"] }) or otherwise combine the MDX feature with any non-minimal starter, assembleFeatureFiles adds the feature's .mdx routes after this starter-only check, while packageJsonOptions.firstPartyExtensions still comes solely from the base template. The generated project therefore omits @veryfront/ext-content-mdx, and its newly scaffolded MDX routes fail at runtime. Derive the required extension from the assembled feature files or propagate it from the MDX feature configuration rather than covering only starter-owned files.

Useful? React with 👍 / 👎.

try {
module = await load();
} catch (error) {
if (isMissingFirstPartyExtensionModule(error, CONTENT_MDX_SPECIFIERS)) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the MDX peer when the generator creates MDX files

In an npm project configured for the pages router, veryfront generate page ... and veryfront generate layout ... still create .mdx files in cli/scaffold/engine.ts, but the generate command neither adds @veryfront/ext-content-mdx to package.json nor tells the user to install it. After this missing-package path returns successfully, veryfront dev reports the generated route as unusable even though the command reported that it was created successfully. Update the MDX-generating flow to install or declare the newly optional peer.

Useful? React with 👍 / 👎.

… template

Addresses the first review finding on #3783, though not by the route it
described.

The finding claimed the `mdx` feature adds `.mdx` routes on top of any
template, leaving them undeclared. Checked, and that mechanism does not
exist: `templates/features/mdx/feature.json` has no `files` key, so the
feature scaffolds nothing. `materializeScaffold({template:"ai-agent",
features:["mdx"]})` yields 15 files and zero `.mdx`.

The gap underneath it is real, though. The feature sets `mdx.enabled` in the
generated config and tips the user to "Create .mdx files in app/ directory".
Since the extension became an optional peer, a user following that tip on any
template gets a runtime failure with nothing in package.json to explain it.

`firstPartyExtensions` now comes from the assembled project rather than the
template config alone, covering both routes into MDX:

- a scaffolded `.mdx` file, which is how `minimal` needs it, and
- the `mdx` feature being selected, which scaffolds no files at all.

This also subsumes the `minimal` special case: the starter is covered because
it ships `app/about/page.mdx`, not because it is named in a table.

Two tests, one per direction — the feature selected on a template with no
`.mdx`, and a template with neither, which must stay undeclared so the fix
cannot over-reach. The positive test also asserts the feature ships zero files,
so it fails loudly if that ever changes rather than passing for a new reason.

Separately noted, not fixed here: `templates/features/mdx/files/app/docs/*.mdx`
exists on disk but is never scaffolded, because the loader goes by
`feature.json`. Either dead fixtures or a feature-loader bug, and outside this
PR's scope.
…xtension

Second review finding on #3783, and reproducible: under the pages router,
`veryfront generate page` writes `pages/<slug>.mdx` and `generate layout`
writes `layouts/<name>.mdx` (cli/scaffold/engine.ts). Now that
@veryfront/ext-content-mdx is an optional peer, a project that never installed
it renders those routes as an error — while the command has just printed
"Created <path>" and exited 0.

The compile path already throws a typed MISSING_EXTENSION_ERROR naming the
package, but by then the developer is debugging a route they were told was
fine. The warning moves that information to the moment the file is created.

Scoped narrowly: it fires only when an `.mdx` file was actually written and
the package is absent from all four dependency groups. A project with no
readable package.json — a Deno project, say — gets no warning rather than a
false one, since the optional-peer problem is npm-specific.

The pages router is opt-in via `generate.preferredRouter` or `router` in the
config, so this is a narrow path; it is also the only one where the CLI itself
creates MDX the user did not ask for by name.
… change

`ci (lint)` gates on the generated API reference being current, and
`withMdxExtension` moved documented surface in the scaffold module.
@kwakayama

Copy link
Copy Markdown
Contributor Author

@codex review
@coderabbitai review

Head is now cfc2807. Since your last pass: both of your P1s are addressed — the MDX extension is now derived from the assembled project rather than the template table, and generate warns when it writes .mdx into a project without the package. Note your first P1's stated mechanism did not reproduce: the mdx feature has no files key and scaffolds zero files. The underlying gap was real and is fixed. Docs regenerated.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026 •

Copy link
Copy Markdown

@kwakayama I will review the updated head. I will verify the corrected MDX extension detection, the generate warning path, and the regenerated documentation.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cfc2807df3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/commands/generate/command.ts Outdated
].some((group) => group?.["@veryfront/ext-content-mdx"] !== undefined);
if (declared) return;
cliLogger.warn(
"This project does not depend on @veryfront/ext-content-mdx, so the generated .mdx file will not render. Install it with: npm install @veryfront/ext-content-mdx",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the project's package manager in the install warning

When generate creates an MDX route in a pnpm, Yarn, or Bun project, this warning instructs the user to run npm install, which can create a conflicting package-lock.json while leaving the project's real lockfile stale. The repository already provides detectProjectInstallTarget(projectDir) and formatInstallCommand() in src/extensions/install-command.ts specifically to produce lockfile-aware extension install instructions, so use that path here instead of hard-coding npm.

AGENTS.md reference: AGENTS.md:L9-L13

Useful? React with 👍 / 👎.

Review finding on the head commit, and correct. The warning hard-coded
`npm install`, which in a pnpm, Yarn or Bun project writes a competing
package-lock.json and leaves the project's real lockfile stale.

`detectProjectInstallTarget(projectDir)` and `formatInstallCommand()` in
src/extensions/install-command.ts already exist for exactly this — the former
reads the lockfile in the directory, falls back to `deno.json`, and searches
ancestors for a workspace lockfile before assuming npm. The warning now routes
through them instead of guessing.

Also replaced the two remaining literal package names with the existing
MDX_EXTENSION_PACKAGE constant so the check and the message cannot drift.
@kwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

Head is 0bea8bd (last pass was cfc2807). Your install-command finding is addressed: the warning now routes through detectProjectInstallTarget(projectDir) and formatInstallCommand() from src/extensions/install-command.ts instead of hard-coding npm install, so a pnpm/yarn/bun project no longer gets told to write a competing lockfile. Also replaced the remaining literal package names with the existing MDX_EXTENSION_PACKAGE constant so the check and the message cannot drift.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 0bea8bdf7d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kwakayama

Copy link
Copy Markdown
Contributor Author

Review: 92/100 — merging

The finding that matters most: this PR contradicts its own issue

#3725 proposes optionalDependencies as the fix. It does not work. npm installs optional dependencies by default — optional means "tolerate installation failure", not "skip". Measured with npm 11.12.1 against the published package shape:

packages node_modules/@types/mdx tsc --noEmit
optionalDependencies 117 present exit 2
optional peer 4 absent exit 0

So the change lands on peerDependencies + peerDependenciesMeta.optional, reusing the existing ROOT_OPTIONAL_RUNTIME_PEERS mechanism. Taking the issue at its word would have produced a PR that looked complete and fixed nothing. The issue text should be corrected too.

Three consequences, each found rather than guessed

  1. Server startup would have broken for every project. ensureBuiltinContentProcessor is called unconditionally, so a missing package would have broken npx veryfront dev even for projects with no .mdx. Now tolerates a missing-module failure and defers to the compile path's typed MISSING_EXTENSION_ERROR. A real load failure inside an installed extension still propagates — the tolerance is narrow, which is the part worth checking.
  2. update_package_version only rewrote pins under dependencies. An RC publish would have shipped an optional peer pointing at a version that was never published. Silent publish break, nobody asked it to look there.
  3. The minimal starter ships app/about/page.mdx, so it declares the extension.

Review findings, both addressed

MDX declaration missed the feature path (P1). The stated mechanism did not reproduce — templates/features/mdx/feature.json has no files key, so --features mdx scaffolds zero files. But the gap underneath was real: the feature sets mdx.enabled and tips the user to "Create .mdx files in app/ directory" while declaring nothing. Declaration now derives from the assembled project — a scaffolded .mdx or the feature being selected — which also subsumes the minimal special case rather than naming it in a table.

generate wrote .mdx with no declaration (P1), then hard-coded npm install (P2). Both fixed; the warning now routes through detectProjectInstallTarget/formatInstallCommand, so a pnpm/yarn/bun project is not told to write a competing lockfile.

Deductions

−5, templates/features/mdx/files/app/docs/*.mdx is dead. Three .mdx fixtures exist on disk and are never scaffolded, because the loader goes by feature.json. Either dead files or a feature-loader bug. Out of scope here, but this PR is the reason anyone knows — it should not vanish with the merge.

−3, the compiled-binary guarantee is verified by reading, not by a test. compile-binary.ts embeds the extension as a compile-time include with no reference to npm metadata, so the binary is genuinely unaffected — but nothing fails if that changes.

Verdict

Green at head (0bea8bdf7, 28/28), reviewed clean at head, all five acceptance items met. Merging.

@kwakayama
kwakayama added this pull request to the merge queue Aug 16, 2026
Merged via the queue into main with commit ff31dbc Aug 16, 2026
34 checks passed
@kwakayama
kwakayama deleted the fix/issue-3725 branch August 16, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI-only extensions should not be runtime dependencies of the library package

1 participant