diff --git a/docs/.vuepress/notes.ts b/docs/.vuepress/notes.ts index 0d49d09c..97712243 100644 --- a/docs/.vuepress/notes.ts +++ b/docs/.vuepress/notes.ts @@ -1,6 +1,8 @@ import { definePlumeNotesConfig } from '@vuepress-plume/vuepress-theme-plume' export default definePlumeNotesConfig({ + dir: 'notes', + link: '/note', notes: [ { text: '', @@ -10,22 +12,22 @@ export default definePlumeNotesConfig({ '', { text: '指南', - children: ['快速开始', '编写文章'], + items: ['快速开始', '编写文章'], }, { text: '配置', - children: [ + items: [ { text: '主题配置', link: '主题配置', - children: ['主题插件配置', 'notes配置'], + items: ['主题插件配置', 'notes配置'], }, '页面配置', ], }, { text: '功能', - children: ['基础功能', 'markdown增强'], + items: ['基础功能', 'markdown增强'], }, ], }, @@ -39,7 +41,7 @@ export default definePlumeNotesConfig({ dir: 'netlify-functions', text: 'plugin-netlify-functions', link: 'netlify-functions', - children: ['', '介绍', '使用', '功能', 'API', 'functions开发指南'], + items: ['', '介绍', '使用', '功能', 'API', 'functions开发指南'], }, ], }, diff --git a/packages/plugin-blog-data/src/client/composables/useBlogPostData.ts b/packages/plugin-blog-data/src/client/composables/useBlogPostData.ts index 6e8290d0..222caea7 100644 --- a/packages/plugin-blog-data/src/client/composables/useBlogPostData.ts +++ b/packages/plugin-blog-data/src/client/composables/useBlogPostData.ts @@ -5,13 +5,13 @@ import type { BlogPostData } from '../../shared/index.js' declare const __VUE_HMR_RUNTIME__: Record -export type ThemeDataRef = Ref +export type BlogDataRef = Ref -export const blogPostData: ThemeDataRef = ref(blogPostDataRaw) +export const blogPostData: BlogDataRef = ref(blogPostDataRaw) export const useBlogPostData = < T extends BlogPostData = BlogPostData ->(): ThemeDataRef => blogPostData as ThemeDataRef +>(): BlogDataRef => blogPostData as BlogDataRef if (import.meta.webpackHot || import.meta.hot) { __VUE_HMR_RUNTIME__.updateBlogData = (data: BlogPostData) => { diff --git a/packages/plugin-notes-data/package.json b/packages/plugin-notes-data/package.json index 9ecc79ba..20d745c4 100644 --- a/packages/plugin-notes-data/package.json +++ b/packages/plugin-notes-data/package.json @@ -30,10 +30,14 @@ "ts:watch": "tsc -b tsconfig.build.json --watch" }, "dependencies": { + "@vue/devtools-api": "^6.4.5", "@vuepress/client": "2.0.0-beta.60", "@vuepress/core": "2.0.0-beta.60", "@vuepress/shared": "2.0.0-beta.60", - "@vuepress/utils": "2.0.0-beta.60" + "@vuepress/utils": "2.0.0-beta.60", + "chokidar": "^3.5.3", + "create-filter": "^1.0.0", + "vue": "^3.2.47" }, "publishConfig": { "access": "public" diff --git a/packages/plugin-notes-data/src/client/clientConfig.ts b/packages/plugin-notes-data/src/client/clientConfig.ts index a726b269..fc251e8f 100644 --- a/packages/plugin-notes-data/src/client/clientConfig.ts +++ b/packages/plugin-notes-data/src/client/clientConfig.ts @@ -1,7 +1,37 @@ +import { setupDevtoolsPlugin } from '@vue/devtools-api' import { defineClientConfig } from '@vuepress/client' +import { useNotesData } from './composables/index.js' + +declare const __VUE_PROD_DEVTOOLS__: boolean export default defineClientConfig({ - setup() { - // do something + enhance({ app }) { + const notesData = useNotesData() + + // setup devtools in dev mode + if (__VUEPRESS_DEV__ || __VUE_PROD_DEVTOOLS__) { + setupDevtoolsPlugin( + { + // fix recursive reference + app: app as any, + id: 'org.vuepress-plume.plugin-notes-data', + label: 'VuePress Notes Data Plugin', + packageName: '@vuepress/plugin-notes-data', + homepage: 'https://pengzhanbo.cn', + logo: 'https://v2.vuepress.vuejs.org/images/hero.png', + componentStateTypes: ['VuePress'], + }, + (api) => { + api.on.inspectComponent((payload) => { + payload.instanceData.state.push({ + type: 'VuePress', + key: 'notesData', + editable: false, + value: notesData.value, + }) + }) + } + ) + } }, }) diff --git a/packages/plugin-notes-data/src/client/composables/index.ts b/packages/plugin-notes-data/src/client/composables/index.ts new file mode 100644 index 00000000..6434f5b2 --- /dev/null +++ b/packages/plugin-notes-data/src/client/composables/index.ts @@ -0,0 +1 @@ +export * from './notesDate.js' diff --git a/packages/plugin-notes-data/src/client/composables/notesDate.ts b/packages/plugin-notes-data/src/client/composables/notesDate.ts new file mode 100644 index 00000000..c9d54087 --- /dev/null +++ b/packages/plugin-notes-data/src/client/composables/notesDate.ts @@ -0,0 +1,20 @@ +import { notesData as notesDataRaw } from '@internal/notesData' +import { ref } from 'vue' +import type { Ref } from 'vue' +import type { NotesData } from '../../shared/index.js' + +declare const __VUE_HMR_RUNTIME__: Record + +export type NotesDataRef = Ref + +export const notesData: NotesDataRef = ref(notesDataRaw) + +export const useNotesData = < + T extends NotesData = NotesData +>(): NotesDataRef => notesData as NotesDataRef + +if (import.meta.webpackHot || import.meta.hot) { + __VUE_HMR_RUNTIME__.updateNotesData = (data: NotesData) => { + notesData.value = data + } +} diff --git a/packages/plugin-notes-data/src/client/index.ts b/packages/plugin-notes-data/src/client/index.ts index 72593733..f419aa80 100644 --- a/packages/plugin-notes-data/src/client/index.ts +++ b/packages/plugin-notes-data/src/client/index.ts @@ -1 +1,2 @@ -export * from '../shared/index.js' +export { NotesData, NotesSidebarItem } from '../shared/index.js' +export * from './composables/index.js' diff --git a/packages/plugin-notes-data/src/client/notesData.d.ts b/packages/plugin-notes-data/src/client/notesData.d.ts new file mode 100644 index 00000000..f5258ec2 --- /dev/null +++ b/packages/plugin-notes-data/src/client/notesData.d.ts @@ -0,0 +1,7 @@ +import type { NotesData } from '../shared/index.js' + +declare module '@internal/notesData' { + const notesData: NotesData + + export { notesData } +} diff --git a/packages/plugin-notes-data/src/node/plugin.ts b/packages/plugin-notes-data/src/node/plugin.ts index 2665ebb8..1a9b64af 100644 --- a/packages/plugin-notes-data/src/node/plugin.ts +++ b/packages/plugin-notes-data/src/node/plugin.ts @@ -1,12 +1,18 @@ import type { App, Plugin } from '@vuepress/core' -import { path } from '@vuepress/utils' +import { getDirname, path } from '@vuepress/utils' import type { NotesDataOptions } from '../shared/index.js' +import { prepareNotesData, watchNotesData } from './prepareNotesData.js' export const notesDataPlugin = (options: NotesDataOptions): Plugin => { return (app: App) => { return { name: '@vuepress-plume/vuepress-plugin-notes-data', - clientConfigFile: path.resolve(__dirname, '../client/clientConfig.js'), + clientConfigFile: path.resolve( + getDirname(import.meta.url), + '../client/clientConfig.js' + ), + onPrepared: () => prepareNotesData(app, options), + onWatched: (app, watchers) => watchNotesData(app, watchers, options), } } } diff --git a/packages/plugin-notes-data/src/node/prepareNotesData.ts b/packages/plugin-notes-data/src/node/prepareNotesData.ts new file mode 100644 index 00000000..dd3e5b71 --- /dev/null +++ b/packages/plugin-notes-data/src/node/prepareNotesData.ts @@ -0,0 +1,159 @@ +import path from 'node:path' +import type { App } from '@vuepress/core' +import * as chokidar from 'chokidar' +import { createFilter } from 'create-filter' +import type { + NotesData, + NotesDataOptions, + NotesItem, + NotesSidebar, + NotesSidebarItem, +} from '../shared/index.js' +import { ensureArray } from './utils.js' + +const HMR_CODE = ` +if (import.meta.webpackHot) { + import.meta.webpackHot.accept() + if (__VUE_HMR_RUNTIME__.updateNotesData) { + __VUE_HMR_RUNTIME__.updateNotesData(notesData) + } +} + +if (import.meta.hot) { + import.meta.hot.accept(({ notesData }) => { + __VUE_HMR_RUNTIME__.updateNotesData(notesData) + }) +} +` + +interface NotePage { + relativePath: string + title: string + link: string +} + +export const prepareNotesData = async ( + app: App, + { include, exclude, notes, dir, link }: NotesDataOptions +) => { + if (!notes || notes.length === 0) return + const filter = createFilter(ensureArray(include), ensureArray(exclude), { + resolve: false, + }) + const DIR_PATTERN = new RegExp(`^${path.join(dir, '/')}`) + const notesPageList: NotePage[] = app.pages + .filter( + (page) => + page.filePathRelative && + page.filePathRelative.startsWith(dir) && + filter(page.filePathRelative) + ) + .map((page) => { + return { + relativePath: page.filePathRelative?.replace(DIR_PATTERN, '') || '', + title: page.title, + link: page.path, + } + }) + + const notesData: NotesData = {} + notes.forEach((note) => { + notesData[path.join('/', link, note.link)] = initSidebar( + note, + notesPageList.filter((page) => + page.relativePath.startsWith(note.dir.trim().replace(/^\/|\/$/g, '')) + ) + ) + }) + let content = ` +export const notesData = ${JSON.stringify(notesData, null, 2)} +` + if (app.env.isDev) { + content += HMR_CODE + } + + await app.writeTemp('internal/notesData.js', content) +} + +export const watchNotesData = ( + app: App, + watchers: any[], + options: NotesDataOptions +): void => { + if (!options.notes || options.notes.length === 0 || !options.dir) return + const dir = path.join('pages', options.dir, '**/*') + const watcher = chokidar.watch(dir, { + cwd: app.dir.temp(), + ignoreInitial: true, + }) + + watcher.on('add', () => prepareNotesData(app, options)) + watcher.on('change', () => prepareNotesData(app, options)) + watcher.on('unlink', () => prepareNotesData(app, options)) + watchers.push(watcher) +} + +function initSidebar(note: NotesItem, pages: NotePage[]): NotesSidebarItem[] { + console.log('pages:', pages) + if (!note.sidebar) return [] + if (note.sidebar === 'auto') return [] + return initSidebarByConfig(note, pages) +} + +function initSidebarByConfig( + { text, link, dir, sidebar }: NotesItem, + pages: NotePage[] +): NotesSidebarItem[] { + return (sidebar as NotesSidebar).map((item) => { + console.log('text: ', text, 's-item: ', item, 'dir: ', dir) + if (typeof item === 'string') { + const current = findNotePage(item, dir, pages) + return { + text: current?.title || text, + link: current?.link, + items: [], + } + } else { + // link = path.join(link || '', item.link || '') + const current = findNotePage(item.link || '', dir, pages) + return { + text: item.text || item.dir || current?.title, + link: current?.link, + items: initSidebarByConfig( + { + link: item.link || '', + text: item.text || '', + sidebar: item.items, + dir: path.join(dir, item.dir || ''), + }, + pages + ), + } + } + }) +} + +function findNotePage( + sidebar: string, + dir: string, + notePageList: NotePage[] +): NotePage | undefined { + if (sidebar === '' || sidebar === 'README.md' || sidebar === 'index.md') { + return notePageList.find((page) => { + const relative = page.relativePath + return ( + relative === path.join(dir, 'README.md') || + relative === path.join(dir, 'index.md') + ) + }) + } else { + return notePageList.find((page) => { + const relative = page.relativePath + return ( + relative === path.join(dir, sidebar) || + relative === path.join(dir, sidebar + '.md') || + page.link === sidebar + ) + }) + } +} diff --git a/packages/plugin-notes-data/src/node/utils.ts b/packages/plugin-notes-data/src/node/utils.ts new file mode 100644 index 00000000..981a3c67 --- /dev/null +++ b/packages/plugin-notes-data/src/node/utils.ts @@ -0,0 +1,5 @@ +export function ensureArray(thing: T | T[] | null | undefined): T[] { + if (Array.isArray(thing)) return thing + if (thing === null || thing === undefined) return [] + return [thing] +} diff --git a/packages/plugin-notes-data/src/shared/index.ts b/packages/plugin-notes-data/src/shared/index.ts index fc318a9b..67d5d9d0 100644 --- a/packages/plugin-notes-data/src/shared/index.ts +++ b/packages/plugin-notes-data/src/shared/index.ts @@ -1,3 +1,25 @@ -export interface NotesDataOptions { - a?: string +export type NotesDataOptions = { + dir: string + link: string + include?: string | string[] + exclude?: string | string[] + notes: NotesItem[] } + +export type NotesItem = { + dir: string + link: string + text: string + sidebar?: NotesSidebar | 'auto' +} + +export type NotesSidebar = (NotesSidebarItem | string)[] + +export type NotesSidebarItem = { + text?: string + link?: string + dir?: string + items?: NotesSidebar +} + +export type NotesData = Record diff --git a/packages/plugin-notes-data/tsconfig.build.json b/packages/plugin-notes-data/tsconfig.build.json index 6bf67375..a08fdad2 100644 --- a/packages/plugin-notes-data/tsconfig.build.json +++ b/packages/plugin-notes-data/tsconfig.build.json @@ -2,7 +2,11 @@ "extends": "../tsconfig.build.json", "compilerOptions": { "rootDir": "./src", - "outDir": "./lib" + "outDir": "./lib", + "paths": { + "@internal/notesData": ["./src/client/notesData.d.ts"] + }, + "types": ["@vuepress/client/types", "vite/client", "webpack-env"] }, "include": ["./src"] } diff --git a/packages/theme/package.json b/packages/theme/package.json index 22235f40..eb23060d 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -39,6 +39,7 @@ "@vuepress-plume/vuepress-plugin-blog-data": "workspace:*", "@vuepress-plume/vuepress-plugin-caniuse": "workspace:*", "@vuepress-plume/vuepress-plugin-copy-code": "workspace:*", + "@vuepress-plume/vuepress-plugin-notes-data": "workspace:*", "@vuepress/client": "2.0.0-beta.60", "@vuepress/core": "2.0.0-beta.60", "@vuepress/plugin-active-header-links": "2.0.0-beta.60", diff --git a/packages/theme/src/node/defineConfig.ts b/packages/theme/src/node/defineConfig.ts index 27fd166e..4d7ca91c 100644 --- a/packages/theme/src/node/defineConfig.ts +++ b/packages/theme/src/node/defineConfig.ts @@ -1,12 +1,10 @@ import type { - PlumeThemeNotesItem, - PlumeThemeNotesOptions, -} from '../shared/index.js' + NotesDataOptions, + NotesItem, +} from '@vuepress-plume/vuepress-plugin-notes-data' export const definePlumeNotesConfig = ( - notes: PlumeThemeNotesOptions -): PlumeThemeNotesOptions => notes + notes: NotesDataOptions +): NotesDataOptions => notes -export const definePlumeNotesItemConfig = ( - item: PlumeThemeNotesItem -): PlumeThemeNotesItem => item +export const definePlumeNotesItemConfig = (item: NotesItem): NotesItem => item diff --git a/packages/theme/src/node/plugins.ts b/packages/theme/src/node/plugins.ts index 4ce3adb9..c2f17666 100644 --- a/packages/theme/src/node/plugins.ts +++ b/packages/theme/src/node/plugins.ts @@ -3,6 +3,7 @@ import { baiduTongjiPlugin } from '@vuepress-plume/vuepress-plugin-baidu-tongji' import { blogDataPlugin } from '@vuepress-plume/vuepress-plugin-blog-data' import { caniusePlugin } from '@vuepress-plume/vuepress-plugin-caniuse' import { copyCodePlugin } from '@vuepress-plume/vuepress-plugin-copy-code' +import { notesDataPlugin } from '@vuepress-plume/vuepress-plugin-notes-data' import type { App, PluginConfig } from '@vuepress/core' import { activeHeaderLinksPlugin } from '@vuepress/plugin-active-header-links' import { docsearchPlugin } from '@vuepress/plugin-docsearch' @@ -39,6 +40,7 @@ export const setupPlugins = ( include: ['**/*.md'], exclude: ['**/{README,index}.md', 'notes/**'], }), + localeOptions.notes ? notesDataPlugin(localeOptions.notes) : [], activeHeaderLinksPlugin({ headerLinkSelector: 'a.theme-plume-toc-link', headerAnchorSelector: '.header-anchor', diff --git a/packages/theme/src/shared/options/index.ts b/packages/theme/src/shared/options/index.ts index 588243bc..3363b0e1 100644 --- a/packages/theme/src/shared/options/index.ts +++ b/packages/theme/src/shared/options/index.ts @@ -39,10 +39,6 @@ export interface PlumeThemeOptions extends PlumeThemeLocaleOptions { */ exclude?: string[] } - - notes?: { - dir?: string - } } export type PlumeThemeLocaleOptions = PlumeThemeData diff --git a/packages/theme/src/shared/options/locale.ts b/packages/theme/src/shared/options/locale.ts index dba9f9f7..3183df1a 100644 --- a/packages/theme/src/shared/options/locale.ts +++ b/packages/theme/src/shared/options/locale.ts @@ -1,3 +1,4 @@ +import type { NotesDataOptions } from '@vuepress-plume/vuepress-plugin-notes-data' import type { LocaleData } from '@vuepress/core' // import type { NavbarConfig, NavLink } from '../layout/index.js' // import type { PlumeThemeNotesOptions } from './notes.js' @@ -5,7 +6,6 @@ import type { LocaleData } from '@vuepress/core' // todo type type NavbarConfig = any type NavLink = any -type PlumeThemeNotesOptions = any export interface PlumeThemeAvatar { /** @@ -135,7 +135,7 @@ export interface PlumeThemeLocaleData extends LocaleData { * * 注:你也可以将notes配置到navbar中,默认自动生成在右侧栏目中 */ - notes?: false | PlumeThemeNotesOptions + notes?: false | NotesDataOptions footer?: false | { content: string; copyright: string } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17881d15..800d7f5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,15 +213,23 @@ importers: packages/plugin-notes-data: specifiers: + '@vue/devtools-api': ^6.4.5 '@vuepress/client': 2.0.0-beta.60 '@vuepress/core': 2.0.0-beta.60 '@vuepress/shared': 2.0.0-beta.60 '@vuepress/utils': 2.0.0-beta.60 + chokidar: ^3.5.3 + create-filter: ^1.0.0 + vue: ^3.2.47 dependencies: + '@vue/devtools-api': 6.4.5 '@vuepress/client': 2.0.0-beta.60 '@vuepress/core': 2.0.0-beta.60 '@vuepress/shared': 2.0.0-beta.60 '@vuepress/utils': 2.0.0-beta.60 + chokidar: 3.5.3 + create-filter: 1.0.0 + vue: 3.2.47 packages/plugin-page-collection: specifiers: @@ -271,6 +279,7 @@ importers: '@vuepress-plume/vuepress-plugin-blog-data': workspace:* '@vuepress-plume/vuepress-plugin-caniuse': workspace:* '@vuepress-plume/vuepress-plugin-copy-code': workspace:* + '@vuepress-plume/vuepress-plugin-notes-data': workspace:* '@vuepress/client': 2.0.0-beta.60 '@vuepress/core': 2.0.0-beta.60 '@vuepress/plugin-active-header-links': 2.0.0-beta.60 @@ -305,6 +314,7 @@ importers: '@vuepress-plume/vuepress-plugin-blog-data': link:../plugin-blog-data '@vuepress-plume/vuepress-plugin-caniuse': link:../plugin-caniuse '@vuepress-plume/vuepress-plugin-copy-code': link:../plugin-copy-code + '@vuepress-plume/vuepress-plugin-notes-data': link:../plugin-notes-data '@vuepress/client': 2.0.0-beta.60 '@vuepress/core': 2.0.0-beta.60 '@vuepress/plugin-active-header-links': 2.0.0-beta.60 diff --git a/tsconfig.json b/tsconfig.json index 146ba7e4..db24b07a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,9 @@ "@internal/blogData": [ "./packages/plugin-blog-data/src/client/blogPostData.d.ts" ], + "@internal/notesData": [ + "./packages/plugin-notes-data/src/client/notesData.d.ts" + ], "@internal/*": ["./docs/.vuepress/.temp/internal/*"], "@vuepress-plume/vuepress-*": ["./packages/*/src/node/index.ts"], "@vuepress-plume/vuepress-theme-plume": [