Module resolution and aliases

webpack · Practical · 10 min

Import errors and duplicated packages usually come down to resolution settings.

When webpack meets import x from "something", its resolver (enhanced-resolve) decides which file that is. The resolve options shape that:

  • extensions: suffixes tried for extension-less imports, e.g. [".tsx", ".ts", ".js"]. Keep the list short; every entry costs file-system lookups.
  • alias: map a prefix to a path: "@": path.resolve(__dirname, "src"). A trailing $ makes an exact match (react$). Aliases also force a single copy of a package, e.g. pointing react at one location in a monorepo. Mirror aliases in tsconfig.json paths for TypeScript.
  • modules: directories searched for bare imports, default ["node_modules"].
  • conditionNames and mainFields: which package.json exports conditions and legacy fields are honoured (browser, module, import, require...). The defaults depend on target.
  • fallback: webpack 5 no longer polyfills Node built-ins like crypto or buffer for the browser. Either avoid them, provide polyfills (fallback: { buffer: require.resolve("buffer/") }), or disable with false.

resolve.fullySpecified relates to ESM packages, where relative imports must include the extension; errors like "fully specified" come from .mjs files or "type": "module" packages.

Debug with npx webpack --stats=detailed or by temporarily setting infrastructureLogging: { level: "verbose" }.

Example

// webpack.config.js (excerpt)
const path = require("node:path");

module.exports = {
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
    alias: {
      "@": path.resolve(__dirname, "src"),           // import "@/lib/api"
      react$: path.resolve(__dirname, "node_modules/react"), // one copy in a monorepo
    },
    fallback: {
      buffer: require.resolve("buffer/"),  // polyfill a Node built-in for the browser
      fs: false,                           // never in the browser
    },
  },
};
// tsconfig.json: "compilerOptions": { "paths": { "@/*": ["./src/*"] } }

Remember

  • extensions, alias, modules steer the resolver
  • webpack 5 does not polyfill Node built-ins: use fallback
  • Mirror aliases in tsconfig paths
  • Keep extensions short for speed

Watch out for

  • Expecting require('crypto') to work in browser code as it did with webpack 4's automatic polyfills.

Check yourself

  1. After upgrading to webpack 5, a dependency fails with "Can't resolve 'buffer'". Why?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: webpack.js.org/configuration/resolve/