Skip to content

Commit

Permalink
Binary relocations (#93)
Browse files Browse the repository at this point in the history
* node-gyp-inlining

* handle bindings cases

* test fixes
  • Loading branch information
guybedford authored and rauchg committed Dec 3, 2018
1 parent 290b4a0 commit da037bd
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 24 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"aws-sdk": "^2.356.0",
"axios": "^0.18.0",
"azure-storage": "^2.10.2",
"bindings": "^1.3.1",
"bytes": "^3.0.0",
"codecov": "^3.1.0",
"copy": "^0.3.2",
Expand All @@ -46,13 +47,15 @@
"jest": "^23.6.0",
"jimp": "^0.5.6",
"koa": "^2.6.2",
"leveldown": "^4.0.1",
"magic-string": "^0.25.1",
"mailgun": "^0.5.0",
"mariadb": "^2.0.1-beta",
"memcached": "^2.2.2",
"mkdirp": "^0.5.1",
"mongoose": "^5.3.12",
"mysql": "^2.16.0",
"node-pre-gyp": "^0.12.0",
"passport": "^0.4.0",
"passport-google-oauth": "^1.0.0",
"path-platform": "^0.11.15",
Expand Down
161 changes: 137 additions & 24 deletions src/asset-relocator.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,32 @@ const MagicString = require('magic-string');
const { attachScopes } = require('rollup-pluginutils');
const evaluate = require('static-eval');
const acorn = require('acorn');
const bindings = require('bindings');

// Very basic first-pass fs.readFileSync inlining
function isReference(node, parent) {
// binary support for inlining logic from - node-pre-gyp/lib/pre-binding.js
function isPregypId (id) {
return id === 'node-pre-gyp' ||
id === 'node-pre-gyp/lib/pre-binding' ||
id === 'node-pre-gyp/lib/pre-binding.js';
}
const versioning = require('node-pre-gyp/lib/util/versioning.js');
const napi = require('node-pre-gyp/lib/util/napi.js');
const pregyp = {
find (package_json_path, opts) {
const package_json = JSON.parse(fs.readFileSync(package_json_path).toString());
versioning.validate_config(package_json, opts);
var napi_build_version;
if (napi.get_napi_build_versions (package_json, opts)) {
napi_build_version = napi.get_best_napi_build_version(package_json, opts);
}
opts = opts || {};
if (!opts.module_root) opts.module_root = path.dirname(package_json_path);
var meta = versioning.evaluate(package_json,opts,napi_build_version);
return meta.module;
}
};

function isExpressionReference(node, parent) {
if (parent.type === 'MemberExpression') return parent.computed || node === parent.object;

// disregard the `bar` in { bar: foo }
Expand All @@ -17,16 +40,19 @@ function isReference(node, parent) {
if (parent.type === 'MethodDefinition') return false;

// disregard the `bar` in `export { foo as bar }`
if (parent.type === 'ExportSpecifier' && node !== parent.local) return false;
if (parent.type === 'ExportSpecifier' && node !== parent.local) return false;

// disregard the `bar` in var bar = asdf
if (parent.type === 'VariableDeclarator' && node.id === node) return false;

return true;
}

const assetRegEx = /_\_dirname|_\_filename|require\.main/;
const relocateRegEx = /_\_dirname|_\_filename|require\.main|node-pre-gyp|bindings/;
module.exports = function (code) {
const id = this.resourcePath;

if (id.endsWith('.json') || !code.match(assetRegEx))
if (id.endsWith('.json') || !code.match(relocateRegEx))
return this.callback(null, code);

const assetNames = Object.create(null);
Expand Down Expand Up @@ -64,6 +90,8 @@ module.exports = function (code) {
let scope = attachScopes(ast, 'scope');

let pathId, pathImportIds = {};
let pregypId, bindingsId;

const shadowDepths = Object.create(null);
shadowDepths.__filename = 0;
shadowDepths.__dirname = 0;
Expand All @@ -90,36 +118,84 @@ module.exports = function (code) {
}
}
}
// import binary from 'node-pre-gyp';
// import * as binary from 'node-pre-gyp';
// import { find } from 'node-pre-gyp' not yet implemented
else if (isPregypId(source)) {
for (const impt of decl.specifiers) {
if (impt.type === 'ImportNamespaceSpecifier' || impt.type === 'ImportDefaultSpecifier') {
pregypId = impt.local.name;
shadowDepths[pregypId] = 0;
}
}
}
// import bindings from 'bindings';
else if (source === 'bindings') {
for (const impt of decl.specifiers) {
if (impt.type === 'ImportDefaultSpecifier') {
bindingsId = impt.local.name;
shadowDepths[bindingsId] = 0;
}
}
}
}
}
}

let transformed = false;

function computeStaticValue (expr, id) {
let staticBindingsInstance = false;
function createBindings (id) {
return (opts = {}) => {
if (typeof opts === 'string')
opts = { bindings: opts };
if (!opts.path) {
opts.path = true;
staticBindingsInstance = true;
}
opts.module_root = path.dirname(id);
return bindings(opts);
};
}
function computeStaticValue (expr, bindingsReq) {
staticBindingsInstance = false;
// function expression analysis disabled due to static-eval locals bug
if (expr.type === 'FunctionExpression')
return;
const vars = {};
if (shadowDepths.__filename === 0)
vars.__dirname = path.resolve(id, '..');
if (shadowDepths.__dirname === 0)
vars.__dirname = path.resolve(id, '..');
if (shadowDepths.__filename === 0)
vars.__filename = id;
if (pathId) {
if (shadowDepths[pathId] === 0)
vars[pathId] = path;
}
if (pregypId) {
if (shadowDepths[pregypId] === 0)
vars[pregypId] = pregyp;
}
if (bindingsId) {
if (shadowDepths[bindingsId] === 0)
vars[bindingsId] = createBindings(id);
}
for (const pathFn of Object.keys(pathImportIds)) {
if (shadowDepths[pathFn] === 0)
vars[pathFn] = path[pathImportIds[pathFn]];
}
if (bindingsReq && shadowDepths.require === 0)
vars.require = function (reqId) {
if (reqId === 'bindings')
return createBindings(id);
};

// evaluate returns undefined for non-statically-analyzable
return evaluate(expr, vars);
}

// statically determinable leaves are tracked, and inlined when the
// greatest parent statically known leaf computation corresponds to an asset path
let staticChildNode, staticChildValue;
let staticChildNode, staticChildValue, staticChildValueBindingsInstance;

// detect require('asdf');
function isStaticRequire (node) {
Expand All @@ -146,20 +222,35 @@ module.exports = function (code) {
return this.skip();

// detect asset leaf expression triggers (if not already)
// __dirname and __filename only currently
// __dirname, __filename, binary only currently as well as require('bindings')(...)
// Can add require.resolve, import.meta.url, even path-like environment variables
if (node.type === 'Identifier' && isReference(node, parent)) {
if ((node.name === '__dirname' || node.name === '__filename') && !shadowDepths[node.name]) {
curStaticValue = computeStaticValue(node, id);
if (node.type === 'Identifier' && isExpressionReference(node, parent)) {
if ((node.name === '__dirname' ||
node.name === '__filename' ||
node.name === pregypId || node.name === bindingsId) && !shadowDepths[node.name]) {
staticChildValue = computeStaticValue(node, false);
// if it computes, then we start backtracking
if (curStaticValue) {
if (staticChildValue) {
staticChildNode = node;
staticChildValueBindingsInstance = staticBindingsInstance;
return this.skip();
}
}
}
// require('bindings')('asdf')
else if (node.type === 'CallExpression' &&
!isESM && isStaticRequire(node.callee) &&
node.callee.arguments[0].value === 'bindings') {
staticChildValue = computeStaticValue(node, true);
if (staticChildValue) {
staticChildNode = node;
staticChildValueBindingsInstance = staticBindingsInstance;
return this.skip();
}
}

else if (node.type === 'MemberExpression' &&
// require.main -> __non_webpack_require__.main
else if (!isESM && node.type === 'MemberExpression' &&
node.object.type === 'Identifier' &&
node.object.name === 'require' &&
!shadowDepths.require &&
Expand All @@ -178,10 +269,24 @@ module.exports = function (code) {
for (const decl of node.declarations) {
// var path = require('path')
if (decl.id.type === 'Identifier' &&
!isESM && isStaticRequire(decl.init) &&
decl.init.arguments[0].value === 'path') {
pathId = decl.id.name;
shadowDepths[pathId] = 0;
!isESM && isStaticRequire(decl.init)) {
if (decl.init.arguments[0].value === 'path') {
pathId = decl.id.name;
shadowDepths[pathId] = 0;
return this.skip();
}
// var binary = require('node-pre-gyp')
else if (isPregypId(decl.init.arguments[0].value)) {
pregypId = decl.id.name;
shadowDepths[pregypId] = 0;
return this.skip();
}
// var bindings = require('bindings')
else if (decl.init.arguments[0].value === 'bindings') {
bindingsId = decl.id.name;
shadowDepths[bindingsId] = 0;
return this.skip();
}
}
// var { join } = path | require('path');
else if (decl.id.type === 'ObjectPattern' && decl.init &&
Expand All @@ -194,19 +299,21 @@ module.exports = function (code) {
continue;
pathImportIds[prop.value.name] = prop.key.name;
shadowDepths[prop.key.name] = 0;
return this.skip();
}
}
// var join = path.join | require('path').join;
// var join = path.join
else if (decl.id.type === 'Identifier' &&
decl.init &&
decl.init.type === 'MemberExpression' &&
decl.init.object.type === 'Identifier' &&
decl.init.object.name === pathId &&
shadowDepths[pathId] === 0 &&
shadowDepths[decl.init.object.name] === 0 &&
decl.init.computed === false &&
decl.init.property.type === 'Identifier') {
pathImportIds[decl.init.property.name] = decl.id.name;
shadowDepths[decl.id.name] = 0;
return this.skip();
}
}
}
Expand All @@ -223,10 +330,11 @@ module.exports = function (code) {
// computing a static expression outward
// -> compute and backtrack
if (staticChildNode) {
const curStaticValue = computeStaticValue(node, id);
const curStaticValue = computeStaticValue(node, false);
if (curStaticValue) {
staticChildNode = node;
staticChildValue = curStaticValue;
staticChildNode = node;
staticChildValueBindingsInstance = staticBindingsInstance;
return;
}
// no static value -> see if we should emit the asset if it exists
Expand All @@ -239,7 +347,12 @@ module.exports = function (code) {
catch (e) {}
}
if (isFile) {
const replacement = emitAsset(path.resolve(staticChildValue));
let replacement = emitAsset(path.resolve(staticChildValue));
// require('bindings')(...)
// -> require(require('bindings')(...))
if (staticChildValueBindingsInstance) {
replacement = '__non_webpack_require__(' + replacement + ')';
}
if (replacement) {
transformed = true;
magicString.overwrite(staticChildNode.start, staticChildNode.end, replacement);
Expand Down
2 changes: 2 additions & 0 deletions test/integration/grpc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const grpc = require("grpc");
module.exports = () => {};
4 changes: 4 additions & 0 deletions test/integration/leveldown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const leveldown = require('leveldown');
module.exports = () => {
db = leveldown('tmp/db');
};

0 comments on commit da037bd

Please sign in to comment.