Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Module resolution: "node" condition does not work when using moduleResolution=bundler #61357

Open
Borewit opened this issue Mar 5, 2025 · 4 comments

Comments

@Borewit
Copy link

Borewit commented Mar 5, 2025

Demo Repo

https://github.com/Borewit/typescript-bundler-with-node-node-condition

Which of the following problems are you reporting?

The module specifier resolves at runtime, but not at build time

Demonstrate the defect described above with a code sample.

import { parseFile } from 'music-metadata';

Run tsc --showConfig and paste its output here

{
    "compilerOptions": {
        "inlineSources": false,
        "module": "esnext",
        "moduleResolution": "bundler",
        "target": "es2020",
        "esModuleInterop": true,
        "baseUrl": "./",
        "allowSyntheticDefaultImports": true,
        "resolvePackageJsonExports": true,
        "resolvePackageJsonImports": true,
        "resolveJsonModule": true
    },
    "files": [
        "./test.ts"
    ]
}

Run tsc --traceResolution and paste its output here

test.ts:1:9 - error TS2305: Module '"music-metadata"' has no exported member 'parseFile'.

1 import {parseFile} from 'music-metadata';
          ~~~~~~~~~


Found 1 error in test.ts:1

Paste the package.json of the importing module, if it exists

{
  "name": "typescript-bundler-with-node-node-condition",
  "version": "0.1.0",
  "description": "Reproduct issue with node condition and module-resultion set to bundler",
  "main": "index.js",
  "scripts": {
    "compile": "tsc"
  },
  "author": "Borewit",
  "license": "MIT",
  "dependencies": {
    "music-metadata": "^11.0.0",
    "typescript": "^5.8.2"
  },
  "devDependencies": {
    "@types/node": "^22.13.9"
  }
}

Paste the package.json of the target module, if it exists

{
  "name": "music-metadata",
  "description": "Music metadata parser for Node.js, supporting virtual any audio and tag format.",
  "version": "11.0.0",
  "author": {
    "name": "Borewit",
    "url": "https://github.com/Borewit"
  },
  "funding": [
    {
      "type": "github",
      "url": "https://github.com/sponsors/Borewit"
    },
    {
      "type": "buymeacoffee",
      "url": "https://buymeacoffee.com/borewit"
    }
  ],
  "type": "module",
  "exports": {
    "node": {
      "import": "./lib/index.js",
      "module-sync": "./lib/index.js",
      "types": "./lib/index.d.ts"
    },
    "default": {
      "import": "./lib/core.js",
      "module-sync": "./lib/core.js",
      "types": "./lib/core.d.ts"
    }
  },
  "types": "lib/index.d.ts",
  "files": [
    "lib/**/*.js",
    "lib/**/*.d.ts"
  ],
  "keywords": [
    "music",
    "metadata",
    "meta",
    "audio",
    "tag",
    "tags",
    "duration",
    "MusicBrainz",
    "Discogs",
    "Picard",
    "ID3",
    "ID3v1",
    "ID3v2",
    "m4a",
    "m4b",
    "mp3",
    "mp4",
    "Vorbis",
    "ogg",
    "flac",
    "Matroska",
    "WebM",
    "EBML",
    "asf",
    "wma",
    "wmv",
    "ape",
    "MonkeyAudio",
    "aiff",
    "wav",
    "WavPack",
    "Opus",
    "speex",
    "musepack",
    "mpc",
    "dsd",
    "dsf",
    "mpc",
    "dff",
    "dsdiff",
    "aac",
    "adts",
    "length",
    "chapter",
    "info",
    "parse",
    "parser",
    "bwf",
    "slt",
    "lyrics"
  ],
  "scripts": {
    "clean": "del-cli 'lib/**/*.js' 'lib/**/*.js.map' 'lib/**/*.d.ts' 'test/**/*.js' 'test/**/*.js.map' 'test/**/*.js' 'test/**/*.js.map' 'doc-gen/**/*.js' 'doc-gen/**/*.js.map'",
    "compile-src": "tsc -p lib",
    "compile-test": "tsc -p test",
    "compile-doc": "tsc -p doc-gen",
    "compile": "yarn run compile-src && yarn compile-test && yarn compile-doc",
    "lint-ts": "biome check",
    "lint-md": "yarn run remark -u remark-preset-lint-consistent .",
    "lint": "yarn run lint-ts && yarn run lint-md",
    "test": "mocha",
    "build": "yarn run clean && yarn compile && yarn run doc-gen",
    "test-coverage": "c8 yarn run test",
    "send-codacy": "c8 report --reporter=text-lcov | codacy-coverage",
    "doc-gen": "yarn node doc-gen/gen.js"
  },
  "dependencies": {
    "@tokenizer/token": "^0.3.0",
    "content-type": "^1.0.5",
    "debug": "^4.4.0",
    "file-type": "^19.6.0",
    "link": "^2.1.1",
    "media-typer": "^1.1.0",
    "strtok3": "^10.2.1",
    "token-types": "^6.0.0",
    "uint8array-extras": "^1.4.0"
  },
  "devDependencies": {
    "@biomejs/biome": "1.9.4",
    "@types/chai": "^5.0.1",
    "@types/chai-as-promised": "^8.0.1",
    "@types/content-type": "^1.1.8",
    "@types/debug": "^4.1.12",
    "@types/media-typer": "^1.1.3",
    "@types/mocha": "^10.0.10",
    "@types/node": "^22.13.4",
    "c8": "^10.1.3",
    "chai": "^5.2.0",
    "chai-as-promised": "^8.0.1",
    "del-cli": "^6.0.0",
    "mime": "^4.0.6",
    "mocha": "^11.1.0",
    "node-readable-to-web-readable-stream": "^0.3.1",
    "remark-cli": "^12.0.1",
    "remark-preset-lint-consistent": "^6.0.1",
    "ts-node": "^10.9.2",
    "typescript": "^5.7.3"
  },
  "engines": {
    "node": ">=18"
  },
  "repository": {
    "type": "https://github.com/borewit/music-metadata.git"
  },
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/Borewit/music-metadata/issues"
  },
  "packageManager": "[email protected]"
}

Any other comments can go here

When the "moduleResolution" resolution is set to "bundler", "node" specific export (depending on that export condition) cannot be imported.

Caused by:

if (moduleResolution !== ModuleResolutionKind.Bundler) {
conditions.push("node");
}

Related issues:

  1. Cannot use parseFile if the TypeScript compiler moduleResolution is set to bundler Borewit/music-metadata#2370
  2. fileTypeFromFile not found sindresorhus/file-type#738

Workaround:

{
  "compilerOptions": {
    "inlineSources": false,
    "module": "esnext",
    "moduleResolution": "bundler",
    "customConditions": ["node"],  // workaround
    "target": "ES2020",
    "esModuleInterop": true,
    "baseUrl": "."
  }
}
@Andarist
Copy link
Contributor

Andarist commented Mar 5, 2025

How else would you be able to disable this condition? What if you bundle exclusively for the browser target?

@Borewit
Copy link
Author

Borewit commented Mar 6, 2025

The node condition is, in principle, a runtime value that indicates whether the code is executed in a Node.js (or compatible) environment. Apparently the TypeScript compiler took a shortcut here, and disabled the node condition when the module-resolution is set to bundler.

What if you bundle exclusively for the browser target?

But what if you don't bundle exclusively for the browser? For instance, Next.js uses the bundler mode even for its Node.js backend.

I would expect the TypeScript compiler to transpile code without altering its runtime behavior. Therefore, I think, the ideal solution would be to derive the node condition from the runtime environment as originally intended.

For scenarios where optimization is needed, such as creating a bundle solely for the browser and removing Node.js-specific functionality, that responsibility should lie with a dedicated bundler, not with the primary role of the TypeScript compiler.

If, for any reason, the node condition must be hard-coded upfront when using the bundler module-resolution, this behavior should be clearly documented as a side effect to alert developers of the potential impact on module resolution.

@Andarist
Copy link
Contributor

Andarist commented Mar 6, 2025

Apparently the TypeScript compiler took a shortcut here, and disabled the node condition when the module-resolution is set to bundler.

I wouldn't say it disabled it. It just didn't enable it. I think it's a somewhat important difference here.

But what if you don't bundle exclusively for the browser? For instance, Next.js uses the bundler mode even for its Node.js backend.

Runtimes can do whatever they want. That's why this mode is flexible. You are in control over all of the conditions that should be used.

I would expect the TypeScript compiler to transpile code without altering its runtime behavior. Therefore, I think, the ideal solution would be to derive the node condition from the runtime environment as originally intended.

How would TypeScript infer what your runtime is doing and what conditions it should enable?

@Borewit
Copy link
Author

Borewit commented Mar 6, 2025

How would TypeScript infer what your runtime is doing and what conditions it should enable?

This correctly pinpoints the core issue: TypeScript cannot infer the runtime environment.

In other words, at compile time, you can only determine that the node condition might evaluate to either true or false.

Therefore, when converting ECMAScript Modules to a bundle, the bundler must account for both scenarios to ensure correct runtime behavior.

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

No branches or pull requests

2 participants