@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
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic client-build generations.
|
|
3
|
+
*
|
|
4
|
+
* A build never writes into the directory currently served by the runtime.
|
|
5
|
+
* It starts from a snapshot of the active generation, builds in staging, and
|
|
6
|
+
* publishes by replacing one small pointer file only after every bundle and
|
|
7
|
+
* the bundle manifest are valid.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from "path";
|
|
11
|
+
import fs from "fs/promises";
|
|
12
|
+
import type { BundleManifest } from "./types";
|
|
13
|
+
|
|
14
|
+
const SCHEMA_VERSION = 1 as const;
|
|
15
|
+
const GENERATIONS_DIR = "generations";
|
|
16
|
+
const ACTIVE_POINTER_FILE = "active-generation.json";
|
|
17
|
+
const BUILD_STATE_FILE = "build-state.json";
|
|
18
|
+
const STAGING_DIR = path.join(".staging", "client-builds");
|
|
19
|
+
const GENERATION_ID_PATTERN = /^[a-z0-9][a-z0-9-]{5,127}$/;
|
|
20
|
+
const GENERATIONS_TO_KEEP = 3;
|
|
21
|
+
const WINDOWS_RENAME_RETRY_DELAYS_MS = [10, 25, 50, 100] as const;
|
|
22
|
+
const WINDOWS_TRANSIENT_RENAME_CODES = new Set(["EACCES", "EBUSY", "ENOTEMPTY", "EPERM"]);
|
|
23
|
+
|
|
24
|
+
export interface BuildGenerationPointer {
|
|
25
|
+
schemaVersion: typeof SCHEMA_VERSION;
|
|
26
|
+
generationId: string;
|
|
27
|
+
publishedAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BuildGenerationAttempt {
|
|
31
|
+
schemaVersion: typeof SCHEMA_VERSION;
|
|
32
|
+
activeGenerationId: string | null;
|
|
33
|
+
lastAttempt: {
|
|
34
|
+
generationId: string;
|
|
35
|
+
status: "building" | "failed" | "published";
|
|
36
|
+
startedAt: string;
|
|
37
|
+
completedAt?: string;
|
|
38
|
+
errors?: string[];
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BuildGeneration {
|
|
43
|
+
id: string;
|
|
44
|
+
rootDir: string;
|
|
45
|
+
startedAt: string;
|
|
46
|
+
stagingRoot: string;
|
|
47
|
+
clientDir: string;
|
|
48
|
+
manifestPath: string;
|
|
49
|
+
finalRoot: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ActiveBuildArtifacts {
|
|
53
|
+
generationId: string | null;
|
|
54
|
+
clientDir: string;
|
|
55
|
+
manifestPath: string;
|
|
56
|
+
source: "generation" | "legacy";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface BuildGenerationPromotionOperations {
|
|
60
|
+
rename(source: string, destination: string): Promise<void>;
|
|
61
|
+
copy(source: string, destination: string): Promise<void>;
|
|
62
|
+
remove(target: string): Promise<void>;
|
|
63
|
+
sleep(milliseconds: number): Promise<void>;
|
|
64
|
+
platform: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const activeArtifactsCache = new Map<string, ActiveBuildArtifacts>();
|
|
68
|
+
const buildQueues = new Map<string, Promise<void>>();
|
|
69
|
+
|
|
70
|
+
function manduDir(rootDir: string): string {
|
|
71
|
+
return path.join(path.resolve(rootDir), ".mandu");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function activePointerPath(rootDir: string): string {
|
|
75
|
+
return path.join(manduDir(rootDir), ACTIVE_POINTER_FILE);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildStatePath(rootDir: string): string {
|
|
79
|
+
return path.join(manduDir(rootDir), BUILD_STATE_FILE);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function generationRoot(rootDir: string, generationId: string): string {
|
|
83
|
+
return path.join(manduDir(rootDir), GENERATIONS_DIR, generationId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createGenerationId(): string {
|
|
87
|
+
const timestamp = Date.now().toString(36);
|
|
88
|
+
const pid = process.pid.toString(36);
|
|
89
|
+
const nonce = crypto.randomUUID().replace(/-/g, "").slice(0, 12);
|
|
90
|
+
return `${timestamp}-${pid}-${nonce}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isMissing(error: unknown): boolean {
|
|
94
|
+
return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function pathExists(target: string): Promise<boolean> {
|
|
98
|
+
try {
|
|
99
|
+
await fs.access(target);
|
|
100
|
+
return true;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isTransientWindowsRenameError(error: unknown): boolean {
|
|
107
|
+
return (
|
|
108
|
+
!!error &&
|
|
109
|
+
typeof error === "object" &&
|
|
110
|
+
"code" in error &&
|
|
111
|
+
typeof error.code === "string" &&
|
|
112
|
+
WINDOWS_TRANSIENT_RENAME_CODES.has(error.code)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function promoteStagedBuildGeneration(
|
|
117
|
+
generation: BuildGeneration,
|
|
118
|
+
overrides: Partial<BuildGenerationPromotionOperations> = {},
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const operations: BuildGenerationPromotionOperations = {
|
|
121
|
+
rename: (source, destination) => fs.rename(source, destination),
|
|
122
|
+
copy: (source, destination) =>
|
|
123
|
+
fs.cp(source, destination, {
|
|
124
|
+
recursive: true,
|
|
125
|
+
force: false,
|
|
126
|
+
errorOnExist: true,
|
|
127
|
+
}),
|
|
128
|
+
remove: (target) => fs.rm(target, { recursive: true, force: true }),
|
|
129
|
+
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
130
|
+
platform: process.platform,
|
|
131
|
+
...overrides,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
for (let attempt = 0; ; attempt++) {
|
|
135
|
+
try {
|
|
136
|
+
await operations.rename(generation.stagingRoot, generation.finalRoot);
|
|
137
|
+
return;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const retryDelay = WINDOWS_RENAME_RETRY_DELAYS_MS[attempt];
|
|
140
|
+
if (
|
|
141
|
+
operations.platform !== "win32" ||
|
|
142
|
+
!isTransientWindowsRenameError(error) ||
|
|
143
|
+
retryDelay === undefined
|
|
144
|
+
) {
|
|
145
|
+
if (
|
|
146
|
+
operations.platform !== "win32" ||
|
|
147
|
+
!isTransientWindowsRenameError(error)
|
|
148
|
+
) {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
await operations.sleep(retryDelay);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Bun 1.4 on Windows can retain build-output handles briefly after Bun.build(),
|
|
158
|
+
// preventing a directory rename. The final directory is not observable until
|
|
159
|
+
// the active pointer is updated, so a validated copy preserves publication
|
|
160
|
+
// atomicity while avoiding a runtime-specific handle race.
|
|
161
|
+
try {
|
|
162
|
+
await operations.copy(generation.stagingRoot, generation.finalRoot);
|
|
163
|
+
const finalClient = path.join(generation.finalRoot, "client");
|
|
164
|
+
const finalManifest = path.join(generation.finalRoot, "manifest.json");
|
|
165
|
+
if (!(await pathExists(finalClient)) || !(await pathExists(finalManifest))) {
|
|
166
|
+
throw new Error(`Copied build generation ${generation.id} is incomplete.`);
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
await operations.remove(generation.finalRoot).catch(() => {});
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await operations.remove(generation.stagingRoot).catch(() => {});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function __promoteStagedBuildGenerationForTests(
|
|
177
|
+
generation: BuildGeneration,
|
|
178
|
+
overrides: Partial<BuildGenerationPromotionOperations> = {},
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
await promoteStagedBuildGeneration(generation, overrides);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function atomicWriteFile(target: string, content: string): Promise<void> {
|
|
184
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
185
|
+
const token = `${process.pid}-${crypto.randomUUID().slice(0, 8)}`;
|
|
186
|
+
const temporary = `${target}.${token}.tmp`;
|
|
187
|
+
const backup = `${target}.${token}.bak`;
|
|
188
|
+
await fs.writeFile(temporary, content, "utf8");
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await fs.rename(temporary, target);
|
|
192
|
+
return;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// Some Windows filesystems do not replace an existing destination via
|
|
195
|
+
// rename. Fall back to a guarded old -> backup -> new handoff.
|
|
196
|
+
if (!(await pathExists(target))) {
|
|
197
|
+
await fs.rm(temporary, { force: true }).catch(() => {});
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await fs.rename(target, backup);
|
|
203
|
+
try {
|
|
204
|
+
await fs.rename(temporary, target);
|
|
205
|
+
await fs.rm(backup, { force: true });
|
|
206
|
+
} catch (error) {
|
|
207
|
+
await fs.rm(target, { force: true }).catch(() => {});
|
|
208
|
+
await fs.rename(backup, target).catch(() => {});
|
|
209
|
+
await fs.rm(temporary, { force: true }).catch(() => {});
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function atomicWriteJson(target: string, value: unknown): Promise<void> {
|
|
215
|
+
await atomicWriteFile(target, `${JSON.stringify(value, null, 2)}\n`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parsePointer(raw: string): BuildGenerationPointer | null {
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(raw) as Partial<BuildGenerationPointer>;
|
|
221
|
+
if (
|
|
222
|
+
parsed.schemaVersion !== SCHEMA_VERSION ||
|
|
223
|
+
typeof parsed.generationId !== "string" ||
|
|
224
|
+
!GENERATION_ID_PATTERN.test(parsed.generationId) ||
|
|
225
|
+
typeof parsed.publishedAt !== "string"
|
|
226
|
+
) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return parsed as BuildGenerationPointer;
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function readActiveBuildGeneration(
|
|
236
|
+
rootDir: string,
|
|
237
|
+
): Promise<BuildGenerationPointer | null> {
|
|
238
|
+
try {
|
|
239
|
+
const raw = await fs.readFile(activePointerPath(rootDir), "utf8");
|
|
240
|
+
return parsePointer(raw);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (!isMissing(error)) {
|
|
243
|
+
console.warn(
|
|
244
|
+
`[Mandu] Could not read active build generation: ${
|
|
245
|
+
error instanceof Error ? error.message : String(error)
|
|
246
|
+
}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function resolveActiveBuildArtifacts(
|
|
254
|
+
rootDir: string,
|
|
255
|
+
): Promise<ActiveBuildArtifacts> {
|
|
256
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
257
|
+
const cached = activeArtifactsCache.get(resolvedRoot);
|
|
258
|
+
const pointer = await readActiveBuildGeneration(resolvedRoot);
|
|
259
|
+
if (cached && cached.generationId === (pointer?.generationId ?? null)) return cached;
|
|
260
|
+
if (pointer) {
|
|
261
|
+
const root = generationRoot(resolvedRoot, pointer.generationId);
|
|
262
|
+
const clientDir = path.join(root, "client");
|
|
263
|
+
const manifestPath = path.join(root, "manifest.json");
|
|
264
|
+
if ((await pathExists(clientDir)) && (await pathExists(manifestPath))) {
|
|
265
|
+
const artifacts: ActiveBuildArtifacts = {
|
|
266
|
+
generationId: pointer.generationId,
|
|
267
|
+
clientDir,
|
|
268
|
+
manifestPath,
|
|
269
|
+
source: "generation",
|
|
270
|
+
};
|
|
271
|
+
activeArtifactsCache.set(resolvedRoot, artifacts);
|
|
272
|
+
return artifacts;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const fallback: ActiveBuildArtifacts = {
|
|
277
|
+
generationId: null,
|
|
278
|
+
clientDir: path.join(manduDir(resolvedRoot), "client"),
|
|
279
|
+
manifestPath: path.join(manduDir(resolvedRoot), "manifest.json"),
|
|
280
|
+
source: "legacy",
|
|
281
|
+
};
|
|
282
|
+
activeArtifactsCache.set(resolvedRoot, fallback);
|
|
283
|
+
return fallback;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function resolveActiveClientDirectory(rootDir: string): Promise<string> {
|
|
287
|
+
return (await resolveActiveBuildArtifacts(rootDir)).clientDir;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function resolveBuildGenerationClientDirectory(
|
|
291
|
+
rootDir: string,
|
|
292
|
+
generationId: string,
|
|
293
|
+
): Promise<string | null> {
|
|
294
|
+
if (!GENERATION_ID_PATTERN.test(generationId)) return null;
|
|
295
|
+
const clientDir = path.join(generationRoot(rootDir, generationId), "client");
|
|
296
|
+
return (await pathExists(clientDir)) ? clientDir : null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function scopeClientAssetUrl(url: string, generationId?: string): string {
|
|
300
|
+
if (!generationId || !GENERATION_ID_PATTERN.test(generationId)) return url;
|
|
301
|
+
if (!url.startsWith("/.mandu/client/") || /(?:\?|&)g=/.test(url)) return url;
|
|
302
|
+
return `${url}${url.includes("?") ? "&" : "?"}g=${generationId}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Produce a response-local manifest whose client URLs are pinned to one
|
|
307
|
+
* immutable generation. The on-disk manifest stays query-free so its strict
|
|
308
|
+
* security schema and disk analyzers keep a simple canonical path shape.
|
|
309
|
+
*/
|
|
310
|
+
export function scopeBundleManifestToGeneration(
|
|
311
|
+
manifest: BundleManifest | undefined,
|
|
312
|
+
): BundleManifest | undefined {
|
|
313
|
+
const generationId = manifest?.generationId;
|
|
314
|
+
if (!manifest || !generationId || !GENERATION_ID_PATTERN.test(generationId)) return manifest;
|
|
315
|
+
const scope = (url: string): string => scopeClientAssetUrl(url, generationId);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
...manifest,
|
|
319
|
+
bundles: Object.fromEntries(
|
|
320
|
+
Object.entries(manifest.bundles).map(([id, bundle]) => [
|
|
321
|
+
id,
|
|
322
|
+
{ ...bundle, js: scope(bundle.js), ...(bundle.css ? { css: scope(bundle.css) } : {}) },
|
|
323
|
+
]),
|
|
324
|
+
),
|
|
325
|
+
...(manifest.islands
|
|
326
|
+
? {
|
|
327
|
+
islands: Object.fromEntries(
|
|
328
|
+
Object.entries(manifest.islands).map(([id, island]) => [
|
|
329
|
+
id,
|
|
330
|
+
{ ...island, js: scope(island.js) },
|
|
331
|
+
]),
|
|
332
|
+
),
|
|
333
|
+
}
|
|
334
|
+
: {}),
|
|
335
|
+
...(manifest.partials
|
|
336
|
+
? {
|
|
337
|
+
partials: Object.fromEntries(
|
|
338
|
+
Object.entries(manifest.partials).map(([id, partial]) => [
|
|
339
|
+
id,
|
|
340
|
+
{ ...partial, js: scope(partial.js) },
|
|
341
|
+
]),
|
|
342
|
+
),
|
|
343
|
+
}
|
|
344
|
+
: {}),
|
|
345
|
+
...(manifest.boundaries
|
|
346
|
+
? {
|
|
347
|
+
boundaries: Object.fromEntries(
|
|
348
|
+
Object.entries(manifest.boundaries).map(([id, boundary]) => [
|
|
349
|
+
id,
|
|
350
|
+
{ ...boundary, js: scope(boundary.js) },
|
|
351
|
+
]),
|
|
352
|
+
),
|
|
353
|
+
}
|
|
354
|
+
: {}),
|
|
355
|
+
shared: {
|
|
356
|
+
...manifest.shared,
|
|
357
|
+
runtime: scope(manifest.shared.runtime),
|
|
358
|
+
vendor: scope(manifest.shared.vendor),
|
|
359
|
+
...(manifest.shared.router ? { router: scope(manifest.shared.router) } : {}),
|
|
360
|
+
...(manifest.shared.fastRefresh
|
|
361
|
+
? {
|
|
362
|
+
fastRefresh: {
|
|
363
|
+
runtime: scope(manifest.shared.fastRefresh.runtime),
|
|
364
|
+
glue: scope(manifest.shared.fastRefresh.glue),
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
: {}),
|
|
368
|
+
},
|
|
369
|
+
...(manifest.importMap
|
|
370
|
+
? {
|
|
371
|
+
importMap: {
|
|
372
|
+
imports: Object.fromEntries(
|
|
373
|
+
Object.entries(manifest.importMap.imports).map(([specifier, url]) => [
|
|
374
|
+
specifier,
|
|
375
|
+
scope(url),
|
|
376
|
+
]),
|
|
377
|
+
),
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
: {}),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function writeAttemptState(
|
|
385
|
+
generation: BuildGeneration,
|
|
386
|
+
status: BuildGenerationAttempt["lastAttempt"]["status"],
|
|
387
|
+
errors?: readonly string[],
|
|
388
|
+
): Promise<void> {
|
|
389
|
+
const active = await readActiveBuildGeneration(generation.rootDir);
|
|
390
|
+
const state: BuildGenerationAttempt = {
|
|
391
|
+
schemaVersion: SCHEMA_VERSION,
|
|
392
|
+
activeGenerationId: status === "published" ? generation.id : active?.generationId ?? null,
|
|
393
|
+
lastAttempt: {
|
|
394
|
+
generationId: generation.id,
|
|
395
|
+
status,
|
|
396
|
+
startedAt: generation.startedAt,
|
|
397
|
+
...(status === "building" ? {} : { completedAt: new Date().toISOString() }),
|
|
398
|
+
...(errors && errors.length > 0
|
|
399
|
+
? { errors: errors.slice(0, 50).map((error) => error.slice(0, 2000)) }
|
|
400
|
+
: {}),
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
await atomicWriteJson(buildStatePath(generation.rootDir), state);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function beginBuildGeneration(rootDir: string): Promise<BuildGeneration> {
|
|
407
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
408
|
+
const id = createGenerationId();
|
|
409
|
+
const stagingRoot = path.join(manduDir(resolvedRoot), STAGING_DIR, id);
|
|
410
|
+
const clientDir = path.join(stagingRoot, "client");
|
|
411
|
+
const generation: BuildGeneration = {
|
|
412
|
+
id,
|
|
413
|
+
rootDir: resolvedRoot,
|
|
414
|
+
startedAt: new Date().toISOString(),
|
|
415
|
+
stagingRoot,
|
|
416
|
+
clientDir,
|
|
417
|
+
manifestPath: path.join(stagingRoot, "manifest.json"),
|
|
418
|
+
finalRoot: generationRoot(resolvedRoot, id),
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
await fs.mkdir(clientDir, { recursive: true });
|
|
422
|
+
const active = await resolveActiveBuildArtifacts(resolvedRoot);
|
|
423
|
+
if (await pathExists(active.clientDir)) {
|
|
424
|
+
await fs.cp(active.clientDir, clientDir, { recursive: true, force: true });
|
|
425
|
+
}
|
|
426
|
+
if (await pathExists(active.manifestPath)) {
|
|
427
|
+
await fs.copyFile(active.manifestPath, generation.manifestPath);
|
|
428
|
+
}
|
|
429
|
+
await writeAttemptState(generation, "building");
|
|
430
|
+
return generation;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function restoreCompatibilityClient(
|
|
434
|
+
legacyClient: string,
|
|
435
|
+
backupClient: string,
|
|
436
|
+
hadLegacyClient: boolean,
|
|
437
|
+
): Promise<void> {
|
|
438
|
+
await fs.rm(legacyClient, { recursive: true, force: true }).catch(() => {});
|
|
439
|
+
if (hadLegacyClient && (await pathExists(backupClient))) {
|
|
440
|
+
await fs.rename(backupClient, legacyClient);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function publishCompatibilityArtifacts(generation: BuildGeneration): Promise<{
|
|
445
|
+
backupClient: string;
|
|
446
|
+
hadLegacyClient: boolean;
|
|
447
|
+
oldManifest: string | null;
|
|
448
|
+
}> {
|
|
449
|
+
const base = manduDir(generation.rootDir);
|
|
450
|
+
const legacyClient = path.join(base, "client");
|
|
451
|
+
const legacyManifest = path.join(base, "manifest.json");
|
|
452
|
+
const compatibilityClient = path.join(base, ".staging", `compat-client-${generation.id}`);
|
|
453
|
+
const backupClient = path.join(base, ".staging", `previous-client-${generation.id}`);
|
|
454
|
+
const finalClient = path.join(generation.finalRoot, "client");
|
|
455
|
+
const finalManifest = path.join(generation.finalRoot, "manifest.json");
|
|
456
|
+
|
|
457
|
+
await fs.rm(compatibilityClient, { recursive: true, force: true });
|
|
458
|
+
await fs.cp(finalClient, compatibilityClient, { recursive: true, force: true });
|
|
459
|
+
|
|
460
|
+
const hadLegacyClient = await pathExists(legacyClient);
|
|
461
|
+
const oldManifest = await fs.readFile(legacyManifest, "utf8").catch((error: unknown) => {
|
|
462
|
+
if (isMissing(error)) return null;
|
|
463
|
+
throw error;
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (hadLegacyClient) {
|
|
467
|
+
await fs.rm(backupClient, { recursive: true, force: true });
|
|
468
|
+
await fs.rename(legacyClient, backupClient);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
await fs.rename(compatibilityClient, legacyClient);
|
|
473
|
+
const nextManifest = await fs.readFile(finalManifest, "utf8");
|
|
474
|
+
await atomicWriteFile(legacyManifest, nextManifest);
|
|
475
|
+
return { backupClient, hadLegacyClient, oldManifest };
|
|
476
|
+
} catch (error) {
|
|
477
|
+
await restoreCompatibilityClient(legacyClient, backupClient, hadLegacyClient);
|
|
478
|
+
if (oldManifest === null) {
|
|
479
|
+
await fs.rm(legacyManifest, { force: true }).catch(() => {});
|
|
480
|
+
} else {
|
|
481
|
+
await atomicWriteFile(legacyManifest, oldManifest).catch(() => {});
|
|
482
|
+
}
|
|
483
|
+
await fs.rm(compatibilityClient, { recursive: true, force: true }).catch(() => {});
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function pruneOldGenerations(rootDir: string, activeId: string): Promise<void> {
|
|
489
|
+
const dir = path.join(manduDir(rootDir), GENERATIONS_DIR);
|
|
490
|
+
let entries: Array<{ name: string; isDirectory(): boolean }>;
|
|
491
|
+
try {
|
|
492
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
493
|
+
} catch {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const candidates = entries
|
|
498
|
+
.filter((entry) => entry.isDirectory() && GENERATION_ID_PATTERN.test(entry.name))
|
|
499
|
+
.map((entry) => entry.name)
|
|
500
|
+
.sort((left, right) => right.localeCompare(left));
|
|
501
|
+
const keep = new Set([activeId, ...candidates.slice(0, GENERATIONS_TO_KEEP)]);
|
|
502
|
+
await Promise.all(
|
|
503
|
+
candidates
|
|
504
|
+
.filter((id) => !keep.has(id))
|
|
505
|
+
.map((id) => fs.rm(path.join(dir, id), { recursive: true, force: true }).catch(() => {})),
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export async function publishBuildGeneration(
|
|
510
|
+
generation: BuildGeneration,
|
|
511
|
+
): Promise<BuildGenerationPointer> {
|
|
512
|
+
if (!(await pathExists(generation.clientDir)) || !(await pathExists(generation.manifestPath))) {
|
|
513
|
+
throw new Error(`Build generation ${generation.id} is incomplete and cannot be published.`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
await fs.mkdir(path.dirname(generation.finalRoot), { recursive: true });
|
|
517
|
+
await promoteStagedBuildGeneration(generation);
|
|
518
|
+
|
|
519
|
+
const legacyClient = path.join(manduDir(generation.rootDir), "client");
|
|
520
|
+
const legacyManifest = path.join(manduDir(generation.rootDir), "manifest.json");
|
|
521
|
+
let compatibility:
|
|
522
|
+
| { backupClient: string; hadLegacyClient: boolean; oldManifest: string | null }
|
|
523
|
+
| undefined;
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
compatibility = await publishCompatibilityArtifacts(generation);
|
|
527
|
+
const pointer: BuildGenerationPointer = {
|
|
528
|
+
schemaVersion: SCHEMA_VERSION,
|
|
529
|
+
generationId: generation.id,
|
|
530
|
+
publishedAt: new Date().toISOString(),
|
|
531
|
+
};
|
|
532
|
+
await atomicWriteJson(activePointerPath(generation.rootDir), pointer);
|
|
533
|
+
|
|
534
|
+
const artifacts: ActiveBuildArtifacts = {
|
|
535
|
+
generationId: generation.id,
|
|
536
|
+
clientDir: path.join(generation.finalRoot, "client"),
|
|
537
|
+
manifestPath: path.join(generation.finalRoot, "manifest.json"),
|
|
538
|
+
source: "generation",
|
|
539
|
+
};
|
|
540
|
+
activeArtifactsCache.set(path.resolve(generation.rootDir), artifacts);
|
|
541
|
+
await fs.rm(compatibility.backupClient, { recursive: true, force: true }).catch(() => {});
|
|
542
|
+
await writeAttemptState(generation, "published").catch((error) => {
|
|
543
|
+
console.warn(
|
|
544
|
+
`[Mandu] Build generation ${generation.id} published but state recording failed: ${
|
|
545
|
+
error instanceof Error ? error.message : String(error)
|
|
546
|
+
}`,
|
|
547
|
+
);
|
|
548
|
+
});
|
|
549
|
+
void pruneOldGenerations(generation.rootDir, generation.id);
|
|
550
|
+
return pointer;
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (compatibility) {
|
|
553
|
+
await restoreCompatibilityClient(
|
|
554
|
+
legacyClient,
|
|
555
|
+
compatibility.backupClient,
|
|
556
|
+
compatibility.hadLegacyClient,
|
|
557
|
+
).catch(() => {});
|
|
558
|
+
if (compatibility.oldManifest === null) {
|
|
559
|
+
await fs.rm(legacyManifest, { force: true }).catch(() => {});
|
|
560
|
+
} else {
|
|
561
|
+
await atomicWriteFile(legacyManifest, compatibility.oldManifest).catch(() => {});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
await fs.rm(generation.finalRoot, { recursive: true, force: true }).catch(() => {});
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export async function failBuildGeneration(
|
|
570
|
+
generation: BuildGeneration,
|
|
571
|
+
errors: readonly string[],
|
|
572
|
+
): Promise<void> {
|
|
573
|
+
await writeAttemptState(generation, "failed", errors).catch((error) => {
|
|
574
|
+
console.warn(
|
|
575
|
+
`[Mandu] Could not record failed build generation ${generation.id}: ${
|
|
576
|
+
error instanceof Error ? error.message : String(error)
|
|
577
|
+
}`,
|
|
578
|
+
);
|
|
579
|
+
});
|
|
580
|
+
await fs.rm(generation.stagingRoot, { recursive: true, force: true }).catch(() => {});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export function runSerializedClientBuild<T>(rootDir: string, task: () => Promise<T>): Promise<T> {
|
|
584
|
+
const key = path.resolve(rootDir);
|
|
585
|
+
const previous = buildQueues.get(key) ?? Promise.resolve();
|
|
586
|
+
const run = previous.then(task, task);
|
|
587
|
+
const tail = run.then(
|
|
588
|
+
() => undefined,
|
|
589
|
+
() => undefined,
|
|
590
|
+
);
|
|
591
|
+
buildQueues.set(key, tail);
|
|
592
|
+
void tail.finally(() => {
|
|
593
|
+
if (buildQueues.get(key) === tail) buildQueues.delete(key);
|
|
594
|
+
});
|
|
595
|
+
return run;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Test-only cache reset. */
|
|
599
|
+
export function __clearBuildGenerationCacheForTests(): void {
|
|
600
|
+
activeArtifactsCache.clear();
|
|
601
|
+
buildQueues.clear();
|
|
602
|
+
}
|
package/src/bundler/index.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export * from "./types";
|
|
7
|
-
export * from "./build";
|
|
8
|
-
export * from "./client-boundary-transform";
|
|
9
|
-
export * from "./dev";
|
|
7
|
+
export * from "./build";
|
|
8
|
+
export * from "./client-boundary-transform";
|
|
9
|
+
export * from "./dev";
|
|
10
10
|
export * from "./css";
|
|
11
11
|
export * from "./plugins";
|