Skip to content

Commit

Permalink
support browserify external analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford committed Dec 4, 2018
1 parent cbfeb45 commit 05e5317
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 6 deletions.
93 changes: 87 additions & 6 deletions src/loaders/relocate-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,42 @@ function isExpressionReference(node, parent) {
return true;
}

// Wrapper detections for require extraction
// detects:
// Wrapper detections for require extraction handles:
//
// When.js-style AMD wrapper:
// (function (define) { 'use strict' define(function (require) { ... }) })
// (typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); })
// // ...
function unwrapIfAMD (ast, scope, magicString, len) {
// ->
// (function (define) { 'use strict' define(function () { ... }) })
// (typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); })
//
// Browserify-style wrapper
// (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.bugsnag = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({
// 1:[function(require,module,exports){
// ...code...
// },{"external":undefined}], 2: ...
// },{},[24])(24)
// });
// ->
// (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.bugsnag = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({
// 1:[function(require,module,exports){
// ...code...
// },{"external":undefined}], 2: ...
// },{
// "external": { exports: require('external') }
// },[24])(24)
// });
//

function handleWrappers (ast, scope, magicString, len) {
let transformed = false;
if (ast.body.length === 1 &&
ast.body[0].type === 'ExpressionStatement' &&
ast.body[0].expression.type === 'CallExpression' &&
ast.body[0].expression.callee.type === 'FunctionExpression' &&
ast.body[0].expression.arguments.length === 1) {

// When.js wrapper
const arg = ast.body[0].expression.arguments[0];
if (arg.type === 'ConditionalExpression' &&
arg.test.type === 'LogicalExpression' &&
Expand Down Expand Up @@ -99,7 +121,6 @@ function unwrapIfAMD (ast, scope, magicString, len) {
arg.alternate.body.body[0].expression.right.arguments[0].type === 'Identifier' &&
arg.alternate.body.body[0].expression.right.arguments[0].name === 'require') {
let iifeBody = ast.body[0].expression.callee.body.body;

if (iifeBody[0].type === 'ExpressionStatement' &&
iifeBody[0].expression.type === 'Literal' &&
iifeBody[0].expression.value === 'use strict') {
Expand All @@ -120,6 +141,66 @@ function unwrapIfAMD (ast, scope, magicString, len) {
transformed = true;
}
}
// browserify wrapper
else if (arg.type === 'FunctionExpression' &&
arg.params.length === 0 &&
arg.body.body.length === 2 &&
arg.body.body[0].type === 'VariableDeclaration' &&
arg.body.body[0].declarations.length === 3 &&
arg.body.body[0].declarations.every(decl => decl.init === null && decl.id.type === 'Identifier') &&
arg.body.body[1].type === 'ReturnStatement' &&
arg.body.body[1].argument.type === 'CallExpression' &&
arg.body.body[1].argument.callee.type === 'CallExpression' &&
arg.body.body[1].argument.arguments.length &&
arg.body.body[1].argument.arguments.every(arg => arg.type === 'Literal' && typeof arg.value === 'number') &&
arg.body.body[1].argument.callee.callee.type === 'CallExpression' &&
arg.body.body[1].argument.callee.callee.callee.type === 'FunctionExpression' &&
arg.body.body[1].argument.callee.callee.arguments.length === 0 &&
// (dont go deeper into browserify loader internals than this)
arg.body.body[1].argument.callee.arguments.length === 3 &&
arg.body.body[1].argument.callee.arguments[0].type === 'ObjectExpression' &&
arg.body.body[1].argument.callee.arguments[1].type === 'ObjectExpression' &&
arg.body.body[1].argument.callee.arguments[2].type === 'ArrayExpression') {
const modules = arg.body.body[1].argument.callee.arguments[0].properties;

// verify modules is the expected data structure
// in the process, extract external requires
const externals = {};
if (modules.every(m => {
if (m.type !== 'Property' ||
m.computed !== false ||
m.key.type !== 'Literal' ||
typeof m.key.value !== 'number' ||
m.value.type !== 'ArrayExpression' ||
m.value.elements.length !== 2 ||
m.value.elements[0].type !== 'FunctionExpression' ||
m.value.elements[1].type !== 'ObjectExpression')
return false;

// detect externals from undefined moduleMap values
const moduleMap = m.value.elements[1].properties;
for (const prop of moduleMap) {
if (prop.type !== 'Property' ||
(prop.value.type !== 'Identifier' && prop.value.type !== 'Literal') ||
prop.key.type !== 'Literal' ||
typeof prop.key.value !== 'string' ||
prop.computed)
return false;
if (prop.value.type === 'Identifier' && prop.value.name === 'undefined')
externals[prop.key.value] = true;
}
return true;
})) {
// if we have externals, inline them into the browserify cache for webpack to pick up
const externalIds = Object.keys(externals);
if (externalIds.length) {
const cache = arg.body.body[1].argument.callee.arguments[1];
const renderedExternals = externalIds.map(ext => `"${ext}": { exports: require("${ext}") }`).join(',\n ');
magicString.appendRight(cache.end - 1, renderedExternals);
transformed = true;
}
}
}
}
return { ast, scope, transformed };
}
Expand Down Expand Up @@ -320,7 +401,7 @@ module.exports = function (code) {
node.arguments[0].type === 'Literal';
}

({ ast, scope, transformed } = unwrapIfAMD(ast, scope, magicString, code.length));
({ ast, scope, transformed } = handleWrappers(ast, scope, magicString, code.length));

walk(ast, {
enter (node, parent) {
Expand Down
1 change: 1 addition & 0 deletions test/unit/browserify/dep1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'dep1';
1 change: 1 addition & 0 deletions test/unit/browserify/dep2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'dep2';
4 changes: 4 additions & 0 deletions test/unit/browserify/input.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

111 changes: 111 additions & 0 deletions test/unit/browserify/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
module.exports =
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

var require;var require;(function(f){if(true){module.exports=f()}else { var g; }})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return require(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
module.exports = [require("./dep1"), require("./dep2")];
},{"./dep1": undefined, "./dep2": undefined}]},{"./dep1": { exports: __webpack_require__(1) },
"./dep2": { exports: __webpack_require__(2) }},[1])(1)
});

/***/ }),
/* 1 */
/***/ (function(module, exports) {

module.exports = 'dep1';

/***/ }),
/* 2 */
/***/ (function(module, exports) {

module.exports = 'dep2';

/***/ })
/******/ ]);

0 comments on commit 05e5317

Please sign in to comment.