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

Improve support for internal packages by resolving path aliases #58657

Open
6 tasks done
zirkelc opened this issue May 25, 2024 · 2 comments
Open
6 tasks done

Improve support for internal packages by resolving path aliases #58657

zirkelc opened this issue May 25, 2024 · 2 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@zirkelc
Copy link

zirkelc commented May 25, 2024

πŸ” Search Terms

Internal packages
Path alias
Resolve paths

βœ… Viability Checklist

⭐ Suggestion

Add a tsconfig.json to the exports field in package.json so that internal packages can share their TypeScript config especially their path aliases.

// ./libs/math/package.json
{
  "name": "@repo/libs-math",
  "version": "1.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    // exports local tsconfig.json so consumers can resolve local path aliases
    "./tsconfig.json": "./tsconfig.json",
    ".": {
      "import": "./src/index.ts",
      "require": "./src/index.ts",
      "types": "./src/index.ts"
    }
  }
}

πŸ“ƒ Motivating Example

Please find a reproduction example here: https://github.com/zirkelc/typescript-internal-packages

Install all dependencies with pnpm install in the project root. Then open apps/foo/src/index.ts and you will see that TypeScript cannot resolve the dependency to @repo/libs-with-alias correctly.


I'm using internal packages to share code in my monorepo. An internal package is package which is NOT published to NPM and which is NOT explicitly compiled with TypeScript before it can be used by other packages. It exports the source TypeScript *.ts files from the package.json via the main, types, and exports fields:

// ./libs/math
{
  "name": "@repo/libs-math",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": {
      "import": "./src/index.ts",
      "require": "./src/index.ts",
      "types": "./src/index.ts"
    }
  }
}
// ./libs/math/src/index.ts
export const add = (a: number, b: number) => {
	return a + b;
};

This internal package is meant to be installed by other packages in the same repo:

// ./apps/foo
{
  "name": "@repo/apps-foo",
  "dependencies": {
    "@repo/libs-math": "workspace:*"
  }
}

And can be used like a normal dependency which resolve to the original uncompiled TypeScript code:

// ./apps/foo/src/index.ts
import { add } from '@repo/libs-math'
//              ^?  Imports the typescript code from the internal package

Everything good so far. This pattern works well as long as none of the internal packages is using TypeScript path aliases. However, let's re-structure the internal package and use path aliases like ~/math:

// ./libs/math/tsconfig.json
{
  "extends": "@tsconfig/node20/tsconfig.json",
  "compilerOptions": {
    "paths": {
      "~/math": ["./src/math/index.ts"],
    },
  },
  "include": [ "src"]
}

The path alias ~/math points to the sub-folder ./src/math/index.ts. The previous ./src/index.ts becomes a simple barrel file exports

// ./libs/math/src/index.ts
export * from '~/math'
// ^? Exports from local path alias pointing to ./libs/math/src/math/index.ts

Now, the internal package @repo/libs-math uses path aliases which are defined its own local tsconfig.json. However, the previous import in ./apps/foo will now start to fail:

// ./apps/foo/src/index.ts
import { add } from '@repo/libs-math'
//               ^? Module '"@repo/libs-math"' has no exported member 'add'

TypeScript doesn't find the function anymore, even though go to definition to @repo/libs-math still works.

I assume the reason is the following: TypeScript resolves the imports starting from the app ./apps/foo/src/index.ts to the internal package ./libs/math/src/index.ts. However, TypeScript still works inside the ./apps/foo/tsconfig.json and is not aware of the path aliases defined by the ./libs/math/tsconfig.json. It would need a way to recognize it is in a different package now and must look for the right tsconfig.json in ./libs/math.

That's where my idea of exporting the tsconfig.json in the package.json comes in. TypeScript would notice it is working in a different package when it was moving from ./apps/foo/src/index.ts to ./libs/math/src/index.ts. Then it sees this new package has a local TypeScript config exported from the local ./libs/math/package.json. It sees the path aliases and applies these aliases for all paths inside ./libs/math/*.

πŸ’» Use Cases

There are two possible workarounds:

  1. global path alias: create a root-level tsconfig.paths.json which defines all path aliases for the entire project. Each package-level tsconfig.json extends from this config. There are two disadvantages to this workaround:
  • packages cannot define package-level path aliases as they would overwrite the inherited root-level aliases
  • each package can import from every other package via path aliases. that means you could accidentally import from the internal package @repo/libs-math via a typescript path alias instead of a node package (workspace) import.
  1. project references: typescript project references can solve this issue, but they add a lot of complexity due intermediate build steps and overhead in extra multiple tsconfig.json. The package @repo/apps-foo has a node dependency to @repo/libs-math via package.json. For project references, this dependency must be replicated again in the tsconfig.json via references. So the dependency graph of package.json must be repeated for every new internal package.
@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Jun 6, 2024
@damianobarbati
Copy link

@zirkelc I'm not sure if you are dealing with the same problem: I'm trying to NOT use the tsconfig paths and use only native module resolution of node.

My understanding is that things we can use are:

  • subpath import: `import Foe from '#api/Foe'
  • module alias import: `import type Foe from '#types/Foe'

While the first seems to work with nodenext, the latter seems to be broken and tsc complains about

error TS2307: Cannot find module '@ts-repro/types/User' or its corresponding type declarations.

Is there a way to proper deal with this? Having relative imports and avoid using tsconfig paths?

@zirkelc
Copy link
Author

zirkelc commented Oct 8, 2024

@damianobarbati indeed my issue is only related to TS and how it resolves the paths.

Sub-path imports are a Node.js feature and I assume they are resolved by Node from within tsc, but import type is TypeScript-specific. I guess when TS sees import type it uses its own resolution algorithm to resolve #types/Foe as a path alias.

Maybe this will be supported in the future. Here is another issue where you might be able to ask for help: #52460

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants