diff --git a/.eslintignore b/.eslintignore index bcf6efa53d824..e5047805b3bbb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ third_party/* utils/doclint/check_public_api/test/ +node6/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 16b3293c69986..deac790ff0145 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ *.pyc .vscode package-lock.json +/node6 diff --git a/.travis.yml b/.travis.yml index e45bf61d95bca..e3927edbf0765 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,10 @@ language: node_js -node_js: - - "7" dist: trusty addons: apt: packages: # This is required to run new chrome on old trusty - libnss3 -env: cache: yarn: true directories: @@ -16,6 +13,15 @@ install: - yarn install # puppeteer's install script downloads Chrome script: - - yarn run lint - - yarn run coverage - - yarn run test-phantom + - 'if [ "$NODE7" = "true" ]; then yarn run lint; fi' + - 'if [ "$NODE7" = "true" ]; then yarn run coverage; fi' + - 'if [ "$NODE7" = "true" ]; then yarn run test-phantom; fi' + - 'if [ "$NODE6" = "true" ]; then yarn run node6; fi' + - 'if [ "$NODE6" = "true" ]; then yarn run test-node6; fi' + - 'if [ "$NODE6" = "true" ]; then yarn run node6-sanity; fi' +jobs: + include: + - node_js: "7" + env: NODE7=true + - node_js: "6.4.0" + env: NODE6=true diff --git a/index.js b/index.js index f4093fdb0ba90..4175b69eedeb2 100644 --- a/index.js +++ b/index.js @@ -14,4 +14,12 @@ * limitations under the License. */ -module.exports = require('./lib/Puppeteer'); +// If node does not support async await, use the compiled version. +let folder = 'lib'; +try { + new Function('async function test(){await 1}'); +} catch (error) { + folder = 'node6'; +} + +module.exports = require(`./${folder}/Puppeteer`); diff --git a/package.json b/package.json index 36ae701f3767b..707c7e5305dc0 100644 --- a/package.json +++ b/package.json @@ -5,18 +5,22 @@ "main": "index.js", "repository": "github:GoogleChrome/puppeteer", "engines": { - "node": ">=7.10.0" + "node": ">=6.4.0" }, "scripts": { "unit": "jasmine test/test.js", "debug-unit": "DEBUG_TEST=true node --inspect-brk ./node_modules/.bin/jasmine test/test.js", "test-phantom": "python third_party/phantomjs/test/run-tests.py", "test-doclint": "jasmine utils/doclint/check_public_api/test/test.js && jasmine utils/doclint/preprocessor/test.js", - "test": "npm run lint --silent && npm run coverage && npm run test-phantom && npm run test-doclint", + "test": "npm run lint --silent && npm run coverage && npm run test-phantom && npm run test-doclint && npm run test-node6", "install": "node install.js", "lint": "([ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .) && npm run doc", "doc": "node utils/doclint/cli.js", - "coverage": "COVERAGE=true npm run unit" + "coverage": "COVERAGE=true npm run unit", + "node6": "node utils/node6-transform/index.js", + "test-node6": "jasmine utils/node6-transform/test/test.js", + "build": "npm run node6", + "node6-sanity": "jasmine test/sanity.js" }, "author": "The Chromium Authors", "license": "SEE LICENSE IN LICENSE", diff --git a/test/sanity.js b/test/sanity.js new file mode 100644 index 0000000000000..e851ccb9e807e --- /dev/null +++ b/test/sanity.js @@ -0,0 +1,31 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +describe('Puppeteer Sanity', function() { + it('should not be insane', function(done) { + const puppeteer = require('..'); + puppeteer.launch().then(browser => { + browser.newPage().then(page => { + page.goto('data:text/html,hello').then(() => { + page.evaluate(() => document.body.textContent).then(content => { + expect(content).toBe('hello'); + done(); + }); + }); + }); + }); + }); +}); diff --git a/utils/ChromiumDownloader.js b/utils/ChromiumDownloader.js index 27df99e9bbd67..611ecb56030d8 100644 --- a/utils/ChromiumDownloader.js +++ b/utils/ChromiumDownloader.js @@ -85,7 +85,7 @@ module.exports = { * @param {?function(number, number)} progressCallback * @return {!Promise} */ - downloadRevision: async function(platform, revision, progressCallback) { + downloadRevision: function(platform, revision, progressCallback) { let url = downloadURLs[platform]; console.assert(url, `Unsupported platform: ${platform}`); url = util.format(url, revision); @@ -93,15 +93,15 @@ module.exports = { const folderPath = getFolderPath(platform, revision); if (fs.existsSync(folderPath)) return; - try { - if (!fs.existsSync(DOWNLOADS_FOLDER)) - fs.mkdirSync(DOWNLOADS_FOLDER); - await downloadFile(url, zipPath, progressCallback); - await extractZip(zipPath, folderPath); - } finally { - if (fs.existsSync(zipPath)) - fs.unlinkSync(zipPath); - } + if (!fs.existsSync(DOWNLOADS_FOLDER)) + fs.mkdirSync(DOWNLOADS_FOLDER); + return downloadFile(url, zipPath, progressCallback) + .then(() => extractZip(zipPath, folderPath)) + .catch(err => err) + .then(() => { + if (fs.existsSync(zipPath)) + fs.unlinkSync(zipPath); + }); }, /** @@ -117,12 +117,13 @@ module.exports = { /** * @param {string} platform * @param {string} revision + * @return {!Promise} */ - removeRevision: async function(platform, revision) { + removeRevision: function(platform, revision) { console.assert(downloadURLs[platform], `Unsupported platform: ${platform}`); const folderPath = getFolderPath(platform, revision); console.assert(fs.existsSync(folderPath)); - await new Promise(fulfill => removeRecursive(folderPath, fulfill)); + return new Promise(fulfill => removeRecursive(folderPath, fulfill)); }, /** diff --git a/utils/doclint/check_public_api/ESTreeWalker.js b/utils/ESTreeWalker.js similarity index 100% rename from utils/doclint/check_public_api/ESTreeWalker.js rename to utils/ESTreeWalker.js diff --git a/utils/doclint/check_public_api/JSBuilder.js b/utils/doclint/check_public_api/JSBuilder.js index d38317a0596cf..843bdb944feda 100644 --- a/utils/doclint/check_public_api/JSBuilder.js +++ b/utils/doclint/check_public_api/JSBuilder.js @@ -15,7 +15,7 @@ */ const esprima = require('esprima'); -const ESTreeWalker = require('./ESTreeWalker'); +const ESTreeWalker = require('../../ESTreeWalker'); const Documentation = require('./Documentation'); class JSOutline { diff --git a/utils/node6-transform/TransformAsyncFunctions.js b/utils/node6-transform/TransformAsyncFunctions.js new file mode 100644 index 0000000000000..db306b6b33849 --- /dev/null +++ b/utils/node6-transform/TransformAsyncFunctions.js @@ -0,0 +1,114 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const esprima = require('esprima'); +const ESTreeWalker = require('../ESTreeWalker'); + +// This is converted from Babel's "transform-async-to-generator" +// https://babeljs.io/docs/plugins/transform-async-to-generator/ +const asyncToGenerator = fn => { + const gen = fn.call(this); + return new Promise((resolve, reject) => { + function step(key, arg) { + let info, value; + try { + info = gen[key](arg); + value = info.value; + } catch (error) { + reject(error); + return; + } + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then( + value => { + step('next', value); + }, + err => { + step('throw', err); + }); + } + } + return step('next'); + }); +}; + +/** + * @param {string} text + */ +function transformAsyncFunctions(text) { + const edits = []; + + const ast = esprima.parseScript(text, {range: true}); + const walker = new ESTreeWalker(node => { + if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression') + onFunction(node); + else if (node.type === 'AwaitExpression') + onAwait(node); + }); + walker.walk(ast); + + edits.sort((a, b) => b.from - a.from); + for (const {replacement, from, to} of edits) + text = text.substring(0, from) + replacement + text.substring(to); + + return text; + + /** + * @param {ESTree.Node} node + */ + function onFunction(node) { + if (!node.async) return; + + let range; + if (node.parent.type === 'MethodDefinition') + range = node.parent.range; + else + range = node.range; + const index = text.substring(range[0], range[1]).indexOf('async') + range[0]; + insertText(index, index + 'async'.length, '/* async */'); + + let before = `{return (${asyncToGenerator.toString()})(function*()`; + let after = `);}`; + if (node.body.type !== 'BlockStatement') { + before += `{ return `; + after = `; }` + after; + } + insertText(node.body.range[0], node.body.range[0], before); + insertText(node.body.range[1], node.body.range[1], after); + } + + /** + * @param {ESTree.Node} node + */ + function onAwait(node) { + const index = text.substring(node.range[0], node.range[1]).indexOf('await') + node.range[0]; + insertText(index, index + 'await'.length, '(yield'); + insertText(node.range[1], node.range[1], ')'); + } + + /** + * @param {number} from + * @param {number} to + * @param {string} replacement + */ + function insertText(from, to, replacement) { + edits.push({from, to, replacement}); + } +} + +module.exports = transformAsyncFunctions; \ No newline at end of file diff --git a/utils/node6-transform/index.js b/utils/node6-transform/index.js new file mode 100644 index 0000000000000..f7e864a7e7dcb --- /dev/null +++ b/utils/node6-transform/index.js @@ -0,0 +1,36 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const path = require('path'); +const removeRecursive = require('rimraf').sync; +const transformAsyncFunctions = require('./TransformAsyncFunctions'); + +const dirPath = path.join(__dirname, '..', '..', 'lib'); +const outPath = path.join(__dirname, '..', '..', 'node6'); +const fileNames = fs.readdirSync(dirPath); +const filePaths = fileNames.filter(fileName => fileName.endsWith('.js')); + +if (fs.existsSync(outPath)) + removeRecursive(outPath); +fs.mkdirSync(outPath); + +filePaths.forEach(filePath => { + const content = fs.readFileSync(path.join(dirPath, filePath), 'utf8'); + const output = transformAsyncFunctions(content); + fs.writeFileSync(path.resolve(outPath, filePath), output); +}); + diff --git a/utils/node6-transform/test/test.js b/utils/node6-transform/test/test.js new file mode 100644 index 0000000000000..0e8c326b5b744 --- /dev/null +++ b/utils/node6-transform/test/test.js @@ -0,0 +1,67 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const transformAsyncFunctions = require('../TransformAsyncFunctions'); + +describe('TransformAsyncFunctions', function() { + it('should convert a function expression', function(done) { + const input = `(async function(){ return 123 })()`; + const output = eval(transformAsyncFunctions(input)); + expect(output instanceof Promise).toBe(true); + output.then(result => expect(result).toBe(123)).then(done); + }); + it('should convert an arrow function', function(done) { + const input = `(async () => 123)()`; + const output = eval(transformAsyncFunctions(input)); + expect(output instanceof Promise).toBe(true); + output.then(result => expect(result).toBe(123)).then(done); + }); + it('should convert an arrow function with curly braces', function(done) { + const input = `(async () => { return 123 })()`; + const output = eval(transformAsyncFunctions(input)); + expect(output instanceof Promise).toBe(true); + output.then(result => expect(result).toBe(123)).then(done); + }); + it('should convert a function declaration', function(done) { + const input = `async function f(){ return 123; } f();`; + const output = eval(transformAsyncFunctions(input)); + expect(output instanceof Promise).toBe(true); + output.then(result => expect(result).toBe(123)).then(done); + }); + it('should convert await', function(done) { + const input = `async function f(){ return 23 + await Promise.resolve(100); } f();`; + const output = eval(transformAsyncFunctions(input)); + expect(output instanceof Promise).toBe(true); + output.then(result => expect(result).toBe(123)).then(done); + }); + it('should convert method', function(done) { + const input = `class X{async f() { return 123 }} (new X()).f();`; + const output = eval(transformAsyncFunctions(input)); + expect(output instanceof Promise).toBe(true); + output.then(result => expect(result).toBe(123)).then(done); + }); + it('should pass arguments', function(done) { + const input = `(async function(a, b){ return await a + await b })(Promise.resolve(100), 23)`; + const output = eval(transformAsyncFunctions(input)); + expect(output instanceof Promise).toBe(true); + output.then(result => expect(result).toBe(123)).then(done); + }); + it('should still work across eval', function(done) { + const input = `var str = (async function(){ return 123; }).toString(); eval('(' + str + ')')();`; + const output = eval(transformAsyncFunctions(input)); + expect(output instanceof Promise).toBe(true); + output.then(result => expect(result).toBe(123)).then(done); + }); +}); \ No newline at end of file