@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
package/src/plugins/define.ts
CHANGED
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ergonomic helper for authoring `ManduPlugin` objects — Phase 18.τ.
|
|
3
|
-
*
|
|
4
|
-
* Passes the plugin through unchanged while validating the shape at
|
|
5
|
-
* definition time. Fail-fast is critical: a plugin that silently
|
|
6
|
-
* corrupts the hook chain (e.g. typo on the hook name, non-function
|
|
7
|
-
* `setup`) is nearly impossible to diagnose from a 5xx log line.
|
|
8
|
-
*
|
|
9
|
-
* Importable from the `@mandujs/core/plugins/define` subpath so users
|
|
10
|
-
* who only want the lightweight plugin API do not pull the whole
|
|
11
|
-
* `@mandujs/core/plugins` surface (registry + categories + etc.) into
|
|
12
|
-
* their dependency graph.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```ts
|
|
16
|
-
* import { definePlugin } from "@mandujs/core/plugins/define";
|
|
17
|
-
*
|
|
18
|
-
* export const sitemap = definePlugin({
|
|
19
|
-
* name: "sitemap",
|
|
20
|
-
* hooks: {
|
|
21
|
-
* async onManifestBuilt(manifest) {
|
|
22
|
-
* await writeSitemap(manifest);
|
|
23
|
-
* },
|
|
24
|
-
* },
|
|
25
|
-
* });
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* @throws {TypeError} when `name` is missing / empty, `hooks` isn't an
|
|
29
|
-
* object, or `setup` isn't a function. These errors fire at import
|
|
30
|
-
* time so bad plugins never reach production.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
import type { ManduHooks, ManduPlugin } from "./hooks";
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Known hook names — used to catch `onRouteRegitered` style typos at
|
|
37
|
-
* definition time. Kept in a runtime-visible array so adding a hook to
|
|
38
|
-
* `ManduHooks` only requires one diff instead of two.
|
|
39
|
-
*/
|
|
40
|
-
const KNOWN_HOOK_NAMES: ReadonlyArray<keyof ManduHooks> = [
|
|
41
|
-
// Legacy lifecycle
|
|
42
|
-
"onBeforeBuild",
|
|
43
|
-
"onAfterBuild",
|
|
44
|
-
"onDevStart",
|
|
45
|
-
"onDevStop",
|
|
46
|
-
"onRouteChange",
|
|
47
|
-
"onBeforeStart",
|
|
48
|
-
// Phase 18.τ
|
|
49
|
-
"onRouteRegistered",
|
|
50
|
-
"onManifestBuilt",
|
|
51
|
-
"defineBundlerPlugin",
|
|
52
|
-
"onBundleComplete",
|
|
53
|
-
"definePrerenderHook",
|
|
54
|
-
"defineMiddlewareChain",
|
|
55
|
-
"defineTestTransform",
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Validate + pass through a `ManduPlugin`. Use at export time:
|
|
60
|
-
*
|
|
61
|
-
* ```ts
|
|
62
|
-
* export default definePlugin({ name: "my-plugin", hooks: { ... } });
|
|
63
|
-
* ```
|
|
64
|
-
*
|
|
65
|
-
* The returned value is `===` the input — the helper does not clone,
|
|
66
|
-
* wrap, or freeze the object, so users retain full control over the
|
|
67
|
-
* plugin instance (e.g. swapping hooks at runtime for tests).
|
|
68
|
-
*/
|
|
69
|
-
export function definePlugin(plugin: ManduPlugin): ManduPlugin {
|
|
70
|
-
if (!plugin || typeof plugin !== "object") {
|
|
71
|
-
throw new TypeError(
|
|
72
|
-
"[Mandu Plugin] definePlugin() requires a plugin object"
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
if (typeof plugin.name !== "string" || plugin.name.length === 0) {
|
|
76
|
-
throw new TypeError(
|
|
77
|
-
"[Mandu Plugin] definePlugin() requires a non-empty `name` string"
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (plugin.hooks !== undefined) {
|
|
82
|
-
if (typeof plugin.hooks !== "object" || plugin.hooks === null) {
|
|
83
|
-
throw new TypeError(
|
|
84
|
-
`[Mandu Plugin "${plugin.name}"] hooks must be an object`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
for (const key of Object.keys(plugin.hooks)) {
|
|
88
|
-
const asHookKey = key as keyof ManduHooks;
|
|
89
|
-
if (!KNOWN_HOOK_NAMES.includes(asHookKey)) {
|
|
90
|
-
throw new TypeError(
|
|
91
|
-
`[Mandu Plugin "${plugin.name}"] unknown hook "${key}". Known hooks: ${KNOWN_HOOK_NAMES.join(", ")}`
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
const fn = (plugin.hooks as Record<string, unknown>)[key];
|
|
95
|
-
if (fn !== undefined && typeof fn !== "function") {
|
|
96
|
-
throw new TypeError(
|
|
97
|
-
`[Mandu Plugin "${plugin.name}"] hook "${key}" must be a function (got ${typeof fn})`
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (plugin.setup !== undefined && typeof plugin.setup !== "function") {
|
|
104
|
-
throw new TypeError(
|
|
105
|
-
`[Mandu Plugin "${plugin.name}"] setup must be a function when provided (got ${typeof plugin.setup})`
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return plugin;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Type guard for runtime checks (e.g. in `validate.ts`).
|
|
114
|
-
*/
|
|
115
|
-
export function isManduPlugin(value: unknown): value is ManduPlugin {
|
|
116
|
-
if (!value || typeof value !== "object") return false;
|
|
117
|
-
const v = value as Record<string, unknown>;
|
|
118
|
-
if (typeof v.name !== "string" || v.name.length === 0) return false;
|
|
119
|
-
if (v.hooks !== undefined && (typeof v.hooks !== "object" || v.hooks === null)) {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
if (v.setup !== undefined && typeof v.setup !== "function") return false;
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Ergonomic helper for authoring `ManduPlugin` objects — Phase 18.τ.
|
|
3
|
+
*
|
|
4
|
+
* Passes the plugin through unchanged while validating the shape at
|
|
5
|
+
* definition time. Fail-fast is critical: a plugin that silently
|
|
6
|
+
* corrupts the hook chain (e.g. typo on the hook name, non-function
|
|
7
|
+
* `setup`) is nearly impossible to diagnose from a 5xx log line.
|
|
8
|
+
*
|
|
9
|
+
* Importable from the `@mandujs/core/plugins/define` subpath so users
|
|
10
|
+
* who only want the lightweight plugin API do not pull the whole
|
|
11
|
+
* `@mandujs/core/plugins` surface (registry + categories + etc.) into
|
|
12
|
+
* their dependency graph.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { definePlugin } from "@mandujs/core/compat/plugins/define";
|
|
17
|
+
*
|
|
18
|
+
* export const sitemap = definePlugin({
|
|
19
|
+
* name: "sitemap",
|
|
20
|
+
* hooks: {
|
|
21
|
+
* async onManifestBuilt(manifest) {
|
|
22
|
+
* await writeSitemap(manifest);
|
|
23
|
+
* },
|
|
24
|
+
* },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @throws {TypeError} when `name` is missing / empty, `hooks` isn't an
|
|
29
|
+
* object, or `setup` isn't a function. These errors fire at import
|
|
30
|
+
* time so bad plugins never reach production.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { ManduHooks, ManduPlugin } from "./hooks";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Known hook names — used to catch `onRouteRegitered` style typos at
|
|
37
|
+
* definition time. Kept in a runtime-visible array so adding a hook to
|
|
38
|
+
* `ManduHooks` only requires one diff instead of two.
|
|
39
|
+
*/
|
|
40
|
+
const KNOWN_HOOK_NAMES: ReadonlyArray<keyof ManduHooks> = [
|
|
41
|
+
// Legacy lifecycle
|
|
42
|
+
"onBeforeBuild",
|
|
43
|
+
"onAfterBuild",
|
|
44
|
+
"onDevStart",
|
|
45
|
+
"onDevStop",
|
|
46
|
+
"onRouteChange",
|
|
47
|
+
"onBeforeStart",
|
|
48
|
+
// Phase 18.τ
|
|
49
|
+
"onRouteRegistered",
|
|
50
|
+
"onManifestBuilt",
|
|
51
|
+
"defineBundlerPlugin",
|
|
52
|
+
"onBundleComplete",
|
|
53
|
+
"definePrerenderHook",
|
|
54
|
+
"defineMiddlewareChain",
|
|
55
|
+
"defineTestTransform",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate + pass through a `ManduPlugin`. Use at export time:
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* export default definePlugin({ name: "my-plugin", hooks: { ... } });
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* The returned value is `===` the input — the helper does not clone,
|
|
66
|
+
* wrap, or freeze the object, so users retain full control over the
|
|
67
|
+
* plugin instance (e.g. swapping hooks at runtime for tests).
|
|
68
|
+
*/
|
|
69
|
+
export function definePlugin(plugin: ManduPlugin): ManduPlugin {
|
|
70
|
+
if (!plugin || typeof plugin !== "object") {
|
|
71
|
+
throw new TypeError(
|
|
72
|
+
"[Mandu Plugin] definePlugin() requires a plugin object"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (typeof plugin.name !== "string" || plugin.name.length === 0) {
|
|
76
|
+
throw new TypeError(
|
|
77
|
+
"[Mandu Plugin] definePlugin() requires a non-empty `name` string"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (plugin.hooks !== undefined) {
|
|
82
|
+
if (typeof plugin.hooks !== "object" || plugin.hooks === null) {
|
|
83
|
+
throw new TypeError(
|
|
84
|
+
`[Mandu Plugin "${plugin.name}"] hooks must be an object`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
for (const key of Object.keys(plugin.hooks)) {
|
|
88
|
+
const asHookKey = key as keyof ManduHooks;
|
|
89
|
+
if (!KNOWN_HOOK_NAMES.includes(asHookKey)) {
|
|
90
|
+
throw new TypeError(
|
|
91
|
+
`[Mandu Plugin "${plugin.name}"] unknown hook "${key}". Known hooks: ${KNOWN_HOOK_NAMES.join(", ")}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const fn = (plugin.hooks as Record<string, unknown>)[key];
|
|
95
|
+
if (fn !== undefined && typeof fn !== "function") {
|
|
96
|
+
throw new TypeError(
|
|
97
|
+
`[Mandu Plugin "${plugin.name}"] hook "${key}" must be a function (got ${typeof fn})`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (plugin.setup !== undefined && typeof plugin.setup !== "function") {
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
`[Mandu Plugin "${plugin.name}"] setup must be a function when provided (got ${typeof plugin.setup})`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return plugin;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Type guard for runtime checks (e.g. in `validate.ts`).
|
|
114
|
+
*/
|
|
115
|
+
export function isManduPlugin(value: unknown): value is ManduPlugin {
|
|
116
|
+
if (!value || typeof value !== "object") return false;
|
|
117
|
+
const v = value as Record<string, unknown>;
|
|
118
|
+
if (typeof v.name !== "string" || v.name.length === 0) return false;
|
|
119
|
+
if (v.hooks !== undefined && (typeof v.hooks !== "object" || v.hooks === null)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (v.setup !== undefined && typeof v.setup !== "function") return false;
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Example plugin: dependency gate.
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates `onRouteRegistered` — we read each route's source file
|
|
5
|
-
* and warn (or throw) when it imports from a forbidden package. Useful
|
|
6
|
-
* for keeping e.g. `node:fs` out of edge-bound routes.
|
|
7
|
-
*
|
|
8
|
-
* Educational only. A production-ready version would use the module
|
|
9
|
-
* graph from the bundler rather than a line-wise regex scan.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```ts
|
|
13
|
-
* export default {
|
|
14
|
-
* plugins: [
|
|
15
|
-
* depCheckPlugin({
|
|
16
|
-
* forbidden: ["node:fs", "node:child_process"],
|
|
17
|
-
* severity: "error",
|
|
18
|
-
* }),
|
|
19
|
-
* ],
|
|
20
|
-
* } satisfies ManduConfig;
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { readFile } from "fs/promises";
|
|
25
|
-
import path from "path";
|
|
26
|
-
import { definePlugin } from "../define";
|
|
27
|
-
import type { ManduPlugin } from "../hooks";
|
|
28
|
-
|
|
29
|
-
export interface DepCheckPluginOptions {
|
|
30
|
-
/** Bare-specifier prefixes that routes must NOT import. */
|
|
31
|
-
forbidden: readonly string[];
|
|
32
|
-
/**
|
|
33
|
-
* How to react to a violation:
|
|
34
|
-
* - `"warn"` (default) — log to stderr.
|
|
35
|
-
* - `"error"` — throw. The runner captures the error per-route so
|
|
36
|
-
* other plugins still run, but the CLI sees a non-empty errors
|
|
37
|
-
* array in `FSGenerateResult.warnings`.
|
|
38
|
-
*/
|
|
39
|
-
severity?: "warn" | "error";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function depCheckPlugin(options: DepCheckPluginOptions): ManduPlugin {
|
|
43
|
-
const { forbidden, severity = "warn" } = options;
|
|
44
|
-
|
|
45
|
-
return definePlugin({
|
|
46
|
-
name: "dep-check",
|
|
47
|
-
hooks: {
|
|
48
|
-
async onRouteRegistered(route) {
|
|
49
|
-
const modulePath = path.resolve(process.cwd(), route.module);
|
|
50
|
-
let source: string;
|
|
51
|
-
try {
|
|
52
|
-
source = await readFile(modulePath, "utf-8");
|
|
53
|
-
} catch {
|
|
54
|
-
return; // Module not readable — not a gate concern.
|
|
55
|
-
}
|
|
56
|
-
const offenders: string[] = [];
|
|
57
|
-
const importRegex =
|
|
58
|
-
/(?:import|from|require)\s*\(?\s*["']([^"']+)["']/g;
|
|
59
|
-
for (const match of source.matchAll(importRegex)) {
|
|
60
|
-
const specifier = match[1];
|
|
61
|
-
for (const f of forbidden) {
|
|
62
|
-
if (specifier === f || specifier.startsWith(`${f}/`)) {
|
|
63
|
-
offenders.push(specifier);
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (offenders.length === 0) return;
|
|
69
|
-
|
|
70
|
-
const message =
|
|
71
|
-
`[dep-check] route ${route.id} (${route.module}) imports forbidden ` +
|
|
72
|
-
`specifiers: ${offenders.join(", ")}`;
|
|
73
|
-
if (severity === "error") {
|
|
74
|
-
throw new Error(message);
|
|
75
|
-
}
|
|
76
|
-
console.warn(message);
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Example plugin: dependency gate.
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates `onRouteRegistered` — we read each route's source file
|
|
5
|
+
* and warn (or throw) when it imports from a forbidden package. Useful
|
|
6
|
+
* for keeping e.g. `node:fs` out of edge-bound routes.
|
|
7
|
+
*
|
|
8
|
+
* Educational only. A production-ready version would use the module
|
|
9
|
+
* graph from the bundler rather than a line-wise regex scan.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* export default {
|
|
14
|
+
* plugins: [
|
|
15
|
+
* depCheckPlugin({
|
|
16
|
+
* forbidden: ["node:fs", "node:child_process"],
|
|
17
|
+
* severity: "error",
|
|
18
|
+
* }),
|
|
19
|
+
* ],
|
|
20
|
+
* } satisfies ManduConfig;
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFile } from "fs/promises";
|
|
25
|
+
import path from "path";
|
|
26
|
+
import { definePlugin } from "../define";
|
|
27
|
+
import type { ManduPlugin } from "../hooks";
|
|
28
|
+
|
|
29
|
+
export interface DepCheckPluginOptions {
|
|
30
|
+
/** Bare-specifier prefixes that routes must NOT import. */
|
|
31
|
+
forbidden: readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* How to react to a violation:
|
|
34
|
+
* - `"warn"` (default) — log to stderr.
|
|
35
|
+
* - `"error"` — throw. The runner captures the error per-route so
|
|
36
|
+
* other plugins still run, but the CLI sees a non-empty errors
|
|
37
|
+
* array in `FSGenerateResult.warnings`.
|
|
38
|
+
*/
|
|
39
|
+
severity?: "warn" | "error";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function depCheckPlugin(options: DepCheckPluginOptions): ManduPlugin {
|
|
43
|
+
const { forbidden, severity = "warn" } = options;
|
|
44
|
+
|
|
45
|
+
return definePlugin({
|
|
46
|
+
name: "dep-check",
|
|
47
|
+
hooks: {
|
|
48
|
+
async onRouteRegistered(route) {
|
|
49
|
+
const modulePath = path.resolve(process.cwd(), route.module);
|
|
50
|
+
let source: string;
|
|
51
|
+
try {
|
|
52
|
+
source = await readFile(modulePath, "utf-8");
|
|
53
|
+
} catch {
|
|
54
|
+
return; // Module not readable — not a gate concern.
|
|
55
|
+
}
|
|
56
|
+
const offenders: string[] = [];
|
|
57
|
+
const importRegex =
|
|
58
|
+
/(?:import|from|require)\s*\(?\s*["']([^"']+)["']/g;
|
|
59
|
+
for (const match of source.matchAll(importRegex)) {
|
|
60
|
+
const specifier = match[1];
|
|
61
|
+
for (const f of forbidden) {
|
|
62
|
+
if (specifier === f || specifier.startsWith(`${f}/`)) {
|
|
63
|
+
offenders.push(specifier);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (offenders.length === 0) return;
|
|
69
|
+
|
|
70
|
+
const message =
|
|
71
|
+
`[dep-check] route ${route.id} (${route.module}) imports forbidden ` +
|
|
72
|
+
`specifiers: ${offenders.join(", ")}`;
|
|
73
|
+
if (severity === "error") {
|
|
74
|
+
throw new Error(message);
|
|
75
|
+
}
|
|
76
|
+
console.warn(message);
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Example plugin: prerender output cache.
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates `definePrerenderHook` — we keep a disk-backed cache of
|
|
5
|
-
* previously rendered pages keyed by pathname + content hash. When the
|
|
6
|
-
* next build encounters an unchanged pathname, we short-circuit with
|
|
7
|
-
* the cached HTML. The bundler's fetch handler is still invoked (we
|
|
8
|
-
* don't get a chance to intercept BEFORE), but we demonstrate how a
|
|
9
|
-
* plugin might use the override return to swap the eventual output.
|
|
10
|
-
*
|
|
11
|
-
* Educational only — a production cache would key off upstream content
|
|
12
|
-
* (markdown source, database row, etc.) rather than the rendered
|
|
13
|
-
* HTML hash. Useful as a scaffold for "only rerender changed pages"
|
|
14
|
-
* strategies.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```ts
|
|
18
|
-
* import { prerenderCachePlugin } from "@mandujs/core/plugins/examples/prerender-cache-plugin";
|
|
19
|
-
*
|
|
20
|
-
* export default {
|
|
21
|
-
* plugins: [prerenderCachePlugin({ cacheFile: ".mandu/prerender-cache.json" })],
|
|
22
|
-
* } satisfies ManduConfig;
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
27
|
-
import path from "path";
|
|
28
|
-
import { createHash } from "node:crypto";
|
|
29
|
-
import { definePlugin } from "../define";
|
|
30
|
-
import type { ManduPlugin } from "../hooks";
|
|
31
|
-
|
|
32
|
-
export interface PrerenderCachePluginOptions {
|
|
33
|
-
/** Absolute / cwd-relative cache file path. Default `".mandu/prerender-cache.json"`. */
|
|
34
|
-
cacheFile?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface CacheEntry {
|
|
38
|
-
hash: string;
|
|
39
|
-
html: string;
|
|
40
|
-
}
|
|
41
|
-
type CacheShape = Record<string, CacheEntry>;
|
|
42
|
-
|
|
43
|
-
function hashContent(s: string): string {
|
|
44
|
-
return createHash("sha256").update(s).digest("hex").slice(0, 16);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function loadCache(file: string): Promise<CacheShape> {
|
|
48
|
-
try {
|
|
49
|
-
const raw = await readFile(file, "utf-8");
|
|
50
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
51
|
-
return parsed && typeof parsed === "object"
|
|
52
|
-
? (parsed as CacheShape)
|
|
53
|
-
: {};
|
|
54
|
-
} catch {
|
|
55
|
-
return {};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function saveCache(file: string, data: CacheShape): Promise<void> {
|
|
60
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
61
|
-
await writeFile(file, JSON.stringify(data, null, 2), "utf-8");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function prerenderCachePlugin(
|
|
65
|
-
options: PrerenderCachePluginOptions = {}
|
|
66
|
-
): ManduPlugin {
|
|
67
|
-
const cacheFile = path.resolve(
|
|
68
|
-
process.cwd(),
|
|
69
|
-
options.cacheFile ?? ".mandu/prerender-cache.json"
|
|
70
|
-
);
|
|
71
|
-
// Cache is loaded lazily on first hit so plugins don't pay the I/O
|
|
72
|
-
// cost when nobody prerenders.
|
|
73
|
-
let cache: CacheShape | null = null;
|
|
74
|
-
// Track the set of pathnames we've touched this build so we can
|
|
75
|
-
// prune stale entries on `onBundleComplete`.
|
|
76
|
-
const touched = new Set<string>();
|
|
77
|
-
|
|
78
|
-
return definePlugin({
|
|
79
|
-
name: "prerender-cache",
|
|
80
|
-
hooks: {
|
|
81
|
-
async definePrerenderHook(ctx) {
|
|
82
|
-
if (!cache) cache = await loadCache(cacheFile);
|
|
83
|
-
const hash = hashContent(ctx.html);
|
|
84
|
-
const prev = cache[ctx.pathname];
|
|
85
|
-
touched.add(ctx.pathname);
|
|
86
|
-
if (prev && prev.hash === hash) {
|
|
87
|
-
// Content unchanged — no override needed, caller writes the
|
|
88
|
-
// same bytes. We could return `{ html: prev.html }` if we
|
|
89
|
-
// wanted to guarantee byte-identical output even if the
|
|
90
|
-
// renderer adds a timestamp.
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
cache[ctx.pathname] = { hash, html: ctx.html };
|
|
94
|
-
await saveCache(cacheFile, cache);
|
|
95
|
-
return undefined;
|
|
96
|
-
},
|
|
97
|
-
async onBundleComplete() {
|
|
98
|
-
// Prune entries that weren't touched this build.
|
|
99
|
-
if (!cache) return;
|
|
100
|
-
let changed = false;
|
|
101
|
-
for (const key of Object.keys(cache)) {
|
|
102
|
-
if (!touched.has(key)) {
|
|
103
|
-
delete cache[key];
|
|
104
|
-
changed = true;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (changed) await saveCache(cacheFile, cache);
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Example plugin: prerender output cache.
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates `definePrerenderHook` — we keep a disk-backed cache of
|
|
5
|
+
* previously rendered pages keyed by pathname + content hash. When the
|
|
6
|
+
* next build encounters an unchanged pathname, we short-circuit with
|
|
7
|
+
* the cached HTML. The bundler's fetch handler is still invoked (we
|
|
8
|
+
* don't get a chance to intercept BEFORE), but we demonstrate how a
|
|
9
|
+
* plugin might use the override return to swap the eventual output.
|
|
10
|
+
*
|
|
11
|
+
* Educational only — a production cache would key off upstream content
|
|
12
|
+
* (markdown source, database row, etc.) rather than the rendered
|
|
13
|
+
* HTML hash. Useful as a scaffold for "only rerender changed pages"
|
|
14
|
+
* strategies.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { prerenderCachePlugin } from "@mandujs/core/compat/plugins/examples/prerender-cache-plugin";
|
|
19
|
+
*
|
|
20
|
+
* export default {
|
|
21
|
+
* plugins: [prerenderCachePlugin({ cacheFile: ".mandu/prerender-cache.json" })],
|
|
22
|
+
* } satisfies ManduConfig;
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
27
|
+
import path from "path";
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
29
|
+
import { definePlugin } from "../define";
|
|
30
|
+
import type { ManduPlugin } from "../hooks";
|
|
31
|
+
|
|
32
|
+
export interface PrerenderCachePluginOptions {
|
|
33
|
+
/** Absolute / cwd-relative cache file path. Default `".mandu/prerender-cache.json"`. */
|
|
34
|
+
cacheFile?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface CacheEntry {
|
|
38
|
+
hash: string;
|
|
39
|
+
html: string;
|
|
40
|
+
}
|
|
41
|
+
type CacheShape = Record<string, CacheEntry>;
|
|
42
|
+
|
|
43
|
+
function hashContent(s: string): string {
|
|
44
|
+
return createHash("sha256").update(s).digest("hex").slice(0, 16);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function loadCache(file: string): Promise<CacheShape> {
|
|
48
|
+
try {
|
|
49
|
+
const raw = await readFile(file, "utf-8");
|
|
50
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
51
|
+
return parsed && typeof parsed === "object"
|
|
52
|
+
? (parsed as CacheShape)
|
|
53
|
+
: {};
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function saveCache(file: string, data: CacheShape): Promise<void> {
|
|
60
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
61
|
+
await writeFile(file, JSON.stringify(data, null, 2), "utf-8");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function prerenderCachePlugin(
|
|
65
|
+
options: PrerenderCachePluginOptions = {}
|
|
66
|
+
): ManduPlugin {
|
|
67
|
+
const cacheFile = path.resolve(
|
|
68
|
+
process.cwd(),
|
|
69
|
+
options.cacheFile ?? ".mandu/prerender-cache.json"
|
|
70
|
+
);
|
|
71
|
+
// Cache is loaded lazily on first hit so plugins don't pay the I/O
|
|
72
|
+
// cost when nobody prerenders.
|
|
73
|
+
let cache: CacheShape | null = null;
|
|
74
|
+
// Track the set of pathnames we've touched this build so we can
|
|
75
|
+
// prune stale entries on `onBundleComplete`.
|
|
76
|
+
const touched = new Set<string>();
|
|
77
|
+
|
|
78
|
+
return definePlugin({
|
|
79
|
+
name: "prerender-cache",
|
|
80
|
+
hooks: {
|
|
81
|
+
async definePrerenderHook(ctx) {
|
|
82
|
+
if (!cache) cache = await loadCache(cacheFile);
|
|
83
|
+
const hash = hashContent(ctx.html);
|
|
84
|
+
const prev = cache[ctx.pathname];
|
|
85
|
+
touched.add(ctx.pathname);
|
|
86
|
+
if (prev && prev.hash === hash) {
|
|
87
|
+
// Content unchanged — no override needed, caller writes the
|
|
88
|
+
// same bytes. We could return `{ html: prev.html }` if we
|
|
89
|
+
// wanted to guarantee byte-identical output even if the
|
|
90
|
+
// renderer adds a timestamp.
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
cache[ctx.pathname] = { hash, html: ctx.html };
|
|
94
|
+
await saveCache(cacheFile, cache);
|
|
95
|
+
return undefined;
|
|
96
|
+
},
|
|
97
|
+
async onBundleComplete() {
|
|
98
|
+
// Prune entries that weren't touched this build.
|
|
99
|
+
if (!cache) return;
|
|
100
|
+
let changed = false;
|
|
101
|
+
for (const key of Object.keys(cache)) {
|
|
102
|
+
if (!touched.has(key)) {
|
|
103
|
+
delete cache[key];
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (changed) await saveCache(cacheFile, cache);
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|