Skip to content

Building from source fails when node_modules is hoisted: hardcoded paths shim for structured-headers #2362

Description

@LukasGoTom

Building from source fails when node_modules is hoisted: hardcoded paths shim for structured-headers

Package: @adcp/sdk@11.2.0 (building from source, repo consumed as a git submodule)
Failure: tscTS2307: Cannot find module 'structured-headers'

Summary

tsconfig.json uses legacy "moduleResolution": "node", which cannot read exports maps. structured-headers@2 exposes its entry points only via an exports map (no top-level main/types fallback):

// node_modules/structured-headers/package.json
{
  "type": "module",
  "exports": {
    "import": "./dist/index.js",
    "require": "./cjs/index.cjs"
  }
  // no "main", no "types"
}

So under node10 resolution TypeScript can't resolve it at all, and the repo works around that with a hardcoded paths entry:

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "paths": {
      "structured-headers": ["./node_modules/structured-headers/cjs/index.d.cts"]
    }
  }
}

That path assumes structured-headers is physically installed at ./node_modules relative to the repo root. In any layout where dependencies hoist — npm workspaces, a monorepo, the repo vendored as a git submodule inside a workspace — the package lands in an ancestor node_modules (e.g. ../../node_modules/structured-headers), the shim points at a directory that doesn't exist, and npm run build:lib fails with TS2307.

Reproduction

mkdir demo && cd demo
cat > package.json <<'EOF'
{ "name": "demo", "private": true, "workspaces": ["vendor/adcp-sdk"] }
EOF
git clone <this repo> vendor/adcp-sdk
npm install                      # deps hoist to demo/node_modules
cd vendor/adcp-sdk && npm run build:lib
# → error TS2307: Cannot find module 'structured-headers' or its corresponding type declarations.

A standalone checkout doesn't hit this because npm install puts the package exactly where the shim expects.

A warning about the tempting wrong fix

Our first attempt was to switch moduleResolution to "bundler" (with "module": "es2022"), which makes the resolution error go away — but it changes the emit: tsc then outputs ES-module syntax with extensionless specifiers into a package declared "type": "commonjs". That output loads fine through a bundler but is not loadable by Node's native loader, and we ended up post-processing dist/ with a fix-up script on every build. Don't go that way; the current CommonJS emit is fine — only the resolution mode is the problem.

Suggested fixes

  1. Preferred: "module": "node16" + "moduleResolution": "node16". Node16 resolution reads exports maps natively, so structured-headers resolves from wherever it's installed and the paths shim can be deleted outright. Because package.json has "type": "commonjs", the emit stays exactly the CJS you ship today. One caveat: node16 mode will (correctly) flag any ESM-only dependency that's synchronously imported from the CJS build — those are latent runtime hazards under require() anyway, so surfacing them is a feature.

  2. Minimal band-aid: multiple paths candidates. TypeScript tries the array in order, so listing the hoisted location as a fallback keeps both layouts working with no other change:

    "paths": {
      "structured-headers": [
        "./node_modules/structured-headers/cjs/index.d.cts",
        "../../node_modules/structured-headers/cjs/index.d.cts"
      ]
    }

    This is what we currently carry in our fork; it works, but it's still guessing at install layouts (a deeper workspace nesting or pnpm layout would need more entries).

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