Skip to content

Support React 19, fix dependency declarations and packaging - #34

Open
Devon-White wants to merge 16 commits into
mainfrom
Devon/deps-security-react19
Open

Support React 19, fix dependency declarations and packaging#34
Devon-White wants to merge 16 commits into
mainfrom
Devon/deps-security-react19

Conversation

@Devon-White

@Devon-White Devon-White commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Adds React 19 support alongside 18 and fixes packaging defects that surface under strict node_modules layouts (pnpm, nested npm).

Breaking

  • @docusaurus/core peer is now ^3.9.0 || ^4.0.0 (was ^3.0.0). Docusaurus added React 19 support in 3.7 and dropped Node 18 in 3.9, so the old range advertised combinations that could not work. v4 is pre-accepted.
  • Minimum Node is 20.
  • Theme react/react-dom peers widened to ^18.0.0 || ^19.0.0. The previous ^18.0.0 cap conflicted with the React 19 types the package already shipped.

Fixes

  • Declared dependencies that previously resolved only by hoisting: tslib on the theme (15 files in the published lib/ require it at runtime), and remark-parse, hast-util-to-mdast, @types/mdast, @docusaurus/logger, @docusaurus/utils, @docusaurus/utils-validation on the plugin.
  • The ChatGPT dropdown icon crashed on fresh installs — react-icons 5.7.0 removed SiOpenai.
  • GFM defaults were dropped when the markdown key was omitted entirely, so two configs with the same intent emitted different Markdown. Table pipe alignment regressed for CJK and emoji content.
  • The copy button now appears on pages with a trailing slash.
  • The plugin no longer imports CommanderStatic, a commander@5-only type name.

Packaging

Test files and lib/.tsbuildinfo are no longer published. Unpacked size drops from 204KB to 111KB (theme) and 473KB to 366KB (plugin).

CI and tests

React 18 and 19 are matrix-tested, the test job now runs tests, and the theme has a Jest harness with Docusaurus module stand-ins. 27 tests; the "renders nothing" cases are mutation-checked, so deleting a guard from the component fails the test named for it.

yarn audit --groups dependencies reports 0 advisories across the 364 production packages.

Three dev-only advisories are accepted in dependency-review.yml, each with its reasoning and removal condition inline: two in js-yaml@4.1.1 and one in brace-expansion@5.0.8, both pinned exactly by lerna/nx. They are reachable only when lerna parses this repo's own config, never by a consumer. resolutions cannot fix them without forcing read-yaml-file (used by @changesets/cli, the release tooling) across a major where the API it calls was removed.

Follow-ups

Adds SECURITY.md and a Dependabot config. Both depend on repo settings that are currently off — private vulnerability reporting (the advisory URL in SECURITY.md 404s for external reporters without it) and Dependabot security updates (without it, the new cooldown window has no bypass).

…sories)

The `security-audit` CI job was unsatisfiable: `yarn audit --groups
dependencies` reported 71 unique advisories and Yarn Classic's `--level`
flag does not affect the exit code (verified: exit 30 even with
`--level critical`).

The cause was our own `resolutions` block plus a ~9-month-stale lockfile,
not the Docusaurus version. `@docusaurus/core@3.9.1` and `@3.10.2` declare
byte-identical ranges for every vulnerable package.

The unscoped `>=x` resolutions collapsed every semver range for a package
name into one lockfile entry, actively creating advisories:

  js-yaml@4.1.0, ^3.10.0, ^3.13.1, ^3.14.2, ^3.6.1, ^4.1.0 -> 3.14.2
  brace-expansion@^1.1.7, ^2.0.1                           -> 4.0.1
  webpack-dev-server@^5.2.2                                -> frozen 5.2.2

Removing them alone: 71 -> 67. Refreshing the lockfile (every remaining
advisory was a caret-ranged transitive whose patch was already in range):
67 -> 5.

