davestewart / alias-hq

The end-to-end solution for configuring, refactoring, maintaining and using path aliases
https://davestewart.co.uk/projects/open-source/alias-hq/
MIT License
331 stars 11 forks source link

Accessing paths from a `tsconfig.json` that extends another `tsconfig.base.json` with the alias paths #30

Open saxoncameron opened 3 years ago

saxoncameron commented 3 years ago

👋 Hey, great library! I've got this working for the most part in a monorepo structure, but I'm having trouble with hq.get('any') returning {} when used inside the /package/* directory roots. This is because the tsconfig.json inside each package extends the tsconfig.base.json at the monorepo root where I have my paths configured. Because the tsconfig.json doesn't explicitly contain the paths, the result is {} and thus I'm having issues plugging in aliases in some package-specific processes like Jest.

For the most part everything else works fine, and I've got a nice clean, centralised alias structure which all runs and builds fine - just trying to figure out this last piece.

Here's the structure:

monorepo/
    ...
    tsconfig.base.json (1)
    packages/
        client/
            ...
            tsconfig.json (2)
        components/
            ...
            tsconfig.json (3)

In the monorepo root I've got a couple processes that consume the aliases set in tsconfig.base.json (1), e.g. a shared Storybook across all packages. In tsconfig.base.json I have my alias paths set up like so:

{
    "compilerOptions": {
        ...
        "rootDir": "./",
        "baseUrl": "./packages",
        "paths": {
            "@client/*": ["../packages/client/*"],
            "@components/*": ["../packages/components/*"]
        }
    }
}

and in my packages, e.g. client, I have a tsconfig.json (2) that extends the root tsconfig.base.json (3):

{
    "extends": "../../tsconfig.base.json",
    "compilerOptions": {
        ...
        "baseUrl": "../",
     }
}

This works in that my code compiles, runs, builds etc and resolves the aliases, but as I've said, if I try and use hq.get('any') ('webpack', 'jest', w/e) in any of the package roots it returns empty. It returns the paths just fine in the root. Sorry for the repetitive explanation, I hope I've made my issue clear.

Thanks again!

davestewart commented 3 years ago

Hey Saxon,

Thanks for the issue, and the explanation.

Unfortunately, I have not had the time to look at any of my OSS projects this year, despite falsely promising to on various occasions 😎 .

Is your project public, or could you create a stripped-down demo project for me?

I can't promise to look at this now, but at least I would be able to get off to a good start when / if I get to this.

saxoncameron commented 3 years ago

Thanks for the quick response! I will endeavour to make you a demo project that replicates the issue; I'll report back.

For what it's worth, I think this could potentially be solved by allowing a parameter to specify the location of the target tsconfig or equivalent file that contains the aliases/paths. Of course, if it's possible to resolve the path names via the extended tsconfig then that'd be even better.

davestewart commented 3 years ago

I haven't looked at the source for a long time, but did you try the load() method?

It looks like I didn't document this, but I'm using it in Alias's own tests:

davestewart commented 3 years ago

Though, reading your issue again, what you would really like would be for Alias to automatically resolve the paths in tsconfig.base.json by parsing the extends property of the local tsconfig.json.

If so, I can leave this ticket open and take a look (at some point!).

Hopefully load() will get you by in the meantime!

saxoncameron commented 3 years ago

load() has indeed got me by in the meantime, although the solution has turned out a little more crude than just pointing to the monorepo root tsconfig.base.json - seems like this library isn't quite built with monorepos in mind...! 😅

FYI I solved the problem in package/client/jest.config.js like so:

const hqLib = require('alias-hq');
const hq = hqLib.load('../../tsconfig.base.json');

/**
 * Load monorepo alias resolves from the repo root in Jest
 * format, accounting for the relative baseline dir `../`.
 */
const monorepoAliases = () => {
    const jestPaths = hq.get('jest');

    return Object.keys(jestPaths).reduce((acc, key) => {
        const value = jestPaths[key].replace('<rootDir>/packages', '<rootDir>/..');
        acc[key] = value;

        return acc;
    }, {});
};

module.exports = {
    ...
    moduleNameMapper: {
        ...hq.get(monorepoAliases),
    },
};

☝ïļ I had to do it that way because hq.get('jest') was returning alias resolution paths like <rootDir>/packages/client/$1 where <rootDir> was resolving to the package root, and alas since we're manually pointing-to/loading the tsconfig.base.json then hq isn't aware of the package root tsconfig.json which would tell it that the baseDir needs to be ../ in order for the aliases to resolve properly.

Thankfully you also have support for custom transformers, so as you can see I've gone ahead and essentially force-rewritten the results of hq.load('../../tsconfig.base.json')get('jest') to account for the ../ value of baseDir that is foregone.

As you say, the optimal solution of somehow resolving the extended tsconfig.json would make this problem moot. At least I have a working solution in the meantime, because having centralised aliases still makes up for this roundabout solution!

davestewart commented 3 years ago

Excellent!

Yes, that will do it.

If you still want to create the example project, I can take a look at some point.

But a) glad you did it and b) pleased Alias was flexible enough to let you do it!

saxoncameron commented 3 years ago

Yes, I think we could further improve the utility of alias-hq with more intuitive monorepo support - a place where aliases can really shine. It's all very complicated at the moment. ðŸĨī

Time permitting and if I don't forget, I'll spin up a stripped down example monorepo with aliases working in what I posit to be an ideal way, and we can look at tweaking/improving/assessing from there. If you are interested.

Thanks again for your responsiveness. ✌ïļ

davestewart commented 3 years ago

Definitely interested, so thanks for bringing this to my attention.

I may have to ask you some questions about monorepos too, as I've only played with some setups, vs used one in anger.

Ideally, Alias would determine what it needed automatically; not sure if it would / should pick this up from directory structure or additional config files (lerna.json or whatever it is) or manually.

Chat at some point in the future then.

Cheers!

davestewart commented 2 years ago

FYI @saxoncameron there is a PR in progress for this here: #37

saxoncameron commented 2 years ago

Great job! That's awesome 👏

davestewart commented 2 years ago

If you could test in your monorepo and feed back, that would be great! Thanks :)

saxoncameron commented 2 years ago

Updated my repo - I no longer have to use .load() in a handful of locations, which is nice. However, I still have to use my DIY mapping function in one of my monorepo package jest configs to patch the paths there, and that's the largest chunk of my alias code at present still.

So if we look at my jest config (as posted above), I've only been able to remove one line from all that:

const hq = require('alias-hq');

// This is how I used to do it with .load()
// const hqLib = require('alias-hq');
// const hq = hqLib.load('../../tsconfig.base.json');  // <---- The one line no longer necessary

/**
 * Load monorepo alias resolves from the repo root in Jest
 * format, accounting for the relative baseline dir `../`.
 */
const monorepoAliases = () => {
    const jestPaths = hq.get('jest');

    return Object.keys(jestPaths).reduce((acc, key) => {
        const value = jestPaths[key].replace('<rootDir>/packages', '<rootDir>/..');
        acc[key] = value;

        return acc;
    }, {});
};

module.exports = {
    ...
    moduleNameMapper: {
        ...hq.get(monorepoAliases),
    },
};
saxoncameron commented 2 years ago

That jest config is here FYI

monorepo/
    ...
    tsconfig.base.json (1) <-------------- all aliases defined here
    packages/
        client/
            ...
            jest.config.js <--------------
            tsconfig.json (2)
        components/
            ...
            tsconfig.json (3)
davestewart commented 2 years ago

Gotcha. Do you fancy setting up a test project I can pull?

Probably going to be easier than typing the structure, then I'll have a look and see if I can't work out is going on.

saxoncameron commented 2 years ago

I know I said I'd make you a test/demo repo a while back, and never did! I'll see to doing it, perhaps this weekend. Making this package monorepo-friendly would be a nice, advertiseable trait! :)

