import assert from 'assert'; import sinon from 'sinon'; import mockfs from 'mock-fs'; import path from 'path'; import precinct from 'precinct'; import rewire from 'rewire'; import Config from '../lib/Config'; const dependencyTree = rewire('../'); describe('dependencyTree', function() { this.timeout(8000); function testTreesForFormat(format, ext = '.js') { it('returns an object form of the dependency tree for a file', function() { const root = `${__dirname}/example/${format}`; const filename = `${root}/a${ext}`; const tree = dependencyTree({filename, root}); assert(tree instanceof Object); const aSubTree = tree[filename]; assert.ok(aSubTree instanceof Object); const filesInSubTree = Object.keys(aSubTree); assert.equal(filesInSubTree.length, 2); }); } function mockStylus() { mockfs({ [__dirname + '/example/stylus']: { 'a.styl': ` @import "b" @require "c.styl" `, 'b.styl': '@import "c"', 'c.styl': '' } }); } function mockSass() { mockfs({ [__dirname + '/example/sass']: { 'a.scss': ` @import "_b"; @import "_c.scss"; `, '_b.scss': 'body { color: blue; }', '_c.scss': 'body { color: pink; }' } }); } function mockLess() { mockfs({ [__dirname + '/example/less']: { 'a.less': ` @import "b.css"; @import "c.less"; `, 'b.css': 'body { color: blue; }', 'c.less': 'body { color: pink; }' } }); } function mockes6() { mockfs({ [__dirname + '/example/es6']: { 'a.js': ` import b from './b'; import c from './c'; `, 'b.js': 'export default function() {};', 'c.js': 'export default function() {};', 'jsx.js': `import c from './c';\n export default ;`, 'foo.jsx': `import React from 'react';\n import b from 'b';\n export default ;`, 'es7.js': `import c from './c';\n export default async function foo() {};` } }); } afterEach(function() { mockfs.restore(); }); it('returns an empty object for a non-existent filename', function() { mockfs({ imaginary: {} }); const root = __dirname + '/imaginary'; const filename = root + '/notafile.js'; const tree = dependencyTree({filename, root}); assert(tree instanceof Object); assert(!Object.keys(tree).length); }); it('handles nested tree structures', function() { const directory = __dirname + '/example/extended'; const filename = directory + '/a.js'; const tree = dependencyTree({filename, directory}); assert(tree[filename] instanceof Object); // b and c const subTree = tree[filename]; assert.equal(Object.keys(subTree).length, 2); const bTree = subTree[directory + '/b.js']; const cTree = subTree[directory + '/c.js']; // d and e assert.equal(Object.keys(bTree).length, 2); // f ang g assert.equal(Object.keys(cTree).length, 2); }); it('does not include files that are not real (#13)', function() { mockfs({ [__dirname + '/onlyRealDeps']: { 'a.js': 'var notReal = require("./notReal");' } }); const directory = __dirname + '/onlyRealDeps'; const filename = directory + '/a.js'; const tree = dependencyTree({filename, directory}); const subTree = tree[filename]; assert.ok(!Object.keys(subTree).some(dep => dep.indexOf('notReal') !== -1)); }); it('does not choke on cyclic dependencies', function() { mockfs({ [__dirname + '/cyclic']: { 'a.js': 'var b = require("./b");', 'b.js': 'var a = require("./a");' } }); const directory = __dirname + '/cyclic'; const filename = directory + '/a.js'; const spy = sinon.spy(dependencyTree, '_getDependencies'); const tree = dependencyTree({filename, directory}); assert(spy.callCount === 2); assert(Object.keys(tree[filename]).length); dependencyTree._getDependencies.restore(); }); it('excludes Nodejs core modules by default', function() { const directory = __dirname + '/example/commonjs'; const filename = directory + '/b.js'; const tree = dependencyTree({filename, directory}); assert(Object.keys(tree[filename]).length === 0); assert(Object.keys(tree)[0].indexOf('b.js') !== -1); }); it('traverses installed 3rd party node modules', function() { const directory = __dirname + '/example/onlyRealDeps'; const filename = directory + '/a.js'; const tree = dependencyTree({filename, directory}); const subTree = tree[filename]; assert(Object.keys(subTree).some(dep => dep === require.resolve('debug'))); }); it('returns a list of absolutely pathed files', function() { const directory = __dirname + '/example/commonjs'; const filename = directory + '/b.js'; const tree = dependencyTree({filename, directory}); for (let node in tree.nodes) { assert(node.indexOf(process.cwd()) !== -1); } }); it('excludes duplicate modules from the tree', function() { mockfs({ root: { // More than one module includes c 'a.js': `import b from "b"; import c from "c";`, 'b.js': 'import c from "c";', 'c.js': 'export default 1;' } }); const tree = dependencyTree.toList({ filename: 'root/a.js', directory: 'root' }); assert(tree.length === 3); }); describe('when given a detective configuration', function() { it('passes it through to precinct', function() { const spy = sinon.spy(precinct, 'paperwork'); const directory = __dirname + '/example/onlyRealDeps'; const filename = directory + '/a.js'; const detectiveConfig = { amd: { skipLazyLoaded: true } }; dependencyTree({ filename, directory, detective: detectiveConfig }); assert.ok(spy.calledWith(filename, detectiveConfig)); spy.restore(); }); }); describe('when given a list to store non existent partials', function() { describe('and the file contains no valid partials', function() { it('stores the invalid partials', function() { mockfs({ [__dirname + '/onlyRealDeps']: { 'a.js': 'var notReal = require("./notReal");' } }); const directory = __dirname + '/onlyRealDeps'; const filename = directory + '/a.js'; const nonExistent = []; const tree = dependencyTree({filename, directory, nonExistent}); assert.equal(nonExistent.length, 1); assert.equal(nonExistent[0], './notReal'); }); }); describe('and the file contains all valid partials', function() { it('does not store anything', function() { mockfs({ [__dirname + '/onlyRealDeps']: { 'a.js': 'var b = require("./b");', 'b.js': 'export default 1;' } }); const directory = __dirname + '/onlyRealDeps'; const filename = directory + '/a.js'; const nonExistent = []; const tree = dependencyTree({filename, directory, nonExistent}); assert.equal(nonExistent.length, 0); }); }); describe('and the file contains a mix of invalid and valid partials', function() { it('stores the invalid ones', function() { mockfs({ [__dirname + '/onlyRealDeps']: { 'a.js': 'var b = require("./b");', 'b.js': 'var c = require("./c"); export default 1;', 'c.js': 'var crap = require("./notRealMan");' } }); const directory = __dirname + '/onlyRealDeps'; const filename = directory + '/a.js'; const nonExistent = []; const tree = dependencyTree({filename, directory, nonExistent}); assert.equal(nonExistent.length, 1); assert.equal(nonExistent[0], './notRealMan'); }); }); describe('and there is more than one reference to the invalid partial', function() { it('only includes the non-existent partial once', function() { mockfs({ [__dirname + '/onlyRealDeps']: { 'a.js': 'var b = require("./b");\nvar crap = require("./notRealMan");', 'b.js': 'var c = require("./c"); export default 1;', 'c.js': 'var crap = require("./notRealMan");' } }); const directory = __dirname + '/onlyRealDeps'; const filename = directory + '/a.js'; const nonExistent = []; const tree = dependencyTree({filename, directory, nonExistent}); assert.equal(nonExistent.length, 1); assert.equal(nonExistent[0], './notRealMan'); }); }); }); describe('throws', function() { beforeEach(function() { this._directory = __dirname + '/example/commonjs'; this._revert = dependencyTree.__set__('traverse', () => []); }); afterEach(function() { this._revert(); }); it('throws if the filename is missing', function() { assert.throws(function() { dependencyTree({ filename: undefined, directory: this._directory }); }); }); it('throws if the root is missing', function() { assert.throws(function() { dependencyTree({filename}); }); }); it('throws if a supplied filter is not a function', function() { assert.throws(function() { const directory = __dirname + '/example/onlyRealDeps'; const filename = directory + '/a.js'; const tree = dependencyTree({ filename, directory, filter: 'foobar' }); }); }); it('does not throw on the legacy `root` option', function() { assert.doesNotThrow(function() { const directory = __dirname + '/example/onlyRealDeps'; const filename = directory + '/a.js'; const tree = dependencyTree({ filename, root: directory }); }); }); }); describe('on file error', function() { beforeEach(function() { this._directory = __dirname + '/example/commonjs'; }); it('does not throw', function() { assert.doesNotThrow(() => { dependencyTree({ filename: 'foo', directory: this._directory }); }); }); it('returns no dependencies', function() { const tree = dependencyTree({filename: 'foo', directory: this._directory}); assert(!tree.length); }); }); describe('when a filter function is supplied', function() { it('uses the filter to determine if a file should be included in the results', function() { const directory = __dirname + '/example/onlyRealDeps'; const filename = directory + '/a.js'; const tree = dependencyTree({ filename, directory, // Skip all 3rd party deps filter: (filePath, moduleFile) => { assert.ok(require.resolve('debug')); assert.ok(moduleFile.match('test/example/onlyRealDeps/a.js')); return filePath.indexOf('node_modules') === -1; } }); const subTree = tree[filename]; assert.ok(Object.keys(tree).length); const has3rdPartyDep = Object.keys(subTree).some(dep => dep === require.resolve('debug')); assert.ok(!has3rdPartyDep); }); }); describe('memoization (#2)', function() { beforeEach(function() { this._spy = sinon.spy(dependencyTree, '_getDependencies'); }); afterEach(function() { dependencyTree._getDependencies.restore(); }); it('accepts a cache object for memoization (#2)', function() { const filename = __dirname + '/example/amd/a.js'; const directory = __dirname + '/example/amd'; const cache = {}; cache[__dirname + '/example/amd/b.js'] = [ __dirname + '/example/amd/b.js', __dirname + '/example/amd/c.js' ]; const tree = dependencyTree({ filename, directory, visited: cache }); assert.equal(Object.keys(tree[filename]).length, 2); assert(this._spy.neverCalledWith(__dirname + '/example/amd/b.js')); }); it('returns the precomputed list of a cached entry point', function() { const filename = __dirname + '/example/amd/a.js'; const directory = __dirname + '/example/amd'; const cache = { // Shouldn't process the first file's tree [filename]: [] }; const tree = dependencyTree({ filename, directory, visited: cache }); assert(!tree.length); }); }); describe('module formats', function() { describe('amd', function() { testTreesForFormat('amd'); }); describe('commonjs', function() { testTreesForFormat('commonjs'); describe('when given a CJS file with lazy requires', function() { beforeEach(function() { mockfs({ [__dirname + '/cjs']: { 'foo.js': 'module.exports = function(bar = require("./bar")) {};', 'bar.js': 'module.exports = 1;' } }); }); it('includes the lazy dependency', function() { const directory = __dirname + '/cjs'; const filename = directory + '/foo.js'; const tree = dependencyTree({filename, directory}); const subTree = tree[filename]; assert.ok(`${directory}/bar.js` in subTree); }); }); describe('when given a CJS file with module property in package.json', function() { beforeEach(function() { mockfs({ [__dirname + '/es6']: { ['module.entry.js']: 'import * as module from "module.entry"', ['node_modules']: { ['module.entry']: { 'index.main.js': 'module.exports = function() {};', 'index.module.js': 'module.exports = function() {};', 'package.json': '{ "main": "index.main.js", "module": "index.module.js" }' } } } }); }); it('it includes the module entry as dependency', function() { const directory = __dirname + '/es6'; const filename = directory + '/module.entry.js'; const tree = dependencyTree({ filename, directory, nodeModulesConfig: { entry: 'module' } }); const subTree = tree[filename]; assert.ok(`${directory}/node_modules/module.entry/index.module.js` in subTree); }); }); }); describe('es6', function() { beforeEach(function() { this._directory = __dirname + '/example/es6'; mockes6(); }); testTreesForFormat('es6'); it('resolves files that have jsx', function() { const filename = `${this._directory}/jsx.js`; const {[filename]: tree} = dependencyTree({ filename, directory: this._directory }); assert.ok(tree[`${this._directory}/c.js`]); }); it('resolves files with a jsx extension', function() { const filename = `${this._directory}/foo.jsx`; const {[filename]: tree} = dependencyTree({ filename, directory: this._directory }); assert.ok(tree[`${this._directory}/b.js`]); }); it('resolves files that have es7', function() { const filename = `${this._directory}/es7.js`; const {[filename]: tree} = dependencyTree({ filename, directory: this._directory }); assert.ok(tree[`${this._directory}/c.js`]); }); describe('when given an es6 file using CJS lazy requires', function() { beforeEach(function() { mockfs({ [__dirname + '/es6']: { 'foo.js': 'export default function(bar = require("./bar")) {};', 'bar.js': 'export default 1;' } }); }); describe('and mixedImport mode is turned on', function() { it('includes the lazy dependency', function() { const directory = __dirname + '/es6'; const filename = directory + '/foo.js'; const tree = dependencyTree({ filename, directory, detective: { es6: { mixedImports: true } } }); const subTree = tree[filename]; assert.ok(`${directory}/bar.js` in subTree); }); it('also works for toList', function() { const directory = __dirname + '/es6'; const filename = directory + '/foo.js'; const results = dependencyTree.toList({ filename, directory, detective: { es6: { mixedImports: true } } }); assert.equal(results[0], `${directory}/bar.js`); assert.equal(results[1], filename); }); }); describe('and mixedImport mode is turned off', function() { it('does not include the lazy dependency', function() { const directory = __dirname + '/es6'; const filename = directory + '/foo.js'; const tree = dependencyTree({ filename, directory }); const subTree = tree[filename]; assert.ok(!(`${directory}/bar.js` in subTree)); }); }); }); describe('when given an es6 file using dynamic imports', function() { beforeEach(function() { mockfs({ [__dirname + '/es6']: { 'foo.js': 'import("./bar");', 'bar.js': 'export default 1;' } }); }); it('includes the dynamic import', function() { const directory = __dirname + '/es6'; const filename = directory + '/foo.js'; const tree = dependencyTree({ filename, directory }); const subTree = tree[filename]; assert.ok(!(`${directory}/bar.js` in subTree)); }); }); }); describe('sass', function() { beforeEach(function() { mockSass(); }); testTreesForFormat('sass', '.scss'); }); describe('stylus', function() { beforeEach(function() { mockStylus(); }); testTreesForFormat('stylus', '.styl'); }); describe('less', function() { beforeEach(function() { mockLess(); }); testTreesForFormat('less', '.less'); }); describe('typescript', function() { testTreesForFormat('ts', '.ts'); it('utilizes a tsconfig', function() { const directory = path.join(__dirname, 'example/ts'); const tsConfigPath = path.join(directory, '.tsconfig'); const results = dependencyTree.toList({ filename: `${__dirname}/example/ts/a.ts`, directory, tsConfig: tsConfigPath }); assert.equal(results[0], path.join(directory, 'b.ts')); assert.equal(results[1], path.join(directory, 'c.ts')); assert.equal(results[2], path.join(directory, 'a.ts')); }); it('supports tsx files', function() { const directory = path.join(__dirname, 'example/ts'); const results = dependencyTree.toList({ filename: `${__dirname}/example/ts/d.tsx`, directory }); assert.equal(results[0], path.join(directory, 'c.ts')); }); }); }); describe('toList', function() { function testToList(format, ext = '.js') { it('returns a post-order list form of the dependency tree', function() { const directory = __dirname + '/example/' + format; const filename = directory + '/a' + ext; const list = dependencyTree.toList({ filename, directory }); assert(list instanceof Array); assert(list.length); }); } it('returns an empty list on a non-existent filename', function() { mockfs({ imaginary: {} }); const directory = __dirname + '/imaginary'; const filename = directory + '/notafile.js'; const list = dependencyTree.toList({ filename, directory }); assert(list instanceof Array); assert(!list.length); }); it('orders the visited files by last visited', function() { const directory = __dirname + '/example/amd'; const filename = directory + '/a.js'; const list = dependencyTree.toList({ filename, directory }); assert(list.length === 3); assert(list[0] === directory + '/c.js'); assert(list[1] === directory + '/b.js'); assert(list[list.length - 1] === filename); }); describe('module formats', function() { describe('amd', function() { testToList('amd'); }); describe('commonjs', function() { testToList('commonjs'); }); describe('es6', function() { beforeEach(function() { mockes6(); }); testToList('es6'); }); describe('sass', function() { beforeEach(function() { mockSass(); }); testToList('sass', '.scss'); }); describe('stylus', function() { beforeEach(function() { mockStylus(); }); testToList('stylus', '.styl'); }); describe('less', function() { beforeEach(function() { mockLess(); }); testToList('less', '.less'); }); describe('typescript', function() { testToList('ts', '.ts'); }); }); }); describe('webpack', function() { beforeEach(function() { // Note: not mocking because webpack's resolver needs a real project with dependencies; // otherwise, we'd have to mock a ton of files. this._root = path.join(__dirname, '../'); this._webpackConfig = this._root + '/webpack.config.js'; this._testResolution = name => { const results = dependencyTree.toList({ filename: `${__dirname}/example/webpack/${name}.js`, directory: this._root, webpackConfig: this._webpackConfig, filter: filename => filename.indexOf('filing-cabinet') !== -1 }); assert.ok(results.some(filename => filename.indexOf('node_modules/filing-cabinet') !== -1)); }; }); it('resolves aliased modules', function() { this.timeout(5000); this._testResolution('aliased'); }); it('resolves unaliased modules', function() { this.timeout(5000); this._testResolution('unaliased'); }); }); describe('requirejs', function() { beforeEach(function() { mockfs({ root: { 'lodizzle.js': 'define({})', 'require.config.js': ` requirejs.config({ baseUrl: './', paths: { F: './lodizzle.js' } }); `, 'a.js': ` define([ 'F' ], function(F) { }); `, 'b.js': ` define([ './lodizzle' ], function(F) { }); ` } }); }); it('resolves aliased modules', function() { const tree = dependencyTree({ filename: 'root/a.js', directory: 'root', config: 'root/require.config.js' }); const filename = path.resolve(process.cwd(), 'root/a.js'); const aliasedFile = path.resolve(process.cwd(), 'root/lodizzle.js'); assert.ok('root/lodizzle.js' in tree[filename]); }); it('resolves non-aliased paths', function() { const tree = dependencyTree({ filename: 'root/b.js', directory: 'root', config: 'root/require.config.js' }); const filename = path.resolve(process.cwd(), 'root/b.js'); const aliasedFile = path.resolve(process.cwd(), 'root/lodizzle.js'); assert.ok('root/lodizzle.js' in tree[filename]); }); }); describe('Config', function() { describe('when cloning', function() { describe('and a detective config was set', function() { it('retains the detective config in the clone', function() { const detectiveConfig = { es6: { mixedImports: true } }; const config = new Config({ detectiveConfig, filename: 'foo', directory: 'bar' }); const clone = config.clone(); assert.deepEqual(clone.detectiveConfig, detectiveConfig); }); }); }); }); });