The 5 survivors are all @docusaurus/core build-tool paths that never reach
a consumer, and all are unfixable at Docusaurus 3.x:
  - image-size x2 (HIGH)  no patch published at any version
  - serialize-javascript x2 via copy-webpack-plugin -- see docusaurus#11801
  - uuid x1 via webpack-dev-server > sockjs (pinned to uuid@^8 by sockjs)

Three breakages surfaced by the refresh, fixed here:

* react-icons 5.7.0 removed `SiOpenai` (Simple Icons dropped the OpenAI
  logo). This was a live bug in the PUBLISHED theme: it declares
  `react-icons: ^5.5.0`, so any fresh install today resolves 5.7.0 where
  `SiOpenai` is undefined at runtime and the ChatGPT dropdown item crashes
  with "Element type is invalid". Switched to `RiOpenaiFill`.

* Docusaurus 3.10 renamed `future.experimental_faster` -> `future.faster`.

* Docusaurus 3.10 added the `mdx1CompatDisabledByDefault` v4 sub-flag
  (absent in 3.9.1), which `future.v4: true` enables. That disables the
  MDX1 HTML-comment shim, so `<!-- truncate -->` no longer parses.
  Converted the blog truncate markers to `{/* truncate */}`.
…eers

Docusaurus is a build tool: it produces static assets and has no server
runtime. Classifying it as a runtime `dependency` made every consumer
install a second copy of @docusaurus/core -- and put webpack-dev-server,
image-size and copy-webpack-plugin into the production audit scope, where
none of them can ever reach a consumer.

