diff --git a/.github/workflows/package-consumers.yml b/.github/workflows/package-consumers.yml index c9bff3d4a..4a83a3c35 100644 --- a/.github/workflows/package-consumers.yml +++ b/.github/workflows/package-consumers.yml @@ -29,3 +29,5 @@ jobs: - run: npm run build - run: npm run coverage - run: npm run test:consumers + - run: npm run build --workspace nextjs + - run: npm run test:nextjs diff --git a/core/README.md b/core/README.md index d59fc5c92..b29112353 100644 --- a/core/README.md +++ b/core/README.md @@ -1064,49 +1064,38 @@ export default function App() { ### Support Nextjs -Use examples in [nextjs](https://nextjs.org/). [`#52`](https://github.com/uiwjs/react-md-editor/issues/52#issuecomment-848969341) [`#224`](https://github.com/uiwjs/react-md-editor/issues/224#issuecomment-901112079) +For Next.js App Router, see the working [Next.js example](https://github.com/uiwjs/react-md-editor/tree/master/example/nextjs) ([#682](https://github.com/uiwjs/react-md-editor/issues/682)) -[![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?logo=codesandbox)](https://codesandbox.io/embed/nextjs-example-react-md-editor-https-github-com-uiwjs-react-md-editor-issues-516-1z56px?fontsize=14&hidenavigation=1&theme=dark) -[![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?logo=codesandbox)](https://codesandbox.io/embed/nextjs-example-react-md-editor-qjhn7?fontsize=14&hidenavigation=1&theme=dark) [![Open in StackBlitz](https://img.shields.io/badge/Open%20In-StackBlitz-green)](https://stackblitz.com/edit/nextjs-react-md-editor?embed=1&file=pages/index.js&hideExplorer=1&hideNavigation=1&theme=dark) -[![#52](https://img.shields.io/github/issues/detail/state/uiwjs/react-md-editor/52)](https://github.com/uiwjs/react-md-editor/issues/52#issuecomment-848969341) -[![#224](https://img.shields.io/github/issues/detail/state/uiwjs/react-md-editor/224)](https://github.com/uiwjs/react-md-editor/issues/224#issuecomment-901112079) +Install both packages so their published styles can be imported directly: ```bash -npm install next-remove-imports -npm install @uiw/react-md-editor@v3.6.0 +npm install @uiw/react-md-editor @uiw/react-markdown-preview ``` -```js -// next.config.js -const removeImports = require('next-remove-imports')(); -module.exports = removeImports({}); +Load global styles from `app/layout.tsx`: + +```tsx +import '@uiw/react-md-editor/markdown-editor.css'; +import '@uiw/react-markdown-preview/markdown.css'; ``` -```jsx -import "@uiw/react-md-editor/markdown-editor.css"; -import "@uiw/react-markdown-preview/markdown.css"; -import dynamic from "next/dynamic"; -import { useState } from "react"; +In a client component, load the editor without server-side rendering: -import * as commands from "@uiw/react-md-editor/commands" +```tsx +'use client'; +import dynamic from 'next/dynamic'; +import { useState } from 'react'; -const MDEditor = dynamic( - () => import("@uiw/react-md-editor"), - { ssr: false } -); +const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false }); -function HomePage() { - const [value, setValue] = useState("**Hello world!!!**"); - return ( -
- -
- ); +export default function Page() { + const [value, setValue] = useState(['```js', 'const answer = 42;', '```'].join('\n')); + return ; } - -export default HomePage; ``` +If highlighting is missing with older Turbopack versions, try `next dev` without `--turbopack` + ### Support dark-mode/night-mode By default, the [`dark-mode`](https://github.com/jaywcjlove/dark-mode/) is automatically switched according to the system. If you need to switch manually, just set the `data-color-mode="dark"` parameter for body. diff --git a/example/nextjs/README.md b/example/nextjs/README.md index e215bc4cc..49c3e4940 100644 --- a/example/nextjs/README.md +++ b/example/nextjs/README.md @@ -1,36 +1,11 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Next.js App Router example -## Getting Started +Run `npm install` from the repository root, then `npm run dev --workspace nextjs` -First, run the development server: +The page shows a JavaScript code block in the live preview, with syntax highlighting from `@uiw/react-markdown-preview` -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +The global editor and preview styles are imported by `src/app/layout.tsx`, not by the client component -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +The editor in `src/app/page.tsx` uses `next/dynamic` with `ssr: false` because the editor needs browser APIs -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/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## 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/app/building-your-application/deploying) for more details. +This example uses Webpack for `next dev`; earlier Turbopack versions could leave syntax highlighting unstyled (see [#682](https://github.com/uiwjs/react-md-editor/issues/682)) diff --git a/example/nextjs/package.json b/example/nextjs/package.json index df5fd5acb..ed70acccc 100644 --- a/example/nextjs/package.json +++ b/example/nextjs/package.json @@ -3,12 +3,14 @@ "version": "4.2.2", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { + "@uiw/react-md-editor": "^4.2.2", + "@uiw/react-markdown-preview": "^5.2.0", "next": "15.3.1", "react": "^18.0.0", "react-dom": "^18.0.0" diff --git a/example/nextjs/src/app/layout.tsx b/example/nextjs/src/app/layout.tsx index d3ef21d11..2853da2c6 100644 --- a/example/nextjs/src/app/layout.tsx +++ b/example/nextjs/src/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from 'next'; import { Geist, Geist_Mono } from 'next/font/google'; import './globals.css'; +import '@uiw/react-md-editor/markdown-editor.css'; +import '@uiw/react-markdown-preview/markdown.css'; const geistSans = Geist({ variable: '--font-geist-sans', diff --git a/example/nextjs/src/app/page.tsx b/example/nextjs/src/app/page.tsx index 8386d7c09..2bd21538b 100644 --- a/example/nextjs/src/app/page.tsx +++ b/example/nextjs/src/app/page.tsx @@ -6,8 +6,17 @@ import type { ContextStore } from '@uiw/react-md-editor'; const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false }); type OnChange = (value?: string, event?: React.ChangeEvent, state?: ContextStore) => void; +const initialMarkdown = [ + '# Syntax highlighting', + '', + '```js', + 'const answer = 42;', + 'console.log(answer);', + '```', +].join('\n'); + export default function Home() { - const [value, setValue] = React.useState('**Hello world!!!**'); + const [value, setValue] = React.useState(initialMarkdown); const onChange = React.useCallback((val) => { setValue(val || ''); diff --git a/package.json b/package.json index b1d7095c6..5e7a1c249 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "remove": "npm run clean && lerna exec \"rm -rf package-lock.json\" --scope react-code-preview-layout --scope website", "clean": "lerna clean --yes", - "test:consumers": "node --test test/package-exports.test.mjs && node test/package-consumers.mjs" + "test:consumers": "node --test test/package-exports.test.mjs && node test/package-consumers.mjs", + "test:nextjs": "node --test test/nextjs-styles.test.mjs" }, "repository": { "type": "git", diff --git a/test/nextjs-highlight.test.tsx b/test/nextjs-highlight.test.tsx new file mode 100644 index 000000000..c59e45dc0 --- /dev/null +++ b/test/nextjs-highlight.test.tsx @@ -0,0 +1,22 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import MDEditor from '../core/src'; +import MDEditorCommon from '../core/src/index.common'; + +const source = ['# Syntax highlighting', '', '```js', 'const answer = 42;', 'console.log(answer);', '```'].join('\n'); + +describe.each([ + ['default', MDEditor], + ['common', MDEditorCommon], +] as const)('%s fenced code preview', (_mode, Editor) => { + it('keeps JavaScript tokens in the preview', () => { + const { container } = render(); + const code = container.querySelector('.w-md-editor-preview code.language-js'); + expect(code).not.toBeNull(); + expect(code?.querySelector('.token.keyword')?.textContent).toBe('const'); + expect(code?.querySelector('.token.function')?.textContent).toBe('log'); + }); +}); diff --git a/test/nextjs-styles.test.mjs b/test/nextjs-styles.test.mjs new file mode 100644 index 000000000..1ca4714ea --- /dev/null +++ b/test/nextjs-styles.test.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { readdir, readFile } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; +import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const staticDir = join(root, 'example/nextjs/.next/static'); + +async function findCss(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = await Promise.all( + entries.map(async (entry) => { + const path = join(directory, entry.name); + if (entry.isDirectory()) return findCss(path); + return entry.isFile() && entry.name.endsWith('.css') ? [path] : []; + }), + ); + return files.flat(); +} + +test('Next.js build includes the editor and syntax highlighting styles', async () => { + const files = await findCss(staticDir); + assert.ok(files.length > 0, 'Next.js did not emit CSS'); + const css = (await Promise.all(files.map((file) => readFile(file, 'utf8')))).join('\n'); + + assert.match(css, /\.w-md-editor\b/, 'editor layout CSS missing'); + assert.match(css, /\.wmde-markdown\b/, 'markdown preview CSS missing'); + assert.match(css, /\.token\.keyword\b/, 'Prism highlighting CSS missing'); + assert.match(css, /--color-prettylights-syntax-keyword\b/, 'syntax colour variables missing'); +});