Skip to content

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

Closed
@yf-yang

Description

@yf-yang

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    ExternalRelates to another program, environment, or user action which we cannot control.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions