swc-project / swc-node

Faster ts-node without typecheck
MIT License
1.81k stars 76 forks source link

Monorepo resolving wrong paths in ESM #808

Closed HommeSauvage closed 4 months ago

HommeSauvage commented 4 months ago

Describe the bug

When using SWC (through @swc-node/register) in an ESM environment ("type": "module") SWC seems to mess up the paths when resolving modules.

In order to do so, create a monorepo and try to resolve files from different packages using "paths" settings in tsconfig.json and .swcrc.

Input code

import { hello } from '@app/functions/hello'

const start = () => {
    hello()
}

start()

Config

{
    "$schema": "https://swc.rs/schema.json",
    "jsc": {
      "externalHelpers": true,
      "parser": {
        "syntax": "typescript",
        "tsx": true
      },
      "target": "esnext",
      "baseUrl": ".",
      "paths": {
        "@app/*": ["./apps/bar/*"]
      }
    },
    "module": {
      "type": "es6",
      "resolveFully": true
    }
  }

Playground link (or link to the minimal reproduction)

https://github.com/DriveFlux/swc-path-issue

SWC Info output

Operating System: Platform: darwin Arch: x64 Machine Type: x86_64 Version: Darwin Kernel Version 23.5.0: Wed May 1 20:09:52 PDT 2024; root:xnu-10063.121.3~5/RELEASE_X86_64 CPU: (16 cores) Models: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz

Binaries:
    Node: 22.3.0
    npm: 10.8.1
    Yarn: 1.22.19
    pnpm: 9.4.0

Relevant Packages:
    @swc/core: 1.6.13
    @swc/helpers: 0.5.11
    @swc/types: N/A
    typescript: 5.5.3

SWC Config:
    output: N/A
    .swcrc path: N/A

Next.js info:
    output: N/A

Strange that swcrc path is N/A. It's at the root as you may notice in the reproduction repo.

Expected behavior

SWC should be able to follow baseUrl and resolve paths correctly.

Actual behavior

