Describe the bug
When jsc.baseUrl is set in builtin:swc-loader, SWC unconditionally calls canonicalize() on file paths, resolving symlinks to their real filesystem paths. This defeats rspack's resolve.symlinks: false setting — relative imports from a symlinked source file resolve from the real path instead of the symlink location.
This affects any project that uses symlinked source files with baseUrl (e.g. monorepos sharing code via symlinks). Without baseUrl, the same setup works correctly because SWC does not create its NodeImportResolver and no canonicalization occurs.
Discovered via Meteor framework: meteor/meteor#14053
Input code
Minimal reproduction: https://github.com/perbergland/swc-symlinks
shared/
source.ts # imports ./dep (dep.ts does NOT exist here)
server/
source.ts # symlink -> ../shared/source.ts
dep.ts # exists here
src/
index.ts # imports ../server/source
shared/source.ts:
import { VALUE } from "./dep";
export const myMethod = () => VALUE;
server/dep.ts:
export const VALUE = "hello from server/dep";
With resolve.symlinks: false, the import ./dep in server/source.ts (symlink) should resolve to server/dep.ts. Instead, SWC canonicalizes the path and looks for shared/dep.ts.
Config
Works (no baseUrl):
{
"jsc": {
"parser": { "syntax": "typescript" }
}
}
Fails (with baseUrl):
{
"jsc": {
"baseUrl": ".",
"parser": { "syntax": "typescript" }
}
}
Playground link (or link to the minimal reproduction)
https://github.com/perbergland/swc-symlinks
git clone https://github.com/perbergland/swc-symlinks
cd swc-symlinks
npm install
npm run setup # creates the symlink
npm run build # works (no baseUrl)
npm run build:baseurl # FAILS (with baseUrl)
Expected behavior
Both builds succeed. resolve.symlinks: false should preserve symlink paths regardless of whether jsc.baseUrl is set.
Actual behavior
npm run build:baseurl fails:
ERROR in ./shared/source.ts 1:0-30
× Module not found: Can't resolve './dep' in '.../swc-symlinks/shared'
SWC resolves the symlink server/source.ts to its real path shared/source.ts, then ./dep is looked up in shared/ where it doesn't exist.
Version
Reproduced with @rspack/core 1.6.7 (uses SWC internally via builtin:swc-loader).
Additional context
Root cause — in swc_ecma_transforms_module/src/path.rs (around line 270):
// Bazel uses symlink
// https://github.com/swc-project/swc/issues/8265
if let FileName::Real(resolved) = &target.filename {
if let Ok(orig) = canonicalize(resolved) {
target.filename = FileName::Real(orig);
}
}
canonicalize() unconditionally resolves symlinks. This was added to fix Bazel sandbox issues (#8265), but it breaks the common case of symlinked source files with resolve.symlinks: false.
Why it only happens with baseUrl: Without baseUrl, SWC skips creating its NodeImportResolver entirely, so no canonicalize() is called and symlink paths pass through unchanged.
Related issues:
Suggested fix: Add a preserveSymlinks option to the resolver config. When enabled, use std::fs::read_link() instead of canonicalize() — or skip canonicalization entirely — so that symlink paths are preserved. This would allow bundlers that set resolve.symlinks: false to work correctly while keeping the Bazel workaround available as the default behavior.
Describe the bug
When
jsc.baseUrlis set inbuiltin:swc-loader, SWC unconditionally callscanonicalize()on file paths, resolving symlinks to their real filesystem paths. This defeats rspack'sresolve.symlinks: falsesetting — relative imports from a symlinked source file resolve from the real path instead of the symlink location.This affects any project that uses symlinked source files with
baseUrl(e.g. monorepos sharing code via symlinks). WithoutbaseUrl, the same setup works correctly because SWC does not create itsNodeImportResolverand no canonicalization occurs.Discovered via Meteor framework: meteor/meteor#14053
Input code
Minimal reproduction: https://github.com/perbergland/swc-symlinks
shared/source.ts:server/dep.ts:With
resolve.symlinks: false, the import./depinserver/source.ts(symlink) should resolve toserver/dep.ts. Instead, SWC canonicalizes the path and looks forshared/dep.ts.Config
Works (no
baseUrl):{ "jsc": { "parser": { "syntax": "typescript" } } }Fails (with
baseUrl):{ "jsc": { "baseUrl": ".", "parser": { "syntax": "typescript" } } }Playground link (or link to the minimal reproduction)
https://github.com/perbergland/swc-symlinks
Expected behavior
Both builds succeed.
resolve.symlinks: falseshould preserve symlink paths regardless of whetherjsc.baseUrlis set.Actual behavior
npm run build:baseurlfails:SWC resolves the symlink
server/source.tsto its real pathshared/source.ts, then./depis looked up inshared/where it doesn't exist.Version
Reproduced with
@rspack/core1.6.7 (uses SWC internally viabuiltin:swc-loader).Additional context
Root cause — in
swc_ecma_transforms_module/src/path.rs(around line 270):canonicalize()unconditionally resolves symlinks. This was added to fix Bazel sandbox issues (#8265), but it breaks the common case of symlinked source files withresolve.symlinks: false.Why it only happens with
baseUrl: WithoutbaseUrl, SWC skips creating itsNodeImportResolverentirely, so nocanonicalize()is called and symlink paths pass through unchanged.Related issues:
canonicalize()code, different scenario)Suggested fix: Add a
preserveSymlinksoption to the resolver config. When enabled, usestd::fs::read_link()instead ofcanonicalize()— or skip canonicalization entirely — so that symlink paths are preserved. This would allow bundlers that setresolve.symlinks: falseto work correctly while keeping the Bazel workaround available as the default behavior.