diff --git a/examples/full/.test-dev.test.ts b/examples/full/.test-dev.test.ts new file mode 100644 index 00000000..fbafdbbc --- /dev/null +++ b/examples/full/.test-dev.test.ts @@ -0,0 +1,2 @@ +import { testRun } from './.testRun' +testRun('pnpm run dev') diff --git a/examples/full/.test-preview.test.ts b/examples/full/.test-preview.test.ts new file mode 100644 index 00000000..6cd5bbe4 --- /dev/null +++ b/examples/full/.test-preview.test.ts @@ -0,0 +1,2 @@ +import { testRun } from './.testRun' +testRun('pnpm run preview') diff --git a/examples/full/.test.ts b/examples/full/.testRun.ts similarity index 52% rename from examples/full/.test.ts rename to examples/full/.testRun.ts index 3e5206a3..4e66d701 100644 --- a/examples/full/.test.ts +++ b/examples/full/.testRun.ts @@ -1,16 +1,19 @@ +export { testRun } + import { test, expect, run, fetchHtml, page, getServerUrl, autoRetry, partRegex } from '@brillout/test-e2e' -runTest() +let isProd: boolean + +function testRun(cmd: `pnpm run ${'dev' | 'preview'}`) { + run(cmd) -function runTest() { - run('pnpm run dev') + isProd = cmd !== 'pnpm run dev' - const textLandingPage = 'Rendered to HTML.' const title = 'My Vike + React App' testUrl({ url: '/', title, - text: textLandingPage, + text: 'Rendered to HTML.', counter: true }) @@ -34,63 +37,86 @@ function runTest() { counter: true }) - { - const url = '/without-ssr' - const textNoSSR = 'This page is rendered only in the browser' - const text = textNoSSR - test(url + ' (HTML)', async () => { - const html = await fetchHtml(url) - // Isn't rendered to HTML - expect(html).toContain('
') - expect(html).not.toContain(text) - expect(getTitle(html)).toBe(title) - }) - test(url + ' (Hydration)', async () => { - await page.goto(getServerUrl() + url) - await testCounter() - const body = await page.textContent('body') - expect(body).toContain(text) - }) - test('Switch between SSR and non-SSR page', async () => { - let body: string | null - const t1 = textNoSSR - const t2 = textLandingPage - - body = await page.textContent('body') - expect(body).toContain(t1) - expect(body).not.toContain(t2) - ensureWasClientSideRouted('/pages/without-ssr') - - await page.click('a:has-text("Welcome")') - await testCounter() - body = await page.textContent('body') - expect(body).toContain(t2) - expect(body).not.toContain(t1) - ensureWasClientSideRouted('/pages/without-ssr') + const textNoSSR = 'This page is rendered only in the browser' + testUrl({ + url: '/without-ssr', + title: 'No SSR', + text: textNoSSR, + counter: true, + noSSR: true + }) - await page.click('a:has-text("Without SSR")') - await testCounter() - body = await page.textContent('body') - expect(body).toContain(t1) - expect(body).not.toContain(t2) - ensureWasClientSideRouted('/pages/without-ssr') - }) - } + testNavigationBetweenWithSSRAndWithoutSSR() } -function testUrl({ url, title, text, counter }: { url: string; title: string; text: string; counter?: true }) { +function testNavigationBetweenWithSSRAndWithoutSSR() { + const textWithSSR = 'Rendered to HTML.' + const textWithoutSSR = "It isn't rendered to HTML" + + const url = '/without-ssr' + test(url + " isn't rendered to HTML", async () => { + const html = await fetchHtml(url) + expect(html).toContain('
') + expect(html).not.toContain(textWithoutSSR) + await page.goto(getServerUrl() + url) + await testCounter() + const body = await page.textContent('body') + expect(body).toContain(textWithoutSSR) + }) + + test('Switch between SSR and non-SSR page', async () => { + let body: string | null + const t1 = textWithoutSSR + const t2 = textWithSSR + + body = await page.textContent('body') + expect(body).toContain(t1) + expect(body).not.toContain(t2) + ensureWasClientSideRouted('/pages/without-ssr') + + await page.click('a:has-text("Welcome")') + await testCounter() + body = await page.textContent('body') + expect(body).toContain(t2) + expect(body).not.toContain(t1) + ensureWasClientSideRouted('/pages/without-ssr') + + await page.click('a:has-text("Without SSR")') + await testCounter() + body = await page.textContent('body') + expect(body).toContain(t1) + expect(body).not.toContain(t2) + ensureWasClientSideRouted('/pages/without-ssr') + }) +} + +function testUrl({ + url, + title, + text, + counter, + noSSR +}: { url: string; title: string; text: string; counter?: true; noSSR?: true }) { test(url + ' (HTML)', async () => { const html = await fetchHtml(url) - expect(html).toContain(text) + if (!noSSR) { + expect(html).toContain(text) + } expect(getTitle(html)).toBe(title) + const hash = /[a-zA-Z0-9_-]+/ + if (!isProd) { + expect(html).toMatch(partRegex``) + } else { + expect(html).toMatch(partRegex``) + } }) test(url + ' (Hydration)', async () => { await page.goto(getServerUrl() + url) - const body = await page.textContent('body') - expect(body).toContain(text) if (counter) { await testCounter() } + const body = await page.textContent('body') + expect(body).toContain(text) }) } diff --git a/examples/full/pages/streaming/+Page.tsx b/examples/full/pages/streaming/+Page.tsx index 6dc6d7cd..4e949b8a 100644 --- a/examples/full/pages/streaming/+Page.tsx +++ b/examples/full/pages/streaming/+Page.tsx @@ -25,7 +25,7 @@ function MovieList() { const movies = useAsync(['star-wars-movies'], async () => { const response = await fetch('https://star-wars.brillout.com/api/films.json') // Simulate slow network - await new Promise((r) => setTimeout(r, 2 * 1000)) + await new Promise((r) => setTimeout(r, 3 * 1000)) const movies: Movie[] = (await response.json()).results return movies }) diff --git a/examples/full/pages/without-ssr/+config.ts b/examples/full/pages/without-ssr/+config.ts index f9ec2d6a..16c70ed4 100644 --- a/examples/full/pages/without-ssr/+config.ts +++ b/examples/full/pages/without-ssr/+config.ts @@ -2,5 +2,6 @@ import type { Config } from 'vike/types' export default { // https://vike.dev/ssr - ssr: false + ssr: false, + title: 'No SSR' } satisfies Config diff --git a/packages/vike-react-query/package.json b/packages/vike-react-query/package.json index f3198939..382c1ba0 100644 --- a/packages/vike-react-query/package.json +++ b/packages/vike-react-query/package.json @@ -12,36 +12,37 @@ }, "scripts": { "dev": "tsc --watch", - "build": "rm -rf dist/ && tsc", + "build": "rimraf dist/ && tsc", "release": "release-me patch", "release:minor": "release-me minor", "release:commit": "release-me commit", "test": "vitest run" }, "peerDependencies": { + "@tanstack/react-query": "5.x.x", "react": "18.x.x", "react-dom": "18.x.x", + "react-streaming": "^0.3.19", "vike": "^0.4.160", "vike-react": "^0.4.4", - "react-streaming": "^0.3.19", - "vite": "^4.3.8 || ^5.0.10", - "@tanstack/react-query": "5.x.x" + "vite": "^4.3.8 || ^5.0.10" }, "devDependencies": { "@brillout/release-me": "^0.3.4", + "@tanstack/react-query": "^5.20.1", + "@testing-library/react": "^14.2.1", "@types/node": "^20.11.17", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", + "jsdom": "^24.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-streaming": "^0.3.27", + "rimraf": "^5.0.5", "typescript": "^5.3.3", "vike": "^0.4.168", "vike-react": "^0.4.8", - "react-streaming": "^0.3.27", - "vitest": "^1.2.2", - "@testing-library/react": "^14.2.1", - "jsdom": "^24.0.0", - "@tanstack/react-query": "^5.20.1" + "vitest": "^1.2.2" }, "dependencies": { "devalue": "^4.3.2", diff --git a/packages/vike-react/package.json b/packages/vike-react/package.json index b5cb6870..fbddecdf 100644 --- a/packages/vike-react/package.json +++ b/packages/vike-react/package.json @@ -15,7 +15,7 @@ }, "scripts": { "dev": "tsc --watch", - "build": "rm -rf dist/ && tsc", + "build": "rimraf dist/ && tsc", "release": "release-me patch", "release:minor": "release-me minor", "release:commit": "release-me commit" @@ -34,6 +34,7 @@ "@types/react-dom": "^18.2.19", "react": "^18.2.0", "react-dom": "^18.2.0", + "rimraf": "^5.0.5", "typescript": "^5.3.3", "vike": "^0.4.168" }, diff --git a/packages/vike-react/src/+config.ts b/packages/vike-react/src/+config.ts index 21ddde42..fe544066 100644 --- a/packages/vike-react/src/+config.ts +++ b/packages/vike-react/src/+config.ts @@ -49,6 +49,9 @@ export default { _streamIsRequied: { env: { server: true } }, + onBeforeRenderClient: { + env: { client: true } + }, onAfterRenderClient: { env: { client: true } }, diff --git a/packages/vike-react/src/renderer/onRenderClient.tsx b/packages/vike-react/src/renderer/onRenderClient.tsx index fbee996b..1e9fc993 100644 --- a/packages/vike-react/src/renderer/onRenderClient.tsx +++ b/packages/vike-react/src/renderer/onRenderClient.tsx @@ -8,6 +8,10 @@ import { getPageElement } from './getPageElement.js' let root: ReactDOM.Root const onRenderClient: OnRenderClientSync = (pageContext): ReturnType => { + // Use case: + // - Store hydration https://github.com/vikejs/vike-react/issues/110 + pageContext.config.onBeforeRenderClient?.(pageContext) + const page = getPageElement(pageContext) // TODO: implement this? So that, upon errors, onRenderClient() throws an error and Vike can render the error. As of April 2024 it isn't released yet. @@ -16,8 +20,13 @@ const onRenderClient: OnRenderClientSync = (pageContext): ReturnType {} const container = document.getElementById('react-root')! - if (container.innerHTML !== '' && pageContext.isHydration) { - // First render (hydration) + if ( + // Whether the page was rendered to HTML. (I.e. whether the user set the [`ssr`](https://vike.dev/ssr) setting to `false`.) + container.innerHTML !== '' && + // Whether the page was already rendered to HTML. (I.e. whether this is the first client-side rendering.) + pageContext.isHydration + ) { + // Hydration root = ReactDOM.hydrateRoot(container, page, { // @ts-expect-error onUncaughtError @@ -48,6 +57,9 @@ const onRenderClient: OnRenderClientSync = (pageContext): ReturnType void - // https://github.com/vikejs/vike-react/pull/96 + /** + * Client-side hook called after the page is rendered. + */ onAfterRenderClient?: (pageContext: PageContextClient) => void // Temporary until Wrapper is cumulative diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8bf5a0a..ce694bfc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,6 +190,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + rimraf: + specifier: ^5.0.5 + version: 5.0.5 typescript: specifier: ^5.3.3 version: 5.3.3 @@ -239,6 +242,9 @@ importers: react-streaming: specifier: ^0.3.27 version: 0.3.27(react-dom@18.2.0)(react@18.2.0) + rimraf: + specifier: ^5.0.5 + version: 5.0.5 typescript: specifier: ^5.3.3 version: 5.3.3 @@ -1273,6 +1279,18 @@ packages: engines: {node: '>=10.13.0'} dev: true + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1327,6 +1345,13 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + /@polka/url@1.0.0-next.23: resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==} @@ -1629,6 +1654,11 @@ packages: engines: {node: '>=8'} dev: true + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -1647,6 +1677,11 @@ packages: engines: {node: '>=10'} dev: true + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + /aria-query@5.1.3: resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} dependencies: @@ -1677,6 +1712,16 @@ packages: engines: {node: '>= 0.4'} dev: true + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} @@ -2034,10 +2079,22 @@ packages: is-obj: 2.0.0 dev: true + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /electron-to-chromium@1.4.616: resolution: {integrity: sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==} dev: false + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2243,6 +2300,14 @@ packages: is-callable: 1.2.7 dev: true + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -2335,6 +2400,18 @@ packages: dependencies: is-glob: 4.0.3 + /glob@10.3.12: + resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.4 + minipass: 7.1.0 + path-scurry: 1.10.2 + dev: true + /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -2521,6 +2598,11 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2630,6 +2712,15 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2742,6 +2833,11 @@ packages: semver: 7.5.4 dev: true + /lru-cache@10.2.2: + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} + engines: {node: 14 || >=16.14} + dev: true + /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -2809,10 +2905,22 @@ packages: engines: {node: '>=12'} dev: true + /minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true + /minipass@7.1.0: + resolution: {integrity: sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + /mlly@1.4.2: resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} dependencies: @@ -2990,6 +3098,14 @@ packages: engines: {node: '>=12'} dev: true + /path-scurry@1.10.2: + resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.2.2 + minipass: 7.1.0 + dev: true + /pathe@1.1.1: resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} dev: true @@ -3196,6 +3312,14 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + /rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 10.3.12 + dev: true + /rollup@3.29.4: resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -3377,6 +3501,24 @@ packages: internal-slot: 1.0.6 dev: true + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3384,6 +3526,13 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -3881,6 +4030,24 @@ packages: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} dev: true + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + /ws@8.16.0: resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} engines: {node: '>=10.0.0'}