node:internal/modules/run_main:115
    triggerUncaughtException(
    ^
Error: All of the aliased extension are not found: ../../../../../../../apps/bar/functions/hello.js cannot be resolved in file://path/to/path-reproduction/internal/foo.ts
    at resolve (file://path/to/path-reproduction/node_modules/.pnpm/@swc-node+register@1.10.3_@swc+core@1.6.13_@swc+helpers@0.5.11__@swc+types@0.1.9_typescript@5.5.3/node_modules/@swc-node/register/esm/esm.mjs:222:15)
    at async nextResolve (node:internal/modules/esm/hooks:750:22)
    at async Hooks.resolve (node:internal/modules/esm/hooks:238:24)
    at async MessagePort.handleMessage (node:internal/modules/esm/worker:199:18)

SWC adds many ../../../ into the path

Version

1.6.13

Additional context

To run the reproduction:

Notice that we register SWC this way: cross-env SWCRC=true TS_NODE_PROJECT=./internal/tsconfig.json node --import @swc-node/register/esm-register

The .swcrc is at the root, pointing to the right paths and setting baseUrl to be the root. The tsonfig.json is inside the internal folder and sets the baseUrl to internal folder but sets the paths correctly.


The use-case of this is to have many packages and apps in the repository and also an internal folder which is intended for terminal use only for quick scripts. Files in the internal folder should be able to execute JS from all the monorepo.

We also use @swc-node/register@1.10.3 which supports "moduleResolution": "Bundler".

We use Node: v22.3.0


I also tried to debug @swc-node/register and found that it correctly passes all the information to the compiler. The rust compiler then messes the path and returns the compiled file with those ../../../

kdy1 commented 4 months ago

cc @Brooooooklyn

Boshen commented 4 months ago

This is a path alias in project references scenario, can you try https://www.typescriptlang.org/docs/handbook/project-references.html?

The following pointing path alias outside of the project does not work.

internal/tsconfig.json:

      "paths": {
        "@app/*": ["../apps/bar/*"]
      },
HommeSauvage commented 4 months ago

I did that, here's the new tsconfig:

{
    "extends": "../packages/tsconfig/base.json",
    "compilerOptions": {
      "baseUrl": ".",
      "rootDir": "..",
      "paths": {
        "@app/*": ["../apps/bar/*"]
      },
    },
    "include": [
      "../apps/bar/**/*.ts",
      "**/*.ts",    
    ],
    "references": [
      {
        "path": "../apps/bar"
      }
    ],
  }

And I added "composite": true to apps/bar/tsconfig.json. This did not change anything.

Brooooooklyn commented 4 months ago

@HommeSauvage you can omit the SWCRC=true from your command, you don't need a swcrc file since you already have tsconfig files, @swc-node can read configs from tsconfig.json files and pass it to SWC.

HommeSauvage commented 4 months ago

Aha, when I removed SWCRC=true, it seems like it was able to compile the first file, which is foo.ts. But now, it's unable to resolve other which is imported as import { other } from '@app/functions/other' from within hello.ts.

I pushed the changes to the reproduction repo. I added there vite which is able to resolve everything correctly. ts-node had the same issue.

node:internal/modules/run_main:115
    triggerUncaughtException(
    ^
Error: Cannot find module '@app/functions/other': @app/functions/other cannot be resolved in file://path/project/path-reproduction/apps/bar/functions/hello.ts
    at resolve (file://path/project/path-reproduction/node_modules/.pnpm/@swc-node+register@1.10.3_@swc+core@1.6.13_@swc+helpers@0.5.11__@swc+types@0.1.9_typescript@5.5.3/node_modules/@swc-node/register/esm/esm.mjs:222:15)
    at async nextResolve (node:internal/modules/esm/hooks:750:22)
    at async Hooks.resolve (node:internal/modules/esm/hooks:238:24)
    at async MessagePort.handleMessage (node:internal/modules/esm/worker:199:18)

Node.js v22.3.0
Boshen commented 4 months ago

Try remove this because aliasing to files outside of the project is not supposed to work.

 "paths": {
        "@app/*": ["../apps/bar/*"]
      },

Since you already specified https://github.com/DriveFlux/swc-path-issue/blob/a5ea7dbf11f5e13b496713eb76ce3781d24f1916/apps/bar/tsconfig.json#L6-L9

HommeSauvage commented 4 months ago

Well, that wouldn't make the files inside internal folder able to resolve @app/functions/hello.

The reason I have 2 tsconfig.json in those projects specifying paths is:

If I remove paths from internal/tsconfig.json, the foo.ts won't resolve hello.ts. But by keeping it, it resolves @app/functions/hello.ts, the hello.ts is not able to resolve its sibling other.ts which is in the same folder and is imported as @app/functions/other.

Boshen commented 4 months ago
diff --git a/internal/tsconfig.json b/internal/tsconfig.json
index be46d71..65c0af7 100644
--- a/internal/tsconfig.json
+++ b/internal/tsconfig.json
@@ -2,14 +2,16 @@
     "extends": "../packages/tsconfig/base.json",
     "compilerOptions": {
       "baseUrl": ".",
       "rootDir": "..",
       "paths": {
-        "@app/*": ["../apps/bar/*"]
+        "@app/*": ["../apps/bar/*"] // makes path alias work in this directory
       },
     },
+    "references": [{
+      "path": "../apps/bar/tsconfig.json" // makes path alias work in the other directory
+    }],
     "include": [
       "../apps/bar/**/*.ts",
       "**/*.ts",    
     ],
     "ts-node": {
       "transpileOnly": true,
@@ -23,4 +25,4 @@
         }
       }
     }
  }

This makes it work.

To understand what's going on in here, it goes all the way to how oxc-resolver has a builtin automatic project references path alias lookup mechanism ported from https://github.com/dividab/tsconfig-paths-webpack-plugin?tab=readme-ov-file#references-_string-defaultundefined

HommeSauvage commented 4 months ago

Aha, what I was missing was the tsconfig.json in references. The way I did it was:

"references": [
      {
        "path": "../apps/bar" // without tsconfig.json
      }
    ],

Thanks @Boshen and @Brooooooklyn