lukeed / tsm

TypeScript Module Loader
MIT License
1.18k stars 19 forks source link

Support path resolvers from tsconfig #6

Open lukasIO opened 2 years ago

lukasIO commented 2 years ago

Hi,

in my tsconfig file i have paths set up like this

"compilerOptions": {
        ...
        "paths": {
            "$lib/*": ["src/lib/*"]
        }
    },

If I import a module for testing (using loadr, tsm and uvu) the resolution fails as soon as the module/file that I import has a reference to $lib with the following error: Error [ERR_MODULE_NOT_FOUND]: Cannot find package '$lib' imported from /path/to/lib/utils.ts

lukeed commented 2 years ago

Oh yeah, oops! I'll add this

lukeed commented 2 years ago

Are people expecting this to follow/work with the "extends" key?

Right now, I'm expecting to only support a tsconfig.json file in the process.cwd() root. Perhaps I'd listen in on tsc's -p/--project flag, but that would only set the initial tsconfig.json ... I'm not particularly incline to resolve the closest tsconfig.json per every file loaded – that would be a big perf hit.

If "extends" is defined, it could point to another tsconfig file that has "paths" defined within it. Would also have to follow "extends" recursively until there aren't any more.

bluwy commented 2 years ago

Perhaps we can use https://github.com/dominikg/tsconfck to resolve and parse the tsconfig? It’s currently used in Vite and implements the TypeScript behaviour well. cc @dominikg

dominikg commented 2 years ago

Thanks for tagging me. I should check if "paths" content is actually correctly handled when they are taken from an extended config. Usually relative pathlike values in tsconfig files are rebased to the file they are defined in, but with paths, it's a key/value pair and the value can actually be an array.

dominikg commented 2 years ago

tsconfck does handle paths the same way as typescript itself.

There are a couple of minor differences in tsconfck output, but they should not affect the results of esbuild

Example output of tsconfck and it's tsc equivalent in https://github.com/dominikg/tsconfck/tree/main/tests/fixtures/parse/valid/with_extends/paths

tsconfck parse import-foo.ts

{
  "extends": "../tsconfig.base",
  "compilerOptions": {
    "types": [
      "foo"
    ],
    "strictNullChecks": true,
    "baseUrl": "/home/dominikg/develop/tsconfck/tests/fixtures/parse/valid/with_extends/paths",
    "paths": {
      "$lib": [
        "*",
        "./lib"
      ],
      "$src": [
        "./src"
      ]
    },
    "noImplicitAny": true
  },
  "include": [
    "../src/**/*",
    "../lib/**/*"
  ],
  "watchOptions": {
    "watchFile": "useFsEvents",
    "watchDirectory": "useFsEvents",
    "fallbackPolling": "dynamicPriority"
  },
  "exclude": [
    "../../**/foo/*"
  ]
}

tsc --showConfig

{
    "compilerOptions": {
        "types": [
            "foo"
        ],
        "strictNullChecks": true,
        "noImplicitAny": true,
        "baseUrl": "..",
        "paths": {
            "$lib": [
                "*",
                "./lib"
            ],
            "$src": [
                "./src"
            ]
        }
    },
    "watchOptions": {
        "watchFile": "usefsevents",
        "watchDirectory": "usefsevents",
        "fallbackPolling": "dynamicpriority"
    },
    "files": [
        "./import-foo.ts",
        "../lib/foo.ts"
    ],
    "include": [
        "../src/**/*",
        "../lib/**/*"
    ],
    "exclude": [
        "../../**/foo/*"
    ]
}
Jamesernator commented 2 years ago

Right now, I'm expecting to only support a tsconfig.json file in the process.cwd() root.

This is not particularly useful when using shebangs in general as the tsconfig.json would be in the path with the script not the path the script is being run from.

e.g. I'm currently using ts-node at the moment with it's TS_NODE_PROJECT env var, but having tsconfig auto detected would be considerably more useful than having to deal with the awkward wrapper scripts I'm currently doing.

I'm not particularly incline to resolve the closest tsconfig.json per every file loaded – that would be a big perf hit.

Packages need to do this for their package.json already so I'm not sure why this would be any more expensive for tsconfig.json. Optionally you could make a requirement to link tsconfig.json from the package.json to hint that it is required.

lukeed commented 2 years ago

What packages have to do and what tsm has to do/is doing are completely separate. Right now, tsm only looks at each file as it's loaded. At most it's checking fs existence during path resolution, but no extra/unnecessary file reads beyond the transformation step itself.

Ocupe commented 2 years ago

hey πŸ‘‹ , thank you for this super fast testing tool! I'm lacking the background knowledge to see a work around in the above conversation. Is there a work around or a solution for this? :)

