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 via "--paths" fails against declaration files generated alongside "--outfile:" #18311

Closed
josh-sachs opened this issue Sep 7, 2017 · 33 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@josh-sachs
Copy link

TypeScript Version: 2.4.0 / nightly (2.5.0-dev.201xxxxx)
2.5.2
Code

/bundles
- app.js
- app.d.ts
- common.js
- common.d.ts
- core.js
- core.d.ts

/app
- main.ts
- tsconfig.ts

/src/common
- index.ts
- file1.ts
- tsconfig.json

/src/core
- index.ts
- file1.ts
- tsconfig.json

//app/main.ts
import { resource } from '@bundles/core'
import { somethingElse } from '@bundles/common'

//common/index.ts
export * from './file1.ts'

//core/index.ts
export * from './file1.ts'

//app/tsconfig.json
...
"target": "es5",
"module": "system",
"rootDir": ".",
"outFile": "../bundles/app.js",
"declarations": true,
"paths": {
   "@bundles/*": ["../bundles/*"]
}
...

//common/tsconfig.json
...
"target": "es5",
"module": "system",
"rootDir": ".",
"outFile": "../bundles/common.js",
"declarations": true,
"paths": {
   "@bundles/*": ["../bundles/*"]
}
...

//core/tsconfig.json
...
"target": "es5",
"module": "system",
"rootDir": ".",
"outFile": "../bundles/core.js",
"declarations": true
"paths": {
   "@bundles/*": ["../bundles/*"]
}

// bundles/common.d.ts
declare module "index" {
    export * from 'file1'
}

// bundles/core.d.ts
declare module "index" {
    export * from 'file1'
}
...

Expected behavior:
TSC to correctly resolve the import statements within app/main.ts against the configured --paths, applying the same logical resolution strategy that lets it resolve barrels.

(note: I've had to escape the [@] symbols for some reason at the start of these lines).

"[@]bundles/common" => '../bundles/common.d.ts' => '../bundles/common.d.ts [declared module "index"]'   (OK)

"[@]bundles/core" => '../bundles/core.d.ts' => '../bundles/core.d.ts [declared module "index"]'   (OK)

"[@]bundles/*" => '../bundles/*.d.ts  => "[declared module *]" => "[declared module index]"  => "../bundles/*/index.d.ts" => "[declared module *]" => "[declared module index]"

Actual behavior:

It appears the path part of the import statement is currently expected to be included in the declared module name.

"[@]bundles/common" => '../bundles/common.d.ts' => '../bundles/common.d.ts [declared module "@bundles/common"]'   (FAIL)

Even if I name index.ts to common.ts for example, this will still fail because the resolution logic is expecting the "@bundles" path token to be in the declared module name.

I can coerce the expected behavior by creating a "@bundles" folder inside my module - but the whole point is that the "--paths" option shouldn't be married to the module structure.

/src/common
- @bundles/
-- common.ts

//src/common/@bundles/common.ts
export * from '../index'

//bundles/common.d.ts
declare module "index" {
  exports "file1
}

declare module "@bundles/common" {
   exports "index"
}
@mhegazy
Copy link
Contributor

mhegazy commented Sep 7, 2017

can you share a repo i can look at, the tsconfig.json seem to be missing baseurl. also it will be easier for me to look at the trace output.

@mhegazy mhegazy added the Needs More Info The issue still hasn't been fully clarified label Sep 7, 2017
@josh-sachs
Copy link
Author

josh-sachs commented Sep 7, 2017

I've created a repo here: https://github.com/josh-sachs/typescript-18311

@mhegazy
Copy link
Contributor

mhegazy commented Sep 7, 2017

there is not a module in file bundles\broken.d.ts, this is just a global file. so you need to change the file to be a module. i.e.

// bundles\broken.d.ts
export * from "resource-1";
// bundles\resource-1.d.ts
export class Resource1 {
}

@mhegazy mhegazy added Question An issue which isn't directly actionable in code and removed Needs More Info The issue still hasn't been fully clarified labels Sep 7, 2017
@josh-sachs
Copy link
Author

josh-sachs commented Sep 7, 2017

I'm not sure I follow...

All of the exports are declared as modules within the declaration files. The declaration files are generated by TSC via the compiler options:

-- moduleResolution: "node",
-- outFile: "...",
-- module: "system"
-- declaration: true
// bundles/broken.d.ts
declare module "resource-1" {
    export class Resource1 {
    }
}
declare module "index" {
    export * from "resource-1";
}
// bundles/workaround.d.ts
declare module "resource-2" {
    export class Resource2 {
    }
}
declare module "index" {
    export * from "resource-2";
}
declare module "@bundles/workaround" {
    export * from "index";
}

Typescript successfully resolves the declaration files broken.d.ts and workaround.d.ts but incorrectly scans them for the convention based entrypoint module "index" and instead only scans for an entry point named for the supplied path (e.g. "@bundles/workaround")

If I were not using the --outFile argument this wouldn't be an issue because typescript identifies typescript modules with an export statement as modules. The problem is that Typescript does not correctly apply the node module resolution strategy to the declaration files it generates.

In essence the declaration file produce by TSC in conjunction with the --outFile option should undergo an additional resolution step where the entire file is treated as if it were a folder... since that is basically what the amd and system module formats encapsulate as modules, and the module resolution strategy is node.

@josh-sachs
Copy link
Author

@mhegazy I think you were too quick to label this thread as a question. Please review again as there is an issue here with module resolution against declaration files generated via --outFile.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 11, 2017

A module is a file that has at least one top-level import/export declarations. a non-module file is refereed to as a global script. Modules have their own logic for scoping, declaration merging and resolution.

An ambient module declaration, something of the form declare module "resource-1" {...} lives in the global scope.
On a related note, compiling a with --outFile says this file is going to be in the global scope, otherwise, how else would it be consumed. if this is a node project, do not use --outFile, and instead distribute your modules as they are.

Now, for module resolution, the target of an import needs to be a module. the compiler asserts that. in the case of your import { Resource1 } from '@bundles/broken';.. here is what goes on:

  • there is a path mapping entry for this, the compiler will use it "@bundles/*": [ "../bundles/*" ]
  • this lands on a file, ../bundles/broken.d.ts, great, looks like what we were looking for..
  • but broken.d.ts is not a module..
  • error.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 11, 2017

more info about module can be found in http://www.typescriptlang.org/docs/handbook/modules.html

@josh-sachs
Copy link
Author

josh-sachs commented Sep 11, 2017 via email

@mhegazy
Copy link
Contributor

mhegazy commented Sep 11, 2017

The declaration file declares modules.

This file does declare modules, one called "index" and one called "resource-1"
The file itself (broken.d.ts), is not a module. it just happens that you are importing the file.

declaring a global module called "index" does not help resolving the module.

declare module "resource-1" {
    export class Resource1 {
    }
}
declare module "index" {
    export * from "resource-1";
}

Typescript resolves the modules, but not using the proper resolution strategy.

Not sure i understand what you mean.

@josh-sachs
Copy link
Author

Typescript allows import statements against the definition file.

If you get the repo and open src/app/main.ts you will see the following imports:

import { Resource1 } from '@bundles/broken'
import { Resource2 } from '@bundles/workaround'

the path '@bundles/*' is defined within tsconfig.json.

Typescript correctly resolves bundles\broken.d.ts and bundles\workaround.d.ts as containing module definitions.

Now notice the following:

import { Resource1 } from '@bundles/broken' - does not work.
import { Resource2 } from '@bundles/workaround' - does work.

The reason that the { Resource 1 } import does not work is because the bundles\broken.d.ts declaration file doesn't declare a module explicitly named:

/// this module is not declared within broken.d.ts
declare module "@bundles/broken" { 
    ...
}

The reason that the { Resource2 } import does work is because I've modified the source folder structure to include a folder called @bundles with a file inside called workaround.ts - this causes the workaround.d.ts file to contain a module explicitly named to include "@bundles/workaround":

/// this module is declared within workaround.d.ts
declare module "@bundles/workaround" { 
    export * from "index"
}

What is expected to happen is that TSC should automatically find the index module when when importing @bundles/broken because an explicitly named module is not present in the declaration file.

/// by convention, this is the default entry point for the *.d.ts file yet typescript doesn't recognize that currently.
declare module "index" {
....
}

Typescript already resolves barrels this way when --outFile is not present...

from: https://www.typescriptlang.org/docs/handbook/module-resolution.html

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json (if it specifies a "types" property)
  5. /root/src/moduleB/index.ts
  6. /root/src/moduleB/index.tsx
  7. /root/src/moduleB/index.d.ts

In this case, moduleB is a single file... lets call it moduleb.d.ts - typescript already succeeds to this point in scanning moduleb.d.ts for

declare module "moduleb" {
...
}

When it does not find that, it does not continue to apply the resolution steps 5-7.... it should do this - which would mean additionally scanning for:

declare module "index" {
...
}

within moduleb.d.ts.

Is this more clear now?

@mhegazy
Copy link
Contributor

mhegazy commented Sep 14, 2017

the two processes happen at different times. the module resolution is a preprocessing step, and does not know about what ambient module declarations are there in the global scope. at the time the second step happens it is too late to change the results of the first step. hope that clarifies it.

@josh-sachs
Copy link
Author

This doesn't make sense, as having an ambient declaration explicitly shimmed into the .d.ts file as with the example repo's workaround.d.ts package makes everything work fine.

import { josh } from @path/josh

/// josh.d.ts

// * this should work but doesn't
declare module 'josh' {
...
}

// * this should work but doesn't
declare module 'index' {
...
}

// * this works, and is an ambient declaration. 
declare module '@path/josh' {
...
}

When --outFile is applied, the entire declaration file is a system/amd representation of a package's physical file structure... tsc should expect the same naming conventions to apply.

Keep in mind none of this is an issue if you don't use the --outFile argument, but right now there is no way to tell tsc to bundle emitted javascript but to not bundle emitted declaration files. The only way to consume a bundle produced by tsc with the --outFile argument is to shim into the package a folder structure that anticipates the consuming project's tsconfig --paths setup which seems ridiculous.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 15, 2017

Keep in mind none of this is an issue if you don't use the --outFile argument, but right now there is no way to tell tsc to bundle emitted javascript but to not bundle emitted declaration files

this is correct. not sure why you need to bundle one and not the other. can you elaborate?
also have you looked at tools like https://github.com/TypeStrong/dts-bundle to bundel the .d.ts for you?

The only way to consume a bundle produced by tsc with the --outFile argument is to shim into the package a folder structure that anticipates the consuming project's tsconfig --paths setup which seems ridiculous.

that is not accurate. it depends on your scenario. you can just have a file that export * from "myBundeledModule";.

@josh-sachs
Copy link
Author

not sure why you need to bundle one and not the other. can you elaborate?

In practice I don't think you would want to, but the fact that the declaration file requires the package to have this hacky folder structure, that additionally must be aware of the consumer's --paths configuration, would create a need to.

that is not accurate. it depends on your scenario. you can just have a file that export * from "myBundeledModule";.

I assume you are keeping in mind that --outFile only works with System and AMD module formats.

That said, creating a file that exports the contents of the bundle will have the same issue... the declaration file is what appears to let TSC understand the shape of the System format bundle. TSC's expectation for the ambient modules declared within the dts file is not consistent with it's expectation for packages/barrells/etc that are defined within individual files.

The bundled dts file will need to declare an ambient module explicitly named "myBundeledModule", but when tsc produces the dts it uses the filename as the ambient module name... which means it is most likely going to be "index." Furthermore, if "myBundeledModule.d.ts" exists somewhere that is defined by --paths argument, then the expectation for the ambient module name currently changes even more, and does so unpredictably.

In the bundle, TSC does not honor the convention that the entry point for the bundle/package/barrel could be an ambient module named "index"

On the file system, when a module resolves to a folder, TSC does honor that convention by checking the folder for an index.d.ts.

What this leads to is a pattern where consuming bundles/packages created by tcs is inherently different than consuming "loose" packages/barrells/etc.

@josh-sachs
Copy link
Author

josh-sachs commented Sep 20, 2017

@mhegazy I'm interested in confirming whether or not the issue I've described is understood.

I'm attempting to split a project consisting of > 1000 ".ts" files into separate typescript projects, each emitting a single-file along with accompanying .d.ts definition.

The definition file is critical in making sure the system format single file is consumable across sections of the application.

-- The time it takes liteserver to deliver thousands of assets to the browser after a page refresh (during development) sucks... single file transpilation during development alleviate this.

-- I'm using the solution proposed in the following comment to achieve compileOnSave without the need to run any background tasks: microsoft/vscode#7015 (comment)

The only issue I have right now is that transpiling > 1000 files every time I save a file takes 5-6 seconds, which negates the benefits of the single file served via liteserver.

By breaking the project up into smaller packages I can reduce this to < 1s and still only require 10 or so assets to be fetched by the browser during development.

The way TSC generates the d.ts file right now for an --outFile package means that consuming the package during development requires a different folder structure than during build.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 22, 2017

I'm attempting to split a project consisting of > 1000 ".ts" files into separate typescript projects, each emitting a single-file along with accompanying .d.ts definition.

are these outputs consumed as commonjs modules? or amd modules? is there a bundler involved?

The only issue I have right now is that transpiling > 1000 files every time I save a file takes 5-6 seconds, which negates the benefits of the single file served via liteserver.

We have a change in #17269 that should make tsc --w much more efficient for modular code bases, and sounds like yours would benefit from that. would you be up for trying this change on your code base?

By breaking the project up into smaller packages I can reduce this to < 1s and still only require 10 or so assets to be fetched by the browser during development.

The part i am missing is why not just generate the .d.ts in a single file instead of multiple files?

@josh-sachs
Copy link
Author

are these outputs consumed as commonjs modules? or amd modules? is there a bundler involved?

During development, they are consumed as system modules. There is no bundler involved... I use --outFile in combination with --compileOnSave to produce the single .js output.

The part i am missing is why not just generate the .d.ts in a single file instead of multiple files?

When the entire source is included in a monolithic ts project (single tsconfig) the time for --compileOnSave action to complete to work is ~6 seconds. The project has distinct feature areas that can be compiled into their own independent single-files to reduce the --compileOnSave time for each individual feature area.

// monolithic  (saving any feature module re-transpiles all features into bundles/app.js)
-- src
---- feature1
------ index.ts
------ modulef1.ts
------ <additional modules>
---- feature2
------ index.ts
------ modulef2.ts
------ <additional modules>
---- tsconfig.json  [module system --outFile "../bundles/app.js"  --declaration true]
-- bundles
---- app.js
---- app.d.ts
// by feature (saving any feature only re-transpiles that feature's modules into bundles/<feature>.js)
-- src
---- feature1
------ index.ts
------ modulef1.ts
------ <additional modules>
------ tsconfig.json  [--module system --outFile "../../bundles/feature1.js"  --declaration true]
---- feature2
------ index.ts
------ modulef2.ts
------ <additional modules>
------ tsconfig.json  [module system --outFile "../../bundles/feature2.js"  --declaration true]
-- bundles
---- feature1.js
---- feature1.d.ts
---- feature2.js
---- feature2.d.ts

All of this works exactly I would expect, EXCEPT that none of the declaration files in either scenario currently produce an appropriate ambient module declaration for their entry points - index.ts - based on how TSC is currently resolving the ambient modules.

// app.d.ts
declare module "feature1/modulef1" {
  ...
}
declare module "feature2/modulef2" {
  ...
}
declare module "feature1/index" {
 export * from "modulef1"
 ...
}
declare module "feature2/index" {
  export * from "modulef2"
  ...
}
// feature1.d.ts
declare module "modulef1" {
 ...
}
declare module "index" {
  export * from "modulef1"
 ...
}

// feature2.d.ts
declare module "modulef2" {
 ...
}
declare module "index {
  export * from "modulef2"
 ...
}

