Skip to content

vp pack --exe fails with "Failed to import module @tsdown/exe" when @tsdown/exe is installed as documented #1586

@Saeris

Description

@Saeris

Describe the bug

Summary

The Pack documentation advertises standalone executable builds as a supported feature, but using it requires non-obvious workarounds that contradict or go beyond what the docs describe. The root cause is a cascading package resolution problem: @tsdown/exe has a hard peer dependency on tsdown, but Vite+ bundles tsdown internally under @voidzero-dev/vite-plus-core and doesn't expose it as a resolvable top-level package.

While the bug here might better be resolved in the tsdown repository, I'm raising it as an issue here because of the confusing documentation and to flag it as an interop concern so that you can adjust your strategy during this early stage of Vite+'s development. Resolution issues like this could easily pop up in other places within the Vite+ toolchain.

Note

In general, I have observed that LLMs in particular have issues with the way Vite+ bundles together all of these tools, not just tsdown. Claude for example often goes down rabbit holes attempting to resolve particular packages like vitest. Numerous times I've had to teach it workarounds because of this architectural decision. Personally, I don't think it's a bad idea, I just want to report what I have observed is a frequent pain point.

A related example can be observed trying to use @tsdown/css. Moving forward it may be worth investigating how to better evolve these tools together. Food for thought.


What I Tried

  1. Adding "tsdown": "npm:vite-plus@latest" to resolutionsvite-plus does expose ./pack and ./internal subpaths, but its ./internal export is Vite's internal module, not tsdown's. The tsdown/internal subpath required by @tsdown/exe exports different symbols (fsExists, fsRemove, Logger) that are not present in vite-plus/internal.

  2. Adding "tsdown": "0.22.0" to resolutions — Yarn 4's resolutions only overrides packages already in the dependency graph. Since no explicit dependency declares tsdown (it's an unsatisfied peer of @tsdown/exe), the resolution is never applied and tsdown is never installed at the top level.

  3. Using packageExtensions in .yarnrc.yml to mark the peer as optional — This silenced the peer warning but didn't make tsdown resolvable at runtime.

Working workaround: Use yarn patch to modify @tsdown/exe's dist, replacing the tsdown/internal import with inline node:fs/promises equivalents:

-import { fsExists, fsRemove } from "tsdown/internal";
+import { stat, rm } from "node:fs/promises";
+const fsExists = async (p) => { try { await stat(p); return true; } catch { return false; } };
+const fsRemove = async (p) => { await rm(p, { recursive: true, force: true }); };

This works but is fragile — it will break on any @tsdown/exe version bump and requires manually re-running yarn patch after each update.


Suggested Fix

