Pulp Engine Document Rendering
Get started
Release v0.34.0

Pulp Engine v0.34.0 — Release Notes

This release consolidates all work since v0.27.0. Eight distinct improvements are shipped together: two operator-facing fixes, one editor UX fix, and five successive rounds of editor bundle optimisation that together cut the initial JavaScript payload by 62 %.

No breaking changes. All existing deployments are unaffected.


What changed

1. SQL Server Docker migration parity

SQL Server operators can now apply schema migrations directly from the versioned image tag — no repo checkout, no TypeScript tooling, no dev dependencies required.

Previously, running the compiled migration script (dist/scripts/migrate-sqlserver.js) inside the container failed because the .sql migration files were not copied to dist/ by tsc. The build step now copies the SQL migration files alongside the compiled JavaScript into dist/storage/sqlserver/migrations/.

# Run migrations (one-off container — exits after apply)
docker run --rm \
  -e SQL_SERVER_URL=mssql://user:pass@host:1433/pulp-engine?trustServerCertificate=true \
  ghcr.io/OWNER/pulp-engine:v0.34.0 \
  node dist/scripts/migrate-sqlserver.js

# Start the app container
docker run -d \
  --name pulp-engine \
  -p 3000:3000 \
  -e NODE_ENV=production \
  -e STORAGE_MODE=sqlserver \
  -e SQL_SERVER_URL=mssql://user:pass@host:1433/pulp-engine?trustServerCertificate=true \
  -e API_KEY_ADMIN=your-secret-key \
  -v /var/pulp-engine/assets:/data/assets \
  ghcr.io/OWNER/pulp-engine:v0.34.0

SQL Server and Postgres now have equal Docker deployment parity. The non-Docker migration path (pnpm --filter @pulp-engine/api db:migrate:sqlserver) continues to work unchanged.


2. Legacy SVG asset detection and remediation

The API now detects legacy SVG assets (uploaded before v0.27.0 began rejecting them) at startup and provides a first-class audit and remediation workflow.

Startup warning with two-signal detection

A structured legacy_svg_detected warning is emitted at startup with the count of affected assets when either signal matches: declared mimeType: image/svg+xml in stored metadata, or filename ending with .svg. The warning repeats on every restart until assets are removed.

GET /assets?legacySvg=true — primary audit endpoint

curl -s "http://localhost:3000/assets?legacySvg=true" \
  -H "X-Api-Key: $API_KEY_ADMIN"

GET /assets?mimeType=<type> — general-purpose MIME filter

Both filters compose with the existing ?q= substring filter using AND logic.

Remediation workflow

  1. Check the startup log for legacy_svg_detected — if absent, no action needed
  2. GET /assets?legacySvg=true to enumerate affected assets
  3. For each: identify template references, upload a PNG/WebP replacement, update the template definition
  4. DELETE /assets/:id once no templates reference the legacy SVG
  5. Restart — the warning will not appear once all matching assets are removed

See the runbook § Asset upload validation for the full workflow.


3. richText undo/redo trust gap closed

Three interconnected bugs that made Ctrl+Z / Ctrl+Y behaviour unpredictable when the cursor was inside a richText Visual editor are fixed. The undo boundary between Tiptap’s local history and the global document history is now reliable and non-overlapping.

Bug #1 — Global undo fired simultaneously with Tiptap local undo

The global window.keydown handler did not check contenteditable elements. When the user pressed Ctrl+Z inside a Tiptap Visual editor, both Tiptap’s local undo and store.undo() fired on a single keystroke.

Fix: guard logic extracted into a testable shouldSkipGlobalUndo helper (apps/editor/src/lib/global-shortcuts.ts) that skips the global handler when e.defaultPrevented, the target is a contenteditable, or the target is an INPUT/TEXTAREA.

Bug #2 — setContent() polluted Tiptap’s local undo history

External content sync (after a global undo or tab switch) called editor.commands.setContent(), which ProseMirror records as a regular transaction. Ctrl+Z inside Visual mode would then silently reverse a restoration.

Fix: external sync now constructs a fresh EditorState via EditorState.create() (empty history stacks) and applies it via editor.view.updateState().

Bug #3 — Session ended mid-undo on every Ctrl+Z

The onBlur() call on the Visual editor wrapper was unconditional, ending the editing session on every Ctrl+Z and causing the next onChange from Tiptap’s own undo to create spurious store history entries.

Fix: the onBlur() call is now conditional on !e.defaultPrevented.

Undo model after this release

ContextCtrl+Z behaviour
Focus inside richText Visual mode, Tiptap has historyTiptap local undo only. Global handler skipped. Session stays open.
Focus inside richText Visual mode, Tiptap has nothing to undoSession ends. Next edit starts a new global history entry.
Focus outside richTextGlobal store undo — normal document-level undo.
After global undo changes richText content, re-enter Visual modeTiptap local history is empty (fresh baseline). Ctrl+Z is a no-op inside Visual mode.

4. Editor initial bundle −62 % (cumulative, five passes)

Five successive optimisation passes cut the initial JavaScript and CSS payload dramatically. All changes use React.lazy / dynamic import() with <Suspense fallback={null}>.

Pass 1 — Tiptap / ProseMirror deferred

RichTextVisualEditor (the heaviest single dependency at 323 kB) is now lazy in both the properties panel (RichTextNodeProps) and the canvas inline editor (RichTextNodeView). Four app-level dialogs (TemplatePickerDialog, NewTemplateDialog, TemplateLoaderDialog, PublishGateDialog) are now conditionally mounted.

ChunkBeforeAfter
index-*.js (main)792 kB457 kB
RichTextVisualEditor-*.js (new lazy)323 kB (deferred)
Gzip: main bundle~236 kB135 kB

Pass 2 — ChartNodeView lazy-split

ChartNodeView and @pulp-engine/chart-renderer are no longer on the initial parse path. The split is applied in NodeRenderer.tsx. ChartNodeProps was also lazified for consistency.

ChunkBeforeAfter
index-*.js (main)457 kB441 kB
ChartNodeView-*.js (new lazy)7.32 kB (deferred)
Gzip: main bundle135 kB130 kB

Pass 3 — CSS critical-path split

editor.css (3,689 lines / 99 kB) was a global monolith. Six feature-scoped CSS files were extracted and co-located with their already-lazy JS components:

New CSS fileLazy component
rich-text-visual-editor.cssRichTextVisualEditor.tsx
chart-node.cssChartNodeView.tsx
preview-panel.cssPreviewPanel.tsx
template-dialogs.cssTemplatePickerDialog.tsx, NewTemplateDialog.tsx
version-history-modal.cssVersionHistoryModal.tsx
render-config-editor.cssRenderConfigEditor.tsx

Initial CSS reduced from 99 kB to 83 kB (16.59 kB / 2.39 kB gzip). Total CSS payload unchanged.

Pass 4 — DocumentCanvas lazy-split

DocumentCanvas and all @dnd-kit/* packages are no longer on the blocking parse path. Applied in EditorShell.tsx using a module-scope preload pattern that races the template API call, so the chunk is typically cached before the canvas is shown (Suspense boundary never fires in the normal case).

Vite split the canvas subtree into two deferred chunks:

  • DocumentCanvas-*.js (~23 kB): DndContext, sensors, collision detection
  • NodeWrapper-*.js (~49 kB): useSortable, all 12 canvas node views, @dnd-kit/sortable
ChunkBeforeAfter
index-*.js (main)441 kB340 kB
Gzip: main bundle130 kB100 kB

Pass 5 — PropertiesPanel lazy pass

TableNodeProps, ImageNodeProps, and HandlebarsContentEditor (with its cmdk dependency) are now lazy in PropertiesPanel.tsx. cmdk was the highest-value remaining eager cost; this pass removes it from the main bundle entirely. The HandlebarsContentEditor lazy boundary aligns with the user’s “Content” accordion click — zero UX cost.

ChunkBeforeAfter
index-*.js (main)340 kB301 kB
Gzip: main bundle100 kB89 kB
command-*.js (new lazy)14 kB / 5.3 kB gz
ImageNodeProps-*.js (new lazy)12 kB / 4.0 kB gz
TableNodeProps-*.js (new lazy)6.5 kB / 2.0 kB gz
HandlebarsContentEditor-*.js (new lazy)4.2 kB / 1.6 kB gz

Cumulative bundle impact (v0.27.0 → v0.34.0)

Metricv0.27.0v0.34.0Change
Main bundle (raw)792 kB301 kB−62 %
Main bundle (gzip)~236 kB89 kB−62 %
Initial CSS (raw)99 kB83 kB−16 %
Initial CSS (gzip)17 kB15 kB−14 %

All drag-and-drop behaviour, undo/redo, canvas editing, and rendering are functionally unchanged.


Upgrading

  1. Pull v0.34.0.
  2. SQL Server operators: run docker run --rm -e SQL_SERVER_URL=... ghcr.io/OWNER/pulp-engine:v0.34.0 node dist/scripts/migrate-sqlserver.js before restarting. Already-applied migrations are skipped.
  3. Postgres and file-mode operators: no change to the upgrade procedure.
  4. Restart the API container.
  5. Upgrading from v0.26.x or v0.27.x (any storage mode): check the startup log for legacy_svg_detected. If present, follow the remediation workflow above.

Tests added in this release

  • apps/editor/src/lib/global-shortcuts.test.tsshouldSkipGlobalUndo unit tests
  • apps/editor/src/components/canvas/nodes/ChartNodeView.test.tsx — direct smoke tests + lazy-boundary tests (NodeRenderer, PropertiesPanel paths)
  • apps/editor/src/components/properties/PropertiesPanel.test.tsx — lazy TableNodeProps resolution and empty-state tests
  • apps/api/src/__tests__/asset-list-filter.test.ts?legacySvg= and ?mimeType= filter tests
  • apps/editor/src/components/properties/nodes/TextNodeProps.test.tsx — lazy HandlebarsContentEditor accordion boundary test
  • apps/editor/e2e/rich-text.spec.ts — undo/redo scenarios added