Files
tech-log-frontend/eslint.config.ts
T
DongHyeonkaandClaude Opus 5 f9d20e8946 fix: setState 업데이터 안에서 event.currentTarget 을 읽지 않는다
프로젝트 편집 화면에서 활동 유형이나 제목을 한 글자 치면 화면이 통째로 죽었다 —
"Cannot read properties of null (reading 'value')".

setState 업데이터는 핸들러가 끝난 뒤 다음 렌더에 실행된다. 그때 React 는 이미
`event.currentTarget` 을 null 로 되돌려 놓았으므로, 업데이터 안에서 그것을 읽으면
반드시 터진다. 타입 검사도 lint 도 이것을 잡지 못했고, 첫 입력에서야 드러났다.

값은 핸들러가 도는 동안 지역 변수로 꺼내 두고 업데이터에는 그 값을 넘긴다.

같은 실수를 다시 못 하도록 lint 규칙을 세운다: `setXxx(...)` 에 곧바로 넘기는 화살표
함수 안에서는 `currentTarget` 을 읽을 수 없다. 처음 쓴 선택자는 동기 핸들러까지 잡아
(map 콜백, 즉시 호출되는 update 등 아홉 자리) 너무 넓었으므로, 실제로 위험한 setter
업데이터만 겨냥하도록 좁혔다. 결함을 되돌려 규칙이 잡는 것을 확인했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
2026-08-23 21:50:47 +09:00

1077 lines
30 KiB
TypeScript

import babelParser from "@babel/eslint-parser";
import eslint from "@eslint/js";
import reactHooks from "eslint-plugin-react-hooks";
import globals from "globals";
import type { Rule } from "eslint";
const sourceExtensions = "{ts,tsx,mts,cts}";
const layerPatterns = {
domain: [
"**/application/**",
"**/presentation/**",
"**/adapters/**",
"**/bootstrap/**",
"react",
"react-dom",
"@tanstack/**",
],
application: [
"**/presentation/**",
"**/adapters/**",
"**/bootstrap/**",
"react",
"react-dom",
"@tanstack/**",
],
presentation: [
"**/adapters/**",
"**/bootstrap/**",
"**/application/ports/out/**",
"@tanstack/**",
],
adapters: ["**/presentation/**", "**/bootstrap/**"],
};
function restrictedImports(patterns: readonly string[]) {
return ["error", { patterns }];
}
const restrictedBrowserDataProperties = [
"error",
...(["globalThis", "window", "self"] as const).flatMap((object) =>
[
"BroadcastChannel",
"localStorage",
"sessionStorage",
"indexedDB",
"caches",
"navigator",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
"URL",
].map((property) => ({
object,
property,
message:
"Browser file/storage APIs are adapter-owned; use the policy port.",
})),
),
{
object: "navigator",
property: "storage",
message: "StorageManager and OPFS access are adapter-owned.",
},
{
object: "URL",
property: "createObjectURL",
message: "Object URL allocation is owned by the preview/download adapter.",
},
{
object: "URL",
property: "revokeObjectURL",
message: "Object URL revocation is owned by the preview/download adapter.",
},
] as const;
const browserCapabilityRoots = new Set([
"globalThis",
"window",
"self",
"navigator",
"URL",
]);
const browserCapabilityProperties = new Set([
"BroadcastChannel",
"localStorage",
"sessionStorage",
"indexedDB",
"caches",
"navigator",
"storage",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
"URL",
"createObjectURL",
"revokeObjectURL",
]);
/**
* no-restricted-properties only recognizes the literal object identifier.
* This rule follows local aliases of browser roots so `const host =
* globalThis; host.indexedDB` cannot bypass the owned-adapter boundary.
*/
const noBrowserCapabilityAliasRule: Rule.RuleModule = {
meta: {
type: "problem",
schema: [],
messages: {
owned:
"Browser file/storage capabilities are owned by the approved common adapters.",
},
},
create(context) {
const aliases = new Set<string>();
const unwrap = (input: any): any => {
let node = input;
while (
node &&
[
"ChainExpression",
"TSAsExpression",
"TSNonNullExpression",
"TSTypeAssertion",
].includes(node.type)
) {
node = node.expression;
}
return node;
};
const propertyName = (input: any): string | null => {
const node = unwrap(input);
if (!node) return null;
if (!node.computed && node.property?.type === "Identifier") {
return node.property.name;
}
if (
node.computed &&
node.property?.type === "Literal" &&
typeof node.property.value === "string"
) {
return node.property.value;
}
if (
node.computed &&
node.property?.type === "StringLiteral"
) {
return node.property.value;
}
return null;
};
const isRootAlias = (input: any): boolean => {
const node = unwrap(input);
if (!node) return false;
if (node.type === "Identifier") {
return browserCapabilityRoots.has(node.name) ||
aliases.has(node.name);
}
if (node.type === "MemberExpression") {
return isRootAlias(node.object);
}
if (node.type === "LogicalExpression") {
return isRootAlias(node.left) || isRootAlias(node.right);
}
if (node.type === "ConditionalExpression") {
return (
isRootAlias(node.consequent) ||
isRootAlias(node.alternate)
);
}
if (node.type === "SequenceExpression") {
return node.expressions.some(isRootAlias);
}
return false;
};
const isBareRootAlias = (input: any): boolean => {
const node = unwrap(input);
if (!node) return false;
if (node.type === "Identifier") {
return browserCapabilityRoots.has(node.name) ||
aliases.has(node.name);
}
if (node.type === "LogicalExpression") {
return (
isBareRootAlias(node.left) ||
isBareRootAlias(node.right)
);
}
if (node.type === "ConditionalExpression") {
return (
isBareRootAlias(node.consequent) ||
isBareRootAlias(node.alternate)
);
}
if (node.type === "SequenceExpression") {
return node.expressions.some(isBareRootAlias);
}
return false;
};
const isDirectBrowserRoot = (input: any): boolean => {
const node = unwrap(input);
return (
node?.type === "Identifier" &&
browserCapabilityRoots.has(node.name)
);
};
const safeMethodThisBinding = (
call: any,
argumentIndex: number,
): boolean => {
if (argumentIndex !== 0) return false;
const callee = unwrap(call.callee);
if (
callee?.type !== "MemberExpression" ||
propertyName(callee) !== "bind"
) {
return false;
}
const target = unwrap(callee.object);
const targetName =
target?.type === "MemberExpression"
? propertyName(target)
: null;
return (
targetName !== null &&
!browserCapabilityProperties.has(targetName) &&
isRootAlias(target.object)
);
};
const rememberPattern = (patternInput: any, source: any): void => {
const pattern = unwrap(patternInput);
if (!pattern || !isRootAlias(source)) return;
if (pattern.type === "Identifier") {
aliases.add(pattern.name);
return;
}
if (pattern.type !== "ObjectPattern") return;
for (const property of pattern.properties ?? []) {
if (property.type !== "Property") continue;
const name =
property.computed
? property.key?.value
: property.key?.name ?? property.key?.value;
if (
typeof name === "string" &&
browserCapabilityProperties.has(name)
) {
context.report({ node: property, messageId: "owned" });
}
}
};
return {
VariableDeclarator(node: any) {
rememberPattern(node.id, node.init);
},
AssignmentExpression(node: any) {
rememberPattern(node.left, node.right);
const target = unwrap(node.left);
if (
target?.type !== "Identifier" &&
target?.type !== "ObjectPattern" &&
isBareRootAlias(node.right)
) {
context.report({ node: node.right, messageId: "owned" });
}
},
AssignmentPattern(node: any) {
rememberPattern(node.left, node.right);
},
PropertyDefinition(node: any) {
if (isBareRootAlias(node.value)) {
context.report({ node: node.value, messageId: "owned" });
}
},
MemberExpression(node: any) {
const name = propertyName(node);
if (
isRootAlias(node.object) &&
((name !== null &&
browserCapabilityProperties.has(name)) ||
(name === null && node.computed))
) {
context.report({ node, messageId: "owned" });
}
},
CallExpression(node: any) {
for (const [index, argument] of (
node.arguments ?? []
).entries()) {
const candidate =
argument.type === "SpreadElement"
? argument.argument
: argument;
if (
isDirectBrowserRoot(candidate) &&
!safeMethodThisBinding(node, index)
) {
context.report({ node: argument, messageId: "owned" });
}
}
const callee = unwrap(node.callee);
if (
callee?.type !== "MemberExpression" ||
propertyName(callee) !== "get" ||
unwrap(callee.object)?.type !== "Identifier" ||
unwrap(callee.object).name !== "Reflect" ||
!isRootAlias(node.arguments?.[0])
) {
return;
}
const key = unwrap(node.arguments?.[1]);
if (
(key?.type === "Literal" ||
key?.type === "StringLiteral") &&
typeof key.value === "string" &&
browserCapabilityProperties.has(key.value)
) {
context.report({ node, messageId: "owned" });
}
},
NewExpression(node: any) {
for (const argument of node.arguments ?? []) {
const candidate =
argument.type === "SpreadElement"
? argument.argument
: argument;
if (isBareRootAlias(candidate)) {
context.report({ node: argument, messageId: "owned" });
}
}
},
Property(node: any) {
if (
node.parent?.type === "ObjectExpression" &&
isBareRootAlias(node.value)
) {
context.report({ node: node.value, messageId: "owned" });
}
},
ArrayExpression(node: any) {
for (const element of node.elements ?? []) {
if (isBareRootAlias(element)) {
context.report({ node: element, messageId: "owned" });
}
}
},
ReturnStatement(node: any) {
if (isBareRootAlias(node.argument)) {
context.report({ node: node.argument, messageId: "owned" });
}
},
ArrowFunctionExpression(node: any) {
if (
node.expression === true &&
isBareRootAlias(node.body)
) {
context.report({ node: node.body, messageId: "owned" });
}
},
};
},
};
const browserDataBoundaryPlugin = {
rules: {
"no-capability-alias": noBrowserCapabilityAliasRule,
},
};
/**
* A DOM element rendered by React carries `__reactFiber$*` / `__reactProps$*`
* as *own enumerable* properties. `node:assert` builds its `AssertionError`
* eagerly, running `util.inspect` over both operands with `depth: 1000`,
* `getters: true` and `maxArrayLength: Infinity`; the fiber graph re-expands
* once per traversal path, so inspecting one rendered element allocates
* gigabytes and the worker dies before any `AssertionError` is ever thrown.
* A genuine regression then reports as an OOM or an opaque timeout instead of
* a failed assertion. Measured on this repo's Asset Library heading:
* depth 6 = 1.3MB, depth 8 = 7.8MB, depth 10 = 36MB, depth 12 = 135MB.
*
* Equality assertions only inspect their operands on failure, so an unsafe
* comparison stays invisible while green and detonates the day the behaviour
* it guards regresses -- which is exactly when the diagnosis is needed.
*
* `expect` is not affected: vitest prints and diffs through pretty-format's
* DOM plugin, which reads tag/attributes/children and never touches the fiber.
* So the safe form is always an `expect` matcher -- `toHaveFocus()`,
* `not.toBeInTheDocument()`, `toBe(element)` -- and this rule only forbids
* handing a DOM element to `node:assert`.
*/
const TESTING_LIBRARY_QUERY =
/^(get|query|find)(All)?By(Role|Text|LabelText|PlaceholderText|AltText|Title|DisplayValue|TestId)$/u;
const domQueryMethods = new Set([
"querySelector",
"querySelectorAll",
"getElementById",
"closest",
]);
const domElementProperties = new Set([
"activeElement",
"parentElement",
"firstElementChild",
"lastElementChild",
"nextElementSibling",
"previousElementSibling",
"offsetParent",
]);
// Fail when the operands *differ*, so on failure at least one element is
// still there to be inspected.
const positiveAssertEqualities = new Set([
"equal",
"strictEqual",
"deepEqual",
"deepStrictEqual",
]);
// Fail when the operands *match*. `assert.notEqual(element, null)` can only
// fail with `null` on both sides, so a nullish literal operand makes these
// safe; anything else leaves an element to inspect.
const negativeAssertEqualities = new Set([
"notEqual",
"notStrictEqual",
"notDeepEqual",
"notDeepStrictEqual",
]);
const noElementOperandEqualityRule: Rule.RuleModule = {
meta: {
type: "problem",
schema: [],
messages: {
unbounded:
"node:assert inspects both operands at depth 1000 to build its failure message, and a React-rendered element's __reactFiber$* graph exhausts the worker heap there, so the regression reports as an OOM instead of an assertion. Use an expect matcher instead -- expect(el).toHaveFocus(), expect(el).not.toBeInTheDocument(), expect(actual).toBe(expected) -- which prints DOM nodes through pretty-format's DOM plugin.",
},
},
create(context) {
const sourceCode = context.sourceCode;
const unwrap = (input: any): any => {
let node = input;
while (
node &&
[
"AwaitExpression",
"ChainExpression",
"TSAsExpression",
"TSNonNullExpression",
"TSSatisfiesExpression",
"TSTypeAssertion",
].includes(node.type)
) {
node = node.type === "AwaitExpression" ? node.argument : node.expression;
}
return node;
};
const memberName = (node: any): string | null => {
if (!node.computed && node.property?.type === "Identifier") {
return node.property.name;
}
if (
node.computed &&
(node.property?.type === "Literal" ||
node.property?.type === "StringLiteral") &&
typeof node.property.value === "string"
) {
return node.property.value;
}
return null;
};
const resolveInit = (node: any): any => {
const scope = sourceCode.getScope(node);
let current: any = scope;
while (current) {
const variable = current.variables.find(
(entry: any) => entry.name === node.name,
);
if (variable) {
const definition = variable.defs.at(-1);
return definition?.node?.type === "VariableDeclarator"
? definition.node.init
: null;
}
current = current.upper;
}
return null;
};
const isElementValued = (input: any, seen = new Set<any>()): boolean => {
const node = unwrap(input);
if (!node || seen.has(node)) return false;
seen.add(node);
if (node.type === "MemberExpression") {
const name = memberName(node);
return name !== null && domElementProperties.has(name);
}
if (node.type === "CallExpression") {
const callee = unwrap(node.callee);
if (callee?.type !== "MemberExpression") return false;
const name = memberName(callee);
return (
name !== null &&
(TESTING_LIBRARY_QUERY.test(name) || domQueryMethods.has(name))
);
}
if (node.type === "Identifier") {
return isElementValued(resolveInit(node), seen);
}
if (node.type === "ConditionalExpression") {
return (
isElementValued(node.consequent, seen) ||
isElementValued(node.alternate, seen)
);
}
return false;
};
const isNullish = (input: any): boolean => {
const node = unwrap(input);
if (!node) return false;
return (
(node.type === "Literal" && node.value === null) ||
(node.type === "Identifier" && node.name === "undefined")
);
};
return {
CallExpression(node: any) {
const callee = unwrap(node.callee);
if (callee?.type !== "MemberExpression") return;
const object = unwrap(callee.object);
if (object?.type !== "Identifier" || object.name !== "assert") return;
const name = memberName(callee);
if (name === null) return;
const positive = positiveAssertEqualities.has(name);
if (!positive && !negativeAssertEqualities.has(name)) return;
const operands = (node.arguments ?? []).slice(0, 2);
if (!positive && operands.some((argument: any) => isNullish(argument))) {
return;
}
const operand = operands.find((argument: any) =>
isElementValued(argument),
);
if (operand) context.report({ node: operand, messageId: "unbounded" });
},
};
},
};
const testAssertionBoundaryPlugin = {
rules: {
"no-element-operand-equality": noElementOperandEqualityRule,
},
};
const commonLanguageOptions = {
ecmaVersion: "latest",
sourceType: "module",
globals: {
...globals.browser,
...globals.node,
},
};
const commonSecurityRules = {
"no-eval": "error",
"no-new-func": "error",
"no-script-url": "error",
"no-restricted-syntax": [
"error",
{
selector: "JSXAttribute[name.name='dangerouslySetInnerHTML']",
message: "Raw HTML injection is prohibited by FE-OC-019.",
},
{
selector:
"CallExpression[callee.object.name='document'][callee.property.name='createElement'][arguments.0.value='script']",
message: "Runtime script construction is prohibited by FE-OC-019.",
},
{
/*
setState 업데이터는 핸들러가 끝난 뒤, 다음 렌더에 실행된다. 그때 React 는 이미
`event.currentTarget` 을 null 로 되돌려 놓았으므로 업데이터 안에서 그것을 읽으면
"Cannot read properties of null" 로 화면이 통째로 죽는다.
타입 검사도 lint 도 잡지 못했고, 첫 입력에서야 드러났다 — 값은 핸들러가 도는 동안
지역 변수로 꺼내 두고 업데이터에는 그 값을 넘긴다.
*/
selector:
"CallExpression[callee.name=/^set[A-Z]/] > ArrowFunctionExpression MemberExpression[property.name='currentTarget']",
message:
"Read event.currentTarget before the setState updater runs — it is null by the time the updater is called.",
},
],
};
const hookRules = {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
};
export default [
{
ignores: [
"dist/**",
"node_modules/**",
"artifacts/**",
"tests/fixtures/typecheck/**",
"tests/fixtures/architecture/forbidden/**",
"tests/fixtures/diagnostics/forbidden/**",
"tests/fixtures/i18n/forbidden/**",
"tests/fixtures/security/forbidden/**",
"tests/fixtures/optional-recipes/**",
"tests/fixtures/browser-file-storage-boundaries/**",
],
},
eslint.configs.recommended,
{
files: [`**/*.${sourceExtensions}`],
languageOptions: {
...commonLanguageOptions,
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
plugins: {
"react-hooks": reactHooks,
},
rules: {
...commonSecurityRules,
...hookRules,
},
},
{
files: ["**/*.{ts,mts,cts}"],
languageOptions: {
...commonLanguageOptions,
parser: babelParser,
parserOptions: {
requireConfigFile: false,
babelOptions: {
plugins: [
["@babel/plugin-syntax-typescript", { isTSX: false }],
],
},
},
},
rules: {
"no-undef": "off",
"no-unused-vars": "off",
},
},
{
files: ["**/*.d.ts"],
languageOptions: {
...commonLanguageOptions,
parser: babelParser,
parserOptions: {
requireConfigFile: false,
babelOptions: {
plugins: [
["@babel/plugin-syntax-typescript", { dts: true }],
],
},
},
},
rules: {
"no-undef": "off",
"no-unused-vars": "off",
},
},
{
files: ["**/*.tsx"],
languageOptions: {
...commonLanguageOptions,
parser: babelParser,
parserOptions: {
requireConfigFile: false,
babelOptions: {
plugins: [
[
"@babel/plugin-syntax-typescript",
{ allExtensions: true, isTSX: true },
],
"@babel/plugin-syntax-jsx",
],
},
},
},
rules: {
"no-undef": "off",
"no-unused-vars": "off",
},
},
{
files: [
`src/domain/**/*.${sourceExtensions}`,
`src/application/**/*.${sourceExtensions}`,
`src/presentation/**/*.${sourceExtensions}`,
`src/bootstrap/**/*.${sourceExtensions}`,
`src/features/**/*.${sourceExtensions}`,
`src/adapters/**/*.${sourceExtensions}`,
],
ignores: [
`src/adapters/browser-file-storage/**/*.${sourceExtensions}`,
`src/adapters/browser-files/**/*.${sourceExtensions}`,
`src/adapters/browser-transfer/**/*.${sourceExtensions}`,
`src/adapters/cache-storage/**/*.${sourceExtensions}`,
`src/adapters/cross-context-invalidation/**/*.${sourceExtensions}`,
`src/adapters/service-worker/**/*.${sourceExtensions}`,
`src/bootstrap/register-service-worker.ts`,
`src/adapters/platform/browser-lifecycle.ts`,
`src/adapters/storage/**/*.${sourceExtensions}`,
`src/adapters/storage/indexeddb/**/*.${sourceExtensions}`,
`src/adapters/storage/opfs/**/*.${sourceExtensions}`,
],
plugins: {
"browser-data-boundary": browserDataBoundaryPlugin,
},
rules: {
"browser-data-boundary/no-capability-alias": "error",
},
},
{
files: [`src/domain/**/*.${sourceExtensions}`],
rules: {
"no-restricted-imports": restrictedImports(layerPatterns.domain),
"no-restricted-properties": restrictedBrowserDataProperties,
"no-restricted-globals": [
"error",
"window",
"document",
"BroadcastChannel",
"localStorage",
"sessionStorage",
"fetch",
"indexedDB",
"caches",
"navigator",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
],
},
},
{
files: [`src/application/**/*.${sourceExtensions}`],
rules: {
"no-restricted-imports": restrictedImports(layerPatterns.application),
"no-restricted-properties": restrictedBrowserDataProperties,
"no-restricted-globals": [
"error",
"window",
"document",
"BroadcastChannel",
"localStorage",
"sessionStorage",
"fetch",
"indexedDB",
"caches",
"navigator",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
],
},
},
{
files: [`src/presentation/**/*.${sourceExtensions}`],
rules: {
"no-restricted-imports": restrictedImports(layerPatterns.presentation),
"no-restricted-properties": restrictedBrowserDataProperties,
"no-restricted-globals": [
"error",
"fetch",
"BroadcastChannel",
"localStorage",
"sessionStorage",
"indexedDB",
"caches",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
],
},
},
{
files: [
`src/presentation/adapters/query/**/*.${sourceExtensions}`,
],
rules: {
"no-restricted-imports": restrictedImports([
"**/adapters/http/**",
"**/adapters/storage/**",
"**/adapters/auth/**",
"**/bootstrap/**",
"**/application/ports/out/**",
]),
},
},
{
files: [`src/presentation/templates/**/*.${sourceExtensions}`],
rules: {
"no-restricted-imports": restrictedImports([
"**/application/**",
"**/adapters/**",
"**/bootstrap/**",
"@tanstack/**",
]),
},
},
{
files: [
`src/bootstrap/**/*.${sourceExtensions}`,
`src/features/installed-feature-*.${sourceExtensions}`,
],
rules: {
"no-restricted-properties": restrictedBrowserDataProperties,
"no-restricted-globals": [
"error",
"BroadcastChannel",
"localStorage",
"sessionStorage",
"indexedDB",
"caches",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
],
},
},
{
files: [`src/adapters/**/*.${sourceExtensions}`],
rules: {
"no-restricted-imports": restrictedImports(layerPatterns.adapters),
"no-restricted-properties": restrictedBrowserDataProperties,
"no-restricted-globals": [
"error",
"BroadcastChannel",
"localStorage",
"sessionStorage",
"indexedDB",
"caches",
"navigator",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
],
},
},
{
files: [
`src/adapters/browser-file-storage/**/*.${sourceExtensions}`,
`src/adapters/browser-files/**/*.${sourceExtensions}`,
`src/adapters/browser-transfer/**/*.${sourceExtensions}`,
`src/adapters/cache-storage/**/*.${sourceExtensions}`,
`src/adapters/cross-context-invalidation/**/*.${sourceExtensions}`,
`src/adapters/service-worker/**/*.${sourceExtensions}`,
`src/bootstrap/register-service-worker.ts`,
`src/adapters/platform/browser-lifecycle.ts`,
`src/adapters/storage/**/*.${sourceExtensions}`,
`src/adapters/storage/indexeddb/**/*.${sourceExtensions}`,
`src/adapters/storage/opfs/**/*.${sourceExtensions}`,
],
rules: {
"no-restricted-properties": "off",
"no-restricted-globals": "off",
},
},
{
files: [`src/features/*/domain/**/*.${sourceExtensions}`],
rules: {
"no-restricted-imports": restrictedImports(layerPatterns.domain),
"no-restricted-properties": restrictedBrowserDataProperties,
"no-restricted-globals": [
"error",
"window",
"document",
"BroadcastChannel",
"localStorage",
"sessionStorage",
"fetch",
"indexedDB",
"caches",
"navigator",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
],
},
},
{
files: [`src/features/*/application/**/*.${sourceExtensions}`],
rules: {
"no-restricted-imports": restrictedImports(layerPatterns.application),
"no-restricted-properties": restrictedBrowserDataProperties,
"no-restricted-globals": [
"error",
"window",
"document",
"BroadcastChannel",
"localStorage",
"sessionStorage",
"fetch",
"indexedDB",
"caches",
"navigator",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
],
},
},
{
files: [`src/features/*/presentation/**/*.${sourceExtensions}`],
rules: {
"no-restricted-imports": restrictedImports([
"**/features/*/adapters/**",
"**/adapters/http/**",
"**/adapters/storage/**",
"**/adapters/auth/**",
"**/bootstrap/**",
"**/application/ports/out/**",
"@tanstack/**",
]),
"no-restricted-properties": restrictedBrowserDataProperties,
"no-restricted-globals": [
"error",
"fetch",
"BroadcastChannel",
"localStorage",
"sessionStorage",
"indexedDB",
"caches",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
],
},
},
{
files: [`src/features/*/adapters/**/*.${sourceExtensions}`],
rules: {
"no-restricted-imports": restrictedImports([
"**/presentation/**",
"**/bootstrap/**",
"@tanstack/**",
]),
"no-restricted-properties": restrictedBrowserDataProperties,
"no-restricted-globals": [
"error",
"BroadcastChannel",
"localStorage",
"sessionStorage",
"indexedDB",
"caches",
"navigator",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
],
},
},
{
files: [`tests/**/*.${sourceExtensions}`],
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
"test-assertion-boundary": testAssertionBoundaryPlugin,
},
rules: {
"test-assertion-boundary/no-element-operand-equality": "error",
},
},
{
files: [`tests/support/browser/**/*.${sourceExtensions}`],
rules: {
"react-hooks/rules-of-hooks": "off",
},
},
{
files: [
`tests/fixtures/architecture/forbidden/**/*.${sourceExtensions}`,
],
rules: {
"no-restricted-imports": restrictedImports([
"**/adapters/**",
"**/application/ports/out/**",
"@tanstack/**",
"react",
"react-dom",
"**/application/**",
]),
"no-restricted-globals": [
"error",
"fetch",
"localStorage",
"sessionStorage",
],
},
},
{
files: [
`tests/fixtures/browser-file-storage-boundaries/forbidden/**/*.${sourceExtensions}`,
],
plugins: {
"browser-data-boundary": browserDataBoundaryPlugin,
},
rules: {
"browser-data-boundary/no-capability-alias": "error",
"no-restricted-properties": restrictedBrowserDataProperties,
"no-restricted-globals": [
"error",
"BroadcastChannel",
"localStorage",
"sessionStorage",
"indexedDB",
"caches",
"File",
"Blob",
"FileSystemFileHandle",
"FileSystemDirectoryHandle",
"showOpenFilePicker",
"showSaveFilePicker",
],
},
},
];