@tbox.cn/app-toolkit 0.4.0 → 0.6.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/dist/chunk-WUULQDOV.js +1 -0
- package/dist/contracts-resolver-PS2UH2G6.js +1 -0
- package/dist/index.d.ts +374 -150
- package/dist/index.js +9 -9
- package/package.json +2 -2
- package/src/assembly/provider-catalog.ts +1 -1
- package/src/assembly/resolve-key.ts +75 -0
- package/src/assembly/walk-manifests.ts +138 -85
- package/src/core/contracts-expected.ts +2 -2
- package/src/core/contracts-resolver.ts +12 -1
- package/src/core/module-schema.ts +90 -14
- package/src/dto.ts +80 -33
- package/src/factory.ts +78 -26
- package/src/index.ts +16 -7
- package/src/integrations/mock-bindings.ts +1 -1
- package/src/integrations/module-binding.ts +60 -0
- package/src/integrations/predicate-io.ts +26 -5
- package/src/integrations/predicate.ts +97 -57
- package/src/integrations/prune-bindings.ts +33 -14
- package/src/integrations/write.ts +47 -27
- package/src/views/app.ts +4 -4
- package/src/views/context.ts +15 -14
- package/src/views/integration-schemas.ts +76 -18
- package/src/views/modules.ts +93 -25
- package/src/views/providers.ts +5 -5
- package/src/views/service-detail.ts +1 -1
- package/src/views/service-resolutions.ts +14 -13
- package/tests/credentials-view.test.ts +2 -2
- package/tests/demo-app.ts +11 -6
- package/tests/dev-manifest-catalog.test.ts +14 -14
- package/tests/file-cache.test.ts +5 -5
- package/tests/module-resources.test.ts +148 -0
- package/tests/module-schema.test.ts +120 -20
- package/tests/resolve-key.test.ts +154 -0
- package/tests/vendor-schema-keywords.test.ts +74 -0
- package/tests/views.test.ts +585 -247
- package/tests/write-core.test.ts +117 -0
- package/dist/chunk-KPD3LUH4.js +0 -1
- package/dist/contracts-resolver-E4SECBOG.js +0 -1
- package/src/integrations/domain-binding.ts +0 -81
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveModuleKey, resolveServiceKey } from '../src/assembly/resolve-key.js';
|
|
3
|
+
import { AppToolkitError } from '../src/core/errors.js';
|
|
4
|
+
import type { WalkManifestsResult, WalkModuleInfo } from '../src/assembly/walk-manifests.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* resolve-key(双键寻址 helper——v7 词汇统一):歧义矩阵单测(纯参数式——walk 手工构造)。
|
|
8
|
+
* 语义锁:id 精确优先 / 展示名唯一命中 / 多命中 404 带候选 / 零命中 404 /
|
|
9
|
+
* 供给足迹进服务键集(O6)/ configuredKeys 注入。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function mod(id: string, name?: string): WalkModuleInfo {
|
|
13
|
+
return {
|
|
14
|
+
id,
|
|
15
|
+
dir: `/x/${id}`,
|
|
16
|
+
pkg: `@app/${id}`,
|
|
17
|
+
descriptor: {
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
id,
|
|
20
|
+
...(name !== undefined ? { name } : {}),
|
|
21
|
+
version: '0.1.0',
|
|
22
|
+
kind: 'business',
|
|
23
|
+
risk: { level: 'low', writeBoundary: 'source' },
|
|
24
|
+
distribution: { defaultMode: 'codegen' },
|
|
25
|
+
declare: {
|
|
26
|
+
handlers: [], tools: [], cards: [], routes: [], pages: [], tabs: [],
|
|
27
|
+
providers: { slots: [] }, owns: [], consumes: [], resources: [],
|
|
28
|
+
},
|
|
29
|
+
dependencies: { modules: [] },
|
|
30
|
+
env: [],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function walk(overrides: Partial<WalkManifestsResult> = {}): WalkManifestsResult {
|
|
36
|
+
return {
|
|
37
|
+
modules: [],
|
|
38
|
+
catalog: { providers: {}, services: {}, credentialTypes: {} },
|
|
39
|
+
serviceSchemas: {},
|
|
40
|
+
vocabulary: [],
|
|
41
|
+
slotMeta: {},
|
|
42
|
+
ownership: {},
|
|
43
|
+
declarationIssues: [],
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('resolveModuleKey(模块双键)', () => {
|
|
49
|
+
it('id 精确命中优先(即使展示名域存在同串冲突)', () => {
|
|
50
|
+
const w = walk({
|
|
51
|
+
modules: [mod('module-a', '模块X'), mod('module-b', 'module-a')],
|
|
52
|
+
});
|
|
53
|
+
expect(resolveModuleKey(w, 'module-a').id).toBe('module-a');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('唯一展示名命中 → 返回该模块', () => {
|
|
57
|
+
const w = walk({ modules: [mod('module-a', '停车缴费'), mod('module-b')] });
|
|
58
|
+
expect(resolveModuleKey(w, '停车缴费').id).toBe('module-a');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('展示名缺省模块:name 兜底 = id(可按 id 命中兜底分支)', () => {
|
|
62
|
+
const w = walk({ modules: [mod('module-b')] });
|
|
63
|
+
expect(resolveModuleKey(w, 'module-b').id).toBe('module-b');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('多展示名命中 → 404 MODULE_NOT_FOUND + 候选列表 + 改用 id 指引', () => {
|
|
67
|
+
const w = walk({ modules: [mod('module-a', '同名'), mod('module-b', '同名')] });
|
|
68
|
+
try {
|
|
69
|
+
resolveModuleKey(w, '同名');
|
|
70
|
+
expect.unreachable('应 throw');
|
|
71
|
+
} catch (e) {
|
|
72
|
+
expect(e).toBeInstanceOf(AppToolkitError);
|
|
73
|
+
const err = e as AppToolkitError;
|
|
74
|
+
expect(err.code).toBe('MODULE_NOT_FOUND');
|
|
75
|
+
expect(err.httpStatus).toBe(404);
|
|
76
|
+
expect(err.message).toContain('id=module-a');
|
|
77
|
+
expect(err.message).toContain('id=module-b');
|
|
78
|
+
expect(err.message).toContain('改用模块 id');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('零命中 → 404 MODULE_NOT_FOUND', () => {
|
|
83
|
+
expect(() => resolveModuleKey(walk({ modules: [mod('module-a')] }), 'ghost')).toThrowError(AppToolkitError);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('resolveServiceKey(服务双键)', () => {
|
|
88
|
+
it('词汇键精确命中(vocabulary)', () => {
|
|
89
|
+
const w = walk({ vocabulary: ['parking.query'] });
|
|
90
|
+
expect(resolveServiceKey(w, [], 'parking.query')).toBe('parking.query');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('demand 侧命中(catalog.services)', () => {
|
|
94
|
+
const w = walk({ catalog: { providers: {}, services: { 'auth.x': { optional: true, defaults: [], owners: [] } }, credentialTypes: {} } });
|
|
95
|
+
expect(resolveServiceKey(w, [], 'auth.x')).toBe('auth.x');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('供给足迹命中(纯供给无词汇/demand——O6 多 impl 形态)', () => {
|
|
99
|
+
const w = walk({
|
|
100
|
+
catalog: {
|
|
101
|
+
providers: { mock: { 'mock-p@1': { provider: 'mock', implementation: 'mock-p@1', credentialType: '-', owner: '@app/provider-mock', services: ['parking.query'] } } },
|
|
102
|
+
services: {},
|
|
103
|
+
credentialTypes: {},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
expect(resolveServiceKey(w, [], 'parking.query')).toBe('parking.query');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('configuredKeys 注入命中(已配置未声明槽)', () => {
|
|
110
|
+
expect(resolveServiceKey(walk(), ['legacy.slot'], 'legacy.slot')).toBe('legacy.slot');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('唯一展示名命中(slotMeta 词汇 meta 源)', () => {
|
|
114
|
+
const w = walk({
|
|
115
|
+
vocabulary: ['parking.query', 'parking.payment'],
|
|
116
|
+
slotMeta: { 'parking.query': { name: '停车查询' } },
|
|
117
|
+
});
|
|
118
|
+
expect(resolveServiceKey(w, [], '停车查询')).toBe('parking.query');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('展示名 demand meta 源命中(词汇缺席回落)', () => {
|
|
122
|
+
const w = walk({
|
|
123
|
+
vocabulary: ['auth.x'],
|
|
124
|
+
catalog: { providers: {}, services: { 'auth.x': { optional: true, defaults: [], name: '登录', owners: [] } }, credentialTypes: {} },
|
|
125
|
+
});
|
|
126
|
+
expect(resolveServiceKey(w, [], '登录')).toBe('auth.x');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('多展示名命中 → 404 SERVICE_NOT_FOUND + 候选列表', () => {
|
|
130
|
+
const w = walk({
|
|
131
|
+
vocabulary: ['a.x', 'b.x'],
|
|
132
|
+
slotMeta: { 'a.x': { name: '同名' }, 'b.x': { name: '同名' } },
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
resolveServiceKey(w, [], '同名');
|
|
136
|
+
expect.unreachable('应 throw');
|
|
137
|
+
} catch (e) {
|
|
138
|
+
const err = e as AppToolkitError;
|
|
139
|
+
expect(err.code).toBe('SERVICE_NOT_FOUND');
|
|
140
|
+
expect(err.httpStatus).toBe(404);
|
|
141
|
+
expect(err.message).toContain('a.x');
|
|
142
|
+
expect(err.message).toContain('b.x');
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('零命中 → 404 SERVICE_NOT_FOUND', () => {
|
|
147
|
+
expect(() => resolveServiceKey(walk({ vocabulary: ['a.x'] }), [], 'ghost')).toThrowError(AppToolkitError);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('原型键不放行(hasOwn 判据——"toString"/"constructor" 非自有键)', () => {
|
|
151
|
+
expect(() => resolveServiceKey(walk(), [], 'toString')).toThrowError(AppToolkitError);
|
|
152
|
+
expect(() => resolveServiceKey(walk(), [], 'constructor')).toThrowError(AppToolkitError);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -22,6 +22,10 @@ const SUPPORTED: ReadonlySet<string> = new Set([
|
|
|
22
22
|
'format', 'minLength', 'maxLength', 'additionalProperties', '$schema',
|
|
23
23
|
// 结构性(只读降级 + 值保全——支持集含之,出现不红)
|
|
24
24
|
'oneOf', 'anyOf', '$ref', 'items', 'patternProperties',
|
|
25
|
+
// G 批(v6 全局字段声明):属性级 x-tbox-global 旗标——表单收窄消费(app/module 轴过滤),
|
|
26
|
+
// 渲染器不解释(toolkit buildIntegrationFieldRegistry 收集);Ajv compile 位若未来启用,
|
|
27
|
+
// 支持集须含本键(当前 config schema 零 compile 位——文档纪律见 integrations-v6 Note)
|
|
28
|
+
'x-tbox-global',
|
|
25
29
|
]);
|
|
26
30
|
|
|
27
31
|
/** 递归走已知 subschema 位,收集 schema 节点自身键(properties 值下的键 = 字段名,不属关键词) */
|
|
@@ -111,3 +115,73 @@ describe('vendor schema 关键词矩阵 ⊆ 渲染器支持集(D32 表单引
|
|
|
111
115
|
}
|
|
112
116
|
});
|
|
113
117
|
});
|
|
118
|
+
|
|
119
|
+
describe('G 批 x-tbox-global 标记结构断言(integrations-v6 全局字段声明)', () => {
|
|
120
|
+
function globalKeysOf(rel: string): string[] {
|
|
121
|
+
const doc = JSON.parse(readFileSync(rel, 'utf8')) as {
|
|
122
|
+
properties?: Record<string, { 'x-tbox-global'?: unknown }>;
|
|
123
|
+
};
|
|
124
|
+
return Object.entries(doc.properties ?? {})
|
|
125
|
+
.filter(([, v]) => v && typeof v === 'object' && v['x-tbox-global'] === true)
|
|
126
|
+
.map(([k]) => k);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
it('标记值恒布尔 true 且标记键 ∈ properties(结构形状 tripwire)', () => {
|
|
130
|
+
for (const { pkg, rel } of schemaFiles()) {
|
|
131
|
+
const doc = JSON.parse(readFileSync(rel, 'utf8')) as {
|
|
132
|
+
properties?: Record<string, Record<string, unknown>>;
|
|
133
|
+
};
|
|
134
|
+
for (const [k, sub] of Object.entries(doc.properties ?? {})) {
|
|
135
|
+
if (sub && 'x-tbox-global' in sub) {
|
|
136
|
+
expect(sub['x-tbox-global'], `${pkg} ${k} 标记值恒 true(布尔旗标非枚举位)`).toBe(true);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('mocktest 期望集锁⑥:member/parking/promotion 全局标记键恒等(消费面 S8 互证基线)', () => {
|
|
143
|
+
// 2026-09-20 拆分:config schema 供给面随 provider-mocktest 迁移(键集不变)
|
|
144
|
+
const base = join(fileURLToPath(new URL('../../..', import.meta.url)), 'modules', 'provider-mocktest', 'schemas');
|
|
145
|
+
expect(globalKeysOf(join(base, 'member.config.json')).sort()).toEqual(['cardName', 'initialPoints', 'levelName']);
|
|
146
|
+
expect(globalKeysOf(join(base, 'parking.config.json'))).toEqual(['parkName']);
|
|
147
|
+
expect(globalKeysOf(join(base, 'promotion.config.json')).sort()).toEqual(['activityTitle', 'couponTitle']);
|
|
148
|
+
expect(globalKeysOf(join(base, 'shopping.config.json'))).toEqual([]); // 零声明负例
|
|
149
|
+
expect(globalKeysOf(join(base, 'mall-info.config.json'))).toEqual([]); // 零声明负例
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('素材对账锁:.real-env 全局层键 ⊆ 对应厂商声明并集(mock/mocktest/wanda/joycity 四档)', () => {
|
|
153
|
+
const repoRoot = fileURLToPath(new URL('../../..', import.meta.url));
|
|
154
|
+
const collect = (vendor: string): Set<string> => {
|
|
155
|
+
const out = new Set<string>();
|
|
156
|
+
for (const f of schemaFiles().filter((x) => x.pkg === `provider-${vendor}`)) {
|
|
157
|
+
for (const k of globalKeysOf(f.rel)) out.add(k);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
};
|
|
161
|
+
const globalLayerKeys = (vendor: string): Set<string> => {
|
|
162
|
+
const ig = JSON.parse(readFileSync(join(repoRoot, '.real-env', 'config', vendor, 'integrations.json'), 'utf8')) as {
|
|
163
|
+
config?: Record<string, unknown>;
|
|
164
|
+
configByInstance?: Record<string, Record<string, unknown>>;
|
|
165
|
+
modules?: Record<string, { config?: Record<string, unknown> }>;
|
|
166
|
+
};
|
|
167
|
+
const out = new Set<string>();
|
|
168
|
+
for (const k of Object.keys(ig.config ?? {})) out.add(k);
|
|
169
|
+
for (const layer of Object.values(ig.configByInstance ?? {})) {
|
|
170
|
+
for (const k of Object.keys(layer)) out.add(k);
|
|
171
|
+
}
|
|
172
|
+
for (const node of Object.values(ig.modules ?? {})) {
|
|
173
|
+
for (const k of Object.keys(node.config ?? {})) out.add(k);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
};
|
|
177
|
+
// mock 保留 = 空集 tripwire(公开包 0.5.0 零 schema 后任何 config 键混回 mock 档即红);
|
|
178
|
+
// mocktest 承接 schema 供给面并续接对账
|
|
179
|
+
for (const vendor of ['mock', 'mocktest', 'wanda', 'joycity']) {
|
|
180
|
+
const declared = collect(vendor);
|
|
181
|
+
const used = globalLayerKeys(vendor);
|
|
182
|
+
for (const k of used) {
|
|
183
|
+
expect(declared.has(k), `${vendor} 档全局层键 "${k}" ∉ schema x-tbox-global 声明并集——补标记或改素材(对账锁)`).toBe(true);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|