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

Module resolution: Is it possible to build a repo without building its dependencies? #57505

Closed
yf-yang opened this issue Feb 23, 2024 · 3 comments
Labels
External Relates to another program, environment, or user action which we cannot control.

Comments

@yf-yang
Copy link

yf-yang commented Feb 23, 2024

Demo Repo

https://github.com/yf-yang/ts-bundler-bug

Which of the following problems are you reporting?

Something else more complicated which I'll explain in more detail

Demonstrate the defect described above with a code sample.

import type { Generic, InterFaceB, InterFaceC } from 'base';

Run tsc --showConfig and paste its output here

{
    "compilerOptions": {
        "allowUnreachableCode": false,
        "allowUnusedLabels": false,
        "declaration": true,
        "forceConsistentCasingInFileNames": true,
        "module": "esnext",
        "noEmitOnError": true,
        "noFallthroughCasesInSwitch": false,
        "noImplicitReturns": true,
        "sourceMap": true,
        "noUncheckedIndexedAccess": true,
        "strict": true,
        "target": "esnext",
        "esModuleInterop": true,
        "skipLibCheck": true,
        "declarationMap": true,
        "extendedDiagnostics": true,
        "jsx": "react-jsx",
        "moduleResolution": "bundler",
        "composite": true,
        "rootDir": "./src",
        "baseUrl": "./src",
        "outDir": "./dist",
        "paths": {
            "@": [
                "."
            ],
            "@/*": [
                "*"
            ]
        }
    },
    "references": [
        {
            "path": "../../packages/base"
        }
    ],
    "files": [
        "./src/index.ts"
    ],
    "include": [
        "src/**/*.ts"
    ],
    "exclude": [
        "../../node_modules"
    ]
}

Run tsc --traceResolution and paste its output here

Too long, I'll skip this part since I've figured out how tsc executes and mention that in the comments below.

Paste the package.json of the importing module, if it exists

{
  "name": "derived",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": "./src/index.ts",
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "base": "workspace:^1.0.0"
  }
}

Paste the package.json of the target module, if it exists

{
  "name": "base",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": "./src/index.ts",
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc"
  }
}

Any other comments can go here

What I am expecting

I am developing a monorepo (specifically, that is for web, with webpack as the bundler). In the example, there are two projects in the monorepo, base and derived, and base is a dependency of derived.

A good feature of the bundler is live update. Whenever I change anything in the base, the change is captured by the dev server provided by the bundler, so I don't need to build base again to view the change in the browser.

Therefore, I am expecting the same stuff with moduleResolution: bundler option. To be specifically, when I change a type annotation of something in base, I can instantly find the type imported in derived is also changed without building base. That is also implemented by tsserver.

However, after reading #51669, it seems apply change to packages that importing this one is not what moduleResolution: bundler is originally designed for. Therefore, Without building base first, I am unable to run tsc in derived repo.

That is OK for development, since the build step can only be executed once after everything is done with the language server. However, the typescript-eslint plugin is also using the typechecker, and the way it using lib/typescript.js is pretty similar to tsc, which means without compiling the base first, the typescript-eslint plugin will be broken. Every imported types fall back to any and trigger many rules that prevents any. That's pretty painful for development.

Why this is happening

After some debugging, I figure out the root cause is here:

let redirectedPath: Path | undefined;
if (isReferencedFile(reason) && !useSourceOfProjectReferenceRedirect) {
const redirectProject = getProjectReferenceRedirectProject(fileName);
if (redirectProject) {
if (outFile(redirectProject.commandLine.options)) {
// Shouldnt create many to 1 mapping file in --out scenario
return undefined;
}
const redirect = getProjectReferenceOutputName(redirectProject, fileName);
fileName = redirect;
// Once we start redirecting to a file, we can potentially come back to it
// via a back-reference from another file in the .d.ts folder. If that happens we'll
// end up trying to add it to the program *again* because we were tracking it via its
// original (un-redirected) name. So we have to map both the original path and the redirected path
// to the source file we're about to find/create
redirectedPath = toPath(redirect);
}
}

Since base is a project reference folder from derived's tsconfig's perspective, its files are always redirected. After reading the code, I find the file name is either directly be converted to .d.ts if outDir is not specified or be converted to a .d.ts file in the outDir. Therefore, it is not viable to make typescript-eslint work merely via directly setting exports to src/index.ts.

The question

After reading the code and the original PR, I want to confirm one question:

Is it possible to build derived (actually, is it possible to make typescript-eslint get the correct type signature from derived) without having base built? How can I achieve the goal, or is it something that the typescript maintenance team cares about?

@RyanCavanaugh
Copy link
Member

We're not the owners of typescript-eslint. They'd need to provide the useSourceOfProjectReferenceRedirect method in their createProgram host in order to surface this behavior, which is up to them.

@RyanCavanaugh RyanCavanaugh added the External Relates to another program, environment, or user action which we cannot control. label Feb 23, 2024
@yf-yang
Copy link
Author

yf-yang commented Feb 24, 2024

So you mean this behavior is customizable for downstream libraries 🤔? Let me discuss the idea with them. Thank you for your quick response!

@yf-yang
Copy link
Author

yf-yang commented Feb 24, 2024

Alright, typescript-eslint has already supported that with flag EXPERIMENTAL_useProjectService. See typescript-eslint/typescript-eslint#6754

@yf-yang yf-yang closed this as completed Feb 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
External Relates to another program, environment, or user action which we cannot control.
Projects
None yet
Development

No branches or pull requests

2 participants