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
- Check the startup log for
legacy_svg_detected— if absent, no action needed GET /assets?legacySvg=trueto enumerate affected assets- For each: identify template references, upload a PNG/WebP replacement, update the template definition
DELETE /assets/:idonce no templates reference the legacy SVG- 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
| Context | Ctrl+Z behaviour |
|---|---|
| Focus inside richText Visual mode, Tiptap has history | Tiptap local undo only. Global handler skipped. Session stays open. |
| Focus inside richText Visual mode, Tiptap has nothing to undo | Session ends. Next edit starts a new global history entry. |
| Focus outside richText | Global store undo — normal document-level undo. |
| After global undo changes richText content, re-enter Visual mode | Tiptap 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.
| Chunk | Before | After |
|---|---|---|
index-*.js (main) | 792 kB | 457 kB |
RichTextVisualEditor-*.js (new lazy) | — | 323 kB (deferred) |
| Gzip: main bundle | ~236 kB | 135 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.
| Chunk | Before | After |
|---|---|---|
index-*.js (main) | 457 kB | 441 kB |
ChartNodeView-*.js (new lazy) | — | 7.32 kB (deferred) |
| Gzip: main bundle | 135 kB | 130 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 file | Lazy component |
|---|---|
rich-text-visual-editor.css | RichTextVisualEditor.tsx |
chart-node.css | ChartNodeView.tsx |
preview-panel.css | PreviewPanel.tsx |
template-dialogs.css | TemplatePickerDialog.tsx, NewTemplateDialog.tsx |
version-history-modal.css | VersionHistoryModal.tsx |
render-config-editor.css | RenderConfigEditor.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 detectionNodeWrapper-*.js(~49 kB):useSortable, all 12 canvas node views,@dnd-kit/sortable
| Chunk | Before | After |
|---|---|---|
index-*.js (main) | 441 kB | 340 kB |
| Gzip: main bundle | 130 kB | 100 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.
| Chunk | Before | After |
|---|---|---|
index-*.js (main) | 340 kB | 301 kB |
| Gzip: main bundle | 100 kB | 89 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)
| Metric | v0.27.0 | v0.34.0 | Change |
|---|---|---|---|
| Main bundle (raw) | 792 kB | 301 kB | −62 % |
| Main bundle (gzip) | ~236 kB | 89 kB | −62 % |
| Initial CSS (raw) | 99 kB | 83 kB | −16 % |
| Initial CSS (gzip) | 17 kB | 15 kB | −14 % |
All drag-and-drop behaviour, undo/redo, canvas editing, and rendering are functionally unchanged.
Upgrading
- Pull v0.34.0.
- SQL Server operators: run
docker run --rm -e SQL_SERVER_URL=... ghcr.io/OWNER/pulp-engine:v0.34.0 node dist/scripts/migrate-sqlserver.jsbefore restarting. Already-applied migrations are skipped. - Postgres and file-mode operators: no change to the upgrade procedure.
- Restart the API container.
- 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.ts—shouldSkipGlobalUndounit testsapps/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— lazyTableNodePropsresolution and empty-state testsapps/api/src/__tests__/asset-list-filter.test.ts—?legacySvg=and?mimeType=filter testsapps/editor/src/components/properties/nodes/TextNodeProps.test.tsx— lazyHandlebarsContentEditoraccordion boundary testapps/editor/e2e/rich-text.spec.ts— undo/redo scenarios added