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: tsc — TS2307: 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):
So under node10 resolution TypeScript can't resolve it at all, and the repo works around that with a hardcoded paths entry:
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
-
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.
-
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:
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).
Building from source fails when
node_modulesis hoisted: hardcodedpathsshim forstructured-headersPackage:
@adcp/sdk@11.2.0(building from source, repo consumed as a git submodule)Failure:
tsc—TS2307: Cannot find module 'structured-headers'Summary
tsconfig.jsonuses legacy"moduleResolution": "node", which cannot readexportsmaps.structured-headers@2exposes its entry points only via anexportsmap (no top-levelmain/typesfallback):So under node10 resolution TypeScript can't resolve it at all, and the repo works around that with a hardcoded
pathsentry:That path assumes
structured-headersis physically installed at./node_modulesrelative 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 ancestornode_modules(e.g.../../node_modules/structured-headers), the shim points at a directory that doesn't exist, andnpm run build:libfails withTS2307.Reproduction
A standalone checkout doesn't hit this because
npm installputs the package exactly where the shim expects.A warning about the tempting wrong fix
Our first attempt was to switch
moduleResolutionto"bundler"(with"module": "es2022"), which makes the resolution error go away — but it changes the emit:tscthen 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-processingdist/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
Preferred:
"module": "node16"+"moduleResolution": "node16". Node16 resolution readsexportsmaps natively, sostructured-headersresolves from wherever it's installed and thepathsshim can be deleted outright. Becausepackage.jsonhas"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 underrequire()anyway, so surfacing them is a feature.Minimal band-aid: multiple
pathscandidates. TypeScript tries the array in order, so listing the hoisted location as a fallback keeps both layouts working with no other change: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).