vite-plugin-siyuan 0.1.0 → 0.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
@@ -18,7 +18,7 @@ import { defineConfig } from 'vite';
18
18
  import siyuan from 'vite-plugin-siyuan';
19
19
 
20
20
  export default defineConfig({
21
- plugins: [siyuan()],
21
+ plugins: [siyuan({ entry: 'src/main.css' })],
22
22
  });
23
23
  ```
24
24
 
@@ -28,13 +28,52 @@ pnpm vite build --watch
28
28
 
29
29
  `vite dev` 不适用:思源从磁盘读产物,而 dev server 只在内存里编译。插件的 `apply` 固定为 `'build'`。
30
30
 
31
+ ## 项目结构
32
+
33
+ **除了 `README.md`,其余文件都放 `public/`。**
34
+
35
+ ```
36
+ 你的项目/
37
+ ├── README.md ← 唯一放项目根的(GitHub 只展示仓库根的 README)
38
+ ├── public/
39
+ │ ├── theme.json ← 主题;插件则是 plugin.json
40
+ │ ├── icon.png
41
+ │ ├── preview.png
42
+ │ └── i18n/
43
+ ├── src/
44
+ │ └── main.css ← entry 指向这里
45
+ └── vite.config.ts
46
+ ```
47
+
48
+ 思源按固定路径从**产物根**加载清单和资源,而 `public/` 是 Vite 唯一「原样复制到产物根」的机制。放这里,dev 和 build 都自动生效,改清单也会触发重建。
49
+
50
+ `README.md` 是唯一例外:GitHub 只认仓库根的 README,思源集市要求它在产物根。插件会把它从项目根复制进产物(清单的 `readme` 字段可指定文件名,缺省按 `README*.md` 兜底);如果你放 `public/README.md`,则以 `public/` 为准。
51
+
52
+ 清单里声明的 `icon` / `preview` 若没出现在产物里,构建会**报错**——这类缺失思源不会报错,只会导致集市缺图。
53
+
54
+ ### 关于 publicDir
55
+
56
+ 默认就是 `public/`。如果改成别的目录(`publicDir: 'static'`),插件会跟着找,无需额外配置。
57
+
58
+ **不要设成 `false`**:思源要求清单出现在产物根,而没有 `publicDir` 就没有任何机制能把清单复制进去。插件会在 `config` 阶段直接报错。
59
+
60
+ ### 不支持根目录布局
61
+
62
+ 官方 [theme-sample](https://github.com/siyuan-note/theme-sample) 和 [plugin-sample](https://github.com/siyuan-note/plugin-sample) 把清单放项目根,但它们是**无构建步骤或 webpack 项目**,不是 Vite 项目。放到项目根会直接报错:
63
+
64
+ ```
65
+ 在 {publicDir} 下找不到 theme.json / plugin.json;请把清单放进该目录,或用 kind 选项显式指定类型
66
+ ```
67
+
31
68
  ## 选项
32
69
 
33
70
  | 选项 | 说明 |
34
71
  |---|---|
35
- | `kind` | `'theme'` / `'plugin'`;缺省时在 `public/`、`dist/`、项目根依次查找 `theme.json` / `plugin.json` 推断 |
72
+ | `entry` | **必填**。构建入口,相对项目根。主题指向 CSS,插件指向 TS/JS |
73
+ | `kind` | `'theme'` / `'plugin'`;缺省时按 `publicDir` 下的清单推断 |
36
74
  | `outDir` | 产物目录;缺省 `./dev`(watch)/ `./dist`(生产) |
37
- | `entry` | 构建入口,相对项目根;缺省保留项目自己的 `input` / `lib` |
75
+
76
+ `entry` 必填是因为产物名是固定契约(主题 `theme.css`、插件 `index.js`)。入口猜错不会报错,只会静默产出思源加载不到的东西。
38
77
 
39
78
  ## 做什么
40
79
 
@@ -42,8 +81,8 @@ pnpm vite build --watch
42
81
 
43
82
  ### watch(`vite build --watch`)
44
83
 
45
- 1. **注入构建配置**:主题产出 `theme.css`;插件在给了 `entry` 时走 `lib` CJS 单文件产出 `index.js`。`base: './'` 保证宿主下资源不 404。用 `mergeConfig` 合并,**用户已写的值优先**。
46
- 2. **链接产物**:把 `dev/` 链接到 `<workspace>/data/themes|plugins/<name>`。插件目录必须是真 symlink(思源 Go 运行时按 `ModeSymlink` 判定),主题可用 junction 兜底。
84
+ 1. **注入构建配置**:主题产出 `theme.css`;插件走 `lib` CJS 单文件产出 `index.js`。`base: './'` 保证宿主下资源不 404。用 `mergeConfig` 合并,**用户已写的值优先**。
85
+ 2. **链接产物**:把 `dev/` 链接到 `{workspace}/data/themes|plugins/{name}`。插件目录必须是真 symlink(思源 Go 运行时按 `ModeSymlink` 判定),主题可用 junction 兜底。
47
86
  3. **激活**:主题调 `setAppearance` 选中它;插件调 `setPetalEnabled` 启用。
48
87
  4. **热重载**:产物变化后合并 250ms 广播 `reloadUI`,思源整页刷新。
49
88
 
@@ -74,13 +113,13 @@ watch 模式首次就绪后还会打印桌面端 / 移动端两个稳定预览
74
113
  | 变量 | 说明 |
75
114
  |---|---|
76
115
  | `SIYUAN_PORT` | kernel 端口;缺省用 kernel 进程 PID 匹配 `port.json`,再探测记录端口与 6806–6809 |
77
- | `SIYUAN_THEME_DIR` | 主题安装父目录(`<workspace>/data/themes`),实际装到 `<它>/<name>` |
78
- | `SIYUAN_PLUGIN_DIR` | 插件安装父目录(`<workspace>/data/plugins`),同上 |
116
+ | `SIYUAN_THEME_DIR` | 主题安装父目录(`{workspace}/data/themes`),实际装到 `{该目录}/{name}` |
117
+ | `SIYUAN_PLUGIN_DIR` | 插件安装父目录(`{workspace}/data/plugins`),同上 |
79
118
  | `SIYUAN_TOKEN` | 开了 API token 时使用 |
80
119
  | `SIYUAN_NO_ELEVATION=1` | 不弹 UAC 提权(CI 环境自动不弹) |
81
120
  | `SIYUAN_DEBUG=1` | 输出 debug 级日志 |
82
121
 
83
- > 旧名 `SYD_NO_ELEVATION` / `SYD_DEBUG` 仍然兼容。
122
+ 旧名 `SYD_NO_ELEVATION` / `SYD_DEBUG` 仍然兼容。
84
123
 
85
124
  ## 配套 CLI
86
125
 
@@ -91,10 +130,9 @@ watch 模式首次就绪后还会打印桌面端 / 移动端两个稳定预览
91
130
  ### 提 PR
92
131
 
93
132
  1. 先开 issue 对齐需求,避免白做。
94
- 2. PR 标题必须符合 [Conventional Commits](https://www.conventionalcommits.org/)——CI 会检查:
95
- - `feat: ...` → 次版本(0.2.0)
96
- - `fix: ...` → 补丁版本(0.1.1)
97
- - `feat!: ...` → 1.0 之前只升次版本
133
+ 2. PR 标题必须符合 [Conventional Commits](https://www.conventionalcommits.org/),CI 会检查:
134
+ - `feat:` → 次版本,`fix:` → 补丁版本
135
+ - `feat!:` → 1.0 之前只升次版本
98
136
  - `docs:` / `chore:` / `ci:` / `test:` / `refactor:` → 不发版
99
137
  3. 合并方式固定为 **Squash and merge**,PR 标题会成为最终 commit。
100
138
 
@@ -107,7 +145,8 @@ watch 模式首次就绪后还会打印桌面端 / 移动端两个稳定预览
107
145
  └─ release-please 开/更新 Release PR「chore(main): release x.y.z」
108
146
  └─ 合并该 PR
109
147
  ├─ 自动打 tag、建 GitHub Release、生成 CHANGELOG.md
110
- └─ 自动发布到 npm(OIDC trusted publishing)
148
+ └─ 自动 stage 到 npm(OIDC trusted publishing)
149
+ └─ 你在 npmjs.com 用安全密钥批准后上线
111
150
  ```
