@starterlib/eslint-config 0.10.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/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @starterlib/eslint-config
2
+
3
+ La configuration ESLint d'une SPA du socle frontal, parametree par son prefixe, et la configuration
4
+ stylelint de ses feuilles. Elle remplace les `eslint.config.mjs` que chaque SPA recopiait, et qui
5
+ divergeaient deja : une regle du socle atteint desormais toutes les SPA a la montee de version.
6
+
7
+ Outil de developpement : jamais en dependance de production.
8
+
9
+ ## Installation
10
+
11
+ A la version exacte des autres paquets du socle, avec ses pairs (versions de la matrice) :
12
+
13
+ ```bash
14
+ pnpm add -D -E @starterlib/eslint-config@<socle> @starterlib/eslint-plugin@<socle>
15
+ ```
16
+
17
+ `eslint.config.mjs` :
18
+
19
+ ```js
20
+ import { configurationDuSocle } from '@starterlib/eslint-config';
21
+
22
+ export default configurationDuSocle({ prefixe: 'ct', racine: import.meta.dirname });
23
+ ```
24
+
25
+ `role: 'hote'` pour la coquille : un remote n'a pas le droit de FOURNIR le noyau, le routeur ou le
26
+ client HTTP (seul l'hote les fournit), l'hote si. `ignores` ajoute des chemins a ne pas lire.
27
+
28
+ `.stylelintrc.json` :
29
+
30
+ ```json
31
+ { "extends": ["@starterlib/eslint-config/stylelint"] }
32
+ ```
33
+
34
+ ## Ce qu'elle tient
35
+
36
+ Chaque regle repond a une ligne du plan SPA §4 ou de l'ADR 0029.
37
+
38
+ - Angular : signaux, `inject`, composants autonomes, OnPush (le defaut d'Angular 22 : en sortir est
39
+ refuse), aucune API experimentale ni en apercu, gabarits et feuilles dans leurs fichiers (un gabarit
40
+ en ligne echappait au lint de gabarit et a stylelint), selecteurs au prefixe de la SPA.
41
+ - Navigateur : ni stockage, ni `window`, ni `document` global (DOCUMENT injecte), ni
42
+ `window.location` (le routeur), ni lecture de cookie.
43
+ - Securite : ni `fetch` ni `XMLHttpRequest` (le HttpClient de l'hote), ni `innerHTML`, ni ecriture
44
+ HTML directe, ni `bypassSecurityTrust*`, ni style pose par `setAttribute`, ni `Function`.
45
+ - Architecture : un seul `ErrorHandler`, ni zone.js, ni TanStack, ni Angular Material, ni
46
+ bibliotheque de traduction ou d'icones ; un remote ne fournit ni `provideStarterCore`, ni
47
+ `provideRouter`, ni `provideHttpClient`.
48
+ - Gabarits (`@starterlib/eslint-plugin`) : aucun element interactif nu, aucun texte litteral, aucun
49
+ pipe de locale d'Angular ; code : un titre par route, aucun toast d'erreur, aucun texte libre dans
50
+ l'adresse.
51
+ - Feuilles (stylelint) : aucune couleur ecrite (un derive de jeton est admis), aucun `px` non nul sur
52
+ un espacement, une police ou un rayon, aucun `z-index` en nombre, ni `order` ni sens inverse.
53
+
54
+ Les essais (`*.spec.ts`) pilotent le DOM et montent des routes et des composants hotes : `window`,
55
+ `document`, les routes sans titre et les gabarits en ligne y sont admis. L'hote de developpement d'un
56
+ remote (`src/hote-dev`, jamais livre) ne parle qu'aux developpeurs : textes litteraux admis.
57
+
58
+ Une exception s'ecrit sur place, avec sa raison : `// eslint-disable-next-line <regle> -- <raison>`,
59
+ `// stylelint-disable-next-line <regle> -- <raison>`.
package/index.mjs ADDED
@@ -0,0 +1,277 @@
1
+ /**
2
+ * `@starterlib/eslint-config` (lot 178) : la configuration ESLint d'une SPA du socle, parametree
3
+ * par son prefixe. Elle remplace les `eslint.config.mjs` recopies dans chaque SPA, qui disaient
4
+ * tous « en attendant @starterlib/eslint-config » et divergeaient deja (la coquille avait window et
5
+ * document, les remotes non).
6
+ *
7
+ * ```js
8
+ * // eslint.config.mjs d'une SPA
9
+ * import { configurationDuSocle } from '@starterlib/eslint-config';
10
+ * export default configurationDuSocle({ prefixe: 'ct', racine: import.meta.dirname });
11
+ * ```
12
+ *
13
+ * Chaque regle repond a une ligne du plan SPA §4 ou de l'ADR 0029 : une regle non outillee est une
14
+ * opinion. Les regles que le socle ecrit lui-meme sont dans `@starterlib/eslint-plugin`.
15
+ */
16
+ import eslint from '@eslint/js';
17
+ import angular from 'angular-eslint';
18
+ import tseslint from 'typescript-eslint';
19
+ import socle from '@starterlib/eslint-plugin';
20
+
21
+ const PREFIXE = /^[a-z][a-z0-9]{1,7}$/;
22
+
23
+ /** Aucun stockage navigateur : la session vit dans un cookie pose par le BFF (ADR 0014). */
24
+ const STOCKAGE = [
25
+ { name: 'localStorage', message: 'Aucun stockage navigateur (ADR 0014).' },
26
+ { name: 'sessionStorage', message: 'Aucun stockage navigateur (ADR 0014).' },
27
+ { name: 'indexedDB', message: 'Aucun stockage navigateur (ADR 0014).' },
28
+ ];
29
+
30
+ /** La fenetre et le document par DOCUMENT injecte : testable, et sans global (constat A0904). */
31
+ const GLOBAUX_DU_NAVIGATEUR = [
32
+ { name: 'window', message: 'Passer par DOCUMENT (defaultView) : testable, et sans global.' },
33
+ { name: 'document', message: "Injecter DOCUMENT plutot que l'acceder globalement." },
34
+ ];
35
+
36
+ /** Tout passe par le HttpClient de l'hote : jeton anti-falsification, erreurs du socle (ADR 0029, 13). */
37
+ const RESEAU = [
38
+ {
39
+ name: 'fetch',
40
+ message: "Le HttpClient de l'hote (httpResource, clients engendres), jamais fetch.",
41
+ },
42
+ { name: 'XMLHttpRequest', message: "Le HttpClient de l'hote, jamais XMLHttpRequest." },
43
+ ];
44
+
45
+ const SYNTAXE_INTERDITE = [
46
+ {
47
+ selector: "CallExpression[callee.property.name='toLocaleString']",
48
+ message: 'Formats par st-money, st-date, st-count (locale du socle).',
49
+ },
50
+ {
51
+ selector: 'MemberExpression[property.name=/^(innerHTML|outerHTML)$/]',
52
+ message: 'Puits DOM interdit (Trusted Types).',
53
+ },
54
+ {
55
+ selector: 'CallExpression[callee.property.name=/^(insertAdjacentHTML|write|writeln)$/]',
56
+ message: 'Ecriture HTML directe interdite (Trusted Types).',
57
+ },
58
+ {
59
+ selector: 'CallExpression[callee.property.name=/^bypassSecurityTrust/]',
60
+ message:
61
+ "bypassSecurityTrust* : la politique angular#unsafe-bypass n'est pas admise par le BFF (ADR 0029).",
62
+ },
63
+ {
64
+ selector: "CallExpression[callee.property.name='setAttribute'][arguments.0.value='style']",
65
+ message: "Style pose par setAttribute : refuse sans 'unsafe-inline'.",
66
+ },
67
+ {
68
+ selector: "NewExpression[callee.name='Function'], CallExpression[callee.name='Function']",
69
+ message: "Execution de chaine interdite par script-src (pas d'unsafe-eval).",
70
+ },
71
+ ];
72
+
73
+ /** Un remote ne fournit pas de gestionnaire d'erreurs : un seul, celui de l'hote. */
74
+ const SECOND_ERROR_HANDLER = {
75
+ selector: "Property[key.name='provide'][value.name='ErrorHandler']",
76
+ message: "Un seul ErrorHandler : celui de l'hote. Un remote n'en fournit jamais.",
77
+ };
78
+
79
+ const IMPORTS_INTERDITS = {
80
+ paths: [
81
+ { name: 'zone.js', message: 'Le socle est zoneless.' },
82
+ {
83
+ name: '@angular/common/http',
84
+ importNames: ['provideHttpClient'],
85
+ message: 'provideStarterCore fournit le client HTTP (intercepteurs du socle).',
86
+ },
87
+ { name: '@angular/material', message: 'Les composants sont ceux du socle (st-*).' },
88
+ ],
89
+ patterns: [
90
+ {
91
+ group: ['@tanstack/*'],
92
+ message: 'La lecture de donnees passe par httpResource() (decision 2026-09-12).',
93
+ },
94
+ {
95
+ group: ['@ngx-translate/*', '@jsverse/transloco', '@ngneat/transloco'],
96
+ message: 'La langue et les catalogues sont ceux du socle (injecterTraduction).',
97
+ },
98
+ {
99
+ group: [
100
+ 'lucide*',
101
+ '@ng-icons/*',
102
+ '@fortawesome/*',
103
+ '@material-design-icons/*',
104
+ '@material-symbols/*',
105
+ 'material-symbols',
106
+ '@tabler/*',
107
+ '@heroicons/*',
108
+ '@phosphor-icons/*',
109
+ ],
110
+ message: "Aucune bibliotheque d'icones : st-icon, le sprite du socle et le sprite metier.",
111
+ },
112
+ ],
113
+ };
114
+
115
+ /** Ce qu'un remote n'a pas le droit de FOURNIR : l'hote le fait (ADR 0029 §2, plan SPA §4). */
116
+ const FOURNI_PAR_L_HOTE = [
117
+ {
118
+ name: '@starterlib/core',
119
+ importNames: ['provideStarterCore', 'withLangue', 'withNavigation'],
120
+ message: "Fourni par l'hote (coquille ou src/hote-dev), jamais par un remote.",
121
+ },
122
+ {
123
+ name: '@starterlib/ui',
124
+ importNames: ['provideStarterUi'],
125
+ message: "Fourni par l'hote (coquille ou src/hote-dev), jamais par un remote.",
126
+ },
127
+ {
128
+ name: '@angular/router',
129
+ importNames: ['provideRouter'],
130
+ message: 'Un seul routeur : celui de la coquille. Un remote expose des Routes.',
131
+ },
132
+ ];
133
+
134
+ /**
135
+ * La configuration d'une SPA. `prefixe` : celui de ses selecteurs (`ct`, jamais `st`) ; `racine` :
136
+ * `import.meta.dirname` de son `eslint.config.mjs` (le projet TypeScript) ; `role` : `remote` (par
137
+ * defaut) ou `hote` ; `ignores` : des chemins de plus a ne pas lire.
138
+ */
139
+ export function configurationDuSocle({ prefixe, racine, role = 'remote', ignores = [] } = {}) {
140
+ if (typeof prefixe !== 'string' || !PREFIXE.test(prefixe) || prefixe === 'st') {
141
+ throw new Error(
142
+ `@starterlib/eslint-config : prefixe "${String(prefixe)}", deux a huit minuscules attendues (jamais st).`,
143
+ );
144
+ }
145
+ if (typeof racine !== 'string' || racine === '') {
146
+ throw new Error(
147
+ "@starterlib/eslint-config : racine attendue (import.meta.dirname de l'appelant).",
148
+ );
149
+ }
150
+ if (role !== 'remote' && role !== 'hote') {
151
+ throw new Error(`@starterlib/eslint-config : role "${String(role)}", remote ou hote attendu.`);
152
+ }
153
+
154
+ return tseslint.config(
155
+ {
156
+ ignores: ['dist/**', 'node_modules/**', 'coverage/**', '.angular/**', ...ignores],
157
+ },
158
+ {
159
+ files: ['**/*.ts'],
160
+ extends: [
161
+ eslint.configs.recommended,
162
+ ...tseslint.configs.strictTypeChecked,
163
+ ...tseslint.configs.stylisticTypeChecked,
164
+ ...angular.configs.tsRecommended,
165
+ ],
166
+ languageOptions: {
167
+ parserOptions: { projectService: true, tsconfigRootDir: racine },
168
+ },
169
+ processor: angular.processInlineTemplates,
170
+ plugins: { socle },
171
+ rules: {
172
+ '@angular-eslint/prefer-signals': 'error',
173
+ '@angular-eslint/prefer-signal-model': 'error',
174
+ '@angular-eslint/prefer-output-emitter-ref': 'error',
175
+ '@angular-eslint/prefer-output-readonly': 'error',
176
+ '@angular-eslint/no-uncalled-signals': 'error',
177
+ '@angular-eslint/prefer-inject': 'error',
178
+ '@angular-eslint/prefer-standalone': 'error',
179
+ '@angular-eslint/prefer-on-push-component-change-detection': 'error',
180
+ '@angular-eslint/no-developer-preview': 'error',
181
+ '@angular-eslint/no-experimental': 'error',
182
+ // Gabarits et feuilles dans leurs fichiers : le lint de gabarit, la revue et stylelint les
183
+ // lisent ; un gabarit en ligne echappait aux trois.
184
+ '@angular-eslint/component-max-inline-declarations': [
185
+ 'error',
186
+ { template: 0, styles: 0, animations: 0 },
187
+ ],
188
+ '@angular-eslint/component-selector': [
189
+ 'error',
190
+ { type: 'element', prefix: prefixe, style: 'kebab-case' },
191
+ ],
192
+ '@angular-eslint/directive-selector': [
193
+ 'error',
194
+ { type: 'attribute', prefix: prefixe, style: 'camelCase' },
195
+ ],
196
+ 'no-new-func': 'error',
197
+ 'no-restricted-globals': ['error', ...STOCKAGE, ...GLOBAUX_DU_NAVIGATEUR, ...RESEAU],
198
+ 'no-restricted-properties': [
199
+ 'error',
200
+ { object: 'window', property: 'localStorage', message: 'Aucun stockage navigateur.' },
201
+ { object: 'window', property: 'sessionStorage', message: 'Aucun stockage navigateur.' },
202
+ {
203
+ object: 'window',
204
+ property: 'location',
205
+ message: 'Le Router (et StNavigation), jamais window.location.',
206
+ },
207
+ {
208
+ object: 'document',
209
+ property: 'cookie',
210
+ message: 'Le cookie est HttpOnly : illisible.',
211
+ },
212
+ ],
213
+ 'no-restricted-syntax': ['error', ...SYNTAXE_INTERDITE],
214
+ 'no-restricted-imports': ['error', IMPORTS_INTERDITS],
215
+ 'socle/route-has-title': 'error',
216
+ 'socle/no-toast-danger': 'error',
217
+ 'socle/no-free-text-query-param': 'error',
218
+ },
219
+ },
220
+ {
221
+ // Le point d'entree s'execute AVANT Angular (aucun DOCUMENT injectable), et charge la
222
+ // federation ; les scenarios d'assemblage evaluent leurs fonctions DANS la page.
223
+ files: ['src/main.ts', 'e2e/**/*.ts'],
224
+ rules: { 'no-restricted-globals': ['error', ...STOCKAGE] },
225
+ },
226
+ {
227
+ // Un essai monte des routes pour les eprouver, sans page a annoncer ; ses composants hotes
228
+ // tiennent en une ligne de gabarit ; il pilote le DOM de jsdom (document, window). Rien de
229
+ // cela ne part en production ; le stockage reste interdit.
230
+ files: ['**/*.spec.ts'],
231
+ rules: {
232
+ 'socle/route-has-title': 'off',
233
+ '@angular-eslint/component-max-inline-declarations': 'off',
234
+ 'no-restricted-globals': ['error', ...STOCKAGE],
235
+ },
236
+ },
237
+ {
238
+ files: ['**/*.html'],
239
+ // `src/index.html` n'est pas un gabarit : la page que le serveur sert, dont le titre est
240
+ // remplace a l'execution par celui de la route.
241
+ ignores: ['src/index.html'],
242
+ extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],
243
+ plugins: { socle },
244
+ rules: {
245
+ 'socle/no-raw-interactive': 'error',
246
+ 'socle/no-literal-text': 'error',
247
+ 'socle/no-angular-locale-pipes': 'error',
248
+ },
249
+ },
250
+ {
251
+ // L'hote de developpement d'un remote (`src/hote-dev`, jamais livre : A0840, lot 269) : il
252
+ // ne parle qu'aux developpeurs. APRES le bloc des gabarits, qui sinon le reprendrait.
253
+ files: ['src/hote-dev/**/*.ts', 'src/hote-dev/**/*.html'],
254
+ rules: {
255
+ 'socle/route-has-title': 'off',
256
+ 'socle/no-literal-text': 'off',
257
+ '@angular-eslint/component-max-inline-declarations': 'off',
258
+ },
259
+ },
260
+ role === 'remote'
261
+ ? {
262
+ // Un remote INJECTE, seul l'hote FOURNIT : ces fournisseurs n'ont leur place que dans
263
+ // l'hote de developpement (src/hote-dev), jamais dans ce qui est expose.
264
+ files: ['src/app/**/*.ts'],
265
+ rules: {
266
+ 'no-restricted-imports': [
267
+ 'error',
268
+ { ...IMPORTS_INTERDITS, paths: [...IMPORTS_INTERDITS.paths, ...FOURNI_PAR_L_HOTE] },
269
+ ],
270
+ 'no-restricted-syntax': ['error', ...SYNTAXE_INTERDITE, SECOND_ERROR_HANDLER],
271
+ },
272
+ }
273
+ : {},
274
+ );
275
+ }
276
+
277
+ export default configurationDuSocle;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@starterlib/eslint-config",
3
+ "version": "0.10.2",
4
+ "description": "Configuration ESLint d'une SPA du socle frontal starter, parametree par son prefixe, et configuration stylelint de ses feuilles. Remplace les eslint.config.mjs recopies.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./index.mjs",
9
+ "./stylelint": "./stylelint.mjs",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "index.mjs",
14
+ "stylelint.mjs"
15
+ ],
16
+ "engines": {
17
+ "node": ">=24.15.0"
18
+ },
19
+ "peerDependencies": {
20
+ "@eslint/js": "^10.0.1",
21
+ "@starterlib/eslint-plugin": "^0.10.0",
22
+ "angular-eslint": "^22.5.0",
23
+ "eslint": "^10.10.0",
24
+ "typescript-eslint": "^8.70.0"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Arnoldkom/starter-web-platform.git",
32
+ "directory": "packages/eslint-config"
33
+ },
34
+ "homepage": "https://github.com/Arnoldkom/starter-web-platform/tree/main/packages/eslint-config#readme",
35
+ "keywords": [
36
+ "starter",
37
+ "socle-frontal",
38
+ "eslint",
39
+ "eslintconfig",
40
+ "stylelint"
41
+ ]
42
+ }
package/stylelint.mjs ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * La configuration stylelint du socle (lot 178, plan SPA §4, « Jetons ») : les feuilles lisent les
3
+ * jetons, elles n'en inventent pas. L'audit de design avait trouve a la main des `z-index` et des
4
+ * `px` qu'elle aurait attrapes.
5
+ *
6
+ * ```json
7
+ * // .stylelintrc.json d'une SPA
8
+ * { "extends": ["@starterlib/eslint-config/stylelint"] }
9
+ * ```
10
+ *
11
+ * - aucune couleur ecrite (hexadecimal, nom, `rgb()`, `hsl()`...) : un jeton `--st-*`, qui suit le
12
+ * theme. Une couleur DERIVEE d'un jeton (`rgb(from var(--st-couleur-primaire) r g b / 10%)`) en est
13
+ * un usage, pas une invention : elle est admise ;
14
+ * - aucun `px` NON NUL sur un espacement, une police ou un rayon : les jetons de rythme, de
15
+ * typographie et de forme, qui suivent la densite et le zoom du texte. Un `0px` de repli
16
+ * (`var(--st-hauteur-entete, 0px)`, `env(safe-area-inset-bottom, 0px)` dans un `calc()`, ou un `0`
17
+ * sans unite serait invalide) n'est pas une mesure ;
18
+ * - aucun `z-index` en nombre : les plans du socle (`--st-plan-*`) ;
19
+ * - ni `order` ni sens inverse (`row-reverse`, `column-reverse`) : l'ordre du DOM est l'ordre visuel,
20
+ * celui de la lecture et de la tabulation.
21
+ *
22
+ * Une exception s'ecrit sur place, avec sa raison :
23
+ * `// stylelint-disable-next-line <regle> -- <raison>`.
24
+ */
25
+
26
+ /** Une fonction de couleur qui n'est pas derivee d'un jeton du socle. */
27
+ const COULEUR_ECRITE = '/(^|[^-\\w])(rgba?|hsla?|hwb)\\((?!\\s*from\\s+var\\(--st-)/';
28
+
29
+ /** Une mesure en pixels non nulle, negative comprise. */
30
+ const PIXELS_NON_NULS = '/(^|[^\\w.])-?(?!0px\\b)(\\d*\\.)?\\d+px\\b/';
31
+
32
+ export default {
33
+ rules: {
34
+ 'color-no-hex': true,
35
+ 'color-named': 'never',
36
+ 'declaration-property-value-disallowed-list': {
37
+ '/.*/': [COULEUR_ECRITE],
38
+ '/^(margin|padding|gap|row-gap|column-gap|inset)/': [PIXELS_NON_NULS],
39
+ '/^(font-size|line-height|letter-spacing|word-spacing)$/': [PIXELS_NON_NULS],
40
+ '/^border(-(start|end)-(start|end)|-(top|bottom)-(left|right))?-radius$/': [PIXELS_NON_NULS],
41
+ 'flex-direction': ['/reverse/'],
42
+ 'flex-flow': ['/reverse/'],
43
+ },
44
+ 'declaration-property-value-allowed-list': {
45
+ 'z-index': ['/^var\\(--st-/', '/^calc\\(var\\(--st-/', 'auto', '0'],
46
+ },
47
+ 'property-disallowed-list': ['order'],
48
+ },
49
+ overrides: [{ files: ['**/*.scss'], customSyntax: 'postcss-scss' }],
50
+ };