diff --git a/.eslintrc.js b/.eslintrc.js index e5a59c4c12..d3a4534888 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,7 +12,9 @@ module.exports = { parserOptions: { ecmaVersion: 2020, }, + plugins: ["schulcloud"], rules: { + "schulcloud/material-icon-imports": "error", "@typescript-eslint/no-explicit-any": "warn", "no-console": process.env.NODE_ENV === "production" ? "off" : "warn", "no-debugger": process.env.NODE_ENV === "production" ? "off" : "warn", diff --git a/Dockerfile b/Dockerfile index 8036526997..24c64b639f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ RUN apt update && apt install -y g++ libcairo2-dev libpango1.0-dev libjpeg-dev l WORKDIR /app COPY package.json package-lock.json ./ +COPY lib/eslint-plugin-schulcloud ./lib/eslint-plugin-schulcloud RUN npm ci COPY babel.config.js .eslintrc.js LICENSE.md .prettierrc.js tsconfig.json tsconfig.build.json .eslintignore .prettierignore ./ diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 37e515e7e3..6a094df10a 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -157,6 +157,7 @@ module.exports = { "@feature-news-form": getDir("src/modules/feature/news-form"), "@feature-media-shelf": getDir("src/modules/feature/media-shelf"), "@feature-room": getDir("src/modules/feature/room"), + "@icons": getDir("src/components/icons"), "@ui-alert": getDir("src/modules/ui/alert"), "@ui-board": getDir("src/modules/ui/board"), "@ui-chip": getDir("src/modules/ui/chip"), diff --git a/jest.config.js b/jest.config.js index ac71ab34b3..d54c09d935 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,6 +18,7 @@ const config = { "^@page-(.*)$": "/src/modules/page/$1", "^@ui-(.*)$": "/src/modules/ui/$1", "^@util-(.*)$": "/src/modules/util/$1", + "^@icons(.*)$": "/src/components/icons/$1", "^@/(.*)$": "/src/$1", "^@@/(.*)$": "/$1", }, @@ -55,6 +56,7 @@ const config = { "/src/utils/**/*.(js|ts)", "/src/composables/**/*.(js|ts)", "/src/layouts/**/*.{js,ts,vue}", + "/lib/eslint-plugin-schulcloud/**/*.{js,ts,vue}", // Exclude "!/src/components/base/**/*", "!/src/components/icons/**/*", diff --git a/lib/eslint-plugin-schulcloud/index.js b/lib/eslint-plugin-schulcloud/index.js new file mode 100644 index 0000000000..160bc91c04 --- /dev/null +++ b/lib/eslint-plugin-schulcloud/index.js @@ -0,0 +1,7 @@ +const materialIconImportRule = require("./material-icon-imports"); + +module.exports = { + rules: { + "material-icon-imports": materialIconImportRule, + }, +}; diff --git a/lib/eslint-plugin-schulcloud/material-icon-imports.js b/lib/eslint-plugin-schulcloud/material-icon-imports.js new file mode 100644 index 0000000000..146ed119f8 --- /dev/null +++ b/lib/eslint-plugin-schulcloud/material-icon-imports.js @@ -0,0 +1,38 @@ +module.exports = { + meta: { + type: "problem", + docs: { + description: + "Enforce that icons are imported from our own subset of icons instead of the full Material Icons library.", + }, + messages: { + noDirectIconImport: + "Material icons should only be imported from '@icons/material'.", + }, + fixable: "code", + }, + + create(context) { + return { + ImportDeclaration(node) { + const importPath = node.source.value; + + node.specifiers.forEach((specifier) => { + const isMaterialIconImport = + specifier.type === "ImportSpecifier" && + /^mdi[A-Z]/.test(specifier.imported.name); // match 'mdi' followed by a capital letter + + if (isMaterialIconImport && importPath !== "@icons/material") { + context.report({ + node: specifier, + messageId: "noDirectIconImport", + fix: (fixer) => { + return fixer.replaceText(node.source, '"@icons/material"'); + }, + }); + } + }); + }, + }; + }, +}; diff --git a/lib/eslint-plugin-schulcloud/material-icon-imports.unit.js b/lib/eslint-plugin-schulcloud/material-icon-imports.unit.js new file mode 100644 index 0000000000..ed01450cd4 --- /dev/null +++ b/lib/eslint-plugin-schulcloud/material-icon-imports.unit.js @@ -0,0 +1,44 @@ +"use strict"; + +const { RuleTester } = require("eslint"); +const rule = require("./material-icon-imports.js"); + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, +}); + +ruleTester.run("material-icon-imports", rule, { + valid: [ + { + code: `import { mdiCheck } from "@icons/material";`, + }, + { + code: `import { useI18n } from "vue-i18n";`, + }, + { + code: `import { mdi } from "vuetify/iconsets/mdi-svg";`, + }, + ], + invalid: [ + { + code: `import { mdiCheck } from "@mdi/js";`, + errors: [{ messageId: "noDirectIconImport" }], + output: `import { mdiCheck } from "@icons/material";`, + }, + { + code: `import { mdiAlert } from "@/components/icons/material";`, + errors: [{ messageId: "noDirectIconImport" }], + output: `import { mdiAlert } from "@icons/material";`, + }, + { + code: `import { mdiCheck } from "some-other-path";`, + errors: [{ messageId: "noDirectIconImport" }], + output: `import { mdiCheck } from "@icons/material";`, + }, + { + code: `import { mdiCheck } from "../icons/material";`, + errors: [{ messageId: "noDirectIconImport" }], + output: `import { mdiCheck } from "@icons/material";`, + }, + ], +}); diff --git a/package-lock.json b/package-lock.json index 2678f37597..9d9a5fefaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-schulcloud": "file:lib/eslint-plugin-schulcloud", "eslint-plugin-vue": "^9.23.0", "eslint-webpack-plugin": "^4.1.0", "fishery": "^2.2.2", @@ -92,6 +93,13 @@ "npm": ">=9" } }, + "eslint-plugin-schulcloud": { + "version": "1.0.0", + "extraneous": true + }, + "lib/eslint-plugin-schulcloud": { + "dev": true + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -7672,6 +7680,10 @@ } } }, + "node_modules/eslint-plugin-schulcloud": { + "resolved": "lib/eslint-plugin-schulcloud", + "link": true + }, "node_modules/eslint-plugin-vue": { "version": "9.27.0", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz", diff --git a/package.json b/package.json index c980de5257..46d19bb7dc 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "test:unit": "npx jest", "test:unit:ci": "npm run test:unit -- --coverage --ci --maxWorkers=4", "lint": "npx eslint 'src/**/*.{ts,js,vue}'", + "lint:fix": "npx eslint 'src/**/*.{ts,js,vue}' --fix", "generate-client:server": "node generate-client.js -c openapitools-for-server.json", "generate-client:filestorage": "node generate-client.js -u 'http://localhost:4444/api/v3/docs-json/' -p 'src/fileStorageApi/v3' -c 'openapitools-for-file-storage.json'", "generate-client:h5p-editor": "node generate-client.js -u 'http://localhost:4448/api/v3/docs-json/' -p 'src/h5pEditorApi/v3' -c 'openapitools-for-h5p-editor.json'" @@ -73,6 +74,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-vue": "^9.23.0", + "eslint-plugin-schulcloud": "file:lib/eslint-plugin-schulcloud", "eslint-webpack-plugin": "^4.1.0", "fishery": "^2.2.2", "html-webpack-plugin": "^5.6.0", diff --git a/src/components/administration/AdminMigrationSection.vue b/src/components/administration/AdminMigrationSection.vue index f9abcf27a2..18beaa9ec9 100644 --- a/src/components/administration/AdminMigrationSection.vue +++ b/src/components/administration/AdminMigrationSection.vue @@ -201,7 +201,7 @@ diff --git a/src/components/atoms/InfoMessage.vue b/src/components/atoms/InfoMessage.vue index 540c9cbb2d..61e64e31b5 100644 --- a/src/components/atoms/InfoMessage.vue +++ b/src/components/atoms/InfoMessage.vue @@ -5,7 +5,7 @@