@koishi-ce/plugin-config 1.1.1 → 1.1.3

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/lib/index.mjs CHANGED
@@ -1,10 +1,9 @@
1
- import { join, resolve } from "node:path";
1
+ import { dirname, join, resolve } from "node:path";
2
2
  import { Context, Logger, Schema, getServiceContext, remove } from "@koishi-ce/koishi";
3
3
  import { DataService } from "@koishi-ce/console";
4
4
  import { LocalScanner, getPluginShortname, isResidentInCache } from "@koishi-ce/registry";
5
5
  import { Loader } from "@koishi-ce/loader";
6
- import { existsSync, readFileSync } from "node:fs";
7
- import { readdir } from "node:fs/promises";
6
+ import { readFileSync } from "node:fs";
8
7
  //#region src/shared/packages.ts
9
8
  /**
10
9
  * 插件包元数据提供器(服务端共享实现)。
@@ -464,9 +463,14 @@ var ConfigWriter = class extends DataService {
464
463
  * 本仓库 koishi.yml 的插件键统一是相对路径(./plugins/...),而 Bun
465
464
  * 不会把未被依赖的 workspace 包链入 node_modules,仅靠 LocalScanner
466
465
  * 会漏掉大部分源码包——因此 collect 时额外按配置键逐个加载
467
- * workspace 包,并写入 paths(配置键 → 包名映射,供前端解析插件名)。
466
+ * workspace 包,并写入 paths(配置键 → 包名映射,供前端解析插件名);
467
+ * 未启用的包(含嵌套 monorepo 子包)则按 workspaces 声明展开收录。
468
468
  */
469
469
  const logger = new Logger("config");
470
+ /** 插件包名的收录口径:只认三种插件命名前缀(与 LocalScanner 一致) */
471
+ const PLUGIN_NAME_PATTERN = /^(koishi-plugin-|@koishi(?:-ce)?\/plugin-)/;
472
+ /** 宿主未声明 workspaces 时的兜底约定(模板默认布局) */
473
+ const FALLBACK_WORKSPACES = ["plugins/*", "external/*"];
470
474
  /**
471
475
  * 本机插件扫描器:在 LocalScanner 的基础上扩展 parsePackage,
472
476
  * 让"扫描到某个包"与"解析该插件的运行时信息"在同一次遍历中完成。
@@ -513,7 +517,7 @@ var PackageProvider$1 = class extends PackageProvider {
513
517
  }
514
518
  /**
515
519
  * 收集全部 workspace 源码包:koishi.yml 已有相对路径键引用的(已启用)
516
- * 加上 external/ 约定目录下的(可能尚未启用)。
520
+ * 加上 workspaces 声明覆盖的(可能尚未启用,含嵌套 monorepo 子包)。
517
521
  */
518
522
  async collectWorkspacePackages() {
519
523
  const index = /* @__PURE__ */ new Map();
@@ -545,39 +549,78 @@ var PackageProvider$1 = class extends PackageProvider {
545
549
  if (!path?.startsWith("./")) return;
546
550
  await addPath(path, true);
547
551
  });
548
- await this.collectExternalPackages(addPath);
552
+ await this.collectWorkspacePlugins(addPath);
549
553
  } catch (error) {
550
554
  logger.warn(error);
551
555
  }
552
556
  return [...index.values()];
553
557
  }
554
558
  /**
555
- * 扫描 external/ 约定目录(官方 koishi-scripts 的插件开发目录),
556
- * 收录其中尚未启用的 workspace 插件包。
559
+ * 收集 workspaces 声明覆盖的未启用插件包(嵌套 monorepo 兼容)。
557
560
  *
558
- * Bun 只把被依赖引用的 workspace 包链入 node_modules,未启用的
559
- * external 插件因此不在本机扫描的视野内(yarn 全量链接生态下的
560
- * 自动发现语义失效)——这里显式遍历目录补齐。收录条目带
561
- * ./external/<name> 路径键,前端启用时以该键写入配置(loader 的
562
- * 相对路径解析原生支持;短名不经 node_modules 会解析失败)。
561
+ * 以宿主 package.json 的 workspaces 通配为唯一真相源:声明写多深,
562
+ * 收录就能看多深——external/ 下克隆的 monorepo 形态插件(packages/
563
+ * 子包在二级及更深)无需平铺即可见。Bun 只把被依赖引用的 workspace
564
+ * 包链入 node_modules,未启用的插件不在本机扫描视野内(yarn 全量
565
+ * 链接生态的自动发现语义失效),这里是唯一补齐入口。
566
+ *
567
+ * 通配是目录模式,拼上 /package.json 以清单文件为锚点扫描
568
+ * (Bun.Glob 的 onlyDirs 语义不可靠,win32 实测返回文件路径且分隔符
569
+ * 为反斜杠,归一为 / 后再派生收录键);负向模式(! 前缀)同样拼
570
+ * 锚点后对正向结果做差集——模板对 external 下 node_modules 的负向
571
+ * 通配即用于排除搬迁残留的依赖目录(注释里不写通配字面量,
572
+ * biome 会把其中的 JSDoc 起始序列误解析成类型)。收录口径与
573
+ * LocalScanner 的本机扫描一致:只认三种插件命名前缀,monorepo
574
+ * 根(@scope/monorepo 形态)与普通库不进列表;收录键
575
+ * ./<相对路径>(loader 的相对路径解析原生支持任意深度)。
563
576
  */
564
- async collectExternalPackages(addPath) {
565
- const base = resolve(this.scanner.baseDir, "external");
566
- const entries = await readdir(base).catch(() => []);
567
- for (const entry of entries) {
568
- const dir = join(base, entry);
569
- if (!existsSync(join(dir, "package.json"))) continue;
570
- try {
571
- const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
572
- if (typeof manifest.name !== "string" || !/^(koishi-plugin-|@koishi(?:-ce)?\/plugin-)/.test(manifest.name)) continue;
573
- await addPath(`./external/${entry}`, false);
574
- } catch (error) {
575
- logger.warn(error);
577
+ async collectWorkspacePlugins(addPath) {
578
+ const baseDir = this.scanner.baseDir;
579
+ const patterns = readWorkspacePatterns(baseDir);
580
+ const anchor = (pattern) => `${pattern.replace(/\/+$/, "")}/package.json`;
581
+ const excluded = /* @__PURE__ */ new Set();
582
+ for (const pattern of patterns) {
583
+ if (!pattern.startsWith("!")) continue;
584
+ for (const rel of new Bun.Glob(anchor(pattern.slice(1))).scanSync({ cwd: baseDir })) excluded.add(normalizeSlashes(rel));
585
+ }
586
+ const seen = /* @__PURE__ */ new Set();
587
+ for (const pattern of patterns) {
588
+ if (pattern.startsWith("!")) continue;
589
+ for (const rel of new Bun.Glob(anchor(pattern)).scanSync({ cwd: baseDir })) {
590
+ const manifestPath = normalizeSlashes(rel);
591
+ if (excluded.has(manifestPath) || seen.has(manifestPath)) continue;
592
+ seen.add(manifestPath);
593
+ try {
594
+ const manifest = JSON.parse(readFileSync(join(baseDir, manifestPath), "utf8"));
595
+ if (typeof manifest.name !== "string" || !PLUGIN_NAME_PATTERN.test(manifest.name)) continue;
596
+ await addPath(`./${dirname(manifestPath)}`, false);
597
+ } catch (error) {
598
+ logger.warn(error);
599
+ }
576
600
  }
577
601
  }
578
602
  }
579
603
  };
580
604
  /**
605
+ * 读取宿主 package.json 的 workspaces 通配声明;缺失、非数组(如 yarn
606
+ * no-hoist 的对象形态)或为空时回退模板默认约定——旧项目与 prod 形态
607
+ * (workspaces 已被脚手架删除)的一级目录可见性不回退。
608
+ */
609
+ function readWorkspacePatterns(baseDir) {
610
+ try {
611
+ const manifest = JSON.parse(readFileSync(join(baseDir, "package.json"), "utf8"));
612
+ if (Array.isArray(manifest.workspaces)) {
613
+ const patterns = manifest.workspaces.filter((pattern) => typeof pattern === "string");
614
+ if (patterns.length) return patterns;
615
+ }
616
+ } catch {}
617
+ return FALLBACK_WORKSPACES;
618
+ }
619
+ /** Bun.Glob 的 scan 在 win32 返回反斜杠分隔的相对路径,统一归一为 / */
620
+ function normalizeSlashes(path) {
621
+ return path.split("\\").join("/");
622
+ }
623
+ /**
581
624
  * 递归遍历插件配置表并对每个插件键执行回调:`$` 开头的键是内部控制
582
625
  * 字段,`group:` 分组键的值内嵌套下一层插件表,其余键原样传入回调。
583
626
  */
@@ -611,8 +654,8 @@ function apply(ctx) {
611
654
  ctx.plugin(ServiceProvider);
612
655
  ctx.plugin(ConfigWriter);
613
656
  ctx.console.addEntry({
614
- dev: resolve(__dirname, "../client/index.ts"),
615
- prod: resolve(__dirname, "../dist")
657
+ dev: resolve(import.meta.dir, "../client/index.ts"),
658
+ prod: resolve(import.meta.dir, "../dist")
616
659
  });
617
660
  }
618
661
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@koishi-ce/plugin-config",
3
3
  "description": "Manage your bots and plugins with console",
4
- "version": "1.1.1",
4
+ "version": "1.1.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.mjs",
7
7
  "types": "lib/index.d.ts",
@@ -67,15 +67,15 @@
67
67
  "@koishi-ce/koishi": "^1.0.0"
68
68
  },
69
69
  "devDependencies": {
70
- "@koishi-ce/client": "^1.2.2",
71
- "@koishi-ce/plugin-hmr": "^1.1.1",
70
+ "@koishi-ce/client": "^1.2.4",
71
+ "@koishi-ce/plugin-hmr": "^1.1.2",
72
72
  "element-plus": "^2.14.5",
73
73
  "vue": "^3.5.12",
74
74
  "vue-router": "^5.2.0"
75
75
  },
76
76
  "dependencies": {
77
- "@koishi-ce/console": "^1.0.0",
78
- "@koishi-ce/loader": "^1.0.7",
77
+ "@koishi-ce/console": "^1.0.1",
78
+ "@koishi-ce/loader": "^1.0.9",
79
79
  "@koishi-ce/registry": "^1.0.5"
80
80
  }
81
81
  }
