Skip to content

Instantly share code, notes, and snippets.

@kumavis
Last active December 18, 2015 01:29
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kumavis/5703920 to your computer and use it in GitHub Desktop.
Save kumavis/5703920 to your computer and use it in GitHub Desktop.
An attempt at browserifying npm

This project:

  1. is a case-study for browserifying anything
  2. part of a dream to create virtual Node.js development environment
  3. likely a major time-sink!

starting out:

mkdir browser-npm && cd browser-npm
npm install npm
npm init

make an index.js

var npm = require('npm')
debugger

and an index.html

<body> b r o w s e r - n p m <script src="bundle.js"></script></body>

ok simple enough lets see what happens next

browserify index.js >| bundle.js

didn't think it would be that easy did you? I get:

Error: module "os" not found from "/Users/kumavis/Dropbox/Development/Node/browser-npm/node_modules/npm/lib/utils/error-handler.js"

ok, what is this os module? in Node I can see that it is provided out of the box, I would consider it a core-module:

> require('os')
{ hostname: [Function],
  loadavg: [Function],
  uptime: [Function],
  freemem: [Function],
  totalmem: [Function],
  cpus: [Function],
  type: [Function],
  release: [Function],
  networkInterfaces: [Function],
  arch: [Function],
  platform: [Function],
  tmpDir: [Function],
  getNetworkInterfaces: [Function: deprecated],
  EOL: '\n' }

if its a core module, why didn't browser-resolve supply it?

ls /usr/local/share/npm/lib/node_modules/browserify/node_modules/browser-resolve/builtin
assert.js         events.js         net.js            querystring.js    sys.js            tty.js
child_process.js  fs.js             path.js           stream.js         timers.js         url.js
dgram.js          https.js          process.js        string_decoder.js tls.js            util.js

Oh well we can make our own as needed. skimming the results of npm search browser os I found os-browserify. It will do! I am going to add it browserify's browser-resolve's deps,

  "dependencies": {
    "resolve": "0.3.1",
    "console-browserify": "0.1.6",
    "vm-browserify": "0.0.1",
    "crypto-browserify": "0.2.1",
    "http-browserify": "0.1.11",
    "buffer-browserify": "0.0.5",
    "zlib-browserify": "0.0.1",
    "os-browserify": "0.1.0"
  },

navigate to browserify/node_modules/browser-resolve/ and npm install.

Next add core['os'] = require.resolve('os-browserify'); to browserify/node_modules/browser-resolve/index.js

oh! I almost forgot. We need to tell browser-resolve to use its own list of core modules, instead of asking regular resolve. Replace this line with this

if (core.hasOwnProperty(id)) {

returning to our browser-npm directory, lets try bundling again...

/usr/local/share/npm/lib/node_modules/browserify/node_modules/insert-module-globals/node_modules/lexical-scope/node_modules/astw/node_modules/esprima/esprima.js:3859
            throw e;
                  ^
Error: Line 9: Illegal return statement
    at throwError (/usr/local/share/npm/lib/node_modules/browserify/node_modules/insert-module-globals/node_modules/lexical-scope/node_modules/astw/node_modules/esprima/esprima.js:1161:21)
    at throwErrorTolerant (/usr/local/share/npm/lib/node_modules/browserify/node_modules/insert-module-globals/node_modules/lexical-scope/node_modules/astw/node_modules/esprima/esprima.js:1172:24)
    at parseReturnStatement (/usr/local/share/npm/lib/node_modules/browserify/node_modules/insert-module-globals/node_modules/lexical-scope/node_modules/astw/node_modules/esprima/esprima.js:2476:13)
    at parseStatement (/usr/local/share/npm/lib/node_modules/browserify/node_modules/insert-module-globals/node_modules/lexical-scope/node_modules/astw/node_modules/esprima/esprima.js:2752:24)
    at parseIfStatement (/usr/local/share/npm/lib/node_modules/browserify/node_modules/insert-module-globals/node_modules/lexical-scope/node_modules/astw/node_modules/esprima/esprima.js:2191:22)
    at parseStatement (/usr/local/share/npm/lib/node_modules/browserify/node_modules/insert-module-globals/node_modules/lexical-scope/node_modules/astw/node_modules/esprima/esprima.js:2750:24)
    at parseSourceElement (/usr/local/share/npm/lib/node_modules/browserify/node_modules/insert-module-globals/node_modules/lexical-scope/node_modules/astw/node_modules/esprima/esprima.js:3044:24)
    at parseSourceElements (/usr/local/share/npm/lib/node_modules/browserify/node_modules/insert-module-globals/node_modules/lexical-scope/node_modules/astw/node_modules/esprima/esprima.js:3082:29)
    at parseProgram (/usr/local/share/npm/lib/node_modules/browserify/node_modules/insert-module-globals/node_modules/lexical-scope/node_modules/astw/node_modules/esprima/esprima.js:3096:19)
    at parse (/usr/local/share/npm/lib/node_modules/browserify/node_modules/insert-module-globals/node_modules/lexical-scope/node_modules/astw/node_modules/esprima/esprima.js:3843:23)

ouch! right in the esprima! ok lets see whats going on here. Well apparently esprima is an "ECMAScript parsing infrastructure". Clearly browserify is using this to seek require calls and bundle dependencies. But something has gone awry - search reveals that others are getting this unhelpful error message, the posted issue unresolved at the time of this writing.

In order to find out what code is being parsed when the error thrown, I add process.stderr.write(source); right above the throwErrorTolerant({}, Messages.IllegalReturn); line, browserify index.js >| bundle.js again, take the console output and use it to do a project-wide search in browser-npm/. Don't forget to undo changes to esprima.js!

It is from browser-npm/node_modules/npm/node_modules/child-process-close/index.js. The above github issue says that you can avoid this Illegal return statement error by wrapping your code in a closure, like so:

(function(){
  < original index.js content >
})();

This is a good time to stretch, get a glass of water. Ready?

browserify index.js >| bundle.js
Error: ENOENT, open 'constants'

Not a lot of details, but browserify is trying to open a file 'constants' and failing. In node, require('constants') resolves to a Hash of integers. This looks relevant, but I'm just going to figure this one is a core module and save [this output}(https://gist.github.com/kumavis/5704326) from the require('constants') to browserify/node_modules/browser-resolve/builtin/constants.js. Since we changed browser-resolve/index.js to rely on its own understanding of core, we should be good. Note: these numbers may be machine specific. You may want to use what is output by your machine instead. I don't really know. We'll figure it out later if we need to.

Continuing onward!

browserify index.js >| bundle.js

/Users/kumavis/Dropbox/Development/Node/browser-npm/node_modules/npm/bin/npm-cli.js:1
(function(process){#!/usr/bin/env node
                   ^
ParseError: Unexpected token ILLEGAL

Hmmm, there's a bit of preprocessor directive or something at the head of npm-cli.js. I'm not exactly sure what that is or what its for, but I'll just comment it out for now.

browserify index.js >| bundle.js
Error: ENOENT, open 'dns'

Can't find the file 'dns'. I'm going to guess its another core module. Node? Node agrees, Lets npm search browser dns. Hmmm nothing. We'll need to roll our own. I'm just going to do a copy-paste-hack job here. It will likely break if you try to actualy use it. Save this as browserify/node_modules/browser-resolve/builtin/dns.js

What's next?

[kumavis:...evelopment/Node/browser-npm]$ browserify index.js >| bundle.js
[kumavis:...evelopment/Node/browser-npm]$

woah. cool. But I wouldn't get too excited, this journey has just begun. Opening up index.html I can use the debugger to see what the first problem is.

Uncaught TypeError: Cannot read property '_ansicursor' of undefined     bundle.js:12175

this error originates from the middle line

var ansi = require('ansi')
log.cursor = ansi(process.stderr)
log.stream = process.stderr

which in turn comes from a half-hearted implementation of process

> process.stderr
undefined

The docs say process.stderr should be a stream

I assumed that this was coming from browserify/node_modules/browser-resolve/builtin/process.js but apparently it actually comes from browserify/node_modules/insert-module-globals/node_modules/process/browser.js. Add the following:

Stream = require('stream');

and

process.stderr = new Stream();
//  copy pasta from `process.stderr.write.toString()`
process.stderr.write = function (data, arg1, arg2) {
  var encoding, cb;

  // parse arguments
  if (arg1) {
    if (typeof arg1 === 'string') {
      encoding = arg1;
      cb = arg2;
    } else if (typeof arg1 === 'function') {
      cb = arg1;
    } else {
      throw new Error('bad arg');
    }
  }

  if (typeof data === 'string') {
    encoding = (encoding || 'utf8').toLowerCase();
    switch (encoding) {
      case 'utf8':
      case 'utf-8':
      case 'ascii':
      case 'ucs2':
      case 'ucs-2':
      case 'utf16le':
      case 'utf-16le':
        // This encoding can be handled in the binding layer.
        break;

      default:
        data = new Buffer(data, encoding);
    }
  } else if (!Buffer.isBuffer(data)) {
    throw new TypeError('First argument must be a buffer or a string.');
  }

  // If we are still connecting, then buffer this for later.
  if (this._connecting) {
    this._connectQueueSize += data.length;
    if (this._connectQueue) {
      this._connectQueue.push([data, encoding, cb]);
    } else {
      this._connectQueue = [[data, encoding, cb]];
    }
    return false;
  }

  return this._write(data, encoding, cb);
}

If you browserify and run you'll find that it isn't able to get the Stream package for you. browserify/node_modules/insert-module-globals/index.js needs you to manually set deps. Adjust line 59 as below:

    deps: {"stream": path.join(__dirname+"../../browser-resolve", 'builtin/stream.js')}

Running again, we hit our debugger statement in our own index.js! Home sweet home. We're clearly making progress. Of course node is very async, so now we have to catch up with all the work various modules have set aside to be done, and for us that means more debugging and patching holes. Let's remove the debugger statement before we carry on.

next we're on browser-npm/node_modules/npm/node_modules/graceful-fs/graceful-fs.js

// lchmod, broken prior to 0.6.2
// back-port the fix here.
if (constants.hasOwnProperty('O_SYMLINK') &&
    process.version.match(/^v0\.6\.[0-2]|^v0\.5\./)) {

Its trying to compensate for old node versions and process.version is undefined. We'll just use Node's output. Add this to browserify/node_modules/insert-module-globals/node_modules/process/browser.js.

process.version = 'v0.8.21';

Next we have something trying to readFileSync /node_modules/npm/node_modules/request/node_modules/mime/types/mime.types using my virtual file system! (1) My file system isn't quite ready for use (2) Its empty! There aren't any files stored on it. Sounds like we'll need to seed our file system with some basic node environment.

See you next time!

@kumavis
Copy link
Author

kumavis commented Jun 4, 2013

Things to add:

  • "Dont try this at home kids" warning. (How to use non-global browserify).
  • Setting up watchify once the bundling is working

@maxleiko
Copy link

I there, is this still a "work in progress" ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment