@starci/hfs 5.0.7 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +7 -8
- package/cache/fake-session.mjs +1 -1
- package/cache/hfs-family.mjs +22 -9
- package/cache/import-graph.d.mts +32 -4
- package/cache/import-graph.mjs +88 -11
- package/cache/lint-family.d.mts +10 -3
- package/cache/lint-family.mjs +65 -37
- package/cache/records.d.mts +7 -0
- package/cache/records.mjs +17 -1
- package/cache/run.d.mts +1 -1
- package/cache/session.mjs +6 -2
- package/cache/side-key.d.mts +2 -0
- package/cache/side-key.mjs +5 -1
- package/cache/structure-memo.d.mts +34 -0
- package/cache/structure-memo.mjs +150 -0
- package/cache/types.d.mts +6 -2
- package/cache/wiring-family.mjs +1 -1
- package/codemod/directive-first.d.mts +48 -0
- package/codemod/directive-first.mjs +136 -0
- package/codemod/explicit-surface-edit.d.mts +14 -0
- package/codemod/explicit-surface-edit.mjs +57 -0
- package/codemod/explicit-surface-names.d.mts +30 -0
- package/codemod/explicit-surface-names.mjs +135 -0
- package/codemod/explicit-surface-render.d.mts +29 -0
- package/codemod/explicit-surface-render.mjs +197 -0
- package/codemod/explicit-surface.d.mts +50 -0
- package/codemod/explicit-surface.mjs +374 -0
- package/codemod/main.d.mts +7 -5
- package/codemod/main.mjs +37 -16
- package/codemod/small-renames.mjs +22 -2
- package/codemod/source-tree.d.mts +8 -0
- package/codemod/source-tree.mjs +23 -0
- package/lint/eslint-list.cjs +55 -0
- package/lint/run.d.mts +35 -61
- package/lint/run.mjs +91 -288
- package/package.json +1 -1
- package/release/lint-report.d.mts +1 -1
- package/release/lint-report.mjs +3 -3
- package/report/sonar.d.mts +1 -1
- package/report/sonar.mjs +3 -3
- package/report/stylelint-findings.d.mts +23 -0
- package/report/stylelint-findings.mjs +9 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +8 -8
- package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +56 -5
- package/runtime/knowledge/hfs/slots.yaml +1 -1
- package/runtime/knowledge/sonar-gate.yaml +2 -2
- package/runtime/modules/kernel/failure-codes.yaml +4 -4
- package/runtime/scripts/api/node/run-node.d.mts +5 -0
- package/runtime/scripts/hfs/architecture/test-world-files.d.mts +4 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +26 -7
- package/runtime/scripts/hfs/check.d.mts +16 -1
- package/runtime/scripts/hfs/check.mjs +18 -7
- package/runtime/scripts/hfs/explicit-surface-rule.d.mts +58 -0
- package/runtime/scripts/hfs/explicit-surface-rule.mjs +59 -0
- package/runtime/scripts/hfs/gate-threshold.mjs +1 -1
- package/runtime/scripts/hfs/rule-memo.d.mts +30 -0
- package/runtime/scripts/hfs/rule-memo.mjs +24 -0
- package/runtime/scripts/hfs/rules/a15-findings.d.mts +6 -0
- package/runtime/scripts/hfs/rules/a15-findings.mjs +11 -2
- package/runtime/scripts/hfs/rules/ci-standard.mjs +2 -2
- package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +9 -11
- package/runtime/scripts/hfs/rules/pipeline.mjs +13 -9
- package/runtime/scripts/hfs/rules/read.d.mts +5 -0
- package/runtime/scripts/hfs/rules/read.mjs +13 -0
- package/runtime/scripts/hfs/rules/secrets.d.mts +2 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -1
- package/runtime/scripts/hfs/rules/stacks-release.mjs +35 -21
- package/runtime/scripts/lib/declaration-surface.d.mts +35 -0
- package/runtime/scripts/lib/declaration-surface.mjs +112 -0
- package/runtime/scripts/lib/worktree-exclude.mjs +1 -1
- package/scaffold/app.mjs +3 -3
- package/src/check-cache-structure-support.d.mts +52 -0
- package/src/main.d.mts +3 -2
- package/src/main.mjs +123 -109
- package/sync/ci-commands.d.mts +2 -1
- package/sync/ci-commands.mjs +19 -20
- package/sync/index.mjs +2 -2
- package/templates/app/package-scripts/core.json +2 -2
- package/templates/app/task-graph/turbo.json +0 -3
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +2 -2
- package/templates/be/inbox/entities.index.ts.tpl +1 -1
- package/templates/be/inbox/module-definition.ts.tpl +8 -2
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +10 -2
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +8 -2
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +3 -2
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -3
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +3 -2
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +7 -6
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/jobs/job.processor.ts.tpl +1 -1
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +5 -4
- package/templates/be/patterns/projections/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -3
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/reactors/consumer.ts.tpl +1 -1
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +1 -1
- package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +1 -1
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +9 -3
- package/templates/be/platform/graphql/graphql.module-definition.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +9 -3
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +3 -2
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +2 -1
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +8 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/http-security/persistence/connection.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/http-security/persistence/rate-limit.sql.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +8 -3
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/http-security/persistence/entities/index.ts +1 -1
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +7 -1
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +7 -1
- package/templates/be/table/api/feature.module-definition.ts.tpl +9 -3
- package/templates/be/table/api/graphql.module-definition.ts.tpl +9 -3
- package/templates/be/table/entities.index.ts.tpl +1 -1
- package/templates/be/table/module-definition.ts.tpl +8 -3
- package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +1 -0
- package/templates/be/test-world/world/test-world.config.ts.tpl +8 -1
- package/templates/fe/common/app-layout.tsx.partial +1 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +2 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +11 -3
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +8 -2
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +2 -1
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +1 -1
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +4 -1
- package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +3 -2
- package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +12 -4
- package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +8 -3
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +4 -3
- package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +1 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +2 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -1
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +4 -3
- package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +2 -1
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +19 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +5 -1
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +4 -3
- package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +3 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +5 -1
- package/templates/fe/table/graphql/useMutateSwr.ts.tpl +2 -1
- package/templates/fe/table/graphql/useQuerySwr.ts.tpl +2 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/no-auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/error.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/http.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/refresh-token.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/retry.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/timeout.ts +1 -1
- package/templates/fe/units/page/index.tsx.tpl +3 -2
- package/templates/fe/units/slot/SlotView.tsx.tpl +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.0
|
|
4
|
+
|
|
5
|
+
- Added: `starci app codemod explicit-surface` writes the inferred type of every export the classifier counts as open (R368): return types, variable, parameter and property types, from the app's own TypeScript program. It prefers a named type the program exports, writes `ReactElement` for components and Next's `Metadata` for `generateMetadata`, adds merged type-only imports after a directive prologue, compiles the result beside the original and writes a file only when it gains no diagnostic, and reports (with a suggested fix) destructured exports and anonymous, computed, too long, `any`, unnameable and predicate types. Prettier of the app formats what is written.
|
|
6
|
+
- Added: `starci app codemod directive-first` moves a `"use client"` or `"use server"` below another statement, or in the form `("use client")`, to the first statement of its module (R369).
|
|
7
|
+
- Changed: the test-world declaration (R47, R89) is read in its typed form, `const world: TestWorldApi = defineTestWorld({ ... })` with `useTestWorld` and `useSandbox` exported typed from it, as well as in the destructured form; the template writes the typed form, and a config with neither is still refused.
|
|
8
|
+
- Added: `template-surface.spec.mts` holds every TypeScript template to R368 and R369 (the front-end units are judged through their generators).
|
|
9
|
+
- Changed: the scaffold templates write the type of every export (R368): Nest module definitions export each `ConfigurableModuleHost` member with its type, hooks name their result interface, async pages and layouts return `Promise<ReactElement>`, redux, zustand, next-intl and Apollo exports are typed, SQL texts are `SqlText`, entity and migration lists are `DatabaseConnectionOptions` members. A new app scaffolds clean under `explicit-export-surface`.
|
|
10
|
+
- Added: `runtime/scripts/lib/declaration-surface.mjs`, the one classifier of a TypeScript file's exported type surface (the isolated declaration emit and the exports it cannot write without inference, with the JSX-only component exemption), and `runtime/scripts/hfs/explicit-surface-rule.mjs`, the factory of the `explicit-export-surface` rule both lint canons build (R368). The app-check ESLint cache reads the same classifier for `open`, so the rule reports exactly the files the cache counts as open.
|
|
11
|
+
- Changed: the ESLint child of the cached lint hands its project-graph snapshot out. The list runner (`lint/eslint-list.cjs`, now also the runner of a whole-side run, as the list `.`) asks each installed canon for `projectGraphSnapshot` after the lint and writes the findings beside its results; `lintSide` writes the `graph` records from them and builds the graph itself only for a canon the child did not answer for. A whole-side lint no longer builds the project graph a second time in the CLI process.
|
|
12
|
+
|
|
13
|
+
## 5.0.8
|
|
14
|
+
|
|
15
|
+
- Removed: the `starci app lint` verb. `starci app check` is the one verb for app quality and carries what the lint verb carried: `--side be|fe` limits the lint gate to one side (reading and writing that side's cache records), `--out <file>` writes the merged Sonar Generic Issue Import document of the run (the findings of the selected gates under `starci-hfs`, then ESLint and stylelint), and `--fix` runs ESLint and stylelint with `--fix` before the check (with `--gate lint`). There is no `--changed` and no `--workspace`: the cache makes a whole-app check of an unchanged tree take seconds. The rendered `lint` and `lint:fix` scripts, the managed CI lint step and the release gate's Sonar row call `app check --gate lint`, and rule R13 requires that form.
|
|
16
|
+
- Changed: usage is printed only for a usage error (`UsageError`), not for a failed check.
|
|
17
|
+
- Fixed: the `small-renames` codemod keeps `size="control"` on a bar control instead of rewriting it to `lg`.
|
|
18
|
+
- Changed: the structure pass keeps its work in the committed cache. A side key carries the hash it took of each input, so a stable untracked input (generated code) no longer discards the record of a scoped step, and `CheckSession.unchanged(paths, captured?)` compares an input with that hash. The per-file content rules (plaintext secrets, bypass markers, soft file size) are kept by the file's hash, the cross-file finders by the read set they recorded, and `--gate secrets` reuses the same per-file records, so a warm run judges nothing and a run after an edit judges that file. The whole-program records of a side with untracked program inputs are kept.
|
|
19
|
+
- Changed: The app-check ESLint cache dirties a file only when the type surface it reads can have changed. Each file records the digest of its isolated declaration emit and whether that surface is open (inferred from other files); a body-only edit lints that file alone, a changed signature its direct importers, an inferred type one more level at a time. A file that only names the types of others (a re-export, an imported type in a signature) and a component that returns only JSX carry a change of those types, never an edit of their bodies. Records without a surface are read as changed.
|
|
20
|
+
- Fixed: A file edited while the ESLint of a side ran loses only its own cache records instead of the records of the whole side.
|
|
21
|
+
- Changed: The files of a diff lint go to ONE ESLint child through a job file read by a small runner (`lint/eslint-list.cjs`) on the ESLint API of the side's install, instead of several chunked command lines that each built the TypeScript program again. The heavy-node request carries the number of files it judges (`files`).
|
|
22
|
+
- Changed: The import graph of the app-check lint cache remembers the files and folders it probes while resolving imports, and the launcher takes the side's scope from the cache instead of enumerating it again; the graph of a large side builds in a sixth of the time.
|
|
23
|
+
|
|
3
24
|
## 5.0.7
|
|
4
25
|
|
|
5
26
|
- Changed: Refresh aligned canonical package pins.
|
package/README.md
CHANGED
|
@@ -16,9 +16,8 @@ starci app add overlay <name> --kind modal|drawer [--form] [--app <fe app>] [--c
|
|
|
16
16
|
starci app add service <dir> <name> [--inject <binding>...] [--cwd <dir>]
|
|
17
17
|
starci app add spec <file>.service.ts [--cwd <dir>]
|
|
18
18
|
starci app add image [--cwd <dir>]
|
|
19
|
-
starci app lint [--cwd <dir>] [--changed <file>...] [--workspace <dir>] [--fix] [--json] [--out <sonar file>]
|
|
20
19
|
starci app sync [--execute] [--cwd <dir>]
|
|
21
|
-
starci app check [--cwd <dir>] [--gate <hfs|format|db|secrets|wiring|types|lint|test|sonar|codecov> ...] [--no-cache] [--json] [--coverage-from <lcov>] [--quiet]
|
|
20
|
+
starci app check [--cwd <dir>] [--gate <hfs|format|db|secrets|wiring|types|lint|test|sonar|codecov> ...] [--side be|fe] [--fix] [--out <sonar file>] [--no-cache] [--json] [--coverage-from <lcov>] [--quiet]
|
|
22
21
|
starci app check --staged [--cwd <dir>]
|
|
23
22
|
starci app upgrade [--execute]
|
|
24
23
|
starci app world <up|down|status> [--stack <dir>] [--services <list>] [--k3d] [--force] [--json] [--cwd <dir>]
|
|
@@ -166,7 +165,7 @@ Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file
|
|
|
166
165
|
| `HFS_UNTRACKED_ROOT_ENTRY` | error | an entry git neither tracks nor ignores (`git ls-files -o --exclude-standard`), outside an `ignored` slot |
|
|
167
166
|
| `HFS_PLAINTEXT_SECRET` | error | a tracked plaintext secret: an env, key or credentials file, a value the push scan refuses (never printed), or an `.enc` that is no sops envelope (R06) |
|
|
168
167
|
| `HFS_STACKS_SHAPE` | error | a `.starcistacks` path outside the standard shape (a sealed file outside `<env>/secrets/`, `runtime/files/`, root `DESIGN.md` or `k8s/`), a local Sonar not owned by the host, a service still rooted at `.stacks` (R10) |
|
|
169
|
-
| `HFS_CI_MISSING_CANON` | error | `ci.yml` without a `run:` step of `starci app lint` (`npm run lint`, or `npx --no-install starci app lint`, the app's pinned copy) (R13) |
|
|
168
|
+
| `HFS_CI_MISSING_CANON` | error | `ci.yml` without a `run:` step of `starci app check --gate lint` (`npm run lint`, or `npx --no-install starci app check --gate lint`, the app's pinned copy) (R13) |
|
|
170
169
|
| `HFS_DEP_VERSION_SKEW` | error | a dependency at two specs across the root and workspace `package.json` files, a dependency declared at another version than the root `overrides` pin, or a nested copy of a declared dependency in `package-lock.json` (R14) |
|
|
171
170
|
| `HFS_CONTRACT_SNAPSHOT_DRIFT` | error | a back-end api app serving GraphQL without `be/contracts/<app>/schema.graphql` (R23); the front end reads that snapshot in place, so there is no copy to drift |
|
|
172
171
|
| `BE_TEST_TOPOLOGY` | error | a `*.test.*` file, a `testing/` folder, a second jest configuration or a `jest` key in `package.json` (R47; `int-spec`, `harness-spec`, the retired test folders and a per-lane test config are the machine's, under the same code) |
|
|
@@ -182,7 +181,7 @@ Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file
|
|
|
182
181
|
| `BE_ASYNC_SPEC_MISSING` | error | a consumed event no e2e spec through `useTestWorld` names, or a saga step whose spec does not also name the compensated event (R167) |
|
|
183
182
|
| `BE_INTEGRATION_SPEC_MISSING` | error | an integration (`src/modules/integrations/<provider>/` with `<provider>.config.ts`) without a `src/tests/integration/<provider>/*.integration-spec.ts` that registers its module through `useTestWorld({ modules })`, references its ErrorCode enum and drives an outage through the world (R112) |
|
|
184
183
|
| `HFS_MONO_WORKSPACES` | error | the app root `package.json` without `workspaces` exactly `["fe/apps/*", "fe/packages/*"]`, `packageManager` `npm@<version>` or a `turbo` devDependency (R143) |
|
|
185
|
-
| `HFS_MONO_FE_WORKSPACE` | error | an fe app `package.json` not named `@<project>/<app>`, not private, or without exactly the workspace scripts; an fe package without `build
|
|
184
|
+
| `HFS_MONO_FE_WORKSPACE` | error | an fe app `package.json` not named `@<project>/<app>`, not private, or without exactly the workspace scripts; an fe package without `build` and `typecheck`; a workspace with a `lint` script (R144) |
|
|
186
185
|
| `HFS_MONO_NEST_PROJECTS` | error | `be/nest-cli.json` not `monorepo: true`, its default project not an api app, or its `projects` not exactly the declared be apps (R145) |
|
|
187
186
|
| `HFS_MONO_WORKSPACE_DEP` | error | an fe workspace imports a package its own `package.json` does not declare, or the root `package.json` declares a workspace package (R146) |
|
|
188
187
|
| `BE_CLI_REQUIRED` | error | a back end with a connection or a command under `src/features/cli/` and no cli app `be/apps/cli`, a cli app of another name, or no `be/apps/cli/Dockerfile` (R147) |
|
|
@@ -200,7 +199,7 @@ Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file
|
|
|
200
199
|
The managed files are judged by `sync/managed.mjs` and `sync/ts-strict.mjs` (the package renders the templates; the runtime copy does not). The list of managed
|
|
201
200
|
files is the `managedBy` slots of `slots.yaml`; `starci app sync --execute` renders them and `starci app check` compares them, so a hand edit and a forgotten `sync` are the same finding.
|
|
202
201
|
Each finding is reported once: the managed ESLint and stylelint factory configs under R17, `tsconfig.json` under R22 when it names a flag, a workflow or hook that lost a canon step under R13 (or R19 for the format step), everything else under R05.
|
|
203
|
-
The root and both sides are rendered by the one mechanism. The root: the `scripts` block of `app.package-manifest` (`dev:be`, `dev:fe`, `build:be`, `build:fe`, `start:<app>`, `lint`, `lint:fix`, `test`, `test:integration`, `test:e2e`, `test:contract`, `test:world`, `codegen`, `contract:emit`, `migrate`, `typecheck`, ...; a be script runs from `be/`, where its tsconfig and jest configuration are), `app.format-config`, `app.hooks`, the CI slots (`app.ci`, `app.ci-e2e`, `app.ci-images`), `app.quality-config` and the managed block of `app.git-meta`. A side: its `be.tool-config` or `fe.tool-config` slot — the side's tsconfig (resolving `@starci/tsconfig` from the root `node_modules`) and its managed ESLint factory config, the back end's build tsconfig, tests tsconfig and jest configuration, the front end's stylelint configuration. `lint` is
|
|
202
|
+
The root and both sides are rendered by the one mechanism. The root: the `scripts` block of `app.package-manifest` (`dev:be`, `dev:fe`, `build:be`, `build:fe`, `start:<app>`, `lint`, `lint:fix`, `test`, `test:integration`, `test:e2e`, `test:contract`, `test:world`, `codegen`, `contract:emit`, `migrate`, `typecheck`, ...; a be script runs from `be/`, where its tsconfig and jest configuration are), `app.format-config`, `app.hooks`, the CI slots (`app.ci`, `app.ci-e2e`, `app.ci-images`), `app.quality-config` and the managed block of `app.git-meta`. A side: its `be.tool-config` or `fe.tool-config` slot — the side's tsconfig (resolving `@starci/tsconfig` from the root `node_modules`) and its managed ESLint factory config, the back end's build tsconfig, tests tsconfig and jest configuration, the front end's stylelint configuration. `lint` is `starci app check --gate lint`: ESLint over each side with its canon and stylelint over fe; `lint:fix` is the same with `--fix`. The front end has no test script, no test configuration and no e2e or coverage file: it has no tests, and `FE_NO_TESTS` (R97) refuses any spec, e2e file, test tool or test script under `fe/`. `turbo.json` stays the app's own
|
|
204
203
|
(slot `app.task-graph`): it carries the task graph, which no preset can render.
|
|
205
204
|
|
|
206
205
|
The architecture machine (`scripts/hfs/architecture.mjs` of the runtime, the same code bundled here): tiers and import
|
|
@@ -218,7 +217,7 @@ Predictions remain diagnostics; actual Sonar CI/release checks and Codecov CI up
|
|
|
218
217
|
findings n new, n known, n fixed`. Progress goes to stderr (`--quiet` silences it). `STARCI_APP_CHECK_CACHE=verify` runs diff then full and exits 2 on any difference;
|
|
219
218
|
`STARCI_APP_CHECK_CACHE=full` never reads the cache (the release gate).
|
|
220
219
|
|
|
221
|
-
Exit codes: 0 clean, 1 an error finding, 2 a tool could not run or a refusal (not a Git work tree, bad flag). `starci app check`
|
|
220
|
+
Exit codes: 0 clean, 1 an error finding, 2 a tool could not run or a refusal (not a Git work tree, bad flag). `starci app check` writes only `.starcistacks/cache/` (and `--out`'s file; `--fix` lets ESLint and stylelint fix in place).
|
|
222
221
|
|
|
223
222
|
## Secrets
|
|
224
223
|
|
|
@@ -267,7 +266,7 @@ Analysis never creates a project, replaces its history or rotates a token. OIDC
|
|
|
267
266
|
One mechanism for both sides: every finding of the canon is imported into Sonar, and the quality gate
|
|
268
267
|
fails while any is open. Nothing is configured per app; the pieces are managed files (`starci app sync`) and this package.
|
|
269
268
|
|
|
270
|
-
One entry, one report, one file: `npm run lint` is `starci app lint`; `
|
|
269
|
+
One entry, one report, one file: `npm run lint` is `starci app check --gate lint`; `starci app check --gate lint --out reports/lint.sonar.json` writes the ONE Sonar file, read through `sonar.externalIssuesReportPaths`. It carries three engines: `starci-hfs` (the repository findings, rule id = the finding code), `eslint` (the BE canon, universal FE canon and selected Grammar-family plugin, rule id = the ESLint rule) and `stylelint` over fe (rule id = the stylelint rule). The lint gate runs each side's ESLint config once, through the cache; `--side be|fe` limits it to one side, `--fix` runs ESLint and stylelint with `--fix` first (refused without `--gate lint`), and the findings the other gates selected join the same document. The exit code is 0 clean, 1 findings, 2 a tool could not run (a missing ESLint install is never a pass).
|
|
271
270
|
|
|
272
271
|
`--out` writes the findings as a Generic Issue Import document (SonarQube 10.3+ format: `{ rules, issues }`) before the verdict, so a
|
|
273
272
|
failing lint still leaves its report. A rule's name and description are the catalog's English title and Vietnamese title, meaning and next step;
|
|
@@ -278,7 +277,7 @@ Sonar's own ESLint import (`sonar.eslint.reportPaths`) is not used: it drops tho
|
|
|
278
277
|
|
|
279
278
|
The managed `sonar-project.properties` carries `sonar.externalIssuesReportPaths` (`reports/lint.sonar.json` only) and no `sonar.host.url` (the host is
|
|
280
279
|
`SONAR_HOST_URL`); the managed CI workflow produces the report and runs the scan and the gate action with `!cancelled()`, so a failed lint step still
|
|
281
|
-
reaches Sonar while the job stays failed. There is no `continue-on-error`. CI runs `
|
|
280
|
+
reaches Sonar while the job stays failed. There is no `continue-on-error`. CI runs `starci app check --gate lint --side <side> --out reports/lint.<side>.sonar.json` per side and merges the reports into `reports/lint.sonar.json`, before the scan. The duplicate-block threshold (`ruleParams.<side>.duplicateBlock`) has no Sonar property for TypeScript (SonarJS detects
|
|
282
281
|
clones with its own token rule), so the machine enforces it (R21) and its findings are imported like every other.
|
|
283
282
|
|
|
284
283
|
The gate is `knowledge/sonar-gate.yaml`, the one declaration: the new-code conditions (coverage 100, duplication, issues of every severity, hotspots) and an `overall` part (coverage 100, 0 open issues on the whole code,
|
package/cache/fake-session.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export function createFakeSession(options = {}) {
|
|
|
44
44
|
decide: () => decision,
|
|
45
45
|
records: (folder) => replaced.get(folder) ?? stored.get(folder) ?? [],
|
|
46
46
|
replace: (folder, records) => void replaced.set(folder, records),
|
|
47
|
-
unchanged: (paths) => paths.every((file) => index.hashOf(file) === initial.get(file)),
|
|
47
|
+
unchanged: (paths, captured) => paths.every((file) => (captured?.[file] === undefined ? index.hashOf(file) === initial.get(file) : (index.hashOf(file) ?? 'absent') === captured[file])),
|
|
48
48
|
report: (report) => void reports.push(report),
|
|
49
49
|
progress,
|
|
50
50
|
setFile: (path, text) => index.set(path, text),
|
package/cache/hfs-family.mjs
CHANGED
|
@@ -12,7 +12,9 @@ import { loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/sl
|
|
|
12
12
|
import { emitContractsAsync } from '../emit/contracts.mjs';
|
|
13
13
|
import { summarize } from '../runtime/scripts/hfs/check-results.mjs';
|
|
14
14
|
import { managedFindings } from '../sync/managed.mjs';
|
|
15
|
+
import { withRuleMemo } from '../runtime/scripts/hfs/rule-memo.mjs';
|
|
15
16
|
import { RecordSet, scopedStep } from './records.mjs';
|
|
17
|
+
import { FILE_RULES_STEP, RULE_STEP_PREFIX, UNIT_STEP_PREFIX, createStructureMemo } from './structure-memo.mjs';
|
|
16
18
|
import { makeReport } from './report.mjs';
|
|
17
19
|
import { sideKey } from './side-key.mjs';
|
|
18
20
|
export const HFS_FOLDER = 'check/hfs';
|
|
@@ -45,10 +47,12 @@ const beDeclaration = (repoRoot) => {
|
|
|
45
47
|
export async function runStructurePass(session, input) {
|
|
46
48
|
const { repoRoot, harness, presets } = input;
|
|
47
49
|
const tracked = [...(input.tracked ?? trackedFiles(repoRoot))];
|
|
48
|
-
const decision = session.decide('hfs');
|
|
50
|
+
const decision = session.decide(input.hfs ? 'hfs' : 'secrets');
|
|
49
51
|
const records = new RecordSet(session.records(HFS_FOLDER));
|
|
50
52
|
const previous = records.toArray();
|
|
51
53
|
const reuse = decision.mode === 'diff';
|
|
54
|
+
// The per-file and cross-file content rules are shared by both families: a record is read only when every selected family runs diff.
|
|
55
|
+
const structure = createStructureMemo({ session, records, reuse: reuse && (!input.hfs || !input.secrets || session.decide('secrets').mode === 'diff'), root: repoRoot, tracked });
|
|
52
56
|
const sideOf = (side) => sideKey(repoRoot, session.index, side);
|
|
53
57
|
const keys = new Map(['be', 'fe'].map((side) => [side, sideOf(side)]));
|
|
54
58
|
const extra = [...await managedFindings({ repoRoot, tracked, presets })];
|
|
@@ -60,7 +64,7 @@ export async function runStructurePass(session, input) {
|
|
|
60
64
|
const bePrefix = 'be/';
|
|
61
65
|
const key = keys.get('be');
|
|
62
66
|
const emit = await scopedStep(session, records, {
|
|
63
|
-
path: 'be', step: 'contract-emit', key: key?.key ?? 'none', reuse, inputs: key?.inputs ?? [],
|
|
67
|
+
path: 'be', step: 'contract-emit', key: key?.key ?? 'none', reuse, inputs: key?.inputs ?? [], captured: key?.hashes ?? {},
|
|
64
68
|
compute: async () => {
|
|
65
69
|
const files = tracked.filter((file) => file.startsWith(bePrefix)).map((file) => file.slice(bePrefix.length));
|
|
66
70
|
const emitted = await contractEmitFindingsAsync({ repoRoot: path.join(repoRoot, 'be'), files, repo: be, emit: (request) => emitContractsAsync({ ...request, execute: input.execute }) });
|
|
@@ -91,7 +95,7 @@ export async function runStructurePass(session, input) {
|
|
|
91
95
|
}
|
|
92
96
|
const report = baseMachine(call);
|
|
93
97
|
checked += 1;
|
|
94
|
-
if (session.unchanged(key.inputs)) {
|
|
98
|
+
if (session.unchanged(key.inputs, key.hashes)) {
|
|
95
99
|
records.put({ path: side, step: 'machine', hash: key.key, findings: [...report.violations, ...report.errors].map((item) => ({ path: item.path ?? side, line: item.line ?? 0, column: item.column ?? 0, rule: item.ruleId, message: item.message })), data: JSON.parse(JSON.stringify(report)) });
|
|
96
100
|
}
|
|
97
101
|
return report;
|
|
@@ -110,7 +114,7 @@ export async function runStructurePass(session, input) {
|
|
|
110
114
|
}
|
|
111
115
|
const found = compute();
|
|
112
116
|
checked += 1;
|
|
113
|
-
if (session.unchanged(key.inputs)) {
|
|
117
|
+
if (session.unchanged(key.inputs, key.hashes)) {
|
|
114
118
|
const list = found;
|
|
115
119
|
records.put({ path: side, step: 'unused-symbol', hash: key.key, findings: list.map(compact), data: JSON.parse(JSON.stringify(found)) });
|
|
116
120
|
}
|
|
@@ -118,13 +122,15 @@ export async function runStructurePass(session, input) {
|
|
|
118
122
|
},
|
|
119
123
|
};
|
|
120
124
|
const step = session.progress.step('hfs', 'structure', tracked.length);
|
|
121
|
-
const result = input.hfs
|
|
125
|
+
const result = withRuleMemo(structure.memo, () => (input.hfs
|
|
122
126
|
? withUnusedSymbolMemo(unusedMemo, () => withoutBootProof(() => checkRepository({ repoRoot, harness, extraFindings: extra, machine: cachedMachine })))
|
|
123
127
|
: (() => {
|
|
124
128
|
const slot = withUnusedSymbolMemo(unusedMemo, () => withoutBootProof(() => checkRepo({ repoRoot, harness, extraFindings: [], surface: 'check' })));
|
|
125
129
|
return { ...slot, machine: { status: 'skipped', reason: '--secrets alone runs the rules, not the machine' } };
|
|
126
|
-
})();
|
|
127
|
-
|
|
130
|
+
})()));
|
|
131
|
+
const filesChecked = structure.counts.filesChecked, filesReused = structure.counts.filesReused;
|
|
132
|
+
const keyedChecked = structure.counts.keyedChecked + structure.counts.unitsChecked, keyedReused = structure.counts.keyedReused + structure.counts.unitsReused;
|
|
133
|
+
step.tick(tracked.length, reused + filesReused + keyedReused, checked + filesChecked + keyedChecked);
|
|
128
134
|
step.end();
|
|
129
135
|
if (contracts)
|
|
130
136
|
result.contracts = contracts;
|
|
@@ -133,6 +139,13 @@ export async function runStructurePass(session, input) {
|
|
|
133
139
|
result.counts = summarize(result.findings);
|
|
134
140
|
result.ok = result.counts.error === 0;
|
|
135
141
|
}
|
|
142
|
+
records.retain(FILE_RULES_STEP, structure.files);
|
|
143
|
+
records.retainPrefix(RULE_STEP_PREFIX, new Set(['.', 'be', 'fe']));
|
|
144
|
+
for (const [step, paths] of structure.units)
|
|
145
|
+
records.retain(step, paths);
|
|
146
|
+
records.dropPrefixExcept(UNIT_STEP_PREFIX, new Set(structure.units.keys()));
|
|
147
|
+
if (!input.hfs)
|
|
148
|
+
session.replace(HFS_FOLDER, records.toArray());
|
|
136
149
|
if (input.hfs) {
|
|
137
150
|
const live = new Set(['be', 'fe']);
|
|
138
151
|
records.retain('machine', live);
|
|
@@ -147,7 +160,7 @@ export async function runStructurePass(session, input) {
|
|
|
147
160
|
}
|
|
148
161
|
const hfsFindings = result.findings.filter((finding) => !SECRET_CODES.has(finding.code));
|
|
149
162
|
const report = input.hfs
|
|
150
|
-
? makeReport({ family: 'hfs', mode: decision.mode, reason: decision.reason, checked, reused, previous: previous.flatMap((record) => record.step === 'hfs' ? record.findings : []), current: hfsFindings.map(compact) })
|
|
163
|
+
? makeReport({ family: 'hfs', mode: decision.mode, reason: decision.reason, checked: checked + filesChecked + keyedChecked, reused: reused + filesReused + keyedReused, previous: previous.flatMap((record) => record.step === 'hfs' ? record.findings : []), current: hfsFindings.map(compact) })
|
|
151
164
|
: null;
|
|
152
165
|
if (report)
|
|
153
166
|
session.report(report);
|
|
@@ -157,7 +170,7 @@ export async function runStructurePass(session, input) {
|
|
|
157
170
|
const prior = session.records(SECRETS_FOLDER);
|
|
158
171
|
const secretFindings = result.findings.filter((finding) => SECRET_CODES.has(finding.code));
|
|
159
172
|
session.replace(SECRETS_FOLDER, findingRecords(secretFindings, session.index.hashOf.bind(session.index), 'secrets'));
|
|
160
|
-
secretsReport = makeReport({ family: 'secrets', mode: secretDecision.mode, reason: secretDecision.reason, checked:
|
|
173
|
+
secretsReport = makeReport({ family: 'secrets', mode: secretDecision.mode, reason: secretDecision.reason, checked: filesChecked, reused: filesReused, previous: prior.flatMap((record) => record.findings), current: secretFindings.map(compact) });
|
|
161
174
|
session.report(secretsReport);
|
|
162
175
|
}
|
|
163
176
|
return { result, contracts, machineReused: reasons, report, secretsReport };
|
package/cache/import-graph.d.mts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type TypeScript from 'typescript';
|
|
2
2
|
type Ts = typeof TypeScript;
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* One file of the graph: its content hash, the side-relative posix paths of the local files it imports, and whether it declares globals.
|
|
5
|
+
*
|
|
6
|
+
* `api` is the digest of the file's type surface, the text of TypeScript's isolated declaration emit (a source that is not TypeScript, or a
|
|
7
|
+
* declaration file, stands for itself: its content hash). `open` says the surface is inferred: the emit reported a diagnostic (a type the compiler
|
|
8
|
+
* has to infer from the bodies and the files they use), so it can change while the emit text does not. `refs` is the local files the emit names (a
|
|
9
|
+
* type imported from them, a re-export): an explicit surface is its emit text plus the surfaces of those files, nothing else. An absent `api`,
|
|
10
|
+
* `open` or `refs` (a record from before they existed) is read as "changed", "inferred" and "unknown".
|
|
11
|
+
*/
|
|
4
12
|
export interface ImportNode {
|
|
5
13
|
hash: string;
|
|
6
14
|
imports: string[];
|
|
7
15
|
declaresGlobals: boolean;
|
|
16
|
+
api?: string | undefined;
|
|
17
|
+
open?: boolean | undefined;
|
|
18
|
+
refs?: string[] | undefined;
|
|
8
19
|
}
|
|
9
20
|
/** The graph of a side: side-relative posix path to node. Leaves (a JSON file, a declaration file out of the lint scope) are nodes with no imports. */
|
|
10
21
|
export type ImportGraph = ReadonlyMap<string, ImportNode>;
|
|
@@ -39,6 +50,17 @@ export declare function referencesOf(ts: Ts, text: string): {
|
|
|
39
50
|
};
|
|
40
51
|
/** The local files `file` (side-relative) imports, resolved with the side's compiler options. Unresolved and installed-package imports are not edges. */
|
|
41
52
|
export declare function resolveImports(host: GraphHost, options: TypeScript.CompilerOptions, file: string, text: string): string[];
|
|
53
|
+
/**
|
|
54
|
+
* The type surface of one source file: the digest of its isolated declaration emit (`ts.transpileDeclaration`, TypeScript 5.5 and later, which reads the
|
|
55
|
+
* file alone) and whether it is open. A body-only edit leaves the emit, and so the digest, as it was; a changed signature, export list or doc comment
|
|
56
|
+
* (`@deprecated` is read by lint rules) changes it. A compiler without `transpileDeclaration` gives no digest, which every reader takes as changed.
|
|
57
|
+
* `open` is true when the emit reported a diagnostic (the compiler must infer a type); `refs` is the local files the emit names.
|
|
58
|
+
*/
|
|
59
|
+
export declare function surfaceOf(host: GraphHost, options: TypeScript.CompilerOptions, file: string, text: string): {
|
|
60
|
+
api: string | undefined;
|
|
61
|
+
open: boolean;
|
|
62
|
+
refs: string[] | undefined;
|
|
63
|
+
};
|
|
42
64
|
/** What a build reports of its work. */
|
|
43
65
|
export interface GraphBuild {
|
|
44
66
|
graph: Map<string, ImportNode>;
|
|
@@ -67,9 +89,15 @@ export interface DirtySet {
|
|
|
67
89
|
changed: Set<string>;
|
|
68
90
|
}
|
|
69
91
|
/**
|
|
70
|
-
* The files to lint again after a change from `previous` (the recorded graph, null when none) to `next
|
|
71
|
-
*
|
|
72
|
-
* file
|
|
92
|
+
* The files to lint again after a change from `previous` (the recorded graph, null when none) to `next`.
|
|
93
|
+
*
|
|
94
|
+
* A file is dirty when it changed, was added or removed, resolves its imports to other files than recorded, or imports a file whose type surface
|
|
95
|
+
* changed, in the new graph or the recorded one (a file that stopped importing such a file still holds a result from the old types). A dirty file's
|
|
96
|
+
* surface changed when it was added or removed, gained an import, its surface digest differs from the record (or either is unknown), its surface
|
|
97
|
+
* is inferred (`open`: whatever dirtied it can have changed the inferred type), or its explicit surface names a file (`refs`) whose surface changed.
|
|
98
|
+
* An explicit surface is its emit text and the surfaces of the files it names, so a body-only edit of such a file dirties that file alone, a
|
|
99
|
+
* changed signature its direct importers, and a type it re-exports or names carries on through the files that name it. A changed file that
|
|
100
|
+
* declares globals, now or before, dirties the whole side.
|
|
73
101
|
*/
|
|
74
102
|
export declare function dirtySetOf(previous: ImportGraph | null, next: ImportGraph): DirtySet;
|
|
75
103
|
export {};
|
package/cache/import-graph.mjs
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// added or removed file and every file that reaches one through imports, in the new graph and in the recorded one. Everything here is pure over
|
|
7
7
|
// its inputs (the compiler, a file reader, the hashes), so a spec drives it with fixtures and the cache itself stays in lint-family.mts.
|
|
8
8
|
import path from 'node:path';
|
|
9
|
+
import { DECLARATION, SCRIPT, TYPESCRIPT, declarationSurface } from '../runtime/scripts/lib/declaration-surface.mjs';
|
|
10
|
+
import { hashText } from './hash.mjs';
|
|
9
11
|
export const posix = (file) => file.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
10
12
|
/** The compiler options of the side's own tsconfig (the file the typed ESLint run reads), or the permissive defaults when the side holds none. */
|
|
11
13
|
export function compilerOptionsOf(ts, sideRoot, readText, fileExists) {
|
|
@@ -82,6 +84,16 @@ function localPath(sideRoot, absolute) {
|
|
|
82
84
|
return null;
|
|
83
85
|
return relative.split('/').includes('node_modules') ? null : relative;
|
|
84
86
|
}
|
|
87
|
+
/** The resolution cache of one compiler-options object: a side's thousands of files share the directory lookups of their (mostly package) imports. */
|
|
88
|
+
const resolutionCaches = new WeakMap();
|
|
89
|
+
const resolutionCacheOf = (ts, sideRoot, options) => {
|
|
90
|
+
let cache = resolutionCaches.get(options);
|
|
91
|
+
if (cache === undefined) {
|
|
92
|
+
cache = ts.createModuleResolutionCache(sideRoot, (name) => name, options);
|
|
93
|
+
resolutionCaches.set(options, cache);
|
|
94
|
+
}
|
|
95
|
+
return cache;
|
|
96
|
+
};
|
|
85
97
|
/** The local files `file` (side-relative) imports, resolved with the side's compiler options. Unresolved and installed-package imports are not edges. */
|
|
86
98
|
export function resolveImports(host, options, file, text) {
|
|
87
99
|
const { ts, sideRoot } = host;
|
|
@@ -95,7 +107,7 @@ export function resolveImports(host, options, file, text) {
|
|
|
95
107
|
};
|
|
96
108
|
const found = new Set();
|
|
97
109
|
for (const specifier of specifiers) {
|
|
98
|
-
const resolved = ts.resolveModuleName(specifier, absolute, options, moduleHost).resolvedModule;
|
|
110
|
+
const resolved = ts.resolveModuleName(specifier, absolute, options, moduleHost, resolutionCacheOf(ts, sideRoot, options)).resolvedModule;
|
|
99
111
|
if (!resolved || resolved.isExternalLibraryImport)
|
|
100
112
|
continue;
|
|
101
113
|
const local = localPath(sideRoot, resolved.resolvedFileName);
|
|
@@ -109,6 +121,29 @@ export function resolveImports(host, options, file, text) {
|
|
|
109
121
|
}
|
|
110
122
|
return [...found].sort();
|
|
111
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* The type surface of one source file: the digest of its isolated declaration emit (`ts.transpileDeclaration`, TypeScript 5.5 and later, which reads the
|
|
126
|
+
* file alone) and whether it is open. A body-only edit leaves the emit, and so the digest, as it was; a changed signature, export list or doc comment
|
|
127
|
+
* (`@deprecated` is read by lint rules) changes it. A compiler without `transpileDeclaration` gives no digest, which every reader takes as changed.
|
|
128
|
+
* `open` is true when the emit reported a diagnostic (the compiler must infer a type); `refs` is the local files the emit names.
|
|
129
|
+
*/
|
|
130
|
+
export function surfaceOf(host, options, file, text) {
|
|
131
|
+
const { ts } = host;
|
|
132
|
+
if (DECLARATION.test(file) || SCRIPT.test(file))
|
|
133
|
+
return { api: hashText(text), open: true, refs: undefined };
|
|
134
|
+
if (!TYPESCRIPT.test(file))
|
|
135
|
+
return { api: hashText(text), open: false, refs: [] };
|
|
136
|
+
try {
|
|
137
|
+
const surface = declarationSurface(ts, file, text, options.module);
|
|
138
|
+
if (surface === null)
|
|
139
|
+
return { api: undefined, open: true, refs: undefined };
|
|
140
|
+
// the classifier is the one the explicit-export-surface rule of the canons reports with: a file is open exactly when it has a problem
|
|
141
|
+
return { api: hashText(surface.outputText), open: surface.problems.length > 0, refs: resolveImports(host, options, file, surface.outputText) };
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return { api: undefined, open: true, refs: undefined };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
112
147
|
/**
|
|
113
148
|
* The import graph of `files` (the side's lint scope, side-relative posix) and the leaves they reach.
|
|
114
149
|
*
|
|
@@ -131,7 +166,8 @@ export function buildImportGraph(host, files, recorded, options = {}) {
|
|
|
131
166
|
if (hash === null)
|
|
132
167
|
return;
|
|
133
168
|
const before = recorded?.get(file);
|
|
134
|
-
|
|
169
|
+
// a record from before the type surface was kept has none: the file is read once more to give it one
|
|
170
|
+
if (!pathsChanged && before !== undefined && before.hash === hash && (before.api !== undefined || !/\.(?:[cm]?[jt]sx?)$/.test(file))) {
|
|
135
171
|
graph.set(file, before);
|
|
136
172
|
reused += 1;
|
|
137
173
|
}
|
|
@@ -139,11 +175,11 @@ export function buildImportGraph(host, files, recorded, options = {}) {
|
|
|
139
175
|
const text = host.readText(path.join(host.sideRoot, file));
|
|
140
176
|
scanned += 1;
|
|
141
177
|
graph.set(file, text === null
|
|
142
|
-
? { hash, imports: [], declaresGlobals: false }
|
|
143
|
-
: { hash, imports: resolveImports(host, compiler, file, text), declaresGlobals: declaresGlobals(host.ts, file, text) });
|
|
178
|
+
? { hash, imports: [], declaresGlobals: false, api: undefined, open: true, refs: undefined }
|
|
179
|
+
: { hash, imports: resolveImports(host, compiler, file, text), declaresGlobals: declaresGlobals(host.ts, file, text), ...surfaceOf(host, compiler, file, text) });
|
|
144
180
|
}
|
|
145
181
|
else
|
|
146
|
-
graph.set(file, { hash, imports: [], declaresGlobals: false });
|
|
182
|
+
graph.set(file, { hash, imports: [], declaresGlobals: false, api: hash, open: false, refs: [] });
|
|
147
183
|
for (const next of graph.get(file)?.imports ?? [])
|
|
148
184
|
visit(next);
|
|
149
185
|
};
|
|
@@ -181,9 +217,15 @@ export function dependentsOf(graph, seeds) {
|
|
|
181
217
|
return reached;
|
|
182
218
|
}
|
|
183
219
|
/**
|
|
184
|
-
* The files to lint again after a change from `previous` (the recorded graph, null when none) to `next
|
|
185
|
-
*
|
|
186
|
-
* file
|
|
220
|
+
* The files to lint again after a change from `previous` (the recorded graph, null when none) to `next`.
|
|
221
|
+
*
|
|
222
|
+
* A file is dirty when it changed, was added or removed, resolves its imports to other files than recorded, or imports a file whose type surface
|
|
223
|
+
* changed, in the new graph or the recorded one (a file that stopped importing such a file still holds a result from the old types). A dirty file's
|
|
224
|
+
* surface changed when it was added or removed, gained an import, its surface digest differs from the record (or either is unknown), its surface
|
|
225
|
+
* is inferred (`open`: whatever dirtied it can have changed the inferred type), or its explicit surface names a file (`refs`) whose surface changed.
|
|
226
|
+
* An explicit surface is its emit text and the surfaces of the files it names, so a body-only edit of such a file dirties that file alone, a
|
|
227
|
+
* changed signature its direct importers, and a type it re-exports or names carries on through the files that name it. A changed file that
|
|
228
|
+
* declares globals, now or before, dirties the whole side.
|
|
187
229
|
*/
|
|
188
230
|
export function dirtySetOf(previous, next) {
|
|
189
231
|
const changed = changedPaths(previous, next);
|
|
@@ -195,10 +237,45 @@ export function dirtySetOf(previous, next) {
|
|
|
195
237
|
}
|
|
196
238
|
}
|
|
197
239
|
// a file whose imports resolve elsewhere now (a new file shadows a module, a missing one appeared) reads other types than before
|
|
198
|
-
const rewired = [...next].filter(([file, node]) => {
|
|
240
|
+
const rewired = new Set([...next].filter(([file, node]) => {
|
|
199
241
|
const before = previous.get(file);
|
|
200
242
|
return before !== undefined && before.hash === node.hash && JSON.stringify(before.imports) !== JSON.stringify(node.imports);
|
|
201
|
-
}).map(([file]) => file);
|
|
202
|
-
const
|
|
243
|
+
}).map(([file]) => file));
|
|
244
|
+
const importers = new Map();
|
|
245
|
+
for (const graph of [next, previous])
|
|
246
|
+
for (const [file, node] of graph)
|
|
247
|
+
for (const target of node.imports) {
|
|
248
|
+
const known = importers.get(target) ?? new Set();
|
|
249
|
+
known.add(file);
|
|
250
|
+
importers.set(target, known);
|
|
251
|
+
}
|
|
252
|
+
const dirty = new Set([...changed, ...rewired]);
|
|
253
|
+
const surface = new Set();
|
|
254
|
+
const surfaceChanged = (file) => {
|
|
255
|
+
const after = next.get(file), before = previous.get(file);
|
|
256
|
+
if (after === undefined || before === undefined)
|
|
257
|
+
return true;
|
|
258
|
+
// a new edge can resolve a name the emit already used to a file that was not there, so what the importers read of this file may differ
|
|
259
|
+
if (rewired.has(file) && after.imports.some((target) => !before.imports.includes(target)))
|
|
260
|
+
return true;
|
|
261
|
+
if (after.hash !== before.hash && (after.api === undefined || before.api === undefined || after.api !== before.api))
|
|
262
|
+
return true;
|
|
263
|
+
if (after.open !== false || before.open !== false || after.refs === undefined || before.refs === undefined)
|
|
264
|
+
return true;
|
|
265
|
+
return [...after.refs, ...before.refs].some((target) => surface.has(target));
|
|
266
|
+
};
|
|
267
|
+
const queue = [...dirty];
|
|
268
|
+
while (queue.length > 0) {
|
|
269
|
+
const file = queue.pop();
|
|
270
|
+
if (surface.has(file) || !surfaceChanged(file))
|
|
271
|
+
continue;
|
|
272
|
+
surface.add(file);
|
|
273
|
+
for (const importer of importers.get(file) ?? []) {
|
|
274
|
+
// a file already judged to keep its surface is looked at again: the file that just changed may be one its surface names
|
|
275
|
+
if (!dirty.has(importer))
|
|
276
|
+
dirty.add(importer);
|
|
277
|
+
queue.push(importer);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
203
280
|
return { dirty, wholeSide: null, changed };
|
|
204
281
|
}
|
package/cache/lint-family.d.mts
CHANGED
|
@@ -78,10 +78,14 @@ export interface StylelintResult {
|
|
|
78
78
|
warnings: StylelintWarning[];
|
|
79
79
|
[key: string]: unknown;
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* The answer of a linter seam: validated results of the files it was asked about, or why it could not complete. `graph` is, for ESLint, the project-graph
|
|
83
|
+
* snapshot each canon's graph rules built in the child, by canon name: the records of the `graph` step are written from it instead of building the graph again.
|
|
84
|
+
*/
|
|
82
85
|
export interface LinterOutcome<R> {
|
|
83
86
|
results: R[];
|
|
84
87
|
errors: string[];
|
|
88
|
+
graph?: Readonly<Record<string, GraphSnapshot>> | undefined;
|
|
85
89
|
}
|
|
86
90
|
/** Everything one side's cached lint needs. */
|
|
87
91
|
export interface LintSideInput {
|
|
@@ -93,8 +97,6 @@ export interface LintSideInput {
|
|
|
93
97
|
scope: readonly string[];
|
|
94
98
|
/** The stylesheets stylelint judges (fe only): side-relative posix; empty otherwise. */
|
|
95
99
|
stylesheets: readonly string[];
|
|
96
|
-
/** The files this run reports when it reports part of a side (a workspace lint); null reports the whole side. A partial run never writes. */
|
|
97
|
-
report: ((file: string) => boolean) | null;
|
|
98
100
|
/** The ESLint canon, its grammar family, in config order. */
|
|
99
101
|
canons: readonly CanonDeclarations[];
|
|
100
102
|
/** The stylelint canon; null when the side has no stylesheet judge. */
|
|
@@ -149,6 +151,11 @@ export declare function crossDigestFactory(options: {
|
|
|
149
151
|
}): (file: string) => string | undefined;
|
|
150
152
|
/** The TypeScript the app installed, resolved from the side folder like the linters are (not this package's own copy). */
|
|
151
153
|
export declare function loadAppTypeScript(sideRoot: string): Ts;
|
|
154
|
+
/**
|
|
155
|
+
* The graph findings of every canon that publishes graph rules, merged by file and sorted. A canon whose snapshot the ESLint child handed out (`handed`, by
|
|
156
|
+
* canon name) is not asked to build its graph again; any other canon builds it through its `projectGraph`.
|
|
157
|
+
*/
|
|
158
|
+
export declare function mergedSnapshot(sideRoot: string, canons: readonly CanonDeclarations[], handed?: Readonly<Record<string, GraphSnapshot>>): Record<string, GraphFinding[]> | null;
|
|
152
159
|
/**
|
|
153
160
|
* Judges one side: ESLint (and, for fe, stylelint) over what the records cannot vouch for, the records of the rest, and the new records.
|
|
154
161
|
* Never throws for a tool failure: those come back in `errors`, and a side with an error stores nothing.
|