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

Convert CLI to 100% typescript #702

Merged
merged 6 commits into from
Oct 24, 2016
Merged

Convert CLI to 100% typescript #702

merged 6 commits into from
Oct 24, 2016

Conversation

samccone
Copy link
Contributor

@samccone samccone commented Sep 23, 2016

But...

Generally (typescript) discourages contributions and makes things more difficult than they need to be

As to this we are already using a mix of closure annotations which have been a constant pain point even for the googlers working on this project.. I am trying to make having the wins we get from closure compiler as far as type checking much easier.


Why

The most value we are going to get from using TS is not going to be in the CLI but rather in the core of lighthouse with the large objects that we are passing around. If we land this I can work on slowing improving the type safety in that area so that the wins become even more apparent.

the end goal here is to make developing in lighthouse easier and not requiring someone to know the shape of the objects off of the top of their head that we pass around (which are quite complex).


Wins

Examples where this would have caught broken code / wrong code paths.


Process changes

This CL will require a slight change in our release process to make sure someone compiles the CLI before npm publish.

@brendankenny brendankenny changed the title Initial typescript work for CLI. [Proposal] Initial typescript work for CLI. Sep 24, 2016
@samccone
Copy link
Contributor Author

samccone commented Sep 24, 2016

image

just an example

@samccone samccone force-pushed the sjs/ts-cli branch 6 times, most recently from 7d91ae4 to c458f03 Compare September 24, 2016 20:56
@samccone samccone changed the title [Proposal] Initial typescript work for CLI. [Proposal] Convert CLI to 100% typescript Sep 24, 2016
@@ -0,0 +1,8 @@
One time setup:

`npm i -g [email protected]`
Copy link
Member

Choose a reason for hiding this comment

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

looks like typings also must be installed. any preference on version?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

so we can actually now just use npm packages for all of the typings.. I might migrate us to that

@samccone
Copy link
Contributor Author

@GoogleChrome/lighthouse thoughts?

@ebidel
Copy link
Contributor

ebidel commented Sep 28, 2016

Unless there's strong motivation for maintainability, I'm not a huge fan of moving projects off of JS and requiring transpilation (looking at you .ts and .coffee). Generally, it discourages contributions and makes things more difficult than they need to be. It might be nice if you're using vscode, but ES6 features get you most of what TS offers.

@samccone
Copy link
Contributor Author

samccone commented Sep 28, 2016

Unless there's strong motivation for maintainability

This is the entire reason for this.

ES6 features get you most of what TS offers.

Not compile time checks.

makes things more difficult than they need to be.

Found this to not be true, if anything it is much easier


Generally, it discourages contributions and makes things more difficult than they need to be

As to this we are already using a mix of closure annotations which have been a constant pain point even for the googlers working on this project.. I am trying to make having the wins we get from closure compiler as far as type checking much easier.

@samccone samccone closed this Sep 28, 2016
@paulirish paulirish deleted the sjs/ts-cli branch September 30, 2016 17:29
@samccone samccone restored the sjs/ts-cli branch October 1, 2016 16:22
@samccone samccone reopened this Oct 1, 2016
@googlebot
Copy link

We found a Contributor License Agreement for you (the sender of this pull request) and all commit authors, but as best as we can tell these commits were authored by someone else. If that's the case, please add them to this pull request and have them confirm that they're okay with these commits being contributed to Google. If we're mistaken and you did author these commits, just reply here to confirm.

@samccone
Copy link
Contributor Author

samccone commented Oct 1, 2016

@googlebot it is ok

@addyosmani
Copy link
Member

addyosmani commented Oct 3, 2016

I think the shift over to using TS here is really tight. Having a proper type system, advanced autocompletion and better tooling around it are all awesome. Great work, @samccone 🌟

Dumb thought: it may be worth considering whether this change might increase the barrier to contribution for folks submitting PRs to LH. Similar to the days of projects being authored in CoffeeScript, it's one more thing they'll need to grok before they can contrib.

I'm personally more excited about the Flow static type checker these days which works without type annotations and is pretty good at inferring types, but this is prolly one of those calls for folks who work on the LH codebase the most often.

@wardpeet
Copy link
Collaborator

wardpeet commented Oct 4, 2016

I'm not a fan of typescript but it's getting adopted pretty quickly so I wouldn't mind using it here.

@samccone
Copy link
Contributor Author

samccone commented Oct 7, 2016

This is now inline with master along with the recent changes we have landed on the CLI. 👍

@paulirish
Copy link
Member

paulirish commented Oct 7, 2016 via email

@brendankenny
Copy link
Member

Happy to talk about this in the morning. I know I've been somewhat negative on this, though I'm trying to keep an open mind.

I think the problem I'm having is that the range of reactions have been from "it discourages contributions" to "I'm not a fan but I wouldn't mind this", so the motivation for this PR is somewhat indistinguishable to me from one LH team member being really into clojure or elm and pushing it hard. The benefits (and overhead) to maintainability seem basically indistinguishable from using the Closure compiler, which incidentally no one cared enough about to maintain, and it's not clear what's changed on that front.

I'm happy enough to try this out. I like type checking and I like language tooling that actually works and I like @samccone :) But I don't like non-functional code and I don't like --watching processes and I don't like compile steps. I also agree with Addy that the first thing I do when I find a CoffeeScript codebase is close the current tab, regardless of what help I was looking for from a project.

So it would ease my mind a lot to define what success looks like or some kind of time table here instead of just eventually defaulting into more or less "there aren't enough problems to back it out".

@@ -0,0 +1,9 @@
One time setup:

`npm i -g [email protected] typings`
Copy link
Member

Choose a reason for hiding this comment

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

any way we can do a local install?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes i will modify so it can all be local, just trying to make it as easy as possible

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This has been addressed

@samccone
Copy link
Contributor Author

samccone commented Oct 7, 2016

So it would ease my mind a lot to define what success looks like or some kind of time table here instead of just eventually defaulting into more or less "there aren't enough problems to back it out".

I defer to my previous answer about why

As to this we are already using a mix of closure annotations which have been a constant pain point even for the googlers working on this project.. I am trying to make having the wins we get from closure compiler as far as type checking much easier.

I am pretty confident around y'all liking this and it making development a lot easier,

in case this is not true and it is terrible and adds little to no value

We can just run this which will add closure style annotations and migrate us back to just vanilla JS.
https://github.com/angular/tsickle

The most value we are going to get from using TS is not going to be in the CLI but rather in the core of lighthouse with the large objects that we are passing around. If we land this I can work on slowing improving the type safety in that area so that the wins become even more apparent.

the end goal here is to make developing in lighthouse easier and not requiring someone to know the shape of the objects off of the top of their head that we pass around (which are quite complex).

As far as timelines let's give it 30 days, if you think it actually adds no value, and detracts from the project as far as contributors vs our current solution of closure compiler ect ect lets remove it

@mprobst
Copy link

mprobst commented Oct 13, 2016

Context: I'm working on TypeScript at Google on the Angular team. We've introduced TypeScript for Angular itself, into a substantial code base, and by now have helped several dozen Google projects onto TypeScript, using Angular 2, but also Angular 1, Polymer, and Vanilla JS.

Some data:

  • We regularly run "developer happiness" surveys with these folks. On a scale from 1-5, average self reported developer happiness is 4.3 (this is in particular for teams coming from Closure Compiler)
  • We measure compilation velocity (internal: go/tsdev-) across a range of large projects, and see transpilation turnaround in under a second even for large projects. Transpilation does not seem to be a hindrance in day to day work.
  • IDE support is a godsend, and works consistently just about everywhere (we track Sublime, Vim, IntelliJ, Eclipse, VS Code, Atom, Emacs)
  • People regularly find bugs through type annotations, even if the code base used Closure Compiler before. Particularly around generic types, TypeScript does a better job.

The main reason to move to TypeScript is the developer tooling though - correctness is nice, but you hopefully catch most of those issues in your test suite. In practice we find the biggest benefit to be go to symbol, auto-complete, refactoring, and the turnaround time to find that you mistyped that property before even running tests.

The main catch is the initial setup. However this is a one time cost, once you have the right npm steps in place, it's just npm watch (or whatever task runner you like, internally it's a bit different). Our experience is that these setups are very stable and reproducible, things don't break and work consistently for different developers.

Ping me directly if you'd like more pointers to data.

Re Flow, we did compare that approach with TypeScript and prefer TypeScript. Global type inference can have very surprising effects, in particular in large code bases (you change code in one file, compile errors abound across the code base). We also like the style of explicitly typing the interface to your code. Beyond that, developer tooling, ubiquity in editors, mind share etc seem to be much better with TS. YMMV.

@samccone samccone force-pushed the sjs/ts-cli branch 5 times, most recently from f6bc48e to ba523e8 Compare October 19, 2016 22:45
@samccone
Copy link
Contributor Author

A note.. this PR should ⚠️ not ⚠️ be squashed if merged. The commits are intended to be atomic.

Thanks

"module": "commonjs",
"target": "es6",
"noImplicitAny": false,
"sourceMap": true
Copy link
Member

Choose a reason for hiding this comment

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

can the map be datauri'd into the end of the .js file rather than a separate .map file?

