Skip to content

[@rollup/plugin-typescript] { compilerOptions: { module: "nodeNext" } } overwritten when coming from "extends" in tsconfig.json #1583

Closed

Description

@jessevanassen

Expected Behavior

I have two typescript config files: tsconfig.base.json and tsconfig.json.
In the tsconfig.base.json I have { compilerOptions: { module: "nodeNext" } }.
tsconfig.json extends tsconfig.base.json.

When I run the typescript config, I expect that it takes the module: "nodeNext" into account that I configured in the tsconfig.base.ts.

Actual Behavior

The plugin overwrites { compilerOptions: { module: "nodeNext" } } with { compilerOptions: { module: "esNext" } }. This produces the following error with TypeScript 5.2 and up (see also microsoft/TypeScript#54567):

(!) Plugin typescript: @rollup/plugin-typescript TS5110: Option 'module' must be set to 'NodeNext' when option 'moduleResolution' is set to 'NodeNext'.

Additional Information

Cause of the bug

https://github.com/rollup/plugins/blob/typescript-v11.1.3/packages/typescript/src/options/tsconfig.ts#L155-L196

export const DEFAULT_COMPILER_OPTIONS: PartialCompilerOptions = {
  module: 'esnext',
  skipLibCheck: true
};

// ...

ts.parseJsonConfigFileContent(
  {
    ...tsConfigFile,
    compilerOptions: {
      ...DEFAULT_COMPILER_OPTIONS,
      ...tsConfigFile.compilerOptions
    }
  },
  // ...
);

This block gets the JSON parsed tsConfigFile that's used to process the project (in the reproduction, thats ./tsconfig.json), adds the default options to the compilerOptions, and passes it on to TypeScript to completely parse the file, fill in defaults, etc.

However, if the tsconfig.json contains an extends which defines the module, the tsconfig.json ends up like this:

{
  "compilerOptions": {
    module: 'esnext',
    skipLibCheck: true,
    composite: true,
  },
  extends: "./tsconfig.base.json",
}

These compilerOptions take precedence over the one in the tsconfig.base.json, effectively overriding the project's module setting.

Workaround

Move / copy { compilerOptions: { module: "nodeNext" } } to the tsconfig.json file.
This way, ...tsConfigFile.compilerOptions overwrites the property from ...DEFAULT_COMPILER_OPTIONS. However, it does lead to repetition that the extends should solve.

Activity

  1. changed the title [-][typescript] { compilerOptions: { module: "nodeNext" } } overwritten when using "extends" in tsconfig.json[/-] [+][@rollup/plugin-typescript] { compilerOptions: { module: "nodeNext" } } overwritten when using "extends" in tsconfig.json[/+] on Sep 14, 2023
  2. changed the title [-][@rollup/plugin-typescript] { compilerOptions: { module: "nodeNext" } } overwritten when using "extends" in tsconfig.json[/-] [+][@rollup/plugin-typescript] { compilerOptions: { module: "nodeNext" } } overwritten when coming from "extends" in tsconfig.json[/+] on Sep 14, 2023
  3. romainmnr commented on Nov 28, 2023

    @romainmnr

    I'm facing the same issue...

    Move / copy { compilerOptions: { module: "nodeNext" } } to the tsconfig.json file.

    This workaround works, but we loose all the purpose of extending a config...

  4. renke0 commented on Jan 29, 2024

    @renke0

    Explicitly adding it to the plugin declaration also works. Not ideal, but IMO it's better than having to change tsconfig.json.

    plugins: [typescript({ module: 'nodeNext' })]
  5. sam-mfb commented on Jul 29, 2024

    @sam-mfb

    you can also dynamically add it to to the plugin declaration using something like this in a rollup.config.mjs file (or .cjs file with some modifications):

    import {
      parseJsonConfigFileContent,
      sys
    } from "typescript"
    import { readFileSync } from "fs"
    
    const getTsConfig = configFilePath => {
      const configFile = readFileSync(configFilePath, "utf8")
      const configObject = JSON.parse(configFile)
      const configParseResult = parseJsonConfigFileContent(
          configObject,
          sys,
          process.cwd()   )
      return configParseResult
    }
    const configObj = getTsConfig("./tsconfig.rollup.json")
    
    // other stuff
    
    plugins: [typescript({
      module: configObj.options.module === 199 ? "nodeNext" : undefined //199 is what the typescript enum value ModuleKind.NodeNext resolves to
    })]
    

    Presumably some version of the getTsConfig function above could be used to actually fix the readTsConfigFile that is the source of the problem. the basic issue is that it uses ts.readConfigFile() rather than ts.parseJsonConfigFileContent() to parse the tsconfig.json file and so it doesn't handle walking through the extends files. i apologize i don't have time to figure that out and do a PR now.

  6. jelovac commented on Nov 7, 2024

    @jelovac

    Run into this issue as well.

    The affected code is located here:

    I believe that the whole tsconfig.ts module is due for a refactor as its current behavior is not correct and misleading.

    The following discussion might help with resolution: microsoft/TypeScript#44573 . There I found mention of https://github.com/dominikg/tsconfck package which can help with parsing of tsconfig files.

  7. stale commented on Apr 27, 2025

    @stale

    Hey folks. This issue hasn't received any traction for 60 days, so we're going to close this for housekeeping. If this is still an ongoing issue, please do consider contributing a Pull Request to resolve it. Further discussion is always welcome even with the issue closed. If anything actionable is posted in the comments, we'll consider reopening it. ⓘ

  8. added 2 commits that reference this issue on May 29, 2026
    4789ec7
    ce1a9c2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions