Skip to content

Enhance serialization with optional chaining and checks#2880

Open
Bredo wants to merge 1 commit into
TypeCellOS:mainfrom
Bredo:patch-1
Open

Enhance serialization with optional chaining and checks#2880
Bredo wants to merge 1 commit into
TypeCellOS:mainfrom
Bredo:patch-1

Conversation

@Bredo

@Bredo Bredo commented Jul 1, 2026

Copy link
Copy Markdown

Summary

Added optional chaining to safely check for class presence and attribute setting. Improved handling of nesting level attribute assignment.

Rationale

When serializing blocks to external HTML (via blocksToHTMLLossy), the serializer evaluates (ret.dom as HTMLElement).classList.contains("bn-block-content"). However, certain block toExternalHTML or render implementations might return node types that aren't strictly HTMLElements (such as DocumentFragment or Text nodes). These nodes do not have a .classList property, which causes the serializer to crash with TypeError: Cannot read properties of undefined (reading 'contains').

I ran in to this specifically using @twentyhq/twenty with numbered lists <ol>

Additionally, if these node types are evaluated in the else branch and nestingLevel > 0, the exporter tries to call .setAttribute("data-nesting-level", ...) on them, causing a secondary crash (TypeError: ...setAttribute is not a function).

This PR guards both of these DOM operations to ensure the HTML exporter fails gracefully and doesn't crash when encountering non-element DOM nodes.

Changes

  • Added optional chaining to safely check if .classList exists before calling .contains("bn-block-content").
  • Guarded data-nesting-level assignments by checking if the .setAttribute method exists via typeof === 'function' before invoking it.

Impact

This PR increases the stability of the HTML exporters. It prevents hard crashes during blocksToHTMLLossy and blocksToFullHTML when the editor encounters complex fragment or text structures. Existing functionalities will remain completely unaffected as this merely guards operations against undefined errors.

Testing

Manual testing was performed by executing blocksToHTMLLossy on a document containing block structures that resolve to nodes lacking .classList properties. Previously, this would crash the frontend application; with this fix, the serialization completes successfully.

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Summary by CodeRabbit

  • Bug Fixes
    • Improved HTML export resilience by handling missing DOM features more safely.
    • Prevented errors when certain element properties are unavailable during block serialization.
    • Made nesting-level attribute updates more defensive, reducing the chance of export failures.

Added optional chaining to safely check for class presence and attribute setting. Improved handling of nesting level attribute assignment.
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

@Bredo is attempting to deploy a commit to the TypeCell Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds defensive guards to the serializeBlock function in the HTML external export utility. It uses optional chaining when checking classList and verifies setAttribute is callable before setting the data-nesting-level attribute, in both the primary and fallback code paths.

Changes

Defensive DOM Guards in Serialization

Layer / File(s) Summary
Safer classList and setAttribute checks
packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
Adds optional chaining for classList.contains and checks that setAttribute is a callable function before setting data-nesting-level in both the bn-block-content and fallback branches.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~5 minutes

Poem

Little rabbit hops through DOM so deep,
Guarding classList while others sleep,
setAttribute checked before it's called,
No more errors, nothing stalled,
Hop hop hooray, the code's now safe! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main serializer hardening change.
Description check ✅ Passed The description matches the template's core sections and covers summary, rationale, changes, impact, testing, and checklist.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts (1)

246-256: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard only covers the last mutation of firstChild; earlier lines still crash on non-elements.

The new check at Line 251 protects only the data-nesting-level assignment, but lines 247 and 250 access/mutate the same ret.dom.firstChild without any guard:

  • Line 247 calls .setAttribute unconditionally in the loop.
  • Line 250 passes firstChild to addAttributesAndRemoveClasses, which does Array.from(element.classList) with no optional chaining — this throws if firstChild lacks classList (e.g. a Text node).

Since ret.dom passing the classList.contains("bn-block-content") check (Line 231) says nothing about what type ret.dom.firstChild is, the same "not an HTMLElement" crash this PR is meant to prevent remains reachable a few lines earlier in the same branch.

🛡️ Proposed fix: guard all firstChild mutations consistently
+    const firstChild = ret.dom.firstChild as HTMLElement | null;
     for (const attr of blockContentDataAttributes) {
-      (ret.dom.firstChild! as HTMLElement).setAttribute(attr.name, attr.value);
+      firstChild?.setAttribute?.(attr.name, attr.value);
     }

-    addAttributesAndRemoveClasses(ret.dom.firstChild! as HTMLElement);
-    if (nestingLevel > 0 && typeof (ret.dom.firstChild as HTMLElement)?.setAttribute === 'function') {
-      (ret.dom.firstChild! as HTMLElement).setAttribute(
+    if (firstChild?.classList) {
+      addAttributesAndRemoveClasses(firstChild);
+    }
+    if (nestingLevel > 0 && typeof firstChild?.setAttribute === "function") {
+      firstChild.setAttribute(
         "data-nesting-level",
         nestingLevel.toString(),
       );
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts`
around lines 246 - 256, The firstChild safety check in
serializeBlocksExternalHTML only protects the final data-nesting-level mutation,
but the earlier uses of ret.dom.firstChild still assume an HTMLElement. Update
the branch in serializeBlocksExternalHTML to consistently guard or narrow
firstChild before any mutation, including the blockContentDataAttributes loop
and the call to addAttributesAndRemoveClasses, so those paths only run when
firstChild is an element. Use the existing ret.dom.firstChild references and
addAttributesAndRemoveClasses helper as the key locations to adjust.
🧹 Nitpick comments (1)
packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts (1)

230-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test coverage added for the new non-HTMLElement guard paths.

This defensive fix guards against classList/setAttribute being absent (e.g. DocumentFragment/Text nodes), but no test exercises a block whose render/toExternalHTML returns such a node. Given this is fixing a real crash scenario, a regression test would help confirm the fix and prevent future regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts`
around lines 230 - 266, Add regression coverage for the new non-HTMLElement
guard paths in serializeBlocksExternalHTML. Create a test that exercises a block
whose render/toExternalHTML returns a DocumentFragment or Text node so the
classList/setAttribute checks are skipped safely. Verify the export no longer
crashes and still appends the node correctly, using the
serializeBlocksExternalHTML flow and the block-content handling branch as the
target location.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts`:
- Around line 246-256: The firstChild safety check in
serializeBlocksExternalHTML only protects the final data-nesting-level mutation,
but the earlier uses of ret.dom.firstChild still assume an HTMLElement. Update
the branch in serializeBlocksExternalHTML to consistently guard or narrow
firstChild before any mutation, including the blockContentDataAttributes loop
and the call to addAttributesAndRemoveClasses, so those paths only run when
firstChild is an element. Use the existing ret.dom.firstChild references and
addAttributesAndRemoveClasses helper as the key locations to adjust.

---

Nitpick comments:
In `@packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts`:
- Around line 230-266: Add regression coverage for the new non-HTMLElement guard
paths in serializeBlocksExternalHTML. Create a test that exercises a block whose
render/toExternalHTML returns a DocumentFragment or Text node so the
classList/setAttribute checks are skipped safely. Verify the export no longer
crashes and still appends the node correctly, using the
serializeBlocksExternalHTML flow and the block-content handling branch as the
target location.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 86855951-5657-4e2e-9b9f-873052eb0e67

📥 Commits

Reviewing files that changed from the base of the PR and between 3094559 and 0814883.

📒 Files selected for processing (1)
  • packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant