@starci/hfs 5.0.7 → 5.0.8
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/CHANGELOG.md +11 -0
- package/README.md +7 -8
- package/cache/fake-session.mjs +1 -1
- package/cache/hfs-family.mjs +22 -9
- package/cache/import-graph.d.mts +32 -4
- package/cache/import-graph.mjs +143 -11
- package/cache/lint-family.d.mts +0 -2
- package/cache/lint-family.mjs +54 -33
- package/cache/records.d.mts +7 -0
- package/cache/records.mjs +17 -1
- package/cache/run.d.mts +1 -1
- package/cache/session.mjs +6 -2
- package/cache/side-key.d.mts +2 -0
- package/cache/side-key.mjs +5 -1
- package/cache/structure-memo.d.mts +34 -0
- package/cache/structure-memo.mjs +150 -0
- package/cache/types.d.mts +6 -2
- package/cache/wiring-family.mjs +1 -1
- package/codemod/small-renames.mjs +22 -2
- package/lint/eslint-list.cjs +26 -0
- package/lint/run.d.mts +18 -58
- package/lint/run.mjs +54 -284
- package/package.json +1 -1
- package/release/lint-report.d.mts +1 -1
- package/release/lint-report.mjs +3 -3
- package/report/sonar.d.mts +1 -1
- package/report/sonar.mjs +3 -3
- package/report/stylelint-findings.d.mts +23 -0
- package/report/stylelint-findings.mjs +9 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +7 -7
- package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +4 -4
- package/runtime/knowledge/hfs/slots.yaml +1 -1
- package/runtime/knowledge/sonar-gate.yaml +2 -2
- package/runtime/modules/kernel/failure-codes.yaml +4 -4
- package/runtime/scripts/api/node/run-node.d.mts +5 -0
- package/runtime/scripts/hfs/check.d.mts +16 -1
- package/runtime/scripts/hfs/check.mjs +18 -7
- package/runtime/scripts/hfs/gate-threshold.mjs +1 -1
- package/runtime/scripts/hfs/rule-memo.d.mts +30 -0
- package/runtime/scripts/hfs/rule-memo.mjs +24 -0
- package/runtime/scripts/hfs/rules/a15-findings.d.mts +6 -0
- package/runtime/scripts/hfs/rules/a15-findings.mjs +11 -2
- package/runtime/scripts/hfs/rules/ci-standard.mjs +2 -2
- package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +9 -11
- package/runtime/scripts/hfs/rules/pipeline.mjs +13 -9
- package/runtime/scripts/hfs/rules/read.d.mts +5 -0
- package/runtime/scripts/hfs/rules/read.mjs +13 -0
- package/runtime/scripts/hfs/rules/secrets.d.mts +2 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -1
- package/runtime/scripts/hfs/rules/stacks-release.mjs +35 -21
- package/runtime/scripts/lib/worktree-exclude.mjs +1 -1
- package/scaffold/app.mjs +3 -3
- package/src/check-cache-structure-support.d.mts +52 -0
- package/src/main.d.mts +3 -2
- package/src/main.mjs +123 -109
- package/sync/ci-commands.d.mts +2 -1
- package/sync/ci-commands.mjs +19 -20
- package/sync/index.mjs +2 -2
- package/templates/app/package-scripts/core.json +2 -2
- package/templates/app/task-graph/turbo.json +0 -3
package/lint/run.d.mts
CHANGED
|
@@ -2,33 +2,27 @@ import { type HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
|
|
|
2
2
|
import { type CanonDeclarations, type LintSideStats } from '../cache/lint-family.mjs';
|
|
3
3
|
import type { CheckSession } from '../cache/types.mjs';
|
|
4
4
|
type Dynamic = any;
|
|
5
|
-
/** The parsed arguments of the lint verb. */
|
|
6
|
-
export interface LintOptions {
|
|
7
|
-
changed: string[] | null;
|
|
8
|
-
fix: boolean;
|
|
9
|
-
format: string;
|
|
10
|
-
out: string | undefined;
|
|
11
|
-
workspace: string | undefined;
|
|
12
|
-
side?: string;
|
|
13
|
-
full?: boolean;
|
|
14
|
-
}
|
|
15
|
-
/** The cache a lint run may use: the session of the check cache. The caller flushes it after the run. */
|
|
16
|
-
export interface LintCache {
|
|
17
|
-
session: CheckSession;
|
|
18
|
-
}
|
|
19
5
|
/** What a linter run answers: its json results, or why it could not complete (both when the run was partial). */
|
|
20
6
|
export interface LinterRun {
|
|
21
7
|
results?: Dynamic[];
|
|
22
8
|
error?: string;
|
|
23
9
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
10
|
+
/**
|
|
11
|
+
* What a linter run is asked for. `list` is the files an ESLint run lints (cwd-relative, posix) when they are named one by one: they go to the child
|
|
12
|
+
* through a file, never the command line, and `args` then holds the options alone. `files` is the number of source files the run judges, which the
|
|
13
|
+
* heavy-command budget of the ESLint kind scales with (a list names its own count).
|
|
14
|
+
*/
|
|
15
|
+
export interface LinterSpec {
|
|
26
16
|
cwd: string;
|
|
27
17
|
pkg: string;
|
|
28
18
|
bin: string;
|
|
29
19
|
args: string[];
|
|
30
20
|
bound?: string | null;
|
|
31
|
-
|
|
21
|
+
list?: readonly string[] | undefined;
|
|
22
|
+
files?: number | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** The seam that runs a linter through the app's own install from cwd. */
|
|
25
|
+
export type LinterRunner = (spec: LinterSpec) => Promise<LinterRun>;
|
|
32
26
|
/** One finding of the lint report. */
|
|
33
27
|
export interface LintFinding {
|
|
34
28
|
engine: string;
|
|
@@ -39,17 +33,9 @@ export interface LintFinding {
|
|
|
39
33
|
line: number | null;
|
|
40
34
|
column: number | null;
|
|
41
35
|
message: string;
|
|
42
|
-
titleVi?: string;
|
|
43
|
-
nextStepVi?: string;
|
|
44
36
|
}
|
|
45
37
|
/** The app declaration the slot machine returns (readRepoDeclaration): sides are present at an app root. */
|
|
46
38
|
type RepoDeclaration = Dynamic;
|
|
47
|
-
export declare const LINT_SCHEMA = "starci/lint@1";
|
|
48
|
-
/** The app-relative workspace of `opts.workspace` (already app-relative), or a refusal when it is not one folder of fe/apps or fe/packages. */
|
|
49
|
-
export declare function workspaceOf(workspace: string): string;
|
|
50
|
-
/** The flags of `starci app lint`; `--changed` takes every argument up to the next flag, and a repeated `--changed` adds to the list (the
|
|
51
|
-
* catalog's list flag, `--changed a --changed b`, as the CLI dispatcher passes it through). */
|
|
52
|
-
export declare function parseLintArgs(argv: readonly string[]): LintOptions;
|
|
53
39
|
/**
|
|
54
40
|
* The heap of the ESLint child in MiB: the starci CLI sets it from its one owner, the eslint memory of ops/liveness.yaml `heavy` (a typed whole-side
|
|
55
41
|
* lint of a large app outgrows node's default heap). Unset, ESLint runs on node's default.
|
|
@@ -57,13 +43,7 @@ export declare function parseLintArgs(argv: readonly string[]): LintOptions;
|
|
|
57
43
|
export declare const ESLINT_HEAP_ENV = "STARCI_ESLINT_HEAP_MB";
|
|
58
44
|
export declare function eslintHeapArgs(env?: NodeJS.ProcessEnv): string[];
|
|
59
45
|
/** A linter's json results through the app's own install, run from `cwd` (a side folder): `{ results }` or `{ error }`. The child runs while the caller goes on. */
|
|
60
|
-
export declare function runLinter({ cwd, pkg, bin, args, bound }:
|
|
61
|
-
cwd: string;
|
|
62
|
-
pkg: string;
|
|
63
|
-
bin: string;
|
|
64
|
-
args: string[];
|
|
65
|
-
bound?: string | null;
|
|
66
|
-
}, execute?: HeavyNodeRun): Promise<LinterRun>;
|
|
46
|
+
export declare function runLinter({ cwd, pkg, bin, args, bound, list, files }: LinterSpec, execute?: HeavyNodeRun): Promise<LinterRun>;
|
|
67
47
|
/**
|
|
68
48
|
* The lint scope a flat config declares: `ignored(file)` for its global ignores (a block holding only `ignores` and `name`), which prune
|
|
69
49
|
* the walk, and `source(file)` for a file some block's `files` select while that block's own `ignores` do not (standard flat-config
|
|
@@ -85,8 +65,8 @@ export declare function installedLintConfig({ repoRoot, cwd, side }: {
|
|
|
85
65
|
cwd: string;
|
|
86
66
|
side: string;
|
|
87
67
|
}): Promise<Dynamic[]>;
|
|
88
|
-
/** Execute each side's installed ESLint config once;
|
|
89
|
-
export declare function eslintRepository({ repoRoot, app, opts, changed, existing,
|
|
68
|
+
/** Execute each side's installed ESLint config once, whole or over a named list; the cached and the cacheless run share scope, coverage and tool-failure judgment. */
|
|
69
|
+
export declare function eslintRepository({ repoRoot, app, opts, changed, existing, run, scopes }: {
|
|
90
70
|
repoRoot: string;
|
|
91
71
|
app?: RepoDeclaration;
|
|
92
72
|
opts?: {
|
|
@@ -95,9 +75,8 @@ export declare function eslintRepository({ repoRoot, app, opts, changed, existin
|
|
|
95
75
|
};
|
|
96
76
|
changed?: readonly string[] | null;
|
|
97
77
|
existing?: readonly string[];
|
|
98
|
-
workspace?: string | null;
|
|
99
|
-
onFe?: string | null;
|
|
100
78
|
run?: LinterRunner | undefined;
|
|
79
|
+
scopes?: Readonly<Record<string, readonly string[]>> | undefined;
|
|
101
80
|
}): Promise<{
|
|
102
81
|
findings: LintFinding[];
|
|
103
82
|
errors: string[];
|
|
@@ -138,30 +117,11 @@ export interface CachedLinters {
|
|
|
138
117
|
* validation of a full run (eslintRepository), the rest take their records. The answer has the shape of eslintRepository's, plus the stylelint
|
|
139
118
|
* results, so the lint report is built the same way from either.
|
|
140
119
|
*/
|
|
141
|
-
export declare function cachedLinters({ repoRoot, app,
|
|
120
|
+
export declare function cachedLinters({ repoRoot, app, side: only, run, session }: {
|
|
142
121
|
repoRoot: string;
|
|
143
122
|
app: RepoDeclaration;
|
|
144
|
-
|
|
145
|
-
workspace: string | null;
|
|
146
|
-
onFe: string | null;
|
|
123
|
+
side?: string | undefined;
|
|
147
124
|
run?: LinterRunner | undefined;
|
|
148
|
-
|
|
125
|
+
session: CheckSession;
|
|
149
126
|
}): Promise<CachedLinters>;
|
|
150
|
-
/**
|
|
151
|
-
* Run the whole lint of the app at `repoRoot`. `hfsCheck(repoRoot)` returns the `starci app check` result (`{ findings, tracked }`); the CLI
|
|
152
|
-
* injects it. Returns `{ report, sonar, exit }`; `sonar` is the merged Generic Issue Import document.
|
|
153
|
-
*/
|
|
154
|
-
export declare function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles, runLinter: linter, cache }: {
|
|
155
|
-
repoRoot: string;
|
|
156
|
-
opts: LintOptions;
|
|
157
|
-
hfsCheck: (repoRoot: string) => Dynamic;
|
|
158
|
-
trackedFiles?: (repoRoot: string) => string[];
|
|
159
|
-
runLinter?: LinterRunner | undefined;
|
|
160
|
-
cache?: LintCache | undefined;
|
|
161
|
-
}): Promise<{
|
|
162
|
-
report: Dynamic;
|
|
163
|
-
sonar: Dynamic;
|
|
164
|
-
exit: number;
|
|
165
|
-
}>;
|
|
166
|
-
export declare function printLintText(report: Dynamic, out: (text: string) => unknown): void;
|
|
167
127
|
export {};
|
package/lint/run.mjs
CHANGED
|
@@ -6,107 +6,31 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
-
// starci app lint
|
|
9
|
+
// The linters of `starci app check --gate lint`: ESLint once per side and stylelint over the fe stylesheets, through the app's own install.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// 2. `starci app check` for what has no TypeScript file to sit on (the tree, managed files, pins, CI, contracts, docs, .starciwork), root and sides;
|
|
18
|
-
// 3. stylelint over the fe side's stylesheets (sync STYLE_GLOB), from fe/ with its stylelint.config.mjs.
|
|
19
|
-
// Their findings become one list of one shape (`starci/lint@1`), every path app-relative. `--json` prints that report on stdout;
|
|
20
|
-
// `--out <file>` writes the same findings as THE Sonar Generic Issue Import file (engine ids starci-hfs, eslint, stylelint in one
|
|
21
|
-
// document); the exit code is the land-gate input: 0 no finding, 1 at least one, 2 a tool could not run (never a pass).
|
|
22
|
-
// --changed restricts ESLint and stylelint to the listed files and keeps of the app findings only those on a listed file or on no file.
|
|
23
|
-
// --workspace scopes the same lint to one npm workspace of the fe side (fe/apps/<app> or fe/packages/<pkg>, app-relative): ESLint and
|
|
24
|
-
// stylelint over that folder only, and of the app findings only those inside it. It is the `lint` script of every fe workspace (the
|
|
25
|
-
// turbo `lint` task); the root `npm run lint` stays the whole app. There is no second linter: a workspace lint is this lint, scoped.
|
|
26
|
-
// The result cache (contexts/project/check-cache.md): when the caller hands lintRepository a cache session, ESLint and stylelint judge only what
|
|
27
|
-
// the records of `.starcistacks/cache/lint/<side>/` cannot vouch for (cache/lint-family.mjs) - the same records and fingerprint `app check --lint`
|
|
28
|
-
// uses, so the two verbs never hold two answers. `--side` reads and writes its side's records, `--workspace` reads and writes none, `--changed`
|
|
29
|
-
// and `--fix` bypass the cache, and `--full` judges everything again and rewrites the records.
|
|
11
|
+
// 1. ESLint runs from the side folder with that side's eslint.config.mjs (be/ the be canon, fe/ the fe canon: the per-file rules and the
|
|
12
|
+
// project-graph rules of the architecture machine, findings on the line of the offending TypeScript file); each side lints only its own
|
|
13
|
+
// files, so the be rules never see fe/ and the fe rules never see be/;
|
|
14
|
+
// 2. stylelint judges the fe side's stylesheets (sync STYLE_GLOB) from fe/ with its stylelint.config.mjs.
|
|
15
|
+
// `cachedLinters` is the one entry of `app check`: per side it judges only what the records of `.starcistacks/cache/lint/<side>/` cannot
|
|
16
|
+
// vouch for (cache/lint-family.mjs, contexts/project/check-cache.md), and `eslintRepository` is the launcher it and the cacheless run share.
|
|
30
17
|
import fs from 'node:fs';
|
|
31
18
|
import path from 'node:path';
|
|
32
19
|
import { createRequire } from 'node:module';
|
|
20
|
+
import { makeTempDir } from '../runtime/scripts/api/fs/make-temp-dir.mjs';
|
|
33
21
|
import { completedNode, isHostHeld, isNodeExecutionRefusal, runHeavyNode } from '../runtime/scripts/api/node/run-node.mjs';
|
|
34
22
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
|
-
import { linterReport, mergeReports, sonarReport, sourceRootsOf } from '../report/sonar.mjs';
|
|
36
23
|
import { SIDES, appRelativeMessages, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
37
|
-
import { isSide } from '../runtime/scripts/hfs/slot-manifest-shape.mjs';
|
|
38
|
-
import { chunkByChars, mapBounded, mergeRuns } from './chunk.mjs';
|
|
39
24
|
import { STYLE_GLOB } from '../sync/index.mjs';
|
|
40
25
|
import { byCodeUnit } from '../report/order.mjs';
|
|
41
26
|
import { braceVariants, globExpression } from '../runtime/scripts/lib/glob.mjs';
|
|
42
27
|
import { grammarLintFor } from '../runtime/scripts/lib/grammar-lint.mjs';
|
|
43
|
-
import { gateVerdict } from '../runtime/scripts/hfs/gate-threshold.mjs';
|
|
44
28
|
import { mergeBase } from '../runtime/scripts/api/git/merge-base.mjs';
|
|
45
29
|
import { lintFamilyReport, lintSide, loadAppTypeScript } from '../cache/lint-family.mjs';
|
|
46
|
-
// --side judges only that side; root HFS findings belong to be, its Sonar report stays on that side.
|
|
47
|
-
export const LINT_SCHEMA = 'starci/lint@1';
|
|
48
30
|
/** The side whose stylesheets stylelint judges. */
|
|
49
31
|
const STYLE_SIDE = 'fe';
|
|
50
32
|
const CODE_PREFIX = /^\[([A-Z][A-Z0-9_]+)\] /;
|
|
51
33
|
const posix = (file) => String(file).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
52
|
-
/** An npm workspace of the fe side, app-relative: one folder below fe/apps or fe/packages (the root package.json workspaces). */
|
|
53
|
-
const WORKSPACE = /^fe\/(?:apps|packages)\/[a-z0-9][a-z0-9-]*$/;
|
|
54
|
-
/** The app-relative workspace of `opts.workspace` (already app-relative), or a refusal when it is not one folder of fe/apps or fe/packages. */
|
|
55
|
-
export function workspaceOf(workspace) {
|
|
56
|
-
const rel = posix(workspace).replace(/\/+$/, '');
|
|
57
|
-
if (!WORKSPACE.test(rel))
|
|
58
|
-
throw new Error(`--workspace ${workspace} is not an fe workspace (fe/apps/<app> or fe/packages/<pkg>, from the app root)`);
|
|
59
|
-
return rel;
|
|
60
|
-
}
|
|
61
|
-
/** Side scopes partition the full lint; Sonar reports share that scope and a frontend workspace cannot belong to be. */
|
|
62
|
-
function validateSide(opts) {
|
|
63
|
-
if (opts.side === undefined)
|
|
64
|
-
return;
|
|
65
|
-
if (!isSide(opts.side))
|
|
66
|
-
throw new Error('LINT_SIDE_INVALID: --side is be or fe');
|
|
67
|
-
if (opts.side === 'be' && opts.workspace !== undefined)
|
|
68
|
-
throw new Error('LINT_SIDE_WORKSPACE_CONFLICT: --side be cannot judge a frontend workspace');
|
|
69
|
-
}
|
|
70
|
-
/** Side findings stay with their side; findings outside both sides belong to the backend lint. */
|
|
71
|
-
function findingOnSide(finding, side) {
|
|
72
|
-
if (side === undefined)
|
|
73
|
-
return true;
|
|
74
|
-
const file = posix(finding.path ?? '');
|
|
75
|
-
const owner = SIDES.find((candidate) => file === candidate || file.startsWith(`${candidate}/`));
|
|
76
|
-
return (owner ?? 'be') === side;
|
|
77
|
-
}
|
|
78
|
-
/** The flags of `starci app lint`; `--changed` takes every argument up to the next flag, and a repeated `--changed` adds to the list (the
|
|
79
|
-
* catalog's list flag, `--changed a --changed b`, as the CLI dispatcher passes it through). */
|
|
80
|
-
export function parseLintArgs(argv) {
|
|
81
|
-
const opts = { changed: null, fix: false, format: 'text', out: undefined, workspace: undefined };
|
|
82
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
83
|
-
const arg = argv[i];
|
|
84
|
-
if (arg === '--changed') {
|
|
85
|
-
opts.changed ??= [];
|
|
86
|
-
while (i + 1 < argv.length && !argv[i + 1].startsWith('--'))
|
|
87
|
-
opts.changed.push(posix(argv[++i]));
|
|
88
|
-
}
|
|
89
|
-
else if (arg === '--fix')
|
|
90
|
-
opts.fix = true;
|
|
91
|
-
else if (arg === '--full')
|
|
92
|
-
opts.full = true;
|
|
93
|
-
else if (arg === '--json')
|
|
94
|
-
opts.format = 'json';
|
|
95
|
-
else if (['--out', '--workspace', '--side'].includes(arg)) {
|
|
96
|
-
if (arg === '--side' && (argv[i + 1] === undefined || argv[i + 1].startsWith('--')))
|
|
97
|
-
throw new Error('LINT_SIDE_INVALID: --side needs be or fe');
|
|
98
|
-
if (argv[i + 1] === undefined)
|
|
99
|
-
throw new Error(`${arg} needs a value`);
|
|
100
|
-
opts[arg.slice(2)] = argv[++i];
|
|
101
|
-
}
|
|
102
|
-
else
|
|
103
|
-
throw new Error(`unknown argument ${arg}`);
|
|
104
|
-
}
|
|
105
|
-
if (opts.workspace !== undefined && opts.changed !== null)
|
|
106
|
-
throw new Error('--workspace and --changed do not combine: a workspace lint judges the whole workspace');
|
|
107
|
-
validateSide(opts);
|
|
108
|
-
return opts;
|
|
109
|
-
}
|
|
110
34
|
/** The typed ESLint run reads nothing above the app root: its TypeScript `sys` is bound by this preload (3.3, bound-sys.cjs). */
|
|
111
35
|
const BOUND_ENV = 'STARCI_LINT_BOUND';
|
|
112
36
|
const linterBoundArgs = () => ['--require', fileURLToPath(new URL('./bound-sys.cjs', import.meta.url))];
|
|
@@ -119,8 +43,10 @@ export function eslintHeapArgs(env = process.env) {
|
|
|
119
43
|
const mb = Number(env[ESLINT_HEAP_ENV]);
|
|
120
44
|
return Number.isInteger(mb) && mb > 0 ? [`--max-old-space-size=${mb}`] : [];
|
|
121
45
|
}
|
|
46
|
+
/** The ESLint list runner (eslint-list.cts, built beside this file). */
|
|
47
|
+
const LIST_RUNNER = fileURLToPath(new URL('./eslint-list.cjs', import.meta.url));
|
|
122
48
|
/** A linter's json results through the app's own install, run from `cwd` (a side folder): `{ results }` or `{ error }`. The child runs while the caller goes on. */
|
|
123
|
-
export async function runLinter({ cwd, pkg, bin, args, bound = null }, execute) {
|
|
49
|
+
export async function runLinter({ cwd, pkg, bin, args, bound = null, list, files }, execute) {
|
|
124
50
|
let entry;
|
|
125
51
|
try {
|
|
126
52
|
const manifest = createRequire(path.join(cwd, 'package.json')).resolve(`${pkg}/package.json`);
|
|
@@ -131,7 +57,23 @@ export async function runLinter({ cwd, pkg, bin, args, bound = null }, execute)
|
|
|
131
57
|
return { error: `${pkg} is not installed for ${cwd}` };
|
|
132
58
|
}
|
|
133
59
|
const heap = pkg === 'eslint' ? eslintHeapArgs() : [];
|
|
134
|
-
|
|
60
|
+
// A named list is written to a file the runner reads: ESLint lints all of it in one process, and no command line holds a path.
|
|
61
|
+
const listed = pkg === 'eslint' && list !== undefined ? makeTempDir('starci-eslint-') : null;
|
|
62
|
+
let childArgs = [entry, ...args];
|
|
63
|
+
if (listed !== null && list !== undefined) {
|
|
64
|
+
const job = path.join(listed, 'job.json');
|
|
65
|
+
fs.writeFileSync(job, JSON.stringify({ files: list, fix: args.includes('--fix') }));
|
|
66
|
+
childArgs = [LIST_RUNNER, job];
|
|
67
|
+
}
|
|
68
|
+
const count = list?.length ?? files;
|
|
69
|
+
let run;
|
|
70
|
+
try {
|
|
71
|
+
run = completedNode(await runHeavyNode(execute, { kind: 'eslint', label: `app check ${pkg} ${path.basename(cwd)}`, args: [...heap, ...(bound ? linterBoundArgs() : []), ...childArgs], cwd, maxBuffer: 64 * 1024 * 1024, ...(count === undefined || pkg !== 'eslint' ? {} : { files: count }), ...(bound ? { env: { ...process.env, [BOUND_ENV]: bound } } : {}) }), pkg);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
if (listed !== null)
|
|
75
|
+
fs.rmSync(listed, { recursive: true, force: true });
|
|
76
|
+
}
|
|
135
77
|
// ESLint prints its report on stdout; stylelint uses stderr. JSON does not prove a completed child: only exits 0/1
|
|
136
78
|
// without a spawn error or signal are measurable, and exit 1 must carry findings.
|
|
137
79
|
let results;
|
|
@@ -151,11 +93,6 @@ const eslintFindings = (results, appRoot) => results.flatMap((result) => (result
|
|
|
151
93
|
engine: 'eslint', rule: message.ruleId ?? 'eslint-error', code: CODE_PREFIX.exec(message.message ?? '')?.[1] ?? null, severity: message.severity === 1 ? 'warning' : 'error',
|
|
152
94
|
path: posix(path.relative(appRoot, result.filePath)), line: message.line ?? null, column: message.column ?? null, message: message.message,
|
|
153
95
|
})));
|
|
154
|
-
const stylelintFindings = (results, appRoot) => results.flatMap((result) => (result.warnings ?? []).map((warning) => ({
|
|
155
|
-
engine: 'stylelint', rule: warning.rule || 'stylelint-error', code: null, severity: 'error',
|
|
156
|
-
path: posix(path.relative(appRoot, result.source)), line: warning.line ?? null, column: warning.column ?? null, message: warning.text,
|
|
157
|
-
})));
|
|
158
|
-
const byLocation = (a, b) => `${a.path ?? ''}:${String(a.line ?? 0).padStart(7, '0')}:${a.rule}`.localeCompare(`${b.path ?? ''}:${String(b.line ?? 0).padStart(7, '0')}:${b.rule}`);
|
|
159
96
|
/** The files of `changed` (app-relative) below `side`/, relative to the side folder. */
|
|
160
97
|
const onSide = (changed, side) => changed.filter((file) => file.startsWith(`${side}/`)).map((file) => file.slice(side.length + 1));
|
|
161
98
|
const fileKey = (file) => process.platform === 'win32' ? path.resolve(file).toLowerCase() : path.resolve(file);
|
|
@@ -276,7 +213,7 @@ export async function installedLintConfig({ repoRoot, cwd, side }) {
|
|
|
276
213
|
return [...config, ...familyConfig];
|
|
277
214
|
}
|
|
278
215
|
/** Read both installed canons' source selectors, then enumerate that scope; unavailable selectors cannot prove coverage. */
|
|
279
|
-
async function sourceFiles({ repoRoot, cwd, side
|
|
216
|
+
async function sourceFiles({ repoRoot, cwd, side }) {
|
|
280
217
|
const config = await installedLintConfig({ repoRoot, cwd, side });
|
|
281
218
|
const scope = canonScope(config);
|
|
282
219
|
const files = [];
|
|
@@ -294,11 +231,11 @@ async function sourceFiles({ repoRoot, cwd, side, scope: folderScope }) {
|
|
|
294
231
|
files.push(file);
|
|
295
232
|
}
|
|
296
233
|
};
|
|
297
|
-
visit(
|
|
234
|
+
visit('');
|
|
298
235
|
return files.sort(byCodeUnit);
|
|
299
236
|
}
|
|
300
237
|
/** Every requested source needs an explicit, well-formed result; suppressed findings and foreign paths cannot witness a completed lint. */
|
|
301
|
-
function checkedEslintResults({ results, expected, cwd,
|
|
238
|
+
function checkedEslintResults({ results, expected, cwd, changed }) {
|
|
302
239
|
const required = new Set(expected.map((file) => fileKey(path.join(cwd, file))));
|
|
303
240
|
const seen = new Set(), valid = [], errors = [];
|
|
304
241
|
for (const result of results) {
|
|
@@ -309,7 +246,7 @@ function checkedEslintResults({ results, expected, cwd, scope, changed }) {
|
|
|
309
246
|
continue;
|
|
310
247
|
}
|
|
311
248
|
const key = fileKey(result.filePath);
|
|
312
|
-
if (!inside(cwd, result.filePath) || (
|
|
249
|
+
if (!inside(cwd, result.filePath) || (changed && !required.has(key))) {
|
|
313
250
|
errors.push(`eslint produced a result outside the requested scope: ${result.filePath}`);
|
|
314
251
|
continue;
|
|
315
252
|
}
|
|
@@ -325,8 +262,6 @@ function checkedEslintResults({ results, expected, cwd, scope, changed }) {
|
|
|
325
262
|
errors.push(`eslint produced no result for requested source files: ${missing.join(', ')}`);
|
|
326
263
|
return { results: valid, errors };
|
|
327
264
|
}
|
|
328
|
-
const ESLINT_ARGV_BUDGET = 24_000;
|
|
329
|
-
const ESLINT_CONCURRENCY = 3;
|
|
330
265
|
/** A launch that threw instead of returning a record is a tool error of the run (exit 2), not a crash of the CLI. */
|
|
331
266
|
const spawnFailure = (error) => {
|
|
332
267
|
if (isNodeExecutionRefusal(error))
|
|
@@ -334,13 +269,9 @@ const spawnFailure = (error) => {
|
|
|
334
269
|
return { error: `could not start eslint: ${String(error?.message ?? error)}` };
|
|
335
270
|
};
|
|
336
271
|
/** Starts the ESLint child of every side that has work and returns one record per side: skipped, a scope error, or the running child. */
|
|
337
|
-
async function launchEslint({ app, repoRoot, opts, changed, existing,
|
|
272
|
+
async function launchEslint({ app, repoRoot, opts, changed, existing, run = runLinter, scopes = {} }) {
|
|
338
273
|
const launches = [];
|
|
339
274
|
for (const side of app ? SIDES.filter((candidate) => opts.side === undefined || opts.side === candidate) : []) {
|
|
340
|
-
if (workspace !== null && side !== STYLE_SIDE) {
|
|
341
|
-
launches.push({ side, skipped: `outside --workspace ${workspace}` });
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
275
|
const sources = onSide(existing, side).filter((file) => /\.(?:[cm]?[jt]sx?)$/.test(file));
|
|
345
276
|
if (changed !== null && !sources.length) {
|
|
346
277
|
launches.push({ side, skipped: 'no changed source file' });
|
|
@@ -348,28 +279,30 @@ async function launchEslint({ app, repoRoot, opts, changed, existing, workspace,
|
|
|
348
279
|
}
|
|
349
280
|
const cwd = path.join(repoRoot, side);
|
|
350
281
|
let expected;
|
|
282
|
+
// a caller that enumerated the side's scope already (the cache) hands it over: the canon config is loaded and the folders walked once per run
|
|
283
|
+
const known = scopes[side];
|
|
351
284
|
try {
|
|
352
|
-
expected = changed ? sources : await sourceFiles({ repoRoot, cwd, side
|
|
285
|
+
expected = changed ? sources : known !== undefined ? [...known] : await sourceFiles({ repoRoot, cwd, side });
|
|
353
286
|
}
|
|
354
287
|
catch (error) {
|
|
355
288
|
launches.push({ side, scopeError: `eslint source scope is unavailable in ${side}/: ${String(error?.message ?? error)}` });
|
|
356
289
|
continue;
|
|
357
290
|
}
|
|
358
291
|
const head = ['--format', 'json', ...(opts.fix ? ['--fix'] : [])];
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
const
|
|
362
|
-
launches.push({ side, cwd, expected, run:
|
|
292
|
+
// The files of a diff go to ONE child through a file (a diff of hundreds of long paths outgrows the Windows command line, and each extra
|
|
293
|
+
// child would build the whole TypeScript program again); the whole side is a single directory argument.
|
|
294
|
+
const launch = Promise.resolve().then(() => run(changed ? { cwd, pkg: 'eslint', bin: 'eslint', args: head, list: sources, bound: repoRoot } : { cwd, pkg: 'eslint', bin: 'eslint', args: [...head, '.'], files: expected.length, bound: repoRoot }));
|
|
295
|
+
launches.push({ side, cwd, expected, run: launch.catch(spawnFailure) });
|
|
363
296
|
}
|
|
364
297
|
return launches;
|
|
365
298
|
}
|
|
366
|
-
/** Execute each side's installed ESLint config once;
|
|
367
|
-
export async function eslintRepository({ repoRoot, app = readRepoDeclaration(loadSlotManifest(), repoRoot), opts = {}, changed = null, existing = [],
|
|
299
|
+
/** Execute each side's installed ESLint config once, whole or over a named list; the cached and the cacheless run share scope, coverage and tool-failure judgment. */
|
|
300
|
+
export async function eslintRepository({ repoRoot, app = readRepoDeclaration(loadSlotManifest(), repoRoot), opts = {}, changed = null, existing = [], run, scopes }) {
|
|
368
301
|
const findings = [], errors = [], raw = [];
|
|
369
302
|
const engine = { sides: {} };
|
|
370
303
|
if (!app.sides)
|
|
371
304
|
throw new Error('ESLint runs at the app root, the folder of hfs.json');
|
|
372
|
-
const launches = await launchEslint({ app, repoRoot, opts, changed, existing,
|
|
305
|
+
const launches = await launchEslint({ app, repoRoot, opts, changed, existing, run, ...(scopes === undefined ? {} : { scopes }) });
|
|
373
306
|
const settled = await Promise.allSettled(launches.map(launch => launch.run ?? null));
|
|
374
307
|
const rejected = settled.filter((run) => run.status === 'rejected');
|
|
375
308
|
const failed = rejected.find(run => isHostHeld(run.reason)) ?? rejected[0];
|
|
@@ -391,7 +324,7 @@ export async function eslintRepository({ repoRoot, app = readRepoDeclaration(loa
|
|
|
391
324
|
if (linted.error)
|
|
392
325
|
errors.push(linted.error);
|
|
393
326
|
if (linted.results) {
|
|
394
|
-
const checked = checkedEslintResults({ results: linted.results, expected: launch.expected, cwd: launch.cwd,
|
|
327
|
+
const checked = checkedEslintResults({ results: linted.results, expected: launch.expected, cwd: launch.cwd, changed: changed !== null });
|
|
395
328
|
errors.push(...checked.errors);
|
|
396
329
|
linted.results = checked.results;
|
|
397
330
|
const appRelative = appRelativeMessages(side, path.join(repoRoot, side));
|
|
@@ -483,22 +416,18 @@ export function gitBaseToken(repoRoot) {
|
|
|
483
416
|
* validation of a full run (eslintRepository), the rest take their records. The answer has the shape of eslintRepository's, plus the stylelint
|
|
484
417
|
* results, so the lint report is built the same way from either.
|
|
485
418
|
*/
|
|
486
|
-
export async function cachedLinters({ repoRoot, app,
|
|
419
|
+
export async function cachedLinters({ repoRoot, app, side: only, run, session }) {
|
|
487
420
|
const eslint = { findings: [], errors: [], raw: [], engine: { sides: {} } };
|
|
488
421
|
const style = { results: [], errors: [], ran: false };
|
|
489
422
|
const sides = [];
|
|
490
|
-
const decision =
|
|
491
|
-
const full =
|
|
423
|
+
const decision = session.decide('lint');
|
|
424
|
+
const full = decision.mode === 'full' ? decision.reason : null;
|
|
492
425
|
const gitToken = gitBaseToken(repoRoot);
|
|
493
|
-
for (const side of SIDES.filter((candidate) =>
|
|
494
|
-
if (workspace !== null && side !== STYLE_SIDE) {
|
|
495
|
-
eslint.engine.sides[side] = { files: 0, skipped: `outside --workspace ${workspace}` };
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
426
|
+
for (const side of SIDES.filter((candidate) => only === undefined || only === candidate)) {
|
|
498
427
|
const cwd = path.join(repoRoot, side);
|
|
499
428
|
let scope, canons;
|
|
500
429
|
try {
|
|
501
|
-
scope = await sourceFiles({ repoRoot, cwd, side
|
|
430
|
+
scope = await sourceFiles({ repoRoot, cwd, side });
|
|
502
431
|
canons = await canonDeclarations({ repoRoot, cwd, side });
|
|
503
432
|
}
|
|
504
433
|
catch (error) {
|
|
@@ -508,16 +437,15 @@ export async function cachedLinters({ repoRoot, app, opts, workspace, onFe, run,
|
|
|
508
437
|
}
|
|
509
438
|
const styled = side === STYLE_SIDE;
|
|
510
439
|
const outcome = await lintSide({
|
|
511
|
-
session
|
|
512
|
-
|
|
513
|
-
canons, styleCanon: styled ? await styleDeclaration({ repoRoot, cwd }) : null, typescript: () => loadAppTypeScript(cwd), extraKey: '', gitToken, full, write: workspace === null,
|
|
440
|
+
session, side, sideRoot: cwd, scope, stylesheets: styled ? stylesheetsOf(cwd) : [],
|
|
441
|
+
canons, styleCanon: styled ? await styleDeclaration({ repoRoot, cwd }) : null, typescript: () => loadAppTypeScript(cwd), extraKey: '', gitToken, full, write: true,
|
|
514
442
|
eslint: async (files) => {
|
|
515
443
|
const listed = files === null ? null : files.map((file) => `${side}/${file}`);
|
|
516
|
-
const sub = await eslintRepository({ repoRoot, app, opts: { side }, changed: listed, existing: listed ?? [],
|
|
444
|
+
const sub = await eslintRepository({ repoRoot, app, opts: { side }, changed: listed, existing: listed ?? [], run, scopes: { [side]: scope } });
|
|
517
445
|
return { results: sub.raw, errors: sub.errors };
|
|
518
446
|
},
|
|
519
447
|
stylelint: styled ? async (files) => {
|
|
520
|
-
const linted = await (run ?? runLinter)({ cwd, pkg: 'stylelint', bin: 'stylelint', args: [...(files ?? [
|
|
448
|
+
const linted = await (run ?? runLinter)({ cwd, pkg: 'stylelint', bin: 'stylelint', args: [...(files ?? [STYLE_GLOB]), '--formatter', 'json'] });
|
|
521
449
|
const appRelative = appRelativeMessages(STYLE_SIDE, cwd);
|
|
522
450
|
for (const result of linted.results ?? [])
|
|
523
451
|
for (const warning of result.warnings ?? [])
|
|
@@ -536,164 +464,6 @@ export async function cachedLinters({ repoRoot, app, opts, workspace, onFe, run,
|
|
|
536
464
|
style.ran = true;
|
|
537
465
|
}
|
|
538
466
|
}
|
|
539
|
-
|
|
467
|
+
session.report(lintFamilyReport(sides));
|
|
540
468
|
return { eslint, stylelint: style, sides };
|
|
541
469
|
}
|
|
542
|
-
/** Starts stylelint over the fe stylesheets (the running child), or returns null when no stylesheet is in scope. */
|
|
543
|
-
function launchStylelint({ repoRoot, opts, changed, existing, onFe, run }) {
|
|
544
|
-
if (opts.side === 'be')
|
|
545
|
-
return null;
|
|
546
|
-
const styles = onSide(existing, STYLE_SIDE).filter((file) => file.endsWith('.css'));
|
|
547
|
-
if (changed !== null && !styles.length)
|
|
548
|
-
return null;
|
|
549
|
-
return (run ?? runLinter)({ cwd: path.join(repoRoot, STYLE_SIDE), pkg: 'stylelint', bin: 'stylelint', args: [...(changed ? styles : [onFe ? `${onFe}/src/**/*.css` : STYLE_GLOB]), '--formatter', 'json', ...(opts.fix ? ['--fix'] : [])] });
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Run the whole lint of the app at `repoRoot`. `hfsCheck(repoRoot)` returns the `starci app check` result (`{ findings, tracked }`); the CLI
|
|
553
|
-
* injects it. Returns `{ report, sonar, exit }`; `sonar` is the merged Generic Issue Import document.
|
|
554
|
-
*/
|
|
555
|
-
export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles = () => [], runLinter: linter, cache }) {
|
|
556
|
-
const changed = opts.changed === null ? null : new Set(opts.changed);
|
|
557
|
-
const errors = [];
|
|
558
|
-
let workspace = null;
|
|
559
|
-
let validSide = true;
|
|
560
|
-
try {
|
|
561
|
-
validateSide(opts);
|
|
562
|
-
}
|
|
563
|
-
catch (error) {
|
|
564
|
-
errors.push(String(error?.message ?? error));
|
|
565
|
-
validSide = false;
|
|
566
|
-
}
|
|
567
|
-
if (opts.workspace !== undefined) {
|
|
568
|
-
try {
|
|
569
|
-
workspace = workspaceOf(opts.workspace);
|
|
570
|
-
}
|
|
571
|
-
catch (error) {
|
|
572
|
-
errors.push(String(error?.message ?? error));
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
/** Whether an app-relative path lies inside the scoped workspace. */
|
|
576
|
-
const inWorkspace = (file) => posix(file).startsWith(`${workspace}/`);
|
|
577
|
-
const existing = changed ? [...changed].filter((file) => fs.existsSync(path.join(repoRoot, file))) : [];
|
|
578
|
-
const findings = [];
|
|
579
|
-
const raw = { eslint: [], stylelint: [] };
|
|
580
|
-
const engines = {};
|
|
581
|
-
let app = null;
|
|
582
|
-
try {
|
|
583
|
-
app = readRepoDeclaration(loadSlotManifest(), repoRoot);
|
|
584
|
-
}
|
|
585
|
-
catch (error) {
|
|
586
|
-
errors.push(`starci app lint runs at the app root: ${String(error?.message ?? error)}`);
|
|
587
|
-
}
|
|
588
|
-
if (app && !app.sides) {
|
|
589
|
-
errors.push(`${repoRoot} is the ${app.side} side of an app; starci app lint runs at the app root, the folder of hfs.json`);
|
|
590
|
-
app = null;
|
|
591
|
-
}
|
|
592
|
-
if (!validSide || (opts.workspace !== undefined && workspace === null))
|
|
593
|
-
app = null;
|
|
594
|
-
if (workspace !== null && !fs.existsSync(path.join(repoRoot, workspace, 'package.json'))) {
|
|
595
|
-
errors.push(`--workspace ${workspace} holds no package.json; it is not a workspace of this app`);
|
|
596
|
-
app = null;
|
|
597
|
-
}
|
|
598
|
-
/** The workspace folder relative to the fe side folder (apps/<app> or packages/<pkg>). */
|
|
599
|
-
const onFe = workspace === null ? null : workspace.slice(STYLE_SIDE.length + 1);
|
|
600
|
-
// The linters and the app check are independent: every child starts first, the app check runs while they work, and the results are read in a fixed order.
|
|
601
|
-
// The cache serves a whole-side lint only: --changed judges named files and --fix rewrites them, so neither reads nor writes a record.
|
|
602
|
-
const cached = cache !== undefined && changed === null && !opts.fix ? cache : null;
|
|
603
|
-
const cachedRun = app && cached ? cachedLinters({ app, repoRoot, opts, workspace, onFe, run: linter, cache: cached }) : null;
|
|
604
|
-
const eslintRun = app ? (cachedRun ? cachedRun.then((answer) => answer.eslint) : eslintRepository({ app, repoRoot, opts, changed: changed ? [...changed] : null, existing, workspace, onFe, run: linter })) : null;
|
|
605
|
-
const styleLaunch = app && !cachedRun ? launchStylelint({ repoRoot, opts, changed, existing, onFe, run: linter }) : null;
|
|
606
|
-
const checkRun = app ? Promise.resolve().then(() => hfsCheck(repoRoot)).then((value) => ({ value, error: undefined }), (error) => ({ value: undefined, error })) : null;
|
|
607
|
-
// A transport refusal does not abandon a sibling child or let its rejection escape unobserved.
|
|
608
|
-
const completed = await Promise.allSettled([eslintRun, styleLaunch, checkRun]);
|
|
609
|
-
const refused = completed.filter((run) => run.status === 'rejected');
|
|
610
|
-
const failure = refused.find(run => isHostHeld(run.reason)) ?? refused[0];
|
|
611
|
-
if (failure !== undefined)
|
|
612
|
-
throw failure.reason;
|
|
613
|
-
// 1. ESLint, once per side, from the side folder with that side's config.
|
|
614
|
-
const eslint = eslintRun ? await eslintRun : { engine: { sides: {} }, errors: [], raw: [], findings: [] };
|
|
615
|
-
engines.eslint = eslint.engine;
|
|
616
|
-
errors.push(...eslint.errors);
|
|
617
|
-
raw.eslint.push(...eslint.raw);
|
|
618
|
-
findings.push(...eslint.findings);
|
|
619
|
-
// 3. stylelint over the fe side's stylesheets.
|
|
620
|
-
if (app) {
|
|
621
|
-
if (cachedRun) {
|
|
622
|
-
const answer = (await cachedRun).stylelint;
|
|
623
|
-
errors.push(...answer.errors);
|
|
624
|
-
if (answer.ran)
|
|
625
|
-
raw.stylelint = answer.results;
|
|
626
|
-
findings.push(...stylelintFindings(answer.results, repoRoot));
|
|
627
|
-
}
|
|
628
|
-
else if (styleLaunch) {
|
|
629
|
-
const linted = await styleLaunch;
|
|
630
|
-
if (linted.error)
|
|
631
|
-
errors.push(linted.error);
|
|
632
|
-
if (linted.results) {
|
|
633
|
-
const appRelative = appRelativeMessages(STYLE_SIDE, path.join(repoRoot, STYLE_SIDE));
|
|
634
|
-
for (const result of linted.results)
|
|
635
|
-
for (const warning of result.warnings ?? [])
|
|
636
|
-
warning.text = appRelative(warning.text);
|
|
637
|
-
raw.stylelint = linted.results;
|
|
638
|
-
findings.push(...stylelintFindings(linted.results, repoRoot));
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
engines.stylelint = { side: STYLE_SIDE, files: raw.stylelint.length };
|
|
642
|
-
}
|
|
643
|
-
// 2. The app check: root and sides.
|
|
644
|
-
let checked = { findings: [] };
|
|
645
|
-
if (checkRun) {
|
|
646
|
-
const settled = await checkRun;
|
|
647
|
-
if (isNodeExecutionRefusal(settled.error))
|
|
648
|
-
throw settled.error;
|
|
649
|
-
if (settled.error)
|
|
650
|
-
errors.push(`starci app check could not run: ${String(settled.error?.message ?? settled.error)}`);
|
|
651
|
-
else
|
|
652
|
-
checked = settled.value;
|
|
653
|
-
}
|
|
654
|
-
const repoErrors = checked.findings.filter((finding) => finding.level === 'error' && findingOnSide(finding, opts.side));
|
|
655
|
-
// A workspace lint keeps only the app findings inside the workspace; the rest belong to the root lint of the whole app.
|
|
656
|
-
const kept = workspace !== null
|
|
657
|
-
? repoErrors.filter((finding) => finding.path && inWorkspace(finding.path))
|
|
658
|
-
: changed === null ? repoErrors : repoErrors.filter((finding) => !finding.path || changed.has(posix(finding.path)));
|
|
659
|
-
for (const finding of kept) {
|
|
660
|
-
findings.push({ engine: 'hfs', rule: finding.code, code: finding.code, severity: 'error', path: finding.path ? posix(finding.path) : null, line: finding.line ?? null, column: finding.column ?? null, message: finding.message, titleVi: finding.titleVi, nextStepVi: finding.nextStepVi });
|
|
661
|
-
}
|
|
662
|
-
engines.hfs = { findings: kept.length };
|
|
663
|
-
findings.sort(byLocation);
|
|
664
|
-
let properties = '';
|
|
665
|
-
try {
|
|
666
|
-
properties = fs.readFileSync(path.join(repoRoot, 'sonar-project.properties'), 'utf8');
|
|
667
|
-
}
|
|
668
|
-
catch { /* no properties: every finding keeps its own path */ }
|
|
669
|
-
const sourceRoots = sourceRootsOf(properties).filter((root) => findingOnSide({ path: root }, opts.side));
|
|
670
|
-
const tracked = sourceRoots.length ? trackedFiles(repoRoot) : [];
|
|
671
|
-
const sonar = mergeReports([
|
|
672
|
-
sonarReport(kept, { sourceRoots, tracked }),
|
|
673
|
-
linterReport('eslint', raw.eslint, { root: repoRoot, sourceRoots, tracked }),
|
|
674
|
-
linterReport('stylelint', raw.stylelint, { root: repoRoot, sourceRoots, tracked }),
|
|
675
|
-
]);
|
|
676
|
-
// Green under the threshold (gate-threshold.mjs): every finding is still reported, only the verdict changes. A warning is a
|
|
677
|
-
// rule of the canon's counting cohort (eslint-canon-fe COUNTING): reported and counted per rule, never part of the verdict.
|
|
678
|
-
const failing = findings.filter((finding) => finding.severity !== 'warning');
|
|
679
|
-
const counting = {};
|
|
680
|
-
for (const finding of findings)
|
|
681
|
-
if (finding.severity === 'warning')
|
|
682
|
-
counting[finding.rule] = (counting[finding.rule] ?? 0) + 1;
|
|
683
|
-
const gate = gateVerdict(failing);
|
|
684
|
-
const report = { schema: LINT_SCHEMA, ok: gate.pass && errors.length === 0, repoRoot, changed: changed ? [...changed].sort(byCodeUnit) : null, workspace, ...(opts.side === undefined ? {} : { side: opts.side }), counts: { error: failing.length, warning: findings.length - failing.length }, counting, engines, errors, findings, gate };
|
|
685
|
-
return { report, sonar, exit: errors.length ? 2 : gate.pass ? 0 : 1 };
|
|
686
|
-
}
|
|
687
|
-
export function printLintText(report, out) {
|
|
688
|
-
for (const f of report.findings)
|
|
689
|
-
out(`${f.path ?? '-'}${f.line ? `:${f.line}${f.column ? `:${f.column}` : ''}` : ''} ${f.severity === 'warning' ? 'warn ' : ''}${f.engine}/${f.rule} ${f.message}\n`);
|
|
690
|
-
for (const e of report.errors)
|
|
691
|
-
out(`ERROR ${e}\n`);
|
|
692
|
-
const per = Object.keys(report.engines).map((engine) => `${engine} ${report.findings.filter((f) => f.engine === engine && f.severity !== 'warning').length}`).join(', ');
|
|
693
|
-
out(`\nstarci app lint: ${report.counts.error} finding${report.counts.error === 1 ? '' : 's'} (${per})${report.errors.length ? `, ${report.errors.length} tool error${report.errors.length === 1 ? '' : 's'}` : ''}\n`);
|
|
694
|
-
const counting = Object.entries((report.counting ?? {})).sort(([a], [b]) => a.localeCompare(b));
|
|
695
|
-
if (counting.length)
|
|
696
|
-
out(`counting (warn, not in the verdict): ${counting.map(([rule, count]) => `${rule} ${count}`).join(', ')}\n`);
|
|
697
|
-
if (report.gate?.line)
|
|
698
|
-
out(`${report.gate.line}\n`);
|
|
699
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { VerbContext } from './index.mjs';
|
|
2
2
|
/**
|
|
3
|
-
* Runs `starci app lint --side <side> --out <file>` for every side the app has, then writes reports/lint.sonar.json.
|
|
3
|
+
* Runs `starci app check --gate lint --side <side> --out <file>` for every side the app has, then writes reports/lint.sonar.json.
|
|
4
4
|
* `launch` turns an argv into [command, args] for the platform. Returns { pass, text }: the honest log of every lint run and the verdict.
|
|
5
5
|
* A lint that exits non-zero (findings or error) or writes no report fails the row; nothing is skipped.
|
|
6
6
|
*/
|