@hublo/sentinel 1.5.0-alpha.9 → 1.5.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 +9 -7
- package/dist/bin/sentinel.js +61 -16
- package/dist/{chunk-KLEXJFJ6.js → chunk-43HQ6BVJ.js} +61 -15
- package/dist/{chunk-YQRDKWSH.js → chunk-IL5PPLXE.js} +11 -1
- package/dist/chunk-IL5PPLXE.js.map +1 -0
- package/dist/chunk-MOELAQPC.js +10365 -0
- package/dist/chunk-WBDBRGLV.js +214 -0
- package/dist/chunk-WBDBRGLV.js.map +1 -0
- package/dist/{chunk-AMCGMLPS.js → chunk-Y57KA4QS.js} +2 -2
- package/dist/index.d.ts +28 -1
- package/dist/index.js +4 -4
- package/dist/roles/build/nest/toolchain.js +68 -18
- package/dist/roles/build/nest/toolchain.js.map +1 -1
- package/dist/roles/static-analysis/config.d.ts +15 -1
- package/dist/roles/static-analysis/config.js +1 -1
- package/dist/roles/test/setup/jest-parity.js +2 -2
- package/dist/roles/test/setup/workspace-entry.js +2 -2
- package/dist/{validate-AWWMBSH5.js → validate-FRMENKHM.js} +2 -2
- package/docs/bundle-analysis-spec.md +272 -0
- package/docs/sql-verification-study.md +401 -0
- package/docs/static-analysis-adoption.md +285 -0
- package/docs/static-analysis-study.md +824 -18
- package/docs/typescript-adoption.md +1 -1
- package/docs/typescript-traces.md +27 -12
- package/docs/using-sentinel.md +22 -12
- package/docs/validating-a-change.md +185 -0
- package/lint/nest.json +98 -18
- package/lint/node.json +83 -3
- package/lint/react-lib.json +121 -31
- package/lint/react.json +101 -2
- package/package.json +27 -6
- package/plugins/hublo/no-palette-common.js +76 -0
- package/plugins/hublo/no-regex-lookbehind.js +70 -0
- package/plugins/hublo/require-reload-document.js +70 -0
- package/plugins/hublo.js +20 -145
- package/plugins/sonar-cognitive.js +108 -0
- package/dist/chunk-EDFIVLUA.js +0 -100
- package/dist/chunk-EDFIVLUA.js.map +0 -1
- package/dist/chunk-OZZICSSZ.js +0 -6469
- package/dist/chunk-YQRDKWSH.js.map +0 -1
- /package/dist/{chunk-AMCGMLPS.js.map → chunk-Y57KA4QS.js.map} +0 -0
package/lint/react-lib.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"react",
|
|
6
6
|
"jsx-a11y",
|
|
7
7
|
"jest",
|
|
8
|
+
"vitest",
|
|
8
9
|
"unicorn",
|
|
9
10
|
"oxc",
|
|
10
11
|
"nextjs"
|
|
@@ -53,6 +54,14 @@
|
|
|
53
54
|
"typescript/prefer-as-const": "error",
|
|
54
55
|
"typescript/prefer-namespace-keyword": "error",
|
|
55
56
|
"typescript/triple-slash-reference": "error",
|
|
57
|
+
"@tanstack/query/exhaustive-deps": "warn",
|
|
58
|
+
"@tanstack/query/infinite-query-property-order": "warn",
|
|
59
|
+
"@tanstack/query/mutation-property-order": "warn",
|
|
60
|
+
"@tanstack/query/no-rest-destructuring": "warn",
|
|
61
|
+
"@tanstack/query/no-unstable-deps": "warn",
|
|
62
|
+
"@tanstack/query/no-void-query-fn": "warn",
|
|
63
|
+
"@tanstack/query/prefer-query-options": "warn",
|
|
64
|
+
"@tanstack/query/stable-query-client": "warn",
|
|
56
65
|
"curly": [
|
|
57
66
|
"error",
|
|
58
67
|
"all"
|
|
@@ -70,39 +79,28 @@
|
|
|
70
79
|
"jest-dom/prefer-to-have-style": "error",
|
|
71
80
|
"jest-dom/prefer-to-have-text-content": "error",
|
|
72
81
|
"jest-dom/prefer-to-have-value": "error",
|
|
73
|
-
"jest/expect-expect":
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"expect",
|
|
78
|
-
"expect*",
|
|
79
|
-
"waitForElementToBeRemoved",
|
|
80
|
-
"screen.findBy*",
|
|
81
|
-
"result.findBy*"
|
|
82
|
-
]
|
|
83
|
-
}
|
|
84
|
-
],
|
|
85
|
-
"jest/no-alias-methods": "error",
|
|
86
|
-
"jest/no-commented-out-tests": "warn",
|
|
87
|
-
"jest/no-conditional-expect": "error",
|
|
82
|
+
"jest/expect-expect": "off",
|
|
83
|
+
"jest/no-alias-methods": "off",
|
|
84
|
+
"jest/no-commented-out-tests": "off",
|
|
85
|
+
"jest/no-conditional-expect": "off",
|
|
88
86
|
"jest/no-deprecated-functions": "error",
|
|
89
|
-
"jest/no-disabled-tests": "
|
|
87
|
+
"jest/no-disabled-tests": "off",
|
|
90
88
|
"jest/no-done-callback": "error",
|
|
91
89
|
"jest/no-export": "error",
|
|
92
|
-
"jest/no-focused-tests": "
|
|
93
|
-
"jest/no-identical-title": "
|
|
94
|
-
"jest/no-interpolation-in-snapshots": "
|
|
90
|
+
"jest/no-focused-tests": "off",
|
|
91
|
+
"jest/no-identical-title": "off",
|
|
92
|
+
"jest/no-interpolation-in-snapshots": "off",
|
|
95
93
|
"jest/no-jasmine-globals": "error",
|
|
96
|
-
"jest/no-mocks-import": "
|
|
97
|
-
"jest/no-standalone-expect": "
|
|
98
|
-
"jest/no-test-prefixes": "
|
|
99
|
-
"jest/prefer-to-be": "
|
|
100
|
-
"jest/prefer-to-contain": "
|
|
101
|
-
"jest/prefer-to-have-length": "
|
|
102
|
-
"jest/valid-describe-callback": "
|
|
103
|
-
"jest/valid-expect": "
|
|
104
|
-
"jest/valid-expect-in-promise": "
|
|
105
|
-
"jest/valid-title": "
|
|
94
|
+
"jest/no-mocks-import": "off",
|
|
95
|
+
"jest/no-standalone-expect": "off",
|
|
96
|
+
"jest/no-test-prefixes": "off",
|
|
97
|
+
"jest/prefer-to-be": "off",
|
|
98
|
+
"jest/prefer-to-contain": "off",
|
|
99
|
+
"jest/prefer-to-have-length": "off",
|
|
100
|
+
"jest/valid-describe-callback": "off",
|
|
101
|
+
"jest/valid-expect": "off",
|
|
102
|
+
"jest/valid-expect-in-promise": "off",
|
|
103
|
+
"jest/valid-title": "off",
|
|
106
104
|
"jsx-a11y/anchor-is-valid": [
|
|
107
105
|
"error",
|
|
108
106
|
{
|
|
@@ -334,7 +332,39 @@
|
|
|
334
332
|
{
|
|
335
333
|
"requireStringLiterals": false
|
|
336
334
|
}
|
|
337
|
-
]
|
|
335
|
+
],
|
|
336
|
+
"vitest/expect-expect": [
|
|
337
|
+
"error",
|
|
338
|
+
{
|
|
339
|
+
"assertFunctionNames": [
|
|
340
|
+
"expect",
|
|
341
|
+
"expect*",
|
|
342
|
+
"waitForElementToBeRemoved",
|
|
343
|
+
"screen.findBy*",
|
|
344
|
+
"result.findBy*"
|
|
345
|
+
]
|
|
346
|
+
}
|
|
347
|
+
],
|
|
348
|
+
"vitest/no-alias-methods": "error",
|
|
349
|
+
"vitest/no-commented-out-tests": "warn",
|
|
350
|
+
"vitest/no-conditional-expect": "error",
|
|
351
|
+
"vitest/no-disabled-tests": "warn",
|
|
352
|
+
"vitest/no-focused-tests": "error",
|
|
353
|
+
"vitest/no-identical-title": "error",
|
|
354
|
+
"vitest/no-interpolation-in-snapshots": "error",
|
|
355
|
+
"vitest/no-mocks-import": "error",
|
|
356
|
+
"vitest/no-standalone-expect": "error",
|
|
357
|
+
"vitest/no-test-prefixes": "error",
|
|
358
|
+
"vitest/prefer-to-be": "error",
|
|
359
|
+
"vitest/prefer-to-contain": "error",
|
|
360
|
+
"vitest/prefer-to-have-length": "error",
|
|
361
|
+
"vitest/require-mock-type-parameters": "off",
|
|
362
|
+
"vitest/require-to-throw-message": "warn",
|
|
363
|
+
"vitest/valid-describe-callback": "error",
|
|
364
|
+
"vitest/valid-expect": "error",
|
|
365
|
+
"vitest/valid-expect-in-promise": "error",
|
|
366
|
+
"vitest/valid-title": "error",
|
|
367
|
+
"vitest/warn-todo": "warn"
|
|
338
368
|
},
|
|
339
369
|
"jsPlugins": [
|
|
340
370
|
"eslint-plugin-no-barrel-files",
|
|
@@ -350,7 +380,11 @@
|
|
|
350
380
|
"name": "storybook",
|
|
351
381
|
"specifier": "eslint-plugin-storybook"
|
|
352
382
|
},
|
|
353
|
-
"../plugins/hublo.js"
|
|
383
|
+
"../plugins/hublo.js",
|
|
384
|
+
{
|
|
385
|
+
"name": "@tanstack/query",
|
|
386
|
+
"specifier": "@tanstack/eslint-plugin-query"
|
|
387
|
+
}
|
|
354
388
|
],
|
|
355
389
|
"overrides": [
|
|
356
390
|
{
|
|
@@ -373,6 +407,62 @@
|
|
|
373
407
|
"storybook/use-storybook-testing-library": "error",
|
|
374
408
|
"react-hooks/rules-of-hooks": "off"
|
|
375
409
|
}
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"files": [
|
|
413
|
+
"**/*.test.ts",
|
|
414
|
+
"**/*.test.tsx",
|
|
415
|
+
"**/*.spec.ts",
|
|
416
|
+
"**/*.spec.tsx",
|
|
417
|
+
"**/*.integration-spec.ts",
|
|
418
|
+
"**/*.e2e-spec.ts"
|
|
419
|
+
],
|
|
420
|
+
"rules": {
|
|
421
|
+
"no-restricted-properties": [
|
|
422
|
+
"warn",
|
|
423
|
+
{
|
|
424
|
+
"object": "Math",
|
|
425
|
+
"property": "random",
|
|
426
|
+
"message": "No Math.random() in a test: use a deterministic fixture, or stub the random source."
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
"object": "Date",
|
|
430
|
+
"property": "now",
|
|
431
|
+
"message": "No Date.now() in a test: use vi.useFakeTimers() with vi.setSystemTime()."
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
"object": "performance",
|
|
435
|
+
"property": "now",
|
|
436
|
+
"message": "No performance.now() in a test: use vi.useFakeTimers() with vi.setSystemTime()."
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
"object": "process",
|
|
440
|
+
"property": "hrtime",
|
|
441
|
+
"message": "No process.hrtime in a test: use vi.useFakeTimers() with vi.setSystemTime()."
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"object": "crypto",
|
|
445
|
+
"property": "randomUUID",
|
|
446
|
+
"message": "No crypto.randomUUID() in a test: use a fixed id, or stub the random source."
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
"object": "crypto",
|
|
450
|
+
"property": "getRandomValues",
|
|
451
|
+
"message": "No crypto.getRandomValues() in a test: use a fixed buffer, or stub the random source."
|
|
452
|
+
}
|
|
453
|
+
],
|
|
454
|
+
"no-restricted-globals": [
|
|
455
|
+
"warn",
|
|
456
|
+
{
|
|
457
|
+
"name": "setTimeout",
|
|
458
|
+
"message": "No real timer in a test: vi.useFakeTimers() then vi.advanceTimersByTime()."
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
"name": "setInterval",
|
|
462
|
+
"message": "No real timer in a test: vi.useFakeTimers() then vi.advanceTimersByTime()."
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
}
|
|
376
466
|
}
|
|
377
467
|
]
|
|
378
468
|
}
|
package/lint/react.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"react",
|
|
6
6
|
"jsx-a11y",
|
|
7
7
|
"jest",
|
|
8
|
+
"vitest",
|
|
8
9
|
"import",
|
|
9
10
|
"unicorn",
|
|
10
11
|
"oxc"
|
|
@@ -53,6 +54,16 @@
|
|
|
53
54
|
"typescript/prefer-as-const": "error",
|
|
54
55
|
"typescript/prefer-namespace-keyword": "error",
|
|
55
56
|
"typescript/triple-slash-reference": "error",
|
|
57
|
+
"@tanstack/query/exhaustive-deps": "warn",
|
|
58
|
+
"@tanstack/query/infinite-query-property-order": "warn",
|
|
59
|
+
"@tanstack/query/mutation-property-order": "warn",
|
|
60
|
+
"@tanstack/query/no-rest-destructuring": "warn",
|
|
61
|
+
"@tanstack/query/no-unstable-deps": "warn",
|
|
62
|
+
"@tanstack/query/no-void-query-fn": "warn",
|
|
63
|
+
"@tanstack/query/prefer-query-options": "warn",
|
|
64
|
+
"@tanstack/query/stable-query-client": "warn",
|
|
65
|
+
"@tanstack/router/create-route-property-order": "warn",
|
|
66
|
+
"@tanstack/router/route-param-names": "warn",
|
|
56
67
|
"curly": [
|
|
57
68
|
"error",
|
|
58
69
|
"all"
|
|
@@ -60,6 +71,7 @@
|
|
|
60
71
|
"for-direction": "error",
|
|
61
72
|
"hublo/no-palette-common": "off",
|
|
62
73
|
"hublo/no-regex-lookbehind": "error",
|
|
74
|
+
"hublo/require-reload-document": "error",
|
|
63
75
|
"jest/expect-expect": "off",
|
|
64
76
|
"jest/no-alias-methods": "off",
|
|
65
77
|
"jest/no-commented-out-tests": "off",
|
|
@@ -456,7 +468,28 @@
|
|
|
456
468
|
{
|
|
457
469
|
"requireStringLiterals": false
|
|
458
470
|
}
|
|
459
|
-
]
|
|
471
|
+
],
|
|
472
|
+
"vitest/expect-expect": "warn",
|
|
473
|
+
"vitest/no-alias-methods": "warn",
|
|
474
|
+
"vitest/no-commented-out-tests": "warn",
|
|
475
|
+
"vitest/no-conditional-expect": "warn",
|
|
476
|
+
"vitest/no-disabled-tests": "warn",
|
|
477
|
+
"vitest/no-focused-tests": "warn",
|
|
478
|
+
"vitest/no-identical-title": "warn",
|
|
479
|
+
"vitest/no-interpolation-in-snapshots": "warn",
|
|
480
|
+
"vitest/no-mocks-import": "warn",
|
|
481
|
+
"vitest/no-standalone-expect": "warn",
|
|
482
|
+
"vitest/no-test-prefixes": "warn",
|
|
483
|
+
"vitest/prefer-to-be": "warn",
|
|
484
|
+
"vitest/prefer-to-contain": "warn",
|
|
485
|
+
"vitest/prefer-to-have-length": "warn",
|
|
486
|
+
"vitest/require-mock-type-parameters": "off",
|
|
487
|
+
"vitest/require-to-throw-message": "warn",
|
|
488
|
+
"vitest/valid-describe-callback": "warn",
|
|
489
|
+
"vitest/valid-expect": "warn",
|
|
490
|
+
"vitest/valid-expect-in-promise": "warn",
|
|
491
|
+
"vitest/valid-title": "warn",
|
|
492
|
+
"vitest/warn-todo": "warn"
|
|
460
493
|
},
|
|
461
494
|
"jsPlugins": [
|
|
462
495
|
"eslint-plugin-no-barrel-files",
|
|
@@ -464,6 +497,10 @@
|
|
|
464
497
|
"name": "nx",
|
|
465
498
|
"specifier": "@nx/eslint-plugin"
|
|
466
499
|
},
|
|
500
|
+
{
|
|
501
|
+
"name": "@tanstack/query",
|
|
502
|
+
"specifier": "@tanstack/eslint-plugin-query"
|
|
503
|
+
},
|
|
467
504
|
{
|
|
468
505
|
"name": "styled-components-a11y",
|
|
469
506
|
"specifier": "eslint-plugin-styled-components-a11y"
|
|
@@ -472,6 +509,68 @@
|
|
|
472
509
|
"name": "react-refresh",
|
|
473
510
|
"specifier": "eslint-plugin-react-refresh"
|
|
474
511
|
},
|
|
475
|
-
"../plugins/hublo.js"
|
|
512
|
+
"../plugins/hublo.js",
|
|
513
|
+
{
|
|
514
|
+
"name": "@tanstack/router",
|
|
515
|
+
"specifier": "@tanstack/eslint-plugin-router"
|
|
516
|
+
}
|
|
517
|
+
],
|
|
518
|
+
"overrides": [
|
|
519
|
+
{
|
|
520
|
+
"files": [
|
|
521
|
+
"**/*.test.ts",
|
|
522
|
+
"**/*.test.tsx",
|
|
523
|
+
"**/*.spec.ts",
|
|
524
|
+
"**/*.spec.tsx",
|
|
525
|
+
"**/*.integration-spec.ts",
|
|
526
|
+
"**/*.e2e-spec.ts"
|
|
527
|
+
],
|
|
528
|
+
"rules": {
|
|
529
|
+
"no-restricted-properties": [
|
|
530
|
+
"warn",
|
|
531
|
+
{
|
|
532
|
+
"object": "Math",
|
|
533
|
+
"property": "random",
|
|
534
|
+
"message": "No Math.random() in a test: use a deterministic fixture, or stub the random source."
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
"object": "Date",
|
|
538
|
+
"property": "now",
|
|
539
|
+
"message": "No Date.now() in a test: use vi.useFakeTimers() with vi.setSystemTime()."
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
"object": "performance",
|
|
543
|
+
"property": "now",
|
|
544
|
+
"message": "No performance.now() in a test: use vi.useFakeTimers() with vi.setSystemTime()."
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
"object": "process",
|
|
548
|
+
"property": "hrtime",
|
|
549
|
+
"message": "No process.hrtime in a test: use vi.useFakeTimers() with vi.setSystemTime()."
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
"object": "crypto",
|
|
553
|
+
"property": "randomUUID",
|
|
554
|
+
"message": "No crypto.randomUUID() in a test: use a fixed id, or stub the random source."
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
"object": "crypto",
|
|
558
|
+
"property": "getRandomValues",
|
|
559
|
+
"message": "No crypto.getRandomValues() in a test: use a fixed buffer, or stub the random source."
|
|
560
|
+
}
|
|
561
|
+
],
|
|
562
|
+
"no-restricted-globals": [
|
|
563
|
+
"warn",
|
|
564
|
+
{
|
|
565
|
+
"name": "setTimeout",
|
|
566
|
+
"message": "No real timer in a test: vi.useFakeTimers() then vi.advanceTimersByTime()."
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
"name": "setInterval",
|
|
570
|
+
"message": "No real timer in a test: vi.useFakeTimers() then vi.advanceTimersByTime()."
|
|
571
|
+
}
|
|
572
|
+
]
|
|
573
|
+
}
|
|
574
|
+
}
|
|
476
575
|
]
|
|
477
576
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.5.0
|
|
3
|
+
"version": "1.5.0",
|
|
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",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"url": "git+https://github.com/hublo/sentinel.git"
|
|
11
11
|
},
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
13
|
+
"node": ">=24.15.0"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
16
|
"sentinel": "./dist/bin/sentinel.js"
|
|
@@ -110,6 +110,8 @@
|
|
|
110
110
|
"dependencies": {
|
|
111
111
|
"@nx/eslint-plugin": "23.0.1",
|
|
112
112
|
"@tailwindcss/vite": "4.1.18",
|
|
113
|
+
"@tanstack/eslint-plugin-query": "5.104.0",
|
|
114
|
+
"@tanstack/eslint-plugin-router": "1.162.0",
|
|
113
115
|
"@vitejs/plugin-react": "6.0.1",
|
|
114
116
|
"@vitest/coverage-v8": "4.1.4",
|
|
115
117
|
"axe-core": "^4.13.0",
|
|
@@ -118,8 +120,17 @@
|
|
|
118
120
|
"eslint-plugin-jest-dom": "5.5.0",
|
|
119
121
|
"eslint-plugin-no-barrel-files": "1.2.1",
|
|
120
122
|
"eslint-plugin-react-refresh": "0.4.19",
|
|
123
|
+
"eslint-plugin-sonarjs": "4.2.2",
|
|
121
124
|
"eslint-plugin-storybook": "10.3.5",
|
|
122
125
|
"eslint-plugin-styled-components-a11y": "2.2.0",
|
|
126
|
+
"graphology": "0.26.0",
|
|
127
|
+
"graphology-communities-louvain": "2.0.2",
|
|
128
|
+
"graphology-components": "1.5.4",
|
|
129
|
+
"graphology-layout": "0.6.1",
|
|
130
|
+
"graphology-layout-forceatlas2": "0.10.1",
|
|
131
|
+
"graphology-metrics": "2.4.2",
|
|
132
|
+
"graphology-svg": "0.1.3",
|
|
133
|
+
"graphology-types": "0.24.8",
|
|
123
134
|
"html-validate": "^11.16.0",
|
|
124
135
|
"jscpd": "5.4.0",
|
|
125
136
|
"jsdom": "^24.1.3",
|
|
@@ -139,11 +150,10 @@
|
|
|
139
150
|
},
|
|
140
151
|
"devDependencies": {
|
|
141
152
|
"@hublo/sentinel": "link:.",
|
|
142
|
-
"@types/node": "^
|
|
153
|
+
"@types/node": "^24.19.2",
|
|
143
154
|
"nx": "^23.1.0",
|
|
144
155
|
"tsup": "^8.3.0",
|
|
145
|
-
"tsx": "^4.19.0"
|
|
146
|
-
"typescript": "^5.9.0"
|
|
156
|
+
"tsx": "^4.19.0"
|
|
147
157
|
},
|
|
148
158
|
"publishConfig": {
|
|
149
159
|
"registry": "https://registry.npmjs.org",
|
|
@@ -160,6 +170,13 @@
|
|
|
160
170
|
},
|
|
161
171
|
"format:fix": {
|
|
162
172
|
"cache": false
|
|
173
|
+
},
|
|
174
|
+
"static-analysis": {
|
|
175
|
+
"cache": true,
|
|
176
|
+
"inputs": [
|
|
177
|
+
"default",
|
|
178
|
+
"{projectRoot}/static-analysis.config.mjs"
|
|
179
|
+
]
|
|
163
180
|
}
|
|
164
181
|
}
|
|
165
182
|
},
|
|
@@ -190,6 +207,10 @@
|
|
|
190
207
|
"docs:traces": "pnpm build && bash scripts/gen-traces.sh",
|
|
191
208
|
"docs:traces:check": "pnpm build && bash scripts/gen-traces.sh --check",
|
|
192
209
|
"format:fix": "node dist/bin/sentinel.js --run --format --fix",
|
|
193
|
-
"bench": "node scripts/bench.mjs"
|
|
210
|
+
"bench": "node scripts/bench.mjs",
|
|
211
|
+
"deps:trust": "tsx scripts/check-trust-policy.ts",
|
|
212
|
+
"static-analysis": "sentinel --run --static-analysis",
|
|
213
|
+
"static-analysis:json": "sentinel --run --static-analysis --output json",
|
|
214
|
+
"static-analysis:html": "sentinel --run --static-analysis --output html"
|
|
194
215
|
}
|
|
195
216
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `palette.common` and `common.*` colour paths in product code.
|
|
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 as a real
|
|
6
|
+
* rule instead, which is what oxlint's JS plugin API is for. The originals, and where each is
|
|
7
|
+
* handled below:
|
|
8
|
+
*
|
|
9
|
+
* 1-2 MemberExpression[object.property.name='palette'][property.name='common'] theme.palette.common
|
|
10
|
+
* …[computed=true][property.value='common'] theme.palette['common']
|
|
11
|
+
* 3-4 MemberExpression[object.name='palette']… palette.common
|
|
12
|
+
* 5-6 Property[key.name|key.value=/<colour props>/] > Literal[value=/^common\./] { color: 'common.white' }
|
|
13
|
+
* 7-8 JSXAttribute[name.name=/<colour props>/] > Literal | JSXExpressionContainer color="common.white"
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const MESSAGE =
|
|
17
|
+
'Do not use palette.common or common.* color paths in product code. Use semantic palette tokens instead.'
|
|
18
|
+
|
|
19
|
+
/** The MUI props that take a colour path, so `common.*` in them is the same mistake. */
|
|
20
|
+
const COLOUR_PROPS =
|
|
21
|
+
/^(color|bgcolor|background|backgroundColor|borderColor|borderTopColor|borderRightColor|borderBottomColor|borderLeftColor|outlineColor|fill|stroke|textDecorationColor|caretColor|accentColor)$/
|
|
22
|
+
|
|
23
|
+
/** `common.something` as a string value, e.g. `{ color: 'common.white' }`. */
|
|
24
|
+
const isCommonPath = (node) =>
|
|
25
|
+
node?.type === 'Literal' && typeof node.value === 'string' && /^common\./.test(node.value)
|
|
26
|
+
|
|
27
|
+
/** @type {import("oxlint").Rule} */
|
|
28
|
+
export const rule = {
|
|
29
|
+
meta: {
|
|
30
|
+
docs: {
|
|
31
|
+
description: 'Ban palette.common and common.* colour paths in product code.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
create(context) {
|
|
35
|
+
const report = (node) => context.report({ node, message: MESSAGE })
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
// 1-4: reaching `common` off a palette, by property or by computed key.
|
|
39
|
+
MemberExpression(node) {
|
|
40
|
+
const onPalette =
|
|
41
|
+
node.object?.property?.name === 'palette' || node.object?.name === 'palette'
|
|
42
|
+
if (!onPalette) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
const reachesCommon =
|
|
46
|
+
node.property?.name === 'common' || (node.computed && node.property?.value === 'common')
|
|
47
|
+
if (reachesCommon) {
|
|
48
|
+
report(node)
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// 5-6: `{ color: 'common.white' }` in an object literal, key written either way.
|
|
53
|
+
Property(node) {
|
|
54
|
+
const key = node.key?.name ?? node.key?.value
|
|
55
|
+
if (typeof key === 'string' && COLOUR_PROPS.test(key) && isCommonPath(node.value)) {
|
|
56
|
+
report(node.value)
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// 7-8: `color="common.white"` and `color={'common.white'}` on a JSX element.
|
|
61
|
+
JSXAttribute(node) {
|
|
62
|
+
const name = node.name?.name
|
|
63
|
+
if (typeof name !== 'string' || !COLOUR_PROPS.test(name)) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const value = node.value
|
|
67
|
+
if (isCommonPath(value)) {
|
|
68
|
+
return report(value)
|
|
69
|
+
}
|
|
70
|
+
if (value?.type === 'JSXExpressionContainer' && isCommonPath(value.expression)) {
|
|
71
|
+
report(value.expression)
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban regex lookbehind, which Safari below 16.4 cannot parse.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ Not a style rule and not a carried-over selector: a browser-support rule. Unlike everything
|
|
5
|
+
* else a preset controls, lookbehind can be neither transpiled nor polyfilled, because no bundler
|
|
6
|
+
* rewrites the contents of a regex literal. Lint is the only layer left that can catch it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Lookbehind, positive or negative. NOT named groups `(?<name>)`, which Safari has had since
|
|
11
|
+
* 11.3: the class is exactly `(?<` followed by `=` or `!`.
|
|
12
|
+
*/
|
|
13
|
+
const LOOKBEHIND = /\(\?<[=!]/
|
|
14
|
+
|
|
15
|
+
const LOOKBEHIND_MESSAGE =
|
|
16
|
+
'Regex lookbehind ((?<=) / (?<!)) is Safari 16.4+ and can be neither transpiled nor polyfilled: on older iOS the literal is a parse-time SyntaxError that takes the whole chunk with it. Rewrite the pattern. Named groups (?<name>) are fine.'
|
|
17
|
+
|
|
18
|
+
/** @type {import("oxlint").Rule} */
|
|
19
|
+
export const rule = {
|
|
20
|
+
meta: {
|
|
21
|
+
docs: {
|
|
22
|
+
description: 'Ban regex lookbehind, unsupported by Safari below 16.4.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
26
|
+
const report = (node) => context.report({ node, message: LOOKBEHIND_MESSAGE })
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `new RegExp(...)` and `RegExp(...)`, when the pattern is readable statically: a
|
|
30
|
+
* string, or a template whose fixed parts carry the lookbehind.
|
|
31
|
+
*
|
|
32
|
+
* Each template chunk is tested on its own rather than joined. Joining invents a
|
|
33
|
+
* `(?<=` that the runtime string never has, across an interpolation boundary:
|
|
34
|
+
* `` `(?<${op}=x)` `` would read as one.
|
|
35
|
+
*/
|
|
36
|
+
const isStaticLookbehind = (pattern) => {
|
|
37
|
+
if (pattern?.type === 'Literal') {
|
|
38
|
+
return typeof pattern.value === 'string' && LOOKBEHIND.test(pattern.value)
|
|
39
|
+
}
|
|
40
|
+
if (pattern?.type === 'TemplateLiteral') {
|
|
41
|
+
return (pattern.quasis ?? []).some((quasi) =>
|
|
42
|
+
LOOKBEHIND.test(quasi.value?.cooked ?? quasi.value?.raw ?? ''),
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const checkRegExpCall = (node) => {
|
|
49
|
+
if (node.callee?.name !== 'RegExp') {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
const pattern = node.arguments?.[0]
|
|
53
|
+
if (isStaticLookbehind(pattern)) {
|
|
54
|
+
report(pattern)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
// The case that actually breaks a page: a literal is an EARLY error, so the whole
|
|
60
|
+
// chunk fails to parse rather than the one call site failing.
|
|
61
|
+
Literal(node) {
|
|
62
|
+
if (node.regex && LOOKBEHIND.test(node.regex.pattern)) {
|
|
63
|
+
report(node)
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
NewExpression: checkRegExpCall,
|
|
67
|
+
CallExpression: checkRegExpCall,
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban a TanStack Router `redirect({ href })` that does not say whether it reloads the document.
|
|
3
|
+
*
|
|
4
|
+
* Carried from an ESLint selector in monorepo #22043, for the same reason as its neighbours: a
|
|
5
|
+
* house rule is code, not configuration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** @type {import("oxlint").Rule} */
|
|
9
|
+
export const rule = {
|
|
10
|
+
meta: {
|
|
11
|
+
docs: {
|
|
12
|
+
description:
|
|
13
|
+
'Ban a TanStack Router redirect({ href }) that does not say whether it reloads the document.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
/**
|
|
18
|
+
* A `redirect({ href })` that declares no `reloadDocument`.
|
|
19
|
+
*
|
|
20
|
+
* ⛔ The failure this exists for ships GREEN and 404s only in production. Since the
|
|
21
|
+
* router-core bump, `redirect({ href })` no longer reloads the document for an absolute
|
|
22
|
+
* URL on the app's own origin: router-core turns it into an in-app navigation. Apps under
|
|
23
|
+
* a basepath share their origin with the app they point at, so the redirect lands on the
|
|
24
|
+
* SOURCE app's 404 instead of loading the target. Tests run on jsdom's localhost, where
|
|
25
|
+
* the same href is cross-origin and does reload, so nothing catches it before production.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ What this rule enforces is "declare the intent", NOT "do not 404". A call that says
|
|
28
|
+
* `reloadDocument: false` passes, and that is the exact in-app navigation which caused the
|
|
29
|
+
* incident; the rule cannot tell an internal path from a cross-app one, because the href is
|
|
30
|
+
* usually computed. It shrinks the target from any `redirect({ href })` to one that opted
|
|
31
|
+
* out explicitly, and that is all it does.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ An `href` arriving through a spread is invisible to it. Measured when the rule was
|
|
34
|
+
* written: zero occurrences of `redirect({ ...opts })` in the whole front end.
|
|
35
|
+
*
|
|
36
|
+
* It is application-agnostic on purpose: every app on TanStack Router under a basepath has
|
|
37
|
+
* the same exposure, so the message names no application.
|
|
38
|
+
*/
|
|
39
|
+
const SPREAD_SAFE_MESSAGE =
|
|
40
|
+
'redirect({ href }) must say whether it reloads the document. Under a basepath the app ' +
|
|
41
|
+
'shares its origin with the app you are pointing at, so the router turns a same-origin ' +
|
|
42
|
+
'href into an in-app navigation and the redirect lands on this app’s 404. Use a ' +
|
|
43
|
+
'helper that sets reloadDocument: true to leave for another app, or set ' +
|
|
44
|
+
'reloadDocument: false to say you mean an in-app navigation.'
|
|
45
|
+
|
|
46
|
+
/** The call is `redirect(...)`, written bare or as a member. */
|
|
47
|
+
const isRedirect = (node) =>
|
|
48
|
+
node.callee?.name === 'redirect' || node.callee?.property?.name === 'redirect'
|
|
49
|
+
|
|
50
|
+
/** A plain `key:` property, which a spread element does not have. */
|
|
51
|
+
const named = (property, name) =>
|
|
52
|
+
property?.type === 'Property' && (property.key?.name === name || property.key?.value === name)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
CallExpression(node) {
|
|
56
|
+
if (!isRedirect(node)) return
|
|
57
|
+
const options = node.arguments?.[0]
|
|
58
|
+
if (options?.type !== 'ObjectExpression') return
|
|
59
|
+
|
|
60
|
+
const properties = options.properties ?? []
|
|
61
|
+
const hasHref = properties.some((property) => named(property, 'href'))
|
|
62
|
+
if (!hasHref) return
|
|
63
|
+
const declaresReload = properties.some((property) => named(property, 'reloadDocument'))
|
|
64
|
+
if (declaresReload) return
|
|
65
|
+
|
|
66
|
+
context.report({ node: options, message: SPREAD_SAFE_MESSAGE })
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
}
|