Node 6 support (#484)

This patch:
- introduces a transpiler which substitutes async/await logic with
generators.
- starts using the transpiler to generate a node6-compatible version of puppeteer
- introduces a runtime-check to decide which version of code to use

Fixes #316.
This commit is contained in:
JoelEinbinder 2017-08-24 12:20:05 -07:00 committed by Andrey Lushnikov
parent 46115f9182
commit 9212863b92
12 changed files with 292 additions and 23 deletions

View File

@ -1,2 +1,3 @@
third_party/*
utils/doclint/check_public_api/test/
node6/*

1
.gitignore vendored
View File

@ -7,3 +7,4 @@
*.pyc
.vscode
package-lock.json
/node6

View File

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

View File

@ -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`);

View File

@ -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",

31
test/sanity.js Normal file
View File

@ -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();
});
});
});
});
});
});

View File

@ -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 {
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));
},
/**

View File

@ -15,7 +15,7 @@
*/
const esprima = require('esprima');
const ESTreeWalker = require('./ESTreeWalker');
const ESTreeWalker = require('../../ESTreeWalker');
const Documentation = require('./Documentation');
class JSOutline {

View File

@ -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;

View File

@ -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);
});

View File

@ -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);
});
});