@bndynet/vue-site 1.2.1 → 1.3.1
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 +95 -1
- package/README.zh.md +30 -1
- package/bin/vue-site.mjs +274 -23
- package/dist/components/DynamicIcon.vue.d.ts +5 -3
- package/dist/components/ShellActions.vue.d.ts +9 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +3900 -32901
- package/dist/style.css +1 -1
- package/dist/types.d.ts +73 -0
- package/package.json +3 -1
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,17 @@ 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) |
|
|
88
|
+
| `icons` | Lucide icon registry. Generated automatically by the CLI; provide manually only when calling `createSiteApp` without the CLI |
|
|
82
89
|
| `pages` | `StandalonePage[]` — full-screen routes outside the `nav` tree (no top bar/sidebar/footer) |
|
|
83
90
|
| `auth` | Central authorization policy (`AuthConfig`) — see [Per-page authorization](#per-page-authorization-auth) |
|
|
84
91
|
| `router` | History mode (`RouterConfig`) — `hash` (default) or HTML5 `web`; see [Router history](#router-history-router) |
|
|
@@ -95,11 +102,59 @@ Add `"dev": "vue-site dev"` (or `vs dev`) in `package.json` scripts if you like.
|
|
|
95
102
|
| `icon` | [Lucide](https://lucide.dev/icons) name |
|
|
96
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) |
|
|
97
104
|
| `path` | Route path (derived from `label`'s default-locale value if omitted; stays stable across languages) |
|
|
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. |
|
|
98
106
|
| `children` | Nested group |
|
|
99
107
|
| `link` | Render as a hyperlink (internal route path or external URL) instead of a page route |
|
|
100
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. |
|
|
101
109
|
| `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
110
|
|
|
111
|
+
### `StandalonePage`
|
|
112
|
+
|
|
113
|
+
| Property | Description |
|
|
114
|
+
|----------|-------------|
|
|
115
|
+
| `path` | Route path, used as-is (e.g. `/landing`) |
|
|
116
|
+
| `page` | Page content. Same forms as `NavItem.page` |
|
|
117
|
+
| `layout` | Optional content width. Omit to keep the standalone default: a full-screen, unpadded canvas. |
|
|
118
|
+
| `visible` | `() => boolean \| Promise<boolean>`, awaited once at startup. Return `false` to skip registering the route. |
|
|
119
|
+
| `auth` | Authorization rule (`AuthRule`) enforced by the same navigation guard as `NavItem.auth`. |
|
|
120
|
+
|
|
121
|
+
### Page layout
|
|
122
|
+
|
|
123
|
+
Set `layout` on a `NavItem` page when the default reading width is too narrow:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
export default defineConfig({
|
|
127
|
+
nav: [
|
|
128
|
+
{ label: 'Docs', icon: 'book', page: './pages/docs.md' },
|
|
129
|
+
{ label: 'Reports', icon: 'table', layout: 'wide', page: './pages/reports.vue' },
|
|
130
|
+
{ label: 'Dashboard', icon: 'gauge', layout: 'full', page: './pages/dashboard.vue' },
|
|
131
|
+
],
|
|
132
|
+
})
|
|
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
|
+
|
|
103
158
|
### `ThemeConfig`
|
|
104
159
|
|
|
105
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.
|
|
@@ -111,6 +166,45 @@ Built-in themes are `light`, `dark`, plus the always-on extras `sepia` and `ocea
|
|
|
111
166
|
| `palettes` | — | Partial overrides for built-in light/dark only |
|
|
112
167
|
| `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
168
|
|
|
169
|
+
## Shell actions (`shell.actions`)
|
|
170
|
+
|
|
171
|
+
Use `shell.actions` to render app-specific Vue components in the active layout's global toolbar.
|
|
172
|
+
In top/tiered navigation they appear at the end of the top header; in sidebar-only navigation they
|
|
173
|
+
appear in the sidebar footer after the built-in links, locale switcher, and theme switcher. This is
|
|
174
|
+
the recommended place for a signed-in user avatar, notification button, help menu, or tenant
|
|
175
|
+
switcher.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
export default defineConfig({
|
|
179
|
+
title: 'My Site',
|
|
180
|
+
shell: {
|
|
181
|
+
actions: ['./components/UserAvatar.vue'],
|
|
182
|
+
align: 'left',
|
|
183
|
+
},
|
|
184
|
+
nav: [/* ... */],
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
With the CLI, each path-like string is rewritten to a dynamic import. In library mode, pass an
|
|
189
|
+
imported component or loader instead:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import UserAvatar from './components/UserAvatar.vue'
|
|
193
|
+
|
|
194
|
+
export default defineConfig({
|
|
195
|
+
shell: {
|
|
196
|
+
actions: [UserAvatar, () => import('./components/HelpMenu.vue')],
|
|
197
|
+
align: 'center',
|
|
198
|
+
},
|
|
199
|
+
nav: [/* ... */],
|
|
200
|
+
})
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The framework only renders the component; auth state, user data, menus, and logout behavior stay in
|
|
204
|
+
your component so the framework does not need to define a user model.
|
|
205
|
+
Set `align` to `'left'`, `'center'`, or `'right'` for sidebar-footer placement; it defaults to
|
|
206
|
+
`'center'`. Top-header placement keeps the built-in toolbar flow.
|
|
207
|
+
|
|
114
208
|
## Internationalization (`i18n`)
|
|
115
209
|
|
|
116
210
|
Set `i18n` to enable multi-language support. The framework adds a locale switcher to the header,
|
|
@@ -461,7 +555,7 @@ app.mount('#app')
|
|
|
461
555
|
|
|
462
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.
|
|
463
557
|
|
|
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`.
|
|
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`.
|
|
465
559
|
|
|
466
560
|
### Theme in Vue pages (`useTheme`)
|
|
467
561
|
|
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'
|
|
@@ -11,17 +14,29 @@ import fs from 'fs'
|
|
|
11
14
|
const __filename = fileURLToPath(import.meta.url)
|
|
12
15
|
const __dirname = dirname(__filename)
|
|
13
16
|
const pkgDir = resolve(__dirname, '..')
|
|
14
|
-
const require = createRequire(import.meta.url)
|
|
15
17
|
const FRAMEWORK_PACKAGE = '@bndynet/vue-site'
|
|
16
18
|
const frameworkEntry = resolve(pkgDir, 'dist/index.es.js')
|
|
17
19
|
const frameworkStyle = resolve(pkgDir, 'dist/style.css')
|
|
18
20
|
|
|
19
|
-
function resolvePkgDir(pkg) {
|
|
20
|
-
return dirname(
|
|
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, '/')
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
const vuePath = resolvePkgDir('vue')
|
|
24
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)
|
|
25
40
|
const cwd = process.cwd()
|
|
26
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. */
|
|
27
42
|
const cwdParent = resolve(cwd, '..')
|
|
@@ -110,6 +125,8 @@ const VIRTUAL_ENTRY = 'virtual:vue-site-entry'
|
|
|
110
125
|
const RESOLVED_ENTRY = '\0' + VIRTUAL_ENTRY
|
|
111
126
|
const VIRTUAL_PACKAGE = 'virtual:vue-site-package'
|
|
112
127
|
const RESOLVED_PACKAGE = '\0' + VIRTUAL_PACKAGE
|
|
128
|
+
const VIRTUAL_ICONS = 'virtual:vue-site-icons'
|
|
129
|
+
const RESOLVED_ICONS = '\0' + VIRTUAL_ICONS
|
|
113
130
|
|
|
114
131
|
function parseRepositoryUrl(pkg) {
|
|
115
132
|
const r = pkg.repository
|
|
@@ -169,6 +186,25 @@ function resolveBootstrapUrl(path) {
|
|
|
169
186
|
return '/' + t.replace(/^\.\//, '')
|
|
170
187
|
}
|
|
171
188
|
|
|
189
|
+
function escapeHtmlAttr(value) {
|
|
190
|
+
return String(value)
|
|
191
|
+
.replace(/&/g, '&')
|
|
192
|
+
.replace(/"/g, '"')
|
|
193
|
+
.replace(/</g, '<')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getConfiguredFavicon(siteConfig) {
|
|
197
|
+
const favicon = siteConfig?.favicon
|
|
198
|
+
return typeof favicon === 'string' ? favicon.trim() : ''
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildFaviconLink(siteConfig) {
|
|
202
|
+
const favicon = getConfiguredFavicon(siteConfig)
|
|
203
|
+
return favicon
|
|
204
|
+
? ` <link rel="icon" href="${escapeHtmlAttr(favicon)}" />\n`
|
|
205
|
+
: ''
|
|
206
|
+
}
|
|
207
|
+
|
|
172
208
|
// Friendly display names for auto-discovered locale files (`/locales/<code>.json`). Used only when
|
|
173
209
|
// the config doesn't declare `i18n.locales`; unknown codes fall back to the code itself.
|
|
174
210
|
const LOCALE_LABELS = {
|
|
@@ -213,12 +249,12 @@ function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
|
|
|
213
249
|
: ''
|
|
214
250
|
return [
|
|
215
251
|
bootstrapImport,
|
|
216
|
-
`import 'element-plus/dist/index.css'`,
|
|
217
252
|
`import 'element-plus/theme-chalk/dark/css-vars.css'`,
|
|
218
253
|
`import { createSiteApp } from '${FRAMEWORK_PACKAGE}'`,
|
|
219
254
|
`import '${FRAMEWORK_PACKAGE}/style.css'`,
|
|
220
255
|
`import siteConfig from '${siteConfigSpecifier}'`,
|
|
221
256
|
`import { repositoryUrl } from '${VIRTUAL_PACKAGE}'`,
|
|
257
|
+
`import { iconRegistry } from '${VIRTUAL_ICONS}'`,
|
|
222
258
|
``,
|
|
223
259
|
`// Auto-discover translations: /locales/<code>.json -> { [code]: { ...messages } }.`,
|
|
224
260
|
`const __localeFiles = import.meta.glob('/locales/*.json', { eager: true, import: 'default' })`,
|
|
@@ -274,6 +310,7 @@ function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
|
|
|
274
310
|
` ...(hasThemeQuery ? { theme: { ...(siteConfig.theme || {}), default: resolvedTheme } } : {}),`,
|
|
275
311
|
` packageRepository: repositoryUrl,`,
|
|
276
312
|
` baseUrl: import.meta.env.BASE_URL,`,
|
|
313
|
+
` icons: { ...iconRegistry, ...(siteConfig.icons || {}) },`,
|
|
277
314
|
` })`,
|
|
278
315
|
` app.mount('#app')`,
|
|
279
316
|
`})()`,
|
|
@@ -287,13 +324,13 @@ function buildEntryCode(siteConfig) {
|
|
|
287
324
|
})
|
|
288
325
|
}
|
|
289
326
|
|
|
290
|
-
function buildHtmlShell(scriptTag) {
|
|
327
|
+
function buildHtmlShell(scriptTag, siteConfig) {
|
|
291
328
|
return `<!DOCTYPE html>
|
|
292
329
|
<html lang="en">
|
|
293
330
|
<head>
|
|
294
331
|
<meta charset="UTF-8" />
|
|
295
332
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
296
|
-
<title></title>
|
|
333
|
+
${buildFaviconLink(siteConfig)} <title></title>
|
|
297
334
|
</head>
|
|
298
335
|
<body>
|
|
299
336
|
<div id="app"></div>
|
|
@@ -302,10 +339,6 @@ function buildHtmlShell(scriptTag) {
|
|
|
302
339
|
</html>`
|
|
303
340
|
}
|
|
304
341
|
|
|
305
|
-
const htmlTemplate = buildHtmlShell(
|
|
306
|
-
`<script type="module" src="/@id/__x00__${VIRTUAL_ENTRY}"></script>`,
|
|
307
|
-
)
|
|
308
|
-
|
|
309
342
|
// esbuild plugin stubbing asset / SFC / `?raw` imports (static or dynamic) to an empty default
|
|
310
343
|
// export, so bundling the config for pre-load doesn't choke on resources Node can't load. Page
|
|
311
344
|
// loaders are never invoked during pre-load (only build-time settings are read).
|
|
@@ -434,19 +467,41 @@ function fileToLocaleGlobExpr(rawPath) {
|
|
|
434
467
|
// per-locale files get bundled. Two shapes are rewritten in the user's JS/TS under `cwd`:
|
|
435
468
|
// localizedPage('./file.md') -> localizedPage(import.meta.glob([...], { query: '?raw' }))
|
|
436
469
|
// page: './file.md' -> page: import.meta.glob([...], { query: '?raw' })
|
|
470
|
+
// shell.actions: ['./User.vue'] -> shell.actions: [() => import('./User.vue')]
|
|
437
471
|
// Loader functions, locale maps (`localizedPage({ en, zh })`) and explicit `import.meta.glob` are
|
|
438
|
-
// left untouched; the
|
|
439
|
-
function
|
|
472
|
+
// left untouched; the string forms only rewrite path-like values (starting with `.` or `/`).
|
|
473
|
+
function configSugarPlugin() {
|
|
440
474
|
const CALL_STRING_ARG = /(\blocalizedPage\s*\(\s*)(['"`])((?:\\.|(?!\2).)*)\2/g
|
|
441
475
|
const PAGE_STRING_FIELD = /(\bpage\s*:\s*)(['"`])((?:\\.|(?!\2).)*)\2/g
|
|
476
|
+
const SHELL_ACTIONS_ARRAY = /(\bshell\s*:\s*\{[\s\S]*?\bactions\s*:\s*\[)([\s\S]*?)(\])/g
|
|
477
|
+
const STRING_LITERAL = /(['"`])((?:\\.|(?!\1).)*)\1/g
|
|
478
|
+
|
|
479
|
+
function rewriteShellActionArray(body) {
|
|
480
|
+
return body.replace(STRING_LITERAL, (match, _q, rawPath, offset) => {
|
|
481
|
+
if (!/^[./]/.test(rawPath)) return match
|
|
482
|
+
|
|
483
|
+
const before = body.slice(0, offset)
|
|
484
|
+
const after = body.slice(offset + match.length)
|
|
485
|
+
if (!/(^|,)\s*$/.test(before) || !/^\s*(,|$)/.test(after)) return match
|
|
486
|
+
|
|
487
|
+
return `() => import(${JSON.stringify(rawPath)})`
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
|
|
442
491
|
return {
|
|
443
|
-
name: 'vue-site:
|
|
492
|
+
name: 'vue-site:config-sugar',
|
|
444
493
|
enforce: 'pre',
|
|
445
494
|
transform(code, id) {
|
|
446
495
|
const file = id.split('?')[0]
|
|
447
496
|
if (!/\.(?:[cm]?[jt]sx?)$/.test(file)) return
|
|
448
497
|
if (!file.startsWith(cwd) || file.includes('/node_modules/')) return
|
|
449
|
-
if (
|
|
498
|
+
if (
|
|
499
|
+
!code.includes('localizedPage(') &&
|
|
500
|
+
!/\bpage\s*:\s*['"`]/.test(code) &&
|
|
501
|
+
!/\bshell\s*:\s*\{/.test(code)
|
|
502
|
+
) {
|
|
503
|
+
return
|
|
504
|
+
}
|
|
450
505
|
let changed = false
|
|
451
506
|
let out = code.replace(CALL_STRING_ARG, (match, head, _q, rawPath) => {
|
|
452
507
|
const expr = fileToLocaleGlobExpr(rawPath)
|
|
@@ -462,18 +517,160 @@ function localizedPageSugarPlugin() {
|
|
|
462
517
|
changed = true
|
|
463
518
|
return `${head}${expr}`
|
|
464
519
|
})
|
|
520
|
+
out = out.replace(SHELL_ACTIONS_ARRAY, (match, head, body, tail) => {
|
|
521
|
+
const nextBody = rewriteShellActionArray(body)
|
|
522
|
+
if (nextBody === body) return match
|
|
523
|
+
changed = true
|
|
524
|
+
return `${head}${nextBody}${tail}`
|
|
525
|
+
})
|
|
465
526
|
return changed ? { code: out, map: null } : undefined
|
|
466
527
|
},
|
|
467
528
|
}
|
|
468
529
|
}
|
|
469
530
|
|
|
470
|
-
|
|
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) {
|
|
471
667
|
return [
|
|
472
668
|
{
|
|
473
669
|
name: 'vue-site:virtual-entry',
|
|
474
670
|
resolveId(id) {
|
|
475
671
|
if (id === VIRTUAL_ENTRY) return RESOLVED_ENTRY
|
|
476
672
|
if (id === VIRTUAL_PACKAGE) return RESOLVED_PACKAGE
|
|
673
|
+
if (id === VIRTUAL_ICONS) return RESOLVED_ICONS
|
|
477
674
|
},
|
|
478
675
|
load(id) {
|
|
479
676
|
if (id === RESOLVED_ENTRY) return entryCode
|
|
@@ -481,6 +678,7 @@ function vueSitePlugin(entryCode) {
|
|
|
481
678
|
const url = readPackageRepositoryUrl()
|
|
482
679
|
return `export const repositoryUrl = ${JSON.stringify(url)}`
|
|
483
680
|
}
|
|
681
|
+
if (id === RESOLVED_ICONS) return iconRegistryCode
|
|
484
682
|
},
|
|
485
683
|
},
|
|
486
684
|
{
|
|
@@ -619,6 +817,11 @@ async function buildViteConfig(options = {}) {
|
|
|
619
817
|
}
|
|
620
818
|
|
|
621
819
|
const entryCode = buildEntryCode(siteConfig)
|
|
820
|
+
const iconRegistryCode = buildIconRegistryCode(siteConfig)
|
|
821
|
+
const htmlTemplate = buildHtmlShell(
|
|
822
|
+
`<script type="module" src="/@id/__x00__${VIRTUAL_ENTRY}"></script>`,
|
|
823
|
+
siteConfig,
|
|
824
|
+
)
|
|
622
825
|
|
|
623
826
|
// When watchPackages uses entryPath, SCSS files imported inside the watched
|
|
624
827
|
// package source (e.g. Lit web components with `import styles from './foo.scss'`
|
|
@@ -651,10 +854,27 @@ async function buildViteConfig(options = {}) {
|
|
|
651
854
|
},
|
|
652
855
|
]
|
|
653
856
|
: []
|
|
857
|
+
const elementPlusResolver = ElementPlusResolver({
|
|
858
|
+
importStyle: 'css',
|
|
859
|
+
})
|
|
654
860
|
|
|
655
861
|
const baseConfig = {
|
|
656
862
|
root: cwd,
|
|
657
|
-
plugins: [
|
|
863
|
+
plugins: [
|
|
864
|
+
configSugarPlugin(),
|
|
865
|
+
AutoImport({
|
|
866
|
+
resolvers: [elementPlusResolver],
|
|
867
|
+
dts: false,
|
|
868
|
+
}),
|
|
869
|
+
Components({
|
|
870
|
+
resolvers: [elementPlusResolver],
|
|
871
|
+
dts: false,
|
|
872
|
+
}),
|
|
873
|
+
vue(vueOpts),
|
|
874
|
+
...watchedScssPlugin,
|
|
875
|
+
...vueSitePlugin(entryCode, htmlTemplate, iconRegistryCode),
|
|
876
|
+
...(userPlugins || []),
|
|
877
|
+
],
|
|
658
878
|
resolve: {
|
|
659
879
|
alias: [
|
|
660
880
|
{ find: /^@bndynet\/vue-site$/, replacement: frameworkEntry },
|
|
@@ -670,20 +890,50 @@ async function buildViteConfig(options = {}) {
|
|
|
670
890
|
find: 'vue-router',
|
|
671
891
|
replacement: resolve(vueRouterPath, 'dist/vue-router.mjs'),
|
|
672
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
|
+
},
|
|
673
925
|
],
|
|
674
926
|
},
|
|
675
927
|
optimizeDeps: {
|
|
676
|
-
// Pre-bundle element-plus so Vite crawls its dependency graph and
|
|
677
|
-
// discovers dayjs (a CJS/UMD package with no ESM default export).
|
|
678
|
-
// Without this, dayjs.min.js is served raw to the browser and
|
|
679
|
-
// Element Plus throws "does not provide an export named 'default'".
|
|
680
|
-
// The virtual entry only imports element-plus CSS, so Vite's static
|
|
681
|
-
// analysis never sees the JS side — this include bridges that gap.
|
|
682
|
-
include: ['element-plus'],
|
|
683
928
|
// The virtual entry and user-authored pages both import the framework.
|
|
684
929
|
// Keep it out of dependency pre-bundling so Vite does not instantiate
|
|
685
930
|
// `dist/index.es.js` once via /@fs and again via node_modules/.vite.
|
|
686
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'],
|
|
687
937
|
},
|
|
688
938
|
server: {
|
|
689
939
|
open: true,
|
|
@@ -723,6 +973,7 @@ async function run() {
|
|
|
723
973
|
})
|
|
724
974
|
const buildHtml = buildHtmlShell(
|
|
725
975
|
`<script type="module">\n${bootstrapScript}\n </script>`,
|
|
976
|
+
siteConfig,
|
|
726
977
|
)
|
|
727
978
|
fs.writeFileSync(tempHtml, buildHtml)
|
|
728
979
|
}
|