@tbox.cn/app-toolkit 0.1.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.md +50 -0
- package/dist/chunk-VV3ERZXS.js +1 -0
- package/dist/contracts-resolver-QM4FBQQV.js +1 -0
- package/dist/index.d.ts +1909 -0
- package/dist/index.js +8 -0
- package/package.json +47 -0
- package/src/assembly/app-manifest.ts +132 -0
- package/src/assembly/provider-catalog.ts +167 -0
- package/src/assembly/walk-manifests.ts +461 -0
- package/src/core/contracts-expected.ts +31 -0
- package/src/core/contracts-resolver.ts +274 -0
- package/src/core/credential-format.ts +98 -0
- package/src/core/credential-mask.ts +19 -0
- package/src/core/env-expansion.ts +51 -0
- package/src/core/errors.ts +46 -0
- package/src/core/file-cache.ts +65 -0
- package/src/core/fskit.ts +21 -0
- package/src/core/module-schema.ts +139 -0
- package/src/dto.ts +298 -0
- package/src/factory.ts +297 -0
- package/src/index.ts +137 -0
- package/src/integrations/credentials.ts +153 -0
- package/src/integrations/mock-bindings.ts +55 -0
- package/src/integrations/predicate-io.ts +199 -0
- package/src/integrations/predicate.ts +598 -0
- package/src/integrations/read.ts +53 -0
- package/src/integrations/write.ts +448 -0
- package/src/views/app.ts +28 -0
- package/src/views/context.ts +70 -0
- package/src/views/modules.ts +48 -0
- package/src/views/providers.ts +165 -0
- package/src/views/service-detail.ts +32 -0
- package/src/views/service-resolutions.ts +323 -0
- package/tests/contracts-resolver.test.ts +203 -0
- package/tests/demo-app.ts +146 -0
- package/tests/dev-manifest-catalog.test.ts +114 -0
- package/tests/error-name-safety.test.ts +26 -0
- package/tests/file-cache.test.ts +242 -0
- package/tests/import-layers.test.ts +111 -0
- package/tests/module-schema.test.ts +84 -0
- package/tests/naming-alignment.test.ts +50 -0
- package/tests/views.test.ts +427 -0
- package/tests/write-core.test.ts +188 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +29 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mkdirSync, readFileSync, symlinkSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createAppToolkit, type AppToolkit } from '../src/factory.js';
|
|
6
|
+
import { invalidateContractsResolver } from '../src/core/contracts-resolver.js';
|
|
7
|
+
import { URL_METHOD_MAP } from './naming-alignment.test.js';
|
|
8
|
+
import { createTempApp, writeFakeContractsPackage, linkContracts, writeAppFile, type TempApp } from './demo-app.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* views/工厂测试矩阵(C4——O1-O16 核心族;O 矩阵挂载点 = 本文件 + file-cache.test.ts W3/W7)。
|
|
12
|
+
* 覆盖:O1 integration 逐字等价 / O2 静态零求值键 / O3 resolutions 键集 + required OR /
|
|
13
|
+
* O8 P14 三处等价 / O9 issues 过滤 / O13 门控正反例 + 默认实例委托 / 门控 503 三分支 /
|
|
14
|
+
* N1 映射表激活(13 行 × 工厂方法 typeof)/ 静态端点写位读对偶。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface Harness {
|
|
18
|
+
app: TempApp;
|
|
19
|
+
tk: AppToolkit;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function harnessWithContracts(opts: { version?: string; withStrict?: boolean } = {}): Harness {
|
|
23
|
+
const app = createTempApp('tbox-views-');
|
|
24
|
+
invalidateContractsResolver();
|
|
25
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
26
|
+
writeFakeContractsPackage(store, { version: opts.version ?? '0.9.0', withStrict: opts.withStrict ?? true });
|
|
27
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
28
|
+
// 应用结构:parking 模块(packages/ 约定发现)+ 注册表两实例 + integrations.json
|
|
29
|
+
mkdirDeep(join(app.appDir, 'packages', 'module-parking'));
|
|
30
|
+
writeAppFile(
|
|
31
|
+
app.appDir,
|
|
32
|
+
'packages/module-parking/tbox.module.json',
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
name: 'module-parking',
|
|
36
|
+
version: '0.1.0',
|
|
37
|
+
kind: 'business',
|
|
38
|
+
contributes: {
|
|
39
|
+
services: [{ service: 'parking.query', optional: false, title: '车位查询' }],
|
|
40
|
+
},
|
|
41
|
+
dependencies: { modules: [] },
|
|
42
|
+
env: [],
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
writeAppFile(
|
|
46
|
+
app.appDir,
|
|
47
|
+
'config/integrations.json',
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
provider: 'mock',
|
|
50
|
+
instances: [
|
|
51
|
+
{ id: 'mall-bj-01', name: '北京店' },
|
|
52
|
+
{ id: 'mall-sh-01', name: '上海店' },
|
|
53
|
+
],
|
|
54
|
+
defaultInstanceId: 'mall-bj-01',
|
|
55
|
+
domains: { parking: { provider: 'wanda' } },
|
|
56
|
+
services: {
|
|
57
|
+
'parking.query': { provider: { provider: 'my-custom-api', credentialType: '-' }, implementation: 'my-custom-parking@1' },
|
|
58
|
+
'auth.alipay-login': {},
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
const tk = createAppToolkit(app.appDir);
|
|
63
|
+
return { app, tk };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function joinStore(appDir: string, name: string): string {
|
|
67
|
+
return join(appDir, name);
|
|
68
|
+
}
|
|
69
|
+
function mkdirDeep(dir: string): void {
|
|
70
|
+
mkdirSync(dir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('views/工厂(O 矩阵核心族)', () => {
|
|
74
|
+
let h: Harness;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
h = harnessWithContracts();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('O1 integration 字段逐字等价(文件节点原值——预填真值不变式 D21)', async () => {
|
|
81
|
+
const detail = await h.tk.loadService('parking.query');
|
|
82
|
+
const fileConfig = ((await h.tk.readIntegrationsConfig()) ?? { services: {} }) as {
|
|
83
|
+
services: Record<string, unknown>;
|
|
84
|
+
};
|
|
85
|
+
expect(detail.integration).toEqual(fileConfig.services['parking.query']);
|
|
86
|
+
// root 节点原值(/app 读对偶)
|
|
87
|
+
const appView = await h.tk.loadApp();
|
|
88
|
+
expect(appView.integration?.provider).toBe('mock');
|
|
89
|
+
// 模块域节点原值
|
|
90
|
+
const mod = await h.tk.loadModule('module-parking');
|
|
91
|
+
expect(mod.integration).toEqual({ provider: 'wanda' });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('O2 静态投影零求值键:/app、/modules/:m、/services/:s 结构性无 resolution/issues/contracts', async () => {
|
|
95
|
+
const appView = await h.tk.loadApp();
|
|
96
|
+
expect(Object.keys(appView)).toEqual(['services', 'integration']);
|
|
97
|
+
const mod = await h.tk.loadModule('module-parking');
|
|
98
|
+
expect(Object.keys(mod)).toEqual(['integration', 'services']);
|
|
99
|
+
const svc = await h.tk.loadService('parking.query');
|
|
100
|
+
expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
|
|
101
|
+
// 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('O8 P14 三处等价:map 值 ≡ 单体 resolution 摘要基(单源函数结构保证)', async () => {
|
|
105
|
+
const batch = await h.tk.loadServiceResolutions();
|
|
106
|
+
const single = await h.tk.loadServiceResolution('parking.query');
|
|
107
|
+
const entry = batch.resolutions['parking.query'];
|
|
108
|
+
// extends 结构:单体 = 摘要基 + instances/inheritance
|
|
109
|
+
expect(single.resolution.status).toBe(entry.status);
|
|
110
|
+
expect(single.resolution.provider).toBe(entry.provider);
|
|
111
|
+
expect(single.resolution.supplied).toBe(entry.supplied);
|
|
112
|
+
expect(single.resolution.module).toBe(entry.module);
|
|
113
|
+
expect(single.resolution.required).toBe(entry.required);
|
|
114
|
+
expect(single.resolution.instances).toBeInstanceOf(Array);
|
|
115
|
+
expect(single.resolution.inheritance).toHaveLength(4);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('O3 键集 = 服务词汇 ∪ 已配置;required echo(OR 聚合 + 平台 false)', async () => {
|
|
119
|
+
// vocabulary 空(无契约包 fixture)→ 键集 = 已配置(parking.query + auth.alipay-login)
|
|
120
|
+
const batch = await h.tk.loadServiceResolutions();
|
|
121
|
+
expect(Object.keys(batch.resolutions).sort()).toEqual(['auth.alipay-login', 'parking.query']);
|
|
122
|
+
// parking.query 由 module-parking demand required:0 → required true
|
|
123
|
+
expect(batch.resolutions['parking.query'].required).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('O9 issues 过滤:单体 = service 过滤子集;集合恒全量', async () => {
|
|
127
|
+
const batch = await h.tk.loadServiceResolutions();
|
|
128
|
+
const single = await h.tk.loadServiceResolution('auth.alipay-login');
|
|
129
|
+
expect(single.issues.every((i) => i.service === 'auth.alipay-login')).toBe(true);
|
|
130
|
+
expect(batch.issues.length).toBeGreaterThanOrEqual(single.issues.length);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('O13 门控正反例:<0.9 求值 503 三分支 + 静态 200;多实例无显式默认 → 通配口径', async () => {
|
|
134
|
+
// 正例:≥0.9 求值可用(harness 默认)
|
|
135
|
+
const batch = await h.tk.loadServiceResolutions();
|
|
136
|
+
expect(batch.contracts.strictValidation).toBe(true);
|
|
137
|
+
h.app.dispose();
|
|
138
|
+
|
|
139
|
+
// 反例:缺 zod 面(0.8.2)→ 求值 throw 503 + message 升级指引;静态照常
|
|
140
|
+
const low = harnessWithContracts({ version: '0.8.2', withStrict: false });
|
|
141
|
+
await expect(low.tk.loadServiceResolutions()).rejects.toMatchObject({
|
|
142
|
+
code: 'CONTRACTS_NOT_RESOLVED',
|
|
143
|
+
httpStatus: 503,
|
|
144
|
+
});
|
|
145
|
+
await expect(low.tk.loadServiceResolutions()).rejects.toThrow(/升级应用 @tbox.cn\/app-contracts/);
|
|
146
|
+
// 静态永不 503
|
|
147
|
+
const appView = await low.tk.loadApp();
|
|
148
|
+
expect(appView.services).toBeInstanceOf(Array);
|
|
149
|
+
// 写端点同样门控
|
|
150
|
+
await expect(
|
|
151
|
+
low.tk.writeServiceIntegration('parking.query', { provider: 'mock' }),
|
|
152
|
+
).rejects.toMatchObject({ code: 'CONTRACTS_NOT_RESOLVED', httpStatus: 503 });
|
|
153
|
+
low.app.dispose();
|
|
154
|
+
|
|
155
|
+
// 默认实例委托:无 defaultInstanceId(多实例)→ '*' 通配口径(normalizeInstances null → '*')
|
|
156
|
+
const wild = harnessWithContracts();
|
|
157
|
+
writeAppFile(
|
|
158
|
+
wild.app.appDir,
|
|
159
|
+
'config/integrations.json',
|
|
160
|
+
JSON.stringify({
|
|
161
|
+
provider: 'mock',
|
|
162
|
+
instances: [
|
|
163
|
+
{ id: 'mall-bj-01', name: '北京店' },
|
|
164
|
+
{ id: 'mall-sh-01', name: '上海店' },
|
|
165
|
+
],
|
|
166
|
+
services: { 'auth.alipay-login': {} },
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
const single = await wild.tk.loadServiceResolution('auth.alipay-login');
|
|
170
|
+
// 通配实例枚举条目在场(无注册表无节点 → instances [];显式空槽 = 通配激活)
|
|
171
|
+
expect(single.resolution.status).toBe('service-not-configured');
|
|
172
|
+
wild.app.dispose();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('N1 命名对齐:13 行映射表 × 工厂方法 typeof(L1 改名漂移即红)', async () => {
|
|
176
|
+
for (const row of URL_METHOD_MAP) {
|
|
177
|
+
const fn = (h.tk as unknown as Record<string, unknown>)[row.method];
|
|
178
|
+
expect(typeof fn, row.method).toBe('function');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('写核心 PUT 全量替换:GET 预填原始节点 → PUT 回整节点(instances map 随节点)+ restartScheduled', async () => {
|
|
183
|
+
let appliedCalls = 0;
|
|
184
|
+
const tk2 = createAppToolkit(h.app.appDir, { onApplied: () => (appliedCalls += 1) });
|
|
185
|
+
const svc = await tk2.loadService('parking.query');
|
|
186
|
+
const node = { ...(svc.integration ?? {}), credentialRef: 'secret://wanda-mall-bj-01-parking' };
|
|
187
|
+
const result = await tk2.writeServiceIntegration('parking.query', node);
|
|
188
|
+
expect(result.written).toBe(true);
|
|
189
|
+
expect(result.restartScheduled).toBe(true);
|
|
190
|
+
expect(appliedCalls).toBe(1);
|
|
191
|
+
// 回读逐字等价(GET = PUT = 落盘物三位一体)
|
|
192
|
+
const reread = await tk2.loadService('parking.query');
|
|
193
|
+
expect(reread.integration).toEqual(node);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('no-diff 深度等价 → written:false + restartScheduled:false;dryRun files 恒 []', async () => {
|
|
197
|
+
const svc = await h.tk.loadService('parking.query');
|
|
198
|
+
const result = await h.tk.writeServiceIntegration('parking.query', svc.integration ?? {});
|
|
199
|
+
expect(result.written).toBe(false);
|
|
200
|
+
expect(result.restartScheduled).toBe(false);
|
|
201
|
+
// dryRun:复读管线不落盘
|
|
202
|
+
const dry = await h.tk.writeServiceIntegration('parking.query', { provider: 'mock' }, { dryRun: true });
|
|
203
|
+
expect(dry.written).toBe(false);
|
|
204
|
+
expect(dry.files).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('写时自动初始化(R13 语义翻转提示)+ N1 清域(模块 {} 删 domains 键)', async () => {
|
|
208
|
+
h.app.dispose();
|
|
209
|
+
const fresh = createTempApp('tbox-views-fresh-');
|
|
210
|
+
invalidateContractsResolver();
|
|
211
|
+
const store = joinStore(fresh.appDir, 'contracts');
|
|
212
|
+
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
213
|
+
linkContracts(fresh.appDir, store, { subdir: 'apps/server' });
|
|
214
|
+
const tk = createAppToolkit(fresh.appDir);
|
|
215
|
+
// 缺席文件首写 → 骨架物化 + R13 warning
|
|
216
|
+
const r = await tk.writeServiceIntegration('auth.alipay-login', {});
|
|
217
|
+
expect(r.written).toBe(true);
|
|
218
|
+
expect(r.issues.some((i) => i.code === 'SKELETON_MATERIALIZED')).toBe(true);
|
|
219
|
+
fresh.dispose();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('O15 安全钉子:configSchema 逃逸三用例 400 + 合法相对路径 200(H9/FX-2a)', async () => {
|
|
223
|
+
const writeConfigWithSchema = (ref: string): void => {
|
|
224
|
+
writeAppFile(
|
|
225
|
+
h.app.appDir,
|
|
226
|
+
'config/integrations.json',
|
|
227
|
+
JSON.stringify({
|
|
228
|
+
provider: 'mock',
|
|
229
|
+
instances: [{ id: 'mall-bj-01', name: '北京店' }],
|
|
230
|
+
defaultInstanceId: 'mall-bj-01',
|
|
231
|
+
services: {
|
|
232
|
+
'parking.query': {
|
|
233
|
+
provider: { provider: 'my-custom-api', credentialType: '-', configSchema: ref },
|
|
234
|
+
implementation: 'my-custom-parking@1',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
// ① 同前缀兄弟目录(原 startsWith 无分隔符边界绕过点——schemas-evil)
|
|
241
|
+
writeConfigWithSchema('config/schemas-evil/x.json');
|
|
242
|
+
await expect(h.tk.loadProviderService('my-custom-api', 'parking.query')).rejects.toMatchObject({
|
|
243
|
+
code: 'VALIDATION_FAILED',
|
|
244
|
+
httpStatus: 400,
|
|
245
|
+
});
|
|
246
|
+
// ② 绝对路径
|
|
247
|
+
writeConfigWithSchema('/etc/passwd');
|
|
248
|
+
await expect(h.tk.loadProviderService('my-custom-api', 'parking.query')).rejects.toMatchObject({
|
|
249
|
+
code: 'VALIDATION_FAILED',
|
|
250
|
+
httpStatus: 400,
|
|
251
|
+
});
|
|
252
|
+
// ③ symlink 逃逸(realpath 复核——凭据 JSON 当 schema 回显防线)
|
|
253
|
+
writeAppFile(h.app.appDir, 'config/credentials/secret.json', JSON.stringify({ type: 'api-key', apiKey: 'REAL-SECRET' }));
|
|
254
|
+
mkdirSync(join(h.app.appDir, 'config', 'schemas'), { recursive: true });
|
|
255
|
+
symlinkSync(join(h.app.appDir, 'config', 'credentials', 'secret.json'), join(h.app.appDir, 'config', 'schemas', 'link.json'));
|
|
256
|
+
writeConfigWithSchema('config/schemas/link.json');
|
|
257
|
+
await expect(h.tk.loadProviderService('my-custom-api', 'parking.query')).rejects.toMatchObject({
|
|
258
|
+
code: 'VALIDATION_FAILED',
|
|
259
|
+
httpStatus: 400,
|
|
260
|
+
});
|
|
261
|
+
// ④ 合法相对路径 → 正常返回 schema;缺席文件 → null(非法不误伤)
|
|
262
|
+
writeAppFile(h.app.appDir, 'config/schemas/ok.json', JSON.stringify({ type: 'object', properties: { baseUrl: { type: 'string' } } }));
|
|
263
|
+
writeConfigWithSchema('config/schemas/ok.json');
|
|
264
|
+
const detail = await h.tk.loadProviderService('my-custom-api', 'parking.query');
|
|
265
|
+
expect(detail.integrationSchemas.configSchema).toEqual({ type: 'object', properties: { baseUrl: { type: 'string' } } });
|
|
266
|
+
writeConfigWithSchema('config/schemas/absent.json');
|
|
267
|
+
const absent = await h.tk.loadProviderService('my-custom-api', 'parking.query');
|
|
268
|
+
expect(absent.integrationSchemas.configSchema).toBeNull();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('O4 effective/credential 填充 + 掩码零真值(FX-2c):effective 投影 + layer + CredentialEcho + inheritance 默认实例', async () => {
|
|
272
|
+
const app = createTempApp('tbox-o4-');
|
|
273
|
+
invalidateContractsResolver();
|
|
274
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
275
|
+
writeFakeContractsPackage(store, {
|
|
276
|
+
version: '0.9.0',
|
|
277
|
+
resolveBody: [
|
|
278
|
+
'const slot = integrations && integrations.services && integrations.services[service];',
|
|
279
|
+
'if (!slot) return { status: "service-not-configured" };',
|
|
280
|
+
'const prov = typeof slot.provider === "string" ? slot.provider : slot.provider && slot.provider.provider;',
|
|
281
|
+
'if (!prov) return { status: "service-not-configured" };',
|
|
282
|
+
'return { status: "ok", effective: { provider: prov, implementation: slot.implementation || "impl@1", config: slot.config || {}, ...(slot.credentialRef ? { credentialRef: slot.credentialRef } : {}) } };',
|
|
283
|
+
].join(' '),
|
|
284
|
+
});
|
|
285
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
286
|
+
writeAppFile(
|
|
287
|
+
app.appDir,
|
|
288
|
+
'config/integrations.json',
|
|
289
|
+
JSON.stringify({
|
|
290
|
+
instances: [{ id: 'mall-1', name: '一店' }],
|
|
291
|
+
defaultInstanceId: 'mall-1',
|
|
292
|
+
services: {
|
|
293
|
+
'parking.query': {
|
|
294
|
+
provider: 'wanda',
|
|
295
|
+
implementation: 'wanda-parking@1',
|
|
296
|
+
credentialRef: 'secret://wanda-parking.query',
|
|
297
|
+
config: { baseUrl: 'https://api.example' },
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
writeAppFile(
|
|
303
|
+
app.appDir,
|
|
304
|
+
'config/credentials/wanda-parking.query.json',
|
|
305
|
+
JSON.stringify({ type: 'wanda-c-signed-v1', appKey: 'SECRET-APP-KEY', appSecret: 'SECRET-APP-SECRET' }),
|
|
306
|
+
);
|
|
307
|
+
const tk = createAppToolkit(app.appDir);
|
|
308
|
+
const detail = await tk.loadServiceResolution('parking.query');
|
|
309
|
+
const inst = detail.resolution.instances.find((i) => i.instanceId === 'mall-1');
|
|
310
|
+
expect(inst?.status).toBe('ok');
|
|
311
|
+
expect(inst?.effective).toEqual({
|
|
312
|
+
provider: 'wanda',
|
|
313
|
+
implementation: 'wanda-parking@1',
|
|
314
|
+
config: { baseUrl: 'https://api.example' },
|
|
315
|
+
credentialRef: 'secret://wanda-parking.query',
|
|
316
|
+
layer: 'service',
|
|
317
|
+
});
|
|
318
|
+
expect(inst?.credential).toEqual({
|
|
319
|
+
type: 'wanda-c-signed-v1',
|
|
320
|
+
ref: 'secret://wanda-parking.query',
|
|
321
|
+
file: 'config/credentials/wanda-parking.query.json',
|
|
322
|
+
masked: { appKey: 'SE****', appSecret: 'SE****' },
|
|
323
|
+
});
|
|
324
|
+
// O4 零真值:序列化后 grep 断言
|
|
325
|
+
const serialized = JSON.stringify(detail);
|
|
326
|
+
expect(serialized).not.toContain('SECRET-APP-KEY');
|
|
327
|
+
expect(serialized).not.toContain('SECRET-APP-SECRET');
|
|
328
|
+
// inheritance 实例层 = 默认实例口径(FX-2c):无实例级声明 → 继承位(provider 缺席)
|
|
329
|
+
expect(detail.resolution.inheritance.find((l) => l.layer === 'instance')).toEqual({ layer: 'instance' });
|
|
330
|
+
app.dispose();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('O6/O7/O10/O14(FX-3 补全)', () => {
|
|
335
|
+
it('O6 供给 implementations 数组保真(同厂商多 impl 声明序——H1 尊重 TIER0 数组真相)', async () => {
|
|
336
|
+
const app = createTempApp('tbox-o6-');
|
|
337
|
+
invalidateContractsResolver();
|
|
338
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
339
|
+
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
340
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
341
|
+
writeAppFile(app.appDir, 'packages/provider-multi/package.json', JSON.stringify({ name: '@app/provider-multi', version: '0.1.0' }));
|
|
342
|
+
writeAppFile(
|
|
343
|
+
app.appDir,
|
|
344
|
+
'packages/provider-multi/tbox.module.json',
|
|
345
|
+
JSON.stringify({
|
|
346
|
+
schemaVersion: 1,
|
|
347
|
+
name: 'provider-multi',
|
|
348
|
+
contributes: {
|
|
349
|
+
providers: {
|
|
350
|
+
slots: [
|
|
351
|
+
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
352
|
+
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify({ type: 'object', properties: { baseUrl: { type: 'string' } } }));
|
|
359
|
+
writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify({ type: 'object', properties: { endpoint: { type: 'string' } } }));
|
|
360
|
+
const tk = createAppToolkit(app.appDir);
|
|
361
|
+
const detail = await tk.loadProvider('joycity');
|
|
362
|
+
expect(detail.services['parking.query']?.implementations).toEqual([
|
|
363
|
+
{ implementation: 'joycity-parking@1', credentialType: 'joycity-c' },
|
|
364
|
+
{ implementation: 'joycity-parking@2', credentialType: 'joycity-c' },
|
|
365
|
+
]);
|
|
366
|
+
// O7 组合单体:多 impl → implementations.length > 1 + schema 取默认 impl(声明序首个)
|
|
367
|
+
const combo = await tk.loadProviderService('joycity', 'parking.query');
|
|
368
|
+
expect(combo.implementations).toHaveLength(2);
|
|
369
|
+
expect(combo.integrationSchemas.configSchema).toEqual({ type: 'object', properties: { baseUrl: { type: 'string' } } });
|
|
370
|
+
expect(combo.integrationSchemas.credentialSchema).toBeNull(); // 供给在场未声明 credentialSchema → null(非 404)
|
|
371
|
+
app.dispose();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('O10 statusMessage 逐实例直投(binding-unresolved 不吞不并)', async () => {
|
|
375
|
+
const app = createTempApp('tbox-o10-');
|
|
376
|
+
invalidateContractsResolver();
|
|
377
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
378
|
+
writeFakeContractsPackage(store, {
|
|
379
|
+
version: '0.9.0',
|
|
380
|
+
resolveBody: [
|
|
381
|
+
'const slot = integrations && integrations.services && integrations.services[service];',
|
|
382
|
+
'if (!slot) return { status: "service-not-configured" };',
|
|
383
|
+
'if (instanceId === "mall-b") return { status: "binding-unresolved", message: service + "@" + instanceId + ":多 impl 需显式钉版(无派生)" };',
|
|
384
|
+
'const prov = typeof slot.provider === "string" ? slot.provider : slot.provider && slot.provider.provider;',
|
|
385
|
+
'if (!prov) return { status: "service-not-configured" };',
|
|
386
|
+
'return { status: "ok", effective: { provider: prov, implementation: "impl@1", config: {} } };',
|
|
387
|
+
].join(' '),
|
|
388
|
+
});
|
|
389
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
390
|
+
writeAppFile(
|
|
391
|
+
app.appDir,
|
|
392
|
+
'config/integrations.json',
|
|
393
|
+
JSON.stringify({
|
|
394
|
+
instances: [{ id: 'mall-a' }, { id: 'mall-b' }],
|
|
395
|
+
defaultInstanceId: 'mall-a',
|
|
396
|
+
services: { 'parking.query': { provider: 'mock' } },
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
const tk = createAppToolkit(app.appDir);
|
|
400
|
+
const detail = await tk.loadServiceResolution('parking.query');
|
|
401
|
+
const byId = Object.fromEntries(detail.resolution.instances.map((i) => [i.instanceId, i]));
|
|
402
|
+
expect(byId['mall-a']?.status).toBe('ok');
|
|
403
|
+
expect(byId['mall-b']?.status).toBe('binding-unresolved');
|
|
404
|
+
expect(byId['mall-b']?.statusMessage).toContain('多 impl 需显式钉版');
|
|
405
|
+
expect(detail.resolution.status).toBe('ok'); // 摘要 = 默认实例(mall-a)口径
|
|
406
|
+
app.dispose();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('O14 谓词 code 枚举 ⊆ api.md §5 清单(additive-only 纪律——文档单源对账)', () => {
|
|
410
|
+
const source = readFileSync(fileURLToPath(new URL('../src/integrations/predicate.ts', import.meta.url)), 'utf8');
|
|
411
|
+
const codes = new Set([...source.matchAll(/code: '([A-Z_]+)'/g)].map((m) => m[1]));
|
|
412
|
+
expect(codes.size).toBeGreaterThan(10); // 自检阈值(扫描面非空)
|
|
413
|
+
// api.md §5 表(谓词 code 枚举单源)+ predicate 头映射表 additive 族
|
|
414
|
+
const ENUM = new Set([
|
|
415
|
+
// api.md §5 表
|
|
416
|
+
'CREDENTIAL_FILE_MISSING', 'CREDENTIAL_TYPE_MISMATCH', 'IMPL_WITHOUT_PROVIDER', 'INSTANCE_SUPPLY_UNMATCHED',
|
|
417
|
+
'SCHEMA_FILE_MISSING', 'DOMAIN_CONFIG_UNUSED', 'DOMAIN_CONFIG_INVALID', 'INSTANCE_KEY_NOT_REGISTERED',
|
|
418
|
+
'REALM_SWITCH_HINT', 'WILDCARD_ALIGNMENT', 'VENDOR_NOT_INSTALLED', 'BINDING_UNRESOLVED', 'DOMAIN_KEY_UNMATCHED',
|
|
419
|
+
'RESOURCE_TYPE_UNKNOWN', 'RESOURCE_NOT_DECLARED',
|
|
420
|
+
// 谓词头映射表 additive(检查族编号映射:#3/#4/#5 内联/#13/v4)
|
|
421
|
+
'SLOT_NOT_DECLARED', 'SLOT_NOT_CONFIGURED', 'INLINE_PROVIDER_INVALID', 'MULTI_INSTANCE_NO_DEFAULT', 'INSTANCE_DISABLED',
|
|
422
|
+
]);
|
|
423
|
+
for (const c of codes) {
|
|
424
|
+
expect(ENUM.has(c), `谓词 code ${c} ∉ api.md §5 清单(additive-only——新增须先登记 api.md §5)`).toBe(true);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createAppToolkit, type AppToolkit } from '../src/factory.js';
|
|
5
|
+
import { AppToolkitError } from '../src/core/errors.js';
|
|
6
|
+
import { createTempApp, writeFakeContractsPackage, linkContracts, writeAppFile, type TempApp } from './demo-app.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 写核心管线矩阵(FX-1c——B1/B2/B3 钉死 + F3 九步定序 + F4 输入扩位)。
|
|
10
|
+
* 评审复现三场景全量入库:T1 writeAppIntegration 节点忽略(B1)、
|
|
11
|
+
* T2 同 node 新凭据丢弃(B2)、T3 dryRun 落凭据文件(B3)。
|
|
12
|
+
* 覆盖:dryRun × no-diff × 凭据 三态组合 / 严格校验失败次序(T5)/
|
|
13
|
+
* 恶意 map 键 400(T6)/ 幂等 overwrote(T7)/ ByInstance ref 回填实际值(T8)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const INTEGRATIONS_FILE = 'config/integrations.json';
|
|
17
|
+
|
|
18
|
+
interface Harness {
|
|
19
|
+
app: TempApp;
|
|
20
|
+
tk: AppToolkit;
|
|
21
|
+
readConfig(): Record<string, unknown>;
|
|
22
|
+
credFile(stem: string): string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function harness(opts: { rejectMarker?: string } = {}): Harness {
|
|
26
|
+
const app = createTempApp('tbox-write-core-');
|
|
27
|
+
const store = join(app.appDir, 'contracts-store');
|
|
28
|
+
writeFakeContractsPackage(store, { version: '0.9.0', withStrict: true, rejectMarker: opts.rejectMarker });
|
|
29
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
30
|
+
writeAppFile(app.appDir, INTEGRATIONS_FILE, JSON.stringify({ services: {} }, null, 2));
|
|
31
|
+
const tk = createAppToolkit(app.appDir);
|
|
32
|
+
return {
|
|
33
|
+
app,
|
|
34
|
+
tk,
|
|
35
|
+
readConfig: () => JSON.parse(readFileSync(join(app.appDir, INTEGRATIONS_FILE), 'utf8')) as Record<string, unknown>,
|
|
36
|
+
credFile: (stem) => join(app.appDir, 'config', 'credentials', `${stem}.json`),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('写核心管线(FX-1c)', () => {
|
|
41
|
+
let h: Harness;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
h = harness();
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
h.app.dispose();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('T1 writeAppIntegration 节点合并(B1 钉死):node 生效 + dryRun 无 dryRun 键注入 + 回读等价', async () => {
|
|
51
|
+
const node = {
|
|
52
|
+
provider: 'mock-x',
|
|
53
|
+
config: { baseUrl: 'https://x.example' },
|
|
54
|
+
instances: [{ id: 'mall-1', name: '一号店' }],
|
|
55
|
+
defaultInstanceId: 'mall-1',
|
|
56
|
+
};
|
|
57
|
+
const r = await h.tk.writeAppIntegration(node as never);
|
|
58
|
+
expect(r.written).toBe(true);
|
|
59
|
+
expect(r.files).toEqual([INTEGRATIONS_FILE]);
|
|
60
|
+
const cfg = h.readConfig();
|
|
61
|
+
expect(cfg.provider).toBe('mock-x');
|
|
62
|
+
expect(cfg.config).toEqual({ baseUrl: 'https://x.example' });
|
|
63
|
+
expect(cfg.instances).toEqual([{ id: 'mall-1', name: '一号店' }]);
|
|
64
|
+
expect(cfg.defaultInstanceId).toBe('mall-1');
|
|
65
|
+
// B1 伴随缺陷钉死:dryRun 键永不进文档
|
|
66
|
+
expect('dryRun' in cfg).toBe(false);
|
|
67
|
+
// 回读逐字等价(GET = PUT = 落盘物)
|
|
68
|
+
const app = await h.tk.loadApp();
|
|
69
|
+
expect(app.integration).toEqual(node);
|
|
70
|
+
// dryRun + 不同节点 → 不落盘
|
|
71
|
+
const r2 = await h.tk.writeAppIntegration({ provider: 'changed' } as never, { dryRun: true });
|
|
72
|
+
expect(r2.written).toBe(false);
|
|
73
|
+
expect(r2.files).toEqual([]);
|
|
74
|
+
expect(h.readConfig().provider).toBe('mock-x');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('T2 同 node 新凭据(B2 钉死):凭据落盘 + written:true + files 仅凭据路径 + integrations 不重写', async () => {
|
|
78
|
+
const node = { provider: 'joycity' };
|
|
79
|
+
const r1 = await h.tk.writeServiceIntegration('parking.query', node, {
|
|
80
|
+
credentials: { type: 'api-key', values: { apiKey: 'FIRST' } },
|
|
81
|
+
});
|
|
82
|
+
expect(r1.written).toBe(true);
|
|
83
|
+
expect(r1.files).toEqual([INTEGRATIONS_FILE, 'config/credentials/joycity-parking.query.json']);
|
|
84
|
+
expect(r1.credentials).toEqual([{ stem: 'joycity-parking.query', ref: 'secret://joycity-parking.query', overwrote: false }]);
|
|
85
|
+
const before = readFileSync(join(h.app.appDir, INTEGRATIONS_FILE), 'utf8');
|
|
86
|
+
// 密钥轮换:同 node + 新凭据值
|
|
87
|
+
const r2 = await h.tk.writeServiceIntegration('parking.query', node, {
|
|
88
|
+
credentials: { type: 'api-key', values: { apiKey: 'ROTATED-SECOND' } },
|
|
89
|
+
});
|
|
90
|
+
expect(r2.written).toBe(true);
|
|
91
|
+
expect(r2.files).toEqual(['config/credentials/joycity-parking.query.json']);
|
|
92
|
+
expect(r2.restartScheduled).toBe(false); // 工厂未注入 onApplied
|
|
93
|
+
expect(r2.credentials[0].overwrote).toBe(true);
|
|
94
|
+
// integrations.json 未被重写(内容逐字不变);凭据文件为新值
|
|
95
|
+
expect(readFileSync(join(h.app.appDir, INTEGRATIONS_FILE), 'utf8')).toBe(before);
|
|
96
|
+
expect(readFileSync(h.credFile('joycity-parking.query'), 'utf8')).toContain('ROTATED-SECOND');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('T3 dryRun + credentials(B3 钉死):零落盘 + credentials[] 预览 + files 恒 []', async () => {
|
|
100
|
+
const r = await h.tk.writeServiceIntegration('parking.query', { provider: 'joycity' }, {
|
|
101
|
+
dryRun: true,
|
|
102
|
+
credentials: { type: 'api-key', values: { apiKey: 'SECRET-DRYRUN' } },
|
|
103
|
+
});
|
|
104
|
+
expect(r.written).toBe(false);
|
|
105
|
+
expect(r.files).toEqual([]);
|
|
106
|
+
expect(r.credentials).toEqual([{ stem: 'joycity-parking.query', ref: 'secret://joycity-parking.query', overwrote: false }]);
|
|
107
|
+
// 凭据目录与 integrations.json 均零变更
|
|
108
|
+
expect(existsSync(h.credFile('joycity-parking.query'))).toBe(false);
|
|
109
|
+
expect((h.readConfig().services as Record<string, unknown>)['parking.query']).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('T4 dryRun 于已写 stem:overwrote:true 预览且磁盘态零变更(不重写)', async () => {
|
|
113
|
+
await h.tk.writeServiceIntegration('parking.query', { provider: 'joycity' }, {
|
|
114
|
+
credentials: { type: 'api-key', values: { apiKey: 'FIRST' } },
|
|
115
|
+
});
|
|
116
|
+
const before = readFileSync(h.credFile('joycity-parking.query'), 'utf8');
|
|
117
|
+
const r = await h.tk.writeServiceIntegration('parking.query', { provider: 'joycity' }, {
|
|
118
|
+
dryRun: true,
|
|
119
|
+
credentials: { type: 'api-key', values: { apiKey: 'SHOULD-NOT-WRITE' } },
|
|
120
|
+
});
|
|
121
|
+
expect(r.credentials[0].overwrote).toBe(true);
|
|
122
|
+
expect(readFileSync(h.credFile('joycity-parking.query'), 'utf8')).toBe(before);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('T5 严格校验失败次序:凭据零落盘 + integrations 零变更(落盘后置于校验通过)', async () => {
|
|
126
|
+
h.app.dispose();
|
|
127
|
+
h = harness({ rejectMarker: '__reject__' });
|
|
128
|
+
await expect(
|
|
129
|
+
h.tk.writeServiceIntegration('parking.query', { provider: 'joycity', __reject__: true } as never, {
|
|
130
|
+
credentials: { type: 'api-key', values: { apiKey: 'X' } },
|
|
131
|
+
}),
|
|
132
|
+
).rejects.toThrow();
|
|
133
|
+
expect(existsSync(h.credFile('joycity-parking.query'))).toBe(false);
|
|
134
|
+
expect((h.readConfig().services as Record<string, unknown>)['parking.query']).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('T6 恶意 map 键 400(F4):instances id / slot instances 键 / credentialsByInstance 键 / ByInstance 键', async () => {
|
|
138
|
+
// root.instances[].id
|
|
139
|
+
await expect(
|
|
140
|
+
h.tk.writeAppIntegration({ instances: [{ id: '../evil', name: 'x' }] } as never),
|
|
141
|
+
).rejects.toMatchObject({ code: 'VALIDATION_FAILED', httpStatus: 400 });
|
|
142
|
+
// slot.instances map 键
|
|
143
|
+
await expect(
|
|
144
|
+
h.tk.writeServiceIntegration('parking.query', { provider: 'mock', instances: { '../evil': {} } } as never),
|
|
145
|
+
).rejects.toMatchObject({ code: 'VALIDATION_FAILED', httpStatus: 400 });
|
|
146
|
+
// credentialsByInstance 选项键
|
|
147
|
+
await expect(
|
|
148
|
+
h.tk.writeServiceIntegration('parking.query', { provider: 'mock' }, {
|
|
149
|
+
credentialsByInstance: { 'a/b': { type: 'api-key', values: { k: 'v' } } },
|
|
150
|
+
}),
|
|
151
|
+
).rejects.toMatchObject({ code: 'VALIDATION_FAILED', httpStatus: 400 });
|
|
152
|
+
// ByInstance 层无通配语义:'*' 亦拒
|
|
153
|
+
await expect(
|
|
154
|
+
h.tk.writeAppIntegration({ credentialRefByInstance: { '*': 'secret://x' } } as never),
|
|
155
|
+
).rejects.toMatchObject({ code: 'VALIDATION_FAILED', httpStatus: 400 });
|
|
156
|
+
// 正例:slot instances 通配键豁免(TIER0 合法态)
|
|
157
|
+
const ok = await h.tk.writeServiceIntegration('parking.query', { provider: 'mock', instances: { '*': {} } });
|
|
158
|
+
expect(ok.written).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('T7 重复同凭据写:overwrote:true 透出幂等(D-B2——written 仍 true)', async () => {
|
|
162
|
+
const cred = { type: 'api-key', values: { apiKey: 'SAME' } };
|
|
163
|
+
const r1 = await h.tk.writeServiceIntegration('parking.query', { provider: 'joycity' }, { credentials: cred });
|
|
164
|
+
expect(r1.credentials[0].overwrote).toBe(false);
|
|
165
|
+
const r2 = await h.tk.writeServiceIntegration('parking.query', { provider: 'joycity' }, { credentials: cred });
|
|
166
|
+
expect(r2.written).toBe(true);
|
|
167
|
+
expect(r2.credentials[0].overwrote).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('T8 applyApp credentialsByInstance ref 回填 = plan 实际 ref(非硬编码格式拼接)', async () => {
|
|
171
|
+
const r = await h.tk.writeAppIntegration({ provider: 'p-x' } as never, {
|
|
172
|
+
credentialsByInstance: { 'mall-1': { type: 'api-key', values: { apiKey: 'V' } } },
|
|
173
|
+
});
|
|
174
|
+
expect(r.written).toBe(true);
|
|
175
|
+
const cfg = h.readConfig();
|
|
176
|
+
expect((cfg.credentialRefByInstance as Record<string, string>)['mall-1']).toBe('secret://p-x-mall-1-global');
|
|
177
|
+
expect(r.credentials).toEqual([{ stem: 'p-x-mall-1-global', ref: 'secret://p-x-mall-1-global', overwrote: false }]);
|
|
178
|
+
expect(existsSync(h.credFile('p-x-mall-1-global'))).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('AppToolkitError 面:code/httpStatus/fields 结构化(消费面属性分发)', () => {
|
|
182
|
+
const err = new AppToolkitError('VALIDATION_FAILED', 400, 'm', ['a.b']);
|
|
183
|
+
expect(err.name).toBe('AppToolkitError');
|
|
184
|
+
expect(err.code).toBe('VALIDATION_FAILED');
|
|
185
|
+
expect(err.httpStatus).toBe(400);
|
|
186
|
+
expect(err.fields).toEqual(['a.b']);
|
|
187
|
+
});
|
|
188
|
+
});
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: {
|
|
5
|
+
index: 'src/index.ts',
|
|
6
|
+
},
|
|
7
|
+
format: ['esm'],
|
|
8
|
+
target: 'node20',
|
|
9
|
+
platform: 'node',
|
|
10
|
+
outDir: 'dist',
|
|
11
|
+
clean: true,
|
|
12
|
+
sourcemap: false,
|
|
13
|
+
// dts worker 以命令行式传参会撞 TS5074(incremental 需单文件 emit 或 tsBuildInfoFile);
|
|
14
|
+
// 声明产物一次性构建无需增量——仅 dts 关闭,typecheck 的增量缓存不受影响。
|
|
15
|
+
dts: { compilerOptions: { incremental: false } },
|
|
16
|
+
// 运行时依赖面 external(contracts 恒 devDep 且限 import type——零 external 条目)
|
|
17
|
+
external: ['zod', 'ajv'],
|
|
18
|
+
// 发布 JS 产物 minify(2026-09-11 沿 app-sdk/cli 先例,commit 72383576):ESM 导出名恒保留
|
|
19
|
+
// (export{mangled as 原名})、d.ts 独立管线原样;标识符 mangle 安全前提与守卫真源见
|
|
20
|
+
// .agents/notes/implemented/process/2026-09-11-publish-dist-minify.md
|
|
21
|
+
// 打包纪律三件套(README 同文):禁 createRequire(import.meta.url);动态 import 路径禁字面量
|
|
22
|
+
// 包名折叠(resolver 运行时计算——agtcodingbox esbuild packages:"external" 已核不进 bundle);
|
|
23
|
+
// 导出 Error 类构造器 this.name 字符串字面量约定(src 态守卫 tests/error-name-safety.test.ts,C4 建立时随 views 落)。
|
|
24
|
+
minify: true,
|
|
25
|
+
esbuildOptions(options) {
|
|
26
|
+
// charset utf8:谓词 message 等中文字面保留(esbuild 缺省 ASCII 转义 \uXXXX——膨胀且不可 grep)
|
|
27
|
+
options.charset = 'utf8';
|
|
28
|
+
},
|
|
29
|
+
});
|