@bndynet/vue-site 1.2.0 → 1.3.0

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
@@ -10,6 +10,7 @@ Configurable Vue 3 site framework: one package, `site.config.ts`, and Markdown p
10
10
  - Hash or HTML5 (`web`) router history, configurable in `site.config.ts`
11
11
  - Markdown (`?raw`) or Vue pages
12
12
  - highlight.js, light/dark theme + localStorage
13
+ - Custom shell actions for avatars, notifications, help menus, and other app controls
13
14
  - Built-in multi-language support (locale switcher, `LocalizedString` config, per-locale pages) — see [Internationalization](#internationalization-i18n)
14
15
  - Project `README.md` as Home
15
16
  - Full TypeScript types
@@ -29,6 +30,7 @@ import { defineConfig } from '@bndynet/vue-site'
29
30
 
30
31
  export default defineConfig({
31
32
  title: 'My Project',
33
+ favicon: '/favicon.ico',
32
34
  nav: [
33
35
  { label: 'Home', icon: 'home', page: () => import('./README.md?raw') },
34
36
  { label: 'Guide', icon: 'book-open', page: () => import('./pages/guide.md?raw') },
@@ -42,6 +44,7 @@ export default defineConfig({
42
44
  my-site/
43
45
  package.json
44
46
  site.config.ts
47
+ public/favicon.ico
45
48
  README.md
46
49
  pages/guide.md
47
50
  ```
@@ -72,13 +75,16 @@ Add `"dev": "vue-site dev"` (or `vs dev`) in `package.json` scripts if you like.
72
75
  |----------|-------------|
73
76
  | `title` | Site title (sidebar + tab). `LocalizedString` |
74
77
  | `nav` | `NavItem[]` |
78
+ | `navPosition` | Optional navigation placement: `'top'` forces the top/tiered layout, `'sidebar'` forces the full nav tree into the sidebar. Omit to keep automatic behavior based on nav depth |
75
79
  | `defaultPath` | Path the site opens at; `/` and unknown paths redirect here. Must match a registered route (a `nav` item's resolved path or a `pages` entry's `path`). Defaults to the first top-level `nav` item |
80
+ | `favicon` | Browser tab icon URL. Use a public asset path, remote URL, or imported asset URL |
76
81
  | `logo` | Logo URL or imported image |
77
82
  | `theme` | See `ThemeConfig` below; set to `false` to disable theming (hides the switcher, forces a fixed `light` palette, no localStorage persistence) |
78
83
  | `i18n` | Multi-language config (`I18nConfig`) — see [Internationalization](#internationalization-i18n) |
79
84
  | `footer` | Footer text. `LocalizedString` |
80
85
  | `readme` | Raw Home content if no `README.md` |
81
86
  | `links` | Header links: Lucide `icon` + `link`, optional `title` (`LocalizedString`) |
87
+ | `shell` | Shell action area config (`ShellConfig`) — see [Shell actions](#shell-actions-shellactions) |
82
88
  | `pages` | `StandalonePage[]` — full-screen routes outside the `nav` tree (no top bar/sidebar/footer) |
83
89
  | `auth` | Central authorization policy (`AuthConfig`) — see [Per-page authorization](#per-page-authorization-auth) |
84
90
  | `router` | History mode (`RouterConfig`) — `hash` (default) or HTML5 `web`; see [Router history](#router-history-router) |
@@ -95,11 +101,36 @@ Add `"dev": "vue-site dev"` (or `vs dev`) in `package.json` scripts if you like.
95
101
  | `icon` | [Lucide](https://lucide.dev/icons) name |
96
102
  | `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) |
97
103
  | `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. |
98
105
  | `children` | Nested group |
99
106
  | `link` | Render as a hyperlink (internal route path or external URL) instead of a page route |
100
107
  | `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. |
101
108
  | `auth` | Authorization rule (`AuthRule`) interpreted by `auth.authorize`. Keeps the route registered and enforces it via a navigation guard (so direct URLs redirect to login). Requires `SiteConfig.auth`. See [Per-page authorization](#per-page-authorization-auth). |
102
109
 
110
+ ### `StandalonePage`
111
+
112
+ | Property | Description |
113
+ |----------|-------------|
114
+ | `path` | Route path, used as-is (e.g. `/landing`) |
115
+ | `page` | Page content. Same forms as `NavItem.page` |
116
+ | `layout` | Optional content width. Omit to keep the standalone default: a full-screen, unpadded canvas. |
117
+ | `visible` | `() => boolean \| Promise<boolean>`, awaited once at startup. Return `false` to skip registering the route. |
118
+ | `auth` | Authorization rule (`AuthRule`) enforced by the same navigation guard as `NavItem.auth`. |
119
+
120
+ ### Page layout
121
+
122
+ Set `layout` on a `NavItem` page when the default reading width is too narrow:
123
+
124
+ ```typescript
125
+ export default defineConfig({
126
+ nav: [
127
+ { label: 'Docs', icon: 'book', page: './pages/docs.md' },
128
+ { label: 'Reports', icon: 'table', layout: 'wide', page: './pages/reports.vue' },
129
+ { label: 'Dashboard', icon: 'gauge', layout: 'full', page: './pages/dashboard.vue' },
130
+ ],
131
+ })
132
+ ```
133
+
103
134
  ### `ThemeConfig`
104
135
 
105
136
  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.
@@ -111,6 +142,45 @@ Built-in themes are `light`, `dark`, plus the always-on extras `sepia` and `ocea
111
142
  | `palettes` | — | Partial overrides for built-in light/dark only |
112
143
  | `extraThemes` | — | Extra themes: `id`, `label`, `icon`, optional `basedOn`, `palette`; reuse a built-in id (`sepia`/`ocean`) to override it. Import `builtinThemePalettes` for full defaults |
113
144
 
145
+ ## Shell actions (`shell.actions`)
146
+
147
+ Use `shell.actions` to render app-specific Vue components in the active layout's global toolbar.
148
+ In top/tiered navigation they appear at the end of the top header; in sidebar-only navigation they
149
+ appear in the sidebar footer after the built-in links, locale switcher, and theme switcher. This is
150
+ the recommended place for a signed-in user avatar, notification button, help menu, or tenant
151
+ switcher.
152
+
153
+ ```typescript
154
+ export default defineConfig({
155
+ title: 'My Site',
156
+ shell: {
157
+ actions: ['./components/UserAvatar.vue'],
158
+ align: 'left',
159
+ },
160
+ nav: [/* ... */],
161
+ })
162
+ ```
163
+
164
+ With the CLI, each path-like string is rewritten to a dynamic import. In library mode, pass an
165
+ imported component or loader instead:
166
+
167
+ ```typescript
168
+ import UserAvatar from './components/UserAvatar.vue'
169
+
170
+ export default defineConfig({
171
+ shell: {
172
+ actions: [UserAvatar, () => import('./components/HelpMenu.vue')],
173
+ align: 'center',
174
+ },
175
+ nav: [/* ... */],
176
+ })
177
+ ```
178
+
179
+ The framework only renders the component; auth state, user data, menus, and logout behavior stay in
180
+ your component so the framework does not need to define a user model.
181
+ Set `align` to `'left'`, `'center'`, or `'right'` for sidebar-footer placement; it defaults to
182
+ `'center'`. Top-header placement keeps the built-in toolbar flow.
183
+
114
184
  ## Internationalization (`i18n`)
115
185
 
116
186
  Set `i18n` to enable multi-language support. The framework adds a locale switcher to the header,
@@ -461,7 +531,7 @@ app.mount('#app')
461
531
 
462
532
  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.
463
533
 
464
- Exports: `createSiteApp`, `defineConfig`, `useTheme`, `useSiteConfig`, `useLocale`, `useLocalize`, `tk`, `resolveLocalized`, `resolveField`, `resolveMessage`, `mergeCatalog`, `flattenMessages`, `isMessageRef`, `localizedPage`, `builtinMessages`, `themeRefKey`, `localeRefKey`. Types: `SiteConfig`, `SiteEnvConfig`, `SiteViteConfig`, `SiteExternalLink`, `NavItem`, `StandalonePage`, `AuthRule`, `AuthContext`, `AuthConfig`, `RouterConfig`, `ThemeConfig`, `ThemeOption`, `ThemePaletteVars`, `ResolvedNavItem`, `I18nConfig`, `LocaleOption`, `LocaleCode`, `LocalizedString`, `MessageRef`, `MessageTree`, `MessageCatalog`, `PageLoader`, `LocalizedPageOptions`.
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`.
465
535
 
466
536
  ### Theme in Vue pages (`useTheme`)
467
537
 
package/README.zh.md CHANGED
@@ -29,6 +29,7 @@ import { defineConfig } from '@bndynet/vue-site'
29
29
 
30
30
  export default defineConfig({
31
31
  title: 'My Project',
32
+ favicon: '/favicon.ico',
32
33
  nav: [
33
34
  { label: 'Home', icon: 'home', page: () => import('./README.md?raw') },
34
35
  { label: 'Guide', icon: 'book-open', page: () => import('./pages/guide.md?raw') },
@@ -42,6 +43,7 @@ export default defineConfig({
42
43
  my-site/
43
44
  package.json
44
45
  site.config.ts
46
+ public/favicon.ico
45
47
  README.md
46
48
  pages/guide.md
47
49
  ```
@@ -72,7 +74,9 @@ npx vue-site build --base /app/
72
74
  |----------|-------------|
73
75
  | `title` | 站点标题(侧边栏 + 标签页)。`LocalizedString` |
74
76
  | `nav` | `NavItem[]` |
77
+ | `navPosition` | 可选的导航位置:`'top'` 强制使用顶部/分层导航,`'sidebar'` 强制将完整导航树放在侧边栏。不传时保持基于导航深度的自动行为 |
75
78
  | `defaultPath` | 站点打开时的路径;`/` 和未知路径会重定向到此。必须匹配一个已注册的路由(某个 `nav` 条目解析后的路径,或某个 `pages` 条目的 `path`)。默认为第一个顶级 `nav` 条目 |
79
+ | `favicon` | 浏览器标签页图标 URL。可使用 public 资源路径、远程 URL 或导入后的资源 URL |
76
80
  | `logo` | Logo 的 URL 或导入的图片 |
77
81
  | `theme` | 见下方 `ThemeConfig`;设为 `false` 可禁用主题(隐藏切换器、强制使用固定的 `light` 调色板、不进行 localStorage 持久化) |
78
82
  | `i18n` | 多语言配置(`I18nConfig`)—— 参见[国际化](#国际化-i18n) |
@@ -95,11 +99,36 @@ npx vue-site build --base /app/
95
99
  | `icon` | [Lucide](https://lucide.dev/icons) 图标名 |
96
100
  | `page` | 页面内容。最简单的是一个**文件路径字符串**,如 `'./pages/AdminView.vue'` 或 `'./README.md'`(自动加载各语言的同名文件,找不到时回退到基础文件)。也接受加载器(`() => import('./Page.vue')` / `() => import('./page.md?raw')`)或 `localizedPage(...)` 的返回值。见[按语言区分的页面内容](#按语言区分的页面内容)与[高级页面加载器](#高级页面加载器) |
97
101
  | `path` | 路由路径(省略时从 `label` 的默认语言值派生;切换语言时保持稳定) |
102
+ | `layout` | 页面内容宽度:`'default'` 保持标准居中阅读宽度,`'wide'` 使用更宽的居中画布,`'full'` 填满可用父容器。对分组和链接无效。 |
98
103
  | `children` | 嵌套分组 |
99
104
  | `link` | 渲染为超链接(内部路由路径或外部 URL),而非页面路由 |
100
105
  | `visible` | `() => boolean \| Promise<boolean>`,在启动时等待执行一次。返回 `false` 会从导航中隐藏该条目并跳过其路由(无法通过直接 URL 访问)。被隐藏的父项会隐藏其整个子树;没有剩余子项的分组会被剪除。对后续变化不具响应性。 |
101
106
  | `auth` | 由 `auth.authorize` 解释的授权规则(`AuthRule`)。会保持路由注册并通过导航守卫强制执行(因此直接访问 URL 会重定向到登录页)。需要配置 `SiteConfig.auth`。参见[按页面授权](#按页面授权-auth)。 |
102
107
 
108
+ ### `StandalonePage`
109
+
110
+ | 属性 | 说明 |
111
+ |----------|-------------|
112
+ | `path` | 路由路径,按原样使用(如 `/landing`) |
113
+ | `page` | 页面内容。接受形式与 `NavItem.page` 相同 |
114
+ | `layout` | 可选内容宽度。省略时保持 standalone 默认行为:全屏、无内边距画布。 |
115
+ | `visible` | `() => boolean \| Promise<boolean>`,在启动时等待执行一次。返回 `false` 会跳过该路由注册。 |
116
+ | `auth` | 授权规则(`AuthRule`),由与 `NavItem.auth` 相同的导航守卫强制执行。 |
117
+
118
+ ### 页面布局
119
+
120
+ 当默认阅读宽度太窄时,可以在 `NavItem` 页面上设置 `layout`:
121
+
122
+ ```typescript
123
+ export default defineConfig({
124
+ nav: [
125
+ { label: 'Docs', icon: 'book', page: './pages/docs.md' },
126
+ { label: 'Reports', icon: 'table', layout: 'wide', page: './pages/reports.vue' },
127
+ { label: 'Dashboard', icon: 'gauge', layout: 'full', page: './pages/dashboard.vue' },
128
+ ],
129
+ })
130
+ ```
131
+
103
132
  ### `ThemeConfig`
104
133
 
105
134
  内置主题为 `light`、`dark`,外加始终可用的额外主题 `sepia` 和 `ocean`(在每个站点的切换器中都会显示)。在 `SiteConfig` 上设置 `theme: false` 可完全禁用主题。
@@ -468,7 +497,7 @@ app.mount('#app')
468
497
  返回 `Promise` 时会 **await** 它。如果你在配置中设置了可选的 `bootstrap`,该模块会在应用创建前加载;
469
498
  如果省略 `bootstrap`,则跳过该步骤。
470
499
 
471
- 导出:`createSiteApp`、`defineConfig`、`useTheme`、`useSiteConfig`、`useLocale`、`useLocalize`、`tk`、`resolveLocalized`、`resolveField`、`resolveMessage`、`mergeCatalog`、`flattenMessages`、`isMessageRef`、`localizedPage`、`builtinMessages`、`themeRefKey`、`localeRefKey`。类型:`SiteConfig`、`SiteEnvConfig`、`SiteViteConfig`、`SiteExternalLink`、`NavItem`、`StandalonePage`、`AuthRule`、`AuthContext`、`AuthConfig`、`RouterConfig`、`ThemeConfig`、`ThemeOption`、`ThemePaletteVars`、`ResolvedNavItem`、`I18nConfig`、`LocaleOption`、`LocaleCode`、`LocalizedString`、`MessageRef`、`MessageTree`、`MessageCatalog`、`PageLoader`、`LocalizedPageOptions`。
500
+ 导出:`createSiteApp`、`defineConfig`、`useTheme`、`useSiteConfig`、`useLocale`、`useLocalize`、`tk`、`resolveLocalized`、`resolveField`、`resolveMessage`、`mergeCatalog`、`flattenMessages`、`isMessageRef`、`localizedPage`、`builtinMessages`、`themeRefKey`、`localeRefKey`。类型:`SiteConfig`、`SiteEnvConfig`、`SiteViteConfig`、`SiteExternalLink`、`NavItem`、`StandalonePage`、`PageLayout`、`AuthRule`、`AuthContext`、`AuthConfig`、`RouterConfig`、`ThemeConfig`、`ThemeOption`、`ThemePaletteVars`、`ResolvedNavItem`、`I18nConfig`、`LocaleOption`、`LocaleCode`、`LocalizedString`、`MessageRef`、`MessageTree`、`MessageCatalog`、`PageLoader`、`LocalizedPageOptions`。
472
501
 
473
502
  ### 在 Vue 页面中使用主题(`useTheme`)
474
503
 
package/bin/vue-site.mjs CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  import { createServer, build, preview, mergeConfig } from 'vite'
4
4
  import vue from '@vitejs/plugin-vue'
5
+ import AutoImport from 'unplugin-auto-import/vite'
6
+ import Components from 'unplugin-vue-components/vite'
7
+ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
5
8
  import { resolve, dirname, basename } from 'path'
6
9
  import { fileURLToPath, pathToFileURL } from 'url'
7
10
  import { createRequire } from 'module'
@@ -12,6 +15,9 @@ const __filename = fileURLToPath(import.meta.url)
12
15
  const __dirname = dirname(__filename)
13
16
  const pkgDir = resolve(__dirname, '..')
14
17
  const require = createRequire(import.meta.url)
18
+ const FRAMEWORK_PACKAGE = '@bndynet/vue-site'
19
+ const frameworkEntry = resolve(pkgDir, 'dist/index.es.js')
20
+ const frameworkStyle = resolve(pkgDir, 'dist/style.css')
15
21
 
16
22
  function resolvePkgDir(pkg) {
17
23
  return dirname(require.resolve(`${pkg}/package.json`))
@@ -166,6 +172,25 @@ function resolveBootstrapUrl(path) {
166
172
  return '/' + t.replace(/^\.\//, '')
167
173
  }
168
174
 
175
+ function escapeHtmlAttr(value) {
176
+ return String(value)
177
+ .replace(/&/g, '&amp;')
178
+ .replace(/"/g, '&quot;')
179
+ .replace(/</g, '&lt;')
180
+ }
181
+
182
+ function getConfiguredFavicon(siteConfig) {
183
+ const favicon = siteConfig?.favicon
184
+ return typeof favicon === 'string' ? favicon.trim() : ''
185
+ }
186
+
187
+ function buildFaviconLink(siteConfig) {
188
+ const favicon = getConfiguredFavicon(siteConfig)
189
+ return favicon
190
+ ? ` <link rel="icon" href="${escapeHtmlAttr(favicon)}" />\n`
191
+ : ''
192
+ }
193
+
169
194
  // Friendly display names for auto-discovered locale files (`/locales/<code>.json`). Used only when
170
195
  // the config doesn't declare `i18n.locales`; unknown codes fall back to the code itself.
171
196
  const LOCALE_LABELS = {
@@ -208,13 +233,11 @@ function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
208
233
  bs != null && String(bs).trim() !== ''
209
234
  ? `import '${resolveBootstrapUrl(bs)}'\n`
210
235
  : ''
211
- const pkgDirUrl = pkgDir.replace(/\\/g, '/')
212
236
  return [
213
237
  bootstrapImport,
214
- `import 'element-plus/dist/index.css'`,
215
238
  `import 'element-plus/theme-chalk/dark/css-vars.css'`,
216
- `import { createSiteApp } from '${pkgDirUrl}/dist/index.es.js'`,
217
- `import '${pkgDirUrl}/dist/style.css'`,
239
+ `import { createSiteApp } from '${FRAMEWORK_PACKAGE}'`,
240
+ `import '${FRAMEWORK_PACKAGE}/style.css'`,
218
241
  `import siteConfig from '${siteConfigSpecifier}'`,
219
242
  `import { repositoryUrl } from '${VIRTUAL_PACKAGE}'`,
220
243
  ``,
@@ -285,13 +308,13 @@ function buildEntryCode(siteConfig) {
285
308
  })
286
309
  }
287
310
 
288
- function buildHtmlShell(scriptTag) {
311
+ function buildHtmlShell(scriptTag, siteConfig) {
289
312
  return `<!DOCTYPE html>
290
313
  <html lang="en">
291
314
  <head>
292
315
  <meta charset="UTF-8" />
293
316
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
294
- <title></title>
317
+ ${buildFaviconLink(siteConfig)} <title></title>
295
318
  </head>
296
319
  <body>
297
320
  <div id="app"></div>
@@ -300,10 +323,6 @@ function buildHtmlShell(scriptTag) {
300
323
  </html>`
301
324
  }
302
325
 
303
- const htmlTemplate = buildHtmlShell(
304
- `<script type="module" src="/@id/__x00__${VIRTUAL_ENTRY}"></script>`,
305
- )
306
-
307
326
  // esbuild plugin stubbing asset / SFC / `?raw` imports (static or dynamic) to an empty default
308
327
  // export, so bundling the config for pre-load doesn't choke on resources Node can't load. Page
309
328
  // loaders are never invoked during pre-load (only build-time settings are read).
@@ -432,19 +451,41 @@ function fileToLocaleGlobExpr(rawPath) {
432
451
  // per-locale files get bundled. Two shapes are rewritten in the user's JS/TS under `cwd`:
433
452
  // localizedPage('./file.md') -> localizedPage(import.meta.glob([...], { query: '?raw' }))
434
453
  // page: './file.md' -> page: import.meta.glob([...], { query: '?raw' })
454
+ // shell.actions: ['./User.vue'] -> shell.actions: [() => import('./User.vue')]
435
455
  // Loader functions, locale maps (`localizedPage({ en, zh })`) and explicit `import.meta.glob` are
436
- // left untouched; the `page:` form only rewrites path-like values (starting with `.` or `/`).
437
- function localizedPageSugarPlugin() {
456
+ // left untouched; the string forms only rewrite path-like values (starting with `.` or `/`).
457
+ function configSugarPlugin() {
438
458
  const CALL_STRING_ARG = /(\blocalizedPage\s*\(\s*)(['"`])((?:\\.|(?!\2).)*)\2/g
439
459
  const PAGE_STRING_FIELD = /(\bpage\s*:\s*)(['"`])((?:\\.|(?!\2).)*)\2/g
460
+ const SHELL_ACTIONS_ARRAY = /(\bshell\s*:\s*\{[\s\S]*?\bactions\s*:\s*\[)([\s\S]*?)(\])/g
461
+ const STRING_LITERAL = /(['"`])((?:\\.|(?!\1).)*)\1/g
462
+
463
+ function rewriteShellActionArray(body) {
464
+ return body.replace(STRING_LITERAL, (match, _q, rawPath, offset) => {
465
+ if (!/^[./]/.test(rawPath)) return match
466
+
467
+ const before = body.slice(0, offset)
468
+ const after = body.slice(offset + match.length)
469
+ if (!/(^|,)\s*$/.test(before) || !/^\s*(,|$)/.test(after)) return match
470
+
471
+ return `() => import(${JSON.stringify(rawPath)})`
472
+ })
473
+ }
474
+
440
475
  return {
441
- name: 'vue-site:localized-page-sugar',
476
+ name: 'vue-site:config-sugar',
442
477
  enforce: 'pre',
443
478
  transform(code, id) {
444
479
  const file = id.split('?')[0]
445
480
  if (!/\.(?:[cm]?[jt]sx?)$/.test(file)) return
446
481
  if (!file.startsWith(cwd) || file.includes('/node_modules/')) return
447
- if (!code.includes('localizedPage(') && !/\bpage\s*:\s*['"`]/.test(code)) return
482
+ if (
483
+ !code.includes('localizedPage(') &&
484
+ !/\bpage\s*:\s*['"`]/.test(code) &&
485
+ !/\bshell\s*:\s*\{/.test(code)
486
+ ) {
487
+ return
488
+ }
448
489
  let changed = false
449
490
  let out = code.replace(CALL_STRING_ARG, (match, head, _q, rawPath) => {
450
491
  const expr = fileToLocaleGlobExpr(rawPath)
@@ -460,12 +501,18 @@ function localizedPageSugarPlugin() {
460
501
  changed = true
461
502
  return `${head}${expr}`
462
503
  })
504
+ out = out.replace(SHELL_ACTIONS_ARRAY, (match, head, body, tail) => {
505
+ const nextBody = rewriteShellActionArray(body)
506
+ if (nextBody === body) return match
507
+ changed = true
508
+ return `${head}${nextBody}${tail}`
509
+ })
463
510
  return changed ? { code: out, map: null } : undefined
464
511
  },
465
512
  }
466
513
  }
467
514
 
468
- function vueSitePlugin(entryCode) {
515
+ function vueSitePlugin(entryCode, htmlTemplate) {
469
516
  return [
470
517
  {
471
518
  name: 'vue-site:virtual-entry',
@@ -617,6 +664,10 @@ async function buildViteConfig(options = {}) {
617
664
  }
618
665
 
619
666
  const entryCode = buildEntryCode(siteConfig)
667
+ const htmlTemplate = buildHtmlShell(
668
+ `<script type="module" src="/@id/__x00__${VIRTUAL_ENTRY}"></script>`,
669
+ siteConfig,
670
+ )
620
671
 
621
672
  // When watchPackages uses entryPath, SCSS files imported inside the watched
622
673
  // package source (e.g. Lit web components with `import styles from './foo.scss'`
@@ -649,24 +700,49 @@ async function buildViteConfig(options = {}) {
649
700
  },
650
701
  ]
651
702
  : []
703
+ const elementPlusResolver = ElementPlusResolver({
704
+ importStyle: 'css',
705
+ })
652
706
 
653
707
  const baseConfig = {
654
708
  root: cwd,
655
- plugins: [localizedPageSugarPlugin(), vue(vueOpts), ...watchedScssPlugin, ...vueSitePlugin(entryCode), ...(userPlugins || [])],
709
+ plugins: [
710
+ configSugarPlugin(),
711
+ AutoImport({
712
+ resolvers: [elementPlusResolver],
713
+ dts: false,
714
+ }),
715
+ Components({
716
+ resolvers: [elementPlusResolver],
717
+ dts: false,
718
+ }),
719
+ vue(vueOpts),
720
+ ...watchedScssPlugin,
721
+ ...vueSitePlugin(entryCode, htmlTemplate),
722
+ ...(userPlugins || []),
723
+ ],
656
724
  resolve: {
657
- alias: {
658
- vue: resolve(vuePath, 'dist/vue.runtime.esm-bundler.js'),
659
- 'vue-router': resolve(vueRouterPath, 'dist/vue-router.mjs'),
660
- },
725
+ alias: [
726
+ { find: /^@bndynet\/vue-site$/, replacement: frameworkEntry },
727
+ {
728
+ find: /^@bndynet\/vue-site\/style\.css$/,
729
+ replacement: frameworkStyle,
730
+ },
731
+ {
732
+ find: 'vue',
733
+ replacement: resolve(vuePath, 'dist/vue.runtime.esm-bundler.js'),
734
+ },
735
+ {
736
+ find: 'vue-router',
737
+ replacement: resolve(vueRouterPath, 'dist/vue-router.mjs'),
738
+ },
739
+ ],
661
740
  },
662
741
  optimizeDeps: {
663
- // Pre-bundle element-plus so Vite crawls its dependency graph and
664
- // discovers dayjs (a CJS/UMD package with no ESM default export).
665
- // Without this, dayjs.min.js is served raw to the browser and
666
- // Element Plus throws "does not provide an export named 'default'".
667
- // The virtual entry only imports element-plus CSS, so Vite's static
668
- // analysis never sees the JS side — this include bridges that gap.
669
- include: ['element-plus'],
742
+ // The virtual entry and user-authored pages both import the framework.
743
+ // Keep it out of dependency pre-bundling so Vite does not instantiate
744
+ // `dist/index.es.js` once via /@fs and again via node_modules/.vite.
745
+ exclude: [FRAMEWORK_PACKAGE],
670
746
  },
671
747
  server: {
672
748
  open: true,
@@ -706,6 +782,7 @@ async function run() {
706
782
  })
707
783
  const buildHtml = buildHtmlShell(
708
784
  `<script type="module">\n${bootstrapScript}\n </script>`,
785
+ siteConfig,
709
786
  )
710
787
  fs.writeFileSync(tempHtml, buildHtml)
711
788
  }
@@ -0,0 +1,9 @@
1
+ type __VLS_Props = {
2
+ placement?: 'top' | 'sidebar';
3
+ };
4
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
5
+ placement: "top" | "sidebar";
6
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
7
+ root: HTMLDivElement;
8
+ }, any>;
9
+ export default _default;
@@ -4,6 +4,7 @@ export interface SiteContext {
4
4
  config: SiteConfig;
5
5
  resolvedNav: ResolvedNavItem[];
6
6
  }
7
+ /** Use `Symbol.for` so pages still inject context if Vite resolves the package twice. */
7
8
  export declare const siteContextKey: InjectionKey<SiteContext>;
8
9
  export declare function provideSiteConfig(context: SiteContext): void;
9
10
  export declare function useSiteConfig(): SiteContext;
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, NavItem, StandalonePage, 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, MessageRef, MessageTree, LocaleOption, I18nConfig, PageLoader, } from './types';
13
13
  export declare function defineConfig(config: SiteConfig): SiteConfig;