@byline/richtext-lexical 3.20.1 → 3.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,9 +16,11 @@ function EditorContext(props) {
16
16
  const editable = true !== readOnly;
17
17
  const rootExtension = useMemo(()=>{
18
18
  const extensionsList = editorConfig.extensions ?? defaultExtensionsList();
19
- const dependencies = extensionsList.clone().configure(InlineImageExtension, {
19
+ const configured = extensionsList.clone();
20
+ if (configured.has(InlineImageExtension)) configured.configure(InlineImageExtension, {
20
21
  collection: editorConfig.settings.inlineImageUploadCollection
21
- }).toArray();
22
+ });
23
+ const dependencies = configured.toArray();
22
24
  return defineExtension({
23
25
  name: '[root]',
24
26
  namespace: editorConfig.lexical.namespace,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "3.20.1",
6
+ "version": "3.20.3",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -73,10 +73,10 @@
73
73
  "lexical": "0.46.0",
74
74
  "npm-run-all": "^4.1.5",
75
75
  "react-error-boundary": "^6.1.2",
76
- "@byline/admin": "3.20.1",
77
- "@byline/ui": "3.20.1",
78
- "@byline/core": "3.20.1",
79
- "@byline/client": "3.20.1"
76
+ "@byline/core": "3.20.3",
77
+ "@byline/admin": "3.20.3",
78
+ "@byline/ui": "3.20.3",
79
+ "@byline/client": "3.20.3"
80
80
  },
81
81
  "peerDependencies": {
82
82
  "react": "^19.0.0",
@@ -0,0 +1,61 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import type { AnyLexicalExtensionArgument } from '@lexical/extension'
10
+ import { describe, expect, it } from 'vitest'
11
+
12
+ import { ExtensionsList } from './extensions-list'
13
+
14
+ // Structural stand-ins — ExtensionsList compares by `.name`, so plain
15
+ // objects are sufficient and keep the heavy extension classes (React,
16
+ // Lexical nodes) out of this node-mode test.
17
+ const extA = { name: 'test/A' } as unknown as AnyLexicalExtensionArgument
18
+ const extB = { name: 'test/B' } as unknown as AnyLexicalExtensionArgument
19
+
20
+ describe('ExtensionsList', () => {
21
+ it('remove() by name string drops the entry; has() reflects it', () => {
22
+ const list = new ExtensionsList([extA, extB])
23
+ expect(list.has('test/A')).toBe(true)
24
+ list.remove('test/A')
25
+ expect(list.has('test/A')).toBe(false)
26
+ expect(list.has('test/B')).toBe(true)
27
+ })
28
+
29
+ it('configure() upserts — documented behaviour: absent extension is ADDED', () => {
30
+ const list = new ExtensionsList([extB])
31
+ // biome-ignore lint/suspicious/noExplicitAny: structural stand-in
32
+ list.configure(extA as any, {})
33
+ expect(list.has('test/A')).toBe(true)
34
+ })
35
+
36
+ it('REGRESSION: callers forwarding settings must gate configure() on has() — the clone survives a prior remove', () => {
37
+ // Mirrors editor-context.tsx: settings forwarding used to call
38
+ // `.configure(InlineImageExtension, …)` unconditionally, and the
39
+ // upsert resurrected an extension the registration had deliberately
40
+ // removed. The gated pattern must leave the removal intact.
41
+ const list = new ExtensionsList([extA, extB])
42
+ list.remove('test/A')
43
+
44
+ const configured = list.clone()
45
+ if (configured.has('test/A')) {
46
+ // biome-ignore lint/suspicious/noExplicitAny: structural stand-in
47
+ configured.configure(extA as any, {})
48
+ }
49
+
50
+ expect(configured.has('test/A')).toBe(false)
51
+ expect(configured.toArray().length).toBe(1)
52
+ })
53
+
54
+ it('clone() is independent — mutations do not leak back', () => {
55
+ const list = new ExtensionsList([extA, extB])
56
+ const copy = list.clone()
57
+ copy.remove('test/B')
58
+ expect(list.has('test/B')).toBe(true)
59
+ expect(copy.has('test/B')).toBe(false)
60
+ })
61
+ })
@@ -80,14 +80,19 @@ export function EditorContext(props: {
80
80
  const extensionsList = editorConfig.extensions ?? defaultExtensionsList()
81
81
 
82
82
  // Forward the inline-image upload collection from the settings facade
83
- // onto the InlineImageExtension's typed config, but only when the
84
- // user hasn't already configured the extension explicitly.
85
- const dependencies = extensionsList
86
- .clone()
87
- .configure(InlineImageExtension, {
83
+ // onto the InlineImageExtension's typed config but ONLY when the
84
+ // extension is actually present. `configure()` has upsert semantics
85
+ // (absent added), so an ungated call here silently resurrected
86
+ // InlineImageExtension on every editor build for registrations that
87
+ // had deliberately removed it via
88
+ // `lexicalEditor((c) => c.extensions.remove(builtInExtensions.InlineImage))`.
89
+ const configured = extensionsList.clone()
90
+ if (configured.has(InlineImageExtension)) {
91
+ configured.configure(InlineImageExtension, {
88
92
  collection: editorConfig.settings.inlineImageUploadCollection,
89
93
  })
90
- .toArray()
94
+ }
95
+ const dependencies = configured.toArray()
91
96
 
92
97
  return defineExtension({
93
98
  name: '[root]',