@mandujs/core 0.54.31 → 0.55.0-beta.1
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/README.md +654 -654
- package/package.json +106 -210
- package/scripts/postinstall-lock.ts +153 -153
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +15 -15
- package/src/a11y/types.ts +125 -125
- package/src/agent/__tests__/apply.test.ts +224 -0
- package/src/agent/__tests__/context.test.ts +97 -98
- package/src/agent/apply.ts +1031 -0
- package/src/agent/context.ts +552 -552
- package/src/agent/index.ts +4 -3
- package/src/agent/plan.ts +161 -243
- package/src/agent/repair.ts +239 -162
- package/src/agent/sync.ts +197 -198
- package/src/agent/types.ts +242 -94
- package/src/agent/verify.ts +55 -55
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +15 -5
- package/src/auth/__tests__/reset.test.ts +2 -2
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +14 -2
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__snapshots__/build.test.ts.snap +5 -5
- package/src/bundler/__tests__/build-runner.ts +166 -166
- package/src/bundler/__tests__/client-boundary-transform.test.ts +524 -524
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +49 -20
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +24 -24
- package/src/bundler/__tests__/generation.test.ts +447 -0
- package/src/bundler/__tests__/hdr.test.ts +24 -18
- package/src/bundler/__tests__/hmr-client.test.ts +62 -24
- package/src/bundler/__tests__/jsx-runtime-shim.test.ts +179 -140
- package/src/bundler/__tests__/manifest-schema.test.ts +305 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +42 -42
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/analyzer.ts +15 -15
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +898 -915
- package/src/bundler/build.ts +113 -24
- package/src/bundler/client-boundary-transform.ts +977 -977
- package/src/bundler/css.ts +65 -44
- package/src/bundler/dev.ts +105 -73
- package/src/bundler/fast-refresh-preamble.ts +47 -47
- package/src/bundler/generation.ts +602 -0
- package/src/bundler/index.ts +3 -3
- package/src/bundler/manifest-schema.ts +55 -40
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +16 -15
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/prerender.ts +29 -9
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +1 -1
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +58 -56
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/props-serialization.test.ts +37 -37
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +2 -2
- package/src/client/index.ts +1 -1
- package/src/client/island.ts +79 -79
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/props-serialization.ts +233 -233
- package/src/client/runtime-entry.ts +598 -598
- package/src/client/runtime.ts +1 -1
- package/src/client/serialize.ts +50 -50
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image-compat.ts +3 -0
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +1 -1
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +9 -9
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/generate-types.ts +1 -1
- package/src/content/index.ts +2 -2
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +9 -9
- package/src/db/index.ts +2 -2
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +82 -82
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/smoke.test.ts +2 -2
- package/src/desktop/__tests__/webview-fallback.test.ts +12 -32
- package/src/desktop/__tests__/window.test.ts +20 -99
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/webview-fallback.ts +17 -4
- package/src/desktop/window.ts +17 -8
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/diagnose/__tests__/checks.test.ts +136 -136
- package/src/diagnose/checks.ts +185 -185
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +10 -10
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +5 -1
- package/src/filling/auth.ts +308 -308
- package/src/filling/body-parse.test.ts +60 -60
- package/src/filling/cookie-codec.ts +5 -3
- package/src/filling/deps.ts +265 -265
- package/src/filling/head-method.test.ts +154 -154
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.test.ts +48 -48
- package/src/generator/templates.ts +219 -219
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/check.ts +39 -24
- package/src/guard/config-guard.ts +13 -13
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/fs-routes-policy.ts +51 -51
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/index.ts +11 -11
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/__tests__/id.test.ts +3 -3
- package/src/id/index.ts +105 -105
- package/src/index.ts +9 -48
- package/src/internal/client-boundary.ts +266 -266
- package/src/internal/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -779
- package/src/kitchen/api/bundle-inspector.ts +179 -0
- package/src/kitchen/api/errors-grouping.ts +126 -126
- package/src/kitchen/api/file-api.ts +11 -11
- package/src/kitchen/kitchen-handler.ts +36 -0
- package/src/kitchen/kitchen-ui.ts +456 -0
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/perf/user-marks.ts +1 -1
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +7 -7
- package/src/resource/__tests__/schema.test.ts +14 -14
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +165 -165
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +146 -146
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/generator-schema.ts +11 -11
- package/src/resource/generators/slot.ts +72 -72
- package/src/resource/schema.ts +21 -21
- package/src/router/client-entry.test.ts +227 -227
- package/src/router/client-entry.ts +218 -218
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.test.ts +532 -532
- package/src/router/fs-routes.ts +53 -53
- package/src/router/fs-scanner.ts +216 -216
- package/src/router/fs-types.ts +19 -19
- package/src/router/route-source-analyzer.ts +521 -521
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/inline-client-hydration.test.ts +237 -237
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
- package/src/runtime/__tests__/page-render-response.test.ts +517 -517
- package/src/runtime/__tests__/request-middleware.test.ts +70 -70
- package/src/runtime/__tests__/searchparams-page-props.test.ts +81 -81
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/devtools-adapter.ts +68 -68
- package/src/runtime/escape.ts +34 -34
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/fast-refresh-types.ts +1 -1
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +30 -11
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/observability-lifecycle.ts +290 -290
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/page-render-response.ts +287 -274
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/rate-limit.ts +1 -1
- package/src/runtime/redirect.ts +1 -1
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/router.test.ts +4 -4
- package/src/runtime/router.ts +10 -10
- package/src/runtime/server.ts +39 -20
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/ssr.ts +43 -17
- package/src/runtime/static-files.ts +369 -289
- package/src/runtime/streaming-ssr.ts +219 -180
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/__tests__/scheduler.test.ts +12 -11
- package/src/scheduler/index.ts +11 -5
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/spec/schema.ts +35 -35
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/index.ts +9 -3
- package/src/testing/lcov.ts +192 -0
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/__tests__/watcher.test.ts +59 -59
- package/src/watcher/watcher.ts +61 -61
|
@@ -1,512 +1,512 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Guard — oxlint type-aware bridge.
|
|
3
|
-
*
|
|
4
|
-
* Spawns `oxlint --type-aware` as a one-shot child process, translates the
|
|
5
|
-
* JSON diagnostic stream into Mandu's `Violation` contract, and returns a
|
|
6
|
-
* merged `{ violations, summary }` envelope that the `guard` runner /
|
|
7
|
-
* MCP surface can slot in next to the architecture-layer results.
|
|
8
|
-
*
|
|
9
|
-
* ## Contract
|
|
10
|
-
*
|
|
11
|
-
* - Bridge never throws for "oxlint not installed" — returns
|
|
12
|
-
* `{ skipped: "oxlint-not-installed" }` so the architecture check keeps
|
|
13
|
-
* running. The bridge DOES throw on malformed JSON output or spawn
|
|
14
|
-
* errors, because those are operator-visible bugs (e.g. a stale
|
|
15
|
-
* oxlint binary) that should fail loudly rather than silently.
|
|
16
|
-
* - Child process is invoked with `cwd: projectRoot`, `stdout: "pipe"`,
|
|
17
|
-
* `stderr: "pipe"`, and a 60-second timeout (configurable via the
|
|
18
|
-
* `MANDU_TSGOLINT_TIMEOUT_MS` env override for slow CI agents).
|
|
19
|
-
* - Exit code is NOT treated as authoritative — oxlint exits non-zero
|
|
20
|
-
* when ANY diagnostic is emitted, even warnings. We always parse
|
|
21
|
-
* stdout; stderr is captured for diagnostics but does not override the
|
|
22
|
-
* JSON body.
|
|
23
|
-
*
|
|
24
|
-
* ## oxlint JSON shape (oxlint >= 1.61)
|
|
25
|
-
*
|
|
26
|
-
* ```json
|
|
27
|
-
* {
|
|
28
|
-
* "diagnostics": [
|
|
29
|
-
* {
|
|
30
|
-
* "message": "Unexpected `any`. Specify a different type.",
|
|
31
|
-
* "code": "typescript-eslint(no-explicit-any)",
|
|
32
|
-
* "severity": "error" | "warning" | "advice",
|
|
33
|
-
* "filename": "src/foo.ts",
|
|
34
|
-
* "labels": [{ "span": { "offset": N, "length": N, "line": N, "column": N } }],
|
|
35
|
-
* "help": "...",
|
|
36
|
-
* "url": "https://oxc.rs/...",
|
|
37
|
-
* "causes": [],
|
|
38
|
-
* "related": []
|
|
39
|
-
* }
|
|
40
|
-
* ],
|
|
41
|
-
* "number_of_files": N,
|
|
42
|
-
* "number_of_rules": N,
|
|
43
|
-
* "start_time": N
|
|
44
|
-
* }
|
|
45
|
-
* ```
|
|
46
|
-
*
|
|
47
|
-
* @module guard/tsgolint-bridge
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
import path from "path";
|
|
51
|
-
import type { Severity, Violation } from "./types";
|
|
52
|
-
|
|
53
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
54
|
-
// Public types
|
|
55
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Bridge invocation options.
|
|
59
|
-
*
|
|
60
|
-
* All fields optional — the bridge's out-of-the-box behavior is
|
|
61
|
-
* `projectRoot = process.cwd()` and everything else picked up from
|
|
62
|
-
* `.oxlintrc.json` at the project root.
|
|
63
|
-
*/
|
|
64
|
-
export interface TsgolintBridgeOptions {
|
|
65
|
-
/**
|
|
66
|
-
* Absolute path to the project root — the cwd the child process runs
|
|
67
|
-
* in AND the directory the binary is resolved under (`<root>/node_modules/.bin/oxlint`).
|
|
68
|
-
*/
|
|
69
|
-
projectRoot: string;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Explicit rule allowlist. When present, only these rules are reported;
|
|
73
|
-
* everything else is filtered out of the bridge's returned violations
|
|
74
|
-
* (oxlint itself still evaluates them, but we drop the non-matching
|
|
75
|
-
* diagnostics — trading a small post-filter cost for a simpler
|
|
76
|
-
* propagation path that avoids CLI rule-flag plumbing).
|
|
77
|
-
*
|
|
78
|
-
* Rule ids follow oxlint's `<plugin>/<rule-name>` convention
|
|
79
|
-
* (`typescript/no-floating-promises`, `no-debugger`).
|
|
80
|
-
*
|
|
81
|
-
* Undefined / empty = "trust oxlint defaults" (respect the user's
|
|
82
|
-
* `.oxlintrc.json` allowlist, emit every reported diagnostic).
|
|
83
|
-
*/
|
|
84
|
-
rules?: string[];
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Override every diagnostic's severity to this value. Undefined =
|
|
88
|
-
* "respect oxlint's per-rule severity". Useful when a project wants
|
|
89
|
-
* type-aware hits to always be warnings (soft-landing period) or
|
|
90
|
-
* always errors (CI gate).
|
|
91
|
-
*/
|
|
92
|
-
severity?: Severity | "off";
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Custom oxlintrc path. Falls through to oxlint's default project-root
|
|
96
|
-
* lookup (`.oxlintrc.json` / `.oxlintrc.jsonc`) when unset.
|
|
97
|
-
*/
|
|
98
|
-
configPath?: string;
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Paths to lint — file or directory globs. Defaults to the project root,
|
|
102
|
-
* letting oxlint's own traversal decide which files to walk.
|
|
103
|
-
*/
|
|
104
|
-
paths?: string[];
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Wall-clock timeout in milliseconds. Default: 60_000 (60s).
|
|
108
|
-
* `MANDU_TSGOLINT_TIMEOUT_MS` env override wins when the caller
|
|
109
|
-
* leaves this undefined.
|
|
110
|
-
*/
|
|
111
|
-
timeoutMs?: number;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Bridge result envelope.
|
|
116
|
-
*
|
|
117
|
-
* Always returns an object — callers that need to branch on "skipped"
|
|
118
|
-
* narrow via the `skipped` discriminator. Shape stays stable whether
|
|
119
|
-
* oxlint was invoked or not so the CLI / MCP renderer has a single
|
|
120
|
-
* code path.
|
|
121
|
-
*/
|
|
122
|
-
export interface TsgolintBridgeResult {
|
|
123
|
-
/** Violations translated into Mandu's contract (empty on skip). */
|
|
124
|
-
violations: Violation[];
|
|
125
|
-
/** Per-run summary metadata. */
|
|
126
|
-
summary: TsgolintBridgeSummary;
|
|
127
|
-
/** When present, oxlint was not invoked and `violations` is empty. */
|
|
128
|
-
skipped?: TsgolintBridgeSkipReason;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export interface TsgolintBridgeSummary {
|
|
132
|
-
/** Rule ids that actually fired (post-filter). Empty on skip. */
|
|
133
|
-
rulesEnabled: string[];
|
|
134
|
-
/** Wall-clock time spent in the child process (ms). 0 on skip. */
|
|
135
|
-
elapsedMs: number;
|
|
136
|
-
/** Total diagnostics seen BEFORE the rule filter. */
|
|
137
|
-
diagnosticsReceived: number;
|
|
138
|
-
/** Number of files oxlint reported walking. 0 on skip. */
|
|
139
|
-
filesAnalyzed: number;
|
|
140
|
-
/** Non-empty stderr, if any (diagnostic hint for operators). */
|
|
141
|
-
stderr?: string;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export type TsgolintBridgeSkipReason =
|
|
145
|
-
| "oxlint-not-installed"
|
|
146
|
-
| "severity-off";
|
|
147
|
-
|
|
148
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
149
|
-
// oxlint JSON shape (internal — not re-exported; the bridge is the contract)
|
|
150
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
151
|
-
|
|
152
|
-
interface OxlintLabelSpan {
|
|
153
|
-
offset: number;
|
|
154
|
-
length: number;
|
|
155
|
-
line: number;
|
|
156
|
-
column: number;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
interface OxlintLabel {
|
|
160
|
-
span: OxlintLabelSpan;
|
|
161
|
-
message?: string;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
interface OxlintDiagnostic {
|
|
165
|
-
message: string;
|
|
166
|
-
/** e.g. `typescript-eslint(no-floating-promises)` or `no-debugger`. */
|
|
167
|
-
code?: string;
|
|
168
|
-
severity?: "error" | "warning" | "advice";
|
|
169
|
-
filename?: string;
|
|
170
|
-
labels?: OxlintLabel[];
|
|
171
|
-
help?: string;
|
|
172
|
-
url?: string;
|
|
173
|
-
causes?: unknown[];
|
|
174
|
-
related?: unknown[];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
interface OxlintJsonOutput {
|
|
178
|
-
diagnostics: OxlintDiagnostic[];
|
|
179
|
-
number_of_files?: number;
|
|
180
|
-
number_of_rules?: number;
|
|
181
|
-
threads_count?: number;
|
|
182
|
-
start_time?: number;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
186
|
-
// Binary resolution
|
|
187
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Resolve the oxlint binary under `<root>/node_modules/.bin/`.
|
|
191
|
-
*
|
|
192
|
-
* Windows: prefer `.exe` then `.cmd`. POSIX: the extensionless shim.
|
|
193
|
-
* Returns `undefined` when no candidate exists so the bridge can
|
|
194
|
-
* emit the `oxlint-not-installed` skip reason without touching the
|
|
195
|
-
* filesystem again.
|
|
196
|
-
*
|
|
197
|
-
* `Bun.file(path).exists()` is async but we stay sync here on purpose —
|
|
198
|
-
* the bridge entry point is already async and this keeps the callsite's
|
|
199
|
-
* control flow linear (`await resolveOxlintBinary(root)`).
|
|
200
|
-
*/
|
|
201
|
-
export async function resolveOxlintBinary(rootDir: string): Promise<string | undefined> {
|
|
202
|
-
const binDir = path.join(rootDir, "node_modules", ".bin");
|
|
203
|
-
const candidates =
|
|
204
|
-
process.platform === "win32"
|
|
205
|
-
? ["oxlint.exe", "oxlint.cmd", "oxlint"]
|
|
206
|
-
: ["oxlint"];
|
|
207
|
-
|
|
208
|
-
for (const name of candidates) {
|
|
209
|
-
const full = path.join(binDir, name);
|
|
210
|
-
if (await Bun.file(full).exists()) {
|
|
211
|
-
return full;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return undefined;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
218
|
-
// Diagnostic translation
|
|
219
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Extract the canonical rule id from an oxlint `code` field.
|
|
223
|
-
*
|
|
224
|
-
* oxlint emits either:
|
|
225
|
-
* - `no-debugger` → `no-debugger`
|
|
226
|
-
* - `typescript-eslint(no-floating-promises)` → `typescript/no-floating-promises`
|
|
227
|
-
*
|
|
228
|
-
* We normalize the second form to the `.oxlintrc.json` rule-id shape
|
|
229
|
-
* (`typescript/no-floating-promises`) so callers can match against
|
|
230
|
-
* their config-level allowlist without a plugin-name-translation table.
|
|
231
|
-
*/
|
|
232
|
-
export function extractRuleId(code: string | undefined): string {
|
|
233
|
-
if (!code) return "unknown";
|
|
234
|
-
const parenStart = code.indexOf("(");
|
|
235
|
-
const parenEnd = code.lastIndexOf(")");
|
|
236
|
-
if (parenStart > 0 && parenEnd > parenStart) {
|
|
237
|
-
const plugin = code.slice(0, parenStart);
|
|
238
|
-
const ruleName = code.slice(parenStart + 1, parenEnd);
|
|
239
|
-
// typescript-eslint → typescript; eslint → (bare); unicorn → unicorn
|
|
240
|
-
const normalizedPlugin =
|
|
241
|
-
plugin === "typescript-eslint"
|
|
242
|
-
? "typescript"
|
|
243
|
-
: plugin === "eslint"
|
|
244
|
-
? ""
|
|
245
|
-
: plugin;
|
|
246
|
-
return normalizedPlugin ? `${normalizedPlugin}/${ruleName}` : ruleName;
|
|
247
|
-
}
|
|
248
|
-
return code;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Map oxlint severity strings onto Mandu's `Severity` vocabulary.
|
|
253
|
-
*
|
|
254
|
-
* - `error` → `error`
|
|
255
|
-
* - `warning` → `warn`
|
|
256
|
-
* - `advice` → `info`
|
|
257
|
-
* - anything else → `warn` (conservative default; unknown severity
|
|
258
|
-
* shouldn't silently disappear as `info`).
|
|
259
|
-
*/
|
|
260
|
-
export function mapOxlintSeverity(raw: string | undefined): Severity {
|
|
261
|
-
if (raw === "error") return "error";
|
|
262
|
-
if (raw === "warning") return "warn";
|
|
263
|
-
if (raw === "advice") return "info";
|
|
264
|
-
return "warn";
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Translate a single oxlint diagnostic into a Mandu `Violation`.
|
|
269
|
-
*
|
|
270
|
-
* Type-aware lint hits don't naturally fit the architecture-layer
|
|
271
|
-
* shape (no `fromLayer` / `toLayer`). We populate those fields with
|
|
272
|
-
* sentinel values (`"type-aware"` / `"<file>"`) so consumers that
|
|
273
|
-
* iterate violations uniformly don't crash on undefined, while the
|
|
274
|
-
* `ruleName` / `ruleDescription` carry the real signal.
|
|
275
|
-
*/
|
|
276
|
-
export function translateDiagnostic(
|
|
277
|
-
diag: OxlintDiagnostic,
|
|
278
|
-
projectRoot: string,
|
|
279
|
-
severityOverride?: Severity,
|
|
280
|
-
): Violation {
|
|
281
|
-
const label = diag.labels?.[0]?.span;
|
|
282
|
-
const line = label?.line ?? 1;
|
|
283
|
-
const column = label?.column ?? 1;
|
|
284
|
-
const ruleId = extractRuleId(diag.code);
|
|
285
|
-
const severity = severityOverride ?? mapOxlintSeverity(diag.severity);
|
|
286
|
-
const filePath = diag.filename
|
|
287
|
-
? path.isAbsolute(diag.filename)
|
|
288
|
-
? diag.filename
|
|
289
|
-
: path.join(projectRoot, diag.filename)
|
|
290
|
-
: projectRoot;
|
|
291
|
-
|
|
292
|
-
const suggestions: string[] = [];
|
|
293
|
-
if (diag.help) suggestions.push(diag.help);
|
|
294
|
-
if (diag.url) suggestions.push(`Docs: ${diag.url}`);
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
// Architectural violation types don't cover lint hits; we reuse
|
|
298
|
-
// `file-type` (the nearest category — "something about the file
|
|
299
|
-
// itself is wrong") so downstream reporters that switch on `type`
|
|
300
|
-
// don't fall into an "unknown enum" default branch.
|
|
301
|
-
type: "file-type",
|
|
302
|
-
filePath,
|
|
303
|
-
line,
|
|
304
|
-
column,
|
|
305
|
-
importStatement: "",
|
|
306
|
-
importPath: "",
|
|
307
|
-
fromLayer: "type-aware",
|
|
308
|
-
toLayer: "type-aware",
|
|
309
|
-
ruleName: ruleId,
|
|
310
|
-
ruleDescription: diag.message,
|
|
311
|
-
severity,
|
|
312
|
-
allowedLayers: [],
|
|
313
|
-
suggestions,
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
318
|
-
// Main entry point
|
|
319
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Default wall-clock cap for the child process.
|
|
323
|
-
*
|
|
324
|
-
* 60s matches the scope constraint. `MANDU_TSGOLINT_TIMEOUT_MS` env
|
|
325
|
-
* override handles the "my CI agent is slow" case without code changes.
|
|
326
|
-
*/
|
|
327
|
-
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Run `oxlint --type-aware` and return translated violations.
|
|
331
|
-
*
|
|
332
|
-
* Never throws for "oxlint binary missing" — returns a skip envelope
|
|
333
|
-
* so the surrounding Guard pipeline stays on the happy path. DOES
|
|
334
|
-
* throw on malformed JSON output or unexpected spawn failures so
|
|
335
|
-
* operators see the real error.
|
|
336
|
-
*
|
|
337
|
-
* @example
|
|
338
|
-
* ```ts
|
|
339
|
-
* const { violations, summary, skipped } = await runTsgolint({
|
|
340
|
-
* projectRoot: process.cwd(),
|
|
341
|
-
* });
|
|
342
|
-
* if (skipped === "oxlint-not-installed") {
|
|
343
|
-
* console.warn("oxlint not installed — skipping type-aware lint");
|
|
344
|
-
* }
|
|
345
|
-
* ```
|
|
346
|
-
*/
|
|
347
|
-
export async function runTsgolint(
|
|
348
|
-
options: TsgolintBridgeOptions,
|
|
349
|
-
): Promise<TsgolintBridgeResult> {
|
|
350
|
-
const {
|
|
351
|
-
projectRoot,
|
|
352
|
-
rules,
|
|
353
|
-
severity,
|
|
354
|
-
configPath,
|
|
355
|
-
paths,
|
|
356
|
-
timeoutMs,
|
|
357
|
-
} = options;
|
|
358
|
-
|
|
359
|
-
// Severity "off" short-circuits before we touch the filesystem.
|
|
360
|
-
// The caller used to have to gate on this themselves; moving the
|
|
361
|
-
// check here centralizes the "no work needed" path and gives the
|
|
362
|
-
// summary / skipped fields a consistent shape.
|
|
363
|
-
if (severity === "off") {
|
|
364
|
-
return {
|
|
365
|
-
violations: [],
|
|
366
|
-
summary: {
|
|
367
|
-
rulesEnabled: [],
|
|
368
|
-
elapsedMs: 0,
|
|
369
|
-
diagnosticsReceived: 0,
|
|
370
|
-
filesAnalyzed: 0,
|
|
371
|
-
},
|
|
372
|
-
skipped: "severity-off",
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const binary = await resolveOxlintBinary(projectRoot);
|
|
377
|
-
if (!binary) {
|
|
378
|
-
return {
|
|
379
|
-
violations: [],
|
|
380
|
-
summary: {
|
|
381
|
-
rulesEnabled: [],
|
|
382
|
-
elapsedMs: 0,
|
|
383
|
-
diagnosticsReceived: 0,
|
|
384
|
-
filesAnalyzed: 0,
|
|
385
|
-
},
|
|
386
|
-
skipped: "oxlint-not-installed",
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Build argv.
|
|
391
|
-
//
|
|
392
|
-
// `--type-aware` toggles tsgolint rules; `--format=json` gives us the
|
|
393
|
-
// structured envelope we parse above. We deliberately do NOT forward
|
|
394
|
-
// `rules` as `-D <rule>` flags — oxlint rule-flag semantics interact
|
|
395
|
-
// non-trivially with categories (enabling `typescript/no-explicit-any`
|
|
396
|
-
// with `-D` would also re-enable the whole `correctness` category
|
|
397
|
-
// on some oxlint versions). Post-filtering keeps the behavior
|
|
398
|
-
// predictable at the cost of a small wasted-work penalty.
|
|
399
|
-
const argv: string[] = [binary, "--type-aware", "--format=json"];
|
|
400
|
-
if (configPath) {
|
|
401
|
-
argv.push("--config", configPath);
|
|
402
|
-
}
|
|
403
|
-
for (const p of paths ?? []) argv.push(p);
|
|
404
|
-
|
|
405
|
-
const envTimeout = (() => {
|
|
406
|
-
const raw = process.env.MANDU_TSGOLINT_TIMEOUT_MS;
|
|
407
|
-
const n = raw ? Number(raw) : NaN;
|
|
408
|
-
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
409
|
-
})();
|
|
410
|
-
const timeout = timeoutMs ?? envTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
411
|
-
|
|
412
|
-
const started = performance.now();
|
|
413
|
-
|
|
414
|
-
// Bun.spawn doesn't expose a built-in timeout knob the same way node
|
|
415
|
-
// child_process does, so we race the `exited` promise against a
|
|
416
|
-
// setTimeout that explicitly kills the process. Follows the
|
|
417
|
-
// Mandu/MCP convention (see #136, runCommand Promise.race timeout).
|
|
418
|
-
const proc = Bun.spawn(argv, {
|
|
419
|
-
cwd: projectRoot,
|
|
420
|
-
stdout: "pipe",
|
|
421
|
-
stderr: "pipe",
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
let timedOut = false;
|
|
425
|
-
const timer = setTimeout(() => {
|
|
426
|
-
timedOut = true;
|
|
427
|
-
try {
|
|
428
|
-
proc.kill();
|
|
429
|
-
} catch {
|
|
430
|
-
/* process already gone */
|
|
431
|
-
}
|
|
432
|
-
}, timeout);
|
|
433
|
-
|
|
434
|
-
let stdout: string;
|
|
435
|
-
let stderr: string;
|
|
436
|
-
try {
|
|
437
|
-
[stdout, stderr] = await Promise.all([
|
|
438
|
-
new Response(proc.stdout).text(),
|
|
439
|
-
new Response(proc.stderr).text(),
|
|
440
|
-
]);
|
|
441
|
-
await proc.exited;
|
|
442
|
-
} finally {
|
|
443
|
-
clearTimeout(timer);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const elapsedMs = Math.round(performance.now() - started);
|
|
447
|
-
|
|
448
|
-
if (timedOut) {
|
|
449
|
-
throw new Error(
|
|
450
|
-
`oxlint --type-aware timed out after ${timeout}ms ` +
|
|
451
|
-
`(MANDU_TSGOLINT_TIMEOUT_MS to override). stderr: ${stderr.slice(0, 400)}`,
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Empty stdout with non-zero exit = binary detonated before writing
|
|
456
|
-
// anything (missing node_modules, permissions, etc.). Surface loudly.
|
|
457
|
-
if (!stdout.trim()) {
|
|
458
|
-
if (stderr.trim()) {
|
|
459
|
-
throw new Error(
|
|
460
|
-
`oxlint --type-aware produced no stdout. stderr: ${stderr.slice(0, 800)}`,
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
// Unusual but not fatal — no files to lint.
|
|
464
|
-
return {
|
|
465
|
-
violations: [],
|
|
466
|
-
summary: {
|
|
467
|
-
rulesEnabled: [],
|
|
468
|
-
elapsedMs,
|
|
469
|
-
diagnosticsReceived: 0,
|
|
470
|
-
filesAnalyzed: 0,
|
|
471
|
-
stderr: stderr.trim() || undefined,
|
|
472
|
-
},
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
let parsed: OxlintJsonOutput;
|
|
477
|
-
try {
|
|
478
|
-
parsed = JSON.parse(stdout) as OxlintJsonOutput;
|
|
479
|
-
} catch (cause) {
|
|
480
|
-
throw new Error(
|
|
481
|
-
`Failed to parse oxlint JSON output: ${cause instanceof Error ? cause.message : String(cause)}. ` +
|
|
482
|
-
`First 400 chars: ${stdout.slice(0, 400)}`,
|
|
483
|
-
{ cause },
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const diagnostics = Array.isArray(parsed.diagnostics)
|
|
488
|
-
? parsed.diagnostics
|
|
489
|
-
: [];
|
|
490
|
-
const ruleFilter = rules && rules.length > 0 ? new Set(rules) : undefined;
|
|
491
|
-
|
|
492
|
-
const violations: Violation[] = [];
|
|
493
|
-
const rulesSeen = new Set<string>();
|
|
494
|
-
for (const diag of diagnostics) {
|
|
495
|
-
const ruleId = extractRuleId(diag.code);
|
|
496
|
-
if (ruleFilter && !ruleFilter.has(ruleId)) continue;
|
|
497
|
-
const violation = translateDiagnostic(diag, projectRoot, severity);
|
|
498
|
-
violations.push(violation);
|
|
499
|
-
rulesSeen.add(ruleId);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
return {
|
|
503
|
-
violations,
|
|
504
|
-
summary: {
|
|
505
|
-
rulesEnabled: [...rulesSeen].sort(),
|
|
506
|
-
elapsedMs,
|
|
507
|
-
diagnosticsReceived: diagnostics.length,
|
|
508
|
-
filesAnalyzed: parsed.number_of_files ?? 0,
|
|
509
|
-
stderr: stderr.trim() || undefined,
|
|
510
|
-
},
|
|
511
|
-
};
|
|
512
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Guard — oxlint type-aware bridge.
|
|
3
|
+
*
|
|
4
|
+
* Spawns `oxlint --type-aware` as a one-shot child process, translates the
|
|
5
|
+
* JSON diagnostic stream into Mandu's `Violation` contract, and returns a
|
|
6
|
+
* merged `{ violations, summary }` envelope that the `guard` runner /
|
|
7
|
+
* MCP surface can slot in next to the architecture-layer results.
|
|
8
|
+
*
|
|
9
|
+
* ## Contract
|
|
10
|
+
*
|
|
11
|
+
* - Bridge never throws for "oxlint not installed" — returns
|
|
12
|
+
* `{ skipped: "oxlint-not-installed" }` so the architecture check keeps
|
|
13
|
+
* running. The bridge DOES throw on malformed JSON output or spawn
|
|
14
|
+
* errors, because those are operator-visible bugs (e.g. a stale
|
|
15
|
+
* oxlint binary) that should fail loudly rather than silently.
|
|
16
|
+
* - Child process is invoked with `cwd: projectRoot`, `stdout: "pipe"`,
|
|
17
|
+
* `stderr: "pipe"`, and a 60-second timeout (configurable via the
|
|
18
|
+
* `MANDU_TSGOLINT_TIMEOUT_MS` env override for slow CI agents).
|
|
19
|
+
* - Exit code is NOT treated as authoritative — oxlint exits non-zero
|
|
20
|
+
* when ANY diagnostic is emitted, even warnings. We always parse
|
|
21
|
+
* stdout; stderr is captured for diagnostics but does not override the
|
|
22
|
+
* JSON body.
|
|
23
|
+
*
|
|
24
|
+
* ## oxlint JSON shape (oxlint >= 1.61)
|
|
25
|
+
*
|
|
26
|
+
* ```json
|
|
27
|
+
* {
|
|
28
|
+
* "diagnostics": [
|
|
29
|
+
* {
|
|
30
|
+
* "message": "Unexpected `any`. Specify a different type.",
|
|
31
|
+
* "code": "typescript-eslint(no-explicit-any)",
|
|
32
|
+
* "severity": "error" | "warning" | "advice",
|
|
33
|
+
* "filename": "src/foo.ts",
|
|
34
|
+
* "labels": [{ "span": { "offset": N, "length": N, "line": N, "column": N } }],
|
|
35
|
+
* "help": "...",
|
|
36
|
+
* "url": "https://oxc.rs/...",
|
|
37
|
+
* "causes": [],
|
|
38
|
+
* "related": []
|
|
39
|
+
* }
|
|
40
|
+
* ],
|
|
41
|
+
* "number_of_files": N,
|
|
42
|
+
* "number_of_rules": N,
|
|
43
|
+
* "start_time": N
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @module guard/tsgolint-bridge
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import path from "path";
|
|
51
|
+
import type { Severity, Violation } from "./types";
|
|
52
|
+
|
|
53
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
54
|
+
// Public types
|
|
55
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Bridge invocation options.
|
|
59
|
+
*
|
|
60
|
+
* All fields optional — the bridge's out-of-the-box behavior is
|
|
61
|
+
* `projectRoot = process.cwd()` and everything else picked up from
|
|
62
|
+
* `.oxlintrc.json` at the project root.
|
|
63
|
+
*/
|
|
64
|
+
export interface TsgolintBridgeOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Absolute path to the project root — the cwd the child process runs
|
|
67
|
+
* in AND the directory the binary is resolved under (`<root>/node_modules/.bin/oxlint`).
|
|
68
|
+
*/
|
|
69
|
+
projectRoot: string;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Explicit rule allowlist. When present, only these rules are reported;
|
|
73
|
+
* everything else is filtered out of the bridge's returned violations
|
|
74
|
+
* (oxlint itself still evaluates them, but we drop the non-matching
|
|
75
|
+
* diagnostics — trading a small post-filter cost for a simpler
|
|
76
|
+
* propagation path that avoids CLI rule-flag plumbing).
|
|
77
|
+
*
|
|
78
|
+
* Rule ids follow oxlint's `<plugin>/<rule-name>` convention
|
|
79
|
+
* (`typescript/no-floating-promises`, `no-debugger`).
|
|
80
|
+
*
|
|
81
|
+
* Undefined / empty = "trust oxlint defaults" (respect the user's
|
|
82
|
+
* `.oxlintrc.json` allowlist, emit every reported diagnostic).
|
|
83
|
+
*/
|
|
84
|
+
rules?: string[];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Override every diagnostic's severity to this value. Undefined =
|
|
88
|
+
* "respect oxlint's per-rule severity". Useful when a project wants
|
|
89
|
+
* type-aware hits to always be warnings (soft-landing period) or
|
|
90
|
+
* always errors (CI gate).
|
|
91
|
+
*/
|
|
92
|
+
severity?: Severity | "off";
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Custom oxlintrc path. Falls through to oxlint's default project-root
|
|
96
|
+
* lookup (`.oxlintrc.json` / `.oxlintrc.jsonc`) when unset.
|
|
97
|
+
*/
|
|
98
|
+
configPath?: string;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Paths to lint — file or directory globs. Defaults to the project root,
|
|
102
|
+
* letting oxlint's own traversal decide which files to walk.
|
|
103
|
+
*/
|
|
104
|
+
paths?: string[];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Wall-clock timeout in milliseconds. Default: 60_000 (60s).
|
|
108
|
+
* `MANDU_TSGOLINT_TIMEOUT_MS` env override wins when the caller
|
|
109
|
+
* leaves this undefined.
|
|
110
|
+
*/
|
|
111
|
+
timeoutMs?: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Bridge result envelope.
|
|
116
|
+
*
|
|
117
|
+
* Always returns an object — callers that need to branch on "skipped"
|
|
118
|
+
* narrow via the `skipped` discriminator. Shape stays stable whether
|
|
119
|
+
* oxlint was invoked or not so the CLI / MCP renderer has a single
|
|
120
|
+
* code path.
|
|
121
|
+
*/
|
|
122
|
+
export interface TsgolintBridgeResult {
|
|
123
|
+
/** Violations translated into Mandu's contract (empty on skip). */
|
|
124
|
+
violations: Violation[];
|
|
125
|
+
/** Per-run summary metadata. */
|
|
126
|
+
summary: TsgolintBridgeSummary;
|
|
127
|
+
/** When present, oxlint was not invoked and `violations` is empty. */
|
|
128
|
+
skipped?: TsgolintBridgeSkipReason;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface TsgolintBridgeSummary {
|
|
132
|
+
/** Rule ids that actually fired (post-filter). Empty on skip. */
|
|
133
|
+
rulesEnabled: string[];
|
|
134
|
+
/** Wall-clock time spent in the child process (ms). 0 on skip. */
|
|
135
|
+
elapsedMs: number;
|
|
136
|
+
/** Total diagnostics seen BEFORE the rule filter. */
|
|
137
|
+
diagnosticsReceived: number;
|
|
138
|
+
/** Number of files oxlint reported walking. 0 on skip. */
|
|
139
|
+
filesAnalyzed: number;
|
|
140
|
+
/** Non-empty stderr, if any (diagnostic hint for operators). */
|
|
141
|
+
stderr?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type TsgolintBridgeSkipReason =
|
|
145
|
+
| "oxlint-not-installed"
|
|
146
|
+
| "severity-off";
|
|
147
|
+
|
|
148
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
149
|
+
// oxlint JSON shape (internal — not re-exported; the bridge is the contract)
|
|
150
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
151
|
+
|
|
152
|
+
interface OxlintLabelSpan {
|
|
153
|
+
offset: number;
|
|
154
|
+
length: number;
|
|
155
|
+
line: number;
|
|
156
|
+
column: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface OxlintLabel {
|
|
160
|
+
span: OxlintLabelSpan;
|
|
161
|
+
message?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface OxlintDiagnostic {
|
|
165
|
+
message: string;
|
|
166
|
+
/** e.g. `typescript-eslint(no-floating-promises)` or `no-debugger`. */
|
|
167
|
+
code?: string;
|
|
168
|
+
severity?: "error" | "warning" | "advice";
|
|
169
|
+
filename?: string;
|
|
170
|
+
labels?: OxlintLabel[];
|
|
171
|
+
help?: string;
|
|
172
|
+
url?: string;
|
|
173
|
+
causes?: unknown[];
|
|
174
|
+
related?: unknown[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface OxlintJsonOutput {
|
|
178
|
+
diagnostics: OxlintDiagnostic[];
|
|
179
|
+
number_of_files?: number;
|
|
180
|
+
number_of_rules?: number;
|
|
181
|
+
threads_count?: number;
|
|
182
|
+
start_time?: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
186
|
+
// Binary resolution
|
|
187
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolve the oxlint binary under `<root>/node_modules/.bin/`.
|
|
191
|
+
*
|
|
192
|
+
* Windows: prefer `.exe` then `.cmd`. POSIX: the extensionless shim.
|
|
193
|
+
* Returns `undefined` when no candidate exists so the bridge can
|
|
194
|
+
* emit the `oxlint-not-installed` skip reason without touching the
|
|
195
|
+
* filesystem again.
|
|
196
|
+
*
|
|
197
|
+
* `Bun.file(path).exists()` is async but we stay sync here on purpose —
|
|
198
|
+
* the bridge entry point is already async and this keeps the callsite's
|
|
199
|
+
* control flow linear (`await resolveOxlintBinary(root)`).
|
|
200
|
+
*/
|
|
201
|
+
export async function resolveOxlintBinary(rootDir: string): Promise<string | undefined> {
|
|
202
|
+
const binDir = path.join(rootDir, "node_modules", ".bin");
|
|
203
|
+
const candidates =
|
|
204
|
+
process.platform === "win32"
|
|
205
|
+
? ["oxlint.exe", "oxlint.cmd", "oxlint"]
|
|
206
|
+
: ["oxlint"];
|
|
207
|
+
|
|
208
|
+
for (const name of candidates) {
|
|
209
|
+
const full = path.join(binDir, name);
|
|
210
|
+
if (await Bun.file(full).exists()) {
|
|
211
|
+
return full;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
218
|
+
// Diagnostic translation
|
|
219
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Extract the canonical rule id from an oxlint `code` field.
|
|
223
|
+
*
|
|
224
|
+
* oxlint emits either:
|
|
225
|
+
* - `no-debugger` → `no-debugger`
|
|
226
|
+
* - `typescript-eslint(no-floating-promises)` → `typescript/no-floating-promises`
|
|
227
|
+
*
|
|
228
|
+
* We normalize the second form to the `.oxlintrc.json` rule-id shape
|
|
229
|
+
* (`typescript/no-floating-promises`) so callers can match against
|
|
230
|
+
* their config-level allowlist without a plugin-name-translation table.
|
|
231
|
+
*/
|
|
232
|
+
export function extractRuleId(code: string | undefined): string {
|
|
233
|
+
if (!code) return "unknown";
|
|
234
|
+
const parenStart = code.indexOf("(");
|
|
235
|
+
const parenEnd = code.lastIndexOf(")");
|
|
236
|
+
if (parenStart > 0 && parenEnd > parenStart) {
|
|
237
|
+
const plugin = code.slice(0, parenStart);
|
|
238
|
+
const ruleName = code.slice(parenStart + 1, parenEnd);
|
|
239
|
+
// typescript-eslint → typescript; eslint → (bare); unicorn → unicorn
|
|
240
|
+
const normalizedPlugin =
|
|
241
|
+
plugin === "typescript-eslint"
|
|
242
|
+
? "typescript"
|
|
243
|
+
: plugin === "eslint"
|
|
244
|
+
? ""
|
|
245
|
+
: plugin;
|
|
246
|
+
return normalizedPlugin ? `${normalizedPlugin}/${ruleName}` : ruleName;
|
|
247
|
+
}
|
|
248
|
+
return code;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Map oxlint severity strings onto Mandu's `Severity` vocabulary.
|
|
253
|
+
*
|
|
254
|
+
* - `error` → `error`
|
|
255
|
+
* - `warning` → `warn`
|
|
256
|
+
* - `advice` → `info`
|
|
257
|
+
* - anything else → `warn` (conservative default; unknown severity
|
|
258
|
+
* shouldn't silently disappear as `info`).
|
|
259
|
+
*/
|
|
260
|
+
export function mapOxlintSeverity(raw: string | undefined): Severity {
|
|
261
|
+
if (raw === "error") return "error";
|
|
262
|
+
if (raw === "warning") return "warn";
|
|
263
|
+
if (raw === "advice") return "info";
|
|
264
|
+
return "warn";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Translate a single oxlint diagnostic into a Mandu `Violation`.
|
|
269
|
+
*
|
|
270
|
+
* Type-aware lint hits don't naturally fit the architecture-layer
|
|
271
|
+
* shape (no `fromLayer` / `toLayer`). We populate those fields with
|
|
272
|
+
* sentinel values (`"type-aware"` / `"<file>"`) so consumers that
|
|
273
|
+
* iterate violations uniformly don't crash on undefined, while the
|
|
274
|
+
* `ruleName` / `ruleDescription` carry the real signal.
|
|
275
|
+
*/
|
|
276
|
+
export function translateDiagnostic(
|
|
277
|
+
diag: OxlintDiagnostic,
|
|
278
|
+
projectRoot: string,
|
|
279
|
+
severityOverride?: Severity,
|
|
280
|
+
): Violation {
|
|
281
|
+
const label = diag.labels?.[0]?.span;
|
|
282
|
+
const line = label?.line ?? 1;
|
|
283
|
+
const column = label?.column ?? 1;
|
|
284
|
+
const ruleId = extractRuleId(diag.code);
|
|
285
|
+
const severity = severityOverride ?? mapOxlintSeverity(diag.severity);
|
|
286
|
+
const filePath = diag.filename
|
|
287
|
+
? path.isAbsolute(diag.filename)
|
|
288
|
+
? diag.filename
|
|
289
|
+
: path.join(projectRoot, diag.filename)
|
|
290
|
+
: projectRoot;
|
|
291
|
+
|
|
292
|
+
const suggestions: string[] = [];
|
|
293
|
+
if (diag.help) suggestions.push(diag.help);
|
|
294
|
+
if (diag.url) suggestions.push(`Docs: ${diag.url}`);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
// Architectural violation types don't cover lint hits; we reuse
|
|
298
|
+
// `file-type` (the nearest category — "something about the file
|
|
299
|
+
// itself is wrong") so downstream reporters that switch on `type`
|
|
300
|
+
// don't fall into an "unknown enum" default branch.
|
|
301
|
+
type: "file-type",
|
|
302
|
+
filePath,
|
|
303
|
+
line,
|
|
304
|
+
column,
|
|
305
|
+
importStatement: "",
|
|
306
|
+
importPath: "",
|
|
307
|
+
fromLayer: "type-aware",
|
|
308
|
+
toLayer: "type-aware",
|
|
309
|
+
ruleName: ruleId,
|
|
310
|
+
ruleDescription: diag.message,
|
|
311
|
+
severity,
|
|
312
|
+
allowedLayers: [],
|
|
313
|
+
suggestions,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
318
|
+
// Main entry point
|
|
319
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Default wall-clock cap for the child process.
|
|
323
|
+
*
|
|
324
|
+
* 60s matches the scope constraint. `MANDU_TSGOLINT_TIMEOUT_MS` env
|
|
325
|
+
* override handles the "my CI agent is slow" case without code changes.
|
|
326
|
+
*/
|
|
327
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Run `oxlint --type-aware` and return translated violations.
|
|
331
|
+
*
|
|
332
|
+
* Never throws for "oxlint binary missing" — returns a skip envelope
|
|
333
|
+
* so the surrounding Guard pipeline stays on the happy path. DOES
|
|
334
|
+
* throw on malformed JSON output or unexpected spawn failures so
|
|
335
|
+
* operators see the real error.
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```ts
|
|
339
|
+
* const { violations, summary, skipped } = await runTsgolint({
|
|
340
|
+
* projectRoot: process.cwd(),
|
|
341
|
+
* });
|
|
342
|
+
* if (skipped === "oxlint-not-installed") {
|
|
343
|
+
* console.warn("oxlint not installed — skipping type-aware lint");
|
|
344
|
+
* }
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
export async function runTsgolint(
|
|
348
|
+
options: TsgolintBridgeOptions,
|
|
349
|
+
): Promise<TsgolintBridgeResult> {
|
|
350
|
+
const {
|
|
351
|
+
projectRoot,
|
|
352
|
+
rules,
|
|
353
|
+
severity,
|
|
354
|
+
configPath,
|
|
355
|
+
paths,
|
|
356
|
+
timeoutMs,
|
|
357
|
+
} = options;
|
|
358
|
+
|
|
359
|
+
// Severity "off" short-circuits before we touch the filesystem.
|
|
360
|
+
// The caller used to have to gate on this themselves; moving the
|
|
361
|
+
// check here centralizes the "no work needed" path and gives the
|
|
362
|
+
// summary / skipped fields a consistent shape.
|
|
363
|
+
if (severity === "off") {
|
|
364
|
+
return {
|
|
365
|
+
violations: [],
|
|
366
|
+
summary: {
|
|
367
|
+
rulesEnabled: [],
|
|
368
|
+
elapsedMs: 0,
|
|
369
|
+
diagnosticsReceived: 0,
|
|
370
|
+
filesAnalyzed: 0,
|
|
371
|
+
},
|
|
372
|
+
skipped: "severity-off",
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const binary = await resolveOxlintBinary(projectRoot);
|
|
377
|
+
if (!binary) {
|
|
378
|
+
return {
|
|
379
|
+
violations: [],
|
|
380
|
+
summary: {
|
|
381
|
+
rulesEnabled: [],
|
|
382
|
+
elapsedMs: 0,
|
|
383
|
+
diagnosticsReceived: 0,
|
|
384
|
+
filesAnalyzed: 0,
|
|
385
|
+
},
|
|
386
|
+
skipped: "oxlint-not-installed",
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Build argv.
|
|
391
|
+
//
|
|
392
|
+
// `--type-aware` toggles tsgolint rules; `--format=json` gives us the
|
|
393
|
+
// structured envelope we parse above. We deliberately do NOT forward
|
|
394
|
+
// `rules` as `-D <rule>` flags — oxlint rule-flag semantics interact
|
|
395
|
+
// non-trivially with categories (enabling `typescript/no-explicit-any`
|
|
396
|
+
// with `-D` would also re-enable the whole `correctness` category
|
|
397
|
+
// on some oxlint versions). Post-filtering keeps the behavior
|
|
398
|
+
// predictable at the cost of a small wasted-work penalty.
|
|
399
|
+
const argv: string[] = [binary, "--type-aware", "--format=json"];
|
|
400
|
+
if (configPath) {
|
|
401
|
+
argv.push("--config", configPath);
|
|
402
|
+
}
|
|
403
|
+
for (const p of paths ?? []) argv.push(p);
|
|
404
|
+
|
|
405
|
+
const envTimeout = (() => {
|
|
406
|
+
const raw = process.env.MANDU_TSGOLINT_TIMEOUT_MS;
|
|
407
|
+
const n = raw ? Number(raw) : NaN;
|
|
408
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
409
|
+
})();
|
|
410
|
+
const timeout = timeoutMs ?? envTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
411
|
+
|
|
412
|
+
const started = performance.now();
|
|
413
|
+
|
|
414
|
+
// Bun.spawn doesn't expose a built-in timeout knob the same way node
|
|
415
|
+
// child_process does, so we race the `exited` promise against a
|
|
416
|
+
// setTimeout that explicitly kills the process. Follows the
|
|
417
|
+
// Mandu/MCP convention (see #136, runCommand Promise.race timeout).
|
|
418
|
+
const proc = Bun.spawn(argv, {
|
|
419
|
+
cwd: projectRoot,
|
|
420
|
+
stdout: "pipe",
|
|
421
|
+
stderr: "pipe",
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
let timedOut = false;
|
|
425
|
+
const timer = setTimeout(() => {
|
|
426
|
+
timedOut = true;
|
|
427
|
+
try {
|
|
428
|
+
proc.kill();
|
|
429
|
+
} catch {
|
|
430
|
+
/* process already gone */
|
|
431
|
+
}
|
|
432
|
+
}, timeout);
|
|
433
|
+
|
|
434
|
+
let stdout: string;
|
|
435
|
+
let stderr: string;
|
|
436
|
+
try {
|
|
437
|
+
[stdout, stderr] = await Promise.all([
|
|
438
|
+
new Response(proc.stdout).text(),
|
|
439
|
+
new Response(proc.stderr).text(),
|
|
440
|
+
]);
|
|
441
|
+
await proc.exited;
|
|
442
|
+
} finally {
|
|
443
|
+
clearTimeout(timer);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const elapsedMs = Math.round(performance.now() - started);
|
|
447
|
+
|
|
448
|
+
if (timedOut) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`oxlint --type-aware timed out after ${timeout}ms ` +
|
|
451
|
+
`(MANDU_TSGOLINT_TIMEOUT_MS to override). stderr: ${stderr.slice(0, 400)}`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Empty stdout with non-zero exit = binary detonated before writing
|
|
456
|
+
// anything (missing node_modules, permissions, etc.). Surface loudly.
|
|
457
|
+
if (!stdout.trim()) {
|
|
458
|
+
if (stderr.trim()) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`oxlint --type-aware produced no stdout. stderr: ${stderr.slice(0, 800)}`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
// Unusual but not fatal — no files to lint.
|
|
464
|
+
return {
|
|
465
|
+
violations: [],
|
|
466
|
+
summary: {
|
|
467
|
+
rulesEnabled: [],
|
|
468
|
+
elapsedMs,
|
|
469
|
+
diagnosticsReceived: 0,
|
|
470
|
+
filesAnalyzed: 0,
|
|
471
|
+
stderr: stderr.trim() || undefined,
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let parsed: OxlintJsonOutput;
|
|
477
|
+
try {
|
|
478
|
+
parsed = JSON.parse(stdout) as OxlintJsonOutput;
|
|
479
|
+
} catch (cause) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`Failed to parse oxlint JSON output: ${cause instanceof Error ? cause.message : String(cause)}. ` +
|
|
482
|
+
`First 400 chars: ${stdout.slice(0, 400)}`,
|
|
483
|
+
{ cause },
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const diagnostics = Array.isArray(parsed.diagnostics)
|
|
488
|
+
? parsed.diagnostics
|
|
489
|
+
: [];
|
|
490
|
+
const ruleFilter = rules && rules.length > 0 ? new Set(rules) : undefined;
|
|
491
|
+
|
|
492
|
+
const violations: Violation[] = [];
|
|
493
|
+
const rulesSeen = new Set<string>();
|
|
494
|
+
for (const diag of diagnostics) {
|
|
495
|
+
const ruleId = extractRuleId(diag.code);
|
|
496
|
+
if (ruleFilter && !ruleFilter.has(ruleId)) continue;
|
|
497
|
+
const violation = translateDiagnostic(diag, projectRoot, severity);
|
|
498
|
+
violations.push(violation);
|
|
499
|
+
rulesSeen.add(ruleId);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
violations,
|
|
504
|
+
summary: {
|
|
505
|
+
rulesEnabled: [...rulesSeen].sort(),
|
|
506
|
+
elapsedMs,
|
|
507
|
+
diagnosticsReceived: diagnostics.length,
|
|
508
|
+
filesAnalyzed: parsed.number_of_files ?? 0,
|
|
509
|
+
stderr: stderr.trim() || undefined,
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|