@dsh-workbench/module-tools 0.1.14 → 0.1.16
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 +71 -0
- package/bin/dsh-wb-module.mjs +4 -2
- package/package.json +3 -2
- package/src/build.mjs +2 -11
- package/src/check.mjs +88 -7
- package/src/checks/alias.mjs +51 -29
- package/src/checks/bundles.mjs +44 -31
- package/src/checks/cards.mjs +119 -107
- package/src/checks/contracts.mjs +621 -116
- package/src/checks/kit.mjs +127 -64
- package/src/checks/markers.mjs +55 -32
- package/src/checks/primitives.mjs +82 -57
- package/src/checks/source.mjs +106 -92
- package/src/checks/standalone.mjs +54 -24
- package/src/checks/style.mjs +133 -94
- package/src/css-injection.mjs +27 -0
- package/src/design.mjs +424 -0
- package/src/discover.mjs +1 -1
- package/src/init.mjs +6 -1
- package/src/publish.mjs +134 -0
- package/template/scripts/smoke.mjs.tpl +104 -4
- package/template-diy/README.md.tpl +282 -0
- package/template-diy/cordis.patch.yml.tpl +3 -0
- package/template-diy/design.json.tpl +38 -0
- package/template-diy/package.json.tpl +43 -0
- package/template-diy/scripts/smoke.mjs.tpl +232 -0
- package/template-diy/src/client.tsx.tpl +258 -0
- package/template-diy/src/host.ts.tpl +131 -0
- package/template-diy/src/messages.ts.tpl +28 -0
- package/template-diy/src/plugin.test.ts.tpl +359 -0
- package/template-diy/tsconfig.json +20 -0
- package/template-diy/types/globals.d.ts +27 -0
- package/template-diy/vitest.config.ts +14 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterAll, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
const shell = vi.hoisted(() => ({
|
|
7
|
+
enabled: false,
|
|
8
|
+
fixture: undefined as { readonly id: string } | undefined,
|
|
9
|
+
resource: { phase: 'loading' } as {
|
|
10
|
+
readonly phase: string;
|
|
11
|
+
readonly value?: unknown;
|
|
12
|
+
readonly error?: { readonly code: string; readonly message: string };
|
|
13
|
+
},
|
|
14
|
+
readEnabled: undefined as boolean | undefined,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('@dsh-workbench/core/client', () => ({
|
|
18
|
+
kit: {
|
|
19
|
+
hooks: {
|
|
20
|
+
useApiResource: (_route: string, _query: unknown, _refreshToken: number, options?: { readonly enabled?: boolean }) => {
|
|
21
|
+
shell.readEnabled = options?.enabled;
|
|
22
|
+
return shell.resource;
|
|
23
|
+
},
|
|
24
|
+
useFixturePick: () => ({
|
|
25
|
+
enabled: shell.enabled,
|
|
26
|
+
active: shell.fixture?.id ?? null,
|
|
27
|
+
current: shell.fixture,
|
|
28
|
+
pick: () => {},
|
|
29
|
+
nonce: 0,
|
|
30
|
+
index: 0,
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
components: {
|
|
34
|
+
Card: () => null,
|
|
35
|
+
MockStrip: () => null,
|
|
36
|
+
CenteredState: () => null,
|
|
37
|
+
EmptyState: () => null,
|
|
38
|
+
StateBlock: () => null,
|
|
39
|
+
IconPanelScreen: () => null,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
import { apply, inject, name, OVERVIEW_ROUTE, readOverview } from './host';
|
|
45
|
+
import {
|
|
46
|
+
apply as applyClient,
|
|
47
|
+
formatBytes,
|
|
48
|
+
inject as injectClient,
|
|
49
|
+
overviewFixtures,
|
|
50
|
+
overviewStateOf,
|
|
51
|
+
} from './client';
|
|
52
|
+
|
|
53
|
+
interface HostFace {
|
|
54
|
+
readonly manifest: { readonly id: string };
|
|
55
|
+
mount(): { readonly routes: Record<string, (search: URLSearchParams) => Promise<unknown>> };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ClientFace {
|
|
59
|
+
readonly manifest: { readonly id: string };
|
|
60
|
+
readonly card: (props: {
|
|
61
|
+
readonly world: { readonly current: { readonly workspace?: { readonly path: string } | null } };
|
|
62
|
+
readonly refreshToken: number;
|
|
63
|
+
readonly onRefresh: () => void;
|
|
64
|
+
readonly t: (key: string) => string;
|
|
65
|
+
}) => unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface OverviewAnswer {
|
|
69
|
+
readonly ok: boolean;
|
|
70
|
+
readonly path: string;
|
|
71
|
+
readonly files: number;
|
|
72
|
+
readonly dirs: number;
|
|
73
|
+
readonly bytes: number;
|
|
74
|
+
readonly largest: readonly { readonly name: string; readonly bytes: number; readonly mtime: string }[];
|
|
75
|
+
readonly error: { readonly code: string; readonly message: string };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hostFace(): HostFace {
|
|
79
|
+
const faces: unknown[] = [];
|
|
80
|
+
const ctx = {
|
|
81
|
+
get: () => ({
|
|
82
|
+
register: (face: unknown) => {
|
|
83
|
+
faces.push(face);
|
|
84
|
+
return () => {};
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
effect: (effect: () => void | (() => void)) => {
|
|
88
|
+
effect();
|
|
89
|
+
return () => {};
|
|
90
|
+
},
|
|
91
|
+
logger: { warn: () => {} },
|
|
92
|
+
};
|
|
93
|
+
apply(ctx as never);
|
|
94
|
+
return faces[0] as HostFace;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function clientFace(): ClientFace {
|
|
98
|
+
const faces: unknown[] = [];
|
|
99
|
+
const ctx = {
|
|
100
|
+
get: () => ({
|
|
101
|
+
registerUnit: (face: unknown) => {
|
|
102
|
+
faces.push(face);
|
|
103
|
+
return () => {};
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
effect: (effect: () => void | (() => void)) => {
|
|
107
|
+
effect();
|
|
108
|
+
return () => {};
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
applyClient(ctx as never);
|
|
112
|
+
return faces[0] as ClientFace;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function overview(search: Record<string, string>): Promise<unknown> {
|
|
116
|
+
return hostFace().mount().routes[OVERVIEW_ROUTE](new URLSearchParams(search));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function textOf(value: unknown, out: string[] = []): string[] {
|
|
120
|
+
if (typeof value === 'string') {
|
|
121
|
+
out.push(value);
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
if (typeof value === 'number') {
|
|
125
|
+
out.push(String(value));
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
if (typeof value === 'boolean' || value === null || value === undefined) return out;
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
for (const one of value) textOf(one, out);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
if (typeof value === 'object') {
|
|
134
|
+
const record = value as Record<string, unknown>;
|
|
135
|
+
const source = 'type' in record && 'props' in record ? (record.props as Record<string, unknown>) : record;
|
|
136
|
+
for (const one of Object.values(source)) {
|
|
137
|
+
if (typeof one !== 'function') textOf(one, out);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const scratch = mkdtempSync(join(tmpdir(), 'dsh-wb-diy-'));
|
|
144
|
+
afterAll(() => {
|
|
145
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const cardProps = {
|
|
149
|
+
world: { current: { workspace: { path: '/tmp/__UNIT__-workspace' } } },
|
|
150
|
+
refreshToken: 1,
|
|
151
|
+
onRefresh: () => {},
|
|
152
|
+
t: (key: string) => key,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
describe('__NAME__ 的面', () => {
|
|
156
|
+
it('插件名就是包名', () => {
|
|
157
|
+
expect(name).toBe('__NAME__');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('两侧都只注入外壳交出来的那个服务', () => {
|
|
161
|
+
expect(inject).toEqual(['workbenchUnits']);
|
|
162
|
+
expect(injectClient).toEqual(['workbenchUnits']);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('宿主半侧与浏览器半侧都把自己的面注册进外壳', () => {
|
|
166
|
+
expect(hostFace().manifest.id).toBe('__UNIT__');
|
|
167
|
+
expect(clientFace().manifest.id).toBe('__UNIT__');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('宿主侧的只读路由', () => {
|
|
172
|
+
it('路由住在自己那一格的命名空间里,且是个 async handler', () => {
|
|
173
|
+
const routes = hostFace().mount().routes;
|
|
174
|
+
expect(Object.keys(routes)).toEqual([OVERVIEW_ROUTE]);
|
|
175
|
+
expect(OVERVIEW_ROUTE).toBe('__UNIT__/overview');
|
|
176
|
+
expect(typeof routes[OVERVIEW_ROUTE]).toBe('function');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('真读一个目录:文件数 / 目录数 / 总字节 / 最大的几个文件(名字 + 字节 + mtime)', async () => {
|
|
180
|
+
const dir = join(scratch, 'sample');
|
|
181
|
+
mkdirSync(join(dir, 'nested'), { recursive: true });
|
|
182
|
+
writeFileSync(join(dir, 'small.txt'), 'x'.repeat(10));
|
|
183
|
+
writeFileSync(join(dir, 'mid.log'), 'y'.repeat(50));
|
|
184
|
+
writeFileSync(join(dir, 'big.bin'), 'z'.repeat(100));
|
|
185
|
+
|
|
186
|
+
const answer = (await overview({ path: dir, limit: '2' })) as OverviewAnswer;
|
|
187
|
+
expect(answer.ok).toBe(true);
|
|
188
|
+
expect(answer.path).toBe(dir);
|
|
189
|
+
expect(answer.files).toBe(3);
|
|
190
|
+
expect(answer.dirs).toBe(1);
|
|
191
|
+
expect(answer.bytes).toBe(160);
|
|
192
|
+
expect(answer.largest.map((file) => file.name)).toEqual(['big.bin', 'mid.log']);
|
|
193
|
+
expect(answer.largest[0]?.bytes).toBe(100);
|
|
194
|
+
expect(Number.isNaN(Date.parse(answer.largest[0]?.mtime ?? ''))).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('limit 由查询参数管,越界会夹住', async () => {
|
|
198
|
+
const dir = join(scratch, 'sample');
|
|
199
|
+
const answer = (await overview({ path: dir, limit: '0' })) as OverviewAnswer;
|
|
200
|
+
expect(answer.largest).toHaveLength(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('path 缺省时回 ok:false 的 no-path,而不是偷偷读宿主进程的工作目录', async () => {
|
|
204
|
+
const answer = (await overview({})) as OverviewAnswer;
|
|
205
|
+
expect(answer.ok).toBe(false);
|
|
206
|
+
expect(answer.error.code).toBe('no-path');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('读不到时交回 ok:false 的 error,而不是抛出去', async () => {
|
|
210
|
+
const answer = (await overview({ path: join(scratch, 'nope') })) as OverviewAnswer;
|
|
211
|
+
expect(answer.ok).toBe(false);
|
|
212
|
+
expect(answer.error.code).toBe('overview-unreadable');
|
|
213
|
+
expect(answer.error.message.length).toBeGreaterThan(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('readOverview 与路由返回的是同一份读数', () => {
|
|
217
|
+
expect(readOverview(join(scratch, 'sample'), 5).bytes).toBe(160);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('卡片:待机 / 加载 / 就绪 / 就绪但空 / 出错', () => {
|
|
222
|
+
it('没有选中工作区时不发请求,画待机那一屏', () => {
|
|
223
|
+
const face = clientFace();
|
|
224
|
+
shell.enabled = false;
|
|
225
|
+
shell.fixture = undefined;
|
|
226
|
+
shell.resource = { phase: 'idle' };
|
|
227
|
+
const withoutProject = { ...cardProps, world: { current: { workspace: null } } };
|
|
228
|
+
|
|
229
|
+
expect(textOf(face.card(withoutProject)).join(' ')).toContain('__UNIT__.needProject');
|
|
230
|
+
expect(shell.readEnabled).toBe(false);
|
|
231
|
+
|
|
232
|
+
shell.resource = { phase: 'loading' };
|
|
233
|
+
expect(textOf(face.card(cardProps)).join(' ')).not.toContain('__UNIT__.needProject');
|
|
234
|
+
expect(shell.readEnabled).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('没有工作区时夹具照样能演(夹具优先于待机)', () => {
|
|
238
|
+
const face = clientFace();
|
|
239
|
+
shell.enabled = true;
|
|
240
|
+
shell.fixture = overviewFixtures[0];
|
|
241
|
+
shell.resource = { phase: 'idle' };
|
|
242
|
+
const withoutProject = { ...cardProps, world: { current: { workspace: null } } };
|
|
243
|
+
expect(textOf(face.card(withoutProject)).join(' ')).toContain('/mock/workbench');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('四态各画得出来', () => {
|
|
247
|
+
const face = clientFace();
|
|
248
|
+
shell.enabled = false;
|
|
249
|
+
shell.fixture = undefined;
|
|
250
|
+
|
|
251
|
+
shell.resource = { phase: 'loading' };
|
|
252
|
+
expect(textOf(face.card(cardProps)).join(' ')).toContain('__UNIT__.loading');
|
|
253
|
+
|
|
254
|
+
shell.resource = { phase: 'error', error: { code: 'internal', message: 'boom' } };
|
|
255
|
+
expect(textOf(face.card(cardProps)).join(' ')).toContain('internal: boom');
|
|
256
|
+
|
|
257
|
+
shell.resource = {
|
|
258
|
+
phase: 'ready',
|
|
259
|
+
value: {
|
|
260
|
+
ok: true,
|
|
261
|
+
path: '/real/workbench',
|
|
262
|
+
files: 2,
|
|
263
|
+
dirs: 1,
|
|
264
|
+
bytes: 2048,
|
|
265
|
+
largest: [{ name: 'big.bin', bytes: 2048, mtime: '2026-01-02T03:04:05.000Z' }],
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
const ready = textOf(face.card(cardProps)).join(' ');
|
|
269
|
+
expect(ready).toContain('/real/workbench');
|
|
270
|
+
expect(ready).toContain('big.bin');
|
|
271
|
+
expect(ready).toContain('2.0 KB');
|
|
272
|
+
expect(ready).toContain('__UNIT__.fileUnit');
|
|
273
|
+
expect(ready).toContain('__UNIT__.largest');
|
|
274
|
+
|
|
275
|
+
shell.resource = {
|
|
276
|
+
phase: 'ready',
|
|
277
|
+
value: { ok: true, path: '/real/empty', files: 0, dirs: 0, bytes: 0, largest: [] },
|
|
278
|
+
};
|
|
279
|
+
expect(textOf(face.card(cardProps)).join(' ')).toContain('__UNIT__.empty');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('刷新中旧数据还在(不退回加载态)', () => {
|
|
283
|
+
const face = clientFace();
|
|
284
|
+
shell.enabled = false;
|
|
285
|
+
shell.fixture = undefined;
|
|
286
|
+
shell.resource = {
|
|
287
|
+
phase: 'loading',
|
|
288
|
+
value: { ok: true, path: '/real/workbench', files: 1, dirs: 0, bytes: 10, largest: [] },
|
|
289
|
+
};
|
|
290
|
+
const text = textOf(face.card(cardProps)).join(' ');
|
|
291
|
+
expect(text).toContain('/real/workbench');
|
|
292
|
+
expect(text).not.toContain('__UNIT__.loading');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('只有 mock 模式才挂出夹具条', () => {
|
|
296
|
+
const face = clientFace();
|
|
297
|
+
shell.fixture = undefined;
|
|
298
|
+
shell.resource = { phase: 'loading' };
|
|
299
|
+
|
|
300
|
+
shell.enabled = false;
|
|
301
|
+
expect(textOf(face.card(cardProps)).join(' ')).not.toContain('__UNIT__.mockNote');
|
|
302
|
+
|
|
303
|
+
shell.enabled = true;
|
|
304
|
+
const text = textOf(face.card(cardProps)).join(' ');
|
|
305
|
+
expect(text).toContain('__UNIT__.mockNote');
|
|
306
|
+
expect(text).toContain('空目录');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('夹具:?wbMock=1 下三种状态都演得出来', () => {
|
|
311
|
+
it('三个夹具各自解出一个状态(有数据 / 空 / 读不到)', () => {
|
|
312
|
+
expect(overviewFixtures.map((fixture) => fixture.id)).toEqual(['ready', 'empty', 'error']);
|
|
313
|
+
const kinds = overviewFixtures.map(
|
|
314
|
+
(fixture) =>
|
|
315
|
+
overviewStateOf({
|
|
316
|
+
fixture,
|
|
317
|
+
phase: 'error',
|
|
318
|
+
value: undefined,
|
|
319
|
+
error: { code: 'network', message: '夹具优先于真请求' },
|
|
320
|
+
}).kind,
|
|
321
|
+
);
|
|
322
|
+
expect(kinds).toEqual(['ready', 'empty', 'error']);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('夹具优先于真请求:三种都画得出来', () => {
|
|
326
|
+
const face = clientFace();
|
|
327
|
+
shell.enabled = true;
|
|
328
|
+
shell.resource = { phase: 'loading' };
|
|
329
|
+
|
|
330
|
+
shell.fixture = overviewFixtures[0];
|
|
331
|
+
const ready = textOf(face.card(cardProps)).join(' ');
|
|
332
|
+
expect(ready).toContain('/mock/workbench');
|
|
333
|
+
expect(ready).toContain('client.js');
|
|
334
|
+
|
|
335
|
+
shell.fixture = overviewFixtures[1];
|
|
336
|
+
expect(textOf(face.card(cardProps)).join(' ')).toContain('__UNIT__.empty');
|
|
337
|
+
|
|
338
|
+
shell.fixture = overviewFixtures[2];
|
|
339
|
+
expect(textOf(face.card(cardProps)).join(' ')).toContain('EACCES');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('夹具数据自己的形状也有人守', () => {
|
|
343
|
+
const ready = overviewFixtures[0]?.payload;
|
|
344
|
+
expect(ready?.files).toBeGreaterThan(0);
|
|
345
|
+
expect(ready?.dirs).toBeGreaterThan(0);
|
|
346
|
+
expect(ready?.bytes).toBeGreaterThan(0);
|
|
347
|
+
expect(ready?.largest).toHaveLength(3);
|
|
348
|
+
expect(Number.isNaN(Date.parse(ready?.largest[0]?.mtime ?? ''))).toBe(false);
|
|
349
|
+
expect(overviewFixtures[1]?.payload?.files).toBe(0);
|
|
350
|
+
expect(overviewFixtures[1]?.payload?.largest).toHaveLength(0);
|
|
351
|
+
expect(overviewFixtures[2]?.error?.message).toContain('EACCES');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('字节格式化不炸也不乱', () => {
|
|
355
|
+
expect(formatBytes(0)).toBe('0 B');
|
|
356
|
+
expect(formatBytes(2048)).toBe('2.0 KB');
|
|
357
|
+
expect(formatBytes(3145728)).toBe('3.0 MB');
|
|
358
|
+
});
|
|
359
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"jsx": "react",
|
|
8
|
+
"jsxFactory": "h",
|
|
9
|
+
"jsxFragmentFactory": "Fragment",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUnusedLocals": true,
|
|
12
|
+
"noImplicitOverride": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"paths": { "@/modules/__UNIT__/*": ["./src/*"] }
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*.d.ts"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器半侧的运行时垫片。
|
|
3
|
+
*
|
|
4
|
+
* dsh 的客户端模块格式是 `window.__ModuleLoader__.load({ id, factory(require) })`,
|
|
5
|
+
* 所以 `require` 不来自模块系统,而是宿主给的作用域。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* React 19 的 `@types/react` 移除了全局 `JSX` 命名空间,而本工程用的是
|
|
10
|
+
* **经典 JSX 运行时**(`jsxFactory: "h"`):`react/jsx-runtime` 不在浏览器模块表里,
|
|
11
|
+
* 所以不能切到自动运行时。这里把全局命名空间接到 `React.JSX` 上 —— 这是官方迁移说明
|
|
12
|
+
* 给经典运行时的做法,写完 JSX 就有完整类型,产出仍是 `h(...)`。
|
|
13
|
+
*/
|
|
14
|
+
import type * as ReactTypes from 'react';
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
namespace JSX {
|
|
18
|
+
interface Element extends ReactTypes.JSX.Element {}
|
|
19
|
+
interface ElementClass extends ReactTypes.JSX.ElementClass {}
|
|
20
|
+
interface IntrinsicElements extends ReactTypes.JSX.IntrinsicElements {}
|
|
21
|
+
/** 有了它,返回 ReactNode 的组件(含 Fragment)才合法 —— React 19 的类型靠这条判断。 */
|
|
22
|
+
type ElementType = ReactTypes.JSX.ElementType;
|
|
23
|
+
interface IntrinsicAttributes extends ReactTypes.JSX.IntrinsicAttributes {}
|
|
24
|
+
interface ElementAttributesProperty extends ReactTypes.JSX.ElementAttributesProperty {}
|
|
25
|
+
interface ElementChildrenAttribute extends ReactTypes.JSX.ElementChildrenAttribute {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -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
|
+
});
|