Export Lexical and document allowedElements in Extensions#1047
Conversation
There was a problem hiding this comment.
Pull request overview
This PR exposes Lexical from Lexxy’s public JS entrypoint so extension authors can access the same Lexical package instance/version that Lexxy uses, and updates the extension docs to use that export while documenting allowedElements.
Changes:
- Re-export
lexicalasLexxy.Lexicalfrom the package entrypoint. - Update the extensions documentation to use
Lexxy.Lexical.defineExtensioninstead of importing fromlexical. - Document
allowedElementsin extensions (example includes allowing<iframe>).
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/index.js | Re-exports the lexical namespace as part of Lexxy’s public API. |
| docs/extensions.md | Updates extension authoring docs to use Lexxy.Lexical and documents allowedElements (incl. iframe example). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| export const configure = Lexxy.configure | ||
| export { default as Extension } from "./extensions/lexxy_extension" | ||
| export * as Lexical from "lexical" |
| return this.#config.enableCoolFeature | ||
| } | ||
|
|
||
| // optional: allow additional elements |
| // optional: allow additional elements | ||
| get allowedElements() { | ||
| return [ { tag: "iframe", attributes: [ "src", "loading", "allow" ] } ] |
|
Hey @excid3 👋 I'm for this in principle. My one concern about package size was unfounded. I've been asking myself the same question since our extensions make use of commands, tags, and I'm not a huge fan of For other imports, I wonder whether exporting lexical at a different path (like import { mergeRegister } from "@37signals/lexxy/lexical"rather than: import Lexxy from "@37signals/lexxy"
const { mergeRegister } = Lexxy.Lexical
// or at the callsite:
Lexxy.Lexical.mergeRegister(//...My only concern would have been on bundle size, but it looks like the hit is ~6k and there won't be any impact on the npm package. Without export lexxy main ❯ yarn build && ls -l app/assets/javascript/
yarn run v1.22.22
$ rollup -c
./src/index.js → ./app/assets/javascript/lexxy.js, ./app/assets/javascript/lexxy.min.js...
created ./app/assets/javascript/lexxy.js, ./app/assets/javascript/lexxy.min.js in 3.2s
Done in 3.36s.
Permissions Size User Date Modified Name
.rw-r--r-- 855k slp 11 May 12:09 lexxy.js
.rw-r--r-- 195k slp 11 May 12:09 lexxy.js.br
.rw-r--r-- 238k slp 11 May 12:09 lexxy.js.gz
.rw-r--r-- 2.0M slp 11 May 12:09 lexxy.js.map
.rw-r--r-- 572k slp 11 May 12:09 lexxy.min.js
.rw-r--r-- 146k slp 11 May 12:09 lexxy.min.js.br
.rw-r--r-- 175k slp 11 May 12:09 lexxy.min.js.gzWith export lexxy main ❯ yarn build && ls -l app/assets/javascript/
yarn run v1.22.22
$ rollup -c
./src/index.js → ./app/assets/javascript/lexxy.js, ./app/assets/javascript/lexxy.min.js...
created ./app/assets/javascript/lexxy.js, ./app/assets/javascript/lexxy.min.js in 3s
Done in 3.21s.
Permissions Size User Date Modified Name
.rw-r--r-- 861k slp 11 May 12:10 lexxy.js
.rw-r--r-- 197k slp 11 May 12:10 lexxy.js.br
.rw-r--r-- 240k slp 11 May 12:10 lexxy.js.gz
.rw-r--r-- 2.0M slp 11 May 12:10 lexxy.js.map
.rw-r--r-- 578k slp 11 May 12:10 lexxy.min.js
.rw-r--r-- 148k slp 11 May 12:10 lexxy.min.js.br
.rw-r--r-- 177k slp 11 May 12:10 lexxy.min.js.gz |
|
@samuelpecher that's not as much as I expected! I'm not super familiar with Lexical, but my thinking was by exposing all of Lexical, you give people the freedom to access any functionality easily. That may not be necessary or common enough, so a wrapper could be a simpler option for users. |
jorgemanrubia
left a comment
There was a problem hiding this comment.
Hey @excid3 could you detail the limitations you found with the current extension support?
I would like to keep Lexical encapsulated as much as possible and favor an extension system that covers most needs, unless we can make a strong case to give full access to Lexical.
Thanks!
|
@jorgemanrubia there didn't seem to be a way to create a Lexxy extension using importmaps without also pinning Lexical separately. Could just be my lack of frontend knowledge. The docs show: pin "lexxy", to: "lexxy.js"import { defineExtension } from "lexical"
import * as Lexxy from "@37signals/lexxy"I was trying to build an extension that lets me paste in like a YouTube or Bunny.net video and allow the iframe tag. import * as Lexxy from "lexxy"
import * as Lexical from "lexical"
class EmbedsExtension extends Lexxy.Extension {
get lexicalExtension() {
return Lexical.defineExtension({ name: "lexxy/embeds_extension" })
}
get allowedElements() {
return [ { tag: "iframe", attributes: [ "src", "loading", "width", "height", "style", "allow" ] } ]
}
}
Lexxy.configure({
global: {
extensions: [EmbedsExtension],
}
})The trouble is I'd have to pin a second copy of Lexical in order to do this currently? |
Yep. Lexical's functions are the "knives" we need to expose for extension writers to cut into Lexical. @jorgemanrubia I think the import for our own VoiceNotes implementation illustrates the issue: import { $getNodeByKey, $getRoot } from "lexical"
import { mergeRegister } from "@lexical/utils"
import { defineExtension } from "@lexical/extension"
// AudioRecorderNode imports DecoratorNode from "lexical"
import { $createAudioRecorderNode, AudioRecorderNode } from "./audio_recorder_node"Or our slash-command extension: import { RootNode, $isParagraphNode } from "lexical"
import { $createCodeNode } from "@lexical/code"If Lexical was OO, it could simply be a case of calling functions on objects we could expose within the extension. With Lexical's implementation, those messages are the functions and types exposed from Since we build Lexxy from |
There was a problem hiding this comment.
Good points @samuelpecher @excid3. I am fine with going forward with this 👍.
I'm not a huge fan of return Lexxy.Lexical.defineExtension(..., perhaps Lexxy.Extension should provide a defineLexicalExtension wrapper for this common use case.
Yes, I think so. In this case I think it is worth adding a wrapper.
We could also add a little section documenting how to access Lexical elements as part of the extensions article, as part of this PR.
|
Hello guys! I would like to add my 2 cents about what happened today. I ran into both sides of this problem while writing custom extensions for Lexxy. Lexical runtime conflict: My extensions imported defineExtension, DecoratorNode, etc. directly from "lexical". Because yarn hoisted my pinned lexical@0.45.0 separately from Lexxy's own copy, I ended up with two Lexical runtimes. Commands dispatched from one instance were invisible to nodes registered via the other — icons inserted fine but importJSON/exportJSON round-trips silently broke. My workaround: a thin ./lexical.js re-export file that all my extensions import instead of "lexical" directly, ensuring esbuild deduplicates to the same module. Same class of problem with DOMPurify: Lexxy bundles its own DOMPurify and uses it internally. My text-align sanitization hook (DOMPurify.addHook(...)) landed on a different DOMPurify instance than the one Lexxy's styleFilterHook runs on, so the hook was never called during sanitization. Fix: pin "dompurify": "3.4.5" as a direct dep so yarn hoists exactly Lexxy's version for everyone. These were my imports: // bootstrap_icon_extension.js
import { defineExtension, DecoratorNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW } from "lexical"
import * as Lexxy from "@37signals/lexxy"
// subscript_superscript_extension.js
import { defineExtension, FORMAT_TEXT_COMMAND, COMMAND_PRIORITY_LOW } from "lexical"
import * as Lexxy from "@37signals/lexxy"
// text_alignment_extension.js
import { defineExtension, FORMAT_ELEMENT_COMMAND, COMMAND_PRIORITY_LOW } from "lexical"
import * as Lexxy from "@37signals/lexxy"
import DOMPurify from "dompurify"
// config.js
import * as Lexxy from "@37signals/lexxy"
import { TextAlignmentExtension } from "./text_alignment_extension"
import { SubscriptSuperscriptExtension } from "./subscript_superscript_extension"
import { BootstrapIconExtension } from "./bootstrap_icon_extension" |
This addresses a couple things:
defineExtensionfrom lexical. However, that means we need to pin the lexical package which could be a different version. Instead, we can export Lexical through Lexxy so users have access to the exact same version.We could export just
defineExtensionto keep it minimal, but I wasn't sure if extensions may need access to other parts of Lexical. I can quickly refactor this if that's the best approach.Does this make sense as an approach @zachasme @samuelpecher @jorgemanrubia?