From f2c968fdb808970a742a2228ad2f65fe708322b0 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Mon, 28 Jan 2019 18:12:45 -0500 Subject: [PATCH] chore(types): generate our own d.ts file from api.md (#3744) Generate `//index.d.ts` file with precise typescript definitions for all of the Puppeteer API. --- .appveyor.yml | 3 +- .cirrus.yml | 2 + .gitignore | 1 + .travis.yml | 1 + docs/api.md | 39 ++- lib/Page.js | 1 - package.json | 5 +- .../doclint/check_public_api/Documentation.js | 31 +- utils/doclint/check_public_api/MDBuilder.js | 181 ++++++++--- .../test/md-builder-common/result.txt | 6 + utils/doclint/generate_types/index.js | 217 ++++++++++++++ utils/doclint/generate_types/test/test.ts | 283 ++++++++++++++++++ .../doclint/generate_types/test/tsconfig.json | 11 + 13 files changed, 706 insertions(+), 75 deletions(-) create mode 100644 utils/doclint/generate_types/index.js create mode 100644 utils/doclint/generate_types/test/test.ts create mode 100644 utils/doclint/generate_types/test/tsconfig.json diff --git a/.appveyor.yml b/.appveyor.yml index bbad0494578be..2a8d3bef9a61f 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -11,7 +11,8 @@ install: - if "%nodejs_version%" == "8.11.3" ( npm run lint && npm run coverage && - npm run test-doclint + npm run test-doclint && + npm run test-types ) else ( npm run unit-node6 ) diff --git a/.cirrus.yml b/.cirrus.yml index 01b75188e88a3..82b078606ecdd 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -20,6 +20,7 @@ task: lint_script: npm run lint coverage_script: npm run coverage test_doclint_script: npm run test-doclint + test_types_script: npm run test-types task: osx_instance: @@ -34,3 +35,4 @@ task: lint_script: npm run lint coverage_script: npm run coverage test_doclint_script: npm run test-doclint + test_types_script: npm run test-types diff --git a/.gitignore b/.gitignore index f4be4d4157ce5..b6fc52d812df5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ yarn.lock /node6 /lib/protocol.d.ts /utils/browser/puppeteer-web.js +/index.d.ts diff --git a/.travis.yml b/.travis.yml index 3e6cc9b70554d..9c54939442c1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,7 @@ script: - 'if [ "$NODE8" = "true" ]; then npm run lint; fi' - 'if [ "$NODE8" = "true" ]; then npm run coverage; fi' - 'if [ "$NODE8" = "true" ]; then npm run test-doclint; fi' + - 'if [ "$NODE8" = "true" ]; then npm run test-types; fi' - 'if [ "$NODE8" = "true" ]; then npm run bundle; fi' - 'if [ "$NODE8" = "true" ]; then npm run unit-bundle; fi' - 'if [ "$NODE6" = "true" ]; then npm run unit-node6; fi' diff --git a/docs/api.md b/docs/api.md index f1cffee59fd42..5b846b401acc5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -565,7 +565,7 @@ The method initiates a GET request to download the revision from the host. - returns: <[Promise]<[Array]<[string]>>> A list of all revisions available locally on disk. #### browserFetcher.platform() -- returns: <[string]> Returns one of `mac`, `linux`, `win32` or `win64`. +- returns: <[string]> One of `mac`, `linux`, `win32` or `win64`. #### browserFetcher.remove(revision) - `revision` <[string]> a revision to remove. The method will throw if the revision has not been downloaded. @@ -582,7 +582,7 @@ The method initiates a GET request to download the revision from the host. ### class: Browser -* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) +* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) A Browser is created when Puppeteer connects to a Chromium instance, either through [`puppeteer.launch`](#puppeteerlaunchoptions) or [`puppeteer.connect`](#puppeteerconnectoptions). @@ -734,7 +734,7 @@ You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/versio ### class: BrowserContext -* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) +* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) BrowserContexts provide a way to operate multiple independent browser sessions. When a browser is launched, it has a single BrowserContext used by default. The method `browser.newPage()` creates a page in the default browser context. @@ -864,7 +864,7 @@ const newWindowTarget = await browserContext.waitForTarget(target => target.url( ### class: Page -* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) +* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in Chromium. One [Browser] instance might have multiple [Page] instances. @@ -1035,7 +1035,7 @@ Shortcut for [page.mainFrame().$$(selector)](#frameselector-1). #### page.$$eval(selector, pageFunction[, ...args]) - `selector` <[string]> A [selector] to query page for -- `pageFunction` <[function]> Function to be evaluated in browser context +- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` - returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` @@ -1050,7 +1050,7 @@ const divsCounts = await page.$$eval('div', divs => divs.length); #### page.$eval(selector, pageFunction[, ...args]) - `selector` <[string]> A [selector] to query page for -- `pageFunction` <[function]> Function to be evaluated in browser context +- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` - returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` @@ -1464,7 +1464,7 @@ Indicates that the page has been closed. - returns: <[Keyboard]> #### page.mainFrame() -- returns: <[Frame]> returns page's main frame. +- returns: <[Frame]> The page's main frame. Page is guaranteed to have a main frame which persists during navigations. @@ -1609,7 +1609,7 @@ Shortcut for [page.mainFrame().executionContext().queryObjects(prototypeHandle)] #### page.select(selector, ...values) - `selector` <[string]> A [selector] to query page for - `...values` <...[string]> Values of options to select. If the `` element matching `selector`, the method throws an error. @@ -1767,7 +1767,7 @@ Shortcut for [page.mainFrame().tap(selector)](#frametapselector). - returns: <[Target]> a target this page was created from. #### page.title() -- returns: <[Promise]<[string]>> Returns page's title. +- returns: <[Promise]<[string]>> The page's title. Shortcut for [page.mainFrame().title()](#frametitle). @@ -2042,7 +2042,7 @@ Most of the accessibility tree gets filtered out when converting from Blink AX T #### accessibility.snapshot([options]) - `options` <[Object]> - `interestingOnly` <[boolean]> Prune uninteresting nodes from the tree. Defaults to `true`. -- returns: <[Promise]<[Object]>> Returns an [AXNode] object with the following properties: +- returns: <[Promise]<[Object]>> An [AXNode] object with the following properties: - `role` <[string]> The [role](https://www.w3.org/TR/wai-aria/#usage_intro). - `name` <[string]> A human readable name for the node. - `value` <[string]|[number]> The current value of the node. @@ -2382,7 +2382,7 @@ The method runs `document.querySelectorAll` within the frame. If no elements mat #### frame.$$eval(selector, pageFunction[, ...args]) - `selector` <[string]> A [selector] to query frame for -- `pageFunction` <[function]> Function to be evaluated in browser context +- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` - returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` @@ -2397,7 +2397,7 @@ const divsCounts = await frame.$$eval('div', divs => divs.length); #### frame.$eval(selector, pageFunction[, ...args]) - `selector` <[string]> A [selector] to query frame for -- `pageFunction` <[function]> Function to be evaluated in browser context +- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` - returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` @@ -2580,12 +2580,12 @@ If the name is empty, returns the id attribute instead. > **NOTE** This value is calculated once when the frame is created, and will not update if the attribute is changed later. #### frame.parentFrame() -- returns: Returns parent frame, if any. Detached frames and main frames return `null`. +- returns: Parent frame, if any. Detached frames and main frames return `null`. #### frame.select(selector, ...values) - `selector` <[string]> A [selector] to query frame for - `...values` <...[string]> Values of options to select. If the `` element matching `selector`, the method throws an error. @@ -2614,7 +2614,7 @@ This method fetches an element with `selector`, scrolls it into view if needed, If there's no element matching `selector`, the method throws an error. #### frame.title() -- returns: <[Promise]<[string]>> Returns page's title. +- returns: <[Promise]<[string]>> The page's title. #### frame.type(selector, text[, options]) - `selector` <[string]> A [selector] of an element to type into. If there are multiple elements satisfying the selector, the first will be used. @@ -2925,8 +2925,7 @@ function, it **will not be called**. > **NOTE** The method will return an empty JSON object if the referenced object is not stringifiable. It will throw an error if the object has circular references. ### class: ElementHandle - -> **NOTE** Class [ElementHandle] extends [JSHandle]. +* extends: [JSHandle] ElementHandle represents an in-page DOM element. ElementHandles can be created with the [page.$](#pageselector) method. @@ -2960,7 +2959,7 @@ The method runs `element.querySelectorAll` within the page. If no elements match #### elementHandle.$$eval(selector, pageFunction[, ...args]) - `selector` <[string]> A [selector] to query page for -- `pageFunction` <[function]> Function to be evaluated in browser context +- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` - returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` @@ -2982,7 +2981,7 @@ expect(await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText))). #### elementHandle.$eval(selector, pageFunction[, ...args]) - `selector` <[string]> A [selector] to query page for -- `pageFunction` <[function]> Function to be evaluated in browser context +- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` - returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` @@ -3432,7 +3431,7 @@ Identifies what kind of target this is. Can be `"page"`, [`"background_page"`](h ### class: CDPSession -* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) +* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) The `CDPSession` instances are used to talk raw Chrome Devtools Protocol: - protocol methods can be called with `session.send` method. diff --git a/lib/Page.js b/lib/Page.js index cefe261a2728b..dc4c2f44e0859 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -46,7 +46,6 @@ class Page extends EventEmitter { await client.send('Page.enable'); const {frameTree} = await client.send('Page.getFrameTree'); const page = new Page(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue); - await Promise.all([ client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}), client.send('Page.setLifecycleEventsEnabled', { enabled: true }), diff --git a/package.json b/package.json index 4f882767104fd..6b72d4e9f521d 100644 --- a/package.json +++ b/package.json @@ -14,18 +14,19 @@ "unit": "node test/test.js", "debug-unit": "node --inspect-brk test/test.js", "test-doclint": "node utils/doclint/check_public_api/test/test.js && node utils/doclint/preprocessor/test.js", - "test": "npm run lint --silent && npm run coverage && npm run test-doclint && npm run test-node6-transformer", + "test": "npm run lint --silent && npm run coverage && npm run test-doclint && npm run test-node6-transformer && npm run test-types", "install": "node install.js", "lint": "([ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .) && npm run tsc && npm run doc", "doc": "node utils/doclint/cli.js", "coverage": "cross-env COVERAGE=true npm run unit", "test-node6-transformer": "node utils/node6-transform/test/test.js", - "build": "node utils/node6-transform/index.js", + "build": "node utils/node6-transform/index.js && node utils/doclint/generate_types", "unit-node6": "node node6/test/test.js", "tsc": "tsc -p .", "prepublishOnly": "npm run build", "apply-next-version": "node utils/apply_next_version.js", "bundle": "npx browserify -r ./index.js:puppeteer -o utils/browser/puppeteer-web.js", + "test-types": "node utils/doclint/generate_types && npx -p typescript@2.1 tsc -p utils/doclint/generate_types/test/", "unit-bundle": "node utils/browser/test.js" }, "author": "The Chromium Authors", diff --git a/utils/doclint/check_public_api/Documentation.js b/utils/doclint/check_public_api/Documentation.js index 5a47b221f0cac..cc83bf88f891e 100644 --- a/utils/doclint/check_public_api/Documentation.js +++ b/utils/doclint/check_public_api/Documentation.js @@ -31,8 +31,10 @@ Documentation.Class = class { /** * @param {string} name * @param {!Array} membersArray + * @param {?string=} extendsName + * @param {string=} comment */ - constructor(name, membersArray) { + constructor(name, membersArray, extendsName = null, comment = '') { this.name = name; this.membersArray = membersArray; /** @type {!Map} */ @@ -43,6 +45,8 @@ Documentation.Class = class { this.methods = new Map(); /** @type {!Map} */ this.events = new Map(); + this.comment = comment; + this.extends = extendsName; for (const member of membersArray) { this.members.set(member.name, member); if (member.kind === 'method') @@ -59,14 +63,17 @@ Documentation.Member = class { /** * @param {string} kind * @param {string} name - * @param {!Documentation.Type} type + * @param {?Documentation.Type} type * @param {!Array} argsArray */ - constructor(kind, name, type, argsArray) { + constructor(kind, name, type, argsArray, comment = '', returnComment = '', required = true) { this.kind = kind; this.name = name; this.type = type; + this.comment = comment; + this.returnComment = returnComment; this.argsArray = argsArray; + this.required = required; /** @type {!Map} */ this.args = new Map(); for (const arg of argsArray) @@ -79,25 +86,29 @@ Documentation.Member = class { * @param {?Documentation.Type} returnType * @return {!Documentation.Member} */ - static createMethod(name, argsArray, returnType) { - return new Documentation.Member('method', name, returnType, argsArray,); + static createMethod(name, argsArray, returnType, returnComment, comment) { + return new Documentation.Member('method', name, returnType, argsArray, comment, returnComment); } /** * @param {string} name - * @param {!Documentation.Type} + * @param {!Documentation.Type} type + * @param {string=} comment + * @param {boolean=} required * @return {!Documentation.Member} */ - static createProperty(name, type) { - return new Documentation.Member('property', name, type, []); + static createProperty(name, type, comment, required) { + return new Documentation.Member('property', name, type, [], comment, undefined, required); } /** * @param {string} name + * @param {?Documentation.Type=} type + * @param {string=} comment * @return {!Documentation.Member} */ - static createEvent(name) { - return new Documentation.Member('event', name, null, []); + static createEvent(name, type = null, comment) { + return new Documentation.Member('event', name, type, [], comment); } }; diff --git a/utils/doclint/check_public_api/MDBuilder.js b/utils/doclint/check_public_api/MDBuilder.js index 375d45fc74275..bc3776aed5148 100644 --- a/utils/doclint/check_public_api/MDBuilder.js +++ b/utils/doclint/check_public_api/MDBuilder.js @@ -30,57 +30,44 @@ class MDOutline { const writer = new commonmark.HtmlRenderer(); const html = writer.render(parsed); + page.on('console', msg => { + console.log(msg.text()); + }); // Extract headings. await page.setContent(html); const {classes, errors} = await page.evaluate(() => { const classes = []; - let currentClass = {}; - let member = {}; const errors = []; - for (const element of document.body.querySelectorAll('h3, h4, h4 + ul > li')) { - if (element.matches('h3')) { - currentClass = { - name: element.textContent, - members: [], - }; - classes.push(currentClass); - } else if (element.matches('h4')) { - member = { - name: element.textContent, - args: [], - returnType: null - }; - currentClass.members.push(member); - } else if (element.matches('li') && element.firstChild.matches && element.firstChild.matches('code')) { - member.args.push(parseProperty(element)); - } else if (element.matches('li') && element.firstChild.nodeType === Element.TEXT_NODE && element.firstChild.textContent.toLowerCase().startsWith('return')) { - member.returnType = parseProperty(element); - const expectedText = 'returns: '; - let actualText = element.firstChild.textContent; - let angleIndex = actualText.indexOf('<'); - let spaceIndex = actualText.indexOf(' '); - angleIndex = angleIndex === -1 ? actualText.length : angleIndex; - spaceIndex = spaceIndex === -1 ? actualText.length : spaceIndex + 1; - actualText = actualText.substring(0, Math.min(angleIndex, spaceIndex)); - if (actualText !== expectedText) - errors.push(`${member.name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.`); - } + const headers = document.body.querySelectorAll('h3'); + for (let i = 0; i < headers.length; i++) { + const fragment = extractSiblingsIntoFragment(headers[i], headers[i + 1]); + classes.push(parseClass(fragment)); } return {classes, errors}; + /** + * @param {HTMLLIElement} element + */ function parseProperty(element) { - const str = element.textContent; - const name = str.substring(0, str.indexOf('<')).trim(); + const clone = element.cloneNode(true); + const ul = clone.querySelector(':scope > ul'); + const str = parseComment(extractSiblingsIntoFragment(clone.firstChild, ul)); + const name = str.substring(0, str.indexOf('<')).replace(/\`/g, '').trim(); const type = findType(str); const properties = []; + const comment = str.substring(str.indexOf('<') + type.length + 2).trim(); // Strings have enum values instead of properties if (!type.includes('string')) { - for (const childElement of element.querySelectorAll(':scope > ul > li')) - properties.push(parseProperty(childElement)); + for (const childElement of element.querySelectorAll(':scope > ul > li')) { + const property = parseProperty(childElement); + property.required = property.comment.includes('***required***'); + properties.push(property); + } } return { name, type, + comment, properties }; } @@ -100,6 +87,107 @@ class MDOutline { } return 'unknown'; } + + /** + * @param {DocumentFragment} content + */ + function parseClass(content) { + const members = []; + const headers = content.querySelectorAll('h4'); + const name = content.firstChild.textContent; + let extendsName = null; + let commentStart = content.firstChild.nextSibling; + const extendsElement = content.querySelector('ul'); + if (extendsElement && extendsElement.textContent.trim().startsWith('extends:')) { + commentStart = extendsElement.nextSibling; + extendsName = extendsElement.querySelector('a').textContent; + } + const comment = parseComment(extractSiblingsIntoFragment(commentStart, headers[0])); + for (let i = 0; i < headers.length; i++) { + const fragment = extractSiblingsIntoFragment(headers[i], headers[i + 1]); + members.push(parseMember(fragment)); + } + return { + name, + comment, + extendsName, + members + }; + } + + /** + * @param {Node} content + */ + function parseComment(content) { + for (const code of content.querySelectorAll('pre > code')) + code.replaceWith('```' + code.className.substring('language-'.length) + '\n' + code.textContent + '```'); + for (const code of content.querySelectorAll('code')) + code.replaceWith('`' + code.textContent + '`'); + for (const strong of content.querySelectorAll('strong')) + strong.replaceWith('**' + parseComment(strong) + '**'); + return content.textContent.trim(); + } + + /** + * @param {string} name + * @param {DocumentFragment} content + */ + function parseMember(content) { + const name = content.firstChild.textContent; + const args = []; + let returnType = null; + + const paramRegex = /^\w+\.[\w$]+\((.*)\)$/; + const matches = paramRegex.exec(name) || ['', '']; + const parameters = matches[1]; + const optionalStartIndex = parameters.indexOf('['); + const optinalParamsStr = optionalStartIndex !== -1 ? parameters.substring(optionalStartIndex).replace(/[\[\]]/g, '') : ''; + const optionalparams = new Set(optinalParamsStr.split(',').filter(x => x).map(x => x.trim())); + const ul = content.querySelector('ul'); + for (const element of content.querySelectorAll('h4 + ul > li')) { + if (element.matches('li') && element.textContent.trim().startsWith('<')) { + returnType = parseProperty(element); + } else if (element.matches('li') && element.firstChild.matches && element.firstChild.matches('code')) { + const property = parseProperty(element); + property.required = !optionalparams.has(property.name); + args.push(property); + } else if (element.matches('li') && element.firstChild.nodeType === Element.TEXT_NODE && element.firstChild.textContent.toLowerCase().startsWith('return')) { + returnType = parseProperty(element); + const expectedText = 'returns: '; + let actualText = element.firstChild.textContent; + let angleIndex = actualText.indexOf('<'); + let spaceIndex = actualText.indexOf(' '); + angleIndex = angleIndex === -1 ? actualText.length : angleIndex; + spaceIndex = spaceIndex === -1 ? actualText.length : spaceIndex + 1; + actualText = actualText.substring(0, Math.min(angleIndex, spaceIndex)); + if (actualText !== expectedText) + errors.push(`${name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.`); + } + } + const comment = parseComment(extractSiblingsIntoFragment(ul ? ul.nextSibling : content)); + return { + name, + args, + returnType, + comment + }; + } + + /** + * @param {!Node} fromInclusive + * @param {!Node} toExclusive + * @return {!DocumentFragment} + */ + function extractSiblingsIntoFragment(fromInclusive, toExclusive) { + const fragment = document.createDocumentFragment(); + let node = fromInclusive; + while (node && node !== toExclusive) { + const next = node.nextSibling; + fragment.appendChild(node); + node = next; + } + return fragment; + } }); return new MDOutline(classes, errors); } @@ -114,11 +202,15 @@ class MDOutline { const eventRegex = /^event: '(\w+)'$/; let currentClassName = null; let currentClassMembers = []; + let currentClassComment = ''; + let currentClassExtends = null; for (const cls of classes) { const match = cls.name.match(classHeading); if (!match) continue; currentClassName = match[1]; + currentClassComment = cls.comment; + currentClassExtends = cls.extendsName; for (const member of cls.members) { if (constructorRegex.test(member.name)) { const match = member.name.match(constructorRegex); @@ -147,15 +239,20 @@ class MDOutline { this.errors.push(`Heading arguments for "${member.name}" do not match described ones, i.e. "${parameters}" != "${member.args.map(a => a.name).join(', ')}"`); const args = member.args.map(createPropertyFromJSON); let returnType = null; - if (member.returnType) - returnType = createPropertyFromJSON(member.returnType).type; - const method = Documentation.Member.createMethod(methodName, args, returnType); + let returnComment = ''; + if (member.returnType) { + const returnProperty = createPropertyFromJSON(member.returnType); + returnType = returnProperty.type; + returnComment = returnProperty.comment; + } + const method = Documentation.Member.createMethod(methodName, args, returnType, returnComment, member.comment); currentClassMembers.push(method); } function createPropertyFromJSON(payload) { const type = new Documentation.Type(payload.type, payload.properties.map(createPropertyFromJSON)); - return Documentation.Member.createProperty(payload.name, type); + const required = payload.required; + return Documentation.Member.createProperty(payload.name, type, payload.comment, required); } function handleProperty(member, className, propertyName) { @@ -163,7 +260,9 @@ class MDOutline { this.errors.push(`Failed to process header as property: ${member.name}`); return; } - currentClassMembers.push(Documentation.Member.createProperty(propertyName)); + const type = member.returnType ? member.returnType.type : null; + const properties = member.returnType ? member.returnType.properties : []; + currentClassMembers.push(createPropertyFromJSON({type, name: propertyName, properties, comment: member.comment})); } function handleEvent(member, eventName) { @@ -171,13 +270,13 @@ class MDOutline { this.errors.push(`Failed to process header as event: ${member.name}`); return; } - currentClassMembers.push(Documentation.Member.createEvent(eventName)); + currentClassMembers.push(Documentation.Member.createEvent(eventName, member.returnType && createPropertyFromJSON(member.returnType).type, member.comment)); } function flushClassIfNeeded() { if (currentClassName === null) return; - this.classes.push(new Documentation.Class(currentClassName, currentClassMembers)); + this.classes.push(new Documentation.Class(currentClassName, currentClassMembers, currentClassExtends, currentClassComment)); currentClassName = null; currentClassMembers = []; } diff --git a/utils/doclint/check_public_api/test/md-builder-common/result.txt b/utils/doclint/check_public_api/test/md-builder-common/result.txt index 0265d3571b5cb..46c065f26b359 100644 --- a/utils/doclint/check_public_api/test/md-builder-common/result.txt +++ b/utils/doclint/check_public_api/test/md-builder-common/result.txt @@ -5,6 +5,9 @@ "members": [ { "name": "frame", + "type": { + "name": "[Frame]" + }, "kind": "event" }, { @@ -25,6 +28,9 @@ }, { "name": "url", + "type": { + "name": "string" + }, "kind": "property" } ] diff --git a/utils/doclint/generate_types/index.js b/utils/doclint/generate_types/index.js new file mode 100644 index 0000000000000..0fa0d54fff42b --- /dev/null +++ b/utils/doclint/generate_types/index.js @@ -0,0 +1,217 @@ +const path = require('path'); +const Source = require('../Source'); +const puppeteer = require('../../..'); +const PROJECT_DIR = path.join(__dirname, '..', '..', '..'); +const fs = require('fs'); +const objectDefinitions = []; +(async function() { + const browser = await puppeteer.launch(); + const page = (await browser.pages())[0]; + const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md')); + const {documentation} = await require('../check_public_api/MDBuilder')(page, [api]); + await browser.close(); + const classes = documentation.classesArray.slice(1); + const root = documentation.classesArray[0]; + const output = `// This file is generated by ${__filename.substring(path.join(__dirname, '..', '..').length)} +import { ChildProcess } from 'child_process'; +import { EventEmitter } from 'events'; +/** + * Can be converted to JSON + */ +interface Serializable {} +interface ConnectionTransport {} + +${root.membersArray.map(member => ` +${memberJSDOC(member, '')}export function ${member.name}${argsFromMember(member)} : ${typeToString(member.type, member.name)}; +`).join('')} +${classes.map(classDesc => classToString(classDesc)).join('\n')} +${objectDefinitionsToString()} +`; + fs.writeFileSync(path.join(PROJECT_DIR, 'index.d.ts'), output, 'utf8'); +})(); + +function objectDefinitionsToString() { + let definition; + const parts = []; + while ((definition = objectDefinitions.pop())) { + const {name, properties} = definition; + parts.push(`interface ${name} {`); + parts.push(properties.map(member => ` ${memberJSDOC(member, ' ')}${nameForProperty(member)}${argsFromMember(member, name)}: ${typeToString(member.type, name, member.name)};`).join('\n\n')); + parts.push('}\n'); + } + return parts.join('\n'); +} + +function nameForProperty(member) { + return (member.required || member.name.startsWith('...')) ? member.name : member.name + '?'; +} + +/** + * @param {import('./check_public_api/Documentation').Class} classDesc + */ +function classToString(classDesc) { + const parts = []; + if (classDesc.comment) { + parts.push(`/** + * ${classDesc.comment.split('\n').join('\n * ')} + */`); + } + parts.push(`export interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`); + for (const method of ['on', 'once', 'addListener']) { + for (const [eventName, value] of classDesc.events) { + if (value.comment) { + parts.push(' /**'); + parts.push(...value.comment.split('\n').map(line => ' * ' + line)); + parts.push(' */'); + } + parts.push(` ${method}(event: '${eventName}', listener: (arg0 : ${typeToString(value && value.type, classDesc.name, eventName, 'payload')}) => void): this;\n`); + } + } + parts.push(classDesc.membersArray.map(member => ` ${memberJSDOC(member, ' ')}${member.name}${argsFromMember(member, classDesc.name)}: ${typeToString(member.type, classDesc.name, member.name)};`).join('\n\n')); + parts.push('}\n'); + return parts.join('\n'); +} + +/** + * @param {import('./check_public_api/Documentation').Type} type + */ +function typeToString(type, ...namespace) { + if (!type) + return 'void'; + let typeString = stringifyType(parseType(type.name)); + for (let i = 0; i < type.properties.length; i++) + typeString = typeString.replace('arg' + i, type.properties[i].name); + if (type.properties.length && typeString.indexOf('Object') !== -1) { + const name = namespace.map(n => n[0].toUpperCase() + n.substring(1)).join(''); + typeString = typeString.replace('Object', name); + objectDefinitions.push({name, properties: type.properties}); + } + return typeString; +} + +/** + * @param {string} type + */ +function parseType(type) { + type = type.trim(); + if (type.startsWith('?')) { + const parsed = parseType(type.substring(1)); + parsed.nullable = true; + return parsed; + } + if (type.startsWith('...')) + return parseType('Array<' + type.substring(3) + '>'); + let name = type; + let next = null; + let template = null; + let args = null; + let retType = null; + let firstTypeLength = type.length; + for (let i = 0; i < type.length; i++) { + if (type[i] === '<') { + name = type.substring(0, i); + const matching = matchingBracket(type.substring(i), '<', '>'); + template = parseType(type.substring(i + 1, i + matching - 1)); + firstTypeLength = i + matching; + break; + } + if (type[i] === '(') { + name = type.substring(0, i); + const matching = matchingBracket(type.substring(i), '(', ')'); + args = parseType(type.substring(i + 1, i + matching - 1)); + i = i + matching; + if (type[i] === ':') { + retType = parseType(type.substring(i + 1)); + next = retType.next; + retType.next = null; + break; + } + } + if (type[i] === '|' || type[i] === ',') { + name = type.substring(0, i); + firstTypeLength = i; + break; + } + } + let pipe = null; + if (type[firstTypeLength] === '|') + pipe = parseType(type.substring(firstTypeLength + 1)); + else if (type[firstTypeLength] === ',') + next = parseType(type.substring(firstTypeLength + 1)); + if (name === 'Promise' && !template) + template = parseType('void'); + return { + name, + args, + retType, + template, + pipe, + next + }; +} + +function stringifyType(parsedType) { + if (!parsedType) + return 'void'; + let out = parsedType.name; + if (parsedType.args) { + let args = parsedType.args; + const stringArgs = []; + while (args) { + const arg = args; + args = args.next; + arg.next = null; + stringArgs.push(stringifyType(arg)); + } + out = `(${stringArgs.map((type, index) => `arg${index} : ${type}`).join(', ')}, ...args: any[]) => ${stringifyType(parsedType.retType)}`; + } else if (parsedType.name === 'function') { + out = 'Function'; + } + if (parsedType.nullable) + out = 'null|' + out; + if (parsedType.template) + out += '<' + stringifyType(parsedType.template) + '>'; + if (parsedType.pipe) + out += '|' + stringifyType(parsedType.pipe); + if (parsedType.next) + out += ', ' + stringifyType(parsedType.next); + return out.trim(); +} + +function matchingBracket(str, open, close) { + let count = 1; + let i = 1; + for (; i < str.length && count; i++) { + if (str[i] === open) + count++; + else if (str[i] === close) + count--; + } + return i; +} + +/** + * @param {import('./check_public_api/Documentation').Member} member + */ +function argsFromMember(member, ...namespace) { + if (member.kind === 'property') + return ''; + return '(' + member.argsArray.map(arg => `${nameForProperty(arg)}: ${typeToString(arg.type, ...namespace, member.name, 'options')}`).join(', ') + ')'; +} +/** + * @param {import('./check_public_api/Documentation').Member} member + */ +function memberJSDOC(member, indent) { + const lines = []; + if (member.comment) + lines.push(...member.comment.split('\n')); + lines.push(...member.argsArray.map(arg => `@param ${arg.name.replace(/\./g, '')} ${arg.comment.replace('\n', ' ')}`)); + if (member.returnComment) + lines.push(`@returns ${member.returnComment}`); + if (!lines.length) + return ''; + return `/** +${indent} * ${lines.join('\n' + indent + ' * ')} +${indent} */ +${indent}`; +} \ No newline at end of file diff --git a/utils/doclint/generate_types/test/test.ts b/utils/doclint/generate_types/test/test.ts new file mode 100644 index 0000000000000..eea0f340ac90c --- /dev/null +++ b/utils/doclint/generate_types/test/test.ts @@ -0,0 +1,283 @@ +import * as puppeteer from "../../../../index"; + +// Examples taken from README +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto("https://example.com"); + await page.screenshot({ path: "example.png" }); + + browser.close(); +})(); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto("https://news.ycombinator.com", { waitUntil: "networkidle0" }); + await page.pdf({ path: "hn.pdf", format: "A4" }); + + browser.close(); +})(); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto("https://example.com"); + + // Get the "viewport" of the page, as reported by the page. + const dimensions = await page.evaluate(() => { + return { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + deviceScaleFactor: window.devicePixelRatio + }; + }); + + console.log("Dimensions:", dimensions); + + browser.close(); +})(); + +// The following examples are taken from the docs itself +puppeteer.launch().then(async browser => { + const page = await browser.newPage(); + page.on("console", (...args: any[]) => { + for (let i = 0; i < args.length; ++i) console.log(`${i}: ${args[i]}`); + }); + page.evaluate(() => console.log(5, "hello", { foo: "bar" })); + + const result = await page.evaluate(() => { + return Promise.resolve(8 * 7); + }); + console.log(await page.evaluate("1 + 2")); + + const bodyHandle = await page.$("body"); + + // Typings for this are really difficult since they depend on internal state + // of the page class. + const html = await page.evaluate( + (body: HTMLElement) => body.innerHTML, + bodyHandle + ); +}); + +import * as crypto from "crypto"; +import * as fs from "fs"; + +puppeteer.launch().then(async browser => { + const page = await browser.newPage(); + page.on("console", console.log); + await page.exposeFunction("md5", (text: string) => + crypto + .createHash("md5") + .update(text) + .digest("hex") + ); + await page.evaluate(async () => { + // use window.md5 to compute hashes + const myString = "PUPPETEER"; + const myHash = await (window as any).md5(myString); + console.log(`md5 of ${myString} is ${myHash}`); + }); + browser.close(); + + page.on("console", console.log); + await page.exposeFunction("readfile", async (filePath: string) => { + return new Promise((resolve, reject) => { + fs.readFile(filePath, "utf8", (err, text) => { + if (err) reject(err); + else resolve(text); + }); + }); + }); + await page.evaluate(async () => { + // use window.readfile to read contents of a file + const content = await (window as any).readfile("/etc/hosts"); + console.log(content); + }); + + await page.emulateMedia("screen"); + await page.pdf({ path: "page.pdf" }); + + await page.setRequestInterception(true); + page.on("request", interceptedRequest => { + if ( + interceptedRequest.url().endsWith(".png") || + interceptedRequest.url().endsWith(".jpg") + ) + interceptedRequest.abort(); + else interceptedRequest.continue(); + }); + + page.keyboard.type("Hello"); // Types instantly + page.keyboard.type("World", { delay: 100 }); // Types slower, like a user + + const watchDog = page.waitForFunction("window.innerWidth < 100"); + page.setViewport({ width: 50, height: 50 }); + await watchDog; + + let currentURL: string; + page + .waitForSelector("img", { visible: true }) + .then(() => console.log("First URL with image: " + currentURL)); + for (currentURL of [ + "https://example.com", + "https://google.com", + "https://bbc.com" + ]) { + await page.goto(currentURL); + } + + page.keyboard.type("Hello World!"); + page.keyboard.press("ArrowLeft"); + + page.keyboard.down("Shift"); + // tslint:disable-next-line prefer-for-of + for (let i = 0; i < " World".length; i++) { + page.keyboard.press("ArrowLeft"); + } + page.keyboard.up("Shift"); + page.keyboard.press("Backspace"); + page.keyboard.sendCharacter("嗨"); + + await page.tracing.start({ path: "trace.json" }); + await page.goto("https://www.google.com"); + await page.tracing.stop(); + + page.on("dialog", async dialog => { + console.log(dialog.message()); + await dialog.dismiss(); + browser.close(); + }); + + const inputElement = (await page.$("input[type=submit]"))!; + await inputElement.click(); +}); + +// Example with launch options +(async () => { + const browser = await puppeteer.launch({ + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + ], + handleSIGINT: true, + handleSIGHUP: true, + handleSIGTERM: true, + }); + const page = await browser.newPage(); + await page.goto("https://example.com"); + await page.screenshot({ path: "example.png" }); + + browser.close(); +})(); + +// Test v0.12 features +(async () => { + const browser = await puppeteer.launch({ + devtools: true, + env: { + JEST_TEST: true + } + }); + const page = await browser.newPage(); + const button = (await page.$("#myButton"))!; + const div = (await page.$("#myDiv"))!; + const input = (await page.$("#myInput"))!; + + if (!button) + throw new Error('Unable to select myButton'); + + if (!input) + throw new Error('Unable to select myInput'); + + await page.addStyleTag({ + url: "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" + }); + + console.log(page.url()); + + page.type("#myInput", "Hello World!"); + + page.on("console", (event: puppeteer.ConsoleMessage, ...args: any[]) => { + console.log(event.text, event.type); + for (let i = 0; i < args.length; ++i) console.log(`${i}: ${args[i]}`); + }); + + await button.focus(); + await button.press("Enter"); + await button.screenshot({ + type: "jpeg", + omitBackground: true, + clip: { + x: 0, + y: 0, + width: 200, + height: 100 + } + }); + console.log(button.toString()); + input.type("Hello World", { delay: 10 }); + + const buttonText = await (await button.getProperty('textContent')).jsonValue(); + + await page.deleteCookie(...await page.cookies()); + + const metrics = await page.metrics(); + console.log(metrics.Documents, metrics.Frames, metrics.JSEventListeners); + + const navResponse = await page.waitForNavigation({ + timeout: 1000 + }); + console.log(navResponse.ok, navResponse.status, navResponse.url, navResponse.headers); + + // evaluate example + const bodyHandle = (await page.$('body'))!; + const html = await page.evaluate((body : HTMLBodyElement) => body.innerHTML, bodyHandle); + await bodyHandle.dispose(); + + // getProperties example + const handle = await page.evaluateHandle(() => ({ window, document })); + const properties = await handle.getProperties(); + const windowHandle = properties.get('window'); + const documentHandle = properties.get('document'); + await handle.dispose(); + + // queryObjects example + // Create a Map object + await page.evaluate(() => (window as any).map = new Map()); + // Get a handle to the Map object prototype + const mapPrototype = await page.evaluateHandle(() => Map.prototype); + // Query all map instances into an array + const mapInstances = await page.queryObjects(mapPrototype); + // Count amount of map objects in heap + const count = await page.evaluate((maps: Map[]) => maps.length, mapInstances); + await mapInstances.dispose(); + await mapPrototype.dispose(); + + // evaluateHandle example + const aHandle = await page.evaluateHandle(() => document.body); + const resultHandle = await page.evaluateHandle((body: Element) => body.innerHTML, aHandle); + console.log(await resultHandle.jsonValue()); + await resultHandle.dispose(); + + browser.close(); +})(); + +// test $eval and $$eval +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto("https://example.com"); + await page.$eval('#someElement', (element, text: string) => { + return element.innerHTML = text; + }, 'hey'); + + let elementText = await page.$$eval('.someClassName', (elements) => { + console.log(elements.length); + console.log(elements.map(x => x)[0].textContent); + return elements[3].innerHTML; + }); + + browser.close(); +})(); diff --git a/utils/doclint/generate_types/test/tsconfig.json b/utils/doclint/generate_types/test/tsconfig.json new file mode 100644 index 0000000000000..a301a9e7877dd --- /dev/null +++ b/utils/doclint/generate_types/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "target": "es2015", + "noEmit": true + }, + "include": [ + "test.ts", + "../../../../index.d.ts" + ] +} \ No newline at end of file