-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Generic ImportJSON/ExportJSON #3931
base: main
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
const serializedNode2 = JSON.parse(JSON.stringify(serializedNode)); | ||
delete serializedNode2.children; | ||
delete serializedNode2.version; | ||
delete serializedNode2.mode; | ||
delete serializedNode2.direction; | ||
delete serializedNode2.format; | ||
let node = new registeredNode.klass(); | ||
// @ts-expect-error | ||
node.setFormat(serializedNode.format); | ||
if ($isTextNode(node)) { | ||
// @ts-expect-error | ||
node.setMode(serializedNode.mode); | ||
} | ||
|
||
const node = nodeClass.importJSON(serializedNode); | ||
if ( | ||
$isElementNode(node) && | ||
type !== 'tablecell' && | ||
type !== 'tablerow' && | ||
type !== 'table' | ||
) { | ||
// @ts-expect-error | ||
node.setDirection(serializedNode.direction); | ||
} | ||
const prefix = '__'; | ||
const withUnderscore = Object.fromEntries( | ||
Object.entries(serializedNode2).map(([k, v]) => [`${prefix}${k}`, v]), | ||
); | ||
node = Object.assign(node, withUnderscore); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the most important part which replaces importJSON.
As a side note, one of the ideas I have for simplifying setters and getters is to use a generic proxy that accesses getLatest() and getWritable(), so probably the underscore __
thing may not be necessary anymore. here or anywhere.
I’m not sure this makes sense. The purpose of this API is to allow custom nodes to control deserialization behavior, which allows for runtime backwards compatibility for Lexical states in storage. With this change, you’ve taken that flexibility and control away from developers and instead created a place in the core that will have to be continuously expanded and special-cased as new core node types are added. |
packages/lexical/src/LexicalNode.ts
Outdated
exportJSON() { | ||
let serializedNode = JSON.parse(JSON.stringify(this.getLatest())); | ||
delete serializedNode.__first; | ||
delete serializedNode.__last; | ||
delete serializedNode.__size; | ||
delete serializedNode.__parent; | ||
delete serializedNode.__next; | ||
delete serializedNode.__prev; | ||
delete serializedNode.__cachedText; | ||
delete serializedNode.__key; | ||
delete serializedNode.__dir; | ||
serializedNode = Object.fromEntries( | ||
Object.entries(serializedNode).map(([k, v]) => [k.slice(2), v]), | ||
); | ||
if ($isElementNode(this)) { | ||
serializedNode = {children: [], ...serializedNode}; | ||
serializedNode.direction = this.getDirection(); | ||
serializedNode.format = this.getFormatType(); | ||
serializedNode.indent = this.getIndent(); | ||
} | ||
if ($isTextNode(this)) { | ||
serializedNode.mode = this.getMode(); | ||
} | ||
serializedNode.type = this.getType(); | ||
serializedNode.version = 1; | ||
return serializedNode; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the other important part, which replaces exportJSON()
I've updated the PR to add I can do the same with What do you think @acywatson? |
My recommendation would be for you to review this PR: #2157 The reason serialization works like this is not because we never thought about just serializing all the properties on the node - that was basically how it was done before. The idea was to create a type-safe abstraction between the serialized types and the internal node structures to insulate against changes to those internal structure breaking old stored Lexical states. I appreciate the effort, but I don't think we have a lot of appetite to revisit this right now. |
I have found in the last commit a way to autogenerate the types of the serialized nodes automatically. As you can see in the video, it works with inheritance. https://www.loom.com/share/4601d7d7411c4eafbfd47d2281bade52 Maybe that solves the problem?
I believe that what I propose is viable and has enormous advantages. But if you don't find value in these changes, please feel free to close the PR. I'd rather use a fork than be a nuisance :) |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
[x: string]: any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This conflicts with the solution I found for serialized node types.
I don't understand why it was done in the first instance. It doesn't seem like a good practice since it gives a false illusion of type-safety, when in fact it breaks it at the beginning of the class hierarchy.
@egonbolton You're not being a nuisance, but I'm also perfectly happy for you to use a fork - it is open source, after all :) Let me spend some time with your work and see if I can get comfortable with the functionality in the scenarios I care about. Specifically, I need to understand what happens when someone decides to change a node to add a new property in. How does deserialization work with old states that don't have that property? What about when a new state (containing the added property) is deserialized by an old editor? How does the type system behave when the user makes changes to the public serialization interface of a node so that they're warned about the two scenarios above and can react accordingly? I'm not saying that your proposal doesn't have advantages (they might even be enormous ones!), but it is potentially very consequential for all of our internal systems, which means I need to spend my time verifying it, which impacts my other priorities. That's the only issue here - I'm happy to consider the idea on it's merits and we're better just for having seen the idea, whether we merge it now or not. I have some free time this weekend, so I will try to check out the branch and verify some of these scenarios and see where I get. |
delete serializedNode2.format; | ||
node.setFormat(serializedNode.format); | ||
if (type !== 'tablecell' && type !== 'tablerow' && type !== 'table') { | ||
node.setDirection(serializedNode.direction); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be better to override these nodes' setDirection to be noop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean to export the direction in those nodes right? I agree.
If that's okay with you, I'll have to modify the unit tests to include that property just like I had to do with colSpan
'LexicalNode: Node %s does not implement .importJSON().', | ||
this.name, | ||
exportJSON() { | ||
let serializedNode = JSON.parse(JSON.stringify(this.getLatest())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried to run it on a doc with 5k nodes, got ~35ms vs ~6ms on main branch, it's not extremely slow, but I can imagine someone to have editor.toJSON within onChange handler. Tried to replace it with looping through prefixed keys and it works better (~9ms) but it leaves more responsibility for custom nodes owners, who need to ensure their props are serializable or provide own import/export overrides. We can try dev mode invariants that will warn about non-serializable values, but won't affect production users
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent improvement, thank you very much!
Tried to replace it with looping through prefixed keys and it works better (~9ms) but it leaves more responsibility for custom nodes owners, who need to ensure their props are serializable or provide own import/export overrides.
If I'm not misunderstanding, that's true for both my implementation and the variation you've made. The purpose of this PR in principle is that it is only necessary to define serialization on non-serializable nodes.
Actually, I believe that automatic serialization could always be achieved by using something like devalue. However, I'm not sure what the implications of this are on performance or memory. Taking caption
for example, I don't know if it would be good to serialize all editor properties like pendingEditorState
and such. Maybe we can open a separate thread to discuss this separately?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I believe that automatic serialization could always be achieved by using something like devalue. However, I'm not sure what the implications of this are on performance or memory.
-
Automatic serialization is not necessarily what we want - more like flexible serialization with sane defaults.
-
Lexical core has no dependencies on any other library - we don't want to change that. Control over performance is one of the main reasons that was done.
Taking caption for example, I don't know if it would be good to serialize all editor properties like pendingEditorState and such.
I don't think it would be good. In the best case it's unnecessary bloat in editor states, in the worst case it encourages depending on implementation details that might change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's what I thought @acywatson
@fantactuka, did you get it to work with your snippet?
even setting direction
, indent
and version
there are some tests that break when I implement it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fantactuka, did you get it to work with your snippet?
No, I didn't get to the point to run testes over it, can take a look closer tomorrow evening
Lexical core has no dependencies on any other library - we don't want to change that.
Also adds 2.9kb to core
We could limit auto-serialization to primitives (numbers, strings, bools, nulls) and nested editors (by just calling .toJSON on it). Everything else should require custom import/export. It should be possible to lint or dev-mode-warn (class with no import/exportJSON and non-primitive props starting with "__"). I might be wrong, but I can only recall MarkNode and its sub-classes that use arrays/maps as serializable props
This is the second in a series of changes I proposed in #3763 to simplify Lexical, remove code duplication, and improve DX.
In short, thanks to this PR, no node needs to define an
importJSON
orexportJSON
method.The most important thing (and the first thing I recommend checking) are the changes in
LexicalUpdate.ts
(forimportJSON
) and inlexicalNode.ts
(forexportJSON
).Pending:
mode
,direction
andformat
properties mismatch between the way they are exported and imported making the algorithm more inefficient. I have a PR in mind that would improve this. Either way, the PR as it is is now functional.