@@ -290,6 +290,67 @@ writeFileSync(
290
290
  mkdirSync(join(fixtureRoot, "external/no-manifest"), {
291
291
  recursive: true,
292
292
  });
293
+ // 嵌套 monorepo(新模板 workspaces 声明形态):子包在二级深度可收录,
294
+ // monorepo 根(@scope/monorepo 命名)不进列表,node_modules 残留探针
295
+ // 被负向通配排除;plugins/ 一级的未启用包(盲区修复)同样可收录
296
+ writeFileSync(
297
+ join(fixtureRoot, "package.json"),
298
+ JSON.stringify({
299
+ name: "fixture-host",
300
+ private: true,
301
+ workspaces: [
302
+ "plugins/*",
303
+ "external/**",
304
+ "!external/**/node_modules/**",
305
+ ],
306
+ }),
307
+ );
308
+ mkdirSync(
309
+ join(fixtureRoot, "external/infra/packages/war-lib"),
310
+ { recursive: true },
311
+ );
312
+ writeFileSync(
313
+ join(
314
+ fixtureRoot,
315
+ "external/infra/packages/war-lib/package.json",
316
+ ),
317
+ JSON.stringify({
318
+ name: "koishi-plugin-war-lib",
319
+ version: "0.2.0",
320
+ }),
321
+ );
322
+ writeFileSync(
323
+ join(fixtureRoot, "external/infra/package.json"),
324
+ JSON.stringify({
325
+ name: "@infra/monorepo",
326
+ version: "1.0.0",
327
+ private: true,
328
+ }),
329
+ );
330
+ mkdirSync(
331
+ join(fixtureRoot, "external/infra/node_modules/zzz"),
332
+ { recursive: true },
333
+ );
334
+ writeFileSync(
335
+ join(
336
+ fixtureRoot,
337
+ "external/infra/node_modules/zzz/package.json",
338
+ ),
339
+ JSON.stringify({
340
+ name: "koishi-plugin-zzz-pollution",
341
+ version: "1.0.0",
342
+ }),
343
+ );
344
+ mkdirSync(join(fixtureRoot, "plugins/local-tool"), {
345
+ recursive: true,
346
+ });
347
+ writeFileSync(
348
+ join(fixtureRoot, "plugins/local-tool/package.json"),
349
+ JSON.stringify({
350
+ name: "koishi-plugin-local-tool",
351
+ version: "0.3.0",
352
+ }),
353
+ );
293
354
  loader.baseDir = fixtureRoot;
294
355
 
295
356
  loader.config = {
@@ -462,6 +523,83 @@ describe("@koishi-ce/plugin-config", () => {
462
523
  expect(data["no-manifest"]).toBeUndefined();
463
524
  });
464
525
 
526
+ it("workspaces 声明驱动收录嵌套 monorepo 子包与 plugins/ 未启用包", async () => {
527
+ const provider = app.get(
528
+ "console.services.packages",
529
+ ) as unknown as {
530
+ get(): Promise<Dict<Record<string, unknown>>>;
531
+ };
532
+ const data = await provider.get();
533
+ // 嵌套子包(external/** 任意深度):收录并以深路径为键
534
+ const lib = data["koishi-plugin-war-lib"];
535
+ expect(lib?.["workspace"]).toBe(true);
536
+ expect(lib?.["paths"]).toEqual([
537
+ "./external/infra/packages/war-lib",
538
+ ]);
539
+ expect(lib?.["runtime"]).toBeUndefined();
540
+ // plugins/ 一级的未启用包同样可收录(此前的可见性盲区)
541
+ expect(
542
+ data["koishi-plugin-local-tool"]?.["paths"],
543
+ ).toEqual(["./plugins/local-tool"]);
544
+ // monorepo 根(@scope/monorepo 命名)不进列表
545
+ expect(data["@infra/monorepo"]).toBeUndefined();
546
+ // node_modules 残留探针被负向通配排除(即使命名合规)
547
+ expect(
548
+ data["koishi-plugin-zzz-pollution"],
549
+ ).toBeUndefined();
550
+ });
551
+
552
+ it("readWorkspacePatterns:无清单 / 非数组 / 空数组时回退约定", async () => {
553
+ const { readWorkspacePatterns } = await import(
554
+ "../node/packages.ts"
555
+ );
556
+ const empty = mkdtempSync(
557
+ join(tmpdir(), "koishi-config-ws-"),
558
+ );
559
+ try {
560
+ // 无 package.json → 兜底约定
561
+ expect(readWorkspacePatterns(empty)).toEqual([
562
+ "plugins/*",
563
+ "external/*",
564
+ ]);
565
+ // 非数组(yarn no-hoist 对象形态)与空数组同理
566
+ writeFileSync(
567
+ join(empty, "package.json"),
568
+ JSON.stringify({
569
+ workspaces: { packages: ["external/*"] },
570
+ }),
571
+ );
572
+ expect(readWorkspacePatterns(empty)).toEqual([
573
+ "plugins/*",
574
+ "external/*",
575
+ ]);
576
+ writeFileSync(
577
+ join(empty, "package.json"),
578
+ JSON.stringify({ workspaces: [] }),
579
+ );
580
+ expect(readWorkspacePatterns(empty)).toEqual([
581
+ "plugins/*",
582
+ "external/*",
583
+ ]);
584
+ // 合法声明原样透传(负向模式保留 ! 前缀)
585
+ writeFileSync(
586
+ join(empty, "package.json"),
587
+ JSON.stringify({
588
+ workspaces: [
589
+ "external/**",
590
+ "!external/**/node_modules/**",
591
+ ],
592
+ }),
593
+ );
594
+ expect(readWorkspacePatterns(empty)).toEqual([
595
+ "external/**",
596
+ "!external/**/node_modules/**",
597
+ ]);
598
+ } finally {
599
+ rmSync(empty, { recursive: true, force: true });
600
+ }
601
+ });
602
+
465
603
  itQuiet(
466
604
  ["config"],
467
605
  "request-runtime 按路径键 / 短名解析并刷新,失败结果同样缓存",
package/src/node/index.ts CHANGED
@@ -50,7 +50,7 @@ export function apply(ctx: Context) {
50
50
  // 路径相对 lib 产物(运行时执行形态)而非 src 源码:本文件在源码里位于
51
51
  // src/node(两层),打包后位于 lib(一层),深度不一致,按 lib 计
52
52
  ctx.console.addEntry({
53
- dev: resolve(__dirname, "../client/index.ts"),
54
- prod: resolve(__dirname, "../dist"),
53
+ dev: resolve(import.meta.dir, "../client/index.ts"),
54
+ prod: resolve(import.meta.dir, "../dist"),
55
55
  });
56
56
  }
@@ -10,11 +10,11 @@
10
10
  * 本仓库 koishi.yml 的插件键统一是相对路径(./plugins/...),而 Bun
11
11
  * 不会把未被依赖的 workspace 包链入 node_modules,仅靠 LocalScanner
12
12
  * 会漏掉大部分源码包——因此 collect 时额外按配置键逐个加载
13
- * workspace 包,并写入 paths(配置键 → 包名映射,供前端解析插件名)。
13
+ * workspace 包,并写入 paths(配置键 → 包名映射,供前端解析插件名);
14
+ * 未启用的包(含嵌套 monorepo 子包)则按 workspaces 声明展开收录。
14
15
  */
15
- import { existsSync, readFileSync } from "node:fs";
16
- import { readdir } from "node:fs/promises";
17
- import { join, resolve } from "node:path";
16
+ import { readFileSync } from "node:fs";
17
+ import { dirname, join, resolve } from "node:path";
18
18
  import { Logger } from "@koishi-ce/koishi";
19
19
  import {
20
20
  getPluginShortname,
@@ -25,6 +25,13 @@ import * as shared from "../shared/index.ts";
25
25
 
26
26
  const logger = new Logger("config");
27
27
 
28
+ /** 插件包名的收录口径:只认三种插件命名前缀(与 LocalScanner 一致) */
29
+ const PLUGIN_NAME_PATTERN =
30
+ /^(koishi-plugin-|@koishi(?:-ce)?\/plugin-)/;
31
+
32
+ /** 宿主未声明 workspaces 时的兜底约定(模板默认布局) */
33
+ const FALLBACK_WORKSPACES = ["plugins/*", "external/*"];
34
+
28
35
  /**
29
36
  * 本机插件扫描器:在 LocalScanner 的基础上扩展 parsePackage,
30
37
  * 让"扫描到某个包"与"解析该插件的运行时信息"在同一次遍历中完成。
@@ -84,7 +91,7 @@ export class PackageProvider extends shared.PackageProvider {
84
91
 
85
92
  /**
86
93
  * 收集全部 workspace 源码包:koishi.yml 已有相对路径键引用的(已启用)
87
- * 加上 external/ 约定目录下的(可能尚未启用)。
94
+ * 加上 workspaces 声明覆盖的(可能尚未启用,含嵌套 monorepo 子包)。
88
95
  */
89
96
  private async collectWorkspacePackages(): Promise<
90
97
  shared.PackageProvider.Data[]
@@ -140,7 +147,7 @@ export class PackageProvider extends shared.PackageProvider {
140
147
  await addPath(path, true);
141
148
  },
142
149
  );
143
- await this.collectExternalPackages(addPath);
150
+ await this.collectWorkspacePlugins(addPath);
144
151
  } catch (error) {
145
152
  logger.warn(error);
146
153
  }
@@ -148,44 +155,108 @@ export class PackageProvider extends shared.PackageProvider {
148
155
  }
149
156
 
150
157
  /**
151
- * 扫描 external/ 约定目录(官方 koishi-scripts 的插件开发目录),
152
- * 收录其中尚未启用的 workspace 插件包。
158
+ * 收集 workspaces 声明覆盖的未启用插件包(嵌套 monorepo 兼容)。
153
159
  *
154
- * Bun 只把被依赖引用的 workspace 包链入 node_modules,未启用的
155
- * external 插件因此不在本机扫描的视野内(yarn 全量链接生态下的
156
- * 自动发现语义失效)——这里显式遍历目录补齐。收录条目带
157
- * ./external/<name> 路径键,前端启用时以该键写入配置(loader 的
158
- * 相对路径解析原生支持;短名不经 node_modules 会解析失败)。
160
+ * 以宿主 package.json 的 workspaces 通配为唯一真相源:声明写多深,
161
+ * 收录就能看多深——external/ 下克隆的 monorepo 形态插件(packages/
162
+ * 子包在二级及更深)无需平铺即可见。Bun 只把被依赖引用的 workspace
163
+ * 包链入 node_modules,未启用的插件不在本机扫描视野内(yarn 全量
164
+ * 链接生态的自动发现语义失效),这里是唯一补齐入口。
165
+ *
166
+ * 通配是目录模式,拼上 /package.json 以清单文件为锚点扫描
167
+ * (Bun.Glob 的 onlyDirs 语义不可靠,win32 实测返回文件路径且分隔符
168
+ * 为反斜杠,归一为 / 后再派生收录键);负向模式(! 前缀)同样拼
169
+ * 锚点后对正向结果做差集——模板对 external 下 node_modules 的负向
170
+ * 通配即用于排除搬迁残留的依赖目录(注释里不写通配字面量,
171
+ * biome 会把其中的 JSDoc 起始序列误解析成类型)。收录口径与
172
+ * LocalScanner 的本机扫描一致:只认三种插件命名前缀,monorepo
173
+ * 根(@scope/monorepo 形态)与普通库不进列表;收录键
174
+ * ./<相对路径>(loader 的相对路径解析原生支持任意深度)。
159
175
  */
160
- private async collectExternalPackages(
176
+ private async collectWorkspacePlugins(
161
177
  addPath: (path: string, warm: boolean) => Promise<void>,
162
178
  ) {
163
- const base = resolve(this.scanner.baseDir, "external");
164
- const entries = await readdir(base).catch(() => []);
165
- for (const entry of entries) {
166
- const dir = join(base, entry);
167
- if (!existsSync(join(dir, "package.json"))) continue;
168
- try {
169
- const manifest = JSON.parse(
170
- readFileSync(join(dir, "package.json"), "utf8"),
171
- ) as { name?: unknown };
172
- // 与 LocalScanner 的本机收录口径一致:只认三种插件
173
- // 命名前缀,external 下的普通库与杂项目录不进列表
179
+ const baseDir = this.scanner.baseDir;
180
+ const patterns = readWorkspacePatterns(baseDir);
181
+ // 目录通配 → 清单文件锚点(去尾斜杠后拼接;负向模式去掉 ! 前缀)
182
+ const anchor = (pattern: string) =>
183
+ `${pattern.replace(/\/+$/, "")}/package.json`;
184
+ const excluded = new Set<string>();
185
+ for (const pattern of patterns) {
186
+ if (!pattern.startsWith("!")) continue;
187
+ for (const rel of new Bun.Glob(
188
+ anchor(pattern.slice(1)),
189
+ ).scanSync({ cwd: baseDir })) {
190
+ excluded.add(normalizeSlashes(rel));
191
+ }
192
+ }
193
+ const seen = new Set<string>();
194
+ for (const pattern of patterns) {
195
+ if (pattern.startsWith("!")) continue;
196
+ for (const rel of new Bun.Glob(
197
+ anchor(pattern),
198
+ ).scanSync({ cwd: baseDir })) {
199
+ const manifestPath = normalizeSlashes(rel);
174
200
  if (
175
- typeof manifest.name !== "string" ||
176
- !/^(koishi-plugin-|@koishi(?:-ce)?\/plugin-)/.test(
177
- manifest.name,
178
- )
201
+ excluded.has(manifestPath) ||
202
+ seen.has(manifestPath)
179
203
  )
180
204
  continue;
181
- await addPath(`./external/${entry}`, false);
182
- } catch (error) {
183
- logger.warn(error);
205
+ seen.add(manifestPath);
206
+ try {
207
+ const manifest = JSON.parse(
208
+ readFileSync(
209
+ join(baseDir, manifestPath),
210
+ "utf8",
211
+ ),
212
+ ) as { name?: unknown };
213
+ if (
214
+ typeof manifest.name !== "string" ||
215
+ !PLUGIN_NAME_PATTERN.test(manifest.name)
216
+ )
217
+ continue;
218
+ await addPath(
219
+ `./${dirname(manifestPath)}`,
220
+ false,
221
+ );
222
+ } catch (error) {
223
+ logger.warn(error);
224
+ }
184
225
  }
185
226
  }
186
227
  }
187
228
  }
188
229
 
230
+ /**
231
+ * 读取宿主 package.json 的 workspaces 通配声明;缺失、非数组(如 yarn
232
+ * no-hoist 的对象形态)或为空时回退模板默认约定——旧项目与 prod 形态
233
+ * (workspaces 已被脚手架删除)的一级目录可见性不回退。
234
+ */
235
+ export function readWorkspacePatterns(
236
+ baseDir: string,
237
+ ): string[] {
238
+ try {
239
+ const manifest = JSON.parse(
240
+ readFileSync(join(baseDir, "package.json"), "utf8"),
241
+ ) as { workspaces?: unknown };
242
+ if (Array.isArray(manifest.workspaces)) {
243
+ const patterns = manifest.workspaces.filter(
244
+ (pattern): pattern is string =>
245
+ typeof pattern === "string",
246
+ );
247
+ if (patterns.length) return patterns;
248
+ }
249
+ } catch {
250
+ // 无宿主清单(异常环境):走兜底约定
251
+ }
252
+ return FALLBACK_WORKSPACES;
253
+ }
254
+
255
+ /** Bun.Glob 的 scan 在 win32 返回反斜杠分隔的相对路径,统一归一为 / */
256
+ function normalizeSlashes(path: string): string {
257
+ return path.split("\\").join("/");
258
+ }
259
+
189
260
  /**
190
261
  * 递归遍历插件配置表并对每个插件键执行回调:`$` 开头的键是内部控制
191
262
  * 字段,`group:` 分组键的值内嵌套下一层插件表,其余键原样传入回调。