Ava: JSPM support?

Created on 5 Nov 2015  路  13Comments  路  Source: avajs/ava

Is there any JSPM support built-in?

Thanks!

Most helpful comment

Combining what @salfield and @eirikb offered, I managed to come up with a loader shim that will read your jspm config and allow you to import modules as normal at the top of your tests.

For the below examples, this file resides in ./test/helpers/jspm_loader.js. If you place it somewhere else, you may need to adjust some of the relative paths.

// NOTE: I needed this here as I wasn't getting babel-register to load the
//       proper presets and plugins from package.json and I'm not using
//       a .babelrc file. This may be optional for you, or you may want
//       different presets/plugins.
require('babel-register')({
    presets: ['es2015'],
    plugins: [
        'transform-object-assign'
    ]
});

const url = require('url');
const path = require('path');
const Module = require('module');
const jspm = require('jspm');
const load = Module._load;

jspm.setPackagePath('../../../');

const System = jspm.Loader(); // Load your project's JSPM config

// NOTE: You may need to add paths here that are in your jspm.browser.js
//       config if you're using JSPM 0.17, as jspm.Loader() currently
//       appears to only load your jspm.config.js file.
System.config({
    paths: {
        '~/*': path.normalize(`${__dirname}/../../src/app/*`)
    }
});

// NOTE: Still an ugly hack. There may be other solutions like
//       shimming `require`? Open to improvements.
Module._load = (name, m) => {
    try {
        return load(name, m);
    } catch (e) {
        var normalizedName;

        if (name.indexOf('.') === 0) {
            // HACK: Handle relative file paths
            normalizedName = path.normalize(`${path.dirname(m.filename)}/${name}.js`);
        } else {
            normalizedName = url.parse(System.normalizeSync(name)).path;
        }

        return load(normalizedName, m);
    }
};

Your package.json should have the following:

"ava": {
  "require": [
    "./test/helpers/jspm_loader.js"
  ]
}

That allowed me to write the following test:

import test from 'ava';
import {default as analytics, optedOut} from '~/helpers/analytics';

test('Tracking Opt-out', assert => {
    analytics.optOut();

    const actual = optedOut();
    const expected = true;

    assert.is(actual, expected,
        'optedOut() should return true after calling analytics.optOut().');
});

All 13 comments

I don't understand the question. JSPM support in what form? Can you elaborate?

@sindresorhus at least as a module loader. I mean, AVA is obviously supports ES2015 syntax, but JSPM includes SystemJS loader. Will AVA resolve modules correctly?

We just run the test file through Babel.js. Whatever Babel supports, we support.

This might help: https://babeljs.io/docs/setup/#jspm ;)

@sindresorhus
I suppose it's not that easy.
The problem is: you need to load somehow SystemJS config (generated by JSPM) before running the tests.

Example of the project
https://github.com/dananichev/ava-jspm-example

npm test

results in

/src/helpers.js:1
(function (exports, require, module, __filename, __dirname) { import * as utils from 'underscore.string';
                                                              ^^^^^^

SyntaxError: Unexpected token import

Am i missing something?

Hi @dananichev

Your import..issue can be solved this way:

  1. Update AVA to latest version.
  2. Add "ava": { "require": [ "babel-core/register" ] to your _package.json_-file.
  3. Replace your _.babel.rc_-file content with {"presets": ["es2015"]}.

You will still have problems with modules not loading from _jspm_packages_.
I don't know how to solve this properly, one super-ugly-don't-do-this-hack is to override require to check _jspm_packages_-folder first (since the layout is a bit different other resolvers might not handle it).
Here is a working example (with the horrible hack): https://gist.github.com/eirikb/d8d9a20673166c02bf62

Note that jspm is not actually used when testing.

@eirikb Thanks. But i think i will stick with Jasmine/Mocha/etc for now. Anyhow AVA seems pretty intriguing and i'm planning to wait for a full JSPM support and so on.

This code below works fine for me. Slightly annoying boilerplate to add to each test file, but works.

import test from 'ava';
import jspm from 'jspm';
jspm.setPackagePath('../../../');
let moduleName;

test.before(t => {
  return jspm.import('path/to/module')
    .then((module) => {
      moduleName = module;
      return
    })
});

test(t => {
  console.log(moduleName);
  /* run a test here */
});

+1 on this.

We're having the issue that babel doesn't know it is supposed to look in jspm_packages for module resolution instead of looking in node_modules.

@sindresorhus, I don't see any way of configuring babel to handle this directly. The way JSPM and Babel work together is via a JSPM plugin, but that doesn't appear to be the right way forward here, seeing as JSPM is not really meant to run tests. Like @dananichev said, the SystemJS config - which includes in it a mapping of module names to their source directories - probably needs to play a role.

The best solution is probably to bundle the JSPM code and test that. We have Babel support and you can do a lot with that. We're not going to add native support for JSPM.

Combining what @salfield and @eirikb offered, I managed to come up with a loader shim that will read your jspm config and allow you to import modules as normal at the top of your tests.

For the below examples, this file resides in ./test/helpers/jspm_loader.js. If you place it somewhere else, you may need to adjust some of the relative paths.

// NOTE: I needed this here as I wasn't getting babel-register to load the
//       proper presets and plugins from package.json and I'm not using
//       a .babelrc file. This may be optional for you, or you may want
//       different presets/plugins.
require('babel-register')({
    presets: ['es2015'],
    plugins: [
        'transform-object-assign'
    ]
});

const url = require('url');
const path = require('path');
const Module = require('module');
const jspm = require('jspm');
const load = Module._load;

jspm.setPackagePath('../../../');

const System = jspm.Loader(); // Load your project's JSPM config

// NOTE: You may need to add paths here that are in your jspm.browser.js
//       config if you're using JSPM 0.17, as jspm.Loader() currently
//       appears to only load your jspm.config.js file.
System.config({
    paths: {
        '~/*': path.normalize(`${__dirname}/../../src/app/*`)
    }
});

// NOTE: Still an ugly hack. There may be other solutions like
//       shimming `require`? Open to improvements.
Module._load = (name, m) => {
    try {
        return load(name, m);
    } catch (e) {
        var normalizedName;

        if (name.indexOf('.') === 0) {
            // HACK: Handle relative file paths
            normalizedName = path.normalize(`${path.dirname(m.filename)}/${name}.js`);
        } else {
            normalizedName = url.parse(System.normalizeSync(name)).path;
        }

        return load(normalizedName, m);
    }
};

Your package.json should have the following:

"ava": {
  "require": [
    "./test/helpers/jspm_loader.js"
  ]
}

That allowed me to write the following test:

import test from 'ava';
import {default as analytics, optedOut} from '~/helpers/analytics';

test('Tracking Opt-out', assert => {
    analytics.optOut();

    const actual = optedOut();
    const expected = true;

    assert.is(actual, expected,
        'optedOut() should return true after calling analytics.optOut().');
});

In order to improve the documentation, @dak's answer may be a good base for a recipe? What do you think?

@sindresorhus

I want to be able to test my modules separately, though! The recipe ideas @dak shared helped a lot.

@dak @forresst

Due to the default behavior of Babel 6, where it doesn't do anything by default unless you've correctly configured your .babelrc, I think it is advisable to suggest setting up a .babelrc as part of the recipe. The JSPM babel plugin works around this by configuring Babel programmatically as you do here, but I find that a bit obtuse. It's too easy to let your test and build configurations get out of sync if you don't use a .babelrc. Better to keep all your Babel config in one place, at least for a default configuration.

@dak Your JSPM config should already have a mapping for app/ (or whatever you named it during jspm init), so you should be able to do away with the ~ resolution code.

Here's what I ended up with, which is a little simpler and seems to work better when importing JSPM-managed dependencies. It also looks to JSPM before NPM because I'm using JSPM as my primary package manager:

// Adapted test/helpers/jspm_loader.js
const Module = require('module')
    , jspm   = require('jspm')
    , path   = require('path')
    , url    = require('url')
    , load   = Module._load

// These were causing me trouble prior to my adapting the original loader, but now this array can probably be deleted?
const SpecialSystemModules = [
  '@empty'
  , '@system-env'
  , '@@global-helpers'
  , '@@cjs-helpers'
]

jspm.setPackagePath('..') // This seems to want to be the path relative to the test directory?

const System = jspm.Loader()

/* No need for additional configuration with the '~' path. I can import my app using:
 *   import app from 'app/main'
 * which is the same as what I would write for my System.import call in a <script> tag if I was bundling:
 *   System.import('app/main')
 */

// Needed this to resolve "app/" imports correctly
function moduleIsInSystemPaths (name) {
  let systemPath = path.parse(name).dir + '/'

  return systemPath in System.paths
}

// Still shimming Module._load, unfortunately.
// I chose to prefer the JSPM dependency over the Node dependency
// because I am managing all of my dependencies via JSPM.
Module._load = (name, m) => {
  // Is the module "special"? (Cannot be looked up in filesystem)
  // Needed this to fix the errors caused by SpecialSystemModules like @system-env
  if (name in System._loader.modules) {
    return System._loader.modules[name].module
  }

  if (name in System.map || moduleIsInSystemPaths(name)) {
    let jspmUri  = System.normalizeSync(name)
    name = url.parse(jspmUri).path
  }

  return load(name, m)
}

This handles

  • Relative imports
  • Imports of JSPM dependencies
  • Imports of NPM dependencies
  • Imports of app source files

_However_, of all the JSPM packages I tested with this, for some reason moment.js doesn't work correctly. Instead of getting the moment module, I get the JSON from the [email protected] overrides file. Everything else works, but moment doesn't. @guybedford, I tried editing the overrides and nothing seemed to fix this. Maybe I'm missing something?

I was able to write the following test files:

// test/example.js
import test from 'ava'

import xtend from 'xtend' // this is an NPM dep
import BigNumber from 'bignumber.js' // this is a JSPM dep

import app  from 'app/main'
import anotherTestFileIGuess from './anotherTestFile.js'

console.log(xtend)
console.log(BigNumber)

async function fn() {
    return Promise.resolve(new BigNumber("1234567890.123456789"))
}

test('example test', async t => {
    t.is((await fn()).toString(), "1234567890.123456789")
})
// test/anotherTestFile.js
import test from 'ava'

test('Always passes!', t => t.pass())

Here is the contents of app/main.js:

console.log(
`
    Hello, world.
`
)

async function test () {
  console.log(`    ${await 5}\n`)
}

test()

I have the following .babelrc:

{
  "presets": [ "es2015", "stage-2" ]
}

And this is my "ava" config in package.json:

"ava": {
  "require": [
    "babel-register",
    "babel-polyfill", // needed for async/await in my source code
    "./test/helpers/jspm_loader"
  ]
}

And everything seems to work splendidly.

A recipe for using JSPM with AVA would be most welcome! :D

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ivogabe picture ivogabe  路  5Comments

nickjanssen picture nickjanssen  路  4Comments

fleg picture fleg  路  3Comments

fregante picture fregante  路  3Comments

sindresorhus picture sindresorhus  路  5Comments