@retailmenot/roux-sass-importer
Version:
A node-sass custom importer for Roux ecosystem ingredients.
662 lines (581 loc) • 15.6 kB
JavaScript
;
var _ = require('lodash');
var fs = require('fs');
var mkdirp = require('mkdirp');
var path = require('path');
var rimraf = require('rimraf');
var sinon = require('sinon');
var tap = require('tap');
var touch = require('touch');
var sass = require('node-sass');
var NODE_SASS_NULL = 'NODE_SASS_NULL';
var rouxSassImporter = require('../');
var FIXTURES_DIR = path.resolve(__dirname, 'fixtures');
var IMPORTING_PATH = '/path/to/importing/file.scss';
function scaffold(directory, config) {
mkdirp.sync(directory);
if (_.isArray(config)) {
_.forEach(config, function (filename) {
touch.sync(path.resolve(directory, filename));
});
} else if (_.isObject(config)) {
_.forEach(config, function (v, k) {
scaffold(path.resolve(directory, k), v);
});
}
}
tap.tearDown(function () {
// remove all fixture pantries
rimraf.sync(FIXTURES_DIR);
});
tap.test('exports a function', function (t) {
t.autoend();
t.ok(
_.isFunction(rouxSassImporter), 'module.exports is a function');
t.test('accepts a config object', function (t) {
t.autoend();
t.doesNotThrow(function () {
rouxSassImporter(NODE_SASS_NULL);
}, 'optional');
_.forEach(
[
'',
'foo',
0,
123
],
function (arg) {
t.throws(function () {
rouxSassImporter(NODE_SASS_NULL, arg);
}, 'must be an object or undefined');
});
t.test('config.pantries', function (t) {
t.autoend();
_.forEach(
[
'',
'foo',
0,
123
],
function (arg) {
t.throws(function () {
rouxSassImporter(NODE_SASS_NULL, {
pantries: arg
});
}, 'must be an object or undefined');
});
});
t.test('config.pantrySearchPaths', function (t) {
t.autoend();
_.forEach(
[
'',
'foo',
0,
123,
{},
new Date(),
function () {}
],
function (arg) {
t.throws(function () {
rouxSassImporter(NODE_SASS_NULL, {
pantrySearchPaths: arg
});
}, 'must be an array or undefined');
});
t.test('defaults to `["$CWD/node_modules"]`', function (t) {
var nodeModulesPath = path.resolve('node_modules');
scaffold(nodeModulesPath, {
pantry: {
ingredient: ['ingredient.md', 'index.scss']
}
});
var config = {
pantries: {}
};
var importer = rouxSassImporter(NODE_SASS_NULL, config).bind({});
var original = 'pantry/ingredient';
var expected = path.resolve(
nodeModulesPath,
'pantry',
'ingredient',
'index.scss'
);
var doneFn = function (result) {
rimraf.sync(path.resolve(nodeModulesPath, 'pantry'));
t.same(result, {
file: expected
}, 'the pantry is found in $CWD/node_modules');
t.end();
};
importer(original, original, doneFn);
});
});
});
});
tap.test('absolute paths are not handled', function (t) {
t.autoend();
var url = '/some/absolute/path';
var doneFn = sinon.spy();
var importer = rouxSassImporter(NODE_SASS_NULL, {}).bind({});
t.same(
importer(url, IMPORTING_PATH, doneFn),
NODE_SASS_NULL,
'`NODE_SASS_NULL` is returned synchronously'
);
t.notOk(doneFn.called, 'the function passed as `done` is not called');
});
tap.test('throws if NODE_SASS_NULL argument is omitted', function (t) {
t.autoend();
var importer = rouxSassImporter.bind();
t.throws(
importer,
'Throws if no arguments passed.'
);
});
tap.test('returns whatever NODE_SASS_NULL it is passed', function (t) {
t.autoend();
var url = '/some/absolute/path';
var doneFn = sinon.spy();
var importer = rouxSassImporter('NODE_SASS_NULL_OVERRIDE', {}).bind({});
t.same(
importer(url, IMPORTING_PATH, doneFn),
'NODE_SASS_NULL_OVERRIDE',
'`NODE_SASS_NULL_OVERRIDE` is returned synchronously'
);
});
tap.test('relative paths are returned unchanged', function (t) {
t.autoend();
var url = '../some/other/relative/path';
var doneFn = sinon.spy();
var importer = rouxSassImporter(NODE_SASS_NULL, {}).bind({});
t.same(
importer(url, IMPORTING_PATH, doneFn),
NODE_SASS_NULL,
'`NODE_SASS_NULL` is returned synchronously'
);
t.notOk(doneFn.called, 'the function passed as `done` is not called');
});
tap.test('paths like `pantry/ingredient`', function (t) {
t.autoend();
t.test('are rewritten synchronously if cached', function (t) {
t.autoend();
var mockPantry = {
ingredients: {
ingredient: {
name: 'ingredient',
path: '/path/to/pantry/ingredient',
entryPoints: {
sass: {
filename: 'index.scss'
}
}
}
}
};
var importer = rouxSassImporter(NODE_SASS_NULL, {
pantries: {
pantry: mockPantry
}
}).bind({});
var original = 'pantry/ingredient';
var expected = path.join(
mockPantry.ingredients.ingredient.path,
mockPantry.ingredients.ingredient.entryPoints.sass.filename
);
var doneFn = sinon.spy();
t.same(importer(original, IMPORTING_PATH, doneFn), {
file: expected
}, 'the path to the Sass entry point is returned synchronously');
t.notOk(doneFn.called, 'the function passed as `done` is not called');
});
t.test('are resolved asynchronously if not cached', function (t) {
t.autoend();
t.test('Error returned if pantry not found', function (t) {
scaffold(FIXTURES_DIR, {
pantry: {
ingredient: ['ingredient.md', 'index.scss']
}
});
var config = {
pantries: {},
pantrySearchPaths: [FIXTURES_DIR]
};
var importer = rouxSassImporter(NODE_SASS_NULL, config).bind({});
var original = 'not-a-pantry/ingredient';
var doneFn = function (result) {
t.type(result, Error, 'an Error is returned');
t.end();
};
var returnValue = importer(original, IMPORTING_PATH, doneFn);
t.equal(
returnValue,
undefined,
'nothing is returned'
);
});
t.test('path rewritten if pantry found', function (t) {
scaffold(FIXTURES_DIR, {
pantry: {
ingredient: ['ingredient.md', 'index.scss']
}
});
var config = {
pantries: {},
pantrySearchPaths: [FIXTURES_DIR]
};
var importer = rouxSassImporter(NODE_SASS_NULL, config).bind({});
var original = 'pantry/ingredient';
var expected = path.resolve(
FIXTURES_DIR,
'pantry',
'ingredient',
'index.scss'
);
var doneFn = function (result) {
t.same(result, {
file: expected
}, 'the path to the Sass entry point is returned asynchronously');
t.end();
};
var returnValue = importer(original, IMPORTING_PATH, doneFn);
t.equal(
returnValue,
undefined,
'nothing is returned'
);
});
t.test('empty file returned on second ingredient import', function (t) {
scaffold(FIXTURES_DIR, {
pantry: {
ingredient: ['ingredient.md', 'index.scss']
}
});
var config = {
pantries: {},
pantrySearchPaths: [FIXTURES_DIR]
};
var importer = rouxSassImporter(NODE_SASS_NULL, config).bind({});
var toImport = 'pantry/ingredient';
var expected = path.resolve(
FIXTURES_DIR,
'pantry',
'ingredient',
'index.scss'
);
var doneFn = function (result) {
t.same(
result,
{
file: expected
},
'the path to the Sass entry point is returned asynchronously'
);
var doneSpyFn = sinon.spy();
t.same(
importer(toImport, IMPORTING_PATH, doneSpyFn),
{
contents: ''
},
'empty contents are returned on the second call to the importer'
);
t.notOk(
doneSpyFn.called, 'the function passed as `done` is not called'
);
t.end();
};
importer(toImport, IMPORTING_PATH, doneFn);
});
});
});
tap.test('paths like `@namespace/pantry/ingredient`', function (t) {
t.autoend();
t.test('are rewritten synchronously if cached', function (t) {
t.autoend();
var mockPantry = {
ingredients: {
ingredient: {
name: 'ingredient',
path: '/path/to/@namespace/pantry/ingredient',
entryPoints: {
sass: {
filename: 'index.scss'
}
}
}
}
};
var importer = rouxSassImporter(NODE_SASS_NULL, {
pantries: {
'@namespace/pantry': mockPantry
}
}).bind({});
var original = '@namespace/pantry/ingredient';
var expected = path.join(
mockPantry.ingredients.ingredient.path,
mockPantry.ingredients.ingredient.entryPoints.sass.filename
);
var doneFn = sinon.spy();
t.same(importer(original, IMPORTING_PATH, doneFn), {
file: expected
}, 'the path to the Sass entry point is returned synchronously');
t.notOk(doneFn.called, 'the function passed as `done` is not called');
});
t.test('are resolved asynchronously if not cached', function (t) {
t.autoend();
t.test('path unchanged if pantry not found', function (t) {
scaffold(FIXTURES_DIR, {
pantry: {
ingredient: ['ingredient.md', 'index.scss']
}
});
var config = {
pantries: {},
pantrySearchPaths: [FIXTURES_DIR]
};
var importer = rouxSassImporter(NODE_SASS_NULL, config).bind({});
var original = '@namespace/not-a-pantry/ingredient';
var doneFn = function (result) {
t.type(result, Error, 'an Error is returned');
t.end();
};
var returnValue = importer(original, IMPORTING_PATH, doneFn);
t.equal(
returnValue,
undefined,
'nothing is returned'
);
});
t.test('path rewritten if pantry found', function (t) {
scaffold(FIXTURES_DIR, {
'@namespace': {
pantry: {
ingredient: ['ingredient.md', 'index.scss']
}
}
});
var config = {
pantries: {},
pantrySearchPaths: [FIXTURES_DIR]
};
var importer = rouxSassImporter(NODE_SASS_NULL, config).bind({});
var original = '@namespace/pantry/ingredient';
var expected = path.resolve(
FIXTURES_DIR,
'@namespace/pantry',
'ingredient',
'index.scss'
);
var doneFn = function (result) {
t.same(result, {
file: expected
}, 'the path to the Sass entry point is returned asynchronously');
t.end();
};
var returnValue = importer(original, IMPORTING_PATH, doneFn);
t.equal(
returnValue,
undefined,
'nothing is returned'
);
});
t.test('pantry cached if found', function (t) {
scaffold(FIXTURES_DIR, {
'@namespace': {
pantry: {
ingredient: ['ingredient.md', 'index.scss']
}
}
});
var config = {
pantries: {},
pantrySearchPaths: [FIXTURES_DIR]
};
var importer = rouxSassImporter(NODE_SASS_NULL, config).bind({});
var original = '@namespace/pantry/ingredient';
var expected = path.resolve(
FIXTURES_DIR,
'@namespace/pantry',
'ingredient',
'index.scss'
);
var doneFn = function (result) {
t.same(result, {
file: expected
}, 'the path to the Sass entry point is returned asynchronously');
var doneSpyFn = sinon.spy();
t.same(
importer(original, IMPORTING_PATH, doneSpyFn),
{
contents: ''
},
'returns an empty file synchronously the 2nd time a file is imported'
);
t.notOk(
doneSpyFn.called, 'the function passed as `done` is not called');
t.end();
};
importer(original, original, doneFn);
});
});
});
tap.test('node-sass integration', function (t) {
var nodeModulesPath = path.resolve('node_modules');
var content = {
'node_modules/npm-pantry/ingredient/index.scss': [
'/* node_modules/npm-pantry/ingredient/index.scss */',
'@import "./relative-import.scss";'
].join('\n'),
'node_modules/npm-pantry/ingredient/relative-import.scss':
'/* node_modules/npm-pantry/ingredient/relative-import.scss */\n',
'node_modules/@rmn/another-npm-pantry/ingredient/index.scss':
'/* node_modules/@rmn/another-npm-pantry/ingredient/index.scss */\n',
'FIXTURES_DIR/pantry/ingredient/index.scss':
'/* FIXTURES_DIR/pantry/ingredient/index.scss */\n',
'FIXTURES_DIR/@namespace/pantry/ingredient/index.scss':
'/* FIXTURES_DIR/@namespace/pantry/ingredient/index.scss */\n',
'cached-pantry/ingredient/index.scss':
'/* cached-pantry/ingredient/index.scss */\n',
'@namespace/cached-pantry/ingredient/index.scss':
'/* @namespace/cached-pantry/ingredient/index.scss */\n'
};
scaffold(nodeModulesPath, {
'npm-pantry': {
ingredient: ['ingredient.md', 'index.scss']
},
'@rmn': {
'another-npm-pantry': {
ingredient: ['ingredient.md', 'index.scss']
}
}
});
fs.writeFileSync(
path.resolve(nodeModulesPath, 'npm-pantry', 'ingredient', 'index.scss'),
content['node_modules/npm-pantry/ingredient/index.scss']
);
fs.writeFileSync(
path.resolve(
nodeModulesPath, 'npm-pantry', 'ingredient', 'relative-import.scss'
),
content['node_modules/npm-pantry/ingredient/relative-import.scss']
);
fs.writeFileSync(
path.resolve(
nodeModulesPath, '@rmn/another-npm-pantry', 'ingredient', 'index.scss'
),
content['node_modules/@rmn/another-npm-pantry/ingredient/index.scss']
);
scaffold(FIXTURES_DIR, {
pantry: {
ingredient: ['ingredient.md', 'index.scss']
},
'@namespace': {
pantry: {
ingredient: ['ingredient.md', 'index.scss']
}
},
cached: {
pantry: {
ingredient: ['ingredient.md', 'index.scss']
},
'@namespace': {
pantry: {
ingredient: ['ingredient.md', 'index.scss']
}
}
}
});
fs.writeFileSync(
path.resolve(
FIXTURES_DIR, 'pantry', 'ingredient', 'index.scss'
),
content['FIXTURES_DIR/pantry/ingredient/index.scss']
);
fs.writeFileSync(
path.resolve(
FIXTURES_DIR, '@namespace/pantry', 'ingredient', 'index.scss'
),
content['FIXTURES_DIR/@namespace/pantry/ingredient/index.scss']
);
fs.writeFileSync(
path.resolve(
FIXTURES_DIR, 'cached', 'pantry', 'ingredient', 'index.scss'
),
content['cached-pantry/ingredient/index.scss']
);
fs.writeFileSync(
path.resolve(
FIXTURES_DIR, 'cached', '@namespace/pantry', 'ingredient', 'index.scss'
),
content['@namespace/cached-pantry/ingredient/index.scss']
);
var importer = rouxSassImporter(NODE_SASS_NULL, {
pantries: {
'cached-pantry': {
ingredients: {
ingredient: {
name: 'ingredient',
path: path.resolve(
FIXTURES_DIR, 'cached', 'pantry', 'ingredient'
),
entryPoints: {
sass: {
filename: 'index.scss'
}
}
}
}
},
'@namespace/cached-pantry': {
ingredients: {
ingredient: {
name: 'ingredient',
path: path.resolve(
FIXTURES_DIR, 'cached', '@namespace/pantry', 'ingredient'
),
entryPoints: {
sass: {
filename: 'index.scss'
}
}
}
}
}
},
pantrySearchPaths: [
nodeModulesPath,
FIXTURES_DIR
]
}).bind({});
var expected =
content['node_modules/npm-pantry/ingredient/index.scss'].split('\n')[0] +
'\n' +
content['node_modules/npm-pantry/ingredient/relative-import.scss'] +
content['node_modules/@rmn/another-npm-pantry/ingredient/index.scss'] +
content['FIXTURES_DIR/pantry/ingredient/index.scss'] +
content['FIXTURES_DIR/@namespace/pantry/ingredient/index.scss'] +
content['cached-pantry/ingredient/index.scss'] +
content['@namespace/cached-pantry/ingredient/index.scss'];
sass.render({
file: null,
data: [
'@import "npm-pantry/ingredient";',
'@import "@rmn/another-npm-pantry/ingredient";',
'@import "pantry/ingredient";',
'@import "@namespace/pantry/ingredient";',
'@import "cached-pantry/ingredient";',
'@import "@namespace/cached-pantry/ingredient";',
].join('\n'),
importer: importer
}, function (err, result) {
rimraf.sync(path.resolve(nodeModulesPath, 'pantry'));
rimraf.sync(FIXTURES_DIR);
if (err) {
throw err;
}
t.equal(result.css.toString(), expected);
t.end();
});
});