New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@roq/eslint-plugin

Package Overview
Dependencies
Maintainers
5
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@roq/eslint-plugin - npm Package Compare versions

Comparing version 1.1.1 to 1.1.2

lib/rules/entities-should-follow-conventions.js

53

.eslintrc.json
{
"root": true,
"env": {
"node": true,
"commonjs": true,
"es2021": true
},
"extends": [
"airbnb-base"
],
"parserOptions": {
"ecmaVersion": 11
},
"rules": {
"no-useless-escape": 0,
"no-restricted-syntax": 0,
"no-nested-ternary": 0,
"global-require": 0,
"max-len": [
"error",
{
"code": 120,
"ignoreTemplateLiterals": true,
"ignoreStrings": true
}
]
}
"root":true,
"env": {
"node": true,
"commonjs": true,
"es2021": true
},
"extends": [
"airbnb-base"
],
"parserOptions": {
"ecmaVersion": 11
},
"rules": {
"no-useless-escape":0,
"no-restricted-syntax":0,
"no-console":0,
"no-nested-ternary":0,
"global-require":0,
"max-len": [
"error",
{
"code": 120,
"ignoreTemplateLiterals": true,
"ignoreStrings": true
}
]
}
}

@@ -11,6 +11,9 @@ const path = require('path');

backend: ['decorators', 'dtos', 'entities', 'enums', 'guards', 'interfaces', 'loaders', 'mappers', 'models', 'repositories', 'resolvers', 'services', 'strategies', 'scalars', 'schemas', 'config'],
frontend: ['components', 'configuration', 'layouts', 'pages', 'shared', 'slices', 'styles', 'utils', 'views'],
frontendCommon: ['interfaces', 'roq-hooks', 'roq-ui'],
frontend: ['configuration', 'layouts', 'modules', 'pages', 'scripts', 'styles', 'views'],
frontendCommon: ['components', 'errors', 'hooks', 'icons', 'interfaces', 'utils'],
frontendModule: ['components', 'actions', 'hocs', 'hooks', 'selectors', 'utils', 'interfaces', 'enums', 'schemas', 'types'],
};
const possibleFileExtensions = ['.ts', '.tsx', '.js'];
module.exports = {

@@ -21,4 +24,4 @@ sep: path.sep,

resourceIdentifiers,
frontendCommonList: resourceIdentifiers.frontendCommon,
moduleIdentifyingDirs,
possibleFileExtensions,
};

@@ -9,3 +9,3 @@ const path = require('path');

const getPathPatterns = (backendBasePattern, frontendBasePattern) => {
const pathPatterns = { backend: {}, frontend: {} };
const pathPatterns = { backend: {}, frontend: { moduleDir: { common: {} } } };
for (const resource of resourceIdentifiers.backend) {

@@ -20,4 +20,13 @@ pathPatterns.backend[resource] = [backendBasePattern, `${allowedNamingPattern}[${escapedSep}a-zA-Z0-9_-]+`, resource].join(escapedSep);

for (const resource of resourceIdentifiers.frontendCommon) {
pathPatterns.frontend[resource] = [frontendBasePattern, 'common', resource].join(escapedSep);
pathPatterns.frontend.moduleDir.common[resource] = [frontendBasePattern, 'modules', 'common', resource].join(escapedSep);
}
for (const resource of resourceIdentifiers.frontendModule) {
if (resource === 'components') {
pathPatterns.frontend.moduleDir[resource] = [frontendBasePattern, 'modules', allowedNamingPattern, resource, allowedNamingPattern].join(escapedSep);
} else {
pathPatterns.frontend.moduleDir[resource] = [frontendBasePattern, 'modules', allowedNamingPattern, resource].join(escapedSep);
}
}
return pathPatterns;

@@ -24,0 +33,0 @@ };

@@ -7,3 +7,3 @@ const path = require('path');

const names = {
camelCased: '', pascalCased: '', dotSeparated: '', hyphenSeparated: '',
camelCased: '', pascalCased: '', snakeCased: '', dotSeparated: '', hyphenSeparated: '',
};

@@ -15,3 +15,5 @@ for (let t = 0; t < array.length; t += 1) {

if (isDir && t === array.length - 1) { // required for directories
part = pluralize.singular(part);
if (part !== 'data') { // The singular form of data is datum, which looks like a word from an unknown world
part = pluralize.singular(part);
}
}

@@ -22,2 +24,3 @@

names.pascalCased += part.charAt(0).toUpperCase() + part.substring(1);
names.snakeCased += `${part}`;
names.dotSeparated += `${part}`;

@@ -28,2 +31,3 @@ names.hyphenSeparated += `${part}`;

names.pascalCased += part.charAt(0).toUpperCase() + part.substring(1);
names.snakeCased += `_${part}`;
names.dotSeparated += `.${part}`;

@@ -68,6 +72,5 @@ names.hyphenSeparated += `-${part}`;

type: {
camelCased: '', pascalCased: '', dotSeparated: '', hyphenSeparated: '',
camelCased: '', pascalCased: '', snakeCased: '', dotSeparated: '', hyphenSeparated: '',
},
};
const parentDirName = parentDir.absolutePath.split(sep).slice(-1)[0];

@@ -84,6 +87,6 @@

resourceType: {
camelCased: '', pascalCased: '', dotSeparated: '', hyphenSeparated: '',
camelCased: '', pascalCased: '', snakeCased: '', dotSeparated: '', hyphenSeparated: '',
},
resourceName: {
camelCased: '', pascalCased: '', dotSeparated: '', hyphenSeparated: '',
camelCased: '', pascalCased: '', snakeCased: '', dotSeparated: '', hyphenSeparated: '',
},

@@ -90,0 +93,0 @@ };

@@ -19,7 +19,6 @@ const pluginName = '@roq';

'imports-should-follow-conventions': require('./lib/rules/imports-should-follow-conventions'),
'exports-should-entity-decorator': require('./lib/rules/exports-should-entity-decorator'),
'entities-should-follow-conventions': require('./lib/rules/entities-should-follow-conventions'),
'define-http-status-enum': require('./lib/rules/define-http-status-enum'),
'exports-should-module-decorator': require('./lib/rules/exports-should-module-decorator'),
'exports-should-object-type-decorator': require('./lib/rules/exports-should-object-type-decorator'),
'correct-location-enums-types-interfaces': require('./lib/rules/correct-location-enums-types-interfaces'),
'only-layouts-location': require('./lib/rules/only-layouts-location'),

@@ -54,3 +53,3 @@ 'repository-correct-export-annotation': require('./lib/rules/repository-correct-export-annotation'),

[`${pluginName}/no-business-logic`]: 'error',
[`${pluginName}/exports-should-entity-decorator`]: 'error',
[`${pluginName}/entities-should-follow-conventions`]: 'error',
[`${pluginName}/only-interface-export`]: 'error',

@@ -79,3 +78,2 @@ [`${pluginName}/exports-should-scalar-decorator`]: 'error',

/* Rules specific to frontend to be added here */
[`${pluginName}/correct-location-enums-types-interfaces`]: 'error',
[`${pluginName}/only-layouts-location`]: 'error',

@@ -82,0 +80,0 @@ [`${pluginName}/view-correct-location-and-name`]: 'error',

const { fileContext, executionContext } = require('../../helper');
const {
allowedNamingPattern,
escapedSep,
} = require('../../constants');
/* what we want to cover
1) exported enums, components, interfaces should have the file-name prefixed to their name (IN PascalCased)
1) exported enums, components, interfaces should have the PascalCased file-name prefixed to their name
2) exported enum and interface names should be suffixed with their type(enum or interface)
3) exported constants in slices or actions should have the file-name prefixed to their name (IN camelCased)
3) exported constants in slices or actions should have the camelCased file-name prefixed to their name
*/

@@ -26,4 +30,4 @@

messages: {
invalidExportNamePrefix: 'Exported enums, components, interfaces should be prefixed with file name they are in (PascalCased)',
invalidExportNamePrefixConstants: 'Exported constants should be prefixed with file name they are in (camelCased)',
invalidExportNamePrefix: 'Exported enums, components, interfaces should be prefixed with PascalCased file name',
invalidExportNamePrefixConstants: 'Exported constants should be prefixed with camelCased file name',
invalidExportNameSuffix: 'Exported enums and interfaces should be suffixed with their type. (Interface/Enum)',

@@ -42,6 +46,10 @@ },

const isComponent = (new RegExp(pathPatterns.frontend['roq-ui'], 'g')).test(dirPath);
const isLayout = (new RegExp(pathPatterns.frontend.layouts, 'g')).test(dirPath);
const isSliceOrAction = (new RegExp(pathPatterns.frontend.slices, 'g')).test(dirPath)
&& (file.nameWithoutExt.endsWith('.slice') || file.nameWithoutExt.endsWith('.action'));
const isComponent = ([new RegExp(`${pathPatterns.frontend.moduleDir.common.components}$`),
new RegExp(`${pathPatterns.frontend.moduleDir.components}$`),
new RegExp(`${[pathPatterns.frontend.layouts, allowedNamingPattern, 'components', allowedNamingPattern].join(escapedSep)}$`),
].some((e) => e.test(dirPath))) && !file.nameWithoutExt.endsWith('.styles'); // styles files do not need to abide by this
// const isLayout = (new RegExp(pathPatterns.frontend.layouts, 'g')).test(dirPath);
const isLayout = (new RegExp(`${[pathPatterns.frontend.layouts, allowedNamingPattern].join(escapedSep)}$`)).test(dirPath) && !file.nameWithoutExt.endsWith('.styles'); // styles files do not need to abide by this
const isSliceOrAction = (file.nameWithoutExt.endsWith('.slice') || file.nameWithoutExt.endsWith('.action'));
const isIndexFile = file.name === 'index.ts' || file.name === 'index.tsx';

@@ -48,0 +56,0 @@

@@ -7,3 +7,3 @@ const fs = require('fs');

const ignoredResources = {
frontend: ['interfaces', 'roq-hooks', 'configuration', 'layouts', 'shared', 'styles', 'utils'],
frontend: ['modules', 'configuration', 'scripts', 'styles', 'views'],
backend: ['config'],

@@ -10,0 +10,0 @@ };

@@ -6,5 +6,4 @@ const {

const { fileContext, executionContext } = require('../../helper');
const ignoredResources = {
frontend: ['slices', 'styles', 'utils', 'shared', 'pages', 'roq-ui', 'configuration'],
frontend: ['pages', 'configuration', 'scripts'],
backend: [],

@@ -29,3 +28,3 @@ };

} else {
throw new Error('Invalid parent directory parameter either instance of parentDir from fileContext helper or a string allowed');
throw new Error('Invalid parent directory parameter. Either instance of parentDir from fileContext helper or a string is allowed');
}

@@ -78,5 +77,6 @@ let expectedFileName = [`${file.resourceName.hyphenSeparated}.${parentDir.type.dotSeparated}${file.extension}`];

const ignoredPaths = [new RegExp(`${backendBasePattern}$`),
new RegExp(`${frontendBasePattern}$`),
new RegExp(`${backendTestsPattern}`),
new RegExp(`^(${backendBasePattern}).*(${escapedSep}config)$`),
new RegExp(`${frontendBasePattern}$`),
new RegExp(`${backendTestsPattern}`),
new RegExp(`^(${backendBasePattern}).*(${escapedSep}config)$`),
new RegExp(`${pathPatterns.frontend.moduleDir.utils}$`),
];

@@ -102,12 +102,20 @@ for (const segment in ignoredResources) {

5) in dtos allow both "arg-type" and "dto" as suffix
6) in frontend components should have a suffix "component".
6) in frontend, components should have a suffix "component".
7) files in root of frontendModules should have a suffix "slice"
8) Components, Partials and Layouts can also have styles file.
*/
const isBackendModuleRoot = executionContext.isBackendModule(dirPath);
const isFrontendModuleRoot = (new RegExp(`${[pathPatterns.frontend.modules, allowedNamingPattern].join(escapedSep)}$`)).test(dirPath);
const isDTO = (new RegExp(pathPatterns.backend.dtos, 'g')).test(dirPath);
const isReactHook = new RegExp(`${[pathPatterns.frontend['roq-hooks'], allowedNamingPattern].join(escapedSep)}$`).test(dirPath);
const isReactHook = [new RegExp(`${[pathPatterns.frontend.layouts, allowedNamingPattern, 'hooks'].join(escapedSep)}$`), new RegExp(`${[pathPatterns.frontend.views, allowedNamingPattern, 'hooks'].join(escapedSep)}$`), new RegExp(`${[pathPatterns.frontend.moduleDir.common, 'hooks'].join(escapedSep)}$`), new RegExp(`${pathPatterns.frontend.moduleDir.hooks}$`)].some((e) => e.test(dirPath));
const isLayout = (new RegExp(`${[pathPatterns.frontend.layouts, allowedNamingPattern].join(escapedSep)}$`)).test(dirPath);
const isView = (new RegExp(`${[pathPatterns.frontend.views, allowedNamingPattern].join(escapedSep)}$`)).test(dirPath);
const isComponent = (new RegExp(`${[pathPatterns.frontend.components, allowedNamingPattern].join(escapedSep)}$`)).test(dirPath);
const isComponent = [new RegExp(`${[pathPatterns.frontend.moduleDir.common.components, allowedNamingPattern].join(escapedSep)}$`),
new RegExp(`${pathPatterns.frontend.moduleDir.components}$`),
new RegExp(`${[pathPatterns.frontend.layouts, allowedNamingPattern, 'components', allowedNamingPattern].join(escapedSep)}$`),
].some((e) => e.test(dirPath));
const isPartial = [new RegExp(`${[pathPatterns.frontend.views, allowedNamingPattern, 'partials'].join(escapedSep)}$`)].some((e) => e.test(dirPath));
if (isComponent) {

@@ -117,6 +125,30 @@ parentType = 'component';

mismatchType = suffixCheck.mismatchType;
if (mismatchType) {
if (mismatchType === 2) { // we can also expect styles files
const stylesSuffixCheck = checkSuffixMismatch(file, 'styles');
if (stylesSuffixCheck.mismatchType) {
mismatchType = stylesSuffixCheck.mismatchType;
expectedFileName = [...stylesSuffixCheck.expectedFileName, ...(stylesSuffixCheck.expectedFileName.map((e) => e.replace('styles', 'component')))];
} else {
mismatchType = 0;
}
} else if (mismatchType) {
expectedFileName = suffixCheck.expectedFileName;
isExceptionalMismatch = true;
}
} else if (isPartial) {
parentType = 'partial';
const suffixCheck = checkSuffixMismatch(file, parentType);
mismatchType = suffixCheck.mismatchType;
if (mismatchType === 2) { // we can also expect styles files
const stylesSuffixCheck = checkSuffixMismatch(file, 'styles');
if (stylesSuffixCheck.mismatchType) {
mismatchType = stylesSuffixCheck.mismatchType;
expectedFileName = [...stylesSuffixCheck.expectedFileName, ...(stylesSuffixCheck.expectedFileName.map((e) => e.replace('styles', 'partial')))];
} else {
mismatchType = 0;
}
} else if (mismatchType) {
expectedFileName = suffixCheck.expectedFileName;
isExceptionalMismatch = true;
}
} else if (isBackendModuleRoot) { // type has to be module

@@ -142,3 +174,12 @@ parentType = 'module';

mismatchType = layoutSuffixCheck.mismatchType;
if (mismatchType !== 0) {
if (mismatchType === 2) { // we can also expect styles files
const stylesSuffixCheck = checkSuffixMismatch(file, 'styles');
if (stylesSuffixCheck.mismatchType) {
mismatchType = stylesSuffixCheck.mismatchType;
expectedFileName = [...stylesSuffixCheck.expectedFileName, ...(stylesSuffixCheck.expectedFileName.map((e) => e.replace('styles', 'layout')))];
} else {
mismatchType = 0;
}
} else if (mismatchType) {
expectedFileName = layoutSuffixCheck.expectedFileName;

@@ -155,2 +196,10 @@ isExceptionalMismatch = true;

}
} else if (isFrontendModuleRoot) {
parentType = 'slice';
const suffixCheck = checkSuffixMismatch(file, parentType);
mismatchType = suffixCheck.mismatchType;
if (mismatchType) {
expectedFileName = suffixCheck.expectedFileName;
isExceptionalMismatch = true;
}
} else if (isDTO && mismatchType !== 0 && mismatchType !== 1) {

@@ -157,0 +206,0 @@ // if not 'dto' then we can only have argType suffix

@@ -0,1 +1,6 @@

const fs = require('fs');
const {
possibleFileExtensions,
} = require('../../constants');
module.exports = {

@@ -30,2 +35,25 @@ meta: {

}
if (node.source && node.source.value) {
const pathToImportFrom = node.source.value;
let basePathStats = null;
try { basePathStats = fs.statSync(pathToImportFrom); } catch (err) { /* DO NOTHING */ }
let isFile = false;
if (basePathStats && basePathStats.isFile()) {
isFile = true;
} else if (!basePathStats) {
for (const ext of possibleFileExtensions) {
try { basePathStats = fs.statSync(pathToImportFrom + ext); } catch (err) { /* DO NOTHING */ }
if (basePathStats) {
isFile = true;
break;
}
}
}
if (isFile) {
context.report({
node,
message: 'Imports should happen from a shared resource directory instead of directly from the resource file.',
});
}
}
},

@@ -32,0 +60,0 @@ };

@@ -12,6 +12,5 @@ const {

missingComponentPrefix: 'Filename should be prefixed with the component name. Possibly could be renamed to one of the following :\n {{expectedFileName}}',
typeDetectedInFileName: 'Types are not expected for common-component filenames. Found {{type}}',
},
docs: {
description: 'Checks that the filenames are prefixed with component name and do not have a type as in our general naming convention. So they should have a pattern : {any-component}-{sub-view}.tsx',
description: 'Checks that the filenames are prefixed with the name of the component. So they should have a pattern : {any-component}-{sub-view}.tsx',
category: 'Stylistic Issues',

@@ -36,24 +35,23 @@ url: 'https://app.archbee.io/public/EpeZApNOPw_vb0lzacxnR/xpch-no-invalid-common-component-naming',

const dirPath = parentDir.absolutePath;
const isCommonComponent = new RegExp(`${[pathPatterns.frontend['roq-ui'], allowedNamingPattern].join(escapedSep)}$`).test(dirPath) && file.extension === '.tsx' && file.nameWithoutExt !== 'index';
const isCommonComponent = new RegExp(`${[pathPatterns.frontend.moduleDir.common.components, allowedNamingPattern].join(escapedSep)}$`).test(dirPath) && file.extension === '.tsx' && file.nameWithoutExt !== 'index';
const reportingObj = {};
if (isCommonComponent && !file.name.startsWith(parentDir.type.hyphenSeparated)) {
if (isCommonComponent) {
reportingObj.Program = (node) => {
if (!relatedComponents || !relatedComponents.includes(parentDir.absolutePath.split(sep).slice(-1)[0])) {
context.report({
node,
messageId: 'missingComponentPrefix',
data: {
expectedFileName: `${parentDir.type.hyphenSeparated}-${file.name}`,
},
});
if (!file.name.startsWith(parentDir.type.hyphenSeparated)) {
let reportMissingComponentPrefix = true;
if (relatedComponents
&& relatedComponents.includes(parentDir.absolutePath.split(sep).slice(-1)[0])) {
reportMissingComponentPrefix = false;
}
if (reportMissingComponentPrefix) {
context.report({
node,
messageId: 'missingComponentPrefix',
data: {
expectedFileName: `${parentDir.type.hyphenSeparated}-${file.name}`,
},
});
}
}
if (file.resourceType.dotSeparated !== '') { // if type is found in filename, it should be reported
context.report({
node,
messageId: 'typeDetectedInFileName',
data: {
type: file.resourceType.dotSeparated,
},
});
}
};

@@ -60,0 +58,0 @@ }

@@ -34,3 +34,3 @@ const {

const isHook = [new RegExp(`${[pathPatterns.frontend['roq-hooks'], allowedNamingPattern].join(escapedSep)}$`), new RegExp(`${[pathPatterns.frontend.components, allowedNamingPattern, 'hooks'].join(escapedSep)}$`)].some((e) => e.test(dirPath));
const isHook = [new RegExp(`${[pathPatterns.frontend.layouts, allowedNamingPattern, 'hooks'].join(escapedSep)}$`), new RegExp(`${[pathPatterns.frontend.views, allowedNamingPattern, 'hooks'].join(escapedSep)}$`), new RegExp(`${[pathPatterns.frontend.moduleDir.common, 'hooks'].join(escapedSep)}$`), new RegExp(`${pathPatterns.frontend.moduleDir.hooks}$`)].some((e) => e.test(dirPath));
const isIndexFile = file.name === 'index.ts' || file.name === 'index.tsx';

@@ -53,3 +53,3 @@ const fileIsUsePrefixed = file.name.startsWith('use-');

reportingObj.ExportNamedDeclaration = (node) => {
if (node.declaration.type === 'VariableDeclaration' && node.declaration.kind === 'const' && node.declaration.declarations
if (node.declaration && node.declaration.type === 'VariableDeclaration' && node.declaration.kind === 'const' && node.declaration.declarations
&& node.declaration.declarations[0]

@@ -56,0 +56,0 @@ && node.declaration.declarations[0].id.name !== expectedHookName) {

@@ -8,3 +8,3 @@ const {

/* what we want to cover
1) default export name should match the directory name and suffixed with 'Page'.
1) The page should default export a view whose name should match the directory name and should be suffixed with 'View'.
*/

@@ -15,3 +15,3 @@ module.exports = {

docs: {
description: 'Default page exports should be named like [{DirectoryName}Page]',
description: 'Default page exports should be named like [{DirectoryName}View]',
category: 'Stylistic Issues',

@@ -21,4 +21,4 @@ url: 'https://app.archbee.io/public/EpeZApNOPw_vb0lzacxnR/OcQv-no-invalid-page-resource',

messages: {
missingOrExtraDefaultExport: 'Exactly one default export is expected, named as {{expectedPageName}}',
invalidPageResource: 'Default Export name should match parent directory name (when pascalCased). Expected {{expectedPageName}}',
missingOrExtraDefaultExport: 'Exactly one default export is expected, named as {{expectedViewName}}',
invalidPageResource: 'Default Export name should match parent directory name (when pascalCased) and suffixed with "View". Expected {{expectedViewName}}',
invalidNestedPageResource: 'Default Export name in case of nested pages, can only include directory names in order (when pascalCased, ignoring directory names enclosed in []).',

@@ -41,6 +41,8 @@ },

const isIndexFile = file.name === 'index.tsx';
const isHomePage = new RegExp(`${pathPatterns.frontend.pages}$`).test(dirPath);
const ruleExecutionRequired = isPage && !ignoredPath && isIndexFile;
const expectedPageName = `${parentDir.rawType.pascalCased}Page`;
let expectedViewName = `${parentDir.rawType.pascalCased}View`;
if (ruleExecutionRequired) {
expectedViewName = isHomePage ? 'HomeView' : expectedViewName;
const checkPagesDirIndex = (name) => name === 'pages';

@@ -64,3 +66,3 @@ const pagesDirIndex = dirPathParts.findIndex(checkPagesDirIndex);

data: {
expectedPageName,
expectedViewName,
},

@@ -79,7 +81,7 @@ });

});
orderedDirectorySequence.push('Page'); // this is for efficient name matching
orderedDirectorySequence.push('View'); // this is for efficient name matching
const exportedPageName = defaultDeclarations[0].declaration.name;
const wordsInExportedName = exportedPageName.split(/(?=[A-Z])/);
let previousWordAtIndex = 0;
const isInvalidName = !exportedPageName.endsWith('Page') || wordsInExportedName.length <= 1 || wordsInExportedName.some((word) => {
const isInvalidName = !exportedPageName.endsWith('View') || wordsInExportedName.length <= 1 || wordsInExportedName.some((word) => {
previousWordAtIndex = orderedDirectorySequence.indexOf(word, previousWordAtIndex);

@@ -94,3 +96,3 @@ return previousWordAtIndex === -1;

}
} else if (defaultDeclarations[0].declaration.name !== expectedPageName) {
} else if (defaultDeclarations[0].declaration.name !== expectedViewName) {
context.report({

@@ -100,3 +102,3 @@ node: defaultDeclarations[0],

data: {
expectedPageName,
expectedViewName,
},

@@ -103,0 +105,0 @@ });

@@ -0,3 +1,8 @@

const fs = require('fs');
const path = require('path');
const { fileContext, executionContext } = require('../../helper');
const { escapedSep } = require('../../constants');
const dirsChecked = [];
module.exports = {

@@ -7,6 +12,9 @@ meta: {

docs: {
description: 'This location should have only layouts',
description: 'This location should contain layout',
category: 'Best Practices',
url: 'https://app.archbee.io/public/EpeZApNOPw_vb0lzacxnR/A6wK-only-layouts-location',
},
messages: {
missingLayoutFile: 'Missing {{ layoutFileName }} in {{ dirPath }}.',
},
schema: [], // no options

@@ -19,3 +27,6 @@ },

const { file, parentDir } = fileContext.get(context);
const isLayout = (new RegExp(pathPatterns.frontend.layouts, 'g')).test(parentDir.absolutePath);
const layoutDirPattern = [pathPatterns.frontend.layouts, '\\w+'].join(escapedSep);
const isLayout = (new RegExp(`${layoutDirPattern}$`, 'g')).test(parentDir.absolutePath);
const dirPath = path.dirname(file.absolutePath);
const layoutFileName = `${path.basename(dirPath)}.layout.tsx`;

@@ -25,20 +36,27 @@ if (isLayout && file.name !== 'index.ts') {

Program(node) {
if (!file.name.includes('layout')) {
context.report({
node,
message: 'This location should have only layouts',
data: {
dir: parentDir.absolutePath,
},
});
if (!dirsChecked.includes(dirPath)) {
const filesInDir = fs.readdirSync(dirPath);
if (!filesInDir.includes(layoutFileName)) {
context.report({
node,
messageId: 'missingLayoutFile',
data: {
layoutFileName,
dirPath,
},
});
}
dirsChecked.push(dirPath);
}
for (const item of node.body) {
if (item.declaration && item.declaration.declarations && item.type === 'ExportNamedDeclaration') {
if (!item.declaration.declarations[0].id.name.toLowerCase().includes('layout')) {
const declarationLocation = item.declaration.declarations[0].id.loc;
context.report({
loc: declarationLocation,
message: 'This location should have only layouts',
});
if (file.name === layoutFileName) {
for (const item of node.body) {
if (item.declaration && item.declaration.declarations && item.type === 'ExportNamedDeclaration') {
if (!item.declaration.declarations[0].id.name.toLowerCase().includes('layout')) {
const declarationLocation = item.declaration.declarations[0].id.loc;
context.report({
loc: declarationLocation,
message: 'This file should export only layout',
});
}
}

@@ -45,0 +63,0 @@ }

@@ -12,21 +12,41 @@ module.exports = {

create(context) {
let parseUUIDPipeImportToken = null;
let programNode = null;
return {
Program(node) {
programNode = node;
},
ImportDeclaration(node) {
const { specifiers } = node;
specifiers.forEach((e) => {
if (e.parent && e.parent.source && e.parent.source.value && e.parent.source.value === '@nestjs/common' && e.imported && e.imported.name === 'ParseUUIDPipe' && e.local && e.local.name) {
parseUUIDPipeImportToken = e.local.name;
}
});
},
MethodDefinition(node) {
if (node.parent && node.parent.type === 'ClassBody' && node.parent.parent.decorators
&& node.parent.parent.decorators[0].expression.type === 'CallExpression'
&& node.parent.parent.decorators[0].expression.callee.name === 'Resolver'
&& node.value.type === 'FunctionExpression' && node.value.params.length
&& node.parent.parent.decorators[0].expression.type === 'CallExpression'
&& node.parent.parent.decorators[0].expression.callee.name === 'Resolver'
&& node.value.type === 'FunctionExpression' && node.value.params.length
) {
const param = node.value.params.find((par) => par.name === 'id');
if (param) {
const { decorators } = node.value.params[0];
if (decorators && decorators.length) {
const args = decorators[0].expression.arguments;
const exist = args.find((a) => a.type === 'Identifier' && a.name === 'ParseUUIDPipe');
if (!exist) {
context.report({
node,
loc: decorators[0].expression.callee.loc,
message: 'ParseUUIDPipe is not found in @Args decorators',
});
const idParamIndex = node.value.params.findIndex((par) => par.name === 'id');
if (idParamIndex !== -1) {
if (!parseUUIDPipeImportToken) {
context.report({
node: programNode,
message: 'Missing/Invalid Destructured Import for ParseUUIDPipe from @nestjs/common',
});
} else {
const { decorators } = node.value.params[idParamIndex];
if (decorators && decorators.length) {
const args = decorators[0].expression.arguments;
const exist = args.find((a) => a.type === 'Identifier' && a.name === parseUUIDPipeImportToken);
if (!exist) {
context.report({
node,
loc: decorators[0].expression.callee.loc,
message: 'ParseUUIDPipe needs to be the second param in the @Args decorator',
});
}
}

@@ -33,0 +53,0 @@ }

@@ -12,3 +12,3 @@ const {

description: 'All views should be placed in this (frontend/src/views/{view-name}/{view-name}.view.tsx) location.'
+ ' Exported function component name should match the view file mame ',
+ ' Exported function component name should match the view file mame and should be the only export from the view ',
category: 'Best Practices',

@@ -26,10 +26,13 @@ url: 'https://app.archbee.io/public/EpeZApNOPw_vb0lzacxnR/ByIC-view-correct-location-and-name',

const isInViewFolder = new RegExp(`${[pathPatterns.frontend.views, allowedNamingPattern].join(escapedSep)}$`, 'g').test(parentDir.absolutePath);
const isExportedDeclarationAFunction = (item) => item.declaration.declarations[0] && item.declaration.declarations[0].type === 'VariableDeclarator' && item.declaration.declarations[0].init && item.declaration.declarations[0].init.type === 'CallExpression';
return {
Program(node) {
if (isView && !isInViewFolder) {
context.report({
node,
message: 'All views should be placed in this location (frontend/src/views/{view-name}/{view-name}.view.tsx)',
});
if (isView) {
if (!isInViewFolder) {
context.report({
node,
message: 'All views should be placed in this location (frontend/src/views/{view-name}/{view-name}.view.tsx)',
});
}
}

@@ -39,8 +42,14 @@ },

if (isView && node.declaration.type === 'VariableDeclaration') {
const exportedMemberIsAFunction = isExportedDeclarationAFunction(node);
const { name, loc } = node.declaration.declarations[0].id;
const fileNameWithExport = file.name.replace(/[-.]/gi, '').slice(0, -3);
const { name, loc } = node.declaration.declarations[0].id;
if (name.toLowerCase() !== fileNameWithExport) {
if (!exportedMemberIsAFunction) {
context.report({
loc,
message: 'Views should only have a single export and that should be the function component',
});
} else if (name.toLowerCase() !== fileNameWithExport) {
context.report({
loc,
message: 'Exported function component name should match the file name',

@@ -47,0 +56,0 @@ });

{
"name": "@roq/eslint-plugin",
"version": "1.1.1",
"version": "1.1.2",
"main": "index.js",

@@ -5,0 +5,0 @@ "scripts": {

@@ -20,11 +20,11 @@ const { RuleTester } = require('eslint');

code: 'export enum AlertIconEnum {}',
filename: 'frontend/src/common/roq-ui/alert/alert-icon.tsx',
filename: 'frontend/src/modules/common/components/alert-icon/alert-icon.component.tsx',
},
{
code: 'export interface AlertIconInterface {}',
filename: 'frontend/src/common/roq-ui/alert/alert-icon.tsx',
filename: 'frontend/src/modules/common/components/alert-icon/alert-icon.component.tsx',
},
{
code: 'export const AlertIcon: FunctionComponent = () => {}',
filename: 'frontend/src/common/roq-ui/alert/alert-icon.tsx',
filename: 'frontend/src/modules/common/components/alert-icon/alert-icon.component.tsx',
},

@@ -39,4 +39,4 @@ {

code: 'export enum AlertEnum {}',
errors: [{ message: 'Exported enums, components, interfaces should be prefixed with file name they are in (PascalCased)' }],
filename: 'frontend/src/common/roq-ui/alert/alert-icon.tsx',
errors: [{ message: 'Exported enums, components, interfaces should be prefixed with PascalCased file name' }],
filename: 'frontend/src/modules/common/components/alert/alert-icon.component.tsx',
},

@@ -46,12 +46,12 @@ {

errors: [{ message: 'Exported enums and interfaces should be suffixed with their type. (Interface/Enum)' }],
filename: 'frontend/src/common/roq-ui/alert/alert-icon.tsx',
filename: 'frontend/src/modules/common/components/alert/alert-icon.component.tsx',
},
{
code: 'export const Alerticon: FunctionComponent = () => {}',
errors: [{ message: 'Exported enums, components, interfaces should be prefixed with file name they are in (PascalCased)' }],
filename: 'frontend/src/common/roq-ui/alert/alert-icon.tsx',
errors: [{ message: 'Exported enums, components, interfaces should be prefixed with PascalCased file name' }],
filename: 'frontend/src/modules/common/components/alert/alert-icon.component.tsx',
},
{
code: 'export const AccountAction = createAsyncThunk()',
errors: [{ message: 'Exported constants should be prefixed with file name they are in (camelCased)' }],
errors: [{ message: 'Exported constants should be prefixed with camelCased file name' }],
filename: 'frontend/src/slices/auth-management/actions/account-activate.action.ts',

@@ -58,0 +58,0 @@ },

@@ -8,3 +8,3 @@ const { RuleTester } = require('eslint');

valid: [
'import SomeComponent from "/common/components" ',
'import SomeComponent from "common/components" ',
'import Something from "layout"',

@@ -46,3 +46,25 @@ ],

},
{
code: 'import Something from "tests/dummies/backend/auth/dtos/sample.ts"',
errors: [{
message: 'Imports should happen from a shared resource directory instead of directly from the resource file.',
line: 1,
column: 1,
endLine: 1,
endColumn: 66,
}],
filename: 'sample.ts',
},
{
code: 'import Something from "tests/dummies/backend/auth/dtos/sample"',
errors: [{
message: 'Imports should happen from a shared resource directory instead of directly from the resource file.',
line: 1,
column: 1,
endLine: 1,
endColumn: 63,
}],
filename: 'sample.ts',
},
],
});

@@ -18,8 +18,8 @@ const { RuleTester } = require('eslint');

{
code: '// File Path : frontend/src/common/roq-ui/time-picker/time-picker-item.tsx',
filename: 'frontend/src/common/roq-ui/time-picker/time-picker-item.tsx',
code: '// File Path : frontend/src/modules/common/time-picker/time-picker-item.tsx',
filename: 'frontend/src/modules/common/components/time-picker/time-picker-item.tsx',
},
{
code: '// With options [\'select\'] File Path : frontend/src/common/roq-ui/select/option-group.tsx',
filename: 'frontend/src/common/roq-ui/select/option-group.tsx',
code: '// With options [\'select\'] File Path : frontend/src/modules/common/select/option-group.tsx',
filename: 'frontend/src/modules/common/components/select/option-group.tsx',
options: [['select']],

@@ -31,3 +31,3 @@ },

{
code: '// File Path : frontend/src/common/roq-ui/time-picker/picker-item.tsx',
code: '// File Path : frontend/src/modules/common/components/time-picker/picker-item.tsx',
errors: [{

@@ -41,6 +41,6 @@ messageId: 'missingComponentPrefix',

endLine: 1,
endColumn: 70,
endColumn: 82,
},
],
filename: 'frontend/src/common/roq-ui/time-picker/picker-item.tsx',
filename: 'frontend/src/modules/common/components/time-picker/picker-item.tsx',
},

@@ -61,22 +61,5 @@ {

],
filename: 'frontend/src/common/roq-ui/select/option-group.tsx',
filename: 'frontend/src/modules/common/components/select/option-group.tsx',
},
{
code: '// With options [\'select\'] File Path : frontend/src/common/roq-ui/select/option.group.tsx',
errors: [
{
messageId: 'typeDetectedInFileName',
data: {
type: 'group',
},
line: 1,
column: 1,
endLine: 1,
endColumn: 90,
},
],
filename: 'frontend/src/common/roq-ui/select/option.group.tsx',
options: [['select']],
},
],
});

@@ -19,3 +19,3 @@ const { RuleTester } = require('eslint');

code: 'export const useAuth = ()=>{/* definition */}',
filename: 'frontend/src/common/roq-hooks/use-auth/use-auth.hook.ts',
filename: 'frontend/src/layouts/auth/hooks/use-auth.hook.ts',
},

@@ -38,3 +38,3 @@ {

],
filename: 'frontend/src/common/roq-hooks/use-auth/use-auth.hook.ts',
filename: 'frontend/src/layouts/auth/hooks/use-auth.hook.ts',
},

@@ -52,3 +52,3 @@ {

],
filename: 'frontend/src/components/notifications/hooks/notif-provider.hook.ts',
filename: 'frontend/src/modules/notifications/hooks/notif-provider.hook.ts',
},

@@ -76,5 +76,5 @@ {

],
filename: 'frontend/src/components/notifications/hooks/notif-provider.hook.ts',
filename: 'frontend/src/modules/notifications/hooks/notif-provider.hook.ts',
},
],
});

@@ -19,3 +19,3 @@ const { RuleTester } = require('eslint');

code: `/* accountActivatePage definition */
export default AccountActivatePage;`,
export default AccountActivateView;`,
filename: 'frontend/src/pages/account-activate/index.tsx',

@@ -25,3 +25,3 @@ },

code: `/* LoginPage definition */
export default LoginPage;`,
export default LoginView;`,
filename: 'frontend/src/pages/login/index.tsx',

@@ -31,3 +31,3 @@ },

code: `/* VerifyEmailPage definition */
export default VerifyEmailPage;`,
export default VerifyEmailView;`,
filename: 'frontend/src/pages/verify-email/index.tsx',

@@ -37,3 +37,3 @@ },

code: `/* UsersEditPage definition */
export default UsersEditPage;`,
export default UsersEditView;`,
filename: 'frontend/src/pages/users/edit/[id]/index.tsx',

@@ -43,3 +43,3 @@ },

code: `/* UsersCreateNewTestPage definition */
export default UsersCreateNewTestPage;`,
export default UsersCreateNewTestView;`,
filename: 'frontend/src/pages/users/create/new/test/index.tsx',

@@ -56,3 +56,3 @@ },

data: {
expectedPageName: 'AccountActivatePage',
expectedViewName: 'AccountActivateView',
},

@@ -74,3 +74,3 @@ line: 2,

data: {
expectedPageName: 'AccountActivatePage',
expectedViewName: 'AccountActivateView',
},

@@ -87,3 +87,3 @@ line: 1,

code: `/* verifyEmailPage definition */
export default verifyEmailPage;`,
export default verifyEmailView;`,
errors: [

@@ -93,3 +93,3 @@ {

data: {
expectedPageName: 'VerifyEmailPage',
expectedViewName: 'VerifyEmailView',
},

@@ -106,3 +106,3 @@ line: 2,

code: `/* UsersEditPage definition */
export default AnyOtherDirNamePage;`,
export default AnyOtherDirNameView;`,
errors: [

@@ -121,3 +121,3 @@ {

code: `/* TestUsersPage definition */
export default TestUsersPage;`,
export default TestUsersView;`,
errors: [

@@ -124,0 +124,0 @@ {

const { RuleTester } = require('eslint');
const ruleUnderTest = require('../../../lib/rules/only-layouts-location');
const { resolve } = require('path');
const testDummiesBasePath = resolve('./tests/dummies');
const ruleTesterInstance = new RuleTester({

@@ -20,3 +23,3 @@ parserOptions: { ecmaVersion: 2021 },

code: 'export const AuthLayout: FunctionComponent<> = () => {}',
filename: 'frontend/src/layouts/auth/auth.layout.tsx',
filename: resolve(testDummiesBasePath, 'frontend/src/layouts/auth/auth.layout.tsx'),
},

@@ -27,11 +30,17 @@ ],

code: 'export const Auth: FunctionComponent<> = () => {}',
errors: [{ message: 'This location should have only layouts' }],
filename: 'frontend/src/layouts/auth/auth.layout.tsx',
errors: [{ message: 'This file should export only layout' }],
filename: resolve(testDummiesBasePath, 'frontend/src/layouts/auth/auth.layout.tsx'),
},
{
code: 'export const AuthLayout: FunctionComponent<> = () => {}',
errors: [{ message: 'This location should have only layouts' }],
filename: 'frontend/src/layouts/auth/auth.tsx',
code: 'export const MainLayout: FunctionComponent<> = () => {}',
errors: [{
messageId: 'missingLayoutFile',
data: {
layoutFileName: 'main.layout.tsx',
dirPath: resolve(testDummiesBasePath, 'frontend/src/layouts/main'),
},
}],
filename: resolve(testDummiesBasePath, 'frontend/src/layouts/main/main.tsx'),
},
],
});

@@ -14,8 +14,21 @@ const { RuleTester } = require('eslint');

code: `
@Resolver(() => UserModel)
class UserResolver {
async user(@Args({ name: 'id', type: () => String }, ParseUUIDPipe) id: string) {
}
}
import { ParseUUIDPipe } from '@nestjs/common';
@Resolver(() => UserModel)
class UserResolver {
async user(
@Args({ name: 'randomNumber', type: () => Number }) random: number,
@Args({ name: 'id', type: () => String }, ParseUUIDPipe) id: string) {}
}
`,
},
{
code: `
import { ParseUUIDPipe as idParsePipe } from '@nestjs/common';
@Resolver(() => UserModel)
class UserResolver {
async user(@Args({ name: 'id', type: () => String }, idParsePipe) id: string) {}
}
`,
}],

@@ -25,12 +38,23 @@ invalid: [

code: `
@Resolver(() => UserModel)
class UserResolver {
async user(@Args({ name: 'id', type: () => String }) id: string) {
import { ParseUUIDPipe } from '@nestjs/common';
}
}
@Resolver(() => UserModel)
class UserResolver {
async user(@Args({ name: 'id', type: () => String }) id: string) {}
}
`,
errors: [{ message: 'ParseUUIDPipe is not found in @Args decorators' }],
errors: [{ message: 'ParseUUIDPipe needs to be the second param in the @Args decorator' }],
},
{
code: `
import { ParseUUIDPipe } from 'some-other-package';
@Resolver(() => UserModel)
class UserResolver {
async user(@Args({ name: 'id', type: () => String }) id: string) {}
}
`,
errors: [{ message: 'Missing/Invalid Destructured Import for ParseUUIDPipe from @nestjs/common' }],
},
],
});

@@ -19,13 +19,9 @@ const { RuleTester } = require('eslint');

{
code: '',
code: 'export const UsersView = withAuth()(() => {});',
filename: 'frontend/src/views/users/users.view.tsx',
},
{
code: 'export const UsersView: FunctionComponent = () => {}',
filename: 'frontend/src/views/users/users.view.tsx',
},
],
invalid: [
{
code: 'export const Users: FunctionComponent = () => {}',
code: 'export const Users = withAuth()(() => {}); // File Path : frontend/src/views/users/users.view.tsx',
errors: [{ message: 'Exported function component name should match the file name' }],

@@ -35,7 +31,17 @@ filename: 'frontend/src/views/users/users.view.tsx',

{
code: '',
code: 'export const UsersView = withAuth()(() => {}); // File Path : frontend/src/utils/users.view.tsx',
errors: [{ message: 'All views should be placed in this location (frontend/src/views/{view-name}/{view-name}.view.tsx)' }],
filename: 'frontend/src/utils/users.view.tsx',
},
{
code: `export const breadcrumbs = [ { label: "home", href: "/", translate: true } ];
export const UsersView = withAuth()(() => {}); // File Path : frontend/src/utils/users.view.tsx`,
errors: [
{ message: 'All views should be placed in this location (frontend/src/views/{view-name}/{view-name}.view.tsx)' },
{ message: 'Views should only have a single export and that should be the function component' },
],
filename: 'frontend/src/utils/users.view.tsx',
},
],
});

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc