Skip to content

Commit

Permalink
Enforce plugin structure and generate documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
ybnd committed Mar 15, 2024
1 parent 36787a2 commit c88391d
Show file tree
Hide file tree
Showing 19 changed files with 833 additions and 446 deletions.
19 changes: 13 additions & 6 deletions lint/README.md
Original file line number Diff line number Diff line change
@@ -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/ts/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
Expand All @@ -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.
Expand Down
85 changes: 85 additions & 0 deletions lint/generate-docs.ts
Original file line number Diff line number Diff line change
@@ -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,
}),
);
}

14 changes: 10 additions & 4 deletions lint/src/rules/html/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
117 changes: 114 additions & 3 deletions lint/src/rules/html/themed-component-usages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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;
18 changes: 12 additions & 6 deletions lint/src/rules/ts/index.ts
Original file line number Diff line number Diff line change
@@ -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),
};
Loading

0 comments on commit c88391d

Please sign in to comment.