"compilerOptions": {
"module": "commonjs",
"target": "es6",
"noImplicitAny": false,
Copy link
Member

Choose a reason for hiding this comment

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

what would it takes to flip this to true? is it worth it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ask.ts(30,22): error TS7006: Parameter '_answer' implicitly has an 'any' type.
bin.ts(111,10): error TS7006: Parameter 'argv' implicitly has an 'any' type.
bin.ts(137,5): error TS7005: Variable 'config' implicitly has an 'any' type.
bin.ts(157,3): error TS7018: Object literal's property 'fns' implicitly has an 'any[]' type.
bin.ts(158,12): error TS7006: Parameter 'fn' implicitly has an 'any' type.
bin.ts(162,37): error TS7006: Parameter 'c' implicitly has an 'any' type.
bin.ts(183,24): error TS7006: Parameter 'addresses' implicitly has an 'any' type.
bin.ts(191,11): error TS7006: Parameter 'results' implicitly has an 'any' type.
bin.ts(192,11): error TS7006: Parameter 'results' implicitly has an 'any' type.
bin.ts(211,27): error TS7006: Parameter 'err' implicitly has an 'any' type.
bin.ts(217,22): error TS7006: Parameter 'err' implicitly has an 'any' type.
chrome-finder.ts(35,9): error TS7005: Variable 'installations' implicitly has an 'any[]' type.
chrome-finder.ts(43,14): error TS7006: Parameter 'inst' implicitly has an 'any' type.
chrome-finder.ts(87,9): error TS7005: Variable 'installations' implicitly has an 'any[]' type.
chrome-finder.ts(108,15): error TS7006: Parameter 'installations' implicitly has an 'any' type.
chrome-finder.ts(108,30): error TS7006: Parameter 'priorities' implicitly has an 'any' type.
chrome-finder.ts(112,10): error TS7006: Parameter 'inst' implicitly has an 'any' type.
chrome-finder.ts(121,12): error TS7006: Parameter 'a' implicitly has an 'any' type.
chrome-finder.ts(121,15): error TS7006: Parameter 'b' implicitly has an 'any' type.
chrome-finder.ts(123,10): error TS7006: Parameter 'pair' implicitly has an 'any' type.
chrome-launcher.ts(102,31): error TS7017: Index signature of object type implicitly has an 'any' type.
chrome-launcher.ts(135,11): error TS7006: Parameter 'client' implicitly has an 'any' type.
chrome-launcher.ts(148,28): error TS7006: Parameter 'err' implicitly has an 'any' type.
chrome-launcher.ts(152,30): error TS7006: Parameter '_' implicitly has an 'any' type.
chrome-launcher.ts(222,19): error TS7006: Parameter 'val' implicitly has an 'any' type.
chrome-launcher.ts(222,24): error TS7006: Parameter 'def' implicitly has an 'any' type.
chrome-launcher.ts(226,16): error TS7006: Parameter 'time' implicitly has an 'any' type.
commands/list-audits.ts(21,48): error TS7006: Parameter 'i' implicitly has an 'any' type.
printer.ts(157,31): error TS7017: Index signature of object type implicitly has an 'any' type.
printer.ts(205,44): error TS7006: Parameter 'err' implicitly has an 'any' type.

I can do a follow up with all of these fixes

Copy link

Choose a reason for hiding this comment

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

We had an intern fix up Angular's code base for any using a semi-automated process based on tslint. Reach out to Igor Minar or Alex Eagle for details. Though that's probably for your relatively few issues.

"module": "commonjs",
"target": "es6",
"noImplicitAny": false,
"sourceMap": true,
Copy link
Member

Choose a reason for hiding this comment

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

the map files everywhere add some noise. can the map be datauri'd into the end of the js files instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

html: 'html'
enum OutputMode { pretty, json, html };
type Mode = 'pretty' | 'json' | 'html';
const Modes = ['pretty', 'json', 'html'];
Copy link
Member

Choose a reason for hiding this comment

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

The duplication of the enum, type alias and array seems avoidable. Is there anything we can do for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We allow users to pass one of a know Mode for output so we need a Mode type to limit that.
We also provide a known list of safe string values Modes

We then need to index into our Modes list. Instead of doing that with a raw int value we use an enum for more readable code.

Copy link

Choose a reason for hiding this comment

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

TypeScript enums can be used to map back to string values, as long as they aren't const enums:

enum E { A, B, C }
console.log(E[E.A]); // 'A'

That's not always advisable – you get more code because you retain the strings, and this can lead to issues when using Closure Compiler in advanced mode. YMMV.

}

return OUTPUT_MODE[mode];
return modeIndex;
Copy link
Member

Choose a reason for hiding this comment

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

this appears to pass off the index which just so happens to match our enum values, but that seems tenuous.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we have type checks on 68 for the input and output of this fn.. we also have an indexOf check to ensure it matches our enum values. so it is not really tenuous :)

Copy link

Choose a reason for hiding this comment

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

You can explicitly give values to the enum values, enum E { A = 42, B = 23, ... }.

