@starci/eslint-canon-fe 4.0.0 → 5.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +34 -16
- package/brand-values.mjs +174 -0
- package/class-names.mjs +1 -1
- package/client-boundary.mjs +199 -0
- package/comments.mjs +144 -190
- package/docs/brand-values.md +30 -0
- package/docs/client-boundary.md +99 -0
- package/docs/e2e-shape.md +161 -0
- package/docs/env-owner.md +52 -0
- package/docs/formatting.md +30 -0
- package/docs/hooks-folder.md +29 -0
- package/docs/hygiene.md +111 -0
- package/docs/lint-escape-hatch.md +32 -0
- package/docs/lists.md +74 -0
- package/docs/native-controls.md +73 -0
- package/docs/next-conventions.md +206 -0
- package/docs/size-and-state-budget.md +73 -0
- package/docs/spec-quality.md +139 -0
- package/docs/translation.md +50 -0
- package/docs/transport.md +168 -0
- package/docs/type-safety.md +95 -0
- package/e2e-shape.mjs +348 -0
- package/env-owner.mjs +134 -0
- package/file-layout.mjs +1 -2
- package/formatting.mjs +106 -0
- package/hooks-folder.mjs +119 -0
- package/hygiene.mjs +235 -0
- package/icon.mjs +210 -211
- package/index.mjs +290 -131
- package/lib/ast.mjs +94 -0
- package/lib/path.mjs +2 -0
- package/lib/scope.mjs +42 -0
- package/lib/why.mjs +304 -0
- package/lint-adoption.mjs +15 -52
- package/lint-escape-hatch.mjs +91 -56
- package/lists.mjs +197 -0
- package/loading.mjs +163 -164
- package/naming.mjs +143 -143
- package/native-controls.mjs +112 -0
- package/next-conventions.mjs +346 -0
- package/package.json +8 -4
- package/served-locale.mjs +160 -160
- package/shape-slot.mjs +1 -2
- package/size-and-state-budget.mjs +196 -0
- package/spec-quality.mjs +239 -0
- package/the-split.mjs +133 -134
- package/tokens.mjs +267 -268
- package/translation.mjs +262 -122
- package/transport.mjs +404 -0
- package/type-safety.mjs +156 -74
- package/typography.mjs +82 -83
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold `next-conventions.md` (HFS `FE_NEXT_CONVENTIONS`, with the lead's locale
|
|
3
|
+
* decision: ONE locale pattern everywhere - `next-intl`, a `[locale]` segment, default `vi`,
|
|
4
|
+
* `localePrefix: "as-needed"`, and `proxy.ts` on Next 16).
|
|
5
|
+
*
|
|
6
|
+
* WHY ONE PATTERN. Three repositories held three spellings of the same thing: a `[lang]` segment in
|
|
7
|
+
* the standard's own example, two i18n stacks in one app so that `/` redirected into a 404, and a
|
|
8
|
+
* `middleware.ts` that Next 16 no longer accepts under that name. Each is invisible to a type
|
|
9
|
+
* check and each is found by a reader hitting the wrong page. The rules below hold the spellings a
|
|
10
|
+
* single file can show; whether `routing.ts` carries `localePrefix: "as-needed"` is the
|
|
11
|
+
* architecture machine's read of `modules/i18n`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { attribute, attributeValue, calleeName, leadingTextOf, stringOf } from "./lib/ast.mjs"
|
|
15
|
+
import { baseName, isSpecFile, stem } from "./lib/scope.mjs"
|
|
16
|
+
import { normalizePath } from "./lib/path.mjs"
|
|
17
|
+
|
|
18
|
+
/** Another i18n stack: a second place that decides which language a reader sees. */
|
|
19
|
+
const OTHER_I18N = /^(?:react-i18next|i18next|next-i18next|react-intl|next-translate|@lingui\/.+|typesafe-i18n|rosetta)$/
|
|
20
|
+
|
|
21
|
+
// -- FE-NEXT-1 -------------------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Next 16 names the request interceptor `proxy.ts`; `middleware.ts` is forbidden. */
|
|
24
|
+
export const noMiddlewareFile = {
|
|
25
|
+
meta: {
|
|
26
|
+
type: "problem",
|
|
27
|
+
docs: { description: "`middleware.ts` is forbidden: the request interceptor is `proxy.ts` and exports `proxy`." },
|
|
28
|
+
schema: [],
|
|
29
|
+
messages: {
|
|
30
|
+
file:
|
|
31
|
+
"`{{name}}` is the retired name of the request interceptor. Next 16 calls it `proxy.ts`: rename the file and export the handler as `proxy`.",
|
|
32
|
+
export:
|
|
33
|
+
"The handler in `proxy.ts` is exported as `middleware`, which is the retired name. Export it as `proxy`.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
create(context) {
|
|
37
|
+
const file = normalizePath(context.filename || context.getFilename())
|
|
38
|
+
const name = baseName(file)
|
|
39
|
+
if (isSpecFile(file)) return {}
|
|
40
|
+
if (stem(file) === "middleware" && /(?:^|\/)src\/middleware\.[cm]?[jt]sx?$/.test(file)) {
|
|
41
|
+
return { Program: (node) => context.report({ node, messageId: "file", data: { name } }) }
|
|
42
|
+
}
|
|
43
|
+
if (stem(file) !== "proxy" || !/(?:^|\/)src\/proxy\.[cm]?[jt]s$/.test(file)) return {}
|
|
44
|
+
const check = (node) => {
|
|
45
|
+
const names = []
|
|
46
|
+
const declaration = node.declaration
|
|
47
|
+
if (declaration && declaration.id) names.push(declaration.id.name)
|
|
48
|
+
if (declaration && declaration.type === "VariableDeclaration") {
|
|
49
|
+
for (const d of declaration.declarations) if (d.id.type === "Identifier") names.push(d.id.name)
|
|
50
|
+
}
|
|
51
|
+
for (const specifier of node.specifiers ?? []) names.push(specifier.exported.name)
|
|
52
|
+
if (names.includes("middleware")) context.report({ node, messageId: "export" })
|
|
53
|
+
}
|
|
54
|
+
return { ExportNamedDeclaration: check, ExportDefaultDeclaration: check }
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// -- FE-NEXT-2 -------------------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/** The locale segment is `[locale]`. */
|
|
61
|
+
export const localeSegmentIsLocale = {
|
|
62
|
+
meta: {
|
|
63
|
+
type: "problem",
|
|
64
|
+
docs: { description: "The locale route segment is `[locale]`, never `[lang]`." },
|
|
65
|
+
schema: [],
|
|
66
|
+
messages: {
|
|
67
|
+
segment:
|
|
68
|
+
"This file sits under a `[{{name}}]` segment. The one locale pattern is `[locale]` (next-intl, default `vi`, `localePrefix: \"as-needed\"`); a second spelling means a second routing table.",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
create(context) {
|
|
72
|
+
const file = normalizePath(context.filename || context.getFilename())
|
|
73
|
+
const match = /\/app\/(?:.*\/)?\[(lang|lng|language|i18n)\]\//.exec(file)
|
|
74
|
+
if (!match) return {}
|
|
75
|
+
return { Program: (node) => context.report({ node, messageId: "segment", data: { name: match[1] } }) }
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// -- FE-NEXT-3 -------------------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/** Exactly one i18n stack: next-intl. */
|
|
82
|
+
export const noSecondI18nStack = {
|
|
83
|
+
meta: {
|
|
84
|
+
type: "problem",
|
|
85
|
+
docs: { description: "`next-intl` is the only i18n library." },
|
|
86
|
+
schema: [],
|
|
87
|
+
messages: {
|
|
88
|
+
stack:
|
|
89
|
+
"`{{name}}` is a second i18n stack. Two stacks decide the language twice and disagree at the edges (one app redirected `/` into a 404 that way). Use `next-intl` through `modules/i18n`.",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
create(context) {
|
|
93
|
+
return {
|
|
94
|
+
ImportDeclaration(node) {
|
|
95
|
+
const source = String(node.source.value)
|
|
96
|
+
if (OTHER_I18N.test(source)) context.report({ node, messageId: "stack", data: { name: source } })
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// -- FE-NEXT-4 -------------------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/** `<html lang>` follows the `[locale]` segment. */
|
|
105
|
+
export const htmlLangFromLocale = {
|
|
106
|
+
meta: {
|
|
107
|
+
type: "problem",
|
|
108
|
+
docs: { description: "`<html lang>` is taken from the `[locale]` segment, never a literal." },
|
|
109
|
+
schema: [],
|
|
110
|
+
messages: {
|
|
111
|
+
lang:
|
|
112
|
+
"`<html lang=\"...\">` is a literal. A Vietnamese product that ships `lang=\"en\"` mislabels every page for screen readers, translation prompts and search engines. Set it from the `[locale]` param: `lang={locale}`.",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
create(context) {
|
|
116
|
+
return {
|
|
117
|
+
JSXOpeningElement(node) {
|
|
118
|
+
if (node.name.type !== "JSXIdentifier" || node.name.name !== "html") return
|
|
119
|
+
for (const attribute of node.attributes) {
|
|
120
|
+
if (attribute.type !== "JSXAttribute" || attribute.name.name !== "lang" || !attribute.value) continue
|
|
121
|
+
const value = attribute.value
|
|
122
|
+
const literal =
|
|
123
|
+
value.type === "Literal" ||
|
|
124
|
+
(value.type === "JSXExpressionContainer" &&
|
|
125
|
+
(value.expression.type === "Literal" ||
|
|
126
|
+
(value.expression.type === "TemplateLiteral" && value.expression.expressions.length === 0)))
|
|
127
|
+
if (literal) context.report({ node: attribute, messageId: "lang" })
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// -- FE-NEXT-5 -------------------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/** True for a route page: a `page.tsx` anywhere under `app`. */
|
|
137
|
+
const isPageFile = (file) => /\/app\/(?:.*\/)?page\.[cm]?tsx?$/.test(file)
|
|
138
|
+
|
|
139
|
+
/** The names a module exports, from declarations and specifiers. */
|
|
140
|
+
const exportedNames = (program) => {
|
|
141
|
+
const names = new Set()
|
|
142
|
+
for (const statement of program.body) {
|
|
143
|
+
if (statement.type !== "ExportNamedDeclaration") continue
|
|
144
|
+
const declaration = statement.declaration
|
|
145
|
+
if (declaration && declaration.id) names.add(declaration.id.name)
|
|
146
|
+
if (declaration && declaration.type === "VariableDeclaration") {
|
|
147
|
+
for (const entry of declaration.declarations) if (entry.id.type === "Identifier") names.add(entry.id.name)
|
|
148
|
+
}
|
|
149
|
+
for (const specifier of statement.specifiers ?? []) names.add(specifier.exported.name)
|
|
150
|
+
}
|
|
151
|
+
return names
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Every page names itself: `metadata` or `generateMetadata`. */
|
|
155
|
+
export const pageExportsMetadata = {
|
|
156
|
+
meta: {
|
|
157
|
+
type: "problem",
|
|
158
|
+
docs: { description: "Every `page.tsx` exports `metadata` or `generateMetadata`." },
|
|
159
|
+
schema: [],
|
|
160
|
+
messages: {
|
|
161
|
+
metadata:
|
|
162
|
+
"This page exports neither `metadata` nor `generateMetadata`, so it ships with whatever title its layout happens to have: every page of the section is called the same in the tab, in the history, in a shared link and in a search result. Export `metadata` (or `generateMetadata` when the title comes from data), with the title and description taken from the message catalogue.",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
create(context) {
|
|
166
|
+
const file = normalizePath(context.filename || context.getFilename())
|
|
167
|
+
if (isSpecFile(file) || !isPageFile(file)) return {}
|
|
168
|
+
return {
|
|
169
|
+
"Program:exit"(program) {
|
|
170
|
+
const names = exportedNames(program)
|
|
171
|
+
if (!names.has("metadata") && !names.has("generateMetadata")) context.report({ node: program, messageId: "metadata" })
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// -- FE-NEXT-6 -------------------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
/** A Suspense boundary shows something while it waits. */
|
|
180
|
+
export const noNullSuspenseFallback = {
|
|
181
|
+
meta: {
|
|
182
|
+
type: "problem",
|
|
183
|
+
docs: { description: "`<Suspense>` has a fallback that renders something; `fallback={null}` is forbidden." },
|
|
184
|
+
schema: [],
|
|
185
|
+
messages: {
|
|
186
|
+
nullFallback:
|
|
187
|
+
"`<Suspense>` with a `null` (or missing) fallback: while the subtree loads the reader sees a hole where content will jump in, with no sign anything is happening. Render the loading state of the thing that is loading (the block's skeleton).",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
create(context) {
|
|
191
|
+
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
192
|
+
return {
|
|
193
|
+
JSXOpeningElement(node) {
|
|
194
|
+
const name = node.name
|
|
195
|
+
const isSuspense =
|
|
196
|
+
(name.type === "JSXIdentifier" && name.name === "Suspense") ||
|
|
197
|
+
(name.type === "JSXMemberExpression" && name.property.name === "Suspense")
|
|
198
|
+
if (!isSuspense) return
|
|
199
|
+
const fallback = attribute(node, "fallback")
|
|
200
|
+
const value = attributeValue(fallback)
|
|
201
|
+
const empty =
|
|
202
|
+
!fallback ||
|
|
203
|
+
!value ||
|
|
204
|
+
(value.type === "Literal" && value.value === null) ||
|
|
205
|
+
(value.type === "Identifier" && value.name === "undefined")
|
|
206
|
+
if (empty) context.report({ node, messageId: "nullFallback" })
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// -- FE-NEXT-7 -------------------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
/** Files that legitimately sit outside the locale provider or own the navigation module. */
|
|
215
|
+
const isNavigationOwner = (file) =>
|
|
216
|
+
/\/modules\/i18n\//.test(file) || /\/app\/global-error\.[cm]?tsx?$/.test(file) || /\/src\/proxy\.[cm]?[jt]s$/.test(file)
|
|
217
|
+
|
|
218
|
+
/** The router helpers that are locale-blind in `next/navigation`. */
|
|
219
|
+
const LOCALE_BLIND = new Set(["useRouter", "usePathname", "redirect", "permanentRedirect"])
|
|
220
|
+
|
|
221
|
+
/** Navigation helpers come from `modules/i18n/navigation`, which knows the locale. */
|
|
222
|
+
export const navigationFromIntl = {
|
|
223
|
+
meta: {
|
|
224
|
+
type: "problem",
|
|
225
|
+
docs: { description: "`Link`, `useRouter`, `usePathname` and `redirect` come from `modules/i18n/navigation`, not from Next." },
|
|
226
|
+
schema: [],
|
|
227
|
+
messages: {
|
|
228
|
+
link:
|
|
229
|
+
"`next/link` does not know the locale: a link built with it drops the `[locale]` prefix, so a Vietnamese reader lands on the default language of the next page (or on a 404 under `localePrefix: \"as-needed\"`). Import `Link` from `modules/i18n/navigation`, which `next-intl` builds from `routing.ts`.",
|
|
230
|
+
helper:
|
|
231
|
+
"`{{name}}` from `next/navigation` does not know the locale: it navigates to a path without the `[locale]` prefix. Import it from `modules/i18n/navigation`, which `next-intl` builds from `routing.ts`.",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
create(context) {
|
|
235
|
+
const file = normalizePath(context.filename || context.getFilename())
|
|
236
|
+
if (isSpecFile(file) || isNavigationOwner(file)) return {}
|
|
237
|
+
return {
|
|
238
|
+
ImportDeclaration(node) {
|
|
239
|
+
if (node.importKind === "type") return
|
|
240
|
+
const source = String(node.source.value)
|
|
241
|
+
if (source === "next/link") return context.report({ node, messageId: "link" })
|
|
242
|
+
if (source !== "next/navigation") return
|
|
243
|
+
for (const specifier of node.specifiers) {
|
|
244
|
+
if (specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && LOCALE_BLIND.has(specifier.imported.name)) {
|
|
245
|
+
context.report({ node: specifier, messageId: "helper", data: { name: specifier.imported.name } })
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// -- FE-NEXT-8 -------------------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
/** An href that leaves the app: a scheme, a protocol-relative URL, or an in-page anchor. */
|
|
256
|
+
const isExternalHref = (text) => /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(text)
|
|
257
|
+
|
|
258
|
+
/** No hand-drawn `<a>` for an internal route; an external one opens safely. */
|
|
259
|
+
export const noNativeAnchor = {
|
|
260
|
+
meta: {
|
|
261
|
+
type: "problem",
|
|
262
|
+
docs: { description: "An internal link is `Link` from `modules/i18n/navigation`; an external `_blank` link carries `rel`." },
|
|
263
|
+
schema: [],
|
|
264
|
+
messages: {
|
|
265
|
+
internal:
|
|
266
|
+
"`<a href=\"{{href}}\">` for a route of this app: it reloads the whole page, skips prefetch and drops the locale prefix. Use `Link` from `modules/i18n/navigation` with an href built by `modules/routes`.",
|
|
267
|
+
rel:
|
|
268
|
+
"`target=\"_blank\"` with no `rel`. The opened page can reach back through `window.opener`. Add `rel=\"noopener noreferrer\"`.",
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
create(context) {
|
|
272
|
+
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
273
|
+
return {
|
|
274
|
+
JSXOpeningElement(node) {
|
|
275
|
+
if (node.name.type !== "JSXIdentifier" || node.name.name !== "a") return
|
|
276
|
+
const href = stringOf(attributeValue(attribute(node, "href")))
|
|
277
|
+
if (href !== null && href !== "" && !isExternalHref(href)) context.report({ node, messageId: "internal", data: { href } })
|
|
278
|
+
const target = stringOf(attributeValue(attribute(node, "target")))
|
|
279
|
+
const rel = attribute(node, "rel")
|
|
280
|
+
if (target === "_blank" && !rel) context.report({ node, messageId: "rel" })
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// -- FE-NEXT-9 -------------------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
/** The functions whose first argument is a route: the router's methods and the two redirects. */
|
|
289
|
+
const isNavigation = (name) =>
|
|
290
|
+
name === "redirect" || name === "permanentRedirect" || /(?:^|\.)router\.(?:push|replace|prefetch)$/.test(name)
|
|
291
|
+
|
|
292
|
+
/** True for `/x`, `/x/y`; false for `//host` and for `/` alone, which every app owns. */
|
|
293
|
+
const isInternalPath = (text) => text.startsWith("/") && !text.startsWith("//") && text.length > 1
|
|
294
|
+
|
|
295
|
+
/** The one place that may write a route: `modules/routes`. */
|
|
296
|
+
const isRoutesModule = (file) => /\/modules\/routes\//.test(file) || /\/modules\/i18n\//.test(file)
|
|
297
|
+
|
|
298
|
+
/** No route written by hand outside `modules/routes`. */
|
|
299
|
+
export const noHardcodedRoute = {
|
|
300
|
+
meta: {
|
|
301
|
+
type: "problem",
|
|
302
|
+
docs: { description: "A route path is built by `modules/routes`, never written as a literal at a link or a navigation call." },
|
|
303
|
+
schema: [],
|
|
304
|
+
messages: {
|
|
305
|
+
route:
|
|
306
|
+
"`{{path}}` is a route written by hand. When the route moves, every copy of it silently points at a 404, and nothing type-checks the difference. Build it with the href builder in `modules/routes` and use that one function everywhere.",
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
create(context) {
|
|
310
|
+
const file = normalizePath(context.filename || context.getFilename())
|
|
311
|
+
if (isSpecFile(file) || isRoutesModule(file)) return {}
|
|
312
|
+
const source = context.sourceCode ?? context.getSourceCode()
|
|
313
|
+
const report = (node, text) => context.report({ node, messageId: "route", data: { path: text } })
|
|
314
|
+
return {
|
|
315
|
+
JSXAttribute(node) {
|
|
316
|
+
if (node.name.type !== "JSXIdentifier" || node.name.name !== "href") return
|
|
317
|
+
const value = attributeValue(node)
|
|
318
|
+
const lead = leadingTextOf(value)
|
|
319
|
+
if (lead !== null && isInternalPath(lead)) report(node, source.getText(value))
|
|
320
|
+
},
|
|
321
|
+
CallExpression(node) {
|
|
322
|
+
const name = calleeName(node.callee)
|
|
323
|
+
if (!name || !isNavigation(name)) return
|
|
324
|
+
const first = node.arguments[0]
|
|
325
|
+
const lead = leadingTextOf(first)
|
|
326
|
+
if (lead !== null && isInternalPath(lead)) report(first, source.getText(first))
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** The rules this law contributes to the plugin. */
|
|
333
|
+
export const rules = {
|
|
334
|
+
"no-middleware-file": noMiddlewareFile,
|
|
335
|
+
"locale-segment-is-locale": localeSegmentIsLocale,
|
|
336
|
+
"no-second-i18n-stack": noSecondI18nStack,
|
|
337
|
+
"html-lang-from-locale": htmlLangFromLocale,
|
|
338
|
+
"page-exports-metadata": pageExportsMetadata,
|
|
339
|
+
"no-null-suspense-fallback": noNullSuspenseFallback,
|
|
340
|
+
"navigation-from-intl": navigationFromIntl,
|
|
341
|
+
"no-native-anchor": noNativeAnchor,
|
|
342
|
+
"no-hardcoded-route": noHardcodedRoute,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Every rule is an error. */
|
|
346
|
+
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/eslint-canon-fe",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin.",
|
|
3
|
+
"version": "5.1.2",
|
|
4
|
+
"description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"private": false,
|
|
@@ -11,15 +11,19 @@
|
|
|
11
11
|
"main": "./index.mjs",
|
|
12
12
|
"files": [
|
|
13
13
|
"*.mjs",
|
|
14
|
+
"lib/*.mjs",
|
|
15
|
+
"docs/*.md",
|
|
14
16
|
"*.ts",
|
|
15
17
|
"!*.test.mjs",
|
|
18
|
+
"CHANGELOG.md",
|
|
16
19
|
"README.md"
|
|
17
20
|
],
|
|
18
21
|
"engines": {
|
|
19
22
|
"node": ">=20.9.0"
|
|
20
23
|
},
|
|
21
24
|
"peerDependencies": {
|
|
22
|
-
"eslint": ">=9"
|
|
25
|
+
"eslint": ">=9",
|
|
26
|
+
"eslint-plugin-react-hooks": ">=7"
|
|
23
27
|
},
|
|
24
28
|
"publishConfig": {
|
|
25
29
|
"access": "public"
|
|
@@ -33,4 +37,4 @@
|
|
|
33
37
|
"starci",
|
|
34
38
|
"canon"
|
|
35
39
|
]
|
|
36
|
-
}
|
|
40
|
+
}
|