feat(devtools): can dev devtools hmr with demo

This commit is contained in:
YunYouJun 2024-12-22 02:54:44 +08:00
parent 2b65948a9a
commit a1919d1462
57 changed files with 1153 additions and 4028 deletions

View File

@ -2,7 +2,7 @@
"name": "valaxy-theme-custom",
"version": "0.0.0",
"private": true,
"packageManager": "pnpm@9.15.0",
"packageManager": "pnpm@9.15.1",
"author": {
"email": "me@yunyoujun.cn",
"name": "YunYouJun",

1
demo/yun/.env Normal file
View File

@ -0,0 +1 @@
VITE_VALAXY_DEVTOOLS_DEV=true

View File

@ -6,8 +6,6 @@ import { addonBangumi } from 'valaxy-addon-bangumi'
import { addonComponents } from 'valaxy-addon-components'
import { addonLightGallery } from 'valaxy-addon-lightgallery'
import { addonTest } from 'valaxy-addon-test'
import { addonTwikoo } from 'valaxy-addon-twikoo'
import { addonWaline } from 'valaxy-addon-waline'
// import { addonMeting } from 'valaxy-addon-meting'
@ -90,14 +88,15 @@ export default defineValaxyConfig<ThemeConfig>({
addonComponents(),
// comments
addonWaline({
serverURL: 'https://waline.yunyoujun.cn',
pageview: true,
comment: true,
}),
addonTwikoo({
envId: 'https://twikoo.vercel.app',
}),
// addonWaline({
// serverURL: 'https://waline.yunyoujun.cn',
// pageview: true,
// comment: true,
// }),
// addonTwikoo({
// envId: 'https://twikoo.vercel.app',
// }),
addonLightGallery(),
// addonMeting({

View File

@ -17,7 +17,7 @@
"valaxy": "link:../packages/valaxy",
"valaxy-addon-algolia": "link:../packages/valaxy-addon-algolia",
"valaxy-addon-components": "workspace:*",
"valaxy-addon-git-log": "^0.1.1",
"valaxy-addon-git-log": "^0.2.0",
"valaxy-theme-press": "link:../packages/valaxy-theme-press"
},
"devDependencies": {

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "0.22.4",
"private": true,
"packageManager": "pnpm@9.15.0",
"packageManager": "pnpm@9.15.1",
"description": "📄 Vite & Vue powered static blog generator.",
"author": {
"email": "me@yunyoujun.cn",
@ -95,7 +95,6 @@
"@types/resolve": "^1.20.6",
"bumpp": "^9.9.1",
"cross-env": "^7.0.3",
"decap-cms-app": "^3.4.0",
"eslint": "^9.17.0",
"https-localhost": "^4.7.1",
"husky": "^9.1.7",

View File

@ -11,3 +11,12 @@ pnpm dev
# in root
pnpm devtools
```
```bash
# 启动 demo查看 devtools代理至开发 devtools 的端口以获取热更新)
pnpm demo:vite
```
```bash [.env]
VITE_VALAXY_DEVTOOLS_DEV=true
```

View File

@ -8,9 +8,12 @@ export default defineBuildConfig({
declaration: true,
externals: [
// in valaxy
'valaxy',
'vite',
'gray-matter',
'fs-extra',
'fast-glob',
'consola',
],
rollup: {
dts: {

View File

@ -19,7 +19,7 @@
"build": "rimraf dist && run-s build:*",
"build:client": "vite build src/client",
"build:node": "unbuild",
"dev": "npm run stub && npm run dev:client",
"dev": "npm run dev:client",
"dev:client": "vite dev src/client --port 5001",
"watch:client": "vite build src/client --watch",
"stub": "unbuild --stub",
@ -30,21 +30,26 @@
"@rollup/pluginutils": "^5.1.4",
"axios": "^1.7.9",
"body-parser": "^2.0.2",
"cors": "^2.8.5",
"http-proxy-middleware": "^3.0.3",
"js-yaml": "^4.1.0",
"picocolors": "^1.1.1",
"sirv": "^3.0.0"
"sirv": "^3.0.0",
"vite-dev-rpc": "^0.1.7"
},
"devDependencies": {
"@advjs/gui": "0.0.7-beta.1",
"@iconify-json/ri": "catalog:",
"@types/body-parser": "^1.19.5",
"@types/splitpanes": "^2.2.6",
"@types/wicg-file-system-access": "^2023.10.5",
"gray-matter": "^4.0.3",
"splitpanes": "^3.1.5",
"typescript": "catalog:",
"unbuild": "catalog:",
"unplugin-vue-router": "^0.10.9",
"vite": "catalog:"
"vite": "catalog:",
"vue-i18n": "^10.0.5",
"zod": "^3.24.1"
}
}

View File

@ -1,20 +1,3 @@
<script lang="ts" setup>
import { onMounted } from 'vue'
import VDHeader from './components/VDHeader.vue'
import { initDevtoolsClient } from './utils'
onMounted(() => {
initDevtoolsClient()
})
</script>
<template>
<main class="h-full" flex="~ col" text="gray-700 dark:gray-200">
<VDHeader />
<!-- <RouterLink to="/about">About</RouterLink> -->
<div style="height: calc(100% - 32px)" overflow="auto">
<RouterView />
</div>
</main>
<ValaxyDevtools />
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
defineProps<{
tag?: string
}>()
</script>
<template>
<component
:is="tag || 'button'"
class="bg-white dark:bg-gray-900 inline-flex justify-center items-center size-8 hover:bg-gray-200 dark:hover:bg-gray-800"
>
<slot />
</component>
</template>

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { toggleDark } from '../composables/dark'
const { t } = useI18n()
</script>
<template>
<MenuBarBtn :title="t('button.toggle_dark')" @click="toggleDark()">
<div i="carbon-sun dark:carbon-moon" />
</MenuBarBtn>
</template>

View File

@ -47,11 +47,13 @@ const routeMenus = [
<div flex="1" />
<a
<SelectRootDir />
<ToggleDark />
<MenuBarBtn
tag="a"
href="https://valaxy.site" target="_blank"
class="bg-white dark:bg-gray-900 inline-flex justify-center items-center w-8 h-8 hover:bg-gray-200"
>
<div i-ri-book-line />
</a>
</MenuBarBtn>
</div>
</template>

View File

@ -16,10 +16,10 @@ function onClickPost(post: any) {
<template>
<ul class="h-full" overflow="auto" pl="12" pr="4" py="4">
<li v-for="post in postList" :key="post.path" class="list-decimal">
<li v-for="post in postList" :key="post.path" class="list-decimal text-sm">
<div flex>
<span
class="inline-flex flex-grow cursor-pointer hover:text-blue-500 text-xs"
class="inline-flex flex-grow cursor-pointer hover:text-blue-500"
:class="{ 'text-blue-500': activePath === post.path }"
@click="onClickPost(post)"
>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { onMounted } from 'vue'
import { initDevtoolsClient } from '../utils'
import 'splitpanes/dist/splitpanes.css'
import '../styles/index.scss'
onMounted(() => {
initDevtoolsClient()
})
</script>
<template>
<main class="h-full" flex="~ col" text="gray-700 dark:gray-200">
<VDHeader />
<!-- <RouterLink to="/about">About</RouterLink> -->
<div style="height: calc(100% - 32px)" overflow="auto">
<PageIndex />
</div>
</main>>
</template>

View File

@ -0,0 +1,57 @@
<script setup lang="ts">
import { useDraggable } from '@vueuse/core'
import { ref } from 'vue'
import { useAppStore } from '../stores/app'
const entryRef = ref()
const { style } = useDraggable(entryRef, {
initialValue: {
x: 12,
y: window.innerHeight - 60,
},
})
const app = useAppStore()
</script>
<template>
<div ref="entryRef" :style="style" class="fixed z-9999999999 valaxy-devtools-entry">
<button
class="bg-white size-10 rounded-full shadow hover:shadow-lg transition p-1 z-1 cursor-pointer"
@click="app.isDevtoolsVisible = !app.isDevtoolsVisible"
>
<ValaxySvgLogo />
</button>
</div>
<template v-if="app.isDevtoolsVisible">
<ValaxyOverlay
class="z-9999999998! op-50"
:show="app.isDevtoolsVisible"
@click="app.isDevtoolsVisible = !app.isDevtoolsVisible"
/>
<div
v-if="app.isDevtoolsVisible"
class="valaxy-devtools-container fixed left-0 right-0 bottom-1 z-9999999999 shadow-xl rounded overflow-hidden m-auto"
bg-white dark:bg-dark-800
>
<ValaxyDevtools />
</div>
</template>
</template>
<style lang="scss" scoped>
@use 'sass:map';
@use 'valaxy-theme-yun/styles/vars.scss' as *;
.valaxy-devtools-entry {
position: fixed;
}
.valaxy-devtools-container {
animation-timing-function: map.get($cubic-bezier, "ease-in");
width: min(80vw, -48px + 100vw);
height: min(60vh, -48px + 100vh);
transform: translate(0, 0);
}
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { useAppStore } from '../../stores/app'
const app = useAppStore()
</script>
<template>
<MenuBarBtn @click="app.getDir">
<div i-vscode-icons:default-root-folder-opened />
</MenuBarBtn>
</template>

View File

@ -1,11 +1,16 @@
<script lang="ts" setup>
import consola from 'consola'
import { Pane, Splitpanes } from 'splitpanes'
import { onMounted } from 'vue'
import { isStaticMode } from '../utils'
import { rpc } from '../../rpc'
import { isStaticMode } from '../../utils'
onMounted(() => {
onMounted(async () => {
if (isStaticMode)
document.title = 'Valaxy DevTools (Production)'
const data = await rpc.getPostList()
consola.log('data', data)
})
</script>

View File

@ -0,0 +1,5 @@
import { useDark, usePreferredDark, useToggle } from '@vueuse/core'
// these APIs are auto-imported from @vueuse/core
export const isDark = useDark()
export const toggleDark = useToggle(isDark)
export const preferredDark = usePreferredDark()

View File

@ -0,0 +1,7 @@
button:
about: About
back: Back
go: GO
home: Home
toggle_dark: Toggle dark mode
toggle_langs: Change languages

View File

@ -0,0 +1,7 @@
button:
about: 关于
back: 返回
go: 确定
home: 首页
toggle_dark: 切换深色模式
toggle_langs: 切换语言

View File

@ -1,14 +1,17 @@
import { createPinia } from 'pinia'
// register vue composition api globally
import { createApp, ref } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import { handleHotUpdate, routes } from 'vue-router/auto-routes'
import App from './App.vue'
import { installI18n } from './modules/i18n'
import '@unocss/reset/tailwind.css'
import 'uno.css'
import './styles/index.css'
import 'splitpanes/dist/splitpanes.css'
const app = createApp(App)
@ -33,5 +36,9 @@ if (import.meta.hot) {
handleHotUpdate(router)
}
const pinia = createPinia()
app.use(pinia)
app.use(router)
installI18n(app)
app.mount('#app')

View File

@ -0,0 +1,51 @@
import type { App } from 'vue'
import type { Locale } from 'vue-i18n'
import { createI18n } from 'vue-i18n'
// Import i18n resources
// https://vitejs.dev/guide/features.html#glob-import
//
// Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite
const i18n = createI18n({
legacy: false,
locale: '',
messages: {},
})
const localesMap = Object.fromEntries(
Object.entries(import.meta.glob('../locales/*.yml'))
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]),
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>
export const availableLocales = Object.keys(localesMap)
const loadedLanguages: string[] = []
function setI18nLanguage(lang: Locale) {
i18n.global.locale.value = lang as any
if (typeof document !== 'undefined')
document.querySelector('html')?.setAttribute('lang', lang)
return lang
}
export async function loadLanguageAsync(lang: string): Promise<Locale> {
// If the same language
if (i18n.global.locale.value === lang)
return setI18nLanguage(lang)
// If the language was already loaded
if (loadedLanguages.includes(lang))
return setI18nLanguage(lang)
// If the language hasn't been loaded yet
const messages = await localesMap[lang]()
i18n.global.setLocaleMessage(lang, messages.default)
loadedLanguages.push(lang)
return setI18nLanguage(lang)
}
export function installI18n(app: App) {
app.use(i18n)
loadLanguageAsync('en')
}

View File

@ -0,0 +1,6 @@
import type { ClientFunctions, ServerFunctions } from '../rpc'
import { createRPCClient } from 'vite-dev-rpc'
export const rpc = createRPCClient<ServerFunctions, ClientFunctions>('demo', import.meta.hot, {
// client functions
})

View File

@ -0,0 +1,29 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { ref } from 'vue'
/**
* Global store for users
* @example
* ```ts
* import { useAppStore } from 'valaxy'
* const appStore = useAppStore()
* ```
*/
export const useAppStore = defineStore('app', () => {
async function getDir() {
const dirHandle = await window.showDirectoryPicker()
return dirHandle
}
const isDevtoolsVisible = ref(false)
// a
return {
getDir,
isDevtoolsVisible,
}
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot))

View File

@ -0,0 +1,5 @@
.valaxy-devtools-container {
.splitpanes__splitter {
background-color: rgba(0, 0, 0, 0.15);
}
}

View File

@ -18,7 +18,6 @@ declare module 'vue-router/auto-routes' {
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/about': RouteRecordInfo<'/about', '/about', Record<never, never>, Record<never, never>>,
'/categories': RouteRecordInfo<'/categories', '/categories', Record<never, never>, Record<never, never>>,
'/migration': RouteRecordInfo<'/migration', '/migration', Record<never, never>, Record<never, never>>,

View File

@ -1,87 +1,118 @@
import { join, resolve } from 'node:path'
import path from 'node:path'
import { componentsDir } from '@advjs/gui/node'
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
import Vue from '@vitejs/plugin-vue'
import Unocss from 'unocss/vite'
import VueComponents from 'unplugin-vue-components/vite'
import VueRouter from 'unplugin-vue-router/vite'
import { defineConfig } from 'vite'
import { unoConfig } from '../../../../uno.config'
import { config } from '../config'
import { ValaxyDevtools } from '../node'
export default defineConfig({
base: './',
export default defineConfig(() => {
return {
base: './',
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler',
},
},
},
resolve: {
alias: {
'~/': __dirname,
},
},
plugins: [
{
name: 'local-object-transform',
transform: {
order: 'post',
async handler(code) {
return `${code}\n/* Injected with object hook! */`
},
},
},
{
name: 'generate-error',
load(id) {
if (id === '/__LOAD_ERROR')
throw new Error('Load error')
if (id === '/__TRANSFORM_ERROR')
return 'transform'
},
transform(code, id) {
if (id === '/__TRANSFORM_ERROR')
throw new SyntaxError('Transform error')
},
},
{
name: 'no-change',
transform: {
order: 'post',
async handler(code) {
return code
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler',
},
},
},
VueRouter({
routesFolder: join(__dirname, 'pages'),
dts: join(__dirname, 'typed-routes.d.ts'),
}),
Vue({
include: [/\.vue$/, /\.md$/],
}),
VueComponents({
dirs: ['components', componentsDir],
dts: join(__dirname, 'components.d.ts'),
}),
Unocss(unoConfig),
],
resolve: {
alias: {
'~/': __dirname,
},
},
optimizeDeps: {
exclude: [
'vite-hot-client',
server: {
proxy: {
/**
* 便 localhost:5001
*
* http://localhost:5001/_mockery_api_/xxx => http://localhost:5002/_mockery_api_/xxx
*/
'^/trpc/.*': {
target: `http://localhost:${config.serverPort}`,
changeOrigin: true,
},
},
cors: true,
},
plugins: [
{
name: 'local-object-transform',
transform: {
order: 'post',
async handler(code) {
return `${code}\n/* Injected with object hook! */`
},
},
},
{
name: 'generate-error',
load(id) {
if (id === '/__LOAD_ERROR')
throw new Error('Load error')
if (id === '/__TRANSFORM_ERROR')
return 'transform'
},
transform(code, id) {
if (id === '/__TRANSFORM_ERROR')
throw new SyntaxError('Transform error')
},
},
{
name: 'no-change',
transform: {
order: 'post',
async handler(code) {
return code
},
},
},
VueRouter({
routesFolder: path.join(__dirname, 'pages'),
dts: path.join(__dirname, 'typed-routes.d.ts'),
}),
Vue({
include: [/\.vue$/, /\.md$/],
}),
VueComponents({
dirs: ['components', componentsDir],
dts: path.join(__dirname, 'components.d.ts'),
}),
Unocss(unoConfig),
// https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n
VueI18n({
runtimeOnly: true,
compositionOnly: true,
fullInstall: true,
include: [path.resolve(__dirname, 'locales/**')],
}),
ValaxyDevtools(),
],
},
build: {
target: 'esnext',
outDir: resolve(__dirname, '../../dist/client'),
minify: false, // 'esbuild',
emptyOutDir: true,
},
optimizeDeps: {
exclude: [
'vite-hot-client',
],
},
build: {
target: 'esnext',
outDir: path.resolve(__dirname, '../../dist/client'),
minify: false, // 'esbuild',
emptyOutDir: true,
},
}
})

View File

@ -0,0 +1,4 @@
export const config = {
clientPort: 5001,
serverPort: 5002,
}

View File

@ -1,82 +1,75 @@
import type { ResolvedConfig, ViteDevServer } from 'vite'
/* eslint-disable unused-imports/no-unused-vars */
import type { Connect, ResolvedConfig, ViteDevServer } from 'vite'
import bodyParser from 'body-parser'
import fs from 'fs-extra'
import matter from 'gray-matter'
import { JSON_SCHEMA } from 'js-yaml'
import { migration } from '../utils/migration'
const prefix = '/valaxy-devtools-api'
/**
* migration
* @param path
* @param frontmatter
*/
export async function migration(path: string, frontmatter: { [key: string]: string }) {
if (fs.existsSync(path)) {
const rawMd = await fs.readFile(path, 'utf-8')
const matterFile = matter(rawMd, { schema: JSON_SCHEMA } as any)
let mod = false
for (const key in frontmatter) {
if (key in matterFile.data) {
matterFile.data[frontmatter[key]] = matterFile.data[key]
delete matterFile.data[key]
mod = true
}
}
if (mod) {
const newMd = matter.stringify(matterFile.content, matterFile.data)
await fs.writeFile(path, newMd)
}
}
else {
// console.error(`post not exist:${path}`)
}
}
const apis: {
route: string
fn: Connect.NextHandleFunction
}[] = [
{
route: '/frontmatter',
fn: async (req, res) => {
// update
if (req.method === 'POST') {
const { pageData, frontmatter: newFm } = await (req as any).body
// filePath
const path = pageData.path
if (fs.existsSync(path)) {
const rawMd = await fs.readFile(path, 'utf-8')
const matterFile = matter(rawMd)
// update frontmatter
matterFile.data = newFm
const newMd = matter.stringify(matterFile.content, matterFile.data)
await fs.writeFile(path, newMd)
}
}
},
},
{
route: '/migration',
fn: async (req, res) => {
// update
if (req.method === 'POST') {
const { pageData, frontmatter } = await (req as any).body
// filePath
const worker: Promise<void>[] = []
for (const item of pageData) {
const path = item
worker.push(migration(path, frontmatter))
}
// worker.push(migration(`${userRoot.root}/pages${item}.md`, frontmatter))
// for (const item of pageData)
// worker.push(migration(item, frontmatter))
Promise.all(worker).then(() => {
res.end('ok')
}).catch((_) => {
res.end('migration error')
})
}
},
},
]
/**
* register api in vite.server
* @param server
* @param _viteConfig
*/
export function registerApi(server: ViteDevServer, _viteConfig: ResolvedConfig) {
const app = server.middlewares
app.use(bodyParser.json())
app.use(`${prefix}/frontmatter`, async (req, _res) => {
// update
if (req.method === 'POST') {
const { pageData, frontmatter: newFm } = await (req as any).body
// filePath
const path = pageData.path
if (fs.existsSync(path)) {
const rawMd = await fs.readFile(path, 'utf-8')
const matterFile = matter(rawMd)
// update frontmatter
matterFile.data = newFm
const newMd = matter.stringify(matterFile.content, matterFile.data)
await fs.writeFile(path, newMd)
}
}
})
app.use(`${prefix}/migration`, async (req, _res) => {
// update
if (req.method === 'POST') {
const { pageData, frontmatter } = await (req as any).body
// filePath
const worker: Promise<void>[] = []
for (const item of pageData) {
const path = item
worker.push(migration(path, frontmatter))
}
// worker.push(migration(`${userRoot.root}/pages${item}.md`, frontmatter))
// for (const item of pageData)
// worker.push(migration(item, frontmatter))
Promise.all(worker).then(() => {
_res.end('ok')
}).catch((_) => {
_res.end('migration error')
})
}
apis.forEach(({ route, fn }) => {
app.use(prefix + route, fn)
})
}

View File

@ -0,0 +1,32 @@
import type { ViteDevServer } from 'vite'
import type { ServerFunctions } from '../rpc'
import type { ValaxyDevtoolsOptions } from './types'
import process from 'node:process'
import fg from 'fast-glob'
import fs from 'fs-extra'
import matter from 'gray-matter'
export function getFunctions(server: ViteDevServer, devtoolsOptions: ValaxyDevtoolsOptions): ServerFunctions {
const userRoot = devtoolsOptions.valaxyApp?.options.userRoot || process.cwd()
// const userRoot = GLOBAL_STATE.valaxyApp?.options.userRoot || process.cwd()
// const userRoot = process.cwd()
// const userRoot = server.config.root
return {
async getPostList() {
const files = await fg(`${userRoot}/pages/posts/**/*.md`)
const posts = []
for await (const i of files) {
const md = await fs.readFile(i, 'utf-8')
const { data } = matter(md)
posts.push(data)
}
return {
posts,
root: userRoot,
}
},
}
}

View File

@ -1,30 +1,29 @@
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
import type { ClientFunctions, ServerFunctions } from '../rpc'
import type { ValaxyDevtoolsOptions } from './types'
import { createProxyMiddleware } from 'http-proxy-middleware'
import c from 'picocolors'
import sirv from 'sirv'
import { createRPCServer } from 'vite-dev-rpc'
import { DIR_CLIENT } from '../dir'
import { registerApi } from './api'
import { getFunctions } from './functions'
const NAME = 'valaxy:devtools'
// import.meta.env.VITE_DEV_VALAXY_DEVTOOLS = 'true'
export default function ValaxyDevtools(options: ValaxyDevtoolsOptions = {}): Plugin {
export function ValaxyDevtools(options: ValaxyDevtoolsOptions = {}): Plugin {
let config: ResolvedConfig
const isDevDevtools = import.meta.env?.VITE_VALAXY_DEVTOOLS_DEV === 'true'
function configureServer(server: ViteDevServer) {
const _print = server.printUrls
const base = (options.base ?? server.config.base) || '/'
const functions = getFunctions(server, options)
createRPCServer<ClientFunctions, ServerFunctions>('demo', server.ws, functions)
const devtoolsUrl = `${base}__valaxy_devtools__/`
if (import.meta.env?.VITE_DEV_VALAXY_DEVTOOLS === 'true') {
server.middlewares.use(devtoolsUrl, createProxyMiddleware({
target: 'http://localhost:5001/#/',
changeOrigin: true,
}) as any)
}
else {
if (!isDevDevtools) {
server.middlewares.use(devtoolsUrl, sirv(DIR_CLIENT, {
single: true,
dev: true,
@ -53,6 +52,7 @@ export default function ValaxyDevtools(options: ValaxyDevtoolsOptions = {}): Plu
console.log(` ${c.green('➜')} ${c.bold('Inspect')}: ${colorUrl(`${host}${base}__inspect/`)}`)
}
// register api to vite.server
registerApi(server, config)
}
@ -61,6 +61,8 @@ export default function ValaxyDevtools(options: ValaxyDevtoolsOptions = {}): Plu
enforce: 'pre',
// config: () => { },
configResolved(_config) {
config = _config
},
@ -72,3 +74,5 @@ export default function ValaxyDevtools(options: ValaxyDevtoolsOptions = {}): Plu
return plugin
}
export default ValaxyDevtools

View File

@ -1,4 +1,7 @@
import type { ValaxyApp } from 'valaxy'
export interface ValaxyDevtoolsOptions {
userRoot?: string
base?: string
valaxyApp?: ValaxyApp
}

View File

@ -0,0 +1,30 @@
import fs from 'fs-extra'
import matter from 'gray-matter'
import { JSON_SCHEMA } from 'js-yaml'
/**
* migration
* @param path
* @param frontmatter
*/
export async function migration(path: string, frontmatter: { [key: string]: string }) {
if (fs.existsSync(path)) {
const rawMd = await fs.readFile(path, 'utf-8')
const matterFile = matter(rawMd, { schema: JSON_SCHEMA } as any)
let mod = false
for (const key in frontmatter) {
if (key in matterFile.data) {
matterFile.data[frontmatter[key]] = matterFile.data[key]
delete matterFile.data[key]
mod = true
}
}
if (mod) {
const newMd = matter.stringify(matterFile.content, matterFile.data)
await fs.writeFile(path, newMd)
}
}
else {
// console.error(`post not exist:${path}`)
}
}

15
packages/devtools/src/rpc.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
import type { Post } from 'valaxy'
export interface ServerFunctions {
// add: (a: number, b: number) => number
/**
*
*/
getPostList: () => Promise<{
posts: Post[]
}>
}
export interface ClientFunctions {
// alert: (message: string) => void
}

View File

@ -10,6 +10,7 @@
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"types": [
"@types/wicg-file-system-access",
"vite/client",
"unplugin-vue-router/client"
],

View File

@ -36,7 +36,7 @@ const infoList = computed(() => {
<template>
<div
class="bg-black/50 fixed bottom-2 left-2 p-2 gap-1 rounded z-9999"
class="bg-black/50 fixed bottom-20 left-2 p-2 gap-1 rounded z-9999"
text="xs white" flex="~ col"
>
<div v-for="item in infoList" :key="item.label" class="gap-2 inline-flex">

View File

@ -1,3 +0,0 @@
<template>
<RouterView />
</template>

View File

@ -1,3 +1,15 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
/**
* @see @valaxyjs/devtools
*/
const ValaxyDevtoolsEntry = import.meta.env
? defineAsyncComponent(() => import('@valaxyjs/devtools/src/client/components/ValaxyDevtoolsEntry.vue'))
: () => null
</script>
<template>
<ValaxyApp />
<ValaxyDevtoolsEntry />
</template>

View File

@ -1,18 +1,6 @@
<script setup lang="ts">
import { definePerson, defineWebPage, defineWebSite, useSchemaOrg } from '@unhead/schema-org'
import { useSeoMeta } from '@unhead/vue'
import { useLayout, useValaxyApp } from '../composables'
// TODO: add docs to override ValaxyApp
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFrontmatter, useValaxyHead } from '../composables'
import { useTimezone } from '../composables/global'
// https://github.com/vueuse/head
// you can use this to manipulate the document head in any components,
// they will be rendered correctly in the html results with vite-ssg
import { useSiteConfig } from '../config'
import ValaxyAddons from './ValaxyAddons.vue'
// @ts-expect-error virtual module
import ValaxyThemeApp from '/@valaxyjs/ThemeAppVue'
@ -22,57 +10,18 @@ import ValaxyUserApp from '/@valaxyjs/UserAppVue'
// <link rel="apple-touch-icon" href="/pwa-192x192.png">
// <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#00aba9">
const siteConfig = useSiteConfig()
// todo, allow user config
const fm = useFrontmatter()
const layout = useLayout()
const { locale } = useI18n()
const title = computed(() => fm.value[`title_${locale.value}`] || fm.value.title)
// seo
// todo: get first image url from markdown
const siteUrl = computed(() => fm.value.url || siteConfig.value.url)
const description = computed(() => fm.value.excerpt || fm.value.description || siteConfig.value.description)
useSeoMeta({
description,
ogDescription: description,
ogLocale: computed(() => locale.value || fm.value.lang || siteConfig.value.lang || 'en'),
ogLocaleAlternate: computed(() => siteConfig.value.languages.filter(l => l !== locale.value)),
ogSiteName: computed(() => siteConfig.value.title),
ogTitle: computed(() => fm.value.title || siteConfig.value.title),
ogImage: computed(() => fm.value.ogImage || fm.value.cover || siteConfig.value.favicon),
ogType: 'website',
ogUrl: siteUrl,
})
// for SEO
useSchemaOrg([
// https://unhead.unjs.io/guide/guides/identity.html
// Personal Website or Blog
definePerson({
name: siteConfig.value.author.name,
url: siteUrl.value,
image: siteConfig.value.author.avatar,
sameAs: siteConfig.value.social.map(s => s.link),
}),
defineWebSite({
name: title.value,
datePublished: computed(() => fm.value.date),
dateModified: computed(() => fm.value.updated),
}),
defineWebPage(),
])
useTimezone()
useValaxyHead()
if (layout.value !== 'empty') {
useValaxyApp()
}
</script>
<template>
<ValaxyThemeApp />
<ValaxyAddons />
<ValaxyUserApp />
<template v-if="layout !== 'empty'">
<ValaxyThemeApp />
<ValaxyAddons />
<ValaxyUserApp />
</template>
<router-view />
</template>

View File

@ -1,5 +1,5 @@
<template>
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_757_102259" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="97" y="222" width="830" height="647">
<path fill-rule="evenodd" clip-rule="evenodd" d="M729.48 756.469C729.478 756.471 729.475 756.473 729.472 756.475C729.606 756.361 729.625 756.36 729.48 756.469ZM690.046 767.723C685.618 766.845 680.868 765.982 675.803 765.143C634.63 758.32 576.843 753.701 512.383 753.12C447.923 752.538 390.063 756.114 348.774 762.193C343.694 762.941 338.929 763.717 334.486 764.515C338.914 765.394 343.664 766.256 348.729 767.095C389.902 773.918 447.689 778.537 512.149 779.119C576.609 779.7 634.469 776.124 675.758 770.045C680.838 769.298 685.603 768.521 690.046 767.723ZM295.261 752.552C295.118 752.441 295.137 752.442 295.269 752.558C295.266 752.556 295.264 752.554 295.261 752.552ZM295.052 775.769C295.054 775.768 295.057 775.766 295.06 775.764C294.926 775.877 294.907 775.878 295.052 775.769ZM729.263 779.68C729.266 779.682 729.268 779.684 729.271 779.686C729.413 779.798 729.395 779.796 729.263 779.68ZM511.932 803.118C642.542 804.296 748.572 788.686 748.756 768.252C748.941 747.819 643.21 730.299 512.6 729.121C381.99 727.942 275.96 743.552 275.776 763.986C275.591 784.42 381.322 801.939 511.932 803.118Z" fill="url(#paint0_linear_757_102259)" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M729.48 756.469C729.478 756.471 729.475 756.473 729.472 756.475C729.606 756.361 729.625 756.36 729.48 756.469ZM690.046 767.723C685.618 766.845 680.868 765.982 675.803 765.143C634.63 758.32 576.843 753.701 512.383 753.12C447.923 752.538 390.063 756.114 348.774 762.193C343.694 762.941 338.929 763.717 334.486 764.515C338.914 765.394 343.664 766.256 348.729 767.095C389.902 773.918 447.689 778.537 512.149 779.119C576.609 779.7 634.469 776.124 675.758 770.045C680.838 769.298 685.603 768.521 690.046 767.723ZM295.261 752.552C295.118 752.441 295.137 752.442 295.269 752.558C295.266 752.556 295.264 752.554 295.261 752.552ZM295.052 775.769C295.054 775.768 295.057 775.766 295.06 775.764C294.926 775.877 294.907 775.878 295.052 775.769ZM729.263 779.68C729.266 779.682 729.268 779.684 729.271 779.686C729.413 779.798 729.395 779.796 729.263 779.68ZM511.932 803.118C642.542 804.296 748.572 788.686 748.756 768.252C748.941 747.819 643.21 730.299 512.6 729.121C381.99 727.942 275.96 743.552 275.776 763.986C275.591 784.42 381.322 801.939 511.932 803.118Z" fill="url(#paint1_linear_757_102259)" />

View File

@ -1,6 +0,0 @@
<template>
<div>
<h1>DevTools</h1>
<p>DevTools</p>
</div>
</template>

View File

@ -1 +1,2 @@
export * from './useValaxyApp'
export * from './useValaxyHead'

View File

@ -0,0 +1,63 @@
import { definePerson, defineWebPage, defineWebSite, useSchemaOrg } from '@unhead/schema-org'
import { useSeoMeta } from '@unhead/vue'
// TODO: add docs to override ValaxyApp
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFrontmatter, useValaxyHead } from '../../composables'
import { useTimezone } from '../../composables/global'
// https://github.com/vueuse/head
// you can use this to manipulate the document head in any components,
// they will be rendered correctly in the html results with vite-ssg
import { useSiteConfig } from '../../config'
export function useValaxyApp() {
const siteConfig = useSiteConfig()
// todo, allow user config
const fm = useFrontmatter()
const { locale } = useI18n()
const title = computed(() => fm.value[`title_${locale.value}`] || fm.value.title)
// seo
// todo: get first image url from markdown
const siteUrl = computed(() => fm.value.url || siteConfig.value.url)
const description = computed(() => fm.value.excerpt || fm.value.description || siteConfig.value.description)
useSeoMeta({
description,
ogDescription: description,
ogLocale: computed(() => locale.value || fm.value.lang || siteConfig.value.lang || 'en'),
ogLocaleAlternate: computed(() => siteConfig.value.languages.filter(l => l !== locale.value)),
ogSiteName: computed(() => siteConfig.value.title),
ogTitle: computed(() => fm.value.title || siteConfig.value.title),
ogImage: computed(() => fm.value.ogImage || fm.value.cover || siteConfig.value.favicon),
ogType: 'website',
ogUrl: siteUrl,
})
// for SEO
useSchemaOrg([
// https://unhead.unjs.io/guide/guides/identity.html
// Personal Website or Blog
definePerson({
name: siteConfig.value.author.name,
url: siteUrl.value,
image: siteConfig.value.author.avatar,
sameAs: siteConfig.value.social.map(s => s.link),
}),
defineWebSite({
name: title.value,
datePublished: computed(() => fm.value.date),
dateModified: computed(() => fm.value.updated),
}),
defineWebPage(),
])
useTimezone()
useValaxyHead()
}

View File

@ -21,7 +21,7 @@ export async function addValaxyTabAndCommand() {
// iframe view
view: {
type: 'iframe',
src: '/__valaxy_devtools__/',
src: import.meta.env.DEV ? 'http://localhost:5001/' : '/__valaxy_devtools__/',
},
// category: 'pinned',
category: 'app',
@ -56,5 +56,5 @@ export async function addValaxyTabAndCommand() {
}
export const install: UserModule = async () => {
await addValaxyTabAndCommand()
// await addValaxyTabAndCommand()
}

View File

@ -33,6 +33,8 @@ export async function execBuild({ ssg, root, output, log }: { ssg: boolean, root
printInfo(options)
const valaxyApp = createValaxyNode(options)
// GLOBAL_STATE.valaxyApp = valaxyApp
// resolve options and create valaxy app
await callHookWithLog('options:resolved', valaxyApp)

View File

@ -11,6 +11,7 @@ import { createValaxyNode } from '../app'
import { commonOptions } from '../cli/options'
import { defaultViteConfig } from '../constants'
import { GLOBAL_STATE } from '../env'
import { resolveOptions } from '../options'
import { isPagesDirExist, setEnv, setTimezone } from '../utils/env'
import { findFreePort } from '../utils/net'
@ -39,6 +40,8 @@ export async function startValaxyDev({
setTimezone(resolvedOptions.config.siteConfig.timezone)
const valaxyApp = createValaxyNode(resolvedOptions)
GLOBAL_STATE.valaxyApp = valaxyApp
const viteConfig: InlineConfig = mergeConfig({
// initial vite config
...defaultViteConfig,

View File

@ -1,7 +1,10 @@
import type { ViteDevServer } from 'vite'
import type { ValaxyApp } from './types'
export const GLOBAL_STATE: {
valaxyApp: ValaxyApp | undefined
server: ViteDevServer | undefined
} = {
valaxyApp: undefined,
server: undefined,
}

View File

@ -5,6 +5,8 @@ export * from './cli'
export * from './common'
export * from './config'
export * from './constants'
// global env
// export * from './env'
export * from './options'
export * from './plugins'
export * from './server'

View File

@ -9,6 +9,8 @@ import { createTransformIncludes } from './include'
import { matterOptions } from './matter'
import { transformMermaid } from './mermaid'
export * from './matter'
export async function createMarkdownPlugin(
options: ResolvedValaxyOptions,
): Promise<Plugin> {

View File

@ -4,6 +4,9 @@ import { EXCERPT_SEPARATOR } from '../../../constants'
type GrayMatterOptions = matter.GrayMatterOption<string, GrayMatterOptions>
/**
* valaxy gray-matter options
*/
export const matterOptions: GrayMatterOptions = {
excerpt_separator: EXCERPT_SEPARATOR,
engines: {

View File

@ -2,17 +2,19 @@ import type { PluginOption } from 'vite'
import type { ValaxyServerOptions } from '../options'
import type { ValaxyNode } from '../types'
import path from 'node:path'
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
import UnheadVite from '@unhead/addons/vite'
import Vue from '@vitejs/plugin-vue'
import consola from 'consola'
import { resolve } from 'pathe'
import Components from 'unplugin-vue-components/vite'
import Layouts from 'vite-plugin-vue-layouts'
import Layouts from 'vite-plugin-vue-layouts'
import { customElements } from '../constants'
import { resolveImportPath } from '../utils'
import { createConfigPlugin } from './extendConfig'
import { createMarkdownPlugin } from './markdown'
import { createFixPlugins } from './patchTransform'
@ -31,6 +33,22 @@ export async function ViteValaxyPlugins(
const MarkdownPlugin = await createMarkdownPlugin(options)
const ValaxyLoader = await createValaxyLoader(options, serverOptions)
/**
* for unplugin-vue-components
*/
const componentsDirs = roots
.map(root => `${root}/components`)
.concat(['src/components', 'components'])
if (valaxyApp.options.mode === 'dev') {
const devtoolsDir = path.dirname(await resolveImportPath('@valaxyjs/devtools/package.json'))
const devtoolsComponentsDir = path.resolve(devtoolsDir, 'src/client/components')
componentsDirs.push(devtoolsComponentsDir)
const { componentsDir } = await import('@advjs/gui/node')
componentsDirs.push(componentsDir)
}
const plugins: (PluginOption | PluginOption[])[] = [
MarkdownPlugin,
createConfigPlugin(options),
@ -80,11 +98,12 @@ export async function ViteValaxyPlugins(
// allow override
allowOverrides: true,
// override: user -> theme -> client
// latter override former
dirs: roots
.map(root => `${root}/components`)
.concat(['src/components', 'components']),
/**
* override: user -> theme -> client
*
* latter override former
*/
dirs: componentsDirs,
dts: resolve(options.tempDir, 'components.d.ts'),
...valaxyConfig.components,

View File

@ -38,7 +38,9 @@ export async function createServer(
// only enable when dev
vitePlugins.push(
(await import('vite-plugin-vue-devtools')).default(),
(await import('@valaxyjs/devtools')).default(),
(await import('@valaxyjs/devtools')).default({
valaxyApp,
}),
)
}

View File

@ -12,6 +12,7 @@ import type Router from 'unplugin-vue-router/vite'
import type { DefaultTheme, PartialDeep, ValaxyAddon, ValaxyConfig } from 'valaxy/types'
import type { UserConfig as ViteUserConfig } from 'vite'
import type Layouts from 'vite-plugin-vue-layouts'
import type { createValaxyNode } from './app'
import type { ResolvedValaxyOptions } from './options'
import type { MarkdownOptions } from './plugins/markdown/types'
@ -245,3 +246,5 @@ export interface ValaxyAddonResolver {
setup?: (node: ValaxyNode) => void
}
export type ValaxyApp = ReturnType<typeof createValaxyNode>

View File

@ -61,9 +61,9 @@
"dependencies": {
"@antfu/install-pkg": "^0.5.0",
"@antfu/utils": "^0.7.10",
"@clack/prompts": "^0.8.2",
"@clack/prompts": "^0.9.0",
"@iconify-json/ri": "catalog:",
"@intlify/unplugin-vue-i18n": "^6.0.1",
"@intlify/unplugin-vue-i18n": "^6.0.2",
"@types/katex": "^0.16.7",
"@unhead/addons": "^1.11.14",
"@unhead/schema-org": "^1.11.14",
@ -75,7 +75,7 @@
"@vueuse/core": "12.0.0-beta.1",
"@vueuse/integrations": "12.0.0-beta.1",
"beasties": "^0.2.0",
"consola": "^3.2.3",
"consola": "^3.3.0",
"cross-spawn": "^7.0.6",
"css-i18n": "^0.0.5",
"dayjs": "^1.11.13",

File diff suppressed because it is too large Load Diff

View File

@ -15,4 +15,4 @@ catalog:
'@iconify-json/vscode-icons': ^1.2.6
typescript: '5.6'
unbuild: ^3.0.1
vite: ^6.0.4
vite: ^6.0.5