microsoft/TypeScript

TypeScript 3 project references with a separate outDir

afroozeh opened this issue · 13 comments

TypeScript Version: 3.1.1

Search Terms: Project References

Code

I'm not sure if this issue is addressed somewhere else, at least I couldn't find an answer, so I'm asking again. I'm trying to use project references for a shared project with specified outDir.

The directory structure looks like this after the compilation:

.
├── A
│   ├── a.ts
│   ├── dist
│   │   └── a.js
│   └── tsconfig.json
└── Shared
    ├── dist
    │   ├── shared.d.ts
    │   └── shared.js
    ├── shared.ts
    └── tsconfig.json

Contents of the Shared directory:

shared.ts

export const name = "name";

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",                          
    "module": "commonjs",                     
    "outDir": "dist",                        
    "strict": true,                           
    "composite": true
  }
}

Contents of the A directory:

a.ts

import { name } from "../Shared/shared"; 

console.log(name);

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "dist",
    "strict": true,
  },
  "references": [
    { "path": "../Shared" }
  ]
}

Compile the files by running tsc -p . in the Shared and A directories.

Expected behavior:
The application run successfully when running node dist/a.js in the A directory.

Actual behavior:

Files compile successfully, but when running node dist/a.js in the A directory, I get the following Error:

module.js:538
    throw err;
    ^

Error: Cannot find module '../Shared/shared'

The reason is that in the generated a.js file, the reference to the imported module is not resolved properly:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var shared_1 = require("../Shared/shared");
console.log(shared_1.name);

Is there a way to get it working without putting all output files into the same directory?

This is actually unrelated to project references. When using outDir with project references, the only thing that they do is signal to TypeScript is to do a rebuild if the .d.ts files are out of date.

Ultimately this is just about how TypeScript doesn't touch import paths at all, and you need to import from wherever the .js and .d.ts files end up (rather than the input files).

I think I'm a bit confused about the project reference feature that is added in TypeScript 3.

If you look at my project structure, before using the project references, I had something like this:

.
├── A
│   ├── a.ts
│   ├── dist
│   │   ├── A
│   │   │   └── a.js
│   │   └── Shared
│   │       └── shared.js
│   └── tsconfig.json
└── Shared
    ├── dist
    │   └── shared.js
    ├── shared.ts
    └── tsconfig.json

And I could successfully run it by running node dist/A/a.js in the A directory.

What I didn't like about this was that everything was getting copied into A's output directory. I thought project references are here to fix this problem.

So, my questions is that how is the project references features is supposed to be used in case like mine?

  • Should I use just one output directory?
  • Should I manually rewrite generated output files to fix the import paths?
  • Is there another, better way that I organize my project to make use of project references?

Thanks in advance for your answer.

@DanielRosenwasser Also related to my previous comment, can you elaborate a bit more on the 'you need to import from wherever the .js and .d.ts files end up (rather than the input files)'? How should I change importing from the shared module when I have project references?

I'm also really confuse about it, all the example i found are not working properly...

Where could I found a working example of project reference feature?
Thanks :)

@DanielRosenwasser can you elaborate a bit more on the 'you need to import from wherever the .js and .d.ts files end up (rather than the input files)'? How should I change importing from the shared module when I have project references?

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

lukka commented

Anyone would expect that when you reference a project, you also reference the output created by that project. Otherwise what you reference it for? This is completely missing the point of Keep It Simple, and make using this technology fun, not a nightmare.

Really weird that this ticket is closed with no proper resolution. At least a link to a working solution should be explicitly pointed out.

Combined with VSCode's automatic imports that link to source .ts file, this is a bad solution.

For example, in my project I have this structure:

├── src
│   ├── foo.ts
│   └── tsconfig.json
├── tests
├─── foo_test.ts
├─── tsconfig.json
└── tsconfig.json

Root tsconfig.json:

{
	"references": [
		{
			"path": "./src/tsconfig.json"
		},
		{
			"path": "./tests/tsconfig.json"
		}
	]
}

src/tsconfig.json:

{
	"extends": "../tsconfig-base.json",
	"compilerOptions": {
		"outDir": "../lib",
		"rootDir": "."
	}
}

tests/tsconfig.json:

{
	"extends": "../tsconfig-base.json",
	"compilerOptions": {
		"outDir": "../tests-compiled",
		"rootDir": "."
	}
}

If src/foo.ts exports type Foo, in tests/foo_test.ts I start typing this:
let foo: Foo
When typing Foo, VSCode suggests Foo type. So I click enter to confirm suggestion, VSCode automatically adds import to top of file, pointing to src/foo.ts

This makes VSCode inconvenient because after every automatic import I need to edit import to point to .js file (also make sure its compiled, so I can't edit two files at the same time). So one software has bug: TypeScript or VSCode. I prefer to think its TypeScript bug.


edit: I was wrong. By adding this to root tsconfig.json VSCode starts linking files from lib, so now it all makes sense:

	"files": [],
	"include": [],

Also related to my previous comment, can you elaborate a bit more on the 'you need to import from wherever the .js and .d.ts files end up (rather than the input files)'?

To clarify, if you're in projectB/src/projBMain.ts, and you need to consume projectA/src/projAMain.ts, you need to import from wherever projAMain.js is going to be emitted to. So you'd need to import projectA/dist/projAMain.js in some way.

Now the actual problem you might run into is that if you use relative paths to projectA from projectB, your output directory in projectB will need to have the same level of nesting. In other words, if projectB/src/projBMain.ts is emitted to projectB/output/folder/very/deep/projBMain.js, then a relative import is going to break, and that's considered working as intended (or perhaps a design limitation).

I think in this situation, your best bet is something like path-mapping or symbolic linking (maybe like in this Lerna example).

I want to mention that this is still a big use ability issue almost 3 years later. Having the structure of your project be implicitly limited in a way that most programming languages allow without any guide or explicit explanation is anti-user design.

Making the generated JS import location patched from the output location of the referenced project seems the most obvious solution at first glance. Or, at the very least, having an explicit warning about import locations if they are relative and not at the same nesting level with the reference projects output.

I have wasted so much time on this (in friendly terms) half-baked and misleading feature now, and no success.

The typescript compiler allows you to "compile" code that's impossible to run. What's the job of the compiler and static analysis then?

This is a big mess. You have to manually mirror your source and target directory structures. It becomes practically impossible as soon as your rootDir is not ., but something like ./src.