precinct
Version:
Unleash the detectives
204 lines (167 loc) • 5.41 kB
JavaScript
;
const fs = require('fs');
const path = require('path');
const process = require('process');
const { debuglog } = require('util');
const getModuleType = require('module-definition');
const Walker = require('node-source-walk');
const detectiveAmd = require('detective-amd');
const detectiveCjs = require('detective-cjs');
const detectiveEs6 = require('detective-es6');
const detectiveLess = require('@dependents/detective-less');
const detectivePostcss = require('detective-postcss');
const detectiveSass = require('detective-sass');
const detectiveScss = require('detective-scss');
const detectiveStylus = require('detective-stylus');
const detectiveTypeScript = require('detective-typescript');
const detectiveVue = require('detective-vue2');
const debug = debuglog('precinct');
// eslint-disable-next-line n/no-deprecated-api
const natives = process.binding('natives');
/**
* Finds the list of dependencies for the given file
*
* @param {String|Object} content - File's content or AST
* @param {Object} [options]
* @param {String} [options.type] - The type of content being passed in. Useful if you want to use a non-js detective
* @return {String[]}
*/
function precinct(content, options = {}) {
debug('options given: %o', options);
let ast;
// We assume we're dealing with a JS file
if (!options.type && typeof content !== 'object') {
debug('we assume this is JS');
const walker = new Walker();
try {
// Parse once and distribute the AST to all detectives
ast = walker.parse(content);
debug('parsed the file content into an ast');
precinct.ast = ast;
} catch (error) {
// In case a previous call had it populated
precinct.ast = null;
debug('could not parse content: %s', error.message);
return [];
}
// SASS files shouldn't be parsed by Acorn
} else {
ast = content;
if (typeof content === 'object') {
precinct.ast = content;
}
}
const type = options.type ?? getModuleType.fromSource(ast);
debug('module type: %s', type);
const detective = getDetective(type, options);
let dependencies = [];
if (detective) {
dependencies = detective(ast, options[type]);
} else {
debug('no detective found for: %s', type);
}
// For non-JS files that we don't parse
if (detective?.ast) {
precinct.ast = detective.ast;
}
return dependencies;
}
/**
* Returns the dependencies for the given file path
*
* @param {String} filename
* @param {Object} [options]
* @param {Boolean} [options.includeCore=true] - Whether or not to include core modules in the dependency list
* @param {Object} [options.fileSystem=undefined] - An alternative fs implementation to use for reading the file path.
* @return {String[]}
*/
precinct.paperwork = (filename, options = {}) => {
options = { includeCore: true, ...options };
const fileSystem = options.fileSystem || fs;
const content = fileSystem.readFileSync(filename, 'utf8');
const ext = path.extname(filename);
let type;
if (ext === '.styl') {
debug('paperwork: converting .styl into the stylus type');
type = 'stylus';
} else if (ext === '.cjs') {
debug('paperwork: converting .cjs into the commonjs type');
type = 'commonjs';
// We need to sniff the JS module to find its type, not by extension.
// Other possible types pass through normally
} else if (!['.js', '.jsx'].includes(ext)) {
debug('paperwork: stripping the dot from the extension to serve as the type');
type = ext.replace('.', '');
}
if (type) {
debug('paperwork: setting the module type');
options.type = type;
}
debug('paperwork: invoking precinct');
const dependencies = precinct(content, options);
if (!options.includeCore) {
return dependencies.filter(dependency => {
if (dependency.startsWith('node:')) return false;
// In Node.js 18, node:test is a builtin but shows up under natives["test"],
// but can only be imported by "node:test." We're correcting this so "test"
// isn't unnecessarily stripped from the imports
if (dependency === 'test') {
debug('paperwork: allowing test import to avoid builtin/natives consideration');
return true;
}
return !natives[dependency];
});
}
debug('paperwork: got these results\n', dependencies);
return dependencies;
};
function getDetective(type, options) {
const mixedMode = options.es6?.mixedImports;
switch (type) {
case 'cjs':
case 'commonjs': {
return mixedMode ? detectiveEs6Cjs : detectiveCjs;
}
case 'css': {
return detectivePostcss;
}
case 'amd': {
return detectiveAmd;
}
case 'mjs':
case 'esm':
case 'es6': {
return mixedMode ? detectiveEs6Cjs : detectiveEs6;
}
case 'sass': {
return detectiveSass;
}
case 'less': {
return detectiveLess;
}
case 'scss': {
return detectiveScss;
}
case 'stylus': {
return detectiveStylus;
}
case 'ts': {
return detectiveTypeScript;
}
case 'tsx': {
return detectiveTypeScript.tsx;
}
case 'vue': {
return detectiveVue;
}
default:
// nothing
}
}
function detectiveEs6Cjs(ast, detectiveOptions) {
return [
...detectiveEs6(ast, detectiveOptions),
...detectiveCjs(ast, detectiveOptions)
];
}
module.exports = precinct;