@starci/hfs 5.4.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +4 -3
- package/cache/test-family.d.mts +1 -1
- package/cache/test-family.mjs +20 -15
- package/codemod/main.mjs +6 -1
- package/codemod/seeds-layout.d.mts +36 -0
- package/codemod/seeds-layout.mjs +113 -0
- package/codemod/sops-rules.d.mts +1 -1
- package/codemod/sops-rules.mjs +8 -3
- package/lint/run.d.mts +5 -4
- package/lint/run.mjs +53 -27
- package/package.json +1 -1
- package/release/lint-report.mjs +4 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +12 -12
- package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +15 -2
- package/runtime/knowledge/hfs/slots.yaml +15 -18
- package/runtime/modules/kernel/failure-codes.yaml +64 -3
- package/runtime/scripts/hfs/architecture/root-packages.mjs +1 -1
- package/runtime/scripts/hfs/check.mjs +4 -1
- package/runtime/scripts/hfs/coverage-scope.d.mts +15 -0
- package/runtime/scripts/hfs/coverage-scope.mjs +26 -0
- package/runtime/scripts/hfs/db-squash.mjs +9 -2
- package/runtime/scripts/hfs/declaration-shape.mjs +3 -1
- package/runtime/scripts/hfs/rules/app-root.mjs +4 -1
- package/runtime/scripts/hfs/rules/ci-standard.mjs +5 -4
- package/runtime/scripts/hfs/rules/coverage-below.mjs +12 -4
- package/runtime/scripts/hfs/rules/database-config.mjs +8 -7
- package/runtime/scripts/hfs/rules/database-constants.d.mts +2 -1
- package/runtime/scripts/hfs/rules/database-constants.mjs +2 -1
- package/runtime/scripts/hfs/rules/database.d.mts +2 -2
- package/runtime/scripts/hfs/rules/database.mjs +7 -6
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -1
- package/runtime/scripts/hfs/rules/monorepo.mjs +1 -1
- package/runtime/scripts/hfs/rules/seeds.d.mts +21 -0
- package/runtime/scripts/hfs/rules/seeds.mjs +194 -0
- package/runtime/scripts/hfs/rules/stacks.mjs +8 -4
- package/runtime/scripts/hfs/slots.d.mts +2 -0
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/stack-seeds-config.d.mts +88 -0
- package/runtime/scripts/hfs/stack-seeds-config.mjs +239 -0
- package/runtime/scripts/hfs/view.d.mts +25 -0
- package/runtime/scripts/hfs/view.mjs +41 -0
- package/scaffold/add-app.mjs +2 -2
- package/scaffold/add-cli-lite.mjs +41 -14
- package/scaffold/add-module.mjs +6 -3
- package/scaffold/lite-exports.mjs +1 -1
- package/scaffold/module-registry.mjs +10 -8
- package/src/main.mjs +8 -8
- package/stack/deploy.d.mts +3 -0
- package/stack/deploy.mjs +4 -1
- package/stack/local-up.mjs +25 -3
- package/stack/seeds.d.mts +29 -0
- package/stack/seeds.mjs +197 -0
- package/sync/canon-layout.d.mts +38 -0
- package/sync/canon-layout.mjs +62 -0
- package/sync/ci-commands.d.mts +2 -1
- package/sync/ci-commands.mjs +10 -7
- package/sync/index.d.mts +13 -2
- package/sync/index.mjs +52 -24
- package/templates/app/ci-workflows/github/workflows/ci.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -2
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +26 -0
- package/templates/app/skeleton/.starcistacks/dev/README.md +21 -10
- package/templates/app/skeleton/.starcistacks/dev/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/dev/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/dev/seeds/seeds.yaml +3 -0
- package/templates/app/skeleton/.starcistacks/production/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/production/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/production/seeds/seeds.yaml +2 -0
- package/templates/app/skeleton/.starcistacks/production/seeds/sql/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/seeds/seeds.yaml +2 -0
- package/templates/app/skeleton/.starcistacks/seeds/sql/.gitkeep +0 -0
- package/templates/app/skeleton/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/dev/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/seeds/seeds.yaml +2 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/seeds/sql/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/production/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/production/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/production/seeds/seeds.yaml +2 -0
- package/templates/app/skeleton-lite/.starcistacks/production/seeds/sql/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/seeds/seeds.yaml +2 -0
- package/templates/app/skeleton-lite/.starcistacks/seeds/sql/.gitkeep +0 -0
- package/templates/app/skeleton-lite/README.md +2 -2
- package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +2 -0
- package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +6 -1
- package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +14 -1
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +5 -1
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +0 -1
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +7 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +15 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +9 -3
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +79 -16
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +39 -9
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +108 -20
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +34 -7
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +28 -4
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +6 -1
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +9 -2
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +11 -2
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +59 -9
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +4 -1
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +25 -4
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +8 -1
- package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +3 -1
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +3 -3
- package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +6 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +14 -9
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +32 -7
- package/templates/be/tool-config/jest.config.js +1 -1
- package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +4 -1
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +3 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +14 -6
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +1 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +1 -0
- package/templates/app/module/jest.config.js.tpl +0 -8
- /package/templates/app/skeleton/.starcistacks/dev/seeds/{primary-notes.sql → sql/primary-notes.sql} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.5.0 - 2026-11-03
|
|
4
|
+
|
|
5
|
+
- Changed: the back end's unit run holds the root packages (`packages/<name>` with a `package.json` and a `src/`, never the registry): their specs run in `npm test`, `app check --gate test` and the CI shards, and their logic is measured at 100 per file (the workbench, the public face and the barrels aside) inside the Sonar and Codecov scope. `test:modules`, the fast job's module-packages step and the per-package `jest.config.js` are removed; `starci app sync` renders the change (PROJECT-3, BE-TEST-13).
|
|
6
|
+
- Fixed: each lint scope runs on its own: a side whose ESLint child does not complete is a tool error of that side (exit 2), and the next side and the root packages are still judged; a host hold still ends the run. The ESLint liveness budget of a diff run scales with the files of the side, whose typed program the child builds, not with the length of the diff.
|
|
7
|
+
- Added: `app check --gate lint --side packages`; an app with root packages lints them in a job of the CI lint matrix (`[be, fe, packages]`) with their own Sonar report, and the release gate's lint report does the same.
|
|
8
|
+
- Added: a root workspace package declares its own error base and result union in its `package.json` (`starci.errorBase`, `starci.outcome`), read by the root-packages view (`readPackageContract`).
|
|
9
|
+
- Fixed: `app sync` renders the root `eslint.config.mjs` and the module registry (`tsconfig*.json`, `src/template`, `src/manifest`) as the canon Prettier config prints them, so an app's prettier hook passes them and formatting them is no longer `HFS_SYNC_DRIFT`; the managed lite sign-in templates are Prettier output.
|
|
10
|
+
- Added: hfs.json `"modules": "data"` declares that an app keeps its agent modules as data (`.starcistacks/seeds/data/modules`): `app sync` renders no registry package `packages/modules` (one the app keeps is its own code), and `app add|remove module` refuse `HFS_MODULES_ARE_DATA`. An app without the key keeps the module-packages render.
|
|
11
|
+
- Added: seed levels `.starcistacks/seeds/` (every environment, first) and `.starcistacks/<env>/seeds/`, each a `seeds.yaml` (`starci/seeds@1`) with `sql/`, `data/` and `files/` and entry kinds `sql`, `data`, `test-account` and `files`, judged by R375 (`HFS_SEEDS_SHAPE`, `HFS_SEED_MANIFEST`, `HFS_SEED_DDL`, `HFS_SEED_DATA_SCHEMA`, `HFS_SEED_TEST_DATA`, `HFS_SEED_SECRET_REF`); `HFS_SEED_PII` also reads JSON and YAML seed data; a production level holds test data only in its one declared `test-account` entry. New apps are scaffolded with the three levels.
|
|
12
|
+
- Added: the seed step (`stack/seeds.mts`): receipts in `public.starci_seed_receipts` (scope, id, content hash, secret member names with a digest), `once` and `upsert` entries, secret references (`{ "secret": "seeds-<name>" }`) opened in memory with the env's age identity and passed to the app's seed command on stdin, never to disk, argv, logs or receipts; `files` entries are checked only. `app stack up` runs it after the migrations, and the deploy after convergence, its dry run listing the entries (PROJECT-STACKS-6, PROJECT-STACKS-12).
|
|
13
|
+
- Changed: `db baseline --squash` writes `.starcistacks/seeds/sql/reference.sql` and declares the entry `reference`; the scaffold's `cli seed run` reads the shared then the env `sql/` folder (the lite one takes `--env`); slot `app.db.seed` is removed and R332 refuses `db/seed.sql`.
|
|
14
|
+
- Fixed: `app codemod sops-rules` keeps the comment header `.sops.yaml` opens with (the managed header of `secret init`).
|
|
15
|
+
- Fixed: a fresh `app scaffold --edition full` passes its own check: the dev environment declares the `local` block `app stack up --env dev` runs, and its runbook names the `starci app stack` verbs instead of docker commands.
|
|
16
|
+
- Fixed: the Next image of a front end that reads `be/contracts/` copies it only when the back end holds it; R187 (`HFS_DOCKER_BUILD_CONTEXT`) refuses a COPY of a missing `be/contracts` and a missing COPY once contracts exist.
|
|
17
|
+
- Changed: the lite sign-up door takes an optional full name and keeps it as `full_name` and `name` in the account's profile data, and the scaffolded sign-up form asks for it; the lite account directory and the Google sign-in read it back into the session (`AccountStanding.name`, `@starci/auth` 0.3.0).
|
|
18
|
+
- Changed: the canon pins carry no release-age wording (a version, security fixes included, may be adopted as soon as it is published), and name `@starci/auth` 0.3.0, `@starci/auth-oidc` 0.1.2, the Grammar family 0.21.17, `@starci/eslint-canon-be` 3.15.0, `@starci/eslint-canon-fe` 9.13.0 and `@starci/jest-preset` 2.7.0.
|
|
19
|
+
- Upgrade: (1) `npx starci app codemod seeds-layout`, read the report, then `--execute`: it moves `db/seed.sql` and loose `.starcistacks/<env>/seeds/*.sql` into the seed levels and writes their manifests; move any secret out of seed data into a `seeds-<name>` member and declare production's one test account as a `test-account` entry. (2) An app that keeps its modules as data adds `"modules": "data"` to hfs.json. (3) An app whose back end emits no contract deletes `COPY be/contracts be/contracts` from its `fe/apps/*/Dockerfile`, which the app owns. (4) `npx starci app sync --execute` renders the jest packages scope, the CI lint matrix and the Prettier-clean managed files.
|
|
20
|
+
- Changed: slot `app.packages` places any root workspace package `packages/<name>/` (the agent SDK, a block library such as `packages/blocks-core`, a module package, later a reviewed plugin package), not only `agent-sdk` and `module-<name>`, so each is linted by the root config (`--side packages`) and read as a first-party package by the import graph; slot manifest 3.19.0 (ruling `root-packages-any-name`).
|
|
21
|
+
|
|
3
22
|
## 5.4.0 - 2026-11-03
|
|
4
23
|
|
|
5
24
|
- Fixed: `starci app sync --execute` never leaves an app uncompilable. Before it writes a managed TypeScript file or deletes a retired one, it compiles each front-end and back-end project with the change applied (the app's own TypeScript; the changed files and every file importing them, against what is on disk). A managed group whose render adds a type error, in its own files or in an app file that imports one, is held whole: `HFS_SYNC_GROUP_HELD`, exit 1, naming the group, its files and the first errors (what the app must add or still imports); the rest is written, and a side without TypeScript installed is named in a notice (`sync/compile-guard.mts`).
|
package/README.md
CHANGED
|
@@ -33,8 +33,9 @@ starci app ci secret set <NAME> [--from-custody] [--env <env>] [--repo <owner/na
|
|
|
33
33
|
`.starcistacks/application-stacks.yaml` (PROJECT-STACKS-6 of `contexts/project/stacks.md`): the host table (`doctor` prints the owner-run install
|
|
34
34
|
step per OS for each missing prerequisite and installs nothing), the dev custody (`secret init --env dev --execute` creates the dev-only values once),
|
|
35
35
|
the setup commands, the compose services and their health, the environment's own database role, the migrations with a receipt (`db migrate` runs its
|
|
36
|
-
client tools in the database container named by `STARCI_DB_CONTAINER`, so the host needs no PostgreSQL client), the seed
|
|
37
|
-
|
|
36
|
+
client tools in the database container named by `STARCI_DB_CONTAINER`, so the host needs no PostgreSQL client), the seed levels of `.starcistacks/`
|
|
37
|
+
with their receipts in the database (`stack/seeds.mts`, PROJECT-STACKS-12; `app codemod seeds-layout` moves an older app's seed data into them),
|
|
38
|
+
the declared seed commands once, the native app processes, readiness and the origins. `prepare`, `up` and `down` are dry runs until `--execute`; `down` never removes a volume. State (process ids,
|
|
38
39
|
logs, receipts) is under the state base (`STARCI_LOCAL_ROOT`), the opened custody under the private secrets directory outside the checkout.
|
|
39
40
|
|
|
40
41
|
`starci app add` writes every front-end unit, in either edition, already in the shape of the harness's `contexts/code/fe`
|
|
@@ -114,7 +115,7 @@ one is refused with both names. Object references and function bodies stay byte-
|
|
|
114
115
|
the kept pgmq queues and the grants on objects outside the app schemas), a marked section of `db/bootstrap.sql` (the source's app roles
|
|
115
116
|
with their attributes and no password, their memberships, their allowed role settings such as `search_path`, the grants on schemas the
|
|
116
117
|
bootstrap creates and the database-wide default privileges, none of which a per-database dump keeps; `starci app sync` judges and keeps
|
|
117
|
-
the rest of the file),
|
|
118
|
+
the rest of the file), `.starcistacks/seeds/sql/reference.sql` (the rows of `--seed-tables`, declared as the entry `reference` of the shared seed level, `insert ... on conflict do nothing` in foreign-key order with their
|
|
118
119
|
sequences set; a table without a primary key or unique constraint is refused) and moves the chain to `db/migrations/archive/`. Before
|
|
119
120
|
writing, a scratch container of the app's db image (built from `.starcistacks/images/db/Dockerfile`, or `--image`) replays bootstrap, baseline and seed (the
|
|
120
121
|
seed twice); the same projection read from the replay must equal the source's, or the verb refuses with the differences. Docker runs every
|
package/cache/test-family.d.mts
CHANGED
|
@@ -96,7 +96,7 @@ export declare function sourcesOf(record: CacheRecord): Record<string, SourceIte
|
|
|
96
96
|
* a failing one's is not, and the folder is replaced with exactly the current specs' records. The merged total is written to `<side>/coverage/lcov.info` and judged.
|
|
97
97
|
*/
|
|
98
98
|
export declare function runTestFamily(options: TestFamilyOptions): Promise<TestFamilyResult>;
|
|
99
|
-
/** The part of a Jest project config the closure needs. */
|
|
99
|
+
/** The part of a Jest project config the closure needs. A project's `roots` are where its haste map finds its files. */
|
|
100
100
|
interface ProjectConfigLike {
|
|
101
101
|
readonly rootDir: string;
|
|
102
102
|
readonly displayName?: {
|
package/cache/test-family.mjs
CHANGED
|
@@ -49,8 +49,9 @@ export function sourcesOf(record) {
|
|
|
49
49
|
}
|
|
50
50
|
function recordOf(spec, key, sources, side) {
|
|
51
51
|
const prefixed = {};
|
|
52
|
+
// A source is relative to the Jest root; a root package's (`../packages/<name>/...`) is outside it, so the joined path is normalised to the app root.
|
|
52
53
|
for (const [source, items] of Object.entries(sources))
|
|
53
|
-
prefixed[side === '' || side === '.' ? source : `${side}/${source}`] = items;
|
|
54
|
+
prefixed[side === '' || side === '.' ? source : path.posix.normalize(`${side}/${source}`)] = items;
|
|
54
55
|
return { path: spec, step: SPEC_STEP, hash: key, findings: [], data: { sources: prefixed } };
|
|
55
56
|
}
|
|
56
57
|
/**
|
|
@@ -163,24 +164,27 @@ export async function createJestEnvironment({ appRoot, side, jestFrom, project =
|
|
|
163
164
|
const Runtime = runtimeModule.default;
|
|
164
165
|
const { DependencyResolver } = fromCore('jest-resolve-dependencies');
|
|
165
166
|
const jestBin = path.join(path.dirname(jestPackage), 'bin', 'jest.js');
|
|
166
|
-
// The preset's config reads the working directory (the back end it describes), so it is read from the Jest root.
|
|
167
|
+
// The preset's config reads the working directory (the back end it describes), so it is read from the Jest root. Several projects may carry the name
|
|
168
|
+
// (the back end's unit project and the one of the app's root packages, rooted at the app root): each keeps its own haste map and resolver.
|
|
167
169
|
const previous = process.cwd();
|
|
168
|
-
|
|
169
|
-
let context;
|
|
170
|
+
const projects = [];
|
|
170
171
|
try {
|
|
171
172
|
process.chdir(root);
|
|
172
173
|
const { configs } = await readConfigs({ $0: 'jest', _: [], selectProjects: [project] }, [root]);
|
|
173
|
-
config
|
|
174
|
-
|
|
174
|
+
for (const config of configs.filter((entry) => entry.displayName?.name === project)) {
|
|
175
|
+
const context = await Runtime.createContext(config, { maxWorkers: 1, watchman: false, console: { log: () => undefined, warn: () => undefined, error: () => undefined } });
|
|
176
|
+
projects.push({ config, resolver: new DependencyResolver(context.resolver, context.hasteFS, undefined), hasteFS: context.hasteFS });
|
|
177
|
+
}
|
|
178
|
+
if (projects.length === 0)
|
|
175
179
|
throw new Error(`the Jest config under ${side} has no project named ${project}`);
|
|
176
|
-
context = await Runtime.createContext(config, { maxWorkers: 1, watchman: false, console: { log: () => undefined, warn: () => undefined, error: () => undefined } });
|
|
177
180
|
}
|
|
178
181
|
finally {
|
|
179
182
|
process.chdir(previous);
|
|
180
183
|
}
|
|
181
|
-
const resolver = new DependencyResolver(context.resolver, context.hasteFS, undefined);
|
|
182
184
|
const tracked = (file) => !file.split(/[\\/]/u).includes('node_modules');
|
|
183
|
-
|
|
185
|
+
/** The project whose haste map holds `file`: a module is resolved the way the project that runs its spec resolves it. */
|
|
186
|
+
const projectOf = (file) => projects.find((entry) => entry.hasteFS.exists(file)) ?? projects[0];
|
|
187
|
+
const closureFrom = (roots, owner) => {
|
|
184
188
|
const seen = new Set();
|
|
185
189
|
const queue = roots.filter(tracked);
|
|
186
190
|
while (queue.length > 0) {
|
|
@@ -188,15 +192,14 @@ export async function createJestEnvironment({ appRoot, side, jestFrom, project =
|
|
|
188
192
|
if (file === undefined || seen.has(file))
|
|
189
193
|
continue;
|
|
190
194
|
seen.add(file);
|
|
191
|
-
for (const next of resolver.resolve(file))
|
|
195
|
+
for (const next of owner.resolver.resolve(file))
|
|
192
196
|
if (tracked(next) && !seen.has(next))
|
|
193
197
|
queue.push(next);
|
|
194
198
|
}
|
|
195
199
|
return [...seen];
|
|
196
200
|
};
|
|
197
201
|
const relative = (file) => posix(path.relative(appRoot, file));
|
|
198
|
-
const
|
|
199
|
-
const setupClosure = closureFrom([...setup('setupFiles'), ...setup('setupFilesAfterEnv')]).map(relative);
|
|
202
|
+
const setupClosures = new Map(projects.map((entry) => [entry, closureFrom([...(entry.config.setupFiles ?? []), ...(entry.config.setupFilesAfterEnv ?? [])], entry).map(relative)]));
|
|
200
203
|
const listed = completedNode(await runHeavyNode(execute, { kind: 'test', label: `app check jest ${project} discovery`, args: [jestBin, '--selectProjects', project, '--listTests'], cwd: root, maxBuffer: 64 * 1024 * 1024 }), 'jest discovery');
|
|
201
204
|
if (listed.status !== 0)
|
|
202
205
|
throw new Error(`jest --listTests failed under ${side}: ${(listed.stderr || listed.stdout).split('\n')[0]}`);
|
|
@@ -204,13 +207,15 @@ export async function createJestEnvironment({ appRoot, side, jestFrom, project =
|
|
|
204
207
|
const closures = new Map();
|
|
205
208
|
return {
|
|
206
209
|
specs,
|
|
207
|
-
configHash: projectConfigHash(config),
|
|
210
|
+
configHash: projects.length === 1 ? projectConfigHash(projects[0].config) : combineHashes(Object.fromEntries(projects.map((entry, index) => [String(index), projectConfigHash(entry.config)]))),
|
|
208
211
|
closureOf(spec) {
|
|
209
212
|
const cached = closures.get(spec);
|
|
210
213
|
if (cached !== undefined)
|
|
211
214
|
return cached;
|
|
212
|
-
const
|
|
213
|
-
const
|
|
215
|
+
const file = path.resolve(appRoot, spec);
|
|
216
|
+
const owner = projectOf(file);
|
|
217
|
+
const own = closureFrom([file], owner).map(relative).filter((entry) => entry !== spec);
|
|
218
|
+
const closure = [...new Set([...own, ...(setupClosures.get(owner) ?? [])])].sort();
|
|
214
219
|
closures.set(spec, closure);
|
|
215
220
|
return closure;
|
|
216
221
|
},
|
package/codemod/main.mjs
CHANGED
|
@@ -14,10 +14,11 @@ import { migrateModuleDefinition, moduleDefinitionMarkdown } from './module-defi
|
|
|
14
14
|
import { migrateQualityBinding, qualityBindingMarkdown } from './quality-binding.mjs';
|
|
15
15
|
import { migrateSopsRules, sopsRulesMarkdown } from './sops-rules.mjs';
|
|
16
16
|
import { migrateSecretsCache, secretsCacheMarkdown } from './secrets-cache.mjs';
|
|
17
|
+
import { migrateSeedsLayout, seedsLayoutMarkdown } from './seeds-layout.mjs';
|
|
17
18
|
import { migrateThemeTokens, themeTokensMarkdown } from './theme-tokens.mjs';
|
|
18
19
|
import { migrateTierBinding, tierBindingMarkdown } from './tier-binding.mjs';
|
|
19
20
|
/** The codemods `starci app codemod` runs, by name. */
|
|
20
|
-
export const CODEMODS = Object.freeze(['button-intent', 'spacing-roles', 'theme-tokens', 'small-renames', 'page-navigation', 'section-frame', 'surface-card-kinds', 'tier-binding', 'loading-states', 'explicit-surface', 'directive-first', 'secrets-cache', 'module-definition', 'sops-rules', 'quality-binding', 'account-doors']);
|
|
21
|
+
export const CODEMODS = Object.freeze(['button-intent', 'spacing-roles', 'theme-tokens', 'small-renames', 'page-navigation', 'section-frame', 'surface-card-kinds', 'tier-binding', 'loading-states', 'explicit-surface', 'directive-first', 'secrets-cache', 'module-definition', 'sops-rules', 'quality-binding', 'account-doors', 'seeds-layout']);
|
|
21
22
|
/** The arguments of every report-style `starci app codemod`: --execute, --json, --format and --out; any other is refused. */
|
|
22
23
|
export function spacingRolesArguments(argv) {
|
|
23
24
|
const options = { write: false, format: 'markdown' };
|
|
@@ -51,6 +52,10 @@ export function spacingRolesArguments(argv) {
|
|
|
51
52
|
}
|
|
52
53
|
/** One of the report-style codemods run at an app root: its report object and the Markdown of it. */
|
|
53
54
|
function runCodemod(name, root, write) {
|
|
55
|
+
if (name === 'seeds-layout') {
|
|
56
|
+
const report = migrateSeedsLayout({ root, write });
|
|
57
|
+
return { report, markdown: seedsLayoutMarkdown(report) };
|
|
58
|
+
}
|
|
54
59
|
if (name === 'quality-binding') {
|
|
55
60
|
const report = migrateQualityBinding({ root, write });
|
|
56
61
|
return { report, markdown: qualityBindingMarkdown(report) };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** One seed file the migration moves into a level's `sql/` and declares in its manifest; `drop` when the target already holds its bytes. */
|
|
2
|
+
export interface SeedsLayoutMove {
|
|
3
|
+
from: string;
|
|
4
|
+
to: string;
|
|
5
|
+
entry: string;
|
|
6
|
+
action: 'move' | 'drop';
|
|
7
|
+
}
|
|
8
|
+
/** One case left for a person. */
|
|
9
|
+
export interface SeedsLayoutFinding {
|
|
10
|
+
file: string;
|
|
11
|
+
code: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
}
|
|
14
|
+
/** The report of a migration (`starci app codemod seeds-layout`). */
|
|
15
|
+
export interface SeedsLayoutReport {
|
|
16
|
+
schema: string;
|
|
17
|
+
root: string;
|
|
18
|
+
write: boolean;
|
|
19
|
+
environments: string[];
|
|
20
|
+
moves: SeedsLayoutMove[];
|
|
21
|
+
created: string[];
|
|
22
|
+
findings: SeedsLayoutFinding[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Migrate an app's seed data into the seed levels of `.starcistacks/` (ruling seed-levels, contexts/project/stacks.md PROJECT-STACKS-12):
|
|
26
|
+
* `db/seed.sql` moves to the shared level's `sql/reference.sql` and each loose SQL file of `<env>/seeds/` to that level's `sql/`, each
|
|
27
|
+
* declared in its level's `seeds.yaml` (target the primary database, mode upsert); every level gets its manifest and a `.gitkeep` for each
|
|
28
|
+
* kind folder that holds nothing. A target with the same bytes drops the old copy; other bytes, a loose seed that is no SQL and a source that
|
|
29
|
+
* still names an old home are left for a person. A second run finds nothing to do.
|
|
30
|
+
*/
|
|
31
|
+
export declare function migrateSeedsLayout({ root, write }: {
|
|
32
|
+
root: string;
|
|
33
|
+
write?: boolean;
|
|
34
|
+
}): SeedsLayoutReport;
|
|
35
|
+
/** Render the migration and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
36
|
+
export declare function seedsLayoutMarkdown(report: SeedsLayoutReport): string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { declaredEnvironments } from '../runtime/scripts/hfs/rules/seeds.mjs';
|
|
4
|
+
import { SEED_FILE } from '../runtime/scripts/hfs/rules/database-constants.mjs';
|
|
5
|
+
import { addSeedEntries, SEED_FOLDERS, SEEDS_KEEP, SEEDS_MANIFEST, SEEDS_SCHEMA, seedLevelDir } from '../runtime/scripts/hfs/stack-seeds-config.mjs';
|
|
6
|
+
import { filesUnder, relativePosix, sideFolders } from './source-tree.mjs';
|
|
7
|
+
/** The lite SQL seed of manifest 3.17 (slot app.db.seed): its rows are every environment's, so it moves to the shared level. */
|
|
8
|
+
const LEGACY_DB_SEED = 'db/seed.sql';
|
|
9
|
+
/** The id and target a moved SQL seed is declared with: the primary database, applied again whenever its content changes. */
|
|
10
|
+
const SQL_TARGET = 'primary';
|
|
11
|
+
/** How a source names an old seed home: the lite seed path, or the seeds folder of an environment read without its sql/ folder. */
|
|
12
|
+
const LEGACY_REFERENCE = /db\/seed\.sql|"\.starcistacks",\s*[A-Za-z_$][\w$]*,\s*"seeds"\)/u;
|
|
13
|
+
const SOURCE = /\.(?:[cm]?[jt]s|json)$/u;
|
|
14
|
+
const isFile = (target) => fs.statSync(target, { throwIfNoEntry: false })?.isFile() === true;
|
|
15
|
+
const isFolder = (target) => fs.statSync(target, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
16
|
+
const entryId = (name) => name.replace(/\.sql$/u, '').toLowerCase().replaceAll(/[^a-z0-9]+/gu, '-').replaceAll(/^-+|-+$/gu, '');
|
|
17
|
+
/** The loose seed files of each declared environment's level (directly in `<env>/seeds/`) and the lite `db/seed.sql`, with where each goes. */
|
|
18
|
+
function legacySeeds(root, environments, findings) {
|
|
19
|
+
const loose = environments.flatMap((env) => {
|
|
20
|
+
const level = path.join(root, seedLevelDir(env));
|
|
21
|
+
if (!isFolder(level))
|
|
22
|
+
return [];
|
|
23
|
+
return fs.readdirSync(level, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name !== SEEDS_MANIFEST && entry.name !== SEEDS_KEEP).flatMap((entry) => {
|
|
24
|
+
const from = `${seedLevelDir(env)}/${entry.name}`;
|
|
25
|
+
if (entry.name.endsWith('.sql'))
|
|
26
|
+
return [{ env, from, name: entry.name }];
|
|
27
|
+
findings.push({ file: from, code: 'SEEDS_LAYOUT_NOT_SQL', reason: `only a SQL seed moves by itself; put it under ${seedLevelDir(env)}/data/ or files/ and declare its target (and, for data, its schema) in ${SEEDS_MANIFEST}` });
|
|
28
|
+
return [];
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
return [...loose, ...(isFile(path.join(root, LEGACY_DB_SEED)) ? [{ env: null, from: LEGACY_DB_SEED, name: path.posix.basename(SEED_FILE) }] : [])];
|
|
32
|
+
}
|
|
33
|
+
/** The sources of the app's sides that still name an old seed home (a seed runner the app owns): each is left for a person. */
|
|
34
|
+
function referenceFindings(root) {
|
|
35
|
+
return sideFolders(root).flatMap((side) => filesUnder(side, SOURCE)).flatMap((file) => {
|
|
36
|
+
const line = fs.readFileSync(file, 'utf8').split('\n').findIndex((text) => LEGACY_REFERENCE.test(text));
|
|
37
|
+
return line === -1 ? [] : [{ file: `${relativePosix(root, file)}:${line + 1}`, code: 'SEEDS_LAYOUT_REFERENCE', reason: 'names the old seed home; read the sql/ folder of .starcistacks/seeds/ and then of .starcistacks/<env>/seeds/ (the scaffold\'s seed runner is the model)' }];
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** The placeholders and empty manifests a level lacks: `seeds.yaml`, and a `.gitkeep` in each kind folder that holds nothing. */
|
|
41
|
+
function levelSkeleton(root, env, landed) {
|
|
42
|
+
const dir = seedLevelDir(env);
|
|
43
|
+
const manifest = isFile(path.join(root, dir, SEEDS_MANIFEST)) || landed.has(`${dir}/${SEEDS_MANIFEST}`) ? [] : [`${dir}/${SEEDS_MANIFEST}`];
|
|
44
|
+
const keeps = SEED_FOLDERS.map((kind) => `${dir}/${kind}`).filter((folder) => !landed.has(folder) && (!isFolder(path.join(root, folder)) || fs.readdirSync(path.join(root, folder)).length === 0));
|
|
45
|
+
return [...manifest, ...keeps.map((folder) => `${folder}/${SEEDS_KEEP}`)];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Migrate an app's seed data into the seed levels of `.starcistacks/` (ruling seed-levels, contexts/project/stacks.md PROJECT-STACKS-12):
|
|
49
|
+
* `db/seed.sql` moves to the shared level's `sql/reference.sql` and each loose SQL file of `<env>/seeds/` to that level's `sql/`, each
|
|
50
|
+
* declared in its level's `seeds.yaml` (target the primary database, mode upsert); every level gets its manifest and a `.gitkeep` for each
|
|
51
|
+
* kind folder that holds nothing. A target with the same bytes drops the old copy; other bytes, a loose seed that is no SQL and a source that
|
|
52
|
+
* still names an old home are left for a person. A second run finds nothing to do.
|
|
53
|
+
*/
|
|
54
|
+
export function migrateSeedsLayout({ root, write = false }) {
|
|
55
|
+
const environments = [...(declaredEnvironments(root)?.keys() ?? [])];
|
|
56
|
+
const report = { schema: 'starci/seeds-layout-report@1', root, write, environments, moves: [], created: [], findings: [] };
|
|
57
|
+
const landed = new Set();
|
|
58
|
+
const entries = new Map();
|
|
59
|
+
for (const { env, from, name } of legacySeeds(root, environments, report.findings)) {
|
|
60
|
+
const to = `${seedLevelDir(env)}/sql/${name}`;
|
|
61
|
+
const source = path.join(root, from);
|
|
62
|
+
const target = path.join(root, to);
|
|
63
|
+
if (isFile(target) && !fs.readFileSync(target).equals(fs.readFileSync(source))) {
|
|
64
|
+
report.findings.push({ file: from, code: 'SEEDS_LAYOUT_CONFLICT', reason: `${to} already holds other data; merge the two by hand and delete ${from}` });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const id = entryId(name);
|
|
68
|
+
report.moves.push({ from, to, entry: id, action: isFile(target) ? 'drop' : 'move' });
|
|
69
|
+
entries.set(env, [...(entries.get(env) ?? []), { id, kind: 'sql', path: `sql/${name}`, target: SQL_TARGET, mode: 'upsert' }]);
|
|
70
|
+
landed.add(`${seedLevelDir(env)}/sql`).add(`${seedLevelDir(env)}/${SEEDS_MANIFEST}`);
|
|
71
|
+
if (!write)
|
|
72
|
+
continue;
|
|
73
|
+
if (isFile(target))
|
|
74
|
+
fs.rmSync(source);
|
|
75
|
+
else {
|
|
76
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
77
|
+
fs.renameSync(source, target);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const [env, list] of entries)
|
|
81
|
+
addSeedEntries(root, env, list, write);
|
|
82
|
+
for (const env of [null, ...environments])
|
|
83
|
+
report.created.push(...levelSkeleton(root, env, landed));
|
|
84
|
+
if (write) {
|
|
85
|
+
for (const file of report.created) {
|
|
86
|
+
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
|
87
|
+
fs.writeFileSync(path.join(root, file), file.endsWith(SEEDS_MANIFEST) ? `schema: ${SEEDS_SCHEMA}\nentries: []\n` : '');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
report.findings.push(...referenceFindings(root));
|
|
91
|
+
return report;
|
|
92
|
+
}
|
|
93
|
+
const cell = (value) => String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
|
|
94
|
+
/** Render the migration and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
95
|
+
export function seedsLayoutMarkdown(report) {
|
|
96
|
+
const done = report.write ? 'moved' : 'would be moved';
|
|
97
|
+
const out = ['# seeds-layout', '', `${report.moves.length} seed file(s) ${done}, ${report.created.length} level file(s) ${report.write ? 'created' : 'would be created'}, ${report.findings.length} left for a person.`, ''];
|
|
98
|
+
if (report.findings.length) {
|
|
99
|
+
out.push('## Left for a person', '', '| file | code | reason |', '|---|---|---|');
|
|
100
|
+
for (const finding of report.findings)
|
|
101
|
+
out.push(`| ${cell(finding.file)} | ${finding.code} | ${cell(finding.reason)} |`);
|
|
102
|
+
out.push('');
|
|
103
|
+
}
|
|
104
|
+
if (report.moves.length) {
|
|
105
|
+
out.push('## Moved', '', '| from | to | entry | action |', '|---|---|---|---|');
|
|
106
|
+
for (const item of report.moves)
|
|
107
|
+
out.push(`| ${cell(item.from)} | ${cell(item.to)} | ${item.entry} | ${item.action} |`);
|
|
108
|
+
out.push('');
|
|
109
|
+
}
|
|
110
|
+
if (report.created.length)
|
|
111
|
+
out.push('## Created', '', ...report.created.map((file) => `- ${file}`), '');
|
|
112
|
+
return `${out.join('\n')}\n`;
|
|
113
|
+
}
|
package/codemod/sops-rules.d.mts
CHANGED
|
@@ -24,7 +24,7 @@ export declare function managedPathRegex(from: string): string;
|
|
|
24
24
|
/**
|
|
25
25
|
* Migrate a legacy `.sops.yaml` to the form `starci app secret init --env` writes (public recipients only, a rule per member spelling): a rule whose
|
|
26
26
|
* path_regex matches the legacy `.enc` spelling alone also matches `.enc.yaml`, the spelling `secret set` writes, which sops otherwise refuses with
|
|
27
|
-
* no creation rule. Recipients
|
|
27
|
+
* no creation rule. Recipients, the other rules and the comment header the file opens with (the managed header of `secret init`) are kept; nothing private is read or written.
|
|
28
28
|
*/
|
|
29
29
|
export declare function migrateSopsRules({ root, write }: {
|
|
30
30
|
root: string;
|
package/codemod/sops-rules.mjs
CHANGED
|
@@ -15,19 +15,24 @@ export function managedPathRegex(from) {
|
|
|
15
15
|
return CURRENT;
|
|
16
16
|
return /secrets/.test(from) && from.endsWith(LEGACY_END) ? `${from.slice(0, -LEGACY_END.length)}${MODERN_END}` : from;
|
|
17
17
|
}
|
|
18
|
+
/** The comment lines a file opens with (its managed header among them), which a rewrite of the YAML body keeps as written. */
|
|
19
|
+
function leadingComments(text) {
|
|
20
|
+
return text.replace(/^((?:[ \t]*(?:#[^\n]*)?\r?\n)*)[\s\S]*$/, '$1');
|
|
21
|
+
}
|
|
18
22
|
/**
|
|
19
23
|
* Migrate a legacy `.sops.yaml` to the form `starci app secret init --env` writes (public recipients only, a rule per member spelling): a rule whose
|
|
20
24
|
* path_regex matches the legacy `.enc` spelling alone also matches `.enc.yaml`, the spelling `secret set` writes, which sops otherwise refuses with
|
|
21
|
-
* no creation rule. Recipients
|
|
25
|
+
* no creation rule. Recipients, the other rules and the comment header the file opens with (the managed header of `secret init`) are kept; nothing private is read or written.
|
|
22
26
|
*/
|
|
23
27
|
export function migrateSopsRules({ root, write = false }) {
|
|
24
28
|
const report = { schema: 'starci/sops-rules-report@1', root, write, file: FILE, changes: [], findings: [] };
|
|
25
29
|
const target = path.join(root, FILE);
|
|
26
30
|
if (!fs.existsSync(target))
|
|
27
31
|
return report;
|
|
32
|
+
const text = fs.readFileSync(target, 'utf8');
|
|
28
33
|
let config;
|
|
29
34
|
try {
|
|
30
|
-
config = parseYaml(
|
|
35
|
+
config = parseYaml(text);
|
|
31
36
|
}
|
|
32
37
|
catch {
|
|
33
38
|
config = null;
|
|
@@ -46,7 +51,7 @@ export function migrateSopsRules({ root, write = false }) {
|
|
|
46
51
|
rule.path_regex = to;
|
|
47
52
|
});
|
|
48
53
|
if (report.changes.length > 0 && write)
|
|
49
|
-
fs.writeFileSync(target, stringifyYaml(config));
|
|
54
|
+
fs.writeFileSync(target, `${leadingComments(text)}${stringifyYaml(config)}`);
|
|
50
55
|
return report;
|
|
51
56
|
}
|
|
52
57
|
const cell = (value) => String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
|
package/lint/run.d.mts
CHANGED
|
@@ -15,8 +15,9 @@ export interface LinterRun {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* What a linter run is asked for. `list` is the files an ESLint run lints (cwd-relative, posix) when they are named one by one: they go to the child
|
|
18
|
-
* through a file, never the command line, and `args` then holds the options alone. `files` is the number of source files the
|
|
19
|
-
* heavy-command budget of the ESLint kind scales with
|
|
18
|
+
* through a file, never the command line, and `args` then holds the options alone. `files` is the number of source files the child's work is made of,
|
|
19
|
+
* which the heavy-command budget of the ESLint kind scales with: the typed program and the project graph of a side are built whole for a named list
|
|
20
|
+
* too, so a list run of a side whose scope is known names the side's count; without `files` a list names its own count.
|
|
20
21
|
*/
|
|
21
22
|
export interface LinterSpec {
|
|
22
23
|
cwd: string;
|
|
@@ -134,8 +135,8 @@ export interface CachedLinters {
|
|
|
134
135
|
}
|
|
135
136
|
/**
|
|
136
137
|
* ESLint and stylelint through the result cache: per side, the files the records cannot vouch for go to the very launcher, chunking and result
|
|
137
|
-
* validation of a full run (eslintRepository), the rest take their records
|
|
138
|
-
* results, so the lint report is built the same way from either.
|
|
138
|
+
* validation of a full run (eslintRepository), the rest take their records; then the root packages, linted whole. Each scope runs on its own
|
|
139
|
+
* (scopeFailure). The answer has the shape of eslintRepository's, plus the stylelint results, so the lint report is built the same way from either.
|
|
139
140
|
*/
|
|
140
141
|
export declare function cachedLinters({ repoRoot, app, side: only, run, session }: {
|
|
141
142
|
repoRoot: string;
|
package/lint/run.mjs
CHANGED
|
@@ -84,7 +84,7 @@ export async function runLinter({ cwd, pkg, bin, args, bound = null, list, files
|
|
|
84
84
|
fs.writeFileSync(job, JSON.stringify({ files: list ?? ['.'], fix: args.includes('--fix'), ...(snapshotFile === null || graph === undefined ? {} : { graph: { out: snapshotFile, canons: graph } }) }));
|
|
85
85
|
childArgs = [LIST_RUNNER, job];
|
|
86
86
|
}
|
|
87
|
-
const count = list?.length
|
|
87
|
+
const count = files ?? list?.length;
|
|
88
88
|
let run;
|
|
89
89
|
let handed;
|
|
90
90
|
try {
|
|
@@ -365,8 +365,10 @@ async function launchEslint({ app, repoRoot, opts, changed, existing, run = runL
|
|
|
365
365
|
// 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); the packages are no side folder and have no graph
|
|
366
366
|
const graph = handOutGraph && !packages ? graphCanonsOf(repoRoot, cwd, side) : undefined;
|
|
367
367
|
// 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
|
|
368
|
-
// child would build the whole TypeScript program again); the whole side is a single directory argument.
|
|
369
|
-
|
|
368
|
+
// child would build the whole TypeScript program again); the whole side is a single directory argument. Either child builds the typed program
|
|
369
|
+
// and the project graph of the whole side, so its liveness budget scales with the side's scope when the caller knows it, never with the diff alone.
|
|
370
|
+
const programFiles = known?.length ?? expected.length;
|
|
371
|
+
const launch = Promise.resolve().then(() => run(changed ? { cwd, pkg: 'eslint', bin: 'eslint', args: head, list: sources, files: programFiles, bound: repoRoot, ...(graph === undefined ? {} : { graph }) } : { cwd, pkg: 'eslint', bin: 'eslint', args: [...head, packages ? PACKAGES : '.'], files: programFiles, bound: repoRoot, ...(graph === undefined ? {} : { graph }) }));
|
|
370
372
|
launches.push({ side, cwd, expected, run: launch.catch(spawnFailure) });
|
|
371
373
|
}
|
|
372
374
|
return launches;
|
|
@@ -489,10 +491,20 @@ export function gitBaseToken(repoRoot) {
|
|
|
489
491
|
}
|
|
490
492
|
return 'no-base';
|
|
491
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* A scope whose linter reached no complete measurement (a child ended by its liveness budget, a capture cut short) is a tool error of that scope, and the
|
|
496
|
+
* next scope still runs: the root packages are judged whatever became of the back end. Nothing of the failed scope is recorded (its records are written
|
|
497
|
+
* only after its child completed). A hold of the host (HOST_HELD, HEAVY_DEFERRED) still ends the run, since no scope would be admitted either.
|
|
498
|
+
*/
|
|
499
|
+
function scopeFailure(error, scope) {
|
|
500
|
+
if (!isNodeExecutionRefusal(error) || isHostHeld(error) || error.code === 'HEAVY_DEFERRED')
|
|
501
|
+
throw error;
|
|
502
|
+
return `eslint did not complete in ${scope}/: ${String(error.message).split('\n')[0]}`;
|
|
503
|
+
}
|
|
492
504
|
/**
|
|
493
505
|
* ESLint and stylelint through the result cache: per side, the files the records cannot vouch for go to the very launcher, chunking and result
|
|
494
|
-
* validation of a full run (eslintRepository), the rest take their records
|
|
495
|
-
* results, so the lint report is built the same way from either.
|
|
506
|
+
* validation of a full run (eslintRepository), the rest take their records; then the root packages, linted whole. Each scope runs on its own
|
|
507
|
+
* (scopeFailure). The answer has the shape of eslintRepository's, plus the stylelint results, so the lint report is built the same way from either.
|
|
496
508
|
*/
|
|
497
509
|
export async function cachedLinters({ repoRoot, app, side: only, run, session }) {
|
|
498
510
|
const eslint = { findings: [], errors: [], raw: [], engine: { sides: {} } };
|
|
@@ -514,23 +526,31 @@ export async function cachedLinters({ repoRoot, app, side: only, run, session })
|
|
|
514
526
|
continue;
|
|
515
527
|
}
|
|
516
528
|
const styled = side === STYLE_SIDE;
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
529
|
+
let outcome;
|
|
530
|
+
try {
|
|
531
|
+
outcome = await lintSide({
|
|
532
|
+
session, side, sideRoot: cwd, scope, stylesheets: styled ? stylesheetsOf(cwd) : [],
|
|
533
|
+
canons, styleCanon: styled ? await styleDeclaration({ repoRoot, cwd }) : null, typescript: () => loadAppTypeScript(cwd), extraKey: '', gitToken, full, write: true,
|
|
534
|
+
eslint: async (files) => {
|
|
535
|
+
const listed = files === null ? null : files.map((file) => `${side}/${file}`);
|
|
536
|
+
const sub = await eslintRepository({ repoRoot, app, opts: { side }, changed: listed, existing: listed ?? [], run, scopes: { [side]: scope }, handOutGraph: true });
|
|
537
|
+
return { results: sub.raw, errors: sub.errors, ...(sub.graphs[side] === undefined ? {} : { graph: sub.graphs[side] }) };
|
|
538
|
+
},
|
|
539
|
+
stylelint: styled ? async (files) => {
|
|
540
|
+
const linted = await (run ?? runLinter)({ cwd, pkg: 'stylelint', bin: 'stylelint', args: [...(files ?? [STYLE_GLOB]), '--formatter', 'json'] });
|
|
541
|
+
const appRelative = appRelativeMessages(STYLE_SIDE, cwd);
|
|
542
|
+
for (const result of linted.results ?? [])
|
|
543
|
+
for (const warning of result.warnings ?? [])
|
|
544
|
+
warning.text = appRelative(warning.text);
|
|
545
|
+
return { results: (linted.results ?? []), errors: linted.error ? [linted.error] : [] };
|
|
546
|
+
} : null,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
eslint.errors.push(scopeFailure(error, side));
|
|
551
|
+
eslint.engine.sides[side] = { files: 0 };
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
534
554
|
sides.push({ side, stats: outcome.stats });
|
|
535
555
|
for (const message of outcome.errors)
|
|
536
556
|
(message.startsWith('stylelint ') ? style.errors : eslint.errors).push(message);
|
|
@@ -544,11 +564,17 @@ export async function cachedLinters({ repoRoot, app, side: only, run, session })
|
|
|
544
564
|
}
|
|
545
565
|
// The root packages are small and have no result records: they are linted whole on every run, through the same launcher.
|
|
546
566
|
if ((only === undefined || only === PACKAGES) && hasPackagesLint(repoRoot)) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
567
|
+
try {
|
|
568
|
+
const linted = await eslintRepository({ repoRoot, app, opts: { side: PACKAGES }, run });
|
|
569
|
+
eslint.errors.push(...linted.errors);
|
|
570
|
+
eslint.raw.push(...linted.raw);
|
|
571
|
+
eslint.findings.push(...linted.findings);
|
|
572
|
+
eslint.engine.sides[PACKAGES] = linted.engine.sides[PACKAGES] ?? { files: 0 };
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
eslint.errors.push(scopeFailure(error, PACKAGES));
|
|
576
|
+
eslint.engine.sides[PACKAGES] = { files: 0 };
|
|
577
|
+
}
|
|
552
578
|
}
|
|
553
579
|
session.report(lintFamilyReport(sides));
|
|
554
580
|
return { eslint, stylelint: style, sides };
|
package/package.json
CHANGED
package/release/lint-report.mjs
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { firstLine, tool } from '../stack/context.mjs';
|
|
8
|
-
|
|
8
|
+
import { PACKAGES, PACKAGES_CONFIG } from '../lint/run.mjs';
|
|
9
|
+
const LINT_SIDES = ['be', 'fe', PACKAGES];
|
|
9
10
|
const MERGED_REPORT = 'reports/lint.sonar.json';
|
|
10
11
|
const sideReport = (side) => `reports/lint.${side}.sonar.json`;
|
|
11
12
|
/** CI merge: rules unique by id (last one wins, first position kept), issues concatenated in side order. */
|
|
@@ -14,8 +15,8 @@ function mergeSonarReports(reports) {
|
|
|
14
15
|
const issues = reports.flatMap((report) => report.issues);
|
|
15
16
|
return { rules, issues };
|
|
16
17
|
}
|
|
17
|
-
/** The
|
|
18
|
-
const sidesOf = (root) => LINT_SIDES.filter((side) => fs.existsSync(path.join(root, side)));
|
|
18
|
+
/** The lint scopes the app has: a be/ or fe/ folder beside the app root, and its root packages when it has their managed config. */
|
|
19
|
+
const sidesOf = (root) => LINT_SIDES.filter((side) => fs.existsSync(path.join(root, side)) && (side !== PACKAGES || fs.existsSync(path.join(root, PACKAGES_CONFIG))));
|
|
19
20
|
/**
|
|
20
21
|
* Runs `starci app check --gate lint --side <side> --out <file>` for every side the app has, then writes reports/lint.sonar.json.
|
|
21
22
|
* `launch` turns an argv into [command, args] for the platform. Returns { pass, text }: the honest log of every lint run and the verdict.
|