Skip to content
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

Merged
merged 16 commits into from Nov 8, 2022
Merged

Add Typescript support #42

merged 16 commits into from Nov 8, 2022

Conversation

simonihmig
Copy link
Collaborator

@simonihmig simonihmig commented Jul 6, 2022

Introduces the --typescript flag, closing #10.

Some notes:

To Do

@simonihmig simonihmig added the enhancement New feature or request label Jul 6, 2022
<% if (typescript) { %> // compile TypeScript to latest JavaScript, including Babel transpilation
typescript({
transpiler: 'babel',
browserslist: false,
Copy link
Collaborator

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

Copy link
Collaborator Author

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! 😉

Copy link
Collaborator Author

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,
Copy link
Collaborator

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';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

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

Copy link
Collaborator

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

Copy link
Contributor

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.

Copy link
Collaborator

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"

index.js Outdated Show resolved Hide resolved
"paths": {
// No paths, no absolute imports, only node_modules imports allowed
// But fallback for type-overrides and such
"*": ["types/*"]
Copy link
Collaborator

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

Copy link
Collaborator Author

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 😏

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I'll investigate

Copy link
Contributor

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.

Copy link
Collaborator

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

Copy link
Contributor

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?

Copy link
Collaborator

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. '*': ['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"

  2. include: ['types'] says "consider everything under types/ implicitly in scope as part of this project, whether it's imported or not" (roughly equivalent to specifying entrypoints for a bundler)

  3. A declare module block anywhere covered by a project's include 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.

Copy link
Collaborator

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",
Copy link
Collaborator

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

Copy link
Collaborator Author

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? 🤷‍♂️

Copy link
Contributor

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 😜

@simonihmig simonihmig force-pushed the typescript branch 2 times, most recently from 46a3379 to 0b7650e Compare July 21, 2022 10:47
@@ -1,5 +1,6 @@
{
"plugins": [
<% if (typescript) { %> "presets": [["@babel/preset-typescript"]],
<% } %> "plugins": [
"@embroider/addon-dev/template-colocation-plugin",
Copy link
Collaborator

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

Copy link
Contributor

@dfreeman dfreeman Oct 17, 2022

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 of preset-typescript as Simon currently has?
  • Why resolve()?
  • Why default users to onlyRemoveTypeImports and optimizeConstEnums? 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)

Copy link
Collaborator

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)

Copy link
Contributor

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)

Copy link
Collaborator

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 💯

@@ -2,7 +2,7 @@

module.exports = {
root: true,
parser: '@babel/eslint-parser',
parser: '<%= typescript ? '@typescript-eslint/parser' : '@babel/eslint-parser' %>',
parserOptions: {
Copy link

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?

Copy link
Collaborator

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

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

@dfreeman dfreeman left a 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",
Copy link
Contributor

@dfreeman dfreeman Oct 17, 2022

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 of preset-typescript as Simon currently has?
  • Why resolve()?
  • Why default users to onlyRemoveTypeImports and optimizeConstEnums? 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)

Comment on lines 38 to 39
"@glint/core": "^0.9.1",
"@glint/environment-ember-loose": "^0.9.1",
Copy link
Contributor

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.

Copy link
Collaborator Author

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

Copy link
Contributor

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/**/*"
Copy link
Contributor

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.

/cc @chriskrycho @jamescdavis

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I share the regret. Deeply. 😩

  2. 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 or internal-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.

Copy link
Collaborator Author

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

Copy link
Contributor

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 🙂

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.

Copy link
Collaborator Author

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",
Copy link
Contributor

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 😜

files/__addonLocation__/tsconfig.json Outdated Show resolved Hide resolved
@@ -0,0 +1,10 @@
import '@glint/environment-ember-loose';
Copy link
Contributor

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.

tests/cli.test.ts Outdated Show resolved Hide resolved
tests/cli.test.ts Outdated Show resolved Hide resolved
files/__addonLocation__/package.json Outdated Show resolved Hide resolved
files/__addonLocation__/package.json Outdated Show resolved Hide resolved
@simonihmig
Copy link
Collaborator Author

@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 types/ is up for debate. I think I would prefer internal-types/ myself...

@simonihmig simonihmig marked this pull request as ready for review October 21, 2022 15:54
@simonihmig
Copy link
Collaborator Author

I have now incorporated the latest outcome of typed-ember/glint#439 here!

@simonihmig simonihmig merged commit 69d6b35 into main Nov 8, 2022
@simonihmig simonihmig deleted the typescript branch November 8, 2022 15:27
@ef4
Copy link
Contributor

ef4 commented Nov 8, 2022

Thanks so much @simonihmig, this is great to have.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants