vitejs / vite

Next generation frontend tooling. It's fast!
http://vitejs.dev
MIT License
66.07k stars 5.9k forks source link

Vite build replaces peer dependencies with {} object when they are missing, leading to subsequent build failure #15733

Open rxliuli opened 5 months ago

rxliuli commented 5 months ago

Describe the bug

Original issue: https://github.com/preactjs/preset-vite/issues/101

I discovered this while using the @preact/preset-vite plugin. After accidentally deleting preact-render-to-string, I encountered some strange errors during the build, instead of being directly informed that this dependency was missing.

[preact:prerender] d is not a function
✓ built in 120ms
error during build:
TypeError: d is not a function

vite configuration

import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'

export default defineConfig({
  plugins: [
    preact({
      prerender: {
        enabled: true,
        renderTarget: '#app',
      },
    }),
  ],
})

src/index.tsx

import { hydrate, prerender as ssr } from 'preact-iso'

export function App() {
  return <div>hello world</div>
}

if (typeof window !== 'undefined') {
  hydrate(<App />, document.getElementById('app'))
}

export async function prerender(data) {
  return await ssr(<App {...data} />)
}

Due to the vague error messages, it took me a while to realize the absence of preact-render-to-string. After checking the implementation of @preact/preset-vite and preact-iso, I found that the code built by vite seems to have some incorrect transformations. For example, the following code in preact-iso

// src/index.js
export function prerender(vnode, options) {
  return import('./prerender.js').then((m) => m.default(vnode, options))
}

// src/prerender.js
import renderToString from 'preact-render-to-string'
// others code...
export default async function prerender() {
  // others code...
  return renderToString()
}

gets transformed into

// prerender-xB4rff8V.js
import { y, E, l } from './index-pBnkxBXA.js'
const renderToString = {}
// others code...
async function prerender() {
  // others code...
  return renderToString()
}

I can even reproduce this without the @preact/preset-vite plugin, just by modifying the vite configuration

import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'

export default defineConfig({
  build: {
    minify: false,
    rollupOptions: {
      input: ['./src/index.tsx'],
      preserveEntrySignatures: 'allow-extension',
    },
  },
})

In dist/prerender-xB4rff8V.js, you can see renderToString is transformed into an empty object, which causes subsequent errors.


I'm not sure if the specific error is vite, rollup, or the preact-vite plugin, but the error being thrown when the peer dependency is not found is incorrect.

Related discussions: https://github.com/vitejs/vite/discussions/15725#discussioncomment-8270527

Reproduction

https://github.com/rxliuli/preact-ssg-demo.git

Steps to reproduce

git clone https://github.com/rxliuli/preact-ssg-demo.git
cd preact-ssg-demo
pnpm i
pnpm vite build --mode test

System Info

System:
    OS: macOS 14.3
    CPU: (12) arm64 Apple M2 Max
    Memory: 10.22 GB / 64.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.10.0 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.2.3 - /usr/local/bin/npm
    pnpm: 8.14.3 - /usr/local/bin/pnpm
    Watchman: 2023.08.14.00 - /opt/homebrew/bin/watchman
  Browsers:
    Brave Browser: 115.1.56.11
    Chrome: 121.0.6167.85
    Safari: 17.3
  npmPackages:
    rollup: ^4.9.6 => 4.9.6 
    vite: ^5.0.12 => 5.0.12

Used Package Manager

pnpm

Logs

No response

Validations

hi-ogawa commented 5 months ago

While looking into further, I realized that this "silently empty object" behavior only applies to default import. If it were named import, vite/rollup build would "correctly" fails.

https://stackblitz.com/edit/github-wbqa6s-e9evwq?file=some-dep%2Findex.js

"noSuchLib" is not exported by "__vite-optional-peer-dep:no-such-lib:some-dep", imported by "node_modules/.pnpm/file+some-dep/node_modules/some-dep/index.js".
file: /home/projects/github-wbqa6s/node_modules/.pnpm/file+some-dep/node_modules/some-dep/index.js:2:9
1: // ...
2: import { noSuchLib } from 'no-such-lib';
            ^

There is another export default {} fallback behavior, which is used for builtin module import (e.g. import fs from "node:fs). In this case, Vite has a warning during resolveId phase:

https://stackblitz.com/edit/github-wbqa6s?file=src%2Findex.js

[plugin:vite:resolve] Module "node:fs" has been externalized for browser compatibility, imported by "/home/projects/github-wbqa6s/src/index.js". See https://vitejs.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.

https://github.com/vitejs/vite/blob/3c9cab6912dc627a22abba4c3fd6074166f5cc4c/packages/vite/src/node/plugins/resolve.ts#L428-L432

Reading how this builtin warning is introduced, it looks like the same story should apply to missing peer dep as well: