diff --git a/packages/nuxt-cli/package.json b/packages/nuxt-cli/package.json index 866fa8702..8b96c5fab 100644 --- a/packages/nuxt-cli/package.json +++ b/packages/nuxt-cli/package.json @@ -87,6 +87,7 @@ "@nuxt/kit": "^4.5.1", "@nuxt/schema": "^4.5.1", "@nuxt/test-utils": "^4.1.0", + "@speed-highlight/core": "^1.2.23", "@types/node": "^24.13.3", "giget": "^3.3.1", "h3": "^1.15.11", diff --git a/packages/nuxt-cli/src/commands/curl.ts b/packages/nuxt-cli/src/commands/curl.ts index 59db81ce6..97e0685e8 100644 --- a/packages/nuxt-cli/src/commands/curl.ts +++ b/packages/nuxt-cli/src/commands/curl.ts @@ -1,3 +1,5 @@ +import type { HighlightLanguage } from '../utils/highlight' + import { Buffer } from 'node:buffer' import { readFile } from 'node:fs/promises' import process from 'node:process' @@ -7,7 +9,8 @@ import { note } from '@clack/prompts' import { defineCommand } from 'citty' import { findDevServer, noDevServerMessage } from '../utils/dev-server' -import { highlightJson } from '../utils/json-highlight' +import { formatHtml } from '../utils/format-html' +import { highlight } from '../utils/highlight' import { logger } from '../utils/logger' import { logNetworkError } from '../utils/network' import { resolveRootDir } from '../utils/paths' @@ -15,7 +18,26 @@ import { rootDirArgs } from './_shared' const HAS_SCHEME_RE = /^[a-z][a-z\d+.-]*:\/\//i const JSON_CONTENT_TYPE_RE = /^application\/(?:[\w.+-]+\+)?json\b/i -const TEXT_CONTENT_TYPE_RE = /^(?:text\/|application\/(?:[\w.+-]+\+)?(?:json|xml|yaml)\b|application\/(?:javascript|ecmascript|x-www-form-urlencoded|x-ndjson)\b)/i +/** Markup reindented as HTML: the HTML rules cover XML documents too. */ +const MARKUP_CONTENT_TYPE_RE = /^(?:text\/(?:html|xml)|(?:application|image)\/(?:[\w.+-]+\+)?xml)\b/i + +/** Newline-delimited JSON keeps one record per line, so each line is highlighted on its own. */ +const NDJSON_CONTENT_TYPE_RE = /^application\/(?:(?:x-)?ndjson|jsonl)\b/i + +/** Languages worth highlighting a response body in, keyed by content type. */ +const CONTENT_TYPE_LANGUAGES: [RegExp, HighlightLanguage][] = [ + [/^text\/(?:x-)?markdown\b/i, 'md'], + [/^(?:text|application)\/(?:x-)?(?:[\w.+-]+\+)?ya?ml\b/i, 'yaml'], + [/^text\/css\b/i, 'css'], + [/^(?:text|application)\/(?:x-)?(?:java|ecma)script\b/i, 'js'], + [/^(?:text|application)\/(?:x-)?typescript\b/i, 'ts'], + [/^(?:text\/x-(?:diff|patch)|application\/x-patch)\b/i, 'diff'], + [/^(?:text\/x-(?:sh|shellscript)|application\/x-sh(?:ellscript)?)\b/i, 'bash'], + [/^(?:text|application)\/x-python\b/i, 'py'], + [/^message\/http\b/i, 'http'], +] +/** Textual types with no highlighter of their own. */ +const TEXT_CONTENT_TYPE_RE = /^(?:text\/|application\/x-www-form-urlencoded\b)/i const BINARY_SNIFF_BYTES = 4096 @@ -68,6 +90,10 @@ export default defineCommand({ alias: 'v', description: 'Print request and response headers to stderr', }, + pretty: { + type: 'boolean', + description: 'Reindent and syntax-highlight output (default: on for a terminal, off when piped). Use `--pretty`/`--no-pretty` to force reindenting.', + }, }, async run(ctx) { const cwd = resolveRootDir(ctx.args) @@ -123,15 +149,17 @@ export default defineCommand({ process.exit(1) } + const pretty = ctx.args.pretty ?? Boolean(process.stdout.isTTY) + if (ctx.args.verbose) { - process.stderr.write(formatResponseHead(response, '< ')) + process.stderr.write(formatResponseHead(response, '< ', process.stderr, ctx.args.pretty ?? Boolean(process.stderr.isTTY))) } if (ctx.args.include || ctx.args.head) { - process.stdout.write(formatResponseHead(response, '')) + process.stdout.write(formatResponseHead(response, '', process.stdout, pretty)) } - await writeResponseBody(response) + await writeResponseBody(response, pretty) if (!response.ok) { process.exit(HTTP_ERROR_EXIT_CODE) @@ -206,50 +234,84 @@ async function readRequestBody(data: string | undefined): Promise= 500) { + return 'red' + } + if (status >= 400) { + return 'yellow' + } + if (status >= 300) { + return 'cyan' + } + return 'green' +} + +function formatResponseHead(response: Response, prefix: string, stream: NodeJS.WriteStream, pretty: boolean): string { + const paint = (style: Parameters[0], text: string): string => + pretty ? styleText(style, text, { stream }) : text + const status = paint(statusStyle(response.status), `${response.status} ${response.statusText}`.trimEnd()) + let head = `${prefix}${paint('dim', 'HTTP/1.1')} ${status}\n` for (const [name, value] of response.headers) { - head += `${prefix}${name}: ${value}\n` + head += `${prefix}${paint('blue', name)}: ${value}\n` } return `${head}${prefix.trimEnd()}\n` } -async function writeResponseBody(response: Response): Promise { +async function writeResponseBody(response: Response, pretty: boolean): Promise { const contentType = response.headers.get('content-type') || '' const buffer = Buffer.from(await response.arrayBuffer()) if (!buffer.length) { return } - if (!process.stdout.isTTY) { + if (!pretty) { process.stdout.write(buffer) return } - if (isBinary(buffer, contentType)) { + const renderer = resolveRenderer(contentType) + + if (isBinary(buffer, contentType, renderer)) { note('Binary data not shown in terminal. Redirect the output to a file to save it.', 'Response body') return } - const text = buffer.toString('utf-8') - process.stdout.write(JSON_CONTENT_TYPE_RE.test(contentType) ? formatJson(text) : text) - if (!text.endsWith('\n')) { + const text = decodeBody(buffer, contentType) + const body = renderer?.(text) ?? text + process.stdout.write(body) + if (!body.endsWith('\n')) { process.stdout.write('\n') } } +/** Decode with the charset the response declares, falling back to UTF-8 when it is absent or unknown. */ +function decodeBody(buffer: Buffer, contentType: string): string { + const charset = /;\s*charset=["']?([\w-]+)/i.exec(contentType)?.[1] + if (charset) { + try { + return new TextDecoder(charset).decode(buffer) + } + catch { + // fall through to UTF-8 + } + } + return buffer.toString('utf-8') +} + /** - * A textual content type is trusted outright; anything else is sniffed for a - * NUL byte, which no valid UTF-8 text response contains. + * A content type we can render, or one matching `TEXT_CONTENT_TYPE_RE`, is + * trusted outright; anything else is sniffed for a NUL byte, which no valid + * UTF-8 text response contains. */ -function isBinary(buffer: Buffer, contentType: string): boolean { - if (TEXT_CONTENT_TYPE_RE.test(contentType)) { +function isBinary(buffer: Buffer, contentType: string, renderer: ((text: string) => string) | undefined): boolean { + if (renderer || TEXT_CONTENT_TYPE_RE.test(contentType)) { return false } return buffer.subarray(0, BINARY_SNIFF_BYTES).includes(0) } -function formatJson(text: string): string { +function renderJson(text: string): string { let json: string try { json = JSON.stringify(JSON.parse(text), null, 2) @@ -257,8 +319,26 @@ function formatJson(text: string): string { catch { return text } + return highlight(json, 'json') +} - return highlightJson(json) +/** + * How to render a response body of `contentType`, or `undefined` to print it + * verbatim. Recognising a renderer is also what marks a type as text, so + * `isBinary` never sniffs something we already know how to highlight. + */ +function resolveRenderer(contentType: string): ((text: string) => string) | undefined { + if (JSON_CONTENT_TYPE_RE.test(contentType)) { + return renderJson + } + if (NDJSON_CONTENT_TYPE_RE.test(contentType)) { + return text => text.replace(/[^\n]+/g, line => highlight(line, 'json')) + } + if (MARKUP_CONTENT_TYPE_RE.test(contentType)) { + return text => highlight(formatHtml(text), 'html') + } + const language = CONTENT_TYPE_LANGUAGES.find(([pattern]) => pattern.test(contentType))?.[1] + return language ? text => highlight(text, language) : undefined } function isJson(value: string): boolean { diff --git a/packages/nuxt-cli/src/commands/task/_utils.ts b/packages/nuxt-cli/src/commands/task/_utils.ts index 5e7bb370f..77935bf61 100644 --- a/packages/nuxt-cli/src/commands/task/_utils.ts +++ b/packages/nuxt-cli/src/commands/task/_utils.ts @@ -8,7 +8,7 @@ import { styleText } from 'node:util' import { join, resolve } from 'pathe' import { findDevServer, findNitroDevWorker, noDevServerMessage, resolveLockDir, toLoopback } from '../../utils/dev-server' -import { highlightJson } from '../../utils/json-highlight' +import { highlight } from '../../utils/highlight' import { logger } from '../../utils/logger' import { logNetworkError } from '../../utils/network' import { getNuxtConfig } from '../../utils/nuxt-config' @@ -216,7 +216,7 @@ function hasFiles(dir: string): boolean { } export function format(value: unknown): string { - return typeof value === 'string' ? value : highlightJson(JSON.stringify(value, null, 2)) + return typeof value === 'string' ? value : highlight(JSON.stringify(value, null, 2), 'json') } async function request(server: TaskServer, path: string, options: RequestOptions = {}): Promise { diff --git a/packages/nuxt-cli/src/utils/format-html.ts b/packages/nuxt-cli/src/utils/format-html.ts new file mode 100644 index 000000000..92d931046 --- /dev/null +++ b/packages/nuxt-cli/src/utils/format-html.ts @@ -0,0 +1,95 @@ +/** Phrasing content, kept on the line of its surrounding text. */ +const INLINE = new Set(['a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', 'em', 'i', 'img', 'kbd', 'mark', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr']) +const VOID = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']) +/** Elements whose content is text rather than markup, and so is never reindented. */ +const RAW = new Set(['pre', 'textarea', 'script', 'style']) + +const NODE_RE = /||<[!/]?[a-z](?:[^>"']|"[^"]*"|'[^']*')*>/gi +const TAG_NAME_RE = /^<\/?\s*([a-z][\w:-]*)/i + +/** + * Reindent an HTML document so a minified response is readable. + * + * Only whitespace between block-level boundaries is rewritten. Inline elements + * keep their surrounding text, and `pre`, `textarea`, `script` and `style` + * bodies are copied verbatim, so what a browser renders does not change. + */ +export function formatHtml(html: string, indent = ' '): string { + const lines: string[] = [] + const stack: { name: string, line: number }[] = [] + let line = '' + let position = 0 + + const pad = (): string => indent.repeat(stack.length) + const push = (text: string): void => { + lines.push(pad() + text.replace(/\n[^\S\n]*/g, `\n${pad()}`)) + } + const flush = (): void => { + if (line.trim()) { + push(line.trim()) + } + line = '' + } + + NODE_RE.lastIndex = 0 + let match: RegExpExecArray | null + // eslint-disable-next-line no-cond-assign + while ((match = NODE_RE.exec(html))) { + line += html.slice(position, match.index) + position = NODE_RE.lastIndex + + const node = match[0] + const name = TAG_NAME_RE.exec(node)?.[1]?.toLowerCase() + if (!name || INLINE.has(name)) { + line += node + continue + } + + const closing = node[1] === '/' + const selfClosing = node.endsWith('/>') || VOID.has(name) + + if (RAW.has(name) && !closing && !selfClosing) { + const rest = html.slice(position) + const end = new RegExp(``, 'i').exec(rest) + const body = end ? rest.slice(0, end.index + end[0].length) : rest + flush() + lines.push(pad() + node + body) + position += body.length + NODE_RE.lastIndex = position + continue + } + + if (closing) { + const index = stack.findLastIndex(entry => entry.name === name) + // A stray closing tag is printed where it stands, without dedenting. + if (index === -1) { + flush() + push(node) + continue + } + const open = stack[index]! + // A block with only inline children stays on the line it opened on. + if (index === stack.length - 1 && open.line === lines.length && line.trim() && !line.includes('\n')) { + lines[open.line - 1] += `${line.trim()}${node}` + line = '' + stack.length = index + continue + } + flush() + stack.length = index + push(node) + continue + } + + flush() + push(node) + if (!selfClosing) { + stack.push({ name, line: lines.length }) + } + } + + line += html.slice(position) + flush() + + return lines.join('\n') +} diff --git a/packages/nuxt-cli/src/utils/highlight.ts b/packages/nuxt-cli/src/utils/highlight.ts new file mode 100644 index 000000000..3c44b7c5e --- /dev/null +++ b/packages/nuxt-cli/src/utils/highlight.ts @@ -0,0 +1,211 @@ +import type { ShjLanguageDefinition, ShjToken } from '@speed-highlight/core' + +import { styleText } from 'node:util' + +import bashLanguage from '@speed-highlight/core/languages/bash.js' +import cssLanguage from '@speed-highlight/core/languages/css.js' +import diffLanguage from '@speed-highlight/core/languages/diff.js' +import htmlLanguage from '@speed-highlight/core/languages/html.js' +import httpLanguage from '@speed-highlight/core/languages/http.js' +import jsLanguage from '@speed-highlight/core/languages/js.js' +import templateLanguage, { type as templateType } from '@speed-highlight/core/languages/js_template_literals.js' +import jsdocLanguage, { type as jsdocType } from '@speed-highlight/core/languages/jsdoc.js' +import jsonLanguage from '@speed-highlight/core/languages/json.js' +import mdLanguage from '@speed-highlight/core/languages/md.js' +import pyLanguage from '@speed-highlight/core/languages/py.js' +import regexLanguage, { type as regexType } from '@speed-highlight/core/languages/regex.js' +import todoLanguage, { type as todoType } from '@speed-highlight/core/languages/todo.js' +import tsLanguage from '@speed-highlight/core/languages/ts.js' +import yamlLanguage from '@speed-highlight/core/languages/yaml.js' + +/** The languages `highlight` can be asked for by name. */ +export type HighlightLanguage = 'bash' | 'css' | 'diff' | 'html' | 'http' | 'js' | 'json' | 'md' | 'py' | 'ts' | 'yaml' + +type Style = Parameters[0] + +interface Language { + sub: ShjLanguageDefinition + type?: ShjToken +} + +/** Language for a nested `sub` we deliberately do not bundle: emit its text unstyled. */ +const PLAIN: Language = { sub: [] } + +/** + * The HTML rules with embedded script blocks read as TypeScript, which is what + * a single-file component's `' + + expect(formatHtml(html)).toMatchInlineSnapshot(` + "
+
  keep
+         me  
+ + + +
" + `) + }) + + it('does not confuse a `>` inside an attribute for the end of a tag', () => { + expect(formatHtml('
t
')).toBe('
t
') + expect(formatHtml('

x

')).toBe('
\n

x

\n
') + expect(formatHtml('

x

')).toBe('
\n

x

\n
') + }) + + it('leaves comments, self-closing tags and bare text alone', () => { + expect(formatHtml('')).toBe('\n\n \n') + expect(formatHtml('plain text')).toBe('plain text') + }) + + it('does not lose content when tags are unbalanced', () => { + expect(formatHtml('
unclosed

x

')).toContain('unclosed') + expect(formatHtml('')).toBe('') + expect(formatHtml('

x

y

')).toBe('
\n

x

\n \n

y

\n
') + }) +}) diff --git a/packages/nuxt-cli/test/unit/utils/highlight.spec.ts b/packages/nuxt-cli/test/unit/utils/highlight.spec.ts new file mode 100644 index 000000000..f0fd7fe03 --- /dev/null +++ b/packages/nuxt-cli/test/unit/utils/highlight.spec.ts @@ -0,0 +1,109 @@ +import process from 'node:process' +import { stripVTControlCharacters, styleText } from 'node:util' + +import { describe, expect, it } from 'vitest' + +process.env.FORCE_COLOR = '3' + +const { highlight } = await import('../../../src/utils/highlight') + +describe('highlight', () => { + describe('json', () => { + const json = JSON.stringify({ name: 'db:seed', count: 3, ok: true, missing: null, list: [1, 'two'] }, null, 2) + + it('colours keys, strings, numbers and booleans', () => { + const highlighted = highlight(json, 'json') + + expect(highlighted).toContain(`${styleText('blue', '"name"')}: ${styleText('green', '"db:seed"')}`) + expect(highlighted).toContain(styleText('magenta', '3')) + expect(highlighted).toContain(styleText('yellow', 'true')) + expect(highlighted).toContain(styleText('magenta', 'null')) + }) + + it('leaves the document parseable', () => { + expect(JSON.parse(stripVTControlCharacters(highlight(json, 'json')))).toEqual(JSON.parse(json)) + }) + + it('does not colour inside strings that look like tokens', () => { + expect(highlight('{\n "a": "true 12 null"\n}', 'json')).toBe(`{\n ${styleText('blue', '"a"')}: ${styleText('green', '"true 12 null"')}\n}`) + }) + }) + + describe('html', () => { + const html = '\n
\n \n text\n
\n' + + it('colours tags, attributes and comments', () => { + const highlighted = highlight(html, 'html') + + expect(highlighted).toContain(styleText('blue', 'div')) + expect(highlighted).toContain(styleText('yellow', 'id')) + expect(highlighted).toContain(styleText('green', '"app"')) + expect(highlighted).toContain(styleText('gray', '')) + }) + + it('colours embedded script and style contents', () => { + const highlighted = highlight('', 'html') + + expect(highlighted).toContain(styleText('magenta', 'const')) + expect(highlighted).toContain(styleText('blue', 'color')) + }) + + it('leaves the document untouched', () => { + expect(stripVTControlCharacters(highlight(html, 'html'))).toBe(html) + }) + }) + + describe('md', () => { + const md = [ + '# Title', + '', + 'Some **bold** and `code`.', + '', + '```ts [app.ts]', + 'const a = 1', + '```', + '', + ].join('\n') + + it('colours headings, emphasis and inline code', () => { + const highlighted = highlight(md, 'md') + + expect(highlighted).toContain(styleText('magenta', '# Title')) + expect(highlighted).toContain(styleText('yellow', '**bold**')) + expect(highlighted).toContain(styleText('green', '`code`')) + }) + + it('colours a fenced block with the language of its info string', () => { + expect(highlight(md, 'md')).toContain(styleText('magenta', 'const')) + expect(highlight('```typescript\nconst a = 1\n```\n', 'md')).toContain(styleText('magenta', 'const')) + }) + + it('colours a vue fence, reading its script block as typescript', () => { + const highlighted = highlight('```vue [app.vue]\n\n\n```\n', 'md') + + expect(highlighted).toContain(styleText('magenta', 'interface')) + expect(highlighted).toContain(styleText('blue', 'style')) + expect(highlighted).toContain(styleText('blue', 'color')) + }) + + it.each<[fence: string, token: string, colour: 'cyan' | 'magenta' | 'red']>([ + ['```bash\necho "hi"\n```\n', 'echo', 'cyan'], + ['```sh\necho "hi"\n```\n', 'echo', 'cyan'], + ['```diff\n- a\n+ b\n```\n', '- a', 'red'], + ['```python\ndef a(): pass\n```\n', 'def', 'magenta'], + ['```http\nGET /x HTTP/1.1\n```\n', 'GET', 'magenta'], + ])('colours a %j fence', (fence, token, colour) => { + expect(highlight(fence, 'md')).toContain(styleText(colour, token)) + }) + + it('leaves a fence in an unknown language unstyled', () => { + const highlighted = highlight('```zig\nconst a = 1\n```\n', 'md') + + expect(highlighted).toContain('\nconst a = 1\n') + }) + + it('leaves the document untouched', () => { + expect(stripVTControlCharacters(highlight(md, 'md'))).toBe(md) + }) + }) +}) diff --git a/packages/nuxt-cli/test/unit/utils/json-highlight.spec.ts b/packages/nuxt-cli/test/unit/utils/json-highlight.spec.ts deleted file mode 100644 index 3c6d7857f..000000000 --- a/packages/nuxt-cli/test/unit/utils/json-highlight.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import process from 'node:process' -import { stripVTControlCharacters, styleText } from 'node:util' - -import { describe, expect, it } from 'vitest' - -process.env.FORCE_COLOR = '3' - -const { highlightJson } = await import('../../../src/utils/json-highlight') - -describe('highlightJson', () => { - const json = JSON.stringify({ name: 'db:seed', count: 3, ok: true, missing: null, list: [1, 'two'] }, null, 2) - - it('colours keys, strings, numbers, booleans and null', () => { - const highlighted = highlightJson(json) - - expect(highlighted).toContain(`${styleText('blue', '"name"')}: ${styleText('green', '"db:seed"')}`) - expect(highlighted).toContain(styleText('magenta', '3')) - expect(highlighted).toContain(styleText('yellow', 'true')) - expect(highlighted).toContain(styleText('dim', 'null')) - }) - - it('leaves the document parseable', () => { - expect(JSON.parse(stripVTControlCharacters(highlightJson(json)))).toEqual(JSON.parse(json)) - }) - - it('does not colour inside strings that look like tokens', () => { - expect(highlightJson('{\n "a": "true 12 null"\n}')).toBe(`{\n ${styleText('blue', '"a"')}: ${styleText('green', '"true 12 null"')}\n}`) - }) -}) diff --git a/packages/nuxt-cli/tsdown.config.ts b/packages/nuxt-cli/tsdown.config.ts index 9d438579c..f71a2f9a7 100644 --- a/packages/nuxt-cli/tsdown.config.ts +++ b/packages/nuxt-cli/tsdown.config.ts @@ -12,6 +12,6 @@ export const packaging: PackagingContract = { export default defineCliConfig({ entry: ['src/index.ts', 'src/dev/index.ts'], - deps: { onlyBundle: ['h3'], neverBundle: PARSER_PACKAGES }, + deps: { onlyBundle: ['h3', '@speed-highlight/core'], neverBundle: PARSER_PACKAGES }, ...packaging, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4776e1f3..b3de3c410 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -260,6 +260,9 @@ importers: '@nuxt/test-utils': specifier: ^4.1.0 version: 4.1.0(esbuild@0.28.0)(magicast@0.5.3)(rolldown@1.2.1)(rollup@4.62.4)(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(yaml@2.9.0))(vitest@4.1.10) + '@speed-highlight/core': + specifier: ^1.2.23 + version: 1.2.23 '@types/node': specifier: ^24.13.3 version: 24.13.3 @@ -2439,8 +2442,8 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@speed-highlight/core@1.2.15': - resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} + '@speed-highlight/core@1.2.23': + resolution: {integrity: sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg==} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -8882,7 +8885,7 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@speed-highlight/core@1.2.15': {} + '@speed-highlight/core@1.2.23': {} '@standard-schema/spec@1.1.0': {} @@ -14206,7 +14209,7 @@ snapshots: dependencies: '@poppinss/colors': 4.1.6 '@poppinss/dumper': 0.7.0 - '@speed-highlight/core': 1.2.15 + '@speed-highlight/core': 1.2.23 cookie-es: 3.1.1 youch-core: 0.3.3