-
Notifications
You must be signed in to change notification settings - Fork 28
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
Add Typescript support #42
Conversation
<% if (typescript) { %> // compile TypeScript to latest JavaScript, including Babel transpilation | ||
typescript({ | ||
transpiler: 'babel', | ||
browserslist: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
browserslist false adds extra transpilation -- I was unable to determine why, but "last 1 firefox versions, last 2 chrome versions" has babel doing the least work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I remember this discussion in your other PR. As I said in the PR description, I just added what worked for me, for now. When your PR is merged, I will update all of this! So let's not duplicate the same discussion here! 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the record: as I already stated in the related embroider-build/embroider#1099, I think the config here is right (browserslist: false
) as documented by rollup-plugin-ts
, it's just that the latter seems to not behave correctly here. See the issue I filed here: wessberg/rollup-plugin-ts#189
Given that this does not break anything, it just produces code that is not as optimized as it could be, I would think to keep this as is now, and not expose workarounds to created addons that later need to get rolled back causing churn for maintainers, and rather rely on getting this fixed upstream eventually!
typescript({ | ||
transpiler: 'babel', | ||
browserslist: false, | ||
transpileOnly: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should add declarationMap here as well -- I use this config: https://github.com/NullVoxPopuli/ember-resources/blob/main/ember-resources/config/rollup.config.js#L52
@@ -0,0 +1,10 @@ | |||
import '@glint/environment-ember-loose'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this have been committed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is intentional. At least that's what worked well for me.
So this file is for making Glint aware of what your own addon consumes. Say this addon depends on your ember-popperjs
. With classic templates, invoking <PopperJS>
would make Glint fail, as it doesn't know what PopperJS
is. So you would import the component here, and add its type to the registry.
The other file src/glint.ts
is about making Glint aware of what your addon exposes. So the app would import this file (which is published on npm!), and have all the components etc. added to the app's registry.
This is what we came up with here: typed-ember/glint#303. And which is documented here: https://typed-ember.gitbook.io/glint/using-glint/ember/authoring-addons#publishing-addons-with-glint-types
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we link to that discussion in this glint.d.ts
file?
it feels "non-standard" to me, but idk if that's because Glint is new.
even so, it's a hidden convention (no tooling/linting, to enforce, etc) 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this relates to the question above about how we want to handle addon-only unpublished type declarations and making it as clear as possible to authors/readers the role they play.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm now 100% in camp "TS addons should have a types directory for local unpublished types"
"paths": { | ||
// No paths, no absolute imports, only node_modules imports allowed | ||
// But fallback for type-overrides and such | ||
"*": ["types/*"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to allow this for the addon?
consuming projects wouldn't know about these types and could error as a result
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what you mean here?
I took it from your comment here actually 😏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm I'll investigate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think v2 addons should aim to avoid needing paths
config at all. There's always declare module
if absolutely necessary, but in general I don't think we want to make it easier for addon authors to write local type declarations that will break things when their consumers don't have them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it turns out this is needed if a v2 addon wants the cached-decorator types.
the cached-decorator types go in the types directory, but we don''t want to provide those types, because they could conflict with the consumer's types for @cached
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's special about @cached
that makes it require a paths
entry?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't *: [types/*]
how you tell ts to check every file in that folder for type defs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
'*': ['types/*']
says "when any import fails, see if the mapping here produces the path to a module you can load instead, and if so, treat that as implicitly being the contents of the thing you were trying to look up" -
include: ['types']
says "consider everything undertypes/
implicitly in scope as part of this project, whether it's imported or not" (roughly equivalent to specifying entrypoints for a bundler) -
A
declare module
block anywhere covered by a project'sinclude
will be taken into account, even without path mapping shenanigans.
Given (2) and (3), anything you're implicitly relying on paths
for can be wrapped in declare module
instead. You can still align your declarations on disk 1:1 with the module layout if you want, but declare module
also gives you the flexibility to e.g. group together declarations for a package in a single .d.ts
file if you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
excellent! thanks for explaining those things!
"types/**/*" | ||
], | ||
"glint": { | ||
"environment": "ember-loose", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why loose?
porque no template-imports?
tho 🤔 maybe that should be done as a part of #41
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I thought to go with the more "classic" setup first.
Otherwise we would have to add additional dependencies etc., so wasn't sure if that's not too experimental yet as a default? 🤷♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I know it isn't even possible to use FCCTs in v2 addons yet, so including the template-imports
environment here would be a pretty mean trick to play on authors 😜
46a3379
to
0b7650e
Compare
@@ -1,5 +1,6 @@ | |||
{ | |||
"plugins": [ | |||
<% if (typescript) { %> "presets": [["@babel/preset-typescript"]], | |||
<% } %> "plugins": [ | |||
"@embroider/addon-dev/template-colocation-plugin", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
before I forget, I think we'll want to change the default settings:
[
resolve('@babel/plugin-transform-typescript'),
{
allowDeclareFields: true,
onlyRemoveTypeImports: true,
// Default enums are IIFEs
optimizeConstEnums: true,
},
],
This is what I use over here: https://github.com/NullVoxPopuli/ember-popperjs/blob/main/ember-popperjs/babel.config.cjs#L8
and cc @chriskrycho
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NullVoxPopuli A few questions:
- Why
plugin-transform-typescript
instead ofpreset-typescript
as Simon currently has? - Why
resolve()
? - Why default users to
onlyRemoveTypeImports
andoptimizeConstEnums
? I think both of those choices can be reasonable ones for an author to intentionally make, but I'm wary of diverging from default behavior in the blueprint unless there's a super compelling reason to do so.
Re: the third bullet, allowDeclareFields
is a perfect example of something I do think is worth overriding for users by default in the blueprint: idiomatic Ember-in-TypeScript uses declare
for decorated framework-managed fields, so folks can run into trouble pretty quickly if it's not set. (Also worth noting that Babel only defaults that one to false
for backcompat reasons and is planning to default it to true
in v8, if/when that day comes)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why plugin-transform-typescript instead of preset-typescript as Simon currently has?
I think we need both -- preset-typescript for all TS things (and a general indicator to users, that we're using "TS stuff" for compilation -- and then the transform plugin to override declare fields.
Why resolve()?
catches typos with better errors at build time -- this is a personal preference, and it's a non issue if you just don't misspell things -- though maybe the error reporting has gotten better.
Why default users to onlyRemoveTypeImports and optimizeConstEnums?
for optimizeConstEnums, I don't understand why anyone would want the previous behavior of an IIFE per-enum.
For onlyRemoveTypeImports
, yeah, I think this is an artifact of a darker age in my babel config's history -- we don't need this one (and it implies confusing behavior, considering what the rollup stuff is doing)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need both
The preset sets up the transform for you, and accepts the same configuration
catches typos with better errors at build time
We aren't using resolve
anywhere else in that file (and in fact we can't—it's .json
). That feels like an unrelated change to this PR if you want to propose it.
for optimizeConstEnums, I don't understand why anyone would want the previous behavior of an IIFE per-enum
Sure, but const enum
is somewhat of a power user feature with a lot of documented caveats and downsides (not to mention the "just use unions" crowd), so my argument here is that diverging from default behavior and introducing extra boilerplate config for maintainers isn't worth the tradeoff.
Someone who knows they have a highly performance-sensitive situation and that it means they want to use const enum
is also likely to be able to determine that they want to change Babel's transpilation behavior as well. (Also, if you're in such a situation, actual consts
+ a union type will be more performant than optimizeConstEnums
's output, since it skips an object field traversal)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The preset sets up the transform for you, and accepts the same configuration
Oh, nice! that would make the config even less verbose!
That feels like an unrelated change to this PR if you want to propose it.
yeah, I was copy pasting from one of my own projects -- I had hoped that we could implicitly convert between all the babel config formats in the split of the comment, rather than the details of the comment 😅
so my argument here is that diverging from default behavior and introducing extra boilerplate config for maintainers isn't worth the tradeoff.
fair enough 💯
0b7650e
to
0765810
Compare
@@ -2,7 +2,7 @@ | |||
|
|||
module.exports = { | |||
root: true, | |||
parser: '@babel/eslint-parser', | |||
parser: '<%= typescript ? '@typescript-eslint/parser' : '@babel/eslint-parser' %>', | |||
parserOptions: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this ember-cli issue is also relevant here: ember-cli/ember-cli#10046
Since both parsers have different options I think the parserOptions
should also be applied conditionally?
I was looking into fixing that in ember-cli but I'm not sure which options are needed exactly. I found this: https://typescript-eslint.io/docs/linting/typed-linting which sounds nice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed here. Eslint configs need to make heavy use of overrides, because we have a mix of node (cjs), node (esm), and browser (esm, js, ts).
Overrides are way easier to extend, change, target specific files, folders, etc.
It's also how my eslint-configs package can simultaneously support app, addon, v2 addon, js, ts, all from the same config
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will wait for ember-cli/ember-cli#10054 to get merged before applying the same fix here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just applied the changes as in ember-cli/ember-cli#10054. Think that should be good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for all your hard work to drive this to completion @simonihmig!
I've left a few comments inline—happy to discuss out-of-band as well on Discord or a call at some point if sync communication is easier for any of it 🙂
@@ -1,5 +1,6 @@ | |||
{ | |||
"plugins": [ | |||
<% if (typescript) { %> "presets": [["@babel/preset-typescript"]], | |||
<% } %> "plugins": [ | |||
"@embroider/addon-dev/template-colocation-plugin", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NullVoxPopuli A few questions:
- Why
plugin-transform-typescript
instead ofpreset-typescript
as Simon currently has? - Why
resolve()
? - Why default users to
onlyRemoveTypeImports
andoptimizeConstEnums
? I think both of those choices can be reasonable ones for an author to intentionally make, but I'm wary of diverging from default behavior in the blueprint unless there's a super compelling reason to do so.
Re: the third bullet, allowDeclareFields
is a perfect example of something I do think is worth overriding for users by default in the blueprint: idiomatic Ember-in-TypeScript uses declare
for decorated framework-managed fields, so folks can run into trouble pretty quickly if it's not set. (Also worth noting that Babel only defaults that one to false
for backcompat reasons and is planning to default it to true
in v8, if/when that day comes)
files/__addonLocation__/package.json
Outdated
"@glint/core": "^0.9.1", | ||
"@glint/environment-ember-loose": "^0.9.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chriskrycho @jamescdavis What do you think; are we ready for primetime?
I could see an argument for waiting for Glint 1.0 (which we're getting close to, but is still blocked on one particularly large internal refactor that I'll need to be able to set aside a nontrivial block of time for).
On the other hand, the reality is that folks are already releasing Glint-enabled addons today, so you could argue we might as well make that as easy as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I don't really think this makes it into "prime time". 😅 At least until this blueprint replaces the official v1 addon blueprint eventually. So as this blueprint is still more on the experimental side of things, I wouldn't mind this to use other more experimental things (pre-stable Glint). 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha that's fair—it's certainly true that anyone using the v2 blueprint today knows they're opting into something that isn't guaranteed to be 100% baked yet.
This will just be the first time (that I know of!) that anyone will find themselves set up with Glint without having gone out of their way to read the docs and decide it's something they want. This blueprint feels to me like a reasonable place to take that step, but I did want to call it out and make sure Chris and James are on the same page
}, | ||
"include": [ | ||
"src/**/*", | ||
"types/**/*" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a v1 addon, the root types
directory is a place we can put non-published type declarations because it actually belongs to the dummy app—it's the exact same types
directory that we generate for a normal app, except that we messed up and it still ended up in the project root for addons instead of under tests/dummy
. That oversight is one of the sins I regret most from our early work to support addons in ember-cli-typescript
, because it creates a blurry picture of the role the types
directory plays in an addon repository.
As it stands, this change is (re)introducing types/
as a first-class unpublished directory belonging only to the addon. For the near term, I think we do need some kind of "internal-only type declarations" location for addons—the local Glint registry is a good example of that, though it eventually goes away with strict mode—but my hope would be that the need for such declarations otherwise would be pretty minimal. The more of them you're relying on them, the easier it is to accidentally end up publishing code that won't typecheck for your consumers because they don't have those declarations themselves.
What do you think of something like local-types/
or internal-types/
, or even just internal-types.d.ts
by itself, to help communicate clearly that these types are not part of the addon itself that you're publishing?
Another alternative might be to home types/
in the test app rather than the addon, but still include it under includes
so that it's accounted for when typechecking the addon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
I share the regret. Deeply. 😩
-
I think we actually have a continuing problem here in that it's actually just really hard to clarify that these types only belong to the test infrastructure for the addon and will not and should not be published, so I think putting them in the test app is the right move—even something like
local-types
orinternal-types
risks some confusion: "local" or "internal" might still imply to someone that they're not public but should be published (if that makes sense).
I also wonder (not a blocker for this PR, as we can definitely iterate) whether we might explicitly have something like:
/
app/
...
addon/
...
tsconfig/
compiler-settings.json
test.json
publish.json
tsconfig.json
where tsconfig.json
has "extends": "./tsconfig/compiler-settings.json"
and the "includes"
you mention, and then tsconfig/publish.json
is configured to do the right thing along similar lines. With smart use of comments (thanks, jsonc!
) in the config files, that combination of naming could help clarify the relationships involved?
It's unfortunate that having those be separate is, if not quite needful, then at least potentially helpful, but it's the pattern I've increasingly moved to for handling things like this in my own libraries and I suspect it might be a helpful secondary move here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see some caveats with regards to moving the internal types to the test app:
- the blueprint creates a single
test-app
, but in some cases people might actually want to have more than one test app, when they need to be different for covering different test cases, e.g. apps with different dependencies (e.g. with and without fastboot). In that case it might become ambiguous and thus confusing where to put these types? (both test-apps might need them) - things could also go into the other direction, that you don't need a test-app at all. Like when you are able to write plain unit tests for your addon that don't depend on an actual runnable Ember app, you could put those (jest, vitest, whatever) tests right into the addon package, but still need to add some ambient types
Does it maybe make sense to put these type declarations into neither the addon nor the test-app, but basically at /types
at the root level of the monorepo, outside of any actual package? The downside is that when you need to import some types within your declarations, you would then need to add those to the monorepo's root devDependencies 😕. Or we make that folder a private types-only package that we import from? Just thinking aloud here...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I spun up a new addon blueprinted from this branch and played around a bit with possible setups.
After playing with it, I think moving the types directory outside of the addon is a tall order, because for anything registry-based (namely loose-mode Glint, but this would also apply to services or Data), you can wind up running into the same issues that prompted typed-ember/glint#439
The registry pattern almost requires that entries be added somewhere within the package/project that consumes them. Someone who's deeply familiar with both their package manager and TS's resolution scheme may be able to massage their config to get it to work (or if they're on Yarn 1, things may be incidentally hoisted in their favor), but I'd be hesitant to count on that.
Given that, I'm now leaning toward keeping local types within the addon directory for the time being. Maybe we'll figure out an alternative strategy (or maybe one of y'all can come up with one now!), but I wouldn't want to block this indefinitely on that. As far as naming confusion, we could try unpublished-development-types
or something equally explicit (à la React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
).
I also still kind of like the idea of a single .d.ts
file rather than a directory, both to discourage too much growth and to give an easy place to leave a // These types are not part of your published addon, be very careful, blah blah etc
explainer comment. I'm not attached enough to that to fight for it, though 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the record, this is now implemented by having a unpublished-development-types/index.d.ts
, that has a comment like suggested and the boilerplate for the glint registry (for the addon's own classic templates)
"types/**/*" | ||
], | ||
"glint": { | ||
"environment": "ember-loose", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I know it isn't even possible to use FCCTs in v2 addons yet, so including the template-imports
environment here would be a pretty mean trick to play on authors 😜
@@ -0,0 +1,10 @@ | |||
import '@glint/environment-ember-loose'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this relates to the question above about how we want to handle addon-only unpublished type declarations and making it as clear as possible to authors/readers the role they play.
@dfreeman thanks for your review! I addressed the directly actionable points (marked the discussion threads as resolved). I think at this point only the role of |
325f894
to
f7b3c9c
Compare
d7ea1b6
to
eb2861b
Compare
I have now incorporated the latest outcome of typed-ember/glint#439 here! |
Thanks so much @simonihmig, this is great to have. |
Introduces the
--typescript
flag, closing #10.Some notes:
tsconfig.json
is using the setup thatember-cli-typescript
is also generating recently, delegating to@tsconfig/ember
.--glint
flag? Related: https://typed-ember.gitbook.io/glint/using-glint/ember/authoring-addons and Support shipping Glint types with v2 addons typed-ember/glint#338--typescript
flag to the official app blueprint (which in the latest canary version does support that as well). If the used Ember CLI version is not one that support it, then a warning message is shown, and the user has to manually setup TS in the test app.To Do