@mandujs/core 0.54.32 → 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 +127 -211
- 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
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
*
|
|
29
29
|
* URL safety model (applies to `shared.runtime`, `shared.vendor`,
|
|
30
30
|
* `shared.router`, `shared.fastRefresh.glue`, `shared.fastRefresh.runtime`,
|
|
31
|
-
* `bundles[].js`, `bundles[].css`, `islands[].js`, `partials[].js`,
|
|
32
|
-
* `importMap.imports[*]`):
|
|
31
|
+
* `bundles[].js`, `bundles[].css`, `islands[].js`, `partials[].js`,
|
|
32
|
+
* `importMap.imports[*]`):
|
|
33
33
|
*
|
|
34
34
|
* ALLOW: absolute paths rooted at `/.mandu/client/` ending in `.js` or `.css`.
|
|
35
35
|
* The bundler itself only ever emits this shape.
|
|
@@ -126,7 +126,7 @@ export class ManifestValidationError extends Error {
|
|
|
126
126
|
* check at preamble emit time — defense in depth against a manifest that
|
|
127
127
|
* slipped past validation (e.g. skipFrameworkBundles fallback path).
|
|
128
128
|
*/
|
|
129
|
-
export function isSafeManduUrl(url: unknown): url is string {
|
|
129
|
+
export function isSafeManduUrl(url: unknown): url is string {
|
|
130
130
|
if (typeof url !== "string") return false;
|
|
131
131
|
if (url.length === 0 || url.length > MAX_MANIFEST_URL_LEN) return false;
|
|
132
132
|
if (FORBIDDEN_URL_CHARS.test(url)) return false;
|
|
@@ -134,7 +134,19 @@ export function isSafeManduUrl(url: unknown): url is string {
|
|
|
134
134
|
if (url.includes(s)) return false;
|
|
135
135
|
}
|
|
136
136
|
return SAFE_MANDU_URL_REGEX.test(url);
|
|
137
|
-
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Response-time variant of `isSafeManduUrl`. Published HTML may add one
|
|
141
|
+
* framework-owned `g=<generationId>` query parameter so an older document
|
|
142
|
+
* keeps loading assets from its own immutable build generation. Raw manifests
|
|
143
|
+
* remain query-free and continue to use the stricter predicate above.
|
|
144
|
+
*/
|
|
145
|
+
export function isSafePublishedManduUrl(url: unknown): url is string {
|
|
146
|
+
if (typeof url !== "string") return false;
|
|
147
|
+
const match = /^(.*)\?g=([a-z0-9][a-z0-9-]{5,127})$/.exec(url);
|
|
148
|
+
return match ? isSafeManduUrl(match[1]) : isSafeManduUrl(url);
|
|
149
|
+
}
|
|
138
150
|
|
|
139
151
|
/**
|
|
140
152
|
* Zod refinement that runs `isSafeManduUrl` with a helpful error message.
|
|
@@ -162,34 +174,36 @@ const BundleEntrySchema = z.object({
|
|
|
162
174
|
priority: PrioritySchema,
|
|
163
175
|
});
|
|
164
176
|
|
|
165
|
-
const IslandEntrySchema = z.object({
|
|
166
|
-
js: safeManduUrl("islands[].js"),
|
|
167
|
-
route: z.string().min(1),
|
|
168
|
-
priority: PrioritySchema,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const PartialEntrySchema = z.object({
|
|
172
|
-
js: safeManduUrl("partials[].js"),
|
|
173
|
-
priority: PrioritySchema,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const BoundaryEntrySchema = z.object({
|
|
177
|
-
route: z.string().min(1),
|
|
178
|
-
js: safeManduUrl("boundaries[].js"),
|
|
179
|
-
module: z.string().min(1),
|
|
180
|
-
exportName: z.string().min(1),
|
|
181
|
-
priority: PrioritySchema,
|
|
182
|
-
hydrate: z.string().min(1),
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const FastRefreshSchema = z.object({
|
|
186
|
-
runtime: safeManduUrl("shared.fastRefresh.runtime"),
|
|
187
|
-
glue: safeManduUrl("shared.fastRefresh.glue"),
|
|
177
|
+
const IslandEntrySchema = z.object({
|
|
178
|
+
js: safeManduUrl("islands[].js"),
|
|
179
|
+
route: z.string().min(1),
|
|
180
|
+
priority: PrioritySchema,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const PartialEntrySchema = z.object({
|
|
184
|
+
js: safeManduUrl("partials[].js"),
|
|
185
|
+
priority: PrioritySchema,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const BoundaryEntrySchema = z.object({
|
|
189
|
+
route: z.string().min(1),
|
|
190
|
+
js: safeManduUrl("boundaries[].js"),
|
|
191
|
+
module: z.string().min(1),
|
|
192
|
+
exportName: z.string().min(1),
|
|
193
|
+
priority: PrioritySchema,
|
|
194
|
+
hydrate: z.string().min(1),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const FastRefreshSchema = z.object({
|
|
198
|
+
runtime: safeManduUrl("shared.fastRefresh.runtime"),
|
|
199
|
+
glue: safeManduUrl("shared.fastRefresh.glue"),
|
|
188
200
|
});
|
|
189
201
|
|
|
190
|
-
const SharedSchema = z.object({
|
|
191
|
-
runtime
|
|
192
|
-
|
|
202
|
+
const SharedSchema = z.object({
|
|
203
|
+
// A successful zero-JS build has no client runtime/vendor artifacts.
|
|
204
|
+
// Empty strings are inert and are the canonical empty-manifest shape.
|
|
205
|
+
runtime: safeManduUrl("shared.runtime").or(z.literal("")),
|
|
206
|
+
vendor: safeManduUrl("shared.vendor").or(z.literal("")),
|
|
193
207
|
router: safeManduUrl("shared.router").optional(),
|
|
194
208
|
fastRefresh: FastRefreshSchema.optional(),
|
|
195
209
|
});
|
|
@@ -230,16 +244,17 @@ const ImportMapSchema = z.object({
|
|
|
230
244
|
* are allowed to be absent, matching the production manifest shape that
|
|
231
245
|
* `build.ts` emits when dev-only assets are not generated.
|
|
232
246
|
*/
|
|
233
|
-
export const BundleManifestSchema = z
|
|
234
|
-
.object({
|
|
235
|
-
version: z.number().int().min(1),
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
export const BundleManifestSchema = z
|
|
248
|
+
.object({
|
|
249
|
+
version: z.number().int().min(1),
|
|
250
|
+
generationId: z.string().regex(/^[a-z0-9][a-z0-9-]{5,127}$/).optional(),
|
|
251
|
+
buildTime: z.string().min(1),
|
|
252
|
+
env: z.enum(["development", "production"]),
|
|
253
|
+
bundles: z.record(z.string(), BundleEntrySchema),
|
|
254
|
+
islands: z.record(z.string(), IslandEntrySchema).optional(),
|
|
255
|
+
partials: z.record(z.string(), PartialEntrySchema).optional(),
|
|
256
|
+
boundaries: z.record(z.string(), BoundaryEntrySchema).optional(),
|
|
257
|
+
shared: SharedSchema,
|
|
243
258
|
importMap: ImportMapSchema.optional(),
|
|
244
259
|
})
|
|
245
260
|
.strict();
|
|
@@ -80,19 +80,19 @@ describe("defaultAllowImporter", () => {
|
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
describe("DEFAULT_BLOCK_FILTER", () => {
|
|
83
|
-
test("matches __generated__ specifiers", () => {
|
|
84
|
-
expect("./__generated__/foo").toMatch(DEFAULT_BLOCK_FILTER);
|
|
85
|
-
expect("../../src/__generated__/routes").toMatch(DEFAULT_BLOCK_FILTER);
|
|
86
|
-
});
|
|
87
|
-
test("matches direct .mandu/generated relative specifiers", () => {
|
|
88
|
-
expect("../.mandu/generated/routes").toMatch(DEFAULT_BLOCK_FILTER);
|
|
89
|
-
expect("../../../.mandu/generated/server/repos/party.repo").toMatch(DEFAULT_BLOCK_FILTER);
|
|
90
|
-
});
|
|
91
|
-
test("does NOT match look-alikes without double underscores", () => {
|
|
92
|
-
expect("./generated/foo".match(DEFAULT_BLOCK_FILTER)).toBeNull();
|
|
93
|
-
expect("./src/generate/foo".match(DEFAULT_BLOCK_FILTER)).toBeNull();
|
|
94
|
-
});
|
|
95
|
-
});
|
|
83
|
+
test("matches __generated__ specifiers", () => {
|
|
84
|
+
expect("./__generated__/foo").toMatch(DEFAULT_BLOCK_FILTER);
|
|
85
|
+
expect("../../src/__generated__/routes").toMatch(DEFAULT_BLOCK_FILTER);
|
|
86
|
+
});
|
|
87
|
+
test("matches direct .mandu/generated relative specifiers", () => {
|
|
88
|
+
expect("../.mandu/generated/routes").toMatch(DEFAULT_BLOCK_FILTER);
|
|
89
|
+
expect("../../../.mandu/generated/server/repos/party.repo").toMatch(DEFAULT_BLOCK_FILTER);
|
|
90
|
+
});
|
|
91
|
+
test("does NOT match look-alikes without double underscores", () => {
|
|
92
|
+
expect("./generated/foo".match(DEFAULT_BLOCK_FILTER)).toBeNull();
|
|
93
|
+
expect("./src/generate/foo".match(DEFAULT_BLOCK_FILTER)).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
96
|
|
|
97
97
|
describe("defaultBundlerPlugins", () => {
|
|
98
98
|
test("installs block-generated-imports by default", () => {
|
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `resolveReactCompilerConfig` — #240 Phase 2 auto-detect tests.
|
|
3
|
-
*
|
|
4
|
-
* The probe only fires when `enabled` is undefined. Explicit `true` /
|
|
5
|
-
* `false` veto the probe so user intent always wins.
|
|
6
|
-
*
|
|
7
|
-
* Cache lifetime is per-process; we reset it between cases so a probe
|
|
8
|
-
* from one fixture doesn't leak to the next.
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, expect, beforeEach } from "bun:test";
|
|
11
|
-
import fs from "node:fs/promises";
|
|
12
|
-
import os from "node:os";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
resolveReactCompilerConfig,
|
|
17
|
-
_resetReactCompilerConfigCache,
|
|
18
|
-
} from "../react-compiler-config";
|
|
19
|
-
|
|
20
|
-
async function makeRoot(prefix: string): Promise<string> {
|
|
21
|
-
return fs.mkdtemp(path.join(os.tmpdir(), `mandu-rc-${prefix}-`));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
_resetReactCompilerConfigCache();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe("resolveReactCompilerConfig", () => {
|
|
29
|
-
it("explicit enabled:true honours the user even when peers are missing", async () => {
|
|
30
|
-
const root = await makeRoot("explicit-on");
|
|
31
|
-
const result = resolveReactCompilerConfig({ enabled: true }, root);
|
|
32
|
-
expect(result.enabled).toBe(true);
|
|
33
|
-
expect(result.autoDetected).toBe(false);
|
|
34
|
-
await fs.rm(root, { recursive: true, force: true });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("explicit enabled:false vetos the probe", async () => {
|
|
38
|
-
const root = await makeRoot("explicit-off");
|
|
39
|
-
const result = resolveReactCompilerConfig({ enabled: false }, root);
|
|
40
|
-
expect(result.enabled).toBe(false);
|
|
41
|
-
expect(result.autoDetected).toBe(false);
|
|
42
|
-
await fs.rm(root, { recursive: true, force: true });
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("undefined enabled + missing peers → disabled silently", async () => {
|
|
46
|
-
const root = await makeRoot("auto-no-peers");
|
|
47
|
-
// Empty rootDir — no node_modules, no package.json, no peer deps.
|
|
48
|
-
const result = resolveReactCompilerConfig(undefined, root);
|
|
49
|
-
expect(result.enabled).toBe(false);
|
|
50
|
-
expect(result.autoDetected).toBe(false);
|
|
51
|
-
await fs.rm(root, { recursive: true, force: true });
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("forwards compilerConfig regardless of enabled state", async () => {
|
|
55
|
-
const root = await makeRoot("compiler-config");
|
|
56
|
-
const cfg = { compilationMode: "annotation" };
|
|
57
|
-
const result = resolveReactCompilerConfig(
|
|
58
|
-
{ enabled: true, compilerConfig: cfg },
|
|
59
|
-
root,
|
|
60
|
-
);
|
|
61
|
-
expect(result.compilerConfig).toBe(cfg);
|
|
62
|
-
await fs.rm(root, { recursive: true, force: true });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("caches by (rootDir, explicit-enabled) — second call hits cache", async () => {
|
|
66
|
-
const root = await makeRoot("cache");
|
|
67
|
-
const a = resolveReactCompilerConfig(undefined, root);
|
|
68
|
-
const b = resolveReactCompilerConfig(undefined, root);
|
|
69
|
-
// Same identity — cache hit returns the stored object.
|
|
70
|
-
expect(a).toBe(b);
|
|
71
|
-
await fs.rm(root, { recursive: true, force: true });
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("treats explicit-true vs auto as separate cache keys", async () => {
|
|
75
|
-
const root = await makeRoot("cache-key");
|
|
76
|
-
const auto = resolveReactCompilerConfig(undefined, root);
|
|
77
|
-
const explicit = resolveReactCompilerConfig({ enabled: true }, root);
|
|
78
|
-
expect(auto).not.toBe(explicit);
|
|
79
|
-
expect(auto.enabled).toBe(false);
|
|
80
|
-
expect(explicit.enabled).toBe(true);
|
|
81
|
-
await fs.rm(root, { recursive: true, force: true });
|
|
82
|
-
});
|
|
83
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* `resolveReactCompilerConfig` — #240 Phase 2 auto-detect tests.
|
|
3
|
+
*
|
|
4
|
+
* The probe only fires when `enabled` is undefined. Explicit `true` /
|
|
5
|
+
* `false` veto the probe so user intent always wins.
|
|
6
|
+
*
|
|
7
|
+
* Cache lifetime is per-process; we reset it between cases so a probe
|
|
8
|
+
* from one fixture doesn't leak to the next.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
11
|
+
import fs from "node:fs/promises";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
resolveReactCompilerConfig,
|
|
17
|
+
_resetReactCompilerConfigCache,
|
|
18
|
+
} from "../react-compiler-config";
|
|
19
|
+
|
|
20
|
+
async function makeRoot(prefix: string): Promise<string> {
|
|
21
|
+
return fs.mkdtemp(path.join(os.tmpdir(), `mandu-rc-${prefix}-`));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
_resetReactCompilerConfigCache();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("resolveReactCompilerConfig", () => {
|
|
29
|
+
it("explicit enabled:true honours the user even when peers are missing", async () => {
|
|
30
|
+
const root = await makeRoot("explicit-on");
|
|
31
|
+
const result = resolveReactCompilerConfig({ enabled: true }, root);
|
|
32
|
+
expect(result.enabled).toBe(true);
|
|
33
|
+
expect(result.autoDetected).toBe(false);
|
|
34
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("explicit enabled:false vetos the probe", async () => {
|
|
38
|
+
const root = await makeRoot("explicit-off");
|
|
39
|
+
const result = resolveReactCompilerConfig({ enabled: false }, root);
|
|
40
|
+
expect(result.enabled).toBe(false);
|
|
41
|
+
expect(result.autoDetected).toBe(false);
|
|
42
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("undefined enabled + missing peers → disabled silently", async () => {
|
|
46
|
+
const root = await makeRoot("auto-no-peers");
|
|
47
|
+
// Empty rootDir — no node_modules, no package.json, no peer deps.
|
|
48
|
+
const result = resolveReactCompilerConfig(undefined, root);
|
|
49
|
+
expect(result.enabled).toBe(false);
|
|
50
|
+
expect(result.autoDetected).toBe(false);
|
|
51
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("forwards compilerConfig regardless of enabled state", async () => {
|
|
55
|
+
const root = await makeRoot("compiler-config");
|
|
56
|
+
const cfg = { compilationMode: "annotation" };
|
|
57
|
+
const result = resolveReactCompilerConfig(
|
|
58
|
+
{ enabled: true, compilerConfig: cfg },
|
|
59
|
+
root,
|
|
60
|
+
);
|
|
61
|
+
expect(result.compilerConfig).toBe(cfg);
|
|
62
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("caches by (rootDir, explicit-enabled) — second call hits cache", async () => {
|
|
66
|
+
const root = await makeRoot("cache");
|
|
67
|
+
const a = resolveReactCompilerConfig(undefined, root);
|
|
68
|
+
const b = resolveReactCompilerConfig(undefined, root);
|
|
69
|
+
// Same identity — cache hit returns the stored object.
|
|
70
|
+
expect(a).toBe(b);
|
|
71
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("treats explicit-true vs auto as separate cache keys", async () => {
|
|
75
|
+
const root = await makeRoot("cache-key");
|
|
76
|
+
const auto = resolveReactCompilerConfig(undefined, root);
|
|
77
|
+
const explicit = resolveReactCompilerConfig({ enabled: true }, root);
|
|
78
|
+
expect(auto).not.toBe(explicit);
|
|
79
|
+
expect(auto.enabled).toBe(false);
|
|
80
|
+
expect(explicit.enabled).toBe(true);
|
|
81
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
runReactCompilerLint,
|
|
5
|
-
formatCompilerReport,
|
|
6
|
-
type ReactCompilerDiagnostic,
|
|
7
|
-
} from "../react-compiler-lint";
|
|
8
|
-
|
|
9
|
-
describe("runReactCompilerLint", () => {
|
|
10
|
-
test("returns empty result with no targets without invoking ESLint", async () => {
|
|
11
|
-
const result = await runReactCompilerLint({
|
|
12
|
-
projectRoot: process.cwd(),
|
|
13
|
-
targetFiles: [],
|
|
14
|
-
});
|
|
15
|
-
expect(result.diagnostics).toEqual([]);
|
|
16
|
-
expect(result.skipped).toBe(false);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("degrades gracefully when ESLint peer dep is missing", async () => {
|
|
20
|
-
// In a clean Mandu checkout `eslint` and
|
|
21
|
-
// `eslint-plugin-react-compiler` are not installed. The runner
|
|
22
|
-
// must report `skipped: true` + empty diagnostics + a skipReason.
|
|
23
|
-
const result = await runReactCompilerLint({
|
|
24
|
-
projectRoot: process.cwd(),
|
|
25
|
-
targetFiles: [path.resolve(__filename)],
|
|
26
|
-
});
|
|
27
|
-
// If ESLint happens to be installed in this env, diagnostics is
|
|
28
|
-
// either empty (this file has no bailouts) or the runner ran
|
|
29
|
-
// successfully. Either shape is valid — the contract we care
|
|
30
|
-
// about is "no throw + typed result."
|
|
31
|
-
expect(Array.isArray(result.diagnostics)).toBe(true);
|
|
32
|
-
expect(typeof result.skipped).toBe("boolean");
|
|
33
|
-
if (result.skipped) {
|
|
34
|
-
expect(typeof result.skipReason).toBe("string");
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("formatCompilerReport", () => {
|
|
40
|
-
test("returns the no-bailouts sentinel for empty input", () => {
|
|
41
|
-
expect(formatCompilerReport([])).toContain("no bailouts detected");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("summarises bailouts with file counts + bullet list", () => {
|
|
45
|
-
const diagnostics: ReactCompilerDiagnostic[] = [
|
|
46
|
-
{
|
|
47
|
-
file: "/app/todos/todo.island.tsx",
|
|
48
|
-
line: 42,
|
|
49
|
-
column: 5,
|
|
50
|
-
message: "Cannot memoize: ref escape in closure.",
|
|
51
|
-
ruleId: "react-compiler/react-compiler",
|
|
52
|
-
severity: "warning",
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
file: "/app/todos/todo.island.tsx",
|
|
56
|
-
line: 87,
|
|
57
|
-
column: 3,
|
|
58
|
-
message: "Cannot memoize: conditional hook call.",
|
|
59
|
-
ruleId: "react-compiler/react-compiler",
|
|
60
|
-
severity: "warning",
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
file: "/app/dashboard/chart.client.tsx",
|
|
64
|
-
line: 14,
|
|
65
|
-
column: 9,
|
|
66
|
-
message: "Cannot memoize: mutation of a shared value.",
|
|
67
|
-
ruleId: "react-compiler/react-compiler",
|
|
68
|
-
severity: "warning",
|
|
69
|
-
},
|
|
70
|
-
];
|
|
71
|
-
const out = formatCompilerReport(diagnostics);
|
|
72
|
-
expect(out).toContain("3 bailout(s) in 2 file(s)");
|
|
73
|
-
expect(out).toContain("todo.island.tsx:42:5");
|
|
74
|
-
expect(out).toContain("chart.client.tsx:14:9");
|
|
75
|
-
expect(out).toContain("https://react.dev");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("truncates past the limit with a '... N more' tail", () => {
|
|
79
|
-
const diagnostics: ReactCompilerDiagnostic[] = Array.from(
|
|
80
|
-
{ length: 30 },
|
|
81
|
-
(_, i) => ({
|
|
82
|
-
file: `/app/x/c${i}.island.tsx`,
|
|
83
|
-
line: i + 1,
|
|
84
|
-
column: 1,
|
|
85
|
-
message: "bailout",
|
|
86
|
-
ruleId: "react-compiler/react-compiler",
|
|
87
|
-
severity: "warning" as const,
|
|
88
|
-
}),
|
|
89
|
-
);
|
|
90
|
-
const out = formatCompilerReport(diagnostics, { limit: 5 });
|
|
91
|
-
expect(out).toContain("30 bailout(s)");
|
|
92
|
-
expect(out).toContain("… and 25 more");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("uses relative paths when projectRoot is provided", () => {
|
|
96
|
-
const diagnostic: ReactCompilerDiagnostic = {
|
|
97
|
-
file: path.resolve("/project/app/todo.island.tsx"),
|
|
98
|
-
line: 1,
|
|
99
|
-
column: 1,
|
|
100
|
-
message: "bailout",
|
|
101
|
-
ruleId: "react-compiler/react-compiler",
|
|
102
|
-
severity: "warning",
|
|
103
|
-
};
|
|
104
|
-
const out = formatCompilerReport([diagnostic], {
|
|
105
|
-
projectRoot: path.resolve("/project"),
|
|
106
|
-
});
|
|
107
|
-
expect(out).toContain(path.join("app", "todo.island.tsx"));
|
|
108
|
-
expect(out).not.toContain(path.resolve("/project/app"));
|
|
109
|
-
});
|
|
110
|
-
});
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
runReactCompilerLint,
|
|
5
|
+
formatCompilerReport,
|
|
6
|
+
type ReactCompilerDiagnostic,
|
|
7
|
+
} from "../react-compiler-lint";
|
|
8
|
+
|
|
9
|
+
describe("runReactCompilerLint", () => {
|
|
10
|
+
test("returns empty result with no targets without invoking ESLint", async () => {
|
|
11
|
+
const result = await runReactCompilerLint({
|
|
12
|
+
projectRoot: process.cwd(),
|
|
13
|
+
targetFiles: [],
|
|
14
|
+
});
|
|
15
|
+
expect(result.diagnostics).toEqual([]);
|
|
16
|
+
expect(result.skipped).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("degrades gracefully when ESLint peer dep is missing", async () => {
|
|
20
|
+
// In a clean Mandu checkout `eslint` and
|
|
21
|
+
// `eslint-plugin-react-compiler` are not installed. The runner
|
|
22
|
+
// must report `skipped: true` + empty diagnostics + a skipReason.
|
|
23
|
+
const result = await runReactCompilerLint({
|
|
24
|
+
projectRoot: process.cwd(),
|
|
25
|
+
targetFiles: [path.resolve(__filename)],
|
|
26
|
+
});
|
|
27
|
+
// If ESLint happens to be installed in this env, diagnostics is
|
|
28
|
+
// either empty (this file has no bailouts) or the runner ran
|
|
29
|
+
// successfully. Either shape is valid — the contract we care
|
|
30
|
+
// about is "no throw + typed result."
|
|
31
|
+
expect(Array.isArray(result.diagnostics)).toBe(true);
|
|
32
|
+
expect(typeof result.skipped).toBe("boolean");
|
|
33
|
+
if (result.skipped) {
|
|
34
|
+
expect(typeof result.skipReason).toBe("string");
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("formatCompilerReport", () => {
|
|
40
|
+
test("returns the no-bailouts sentinel for empty input", () => {
|
|
41
|
+
expect(formatCompilerReport([])).toContain("no bailouts detected");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("summarises bailouts with file counts + bullet list", () => {
|
|
45
|
+
const diagnostics: ReactCompilerDiagnostic[] = [
|
|
46
|
+
{
|
|
47
|
+
file: "/app/todos/todo.island.tsx",
|
|
48
|
+
line: 42,
|
|
49
|
+
column: 5,
|
|
50
|
+
message: "Cannot memoize: ref escape in closure.",
|
|
51
|
+
ruleId: "react-compiler/react-compiler",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
file: "/app/todos/todo.island.tsx",
|
|
56
|
+
line: 87,
|
|
57
|
+
column: 3,
|
|
58
|
+
message: "Cannot memoize: conditional hook call.",
|
|
59
|
+
ruleId: "react-compiler/react-compiler",
|
|
60
|
+
severity: "warning",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
file: "/app/dashboard/chart.client.tsx",
|
|
64
|
+
line: 14,
|
|
65
|
+
column: 9,
|
|
66
|
+
message: "Cannot memoize: mutation of a shared value.",
|
|
67
|
+
ruleId: "react-compiler/react-compiler",
|
|
68
|
+
severity: "warning",
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
const out = formatCompilerReport(diagnostics);
|
|
72
|
+
expect(out).toContain("3 bailout(s) in 2 file(s)");
|
|
73
|
+
expect(out).toContain("todo.island.tsx:42:5");
|
|
74
|
+
expect(out).toContain("chart.client.tsx:14:9");
|
|
75
|
+
expect(out).toContain("https://react.dev");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("truncates past the limit with a '... N more' tail", () => {
|
|
79
|
+
const diagnostics: ReactCompilerDiagnostic[] = Array.from(
|
|
80
|
+
{ length: 30 },
|
|
81
|
+
(_, i) => ({
|
|
82
|
+
file: `/app/x/c${i}.island.tsx`,
|
|
83
|
+
line: i + 1,
|
|
84
|
+
column: 1,
|
|
85
|
+
message: "bailout",
|
|
86
|
+
ruleId: "react-compiler/react-compiler",
|
|
87
|
+
severity: "warning" as const,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
const out = formatCompilerReport(diagnostics, { limit: 5 });
|
|
91
|
+
expect(out).toContain("30 bailout(s)");
|
|
92
|
+
expect(out).toContain("… and 25 more");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("uses relative paths when projectRoot is provided", () => {
|
|
96
|
+
const diagnostic: ReactCompilerDiagnostic = {
|
|
97
|
+
file: path.resolve("/project/app/todo.island.tsx"),
|
|
98
|
+
line: 1,
|
|
99
|
+
column: 1,
|
|
100
|
+
message: "bailout",
|
|
101
|
+
ruleId: "react-compiler/react-compiler",
|
|
102
|
+
severity: "warning",
|
|
103
|
+
};
|
|
104
|
+
const out = formatCompilerReport([diagnostic], {
|
|
105
|
+
projectRoot: path.resolve("/project"),
|
|
106
|
+
});
|
|
107
|
+
expect(out).toContain(path.join("app", "todo.island.tsx"));
|
|
108
|
+
expect(out).not.toContain(path.resolve("/project/app"));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bun bundler plugin — hard-fail on direct generated-artifact imports.
|
|
2
|
+
* Bun bundler plugin — hard-fail on direct generated-artifact imports.
|
|
3
3
|
*
|
|
4
4
|
* Background
|
|
5
5
|
* ──────────
|
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
* but it only runs when the user (or CI) invokes `mandu guard check`.
|
|
9
9
|
* Autonomous coding agents routinely bypass that step. This plugin closes
|
|
10
10
|
* the gap at the bundler level: every `mandu dev` / `mandu build` pass
|
|
11
|
-
* installs it by default, and any import whose specifier targets
|
|
12
|
-
* `__generated__` or `.mandu/generated` fails the build with a structured,
|
|
13
|
-
* actionable error.
|
|
11
|
+
* installs it by default, and any import whose specifier targets
|
|
12
|
+
* `__generated__` or `.mandu/generated` fails the build with a structured,
|
|
13
|
+
* actionable error.
|
|
14
14
|
*
|
|
15
15
|
* Design
|
|
16
16
|
* ──────
|
|
17
|
-
* - `onResolve({ filter: DEFAULT_BLOCK_FILTER })` — Bun hands us every
|
|
18
|
-
* import whose *specifier* matches the regex, along with the importer's
|
|
19
|
-
* path (`args.importer`). We never return a result; we always throw.
|
|
17
|
+
* - `onResolve({ filter: DEFAULT_BLOCK_FILTER })` — Bun hands us every
|
|
18
|
+
* import whose *specifier* matches the regex, along with the importer's
|
|
19
|
+
* path (`args.importer`). We never return a result; we always throw.
|
|
20
20
|
* - The error is `ForbiddenGeneratedImportError`, a named subclass of
|
|
21
21
|
* `Error`. Tests can `instanceof`-check; Bun surfaces `error.message` in
|
|
22
22
|
* its `result.logs` output for CLI display.
|
|
@@ -45,8 +45,9 @@
|
|
|
45
45
|
|
|
46
46
|
import type { BunPlugin } from "bun";
|
|
47
47
|
import {
|
|
48
|
-
buildForbiddenGeneratedImportMessage,
|
|
49
|
-
|
|
48
|
+
buildForbiddenGeneratedImportMessage,
|
|
49
|
+
FORBIDDEN_GENERATED_IMPORT_PATTERN,
|
|
50
|
+
FORBIDDEN_GENERATED_IMPORT_SUGGESTION,
|
|
50
51
|
GENERATED_IMPORT_DOCS_URL,
|
|
51
52
|
} from "../../guard/check";
|
|
52
53
|
|
|
@@ -91,8 +92,8 @@ export interface BlockGeneratedImportsOptions {
|
|
|
91
92
|
allowImporter?: (importerPath: string) => boolean;
|
|
92
93
|
/**
|
|
93
94
|
* Custom filter regex applied to the import specifier. Defaults to
|
|
94
|
-
* generated-artifact paths. Mandu ships a single default — exposing this
|
|
95
|
-
* for test harnesses that want to narrow or broaden the filter.
|
|
95
|
+
* generated-artifact paths. Mandu ships a single default — exposing this
|
|
96
|
+
* for test harnesses that want to narrow or broaden the filter.
|
|
96
97
|
*/
|
|
97
98
|
filter?: RegExp;
|
|
98
99
|
}
|
|
@@ -115,7 +116,7 @@ export function defaultAllowImporter(importerPath: string): boolean {
|
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/**
|
|
118
|
-
* Build a `BunPlugin` that blocks direct generated-artifact imports.
|
|
119
|
+
* Build a `BunPlugin` that blocks direct generated-artifact imports.
|
|
119
120
|
*
|
|
120
121
|
* Usage — call from `defaultBundlerPlugins(config)` (see `./index.ts`).
|
|
121
122
|
* Every `safeBuild` / `Bun.build` invocation in Mandu funnels through
|
|
@@ -124,7 +125,7 @@ export function defaultAllowImporter(importerPath: string): boolean {
|
|
|
124
125
|
export function blockGeneratedImports(
|
|
125
126
|
options: BlockGeneratedImportsOptions = {},
|
|
126
127
|
): BunPlugin {
|
|
127
|
-
const filter = options.filter ?? DEFAULT_BLOCK_FILTER;
|
|
128
|
+
const filter = options.filter ?? DEFAULT_BLOCK_FILTER;
|
|
128
129
|
const allowImporter = options.allowImporter ?? defaultAllowImporter;
|
|
129
130
|
|
|
130
131
|
return {
|
|
@@ -152,5 +153,5 @@ export function blockGeneratedImports(
|
|
|
152
153
|
};
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
/** Exported for unit-test convenience — keep the filter text assertable. */
|
|
156
|
-
export const DEFAULT_BLOCK_FILTER =
|
|
156
|
+
/** Exported for unit-test convenience — keep the filter text assertable. */
|
|
157
|
+
export const DEFAULT_BLOCK_FILTER = FORBIDDEN_GENERATED_IMPORT_PATTERN;
|