mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
234 lines
6.7 KiB
JavaScript
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 },
|
|
);
|
|
},
|
|
};
|