@hublo/sentinel 1.2.0-alpha.21 → 1.2.0-alpha.22
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/dist/bin/sentinel.js +1 -1
- package/dist/{chunk-RKVGZNJ7.js → chunk-YBJLWNTQ.js} +34 -16
- package/dist/index.js +1 -1
- package/lint/react-lib.json +3 -1
- package/lint/react.json +4 -1
- package/package.json +2 -3
- package/plugins/hublo.js +68 -0
package/dist/bin/sentinel.js
CHANGED
|
@@ -267,7 +267,8 @@ var toolchain_default = {
|
|
|
267
267
|
"UserConfig",
|
|
268
268
|
"UserConfigExport"
|
|
269
269
|
],
|
|
270
|
-
why: "the bundler itself. `defineConfig`, `loadEnv` and `mergeConfig` are what every config in this repo imports from it, and the types travel with them: a config importing PluginOption from a package it no longer declares builds but does not typecheck."
|
|
270
|
+
why: "the bundler itself. `defineConfig`, `loadEnv` and `mergeConfig` are what every config in this repo imports from it, and the types travel with them: a config importing PluginOption from a package it no longer declares builds but does not typecheck.",
|
|
271
|
+
floor: "Never below 8.2.2. Vite 8.0.8 resolves rolldown 1.0.0-rc.15, a release candidate whose deferred-module-initialisation codegen is broken: it wrapped a plain re-export barrel in an init helper, then required the module without binding its exports before calling the very function that binding provides. Measured, that crashed 7 of the 8 Nest services with a transformer at their first require (`init_<module> is not defined`), and quietly moved the published contract of 4 others. 8.2.2 resolves rolldown 1.2.6 and all 35 services regenerate their committed OpenAPI document identically. The version is not a preference, it is the floor at which those measurements hold."
|
|
271
272
|
},
|
|
272
273
|
{
|
|
273
274
|
package: "@vitejs/plugin-react",
|
|
@@ -956,6 +957,9 @@ function planReactApp(context, configFile) {
|
|
|
956
957
|
`removed ${owned.map(([, name]) => name).join(", ")} from this module: sentinel owns them now, and two copies of Vite in one build give the plugins a different Vite than the one running them.`
|
|
957
958
|
);
|
|
958
959
|
}
|
|
960
|
+
notes.push(
|
|
961
|
+
`this config is code, and it runs before the build does: any environment variable it reads has to be present wherever you build, including a fresh clone, another CI job or an image. Adoption does not change what it reads, but a failure there arrives early and names this role rather than the variable.`
|
|
962
|
+
);
|
|
959
963
|
return { operations, notes };
|
|
960
964
|
}
|
|
961
965
|
function planNestService(context, webpackTarget) {
|
|
@@ -1180,7 +1184,7 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
|
|
|
1180
1184
|
};
|
|
1181
1185
|
|
|
1182
1186
|
// src/roles/build/declared-outputs.ts
|
|
1183
|
-
import { existsSync as existsSync11, statSync } from "fs";
|
|
1187
|
+
import { existsSync as existsSync11, rmSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1184
1188
|
import { join as join11 } from "path";
|
|
1185
1189
|
|
|
1186
1190
|
// src/core/workspace-prep.ts
|
|
@@ -1350,6 +1354,17 @@ function resolveOutputToken(token, cwd) {
|
|
|
1350
1354
|
}
|
|
1351
1355
|
return join11(cwd, token);
|
|
1352
1356
|
}
|
|
1357
|
+
function buildStamp(cwd) {
|
|
1358
|
+
const marker = join11(cwd, `.sentinel-build-stamp-${process.pid}`);
|
|
1359
|
+
try {
|
|
1360
|
+
writeFileSync2(marker, "");
|
|
1361
|
+
const stamp = statSync(marker).mtimeMs;
|
|
1362
|
+
rmSync(marker, { force: true });
|
|
1363
|
+
return stamp;
|
|
1364
|
+
} catch {
|
|
1365
|
+
return Date.now();
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1353
1368
|
function writtenSince(path, startedAt) {
|
|
1354
1369
|
if (!existsSync11(path)) return false;
|
|
1355
1370
|
try {
|
|
@@ -1420,7 +1435,7 @@ var ViteAdapter = class extends ViteRoleAdapter {
|
|
|
1420
1435
|
);
|
|
1421
1436
|
return { ok: false, code: prerequisite.code };
|
|
1422
1437
|
}
|
|
1423
|
-
const startedAt =
|
|
1438
|
+
const startedAt = buildStamp(ctx.cwd);
|
|
1424
1439
|
const result = await this.spawnVite(ctx, ready.vite, ["build"], 1);
|
|
1425
1440
|
if (!result.ok) return result;
|
|
1426
1441
|
const problem = outputsProblem(ctx.cwd, startedAt);
|
|
@@ -2348,7 +2363,7 @@ function registerFormat() {
|
|
|
2348
2363
|
|
|
2349
2364
|
// src/roles/lint/adapters/oxlint/oxlint.adapter.ts
|
|
2350
2365
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2351
|
-
import { existsSync as existsSync23, readFileSync as readFileSync21, rmSync, writeFileSync as
|
|
2366
|
+
import { existsSync as existsSync23, readFileSync as readFileSync21, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2352
2367
|
import { join as join27 } from "path";
|
|
2353
2368
|
|
|
2354
2369
|
// src/core/config/deferred-rules.ts
|
|
@@ -2885,7 +2900,8 @@ var react_lib_default = {
|
|
|
2885
2900
|
{
|
|
2886
2901
|
name: "storybook",
|
|
2887
2902
|
specifier: "eslint-plugin-storybook"
|
|
2888
|
-
}
|
|
2903
|
+
},
|
|
2904
|
+
"../plugins/hublo.js"
|
|
2889
2905
|
],
|
|
2890
2906
|
settings: null,
|
|
2891
2907
|
renames: {
|
|
@@ -2894,6 +2910,7 @@ var react_lib_default = {
|
|
|
2894
2910
|
rules: {
|
|
2895
2911
|
curly: ["error", "all"],
|
|
2896
2912
|
"for-direction": "error",
|
|
2913
|
+
"hublo/no-regex-lookbehind": "error",
|
|
2897
2914
|
"jest-dom/prefer-checked": "error",
|
|
2898
2915
|
"jest-dom/prefer-empty": "error",
|
|
2899
2916
|
"jest-dom/prefer-enabled-disabled": "error",
|
|
@@ -3245,7 +3262,7 @@ var react_lib_default = {
|
|
|
3245
3262
|
],
|
|
3246
3263
|
uncovered: {
|
|
3247
3264
|
"no-octal": "no oxlint equivalent",
|
|
3248
|
-
"no-restricted-syntax": "no oxlint equivalent; the house rules it encoded ship as the local `hublo` plugin instead,
|
|
3265
|
+
"no-restricted-syntax": "no oxlint equivalent; the house rules it encoded ship as the local `hublo` plugin instead, so this tier hosts `../plugins/hublo.js` too. It enforces exactly one of them: `hublo/no-regex-lookbehind`, because a library is where that bug is WRITTEN and it reaches an app tier only by being imported (HUB-43431 shipped from libs/front/components into the worker app). The palette selectors stay app-tier only.",
|
|
3249
3266
|
semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between."
|
|
3250
3267
|
}
|
|
3251
3268
|
};
|
|
@@ -3309,6 +3326,7 @@ var react_default = {
|
|
|
3309
3326
|
severity: "off",
|
|
3310
3327
|
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"
|
|
3311
3328
|
},
|
|
3329
|
+
"hublo/no-regex-lookbehind": "error",
|
|
3312
3330
|
"jest/expect-expect": {
|
|
3313
3331
|
severity: "off",
|
|
3314
3332
|
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"
|
|
@@ -3795,7 +3813,7 @@ var react_default = {
|
|
|
3795
3813
|
uncovered: {
|
|
3796
3814
|
"import/order": "import ordering is FORMATTING, and the format role owns it: oxfmt's `sortImports` reproduces exactly these groups (builtin, external, internal, parent/index, sibling, alphabetical, blank line between) and sorts a file completely in ONE pass, because it rewrites the whole import block. A lint fixer cannot: it emits text edits over overlapping ranges, so this rule needed several runs to finish (five imports took four passes, measured) and sentinel deliberately runs autofix once. Enforced by `pnpm run format`, not by the linter, which is also why `eslint-plugin-import` is no longer hosted at all: its only other rule, no-duplicates, is native in oxlint.",
|
|
3797
3815
|
"no-octal": "no oxlint equivalent",
|
|
3798
|
-
"no-restricted-syntax": "no oxlint equivalent as configuration, so the
|
|
3816
|
+
"no-restricted-syntax": "no oxlint equivalent as configuration, so the AST selectors this repo used are shipped as real rules in the local `hublo` plugin instead, which this tier hosts (`../plugins/hublo.js`). Two are hosted today: the eight palette selectors as `hublo/no-palette-common`, and the lookbehind selector as `hublo/no-regex-lookbehind`. Any selector NOT in that plugin is not enforced here; propose it and it joins the plugin rather than a per-module copy.",
|
|
3799
3817
|
semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between."
|
|
3800
3818
|
}
|
|
3801
3819
|
};
|
|
@@ -5105,7 +5123,7 @@ ${result.stderr ?? ""}`);
|
|
|
5105
5123
|
const measurePath = join27(ctx.cwd, LINT_MEASURE_FILE);
|
|
5106
5124
|
let measured;
|
|
5107
5125
|
try {
|
|
5108
|
-
|
|
5126
|
+
writeFileSync3(
|
|
5109
5127
|
measurePath,
|
|
5110
5128
|
`${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
|
|
5111
5129
|
`
|
|
@@ -5123,7 +5141,7 @@ ${result.stderr ?? ""}`);
|
|
|
5123
5141
|
{ cwd: ctx.cwd, encoding: "utf8", env, maxBuffer: CAPTURE_MAX_BUFFER }
|
|
5124
5142
|
);
|
|
5125
5143
|
} finally {
|
|
5126
|
-
|
|
5144
|
+
rmSync2(measurePath, { force: true });
|
|
5127
5145
|
}
|
|
5128
5146
|
if (measured.error || !(measured.stdout ?? "").includes('"diagnostics"')) {
|
|
5129
5147
|
process.stderr.write(
|
|
@@ -5137,15 +5155,15 @@ ${result.stderr ?? ""}`);
|
|
|
5137
5155
|
const holds = holdsFrom(errorCountsByConfigRule(measured.stdout ?? ""));
|
|
5138
5156
|
const next = extendsWithBaseline(current, holds.length > 0);
|
|
5139
5157
|
if (holds.length > 0) {
|
|
5140
|
-
|
|
5158
|
+
writeFileSync3(baselinePath, renderBaseline(holds, readOwnPackage().version));
|
|
5141
5159
|
process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
|
|
5142
5160
|
`);
|
|
5143
5161
|
} else {
|
|
5144
|
-
|
|
5162
|
+
rmSync2(baselinePath, { force: true });
|
|
5145
5163
|
}
|
|
5146
5164
|
if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
|
|
5147
5165
|
config.extends = next;
|
|
5148
|
-
|
|
5166
|
+
writeFileSync3(configPath, `${JSON.stringify(config, null, 2)}
|
|
5149
5167
|
`);
|
|
5150
5168
|
}
|
|
5151
5169
|
}
|
|
@@ -6161,7 +6179,7 @@ function replaceLines(current, replacements) {
|
|
|
6161
6179
|
}
|
|
6162
6180
|
|
|
6163
6181
|
// src/core/apply-plan.ts
|
|
6164
|
-
import { existsSync as existsSync25, readFileSync as readFileSync24, renameSync, rmSync as
|
|
6182
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6165
6183
|
import { resolve as resolve4, sep } from "path";
|
|
6166
6184
|
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
6167
6185
|
|
|
@@ -6282,14 +6300,14 @@ function preparePlan(cwd, plan3) {
|
|
|
6282
6300
|
}
|
|
6283
6301
|
function writeFileAtomic(absolutePath, contents) {
|
|
6284
6302
|
const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
|
|
6285
|
-
|
|
6303
|
+
writeFileSync4(tempPath, contents);
|
|
6286
6304
|
renameSync(tempPath, absolutePath);
|
|
6287
6305
|
}
|
|
6288
6306
|
function applyPlan(cwd, plan3) {
|
|
6289
6307
|
const changed = preparePlan(cwd, plan3).filter((file) => file.before !== file.after);
|
|
6290
6308
|
for (const file of changed) {
|
|
6291
6309
|
if (file.deleted) {
|
|
6292
|
-
|
|
6310
|
+
rmSync3(file.absolutePath, { force: true });
|
|
6293
6311
|
continue;
|
|
6294
6312
|
}
|
|
6295
6313
|
writeFileAtomic(file.absolutePath, file.after);
|
|
@@ -6520,4 +6538,4 @@ export {
|
|
|
6520
6538
|
detectFramework,
|
|
6521
6539
|
dispatch
|
|
6522
6540
|
};
|
|
6523
|
-
//# sourceMappingURL=chunk-
|
|
6541
|
+
//# sourceMappingURL=chunk-YBJLWNTQ.js.map
|
package/dist/index.js
CHANGED
package/lint/react-lib.json
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"all"
|
|
59
59
|
],
|
|
60
60
|
"for-direction": "error",
|
|
61
|
+
"hublo/no-regex-lookbehind": "error",
|
|
61
62
|
"jest-dom/prefer-checked": "error",
|
|
62
63
|
"jest-dom/prefer-empty": "error",
|
|
63
64
|
"jest-dom/prefer-enabled-disabled": "error",
|
|
@@ -348,7 +349,8 @@
|
|
|
348
349
|
{
|
|
349
350
|
"name": "storybook",
|
|
350
351
|
"specifier": "eslint-plugin-storybook"
|
|
351
|
-
}
|
|
352
|
+
},
|
|
353
|
+
"../plugins/hublo.js"
|
|
352
354
|
],
|
|
353
355
|
"overrides": [
|
|
354
356
|
{
|
package/lint/react.json
CHANGED
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
"all"
|
|
59
59
|
],
|
|
60
60
|
"for-direction": "error",
|
|
61
|
+
"hublo/no-palette-common": "off",
|
|
62
|
+
"hublo/no-regex-lookbehind": "error",
|
|
61
63
|
"jest/expect-expect": "off",
|
|
62
64
|
"jest/no-alias-methods": "off",
|
|
63
65
|
"jest/no-commented-out-tests": "off",
|
|
@@ -469,6 +471,7 @@
|
|
|
469
471
|
{
|
|
470
472
|
"name": "react-refresh",
|
|
471
473
|
"specifier": "eslint-plugin-react-refresh"
|
|
472
|
-
}
|
|
474
|
+
},
|
|
475
|
+
"../plugins/hublo.js"
|
|
473
476
|
]
|
|
474
477
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.22",
|
|
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",
|
|
@@ -85,8 +85,7 @@
|
|
|
85
85
|
"nx": "^23.1.0",
|
|
86
86
|
"tsup": "^8.3.0",
|
|
87
87
|
"tsx": "^4.19.0",
|
|
88
|
-
"typescript": "^5.9.0"
|
|
89
|
-
"vitest": "^2.1.0"
|
|
88
|
+
"typescript": "^5.9.0"
|
|
90
89
|
},
|
|
91
90
|
"publishConfig": {
|
|
92
91
|
"registry": "https://registry.npmjs.org",
|
package/plugins/hublo.js
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
* sentinel exists to remove. The cost is that `@hublo/sentinel` carries a rule about Hublo's
|
|
11
11
|
* colour palette, which is honest for a package named after the company.
|
|
12
12
|
*
|
|
13
|
+
* It now hosts a SECOND, unrelated rule. `no-regex-lookbehind` is not a carried-over selector
|
|
14
|
+
* but a browser-support rule: regex lookbehind is Safari 16.4+, and unlike everything else a
|
|
15
|
+
* preset controls it can be neither transpiled nor polyfilled, because no bundler rewrites the
|
|
16
|
+
* contents of a regex literal. It is here for the same reason as the palette rule — a house
|
|
17
|
+
* rule is code, not configuration — and because lint is the only layer left that can catch it.
|
|
18
|
+
*
|
|
13
19
|
* The original selectors, and where each is handled below:
|
|
14
20
|
* 1-2 MemberExpression[object.property.name='palette'][property.name='common'] theme.palette.common
|
|
15
21
|
* …[computed=true][property.value='common'] theme.palette['common']
|
|
@@ -25,6 +31,15 @@ const MESSAGE =
|
|
|
25
31
|
const COLOUR_PROPS =
|
|
26
32
|
/^(color|bgcolor|background|backgroundColor|borderColor|borderTopColor|borderRightColor|borderBottomColor|borderLeftColor|outlineColor|fill|stroke|textDecorationColor|caretColor|accentColor)$/
|
|
27
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Lookbehind, positive or negative. NOT named groups `(?<name>)`, which Safari has had since
|
|
36
|
+
* 11.3: the class is exactly `(?<` followed by `=` or `!`.
|
|
37
|
+
*/
|
|
38
|
+
const LOOKBEHIND = /\(\?<[=!]/
|
|
39
|
+
|
|
40
|
+
const LOOKBEHIND_MESSAGE =
|
|
41
|
+
'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.'
|
|
42
|
+
|
|
28
43
|
/** `common.something` as a string value, e.g. `{ color: 'common.white' }`. */
|
|
29
44
|
const isCommonPath = (node) =>
|
|
30
45
|
node?.type === 'Literal' && typeof node.value === 'string' && /^common\./.test(node.value)
|
|
@@ -82,6 +97,59 @@ const plugin = {
|
|
|
82
97
|
}
|
|
83
98
|
},
|
|
84
99
|
},
|
|
100
|
+
|
|
101
|
+
'no-regex-lookbehind': {
|
|
102
|
+
meta: {
|
|
103
|
+
docs: {
|
|
104
|
+
description: 'Ban regex lookbehind, unsupported by Safari below 16.4.',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
create(context) {
|
|
108
|
+
const report = (node) => context.report({ node, message: LOOKBEHIND_MESSAGE })
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* `new RegExp(...)` and `RegExp(...)`, when the pattern is readable statically: a
|
|
112
|
+
* string, or a template whose fixed parts carry the lookbehind.
|
|
113
|
+
*
|
|
114
|
+
* Each template chunk is tested on its own rather than joined. Joining invents a
|
|
115
|
+
* `(?<=` that the runtime string never has, across an interpolation boundary:
|
|
116
|
+
* `` `(?<${op}=x)` `` would read as one.
|
|
117
|
+
*/
|
|
118
|
+
const isStaticLookbehind = (pattern) => {
|
|
119
|
+
if (pattern?.type === 'Literal') {
|
|
120
|
+
return typeof pattern.value === 'string' && LOOKBEHIND.test(pattern.value)
|
|
121
|
+
}
|
|
122
|
+
if (pattern?.type === 'TemplateLiteral') {
|
|
123
|
+
return (pattern.quasis ?? []).some((quasi) =>
|
|
124
|
+
LOOKBEHIND.test(quasi.value?.cooked ?? quasi.value?.raw ?? ''),
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const checkRegExpCall = (node) => {
|
|
131
|
+
if (node.callee?.name !== 'RegExp') {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
const pattern = node.arguments?.[0]
|
|
135
|
+
if (isStaticLookbehind(pattern)) {
|
|
136
|
+
report(pattern)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
// The case that actually breaks a page: a literal is an EARLY error, so the whole
|
|
142
|
+
// chunk fails to parse rather than the one call site failing.
|
|
143
|
+
Literal(node) {
|
|
144
|
+
if (node.regex && LOOKBEHIND.test(node.regex.pattern)) {
|
|
145
|
+
report(node)
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
NewExpression: checkRegExpCall,
|
|
149
|
+
CallExpression: checkRegExpCall,
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
},
|
|
85
153
|
},
|
|
86
154
|
}
|
|
87
155
|
|