@starci/hfs 5.0.7 → 5.1.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/CHANGELOG.md +21 -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 +88 -11
- package/cache/lint-family.d.mts +10 -3
- package/cache/lint-family.mjs +65 -37
- 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/directive-first.d.mts +48 -0
- package/codemod/directive-first.mjs +136 -0
- package/codemod/explicit-surface-edit.d.mts +14 -0
- package/codemod/explicit-surface-edit.mjs +57 -0
- package/codemod/explicit-surface-names.d.mts +30 -0
- package/codemod/explicit-surface-names.mjs +135 -0
- package/codemod/explicit-surface-render.d.mts +29 -0
- package/codemod/explicit-surface-render.mjs +197 -0
- package/codemod/explicit-surface.d.mts +50 -0
- package/codemod/explicit-surface.mjs +374 -0
- package/codemod/main.d.mts +7 -5
- package/codemod/main.mjs +37 -16
- package/codemod/small-renames.mjs +22 -2
- package/codemod/source-tree.d.mts +8 -0
- package/codemod/source-tree.mjs +23 -0
- package/lint/eslint-list.cjs +55 -0
- package/lint/run.d.mts +35 -61
- package/lint/run.mjs +91 -288
- 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 +8 -8
- package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +56 -5
- 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/architecture/test-world-files.d.mts +4 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +26 -7
- package/runtime/scripts/hfs/check.d.mts +16 -1
- package/runtime/scripts/hfs/check.mjs +18 -7
- package/runtime/scripts/hfs/explicit-surface-rule.d.mts +58 -0
- package/runtime/scripts/hfs/explicit-surface-rule.mjs +59 -0
- 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/declaration-surface.d.mts +35 -0
- package/runtime/scripts/lib/declaration-surface.mjs +112 -0
- 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/templates/be/inbox/delivery-recorded.event.ts.tpl +2 -2
- package/templates/be/inbox/entities.index.ts.tpl +1 -1
- package/templates/be/inbox/module-definition.ts.tpl +8 -2
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +10 -2
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +8 -2
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +3 -2
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -3
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +3 -2
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +7 -6
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/jobs/job.processor.ts.tpl +1 -1
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +5 -4
- package/templates/be/patterns/projections/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -3
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/reactors/consumer.ts.tpl +1 -1
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +1 -1
- package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +1 -1
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +9 -3
- package/templates/be/platform/graphql/graphql.module-definition.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +9 -3
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +3 -2
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +2 -1
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +8 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/http-security/persistence/connection.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/http-security/persistence/rate-limit.sql.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +8 -3
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/http-security/persistence/entities/index.ts +1 -1
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +7 -1
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +7 -1
- package/templates/be/table/api/feature.module-definition.ts.tpl +9 -3
- package/templates/be/table/api/graphql.module-definition.ts.tpl +9 -3
- package/templates/be/table/entities.index.ts.tpl +1 -1
- package/templates/be/table/module-definition.ts.tpl +8 -3
- package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +1 -0
- package/templates/be/test-world/world/test-world.config.ts.tpl +8 -1
- package/templates/fe/common/app-layout.tsx.partial +1 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +2 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +11 -3
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +8 -2
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +2 -1
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +1 -1
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +4 -1
- package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +3 -2
- package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +12 -4
- package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +8 -3
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +4 -3
- package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +1 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +2 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -1
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +4 -3
- package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +2 -1
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +19 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +5 -1
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +4 -3
- package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +3 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +5 -1
- package/templates/fe/table/graphql/useMutateSwr.ts.tpl +2 -1
- package/templates/fe/table/graphql/useQuerySwr.ts.tpl +2 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/no-auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/error.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/http.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/refresh-token.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/retry.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/timeout.ts +1 -1
- package/templates/fe/units/page/index.tsx.tpl +3 -2
- package/templates/fe/units/slot/SlotView.tsx.tpl +1 -1
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,25 @@ 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));
|
|
48
|
+
/** The snapshots an ESLint child wrote, by canon name; undefined when it wrote none or a file that is not a snapshot list (the cache then builds the graph itself). */
|
|
49
|
+
function readGraphSnapshots(file) {
|
|
50
|
+
try {
|
|
51
|
+
const written = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
52
|
+
const snapshots = written?.snapshots;
|
|
53
|
+
if (snapshots === null || typeof snapshots !== 'object' || Array.isArray(snapshots))
|
|
54
|
+
return undefined;
|
|
55
|
+
const sound = Object.values(snapshots).every((snapshot) => snapshot !== null && typeof snapshot === 'object' && snapshot.byFile !== null && typeof snapshot.byFile === 'object' && !Array.isArray(snapshot.byFile)
|
|
56
|
+
&& Object.values(snapshot.byFile).every((findings) => Array.isArray(findings)));
|
|
57
|
+
return sound ? snapshots : undefined;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
122
63
|
/** 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) {
|
|
64
|
+
export async function runLinter({ cwd, pkg, bin, args, bound = null, list, files, graph }, execute) {
|
|
124
65
|
let entry;
|
|
125
66
|
try {
|
|
126
67
|
const manifest = createRequire(path.join(cwd, 'package.json')).resolve(`${pkg}/package.json`);
|
|
@@ -131,7 +72,27 @@ export async function runLinter({ cwd, pkg, bin, args, bound = null }, execute)
|
|
|
131
72
|
return { error: `${pkg} is not installed for ${cwd}` };
|
|
132
73
|
}
|
|
133
74
|
const heap = pkg === 'eslint' ? eslintHeapArgs() : [];
|
|
134
|
-
|
|
75
|
+
// A named list, or a run that hands its project graph out, goes through the list runner: the job is a file it reads (ESLint lints all of it in one
|
|
76
|
+
// process, no command line holds a path), the whole side is the list `.`, and the graph snapshots come back in a file beside the job.
|
|
77
|
+
const listed = pkg === 'eslint' && (list !== undefined || graph !== undefined) ? makeTempDir('starci-eslint-') : null;
|
|
78
|
+
let childArgs = [entry, ...args];
|
|
79
|
+
const snapshotFile = listed === null || graph === undefined ? null : path.join(listed, 'graph.json');
|
|
80
|
+
if (listed !== null) {
|
|
81
|
+
const job = path.join(listed, 'job.json');
|
|
82
|
+
fs.writeFileSync(job, JSON.stringify({ files: list ?? ['.'], fix: args.includes('--fix'), ...(snapshotFile === null || graph === undefined ? {} : { graph: { out: snapshotFile, canons: graph } }) }));
|
|
83
|
+
childArgs = [LIST_RUNNER, job];
|
|
84
|
+
}
|
|
85
|
+
const count = list?.length ?? files;
|
|
86
|
+
let run;
|
|
87
|
+
let handed;
|
|
88
|
+
try {
|
|
89
|
+
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);
|
|
90
|
+
handed = snapshotFile === null ? undefined : readGraphSnapshots(snapshotFile);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (listed !== null)
|
|
94
|
+
fs.rmSync(listed, { recursive: true, force: true });
|
|
95
|
+
}
|
|
135
96
|
// ESLint prints its report on stdout; stylelint uses stderr. JSON does not prove a completed child: only exits 0/1
|
|
136
97
|
// without a spawn error or signal are measurable, and exit 1 must carry findings.
|
|
137
98
|
let results;
|
|
@@ -145,17 +106,12 @@ export async function runLinter({ cwd, pkg, bin, args, bound = null }, execute)
|
|
|
145
106
|
return { error: `${pkg} produced a report that is not a result list` };
|
|
146
107
|
const completed = !run.error && !run.signal && [0, 1].includes(run.status)
|
|
147
108
|
&& (run.status === 0 || results.some((result) => result?.messages?.length || result?.warnings?.length));
|
|
148
|
-
return { results, ...(completed ? {} : { error: `${pkg} could not complete in ${path.basename(cwd)}/ (exit ${run.status}, signal ${run.signal ?? 'none'}): ${String(run.error?.message || run.stderr || run.stdout || '').trim().split('\n')[0]}` }) };
|
|
109
|
+
return { results, ...(handed === undefined || !completed ? {} : { graph: handed }), ...(completed ? {} : { error: `${pkg} could not complete in ${path.basename(cwd)}/ (exit ${run.status}, signal ${run.signal ?? 'none'}): ${String(run.error?.message || run.stderr || run.stdout || '').trim().split('\n')[0]}` }) };
|
|
149
110
|
}
|
|
150
111
|
const eslintFindings = (results, appRoot) => results.flatMap((result) => (result.messages ?? []).map((message) => ({
|
|
151
112
|
engine: 'eslint', rule: message.ruleId ?? 'eslint-error', code: CODE_PREFIX.exec(message.message ?? '')?.[1] ?? null, severity: message.severity === 1 ? 'warning' : 'error',
|
|
152
113
|
path: posix(path.relative(appRoot, result.filePath)), line: message.line ?? null, column: message.column ?? null, message: message.message,
|
|
153
114
|
})));
|
|
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
115
|
/** The files of `changed` (app-relative) below `side`/, relative to the side folder. */
|
|
160
116
|
const onSide = (changed, side) => changed.filter((file) => file.startsWith(`${side}/`)).map((file) => file.slice(side.length + 1));
|
|
161
117
|
const fileKey = (file) => process.platform === 'win32' ? path.resolve(file).toLowerCase() : path.resolve(file);
|
|
@@ -276,7 +232,7 @@ export async function installedLintConfig({ repoRoot, cwd, side }) {
|
|
|
276
232
|
return [...config, ...familyConfig];
|
|
277
233
|
}
|
|
278
234
|
/** Read both installed canons' source selectors, then enumerate that scope; unavailable selectors cannot prove coverage. */
|
|
279
|
-
async function sourceFiles({ repoRoot, cwd, side
|
|
235
|
+
async function sourceFiles({ repoRoot, cwd, side }) {
|
|
280
236
|
const config = await installedLintConfig({ repoRoot, cwd, side });
|
|
281
237
|
const scope = canonScope(config);
|
|
282
238
|
const files = [];
|
|
@@ -294,11 +250,11 @@ async function sourceFiles({ repoRoot, cwd, side, scope: folderScope }) {
|
|
|
294
250
|
files.push(file);
|
|
295
251
|
}
|
|
296
252
|
};
|
|
297
|
-
visit(
|
|
253
|
+
visit('');
|
|
298
254
|
return files.sort(byCodeUnit);
|
|
299
255
|
}
|
|
300
256
|
/** 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,
|
|
257
|
+
function checkedEslintResults({ results, expected, cwd, changed }) {
|
|
302
258
|
const required = new Set(expected.map((file) => fileKey(path.join(cwd, file))));
|
|
303
259
|
const seen = new Set(), valid = [], errors = [];
|
|
304
260
|
for (const result of results) {
|
|
@@ -309,7 +265,7 @@ function checkedEslintResults({ results, expected, cwd, scope, changed }) {
|
|
|
309
265
|
continue;
|
|
310
266
|
}
|
|
311
267
|
const key = fileKey(result.filePath);
|
|
312
|
-
if (!inside(cwd, result.filePath) || (
|
|
268
|
+
if (!inside(cwd, result.filePath) || (changed && !required.has(key))) {
|
|
313
269
|
errors.push(`eslint produced a result outside the requested scope: ${result.filePath}`);
|
|
314
270
|
continue;
|
|
315
271
|
}
|
|
@@ -325,22 +281,26 @@ function checkedEslintResults({ results, expected, cwd, scope, changed }) {
|
|
|
325
281
|
errors.push(`eslint produced no result for requested source files: ${missing.join(', ')}`);
|
|
326
282
|
return { results: valid, errors };
|
|
327
283
|
}
|
|
328
|
-
const ESLINT_ARGV_BUDGET = 24_000;
|
|
329
|
-
const ESLINT_CONCURRENCY = 3;
|
|
330
284
|
/** 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
285
|
const spawnFailure = (error) => {
|
|
332
286
|
if (isNodeExecutionRefusal(error))
|
|
333
287
|
throw error;
|
|
334
288
|
return { error: `could not start eslint: ${String(error?.message ?? error)}` };
|
|
335
289
|
};
|
|
290
|
+
/** The installed canon of `side`, which publishes the project-graph rules, as the child imports it; undefined when it cannot be resolved (the cache then builds the graph itself). */
|
|
291
|
+
function graphCanonsOf(repoRoot, cwd, side) {
|
|
292
|
+
const name = `@starci/eslint-canon-${side}`;
|
|
293
|
+
try {
|
|
294
|
+
return [{ name, entry: ownCanonEntry({ repoRoot, cwd }, name) }];
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
336
300
|
/** 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,
|
|
301
|
+
async function launchEslint({ app, repoRoot, opts, changed, existing, run = runLinter, scopes = {}, handOutGraph = false }) {
|
|
338
302
|
const launches = [];
|
|
339
303
|
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
304
|
const sources = onSide(existing, side).filter((file) => /\.(?:[cm]?[jt]sx?)$/.test(file));
|
|
345
305
|
if (changed !== null && !sources.length) {
|
|
346
306
|
launches.push({ side, skipped: 'no changed source file' });
|
|
@@ -348,28 +308,32 @@ async function launchEslint({ app, repoRoot, opts, changed, existing, workspace,
|
|
|
348
308
|
}
|
|
349
309
|
const cwd = path.join(repoRoot, side);
|
|
350
310
|
let expected;
|
|
311
|
+
// 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
|
|
312
|
+
const known = scopes[side];
|
|
351
313
|
try {
|
|
352
|
-
expected = changed ? sources : await sourceFiles({ repoRoot, cwd, side
|
|
314
|
+
expected = changed ? sources : known !== undefined ? [...known] : await sourceFiles({ repoRoot, cwd, side });
|
|
353
315
|
}
|
|
354
316
|
catch (error) {
|
|
355
317
|
launches.push({ side, scopeError: `eslint source scope is unavailable in ${side}/: ${String(error?.message ?? error)}` });
|
|
356
318
|
continue;
|
|
357
319
|
}
|
|
358
320
|
const head = ['--format', 'json', ...(opts.fix ? ['--fix'] : [])];
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
321
|
+
// the child that builds the canon's project graph for its rules hands the snapshot out when the caller keeps the graph findings (the cache)
|
|
322
|
+
const graph = handOutGraph ? graphCanonsOf(repoRoot, cwd, side) : undefined;
|
|
323
|
+
// 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
|
|
324
|
+
// child would build the whole TypeScript program again); the whole side is a single directory argument.
|
|
325
|
+
const launch = Promise.resolve().then(() => run(changed ? { cwd, pkg: 'eslint', bin: 'eslint', args: head, list: sources, bound: repoRoot, ...(graph === undefined ? {} : { graph }) } : { cwd, pkg: 'eslint', bin: 'eslint', args: [...head, '.'], files: expected.length, bound: repoRoot, ...(graph === undefined ? {} : { graph }) }));
|
|
326
|
+
launches.push({ side, cwd, expected, run: launch.catch(spawnFailure) });
|
|
363
327
|
}
|
|
364
328
|
return launches;
|
|
365
329
|
}
|
|
366
|
-
/** Execute each side's installed ESLint config once;
|
|
367
|
-
export async function eslintRepository({ repoRoot, app = readRepoDeclaration(loadSlotManifest(), repoRoot), opts = {}, changed = null, existing = [],
|
|
368
|
-
const findings = [], errors = [], raw = [];
|
|
330
|
+
/** 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. */
|
|
331
|
+
export async function eslintRepository({ repoRoot, app = readRepoDeclaration(loadSlotManifest(), repoRoot), opts = {}, changed = null, existing = [], run, scopes, handOutGraph }) {
|
|
332
|
+
const findings = [], errors = [], raw = [], graphs = {};
|
|
369
333
|
const engine = { sides: {} };
|
|
370
334
|
if (!app.sides)
|
|
371
335
|
throw new Error('ESLint runs at the app root, the folder of hfs.json');
|
|
372
|
-
const launches = await launchEslint({ app, repoRoot, opts, changed, existing,
|
|
336
|
+
const launches = await launchEslint({ app, repoRoot, opts, changed, existing, run, ...(scopes === undefined ? {} : { scopes }), ...(handOutGraph === true ? { handOutGraph } : {}) });
|
|
373
337
|
const settled = await Promise.allSettled(launches.map(launch => launch.run ?? null));
|
|
374
338
|
const rejected = settled.filter((run) => run.status === 'rejected');
|
|
375
339
|
const failed = rejected.find(run => isHostHeld(run.reason)) ?? rejected[0];
|
|
@@ -391,7 +355,7 @@ export async function eslintRepository({ repoRoot, app = readRepoDeclaration(loa
|
|
|
391
355
|
if (linted.error)
|
|
392
356
|
errors.push(linted.error);
|
|
393
357
|
if (linted.results) {
|
|
394
|
-
const checked = checkedEslintResults({ results: linted.results, expected: launch.expected, cwd: launch.cwd,
|
|
358
|
+
const checked = checkedEslintResults({ results: linted.results, expected: launch.expected, cwd: launch.cwd, changed: changed !== null });
|
|
395
359
|
errors.push(...checked.errors);
|
|
396
360
|
linted.results = checked.results;
|
|
397
361
|
const appRelative = appRelativeMessages(side, path.join(repoRoot, side));
|
|
@@ -402,8 +366,10 @@ export async function eslintRepository({ repoRoot, app = readRepoDeclaration(loa
|
|
|
402
366
|
findings.push(...eslintFindings(linted.results, repoRoot));
|
|
403
367
|
}
|
|
404
368
|
engine.sides[side] = { files: linted.results?.length ?? 0 };
|
|
369
|
+
if (linted.graph !== undefined)
|
|
370
|
+
graphs[side] = linted.graph;
|
|
405
371
|
});
|
|
406
|
-
return { findings, errors, raw, engine };
|
|
372
|
+
return { findings, errors, raw, engine, graphs };
|
|
407
373
|
}
|
|
408
374
|
// ------------------------------------------------------------------------------------------------ the cached lint
|
|
409
375
|
/** A `crossFileInputs` export that has the shape the cache reads, or null (an installed canon without it, or with a broken one, is linted whole). */
|
|
@@ -483,22 +449,18 @@ export function gitBaseToken(repoRoot) {
|
|
|
483
449
|
* validation of a full run (eslintRepository), the rest take their records. The answer has the shape of eslintRepository's, plus the stylelint
|
|
484
450
|
* results, so the lint report is built the same way from either.
|
|
485
451
|
*/
|
|
486
|
-
export async function cachedLinters({ repoRoot, app,
|
|
452
|
+
export async function cachedLinters({ repoRoot, app, side: only, run, session }) {
|
|
487
453
|
const eslint = { findings: [], errors: [], raw: [], engine: { sides: {} } };
|
|
488
454
|
const style = { results: [], errors: [], ran: false };
|
|
489
455
|
const sides = [];
|
|
490
|
-
const decision =
|
|
491
|
-
const full =
|
|
456
|
+
const decision = session.decide('lint');
|
|
457
|
+
const full = decision.mode === 'full' ? decision.reason : null;
|
|
492
458
|
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
|
-
}
|
|
459
|
+
for (const side of SIDES.filter((candidate) => only === undefined || only === candidate)) {
|
|
498
460
|
const cwd = path.join(repoRoot, side);
|
|
499
461
|
let scope, canons;
|
|
500
462
|
try {
|
|
501
|
-
scope = await sourceFiles({ repoRoot, cwd, side
|
|
463
|
+
scope = await sourceFiles({ repoRoot, cwd, side });
|
|
502
464
|
canons = await canonDeclarations({ repoRoot, cwd, side });
|
|
503
465
|
}
|
|
504
466
|
catch (error) {
|
|
@@ -508,16 +470,15 @@ export async function cachedLinters({ repoRoot, app, opts, workspace, onFe, run,
|
|
|
508
470
|
}
|
|
509
471
|
const styled = side === STYLE_SIDE;
|
|
510
472
|
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,
|
|
473
|
+
session, side, sideRoot: cwd, scope, stylesheets: styled ? stylesheetsOf(cwd) : [],
|
|
474
|
+
canons, styleCanon: styled ? await styleDeclaration({ repoRoot, cwd }) : null, typescript: () => loadAppTypeScript(cwd), extraKey: '', gitToken, full, write: true,
|
|
514
475
|
eslint: async (files) => {
|
|
515
476
|
const listed = files === null ? null : files.map((file) => `${side}/${file}`);
|
|
516
|
-
const sub = await eslintRepository({ repoRoot, app, opts: { side }, changed: listed, existing: listed ?? [],
|
|
517
|
-
return { results: sub.raw, errors: sub.errors };
|
|
477
|
+
const sub = await eslintRepository({ repoRoot, app, opts: { side }, changed: listed, existing: listed ?? [], run, scopes: { [side]: scope }, handOutGraph: true });
|
|
478
|
+
return { results: sub.raw, errors: sub.errors, ...(sub.graphs[side] === undefined ? {} : { graph: sub.graphs[side] }) };
|
|
518
479
|
},
|
|
519
480
|
stylelint: styled ? async (files) => {
|
|
520
|
-
const linted = await (run ?? runLinter)({ cwd, pkg: 'stylelint', bin: 'stylelint', args: [...(files ?? [
|
|
481
|
+
const linted = await (run ?? runLinter)({ cwd, pkg: 'stylelint', bin: 'stylelint', args: [...(files ?? [STYLE_GLOB]), '--formatter', 'json'] });
|
|
521
482
|
const appRelative = appRelativeMessages(STYLE_SIDE, cwd);
|
|
522
483
|
for (const result of linted.results ?? [])
|
|
523
484
|
for (const warning of result.warnings ?? [])
|
|
@@ -536,164 +497,6 @@ export async function cachedLinters({ repoRoot, app, opts, workspace, onFe, run,
|
|
|
536
497
|
style.ran = true;
|
|
537
498
|
}
|
|
538
499
|
}
|
|
539
|
-
|
|
500
|
+
session.report(lintFamilyReport(sides));
|
|
540
501
|
return { eslint, stylelint: style, sides };
|
|
541
502
|
}
|
|
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
|
*/
|
package/release/lint-report.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// lint-report.mjs - the external issues report the release gate's sonar row produces before it runs the scanner.
|
|
2
|
-
// The app declares `sonar.externalIssuesReportPaths=reports/lint.sonar.json`; the managed CI makes it with `lint --side <side> --out reports/lint.<side>.sonar.json`
|
|
2
|
+
// The app declares `sonar.externalIssuesReportPaths=reports/lint.sonar.json`; the managed CI makes it with `app check --gate lint --side <side> --out reports/lint.<side>.sonar.json`
|
|
3
3
|
// per lint matrix side and a "merge Sonar lint reports" step. This module does the same locally, through the same process seam as every gate
|
|
4
4
|
// (an argument array through ctx.spawn, never a shell). The merge below is the one in packages/hfs/sync/ci-commands.mjs (sonarMergeSteps): keep them identical.
|
|
5
5
|
import fs from 'node:fs';
|
|
@@ -17,7 +17,7 @@ function mergeSonarReports(reports) {
|
|
|
17
17
|
/** The sides the app has: a be/ or fe/ folder beside the app root. */
|
|
18
18
|
const sidesOf = (root) => LINT_SIDES.filter((side) => fs.existsSync(path.join(root, side)));
|
|
19
19
|
/**
|
|
20
|
-
* Runs `starci app lint --side <side> --out <file>` for every side the app has, then writes reports/lint.sonar.json.
|
|
20
|
+
* Runs `starci app check --gate lint --side <side> --out <file>` for every side the app has, then writes reports/lint.sonar.json.
|
|
21
21
|
* `launch` turns an argv into [command, args] for the platform. Returns { pass, text }: the honest log of every lint run and the verdict.
|
|
22
22
|
* A lint that exits non-zero (findings or error) or writes no report fails the row; nothing is skipped.
|
|
23
23
|
*/
|
|
@@ -30,7 +30,7 @@ export function produceLintReports(ctx, { launch, platform, env }) {
|
|
|
30
30
|
for (const side of sides) {
|
|
31
31
|
const file = path.join(ctx.cwd, sideReport(side));
|
|
32
32
|
fs.rmSync(file, { force: true });
|
|
33
|
-
const argv = ['npx', 'starci', 'app', 'lint', '--side', side, '--out', sideReport(side)];
|
|
33
|
+
const argv = ['npx', 'starci', 'app', 'check', '--gate', 'lint', '--side', side, '--out', sideReport(side)];
|
|
34
34
|
const [command, args] = launch(argv, platform);
|
|
35
35
|
out.push(`$ ${argv.join(' ')}\n`);
|
|
36
36
|
const run = tool(ctx, command, args, { cwd: ctx.cwd, env });
|
package/report/sonar.d.mts
CHANGED
|
@@ -48,7 +48,7 @@ interface CheckFinding {
|
|
|
48
48
|
line?: number;
|
|
49
49
|
}
|
|
50
50
|
type LinterResult = any;
|
|
51
|
-
/** One document of several (the starci-hfs, eslint and stylelint documents of one app
|
|
51
|
+
/** One document of several (the starci-hfs, eslint and stylelint documents of one app check): rules by id, issues in the one sort order. */
|
|
52
52
|
export declare function mergeReports(reports: readonly SonarDocument[]): SonarDocument;
|
|
53
53
|
/**
|
|
54
54
|
* The Generic Issue Import document of a check's findings (the `findings` of checkRepository, each with the catalog's
|
package/report/sonar.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// Sonar documents of the
|
|
1
|
+
// Sonar documents of the lint gate of `starci app check` (`--out`): the building blocks that turn the
|
|
2
2
|
// findings of the StarCi canon into a Sonar Generic Issue Import document (SonarQube 10.3+ format: `{ rules, issues }`).
|
|
3
3
|
// sonarReport(findings) the repository findings of `starci app check`, engineId `starci-hfs`, rule id = the finding code
|
|
4
4
|
// linterReport(kind, results) a linter's own json output (`eslint -f json`, `stylelint --formatter json`), engineId `eslint` / `stylelint`,
|
|
5
5
|
// rule id = the linter's rule. Sonar's own ESLint import (sonar.eslint.reportPaths) is not used: it drops an
|
|
6
6
|
// issue on a file outside sonar.sources, and a stylelint result has no native import at all.
|
|
7
|
-
// mergeReports(reports) the documents of one `starci app
|
|
7
|
+
// mergeReports(reports) the documents of one `starci app check` run as ONE file
|
|
8
8
|
// One placement rule for every engine: Sonar imports an issue only on a file it indexes (a tracked source or stylesheet under
|
|
9
9
|
// sonar.sources). A finding on any other path (hfs.json, a workflow, a package under an unindexed root, e2e/, a directory) is filed
|
|
10
10
|
// on the first source file of sonar.sources and its message names the real path, so no finding is dropped.
|
|
@@ -33,7 +33,7 @@ function document(rules, issues) {
|
|
|
33
33
|
const used = new Set(sorted.map((issue) => issue.ruleId));
|
|
34
34
|
return { rules: [...rules.values()].filter((rule) => used.has(rule.id)).sort((a, b) => cmp(a.id, b.id)), issues: sorted };
|
|
35
35
|
}
|
|
36
|
-
/** One document of several (the starci-hfs, eslint and stylelint documents of one app
|
|
36
|
+
/** One document of several (the starci-hfs, eslint and stylelint documents of one app check): rules by id, issues in the one sort order. */
|
|
37
37
|
export function mergeReports(reports) {
|
|
38
38
|
const rules = new Map(reports.flatMap((report) => report.rules).map((rule) => [rule.id, rule]));
|
|
39
39
|
return document(rules, reports.flatMap((report) => report.issues));
|