Skip to content

fix: stop overriding tsconfig's preserveSymlinks - #406

Open
dayongkr wants to merge 2 commits into
Swatinem:masterfrom
dayongkr:fix/preserve-symlinks-default
Open

dayongkr wants to merge 2 commits into
Swatinem:masterfrom
dayongkr:fix/preserve-symlinks-default

Conversation

@dayongkr

@dayongkr dayongkr commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Opened #405 as a question. I think this is the right direction, so here is the change alongside it.

What

DEFAULT_OPTIONS is spread after the user's tsconfig in getCompilerOptions:

compilerOptions: {
  ...options,          // tsconfig
  ...compilerOptions,  // DEFAULT_OPTIONS + plugin overrides
}

So preserveSymlinks: true was not a default. It was unsettable from tsconfig.

This removes the key rather than setting it to false. Hardcoding false keeps the same defect mirrored: anyone who needs true for npm link would be silently overridden. Removing it restores tsconfig as the source of truth and falls back to the TypeScript default (false), which fixes pnpm out of the box.

The old comment looks backwards

// Ensure TS2742 errors are visible is the opposite of what happens. Forcing it on hides the failure and emits invalid output. With it unset, TypeScript resolves the package and reports the portability error (TS2883).

Test

tests/testcases/externals-pnpm-layout reproduces pnpm's layout: pkg-a is symlinked into a store directory where its transitive pkg-b sits as a sibling, reachable only through realpath.

emitted
preserveSymlinks: true (before) declare const value: any;
unset (after) declare const value: number;

Nothing is reported in the first case. The resolution failure is inside pkg-a's .d.ts, which skipLibCheck: true skips, so noEmitOnError has no diagnostic to act on.

Full suite passes.

Compatibility

Breaking for symlinked packages. A linked package resolving peer dependencies against its consumer (npm link) relied on the forced true and now needs it declared:

{ "compilerOptions": { "preserveSymlinks": true } }

Probably worth a major. Happy to adjust.

Downstream has been working around this for a while. unbuild passes preserveSymlinks: false into this plugin citing #143, and tsup carries a copy of DEFAULT_OPTIONS with the flag flipped.

Refs #405, and #143 which reported this in 2021.

`DEFAULT_OPTIONS` takes precedence over the user's tsconfig, so forcing
`preserveSymlinks: true` made the option unsettable and broke resolution
on package managers that reach transitive dependencies through realpath.
On pnpm this silently emitted invalid declarations: the failure happens
inside a dependency's `.d.ts`, which `skipLibCheck: true` ignores, so
`noEmitOnError` has no diagnostic to act on.

Dropping the key restores tsconfig as the source of truth and falls back
to the TypeScript default (`false`) when unset.

Refs Swatinem#405, Swatinem#143
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.

1 participant