This is the remedy the Docusaurus maintainers themselves prescribe
(facebook/docusaurus#5501, which defers to react/create-react-app#11174):
move the build tool out of `dependencies`, then audit production only.

theme-llms-txt
  - @docusaurus/core + theme-common: dependencies -> peerDependencies
    (matches docusaurus-theme-openapi-docs, redocusaurus, and every other
    maintained third-party theme; none ship core as a dependency)
  - react/react-dom peers: ^18.0.0 -> ^18.0.0 || ^19.0.0, matching
    @docusaurus/theme-classic exactly. The old range was self-contradictory:
    it capped at React 18 while shipping @types/react ^19.1.13 and being
    consumed by a React 19 site.

plugin-llms-txt
  - declare peers for what is actually imported: @docusaurus/logger, utils,
    utils-validation. These were devDependencies only, so at consumer sites
    they resolved purely by hoisting.
  - remark-parse: undeclared but imported -> dependencies
  - hast-util-to-mdast, @types/mdast: undeclared type-only imports -> devDeps
  - commander: NOT declared. It is genuinely Docusaurus-supplied -- the
    instance arrives via `extendCli`, hoisted from @docusaurus/core and
    @docusaurus/types. Declaring our own copy could resolve to a different
    major than the one we receive. Instead the type is now derived from
    Docusaurus's own Plugin interface:
      type ExtendCliArg = Parameters<NonNullable<Plugin['extendCli']>>[0]
    This also drops `CommanderStatic`, a commander@5-only name.
  - drop @types/react-dom (unused); @types/react -> ^19

website
  - all @docusaurus/*, sass, docusaurus-plugin-sass, typescript -> devDeps;
    `dependencies` now holds only what reaches the browser bundle
  - remove the duplicate block that declared four @Docusaurus packages in
    BOTH dependencies (^3.9.1) and devDependencies (pinned 3.8.1, a version
    the lockfile did not even contain)
  - typescript ~5.6.2 -> ^5.8.3, matching the root

root
  - add react/react-dom/@types to devDependencies. Yarn Classic evaluates
    workspace peers against the root manifest, so without this it emitted
    false "unmet peer dependency react" warnings. Also gives the React
    18-vs-19 CI matrix a single hoisted copy to swap.

engines.node: >=18.0.0 -> >=20.0 across all three (the root already said
>=20.0.0; Docusaurus 3.9 dropped Node 18).

Audit result, production scope: 71 advisories / exit 30 -> 0 advisories /
exit 0. Unmet-peer warnings on install: 27 -> 10.
…tests

Audit gate
----------
Removed the `security-audit` job (`yarn audit --groups dependencies`) and
added .github/workflows/dependency-review.yml using
actions/dependency-review-action@v5.

The old job gated absolute INVENTORY, which meant any PR -- even a README
edit -- failed on pre-existing transitive advisories, with no mechanism to
accept one that has no fix. The new job gates the DELTA: it fails only on
dependencies a PR actually introduces. `fail-on-scopes` defaults to
`runtime`, so build tooling is out of scope by design, and `allow-ghsas`
gives a first-party way to accept an advisory with no available fix.

This mirrors facebook/docusaurus, which runs no audit step at all across
any of its 18 workflows -- only dependency-review, CodeQL, a Socket
Firewall supply-chain job, and Dependabot.

Added .github/dependabot.yml (cooldown + grouped updates, modelled on
Docusaurus's) so the lockfile cannot rot for nine months again -- that
staleness was the root cause of the whole pile-up.

Tests
-----
`yarn test` ran `lerna run test`, and neither package defined a `test`
script, so the CI test job passed having executed nothing. The one test
file in the repo had never run in CI.

- root `test` now runs jest directly; dropped the misleading `test:packages`
  and `lerna:test` aliases
- jest.config.cjs split into three projects: `monorepo` and `plugin` on the
  node environment, `theme` on jsdom
- added jsdom + @testing-library/{react,dom,jest-dom,user-event}; RTL 16
  peers on react ^18 || ^19, so it works under both matrix legs
- `@docusaurus/*` and `@theme/*` are webpack aliases with no Jest
  equivalent, so test/mocks/ provides stand-ins with settable state, reset
  between tests by test/setup-theme.ts
- 18 new tests covering useDropdownState (including the ref that carries
  the React-19-only RefObject<T | null> signature) and CopyPageContent
  (the null-while-loading hydration path, trailing-slash key probing from
  PR #21, and the remount fetch cache)

Two pre-existing CI blind spots fixed along the way:

* `.eslintignore` excluded `__tests__/`, so no test file had ever been
  linted and the eslintrc override for them was dead config. Removing it
  surfaced real errors in monorepo.test.ts (conditional expects, invalid
  titles) -- fixed rather than suppressed. The conditional-expect block
  silently passed via `expect(true).toBe(true)` when it found no plugin
  packages; it now asserts unconditionally.

* per-package `format:check` used an unquoted `src/**/*.{ts,tsx}` glob. The
  plugin has no .tsx files, so the shell either errored or passed the
  pattern through literally and prettier exited 2. Quoted so prettier
  expands it itself.

monorepo.test.ts also gained two regression tests pinning the invariants
this branch establishes: @docusaurus/core must never be a runtime
dependency, and React peers must accept both 18 and 19.

React hygiene
-------------
With `jsx: react-jsx` already set, only CopyPageContent used React as a
value. Converted the other ten files to `import type React`, dropped the
entirely-unused import in DocBreadcrumbs, and switched React.useMemo to a
named import. Removed the dead `globals: { JSX: true }` from .eslintrc.cjs,
a leftover from the global JSX namespace that @types/react@19 deleted.
The packages now claim `react: ^18.0.0 || ^19.0.0`. This makes that claim
verifiable instead of aspirational.

The `test` job runs as a matrix over React 18 and 19, replacing the separate
test-plugins and test-website jobs (both now run inside each leg, so the
website is built against the matrix version too). Only the "WIP" check is
required by branch protection, so the job rename does not affect the merge
gate.

The pin step swaps React in the root AND the website workspace: the website
declares its own React range, so swapping only the root leaves a nested copy
behind and the leg would silently test the wrong version. A guard step asserts
the root and the website resolve the same react/package.json and that its
version matches the matrix leg, so a botched pin fails loudly rather than
producing a false pass.

Docs
----
- compatibility tables in both package READMEs (there were none)
- root README said Node >= 18 while engines said >= 20, and listed Husky as
  part of the toolchain though no .husky directory exists -- both corrected
- SECURITY.md explains the gating model: why we gate the delta rather than the
  inventory, why runtime scope is the right filter for a static-site build
  tool, how to accept an unfixable advisory via `allow-ghsas`, and why
  unscoped Yarn Classic `resolutions` are a trap
- `packageManager: yarn@1.22.22` so Corepack pins the version; dropped the
  stray `engines.npm` from what is a Yarn-only repo

Verified against packed tarballs installed with npm, which enforces peer
ranges strictly where Yarn Classic only warns:

  published 1.0.0-alpha.9 + React 19 -> npm install FAILS
    ERESOLVE: peer react@"^18.0.0" ... Found: react@19.2.8
  this branch + React 18            -> installs clean, 1 @docusaurus/core
  this branch + React 19            -> installs clean, 1 @docusaurus/core

Both consumer sites then ran `docusaurus build` against Docusaurus 3.10.2
successfully, generated llms.txt / llms-full.txt / markdown, and shipped the
theme into the client bundle.
…plugin

The earlier commits tightened the plugin's manifest but left equivalent
issues in the theme. Bringing it to parity.

Removed three unused devDependencies from the theme:
  - @docusaurus/plugin-content-docs -- no code reference. The only thing that
    needed it was thought to be `@theme-init/DocBreadcrumbs`, but that is
    declared by @docusaurus/module-type-aliases (`declare module
    '@theme-init/*'`), which the theme already keeps.
  - fs-extra -- not imported by the theme. The build calls
    ../../scripts/copyUntypedFiles.js, which resolves fs-extra from the root.
  - @types/react-dom -- the theme has zero react-dom imports, and the root
    now provides the types workspace-wide. react-dom stays a peerDependency.

Removed src/utils/htmlExtractor.ts. `extractHtmlContent` was unreferenced
dead code and a superseded duplicate of `extractContentFromDom` in
useCopyActions -- identical selector-then-fallback logic, except it parsed a
string via DOMParser instead of reading the live document. It was being
compiled into lib/ and published.

Removed packages/tsconfig.json. Nothing extended, built, or referenced it,
and it disagreed with the configs actually in use (outDir ./dist against the
lib/ everything else emits to, rootDir ../).

Added the theme to the root tsconfig.json `references`. The plugin and the
website were listed but the theme never was, so `tsc --build` at the root
silently skipped it. It now type-checks as part of the solution.

Verified: `tsc --build` at the root now succeeds across all three projects;
the theme's client project type-checks standalone; and clean-built tarballs
install and build in scratch Docusaurus 3.10.2 consumers on React 18 and 19,
with one @docusaurus/core and llms.txt generated in both.

Note on packaging: `yarn pack` does not run `prepublishOnly`, so a stale
lib/ can leak orphaned output into a locally packed tarball -- tsc --build
does not delete outputs whose sources are gone. The real publish path is
unaffected because prepublishOnly runs `clean` first, confirmed by packing
after a clean build.
Three defects found while chasing editor errors on the theme's .tsx files.

1. The theme's type-check was a no-op
------------------------------------
The package had two overlapping TS projects, both with `include: ["src"]`:
tsconfig.json (NodeNext, no jsx) referencing tsconfig.client.json (CommonJS,
jsx, Docusaurus types). Project-reference resolution meant the root project
ended up with exactly ONE file -- src/theme-llms-txt.d.ts -- while the client
project compiled everything real.

Consequences:
  - `yarn type-check` (tsc --noEmit on the root project) checked 1 file. Only
    `tsc --build` caught anything, which is why the react-icons SiOpenai
    breakage showed up in `build` but not `type-check`.
  - Editors resolve the nearest tsconfig.json, which did not include any .tsx
    file, so tsserver fell back to an inferred project with no `jsx` and no
    module aliases. Every theme component showed spurious "Cannot find module"
    errors for @theme/*, @docusaurus/*, and *.module.css -- 11 in total.

The split bought nothing: the package is entirely CommonJS + JSX (no
"type": "module"), and the client project already compiled the Node-side
entry too. Collapsed into a single project and deleted tsconfig.client.json.
Theme type-check coverage: 1 file -> 22.

2. Test files were being published
----------------------------------
The theme ships src/theme as the TypeScript swizzle template, so the
component test added earlier was landing in the tarball and would have been
copied into users' sites by `docusaurus swizzle --typescript`. Moved theme
component tests to src/__tests__, outside any published directory.

3. lib/.tsbuildinfo was being published
---------------------------------------
TypeScript's incremental build cache was the largest single file in both
tarballs. Moved it out of lib/ (declared once per base config as
${configDir}/.tsbuildinfo) and extended `clean` to remove it -- otherwise a
clean followed by a build would be skipped as up to date. Unpacked size:
204KB -> 111KB (theme), 473KB -> 366KB (plugin). This also removes a stray
lib/.tsbuildinfo that tsconfig.base.json was writing to the repo root.

A note on the fix for 2 and 3
-----------------------------
The obvious fix -- npm-style "!" negations in files[] -- is a trap here.
Yarn Classic does not implement that syntax, and on encountering one it stops
honouring files[] as a whitelist altogether, packing MORE than intended.
Measured on the theme with negations present: npm pack produced 90 entries
with 0 leaks, yarn pack produced 141 entries with 5 leaks. Since the release
path may invoke either, files[] is kept purely positive and correctness comes
from layout instead. Verified both packers now emit identical file sets with
zero leaks.

Also fixed: getTsconfigFiles in the monorepo test used fs.readJSON, which
throws on comments. tsconfig is JSONC and the repo uses comments, so it now
reads via ts.readConfigFile -- TypeScript is already a devDependency.

Two new regression tests, both confirmed to fail when the defect is
reintroduced: published tarballs contain no test files or build metadata
(asserted against `npm pack --dry-run`), and files[] contains no negation
patterns.
Type surface
  - plugin-llms-txt.d.ts declared `finalConfig: any` for the CopyButton and
    DropdownMenu props -- the two `any`s in the published type surface, on the
    props swizzlers are most likely to touch. Replaced with an explicit
    ResolvedCopyPageContentOptions interface mirroring the theme's
    useCopyButtonConfig. Restated rather than imported because these @theme/*
    declarations intentionally live in the plugin package (documented in the
    theme's own d.ts), which does not depend on the theme. A stale comment
    referred to it by a name that does not exist, ResolvedCopyPageButtonOptions.

Dependency ranges
  - root @docusaurus/{eslint-plugin,utils,utils-validation} were still ^3.0.0
    while the packages moved to ^3.10.2; aligned.
  - root @types/node ^20.8.0 -> ^22.15.19, matching both packages.
  - dropped root `concurrently` (unused; only the theme uses it, at ^9, so the
    root's ^8 was a second major of the same tool in one workspace).
  - dropped @typescript-eslint/{eslint-plugin,parser} from the plugin package.
    ESLint runs only at the root, the package has no eslint config, and its
    lint script is an echo -- they were dead weight pinned a patch behind root.

New regression test
  The theme reads the plugin's global data via a hardcoded
  'docusaurus-plugin-llms-txt', while the plugin owns that string as
  PLUGIN_NAME. The theme cannot import it without turning a types-only
  devDependency into a runtime one, so the duplication is deliberate -- but if
  the two drift, usePluginData returns undefined and the copy button silently
  renders nothing, with no error anywhere. Now asserted. Confirmed to fail when
  the name is changed on either side.
The header opened a second /** inside an already-open block comment, so the
license line and package description were part of a broken comment structure.
tslib
  The theme build sets importHelpers/noEmitHelpers, so 15 files in the
  published lib/ require tslib at runtime. It was declared nowhere: it had
  been supplied transitively by @docusaurus/core, and moving core to a peer
  in 2aa2cf3 removed that path. Under pnpm or a nested npm layout the theme
  failed on first render with "Cannot find module 'tslib'".

@docusaurus/{logger,utils,utils-validation}
  These were reclassified as peerDependencies, but they are libraries the
  plugin value-imports at runtime across ten sites, not build tooling. Sites
  install @docusaurus/core and a preset and never name these, so Yarn Classic
  left three unmet peers and installed nothing -- the plugin kept working
  only by hoisting, which is exactly what the reclassification set out to
  fix. Every first-party Docusaurus plugin declares them as dependencies and
  peers only @docusaurus/core; matched that.

GFM defaults
  Joi does not fill nested defaults when the parent key is absent, so
  {llmsTxt: {...}} left markdown.remarkGfm undefined while
  {markdown: {}, llmsTxt: {...}} set it to true. applyGfmConfiguration read
  undefined as "not configured, skip" and returned early, so DEFAULT_GFM was
  never merged; getMarkdownConfig then coerced it to true and the plugin
  registry passed remark-gfm no options at all. Two configs with identical
  intent emitted different Markdown, and table pipe alignment silently
  regressed for CJK and emoji content. undefined now resolves to the same
  defaults as an explicit `remarkGfm: true`, and DEFAULT_GFM is copied rather
  than handed out by reference.

  Verified against the built output: markdown absent, markdown: {} and
  remarkGfm: true all now yield singleTilde=false tablePipeAlign=true
  tableCellPadding=true stringLength=function; false still disables; an
  object still merges over the defaults.
Three of the theme's "renders nothing" tests passed for any input.
CopyPageContent returns null on its first pass regardless, because
useCopyContentData starts isLoading and only fetches inside an effect -- and
`await waitFor(() => expect(fetch).toHaveBeenCalled())` resolves on its first
synchronous check, before the fetch chain flushes. So the assertions were
equally true of a route that must render the button.

Each test now renders once at a displayable route first. That doubles as a
positive control and warms useCopyContentData's module-level cache, so the
second render takes the cache's synchronous branch and has already settled by
the time render() returns.

Confirmed by mutation. Deleting a guard from the component now fails the test
named for it:

  !shouldDisplay       -> "route is marked shouldDisplay: false"
  pluginConfig=false   -> "the plugin is disabled"
  !siteConfig          -> "global data omits siteConfig"

Against the previous tests, deleting the first two simultaneously left both
of their tests passing; only an unrelated test caught it.

The third is a new case. Removing !siteConfig failed nothing even after the
rewrite: with global data absent entirely there is no data URL either, so
!shouldDisplay returns null first and the guard is unreachable. siteConfig is
optional on PluginGlobalData, so the guard is load-bearing -- the new test
supplies route data that says displayable with siteConfig missing, the only
shape where that guard is what stops useCopyActions receiving undefined.

Also:

  - jest.config.cjs counted the co-located spec files as production source in
    collectCoverageFrom, so adding a test lowered reported coverage.
  - tsconfig.test.json inherited rootDir "${configDir}/src" from the base
    config, which resolves to a nonexistent <repo>/src. `tsc -p` exited 2 on
    option diagnostics and so reported no semantic errors at all. With rootDir
    corrected and @docusaurus/module-type-aliases added to types (for the
    *.module.css declarations) it exits 0. Nothing invokes it in CI yet.
  - test/mocks/style.cjs returned a string for every key including
    Symbol.toPrimitive, so any string coercion of the styles object threw
    "string Symbol(Symbol.toPrimitive) is not a function". Symbol keys now
    fall through as undefined.
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Dependency Review Summary

The full dependency review summary was too large to display here (1027KB, limit is 1024KB).

Please download the artifact named "dependency-review-summary" to view the complete report.

View full job summary

Every CI job failed at install:

  error @testing-library/jest-dom@6.10.0: The engine "node" is incompatible
  with this module. Expected version ">=22". Got "20.20.2"

jest-dom 6.10.0 raised engines.node from ">=14" to ">=22". The declaration
here was an unpinned "^6", so the caret floated across that bump. Yarn Classic
treats an engines mismatch as an error, not a warning, so `yarn install
--frozen-lockfile` aborted before any job reached its first step.

Pinned to ~6.9.1, the last release supporting Node 20. Both packages declare
engines.node ">=20.0" and .github/actions/setup pins CI to Node 20, so the
alternative -- raising the floor to 22 for a devDependency -- would have meant
either contradicting the published support contract or no longer testing it.

Audited the rest of the tree: jest-dom was the only installed package
requiring Node 22 unconditionally. Everything else that looks close
(lerna ^20.19, the npmcli/sigstore chain ^20.17, jest ^20.0.0) is satisfied
by 20.20.2.

Added .nvmrc so local shells match the CI default. This went unnoticed
because it only reproduces below Node 22, and the version was pinned in the
composite action but nowhere a contributor's shell would read it.

Verified on Node 20.20.2 -- the exact version CI reported -- via nvm:
install --frozen-lockfile, build:packages, format:check, lint, type-check,
test:ci (27 tests) and build:website all exit 0.
Dependency Review failed the PR on two high-severity advisories:

  yarn.lock >> tar@6.2.1
    GHSA-8qq5-rm4j-mr97  Arbitrary File Overwrite and Symlink Poisoning via
                         Insufficient Path Sanitization
    GHSA-34x7-hfp2-rc4v  Arbitrary File Creation/Overwrite via Hardlink Path
                         Traversal

Both are patched only in the 7.x line (7.5.3 and 7.5.7); 6.2.1 is the last
6.x release, so there is no in-major upgrade. lerna@9.0.1 and
@lerna/create@9.0.1 pin tar to exactly 6.2.1 -- they are the only things in
the tree still on 6.x, everything else already resolves 7.5.22.

main carried `"tar": ">=7.5.2"` in resolutions, which is what had been holding
this down; 12cad9c removed the whole resolutions block. That removal was right
in general -- the unscoped entries collapsed unrelated semver ranges and in
some cases forced packages DOWN a major (js-yaml 4.x -> 3.14.2) -- but it also
dropped the one entry doing real work. Restored just that entry.

Why forcing lerna across a tar major is safe here: this repo publishes with
changesets, not `lerna publish`. lerna is only ever invoked as `lerna run
<script> --stream` (build, type-check, lint, format, clean, prepublishOnly),
which is the task runner and never touches tar. tar sits on lerna's pack and
publish path, which nothing in this repo calls.

Tried the narrow form first -- `"lerna/tar"` and `"@lerna/create/tar"` --
but Yarn Classic did not apply it; both nested copies stayed at 6.2.1.

Bumping lerna instead is not an option yet: lerna@10.0.0 does ship tar@7.5.20,
but its engines are `^22.13.0 || ^24.0.0 || ^26.0.0`, which would break the
Node 20 floor both packages declare and CI pins.

Verified on Node 20.20.2: tar resolves to 7.5.22 everywhere with no 6.x copy
left, `lerna --version` still reports 9.0.1, and install --frozen-lockfile,
build:packages, format:check, lint, type-check, test:ci (27 tests) and
build:website all exit 0.
Two unrelated CI failures.

1. Test (React 18) -- Cannot find module '@testing-library/react'
-----------------------------------------------------------------
Only the 18 leg failed; 19 passed. Reproduced in a clean worktree on Node
20.20.2: `yarn workspace website add react@^18 react-dom@^18` leaves the root
tree incomplete. It removes hoisted root devDependencies from node_modules
while leaving them in both package.json and yarn.lock --
@testing-library/react, @testing-library/user-event, @types/react-dom and
rimraf all vanish, so test/setup-theme.ts cannot resolve its imports and both
theme suites fail to run. Nothing about React 18 causes this; the 19 leg
survived only because its `yarn add` was a no-op against the already-resolved
tree.

A plain `yarn install` after the pin steps restores everything and leaves
React at the pinned 18.3.1. Verified end to end in the worktree: react 18.3.1
with build:packages, type-check, test:ci (27 tests) and build:website all
exiting 0.

2. Dependency Review -- js-yaml@4.1.0
-------------------------------------
  GHSA-5p4m-2wfm-xmqj  Quadratic CPU consumption in !!omap resolution
  GHSA-52cp-r559-cp3m  YAML merge-key chains force quadratic CPU consumption

Same shape as the tar failure: lerna pins the version exactly. Bumped lerna
9.0.1 -> 9.0.7, the newest 9.x, which keeps `node: ^20.19.0 || ...` and moves
its own pins to tar@7.5.11 and js-yaml@4.1.1. That also makes the existing
tar resolution honest -- it is now an in-major bump (7.5.11 -> 7.5.22) rather
than forcing lerna across a major, so the caveat in the previous commit no
longer applies.

js-yaml is still short of the 4.3.1 fix, and cannot be resolved away:
Yarn Classic 1.22 ignores the scoped `lerna/js-yaml` form -- verified, the
nested copy stays put -- and an unscoped entry rewrites every range for the
name, dragging read-yaml-file@1.1.0 from 3.15.1 to 4.x. That package calls
`yaml.safeLoad`, removed in v4, and @changesets/cli depends on it. Since
changesets is what publishes these packages, forcing it would trade a
dev-time DoS for a broken release pipeline. lerna@10 ships js-yaml 4.3.1 but
requires Node >=22, which the Node 20 floor rules out.

Allow-listed both, with the reasoning and the removal condition in the
workflow. Neither reaches a consumer: they are reachable only when lerna
parses this repo's own config, and `yarn audit --groups dependencies` reports
0 advisories across the 364 runtime packages.

Full gate on Node 20.20.2 with lerna 9.0.7: install --frozen-lockfile,
build:packages, format:check, lint, type-check, test:ci (27 tests) and
build:website all exit 0.
Third and last advisory in the delta. Scanned all 707 packages this branch
adds to the lockfile against the GitHub advisory database: exactly three
HIGH/CRITICAL hits, the two js-yaml ones already accepted and this.

brace-expansion@5.0.8 is pinned exactly by nx, which lerna depends on at
`>=21.5.3 <23.0.0`. GHSA-rgw5-rvv9-x895 is patched in 5.0.9 and no release in
that range ships it -- only nx 23.2.0 canaries do, and 23.x is outside
lerna's range anyway.

Not a regression: main resolves brace-expansion to 4.0.1, which the same
advisory covers (>= 4.0.0, < 5.0.9). The delta gate flags this only because
the version string changed, and 5.0.8 is the better of the two -- it already
carries the GHSA-mh99-v99m-4gvg fix that 4.0.1 lacks. Everything else in the
tree is on the patched 5.0.9; only nx's exact pin holds 5.0.8.

An unscoped `brace-expansion` resolution is the same trap as js-yaml: it
would drag the 1.1.18 and 2.1.4 consumers across two majors. main had exactly
that entry, and it is why main sits on 4.0.1 for everything.
51 net comment lines removed. Clears the two max-len warnings in
CopyPageContent.test.tsx and keeps every comment inside the 80-char limit
.eslintrc.cjs sets for comments. Rationale that belongs in history rather
than beside the code now lives in the commit messages that introduced it.
The version/publish/changelog/ignoreChanges blocks were copied from
facebook/docusaurus (identical labels and cacheDir) and have never run:
no commit in history matches either of lerna's configured release messages,
both CHANGELOGs are changesets format, and lerna-changelog isn't installed.
command.build was inert too -- lerna has no build command.

Versioning is changesets, as the README already stated. Two configured
release systems next to each other is a trap: `lerna version` would apply
conventional-commit bumps while the .changeset/*.md files sat unused.

lerna now declares only what it does here -- run and exec. bootstrap and
link were removed in lerna 7, so yarn workspaces does all linking.
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