Skip to content

SSR external packages with entries break when not hoisted #9926

@mattcompiles

Description

@mattcompiles

Describe the bug

When marking a package as an SSR external, it should also mark any sub-entry points of the same module as external.
While it's not explicitly documented, this is what I believe the intended behaviour is from reading prior issues and the source code.

e.g.

// vite.config.js
export default {
  ssr: {
    external: ['lodash']
  }
}
// should be external
import lodash from 'lodash';

// should also be external
import omit from 'lodash/omit';

Unfortunately, this feature (sub-entry externals) doesn't work when the package being marked as external isn't a top-level package inside node_modules. Package managers that use hoisting likely get around this issue, however pnpm and other package manages that don't hoist seem to break this feature.

In practice, this means that the behaviour of ssr.external is different depending on whether packages are top-level dependencies or not. Due to the way ssr.noExternal works, using ssr.external on nested deps is essential in more complex scenarios.

After debugging the source code, this issue seems to occur because import specifiers that exactly match the external are treated differently to package matches. Package matches have to go through a node resolve check in order to be externalised, however the resolve check runs from the project root, therefore always failing for non top-level packages.

Excerpt from the source code:

// copied from https://github.com/vitejs/vite/blob/9ac5075825212e5f4f3b262225ff4a3e46b8e8d1/packages/vite/src/node/ssr/ssrExternal.ts#L158

  // Returns true if it is configured as external, false if it is filtered
  // by noExternal and undefined if it isn't affected by the explicit config
  return (id: string) => {
    const { ssr } = config
    if (ssr) {
      if (
        // If this id is defined as external, force it as external
        // Note that individual package entries are allowed in ssr.external
        ssr.external?.includes(id)
      ) { 
        return true // <-- straight external match doesn't require more validation
      }
      const pkgName = getNpmPackageName(id)
      if (!pkgName) {
        return isExternalizable(id)
      }
      if (
        // A package name in ssr.external externalizes every
        // externalizable package entry
        ssr.external?.includes(pkgName)
      ) {
        return isExternalizable(id, true) // <-- package entry external match requires resolve check for some reason?
      }
      if (typeof noExternal === 'boolean') {
        return !noExternal
      }
      if (noExternalFilter && !noExternalFilter(pkgName)) {
        return false
      }
    }
    return isExternalizable(id)
  }

I have validated that changing the package entry check above to immediately return true rather than going through isExternalizable fixes this issue. Unless this breaks other Vite behaviour, I'd suggest that would be the easiest fix and I'd be happy to submit a PR doing this.

Alternatively, the node resolve algorithm inside isExternalizable could resolve from the dependent package location rather than the project root.

I think this issue is related to but not the same as #9710.

Reproduction

https://github.com/mattcompiles/vite-ssr-external-repro

System Info

System:
    OS: macOS 12.5
    CPU: (10) arm64 Apple M1 Pro
    Memory: 609.50 MB / 32.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 16.15.0 - ~/.volta/tools/image/node/16.15.0/bin/node
    npm: 8.5.5 - ~/.volta/tools/image/node/16.15.0/bin/npm
    pnpm: 7.9.5
  Browsers:
    Chrome: 104.0.5112.101
    Firefox: 104.0.1
    Safari: 15.6
  npmPackages:
    @vitejs/plugin-react: ^2.0.0 => 2.0.0
    vite: ^3.0.9 => 3.0.9

Used Package Manager

pnpm

Logs

Click to expand!
> ssr-external-repro@0.0.0 test /Users/mjones/projects/ssr-external-repro
> node test

node:internal/process/esm_loader:94
    internalBinding('errors').triggerUncaughtException(
                              ^

ReferenceError: require is not defined
    at eval (/node_modules/.pnpm/lodash@4.17.21/node_modules/lodash/mapValues.js:3:36)
    at instantiateModule (file:///Users/mjones/projects/ssr-external-repro/node_modules/.pnpm/vite@3.0.9/node_modules/vite/dist/node/chunks/dep-0fc8e132.js:50548:15)

Validations

Metadata

Metadata

Assignees

No one assigned

    Labels

    feat: ssrp3-minor-bugAn edge case that only affects very specific usage (priority)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions