@hublo/sentinel 1.1.0-alpha.1 → 1.1.0-alpha.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.
@@ -14,7 +14,7 @@ import {
14
14
  registerAdapters,
15
15
  resolve,
16
16
  resolveBin
17
- } from "../chunk-RLMXN2ZJ.js";
17
+ } from "../chunk-EZCRU6XE.js";
18
18
 
19
19
  // bin/sentinel.ts
20
20
  import { program } from "commander";
@@ -187,8 +187,204 @@ var NEST_DISABLED = [
187
187
  reason: "the single occurrence is a regex whose whole purpose is stripping ASCII control characters before XML output, which the rule cannot distinguish from an accident"
188
188
  }
189
189
  ];
190
+ var REACT_DISABLED = [
191
+ // The jest suite is the single largest group: enabling it turned 135 errors into a
192
+ // wall nobody would read on day one. Adoption has to be a lateral move first.
193
+ {
194
+ rule: "jest/expect-expect",
195
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
196
+ },
197
+ {
198
+ rule: "jest/no-alias-methods",
199
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
200
+ },
201
+ {
202
+ rule: "jest/no-commented-out-tests",
203
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
204
+ },
205
+ {
206
+ rule: "jest/no-conditional-expect",
207
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
208
+ },
209
+ {
210
+ rule: "jest/no-deprecated-functions",
211
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
212
+ },
213
+ {
214
+ rule: "jest/no-disabled-tests",
215
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
216
+ },
217
+ {
218
+ rule: "jest/no-done-callback",
219
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
220
+ },
221
+ {
222
+ rule: "jest/no-export",
223
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
224
+ },
225
+ {
226
+ rule: "jest/no-focused-tests",
227
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
228
+ },
229
+ {
230
+ rule: "jest/no-identical-title",
231
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
232
+ },
233
+ {
234
+ rule: "jest/no-interpolation-in-snapshots",
235
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
236
+ },
237
+ {
238
+ rule: "jest/no-jasmine-globals",
239
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
240
+ },
241
+ {
242
+ rule: "jest/no-mocks-import",
243
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
244
+ },
245
+ {
246
+ rule: "jest/no-standalone-expect",
247
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
248
+ },
249
+ {
250
+ rule: "jest/no-test-prefixes",
251
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
252
+ },
253
+ {
254
+ rule: "jest/prefer-to-be",
255
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
256
+ },
257
+ {
258
+ rule: "jest/prefer-to-contain",
259
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
260
+ },
261
+ {
262
+ rule: "jest/prefer-to-have-length",
263
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
264
+ },
265
+ {
266
+ rule: "jest/valid-describe-callback",
267
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
268
+ },
269
+ {
270
+ rule: "jest/valid-expect",
271
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
272
+ },
273
+ {
274
+ rule: "jest/valid-expect-in-promise",
275
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
276
+ },
277
+ {
278
+ rule: "jest/valid-title",
279
+ reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
280
+ },
281
+ // TanStack Query rules the codebase has never satisfied.
282
+ {
283
+ rule: "@tanstack/query/exhaustive-deps",
284
+ reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
285
+ },
286
+ {
287
+ rule: "@tanstack/query/infinite-query-property-order",
288
+ reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
289
+ },
290
+ {
291
+ rule: "@tanstack/query/mutation-property-order",
292
+ reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
293
+ },
294
+ {
295
+ rule: "@tanstack/query/no-rest-destructuring",
296
+ reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
297
+ },
298
+ {
299
+ rule: "@tanstack/query/no-unstable-deps",
300
+ reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
301
+ },
302
+ {
303
+ rule: "@tanstack/query/no-void-query-fn",
304
+ reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
305
+ },
306
+ {
307
+ rule: "@tanstack/query/stable-query-client",
308
+ reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
309
+ },
310
+ // typescript-eslint rules carrying real, pre-existing debt.
311
+ {
312
+ rule: "typescript/no-empty-function",
313
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
314
+ },
315
+ {
316
+ rule: "typescript/no-explicit-any",
317
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
318
+ },
319
+ {
320
+ rule: "typescript/no-unnecessary-type-assertion",
321
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
322
+ },
323
+ {
324
+ rule: "typescript/no-unsafe-argument",
325
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
326
+ },
327
+ {
328
+ rule: "typescript/no-unsafe-assignment",
329
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
330
+ },
331
+ {
332
+ rule: "typescript/no-unsafe-call",
333
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
334
+ },
335
+ {
336
+ rule: "typescript/no-unsafe-enum-comparison",
337
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
338
+ },
339
+ {
340
+ rule: "typescript/no-unsafe-member-access",
341
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
342
+ },
343
+ {
344
+ rule: "typescript/no-unsafe-return",
345
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
346
+ },
347
+ {
348
+ rule: "typescript/no-unused-vars",
349
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
350
+ },
351
+ {
352
+ rule: "typescript/only-throw-error",
353
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
354
+ },
355
+ {
356
+ rule: "typescript/require-await",
357
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
358
+ },
359
+ // One-offs, each for its own reason.
360
+ {
361
+ rule: "no-console",
362
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
363
+ },
364
+ {
365
+ rule: "no-unsafe-optional-chaining",
366
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
367
+ },
368
+ {
369
+ rule: "react/jsx-curly-brace-presence",
370
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
371
+ },
372
+ {
373
+ rule: "styled-components-a11y/label-has-for",
374
+ reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
375
+ },
376
+ {
377
+ rule: "jsx-a11y/img-redundant-alt",
378
+ reason: 'one real finding, a decorative image whose title is already rendered as a heading beside it, so the fix is alt="". A genuine accessibility issue rather than a false positive: parked only so swapping linters stays a lateral move, and worth switching back on as soon as it is fixed'
379
+ },
380
+ {
381
+ rule: "hublo/no-palette-common",
382
+ reason: "the house rule finds 3 real violations; they are fixed as their own change rather than inside a linter swap, so the rule ships visible-but-off and can be switched on once they are"
383
+ }
384
+ ];
190
385
  var BY_FLAVOUR = {
191
- nest: NEST_DISABLED
386
+ nest: NEST_DISABLED,
387
+ react: REACT_DISABLED
192
388
  };
193
389
  function disabledRulesFor(flavour) {
194
390
  return BY_FLAVOUR[flavour] ?? [];
@@ -393,6 +589,261 @@ var nestLayer = {
393
589
  };
394
590
  var nestRules = { ...lintBase, ...nestLayer };
395
591
 
592
+ // src/roles/lint/flavours/react.ts
593
+ var reactLayer = {
594
+ // ESLint core (42)
595
+ curly: ["error", "all"],
596
+ "for-direction": "error",
597
+ "no-async-promise-executor": "error",
598
+ "no-case-declarations": "error",
599
+ "no-compare-neg-zero": "error",
600
+ "no-cond-assign": ["error", "except-parens"],
601
+ "no-constant-binary-expression": "error",
602
+ "no-constant-condition": ["error", { checkLoops: "allExceptWhileTrue" }],
603
+ "no-control-regex": "error",
604
+ "no-debugger": "error",
605
+ "no-delete-var": "error",
606
+ "no-dupe-else-if": "error",
607
+ "no-duplicate-case": "error",
608
+ "no-empty": ["error", { allowEmptyCatch: false }],
609
+ "no-empty-character-class": "error",
610
+ "no-empty-pattern": ["error", { allowObjectPatternsAsParameters: false }],
611
+ "no-empty-static-block": "error",
612
+ "no-ex-assign": "error",
613
+ "no-extra-boolean-cast": ["error", {}],
614
+ "no-fallthrough": ["error", { allowEmptyCase: false, reportUnusedFallthroughComment: false }],
615
+ "no-global-assign": ["error", { exceptions: [] }],
616
+ "no-invalid-regexp": ["error", {}],
617
+ "no-irregular-whitespace": [
618
+ "error",
619
+ {
620
+ skipComments: false,
621
+ skipJSXText: false,
622
+ skipRegExps: false,
623
+ skipStrings: true,
624
+ skipTemplates: false
625
+ }
626
+ ],
627
+ "no-loss-of-precision": "error",
628
+ "no-misleading-character-class": "error",
629
+ "no-nonoctal-decimal-escape": "error",
630
+ "no-prototype-builtins": "error",
631
+ "no-regex-spaces": "error",
632
+ "no-restricted-imports": [
633
+ "error",
634
+ {
635
+ patterns: [
636
+ {
637
+ group: ["next/*"],
638
+ message: "Importing from next/* is not allowed in the admin project."
639
+ },
640
+ {
641
+ group: ["storybook/*"],
642
+ message: "Importing from storybook/* is not allowed in the admin project."
643
+ },
644
+ {
645
+ group: ["@nestjs/*"],
646
+ message: "Importing from @nestjs/* is not allowed in the admin project."
647
+ },
648
+ {
649
+ group: ["next-i18next"],
650
+ message: "Importing from next-i18next is not allowed in the admin project."
651
+ },
652
+ {
653
+ group: ["@hublo/style/colors", "@hublo/style/colors/*"],
654
+ message: "Importing runtime colors directly is not allowed. Use the MUI theme, theme-derived CSS variables, or an explicit product-specific mapping."
655
+ }
656
+ ]
657
+ }
658
+ ],
659
+ "no-self-assign": ["error", { props: true }],
660
+ "no-shadow-restricted-names": ["error", { reportGlobalThis: false }],
661
+ "no-sparse-arrays": "error",
662
+ "no-unexpected-multiline": "error",
663
+ "no-unsafe-finally": "error",
664
+ "no-unused-labels": "error",
665
+ "no-unused-private-class-members": "error",
666
+ "no-useless-backreference": "error",
667
+ "no-useless-catch": "error",
668
+ "no-useless-escape": ["error", { allowRegexCharacters: [] }],
669
+ "require-yield": "error",
670
+ "use-isnan": ["error", { enforceForIndexOf: false, enforceForSwitchCase: true }],
671
+ "valid-typeof": ["error", { requireStringLiterals: false }],
672
+ // eslint-plugin-styled-components-a11y (hosted) (32)
673
+ "styled-components-a11y/alt-text": "error",
674
+ "styled-components-a11y/anchor-has-content": "error",
675
+ "styled-components-a11y/anchor-is-valid": "error",
676
+ "styled-components-a11y/aria-activedescendant-has-tabindex": "error",
677
+ "styled-components-a11y/aria-props": "error",
678
+ "styled-components-a11y/aria-proptypes": "error",
679
+ "styled-components-a11y/aria-role": "error",
680
+ "styled-components-a11y/aria-unsupported-elements": "error",
681
+ "styled-components-a11y/autocomplete-valid": "error",
682
+ "styled-components-a11y/click-events-have-key-events": "error",
683
+ "styled-components-a11y/control-has-associated-label": [
684
+ "off",
685
+ {
686
+ ignoreElements: ["audio", "canvas", "embed", "input", "textarea", "tr", "video"],
687
+ ignoreRoles: [
688
+ "grid",
689
+ "listbox",
690
+ "menu",
691
+ "menubar",
692
+ "radiogroup",
693
+ "row",
694
+ "tablist",
695
+ "toolbar",
696
+ "tree",
697
+ "treegrid"
698
+ ],
699
+ includeRoles: ["alert", "dialog"]
700
+ }
701
+ ],
702
+ "styled-components-a11y/heading-has-content": "error",
703
+ "styled-components-a11y/html-has-lang": "error",
704
+ "styled-components-a11y/iframe-has-title": "error",
705
+ "styled-components-a11y/img-redundant-alt": "error",
706
+ "styled-components-a11y/interactive-supports-focus": [
707
+ "error",
708
+ {
709
+ tabbable: [
710
+ "button",
711
+ "checkbox",
712
+ "link",
713
+ "progressbar",
714
+ "searchbox",
715
+ "slider",
716
+ "spinbutton",
717
+ "switch",
718
+ "textbox"
719
+ ]
720
+ }
721
+ ],
722
+ "styled-components-a11y/label-has-associated-control": "error",
723
+ "styled-components-a11y/media-has-caption": "error",
724
+ "styled-components-a11y/mouse-events-have-key-events": "error",
725
+ "styled-components-a11y/no-access-key": "error",
726
+ "styled-components-a11y/no-autofocus": "error",
727
+ "styled-components-a11y/no-distracting-elements": "error",
728
+ "styled-components-a11y/no-interactive-element-to-noninteractive-role": "error",
729
+ "styled-components-a11y/no-noninteractive-element-interactions": [
730
+ "error",
731
+ { body: ["onError", "onLoad"], iframe: ["onError", "onLoad"], img: ["onError", "onLoad"] }
732
+ ],
733
+ "styled-components-a11y/no-noninteractive-element-to-interactive-role": "error",
734
+ "styled-components-a11y/no-noninteractive-tabindex": "error",
735
+ "styled-components-a11y/no-redundant-roles": "error",
736
+ "styled-components-a11y/no-static-element-interactions": "error",
737
+ "styled-components-a11y/role-has-required-aria-props": "error",
738
+ "styled-components-a11y/role-supports-aria-props": "error",
739
+ "styled-components-a11y/scope": "error",
740
+ "styled-components-a11y/tabindex-no-positive": "error",
741
+ // eslint-plugin-jsx-a11y (31)
742
+ "jsx-a11y/alt-text": "error",
743
+ "jsx-a11y/anchor-has-content": "error",
744
+ "jsx-a11y/anchor-is-valid": "error",
745
+ "jsx-a11y/aria-activedescendant-has-tabindex": "error",
746
+ "jsx-a11y/aria-props": "error",
747
+ "jsx-a11y/aria-proptypes": "error",
748
+ "jsx-a11y/aria-role": "error",
749
+ "jsx-a11y/aria-unsupported-elements": "error",
750
+ "jsx-a11y/autocomplete-valid": "error",
751
+ "jsx-a11y/click-events-have-key-events": "error",
752
+ "jsx-a11y/heading-has-content": "error",
753
+ "jsx-a11y/html-has-lang": "error",
754
+ "jsx-a11y/iframe-has-title": "error",
755
+ "jsx-a11y/img-redundant-alt": "error",
756
+ "jsx-a11y/interactive-supports-focus": [
757
+ "error",
758
+ {
759
+ tabbable: [
760
+ "button",
761
+ "checkbox",
762
+ "link",
763
+ "progressbar",
764
+ "searchbox",
765
+ "slider",
766
+ "spinbutton",
767
+ "switch",
768
+ "textbox"
769
+ ]
770
+ }
771
+ ],
772
+ "jsx-a11y/label-has-associated-control": "error",
773
+ "jsx-a11y/media-has-caption": "error",
774
+ "jsx-a11y/mouse-events-have-key-events": "error",
775
+ "jsx-a11y/no-access-key": "error",
776
+ "jsx-a11y/no-autofocus": "error",
777
+ "jsx-a11y/no-distracting-elements": "error",
778
+ "jsx-a11y/no-interactive-element-to-noninteractive-role": "error",
779
+ "jsx-a11y/no-noninteractive-element-interactions": [
780
+ "error",
781
+ { body: ["onError", "onLoad"], iframe: ["onError", "onLoad"], img: ["onError", "onLoad"] }
782
+ ],
783
+ "jsx-a11y/no-noninteractive-element-to-interactive-role": "error",
784
+ "jsx-a11y/no-noninteractive-tabindex": "error",
785
+ "jsx-a11y/no-redundant-roles": "error",
786
+ "jsx-a11y/no-static-element-interactions": "error",
787
+ "jsx-a11y/role-has-required-aria-props": "error",
788
+ "jsx-a11y/role-supports-aria-props": "error",
789
+ "jsx-a11y/scope": "error",
790
+ "jsx-a11y/tabindex-no-positive": "error",
791
+ // typescript-eslint (18)
792
+ "typescript/await-thenable": "error",
793
+ "typescript/no-array-delete": "error",
794
+ "typescript/no-base-to-string": "error",
795
+ "typescript/no-duplicate-type-constituents": "error",
796
+ "typescript/no-floating-promises": ["error", { ignoreVoid: true }],
797
+ "typescript/no-for-in-array": "error",
798
+ "typescript/no-implied-eval": "error",
799
+ "typescript/no-inferrable-types": "error",
800
+ "typescript/no-misused-promises": ["error", { checksVoidReturn: { attributes: false } }],
801
+ "typescript/no-non-null-assertion": "warn",
802
+ "typescript/no-redundant-type-constituents": "error",
803
+ "typescript/no-require-imports": "error",
804
+ "typescript/no-unsafe-unary-minus": "error",
805
+ "typescript/prefer-promise-reject-errors": "error",
806
+ "typescript/restrict-plus-operands": "error",
807
+ "typescript/restrict-template-expressions": "error",
808
+ "typescript/switch-exhaustiveness-check": "warn",
809
+ "typescript/unbound-method": "error",
810
+ // eslint-plugin-react-hooks (2)
811
+ "react-hooks/exhaustive-deps": "warn",
812
+ "react-hooks/rules-of-hooks": "error",
813
+ // eslint-plugin-no-barrel-files (hosted) (1)
814
+ "no-barrel-files/no-barrel-files": "warn",
815
+ // @nx/eslint-plugin (hosted) (1)
816
+ "nx/enforce-module-boundaries": [
817
+ "error",
818
+ {
819
+ enforceBuildableLibDependency: true,
820
+ allow: [],
821
+ depConstraints: [{ sourceTag: "*", onlyDependOnLibsWithTags: ["*"] }]
822
+ }
823
+ ],
824
+ // eslint-plugin-import — aliased, oxlint reserves `import` (1)
825
+ "import-js/order": [
826
+ "error",
827
+ {
828
+ "newlines-between": "always",
829
+ groups: ["builtin", "external", "internal", ["parent", "index"], "sibling"],
830
+ alphabetize: { order: "asc" },
831
+ pathGroups: [
832
+ { pattern: "@nestjs/**", group: "external" },
833
+ {
834
+ pattern: "{@admin/**,@authentication/**,@featureToggles/**,@institution/**,@hublo/**,@network/**,@mission/**,@notification/**,@shared/**,@talent/**,@worker/**,@contract/**,@config/**,@front/**}",
835
+ group: "internal",
836
+ position: "before"
837
+ }
838
+ ],
839
+ pathGroupsExcludedImportTypes: ["builtin"]
840
+ }
841
+ ],
842
+ // eslint-plugin-react-refresh (hosted) (1)
843
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }]
844
+ };
845
+ var reactRules = { ...lintBase, ...reactLayer };
846
+
396
847
  // src/roles/lint/presets.ts
397
848
  var TS_EXTENSIONS = [".ts", ".cts", ".mts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
398
849
  var IMPORT_SETTINGS = {
@@ -418,8 +869,26 @@ var BASE_PRESET = {
418
869
  env: { es2022: true },
419
870
  rules: lintBase
420
871
  };
872
+ var REACT_PRESET = {
873
+ plugins: ["typescript", "react", "jsx-a11y", "jest", "import", "unicorn", "oxc"],
874
+ jsPlugins: [
875
+ "eslint-plugin-no-barrel-files",
876
+ { name: "nx", specifier: "@nx/eslint-plugin" },
877
+ { name: "import-js", specifier: "eslint-plugin-import" },
878
+ { name: "@tanstack/query", specifier: "@tanstack/eslint-plugin-query" },
879
+ { name: "styled-components-a11y", specifier: "eslint-plugin-styled-components-a11y" },
880
+ { name: "react-refresh", specifier: "eslint-plugin-react-refresh" },
881
+ // Ships inside sentinel, resolved relative to THIS preset file. Oxlint has no
882
+ // `no-restricted-syntax`, so the Hublo house rules are a real rule instead.
883
+ "../plugins/hublo.js"
884
+ ],
885
+ env: { browser: true, es2022: true, node: true, jest: true },
886
+ settings: IMPORT_SETTINGS,
887
+ rules: reactRules
888
+ };
421
889
  var BY_FLAVOUR2 = {
422
890
  nest: NEST_PRESET,
891
+ react: REACT_PRESET,
423
892
  // `node` ships the base alone, deliberately. It is the honest answer for a module that
424
893
  // belongs to no stack, and it keeps `--init` with no named target working everywhere
425
894
  // rather than blocking a whole workspace sweep on one unshipped flavour. It is thin (21
@@ -437,17 +906,43 @@ function lintPresetFor(flavour) {
437
906
  // src/roles/lint/rename-suppressions.ts
438
907
  import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
439
908
  import { join as join2, relative } from "path";
440
- var SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
909
+ var SOURCE_EXTENSIONS = [
910
+ ".ts",
911
+ ".tsx",
912
+ ".js",
913
+ ".jsx",
914
+ ".mjs",
915
+ ".cjs",
916
+ ".mts",
917
+ ".cts",
918
+ ".svelte",
919
+ ".vue"
920
+ ];
441
921
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage", ".git", ".nx"]);
442
922
  var SUPPRESSION = /(eslint-disable(?:-next-line|-line)?)\s+([^\n*]+?)(?:\s*--.*)?(?:\s*\*\/)?\s*$/;
443
- function renamedPrefixes(jsPlugins) {
923
+ function renamedPrefixes(jsPlugins, declared = {}) {
444
924
  const renames = /* @__PURE__ */ new Map();
445
925
  for (const plugin of jsPlugins) {
446
926
  if (typeof plugin === "string") continue;
447
927
  const original = plugin.specifier.replace(/^eslint-plugin-/, "").replace(/\/eslint-plugin$/, "");
448
928
  if (original !== plugin.name) renames.set(`${original}/`, `${plugin.name}/`);
449
929
  }
450
- return renames;
930
+ const slash = (value) => value.endsWith("/") ? value : `${value}/`;
931
+ for (const [from, to] of Object.entries(declared)) renames.set(slash(from), slash(to));
932
+ return collapseChains(renames);
933
+ }
934
+ function collapseChains(renames) {
935
+ const collapsed = /* @__PURE__ */ new Map();
936
+ for (const [from, to] of renames) {
937
+ let target = to;
938
+ for (let hop = 0; hop < renames.size; hop += 1) {
939
+ const next = renames.get(target);
940
+ if (next === void 0 || next === target) break;
941
+ target = next;
942
+ }
943
+ collapsed.set(from, target);
944
+ }
945
+ return collapsed;
451
946
  }
452
947
  function* sourceFiles(dir) {
453
948
  let entries;
@@ -684,6 +1179,14 @@ function oxlintSearchPath(cwd) {
684
1179
 
685
1180
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
686
1181
  var MAX_FIX_PASSES = 3;
1182
+ function summariseByPlugin(rules) {
1183
+ const counts = /* @__PURE__ */ new Map();
1184
+ for (const name of deferredRuleNames(rules)) {
1185
+ const plugin = name.includes("/") ? name.split("/")[0] ?? name : "core";
1186
+ counts.set(plugin, (counts.get(plugin) ?? 0) + 1);
1187
+ }
1188
+ return [...counts.entries()].sort((left, right) => right[1] - left[1]).map(([plugin, count]) => `${count} ${plugin}`).join(", ");
1189
+ }
687
1190
  var OxlintAdapter = class extends BaseAdapter {
688
1191
  target = "lint";
689
1192
  runner = "oxlint";
@@ -735,10 +1238,15 @@ var OxlintAdapter = class extends BaseAdapter {
735
1238
  }
736
1239
  const renames = findSuppressionRenames(
737
1240
  context.cwd,
738
- renamedPrefixes(lintPresetFor(context.flavour).jsPlugins)
1241
+ renamedPrefixes(
1242
+ lintPresetFor(context.flavour).jsPlugins,
1243
+ lintPresetFor(context.flavour).renames
1244
+ )
739
1245
  );
1246
+ const deleting = new Set(eslintConfigs);
1247
+ const rewritten = renames.filter((entry) => !deleting.has(entry.file));
740
1248
  const byFile = /* @__PURE__ */ new Map();
741
- for (const entry of renames) {
1249
+ for (const entry of rewritten) {
742
1250
  const list = byFile.get(entry.file) ?? [];
743
1251
  list.push({ from: entry.from, to: entry.to });
744
1252
  byFile.set(entry.file, list);
@@ -751,15 +1259,15 @@ var OxlintAdapter = class extends BaseAdapter {
751
1259
  ];
752
1260
  if (disabled.length > 0) {
753
1261
  notes.push(
754
- `${disabled.length} rule(s) not enforced by this preset: ${deferredRuleNames(disabled).join(", ")}. Run --inspect --lint for the reasons.`
1262
+ `${disabled.length} rule(s) not enforced by this preset (${summariseByPlugin(disabled)}). Run --inspect --lint for each rule and why.`
755
1263
  );
756
1264
  }
757
1265
  if (eslintConfigs.length > 0) {
758
1266
  notes.push(`removed ${eslintConfigs.join(", ")} and pointed the nx lint target at oxlint`);
759
1267
  }
760
- if (renames.length > 0) {
1268
+ if (rewritten.length > 0) {
761
1269
  notes.push(
762
- `renamed ${renames.length} suppression comment(s) whose plugin oxlint registers under a different name (${[...new Set(renames.map((entry) => entry.rule))].join(", ")}); left alone they would stop suppressing anything, silently`
1270
+ `renamed ${rewritten.length} suppression comment(s) whose plugin oxlint registers under a different name (${[...new Set(rewritten.map((entry) => entry.rule))].join(", ")}); left alone they would stop suppressing anything, silently`
763
1271
  );
764
1272
  }
765
1273
  notes.push("run `pnpm install`, then `pnpm run lint`");
@@ -816,35 +1324,35 @@ var OxlintAdapter = class extends BaseAdapter {
816
1324
  return { ok: false, code: 1 };
817
1325
  }
818
1326
  this.announceDisabled(ctx.flavour);
819
- const args = ["-c", LINT_CONFIG_FILE];
820
- if (canRunTypeAware(ctx.cwd)) {
821
- args.push("--type-aware");
822
- } else {
1327
+ const typeAware = canRunTypeAware(ctx.cwd);
1328
+ if (!typeAware) {
823
1329
  process.stderr.write(
824
1330
  `${palette(process.stderr).warn("sentinel lint(oxlint): oxlint-tsgolint is not installed, so type-aware rules will NOT run")}
825
1331
  `
826
1332
  );
827
1333
  }
828
- if (ctx.fix) args.push("--fix");
829
- args.push(".");
830
- const passes = ctx.fix ? MAX_FIX_PASSES : 1;
831
- let code = 1;
832
- for (let pass = 0; pass < passes; pass += 1) {
833
- const result = spawnSync(oxlint, args, {
1334
+ const env = { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) };
1335
+ const lint = (extra) => {
1336
+ const result = spawnSync(oxlint, ["-c", LINT_CONFIG_FILE, ...extra, "."], {
834
1337
  cwd: ctx.cwd,
835
1338
  stdio: "inherit",
836
- env: { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) }
1339
+ env
837
1340
  });
838
1341
  if (result.error) {
839
1342
  process.stderr.write(
840
1343
  `sentinel lint(oxlint): could not run oxlint (${result.error.message})
841
1344
  `
842
1345
  );
843
- return { ok: false, code: 1 };
1346
+ return 1;
1347
+ }
1348
+ return result.status ?? 1;
1349
+ };
1350
+ if (ctx.fix) {
1351
+ for (let pass = 0; pass < MAX_FIX_PASSES; pass += 1) {
1352
+ if (lint(["--fix"]) === 0) break;
844
1353
  }
845
- code = result.status ?? 1;
846
- if (code === 0) break;
847
1354
  }
1355
+ const code = lint(typeAware ? ["--type-aware"] : []);
848
1356
  return { ok: code === 0, code };
849
1357
  }
850
1358
  /** The module's resolved lint configuration, and what it deliberately does not enforce. */
@@ -915,7 +1423,7 @@ var OxlintAdapter = class extends BaseAdapter {
915
1423
  if (disabled.length === 0) return;
916
1424
  process.stderr.write(
917
1425
  `${palette(process.stderr).warn(
918
- `sentinel lint(oxlint): ${disabled.length} rule(s) not enforced by default (${deferredRuleNames(disabled).join(", ")}); run --inspect --lint for the reasons`
1426
+ `sentinel lint(oxlint): ${disabled.length} rule(s) not enforced by default (${summariseByPlugin(disabled)}); run --inspect --lint for each rule and why`
919
1427
  )}
920
1428
  `
921
1429
  );
@@ -1551,4 +2059,4 @@ export {
1551
2059
  detectFramework,
1552
2060
  dispatch
1553
2061
  };
1554
- //# sourceMappingURL=chunk-RLMXN2ZJ.js.map
2062
+ //# sourceMappingURL=chunk-EZCRU6XE.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-RLMXN2ZJ.js";
10
+ } from "./chunk-EZCRU6XE.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
@@ -0,0 +1,570 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": [
4
+ "typescript",
5
+ "react",
6
+ "jsx-a11y",
7
+ "jest",
8
+ "import",
9
+ "unicorn",
10
+ "oxc"
11
+ ],
12
+ "env": {
13
+ "browser": true,
14
+ "es2022": true,
15
+ "node": true,
16
+ "jest": true
17
+ },
18
+ "categories": {
19
+ "correctness": "off",
20
+ "suspicious": "off",
21
+ "pedantic": "off",
22
+ "perf": "off",
23
+ "style": "off",
24
+ "restriction": "off",
25
+ "nursery": "off"
26
+ },
27
+ "ignorePatterns": [
28
+ "**/dist/**",
29
+ "**/node_modules/**",
30
+ "**/coverage/**",
31
+ "**/*.mock.ts"
32
+ ],
33
+ "rules": {
34
+ "typescript/ban-ts-comment": "warn",
35
+ "typescript/no-array-constructor": "warn",
36
+ "typescript/no-duplicate-enum-values": "warn",
37
+ "typescript/no-empty-object-type": "warn",
38
+ "typescript/no-extra-non-null-assertion": "warn",
39
+ "typescript/no-misused-new": "warn",
40
+ "typescript/no-namespace": "warn",
41
+ "typescript/no-non-null-asserted-optional-chain": "warn",
42
+ "typescript/no-this-alias": "warn",
43
+ "typescript/no-unnecessary-type-constraint": "warn",
44
+ "typescript/no-unsafe-declaration-merging": "warn",
45
+ "typescript/no-unsafe-function-type": "warn",
46
+ "typescript/no-unused-expressions": [
47
+ "warn",
48
+ {
49
+ "allowShortCircuit": false,
50
+ "allowTaggedTemplates": false,
51
+ "allowTernary": false
52
+ }
53
+ ],
54
+ "typescript/no-wrapper-object-types": "warn",
55
+ "typescript/prefer-as-const": "warn",
56
+ "typescript/prefer-namespace-keyword": "warn",
57
+ "typescript/triple-slash-reference": "warn",
58
+ "no-var": "warn",
59
+ "prefer-const": [
60
+ "warn",
61
+ {
62
+ "destructuring": "any",
63
+ "ignoreReadBeforeAssign": false
64
+ }
65
+ ],
66
+ "prefer-rest-params": "warn",
67
+ "prefer-spread": "warn",
68
+ "curly": [
69
+ "error",
70
+ "all"
71
+ ],
72
+ "for-direction": "error",
73
+ "no-async-promise-executor": "error",
74
+ "no-case-declarations": "error",
75
+ "no-compare-neg-zero": "error",
76
+ "no-cond-assign": [
77
+ "error",
78
+ "except-parens"
79
+ ],
80
+ "no-constant-binary-expression": "error",
81
+ "no-constant-condition": [
82
+ "error",
83
+ {
84
+ "checkLoops": "allExceptWhileTrue"
85
+ }
86
+ ],
87
+ "no-control-regex": "error",
88
+ "no-debugger": "error",
89
+ "no-delete-var": "error",
90
+ "no-dupe-else-if": "error",
91
+ "no-duplicate-case": "error",
92
+ "no-empty": [
93
+ "error",
94
+ {
95
+ "allowEmptyCatch": false
96
+ }
97
+ ],
98
+ "no-empty-character-class": "error",
99
+ "no-empty-pattern": [
100
+ "error",
101
+ {
102
+ "allowObjectPatternsAsParameters": false
103
+ }
104
+ ],
105
+ "no-empty-static-block": "error",
106
+ "no-ex-assign": "error",
107
+ "no-extra-boolean-cast": [
108
+ "error",
109
+ {}
110
+ ],
111
+ "no-fallthrough": [
112
+ "error",
113
+ {
114
+ "allowEmptyCase": false,
115
+ "reportUnusedFallthroughComment": false
116
+ }
117
+ ],
118
+ "no-global-assign": [
119
+ "error",
120
+ {
121
+ "exceptions": []
122
+ }
123
+ ],
124
+ "no-invalid-regexp": [
125
+ "error",
126
+ {}
127
+ ],
128
+ "no-irregular-whitespace": [
129
+ "error",
130
+ {
131
+ "skipComments": false,
132
+ "skipJSXText": false,
133
+ "skipRegExps": false,
134
+ "skipStrings": true,
135
+ "skipTemplates": false
136
+ }
137
+ ],
138
+ "no-loss-of-precision": "error",
139
+ "no-misleading-character-class": "error",
140
+ "no-nonoctal-decimal-escape": "error",
141
+ "no-prototype-builtins": "error",
142
+ "no-regex-spaces": "error",
143
+ "no-restricted-imports": [
144
+ "error",
145
+ {
146
+ "patterns": [
147
+ {
148
+ "group": [
149
+ "next/*"
150
+ ],
151
+ "message": "Importing from next/* is not allowed in the admin project."
152
+ },
153
+ {
154
+ "group": [
155
+ "storybook/*"
156
+ ],
157
+ "message": "Importing from storybook/* is not allowed in the admin project."
158
+ },
159
+ {
160
+ "group": [
161
+ "@nestjs/*"
162
+ ],
163
+ "message": "Importing from @nestjs/* is not allowed in the admin project."
164
+ },
165
+ {
166
+ "group": [
167
+ "next-i18next"
168
+ ],
169
+ "message": "Importing from next-i18next is not allowed in the admin project."
170
+ },
171
+ {
172
+ "group": [
173
+ "@hublo/style/colors",
174
+ "@hublo/style/colors/*"
175
+ ],
176
+ "message": "Importing runtime colors directly is not allowed. Use the MUI theme, theme-derived CSS variables, or an explicit product-specific mapping."
177
+ }
178
+ ]
179
+ }
180
+ ],
181
+ "no-self-assign": [
182
+ "error",
183
+ {
184
+ "props": true
185
+ }
186
+ ],
187
+ "no-shadow-restricted-names": [
188
+ "error",
189
+ {
190
+ "reportGlobalThis": false
191
+ }
192
+ ],
193
+ "no-sparse-arrays": "error",
194
+ "no-unexpected-multiline": "error",
195
+ "no-unsafe-finally": "error",
196
+ "no-unused-labels": "error",
197
+ "no-unused-private-class-members": "error",
198
+ "no-useless-backreference": "error",
199
+ "no-useless-catch": "error",
200
+ "no-useless-escape": [
201
+ "error",
202
+ {
203
+ "allowRegexCharacters": []
204
+ }
205
+ ],
206
+ "require-yield": "error",
207
+ "use-isnan": [
208
+ "error",
209
+ {
210
+ "enforceForIndexOf": false,
211
+ "enforceForSwitchCase": true
212
+ }
213
+ ],
214
+ "valid-typeof": [
215
+ "error",
216
+ {
217
+ "requireStringLiterals": false
218
+ }
219
+ ],
220
+ "styled-components-a11y/alt-text": "error",
221
+ "styled-components-a11y/anchor-has-content": "error",
222
+ "styled-components-a11y/anchor-is-valid": "error",
223
+ "styled-components-a11y/aria-activedescendant-has-tabindex": "error",
224
+ "styled-components-a11y/aria-props": "error",
225
+ "styled-components-a11y/aria-proptypes": "error",
226
+ "styled-components-a11y/aria-role": "error",
227
+ "styled-components-a11y/aria-unsupported-elements": "error",
228
+ "styled-components-a11y/autocomplete-valid": "error",
229
+ "styled-components-a11y/click-events-have-key-events": "error",
230
+ "styled-components-a11y/control-has-associated-label": [
231
+ "off",
232
+ {
233
+ "ignoreElements": [
234
+ "audio",
235
+ "canvas",
236
+ "embed",
237
+ "input",
238
+ "textarea",
239
+ "tr",
240
+ "video"
241
+ ],
242
+ "ignoreRoles": [
243
+ "grid",
244
+ "listbox",
245
+ "menu",
246
+ "menubar",
247
+ "radiogroup",
248
+ "row",
249
+ "tablist",
250
+ "toolbar",
251
+ "tree",
252
+ "treegrid"
253
+ ],
254
+ "includeRoles": [
255
+ "alert",
256
+ "dialog"
257
+ ]
258
+ }
259
+ ],
260
+ "styled-components-a11y/heading-has-content": "error",
261
+ "styled-components-a11y/html-has-lang": "error",
262
+ "styled-components-a11y/iframe-has-title": "error",
263
+ "styled-components-a11y/img-redundant-alt": "error",
264
+ "styled-components-a11y/interactive-supports-focus": [
265
+ "error",
266
+ {
267
+ "tabbable": [
268
+ "button",
269
+ "checkbox",
270
+ "link",
271
+ "progressbar",
272
+ "searchbox",
273
+ "slider",
274
+ "spinbutton",
275
+ "switch",
276
+ "textbox"
277
+ ]
278
+ }
279
+ ],
280
+ "styled-components-a11y/label-has-associated-control": "error",
281
+ "styled-components-a11y/media-has-caption": "error",
282
+ "styled-components-a11y/mouse-events-have-key-events": "error",
283
+ "styled-components-a11y/no-access-key": "error",
284
+ "styled-components-a11y/no-autofocus": "error",
285
+ "styled-components-a11y/no-distracting-elements": "error",
286
+ "styled-components-a11y/no-interactive-element-to-noninteractive-role": "error",
287
+ "styled-components-a11y/no-noninteractive-element-interactions": [
288
+ "error",
289
+ {
290
+ "body": [
291
+ "onError",
292
+ "onLoad"
293
+ ],
294
+ "iframe": [
295
+ "onError",
296
+ "onLoad"
297
+ ],
298
+ "img": [
299
+ "onError",
300
+ "onLoad"
301
+ ]
302
+ }
303
+ ],
304
+ "styled-components-a11y/no-noninteractive-element-to-interactive-role": "error",
305
+ "styled-components-a11y/no-noninteractive-tabindex": "error",
306
+ "styled-components-a11y/no-redundant-roles": "error",
307
+ "styled-components-a11y/no-static-element-interactions": "error",
308
+ "styled-components-a11y/role-has-required-aria-props": "error",
309
+ "styled-components-a11y/role-supports-aria-props": "error",
310
+ "styled-components-a11y/scope": "error",
311
+ "styled-components-a11y/tabindex-no-positive": "error",
312
+ "jsx-a11y/alt-text": "error",
313
+ "jsx-a11y/anchor-has-content": "error",
314
+ "jsx-a11y/anchor-is-valid": "error",
315
+ "jsx-a11y/aria-activedescendant-has-tabindex": "error",
316
+ "jsx-a11y/aria-props": "error",
317
+ "jsx-a11y/aria-proptypes": "error",
318
+ "jsx-a11y/aria-role": "error",
319
+ "jsx-a11y/aria-unsupported-elements": "error",
320
+ "jsx-a11y/autocomplete-valid": "error",
321
+ "jsx-a11y/click-events-have-key-events": "error",
322
+ "jsx-a11y/heading-has-content": "error",
323
+ "jsx-a11y/html-has-lang": "error",
324
+ "jsx-a11y/iframe-has-title": "error",
325
+ "jsx-a11y/img-redundant-alt": "off",
326
+ "jsx-a11y/interactive-supports-focus": [
327
+ "error",
328
+ {
329
+ "tabbable": [
330
+ "button",
331
+ "checkbox",
332
+ "link",
333
+ "progressbar",
334
+ "searchbox",
335
+ "slider",
336
+ "spinbutton",
337
+ "switch",
338
+ "textbox"
339
+ ]
340
+ }
341
+ ],
342
+ "jsx-a11y/label-has-associated-control": "error",
343
+ "jsx-a11y/media-has-caption": "error",
344
+ "jsx-a11y/mouse-events-have-key-events": "error",
345
+ "jsx-a11y/no-access-key": "error",
346
+ "jsx-a11y/no-autofocus": "error",
347
+ "jsx-a11y/no-distracting-elements": "error",
348
+ "jsx-a11y/no-interactive-element-to-noninteractive-role": "error",
349
+ "jsx-a11y/no-noninteractive-element-interactions": [
350
+ "error",
351
+ {
352
+ "body": [
353
+ "onError",
354
+ "onLoad"
355
+ ],
356
+ "iframe": [
357
+ "onError",
358
+ "onLoad"
359
+ ],
360
+ "img": [
361
+ "onError",
362
+ "onLoad"
363
+ ]
364
+ }
365
+ ],
366
+ "jsx-a11y/no-noninteractive-element-to-interactive-role": "error",
367
+ "jsx-a11y/no-noninteractive-tabindex": "error",
368
+ "jsx-a11y/no-redundant-roles": "error",
369
+ "jsx-a11y/no-static-element-interactions": "error",
370
+ "jsx-a11y/role-has-required-aria-props": "error",
371
+ "jsx-a11y/role-supports-aria-props": "error",
372
+ "jsx-a11y/scope": "error",
373
+ "jsx-a11y/tabindex-no-positive": "error",
374
+ "typescript/await-thenable": "error",
375
+ "typescript/no-array-delete": "error",
376
+ "typescript/no-base-to-string": "error",
377
+ "typescript/no-duplicate-type-constituents": "error",
378
+ "typescript/no-floating-promises": [
379
+ "error",
380
+ {
381
+ "ignoreVoid": true
382
+ }
383
+ ],
384
+ "typescript/no-for-in-array": "error",
385
+ "typescript/no-implied-eval": "error",
386
+ "typescript/no-inferrable-types": "error",
387
+ "typescript/no-misused-promises": [
388
+ "error",
389
+ {
390
+ "checksVoidReturn": {
391
+ "attributes": false
392
+ }
393
+ }
394
+ ],
395
+ "typescript/no-non-null-assertion": "warn",
396
+ "typescript/no-redundant-type-constituents": "error",
397
+ "typescript/no-require-imports": "error",
398
+ "typescript/no-unsafe-unary-minus": "error",
399
+ "typescript/prefer-promise-reject-errors": "error",
400
+ "typescript/restrict-plus-operands": "error",
401
+ "typescript/restrict-template-expressions": "error",
402
+ "typescript/switch-exhaustiveness-check": "warn",
403
+ "typescript/unbound-method": "error",
404
+ "react-hooks/exhaustive-deps": "warn",
405
+ "react-hooks/rules-of-hooks": "error",
406
+ "no-barrel-files/no-barrel-files": "warn",
407
+ "nx/enforce-module-boundaries": [
408
+ "error",
409
+ {
410
+ "enforceBuildableLibDependency": true,
411
+ "allow": [],
412
+ "depConstraints": [
413
+ {
414
+ "sourceTag": "*",
415
+ "onlyDependOnLibsWithTags": [
416
+ "*"
417
+ ]
418
+ }
419
+ ]
420
+ }
421
+ ],
422
+ "import-js/order": [
423
+ "error",
424
+ {
425
+ "newlines-between": "always",
426
+ "groups": [
427
+ "builtin",
428
+ "external",
429
+ "internal",
430
+ [
431
+ "parent",
432
+ "index"
433
+ ],
434
+ "sibling"
435
+ ],
436
+ "alphabetize": {
437
+ "order": "asc"
438
+ },
439
+ "pathGroups": [
440
+ {
441
+ "pattern": "@nestjs/**",
442
+ "group": "external"
443
+ },
444
+ {
445
+ "pattern": "{@admin/**,@authentication/**,@featureToggles/**,@institution/**,@hublo/**,@network/**,@mission/**,@notification/**,@shared/**,@talent/**,@worker/**,@contract/**,@config/**,@front/**}",
446
+ "group": "internal",
447
+ "position": "before"
448
+ }
449
+ ],
450
+ "pathGroupsExcludedImportTypes": [
451
+ "builtin"
452
+ ]
453
+ }
454
+ ],
455
+ "react-refresh/only-export-components": [
456
+ "warn",
457
+ {
458
+ "allowConstantExport": true
459
+ }
460
+ ],
461
+ "jest/expect-expect": "off",
462
+ "jest/no-alias-methods": "off",
463
+ "jest/no-commented-out-tests": "off",
464
+ "jest/no-conditional-expect": "off",
465
+ "jest/no-deprecated-functions": "off",
466
+ "jest/no-disabled-tests": "off",
467
+ "jest/no-done-callback": "off",
468
+ "jest/no-export": "off",
469
+ "jest/no-focused-tests": "off",
470
+ "jest/no-identical-title": "off",
471
+ "jest/no-interpolation-in-snapshots": "off",
472
+ "jest/no-jasmine-globals": "off",
473
+ "jest/no-mocks-import": "off",
474
+ "jest/no-standalone-expect": "off",
475
+ "jest/no-test-prefixes": "off",
476
+ "jest/prefer-to-be": "off",
477
+ "jest/prefer-to-contain": "off",
478
+ "jest/prefer-to-have-length": "off",
479
+ "jest/valid-describe-callback": "off",
480
+ "jest/valid-expect": "off",
481
+ "jest/valid-expect-in-promise": "off",
482
+ "jest/valid-title": "off",
483
+ "@tanstack/query/exhaustive-deps": "off",
484
+ "@tanstack/query/infinite-query-property-order": "off",
485
+ "@tanstack/query/mutation-property-order": "off",
486
+ "@tanstack/query/no-rest-destructuring": "off",
487
+ "@tanstack/query/no-unstable-deps": "off",
488
+ "@tanstack/query/no-void-query-fn": "off",
489
+ "@tanstack/query/stable-query-client": "off",
490
+ "typescript/no-empty-function": "off",
491
+ "typescript/no-explicit-any": "off",
492
+ "typescript/no-unnecessary-type-assertion": "off",
493
+ "typescript/no-unsafe-argument": "off",
494
+ "typescript/no-unsafe-assignment": "off",
495
+ "typescript/no-unsafe-call": "off",
496
+ "typescript/no-unsafe-enum-comparison": "off",
497
+ "typescript/no-unsafe-member-access": "off",
498
+ "typescript/no-unsafe-return": "off",
499
+ "typescript/no-unused-vars": "off",
500
+ "typescript/only-throw-error": "off",
501
+ "typescript/require-await": "off",
502
+ "no-console": "off",
503
+ "no-unsafe-optional-chaining": "off",
504
+ "react/jsx-curly-brace-presence": "off",
505
+ "styled-components-a11y/label-has-for": "off",
506
+ "hublo/no-palette-common": "off"
507
+ },
508
+ "jsPlugins": [
509
+ "eslint-plugin-no-barrel-files",
510
+ {
511
+ "name": "nx",
512
+ "specifier": "@nx/eslint-plugin"
513
+ },
514
+ {
515
+ "name": "import-js",
516
+ "specifier": "eslint-plugin-import"
517
+ },
518
+ {
519
+ "name": "@tanstack/query",
520
+ "specifier": "@tanstack/eslint-plugin-query"
521
+ },
522
+ {
523
+ "name": "styled-components-a11y",
524
+ "specifier": "eslint-plugin-styled-components-a11y"
525
+ },
526
+ {
527
+ "name": "react-refresh",
528
+ "specifier": "eslint-plugin-react-refresh"
529
+ },
530
+ "../plugins/hublo.js"
531
+ ],
532
+ "settings": {
533
+ "import/extensions": [
534
+ ".ts",
535
+ ".cts",
536
+ ".mts",
537
+ ".tsx",
538
+ ".js",
539
+ ".jsx",
540
+ ".mjs",
541
+ ".cjs"
542
+ ],
543
+ "import/external-module-folders": [
544
+ "node_modules",
545
+ "node_modules/@types"
546
+ ],
547
+ "import/parsers": {
548
+ "@typescript-eslint/parser": [
549
+ ".ts",
550
+ ".cts",
551
+ ".mts",
552
+ ".tsx"
553
+ ]
554
+ },
555
+ "import/resolver": {
556
+ "node": {
557
+ "extensions": [
558
+ ".ts",
559
+ ".cts",
560
+ ".mts",
561
+ ".tsx",
562
+ ".js",
563
+ ".jsx",
564
+ ".mjs",
565
+ ".cjs"
566
+ ]
567
+ }
568
+ }
569
+ }
570
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.1.0-alpha.1",
3
+ "version": "1.1.0-alpha.2",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,6 +36,7 @@
36
36
  "files": [
37
37
  "dist",
38
38
  "oxlint",
39
+ "plugins",
39
40
  "!dist/**/*.map"
40
41
  ],
41
42
  "dependencies": {
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Local oxlint plugin: Hublo house rules that have no equivalent anywhere else.
3
+ *
4
+ * oxlint has no `no-restricted-syntax`, so the eight AST selectors that used to live in
5
+ * `restricted-syntax.cjs` cannot be carried across as configuration. They are expressed
6
+ * here as a real rule instead, which is what oxlint's JS plugin API is for.
7
+ *
8
+ * It ships INSIDE sentinel rather than being copied into each module. A house rule is code,
9
+ * not configuration: a per-module copy drifts, and drifting house rules are the problem
10
+ * sentinel exists to remove. The cost is that `@hublo/sentinel` carries a rule about Hublo's
11
+ * colour palette, which is honest for a package named after the company.
12
+ *
13
+ * The original selectors, and where each is handled below:
14
+ * 1-2 MemberExpression[object.property.name='palette'][property.name='common'] theme.palette.common
15
+ * …[computed=true][property.value='common'] theme.palette['common']
16
+ * 3-4 MemberExpression[object.name='palette']… palette.common
17
+ * 5-6 Property[key.name|key.value=/<colour props>/] > Literal[value=/^common\./] { color: 'common.white' }
18
+ * 7-8 JSXAttribute[name.name=/<colour props>/] > Literal | JSXExpressionContainer color="common.white"
19
+ */
20
+
21
+ const MESSAGE =
22
+ 'Do not use palette.common or common.* color paths in product code. Use semantic palette tokens instead.'
23
+
24
+ /** The MUI props that take a colour path, so `common.*` in them is the same mistake. */
25
+ const COLOUR_PROPS =
26
+ /^(color|bgcolor|background|backgroundColor|borderColor|borderTopColor|borderRightColor|borderBottomColor|borderLeftColor|outlineColor|fill|stroke|textDecorationColor|caretColor|accentColor)$/
27
+
28
+ /** `common.something` as a string value, e.g. `{ color: 'common.white' }`. */
29
+ const isCommonPath = (node) =>
30
+ node?.type === 'Literal' && typeof node.value === 'string' && /^common\./.test(node.value)
31
+
32
+ const plugin = {
33
+ meta: { name: 'hublo' },
34
+ rules: {
35
+ 'no-palette-common': {
36
+ meta: {
37
+ docs: {
38
+ description: 'Ban palette.common and common.* colour paths in product code.',
39
+ },
40
+ },
41
+ create(context) {
42
+ const report = (node) => context.report({ node, message: MESSAGE })
43
+
44
+ return {
45
+ // 1-4: reaching `common` off a palette, by property or by computed key.
46
+ MemberExpression(node) {
47
+ const onPalette =
48
+ node.object?.property?.name === 'palette' || node.object?.name === 'palette'
49
+ if (!onPalette) {
50
+ return
51
+ }
52
+ const reachesCommon =
53
+ node.property?.name === 'common' ||
54
+ (node.computed && node.property?.value === 'common')
55
+ if (reachesCommon) {
56
+ report(node)
57
+ }
58
+ },
59
+
60
+ // 5-6: `{ color: 'common.white' }` in an object literal, key written either way.
61
+ Property(node) {
62
+ const key = node.key?.name ?? node.key?.value
63
+ if (typeof key === 'string' && COLOUR_PROPS.test(key) && isCommonPath(node.value)) {
64
+ report(node.value)
65
+ }
66
+ },
67
+
68
+ // 7-8: `color="common.white"` and `color={'common.white'}` on a JSX element.
69
+ JSXAttribute(node) {
70
+ const name = node.name?.name
71
+ if (typeof name !== 'string' || !COLOUR_PROPS.test(name)) {
72
+ return
73
+ }
74
+ const value = node.value
75
+ if (isCommonPath(value)) {
76
+ return report(value)
77
+ }
78
+ if (value?.type === 'JSXExpressionContainer' && isCommonPath(value.expression)) {
79
+ report(value.expression)
80
+ }
81
+ },
82
+ }
83
+ },
84
+ },
85
+ },
86
+ }
87
+
88
+ export default plugin