@1agh/maude 1.0.11 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/studio/acp/bridge.ts +18 -13
- package/apps/studio/acp/transcript-io.ts +394 -0
- package/apps/studio/acp/transcript.ts +103 -38
- package/apps/studio/api.ts +139 -26
- package/apps/studio/bin/_import-figma.mjs +57 -11
- package/apps/studio/bin/_seed-fixture.mjs +163 -0
- package/apps/studio/canvas-imports.ts +89 -0
- package/apps/studio/client/app.jsx +257 -7
- package/apps/studio/client/export-center.jsx +15 -7
- package/apps/studio/client/panels/SyncPanel.jsx +125 -4
- package/apps/studio/client/report-bug.jsx +19 -1
- package/apps/studio/client/styles/3-shell-maude.css +34 -0
- package/apps/studio/client/styles/4-components.css +26 -0
- package/apps/studio/collab/persistence.ts +132 -16
- package/apps/studio/collab/registry.ts +10 -18
- package/apps/studio/debug-bundle.ts +13 -0
- package/apps/studio/dist/client.bundle.js +1128 -1128
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/export-dialog.tsx +54 -0
- package/apps/studio/exporters/degraded.ts +19 -3
- package/apps/studio/exporters/ghostscript.ts +194 -0
- package/apps/studio/exporters/pdf-fonts.ts +261 -0
- package/apps/studio/exporters/pdf.ts +152 -3
- package/apps/studio/exporters/remote.ts +36 -1
- package/apps/studio/fs-watch.ts +45 -1
- package/apps/studio/http.ts +7 -1
- package/apps/studio/mem.ts +45 -9
- package/apps/studio/server.ts +11 -4
- package/apps/studio/sync/agent.ts +136 -29
- package/apps/studio/sync/asset-push.ts +1 -47
- package/apps/studio/sync/codec.ts +122 -31
- package/apps/studio/sync/cold-start-apply.ts +7 -0
- package/apps/studio/sync/cold-start.ts +118 -22
- package/apps/studio/sync/comment-identity.ts +68 -0
- package/apps/studio/sync/connection-state.ts +15 -1
- package/apps/studio/sync/file-ledger.ts +117 -0
- package/apps/studio/sync/file-plane.ts +623 -27
- package/apps/studio/sync/index.ts +711 -20
- package/apps/studio/sync/journal-client.ts +13 -1
- package/apps/studio/sync/migrate-seed.ts +40 -3
- package/apps/studio/sync/presentation.ts +26 -3
- package/apps/studio/sync/retry-after.ts +169 -0
- package/apps/studio/sync/seed-progress.ts +218 -0
- package/apps/studio/sync/status.ts +122 -5
- package/apps/studio/sync/transport-error.ts +111 -0
- package/apps/studio/whats-new.json +72 -0
- package/cli/bin/maude.mjs +2 -0
- package/cli/commands/codex.mjs +191 -0
- package/cli/commands/harness.mjs +1051 -0
- package/cli/commands/help.mjs +13 -0
- package/cli/lib/design-link.mjs +190 -2
- package/cli/lib/harness/capabilities.mjs +302 -0
- package/cli/lib/harness/codex-runtime.mjs +889 -0
- package/cli/lib/harness/compatibility.mjs +27 -0
- package/cli/lib/harness/discover-claude.mjs +1393 -0
- package/cli/lib/harness/managed-state.mjs +1296 -0
- package/cli/lib/harness/model.mjs +86 -0
- package/cli/lib/harness/secrets.mjs +390 -0
- package/cli/lib/harness/targets/codex.mjs +1141 -0
- package/cli/lib/harness/targets/opencode.mjs +807 -0
- package/cli/lib/harness/transaction.mjs +1099 -0
- package/cli/lib/hubs-config.mjs +35 -8
- package/cli/templates/harness/opencode/maude-projector.ts +58 -0
- package/docs/harness-capability-matrix.md +125 -0
- package/docs/harness-environment-projection.md +95 -0
- package/package.json +32 -9
- package/plugins/flow/.claude-plugin/config.schema.json +8 -0
- package/apps/studio/.ai/cache/_stats.json +0 -7
- package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
- package/apps/studio/bin/_curl-local.test.mjs +0 -280
- package/apps/studio/bin/_pw-launch.test.mjs +0 -90
- package/apps/studio/bin/_smart-frames.test.mjs +0 -195
- package/apps/studio/bin/_transcribe.test.mjs +0 -80
- package/apps/studio/bun.lock +0 -928
- package/apps/studio/draw/test/animate.test.ts +0 -172
- package/apps/studio/draw/test/brush.test.ts +0 -213
- package/apps/studio/draw/test/composition.test.ts +0 -141
- package/apps/studio/draw/test/geometry.test.ts +0 -199
- package/apps/studio/draw/test/gradient.test.ts +0 -167
- package/apps/studio/draw/test/layout.test.ts +0 -120
- package/apps/studio/draw/test/morph.test.ts +0 -107
- package/apps/studio/draw/test/optimize.test.ts +0 -40
- package/apps/studio/draw/test/palette.test.ts +0 -123
- package/apps/studio/draw/test/primitives.test.ts +0 -129
- package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
- package/apps/studio/draw/test/serialize.test.ts +0 -105
- package/apps/studio/figma/assets.test.ts +0 -464
- package/apps/studio/figma/client.test.ts +0 -395
- package/apps/studio/figma/codegen-client.test.ts +0 -276
- package/apps/studio/figma/codegen-fonts.test.ts +0 -103
- package/apps/studio/figma/codegen-values.test.ts +0 -179
- package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
- package/apps/studio/figma/fig-decode.test.ts +0 -788
- package/apps/studio/figma/fig-differential.test.ts +0 -182
- package/apps/studio/figma/fig-translator.test.ts +0 -192
- package/apps/studio/figma/fig-vector.test.ts +0 -113
- package/apps/studio/figma/from-codegen.test.ts +0 -408
- package/apps/studio/figma/sanitize.test.ts +0 -325
- package/apps/studio/figma/tailwind-map.test.ts +0 -142
- package/apps/studio/figma/to-artboard.test.ts +0 -808
- package/apps/studio/figma/to-render.test.ts +0 -180
- package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
- package/apps/studio/figma/to-strokes.test.ts +0 -705
- package/apps/studio/figma/to-tokens.test.ts +0 -321
- package/apps/studio/figma/url.test.ts +0 -167
- package/apps/studio/footage/schema.test.ts +0 -287
- package/apps/studio/generation/adapters/groq.test.ts +0 -80
- package/apps/studio/generation/audio-library.test.ts +0 -95
- package/apps/studio/generation/captions.test.ts +0 -78
- package/apps/studio/generation/elevenlabs.test.ts +0 -223
- package/apps/studio/generation/gemini.test.ts +0 -350
- package/apps/studio/generation/jobs.test.ts +0 -128
- package/apps/studio/generation/keys.test.ts +0 -75
- package/apps/studio/generation/prefs.test.ts +0 -95
- package/apps/studio/generation/types.test.ts +0 -123
- package/apps/studio/generation/whisper-models.test.ts +0 -179
- package/apps/studio/test/_helpers.ts +0 -108
- package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
- package/apps/studio/test/acp-activity.test.ts +0 -154
- package/apps/studio/test/acp-ai-activity.test.ts +0 -182
- package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
- package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
- package/apps/studio/test/acp-branch-guard.test.ts +0 -123
- package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
- package/apps/studio/test/acp-bridge.test.ts +0 -545
- package/apps/studio/test/acp-capabilities.test.ts +0 -123
- package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
- package/apps/studio/test/acp-commands.test.ts +0 -119
- package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
- package/apps/studio/test/acp-elicitation.test.ts +0 -251
- package/apps/studio/test/acp-env.test.ts +0 -95
- package/apps/studio/test/acp-mode-banner.test.ts +0 -45
- package/apps/studio/test/acp-origin-gate.test.ts +0 -160
- package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
- package/apps/studio/test/acp-permission.test.ts +0 -278
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
- package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
- package/apps/studio/test/acp-session-plugins.test.ts +0 -209
- package/apps/studio/test/acp-toolgroup.test.ts +0 -76
- package/apps/studio/test/acp-transcript-view.test.ts +0 -71
- package/apps/studio/test/acp-transcript.test.ts +0 -238
- package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
- package/apps/studio/test/acp-usage.test.ts +0 -143
- package/apps/studio/test/acp-write-gate.test.ts +0 -323
- package/apps/studio/test/acp-write-scope.test.ts +0 -533
- package/apps/studio/test/active-state.test.ts +0 -186
- package/apps/studio/test/activity.test.ts +0 -189
- package/apps/studio/test/ai-activity.test.ts +0 -113
- package/apps/studio/test/annotate-write.test.ts +0 -1074
- package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
- package/apps/studio/test/annotations-align.test.ts +0 -88
- package/apps/studio/test/annotations-api.test.ts +0 -145
- package/apps/studio/test/annotations-bindings.test.ts +0 -262
- package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
- package/apps/studio/test/annotations-groups.test.ts +0 -231
- package/apps/studio/test/annotations-layer.test.ts +0 -1011
- package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
- package/apps/studio/test/annotations-snap.test.ts +0 -135
- package/apps/studio/test/annotations-text-format.test.ts +0 -396
- package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
- package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
- package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
- package/apps/studio/test/artboard-kinds.test.tsx +0 -83
- package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
- package/apps/studio/test/asset-api.test.ts +0 -149
- package/apps/studio/test/assets-s3.test.ts +0 -249
- package/apps/studio/test/binary-smoke.test.ts +0 -47
- package/apps/studio/test/boot-self-heal.test.ts +0 -87
- package/apps/studio/test/browse-posture.test.tsx +0 -107
- package/apps/studio/test/bundle-smoke.test.ts +0 -46
- package/apps/studio/test/camera-reveal.test.tsx +0 -173
- package/apps/studio/test/canvas-artifacts.test.ts +0 -125
- package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
- package/apps/studio/test/canvas-build.test.ts +0 -141
- package/apps/studio/test/canvas-create-api.test.ts +0 -676
- package/apps/studio/test/canvas-cursors.test.ts +0 -164
- package/apps/studio/test/canvas-edit.test.ts +0 -470
- package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
- package/apps/studio/test/canvas-header.test.ts +0 -127
- package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
- package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
- package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
- package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
- package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
- package/apps/studio/test/canvas-list-watch.test.ts +0 -388
- package/apps/studio/test/canvas-media-drop.test.ts +0 -187
- package/apps/studio/test/canvas-meta-api.test.ts +0 -707
- package/apps/studio/test/canvas-move-api.test.ts +0 -587
- package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
- package/apps/studio/test/canvas-pipeline.test.ts +0 -180
- package/apps/studio/test/canvas-rects.test.ts +0 -198
- package/apps/studio/test/canvas-reorder.test.ts +0 -211
- package/apps/studio/test/canvas-route.test.ts +0 -179
- package/apps/studio/test/canvas-shell-base.test.ts +0 -107
- package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
- package/apps/studio/test/canvas-url.test.ts +0 -138
- package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
- package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
- package/apps/studio/test/capture-core.test.ts +0 -32
- package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
- package/apps/studio/test/chat-attachments.test.ts +0 -117
- package/apps/studio/test/chat-context.test.ts +0 -129
- package/apps/studio/test/chat-markdown.test.tsx +0 -58
- package/apps/studio/test/clip-addressing.test.ts +0 -738
- package/apps/studio/test/clip-ops.test.ts +0 -922
- package/apps/studio/test/cloud-build.test.ts +0 -95
- package/apps/studio/test/cloud-connect-note.test.ts +0 -135
- package/apps/studio/test/cloud-endpoints.test.ts +0 -590
- package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
- package/apps/studio/test/cloud-history-posture.test.ts +0 -230
- package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
- package/apps/studio/test/cloud-renew.test.ts +0 -205
- package/apps/studio/test/cloud-session-role.test.ts +0 -161
- package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
- package/apps/studio/test/cold-start-apply.test.ts +0 -303
- package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
- package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
- package/apps/studio/test/collab-bridge.test.ts +0 -79
- package/apps/studio/test/collab-loopback.test.ts +0 -63
- package/apps/studio/test/collab-origin-gate.test.ts +0 -323
- package/apps/studio/test/collab-protocol.test.ts +0 -146
- package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
- package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
- package/apps/studio/test/collab-room.test.ts +0 -188
- package/apps/studio/test/collab-session-survival.test.tsx +0 -176
- package/apps/studio/test/collab-stress.test.ts +0 -129
- package/apps/studio/test/comment-mount.test.ts +0 -97
- package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
- package/apps/studio/test/comments-api.test.ts +0 -277
- package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
- package/apps/studio/test/comments-overlay.test.ts +0 -117
- package/apps/studio/test/compile-entry.test.ts +0 -133
- package/apps/studio/test/component-map.test.ts +0 -48
- package/apps/studio/test/config-projection.test.ts +0 -104
- package/apps/studio/test/config-reload.test.ts +0 -230
- package/apps/studio/test/config-version.test.ts +0 -88
- package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
- package/apps/studio/test/convert-to-absolute.test.ts +0 -333
- package/apps/studio/test/credential-grammar.test.ts +0 -107
- package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
- package/apps/studio/test/csrf-write-guard.test.ts +0 -141
- package/apps/studio/test/debug-bundle.test.ts +0 -105
- package/apps/studio/test/design-setup-readiness.test.ts +0 -155
- package/apps/studio/test/detach-component.test.ts +0 -94
- package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
- package/apps/studio/test/dom-selection.test.ts +0 -198
- package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
- package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
- package/apps/studio/test/edit-persistence.test.ts +0 -91
- package/apps/studio/test/edit-scope-api.test.ts +0 -119
- package/apps/studio/test/edit-source-command.test.ts +0 -142
- package/apps/studio/test/edit-suppress.test.ts +0 -170
- package/apps/studio/test/editing-presence.test.ts +0 -103
- package/apps/studio/test/element-resize.test.ts +0 -136
- package/apps/studio/test/element-structural-api.test.ts +0 -434
- package/apps/studio/test/element-structural-edit.test.ts +0 -750
- package/apps/studio/test/ensure-browser.test.ts +0 -92
- package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
- package/apps/studio/test/export-assemble.test.ts +0 -53
- package/apps/studio/test/export-browser-lane.test.ts +0 -126
- package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
- package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
- package/apps/studio/test/export-center.test.tsx +0 -287
- package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
- package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
- package/apps/studio/test/export-lane.test.ts +0 -245
- package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
- package/apps/studio/test/exporters/canva.test.ts +0 -63
- package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
- package/apps/studio/test/exporters/endpoint.test.ts +0 -121
- package/apps/studio/test/exporters/history.test.ts +0 -107
- package/apps/studio/test/exporters/html.test.ts +0 -26
- package/apps/studio/test/exporters/jobs.test.ts +0 -342
- package/apps/studio/test/exporters/pdf.test.ts +0 -85
- package/apps/studio/test/exporters/png.test.ts +0 -103
- package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
- package/apps/studio/test/exporters/pptx.test.ts +0 -31
- package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
- package/apps/studio/test/exporters/runtime.test.ts +0 -59
- package/apps/studio/test/exporters/scope.test.ts +0 -292
- package/apps/studio/test/exporters/svg.test.ts +0 -29
- package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
- package/apps/studio/test/exporters/zip.test.ts +0 -104
- package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
- package/apps/studio/test/figjam-v3-model.test.ts +0 -342
- package/apps/studio/test/figma-explode.test.ts +0 -438
- package/apps/studio/test/figma-provenance.test.ts +0 -108
- package/apps/studio/test/figma-routes.test.ts +0 -294
- package/apps/studio/test/file-lock.test.ts +0 -84
- package/apps/studio/test/fixtures/backgrounding-claude-auth.sh +0 -12
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -23
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
- package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
- package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
- package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
- package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
- package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
- package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
- package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
- package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
- package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
- package/apps/studio/test/fixtures/noisy-claude-auth.mjs +0 -27
- package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
- package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
- package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
- package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
- package/apps/studio/test/fixtures/wedged-claude-auth.sh +0 -11
- package/apps/studio/test/footage-store.test.ts +0 -100
- package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
- package/apps/studio/test/fs-watch.test.ts +0 -41
- package/apps/studio/test/gemma-models.test.ts +0 -245
- package/apps/studio/test/generate-route.test.ts +0 -106
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +0 -341
- package/apps/studio/test/git-cloud-posture.test.ts +0 -59
- package/apps/studio/test/git-grouping.test.ts +0 -106
- package/apps/studio/test/git-lifecycle.test.ts +0 -100
- package/apps/studio/test/git-log-format.test.ts +0 -95
- package/apps/studio/test/git-safe-rel.test.ts +0 -132
- package/apps/studio/test/git-system-engine.test.ts +0 -135
- package/apps/studio/test/git-watch.test.ts +0 -97
- package/apps/studio/test/github-api.test.ts +0 -577
- package/apps/studio/test/grid-track-handles.test.ts +0 -160
- package/apps/studio/test/handoff-static-frames.test.ts +0 -215
- package/apps/studio/test/handoff.test.ts +0 -329
- package/apps/studio/test/history-rollback.test.ts +0 -88
- package/apps/studio/test/hmr-broadcast.test.ts +0 -236
- package/apps/studio/test/hmr-classify.test.ts +0 -87
- package/apps/studio/test/hub-link.test.ts +0 -80
- package/apps/studio/test/identity-cache.test.ts +0 -83
- package/apps/studio/test/import-asset-browser.test.ts +0 -64
- package/apps/studio/test/import-asset-route.test.ts +0 -71
- package/apps/studio/test/import-asset.test.ts +0 -427
- package/apps/studio/test/import-brand-browser.test.ts +0 -88
- package/apps/studio/test/import-brand-route.test.ts +0 -120
- package/apps/studio/test/import-brand.test.ts +0 -206
- package/apps/studio/test/import-figma.test.ts +0 -667
- package/apps/studio/test/import-tokens.test.ts +0 -722
- package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
- package/apps/studio/test/input-router.test.ts +0 -757
- package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
- package/apps/studio/test/inspect-selections.test.ts +0 -219
- package/apps/studio/test/knob-props-authored.test.ts +0 -87
- package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
- package/apps/studio/test/locator.test.ts +0 -214
- package/apps/studio/test/marquee-overlay.test.ts +0 -94
- package/apps/studio/test/media-commit-chain.test.ts +0 -210
- package/apps/studio/test/mode-toggle.test.tsx +0 -226
- package/apps/studio/test/motion-sample.test.ts +0 -35
- package/apps/studio/test/move-artboards-command.test.ts +0 -108
- package/apps/studio/test/participants-chrome.test.ts +0 -65
- package/apps/studio/test/paths.test.ts +0 -88
- package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
- package/apps/studio/test/perf-harness.ts +0 -193
- package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
- package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
- package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
- package/apps/studio/test/photo-edit-api.test.ts +0 -201
- package/apps/studio/test/photo-filters.test.ts +0 -149
- package/apps/studio/test/photo-pipeline.test.ts +0 -106
- package/apps/studio/test/photo-taxonomy.test.ts +0 -96
- package/apps/studio/test/print-marks.test.ts +0 -113
- package/apps/studio/test/print-units.test.ts +0 -173
- package/apps/studio/test/read-annotations.test.ts +0 -727
- package/apps/studio/test/read-only-gate.test.ts +0 -188
- package/apps/studio/test/read-only-role.test.ts +0 -95
- package/apps/studio/test/readiness.test.ts +0 -337
- package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
- package/apps/studio/test/reorder-api.test.ts +0 -210
- package/apps/studio/test/repo-concurrency.test.ts +0 -146
- package/apps/studio/test/repo-lock.test.ts +0 -350
- package/apps/studio/test/report-proxy.test.ts +0 -125
- package/apps/studio/test/ripple.test.ts +0 -197
- package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
- package/apps/studio/test/runtime-bundle.test.ts +0 -69
- package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
- package/apps/studio/test/scaffold-design.test.ts +0 -122
- package/apps/studio/test/server-lifecycle.test.ts +0 -38
- package/apps/studio/test/session-runtime-state.test.ts +0 -226
- package/apps/studio/test/session-scope.test.ts +0 -100
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
- package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
- package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
- package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
- package/apps/studio/test/shared-doc-projection.test.ts +0 -238
- package/apps/studio/test/shell-importmap.test.ts +0 -56
- package/apps/studio/test/sizing-mode.test.ts +0 -102
- package/apps/studio/test/slash-commands.test.ts +0 -117
- package/apps/studio/test/snap-distance-pill.test.ts +0 -68
- package/apps/studio/test/spacing-handles.test.ts +0 -138
- package/apps/studio/test/specimen-select.test.ts +0 -88
- package/apps/studio/test/sync-agent.test.ts +0 -660
- package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
- package/apps/studio/test/sync-asset-push.test.ts +0 -931
- package/apps/studio/test/sync-atomic-write.test.ts +0 -62
- package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
- package/apps/studio/test/sync-autocommit.test.ts +0 -537
- package/apps/studio/test/sync-canvas-path.test.ts +0 -200
- package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
- package/apps/studio/test/sync-codec.test.ts +0 -230
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
- package/apps/studio/test/sync-cold-start.test.ts +0 -304
- package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
- package/apps/studio/test/sync-connection-state.test.ts +0 -337
- package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
- package/apps/studio/test/sync-decide-file.test.ts +0 -420
- package/apps/studio/test/sync-doc-name.test.ts +0 -281
- package/apps/studio/test/sync-echo-guard.test.ts +0 -96
- package/apps/studio/test/sync-file-ledger.test.ts +0 -334
- package/apps/studio/test/sync-file-membership.test.ts +0 -347
- package/apps/studio/test/sync-file-plane.test.ts +0 -976
- package/apps/studio/test/sync-file-pull.test.ts +0 -333
- package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
- package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
- package/apps/studio/test/sync-hardening.test.ts +0 -586
- package/apps/studio/test/sync-hub-listing.test.ts +0 -46
- package/apps/studio/test/sync-hubs-config.test.ts +0 -133
- package/apps/studio/test/sync-incident-replay.test.ts +0 -211
- package/apps/studio/test/sync-journal.test.ts +0 -176
- package/apps/studio/test/sync-meta-codec.test.ts +0 -199
- package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
- package/apps/studio/test/sync-move-retirement.test.ts +0 -231
- package/apps/studio/test/sync-panel-surface.test.ts +0 -153
- package/apps/studio/test/sync-path-pull.test.ts +0 -531
- package/apps/studio/test/sync-presentation.test.ts +0 -262
- package/apps/studio/test/sync-pull-budget.test.ts +0 -169
- package/apps/studio/test/sync-remote-docs.test.ts +0 -333
- package/apps/studio/test/sync-resync-routes.test.ts +0 -168
- package/apps/studio/test/sync-runtime.test.ts +0 -1675
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
- package/apps/studio/test/sync-settings-routes.test.ts +0 -195
- package/apps/studio/test/sync-settings.test.ts +0 -151
- package/apps/studio/test/sync-status.test.ts +0 -261
- package/apps/studio/test/sync-supervisor.test.ts +0 -262
- package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
- package/apps/studio/test/sync-trash.test.ts +0 -132
- package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
- package/apps/studio/test/sync-untrusted.test.ts +0 -103
- package/apps/studio/test/system-endpoint.test.ts +0 -144
- package/apps/studio/test/text-editability-stamp.test.ts +0 -131
- package/apps/studio/test/timeline-comp-target.test.ts +0 -139
- package/apps/studio/test/timeline-media-cache.test.ts +0 -41
- package/apps/studio/test/timeline-parse.test.ts +0 -184
- package/apps/studio/test/timeline-scale.test.ts +0 -65
- package/apps/studio/test/timeline-snap.test.ts +0 -85
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
- package/apps/studio/test/tool-palette.test.tsx +0 -55
- package/apps/studio/test/tour-overlay.test.tsx +0 -90
- package/apps/studio/test/trusted-request-host.test.ts +0 -66
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
- package/apps/studio/test/undo-stack.test.ts +0 -244
- package/apps/studio/test/use-agent-presence.test.tsx +0 -117
- package/apps/studio/test/use-annotation-resize.test.ts +0 -402
- package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
- package/apps/studio/test/use-artboard-drag.test.ts +0 -324
- package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
- package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
- package/apps/studio/test/use-selection-set.test.tsx +0 -166
- package/apps/studio/test/use-snap-guides.test.ts +0 -271
- package/apps/studio/test/use-tool-mode.test.tsx +0 -186
- package/apps/studio/test/use-undo-stack.test.tsx +0 -223
- package/apps/studio/test/video-asset.test.ts +0 -163
- package/apps/studio/test/video-comp-fixture.test.ts +0 -50
- package/apps/studio/test/video-comp.test.ts +0 -270
- package/apps/studio/test/video-encode-lib.test.ts +0 -63
- package/apps/studio/test/video-render-bridge.test.ts +0 -171
- package/apps/studio/test/whats-new.test.ts +0 -113
- package/apps/studio/test/whisper-setup.test.ts +0 -97
- package/apps/studio/test/workspace-containment.test.ts +0 -382
- package/apps/studio/test/workspace-signin.test.ts +0 -292
- package/apps/studio/test/ws-handshake.test.ts +0 -33
- package/cli/commands/cache.test.mjs +0 -166
- package/cli/commands/design-link.test.mjs +0 -419
- package/cli/commands/design.test.mjs +0 -240
- package/cli/commands/doctor.test.mjs +0 -237
- package/cli/commands/hub-workspace.test.mjs +0 -275
- package/cli/commands/hub.test.mjs +0 -225
- package/cli/commands/kg.test.mjs +0 -142
- package/cli/commands/preflight.test.mjs +0 -46
- package/cli/lib/argv.test.mjs +0 -46
- package/cli/lib/cache.test.mjs +0 -303
- package/cli/lib/cell-plan.test.mjs +0 -225
- package/cli/lib/config-lint.test.mjs +0 -117
- package/cli/lib/ddr-to-kgai.test.mjs +0 -118
- package/cli/lib/design-ownership.test.mjs +0 -329
- package/cli/lib/fetch-asset.test.mjs +0 -213
- package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
- package/cli/lib/figma-import-controls.test.mjs +0 -70
- package/cli/lib/flow-design-integration.test.mjs +0 -165
- package/cli/lib/gitignore-block.test.mjs +0 -136
- package/cli/lib/gitignore-drift.test.mjs +0 -156
- package/cli/lib/hubs-config.test.mjs +0 -165
- package/cli/lib/pkg-root.test.mjs +0 -111
- package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
- package/cli/lib/plugin-name-namespace.test.mjs +0 -71
- package/cli/lib/reconstruct-toolset.test.mjs +0 -194
- package/cli/lib/stack-detect.test.mjs +0 -121
- package/cli/lib/update-check.test.mjs +0 -32
- package/cli/lib/workspace-plan.test.mjs +0 -699
package/apps/studio/api.ts
CHANGED
|
@@ -17,7 +17,10 @@ import path from 'node:path';
|
|
|
17
17
|
import { createAssetMirror, s3ConfigFromEnv } from './assets-s3.ts';
|
|
18
18
|
import { canvasArtifacts, locatorKeyFor, relocatedName } from './canvas-artifacts.ts';
|
|
19
19
|
import { renderBriefBoard, validateCanvasName, validateFolderName } from './canvas-create.ts';
|
|
20
|
+
import { rewriteRelativeImports } from './canvas-imports.ts';
|
|
20
21
|
import { canvasSlugFromRel } from './canvas-slug.ts';
|
|
22
|
+
import { atomicWrite } from './sync/atomic-write.ts';
|
|
23
|
+
import { dedupeCommentsById } from './sync/comment-identity.ts';
|
|
21
24
|
|
|
22
25
|
// Re-exported so existing external callers (canvas-list-watch.ts, tests) keep
|
|
23
26
|
// importing it from api.ts — the actual implementation now lives in
|
|
@@ -720,7 +723,20 @@ export interface Api {
|
|
|
720
723
|
}
|
|
721
724
|
|
|
722
725
|
export interface ApiHooks {
|
|
723
|
-
|
|
726
|
+
/**
|
|
727
|
+
* Publish a comments mutation to the SHARED DOC (and the shell's sidebar).
|
|
728
|
+
*
|
|
729
|
+
* `comments` is the post-mutation list, handed over IN MEMORY — issue #111.
|
|
730
|
+
* The hook used to take only `file` and re-read `_comments/<slug>.json`
|
|
731
|
+
* itself, which put a full disk round-trip between the mutation's write and
|
|
732
|
+
* the doc publish. `Room.flush()` fires on ANY doc update (every hub update
|
|
733
|
+
* re-arms it), so on a hub-linked project a flush landed inside that window,
|
|
734
|
+
* projected the PRE-mutation doc back over the file, and the new comment was
|
|
735
|
+
* gone before the hook's own read reached it — "only the first comment
|
|
736
|
+
* stays". Passing the list removes the read, and `publishComments` awaits
|
|
737
|
+
* this hook BEFORE touching disk so the doc is never the stale side.
|
|
738
|
+
*/
|
|
739
|
+
onCommentsChanged: (file: string, comments: Comment[]) => void | Promise<void>;
|
|
724
740
|
/** Phase 8 Task 5 — fires after a successful PUT /_api/annotations write. */
|
|
725
741
|
onAnnotationsChanged?: (file: string, svg: string) => void;
|
|
726
742
|
/**
|
|
@@ -1095,7 +1111,14 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1095
1111
|
// Phase 6 — default-fill `author` / `thread` / `mentions` for legacy
|
|
1096
1112
|
// rows. No write-back here; the on-disk shape stays stable until the
|
|
1097
1113
|
// next mutation persists the upgraded record.
|
|
1098
|
-
|
|
1114
|
+
//
|
|
1115
|
+
// Deduped by comment identity first (issue #112). This is the read half
|
|
1116
|
+
// of the repair: a project whose `_comments/<slug>.json` was already
|
|
1117
|
+
// doubled by the pre-fix sync lane heals the moment it is loaded, and no
|
|
1118
|
+
// consumer — pins, sidebar, `/_comments`, the /design:edit agent — ever
|
|
1119
|
+
// sees the same comment eight times. First occurrence wins, matching the
|
|
1120
|
+
// rule the codec and the cold-start union use.
|
|
1121
|
+
return dedupeCommentsById(arr).map(backfillComment);
|
|
1099
1122
|
} catch {
|
|
1100
1123
|
return [];
|
|
1101
1124
|
}
|
|
@@ -1135,7 +1158,39 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1135
1158
|
}
|
|
1136
1159
|
|
|
1137
1160
|
async function saveCommentsForFile(file: string, list: Comment[]) {
|
|
1138
|
-
|
|
1161
|
+
// The write half of the #112 repair, and the reason it belongs HERE: this
|
|
1162
|
+
// is the one choke point every writer passes through — the API mutations,
|
|
1163
|
+
// and the collab room's `persistJson`, which materializes the Y.Array
|
|
1164
|
+
// straight to disk. Deduping at the door means a doc caught mid-convergence
|
|
1165
|
+
// (or a peer still running the old wholesale write) cannot persist a
|
|
1166
|
+
// duplicated list as the canvas's new truth, which is how the doubling
|
|
1167
|
+
// survived restarts and compounded.
|
|
1168
|
+
await Bun.write(commentsPath(file), JSON.stringify(dedupeCommentsById(list), null, 2));
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Land one comments mutation: SHARED DOC FIRST, disk second (issue #111).
|
|
1173
|
+
*
|
|
1174
|
+
* The order is the fix, not a style choice. Under DDR-064 the room's Y.Doc is
|
|
1175
|
+
* the shared object and `_comments/<slug>.json` is its projection — and
|
|
1176
|
+
* `Room.flush()` projects doc→file on an 800 ms trailing debounce that ANY
|
|
1177
|
+
* doc update re-arms, hub traffic included. So on a linked project a flush is
|
|
1178
|
+
* almost always pending, and any window in which the doc is behind the file
|
|
1179
|
+
* is a window in which that flush silently writes the older list back.
|
|
1180
|
+
*
|
|
1181
|
+
* Publishing to the doc first removes the window instead of narrowing it: a
|
|
1182
|
+
* flush firing at any point from here on carries a doc that already holds the
|
|
1183
|
+
* mutation. Both halves are awaited, so the HTTP response cannot report a
|
|
1184
|
+
* comment the doc never received.
|
|
1185
|
+
*
|
|
1186
|
+
* The list is deduped ONCE here so the doc and the file are handed byte-equal
|
|
1187
|
+
* content — otherwise the file→doc import that follows the write would see a
|
|
1188
|
+
* difference and re-enter the loop.
|
|
1189
|
+
*/
|
|
1190
|
+
async function publishComments(file: string, list: Comment[]): Promise<void> {
|
|
1191
|
+
const settled = dedupeCommentsById(list);
|
|
1192
|
+
await onCommentsChanged(file, settled);
|
|
1193
|
+
await saveCommentsForFile(file, settled);
|
|
1139
1194
|
}
|
|
1140
1195
|
|
|
1141
1196
|
async function loadAllComments(): Promise<Record<string, Comment[]>> {
|
|
@@ -1314,8 +1369,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1314
1369
|
if (anchor.clipStableId != null || anchor.frame != null) c.timeline = anchor;
|
|
1315
1370
|
}
|
|
1316
1371
|
list.push(c);
|
|
1317
|
-
await
|
|
1318
|
-
onCommentsChanged(payload.file);
|
|
1372
|
+
await publishComments(payload.file, list);
|
|
1319
1373
|
return c;
|
|
1320
1374
|
}
|
|
1321
1375
|
|
|
@@ -1343,31 +1397,38 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1343
1397
|
};
|
|
1344
1398
|
entry.thread = [...entry.thread, reply];
|
|
1345
1399
|
entry.mentions = mentionsUnion(entry);
|
|
1346
|
-
await
|
|
1347
|
-
onCommentsChanged(file);
|
|
1400
|
+
await publishComments(file, list);
|
|
1348
1401
|
return entry;
|
|
1349
1402
|
}
|
|
1350
1403
|
return null;
|
|
1351
1404
|
}
|
|
1352
1405
|
|
|
1406
|
+
// Mutations are ID-TOTAL, not first-match (issue #112). `loadAllComments`
|
|
1407
|
+
// dedupes, so under normal operation there is exactly one entry per id and
|
|
1408
|
+
// these behave as they always did. They stay total as defense in depth: the
|
|
1409
|
+
// reporter's second symptom — "I can't then close or resolve the comments" —
|
|
1410
|
+
// was a `findIndex` + `return` marking copy 1 of 8 resolved while the overlay
|
|
1411
|
+
// (which filters `status !== 'resolved'`) kept drawing the other seven, and a
|
|
1412
|
+
// delete removing one copy per click. A duplicated list can still reach these
|
|
1413
|
+
// from an older peer mid-upgrade; resolve must resolve either way.
|
|
1353
1414
|
async function commentsPatch(id: string, patch: Partial<Comment>) {
|
|
1354
1415
|
const all = await loadAllComments();
|
|
1355
1416
|
for (const [file, list] of Object.entries(all)) {
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1417
|
+
const matches = list.filter((c) => c.id === id);
|
|
1418
|
+
const first = matches[0];
|
|
1419
|
+
if (!first) continue;
|
|
1420
|
+
for (const entry of matches) {
|
|
1421
|
+
if (patch.status === 'resolved' || patch.status === 'open') {
|
|
1422
|
+
entry.status = patch.status;
|
|
1423
|
+
entry.resolved_at = patch.status === 'resolved' ? new Date().toISOString() : null;
|
|
1424
|
+
}
|
|
1425
|
+
if (typeof patch.text === 'string' && patch.text.trim()) {
|
|
1426
|
+
entry.text = patch.text.trim().slice(0, 4000);
|
|
1427
|
+
entry.mentions = mentionsUnion(entry);
|
|
1428
|
+
}
|
|
1367
1429
|
}
|
|
1368
|
-
await
|
|
1369
|
-
|
|
1370
|
-
return entry;
|
|
1430
|
+
await publishComments(file, list);
|
|
1431
|
+
return first;
|
|
1371
1432
|
}
|
|
1372
1433
|
return null;
|
|
1373
1434
|
}
|
|
@@ -1375,11 +1436,9 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1375
1436
|
async function commentsDelete(id: string): Promise<boolean> {
|
|
1376
1437
|
const all = await loadAllComments();
|
|
1377
1438
|
for (const [file, list] of Object.entries(all)) {
|
|
1378
|
-
const
|
|
1379
|
-
if (
|
|
1380
|
-
|
|
1381
|
-
await saveCommentsForFile(file, list);
|
|
1382
|
-
onCommentsChanged(file);
|
|
1439
|
+
const remaining = list.filter((c) => c.id !== id);
|
|
1440
|
+
if (remaining.length === list.length) continue;
|
|
1441
|
+
await publishComments(file, remaining);
|
|
1383
1442
|
return true;
|
|
1384
1443
|
}
|
|
1385
1444
|
return false;
|
|
@@ -2946,6 +3005,44 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2946
3005
|
return realTarget === realRoot || realTarget.startsWith(`${realRoot}${path.sep}`);
|
|
2947
3006
|
}
|
|
2948
3007
|
|
|
3008
|
+
/**
|
|
3009
|
+
* Re-root a moved canvas's relative imports for its new folder depth
|
|
3010
|
+
* (issue #114). `fromDir` / `toDir` are POSIX, design-root-relative.
|
|
3011
|
+
*
|
|
3012
|
+
* Runs AFTER the rename, on the file at its destination. Best-effort by
|
|
3013
|
+
* design: the move has already committed, and failing it here would leave a
|
|
3014
|
+
* half-moved tree — strictly worse than the un-rewritten imports that were
|
|
3015
|
+
* the status quo. A no-op rewrite writes nothing.
|
|
3016
|
+
*/
|
|
3017
|
+
async function rewriteCanvasImports(abs: string, fromDir: string, toDir: string): Promise<void> {
|
|
3018
|
+
if (fromDir === toDir) return;
|
|
3019
|
+
try {
|
|
3020
|
+
// NEVER DEREFERENCE A SYMLINK HERE. The move's containment checks cover
|
|
3021
|
+
// the source *directory* (`assertRealpathContained(path.dirname(...))`)
|
|
3022
|
+
// and the destination, but never the source FILE — which was inert while
|
|
3023
|
+
// the move was a bare `rename()`, because renaming a symlink moves the
|
|
3024
|
+
// LINK and no out-of-root byte is ever touched. This function is the
|
|
3025
|
+
// first code on the path that reads and writes the file's contents, so
|
|
3026
|
+
// without this guard a planted `ui/evil.tsx -> /elsewhere/secrets.ts`
|
|
3027
|
+
// would turn an ordinary drag-and-drop into an out-of-root read AND an
|
|
3028
|
+
// out-of-root write. `lstat` does not follow; a link is skipped, not
|
|
3029
|
+
// repaired — its target is not ours to rewrite.
|
|
3030
|
+
const st = await lstat(abs);
|
|
3031
|
+
if (!st.isFile()) return;
|
|
3032
|
+
const source = await readFile(abs, 'utf8');
|
|
3033
|
+
const next = rewriteRelativeImports(source, fromDir, toDir);
|
|
3034
|
+
// atomicWrite, not `writeFile` — this lands on a `.tsx` the fs-mirror is
|
|
3035
|
+
// watching, so a truncate-then-write can surface a partial-content watch
|
|
3036
|
+
// event (the exact hazard atomic-write.ts was built for), and a crash
|
|
3037
|
+
// mid-write would leave a truncated canvas.
|
|
3038
|
+
if (next !== source) atomicWrite(abs, next);
|
|
3039
|
+
} catch (err) {
|
|
3040
|
+
console.warn(
|
|
3041
|
+
`[move] could not rewrite relative imports in ${abs}: ${(err as Error).message}`
|
|
3042
|
+
);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
2949
3046
|
// feature-file-tree-drag-drop-folders (Task 3) — move/rename a canvas + its
|
|
2950
3047
|
// full artifact set (POST /_api/fs-move). Same main-origin-only trust
|
|
2951
3048
|
// boundary as createCanvas/deleteCanvas (DDR-054): never reachable from the
|
|
@@ -3124,6 +3221,14 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3124
3221
|
return { ok: false, status: 500, error: err instanceof Error ? err.message : 'move failed' };
|
|
3125
3222
|
}
|
|
3126
3223
|
|
|
3224
|
+
// The canvas is at a new depth now, so its relative specifiers point at the
|
|
3225
|
+
// wrong place (issue #114). Bytes used to move without them, and the canvas
|
|
3226
|
+
// 500s on the next build with `Could not resolve`. Best-effort: the move
|
|
3227
|
+
// itself already succeeded, and a rewrite failure must not leave the tree
|
|
3228
|
+
// half-moved — it leaves a build error the user can fix by hand, which is
|
|
3229
|
+
// exactly where they were before this existed.
|
|
3230
|
+
await rewriteCanvasImports(toAbs, path.posix.dirname(rel), path.posix.dirname(toRel));
|
|
3231
|
+
|
|
3127
3232
|
const moved: string[] = [path.relative(paths.repoRoot, toAbs)];
|
|
3128
3233
|
for (const artifact of canvasArtifacts({ rel, paths })) {
|
|
3129
3234
|
if (artifact.kind === 'primary') continue; // already moved above
|
|
@@ -3371,6 +3476,14 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3371
3476
|
const toRel = toRelDir + fromRel.slice(relDir.length);
|
|
3372
3477
|
const fromSlug = fileSlug(fromRel);
|
|
3373
3478
|
const toSlug = fileSlug(toRel);
|
|
3479
|
+
// Same depth change as moveCanvas, once per nested canvas (issue #114).
|
|
3480
|
+
// A folder move relocates every canvas inside it by the same delta, but
|
|
3481
|
+
// each one's own dir differs, so the rewrite is per file, not per folder.
|
|
3482
|
+
await rewriteCanvasImports(
|
|
3483
|
+
path.join(paths.designRoot, toRel),
|
|
3484
|
+
path.posix.dirname(fromRel),
|
|
3485
|
+
path.posix.dirname(toRel)
|
|
3486
|
+
);
|
|
3374
3487
|
for (const artifact of canvasArtifacts({ rel: fromRel, paths })) {
|
|
3375
3488
|
if (artifact.kind !== 'slug-keyed') continue; // primary/siblings already moved with the dir
|
|
3376
3489
|
const dest = relocatedName(artifact, fromRel, toRel, paths);
|
|
@@ -29,12 +29,14 @@
|
|
|
29
29
|
|
|
30
30
|
import { createHash } from 'node:crypto';
|
|
31
31
|
import {
|
|
32
|
+
copyFileSync,
|
|
32
33
|
existsSync,
|
|
33
34
|
mkdirSync,
|
|
34
35
|
mkdtempSync,
|
|
35
36
|
readFileSync,
|
|
36
37
|
renameSync,
|
|
37
38
|
rmSync,
|
|
39
|
+
unlinkSync,
|
|
38
40
|
writeFileSync,
|
|
39
41
|
} from 'node:fs';
|
|
40
42
|
import { homedir, tmpdir } from 'node:os';
|
|
@@ -143,6 +145,50 @@ function makeStagingDir() {
|
|
|
143
145
|
return mkdtempSync(join(tmpdir(), 'maude-figma-'));
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Promote one staged file into the project tree.
|
|
150
|
+
*
|
|
151
|
+
* A bare `renameSync` is atomic — and SAME-FILESYSTEM ONLY. Staging lives under
|
|
152
|
+
* the OS temp root (or `~/.cache/maude/figma-staging` for the codegen lane)
|
|
153
|
+
* while the project can be anywhere: a second partition, an external drive, a
|
|
154
|
+
* container volume, a tmpfs `/tmp`. Those are different devices, so the rename
|
|
155
|
+
* threw `EXDEV: cross-device link not permitted` and the whole import died at
|
|
156
|
+
* the final step, after every byte of work was already done. It reproduces on
|
|
157
|
+
* any Linux box where `/tmp` is a tmpfs, which is most of them, and it is why
|
|
158
|
+
* the figma-explode suite was red.
|
|
159
|
+
*
|
|
160
|
+
* The atomicity is not negotiable — nothing may land half-written in the
|
|
161
|
+
* versioned tree — so this does NOT fall back to a plain copy. It copies into a
|
|
162
|
+
* sibling temp file IN THE DESTINATION DIRECTORY and renames within that
|
|
163
|
+
* directory: same filesystem by construction, so the step that publishes the
|
|
164
|
+
* file is still a single atomic rename.
|
|
165
|
+
*/
|
|
166
|
+
function promoteFile(src, dest) {
|
|
167
|
+
try {
|
|
168
|
+
renameSync(src, dest);
|
|
169
|
+
return;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err?.code !== 'EXDEV') throw err;
|
|
172
|
+
}
|
|
173
|
+
const tmp = `${dest}.${process.pid}.promote-tmp`;
|
|
174
|
+
copyFileSync(src, tmp);
|
|
175
|
+
try {
|
|
176
|
+
renameSync(tmp, dest);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
try {
|
|
179
|
+
unlinkSync(tmp);
|
|
180
|
+
} catch {
|
|
181
|
+
/* best effort — the staged original is still intact */
|
|
182
|
+
}
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
unlinkSync(src);
|
|
187
|
+
} catch {
|
|
188
|
+
/* staging is reaped wholesale by its own finally */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
146
192
|
/**
|
|
147
193
|
* DDR-219 D8 — a staging directory outside the synced tree, under a STABLE
|
|
148
194
|
* parent.
|
|
@@ -548,9 +594,9 @@ export async function importBoard({
|
|
|
548
594
|
mkdirSync(join(root, designRootRel, 'ui'), { recursive: true });
|
|
549
595
|
// Promote by rename — atomic, and nothing lands in the versioned tree
|
|
550
596
|
// until the whole translation has succeeded.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
597
|
+
promoteFile(stagedTsx, finalTsx);
|
|
598
|
+
promoteFile(stagedMeta, finalMeta);
|
|
599
|
+
promoteFile(stagedSvg, finalPath);
|
|
554
600
|
return {
|
|
555
601
|
slug: annSlug,
|
|
556
602
|
canvas: canvasRel,
|
|
@@ -977,8 +1023,8 @@ export async function importPages({
|
|
|
977
1023
|
mkdirSync(outDir, { recursive: true });
|
|
978
1024
|
const finalTsx = assertContained(root, designRootRel, join(outDir, `${title}.tsx`));
|
|
979
1025
|
const finalMeta = assertContained(root, designRootRel, join(outDir, `${title}.meta.json`));
|
|
980
|
-
|
|
981
|
-
|
|
1026
|
+
promoteFile(stagedTsx, finalTsx);
|
|
1027
|
+
promoteFile(stagedMeta, finalMeta);
|
|
982
1028
|
|
|
983
1029
|
// The page's annotation layer has TWO sources, and the second one is the
|
|
984
1030
|
// reason a tree-walking import felt half-migrated:
|
|
@@ -1060,7 +1106,7 @@ export async function importPages({
|
|
|
1060
1106
|
designRootRel,
|
|
1061
1107
|
join(root, designRootRel, `${annSlug}.annotations.svg`)
|
|
1062
1108
|
);
|
|
1063
|
-
|
|
1109
|
+
promoteFile(stagedAnn, finalAnn);
|
|
1064
1110
|
}
|
|
1065
1111
|
written.push({
|
|
1066
1112
|
title,
|
|
@@ -1240,8 +1286,8 @@ export async function importFrames({
|
|
|
1240
1286
|
mkdirSync(uiDir, { recursive: true });
|
|
1241
1287
|
const finalTsx = assertContained(root, designRootRel, join(uiDir, `${base}.tsx`));
|
|
1242
1288
|
const finalMeta = assertContained(root, designRootRel, join(uiDir, `${base}.meta.json`));
|
|
1243
|
-
|
|
1244
|
-
|
|
1289
|
+
promoteFile(stagedTsx, finalTsx);
|
|
1290
|
+
promoteFile(stagedMeta, finalMeta);
|
|
1245
1291
|
written.push({
|
|
1246
1292
|
slug: base,
|
|
1247
1293
|
path: finalTsx,
|
|
@@ -1559,8 +1605,8 @@ export async function explodeArtboard({
|
|
|
1559
1605
|
const stagedMeta = join(staging, 'canvas.meta.json');
|
|
1560
1606
|
writeFileSync(stagedTsx, tsxOut, 'utf8');
|
|
1561
1607
|
writeFileSync(stagedMeta, `${JSON.stringify(nextMeta, null, 2)}\n`, 'utf8');
|
|
1562
|
-
|
|
1563
|
-
|
|
1608
|
+
promoteFile(stagedTsx, tsxPath);
|
|
1609
|
+
promoteFile(stagedMeta, metaPath);
|
|
1564
1610
|
|
|
1565
1611
|
return {
|
|
1566
1612
|
canvas: rel,
|
|
@@ -1624,7 +1670,7 @@ export async function importTokens({ url, root, designRootRel = '.design', dryRu
|
|
|
1624
1670
|
designRootRel,
|
|
1625
1671
|
join(outDir, `figma-tokens-${target.fileKey.slice(0, 8).toLowerCase()}.json`)
|
|
1626
1672
|
);
|
|
1627
|
-
|
|
1673
|
+
promoteFile(staged, finalPath);
|
|
1628
1674
|
return { ...result, path: finalPath };
|
|
1629
1675
|
} finally {
|
|
1630
1676
|
rmSync(staging, { recursive: true, force: true });
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generate a synthetic `.design/` with the SHAPE of a large real project.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS. Every defect in feature-large-project-seed only appears at
|
|
5
|
+
// scale: the request ceiling needs >200 paths to bite, the hourly write quota
|
|
6
|
+
// needs >2 GiB, the doručenka's unbounded map needs thousands of rows before
|
|
7
|
+
// its cost shows, and the door's 95 MB ceiling needs a file that exceeds it.
|
|
8
|
+
// The only project that reproduced all of them is 8.8 GB and 47 751 files, so
|
|
9
|
+
// "run it against alligators" was the entire test plan — which meant the
|
|
10
|
+
// regression surface was one person's laptop.
|
|
11
|
+
//
|
|
12
|
+
// SPARSE BY DEFAULT. The bytes are allocated, not written: a 3 GB fixture
|
|
13
|
+
// costs a few hundred milliseconds and almost no disk on APFS/ext4, because
|
|
14
|
+
// nothing here needs the content to be anything in particular. Pass
|
|
15
|
+
// `--real-bytes` when a test genuinely needs them materialised (hashing
|
|
16
|
+
// throughput, compression), and expect it to be slow.
|
|
17
|
+
//
|
|
18
|
+
// Deliberately NOT a `maude design` verb: it is a test fixture generator, and
|
|
19
|
+
// putting it on the user-facing surface would invite someone to run it at a
|
|
20
|
+
// real project. Internal, like `_screenshot-playwright.mjs`.
|
|
21
|
+
//
|
|
22
|
+
// Usage:
|
|
23
|
+
// node apps/studio/bin/_seed-fixture.mjs --out /tmp/big --files 3000 --bytes 2GB
|
|
24
|
+
// node apps/studio/bin/_seed-fixture.mjs --out /tmp/big --files 50 --bytes 10MB --real-bytes
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
closeSync,
|
|
28
|
+
ftruncateSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
openSync,
|
|
31
|
+
rmSync,
|
|
32
|
+
writeFileSync,
|
|
33
|
+
writeSync,
|
|
34
|
+
} from 'node:fs';
|
|
35
|
+
import { join } from 'node:path';
|
|
36
|
+
|
|
37
|
+
function parseBytes(raw) {
|
|
38
|
+
const m = String(raw ?? '')
|
|
39
|
+
.trim()
|
|
40
|
+
.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)?$/i);
|
|
41
|
+
if (!m) return null;
|
|
42
|
+
const mult = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 }[(m[2] ?? 'B').toLowerCase()];
|
|
43
|
+
return Math.round(Number(m[1]) * mult);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseArgs(argv) {
|
|
47
|
+
const out = { files: 3000, bytes: 2 * 1024 ** 3, out: null, realBytes: false, force: false };
|
|
48
|
+
for (let i = 0; i < argv.length; i++) {
|
|
49
|
+
const a = argv[i];
|
|
50
|
+
if (a === '--files') out.files = Number(argv[++i]);
|
|
51
|
+
else if (a === '--bytes') out.bytes = parseBytes(argv[++i]);
|
|
52
|
+
else if (a === '--out') out.out = argv[++i];
|
|
53
|
+
else if (a === '--real-bytes') out.realBytes = true;
|
|
54
|
+
else if (a === '--force') out.force = true;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const args = parseArgs(process.argv.slice(2));
|
|
60
|
+
if (!args.out || !Number.isFinite(args.files) || args.files < 1 || !Number.isFinite(args.bytes)) {
|
|
61
|
+
process.stderr.write(
|
|
62
|
+
'usage: _seed-fixture.mjs --out <dir> [--files N] [--bytes 2GB] [--real-bytes] [--force]\n'
|
|
63
|
+
);
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// REFUSE TO WRITE INTO SOMETHING THAT LOOKS REAL. This generator creates and
|
|
68
|
+
// deletes a `.design/` tree; pointing it at a project by accident would be
|
|
69
|
+
// unrecoverable, and `--force` is the only way past.
|
|
70
|
+
const root = args.out;
|
|
71
|
+
const designRoot = join(root, '.design');
|
|
72
|
+
if (!args.force && !/(^\/tmp\/|^\/private\/tmp\/|fixture|scratch)/.test(root)) {
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
`refusing to generate into ${root} — it does not look like a scratch path.\n` +
|
|
75
|
+
'Use a path under /tmp, or one containing "fixture"/"scratch", or pass --force.\n'
|
|
76
|
+
);
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
79
|
+
rmSync(designRoot, { recursive: true, force: true });
|
|
80
|
+
|
|
81
|
+
/** Allocate a file of `size` without writing `size` bytes. */
|
|
82
|
+
function makeFile(abs, size, real) {
|
|
83
|
+
if (real) {
|
|
84
|
+
// A repeating pattern rather than zeros, so a compression-sensitive test
|
|
85
|
+
// is not accidentally measuring a best case.
|
|
86
|
+
const chunk = Buffer.alloc(Math.min(size, 1 << 20), 0);
|
|
87
|
+
for (let i = 0; i < chunk.length; i++) chunk[i] = (i * 31) & 0xff;
|
|
88
|
+
const fd = openSync(abs, 'w');
|
|
89
|
+
let left = size;
|
|
90
|
+
while (left > 0) {
|
|
91
|
+
const n = Math.min(left, chunk.length);
|
|
92
|
+
writeSync(fd, chunk, 0, n);
|
|
93
|
+
left -= n;
|
|
94
|
+
}
|
|
95
|
+
closeSync(fd);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const fd = openSync(abs, 'w');
|
|
99
|
+
ftruncateSync(fd, size); // sparse — allocated, not written
|
|
100
|
+
closeSync(fd);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const dirs = [
|
|
104
|
+
'system/fixture-ds/assets/photos',
|
|
105
|
+
'system/fixture-ds/assets/photos-cut',
|
|
106
|
+
'system/fixture-ds/assets/video',
|
|
107
|
+
'system/fixture-ds/assets/graphics',
|
|
108
|
+
'system/fixture-ds/preview',
|
|
109
|
+
'ui',
|
|
110
|
+
];
|
|
111
|
+
for (const d of dirs) mkdirSync(join(designRoot, d), { recursive: true });
|
|
112
|
+
|
|
113
|
+
writeFileSync(
|
|
114
|
+
join(designRoot, 'config.json'),
|
|
115
|
+
`${JSON.stringify({ designSystems: ['fixture-ds'], canvasGroups: [{ path: 'ui' }] }, null, 2)}\n`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// The canvas layer — small, numerous, and the part a real project actually
|
|
119
|
+
// edits. Kept to ~2 % of the file count, matching the observed ratio
|
|
120
|
+
// (149 .tsx against 4 237 .jpg).
|
|
121
|
+
const canvasCount = Math.max(1, Math.round(args.files * 0.02));
|
|
122
|
+
for (let i = 0; i < canvasCount; i++) {
|
|
123
|
+
writeFileSync(
|
|
124
|
+
join(designRoot, 'ui', `screen-${i}.tsx`),
|
|
125
|
+
`export default function Screen${i}() {\n return null;\n}\n`
|
|
126
|
+
);
|
|
127
|
+
writeFileSync(
|
|
128
|
+
join(designRoot, 'ui', `screen-${i}.meta.json`),
|
|
129
|
+
`${JSON.stringify({ title: `Screen ${i}`, status: 'draft' })}\n`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The asset bulk. Sizes are drawn deterministically around the observed
|
|
134
|
+
// distribution (0.5–3 MB stills), so a run is reproducible.
|
|
135
|
+
const assetCount = Math.max(0, args.files - canvasCount * 2);
|
|
136
|
+
let remaining = args.bytes;
|
|
137
|
+
let made = 0;
|
|
138
|
+
const folders = ['photos', 'photos-cut', 'graphics'];
|
|
139
|
+
for (let i = 0; i < assetCount && remaining > 0; i++) {
|
|
140
|
+
const folder = folders[i % folders.length];
|
|
141
|
+
const share = Math.max(1024, Math.round(remaining / (assetCount - i)));
|
|
142
|
+
const size = Math.min(remaining, share);
|
|
143
|
+
makeFile(join(designRoot, 'system/fixture-ds/assets', folder, `a${i}.jpg`), size, args.realBytes);
|
|
144
|
+
remaining -= size;
|
|
145
|
+
made += 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// TWO FILES OVER THE DOOR'S 95 MB CEILING, because that is the case with no
|
|
149
|
+
// backoff answer — the real project has exactly two (164.9 MB, 465.8 MB) and
|
|
150
|
+
// they are what proved the client and the door disagreed about the limit.
|
|
151
|
+
const over = [
|
|
152
|
+
['system/fixture-ds/assets/video/over-cap-1.mp4', 165 * 1024 ** 2],
|
|
153
|
+
['system/fixture-ds/assets/video/over-cap-2.mp4', 466 * 1024 ** 2],
|
|
154
|
+
];
|
|
155
|
+
for (const [rel, size] of over) makeFile(join(designRoot, rel), size, args.realBytes);
|
|
156
|
+
|
|
157
|
+
const totalFiles = canvasCount * 2 + made + over.length;
|
|
158
|
+
process.stdout.write(
|
|
159
|
+
`${designRoot}\n` +
|
|
160
|
+
` ${totalFiles} files · ${(args.bytes / 1024 ** 3).toFixed(2)} GB nominal` +
|
|
161
|
+
`${args.realBytes ? ' (materialised)' : ' (sparse)'}\n` +
|
|
162
|
+
` ${canvasCount} canvases · ${made} assets · ${over.length} over the 95 MB door ceiling\n`
|
|
163
|
+
);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Relative-import rewriting for a canvas that CHANGES FOLDER (issue #114).
|
|
2
|
+
//
|
|
3
|
+
// THE BUG THIS MODULE EXISTS TO FIX. `moveCanvas` / `moveFolder` relocated a
|
|
4
|
+
// canvas with a bare `rename()` — bytes moved, specifiers didn't. A canvas
|
|
5
|
+
// dragged from `ui/` into `ui/print/` kept
|
|
6
|
+
//
|
|
7
|
+
// import { … } from "../system/alligators/preview/_kit";
|
|
8
|
+
// import "../system/alligators/preview/_layout.css";
|
|
9
|
+
//
|
|
10
|
+
// which is only correct one level up, so the canvas 500s on the next build with
|
|
11
|
+
// `Could not resolve`. Nothing anywhere in the tree rewrote imports; the
|
|
12
|
+
// capability simply did not exist. It reads like a sync bug from the outside
|
|
13
|
+
// (the broken file shows up on every peer), which is how it got reported
|
|
14
|
+
// alongside one — but it is entirely local to the move.
|
|
15
|
+
//
|
|
16
|
+
// Deliberately regex-over-known-vocabulary rather than a parser: it matches the
|
|
17
|
+
// house style of the other static readers (`_canvas-rects-static.mjs`), it
|
|
18
|
+
// cannot fail closed on a canvas whose syntax a parser would reject, and the
|
|
19
|
+
// vocabulary is small and fixed. Only `./`- and `../`-prefixed specifiers are
|
|
20
|
+
// touched — a bare or aliased specifier (`react`, `@maude/canvas-lib`) is
|
|
21
|
+
// resolved by the bundler, not by depth, and rewriting one would break it.
|
|
22
|
+
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Every shape that can carry a module specifier in a canvas body, as
|
|
27
|
+
* [pattern, index of the capture group holding the quoted specifier].
|
|
28
|
+
*
|
|
29
|
+
* from "…" — `import x from`, `export … from`
|
|
30
|
+
* import "…" — side-effect import (this is the one that carries `_layout.css`)
|
|
31
|
+
* import("…") — dynamic import
|
|
32
|
+
*
|
|
33
|
+
* `require("…")` is deliberately absent: canvases are ESM (Bun.build), and a
|
|
34
|
+
* `require` in one is not something this should quietly rewrite.
|
|
35
|
+
*/
|
|
36
|
+
const SPECIFIER_PATTERNS: readonly RegExp[] = [
|
|
37
|
+
/(\bfrom\s*)(["'])([^"']+)\2/g,
|
|
38
|
+
/(\bimport\s*)(["'])([^"']+)\2/g,
|
|
39
|
+
/(\bimport\s*\(\s*)(["'])([^"']+)\2/g,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** Is this a path-relative specifier (the only kind whose meaning depends on
|
|
43
|
+
* which folder the importing file sits in)? */
|
|
44
|
+
export function isRelativeSpecifier(spec: string): boolean {
|
|
45
|
+
return spec.startsWith('./') || spec.startsWith('../');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Re-root one relative specifier from `fromDir` to `toDir`.
|
|
50
|
+
*
|
|
51
|
+
* Both dirs are POSIX, design-root-relative (`''` = the design root itself).
|
|
52
|
+
* Returns the specifier unchanged when it isn't relative.
|
|
53
|
+
*/
|
|
54
|
+
export function rerootSpecifier(spec: string, fromDir: string, toDir: string): string {
|
|
55
|
+
if (!isRelativeSpecifier(spec)) return spec;
|
|
56
|
+
// What the specifier pointed AT, as a design-root-relative path. `join` +
|
|
57
|
+
// `normalize` collapse the `..` segments; a target above the design root stays
|
|
58
|
+
// expressed with leading `..` and re-roots just as correctly.
|
|
59
|
+
const target = path.posix.normalize(path.posix.join(fromDir, spec));
|
|
60
|
+
let next = path.posix.relative(toDir, target);
|
|
61
|
+
// `relative` drops the leading `./` that makes a same-dir specifier relative
|
|
62
|
+
// rather than bare — without it `sibling.css` would resolve as a PACKAGE.
|
|
63
|
+
if (!next.startsWith('.')) next = `./${next}`;
|
|
64
|
+
// A trailing slash in the source (`from "../lib/"`) is meaningful to a
|
|
65
|
+
// resolver; `normalize`/`relative` drop it, so put it back.
|
|
66
|
+
if (spec.endsWith('/') && !next.endsWith('/')) next = `${next}/`;
|
|
67
|
+
return next;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Rewrite every relative import in `source` for a file that moved from
|
|
72
|
+
* `fromDir` to `toDir` (both POSIX, design-root-relative).
|
|
73
|
+
*
|
|
74
|
+
* Returns the source unchanged when the two dirs are the same or nothing
|
|
75
|
+
* relative was found, so a caller can cheaply skip the write.
|
|
76
|
+
*/
|
|
77
|
+
export function rewriteRelativeImports(source: string, fromDir: string, toDir: string): string {
|
|
78
|
+
const from = path.posix.normalize(fromDir === '' ? '.' : fromDir);
|
|
79
|
+
const to = path.posix.normalize(toDir === '' ? '.' : toDir);
|
|
80
|
+
if (from === to) return source;
|
|
81
|
+
let out = source;
|
|
82
|
+
for (const pattern of SPECIFIER_PATTERNS) {
|
|
83
|
+
out = out.replace(pattern, (whole, lead: string, quote: string, spec: string) => {
|
|
84
|
+
if (!isRelativeSpecifier(spec)) return whole;
|
|
85
|
+
return `${lead}${quote}${rerootSpecifier(spec, from, to)}${quote}`;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|