Migrate to shikiji

This commit is contained in:
SonMooSans
2023-12-05 21:59:44 +08:00
parent 0da29c83e0
commit de44efe4ff
19 changed files with 185 additions and 228 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'next-docs-zeta': minor
'next-docs-mdx': minor
'next-docs-ui': minor
---
Migrate to Shikiji
+7
View File
@@ -0,0 +1,7 @@
---
'next-docs-zeta': minor
'next-docs-mdx': minor
'next-docs-ui': minor
---
Support code highlighting options
@@ -1,14 +1,14 @@
'use client'
import { useEffect, useState } from 'react'
import type { IThemedToken } from 'shiki'
import type { ThemedToken } from 'shikiji'
export function TypingCode({
tokens,
delay,
time
}: {
tokens: IThemedToken[]
tokens: ThemedToken[]
/**
* Delay to start animation (in ms)
*/
@@ -36,7 +36,7 @@ export function TypingCode({
let currentLen = 0
return (
<code data-line>
<span data-line>
{tokens.flatMap((s, j) => {
if (currentLen > length) return
const slicedContent = s.content.slice(
@@ -51,6 +51,6 @@ export function TypingCode({
</span>
)
})}
</code>
</span>
)
}
+36 -27
View File
@@ -1,11 +1,12 @@
import { cn } from '@/utils/cn'
import { Pre } from 'next-docs-ui/mdx-server'
import { useMemo, type ComponentPropsWithoutRef } from 'react'
import { getHighlighter, type IThemedToken } from 'shiki'
import { getHighlighter, type ThemedToken } from 'shikiji'
import { TypingCode } from './typing-code-block.client'
const highlighter = await getHighlighter({
theme: 'css-variables'
langs: ['bash', 'ts', 'tsx'],
themes: ['github-dark']
})
export type CodeBlockProps = ComponentPropsWithoutRef<typeof Pre> & {
@@ -15,21 +16,26 @@ export type CodeBlockProps = ComponentPropsWithoutRef<typeof Pre> & {
export function CodeBlock({ code, lang, className, ...props }: CodeBlockProps) {
const tokens = useMemo(
() => highlighter.codeToThemedTokens(code, lang),
() =>
highlighter.codeToThemedTokens(code, {
lang
}),
[code]
)
return (
<Pre className={cn('grid', className)} {...props}>
{tokens.map((token, i) => (
<code data-line key={i}>
{token.map((s, j) => (
<span key={j} style={{ color: s.color }}>
{s.content}
</span>
))}
</code>
))}
<Pre className={cn('bg-black', className)} {...props}>
<code className="grid">
{tokens.map((token, i) => (
<span data-line key={i}>
{token.map((s, j) => (
<span key={j} style={{ color: s.color }}>
{s.content}
</span>
))}
</span>
))}
</code>
</Pre>
)
}
@@ -41,27 +47,30 @@ export function TypingCodeBlock({
...props
}: CodeBlockProps) {
const result = useMemo(
() => highlighter.codeToThemedTokens(code, lang),
() =>
highlighter.codeToThemedTokens(code, { lang, includeExplanation: false }),
[code]
)
return (
<Pre className={cn('grid', className)} {...props}>
{result.map((tokens, i) => (
<TypingCode
delay={result
.slice(0, i)
.map(t => tokenLength(t) * 50)
.reduce((a, b) => a + b + 50, 0)}
key={i}
tokens={tokens}
time={50}
/>
))}
<Pre className={cn('bg-black', className)} {...props}>
<code className="grid">
{result.map((tokens, i) => (
<TypingCode
delay={result
.slice(0, i)
.map(t => tokenLength(t) * 50)
.reduce((a, b) => a + b + 50, 0)}
key={i}
tokens={tokens}
time={50}
/>
))}
</code>
</Pre>
)
}
function tokenLength(tokens: IThemedToken[]): number {
function tokenLength(tokens: ThemedToken[]): number {
return tokens.map(s => s.content.length).reduce((a, b) => a + b, 0)
}
+1 -1
View File
@@ -21,7 +21,7 @@ It's recommended to use Next Docs with [Tailwind CSS](https://tailwindcss.com) +
</Callout>
```package-install
next-docs-zeta shiki
next-docs-zeta
```
### Demo
@@ -22,7 +22,7 @@ module.exports = withContentlayer(config)
Display code blocks, required by default.
- Syntax highlighting powered by [Shiki](https://github.com/shikijs/shiki).
- Syntax highlighting powered by Shikiji
- Copy button
- Custom Titles
+9
View File
@@ -12,6 +12,15 @@ const withAnalyzer = createBundleAnalyzer({
const config = {
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
reactStrictMode: true,
webpack: config => {
// Next.js can't load shikiji without this config
config.module.rules.push({
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto'
})
return config
},
eslint: {
// Replaced by root workspace command
ignoreDuringBuilds: true
+1 -1
View File
@@ -29,7 +29,7 @@
"react-dom": "18.2.0",
"rehype-katex": "^6.0.3",
"remark-math": "^5.1.1",
"shiki": "^0.14.5",
"shikiji": "^0.7.6",
"tailwind-merge": "^2.0.0",
"zod": "^3.22.4"
},
+10 -3
View File
@@ -5,7 +5,8 @@ import {
rehypeNextDocs,
remarkGfm,
remarkStructure,
remarkToc
remarkToc,
type RehypeNextDocsOptions
} from 'next-docs-zeta/mdx-plugins'
import remarkFrontmatter, {
type Options as RemarkFrontmatterOptions
@@ -48,6 +49,11 @@ type NextDocsMDXOptions = {
* @default './content/docs`
*/
rootContentPath?: string
/**
* Options passed to the built-in rehype plugin
*/
pluginOptions?: RehypeNextDocsOptions
}
const createNextDocs =
@@ -57,7 +63,8 @@ const createNextDocs =
mdxOptions = {},
cwd = process.cwd(),
rootMapPath = './_map.ts',
rootContentPath = './content/docs'
rootContentPath = './content/docs',
pluginOptions
}: NextDocsMDXOptions = {}) =>
(nextConfig: NextConfig = {}) => {
const exports = ['structuredData', 'toc', ...dataExports]
@@ -74,7 +81,7 @@ const createNextDocs =
]
const rehypePlugins: PluggableList = [
rehypeNextDocs,
[rehypeNextDocs, pluginOptions],
...(mdxOptions?.rehypePlugins ?? [])
]
+2 -2
View File
@@ -2,7 +2,7 @@
The Next.js framework for building documentation website.
It's build on the top of Next.js, Radix UI and **Next Docs**, fully styled using
It's built on top of Next.js, Radix UI and **Next Docs**, fully styled using
Tailwind CSS.
## Features
@@ -13,7 +13,7 @@ Next Docs UI provides many features out of the box.
- Full-text Search
- Table of Contents
- Built-in Components
- Code Syntax highlighting powered by Shiki
- Code Syntax highlighting powered by Shikiji
- Well Designed UI & Accessibility
**Moreover:**
-93
View File
@@ -1,93 +0,0 @@
:root {
--shiki-color-text: oklch(37.53% 0 0);
--shiki-color-background: transparent;
--shiki-token-constant: oklch(56.45% 0.163 253.27);
--shiki-token-string: oklch(54.64% 0.144 147.32);
--shiki-token-comment: oklch(73.8% 0 0);
--shiki-token-keyword: oklch(56.8% 0.2 26.41);
--shiki-token-parameter: oklch(77.03% 0.174 64.05);
--shiki-token-function: oklch(50.15% 0.188 294.99);
--shiki-token-string-expression: var(--shiki-token-string);
--shiki-token-punctuation: oklch(24.78% 0 0);
--shiki-token-link: var(--shiki-token-string);
/* from github-light */
--shiki-color-ansi-black: #24292e;
--shiki-color-ansi-black-dim: #24292e80;
--shiki-color-ansi-red: #d73a49;
--shiki-color-ansi-red-dim: #d73a4980;
--shiki-color-ansi-green: #28a745;
--shiki-color-ansi-green-dim: #28a74580;
--shiki-color-ansi-yellow: #dbab09;
--shiki-color-ansi-yellow-dim: #dbab0980;
--shiki-color-ansi-blue: #0366d6;
--shiki-color-ansi-blue-dim: #0366d680;
--shiki-color-ansi-magenta: #5a32a3;
--shiki-color-ansi-magenta-dim: #5a32a380;
--shiki-color-ansi-cyan: #1b7c83;
--shiki-color-ansi-cyan-dim: #1b7c8380;
--shiki-color-ansi-white: #6a737d;
--shiki-color-ansi-white-dim: #6a737d80;
--shiki-color-ansi-bright-black: #959da5;
--shiki-color-ansi-bright-black-dim: #959da580;
--shiki-color-ansi-bright-red: #cb2431;
--shiki-color-ansi-bright-red-dim: #cb243180;
--shiki-color-ansi-bright-green: #22863a;
--shiki-color-ansi-bright-green-dim: #22863a80;
--shiki-color-ansi-bright-yellow: #b08800;
--shiki-color-ansi-bright-yellow-dim: #b0880080;
--shiki-color-ansi-bright-blue: #005cc5;
--shiki-color-ansi-bright-blue-dim: #005cc580;
--shiki-color-ansi-bright-magenta: #5a32a3;
--shiki-color-ansi-bright-magenta-dim: #5a32a380;
--shiki-color-ansi-bright-cyan: #3192aa;
--shiki-color-ansi-bright-cyan-dim: #3192aa80;
--shiki-color-ansi-bright-white: #d1d5da;
--shiki-color-ansi-bright-white-dim: #d1d5da80;
}
.dark {
--shiki-color-text: oklch(91.18% 0.019 257.29);
--shiki-token-constant: oklch(76.85% 0.121 252.34);
--shiki-token-string: oklch(81.11% 0.124 0);
--shiki-token-comment: oklch(55.18% 0.017 251.27);
--shiki-token-keyword: oklch(72.14% 0.162 15.49);
--shiki-token-function: oklch(72.67% 0.137 299.15);
--shiki-token-string-expression: oklch(69.28% 0.179 143.2);
--shiki-token-punctuation: oklch(79.21% 0 0);
--shiki-token-link: var(--shiki-token-string);
/* from github-dark */
--shiki-color-ansi-black: #586069;
--shiki-color-ansi-black-dim: #58606980;
--shiki-color-ansi-red: #ea4a5a;
--shiki-color-ansi-red-dim: #ea4a5a80;
--shiki-color-ansi-green: #34d058;
--shiki-color-ansi-green-dim: #34d05880;
--shiki-color-ansi-yellow: #ffea7f;
--shiki-color-ansi-yellow-dim: #ffea7f80;
--shiki-color-ansi-blue: #2188ff;
--shiki-color-ansi-blue-dim: #2188ff80;
--shiki-color-ansi-magenta: #b392f0;
--shiki-color-ansi-magenta-dim: #b392f080;
--shiki-color-ansi-cyan: #39c5cf;
--shiki-color-ansi-cyan-dim: #39c5cf80;
--shiki-color-ansi-white: #d1d5da;
--shiki-color-ansi-white-dim: #d1d5da80;
--shiki-color-ansi-bright-black: #959da5;
--shiki-color-ansi-bright-black-dim: #959da580;
--shiki-color-ansi-bright-red: #f97583;
--shiki-color-ansi-bright-red-dim: #f9758380;
--shiki-color-ansi-bright-green: #85e89d;
--shiki-color-ansi-bright-green-dim: #85e89d80;
--shiki-color-ansi-bright-yellow: #ffea7f;
--shiki-color-ansi-bright-yellow-dim: #ffea7f80;
--shiki-color-ansi-bright-blue: #79b8ff;
--shiki-color-ansi-bright-blue-dim: #79b8ff80;
--shiki-color-ansi-bright-magenta: #b392f0;
--shiki-color-ansi-bright-magenta-dim: #b392f080;
--shiki-color-ansi-bright-cyan: #56d4dd;
--shiki-color-ansi-bright-cyan-dim: #56d4dd80;
--shiki-color-ansi-bright-white: #fafbfc;
--shiki-color-ansi-bright-white-dim: #fafbfc80;
}
+10 -4
View File
@@ -1,5 +1,3 @@
@import './codeblock.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -12,11 +10,11 @@ body {
@apply nd-bg-background nd-text-foreground;
}
[data-line] {
[data-nd-codeblock] [data-line] {
@apply nd-px-4 nd-border-l-2 nd-border-transparent;
}
[data-highlighted-line] {
[data-highlighted-line][data-line] {
@apply nd-border-primary nd-bg-primary/10;
}
@@ -24,6 +22,14 @@ body {
@apply nd-bg-primary/10 nd-border-b-2 nd-border-primary;
}
[data-line] span {
color: var(--shiki-light);
}
.dark [data-line] span {
color: var(--shiki-dark);
}
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
@@ -1,44 +1,55 @@
import { ScrollArea } from '@/components/ui/scroll-area'
import { cn } from '@/utils/cn'
import { CheckIcon, CopyIcon } from 'lucide-react'
import type { ButtonHTMLAttributes, ComponentPropsWithoutRef } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import type { ButtonHTMLAttributes, HTMLAttributes } from 'react'
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
type PreProps = ComponentPropsWithoutRef<'pre'> & {
type PreProps = HTMLAttributes<HTMLElement> & {
allowCopy?: boolean
}
export function Pre({ title, allowCopy = true, ...props }: PreProps) {
const ref = useRef<HTMLPreElement>(null)
const onCopy = useCallback(() => {
if (ref.current?.textContent == null) return
export const Pre = forwardRef<HTMLElement, PreProps>(
({ title, allowCopy = true, className, ...props }, ref) => {
const preRef = useRef<HTMLPreElement>(null)
const onCopy = useCallback(() => {
if (preRef.current?.textContent == null) return
navigator.clipboard.writeText(ref.current.textContent)
}, [])
navigator.clipboard.writeText(preRef.current.textContent)
}, [])
return (
<div className="nd-relative nd-overflow-hidden nd-group nd-border nd-rounded-lg nd-text-sm nd-bg-secondary/50 nd-my-6 nd-not-prose">
{title ? (
<div className="nd-flex nd-flex-row nd-items-center nd-bg-muted nd-text-muted-foreground nd-pl-4 nd-pr-2 nd-py-1.5 nd-border-b">
<span className="nd-flex-1">{title}</span>
{allowCopy && <CopyButton onCopy={onCopy} />}
</div>
) : (
allowCopy && (
<CopyButton
className="nd-absolute nd-top-2 nd-right-2 nd-z-[2]"
onCopy={onCopy}
/>
)
)}
<ScrollArea>
<pre ref={ref} {...props} className={cn('nd-py-4', props.className)}>
{props.children}
</pre>
</ScrollArea>
</div>
)
}
return (
<figure
ref={ref}
className={cn(
'nd-relative nd-overflow-hidden nd-group nd-border nd-rounded-lg nd-text-sm nd-bg-secondary/50 nd-my-6 nd-not-prose',
className
)}
{...props}
>
{title ? (
<div className="nd-flex nd-flex-row nd-items-center nd-bg-muted nd-text-muted-foreground nd-pl-4 nd-pr-2 nd-py-1.5 nd-border-b">
<figcaption className="nd-flex-1">{title}</figcaption>
{allowCopy && <CopyButton onCopy={onCopy} />}
</div>
) : (
allowCopy && (
<CopyButton
className="nd-absolute nd-top-2 nd-right-2 nd-z-[2]"
onCopy={onCopy}
/>
)
)}
<ScrollArea>
<pre data-nd-codeblock ref={preRef} className="nd-py-4">
{props.children}
</pre>
</ScrollArea>
</figure>
)
}
)
Pre.displayName = 'Pre'
function CopyButton({
className,
+6 -1
View File
@@ -30,7 +30,12 @@ export function RootProvider({
)
return enableThemeProvider ? (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{body}
</ThemeProvider>
) : (
+1 -1
View File
@@ -129,7 +129,7 @@
"remark-gfm": "^3.0.1",
"remark-mdx": "^2.3.0",
"scroll-into-view-if-needed": "^3.1.0",
"shiki": "^0.14.5",
"shikiji": "^0.7.6",
"swr": "^2.2.2",
"unist-util-visit": "^4.1.2"
},
@@ -7,6 +7,7 @@
*/
import { rehypeImgSize, rehypeNextDocs, remarkGfm } from '@/mdx-plugins'
import type { RehypeNextDocsOptions } from '@/mdx-plugins/rehype-next-docs'
import type { Args, ComputedFields, FieldDef } from 'contentlayer/source-files'
import { defineDocumentType } from 'contentlayer/source-files'
import type { Options as ImgSizeOptions } from 'rehype-img-size'
@@ -50,10 +51,12 @@ type Options = {
*/
imgDirPath: string
docFields?: Record<string, FieldDef>
docsComputedFields?: ComputedFields<'Docs'>
metaFields?: Record<string, FieldDef>
metaComputedFields?: ComputedFields<'Meta'>
pluginOptions: RehypeNextDocsOptions
docFields: Record<string, FieldDef>
docsComputedFields: ComputedFields<'Docs'>
metaFields: Record<string, FieldDef>
metaComputedFields: ComputedFields<'Meta'>
}
export function createConfig(options: Partial<Options> = {}): Args {
@@ -64,6 +67,7 @@ export function createConfig(options: Partial<Options> = {}): Args {
docFields,
metaFields,
docsComputedFields,
pluginOptions,
metaComputedFields
} = options
@@ -146,7 +150,7 @@ export function createConfig(options: Partial<Options> = {}): Args {
documentTypes: [Docs, Meta],
mdx: {
rehypePlugins: [
rehypeNextDocs,
[rehypeNextDocs, pluginOptions],
[
// @ts-ignore
rehypeImgSize,
+4 -1
View File
@@ -6,7 +6,10 @@ export {
default as remarkGfm,
type Options as RemarkGfmOptions
} from 'remark-gfm'
export { rehypeNextDocs } from './mdx-plugins/rehype-next-docs'
export {
type RehypeNextDocsOptions,
rehypeNextDocs
} from './mdx-plugins/rehype-next-docs'
export {
remarkDynamicContent,
type Options as RemarkDynamicContentOptions
@@ -1,5 +1,5 @@
import Slugger from 'github-slugger'
import type { Root } from 'hast'
import type { Element, Root } from 'hast'
import rehypePrettycode, {
type Options as RehypePrettyCodeOptions
} from 'rehype-pretty-code'
@@ -11,7 +11,10 @@ const headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
const customMetaRegex = /custom="([^"]+)"/
const rehypePrettyCodeOptions: RehypePrettyCodeOptions = {
theme: 'css-variables',
theme: {
light: 'github-light',
dark: 'github-dark'
},
defaultLang: {
block: 'text'
},
@@ -22,10 +25,16 @@ const rehypePrettyCodeOptions: RehypePrettyCodeOptions = {
}
}
export type RehypeNextDocsOptions = {
codeOptions?: RehypePrettyCodeOptions
}
/**
* Handle codeblocks and heading slugs
*/
export function rehypeNextDocs(): Transformer<Root, Root> {
export function rehypeNextDocs({
codeOptions
}: RehypeNextDocsOptions = {}): Transformer<Root, Root> {
return async tree => {
slugger.reset()
@@ -52,31 +61,29 @@ export function rehypeNextDocs(): Transformer<Root, Root> {
})
// @ts-ignore
await rehypePrettycode(rehypePrettyCodeOptions)(tree)
await rehypePrettycode({ ...rehypePrettyCodeOptions, ...codeOptions })(tree)
visit(tree, ['div', 'pre'], node => {
visit(tree, ['figure', 'pre'], node => {
node.properties ||= {}
// Remove default fragment div
// Remove default fragment element
// Add title to pre element
if ('data-rehype-pretty-code-fragment' in node.properties) {
if (node.children.length === 0) return
let title: string | undefined = undefined
if ('data-rehype-pretty-code-figure' in node.properties) {
const titleNode = node.children.find(
child => child.type === 'element' && child.tagName === 'figcaption'
) as Element | undefined
const preNode = node.children.find(
child => child.type === 'element' && child.tagName === 'pre'
) as Element | undefined
for (const child of node.children) {
if (child.type !== 'element') continue
child.properties ||= {}
if (!preNode) return
if ('data-rehype-pretty-code-title' in child.properties) {
title = flattenNode(child)
}
if (child.tagName === 'pre') {
child.properties.title = title
Object.assign(node, child)
break
}
if (titleNode) {
preNode.properties ||= {}
preNode.properties.title = flattenNode(titleNode)
}
Object.assign(node, preNode)
}
// Add custom meta to properties
+6 -31
View File
@@ -113,9 +113,9 @@ importers:
remark-math:
specifier: ^5.1.1
version: 5.1.1
shiki:
specifier: ^0.14.5
version: 0.14.5
shikiji:
specifier: ^0.7.6
version: 0.7.6
tailwind-merge:
specifier: ^2.0.0
version: 2.0.0
@@ -343,9 +343,9 @@ importers:
scroll-into-view-if-needed:
specifier: ^3.1.0
version: 3.1.0
shiki:
specifier: ^0.14.5
version: 0.14.5
shikiji:
specifier: ^0.7.6
version: 0.7.6
swr:
specifier: ^2.2.2
version: 2.2.2([email protected])
@@ -3595,10 +3595,6 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
/[email protected]:
resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==}
dev: false
/[email protected]:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
@@ -5873,10 +5869,6 @@ packages:
engines: {node: '>=6'}
hasBin: true
/[email protected]:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
dev: false
/[email protected]:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
optionalDependencies:
@@ -8124,15 +8116,6 @@ packages:
resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==}
dev: true
/[email protected]:
resolution: {integrity: sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==}
dependencies:
ansi-sequence-parser: 1.1.1
jsonc-parser: 3.2.0
vscode-oniguruma: 1.7.0
vscode-textmate: 8.0.0
dev: false
/[email protected]:
resolution: {integrity: sha512-KzEtvSGQtBvfwVIB70kOmIfl/5rz1LC8j+tvlHXsJKAIdONNQvG1at7ivUUq3xUctqgO6fsO3AGomUSh0F+wsQ==}
dependencies:
@@ -9079,14 +9062,6 @@ packages:
vfile-message: 4.0.2
dev: false
/[email protected]:
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
dev: false
/[email protected]:
resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
dev: false
/[email protected]:
resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
engines: {node: '>=10.13.0'}