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

Binary relocations #93

Merged
merged 3 commits into from
Dec 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
};