diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/.gitignore b/examples/nextjs-code-shiki/.gitignore similarity index 100% rename from scripts/__tests__/integration/fixtures/lexical-esm-nextjs/.gitignore rename to examples/nextjs-code-shiki/.gitignore diff --git a/examples/nextjs-code-shiki/README.md b/examples/nextjs-code-shiki/README.md new file mode 100644 index 00000000000..32323826e33 --- /dev/null +++ b/examples/nextjs-code-shiki/README.md @@ -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/` 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 +``` diff --git a/examples/nextjs-code-shiki/app/EditorClient.tsx b/examples/nextjs-code-shiki/app/EditorClient.tsx new file mode 100644 index 00000000000..e9cec88ac02 --- /dev/null +++ b/examples/nextjs-code-shiki/app/EditorClient.tsx @@ -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 ( +
{ + const root = document.createElement('div'); + editor.setRootElement(root); + const {innerHTML} = root; + editor.setRootElement(null); + return innerHTML; + }) + : '', + }} + /> + ); +} + +export default function EditorClient() { + return ( +
+
+ } + /> +
+
+ ); +} diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/ExampleTheme.ts b/examples/nextjs-code-shiki/app/ExampleTheme.ts similarity index 100% rename from scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/ExampleTheme.ts rename to examples/nextjs-code-shiki/app/ExampleTheme.ts diff --git a/examples/nextjs-code-shiki/app/extensions/CodeShikiDemoExtension.ts b/examples/nextjs-code-shiki/app/extensions/CodeShikiDemoExtension.ts new file mode 100644 index 00000000000..e33db32d321 --- /dev/null +++ b/examples/nextjs-code-shiki/app/extensions/CodeShikiDemoExtension.ts @@ -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/` + * 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/` import, then appends `Loaded: ` 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; + }; + }, +}); diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/favicon.ico b/examples/nextjs-code-shiki/app/favicon.ico similarity index 100% rename from scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/favicon.ico rename to examples/nextjs-code-shiki/app/favicon.ico diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/layout.tsx b/examples/nextjs-code-shiki/app/layout.tsx similarity index 75% rename from scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/layout.tsx rename to examples/nextjs-code-shiki/app/layout.tsx index 09c4015989c..214fe63e5b7 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/layout.tsx +++ b/examples/nextjs-code-shiki/app/layout.tsx @@ -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({ diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/page.tsx b/examples/nextjs-code-shiki/app/page.tsx similarity index 71% rename from scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/page.tsx rename to examples/nextjs-code-shiki/app/page.tsx index 18ef069d848..8110eaffdab 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/page.tsx +++ b/examples/nextjs-code-shiki/app/page.tsx @@ -6,15 +6,15 @@ * */ -import EditorUseClient from "./EditorUseClient"; +import EditorClient from './EditorClient'; export const dynamic = 'force-dynamic'; export default function Home() { return (
-

Next.js Rich Text Lexical Example

- +

Lexical Next.js Code Shiki Example

+
); } diff --git a/examples/nextjs-code-shiki/app/styles.css b/examples/nextjs-code-shiki/app/styles.css new file mode 100644 index 00000000000..c7bdf40a8dc --- /dev/null +++ b/examples/nextjs-code-shiki/app/styles.css @@ -0,0 +1,210 @@ +/** + * 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. + * + */ + +body { + margin: 0; + background: #eee; + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + '.SFNSText-Regular', + sans-serif; + font-weight: 500; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +h1 { + font-size: 24px; + color: #333; +} + +.editor-container { + margin: 20px auto 20px auto; + border-radius: 2px; + max-width: 600px; + color: #000; + position: relative; + line-height: 20px; + font-weight: 400; + text-align: left; + border-top-left-radius: 10px; + border-top-right-radius: 10px; +} + +.editor-inner { + background: #fff; + position: relative; +} + +.editor-input { + min-height: 150px; + resize: none; + font-size: 16px; + caret-color: rgb(5, 5, 5); + position: relative; + tab-size: 1; + outline: 0; + padding: 15px 10px; + caret-color: #444; +} + +.editor-placeholder { + color: #999; + overflow: hidden; + position: absolute; + text-overflow: ellipsis; + top: 15px; + left: 10px; + font-size: 16px; + user-select: none; + display: inline-block; + pointer-events: none; +} + +.editor-text-bold { + font-weight: bold; +} + +.editor-text-italic { + font-style: italic; +} + +.editor-text-underline { + text-decoration: underline; +} + +.editor-text-strikethrough { + text-decoration: line-through; +} + +.editor-text-underlineStrikethrough { + text-decoration: underline line-through; +} + +.editor-text-code { + background-color: rgb(240, 242, 245); + padding: 1px 0.25rem; + font-family: Menlo, Consolas, Monaco, monospace; + font-size: 94%; +} + +.editor-link { + color: rgb(33, 111, 219); + text-decoration: none; +} + +.editor-code { + background-color: rgb(240, 242, 245); + font-family: Menlo, Consolas, Monaco, monospace; + display: block; + padding: 8px 8px 8px 52px; + line-height: 1.53; + font-size: 13px; + margin: 0; + margin-top: 8px; + margin-bottom: 8px; + tab-size: 2; + overflow-x: auto; + position: relative; +} + +.editor-code:before { + content: attr(data-gutter); + position: absolute; + background-color: #eee; + left: 0; + top: 0; + border-right: 1px solid #ccc; + padding: 8px; + color: #777; + white-space: pre-wrap; + text-align: right; + min-width: 25px; +} + +.editor-code:after { + content: attr(data-highlight-language); + top: 0; + right: 3px; + padding: 3px; + font-size: 10px; + text-transform: uppercase; + position: absolute; + color: rgba(0, 0, 0, 0.5); +} + +.editor-paragraph { + margin: 0; + margin-bottom: 8px; + position: relative; +} + +.editor-paragraph:last-child { + margin-bottom: 0; +} + +.editor-heading-h1 { + font-size: 24px; + color: rgb(5, 5, 5); + font-weight: 400; + margin: 0; + margin-bottom: 12px; + padding: 0; +} + +.editor-heading-h2 { + font-size: 16px; + color: rgb(101, 103, 107); + font-weight: 700; + margin: 0; + margin-top: 10px; + padding: 0; + text-transform: uppercase; +} + +.editor-quote { + margin: 0; + margin-left: 20px; + font-size: 16px; + color: rgb(101, 103, 107); + border-left-color: rgb(206, 208, 212); + border-left-width: 4px; + border-left-style: solid; + padding-left: 16px; +} + +.editor-list-ol { + padding: 0; + margin: 0; + margin-left: 16px; +} + +.editor-list-ul { + padding: 0; + margin: 0; + margin-left: 16px; +} + +.editor-listitem { + margin: 8px 32px 8px 32px; +} + +.editor-nested-listitem { + list-style-type: none; +} + +pre::-webkit-scrollbar { + background: transparent; + width: 10px; +} + +pre::-webkit-scrollbar-thumb { + background: #999; +} diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/next.config.mjs b/examples/nextjs-code-shiki/next.config.mjs similarity index 100% rename from scripts/__tests__/integration/fixtures/lexical-esm-nextjs/next.config.mjs rename to examples/nextjs-code-shiki/next.config.mjs diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json b/examples/nextjs-code-shiki/package.json similarity index 65% rename from scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json rename to examples/nextjs-code-shiki/package.json index 3b2ef5caf88..21b3ad2b960 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json +++ b/examples/nextjs-code-shiki/package.json @@ -1,5 +1,5 @@ { - "name": "lexical-esm-nextjs", + "name": "@lexical/nextjs-code-shiki-example", "version": "0.44.0", "private": true, "scripts": { @@ -9,9 +9,13 @@ "test": "playwright test" }, "dependencies": { - "@lexical/code": "0.44.0", - "@lexical/plain-text": "0.44.0", + "@lexical/code-core": "0.44.0", + "@lexical/code-shiki": "0.44.0", + "@lexical/extension": "0.44.0", + "@lexical/headless": "0.44.0", + "@lexical/history": "0.44.0", "@lexical/react": "0.44.0", + "@lexical/rich-text": "0.44.0", "lexical": "0.44.0", "next": "^15.5.14", "react": "^19", @@ -22,9 +26,6 @@ "@types/node": "^20", "@types/react": "^19.2.14", "@types/react-dom": "^19.1.9", - "autoprefixer": "^10.0.1", - "postcss": "^8", - "tailwindcss": "^3.3.0", "typescript": "^5.9.2" } } diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/playwright.config.ts b/examples/nextjs-code-shiki/playwright.config.ts similarity index 58% rename from scripts/__tests__/integration/fixtures/lexical-esm-nextjs/playwright.config.ts rename to examples/nextjs-code-shiki/playwright.config.ts index 0128b611ef0..e502894f552 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/playwright.config.ts +++ b/examples/nextjs-code-shiki/playwright.config.ts @@ -6,15 +6,15 @@ * */ -import type { PlaywrightTestConfig } from '@playwright/test'; +import type {PlaywrightTestConfig} from '@playwright/test'; const config: PlaywrightTestConfig = { - webServer: { - command: 'pnpm run start', - port: 3000 - }, - testDir: 'tests', - testMatch: /(.+\.)?(test|spec)\.[jt]s/ + testDir: 'tests', + testMatch: /(.+\.)?(test|spec)\.[jt]s/, + webServer: { + command: 'pnpm run start', + port: 3000, + }, }; export default config; diff --git a/examples/nextjs-code-shiki/tests/test.ts b/examples/nextjs-code-shiki/tests/test.ts new file mode 100644 index 00000000000..d3666b93fbf --- /dev/null +++ b/examples/nextjs-code-shiki/tests/test.ts @@ -0,0 +1,26 @@ +/** + * 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 {expect, test} from '@playwright/test'; + +test('index page has expected h1 and lexical state', async ({page}) => { + await page.goto('/'); + await expect( + page.getByRole('heading', {name: 'Lexical Next.js Code Shiki Example'}), + ).toBeVisible(); + // The synchronous list comes from shiki/langs bundledLanguagesInfo + await expect( + page.locator('.editor-input .editor-paragraph').first(), + ).toContainText(/Registered:.*typescript/); + // Confirms the dynamic `@shikijs/langs/python` import succeeded under + // Next.js, which only works if shiki packages are external in the + // published @lexical/code-shiki bundle. + await expect( + page.locator('.editor-input').getByText('Loaded: python'), + ).toBeVisible(); +}); diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/tsconfig.json b/examples/nextjs-code-shiki/tsconfig.json similarity index 79% rename from scripts/__tests__/integration/fixtures/lexical-esm-nextjs/tsconfig.json rename to examples/nextjs-code-shiki/tsconfig.json index f15eefe22cf..167c6a3acc2 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/tsconfig.json +++ b/examples/nextjs-code-shiki/tsconfig.json @@ -1,10 +1,6 @@ { "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -22,9 +18,7 @@ } ], "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] }, "target": "ES2017" }, @@ -35,7 +29,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/README.md b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/README.md deleted file mode 100644 index c4033664f80..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/EditorUseClient.tsx b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/EditorUseClient.tsx deleted file mode 100644 index 3ba131b4b81..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/EditorUseClient.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client"; -/** - * 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 { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin"; -import { LexicalComposer } from "@lexical/react/LexicalComposer"; -import { ContentEditable } from "@lexical/react/LexicalContentEditable"; -import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"; -import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; -import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; -import { - CodeHighlightNode, - CodeNode, - // TODO: Using deprecated re-exports from @lexical/code-prism to test #8198, this can be refactored after that release - getCodeLanguages, - registerCodeHighlighting, -} from "@lexical/code"; - -import * as React from 'react'; - -import ExampleTheme from "./ExampleTheme"; -import ToolbarPlugin from "./plugins/ToolbarPlugin"; -import TreeViewPlugin from "./plugins/TreeViewPlugin"; -import { useEffect } from "react"; -import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { $getRoot } from "lexical"; - -const editorConfig = { - namespace: "React.js Demo", - nodes: [CodeNode, CodeHighlightNode], - // Handling of errors during update - onError(error: Error) { - throw error; - }, - // The editor theme - theme: ExampleTheme, -}; - -const placeholder = 'Enter some rich text...'; - -function CodeHighlightingPlugin() { - const [editor] = useLexicalComposerContext(); - useEffect(() => { - registerCodeHighlighting(editor); - editor.update(() => { - $getRoot() - .clear() - .selectEnd() - .insertRawText(["Registered:", ...getCodeLanguages()].join("\n")); - }); - }, [editor]); - return null; -} - -export default function App() { - return ( - -
- -
- {placeholder}
- } - /> - } - ErrorBoundary={LexicalErrorBoundary} - /> - - - - -
-
- - ); -} diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/plugins/ToolbarPlugin.tsx b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/plugins/ToolbarPlugin.tsx deleted file mode 100644 index 0d9ec2c73a8..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/plugins/ToolbarPlugin.tsx +++ /dev/null @@ -1,172 +0,0 @@ -/** - * 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 {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import { - $getSelection, - $isRangeSelection, - COMMAND_PRIORITY_LOW, - CAN_REDO_COMMAND, - CAN_UNDO_COMMAND, - FORMAT_ELEMENT_COMMAND, - FORMAT_TEXT_COMMAND, - REDO_COMMAND, - SELECTION_CHANGE_COMMAND, - UNDO_COMMAND, - mergeRegister, -} from 'lexical'; -import {useCallback, useEffect, useRef, useState} from 'react'; -// import * as React from 'react'; - -function Divider() { - return
; -} - -export default function ToolbarPlugin() { - const [editor] = useLexicalComposerContext(); - const toolbarRef = useRef(null); - const [canUndo, setCanUndo] = useState(false); - const [canRedo, setCanRedo] = useState(false); - const [isBold, setIsBold] = useState(false); - const [isItalic, setIsItalic] = useState(false); - const [isUnderline, setIsUnderline] = useState(false); - const [isStrikethrough, setIsStrikethrough] = useState(false); - - const updateToolbar = useCallback(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - // Update text format - setIsBold(selection.hasFormat('bold')); - setIsItalic(selection.hasFormat('italic')); - setIsUnderline(selection.hasFormat('underline')); - setIsStrikethrough(selection.hasFormat('strikethrough')); - } - }, []); - - useEffect(() => { - return mergeRegister( - editor.registerUpdateListener(({editorState}) => { - editorState.read(() => { - updateToolbar(); - }); - }), - editor.registerCommand( - SELECTION_CHANGE_COMMAND, - (_payload, _newEditor) => { - updateToolbar(); - return false; - }, - COMMAND_PRIORITY_LOW, - ), - editor.registerCommand( - CAN_UNDO_COMMAND, - (payload) => { - setCanUndo(payload); - return false; - }, - COMMAND_PRIORITY_LOW, - ), - editor.registerCommand( - CAN_REDO_COMMAND, - (payload) => { - setCanRedo(payload); - return false; - }, - COMMAND_PRIORITY_LOW, - ), - ); - }, [editor, updateToolbar]); - - return ( -
- - - - - - - - - - - - {' '} -
- ); -} diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/plugins/TreeViewPlugin.tsx b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/plugins/TreeViewPlugin.tsx deleted file mode 100644 index f72600c947e..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/plugins/TreeViewPlugin.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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 type {JSX} from 'react'; - -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {TreeView} from '@lexical/react/LexicalTreeView'; -// import * as React from 'react'; - -export default function TreeViewPlugin(): JSX.Element { - const [editor] = useLexicalComposerContext(); - return ( - - ); -} diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/styles.css b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/styles.css deleted file mode 100644 index cd1742409b9..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/app/styles.css +++ /dev/null @@ -1,442 +0,0 @@ -/** - * 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. - * - */ - -body { - margin: 0; - background: #eee; - font-family: system-ui, -apple-system, BlinkMacSystemFont, '.SFNSText-Regular', - sans-serif; - font-weight: 500; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.other h2 { - font-size: 18px; - color: #444; - margin-bottom: 7px; -} - -.other a { - color: #777; - text-decoration: underline; - font-size: 14px; -} - -.other ul { - padding: 0; - margin: 0; - list-style-type: none; -} - -.App { - font-family: sans-serif; - text-align: center; -} - -h1 { - font-size: 24px; - color: #333; -} - -.editor-container { - margin: 20px auto 20px auto; - border-radius: 2px; - max-width: 600px; - color: #000; - position: relative; - line-height: 20px; - font-weight: 400; - text-align: left; - border-top-left-radius: 10px; - border-top-right-radius: 10px; -} - -.editor-inner { - background: #fff; - position: relative; -} - -.editor-input { - min-height: 150px; - resize: none; - font-size: 16px; - caret-color: rgb(5, 5, 5); - position: relative; - tab-size: 1; - outline: 0; - padding: 15px 10px; - caret-color: #444; -} - -.editor-placeholder { - color: #999; - overflow: hidden; - position: absolute; - text-overflow: ellipsis; - top: 15px; - left: 10px; - font-size: 16px; - user-select: none; - display: inline-block; - pointer-events: none; -} - -.editor-text-bold { - font-weight: bold; -} - -.editor-text-italic { - font-style: italic; -} - -.editor-text-underline { - text-decoration: underline; -} - -.editor-text-strikethrough { - text-decoration: line-through; -} - -.editor-text-underlineStrikethrough { - text-decoration: underline line-through; -} - -.editor-text-code { - background-color: rgb(240, 242, 245); - padding: 1px 0.25rem; - font-family: Menlo, Consolas, Monaco, monospace; - font-size: 94%; -} - -.editor-link { - color: rgb(33, 111, 219); - text-decoration: none; -} - -.tree-view-output { - display: block; - background: #222; - color: #fff; - padding: 5px; - font-size: 12px; - white-space: pre-wrap; - margin: 1px auto 10px auto; - max-height: 250px; - position: relative; - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; - overflow: auto; - line-height: 14px; -} - -.editor-code { - background-color: rgb(240, 242, 245); - font-family: Menlo, Consolas, Monaco, monospace; - display: block; - padding: 8px 8px 8px 52px; - line-height: 1.53; - font-size: 13px; - margin: 0; - margin-top: 8px; - margin-bottom: 8px; - tab-size: 2; - /* white-space: pre; */ - overflow-x: auto; - position: relative; -} - -.editor-code:before { - content: attr(data-gutter); - position: absolute; - background-color: #eee; - left: 0; - top: 0; - border-right: 1px solid #ccc; - padding: 8px; - color: #777; - white-space: pre-wrap; - text-align: right; - min-width: 25px; -} -.editor-code:after { - content: attr(data-highlight-language); - top: 0; - right: 3px; - padding: 3px; - font-size: 10px; - text-transform: uppercase; - position: absolute; - color: rgba(0, 0, 0, 0.5); -} - -.editor-tokenComment { - color: slategray; -} - -.editor-tokenPunctuation { - color: #999; -} - -.editor-tokenProperty { - color: #905; -} - -.editor-tokenSelector { - color: #690; -} - -.editor-tokenOperator { - color: #9a6e3a; -} - -.editor-tokenAttr { - color: #07a; -} - -.editor-tokenVariable { - color: #e90; -} - -.editor-tokenFunction { - color: #dd4a68; -} - -.editor-paragraph { - margin: 0; - margin-bottom: 8px; - position: relative; -} - -.editor-paragraph:last-child { - margin-bottom: 0; -} - -.editor-heading-h1 { - font-size: 24px; - color: rgb(5, 5, 5); - font-weight: 400; - margin: 0; - margin-bottom: 12px; - padding: 0; -} - -.editor-heading-h2 { - font-size: 16px; - color: rgb(101, 103, 107); - font-weight: 700; - margin: 0; - margin-top: 10px; - padding: 0; - text-transform: uppercase; -} - -.editor-quote { - margin: 0; - margin-left: 20px; - font-size: 16px; - color: rgb(101, 103, 107); - border-left-color: rgb(206, 208, 212); - border-left-width: 4px; - border-left-style: solid; - padding-left: 16px; -} - -.editor-list-ol { - padding: 0; - margin: 0; - margin-left: 16px; -} - -.editor-list-ul { - padding: 0; - margin: 0; - margin-left: 16px; -} - -.editor-listitem { - margin: 8px 32px 8px 32px; -} - -.editor-nested-listitem { - list-style-type: none; -} - -pre::-webkit-scrollbar { - background: transparent; - width: 10px; -} - -pre::-webkit-scrollbar-thumb { - background: #999; -} - -.debug-timetravel-panel { - overflow: hidden; - padding: 0 0 10px 0; - margin: auto; - display: flex; -} - -.debug-timetravel-panel-slider { - padding: 0; - flex: 8; -} - -.debug-timetravel-panel-button { - padding: 0; - border: 0; - background: none; - flex: 1; - color: #fff; - font-size: 12px; -} - -.debug-timetravel-panel-button:hover { - text-decoration: underline; -} - -.debug-timetravel-button { - border: 0; - padding: 0; - font-size: 12px; - top: 10px; - right: 15px; - position: absolute; - background: none; - color: #fff; -} - -.debug-timetravel-button:hover { - text-decoration: underline; -} - -.toolbar { - display: flex; - margin-bottom: 1px; - background: #fff; - padding: 4px; - border-top-left-radius: 10px; - border-top-right-radius: 10px; - vertical-align: middle; -} - -.toolbar button.toolbar-item { - border: 0; - display: flex; - background: none; - border-radius: 10px; - padding: 8px; - cursor: pointer; - vertical-align: middle; -} - -.toolbar button.toolbar-item:disabled { - cursor: not-allowed; -} - -.toolbar button.toolbar-item.spaced { - margin-right: 2px; -} - -.toolbar button.toolbar-item i.format { - background-size: contain; - display: inline-block; - height: 18px; - width: 18px; - margin-top: 2px; - vertical-align: -0.25em; - display: flex; - opacity: 0.6; -} - -.toolbar button.toolbar-item:disabled i.format { - opacity: 0.2; -} - -.toolbar button.toolbar-item.active { - background-color: rgba(223, 232, 250, 0.3); -} - -.toolbar button.toolbar-item.active i { - opacity: 1; -} - -.toolbar .toolbar-item:hover:not([disabled]) { - background-color: #eee; -} - -.toolbar .divider { - width: 1px; - background-color: #eee; - margin: 0 4px; -} - -.toolbar .toolbar-item .text { - display: flex; - line-height: 20px; - width: 200px; - vertical-align: middle; - font-size: 14px; - color: #777; - text-overflow: ellipsis; - width: 70px; - overflow: hidden; - height: 20px; - text-align: left; -} - -.toolbar .toolbar-item .icon { - display: flex; - width: 20px; - height: 20px; - user-select: none; - margin-right: 8px; - line-height: 16px; - background-size: contain; -} - -i.undo { - background-image: url(/icons/arrow-counterclockwise.svg); -} - -i.redo { - background-image: url(/icons/arrow-clockwise.svg); -} - -i.bold { - background-image: url(/icons/type-bold.svg); -} - -i.italic { - background-image: url(/icons/type-italic.svg); -} - -i.underline { - background-image: url(/icons/type-underline.svg); -} - -i.strikethrough { - background-image: url(/icons/type-strikethrough.svg); -} - -i.left-align { - background-image: url(/icons/text-left.svg); -} - -i.center-align { - background-image: url(/icons/text-center.svg); -} - -i.right-align { - background-image: url(/icons/text-right.svg); -} - -i.justify-align { - background-image: url(/icons/justify.svg); -} diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/postcss.config.js b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/postcss.config.js deleted file mode 100644 index 08a1351d9d3..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/postcss.config.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 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. - * - */ - -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/LICENSE.md b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/LICENSE.md deleted file mode 100644 index ce74f6abeed..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/LICENSE.md +++ /dev/null @@ -1,5 +0,0 @@ -Bootstrap Icons -https://icons.getbootstrap.com - -Licensed under MIT license -https://github.com/twbs/icons/blob/main/LICENSE.md diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/arrow-clockwise.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/arrow-clockwise.svg deleted file mode 100644 index b072eb097ab..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/arrow-clockwise.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/arrow-counterclockwise.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/arrow-counterclockwise.svg deleted file mode 100644 index b0b23b9bbc4..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/arrow-counterclockwise.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/journal-text.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/journal-text.svg deleted file mode 100644 index 9b66f43aab5..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/journal-text.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/justify.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/justify.svg deleted file mode 100644 index 009bd7214d9..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/justify.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-center.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-center.svg deleted file mode 100644 index 2887a99f267..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-center.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-left.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-left.svg deleted file mode 100644 index 04526116489..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-paragraph.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-paragraph.svg deleted file mode 100644 index 9779beabf1c..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-paragraph.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-right.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-right.svg deleted file mode 100644 index 34686b0f1ff..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/text-right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-bold.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-bold.svg deleted file mode 100644 index 276d133c25c..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-bold.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-italic.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-italic.svg deleted file mode 100644 index 3ac6b09f02a..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-italic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-strikethrough.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-strikethrough.svg deleted file mode 100644 index 1c940e42a87..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-strikethrough.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-underline.svg b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-underline.svg deleted file mode 100644 index c299b8bf2f0..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/public/icons/type-underline.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/tailwind.config.ts b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/tailwind.config.ts deleted file mode 100644 index d8298e3a252..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/tailwind.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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 type { Config } from "tailwindcss"; - -const config: Config = { - content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - "./app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, - }, - }, - plugins: [], -}; -export default config; diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/tests/test.ts b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/tests/test.ts deleted file mode 100644 index 53f1ef3ea9a..00000000000 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/tests/test.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * 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 { expect, test } from '@playwright/test'; - -test('index page has expected h1 and lexical state', async ({ page }) => { - await page.goto('/'); - await expect( - page.getByRole('heading', { name: 'Next.js Rich Text Lexical Example' }) - ).toBeVisible(); - await expect(page.locator('.editor-input .editor-paragraph').first()).toContainText(/^Registered:.*typescript/); -}); diff --git a/scripts/build.mjs b/scripts/build.mjs index 65f8d146ccd..45d2562f3b4 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -126,6 +126,12 @@ const thirdPartyExternals = [ 'y-websocket', 'happy-dom', 'jsdom', + // The @lexical/code-shiki package declares shiki and @shikijs/* as + // npm dependencies and loads languages/themes via dynamic import, so + // they must remain external in the published bundle rather than be + // inlined by Rollup. + 'shiki', + '@shikijs', ...(isWWW ? [':server-only-hack:.*'] : ['react-error-boundary', '@floating-ui/react']), @@ -322,11 +328,7 @@ async function build( }, ], // Lexical Code: this ensures PrismJS imports get included in the bundle - // Lexical Code Shiki: 'recommended' preset has treeshake.tryCatchDeoptimization: true which avoids - // feature detection of oniguruma-to-es to be optimized out and cause a bug - treeshake: ['smallest', false, 'recommended'][ - 1 + ['Lexical Code Prism', 'Lexical Code Shiki'].indexOf(name) - ], + treeshake: name === 'Lexical Code Prism' ? false : 'smallest', }; /** @type {import('rollup').OutputOptions} */ const outputOptions = {