Closed NullVoxPopuli closed 2 years ago
Did some more digging, the tslib's decorator implementation looks very similar to the legacy babel implementation:
// output from tslib
function __decorate(decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc,
d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if (d = decorators[i])
r = (
c < 3
? d(r)
: c > 3
? d(target, key, r)
: d(target, key)
) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
export { __decorate as _ };
import { _ as __decorate } from '../../tslib.es6-ddb164bc.js';
import { tracked } from '@glimmer/tracking';
class Demo extends Component {
// ...
}
__decorate([tracked], Demo.prototype, "active", void 0);
And interop with the assumed-to-be-compiled-by-babel decorator (tracked) and this code from my test library seems to be ok?
but, I suppose my question still remains though -- since babel can handle this, how do we let babel handle this?
Hi there.
You're right that decorators are also being standardized as part of TC39 and finally seem to be coming along nicely. But as of today, TypeScript decorators are still treated by TypeScript as part of the TypeScript superset (like enums), and is still something you opt in to use with the experimentalDecorators
tsconfig option. The decorators proposal has changed a lot throughout the years. At some point, the decorators proposal diverged quite a bit from how TypeScript decorators work (and the syntax from that proposal was implemented as part of @babel/plugin-proposal-decorators
, but you could set the legacy: true
option, like you do, to have it transpile according to the old proposal, which was a lot like TypeScript decorators, which is also why the implementation in tslib is practically identical to the one in the legacy implementation of the babel plugin).
That is why decorators are removed from TypeScript even when you target ESNext. There is in fact no way to preserve them after the TypeScript compiler has been doing its thing such that other tools, like Babel, can enter in a later step and transform the decorator syntax. To better understand this, imagine running tsc
first, and then Babel in a secondary step. You wouldn't be able to preserve the decorators for Babel to later parse and transpile, so long as you used the TypeScript compiler and its APIs first. rollup-plugin-ts
does something similar to this, by building up an incremental TypeScript BuilderProgram and then emitting with target ESNext before delegating the results to Babel for the actual syntax lowering.
However, this will all probably change now that decorators have entered stage 3 in the standards process. When it eventually arrives in stage 4 and becomes part of the next major version of EcmaScript, potentially ES2023, then TypeScript decorators will align with the standardized feature and be preserved when you target ESNext or ES2023 as the target of a tsconfig, and by then tools like rollup-plugin-ts
will preserve them too such that babel will be responsible of transpiling them down to whatever browsers/runtimes you're targeting.
I hope this explanation makes sense. If not, feel free to comment again, and I'll gladly elaborate.
@wessberg that explanation does indeed make sense and is in fact eminently reasonable. However, there are a number of cases where you need to use TS for type checking while using Babel (or other tools) for emit for decorators. A key example here—and the motivating one for @NullVoxPopuli's comment—is the entire Ember ecosystem, which standardized on using the Babel legacy decorator transform and indeed using Babel for all emit (using TS only for type checking) back in 2018 precisely because of the interop hazards that exist when trying to use both Babel and TS's decorators. The bugs are many and subtle!
Given that (a) TypeScript has yet to implement support for the Stage 3 decorators proposal, and the earliest we'd expect to see it is in TS 4.8, coming in mid-August; (b) there is not yet any clear migration path for existing users of the legacy transforms for either Babel or TypeScript; and (c) there are many users who rely on the ability to do all emit via Babel etc., rather than using tsc
for any part of the emit pipeline—would you be open to adding a flag to entirely opt out of tsc
-based emit? That would be enormously helpful!
Hi there @chriskrycho.
I guess it would make sense to support delegating the full responsibility of syntax transformation to whatever transpiler has been chosen, such as babel. It could lead to some performance gains, albeit minor, since a TypeScript builder program would still be constructed based on the source file(s), since it is needed not just for diagnostics, but especially so for declaration files (which too need a proper type checker for the relationships between TS symbols to be correct in the declaration bundling phase).
In fact, I can see some value in not even exposing this as a config option, but enabling this behavior by default if @babel/preset-typescript
can be found inside the combined Babel config, as that signals an intent from the library consumer to delegate the full responsibility to Babel. This is, of course, "magic", which has downsides as well, such as less transparency. On the other hand, this library has always been about making things "just work" with minimal to absolutely no configuration, and I'm trying to stick to that philosophy. Of course, for some users, doing the first emit with the TypeScript Compiler APIs will have some advantages, but in these cases these users can just leave out @babel/preset-typescript
from their babel configs.
What do you think about a solution such as this one?
I think the default should be to use only the specified transpiler for emitting code if a non-tsc
option is supplied there, using tsc
only for emitting declarations in that case, but with the ability to opt into using tsc
for its own specific features (const enum
, decorators, any others I’m not thinking of). That would be a breaking change for existing usage so you might need
I would very much prefer “good defaults, no magic, configurable if the defaults don’t fit with your needs.”
If we do it like that, then we will have to add @babel/preset-typescript
as an (optional) peer dependency, since otherwise just selecting transpiler: "babel"
in the plugin config would lead to crashes, as the syntax wouldn't be supported.
I'm very much in agreement with you generally in terms of magic. For rollup-plugin-ts
, there is quite a bit of magic in place here, which historically has been a key reason why this plugin has tended to "just work", where it has been a battle to configure everything correctly with other solutions. For example, if @babel/plugin-transform-runtime
is found in the combined babel config, some options are forced on it such as to use ES modules and to import helpers instead of inline them to ensure that it plays nice with Rollup that will ultimately inline them (unless marked as external in the Rollup config), which builds up an internal module dependency graph of ES modules, as well as to let Rollup potentially code split the usage of helpers and avoid redundancy.
In terms of the "good, but overridable defaults" principle, in practice here what tends to happen is that people bring in their babel configs from other projects, and these very often come with configuration options that simply won't produce great Rollup bundles. And instead of writing a ton of documentation of best practices and which options to disable when combining with Rollup, it has been a good solution to forcefully ignore some of those options that simply don't play work well with this integration.
There are other examples of magic too, such as splitting babel configs into two if babel-minify plugins are identified, to ensure that the minification related plugins only run once per chunk, whereas others run once per file. All this to say that where I tend to do the opposite with other libraries, with rollup-plugin-ts
I've always been hesitant to add config options if I can come up with a solution that "just works", even if it means breaking transparency at times. I believe that's what keeps this library's promise of just working with absolutely no config options given intact, even if there's some Babel, tsconfig, or swc config files in the root of the directory that may come with options that could otherwise be problematic.
Okay, sorry for the wall of text there. But that's the context. I'm still fairly sure I would prefer a default behavior of only doing the full emit with Babel if I can find @babel/preset-typescript
in the babel config, as I feel like that principle has worked well for this plugin in the past, but I'm also open for letting users explicitly decide this behavior with a config option.
In that case, I'm thinking of extending the transpiler option like this:
// It can be a string
{
transpiler: "babel"
}
// But it can also be a record
{
transpiler: {
typescriptSyntax: "typescript",
otherSyntax: "babel"
}
}
With that option, we're back to fully allowing customizing the transpilation behavior, while still retaining good defaults and respecting the input babel/swc configs as much as possible while still ignoring the ones that could break Rollup or produce inefficient bundles.
How does that sound?
This has been implemented as part of v3.0.0.
What I ended up implementing was a solution in which the chosen transpiler is used for the entire syntax transformation. I also ended up marking @babel/preset-typescript
as an optional peer dependency for when babel
is selected as a transpiler.
This behavior is fully customizable, since the transpiler option now optionally takes an options record, as described above. This means that transpilers can be mixed, e.g.:
transpiler: {
typescriptSyntax: "typescript",
otherSyntax: "babel"
}
When mixed like so, @babel-preset-typescript
is not required, as it isn't relevant to that configuration. However, when used simply like transpiler: "babel"
, it is.
Outstanding. Thank you!
I tested the new release, with regard to the problem discussed here, and it worked perfectly! Thanks so much @wessberg 🙏
Anyone see anything obvious I did wrong here? https://github.com/NullVoxPopuli/ember-popperjs/pull/166/files
I still get:
ember-popperjs/ember-popperjs on simpler-build took 11s
❯ pnpm build:js
> ember-popperjs@3.0.0 build:js /home/psego/Development/NullVoxPopuli/ember-popperjs/ember-popperjs
> rollup -c ./rollup.config.js
→ dist...
(!) Unresolved dependencies
https://rollupjs.org/guide/en/#warning-treating-module-as-external-dependency
@babel/runtime/helpers/esm/applyDecoratedDescriptor (imported by src/components/popper-j-s.ts)
@babel/runtime/helpers/esm/initializerWarningHelper (imported by src/components/popper-j-s.ts)
[!] (plugin Typescript) TS2354: This syntax requires an imported helper but module 'tslib' cannot be found.
src/components/popper-j-s.ts (45:3)
45 @tracked isShown = false;
~~~~~~~~
ELIFECYCLE Command failed with exit code 1.
so tslib
is required for parsing (cause Acorn doesn't support decorators). I confirm that even with tslib
in my devDependencies, my decorators are using the babel decorator polyfill. Most excellent :100:
That's correct, tslib
isn't actually being used in the actual transformation phase in your case, but still the TypeScript compiler options declare importHelpers: true
, which triggers this error.
However, I've come to realize there's actually no need to enforce importHelpers: true
for the case where babel/swc is used for the entirety of the actual transformation, so from the next release of rollup-plugin-ts
, this error will go away for the case where you're using babel/swc for everything.
As for the warning about @babel/runtime/helpers/esm/applyDecoratedDescriptor
and @babel/runtime/helpers/esm/initializerWarningHelper
being treated as external dependencies, that typically either means you've forgotten to install @babel/runtime
, which is a peer dependency, or that you marked it as external in your rollup config.
Question
This part of the documentation says that typescript is used for TS-specific features..
but decorators are not TS-specific.
TS decorators are different from the
@babel/plugin-proposal-decorators
output -- how can I use this babel plugin for decorators instead of TS (forcing atslib
dependency, which I don't want :sweat_smile: )My babel config:
my ts setup: