Skip to content

Publish the generator to npm, as npx @pmndrs/docs - #574

Merged
abernier merged 6 commits into
mainfrom
npm-cli
Aug 19, 2026
Merged

Publish the generator to npm, as npx @pmndrs/docs#574
abernier merged 6 commits into
mainfrom
npm-cli

Conversation

@abernier

@abernier abernier commented Aug 19, 2026

Copy link
Copy Markdown
Member

Makes the generator installable from npm, so a docs build no longer needs Docker.

$ cat foo.mdx | npx @pmndrs/docs               # one HTML fragment, on stdout
$ npx @pmndrs/docs                            # …or type the MDX yourself, Ctrl-D to compile
$ npx @pmndrs/docs docs out                    # one .html per .mdx, assets alongside
$ npx @pmndrs/docs docs out --format website   # the whole website, statically exported

--format fragment, the default, is the compiled MDX and nothing else — no layout, no stylesheet, no script — so it needs nothing but node. Mermaid stays a fenced block (server-side SVG is a follow-up) and Sandpack shows its code without the editor. Every website option (--libname, --base-path, --icon, --theme-*…) falls back to the environment variable it maps to, so Docker and build.yml keep working untouched.

Notable

  • Bare npx @pmndrs/docs reads the terminal. It always could, but in silence, which is indistinguishable from a hang — it now says what it expects and how to end it, on stderr so stdout stays a clean pipe.
  • Built on commander, as abernier/material-theme-builder is. Every website option is declared once with the environment variable the app reads it as, and Option.env() does the fallback — so --help documents the pairing and the Docker image keeps working with no flags at all.
  • Turbopack refuses to build a project rooted under node_modules, which is exactly where an installed package lives. The app is therefore copied out before building — which also simplified things: /api is kept out of the export by not being copied, so nothing is moved aside and put back, and no exit handler is needed to survive nextBuild calling process.exit() itself.
  • bin/build.mjs is deleted. It predated the Docker image, was never usable (the package was private, so npx @pmndrs/docs build always 404'd), and built a server bundle rather than a static export.
  • Toc leaves the MDX components map. The page renders it from the layout with toc={doc.tableOfContents}; written in a doc it only ever crashed.
  • compileMdxContent takes an options object, so the CLI can override components — that is how a fragment keeps the h1 the website draws from frontmatter.
  • next/image<img> in People. images.unoptimized is already set, so it bought nothing but a width requirement that broke rendering outside Next.

Verified

npm pack → install in a third-party folder → the three modes ✅ 17 fragments, 20 pages exported
repo build, as Docker and CI run it ✅ unchanged
lint · format · types · 108 unit tests
tarball 1.0 MB

Before merging

NPM_TOKEN must exist as a repository secret. The release step published nothing while the package was private; now it really publishes, and this PR wires the token into its env.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HfCBpbazo3fr1n2EUXNyh5

`--format website` statically exports the site, as the Docker image does.
`--format fragment` — the default — compiles MDX to plain HTML with no layout,
stylesheet or script, from a folder or from stdin, and needs nothing but node.

The CLI is built on commander, as `material-theme-builder` is. Every website
option is declared once, with the environment variable the app reads it as, and
`Option.env()` does the fallback — which is how the Docker image and the
reusable workflow keep working with no flags at all.

The app is copied out of the package before being built: Turbopack refuses to
process a project rooted under `node_modules`, which is exactly where an
installed package lives. The copy mirrors what `files` publishes — no `/api`,
whose Route Handlers cannot be exported, and no tests or stories, which reach
for `.storybook` and `vitest` — so nothing has to be moved aside and put back.

`bin/build.mjs` is gone: it predated the Docker image, was never published (the
package was `private`), and built a server bundle rather than a static export.

`Toc` leaves the MDX components map. The page renders it from the layout with
`toc={doc.tableOfContents}`, so writing `<Toc />` in a doc only ever crashed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HfCBpbazo3fr1n2EUXNyh5
Tailwind v4 discovers its sources from the project root, so a `className=` written in an
`.mdx` file only produced a rule when the docs happened to sit inside that root. True of a
build run from this repo — the in-repo build was right by accident — and false of one run
from the CLI, which is handed a folder from anywhere and builds in a copy under
`.pmndrs-docs`. Every utility class authored in MDX was missing from the stylesheet, and
the elements using it rendered unstyled: `docs/authoring/introduction.mdx` sizes an iframe
with `h-60`, the rule was dropped, and the page collapsed by 105px.

`globals.css` now names the docs itself. The declaration has to be static — Tailwind
resolves it at build time, against the stylesheet — so the CLI stages the folder at the
path it names rather than the other way around. That is also where the Docker image already
mounts it, `/app/docs`.

Note that `tailwind.config.ts` could not carry this: its `content` is v3 syntax, and v4
only reads the file through an `@config` directive this app does not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HfCBpbazo3fr1n2EUXNyh5
Tailwind v4 only loads a config file through an `@config` directive, which this app does
not have — so `tailwind.config.ts` has been inert. Its `content` globs never applied
(automatic source detection did the work), and neither did its two plugins: `prose` appears
nowhere in the app or the docs, and `aspect-video`, the one aspect utility in use, is core
in v4.

Being inert is not the problem; looking authoritative is. `Layout.tsx` pointed at it for
the `grid-areas-*` values, which actually live in `globals.css` as `@utility` rules — and
the natural place to reach for when a class goes missing from the stylesheet is exactly the
file that cannot fix it.

Verified by rebuilding both ways: the emitted CSS is byte-identical with the file and
without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HfCBpbazo3fr1n2EUXNyh5
`npx @pmndrs/docs build docs out`, so that the commands to come — `dev`, `serve` —
sit next to it rather than behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HfCBpbazo3fr1n2EUXNyh5
`npx @pmndrs/docs build --format website` could not find Next: the app was copied to the
caller's folder, and Node looks for Next by walking up from there — through folders that have
no reason to contain it. It only worked when the caller happened to have Next installed too.

The copy now goes beside the `node_modules` this package's own dependencies are in, wherever a
package manager put them. Linking them into the caller's folder instead is not an option:
Turbopack rejects a symlink that leaves its project root.

Verified on the three install shapes — cold `npx`, a local install, and a run from this repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HfCBpbazo3fr1n2EUXNyh5
npm authenticates the release with OIDC, the way `@pmndrs/uikit` and
`abernier/material-theme-builder` already do, so there is no `NPM_TOKEN` secret to create,
rotate or leak. Setting one would in fact break it: `changesets/action` then writes an
`.npmrc` and skips OIDC.

The `id-token: write` permission the job already had is what signs the publish.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HfCBpbazo3fr1n2EUXNyh5
@abernier
abernier merged commit e70f51e into main Aug 19, 2026
9 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 19, 2026
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.

1 participant