web: update to ESLint 9 (#10812)

* web: update to ESLint 9

ESLint 9 has been out for awhile now, and all of the plug-ins that we use have caught up, so it is
time to bite the bullet and upgrade.  This commit:

- upgrades to ESLint 9, and upgrades all associated plugins
- Replaces the `.eslintrc` and `.eslintignore` files with the new, "flat" configuration file,
  "eslint.config.mjs".
- Places the previous "precommit" and "nightmare" rules in `./scripts/eslint.precommit.mjs` and
  `./scripts/eslint.nightmare.mjs`, respectively
- Replaces the scripted wrappers for eslint (`eslint`, `eslint-precommit`) with a single executable
  that takes the arguments `--precommit`, which applies a stricter set of rules, and `--nightmare`,
  which applies an even more terrifyingly strict set of rules.
- Provides the scripted wrapper `./scripts/eslint.mjs` so that eslint can be run from `bun`, if one
  so chooses.
- Fixes *all* of the lint `eslint.config.mjs` now finds, including removing all of the `eslint`
  styling rules and overrides because Eslint now proudly leaves that entirely up to Prettier.

To shut Dependabot up about ESLint.

* Added explanation for no-console removal.

* web: did not need the old and unmaintained nightmare mode; it can be configured directly.
This commit is contained in:
Ken Sternberg
2024-08-07 15:04:18 -07:00
committed by GitHub
parent 322ae4c4ed
commit 79c01ca473
34 changed files with 5658 additions and 5076 deletions

View File

@ -1,9 +0,0 @@
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
dist
# don't lint nyc coverage output
coverage
src/locale-codes.ts
storybook-static/
src/locales/**

View File

@ -1,38 +0,0 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:lit/recommended",
"plugin:custom-elements/recommended",
"plugin:storybook/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module",
"project": true
},
"plugins": ["@typescript-eslint", "lit", "custom-elements"],
"ignorePatterns": ["authentik-live-tests/**"],
"rules": {
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", { "avoidEscape": true }],
"semi": ["error", "always"],
"@typescript-eslint/ban-ts-comment": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"no-console": ["error", { "allow": ["debug", "warn", "error"] }]
}
}

View File

@ -16,15 +16,13 @@ try {
authentikProjectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
encoding: "utf8",
}).replace("\n", "");
} catch (exc) {
} catch (_exc) {
// We probably don't have a .git folder, which could happen in container builds
}
const rootPackage = JSON.parse(fs.readFileSync(path.join(authentikProjectRoot, "./package.json")));
// eslint-disable-next-line no-undef
const isProdBuild = process.env.NODE_ENV === "production";
// eslint-disable-next-line no-undef
const apiBasePath = process.env.AK_API_BASE_PATH || "";
const envGitHashKey = "GIT_BUILD_HASH";
@ -35,10 +33,11 @@ const definitions = {
"process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
};
// All is magic is just to make sure the assets are copied into the right places. This is a very stripped down version
// of what the rollup-copy-plugin does, without any of the features we don't use, and using globSync instead of globby
// since we already had globSync lying around thanks to Typescript. If there's a third argument in an array entry, it's
// used to replace the internal path before concatenating it all together as the destination target.
// All is magic is just to make sure the assets are copied into the right places. This is a very
// stripped down version of what the rollup-copy-plugin does, without any of the features we don't
// use, and using globSync instead of globby since we already had globSync lying around thanks to
// Typescript. If there's a third argument in an array entry, it's used to replace the internal path
// before concatenating it all together as the destination target.
const otherFiles = [
["node_modules/@patternfly/patternfly/patternfly.min.css", "."],
@ -67,8 +66,8 @@ for (const [source, rawdest, strip] of otherFiles) {
}
}
// This starts the definitions used for esbuild: Our targets, our arguments, the function for running a build, and three
// options for building: watching, building, and building the proxy.
// This starts the definitions used for esbuild: Our targets, our arguments, the function for
// running a build, and three options for building: watching, building, and building the proxy.
// Ordered by largest to smallest interface to build even faster
const interfaces = [
["admin/AdminInterface/AdminInterface.ts", "admin"],
@ -104,7 +103,6 @@ function getVersion() {
async function buildOneSource(source, dest) {
const DIST = path.join(__dirname, "./dist", dest);
// eslint-disable-next-line no-console
console.log(`[${new Date(Date.now()).toISOString()}] Starting build for target ${source}`);
try {
@ -116,7 +114,6 @@ async function buildOneSource(source, dest) {
outdir: DIST,
});
const end = Date.now();
// eslint-disable-next-line no-console
console.log(
`[${new Date(end).toISOString()}] Finished build for target ${source} in ${Date.now() - start}ms`,
);
@ -135,14 +132,12 @@ function debouncedBuild() {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
// eslint-disable-next-line no-console
console.clear();
buildAuthentik(interfaces);
}, 250);
}
if (process.argv.length > 2 && (process.argv[2] === "-h" || process.argv[2] === "--help")) {
// eslint-disable-next-line no-console
console.log(`Build the authentikUI
options:
@ -154,7 +149,6 @@ options:
}
if (process.argv.length > 2 && (process.argv[2] === "-w" || process.argv[2] === "--watch")) {
// eslint-disable-next-line no-console
console.log("Watching ./src for changes");
chokidar.watch("./src").on("all", (event, path) => {
if (!["add", "change", "unlink"].includes(event)) {

80
web/eslint.config.mjs Normal file
View File

@ -0,0 +1,80 @@
import eslint from "@eslint/js";
import tsparser from "@typescript-eslint/parser";
import litconf from "eslint-plugin-lit";
import wcconf from "eslint-plugin-wc";
import globals from "globals";
import tseslint from "typescript-eslint";
export default [
// You would not believe how much this change has frustrated users: ["if an ignores key is used
// without any other keys in the configuration object, then the patterns act as global
// ignores"](https://eslint.org/docs/latest/use/configure/ignore)
{
ignores: [
"dist/",
// don't ever lint node_modules
"node_modules/",
".storybook/*",
// don't lint build output (make sure it's set to your correct build folder name)
// don't lint nyc coverage output
"coverage/",
"src/locale-codes.ts",
"storybook-static/",
"src/locales/",
],
},
eslint.configs.recommended,
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
},
files: ["src/**"],
rules: {
"no-unused-vars": "off",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.nodeBuiltin,
},
},
files: ["scripts/*.mjs", "*.ts", "*.mjs"],
rules: {
"no-unused-vars": "off",
// We WANT our scripts to output to the console!
"no-console": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
];

9810
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,8 @@
"watch": "run-s build-locales build:manifest esbuild:watch",
"lint": "cross-env NODE_OPTIONS='--max_old_space_size=65536' eslint . --max-warnings 0 --fix",
"lint:lockfile": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https",
"lint:precommit": "bun scripts/eslint-precommit.mjs",
"lint:precommit": "bun ./scripts/eslint.mjs --precommit",
"lint:nightmare": "bun ./scripts/eslint.mjs --nightmare",
"lint:spelling": "node scripts/check-spelling.mjs",
"lit-analyse": "lit-analyzer src",
"lit-analyse:strict": "lit-analyzer src --strict",
@ -86,6 +87,7 @@
"@babel/preset-typescript": "^7.24.7",
"@changesets/cli": "^2.27.5",
"@custom-elements-manifest/analyzer": "^0.10.2",
"@eslint/js": "^9.8.0",
"@genesiscommunitysuccess/custom-elements-lsp": "^5.0.3",
"@hcaptcha/types": "^1.0.4",
"@jeysal/storybook-addon-css-user-preferences": "^0.2.0",
@ -102,11 +104,12 @@
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "5.60.15",
"@types/eslint__js": "^8.42.3",
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "1.5.2",
"@types/showdown": "^2.0.6",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1",
"@wdio/browser-runner": "^8.40.1",
"@wdio/cli": "^8.40.0",
"@wdio/mocha-framework": "^8.40.0",
@ -116,14 +119,14 @@
"chokidar": "^3.6.0",
"cross-env": "^7.0.3",
"esbuild": "^0.23.0",
"eslint": "^8.57.0",
"eslint": "^9.8.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-lit": "^1.11.0",
"eslint-plugin-sonarjs": "^1.0.3",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-lit": "^1.14.0",
"eslint-plugin-sonarjs": "^1.0.4",
"eslint-plugin-wc": "^2.1.0",
"github-slugger": "^2.0.0",
"glob": "^11.0.0",
"globals": "^15.9.0",
"lit-analyzer": "^2.0.3",
"lockfile-lint": "^4.14.0",
"npm-run-all": "^4.1.5",
@ -140,6 +143,7 @@
"tslib": "^2.6.3",
"turnstile-types": "^1.2.1",
"typescript": "^5.5.4",
"typescript-eslint": "^8.0.1",
"vite-tsconfig-paths": "^4.3.2",
"wdio-wait-for": "^3.0.11"
},

View File

@ -60,9 +60,7 @@ if (!upToDate) {
.map((locale) => `Locale '${locale}' has ${counts.get(locale)} missing translations`)
.join("\n");
// eslint-disable-next-line no-console
console.log(`Translation tables rebuilt.\n${report}\n`);
}
// eslint-disable-next-line no-console
console.log("Locale ./src is up-to-date");

View File

@ -4,7 +4,6 @@ import { fileURLToPath } from "url";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function* walkFilesystem(dir) {
const openeddir = fs.opendirSync(dir);
if (!openeddir) {

View File

@ -12,5 +12,4 @@ const cmd = [
"-S './src/locales/**' ./src -s",
].join(" ");
// eslint-disable-next-line no-console
console.log(execSync(cmd, { encoding: "utf8" }));

View File

@ -1,55 +0,0 @@
import { execFileSync } from "child_process";
import { ESLint } from "eslint";
import path from "path";
import process from "process";
// Code assumes this script is in the './web/scripts' folder.
const projectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
encoding: "utf8",
}).replace("\n", "");
process.chdir(path.join(projectRoot, "./web"));
const eslintConfig = {
overrideConfig: {
env: {
browser: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:lit/recommended",
"plugin:custom-elements/recommended",
"plugin:storybook/recommended",
"plugin:sonarjs/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
plugins: ["@typescript-eslint", "lit", "custom-elements", "sonarjs"],
rules: {
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", { avoidEscape: true }],
"semi": ["error", "always"],
"@typescript-eslint/ban-ts-comment": "off",
"sonarjs/cognitive-complexity": ["error", 9],
"sonarjs/no-duplicate-string": "off",
"sonarjs/no-nested-template-literals": "off",
},
},
};
const updated = ["./src/", "./build.mjs", "./scripts/*.mjs"];
const eslint = new ESLint(eslintConfig);
const results = await eslint.lintFiles(updated);
const formatter = await eslint.loadFormatter("stylish");
const resultText = formatter.format(results);
const errors = results.reduce((acc, result) => acc + result.errorCount, 0);
// eslint-disable-next-line no-console
console.log(resultText);
process.exit(errors > 1 ? 1 : 0);

View File

@ -1,83 +0,0 @@
import { execFileSync } from "child_process";
import { ESLint } from "eslint";
import path from "path";
import process from "process";
// Code assumes this script is in the './web/scripts' folder.
const projectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
encoding: "utf8",
}).replace("\n", "");
process.chdir(path.join(projectRoot, "./web"));
const eslintConfig = {
overrideConfig: {
env: {
browser: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:lit/recommended",
"plugin:custom-elements/recommended",
"plugin:storybook/recommended",
"plugin:sonarjs/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
plugins: ["@typescript-eslint", "lit", "custom-elements", "sonarjs"],
ignorePatterns: ["!./.storybook/**/*.ts"],
rules: {
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", { avoidEscape: true }],
"semi": ["error", "always"],
"@typescript-eslint/ban-ts-comment": "off",
"sonarjs/cognitive-complexity": ["warn", 9],
"sonarjs/no-duplicate-string": "off",
"sonarjs/no-nested-template-literals": "off",
},
},
};
const porcelainV1 = /^(..)\s+(.*$)/;
const gitStatus = execFileSync("git", ["status", "--porcelain", "."], { encoding: "utf8" });
const statuses = gitStatus.split("\n").reduce((acc, line) => {
const match = porcelainV1.exec(line.replace("\n"));
if (!match) {
return acc;
}
const [status, path] = Array.from(match).slice(1, 3);
return [...acc, [status, path.split("\x00")[0]]];
}, []);
const isModified = /^(M|\?|\s)(M|\?|\s)/;
const modified = (s) => isModified.test(s);
const isCheckable = /\.(ts|js|mjs)$/;
const checkable = (s) => isCheckable.test(s);
const ignored = /\/\.storybook\//;
const notIgnored = (s) => !ignored.test(s);
const updated = statuses.reduce(
(acc, [status, filename]) =>
modified(status) && checkable(filename) && notIgnored(filename)
? [...acc, path.join(projectRoot, filename)]
: acc,
[],
);
const eslint = new ESLint(eslintConfig);
const results = await eslint.lintFiles(updated);
const formatter = await eslint.loadFormatter("stylish");
const resultText = formatter.format(results);
const errors = results.reduce((acc, result) => acc + result.errorCount, 0);
// eslint-disable-next-line no-console
console.log(resultText);
process.exit(errors > 1 ? 1 : 0);

View File

@ -1,63 +1,56 @@
#!/usr/bin/env node --max_old_space_size=65536
import { execFileSync } from "child_process";
import { ESLint } from "eslint";
import fs from "fs";
import path from "path";
import process from "process";
import { fileURLToPath } from "url";
// Code assumes this script is in the './web/scripts' folder.
const projectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
encoding: "utf8",
}).replace("\n", "");
process.chdir(path.join(projectRoot, "./web"));
function changedFiles() {
const gitStatus = execFileSync("git", ["diff", "--name-only", "HEAD"], { encoding: "utf8" });
const gitUntracked = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
encoding: "utf8",
});
const eslintConfig = {
fix: true,
overrideConfig: {
env: {
browser: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:lit/recommended",
"plugin:custom-elements/recommended",
"plugin:storybook/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
project: true,
},
plugins: ["@typescript-eslint", "lit", "custom-elements"],
ignorePatterns: ["authentik-live-tests/**"],
rules: {
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", { avoidEscape: true }],
"semi": ["error", "always"],
"@typescript-eslint/ban-ts-comment": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
},
},
};
const changed = gitStatus
.split("\n")
.filter((line) => line.trim().substring(0, 4) === "web/")
.filter((line) => /\.(m|c)?(t|j)s$/.test(line))
.map((line) => line.substring(4))
.filter((line) => fs.existsSync(line));
const eslint = new ESLint(eslintConfig);
const results = await eslint.lintFiles(".");
const untracked = gitUntracked
.split("\n")
.filter((line) => /\.(m|c)?(t|j)s$/.test(line))
.filter((line) => fs.existsSync(line));
const sourceFiles = [...changed, ...untracked].filter((line) => /^src\//.test(line));
const scriptFiles = [...changed, ...untracked].filter(
(line) => /^scripts\//.test(line) || !/^src\//.test(line),
);
return [...sourceFiles, ...scriptFiles];
}
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const projectRoot = path.join(__dirname, "..");
process.chdir(projectRoot);
const hasFlag = (flags) => process.argv.length > 1 && flags.includes(process.argv[2]);
const [configFile, files] = hasFlag(["-n", "--nightmare"])
? [path.join(__dirname, "eslint.nightmare.mjs"), changedFiles()]
: hasFlag(["-p", "--precommit"])
? [path.join(__dirname, "eslint.precommit.mjs"), changedFiles()]
: [path.join(projectRoot, "eslint.config.mjs"), ["."]];
const eslint = new ESLint({
overrideConfigFile: configFile,
warnIgnored: false,
});
const results = await eslint.lintFiles(files);
const formatter = await eslint.loadFormatter("stylish");
const resultText = formatter.format(results);
const errors = results.reduce((acc, result) => acc + result.errorCount, 0);
// eslint-disable-next-line no-console
console.log(resultText);
process.exit(errors > 1 ? 1 : 0);

View File

@ -0,0 +1,201 @@
import eslint from "@eslint/js";
import tsparser from "@typescript-eslint/parser";
import litconf from "eslint-plugin-lit";
import sonar from "eslint-plugin-sonarjs";
import wcconf from "eslint-plugin-wc";
import globals from "globals";
import tseslint from "typescript-eslint";
export default [
// You would not believe how much this change has frustrated users: ["if an ignores key is used
// without any other keys in the configuration object, then the patterns act as global
// ignores"](https://eslint.org/docs/latest/use/configure/ignore)
{
ignores: [
"dist/",
// don't ever lint node_modules
"node_modules/",
".storybook/*",
// don't lint build output (make sure it's set to your correct build folder name)
// don't lint nyc coverage output
"coverage/",
"src/locale-codes.ts",
"storybook-static/",
"src/locales/",
],
},
eslint.configs.recommended,
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
sonar.configs.recommended,
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.browser,
},
},
files: ["src/**"],
rules: {
"accessor-pairs": "error",
"array-callback-return": "error",
"block-scoped-var": "error",
"consistent-return": "error",
"consistent-this": ["error", "that"],
"curly": ["error", "all"],
"dot-notation": [
"error",
{
allowKeywords: true,
},
],
"eqeqeq": "error",
"func-names": "error",
"guard-for-in": "error",
"max-depth": ["error", 4],
"max-nested-callbacks": ["error", 4],
"max-params": ["error", 5],
"new-cap": "error",
"no-alert": "error",
"no-array-constructor": "error",
"no-bitwise": "error",
"no-caller": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-div-regex": "error",
"no-dupe-args": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-else-return": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-function": "error",
"no-labels": "error",
"no-eq-null": "error",
"no-eval": "error",
"no-ex-assign": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-boolean-cast": "error",
"no-extra-label": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-implied-eval": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-inner-declarations": ["error", "functions"],
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-iterator": "error",
"no-invalid-this": "error",
"no-label-var": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-magic-numbers": ["error", { ignore: [0, 1, -1] }],
"no-multi-str": "error",
"no-negated-condition": "error",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-param-reassign": "error",
"no-proto": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-restricted-syntax": ["error", "WithStatement"],
"no-script-url": "error",
"no-self-assign": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef": "error",
"no-undef-init": "error",
"no-unexpected-multiline": "error",
"no-useless-constructor": "error",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unreachable": "error",
"no-unused-expressions": "error",
"no-unused-labels": "error",
"no-use-before-define": "error",
"no-useless-call": "error",
"no-dupe-class-members": "error",
"no-var": "error",
"no-void": "error",
"no-with": "error",
"prefer-arrow-callback": "error",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"radix": "error",
"require-yield": "error",
"strict": ["error", "global"],
"use-isnan": "error",
"valid-typeof": "error",
"vars-on-top": "error",
"yoda": ["error", "never"],
"no-unused-vars": "off",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
"sonarjs/cognitive-complexity": ["off", 9],
"sonarjs/no-duplicate-string": "off",
"sonarjs/no-nested-template-literals": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.nodeBuiltin,
},
},
files: ["scripts/*.mjs", "*.ts", "*.mjs"],
rules: {
"no-unused-vars": "off",
"no-console": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
];

View File

@ -0,0 +1,84 @@
import eslint from "@eslint/js";
import tsparser from "@typescript-eslint/parser";
import litconf from "eslint-plugin-lit";
import sonar from "eslint-plugin-sonarjs";
import wcconf from "eslint-plugin-wc";
import globals from "globals";
import tseslint from "typescript-eslint";
export default [
// You would not believe how much this change has frustrated users: ["if an ignores key is used
// without any other keys in the configuration object, then the patterns act as global
// ignores"](https://eslint.org/docs/latest/use/configure/ignore)
{
ignores: [
"dist/",
// don't ever lint node_modules
"node_modules/",
".storybook/*",
// don't lint build output (make sure it's set to your correct build folder name)
// don't lint nyc coverage output
"coverage/",
"src/locale-codes.ts",
"storybook-static/",
"src/locales/",
],
},
eslint.configs.recommended,
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
sonar.configs.recommended,
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
},
files: ["src/**"],
rules: {
"no-unused-vars": "off",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
"sonarjs/cognitive-complexity": ["off", 9],
"sonarjs/no-duplicate-string": "off",
"sonarjs/no-nested-template-literals": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.nodeBuiltin,
},
},
files: ["scripts/*.mjs", "*.ts", "*.mjs"],
rules: {
"no-unused-vars": "off",
"no-console": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
];

View File

@ -154,6 +154,8 @@ class Stage<T extends FlowInfoChallenge> {
}
}
const IS_INVALID = "is-invalid";
class IdentificationStage extends Stage<IdentificationChallenge> {
render() {
this.html(`
@ -173,7 +175,7 @@ class IdentificationStage extends Stage<IdentificationChallenge> {
${
this.challenge.passwordFields
? `<div class="form-label-group my-3 has-validation">
<input type="password" class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
<input type="password" class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
${this.renderInputError("password")}
</div>`
: ""
@ -197,7 +199,7 @@ class PasswordStage extends Stage<PasswordChallenge> {
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="form-label-group my-3 has-validation">
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
${this.renderInputError("password")}
</div>
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
@ -309,12 +311,10 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
return Object.assign({}, credentialCreateOptions, {
challenge,
user,
});
return transformedCredentialCreateOptions;
}
/**
@ -354,12 +354,10 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
},
);
const transformedCredentialRequestOptions = Object.assign({}, credentialRequestOptions, {
return Object.assign({}, credentialRequestOptions, {
challenge,
allowCredentials,
});
return transformedCredentialRequestOptions;
}
/**
@ -407,14 +405,11 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
}
renderChallengePicker() {
const challenges = this.challenge.deviceChallenges.filter((challenge) => {
if (challenge.deviceClass === "webauthn") {
if (!this.checkWebAuthnSupport()) {
return undefined;
}
}
return challenge;
});
const challenges = this.challenge.deviceChallenges.filter((challenge) =>
challenge.deviceClass === "webauthn" && !this.checkWebAuthnSupport()
? undefined
: challenge,
);
this.html(`<form id="picker-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
@ -467,7 +462,7 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="form-label-group my-3 has-validation">
<input type="text" autofocus class="form-control ${this.error("code").length > 0 ? "is-invalid" : ""}" name="code" placeholder="Please enter your code" autocomplete="one-time-code">
<input type="text" autofocus class="form-control ${this.error("code").length > 0 ? IS_INVALID : ""}" name="code" placeholder="Please enter your code" autocomplete="one-time-code">
${this.renderInputError("code")}
</div>
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>

View File

@ -93,7 +93,10 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
// a browser reflow, which may trigger some other styling the application is monitoring,
// triggering a re-render which triggers a browser reflow, ad infinitum. But we've been
// living with that since jQuery, and it's both well-known and fortunately rare.
// eslint-disable-next-line wc/no-self-class
this.classList.remove("pf-m-expanded", "pf-m-collapsed");
// eslint-disable-next-line wc/no-self-class
this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed");
}
@ -153,7 +156,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
? { ".activeWhen": attributes }
: (attributes ?? {});
if (path) {
properties["path"] = path;
properties.path = path;
}
return html`<ak-sidebar-item ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}

View File

@ -26,11 +26,6 @@ const selectStyles = css`
*/
@customElement("ak-multi-select")
export class AkMultiSelect extends AkControlElement {
constructor() {
super();
this.dataset.akControl = "true";
}
static get styles() {
return [PFBase, PFForm, PFFormControl, selectStyles];
}
@ -94,6 +89,11 @@ export class AkMultiSelect extends AkControlElement {
return this.values;
}
connectedCallback() {
super.connectedCallback();
this.dataset.akControl = "true";
}
renderHelp() {
return [
this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing,

View File

@ -8,8 +8,11 @@ import { classMap } from "lit/directives/class-map.js";
import PFLabel from "@patternfly/patternfly/components/Label/label.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
const statusNames = ["error", "warning", "info"] as const;
type StatusName = (typeof statusNames)[number];
// The 'const ... as const' construction will throw a compilation error if the const variable is
// only ever used to generate the type information, so the `_` (ignore unused variable) prefix must
// be used here.
const _statusNames = ["error", "warning", "info"] as const;
type StatusName = (typeof _statusNames)[number];
const statusToDetails = new Map<StatusName, [string, string]>([
["error", ["pf-m-red", "fa-times"]],

View File

@ -73,9 +73,10 @@ export class CodeMirrorTextarea<T> extends AKElement {
}
@property()
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types
set value(v: T | string) {
if (v === null || v === undefined) return;
if (v === null || v === undefined) {
return;
}
// Value might be an object if within an iron-form, as that calls the getter of value
// in the beginning and the calls this setter on reset
let textValue = v;
@ -114,7 +115,7 @@ export class CodeMirrorTextarea<T> extends AKElement {
default:
return this.getInnerValue();
}
} catch (e) {
} catch (_e: unknown) {
return this.getInnerValue();
}
}

View File

@ -136,7 +136,6 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
constructor() {
super();
this.onClick = this.onClick.bind(this);
this.dataset.akControl = "true";
}
onClick(ev: Event) {
@ -173,6 +172,7 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
connectedCallback() {
super.connectedCallback();
this.dataset.akControl = "true";
if (this.name && !this.internals) {
this.internals = this.attachInternals();
}

View File

@ -56,7 +56,6 @@ const container = (testItem: TemplateResult) =>
const displayMessage = (result: any) => {
const doc = new DOMParser().parseFromString(`<li><i>Event</i>: ${result}</li>`, "text/xml");
const target = document.querySelector("#action-button-message-pad");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
target!.appendChild(doc.firstChild!);
};

View File

@ -42,7 +42,6 @@ const container = (testItem: TemplateResult) =>
const displayMessage = (result: any) => {
const doc = new DOMParser().parseFromString(`<p><i>Content</i>: ${result}</p>`, "text/xml");
const target = document.querySelector("#action-button-message-pad");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
target!.replaceChildren(doc.firstChild!);
};
@ -51,7 +50,6 @@ const displayMessage2 = (result: any) => {
console.debug("Huh.");
const doc = new DOMParser().parseFromString(`<p><i>Behavior</i>: ${result}</p>`, "text/xml");
const target = document.querySelector("#action-button-message-pad-2");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
target!.replaceChildren(doc.firstChild!);
};

View File

@ -52,7 +52,6 @@ const displayMessage = (result: any) => {
"text/xml",
);
const target = document.querySelector("#action-button-message-pad");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
target!.appendChild(doc.firstChild!);
};

View File

@ -6,25 +6,33 @@ import { customElement } from "lit/decorators.js";
@customElement("ak-dropdown")
export class DropdownButton extends AKElement {
menu: HTMLElement | null;
menu: HTMLElement | null = null;
constructor() {
super();
this.menu = this.querySelector<HTMLElement>(".pf-c-dropdown__menu");
this.querySelectorAll("button.pf-c-dropdown__toggle").forEach((btn) => {
btn.addEventListener("click", () => {
if (!this.menu) return;
this.menu.hidden = !this.menu.hidden;
});
});
window.addEventListener(EVENT_REFRESH, this.clickHandler);
}
clickHandler = (): void => {
if (!this.menu) return;
if (!this.menu) {
return;
}
this.menu.hidden = true;
};
connectedCallback() {
super.connectedCallback();
this.menu = this.querySelector<HTMLElement>(".pf-c-dropdown__menu");
this.querySelectorAll("button.pf-c-dropdown__toggle").forEach((btn) => {
btn.addEventListener("click", () => {
if (!this.menu) {
return;
}
this.menu.hidden = !this.menu.hidden;
});
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener(EVENT_REFRESH, this.clickHandler);

View File

@ -51,7 +51,6 @@ const displayMessage = (result: any) => {
"text/xml",
);
const target = document.querySelector("#action-button-message-pad");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
target!.appendChild(doc.firstChild!);
};

View File

@ -98,7 +98,6 @@ export class ModalOrchestrationController implements ReactiveController {
// Pop off modals until you find the first live one, schedule it to be closed, and make that
// cleaned list the current state. Since this is our *only* state object, this has the
// effect of creating a new "knownModals" collection with some semantics.
// eslint-disable-next-line no-constant-condition
while (true) {
const modal = knownModals.pop();
if (!modal) {

View File

@ -48,7 +48,9 @@ function assignValue(element: HTMLNamedElement, value: unknown, json: KeyUnknown
for (let index = 0; index < nameElements.length - 1; index++) {
const nameEl = nameElements[index];
// Ensure all nested structures exist
if (!(nameEl in parent)) parent[nameEl] = {};
if (!(nameEl in parent)) {
parent[nameEl] = {};
}
parent = parent[nameEl] as { [key: string]: unknown };
}
parent[nameElements[nameElements.length - 1]] = value;
@ -103,7 +105,7 @@ export function serializeForm<T extends KeyUnknown>(
} else if (
inputElement.tagName.toLowerCase() === "input" &&
"type" in inputElement.dataset &&
inputElement.dataset["type"] === "datetime-local"
inputElement.dataset.type === "datetime-local"
) {
// Workaround for Firefox <93, since 92 and older don't support
// datetime-local fields
@ -122,6 +124,9 @@ export function serializeForm<T extends KeyUnknown>(
return json as unknown as T;
}
const HTTP_BAD_REQUEST = 400;
const HTTP_INTERNAL_SERVICE_ERROR = 500;
/**
* Form
*
@ -188,7 +193,7 @@ export abstract class Form<T> extends AKElement {
*/
get isInViewport(): boolean {
const rect = this.getBoundingClientRect();
return !(rect.x + rect.y + rect.width + rect.height === 0);
return rect.x + rect.y + rect.width + rect.height !== 0;
}
getSuccessMessage(): string {
@ -275,7 +280,6 @@ export abstract class Form<T> extends AKElement {
}
return serializeForm(elements) as T;
}
/**
* Serialize and send the form to the destination. The `send()` method must be overridden for
* this to work. If processing the data results in an error, we catch the error, distribute
@ -304,9 +308,14 @@ export abstract class Form<T> extends AKElement {
} catch (ex) {
if (ex instanceof ResponseError) {
let msg = ex.response.statusText;
if (ex.response.status > 399 && ex.response.status < 500) {
if (
ex.response.status >= HTTP_BAD_REQUEST &&
ex.response.status < HTTP_INTERNAL_SERVICE_ERROR
) {
const errorMessage = ValidationErrorFromJSON(await ex.response.json());
if (!errorMessage) return errorMessage;
if (!errorMessage) {
return errorMessage;
}
if (errorMessage instanceof Error) {
throw errorMessage;
}
@ -318,7 +327,9 @@ export abstract class Form<T> extends AKElement {
elements.forEach((element) => {
element.requestUpdate();
const elementName = element.name;
if (!elementName) return;
if (!elementName) {
return;
}
if (camelToSnake(elementName) in errorMessage) {
element.errorMessages = errorMessage[camelToSnake(elementName)];
element.invalid = true;

View File

@ -20,7 +20,8 @@ import {
SearchSelectSelectEvent,
SearchSelectSelectMenuEvent,
} from "./SearchSelectEvents.js";
import type { SearchOptions, SearchTuple } from "./types.js";
import type { SearchOptions } from "./types.js";
import { optionsToOptionsMap } from "./utils.js";
/**
* @class SearchSelectView
@ -225,8 +226,8 @@ export class SearchSelectView extends AKElement {
}
updated() {
if (!(this.inputRef?.value && this.inputRef?.value?.value === this.displayValue)) {
this.inputRef.value && (this.inputRef.value.value = this.displayValue);
if (this.inputRef?.value && this.inputRef?.value?.value !== this.displayValue) {
this.inputRef.value.value = this.displayValue;
}
}
@ -264,21 +265,6 @@ export class SearchSelectView extends AKElement {
}
}
type Pair = [string, string];
const justThePair = ([key, label]: SearchTuple): Pair => [key, label];
function optionsToOptionsMap(options: SearchOptions): Map<string, string> {
const pairs: Pair[] = Array.isArray(options)
? options.map(justThePair)
: options.grouped
? options.options.reduce(
(acc: Pair[], { options }): Pair[] => [...acc, ...options.map(justThePair)],
[] as Pair[],
)
: options.options.map(justThePair);
return new Map(pairs);
}
declare global {
interface HTMLElementTagNameMap {
"ak-search-select-view": SearchSelectView;

View File

@ -97,11 +97,6 @@ export class SearchSelect<T> extends CustomEmitterElement(AkControlElement) {
@state()
error?: APIErrorTypes;
constructor() {
super();
this.dataset.akControl = "true";
}
toForm(): unknown {
if (!this.objects) {
throw new PreventFormSubmit(msg("Loading options..."));
@ -113,9 +108,9 @@ export class SearchSelect<T> extends CustomEmitterElement(AkControlElement) {
return this.toForm();
}
updateData() {
async updateData() {
if (this.isFetchingData) {
return;
return Promise.resolve();
}
this.isFetchingData = true;
return this.fetchObjects(this.query)
@ -140,6 +135,7 @@ export class SearchSelect<T> extends CustomEmitterElement(AkControlElement) {
connectedCallback(): void {
super.connectedCallback();
this.dataset.akControl = "true";
this.updateData();
this.addEventListener(EVENT_REFRESH, this.updateData);
}

View File

@ -29,7 +29,6 @@ const metadata: Meta<SearchSelectMenu> = {
export default metadata;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onClick = (event: SearchSelectSelectMenuEvent) => {
const target = document.querySelector("#action-button-message-pad");
target!.innerHTML = "";

View File

@ -0,0 +1,16 @@
import type { SearchOptions, SearchTuple } from "./types.js";
type Pair = [string, string];
const justThePair = ([key, label]: SearchTuple): Pair => [key, label];
export function optionsToOptionsMap(options: SearchOptions): Map<string, string> {
const pairs: Pair[] = Array.isArray(options)
? options.map(justThePair)
: options.grouped
? options.options.reduce(
(acc: Pair[], { options }): Pair[] => [...acc, ...options.map(justThePair)],
[] as Pair[],
)
: options.options.map(justThePair);
return new Map(pairs);
}

View File

@ -4,44 +4,44 @@
/**
* The locale code that templates in this source code are written in.
*/
export const sourceLocale = `en`;
export const sourceLocale = "en";
/**
* The other locale codes that this application is localized into. Sorted
* lexicographically.
*/
export const targetLocales = [
`de`,
`en`,
`es`,
`fr`,
`ko`,
`nl`,
`pl`,
`pseudo-LOCALE`,
`tr`,
`zh_TW`,
`zh-CN`,
`zh-Hans`,
`zh-Hant`,
"de",
"en",
"es",
"fr",
"ko",
"nl",
"pl",
"pseudo-LOCALE",
"tr",
"zh_TW",
"zh-CN",
"zh-Hans",
"zh-Hant",
] as const;
/**
* All valid project locale codes. Sorted lexicographically.
*/
export const allLocales = [
`de`,
`en`,
`en`,
`es`,
`fr`,
`ko`,
`nl`,
`pl`,
`pseudo-LOCALE`,
`tr`,
`zh_TW`,
`zh-CN`,
`zh-Hans`,
`zh-Hant`,
"de",
"en",
"en",
"es",
"fr",
"ko",
"nl",
"pl",
"pseudo-LOCALE",
"tr",
"zh_TW",
"zh-CN",
"zh-Hans",
"zh-Hant",
] as const;

View File

@ -104,7 +104,7 @@ export class LibraryPage extends AKElement {
searchUpdated(event: LibraryPageSearchUpdated) {
event.stopPropagation();
const apps = event.apps;
if (!(apps.length > 0)) {
if (apps.length <= 0) {
throw new Error(
"LibaryPageSearchUpdated had empty results body. This must not happen.",
);
@ -116,7 +116,9 @@ export class LibraryPage extends AKElement {
@bound
launchRequest(event: LibraryPageSearchSelected) {
event.stopPropagation();
this.selectedApp?.launchUrl && window.location.assign(this.selectedApp?.launchUrl);
if (this.selectedApp?.launchUrl) {
window.location.assign(this.selectedApp?.launchUrl);
}
}
@bound

View File

@ -117,7 +117,6 @@ const customStyles = css`
@customElement("ak-interface-user-presentation")
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class UserInterfacePresentation extends AKElement {
static get styles() {
return [
@ -169,7 +168,7 @@ class UserInterfacePresentation extends AKElement {
}
get isFullyConfigured() {
return !!(this.uiConfig && this.me && this.brand);
return Boolean(this.uiConfig && this.me && this.brand);
}
render() {
@ -455,7 +454,7 @@ export class UserInterface extends EnterpriseAwareInterface {
}
get isFullyConfigured() {
return !!(this.uiConfig && this.me);
return Boolean(this.uiConfig && this.me);
}
render() {