Repository navigation
fix(vuepress): set NODE_ENV before loading bundler in bin shorthands - #1723
Conversation
`vuepress-vite` and `vuepress-webpack` imported the bundler statically, so it was evaluated before the `dev` / `build` command set the default `NODE_ENV`. The bundlers load `vue` at module top level (e.g. `@vuepress/bundlerutils` imports `ssrContextKey` from `vue`, and `@vitejs/plugin-vue` / `vue-loader` load `vue` / `vue/compiler-sfc`), and the CJS entries of `vue` and `@vue/*` pick their development or production builds by `NODE_ENV` when first required. As a result, `vuepress-vite build` and `vuepress-webpack build` ran with the development builds of the Vue runtime and template compiler, while `vue/server-renderer`, imported later for SSR, used its production build, mixing both in one process. SSR was noticeably slower, and the output differed from `vuepress build` with the bundler set in the config file, e.g. the dev compiler kept HTML comments of Markdown pages. Set the default `NODE_ENV` according to the command in these bins, and import the bundler and the cli dynamically afterwards. An explicitly provided `NODE_ENV` is still respected, and other commands and options are left to the cli as before.
There was a problem hiding this comment.
Copilot wasn't able to review any files in this pull request. Check if the Files changed in this pull request are included in default exclusions.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Nice catch, I believe this also fixes #1718. |
|
Thanks for the review! I don't think this PR fixes #1718, though — it actually makes #1718 comes from Verified with a minimal site (webpack bundler, two pages whose
The actual fix for #1718 is in #1725. It would be good to merge it together with or before this PR, so that |
Before submitting the PR, please make sure you do the following
close #123).What is the purpose of this pull request?
Description
Symptom
vuepress-vite buildandvuepress-webpack buildrun the Node side with the development builds of Vue (vue.cjs.js,@vue/runtime-core,@vue/reactivity,@vue/shared,@vue/compiler-core,@vue/compiler-dom), whilevue/server-rendereris loaded as the production build. So dev and prod builds of Vue end up mixed in one process. This has two effects:vuepress buildwith the same bundler set in the config file, because the template compiler that@vitejs/plugin-vue/vue-loaderuse is also the dev build. For example, the dev compiler keeps HTML comments of Markdown pages (commentsdefaults to__DEV__), and emits"v-if"instead of""as the text of the v-if placeholder comment.The
devcommand of these bins is effectively unaffected (the dev builds are the expected ones there), and the plainvuepressbin is not affected at all.Root cause
The shorthand bins import the bundler statically:
Static imports are evaluated before
cli()runs, but the defaultNODE_ENVis only set inside the commands (process.env.NODE_ENV ??= 'production'at the start ofbuild(),??= 'development'indev()). Meanwhile the bundler packages loadvueat module top level:@vuepress/bundlerutilsimportsssrContextKeyfromvue(used by both bundlers);@vitejs/plugin-vueimportscomputed/shallowReffromvue;vue-loaderrequiresvue/compiler-sfc.The CJS entries of
vueand@vue/*(index.js) choose*.cjs.prod.jsor*.cjs.jsbyprocess.env.NODE_ENVwhen they are first required, and the result stays in the module cache. At that momentNODE_ENVis stillundefined, so the dev builds are selected. Later,renderPageToStringdynamically importsvue/server-rendererafterNODE_ENVhas becomeproduction, so the prod server renderer is loaded, while itsrequire('vue')gets the cached dev build.With the plain
vuepressbin, the bundler comes from the user config, which is loaded afterNODE_ENVhas been set, so everything is the production build. That's why only the shorthand bins are affected.Note that making the
vueimport in@vuepress/bundlerutilslazy would not be enough, because@vitejs/plugin-vueandvue-loaderalso load Vue eagerly (and so could any future dependency).How it was detected
On a real site (1253 pages,
vuepress-theme-hope@2.0.0-rc.109,vuepress@2.0.0-rc.31, Vite 8, Node 24, built withvuepress-vite build):--requirepreload wrappingModule._loadshowed that the first load ofvue/index.jshappened withNODE_ENV=undefined; amodule.registerHooks()resolve hook showed that the importers were@vuepress/bundlerutils/dist/index.jsand@vitejs/plugin-vue/dist/index.mjs. At exitrequire.cachecontainedvue/dist/vue.cjs.js,@vue/runtime-core/dist/runtime-core.cjs.js, and bothserver-renderer.cjs.jsandserver-renderer.cjs.prod.js.NODE_ENV=productionalready set (4-core limit): "Rendering 1253 pages" went from 22.2 s / 25.3 s to 14.3 s / 18.6 s (about 30% faster). The generated HTML was the same apart from one page whose entry order also varies between two builds in the same mode (the site setstemplate.compilerOptions.comments: false, so the comment difference described above did not show up there). The only client bundle difference was the v-if placeholder comment text ("v-if"vs"") in a few chunks, i.e. the client build was using the dev template compiler too.The fix
In
vuepress-viteandvuepress-webpack, set the defaultNODE_ENVaccording to the command before loading anything else, then import the bundler and the cli dynamically:This makes
vuepress-vite buildbehave the same asvuepress buildwithbundler: viteBundler()in the config file. Vite's own bin uses the same pattern: it inspectsprocess.argvand sets env vars (DEBUG) before dynamically importing its CLI.Trade-offs / design notes:
??=never overrides an explicitly providedNODE_ENV;devstill defaults todevelopmentandbuildtoproduction; the??=in thedev/buildcommands of@vuepress/cliis kept for Node API users and for the cases below.process.argv[2]is checked on purpose. When it isdev/build, cac is guaranteed to match the same command (it is the first positional argument), so the early default can never be wrong. When options come before the command (e.g.vuepress-webpack --debug build docs, rare) or forinfo/--help/--version, nothing changes compared to now and the cli handles it as before. Parsing argv with cac here would duplicate the command definitions of the cli.@vuepress/cli; a comment asks to keep them in sync. Alternatives considered: lazy-loadingvueinside the bundler packages (insufficient, see above, and fragile); a lazyBundlerproxy (not possible,Bundler.mergeConfigis synchronous); lettingcli()accept a lazy default app config (public API change for the same effect).buildnow uses the production builds of Vue on the Node side, exactly likevuepress build. Visible consequences are the expected production ones: HTML comments in Markdown are no longer rendered, and SSR is faster. One thing reviewers may want to be aware of: bundler-webpack: SSR bundle inlines a second Vue runtime -> 'Cannot redefine property' when rendering multiple pages #1718 (Cannot redefine propertywith@vuepress/bundler-webpack) only reproduces with the production runtime, sovuepress-webpack buildusers who were masked by the dev runtime would now see it, just likevuepress buildusers with the webpack bundler already do. The e2e site builds fine with webpack after this change.vuepresspackage, which has no tests (and is excluded from coverage), and a meaningful test would need to spawn the built bin and hook Node's module loader. It is verified manually as below instead.Verification
Environment: Node 24.21.0, pnpm 11.15.1, Vite 8.1.5, Vue 3.5.40.
pnpm build,pnpm lint(ESLint +oxfmt --check),pnpm check-types,pnpm test:unit(63 files, 666 tests): all pass.NODE_OPTIONS="--require ./vue-env-hook.cjs"), before and after this change. Files markedloaded=falseare entries that Node's ESM-to-CJS export pre-parsing adds torequire.cachewithout executing them; they are left out except in the first block. Output is sorted and paths are shortened.vuepress-vite build docs --clean-cache --clean-temp, before:vuepress-vite build docs --clean-cache --clean-temp, after (identical tovuepress build docs, i.e.pnpm docs:build, before and after):E2E_BUNDLER=webpack vuepress-webpack build docs: before, the first load ofvuealso happened withNODE_ENV=undefinedand the same dev builds plusserver-renderer.cjs.prod.jswere executed; after, the first load happens withNODE_ENV=productionand only*.cjs.prod.jsbuilds are executed (same asvuepress buildwith the webpack bundler). The build succeeds.Output comparison of the e2e site built with
vuepress-vite build(content hashes normalized):markdown/images/images.html), and only in HTML comments that come from the Markdown source and were kept by the dev template compiler; client chunks differ only in the code generated by the dev / prod template compiler (and the resulting hashes).vuepress build: identical except for the order of routes in the app chunk.Rendering time, with 1000 extra copies of
components/route-link.md/components/auto-link.mdadded to the e2e site (1055 pages),taskset -c 0-3, 3 alternating runs each:vuepress-vite buildOther checks with the new bins:
NODE_ENV=development vuepress-vite build docs: the first load ofvuestill seesdevelopmentandvue.cjs.jsis used, i.e. an explicit value is not overridden.vuepress-vite dev docsandE2E_BUNDLER=webpack vuepress-webpack dev docs: the first load ofvuenow seesNODE_ENV=development(before:undefined); the dev servers start and respond with HTTP 200.pnpm docs:devalso still works.vuepress-vite --help,--version,build --help,info,vuepress-webpack --help, and no arguments behave as before.Preload hook used above
Screenshots
N/A