@zhangzhian/dsh-devtools-terminal 0.0.0-stage → 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/LICENSE +21 -0
- package/README.en.md +89 -0
- package/README.md +160 -2
- package/client/chunk-terminal.js +8025 -0
- package/client/client.js +1380 -0
- package/client/client.js.map +1 -0
- package/lib/index.js +11 -0
- package/lib/link-target.js +73 -0
- package/lib/links.js +222 -0
- package/lib/routes.js +115 -0
- package/lib/types/index.d.ts +29 -0
- package/lib/types/link-target.d.ts +50 -0
- package/lib/types/links.d.ts +116 -0
- package/lib/types/routes.d.ts +10 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +62 -3
package/lib/routes.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主侧唯一的一条路由:把 xterm 懒加载块发出去。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要宿主路由
|
|
5
|
+
*
|
|
6
|
+
* 浏览器里 xterm 是几百 KB,不该进启动路径。DSH 的客户端加载器没有代码分割能力
|
|
7
|
+
* (factory 里的 `require` 只认种子模块和已加载的插件行,网络上的块拿不到),
|
|
8
|
+
* 所以懒加载只能自己搭:客户端插一个
|
|
9
|
+
* `<script src="/devtools-terminal/bundle/chunk-terminal.js">`,这个 URL 由宿主发。
|
|
10
|
+
*
|
|
11
|
+
* ## 两个必须守住的点
|
|
12
|
+
*
|
|
13
|
+
* 1. **白名单**:`<name>` 直接拼进文件路径,不校验就是任意文件读取。
|
|
14
|
+
* 2. **Host 围栏**:这个路由不需要会话、不需要工作区,所以任何能访问到端口的
|
|
15
|
+
* 东西都能拿到。绑定在回环上时要求 Host 也是回环名 —— 否则一个恶意网页
|
|
16
|
+
* 可以用 DNS rebinding 把请求打进来读本地文件。绑定 `0.0.0.0` 时用户
|
|
17
|
+
* 已经明确选择对外,放行。
|
|
18
|
+
*
|
|
19
|
+
* (与 `dsh-devtools-workbench` 的同名函数刻意各自实现:这是安全边界,
|
|
20
|
+
* 让它跟着各自的路由表走,比共享一份更不容易被改坏。)
|
|
21
|
+
*/
|
|
22
|
+
import { once } from 'node:events';
|
|
23
|
+
import { createReadStream } from 'node:fs';
|
|
24
|
+
/** 路由前缀(客户端 `chunk-loader.ts` 里的 URL 必须与它一致)。 */
|
|
25
|
+
export const PREFIX = '/devtools-terminal';
|
|
26
|
+
/** 允许发出去的产物名(白名单,**不要**改成读目录)。 */
|
|
27
|
+
const BUNDLE_NAMES = new Set(['chunk-terminal.js']);
|
|
28
|
+
/** 认定为回环的 Host 名。 */
|
|
29
|
+
const LOOPBACK = new Set(['127.0.0.1', 'localhost', '::1', '[::1]', '0.0.0.0']);
|
|
30
|
+
/** 客户端产物目录。 */
|
|
31
|
+
const clientDir = new URL('../client/', import.meta.url);
|
|
32
|
+
/**
|
|
33
|
+
* 注册路由。
|
|
34
|
+
*
|
|
35
|
+
* @param ctx - host 上下文(需要 `ctx.webServer`)。
|
|
36
|
+
* @returns 卸载函数。
|
|
37
|
+
*/
|
|
38
|
+
export function registerRoutes(ctx) {
|
|
39
|
+
return ctx.webServer.register({
|
|
40
|
+
kind: 'prefix',
|
|
41
|
+
path: `${PREFIX}/bundle`,
|
|
42
|
+
handler: (request, response) => {
|
|
43
|
+
serveBundle(ctx, request, response);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 发一个懒加载块。
|
|
49
|
+
*
|
|
50
|
+
* @param ctx - host 上下文。
|
|
51
|
+
* @param request - 请求。
|
|
52
|
+
* @param response - 响应。
|
|
53
|
+
*/
|
|
54
|
+
async function serveBundle(ctx, request, response) {
|
|
55
|
+
if (!trusted(request, ctx)) {
|
|
56
|
+
send(response, 403, 'forbidden');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
60
|
+
send(response, 405, 'method not allowed');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const name = decodeURIComponent((request.url ?? '').split('?')[0] ?? '').slice(`${PREFIX}/bundle/`.length);
|
|
64
|
+
if (!BUNDLE_NAMES.has(name)) {
|
|
65
|
+
// 白名单之外一律 404(**不要**回显 name:那是反射)。
|
|
66
|
+
send(response, 404, 'not found');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const stream = createReadStream(new URL(name, clientDir));
|
|
70
|
+
try {
|
|
71
|
+
await once(stream, 'open');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
send(response, 404, 'not found');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
response.writeHead(200, {
|
|
78
|
+
'content-type': 'text/javascript; charset=utf-8',
|
|
79
|
+
'cache-control': 'no-store',
|
|
80
|
+
'x-content-type-options': 'nosniff',
|
|
81
|
+
});
|
|
82
|
+
stream.pipe(response);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 请求来自可信来源吗。
|
|
86
|
+
*
|
|
87
|
+
* @param request - 请求。
|
|
88
|
+
* @param ctx - host 上下文。
|
|
89
|
+
* @returns 可信时为 `true`。
|
|
90
|
+
*/
|
|
91
|
+
function trusted(request, ctx) {
|
|
92
|
+
if (ctx.webServer.host === '0.0.0.0')
|
|
93
|
+
return true;
|
|
94
|
+
const host = request.headers.host ?? '';
|
|
95
|
+
// Host 可能带端口:`127.0.0.1:3080`。
|
|
96
|
+
const name = host.startsWith('[') ? host.slice(0, host.indexOf(']') + 1) : (host.split(':')[0] ?? '');
|
|
97
|
+
return LOOPBACK.has(name);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 回一个短文本响应。
|
|
101
|
+
*
|
|
102
|
+
* @param response - 响应。
|
|
103
|
+
* @param status - 状态码。
|
|
104
|
+
* @param body - 响应体。
|
|
105
|
+
*/
|
|
106
|
+
function send(response, status, body) {
|
|
107
|
+
if (response.headersSent || response.writableEnded)
|
|
108
|
+
return;
|
|
109
|
+
response.writeHead(status, {
|
|
110
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
111
|
+
'cache-control': 'no-store',
|
|
112
|
+
'x-content-type-options': 'nosniff',
|
|
113
|
+
});
|
|
114
|
+
response.end(body);
|
|
115
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dsh-devtools-terminal` — 右侧栏终端(接管平台内置 `terminal` 页签,带链接能力)。
|
|
3
|
+
*
|
|
4
|
+
* ## 这一行为什么存在
|
|
5
|
+
*
|
|
6
|
+
* 平台的右侧栏终端只做了「PTY → xterm」,**没有 link provider**:输出里的 URL
|
|
7
|
+
* 只是普通文字,点不了。平台把那个终端做成了**私有**懒加载块,第三方插件既拿
|
|
8
|
+
* 不到 xterm 实例、也换不掉它的 UI —— 唯一干净的扩展点是页签类型注册表:
|
|
9
|
+
* 一个 `priority: 'extension'` 的注册可以**顶替**同 kind 的内置类型
|
|
10
|
+
* (`sidebar-right` 的 `tab-registry` 文档原话;仓库里的 `dsh-devtools-files`
|
|
11
|
+
* 已经用同一手法接管了内置 `files`)。
|
|
12
|
+
*
|
|
13
|
+
* 所以我们接管 `terminal` 这个 kind:自己渲染终端(xterm 走懒加载块),
|
|
14
|
+
* 顺手在 xterm 上注册 link provider。PTY 仍然用**平台的** `ctx.webTerminals`,
|
|
15
|
+
* 不自己起进程 —— 否则会和宿主的并发上限、空闲回收、崩溃恢复三套机制打架。
|
|
16
|
+
*
|
|
17
|
+
* ## host 半
|
|
18
|
+
*
|
|
19
|
+
* 只有一条路由:把 `chunk-terminal.js`(xterm + FitAddon)发给浏览器。
|
|
20
|
+
*/
|
|
21
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
22
|
+
/** 依赖门:只有 `webServer`(懒加载块路由)。 */
|
|
23
|
+
export declare const inject: string[];
|
|
24
|
+
/**
|
|
25
|
+
* 挂载 host 半。
|
|
26
|
+
*
|
|
27
|
+
* @param ctx - host 根上下文。
|
|
28
|
+
*/
|
|
29
|
+
export declare function apply(ctx: Context): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** 链接最终落到哪儿。 */
|
|
2
|
+
export type LinkDestination = 'panel' | 'external';
|
|
3
|
+
/**
|
|
4
|
+
* 决定一次打开该落到哪儿。
|
|
5
|
+
*
|
|
6
|
+
* 面板可用就优先面板 —— 这是本插件要的产品行为(用户要的是"在 DSH 插件浏览器
|
|
7
|
+
* 里打开",而不是跳出去)。不可用(profile 关掉了那一行 / 平台版本没有这个页签
|
|
8
|
+
* 类型)才回退系统浏览器。
|
|
9
|
+
*
|
|
10
|
+
* @param panelAvailable - 右栏浏览器页签当前是否可用。
|
|
11
|
+
* @returns 落点。
|
|
12
|
+
*/
|
|
13
|
+
export declare function linkDestination(panelAvailable: boolean): LinkDestination;
|
|
14
|
+
/** 打开链接需要的外部动作(由客户端注入;host 侧测试喂假的)。 */
|
|
15
|
+
export interface LinkOpenPorts {
|
|
16
|
+
/**
|
|
17
|
+
* 右栏浏览器页签现在可用吗。
|
|
18
|
+
*
|
|
19
|
+
* 每次**点击时**都问一次(不是启动时问一次):`browser` 页签类型由平台插件
|
|
20
|
+
* 注册/卸载,和本插件同批加载、顺序不保证;而且 profile 的 patch 可能随时改。
|
|
21
|
+
*
|
|
22
|
+
* @returns 页签类型在注册表里存在时为 true。
|
|
23
|
+
*/
|
|
24
|
+
panelAvailable(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* 在右栏浏览器里打开一个地址。
|
|
27
|
+
*
|
|
28
|
+
* 地址已经过 {@link isHttpUrl} 校验;实现方不必再验。**允许抛错**(页签刚被
|
|
29
|
+
* 关掉、面板没有会话作用域等),抛了就回退 {@link LinkOpenPorts.openExternal}。
|
|
30
|
+
*
|
|
31
|
+
* @param url - http(s) 地址。
|
|
32
|
+
*/
|
|
33
|
+
openInPanel(url: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* 回退:交给系统 / 默认浏览器。
|
|
36
|
+
*
|
|
37
|
+
* @param url - http(s) 地址。
|
|
38
|
+
*/
|
|
39
|
+
openExternal(url: string): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 造一个「打开终端链接」的动作。
|
|
43
|
+
*
|
|
44
|
+
* 语义(与 VSCode 终端一致):只认 http(s);面板优先;面板这次没开成功就回退
|
|
45
|
+
* 系统浏览器 —— **不能因为面板抛错就什么都不做**,那等于点击没反应。
|
|
46
|
+
*
|
|
47
|
+
* @param ports - 外部动作。
|
|
48
|
+
* @returns 打开动作(非 http(s) 静默忽略)。
|
|
49
|
+
*/
|
|
50
|
+
export declare function createLinkOpener(ports: LinkOpenPorts): (url: string) => void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 终端链接的**纯逻辑**:URL 识别、折行拼接、字符→单元格映射。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么单独放这里(不塞进客户端半)
|
|
5
|
+
*
|
|
6
|
+
* 客户端半进浏览器、只能靠 E2E 验;而这几个函数是纯字符串/坐标运算,
|
|
7
|
+
* 放在 host 可编译进 `lib/`,用 `node --test` 就能把边界钉死
|
|
8
|
+
* (折行、宽字符、中文标点、成对括号……)。客户端分块再 import 同一份代码
|
|
9
|
+
* (tsdown 会内联进去)。
|
|
10
|
+
*
|
|
11
|
+
* ## 只依赖「行」的最小结构
|
|
12
|
+
*
|
|
13
|
+
* xterm 的 `IBufferLine` 在这里只以结构类型出现(`isWrapped` / `length` /
|
|
14
|
+
* `getCell`),不 import xterm —— 于是测试可以喂假行,客户端也照常编译。
|
|
15
|
+
*/
|
|
16
|
+
/** 一条逻辑行最多往回/往前拼多少行(防病态 scrollback 把主线程卡住)。 */
|
|
17
|
+
export declare const MAX_LOGICAL_ROWS = 256;
|
|
18
|
+
/** 一行里的一个字符落在哪个单元格(`row`/`col` 为 0 基,`width` 为 1 或 2)。 */
|
|
19
|
+
export interface CellRef {
|
|
20
|
+
readonly row: number;
|
|
21
|
+
readonly col: number;
|
|
22
|
+
readonly width: number;
|
|
23
|
+
}
|
|
24
|
+
/** 一条逻辑行:拼好的文本 + 每个字符的单元格映射。 */
|
|
25
|
+
export interface LogicalLine {
|
|
26
|
+
readonly text: string;
|
|
27
|
+
readonly cells: readonly CellRef[];
|
|
28
|
+
}
|
|
29
|
+
/** 文本里命中的一条 URL(下标是**文本下标**,不是列号)。 */
|
|
30
|
+
export interface UrlMatch {
|
|
31
|
+
readonly url: string;
|
|
32
|
+
readonly start: number;
|
|
33
|
+
readonly end: number;
|
|
34
|
+
}
|
|
35
|
+
/** 按行切开的一段链接范围(`startCol`/`endCol` 0 基,`endCol` 是最后一个被占单元格)。 */
|
|
36
|
+
export interface LinkSegment {
|
|
37
|
+
readonly row: number;
|
|
38
|
+
readonly startCol: number;
|
|
39
|
+
readonly endCol: number;
|
|
40
|
+
}
|
|
41
|
+
/** xterm `IBufferCell` 里我们用到的两个方法。 */
|
|
42
|
+
export interface LineCellLike {
|
|
43
|
+
getWidth(): number;
|
|
44
|
+
getChars(): string;
|
|
45
|
+
}
|
|
46
|
+
/** xterm `IBufferLine` 里我们用到的三个成员。 */
|
|
47
|
+
export interface LineLike {
|
|
48
|
+
readonly isWrapped: boolean;
|
|
49
|
+
readonly length: number;
|
|
50
|
+
getCell(col: number): LineCellLike | undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 判断字符串是不是可打开的 http(s) 地址。
|
|
54
|
+
*
|
|
55
|
+
* @param text - 候选文本。
|
|
56
|
+
* @returns http/https 且能被 URL 解析时为 true。
|
|
57
|
+
*/
|
|
58
|
+
export declare function isHttpUrl(text: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* 剥掉 URL 末尾不属于它的标点。
|
|
61
|
+
*
|
|
62
|
+
* 两种规则:常见中英文句读一律剥;收尾的 `)`/`]`/`}` 只有在**没有配对的开括号**
|
|
63
|
+
* 时才剥(`https://en.wikipedia.org/wiki/Foo_(bar)` 保持完整)。
|
|
64
|
+
*
|
|
65
|
+
* @param url - 正则刚匹配出来的候选。
|
|
66
|
+
* @returns 剥过尾巴的候选(可能为空串)。
|
|
67
|
+
*/
|
|
68
|
+
export declare function trimTrailing(url: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* 找出一段文本里的所有 http(s) URL。
|
|
71
|
+
*
|
|
72
|
+
* @param text - 一条逻辑行的纯文本。
|
|
73
|
+
* @returns 命中列表(`end` 开区间)。
|
|
74
|
+
*/
|
|
75
|
+
export declare function findUrls(text: string): UrlMatch[];
|
|
76
|
+
/**
|
|
77
|
+
* 从请求行往回走到逻辑行开头,再把续行拼起来。
|
|
78
|
+
*
|
|
79
|
+
* 终端里的「一行」在 buffer 里可能是多行:**只有续行**的 `isWrapped` 为 true。
|
|
80
|
+
* URL 被折成两行是常见情况,所以识别必须以逻辑行为单位。
|
|
81
|
+
*
|
|
82
|
+
* @param getLine - `(row0) => 行 | undefined`,row0 为 0 基行号。
|
|
83
|
+
* @param row - 请求行(0 基)。
|
|
84
|
+
* @returns 逻辑行;拿不到行时返回 null。
|
|
85
|
+
*/
|
|
86
|
+
export declare function readLogicalLine(getLine: (row: number) => LineLike | undefined, row: number): LogicalLine | null;
|
|
87
|
+
/**
|
|
88
|
+
* 把「文本下标区间」切成按行分段的单元格区间。
|
|
89
|
+
*
|
|
90
|
+
* xterm 的 ILink 可以跨行,但渲染器按行画下划线;按行切段最省事,每段都带完整
|
|
91
|
+
* URL 文本(点任何一段打开的都是整个 URL)。
|
|
92
|
+
*
|
|
93
|
+
* @param cells - {@link readLogicalLine} 的 cells。
|
|
94
|
+
* @param start - 起始文本下标(含)。
|
|
95
|
+
* @param end - 结束文本下标(不含)。
|
|
96
|
+
* @returns 按行的分段。
|
|
97
|
+
*/
|
|
98
|
+
export declare function segmentsOf(cells: readonly CellRef[], start: number, end: number): LinkSegment[];
|
|
99
|
+
/**
|
|
100
|
+
* 生成请求行上的链接范围(1 基、含首尾,xterm ILink 的坐标约定)。
|
|
101
|
+
*
|
|
102
|
+
* @param getLine - `(row0) => 行 | undefined`。
|
|
103
|
+
* @param row - 请求行(0 基)。
|
|
104
|
+
* @param makeLink - `(url, range) => T`,由调用方决定 ILink 的 activate 实现。
|
|
105
|
+
* @returns 请求行上的链接。
|
|
106
|
+
*/
|
|
107
|
+
export declare function linksForRow<T>(getLine: (row: number) => LineLike | undefined, row: number, makeLink: (url: string, range: {
|
|
108
|
+
start: {
|
|
109
|
+
x: number;
|
|
110
|
+
y: number;
|
|
111
|
+
};
|
|
112
|
+
end: {
|
|
113
|
+
x: number;
|
|
114
|
+
y: number;
|
|
115
|
+
};
|
|
116
|
+
}) => T): T[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
/** 路由前缀(客户端 `chunk-loader.ts` 里的 URL 必须与它一致)。 */
|
|
3
|
+
export declare const PREFIX = "/devtools-terminal";
|
|
4
|
+
/**
|
|
5
|
+
* 注册路由。
|
|
6
|
+
*
|
|
7
|
+
* @param ctx - host 上下文(需要 `ctx.webServer`)。
|
|
8
|
+
* @returns 卸载函数。
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerRoutes(ctx: Context): () => void;
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhangzhian/dsh-devtools-terminal",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "DSH 开发工具:右侧栏终端(Ctrl/Cmd+点击打开输出里的 URL)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"dsh",
|
|
9
|
+
"dsh-plugin",
|
|
10
|
+
"deepseek-harness",
|
|
11
|
+
"terminal"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://codeup.aliyun.com/67617ae720a07de19c7c1633/dsh-plugins.git",
|
|
16
|
+
"directory": "packages/dsh-devtools-terminal"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lib",
|
|
23
|
+
"client",
|
|
24
|
+
"locale"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./lib/types/index.d.ts",
|
|
29
|
+
"default": "./lib/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./client": "./client/client.js",
|
|
32
|
+
"./locale/*.json": "./locale/*.json",
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
37
|
+
"@xterm/xterm": "^5.5.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right": "^0.2.0-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/react": "~18.3.1",
|
|
48
|
+
"@types/react-dom": "~18.3.0",
|
|
49
|
+
"react": "^18.3.1",
|
|
50
|
+
"react-dom": "^18.3.1",
|
|
51
|
+
"tsdown": "^0.23.0",
|
|
52
|
+
"typescript": "^5.9.0"
|
|
53
|
+
},
|
|
54
|
+
"dsh": {
|
|
55
|
+
"client": {
|
|
56
|
+
"platform": "web"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "node -e \"require('node:fs').rmSync('client',{recursive:true,force:true})\" && tsc -p tsconfig.json && tsc -p tsconfig.client.json && tsdown",
|
|
61
|
+
"watch": "tsdown --watch",
|
|
62
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
63
|
+
"test": "pnpm build && tsc -p tsconfig.json && node --test test/*.test.mjs"
|
|
64
|
+
}
|
|
6
65
|
}
|