rollup-plugin-typescript2: Declaration generated in wrong folder when using object as `input` (multi-entry)

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.

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 14
  • Comments: 35 (11 by maintainers)

Commits related to this issue

Most upvoted comments

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.

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.

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

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.

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

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.

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?

No, plugin uses LanguageService (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…

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.

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.)

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’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. 😃

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.

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?

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.

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)

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.

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

This might be fixed by #142, released in 0.20.0