@bndynet/vue-site 1.3.2 → 1.4.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 CHANGED
@@ -23,6 +23,15 @@ Configurable Vue 3 site framework: one package, `site.config.ts`, and Markdown p
23
23
  npm install @bndynet/vue-site
24
24
  ```
25
25
 
26
+ Create starter files in the current directory:
27
+
28
+ ```bash
29
+ npx vue-site init
30
+ ```
31
+
32
+ This creates `site.config.ts`, `env.d.ts`, and `README.md`. Existing files are never overwritten.
33
+ You can also create the configuration manually:
34
+
26
35
  **`site.config.ts`**
27
36
 
28
37
  ```typescript
@@ -44,6 +53,7 @@ export default defineConfig({
44
53
  my-site/
45
54
  package.json
46
55
  site.config.ts
56
+ env.d.ts
47
57
  public/favicon.ico
48
58
  README.md
49
59
  pages/guide.md
@@ -52,15 +62,32 @@ my-site/
52
62
  **CLI** (`vue-site` and `vs` are the same)
53
63
 
54
64
  ```bash
65
+ npx vue-site init
55
66
  npx vue-site dev
56
67
  npx vue-site build
57
68
  npx vue-site preview
58
69
  ```
59
70
 
71
+ `init` is safe to run more than once: it creates only missing starter files and leaves existing
72
+ configuration, type declarations, and README content unchanged.
73
+
74
+ Use a different config for an environment with `--config` / `-c`. The selected file must live in
75
+ the current site root and may use the `.ts`, `.js`, `.mts`, or `.mjs` extension. `dev`, `build`, and
76
+ `preview` all support the option:
77
+
78
+ ```bash
79
+ npx vue-site dev --config site.config.dev.ts
80
+ npx vue-site build --config site.config.prod.ts
81
+ npx vue-site preview -c site.config.prod.ts
82
+ ```
83
+
84
+ When `--config` is omitted, the CLI keeps auto-discovering `site.config.ts`, `site.config.js`,
85
+ `site.config.mts`, or `site.config.mjs` in that order.
86
+
60
87
  Subpath deploy: pass Vite’s public base on the CLI (overrides `env.vite.base` in `site.config`):
61
88
 
62
89
  ```bash
63
- npx vue-site build --base=/app/
90
+ npx vue-site build --config site.config.prod.ts --base=/app/
64
91
  # or
65
92
  npx vue-site build --base /app/
66
93
  ```
@@ -90,10 +117,67 @@ Add `"dev": "vue-site dev"` (or `vs dev`) in `package.json` scripts if you like.
90
117
  | `auth` | Central authorization policy (`AuthConfig`) — see [Per-page authorization](#per-page-authorization-auth) |
91
118
  | `router` | History mode (`RouterConfig`) — `hash` (default) or HTML5 `web`; see [Router history](#router-history-router) |
92
119
  | `packageRepository` | Usually set by CLI from `package.json`; omit when using `createSiteApp` alone |
120
+ | `custom` | Public application-specific values, available from `useSiteConfig().config.custom`; see [Custom client configuration](#custom-client-configuration-custom) |
93
121
  | `env` | Dev/build options — see below |
94
122
  | `bootstrap` | Optional path from site root (e.g. `./bootstrap.ts`) — module loaded once before the Vue app |
95
123
  | `configureApp` | Optional `(app) => void \| Promise<void>` after router install, before `mount` (see [Local packages in `configureApp`](#local-packages-in-configureapp)) |
96
124
 
125
+ ### Custom client configuration (`custom`)
126
+
127
+ Use `custom` for public, application-specific values that the framework should preserve without
128
+ interpreting. They are available to every component through `useSiteConfig()`:
129
+
130
+ ```typescript
131
+ // site.config.ts
132
+ export default defineConfig({
133
+ title: 'My Site',
134
+ nav: [/* ... */],
135
+ custom: {
136
+ apiBaseUrl: 'http://localhost/api/v1',
137
+ },
138
+ })
139
+ ```
140
+
141
+ ```typescript
142
+ // Inside a Vue component
143
+ const { config } = useSiteConfig()
144
+ const apiBaseUrl = config.custom?.apiBaseUrl
145
+ ```
146
+
147
+ Use a different config file when the value changes between builds:
148
+
149
+ ```typescript
150
+ // site.config.prod.ts
151
+ export default defineConfig({
152
+ title: 'My Site',
153
+ nav: [/* ... */],
154
+ custom: {
155
+ apiBaseUrl: 'https://prod.com/api/v1',
156
+ },
157
+ })
158
+ ```
159
+
160
+ ```bash
161
+ npx vue-site build --config site.config.prod.ts
162
+ ```
163
+
164
+ `custom` is typed as `SiteCustomConfig`. Application-specific keys can be made type-safe with
165
+ module augmentation:
166
+
167
+ ```typescript
168
+ // site-custom.d.ts
169
+ import '@bndynet/vue-site'
170
+
171
+ declare module '@bndynet/vue-site' {
172
+ interface SiteCustomConfig {
173
+ apiBaseUrl: string
174
+ }
175
+ }
176
+ ```
177
+
178
+ All `custom` values are bundled into client code and are visible to users. Never store passwords,
179
+ tokens, private keys, or other secrets in this field.
180
+
97
181
  ### `NavItem`
98
182
 
99
183
  | Property | Description |
@@ -454,7 +538,7 @@ export default defineConfig({
454
538
 
455
539
  - `authorize` runs **on every navigation** (a Vue Router `beforeEach` guard). It receives `{ rule, item, to, from }` and returns `true` (allow), `false` (deny), or a path `string` (redirect, e.g. to a login page).
456
540
  - On `false`, the user is sent to `auth.loginPath` with the requested path as a `redirect` query (`/login?redirect=/admin`); if `loginPath` is unset, the navigation is cancelled.
457
- - `authorize` also runs **once at startup** (with only `rule` / `item`) to hide unauthorized items from the nav menu. Like `visible`, this menu filtering is not reactive it reflects the state at app creation, so update it by recreating the app (e.g. a full reload after login).
541
+ - `authorize` also runs when the auth-filtered nav menu is refreshed (with only `rule` / `item`) to hide unauthorized items from the nav menu. The framework refreshes this menu after navigations; after changing auth state without a navigation, call `useSiteConfig().refreshAuthNav()`.
458
542
  - Guarded routes stay **registered**, so visiting a protected URL directly triggers the guard (and your login redirect) rather than silently 404-ing.
459
543
  - The login page itself must **not** carry an `auth` rule (and `loginPath` is always allowed by the guard) to avoid redirect loops.
460
544
 
@@ -472,9 +556,9 @@ export default defineConfig({
472
556
  | | `visible` | `auth` |
473
557
  |--|-----------|--------|
474
558
  | Decides | Whether the item/route **exists** | Whether the **current user** may enter |
475
- | When | Build/startup (once) | Navigation (every time) + startup for menu filtering |
559
+ | When | Build/startup (once) | Navigation (every time) + refreshed menu filtering |
476
560
  | Route registered | No (unreachable by URL) | Yes (guarded; can redirect to login) |
477
- | Reacts to login/logout | No | Guard yes; menu filtering no |
561
+ | Reacts to login/logout | No | Guard yes; menu filtering yes after navigation or `refreshAuthNav()` |
478
562
  | Best for | Env / feature-flag / static trimming | Login state, roles, login redirects |
479
563
 
480
564
  Use `visible` for static existence trimming and `auth` for user-based access. They can be combined on the same item.
@@ -555,7 +639,7 @@ app.mount('#app')
555
639
 
556
640
  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.
557
641
 
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`.
642
+ Exports: `createSiteApp`, `defineConfig`, `useTheme`, `useSiteConfig`, `useLocale`, `useLocalize`, `tk`, `resolveLocalized`, `resolveField`, `resolveMessage`, `mergeCatalog`, `flattenMessages`, `isMessageRef`, `localizedPage`, `builtinMessages`, `themeRefKey`, `localeRefKey`. Types: `SiteConfig`, `SiteEnvConfig`, `SiteViteConfig`, `SiteCustomConfig`, `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`.
559
643
 
560
644
  ### Theme in Vue pages (`useTheme`)
561
645
 
package/README.zh.md CHANGED
@@ -22,6 +22,15 @@
22
22
  npm install @bndynet/vue-site
23
23
  ```
24
24
 
25
+ 在当前目录创建初始文件:
26
+
27
+ ```bash
28
+ npx vue-site init
29
+ ```
30
+
31
+ 该命令会创建 `site.config.ts`、`env.d.ts` 和 `README.md`,绝不会覆盖已有文件。
32
+ 也可以手动创建配置:
33
+
25
34
  **`site.config.ts`**
26
35
 
27
36
  ```typescript
@@ -43,6 +52,7 @@ export default defineConfig({
43
52
  my-site/
44
53
  package.json
45
54
  site.config.ts
55
+ env.d.ts
46
56
  public/favicon.ico
47
57
  README.md
48
58
  pages/guide.md
@@ -51,15 +61,30 @@ my-site/
51
61
  **CLI**(`vue-site` 与 `vs` 等价)
52
62
 
53
63
  ```bash
64
+ npx vue-site init
54
65
  npx vue-site dev
55
66
  npx vue-site build
56
67
  npx vue-site preview
57
68
  ```
58
69
 
70
+ `init` 可以安全地重复执行:它只创建缺失的初始文件,不会改动已有配置、类型声明或 README。
71
+
72
+ 可以通过 `--config` / `-c` 为不同环境选择配置文件。指定的文件必须直接位于当前站点根目录,
73
+ 扩展名可以是 `.ts`、`.js`、`.mts` 或 `.mjs`;`dev`、`build`、`preview` 都支持该参数:
74
+
75
+ ```bash
76
+ npx vue-site dev --config site.config.dev.ts
77
+ npx vue-site build --config site.config.prod.ts
78
+ npx vue-site preview -c site.config.prod.ts
79
+ ```
80
+
81
+ 省略 `--config` 时,CLI 仍会依次自动查找 `site.config.ts`、`site.config.js`、
82
+ `site.config.mts`、`site.config.mjs`。
83
+
59
84
  子路径部署:在 CLI 上传入 Vite 的公共 base(会覆盖 `site.config` 中的 `env.vite.base`):
60
85
 
61
86
  ```bash
62
- npx vue-site build --base=/app/
87
+ npx vue-site build --config site.config.prod.ts --base=/app/
63
88
  # 或
64
89
  npx vue-site build --base /app/
65
90
  ```
@@ -87,10 +112,65 @@ npx vue-site build --base /app/
87
112
  | `auth` | 中心化的授权策略(`AuthConfig`)—— 参见[按页面授权](#按页面授权-auth) |
88
113
  | `router` | 历史模式(`RouterConfig`)—— `hash`(默认)或 HTML5 `web`;参见[路由历史](#路由历史-router) |
89
114
  | `packageRepository` | 通常由 CLI 从 `package.json` 设置;单独使用 `createSiteApp` 时可省略 |
115
+ | `custom` | 公开的业务自定义配置,可通过 `useSiteConfig().config.custom` 读取;参见[自定义客户端配置](#自定义客户端配置-custom) |
90
116
  | `env` | 开发/构建选项 —— 见下文 |
91
117
  | `bootstrap` | 可选的站点根目录相对路径(如 `./bootstrap.ts`)—— 在 Vue 应用之前加载一次的模块 |
92
118
  | `configureApp` | 可选的 `(app) => void \| Promise<void>`,在路由安装之后、`mount` 之前执行(参见 [`configureApp` 中的本地包](#在-configureapp-中使用本地包)) |
93
119
 
120
+ ### 自定义客户端配置(`custom`)
121
+
122
+ `custom` 用于存放框架无需解释、只需原样保留的公开业务配置。任何组件都可以通过
123
+ `useSiteConfig()` 读取:
124
+
125
+ ```typescript
126
+ // site.config.ts
127
+ export default defineConfig({
128
+ title: '我的站点',
129
+ nav: [/* ... */],
130
+ custom: {
131
+ apiBaseUrl: 'http://localhost/api/v1',
132
+ },
133
+ })
134
+ ```
135
+
136
+ ```typescript
137
+ // Vue 组件中
138
+ const { config } = useSiteConfig()
139
+ const apiBaseUrl = config.custom?.apiBaseUrl
140
+ ```
141
+
142
+ 如果不同构建环境使用不同的值,可以选择不同的配置文件:
143
+
144
+ ```typescript
145
+ // site.config.prod.ts
146
+ export default defineConfig({
147
+ title: '我的站点',
148
+ nav: [/* ... */],
149
+ custom: {
150
+ apiBaseUrl: 'https://prod.com/api/v1',
151
+ },
152
+ })
153
+ ```
154
+
155
+ ```bash
156
+ npx vue-site build --config site.config.prod.ts
157
+ ```
158
+
159
+ `custom` 的类型是 `SiteCustomConfig`。可以通过模块扩充为业务字段提供强类型:
160
+
161
+ ```typescript
162
+ // site-custom.d.ts
163
+ import '@bndynet/vue-site'
164
+
165
+ declare module '@bndynet/vue-site' {
166
+ interface SiteCustomConfig {
167
+ apiBaseUrl: string
168
+ }
169
+ }
170
+ ```
171
+
172
+ 所有 `custom` 值都会进入客户端代码,用户可以查看。不要在这里存放密码、Token、私钥或其他密钥。
173
+
94
174
  ### `NavItem`
95
175
 
96
176
  | 属性 | 说明 |
@@ -378,9 +458,9 @@ export default defineConfig({
378
458
  并返回 `true`(允许)、`false`(拒绝)或一个路径 `string`(重定向,例如到登录页)。
379
459
  - 返回 `false` 时,用户会被发送到 `auth.loginPath`,并把请求的路径作为 `redirect` 查询参数
380
460
  (`/login?redirect=/admin`);如果未设置 `loginPath`,则取消该次导航。
381
- - `authorize` 也会在**启动时运行一次**(仅带 `rule` / `item`),以从导航菜单中隐藏未授权的条目。
382
- 与 `visible` 一样,此菜单过滤不具响应性 —— 它反映应用创建时的状态,因此需要通过重建应用来更新它
383
- (例如登录后整页刷新)。
461
+ - `authorize` 也会在 auth 过滤后的导航菜单刷新时运行(仅带 `rule` / `item`),以从导航菜单中隐藏未授权的条目。
462
+ 框架会在导航完成后刷新此菜单;如果你在没有导航的情况下改变登录状态,请调用
463
+ `useSiteConfig().refreshAuthNav()`。
384
464
  - 受保护的路由仍保持**注册**,因此直接访问受保护 URL 会触发守卫(及你的登录重定向),
385
465
  而不是静默地 404。
386
466
  - 登录页本身**不能**携带 `auth` 规则(且 `loginPath` 始终被守卫允许),以避免重定向循环。
@@ -400,9 +480,9 @@ export default defineConfig({
400
480
  | | `visible` | `auth` |
401
481
  |--|-----------|--------|
402
482
  | 决定 | 条目/路由是否**存在** | **当前用户**是否可以进入 |
403
- | 时机 | 构建/启动(一次) | 导航(每次)+ 启动时的菜单过滤 |
483
+ | 时机 | 构建/启动(一次) | 导航(每次)+ 刷新时的菜单过滤 |
404
484
  | 路由是否注册 | 否(无法通过 URL 访问) | 是(受守卫保护;可重定向到登录) |
405
- | 对登录/登出的响应 | 否 | 守卫:是;菜单过滤:否 |
485
+ | 对登录/登出的响应 | 否 | 守卫:是;菜单过滤:导航后或 `refreshAuthNav()` 后是 |
406
486
  | 最适合 | 环境 / 功能开关 / 静态裁剪 | 登录状态、角色、登录重定向 |
407
487
 
408
488
  用 `visible` 进行静态存在性裁剪,用 `auth` 进行基于用户的访问控制。它们可以组合在同一个条目上。
@@ -497,7 +577,7 @@ app.mount('#app')
497
577
  返回 `Promise` 时会 **await** 它。如果你在配置中设置了可选的 `bootstrap`,该模块会在应用创建前加载;
498
578
  如果省略 `bootstrap`,则跳过该步骤。
499
579
 
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`。
580
+ 导出:`createSiteApp`、`defineConfig`、`useTheme`、`useSiteConfig`、`useLocale`、`useLocalize`、`tk`、`resolveLocalized`、`resolveField`、`resolveMessage`、`mergeCatalog`、`flattenMessages`、`isMessageRef`、`localizedPage`、`builtinMessages`、`themeRefKey`、`localeRefKey`。类型:`SiteConfig`、`SiteEnvConfig`、`SiteViteConfig`、`SiteCustomConfig`、`SiteExternalLink`、`NavItem`、`StandalonePage`、`PageLayout`、`AuthRule`、`AuthContext`、`AuthConfig`、`RouterConfig`、`ThemeConfig`、`ThemeOption`、`ThemePaletteVars`、`ResolvedNavItem`、`I18nConfig`、`LocaleOption`、`LocaleCode`、`LocalizedString`、`MessageRef`、`MessageTree`、`MessageCatalog`、`PageLoader`、`LocalizedPageOptions`。
501
581
 
502
582
  ### 在 Vue 页面中使用主题(`useTheme`)
503
583
 
package/bin/vue-site.mjs CHANGED
@@ -45,7 +45,7 @@ const cwdGrandparent = resolve(cwd, '../..')
45
45
 
46
46
  /**
47
47
  * @param {string[]} argv
48
- * @returns {{ command: string, cliBase?: string }}
48
+ * @returns {{ command: string, cliBase?: string, cliConfig?: string }}
49
49
  */
50
50
  function parseCliArgv(argv = process.argv) {
51
51
  const sub = argv[2]
@@ -55,6 +55,7 @@ function parseCliArgv(argv = process.argv) {
55
55
  : 'dev'
56
56
 
57
57
  let cliBase
58
+ let cliConfig
58
59
  const flagStart = command === sub && sub ? 3 : 2
59
60
  for (let i = flagStart; i < argv.length; i++) {
60
61
  const a = argv[i]
@@ -77,9 +78,28 @@ function parseCliArgv(argv = process.argv) {
77
78
  process.exit(1)
78
79
  }
79
80
  cliBase = v
81
+ } else if (a === '--config' || a === '-c') {
82
+ const v = argv[i + 1]
83
+ if (!v || v.startsWith('-')) {
84
+ console.error(
85
+ '[vue-site] --config requires a value (e.g. --config site.config.prod.ts)',
86
+ )
87
+ process.exit(1)
88
+ }
89
+ cliConfig = v
90
+ i++
91
+ } else if (a.startsWith('--config=')) {
92
+ const v = a.slice('--config='.length)
93
+ if (!v) {
94
+ console.error(
95
+ '[vue-site] --config= requires a value (e.g. --config=site.config.prod.ts)',
96
+ )
97
+ process.exit(1)
98
+ }
99
+ cliConfig = v
80
100
  }
81
101
  }
82
- return { command, cliBase }
102
+ return { command, cliBase, cliConfig }
83
103
  }
84
104
 
85
105
  function isLikelyFilesystemRoot(dir) {
@@ -105,20 +125,114 @@ const configCandidates = [
105
125
  'site.config.mts',
106
126
  'site.config.mjs',
107
127
  ]
108
- const foundConfig = configCandidates.find((f) => fs.existsSync(resolve(cwd, f)))
109
- if (!foundConfig) {
110
- console.error(
128
+
129
+ const initSiteConfig = `import { defineConfig } from '@bndynet/vue-site'
130
+
131
+ export default defineConfig({
132
+ title: 'My Site',
133
+ // Public client configuration. Never place secrets here.
134
+ custom: {
135
+ // apiBaseUrl: 'http://localhost/api/v1',
136
+ },
137
+ nav: [
138
+ { label: 'Home', icon: 'home', page: './README.md' },
139
+ ],
140
+ })
141
+ `
142
+
143
+ const initEnvTypes = `/// <reference types="vite/client" />
144
+
145
+ import '@bndynet/vue-site'
146
+
147
+ declare module '@bndynet/vue-site' {
148
+ interface SiteCustomConfig {
149
+ // apiBaseUrl: string
150
+ }
151
+ }
152
+ `
153
+
154
+ const initReadme = `# My Site
155
+
156
+ Welcome to your vue-site project.
157
+ `
158
+
159
+ class CliConfigError extends Error {}
160
+
161
+ function createInitFile(name, contents) {
162
+ const filePath = resolve(cwd, name)
163
+ try {
164
+ fs.writeFileSync(filePath, contents, { flag: 'wx' })
165
+ console.log(` created ${name}`)
166
+ return true
167
+ } catch (e) {
168
+ if (e && typeof e === 'object' && e.code === 'EEXIST') {
169
+ console.log(` skipped ${name} (already exists)`)
170
+ return false
171
+ }
172
+ throw e
173
+ }
174
+ }
175
+
176
+ function initSite() {
177
+ console.log('[vue-site] Initializing site files:')
178
+
179
+ let created = 0
180
+ const existingConfig = configCandidates.find((file) =>
181
+ fs.existsSync(resolve(cwd, file)),
182
+ )
183
+
184
+ if (existingConfig) {
185
+ console.log(` skipped site.config.ts (${existingConfig} already exists)`)
186
+ } else if (createInitFile('site.config.ts', initSiteConfig)) {
187
+ created++
188
+ }
189
+
190
+ if (createInitFile('env.d.ts', initEnvTypes)) created++
191
+ if (createInitFile('README.md', initReadme)) created++
192
+
193
+ console.log(
194
+ created > 0
195
+ ? `\n[vue-site] Created ${created} file${created === 1 ? '' : 's'}. Run \`npx vue-site dev\` to start.`
196
+ : '\n[vue-site] Nothing to create; existing files were left unchanged.',
197
+ )
198
+ }
199
+
200
+ function resolveSiteConfig(cliConfig) {
201
+ if (cliConfig) {
202
+ const configPath = resolve(cwd, cliConfig)
203
+ if (dirname(configPath) !== cwd) {
204
+ throw new CliConfigError(
205
+ `[vue-site] --config must name a file in the site root (${cwd}): ${cliConfig}`,
206
+ )
207
+ }
208
+ if (!/\.(?:ts|js|mts|mjs)$/.test(configPath)) {
209
+ throw new CliConfigError(
210
+ `[vue-site] Unsupported config extension: ${cliConfig}. ` +
211
+ 'Use a .ts, .js, .mts, or .mjs file.',
212
+ )
213
+ }
214
+ if (!fs.existsSync(configPath) || !fs.statSync(configPath).isFile()) {
215
+ throw new CliConfigError(`[vue-site] Config file not found: ${configPath}`)
216
+ }
217
+ return basename(configPath)
218
+ }
219
+
220
+ const foundConfig = configCandidates.find((f) =>
221
+ fs.existsSync(resolve(cwd, f)),
222
+ )
223
+ if (foundConfig) return foundConfig
224
+
225
+ throw new CliConfigError(
111
226
  '\x1b[31mError: No site.config.ts found in the current directory.\x1b[0m\n\n' +
112
- 'Create a site.config.ts file:\n\n' +
113
- ' import type { SiteConfig } from \'@bndynet/vue-site\'\n\n' +
114
- ' export default {\n' +
115
- ' title: \'My Site\',\n' +
116
- ' nav: [\n' +
117
- ' { label: \'Home\', icon: \'home\', page: () => import(\'./README.md?raw\') },\n' +
118
- ' ],\n' +
119
- ' } satisfies SiteConfig\n'
227
+ 'Run `npx vue-site init` to create starter files, or create site.config.ts manually:\n\n' +
228
+ ' import type { SiteConfig } from \'@bndynet/vue-site\'\n\n' +
229
+ ' export default {\n' +
230
+ ' title: \'My Site\',\n' +
231
+ ' nav: [\n' +
232
+ ' { label: \'Home\', icon: \'home\', page: () => import(\'./README.md?raw\') },\n' +
233
+ ' ],\n' +
234
+ ' } satisfies SiteConfig\n',
120
235
  )
121
- process.exit(1)
122
236
  }
123
237
 
124
238
  const VIRTUAL_ENTRY = 'virtual:vue-site-entry'
@@ -317,10 +431,10 @@ function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
317
431
  ].join('\n')
318
432
  }
319
433
 
320
- function buildEntryCode(siteConfig) {
434
+ function buildEntryCode(siteConfig, configFile) {
321
435
  return buildBootstrapScript({
322
436
  siteConfig,
323
- siteConfigSpecifier: `/${foundConfig}`,
437
+ siteConfigSpecifier: `/${configFile}`,
324
438
  })
325
439
  }
326
440
 
@@ -358,8 +472,8 @@ function preloadAssetStubPlugin() {
358
472
  }
359
473
  }
360
474
 
361
- async function loadSiteConfig() {
362
- const configPath = resolve(cwd, foundConfig)
475
+ async function loadSiteConfig(configFile) {
476
+ const configPath = resolve(cwd, configFile)
363
477
  const raw = fs.readFileSync(configPath, 'utf-8')
364
478
 
365
479
  // Stub the framework's value imports so the config evaluates without the real (browser-only)
@@ -398,7 +512,7 @@ async function loadSiteConfig() {
398
512
  stubbed.replace(/import\.meta\.glob/g, '__vueSiteGlobStub')
399
513
  }
400
514
 
401
- const isTs = /\.m?ts$/.test(foundConfig)
515
+ const isTs = /\.m?ts$/.test(configFile)
402
516
  // Write the stubbed entry next to the original so its relative imports (`./locales`) resolve.
403
517
  const entryFile = resolve(
404
518
  dirname(configPath),
@@ -428,7 +542,7 @@ async function loadSiteConfig() {
428
542
  return mod.default || {}
429
543
  } catch (e) {
430
544
  throw new Error(
431
- `[vue-site] Could not pre-load site config from ${foundConfig}: ${e.message}\n` +
545
+ `[vue-site] Could not pre-load site config from ${configFile}: ${e.message}\n` +
432
546
  ` This usually means your config imports modules Node can't resolve directly ` +
433
547
  `(path aliases like @/..., or framework APIs other than defineConfig).`,
434
548
  )
@@ -704,8 +818,8 @@ function vueSitePlugin(entryCode, htmlTemplate, iconRegistryCode) {
704
818
  }
705
819
 
706
820
  async function buildViteConfig(options = {}) {
707
- const { cliBase, siteConfig: siteConfigOption } = options
708
- const siteConfig = siteConfigOption ?? (await loadSiteConfig())
821
+ const { cliBase, configFile, siteConfig: siteConfigOption } = options
822
+ const siteConfig = siteConfigOption ?? (await loadSiteConfig(configFile))
709
823
  const env = siteConfig.env || {}
710
824
  const {
711
825
  port,
@@ -816,7 +930,7 @@ async function buildViteConfig(options = {}) {
816
930
  }
817
931
  }
818
932
 
819
- const entryCode = buildEntryCode(siteConfig)
933
+ const entryCode = buildEntryCode(siteConfig, configFile)
820
934
  const iconRegistryCode = buildIconRegistryCode(siteConfig)
821
935
  const htmlTemplate = buildHtmlShell(
822
936
  `<script type="module" src="/@id/__x00__${VIRTUAL_ENTRY}"></script>`,
@@ -953,9 +1067,15 @@ async function buildViteConfig(options = {}) {
953
1067
  }
954
1068
 
955
1069
  async function run() {
956
- const { command, cliBase } = parseCliArgv()
957
- const siteConfig = await loadSiteConfig()
958
- const viteConfig = await buildViteConfig({ cliBase, siteConfig })
1070
+ const { command, cliBase, cliConfig } = parseCliArgv()
1071
+ if (command === 'init') {
1072
+ initSite()
1073
+ return
1074
+ }
1075
+
1076
+ const configFile = resolveSiteConfig(cliConfig)
1077
+ const siteConfig = await loadSiteConfig(configFile)
1078
+ const viteConfig = await buildViteConfig({ cliBase, configFile, siteConfig })
959
1079
 
960
1080
  if (command === 'dev') {
961
1081
  const server = await createServer(viteConfig)
@@ -969,7 +1089,7 @@ async function run() {
969
1089
  if (!hadHtml) {
970
1090
  const bootstrapScript = buildBootstrapScript({
971
1091
  siteConfig,
972
- siteConfigSpecifier: `./${foundConfig}`,
1092
+ siteConfigSpecifier: `./${configFile}`,
973
1093
  })
974
1094
  const buildHtml = buildHtmlShell(
975
1095
  `<script type="module">\n${bootstrapScript}\n </script>`,
@@ -992,14 +1112,16 @@ async function run() {
992
1112
  server.printUrls()
993
1113
  } else {
994
1114
  console.log(
995
- 'Usage: vue-site|vs <dev|build|preview> [--base=<path>]\n' +
996
- ' --base Public path for assets (overrides env.vite.base); e.g. --base=/app/',
1115
+ 'Usage: vue-site|vs <init|dev|build|preview> [--base=<path>] [--config=<file>]\n' +
1116
+ ' init Create site.config.ts, env.d.ts, and README.md without overwriting files\n' +
1117
+ ' --base Public path for assets (overrides env.vite.base); e.g. --base=/app/\n' +
1118
+ ' --config, -c Site config file in the current site root; e.g. site.config.prod.ts',
997
1119
  )
998
1120
  process.exit(1)
999
1121
  }
1000
1122
  }
1001
1123
 
1002
1124
  run().catch((err) => {
1003
- console.error(err)
1125
+ console.error(err instanceof CliConfigError ? err.message : err)
1004
1126
  process.exit(1)
1005
1127
  })
package/dist/auth.d.ts CHANGED
@@ -3,11 +3,11 @@ import { AuthConfig, ResolvedNavItem } from './types';
3
3
  /** Query param carrying the originally requested path when redirecting to the login page. */
4
4
  export declare const AUTH_REDIRECT_QUERY = "redirect";
5
5
  /**
6
- * Startup-once pass that drops nav items the current user is not authorized to see, so they do not
7
- * appear in the rendered menu. Mirrors the pruning of `filterNavItems`: a hidden parent removes its
8
- * subtree, and a group left with no children and no own `page` / `link` is pruned. Routes are not
9
- * affected (they are still registered by `createSiteRouter`); only the menu list is filtered. An
10
- * item is shown only when `authorize` returns exactly `true`.
6
+ * Drop nav items the current user is not authorized to see, so they do not appear in the rendered
7
+ * menu. Mirrors the pruning of `filterNavItems`: a hidden parent removes its subtree, and a group
8
+ * left with no children and no own `page` / `link` is pruned. Routes are not affected (they are
9
+ * still registered by `createSiteRouter`); only the menu list is filtered. An item is shown only
10
+ * when `authorize` returns exactly `true`.
11
11
  */
12
12
  export declare function pruneNavByAuth(items: ResolvedNavItem[], auth: AuthConfig): Promise<ResolvedNavItem[]>;
13
13
  /**
@@ -1,8 +1,12 @@
1
1
  import { InjectionKey } from 'vue';
2
2
  import { SiteConfig, ResolvedNavItem } from '../types';
3
3
  export interface SiteContext {
4
+ /** Original site configuration passed to `createSiteApp()`. */
4
5
  config: SiteConfig;
6
+ /** Auth-filtered nav tree rendered by the layout. Kept as a reactive array. */
5
7
  resolvedNav: ResolvedNavItem[];
8
+ /** Re-run auth menu filtering after consumer auth state changes, such as login/logout. */
9
+ refreshAuthNav: () => Promise<void>;
6
10
  }
7
11
  /** Use `Symbol.for` so pages still inject context if Vite resolves the package twice. */
8
12
  export declare const siteContextKey: InjectionKey<SiteContext>;
@@ -1,2 +1,11 @@
1
1
  import { SiteConfig } from './types';
2
+ /**
3
+ * Returns the site config passed to `createSiteApp()`. Can be called from anywhere — plain
4
+ * modules, `bootstrap.ts` (after the app is created), utility functions, etc. — without a Vue
5
+ * injection context. Returns `undefined` if `createSiteApp` has not been called yet.
6
+ *
7
+ * For reactive, component-scoped access to the full `SiteContext` (config + nav + refreshAuthNav),
8
+ * use {@link useSiteConfig} inside a Vue component instead.
9
+ */
10
+ export declare function getSiteConfig(): SiteConfig | undefined;
2
11
  export declare function createSiteApp(config: SiteConfig): Promise<import('vue').App<Element>>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SiteConfig } from './types';
2
- export { createSiteApp } from './create-app';
2
+ export { createSiteApp, getSiteConfig } from './create-app';
3
3
  export { useTheme, themeRefKey } from './composables/useTheme';
4
4
  export { useLocale, localeRefKey } from './composables/useLocale';
5
5
  export { useLocalize } from './composables/useLocalize';
@@ -9,5 +9,23 @@ 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, IconRegistry, MessageRef, MessageTree, LocaleOption, I18nConfig, PageLoader, } from './types';
12
+ export type { SiteConfig, SiteEnvConfig, SiteViteConfig, SiteCustomConfig, 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
+ /**
14
+ * Type-safe identity for a single config object. Use the single-argument form for a standalone
15
+ * config file.
16
+ *
17
+ * When composing multiple config files (e.g. `site.config.prod.ts` importing a shared
18
+ * `site.config.common.ts`), use the two-argument form so `custom` is deep-merged: properties
19
+ * from the base `custom` are preserved unless explicitly overridden.
20
+ *
21
+ * @example
22
+ * // site.config.common.ts
23
+ * export default defineConfig({ custom: { apiBaseUrl: '/api', appName: 'MyApp' }, ... })
24
+ *
25
+ * // site.config.prod.ts
26
+ * import common from './site.config.common'
27
+ * export default defineConfig(common, { custom: { apiBaseUrl: 'https://prod.example.com/api' } })
28
+ * // Result: custom.apiBaseUrl is overridden, custom.appName is preserved.
29
+ */
13
30
  export declare function defineConfig(config: SiteConfig): SiteConfig;
31
+ export declare function defineConfig(base: SiteConfig, overrides: Partial<SiteConfig>): SiteConfig;