dominikg commented 2 years ago

No workaround. But implementing it with tsconfck would be just a couple of lines. the tsconfig needs to be set on esbuild transform call here https://github.com/lukeed/tsm/blob/73b8c77cf9f2cc8142522a685f0ffa46e6a75a52/src/loader.ts#L118, as tsconfigRaw iirc.

Ocupe commented 2 years ago

Thank you for your tip @dominikg! I'm struggling with it tough. Is this going into the right direction?

import { parse } from "tsconfck";

...

export const transformSource: Transform = async function ( source, context, xform) {
    let options = await toOptions(context.url);
    if (options == null) return xform(source, context, xform);

    const {
        tsconfigFile, // full path to found tsconfig
        tsconfig, // tsconfig object including merged values from extended configs
        extended, // separate unmerged results of all tsconfig files that contributed to tsconfig
        solution, // solution result if tsconfig is part of a solution
        referenced, // referenced tsconfig results if tsconfig is a solution
    } = await parse(context.url);

    // TODO: decode SAB/U8 correctly
    esbuild = esbuild || await import('esbuild');
    let result = await esbuild.transform(source.toString(), {
        ...options,
        sourcefile: context.url,
        format: context.format === "module" ? "esm" : "cjs",
        tsconfigRaw: tsconfig,
    });

    return { source: result.code };
};
dominikg commented 2 years ago

You don't need to define the unused constants and if there is a chance this code runs multiple times if sourcefile imports other files, using the cache feature of tsconfck might be a good idea.

Not 100% sure if this location is the best to do it or if it should be in toOptions, maybe @lukeed has a preference on that

yoursunny commented 2 years ago

NDNts monorepo is currently using @k-foss/ts-esnode but I found myself adding more and more monkeypatches to make it work with recent Node versions. I'm looking to switch to tsm but the lack of paths is the first blocker.

Are people expecting this to follow/work with the "extends" key?

My use case do not require this feature.

However, I hope I can customize which tsconfig.json is loaded via TS_CONFIG_PATH environment variable. This would allow me to prepare a special tsconfig.json that contains all the necessary paths.

lukeed commented 2 years ago

Is that ENV a known/standard variable? I was planning on doing a --tsm-tsconfig flag and have that be it. Only left I have left to investigate is whether or not each file actually has to search for its closest tsconfig file.

yoursunny commented 2 years ago

TS_CONFIG_PATH is not a standard. It's just what @k-foss/ts-esnode uses to locate a tsconfig.json file. https://github.com/K-FOSS/TS-ESNode/pull/64

Having --tsm-tsconfig flag is OK. In my use case, I can provide an absolute path, so that there's no searching involved.

bhvngt commented 2 years ago

This will be a great edition to this package.

I do use extended tsconfig.json as part of my monorepo to maintain consistency for source path alias.

As a workaround I have been using require hooks from module-alias. This works with uvu command line -- uvu -r tsm -r module-alias/register tests where my tests are using path alias.

However it does not work when I use it directly with node -- node --loader tsm -r module-alias/register src/m1/a.ts where a.ts is using path-alias.

Hopefully, I wont have to use module-alias if support for path-alias is baked in.

RoenLie commented 2 years ago

what is the status on this issue?

Mitsunee commented 2 years ago

I'm currently using tsm to help migrate a codebase with some dumb issues (TL;DR mjs scripts in a repo that's stuck with "type": "commonjs" due to the framework I'm using for the frontend and I regret not doing a monorepo) that currently prevent me from compiling stuff with tsc and cannot run anything without these path aliases working. Could this be implemented anytime soon?

Edit: just tested basePath as a workaround and that is also not currently supported but very related to this (I think the aliases even depend on it?)

ivanhofer commented 2 years ago

I recently switched to tsx for running TypeScript files like DB-migrations and other stuff

Mitsunee commented 2 years ago

@ivanhofer I tried tsx and it can't seem to resolve the relative paths (mjs file importing from "../../../foobar/cache.ts") that tsm was able to understand after I rewrote all my path aliases.

It seems that easy and painless migration of mjs to typescript remains a myth. Not that I can fault tsm and tsx entirely for that, the esm support of typescript is incomplete and confusing (I've had tsc tell me to rename my file to cache.mts and then complain that I cannot import from a *.mts file as that's an "unsupported file extension" even after I had added "**/*.mts" to the include array in my tsconfig).

saitonakamura commented 1 year ago

tsx uses another ENV variable to customise tsconfig name. They do not however resolve it per file, but rather only one time. https://github.com/esbuild-kit/esm-loader/blob/develop/src/utils.ts#L12

isaac-j-miller commented 12 months ago

Any update on this issue?