[lexical-code-shiki][nextjs-code-shiki] Bug Fix: Externalize shiki dependencies in the published bundle#8514
Merged
Conversation
…ublished bundle `shiki`, `@shikijs/core`, `@shikijs/engine-javascript`, etc. are declared as runtime dependencies of `@lexical/code-shiki` but were still being inlined by Rollup, producing a ~9.8 MB bundle that shipped its own copy of the shiki sources. Treat `shiki` and `@shikijs/*` as third-party externals so the published bundle imports them from node_modules and shrinks to ~18 KB (prod ~6 KB). The previous `treeshake: 'recommended'` workaround for the inlined `oniguruma-to-es` feature detection is no longer needed and is reverted to `'smallest'`.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…@lexical/code-shiki Switch the lexical-esm-nextjs integration fixture from `@lexical/code` (which transitively exercises Prism) to `@lexical/code-shiki`, and load a non-default language (`python`) via `loadCodeLanguage` to exercise the dynamic `@shikijs/langs/<lang>` import path through Next.js. This gives the `prepare-release` integration tests release-tarball coverage for the shiki bundle, which would have caught a regression like shiki being inlined or its dynamic imports being inadvertently externalized without a corresponding declared dependency. The Astro fixture is left on the Prism path so both highlighters remain covered by release-artifact tests.
… a top-level example and convert to extensions
Move scripts/__tests__/integration/fixtures/lexical-esm-nextjs to
examples/nextjs-code-shiki and rename the package to
@lexical/nextjs-code-shiki-example. The prepare-release integration
test still picks it up automatically: prepare-release.test.mjs globs
both `examples/*/package.json` and
`scripts/__tests__/integration/fixtures/*/package.json`, so the same
release-tarball install/build/Playwright flow now runs against the
example.
While moving, port the example to the Lexical extension system:
- Drop the legacy LexicalComposer/RichTextPlugin/HistoryPlugin/
AutoFocusPlugin wiring and the custom CodeHighlightingPlugin
useEffect. The editor is now built from a single root extension
whose dependencies are RichTextExtension, HistoryExtension,
AutoFocusExtension, and a small example-owned
CodeShikiDemoExtension.
- CodeShikiDemoExtension uses $initialEditorState to seed the
"Registered: ..." text from getCodeLanguageOptions() and a
register() hook to await loadCodeLanguage('python'), appending
"Loaded: python" once the dynamic @shikijs/langs/python import
resolves. This is the load-bearing assertion that confirms
@shikijs/* are external in the published @lexical/code-shiki bundle
rather than inlined by Rollup.
- Remove the unused ToolbarPlugin, TreeViewPlugin, public/icons SVG
set, and tailwind/postcss/autoprefixer config (none of which were
actually used). Trim styles.css to just the rules the simplified
editor still references.
- Update the page title to "Lexical Next.js Code Shiki Example" and
update the Playwright assertion accordingly.
The Astro fixture is left in scripts/__tests__/integration/fixtures so
@lexical/code (Prism path) keeps release-artifact coverage too.
…: assertion The SSR commit changed CodeShikiDemoExtension to prepend the bold `Loaded: python` node via `$getRoot().selectStart().insertNodes(...)`, so the first .editor-paragraph's text content now starts with `Loaded: python\nRegistered:...` instead of `Registered:...`. Drop the `^` anchor on the regex so the assertion stays a substring test for `Registered:.*typescript` regardless of insertion order.
zurfyx
approved these changes
May 14, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
@lexical/code-shikideclaresshiki,@shikijs/core,@shikijs/engine-javascript,@shikijs/langs, and@shikijs/themesas runtimedependencies, butscripts/build.mjswas inlining all of them into the published bundle. The dev artifactpackages/lexical-code-shiki/dist/LexicalCodeShiki.mjsshipped a private copy of the shiki sources at ~9.8 MB even though the same packages would be installed from npm via the declared dependencies. Apps that depend on@lexical/code-shikitherefore paid the shiki cost twice and the in-bundle copy could drift from the npm copy.This PR adds
shikiand@shikijsto thethirdPartyExternalslist inscripts/build.mjsso Rollup marksshiki/*and@shikijs/*external for the published bundle. The output nowimports them fromnode_modulesat runtime, including the dynamic@shikijs/langs/<lang>and@shikijs/themes/<theme>imports shiki uses for on-demand language/theme loading.The previous
treeshake: 'recommended'carve-out for@lexical/code-shikiwas a workaround for anoniguruma-to-esfeature-detection bug that only surfaced because the engine was being inlined. With shiki externalized that workaround is no longer relevant, so the per-package treeshake selector is simplified back to the default'smallest'for everything except@lexical/code-prism(which still inlinesprismjsas before).Shiki is kept as a regular
dependencyrather than promoted topeerDependencies. This matches the existing repo convention — peers are reserved for ecosystem singletons whose duplication actively breaks things (react,react-dom,yjs); other internal third-party deps (@floating-ui/react,react-error-boundary,prismjs) stay as regular dependencies. Shiki's only mutable state is thecreateHighlighterCoreSync(...)instance owned by@lexical/code-shikiitself, so two on-disk copies of shiki would not silently mis-render.To prevent this from regressing again, the existing
lexical-esm-nextjsrelease-artifact fixture is moved to a top-level example atexamples/nextjs-code-shiki(@lexical/nextjs-code-shiki-example) and rewritten to use the Lexical extension system end-to-end:RichTextExtension,HistoryExtension,AutoFocusExtension, and a small example-ownedCodeShikiDemoExtension. No legacyLexicalComposer/*Pluginwrappers.CodeShikiDemoExtensionpulls inCodeShikiExtensionas a dependency, uses$initialEditorStateto seed the document fromgetCodeLanguageOptions(), and from itsregisterhookawaitsloadCodeLanguage('python')to exercise shiki's dynamic@shikijs/langs/<lang>import path. On resolve it inserts a boldLoaded: pythonnode. The async path is gated by anssrconfig field so it only runs in the browser.SSRContentEditablecomponent that useswithDOMfrom@lexical/headless/domto render the seeded editor state into HTML on the server (via a throwaway root element andinnerHTML), injects it withdangerouslySetInnerHTML+suppressHydrationWarning, and rebinds the real DOM root with a ref on the client.ToolbarPlugin,TreeViewPlugin,public/icons/*SVG set, andtailwindcss/postcss/autoprefixerconfig/devDeps are removed.styles.csstrims to just the rules the simplified editor uses.prepare-release.test.mjsglobs bothexamples/*/package.jsonandscripts/__tests__/integration/fixtures/*/package.json, so moving the fixture underexamples/keeps the same install-tarball/next build/Playwright flow without changes to the test harness. The Astro fixture is left inscripts/__tests__/integration/fixtures/lexical-esm-astro-react, so@lexical/code(Prism path) continues to have release-tarball coverage too.Bundle size impact for
@lexical/code-shiki:LexicalCodeShiki.mjs(dev)LexicalCodeShiki.js(dev)LexicalCodeShiki.{mjs,js}(prod)Closes #8515
Test plan
Before
packages/lexical-code-shiki/dist/LexicalCodeShiki.mjsis ~9.8 MB and contains inlined shiki source (ShikiError,clone, oniguruma-to-es feature detection, etc.) instead ofimportstatements for the declared dependencies.After
node scripts/build.mjsproduces ~18 KBLexicalCodeShiki.{mjs,js}whose only shiki references areimport { ... } from '@shikijs/core',from '@shikijs/engine-javascript',from 'shiki/langs', andfrom 'shiki/themes'.pnpm run clean && node scripts/build.mjs --prodproduces ~6 KB minified bundles with the same external imports.pnpm run test-unit— 3086 passed, 1 skipped.pnpm exec vitest --project scripts-unit --no-watch— 588 passed.pnpm run tsc— clean.pnpm run lintandpnpm exec prettier --check— clean.@lexical/code-prismbundle is unchanged (still inlinesprismjswithtreeshake: false).prepare-release.test.mjswill exercise the newexamples/nextjs-code-shikiexample: install the@lexical/code-shiki-0.44.0.tgz(and friends) tarballs, runnext build(which both validates thatshiki/@shikijs/*resolve as external dependencies under Next.js / webpack, and renders the seeded HTML on the server viawithDOM), then runnpm run test(Playwright) againstnext start. Assertions:Registered:.*typescript— synchronousbundledLanguagesInfolist resolved through the published bundle, rendered in both the SSR HTML and post-hydration.Loaded: python— the client-side dynamicimport('@shikijs/langs/python')inside shiki resolved at runtime, confirming that@shikijs/langsstayed external in the published bundle rather than getting inlined by Rollup.