Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions examples/nextjs-code-shiki/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Next.js + `@lexical/code-shiki` example

A minimal [Next.js](https://nextjs.org/) app that drives a Lexical rich-text
editor with Shiki-based code highlighting, wired entirely through the
Lexical [Extension](https://lexical.dev/docs/concepts/extensions) system.

## What it demonstrates

- Using `LexicalExtensionComposer` (from `@lexical/react/LexicalExtensionComposer`)
with `RichTextExtension`, `HistoryExtension`, `AutoFocusExtension`, and a
small example-owned `CodeShikiDemoExtension` that pulls in
`CodeShikiExtension` as a dependency. No legacy `LexicalComposer` /
`*Plugin` wrappers.
- Driving `@lexical/code-shiki`'s public API from the extension:
- `getCodeLanguageOptions()` to seed the editor with the language list
that shiki has bundled metadata for, via the extension's
`$initialEditorState` hook.
- `loadCodeLanguage('python')` from the extension's `register` hook to
exercise shiki's dynamic `@shikijs/langs/<lang>` import path through
Next.js' bundler. Once the promise resolves and `isCodeLanguageLoaded`
confirms it, the extension appends `Loaded: python` to the document.

## Why this example exists

This is also the release-artifact integration test for `@lexical/code-shiki`.
`scripts/__tests__/integration/prepare-release.test.mjs` globs
`examples/*/package.json`, installs each one against the freshly built
`npm/*.tgz` tarballs, runs `npm run build` (so Next.js bundles the example
against the published `@lexical/code-shiki`), then runs `npm run test`
(Playwright) against the production build. The Playwright assertions
confirm:

1. `Registered:.*typescript` — the synchronous `bundledLanguagesInfo` list
resolved through the published bundle.
2. `Loaded: python` — the dynamic `import('@shikijs/langs/python')`
inside shiki resolved at runtime, which only works if
`shiki` / `@shikijs/*` are **external** in the published
`@lexical/code-shiki` bundle rather than inlined by Rollup.

## Local development

```bash
pnpm install
pnpm run dev # next dev on http://localhost:3000
pnpm run build # next build
pnpm run start # next start (used by playwright.config.ts)
pnpm run test # playwright tests against the built app
```
67 changes: 67 additions & 0 deletions examples/nextjs-code-shiki/app/EditorClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use client';

import {AutoFocusExtension} from '@lexical/extension';
import {withDOM} from '@lexical/headless/dom';
import {HistoryExtension} from '@lexical/history';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';
import {RichTextExtension} from '@lexical/rich-text';
import {defineExtension} from 'lexical';

import ExampleTheme from './ExampleTheme';
import {CodeShikiDemoExtension} from './extensions/CodeShikiDemoExtension';

const editorExtension = defineExtension({
dependencies: [
RichTextExtension,
HistoryExtension,
AutoFocusExtension,
CodeShikiDemoExtension,
],
name: '@lexical/nextjs-code-shiki-example/Editor',
namespace: '@lexical/nextjs-code-shiki-example',
theme: ExampleTheme,
});

function SSRContentEditable() {
const [editor] = useLexicalComposerContext();
return (
<div
className="editor-input"
suppressHydrationWarning={true}
ref={editor.setRootElement.bind(editor)}
dangerouslySetInnerHTML={{
__html:
typeof window === 'undefined'
? withDOM(() => {
const root = document.createElement('div');
editor.setRootElement(root);
const {innerHTML} = root;
editor.setRootElement(null);
return innerHTML;
})
: '',
}}
/>
);
}

export default function EditorClient() {
return (
<div className="editor-container">
<div className="editor-inner">
<LexicalExtensionComposer
extension={editorExtension}
contentEditable={<SSRContentEditable />}
/>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
CodeShikiExtension,
getCodeLanguageOptions,
isCodeLanguageLoaded,
loadCodeLanguage,
} from '@lexical/code-shiki';
import {
$createLineBreakNode,
$createTextNode,
$getRoot,
defineExtension,
} from 'lexical';

/**
* Language we dynamically load to exercise the `@shikijs/langs/<lang>`
* dynamic-import path through the Next.js bundler. The Playwright
* assertion `Loaded: ${DEMO_LANGUAGE}` only passes if the import
* resolves at runtime, which in turn requires `@shikijs/langs` to be
* external in the published `@lexical/code-shiki` bundle.
*/
const DEMO_LANGUAGE = 'python';

function $seedDemo() {
const registeredIds = getCodeLanguageOptions().map(([id]) => id);
$getRoot()
.clear()
.selectEnd()
.insertRawText(['Registered:', ...registeredIds].join('\n'));
}

/**
* Demo wiring for the Next.js code-shiki example:
*
* - Seeds the editor (via `$initialEditorState`) with `Registered: ...`
* followed by every language id from `getCodeLanguageOptions()`, which
* pulls from shiki's `bundledLanguagesInfo`.
* - Calls `loadCodeLanguage(DEMO_LANGUAGE)` to trigger the dynamic
* `@shikijs/langs/<lang>` import, then appends `Loaded: <lang>` once
* the promise resolves.
*
* Pulls `CodeShikiExtension` in as a dependency so the highlighter is
* registered on the editor automatically.
*/
export const CodeShikiDemoExtension = defineExtension({
$initialEditorState: $seedDemo,
config: {ssr: typeof window === 'undefined'},
dependencies: [CodeShikiExtension],
name: '@lexical/nextjs-code-shiki-example/CodeShikiDemo',
register(editor, config) {
let cancelled = false;
if (!config.ssr) {
void Promise.resolve(loadCodeLanguage(DEMO_LANGUAGE))
.then(() => {
if (cancelled || !isCodeLanguageLoaded(DEMO_LANGUAGE)) {
return;
}
editor.update(() => {
$getRoot()
.selectStart()
.insertNodes([
$createTextNode(`Loaded: ${DEMO_LANGUAGE}`).toggleFormat(
'bold',
),
$createLineBreakNode(),
]);
});
})
.catch(err => console.error(err));
}
return () => {
cancelled = true;
};
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
*
*/

import type { Metadata } from "next";
import "./styles.css";
import type {Metadata} from 'next';

import './styles.css';

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
description: 'Generated by create next app',
title: 'Create Next App',
};

export default function RootLayout({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
*
*/

import EditorUseClient from "./EditorUseClient";
import EditorClient from './EditorClient';

export const dynamic = 'force-dynamic';

export default function Home() {
return (
<main>
<h1>Next.js Rich Text Lexical Example</h1>
<EditorUseClient />
<h1>Lexical Next.js Code Shiki Example</h1>
<EditorClient />
</main>
);
}
Loading