@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/src/main.mjs
CHANGED
|
@@ -8,7 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
// The @starci/hfs implementation behind `starci app`: one repository, `<app>/`, with one package.json, lockfile and node_modules
|
|
10
10
|
// at its root and two sides, be/ and fe/, declared by the one hfs.json of kind app. Every verb runs at the app root.
|
|
11
|
-
// starci app check [--cwd <dir>] [--json] [--gate <hfs|format|db|secrets|wiring|types|lint|test|sonar|codecov>]... [--no-cache] [--coverage-from <lcov>] [--shard <i>/<n>] | check --staged
|
|
11
|
+
// starci app check [--cwd <dir>] [--json] [--gate <hfs|format|db|secrets|wiring|types|lint|test|sonar|codecov>]... [--side be|fe] [--fix] [--out <sonar file>] [--no-cache] [--coverage-from <lcov>] [--shard <i>/<n>] | check --staged
|
|
12
12
|
// every tracked path has a slot; required files exist; nothing forbidden or
|
|
13
13
|
// tracked-that-must-be-ignored; pins match; every managed file equals its render
|
|
14
14
|
// (sync/managed.mjs, the .gitignore block and sonar-project.properties included); no empty or ghost
|
|
@@ -29,17 +29,14 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
29
29
|
// progress goes to stderr (--quiet silences it). STARCI_APP_CHECK_CACHE=verify runs diff then full and fails (exit 2)
|
|
30
30
|
// on any difference; =full never reads the cache (the release gate).
|
|
31
31
|
// --coverage-from <lcov> judges a coverage report produced elsewhere (the merge of a sharded unit run) and records nothing.
|
|
32
|
+
// --side be|fe limits the lint gate to one side (a CI matrix runs one per side). --out <file> writes every finding of the run as ONE
|
|
33
|
+
// Sonar Generic Issue Import document (engine ids starci-hfs, eslint, stylelint). --fix runs ESLint and stylelint with --fix before the
|
|
34
|
+
// check and is refused unless --gate lint is selected.
|
|
32
35
|
// --staged: the pre-commit guard alone (sync/hygiene.mjs: staged .starciwork and .starcistacks paths, the secrets
|
|
33
36
|
// guard and the bypass guard over every staged file read from the index, a new back-end logic file staged
|
|
34
37
|
// without its unit spec); it takes no other flag.
|
|
35
38
|
// Exit 1 at 20 error findings or a hard blocker (gate-threshold.mjs). The root checks run once and
|
|
36
39
|
// the side checks and the machine once per side, the side folder as their root; every path is app-relative.
|
|
37
|
-
// starci app lint [--cwd <dir>] [--changed <file>...] [--workspace <dir>] [--side be|fe] [--fix] [--full] [--json] [--out <file>]
|
|
38
|
-
// the ONE lint entry (npm run lint): ESLint per side with that side's canon config (per-file rules and
|
|
39
|
-
// the project-graph rules), this check's findings, and stylelint over the fe side, as one starci/lint@1
|
|
40
|
-
// report; --out writes the one Sonar import file. Exit 0 under 20 error findings and no hard blocker, 1 otherwise, 2 a tool could not run (lint/run.mjs).
|
|
41
|
-
// --workspace <dir> (from the current directory) scopes the same lint to one fe workspace: the
|
|
42
|
-
// `lint` script of fe/apps/<app> and fe/packages/<pkg>, run by the turbo lint task.
|
|
43
40
|
// starci app scaffold <name> [--cwd <dir>] a new app <dir>/<name>/ (<dir> is the global --cwd): the root (hfs.json, package.json, managed files, .starciwork) and the
|
|
44
41
|
// be/ and fe/ skeletons (scaffold/app.mjs). Accepts an empty unborn main Git root.
|
|
45
42
|
// starci app slot <path> [--cwd <dir>] [--json] which slot owns the path, its tier, allowed imports, required tests.
|
|
@@ -80,7 +77,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
80
77
|
// one front-end unit in the shape-slot pattern from templates/fe/units (scaffold/add-fe-unit.mjs); the
|
|
81
78
|
// first block writes the app's one copy of the status owners. Never overwrites a unit.
|
|
82
79
|
// The flags of every verb are those of USAGE below, generated from the parser tables that refuse every other flag.
|
|
83
|
-
// Every finding names a why code and carries its Vietnamese text. check
|
|
80
|
+
// Every finding names a why code and carries its Vietnamese text. check and slot read the app, never write to it. Exit codes:
|
|
84
81
|
// 0 clean, 1 error findings, 2 a refusal or bad usage.
|
|
85
82
|
import fs from 'node:fs';
|
|
86
83
|
import { isHostHeld } from '../runtime/scripts/api/node/run-node.mjs';
|
|
@@ -96,17 +93,17 @@ import { dbBaselineMain } from '../runtime/scripts/hfs/db-baseline-verb.mjs';
|
|
|
96
93
|
import { dbRestampMain } from '../runtime/scripts/hfs/db-restamp-verb.mjs';
|
|
97
94
|
import { formatCoverageReport } from '../runtime/scripts/hfs/coverage-from.mjs';
|
|
98
95
|
import { gateVerdict } from '../runtime/scripts/hfs/gate-threshold.mjs';
|
|
99
|
-
import { HFS_DECLARATION_FILE, HfsSlotsError, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
96
|
+
import { HFS_DECLARATION_FILE, HfsSlotsError, SIDES, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
100
97
|
import { main as syncMain } from '../sync/cli.mjs';
|
|
101
98
|
import { runWorkHygiene } from '../sync/hygiene.mjs';
|
|
102
|
-
import { SyncError, bundledPresets } from '../sync/index.mjs';
|
|
99
|
+
import { STYLE_GLOB, SyncError, bundledPresets } from '../sync/index.mjs';
|
|
103
100
|
import { emitContractsAsync } from '../emit/contracts.mjs';
|
|
104
101
|
import { ScaffoldError, newService, newSpec } from '../scaffold/service.mjs';
|
|
105
102
|
import { EditionRefusal } from '../scaffold/edition-gate.mjs';
|
|
106
103
|
import { addKind } from '../scaffold/add.mjs';
|
|
107
104
|
import { scaffoldApp } from '../scaffold/app.mjs';
|
|
108
105
|
import { newImages } from '../scaffold/image.mjs';
|
|
109
|
-
import { cachedLinters, eslintRepository,
|
|
106
|
+
import { cachedLinters, eslintRepository, runLinter as nativeLinter } from '../lint/run.mjs';
|
|
110
107
|
import { runSonarForApp, runTestForApp, runTestShard, runTypesForApp } from '../cache/app-families.mjs';
|
|
111
108
|
import { CACHE_ENV, createDiskSession } from '../cache/session.mjs';
|
|
112
109
|
import { DEFAULT_FAMILIES, runAppFamilies } from '../cache/run.mjs';
|
|
@@ -115,7 +112,8 @@ import { familyLine } from '../cache/report.mjs';
|
|
|
115
112
|
import { writeRunLog } from '../cache/run-log.mjs';
|
|
116
113
|
import { FAMILIES } from '../cache/types.mjs';
|
|
117
114
|
import { SECRET_CODES as SECRET_CODE_LIST } from '../cache/hfs-family.mjs';
|
|
118
|
-
import { writeReport } from '../report/sonar.mjs';
|
|
115
|
+
import { linterReport, mergeReports, sonarReport, sourceRootsOf, writeReport } from '../report/sonar.mjs';
|
|
116
|
+
import { stylelintFindings } from '../report/stylelint-findings.mjs';
|
|
119
117
|
import { checkEdition } from '../upgrade/check.mjs';
|
|
120
118
|
import { UpgradeError, upgradeMain } from '../upgrade/index.mjs';
|
|
121
119
|
import { ACTIONS as STACK_ACTIONS, FLAGS as STACK_FLAGS, OPERANDS as STACK_OPERANDS, READ_ONLY as STACK_READ_ONLY, REPEAT as STACK_REPEAT, VALUE as STACK_VALUE, stackMain as deployStackMain } from '../stack/cli.mjs';
|
|
@@ -139,7 +137,10 @@ const valueFlag = (opts, flag) => { const given = opts[flag]; return typeof give
|
|
|
139
137
|
const booleanFlag = (opts, flag) => { const given = opts[flag]; return typeof given === 'boolean' ? given : undefined; };
|
|
140
138
|
/** A list flag as parse stores it: every value given, else undefined. */
|
|
141
139
|
const listFlag = (opts, flag) => { const given = opts[flag]; return Array.isArray(given) ? given : undefined; };
|
|
142
|
-
|
|
140
|
+
/** An argument the parser refuses (an unknown flag, a missing value, a form a verb does not take): the one error `main` follows with the usage. */
|
|
141
|
+
export class UsageError extends Error {
|
|
142
|
+
}
|
|
143
|
+
export const VERBS = Object.freeze(['scaffold', 'add', 'sync', 'check', 'upgrade', 'stack', 'world', 'release', 'slot', 'emit', 'secret', 'ci', 'db', 'coverage', 'codemod']);
|
|
143
144
|
const DATABASE_VERBS = new Map([['baseline', dbBaselineMain], ['restamp', dbRestampMain], ['migrate', dbMigrateMain], ['post-cutover', dbPostCutoverMain], ['dump', dbDumpMain], ['new', dbNewMain], ['drift', dbDriftMain]]);
|
|
144
145
|
/**
|
|
145
146
|
* The verbs parsed here, each with the only flags it takes ({ boolean, value, list }); a flag of another verb is refused, never ignored.
|
|
@@ -163,7 +164,7 @@ const PARSED = {
|
|
|
163
164
|
{ operands: 'image', flags: [] },
|
|
164
165
|
],
|
|
165
166
|
},
|
|
166
|
-
check: { run: checkMain, value: ['--coverage-from', '--shard'], list: GATE_FLAGS, boolean: ['--json', '--staged', '--no-cache'], edition: true, forms: [{ only: { boolean: ['--json', '--no-cache'], list: GATE_FLAGS, value: ['--coverage-from', '--shard'] } }, { only: { boolean: ['--staged'] }, edition: false }] },
|
|
167
|
+
check: { run: checkMain, value: ['--coverage-from', '--shard', '--side', '--out'], list: GATE_FLAGS, boolean: ['--json', '--staged', '--no-cache', '--fix'], edition: true, forms: [{ only: { boolean: ['--json', '--no-cache', '--fix'], list: GATE_FLAGS, value: ['--coverage-from', '--shard', '--side', '--out'] } }, { only: { boolean: ['--staged'] }, edition: false }] },
|
|
167
168
|
slot: { run: slotMain, boolean: ['--json'], forms: [{ operands: '<path>' }] },
|
|
168
169
|
emit: { run: emitMain, forms: [{}] },
|
|
169
170
|
};
|
|
@@ -174,7 +175,7 @@ const PARSED_FLAGS = new Set([...Object.values(PARSED).flatMap(flagsOf), '--edit
|
|
|
174
175
|
/** The placeholder of a value flag in the usage; any other prints as <name>. */
|
|
175
176
|
const PLACEHOLDERS = {
|
|
176
177
|
'--gate': `<${CHECK_GATES.join('|')}>`,
|
|
177
|
-
'--registry-visibility': 'public|private', '--edition': 'full|lite', '--base': '<ref>', '--coverage-from': '<lcov>', '--shard': '<i>/<n>', '--inject': '<binding>',
|
|
178
|
+
'--registry-visibility': 'public|private', '--side': 'be|fe', '--edition': 'full|lite', '--base': '<ref>', '--coverage-from': '<lcov>', '--shard': '<i>/<n>', '--inject': '<binding>',
|
|
178
179
|
'--event': '<event>', '--from': '<service>', '--service': '<Class>=<module>', '--owner': '<service>', '--failed': '<event>', '--done': '<event>',
|
|
179
180
|
'--shapes': '<a,b,c>', '--blocks': '<a,b>', '--tabs': '<a,b>', '--kind': 'modal|drawer', '--image': '<service>=<ref>', '--sha': '<sha>',
|
|
180
181
|
'--previous': '<release>', '--release': '<release>', '--out': '<path>', '--since': '<ISO>', '--timeout': '<seconds>', '--retry': '<seconds>',
|
|
@@ -194,10 +195,9 @@ const specUsage = ({ boolean = [], value = [], list = [] }, placeholders = PLACE
|
|
|
194
195
|
/** A dbOptions list (bare names) as a spec; --cwd is the global flag. */
|
|
195
196
|
const dbSpec = (names) => ({ boolean: names.filter((name) => DB_BOOLEAN_OPTIONS.includes(name)).map((name) => `--${name}`), value: names.filter((name) => name !== 'cwd' && !DB_BOOLEAN_OPTIONS.includes(name)).map((name) => `--${name}`) });
|
|
196
197
|
const DATABASE_USAGE = [['new', '<kebab-name>', DB_NEW_FLAGS], ['baseline', '', dbSpec(BASELINE_OPTIONS)], ['restamp', '', dbSpec(RESTAMP_OPTIONS)], ['migrate', '', DB_MIGRATE_FLAGS], ['post-cutover', '', DB_POST_CUTOVER_FLAGS], ['dump', '', DB_DUMP_FLAGS], ['drift', '', DB_DRIFT_FLAGS]];
|
|
197
|
-
/** The usage of each verb, from the tables its parser refuses every other flag with (
|
|
198
|
+
/** The usage of each verb, from the tables its parser refuses every other flag with (codemod and upgrade keep hand-parsed arguments, held by main.spec.mjs). */
|
|
198
199
|
const VERB_USAGE = {
|
|
199
200
|
...Object.fromEntries(Object.entries(PARSED).map(([verb, spec]) => [verb, spec.forms.map((form) => line(`starci app ${verb}`, form.operands, specUsage(form.only ?? (form.flags === undefined ? spec : { value: form.flags })), spec.edition && form.edition !== false ? flagUsage('--edition', { value: true }) : ''))])),
|
|
200
|
-
lint: ['starci app lint [--changed <file>...] [--workspace <dir>] [--side be|fe] [--fix] [--full] [--full] [--json] [--out <file>]'],
|
|
201
201
|
codemod: CODEMODS.map((name) => `starci app codemod ${name} [--execute] [--json] [--format json|markdown] [--out <file.json|file.md>]`),
|
|
202
202
|
upgrade: ['starci app upgrade [--execute]'],
|
|
203
203
|
sync: [SYNC_USAGE.sync],
|
|
@@ -235,18 +235,18 @@ function parse(verb, argv) {
|
|
|
235
235
|
else if (value.includes(flag) || list.includes(flag)) {
|
|
236
236
|
const given = inline ?? args.next().value;
|
|
237
237
|
if (!given || given.startsWith('--'))
|
|
238
|
-
throw new
|
|
238
|
+
throw new UsageError(`${flag} needs a value`);
|
|
239
239
|
if (list.includes(flag))
|
|
240
240
|
opts[key] = [...(opts[key] ?? []), given];
|
|
241
241
|
else if (opts[key] !== undefined)
|
|
242
|
-
throw new
|
|
242
|
+
throw new UsageError(`${flag} may be given only once`);
|
|
243
243
|
else
|
|
244
244
|
opts[key] = given;
|
|
245
245
|
}
|
|
246
246
|
else if (boolean.includes(flag))
|
|
247
|
-
throw new
|
|
247
|
+
throw new UsageError(`${flag} takes no value`);
|
|
248
248
|
else
|
|
249
|
-
throw new
|
|
249
|
+
throw new UsageError(PARSED_FLAGS.has(flag) ? `starci app ${verb} takes no ${flag}` : `unknown flag ${flag}`);
|
|
250
250
|
}
|
|
251
251
|
return opts;
|
|
252
252
|
}
|
|
@@ -281,7 +281,7 @@ function printCheckOverview(result, out) {
|
|
|
281
281
|
out(` ESLint: ${sides}
|
|
282
282
|
`);
|
|
283
283
|
for (const error of result.eslint.errors)
|
|
284
|
-
out(` ESLint tool error: ${error}
|
|
284
|
+
out(` ${error.startsWith('stylelint ') ? 'stylelint' : 'ESLint'} tool error: ${error}
|
|
285
285
|
`);
|
|
286
286
|
}
|
|
287
287
|
}
|
|
@@ -305,6 +305,9 @@ function printCheck(result, out) {
|
|
|
305
305
|
for (const [code, list] of byCode)
|
|
306
306
|
printFindingGroup(code, list, out);
|
|
307
307
|
out(`\n${counts.error} error finding${counts.error === 1 ? '' : 's'}, ${counts.warning} warning${counts.warning === 1 ? '' : 's'}, ${counts.info} report-only\n`);
|
|
308
|
+
const counting = Object.entries((result.counting ?? {})).sort(([a], [b]) => a.localeCompare(b));
|
|
309
|
+
if (counting.length)
|
|
310
|
+
out(`counting (warn, not in the verdict): ${counting.map(([rule, count]) => `${rule} ${count}`).join(', ')}\n`);
|
|
308
311
|
}
|
|
309
312
|
function printNoSlot(e, out) {
|
|
310
313
|
out(` no slot owns this path (${e.code}): ${e.titleVi}\n ${e.whyVi}\n`);
|
|
@@ -342,7 +345,7 @@ function selectedFamilies(opts, fallback) {
|
|
|
342
345
|
return 'sonar';
|
|
343
346
|
const family = FAMILIES.find((candidate) => candidate === value);
|
|
344
347
|
if (family === undefined)
|
|
345
|
-
throw new
|
|
348
|
+
throw new UsageError(`--gate accepts ${CHECK_GATES.join('|')}; got ${JSON.stringify(value)}`);
|
|
346
349
|
return family;
|
|
347
350
|
});
|
|
348
351
|
return new Set(given.length > 0 ? given : fallback);
|
|
@@ -353,14 +356,11 @@ function openSession({ repoRoot, tracked, io, full, use, planned }) {
|
|
|
353
356
|
const env = use === 'bypass' ? { ...io.env, [CACHE_ENV]: 'full' } : use === 'read' ? { ...io.env, [CACHE_ENV]: io.env[CACHE_ENV] === 'full' ? 'full' : 'verify' } : io.env;
|
|
354
357
|
return createDiskSession({ root: repoRoot, tracked, env, progress, cliVersion: io.harness?.version, full });
|
|
355
358
|
}
|
|
356
|
-
/**
|
|
357
|
-
|
|
358
|
-
* `only` limits the formatter to those (app-relative) files and bypasses the cache (`app lint --changed`).
|
|
359
|
-
*/
|
|
360
|
-
async function appPass({ repoRoot, harness, only, presets, prettier, io, selected, full, use = 'normal', session: given }) {
|
|
359
|
+
/** The app pass of `starci app check`: the families the verb selects, against one cache session, merged into one result. */
|
|
360
|
+
async function appPass({ repoRoot, harness, presets, prettier, io, selected, full, use = 'normal', session: given }) {
|
|
361
361
|
const tracked = trackedFiles(repoRoot);
|
|
362
|
-
const session = given ?? openSession({ repoRoot, tracked, io, full, use
|
|
363
|
-
const result = await runAppFamilies({ repoRoot, tracked, session, selected, harness, presets, prettier,
|
|
362
|
+
const session = given ?? openSession({ repoRoot, tracked, io, full, use, planned: tracked.length * Math.max(1, selected.size) });
|
|
363
|
+
const result = await runAppFamilies({ repoRoot, tracked, session, selected, harness, presets, prettier, edition: undefined, execute: io.runHeavyNode });
|
|
364
364
|
if (given !== undefined)
|
|
365
365
|
return result;
|
|
366
366
|
const flushed = session.flush();
|
|
@@ -374,49 +374,6 @@ function verdictFindings(session) {
|
|
|
374
374
|
const why = readWhy(skillRoot, [...new Set(verdicts.map((verdict) => verdict.code))]);
|
|
375
375
|
return withWhy(verdicts.map((verdict) => ({ code: verdict.code, level: verdict.level, path: verdict.path, message: verdict.message })), why);
|
|
376
376
|
}
|
|
377
|
-
/** The nearest folder at or above `dir` that holds hfs.json (the app root of a workspace), or `dir` itself when none does (the lint then refuses it). */
|
|
378
|
-
export function appRootAbove(dir) {
|
|
379
|
-
for (let at = dir;; at = path.dirname(at)) {
|
|
380
|
-
if (fs.existsSync(path.join(at, HFS_DECLARATION_FILE)))
|
|
381
|
-
return at;
|
|
382
|
-
if (path.dirname(at) === at)
|
|
383
|
-
return dir;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
/** `starci app lint`: ESLint per side, the app checks and stylelint over the fe side as one report; see lint/run.mjs. */
|
|
387
|
-
async function lintMain(argv, io) {
|
|
388
|
-
const { cwd, harness, stdout, presets, prettier, runLinter, checkApp = appPass } = io;
|
|
389
|
-
const opts = parseLintArgs(argv);
|
|
390
|
-
// --workspace names a folder from the working directory (`starci app lint --workspace .` is an fe workspace script); the app
|
|
391
|
-
// root is then the nearest folder above it that holds hfs.json, and the workspace is passed on app-relative.
|
|
392
|
-
const workspaceDir = opts.workspace === undefined ? null : path.resolve(cwd, opts.workspace);
|
|
393
|
-
const repoRoot = path.resolve(workspaceDir ? appRootAbove(workspaceDir) : cwd);
|
|
394
|
-
if (workspaceDir)
|
|
395
|
-
opts.workspace = path.relative(repoRoot, workspaceDir).split(path.sep).join('/');
|
|
396
|
-
// The lint shares the cache of `app check`: --workspace reads it and writes none, --changed and --fix neither read nor write, --full judges again.
|
|
397
|
-
const use = opts.changed !== null || opts.fix ? 'bypass' : opts.workspace !== undefined ? 'read' : 'normal';
|
|
398
|
-
const selected = new Set(['hfs', 'secrets', 'db', 'format']);
|
|
399
|
-
// One session for the app findings and the linters, so the two never hold two answers; checkApp is a spec seam that brings its own.
|
|
400
|
-
const session = io.checkApp === undefined ? openSession({ repoRoot, tracked: trackedFiles(repoRoot), io, full: opts.full === true ? new Set([...selected, 'lint']) : undefined, use, planned: 1 }) : undefined;
|
|
401
|
-
const { report, sonar, exit } = await lintRepository({
|
|
402
|
-
repoRoot, opts, trackedFiles, runLinter, ...(session === undefined ? {} : { cache: { session } }),
|
|
403
|
-
hfsCheck: (root) => checkApp({ repoRoot: root, harness, only: opts.changed ?? undefined, presets, prettier, io, selected, use, session }),
|
|
404
|
-
});
|
|
405
|
-
const flushed = session?.flush();
|
|
406
|
-
const verdicts = session === undefined ? [] : verdictFindings(session);
|
|
407
|
-
if (opts.out !== undefined)
|
|
408
|
-
writeReport(path.resolve(cwd, opts.out), sonar);
|
|
409
|
-
if (opts.format === 'json')
|
|
410
|
-
stdout(`${JSON.stringify(report, null, 2)}
|
|
411
|
-
`);
|
|
412
|
-
else
|
|
413
|
-
printLintText(report, stdout);
|
|
414
|
-
for (const verdict of verdicts)
|
|
415
|
-
stdout(`${verdict.level.toUpperCase()} ${verdict.code}: ${verdict.message}\n`);
|
|
416
|
-
for (const warning of flushed?.warnings ?? [])
|
|
417
|
-
stdout(`cache: ${warning}\n`);
|
|
418
|
-
return Math.max(exit, verdicts.some((verdict) => verdict.level === 'error') ? 1 : 0);
|
|
419
|
-
}
|
|
420
377
|
/**
|
|
421
378
|
* The jest preset a new app renders its Sonar exclusions from: the copy this @starci/hfs ships (bundledPresets), the same version the
|
|
422
379
|
* scaffold pins, so `npx starci-harness app scaffold` works before any dependency of the app is installed.
|
|
@@ -426,7 +383,7 @@ const scaffoldPresets = () => bundledPresets();
|
|
|
426
383
|
function flagValue(flag, arg, args) {
|
|
427
384
|
const value = arg.startsWith(`${flag}=`) ? arg.slice(flag.length + 1) : args.next().value;
|
|
428
385
|
if (!value || value.startsWith('--'))
|
|
429
|
-
throw new
|
|
386
|
+
throw new UsageError(`${flag} needs a value`);
|
|
430
387
|
return value;
|
|
431
388
|
}
|
|
432
389
|
/** The flags every verb takes (--cwd, --quiet, --help), taken out of argv before the verb parses the rest. */
|
|
@@ -463,7 +420,7 @@ function editionOption(verb, argv) {
|
|
|
463
420
|
}
|
|
464
421
|
edition = flagValue('--edition', arg, args);
|
|
465
422
|
if (!['full', 'lite'].includes(edition))
|
|
466
|
-
throw new
|
|
423
|
+
throw new UsageError('--edition accepts full or lite');
|
|
467
424
|
}
|
|
468
425
|
return { edition, argv: rest };
|
|
469
426
|
}
|
|
@@ -486,7 +443,7 @@ function databaseMain(argv, io) {
|
|
|
486
443
|
const [verb, ...rest] = argv;
|
|
487
444
|
const dispatch = DATABASE_VERBS.get(verb);
|
|
488
445
|
if (!dispatch)
|
|
489
|
-
throw new
|
|
446
|
+
throw new UsageError('starci app db takes new, baseline, restamp, migrate, post-cutover, dump or drift');
|
|
490
447
|
return dispatch(rest, io);
|
|
491
448
|
}
|
|
492
449
|
function printCheckProofs(result, out) {
|
|
@@ -496,7 +453,7 @@ function printCheckProofs(result, out) {
|
|
|
496
453
|
out(formatCoverageReport(result.coverage));
|
|
497
454
|
}
|
|
498
455
|
/** The flags `check --staged` refuses: it is the pre-commit guard alone, so none of the whole-app pass's options means anything with it. */
|
|
499
|
-
const STAGED_REFUSES = ['--json', '--no-cache', '--coverage-from', '--shard', ...GATE_FLAGS];
|
|
456
|
+
const STAGED_REFUSES = ['--json', '--no-cache', '--fix', '--coverage-from', '--shard', '--side', '--out', ...GATE_FLAGS];
|
|
500
457
|
/** The family a finding belongs to, for naming it when a verified run disagrees with the cache. */
|
|
501
458
|
function familyOf(finding) {
|
|
502
459
|
const code = finding.code ?? '';
|
|
@@ -510,24 +467,39 @@ function familyOf(finding) {
|
|
|
510
467
|
return 'wiring';
|
|
511
468
|
return SECRET_CODE_LIST.has(code) ? 'secrets' : 'hfs';
|
|
512
469
|
}
|
|
513
|
-
/**
|
|
514
|
-
async function
|
|
470
|
+
/** The linters with --fix before the check judges the tree: ESLint through the shared execution, stylelint over the fe stylesheets. What they still find is the check's to report. */
|
|
471
|
+
async function fixLint({ repoRoot, side, run }) {
|
|
472
|
+
const app = readRepoDeclaration(loadSlotManifest(), repoRoot);
|
|
473
|
+
await eslintRepository({ repoRoot, app, opts: { ...(side === undefined ? {} : { side }), fix: true }, run });
|
|
474
|
+
if (side !== 'be')
|
|
475
|
+
await (run ?? nativeLinter)({ cwd: path.join(repoRoot, 'fe'), pkg: 'stylelint', bin: 'stylelint', args: [STYLE_GLOB, '--formatter', 'json', '--fix'] });
|
|
476
|
+
}
|
|
477
|
+
/** One run of the selected families against the cache, then the coverage proof on top; returns the result, what it took and the raw linter results. */
|
|
478
|
+
async function checkRun({ io, selected, full, use, coverageFrom, side, fix = false }) {
|
|
515
479
|
const { cwd: repoRoot, harness, edition, presets, prettier, checkApp = appPass, runLinter } = io;
|
|
516
480
|
const started = Date.now();
|
|
481
|
+
if (fix)
|
|
482
|
+
await fixLint({ repoRoot, side, run: runLinter });
|
|
517
483
|
const tracked = io.checkApp === undefined ? trackedFiles(repoRoot) : [];
|
|
518
484
|
const session = io.checkApp === undefined ? openSession({ repoRoot, tracked, io, full, use, planned: tracked.length * Math.max(1, selected.size) }) : undefined;
|
|
519
485
|
const checked = await checkEdition({ repoRoot, edition, normal: () => checkApp({ repoRoot, harness, presets, prettier, io, selected, full, use, session }), presets, prettier });
|
|
520
486
|
const proof = withCheckProofs(checked, { repoRoot, edition, wiring: false, coverageFrom });
|
|
521
487
|
const extra = [];
|
|
522
488
|
const lines = [];
|
|
489
|
+
const raw = { eslint: [], stylelint: [] };
|
|
523
490
|
let eslint = null;
|
|
491
|
+
let style = { results: [], errors: [] };
|
|
524
492
|
if (selected.has('lint')) {
|
|
525
493
|
if (session !== undefined) {
|
|
526
494
|
const app = readRepoDeclaration(loadSlotManifest(), repoRoot);
|
|
527
|
-
|
|
495
|
+
const linted = await cachedLinters({ repoRoot, app, side, run: runLinter, session });
|
|
496
|
+
eslint = linted.eslint;
|
|
497
|
+
style = linted.stylelint;
|
|
528
498
|
}
|
|
529
499
|
else
|
|
530
|
-
eslint = await eslintRepository({ repoRoot, changed: null, existing: [], run: runLinter });
|
|
500
|
+
eslint = await eslintRepository({ repoRoot, opts: side === undefined ? {} : { side }, changed: null, existing: [], run: runLinter });
|
|
501
|
+
raw.eslint = eslint.raw;
|
|
502
|
+
raw.stylelint = style.results;
|
|
531
503
|
}
|
|
532
504
|
if (session !== undefined && selected.has('types'))
|
|
533
505
|
extra.push(...(await runTypesForApp(session, { repoRoot, tracked, env: io.env, execute: io.runHeavyNode })).findings);
|
|
@@ -540,18 +512,46 @@ async function checkRun({ io, selected, full, use, coverageFrom }) {
|
|
|
540
512
|
const flushed = session?.flush();
|
|
541
513
|
const verdicts = session === undefined ? [] : verdictFindings(session);
|
|
542
514
|
const reports = session === undefined ? [...(checked.families ?? [])] : [...session.reports];
|
|
543
|
-
|
|
515
|
+
// A rule of the canon's counting cohort is a warning: listed and counted, never part of the verdict.
|
|
516
|
+
const linterFindings = [...(eslint?.findings ?? []).map(finding => ({ ...finding, code: finding.code ?? finding.rule, level: finding.severity === 'warning' ? 'warning' : 'error' })), ...stylelintFindings(style.results, repoRoot)];
|
|
517
|
+
const findings = [...proof.findings, ...extra, ...verdicts, ...linterFindings];
|
|
544
518
|
// Green under the threshold (gate-threshold.mjs): every finding is still listed, only the verdict changes.
|
|
545
519
|
const verdict = gateVerdict(findings.filter(finding => finding.level === 'error'));
|
|
546
|
-
const toolErrors = eslint?.errors ?? [];
|
|
520
|
+
const toolErrors = [...(eslint?.errors ?? []), ...style.errors];
|
|
547
521
|
const counts = { ...proof.counts, error: findings.filter(finding => finding.level === 'error').length, warning: findings.filter(finding => finding.level === 'warning').length };
|
|
548
|
-
const
|
|
549
|
-
|
|
522
|
+
const counting = {};
|
|
523
|
+
for (const finding of linterFindings)
|
|
524
|
+
if (finding.level === 'warning')
|
|
525
|
+
counting[finding.rule] = (counting[finding.rule] ?? 0) + 1;
|
|
526
|
+
const result = { ...proof, findings, families: reports, predictions: lines, counting, cacheWarnings: [...(checked.cacheWarnings ?? []), ...(flushed?.warnings ?? [])], counts, ...(eslint ? { eslint: { ...eslint.engine, errors: toolErrors } } : {}), ok: verdict.pass && toolErrors.length === 0, gate: verdict };
|
|
527
|
+
return { result, seconds: (Date.now() - started) / 1000, raw };
|
|
528
|
+
}
|
|
529
|
+
/** `--out`: every finding the run produced as ONE Sonar Generic Issue Import document (the check findings under starci-hfs, then eslint and stylelint), placed on the `sonar.sources` roots of the side. */
|
|
530
|
+
function writeSonarImport({ repoRoot, file, findings, raw, side }) {
|
|
531
|
+
let properties = '';
|
|
532
|
+
try {
|
|
533
|
+
properties = fs.readFileSync(path.join(repoRoot, 'sonar-project.properties'), 'utf8');
|
|
534
|
+
}
|
|
535
|
+
catch { /* no properties: every finding keeps its own path */ }
|
|
536
|
+
const onSide = (candidate) => {
|
|
537
|
+
if (side === undefined)
|
|
538
|
+
return true;
|
|
539
|
+
const owner = SIDES.find((each) => candidate === each || candidate.startsWith(`${each}/`));
|
|
540
|
+
return (owner ?? 'be') === side;
|
|
541
|
+
};
|
|
542
|
+
const sourceRoots = sourceRootsOf(properties).filter(onSide);
|
|
543
|
+
const tracked = sourceRoots.length ? trackedFiles(repoRoot) : [];
|
|
544
|
+
const own = findings.filter(finding => finding.engine === undefined && finding.level === 'error' && onSide(String(finding.path ?? '').replace(/\\/g, '/')));
|
|
545
|
+
writeReport(file, mergeReports([
|
|
546
|
+
sonarReport(own, { sourceRoots, tracked }),
|
|
547
|
+
linterReport('eslint', raw.eslint, { root: repoRoot, sourceRoots, tracked }),
|
|
548
|
+
linterReport('stylelint', raw.stylelint, { root: repoRoot, sourceRoots, tracked }),
|
|
549
|
+
]));
|
|
550
550
|
}
|
|
551
551
|
/** STARCI_APP_CHECK_CACHE=verify: the diff-only run, then the full run, compared finding by finding; any difference names family and file (exit 2). */
|
|
552
|
-
async function verifyCache(io, selected, coverageFrom) {
|
|
553
|
-
const diff = await checkRun({ io, selected, full: undefined, use: 'read', coverageFrom });
|
|
554
|
-
const full = await checkRun({ io, selected, full: new Set(selected), use: 'bypass', coverageFrom });
|
|
552
|
+
async function verifyCache(io, selected, coverageFrom, side) {
|
|
553
|
+
const diff = await checkRun({ io, selected, full: undefined, use: 'read', coverageFrom, side });
|
|
554
|
+
const full = await checkRun({ io, selected, full: new Set(selected), use: 'bypass', coverageFrom, side });
|
|
555
555
|
const keyed = (result) => new Map(result.findings.map((finding) => [`${finding.code}|${finding.path ?? ''}|${finding.message}`, `${familyOf(finding)}: ${finding.path ?? finding.code}`]));
|
|
556
556
|
const left = keyed(diff.result);
|
|
557
557
|
const right = keyed(full.result);
|
|
@@ -571,33 +571,44 @@ async function verifyCache(io, selected, coverageFrom) {
|
|
|
571
571
|
async function checkMain(opts, io) {
|
|
572
572
|
const { cwd: repoRoot, edition, stdout: out } = io;
|
|
573
573
|
if (opts.positional.length)
|
|
574
|
-
throw new
|
|
574
|
+
throw new UsageError('starci app check takes no path');
|
|
575
575
|
if (opts.staged === true) {
|
|
576
576
|
const given = [...STAGED_REFUSES.filter((flag) => opts[flag.slice(2)] !== undefined), ...(edition === undefined ? [] : ['--edition'])];
|
|
577
577
|
if (given.length)
|
|
578
|
-
throw new
|
|
578
|
+
throw new UsageError(`starci app check --staged is the pre-commit guard alone and takes no ${given.join(', ')}`);
|
|
579
579
|
return runWorkHygiene({ cwd: repoRoot, out: (text) => out(`${text}\n`) });
|
|
580
580
|
}
|
|
581
581
|
const coverageFrom = valueFlag(opts, 'coverage-from');
|
|
582
582
|
const shard = valueFlag(opts, 'shard');
|
|
583
|
+
const side = valueFlag(opts, 'side');
|
|
584
|
+
const sonarOut = valueFlag(opts, 'out');
|
|
585
|
+
const fix = booleanFlag(opts, 'fix') === true;
|
|
583
586
|
const explicit = selectedFamilies(opts, []);
|
|
587
|
+
if (side !== undefined && !['be', 'fe'].includes(side))
|
|
588
|
+
throw new UsageError('LINT_SIDE_INVALID: --side is be or fe');
|
|
589
|
+
if (fix && !explicit.has('lint'))
|
|
590
|
+
throw new UsageError('--fix runs the linters and needs --gate lint');
|
|
584
591
|
if (shard !== undefined) {
|
|
592
|
+
if (side !== undefined || sonarOut !== undefined)
|
|
593
|
+
throw new UsageError('--shard <i>/<n> takes no --side or --out');
|
|
585
594
|
// A selected test shard is always a whole-unit shard, in the shape the coverage merge reads; nothing is recorded.
|
|
586
595
|
const match = /^(\d+)\/(\d+)$/.exec(shard);
|
|
587
596
|
if (explicit.size !== 1 || !explicit.has('test') || coverageFrom !== undefined)
|
|
588
|
-
throw new
|
|
597
|
+
throw new UsageError('--shard <i>/<n> requires --gate test alone and takes no --coverage-from');
|
|
589
598
|
const index = Number(match?.[1]);
|
|
590
599
|
const total = Number(match?.[2]);
|
|
591
600
|
if (match === null || !Number.isSafeInteger(index) || !Number.isSafeInteger(total) || index < 1 || total < index)
|
|
592
|
-
throw new
|
|
601
|
+
throw new UsageError('--shard needs <i>/<n> with 1 <= i <= n');
|
|
593
602
|
return runTestShard(repoRoot, index, total, io.runHeavyNode);
|
|
594
603
|
}
|
|
595
604
|
// A coverage report judged on its own (the merge job of a sharded run) runs no family; with selectors it comes on top of them.
|
|
596
605
|
const selected = explicit.size > 0 ? explicit : coverageFrom === undefined ? new Set(DEFAULT_FAMILIES) : new Set();
|
|
597
606
|
const full = opts['no-cache'] === true ? new Set(selected) : undefined;
|
|
598
607
|
if (io.env[CACHE_ENV] === 'verify')
|
|
599
|
-
return verifyCache(io, selected, coverageFrom);
|
|
600
|
-
const { result, seconds } = await checkRun({ io, selected, full, use: coverageFrom !== undefined && explicit.size === 0 ? 'bypass' : 'normal', coverageFrom });
|
|
608
|
+
return verifyCache(io, selected, coverageFrom, side);
|
|
609
|
+
const { result, seconds, raw } = await checkRun({ io, selected, full, use: coverageFrom !== undefined && explicit.size === 0 ? 'bypass' : 'normal', coverageFrom, side, fix });
|
|
610
|
+
if (sonarOut !== undefined)
|
|
611
|
+
writeSonarImport({ repoRoot, file: path.resolve(repoRoot, sonarOut), findings: result.findings, raw, side });
|
|
601
612
|
if (opts.json)
|
|
602
613
|
out(`${JSON.stringify(result, null, 2)}\n`);
|
|
603
614
|
else {
|
|
@@ -607,12 +618,12 @@ async function checkMain(opts, io) {
|
|
|
607
618
|
out(`${result.gate.line}\n`);
|
|
608
619
|
}
|
|
609
620
|
const exit = result.eslint?.errors.length ? 2 : result.ok ? 0 : 1;
|
|
610
|
-
writeRunLog(repoRoot, { command: `check${[...new Set(listFlag(opts, 'gate') ?? [])].map((gate) => ` --gate ${gate}`).join('')}${opts['no-cache'] === true ? ' --no-cache' : ''}${coverageFrom === undefined ? '' : ` --coverage-from ${JSON.stringify(coverageFrom)}`}`, seconds, exit, families: result.families.map((report) => ({ ...report, seconds: 0 })) }, io.env);
|
|
621
|
+
writeRunLog(repoRoot, { command: `check${[...new Set(listFlag(opts, 'gate') ?? [])].map((gate) => ` --gate ${gate}`).join('')}${side === undefined ? '' : ` --side ${side}`}${fix ? ' --fix' : ''}${opts['no-cache'] === true ? ' --no-cache' : ''}${coverageFrom === undefined ? '' : ` --coverage-from ${JSON.stringify(coverageFrom)}`}`, seconds, exit, families: result.families.map((report) => ({ ...report, seconds: 0 })) }, io.env);
|
|
611
622
|
return exit;
|
|
612
623
|
}
|
|
613
624
|
async function emitMain(opts, { cwd: repoRoot, stdout: out, runHeavyNode }) {
|
|
614
625
|
if (opts.positional.length)
|
|
615
|
-
throw new
|
|
626
|
+
throw new UsageError('starci app emit takes no path');
|
|
616
627
|
const be = readRepoDeclaration(loadSlotManifest(), repoRoot).sides?.be;
|
|
617
628
|
if (!be)
|
|
618
629
|
throw new Error('starci app emit runs at the app root (the folder of hfs.json)');
|
|
@@ -636,9 +647,9 @@ function addFileNoun(noun, opts, repoRoot, out) {
|
|
|
636
647
|
const [first = '', second = ''] = args;
|
|
637
648
|
const foreign = ADD_FLAGS_ALL.filter((flag) => opts[flag.slice(2)] !== undefined);
|
|
638
649
|
if (foreign.length)
|
|
639
|
-
throw new
|
|
650
|
+
throw new UsageError(`starci app add ${noun} takes no ${foreign.join(', ')}`);
|
|
640
651
|
if (noun !== 'service' && opts.inject !== undefined)
|
|
641
|
-
throw new
|
|
652
|
+
throw new UsageError(`starci app add ${noun} takes no --inject; only add service does`);
|
|
642
653
|
let written;
|
|
643
654
|
if (noun === 'service' && args.length === 2)
|
|
644
655
|
written = newService({ repoRoot, dir: first, name: second, inject: listFlag(opts, 'inject') ?? [] });
|
|
@@ -647,7 +658,7 @@ function addFileNoun(noun, opts, repoRoot, out) {
|
|
|
647
658
|
else if (noun === 'image' && args.length === 0)
|
|
648
659
|
written = newImages({ repoRoot });
|
|
649
660
|
else
|
|
650
|
-
throw new
|
|
661
|
+
throw new UsageError('starci app add takes `service <dir> <name> [--inject ...]`, `spec <file>.service.ts` or `image`');
|
|
651
662
|
for (const file of written)
|
|
652
663
|
out(`created ${file}\n`);
|
|
653
664
|
return 0;
|
|
@@ -657,9 +668,9 @@ function addMain(opts, { cwd: repoRoot, stdout: out }) {
|
|
|
657
668
|
if (noun !== undefined && ADD_FILE_NOUNS.includes(noun))
|
|
658
669
|
return addFileNoun(noun, opts, repoRoot, out);
|
|
659
670
|
if (opts.inject !== undefined)
|
|
660
|
-
throw new
|
|
671
|
+
throw new UsageError(`starci app add ${noun ?? '<noun>'} takes no --inject; only add service does`);
|
|
661
672
|
if (!noun || !name || extra.length)
|
|
662
|
-
throw new
|
|
673
|
+
throw new UsageError('starci app add takes `<noun> <name>` and the options of the noun');
|
|
663
674
|
const { created, registered } = addKind({ repoRoot, noun, name, options: { event: opts.event, from: opts.from, service: opts.service, connection: opts.connection, owner: opts.owner, failed: opts.failed, done: opts.done, fe: booleanFlag(opts, 'fe'), shapes: valueFlag(opts, 'shapes'), read: valueFlag(opts, 'read'), app: valueFlag(opts, 'app'), kind: valueFlag(opts, 'kind'), form: booleanFlag(opts, 'form'), blocks: valueFlag(opts, 'blocks'), tabs: valueFlag(opts, 'tabs') } });
|
|
664
675
|
for (const file of created)
|
|
665
676
|
out(`created ${file}\n`);
|
|
@@ -673,14 +684,14 @@ async function scaffoldMain(opts, { cwd, harness, edition = 'full', presets, loc
|
|
|
673
684
|
const [name, ...extra] = opts.positional;
|
|
674
685
|
const registryVisibility = valueFlag(opts, 'registry-visibility');
|
|
675
686
|
if (!name || extra.length)
|
|
676
|
-
throw new
|
|
687
|
+
throw new UsageError(`starci app scaffold takes \`${VERB_USAGE.scaffold[0].slice('starci app scaffold '.length)}\``);
|
|
677
688
|
const { root: created, files } = scaffoldApp({ name, into: path.resolve(cwd), harness, edition, presets: presets ?? scaffoldPresets(), ...(registryVisibility === undefined ? {} : { registryVisibility }), ...(lock ? { lock } : {}) });
|
|
678
|
-
out(`starci app scaffold: created ${created} (${files.length} files); next: npm ci (the lockfile, then the git hooks through npm run prepare; after npm ci --ignore-scripts run npm run prepare), then starci app lint\n`);
|
|
689
|
+
out(`starci app scaffold: created ${created} (${files.length} files); next: npm ci (the lockfile, then the git hooks through npm run prepare; after npm ci --ignore-scripts run npm run prepare), then npm run lint, which is starci app check --gate lint\n`);
|
|
679
690
|
return 0;
|
|
680
691
|
}
|
|
681
692
|
function slotMain(opts, { cwd: repoRoot, stdout: out }) {
|
|
682
693
|
if (opts.positional.length !== 1)
|
|
683
|
-
throw new
|
|
694
|
+
throw new UsageError('starci app slot takes exactly one path');
|
|
684
695
|
const [input = ''] = opts.positional;
|
|
685
696
|
const explained = explainPath({ repoRoot, input });
|
|
686
697
|
if (opts.json)
|
|
@@ -710,13 +721,13 @@ const WORLD_ACTIONS = ['up', 'down', 'status'];
|
|
|
710
721
|
/** `starci app world <up|down|status>`: the local test world. */
|
|
711
722
|
async function worldAppMain(argv, { cwd, edition, stackMain, stdout: out, stderr: err }) {
|
|
712
723
|
if (!WORLD_ACTIONS.includes(argv[0]))
|
|
713
|
-
throw new
|
|
724
|
+
throw new UsageError(`starci app world takes ${WORLD_ACTIONS.join(', ')}`);
|
|
714
725
|
if ((edition ?? declaredEdition(cwd)) === 'lite')
|
|
715
726
|
throw new Error('starci app world up|down|status manage the local test world of the full edition; a lite app starts its local database with npm run db:start');
|
|
716
727
|
const run = stackMain ?? await installedStackMain(cwd);
|
|
717
728
|
return run(argv, { cwd, out, err });
|
|
718
729
|
}
|
|
719
|
-
const DIRECT_VERBS = new Map([['ci', async (argv, io) => (await import('../ci/cli.mjs')).ciMain(argv, io)], ['
|
|
730
|
+
const DIRECT_VERBS = new Map([['ci', async (argv, io) => (await import('../ci/cli.mjs')).ciMain(argv, io)], ['secret', secretMain], ['db', databaseMain], ['upgrade', upgradeAppMain], ['stack', stackAppMain], ['world', worldAppMain], ['release', releaseMain], ['coverage', async (argv, io) => (await import('../coverage/cli.mjs')).coverageMain(argv, io)], ['codemod', codemodMain]]);
|
|
720
731
|
function dispatchMain(verb, argv, io) {
|
|
721
732
|
if (verb === 'sync')
|
|
722
733
|
return syncMain([verb, ...argv], io);
|
|
@@ -725,12 +736,15 @@ function dispatchMain(verb, argv, io) {
|
|
|
725
736
|
return direct(argv, io);
|
|
726
737
|
return PARSED[verb].run(parse(verb, argv), io);
|
|
727
738
|
}
|
|
739
|
+
/** What `main` prints for a thrown error: the usage only after a usage error; a refusal or a tool failure prints its code and message alone. */
|
|
728
740
|
function errorMessage(error) {
|
|
729
741
|
if (error instanceof EditionRefusal || error instanceof UpgradeError || error instanceof HfsSlotsError || error instanceof SyncError)
|
|
730
742
|
return `${error.message}\n`;
|
|
731
743
|
if (error instanceof ScaffoldError)
|
|
732
744
|
return `${error.code}: ${error.message}\n`;
|
|
733
|
-
|
|
745
|
+
if (error instanceof UsageError)
|
|
746
|
+
return `starci app: ${error.message}\n${USAGE}`;
|
|
747
|
+
return `starci app: ${error.message}\n`;
|
|
734
748
|
}
|
|
735
749
|
/**
|
|
736
750
|
* `harness` ({ version }) is the CLI that runs this package in-process: its version is the starci-harness an app is judged and
|
|
@@ -742,8 +756,8 @@ export async function main(argv, { cwd: baseCwd = process.cwd(), harness, stdout
|
|
|
742
756
|
try {
|
|
743
757
|
const global = globalOptions(argv, path.resolve(baseCwd));
|
|
744
758
|
const [verb, ...given] = global.argv;
|
|
745
|
-
// --quiet silences stdout for every verb but check
|
|
746
|
-
const out = global.quiet &&
|
|
759
|
+
// --quiet silences stdout for every verb but check, whose diagnosis it keeps (its progress on stderr is what it mutes).
|
|
760
|
+
const out = global.quiet && verb !== 'check' ? () => { } : stdout;
|
|
747
761
|
if (global.help) {
|
|
748
762
|
out(USAGE);
|
|
749
763
|
return 0;
|
package/sync/ci-commands.d.mts
CHANGED
|
@@ -11,8 +11,9 @@ interface CiSource {
|
|
|
11
11
|
/** jest --shard=i/N gives each shard ceil(specs / N) spec files in order, so shard N is empty unless (N - 1) * ceil(specs / N) < specs. */
|
|
12
12
|
export declare function filledShards(specs: number, wanted: number): number;
|
|
13
13
|
/** An omitted quality declaration preserves hosted Sonar, required OIDC Codecov, and the high production dependency audit. */
|
|
14
|
-
export declare function ciWorkflowCommands(source: CiSource | undefined, lcovReport?: string, quality?: Declaration, { nodeMajor, app }?: {
|
|
14
|
+
export declare function ciWorkflowCommands(source: CiSource | undefined, lcovReport?: string, quality?: Declaration, { nodeMajor, app, packages }?: {
|
|
15
15
|
nodeMajor?: string;
|
|
16
|
+
packages?: boolean;
|
|
16
17
|
app?: {
|
|
17
18
|
sides: {
|
|
18
19
|
fe: {
|
package/sync/ci-commands.mjs
CHANGED
|
@@ -179,7 +179,7 @@ function ciJobSettings(declaration, source) {
|
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
181
|
/** An omitted quality declaration preserves hosted Sonar, required OIDC Codecov, and the high production dependency audit. */
|
|
182
|
-
export function ciWorkflowCommands(source, lcovReport = 'be/coverage/lcov.info', quality = undefined, { nodeMajor = '22', app } = {}) {
|
|
182
|
+
export function ciWorkflowCommands(source, lcovReport = 'be/coverage/lcov.info', quality = undefined, { nodeMajor = '22', app, packages = false } = {}) {
|
|
183
183
|
const configuredSonar = quality === undefined ? { token: 'SONAR_TOKEN', hostVar: 'SONAR_HOST_URL' } : quality.sonar;
|
|
184
184
|
const sonar = configuredSonar?.wiring === 'not-used' || configuredSonar?.mode === 'disabled' ? null : configuredSonar;
|
|
185
185
|
const codecov = quality?.codecov ?? { enabled: true, auth: 'oidc' };
|
|
@@ -195,8 +195,6 @@ export function ciWorkflowCommands(source, lcovReport = 'be/coverage/lcov.info',
|
|
|
195
195
|
shardCoverageReport: lcovReport.split('/').slice(0, -1).concat('coverage-final.json').join('/'),
|
|
196
196
|
ciBuildEnv: build.env,
|
|
197
197
|
ciBuildSteps: build.steps,
|
|
198
|
-
ciLint: exampleCommand(source, 'npm run lint'),
|
|
199
|
-
ciLintSonar: exampleCommand(source, 'npm run lint -- --out reports/lint.sonar.json'),
|
|
200
198
|
// The coverage judgement alone over the merged report: the fast and lint jobs already ran every other rule of the app check.
|
|
201
199
|
ciCoverageProof: exampleCommand(source, 'npx starci app coverage verify ' + lcovReport),
|
|
202
200
|
// An app with an entity list also checks its entities against the schema its migrations build, in the build job (a runner with Docker).
|
|
@@ -205,23 +203,24 @@ export function ciWorkflowCommands(source, lcovReport = 'be/coverage/lcov.info',
|
|
|
205
203
|
sonarTokenSecret: sonar?.token ?? 'SONAR_TOKEN',
|
|
206
204
|
sonarHostVar: sonar?.hostVar ?? 'SONAR_HOST_URL',
|
|
207
205
|
// One lint job per side (the lint matrix): with Sonar each side writes its own report, uploaded as sonar-lint-<side> and merged by the sonar job.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
206
|
+
// The typed lint reads the generated code and the types the fe packages declare (their dist), so both are built first, the way the root lint script does.
|
|
207
|
+
ciLintSteps: [' - name: build the lint inputs', ` run: ${exampleCommand(source, packages ? 'npm run codegen --silent && npx --no-install turbo run build --filter=./fe/packages/*' : 'npm run codegen --silent')}`, ...(sonar ? [
|
|
208
|
+
' - name: lint with Sonar import',
|
|
209
|
+
` run: ${exampleCommand(source, 'npx --no-install starci app check --gate lint --side ' + side + ' --out reports/lint.' + side + '.sonar.json')}`,
|
|
210
|
+
' env:',
|
|
211
|
+
` NODE_OPTIONS: ${settings.nodeOptions}`,
|
|
212
|
+
` - uses: ${UPLOAD_ARTIFACT}`,
|
|
213
|
+
// Inside an if: expression the matrix value is an argument of format(), never a nested ${{ }} (actionlint: a nested expression makes the condition always true).
|
|
214
|
+
" if: always() && hashFiles(format('reports/lint.{0}.sonar.json', matrix.side)) != ''",
|
|
215
|
+
' with:',
|
|
216
|
+
` name: sonar-lint-${side}`,
|
|
217
|
+
` path: reports/lint.${side}.sonar.json`,
|
|
218
|
+
] : [
|
|
219
|
+
' - name: lint',
|
|
220
|
+
` run: ${exampleCommand(source, 'npx --no-install starci app check --gate lint --side ' + side)}`,
|
|
221
|
+
' env:',
|
|
222
|
+
` NODE_OPTIONS: ${settings.nodeOptions}`,
|
|
223
|
+
])].join('\n'),
|
|
225
224
|
codecovSteps: codecovSteps(lcovReport, codecov),
|
|
226
225
|
verdictSonarNeed: sonar ? ', sonar' : '',
|
|
227
226
|
verdictSonarCheck: sonar ? `\n if [[ "$FULL" == true && "$CHECKS" == true ]]; then expect sonar success; else expect sonar idle; fi` : '',
|