Skip to content

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.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories