PR 3: PT as single source of truth — childless voids + value unification#2226
Draft
christianhg wants to merge 23 commits intomainfrom
Draft
PR 3: PT as single source of truth — childless voids + value unification#2226christianhg wants to merge 23 commits intomainfrom
christianhg wants to merge 23 commits intomainfrom
Conversation
Vendor `slate@0.120.0`, `slate-dom@0.119.0`, and `slate-react@0.120.0` source from github.com/ianstormtaylor/slate (tag: slate@0.120.0). Pure copy of src/ directories — no modifications, no git history. packages/editor/src/slate/ — 118 files (slate core) packages/editor/src/slate-dom/ — 14 files (DOM integration) packages/editor/src/slate-react/ — 40 files (React bindings)
Rewire all bare 'slate', 'slate-dom', 'slate-react' imports to relative paths. Fix module augmentation (declare module '../slate/index' instead of 'slate'). Remove slate/slate-dom/slate-react npm dependencies, add their transitive deps (direction, is-hotkey, lodash, etc.). Fix 583 TypeScript errors from vendored source hitting PTE's strict tsconfig (@sanity/tsconfig strictest): Structural fixes: - Widen DOMEditor/ReactEditor params to Editor (resolves CustomTypes) - withDOM/withReact generic: <T extends BaseEditor> → <T extends Editor> - Remove vendored CustomTypes augmentation (PTE's types/slate.ts is source) - Convert enum to const object (erasableSyntaxOnly) - Cast create-editor.ts delegates through any (incremental object build) Mechanical fixes: - verbatimModuleSyntax: type-only re-exports (biome --fix) - erasableSyntaxOnly: angle bracket → as assertions, import aliases - noUncheckedIndexedAccess: non-null assertions on bounded array access - Unused variables prefixed with _ - override keyword on React lifecycle methods - JSX namespace imports for React 19 - React 19 ref types (RefObject<T | null>, useRef(undefined)) Lint fixes: - Rename String component to SlateString (no shadow global) - Block statements for single-line ifs - Explicit types for uninitialized let declarations - biome-ignore for Slate upstream patterns (hooks in conditionals, assignment in expressions, non-null after optional chain) All 317 unit tests pass. Zero type errors. Zero lint errors.
The vendored Slate source files are now raw TypeScript compiled alongside PTE, which means the React Compiler babel plugin transforms them. Slate's components use manual React.memo with custom equality functions that the React Compiler interferes with, causing incorrect re-render optimization (skipping renders that should happen). Exclude src/slate/, src/slate-dom/, and src/slate-react/ from @vitejs/plugin-react so the React Compiler doesn't process them. esbuild still handles JSX transformation for these files.
lodash is CJS-only and doesn't resolve under Node's ESM module resolution. Since the editor package is "type": "module", downstream consumers running tests in Node (e.g. Vitest) fail with "Cannot find module 'lodash/debounce'". Replaces the two lodash imports from vendored Slate with minimal inline implementations and removes lodash from dependencies.
… in vendored Slate
The set_node operation handler and Transforms.setNodes previously rejected 'text' and 'children' properties unconditionally. This was overly restrictive: - 'text' on Text nodes must go through insert_text/remove_text for correct history, but 'text' on Element nodes is a valid custom property. - 'children' on non-void elements is structural and must not be set directly, but 'children' on void elements is not structural. Changes: - general.ts: Only throw for 'text' on Text nodes, only throw for 'children' on non-void Elements. - set-nodes.ts: Same conditional filtering, plus import Text. - apply-operation-to-portable-text.ts: Skip 'children' when applying set_node to object nodes in the PT value layer, preventing Slate void-child internals from leaking into portable text output.
…l checks
Phase 1 of the identity migration:
- Add isText/isElement to BaseEditor interface
- Default implementations in createEditor() use structural checks
- Schema plugin overrides with schema-driven checks (_type === span.name)
- Node.* methods use structural hasChildren() helper instead of Text.isText/Element.isElement
- modify.ts and get-dirty-paths.ts converted to structural checks
Node.* methods no longer depend on type identity. Tree traversal is purely
structural: 'does this node have children?' All type identity questions
('is this a span?') are answered by editor.isText/editor.isElement.
…ement across Slate layer
…ement across PTE layer Convert 46 call sites across 15 files in the PTE layer (operations, internal-utils, editor/) from static Text.isText/Element.isElement to instance methods editor.isText()/editor.isElement(). Operations files (7): Use operation.editor for instance method access. Match callbacks use arrow wrappers: (n) => editor.isText(n). Internal utils: - operation-to-patches.ts: Thread editor as first param to setNodePatch/insertNodePatch. Update caller and tests. - applyPatch.ts: Use editor methods for node checks. Use structural checks for patch values (partial objects without _type). - apply-operation-to-portable-text.ts: Structural Array.isArray check (pure function, no editor in scope). - values.ts: Structural checks (no editor in scope). Editor files: - sync-machine.ts: Use slateEditor.isText(). - create-editable-api.ts: Use editor.isText() in annotation queries. - range-decorations-machine.ts: Use slateEditor.isElement() in decorate. All Text/Element value imports removed or converted to type-only.
…void selection paths, WebKit keyboard behaviors
…d children
Eliminate the dual-tree architecture. editor.children IS the PT value.
Values (toSlateBlock/fromSlateBlock):
- Block objects: return {_type, _key, ...rest} directly (no children, value)
- Inline objects: return {_type, _key, ...props} directly (no children,
value, __inline)
- Spans: ensure marks is always present (marks ?? [])
- fromSlateBlock: near-identity (tree is already PT-shaped)
Schema plugin:
- isInline: purely schema-based (remove __inline check)
- Dual-position types: check editor.children.includes() for position
Update value plugin:
- Remove applyOperationToPortableText call
- Delete editor.value — editor.children IS the PT value
- Apply operation first, then rebuild index maps
Operation handlers:
- insert.child: insert inline objects directly as PT-shaped nodes
- block.set: read props directly, safe/unsafe split for text/children
- child.set: delta-only setNodes, direct props on inline objects
- child.unset: Transforms.unsetNodes for inline objects
- delete: replace VOID_CHILD_KEY with isElement+isInline check
Rendering:
- render.element: schema-based isInline check (remove __inline)
- render.inline-object: destructure element directly (no value wrapper)
Types:
- VoidElement: remove children/value/__inline, add [key: string]: unknown
- Remove editor.value from PortableTextSlateEditor
All editor.value references replaced with editor.children (cast as
Array<PortableTextBlock> where needed for type compatibility).
42 files changed, -114 net lines.
operation-to-patches.ts:
- Block objects: read properties directly from operation.newProperties
(no value unwrapping). Skip _key, _type, children.
- Inline objects: same direct read pattern.
- insertNodePatch: destructure {children: _c, ...nodeProps} from node.
Type system:
- PortableTextSlateEditor.children typed as Array<PortableTextBlock>
- Remove all 'as Array<PortableTextBlock>' casts (24 files)
- Remove unused PortableTextBlock imports
- Slate internals: children guards (?? []) for optional children
29 files changed, -28 net lines.
applyPatch.ts: - Inline object set/unset: apply patches directly to node (no value unwrapping). Use applyAll on node with propPath, then setNodes/unsetNodes. - Block object set/unset: same direct pattern. Detect block objects via !isTextBlock instead of 'value' in node. - Remove __inline from reserved props lists. - Remove all 'value' wrapper references. operation-to-patches.test.ts: - Update all test data to new format: properties directly on nodes, no value wrapper, no __inline, no void children. - 317/317 unit tests pass. 2 files changed, -41 net lines.
Childless void elements can leave stale selection paths (e.g., [0,0] pointing inside a void with no children). Add Node.has guards before Node.get in three locations: - editable.tsx mark placeholder handling (render phase) - editable.tsx mark insertion useEffect (setTimeout) - android-input-manager.ts deleteContent handling All three follow the same pattern: check Node.has first, return undefined if the path doesn't exist, then guard the Text.isText check with a truthiness test.
The blockIndexMap was empty on initial render because createSlateEditor called buildIndexMaps with instance.children (which is []) before the <Slate> component sets editor.children = initialValue. Fix: set instance.children = initialValue in createSlateEditor before calling buildIndexMaps. This ensures the initial render has correct blockIndexMap entries, preventing text blocks from being misclassified as block objects. This was the root cause of 126 browser test failures — text blocks rendered as block objects caused 'Unable to find Block Object' warnings and cascading 'Maximum call stack size exceeded' errors.
operation.block.unset.ts: - Rewrite to use Transforms.unsetNodes directly (same pattern as text blocks). Remove old applyAll + value wrapper approach. applyPatch.ts: - Fix nested unset on block objects (e.g., _map.foo). The old code only detected removed top-level keys, missing nested property changes. Now also detects changed properties via reference comparison and applies them with Transforms.setNodes. event.block.set.test.tsx: - Remove expected href patch — only changed properties emit patches now. event.child.set.test.tsx: - Remove expected abilities/alive patches from set_key and set_property tests — only changed properties emit patches now. 7 browser test failures remain (4 collaborative editing normalization patch timing, 1 normalization marks, 2 inline object).
collaborative-editing.test.tsx (4 tests): - Remove expected 'set marks: []' deferred normalization patches. toSlateBlock now adds marks: [] during sync, so normalization has nothing to fix and no patch is emitted. The deferred normalization system still works for other properties (style, markDefs). self-solving.test.tsx (1 test): - Same: remove spanPatch (marks: []) from expected patches/mutations. Only blockPatch (markDefs: []) is deferred now. event.insert.inline-object.test.tsx (1 test): - Fix key ordering: expect ['_type', '_key'] instead of ['_key', '_type']. Object.keys order changed with direct property model. All browser tests pass (0 real failures, only sandbox iframe flakes).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: dc1bb71 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
The childless voids commit (e1d2c4c) reverted the smart set_node guards from PR #2224 back to unconditional text/children rejection. Restores context-aware guards in both locations: - general.ts (operation handler): only throw for 'text' on Text nodes, only throw for 'children' on non-void Elements - set-nodes.ts (Transforms.setNodes): same conditional filtering Uses instance methods (editor.isText, editor.isElement, editor.isVoid) consistent with the Phase 2 migration. Also fixes prettier formatting on 8 files flagged by CI.
3a640a2 to
2907698
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Eliminates the dual-tree architecture. Portable Text is now the single source of truth — no value wrappers, no
__inlinemarkers, no void children.104 files changed, 977 insertions, 963 deletions (nearly net-zero).
Commit stack
9bbeb9feeditor.createTextNode()for schema-aware text node creationd8532121set_node— relax text/children constraints for void elements7a8fc317editor.isText/isElementfoundation with structural checks9b327ad7686986f2e1d2c4c6948884ca__inlinemarker, void children1247e02b04c7db080bc54054db1fae23211a2bf7a1f49f5b5623a4cc220285029f5d1a0aad8af02dKey changes
Data model
valuewrapper)__inline: truemarker[{_key, _type: "span", text: "", marks: []}])editor.childrentyped asArray<PortableTextBlock>Operations & patches
operation-to-patches.ts: reads properties directly fromoperation.newPropertiesapplyPatch.ts: inline/block object handlers apply patches directly to nodeoperation.block.unset.ts: usesTransforms.unsetNodesdirectly_map.foo) now works correctlyNormalization
marks: []added bytoSlateBlockduring sync (no deferred normalization patch)markDefs,stylestill normalized via deferred patchesSelection
[N, 0]inside childless voids)Node.hasguards prevent crashes from stale selection pathsTesting