@bndynet/vue-site 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,6 +85,7 @@ Add `"dev": "vue-site dev"` (or `vs dev`) in `package.json` scripts if you like.
85
85
  | `readme` | Raw Home content if no `README.md` |
86
86
  | `links` | Header links: Lucide `icon` + `link`, optional `title` (`LocalizedString`) |
87
87
  | `shell` | Shell action area config (`ShellConfig`) — see [Shell actions](#shell-actions-shellactions) |
88
+ | `icons` | Lucide icon registry. Generated automatically by the CLI; provide manually only when calling `createSiteApp` without the CLI |
88
89
  | `pages` | `StandalonePage[]` — full-screen routes outside the `nav` tree (no top bar/sidebar/footer) |
89
90
  | `auth` | Central authorization policy (`AuthConfig`) — see [Per-page authorization](#per-page-authorization-auth) |
90
91
  | `router` | History mode (`RouterConfig`) — `hash` (default) or HTML5 `web`; see [Router history](#router-history-router) |
@@ -101,7 +102,7 @@ Add `"dev": "vue-site dev"` (or `vs dev`) in `package.json` scripts if you like.
101
102
  | `icon` | [Lucide](https://lucide.dev/icons) name |
102
103
  | `page` | Page content. Simplest is a **file-path string** like `'./pages/AdminView.vue'` or `'./README.md'` (auto-loads per-locale siblings, falls back to the base file). Also accepts a loader (`() => import('./Page.vue')` / `() => import('./page.md?raw')`) or a `localizedPage(...)` result. See [Per-locale page content](#per-locale-page-content) and [Advanced page loaders](#advanced-page-loaders) |
103
104
  | `path` | Route path (derived from `label`'s default-locale value if omitted; stays stable across languages) |
104
- | `layout` | Page content width: `'default'` keeps the standard centered reading width, `'wide'` uses a wider centered canvas, `'full'` fills the available parent container. Ignored for groups and links. |
105
+ | `layout` | Page content width: `'default'` keeps the standard centered reading width, `'wide'` uses a wider centered canvas, `'full'` fills the available parent container with no framework padding. Ignored for groups and links. |
105
106
  | `children` | Nested group |
106
107
  | `link` | Render as a hyperlink (internal route path or external URL) instead of a page route |
107
108
  | `visible` | `() => boolean \| Promise<boolean>`, awaited once at startup. Return `false` to hide the item from the nav and skip its route (not reachable by direct URL). A hidden parent hides its subtree; a group with no remaining children is pruned. Not reactive to later changes. |
@@ -131,6 +132,29 @@ export default defineConfig({
131
132
  })
132
133
  ```
133
134
 
135
+ ### Icon registry
136
+
137
+ The CLI scans configured Lucide icon names in `nav`, `links`, `i18n.locales`, and `theme.extraThemes`
138
+ and generates an icon registry automatically. Only the icons referenced by the config are bundled.
139
+
140
+ When using the library directly without the CLI, pass the registry yourself:
141
+
142
+ ```typescript
143
+ import Home from 'lucide-vue-next/dist/esm/icons/home.js'
144
+ import BookOpen from 'lucide-vue-next/dist/esm/icons/book-open.js'
145
+
146
+ createSiteApp({
147
+ title: 'My Site',
148
+ icons: {
149
+ home: Home,
150
+ 'book-open': BookOpen,
151
+ },
152
+ nav: [
153
+ { label: 'Home', icon: 'home', page: () => import('./README.md?raw') },
154
+ ],
155
+ })
156
+ ```
157
+
134
158
  ### `ThemeConfig`
135
159
 
136
160
  Built-in themes are `light`, `dark`, plus the always-on extras `sepia` and `ocean` (shown in the switcher for every site). Set `theme: false` on `SiteConfig` to disable theming entirely.
@@ -531,7 +555,7 @@ app.mount('#app')
531
555
 
532
556
  Use a top-level `await` in your entry (or an async IIFE): `createSiteApp` is async and **awaits** `configureApp` when it returns a `Promise`. If you set optional `bootstrap` in config, that module loads before the app is created; if you omit `bootstrap`, that step is skipped.
533
557
 
534
- Exports: `createSiteApp`, `defineConfig`, `useTheme`, `useSiteConfig`, `useLocale`, `useLocalize`, `tk`, `resolveLocalized`, `resolveField`, `resolveMessage`, `mergeCatalog`, `flattenMessages`, `isMessageRef`, `localizedPage`, `builtinMessages`, `themeRefKey`, `localeRefKey`. Types: `SiteConfig`, `SiteEnvConfig`, `SiteViteConfig`, `SiteExternalLink`, `ShellConfig`, `ShellAction`, `ShellActionLoader`, `NavItem`, `StandalonePage`, `PageLayout`, `AuthRule`, `AuthContext`, `AuthConfig`, `RouterConfig`, `ThemeConfig`, `ThemeOption`, `ThemePaletteVars`, `ResolvedNavItem`, `I18nConfig`, `LocaleOption`, `LocaleCode`, `LocalizedString`, `MessageRef`, `MessageTree`, `MessageCatalog`, `PageLoader`, `LocalizedPageOptions`.
558
+ Exports: `createSiteApp`, `defineConfig`, `useTheme`, `useSiteConfig`, `useLocale`, `useLocalize`, `tk`, `resolveLocalized`, `resolveField`, `resolveMessage`, `mergeCatalog`, `flattenMessages`, `isMessageRef`, `localizedPage`, `builtinMessages`, `themeRefKey`, `localeRefKey`. Types: `SiteConfig`, `SiteEnvConfig`, `SiteViteConfig`, `SiteExternalLink`, `ShellConfig`, `ShellAction`, `ShellActionLoader`, `NavItem`, `StandalonePage`, `PageLayout`, `AuthRule`, `AuthContext`, `AuthConfig`, `RouterConfig`, `ThemeConfig`, `ThemeOption`, `ThemePaletteVars`, `ResolvedNavItem`, `I18nConfig`, `LocaleOption`, `LocaleCode`, `LocalizedString`, `IconRegistry`, `MessageRef`, `MessageTree`, `MessageCatalog`, `PageLoader`, `LocalizedPageOptions`.
535
559
 
536
560
  ### Theme in Vue pages (`useTheme`)
537
561
 
package/README.zh.md CHANGED
@@ -99,7 +99,7 @@ npx vue-site build --base /app/
99
99
  | `icon` | [Lucide](https://lucide.dev/icons) 图标名 |
100
100
  | `page` | 页面内容。最简单的是一个**文件路径字符串**,如 `'./pages/AdminView.vue'` 或 `'./README.md'`(自动加载各语言的同名文件,找不到时回退到基础文件)。也接受加载器(`() => import('./Page.vue')` / `() => import('./page.md?raw')`)或 `localizedPage(...)` 的返回值。见[按语言区分的页面内容](#按语言区分的页面内容)与[高级页面加载器](#高级页面加载器) |
101
101
  | `path` | 路由路径(省略时从 `label` 的默认语言值派生;切换语言时保持稳定) |
102
- | `layout` | 页面内容宽度:`'default'` 保持标准居中阅读宽度,`'wide'` 使用更宽的居中画布,`'full'` 填满可用父容器。对分组和链接无效。 |
102
+ | `layout` | 页面内容宽度:`'default'` 保持标准居中阅读宽度,`'wide'` 使用更宽的居中画布,`'full'` 以无框架内边距的方式填满可用父容器。对分组和链接无效。 |
103
103
  | `children` | 嵌套分组 |
104
104
  | `link` | 渲染为超链接(内部路由路径或外部 URL),而非页面路由 |
105
105
  | `visible` | `() => boolean \| Promise<boolean>`,在启动时等待执行一次。返回 `false` 会从导航中隐藏该条目并跳过其路由(无法通过直接 URL 访问)。被隐藏的父项会隐藏其整个子树;没有剩余子项的分组会被剪除。对后续变化不具响应性。 |
package/bin/vue-site.mjs CHANGED
@@ -14,17 +14,29 @@ import fs from 'fs'
14
14
  const __filename = fileURLToPath(import.meta.url)
15
15
  const __dirname = dirname(__filename)
16
16
  const pkgDir = resolve(__dirname, '..')
17
- const require = createRequire(import.meta.url)
18
17
  const FRAMEWORK_PACKAGE = '@bndynet/vue-site'
19
18
  const frameworkEntry = resolve(pkgDir, 'dist/index.es.js')
20
19
  const frameworkStyle = resolve(pkgDir, 'dist/style.css')
21
20
 
22
- function resolvePkgDir(pkg) {
23
- return dirname(require.resolve(`${pkg}/package.json`))
21
+ function resolvePkgDir(pkg, from = import.meta.url) {
22
+ return dirname(createRequire(from).resolve(`${pkg}/package.json`))
23
+ }
24
+
25
+ function toVitePath(file) {
26
+ return file.replace(/\\/g, '/')
24
27
  }
25
28
 
26
29
  const vuePath = resolvePkgDir('vue')
27
30
  const vueRouterPath = resolvePkgDir('vue-router')
31
+ const lucidePath = resolvePkgDir('lucide-vue-next')
32
+ const elementPlusPath = resolvePkgDir('element-plus')
33
+ const elementPlusPackage = resolve(elementPlusPath, 'package.json')
34
+ const elementPlusIconsPath = resolvePkgDir('@element-plus/icons-vue', elementPlusPackage)
35
+ const dayjsPath = resolvePkgDir('dayjs', elementPlusPackage)
36
+ const lucideAliasPath = toVitePath(lucidePath)
37
+ const elementPlusAliasPath = toVitePath(elementPlusPath)
38
+ const elementPlusIconsAliasPath = toVitePath(elementPlusIconsPath)
39
+ const dayjsAliasPath = toVitePath(dayjsPath)
28
40
  const cwd = process.cwd()
29
41
  /** Lets `import('../file.md?raw')` work when `site.config` lives in a subfolder (README next to cwd). In-repo, ../ often falls under pkgDir; from npm install it does not, so we allow cwd's parent explicitly. */
30
42
  const cwdParent = resolve(cwd, '..')
@@ -113,6 +125,8 @@ const VIRTUAL_ENTRY = 'virtual:vue-site-entry'
113
125
  const RESOLVED_ENTRY = '\0' + VIRTUAL_ENTRY
114
126
  const VIRTUAL_PACKAGE = 'virtual:vue-site-package'
115
127
  const RESOLVED_PACKAGE = '\0' + VIRTUAL_PACKAGE
128
+ const VIRTUAL_ICONS = 'virtual:vue-site-icons'
129
+ const RESOLVED_ICONS = '\0' + VIRTUAL_ICONS
116
130
 
117
131
  function parseRepositoryUrl(pkg) {
118
132
  const r = pkg.repository
@@ -240,6 +254,7 @@ function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
240
254
  `import '${FRAMEWORK_PACKAGE}/style.css'`,
241
255
  `import siteConfig from '${siteConfigSpecifier}'`,
242
256
  `import { repositoryUrl } from '${VIRTUAL_PACKAGE}'`,
257
+ `import { iconRegistry } from '${VIRTUAL_ICONS}'`,
243
258
  ``,
244
259
  `// Auto-discover translations: /locales/<code>.json -> { [code]: { ...messages } }.`,
245
260
  `const __localeFiles = import.meta.glob('/locales/*.json', { eager: true, import: 'default' })`,
@@ -295,6 +310,7 @@ function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
295
310
  ` ...(hasThemeQuery ? { theme: { ...(siteConfig.theme || {}), default: resolvedTheme } } : {}),`,
296
311
  ` packageRepository: repositoryUrl,`,
297
312
  ` baseUrl: import.meta.env.BASE_URL,`,
313
+ ` icons: { ...iconRegistry, ...(siteConfig.icons || {}) },`,
298
314
  ` })`,
299
315
  ` app.mount('#app')`,
300
316
  `})()`,
@@ -512,13 +528,149 @@ function configSugarPlugin() {
512
528
  }
513
529
  }
514
530
 
515
- function vueSitePlugin(entryCode, htmlTemplate) {
531
+ const BUILTIN_ICON_NAMES = [
532
+ 'sun',
533
+ 'moon',
534
+ 'palette',
535
+ 'languages',
536
+ 'github',
537
+ 'coffee',
538
+ 'waves',
539
+ ]
540
+
541
+ function normalizeLucideIconName(name) {
542
+ const trimmed = String(name ?? '').trim()
543
+ if (!trimmed) return ''
544
+ return trimmed
545
+ .replace(/^lucide[\s_-]?/i, '')
546
+ .replace(/[\s_-]?icon$/i, '')
547
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
548
+ .replace(/[\s_]+/g, '-')
549
+ .toLowerCase()
550
+ }
551
+
552
+ function toPascalIconName(name) {
553
+ return normalizeLucideIconName(name)
554
+ .split('-')
555
+ .filter(Boolean)
556
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
557
+ .join('')
558
+ }
559
+
560
+ function readLucideIconExportMap() {
561
+ const map = new Map()
562
+ const entry = resolve(lucidePath, 'dist/esm/lucide-vue-next.js')
563
+ const code = fs.readFileSync(entry, 'utf-8')
564
+ const re = /export\s+\{([^}]+)\}\s+from\s+['"]\.\/icons\/([^'"]+)['"]/g
565
+ let match
566
+
567
+ while ((match = re.exec(code))) {
568
+ const exports = match[1]
569
+ const file = match[2]
570
+ for (const part of exports.split(',')) {
571
+ const alias = /default\s+as\s+([A-Za-z0-9_$]+)/.exec(part.trim())?.[1]
572
+ if (alias) map.set(alias, file)
573
+ }
574
+ }
575
+
576
+ return map
577
+ }
578
+
579
+ const lucideIconExportMap = readLucideIconExportMap()
580
+
581
+ function addIconName(set, value) {
582
+ if (typeof value !== 'string') return
583
+ const name = value.trim()
584
+ if (name) set.add(name)
585
+ }
586
+
587
+ function collectNavIconNames(set, nav = []) {
588
+ for (const item of nav) {
589
+ addIconName(set, item?.icon)
590
+ collectNavIconNames(set, item?.children ?? [])
591
+ }
592
+ }
593
+
594
+ function collectIconNames(siteConfig = {}) {
595
+ const names = new Set(BUILTIN_ICON_NAMES)
596
+ const themeConfig =
597
+ siteConfig.theme && typeof siteConfig.theme === 'object'
598
+ ? siteConfig.theme
599
+ : undefined
600
+
601
+ collectNavIconNames(names, siteConfig.nav ?? [])
602
+
603
+ for (const link of siteConfig.links ?? []) {
604
+ addIconName(names, link?.icon)
605
+ }
606
+ for (const locale of siteConfig.i18n?.locales ?? []) {
607
+ addIconName(names, locale?.icon)
608
+ }
609
+ for (const theme of themeConfig?.extraThemes ?? []) {
610
+ addIconName(names, theme?.icon)
611
+ }
612
+
613
+ return [...names]
614
+ }
615
+
616
+ function buildIconRegistryCode(siteConfig) {
617
+ const imports = []
618
+ const entries = []
619
+ const imported = new Map()
620
+ const addedEntries = new Set()
621
+
622
+ for (const name of collectIconNames(siteConfig)) {
623
+ const normalized = normalizeLucideIconName(name)
624
+ if (!normalized) continue
625
+
626
+ const pascal = toPascalIconName(name)
627
+ const iconSource =
628
+ lucideIconExportMap.get(pascal) ??
629
+ lucideIconExportMap.get(`${pascal}Icon`) ??
630
+ `${normalized}.js`
631
+ const iconFile = resolve(lucidePath, 'dist/esm/icons', iconSource)
632
+
633
+ if (!fs.existsSync(iconFile)) {
634
+ console.warn(
635
+ `[vue-site] Icon "${name}" was not found in lucide-vue-next. It will render empty.`,
636
+ )
637
+ continue
638
+ }
639
+
640
+ let local = imported.get(normalized)
641
+ if (!local) {
642
+ local = `Icon${imported.size}`
643
+ imported.set(normalized, local)
644
+ imports.push(
645
+ `import ${local} from ${JSON.stringify(
646
+ `lucide-vue-next/dist/esm/icons/${iconSource}`,
647
+ )}`,
648
+ )
649
+ }
650
+
651
+ for (const key of new Set([name, normalized])) {
652
+ if (addedEntries.has(key)) continue
653
+ addedEntries.add(key)
654
+ entries.push(` ${JSON.stringify(key)}: ${local},`)
655
+ }
656
+ }
657
+
658
+ return [
659
+ ...imports,
660
+ `export const iconRegistry = {`,
661
+ ...entries,
662
+ `}`,
663
+ ].join('\n')
664
+ }
665
+
666
+ function vueSitePlugin(entryCode, htmlTemplate, iconRegistryCode) {
516
667
  return [
517
668
  {
518
669
  name: 'vue-site:virtual-entry',
519
670
  resolveId(id) {
520
671
  if (id === VIRTUAL_ENTRY) return RESOLVED_ENTRY
521
672
  if (id === VIRTUAL_PACKAGE) return RESOLVED_PACKAGE
673
+ if (id === VIRTUAL_ICONS) return RESOLVED_ICONS
522
674
  },
523
675
  load(id) {
524
676
  if (id === RESOLVED_ENTRY) return entryCode
@@ -526,6 +678,7 @@ function vueSitePlugin(entryCode, htmlTemplate) {
526
678
  const url = readPackageRepositoryUrl()
527
679
  return `export const repositoryUrl = ${JSON.stringify(url)}`
528
680
  }
681
+ if (id === RESOLVED_ICONS) return iconRegistryCode
529
682
  },
530
683
  },
531
684
  {
@@ -664,6 +817,7 @@ async function buildViteConfig(options = {}) {
664
817
  }
665
818
 
666
819
  const entryCode = buildEntryCode(siteConfig)
820
+ const iconRegistryCode = buildIconRegistryCode(siteConfig)
667
821
  const htmlTemplate = buildHtmlShell(
668
822
  `<script type="module" src="/@id/__x00__${VIRTUAL_ENTRY}"></script>`,
669
823
  siteConfig,
@@ -718,7 +872,7 @@ async function buildViteConfig(options = {}) {
718
872
  }),
719
873
  vue(vueOpts),
720
874
  ...watchedScssPlugin,
721
- ...vueSitePlugin(entryCode, htmlTemplate),
875
+ ...vueSitePlugin(entryCode, htmlTemplate, iconRegistryCode),
722
876
  ...(userPlugins || []),
723
877
  ],
724
878
  resolve: {
@@ -736,6 +890,38 @@ async function buildViteConfig(options = {}) {
736
890
  find: 'vue-router',
737
891
  replacement: resolve(vueRouterPath, 'dist/vue-router.mjs'),
738
892
  },
893
+ {
894
+ find: /^lucide-vue-next$/,
895
+ replacement: toVitePath(resolve(lucidePath, 'dist/esm/lucide-vue-next.js')),
896
+ },
897
+ {
898
+ find: /^lucide-vue-next\//,
899
+ replacement: `${lucideAliasPath}/`,
900
+ },
901
+ {
902
+ find: /^element-plus$/,
903
+ replacement: toVitePath(resolve(elementPlusPath, 'es/index.mjs')),
904
+ },
905
+ {
906
+ find: /^element-plus\//,
907
+ replacement: `${elementPlusAliasPath}/`,
908
+ },
909
+ {
910
+ find: /^@element-plus\/icons-vue$/,
911
+ replacement: toVitePath(resolve(elementPlusIconsPath, 'dist/index.js')),
912
+ },
913
+ {
914
+ find: /^@element-plus\/icons-vue\//,
915
+ replacement: `${elementPlusIconsAliasPath}/`,
916
+ },
917
+ {
918
+ find: /^dayjs$/,
919
+ replacement: dayjsAliasPath,
920
+ },
921
+ {
922
+ find: /^dayjs\//,
923
+ replacement: `${dayjsAliasPath}/`,
924
+ },
739
925
  ],
740
926
  },
741
927
  optimizeDeps: {
@@ -743,6 +929,11 @@ async function buildViteConfig(options = {}) {
743
929
  // Keep it out of dependency pre-bundling so Vite does not instantiate
744
930
  // `dist/index.es.js` once via /@fs and again via node_modules/.vite.
745
931
  exclude: [FRAMEWORK_PACKAGE],
932
+ // Element Plus imports dayjs from ESM files, but dayjs itself ships as
933
+ // CommonJS. Force Vite's CJS interop for the package while letting
934
+ // discovered `dayjs/*` plugin imports be optimized normally.
935
+ include: ['dayjs'],
936
+ needsInterop: ['dayjs'],
746
937
  },
747
938
  server: {
748
939
  open: true,
@@ -1,6 +1,8 @@
1
- type __VLS_Props = {
1
+ declare const _default: import('vue').DefineComponent<{
2
2
  name: string;
3
3
  size?: number;
4
- };
5
- declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
4
+ }, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{
5
+ name: string;
6
+ size?: number;
7
+ }> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
6
8
  export default _default;
package/dist/index.d.ts CHANGED
@@ -9,5 +9,5 @@ export { builtinMessages } from './i18n-messages';
9
9
  export { useSiteConfig } from './composables/useSiteConfig';
10
10
  export { builtinThemePalettes } from './theme/presets';
11
11
  export { ElMessage, ElMessageBox, ElNotification, } from 'element-plus';
12
- export type { SiteConfig, SiteEnvConfig, SiteViteConfig, SiteExternalLink, ShellConfig, ShellAction, ShellActionLoader, NavItem, StandalonePage, PageLayout, AuthRule, AuthContext, AuthConfig, RouterConfig, ThemeConfig, ThemeOption, ThemePaletteVars, ResolvedNavItem, LocaleCode, LocalizedString, MessageRef, MessageTree, LocaleOption, I18nConfig, PageLoader, } from './types';
12
+ export type { SiteConfig, SiteEnvConfig, SiteViteConfig, SiteExternalLink, ShellConfig, ShellAction, ShellActionLoader, NavItem, StandalonePage, PageLayout, AuthRule, AuthContext, AuthConfig, RouterConfig, ThemeConfig, ThemeOption, ThemePaletteVars, ResolvedNavItem, LocaleCode, LocalizedString, IconRegistry, MessageRef, MessageTree, LocaleOption, I18nConfig, PageLoader, } from './types';
13
13
  export declare function defineConfig(config: SiteConfig): SiteConfig;