Skip to content

Docusaurus example: docusaurus build fails to deploy as real Pages site (require.resolveWeak + @theme/* alias leaks) #13

@jdforsythe

Description

@jdforsythe

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:

  1. 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.
  2. 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.
  3. @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.
  4. 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.
  5. 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

  • pnpm -F @act-spec/example-docusaurus-docs run build:site exits 0 on Node 22 + Linux (CI runner) and macOS (dev).
  • examples/docusaurus-docs/build/ contains a real Docusaurus HTML site (an index.html, docs/<route>/index.html for every route in the corpus, and the ACT artefacts copied from static/).
  • scripts/assemble-examples.mjs#EXAMPLES picks up docusaurus-docs (with dist: 'build' and humanUrlStrategy: 'prefix').
  • examples/docusaurus-docs/package.json#scripts.conformance chains … && pnpm run build:site && tsx scripts/validate.ts so CI gates on the full pipeline.
  • examples/docusaurus-docs/docusaurus.config.mjs honors ACT_PAGES_BASE (the env-aware baseUrl change is a one-liner; reintroduce when build:site works).
  • apps/website/src/pages/examples.astro#DEPLOYED_SLUGS includes docusaurus-docs.
  • .github/workflows/pages.yml's "Build deployable examples" step adds ACT_PAGES_BASE=/examples/docusaurus-docs/ pnpm -F @act-spec/example-docusaurus-docs conformance.
  • spec/v0.2/generators/docusaurus.md and spec/v0.2/adapters/markdown.md callouts revert to the deployed wording; this issue is referenced in the closing PR.
  • After deploy, https://act-spec.org/examples/docusaurus-docs/ loads the rendered docs site, and /examples/docusaurus-docs/.well-known/act.json resolves with rebased URL templates.

Recorded in #12. Linked spec doc: spec/v0.2/generators/docusaurus.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions