Extension model and composition
Understand create, configure, extend, installation order and ownership.
Choose the owner first
Extension supplies general behavior and presets; Node describes source nodes; Mark describes inline annotations. They share authoring hooks. They do not create a ProseMirror schema or a second JSON document.
| Goal | Entry point | Walkthrough |
|---|---|---|
| Change defaults | .configure() | Containers |
| Keep syntax, replace controls | .extend({ addNodeView() {} }) | Code blocks |
| Introduce syntax with writable fields | Node.create() + defineSource() | Custom nodes |
| Add parsing rules | addMarkdownExtensions() | Markdown plugins |
| Render multiline structural previews | addProjectionLayers() | Diagrams |
| Add transient state or input behavior | Extension.create() | State, input rules |
create: declare a reusable definition
import { Extension } from '@quirekit/msfe-core'
interface Options {
/** Host application name used by this extension. */
label: string
}
interface Storage {
/** Accepted source changes observed in this installation. */
changes: number
}
export const Audit = Extension.create<Options, Storage>({
name: 'audit',
addOptions() { return { label: 'editor' } },
addStorage() { return { changes: 0 } },
onUpdate() { this.storage.changes += 1 },
})
Definitions can be reused across editors. Options and storage are resolved independently per installation. Do not put instance state in module globals. Storage suits temporary services and counters; use defineState() for transaction mapping, history or projection dependencies.
configure: change options
const ReportAudit = Audit.configure({ label: 'report' })
This returns a new definition. Plain records merge recursively; arrays replace entirely; functions and non-plain objects replace by value. Retain default container names explicitly when changing types. Configuration is not a mounted-instance hot update. Use instance methods for theme, mode and permission changes.
extend: override a hook deliberately
const Audited = Audit.extend({
onUpdate(event) {
this.parent?.(event)
console.debug(this.options.label, event.revision)
},
})
Overridden hooks do not automatically combine results. Call the current hook's this.parent?.() to retain parent behavior and explicitly combine returned arrays. Use ordinary methods whenever a hook needs this.options, this.storage, this.editor or this.parent; arrow functions capture a different this.
Derived definitions inherit the name. Replace the corresponding preset entry instead of installing both definitions. Renaming alone does not resolve competing replacement projections.
Install one preset
import { StarterKit } from '@quirekit/msfe-starter-kit'
import { CodeBlock } from '@quirekit/msfe-code-block'
const CustomCodeBlock = CodeBlock.extend({
// Add the renderer from the code-block tutorial here.
})
const extensions = [
StarterKit.configure({ codeBlock: CustomCodeBlock, image: false }),
Audit,
]
Use addExtensions() to compose a business preset, then install that preset once. requires names extension dependencies. Dependency-ready extensions install in descending priority, default 100. Duplicate names and missing dependencies are diagnosed before mounting; array reordering should not conceal configuration errors.
Lifecycle and ownership
| Phase | Available context | Appropriate work |
|---|---|---|
| Configuration and state creation | Definitions, options and slots | Repeatable work without DOM effects |
onBeforeCreate | Configuration and state, no mounted DOM | Logic independent of mounting |
onCreate | Mounted editor | Subscribe to host services; optionally return a disposer |
onUpdate | Accepted source changes | Incremental observation and dirty state |
onTransaction | Every accepted transaction | Bounded reads, including selection/effects |
onSelectionUpdate | A changed selection | Refresh dependent toolbar state |
onDestroy | Disposal in progress | Cleanup in reverse installation order |
Components can unmount on scrolling, source reveal or mode changes. Their lifetime is shorter than the editor's. Bind asynchronous resources to the appropriate scope. See Extension API for exact hook signatures.
Earlier descriptors
defineExtension() and defineNodeView() remain public low-level entries. Their earlier descriptor configuration does not share all modern deep-merge semantics. Prefer the authoring API for new features. Tiptap-style names do not imply support for parseHTML, renderHTML, ProseMirror schema content or direct ProseMirror plugin installation.