Summary
examples/docusaurus-docs cannot run docusaurus build end-to-end under Node 22 in this monorepo, which is why it is CI-gated only — its pnpm conformance script stops at the ACT artefacts (tsx scripts/build.ts && tsx scripts/validate.ts) and never invokes docusaurus build. As a result, examples/docusaurus-docs/ is intentionally not in scripts/assemble-examples.mjs#EXAMPLES and not deployed to GitHub Pages, while the five other site-emitting examples (astro-docs, ecommerce-catalog, eleventy-blog, nextjs-marketing, starlight-docs, vitepress-docs) are.
This issue records the failure modes I hit while attempting to make docusaurus build work, the patches I prototyped, and where the trail goes cold — so the next person picking this up doesn't repeat the loop. Closing this issue means: docusaurus build runs cleanly during conformance, the example deploys to https://act-spec.org/examples/docusaurus-docs/, and the spec-doc callout in spec/v0.2/generators/docusaurus.md flips back from "Reference example. …not deployed yet" to "Live example. …deployed at /examples/docusaurus-docs/".
Repro
# from repo root
pnpm install
pnpm -F @act-spec/example-docusaurus-docs run build # OK — emits ACT artefacts to static/
pnpm -F @act-spec/example-docusaurus-docs run build:site # FAILS
Environment where the failure was observed:
- Node
v22.19.0
- pnpm
10.17.1
- Reproduces on both
@docusaurus/core@3.6.3 (current pin) and @docusaurus/core@3.10.1 (latest)
- Reproduces with both
webpack@5.95.0 (current pin) and webpack@5.106.2 (latest)
- macOS arm64; not retested on Linux CI runners but the failure is in pure JS / Node so should reproduce there
Failure cascade
There are at least four distinct failures stacked behind each other. Each one was unblocked by a patch; the next one then surfaced.
1. TypeError: require.resolveWeak is not a function
[cause]: TypeError: require.resolveWeak is not a function
at 297 (server.bundle.js:6332:253)
at __webpack_require__ (server.bundle.js:6911:41)
...
at Script.runInContext (node:vm:149:12)
at module.exports (node_modules/.pnpm/eval@0.1.8/node_modules/eval/eval.js:84:12)
at .../@docusaurus/core/lib/ssg/ssg.js
Root cause. Docusaurus's SSG path reads the webpack-bundled build/__server/server.bundle.js from disk and evaluates it via the eval package against a custom require shim built in lib/ssg/ssgNodeRequire.js#createSSGRequire. The shim copies resolve, cache, extensions, and main from a Module.createRequire(serverBundlePath) but does not define resolveWeak. The webpack server bundle keeps literal require.resolveWeak("…") calls in its route registry (the third tuple entry in each __WEBPACK_DEFAULT_EXPORT__ lazy-route record), so when the SSG executes the bundle those calls land on the shim and throw.
Patch I prototyped (in node_modules/.../ssgNodeRequire.js via pnpm patch):
ssgRequireFunction.resolveWeak = (id) => id;
resolveWeak returns an opaque module identifier the loader registry treats as a key — webpack's own runtime returns a chunk ID; returning the input string verbatim is enough for the SSG path. (Aliasing to realRequire.resolve was wrong: it triggers Node's resolver against webpack aliases like @site/..., which then MODULE_NOT_FOUND.)
Unblocks → next failure.
2. CSS files reach Node's require: SyntaxError: Unexpected token ':'
SyntaxError: Unexpected token ':'
at .../infima/dist/css/default/default.css:24
at ssgRequireFunction (.../@docusaurus/core/lib/ssg/ssgNodeRequire.js:26:24)
Root cause. Docusaurus's genClientModules (in lib/server/codegen/codegen.js) generates .docusaurus/client-modules.js with literal absolute-path require() calls:
export default [
require("/.../infima/dist/css/default/default.css"),
require("/.../theme-classic/lib/prism-include-languages"),
require("/.../theme-classic/lib/nprogress"),
require("/Users/jforsythe/dev/ai/act/examples/docusaurus-docs/src/css/custom.css"),
];
Webpack bundles this file but keeps the require() calls literal — they appear unchanged in server.bundle.js (module index 3826 in the build I inspected). Webpack 5 with target: 'node22.19' is treating these absolute-path requires as commonjs externals, even though the project's webpack config has CSS rules that should null-loader them on the server. (I never figured out exactly why webpack externalized them — see "Where the trail goes cold" below.)
On the client these would go through mini-css-extract-plugin. On the server, @docusaurus/bundler/lib/loaders/styleLoader.js returns [{ loader: require.resolve('null-loader') }] for non-module CSS — but the literal require() calls in client-modules.js bypass that pipeline entirely.
Patch I prototyped:
const ASSET_EXT_RE = /\.(?:css|scss|sass|less|styl)$/i;
const ssgRequireFunction = (id) => {
if (typeof id === 'string' && ASSET_EXT_RE.test(id)) return {};
...
};
Mirrors null-loader's no-op behaviour.
Unblocks → next failure.
3. @theme/* aliased imports inside theme-classic client bootstraps: Cannot find package '@theme/prism-include-languages'
[cause]: Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@theme/prism-include-languages'
imported from .../@docusaurus/theme-classic/lib/prism-include-languages.js
Root cause. theme-classic/lib/prism-include-languages.js is:
import { Prism } from 'prism-react-renderer';
import prismIncludeLanguages from '@theme/prism-include-languages';
prismIncludeLanguages(Prism);
When the SSG bundle requires this file, Node loads the JS source directly — Node has no idea what the webpack alias @theme/prism-include-languages means, so it throws ERR_MODULE_NOT_FOUND. The file is a client-only side-effect (configures Prism syntax-highlight extensions on the browser-side Prism instance) so it has zero contribution to SSG'd HTML.
Patch I prototyped:
catch (err) {
const isWebpackAlias =
/Cannot find package '(?:@theme|@docusaurus|@site|@generated)\//
.test(err?.message ?? '');
if (err?.code === 'ERR_MODULE_NOT_FOUND' && isWebpackAlias) return {};
throw err;
}
Unblocks → next failure.
4. Sibling .css import inside nprogress.js: ERR_UNKNOWN_FILE_EXTENSION
[cause]: TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension \".css\"
for .../theme-classic/lib/nprogress.css
nprogress.js is:
import nprogress from 'nprogress';
import './nprogress.css';
nprogress.configure({ showSpinner: false });
This time the nprogress.js file itself loads (it doesn't use @theme/*), but its sibling import './nprogress.css' fails because Node's ESM loader has no .css extension handler.
Patch I prototyped: broaden the catch to swallow ERR_UNKNOWN_FILE_EXTENSION too.
Unblocks → next failure.
5. Cannot find module '@theme/DocsRoot' (from inside the bundle, not the shim)
This is where my patches stop working. Once the client-modules registry stops crashing, SSG starts rendering routes — and routes ultimately need theme components like @theme/DocsRoot, @theme/DocItem, @theme/MDXPage, @theme/DocVersionRoot, @theme/DocRoot. These appear in the bundle as literal require("@theme/DocsRoot") calls inside route-registry thunks:
"5e95c892":[
()=>Promise.resolve().then(()=>_interopRequireWildcard(require("@theme/DocsRoot"))),
"@theme/DocsRoot",
require.resolveWeak("@theme/DocsRoot")
],
Unlike the client-modules registry (which is side-effect-only and tolerable as {}), these theme components are the actual rendering targets. Returning {} here would render empty pages. There's no semantically-correct shim — webpack must resolve these aliases at bundle time so the theme components are inlined into server.bundle.js.
This is the same root cause as failure #2 (webpack treating absolute-path / aliased requires as commonjs externals on the server target), but the fix has to be in the webpack config, not in a runtime require shim.
Where the trail goes cold
I did not figure out why webpack 5 with target: 'node...' is keeping these requires as externals. Things I checked:
- The server config (
@docusaurus/core/lib/webpack/server.js) sets target: \node${NODE_MAJOR}.${NODE_MINOR}`andoutput.libraryTarget: 'commonjs2'. There is no explicit externals` config.
- The base config (
lib/webpack/base.js) has resolve.alias: { '@theme': themeAliases, '@site': siteDir, '@generated': generatedFilesDir, ... }.
- The CSS rule applies on both client and server; on server
getStyleLoaders returns [null-loader] for non-module CSS (@docusaurus/bundler/lib/loaders/styleLoader.js).
- The bundle output (
build/__server/server.bundle.js) shows webpack-bundled modules with __webpack_require__(...) for normal imports, but the route-registry and client-modules entries keep literal require(...) calls.
The pattern (literal require() for alias-resolved paths inside () => ... thunks) suggests webpack's static analysis isn't reaching into the thunks. But:
- Plain
require("@theme/Foo") at module top-level should resolve via the alias — it doesn't here either.
require.resolveWeak("@theme/Foo") is a webpack-runtime intrinsic that should compile to a chunk ID lookup.
Possible avenues someone fresh on this should try, in rough order of likely payoff:
- Use
configureWebpack to force-bundle the route-registry / client-modules. From docusaurus.config.mjs we can supply a webpack overlay that adds e.g. externals: [] or resolve.alias overrides, or that swaps null-loader for a stub-string loader, or that explicitly opts out of any commonjs externals heuristic. Worth diffing a fresh npx create-docusaurus@latest site's server.bundle.js against ours to see if theirs has these as __webpack_require__ calls — if so, something in our wiring is regressing the bundling.
future.experimental_faster.rspackBundler: true (and friends). Docusaurus 3.x has an experimental_faster feature that swaps webpack for rspack. rspack may handle these aliases differently and either fix the issue outright or surface a different error that's more tractable.
@act-spec/plugin-docusaurus interaction. Our example loads the ACT plugin via actDocusaurusPlugin in docusaurus.config.mjs. If the plugin (or resolveConfig from @act-spec/plugin-docusaurus) injects a webpack hook that interferes with alias resolution, dropping it temporarily and rerunning docusaurus build would isolate that.
- Vanilla
npx create-docusaurus@latest repro. Cheapest way to localize whether this is repo-specific or upstream-broken on Node 22. If a fresh skeleton breaks identically, file upstream; if it builds, bisect against our example.
- Node version. Docusaurus 3.x officially supports Node 18+ but most of their CI runs on 20. Trying
nvm use 20 (or even 18) on an unmodified repo would tell us whether this is purely a Node-22-specific regression.
I'd start with (4) — a 5-minute test that either redirects this upstream or proves it's local.
What I shipped instead
In #12 (feat(examples): deploy ecommerce-catalog + nextjs-marketing as real Pages sites):
- Reverted my
docusaurus.config.mjs edit (baseUrl: PAGES_BASE) — harmless when env is unset, but pointless without a working docusaurus build to honor it.
- Reverted the package.json bump to 3.10.1 (kept on 3.6.3 to match
@docusaurus/preset-classic).
- Removed all the
pnpm patch artefacts (patches/@docusaurus__core@*.patch, pnpm.patchedDependencies).
- Updated
spec/v0.2/generators/docusaurus.md and spec/v0.2/adapters/markdown.md callouts to flag docusaurus-docs as CI-only with a one-liner pointing at this issue.
The four prototype patches are not in the tree. They lived in the pnpm patch staging dir during debugging and were committed to the repo only as a temporary patches/ file that I later removed. If anyone wants to resume from where I stopped, the relevant code blocks are reproduced in the failure-cascade sections above.
Done when
Recorded in #12. Linked spec doc: spec/v0.2/generators/docusaurus.md.
Summary
examples/docusaurus-docscannot rundocusaurus buildend-to-end under Node 22 in this monorepo, which is why it is CI-gated only — itspnpm conformancescript stops at the ACT artefacts (tsx scripts/build.ts && tsx scripts/validate.ts) and never invokesdocusaurus build. As a result,examples/docusaurus-docs/is intentionally not inscripts/assemble-examples.mjs#EXAMPLESand not deployed to GitHub Pages, while the five other site-emitting examples (astro-docs,ecommerce-catalog,eleventy-blog,nextjs-marketing,starlight-docs,vitepress-docs) are.This issue records the failure modes I hit while attempting to make
docusaurus buildwork, the patches I prototyped, and where the trail goes cold — so the next person picking this up doesn't repeat the loop. Closing this issue means:docusaurus buildruns cleanly during conformance, the example deploys tohttps://act-spec.org/examples/docusaurus-docs/, and the spec-doc callout inspec/v0.2/generators/docusaurus.mdflips back from "Reference example. …not deployed yet" to "Live example. …deployed at /examples/docusaurus-docs/".Repro
Environment where the failure was observed:
v22.19.010.17.1@docusaurus/core@3.6.3(current pin) and@docusaurus/core@3.10.1(latest)webpack@5.95.0(current pin) andwebpack@5.106.2(latest)Failure cascade
There are at least four distinct failures stacked behind each other. Each one was unblocked by a patch; the next one then surfaced.
1.
TypeError: require.resolveWeak is not a functionRoot cause. Docusaurus's SSG path reads the webpack-bundled
build/__server/server.bundle.jsfrom disk and evaluates it via theevalpackage against a customrequireshim built inlib/ssg/ssgNodeRequire.js#createSSGRequire. The shim copiesresolve,cache,extensions, andmainfrom aModule.createRequire(serverBundlePath)but does not defineresolveWeak. The webpack server bundle keeps literalrequire.resolveWeak("…")calls in its route registry (the third tuple entry in each__WEBPACK_DEFAULT_EXPORT__lazy-route record), so when the SSG executes the bundle those calls land on the shim and throw.Patch I prototyped (in
node_modules/.../ssgNodeRequire.jsviapnpm patch):resolveWeakreturns an opaque module identifier the loader registry treats as a key — webpack's own runtime returns a chunk ID; returning the input string verbatim is enough for the SSG path. (Aliasing torealRequire.resolvewas wrong: it triggers Node's resolver against webpack aliases like@site/..., which thenMODULE_NOT_FOUND.)Unblocks → next failure.
2. CSS files reach Node's
require:SyntaxError: Unexpected token ':'Root cause. Docusaurus's
genClientModules(inlib/server/codegen/codegen.js) generates.docusaurus/client-modules.jswith literal absolute-pathrequire()calls:Webpack bundles this file but keeps the
require()calls literal — they appear unchanged inserver.bundle.js(module index3826in the build I inspected). Webpack 5 withtarget: 'node22.19'is treating these absolute-path requires as commonjs externals, even though the project's webpack config has CSS rules that should null-loader them on the server. (I never figured out exactly why webpack externalized them — see "Where the trail goes cold" below.)On the client these would go through
mini-css-extract-plugin. On the server,@docusaurus/bundler/lib/loaders/styleLoader.jsreturns[{ loader: require.resolve('null-loader') }]for non-module CSS — but the literalrequire()calls inclient-modules.jsbypass that pipeline entirely.Patch I prototyped:
Mirrors null-loader's no-op behaviour.
Unblocks → next failure.
3.
@theme/*aliased imports inside theme-classic client bootstraps:Cannot find package '@theme/prism-include-languages'Root cause.
theme-classic/lib/prism-include-languages.jsis:When the SSG bundle requires this file, Node loads the JS source directly — Node has no idea what the webpack alias
@theme/prism-include-languagesmeans, so it throwsERR_MODULE_NOT_FOUND. The file is a client-only side-effect (configures Prism syntax-highlight extensions on the browser-side Prism instance) so it has zero contribution to SSG'd HTML.Patch I prototyped:
Unblocks → next failure.
4. Sibling
.cssimport insidenprogress.js:ERR_UNKNOWN_FILE_EXTENSIONnprogress.jsis:This time the
nprogress.jsfile itself loads (it doesn't use@theme/*), but its siblingimport './nprogress.css'fails because Node's ESM loader has no.cssextension handler.Patch I prototyped: broaden the catch to swallow
ERR_UNKNOWN_FILE_EXTENSIONtoo.Unblocks → next failure.
5.
Cannot find module '@theme/DocsRoot'(from inside the bundle, not the shim)This is where my patches stop working. Once the client-modules registry stops crashing, SSG starts rendering routes — and routes ultimately need theme components like
@theme/DocsRoot,@theme/DocItem,@theme/MDXPage,@theme/DocVersionRoot,@theme/DocRoot. These appear in the bundle as literalrequire("@theme/DocsRoot")calls inside route-registry thunks:Unlike the client-modules registry (which is side-effect-only and tolerable as
{}), these theme components are the actual rendering targets. Returning{}here would render empty pages. There's no semantically-correct shim — webpack must resolve these aliases at bundle time so the theme components are inlined intoserver.bundle.js.This is the same root cause as failure #2 (webpack treating absolute-path / aliased requires as commonjs externals on the server target), but the fix has to be in the webpack config, not in a runtime require shim.
Where the trail goes cold
I did not figure out why webpack 5 with
target: 'node...'is keeping these requires as externals. Things I checked:@docusaurus/core/lib/webpack/server.js) setstarget: \node${NODE_MAJOR}.${NODE_MINOR}`andoutput.libraryTarget: 'commonjs2'. There is no explicitexternals` config.lib/webpack/base.js) hasresolve.alias: { '@theme': themeAliases, '@site': siteDir, '@generated': generatedFilesDir, ... }.getStyleLoadersreturns[null-loader]for non-module CSS (@docusaurus/bundler/lib/loaders/styleLoader.js).build/__server/server.bundle.js) shows webpack-bundled modules with__webpack_require__(...)for normal imports, but the route-registry and client-modules entries keep literalrequire(...)calls.The pattern (literal
require()for alias-resolved paths inside() => ...thunks) suggests webpack's static analysis isn't reaching into the thunks. But:require("@theme/Foo")at module top-level should resolve via the alias — it doesn't here either.require.resolveWeak("@theme/Foo")is a webpack-runtime intrinsic that should compile to a chunk ID lookup.Possible avenues someone fresh on this should try, in rough order of likely payoff:
configureWebpackto force-bundle the route-registry / client-modules. Fromdocusaurus.config.mjswe can supply a webpack overlay that adds e.g.externals: []orresolve.aliasoverrides, or that swapsnull-loaderfor a stub-string loader, or that explicitly opts out of any commonjs externals heuristic. Worth diffing a freshnpx create-docusaurus@latestsite'sserver.bundle.jsagainst ours to see if theirs has these as__webpack_require__calls — if so, something in our wiring is regressing the bundling.future.experimental_faster.rspackBundler: true(and friends). Docusaurus 3.x has anexperimental_fasterfeature that swaps webpack for rspack. rspack may handle these aliases differently and either fix the issue outright or surface a different error that's more tractable.@act-spec/plugin-docusaurusinteraction. Our example loads the ACT plugin viaactDocusaurusPluginindocusaurus.config.mjs. If the plugin (orresolveConfigfrom@act-spec/plugin-docusaurus) injects a webpack hook that interferes with alias resolution, dropping it temporarily and rerunningdocusaurus buildwould isolate that.npx create-docusaurus@latestrepro. Cheapest way to localize whether this is repo-specific or upstream-broken on Node 22. If a fresh skeleton breaks identically, file upstream; if it builds, bisect against our example.nvm use 20(or even 18) on an unmodified repo would tell us whether this is purely a Node-22-specific regression.I'd start with (4) — a 5-minute test that either redirects this upstream or proves it's local.
What I shipped instead
In #12 (
feat(examples): deploy ecommerce-catalog + nextjs-marketing as real Pages sites):docusaurus.config.mjsedit (baseUrl: PAGES_BASE) — harmless when env is unset, but pointless without a workingdocusaurus buildto honor it.@docusaurus/preset-classic).pnpm patchartefacts (patches/@docusaurus__core@*.patch,pnpm.patchedDependencies).spec/v0.2/generators/docusaurus.mdandspec/v0.2/adapters/markdown.mdcallouts to flagdocusaurus-docsas CI-only with a one-liner pointing at this issue.The four prototype patches are not in the tree. They lived in the pnpm patch staging dir during debugging and were committed to the repo only as a temporary
patches/file that I later removed. If anyone wants to resume from where I stopped, the relevant code blocks are reproduced in the failure-cascade sections above.Done when
pnpm -F @act-spec/example-docusaurus-docs run build:siteexits 0 on Node 22 + Linux (CI runner) and macOS (dev).examples/docusaurus-docs/build/contains a real Docusaurus HTML site (anindex.html,docs/<route>/index.htmlfor every route in the corpus, and the ACT artefacts copied fromstatic/).scripts/assemble-examples.mjs#EXAMPLESpicks updocusaurus-docs(withdist: 'build'andhumanUrlStrategy: 'prefix').examples/docusaurus-docs/package.json#scripts.conformancechains… && pnpm run build:site && tsx scripts/validate.tsso CI gates on the full pipeline.examples/docusaurus-docs/docusaurus.config.mjshonorsACT_PAGES_BASE(the env-awarebaseUrlchange is a one-liner; reintroduce when build:site works).apps/website/src/pages/examples.astro#DEPLOYED_SLUGSincludesdocusaurus-docs..github/workflows/pages.yml's "Build deployable examples" step addsACT_PAGES_BASE=/examples/docusaurus-docs/ pnpm -F @act-spec/example-docusaurus-docs conformance.spec/v0.2/generators/docusaurus.mdandspec/v0.2/adapters/markdown.mdcallouts revert to the deployed wording; this issue is referenced in the closing PR.https://act-spec.org/examples/docusaurus-docs/loads the rendered docs site, and/examples/docusaurus-docs/.well-known/act.jsonresolves with rebased URL templates.Recorded in #12. Linked spec doc:
spec/v0.2/generators/docusaurus.md.