@starci/eslint-canon-fe 1.0.1 → 1.2.0
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/README.md +86 -86
- package/contract.mjs +1363 -1199
- package/contracts.ts +1 -1
- package/file-layout.mjs +384 -310
- package/index.mjs +17 -0
- package/landmark.mjs +6 -6
- package/lint-adoption.mjs +116 -116
- package/package.json +1 -1
- package/props-and-slots.mjs +339 -188
- package/props.ts +33 -33
- package/served-locale.mjs +1 -1
- package/typography.mjs +83 -83
- package/vendor-boundary.mjs +510 -526
package/index.mjs
CHANGED
|
@@ -90,6 +90,23 @@ export const ruleOwners = Object.fromEntries(
|
|
|
90
90
|
CONTRIBUTIONS.flatMap((entry) => Object.keys(entry.rules).map((name) => [name, entry.law])),
|
|
91
91
|
)
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Every (law, rule name) pair as DECLARED, before any collapsing.
|
|
95
|
+
*
|
|
96
|
+
* `ruleOwners` above and `rules` below are both built with `Object.fromEntries`, which silently
|
|
97
|
+
* keeps the last writer when two laws declare one name. A guard that walks either of them cannot
|
|
98
|
+
* see a collision - the duplicate is gone before it looks, so every name appears exactly once BY
|
|
99
|
+
* CONSTRUCTION and the check passes forever while being blind.
|
|
100
|
+
*
|
|
101
|
+
* That is not hypothetical. The back-end twin had this exact shape, three rules were declared by two
|
|
102
|
+
* laws each, one copy was discarded on import, and its guard reported green throughout. This axis
|
|
103
|
+
* has no collision today; publishing the raw declarations is what keeps that a FACT rather than an
|
|
104
|
+
* assumption nobody can test.
|
|
105
|
+
*/
|
|
106
|
+
export const ruleDeclarations = CONTRIBUTIONS.flatMap((entry) =>
|
|
107
|
+
Object.keys(entry.rules).map((name) => ({ law: entry.law, name })),
|
|
108
|
+
)
|
|
109
|
+
|
|
93
110
|
/** Every rule this canon publishes, keyed by its published name. */
|
|
94
111
|
export const rules = Object.fromEntries(
|
|
95
112
|
CONTRIBUTIONS.flatMap((entry) => Object.entries(entry.rules)),
|
package/landmark.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rules for the LANDMARK law - `fe/
|
|
2
|
+
* Rules for the LANDMARK law - `patterns/fe/landmark/INDEX.md`.
|
|
3
3
|
*
|
|
4
4
|
* WHY THESE EXIST. A registry makes the failure silent. A key named `dashboard-main` records the
|
|
5
5
|
* intent exactly and renders a `div`, because the branch that draws registry nodes draws divs. An
|
|
@@ -51,13 +51,13 @@ export const isRouteFile = (filename) => /\/app\/(?:.*\/)?(?:layout|page)\.tsx$/
|
|
|
51
51
|
* The landmark used to be a named branch, so "the outermost file that composes the screen" and "a
|
|
52
52
|
* file under `app/`" were the same sentence — the branch was imported into the route file and that
|
|
53
53
|
* was that. Then the mark moved into the contract entry as `host: "main"`, and the two sentences
|
|
54
|
-
* came apart: the entry is rendered by whoever renders the screen's outermost frame, and
|
|
54
|
+
* came apart: the entry is rendered by whoever renders the screen's outermost frame, and FILE-6
|
|
55
55
|
* says that is emphatically NOT the route file. A route names which page renders at which URL and
|
|
56
56
|
* draws nothing.
|
|
57
57
|
*
|
|
58
58
|
* Held to route files only, the two laws then refused each other. Every page moved out of `app/` to
|
|
59
|
-
* satisfy
|
|
60
|
-
* once was to leave the page owner in the routing tree — which is the defect
|
|
59
|
+
* satisfy FILE-6 was reported for misplacing its landmark, and the only way to satisfy both at
|
|
60
|
+
* once was to leave the page owner in the routing tree — which is the defect FILE-6 exists to
|
|
61
61
|
* prevent. A rule that can only be satisfied by breaking another rule is a finding about the rule.
|
|
62
62
|
*
|
|
63
63
|
* What the guarantee actually was, and still is: ONE main per document, owned by whoever owns the
|
|
@@ -213,10 +213,10 @@ export const rules = {
|
|
|
213
213
|
*
|
|
214
214
|
* A frame whose CONTRACT ENTRY declares `host: "main"` is not that. Nobody imported a landmark;
|
|
215
215
|
* the registry says which element this key's node opens, and the frame obeys. That entry is
|
|
216
|
-
* rendered by whoever renders the screen's outermost node - and
|
|
216
|
+
* rendered by whoever renders the screen's outermost node - and FILE-6 says the route file is
|
|
217
217
|
* emphatically not it, because a route mounts a page and draws nothing. Holding both shapes to
|
|
218
218
|
* route files only made the two laws refuse each other, and the only way to satisfy both was to
|
|
219
|
-
* leave the page owner in the routing tree, which is the defect
|
|
219
|
+
* leave the page owner in the routing tree, which is the defect FILE-6 exists to prevent.
|
|
220
220
|
*/
|
|
221
221
|
create(context) {
|
|
222
222
|
const filename = context.filename || context.getFilename()
|
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/package.json
CHANGED