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

update protocol-dts-generator to add command and event to types mapping #113

Merged
merged 9 commits into from
Aug 31, 2018

Conversation

brendankenny
Copy link
Contributor

@brendankenny brendankenny commented Aug 13, 2018

Previously the protocol's typescript definition included not just the types used by the protocol but also a set of generated methods that a theoretical interface for the API could use, e.g. the protocol command Console.clearMessages has a matching interface entry Protocol.ConsoleApi.clearMessages(): Promise<void>;.

That can be confusing because all the different clients have exposed the protocol in different ways. e.g. lighthouse supports await sendCommand('Console.clearMessages'), chrome-remote-interface has await client.Console.clearMessages(), etc.

This PR removes those methods (leaving all the type definitions), and instead builds foundational types that map events to their payloads and commands to their parameters and responses. e.g.

export module ProtocolMapping {
  export interface Events {
    'Console.messageAdded': Protocol.Console.MessageAddedEvent;
    'Debugger.breakpointResolved': Protocol.Debugger.BreakpointResolvedEvent;
    // ...
  }

  export interface Commands {
    'Console.clearMessages': {
      paramsType: [];
      returnType: void;
    };
    'Debugger.continueToLocation': {
      paramsType: [Protocol.Debugger.ContinueToLocationRequest];
      returnType: void;
    };
  }
}

Libraries that use the protocol can then uses these mappings to build their own type definitions (in their own repos or in DefinitelyTyped)

This is similar to the model Lighthouse and Puppeteer currently use. In Lighthouse we take this mapping and then transform it for different purposes (for instance, this defines the union of possible JSON responses from sending protocol commands).

Also cleans up the dts generator a bit.

@brendankenny
Copy link
Contributor Author

One new thing is that this uses the new tuple features in TypeScript 3.0, so I've kept the mappings in a new file in case users of older versions of typescript need the base object types.

The new tuple syntax allow bypassing a lot of the backflips we had to do because typescript struggles at the boundary of function overloading and type unions. This was an issue because we're building the types up dynamically but some commands have parameters and some don't (and some have parameters that are entirely optional). .

Now ParamType, (void | ParamType), and void are unified as [ParamType], [ParamType?], and [], all of which can now just be spread in a single function declaration instead of having to deal with overloading at all.

@paulirish
Copy link
Member

cc @nojvek @JoelEinbinder wanna take a look?

@@ -2,94 +2,14 @@
* Auto-generated by protocol-dts-generator.ts, do not edit manually. *
**********************************************************************/

export namespace Protocol {
export module Protocol {
Copy link
Contributor Author

@brendankenny brendankenny Aug 13, 2018

Choose a reason for hiding this comment

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

the diff gets a little wonky depending on how far you scroll but the only changes in here are

  • s/namespace/module
  • DomainApi interfaces deleted
  • whitespace adjustments
  • descriptions for event types now included in jsdoc

Copy link
Contributor

Choose a reason for hiding this comment

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

why module but not namespaces?

Initially added the DomainApi so the types work with noice-rpc-json out of the box. I believe vscode-chrome-debugger also uses this info so this could be a breaking change for some clients.

@roblourens please confirm.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

looks like vscode-chrome-debug-core does indeed depend on these. We'll figure out what we should do

Copy link
Contributor Author

Choose a reason for hiding this comment

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

why module but not namespaces?

yeah, looks like you're right

const eventDefs = domain.events ? domain.events.map(e => emitEvent(e, domainName)) : []
emitOpenBlock(`export module ${domainName}`)
if (domain.types) domain.types.forEach(emitDomainType)
if (domain.commands) domain.commands.forEach(c => emitCommand(c))
Copy link
Member

Choose a reason for hiding this comment

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

forEach(emitCommand) right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah yeah, good call

emitOpenBlock(`export module ${domainName}`)
if (domain.types) domain.types.forEach(emitDomainType)
if (domain.commands) domain.commands.forEach(c => emitCommand(c))
if (domain.events) domain.events.forEach(e => emitEvent(e))
Copy link
Member

Choose a reason for hiding this comment

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

forEach(emitEvent)

return `${getPropertyType(prop.items)}[]`
else if (prop.type == 'object')
return `any`
else if (prop.type == 'function')
Copy link
Member

Choose a reason for hiding this comment

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

just checking: function and lambda dont need to be handled any longer?

Copy link
Contributor Author

@brendankenny brendankenny Aug 13, 2018

Choose a reason for hiding this comment

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

just checking: function and lambda dont need to be handled any longer?

right. They were types constructed just by the dts generator itself (in the old versions of emitCommand and emitEvent) to be able to use the same emitInterface pipeline to output the generated command and event listener functions.

(similar to the new getCommandMapping and getEventMapping, but they just reuse the RefType that the protocol already uses for cross references within itself)

@brendankenny
Copy link
Contributor Author

Update to use these definitions in Lighthouse: GoogleChrome/lighthouse@049af19...7488a41
(if this PR lands, will also delete crdp-mapping.d.ts in favor of the protocol-mapping.d.ts from this PR).

sendCommand in driver.js shows the much simpler rest/spread of Protocol.Commands[C]['paramsType'], which automatically handles the 0, 1, and optional argument cases (rather than dealing with overloading sendCommand to accommodate void and non-void parameters)

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.

lgtm

anyone else want to look before we merge?

}
const getPropertyDef = (prop: P.PropertyType): string => {
// Quote key if it has a . in it.
const propName = /\./.test(prop.name) ? `'${prop.name}'` : prop.name
Copy link
Member

Choose a reason for hiding this comment

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

i'm a sucker for str.includes but w/e

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd agree that str.includes would be more readable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd agree that str.includes would be more readable

lol ok fine

@JoelEinbinder
Copy link

Concept looks sound.

@nojvek
Copy link
Contributor

nojvek commented Aug 15, 2018 via email

"ts-node": "^5.0.1",
"typescript": "^2.7.2"
"ts-node": "^7.0.0",
"typescript": "3.0.1"
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

type: 'object',
name: `${domainName}.${command.name}`,
// TODO(bckenny): does having a description help anywhere?
// description: command.description,
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 the harm in having description?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what's the harm in having description?

no harm except file size and maybe ts parse time? The main thing is I couldn't find any place where the descriptions end up surfaced anyways since the mapping objects can't really be used directly, so it didn't seem worth it. I left them commented out so if it turns out we do want them you don't have to wade into the code again.

It seemed maybe better to take the command descriptions and somehow modify them to apply them to the CommandRequest and CommandResponse types, as those do show up in completions and intellisense, but I didn't see an easy way to modify the strings to apply the types. "The request parameters for a command that..." or something :)

}

type ParamOrFunction = P.ParameterType | P.FunctionType
const jsProtocol: IProtocol = require('../json/js_protocol.json')
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you 🙏 . If latest tsc does indeed validate the require-ed json against the IProtocol type, you can remove the TODO on line 5.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If latest tsc does indeed validate the require-ed json against the IProtocol type, you can remove the TODO on line 5.

sadly it does not, the require() comes in as any, but if I do "esModuleInterop": true in tsconfig and go back to import jsProtocol from '../json/js_protocol.json'; I can't apply an interface to the import statement (and if you apply the interface later, things like property type fields have been widened to string instead of their literal values so it fails).

Weirdly the js version of this works fine

/** @typedef {import('./protocol-schema').IProtocol} IProtocol */

/** @type {IProtocol} */
const jsProtocol = require('../json/js_protocol.json')

and any changes to the schema or json are flagged as type errors.

So, leaving the TODO for now, but I can unassign you if you'd like :)

Copy link
Contributor

Choose a reason for hiding this comment

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

works for me as is. Don't remove the TODO just yet.

I do quite like the work tsc team having doing regarding jsdoc type checking and //@ts-check. I know Google's lingua franca is Js. So if we want this file to be JS with jsdoc type annotations, I don't mind it at all.

Doesn't have to be this PR right away.

emitLine()
}

const emitModule = (moduleName:string, domains: P.Domain[]) => {
const emitModule = (moduleName: string, domains: P.Domain[]) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

ha! spaces! We should just get this tslinted so you don't have to curse me everytime you see a missing space.

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 should just get this tslinted

agreed :)

// TODO: actually 'any'? or can use generic '[key: string]: string'?
return `any`
} else {
// hack: access indent, \n directly
Copy link
Contributor

Choose a reason for hiding this comment

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

You could make this into a less hacky version by implementing a startSubEmit() and endSubEmit(). startSubEmit saves old str and indent in a stack. all emitX calls go to a new str, so you can you use emitLine(), emiOpenBlock and emitCloseBlock without guilt. endSubEmit() would return the str that was generated by the emitCalls.

I know you're only using it here, but I imagine the next dev may follow hack pattern to make more hacks. Up-to-you though, don't wanna bikeshed too much.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You could make this into a less hacky version by implementing a startSubEmit() and endSubEmit()

In the original form of this PR I reworked the whole emit system just for this one spot, but it ended up seeming like way too many changes for what's just a simple reach into the indent state, so I reverted all that and did this instead.

I know you're only using it here, but I imagine the next dev may follow hack pattern to make more hacks.

Agreed, but maybe the (eventual) need for a second hack will be a better justification for the refactor, and give better insight on what form it should take.

/**
* Line number in the resource that generated this message (1-based).
*/
line?: integer;

Copy link
Contributor

Choose a reason for hiding this comment

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

Don't the spaces make the file more readable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't the spaces make the file more readable?

It was way too much whitespace in the mappings file and removing it from interfaces in the protocol file seemed still readable since most have a description comment block. And I didn't want to pipe in a boolean for extra lines or not :)

Can re-add spacing if others agree it's better with it.

Copy link
Member

Choose a reason for hiding this comment

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

no strong feelings but i'm fine without the spaces. it's dense but the comments already give things plenty of room.

Copy link
Contributor

@nojvek nojvek left a comment

Choose a reason for hiding this comment

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

W4m, minor nits and ask that we don't break any existing clients.

@brendankenny
Copy link
Contributor Author

ok, I added back the ProtocolApi interfaces and methods since at least vscode-chrome-debug-core uses them.

The compromise is that they're now in a different file than protocol.d.ts, so they'll have to be imported from protocol-api.d.ts. What do folks think?

Also happy to bikeshed on naming. It might be a bit weird to call it protocol-api.d.ts since it's really an API for the protocol, not the API :)

@nojvek
Copy link
Contributor

nojvek commented Aug 17, 2018

I propose we call it protocol-rpc-api or protocol-proxy-api, up to you.

I assume this will be a major release for devtools-protocol since it's probably a breaking change for some clients as files have moved.

Release notes on what changed would be nice too.

Thanks for grinding through this @brendankenny . Feels good to see puppeteer will be using something that started as an evening hack.

@brendankenny
Copy link
Contributor Author

sorry for the pause here. I ran into a significant typescript performance issue with the approach in protocol-mapping.d.ts (microsoft/TypeScript#26756), but that's been fixed on master in microsoft/TypeScript#26790. Since this is a totally new file that won't affect existing users, I feel good moving forward with it even though it requires typescript@next for decent performance until 3.1 comes out.

Also happy to bikeshed on file naming. It might be a bit weird to call it protocol-api.d.ts since it's really an API for the protocol, not the API :)

I propose we call it protocol-rpc-api or protocol-proxy-api, up to you.

@paulirish thoughts on file naming?

Release notes on what changed would be nice too.

@paulirish on this too. Looks like the changelog is updated automatically. Should I just insert a note in there anyways or is there a better spot?

@paulirish
Copy link
Member

naming

protocol-proxy-api sg.
domain-namespace-api is the closest collection of terms that I think of that fits, but i dont think its great.

changelog

changelog generation will overwrite it. that changelog is only about the protocol anyway, not the package. lets just put something the readme.

@brendankenny
Copy link
Contributor Author

done

@paulirish paulirish merged commit 93338d7 into ChromeDevTools:master Aug 31, 2018
@paulirish
Copy link
Member

published to devtools-protocol@0.0.588129

@paulirish
Copy link
Member

thanks so much to @brendankenny for taking this on!

@@ -6,5 +6,8 @@ Please [file issues](https://github.com/ChromeDevTools/devtools-protocol/issues)
Use the [protocol viewer](https://chromedevtools.github.io/devtools-protocol/) for navigating the protocol. File issues at [its repo](https://github.com/ChromeDevTools/debugger-protocol-viewer) if you have a bug with the documentation webapp.


TypeScript definitions for the protocol's types are available in ['types/protocol.d.ts'](https://github.com/ChromeDevTools/devtools-protocol/tree/master/types). Mappings from Commands and events to these types are available in either generated `DomainApi` style in [`types/protocol-proxy-api.d.ts`](https://github.com/ChromeDevTools/devtools-protocol/blob/master/types/protocol-proxy-api.d.ts) or in simple name-to-type-interface style in [`types/protocol-mapping.d.ts`](https://github.com/ChromeDevTools/devtools-protocol/blob/master/types/protocol-mapping.d.ts).
Copy link
Contributor

Choose a reason for hiding this comment

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

😍

@brendankenny brendankenny deleted the newdts branch August 31, 2018 20:56
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.

4 participants