Skip to content

fix(plugin): rebase readonly import paths onto the output directory - #4019

Merged
kamilmysliwiec merged 1 commit into
nestjs:masterfrom
kyungseopk1m:fix/readonly-outdir-import-path
Aug 17, 2026
Merged

fix(plugin): rebase readonly import paths onto the output directory#4019
kamilmysliwiec merged 1 commit into
nestjs:masterfrom
kyungseopk1m:fix/readonly-outdir-import-path

Conversation

@kyungseopk1m

Copy link
Copy Markdown
Contributor

What

When the CLI plugin runs in readonly mode (the metadata-generation path used by the SWC builder), generated relative imports are computed incorrectly whenever outDir is deeper than rootDir: the output directory leaks into the emitted relative path and the import fails to resolve.

For example, with rootDir: "." and outDir: "./dist", an import that should be ../../entities/entity.dto is emitted as ../../../dist/entities/entity.dto.

Why

replaceImportPath() computes the from side differently per mode:

const from = options?.readonly
  ? safeDecodeURIComponent(convertPath(options.pathToSource))
  : getOutputDir(fileName, options);

The non-readonly branch rebases from onto outDir (added in #3847), and the import target was later rebased as well (#3858). The readonly branch, however, keeps the raw pathToSource while the target side still goes through the rebase, so the two are compared in different coordinate spaces.

The second half of the problem is that even once the readonly branch rebases from, no real caller supplies outDir/rootDir in readonly mode: nest-cli's metadata generator constructs ReadonlyVisitor without them.

Fix

  1. Rebase the readonly from path through the same output-directory logic as the non-readonly path (extracted into a shared helper reused by getOutputDir).
  2. Have ReadonlyVisitor.visit() derive outDir/rootDir from program.getCompilerOptions() when the caller did not pass them, mirroring how compiler-plugin.ts's before() hook already does it. Explicit options passed by a caller still take precedence.

Tests

  • A unit test in plugin-utils.spec.ts comparing readonly vs non-readonly output for a nested outDir, asserting no output-directory leak.
  • readonly-visitor-outdir.spec.ts, modelled on the existing readonly-visitor-project-refs.spec.ts: it builds a real ts.Program from a fixture whose import target lives outside rootDir (so the rebase is not accidentally cancelled out), constructs ReadonlyVisitor without explicit outDir/rootDir, and asserts the emitted import path is correct. Reverting either part of the fix independently makes this test fail. A second case asserts that an explicit outDir passed to the visitor is not overwritten by the program's compiler options.

Full unit suite passes (406 tests).

In readonly mode the `from` side of a generated import kept the raw
source path while the target side was rebased onto `outDir`, so the two
were computed in different coordinate spaces. When `outDir` is deeper
than `rootDir` the emitted relative import in the generated metadata
leaked the output directory into the path and failed to resolve.

Rebase the readonly `from` path through the same output-directory logic
used for the non-readonly path, and let `ReadonlyVisitor` derive
`outDir`/`rootDir` from the program's compiler options when the caller
did not pass them explicitly (explicit options still take precedence),
which is the case for the SWC metadata generator that constructs the
visitor without those fields.
@kamilmysliwiec
kamilmysliwiec merged commit e5203bf into nestjs:master Aug 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants