From 7dbceaa98d47e5d79e4d16924b149721363df532 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sat, 9 Jul 2022 17:52:49 +0530 Subject: [PATCH] fix: make hmr work with imported code snippets and included markdown files --- src/node/markdown/plugins/snippet.ts | 6 +- src/node/markdownToVue.ts | 228 ++++++++++++++------------- src/node/plugin.ts | 26 ++- 3 files changed, 147 insertions(+), 113 deletions(-) diff --git a/src/node/markdown/plugins/snippet.ts b/src/node/markdown/plugins/snippet.ts index c8e521534ebe..9ccf6bdfaf16 100644 --- a/src/node/markdown/plugins/snippet.ts +++ b/src/node/markdown/plugins/snippet.ts @@ -135,15 +135,15 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => { const fence = md.renderer.rules.fence! md.renderer.rules.fence = (...args) => { - const [tokens, idx, , { loader }] = args + const [tokens, idx, , { includes }] = args const token = tokens[idx] // @ts-ignore const tokenSrc = token.src const [src, regionName] = tokenSrc ? tokenSrc.split('#') : [''] if (src) { - if (loader) { - loader.addDependency(src) + if (includes) { + includes.push(src) } const isAFile = fs.lstatSync(src).isFile() if (fs.existsSync(src) && isAFile) { diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts index e61a5bb2092e..ec907b64343a 100644 --- a/src/node/markdownToVue.ts +++ b/src/node/markdownToVue.ts @@ -37,126 +37,138 @@ export async function createMarkdownToVueRenderFn( const replaceRegex = genReplaceRegexp(userDefines, isBuild) - return async ( - src: string, - file: string, - publicDir: string - ): Promise => { - const relativePath = slash(path.relative(srcDir, file)) - const dir = path.dirname(file) - const cacheKey = JSON.stringify({ src, file }) - - const cached = cache.get(cacheKey) - if (cached) { - debug(`[cache hit] ${relativePath}`) - return cached - } + return { + markdownToVue: async ( + src: string, + file: string, + publicDir: string + ): Promise => { + const relativePath = slash(path.relative(srcDir, file)) + const dir = path.dirname(file) + const cacheKey = JSON.stringify({ src, file }) - const start = Date.now() - - // resolve includes - let includes: string[] = [] - src = src.replace(includesRE, (m, m1) => { - try { - const includePath = path.join(dir, m1) - const content = fs.readFileSync(includePath, 'utf-8') - includes.push(slash(includePath)) - return content - } catch (error) { - return m // silently ignore error if file is not present + const cached = cache.get(cacheKey) + if (cached) { + debug(`[cache hit] ${relativePath}`) + return cached } - }) - - const { content, data: frontmatter } = matter(src) - - // reset state before render - md.__path = file - md.__relativePath = relativePath - - const html = md.render(content, { - path: file, - relativePath, - cleanUrls, - frontmatter - }) - const data = md.__data - - // validate data.links - const deadLinks: string[] = [] - const recordDeadLink = (url: string) => { - console.warn( - c.yellow( - `\n(!) Found dead link ${c.cyan(url)} in file ${c.white( - c.dim(file) - )}\nIf it is intended, you can use:\n ${c.cyan( - `${url}` - )}` - ) - ) - deadLinks.push(url) - } - if (data.links) { - const dir = path.dirname(file) - for (let url of data.links) { - if (/\.(?!html|md)\w+($|\?)/i.test(url)) continue - - if (url.replace(EXTERNAL_URL_RE, '').startsWith('//localhost:')) { - recordDeadLink(url) - continue + const start = Date.now() + + // resolve includes + let includes: string[] = [] + src = src.replace(includesRE, (m, m1) => { + try { + const includePath = path.join(dir, m1) + const content = fs.readFileSync(includePath, 'utf-8') + includes.push(slash(includePath)) + return content + } catch (error) { + return m // silently ignore error if file is not present } - - url = url.replace(/[?#].*$/, '').replace(/\.(html|md)$/, '') - if (url.endsWith('/')) url += `index` - const resolved = decodeURIComponent( - slash( - url.startsWith('/') - ? url.slice(1) - : path.relative(srcDir, path.resolve(dir, url)) + }) + + const { content, data: frontmatter } = matter(src) + + // reset state before render + md.__path = file + md.__relativePath = relativePath + + const html = md.render(content, { + path: file, + relativePath, + cleanUrls, + frontmatter, + includes + }) + const data = md.__data + + // validate data.links + const deadLinks: string[] = [] + const recordDeadLink = (url: string) => { + console.warn( + c.yellow( + `\n(!) Found dead link ${c.cyan(url)} in file ${c.white( + c.dim(file) + )}\nIf it is intended, you can use:\n ${c.cyan( + `${url}` + )}` ) ) - if ( - !pages.includes(resolved) && - !fs.existsSync(path.resolve(dir, publicDir, `${resolved}.html`)) - ) { - recordDeadLink(url) + deadLinks.push(url) + } + + if (data.links) { + const dir = path.dirname(file) + for (let url of data.links) { + if (/\.(?!html|md)\w+($|\?)/i.test(url)) continue + + if (url.replace(EXTERNAL_URL_RE, '').startsWith('//localhost:')) { + recordDeadLink(url) + continue + } + + url = url.replace(/[?#].*$/, '').replace(/\.(html|md)$/, '') + if (url.endsWith('/')) url += `index` + const resolved = decodeURIComponent( + slash( + url.startsWith('/') + ? url.slice(1) + : path.relative(srcDir, path.resolve(dir, url)) + ) + ) + if ( + !pages.includes(resolved) && + !fs.existsSync(path.resolve(dir, publicDir, `${resolved}.html`)) + ) { + recordDeadLink(url) + } } } - } - const pageData: PageData = { - title: inferTitle(frontmatter, content), - titleTemplate: frontmatter.titleTemplate, - description: inferDescription(frontmatter), - frontmatter, - headers: data.headers || [], - relativePath - } + const pageData: PageData = { + title: inferTitle(frontmatter, content), + titleTemplate: frontmatter.titleTemplate, + description: inferDescription(frontmatter), + frontmatter, + headers: data.headers || [], + relativePath + } - if (includeLastUpdatedData) { - pageData.lastUpdated = await getGitTimestamp(file) - } + if (includeLastUpdatedData) { + pageData.lastUpdated = await getGitTimestamp(file) + } - const vueSrc = - genPageDataCode(data.hoistedTags || [], pageData, replaceRegex).join( - '\n' - ) + - `\n` - - debug(`[render] ${file} in ${Date.now() - start}ms.`) - - const result = { - vueSrc, - pageData, - deadLinks, - includes + const vueSrc = + genPageDataCode(data.hoistedTags || [], pageData, replaceRegex).join( + '\n' + ) + + `\n` + + debug(`[render] ${file} in ${Date.now() - start}ms.`) + + const result = { + vueSrc, + pageData, + deadLinks, + includes + } + cache.set(cacheKey, result) + return result + }, + + invalidateMdCache: (files: string[]) => { + const keys = files.map((file) => JSON.stringify({ file }).slice(1)) + ;[...cache.keys()].forEach((key) => { + if (keys.some((file) => key.endsWith(file))) { + cache.delete(key) + } + }) } - cache.set(cacheKey, result) - return result } } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index f9c38be11e08..843b315162bd 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -50,7 +50,12 @@ export async function createVitePressPlugin( cleanUrls } = siteConfig - let markdownToVue: Awaited> + let markdownToVue: Awaited< + ReturnType + >['markdownToVue'] + let invalidateMdCache: Awaited< + ReturnType + >['invalidateMdCache'] // lazy require plugin-vue to respect NODE_ENV in @vue/compiler-x const vuePlugin = await import('@vitejs/plugin-vue').then((r) => @@ -73,12 +78,17 @@ export async function createVitePressPlugin( let hasDeadLinks = false let config: ResolvedConfig + let moduleMap: Record = {} + const vitePressPlugin: Plugin = { name: 'vitepress', async configResolved(resolvedConfig) { config = resolvedConfig - markdownToVue = await createMarkdownToVueRenderFn( + let { + markdownToVue: _markdownToVue, + invalidateMdCache: _invalidateMdCache + } = await createMarkdownToVueRenderFn( srcDir, markdown, pages, @@ -88,6 +98,8 @@ export async function createVitePressPlugin( lastUpdated, cleanUrls ) + markdownToVue = _markdownToVue + invalidateMdCache = _invalidateMdCache }, config() { @@ -149,6 +161,9 @@ export async function createVitePressPlugin( } if (includes.length) { includes.forEach((i) => { + i = slash(i) + if (!moduleMap[i]) moduleMap[i] = [id] + else if (!moduleMap[i].includes(id)) moduleMap[i].push(id) this.addWatchFile(i) }) } @@ -290,6 +305,13 @@ export async function createVitePressPlugin( // overwrite src so vue plugin can handle the HMR ctx.read = () => vueSrc } + + if (moduleMap[file]) { + invalidateMdCache(moduleMap[file]) + return moduleMap[file].map( + (id) => server.moduleGraph.getModuleById(id)! + ) + } } }