In order of preference:

  1. Export tsdown/internal from vite-plus — add a "./tsdown/internal" (or "./pack/internal") subpath export to vite-plus that re-exports fsExists, fsRemove, and Logger, similar to how vite-plus/pack/client was added as a tsdown/client equivalent in v0.1.21 (#1501). Then document that "tsdown": "npm:vite-plus@latest" in resolutions satisfies the peer.

  2. Bundle @tsdown/exe inside vite-plus-core — the same way tsdown itself is bundled, so users don't need to install or manage @tsdown/exe at all. This would match the zero-config philosophy and eliminate the peer dependency chain entirely.

  3. Document the workaround explicitly — at minimum, the pack docs should warn that @tsdown/exe has a tsdown peer that isn't satisfied by Vite+'s internal bundling, and show the steps needed to work around it for each package manager.

Reproduction

https://github.com/saeris/plex-monitor/tree/e25d8ea2096c040ad201ea51a47527e403b13cf6

Steps to reproduce

I have linked to the repo this occurred in on the commit with the included yarn patch.

To reproduce, open a terminal in the sandbox and run vp pack. Note: the .tar.xz extraction failure seen on Windows (missing xz) will not occur in this environment, but the initial Failed to import module "@tsdown/exe" error will.


Generalized reproduction steps:

  1. Add exe config to vite.config.ts as documented:
    pack: {
      entry: ['src/cli.ts'],
      exe: {
        fileName: 'myapp',
        targets: [
          { platform: 'linux', arch: 'x64', nodeVersion: '26.1.0' },
          { platform: 'win', arch: 'x64', nodeVersion: '26.1.0' },
        ]
      }
    }
  2. Install @tsdown/exe as a dev dependency per tsdown's documentation
  3. Run vp pack

Expected: Executables are built for each target platform.

Actual:

error: Failed to import module "@tsdown/exe". Please ensure it is installed.

This error is misleading — @tsdown/exe is installed. The real failure happens when @tsdown/exe tries to import { fsExists, fsRemove } from "tsdown/internal". Because Vite+ bundles tsdown inside @voidzero-dev/vite-plus-core's dist rather than exposing it as a standalone tsdown package, the import cannot be resolved.

Full error output and stack trace:

ℹ entry: src/cli.ts
ℹ target: node26.1.0
ℹ tsconfig: tsconfig.json
ℹ `exe` option is experimental and may change in future releases.
ℹ Build start
ℹ dist/cli.mjs  35.99 kB │ gzip: 9.86 kB
ℹ 1 files, total: 35.99 kB
✔ Build complete in 38ms
error: Failed to import module "@tsdown/exe". Please ensure it is installed.
    at importWithError (file:///path/to/project/node_modules/@voidzero-dev/vite-plus-core/dist/tsdown/main-BNUO6D3N.js:49:9)
    at async buildExe (file:///path/to/project/node_modules/@voidzero-dev/vite-plus-core/dist/tsdown/build-5FURNVr0-C8s5A3Ji.js:3493:50)
    at async postBuild (file:///path/to/project/node_modules/@voidzero-dev/vite-plus-core/dist/tsdown/build-5FURNVr0-C8s5A3Ji.js:5398:3)
    at async buildSingle (file:///path/to/project/node_modules/@voidzero-dev/vite-plus-core/dist/tsdown/build-5FURNVr0-C8s5A3Ji.js:5331:3)
    at async Promise.all (index 0)
    at async buildWithConfigs (file:///path/to/project/node_modules/@voidzero-dev/vite-plus-core/dist/tsdown/build-5FURNVr0-C8s5A3Ji.js:5282:18)
    at async runBuild (file:///path/to/project/node_modules/vite-plus/dist/pack-bin.js:672:3)
    at async CAC.<anonymous> (file:///path/to/project/node_modules/vite-plus/dist/pack-bin.js:674:2)
    at async runCLI (file:///path/to/project/node_modules/vite-plus/dist/pack-bin.js:680:3)
    at async file:///path/to/project/node_modules/vite-plus/dist/pack-bin.js:687:1

The underlying cause is visible when importing @tsdown/exe directly:

$ node --input-type=module -e "import('@tsdown/exe')"
file:///[eval1]:1
import('@tsdown/exe')
         ^^^^^^^^
Error: Cannot find package 'tsdown' imported from
  node_modules/@tsdown/exe/dist/index.mjs

The offending line in @tsdown/exe/dist/index.mjs:

import { fsExists, fsRemove } from "tsdown/internal";

System Info

Environment:
  Version      26.1.0
  Source       .session-node-version
  Source Path  C:\Users\saeris\.vite-plus\.session-node-version

Tool Paths:
  node  C:\Users\saeris\.vite-plus\js_runtime\node\26.1.0\node.exe
  npm   C:\Users\saeris\.vite-plus\js_runtime\node\26.1.0\npm.cmd
  npx   C:\Users\saeris\.vite-plus\js_runtime\node\26.1.0\npx.cmd



vp v0.1.21

Local vite-plus:
  vite-plus  v0.1.21

Tools:
  vite             v8.0.11
  rolldown         v1.0.0
  vitest           v4.1.5
  oxfmt            v0.48.0
  oxlint           v1.63.0
  oxlint-tsgolint  v0.22.1
  tsdown           v0.22.0

Environment:
  Package manager  yarn v4.14.1
  Node.js          v26.1.0 (.session-node-version)


---

- `@tsdown/exe`: `0.22.0`
- Package manager: Yarn 4.14.1 with `nodeLinker: node-modules`
- OS: Windows 10

Used Package Manager

yarn

Validations

Metadata

Metadata

Assignees

No one assigned

    Type

    Priority

    None yet

    Effort

    None yet

    Target date

    None yet

    Start date

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions