refactor: load virtual modules by import folder function

This commit is contained in:
YunYouJun 2025-02-16 18:13:31 +08:00
parent 4d5b9460a0
commit d76b956788
19 changed files with 237 additions and 189 deletions

View File

@ -93,27 +93,29 @@ For example, you can override the default font in 'styles/css-vars.scss'.
- `text`: 文本选择图标。
```scss
// $cursor-default = hexo-config('cursor.default');
// $cursor-pointer = hexo-config('cursor.pointer');
// $cursor-text = hexo-config('cursor.text');
:root {
--cursor-default: url('https://cdn.yunyoujun.cn/css/md-cursors/pointer.cur');
--cursor-pointer: url('https://cdn.yunyoujun.cn/css/md-cursors/link.cur');
--cursor-text: url('https://cdn.yunyoujun.cn/css/md-cursors/text.cur');
}
body {
cursor: url($cursor-default), auto;
cursor: var(--cursor-default), auto;
}
a {
cursor: url($cursor-pointer), auto;
cursor: var(--cursor-pointer), auto;
&:hover {
cursor: url($cursor-pointer), auto;
cursor: var(--cursor-pointer), auto;
}
}
.hty-icon-button {
cursor: url($cursor-pointer), pointer;
button {
cursor: var(--cursor-pointer), pointer;
}
input {
cursor: url($cursor-text), auto;
cursor: var(--cursor-text), text;
}
```

View File

@ -12,19 +12,9 @@ import AppLink from './components/AppLink.vue'
import setupMain from './setup/main'
import { setupValaxyDevTools } from './utils/dev'
// reset styles, load css before app
// import '@unocss/reset/tailwind.css'
// https://unocss.dev/guide/style-reset#tailwind-compat
// minus the background color override for buttons to avoid conflicts with UI frameworks
import '@unocss/reset/tailwind-compat.css'
// css
import './styles/css/css-vars.css'
import './styles/css/main.css'
// generate user styles
import '/@valaxyjs/styles'
import 'uno.css'
import '#valaxy/styles'
const valaxyConfig = initValaxyConfig()

View File

@ -1,3 +1,10 @@
// Types for virtual modules
// `#valaxy/*` is an alias for `/@valaxyjs/*`, because TS will consider `/@valaxyjs/*` as an absolute path that we can't override
declare module 'virtual:valaxy-theme' {
export default any
}
declare module '#valaxy/styles' {
// side-effects only
}

View File

@ -66,6 +66,27 @@ html.dark {
--va-c-text-3: rgba(235, 235, 245, 0.38);
}
// bg
:root {
--va-c-bg: #ffffff;
--va-c-bg-light: #ffffff;
--va-c-bg-dark: #fafafa;
--va-c-bg-opacity: rgba(255, 255, 255, 0.8);
--va-c-bg-soft: #f9f9f9;
--va-c-bg-alt: #f9f9f9;
--va-c-bg-mute: #f1f1f1;
}
html.dark {
--va-c-bg: #1a1a1d;
--va-c-bg-light: #202127;
--va-c-bg-dark: #1a1a1a;
--va-c-bg-opacity: rgba(0, 0, 0, 0.8);
--va-c-bg-alt: #161618;
--va-c-bg-soft: #202127;
--va-c-bg-mute: #2f2f2f;
}
/* code */
:root {
--va-code-line-height: 1.7;

View File

@ -1,19 +0,0 @@
:root {
--va-c-bg: #ffffff;
--va-c-bg-light: #ffffff;
--va-c-bg-dark: #fafafa;
--va-c-bg-opacity: rgba(255, 255, 255, 0.8);
--va-c-bg-soft: #f9f9f9;
--va-c-bg-alt: #f9f9f9;
--va-c-bg-mute: #f1f1f1;
}
html.dark {
--va-c-bg: #1a1a1d;
--va-c-bg-light: #202127;
--va-c-bg-dark: #1a1a1a;
--va-c-bg-opacity: rgba(0, 0, 0, 0.8);
--va-c-bg-alt: #161618;
--va-c-bg-soft: #202127;
--va-c-bg-mute: #2f2f2f;
}

View File

@ -1,15 +0,0 @@
html,
body,
#app {
margin: 0;
padding: 0;
line-height: 2;
}
html {
background-color: var(--va-c-bg);
}
a {
cursor: pointer;
}

View File

@ -10,3 +10,18 @@ $c-primary: #0078e7 !default;
@use "css-i18n/src/styles/index.scss" as *;
// components import by yourself
html,
body,
#app {
margin: 0;
padding: 0;
line-height: 2;
}
html {
background-color: var(--va-c-bg);
}
a {
cursor: pointer;
}

View File

@ -3,21 +3,22 @@ import type { ResolvedValaxyOptions } from '../../options'
import { readFile } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { ensurePrefix } from '@antfu/utils'
import consola from 'consola'
import { colors } from 'consola/utils'
import dayjs from 'dayjs'
import fg from 'fast-glob'
import { Feed } from 'feed'
import fs from 'fs-extra'
import matter from 'gray-matter'
import MarkdownIt from 'markdown-it'
import ora from 'ora'
import ora from 'ora'
import { getBorderCharacters, table } from 'table'
import { matterOptions } from '../../plugins/markdown/transform/matter'
import { ensurePrefix, isExternal } from '../../utils'
import { isExternal } from '../../utils'
import { getCreatedTime, getUpdatedTime } from '../../utils/date'
const markdown = MarkdownIt({

View File

@ -161,7 +161,12 @@ export async function getAlias(options: ResolvedValaxyOptions): Promise<AliasOpt
{ find: 'valaxy/package.json', replacement: toAtFS(resolve(options.clientRoot, '../package.json')) },
{ find: /^valaxy$/, replacement: toAtFS(resolve(options.clientRoot, 'index.ts')) },
{ find: '@valaxyjs/client/', replacement: `${toAtFS(options.clientRoot)}/` },
// virtual module to import theme
// virtual module alias
{
find: /^#valaxy\/(.*)/,
replacement: '/@valaxyjs/$1',
},
// import theme
{ find: 'virtual:valaxy-theme', replacement: `${toAtFS(options.themeRoot)}/client/index.ts` },
{ find: `valaxy-theme-${options.theme}/client`, replacement: `${toAtFS(resolve(options.themeRoot))}/client/index.ts` },
{ find: `valaxy-theme-${options.theme}/`, replacement: `${toAtFS(resolve(options.themeRoot))}/` },

View File

@ -5,118 +5,22 @@
import type { DefaultTheme, Pkg, SiteConfig } from 'valaxy/types'
import type { Plugin, ResolvedConfig } from 'vite'
import type { RouteRecordRaw } from 'vue-router'
import type { PageDataPayload } from '../../../types'
import type { ResolvedValaxyOptions, ValaxyServerOptions } from '../../options'
import type { ValaxyNodeConfig } from '../../types'
import consola from 'consola'
import fs from 'fs-extra'
import pascalCase from 'pascalcase'
import { join, relative, resolve } from 'pathe'
import { dim, yellow } from 'picocolors'
import { defaultSiteConfig, mergeValaxyConfig, resolveSiteConfig, resolveUserThemeConfig } from '../../config'
import { replaceArrMerge } from '../../config/merge'
import { vLogger } from '../../logger'
import { processValaxyOptions, resolveOptions, resolveThemeValaxyConfig } from '../../options'
import { resolveImportPath, toAtFS } from '../../utils'
import { isProd } from '../../utils/env'
import { toAtFS } from '../../utils'
import { countPerformanceTime } from '../../utils/performance'
import { templates } from '../../virtual'
import { createMarkdownToVueRenderFn } from '../markdown/markdownToVue'
function generateConfig(options: ResolvedValaxyOptions) {
const routes = options.redirects.map<RouteRecordRaw>((redirect) => {
return {
path: redirect.from,
redirect: redirect.to,
}
})
options.config.runtimeConfig.redirects = {
useVueRouter: isProd() ? options.config.siteConfig.redirects!.useVueRouter! : true,
redirectRoutes: routes,
}
return `export default ${JSON.stringify(JSON.stringify(options.config))}`
}
/**
* for /@valaxyjs/styles
* @param roots
*/
async function generateStyles(roots: string[], options: ResolvedValaxyOptions) {
const imports: string[] = []
// katex
if (options.config.features?.katex) {
imports.push(`import "${toAtFS(await resolveImportPath('katex/dist/katex.min.css', true))}"`)
imports.push(`import "${toAtFS(join(options.clientRoot, 'styles/third/katex.scss'))}"`)
}
for (const root of roots) {
const styles: string[] = []
const autoloadNames = ['css-vars', 'index']
autoloadNames.forEach((name) => {
styles.push(join(root, 'styles', `${name}.css`))
styles.push(join(root, 'styles', `${name}.scss`))
})
for (const style of styles) {
if (fs.existsSync(style))
imports.push(`import "${toAtFS(style)}"`)
}
}
return imports.join('\n')
}
function generateLocales(roots: string[]) {
const imports: string[] = [
'import { createDefu } from "defu"',
'const messages = { "zh-CN": {}, en: {} }',
`
const replaceArrMerge = createDefu((obj, key, value) => {
if (key && obj[key] && Array.isArray(obj[key]) && Array.isArray(value)) {
obj[key] = value
return true
}
})
`,
]
const languages = ['zh-CN', 'en']
roots.forEach((root, i) => {
languages.forEach((lang) => {
const langYml = `${root}/locales/${lang}.yml`
if (fs.existsSync(langYml) && fs.readFileSync(langYml, 'utf-8')) {
const varName = lang.replace('-', '') + i
imports.unshift(`import ${varName} from "${toAtFS(langYml)}"`)
// pre override next
imports.push(`messages['${lang}'] = replaceArrMerge(${varName}, messages['${lang}'])`)
}
})
})
imports.push('export default messages')
return imports.join('\n')
}
function generateAddons(options: ResolvedValaxyOptions) {
const globalAddonComponents = options.addons
.filter(v => v.global)
.filter(v => fs.existsSync(join(v.root, './App.vue')))
const spliceImportName = (str: string) => `Addon${pascalCase(str)}App`
const imports = globalAddonComponents
.map(addon => `import ${spliceImportName(addon.name)} from "${addon.name}/App.vue"`)
.join('\n')
const components = globalAddonComponents
.map(addon => `{ component: ${spliceImportName(addon.name)}, props: ${JSON.stringify(addon.props)} }`)
.join(',')
return `${imports}\n` + `export default [${components}]`
}
/**
* vue component render null
*/
@ -151,8 +55,6 @@ export async function createValaxyLoader(options: ResolvedValaxyOptions, serverO
const valaxyPrefix = '/@valaxy'
const roots = options.roots
let hasDeadLinks = false
let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>>
@ -187,9 +89,13 @@ export async function createValaxyLoader(options: ResolvedValaxyOptions, serverO
},
async load(id) {
if (id === '/@valaxyjs/config')
// stringify twice for \"
return generateConfig(options)
const template = templates.find(t => t.id === id)
if (template) {
return {
code: await template.getContent.call(this, options),
map: { mappings: '' },
}
}
if (id === '/@valaxyjs/context') {
return `export default ${JSON.stringify(JSON.stringify({
@ -201,16 +107,6 @@ export async function createValaxyLoader(options: ResolvedValaxyOptions, serverO
// TODO: custom dynamic css vars
// if (id === 'virtual:valaxy-css-vars') {}
// generate styles
if (id === '/@valaxyjs/styles')
return await generateStyles(roots, options)
if (id === '/@valaxyjs/locales')
return generateLocales(roots)
if (id === '/@valaxyjs/addons')
return generateAddons(options)
// root client
if (id === '/@valaxyjs/AppVue')
return generateAppVue(options.clientRoot)

View File

@ -8,24 +8,6 @@ export function isExternal(str: string) {
return EXTERNAL_URL_RE.test(str)
}
/**
* slash path for windows
* @param str
*/
export function slash(str: string) {
return str.replace(/\\/g, '/')
}
export function ensurePrefix(prefix: string, str: string) {
if (!str.startsWith(prefix))
return prefix + str
return str
}
export function toAtFS(path: string) {
return `/@fs${ensurePrefix('/', slash(path))}`
}
export function isPath(name: string) {
return name.startsWith('/') || /^\.\.?[/\\]/.test(name)
}

View File

@ -1,9 +1,21 @@
import { ensurePrefix, slash } from '@antfu/utils'
import consola from 'consola'
import { resolvePath } from 'mlly'
import { resolveGlobal } from 'resolve-global'
export const isInstalledGlobally: { value?: boolean } = {}
/**
* Resolve path for import url on Vite client side
*/
export async function resolveImportUrl(id: string) {
return toAtFS(await resolveImportPath(id, true))
}
export function toAtFS(path: string) {
return `/@fs${ensurePrefix('/', slash(path))}`
}
/**
* Before is CJS: use 'resolve'
* ESM: use 'mlly'

View File

@ -0,0 +1,24 @@
import type { VirtualModuleTemplate } from './types'
import fs from 'fs-extra'
import pascalCase from 'pascalcase'
import { join } from 'pathe'
export const templateAddons: VirtualModuleTemplate = {
id: '/@valaxyjs/addons',
async getContent(options) {
const globalAddonComponents = options.addons
.filter(v => v.global)
.filter(v => fs.existsSync(join(v.root, './App.vue')))
const spliceImportName = (str: string) => `Addon${pascalCase(str)}App`
const imports = globalAddonComponents
.map(addon => `import ${spliceImportName(addon.name)} from "${addon.name}/App.vue"`)
.join('\n')
const components = globalAddonComponents
.map(addon => `{ component: ${spliceImportName(addon.name)}, props: ${JSON.stringify(addon.props)} }`)
.join(',')
return `${imports}\n` + `export default [${components}]`
},
}

View File

@ -0,0 +1,22 @@
import type { RouteRecordRaw } from 'vue-router'
import type { VirtualModuleTemplate } from './types'
import { isProd } from '../utils/env'
export const templateConfig: VirtualModuleTemplate = {
id: '/@valaxyjs/config',
async getContent(options) {
const routes = options.redirects.map<RouteRecordRaw>((redirect) => {
return {
path: redirect.from,
redirect: redirect.to,
}
})
options.config.runtimeConfig.redirects = {
useVueRouter: isProd() ? options.config.siteConfig.redirects!.useVueRouter! : true,
redirectRoutes: routes,
}
// stringify twice for \"
return `export default ${JSON.stringify(JSON.stringify(options.config))}`
},
}

View File

@ -0,0 +1,11 @@
import { templateAddons } from './addons'
import { templateConfig } from './config'
import { templateLocales } from './locales'
import { templateStyles } from './styles'
export const templates = [
templateAddons,
templateConfig,
templateLocales,
templateStyles,
]

View File

@ -0,0 +1,37 @@
import type { VirtualModuleTemplate } from './types'
import fs from 'fs-extra'
import { toAtFS } from '../utils'
export const templateLocales: VirtualModuleTemplate = {
id: '/@valaxyjs/locales',
async getContent({ roots }) {
const imports: string[] = [
'import { createDefu } from "defu"',
'const messages = { "zh-CN": {}, en: {} }',
`
const replaceArrMerge = createDefu((obj, key, value) => {
if (key && obj[key] && Array.isArray(obj[key]) && Array.isArray(value)) {
obj[key] = value
return true
}
})
`,
]
const languages = ['zh-CN', 'en']
roots.forEach((root, i) => {
languages.forEach((lang) => {
const langYml = `${root}/locales/${lang}.yml`
if (fs.existsSync(langYml) && fs.readFileSync(langYml, 'utf-8')) {
const varName = lang.replace('-', '') + i
imports.unshift(`import ${varName} from "${toAtFS(langYml)}"`)
// pre override next
imports.push(`messages['${lang}'] = replaceArrMerge(${varName}, messages['${lang}'])`)
}
})
})
imports.push('export default messages')
return imports.join('\n')
},
}

View File

@ -0,0 +1,48 @@
import type { VirtualModuleTemplate } from './types'
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { resolveImportUrl, toAtFS } from '../utils'
export const templateStyles: VirtualModuleTemplate = {
id: '/@valaxyjs/styles',
async getContent({ clientRoot, roots, config }) {
function resolveUrlOfClient(name: string) {
return toAtFS(join(clientRoot, name))
}
const imports: string[] = []
if (config.features?.katex) {
imports.push(`import "${await resolveImportUrl('katex/dist/katex.min.css')}"`)
imports.push(`import "${resolveUrlOfClient('styles/third/katex.scss')}"`)
}
for (const root of roots) {
const styles = [
join(root, 'styles', 'index.ts'),
join(root, 'styles', 'index.css'),
join(root, 'styles', 'index.scss'),
join(root, 'styles', 'css-vars.css'),
join(root, 'styles', 'css-vars.scss'),
]
for (const style of styles) {
if (existsSync(style)) {
imports.push(`import "${toAtFS(style)}"`)
continue
}
}
}
// reset styles, load css before app
// import '@unocss/reset/tailwind.css'
// https://unocss.dev/guide/style-reset#tailwind-compat
// minus the background color override for buttons to avoid conflicts with UI frameworks
imports.unshift(`import "${await resolveImportUrl('@unocss/reset/tailwind-compat.css')}"`)
imports.push('import "uno.css"')
return imports.join('\n')
},
}

View File

@ -0,0 +1,8 @@
import type { Awaitable } from '@antfu/utils'
import type { PluginContext } from 'rollup'
import type { ResolvedValaxyOptions } from '../options'
export interface VirtualModuleTemplate {
id: string
getContent: (this: PluginContext, options: ResolvedValaxyOptions) => Awaitable<string>
}

View File

@ -17,6 +17,7 @@ export default defineConfig((options) => {
format: ['esm'],
minify: !options.watch,
external: [
'/@valaxyjs/',
'/@valaxyjs/config',
'/@valaxyjs/context',