-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patheslint.config.js
More file actions
163 lines (158 loc) · 8.05 KB
/
Copy patheslint.config.js
File metadata and controls
163 lines (158 loc) · 8.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import { defineConfig } from "eslint/config";
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";
import globals from "globals";
// defineConfig (eslint/config) rather than tseslint.config(). The latter's variadic overload is
// deprecated, and this config never used its `extends` sugar (it spreads tseslint.configs.recommended
// inline), so the two are equivalent here.
export default defineConfig(
{
ignores: [
"dist/**",
"coverage/**",
"node_modules/**",
"src-tauri/**",
"src/types/generated/**",
// Stryker's working copies of the whole tree, and the report it writes. Both are
// gitignored, which is not the same thing. Eslint reads this list, not .gitignore, and
// `pnpm lint` is `eslint .`. A run that finishes cleans its sandboxes up, so this is
// invisible until one is interrupted, and then each surviving sandbox is a full second
// copy of src/ and scripts/, linted as if it were source. Two of them turned `pnpm lint`
// into 7337 errors that named files nobody had written.
".stryker-tmp/**",
"reports/**",
],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["src/**/*.{ts,tsx}"],
languageOptions: {
globals: {
...globals.browser,
},
// Type-aware linting for the app sources (all covered by tsconfig.json), so the
// async-safety rules below can see promise types. Scoped to src/ only. The plain-JS
// scripts and ignored config files are not part of a tsconfig project.
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
"react-hooks": reactHooks,
},
rules: {
// TypeScript's compiler already reports undefined references and unused
// locals/params (noUnusedLocals/noUnusedParameters); leave those to tsc to
// avoid duplicate, noisier reports here.
"no-undef": "off",
"@typescript-eslint/no-unused-vars": "off",
// The point of adding ESLint. Enforce React's hook rules that tsc cannot see.
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
// Catch unhandled async work. The codebase already marks fire-and-forget with a
// `void` prefix and try/catch; these lock that discipline in so a future unawaited
// promise (a lost error, an out-of-order write) fails lint instead of slipping by.
// `checksVoidReturn.attributes` is off. An async React event handler (e.g.
// `onClick={doAsync}`) is a deliberate, safe pattern here (rejections are caught by
// the global handler), not the misuse this rule targets.
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": [
"error",
{ checksVoidReturn: { attributes: false } },
],
// The threat model (docs/THREAT-MODEL.md) rests on YouTube-derived text (titles, comments, live
// chat, author names), never being rendered as raw HTML. It is always React children,
// so React's escaping neutralizes it, which is what keeps the relaxed
// `style-src 'unsafe-inline'` acceptable (there is no injection sink for it to abuse).
// Nothing enforced that mechanically, so a future dangerouslySetInnerHTML, a direct
// innerHTML/outerHTML write, or an eval would silently reopen the sink. These turn that
// into a lint failure rather than a convention a reviewer has to remember. Done with
// no-restricted-syntax (a core rule) rather than eslint-plugin-react so no dependency is
// added to a tree that runs minimumReleaseAge/blockExoticSubdeps.
"no-eval": "error",
"no-restricted-syntax": [
"error",
{
selector: "JSXAttribute[name.name='dangerouslySetInnerHTML']",
message:
"Rendering raw HTML reopens the XSS sink the threat model (docs/THREAT-MODEL.md) depends on not existing. Render text as React children so React escapes it.",
},
{
selector: "AssignmentExpression[left.property.name=/^(inner|outer)HTML$/]",
message:
"Assigning innerHTML/outerHTML renders raw HTML and reopens the XSS sink the threat model (docs/THREAT-MODEL.md) depends on not existing.",
},
],
// Keep every Tauri touchpoint inside the src/lib seam (docs/ARCHITECTURE.md).
// tauri-client.ts wraps the IPC commands/events with consistent error normalization
// (invokeCommand/invokeVoid/listenTauri), and tauri-platform.ts re-exports the
// platform capabilities (dialogs, opener, process, updater, app version,
// convertFileSrc). Banning `@tauri-apps` everywhere else is what keeps "which Tauri
// capabilities does this app actually use?" (the question every review against
// src-tauri/capabilities/ asks), a two-file read instead of a tree-wide grep a new
// caller can silently invalidate. This was previously scoped to invoke()/listen()
// only, which left every plugin import (dialog/opener/process/updater) outside the
// boundary it was supposed to enforce.
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["@tauri-apps/*", "@tauri-apps/**"],
message:
"Import Tauri through the src/lib seam: tauri-client.ts for commands/events (invokeCommand/invokeVoid/listenTauri), tauri-platform.ts for dialogs, opener, process, updater, app version and convertFileSrc.",
},
],
},
],
},
},
{
// The two seam modules are the only files allowed to import @tauri-apps. tauri-client.ts
// wraps the raw IPC primitives, tauri-platform.ts re-exports the platform capabilities.
files: ["src/lib/tauri-client.ts", "src/lib/tauri-platform.ts"],
rules: {
"no-restricted-imports": "off",
},
},
{
// Test files run under jsdom with node-style globals and looser patterns.
files: ["src/**/*.test.{ts,tsx}", "src/test/**/*.{ts,tsx}"],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
// Test mocks legitimately use `any`; production code has none (enforced above).
"@typescript-eslint/no-explicit-any": "off",
},
},
{
// Release/build helper scripts. Plain ESM run by Node, not the browser bundle.
files: ["scripts/**/*.js"],
languageOptions: {
globals: {
...globals.node,
},
},
},
{
// Build/tooling config, which is run by Node rather than bundled. These were ignored
// outright until now, which is the reason this block exists rather than the config files
// simply inheriting. An ignore is invisible, so `eslint .` reported success on a tree it
// was not reading four files of. Only the `.cjs` one strictly needs these globals today
// (`module.exports`, which `no-undef` flags; typescript-eslint disables that rule for the
// `.ts` ones), but the whole group is named so a `process.env` added to any of them does
// not reintroduce the same silence.
files: ["*.config.{js,cjs,mjs,ts}"],
languageOptions: {
globals: {
...globals.node,
},
},
}
);