Files
dspace-angular/lint/src/rules/ts/themed-component-selectors.ts
2024-03-29 10:46:20 +01:00

258 lines
6.9 KiB
TypeScript

/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import {
ESLintUtils,
TSESLint,
TSESTree,
} from '@typescript-eslint/utils';
import { fixture } from '../../../test/fixture';
import { getComponentSelectorNode } from '../../util/angular';
import { stringLiteral } from '../../util/misc';
import { DSpaceESLintRuleInfo } from '../../util/structure';
import {
inThemedComponentOverrideFile,
isThemeableComponent,
isThemedComponentWrapper,
} from '../../util/theme-support';
import { getFilename } from '../../util/typescript';
export enum Message {
BASE = 'wrongSelectorUnthemedComponent',
WRAPPER = 'wrongSelectorThemedComponentWrapper',
THEMED = 'wrongSelectorThemedComponentOverride',
}
export const info = {
name: 'themed-component-selectors',
meta: {
docs: {
description: `Themeable component selectors should follow the DSpace convention
Each themeable component is comprised of a base component, a wrapper component and any number of themed components
- Base components should have a selector starting with \`ds-base-\`
- Themed components should have a selector starting with \`ds-themed-\`
- Wrapper components should have a selector starting with \`ds-\`, but not \`ds-base-\` or \`ds-themed-\`
- This is the regular DSpace selector prefix
- **When making a regular component themeable, its selector prefix should be changed to \`ds-base-\`, and the new wrapper's component should reuse the previous selector**
Unit tests are exempt from this rule, because they may redefine components using the same class name as other themeable components elsewhere in the source.
`,
},
type: 'problem',
schema: [],
fixable: 'code',
messages: {
[Message.BASE]: 'Unthemed version of themeable component should have a selector starting with \'ds-base-\'',
[Message.WRAPPER]: 'Themed component wrapper of themeable component shouldn\'t have a selector starting with \'ds-themed-\'',
[Message.THEMED]: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'',
},
},
defaultOptions: [],
} as DSpaceESLintRuleInfo;
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
create(context: TSESLint.RuleContext<Message, unknown[]>) {
const filename = getFilename(context);
if (filename.endsWith('.spec.ts')) {
return {};
}
function enforceWrapperSelector(selectorNode: TSESTree.StringLiteral) {
if (selectorNode?.value.startsWith('ds-themed-')) {
context.report({
messageId: Message.WRAPPER,
node: selectorNode,
fix(fixer) {
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-themed-', 'ds-')));
},
});
}
}
function enforceBaseSelector(selectorNode: TSESTree.StringLiteral) {
if (!selectorNode?.value.startsWith('ds-base-')) {
context.report({
messageId: Message.BASE,
node: selectorNode,
fix(fixer) {
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-base-')));
},
});
}
}
function enforceThemedSelector(selectorNode: TSESTree.StringLiteral) {
if (!selectorNode?.value.startsWith('ds-themed-')) {
context.report({
messageId: Message.THEMED,
node: selectorNode,
fix(fixer) {
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-themed-')));
},
});
}
}
return {
'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: TSESTree.Decorator) {
const selectorNode = getComponentSelectorNode(node);
if (selectorNode === undefined) {
return;
}
const selector = selectorNode?.value;
const classNode = node.parent as TSESTree.ClassDeclaration;
const className = classNode.id?.name;
if (selector === undefined || className === undefined) {
return;
}
if (isThemedComponentWrapper(node)) {
enforceWrapperSelector(selectorNode);
} else if (inThemedComponentOverrideFile(filename)) {
enforceThemedSelector(selectorNode);
} else if (isThemeableComponent(className)) {
enforceBaseSelector(selectorNode);
}
},
};
},
});
export const tests = {
plugin: info.name,
valid: [
{
name: 'Regular non-themeable component selector',
code: `
@Component({
selector: 'ds-something',
})
class Something {
}
`,
},
{
name: 'Themeable component selector should replace the original version, unthemed version should be changed to ds-base-',
code: `
@Component({
selector: 'ds-base-something',
})
class Something {
}
@Component({
selector: 'ds-something',
})
class ThemedSomething extends ThemedComponent<Something> {
}
@Component({
selector: 'ds-themed-something',
})
class OverrideSomething extends Something {
}
`,
},
{
name: 'Other themed component wrappers should not interfere',
code: `
@Component({
selector: 'ds-something',
})
class Something {
}
@Component({
selector: 'ds-something-else',
})
class ThemedSomethingElse extends ThemedComponent<SomethingElse> {
}
`,
},
],
invalid: [
{
name: 'Wrong selector for base component',
filename: fixture('src/app/test/test-themeable.component.ts'),
code: `
@Component({
selector: 'ds-something',
})
class TestThemeableComponent {
}
`,
errors: [
{
messageId: Message.BASE,
},
],
output: `
@Component({
selector: 'ds-base-something',
})
class TestThemeableComponent {
}
`,
},
{
name: 'Wrong selector for wrapper component',
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
code: `
@Component({
selector: 'ds-themed-something',
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
errors: [
{
messageId: Message.WRAPPER,
},
],
output: `
@Component({
selector: 'ds-something',
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
},
{
name: 'Wrong selector for theme override',
filename: fixture('src/themes/test/app/test/test-themeable.component.ts'),
code: `
@Component({
selector: 'ds-something',
})
class TestThememeableComponent extends BaseComponent {
}
`,
errors: [
{
messageId: Message.THEMED,
},
],
output: `
@Component({
selector: 'ds-themed-something',
})
class TestThememeableComponent extends BaseComponent {
}
`,
},
],
};
export default rule;