Rollup-plugin-typescript2: Typings file generated in wrong folder within object as `input` rollup

Created on 5 Feb 2019  ·  33Comments  ·  Source: ezolenko/rollup-plugin-typescript2

What happens and why it is wrong

Possibly this is a misconfiguration on my end, but it looks like a bug. When you use an object as the input property for the rollup configuration, the typings file for the input files are generated in the root folder of the repo (same folder as the rollup config file) and not the target output directory (here, ./dist) along with the bundled JS file.

Versions

  • typescript: 3.2.4
  • rollup: 1.1.2
  • rollup-plugin-typescript2: 0.19.2

rollup.config.js

{
    ...
    input: {
        Lib1: './src/Lib1.tsx',
        Lib2: './src/Lib2.tsx'
    },
    output: {
        dir: './dist',
        format: 'cjs',
        sourcemap: true,
        entryFileNames: '[name].js'
    }
}

tsconfig.json

{
  ...
  "compilerOptions": {
    "outDir": "./dist"
  },
}

Result:

// These files are created
./Lib1.d.ts
./Lib2.d.ts

// instead of (expected):
./dist/Lib1.d.ts
./dist/Lib2.d.ts

Curiously, if you enter entryFileNames: 'dist/[name].js' it creates a superfluous subfolder called dist within the dist folder and creates them there.

bug more info needed

Most helpful comment

Yup. that seems to be the only way unless you let tsc do the bundling end-to-end.
These rollup plugins seem to be effecively asking tsc to do two different tasks:

  1. "Type-check and convert all these individual modules to JavaScript" and let me (i.e. Rollup) do the bundling and writing to disk.
  2. "Make all the declarations for this TS project folder and dump them over there."

The results of 1 and 2 don't, and can't match when you're using an "input map" and code-splitting, etc. – AFAICT.

...unless you make the rollup plugin a whole lot more involved in the type-declaration generation process.

All 33 comments

The useTsconfigDeclarationDir plugin option is a workaround for now.

This might be fixed by #142, released in 0.20.0

Hi @ezolenkom & @jakearchibald, no luck on either actually.

When adding the useTsconfigDeclarationDir option in my rollup plugin the declaration files are no longer created at all. And updating to 0.20.1 didn't make a difference, unfortunately.

@benkeen when using useTsconfigDeclarationDir: true plugin option you also need to have declarationDir value in tsconfig.json

have similar trouble, my project layout is like this:

export default [ 
  {
    input: 'src/subFolder1/one.ts,
    output: [
                    {
                      file: 'dist/one.js'
                      .....                      
                     }
    ],
    plugins: [
           typescript({
                typescript: require('typescript'),
            }),
    ]
  },
      input: 'src/subFolder2/two.ts,
      output: [
                    {
                      file: 'dist/two.js'
                      .....                      
                     }
    ],
     ...
  },
]

As an output I have:

  |---subFolder1
  |          |--------one.d.ts
  |
  |---subFolder2
  |          |---------two.d.ts
  |-----one.js
  |-----two.js

Works for me on 0.20.1 with useTsconfigDeclarationDir: false -- d.ts files go into the folders specified in output.[].dir. @benkeen could you try again?

@AntonPilyak that is by design -- if d.ts were going into one folder without sub paths, then if you had subFolder1/one.ts and subFolder2/one.ts, one of those would be overwriting another.

@ezolenko : this feature, I think, creates more inconveniences rather then solves a real problem (due to it is hard to maintain all those directory structure). I have to create a script that copies all these type declarations to the root of the bundle, and add the directories to .npmignore in order to create a bundle that is easy to include. I would suggest you to create these additional directories ONLY for the type files with the same names. And use a directory defined in 'output' section of rollup.config.json for the rest.

@AntonPilyak you might be able to use useTsconfigDeclarationDir: true option, setup declarationDir in tsconfig, this should let typescript itself handle the typings output paths.