lighthouseRun(urls).catch(handleError);
} else {
// because you can't cancel a promise yet
const SIGINT = Symbol('SIGINT');
Copy link
Member

Choose a reason for hiding this comment

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

why no more symbol?

Copy link
Contributor Author

@samccone samccone Oct 21, 2016

Choose a reason for hiding this comment

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

fixed, when I initially did this Symbol was not supported in TS (with out config) heh

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok so even though we are now compiling to es2015 this does not mean that node supports what we are trying to do.

Older versions of node do not support passing symbols to event listeners

image

image

going back to just a const value

}
};

function launchChromeAndRun(addresses: Array<string>) {
Copy link
Member

Choose a reason for hiding this comment

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

can you put up a PR (building on this one) that fixes #805?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes

.catch(err => {
console.error(err);
console.error(err.stack);
}).then(() => process.exit(_ERROR_EXIT_CODE));
Copy link
Member

Choose a reason for hiding this comment

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

why the reordering here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

since we were always exiting with 130, and only in the case of errors logging, i refactored the code to DRY it up

@@ -74,6 +75,7 @@ const cli = yargs
'perf': 'Use a performance-test-only configuration',
'skip-autolaunch': 'Skip autolaunch of chrome when accessing port 9222 fails',
'select-chrome': 'Choose chrome location to use when multiple installations are found',
'disable-network-throttling': 'Disable network throttling when running audits.',
Copy link
Member

Choose a reason for hiding this comment

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

wording nit: its not when running audits. i'd leave out the "when..."

@@ -480,7 +489,7 @@ class Driver {
*/
goOnline(options) {
return this.sendCommand('Network.enable').then(_ => {
if (options.flags.mobile) {
if (options.flags.mobile && !this.opts['disable-network-throttling']) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we want these options to be owned by the driver. They belong to the entire run, and we may want to change opts like throttling settings per pass.

Let's follow the pattern in #747 and pass the flags into beginEmulation and selectively choose which emulations to apply.

Bonus points to change mobile flag to disable-device-emulation while you're in here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

going to pull this commit an land it after

@samccone samccone force-pushed the sjs/ts-cli branch 4 times, most recently from 5609403 to 7b7888b Compare October 21, 2016 23:31
pretty: 'pretty',
json: 'json',
html: 'html'
enum OutputMode { pretty, json, html };
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@paulirish this removes the layer of indirection :) .. using the nice reverse lookup enum name trick @mprobst tipped me off to :)

// Get audit object from inside of results.audits under name subitem.
// Coming soon events are not located inside of results.audits.
subitem = results.audits[subitem] || subitem;
let subscore: AuditResult;
Copy link
Member

Choose a reason for hiding this comment

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

subitem is a fine name. let's leave it there.

Copy link
Contributor Author

@samccone samccone Oct 22, 2016

Choose a reason for hiding this comment

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

we are defining a new var with a new type so we need a new value since subitem can be either a string or a AuditResult.

if we simply redefine the subitem it still is a valid union type of string or AuditResult thus the rest of the code complains since props like extendedInfo do not exist on type string.

Copy link
Member

Choose a reason for hiding this comment

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

  1. subScore doesn't make sense as a name for AuditResult objects.
  2. you nuked if (subitem.comingSoon) return; and don't handle it. you can address your above conflict by filtering out the comingSoon guys this way, eh?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated, refer to the last commit

Copy link
Contributor Author

Choose a reason for hiding this comment

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

as for 2. this was removed based on a prior convo we had about this going away. I have reverted that and now the code lines up with hiding those audit results on the CLI (as ToT currently does).

Thanks!

@@ -0,0 +1,11 @@
{
"scripts": {
Copy link
Member

Choose a reason for hiding this comment

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

does it make sense to add a one-off compile to the parent module's postinstall and prepublish?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

def not postinstall since it does not do what you expect, maybe to prepublish though.. but this is more related to how we do releases which is already pretty complex. I would prefer to simply make a release script than shoehorn the functionality into the lifecycle scripts of npm.

Copy link
Member

Choose a reason for hiding this comment

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

okay can you followup with @brendankenny then? He's looking at streamlining our release process.

Also, please add developer docs for this to the main readme asap.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

@paulirish paulirish left a comment

Choose a reason for hiding this comment

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

Alright I think this is looking good. Once the scripts question is settled, then we're good to land.

There is still some hesitation from folks that I'm sensitive to about landing this TS refactor, but I feel confident it's an overall win, gaining assuredness at the expense of a compile step. That said, if it's a consistent problem for people in ~1 month (by Thanksgiving), we can revert if need be.

I'm an optimist, though; let's try to make it work. :)

@paulirish paulirish merged commit efd7105 into master Oct 24, 2016
@paulirish paulirish changed the title [Proposal] Convert CLI to 100% typescript Convert CLI to 100% typescript Oct 24, 2016
@samccone samccone deleted the sjs/ts-cli branch October 24, 2016 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants