Skip to content
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

[Bug]: Extension hooks sometimes called multiple times. #5548

Open
1 task done
634750802 opened this issue Aug 24, 2024 · 3 comments
Open
1 task done

[Bug]: Extension hooks sometimes called multiple times. #5548

634750802 opened this issue Aug 24, 2024 · 3 comments
Labels
Category: Open Source The issue or pull reuqest is related to the open source packages of Tiptap. Type: Bug The issue or pullrequest is related to a bug

Comments

@634750802
Copy link

634750802 commented Aug 24, 2024

Affected Packages

core

Version(s)

2.6.4

Bug Description

This issue was originally discussed in #5136 and fixed by #5147

I have found a new case not covered by this PR:

const Parent = Extension.create({
  name: 'Parent',
  onCreate() {
    this.parent?.();
    // some code here
  }
})

const Child = Parent.config() // does not override parent's onCreate hook

Let's see the source code of Extension.extend():

// ...
    const extension = new Extension<ExtendedOptions, ExtendedStorage>({ ...this.config, ...extendedConfig })
    extension.parent = this
    this.child = extension
    extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name
// ...

Child copied Parent's config onCreate, the onCreate hook will be called twice:

  • Child.config.onCreate
  • Child.parent(aka Parent).onCreate

Browser Used

Chrome

Code Example URL

No response

Expected Behavior

Extension/Node/Mark hooks not be called multiple times.

Additional Context (Optional)

No response

Dependency Updates

  • Yes, I've updated all my dependencies.
@634750802 634750802 added Category: Open Source The issue or pull reuqest is related to the open source packages of Tiptap. Type: Bug The issue or pullrequest is related to a bug labels Aug 24, 2024
@634750802
Copy link
Author

@nperez0111

@nperez0111
Copy link
Contributor

Appreciate the report, unfortunately the way that this was implemented is rather poor & prone to this sort of a bug, I plan to rewrite this to use proper classes in v3. So, I don't want to spend time on addressing this long standing issue right now, if you want to fix it, I'd be happy to take a PR for it. You can add to the tests here:

/// <reference types="cypress" />
import {
Extension, getExtensionField, Mark, Node,
} from '@tiptap/core'
describe('extend extensions', () => {
[Extension, Node, Mark].forEach(Extendable => {
describe(Extendable.create().type, () => {
it('should define a config', () => {
const extension = Extendable.create({
addAttributes() {
return {
foo: {},
}
},
})
const attributes = getExtensionField(extension, 'addAttributes')()
expect(attributes).to.deep.eq({
foo: {},
})
})
it('should overwrite a config', () => {
const extension = Extendable
.create({
addAttributes() {
return {
foo: {},
}
},
})
.extend({
addAttributes() {
return {
bar: {},
}
},
})
const attributes = getExtensionField(extension, 'addAttributes')()
expect(attributes).to.deep.eq({
bar: {},
})
})
it('should have a parent', () => {
const extension = Extendable
.create({
addAttributes() {
return {
foo: {},
}
},
})
const newExtension = extension
.extend({
addAttributes() {
return {
bar: {},
}
},
})
const parent = newExtension.parent
expect(parent).to.eq(extension)
})
it('should merge configs', () => {
const extension = Extendable
.create({
addAttributes() {
return {
foo: {},
}
},
})
.extend({
addAttributes() {
return {
...this.parent?.(),
bar: {},
}
},
})
const attributes = getExtensionField(extension, 'addAttributes')()
expect(attributes).to.deep.eq({
foo: {},
bar: {},
})
})
it('should merge configs multiple times', () => {
const extension = Extendable
.create({
addAttributes() {
return {
foo: {},
}
},
})
.extend({
addAttributes() {
return {
...this.parent?.(),
bar: {},
}
},
})
.extend({
addAttributes() {
return {
...this.parent?.(),
baz: {},
}
},
})
const attributes = getExtensionField(extension, 'addAttributes')()
expect(attributes).to.deep.eq({
foo: {},
bar: {},
baz: {},
})
})
it('should set parents multiple times', () => {
const grandparentExtension = Extendable
.create({
addAttributes() {
return {
foo: {},
}
},
})
const parentExtension = grandparentExtension
.extend({
addAttributes() {
return {
...this.parent?.(),
bar: {},
}
},
})
const childExtension = parentExtension
.extend({
addAttributes() {
return {
...this.parent?.(),
baz: {},
}
},
})
expect(parentExtension.parent).to.eq(grandparentExtension)
expect(childExtension.parent).to.eq(parentExtension)
})
it('should merge configs without direct parent configuration', () => {
const extension = Extendable
.create({
addAttributes() {
return {
foo: {},
}
},
})
.extend()
.extend({
addAttributes() {
return {
...this.parent?.(),
bar: {},
}
},
})
const attributes = getExtensionField(extension, 'addAttributes')()
expect(attributes).to.deep.eq({
foo: {},
bar: {},
})
})
it('should call ancestors only once', () => {
const callCounts = {
grandparent: 0,
parent: 0,
child: 0,
}
const extension = Extendable
.create({
addAttributes() {
callCounts.grandparent += 1
return {
foo: {},
}
},
})
.extend({
addAttributes() {
callCounts.parent += 1
return {
...this.parent?.(),
bar: {},
}
},
})
.extend({
addAttributes() {
callCounts.child += 1
return {
...this.parent?.(),
bar: {},
}
},
})
getExtensionField(extension, 'addAttributes')()
expect(callCounts).to.deep.eq({
grandparent: 1,
parent: 1,
child: 1,
})
})
it('should call ancestors only once on configure', () => {
const callCounts = {
grandparent: 0,
parent: 0,
child: 0,
}
const extension = Extendable
.create({
addAttributes() {
callCounts.grandparent += 1
return {
foo: {},
}
},
})
.extend({
addAttributes() {
callCounts.parent += 1
return {
...this.parent?.(),
bar: {},
}
},
})
.extend({
addAttributes() {
callCounts.child += 1
return {
...this.parent?.(),
bar: {},
}
},
})
.configure({
baz: {},
})
getExtensionField(extension, 'addAttributes')()
expect(callCounts).to.deep.eq({
grandparent: 1,
parent: 1,
child: 1,
})
})
it('should use grandparent as parent on configure (not parent)', () => {
const grandparentExtension = Extendable
.create({
addAttributes() {
return {
foo: {},
}
},
})
const parentExtension = grandparentExtension
.extend({
addAttributes() {
return {
...this.parent?.(),
bar: {},
}
},
})
const childExtension = parentExtension
.configure({
baz: {},
})
expect(parentExtension.parent).to.eq(grandparentExtension)
expect(childExtension.parent).to.eq(grandparentExtension)
})
it('should use parent\'s config on `configure`', () => {
const grandparentExtension = Extendable
.create({
name: 'grandparent',
addAttributes() {
return {
foo: {},
}
},
})
const parentExtension = grandparentExtension
.extend({
name: 'parent',
addAttributes() {
return {
...this.parent?.(),
bar: {},
}
},
})
const childExtension = parentExtension
.configure({
baz: {},
})
expect(childExtension.config.name).to.eq('parent')
})
it('should allow extending a configure', () => {
const parentExtension = Extendable
.create({
addAttributes() {
return { foo: 'bar' }
},
})
const childExtension = parentExtension
.configure().extend()
const attributes = getExtensionField(childExtension, 'addAttributes')()
expect(attributes).to.deep.eq({
foo: 'bar',
})
})
it('should allow calling this.parent when extending a configure', () => {
const parentExtension = Extendable
.create({
name: 'parentExtension',
addAttributes() {
return {
foo: {},
}
},
})
const childExtension = parentExtension
.configure({}).extend({
addAttributes() {
return {
...this.parent?.(),
bar: {},
}
},
})
const attributes = getExtensionField(childExtension, 'addAttributes')()
expect(attributes).to.deep.eq({
foo: {},
bar: {},
})
})
it('should configure to be in addition to the parent options', () => {
const parentExtension = Extendable
.create({
name: 'parentExtension',
addOptions() {
return { parent: 'exists', overwrite: 'parent' }
},
})
const childExtension = parentExtension
.configure({ child: 'exists-too', overwrite: 'child' })
expect(childExtension.options).to.deep.eq({
parent: 'exists',
child: 'exists-too',
overwrite: 'child',
})
})
it('should deeply merge options when extending a configured extension', () => {
const parentExtension = Extendable
.create({
name: 'parentExtension',
addOptions() {
return { defaultOptions: 'exists', overwrite: 'parent' }
},
})
const childExtension = parentExtension
.configure({ configuredOptions: 'exists-too', overwrite: 'configure' }).extend({
name: 'childExtension',
addOptions() {
return { ...this.parent?.(), additionalOptions: 'exist-too', overwrite: 'child' }
},
})
expect(childExtension.options).to.deep.eq({
defaultOptions: 'exists',
configuredOptions: 'exists-too',
additionalOptions: 'exist-too',
overwrite: 'child',
})
})
})
})
})

@634750802
Copy link
Author

I’m unsure of the original intention behind these codes. Changing them might disrupt other usages or custom workarounds. It's okay not to fix this; just exercise caution when using custom extensions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Category: Open Source The issue or pull reuqest is related to the open source packages of Tiptap. Type: Bug The issue or pullrequest is related to a bug
Projects
Status: Triage open
Development

No branches or pull requests

2 participants