mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Enforce plugin structure and generate documentation
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
# ESLint plugins
|
||||
# DSpace ESLint plugins
|
||||
|
||||
Custom ESLint rules for DSpace Angular peculiarities.
|
||||
|
||||
## Overview
|
||||
## Documentation
|
||||
|
||||
The rules are split up into plugins by language:
|
||||
- [TypeScript rules](./docs/ts/index.md)
|
||||
- [HTML rules](./docs/html/index.md)
|
||||
|
||||
> Run `yarn docs:lint` to generate this documentation!
|
||||
|
||||
## Developing
|
||||
|
||||
### Overview
|
||||
|
||||
- Different file types must be handled by separate plugins. We support:
|
||||
- [TypeScript](./src/ts)
|
||||
- [HTML](./src/html)
|
||||
- All rules are written in TypeScript and compiled into [`dist`](./dist)
|
||||
- The plugins are linked into the main project dependencies from here
|
||||
- These directories already contain the necessary `package.json` files to mark them as ESLint plugins
|
||||
@@ -16,7 +23,7 @@ Custom ESLint rules for DSpace Angular peculiarities.
|
||||
- [Custom rules in typescript-eslint](https://typescript-eslint.io/developers/custom-rules)
|
||||
- [Angular ESLint](https://github.com/angular-eslint/angular-eslint)
|
||||
|
||||
## Parsing project metadata in advance ~ TypeScript AST
|
||||
### Parsing project metadata in advance ~ TypeScript AST
|
||||
|
||||
While it is possible to retain persistent state between files during the linting process, it becomes quite complicated if the content of one file determines how we want to lint another file.
|
||||
Because the two files may be linted out of order, we may not know whether the first file is wrong before we pass by the second. This means that we cannot report or fix the issue, because the first file is already detached from the linting context.
|
||||
|
85
lint/generate-docs.ts
Normal file
85
lint/generate-docs.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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 {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
} from 'fs';
|
||||
import { rmSync } from 'node:fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { default as htmlPlugin } from './src/rules/html';
|
||||
import { default as tsPlugin } from './src/rules/ts';
|
||||
|
||||
const templates = new Map();
|
||||
|
||||
function lazyEJS(path: string, data: object) {
|
||||
if (!templates.has(path)) {
|
||||
templates.set(path, require('ejs').compile(readFileSync(path).toString()));
|
||||
}
|
||||
|
||||
return templates.get(path)(data);
|
||||
}
|
||||
|
||||
const docsDir = join('lint', 'docs');
|
||||
const tsDir = join(docsDir, 'ts');
|
||||
const htmlDir = join(docsDir, 'html');
|
||||
|
||||
if (existsSync(docsDir)) {
|
||||
rmSync(docsDir, { recursive: true });
|
||||
}
|
||||
|
||||
mkdirSync(join(tsDir, 'rules'), { recursive: true });
|
||||
mkdirSync(join(htmlDir, 'rules'), { recursive: true });
|
||||
|
||||
function template(name: string): string {
|
||||
return join('lint', 'src', 'util', 'templates', name);
|
||||
}
|
||||
|
||||
// TypeScript docs
|
||||
writeFileSync(
|
||||
join(tsDir, 'index.md'),
|
||||
lazyEJS(template('index.ejs'), {
|
||||
plugin: tsPlugin,
|
||||
rules: tsPlugin.index.map(rule => rule.info),
|
||||
}),
|
||||
);
|
||||
|
||||
for (const rule of tsPlugin.index) {
|
||||
writeFileSync(
|
||||
join(tsDir, 'rules', rule.info.name + '.md'),
|
||||
lazyEJS(template('rule.ejs'), {
|
||||
plugin: tsPlugin,
|
||||
rule: rule.info,
|
||||
tests: rule.tests,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// HTML docs
|
||||
writeFileSync(
|
||||
join(htmlDir, 'index.md'),
|
||||
lazyEJS(template('index.ejs'), {
|
||||
plugin: htmlPlugin,
|
||||
rules: htmlPlugin.index.map(rule => rule.info),
|
||||
}),
|
||||
);
|
||||
|
||||
for (const rule of htmlPlugin.index) {
|
||||
writeFileSync(
|
||||
join(htmlDir, 'rules', rule.info.name + '.md'),
|
||||
lazyEJS(template('rule.ejs'), {
|
||||
plugin: htmlPlugin,
|
||||
rule: rule.info,
|
||||
tests: rule.tests,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@@ -6,11 +6,17 @@
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import themedComponentUsages from './themed-component-usages';
|
||||
import {
|
||||
bundle,
|
||||
RuleExports,
|
||||
} from '../../util/structure';
|
||||
import * as themedComponentUsages from './themed-component-usages';
|
||||
|
||||
const index = [
|
||||
themedComponentUsages,
|
||||
] as unknown as RuleExports[];
|
||||
|
||||
export = {
|
||||
rules: {
|
||||
'themed-component-usages': themedComponentUsages,
|
||||
},
|
||||
parser: require('@angular-eslint/template-parser'),
|
||||
...bundle('dspace-angular-html', 'HTML', index),
|
||||
};
|
||||
|
@@ -5,20 +5,39 @@
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { fixture } from '../../../test/fixture';
|
||||
import { DSpaceESLintRuleInfo } from '../../util/structure';
|
||||
import {
|
||||
DISALLOWED_THEME_SELECTORS,
|
||||
fixSelectors,
|
||||
} from '../../util/theme-support';
|
||||
|
||||
export default {
|
||||
export enum Message {
|
||||
WRONG_SELECTOR = 'mustUseThemedWrapperSelector',
|
||||
}
|
||||
|
||||
export const info = {
|
||||
name: 'themed-component-usages',
|
||||
meta: {
|
||||
docs: {
|
||||
description: `Themeable components should be used via the selector of their \`ThemedComponent\` wrapper class
|
||||
|
||||
This ensures that custom themes can correctly override _all_ instances of this component.
|
||||
The only exception to this rule are unit tests, where we may want to use the base component in order to keep the test setup simple.
|
||||
`,
|
||||
},
|
||||
type: 'problem',
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
messages: {
|
||||
mustUseThemedWrapperSelector: 'Themeable components should be used via their ThemedComponent wrapper\'s selector',
|
||||
[Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper\'s selector',
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
export const rule = {
|
||||
...info,
|
||||
create(context: any) {
|
||||
if (context.getFilename().includes('.spec.ts')) {
|
||||
// skip inline templates in unit tests
|
||||
@@ -28,7 +47,7 @@ export default {
|
||||
return {
|
||||
[`Element$1[name = /^${DISALLOWED_THEME_SELECTORS}/]`](node: any) {
|
||||
context.report({
|
||||
messageId: 'mustUseThemedWrapperSelector',
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
node,
|
||||
fix(fixer: any) {
|
||||
const oldSelector = node.name;
|
||||
@@ -59,3 +78,95 @@ export default {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const tests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
@Component({
|
||||
template: '<ds-test-themeable></ds-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
filename: fixture('src/test.spec.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
template: '<ds-test-themeable></ds-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
filename: fixture('src/test.spec.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
template: '<ds-base-test-themeable></ds-base-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
<ds-themed-test-themeable/>
|
||||
<ds-themed-test-themeable></ds-themed-test-themeable>
|
||||
<ds-themed-test-themeable [test]="something"></ds-themed-test-themeable>
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
<ds-base-test-themeable/>
|
||||
<ds-base-test-themeable></ds-base-test-themeable>
|
||||
<ds-base-test-themeable [test]="something"></ds-base-test-themeable>
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default rule;
|
||||
|
@@ -1,9 +1,15 @@
|
||||
import themedComponentSelectors from './themed-component-selectors';
|
||||
import themedComponentUsages from './themed-component-usages';
|
||||
import {
|
||||
bundle,
|
||||
RuleExports,
|
||||
} from '../../util/structure';
|
||||
import * as themedComponentUsages from './themed-component-usages';
|
||||
import * as themedComponentSelectors from './themed-component-selectors';
|
||||
|
||||
const index = [
|
||||
themedComponentUsages,
|
||||
themedComponentSelectors,
|
||||
] as unknown as RuleExports[];
|
||||
|
||||
export = {
|
||||
rules: {
|
||||
'themed-component-selectors': themedComponentSelectors,
|
||||
'themed-component-usages': themedComponentUsages,
|
||||
},
|
||||
...bundle('dspace-angular-ts', 'TypeScript', index),
|
||||
};
|
||||
|
@@ -6,27 +6,53 @@
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { ESLintUtils } 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';
|
||||
|
||||
export default ESLintUtils.RuleCreator.withoutDocs({
|
||||
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: {
|
||||
wrongSelectorUnthemedComponent: 'Unthemed version of themeable components should have a selector starting with \'ds-base-\'',
|
||||
wrongSelectorThemedComponentWrapper: 'Themed component wrapper of themeable components shouldn\'t have a selector starting with \'ds-themed-\'',
|
||||
wrongSelectorThemedComponentOverride: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'',
|
||||
[Message.BASE]: 'Unthemed version of themeable components should have a selector starting with \'ds-base-\'',
|
||||
[Message.WRAPPER]: 'Themed component wrapper of themeable components 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: any): any {
|
||||
if (context.getFilename()?.endsWith('.spec.ts')) {
|
||||
return {};
|
||||
@@ -35,7 +61,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({
|
||||
function enforceWrapperSelector(selectorNode: any) {
|
||||
if (selectorNode?.value.startsWith('ds-themed-')) {
|
||||
context.report({
|
||||
messageId: 'wrongSelectorThemedComponentWrapper',
|
||||
messageId: Message.WRAPPER,
|
||||
node: selectorNode,
|
||||
fix(fixer: any) {
|
||||
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-themed-', 'ds-')));
|
||||
@@ -47,7 +73,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({
|
||||
function enforceBaseSelector(selectorNode: any) {
|
||||
if (!selectorNode?.value.startsWith('ds-base-')) {
|
||||
context.report({
|
||||
messageId: 'wrongSelectorUnthemedComponent',
|
||||
messageId: Message.BASE,
|
||||
node: selectorNode,
|
||||
fix(fixer: any) {
|
||||
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-base-')));
|
||||
@@ -59,7 +85,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({
|
||||
function enforceThemedSelector(selectorNode: any) {
|
||||
if (!selectorNode?.value.startsWith('ds-themed-')) {
|
||||
context.report({
|
||||
messageId: 'wrongSelectorThemedComponentOverride',
|
||||
messageId: Message.THEMED,
|
||||
node: selectorNode,
|
||||
fix(fixer: any) {
|
||||
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-themed-')));
|
||||
@@ -91,3 +117,130 @@ export default ESLintUtils.RuleCreator.withoutDocs({
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
|
@@ -6,8 +6,9 @@
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
|
||||
import { fixture } from '../../../test/fixture';
|
||||
import { findUsages } from '../../util/misc';
|
||||
import { DSpaceESLintRuleInfo } from '../../util/structure';
|
||||
import {
|
||||
allThemeableComponents,
|
||||
DISALLOWED_THEME_SELECTORS,
|
||||
@@ -17,17 +18,40 @@ import {
|
||||
isAllowedUnthemedUsage,
|
||||
} from '../../util/theme-support';
|
||||
|
||||
export default ESLintUtils.RuleCreator.withoutDocs({
|
||||
export enum Message {
|
||||
WRONG_CLASS = 'mustUseThemedWrapperClass',
|
||||
WRONG_IMPORT = 'mustImportThemedWrapper',
|
||||
WRONG_SELECTOR = 'mustUseThemedWrapperSelector',
|
||||
}
|
||||
|
||||
export const info = {
|
||||
name: 'themed-component-usages',
|
||||
meta: {
|
||||
docs: {
|
||||
description: `Themeable components should be used via their \`ThemedComponent\` wrapper class
|
||||
|
||||
This ensures that custom themes can correctly override _all_ instances of this component.
|
||||
There are a few exceptions where the base class can still be used:
|
||||
- Class declaration expressions (otherwise we can't declare, extend or override the class in the first place)
|
||||
- Angular modules (except for routing modules)
|
||||
- Angular \`@ViewChild\` decorators
|
||||
- Type annotations
|
||||
`,
|
||||
},
|
||||
type: 'problem',
|
||||
schema: [],
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
mustUseThemedWrapper: 'Themeable components should be used via their ThemedComponent wrapper',
|
||||
mustImportThemedWrapper: 'Themeable components should be used via their ThemedComponent wrapper',
|
||||
[Message.WRONG_CLASS]: 'Themeable components should be used via their ThemedComponent wrapper',
|
||||
[Message.WRONG_IMPORT]: 'Themeable components should be used via their ThemedComponent wrapper',
|
||||
[Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper',
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: any, options: any): any {
|
||||
function handleUnthemedUsagesInTypescript(node: any) {
|
||||
if (isAllowedUnthemedUsage(node)) {
|
||||
@@ -42,7 +66,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({
|
||||
}
|
||||
|
||||
context.report({
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
messageId: Message.WRONG_CLASS,
|
||||
node: node,
|
||||
fix(fixer: any) {
|
||||
return fixer.replaceText(node, entry.wrapperClass);
|
||||
@@ -53,7 +77,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({
|
||||
function handleThemedSelectorQueriesInTests(node: any) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
fix(fixer: any){
|
||||
const newSelector = fixSelectors(node.raw);
|
||||
return fixer.replaceText(node, newSelector);
|
||||
@@ -79,7 +103,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({
|
||||
}
|
||||
|
||||
context.report({
|
||||
messageId: 'mustImportThemedWrapper',
|
||||
messageId: Message.WRONG_IMPORT,
|
||||
node: importedNode,
|
||||
fix(fixer: any) {
|
||||
const ops = [];
|
||||
@@ -133,3 +157,175 @@ export default ESLintUtils.RuleCreator.withoutDocs({
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
export const tests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'allow wrapper class usages',
|
||||
code: `
|
||||
import { ThemedTestThemeableComponent } from '../test/themed-test-themeable.component.ts';
|
||||
|
||||
const config = {
|
||||
a: ThemedTestThemeableComponent,
|
||||
b: ChipsComponent,
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'allow base class in class declaration',
|
||||
code: `
|
||||
export class TestThemeableComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'allow inheriting from base class',
|
||||
code: `
|
||||
import { TestThemeableComponent } from '../test/test-themeable.component.ts';
|
||||
|
||||
export class ThemedAdminSidebarComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'allow base class in ViewChild',
|
||||
code: `
|
||||
import { TestThemeableComponent } from '../test/test-themeable.component.ts';
|
||||
|
||||
export class Something {
|
||||
@ViewChild(TestThemeableComponent) test: TestThemeableComponent;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'allow wrapper selectors in test queries',
|
||||
filename: fixture('src/app/test/test.component.spec.ts'),
|
||||
code: `
|
||||
By.css('ds-themeable');
|
||||
By.Css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'allow wrapper selectors in cypress queries',
|
||||
filename: fixture('src/app/test/test.component.cy.ts'),
|
||||
code: `
|
||||
By.css('ds-themeable');
|
||||
By.Css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'disallow direct usages of base class',
|
||||
code: `
|
||||
import { TestThemeableComponent } from '../test/test-themeable.component.ts';
|
||||
import { TestComponent } from '../test/test.component.ts';
|
||||
|
||||
const config = {
|
||||
a: TestThemeableComponent,
|
||||
b: TestComponent,
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_IMPORT,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_CLASS,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
import { ThemedTestThemeableComponent } from '../test/themed-test-themeable.component.ts';
|
||||
import { TestComponent } from '../test/test.component.ts';
|
||||
|
||||
const config = {
|
||||
a: ThemedTestThemeableComponent,
|
||||
b: TestComponent,
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disallow override selector in test queries',
|
||||
filename: fixture('src/app/test/test.component.spec.ts'),
|
||||
code: `
|
||||
By.css('ds-themed-themeable');
|
||||
By.css('#test > ds-themed-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disallow base selector in test queries',
|
||||
filename: fixture('src/app/test/test.component.spec.ts'),
|
||||
code: `
|
||||
By.css('ds-base-themeable');
|
||||
By.css('#test > ds-base-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disallow override selector in cypress queries',
|
||||
filename: fixture('src/app/test/test.component.cy.ts'),
|
||||
code: `
|
||||
cy.get('ds-themed-themeable');
|
||||
cy.get('#test > ds-themed-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
cy.get('ds-themeable');
|
||||
cy.get('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disallow base selector in cypress queries',
|
||||
filename: fixture('src/app/test/test.component.cy.ts'),
|
||||
code: `
|
||||
cy.get('ds-base-themeable');
|
||||
cy.get('#test > ds-base-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
cy.get('ds-themeable');
|
||||
cy.get('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default rule;
|
||||
|
68
lint/src/util/structure.ts
Normal file
68
lint/src/util/structure.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 { TSESLint } from '@typescript-eslint/utils';
|
||||
import { RuleTester } from 'eslint';
|
||||
import { EnumType } from 'typescript';
|
||||
|
||||
export type Meta = TSESLint.RuleMetaData<string>;
|
||||
export type Valid = RuleTester.ValidTestCase | TSESLint.ValidTestCase<unknown[]>;
|
||||
export type Invalid = RuleTester.InvalidTestCase | TSESLint.InvalidTestCase<string, unknown[]>;
|
||||
|
||||
|
||||
export interface DSpaceESLintRuleInfo {
|
||||
name: string;
|
||||
meta: Meta,
|
||||
defaultOptions: any[],
|
||||
}
|
||||
|
||||
export interface DSpaceESLintTestInfo {
|
||||
rule: string;
|
||||
valid: Valid[];
|
||||
invalid: Invalid[];
|
||||
}
|
||||
|
||||
export interface DSpaceESLintPluginInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
rules: DSpaceESLintRuleInfo;
|
||||
tests: DSpaceESLintTestInfo;
|
||||
}
|
||||
|
||||
export interface DSpaceESLintInfo {
|
||||
html: DSpaceESLintPluginInfo;
|
||||
ts: DSpaceESLintPluginInfo;
|
||||
}
|
||||
|
||||
export interface RuleExports {
|
||||
Message: EnumType,
|
||||
info: DSpaceESLintRuleInfo,
|
||||
rule: any,
|
||||
tests: any,
|
||||
default: any,
|
||||
}
|
||||
|
||||
export function bundle(
|
||||
name: string,
|
||||
language: string,
|
||||
index: RuleExports[],
|
||||
): {
|
||||
name: string,
|
||||
language: string,
|
||||
rules: Record<string, any>,
|
||||
index: RuleExports[],
|
||||
} {
|
||||
return index.reduce((o: any, i: any) => {
|
||||
o.rules[i.info.name] = i.rule;
|
||||
return o;
|
||||
}, {
|
||||
name,
|
||||
language,
|
||||
rules: {},
|
||||
index,
|
||||
});
|
||||
}
|
5
lint/src/util/templates/index.ejs
Normal file
5
lint/src/util/templates/index.ejs
Normal file
@@ -0,0 +1,5 @@
|
||||
[DSpace ESLint plugins](../../README.md) > <%= plugin.language %> rules
|
||||
|
||||
<% rules.forEach(rule => { %>
|
||||
- [`<%= plugin.name %>/<%= rule.name %>`](./rules/<%= rule.name %>.md)<% if (rule.meta?.docs?.description) {%>: <%= rule.meta.docs.description.split('\n')[0] %><% }%>
|
||||
<% }) %>
|
36
lint/src/util/templates/rule.ejs
Normal file
36
lint/src/util/templates/rule.ejs
Normal file
@@ -0,0 +1,36 @@
|
||||
[DSpace ESLint plugins](../../../README.md) > [<%= plugin.language %> rules](../index.md) > `<%= plugin.name %>/<%= rule.name %>`
|
||||
_______
|
||||
|
||||
<%- rule.meta.docs?.description %>
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../src/rules/<%- plugin.name.replace('dspace-angular-', '') %>/<%- rule.name %>.ts)
|
||||
|
||||
### Examples
|
||||
|
||||
<% if (tests.valid) {%>
|
||||
#### Valid code
|
||||
<% tests.valid.forEach(test => { %>
|
||||
<% if (test.filename) { %>
|
||||
Filename: `<%- test.filename %>`
|
||||
<% } %>
|
||||
```
|
||||
<%- test.code.trim() %>
|
||||
```
|
||||
<% }) %>
|
||||
<% } %>
|
||||
|
||||
<% if (tests.invalid) {%>
|
||||
#### Invalid code
|
||||
<% tests.invalid.forEach(test => { %>
|
||||
|
||||
<% if (test.filename) { %>
|
||||
Filename: `<%- test.filename %>`
|
||||
<% } %>
|
||||
```
|
||||
<%- test.code.trim() %>
|
||||
```
|
||||
|
||||
<% }) %>
|
||||
<% } %>
|
13
lint/test/fixture/index.ts
Normal file
13
lint/test/fixture/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
||||
export const FIXTURE = 'lint/test/fixture/';
|
||||
|
||||
export function fixture(path: string): string {
|
||||
return FIXTURE + path;
|
||||
}
|
26
lint/test/rules.spec.ts
Normal file
26
lint/test/rules.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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 { default as htmlPlugin } from '../src/rules/html';
|
||||
import { default as tsPlugin } from '../src/rules/ts';
|
||||
import {
|
||||
htmlRuleTester,
|
||||
tsRuleTester,
|
||||
} from './testing';
|
||||
|
||||
describe('TypeScript rules', () => {
|
||||
for (const { info, rule, tests } of tsPlugin.index) {
|
||||
tsRuleTester.run(info.name, rule, tests);
|
||||
}
|
||||
});
|
||||
|
||||
describe('HTML rules', () => {
|
||||
for (const { info, rule, tests } of htmlPlugin.index) {
|
||||
htmlRuleTester.run(info.name, rule, tests);
|
||||
}
|
||||
});
|
@@ -1,140 +0,0 @@
|
||||
/**
|
||||
* 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 rule from '../../src/rules/ts/themed-component-selectors';
|
||||
import {
|
||||
fixture,
|
||||
tsRuleTester,
|
||||
} from '../testing';
|
||||
|
||||
describe('themed-component-selectors', () => {
|
||||
tsRuleTester.run('themed-component-selectors', rule as any, {
|
||||
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: 'wrongSelectorUnthemedComponent',
|
||||
},
|
||||
],
|
||||
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: 'wrongSelectorThemedComponentWrapper',
|
||||
},
|
||||
],
|
||||
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: 'wrongSelectorThemedComponentOverride',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-themed-something',
|
||||
})
|
||||
class TestThememeableComponent extends BaseComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
});
|
@@ -1,265 +0,0 @@
|
||||
/**
|
||||
* 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 htmlRule from '../../src/rules/html/themed-component-usages';
|
||||
import tsRule from '../../src/rules/ts/themed-component-usages';
|
||||
import {
|
||||
fixture,
|
||||
htmlRuleTester,
|
||||
tsRuleTester,
|
||||
} from '../testing';
|
||||
|
||||
describe('themed-component-usages (TypeScript)', () => {
|
||||
tsRuleTester.run('themed-component-usages', tsRule as any, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const config = {
|
||||
a: ThemedTestThemeableComponent,
|
||||
b: ChipsComponent,
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
export class TestThemeableComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
import { TestThemeableComponent } from '../test/test-themeable.component.ts';
|
||||
|
||||
export class ThemedAdminSidebarComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
import { TestThemeableComponent } from '../test/test-themeable.component.ts';
|
||||
|
||||
export class Something {
|
||||
@ViewChild(TestThemeableComponent) test: TestThemeableComponent;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: fixture('src/app/test/test.component.spec.ts'),
|
||||
code: `
|
||||
By.css('ds-themeable');
|
||||
By.Css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: fixture('src/app/test/test.component.cy.ts'),
|
||||
code: `
|
||||
By.css('ds-themeable');
|
||||
By.Css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
import { TestThemeableComponent } from '../test/test-themeable.component.ts';
|
||||
import { TestComponent } from '../test/test.component.ts';
|
||||
|
||||
const config = {
|
||||
a: TestThemeableComponent,
|
||||
b: TestComponent,
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'mustImportThemedWrapper',
|
||||
},
|
||||
{
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
import { ThemedTestThemeableComponent } from '../test/themed-test-themeable.component.ts';
|
||||
import { TestComponent } from '../test/test.component.ts';
|
||||
|
||||
const config = {
|
||||
a: ThemedTestThemeableComponent,
|
||||
b: TestComponent,
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
filename: fixture('src/app/test/test.component.spec.ts'),
|
||||
code: `
|
||||
By.css('ds-themed-themeable');
|
||||
By.css('#test > ds-themed-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
},
|
||||
{
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
filename: fixture('src/app/test/test.component.spec.ts'),
|
||||
code: `
|
||||
By.css('ds-base-themeable');
|
||||
By.css('#test > ds-base-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
},
|
||||
{
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
filename: fixture('src/app/test/test.component.cy.ts'),
|
||||
code: `
|
||||
cy.get('ds-themed-themeable');
|
||||
cy.get('#test > ds-themed-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
},
|
||||
{
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
cy.get('ds-themeable');
|
||||
cy.get('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
filename: fixture('src/app/test/test.component.cy.ts'),
|
||||
code: `
|
||||
cy.get('ds-base-themeable');
|
||||
cy.get('#test > ds-base-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
},
|
||||
{
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
cy.get('ds-themeable');
|
||||
cy.get('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
});
|
||||
|
||||
describe('themed-component-usages (HTML)', () => {
|
||||
htmlRuleTester.run('themed-component-usages', htmlRule, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: fixture('src/test.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
template: '<ds-test-themeable></ds-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: fixture('src/test.spec.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
template: '<ds-test-themeable></ds-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
filename: fixture('src/test.spec.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
template: '<ds-base-test-themeable></ds-base-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
<ds-themed-test-themeable/>
|
||||
<ds-themed-test-themeable></ds-themed-test-themeable>
|
||||
<ds-themed-test-themeable [test]="something"></ds-themed-test-themeable>
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'mustUseThemedWrapperSelector',
|
||||
},
|
||||
{
|
||||
messageId: 'mustUseThemedWrapperSelector',
|
||||
},
|
||||
{
|
||||
messageId: 'mustUseThemedWrapperSelector',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
<ds-base-test-themeable/>
|
||||
<ds-base-test-themeable></ds-base-test-themeable>
|
||||
<ds-base-test-themeable [test]="something"></ds-base-test-themeable>
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'mustUseThemedWrapperSelector',
|
||||
},
|
||||
{
|
||||
messageId: 'mustUseThemedWrapperSelector',
|
||||
},
|
||||
{
|
||||
messageId: 'mustUseThemedWrapperSelector',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
76
lint/test/structure.spec.ts
Normal file
76
lint/test/structure.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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 { default as html } from '../src/rules/html';
|
||||
import { default as ts } from '../src/rules/ts';
|
||||
|
||||
describe('plugin structure', () => {
|
||||
for (const pluginExports of [ts, html]) {
|
||||
const pluginName = pluginExports.name ?? 'UNNAMED PLUGIN';
|
||||
|
||||
describe(pluginName, () => {
|
||||
it('should have a name', () => {
|
||||
expect(pluginExports.name).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have rules', () => {
|
||||
expect(pluginExports.index).toBeTruthy();
|
||||
expect(pluginExports.rules).toBeTruthy();
|
||||
expect(pluginExports.index.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
for (const ruleExports of pluginExports.index) {
|
||||
const ruleName = ruleExports.info.name ?? 'UNNAMED RULE';
|
||||
|
||||
describe(ruleName, () => {
|
||||
it('should have a name', () => {
|
||||
expect(ruleExports.info.name).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be included under the right name in the plugin', () => {
|
||||
expect(pluginExports.rules[ruleExports.info.name]).toBe(ruleExports.rule);
|
||||
});
|
||||
|
||||
it('should contain metadata', () => {
|
||||
expect(ruleExports.info).toBeTruthy();
|
||||
expect(ruleExports.info.name).toBeTruthy();
|
||||
expect(ruleExports.info.meta).toBeTruthy();
|
||||
expect(ruleExports.info.defaultOptions).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should contain messages', () => {
|
||||
expect(ruleExports.Message).toBeTruthy();
|
||||
expect(ruleExports.info.meta.messages).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('messages', () => {
|
||||
for (const member of Object.keys(ruleExports.Message)) {
|
||||
describe(member, () => {
|
||||
const id = (ruleExports.Message as any)[member];
|
||||
|
||||
it('should have a valid ID', () => {
|
||||
expect(id).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have valid metadata', () => {
|
||||
expect(ruleExports.info.meta.messages[id]).toBeTruthy();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should contain tests', () => {
|
||||
expect(ruleExports.tests).toBeTruthy();
|
||||
expect(ruleExports.tests.valid.length).toBeGreaterThan(0);
|
||||
expect(ruleExports.tests.invalid.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@@ -8,20 +8,19 @@
|
||||
|
||||
import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { RuleTester } from 'eslint';
|
||||
import {
|
||||
FIXTURE,
|
||||
fixture,
|
||||
} from './fixture';
|
||||
|
||||
import { themeableComponents } from '../src/util/theme-support';
|
||||
|
||||
const FIXTURE = 'lint/test/fixture/';
|
||||
|
||||
// Register themed components from test fixture
|
||||
themeableComponents.initialize(FIXTURE);
|
||||
|
||||
TypeScriptRuleTester.itOnly = fit;
|
||||
|
||||
export function fixture(path: string): string {
|
||||
return FIXTURE + path;
|
||||
}
|
||||
|
||||
export const tsRuleTester = new TypeScriptRuleTester({
|
||||
parser: '@typescript-eslint/parser',
|
||||
defaultFilenames: {
|
||||
|
@@ -6,7 +6,7 @@
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { themeableComponents } from '../../src/util/theme-support';
|
||||
import { themeableComponents } from '../src/util/theme-support';
|
||||
|
||||
describe('theme-support', () => {
|
||||
describe('themeable component registry', () => {
|
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"lib": [
|
||||
"es2021"
|
||||
],
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"noImplicitReturns": true,
|
||||
@@ -7,14 +11,14 @@
|
||||
"strict": true,
|
||||
"outDir": "./dist",
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
|
Reference in New Issue
Block a user