Skip to content

jsc.baseUrl causes canonicalization of symlinked source files, breaking bundler symlinks: false #11584

@perbergland

Description

@perbergland

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions