Skip to content

feat: expose TipTap/ProseMirror globals for custom rich editor extensions#19559

Open
leek wants to merge 4 commits intofilamentphp:4.xfrom
leek:feat/expose-tiptap-globals
Open

feat: expose TipTap/ProseMirror globals for custom rich editor extensions#19559
leek wants to merge 4 commits intofilamentphp:4.xfrom
leek:feat/expose-tiptap-globals

Conversation

@leek
Copy link
Copy Markdown
Contributor

@leek leek commented Mar 24, 2026

Problem

Custom extensions loaded via RichContentPlugin::getTipTapJsExtensions() are fetched at runtime via dynamic import(). Each extension bundle must currently include its own copy of TipTap/ProseMirror because there's no way to reference the copy already bundled in rich-editor.js. This leads to:

  • Multiple ProseMirror instances on the same page
  • instanceof failures across bundles (e.g. DecorationSet, Node, Mark) — requiring monkey-patches to work around
  • ~150-200 KB of redundant JS per custom extension (each re-bundles @tiptap/core, prosemirror-state, etc.)

This is particularly painful for Tiptap Pro extensions (Pages, TableKit) which import heavily from @tiptap/core and @tiptap/pm/*.

Solution

Expose window.Filament.TipTap with the core ProseMirror modules that are already bundled in rich-editor.js:

window.Filament.TipTap = {
    'core': /* @tiptap/core */,
    'pm/state': /* @tiptap/pm/state */,
    'pm/view': /* @tiptap/pm/view */,
    'pm/model': /* @tiptap/pm/model */,
}

Custom extension builds can then mark @tiptap/* as external and resolve from this global at runtime, sharing the exact same ProseMirror instance as the host editor.

Example: esbuild plugin for custom extensions

// In your project's build script
const tiptapSharedPlugin = {
    name: 'tiptap-shared',
    setup(build) {
        build.onResolve({ filter: /^@tiptap\// }, args => ({
            path: args.path,
            namespace: 'tiptap-shared',
        }))

        build.onLoad({ filter: /.*/, namespace: 'tiptap-shared' }, async (args) => {
            const key = args.path.replace('@tiptap/', '')
            const realModule = await import(args.path)
            const namedExports = Object.keys(realModule)
                .filter(k => k !== '__esModule' && k !== 'default')

            let code = `const __m = window.Filament.TipTap[${JSON.stringify(key)}];\n`
            if (namedExports.length) {
                code += `export const { ${namedExports.join(', ')} } = __m;\n`
            }
            code += `export default __m?.default ?? __m;\n`
            return { contents: code, loader: 'js' }
        })
    },
}

Changes

  • packages/forms/resources/js/components/rich-editor-entry.js — new thin wrapper that exposes window.Filament.TipTap and re-exports the existing Alpine component
  • bin/build.js — points the rich-editor build at the wrapper entry point
  • packages/forms/dist/components/rich-editor.js — rebuilt (bundle size unchanged — the modules were already included, we're just assigning them to window)

Non-breaking

Existing custom extensions that bundle their own TipTap continue to work unchanged. This simply gives extension authors the option to externalize @tiptap/* and eliminate duplicate ProseMirror instances.

Custom extensions loaded via RichContentPlugin::getTipTapJsExtensions()
are fetched at runtime via dynamic import(). Each extension bundle must
currently include its own copy of TipTap/ProseMirror, leading to:

- Duplicate ProseMirror instances on the page
- instanceof checks failing across bundles (e.g. DecorationSet)
- ~150-200 KB of redundant JS per extension

This adds window.Filament.TipTap with the core ProseMirror modules
(@tiptap/core, @tiptap/pm/state, @tiptap/pm/view, @tiptap/pm/model)
already bundled in rich-editor.js. Custom extension builds can mark
@tiptap/* as external and resolve from this global at runtime,
sharing the same ProseMirror instance as the host editor.

Non-breaking: existing extensions that bundle their own TipTap
continue to work unchanged.
@leek leek marked this pull request as ready for review March 24, 2026 13:49
@danharrin danharrin added enhancement New feature or request pending review labels Mar 27, 2026
@danharrin danharrin added this to the v4 milestone Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request pending review

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants