1
0

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
This commit is contained in:
Yury Bondarenko
2024-03-14 10:00:10 +01:00
parent 41eccbbfe1
commit 3937be13f2
35 changed files with 1352 additions and 34 deletions

View File

@@ -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": [

View File

@@ -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

4
lint/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/dist/
/coverage/
/node-modules/
/docs/

31
lint/README.md Normal file
View File

@@ -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)

6
lint/dist/src/rules/html/package.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "eslint-plugin-dspace-angular-html",
"version": "0.0.0",
"main": "./index.js",
"private": true
}

6
lint/dist/src/rules/ts/package.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "eslint-plugin-dspace-angular-ts",
"version": "0.0.0",
"main": "./index.js",
"private": true
}

7
lint/jasmine.json Normal file
View File

@@ -0,0 +1,7 @@
{
"spec_files": ["**/*.spec.js"],
"spec_dir": "lint/dist/test",
"helpers": [
"./test/helpers.js"
]
}

View File

@@ -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'),
};

View File

@@ -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;
}
});
},
};
}
};

View File

@@ -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,
},
};

View File

@@ -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);
}
}
};
}
});

View File

@@ -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,
};
}, {},
);
}
},
});

16
lint/src/util/angular.ts Normal file
View File

@@ -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;
}

42
lint/src/util/misc.ts Normal file
View File

@@ -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';
}

View File

@@ -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<ThemeableComponentRegistryEntry>;
public readonly byBaseClass: Map<string, ThemeableComponentRegistryEntry>;
public readonly byBasePath: Map<string, ThemeableComponentRegistryEntry>;
public readonly byWrapperPath: Map<string, ThemeableComponentRegistryEntry>;
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-');
}

View File

@@ -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')`

View File

@@ -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,
}
];

View File

@@ -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 {
}

View File

@@ -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/
*/

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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<TestThemeableComponent> {
protected getComponentName(): string {
return '';
}
protected importThemedComponent(themeName: string): Promise<any> {
return Promise.resolve(undefined);
}
protected importUnthemedComponent(): Promise<any> {
return Promise.resolve(undefined);
}
}

View File

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -0,0 +1,7 @@
{
"extends": "../../../tsconfig.json",
"include": [
"src/**/*.ts"
],
"exclude": ["dist"]
}

13
lint/test/helpers.js Normal file
View File

@@ -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,
},
}));

View File

@@ -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<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);
});

View File

@@ -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<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');
`,
},
],
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: `
<ds-test-themeable/>
<ds-test-themeable></ds-test-themeable>
<ds-test-themeable [test]="something"></ds-test-themeable>
`,
},
],
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>
`,
},
]
});
});

52
lint/test/testing.ts Normal file
View File

@@ -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'),
});

View File

@@ -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();
});
});
});

23
lint/tsconfig.json Normal file
View File

@@ -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"
]
}

View File

@@ -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",

View File

@@ -55,6 +55,7 @@
}
},
"exclude": [
"cypress.config.ts"
"cypress.config.ts",
"lint"
]
}

134
yarn.lock
View File

@@ -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"