diff --git a/AGENTS.md b/AGENTS.md index c8a641ee8..6cf0f16e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -136,6 +136,14 @@ test/ All commands are fast in this repository, but network issues or system load can cause delays. Always wait for completion. + + +## This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + + + ## Docs Infra Conventions Follow additional instructions when working in the `@mui/internal-docs-infra` (`packages/docs-infra`) package or `docs/app/docs-infra` docs: diff --git a/docs/.gitignore b/docs/.gitignore index 5ef6a5207..546c174dd 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -39,3 +39,11 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/docs/app/docs-infra/components/code-highlighter/demos/DemoContent.module.css b/docs/app/docs-infra/components/code-highlighter/demos/DemoContent.module.css index 189488d57..267a32a87 100644 --- a/docs/app/docs-infra/components/code-highlighter/demos/DemoContent.module.css +++ b/docs/app/docs-infra/components/code-highlighter/demos/DemoContent.module.css @@ -4,10 +4,14 @@ } .demoSection { - padding: 24px; + padding: 4px; border-radius: 7px 7px 0 0; } +.demoSection :global(.demo) { + padding: 20px; +} + .codeSection { border-top: 1px solid #d0cdd7; } diff --git a/docs/app/docs-infra/components/code-highlighter/demos/DemoContent.tsx b/docs/app/docs-infra/components/code-highlighter/demos/DemoContent.tsx index 294339889..b72dd427a 100644 --- a/docs/app/docs-infra/components/code-highlighter/demos/DemoContent.tsx +++ b/docs/app/docs-infra/components/code-highlighter/demos/DemoContent.tsx @@ -45,7 +45,9 @@ export function DemoContent(props: ContentProps) { ))}
-
{demo.component}
+
+
{demo.component}
+
diff --git a/docs/app/docs-infra/components/code-highlighter/demos/demo-variants/page.tsx b/docs/app/docs-infra/components/code-highlighter/demos/demo-variants/page.tsx new file mode 100644 index 000000000..dc89f2589 --- /dev/null +++ b/docs/app/docs-infra/components/code-highlighter/demos/demo-variants/page.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +import { DemoCodeHighlighterDemoVariants } from '.'; + +export default function Page() { + return ; +} diff --git a/docs/app/docs-infra/components/code-highlighter/demos/demo-variants/test.ts b/docs/app/docs-infra/components/code-highlighter/demos/demo-variants/test.ts new file mode 100644 index 000000000..1c10fa8ae --- /dev/null +++ b/docs/app/docs-infra/components/code-highlighter/demos/demo-variants/test.ts @@ -0,0 +1,20 @@ +import path from 'node:path'; +import { test } from '@playwright/test'; + +const route = path + .dirname(import.meta.filename) + .split('/app') + .pop()!; + +test('code highlighter with multiple variants', async ({ page }) => { + await page.goto(route); + await page.waitForLoadState('networkidle'); + + await page.getByRole('combobox').click(); + + await page + .locator('.demo') + .first() + /* file://./../../../../../../public/docs-infra/components/code-highlighter/demos/demo-variants.png */ + .screenshot({ path: `public/${route}.png` }); +}); diff --git a/docs/app/docs-infra/components/code-highlighter/page.mdx b/docs/app/docs-infra/components/code-highlighter/page.mdx index 4123893cf..66eb2d384 100644 --- a/docs/app/docs-infra/components/code-highlighter/page.mdx +++ b/docs/app/docs-infra/components/code-highlighter/page.mdx @@ -469,3 +469,17 @@ These types are available by importing from `@mui/internal-docs-infra/CodeHighli [See Types](./types.md#CodeHighlighterTypes) + +export const metadata = + /** @type {import('@mui/internal-docs-infra/createSitemap/types').NextMetadata} */ ({ + keywords: [], + openGraph: { + images: [ + { + /* file://./../../../../public/docs-infra/components/code-highlighter/demos/demo-variants.png */ + url: '/docs-infra/components/code-highlighter/demos/demo-variants.png', + alt: 'Highlighted Code with Variants', + }, + ], + }, + }); diff --git a/docs/app/docs-infra/components/page.mdx b/docs/app/docs-infra/components/page.mdx index 078df971a..3f738b3d8 100644 --- a/docs/app/docs-infra/components/page.mdx +++ b/docs/app/docs-infra/components/page.mdx @@ -16,6 +16,8 @@ The `CodeHighlighter` component provides a powerful and flexible way to display interactive code examples with syntax highlighting, multiple variants, and live previews. It supports both static code blocks and interactive demos with component previews. +![Highlighted Code with Variants](../../../public/docs-infra/components/code-highlighter/demos/demo-variants.png) +
Outline diff --git a/docs/components/PagesIndex/PagesIndex.module.css b/docs/components/PagesIndex/PagesIndex.module.css index bbb322c43..84f6df592 100644 --- a/docs/components/PagesIndex/PagesIndex.module.css +++ b/docs/components/PagesIndex/PagesIndex.module.css @@ -1,20 +1,25 @@ .pages { - display: flex; - flex-direction: row; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(2, 1fr); gap: 12px; } +@media (max-width: 768px) { + .pages { + grid-template-columns: 1fr; + } +} + .page { display: flex; flex-direction: column; border: 1px solid #d0cdd7; - max-width: 222px; padding: 16px; border-radius: 12px; background-color: transparent; transition: all 0.2s ease; text-decoration: none; + overflow: hidden; } .page:hover { @@ -26,6 +31,14 @@ background-color: #20003820; } +.pageImage { + width: 100%; + height: auto; + border-radius: 6px; + margin-bottom: 12px; + object-fit: cover; +} + .pageTitle { font-weight: bold; display: flex; diff --git a/docs/components/PagesIndex/PagesIndex.tsx b/docs/components/PagesIndex/PagesIndex.tsx index 60252b7f8..449313d15 100644 --- a/docs/components/PagesIndex/PagesIndex.tsx +++ b/docs/components/PagesIndex/PagesIndex.tsx @@ -1,5 +1,6 @@ import type { SitemapSectionData } from '@mui/internal-docs-infra/createSitemap/types'; import * as React from 'react'; +import Image from 'next/image'; import Link from 'next/link'; import styles from './PagesIndex.module.css'; @@ -13,6 +14,15 @@ export function PagesIndex({ data }: { data?: SitemapSectionData }) { href={`${data.prefix}${page.path.replace(/^\.\//, '').replace(/\/page\.mdx$/, '')}`} className={styles.page} > + {page.image && ( + {page.image.alt + )}
{page.title} diff --git a/docs/next.config.mjs b/docs/next.config.mjs index b2b9af7d4..3c431cfd1 100644 --- a/docs/next.config.mjs +++ b/docs/next.config.mjs @@ -41,6 +41,7 @@ const nextConfig = { devIndicators: { position: 'bottom-right', }, + images: { unoptimized: true }, experimental: { turbopackFileSystemCacheForBuild: true, }, diff --git a/docs/package.json b/docs/package.json index a0d2e2824..c08e6e2a5 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,5 +1,6 @@ { "name": "docs", + "type": "module", "private": true, "scripts": { "dev": "next dev", @@ -8,17 +9,20 @@ "link-check": "tsx ./scripts/reportBrokenLinks.mts", "start": "serve ./export", "docs-infra": "docs-infra", - "validate": "docs-infra validate --command 'pnpm validate'" + "validate": "docs-infra validate --command 'pnpm validate'", + "screenshot": "docs-infra browser --script screenshot:unconfined --playwright-version 1.59.1 --", + "screenshot:unconfined": "playwright test", + "screenshot:codegen": "playwright codegen" }, "dependencies": { "@base-ui/react": "1.3.0", "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", "@mui/internal-docs-infra": "workspace:^", - "@next/mdx": "^16.1.6", + "@next/mdx": "^16.2.1", "@types/mdx": "^2.0.13", "lucide-react": "^0.577.0", - "next": "^16.1.6", + "next": "^16.2.1", "react": "^19.2.4", "react-dom": "^19.2.4", "react-runner": "^1.0.5", @@ -30,7 +34,8 @@ "web-vitals": "^5.1.0" }, "devDependencies": { - "@next/bundle-analyzer": "16.1.6", + "@next/bundle-analyzer": "16.2.1", + "@playwright/test": "1.59.1", "@types/node": "22.19.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", diff --git a/docs/playwright.config.ts b/docs/playwright.config.ts new file mode 100644 index 000000000..af4431983 --- /dev/null +++ b/docs/playwright.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './app', + testMatch: '**/test.ts', + forbidOnly: !!process.env.CI, + reporter: 'html', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + viewport: { width: 2560, height: 1440 }, + deviceScaleFactor: 3, + connectOptions: process.env.PLAYWRIGHT_SERVER + ? { wsEndpoint: process.env.PLAYWRIGHT_SERVER } + : undefined, + }, + webServer: { + command: 'pnpm build && pnpm start', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, +}); diff --git a/docs/public/docs-infra/components/code-highlighter/demos/demo-variants.png b/docs/public/docs-infra/components/code-highlighter/demos/demo-variants.png new file mode 100644 index 000000000..6ebd85e9d Binary files /dev/null and b/docs/public/docs-infra/components/code-highlighter/demos/demo-variants.png differ diff --git a/package.json b/package.json index e83ff9d99..c9588cca3 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "docs:start": "pnpm -F \"./docs\" run start", "docs:infra": "pnpm -F \"./docs\" run docs-infra", "docs:validate": "pnpm -F \"./docs\" run docs-infra validate --command 'pnpm docs:validate'", + "docs:screenshot": "pnpm -F \"./docs\" run screenshot", "docs:lint": "pnpm -F \"./docs\" run lint", "docs:lib": "pnpm -F \"./packages/docs-infra\" run build", "test:benchmark": "pnpm -F ./test/performance benchmark" diff --git a/packages/docs-infra/src/pipeline/parseSource/addLineGutters.test.ts b/packages/docs-infra/src/pipeline/parseSource/addLineGutters.test.ts index f1d8e31fb..3ceaca22c 100644 --- a/packages/docs-infra/src/pipeline/parseSource/addLineGutters.test.ts +++ b/packages/docs-infra/src/pipeline/parseSource/addLineGutters.test.ts @@ -229,6 +229,33 @@ describe('starryNightGutter', () => { ]); }); + it('preserves frame boundary newlines in dataAsString for non-final frames', () => { + const tree: Root = { + type: 'root', + children: [ + { + type: 'text', + value: 'line1\nline2\nline3', + }, + ], + }; + + starryNightGutter(tree, ['line1', 'line2', 'line3'], 2); + + expect(tree.children).toHaveLength(2); + + const firstFrame = tree.children[0]; + const secondFrame = tree.children[1]; + + expect(firstFrame.type).toBe('element'); + expect(secondFrame.type).toBe('element'); + + if (firstFrame.type === 'element' && secondFrame.type === 'element') { + expect(firstFrame.properties?.dataAsString).toBe('line1\nline2\n'); + expect(secondFrame.properties?.dataAsString).toBe('line3'); + } + }); + it('should handle multiple consecutive empty lines', () => { const tree: Root = { type: 'root', diff --git a/packages/docs-infra/src/pipeline/parseSource/addLineGutters.ts b/packages/docs-infra/src/pipeline/parseSource/addLineGutters.ts index 8719d2f7f..e76a9a655 100644 --- a/packages/docs-infra/src/pipeline/parseSource/addLineGutters.ts +++ b/packages/docs-infra/src/pipeline/parseSource/addLineGutters.ts @@ -155,7 +155,9 @@ export function starryNightGutter( if (lineChildren.length > 0) { const startLine = Number(lineChildren[0].properties.dataLn) - 1; const endLine = Number(lineChildren[lineChildren.length - 1].properties.dataLn); - frame.properties.dataAsString = sourceLines.slice(startLine, endLine).join('\n'); + const hasFollowingLine = endLine < sourceLines.length; + frame.properties.dataAsString = + sourceLines.slice(startLine, endLine).join('\n') + (hasFollowingLine ? '\n' : ''); } } } diff --git a/packages/docs-infra/src/pipeline/syncPageIndex/metadataToMarkdown.test.ts b/packages/docs-infra/src/pipeline/syncPageIndex/metadataToMarkdown.test.ts index 5ed48c42d..f15cccc8b 100644 --- a/packages/docs-infra/src/pipeline/syncPageIndex/metadataToMarkdown.test.ts +++ b/packages/docs-infra/src/pipeline/syncPageIndex/metadataToMarkdown.test.ts @@ -135,6 +135,78 @@ describe('metadataToMarkdown', () => { expect(result).toContain('A button component.'); }); + it('should rewrite absolute image URLs to relative public paths when path is provided', () => { + const data: PagesMetadata = { + title: 'Components', + pages: [ + { + slug: 'button', + path: './button/page.mdx', + title: 'Button', + description: 'A button component.', + image: { + url: '/docs-infra/components/button/screenshot.png', + alt: 'Button screenshot', + }, + }, + ], + }; + + const result = metadataToMarkdown(data, { + path: 'app/docs-infra/components/page.mdx', + }); + + expect(result).toContain( + '![Button screenshot](../../../public/docs-infra/components/button/screenshot.png)', + ); + }); + + it('should not rewrite non-absolute image URLs when path is provided', () => { + const data: PagesMetadata = { + title: 'Components', + pages: [ + { + slug: 'button', + path: './button/page.mdx', + title: 'Button', + description: 'A button component.', + image: { + url: 'https://example.com/button.png', + alt: 'Button image', + }, + }, + ], + }; + + const result = metadataToMarkdown(data, { + path: 'app/docs-infra/components/page.mdx', + }); + + expect(result).toContain('![Button image](https://example.com/button.png)'); + }); + + it('should not rewrite absolute image URLs when path is not provided', () => { + const data: PagesMetadata = { + title: 'Components', + pages: [ + { + slug: 'button', + path: './button/page.mdx', + title: 'Button', + description: 'A button component.', + image: { + url: '/docs-infra/components/button/screenshot.png', + alt: 'Button screenshot', + }, + }, + ], + }; + + const result = metadataToMarkdown(data); + + expect(result).toContain('![Button screenshot](/docs-infra/components/button/screenshot.png)'); + }); + it('should handle pages without keywords', () => { const data: PagesMetadata = { title: 'Components', @@ -357,6 +429,40 @@ describe('metadataToMarkdownAst', () => { expect(imageNode.children[0].alt).toBe('Button image'); }); + it('should rewrite absolute image URLs to relative public paths in AST when path is provided', () => { + const data: PagesMetadata = { + title: 'Components', + pages: [ + { + slug: 'button', + path: './button/page.mdx', + title: 'Button', + description: 'A button component.', + image: { + url: '/docs-infra/components/button/screenshot.png', + alt: 'Button screenshot', + }, + }, + ], + }; + + const result = metadataToMarkdownAst(data, { + path: 'app/docs-infra/components/page.mdx', + }); + + const imageNode = result.children.find( + (child: any) => + child.type === 'paragraph' && + child.children && + child.children.some((c: any) => c.type === 'image'), + ) as any; + + expect(imageNode).toBeDefined(); + expect(imageNode.children[0].url).toBe( + '../../../public/docs-infra/components/button/screenshot.png', + ); + }); + it('should include keywords in AST when provided', () => { const data: PagesMetadata = { title: 'Components', @@ -436,6 +542,29 @@ A button component. expect(page?.image?.alt).toBe('A simple button'); }); + it('should convert relative public image URLs back to absolute paths', async () => { + const markdown = `# Components + +[//]: # 'This section is autogenerated, but the following list order, title, and [Tag]s can be modified, but nothing within the parentheses.' + +- Button - ([Outline](#button), [Contents](./button/page.mdx)) + +[//]: # 'This section is autogenerated, DO NOT EDIT AFTER THIS LINE' + +## Button + +A button component. + +![Button screenshot](../../../public/docs-infra/components/button/screenshot.png) + +[Read more](./button/page.mdx) +`; + + const result = await markdownToMetadata(markdown); + + expect(result?.pages[0]?.image?.url).toBe('/docs-infra/components/button/screenshot.png'); + }); + it('should parse multiple pages', async () => { const markdown = `# UI Components diff --git a/packages/docs-infra/src/pipeline/syncPageIndex/metadataToMarkdown.ts b/packages/docs-infra/src/pipeline/syncPageIndex/metadataToMarkdown.ts index b6753cf7c..dc991610c 100644 --- a/packages/docs-infra/src/pipeline/syncPageIndex/metadataToMarkdown.ts +++ b/packages/docs-infra/src/pipeline/syncPageIndex/metadataToMarkdown.ts @@ -6,6 +6,41 @@ import type { ExtractedMetadata, HeadingHierarchy } from '../transformMarkdownMe import { heading, paragraph, text, link, comment } from './createMarkdownNodes'; import { Audience } from '../../createSitemap/types'; +/** + * Converts an absolute image URL (starting with `/`) to a relative path pointing + * to the `public/` directory that is a sibling of `app/`. + * This makes images visible when viewing the markdown in editors or GitHub. + * + * @param imageUrl - The image URL to convert + * @param indexFilePath - The relative path of the index file (e.g., `app/docs-infra/components/page.mdx`) + * @returns The relative path to the image in the `public/` directory, or the original URL if not applicable + */ +function toRelativePublicUrl(imageUrl: string, indexFilePath: string | undefined): string { + if (!indexFilePath || !imageUrl.startsWith('/')) { + return imageUrl; + } + const dirParts = indexFilePath.split('/').filter(Boolean); + // Remove the filename to get just the directory parts + dirParts.pop(); + const prefix = '../'.repeat(dirParts.length); + return `${prefix}public${imageUrl}`; +} + +/** + * Converts a relative `public/` image URL back to an absolute path. + * This reverses the transformation done by `toRelativePublicUrl`. + * + * @param imageUrl - The image URL that may be a relative public path + * @returns The absolute path if the URL matches the relative public pattern, or the original URL + */ +function fromRelativePublicUrl(imageUrl: string): string { + const match = imageUrl.match(/^(?:\.\.\/)+public\/(.+)$/); + if (match) { + return `/${match[1]}`; + } + return imageUrl; +} + /** * Escapes underscores in a string for markdown compatibility. * Prevents underscores from being interpreted as emphasis markers. @@ -728,7 +763,7 @@ export function metadataToMarkdownAst( children: [ { type: 'image', - url: image.url, + url: toRelativePublicUrl(image.url, path), alt: image.alt || pageTitle, }, ], @@ -1160,7 +1195,7 @@ export function metadataToMarkdown( // Add image if available if (image) { - lines.push(`![${image.alt || pageTitle}](${image.url})`); + lines.push(`![${image.alt || pageTitle}](${toRelativePublicUrl(image.url, path)})`); lines.push(''); } @@ -1706,7 +1741,7 @@ export async function markdownToMetadata(markdown: string): Promise { + const { syncPageIndex } = await import('../syncPageIndex'); + const mockSyncPageIndex = vi.mocked(syncPageIndex); + mockSyncPageIndex.mockClear(); + + const input = `# Code Highlighter + +A powerful code highlighting component. + +## Features + +export const metadata = + /** @type {import('@mui/internal-docs-infra/createSitemap/types').NextMetadata} */ ({ + keywords: [], + openGraph: { + images: [{ url: '/docs-infra/components/code-highlighter/screenshot.png', alt: 'Code Highlighter' }], + } + });`; + + const processor = unified() + .use(remarkParse) + .use(remarkMdx) + .use(transformMarkdownMetadata, { + extractToIndex: { + include: ['app'], + exclude: [], + baseDir: '/test', + }, + }); + + const tree = processor.parse(input); + const file = { path: '/test/app/components/code-highlighter/page.mdx', value: input }; + await processor.run(tree, file as any); + + expect(mockSyncPageIndex).toHaveBeenCalledTimes(1); + const callArgs = mockSyncPageIndex.mock.calls[0][0]; + expect(callArgs.metadata?.image).toEqual({ + url: '/docs-infra/components/code-highlighter/screenshot.png', + alt: 'Code Highlighter', + }); + }); + + it('should throw when metadata has top-level image field', async () => { + const input = `# Code Highlighter + +A powerful code highlighting component. + +## Features + +export const metadata = { + image: { url: '/explicit-image.png', alt: 'Explicit' }, + openGraph: { + images: [{ url: '/og-image.png', alt: 'OG Image' }], + }, +};`; + + const processor = unified() + .use(remarkParse) + .use(remarkMdx) + .use(transformMarkdownMetadata, { + extractToIndex: { + include: ['app'], + exclude: [], + baseDir: '/test', + }, + }); + + const tree = processor.parse(input); + const file = { path: '/test/app/components/code-highlighter/page.mdx', value: input }; + + await expect(processor.run(tree, file as any)).rejects.toThrow( + 'Metadata "image" is not a valid Next.js metadata field', + ); + }); }); diff --git a/packages/docs-infra/src/pipeline/transformMarkdownMetadata/transformMarkdownMetadata.ts b/packages/docs-infra/src/pipeline/transformMarkdownMetadata/transformMarkdownMetadata.ts index 9437613f4..3f1d1a6ce 100644 --- a/packages/docs-infra/src/pipeline/transformMarkdownMetadata/transformMarkdownMetadata.ts +++ b/packages/docs-infra/src/pipeline/transformMarkdownMetadata/transformMarkdownMetadata.ts @@ -489,6 +489,37 @@ export const transformMarkdownMetadata: Plugin<[TransformMarkdownMetadataOptions ) { const extracted = parseMetadataFromEstree(node.data.estree as Program); if (extracted) { + // Extract image from openGraph.images (Next.js has no top-level image field) + const rawMetadata = extracted as Record; + + if (rawMetadata.image) { + throw new Error( + `Metadata "image" is not a valid Next.js metadata field. Use "openGraph.images" instead:\n` + + ` openGraph: {\n` + + ` images: [{ url: '...', alt: '...' }],\n` + + ` }`, + ); + } + + const openGraph = rawMetadata.openGraph as Record | undefined; + if (openGraph) { + const images = openGraph.images as Array | string> | undefined; + if (Array.isArray(images) && images.length > 0) { + const firstImage = images[0]; + if (typeof firstImage === 'string') { + extracted.image = { url: firstImage }; + } else if ( + typeof firstImage === 'object' && + firstImage !== null && + 'url' in firstImage + ) { + extracted.image = { + url: firstImage.url as string, + alt: (firstImage.alt as string) || undefined, + }; + } + } + } metadata = extracted; metadataNode = node; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f111595f..2eaacdd03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,7 +284,7 @@ importers: specifier: workspace:^ version: link:../packages/docs-infra/build '@next/mdx': - specifier: ^16.1.6 + specifier: ^16.2.1 version: 16.2.1(@mdx-js/loader@3.1.1(webpack@5.102.1(esbuild@0.27.1)(jiti@2.6.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)) '@types/mdx': specifier: ^2.0.13 @@ -293,7 +293,7 @@ importers: specifier: ^0.577.0 version: 0.577.0(react@19.2.4) next: - specifier: ^16.1.6 + specifier: ^16.2.1 version: 16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.4 @@ -324,8 +324,11 @@ importers: version: 5.1.0 devDependencies: '@next/bundle-analyzer': - specifier: 16.1.6 - version: 16.1.6 + specifier: 16.2.1 + version: 16.2.1 + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 '@types/node': specifier: 22.19.0 version: 22.19.0 @@ -4140,8 +4143,8 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/bundle-analyzer@16.1.6': - resolution: {integrity: sha512-ee2kagdTaeEWPlotgdTOqFHYcD3e2m2bbE3I9Rq2i6ABYi5OgopmtEUe8NM23viaYxLV2tDH/2nd5+qKoEr6cw==} + '@next/bundle-analyzer@16.2.1': + resolution: {integrity: sha512-fbj2WE6dnCyG8CvQnrBfpHyxdOIyZ4aEHJY0bSqAmamRiIXDqunFQPDvuSOPo24mJE9zQHw7TY6d+sGrXO98TQ==} '@next/env@16.2.3': resolution: {integrity: sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==} @@ -16612,7 +16615,7 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/bundle-analyzer@16.1.6': + '@next/bundle-analyzer@16.2.1': dependencies: webpack-bundle-analyzer: 4.10.1 transitivePeerDependencies: @@ -17091,7 +17094,6 @@ snapshots: '@playwright/test@1.59.1': dependencies: playwright: 1.59.1 - optional: true '@pnpm/config.env-replace@1.1.0': {} @@ -24853,8 +24855,7 @@ snapshots: playwright-core@1.58.2: {} - playwright-core@1.59.1: - optional: true + playwright-core@1.59.1: {} playwright@1.58.2: dependencies: @@ -24867,7 +24868,6 @@ snapshots: playwright-core: 1.59.1 optionalDependencies: fsevents: 2.3.2 - optional: true pluralize@3.1.0: {}