console/rules/enforce-deps-in-dev.cjs
renovate[bot] 5220571be5
chore(deps): update dependency bullmq to v5.4.2 (#4169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-17 10:21:08 +00:00

234 lines
6.7 KiB
JavaScript

/**
* Why? Few reasons:
* - tsup treats dependencies as external code and does not bundle them
* - without dependencies turborepo will always serve stale code when some of dependencies changed
*
* Moving internal dependencies to devDependencies makes tsup treat them as non-external and turborepo still keep tracks of relations
*/
/// @ts-check
const path = require('path');
const fs = require('fs');
const minimatch = require('minimatch');
const readPkgUp = require('eslint-module-utils/readPkgUp').default;
const moduleVisitor = require('eslint-module-utils/moduleVisitor').default;
function isHivePackage(packageName, scopes) {
return (
typeof packageName === 'string' && scopes.some(scope => packageName.startsWith(`${scope}/`))
);
}
const depFieldCache = new Map();
function hasKeys(obj = {}) {
return Object.keys(obj).length > 0;
}
function extractDepFields(pkg) {
return {
name: pkg.name,
dependencies: pkg.dependencies || {},
devDependencies: pkg.devDependencies || {},
optionalDependencies: pkg.optionalDependencies || {},
peerDependencies: pkg.peerDependencies || {},
};
}
function getDependencies(context, packageDir) {
let paths = [];
try {
const packageContent = {
name: '',
dependencies: {},
devDependencies: {},
optionalDependencies: {},
peerDependencies: {},
};
if (packageDir && packageDir.length > 0) {
if (!Array.isArray(packageDir)) {
paths = [path.resolve(packageDir)];
} else {
paths = packageDir.map(dir => path.resolve(dir));
}
}
if (paths.length > 0) {
// use rule config to find package.json
paths.forEach(dir => {
const packageJsonPath = path.join(dir, 'package.json');
if (!depFieldCache.has(packageJsonPath)) {
const depFields = extractDepFields(JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')));
depFieldCache.set(packageJsonPath, depFields);
}
const _packageContent = depFieldCache.get(packageJsonPath);
Object.keys(packageContent).forEach(depsKey =>
Object.assign(packageContent[depsKey], _packageContent[depsKey]),
);
});
} else {
// use closest package.json
Object.assign(
packageContent,
extractDepFields(
readPkgUp({
cwd: context.getPhysicalFilename
? context.getPhysicalFilename()
: context.getFilename(),
normalize: false,
}).pkg,
),
);
}
if (
![
packageContent.dependencies,
packageContent.devDependencies,
packageContent.optionalDependencies,
packageContent.peerDependencies,
].some(hasKeys)
) {
return null;
}
return packageContent;
} catch (e) {
if (paths.length > 0 && e.code === 'ENOENT') {
context.report({
message: 'The package.json file could not be found.',
loc: { line: 0, column: 0 },
});
}
if (e.name === 'JSONError' || e instanceof SyntaxError) {
context.report({
message: 'The package.json file could not be parsed: ' + e.message,
loc: { line: 0, column: 0 },
});
}
return null;
}
}
function missingErrorMessage(packageName) {
return `'${packageName}' should be listed in the project's devDependencies. `;
}
function directDepErrorMessage(packageName) {
return `'${packageName}' should be listed in the project's devDependencies, not dependencies.`;
}
function optDepErrorMessage(packageName) {
return `'${packageName}' should be listed in the project's devDependencies, not optionalDependencies`;
}
function peerDepErrorMessage(packageName) {
return `'${packageName}' should be listed in the project's devDependencies, not peerDependencies`;
}
function getModuleOriginalName(name) {
const [first, second] = name.split('/');
return first.startsWith('@') ? `${first}/${second}` : first;
}
function checkDependencyDeclaration(deps, packageName, declarationStatus) {
const newDeclarationStatus = declarationStatus || {
isInDeps: false,
isInDevDeps: false,
isInOptDeps: false,
isInPeerDeps: false,
};
// in case of sub package.json inside a module
// check the dependencies on all hierarchy
const packageHierarchy = [];
const packageNameParts = packageName ? packageName.split('/') : [];
packageNameParts.forEach((namePart, index) => {
if (!namePart.startsWith('@')) {
const ancestor = packageNameParts.slice(0, index + 1).join('/');
packageHierarchy.push(ancestor);
}
});
return packageHierarchy.reduce((result, ancestorName) => {
return {
isInDeps: result.isInDeps || deps.dependencies[ancestorName] !== undefined,
isInDevDeps: result.isInDevDeps || deps.devDependencies[ancestorName] !== undefined,
isInOptDeps: result.isInOptDeps || deps.optionalDependencies[ancestorName] !== undefined,
isInPeerDeps: result.isInPeerDeps || deps.peerDependencies[ancestorName] !== undefined,
};
}, newDeclarationStatus);
}
function reportIfMissing(context, deps, node, name, scopes) {
if (node.importKind === 'type' || node.importKind === 'typeof') {
return;
}
if (!isHivePackage(name, scopes)) {
return;
}
const importPackageName = getModuleOriginalName(name);
let declarationStatus = checkDependencyDeclaration(deps, importPackageName);
if (declarationStatus.isInDevDeps) {
return;
}
if (declarationStatus.isInDeps) {
context.report(node, directDepErrorMessage(importPackageName));
return;
}
if (declarationStatus.isInOptDeps) {
context.report(node, optDepErrorMessage(importPackageName));
return;
}
if (declarationStatus.isInPeerDeps) {
context.report(node, peerDepErrorMessage(importPackageName));
return;
}
context.report(node, missingErrorMessage(importPackageName));
}
module.exports = {
meta: {
type: 'problem',
},
create(context) {
const options = context.options[0] || {};
const deps = getDependencies(context, options.packageDir) || extractDepFields({});
if (Array.isArray(options.ignored)) {
const filepath = context.getPhysicalFilename
? context.getPhysicalFilename()
: context.getFilename();
if (
options.ignored.some(
ignored =>
minimatch(filepath, ignored) || minimatch(filepath, path.join(process.cwd(), ignored)),
)
) {
return {};
}
}
if (!Array.isArray(options.scopes)) {
throw new Error('[hive/enforce-deps-in-dev] The scopes option must be an array.');
}
return moduleVisitor(
(source, node) => {
reportIfMissing(context, deps, node, source.value, options.scopes);
},
{ commonjs: true },
);
},
};