Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
saxoncameron opened this issue Aug 3, 2021 · 22 comments · Fixed by #37
Labels
help wanted Extra attention is needed

Comments

@saxoncameron
Copy link

👋 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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Owner

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

davestewart pushed a commit that referenced this issue Nov 25, 2021
* Use typescript to parse tsconfig.json

* Handle extends in config files

* Fix linting errors

* Bump version

Co-authored-by: Dave Stewart <[email protected]>

Closes #28 
Closes #30
@saxoncameron
Copy link
Author

Great job! That's awesome 👏

@davestewart
Copy link
Owner

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

@saxoncameron
Copy link
Author

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
Copy link
Author

saxoncameron commented Nov 26, 2021

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Owner

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 davestewart reopened this Sep 21, 2022
@davestewart
Copy link
Owner

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.

@davestewart davestewart added the help wanted Extra attention is needed label Apr 25, 2023
@saxoncameron
Copy link
Author

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
Copy link
Owner

davestewart commented Apr 26, 2023

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:

alias-hq/src/index.js

Lines 144 to 170 in ed575eb

function loadConfig (path) {
/**
* @type {TSConfig}
*/
const json = loadJson(path)
if (json) {
const { compilerOptions } = json
if (compilerOptions) {
const { baseUrl, paths } = compilerOptions
// Note that paths in the extending file take priority over paths in the extended file
// https://stackoverflow.com/questions/53804566/how-to-get-compileroptions-from-tsconfig-json
if (paths) {
settings.configFile = path
config.rootUrl = Path.dirname(path)
config.baseUrl = baseUrl || ''
config.paths = paths
return true
}
}
// if no paths, check extends
if (json.extends) {
return loadConfig(resolve(Path.dirname(path), json.extends))
}
}
}

@saxoncameron
Copy link
Author

Wish I could tell you, no longer working for that client, and no longer have access to that repo 🥲

@davestewart
Copy link
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants