From 3937be13f2e2cc3a7fe8f135983784f2070335f2 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Thu, 14 Mar 2024 10:00:10 +0100 Subject: [PATCH] Custom ESLint rules to enforce new ThemedComponent selector convention The following cases are covered: - ThemedComponent wrapper selectors must not start with ds-themed- - Base component selectors must start with ds-base- - Themed component selectors must start with ds-themed- - The ThemedComponent wrapper must always be used in HTML - The ThemedComponent wrapper must be used in TypeScript _where appropriate_: - Required - Explicit usages (e.g. modal instantiation, routing modules, ...) - By.css selector queries (in order to align with the HTML rule) - Unchecked - Non-routing modules (to ensure the components can be declared) - ViewChild hooks (since they need to attach to the underlying component) All rules work with --fix to automatically migrate to the new convention This covers most of the codebase, but minor manual adjustment are needed afterwards --- .eslintrc.json | 22 +- .github/workflows/build.yml | 8 +- lint/.gitignore | 4 + lint/README.md | 31 +++ lint/dist/src/rules/html/package.json | 6 + lint/dist/src/rules/ts/package.json | 6 + lint/jasmine.json | 7 + lint/src/rules/html/index.ts | 16 ++ .../src/rules/html/themed-component-usages.ts | 56 +++++ lint/src/rules/ts/index.ts | 9 + .../rules/ts/themed-component-selectors.ts | 92 +++++++++ lint/src/rules/ts/themed-component-usages.ts | 132 ++++++++++++ lint/src/util/angular.ts | 16 ++ lint/src/util/misc.ts | 42 ++++ lint/src/util/theme-support.ts | 192 ++++++++++++++++++ lint/test/fixture/README.md | 9 + .../src/app/test/test-routing.module.ts | 14 ++ .../src/app/test/test-themeable.component.ts | 15 ++ .../src/app/test/test.component.spec.ts | 8 + .../fixture/src/app/test/test.component.ts | 15 ++ lint/test/fixture/src/app/test/test.module.ts | 23 +++ .../test/themed-test-themeable.component.ts | 28 +++ lint/test/fixture/src/test.ts | 0 .../test/app/test/test-themeable.component.ts | 17 ++ .../fixture/src/themes/test/test.module.ts | 19 ++ lint/test/fixture/tsconfig.json | 7 + lint/test/helpers.js | 13 ++ .../rules/themed-component-selectors.spec.ts | 140 +++++++++++++ .../rules/themed-component-usages.spec.ts | 190 +++++++++++++++++ lint/test/testing.ts | 52 +++++ lint/test/util/theme-support.spec.ts | 24 +++ lint/tsconfig.json | 23 +++ package.json | 13 +- tsconfig.json | 3 +- yarn.lock | 134 +++++++++--- 35 files changed, 1352 insertions(+), 34 deletions(-) create mode 100644 lint/.gitignore create mode 100644 lint/README.md create mode 100644 lint/dist/src/rules/html/package.json create mode 100644 lint/dist/src/rules/ts/package.json create mode 100644 lint/jasmine.json create mode 100644 lint/src/rules/html/index.ts create mode 100644 lint/src/rules/html/themed-component-usages.ts create mode 100644 lint/src/rules/ts/index.ts create mode 100644 lint/src/rules/ts/themed-component-selectors.ts create mode 100644 lint/src/rules/ts/themed-component-usages.ts create mode 100644 lint/src/util/angular.ts create mode 100644 lint/src/util/misc.ts create mode 100644 lint/src/util/theme-support.ts create mode 100644 lint/test/fixture/README.md create mode 100644 lint/test/fixture/src/app/test/test-routing.module.ts create mode 100644 lint/test/fixture/src/app/test/test-themeable.component.ts create mode 100644 lint/test/fixture/src/app/test/test.component.spec.ts create mode 100644 lint/test/fixture/src/app/test/test.component.ts create mode 100644 lint/test/fixture/src/app/test/test.module.ts create mode 100644 lint/test/fixture/src/app/test/themed-test-themeable.component.ts create mode 100644 lint/test/fixture/src/test.ts create mode 100644 lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts create mode 100644 lint/test/fixture/src/themes/test/test.module.ts create mode 100644 lint/test/fixture/tsconfig.json create mode 100644 lint/test/helpers.js create mode 100644 lint/test/rules/themed-component-selectors.spec.ts create mode 100644 lint/test/rules/themed-component-usages.spec.ts create mode 100644 lint/test/testing.ts create mode 100644 lint/test/util/theme-support.spec.ts create mode 100644 lint/tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json index 50a9be3d59..a18f5873b4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,7 +11,10 @@ "eslint-plugin-jsonc", "eslint-plugin-rxjs", "eslint-plugin-simple-import-sort", - "eslint-plugin-import-newlines" + "eslint-plugin-import-newlines", + "eslint-plugin-jsonc", + "dspace-angular-ts", + "dspace-angular-html" ], "overrides": [ { @@ -238,7 +241,11 @@ "method" ], - "rxjs/no-nested-subscribe": "off" // todo: go over _all_ cases + "rxjs/no-nested-subscribe": "off", // todo: go over _all_ cases + + // Custom DSpace Angular rules + "dspace-angular-ts/themed-component-selectors": "error", + "dspace-angular-ts/themed-component-usages": "error" } }, { @@ -253,7 +260,10 @@ "createDefaultProgram": true }, "rules": { - "prefer-const": "off" + "prefer-const": "off", + + // Custom DSpace Angular rules + "dspace-angular-ts/themed-component-usages": "error" } }, { @@ -262,7 +272,11 @@ ], "extends": [ "plugin:@angular-eslint/template/recommended" - ] + ], + "rules": { + // Custom DSpace Angular rules + "dspace-angular-html/themed-component-usages": "error" + } }, { "files": [ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52f20470a3..e7d0e46f66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,8 +85,14 @@ jobs: - name: Install Yarn dependencies run: yarn install --frozen-lockfile + - name: Build lint plugins + run: yarn run build:lint + + - name: Run lint plugin tests + run: yarn run test:lint:nobuild + - name: Run lint - run: yarn run lint --quiet + run: yarn run lint:nobuild --quiet - name: Check for circular dependencies run: yarn run check-circ-deps diff --git a/lint/.gitignore b/lint/.gitignore new file mode 100644 index 0000000000..6b6bf3270b --- /dev/null +++ b/lint/.gitignore @@ -0,0 +1,4 @@ +/dist/ +/coverage/ +/node-modules/ +/docs/ diff --git a/lint/README.md b/lint/README.md new file mode 100644 index 0000000000..5fff29b1b2 --- /dev/null +++ b/lint/README.md @@ -0,0 +1,31 @@ +# ESLint plugins + +Custom ESLint rules for DSpace Angular peculiarities. + +## 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 +- The plugins are declared in [`.eslintrc.json`](../.eslintrc.json). Individual rules can be configured or disabled there, like usual. +- Some useful links + - [Developing ESLint plugins](https://eslint.org/docs/latest/extend/plugins) + - [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 + +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. + +For example, we cannot consistently determine which components are themeable (i.e. have a `ThemedComponent` wrapper) while linting. +To work around this issue, we construct a registry of themeable components _before_ linting anything. +- We don't have a good way to hook into the ESLint parser at this time +- Instead, we leverage the actual TypeScript AST parser + - Retrieve all `ThemedComponent` wrapper files by the pattern of their path (`themed-*.component.ts`) + - Determine the themed component they're linked to (by the actual type annotation/import path, since filenames are prone to errors) + - Store metadata describing these component pairs in a global registry that can be shared between rules +- This only needs to happen once, and only takes a fraction of a second (for ~100 themeable components) \ No newline at end of file diff --git a/lint/dist/src/rules/html/package.json b/lint/dist/src/rules/html/package.json new file mode 100644 index 0000000000..d3f310d23b --- /dev/null +++ b/lint/dist/src/rules/html/package.json @@ -0,0 +1,6 @@ +{ + "name": "eslint-plugin-dspace-angular-html", + "version": "0.0.0", + "main": "./index.js", + "private": true +} diff --git a/lint/dist/src/rules/ts/package.json b/lint/dist/src/rules/ts/package.json new file mode 100644 index 0000000000..f19e18756a --- /dev/null +++ b/lint/dist/src/rules/ts/package.json @@ -0,0 +1,6 @@ +{ + "name": "eslint-plugin-dspace-angular-ts", + "version": "0.0.0", + "main": "./index.js", + "private": true +} diff --git a/lint/jasmine.json b/lint/jasmine.json new file mode 100644 index 0000000000..dfacd41a96 --- /dev/null +++ b/lint/jasmine.json @@ -0,0 +1,7 @@ +{ + "spec_files": ["**/*.spec.js"], + "spec_dir": "lint/dist/test", + "helpers": [ + "./test/helpers.js" + ] +} diff --git a/lint/src/rules/html/index.ts b/lint/src/rules/html/index.ts new file mode 100644 index 0000000000..ef0b7a87ed --- /dev/null +++ b/lint/src/rules/html/index.ts @@ -0,0 +1,16 @@ +/** + * 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 themedComponentUsages from './themed-component-usages'; + +export = { + rules: { + 'themed-component-usages': themedComponentUsages, + }, + parser: require('@angular-eslint/template-parser'), +}; diff --git a/lint/src/rules/html/themed-component-usages.ts b/lint/src/rules/html/themed-component-usages.ts new file mode 100644 index 0000000000..6184805a2b --- /dev/null +++ b/lint/src/rules/html/themed-component-usages.ts @@ -0,0 +1,56 @@ +/** + * 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 { + DISALLOWED_THEME_SELECTORS, + fixSelectors, +} from '../../util/theme-support'; + +export default { + meta: { + type: 'problem', + fixable: 'code', + schema: [], + messages: { + mustUseThemedWrapperSelector: 'Themeable components should be used via their ThemedComponent wrapper\'s selector', + } + }, + create(context: any) { + return { + [`Element$1[name = /^${DISALLOWED_THEME_SELECTORS}/]`](node: any) { + context.report({ + messageId: 'mustUseThemedWrapperSelector', + node, + fix(fixer: any) { + const oldSelector = node.name; + const newSelector = fixSelectors(oldSelector); + + const openTagRange = [ + node.startSourceSpan.start.offset + 1, + node.startSourceSpan.start.offset + 1 + oldSelector.length + ]; + + const ops = [ + fixer.replaceTextRange(openTagRange, newSelector), + ]; + + // make sure we don't mangle self-closing tags + if (node.startSourceSpan.end.offset !== node.endSourceSpan.end.offset) { + const closeTagRange = [ + node.endSourceSpan.start.offset + 2, + node.endSourceSpan.end.offset - 1 + ]; + ops.push(fixer.replaceTextRange(closeTagRange, newSelector)); + } + + return ops; + } + }); + }, + }; + } +}; diff --git a/lint/src/rules/ts/index.ts b/lint/src/rules/ts/index.ts new file mode 100644 index 0000000000..b33135d7b0 --- /dev/null +++ b/lint/src/rules/ts/index.ts @@ -0,0 +1,9 @@ +import themedComponentSelectors from './themed-component-selectors'; +import themedComponentUsages from './themed-component-usages'; + +export = { + rules: { + 'themed-component-selectors': themedComponentSelectors, + 'themed-component-usages': themedComponentUsages, + }, +}; diff --git a/lint/src/rules/ts/themed-component-selectors.ts b/lint/src/rules/ts/themed-component-selectors.ts new file mode 100644 index 0000000000..e150bb41a8 --- /dev/null +++ b/lint/src/rules/ts/themed-component-selectors.ts @@ -0,0 +1,92 @@ +/** + * 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 } from '@typescript-eslint/utils'; +import { getComponentSelectorNode } from '../../util/angular'; +import { stringLiteral } from '../../util/misc'; +import { + inThemedComponentOverrideFile, + isThemeableComponent, + isThemedComponentWrapper, +} from '../../util/theme-support'; + +export default ESLintUtils.RuleCreator.withoutDocs({ + meta: { + 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-\'', + } + }, + defaultOptions: [], + create(context: any): any { + if (context.getFilename()?.endsWith('.spec.ts')) { + return {}; + } + + function enforceWrapperSelector(selectorNode: any) { + if (selectorNode?.value.startsWith('ds-themed-')) { + context.report({ + messageId: 'wrongSelectorThemedComponentWrapper', + node: selectorNode, + fix(fixer: any) { + return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-themed-', 'ds-'))); + }, + }); + } + } + + function enforceBaseSelector(selectorNode: any) { + if (!selectorNode?.value.startsWith('ds-base-')) { + context.report({ + messageId: 'wrongSelectorUnthemedComponent', + node: selectorNode, + fix(fixer: any) { + return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-base-'))); + }, + }); + } + } + + function enforceThemedSelector(selectorNode: any) { + if (!selectorNode?.value.startsWith('ds-themed-')) { + context.report({ + messageId: 'wrongSelectorThemedComponentOverride', + node: selectorNode, + fix(fixer: any) { + return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-themed-'))); + }, + }); + } + } + + return { + 'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: any) { + // keep track of all @Component nodes by their selector + const selectorNode = getComponentSelectorNode(node); + const selector = selectorNode?.value; + const classNode = node.parent; + const className = classNode.id?.name; + + if (selector === undefined || className === undefined) { + return; + } + + if (isThemedComponentWrapper(node)) { + enforceWrapperSelector(selectorNode); + } else if (inThemedComponentOverrideFile(context)) { + enforceThemedSelector(selectorNode); + } else if (isThemeableComponent(className)) { + enforceBaseSelector(selectorNode); + } + } + }; + } +}); diff --git a/lint/src/rules/ts/themed-component-usages.ts b/lint/src/rules/ts/themed-component-usages.ts new file mode 100644 index 0000000000..5934eb5e2e --- /dev/null +++ b/lint/src/rules/ts/themed-component-usages.ts @@ -0,0 +1,132 @@ +/** + * 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 } from '@typescript-eslint/utils'; +import { findUsages } from '../../util/misc'; +import { + allThemeableComponents, + DISALLOWED_THEME_SELECTORS, + fixSelectors, + getThemeableComponentByBaseClass, + inThemedComponentFile, + isAllowedUnthemedUsage, +} from '../../util/theme-support'; + +export default ESLintUtils.RuleCreator.withoutDocs({ + meta: { + 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', + }, + }, + defaultOptions: [], + create(context: any, options: any): any { + function handleUnthemedUsagesInTypescript(node: any) { + if (isAllowedUnthemedUsage(node)) { + return; + } + + const entry = getThemeableComponentByBaseClass(node.name); + + if (entry === undefined) { + // this should never happen + throw new Error(`No such themeable component in registry: '${node.name}'`); + } + + context.report({ + messageId: 'mustUseThemedWrapper', + node: node, + fix(fixer: any) { + return fixer.replaceText(node, entry.wrapperClass); + }, + }); + } + + function handleThemedSelectorQueriesInTests(node: any) { + + } + + function handleUnthemedImportsInTypescript(specifierNode: any) { + const allUsages = findUsages(context, specifierNode.local); + const badUsages = allUsages.filter(usage => !isAllowedUnthemedUsage(usage)); + + if (badUsages.length === 0) { + return; + } + + const importedNode = specifierNode.imported; + const declarationNode = specifierNode.parent; + + const entry = getThemeableComponentByBaseClass(importedNode.name); + if (entry === undefined) { + // this should never happen + throw new Error(`No such themeable component in registry: '${importedNode.name}'`); + } + + context.report({ + messageId: 'mustImportThemedWrapper', + node: importedNode, + fix(fixer: any) { + const ops = []; + + const oldImportSource = declarationNode.source.value; + const newImportLine = `import { ${entry.wrapperClass} } from '${oldImportSource.replace(entry.baseFileName, entry.wrapperFileName)}';`; + + if (declarationNode.specifiers.length === 1) { + if (allUsages.length === badUsages.length) { + ops.push(fixer.replaceText(declarationNode, newImportLine)); + } else { + ops.push(fixer.insertTextAfter(declarationNode, newImportLine)); + } + } else { + ops.push(fixer.replaceText(specifierNode, entry.wrapperClass)); + ops.push(fixer.insertTextAfter(declarationNode, newImportLine)); + } + + return ops; + }, + }); + } + + // ignore tests and non-routing modules + if (context.getFilename()?.endsWith('.spec.ts')) { + return { + [`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`](node: any) { + context.report({ + node, + messageId: 'mustUseThemedWrapper', + fix(fixer: any){ + const newSelector = fixSelectors(node.raw); + return fixer.replaceText(node, newSelector); + } + }); + }, + }; + } else if ( + context.getFilename()?.match(/(?!routing).module.ts$/) + || context.getFilename()?.match(/themed-.+\.component\.ts$/) + || inThemedComponentFile(context) + ) { + // do nothing + return {}; + } else { + return allThemeableComponents().reduce( + (rules, entry) => { + return { + ...rules, + [`:not(:matches(ClassDeclaration, ImportSpecifier)) > Identifier[name = "${entry.baseClass}"]`]: handleUnthemedUsagesInTypescript, + [`ImportSpecifier[imported.name = "${entry.baseClass}"]`]: handleUnthemedImportsInTypescript, + }; + }, {}, + ); + } + + }, +}); diff --git a/lint/src/util/angular.ts b/lint/src/util/angular.ts new file mode 100644 index 0000000000..cb122a16dc --- /dev/null +++ b/lint/src/util/angular.ts @@ -0,0 +1,16 @@ +/** + * 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 function getComponentSelectorNode(componentDecoratorNode: any): any | undefined { + for (const property of componentDecoratorNode.expression.arguments[0].properties) { + if (property.key?.name === 'selector') { + return property?.value; + } + } + return undefined; +} diff --git a/lint/src/util/misc.ts b/lint/src/util/misc.ts new file mode 100644 index 0000000000..1cd610fcd7 --- /dev/null +++ b/lint/src/util/misc.ts @@ -0,0 +1,42 @@ +/** + * 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 function stringLiteral(value: string): string { + return `'${value}'`; +} + +export function match(rangeA: number[], rangeB: number[]) { + return rangeA[0] === rangeB[0] && rangeA[1] === rangeB[1]; +} + +export function findUsages(context: any, localNode: any): any[] { + const ast = context.getSourceCode().ast; + + const usages: any[] = []; + + for (const token of ast.tokens) { + if (token.type === 'Identifier' && token.value === localNode.name && !match(token.range, localNode.range)) { + usages.push(context.getSourceCode().getNodeByRangeIndex(token.range[0])); + } + } + + return usages; +} + + +export function isPartOfTypeExpression(node: any): boolean { + return node.parent.type.startsWith('TSType'); +} + +export function isClassDeclaration(node: any): boolean { + return node.parent.type === 'ClassDeclaration'; +} + +export function isPartOfViewChild(node: any): boolean { + return node.parent?.callee?.name === 'ViewChild'; +} diff --git a/lint/src/util/theme-support.ts b/lint/src/util/theme-support.ts new file mode 100644 index 0000000000..bf7c265e2e --- /dev/null +++ b/lint/src/util/theme-support.ts @@ -0,0 +1,192 @@ +/** + * 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 { readFileSync } from 'fs'; +import { basename } from 'path'; +import ts from 'typescript'; +import { + isClassDeclaration, + isPartOfTypeExpression, + isPartOfViewChild, +} from './misc'; + +const glob = require('glob'); + +/** + * Couples a themeable Component to its ThemedComponent wrapper + */ +export interface ThemeableComponentRegistryEntry { + basePath: string; + baseFileName: string, + baseClass: string; + + wrapperPath: string; + wrapperFileName: string, + wrapperClass: string; +} + +/** + * Listing of all themeable Components + */ +class ThemeableComponentRegistry { + public readonly entries: Set; + public readonly byBaseClass: Map; + public readonly byBasePath: Map; + public readonly byWrapperPath: Map; + + constructor() { + this.entries = new Set(); + this.byBaseClass = new Map(); + this.byBasePath = new Map(); + this.byWrapperPath = new Map(); + } + + public initialize(prefix = '') { + if (this.entries.size > 0) { + return; + } + + function registerWrapper(path: string) { + const source = getSource(path); + + function traverse(node: any) { + if (node.kind === ts.SyntaxKind.Decorator && node.expression.expression.escapedText === 'Component' && node.parent.kind === ts.SyntaxKind.ClassDeclaration) { + const wrapperClass = node.parent.name.escapedText; + + for (const heritageClause of node.parent.heritageClauses) { + for (const type of heritageClause.types) { + if (type.expression.escapedText === 'ThemedComponent') { + const baseClass = type.typeArguments[0].typeName?.escapedText; + + ts.forEachChild(source, (topNode: any) => { + if (topNode.kind === ts.SyntaxKind.ImportDeclaration) { + for (const element of topNode.importClause.namedBindings.elements) { + if (element.name.escapedText === baseClass) { + const basePath = resolveLocalPath(topNode.moduleSpecifier.text, path); + + themeableComponents.add({ + baseClass, + basePath: basePath.replace(new RegExp(`^${prefix}`), ''), + baseFileName: basename(basePath).replace(/\.ts$/, ''), + wrapperClass, + wrapperPath: path.replace(new RegExp(`^${prefix}`), ''), + wrapperFileName: basename(path).replace(/\.ts$/, ''), + }); + } + } + } + }); + } + } + } + + return; + } else { + ts.forEachChild(node, traverse); + } + } + + traverse(source); + } + + const wrappers: string[] = glob.GlobSync(prefix + 'src/app/**/themed-*.component.ts', { ignore: 'node_modules/**' }).found; + + for (const wrapper of wrappers) { + registerWrapper(wrapper); + } + } + + private add(entry: ThemeableComponentRegistryEntry) { + this.entries.add(entry); + this.byBaseClass.set(entry.baseClass, entry); + this.byBasePath.set(entry.basePath, entry); + this.byWrapperPath.set(entry.wrapperPath, entry); + } +} + +export const themeableComponents = new ThemeableComponentRegistry(); + +/** + * Construct the AST of a TypeScript source file + * @param file + */ +function getSource(file: string): ts.SourceFile { + return ts.createSourceFile( + file, + readFileSync(file).toString(), + ts.ScriptTarget.ES2020, // todo: actually use tsconfig.json? + /*setParentNodes */ true, + ); +} + +/** + * Resolve a possibly relative local path into an absolute path starting from the root directory of the project + */ +function resolveLocalPath(path: string, relativeTo: string) { + if (path.startsWith('src/')) { + return path; + } else if (path.startsWith('./')) { + const parts = relativeTo.split('/'); + return [ + ...parts.slice(0, parts.length - 1), + path.replace(/^.\//, '') + ].join('/') + '.ts'; + } else { + throw new Error(`Unsupported local path: ${path}`); + } +} + +export function isThemedComponentWrapper(node: any): boolean { + return node.parent.superClass?.name === 'ThemedComponent'; +} + +export function isThemeableComponent(className: string): boolean { + themeableComponents.initialize(); + return themeableComponents.byBaseClass.has(className); +} + +export function inThemedComponentOverrideFile(context: any): boolean { + const match = context.getFilename().match(/src\/themes\/[^\/]+\/(app\/.*)/); + + if (!match) { + return false; + } + themeableComponents.initialize(); + // todo: this is fragile! + return themeableComponents.byBasePath.has(`src/${match[1]}`); +} + +export function inThemedComponentFile(context: any): boolean { + themeableComponents.initialize(); + + return [ + () => themeableComponents.byBasePath.has(context.getFilename()), + () => themeableComponents.byWrapperPath.has(context.getFilename()), + () => inThemedComponentOverrideFile(context), + ].some(predicate => predicate()); +} + +export function allThemeableComponents(): ThemeableComponentRegistryEntry[] { + themeableComponents.initialize(); + return [...themeableComponents.entries]; +} + +export function getThemeableComponentByBaseClass(baseClass: string): ThemeableComponentRegistryEntry | undefined { + themeableComponents.initialize(); + return themeableComponents.byBaseClass.get(baseClass); +} + +export function isAllowedUnthemedUsage(usageNode: any) { + return isClassDeclaration(usageNode) || isPartOfTypeExpression(usageNode) || isPartOfViewChild(usageNode); +} + +export const DISALLOWED_THEME_SELECTORS = 'ds-(base|themed)-'; + +export function fixSelectors(text: string): string { + return text.replaceAll(/ds-(base|themed)-/g, 'ds-'); +} diff --git a/lint/test/fixture/README.md b/lint/test/fixture/README.md new file mode 100644 index 0000000000..b19ae11b55 --- /dev/null +++ b/lint/test/fixture/README.md @@ -0,0 +1,9 @@ +# ESLint testing fixtures + +The files in this directory are used for the ESLint testing environment +- Some rules rely on registries that must be built up _before_ the rule is run + - In order to test these registries, the fixture sources contain a few dummy components +- The TypeScript ESLint test runner requires at least one dummy file to exist to run any tests + - By default, [`test.ts`](./src/test.ts) is used. Note that this file is empty; it's only there for the TypeScript configuration, the actual content is injected from the `code` property in the tests. + - To test rules that make assertions based on the path of the file, you'll need to include the `filename` property in the test configuration. Note that it must point to an existing file too! + - The `filename` must be provided as `fixture('src/something.ts')` \ No newline at end of file diff --git a/lint/test/fixture/src/app/test/test-routing.module.ts b/lint/test/fixture/src/app/test/test-routing.module.ts new file mode 100644 index 0000000000..d3a16bb6d6 --- /dev/null +++ b/lint/test/fixture/src/app/test/test-routing.module.ts @@ -0,0 +1,14 @@ +/** + * 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 { ThemedTestThemeableComponent } from './themed-test-themeable.component'; + +export const ROUTES = [ + { + component: ThemedTestThemeableComponent, + } +]; diff --git a/lint/test/fixture/src/app/test/test-themeable.component.ts b/lint/test/fixture/src/app/test/test-themeable.component.ts new file mode 100644 index 0000000000..bd731d8afa --- /dev/null +++ b/lint/test/fixture/src/app/test/test-themeable.component.ts @@ -0,0 +1,15 @@ +/** + * 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 { Component } from '@angular/core'; + +@Component({ + selector: 'ds-base-test-themeable', + template: '', +}) +export class TestThemeableComponent { +} diff --git a/lint/test/fixture/src/app/test/test.component.spec.ts b/lint/test/fixture/src/app/test/test.component.spec.ts new file mode 100644 index 0000000000..2300ac4a56 --- /dev/null +++ b/lint/test/fixture/src/app/test/test.component.spec.ts @@ -0,0 +1,8 @@ +/** + * 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/ + */ + diff --git a/lint/test/fixture/src/app/test/test.component.ts b/lint/test/fixture/src/app/test/test.component.ts new file mode 100644 index 0000000000..c01f104c98 --- /dev/null +++ b/lint/test/fixture/src/app/test/test.component.ts @@ -0,0 +1,15 @@ +/** + * 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 { Component } from '@angular/core'; + +@Component({ + selector: 'ds-test', + template: '', +}) +export class TestComponent { +} diff --git a/lint/test/fixture/src/app/test/test.module.ts b/lint/test/fixture/src/app/test/test.module.ts new file mode 100644 index 0000000000..633ef492fb --- /dev/null +++ b/lint/test/fixture/src/app/test/test.module.ts @@ -0,0 +1,23 @@ +/** + * 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/ + */ +// @ts-ignore +import { NgModule } from '@angular/core'; +import { TestThemeableComponent } from './test-themeable.component'; +import { TestComponent } from './test.component'; +import { ThemedTestThemeableComponent } from './themed-test-themeable.component'; + +@NgModule({ + declarations: [ + TestComponent, + TestThemeableComponent, + ThemedTestThemeableComponent, + ] +}) +export class TestModule { + +} diff --git a/lint/test/fixture/src/app/test/themed-test-themeable.component.ts b/lint/test/fixture/src/app/test/themed-test-themeable.component.ts new file mode 100644 index 0000000000..81eb59d418 --- /dev/null +++ b/lint/test/fixture/src/app/test/themed-test-themeable.component.ts @@ -0,0 +1,28 @@ +/** + * 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 { Component } from '@angular/core'; +import { ThemedComponent } from '../../../../../../src/app/shared/theme-support/themed.component'; +import { TestThemeableComponent } from './test-themeable.component'; + +@Component({ + selector: 'ds-test-themeable', + template: '', +}) +export class ThemedTestThemeableComponent extends ThemedComponent { + protected getComponentName(): string { + return ''; + } + + protected importThemedComponent(themeName: string): Promise { + return Promise.resolve(undefined); + } + + protected importUnthemedComponent(): Promise { + return Promise.resolve(undefined); + } +} diff --git a/lint/test/fixture/src/test.ts b/lint/test/fixture/src/test.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts b/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts new file mode 100644 index 0000000000..05ba4e3d1b --- /dev/null +++ b/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts @@ -0,0 +1,17 @@ +/** + * 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 { Component } from '@angular/core'; +import { TestThemeableComponent as BaseComponent } from '../../../../app/test/test-themeable.component'; + +@Component({ + selector: 'ds-themed-test-themeable', + template: '', +}) +export class TestThemeableComponent extends BaseComponent { + +} diff --git a/lint/test/fixture/src/themes/test/test.module.ts b/lint/test/fixture/src/themes/test/test.module.ts new file mode 100644 index 0000000000..6d7601bd52 --- /dev/null +++ b/lint/test/fixture/src/themes/test/test.module.ts @@ -0,0 +1,19 @@ +/** + * 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/ + */ +// @ts-ignore +import { NgModule } from '@angular/core'; +import { TestThemeableComponent } from './app/test/test-themeable.component'; + +@NgModule({ + declarations: [ + TestThemeableComponent, + ] +}) +export class TestModule { + +} diff --git a/lint/test/fixture/tsconfig.json b/lint/test/fixture/tsconfig.json new file mode 100644 index 0000000000..1fd3745ec8 --- /dev/null +++ b/lint/test/fixture/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "include": [ + "src/**/*.ts" + ], + "exclude": ["dist"] +} diff --git a/lint/test/helpers.js b/lint/test/helpers.js new file mode 100644 index 0000000000..bd648d007f --- /dev/null +++ b/lint/test/helpers.js @@ -0,0 +1,13 @@ +const SpecReporter = require('jasmine-spec-reporter').SpecReporter; +const StacktraceOption = require('jasmine-spec-reporter').StacktraceOption; + +jasmine.getEnv().clearReporters(); // Clear default console reporter for those instead +jasmine.getEnv().addReporter(new SpecReporter({ + spec: { + displayErrorMessages: false, + }, + summary: { + displayFailed: true, + displayStacktrace: StacktraceOption.PRETTY, + }, +})); diff --git a/lint/test/rules/themed-component-selectors.spec.ts b/lint/test/rules/themed-component-selectors.spec.ts new file mode 100644 index 0000000000..2f2e9786c2 --- /dev/null +++ b/lint/test/rules/themed-component-selectors.spec.ts @@ -0,0 +1,140 @@ +/** + * 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 { + fixture, + tsRuleTester, +} from '../testing'; +import rule from '../../src/rules/ts/themed-component-selectors'; + +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 { +} + +@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 { +} + `, + }, + ], + 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 { +} + `, + errors: [ + { + messageId: 'wrongSelectorThemedComponentWrapper', + }, + ], + output: ` +@Component({ + selector: 'ds-something', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + { + 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); +}); diff --git a/lint/test/rules/themed-component-usages.spec.ts b/lint/test/rules/themed-component-usages.spec.ts new file mode 100644 index 0000000000..2f5dbcec20 --- /dev/null +++ b/lint/test/rules/themed-component-usages.spec.ts @@ -0,0 +1,190 @@ +/** + * 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 { + fixture, + htmlRuleTester, + tsRuleTester, +} from '../testing'; +import tsRule from '../../src/rules/ts/themed-component-usages'; +import htmlRule from '../../src/rules/html/themed-component-usages'; + +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 { +} + `, + }, + { + 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'); + `, + }, + ], + 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'); + `, + }, + ], + } as any); +}); + +describe('themed-component-usages (HTML)', () => { + htmlRuleTester.run('themed-component-usages', htmlRule, { + valid: [ + { + code: ` + + + + `, + }, + ], + invalid: [ + { + code: ` + + + + `, + errors: [ + { + messageId: 'mustUseThemedWrapperSelector', + }, + { + messageId: 'mustUseThemedWrapperSelector', + }, + { + messageId: 'mustUseThemedWrapperSelector', + }, + ], + output: ` + + + + `, + }, + { + code: ` + + + + `, + errors: [ + { + messageId: 'mustUseThemedWrapperSelector', + }, + { + messageId: 'mustUseThemedWrapperSelector', + }, + { + messageId: 'mustUseThemedWrapperSelector', + }, + ], + output: ` + + + + `, + }, + ] + }); +}); diff --git a/lint/test/testing.ts b/lint/test/testing.ts new file mode 100644 index 0000000000..631d956b0b --- /dev/null +++ b/lint/test/testing.ts @@ -0,0 +1,52 @@ +/** + * 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 { RuleTester } from 'eslint'; +import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester'; +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: { + ts: fixture('src/test.ts'), + tsx: 'n/a', + }, + parserOptions: { + project: fixture('tsconfig.json'), + } +}); + +class HtmlRuleTester extends RuleTester { + run(name: string, rule: any, tests: { valid: any[], invalid: any[] }) { + super.run(name, rule, { + valid: tests.valid.map((test) => ({ + filename: fixture('test.html'), + ...test, + })), + invalid: tests.invalid.map((test) => ({ + filename: fixture('test.html'), + ...test, + })), + }); + } +} + +export const htmlRuleTester = new HtmlRuleTester({ + parser: require.resolve('@angular-eslint/template-parser'), +}); diff --git a/lint/test/util/theme-support.spec.ts b/lint/test/util/theme-support.spec.ts new file mode 100644 index 0000000000..52e63b4fed --- /dev/null +++ b/lint/test/util/theme-support.spec.ts @@ -0,0 +1,24 @@ +/** + * 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 { themeableComponents } from '../../src/util/theme-support'; + +describe('theme-support', () => { + describe('themeable component registry', () => { + it('should contain all themeable components from the fixture', () => { + expect(themeableComponents.entries.size).toBe(1); + expect(themeableComponents.byBasePath.size).toBe(1); + expect(themeableComponents.byWrapperPath.size).toBe(1); + expect(themeableComponents.byBaseClass.size).toBe(1); + + expect(themeableComponents.byBaseClass.get('TestThemeableComponent')).toBeTruthy(); + expect(themeableComponents.byBasePath.get('src/app/test/test-themeable.component.ts')).toBeTruthy(); + expect(themeableComponents.byWrapperPath.get('src/app/test/themed-test-themeable.component.ts')).toBeTruthy(); + }); + }); +}); diff --git a/lint/tsconfig.json b/lint/tsconfig.json new file mode 100644 index 0000000000..2c74bddb24 --- /dev/null +++ b/lint/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "noImplicitReturns": true, + "skipLibCheck": true, + "strict": true, + "outDir": "./dist", + "sourceMap": true, + "types": [ + "jasmine", + "node" + ] + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts", + ], + "exclude": [ + "dist", + "test/fixture" + ] +} diff --git a/package.json b/package.json index c0a3843605..b8f6402cfb 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,15 @@ "build:stats": "ng build --stats-json", "build:prod": "cross-env NODE_ENV=production yarn run build:ssr", "build:ssr": "ng build --configuration production && ng run dspace-angular:server:production", + "build:lint": "rimraf 'lint/dist/**/*.js' 'lint/dist/**/*.js.map' && tsc -b lint/tsconfig.json", "test": "ng test --source-map=true --watch=false --configuration test", "test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"", "test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", - "lint": "ng lint", - "lint-fix": "ng lint --fix=true", + "test:lint": "yarn build:lint && jasmine --config=lint/jasmine.json", + "test:lint:nobuild": "jasmine --config=lint/jasmine.json", + "lint": "yarn build:lint && ng lint", + "lint:nobuild": "ng lint", + "lint-fix": "yarn build:lint && ng lint --fix=true", "e2e": "cross-env NODE_ENV=production ng e2e", "clean:dev:config": "rimraf src/assets/config.json", "clean:coverage": "rimraf coverage", @@ -94,6 +98,8 @@ "date-fns-tz": "^1.3.7", "deepmerge": "^4.3.1", "ejs": "^3.1.9", + "eslint-plugin-dspace-angular-html": "link:./lint/dist/src/rules/html", + "eslint-plugin-dspace-angular-ts": "link:./lint/dist/src/rules/ts", "express": "^4.18.2", "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.1.1", @@ -160,6 +166,8 @@ "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/parser": "^5.59.1", + "@typescript-eslint/rule-tester": "^7.2.0", + "@typescript-eslint/utils": "^7.2.0", "axe-core": "^4.7.2", "compression-webpack-plugin": "^9.2.0", "copy-webpack-plugin": "^6.4.1", @@ -178,6 +186,7 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-unused-imports": "^2.0.0", "express-static-gzip": "^2.1.7", + "jasmine": "^3.8.0", "jasmine-core": "^3.8.0", "jasmine-marbles": "0.9.2", "karma": "^6.4.2", diff --git a/tsconfig.json b/tsconfig.json index afd00f8568..66921afd47 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -55,6 +55,7 @@ } }, "exclude": [ - "cypress.config.ts" + "cypress.config.ts", + "lint" ] } diff --git a/yarn.lock b/yarn.lock index 849cbe2eda..a137a12cbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1836,7 +1836,7 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz#200a0965cf654ac28b971358ecdca9cc5b44c335" integrity sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg== -"@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== @@ -2565,7 +2565,7 @@ "@types/jasmine@~3.6.0": version "3.6.11" - resolved "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.11.tgz" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.6.11.tgz#4b1d77aa9dfc757407cb9e277216d8e83553f09d" integrity sha512-S6pvzQDvMZHrkBz2Mcn/8Du7cpr76PlRJBAoHnSDNbulULsH5dp0Gns+WRyNX5LHejz/ljxK4/vIHK/caHt6SQ== "@types/js-cookie@2.2.6": @@ -2578,6 +2578,11 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" @@ -2671,6 +2676,11 @@ resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + "@types/serve-index@^1.9.1": version "1.9.1" resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz" @@ -2767,6 +2777,17 @@ "@typescript-eslint/typescript-estree" "5.59.1" debug "^4.3.4" +"@typescript-eslint/rule-tester@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/rule-tester/-/rule-tester-7.2.0.tgz#ca72af90fc4d46f1c53a4fc1c28d95fe7a96e879" + integrity sha512-V/jxkkx+buBn9uM2QvdHzi1XzxBm2M+QpEORNZCRkq3vKhnZO2Sto1X0xaZ6vVbmHvOE+Zlkv7GO98PXvgGKVg== + dependencies: + "@typescript-eslint/typescript-estree" "7.2.0" + "@typescript-eslint/utils" "7.2.0" + ajv "^6.10.0" + lodash.merge "4.6.2" + semver "^7.5.4" + "@typescript-eslint/scope-manager@5.48.2": version "5.48.2" resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.48.2.tgz" @@ -2799,6 +2820,14 @@ "@typescript-eslint/types" "5.59.6" "@typescript-eslint/visitor-keys" "5.59.6" +"@typescript-eslint/scope-manager@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz#cfb437b09a84f95a0930a76b066e89e35d94e3da" + integrity sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg== + dependencies: + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" + "@typescript-eslint/type-utils@5.48.2": version "5.48.2" resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.2.tgz" @@ -2839,6 +2868,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.6.tgz#5a6557a772af044afe890d77c6a07e8c23c2460b" integrity sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA== +"@typescript-eslint/types@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.2.0.tgz#0feb685f16de320e8520f13cca30779c8b7c403f" + integrity sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA== + "@typescript-eslint/typescript-estree@5.48.2": version "5.48.2" resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.2.tgz" @@ -2891,6 +2925,20 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz#5beda2876c4137f8440c5a84b4f0370828682556" + integrity sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA== + dependencies: + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/utils@5.48.2": version "5.48.2" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.2.tgz" @@ -2933,6 +2981,19 @@ eslint-scope "^5.1.1" semver "^7.3.7" +"@typescript-eslint/utils@7.2.0", "@typescript-eslint/utils@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.2.0.tgz#fc8164be2f2a7068debb4556881acddbf0b7ce2a" + integrity sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "7.2.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/typescript-estree" "7.2.0" + semver "^7.5.4" + "@typescript-eslint/utils@^5.57.0": version "5.58.0" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz" @@ -2979,6 +3040,14 @@ "@typescript-eslint/types" "5.59.6" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz#5035f177752538a5750cca1af6044b633610bf9e" + integrity sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A== + dependencies: + "@typescript-eslint/types" "7.2.0" + eslint-visitor-keys "^3.4.1" + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz" @@ -5454,6 +5523,14 @@ eslint-plugin-deprecation@^1.4.1: tslib "^2.3.1" tsutils "^3.21.0" +"eslint-plugin-dspace-angular-html@link:./lint/dist/src/rules/html": + version "0.0.0" + uid "" + +"eslint-plugin-dspace-angular-ts@link:./lint/dist/src/rules/ts": + version "0.0.0" + uid "" + eslint-plugin-import-newlines@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import-newlines/-/eslint-plugin-import-newlines-1.3.1.tgz#e21705667778e8134382b50079fbb2c8d3a2fcde" @@ -5583,6 +5660,11 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4 resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz" integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ== +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + eslint@^8.39.0: version "8.39.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1" @@ -7296,7 +7378,7 @@ jake@^10.8.5: filelist "^1.0.1" minimatch "^3.0.4" -jasmine-core@^3.6.0, jasmine-core@^3.8.0: +jasmine-core@^3.6.0, jasmine-core@^3.8.0, jasmine-core@~3.99.0: version "3.99.1" resolved "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz" integrity sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg== @@ -7308,6 +7390,14 @@ jasmine-marbles@0.9.2: dependencies: lodash "^4.17.20" +jasmine@^3.8.0: + version "3.99.0" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.99.0.tgz#7cc7aeda7ade2d57694fc818a374f778cbb4ea62" + integrity sha512-YIThBuHzaIIcjxeuLmPD40SjxkEcc8i//sGMDKCgkRMVgIwRJf5qyExtlJpQeh7pkeoBSOe6lQEdg+/9uKg9mw== + dependencies: + glob "^7.1.6" + jasmine-core "~3.99.0" + jest-worker@^27.4.5: version "27.5.1" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" @@ -7885,7 +7975,7 @@ lodash.isfinite@^3.3.2: resolved "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz" integrity sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA== -lodash.merge@^4.6.2: +lodash.merge@4.6.2, lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== @@ -8231,6 +8321,13 @@ minimalistic-assert@^1.0.0: resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== +minimatch@9.0.3, minimatch@^9.0.0: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -8259,13 +8356,6 @@ minimatch@^8.0.2: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.0: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -10537,7 +10627,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.1: +semver@^7.5.1, semver@^7.5.4: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== @@ -11362,6 +11452,11 @@ tree-kill@1.2.2, tree-kill@^1.2.2: resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +ts-api-utils@^1.0.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" + integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA== + ts-node@10.2.1, ts-node@^10.0.0: version "10.2.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.2.1.tgz#4cc93bea0a7aba2179497e65bb08ddfc198b3ab5" @@ -12303,7 +12398,7 @@ yargs@17.1.1: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@17.7.2: +yargs@17.7.2, yargs@^17.0.0: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== @@ -12329,19 +12424,6 @@ yargs@^16.1.1: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - yargs@^17.2.1, yargs@^17.3.1: version "17.7.1" resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz"