@starci/eslint-canon-fe 1.4.1 → 1.4.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/lint-adoption.mjs CHANGED
@@ -1,116 +1,116 @@
1
- /**
2
- * Repository-level audit for effective StarCi FE lint adoption.
3
- *
4
- * An ESLint config can import a plugin, copy a plugin, or call itself StarCi while still leaving
5
- * canonical rules switched off. The only useful evidence is the effective config ESLint applies
6
- * to a real production file. This audit compares that printed config with this canon.
7
- */
8
- /**
9
- * Where the component tree sits, per supported repository shape.
10
- *
11
- * ONE BRANCH, IN ONE PLACE. The difference between a monorepo and a single app is not a difference
12
- * of law - the rules, their severities and the inline-config refusal are identical. It is only
13
- * WHERE the law applies. Written out in each repository's config, that fact became two hand-kept
14
- * lists, and the lists did what hand-kept lists do: one enabled 43 rules, the other 53, and canon
15
- * had 49. Neither repository was wrong on purpose; nothing told either of them.
16
- *
17
- * The candidate path is included deliberately. A Preview candidate is the exact source Apply later
18
- * ports into production, and leaving it ungoverned means the one file that becomes production was
19
- * judged by nothing.
20
- */
21
- const LAYOUT_GLOBS = {
22
- monorepo: [
23
- "packages/ui/src/**/*.{ts,tsx}",
24
- "apps/*/src/**/*.{ts,tsx}",
25
- "**/candidate/src/**/*.{ts,tsx}",
26
- ],
27
- "single-app": [
28
- "src/**/*.{ts,tsx}",
29
- "**/candidate/src/**/*.{ts,tsx}",
30
- ],
31
- }
32
-
33
- /** The repository shapes this canon knows how to govern. */
34
- export const LAYOUTS = Object.keys(LAYOUT_GLOBS)
35
-
36
- /**
37
- * The one flat-config block that attaches this canon to a repository.
38
- *
39
- * A consuming `eslint.config.mjs` spreads the result and owns nothing else about the rules: not
40
- * which are on, not their severity, not whether an inline comment may switch one off. Its own
41
- * plugins, its own ignores and its own language options stay its own business - what it may not
42
- * keep is a second opinion about the law.
43
- *
44
- * @param {object} input - Attachment options.
45
- * @param {"monorepo" | "single-app"} input.layout - Which shape this repository has.
46
- * @param {object} input.plugin - The plugin object, imported from this canon.
47
- * @param {object} input.recommended - The recommendation, imported from this canon.
48
- * @param {object} input.linterOptions - The linter options, imported from this canon.
49
- * @returns {object} One flat-config block.
50
- */
51
- export const starciFeConfig = ({ layout, plugin, recommended, linterOptions }) => {
52
- const files = LAYOUT_GLOBS[layout]
53
- if (!files) {
54
- throw new Error(`unknown layout "${layout}" - expected one of: ${LAYOUTS.join(", ")}`)
55
- }
56
- /*
57
- * `recommended` is a FLAT rule map here, not a config object with a `rules` key - and reaching
58
- * for `.rules` on it is the mistake this line exists to stop repeating. Spread that way it
59
- * yields `undefined`, the block ends up with zero rules, and the repository lints clean while
60
- * being governed by nothing. That is the worst possible failure for this file: green, silent,
61
- * and indistinguishable from success. Both spellings are accepted so neither shape can produce
62
- * an empty block.
63
- */
64
- const rules = recommended?.rules ?? recommended
65
- if (!rules || Object.keys(rules).length === 0) {
66
- throw new Error("starciFeConfig received an empty recommendation - a config with no rules is not adoption")
67
- }
68
- return {
69
- files,
70
- linterOptions,
71
- plugins: { "starci-fe": plugin },
72
- rules: { ...rules },
73
- }
74
- }
75
-
76
- /** Convert every flat-config severity spelling to its numeric meaning. */
77
- const severityOf = (setting) => {
78
- const severity = Array.isArray(setting) ? setting[0] : setting
79
- if (severity === "error") return 2
80
- if (severity === "warn") return 1
81
- if (severity === "off") return 0
82
- return severity
83
- }
84
-
85
- /**
86
- * Compare an `eslint --print-config` result with the canonical recommendation.
87
- *
88
- * @param {object} printedConfig - Parsed effective ESLint config for one production probe file.
89
- * @param {Record<string, string | number | unknown[]>} expectedRules - Canonical rule levels.
90
- */
91
- export const auditLintAdoption = (printedConfig, expectedRules) => {
92
- const actualRules = printedConfig?.rules ?? {}
93
- const missing = []
94
- const nonError = []
95
-
96
- for (const name of Object.keys(expectedRules).sort()) {
97
- if (actualRules[name] === undefined) {
98
- missing.push(name)
99
- continue
100
- }
101
- if (severityOf(actualRules[name]) !== 2) nonError.push(name)
102
- }
103
-
104
- const refusesInlineConfig = printedConfig?.linterOptions?.noInlineConfig === true
105
- return {
106
- ok: missing.length === 0 && nonError.length === 0 && refusesInlineConfig,
107
- missing,
108
- nonError,
109
- refusesInlineConfig,
110
- }
111
- }
112
-
113
- /** Repository audits are gathered beside rules, but are not ESLint AST rules themselves. */
114
- export const audits = { "effective-config": auditLintAdoption }
115
- export const rules = {}
116
- export const recommended = {}
1
+ /**
2
+ * Repository-level audit for effective StarCi FE lint adoption.
3
+ *
4
+ * An ESLint config can import a plugin, copy a plugin, or call itself StarCi while still leaving
5
+ * canonical rules switched off. The only useful evidence is the effective config ESLint applies
6
+ * to a real production file. This audit compares that printed config with this canon.
7
+ */
8
+ /**
9
+ * Where the component tree sits, per supported repository shape.
10
+ *
11
+ * ONE BRANCH, IN ONE PLACE. The difference between a monorepo and a single app is not a difference
12
+ * of law - the rules, their severities and the inline-config refusal are identical. It is only
13
+ * WHERE the law applies. Written out in each repository's config, that fact became two hand-kept
14
+ * lists, and the lists did what hand-kept lists do: one enabled 43 rules, the other 53, and canon
15
+ * had 49. Neither repository was wrong on purpose; nothing told either of them.
16
+ *
17
+ * The candidate path is included deliberately. A Preview candidate is the exact source Apply later
18
+ * ports into production, and leaving it ungoverned means the one file that becomes production was
19
+ * judged by nothing.
20
+ */
21
+ const LAYOUT_GLOBS = {
22
+ monorepo: [
23
+ "packages/ui/src/**/*.{ts,tsx}",
24
+ "apps/*/src/**/*.{ts,tsx}",
25
+ "**/candidate/src/**/*.{ts,tsx}",
26
+ ],
27
+ "single-app": [
28
+ "src/**/*.{ts,tsx}",
29
+ "**/candidate/src/**/*.{ts,tsx}",
30
+ ],
31
+ }
32
+
33
+ /** The repository shapes this canon knows how to govern. */
34
+ export const LAYOUTS = Object.keys(LAYOUT_GLOBS)
35
+
36
+ /**
37
+ * The one flat-config block that attaches this canon to a repository.
38
+ *
39
+ * A consuming `eslint.config.mjs` spreads the result and owns nothing else about the rules: not
40
+ * which are on, not their severity, not whether an inline comment may switch one off. Its own
41
+ * plugins, its own ignores and its own language options stay its own business - what it may not
42
+ * keep is a second opinion about the law.
43
+ *
44
+ * @param {object} input - Attachment options.
45
+ * @param {"monorepo" | "single-app"} input.layout - Which shape this repository has.
46
+ * @param {object} input.plugin - The plugin object, imported from this canon.
47
+ * @param {object} input.recommended - The recommendation, imported from this canon.
48
+ * @param {object} input.linterOptions - The linter options, imported from this canon.
49
+ * @returns {object} One flat-config block.
50
+ */
51
+ export const starciFeConfig = ({ layout, plugin, recommended, linterOptions }) => {
52
+ const files = LAYOUT_GLOBS[layout]
53
+ if (!files) {
54
+ throw new Error(`unknown layout "${layout}" - expected one of: ${LAYOUTS.join(", ")}`)
55
+ }
56
+ /*
57
+ * `recommended` is a FLAT rule map here, not a config object with a `rules` key - and reaching
58
+ * for `.rules` on it is the mistake this line exists to stop repeating. Spread that way it
59
+ * yields `undefined`, the block ends up with zero rules, and the repository lints clean while
60
+ * being governed by nothing. That is the worst possible failure for this file: green, silent,
61
+ * and indistinguishable from success. Both spellings are accepted so neither shape can produce
62
+ * an empty block.
63
+ */
64
+ const rules = recommended?.rules ?? recommended
65
+ if (!rules || Object.keys(rules).length === 0) {
66
+ throw new Error("starciFeConfig received an empty recommendation - a config with no rules is not adoption")
67
+ }
68
+ return {
69
+ files,
70
+ linterOptions,
71
+ plugins: { "starci-fe": plugin },
72
+ rules: { ...rules },
73
+ }
74
+ }
75
+
76
+ /** Convert every flat-config severity spelling to its numeric meaning. */
77
+ const severityOf = (setting) => {
78
+ const severity = Array.isArray(setting) ? setting[0] : setting
79
+ if (severity === "error") return 2
80
+ if (severity === "warn") return 1
81
+ if (severity === "off") return 0
82
+ return severity
83
+ }
84
+
85
+ /**
86
+ * Compare an `eslint --print-config` result with the canonical recommendation.
87
+ *
88
+ * @param {object} printedConfig - Parsed effective ESLint config for one production probe file.
89
+ * @param {Record<string, string | number | unknown[]>} expectedRules - Canonical rule levels.
90
+ */
91
+ export const auditLintAdoption = (printedConfig, expectedRules) => {
92
+ const actualRules = printedConfig?.rules ?? {}
93
+ const missing = []
94
+ const nonError = []
95
+
96
+ for (const name of Object.keys(expectedRules).sort()) {
97
+ if (actualRules[name] === undefined) {
98
+ missing.push(name)
99
+ continue
100
+ }
101
+ if (severityOf(actualRules[name]) !== 2) nonError.push(name)
102
+ }
103
+
104
+ const refusesInlineConfig = printedConfig?.linterOptions?.noInlineConfig === true
105
+ return {
106
+ ok: missing.length === 0 && nonError.length === 0 && refusesInlineConfig,
107
+ missing,
108
+ nonError,
109
+ refusesInlineConfig,
110
+ }
111
+ }
112
+
113
+ /** Repository audits are gathered beside rules, but are not ESLint AST rules themselves. */
114
+ export const audits = { "effective-config": auditLintAdoption }
115
+ export const rules = {}
116
+ export const recommended = {}
package/naming.mjs CHANGED
@@ -44,7 +44,7 @@ export const preferArrowExport = {
44
44
  // -- NAMING-2 --------------------------------------------------------------------------------------
45
45
 
46
46
  /** Something a reader triggers is named `onX`, so the name survives being passed. */
47
- export const handlerOnPrefix = {
47
+ export const handlerOnPrefix = {
48
48
  meta: {
49
49
  type: "suggestion",
50
50
  docs: { description: "Handlers are named `onX`, not `handleX`." },
@@ -70,7 +70,35 @@ export const handlerOnPrefix = {
70
70
  },
71
71
  }
72
72
  },
73
- }
73
+ }
74
+
75
+ // -- machine-only identity -------------------------------------------------------------------------
76
+
77
+ /** A const introduces a value; it does not merely give an existing identifier a second name. */
78
+ export const noDirectConstAlias = {
79
+ meta: {
80
+ type: "problem",
81
+ docs: { description: "A const must not be a direct identifier-to-identifier alias." },
82
+ schema: [],
83
+ messages: {
84
+ alias:
85
+ "`const {{alias}} = {{original}}` gives one value two names without adding behavior. Use `{{original}}` directly, or make `{{alias}}` the result of the transformation it claims to represent.",
86
+ },
87
+ },
88
+ create(context) {
89
+ return {
90
+ VariableDeclarator(node) {
91
+ if (node.parent?.kind !== "const") return
92
+ if (node.id?.type !== "Identifier" || node.init?.type !== "Identifier") return
93
+ context.report({
94
+ node: node.id,
95
+ messageId: "alias",
96
+ data: { alias: node.id.name, original: node.init.name },
97
+ })
98
+ },
99
+ }
100
+ },
101
+ }
74
102
 
75
103
  /** The rules this law contributes to the plugin. */
76
104
  // -- NAMING-3 --------------------------------------------------------------------------------------
@@ -134,11 +162,12 @@ export const noSecondLanguageInPath = {
134
162
  },
135
163
  }
136
164
 
137
- export const rules = {
138
- "prefer-arrow-export": preferArrowExport,
139
- "handler-on-prefix": handlerOnPrefix,
140
- "no-second-language-in-path": noSecondLanguageInPath,
141
- }
165
+ export const rules = {
166
+ "prefer-arrow-export": preferArrowExport,
167
+ "handler-on-prefix": handlerOnPrefix,
168
+ "no-second-language-in-path": noSecondLanguageInPath,
169
+ "no-direct-const-alias": noDirectConstAlias,
170
+ }
142
171
 
143
172
  /**
144
173
  * The level this law asks for, as the plugin's own opinion.
package/package.json CHANGED
@@ -1,38 +1,38 @@
1
- {
2
- "name": "@starci/eslint-canon-fe",
3
- "version": "1.4.1",
4
- "description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin.",
5
- "type": "module",
6
- "license": "UNLICENSED",
7
- "private": false,
8
- "exports": {
9
- ".": "./index.mjs",
10
- "./contracts": "./contracts.ts",
11
- "./props": "./props.ts"
12
- },
13
- "main": "./index.mjs",
14
- "files": [
15
- "*.mjs",
16
- "*.ts",
17
- "!*.test.mjs",
18
- "README.md"
19
- ],
20
- "engines": {
21
- "node": ">=20.9.0"
22
- },
23
- "peerDependencies": {
24
- "eslint": ">=9"
25
- },
26
- "publishConfig": {
27
- "access": "public"
28
- },
29
- "scripts": {
30
- "test": "node --test \"*.test.mjs\""
31
- },
32
- "keywords": [
33
- "eslint",
34
- "eslintplugin",
35
- "starci",
36
- "canon"
37
- ]
38
- }
1
+ {
2
+ "name": "@starci/eslint-canon-fe",
3
+ "version": "1.4.2",
4
+ "description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "private": false,
8
+ "exports": {
9
+ ".": "./index.mjs",
10
+ "./contracts": "./contracts.ts",
11
+ "./props": "./props.ts"
12
+ },
13
+ "main": "./index.mjs",
14
+ "files": [
15
+ "*.mjs",
16
+ "*.ts",
17
+ "!*.test.mjs",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20.9.0"
22
+ },
23
+ "peerDependencies": {
24
+ "eslint": ">=9"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "test": "node --test \"*.test.mjs\""
31
+ },
32
+ "keywords": [
33
+ "eslint",
34
+ "eslintplugin",
35
+ "starci",
36
+ "canon"
37
+ ]
38
+ }