pipework 0.252.0 → 0.253.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 +10 -0
- package/dist/infra/support/handle.d.ts +8 -1
- package/dist/infra/support/handle.d.ts.map +1 -1
- package/dist/infra/support/handle.js +9 -2
- package/dist/infra/support/handle.js.map +1 -1
- package/dist/infra/support/namespace.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/async/jobs/abortable-delay.ts +18 -0
- package/src/async/jobs/claims.ts +52 -0
- package/src/async/jobs/cron.ts +179 -0
- package/src/async/jobs/errors.ts +56 -0
- package/src/async/jobs/exclusion.ts +117 -0
- package/src/async/jobs/execute.ts +219 -0
- package/src/async/jobs/index.ts +10 -0
- package/src/async/jobs/listener.ts +135 -0
- package/src/async/jobs/namespace.ts +20 -0
- package/src/async/jobs/queue-door.ts +44 -0
- package/src/async/jobs/queue.ts +1012 -0
- package/src/async/jobs/run-script.ts +67 -0
- package/src/async/jobs/scheduler.ts +218 -0
- package/src/async/jobs/schedules-table.ts +37 -0
- package/src/async/jobs/serialize-error.ts +48 -0
- package/src/async/jobs/table.ts +191 -0
- package/src/async/pipeline/define.ts +53 -0
- package/src/async/pipeline/errors.ts +35 -0
- package/src/async/pipeline/execute.ts +53 -0
- package/src/async/pipeline/index.ts +13 -0
- package/src/async/pipeline/namespace.ts +12 -0
- package/src/async/pipeline/resume.ts +67 -0
- package/src/async/pipeline/run-steps.ts +98 -0
- package/src/async/pipeline/types.ts +57 -0
- package/src/async/state-machine/define.ts +64 -0
- package/src/async/state-machine/errors.ts +29 -0
- package/src/async/state-machine/index.ts +3 -0
- package/src/async/state-machine/namespace.ts +12 -0
- package/src/async/state-machine/transition.ts +90 -0
- package/src/async/state-machine/types.ts +37 -0
- package/src/async/steps/errors.ts +31 -0
- package/src/async/steps/graph.ts +1214 -0
- package/src/async/steps/hash.ts +40 -0
- package/src/async/steps/index.ts +7 -0
- package/src/async/steps/namespace.ts +7 -0
- package/src/async/steps/table.ts +97 -0
- package/src/async/steps/types.ts +318 -0
- package/src/audit/chain/index.ts +9 -0
- package/src/audit/chain/install.ts +113 -0
- package/src/audit/chain/retire.ts +52 -0
- package/src/audit/chain/verify.ts +257 -0
- package/src/audit/query/cursor.ts +37 -0
- package/src/audit/query/index.ts +9 -0
- package/src/audit/query/query.ts +223 -0
- package/src/audit/schema/audit-record-fields.ts +68 -0
- package/src/audit/schema/audit-record.ts +23 -0
- package/src/auth/auth/audit.ts +31 -0
- package/src/auth/auth/chain.ts +84 -0
- package/src/auth/auth/cookie.ts +136 -0
- package/src/auth/auth/email-links.ts +311 -0
- package/src/auth/auth/guard.ts +128 -0
- package/src/auth/auth/index.ts +18 -0
- package/src/auth/auth/multi-org.ts +337 -0
- package/src/auth/auth/namespace.ts +34 -0
- package/src/auth/auth/parse-expiry.ts +13 -0
- package/src/auth/auth/passwords.ts +110 -0
- package/src/auth/auth/schema.ts +110 -0
- package/src/auth/auth/sessions.ts +565 -0
- package/src/auth/auth/sign-in.ts +156 -0
- package/src/auth/auth/signing-keys.ts +69 -0
- package/src/auth/auth/types.ts +26 -0
- package/src/auth/auth/uuid.ts +10 -0
- package/src/auth/rbac/cache.ts +55 -0
- package/src/auth/rbac/check.ts +30 -0
- package/src/auth/rbac/create.ts +35 -0
- package/src/auth/rbac/index.ts +5 -0
- package/src/auth/rbac/middleware.ts +20 -0
- package/src/auth/rbac/namespace.ts +15 -0
- package/src/auth/rbac/resolve.ts +31 -0
- package/src/auth/rbac/schema.sql +31 -0
- package/src/auth/rbac/seed.ts +67 -0
- package/src/auth/rbac/tables.ts +32 -0
- package/src/auth/rbac/types.ts +65 -0
- package/src/auth/tenant/extract.ts +33 -0
- package/src/auth/tenant/in-tenant.ts +49 -0
- package/src/auth/tenant/index.ts +7 -0
- package/src/auth/tenant/namespace.ts +23 -0
- package/src/auth/tenant/propagate.ts +20 -0
- package/src/auth/tenant/rls.ts +120 -0
- package/src/auth/tenant/scope.ts +33 -0
- package/src/auth/tenant/scoped-db.ts +581 -0
- package/src/auth/tenant/types.ts +14 -0
- package/src/auth/tenant/validate.ts +27 -0
- package/src/cli/commands/add.ts +158 -0
- package/src/cli/commands/alerts.ts +98 -0
- package/src/cli/commands/check.ts +1418 -0
- package/src/cli/commands/codemod.ts +78 -0
- package/src/cli/commands/db.ts +356 -0
- package/src/cli/commands/dev.ts +101 -0
- package/src/cli/commands/generate.ts +75 -0
- package/src/cli/commands/init.ts +36 -0
- package/src/cli/commands/install.ts +28 -0
- package/src/cli/commands/lint.ts +62 -0
- package/src/cli/commands/migrate.ts +132 -0
- package/src/cli/commands/openapi.ts +63 -0
- package/src/cli/commands/remove.ts +72 -0
- package/src/cli/commands/run.ts +50 -0
- package/src/cli/commands/scope.ts +54 -0
- package/src/cli/commands/serve.ts +68 -0
- package/src/cli/commands/shard.ts +31 -0
- package/src/cli/commands/shared/config-pipework-version.ts +28 -0
- package/src/cli/commands/shared/find-config.ts +22 -0
- package/src/cli/commands/shared/load-config.ts +35 -0
- package/src/cli/commands/shared/pnpm-install.ts +95 -0
- package/src/cli/commands/shared/report-removed.ts +8 -0
- package/src/cli/commands/sync.ts +32 -0
- package/src/cli/commands/test.ts +307 -0
- package/src/cli/commands/upgrade.ts +48 -0
- package/src/cli/dev-bootstrap.ts +24 -0
- package/src/cli/errors.ts +10 -0
- package/src/cli/index.ts +303 -0
- package/src/cli/output.ts +17 -0
- package/src/cli/port.ts +15 -0
- package/src/cli/surface-kinds.ts +20 -0
- package/src/client/index.ts +4 -0
- package/src/client/poll.ts +130 -0
- package/src/client/report-fault.ts +119 -0
- package/src/core/config/app-pool.ts +19 -0
- package/src/core/config/aws-env.ts +29 -0
- package/src/core/config/base-dir.ts +24 -0
- package/src/core/config/discover.ts +64 -0
- package/src/core/config/env-resolve.ts +87 -0
- package/src/core/config/env-types.ts +116 -0
- package/src/core/config/env.ts +55 -0
- package/src/core/config/errors.ts +9 -0
- package/src/core/config/import-config.ts +40 -0
- package/src/core/config/index.ts +8 -0
- package/src/core/config/load.ts +417 -0
- package/src/core/config/namespace.ts +14 -0
- package/src/core/config/resolve.ts +195 -0
- package/src/core/config/schema.ts +488 -0
- package/src/core/config/statement-observer.ts +48 -0
- package/src/core/config/ts-inline-hooks.ts +75 -0
- package/src/core/config/ts-register.ts +4 -0
- package/src/core/config/ts-resolve-hooks.ts +101 -0
- package/src/core/errors/base.ts +49 -0
- package/src/core/errors/http.ts +146 -0
- package/src/core/errors/index.ts +2 -0
- package/src/core/errors/namespace.ts +31 -0
- package/src/core/lifecycle/drain.ts +33 -0
- package/src/core/lifecycle/health.ts +72 -0
- package/src/core/lifecycle/index.ts +4 -0
- package/src/core/lifecycle/namespace.ts +7 -0
- package/src/core/lifecycle/startup.ts +102 -0
- package/src/core/lifecycle/types.ts +36 -0
- package/src/core/logging/context.ts +60 -0
- package/src/core/logging/error-chain.ts +68 -0
- package/src/core/logging/index.ts +5 -0
- package/src/core/logging/logger.ts +65 -0
- package/src/core/logging/namespace.ts +16 -0
- package/src/core/logging/proxy.ts +22 -0
- package/src/core/logging/redact-match.ts +24 -0
- package/src/core/logging/types.ts +47 -0
- package/src/core/pipework.ts +566 -0
- package/src/core/runtime/broker.ts +69 -0
- package/src/core/surface/http.ts +133 -0
- package/src/core/surface/index.ts +11 -0
- package/src/core/surface/job-timeout.ts +40 -0
- package/src/core/surface/namespace.ts +13 -0
- package/src/core/surface/script.ts +33 -0
- package/src/core/surface/start.ts +142 -0
- package/src/core/surface/types.ts +225 -0
- package/src/core/surface/validate.ts +88 -0
- package/src/core/surface/worker.ts +528 -0
- package/src/core/validation/connectivity.ts +49 -0
- package/src/core/validation/domain.ts +99 -0
- package/src/core/validation/index.ts +5 -0
- package/src/core/validation/queue-tables.ts +312 -0
- package/src/core/validation/rls-role.ts +138 -0
- package/src/core/validation/startup.ts +51 -0
- package/src/core/validation/surfaces.ts +65 -0
- package/src/core/validation/types.ts +25 -0
- package/src/data/db/advisory-lock.ts +55 -0
- package/src/data/db/aggregates.ts +172 -0
- package/src/data/db/array-elements.ts +111 -0
- package/src/data/db/bulk-set.ts +53 -0
- package/src/data/db/col/index.ts +14 -0
- package/src/data/db/composed.ts +53 -0
- package/src/data/db/db.ts +276 -0
- package/src/data/db/excluded.ts +16 -0
- package/src/data/db/expressions.ts +473 -0
- package/src/data/db/filter.ts +90 -0
- package/src/data/db/function-source.ts +114 -0
- package/src/data/db/identifiers.ts +16 -0
- package/src/data/db/idx/index.ts +9 -0
- package/src/data/db/in-list.ts +49 -0
- package/src/data/db/index.ts +9 -0
- package/src/data/db/insert-split.ts +137 -0
- package/src/data/db/instrumentation.ts +82 -0
- package/src/data/db/maintenance.ts +28 -0
- package/src/data/db/namespace.ts +107 -0
- package/src/data/db/owner-labels.ts +9 -0
- package/src/data/db/pool.ts +187 -0
- package/src/data/db/query-label.ts +13 -0
- package/src/data/db/query-watch.ts +116 -0
- package/src/data/db/raw-client-warning.ts +37 -0
- package/src/data/db/readonly.ts +23 -0
- package/src/data/db/reservation.ts +151 -0
- package/src/data/db/schema-features.ts +14 -0
- package/src/data/db/serializable.ts +83 -0
- package/src/data/db/sql.ts +1 -0
- package/src/data/db/timeouts.ts +76 -0
- package/src/data/db/types.ts +18 -0
- package/src/data/db/unbound.ts +33 -0
- package/src/data/db/values.ts +29 -0
- package/src/data/domain/brand.ts +15 -0
- package/src/data/domain/build-factory.ts +193 -0
- package/src/data/domain/build-schema.ts +161 -0
- package/src/data/domain/build-table.ts +355 -0
- package/src/data/domain/define.ts +239 -0
- package/src/data/domain/external-registry.ts +34 -0
- package/src/data/domain/external.ts +77 -0
- package/src/data/domain/field.ts +279 -0
- package/src/data/domain/index.ts +13 -0
- package/src/data/domain/naming.ts +6 -0
- package/src/data/domain/projections.ts +57 -0
- package/src/data/domain/trace-safe-projection.ts +31 -0
- package/src/data/domain/traits.ts +108 -0
- package/src/data/domain/types.ts +289 -0
- package/src/data/jsonb/composed.ts +18 -0
- package/src/data/jsonb/index.ts +1 -0
- package/src/data/jsonb/namespace.ts +30 -0
- package/src/data/jsonb/query.ts +86 -0
- package/src/data/migrate/append-only-generate.ts +47 -0
- package/src/data/migrate/applied.ts +47 -0
- package/src/data/migrate/apply.ts +309 -0
- package/src/data/migrate/check.ts +119 -0
- package/src/data/migrate/diff.ts +357 -0
- package/src/data/migrate/door-generate.ts +130 -0
- package/src/data/migrate/framework-rls.ts +58 -0
- package/src/data/migrate/generate.ts +201 -0
- package/src/data/migrate/history.ts +215 -0
- package/src/data/migrate/index.ts +9 -0
- package/src/data/migrate/internal-definitions.ts +175 -0
- package/src/data/migrate/post-process.ts +73 -0
- package/src/data/migrate/pragma.ts +73 -0
- package/src/data/migrate/provision-framework.ts +147 -0
- package/src/data/migrate/rls-generate.ts +61 -0
- package/src/data/migrate/run.ts +124 -0
- package/src/data/migrate/safety.ts +167 -0
- package/src/data/migrate/snapshot.ts +368 -0
- package/src/data/migrate/sql-emit.ts +392 -0
- package/src/data/migrate/statements.ts +312 -0
- package/src/data/query/alias.ts +126 -0
- package/src/data/query/batch.ts +8 -0
- package/src/data/query/cache/core/cache.ts +78 -0
- package/src/data/query/cache/core/index.ts +1 -0
- package/src/data/query/cache/core/types.ts +29 -0
- package/src/data/query/cache/readme.md +230 -0
- package/src/data/query/casing.ts +76 -0
- package/src/data/query/collation.ts +11 -0
- package/src/data/query/column-builder.ts +370 -0
- package/src/data/query/column.ts +150 -0
- package/src/data/query/entity.ts +42 -0
- package/src/data/query/errors.ts +33 -0
- package/src/data/query/index.ts +14 -0
- package/src/data/query/logger.ts +47 -0
- package/src/data/query/operations.ts +45 -0
- package/src/data/query/pg-core/alias.ts +12 -0
- package/src/data/query/pg-core/checks.ts +32 -0
- package/src/data/query/pg-core/columns/all.ts +71 -0
- package/src/data/query/pg-core/columns/bigint.ts +110 -0
- package/src/data/query/pg-core/columns/bigserial.ts +128 -0
- package/src/data/query/pg-core/columns/boolean.ts +43 -0
- package/src/data/query/pg-core/columns/char.ts +85 -0
- package/src/data/query/pg-core/columns/cidr.ts +43 -0
- package/src/data/query/pg-core/columns/common.ts +366 -0
- package/src/data/query/pg-core/columns/custom.ts +232 -0
- package/src/data/query/pg-core/columns/date.common.ts +15 -0
- package/src/data/query/pg-core/columns/date.ts +112 -0
- package/src/data/query/pg-core/columns/double-precision.ts +55 -0
- package/src/data/query/pg-core/columns/enum.ts +201 -0
- package/src/data/query/pg-core/columns/index.ts +35 -0
- package/src/data/query/pg-core/columns/inet.ts +43 -0
- package/src/data/query/pg-core/columns/int.common.ts +57 -0
- package/src/data/query/pg-core/columns/integer.ts +53 -0
- package/src/data/query/pg-core/columns/interval.ts +83 -0
- package/src/data/query/pg-core/columns/json.ts +58 -0
- package/src/data/query/pg-core/columns/jsonb.ts +56 -0
- package/src/data/query/pg-core/columns/line.ts +117 -0
- package/src/data/query/pg-core/columns/macaddr.ts +43 -0
- package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
- package/src/data/query/pg-core/columns/numeric.ts +227 -0
- package/src/data/query/pg-core/columns/point.ts +127 -0
- package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
- package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
- package/src/data/query/pg-core/columns/real.ts +58 -0
- package/src/data/query/pg-core/columns/serial.ts +55 -0
- package/src/data/query/pg-core/columns/smallint.ts +53 -0
- package/src/data/query/pg-core/columns/smallserial.ts +60 -0
- package/src/data/query/pg-core/columns/text.ts +67 -0
- package/src/data/query/pg-core/columns/time.ts +72 -0
- package/src/data/query/pg-core/columns/timestamp.ts +163 -0
- package/src/data/query/pg-core/columns/uuid.ts +51 -0
- package/src/data/query/pg-core/columns/varchar.ts +89 -0
- package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
- package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
- package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
- package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
- package/src/data/query/pg-core/db.ts +674 -0
- package/src/data/query/pg-core/dialect.ts +1404 -0
- package/src/data/query/pg-core/expressions.ts +25 -0
- package/src/data/query/pg-core/foreign-keys.ts +130 -0
- package/src/data/query/pg-core/index.ts +21 -0
- package/src/data/query/pg-core/indexes.ts +276 -0
- package/src/data/query/pg-core/policies.ts +56 -0
- package/src/data/query/pg-core/primary-keys.ts +62 -0
- package/src/data/query/pg-core/query-builders/count.ts +86 -0
- package/src/data/query/pg-core/query-builders/delete.ts +293 -0
- package/src/data/query/pg-core/query-builders/index.ts +7 -0
- package/src/data/query/pg-core/query-builders/insert.ts +452 -0
- package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
- package/src/data/query/pg-core/query-builders/query.ts +157 -0
- package/src/data/query/pg-core/query-builders/raw.ts +49 -0
- package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
- package/src/data/query/pg-core/query-builders/select.ts +1346 -0
- package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
- package/src/data/query/pg-core/query-builders/update.ts +626 -0
- package/src/data/query/pg-core/roles.ts +41 -0
- package/src/data/query/pg-core/schema.ts +72 -0
- package/src/data/query/pg-core/sequence.ts +41 -0
- package/src/data/query/pg-core/session.ts +292 -0
- package/src/data/query/pg-core/subquery.ts +46 -0
- package/src/data/query/pg-core/table.ts +252 -0
- package/src/data/query/pg-core/unique-constraint.ts +73 -0
- package/src/data/query/pg-core/utils/array.ts +95 -0
- package/src/data/query/pg-core/utils/index.ts +1 -0
- package/src/data/query/pg-core/utils.ts +104 -0
- package/src/data/query/pg-core/view-base.ts +14 -0
- package/src/data/query/pg-core/view-common.ts +1 -0
- package/src/data/query/pg-core/view.ts +435 -0
- package/src/data/query/pg-core/with-builder.ts +41 -0
- package/src/data/query/postgres-js/README.md +46 -0
- package/src/data/query/postgres-js/array-parser.ts +45 -0
- package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
- package/src/data/query/postgres-js/driver.ts +163 -0
- package/src/data/query/postgres-js/index.ts +2 -0
- package/src/data/query/postgres-js/session.ts +234 -0
- package/src/data/query/primary-key.ts +11 -0
- package/src/data/query/query-builders/query-builder.ts +19 -0
- package/src/data/query/query-builders/select.types.ts +175 -0
- package/src/data/query/query-promise.ts +35 -0
- package/src/data/query/relations.ts +725 -0
- package/src/data/query/runnable-query.ts +12 -0
- package/src/data/query/selection-proxy.ts +121 -0
- package/src/data/query/session.ts +8 -0
- package/src/data/query/sql/expressions/conditions.ts +741 -0
- package/src/data/query/sql/expressions/index.ts +2 -0
- package/src/data/query/sql/expressions/select.ts +45 -0
- package/src/data/query/sql/functions/aggregate.ts +129 -0
- package/src/data/query/sql/functions/index.ts +2 -0
- package/src/data/query/sql/functions/vector.ts +178 -0
- package/src/data/query/sql/index.ts +3 -0
- package/src/data/query/sql/sql.ts +794 -0
- package/src/data/query/subquery.ts +53 -0
- package/src/data/query/table.ts +208 -0
- package/src/data/query/table.utils.ts +2 -0
- package/src/data/query/tracing-utils.ts +3 -0
- package/src/data/query/tracing.ts +11 -0
- package/src/data/query/utils.ts +357 -0
- package/src/data/query/version.ts +4 -0
- package/src/data/query/view-common.ts +1 -0
- package/src/data/schema/branded.ts +13 -0
- package/src/data/schema/generated/coerce.ts +5 -0
- package/src/data/schema/generated/combinator.ts +6 -0
- package/src/data/schema/generated/encoding.ts +5 -0
- package/src/data/schema/generated/error.ts +6 -0
- package/src/data/schema/generated/format.ts +5 -0
- package/src/data/schema/generated/type-constructor.ts +10 -0
- package/src/data/schema/generated/type.ts +6 -0
- package/src/data/schema/index.ts +6 -0
- package/src/data/schema/json-schema.ts +33 -0
- package/src/data/schema/namespace.ts +55 -0
- package/src/data/schema/parse.ts +39 -0
- package/src/data/schema/types.ts +39 -0
- package/src/data/temporal/append-only.ts +134 -0
- package/src/data/temporal/close.ts +27 -0
- package/src/data/temporal/columns.ts +10 -0
- package/src/data/temporal/definition-queries.ts +70 -0
- package/src/data/temporal/errors.ts +10 -0
- package/src/data/temporal/get-current.ts +22 -0
- package/src/data/temporal/index.ts +9 -0
- package/src/data/temporal/namespace.ts +36 -0
- package/src/data/temporal/query.ts +25 -0
- package/src/data/temporal/resolve.ts +17 -0
- package/src/data/temporal/revise.ts +67 -0
- package/src/data/temporal/types.ts +22 -0
- package/src/data/valve/artifact.ts +33 -0
- package/src/data/valve/contract-diff.ts +21 -0
- package/src/data/valve/cross.ts +57 -0
- package/src/data/valve/crossing-ledger.ts +50 -0
- package/src/data/valve/decision.ts +44 -0
- package/src/data/valve/durable-crossing.ts +52 -0
- package/src/data/valve/execute-crossing.ts +77 -0
- package/src/data/valve/faucet.ts +56 -0
- package/src/data/valve/gate.ts +114 -0
- package/src/data/valve/graph.ts +77 -0
- package/src/data/valve/index.ts +53 -0
- package/src/data/valve/namespace.ts +57 -0
- package/src/data/valve/registry.ts +50 -0
- package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
- package/src/data/valve/schema/valve-crossing.ts +9 -0
- package/src/data/valve/supply.ts +154 -0
- package/src/data/valve/types.ts +143 -0
- package/src/data/vector/columns.ts +27 -0
- package/src/data/vector/index.ts +4 -0
- package/src/data/vector/namespace.ts +33 -0
- package/src/data/vector/query.ts +85 -0
- package/src/data/vector/validate.ts +16 -0
- package/src/data/write/audit/actor.ts +15 -0
- package/src/data/write/audit/audit-columns.ts +18 -0
- package/src/data/write/audit/audit-select.ts +111 -0
- package/src/data/write/audit/bound-transaction.ts +14 -0
- package/src/data/write/audit/cte-compile.ts +32 -0
- package/src/data/write/audit/decode-returning.ts +47 -0
- package/src/data/write/audit/final-select.ts +43 -0
- package/src/data/write/audit/infra-audit.ts +36 -0
- package/src/data/write/audit/primary-key.ts +65 -0
- package/src/data/write/audit/projection.ts +41 -0
- package/src/data/write/audit/read-touches.ts +86 -0
- package/src/data/write/audit/touch-match.ts +78 -0
- package/src/data/write/audit/verb-delete.ts +62 -0
- package/src/data/write/audit/verb-insert.ts +36 -0
- package/src/data/write/audit/verb-select.ts +47 -0
- package/src/data/write/audit/verb-update.ts +60 -0
- package/src/data/write/audit/verb-upsert.ts +104 -0
- package/src/data/write/conflict.ts +43 -0
- package/src/data/write/delete.ts +38 -0
- package/src/data/write/execute.ts +243 -0
- package/src/data/write/index.ts +11 -0
- package/src/data/write/insert.ts +63 -0
- package/src/data/write/state.ts +43 -0
- package/src/data/write/update.ts +54 -0
- package/src/data/write/upsert.ts +58 -0
- package/src/data/write/values.ts +19 -0
- package/src/index.ts +230 -0
- package/src/infra/access-links/code.ts +70 -0
- package/src/infra/access-links/handle.ts +892 -0
- package/src/infra/access-links/index.ts +9 -0
- package/src/infra/access-links/namespace.ts +30 -0
- package/src/infra/access-links/person-check.ts +177 -0
- package/src/infra/access-links/schema.ts +71 -0
- package/src/infra/alerts/async-sources.ts +231 -0
- package/src/infra/alerts/audit.ts +36 -0
- package/src/infra/alerts/beat.ts +123 -0
- package/src/infra/alerts/client-faults.ts +111 -0
- package/src/infra/alerts/faults.ts +156 -0
- package/src/infra/alerts/handle.ts +778 -0
- package/src/infra/alerts/http.ts +192 -0
- package/src/infra/alerts/index.ts +12 -0
- package/src/infra/alerts/infra-sources.ts +180 -0
- package/src/infra/alerts/namespace.ts +81 -0
- package/src/infra/alerts/redact.ts +21 -0
- package/src/infra/alerts/request-faults.ts +41 -0
- package/src/infra/alerts/schema.ts +124 -0
- package/src/infra/alerts/sinks.ts +125 -0
- package/src/infra/alerts/source-options.ts +26 -0
- package/src/infra/alerts/sources.ts +503 -0
- package/src/infra/alerts/subsystem-sources.ts +137 -0
- package/src/infra/audit/emit.ts +87 -0
- package/src/infra/audit/index.ts +2 -0
- package/src/infra/audit/namespace.ts +22 -0
- package/src/infra/audit/schema.sql +35 -0
- package/src/infra/audit/types.ts +18 -0
- package/src/infra/aws/credentials.ts +147 -0
- package/src/infra/aws/sigv4.ts +140 -0
- package/src/infra/cache/create.ts +58 -0
- package/src/infra/cache/entry.ts +81 -0
- package/src/infra/cache/index.ts +4 -0
- package/src/infra/cache/namespace.ts +10 -0
- package/src/infra/cache/tenant.ts +93 -0
- package/src/infra/cache/types.ts +24 -0
- package/src/infra/crawl/audit.ts +35 -0
- package/src/infra/crawl/errors.ts +25 -0
- package/src/infra/crawl/handle.ts +693 -0
- package/src/infra/crawl/index.ts +9 -0
- package/src/infra/crawl/links.ts +160 -0
- package/src/infra/crawl/namespace.ts +36 -0
- package/src/infra/crawl/robots.ts +100 -0
- package/src/infra/crawl/schema.ts +89 -0
- package/src/infra/credit-line/handle.ts +883 -0
- package/src/infra/credit-line/index.ts +6 -0
- package/src/infra/credit-line/namespace.ts +7 -0
- package/src/infra/credit-line/schema.ts +96 -0
- package/src/infra/credits/handle.ts +706 -0
- package/src/infra/credits/index.ts +5 -0
- package/src/infra/credits/namespace.ts +7 -0
- package/src/infra/credits/schema.ts +83 -0
- package/src/infra/email/audit.ts +36 -0
- package/src/infra/email/inbound.ts +622 -0
- package/src/infra/email/index.ts +11 -0
- package/src/infra/email/mail.ts +517 -0
- package/src/infra/email/mime.ts +447 -0
- package/src/infra/email/namespace.ts +22 -0
- package/src/infra/email/providers.ts +111 -0
- package/src/infra/email/schema.ts +96 -0
- package/src/infra/email/sns.ts +200 -0
- package/src/infra/email/unsubscribe.ts +44 -0
- package/src/infra/fetch/address.ts +88 -0
- package/src/infra/fetch/connect-retry.ts +87 -0
- package/src/infra/fetch/handle.ts +329 -0
- package/src/infra/fetch/index.ts +7 -0
- package/src/infra/fetch/namespace.ts +70 -0
- package/src/infra/idempotency/index.ts +3 -0
- package/src/infra/idempotency/middleware.ts +87 -0
- package/src/infra/idempotency/store.ts +92 -0
- package/src/infra/idempotency/types.ts +30 -0
- package/src/infra/join-requests/handle.ts +312 -0
- package/src/infra/join-requests/index.ts +2 -0
- package/src/infra/join-requests/namespace.ts +7 -0
- package/src/infra/join-requests/schema.ts +33 -0
- package/src/infra/outbox/core.ts +165 -0
- package/src/infra/quota/errors.ts +32 -0
- package/src/infra/quota/handle.ts +215 -0
- package/src/infra/quota/index.ts +5 -0
- package/src/infra/quota/namespace.ts +7 -0
- package/src/infra/quota/schema.ts +15 -0
- package/src/infra/storage/backend.ts +34 -0
- package/src/infra/storage/handle.ts +513 -0
- package/src/infra/storage/index.ts +9 -0
- package/src/infra/storage/local.ts +104 -0
- package/src/infra/storage/namespace.ts +13 -0
- package/src/infra/storage/s3.ts +257 -0
- package/src/infra/storage/schema.ts +24 -0
- package/src/infra/support/handle.ts +869 -0
- package/src/infra/support/http.ts +229 -0
- package/src/infra/support/index.ts +7 -0
- package/src/infra/support/namespace.ts +12 -0
- package/src/infra/support/schema.ts +96 -0
- package/src/infra/webhook/inbound.ts +45 -0
- package/src/infra/webhook/index.ts +5 -0
- package/src/infra/webhook/namespace.ts +24 -0
- package/src/infra/webhook/outbound.ts +315 -0
- package/src/infra/webhook/sign.ts +32 -0
- package/src/infra/webhook/verify.ts +82 -0
- package/src/invariants/assert.ts +26 -0
- package/src/invariants/index.ts +1 -0
- package/src/lint/config.ts +56 -0
- package/src/lint/doctrine.ts +163 -0
- package/src/lint/include.ts +97 -0
- package/src/lint/index.ts +3 -0
- package/src/lint/preset.ts +10 -0
- package/src/lint/resolver.ts +218 -0
- package/src/lint/rules/no-reexports.ts +46 -0
- package/src/request/behavior/audited.ts +85 -0
- package/src/request/behavior/cached.ts +62 -0
- package/src/request/behavior/compose.ts +28 -0
- package/src/request/behavior/index.ts +5 -0
- package/src/request/behavior/namespace.ts +16 -0
- package/src/request/behavior/types.ts +26 -0
- package/src/request/behavior/versioned.ts +54 -0
- package/src/request/context/create.ts +103 -0
- package/src/request/context/guard.ts +49 -0
- package/src/request/context/index.ts +4 -0
- package/src/request/context/run-in-transaction.ts +200 -0
- package/src/request/context/store.ts +28 -0
- package/src/request/context/types.ts +50 -0
- package/src/request/di/builder.ts +535 -0
- package/src/request/di/index.ts +8 -0
- package/src/request/di/resolve.ts +271 -0
- package/src/request/di/types.ts +262 -0
- package/src/request/fitting/namespace.ts +7 -0
- package/src/request/fixture/batch.ts +71 -0
- package/src/request/fixture/builder.ts +26 -0
- package/src/request/fixture/errors.ts +10 -0
- package/src/request/fixture/index.ts +8 -0
- package/src/request/fixture/namespace.ts +23 -0
- package/src/request/fixture/paginate.ts +44 -0
- package/src/request/fixture/to-handlers.ts +260 -0
- package/src/request/fixture/types.ts +79 -0
- package/src/request/flow/act-as.ts +78 -0
- package/src/request/flow/namespace.ts +17 -0
- package/src/request/http/error-step.ts +42 -0
- package/src/request/http/file.ts +141 -0
- package/src/request/http/index.ts +4 -0
- package/src/request/http/middleware.ts +141 -0
- package/src/request/http/namespace.ts +5 -0
- package/src/request/http/rate-limit-classes.ts +170 -0
- package/src/request/http/response.ts +82 -0
- package/src/request/http/server.ts +916 -0
- package/src/request/http/sniff.ts +261 -0
- package/src/request/http/types.ts +250 -0
- package/src/request/http/upload.ts +284 -0
- package/src/request/internal/text.ts +6 -0
- package/src/request/openapi/generate.ts +205 -0
- package/src/request/openapi/index.ts +2 -0
- package/src/request/openapi/namespace.ts +7 -0
- package/src/test/auth.ts +13 -0
- package/src/test/auto-setup.ts +45 -0
- package/src/test/context.ts +91 -0
- package/src/test/crawl-fixture.ts +157 -0
- package/src/test/discover-fork.ts +17 -0
- package/src/test/fingerprint.ts +135 -0
- package/src/test/globals.d.ts +7 -0
- package/src/test/index.ts +19 -0
- package/src/test/namespace.ts +87 -0
- package/src/test/plugin.ts +189 -0
- package/src/test/progress.ts +185 -0
- package/src/test/reap-after-run.ts +28 -0
- package/src/test/reap.ts +286 -0
- package/src/test/reset.ts +253 -0
- package/src/test/savepoints.ts +172 -0
- package/src/test/setup.ts +1422 -0
- package/src/test/system-channel.ts +50 -0
- package/src/test/vitest-plugin.ts +142 -0
- package/src/test/vitest-workspace.ts +507 -0
- package/src/test/vitest.ts +333 -0
- package/src/test/watch.ts +105 -0
- package/src/trace/capture/bounded-serializer.ts +146 -0
- package/src/trace/cardinality/analyze.ts +173 -0
- package/src/trace/cardinality/branch-context.ts +214 -0
- package/src/trace/cardinality/check.ts +33 -0
- package/src/trace/cardinality/classify-edge.ts +64 -0
- package/src/trace/cardinality/structural-max.ts +107 -0
- package/src/trace/check/coverage-check.ts +188 -0
- package/src/trace/check/flow-check.ts +103 -0
- package/src/trace/check/flow-step-site.ts +234 -0
- package/src/trace/check/io-pattern.ts +116 -0
- package/src/trace/codemod/add-cardinality.ts +152 -0
- package/src/trace/codemod/run.ts +80 -0
- package/src/trace/entry/child-trace.ts +21 -0
- package/src/trace/entry/close-trace.ts +87 -0
- package/src/trace/entry/finalize-trace.ts +28 -0
- package/src/trace/entry/open-trace.ts +138 -0
- package/src/trace/entry/parent-trace.ts +20 -0
- package/src/trace/entry/parse-traceparent.ts +39 -0
- package/src/trace/entry/process-start.ts +38 -0
- package/src/trace/entry/trace-context.ts +26 -0
- package/src/trace/entry/tracing-enabled.ts +10 -0
- package/src/trace/external/record.ts +195 -0
- package/src/trace/external/report.ts +66 -0
- package/src/trace/namespace.ts +19 -0
- package/src/trace/partition/day.ts +47 -0
- package/src/trace/partition/index.ts +14 -0
- package/src/trace/partition/partition-name.ts +23 -0
- package/src/trace/partition/plan.ts +82 -0
- package/src/trace/partition/schedule.ts +26 -0
- package/src/trace/partition/startup-hook.ts +39 -0
- package/src/trace/partition/sync.ts +154 -0
- package/src/trace/partition/tables.ts +53 -0
- package/src/trace/read/tree.ts +83 -0
- package/src/trace/retain/copy-on-error.ts +76 -0
- package/src/trace/runtime/expire-stale-traces.ts +54 -0
- package/src/trace/runtime/flow-step.ts +147 -0
- package/src/trace/runtime/flush-steps.ts +31 -0
- package/src/trace/runtime/pending-writes.ts +54 -0
- package/src/trace/runtime/step-buffer.ts +298 -0
- package/src/trace/runtime/step-error.ts +21 -0
- package/src/trace/runtime/step-meta.ts +55 -0
- package/src/trace/runtime/thenable.ts +12 -0
- package/src/trace/runtime/truncate-trace.ts +62 -0
- package/src/trace/schema/trace-fields.ts +52 -0
- package/src/trace/schema/trace-retained.ts +22 -0
- package/src/trace/schema/trace-step-fields.ts +44 -0
- package/src/trace/schema/trace-step-retained.ts +25 -0
- package/src/trace/schema/trace-step.ts +18 -0
- package/src/trace/schema/trace.ts +12 -0
- package/src/trace/transformer/apply.ts +79 -0
- package/src/trace/transformer/build-meta-literal.ts +30 -0
- package/src/trace/transformer/find-flow-step-sites.ts +85 -0
- package/src/trace/transformer/inject-flow-step-import.ts +39 -0
- package/src/trace/transformer/transform-error.ts +14 -0
- package/src/trace/transformer/transform-flow-steps.ts +123 -0
- package/src/trace/transformer/wrap-function-declaration.ts +62 -0
- package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
- package/src/workspace/affected.ts +110 -0
- package/src/workspace/changed-files.ts +163 -0
- package/src/workspace/check-deps.ts +213 -0
- package/src/workspace/checksum.ts +20 -0
- package/src/workspace/dependency-graph.ts +67 -0
- package/src/workspace/detect-pipework-copies.ts +93 -0
- package/src/workspace/discover.ts +206 -0
- package/src/workspace/generate.ts +401 -0
- package/src/workspace/index.ts +7 -0
- package/src/workspace/module-imports-pipework.ts +39 -0
- package/src/workspace/module-package-name.ts +12 -0
- package/src/workspace/scrub-git-env.ts +23 -0
- package/src/workspace/source-paths.ts +36 -0
- package/src/workspace/sync.ts +182 -0
- package/dist/REFERENCE.md +0 -1477
- package/dist/infra/access-links/rate-limit.d.ts +0 -10
- package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
- package/dist/infra/access-links/rate-limit.js +0 -28
- package/dist/infra/access-links/rate-limit.js.map +0 -1
- package/dist/infra/email/sigv4.d.ts +0 -24
- package/dist/infra/email/sigv4.d.ts.map +0 -1
- package/dist/infra/email/sigv4.js +0 -55
- package/dist/infra/email/sigv4.js.map +0 -1
|
@@ -0,0 +1,1418 @@
|
|
|
1
|
+
import { execSync, execFile } from 'node:child_process'
|
|
2
|
+
import { promisify } from 'node:util'
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { createHash } from 'node:crypto'
|
|
5
|
+
import { join, dirname, isAbsolute, resolve } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile)
|
|
10
|
+
import { computeChecksum, checksumFromPackageJson } from '../../workspace/checksum.js'
|
|
11
|
+
import { VITEST_CONFIG_CONTENT, ESLINT_CONFIG_CONTENT } from '../../workspace/generate.js'
|
|
12
|
+
import { validateDoctrine, findSourceFiles } from '../../lint/doctrine.js'
|
|
13
|
+
import { checkFlowAnnotations } from '../../trace/check/flow-check.js'
|
|
14
|
+
import { checkCoverage } from '../../trace/check/coverage-check.js'
|
|
15
|
+
import { checkCardinality } from '../../trace/cardinality/check.js'
|
|
16
|
+
import { checkDependencies } from '../../workspace/check-deps.js'
|
|
17
|
+
import { detectPipeworkInstall, distinctPipeworkVersions } from '../../workspace/detect-pipework-copies.js'
|
|
18
|
+
import { info, success, error as logError, warn } from '../output.js'
|
|
19
|
+
import { OWNER_DDL_LABELS } from '../../data/db/owner-labels.js'
|
|
20
|
+
import type { ScopeMode } from '../../workspace/affected.js'
|
|
21
|
+
import { resolveAffected } from '../../workspace/affected.js'
|
|
22
|
+
import { findConfig } from '../../core/config/discover.js'
|
|
23
|
+
import { expandGlob, resolveModules, type ResolvedModule } from '../../workspace/discover.js'
|
|
24
|
+
import { filesMatchingGlobs, globRoot, lintIncludeGlobs } from '../../lint/include.js'
|
|
25
|
+
import { StartupValidationError } from '../../core/validation/types.js'
|
|
26
|
+
import type { Manifold } from '../../core/pipework.js'
|
|
27
|
+
import type { LintConfig, VitestConfig } from '../../core/config/schema.js'
|
|
28
|
+
import { shardArg, shardModules, type Shard } from './shard.js'
|
|
29
|
+
|
|
30
|
+
// #322: discoverInstance evaluates pipework.config.ts — a dynamic import plus a
|
|
31
|
+
// full module evaluation. A single `check` run needs it from four call sites;
|
|
32
|
+
// memoizing collapses that to one load. The cache is reset at the start of
|
|
33
|
+
// every `check` so a long-lived process still picks up later config edits.
|
|
34
|
+
let instanceCache = new Map<string, Promise<Manifold>>()
|
|
35
|
+
|
|
36
|
+
async function discoverInstanceCached(cwd: string): Promise<Manifold> {
|
|
37
|
+
let loading = instanceCache.get(cwd)
|
|
38
|
+
if (loading === undefined) {
|
|
39
|
+
loading = import('../../core/config/discover.js').then(m => m.discoverInstance(cwd))
|
|
40
|
+
instanceCache.set(cwd, loading)
|
|
41
|
+
}
|
|
42
|
+
return loading
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function check(cwd: string, scope?: ScopeMode, shard?: Shard): Promise<void> {
|
|
46
|
+
instanceCache = new Map()
|
|
47
|
+
let failed = false
|
|
48
|
+
|
|
49
|
+
const checksumOk = verifyChecksums(cwd)
|
|
50
|
+
if (!checksumOk) {
|
|
51
|
+
failed = true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const vitestOk = await verifyVitestConfig(cwd)
|
|
55
|
+
if (!vitestOk) {
|
|
56
|
+
failed = true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const eslintOk = verifyEslintConfig(cwd)
|
|
60
|
+
if (!eslintOk) {
|
|
61
|
+
failed = true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { paths: modulePaths, all: fullScope, configError, basis } = await resolveModulePaths(cwd, scope)
|
|
65
|
+
|
|
66
|
+
// #335: a pipework.config.ts that exists but cannot be loaded used to be
|
|
67
|
+
// swallowed — module scoping silently degraded to filesystem discovery, which
|
|
68
|
+
// in a package with no pnpm-workspace.yaml resolves to *no* modules, so lint,
|
|
69
|
+
// type check, doctrine and the flow checks never ran and `check` still exited
|
|
70
|
+
// 0 with "All checks passed". A step that cannot run is a failed step: the
|
|
71
|
+
// load failure is reported here and fails the run. The remaining steps still
|
|
72
|
+
// execute so one run surfaces everything it can, but none of them can green it.
|
|
73
|
+
if (configError !== undefined) {
|
|
74
|
+
info('Config load')
|
|
75
|
+
logError('Config load — pipework.config.ts could not be loaded. Config-derived steps ran degraded or not at all.')
|
|
76
|
+
for (const line of configError.message.split('\n')) {
|
|
77
|
+
process.stderr.write(` ${line}\n`)
|
|
78
|
+
}
|
|
79
|
+
failed = true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (modulePaths.length > 0) {
|
|
83
|
+
const sourceFiles = findSourceFiles(cwd, modulePaths)
|
|
84
|
+
|
|
85
|
+
const doctrineOk = runDoctrineValidation(cwd, sourceFiles)
|
|
86
|
+
if (!doctrineOk) {
|
|
87
|
+
failed = true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Phase 4b: the flow and cardinality checks are hard errors. Consumers
|
|
91
|
+
// upgrading from an earlier release run `pipework codemod` once to fill
|
|
92
|
+
// missing `@downstream` cardinalities; anything the codemod cannot
|
|
93
|
+
// resolve (indirection, non-trivial form, method flow steps, orphans)
|
|
94
|
+
// must be fixed by hand before the build will pass.
|
|
95
|
+
if (!runFlowCheck(cwd, sourceFiles)) {
|
|
96
|
+
failed = true
|
|
97
|
+
}
|
|
98
|
+
const configuredStructuralMax = await loadConfiguredStructuralMax(cwd)
|
|
99
|
+
if (!runCardinalityCheck(cwd, sourceFiles, configuredStructuralMax, fullScope)) {
|
|
100
|
+
failed = true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Phase 6f: Tier-2 coverage ships as warning. Surfaces structural gaps —
|
|
104
|
+
// I/O outside any flow_step, more than one I/O in a single flow_step —
|
|
105
|
+
// without failing the build. Phase 7a flips it to error once consumer
|
|
106
|
+
// corpora have migrated to the new write surface. Consumers extend the
|
|
107
|
+
// I/O call-target registry through `trace.io` in pipework.config.ts (Q9).
|
|
108
|
+
const consumerIoPatterns = await loadConsumerIoPatterns(cwd)
|
|
109
|
+
runCoverageCheckWarning(cwd, sourceFiles, consumerIoPatterns)
|
|
110
|
+
|
|
111
|
+
const lintOk = await runLint(cwd, modulePaths, shard)
|
|
112
|
+
if (!lintOk) {
|
|
113
|
+
failed = true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const typeCheckOk = await runTypeCheck(cwd, modulePaths, shard)
|
|
117
|
+
if (!typeCheckOk) {
|
|
118
|
+
failed = true
|
|
119
|
+
}
|
|
120
|
+
} else if (scope !== undefined && scope.kind === 'diff') {
|
|
121
|
+
// The basis says WHY the set is empty — a green here must state the scope
|
|
122
|
+
// it rested on, or an empty basis reads as "everything passed".
|
|
123
|
+
success(`No affected modules — per-module checks skipped (basis: ${basis ?? 'unknown'})`)
|
|
124
|
+
} else {
|
|
125
|
+
// Not a diff scope, so this is not "nothing changed" — no module could be
|
|
126
|
+
// resolved at all. Every per-module step was skipped; say so rather than
|
|
127
|
+
// letting their absence read as a clean pass (#335).
|
|
128
|
+
warn('No modules resolved — lint, type check, doctrine and flow checks did NOT run')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (configError === undefined && findConfig(cwd) !== null) {
|
|
132
|
+
if (!(await runStartupChecks(cwd))) {
|
|
133
|
+
failed = true
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const boundaryScript = join(cwd, 'scripts', 'check-boundaries.js')
|
|
138
|
+
if (existsSync(boundaryScript)) {
|
|
139
|
+
if (!runStep('Boundaries', 'node scripts/check-boundaries.js', cwd)) {
|
|
140
|
+
failed = true
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const depCheckOk = runDependencyCheck(cwd)
|
|
145
|
+
if (!depCheckOk) {
|
|
146
|
+
failed = true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const copiesOk = runPipeworkCopiesCheck(cwd)
|
|
150
|
+
if (!copiesOk) {
|
|
151
|
+
failed = true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (failed) process.exit(1)
|
|
155
|
+
success('All checks passed')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The startup checks that need no database — what `serve` would refuse before connecting. */
|
|
159
|
+
export async function runStartupChecks(cwd: string): Promise<boolean> {
|
|
160
|
+
info('Startup checks')
|
|
161
|
+
let doors: readonly string[] = []
|
|
162
|
+
let roles: readonly string[] = []
|
|
163
|
+
try {
|
|
164
|
+
const instance = await discoverInstanceCached(cwd)
|
|
165
|
+
await instance.validate()
|
|
166
|
+
// Loaded here, not at the top: it pulls in the runtime, which `lintDirectory` callers must not load.
|
|
167
|
+
const { queueFrameworkDoors } = await import('../../async/jobs/queue-door.js')
|
|
168
|
+
doors = describeDoors({ ...instance.config.doors, ...instance.config.frameworkDoors, ...queueFrameworkDoors(instance.config) })
|
|
169
|
+
roles = describeRuntimeRoles(instance.config.databases())
|
|
170
|
+
} catch (err: unknown) {
|
|
171
|
+
if (err instanceof StartupValidationError) {
|
|
172
|
+
logError(`Startup checks — ${String(err.issues.length)} issue${err.issues.length === 1 ? '' : 's'}`)
|
|
173
|
+
for (const issue of err.issues) {
|
|
174
|
+
process.stderr.write(` [${issue.phase}] ${issue.message}\n`)
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
logError('Startup checks — could not run')
|
|
178
|
+
process.stderr.write(` ${err instanceof Error ? err.message : String(err)}\n`)
|
|
179
|
+
}
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
success('Startup checks')
|
|
183
|
+
for (const line of roles) info(line)
|
|
184
|
+
if (doors.length > 0) {
|
|
185
|
+
success(`Doors (${String(doors.length)} declared)`)
|
|
186
|
+
for (const line of doors) info(` ${line}`)
|
|
187
|
+
}
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** One line per platform-mode database: the role a running manifold connects as, and the only callers that may open the owner. */
|
|
192
|
+
export function describeRuntimeRoles(databases: ReadonlyMap<string, { readonly name: string; readonly rls?: { readonly platformRole?: string } }>): string[] {
|
|
193
|
+
const platform = [...databases.values()].filter(db => db.rls?.platformRole !== undefined)
|
|
194
|
+
return platform.map(db =>
|
|
195
|
+
`runtime role: ${db.rls!.platformRole!} (owner used only for: ${OWNER_DDL_LABELS.join(', ')})` +
|
|
196
|
+
(platform.length > 1 ? ` [${db.name}]` : ''))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** One line per declared door: `name -> database.table: reason`. */
|
|
200
|
+
export function describeDoors(doors: Readonly<Record<string, { database: string; table: string; reason: string; framework?: boolean }>>): string[] {
|
|
201
|
+
return Object.entries(doors).map(([name, door]) =>
|
|
202
|
+
`${name} -> ${door.database}.${door.table}: ${door.reason}${door.framework === true ? ' (pipework\'s own)' : ''}`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface ModuleScope {
|
|
206
|
+
readonly paths: string[]
|
|
207
|
+
/**
|
|
208
|
+
* True when every module is in scope. A graph-wide derivation — the Q7
|
|
209
|
+
* `structuralMax` — only reflects the whole codebase when this holds; under a
|
|
210
|
+
* scoped run it sees a subset and the derived value is a lower bound.
|
|
211
|
+
*/
|
|
212
|
+
readonly all: boolean
|
|
213
|
+
/**
|
|
214
|
+
* Set when a pipework.config.ts was found but could not be loaded (#335).
|
|
215
|
+
* The module set then comes from filesystem discovery and is not the declared
|
|
216
|
+
* one — `--module` is not applied and the set can be empty. `check` reports
|
|
217
|
+
* this as a failed step; it must never contribute to a green exit.
|
|
218
|
+
*/
|
|
219
|
+
readonly configError?: Error
|
|
220
|
+
/** Population line from `resolveAffected` — what the module scope was computed from. */
|
|
221
|
+
readonly basis?: string
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function resolveModulePaths(cwd: string, scope?: ScopeMode): Promise<ModuleScope> {
|
|
225
|
+
const configPath = findConfig(cwd)
|
|
226
|
+
|
|
227
|
+
if (configPath !== null) {
|
|
228
|
+
let instance: Manifold
|
|
229
|
+
try {
|
|
230
|
+
instance = await discoverInstanceCached(cwd)
|
|
231
|
+
} catch (err: unknown) {
|
|
232
|
+
// Only the *load* is tolerated (degraded, and reported by the caller).
|
|
233
|
+
// Everything below — an unknown `--module`, a malformed `modules` map —
|
|
234
|
+
// is a user error with an actionable message, and swallowing it was its
|
|
235
|
+
// own false green: `pipework check --module typo` silently checked
|
|
236
|
+
// something other than what was asked for.
|
|
237
|
+
return {
|
|
238
|
+
paths: discoverModulePaths(cwd),
|
|
239
|
+
all: true,
|
|
240
|
+
configError: err instanceof Error ? err : new Error(String(err)),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const blueprint = instance.config.raw
|
|
245
|
+
const configDir = dirname(configPath)
|
|
246
|
+
const resolved = resolveModules(blueprint.modules, configDir)
|
|
247
|
+
|
|
248
|
+
// No declared modules: this is a single-package repo whose one implicit
|
|
249
|
+
// module is the repo root, addressed as '.'. Synthesize it so check
|
|
250
|
+
// typechecks/lints/validates the root `src/` — `runTypeCheck`, `runLint`,
|
|
251
|
+
// and `findSourceFiles` all collapse a '.' module path back to the root.
|
|
252
|
+
// `resolveVitestModules` projects the same '.' module, keeping the two in
|
|
253
|
+
// sync. Without this, `discoverModulePaths` excludes '.' and check silently
|
|
254
|
+
// skips typecheck/lint/doctrine for the entire project.
|
|
255
|
+
const effectiveModules: ResolvedModule[] = resolved.length === 0
|
|
256
|
+
? [{ key: '.', relativePath: '.', config: {} }]
|
|
257
|
+
: resolved
|
|
258
|
+
|
|
259
|
+
const effectiveScope = scope ?? { kind: 'all' } as ScopeMode
|
|
260
|
+
const result = resolveAffected(configDir, effectiveModules, blueprint.name ?? '', effectiveScope, blueprint.fastPath?.baseBranches)
|
|
261
|
+
|
|
262
|
+
// Population line: what this run's module scope was computed from. Printed
|
|
263
|
+
// unconditionally so scope-of-invocation is visible next to every verdict.
|
|
264
|
+
info(`Scope: ${result.basis}`)
|
|
265
|
+
|
|
266
|
+
if (result.failedOpen === true) {
|
|
267
|
+
warn('Affected set could not be computed — checking all modules (fast path failed open). Issue #325.')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (result.modules.size === 0) {
|
|
271
|
+
return { paths: [], all: result.all, basis: result.basis }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const paths = effectiveModules.map(m => m.relativePath).filter(p => result.modules.has(p))
|
|
275
|
+
|
|
276
|
+
if (!result.all) {
|
|
277
|
+
info(`Scoped to ${String(paths.length)} of ${String(effectiveModules.length)} module(s): ${paths.join(', ')}`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { paths, all: result.all, basis: result.basis }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { paths: discoverModulePaths(cwd), all: true }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function verifyChecksums(cwd: string): boolean {
|
|
287
|
+
const pkgPath = join(cwd, 'package.json')
|
|
288
|
+
|
|
289
|
+
if (!existsSync(pkgPath)) {
|
|
290
|
+
return true
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let pkg: Record<string, unknown>
|
|
294
|
+
try {
|
|
295
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as Record<string, unknown>
|
|
296
|
+
} catch {
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (typeof pkg['_generated'] !== 'string') {
|
|
301
|
+
return true
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
info('Workspace checksums')
|
|
305
|
+
|
|
306
|
+
const storedChecksum = checksumFromPackageJson(readFileSync(pkgPath, 'utf-8'))
|
|
307
|
+
if (storedChecksum === undefined) {
|
|
308
|
+
warn('package.json has _generated marker but no _checksum')
|
|
309
|
+
return true
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const withoutChecksum = { ...pkg }
|
|
313
|
+
delete withoutChecksum['_checksum']
|
|
314
|
+
const content = JSON.stringify(withoutChecksum, null, 2) + '\n'
|
|
315
|
+
const expected = computeChecksum(content)
|
|
316
|
+
|
|
317
|
+
if (storedChecksum !== expected) {
|
|
318
|
+
logError('Workspace checksums — package.json has been modified. Run `pipework install` to regenerate.')
|
|
319
|
+
return false
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
success('Workspace checksums')
|
|
323
|
+
return true
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function verifyVitestConfig(cwd: string): Promise<boolean> {
|
|
327
|
+
const configPath = join(cwd, 'vitest.config.ts')
|
|
328
|
+
if (!existsSync(configPath)) {
|
|
329
|
+
return true
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
info('Vitest config')
|
|
333
|
+
|
|
334
|
+
let ok = true
|
|
335
|
+
const hasVitest = await blueprintHasVitest(cwd)
|
|
336
|
+
|
|
337
|
+
if (hasVitest === undefined) {
|
|
338
|
+
// pipework.config.ts is absent or could not be loaded — we cannot confirm
|
|
339
|
+
// whether pipework owns vitest.config.ts, so we must not fail on a file
|
|
340
|
+
// ownership we can't verify.
|
|
341
|
+
warn('vitest.config.ts present but pipework.config.ts could not be loaded — skipping vitest config check')
|
|
342
|
+
} else if (hasVitest) {
|
|
343
|
+
// The blueprint declares `vitest`, so pipework generates and owns
|
|
344
|
+
// vitest.config.ts — it must match the generated template byte-for-byte.
|
|
345
|
+
const content = readFileSync(configPath, 'utf-8')
|
|
346
|
+
if (content !== VITEST_CONFIG_CONTENT) {
|
|
347
|
+
logError('vitest.config.ts is stale. Run `pipework install` to regenerate.')
|
|
348
|
+
ok = false
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
// The blueprint does not declare `vitest`, so pipework did not generate
|
|
352
|
+
// this file. Test config is centralized — a hand-written vitest.config.ts
|
|
353
|
+
// is not a supported layout.
|
|
354
|
+
logError(
|
|
355
|
+
'vitest.config.ts is hand-written, but test config is centralized.\n' +
|
|
356
|
+
' Move its settings into the `vitest` key of pipework.config.ts and delete this file,\n' +
|
|
357
|
+
' then run `pipework install` to regenerate it.',
|
|
358
|
+
)
|
|
359
|
+
ok = false
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const staleConfigs = findStaleVitestConfigs(cwd)
|
|
363
|
+
if (staleConfigs.length > 0) {
|
|
364
|
+
warn('Stale vitest configs (test config is centralized in pipework.config.ts):')
|
|
365
|
+
for (const file of staleConfigs) {
|
|
366
|
+
warn(` ${file} — remove this file`)
|
|
367
|
+
}
|
|
368
|
+
ok = false
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (ok) {
|
|
372
|
+
success(`Vitest config${await describeWorkerIsolation(cwd)}`)
|
|
373
|
+
}
|
|
374
|
+
return ok
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Name the worker pool and isolation the projects actually run with.
|
|
379
|
+
*
|
|
380
|
+
* Pipework's defaults (`forks`, `isolate: false`) differ from vitest's own, so a suite
|
|
381
|
+
* migrating off a hand-written config changes isolation. Nothing said so (#394); the
|
|
382
|
+
* green line now does, and names each project that overrides.
|
|
383
|
+
*/
|
|
384
|
+
async function describeWorkerIsolation(cwd: string): Promise<string> {
|
|
385
|
+
let vitest: VitestConfig | undefined
|
|
386
|
+
try {
|
|
387
|
+
vitest = (await discoverInstanceCached(cwd)).config.raw.vitest
|
|
388
|
+
} catch {
|
|
389
|
+
return ''
|
|
390
|
+
}
|
|
391
|
+
if (vitest === undefined) return ''
|
|
392
|
+
|
|
393
|
+
const overrides: string[] = []
|
|
394
|
+
for (const [path, mod] of Object.entries(vitest.modules ?? {})) {
|
|
395
|
+
if (mod.pool !== undefined || mod.isolate !== undefined) overrides.push(path)
|
|
396
|
+
for (const [name, profile] of Object.entries(mod.profiles ?? {})) {
|
|
397
|
+
if (profile.pool !== undefined || profile.isolate !== undefined) overrides.push(`${path}:${name}`)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const base = ` (pool ${vitest.pool ?? 'forks'}, isolate ${String(vitest.isolate ?? false)})`
|
|
402
|
+
return overrides.length === 0 ? base : `${base}; overridden by ${overrides.join(', ')}`
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function verifyEslintConfig(cwd: string): boolean {
|
|
406
|
+
const configPath = join(cwd, 'eslint.config.js')
|
|
407
|
+
if (!existsSync(configPath)) {
|
|
408
|
+
return true
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
info('Eslint config')
|
|
412
|
+
|
|
413
|
+
// eslint.config.js is unconditionally generated by `pipework install` —
|
|
414
|
+
// when present it must match the template byte-for-byte. Consumer extensions
|
|
415
|
+
// (plugins, rules, additional flat-config blocks) flow through `lint.extra`
|
|
416
|
+
// in pipework.config.ts; the generated file is a thin resolver shell.
|
|
417
|
+
const content = readFileSync(configPath, 'utf-8')
|
|
418
|
+
if (content !== ESLINT_CONFIG_CONTENT) {
|
|
419
|
+
logError(
|
|
420
|
+
'eslint.config.js is stale. Run `pipework install` to regenerate.\n' +
|
|
421
|
+
' Consumer plugins, rules, and additional flat-config blocks belong under\n' +
|
|
422
|
+
' `lint.extra` in pipework.config.ts — the generated file is a resolver shell.',
|
|
423
|
+
)
|
|
424
|
+
return false
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
success('Eslint config')
|
|
428
|
+
return true
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Returns whether the project's blueprint declares centralized `vitest` config,
|
|
433
|
+
* or `undefined` when there is no loadable pipework.config.ts to consult.
|
|
434
|
+
*/
|
|
435
|
+
async function blueprintHasVitest(cwd: string): Promise<boolean | undefined> {
|
|
436
|
+
if (findConfig(cwd) === null) return undefined
|
|
437
|
+
try {
|
|
438
|
+
const instance = await discoverInstanceCached(cwd)
|
|
439
|
+
return instance.config.raw.vitest !== undefined
|
|
440
|
+
} catch {
|
|
441
|
+
return undefined
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Returns the configured `trace.structuralMax`, or `undefined` when the
|
|
447
|
+
* blueprint either doesn't set it or can't be loaded. The cardinality check
|
|
448
|
+
* uses this to verify the runtime cap against the value the analyzer derives.
|
|
449
|
+
*/
|
|
450
|
+
async function loadConfiguredStructuralMax(cwd: string): Promise<number | undefined> {
|
|
451
|
+
if (findConfig(cwd) === null) return undefined
|
|
452
|
+
try {
|
|
453
|
+
const instance = await discoverInstanceCached(cwd)
|
|
454
|
+
return instance.config.trace.structuralMax
|
|
455
|
+
} catch {
|
|
456
|
+
return undefined
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Returns the configured `trace.io` registry (Q9), or an empty record when
|
|
462
|
+
* the blueprint either doesn't extend the registry or can't be loaded. The
|
|
463
|
+
* Tier-2 coverage check unions this with the stock pipework registry.
|
|
464
|
+
*/
|
|
465
|
+
async function loadConsumerIoPatterns(cwd: string): Promise<Readonly<Record<string, 'read' | 'write'>>> {
|
|
466
|
+
if (findConfig(cwd) === null) return {}
|
|
467
|
+
try {
|
|
468
|
+
const instance = await discoverInstanceCached(cwd)
|
|
469
|
+
return instance.config.trace.io
|
|
470
|
+
} catch {
|
|
471
|
+
return {}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function findStaleVitestConfigs(cwd: string): string[] {
|
|
476
|
+
const workspaceYamlPath = join(cwd, 'pnpm-workspace.yaml')
|
|
477
|
+
if (!existsSync(workspaceYamlPath)) return []
|
|
478
|
+
|
|
479
|
+
const yaml = readFileSync(workspaceYamlPath, 'utf-8')
|
|
480
|
+
const packages: string[] = []
|
|
481
|
+
for (const line of yaml.split('\n')) {
|
|
482
|
+
const match = line.match(/^\s*-\s*"(.+)"/)
|
|
483
|
+
if (match?.[1] !== undefined && match[1] !== '.') {
|
|
484
|
+
packages.push(match[1])
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const stale: string[] = []
|
|
489
|
+
for (const pkg of packages) {
|
|
490
|
+
const pkgDir = join(cwd, pkg)
|
|
491
|
+
if (!existsSync(pkgDir)) continue
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
const entries = readdirSync(pkgDir)
|
|
495
|
+
for (const entry of entries) {
|
|
496
|
+
if (/^vitest.*\.config\.(ts|js|mts|mjs)$/.test(entry)) {
|
|
497
|
+
stale.push(join(pkg, entry))
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return stale.sort()
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function runDoctrineValidation(cwd: string, sourceFiles: string[]): boolean {
|
|
509
|
+
info('Doctrine validation')
|
|
510
|
+
|
|
511
|
+
if (sourceFiles.length === 0) {
|
|
512
|
+
success('Doctrine validation (no source files)')
|
|
513
|
+
return true
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const result = validateDoctrine(cwd, sourceFiles)
|
|
517
|
+
|
|
518
|
+
if (result.violations.length === 0) {
|
|
519
|
+
success(`Doctrine validation (${String(result.scanned)} files scanned)`)
|
|
520
|
+
return true
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const grouped = groupByRule(result.violations)
|
|
524
|
+
for (const [rule, items] of grouped) {
|
|
525
|
+
logError(`${rule} (${String(items.length)} violation${items.length === 1 ? '' : 's'})`)
|
|
526
|
+
for (const v of items.slice(0, 5)) {
|
|
527
|
+
warn(` ${v.file}:${String(v.line)} — ${v.message}`)
|
|
528
|
+
}
|
|
529
|
+
if (items.length > 5) {
|
|
530
|
+
warn(` ... and ${String(items.length - 5)} more`)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return false
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function runFlowCheck(cwd: string, sourceFiles: string[]): boolean {
|
|
538
|
+
info('Flow annotations')
|
|
539
|
+
|
|
540
|
+
if (sourceFiles.length === 0) {
|
|
541
|
+
success('Flow annotations (no source files)')
|
|
542
|
+
return true
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const result = checkFlowAnnotations(cwd, sourceFiles)
|
|
546
|
+
|
|
547
|
+
if (result.violations.length === 0) {
|
|
548
|
+
success(`Flow annotations (${String(result.scanned)} files scanned)`)
|
|
549
|
+
return true
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const grouped = groupByRule(result.violations)
|
|
553
|
+
for (const [rule, items] of grouped) {
|
|
554
|
+
logError(`${rule} (${String(items.length)} violation${items.length === 1 ? '' : 's'})`)
|
|
555
|
+
for (const v of items.slice(0, 5)) {
|
|
556
|
+
warn(` ${v.file}:${String(v.line)} — ${v.message}`)
|
|
557
|
+
}
|
|
558
|
+
if (items.length > 5) {
|
|
559
|
+
warn(` ... and ${String(items.length - 5)} more`)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return false
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function runCardinalityCheck(
|
|
566
|
+
cwd: string,
|
|
567
|
+
sourceFiles: string[],
|
|
568
|
+
configuredStructuralMax?: number,
|
|
569
|
+
fullScope = true,
|
|
570
|
+
): boolean {
|
|
571
|
+
info('Flow cardinality')
|
|
572
|
+
|
|
573
|
+
if (sourceFiles.length === 0) {
|
|
574
|
+
success('Flow cardinality (no source files)')
|
|
575
|
+
return true
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const result = checkCardinality(cwd, sourceFiles)
|
|
579
|
+
|
|
580
|
+
// Verify the configured cap against what the analyzer just derived (Q7).
|
|
581
|
+
// Unset config + flow_steps present → warn with the recommended value (the
|
|
582
|
+
// runtime cap stays open until the user sets it). Zero flow_steps → no cap to
|
|
583
|
+
// check.
|
|
584
|
+
//
|
|
585
|
+
// The analyzer derives structuralMax from the source files it was handed.
|
|
586
|
+
// Under a scoped run (`--staged`, a diff) it sees only the affected modules,
|
|
587
|
+
// and dropping modules only ever shortens the longest step-chain — so the
|
|
588
|
+
// derived value is a *lower bound* on the true global max. That makes the two
|
|
589
|
+
// mismatch directions asymmetric:
|
|
590
|
+
// derived > configured → a subset already exceeds the cap, so the whole
|
|
591
|
+
// graph does too. The cap is genuinely too low: good traces would be
|
|
592
|
+
// truncated. Sound under any scope → hard error.
|
|
593
|
+
// derived < configured → only conclusive when the analyzer saw every module
|
|
594
|
+
// (`fullScope`). Under a scoped run an unscanned module may hold the
|
|
595
|
+
// deeper chain, so this is deferred to the full-scope check (pre-push).
|
|
596
|
+
let configFailed = false
|
|
597
|
+
if (result.analyzed > 0 && configuredStructuralMax === undefined) {
|
|
598
|
+
warn(
|
|
599
|
+
` trace.structuralMax is unset — runtime step cap is open (truncation never fires). ` +
|
|
600
|
+
`Add this to pipework.config.ts: trace: { structuralMax: ${String(result.structuralMax)} }`,
|
|
601
|
+
)
|
|
602
|
+
} else if (result.analyzed > 0 && result.structuralMax > configuredStructuralMax!) {
|
|
603
|
+
logError(
|
|
604
|
+
` trace.structuralMax is too low — config has ${String(configuredStructuralMax)}, ` +
|
|
605
|
+
`analyzer derived ${String(result.structuralMax)}. A trace this deep exceeds the cap and ` +
|
|
606
|
+
`would be truncated. Update pipework.config.ts: trace: { structuralMax: ${String(result.structuralMax)} }`,
|
|
607
|
+
)
|
|
608
|
+
configFailed = true
|
|
609
|
+
} else if (result.analyzed > 0 && result.structuralMax < configuredStructuralMax! && fullScope) {
|
|
610
|
+
logError(
|
|
611
|
+
` trace.structuralMax is stale — config has ${String(configuredStructuralMax)}, ` +
|
|
612
|
+
`analyzer derived ${String(result.structuralMax)}. ` +
|
|
613
|
+
`Update pipework.config.ts: trace: { structuralMax: ${String(result.structuralMax)} }`,
|
|
614
|
+
)
|
|
615
|
+
configFailed = true
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (result.violations.length === 0) {
|
|
619
|
+
if (!configFailed) {
|
|
620
|
+
success(`Flow cardinality (${String(result.analyzed)} flow step${result.analyzed === 1 ? '' : 's'} analyzed)`)
|
|
621
|
+
}
|
|
622
|
+
return !configFailed
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const grouped = groupByRule(result.violations)
|
|
626
|
+
for (const [rule, items] of grouped) {
|
|
627
|
+
logError(`${rule} (${String(items.length)} violation${items.length === 1 ? '' : 's'})`)
|
|
628
|
+
for (const v of items.slice(0, 5)) {
|
|
629
|
+
warn(` ${v.file}:${String(v.line)} — ${v.message}`)
|
|
630
|
+
}
|
|
631
|
+
if (items.length > 5) {
|
|
632
|
+
warn(` ... and ${String(items.length - 5)} more`)
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return false
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function runCoverageCheckWarning(
|
|
639
|
+
cwd: string,
|
|
640
|
+
sourceFiles: string[],
|
|
641
|
+
extras: Readonly<Record<string, 'read' | 'write'>> = {},
|
|
642
|
+
): void {
|
|
643
|
+
info('Flow coverage (Tier-2, warning)')
|
|
644
|
+
|
|
645
|
+
if (sourceFiles.length === 0) {
|
|
646
|
+
success('Flow coverage (no source files)')
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const result = checkCoverage(cwd, sourceFiles, extras)
|
|
651
|
+
|
|
652
|
+
if (result.violations.length === 0) {
|
|
653
|
+
success(`Flow coverage (${String(result.scanned)} files scanned)`)
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Warning mode: emit diagnostics but never bump `failed`. The caller never
|
|
658
|
+
// sees a return value — this rule cannot fail the build until Phase 7a.
|
|
659
|
+
const grouped = groupByRule(result.violations)
|
|
660
|
+
warn(`Flow coverage (${String(result.violations.length)} warning${result.violations.length === 1 ? '' : 's'})`)
|
|
661
|
+
for (const [rule, items] of grouped) {
|
|
662
|
+
warn(` ${rule} (${String(items.length)})`)
|
|
663
|
+
for (const v of items.slice(0, 5)) {
|
|
664
|
+
warn(` ${v.file}:${String(v.line)} — ${v.message}`)
|
|
665
|
+
}
|
|
666
|
+
if (items.length > 5) {
|
|
667
|
+
warn(` ... and ${String(items.length - 5)} more`)
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function groupByRule<T extends { readonly rule: string }>(items: readonly T[]): Map<string, T[]> {
|
|
673
|
+
const map = new Map<string, T[]>()
|
|
674
|
+
for (const item of items) {
|
|
675
|
+
let arr = map.get(item.rule)
|
|
676
|
+
if (arr === undefined) {
|
|
677
|
+
arr = []
|
|
678
|
+
map.set(item.rule, arr)
|
|
679
|
+
}
|
|
680
|
+
arr.push(item)
|
|
681
|
+
}
|
|
682
|
+
return map
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function discoverModulePaths(cwd: string): string[] {
|
|
686
|
+
const workspaceYamlPath = join(cwd, 'pnpm-workspace.yaml')
|
|
687
|
+
if (!existsSync(workspaceYamlPath)) return []
|
|
688
|
+
|
|
689
|
+
const yaml = readFileSync(workspaceYamlPath, 'utf-8')
|
|
690
|
+
const paths: string[] = []
|
|
691
|
+
for (const line of yaml.split('\n')) {
|
|
692
|
+
const match = line.match(/^\s*-\s*["']?([^"'\s]+)["']?/)
|
|
693
|
+
if (match?.[1] !== undefined && match[1] !== '.') {
|
|
694
|
+
const pattern = match[1]
|
|
695
|
+
if (pattern.endsWith('/*')) {
|
|
696
|
+
const base = pattern.slice(0, -2)
|
|
697
|
+
const baseDir = join(cwd, base)
|
|
698
|
+
if (existsSync(baseDir)) {
|
|
699
|
+
try {
|
|
700
|
+
const entries = readdirSync(baseDir, { withFileTypes: true })
|
|
701
|
+
for (const entry of entries) {
|
|
702
|
+
if (entry.isDirectory()) {
|
|
703
|
+
paths.push(join(base, entry.name))
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
} catch {
|
|
707
|
+
// skip
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
paths.push(pattern)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return paths
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function runStep(name: string, cmd: string, cwd: string): boolean {
|
|
720
|
+
info(name)
|
|
721
|
+
try {
|
|
722
|
+
execSync(cmd, { cwd, stdio: 'pipe' })
|
|
723
|
+
success(name)
|
|
724
|
+
return true
|
|
725
|
+
} catch (err: unknown) {
|
|
726
|
+
logError(`${name} failed`)
|
|
727
|
+
if (typeof err === 'object' && err !== null && 'stdout' in err) {
|
|
728
|
+
process.stderr.write(String((err as { stdout: Buffer }).stdout))
|
|
729
|
+
}
|
|
730
|
+
return false
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Type-check every module serially. Each `tsc` instance can consume 800 MB–1.5
|
|
735
|
+
// GB; spawning them concurrently via `Promise.all` exceeded a 2 GB ceiling on
|
|
736
|
+
// memory-constrained hosts (an OOM kill, not a slow run). Serializing 2–3
|
|
737
|
+
// modules costs negligible wall-clock next to that, and peak memory drops from
|
|
738
|
+
// N×(process size) to 1×. The `tsc` binary is still resolved once rather than
|
|
739
|
+
// re-resolved through `npx` per module (#322).
|
|
740
|
+
async function runTypeCheck(cwd: string, allModules: string[], shard?: Shard): Promise<boolean> {
|
|
741
|
+
const modulePaths = shardModules(allModules, shard)
|
|
742
|
+
if (shard !== undefined && allModules.length > 0) {
|
|
743
|
+
info(`Type check shard ${shardArg(shard)}: ${String(modulePaths.length)} of ${String(allModules.length)} module(s)`)
|
|
744
|
+
if (modulePaths.length === 0) return true
|
|
745
|
+
}
|
|
746
|
+
if (allModules.length === 0) {
|
|
747
|
+
if (shard !== undefined && shard.index !== 1) return true
|
|
748
|
+
if (existsSync(join(cwd, 'tsconfig.json'))) {
|
|
749
|
+
return runStep('Type check', 'npx tsc --noEmit', cwd)
|
|
750
|
+
}
|
|
751
|
+
success('Type check (no tsconfig.json found)')
|
|
752
|
+
return true
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
info('Type check')
|
|
756
|
+
const tscBin = resolveTscBin(cwd)
|
|
757
|
+
|
|
758
|
+
let ok = true
|
|
759
|
+
for (const mod of modulePaths) {
|
|
760
|
+
const modDir = join(cwd, mod)
|
|
761
|
+
// `tsconfig.check.json` resolves cross-package imports from `src/`, so a
|
|
762
|
+
// clean checkout type-checks without a prior build (#270). The synthesized
|
|
763
|
+
// root module '.' has no check config — it has no siblings to resolve — so
|
|
764
|
+
// fall back to its build `tsconfig.json`.
|
|
765
|
+
const checkTsconfig = join(modDir, 'tsconfig.check.json')
|
|
766
|
+
const tsconfig = existsSync(checkTsconfig) ? checkTsconfig : join(modDir, 'tsconfig.json')
|
|
767
|
+
if (!existsSync(tsconfig)) continue
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
await execFileAsync(tscBin, ['--noEmit', '-p', tsconfig], { cwd })
|
|
771
|
+
} catch (err: unknown) {
|
|
772
|
+
logError(`Type check failed: ${mod}`)
|
|
773
|
+
if (typeof err === 'object' && err !== null && 'stdout' in err) {
|
|
774
|
+
process.stderr.write(String((err as { stdout: string | Buffer }).stdout))
|
|
775
|
+
}
|
|
776
|
+
ok = false
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (ok) {
|
|
781
|
+
success(`Type check (${String(modulePaths.length)} module${modulePaths.length === 1 ? '' : 's'})`)
|
|
782
|
+
}
|
|
783
|
+
return ok
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Resolve the `tsc` binary once, the same way `resolveEslintBin` resolves
|
|
787
|
+
// eslint — module manifest first, PATH fallback — so each module's type check
|
|
788
|
+
// spawns the binary directly instead of paying an `npx` resolution per module.
|
|
789
|
+
export function resolveTscBin(cwd: string): string {
|
|
790
|
+
const thisDir = dirname(fileURLToPath(import.meta.url))
|
|
791
|
+
const require = createRequire(import.meta.url)
|
|
792
|
+
try {
|
|
793
|
+
const pkgJsonPath = require.resolve('typescript/package.json', { paths: [cwd, thisDir] })
|
|
794
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as { bin?: string | Record<string, string> }
|
|
795
|
+
const binRel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.['tsc']
|
|
796
|
+
if (binRel !== undefined) return join(dirname(pkgJsonPath), binRel)
|
|
797
|
+
} catch {
|
|
798
|
+
// fall through to PATH lookup
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
const found = execSync('which tsc', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
803
|
+
return isAbsolute(found) ? found : resolve(cwd, found)
|
|
804
|
+
} catch {
|
|
805
|
+
throw new Error('tsc not found — typescript should be installed as a dependency of pipework')
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export function resolveEslintBin(cwd: string): string {
|
|
810
|
+
// The resolved path is later handed to execSync with `cwd` set to each
|
|
811
|
+
// per-module directory, so it MUST be absolute — a relative path resolves
|
|
812
|
+
// against the wrong directory and fails to spawn.
|
|
813
|
+
const thisDir = dirname(fileURLToPath(import.meta.url))
|
|
814
|
+
|
|
815
|
+
// Resolve eslint as a module from the project root (and pipework's own
|
|
816
|
+
// install as a fallback). `./package.json` is in eslint's `exports`, the
|
|
817
|
+
// `./bin/eslint.js` subpath is not — so resolve the manifest, then derive
|
|
818
|
+
// the bin from it. This finds eslint whether pnpm hoisted it to the
|
|
819
|
+
// workspace root or nested it under pipework.
|
|
820
|
+
const require = createRequire(import.meta.url)
|
|
821
|
+
try {
|
|
822
|
+
const pkgJsonPath = require.resolve('eslint/package.json', { paths: [cwd, thisDir] })
|
|
823
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as { bin?: string | Record<string, string> }
|
|
824
|
+
const binRel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.['eslint']
|
|
825
|
+
if (binRel !== undefined) return join(dirname(pkgJsonPath), binRel)
|
|
826
|
+
} catch {
|
|
827
|
+
// fall through to PATH lookup
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
const found = execSync('which eslint', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
832
|
+
// Under pnpm, `which` can return a cwd-relative path (./node_modules/.bin/eslint).
|
|
833
|
+
// Anchor it to cwd so it survives the cwd change in lintDirectory().
|
|
834
|
+
return isAbsolute(found) ? found : resolve(cwd, found)
|
|
835
|
+
} catch {
|
|
836
|
+
throw new Error('eslint not found — it should be installed as a dependency of pipework')
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// ESLint's `--cache` key is (file content + local eslint.config + eslint version)
|
|
841
|
+
// — it does NOT include the transitive pipework ruleset that the local config
|
|
842
|
+
// imports. So a pipework bump that tightens rules leaves every unchanged file's
|
|
843
|
+
// cached "pass" verdict in place, false-greening pre-existing violations until
|
|
844
|
+
// the file itself changes. Stamping the cache path with pipework's own version
|
|
845
|
+
// (read here from pipework's package.json) makes a bump point eslint at a fresh
|
|
846
|
+
// cache file, busting the stale verdicts. #781-followup
|
|
847
|
+
function pipeworkVersion(): string {
|
|
848
|
+
// dist/cli/commands/check.js → pipework root is three levels up; mirrors the
|
|
849
|
+
// builtin-preset path resolution used in lintDirectory.
|
|
850
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'package.json')
|
|
851
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string }
|
|
852
|
+
return pkg.version ?? '0.0.0'
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Absolute path of the stamped eslint cache for one module. Under the
|
|
856
|
+
// workspace-root `node_modules/.cache` (always gitignored, so the cache is never
|
|
857
|
+
// committed and never travels between clones) and keyed on the pipework version
|
|
858
|
+
// (so a ruleset bump points eslint at a fresh file instead of reusing stale
|
|
859
|
+
// "pass" verdicts) and on `fingerprint`, the resolved tsconfig chain (#409 — the
|
|
860
|
+
// type-aware rules' verdicts depend on it and eslint's own cache key does not).
|
|
861
|
+
// `label` is a module path, so `/` and `\` are flattened to `__` — one file per
|
|
862
|
+
// module, no nested dirs, no traversal out of the cache root.
|
|
863
|
+
export function eslintCacheLocation(cwd: string, version: string, label: string, fingerprint?: string): string {
|
|
864
|
+
const stamp = fingerprint === undefined ? version : `${version}-${fingerprint}`
|
|
865
|
+
return join(cwd, 'node_modules', '.cache', 'pipework', `eslint-${stamp}-${label.replace(/[/\\]/g, '__')}`)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* A hash of the tsconfigs the typescript project service will resolve for `targets`.
|
|
870
|
+
*
|
|
871
|
+
* The type-aware rules produce their verdict from the program that tsconfig defines,
|
|
872
|
+
* and a parse error is a verdict about the tsconfig alone. eslint's `--cache` key is
|
|
873
|
+
* (content + eslint config + eslint version) — none of that moves when a tsconfig
|
|
874
|
+
* does, so a cached verdict survives the checkout that changed it (#409). Stamping
|
|
875
|
+
* the chain into the cache path makes any change point eslint at a fresh file.
|
|
876
|
+
*
|
|
877
|
+
* Covered: the nearest `tsconfig.json` above each target, up to `dir`, and every file
|
|
878
|
+
* its `extends` reaches. Not covered: `references`, and a tsconfig outside `dir`.
|
|
879
|
+
*/
|
|
880
|
+
export function tsconfigFingerprint(dir: string, targets: readonly string[]): string {
|
|
881
|
+
const seen = new Map<string, string>()
|
|
882
|
+
for (const target of targets) {
|
|
883
|
+
const start = nearestTsconfig(dir, globRoot(target))
|
|
884
|
+
if (start !== undefined) collectTsconfigChain(start, seen)
|
|
885
|
+
}
|
|
886
|
+
const hash = createHash('sha256')
|
|
887
|
+
for (const path of [...seen.keys()].sort()) hash.update(path).update('\0').update(seen.get(path) ?? '').update('\0')
|
|
888
|
+
return hash.digest('hex').slice(0, 16)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// The tsconfig the project service picks for a file: the nearest one at or above it,
|
|
892
|
+
// stopping at `dir` — the lint root, past which the service is not looking either.
|
|
893
|
+
function nearestTsconfig(dir: string, relativeRoot: string): string | undefined {
|
|
894
|
+
let current = relativeRoot === '' ? dir : join(dir, relativeRoot)
|
|
895
|
+
for (;;) {
|
|
896
|
+
const candidate = join(current, 'tsconfig.json')
|
|
897
|
+
if (existsSync(candidate)) return candidate
|
|
898
|
+
if (resolve(current) === resolve(dir)) return undefined
|
|
899
|
+
const parent = dirname(current)
|
|
900
|
+
if (parent === current) return undefined
|
|
901
|
+
current = parent
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function collectTsconfigChain(path: string, seen: Map<string, string>): void {
|
|
906
|
+
const key = resolve(path)
|
|
907
|
+
if (seen.has(key)) return
|
|
908
|
+
let raw: string
|
|
909
|
+
try {
|
|
910
|
+
raw = readFileSync(key, 'utf-8')
|
|
911
|
+
} catch {
|
|
912
|
+
return
|
|
913
|
+
}
|
|
914
|
+
seen.set(key, raw)
|
|
915
|
+
|
|
916
|
+
let extendsField: unknown
|
|
917
|
+
try {
|
|
918
|
+
extendsField = (JSON.parse(stripJsonComments(raw)) as { extends?: unknown }).extends
|
|
919
|
+
} catch {
|
|
920
|
+
// An unparseable tsconfig is itself part of the fingerprint — its content is
|
|
921
|
+
// already recorded, and there is no chain to follow.
|
|
922
|
+
return
|
|
923
|
+
}
|
|
924
|
+
const parents = typeof extendsField === 'string' ? [extendsField] : Array.isArray(extendsField) ? extendsField : []
|
|
925
|
+
for (const parent of parents) {
|
|
926
|
+
// Only a relative extends resolves to a file in the checkout; a package
|
|
927
|
+
// specifier lives in node_modules, which the pipework version stamp covers.
|
|
928
|
+
if (typeof parent === 'string' && parent.startsWith('.')) {
|
|
929
|
+
collectTsconfigChain(join(dirname(key), parent.endsWith('.json') ? parent : `${parent}.json`), seen)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// tsconfig.json is JSONC. Enough to reach `extends`; a miss only costs a coarser
|
|
935
|
+
// fingerprint, never a wrong one, because the raw content is hashed regardless.
|
|
936
|
+
function stripJsonComments(raw: string): string {
|
|
937
|
+
return raw.replace(/"(?:[^"\\]|\\.)*"|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, m => (m.startsWith('"') ? m : ''))
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* True when eslint reported a fatal parse error — a file it could not read at all.
|
|
942
|
+
*
|
|
943
|
+
* Distinct from a rule failure: it says the program the tsconfig defines could not be
|
|
944
|
+
* built, so it is never a fact about the file's content alone, and must not outlive
|
|
945
|
+
* the state that produced it in a cache eslint will happily reuse (#409).
|
|
946
|
+
*/
|
|
947
|
+
export function hasFatalParseError(stdout: string): boolean {
|
|
948
|
+
let results: unknown
|
|
949
|
+
try {
|
|
950
|
+
results = JSON.parse(stdout)
|
|
951
|
+
} catch {
|
|
952
|
+
return false
|
|
953
|
+
}
|
|
954
|
+
if (!Array.isArray(results)) return false
|
|
955
|
+
return results.some(r => {
|
|
956
|
+
const messages = (r as { messages?: unknown }).messages
|
|
957
|
+
return Array.isArray(messages) && messages.some(m => (m as { fatal?: unknown }).fatal === true)
|
|
958
|
+
})
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function hasEslintConfig(dir: string): boolean {
|
|
962
|
+
return existsSync(join(dir, 'eslint.config.js'))
|
|
963
|
+
|| existsSync(join(dir, 'eslint.config.mjs'))
|
|
964
|
+
|| existsSync(join(dir, 'eslint.config.ts'))
|
|
965
|
+
|| existsSync(join(dir, 'eslint.config.mts'))
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* The globs eslint is pointed at for `dir`, relative to `dir`.
|
|
970
|
+
*
|
|
971
|
+
* `configured` is the module's `lint.include` (or the global one). Unset, the globs
|
|
972
|
+
* cover `src` plus the `src` of each workspace package under `dir` — a nested pnpm
|
|
973
|
+
* workspace keeps its sources one level further down, and `eslint src` never sees
|
|
974
|
+
* them (#393).
|
|
975
|
+
*/
|
|
976
|
+
export function lintTargets(dir: string, configured?: readonly string[]): string[] {
|
|
977
|
+
return lintIncludeGlobs(dir, configured).filter(glob => {
|
|
978
|
+
const root = globRoot(glob)
|
|
979
|
+
return root === '' || existsSync(join(dir, root))
|
|
980
|
+
})
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
interface EslintFileResult {
|
|
984
|
+
readonly filePath: string
|
|
985
|
+
readonly messages?: ReadonlyArray<{ readonly message?: string; readonly severity?: number; readonly ruleId?: string | null }>
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* The files eslint actually linted, and any it refused, from `--format json` output.
|
|
990
|
+
*
|
|
991
|
+
* A file eslint declines to lint still appears in the report, carrying a single
|
|
992
|
+
* message with no rule — "File ignored because no matching configuration was
|
|
993
|
+
* supplied", or because it sits outside the base path. That is a silent skip, not a
|
|
994
|
+
* pass, so `check` treats it as a failure rather than counting the file green.
|
|
995
|
+
*/
|
|
996
|
+
export function readEslintReport(stdout: string): { linted: string[]; ignored: Array<{ file: string; reason: string }> } {
|
|
997
|
+
let results: EslintFileResult[]
|
|
998
|
+
try {
|
|
999
|
+
results = JSON.parse(stdout) as EslintFileResult[]
|
|
1000
|
+
} catch {
|
|
1001
|
+
return { linted: [], ignored: [] }
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const linted: string[] = []
|
|
1005
|
+
const ignored: Array<{ file: string; reason: string }> = []
|
|
1006
|
+
for (const result of results) {
|
|
1007
|
+
// eslint reports a skip as its only message, with no rule and warning severity —
|
|
1008
|
+
// narrow on all three so a rule whose text happens to say "ignored" is not mistaken
|
|
1009
|
+
// for one.
|
|
1010
|
+
const skip = result.messages?.length === 1
|
|
1011
|
+
? result.messages.find(m =>
|
|
1012
|
+
(m.ruleId === null || m.ruleId === undefined)
|
|
1013
|
+
&& m.severity === 1
|
|
1014
|
+
&& m.message !== undefined
|
|
1015
|
+
&& /^File ignored\b/i.test(m.message))
|
|
1016
|
+
: undefined
|
|
1017
|
+
if (skip !== undefined) {
|
|
1018
|
+
ignored.push({ file: result.filePath, reason: skip.message ?? 'ignored' })
|
|
1019
|
+
} else {
|
|
1020
|
+
linted.push(result.filePath)
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return { linted, ignored }
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* `--config <preset>` for a directory with no eslint config of its own, else nothing.
|
|
1028
|
+
* `check` and `pipework lint` both use it, so they run the same resolved config.
|
|
1029
|
+
*/
|
|
1030
|
+
export function builtinConfigArgs(dir: string): string[] {
|
|
1031
|
+
if (hasEslintConfig(dir)) return []
|
|
1032
|
+
return ['--config', join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'lint', 'preset.js')]
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
export async function lintDirectory(
|
|
1036
|
+
eslintBin: string,
|
|
1037
|
+
dir: string,
|
|
1038
|
+
label: string,
|
|
1039
|
+
cacheLocation: string | ((fingerprint: string) => string),
|
|
1040
|
+
configured?: readonly string[],
|
|
1041
|
+
): Promise<boolean> {
|
|
1042
|
+
const globs = lintIncludeGlobs(dir, configured)
|
|
1043
|
+
const targets = lintTargets(dir, configured)
|
|
1044
|
+
// #392: a module whose globs reach nothing on disk used to report Lint ✓. A check
|
|
1045
|
+
// that ran over nothing is not a check that passed.
|
|
1046
|
+
if (targets.length === 0) return refuseEmptyLint(label, globs, 'none of these paths exist')
|
|
1047
|
+
|
|
1048
|
+
// The cache file is chosen only once the targets are known, because the tsconfig
|
|
1049
|
+
// chain they resolve is part of its key (#409).
|
|
1050
|
+
const cacheFile = typeof cacheLocation === 'string'
|
|
1051
|
+
? cacheLocation
|
|
1052
|
+
: cacheLocation(tsconfigFingerprint(dir, targets))
|
|
1053
|
+
|
|
1054
|
+
// `--cache` lets ESLint skip files unchanged since the last run — without it
|
|
1055
|
+
// every `check` rebuilds the whole program from scratch. #322
|
|
1056
|
+
// `--cache-location` is version-stamped and lives under node_modules/.cache
|
|
1057
|
+
// (computed in runLint) so a pipework ruleset bump busts the cache and a stale
|
|
1058
|
+
// cache never gets committed or travels between clones. See pipeworkVersion.
|
|
1059
|
+
// `--format json` so the pass can name what it linted and refuse a file eslint
|
|
1060
|
+
// reported as ignored — the run's own record of coverage, not an exit code alone.
|
|
1061
|
+
// `--no-error-on-unmatched-pattern`: a glob that reaches nothing is refused here, by
|
|
1062
|
+
// refuseEmptyLint, which can say what to change. eslint's own error for it names the
|
|
1063
|
+
// pattern and stops — it cannot mention `lint.include`.
|
|
1064
|
+
const args = [
|
|
1065
|
+
...targets,
|
|
1066
|
+
'--max-warnings', '0',
|
|
1067
|
+
'--format', 'json',
|
|
1068
|
+
'--no-error-on-unmatched-pattern',
|
|
1069
|
+
'--cache', '--cache-location', cacheFile,
|
|
1070
|
+
]
|
|
1071
|
+
args.push(...builtinConfigArgs(dir))
|
|
1072
|
+
|
|
1073
|
+
let stdout: string
|
|
1074
|
+
try {
|
|
1075
|
+
stdout = (await execFileAsync(eslintBin, args, { cwd: dir })).stdout
|
|
1076
|
+
} catch (err: unknown) {
|
|
1077
|
+
logError(`Lint failed: ${label}`)
|
|
1078
|
+
const failureStdout = typeof err === 'object' && err !== null && 'stdout' in err
|
|
1079
|
+
? String((err as { stdout: string | Buffer }).stdout)
|
|
1080
|
+
: ''
|
|
1081
|
+
if (failureStdout !== '') writeEslintFailures(failureStdout)
|
|
1082
|
+
// A parse error is a verdict about the program the tsconfig defines, not about
|
|
1083
|
+
// the file's content — so it must not be left in a cache eslint will reuse after
|
|
1084
|
+
// a checkout fixes the tsconfig. The stamp above already separates the two
|
|
1085
|
+
// states; dropping the file makes a stale red impossible rather than unlikely.
|
|
1086
|
+
if (hasFatalParseError(failureStdout)) rmSync(cacheFile, { force: true })
|
|
1087
|
+
// A spawn failure (eslint not found, bad path) reports on stderr, not
|
|
1088
|
+
// stdout — surface it so the real error isn't hidden behind "Lint failed".
|
|
1089
|
+
const stderr = typeof err === 'object' && err !== null && 'stderr' in err
|
|
1090
|
+
? String((err as { stderr: string | Buffer }).stderr)
|
|
1091
|
+
: ''
|
|
1092
|
+
process.stderr.write(stderr)
|
|
1093
|
+
// eslint's own wording for "you pointed me at a directory I have no config for".
|
|
1094
|
+
// Say which pipework-side cause produces it, since the fix is not in eslint's config.
|
|
1095
|
+
if (/are ignored/.test(stderr)) {
|
|
1096
|
+
process.stderr.write(
|
|
1097
|
+
' A lint target with no matching `files` glob. Run `pipework install` to regenerate\n' +
|
|
1098
|
+
' eslint.config.js — an older one carries a single `src` glob and cannot reach a\n' +
|
|
1099
|
+
' nested workspace package (#393).\n',
|
|
1100
|
+
)
|
|
1101
|
+
}
|
|
1102
|
+
return false
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return reportLintCoverage(eslintBin, dir, label, globs, targets, stdout)
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Refuse when a source file in scope was not linted.
|
|
1110
|
+
*
|
|
1111
|
+
* `check` enumerates the same sources doctrine walks, so a file present on disk and
|
|
1112
|
+
* absent from eslint's report is a file that matched no config block — the #393
|
|
1113
|
+
* failure, which until now read as Lint ✓. eslint is asked for its own reason before
|
|
1114
|
+
* the run is failed: a file left out by a deliberate `ignores` pattern is a choice and
|
|
1115
|
+
* is reported, not refused; "no matching configuration" and an outside-base-path
|
|
1116
|
+
* refusal are the bug and fail the run.
|
|
1117
|
+
*/
|
|
1118
|
+
async function reportLintCoverage(
|
|
1119
|
+
eslintBin: string,
|
|
1120
|
+
dir: string,
|
|
1121
|
+
label: string,
|
|
1122
|
+
globs: readonly string[],
|
|
1123
|
+
targets: string[],
|
|
1124
|
+
stdout: string,
|
|
1125
|
+
): Promise<boolean> {
|
|
1126
|
+
const linted = new Set(readEslintReport(stdout).linted.map(f => resolve(dir, f)))
|
|
1127
|
+
const inScope = filesMatchingGlobs(dir, globs)
|
|
1128
|
+
const missed = inScope.filter(f => !linted.has(resolve(dir, f)))
|
|
1129
|
+
const reportOutside = (): void => reportOutsideGlobs(dir, label, globs, inScope)
|
|
1130
|
+
|
|
1131
|
+
if (missed.length === 0) {
|
|
1132
|
+
if (linted.size === 0) return refuseEmptyLint(label, globs, 'they matched no file')
|
|
1133
|
+
reportOutside()
|
|
1134
|
+
info(` ${label}: ${String(linted.size)} file${linted.size === 1 ? '' : 's'} linted (${targets.join(', ')})`)
|
|
1135
|
+
return true
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const reasons = await explainUnlinted(eslintBin, dir, missed)
|
|
1139
|
+
const deliberate = reasons.filter(r => /matching ignore pattern/i.test(r.reason))
|
|
1140
|
+
const unconfigured = reasons.filter(r => !/matching ignore pattern/i.test(r.reason))
|
|
1141
|
+
|
|
1142
|
+
if (unconfigured.length === 0) {
|
|
1143
|
+
if (linted.size === 0) return refuseEmptyLint(label, globs, 'every file they matched is ignored')
|
|
1144
|
+
reportOutside()
|
|
1145
|
+
info(` ${label}: ${String(linted.size)} file${linted.size === 1 ? '' : 's'} linted, ${String(deliberate.length)} ignored by pattern (${targets.join(', ')})`)
|
|
1146
|
+
return true
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
logError(`Lint incomplete: ${label} — ${String(unconfigured.length)} file(s) in scope were never linted`)
|
|
1150
|
+
for (const { file, reason } of unconfigured) {
|
|
1151
|
+
process.stderr.write(` ${file} ${reason}\n`)
|
|
1152
|
+
}
|
|
1153
|
+
process.stderr.write(
|
|
1154
|
+
" eslint resolves flat-config `files` globs against eslint.config.js's directory.\n" +
|
|
1155
|
+
' Run `pipework install` to regenerate eslint.config.js — an older one carries a single\n' +
|
|
1156
|
+
' `src` glob and cannot reach a nested workspace package. A green Lint over files that\n' +
|
|
1157
|
+
' were never read is the failure this refuses.\n',
|
|
1158
|
+
)
|
|
1159
|
+
return false
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Sources under the module's src/ that no lint glob reaches. A configured `include` is a
|
|
1163
|
+
// choice, so this is said, not failed (#348).
|
|
1164
|
+
function reportOutsideGlobs(dir: string, label: string, globs: readonly string[], inScope: readonly string[]): void {
|
|
1165
|
+
const covered = new Set(inScope)
|
|
1166
|
+
const outside = findSourceFiles(dir, ['.']).filter(f => !covered.has(f))
|
|
1167
|
+
if (outside.length === 0) return
|
|
1168
|
+
warn(` ${label}: ${String(outside.length)} source file(s) under src/ are outside the lint globs (${globs.join(', ')})`)
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// A module that linted nothing names the globs it tried and why they came up empty —
|
|
1172
|
+
// the alternative is a ✓ that means "there was nothing to look at" (#392).
|
|
1173
|
+
function refuseEmptyLint(label: string, globs: readonly string[], why: string): boolean {
|
|
1174
|
+
logError(`Lint linted no files: ${label} — ${why}`)
|
|
1175
|
+
for (const glob of globs) process.stderr.write(` ${glob}\n`)
|
|
1176
|
+
process.stderr.write(
|
|
1177
|
+
' Point `lint.include` (or `lint.modules["' + label + '"].include`) in pipework.config.ts\n' +
|
|
1178
|
+
' at this module\'s sources, or drop the module from `modules` if it has none. A lint\n' +
|
|
1179
|
+
' pass over zero files is reported as a failure, not a pass.\n',
|
|
1180
|
+
)
|
|
1181
|
+
return false
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* eslint's own reason for leaving each of `files` out, one explicit-path run.
|
|
1186
|
+
*
|
|
1187
|
+
* Passing a directory makes eslint skip an unmatched file in silence; passing the path
|
|
1188
|
+
* makes it say why. Only reached when something was missing, so the extra run costs
|
|
1189
|
+
* nothing on a healthy repo.
|
|
1190
|
+
*/
|
|
1191
|
+
async function explainUnlinted(
|
|
1192
|
+
eslintBin: string,
|
|
1193
|
+
dir: string,
|
|
1194
|
+
files: string[],
|
|
1195
|
+
): Promise<Array<{ file: string; reason: string }>> {
|
|
1196
|
+
const args = [...files, '--format', 'json', '--no-error-on-unmatched-pattern']
|
|
1197
|
+
if (!hasEslintConfig(dir)) {
|
|
1198
|
+
args.push('--config', join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'lint', 'preset.js'))
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
let stdout: string
|
|
1202
|
+
try {
|
|
1203
|
+
stdout = (await execFileAsync(eslintBin, args, { cwd: dir })).stdout
|
|
1204
|
+
} catch (err: unknown) {
|
|
1205
|
+
stdout = typeof err === 'object' && err !== null && 'stdout' in err
|
|
1206
|
+
? String((err as { stdout: string | Buffer }).stdout)
|
|
1207
|
+
: ''
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const { ignored } = readEslintReport(stdout)
|
|
1211
|
+
const byFile = new Map(ignored.map(i => [resolve(dir, i.file), i.reason]))
|
|
1212
|
+
return files.map(file => ({
|
|
1213
|
+
file,
|
|
1214
|
+
reason: byFile.get(resolve(dir, file)) ?? 'matched no configuration block',
|
|
1215
|
+
}))
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// eslint's stdout on a failing run is the JSON report; anything else (a crash before
|
|
1219
|
+
// it formats) is passed through verbatim rather than swallowed.
|
|
1220
|
+
function writeEslintFailures(stdout: string): void {
|
|
1221
|
+
process.stderr.write(stdout.trimStart().startsWith('[') ? formatEslintJson(stdout) : stdout)
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// `--format json` costs the human-readable report, so rebuild the essentials:
|
|
1225
|
+
// file, line:col, rule, message — enough to fix without a second run.
|
|
1226
|
+
function formatEslintJson(stdout: string): string {
|
|
1227
|
+
let results: Array<{ filePath: string; messages?: Array<{ line?: number; column?: number; ruleId?: string | null; message?: string; severity?: number }> }>
|
|
1228
|
+
try {
|
|
1229
|
+
results = JSON.parse(stdout) as typeof results
|
|
1230
|
+
} catch {
|
|
1231
|
+
return stdout
|
|
1232
|
+
}
|
|
1233
|
+
let out = ''
|
|
1234
|
+
for (const result of results) {
|
|
1235
|
+
const messages = result.messages ?? []
|
|
1236
|
+
if (messages.length === 0) continue
|
|
1237
|
+
out += `\n${result.filePath}\n`
|
|
1238
|
+
for (const m of messages) {
|
|
1239
|
+
const where = `${String(m.line ?? 0)}:${String(m.column ?? 0)}`
|
|
1240
|
+
const level = m.severity === 1 ? 'warning' : 'error'
|
|
1241
|
+
out += ` ${where.padEnd(8)} ${level.padEnd(7)} ${m.message ?? ''} ${m.ruleId ?? ''}\n`
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return out + '\n'
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function runDependencyCheck(cwd: string): boolean {
|
|
1248
|
+
info('Dependency boundaries')
|
|
1249
|
+
|
|
1250
|
+
const result = checkDependencies(cwd)
|
|
1251
|
+
|
|
1252
|
+
if (result.packagesChecked === 0) {
|
|
1253
|
+
success('Dependency boundaries (no workspace packages)')
|
|
1254
|
+
return true
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (result.violations.length === 0) {
|
|
1258
|
+
success(`Dependency boundaries (${String(result.packagesChecked)} package${result.packagesChecked === 1 ? '' : 's'})`)
|
|
1259
|
+
return true
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
for (const v of result.violations) {
|
|
1263
|
+
logError(` ${v.package} → ${v.dependency} (${v.field})`)
|
|
1264
|
+
warn(` ${v.message}`)
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
logError(`Dependency boundaries: ${String(result.violations.length)} violation(s)`)
|
|
1268
|
+
return false
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Lint every module serially. Each eslint run with type-aware rules can consume
|
|
1272
|
+
// 300–800 MB, and concurrent spawning compounds with `tsc` to exceed memory
|
|
1273
|
+
// limits on constrained hosts (#322 ran these via `Promise.all`; that is the
|
|
1274
|
+
// OOM this reverts). The wall-clock cost of a serial loop over 2–3 modules is
|
|
1275
|
+
// negligible next to an OOM kill.
|
|
1276
|
+
async function runLint(cwd: string, allModules: string[], shard?: Shard): Promise<boolean> {
|
|
1277
|
+
const modulePaths = shardModules(allModules, shard)
|
|
1278
|
+
if (shard !== undefined && allModules.length > 0) {
|
|
1279
|
+
info(`Lint shard ${shardArg(shard)}: ${String(modulePaths.length)} of ${String(allModules.length)} module(s)`)
|
|
1280
|
+
}
|
|
1281
|
+
if (allModules.length === 0 && shard !== undefined && shard.index !== 1) return true
|
|
1282
|
+
info('Lint')
|
|
1283
|
+
|
|
1284
|
+
const eslintBin = resolveEslintBin(cwd)
|
|
1285
|
+
const includes = await loadLintIncludes(cwd)
|
|
1286
|
+
|
|
1287
|
+
// Per-module eslint cache, version-stamped and under the workspace-root
|
|
1288
|
+
// node_modules/.cache — always present, always gitignored. The version stamp
|
|
1289
|
+
// busts the cache on a pipework ruleset bump; the node_modules location keeps
|
|
1290
|
+
// it out of git so a stale cache never travels between clones. See lintDirectory.
|
|
1291
|
+
const version = pipeworkVersion()
|
|
1292
|
+
mkdirSync(join(cwd, 'node_modules', '.cache', 'pipework'), { recursive: true })
|
|
1293
|
+
const cacheFor = (label: string) => (fingerprint: string): string =>
|
|
1294
|
+
eslintCacheLocation(cwd, version, label, fingerprint)
|
|
1295
|
+
|
|
1296
|
+
if (allModules.length === 0) {
|
|
1297
|
+
const ok = await lintDirectory(eslintBin, cwd, '.', cacheFor('root'), includes.for('.'))
|
|
1298
|
+
if (ok) success('Lint')
|
|
1299
|
+
return ok
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
let ok = true
|
|
1303
|
+
for (const mod of modulePaths) {
|
|
1304
|
+
if (!await lintDirectory(eslintBin, join(cwd, mod), mod, cacheFor(mod), includes.for(mod))) {
|
|
1305
|
+
ok = false
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// #402: the module loop never reaches a lintable file sitting at the workspace
|
|
1310
|
+
// root, outside every module. Those files were missing from the files-linted
|
|
1311
|
+
// count entirely — a coverage reading produced from a population that excluded
|
|
1312
|
+
// them. They get their own pass, counted and named like any module's.
|
|
1313
|
+
const rootOnly = shard !== undefined && shard.index !== 1
|
|
1314
|
+
? []
|
|
1315
|
+
: rootFilesOutsideModules(cwd, allModules, includes.for('.'))
|
|
1316
|
+
if (rootOnly.length > 0) {
|
|
1317
|
+
if (!await lintDirectory(eslintBin, cwd, 'root', cacheFor('__root'), rootOnly)) {
|
|
1318
|
+
ok = false
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
if (ok) {
|
|
1323
|
+
const modules = `${String(modulePaths.length)} module${modulePaths.length === 1 ? '' : 's'}`
|
|
1324
|
+
const root = rootOnly.length > 0
|
|
1325
|
+
? ` + ${String(rootOnly.length)} root file${rootOnly.length === 1 ? '' : 's'}`
|
|
1326
|
+
: ''
|
|
1327
|
+
success(`Lint (${modules}${root})`)
|
|
1328
|
+
}
|
|
1329
|
+
return ok
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Root-level lintable files that no module covers (#402).
|
|
1334
|
+
*
|
|
1335
|
+
* `lint.include` for `.` — else the default `src` globs — minus everything under a
|
|
1336
|
+
* module path, which the module loop already linted. What remains is the population
|
|
1337
|
+
* `check` used to count as if it did not exist.
|
|
1338
|
+
*/
|
|
1339
|
+
export function rootFilesOutsideModules(
|
|
1340
|
+
cwd: string,
|
|
1341
|
+
modulePaths: readonly string[],
|
|
1342
|
+
configured?: readonly string[],
|
|
1343
|
+
): string[] {
|
|
1344
|
+
const owners = modulePaths.map(normalizeModulePath)
|
|
1345
|
+
// A module at the workspace root owns every root file. Its own pass already lints
|
|
1346
|
+
// them, so a root pass here would lint and count each of them a second time (#408).
|
|
1347
|
+
if (owners.includes('')) return []
|
|
1348
|
+
|
|
1349
|
+
const files = filesMatchingGlobs(cwd, lintIncludeGlobs(cwd, configured))
|
|
1350
|
+
return files.filter(f => !owners.some(m => f === m || f.startsWith(m + '/')))
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// `.`, `./` and `` all name the workspace root, and `./pkg` names the same module as
|
|
1354
|
+
// `pkg`. Compared as raw strings they did not, so a module at `.` excluded nothing.
|
|
1355
|
+
function normalizeModulePath(path: string): string {
|
|
1356
|
+
return path.replace(/^\.\/+/, '').replace(/\/+$/, '').replace(/^\.$/, '')
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* `lint.include` from pipework.config.ts, global and per module (#392).
|
|
1361
|
+
*
|
|
1362
|
+
* A module entry replaces the global one; both replace the `src`-derived default. A
|
|
1363
|
+
* config that cannot be loaded yields no includes — the load failure is already
|
|
1364
|
+
* reported by `check` itself, and defaulting here would hide it behind a lint error.
|
|
1365
|
+
*/
|
|
1366
|
+
export async function loadLintIncludes(cwd: string): Promise<{ for: (mod: string) => readonly string[] | undefined }> {
|
|
1367
|
+
let lint: LintConfig | undefined
|
|
1368
|
+
try {
|
|
1369
|
+
lint = (await discoverInstanceCached(cwd)).config.raw.lint
|
|
1370
|
+
} catch {
|
|
1371
|
+
lint = undefined
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// Glob keys expand the same way resolveLintConfig expands them, so `check` and the
|
|
1375
|
+
// eslint config agree on which module an entry belongs to.
|
|
1376
|
+
const entries: Array<{ path: string; include?: readonly string[] }> = []
|
|
1377
|
+
for (const [key, config] of Object.entries(lint?.modules ?? {})) {
|
|
1378
|
+
const paths = key.includes('*') ? expandGlob(key, cwd) : [key]
|
|
1379
|
+
for (const path of paths) entries.push({ path, ...(config.include !== undefined ? { include: config.include } : {}) })
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
return {
|
|
1383
|
+
for: (mod: string): readonly string[] | undefined => {
|
|
1384
|
+
// Deepest match wins; entries.sort is by key length, so walk it from the back.
|
|
1385
|
+
const matching = entries
|
|
1386
|
+
.filter(e => mod === e.path || mod.startsWith(e.path + '/'))
|
|
1387
|
+
.sort((a, b) => a.path.length - b.path.length)
|
|
1388
|
+
for (let i = matching.length - 1; i >= 0; i--) {
|
|
1389
|
+
const include = matching[i]!.include
|
|
1390
|
+
if (include !== undefined) return include
|
|
1391
|
+
}
|
|
1392
|
+
return lint?.include
|
|
1393
|
+
},
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function runPipeworkCopiesCheck(cwd: string): boolean {
|
|
1398
|
+
info('pipework install integrity')
|
|
1399
|
+
const { copies, orphans } = detectPipeworkInstall(cwd)
|
|
1400
|
+
const versions = distinctPipeworkVersions(copies)
|
|
1401
|
+
for (const o of orphans) {
|
|
1402
|
+
warn(`Orphaned pipework ${o.version} at ${o.path}: a stale copy pnpm left in its virtual store; \`pnpm prune\` removes it.`)
|
|
1403
|
+
}
|
|
1404
|
+
if (versions.length <= 1) {
|
|
1405
|
+
success(`pipework install integrity (${String(copies.length)} cop${copies.length === 1 ? 'y' : 'ies'}, ${versions.length === 1 ? 'one version' : 'none resolved'})`)
|
|
1406
|
+
return true
|
|
1407
|
+
}
|
|
1408
|
+
logError(`Multiple pipework versions resolved in one install: ${versions.join(', ')}.`)
|
|
1409
|
+
for (const c of copies) {
|
|
1410
|
+
process.stderr.write(` ${c.version} ${c.path}\n`)
|
|
1411
|
+
}
|
|
1412
|
+
process.stderr.write(
|
|
1413
|
+
' A shared library that imports pipework must resolve the SAME version as its consuming\n' +
|
|
1414
|
+
' workspace; otherwise their AsyncLocalStorage and error classes fork (no active context;\n' +
|
|
1415
|
+
' 403->500). Align every pipework copy to one version.\n',
|
|
1416
|
+
)
|
|
1417
|
+
return false
|
|
1418
|
+
}
|