Build1 publisher3 min readPublished
A missing package.json line broke every pnpm user of a React dashboard template
ViteDash 2.2 declared a dependency npm's flat node_modules had been hiding, then found its own manual chunk config had made first paint worse than shipping one big file.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction
What happened
- ViteDash is a free admin dashboard template built with React 19, Vite 8 and Ant Design 6; version 2.2 was released.
- The template has twenty something pages and light and dark mode implemented through Ant Design's theme algorithm rather than CSS overrides.
- The production build was a single chunk: dist/assets/index-BkAqHA3M.js at 1,620,471 bytes.
- @ant-design/icons was imported in 22 files and was not listed in package.json.
- The undeclared import works for npm users because npm flattens node_modules and antd depends on the icons package, so the import resolves through a copy sitting at the top level.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
ViteDash, a free admin template built on React 19, Vite 8 and Ant Design 6, shipped version 2.2 with two repairs: a dependency it had never declared, and a production build that was one 1,620,471-byte JavaScript file [1][3]. The install bug is the one to copy into your own checklist, because npm's flat node_modules hid it from the maintainer and from every npm user while breaking pnpm and Yarn PnP users outright [5][6].
According to the maintainer's write-up on dev.to, `@ant-design/icons` was imported in 22 source files and was absent from package.json [4]. Under npm the import resolves anyway: npm flattens node_modules, antd itself depends on the icons package, and the import finds a copy sitting at the top level [5]. pnpm keeps a strict node_modules where a package can only import what it declared, and Yarn PnP behaves the same way [6]. Users who cloned, installed and ran dev got `Failed to resolve import "@ant-design/icons"` [6]. The README recommended pnpm [7]. The fix was one line in package.json [8]; the detection cost is one install run under a strict resolver, which is the maintainer's own conclusion: a transitive dependency you never asked for is not a dependency you have [9].
The bundle story is the more interesting failure. With everything in one chunk, the sign-in screen waited on the Kanban board, the invoice drawer and every other page before it painted [10]. Route-level lazy imports, with the Suspense boundary inside the shell rather than around it so the sidebar and header stay on screen, fixed that [11]. Then came the advice from every "Vite bundle too large" thread: a `manualChunks` function grouping React, antd, icons and charts into named vendor chunks [12]. The chunk list looked tidy. Summing the files named in the built index.html's modulepreload tags gave 2,086 kB raw and 643 kB gzipped before first paint [13], roughly 29 percent more raw bytes than the single unsplit chunk it replaced [3].
The mechanism is worth understanding before you write your own version. `manualChunks` is a hard instruction, not a hint, and a chunk is eager if any module in it is reachable from the entry [14]. The app shell imports Layout, Menu and Button, so the antd chunk went eager and carried Table, Splitter and Calendar along with it [14]. Recharts is imported only by the Charts page, but naming it as a chunk pulled 427 kB of chart library into the entry graph for people who never opened a chart [15]. Deleting the config dropped the eager set to 1,004 kB raw across 12 files and 324 kB gzipped [16], about 52 percent less raw and 50 percent less gzipped [1][2].
The default behaviour is already the behaviour most apps want: a module reachable from the entry lands in the entry chunk, a module used by two lazy routes lands in a shared chunk that loads when either does, and a module used by one lazy route lands in that route's chunk [17]. The maintainer's remaining case for `manualChunks` is narrow: group a library when the analyzer shows it genuinely duplicated across many chunks, because you measured rather than because vendor splitting is considered good practice [18].
Two checks fall out of this for anyone shipping code other people install. Add a pnpm or Yarn PnP install to CI, so "works on my machine" means "resolves without hoisting". And judge a chunking change by the eager modulepreload set in the built HTML, not by the shape of the chunk list.