feat(ember-vite): static docs export (.md per page + llms.txt) - #210
Merged
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Validate staticExport eagerly in buildStart via a new validateStaticExportOptions helper and this.error(...), so a missing siteUrl fails the Vite build instead of being swallowed silently. Also add an optional projectName to emit the llms.txt-convention H1, warn in the README against the meta.pluginData footgun, and add doc comments plus options-table clarifications. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Test step only ran packages/core, so ember-vite's suite never executed in CI. Added as an explicit step rather than switching to the root `yarn test` (`lerna run --parallel test`), which would also pull in test-app-vite's `testem ci` browser suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e links llms.txt and llms-full.txt now default to root-relative links (e.g. /docs/about.md) instead of requiring an absolute siteUrl, so deploy previews, forks, staging, and local builds all work without configuration. siteUrl remains available as an opt-in for absolute links, and is now validated as an absolute http(s) URL when provided. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A siteUrl like "https://docfy.dev/foo?x=1" passed validation but produced "https://docfy.dev/foo?x=1/docs/about.md", since the page path is appended to the configured origin. A path is still allowed — docs served under a subpath concatenate correctly — only a query or fragment is rejected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The llms.txt convention treats the H1 project name as its only required
element, but projectName was optional, so a minimal `{ enabled: true }`
emitted an llms.txt with no H1 while the docs claimed convention
adherence.
@docfy/core has no project-name option to reuse, so this falls back to
the consuming app's package.json name — the same source `repository`
already defaults from, adding no new user-facing config. An explicit
projectName still wins.
Also documents the whole static export feature in docs/ember/ember-vite.md
so it appears on the docs site, not just in the package README.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces yarn 1 workspaces with pnpm. Three latent bugs that yarn's flat node_modules had been masking had to be fixed for a clean install: - Cross-workspace deps used plain semver ranges. pnpm 10+ defaults link-workspace-packages to false, so those resolved from the registry instead of linking locally — test-app-vite was building against the published @docfy/ember-vite. Converted all 9 declarations to the workspace: protocol. - packages/plugin-with-prose/tsconfig.json hardcoded typeRoots to the root node_modules/@types, which only exists under a hoisted layout. Removed the override; TypeScript's default resolution finds the package's own @types. The "types" dir it also listed does not exist. - @docfy/ember-cli imports from 'unist' but declared no @types at all, relying on a sibling's hoisted copy. Declared @types/unist directly. pnpm 11 reads `overrides` from pnpm-workspace.yaml, not package.json, so yarn's `resolutions` pin moved there. Without it @types/unist resolves to 2.0.11, whose generic Node breaks @docfy/ember-cli's compile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Root scripts move to pnpm's native recursive runner; lerna.json switches npmClient to pnpm and drops the dead command.bootstrap block (lerna 9 has no bootstrap command). Two more phantom dependencies had to be declared before `pnpm compile` would succeed — both were satisfied by accident under yarn's flat tree: - @docfy/ember imports @docfy/core/lib/types in four source files but declared @docfy/core nowhere. Added to dependencies rather than devDependencies because its published declarations/ reference those types, so consumers must be able to resolve them. - @docfy/ember-vite imports from 'unist' in src/types.ts without declaring @types/unist. Added to devDependencies, matching how @docfy/core already declares it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two more phantom dependencies: @docfy/ember-vite and @docfy/ember-cli both import '@eslint/js' in their eslint.config.mjs without declaring it. Every other package in the repo declares it at ^9.32.0; these two relied on yarn hoisting it from a sibling. pnpm also blocks dependency build scripts by default. esbuild's postinstall installs its platform binary (vite cannot build without it) and nx is lerna's task engine, so both are allowed via allowBuilds. core-js 2.x's postinstall only prints a funding banner and stays off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three tsconfigs hardcoded typeRoots to the repo-root node_modules/@types, which only exists under a hoisted layout. Under pnpm this left @types/jest unresolvable, so @docfy/core's 11 jest suites failed to run with "Cannot find name 'describe'". Each now points at its own package's node_modules/@types. The local "types" entries are preserved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pnpm/action-setup precedes setup-node so `cache: pnpm` can resolve the store path, and takes its version from the root packageManager field. Drops `npm install -g yarn` and gains dependency caching. This is the change the migration exists for: yarn 1 fetches from registry.yarnpkg.com, which the runners could not reach; pnpm fetches from registry.npmjs.org. Test steps stay explicit per package rather than `pnpm -r run test`, which would pull in the two browser suites — one of which is red for an unrelated, undiagnosed reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Consumer-facing install docs are deliberately left on npm/yarn — how a user installs @docfy/* into their own project is not this repo's concern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@docfy/ember-vite and @docfy/plugin-with-prose both import eslint-plugin-jest in their eslint.config.mjs without declaring it, relying on yarn hoisting it from @docfy/core. Found by auditing every package's eslint config imports against its declared dependencies rather than fixing them one failure at a time; that audit now reports all imports declared. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chore: migrate from yarn 1 to pnpm
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an opt-in static text export to
@docfy/ember-vite. When enabled, a production build emits a statically-servable text mirror of the docs alongside the app:dist/<page-url>.md— one file per page, at the same path as the live route plus a.mdsuffixdist/llms.txt— links-only index grouped by section, per the llms.txt conventiondist/llms-full.txt— every page's content concatenatedWhy: the docs site is client-rendered only, so a plain HTTP fetch — a crawler,
curl, or an AI agent's web-fetch tool — gets the SPA shell rather than content. This makes the docs readable without running JavaScript.Off by default and build-only; nothing is emitted during
vite dev.Links default to root-relative (
/docs/getting-started.md), which the llms.txt spec permits — its format definition is only[name](url)— and which stays correct on deploy previews, forks, staging, and localhost. Set the optionalsiteUrlto emit absolute links instead, for consumers that read the text detached from its origin.No changes to
@docfy/core.Design notes
The one non-obvious decision worth calling out. The export payload is
page.pluginData.staticMarkdown ?? page.markdown, with the frontmatter block stripped.page.markdownis the raw, untouched file source — set once inDocfy.createPage()and never written by any core plugin (nothing else in the monorepo reads it). Consuming apps that need transformed output — for example replacing a<Signature @component="Button" />tag with a real Markdown table — setpluginData.staticMarkdownfrom a Docfy plugin inrunAfter.Doing it in
runAfteron Markdown text, rather than mutating the AST, is deliberate: by that pointpage.asthas already been destructively converted to hast, andpage.rendered— which the live SPA route templates are built from — is stringified from that same object. Mutating the AST would corrupt the rendered app. Mutating markdown text is inherently isolated, so no cloning ofresult.contentis needed.Ordering for both
llms.txtandllms-full.txtcomes solely fromresult.nestedPageMetadata, which already encodes section labels and the resolvedsectionsorder config.Changes
src/static-export.tssrc/file-manager.tswriteTextToPublic(content, fileName);writeJsonToPublicnow delegates to it. Behavior preserved for its existing caller.src/config.tsstaticExporttoDocfyViteOptions, destructured out so it can't leak into@docfy/core's options.src/docfy-processor.tshandleStaticExport(), called fromprocessAll(), gated build-only.src/index.tssiteUrlvalidation viathis.error().README.mdpluginData.staticMarkdowncontract.test-app-vite/vite.config.mjsTesting
54 unit tests across 4 files (the package's first tests —
vitestwas configured but unused).yarn typecheck,yarn lint, and Prettier all clean.Also verified against a real build of
test-app-vite, which sources this repo's owndocs/:.mdfiles emitted;dist/docs/index.mdanddist/docs/ember/index.mdcorrect — trailing-slash index URLs becomeindex.mdrather than a brokendocs/.mdllms.txtcarries the required# DocfyH1, then the blockquote, then## Documentation/## Emberin the configured section order; all 15 root-relative links resolve to real emitted filespublic/siteUrl(missing scheme, wrong protocol, or carrying a query/fragment) fails the build with a clear message and a non-zero exit, rather than silently emitting broken linksNote
test-app-vite's acceptance suite (testem ci) is currently red and I could not observe it green. It fails withUncaught ReferenceError: define is not definedin@embroider/virtual/test-support.js— an AMD-loader ordering failure in the test bundle.This looks pre-existing and unrelated: the failure reproduces identically with this branch's config change reverted, and the only runtime-affecting change here is
emitFilecalls for standalone.md/.txtassets, which cannot alter module format, chunk graph, or script order. It was not diagnosed, though, so it's worth a look in real CI and probably its own issue — flagging it explicitly rather than letting a red suite get silently inherited.Follow-ups (deliberately not in this PR)
frontileside, which this unblocks: emit signature data as JSON, add the<Signature>→ Markdown-table plugin writing topluginData.staticMarkdown, enable the option.frontile/docs/superpowers/specs/sits inside that app's Docfy source root with pattern**/*.md, and core'sDEFAULT_IGNOREonly coversnode_modules/.git/dist— so design docs are already built as docs pages and would land inllms.txt. Needsignore: ['superpowers/**']..mdso an agent crawling the mirror stays in the mirror..gjstemplates undertest-app-vite/app/templates/docs/are git-tracked and get rewritten by every build. Pre-existing; worth gitignoring.handleAssets(result: any)/processAll(): Promise<any>now thatDocfyResultis imported.🤖 Generated with Claude Code