davestewart commented 2 years ago

No worries!

Yeah, it literally needs to be a few folders and files, but would be really useful so I'm not second guessing your requirements.

If all goes well, I'll fork it as a demo!

davestewart commented 2 years ago

I've run into this issue myself today with a new Vite setup, so I'm going to need to tackle it.

This lib came up in my Google search:

I'll hopefully take a look later today and see if it's a quick win...

FYI @IanVS

davestewart commented 1 year ago

FYI @saxoncameron I closed #41 today by replacing TypeScript with JSON5 and loading / parsing the extends target manually.

I'm not sure if this will solve the problem with paths in the monorepo, but when reviewing the open issues I came across this one again. I did some work on monorepos last year so I know a bit more about them now; if I get time I may try to set something up soon and see how Alias handles it.

Annoyingly, I can't remember what the Vite issue I mentioned above was now; it may be on one of the the Spaceman demos.

saxoncameron commented 1 year ago

Thanks for the update @davestewart! And sorry I never did get around to making that repro repo ðŸĪŠ

I'm still a happy user of this package, but dont face this kind of complexity nowadays since the monorepos I'm working in have all linting/testing and related infra in the monorepo root. Anyway, not having issues at present, and dont need any of the snippets I posted above anymore

davestewart commented 1 year ago

OK! Thanks for getting back to me 😃

I may experiment later today just for completeness' sake, and get back to you.

One thing I did find out on the latest work is that TypeScript only respects the paths in the top-most tsconfig.*.json file.

Screenshot 2023-04-26 at 09 07 31

So there's no way to "combine" paths from separate configs.

Did you find the same thing?


I'm also going to leave these scripts here for reference:

// index.mjs
import Path from 'path'
import pkg from 'typescript'
import { getTsconfig } from 'get-tsconfig'

const {
  sys,
  findConfigFile,
  readConfigFile,
  parseJsonConfigFileContent
} = pkg

console.clear()

// uses typescript
// @see https://stackoverflow.com/questions/67956755/how-to-compile-tsconfig-json-into-a-config-object-using-typescript-api
function a () {
  const tsconfigPath = findConfigFile(process.cwd(), sys.fileExists, 'tsconfig.json')
  const tsconfigFile = readConfigFile(tsconfigPath, sys.readFile)
  const parsedTsconfig = parseJsonConfigFileContent(tsconfigFile.config, sys, Path.dirname(tsconfigPath))

  // console.log(parsedTsconfig)

  function getCompilerOptionsJSONFollowExtends (filename) {
    let options = {}
    const config = readConfigFile(filename, sys.readFile).config
    // console.log({ config })
    if (config.extends) {
      const path = Path.resolve(Path.dirname(filename), config.extends) + '.json'
      // console.log({ path })
      options = getCompilerOptionsJSONFollowExtends(path)
    }
    return {
      ...options,
      ...config.compilerOptions,
    }
  }

  console.log(getCompilerOptionsJSONFollowExtends('tsconfig.json'))
}

// uses code from stack overflow
// @see https://stackoverflow.com/questions/53804566/how-to-get-compileroptions-from-tsconfig-json/53898219#53898219
function b () {
  const configFileName = findConfigFile('./', sys.fileExists, 'tsconfig.json')
  const configFile = readConfigFile(configFileName, sys.readFile)
  const compilerOptions = parseJsonConfigFileContent(configFile.config, sys, './')
  console.log(compilerOptions.options)
}

// uses get-tsconfig
function c () {
  const config = getTsconfig('tsconfig.json').config.compilerOptions.paths
  console.log(config)
}

My final code (using JSON5) here:

https://github.com/davestewart/alias-hq/blob/ed575ebff6067721f4e725b4fa15759db23a0c55/src/index.js#L144-L170

saxoncameron commented 1 year ago

Wish I could tell you, no longer working for that client, and no longer have access to that repo ðŸĨē

davestewart commented 1 year ago

I've been there many times! OK, thanks for the input anyway 🙏