112
151
 
113
152
  `package.json` 的 `version` 由 Release PR 更新,不要手动改。
@@ -1,13 +1,18 @@
1
1
  import type { Logger } from '../support/log.ts';
2
- /** 把 `public/**` 逐个交给构建器登记监听。 */
2
+ /** 把 publicDir 下的文件逐个交给构建器登记监听。 */
3
3
  export declare function watchPublicFiles(publicDir: string, registerWatchFile: (file: string) => void, log: Logger): void;
4
4
  /**
5
- * 把清单声明的元数据文件搬进产物目录;已由构建器搬过的(public/ 下)跳过。
5
+ * 把 README 从项目根搬进产物目录(GitHub 只认根目录,思源要求产物根)。
6
+ *
7
+ * 清单声明的 README 优先;没声明时按 `README*.md` 兜底。
8
+ *
9
+ * **publicDir 优先**:若同名文件已经在 publicDir 下,Vite 已经把它复制进产物,
10
+ * 这里直接跳过——否则项目根那份会静默覆盖用户放在 publicDir 的版本。
6
11
  *
7
12
  * watch 下复制失败只报错继续(一次文件锁不该停掉开发服务器);
8
- * 生产构建必须失败——否则 package.zip 会缺清单声明的 readme/icon/preview。
13
+ * 生产构建必须失败——否则 package.zip 会缺 README。
9
14
  */
10
- export declare function copyMetadata({ root, publicDir, outDir, manifestName, watch, log, }: {
15
+ export declare function copyReadmes({ root, publicDir, outDir, manifestName, watch, log, }: {
11
16
  root: string;
12
17
  publicDir: string;
13
18
  outDir: string;
@@ -1,16 +1,27 @@
1
1
  /**
2
- * 思源产物契约:让 `public/` 的改动也触发重建,并搬运清单声明的元数据文件。
2
+ * 思源产物契约:让 `public/` 的改动也触发重建,并搬运唯一必须放项目根的文件。
3
+ *
4
+ * ## 文件放哪
5
+ *
6
+ * **只有 README 必须放项目根**:GitHub 只展示仓库根的 README,而思源集市要求
7
+ * 它出现在产物根。这一份由本模块搬进产物。
8
+ *
9
+ * 其他一切(`theme.json` / `plugin.json` / `icon` / `preview` / `i18n/` / 图片字体
10
+ * 等静态资源)都放 `public/`,由 Vite 的 `publicDir` 原样复制到产物根,本模块不介入。
11
+ *
12
+ * ## 为什么要监听 public/
3
13
  *
4
14
  * 主流构建器的 `publicDir` 都只在**启动时**复制进产物,之后不再监听它,
5
- * 而思源的清单恰恰在 `public/` 里——改个 `theme.json` 的 frontends 或版本号,
15
+ * 而思源的清单恰恰在 `public/` 里——改个 `theme.json` 的 modes 或版本号,
6
16
  * 不重启就永远不生效。所以要把 `public/**` 显式加进监听集合。
7
17
  */
8
18
  import { copyFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
9
19
  import { dirname, join, resolve } from 'node:path';
10
20
  import { readJsonFile, walkFiles } from '../support/fs.js';
11
21
  import { isRecord } from '../support/guards.js';
22
+ import { declaredAssets } from '../siyuan/manifest.js';
12
23
  const README_PATTERN = /^README.*\.md$/;
13
- /** 把 `public/**` 逐个交给构建器登记监听。 */
24
+ /** 把 publicDir 下的文件逐个交给构建器登记监听。 */
14
25
  export function watchPublicFiles(publicDir, registerWatchFile, log) {
15
26
  if (!publicDir || !existsSync(publicDir)) {
16
27
  log(`publicDir 不存在,跳过监听:${publicDir}`, 'warn');
@@ -19,58 +30,43 @@ export function watchPublicFiles(publicDir, registerWatchFile, log) {
19
30
  for (const file of walkFiles(publicDir))
20
31
  registerWatchFile(file.path);
21
32
  }
22
- /** 从清单的 readme / icon / preview 收集要搬的文件名。 */
23
- function declaredFiles(manifestPath, log) {
24
- const parsed = readJsonFile(manifestPath);
25
- if (!isRecord(parsed)) {
26
- log(`读不到或无法解析清单 ${manifestPath};元数据按 README*.md 兜底`, 'warn');
27
- return null;
28
- }
29
- const files = new Set();
30
- const readme = parsed['readme'];
31
- if (typeof readme === 'string')
32
- files.add(readme);
33
- else if (isRecord(readme)) {
34
- for (const value of Object.values(readme))
35
- if (typeof value === 'string')
36
- files.add(value);
37
- }
38
- for (const key of ['icon', 'preview']) {
39
- const value = parsed[key];
40
- if (typeof value === 'string')
41
- files.add(value);
42
- }
43
- return files.size ? [...files] : null;
44
- }
45
33
  /**
46
- * 把清单声明的元数据文件搬进产物目录;已由构建器搬过的(public/ 下)跳过。
34
+ * 把 README 从项目根搬进产物目录(GitHub 只认根目录,思源要求产物根)。
35
+ *
36
+ * 清单声明的 README 优先;没声明时按 `README*.md` 兜底。
37
+ *
38
+ * **publicDir 优先**:若同名文件已经在 publicDir 下,Vite 已经把它复制进产物,
39
+ * 这里直接跳过——否则项目根那份会静默覆盖用户放在 publicDir 的版本。
47
40
  *
48
41
  * watch 下复制失败只报错继续(一次文件锁不该停掉开发服务器);
49
- * 生产构建必须失败——否则 package.zip 会缺清单声明的 readme/icon/preview。
42
+ * 生产构建必须失败——否则 package.zip 会缺 README。
50
43
  */
51
- export function copyMetadata({ root, publicDir, outDir, manifestName, watch, log, }) {
44
+ export function copyReadmes({ root, publicDir, outDir, manifestName, watch, log, }) {
52
45
  const absoluteOutDir = resolve(root, outDir);
53
46
  mkdirSync(absoluteOutDir, { recursive: true });
54
- const candidates = [join(root, 'public', manifestName), join(root, manifestName)];
55
- const manifestPath = candidates.find((file) => existsSync(file)) ?? candidates[0];
56
- const declared = declaredFiles(manifestPath, log);
47
+ const manifestPath = publicDir ? join(publicDir, manifestName) : join(root, 'public', manifestName);
48
+ const parsed = readJsonFile(manifestPath);
49
+ const declared = isRecord(parsed) ? declaredAssets(parsed, ['readme']) : null;
50
+ if (!isRecord(parsed))
51
+ log(`读不到或无法解析清单 ${manifestPath};README 按 README*.md 兜底`, 'warn');
57
52
  const names = [];
58
- if (declared) {
53
+ if (declared?.length) {
59
54
  names.push(...declared);
60
55
  }
61
56
  else {
62
- log(`清单 ${manifestPath} 没声明 readme/icon/preview,按 README*.md 兜底搬运`, 'warn');
57
+ log(`清单 ${manifestPath} 没声明 readme,按 README*.md 兜底搬运`, 'warn');
63
58
  names.push(...readdirSync(root, { withFileTypes: true })
64
59
  .filter((entry) => entry.isFile() && README_PATTERN.test(entry.name))
65
60
  .map((entry) => entry.name));
66
61
  }
67
62
  const failures = [];
68
63
  for (const name of names) {
64
+ // publicDir 优先:Vite 已复制过,不再覆盖。
65
+ if (publicDir && existsSync(join(publicDir, name)))
66
+ continue;
69
67
  const source = resolve(root, name);
70
68
  if (!existsSync(source)) {
71
- if (publicDir && existsSync(join(publicDir, name)))
72
- continue;
73
- failures.push(`${name}:项目根与 public/ 下都不存在`);
69
+ failures.push(`${name}:项目根与 public 目录下都不存在`);
74
70
  continue;
75
71
  }
76
72
  try {
@@ -86,7 +82,7 @@ export function copyMetadata({ root, publicDir, outDir, manifestName, watch, log
86
82
  return;
87
83
  const detail = failures.join(';');
88
84
  if (watch)
89
- log(`资产复制失败 ${detail}`, 'error');
85
+ log(`README 复制失败 ${detail}`, 'error');
90
86
  else
91
- throw new Error(`资产复制失败:${detail}`);
87
+ throw new Error(`README 复制失败:${detail}`);
92
88
  }
@@ -8,8 +8,9 @@ export interface VerifyResult {
8
8
  issues: VerifyIssue[];
9
9
  }
10
10
  /** 校验一个目标的清单与产物;返回问题列表,空表示通过。 */
11
- export declare function verifyTarget(kind: Kind, { root, outDir }: {
11
+ export declare function verifyTarget(kind: Kind, { root, publicDir, outDir }: {
12
12
  root: string;
13
+ publicDir: string;
13
14
  outDir: string;
14
15
  }): VerifyResult;
15
16
  /** 把问题列表排成可读文本。 */
@@ -8,7 +8,7 @@ import { existsSync } from 'node:fs';
8
8
  import { isAbsolute, join } from 'node:path';
9
9
  import { readJsonFile, readTextFile, walkFiles } from '../support/fs.js';
10
10
  import { isRecord } from '../support/guards.js';
11
- import { readManifest } from '../siyuan/manifest.js';
11
+ import { declaredAssets, readManifest } from '../siyuan/manifest.js';
12
12
  import { kindSpec } from '../siyuan/kinds.js';
13
13
  import { manifestIssueMessage, pluginManifestShape, themeManifestShape } from './manifest-schema.js';
14
14
  /** 去掉 CSS 注释:watch 产物不压缩,注释里常写着「别写 rgb(var(--b3-*))」。 */
@@ -26,13 +26,12 @@ function findDoubleWrappedColors(css) {
26
26
  return problems;
27
27
  }
28
28
  /** 校验一个目标的清单与产物;返回问题列表,空表示通过。 */
29
- export function verifyTarget(kind, { root, outDir }) {
29
+ export function verifyTarget(kind, { root, publicDir, outDir }) {
30
30
  const spec = kindSpec(kind);
31
31
  const issues = [];
32
- const declaredOutDir = isAbsolute(outDir) ? outDir : join(root, outDir);
33
32
  // 只校验本次构建的产物目录。不做「产物目录不存在就改扫项目根」的兜底:
34
33
  // 插件永远在真实构建之后校验,那种兜底只会把 node_modules 当成产物扫进来。
35
- const absOutDir = declaredOutDir;
34
+ const absOutDir = isAbsolute(outDir) ? outDir : join(root, outDir);
36
35
  const walked = walkFiles(absOutDir);
37
36
  const files = walked.map((file) => file.name);
38
37
  const byName = new Map(walked.map((file) => [file.name, file.path]));
@@ -40,13 +39,13 @@ export function verifyTarget(kind, { root, outDir }) {
40
39
  const path = byName.get(file);
41
40
  return path ? readTextFile(path) : null;
42
41
  };
43
- const manifest = readManifest(root, spec.manifestName, [absOutDir])?.data ?? null;
42
+ const manifest = readManifest(publicDir, spec.manifestName, [absOutDir])?.data ?? null;
44
43
  const packageJsonRaw = readJsonFile(join(root, 'package.json'));
45
44
  const packageJson = isRecord(packageJsonRaw) ? packageJsonRaw : null;
46
45
  // ---- 通用:清单存在与形状 ----
47
46
  if (!manifest) {
48
47
  issues.push({
49
- message: `读不到清单:${spec.manifestName}(找过 public/、dist/、项目根)`,
48
+ message: `读不到清单:${spec.manifestName}(应在 public 目录下,或已在产物目录里)`,
50
49
  hint: '思源按清单识别主题/插件,缺了它不会出现在列表里。',
51
50
  });
52
51
  }
@@ -78,6 +77,22 @@ export function verifyTarget(kind, { root, outDir }) {
78
77
  }
79
78
  }
80
79
  }
80
+ // ---- 清单声明的资源必须真的在产物里 ----
81
+ // icon / preview / readme 由清单引用,思源集市按这些名字去找。它们只能通过
82
+ // public/ 进产物(README 额外支持项目根),放错位置不会报错、只是集市缺图。
83
+ if (manifest && existsSync(absOutDir)) {
84
+ for (const name of declaredAssets(manifest)) {
85
+ if (files.includes(name))
86
+ continue;
87
+ const inPublic = publicDir !== '' && existsSync(join(publicDir, name));
88
+ issues.push({
89
+ message: `清单声明的 ${name} 不在产物里`,
90
+ hint: inPublic
91
+ ? `它在 public 目录下,但没被复制进 ${absOutDir};确认 publicDir 配置。`
92
+ : `把 ${name} 放进 public 目录,它会随构建复制到产物根。`,
93
+ });
94
+ }
95
+ }
81
96
  // ---- 类型特有的产物检查(清单形状已由上面的 schema 覆盖) ----
82
97
  if (existsSync(absOutDir) && kind === 'theme') {
83
98
  const scripts = files.filter((file) => file.endsWith('.js'));
package/dist/config.d.ts CHANGED
@@ -4,8 +4,8 @@ import type { Kind } from './siyuan/kinds.ts';
4
4
  export interface BuildSpecInput {
5
5
  kind: Kind;
6
6
  watch: boolean;
7
- /** 入口文件绝对路径;不给则保留项目自己的 input/lib。 */
8
- entry: string | null;
7
+ /** 入口文件绝对路径。必填:产物名是固定契约,入口不能靠猜。 */
8
+ entry: string;
9
9
  /** 插件选项里显式给的产物目录。 */
10
10
  outDir: string | null;
11
11
  }
package/dist/config.js CHANGED
@@ -35,9 +35,7 @@ function optionsKey(userConfig) {
35
35
  export function buildSpecFor(input) {
36
36
  const spec = kindSpec(input.kind);
37
37
  const outDir = input.outDir ?? (input.watch ? './dev' : './dist');
38
- const options = {};
39
- if (input.entry)
40
- options.input = input.entry;
38
+ const options = { input: input.entry };
41
39
  if (input.kind === 'plugin') {
42
40
  options.external = ['siyuan', 'process'];
43
41
  options.output = {
@@ -55,7 +53,7 @@ export function buildSpecFor(input) {
55
53
  // watch 不压缩(便于直接看落盘产物),生产用默认压缩器。
56
54
  minify: !input.watch,
57
55
  };
58
- if (input.entry && input.kind === 'plugin') {
56
+ if (input.kind === 'plugin') {
59
57
  // 插件入口是 `export default class ...`,必须走 lib:lib cjs 产出
60
58
  // `module.exports = 类`;普通 input 只得到 `exports.default`,思源报
61
59
  // "plugin xxx has no export"。
package/dist/index.d.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import type { Plugin } from 'vite';
2
2
  import type { Kind } from './siyuan/kinds.ts';
3
3
  export interface SiYuanOptions {
4
+ /**
5
+ * 构建入口,相对项目根。必填:主题要产出固定名 `theme.css`、插件要产出
6
+ * CJS 单文件 `index.js`,产物名是思源加载机制决定的契约,入口不能靠猜——
7
+ * 猜错不会报错,只会静默产出思源加载不到的东西。
8
+ */
9
+ entry: string;
4
10
  /** 被测类型(theme / plugin);缺省从清单推断。 */
5
11
  kind?: Kind | null;
6
12
  /** 产物目录;缺省 `./dev`(watch)/ `./dist`(生产)。 */
7
13
  outDir?: string | null;
8
- /** 构建入口,相对项目根;缺省保留项目自己写的 input/lib。 */
9
- entry?: string | null;
10
14
  }
11
- export default function siyuan(options?: SiYuanOptions): Plugin;
15
+ export default function siyuan(options: SiYuanOptions): Plugin;
package/dist/index.js CHANGED
@@ -16,14 +16,16 @@
16
16
  * closeBundle 校验 + 打包 + 汇报 + 构建完成日志
17
17
  * closeWatcher 释放定时器
18
18
  */
19
+ import { existsSync } from 'node:fs';
19
20
  import { resolve } from 'node:path';
20
21
  import { applyBuildConfig } from './config.js';
21
22
  import { detectKind } from './siyuan/kinds.js';
23
+ import { resolvePublicDir } from './siyuan/manifest.js';
22
24
  import { createTaggedLogger } from './output/log.js';
23
25
  import { createBuildSession } from './session.js';
24
26
  import { describeError, UserError } from './support/errors.js';
25
27
  const NAME = 'vite-plugin-siyuan';
26
- export default function siyuan(options = {}) {
28
+ export default function siyuan(options) {
27
29
  const log = createTaggedLogger(NAME);
28
30
  let session = null;
29
31
  let kind = options.kind ?? null;
@@ -32,16 +34,17 @@ export default function siyuan(options = {}) {
32
34
  apply: 'build',
33
35
  config(userConfig) {
34
36
  const root = typeof userConfig.root === 'string' ? resolve(userConfig.root) : process.cwd();
35
- try {
36
- kind ??= detectKind(root);
37
- }
38
- catch (cause) {
39
- // 没有清单就不知道产物该叫什么;交给项目自己配,不在这里报错。
40
- log(`跳过构建配置注入:${describeError(cause)}`, 'warn');
41
- return undefined;
37
+ const entry = resolve(root, options.entry);
38
+ if (!existsSync(entry)) {
39
+ throw new UserError(`entry 指向的文件不存在:${entry}(选项值:${options.entry})`);
42
40
  }
41
+ // entry 必填意味着用户确实在建思源产物,所以识别不到类型是真错误,
42
+ // 不能像以前那样 warn 后跳过——那会让构建继续跑,最后崩在 Vite 的
43
+ // "Cannot resolve entry module index.html" 上,完全指错方向。
44
+ // publicDir 在这里还没解析(configResolved 才给),先按用户配置算一遍。
45
+ const publicDir = resolvePublicDir(root, userConfig.publicDir);
46
+ kind ??= detectKind(publicDir);
43
47
  const watch = Boolean(userConfig.build?.watch);
44
- const entry = options.entry ? resolve(root, options.entry) : null;
45
48
  return applyBuildConfig(userConfig, { kind, watch, entry, outDir: options.outDir ?? null });
46
49
  },
47
50
  configResolved(resolvedConfig) {
@@ -49,7 +52,6 @@ export default function siyuan(options = {}) {
49
52
  const outDir = resolvedConfig.build.outDir;
50
53
  const publicDir = resolvedConfig.publicDir;
51
54
  const watch = resolvedConfig.command === 'build' && Boolean(resolvedConfig.build.watch);
52
- kind ??= options.kind ?? null;
53
55
  if (!kind) {
54
56
  log('识别不到被测类型,跳过链接与校验', 'warn');
55
57
  return;
package/dist/session.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { relative, resolve } from 'node:path';
7
7
  import { activate, reloadUI } from './siyuan/activate.js';
8
- import { copyMetadata, watchPublicFiles } from './artifact/assets.js';
8
+ import { copyReadmes, watchPublicFiles } from './artifact/assets.js';
9
9
  import { checkLinkedFiles, ensureLink } from './siyuan/link.js';
10
10
  import { kindSpec, readName } from './siyuan/kinds.js';
11
11
  import { findKernelPort } from './siyuan/port.js';
@@ -38,7 +38,7 @@ export function createBuildSession(facts, options) {
38
38
  /** 建会话:找端口 → 解析安装目录 → 链接 → 激活。失败抛可读错误。 */
39
39
  async function start() {
40
40
  const current = facts;
41
- const name = readName(current.root, kind);
41
+ const name = readName(current.publicDir, kind);
42
42
  const port = await findKernelPort();
43
43
  const installPath = await resolveInstallDir({
44
44
  kind,
@@ -113,7 +113,7 @@ export function createBuildSession(facts, options) {
113
113
  async onWritten(files) {
114
114
  const current = facts;
115
115
  // 元数据搬运先做:校验时文件必须已经就位。
116
- copyMetadata({
116
+ copyReadmes({
117
117
  root: current.root,
118
118
  publicDir: current.publicDir,
119
119
  outDir: relative(current.root, current.outDir) || '.',
@@ -146,7 +146,11 @@ export function createBuildSession(facts, options) {
146
146
  onFinished() {
147
147
  const current = facts;
148
148
  buildCount += 1;
149
- const result = verifyTarget(kind, { root: current.root, outDir: current.outDir });
149
+ const result = verifyTarget(kind, {
150
+ root: current.root,
151
+ publicDir: current.publicDir,
152
+ outDir: current.outDir,
153
+ });
150
154
  if (!result.ok) {
151
155
  const text = formatIssues(result.issues);
152
156
  if (current.watch) {
@@ -17,9 +17,9 @@ export interface KindSpec {
17
17
  }
18
18
  export declare function kindSpec(kind: Kind): KindSpec;
19
19
  /**
20
- * 从项目目录推断类型:看 public/、dist/ 或项目根存在哪份清单。
20
+ * 从项目目录推断类型:看 publicDir 下存在哪份清单。
21
21
  * 同时存在多种时必须让人指定——装错类型的产物名会静默加载不到。
22
22
  */
23
- export declare function detectKind(root: string): Kind;
23
+ export declare function detectKind(publicDir: string): Kind;
24
24
  /** 读被测对象的名字;读不到抛可读错误,不猜默认值。 */
25
- export declare function readName(root: string, kind: Kind): string;
25
+ export declare function readName(publicDir: string, kind: Kind): string;
@@ -33,21 +33,28 @@ export function kindSpec(kind) {
33
33
  }
34
34
  const KIND_NAMES = ['theme', 'plugin'];
35
35
  /**
36
- * 从项目目录推断类型:看 public/、dist/ 或项目根存在哪份清单。
36
+ * 从项目目录推断类型:看 publicDir 下存在哪份清单。
37
37
  * 同时存在多种时必须让人指定——装错类型的产物名会静默加载不到。
38
38
  */
39
- export function detectKind(root) {
40
- const matches = KIND_NAMES.filter((kind) => manifestCandidates(root, SPECS[kind].manifestName).some((file) => existsSync(file)));
39
+ export function detectKind(publicDir) {
40
+ // publicDir 关掉时清单无处安放:思源要求清单在产物根,而没有 publicDir 就没有
41
+ // 「原样复制到产物根」的机制。这里直接说清楚,不要让用户去猜「产物缺少」。
42
+ if (!publicDir) {
43
+ throw new UserError('publicDir 已关闭,清单(theme.json / plugin.json)无处安放。\n' +
44
+ ' 思源要求清单出现在产物根,而 publicDir 是 Vite 唯一能原样复制到产物根的机制。\n' +
45
+ ' 请移除 publicDir: false,或改成指向存放清单的目录。');
46
+ }
47
+ const matches = KIND_NAMES.filter((kind) => manifestCandidates(publicDir, SPECS[kind].manifestName).some((file) => existsSync(file)));
41
48
  const [only] = matches;
42
49
  if (matches.length === 1 && only !== undefined)
43
50
  return only;
44
51
  throw new UserError(matches.length === 0
45
- ? `在 ${root} 的 public/、dist/ 或项目根下找不到 theme.json / plugin.json;请用 kind 选项显式指定类型`
52
+ ? `在 ${publicDir} 下找不到 theme.json / plugin.json;请把清单放进该目录,或用 kind 选项显式指定类型`
46
53
  : `同时匹配到多种类型:${matches.join('、')};请用 kind 选项显式指定`);
47
54
  }
48
55
  /** 读被测对象的名字;读不到抛可读错误,不猜默认值。 */
49
- export function readName(root, kind) {
50
- for (const file of manifestCandidates(root, SPECS[kind].manifestName)) {
56
+ export function readName(publicDir, kind) {
57
+ for (const file of manifestCandidates(publicDir, SPECS[kind].manifestName)) {
51
58
  const data = readJsonFile(file);
52
59
  if (isRecord(data)) {
53
60
  const name = data['name'];
@@ -3,7 +3,21 @@ export interface ManifestFile {
3
3
  path: string;
4
4
  data: Record<string, unknown>;
5
5
  }
6
- /** 清单候选路径,按优先级排列。 */
7
- export declare function manifestCandidates(root: string, manifest: string, extraDirs?: readonly string[]): string[];
6
+ /**
7
+ * 解析实际生效的 publicDir 绝对路径;`publicDir: false` 时返回空串。
8
+ *
9
+ * Vite 的 `publicDir` 可以指向任意目录(默认 `public`),也可以关掉。
10
+ * 所有找清单/资源的地方都必须走这里,不能拼死 `'public'`。
11
+ */
12
+ export declare function resolvePublicDir(root: string, configured: string | false | undefined): string;
13
+ /** 清单候选路径,按优先级排列。publicDir 关闭时只剩显式传入的目录。 */
14
+ export declare function manifestCandidates(publicDir: string, manifest: string, extraDirs?: readonly string[]): string[];
8
15
  /** 读一份清单与其路径;都读不到返回 null。 */
9
- export declare function readManifest(root: string, manifest: string, extraDirs?: readonly string[]): ManifestFile | null;
16
+ export declare function readManifest(publicDir: string, manifest: string, extraDirs?: readonly string[]): ManifestFile | null;
17
+ /**
18
+ * 从清单里收集引用到的文件名。
19
+ *
20
+ * `readme` 可能是字符串,也可能是 `{ default, zh_CN, ... }` 的 i18n 对象;
21
+ * `icon` / `preview` 只能是字符串。`keys` 用于只取其中一部分(比如只搬 README)。
22
+ */
23
+ export declare function declaredAssets(manifest: Record<string, unknown>, keys?: readonly string[]): string[];
@@ -1,23 +1,69 @@
1
1
  /**
2
- * 清单文件的读取。
2
+ * 清单文件的读取与 publicDir 解析。
3
3
  *
4
- * 清单可能出现在三处:`public/`(源码)、`dist/`(构建产物)、项目根
5
- * (解包后的 `package.zip` 就是这种布局)。
4
+ * 清单**只从 publicDir 读**:它是思源要求的产物根文件,而 `publicDir` 是 Vite
5
+ * 唯一「原样复制到产物根」的机制。项目根那份官方 sample 布局不做支持——
6
+ * 探测到它却复制不进产物,只会得到「产物缺少 theme.json」这种半截行为。
7
+ *
8
+ * 构建产物里的清单由调用方通过 `extraDirs` 显式传入(校验时用),
9
+ * 不作为默认候选——否则上一次构建留下的旧清单会盖住源码里的新清单。
6
10
  */
7
- import { join } from 'node:path';
11
+ import { join, resolve } from 'node:path';
8
12
  import { readJsonFile } from '../support/fs.js';
9
13
  import { isRecord } from '../support/guards.js';
10
- /** 清单候选路径,按优先级排列。 */
11
- export function manifestCandidates(root, manifest, extraDirs = []) {
12
- const dirs = [join(root, 'public'), join(root, 'dist'), root, ...extraDirs];
14
+ /**
15
+ * 解析实际生效的 publicDir 绝对路径;`publicDir: false` 时返回空串。
16
+ *
17
+ * Vite 的 `publicDir` 可以指向任意目录(默认 `public`),也可以关掉。
18
+ * 所有找清单/资源的地方都必须走这里,不能拼死 `'public'`。
19
+ */
20
+ export function resolvePublicDir(root, configured) {
21
+ if (configured === false)
22
+ return '';
23
+ if (typeof configured === 'string' && configured)
24
+ return resolve(root, configured);
25
+ return join(root, 'public');
26
+ }
27
+ /** 清单候选路径,按优先级排列。publicDir 关闭时只剩显式传入的目录。 */
28
+ export function manifestCandidates(publicDir, manifest, extraDirs = []) {
29
+ const dirs = [...(publicDir ? [publicDir] : []), ...extraDirs];
13
30
  return dirs.map((dir) => join(dir, manifest));
14
31
  }
15
32
  /** 读一份清单与其路径;都读不到返回 null。 */
16
- export function readManifest(root, manifest, extraDirs = []) {
17
- for (const path of manifestCandidates(root, manifest, extraDirs)) {
33
+ export function readManifest(publicDir, manifest, extraDirs = []) {
34
+ for (const path of manifestCandidates(publicDir, manifest, extraDirs)) {
18
35
  const data = readJsonFile(path);
19
36
  if (isRecord(data))
20
37
  return { path, data };
21
38
  }
22
39
  return null;
23
40
  }
41
+ /** 清单里引用外部文件名的字段名。 */
42
+ const ASSET_KEYS = ['readme', 'icon', 'preview'];
43
+ /**
44
+ * 从清单里收集引用到的文件名。
45
+ *
46
+ * `readme` 可能是字符串,也可能是 `{ default, zh_CN, ... }` 的 i18n 对象;
47
+ * `icon` / `preview` 只能是字符串。`keys` 用于只取其中一部分(比如只搬 README)。
48
+ */
49
+ export function declaredAssets(manifest, keys = ASSET_KEYS) {
50
+ const names = new Set();
51
+ if (keys.includes('readme')) {
52
+ const readme = manifest['readme'];
53
+ if (typeof readme === 'string')
54
+ names.add(readme);
55
+ else if (isRecord(readme)) {
56
+ for (const value of Object.values(readme))
57
+ if (typeof value === 'string')
58
+ names.add(value);
59
+ }
60
+ }
61
+ for (const key of keys) {
62
+ if (key === 'readme')
63
+ continue;
64
+ const value = manifest[key];
65
+ if (typeof value === 'string')
66
+ names.add(value);
67
+ }
68
+ return [...names];
69
+ }
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "vite-plugin-siyuan",
3
- "version": "0.1.0",
4
- "description": "Vite plugin for SiYuan theme/plugin development: config injection, link to workspace, hot reload, verify and package",
5
- "keywords": [
6
- "siyuan",
7
- "siyuan-theme",
8
- "siyuan-plugin",
9
- "vite-plugin",
10
- "hot-reload"
11
- ],
12
- "type": "module",
13
- "license": "MIT",
14
- "engines": {
15
- "node": ">=22.18.0"
16
- },
17
- "exports": {
18
- ".": {
19
- "types": "./dist/index.d.ts",
20
- "default": "./dist/index.js"
21
- },
22
- "./package.json": "./package.json"
23
- },
24
- "files": [
25
- "dist"
26
- ],
27
- "scripts": {
28
- "clean": "node --input-type=module -e \"import fs from 'node:fs'; fs.rmSync('dist',{recursive:true,force:true})\"",
29
- "build": "pnpm run clean && tsc -p tsconfig.build.json",
30
- "type-check": "tsc -p tsconfig.json --noEmit",
31
- "test": "vitest run",
32
- "check": "pnpm run type-check && pnpm run test"
33
- },
34
- "dependencies": {
35
- "consola": "^3.4.2",
36
- "fflate": "^0.8.3",
37
- "ps-list": "^9.0.0",
38
- "semver": "^7.8.6",
39
- "zod": "^4.6.7"
40
- },
41
- "peerDependencies": {
42
- "vite": ">=6"
43
- },
44
- "devDependencies": {
45
- "@types/node": "^26.6.6",
46
- "@types/semver": "^7.8.0",
47
- "typescript": "^7.0.2",
48
- "vite": "^8.3.5",
49
- "vitest": "^5.0.5"
50
- },
51
- "repository": {
52
- "type": "git",
53
- "url": "git+https://github.com/svchord/vite-plugin-siyuan.git"
54
- },
55
- "author": "svchord",
56
- "publishConfig": {
57
- "access": "public",
58
- "registry": "https://registry.npmjs.org/"
59
- }
60
- }
1
+ {
2
+ "name": "vite-plugin-siyuan",
3
+ "version": "0.3.0",
4
+ "description": "Vite plugin for SiYuan theme/plugin development: config injection, link to workspace, hot reload, verify and package",
5
+ "keywords": [
6
+ "siyuan",
7
+ "siyuan-theme",
8
+ "siyuan-plugin",
9
+ "vite-plugin",
10
+ "hot-reload"
11
+ ],
12
+ "type": "module",
13
+ "license": "MIT",
14
+ "engines": {
15
+ "node": ">=22.18.0"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "dependencies": {
28
+ "consola": "^3.4.2",
29
+ "fflate": "^0.8.3",
30
+ "ps-list": "^9.0.0",
31
+ "semver": "^7.8.6",
32
+ "zod": "^4.6.7"
33
+ },
34
+ "peerDependencies": {
35
+ "vite": ">=6"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.20.5",
39
+ "@types/semver": "^7.8.0",
40
+ "typescript": "^7.0.2",
41
+ "vite": "^8.3.5",
42
+ "vitest": "^5.0.5"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/svchord/vite-plugin-siyuan.git"
47
+ },
48
+ "author": "svchord",
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "registry": "https://registry.npmjs.org/"
52
+ },
53
+ "scripts": {
54
+ "clean": "node --input-type=module -e \"import fs from 'node:fs'; fs.rmSync('dist',{recursive:true,force:true})\"",
55
+ "build": "pnpm run clean && tsc -p tsconfig.build.json",
56
+ "type-check": "tsc -p tsconfig.json --noEmit",
57
+ "test": "vitest run",
58
+ "check": "pnpm run type-check && pnpm run test"
59
+ }
60
+ }