Skip to content

Conversation

jungwoo3490
Copy link
Contributor

@jungwoo3490 jungwoo3490 commented Aug 20, 2025

Problem

I tried to use knip in Yarn PnP environments with this vite.config.ts

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import svgr from "vite-plugin-svgr";

export default defineConfig({
  plugins: [react(), svgr()],
});

and this error occured:

ERROR: Error loading vite.config.ts
Reason: (0, vitePluginSvgr.default) is not a function

This error only occurred in Yarn PnP, and did not appear in Yarn with node_modules or in other package managers that resolve module paths through node_modules.

I analyzed the root cause and found that the failure was due to the following reasons:

  1. knip loads config files using jiti internally:

    // Inside knip's loader
    const config = await jiti.import("./vite.config.ts", { default: true });
  2. Multiple scenarios cause CJS version selection:

    // Scenario 1: Yarn PnP - mlly fails, fallback to require.resolve
    resolved = ctx.nativeRequire.resolve("vite-plugin-svgr");
    // → selects dist/index.cjs instead of dist/index.js
    
    // Scenario 2: Forced require conditions
    const result = await jiti.import("vite-plugin-svgr", {
      default: true,
      conditions: ["require", "node"],
    });
  3. CJS module structure:

    // dist/index.cjs contains:
    module.exports = {
      default: function vitePluginSvgr() {
        /* ... */
      },
      __esModule: true,
    };
  4. jiti's interop logic fails:

    // When Node.js loads CJS via ESM import(), the result is:
    const loaded = { default: function, __esModule: true };
    
    // jiti receives this loaded module and should unwrap it
    // With { default: true }, jiti should return the function directly
    // But current interop logic returns the whole object instead

Solution

The problem lies in jiti's interopDefault Proxy logic. When { default: true } is used, jiti tries to unwrap the default export, but the current logic doesn't handle CJS interop objects properly.

Current logic (broken):

if (prop === "default") {
  return defIsNil ? mod : def; // Returns the whole CJS object
}

Fixed logic:

if (prop === "default") {
  // Detect CJS interop pattern: { default: function, __esModule: true }
  if (
    !defIsNil &&
    typeof def === "object" &&
    typeof def.default === "function"
  ) {
    return def.default; // Unwrap and return the actual function
  }
  return defIsNil ? mod : def; // Keep existing behavior for other cases
}

Result

AS-IS

const plugin = await jiti.import("vite-plugin-svgr", { default: true });
console.log(typeof plugin); // 'object'
console.log(typeof plugin.default); // 'function'
plugin(); // TypeError: plugin is not a function

TO-BE

const plugin = await jiti.import("vite-plugin-svgr", { default: true });
console.log(typeof plugin); // 'function'
plugin(); // Works correctly

Reproduce Bug

You can compare the AS-IS and TO-BE examples here: https://github.com/jungwoo3490/repro-jiti-396

1. Clone the repository

git clone https://github.com/jungwoo3490/repro-jiti-396.git && cd repro-jiti-396

2. Move to the as-is directory, install dependencies, and run knip.

cd as-is && yarn install && yarn knip

then you should see the following error log:

image

3. Move to the jiti directory, install dependencies, and build the project.

The jiti code here has already been modified as described above.

cd ../jiti && pnpm install && pnpn build

4. Move to the to-be directory, install dependencies, and run knip:

The to-be setup uses the modified jiti bundle.

cd ../to-be && yarn install && yarn knip

then you should see the following successful output:

스크린샷 2025-08-20 오후 7 50 38

@pi0
Copy link
Member

pi0 commented Aug 20, 2025

Do you mind to add a test fixture?

@jungwoo3490
Copy link
Contributor Author

jungwoo3490 commented Aug 20, 2025

Do you mind to add a test fixture?

@pi0 I added a test fixture. Can you check it?

@pi0 pi0 changed the title fix: handle CJS function default exports in jiti interop fix(cjs-interop): handle function default exports Sep 22, 2025
@pi0 pi0 merged commit 7aa365b into unjs:main Sep 22, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants