Merge upstream main into feat/disable-auto-html-dir

pull/5416/head
Achord Chan 5 days ago
commit c87875acb2

@ -0,0 +1,21 @@
<script lang="ts">
if (typeof window === 'undefined') {
throw new Error('This component can only be imported in the browser')
}
</script>
<script setup lang="ts">
defineProps<{ message?: string }>()
</script>
<template>
<section class="client-component">
<header>
<slot name="header" :message="message || 'scoped value'">
header fallback
</slot>
</header>
<div class="default-slot"><slot>default fallback</slot></div>
<footer><slot name="footer" /></footer>
</section>
</template>

@ -0,0 +1,78 @@
describe('client components', () => {
beforeEach(async () => {
await goto('/client-component/')
await page.waitForSelector('#forwarded.client-component')
})
test('forwards default and scoped slots and keeps them reactive', async () => {
const header = page.locator('#forwarded header')
const content = page.locator('#forwarded .default-slot')
expect(await header.textContent()).toBe('scoped value 0')
expect((await content.textContent())?.trim()).toBe('default content 0')
expect(await page.locator('#forwarded footer').textContent()).toBe('')
await page.locator('#increment').click()
await expect.poll(() => header.textContent()).toBe('scoped value 1')
expect((await content.textContent())?.trim()).toBe('default content 1')
expect(await page.locator('#forwarded footer').textContent()).toBe(
'conditional slot'
)
await page.locator('#increment').click()
await expect.poll(() => header.textContent()).toBe('scoped value 2')
expect(await page.locator('#forwarded footer').textContent()).toBe('')
})
test('forwards slots when args only provide props', async () => {
expect(await page.locator('#with-props header').textContent()).toBe(
'from args'
)
expect(
(await page.locator('#with-props .default-slot').textContent())?.trim()
).toBe('content with props')
expect(await page.locator('#empty-args .default-slot').textContent()).toBe(
'content with empty args'
)
expect(await page.locator('#null-props .default-slot').textContent()).toBe(
'content with null props'
)
})
test('preserves explicitly provided slots and shorthand children', async () => {
expect(await page.locator('#explicit header').textContent()).toBe(
'explicit header'
)
expect(await page.locator('#explicit .default-slot').textContent()).toBe(
'explicit default'
)
for (const [id, content] of [
['string', 'string child'],
['array', 'array child'],
['vnode', 'vnode child'],
['function', 'function child'],
['number', '42'],
['null', 'default fallback'],
['undefined', 'default fallback'],
['multiple', 'firstsecond']
]) {
expect(
await page.locator(`#legacy-${id} .default-slot`).textContent()
).toBe(content)
}
})
test.runIf(!!process.env.VITE_TEST_BUILD)(
'does not render the component or its slots on the server',
async () => {
const response = await page.request.get(page.url())
const html = await response.text()
expect(html).not.toContain('class="client-component"')
expect(html).not.toContain('default content 0')
}
)
})

@ -0,0 +1,57 @@
<script setup>
import { h, ref } from 'vue'
import { defineClientComponent } from 'vitepress'
const load = () => import('./Slots.vue')
const ClientComponent = defineClientComponent(load)
const WithProps = defineClientComponent(load, [{ message: 'from args' }])
const WithEmptyArgs = defineClientComponent(load, [])
const WithNullProps = defineClientComponent(load, [null])
const WithExplicitSlots = defineClientComponent(load, [null, {
default: () => 'explicit default',
header: () => 'explicit header'
}])
const legacyChildren = [
['string', ['string child']],
['array', [[h('span', 'array child')]]],
['vnode', [h('span', 'vnode child')]],
['function', [() => 'function child']],
['number', [42]],
['null', [null, null]],
['undefined', [null, undefined]],
['multiple', [null, 'first', 'second']]
].map(([id, args]) => ({ id, component: defineClientComponent(load, args) }))
const count = ref(0)
</script>
# Client Components
<button id="increment" @click="count++">Increment</button>
<ClientComponent id="forwarded">
<template #header="{ message }">{{ message }} {{ count }}</template>
<template v-if="count % 2" #footer>conditional slot</template>
default content {{ count }}
</ClientComponent>
<WithProps id="with-props">
<template #header="{ message }">{{ message }}</template>
content with props
</WithProps>
<WithEmptyArgs id="empty-args">content with empty args</WithEmptyArgs>
<WithNullProps id="null-props">content with null props</WithNullProps>
<WithExplicitSlots id="explicit">
<template #header>ignored header</template>
ignored default
</WithExplicitSlots>
<component
v-for="{ id, component } in legacyChildren"
:is="component"
:id="`legacy-${id}`"
>
ignored content
</component>

@ -0,0 +1,38 @@
describe('themeable image alt overrides', () => {
beforeEach(async () => {
await goto('/theme-image/')
})
test.each(['light', 'dark'])('uses the alt prop in %s mode', async (mode) => {
await page.evaluate((mode) => {
document.documentElement.classList.toggle('dark', mode === 'dark')
}, mode)
expect(
await page.locator('.image-override img:visible').getAttribute('alt')
).toBe('Override description')
expect(
await page.locator('.image-decorative img:visible').getAttribute('alt')
).toBe('')
expect(
await page.locator('.image-fallback img:visible').getAttribute('alt')
).toBe('Image description')
})
test('updates both images when the alt prop changes', async () => {
const descriptions = () =>
page
.locator('.image-override img')
.evaluateAll((images) =>
images.map((image) => image.getAttribute('alt'))
)
await page.getByRole('button', { name: 'Update description' }).click()
await expect
.poll(descriptions)
.toEqual(['Updated description', 'Updated description'])
await page.getByRole('button', { name: 'Clear description' }).click()
await expect.poll(descriptions).toEqual(['', ''])
})
})

@ -0,0 +1,28 @@
<script setup>
import { ref } from 'vue'
import { VPImage } from 'vitepress/theme'
const description = ref('Override description')
const image = {
light: '/pwa_light.svg',
dark: '/pwa_dark.svg',
alt: 'Image description'
}
</script>
# Themeable image descriptions
<div class="image-override">
<VPImage :image :alt="description" />
</div>
<div class="image-decorative">
<VPImage :image alt="" />
</div>
<div class="image-fallback">
<VPImage :image />
</div>
<button @click="description = 'Updated description'">Update description</button>
<button @click="description = ''">Clear description</button>

@ -0,0 +1,41 @@
// @ts-expect-error the unit typecheck uses tsc; Vite compiles this SFC at runtime
import VPImage from 'client/theme-default/components/VPImage.vue'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
vi.mock('vitepress', () => ({ withBase: (path: string) => path }))
const images = [
{ name: 'string', image: 'logo.svg', fallback: '', count: 1 },
{
name: 'single source',
image: { src: 'logo.svg', alt: 'Image description' },
fallback: 'Image description',
count: 1
},
{
name: 'themed sources',
image: { light: 'light.svg', dark: 'dark.svg', alt: 'Image description' },
fallback: 'Image description',
count: 2
},
{
name: 'themed sources without alt',
image: { light: 'light.svg', dark: 'dark.svg' },
fallback: '',
count: 2
}
]
describe.each(images)('VPImage with $name', ({ image, fallback, count }) => {
test.each(['Override description', '', undefined])(
'respects the alt prop %j',
async (alt) => {
const html = await renderToString(createSSRApp(VPImage, { image, alt }))
const descriptions = [...html.matchAll(/ alt(?:="([^"]*)")?/g)].map(
([, value]) => value ?? ''
)
expect(descriptions).toEqual(Array(count).fill(alt ?? fallback))
}
)
})

@ -8,13 +8,18 @@ import type { MarkdownEnv } from 'node/shared'
async function render( async function render(
src: string, src: string,
options: MarkdownOptions = {}, options: MarkdownOptions = {},
env?: Partial<MarkdownEnv> env?: Partial<MarkdownEnv>,
base = '/'
) { ) {
disposeMdItInstance() disposeMdItInstance()
const md = await createMarkdownRenderer('.', { const md = await createMarkdownRenderer(
'.',
{
highlight: (code) => code, highlight: (code) => code,
...options ...options
}) },
base
)
return md.renderAsync(src, env) return md.renderAsync(src, env)
} }
@ -114,6 +119,48 @@ describe('node/markdown/plugins/containers', () => {
`) `)
}) })
test.each([
['/', true, true, '/guide/start'],
['./', false, true, '../guide/start.html'],
['./', true, true, '../guide/start'],
['./', false, false, '/guide/start.html']
])(
'uses page URL settings in titles (base: %s, cleanUrls: %s, relativizeUrls: %s)',
async (base, cleanUrls, relativizeUrls, href) => {
const src = [
'::: tip [Guide][guide]',
'[Guide][guide]',
':::',
'',
'::: details [Guide][guide]',
'content',
':::',
'',
'[guide]: /guide/start.md'
].join('\n')
const html = await render(
src,
{},
{ cleanUrls, relativePath: 'nested/page.md', relativizeUrls },
base
)
const link = `<a href="${href}">Guide</a>`
expect(html).toContain(`<p class="custom-block-title">${link}</p>`)
expect(html).toContain(`<summary>${link}</summary>`)
expect(html).toContain(`<p>${link}</p>`)
}
)
test('keeps page footnotes out of container titles', async () => {
const html = await render(
'::: tip [Guide](/guide.md)\nText[^1]\n:::\n\n[^1]: A note'
)
expect(html).toContain(
'<p class="custom-block-title"><a href="/guide.html">Guide</a></p>'
)
expect(html.match(/class="footnotes"/g)).toHaveLength(1)
})
test('respects custom labels from container options', async () => { test('respects custom labels from container options', async () => {
const src = '::: tip\n提示内容\n:::\n\n::: details\n详情内容\n:::' const src = '::: tip\n提示内容\n:::\n\n::: details\n详情内容\n:::'
expect( expect(

@ -0,0 +1,70 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { resolveConfig } from 'node/config'
import { disposeMdItInstance } from 'node/markdown/markdown'
import { clearCache, createMarkdownToVueRenderFn } from 'node/markdownToVue'
describe('node/markdownToVue cache invalidation', () => {
let root: string
beforeEach(async () => {
clearCache()
disposeMdItInstance()
root = await mkdtemp(path.join(tmpdir(), 'vitepress-md-cache-'))
})
afterEach(async () => {
clearCache()
disposeMdItInstance()
await rm(root, { recursive: true, force: true })
})
test.each([
['include', '<!-- @include: ./shared.md -->'],
['snippet', '<<< ./shared.md{txt}']
])(
'invalidates every source version when its %s changes',
async (_, importSrc) => {
const file = path.join(root, 'page.md')
const otherFile = path.join(root, 'other-page.md')
const sharedFile = path.join(root, 'shared.md')
const sources = ['First', 'Second', 'Third'].map(
(title) => `# ${title}\n\n${importSrc}\n`
)
await writeFile(file, sources[0])
await writeFile(otherFile, '# Other page')
await writeFile(sharedFile, 'old shared content')
const config = await resolveConfig(root, 'serve', 'development')
const render = await createMarkdownToVueRenderFn(
config.srcDir,
{ highlight: (code) => code },
'/',
false,
false,
config
)
// Editing the page creates several cached versions of the same path.
const first = await render(sources[0], file)
expect(await render(sources[0], file)).toBe(first)
await render(sources[1], file)
const other = await render('# Other page', otherFile)
await render(sources[2], file)
await writeFile(sharedFile, 'updated shared content')
// The include/snippet HMR handler invalidates each importing page by path.
clearCache('page.md')
// Undoing a page edit must not revive a stale version of the dependency.
for (const src of [...sources].reverse()) {
const result = await render(src, file)
expect(result.vueSrc).toContain('updated shared content')
expect(result.vueSrc).not.toContain('old shared content')
}
expect(await render('# Other page', otherFile)).toBe(other)
}
)
})

@ -55,7 +55,7 @@ interface LocaleSpecificConfig<ThemeConfig = any> {
Set `dir: false` at the site or locale level when application code manages the `<html dir>` attribute at runtime. VitePress will omit the attribute from generated HTML and will not overwrite it during client-side navigation. Set `dir: false` at the site or locale level when application code manages the `<html dir>` attribute at runtime. VitePress will omit the attribute from generated HTML and will not overwrite it during client-side navigation.
Refer [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) interface for details on customizing the placeholder texts of the default theme. Don't override `themeConfig.algolia` or `themeConfig.carbonAds` at locale-level. Refer [Algolia docs](../reference/default-theme-search#i18n) for using multilingual search. Refer [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) interface for details on customizing the placeholder texts of the default theme. Don't override `themeConfig.algolia` or `themeConfig.carbonAds` at locale-level. Refer [Algolia docs](../reference/default-theme-search#algolia-search-i18n) for using multilingual search.
**Pro tip:** Config file can be stored at `docs/.vitepress/config/index.ts` too. It might help you organize stuff by creating a configuration file per locale and then merge and export them from `index.ts`. **Pro tip:** Config file can be stored at `docs/.vitepress/config/index.ts` too. It might help you organize stuff by creating a configuration file per locale and then merge and export them from `index.ts`.

@ -53,7 +53,7 @@ interface LocaleSpecificConfig<ThemeConfig = any> {
} }
``` ```
برای جزئیات درباره تنظیمات پیش‌فرض، به رابط `DefaultTheme.Config` در [اینجا](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) مراجعه کنید. لطفاً خصوصیات `themeConfig.algolia` یا `themeConfig.carbonAds` را در سطح زبان تغییر ندهید. برای استفاده از جستجوی چندزبانه، به [مستندات Algolia](../reference/default-theme-search#i18n) مراجعه کنید. برای جزئیات درباره تنظیمات پیش‌فرض، به رابط `DefaultTheme.Config` در [اینجا](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) مراجعه کنید. لطفاً خصوصیات `themeConfig.algolia` یا `themeConfig.carbonAds` را در سطح زبان تغییر ندهید. برای استفاده از جستجوی چندزبانه، به [مستندات Algolia](../reference/default-theme-search#algolia-search-i18n) مراجعه کنید.
**نکته حرفه‌ای:** فایل پیکربندی را می‌توانید در `docs/.vitepress/config/index.ts` نیز ذخیره کنید. این کار به شما کمک می‌کند که با ایجاد یک فایل پیکربندی برای هر زبان و سپس ادغام و صدور آنها از `index.ts`، موارد را سازماندهی کنید. **نکته حرفه‌ای:** فایل پیکربندی را می‌توانید در `docs/.vitepress/config/index.ts` نیز ذخیره کنید. این کار به شما کمک می‌کند که با ایجاد یک فایل پیکربندی برای هر زبان و سپس ادغام و صدور آنها از `index.ts`، موارد را سازماندهی کنید.

@ -53,7 +53,7 @@ interface LocaleSpecificConfig<ThemeConfig = any> {
} }
``` ```
デフォルトテーマのプレースホルダ文言のカスタマイズについては [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) を参照してください。`themeConfig.algolia` や `themeConfig.carbonAds` をロケール単位で上書きしないでください。多言語検索の利用は [Algolia ドキュメント](../reference/default-theme-search#i18n) を参照。 デフォルトテーマのプレースホルダ文言のカスタマイズについては [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) を参照してください。`themeConfig.algolia` や `themeConfig.carbonAds` をロケール単位で上書きしないでください。多言語検索の利用は [Algolia ドキュメント](../reference/default-theme-search#algolia-search-i18n) を参照。
**プロ向けヒント:** 設定ファイルは `docs/.vitepress/config/index.ts` に置くこともできます。ロケールごとに設定ファイルを作成して、`index.ts` でマージ・エクスポートすると整理しやすくなります。 **プロ向けヒント:** 設定ファイルは `docs/.vitepress/config/index.ts` に置くこともできます。ロケールごとに設定ファイルを作成して、`index.ts` でマージ・エクスポートすると整理しやすくなります。

@ -53,7 +53,7 @@ interface LocaleSpecificConfig<ThemeConfig = any> {
} }
``` ```
기본 테마의 플레이스홀더 텍스트를 커스텀하는 방법에 대한 자세한 내용은 [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) 인터페이스를 참고하십시오. 로케일 수준에서 `themeConfig.algolia` 또는 `themeConfig.carbonAds`를 재정의하지 마십시오. 다국어 검색을 사용하려면 [Algolia docs](../reference/default-theme-search#i18n)를 참조하십시오. 기본 테마의 플레이스홀더 텍스트를 커스텀하는 방법에 대한 자세한 내용은 [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) 인터페이스를 참고하십시오. 로케일 수준에서 `themeConfig.algolia` 또는 `themeConfig.carbonAds`를 재정의하지 마십시오. 다국어 검색을 사용하려면 [Algolia docs](../reference/default-theme-search#algolia-search-i18n)를 참조하십시오.
**전문가 팁:** 구성 파일은 `docs/.vitepress/config/index.ts`로 저장할 수도 있습니다. 로케일마다 구성 파일을 생성한 후 `index.ts`에서 병합하고 내보내어 내용을 구성하는 데 도움이 될 수 있습니다. **전문가 팁:** 구성 파일은 `docs/.vitepress/config/index.ts`로 저장할 수도 있습니다. 로케일마다 구성 파일을 생성한 후 `index.ts`에서 병합하고 내보내어 내용을 구성하는 데 도움이 될 수 있습니다.

@ -53,7 +53,7 @@ interface LocaleSpecificConfig<ThemeConfig = any> {
} }
``` ```
Consulte a interface [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) para obter detalhes sobre a personalização dos textos marcadores do tema padrão. Não substitua `themeConfig.algolia` ou `themeConfig.carbonAds` no nível do idioma. Consulte a [documentação Algolia](../reference/default-theme-search#i18n) para usar a pesquisa multilínguas. Consulte a interface [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) para obter detalhes sobre a personalização dos textos marcadores do tema padrão. Não substitua `themeConfig.algolia` ou `themeConfig.carbonAds` no nível do idioma. Consulte a [documentação Algolia](../reference/default-theme-search#algolia-search-i18n) para usar a pesquisa multilínguas.
**Dica profissional:** O arquivo de configuração pode ser armazenado em `docs/.vitepress/config/index.ts` também. Isso pode ajudar a organizar as coisas criando um arquivo de configuração por idioma e então mesclá-los e exportá-los a partir de `index.ts`. **Dica profissional:** O arquivo de configuração pode ser armazenado em `docs/.vitepress/config/index.ts` também. Isso pode ajudar a organizar as coisas criando um arquivo de configuração por idioma e então mesclá-los e exportá-los a partir de `index.ts`.

@ -53,7 +53,7 @@ interface LocaleSpecificConfig<ThemeConfig = any> {
} }
``` ```
有关自定义默认主题的文本占位符的信息,请参考 [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) 接口。不要在 locale 级别覆盖 `themeConfig.algolia` 或 `themeConfig.carbonAds`。想获取多语言搜索的信息,请参考 [Algolia 文档](../reference/default-theme-search#i18n)。 有关自定义默认主题的文本占位符的信息,请参考 [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) 接口。不要在 locale 级别覆盖 `themeConfig.algolia` 或 `themeConfig.carbonAds`。想获取多语言搜索的信息,请参考 [Algolia 文档](../reference/default-theme-search#algolia-search-i18n)。
**提示**:配置文件也可以是 `docs/.vitepress/config/index.ts`。通过为每个语言环境创建一个配置文件,然后从 `index.ts` 合并并导出它们,可以更好地组织文件。 **提示**:配置文件也可以是 `docs/.vitepress/config/index.ts`。通过为每个语言环境创建一个配置文件,然后从 `index.ts` 合并并导出它们,可以更好地组织文件。

@ -1,5 +1,12 @@
import { tryOnUnmounted } from '@vueuse/core' import { tryOnUnmounted } from '@vueuse/core'
import { h, onMounted, shallowRef, type AsyncComponentLoader } from 'vue' import {
h,
isVNode,
onMounted,
shallowRef,
type AsyncComponentLoader,
type SetupContext
} from 'vue'
import { import {
EXTERNAL_URL_RE, EXTERNAL_URL_RE,
@ -116,7 +123,7 @@ export function defineClientComponent(
cb?: () => Awaitable<void> cb?: () => Awaitable<void>
) { ) {
return { return {
setup() { setup(_: unknown, { slots }: SetupContext) {
const comp = shallowRef() const comp = shallowRef()
onMounted(async () => { onMounted(async () => {
let res = await loader() let res = await loader()
@ -127,7 +134,23 @@ export function defineClientComponent(
comp.value = res comp.value = res
await cb?.() await cb?.()
}) })
return () => (comp.value ? h(comp.value, ...(args ?? [])) : null) return () => {
if (!comp.value) return null
const props = args?.[0]
// Forward slots unless args already supply children in an h() overload.
if (
(!args || args.length < 2) &&
(props == null ||
(typeof props === 'object' &&
!Array.isArray(props) &&
!isVNode(props)))
) {
return h(comp.value, props, slots)
}
return h(comp.value, ...(args ?? []))
}
} }
} }
} }

@ -167,7 +167,7 @@ useEventListener('pointerdown', (e) => {
.text { .text {
display: flex; display: flex;
align-items: center; align-items: center;
line-height: var(--vp-nav-height); line-height: 1.5;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
color: var(--vp-c-text-1); color: var(--vp-c-text-1);

@ -305,6 +305,7 @@ const { heroImageSlotExists } = inject(
width: 12rem; width: 12rem;
height: 12rem; height: 12rem;
background-image: var(--vp-home-hero-image-background-image); background-image: var(--vp-home-hero-image-background-image);
will-change: filter;
filter: var(--vp-home-hero-image-filter); filter: var(--vp-home-hero-image-filter);
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }

@ -23,13 +23,13 @@ defineOptions({ inheritAttrs: false })
<VPImage <VPImage
class="dark" class="dark"
:image="image.dark" :image="image.dark"
:alt="image.alt" :alt="alt ?? image.alt"
v-bind="$attrs" v-bind="$attrs"
/> />
<VPImage <VPImage
class="light" class="light"
:image="image.light" :image="image.light"
:alt="image.alt" :alt="alt ?? image.alt"
v-bind="$attrs" v-bind="$attrs"
/> />
</template> </template>

@ -439,6 +439,7 @@
} }
.vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) { .vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) {
will-change: filter;
filter: blur(0.095rem); filter: blur(0.095rem);
opacity: 0.4; opacity: 0.4;
transition: transition:

@ -138,7 +138,13 @@ function createOpenRender(
if (noTitle) return `<div ${renderedAttrs}>\n` if (noTitle) return `<div ${renderedAttrs}>\n`
const title = md.renderInline( const title = md.renderInline(
info || titlesFor(titles, env.localeIndex)[name], info || titlesFor(titles, env.localeIndex)[name],
{ references: env.references, frontmatter: env.frontmatter } {
references: env.references,
frontmatter: env.frontmatter,
cleanUrls: env.cleanUrls,
relativePath: env.relativePath,
relativizeUrls: env.relativizeUrls
}
) )
if (name === 'details') if (name === 'details')
return `<details ${renderedAttrs}><summary>${title}</summary>\n` return `<details ${renderedAttrs}><summary>${title}</summary>\n`

@ -59,7 +59,9 @@ export function clearCache(relativePath?: string) {
return return
} }
cache.find((_, key) => key.endsWith(`:${relativePath}`) && cache.delete(key)) for (const key of cache.keys()) {
if (key.endsWith(`:${relativePath}`)) cache.delete(key)
}
} }
function normalizeDriveLetter(file: string) { function normalizeDriveLetter(file: string) {

Loading…
Cancel
Save