chore(release): bump version to 0.8.2

This commit is contained in:
Kayshen-X 2026-07-23 21:23:21 +08:00
parent 70e202d802
commit a1e7151f4b
23 changed files with 224 additions and 63 deletions

54
Cargo.lock generated
View file

@ -3143,7 +3143,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]] [[package]]
name = "op-acp" name = "op-acp"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"op-ai", "op-ai",
@ -3157,11 +3157,11 @@ dependencies = [
[[package]] [[package]]
name = "op-ai" name = "op-ai"
version = "0.8.1" version = "0.8.2"
[[package]] [[package]]
name = "op-ai-skills" name = "op-ai-skills"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"include_dir", "include_dir",
"jian-ops-schema", "jian-ops-schema",
@ -3170,7 +3170,7 @@ dependencies = [
[[package]] [[package]]
name = "op-cli" name = "op-cli"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"base64", "base64",
"jian-ops-schema", "jian-ops-schema",
@ -3186,7 +3186,7 @@ dependencies = [
[[package]] [[package]]
name = "op-codegen" name = "op-codegen"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"base64", "base64",
"jian-ops-schema", "jian-ops-schema",
@ -3199,7 +3199,7 @@ dependencies = [
[[package]] [[package]]
name = "op-config-store" name = "op-config-store"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@ -3207,7 +3207,7 @@ dependencies = [
[[package]] [[package]]
name = "op-design-lint" name = "op-design-lint"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"jian-ops-schema", "jian-ops-schema",
"serde", "serde",
@ -3216,7 +3216,7 @@ dependencies = [
[[package]] [[package]]
name = "op-editor-core" name = "op-editor-core"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"glam", "glam",
"jian-core", "jian-core",
@ -3231,7 +3231,7 @@ dependencies = [
[[package]] [[package]]
name = "op-editor-host-core" name = "op-editor-host-core"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"jian-ops-schema", "jian-ops-schema",
"op-ai", "op-ai",
@ -3245,7 +3245,7 @@ dependencies = [
[[package]] [[package]]
name = "op-editor-ui" name = "op-editor-ui"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"base64", "base64",
@ -3263,7 +3263,7 @@ dependencies = [
[[package]] [[package]]
name = "op-figma" name = "op-figma"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"base64", "base64",
"flate2", "flate2",
@ -3278,7 +3278,7 @@ dependencies = [
[[package]] [[package]]
name = "op-git" name = "op-git"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"git2", "git2",
"op-config-store", "op-config-store",
@ -3289,7 +3289,7 @@ dependencies = [
[[package]] [[package]]
name = "op-host-desktop" name = "op-host-desktop"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_macos", "accesskit_macos",
@ -3346,7 +3346,7 @@ dependencies = [
[[package]] [[package]]
name = "op-host-native" name = "op-host-native"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"casement", "casement",
@ -3375,7 +3375,7 @@ dependencies = [
[[package]] [[package]]
name = "op-host-services" name = "op-host-services"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"agent", "agent",
"anthropic-agent-sdk", "anthropic-agent-sdk",
@ -3412,7 +3412,7 @@ dependencies = [
[[package]] [[package]]
name = "op-host-web" name = "op-host-web"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"base64", "base64",
@ -3439,7 +3439,7 @@ dependencies = [
[[package]] [[package]]
name = "op-host-web-server" name = "op-host-web-server"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"op-host-services", "op-host-services",
"serde_json", "serde_json",
@ -3447,7 +3447,7 @@ dependencies = [
[[package]] [[package]]
name = "op-html" name = "op-html"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"base64", "base64",
"encoding_rs", "encoding_rs",
@ -3462,11 +3462,11 @@ dependencies = [
[[package]] [[package]]
name = "op-i18n" name = "op-i18n"
version = "0.8.1" version = "0.8.2"
[[package]] [[package]]
name = "op-mcp" name = "op-mcp"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"jian-ops-schema", "jian-ops-schema",
"jian-scene", "jian-scene",
@ -3484,7 +3484,7 @@ dependencies = [
[[package]] [[package]]
name = "op-opmerge" name = "op-opmerge"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"jian-ops-schema", "jian-ops-schema",
"serde_json", "serde_json",
@ -3493,7 +3493,7 @@ dependencies = [
[[package]] [[package]]
name = "op-orchestrator" name = "op-orchestrator"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"futures", "futures",
"jian-ops-schema", "jian-ops-schema",
@ -3512,7 +3512,7 @@ dependencies = [
[[package]] [[package]]
name = "op-pen-loader" name = "op-pen-loader"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"jian-core", "jian-core",
"jian-ops-schema", "jian-ops-schema",
@ -3527,14 +3527,14 @@ dependencies = [
[[package]] [[package]]
name = "op-process-io" name = "op-process-io"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"tokio", "tokio",
] ]
[[package]] [[package]]
name = "op-rpc-transport" name = "op-rpc-transport"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@ -3542,7 +3542,7 @@ dependencies = [
[[package]] [[package]]
name = "op-smoke" name = "op-smoke"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"agent", "agent",
"futures", "futures",
@ -3561,7 +3561,7 @@ dependencies = [
[[package]] [[package]]
name = "op-web-sdk" name = "op-web-sdk"
version = "0.8.1" version = "0.8.2"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"jian-ops-schema", "jian-ops-schema",

View file

@ -24,7 +24,7 @@ exclude = [
default-members = ["crates/op-host-desktop", "crates/op-cli", "crates/op-host-web-server"] default-members = ["crates/op-host-desktop", "crates/op-cli", "crates/op-host-web-server"]
[workspace.package] [workspace.package]
version = "0.8.1" version = "0.8.2"
edition = "2021" edition = "2021"
# 1.87 floor: op-mcp's `script` feature (enabled unconditionally by # 1.87 floor: op-mcp's `script` feature (enabled unconditionally by
# op-host-services + op-orchestrator) depends on rquickjs 0.12, whose # op-host-services + op-orchestrator) depends on rquickjs 0.12, whose

95
RELEASE_NOTES/v0.8.2.md Normal file
View file

@ -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.

View file

@ -1,5 +1,5 @@
//! Account / sign-in state for the v0.8.2 user system (platform + //! Account / sign-in state for the planned platform + zseven-sso user
//! zseven-sso). The real backend is not live yet — this module carries //! 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 //! 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 //! 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 //! Account tab can all be built and exercised end-to-end before the

View file

@ -25,7 +25,7 @@ pub enum AgentSettingsTab {
Images, Images,
Fonts, Fonts,
System, System,
/// Sign-in status + workspace identity (v0.8.2 user system). /// Sign-in status + workspace identity for the planned user system.
Account, Account,
} }

View file

@ -1096,7 +1096,7 @@ pub struct EditorUiState {
/// settings-modal input. /// settings-modal input.
pub settings_input: jian_core::text_input::TextInputState, 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 + /// Signed-in / signed-out identity. The backend (OIDC Auth Code +
/// PKCE via system browser) is not wired yet — see /// PKCE via system browser) is not wired yet — see
/// `AccountState::dev_fake_signed_in` for the dev-only fast path. /// `AccountState::dev_fake_signed_in` for the dev-only fast path.

View file

@ -3,8 +3,8 @@
//! authentication focused: OpenPencil identity, one browser action, //! authentication focused: OpenPencil identity, one browser action,
//! and a concise security/status note. //! and a concise security/status note.
//! //!
//! The real auth flow (v0.8.2 M3: OIDC Auth Code + PKCE via system //! The planned auth flow (OIDC Auth Code + PKCE via the system browser)
//! browser) is not wired yet, so the primary button is an honest stub: //! is not wired yet, so the primary button is an honest stub:
//! it stays clickable, but production builds reveal a "coming soon" //! it stays clickable, but production builds reveal a "coming soon"
//! note instead of pretending to sign in. `AccountState:: //! note instead of pretending to sign in. `AccountState::
//! dev_fake_signed_in` is the dev/demo fast path (gated host-side by //! dev_fake_signed_in` is the dev/demo fast path (gated host-side by

View file

@ -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 /// has no equivalent runtime/measurement bridge, so hide the button there
/// rather than exposing a non-interactive preview flag. /// rather than exposing a non-interactive preview flag.
pub(super) const PREVIEW_BUTTON_AVAILABLE: bool = !cfg!(target_arch = "wasm32"); 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 /// browser) is desktop-only. The web build has no equivalent, so the
/// avatar button (and its layout slot) is compiled out there rather /// avatar button (and its layout slot) is compiled out there rather
/// than painting a click that goes nowhere (same pattern as /// than painting a click that goes nowhere (same pattern as

View file

@ -256,7 +256,7 @@ mod tests {
#[test] #[test]
fn visits_every_typed_image_source_shape_used_by_imports() { fn visits_every_typed_image_source_shape_used_by_imports() {
let document = serde_json::from_value(serde_json::json!({ let document = serde_json::from_value(serde_json::json!({
"version": "0.8.2", "version": "1.0.0",
"children": [ "children": [
{ {
"type": "rectangle", "type": "rectangle",
@ -305,7 +305,7 @@ mod tests {
let png_src = "data:image/png;base64,U0FNRQ=="; let png_src = "data:image/png;base64,U0FNRQ==";
let jpeg_src = "data:image/jpeg;base64,U0FNRQ=="; let jpeg_src = "data:image/jpeg;base64,U0FNRQ==";
let document = serde_json::from_value(serde_json::json!({ let document = serde_json::from_value(serde_json::json!({
"version": "0.8.2", "version": "1.0.0",
"children": [ "children": [
{"type": "image", "id": "png", "src": png_src}, {"type": "image", "id": "png", "src": png_src},
{"type": "image", "id": "jpeg", "src": jpeg_src} {"type": "image", "id": "jpeg", "src": jpeg_src}

View file

@ -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 final_src = format!("data:image/jpeg;base64,{}", B64.encode(final_bytes));
let provisional_src = format!("data:image/png;base64,{}", B64.encode(provisional)); let provisional_src = format!("data:image/png;base64,{}", B64.encode(provisional));
let doc = serde_json::from_value(serde_json::json!({ let doc = serde_json::from_value(serde_json::json!({
"version": "0.8.2", "version": "1.0.0",
"children": [{ "children": [{
"type": "rectangle", "type": "rectangle",
"id": "image-fill", "id": "image-fill",

View file

@ -43,8 +43,8 @@ impl WidgetHostNative {
} }
LoginModalHit::SignIn => { LoginModalHit::SignIn => {
// Dev/demo fast path — never reachable in a production // Dev/demo fast path — never reachable in a production
// build; the real flow is `v0.8.2 M3: OIDC Auth Code + // build; the planned real flow uses OIDC Auth Code +
// PKCE via system browser`. // PKCE via the system browser.
if dev_fake_login_enabled() { if dev_fake_login_enabled() {
self.editor_state.editor_ui.account = self.editor_state.editor_ui.account =
op_editor_core::AccountState::dev_fake_signed_in(); op_editor_core::AccountState::dev_fake_signed_in();

View file

@ -288,8 +288,8 @@ impl WidgetHost {
} }
AgentSettingsHit::OpenLoginModal | AgentSettingsHit::SignOutAccount => { AgentSettingsHit::OpenLoginModal | AgentSettingsHit::SignOutAccount => {
// Unreachable: `AgentSettingsPanelMode::WebBuiltinOnly` // Unreachable: `AgentSettingsPanelMode::WebBuiltinOnly`
// omits the Account tab from `visible_tabs` (the v0.8.2 // omits the Account tab from `visible_tabs` (the sign-in
// sign-in flow is desktop-only), so `hit_test` never // flow is desktop-only), so `hit_test` never
// resolves to these variants on the web build. // resolves to these variants on the web build.
} }
AgentSettingsHit::FocusMcpPort => { AgentSettingsHit::FocusMcpPort => {

View file

@ -516,7 +516,7 @@ impl WidgetHost {
TopBarHit::Account => { TopBarHit::Account => {
// Unreachable: `ACCOUNT_BUTTON_AVAILABLE` gates the // Unreachable: `ACCOUNT_BUTTON_AVAILABLE` gates the
// avatar button out of the web build's hit-test (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(); self.mark_dirty();

View file

@ -28,7 +28,7 @@
}, },
"op-web-sdk": { "op-web-sdk": {
"name": "@zseven-w/op-web-sdk", "name": "@zseven-w/op-web-sdk",
"version": "0.8.1", "version": "0.8.2",
"devDependencies": { "devDependencies": {
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"tsup": "^8.0.0", "tsup": "^8.0.0",
@ -38,7 +38,7 @@
}, },
"op-web-sdk-react": { "op-web-sdk-react": {
"name": "@zseven-w/op-web-sdk-react", "name": "@zseven-w/op-web-sdk-react",
"version": "0.8.1", "version": "0.8.2",
"dependencies": { "dependencies": {
"@zseven-w/op-web-sdk": "workspace:*", "@zseven-w/op-web-sdk": "workspace:*",
}, },
@ -60,7 +60,7 @@
}, },
"op-web-sdk-vue": { "op-web-sdk-vue": {
"name": "@zseven-w/op-web-sdk-vue", "name": "@zseven-w/op-web-sdk-vue",
"version": "0.8.1", "version": "0.8.2",
"dependencies": { "dependencies": {
"@zseven-w/op-web-sdk": "workspace:*", "@zseven-w/op-web-sdk": "workspace:*",
}, },

View file

@ -1,6 +1,6 @@
{ {
"name": "@zseven-w/op-web-sdk-react", "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", "description": "React adapter for the OpenPencil read-only web viewer SDK",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",

View file

@ -1,4 +1,4 @@
export const VERSION = '0.8.1'; export const VERSION = '0.8.2';
export { DesignProvider, useViewer } from './use-viewer.js'; export { DesignProvider, useViewer } from './use-viewer.js';
export { useDocument, useViewport, useActivePage } from './hooks.js'; export { useDocument, useViewport, useActivePage } from './hooks.js';
export { DesignView, type DesignViewProps } from './design-view.js'; export { DesignView, type DesignViewProps } from './design-view.js';

View file

@ -1,6 +1,6 @@
{ {
"name": "@zseven-w/op-web-sdk-vue", "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", "description": "Vue 3 adapter for the OpenPencil read-only web viewer SDK",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",

View file

@ -1,4 +1,4 @@
export const VERSION = '0.8.1'; export const VERSION = '0.8.2';
export { viewerKey } from './injection.js'; export { viewerKey } from './injection.js';
export { provideViewer, provideViewerRef, useViewer } from './use-viewer.js'; export { provideViewer, provideViewerRef, useViewer } from './use-viewer.js';
export { useDocument, useViewport, useActivePage } from './composables.js'; export { useDocument, useViewport, useActivePage } from './composables.js';

View file

@ -1,6 +1,6 @@
{ {
"name": "@zseven-w/op-web-sdk", "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)", "description": "Read-only OpenPencil .op viewer SDK for the web (wasm-backed)",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",

View file

@ -1,5 +1,5 @@
// Public entry for the OpenPencil read-only web SDK core. // 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 { createViewer, OpViewer } from './viewer.js';
export type { Viewport, CreateViewerOptions, PenDocument, PenPage } from './types.js'; export type { Viewport, CreateViewerOptions, PenDocument, PenPage } from './types.js';
export type { ViewerEvent } from './events.js'; export type { ViewerEvent } from './events.js';

View file

@ -1,6 +1,6 @@
{ {
"name": "@zseven-w/openpencil-packages", "name": "@zseven-w/openpencil-packages",
"version": "0.8.1", "version": "0.8.2",
"private": true, "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.", "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": { "author": {

View file

@ -278,6 +278,40 @@ export function inspectBunLockWorkspaceVersions(source) {
return versions; 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) { export function collectVersionDrift(expectedVersion, consumers) {
return consumers return consumers
.filter(({ actualVersion }) => actualVersion !== expectedVersion) .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)); const drift = collectVersionDrift(version, await readConsumers(packagesRoot));
if (drift.length > 0) { if (drift.length > 0) {
throw postWriteValidationError(drift); throw postWriteValidationError(drift);

View file

@ -9,6 +9,7 @@ import * as syncVersion from './sync-version.mjs';
import { import {
collectVersionDrift, collectVersionDrift,
inspectBunLockWorkspaceVersions, inspectBunLockWorkspaceVersions,
renderBunLockWorkspaceVersions,
renderPackageManifest, renderPackageManifest,
renderSdkEntry, renderSdkEntry,
} from './sync-version.mjs'; } 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); 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', () => { test('drift collection reports each stale path with expected and actual versions', () => {
assert.deepEqual( assert.deepEqual(
collectVersionDrift('2.3.4', [ 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); 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 fixture = await createRepositoryFixture(t);
const before = await readManagedFiles(fixture.managedPaths);
const runner = createCommandRunner(fixture.packagesRoot, '2.3.4', { const runner = createCommandRunner(fixture.packagesRoot, '2.3.4', {
regenerateLock: false, regenerateLock: false,
}); });
await assert.rejects( const result = await syncVersion.synchronizeVersions({
syncVersion.synchronizeVersions({ mode: 'write',
mode: 'write', ...fixture,
...fixture, runCommand: runner.runCommand,
runCommand: runner.runCommand, });
}),
/post-write validation failed.*bun\.lock/i, 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) => { test('write mode waits for sibling writes to settle before rollback', async (t) => {