A bare <img> sent a Referer that Google's image CDNs refuse and had no retry and no fallback, so a Home shelf card showed the browser's broken-image mark. ui/RemoteImage gathers what Avatar, the channel banner and the download rows had each hand-rolled: no Referer, up to three retries past the browser's negative cache, the caller's fallback (by default the card's empty frame) instead of the broken mark, the failure remembered by URL. Every picture now goes through it; a lint rule refuses a bare <img>.
1399 lines
56 KiB
JSON
1399 lines
56 KiB
JSON
{
|
||
"eslint": {
|
||
"design-lab": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": { "local/literal-a11y-text": [0] },
|
||
"settings": null
|
||
},
|
||
"e2e": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "82 names, sha1 29467d490e30",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [
|
||
"local/button-primitive",
|
||
"local/glyph-link",
|
||
"local/input-primitive",
|
||
"local/jsx-comment-text",
|
||
"local/literal-a11y-text",
|
||
"local/locale-format",
|
||
"local/sortable-a11y",
|
||
"local/surface-role",
|
||
"no-console",
|
||
"no-restricted-globals",
|
||
"no-restricted-properties",
|
||
"no-restricted-syntax",
|
||
"react-hooks/capitalized-calls",
|
||
"react-hooks/component-hook-factories",
|
||
"react-hooks/config",
|
||
"react-hooks/error-boundaries",
|
||
"react-hooks/exhaustive-deps",
|
||
"react-hooks/gating",
|
||
"react-hooks/globals",
|
||
"react-hooks/hooks",
|
||
"react-hooks/immutability",
|
||
"react-hooks/incompatible-library",
|
||
"react-hooks/invariant",
|
||
"react-hooks/memo-dependencies",
|
||
"react-hooks/no-deriving-state-in-effects",
|
||
"react-hooks/preserve-manual-memoization",
|
||
"react-hooks/purity",
|
||
"react-hooks/refs",
|
||
"react-hooks/rules-of-hooks",
|
||
"react-hooks/set-state-in-effect",
|
||
"react-hooks/set-state-in-render",
|
||
"react-hooks/static-components",
|
||
"react-hooks/syntax",
|
||
"react-hooks/todo",
|
||
"react-hooks/unsupported-syntax",
|
||
"react-hooks/use-memo",
|
||
"react-hooks/void-use-memo",
|
||
"react-refresh/only-export-components"
|
||
],
|
||
"plugins": ["@", "@eslint-community/eslint-comments", "@typescript-eslint", "playwright"],
|
||
"rules": {
|
||
"@typescript-eslint/no-unused-expressions": [
|
||
2,
|
||
{ "allowShortCircuit": false, "allowTaggedTemplates": false, "allowTernary": false }
|
||
],
|
||
"playwright/no-focused-test": [2],
|
||
"playwright/no-skipped-test": [2]
|
||
},
|
||
"settings": null
|
||
},
|
||
"js configs": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "82 names, sha1 29467d490e30",
|
||
"parser": "(unnamed parser)",
|
||
"parserOptions": { "sourceType": "module" },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [
|
||
"@typescript-eslint/ban-ts-comment",
|
||
"@typescript-eslint/no-array-constructor",
|
||
"@typescript-eslint/no-deprecated",
|
||
"@typescript-eslint/no-duplicate-enum-values",
|
||
"@typescript-eslint/no-empty-object-type",
|
||
"@typescript-eslint/no-explicit-any",
|
||
"@typescript-eslint/no-extra-non-null-assertion",
|
||
"@typescript-eslint/no-floating-promises",
|
||
"@typescript-eslint/no-misused-new",
|
||
"@typescript-eslint/no-namespace",
|
||
"@typescript-eslint/no-non-null-asserted-optional-chain",
|
||
"@typescript-eslint/no-require-imports",
|
||
"@typescript-eslint/no-this-alias",
|
||
"@typescript-eslint/no-unnecessary-type-constraint",
|
||
"@typescript-eslint/no-unsafe-declaration-merging",
|
||
"@typescript-eslint/no-unsafe-function-type",
|
||
"@typescript-eslint/no-unused-expressions",
|
||
"@typescript-eslint/no-unused-vars",
|
||
"@typescript-eslint/no-wrapper-object-types",
|
||
"@typescript-eslint/prefer-as-const",
|
||
"@typescript-eslint/prefer-namespace-keyword",
|
||
"@typescript-eslint/triple-slash-reference",
|
||
"local/button-primitive",
|
||
"local/glyph-link",
|
||
"local/input-primitive",
|
||
"local/jsx-comment-text",
|
||
"local/literal-a11y-text",
|
||
"local/locale-format",
|
||
"local/sortable-a11y",
|
||
"local/surface-role",
|
||
"no-array-constructor",
|
||
"no-console",
|
||
"no-new-symbol",
|
||
"no-restricted-globals",
|
||
"no-restricted-properties",
|
||
"no-restricted-syntax",
|
||
"no-unused-expressions",
|
||
"no-var",
|
||
"prefer-const",
|
||
"prefer-rest-params",
|
||
"prefer-spread",
|
||
"react-hooks/capitalized-calls",
|
||
"react-hooks/component-hook-factories",
|
||
"react-hooks/config",
|
||
"react-hooks/error-boundaries",
|
||
"react-hooks/exhaustive-deps",
|
||
"react-hooks/gating",
|
||
"react-hooks/globals",
|
||
"react-hooks/hooks",
|
||
"react-hooks/immutability",
|
||
"react-hooks/incompatible-library",
|
||
"react-hooks/invariant",
|
||
"react-hooks/memo-dependencies",
|
||
"react-hooks/no-deriving-state-in-effects",
|
||
"react-hooks/preserve-manual-memoization",
|
||
"react-hooks/purity",
|
||
"react-hooks/refs",
|
||
"react-hooks/rules-of-hooks",
|
||
"react-hooks/set-state-in-effect",
|
||
"react-hooks/set-state-in-render",
|
||
"react-hooks/static-components",
|
||
"react-hooks/syntax",
|
||
"react-hooks/todo",
|
||
"react-hooks/unsupported-syntax",
|
||
"react-hooks/use-memo",
|
||
"react-hooks/void-use-memo",
|
||
"react-refresh/only-export-components"
|
||
],
|
||
"plugins": ["@", "@eslint-community/eslint-comments"],
|
||
"rules": {
|
||
"constructor-super": [2],
|
||
"getter-return": [2, { "allowImplicit": false }],
|
||
"no-class-assign": [2],
|
||
"no-const-assign": [2],
|
||
"no-dupe-args": [2],
|
||
"no-dupe-class-members": [2],
|
||
"no-dupe-keys": [2],
|
||
"no-func-assign": [2],
|
||
"no-import-assign": [2],
|
||
"no-new-native-nonconstructor": [2],
|
||
"no-obj-calls": [2],
|
||
"no-redeclare": [2, { "builtinGlobals": true }],
|
||
"no-setter-return": [2],
|
||
"no-this-before-super": [2],
|
||
"no-undef": [2, { "typeof": false }],
|
||
"no-unreachable": [2],
|
||
"no-unsafe-negation": [2, { "enforceForOrderingRelations": false }],
|
||
"no-unused-vars": [2],
|
||
"no-with": [2]
|
||
},
|
||
"settings": null
|
||
},
|
||
"lib tests": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh",
|
||
"vitest"
|
||
],
|
||
"rules": {
|
||
"local/glyph-link": [0],
|
||
"local/input-primitive": [0],
|
||
"local/literal-a11y-text": [0],
|
||
"local/surface-role": [0],
|
||
"no-restricted-globals": [
|
||
0,
|
||
{
|
||
"message": "Use the logged request layer: `fetch` belongs to src/lib/http.ts only — a request outside it never reaches the client log.",
|
||
"name": "fetch"
|
||
},
|
||
{
|
||
"message": "Use the logged request layer: `XMLHttpRequest` belongs to src/lib/api/core.ts only — a request outside it never reaches the client log.",
|
||
"name": "XMLHttpRequest"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "lib/ sits below components/: it may not import from components/, not even a type. Move the shared piece down into lib/.",
|
||
"regex": "^(\\.\\./)+components(/|$)"
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"no-restricted-properties": [
|
||
0,
|
||
{ "message": "Use lib/http.ts `send`.", "object": "window", "property": "fetch" },
|
||
{ "message": "Use lib/http.ts `send`.", "object": "globalThis", "property": "fetch" },
|
||
{
|
||
"message": "Use lib/api/core.ts `beacon`.",
|
||
"object": "navigator",
|
||
"property": "sendBeacon"
|
||
}
|
||
],
|
||
"vitest/no-disabled-tests": [2],
|
||
"vitest/no-focused-tests": [2, { "fixable": true }]
|
||
},
|
||
"settings": null
|
||
},
|
||
"lib/api/core.ts": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": {
|
||
"no-restricted-globals": [
|
||
2,
|
||
{
|
||
"message": "Use the logged request layer: `fetch` belongs to src/lib/http.ts only — a request outside it never reaches the client log.",
|
||
"name": "fetch"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "lib/ sits below components/: it may not import from components/, not even a type. Move the shared piece down into lib/.",
|
||
"regex": "^(\\.\\./)+components(/|$)"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"settings": null
|
||
},
|
||
"lib/http.ts": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": {
|
||
"no-restricted-globals": [
|
||
2,
|
||
{
|
||
"message": "Use the logged request layer: `XMLHttpRequest` belongs to src/lib/api/core.ts only — a request outside it never reaches the client log.",
|
||
"name": "XMLHttpRequest"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "lib/ sits below components/: it may not import from components/, not even a type. Move the shared piece down into lib/.",
|
||
"regex": "^(\\.\\./)+components(/|$)"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"settings": null
|
||
},
|
||
"src": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": {
|
||
"@babel/object-curly-spacing": [0],
|
||
"@babel/semi": [0],
|
||
"@eslint-community/eslint-comments/no-unlimited-disable": [2],
|
||
"@eslint-community/eslint-comments/require-description": [2, { "ignore": [] }],
|
||
"@stylistic/array-bracket-newline": [0],
|
||
"@stylistic/array-bracket-spacing": [0],
|
||
"@stylistic/array-element-newline": [0],
|
||
"@stylistic/arrow-parens": [0],
|
||
"@stylistic/arrow-spacing": [0],
|
||
"@stylistic/block-spacing": [0],
|
||
"@stylistic/brace-style": [0],
|
||
"@stylistic/comma-dangle": [0],
|
||
"@stylistic/comma-spacing": [0],
|
||
"@stylistic/comma-style": [0],
|
||
"@stylistic/computed-property-spacing": [0],
|
||
"@stylistic/dot-location": [0],
|
||
"@stylistic/eol-last": [0],
|
||
"@stylistic/func-call-spacing": [0],
|
||
"@stylistic/function-call-argument-newline": [0],
|
||
"@stylistic/function-call-spacing": [0],
|
||
"@stylistic/function-paren-newline": [0],
|
||
"@stylistic/generator-star-spacing": [0],
|
||
"@stylistic/implicit-arrow-linebreak": [0],
|
||
"@stylistic/indent": [0],
|
||
"@stylistic/indent-binary-ops": [0],
|
||
"@stylistic/js/array-bracket-newline": [0],
|
||
"@stylistic/js/array-bracket-spacing": [0],
|
||
"@stylistic/js/array-element-newline": [0],
|
||
"@stylistic/js/arrow-parens": [0],
|
||
"@stylistic/js/arrow-spacing": [0],
|
||
"@stylistic/js/block-spacing": [0],
|
||
"@stylistic/js/brace-style": [0],
|
||
"@stylistic/js/comma-dangle": [0],
|
||
"@stylistic/js/comma-spacing": [0],
|
||
"@stylistic/js/comma-style": [0],
|
||
"@stylistic/js/computed-property-spacing": [0],
|
||
"@stylistic/js/dot-location": [0],
|
||
"@stylistic/js/eol-last": [0],
|
||
"@stylistic/js/func-call-spacing": [0],
|
||
"@stylistic/js/function-call-argument-newline": [0],
|
||
"@stylistic/js/function-call-spacing": [0],
|
||
"@stylistic/js/function-paren-newline": [0],
|
||
"@stylistic/js/generator-star-spacing": [0],
|
||
"@stylistic/js/implicit-arrow-linebreak": [0],
|
||
"@stylistic/js/indent": [0],
|
||
"@stylistic/js/jsx-quotes": [0],
|
||
"@stylistic/js/key-spacing": [0],
|
||
"@stylistic/js/keyword-spacing": [0],
|
||
"@stylistic/js/linebreak-style": [0],
|
||
"@stylistic/js/lines-around-comment": [0],
|
||
"@stylistic/js/max-len": [0],
|
||
"@stylistic/js/max-statements-per-line": [0],
|
||
"@stylistic/js/multiline-ternary": [0],
|
||
"@stylistic/js/new-parens": [0],
|
||
"@stylistic/js/newline-per-chained-call": [0],
|
||
"@stylistic/js/no-confusing-arrow": [0],
|
||
"@stylistic/js/no-extra-parens": [0],
|
||
"@stylistic/js/no-extra-semi": [0],
|
||
"@stylistic/js/no-floating-decimal": [0],
|
||
"@stylistic/js/no-mixed-operators": [0],
|
||
"@stylistic/js/no-mixed-spaces-and-tabs": [0],
|
||
"@stylistic/js/no-multi-spaces": [0],
|
||
"@stylistic/js/no-multiple-empty-lines": [0],
|
||
"@stylistic/js/no-tabs": [0],
|
||
"@stylistic/js/no-trailing-spaces": [0],
|
||
"@stylistic/js/no-whitespace-before-property": [0],
|
||
"@stylistic/js/nonblock-statement-body-position": [0],
|
||
"@stylistic/js/object-curly-newline": [0],
|
||
"@stylistic/js/object-curly-spacing": [0],
|
||
"@stylistic/js/object-property-newline": [0],
|
||
"@stylistic/js/one-var-declaration-per-line": [0],
|
||
"@stylistic/js/operator-linebreak": [0],
|
||
"@stylistic/js/padded-blocks": [0],
|
||
"@stylistic/js/quote-props": [0],
|
||
"@stylistic/js/quotes": [0],
|
||
"@stylistic/js/rest-spread-spacing": [0],
|
||
"@stylistic/js/semi": [0],
|
||
"@stylistic/js/semi-spacing": [0],
|
||
"@stylistic/js/semi-style": [0],
|
||
"@stylistic/js/space-before-blocks": [0],
|
||
"@stylistic/js/space-before-function-paren": [0],
|
||
"@stylistic/js/space-in-parens": [0],
|
||
"@stylistic/js/space-infix-ops": [0],
|
||
"@stylistic/js/space-unary-ops": [0],
|
||
"@stylistic/js/switch-colon-spacing": [0],
|
||
"@stylistic/js/template-curly-spacing": [0],
|
||
"@stylistic/js/template-tag-spacing": [0],
|
||
"@stylistic/js/wrap-iife": [0],
|
||
"@stylistic/js/wrap-regex": [0],
|
||
"@stylistic/js/yield-star-spacing": [0],
|
||
"@stylistic/jsx-child-element-spacing": [0],
|
||
"@stylistic/jsx-closing-bracket-location": [0],
|
||
"@stylistic/jsx-closing-tag-location": [0],
|
||
"@stylistic/jsx-curly-newline": [0],
|
||
"@stylistic/jsx-curly-spacing": [0],
|
||
"@stylistic/jsx-equals-spacing": [0],
|
||
"@stylistic/jsx-first-prop-new-line": [0],
|
||
"@stylistic/jsx-indent": [0],
|
||
"@stylistic/jsx-indent-props": [0],
|
||
"@stylistic/jsx-max-props-per-line": [0],
|
||
"@stylistic/jsx-newline": [0],
|
||
"@stylistic/jsx-one-expression-per-line": [0],
|
||
"@stylistic/jsx-props-no-multi-spaces": [0],
|
||
"@stylistic/jsx-quotes": [0],
|
||
"@stylistic/jsx-tag-spacing": [0],
|
||
"@stylistic/jsx-wrap-multilines": [0],
|
||
"@stylistic/jsx/jsx-child-element-spacing": [0],
|
||
"@stylistic/jsx/jsx-closing-bracket-location": [0],
|
||
"@stylistic/jsx/jsx-closing-tag-location": [0],
|
||
"@stylistic/jsx/jsx-curly-newline": [0],
|
||
"@stylistic/jsx/jsx-curly-spacing": [0],
|
||
"@stylistic/jsx/jsx-equals-spacing": [0],
|
||
"@stylistic/jsx/jsx-first-prop-new-line": [0],
|
||
"@stylistic/jsx/jsx-indent": [0],
|
||
"@stylistic/jsx/jsx-indent-props": [0],
|
||
"@stylistic/jsx/jsx-max-props-per-line": [0],
|
||
"@stylistic/key-spacing": [0],
|
||
"@stylistic/keyword-spacing": [0],
|
||
"@stylistic/linebreak-style": [0],
|
||
"@stylistic/lines-around-comment": [0],
|
||
"@stylistic/max-len": [0],
|
||
"@stylistic/max-statements-per-line": [0],
|
||
"@stylistic/member-delimiter-style": [0],
|
||
"@stylistic/multiline-ternary": [0],
|
||
"@stylistic/new-parens": [0],
|
||
"@stylistic/newline-per-chained-call": [0],
|
||
"@stylistic/no-confusing-arrow": [0],
|
||
"@stylistic/no-extra-parens": [0],
|
||
"@stylistic/no-extra-semi": [0],
|
||
"@stylistic/no-floating-decimal": [0],
|
||
"@stylistic/no-mixed-operators": [0],
|
||
"@stylistic/no-mixed-spaces-and-tabs": [0],
|
||
"@stylistic/no-multi-spaces": [0],
|
||
"@stylistic/no-multiple-empty-lines": [0],
|
||
"@stylistic/no-tabs": [0],
|
||
"@stylistic/no-trailing-spaces": [0],
|
||
"@stylistic/no-whitespace-before-property": [0],
|
||
"@stylistic/nonblock-statement-body-position": [0],
|
||
"@stylistic/object-curly-newline": [0],
|
||
"@stylistic/object-curly-spacing": [0],
|
||
"@stylistic/object-property-newline": [0],
|
||
"@stylistic/one-var-declaration-per-line": [0],
|
||
"@stylistic/operator-linebreak": [0],
|
||
"@stylistic/padded-blocks": [0],
|
||
"@stylistic/quote-props": [0],
|
||
"@stylistic/quotes": [0],
|
||
"@stylistic/rest-spread-spacing": [0],
|
||
"@stylistic/semi": [0],
|
||
"@stylistic/semi-spacing": [0],
|
||
"@stylistic/semi-style": [0],
|
||
"@stylistic/space-before-blocks": [0],
|
||
"@stylistic/space-before-function-paren": [0],
|
||
"@stylistic/space-in-parens": [0],
|
||
"@stylistic/space-infix-ops": [0],
|
||
"@stylistic/space-unary-ops": [0],
|
||
"@stylistic/switch-colon-spacing": [0],
|
||
"@stylistic/template-curly-spacing": [0],
|
||
"@stylistic/template-tag-spacing": [0],
|
||
"@stylistic/ts/block-spacing": [0],
|
||
"@stylistic/ts/brace-style": [0],
|
||
"@stylistic/ts/comma-dangle": [0],
|
||
"@stylistic/ts/comma-spacing": [0],
|
||
"@stylistic/ts/func-call-spacing": [0],
|
||
"@stylistic/ts/function-call-spacing": [0],
|
||
"@stylistic/ts/indent": [0],
|
||
"@stylistic/ts/key-spacing": [0],
|
||
"@stylistic/ts/keyword-spacing": [0],
|
||
"@stylistic/ts/lines-around-comment": [0],
|
||
"@stylistic/ts/member-delimiter-style": [0],
|
||
"@stylistic/ts/no-extra-parens": [0],
|
||
"@stylistic/ts/no-extra-semi": [0],
|
||
"@stylistic/ts/object-curly-spacing": [0],
|
||
"@stylistic/ts/quotes": [0],
|
||
"@stylistic/ts/semi": [0],
|
||
"@stylistic/ts/space-before-blocks": [0],
|
||
"@stylistic/ts/space-before-function-paren": [0],
|
||
"@stylistic/ts/space-infix-ops": [0],
|
||
"@stylistic/ts/type-annotation-spacing": [0],
|
||
"@stylistic/type-annotation-spacing": [0],
|
||
"@stylistic/type-generic-spacing": [0],
|
||
"@stylistic/type-named-tuple-spacing": [0],
|
||
"@stylistic/wrap-iife": [0],
|
||
"@stylistic/wrap-regex": [0],
|
||
"@stylistic/yield-star-spacing": [0],
|
||
"@typescript-eslint/ban-ts-comment": [2],
|
||
"@typescript-eslint/block-spacing": [0],
|
||
"@typescript-eslint/brace-style": [0],
|
||
"@typescript-eslint/comma-dangle": [0],
|
||
"@typescript-eslint/comma-spacing": [0],
|
||
"@typescript-eslint/func-call-spacing": [0],
|
||
"@typescript-eslint/indent": [0],
|
||
"@typescript-eslint/key-spacing": [0],
|
||
"@typescript-eslint/keyword-spacing": [0],
|
||
"@typescript-eslint/lines-around-comment": [0],
|
||
"@typescript-eslint/member-delimiter-style": [0],
|
||
"@typescript-eslint/no-array-constructor": [2],
|
||
"@typescript-eslint/no-deprecated": [2],
|
||
"@typescript-eslint/no-duplicate-enum-values": [2],
|
||
"@typescript-eslint/no-empty-object-type": [2],
|
||
"@typescript-eslint/no-explicit-any": [2],
|
||
"@typescript-eslint/no-extra-non-null-assertion": [2],
|
||
"@typescript-eslint/no-extra-parens": [0],
|
||
"@typescript-eslint/no-extra-semi": [0],
|
||
"@typescript-eslint/no-floating-promises": [
|
||
2,
|
||
{
|
||
"allowForKnownSafeCalls": [
|
||
{
|
||
"from": "package",
|
||
"name": [
|
||
"invalidateQueries",
|
||
"refetchQueries",
|
||
"cancelQueries",
|
||
"resetQueries",
|
||
"prefetchQuery",
|
||
"prefetchInfiniteQuery"
|
||
],
|
||
"package": "@tanstack/query-core"
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"@typescript-eslint/no-misused-new": [2],
|
||
"@typescript-eslint/no-namespace": [2],
|
||
"@typescript-eslint/no-non-null-asserted-optional-chain": [2],
|
||
"@typescript-eslint/no-require-imports": [2],
|
||
"@typescript-eslint/no-this-alias": [2],
|
||
"@typescript-eslint/no-unnecessary-type-constraint": [2],
|
||
"@typescript-eslint/no-unsafe-declaration-merging": [2],
|
||
"@typescript-eslint/no-unsafe-function-type": [2],
|
||
"@typescript-eslint/no-unused-expressions": [
|
||
2,
|
||
{ "allowShortCircuit": true, "allowTaggedTemplates": false, "allowTernary": true }
|
||
],
|
||
"@typescript-eslint/no-unused-vars": [0],
|
||
"@typescript-eslint/no-wrapper-object-types": [2],
|
||
"@typescript-eslint/object-curly-spacing": [0],
|
||
"@typescript-eslint/prefer-as-const": [2],
|
||
"@typescript-eslint/prefer-namespace-keyword": [2],
|
||
"@typescript-eslint/quotes": [0],
|
||
"@typescript-eslint/semi": [0],
|
||
"@typescript-eslint/space-before-blocks": [0],
|
||
"@typescript-eslint/space-before-function-paren": [0],
|
||
"@typescript-eslint/space-infix-ops": [0],
|
||
"@typescript-eslint/triple-slash-reference": [2],
|
||
"@typescript-eslint/type-annotation-spacing": [0],
|
||
"array-bracket-newline": [0],
|
||
"array-bracket-spacing": [0],
|
||
"array-element-newline": [0],
|
||
"arrow-parens": [0],
|
||
"arrow-spacing": [0],
|
||
"babel/object-curly-spacing": [0],
|
||
"babel/quotes": [0],
|
||
"babel/semi": [0],
|
||
"block-spacing": [0],
|
||
"brace-style": [0],
|
||
"comma-dangle": [0],
|
||
"comma-spacing": [0],
|
||
"comma-style": [0],
|
||
"computed-property-spacing": [0],
|
||
"constructor-super": [0],
|
||
"curly": [0, "all"],
|
||
"dot-location": [0],
|
||
"eol-last": [0],
|
||
"flowtype/boolean-style": [0],
|
||
"flowtype/delimiter-dangle": [0],
|
||
"flowtype/generic-spacing": [0],
|
||
"flowtype/object-type-curly-spacing": [0],
|
||
"flowtype/object-type-delimiter": [0],
|
||
"flowtype/quotes": [0],
|
||
"flowtype/semi": [0],
|
||
"flowtype/space-after-type-colon": [0],
|
||
"flowtype/space-before-generic-bracket": [0],
|
||
"flowtype/space-before-type-colon": [0],
|
||
"flowtype/union-intersection-spacing": [0],
|
||
"for-direction": [2],
|
||
"func-call-spacing": [0],
|
||
"function-call-argument-newline": [0],
|
||
"function-paren-newline": [0],
|
||
"generator-star": [0],
|
||
"generator-star-spacing": [0],
|
||
"getter-return": [0, { "allowImplicit": false }],
|
||
"implicit-arrow-linebreak": [0],
|
||
"indent": [0],
|
||
"indent-legacy": [0],
|
||
"jsx-quotes": [0],
|
||
"key-spacing": [0],
|
||
"keyword-spacing": [0],
|
||
"linebreak-style": [0],
|
||
"lines-around-comment": [0],
|
||
"local/button-primitive": [2],
|
||
"local/glyph-link": [2],
|
||
"local/input-primitive": [2],
|
||
"local/jsx-comment-text": [2],
|
||
"local/literal-a11y-text": [2],
|
||
"local/locale-format": [2],
|
||
"local/sortable-a11y": [2],
|
||
"local/surface-role": [2],
|
||
"max-len": [0],
|
||
"max-statements-per-line": [0],
|
||
"multiline-ternary": [0],
|
||
"new-parens": [0],
|
||
"newline-per-chained-call": [0],
|
||
"no-array-constructor": [0],
|
||
"no-arrow-condition": [0],
|
||
"no-async-promise-executor": [2],
|
||
"no-case-declarations": [2],
|
||
"no-class-assign": [0],
|
||
"no-comma-dangle": [0],
|
||
"no-compare-neg-zero": [2],
|
||
"no-cond-assign": [2, "except-parens"],
|
||
"no-confusing-arrow": [0],
|
||
"no-console": [2, {}],
|
||
"no-const-assign": [0],
|
||
"no-constant-binary-expression": [2],
|
||
"no-constant-condition": [2, { "checkLoops": "allExceptWhileTrue" }],
|
||
"no-control-regex": [2],
|
||
"no-debugger": [2],
|
||
"no-delete-var": [2],
|
||
"no-dupe-args": [0],
|
||
"no-dupe-class-members": [0],
|
||
"no-dupe-else-if": [2],
|
||
"no-dupe-keys": [0],
|
||
"no-duplicate-case": [2],
|
||
"no-empty": [2, { "allowEmptyCatch": false }],
|
||
"no-empty-character-class": [2],
|
||
"no-empty-pattern": [2, { "allowObjectPatternsAsParameters": false }],
|
||
"no-empty-static-block": [2],
|
||
"no-ex-assign": [2],
|
||
"no-extra-boolean-cast": [2, {}],
|
||
"no-extra-parens": [0],
|
||
"no-extra-semi": [0],
|
||
"no-fallthrough": [2, { "allowEmptyCase": false, "reportUnusedFallthroughComment": false }],
|
||
"no-floating-decimal": [0],
|
||
"no-func-assign": [0],
|
||
"no-global-assign": [2, { "exceptions": [] }],
|
||
"no-import-assign": [0],
|
||
"no-invalid-regexp": [2, {}],
|
||
"no-irregular-whitespace": [
|
||
2,
|
||
{
|
||
"skipComments": false,
|
||
"skipJSXText": false,
|
||
"skipRegExps": false,
|
||
"skipStrings": true,
|
||
"skipTemplates": false
|
||
}
|
||
],
|
||
"no-loss-of-precision": [2],
|
||
"no-misleading-character-class": [2, { "allowEscape": false }],
|
||
"no-mixed-operators": [0],
|
||
"no-mixed-spaces-and-tabs": [0],
|
||
"no-multi-spaces": [0],
|
||
"no-multiple-empty-lines": [0],
|
||
"no-new-native-nonconstructor": [0],
|
||
"no-new-symbol": [0],
|
||
"no-nonoctal-decimal-escape": [2],
|
||
"no-obj-calls": [0],
|
||
"no-octal": [2],
|
||
"no-prototype-builtins": [2],
|
||
"no-redeclare": [0, { "builtinGlobals": true }],
|
||
"no-regex-spaces": [2],
|
||
"no-reserved-keys": [0],
|
||
"no-restricted-globals": [
|
||
2,
|
||
{
|
||
"message": "Use the logged request layer: `fetch` belongs to src/lib/http.ts only — a request outside it never reaches the client log.",
|
||
"name": "fetch"
|
||
},
|
||
{
|
||
"message": "Use the logged request layer: `XMLHttpRequest` belongs to src/lib/api/core.ts only — a request outside it never reaches the client log.",
|
||
"name": "XMLHttpRequest"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-properties": [
|
||
2,
|
||
{ "message": "Use lib/http.ts `send`.", "object": "window", "property": "fetch" },
|
||
{ "message": "Use lib/http.ts `send`.", "object": "globalThis", "property": "fetch" },
|
||
{
|
||
"message": "Use lib/api/core.ts `beacon`.",
|
||
"object": "navigator",
|
||
"property": "sendBeacon"
|
||
}
|
||
],
|
||
"no-restricted-syntax": [
|
||
2,
|
||
{
|
||
"message": "fetchNextPage() cancels and re-sends a page already loading: call fetchNextPage(NEXT_PAGE) (lib/nextPage.ts).",
|
||
"selector": "CallExpression[arguments.length=0]:matches([callee.name='fetchNextPage'], [callee.property.name='fetchNextPage'])"
|
||
},
|
||
{
|
||
"message": "`enabled:hover:` misses a focused FocusSafeButton (aria-disabled): use `act:hover:` (index.css).",
|
||
"selector": "Literal[value=/(^|\\s)enabled:hover:/]"
|
||
},
|
||
{
|
||
"message": "`enabled:hover:` misses a focused FocusSafeButton (aria-disabled): use `act:hover:` (index.css).",
|
||
"selector": "TemplateElement[value.raw=/(^|\\s)enabled:hover:/]"
|
||
},
|
||
{
|
||
"message": "a class split by an interpolation (`${x}hover:…`): Tailwind never sees it whole and emits no CSS; write the class literally (a shared guard is the `act:` variant).",
|
||
"selector": "TemplateLiteral > TemplateElement:not(:first-child)[value.raw=/^([a-z][\\w-]*:[\\w\\[!-]|:[a-z\\[!])/]"
|
||
},
|
||
{
|
||
"message": "No native <select>: use ui/SegmentedToggle for a short fixed set, ui/FilterSelect for a longer one or a user's own list.",
|
||
"selector": "JSXOpeningElement[name.name='select']"
|
||
},
|
||
{
|
||
"message": "No bare <img>: use ui/RemoteImage — no Referer, a retry past the browser's negative cache, and a fallback instead of the broken-image mark.",
|
||
"selector": "JSXOpeningElement[name.name='img']"
|
||
},
|
||
{
|
||
"message": "px-frozen font size doesn't scale with --font-scale: use text-2xs/text-3xs or text-[Nrem], not text-[Npx].",
|
||
"selector": "Literal[value=/text-\\[[\\d.]+px\\]/]"
|
||
},
|
||
{
|
||
"message": "px-frozen font size doesn't scale with --font-scale: use text-2xs/text-3xs or text-[Nrem], not text-[Npx].",
|
||
"selector": "TemplateElement[value.raw=/text-\\[[\\d.]+px\\]/]"
|
||
},
|
||
{
|
||
"message": "px-frozen box size doesn't scale with --font-scale: use the spacing scale (w-37.5, max-w-60) or rem (minmax(9.375rem,1fr)), not w-[Npx] or minmax(Npx,…).",
|
||
"selector": "Literal[value=/(^|[\\s:])(min-|max-)?(w|h|size|left|right|top|bottom|inset|gap|p[xytrbl]?|m[xytrbl]?)-\\[[\\d.]+px\\]|minmax\\([\\d.]+px,/]"
|
||
},
|
||
{
|
||
"message": "px-frozen box size doesn't scale with --font-scale: use the spacing scale (w-37.5, max-w-60) or rem (minmax(9.375rem,1fr)), not w-[Npx] or minmax(Npx,…).",
|
||
"selector": "TemplateElement[value.raw=/(^|[\\s:])(min-|max-)?(w|h|size|left|right|top|bottom|inset|gap|p[xytrbl]?|m[xytrbl]?)-\\[[\\d.]+px\\]|minmax\\([\\d.]+px,/]"
|
||
},
|
||
{
|
||
"message": "hover by a colour swap: use the B3 layer (`state-layer`, Pressable `layer`, `state-filled` on a fill), not hover:bg-card / bg-border / opacity-N.",
|
||
"selector": "Literal[value=/(^|[\\s:])hover:(bg-(card|border|surface|muted)(?![\\w-])|bg-\\(--(hover|press)\\)|opacity-)/]"
|
||
},
|
||
{
|
||
"message": "hover by a colour swap: use the B3 layer (`state-layer`, Pressable `layer`, `state-filled` on a fill), not hover:bg-card / bg-border / opacity-N.",
|
||
"selector": "TemplateElement[value.raw=/(^|[\\s:])hover:(bg-(card|border|surface|muted)(?![\\w-])|bg-\\(--(hover|press)\\)|opacity-)/]"
|
||
},
|
||
{
|
||
"message": "a palette colour: use a semantic token (text-rating, bg-live, bg-media-chrome, text-danger, …; DESIGN.md Tokens), not text-amber-500.",
|
||
"selector": "Literal[value=/(^|[\\s:])(text|bg|border|fill|stroke|from|via|to|ring|outline|decoration|divide|caret|shadow)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-\\d/]"
|
||
},
|
||
{
|
||
"message": "a palette colour: use a semantic token (text-rating, bg-live, bg-media-chrome, text-danger, …; DESIGN.md Tokens), not text-amber-500.",
|
||
"selector": "TemplateElement[value.raw=/(^|[\\s:])(text|bg|border|fill|stroke|from|via|to|ring|outline|decoration|divide|caret|shadow)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-\\d/]"
|
||
}
|
||
],
|
||
"no-self-assign": [2, { "props": true }],
|
||
"no-setter-return": [0],
|
||
"no-shadow-restricted-names": [2, { "reportGlobalThis": false }],
|
||
"no-space-before-semi": [0],
|
||
"no-spaced-func": [0],
|
||
"no-sparse-arrays": [2],
|
||
"no-tabs": [0],
|
||
"no-this-before-super": [0],
|
||
"no-trailing-spaces": [0],
|
||
"no-undef": [0, { "typeof": false }],
|
||
"no-unexpected-multiline": [0],
|
||
"no-unreachable": [0],
|
||
"no-unsafe-finally": [2],
|
||
"no-unsafe-negation": [0, { "enforceForOrderingRelations": false }],
|
||
"no-unsafe-optional-chaining": [2, { "disallowArithmeticOperators": false }],
|
||
"no-unused-expressions": [
|
||
0,
|
||
{
|
||
"allowShortCircuit": false,
|
||
"allowTaggedTemplates": false,
|
||
"allowTernary": false,
|
||
"enforceForJSX": false,
|
||
"ignoreDirectives": false
|
||
}
|
||
],
|
||
"no-unused-labels": [2],
|
||
"no-unused-private-class-members": [2],
|
||
"no-unused-vars": [0],
|
||
"no-useless-backreference": [2],
|
||
"no-useless-catch": [2],
|
||
"no-useless-escape": [2, { "allowRegexCharacters": [] }],
|
||
"no-var": [2],
|
||
"no-whitespace-before-property": [0],
|
||
"no-with": [0],
|
||
"no-wrap-func": [0],
|
||
"nonblock-statement-body-position": [0],
|
||
"object-curly-newline": [0],
|
||
"object-curly-spacing": [0],
|
||
"object-property-newline": [0],
|
||
"one-var-declaration-per-line": [0],
|
||
"operator-linebreak": [0],
|
||
"padded-blocks": [0],
|
||
"prefer-const": [2, { "destructuring": "any", "ignoreReadBeforeAssign": false }],
|
||
"prefer-rest-params": [2],
|
||
"prefer-spread": [2],
|
||
"quote-props": [0],
|
||
"quotes": [0],
|
||
"react-hooks/capitalized-calls": [2],
|
||
"react-hooks/component-hook-factories": [2],
|
||
"react-hooks/config": [2],
|
||
"react-hooks/error-boundaries": [2],
|
||
"react-hooks/exhaustive-deps": [1],
|
||
"react-hooks/gating": [2],
|
||
"react-hooks/globals": [2],
|
||
"react-hooks/hooks": [2],
|
||
"react-hooks/immutability": [2],
|
||
"react-hooks/incompatible-library": [1],
|
||
"react-hooks/invariant": [2],
|
||
"react-hooks/memo-dependencies": [2],
|
||
"react-hooks/no-deriving-state-in-effects": [2],
|
||
"react-hooks/preserve-manual-memoization": [2],
|
||
"react-hooks/purity": [2],
|
||
"react-hooks/refs": [2],
|
||
"react-hooks/rules-of-hooks": [2],
|
||
"react-hooks/set-state-in-effect": [2],
|
||
"react-hooks/set-state-in-render": [2],
|
||
"react-hooks/static-components": [2],
|
||
"react-hooks/syntax": [2],
|
||
"react-hooks/todo": [2],
|
||
"react-hooks/unsupported-syntax": [1],
|
||
"react-hooks/use-memo": [2],
|
||
"react-hooks/void-use-memo": [2],
|
||
"react-refresh/only-export-components": [1, { "allowConstantExport": true }],
|
||
"react/jsx-child-element-spacing": [0],
|
||
"react/jsx-closing-bracket-location": [0],
|
||
"react/jsx-closing-tag-location": [0],
|
||
"react/jsx-curly-newline": [0],
|
||
"react/jsx-curly-spacing": [0],
|
||
"react/jsx-equals-spacing": [0],
|
||
"react/jsx-first-prop-new-line": [0],
|
||
"react/jsx-indent": [0],
|
||
"react/jsx-indent-props": [0],
|
||
"react/jsx-max-props-per-line": [0],
|
||
"react/jsx-newline": [0],
|
||
"react/jsx-one-expression-per-line": [0],
|
||
"react/jsx-props-no-multi-spaces": [0],
|
||
"react/jsx-space-before-closing": [0],
|
||
"react/jsx-tag-spacing": [0],
|
||
"react/jsx-wrap-multilines": [0],
|
||
"require-yield": [2],
|
||
"rest-spread-spacing": [0],
|
||
"semi": [0],
|
||
"semi-spacing": [0],
|
||
"semi-style": [0],
|
||
"space-after-function-name": [0],
|
||
"space-after-keywords": [0],
|
||
"space-before-blocks": [0],
|
||
"space-before-function-paren": [0],
|
||
"space-before-function-parentheses": [0],
|
||
"space-before-keywords": [0],
|
||
"space-in-brackets": [0],
|
||
"space-in-parens": [0],
|
||
"space-infix-ops": [0],
|
||
"space-return-throw-case": [0],
|
||
"space-unary-ops": [0],
|
||
"space-unary-word-ops": [0],
|
||
"standard/array-bracket-even-spacing": [0],
|
||
"standard/computed-property-even-spacing": [0],
|
||
"standard/object-curly-even-spacing": [0],
|
||
"switch-colon-spacing": [0],
|
||
"template-curly-spacing": [0],
|
||
"template-tag-spacing": [0],
|
||
"unicorn/empty-brace-spaces": [0],
|
||
"unicorn/no-nested-ternary": [0],
|
||
"unicorn/number-literal-case": [0],
|
||
"unicorn/template-indent": [0],
|
||
"use-isnan": [2, { "enforceForIndexOf": false, "enforceForSwitchCase": true }],
|
||
"valid-typeof": [2, { "requireStringLiterals": false }],
|
||
"vue/array-bracket-newline": [0],
|
||
"vue/array-bracket-spacing": [0],
|
||
"vue/array-element-newline": [0],
|
||
"vue/arrow-spacing": [0],
|
||
"vue/block-spacing": [0],
|
||
"vue/block-tag-newline": [0],
|
||
"vue/brace-style": [0],
|
||
"vue/comma-dangle": [0],
|
||
"vue/comma-spacing": [0],
|
||
"vue/comma-style": [0],
|
||
"vue/dot-location": [0],
|
||
"vue/func-call-spacing": [0],
|
||
"vue/html-closing-bracket-newline": [0],
|
||
"vue/html-closing-bracket-spacing": [0],
|
||
"vue/html-end-tags": [0],
|
||
"vue/html-indent": [0],
|
||
"vue/html-quotes": [0],
|
||
"vue/html-self-closing": [0],
|
||
"vue/key-spacing": [0],
|
||
"vue/keyword-spacing": [0],
|
||
"vue/max-attributes-per-line": [0],
|
||
"vue/max-len": [0],
|
||
"vue/multiline-html-element-content-newline": [0],
|
||
"vue/multiline-ternary": [0],
|
||
"vue/mustache-interpolation-spacing": [0],
|
||
"vue/no-extra-parens": [0],
|
||
"vue/no-multi-spaces": [0],
|
||
"vue/no-spaces-around-equal-signs-in-attribute": [0],
|
||
"vue/object-curly-newline": [0],
|
||
"vue/object-curly-spacing": [0],
|
||
"vue/object-property-newline": [0],
|
||
"vue/operator-linebreak": [0],
|
||
"vue/quote-props": [0],
|
||
"vue/script-indent": [0],
|
||
"vue/singleline-html-element-content-newline": [0],
|
||
"vue/space-in-parens": [0],
|
||
"vue/space-infix-ops": [0],
|
||
"vue/space-unary-ops": [0],
|
||
"vue/template-curly-spacing": [0],
|
||
"wrap-iife": [0],
|
||
"wrap-regex": [0],
|
||
"yield-star-spacing": [0]
|
||
},
|
||
"settings": null
|
||
},
|
||
"src tests": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh",
|
||
"vitest"
|
||
],
|
||
"rules": {
|
||
"local/glyph-link": [0],
|
||
"local/input-primitive": [0],
|
||
"local/literal-a11y-text": [0],
|
||
"local/surface-role": [0],
|
||
"no-restricted-globals": [
|
||
0,
|
||
{
|
||
"message": "Use the logged request layer: `fetch` belongs to src/lib/http.ts only — a request outside it never reaches the client log.",
|
||
"name": "fetch"
|
||
},
|
||
{
|
||
"message": "Use the logged request layer: `XMLHttpRequest` belongs to src/lib/api/core.ts only — a request outside it never reaches the client log.",
|
||
"name": "XMLHttpRequest"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-properties": [
|
||
0,
|
||
{ "message": "Use lib/http.ts `send`.", "object": "window", "property": "fetch" },
|
||
{ "message": "Use lib/http.ts `send`.", "object": "globalThis", "property": "fetch" },
|
||
{
|
||
"message": "Use lib/api/core.ts `beacon`.",
|
||
"object": "navigator",
|
||
"property": "sendBeacon"
|
||
}
|
||
],
|
||
"vitest/no-disabled-tests": [2],
|
||
"vitest/no-focused-tests": [2, { "fixable": true }]
|
||
},
|
||
"settings": null
|
||
},
|
||
"src/lib": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": {
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "lib/ sits below components/: it may not import from components/, not even a type. Move the shared piece down into lib/.",
|
||
"regex": "^(\\.\\./)+components(/|$)"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"settings": null
|
||
},
|
||
"ts configs": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "82 names, sha1 29467d490e30",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [
|
||
"local/button-primitive",
|
||
"local/glyph-link",
|
||
"local/input-primitive",
|
||
"local/jsx-comment-text",
|
||
"local/literal-a11y-text",
|
||
"local/locale-format",
|
||
"local/sortable-a11y",
|
||
"local/surface-role",
|
||
"no-console",
|
||
"no-restricted-globals",
|
||
"no-restricted-properties",
|
||
"no-restricted-syntax",
|
||
"react-hooks/capitalized-calls",
|
||
"react-hooks/component-hook-factories",
|
||
"react-hooks/config",
|
||
"react-hooks/error-boundaries",
|
||
"react-hooks/exhaustive-deps",
|
||
"react-hooks/gating",
|
||
"react-hooks/globals",
|
||
"react-hooks/hooks",
|
||
"react-hooks/immutability",
|
||
"react-hooks/incompatible-library",
|
||
"react-hooks/invariant",
|
||
"react-hooks/memo-dependencies",
|
||
"react-hooks/no-deriving-state-in-effects",
|
||
"react-hooks/preserve-manual-memoization",
|
||
"react-hooks/purity",
|
||
"react-hooks/refs",
|
||
"react-hooks/rules-of-hooks",
|
||
"react-hooks/set-state-in-effect",
|
||
"react-hooks/set-state-in-render",
|
||
"react-hooks/static-components",
|
||
"react-hooks/syntax",
|
||
"react-hooks/todo",
|
||
"react-hooks/unsupported-syntax",
|
||
"react-hooks/use-memo",
|
||
"react-hooks/void-use-memo",
|
||
"react-refresh/only-export-components"
|
||
],
|
||
"plugins": ["@", "@eslint-community/eslint-comments", "@typescript-eslint"],
|
||
"rules": {
|
||
"@typescript-eslint/no-unused-expressions": [
|
||
2,
|
||
{ "allowShortCircuit": false, "allowTaggedTemplates": false, "allowTernary": false }
|
||
]
|
||
},
|
||
"settings": null
|
||
},
|
||
"ui": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": {
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not import a feature component. Invert the dependency (take a prop/render-prop) or keep the component outside ui/.",
|
||
"regex": "^\\.\\./(?!\\.\\./)"
|
||
},
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not import a feature component. Invert the dependency (take a prop/render-prop) or keep the component outside ui/.",
|
||
"regex": "^\\.\\./\\.\\./components/(?!ui/)"
|
||
},
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not call a domain API slice. Take the data as props.",
|
||
"regex": "^\\.\\./\\.\\./lib/api(/|$)"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"settings": null
|
||
},
|
||
"ui tests": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh",
|
||
"vitest"
|
||
],
|
||
"rules": {
|
||
"local/glyph-link": [0],
|
||
"local/input-primitive": [0],
|
||
"local/literal-a11y-text": [0],
|
||
"local/surface-role": [0],
|
||
"no-restricted-globals": [
|
||
0,
|
||
{
|
||
"message": "Use the logged request layer: `fetch` belongs to src/lib/http.ts only — a request outside it never reaches the client log.",
|
||
"name": "fetch"
|
||
},
|
||
{
|
||
"message": "Use the logged request layer: `XMLHttpRequest` belongs to src/lib/api/core.ts only — a request outside it never reaches the client log.",
|
||
"name": "XMLHttpRequest"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not import a feature component. Invert the dependency (take a prop/render-prop) or keep the component outside ui/.",
|
||
"regex": "^\\.\\./(?!\\.\\./)"
|
||
},
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not import a feature component. Invert the dependency (take a prop/render-prop) or keep the component outside ui/.",
|
||
"regex": "^\\.\\./\\.\\./components/(?!ui/)"
|
||
},
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not call a domain API slice. Take the data as props.",
|
||
"regex": "^\\.\\./\\.\\./lib/api(/|$)"
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"no-restricted-properties": [
|
||
0,
|
||
{ "message": "Use lib/http.ts `send`.", "object": "window", "property": "fetch" },
|
||
{ "message": "Use lib/http.ts `send`.", "object": "globalThis", "property": "fetch" },
|
||
{
|
||
"message": "Use lib/api/core.ts `beacon`.",
|
||
"object": "navigator",
|
||
"property": "sendBeacon"
|
||
}
|
||
],
|
||
"vitest/no-disabled-tests": [2],
|
||
"vitest/no-focused-tests": [2, { "fixable": true }]
|
||
},
|
||
"settings": null
|
||
}
|
||
},
|
||
"lanes": {
|
||
"backend/Dockerfile.test": [
|
||
"FROM python:3.12-slim",
|
||
"WORKDIR /app",
|
||
"RUN apt-get update \\",
|
||
" && apt-get install -y --no-install-recommends ffmpeg \\",
|
||
" && rm -rf /var/lib/apt/lists/*",
|
||
"COPY requirements.lock requirements-dev.txt ./",
|
||
"RUN pip install --no-cache-dir -r requirements.lock -r requirements-dev.txt"
|
||
],
|
||
"backend/docker-compose.test.yml": [
|
||
"services:",
|
||
" testdb:",
|
||
" image: postgres:16-alpine",
|
||
" environment:",
|
||
" POSTGRES_USER: siftlode",
|
||
" POSTGRES_PASSWORD: siftlode",
|
||
" POSTGRES_DB: siftlode_test",
|
||
" tmpfs:",
|
||
" - /var/lib/postgresql/data # ephemeral + fast; the schema is rebuilt per run",
|
||
" healthcheck:",
|
||
" test: [\"CMD-SHELL\", \"pg_isready -U siftlode -d siftlode_test\"]",
|
||
" interval: 1s",
|
||
" timeout: 3s",
|
||
" retries: 30",
|
||
" test:",
|
||
" build:",
|
||
" context: .",
|
||
" dockerfile: Dockerfile.test",
|
||
" depends_on:",
|
||
" testdb:",
|
||
" condition: service_healthy",
|
||
" environment:",
|
||
" DATABASE_URL: postgresql+psycopg://siftlode:siftlode@testdb:5432/siftlode_test",
|
||
" SIFTLODE_TEST_REQUIRE_DB: \"1\"",
|
||
" volumes:",
|
||
" - ./:/app",
|
||
" working_dir: /app",
|
||
" command: [\"python\", \"-m\", \"pytest\"]"
|
||
],
|
||
"frontend/.prettierignore": [
|
||
"dist",
|
||
"node_modules",
|
||
"playwright-report",
|
||
"test-results",
|
||
"e2e/.auth",
|
||
"package-lock.json"
|
||
],
|
||
"frontend/package.json scripts": {
|
||
"build": "npm run typecheck:app && npm run typecheck:node && vite build",
|
||
"dev": "vite",
|
||
"e2e": "playwright test",
|
||
"format": "prettier --write .",
|
||
"format:check": "prettier --check .",
|
||
"knip": "knip",
|
||
"lint": "eslint . --max-warnings 0",
|
||
"preview": "vite preview",
|
||
"test": "vitest run",
|
||
"test:watch": "vitest",
|
||
"typecheck": "npm run typecheck:app && npm run typecheck:node && npm run typecheck:e2e",
|
||
"typecheck:app": "tsc --noEmit -p tsconfig.json",
|
||
"typecheck:e2e": "tsc --noEmit -p e2e/tsconfig.json",
|
||
"typecheck:node": "tsc -p tsconfig.node.json"
|
||
},
|
||
"frontend/vitest.config.ts": [
|
||
"import { defineConfig } from \"vitest/config\";",
|
||
"import { reactWithCompiler } from \"./reactPlugin\";",
|
||
"export default defineConfig({",
|
||
" plugins: [reactWithCompiler()],",
|
||
" test: {",
|
||
" include: [\"src/**/*.test.{ts,tsx}\"],",
|
||
" environment: \"node\",",
|
||
" coverage: {",
|
||
" provider: \"v8\",",
|
||
" include: [\"src/**/*.{ts,tsx}\"],",
|
||
" exclude: [\"src/**/*.test.{ts,tsx}\", \"src/**/*.testkit.ts\", \"src/**/*.d.ts\"],",
|
||
" reporter: [\"text-summary\"],",
|
||
" },",
|
||
" },",
|
||
"});"
|
||
],
|
||
"scripts/check.sh": [
|
||
"set -u",
|
||
"cd \"$(dirname \"$0\")/..\" || exit 2",
|
||
"mode=all",
|
||
"brief=0",
|
||
"for arg in \"$@\"; do",
|
||
" case \"$arg\" in",
|
||
" all | lint) mode=\"$arg\" ;;",
|
||
" --brief) brief=1 ;;",
|
||
" esac",
|
||
"done",
|
||
"export MSYS_NO_PATHCONV=1 # Git Bash: do not rewrite the in-container paths below",
|
||
"TESTCOMPOSE=backend/docker-compose.test.yml",
|
||
"failed=()",
|
||
"lane() { # lane <name> <command…> — run it, remember a failure, never stop",
|
||
" local name=\"$1\"",
|
||
" shift",
|
||
" if [ \"$brief\" = 0 ]; then",
|
||
" echo \">> $name…\"",
|
||
" \"$@\" || failed+=(\"$name\")",
|
||
" return 0",
|
||
" fi",
|
||
" local out",
|
||
" out=\"$(mktemp)\"",
|
||
" if \"$@\" >\"$out\" 2>&1; then",
|
||
" echo \">> $name: ok\"",
|
||
" else",
|
||
" failed+=(\"$name\")",
|
||
" echo \">> $name: FAILED\"",
|
||
" brief_excerpt \"$out\"",
|
||
" fi",
|
||
" rm -f \"$out\"",
|
||
"}",
|
||
"brief_excerpt() {",
|
||
" local log hits",
|
||
" log=\"$(grep -v -E '^testdb-[0-9]+ ' \"$1\")\"",
|
||
" if [ \"$(printf '%s\\n' \"$log\" | wc -l)\" -le 60 ]; then",
|
||
" printf '%s\\n' \"$log\"",
|
||
" return",
|
||
" fi",
|
||
" hits=\"$(printf '%s\\n' \"$log\" |",
|
||
" grep -E -i -B 1 -A 2 -m 20 'error|fail|✗|×|warn|would reformat|unused|vulnerab|found [0-9]')\"",
|
||
" if [ -n \"$hits\" ]; then printf '%s\\n' \"$hits\"; else printf '%s\\n' \"$log\" | tail -n 20; fi",
|
||
"}",
|
||
"ruff_pinned() {",
|
||
" local pin host",
|
||
" pin=\"$(sed -n 's/^ruff==\\([0-9.]*\\).*/\\1/p' backend/requirements-dev.txt)\"",
|
||
" host=\"$(python -m ruff --version 2>/dev/null | awk '{print $2}')\"",
|
||
" [ -n \"$pin\" ] && [ \"$host\" = \"$pin\" ] && return 0",
|
||
" echo \"ruff ${host:-missing} on the host, ${pin:-nothing} pinned — python -m pip install ruff==$pin\" >&2",
|
||
" return 1",
|
||
"}",
|
||
"ruff_check() { ruff_pinned && python -m ruff check --config backend/ruff.toml backend scripts; }",
|
||
"ruff_format() { ruff_pinned && python -m ruff format --check --config backend/ruff.toml backend scripts; }",
|
||
"basedpyright() {",
|
||
" local before",
|
||
" before=\"$(git hash-object backend/.basedpyright/baseline.json)\"",
|
||
" docker compose -f \"$TESTCOMPOSE\" run --rm --no-deps --build test \\",
|
||
" python -m basedpyright --project pyrightconfig.json || return 1",
|
||
" if [ \"$(git hash-object backend/.basedpyright/baseline.json)\" != \"$before\" ]; then",
|
||
" echo \"this run rewrote backend/.basedpyright/baseline.json (errors were fixed) — commit it\" >&2",
|
||
" return 1",
|
||
" fi",
|
||
" git diff HEAD --quiet -- backend/.basedpyright/baseline.json && return 0",
|
||
" echo \"backend/.basedpyright/baseline.json is uncommitted — commit it\" >&2",
|
||
" return 1",
|
||
"}",
|
||
"frontend() { (cd frontend && \"$@\"); }",
|
||
"lane \"ruff\" ruff_check",
|
||
"lane \"ruff format --check\" ruff_format",
|
||
"lane \"basedpyright (new errors only)\" basedpyright",
|
||
"lane \"lock check (frontend/node_modules == package-lock.json)\" node scripts/check-node-modules.mjs",
|
||
"lane \"tsc (src + configs + e2e)\" frontend npm run --silent typecheck",
|
||
"lane \"eslint (warnings fail too)\" frontend npm run --silent lint",
|
||
"lane \"prettier --check\" frontend npm run --silent format:check",
|
||
"lane \"knip (unused files, exports, dependencies)\" frontend npm run --silent knip",
|
||
"pytest_lane() { # the full suite against a throwaway Postgres; --pull keeps the base image current",
|
||
" docker compose -f \"$TESTCOMPOSE\" build --pull -q test || return 1",
|
||
" local rc=0",
|
||
" docker compose -f \"$TESTCOMPOSE\" up --build --abort-on-container-exit --exit-code-from test || rc=1",
|
||
" docker compose -f \"$TESTCOMPOSE\" down -v >/dev/null 2>&1",
|
||
" return $rc",
|
||
"}",
|
||
"pip_audit() {",
|
||
" docker compose -f \"$TESTCOMPOSE\" run --rm --no-deps test \\",
|
||
" pip-audit -r requirements.lock --no-deps --disable-pip",
|
||
"}",
|
||
"if [ \"$mode\" = all ]; then",
|
||
" lane \"vitest\" frontend npm run --silent test",
|
||
" lane \"vite build\" frontend npx vite build --logLevel warn",
|
||
" lane \"pytest (pure-logic + DB lane)\" pytest_lane",
|
||
" lane \"pip-audit (backend lock)\" pip_audit",
|
||
" lane \"npm audit (production dependencies)\" frontend npm audit --omit=dev --audit-level=high",
|
||
"fi",
|
||
"if [ ${#failed[@]} -eq 0 ]; then",
|
||
" echo \">> gate ($mode): green ✓\"",
|
||
" exit 0",
|
||
"fi",
|
||
"echo \">> gate ($mode): RED — ${failed[*]}\" >&2",
|
||
"exit 1"
|
||
]
|
||
}
|
||
}
|