diff --git a/Cargo.lock b/Cargo.lock index a9e34ebd4..45fe80387 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3143,7 +3143,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "op-acp" -version = "0.8.1" +version = "0.8.2" dependencies = [ "futures-util", "op-ai", @@ -3157,11 +3157,11 @@ dependencies = [ [[package]] name = "op-ai" -version = "0.8.1" +version = "0.8.2" [[package]] name = "op-ai-skills" -version = "0.8.1" +version = "0.8.2" dependencies = [ "include_dir", "jian-ops-schema", @@ -3170,7 +3170,7 @@ dependencies = [ [[package]] name = "op-cli" -version = "0.8.1" +version = "0.8.2" dependencies = [ "base64", "jian-ops-schema", @@ -3186,7 +3186,7 @@ dependencies = [ [[package]] name = "op-codegen" -version = "0.8.1" +version = "0.8.2" dependencies = [ "base64", "jian-ops-schema", @@ -3199,7 +3199,7 @@ dependencies = [ [[package]] name = "op-config-store" -version = "0.8.1" +version = "0.8.2" dependencies = [ "serde", "serde_json", @@ -3207,7 +3207,7 @@ dependencies = [ [[package]] name = "op-design-lint" -version = "0.8.1" +version = "0.8.2" dependencies = [ "jian-ops-schema", "serde", @@ -3216,7 +3216,7 @@ dependencies = [ [[package]] name = "op-editor-core" -version = "0.8.1" +version = "0.8.2" dependencies = [ "glam", "jian-core", @@ -3231,7 +3231,7 @@ dependencies = [ [[package]] name = "op-editor-host-core" -version = "0.8.1" +version = "0.8.2" dependencies = [ "jian-ops-schema", "op-ai", @@ -3245,7 +3245,7 @@ dependencies = [ [[package]] name = "op-editor-ui" -version = "0.8.1" +version = "0.8.2" dependencies = [ "accesskit", "base64", @@ -3263,7 +3263,7 @@ dependencies = [ [[package]] name = "op-figma" -version = "0.8.1" +version = "0.8.2" dependencies = [ "base64", "flate2", @@ -3278,7 +3278,7 @@ dependencies = [ [[package]] name = "op-git" -version = "0.8.1" +version = "0.8.2" dependencies = [ "git2", "op-config-store", @@ -3289,7 +3289,7 @@ dependencies = [ [[package]] name = "op-host-desktop" -version = "0.8.1" +version = "0.8.2" dependencies = [ "accesskit", "accesskit_macos", @@ -3346,7 +3346,7 @@ dependencies = [ [[package]] name = "op-host-native" -version = "0.8.1" +version = "0.8.2" dependencies = [ "accesskit", "casement", @@ -3375,7 +3375,7 @@ dependencies = [ [[package]] name = "op-host-services" -version = "0.8.1" +version = "0.8.2" dependencies = [ "agent", "anthropic-agent-sdk", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "op-host-web" -version = "0.8.1" +version = "0.8.2" dependencies = [ "accesskit", "base64", @@ -3439,7 +3439,7 @@ dependencies = [ [[package]] name = "op-host-web-server" -version = "0.8.1" +version = "0.8.2" dependencies = [ "op-host-services", "serde_json", @@ -3447,7 +3447,7 @@ dependencies = [ [[package]] name = "op-html" -version = "0.8.1" +version = "0.8.2" dependencies = [ "base64", "encoding_rs", @@ -3462,11 +3462,11 @@ dependencies = [ [[package]] name = "op-i18n" -version = "0.8.1" +version = "0.8.2" [[package]] name = "op-mcp" -version = "0.8.1" +version = "0.8.2" dependencies = [ "jian-ops-schema", "jian-scene", @@ -3484,7 +3484,7 @@ dependencies = [ [[package]] name = "op-opmerge" -version = "0.8.1" +version = "0.8.2" dependencies = [ "jian-ops-schema", "serde_json", @@ -3493,7 +3493,7 @@ dependencies = [ [[package]] name = "op-orchestrator" -version = "0.8.1" +version = "0.8.2" dependencies = [ "futures", "jian-ops-schema", @@ -3512,7 +3512,7 @@ dependencies = [ [[package]] name = "op-pen-loader" -version = "0.8.1" +version = "0.8.2" dependencies = [ "jian-core", "jian-ops-schema", @@ -3527,14 +3527,14 @@ dependencies = [ [[package]] name = "op-process-io" -version = "0.8.1" +version = "0.8.2" dependencies = [ "tokio", ] [[package]] name = "op-rpc-transport" -version = "0.8.1" +version = "0.8.2" dependencies = [ "serde", "serde_json", @@ -3542,7 +3542,7 @@ dependencies = [ [[package]] name = "op-smoke" -version = "0.8.1" +version = "0.8.2" dependencies = [ "agent", "futures", @@ -3561,7 +3561,7 @@ dependencies = [ [[package]] name = "op-web-sdk" -version = "0.8.1" +version = "0.8.2" dependencies = [ "console_error_panic_hook", "jian-ops-schema", diff --git a/Cargo.toml b/Cargo.toml index 1aa54dd85..871f4083b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ exclude = [ default-members = ["crates/op-host-desktop", "crates/op-cli", "crates/op-host-web-server"] [workspace.package] -version = "0.8.1" +version = "0.8.2" edition = "2021" # 1.87 floor: op-mcp's `script` feature (enabled unconditionally by # op-host-services + op-orchestrator) depends on rquickjs 0.12, whose diff --git a/RELEASE_NOTES/v0.8.2.md b/RELEASE_NOTES/v0.8.2.md new file mode 100644 index 000000000..234f2cee7 --- /dev/null +++ b/RELEASE_NOTES/v0.8.2.md @@ -0,0 +1,95 @@ +# OpenPencil v0.8.2 + +`v0.8.2` is a pre-release update focused on import fidelity, large-document +performance, safer persistence, and backward-compatible OpenPencil files. + +## Highlights + +- Figma imports preserve substantially more authored structure and appearance, + including component instances and swaps, masks, blend modes, page + backgrounds, image tiling, vectors, text, child order, and absolute layout. +- Import preparation, image resolution, text measurement, and component lookup + reuse cached work, while cancellation and background conversion keep the + desktop UI responsive. +- Imported desktop files can be written as adjacent `.op` documents. Existing + targets require confirmation; choosing not to replace creates a numbered + copy. +- Large `.op` files use memory-mapped input and a fast Preserve-mode loader. + Only the active page scene is built for editing, and the previous page scene + is released when switching pages. +- Page switches automatically fit and center the newly active page. +- Save and Save As run through serialized background jobs. Clean bound + documents can stream their source bytes while rewriting only editor + metadata, avoiding a full document clone. +- Legacy `.op` files are detected after compatibility repair and can be + rewritten in the current format, saved as a numbered copy, or opened without + conversion. +- HTML import accepts complete pages, multi-file projects, and ZIP packages + with local CSS, images, nested imports, and common text encodings. + +## Figma Fidelity + +- Component properties and instance overrides retain component backing data, + swap targets, authored geometry, and style references. +- Alpha, vector, and luminance masks render through the shared native and web + paint paths. +- Layer and fill blend modes, image tile scale, and page background color are + represented in the schema and editable in the property inspector. +- Boolean fallbacks, vector bounds, image fills, text metrics, layout + positioning, and paint ordering more closely match the source Figma file. +- The generated TypeScript schema bindings and web SDK types remain compatible + with older optional fields. + +## Persistence and Compatibility + +- The current `.op` format marker is `1.2`. +- Existing current-format documents do not trigger migration solely because + optional editor metadata is absent. +- Legacy conversion preserves unknown top-level and nested fields, handles + deeply nested documents with stack protection, and writes through an atomic + sibling temporary file. +- Before replacing an original document, OpenPencil verifies that the source + has not changed since it was opened. A concurrent external change falls back + to a numbered copy. +- Failed migration or save publication does not bind stale in-memory state to + the target path. + +## Performance + +- Figma preparation caches decoded values, text measurements, image lookups, + instance assignments, and component style references. +- Desktop document loading maps the source file instead of first copying the + entire file into a second byte buffer. +- Preserve-authored documents can skip the generic full JSON compatibility DOM + when the fast loader proves the current shape is safe. +- Scene construction is active-page scoped across desktop, web, MCP, image + search, and export consumers. +- Large import and save payloads use indirection or streaming paths to reduce + queue size and peak allocations. + +## Web and SDK + +- Web Figma conversion stages output through a worker-backed temporary writer + instead of retaining every generated byte in the main UI task. +- Web save requests are queued and coalesced so an older serialization cannot + publish after a newer edit. +- Page fitting, compositing properties, masks, image tiling, and editor metadata + round-trip through the web host. +- `@zseven-w/op-web-sdk`, `@zseven-w/op-web-sdk-react`, and + `@zseven-w/op-web-sdk-vue` are synchronized at `0.8.2`. + +## Release Artifacts + +- Native desktop installers and standalone `op` CLI archives for macOS, + Windows, and Linux. +- `ghcr.io/zseven-w/openpencil-web:v0.8.2` for the Rust web host. +- Web SDK packages under the npm `next` tag, with package tarballs attached to + the GitHub prerelease. + +## Compatibility Notes + +- The public web SDK remains a read-only viewer surface. +- Active-page scene loading reduces renderer memory, but the canonical document + still retains all page data while open. +- Future incompatible format majors are rejected instead of being rewritten. +- The retired TypeScript application workspace is not part of this release. diff --git a/crates/op-editor-core/src/account_state.rs b/crates/op-editor-core/src/account_state.rs index 2bdd8d05c..cd4d9f8e3 100644 --- a/crates/op-editor-core/src/account_state.rs +++ b/crates/op-editor-core/src/account_state.rs @@ -1,5 +1,5 @@ -//! Account / sign-in state for the v0.8.2 user system (platform + -//! zseven-sso). The real backend is not live yet — this module carries +//! Account / sign-in state for the planned platform + zseven-sso user +//! system. The real backend is not live yet — this module carries //! the state model plus a dev-only fake-login seam so the topbar avatar //! button, its dropdown, the sign-in modal, and the settings modal's //! Account tab can all be built and exercised end-to-end before the diff --git a/crates/op-editor-core/src/agent_settings.rs b/crates/op-editor-core/src/agent_settings.rs index c036e9f8f..8b9176db4 100644 --- a/crates/op-editor-core/src/agent_settings.rs +++ b/crates/op-editor-core/src/agent_settings.rs @@ -25,7 +25,7 @@ pub enum AgentSettingsTab { Images, Fonts, System, - /// Sign-in status + workspace identity (v0.8.2 user system). + /// Sign-in status + workspace identity for the planned user system. Account, } diff --git a/crates/op-editor-core/src/editor_ui_state.rs b/crates/op-editor-core/src/editor_ui_state.rs index aefced52f..304e4e82b 100644 --- a/crates/op-editor-core/src/editor_ui_state.rs +++ b/crates/op-editor-core/src/editor_ui_state.rs @@ -1096,7 +1096,7 @@ pub struct EditorUiState { /// settings-modal input. pub settings_input: jian_core::text_input::TextInputState, - // --- Account (v0.8.2 user system: platform + zseven-sso) -------- + // --- Account (planned platform + zseven-sso user system) ------- /// Signed-in / signed-out identity. The backend (OIDC Auth Code + /// PKCE via system browser) is not wired yet — see /// `AccountState::dev_fake_signed_in` for the dev-only fast path. diff --git a/crates/op-editor-ui/src/widgets/login_modal.rs b/crates/op-editor-ui/src/widgets/login_modal.rs index 9e079bef3..ef4d331aa 100644 --- a/crates/op-editor-ui/src/widgets/login_modal.rs +++ b/crates/op-editor-ui/src/widgets/login_modal.rs @@ -3,8 +3,8 @@ //! authentication focused: OpenPencil identity, one browser action, //! and a concise security/status note. //! -//! The real auth flow (v0.8.2 M3: OIDC Auth Code + PKCE via system -//! browser) is not wired yet, so the primary button is an honest stub: +//! The planned auth flow (OIDC Auth Code + PKCE via the system browser) +//! is not wired yet, so the primary button is an honest stub: //! it stays clickable, but production builds reveal a "coming soon" //! note instead of pretending to sign in. `AccountState:: //! dev_fake_signed_in` is the dev/demo fast path (gated host-side by diff --git a/crates/op-editor-ui/src/widgets/top_bar.rs b/crates/op-editor-ui/src/widgets/top_bar.rs index 4fed1c73d..ae1b109fe 100644 --- a/crates/op-editor-ui/src/widgets/top_bar.rs +++ b/crates/op-editor-ui/src/widgets/top_bar.rs @@ -41,7 +41,7 @@ pub(super) const GIT_BUTTON_AVAILABLE: bool = !cfg!(target_arch = "wasm32"); /// has no equivalent runtime/measurement bridge, so hide the button there /// rather than exposing a non-interactive preview flag. pub(super) const PREVIEW_BUTTON_AVAILABLE: bool = !cfg!(target_arch = "wasm32"); -/// The v0.8.2 sign-in flow (OIDC Auth Code + PKCE via the system +/// The planned sign-in flow (OIDC Auth Code + PKCE via the system /// browser) is desktop-only. The web build has no equivalent, so the /// avatar button (and its layout slot) is compiled out there rather /// than painting a click that goes nowhere (same pattern as diff --git a/crates/op-host-desktop/src/figma_import_session/image_sources.rs b/crates/op-host-desktop/src/figma_import_session/image_sources.rs index 07320cc15..0b31b406c 100644 --- a/crates/op-host-desktop/src/figma_import_session/image_sources.rs +++ b/crates/op-host-desktop/src/figma_import_session/image_sources.rs @@ -256,7 +256,7 @@ mod tests { #[test] fn visits_every_typed_image_source_shape_used_by_imports() { let document = serde_json::from_value(serde_json::json!({ - "version": "0.8.2", + "version": "1.0.0", "children": [ { "type": "rectangle", @@ -305,7 +305,7 @@ mod tests { let png_src = "data:image/png;base64,U0FNRQ=="; let jpeg_src = "data:image/jpeg;base64,U0FNRQ=="; let document = serde_json::from_value(serde_json::json!({ - "version": "0.8.2", + "version": "1.0.0", "children": [ {"type": "image", "id": "png", "src": png_src}, {"type": "image", "id": "jpeg", "src": jpeg_src} diff --git a/crates/op-host-desktop/src/figma_import_session/tests.rs b/crates/op-host-desktop/src/figma_import_session/tests.rs index ae852ca5d..a1e4c9d5e 100644 --- a/crates/op-host-desktop/src/figma_import_session/tests.rs +++ b/crates/op-host-desktop/src/figma_import_session/tests.rs @@ -252,7 +252,7 @@ fn deferred_thumbnail_binding_uses_the_final_source_string() { let final_src = format!("data:image/jpeg;base64,{}", B64.encode(final_bytes)); let provisional_src = format!("data:image/png;base64,{}", B64.encode(provisional)); let doc = serde_json::from_value(serde_json::json!({ - "version": "0.8.2", + "version": "1.0.0", "children": [{ "type": "rectangle", "id": "image-fill", diff --git a/crates/op-host-native/src/widget_host/account_press.rs b/crates/op-host-native/src/widget_host/account_press.rs index f6a0ddea1..00c550fe5 100644 --- a/crates/op-host-native/src/widget_host/account_press.rs +++ b/crates/op-host-native/src/widget_host/account_press.rs @@ -43,8 +43,8 @@ impl WidgetHostNative { } LoginModalHit::SignIn => { // Dev/demo fast path — never reachable in a production - // build; the real flow is `v0.8.2 M3: OIDC Auth Code + - // PKCE via system browser`. + // build; the planned real flow uses OIDC Auth Code + + // PKCE via the system browser. if dev_fake_login_enabled() { self.editor_state.editor_ui.account = op_editor_core::AccountState::dev_fake_signed_in(); diff --git a/crates/op-host-web/src/widget_host/agent_settings_press.rs b/crates/op-host-web/src/widget_host/agent_settings_press.rs index c274e20f6..bbbc744de 100644 --- a/crates/op-host-web/src/widget_host/agent_settings_press.rs +++ b/crates/op-host-web/src/widget_host/agent_settings_press.rs @@ -288,8 +288,8 @@ impl WidgetHost { } AgentSettingsHit::OpenLoginModal | AgentSettingsHit::SignOutAccount => { // Unreachable: `AgentSettingsPanelMode::WebBuiltinOnly` - // omits the Account tab from `visible_tabs` (the v0.8.2 - // sign-in flow is desktop-only), so `hit_test` never + // omits the Account tab from `visible_tabs` (the sign-in + // flow is desktop-only), so `hit_test` never // resolves to these variants on the web build. } AgentSettingsHit::FocusMcpPort => { diff --git a/crates/op-host-web/src/widget_host/press.rs b/crates/op-host-web/src/widget_host/press.rs index ead9c3160..ea7c3a594 100644 --- a/crates/op-host-web/src/widget_host/press.rs +++ b/crates/op-host-web/src/widget_host/press.rs @@ -516,7 +516,7 @@ impl WidgetHost { TopBarHit::Account => { // Unreachable: `ACCOUNT_BUTTON_AVAILABLE` gates the // avatar button out of the web build's hit-test (the - // v0.8.2 sign-in flow is desktop-only). + // sign-in flow is desktop-only). } } self.mark_dirty(); diff --git a/packages/bun.lock b/packages/bun.lock index ce2247c96..3c75389a0 100644 --- a/packages/bun.lock +++ b/packages/bun.lock @@ -28,7 +28,7 @@ }, "op-web-sdk": { "name": "@zseven-w/op-web-sdk", - "version": "0.8.1", + "version": "0.8.2", "devDependencies": { "jsdom": "^25.0.0", "tsup": "^8.0.0", @@ -38,7 +38,7 @@ }, "op-web-sdk-react": { "name": "@zseven-w/op-web-sdk-react", - "version": "0.8.1", + "version": "0.8.2", "dependencies": { "@zseven-w/op-web-sdk": "workspace:*", }, @@ -60,7 +60,7 @@ }, "op-web-sdk-vue": { "name": "@zseven-w/op-web-sdk-vue", - "version": "0.8.1", + "version": "0.8.2", "dependencies": { "@zseven-w/op-web-sdk": "workspace:*", }, diff --git a/packages/op-web-sdk-react/package.json b/packages/op-web-sdk-react/package.json index feb723feb..c56f2693f 100644 --- a/packages/op-web-sdk-react/package.json +++ b/packages/op-web-sdk-react/package.json @@ -1,6 +1,6 @@ { "name": "@zseven-w/op-web-sdk-react", - "version": "0.8.1", + "version": "0.8.2", "description": "React adapter for the OpenPencil read-only web viewer SDK", "license": "MIT", "type": "module", diff --git a/packages/op-web-sdk-react/src/index.ts b/packages/op-web-sdk-react/src/index.ts index d1a37d900..48a050aec 100644 --- a/packages/op-web-sdk-react/src/index.ts +++ b/packages/op-web-sdk-react/src/index.ts @@ -1,4 +1,4 @@ -export const VERSION = '0.8.1'; +export const VERSION = '0.8.2'; export { DesignProvider, useViewer } from './use-viewer.js'; export { useDocument, useViewport, useActivePage } from './hooks.js'; export { DesignView, type DesignViewProps } from './design-view.js'; diff --git a/packages/op-web-sdk-vue/package.json b/packages/op-web-sdk-vue/package.json index 54df528af..dccbdcf1c 100644 --- a/packages/op-web-sdk-vue/package.json +++ b/packages/op-web-sdk-vue/package.json @@ -1,6 +1,6 @@ { "name": "@zseven-w/op-web-sdk-vue", - "version": "0.8.1", + "version": "0.8.2", "description": "Vue 3 adapter for the OpenPencil read-only web viewer SDK", "license": "MIT", "type": "module", diff --git a/packages/op-web-sdk-vue/src/index.ts b/packages/op-web-sdk-vue/src/index.ts index a981636fa..a3f5d2d4b 100644 --- a/packages/op-web-sdk-vue/src/index.ts +++ b/packages/op-web-sdk-vue/src/index.ts @@ -1,4 +1,4 @@ -export const VERSION = '0.8.1'; +export const VERSION = '0.8.2'; export { viewerKey } from './injection.js'; export { provideViewer, provideViewerRef, useViewer } from './use-viewer.js'; export { useDocument, useViewport, useActivePage } from './composables.js'; diff --git a/packages/op-web-sdk/package.json b/packages/op-web-sdk/package.json index fd94a30d5..fd8d313e3 100644 --- a/packages/op-web-sdk/package.json +++ b/packages/op-web-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zseven-w/op-web-sdk", - "version": "0.8.1", + "version": "0.8.2", "description": "Read-only OpenPencil .op viewer SDK for the web (wasm-backed)", "license": "MIT", "type": "module", diff --git a/packages/op-web-sdk/src/index.ts b/packages/op-web-sdk/src/index.ts index dce4d3b56..20eb4ea74 100644 --- a/packages/op-web-sdk/src/index.ts +++ b/packages/op-web-sdk/src/index.ts @@ -1,5 +1,5 @@ // Public entry for the OpenPencil read-only web SDK core. -export const VERSION = '0.8.1'; +export const VERSION = '0.8.2'; export { createViewer, OpViewer } from './viewer.js'; export type { Viewport, CreateViewerOptions, PenDocument, PenPage } from './types.js'; export type { ViewerEvent } from './events.js'; diff --git a/packages/package.json b/packages/package.json index 8e8e6516c..ff18a7a97 100644 --- a/packages/package.json +++ b/packages/package.json @@ -1,6 +1,6 @@ { "name": "@zseven-w/openpencil-packages", - "version": "0.8.1", + "version": "0.8.2", "private": true, "description": "Web SDK workspace for OpenPencil (the Rust product lives in ../crates). Hosts the wasm-backed op-web-sdk viewer family plus the JS build glue that feeds the Rust web target.", "author": { diff --git a/packages/scripts/sync-version.mjs b/packages/scripts/sync-version.mjs index 79bfed85c..04c29ae0a 100644 --- a/packages/scripts/sync-version.mjs +++ b/packages/scripts/sync-version.mjs @@ -278,6 +278,40 @@ export function inspectBunLockWorkspaceVersions(source) { return versions; } +export function renderBunLockWorkspaceVersions(source, version) { + const rootEntries = readJsoncObjectEntries(source, 0); + const workspaces = rootEntries.find((entry) => entry.key === 'workspaces'); + if (workspaces === undefined) { + throw new Error('bun.lock does not contain a top-level workspaces object'); + } + + const workspaceEntries = readJsoncObjectEntries(source, workspaces.valueStart); + const replacements = sdkWorkspaceNames.map((workspaceName) => { + const workspace = workspaceEntries.find((entry) => entry.key === workspaceName); + if (workspace === undefined || source[workspace.valueStart] !== '{') { + throw new Error(`bun.lock is missing workspace ${workspaceName}`); + } + const versionEntry = readJsoncObjectEntries(source, workspace.valueStart).find( + (entry) => entry.key === 'version', + ); + if (versionEntry === undefined || source[versionEntry.valueStart] !== '"') { + throw new Error(`bun.lock workspace ${workspaceName} is missing a string version`); + } + return { + start: versionEntry.valueStart, + end: versionEntry.valueEnd, + output: JSON.stringify(version), + }; + }); + + let output = source; + for (const replacement of replacements.toSorted((left, right) => right.start - left.start)) { + output = + output.slice(0, replacement.start) + replacement.output + output.slice(replacement.end); + } + return output; +} + export function collectVersionDrift(expectedVersion, consumers) { return consumers .filter(({ actualVersion }) => actualVersion !== expectedVersion) @@ -481,6 +515,13 @@ export async function synchronizeVersions({ }); } + const lockPath = resolve(packagesRoot, 'bun.lock'); + const lockSource = await readFile(lockPath, 'utf8'); + const lockOutput = renderBunLockWorkspaceVersions(lockSource, version); + if (lockSource !== lockOutput) { + await writeManagedFile(lockPath, lockOutput); + } + const drift = collectVersionDrift(version, await readConsumers(packagesRoot)); if (drift.length > 0) { throw postWriteValidationError(drift); diff --git a/packages/scripts/sync-version.test.mjs b/packages/scripts/sync-version.test.mjs index f21e03797..2cf4f3354 100644 --- a/packages/scripts/sync-version.test.mjs +++ b/packages/scripts/sync-version.test.mjs @@ -9,6 +9,7 @@ import * as syncVersion from './sync-version.mjs'; import { collectVersionDrift, inspectBunLockWorkspaceVersions, + renderBunLockWorkspaceVersions, renderPackageManifest, renderSdkEntry, } from './sync-version.mjs'; @@ -432,6 +433,25 @@ test('Bun lock inspection still rejects unterminated block comments after primit assert.throws(() => inspectBunLockWorkspaceVersions(lockfile), /unterminated block comment/i); }); +test('Bun lock rendering updates only managed workspace version literals', () => { + const lockfile = `{ + "workspaces": { + "": { "name": "@zseven-w/openpencil-packages" }, + "op-web-sdk": { "version": "1.0.0", "fixture": "1.0.0" }, + "op-web-sdk-react": { "version": "1.0.1" }, + "op-web-sdk-vue": { "version": "1.0.2" }, + }, + }`; + + const output = renderBunLockWorkspaceVersions(lockfile, '2.3.4'); + assert.deepEqual(inspectBunLockWorkspaceVersions(output), { + 'op-web-sdk': '2.3.4', + 'op-web-sdk-react': '2.3.4', + 'op-web-sdk-vue': '2.3.4', + }); + assert.match(output, /"fixture": "1\.0\.0"/); +}); + test('drift collection reports each stale path with expected and actual versions', () => { assert.deepEqual( collectVersionDrift('2.3.4', [ @@ -608,22 +628,27 @@ test('write mode restores every managed file when Bun lock regeneration fails', assert.deepEqual(await readManagedFiles(fixture.managedPaths), before); }); -test('write mode restores every managed file when post-write validation fails', async (t) => { +test('write mode repairs workspace versions when Bun leaves them stale', async (t) => { const fixture = await createRepositoryFixture(t); - const before = await readManagedFiles(fixture.managedPaths); const runner = createCommandRunner(fixture.packagesRoot, '2.3.4', { regenerateLock: false, }); - await assert.rejects( - syncVersion.synchronizeVersions({ - mode: 'write', - ...fixture, - runCommand: runner.runCommand, - }), - /post-write validation failed.*bun\.lock/i, + const result = await syncVersion.synchronizeVersions({ + mode: 'write', + ...fixture, + runCommand: runner.runCommand, + }); + + assert.deepEqual(result.drift, []); + assert.deepEqual( + inspectBunLockWorkspaceVersions(await readFile(fixture.managedPaths[7], 'utf8')), + { + 'op-web-sdk': '2.3.4', + 'op-web-sdk-react': '2.3.4', + 'op-web-sdk-vue': '2.3.4', + }, ); - assert.deepEqual(await readManagedFiles(fixture.managedPaths), before); }); test('write mode waits for sibling writes to settle before rollback', async (t) => {