@dsh-workbench/module-tools 0.1.10 → 0.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/module-tools",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -61,8 +61,15 @@ for (const pkg of packages) {
61
61
  if (dsh === undefined) continue;
62
62
  declared += 1;
63
63
 
64
- if (dsh.bundle?.patch !== undefined && !existsSync(join(pkg.dir, dsh.bundle.patch))) {
65
- problems.push(`${name}: dsh.bundle.patch 指的文件不存在 —— ${dsh.bundle.patch}`);
64
+ // `dsh.bundle.patch` 允许是**一个字符串,也允许是一个数组**(dsh 自己的 dsh-web-app 就是数组:
65
+ // 它的 cordis.patch.yml + 四个 preset 文件全挂在这一处)。这里原来只按字符串处理,
66
+ // 于是第一个用数组的包会把判据**打崩**(ERR_INVALID_ARG_TYPE,堆栈指在 path.join 上,
67
+ // 而真正的问题是"声明形状没接住")—— 外壳加 DIY 的 preset 时踩到。
68
+ const patches = dsh.bundle?.patch;
69
+ for (const patch of patches === undefined ? [] : Array.isArray(patches) ? patches : [patches]) {
70
+ if (!existsSync(join(pkg.dir, patch))) {
71
+ problems.push(`${name}: dsh.bundle.patch 指的 ${patch} 不存在`);
72
+ }
66
73
  }
67
74
 
68
75
  if (dsh.client !== undefined) {
@@ -6,60 +6,64 @@ import { discoverModulePackages, discoverPackages } from '../discover.mjs';
6
6
  const SHELL = '@dsh-workbench/core';
7
7
 
8
8
  const shell = discoverPackages({ root: process.cwd() }).find((pkg) => pkg.manifest.name === SHELL);
9
- if (shell === undefined || !existsSync(join(shell.dir, 'src'))) {
10
- console.log(`✔ 独立构建:本仓没有 ${SHELL} 的源码(模块侧靠"不许 @/ 指外壳"那条判据守)`);
11
- process.exit(0);
9
+ const visible = shell === undefined ? undefined : join(shell.dir, 'src');
10
+
11
+ if (visible === undefined || !existsSync(visible)) {
12
+ console.log(`✔ 独立构建:跳过 —— 本仓没有 ${SHELL} 的源码("不许 @/ 指外壳"那条判据照旧在守)`);
13
+ } else {
14
+ runStandalone(visible);
12
15
  }
13
16
 
14
- const modules = discoverModulePackages({ root: process.cwd() });
15
- const visible = join(shell.dir, 'src');
16
- const hidden = join(shell.dir, 'src.hidden');
17
+ function runStandalone(visible) {
18
+ const modules = discoverModulePackages({ root: process.cwd() });
19
+ const hidden = join(shell.dir, 'src.hidden');
17
20
 
18
- const restore = () => {
19
- if (existsSync(hidden) && !existsSync(visible)) renameSync(hidden, visible);
20
- };
21
- process.on('SIGINT', () => {
22
- restore();
23
- process.exit(130);
24
- });
21
+ const restore = () => {
22
+ if (existsSync(hidden) && !existsSync(visible)) renameSync(hidden, visible);
23
+ };
24
+ process.on('SIGINT', () => {
25
+ restore();
26
+ process.exit(130);
27
+ });
25
28
 
26
- if (existsSync(hidden)) {
27
- console.error(`上次没还原干净:${hidden} 还在,手动改名回去再跑。`);
28
- process.exit(1);
29
- }
29
+ if (existsSync(hidden)) {
30
+ console.error(`上次没还原干净:${hidden} 还在,手动改名回去再跑。`);
31
+ process.exit(1);
32
+ }
30
33
 
31
- const rows = [];
32
- const problems = [];
33
- renameSync(visible, hidden);
34
- try {
35
- for (const pkg of modules) {
36
- const name = pkg.manifest.name;
37
- for (const script of ['build', 'typecheck']) {
38
- if (pkg.manifest.scripts?.[script] === undefined) continue;
39
- try {
40
- execFileSync('pnpm', ['run', script], { cwd: pkg.dir, stdio: 'pipe' });
41
- rows.push(`${name} ${script}: ✅`);
42
- } catch (error) {
43
- const output = `${error.stdout ?? ''}${error.stderr ?? ''}`;
44
- const first = output
45
- .split('\n')
46
- .filter((line) => /error|Cannot find|Could not resolve|ERR_/.test(line))
47
- .slice(0, 2)
48
- .map((line) => line.trim())
49
- .join(' | ');
50
- rows.push(`${name} ${script}: ❌`);
51
- problems.push(`${name} ${script} 在外壳源码不在场时失败 —— ${first.slice(0, 220)}`);
34
+ const rows = [];
35
+ const problems = [];
36
+ renameSync(visible, hidden);
37
+ try {
38
+ for (const pkg of modules) {
39
+ const name = pkg.manifest.name;
40
+ for (const script of ['build', 'typecheck']) {
41
+ if (pkg.manifest.scripts?.[script] === undefined) continue;
42
+ try {
43
+ execFileSync('pnpm', ['run', script], { cwd: pkg.dir, stdio: 'pipe' });
44
+ rows.push(`${name} ${script}: ✅`);
45
+ } catch (error) {
46
+ const output = `${error.stdout ?? ''}${error.stderr ?? ''}`;
47
+ const first = output
48
+ .split('\n')
49
+ .filter((line) => /error|Cannot find|Could not resolve|ERR_/.test(line))
50
+ .slice(0, 2)
51
+ .map((line) => line.trim())
52
+ .join(' | ');
53
+ rows.push(`${name} ${script}: ❌`);
54
+ problems.push(`${name} ${script} 在外壳源码不在场时失败 —— ${first.slice(0, 220)}`);
55
+ }
52
56
  }
53
57
  }
58
+ } finally {
59
+ restore();
54
60
  }
55
- } finally {
56
- restore();
57
- }
58
61
 
59
- if (problems.length > 0) {
60
- console.error(`✖ 独立构建 ${problems.length} 条不过:`);
61
- for (const problem of problems) console.error(` · ${problem}`);
62
- console.error(' (上面的清单就是"发布出去会散架"的地方:模块只许依赖已发布的接口)');
63
- process.exit(1);
62
+ if (problems.length > 0) {
63
+ console.error(`✖ 独立构建 ${problems.length} 条不过:`);
64
+ for (const problem of problems) console.error(` · ${problem}`);
65
+ console.error(' (上面的清单就是"发布出去会散架"的地方:模块只许依赖已发布的接口)');
66
+ process.exit(1);
67
+ }
68
+ console.log(`✔ 独立构建通过:${modules.length} 个模块包在外壳源码不在场时都能 build + typecheck`);
64
69
  }
65
- console.log(`✔ 独立构建通过:${modules.length} 个模块包在外壳源码不在场时都能 build + typecheck`);
package/src/init.mjs CHANGED
@@ -2,14 +2,25 @@ import { cpSync, existsSync, readFileSync, readdirSync, rmSync, writeFileSync }
2
2
  import { join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
- const TEMPLATE = fileURLToPath(new URL('../template', import.meta.url));
5
+ const TEMPLATES = {
6
+ minimal: '../template',
7
+ diy: '../template-diy',
8
+ };
6
9
  const UNIT_ID = /^[a-z][a-z0-9-]*$/;
7
10
 
8
11
  export async function init({ root, args = [] }) {
9
12
  const unit = args[0];
10
13
  if (unit === undefined || !UNIT_ID.test(unit)) {
11
- console.error('用法:dsh-wb-module init <unit> [--dir <目录>] [--description <文字>]');
14
+ console.error('用法:dsh-wb-module init <unit> [--dir <目录>] [--template minimal|diy] [--description <文字>]');
12
15
  console.error(' unit 是小写短横线标识(它同时是单元 id、包名后缀、卡片 class 前缀)');
16
+ console.error(' --template diy:带一个真数据源 + 三态 + ?wbMock=1 夹具的样板(默认 minimal 是空壳)');
17
+ process.exit(2);
18
+ }
19
+ const picked = args[args.indexOf('--template') + 1];
20
+ const which = args.includes('--template') ? picked : 'minimal';
21
+ const template = TEMPLATES[which];
22
+ if (template === undefined) {
23
+ console.error(`不认识的模板:${String(which)}(可用:${Object.keys(TEMPLATES).join(' / ')})`);
13
24
  process.exit(2);
14
25
  }
15
26
  const at = args.indexOf('--dir');
@@ -26,7 +37,12 @@ export async function init({ root, args = [] }) {
26
37
  __DESC__: description,
27
38
  };
28
39
  const fill = (text) => text.replace(/__[A-Z]+__/g, (token) => tokens[token] ?? token);
29
- cpSync(TEMPLATE, target, { recursive: true });
40
+ const templateDir = fileURLToPath(new URL(template, import.meta.url));
41
+ if (!existsSync(templateDir)) {
42
+ console.error(`模板 ${which} 的目录不在:${templateDir}`);
43
+ process.exit(2);
44
+ }
45
+ cpSync(templateDir, target, { recursive: true });
30
46
  const walk = (dir) => {
31
47
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
32
48
  const full = join(dir, entry.name);
@@ -44,5 +60,5 @@ export async function init({ root, args = [] }) {
44
60
  }
45
61
  };
46
62
  walk(target);
47
- console.log(`${target} 建好了:${unit} 模块骨架(build / check / selfcheck 已接工具)`);
63
+ console.log(`${target} 建好了:${unit} 模块骨架(模板 ${which};build / check / selfcheck 已接工具)`);
48
64
  }
@@ -4,7 +4,17 @@
4
4
 
5
5
  ```bash
6
6
  dsh-wb-module build # 出 dist/host.js 与套了 loader 壳的 client.js
7
- dsh-wb-module check # 契约 / 产物边界 / 源码边界 / 卡片渲染
7
+ dsh-wb-module check # 契约 / 产物边界 / 源码边界 / dsh 原语 / kit 面 / 卡片渲染 / 独立构建 / 内部标识 / 样式归属 / 不指外壳
8
+ dsh-wb-module check --only cards # 十条可以单跑
9
+ ```
10
+
11
+ 包自带的四个脚本(`check` 的契约判据要求它们都在):
12
+
13
+ ```bash
14
+ pnpm build # 构建产物
15
+ pnpm typecheck # tsc --noEmit
16
+ pnpm test # vitest(src/**/*.test.ts)
17
+ pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍,看卡片真进没进外壳
8
18
  ```
9
19
 
10
20
  装进本地工作台:
@@ -15,4 +25,8 @@ dsh plugin --profile workbench add <本目录>
15
25
 
16
26
  面板只读、写入只走 agent 工具;共享代码从 `@dsh-workbench/shared` 拿,
17
27
  单例机制(hooks / 容器组件 / 读缝)只能从 `@dsh-workbench/core/client` 的 kit 拿。
18
- 要发布:把 `package.json` 里的 `private` 去掉、按需改 `publishConfig`。
28
+ dsh 已有的组件与样式**先从 kit 拿**,别从 DOM + CSS 重画。
29
+
30
+ 要发布:把 `package.json` 里的 `private` 去掉,并显式写上发布目标
31
+ (`publishConfig.access: "public"` 或指向内网源的 `publishConfig.registry`)——
32
+ `private: true` 的包不许发到任何源,这是刻意的默认。
@@ -13,7 +13,7 @@
13
13
  "build": "dsh-wb-module build",
14
14
  "typecheck": "tsc --noEmit",
15
15
  "test": "vitest run",
16
- "selfcheck": "dsh-wb-module build && dsh-wb-module check",
16
+ "selfcheck": "dsh-wb-module build && node scripts/smoke.mjs",
17
17
  "prepack": "dsh-wb-module build"
18
18
  },
19
19
  "dsh": {
@@ -38,6 +38,5 @@
38
38
  "react": "^19.3.0",
39
39
  "typescript": "5.9",
40
40
  "vitest": "^5.0.2"
41
- },
42
- "publishConfig": { "access": "public" }
41
+ }
43
42
  }
@@ -0,0 +1,94 @@
1
+ // 模块包的接线自检:把 client.js 当成浏览器里的加载器那样跑一遍。
2
+ //
3
+ // 为什么值得有:宿主侧挂没挂上,curl /panels 就能看见;浏览器侧挂没挂上,过去只能靠"刷个页面看看"。
4
+ // 这个脚本复刻 dsh 加载器的**契约**(`window.__ModuleLoader__.load({ id, factory })` + `factory(require)`),
5
+ // 于是"模块注册的卡片到底进没进外壳"变成一条机器判据,而不再是人的眼睛。
6
+ //
7
+ // 负例在最后一段:服务不在的时候必须**报出来**,不许静默通过 —— 静默的模块是最难查的那种坏。
8
+
9
+ const PLUGIN_ID = '__NAME__';
10
+ const UNIT_ID = '__UNIT__';
11
+ const failures = [];
12
+
13
+ function check(name, ok, detail) {
14
+ if (ok) {
15
+ console.log(`✅ ${name}`);
16
+ return;
17
+ }
18
+ failures.push(name);
19
+ console.log(`❌ ${name}${detail === undefined ? '' : ` —— ${detail}`}`);
20
+ }
21
+
22
+ function moduleRequire(name) {
23
+ if (name === 'react') return { createElement: () => null };
24
+ if (name === '@dsh-workbench/core/client') return { kit: { components: { Card: () => null } } };
25
+ throw new Error(`模块包不该 require ${JSON.stringify(name)}:模块表里只有外壳的 seed 词与外部声明`);
26
+ }
27
+
28
+ let boot = null;
29
+ globalThis.window = {
30
+ __ModuleLoader__: {
31
+ load(entry) {
32
+ boot = entry;
33
+ },
34
+ },
35
+ };
36
+
37
+ await import(new URL('../client.js', import.meta.url).href);
38
+
39
+ check('client.js 调用 __ModuleLoader__.load()', boot !== null);
40
+ check('模块 id 等于包名', boot?.id === PLUGIN_ID, boot?.id);
41
+ if (boot === null) {
42
+ console.error(`\n${failures.length} 条没过`);
43
+ process.exit(1);
44
+ }
45
+
46
+ const pluginExports = boot.factory(moduleRequire);
47
+ check(
48
+ '注入 workbenchUnits(外壳交出来的那个服务名)',
49
+ Array.isArray(pluginExports.inject) && pluginExports.inject.includes('workbenchUnits'),
50
+ String(pluginExports.inject),
51
+ );
52
+ check('导出了 apply', typeof pluginExports.apply === 'function');
53
+
54
+ const faces = [];
55
+ const ctx = {
56
+ get: (name) =>
57
+ name === 'workbenchUnits'
58
+ ? {
59
+ registerUnit(face) {
60
+ faces.push(face);
61
+ return () => {};
62
+ },
63
+ }
64
+ : undefined,
65
+ effect: (fn) => {
66
+ fn();
67
+ },
68
+ };
69
+
70
+ pluginExports.apply(ctx);
71
+ check('卡片真的注册进了外壳', faces.length === 1, `注册了 ${faces.length} 个`);
72
+ check(`注册的是 ${UNIT_ID} 单元`, faces[0]?.manifest.id === UNIT_ID, faces[0]?.manifest.id);
73
+ check('卡片是个能画的组件', typeof faces[0]?.card === 'function');
74
+ check('卡片自己带标题与顺序(外壳的看板据此排)', faces[0]?.manifest.card?.order === 90);
75
+
76
+ const warnings = [];
77
+ const realWarn = console.warn;
78
+ console.warn = (...args) => warnings.push(args.join(' '));
79
+ pluginExports.apply({
80
+ get: () => undefined,
81
+ effect: (fn) => {
82
+ fn();
83
+ },
84
+ });
85
+ console.warn = realWarn;
86
+
87
+ check(
88
+ '负例:外壳没交服务时必须报出来(不许静默通过)',
89
+ warnings.some((line) => line.includes('workbenchUnits')),
90
+ warnings.join(' | '),
91
+ );
92
+
93
+ console.log(`\n${failures.length === 0 ? '模块自检通过' : `${failures.length} 条没过`}`);
94
+ if (failures.length > 0) process.exit(1);
@@ -1,27 +1,73 @@
1
1
  import { createElement as h } from 'react';
2
+ import { kit } from '@dsh-workbench/core/client';
3
+ import { messages } from './messages';
2
4
 
3
5
  const UNIT_ID = '__UNIT__';
4
6
 
7
+ interface UnitManifest {
8
+ readonly id: string;
9
+ readonly kind: string;
10
+ readonly card: {
11
+ readonly titleKey: string;
12
+ readonly order: number;
13
+ readonly enabledByDefault: boolean;
14
+ } | null;
15
+ readonly tools: readonly unknown[];
16
+ readonly host: { readonly inject: readonly string[] };
17
+ }
18
+
19
+ type Rendered = ReturnType<typeof h>;
20
+ type Translate = (key: string, params?: Record<string, unknown>) => string;
21
+
22
+ interface UnitClientFace {
23
+ readonly manifest: UnitManifest;
24
+ readonly messages: { readonly zh: Readonly<Record<string, string>>; readonly en: Readonly<Record<string, string>> };
25
+ readonly card: (props: { readonly t: Translate; readonly onRefresh: () => void }) => Rendered;
26
+ }
27
+
28
+ interface UnitClientApi {
29
+ registerUnit(face: UnitClientFace): () => void;
30
+ }
31
+
32
+ interface ClientContext {
33
+ get<T = unknown>(name: string): T | undefined;
34
+ effect(effect: () => void | (() => void), label?: string): () => void;
35
+ }
36
+
37
+ const manifest: UnitManifest = {
38
+ id: UNIT_ID,
39
+ kind: 'board',
40
+ card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
41
+ tools: [],
42
+ host: { inject: [] },
43
+ };
44
+
45
+ const face: UnitClientFace = {
46
+ manifest,
47
+ messages,
48
+ card: (props) => {
49
+ const { Card } = kit.components;
50
+ return h(
51
+ Card,
52
+ {
53
+ t: props.t,
54
+ title: props.t(`${UNIT_ID}.cardTitle`),
55
+ refreshing: false,
56
+ onRefresh: props.onRefresh,
57
+ },
58
+ h('p', null, props.t(`${UNIT_ID}.heading`)),
59
+ h('p', null, props.t(`${UNIT_ID}.body`)),
60
+ );
61
+ },
62
+ };
63
+
5
64
  export const inject = ['workbenchUnits'];
6
65
 
7
- export function apply(ctx) {
8
- const units = ctx.get('workbenchUnits');
66
+ export function apply(ctx: ClientContext): void {
67
+ const units = ctx.get<UnitClientApi>('workbenchUnits');
9
68
  if (units === undefined) {
10
69
  console.warn(`[${UNIT_ID}] 外壳没有提供 workbenchUnits 服务:这张卡片装不进来`);
11
70
  return;
12
71
  }
13
- ctx.effect(
14
- () =>
15
- units.registerUnit({
16
- manifest: {
17
- id: UNIT_ID,
18
- kind: 'board',
19
- card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
20
- tools: [],
21
- host: { inject: [] },
22
- },
23
- card: () => h('section', { className: `wb-${UNIT_ID}` }, h('h2', null, `${UNIT_ID}`)),
24
- }),
25
- `${UNIT_ID}: 注册`,
26
- );
72
+ ctx.effect(() => units.registerUnit(face), `__NAME__: 注册 ${UNIT_ID}`);
27
73
  }
@@ -4,24 +4,51 @@ export const name = '__NAME__';
4
4
 
5
5
  export const inject = ['workbenchUnits'];
6
6
 
7
- export function apply(ctx) {
8
- const units = ctx.get('workbenchUnits');
7
+ interface UnitManifest {
8
+ readonly id: string;
9
+ readonly kind: string;
10
+ readonly card: {
11
+ readonly titleKey: string;
12
+ readonly order: number;
13
+ readonly enabledByDefault: boolean;
14
+ } | null;
15
+ readonly tools: readonly unknown[];
16
+ readonly host: { readonly inject: readonly string[] };
17
+ }
18
+
19
+ interface UnitHostFace {
20
+ readonly manifest: UnitManifest;
21
+ mount(): { routes: Record<string, never>; registerTools(): Array<() => void> };
22
+ }
23
+
24
+ interface UnitRegistry {
25
+ register(face: UnitHostFace): () => void;
26
+ }
27
+
28
+ interface HostContext {
29
+ get<T = unknown>(name: string): T | undefined;
30
+ effect(effect: () => void | (() => void), label?: string): () => void;
31
+ logger?: { warn?(message: string): void } | undefined;
32
+ }
33
+
34
+ const manifest: UnitManifest = {
35
+ id: UNIT_ID,
36
+ kind: 'board',
37
+ card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
38
+ tools: [],
39
+ host: { inject: [] },
40
+ };
41
+
42
+ const face: UnitHostFace = {
43
+ manifest,
44
+ mount: () => ({ routes: {}, registerTools: () => [] }),
45
+ };
46
+
47
+ export function apply(ctx: HostContext): void {
48
+ const units = ctx.get<UnitRegistry>('workbenchUnits');
9
49
  if (units === undefined) {
10
50
  ctx.logger?.warn?.(`[${UNIT_ID}] 外壳没有提供 workbenchUnits 服务:这个模块装不进来`);
11
51
  return;
12
52
  }
13
- ctx.effect(
14
- () =>
15
- units.register({
16
- manifest: {
17
- id: UNIT_ID,
18
- kind: 'board',
19
- card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
20
- tools: [],
21
- host: { inject: [] },
22
- },
23
- mount: () => ({ routes: {}, registerTools: () => [] }),
24
- }),
25
- `${UNIT_ID}: 注册`,
26
- );
53
+ ctx.effect(() => units.register(face), `__NAME__: 注册 ${UNIT_ID}`);
27
54
  }
@@ -0,0 +1,12 @@
1
+ export const messages = {
2
+ zh: {
3
+ '__UNIT__.cardTitle': '新模块',
4
+ '__UNIT__.heading': '这张卡还是空的',
5
+ '__UNIT__.body': '改 src/client.tsx 画你要的内容,然后跑 dsh-wb-module build。',
6
+ },
7
+ en: {
8
+ '__UNIT__.cardTitle': 'New module',
9
+ '__UNIT__.heading': 'This card is still empty',
10
+ '__UNIT__.body': 'Edit src/client.tsx to render what you need, then run dsh-wb-module build.',
11
+ },
12
+ };
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { apply, inject, name } from './host';
3
+ import { apply as applyClient, inject as injectClient } from './client';
4
+
5
+ vi.mock('@dsh-workbench/core/client', () => ({
6
+ kit: { components: { Card: () => null } },
7
+ }));
8
+
9
+ function fakeCtx(service: unknown) {
10
+ const faces: unknown[] = [];
11
+ return {
12
+ faces,
13
+ ctx: {
14
+ get: () => service,
15
+ effect: (effect: () => void | (() => void)) => {
16
+ effect();
17
+ return () => {};
18
+ },
19
+ },
20
+ };
21
+ }
22
+
23
+ describe('__NAME__ 的面', () => {
24
+ it('插件名就是包名', () => {
25
+ expect(name).toBe('__NAME__');
26
+ });
27
+
28
+ it('两侧都只注入外壳交出来的那个服务', () => {
29
+ expect(inject).toEqual(['workbenchUnits']);
30
+ expect(injectClient).toEqual(['workbenchUnits']);
31
+ });
32
+
33
+ it('宿主半侧把自己的面注册进外壳', () => {
34
+ const { ctx, faces } = fakeCtx({
35
+ register: (face: unknown) => {
36
+ faces.push(face);
37
+ return () => {};
38
+ },
39
+ });
40
+ apply(ctx as never);
41
+ expect(faces).toHaveLength(1);
42
+ expect((faces[0] as { manifest: { id: string } }).manifest.id).toBe('__UNIT__');
43
+ });
44
+
45
+ it('浏览器半侧把自己的卡片注册进外壳', () => {
46
+ const { ctx, faces } = fakeCtx({
47
+ registerUnit: (face: unknown) => {
48
+ faces.push(face);
49
+ return () => {};
50
+ },
51
+ });
52
+ applyClient(ctx as never);
53
+ expect(faces).toHaveLength(1);
54
+ });
55
+ });
@@ -14,7 +14,7 @@
14
14
  "skipLibCheck": true,
15
15
  "isolatedModules": true,
16
16
  "verbatimModuleSyntax": true,
17
- "paths": { "@/modules/__UNIT__/*": ["src/*"] }
17
+ "paths": { "@/modules/__UNIT__/*": ["./src/*"] }
18
18
  },
19
19
  "include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*.d.ts"]
20
20
  }
@@ -0,0 +1,14 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { defineConfig } from 'vitest/config';
3
+
4
+ const here = (relative: string): string => fileURLToPath(new URL(relative, import.meta.url));
5
+
6
+ export default defineConfig({
7
+ resolve: {
8
+ alias: [
9
+ { find: /^@\/modules\/__UNIT__\//, replacement: `${here('./src')}/` },
10
+ { find: /^@\/platforms\/[^/]+\//, replacement: `${here('./src/platforms')}/` },
11
+ ],
12
+ },
13
+ test: { environment: 'node', include: ['src/**/*.test.ts', 'src/**/*.test.tsx'] },
14
+ });