npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

metafunction

v0.0.10

Published

provides capabilities for function introspection and mocking

Downloads

8

Readme

metafunction

Build Status

This is a testing library that decorates JavaScript's Function.prototype in to provide capabilities for reflection and mocking to address the following:

  • how can we inspect items defined in closures?
  • how can we override (or mock) them?

Following an exchange with Phil Walton (@philwalton) (see [Mocking - not testing - private functions] (https://gist.github.com/dfkaye/5987716)), I developed this idea while working on vm-shim from which this repo borrows some internal methods.

The main trick uses Function.prototype.toString() to get a function descriptor (arguments, name, returns, source), and provide some methods for overwriting symbols and references within the function source and executing the new source in a new context.

install

npm install metafunction

git clone https://github.com/dfkaye/metafunction.git

use

  • in node.js:

    require('metafunction')

  • in browser, metafunction is defined on the global scope

    <script src="../metafunction.js"></script>

Those will give you...

Function.prototype.meta(alias?)

Any function or method now has a .meta method:

var meta = someFunction.meta();

The alias argument is optional but really should be used any time you want to run a new invocation (much like naming your test iterations).

The object returned by this call is actually a function. The returned meta function contains a descriptor object containing the parts of the original function definition (arguments, name, returns, source).

It also contains methods for the aliasing, mocking/overriding and inspection of closures and closure references, described below.

complete example spec

Best is first to view a fairly complete example:

describe('README example', function () {

  // fixture
  var fn = (function () {
    var closure = true;
    var inner = function fn(exampleArg) {
      // I'm a closure inside by an IIFE
      return closure;
    };
    return inner;
  }());
  
  var meta;
  
  beforeEach(function () {
    meta = fn.meta();
  });
  
  it ('descriptor data', function () {
  
    var descriptor = meta.descriptor;
    
    expect(descriptor.source).toBe(fn.toString());
    expect(descriptor.arguments[0]).toBe('exampleArg');
    expect(descriptor.name).toBe('fn');
    expect(descriptor.source).toContain('// I\'m a closure inside by an' +
                                        ' IIFE');
    expect(descriptor.source).toContain('return closure');
    expect(descriptor.returns.length).toBe(1);
    expect(descriptor.returns[0]).toBe('closure');
  });
  
  it ('invoke with context', function () {
    meta.invoke(function () {
    
      // should pass, calling alias()
      expect(fn()).toBe('mocked');
      
      // should see context object and its properties
      expect(context).toBeDefined();
      
      // should see context object and its properties
      expect(context.closure).toBe('mocked');
      
      // should see context object and its properties
      expect(context.expect).toBe(expect);
      
    }, { expect: expect, closure: 'mocked' });
  });
  
  it ('alias, inject, invoke', function () {
  
    meta('alias'); // alias is used by invocation
    meta.inject('closure', 'mockClosure');
    meta.invoke(function () {
    
      // should pass, calling alias()
      expect(alias()).toBe('mocked');
      
      // should see context object and its properties
      expect(context.mockClosure).toBe('mocked');
      
    }, { expect: expect, mockClosure: 'mocked' });
  });
  
  it('chained API example', function () {
  
    meta('chain').inject('closure', 'mockClosure').invoke(function () {
    
      expect(chain()).toBe('mocked'); // should pass, calling chain()
      
    }, { expect: expect, mockClosure: 'mocked' });
  });
  
  it('lisped API example with 2-arg', function () {
  
    (meta('lisp')
    ('closure', 'mockedClosure')
    (function () {

      expect(lisp()).toBe('mocked'); // should pass, calling lisp()
      
     }, { 
      expect: expect, mockedClosure: 'mocked' 
     }));
  });

  it('alternate lisped API example', function () {
    (meta('lisp')
    ({ expect: expect, closure: 'mocked' })
    (function () {
      expect(lisp()).toBe('mocked'); // should pass, calling lisp()
    }));
  });
  
  it('really terse alternate lisped API example', function () {
     // surround function object entirely
    (meta) 
     // pass alias arg to it
    ('lisp')
     // set context
    ({ expect: expect, closure: 'mocked' })
     // set invoke function which should pass, calling lisp()
    (function () {
      expect(lisp()).toBe('mocked'); 
    });
     // final semi-colon, only one parenthesis after final call
  });
  
  it('nested invocation example', function () {

    var ctx = { expect: expect, meta: meta, closure: 'mocked' };    
    
    meta('main').invoke(function() {
      
      expect(main()).toBe('mocked');
      
      // context argument to invoke() is visible in function scope
      context.closure = 'nested';
      
      meta('nestedMain').invoke(function() {
        expect(nestedMain()).toBe('nested');
      }, context);
      
    }, ctx);
  });
});

Instead of figuring out the internal value inside the closure, we only override the reference to the variable holding onto it, first with inject which takes the target name and the mocking name, then with invoke with takes a function inside of which we can call the alias of the closure, and a context argument (a la vm.runInContext) which contains references to our test library's expect method (I'm using jasmine for this repo), a value to be set for the the injected name 'mockInside.' You also have access to the context inside the function executed by invoke.

scope of invoke()

To prevent scope leaking, in any invoke(fn, ctx) call, the function fn argument's scope (this) is set to the context argument. Thus the following bdd test should pass:

meta('thisTest').invoke(function() {

  expect(this).toBe(context);
  
}, { expect: expect });

global === window

Where node.js has a named global object, browser engines typically do not. The window object contains a global reference to itself so you can use global in both environments.

method chaining

The meta() API supports chaining the individual methods together:

meta('chain').
inject('closure', 'mockClosure').
invoke(function () {
  expect(chain()).toBe('mocked') // should pass, calling chain()
}, { expect: expect, mockClosure: 'mocked' })

method lisping

Wait what? If you're like @HipsterHacker, you can use the experimental 'lisped' API, based on this idea:

(meta('lisp')
  ('closure', 'mockClosure')
  (function () {
    expect(lisp()).toBe('mocked') // should pass, calling lisp()
   }, { 
      expect: expect, mockClosure: 'mocked' 
   }));
  

Here's an evolving alternate form that uses a configuration specifier as the invocation context and avoids the inject re-naming step:

(meta('lisp')
  ({ expect: expect, closure: 'mocked' })
  (function () {
    expect(lisp()).toBe('mocked')
  }));

Another wiggier form:

(meta)
('lisp')
({ expect: expect, closure: 'mocked' })
(function () {
  expect(lisp()).toBe('mocked')
});

TIP: Expect this form of invocation sequence to show up elsewhere.

nested invocations

More proof-of-concept gold-plating, to call invocations inside other invocations you can pass the meta function object to the context, update the context as needed, then call invoke again:

var ctx = { expect: expect, meta: meta, closure: 'mocked' };    

meta('main').invoke(function() {
  expect(main()).toBe('mocked')
  
  // context argument to invoke() is visible in function scope
  context.closure = 'nested'
  
  meta('nestedMain').invoke(function() {
    expect(nestedMain()).toBe('nested')
  }, context)
  
}, ctx)

This is just a proof test that we're not blowing up the call stack in certain environments (namely, Internet Explorer).

extracting private functions directly

I've added an extract(functionName) method that can be used for excavating private functions from within an IFFE. This requires that you name the IFFE itself, then attach it to the exported return value ('main' in this case):

var fn = (function iffe(){

  // private
  function increment(n) {
    return n + 1;
  }
  
  // public
  function main(n) {
    return increment(n);
  };
  
  // expose the iffe - could make this conditional by environment/flag
  main.iffe = iffe;
  
  // make return statement separate from definition
  return main;
}());

Given that the iffe is now reachable as fn.iffe you can meta-fy it, then use extract('increment') to get a copy of the increment function, then use invoke() to exercise it with the context param:

it('should extract increment() function and invoke it', function(){

  var meta = fn.iffe.meta();
  var increment = meta.extract('increment');
  
  expect(typeof increment).toBe('function');
  
  for (var i = 0; i < 10; i++) {
  
    meta.invoke(function() {
    
      expect(increment(i)).toBe(i + 1);
      
    }, { increment: increment, i : i });
  }
});
  
  

tests

The complete test spec is contained in [https://github.com/dfkaye/metafunction/blob/master/test/meta.spec.js] (https://github.com/dfkaye/metafunction/blob/master/test/meta.spec.js).

node tests

Using Misko Hevery's jasmine-node to run command line tests on node (even though this project initially aimed at a browser shim).

npm test
# => jasmine-node --verbose ./test/node.spec.js

browser tests

Using @pivotallabs' jasmine-2.0.0 for the browser suite.

The jasmine2 browser test page is viewable on rawgit.

testem

Using Toby Ho's MAGNIFICENT testemjs to drive tests in multiple browsers for jasmine-2.0.0 (see how to hack testem for jasmine 2), as well as jasmine-node. The testem.json file uses the standalone test page above, and also uses a custom launcher for jasmine-node (v 1.3.1).

View both test types at the console by running:

testem -l j