Deciding if subdirectories should be used based on uniqueness of the paths would be a nightmare in an evolving project, suddenly your typing would move to a subfolder only because you added a new file with the same name in a different folder.

Normally if you want pretty typings you will want to merge them into one file anyway (there was an npm module I keep forgetting the name of for that)

I am using version 0.22.0 and I have pretty much the same problem as @AntonPilyak

{
    ...
    input: {
        foo: 'src/lorem/foo.ts',
        bar: 'src/ipsum/bar.ts'
    },
    output: {
        dir: 'dist',
        format: 'cjs',
        sourcemap: true,
    }
}

And I get this out:

dist/
    lorem/
        foo.d.ts
    ipsum/
        bar.d.ts
    foo.js
    foo.js.map
    bar.js
    bar.js.map

Obviously, I'd like the *.d.ts files to sit alongside the *.js and *.js.map files.

I've tried fiddling with useTsconfigDeclarationDir and declarationDir to no avail.

The definition files always end up inside lorem and ipsum sub-folders respectively, and never in a flat list.

Moreover it seems like it creates declaration files for every .ts file it finds, not just the entry points.

dist/
    lorem/
        foo.d.ts
    ipsum/
        utils/
            bar-helper.d.ts
        bar.d.ts
     some_other_ts_file/
         not_imported_by/
             neither_foo_nor_bar/
                  wat.d.ts
    foo.js
    foo.js.map
    bar.js
    bar.js.map

I've tried scoping options.tsconfigOverride.input to just the entry points, but that seems to have no effect whatsover. In fact I'm not even sure if tsconfigOverride.input works at all.

This is all a bit confusing.

...

I mean, fair enough if it needs to create a declaration file for utils/bar-helper.ts but at least wat.ts should be left out of the whole thing.

Yeah, it generates declarations for all files found by tsconfig, if you don't want particular file touched, make sure it is excluded or not included in tsconfig. To see list of files found by typescript, run plugin with verbosity 3 and look for "parsed tsconfig" entry.

For definitions path, where in your example would you want definition for 'src/ipsum/foo.ts' to sit?

Since it is a definition for dist/foo.js I had assumed it would end up next to it - just like the source map file – i.e. in dist/foo.d.ts.

Will TypeScript know to pair up dist/foo.js and dist/lorem/foo.d.ts?

The full scenario where this breaks down is:
src/dir1/foo.ts
src/dir2/foo.ts

dir1/foo.ts us set as rollup input and it imports dir2/foo.ts. Rollup will create dist/foo.js bundle that will contain relevant parts of both source files, but typescript needs to create 2 type definition files with the same name somewhere.

Easy solution is to use subfolders, mirroring source layout. I think that's what tsc does as well, unless you tell it to merge type definitions (rpt2 doesn't support definition merging).

And yes, typescript will know where to find type definitions. So aside from being a bit messy, this shouldn't be a problem. If you want to deploy package with types on npm, set "types" in package.json to d.ts file for your entry point. Typescript will find things from there.

Ah, so this plugin doesn't really manage any part of the declaration-build process. It just fires up tsc to do its thing, irrelevant of Rollup's process?

Still, if I'm only exposing/publishing a couple of modules, why would I want TypeScript to generate *.d.ts files for every single ts file it encounters? Is there no way to limit it to just the entry points?

Thing is, I'm exporting a set of standalone modules (import tool1 from 'myTools/tool1'; etc..) so there's no single entry point to use in pkg.types. Each definition file must reside next to its *.js counterpart for VSCode to pick it up.

Worse still, if I have

{
    ...
    input: {
        fooscript: 'src/foo/index.ts',
        barscript: 'src/bar/index.ts'
    },
    output: {
        dir: 'dist',
        format: 'cjs',
        sourcemap: true,
    }
}

I get:

dist/
    foo/
        index.d.ts
    bar/
        index.d.ts
    fooscript.js
    fooscript.js.map
    barscript.js
    barscript.js.map

And now there's no way for TypeScript to pair up dist/fooscript.js and dist/foo/index.d.ts.

...

Is there no way your plugin could feed TypeScript the destination/output path for each JavaScript bundle file? It seems that tsc is receiving the original entry filename and holding on to it tight when generating the declarations.
...or for each of the input files hunt for the corresponding *.d.ts file and move+rename it to the input-file map destination. (You may notice I am talking out of my ass here, so... ;-)

This is something I'm bound to try manually after Rollup has finished - but it would be so much nicer if the plugin would handle it automatically.

No, plugin uses LanguageServices (same typescript API IDEs are usually using), so I have some control on what to write and where.

To write one type definition per rollup input we'll need to merge definitions for all imports for that input. Rollup might be seeing one input ts file, but it could be importing and bundling any number of source files when building that, those need types generated as well.

Currently I'm erring on the side of generating definitions for everything typescript finds when looking at tsconfig file. If I generate types only for transpiled modules, type only files will be ignored.

I might have to revisit that part, API changed considerable since that part was done...

I'll have to look at how to make your example work. Might need better support for multiple bundles in one rollup config.

How do you consume that package after building it though? It is not something you can publish on npm and import by package name...

I'm making a collection of standalone utilities. The root-folder of the published/installed package contains all the built module files in a flat list.

(I'm writing them in TS/ES6 in a nested folder-structure, with test-files mixed in, and relying on Rollup to convert them to a flat list of CJS files with an efficient mix of code-splitting and inlining for any shared code.)

I then consume these tools by importing them by file-name – like so:

import tool1 from 'myTools/tool1';
import tool2 from 'myTools/tool2';

The idea is that by not having a single entry point for the whole toolkit (no pkg.main or pkg.module entries), I can be care-free to add rarely-used, and even experimental, tools without adding weight/bloat for downstream consumers (which might not have efficient tree-shaking).

...I've been playing with tsc on the commandline a bit now, and I think I see now the limitations you're dealing with.

I noticed, however, that if you run tsc and set the module/output format to either "amd" or "system", it generates a single *.d.ts file with a neat list of all the necessary declare module "..." blocks inlined.

I wonder if this behavior could be exploited somehow - via file rename/move and unwrapping/rewriting the last (main) module declaration?


Side note: I also notice that tsc doesn't seem to put much effort into tree-shaking that declaration file. In one case I see a main (entry point) module that exports a single, very simple, function signature, and yet the declaration file exposes declaration blocks for all the private modules used "behind the scenes". Funny. :-)

Hi again.
For now, I'm setting a custom declarationDir for tsc to dump all the *.d.ts into, and then run a standalone script that imports the same entry-file-to-output-file object as I feed to Rollup, and it generates bundle-destination-level declaration files that simply export * from the actual declaration file.

Essentially, I generate dist/fooscript.d.ts which contains only:

export *  from './__types/foo/index';

It's an ugly standalone hack, but it provides predictably correct results.

I wonder if rollup-plugin-typescript2 could do something similar?

That would work, yeah.

I have a couple of ideas (when I get around to them), for example generating <bundlename>.d.ts that has bunch of /// <reference types=""/> for each d.ts involved.

First I might have to fix related bug of generating too many type declarations and limit that to only root file and anything it actually imports.

A few thoughts:

IMO, the only truly ugly part of my hack is the fact that it stands apart from the Rollup process. The intermediary *.d.ts are actually a pretty neat and idiomatic way to bridge between Rollup's generated bundles and tsc's definitions.

Since the tsc-generated declaration files reference each other via relative paths, that file/folder structure is probably best left alone – lest you end up re-implementing parts of Rollup's functionality, for a custom language syntax.

And if rollup-plugin-typescript2 sets something like output.dir + '__types' as the default declarationDir, then any mess caused by tsc's over-eagerness is neatly swept out-of-sight inside a clearly-named folder. That way the extraneous definition files become a very minor concern IMO.


FWIW, here's the meat of my script:

const { makeInputMap, getEntrypoints, distFolder, srcFolder } = require('./buildHelpers');
const { writeFileSync } = require('fs');
const { relative } = require('path');

const srcPrefixRe = new RegExp('^' + srcFolder + '/');
const tsExtRe = /\.tsx?$/;

const declDirRelative = './' + relative(
  distFolder,
  require('../tsconfig.json').compilerOptions.declarationDir
);

const tsEntrypoints = getEntrypoints()
  .filter((fileName) => tsExtRe.test(fileName));

Object.entries(makeInputMap(tsEntrypoints))
  .forEach(([moduleName, sourcePath]) => {
    const tscDeclFile = sourcePath
      .replace(srcPrefixRe, declDirRelative + '/')
      .replace(tsExtRe, '');
    const outFile = distFolder + '/' + moduleName + '.d.ts';
    writeFileSync(
      outFile,
      [
        'export * from "' + tscDeclFile + '";',
        'import x from "' + tscDeclFile + '";',
        'export default x;',
        '',
      ].join('\n')
    );
  });

console.info('Created local declaration files for TypeScripted entrypoints.');

I updated the example above, as it seems that default exports have to be explicitly re-exported.

Preliminary testing indicates that blindly re-exporting default from a declaration file that has no default export is harmless, and silently ignored. (At least by VSCode.)

Hi, should this issue be resolved in v0.23.0 ?

(...wondering if I should try and update my project to drop all of the custom faffing)

0.23 will not generate declaration for everything in sight, but it will still generate declarations you actually import in their relative subfolders. You can probably update and have lighter types, but there is nothing to generate root type declarations yet.

My custom workaround build script (see above) is gradually creeping into more and more of my projects. o_O

FYI: Development of the official @rollup/plugin-typescript package has since resumed, and it now features type-checking and outputting declaration files, so I'm personally switching over to use that.
The limitations are much the same, however.

@maranomynet Do you still rely on your script to move the declaration files after rollup does its thing? I didn't have much luck switching myself.

Yup. that seems to be the only way unless you let tsc do the bundling end-to-end.
These rollup plugins seem to be effecively asking tsc to do two different tasks:

  1. "Type-check and convert all these individual modules to JavaScript" and let me (i.e. Rollup) do the bundling and writing to disk.
  2. "Make all the declarations for this TS project folder and dump them over there."

The results of 1 and 2 don't, and can't match when you're using an "input map" and code-splitting, etc. – AFAICT.

...unless you make the rollup plugin a whole lot more involved in the type-declaration generation process.

Ah yeah that's sort of what I figured, thanks! Gave you a 👍 for your helpful answer, and the 👎 is for how I feel about it.

Any luck with this?

For any struggling soul out there that has the same src folder structure as @maranomynet and me.

I came across a quick fix for it, may be another ugly way but works for me.

I'm using rollup-plugin/typescript2 so I'll go with its config in this answer:
Define your own declarationDir in your tsconfig.json so your project can dump all *.d.ts files in its own path in the bundled dist. And make sure to set the useTsConfigDeclarationDir to true in your typescript plugin in rollup config file.
Also, where you define the output paths for your individual component bundles in your rollup.config.js (and package.json), change those paths to be the same as your 'declarationDir' + 'how your src component route is'. So if your component in src is like:

src/homepage/foo.tsx

And your declarationDir is:

dist/MyDeclarationDir

So your output path needs to be like:

dist/MyDeclarationDir/homepage/foo.js

This way, rollup will include your types in the same directory as your component main.js and your TS consumer project will pick up the typings.

So the bundle will look something like:

dist/

    declarationDirPath/
                  component1/
                        foo.js/
                              foo.js
                              foo.map.js
                        foo.d.ts
                   component2/
                        bar.js/
                               bar.js
                               bar.map.js
                        bar.d.ts
Was this page helpful?
0 / 5 - 0 ratings