'use strict'; const precinct = require('precinct'); const path = require('path'); const fs = require('fs'); const cabinet = require('filing-cabinet'); const debug = require('debug')('tree'); const Config = require('./lib/Config'); /** * Recursively find all dependencies (avoiding circular) traversing the entire dependency tree * and returns a flat list of all unique, visited nodes * * @param {Object} options * @param {String} options.filename - The path of the module whose tree to traverse * @param {String} options.directory - The directory containing all JS files * @param {String} [options.requireConfig] - The path to a requirejs config * @param {String} [options.webpackConfig] - The path to a webpack config * @param {String} [options.nodeModulesConfig] - config for resolving entry file for node_modules * @param {Object} [options.visited] - Cache of visited, absolutely pathed files that should not be reprocessed. * Format is a filename -> tree as list lookup table * @param {Array} [options.nonExistent] - List of partials that do not exist * @param {Boolean} [options.isListForm=false] * @param {String|Object} [options.tsConfig] Path to a typescript config (or a preloaded one). * @return {Object} */ module.exports = function(options) { const config = new Config(options); if (!fs.existsSync(config.filename)) { debug('file ' + config.filename + ' does not exist'); return config.isListForm ? [] : {}; } const results = traverse(config); debug('traversal complete', results); dedupeNonExistent(config.nonExistent); debug('deduped list of nonExistent partials: ', config.nonExistent); let tree; if (config.isListForm) { debug('list form of results requested'); tree = Array.from(results); } else { debug('object form of results requested'); tree = {}; tree[config.filename] = results; } debug('final tree', tree); return tree; }; /** * Executes a post-order depth first search on the dependency tree and returns a * list of absolute file paths. The order of files in the list will be the * proper concatenation order for bundling. * * In other words, for any file in the list, all of that file's dependencies (direct or indirect) will appear at * lower indices in the list. The root (entry point) file will therefore appear last. * * The list will not contain duplicates. * * Params are those of module.exports */ module.exports.toList = function(options) { options.isListForm = true; return module.exports(options); }; /** * Returns the list of dependencies for the given filename * * Protected for testing * * @param {Config} config * @return {Array} */ module.exports._getDependencies = function(config) { let dependencies; const precinctOptions = config.detectiveConfig; precinctOptions.includeCore = false; try { dependencies = precinct.paperwork(config.filename, precinctOptions); debug('extracted ' + dependencies.length + ' dependencies: ', dependencies); } catch (e) { debug('error getting dependencies: ' + e.message); debug(e.stack); return []; } const resolvedDependencies = []; for (let i = 0, l = dependencies.length; i < l; i++) { const dep = dependencies[i]; const result = cabinet({ partial: dep, filename: config.filename, directory: config.directory, ast: precinct.ast, config: config.requireConfig, webpackConfig: config.webpackConfig, nodeModulesConfig: config.nodeModulesConfig, tsConfig: config.tsConfig }); if (!result) { debug('skipping an empty filepath resolution for partial: ' + dep); config.nonExistent.push(dep); continue; } const exists = fs.existsSync(result); if (!exists) { config.nonExistent.push(dep); debug('skipping non-empty but non-existent resolution: ' + result + ' for partial: ' + dep); continue; } resolvedDependencies.push(result); } return resolvedDependencies; }; /** * @param {Config} config * @return {Object|Set} */ function traverse(config) { let subTree = config.isListForm ? new Set() : {}; debug('traversing ' + config.filename); if (config.visited[config.filename]) { debug('already visited ' + config.filename); return config.visited[config.filename]; } let dependencies = module.exports._getDependencies(config); debug('cabinet-resolved all dependencies: ', dependencies); // Prevents cycles by eagerly marking the current file as read // so that any dependent dependencies exit config.visited[config.filename] = config.isListForm ? [] : {}; if (config.filter) { debug('using filter function to filter out dependencies'); debug('unfiltered number of dependencies: ' + dependencies.length); dependencies = dependencies.filter(function(filePath) { return config.filter(filePath, config.filename); }); debug('filtered number of dependencies: ' + dependencies.length); } for (let i = 0, l = dependencies.length; i < l; i++) { const d = dependencies[i]; const localConfig = config.clone(); localConfig.filename = d; if (localConfig.isListForm) { for (let item of traverse(localConfig)) { subTree.add(item); } } else { subTree[d] = traverse(localConfig); } } if (config.isListForm) { subTree.add(config.filename); config.visited[config.filename].push(...subTree); } else { config.visited[config.filename] = subTree; } return subTree; } // Mutate the list input to do a dereferenced modification of the user-supplied list function dedupeNonExistent(nonExistent) { const deduped = new Set(nonExistent); nonExistent.length = deduped.size; let i = 0; for (const elem of deduped) { nonExistent[i] = elem; i++; } }