Typescript: Transpile to multiple targets at once

Created on 14 May 2017  ยท  12Comments  ยท  Source: microsoft/TypeScript

By allowing multiple targets, transpilation speeds would be vastly improved for any project doing this already, as the AST only needs to be generated once.

Awaiting More Feedback Suggestion

Most helpful comment

also interested in this. That said as opposed to having different output directories based on module, I'd like to see an option for different output file extensions, so I don't have to split by folder.

Yes! With multiple folders you may have duplicate declaration files (not ideal). For any npm package, I should be able to have a configuration that outputs both CJS and ESM, but only one set of declarations like this:

{
  "compilerOptions": {
    "outDir": "./dist",
    "emit": [
      { "module": "CommonJS", "declaration": true },
      { "module": "ESNext", "extension": "mjs" },
    ]
  }
}

The output should be like this:

.
โ”œโ”€โ”€ dist
โ”‚   โ”œโ”€โ”€ index.d.ts
โ”‚   โ”œโ”€โ”€ index.js
โ”‚   โ””โ”€โ”€ index.mjs
โ””โ”€โ”€ src
    โ””โ”€โ”€ index.ts

And the package.json would look like this:

{
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts"
}

Perhaps even a preset that does the same thing would be in order?

{
  "compilerOptions": {
    "outDir": "./dist",
    "emit": ["npm"]
  }
}

All 12 comments

transpilation speeds would be vastly improved

Maybe, but this really depends a lot on your project. Type-checking will need to be redone depending on your compilation target, and since type-checking tends to take the longest, you might actually be better off using concurrently to run several different tsc instances.

Since the compiler is single-threaded, parallelizing the builds as suggested should be nearly "free" in terms of end-to-end time.

I would also like to see multiple modules supported.

At present, very very few TypeScript packages on npm have builds for EcmaScript Modules, and this is a huge barrier to JavaScript moving to a better, more consistent, future.

Being able to output a ES5-target CommonJS-module and a ESNext-target ESM-module (at the same time) would be ideal.


I run into non-ESM supporting modules pretty regular, and griped about it on twitter recently. @robpalme talked about writing up some more official guides for how to dual-publish, to keep TypeScript from becoming the IMHO log-jam of ESM compatibility issues it's on course for now. And I think this ticket could go a long way towards easing this backup, giving A) package maintainers better tools to write packages that support a modern, decent future while also continuing to have very backwards compatible output, and B) package consumers more ready access to ESM versions that they crave.

Ideally there could even be some complex targets built into TypeScript that could do all this. With Node's new ESM implementation, for example, ideally I could imagine a --target=dual that produced (es5) .cjs & (esnext) .mjs files at the same time. This would be so good & helpful, insuring TypeScript's compilation doesn't become a major roadblock on the golden path to ESM. --target=dual should be the default target.

@robpalme talked about writing up some more official guides for how to dual-publish, to keep TypeScript from becoming the IMHO log-jam of ESM compatibility issues it's on course for now

The advice is simple right now: _Don't._

There's no equivalency for node's es modules as they are currently designed - the cjs module and esm module can't be drop in replacements for one another. (And very few people on the modules WG seem to value this feature, so it's unlikely to change) You choose one style, and you stick with it, and you get all the downsides associated with that. Now, that still has a chance to change (and I sincerely hope it does), but as it is right now, if you value compatibility at all, you should still _just_ be shipping cjs. Attempting to ship esm "side-by-side" is just going to create runtime confusion as you have the esm version and the cjs version of your package both being included via different means. (Once via imports and once via requires.) If you don't value compatibility, just only ship esm (once it's supported - I wouldn't publish a package with esm before then, since how it works in total is still a big open experiment).

Right now, there's no good way to ship both and have a _consistent_ view of your package. As-is, it's at best like having two packages with divergent API surfaces stuffed into the same namespace, which is a _terrifyingly bad experience_ for anyone who didn't want to think about how you shipped your package and what format you shipped it in.

This would be so good & helpful, insuring TypeScript's compilation doesn't become a major roadblock on the golden path to ESM

Node's current implementation doesn't have a golden path to esm, btw, and TS isn't something in the way of that. It's (current, admittedly nonfinal) design decisions favor alignment with browsers over all else, to the detriment of anyone hoping for a simple migration from cjs to esm or from babel/ts-transpiled esm to node's esm. Migration stories and golden cowpaths have not been prioritized much at all at this point - "browser alignment" reigns as feature supreme. A handful of feature additions could fix this, though - and should that happen, I'll happily revise my stance here.

fwiw, an explicit goal of our implementation is to allow a cjs and esm source to coexist in the same package. we just haven't figured out the specifics of how that will actually work.

Righto, it still could work out OK, the currently open proposal for dual mode packages is, to me, pretty much a nonstarter, though. A few changes that make it such that cjs is only loaded in older node and the esm is always loaded in newer nodes that support it would be all that it needs to become relatively workable, however - and at that point there's definitely _some_ value in attempting to ship multiple formats at once. Sadly, that might mean your package might have different API surfaces on node 11 vs node 12, which'd still be bad, but it'd be more workable than having multiple API surfaces _within the same runtime_.

In any case, the OP's question was free of es6 module connotations that drive me into a rant, and I can see some kind of value in being able to, for example, output amd and cjs modules side-by-side. So long as we take the most "restrictive" of the chosen output formats (or if the module setting required for a check is one of the output module kinds), I could see us being able to only typecheck once, which could allow for some (total computation used - not wall clock time) savings at emit time. I'm unsure how the output would be handled though? Multiple outDirs? Maybe a new path-mapping like config like:

{
   "compilerOptions": {
        "emit": [
            {"outDir": "./amd", "module": "amd"},
            {"outDir": "./cjs", "module": "cjs"},
        ]
    }
}

where each element of emit is some subset of our emit-related compiler options could work?

@weswigham keep in mind that you have to also provide entry points per target, those are not always the same

Type-checking will need to be redone depending on your compilation target, and since type-checking tends to take the longest, you might actually be better off using concurrently to run several different tsc instances.

@DanielRosenwasser When specifying the libs explicitly, there would be no difference in type-checking, but rather just in emit, right? For our browser-based app we currently have ES5 + corejs to polyfill the ES6 functionality we need, and therefore explicitly specify the lib anyways.

We'd like to have both ES5 and ES6 js files generated and use a detection to configure the module loader (AMD / RequireJS in our case) to use ES6 sources if possible, and ES5 otherwise. So +1 for having multiple targets (and possibly module systems if these do not affect AST generation) configurable on a single compile run.

also interested in this. That said as opposed to having different output directories based on module, I'd like to see an option for different output file extensions, so I don't have to split by folder. Similarly, we could also have multiple outputs varying on the target - it would be really nice to be able to ship different output for older browsers vs. ES6+ for newer browsers.

I'm unsure how the output would be handled though? Multiple outDirs? Maybe a new path-mapping like config like:

{
   "compilerOptions": {
        "emit": [
            {"outDir": "./amd", "module": "amd"},
            {"outDir": "./cjs", "module": "cjs"},
        ]
    }
}

where each element of emit is some subset of our emit-related compiler options could work?

This is exactly what we're looking for. We ship cjs + esm (both targeting es5) side by side using multiple outDirs and would love the improved execution time + simplicity of only having to run tsc once.
We're also looking to add a third output that targets esnext to reduce bundle size bloat for browsers that support the latest features.

@weswigham is this still under consideration?

also interested in this. That said as opposed to having different output directories based on module, I'd like to see an option for different output file extensions, so I don't have to split by folder.

Yes! With multiple folders you may have duplicate declaration files (not ideal). For any npm package, I should be able to have a configuration that outputs both CJS and ESM, but only one set of declarations like this:

{
  "compilerOptions": {
    "outDir": "./dist",
    "emit": [
      { "module": "CommonJS", "declaration": true },
      { "module": "ESNext", "extension": "mjs" },
    ]
  }
}

The output should be like this:

.
โ”œโ”€โ”€ dist
โ”‚   โ”œโ”€โ”€ index.d.ts
โ”‚   โ”œโ”€โ”€ index.js
โ”‚   โ””โ”€โ”€ index.mjs
โ””โ”€โ”€ src
    โ””โ”€โ”€ index.ts

And the package.json would look like this:

{
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts"
}

Perhaps even a preset that does the same thing would be in order?

{
  "compilerOptions": {
    "outDir": "./dist",
    "emit": ["npm"]
  }
}
Was this page helpful?
0 / 5 - 0 ratings