Consider either of these import statements...

import { modulef1 } from 'bundles/app'  // <= this resolves to bundles/app.d.ts
import { modulef1 } from 'bundles/feature1' // <= this resolves to bundles/feature1.d.ts

TSC successfully locates the declaration file, but does not resolve modulef1 because there is no explicit declaration for bundles/modulef1 in either scenario. There is only feature1/modulef1 in the bundles/app.d.ts and modulef1 in bundles/feature1.d.ts

If I manually go into the either of the declaration files, and rename the ambient module declaration to bundles/modulef1 it will work... but that is not practical because it will be overwritten every time the declaration file is recreated.

The only way to coerce it to work is to use the hacky workaround I outlined in my original repro.

If I had not used --outFile argument, the resolution would be fine, because when TSC encounters a folder it checks for an index.ts entrypoint (steps 5-7 in module resolution). When it encounters the ambient declaration file, it does not apply steps 5-7 within that file (look for an ambient declaration named "index")

This is an oversight/bug in the resolution steps as it relates to declaration files generated alongside the --outFile argument. The path of the d.ts file is expected to be included in the ambient module name, and that is an unreasonable expectation (the d.ts file can be moved, can be mapped virtually with the --paths argument, etc).

I'm of the impression that I still haven't clarified for you what the issue is, but I'm not sure how to be more clear. This is not a workflow question, and I think the issue has been done a disservice to have been so hastily labeled as a "question." If the decision is "will-no-fix", then I guess that is that, but there is an issue I'm reporting, not a question.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 25, 2017

If I had not used --outFile argument, the resolution would be fine, because when TSC encounters a folder it checks for an index.ts entrypoint (steps 5-7 in module resolution).

Yes. and there is a reason why --outFile is not allowed for node output. if you are using node modules do not use --outFile.

@aluanhaddad
Copy link
Contributor

@josh-sachs have you considered emitting declaration files in a separate tsc pass?

One pass could create the bundle and another pass or even a parallel instance could create the declaration files. Just a thought.

@josh-sachs
Copy link
Author

@mhegazy I feel like I'm getting punked here... I know that --outFile can't be used with node modules, I've stated numerous times (and it is provided in the repro) that these are system modules being emitted as a single file via --outFile. TSC is attempting to provide Type resolution against the ambient modules within declaration file (*.d.ts) produced via the --declarations flag. I repeat - This is desirable! The only thing that is broken is that within the ambient declaration file the node module resolution strategy is not applied correctly. The .d.ts file generated by TSC correctly declares ambient modules relative to the physical locality of the module (.ts) file they represent with respect to the tsconfig.json. TSC does not correctly apply module resolution within the declaration files it produces. An ambient module named "index" within a *.d.ts file should be treated identically to a physical module file named "index.ts" within a folder.

@aluanhaddad I'm not trying to create individual declaration files alongside a single .js file. I'm trying to consume the single definition file generated by TSC via --outFile but TSC is not correctly resolving the ambient declarations.

Can someone confirm that they have actually forked the repro I provided locally and observed the phenomenon I'm describing?

@mhegazy mhegazy added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Question An issue which isn't directly actionable in code labels Sep 26, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Sep 26, 2017

As I have described earlier. declare module "foo/index" does not work for your scenario. --outFile declaration merging is meant for a specific scenario, which is bundling one library in one .js and one .d.ts file. it is meant to be used with AMD modules (possibly in conjunction with /// <amd-module name="" /> directive or "paths" mapping in your tsconfig.json.

The scenario you describe is not supported.

I'm of the impression that I still haven't clarified for you what the issue is, but I'm not sure how to be more clear. This is not a workflow question, and I think the issue has been done a disservice to have been so hastily labeled as a "question." If the decision is "will-no-fix", then I guess that is that, but there is an issue I'm reporting, not a question.

I agree this does not seem like a question. relabeling as such.

@josh-sachs
Copy link
Author

The issue I'm reporting is exactly that, one .js file and one .d.ts file. The only reason anything else was even brought into the thread is because the discussion was steered away from the issue into unsolicited workflow suggestions.

per your own documentation, and the actual observed behavior of tsc, --outFile works for system and amd modules. I've repeatedly said I'm generating a single file library using the system module format, and alongside which a corresponding .d.ts file is being generated which contains a collection of ambient module declarations describing the library.

--moduleResolution strategy is node, but at no time have I indicated that I am attempting to generate node modules.

It's become seeming clear that you've not taken the time to review what I've put together as part of a working environment. Its frustrating because the error here is so extremely simple to observe if you would just do so. When you import a library defined via ambient modules in a .d.ts file, type resolution should continue to work as if you were importing the non-ambient modules -- after all, that is the entire freaking point of the ambient declarations.

Right now, you are basically asserting that for a situation where a single .d.ts file, which accurately describes a corresponding system or amd format library, tsc should make no effort to correctly apply the node resolution strategy against the library's declared ambient modules.

*** I REPEAT AGAIN EVERY SINGLE ASPECT OF THIS WORKS WITH ONE ASININE EXCEPTION ***

The exception is that tsc generates the .d.ts file with an ambient module named "index", but when consuming the d.ts file does not interpret the ambient module named "index" as the library's entry point during type resolution.

All I've asked - repeatedly - was that you give some indication that I've successfully communicated to you what the issue is that I'm reporting. It appears there has been some failure in communication between us and instead of making honest efforts to rectify, you've chosen to hastily take innacurate administrative action against the ticket, denying anyone else on your team the opportunity to review the issue and see if they might not be able to come to a better understanding of what I'm reporting.

From: https://www.typescriptlang.org/docs/handbook/module-resolution.html

How TypeScript resolves modules

TypeScript will mimic the Node.js run-time resolution strategy in order to locate definition files for modules at compile-time. To accomplish this, TypeScript overlays the TypeScript source file extensions (.ts, .tsx, and .d.ts) over the Node’s resolution logic. TypeScript will also use a field in package.json named "types" to mirror the purpose of "main" - the compiler will use it to find the “main” definition file to consult.

For example, an import statement like import { b } from "./moduleB" in /root/src/moduleA.ts would result in attempting the following locations for locating "./moduleB":

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json (if it specifies a "types" property)
  5. /root/src/moduleB/index.ts
  6. /root/src/moduleB/index.tsx
  7. /root/src/moduleB/index.d.ts

When this process succeeds at steps 3 or 7, any type resolution should evaluate the d.ts file for...

declare module "moduleB" or declare module "index"

... because when tsc generates the d.ts for a library via --outFile argument, there won't be an ambient module declared as declare module "./moduleB" which is currently what the type resolution process expects.

-- This is a BUG.

@kitsonk
Copy link
Contributor

kitsonk commented Sep 27, 2017

Calm down dude... Code of Conduct

@RyanCavanaugh
Copy link
Member

It appears there has been some failure in communication between us and instead of making honest efforts to rectify, you've chosen to hastily take innacurate administrative action against the ticket, denying anyone else on your team the opportunity to review the issue and see if they might not be able to come to a better understanding of what I'm reporting.

Honestly I've been reading this ticket as well but right now it's just a wall of angry text and it doesn't seem like we're going to reach mutual understanding with this attitude.

If this is really as straightforward as you're trying to make it sound, there should be a three- or four-file repro.

@josh-sachs
Copy link
Author

josh-sachs commented Sep 27, 2017

Calm down dude... Code of Conduct

I apologize if too much of my frustration trying to communicate the issue has come through in my responses. Honestly, I've been trying to go to great lengths to make sure that my followups remain courteous and focus strictly on shoring up any points where there may be lack of clarity - I'm not sure certain which point in the code of conduct I could possibly have violated. I haven't insulted anyone, used vulgar language, or even yelled (all caps, etc) for that matter. There is one line in my last post that was caps, but only used as emphasis. If I've violated some grammar, language or forum style rule that has resulted in something being misconstrued otherwise I'm really sorry.

Honestly I've been reading this ticket as well but right now it's just a wall of angry text and it doesn't seem like we're going to reach mutual understanding with this attitude.

Again - I'm sorry if somehow I've triggered something that has made anyone feel the need to be defensive. I've reviewed the entire thread and while my responses are verbose they only contain information to help achieve clarity. I'm at a loss for how it could be construed as a wall of angry text - but again I realize communication is a two-way endeavor, so if what I'm saying is being received in an unintended way than I recognize I must not be saying it correctly.

All of that said I'm glad to know there are additional eyes on the issue. Assuming we can move past any misunderstandings as it relates to decorum, I want to provide a brief primer to make sure that all of the terminology I'm using is up-to-snuff.

  • barrel - a folder containing a conventionally defined entry point index.ts which exports the barrel's modules.
  • module - a *.ts file that has exactly one export.
    e.g. export class module { ... }
  • library - the resulting single file <_library_>.js output resulting from tsc transpilation against the --outFile argument. It contains transpiled javascript code in either the system or amd module format.
  • declaration file - describes a library for type-checking. It is the resulting single file <_library_>.d.ts output resulting from tsc transpilation against --outFile and --declaration arguments. It contains ambient module declarations for all of it's library's modules (including the module's entry point)
  • ambient module - describes a module for tsc type-checking. For the purpose of this issue, they are defined exclusively within <_library_>.d.ts and are generated automatically by tsc when it creates the declaration file.
    e.g. declare module "module" { ... }

If this is really as straightforward as you're trying to make it sound, there should be a three- or four-file repro.

I've provided a repo at the beginning of the thread, it consists of more than three or four files, but only because that is what any project seems to require these days simply in order to make it build.

https://github.com/josh-sachs/typescript-18311

The src folder contains what are essentially two barrels, one called "broken" and one called "workaround." Each barrel consists of exactly the minimum amount of files you could possibly expect in order for them to be transpiled into a library:

-- index.ts            // entry point for the barrel
-- resource-<#>.ts     // a single module representing the meat of the barrel.
-- tsconfig.json       // required by tsc

The "workaround" barrel contains one additional folder, inside which there is one additional file. The intent of which is to help illustrate the problem.

-- @bundles/workaround.ts

If you fork the repo, and transpile the two barrels, it will create two corresponding libraries and two corresponding declaration files (4 files total).

-- bundles/broken.js
-- bundles/broken.d.ts
-- bundles/workaround.js
-- bundles/workaround.d.ts

the declaration file broken.d.ts defines two ambient modules.

// broken.d.ts
declare module "resource-1" {
    export class Resource1 {
    }
}
declare module "index" {
    export * from "resource-1";
}

the declaration file workaround.d.ts similarly defines two ambient modules, but includes a third ambient module as a result of the barrel's additional folder structure.

// workaround.d.ts
declare module "resource-2" {
    export class Resource2 {
    }
}
declare module "index" {
    export * from "resource-2";
}
declare module "@bundles/workaround" {
    export * from "index";
}

Once the libraries have been transpiled, open up the file /src/app/main.ts (vscode or any other tsc aware editor)

Observe the type-checker does not successfully resolve that there is a module named { Resource1 } within the library @bundles/broken

Observe the type-checker does successfully resolve that there is a module named { Resource2 } within the library @bundles/workaround

It is clear by this example that the latter works because the declaration file contains an additional ambient module declared by the name "@bundles/workaround". I was able to coerce the transpiler into creating this additional ambient declaration by adding an additional module to the barrel (see above).

The issue is that '@bundles' is a --path defined in the app's tsconfig.json. The library would typically have no knowledge of the consuming application's --paths configuration. Furthermore, even if we were to omit @bundles/, the type-checker would still expect that an ambient module explicitly named "workaround" (or "broken") exist within the declaration file, and it would preclude any consumer of the library from using --paths option at all.

In practice, the convention is that a barrel contain an index.ts file, which in this case results in an ambient "index" module within the declaration file. When the type-checker is evaluating a declaration file's ambient modules, it should successfully interpret any ambient module named "index" as a valid entry point.

@RyanCavanaugh RyanCavanaugh added Needs Investigation This issue needs a team member to investigate its status. and removed Working as Intended The behavior described is the intended behavior; this is not a bug labels Sep 27, 2017
@josh-sachs
Copy link
Author

Thank you for taking the time to reassess! If there is any way for me to buy you guys a beer or twelve, I would be happy to do so. Again - I apologize for any perceived hostility in the thread, that was not my intent.

@pocesar
Copy link

pocesar commented Oct 2, 2017

#15951 is related, I'm still seeing the behavior (of erroneous file paths when using "system" and side-by-side definition files). it's hard to understand why, although I'm using a symlink instead of subfolders

@josh-sachs
Copy link
Author

@pocesar I reviewed #15951 but I'm not sure they are related beyond having impact on --outFile and .d.ts generation. The specific issue I'm trying to highlight relates to ambient module resolution within a declaration file produced by tsc. I believe resolution candidate for this ticket would be a change so that the tsc type-checker recognizes an ambient "index" module declaration as an entry point for resolving additional ambient modules within the declaration file.

Only providing clarification to prevent them from being combined as duplicates.

@pocesar
Copy link

pocesar commented Oct 5, 2017

@josh-sachs yeah, just pointed out that it's the same issue when using system, but different outcomes. the common points are: using system, outFile (works with with outDir) and declaration set to true. they are definitely separated issues, but has some similarities on the generated files

@RyanCavanaugh
Copy link
Member

@rbuckton FYI just pick up the thread here

@RyanCavanaugh RyanCavanaugh added the Rescheduled This issue was previously scheduled to an earlier milestone label Aug 31, 2020
@rbuckton
Copy link
Member

rbuckton commented Nov 4, 2020

There are a number of factors that are causing this issue:

  1. When we emit a bundled .d.ts file, we emit module IDs (such as broken/index) for the ambient module declarations, as we do not allow "relative" paths (like ./index) in module declarations.
  2. We follow the NodeJS module resolution algorithm when resolving these packages, such that "broken" is considered a module ID, not a path. In general this means that you cannot write import {} from "../bundles/broken" in an adjacent directory, as that resolves to a path, not a module ID.
  3. Our --path resolution does not look at a file named broken.d.ts that contains a declare module "broken" {} and think those things mean the same thing (because often they don't).

This is further complicated by the fact that in TS 4.1 we are adding a new --bundledPackageName option that you must specify if you are using --outFile and --moduleResolution node along with either --module system or --module amd.

I don't think we can change any of those behaviors without a number of unintended consequences. We may instead need a way to opt-in to a differing behavior. For something like this to work we would need a way to specify that the bundled modules should be treated as relative paths in some way.

One option might be to have a syntactic marker to indicate relative module resolution within the file:

// broken.d.ts
declare module "broken/resource-1" {
    export class Resource1 {
    }
}
declare module "broken" {
    export * from "broken/resource-1";
}

// indicate this file is a bundle and paths are resolved relative to the bundle:
declare module "."; 

// or (to be more specific about the entry point):
declare module "broken" as "."; 

Or, alternatively:

// broken.d.ts
declare module "./resource-1" { // resolved as `broken/resource-1` (from the file name)
    export class Resource1 {
    }
}
declare module "." { // resolved as `broken` (from the file name)
    export * from "./resource-1";
}

Another might be to modify paths to add a specific mechanism for resolving non-relative paths from within a bundle:

{
  "compilerOptions": {
    "paths": {
      // one placeholder with *-1 cardinality could indicate a bundle?
      "@bundles/broken/*": ["../bundles/broken"],

      // two placeholders with 1-1 and *-1 cardinalities (respectively) could indicate nested bundles?
      "@bundles/*/*": ["../bundles/*"],

      // object literal to specify behavior?
      "@bundles/*": [{ "path": "../bundles/*", bundle: true }]
    }
  }
}

/cc @weswigham, @andrewbranch for some additional perspectives.

@RyanCavanaugh RyanCavanaugh added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone labels Nov 12, 2020
@RyanCavanaugh
Copy link
Member

Long thread but I think @rbuckton 's comment summarizes it well.

We'd be open to a concise suggestion for some new declaration style that enables this scenario to work more ergonomically, but the behavior as-is in this scenario is the intended behavior, not a bug. The longstanding lack of feedback on this one would indicate that this isn't a mainline scenario blocker.

@wycats
Copy link

wycats commented Feb 2, 2021

@RyanCavanaugh What is a user supposed to do if they come across a package that points at a .d.ts file that contains a module declaration, and you want to map to it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

9 participants