bk-schema-checker 0.1.0 → 0.1.2
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 +1 -1
- package/package.json +1 -1
- package/src/mock-shim.js +3 -1
- package/src/runtime.js +34 -10
- package/src/ts.js +119 -7
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
请求参数不看 mock 读了什么,而看 services 调用的参数类型:GET 的第二个参数是查询参数,POST、PUT、PATCH、DELETE 的第二个参数是请求体。路径参数来自 Django 的 `<str:system_id>` 这类转换器。
|
|
16
16
|
|
|
17
|
-
后台用 Python 标准库 `ast` 静态读取,包括字段继承、自定义字段子类、`SerializerMethodField` 的返回注解,以及能静态求值的 `choices`。前端 `tsconfig.json` 的 `paths` 用来展开 `@common/const` 这类别名;`export enum`、`export interface`
|
|
17
|
+
后台用 Python 标准库 `ast` 静态读取,包括字段继承、自定义字段子类、`SerializerMethodField` 的返回注解,以及能静态求值的 `choices`。前端 `tsconfig.json` 的 `paths` 用来展开 `@common/const` 这类别名;`export enum`、`export interface`(含 `extends`)、`A & { extra }` 交叉类型和 `` `${Enum}` `` 会展开成具体类型再比较。
|
|
18
18
|
|
|
19
19
|
## 环境要求
|
|
20
20
|
|
package/package.json
CHANGED
package/src/mock-shim.js
CHANGED
|
@@ -23,7 +23,7 @@ export const listResult = (results) => ({
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
export const getRequestParam = (config, key) => {
|
|
26
|
-
const value = config?.params?.[key];
|
|
26
|
+
const value = config?.params?.[key] ?? new URL(config?.url || "", "https://mock.local").searchParams.get(key);
|
|
27
27
|
return value === undefined || value === null ? "" : String(value);
|
|
28
28
|
};
|
|
29
29
|
|
|
@@ -38,3 +38,5 @@ export const paginatedListResult = (results, config) => {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
export const getMethod = (config) => (config?.method || "get").toLowerCase();
|
|
41
|
+
|
|
42
|
+
export const getPathname = (url) => new URL(url || "", "https://mock.local").pathname;
|
package/src/runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
5
|
|
|
@@ -9,12 +10,12 @@ const METHOD_ORDER = ["get", "post", "put", "patch", "delete"];
|
|
|
9
10
|
* 同一个运行时里模块缓存是共享的,后执行的接口能看到前面写入的内存数据。
|
|
10
11
|
*/
|
|
11
12
|
export async function createMockRuntime({ root, mockDir, servicesDir }) {
|
|
12
|
-
const createJiti = await loadCreateJiti(root);
|
|
13
|
+
const createJiti = await loadCreateJiti(root, mockDir);
|
|
13
14
|
const jiti = createJiti(import.meta.url, {
|
|
14
15
|
alias: {
|
|
15
16
|
"@mock": mockDir,
|
|
16
17
|
"@services": servicesDir,
|
|
17
|
-
"bk-local-mock": findMockPackage(root),
|
|
18
|
+
"bk-local-mock": findMockPackage(root, mockDir),
|
|
18
19
|
},
|
|
19
20
|
interopDefault: true,
|
|
20
21
|
fsCache: false,
|
|
@@ -200,7 +201,7 @@ function snapshot(value) {
|
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
|
|
203
|
-
async function loadCreateJiti(root) {
|
|
204
|
+
async function loadCreateJiti(root, mockDir) {
|
|
204
205
|
try {
|
|
205
206
|
const mod = await import("jiti");
|
|
206
207
|
if (typeof mod.createJiti === "function") {
|
|
@@ -209,6 +210,17 @@ async function loadCreateJiti(root) {
|
|
|
209
210
|
} catch {
|
|
210
211
|
// 检查器自己没装 jiti 时,用前端工程里的那一份。
|
|
211
212
|
}
|
|
213
|
+
for (const start of moduleResolveStarts(root, mockDir)) {
|
|
214
|
+
try {
|
|
215
|
+
const resolved = createRequire(start).resolve("jiti");
|
|
216
|
+
const mod = await import(pathToFileURL(resolved).href);
|
|
217
|
+
if (typeof mod.createJiti === "function") {
|
|
218
|
+
return mod.createJiti;
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// 这一层没有安装,继续往上看。
|
|
222
|
+
}
|
|
223
|
+
}
|
|
212
224
|
for (const candidate of jitiCandidates(root)) {
|
|
213
225
|
if (fs.existsSync(candidate)) {
|
|
214
226
|
const mod = await import(pathToFileURL(candidate).href);
|
|
@@ -220,13 +232,25 @@ async function loadCreateJiti(root) {
|
|
|
220
232
|
throw new Error("执行 mock 需要 jiti。请在 frontend 安装依赖,或在 bk-schema-checker 中安装 jiti。");
|
|
221
233
|
}
|
|
222
234
|
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
path.join(
|
|
226
|
-
path.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
function moduleResolveStarts(root, mockDir) {
|
|
236
|
+
return [
|
|
237
|
+
mockDir && path.join(mockDir, "handler.js"),
|
|
238
|
+
root && path.join(root, "package.json"),
|
|
239
|
+
root && path.join(root, "frontend/package.json"),
|
|
240
|
+
path.join(fileDir(), "runtime.js"),
|
|
241
|
+
].filter(Boolean);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 按 Node 解析规则找已安装的 bk-local-mock。
|
|
246
|
+
* 配置文件常放在 frontend/,旧逻辑只认 root/frontend/node_modules,会漏掉当前工程自己的依赖。
|
|
247
|
+
*/
|
|
248
|
+
export function findMockPackage(root, mockDir) {
|
|
249
|
+
for (const start of moduleResolveStarts(root, mockDir)) {
|
|
250
|
+
try {
|
|
251
|
+
return createRequire(start).resolve("bk-local-mock");
|
|
252
|
+
} catch {
|
|
253
|
+
// 这一层没有安装,继续往上看。
|
|
230
254
|
}
|
|
231
255
|
}
|
|
232
256
|
return path.join(fileDir(), "mock-shim.js");
|
package/src/ts.js
CHANGED
|
@@ -6,6 +6,7 @@ import { maskComments, readIdentifier, skipSpace, sliceBalanced, splitTop } from
|
|
|
6
6
|
const HELPERS = new Set([
|
|
7
7
|
"clone",
|
|
8
8
|
"getMethod",
|
|
9
|
+
"getPathname",
|
|
9
10
|
"getRequestParam",
|
|
10
11
|
"listResult",
|
|
11
12
|
"paginatedListResult",
|
|
@@ -480,7 +481,10 @@ function parseTypeExport(source, cursor, mod) {
|
|
|
480
481
|
if (interfaceLike || source[bodyAt] === "{") {
|
|
481
482
|
const open = source.indexOf("{", bodyAt);
|
|
482
483
|
const block = sliceBalanced(source, open);
|
|
483
|
-
|
|
484
|
+
const body = source.slice(open, block.end + 1);
|
|
485
|
+
const parents = interfaceLike ? parseInterfaceExtends(source, bodyAt, open) : [];
|
|
486
|
+
const composed = parents.length ? `${parents.join(" & ")} & ${body}` : body;
|
|
487
|
+
mod.types.set(ident.name, { kind: "source", source: composed });
|
|
484
488
|
return block.end + 1;
|
|
485
489
|
}
|
|
486
490
|
if (source[bodyAt] === "=") {
|
|
@@ -492,6 +496,31 @@ function parseTypeExport(source, cursor, mod) {
|
|
|
492
496
|
return skipStatement(source, cursor).end;
|
|
493
497
|
}
|
|
494
498
|
|
|
499
|
+
function parseInterfaceExtends(source, start, open) {
|
|
500
|
+
const text = source.slice(start, open).trim();
|
|
501
|
+
if (!/^extends\b/.test(text)) {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
const names = [];
|
|
505
|
+
let cursor = skipSpace(text, "extends".length);
|
|
506
|
+
while (cursor < text.length) {
|
|
507
|
+
const ident = readIdentifier(text, cursor);
|
|
508
|
+
if (!ident) {
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
names.push(ident.name);
|
|
512
|
+
cursor = skipSpace(text, ident.end);
|
|
513
|
+
if (text[cursor] === "<") {
|
|
514
|
+
const generic = sliceAngles(text, cursor);
|
|
515
|
+
cursor = skipSpace(text, generic ? generic.end + 1 : cursor + 1);
|
|
516
|
+
}
|
|
517
|
+
if (text[cursor] === ",") {
|
|
518
|
+
cursor = skipSpace(text, cursor + 1);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return names;
|
|
522
|
+
}
|
|
523
|
+
|
|
495
524
|
function parseEnumExport(source, cursor, mod) {
|
|
496
525
|
const head = source.slice(cursor, cursor + 24);
|
|
497
526
|
const keyword = /^export\s+const\s+enum\b/.test(head) ? "export const enum" : "export enum";
|
|
@@ -1504,7 +1533,7 @@ function evalCall(node, env) {
|
|
|
1504
1533
|
}
|
|
1505
1534
|
return new Partial("string");
|
|
1506
1535
|
}
|
|
1507
|
-
if (callee?.helper === "getMethod") {
|
|
1536
|
+
if (callee?.helper === "getMethod" || callee?.helper === "getPathname") {
|
|
1508
1537
|
return new Partial("string");
|
|
1509
1538
|
}
|
|
1510
1539
|
if (callee && callee.kind === "function") {
|
|
@@ -1747,6 +1776,17 @@ function skeleton(type) {
|
|
|
1747
1776
|
value.fields = Object.fromEntries(type.props.map((prop) => [prop.name, skeleton(prop.type)]));
|
|
1748
1777
|
return value;
|
|
1749
1778
|
}
|
|
1779
|
+
if (type.kind === "intersection") {
|
|
1780
|
+
const value = new Partial("object");
|
|
1781
|
+
value.fields = {};
|
|
1782
|
+
for (const item of type.types) {
|
|
1783
|
+
const next = skeleton(item);
|
|
1784
|
+
if (next instanceof Partial && next.fields) {
|
|
1785
|
+
Object.assign(value.fields, next.fields);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return value;
|
|
1789
|
+
}
|
|
1750
1790
|
return new Partial();
|
|
1751
1791
|
}
|
|
1752
1792
|
|
|
@@ -1764,7 +1804,7 @@ function parseTypeAt(source, cursor) {
|
|
|
1764
1804
|
}
|
|
1765
1805
|
|
|
1766
1806
|
function parseUnionType(source, cursor) {
|
|
1767
|
-
let current =
|
|
1807
|
+
let current = parseIntersectionType(source, cursor);
|
|
1768
1808
|
const types = [current.type];
|
|
1769
1809
|
let end = current.end;
|
|
1770
1810
|
while (true) {
|
|
@@ -1772,13 +1812,29 @@ function parseUnionType(source, cursor) {
|
|
|
1772
1812
|
if (source[next] !== "|") {
|
|
1773
1813
|
break;
|
|
1774
1814
|
}
|
|
1775
|
-
current =
|
|
1815
|
+
current = parseIntersectionType(source, next + 1);
|
|
1776
1816
|
types.push(current.type);
|
|
1777
1817
|
end = current.end;
|
|
1778
1818
|
}
|
|
1779
1819
|
return { type: types.length === 1 ? types[0] : { kind: "union", types }, end };
|
|
1780
1820
|
}
|
|
1781
1821
|
|
|
1822
|
+
function parseIntersectionType(source, cursor) {
|
|
1823
|
+
let current = parsePrimaryType(source, cursor);
|
|
1824
|
+
const types = [current.type];
|
|
1825
|
+
let end = current.end;
|
|
1826
|
+
while (true) {
|
|
1827
|
+
const next = skipSpace(source, end);
|
|
1828
|
+
if (source[next] !== "&") {
|
|
1829
|
+
break;
|
|
1830
|
+
}
|
|
1831
|
+
current = parsePrimaryType(source, next + 1);
|
|
1832
|
+
types.push(current.type);
|
|
1833
|
+
end = current.end;
|
|
1834
|
+
}
|
|
1835
|
+
return { type: types.length === 1 ? types[0] : { kind: "intersection", types }, end };
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1782
1838
|
function parsePrimaryType(source, cursor) {
|
|
1783
1839
|
const start = skipSpace(source, cursor);
|
|
1784
1840
|
if (source[start] === "{") {
|
|
@@ -2004,16 +2060,19 @@ function splitTypeProps(source) {
|
|
|
2004
2060
|
export function resolveTsType(type, mod, index, seen = new Set()) {
|
|
2005
2061
|
if (!type || type.kind !== "ref" && type.kind !== "index" && type.kind !== "parameters") {
|
|
2006
2062
|
if (type?.kind === "array") {
|
|
2007
|
-
return { kind: "array", item: resolveTsType(type.item, mod, index, seen) };
|
|
2063
|
+
return { kind: "array", item: resolveTsType(type.item, mod, index, new Set(seen)) };
|
|
2008
2064
|
}
|
|
2009
2065
|
if (type?.kind === "union") {
|
|
2010
|
-
return { kind: "union", types: type.types.map((item) => resolveTsType(item, mod, index, seen)) };
|
|
2066
|
+
return { kind: "union", types: type.types.map((item) => resolveTsType(item, mod, index, new Set(seen))) };
|
|
2067
|
+
}
|
|
2068
|
+
if (type?.kind === "intersection") {
|
|
2069
|
+
return mergeResolvedTypes(type.types.map((item) => resolveTsType(item, mod, index, new Set(seen))));
|
|
2011
2070
|
}
|
|
2012
2071
|
if (type?.kind === "object") {
|
|
2013
2072
|
return {
|
|
2014
2073
|
kind: "object",
|
|
2015
2074
|
open: Boolean(type.open),
|
|
2016
|
-
props: type.props.map((prop) => ({ ...prop, type: resolveTsType(prop.type, mod, index, seen) })),
|
|
2075
|
+
props: type.props.map((prop) => ({ ...prop, type: resolveTsType(prop.type, mod, index, new Set(seen)) })),
|
|
2017
2076
|
};
|
|
2018
2077
|
}
|
|
2019
2078
|
return type || { kind: "any" };
|
|
@@ -2044,6 +2103,59 @@ export function resolveTsType(type, mod, index, seen = new Set()) {
|
|
|
2044
2103
|
return resolved;
|
|
2045
2104
|
}
|
|
2046
2105
|
|
|
2106
|
+
function mergeResolvedTypes(types) {
|
|
2107
|
+
const parts = types.flatMap((type) => (type?.kind === "intersection" ? type.types : [type])).filter(Boolean);
|
|
2108
|
+
const unions = parts.filter((type) => type.kind === "union");
|
|
2109
|
+
if (unions.length) {
|
|
2110
|
+
const rest = parts.filter((type) => type.kind !== "union");
|
|
2111
|
+
return {
|
|
2112
|
+
kind: "union",
|
|
2113
|
+
types: unions[0].types.map((item) => mergeResolvedTypes([item, ...unions.slice(1), ...rest])),
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
const objects = parts.filter((type) => type.kind === "object");
|
|
2117
|
+
if (objects.length) {
|
|
2118
|
+
return mergeObjectTypes(objects);
|
|
2119
|
+
}
|
|
2120
|
+
const concrete = parts.filter((type) => type.kind !== "any" && type.kind !== "null");
|
|
2121
|
+
return concrete[0] || parts[0] || { kind: "any" };
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
function mergeObjectTypes(objects) {
|
|
2125
|
+
const props = [];
|
|
2126
|
+
let open = false;
|
|
2127
|
+
for (const object of objects) {
|
|
2128
|
+
open = open || Boolean(object.open);
|
|
2129
|
+
for (const prop of object.props || []) {
|
|
2130
|
+
const index = props.findIndex((item) => item.name === prop.name);
|
|
2131
|
+
if (index === -1) {
|
|
2132
|
+
props.push({ ...prop });
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
const prev = props[index];
|
|
2136
|
+
props[index] = {
|
|
2137
|
+
name: prop.name,
|
|
2138
|
+
optional: Boolean(prev.optional && prop.optional),
|
|
2139
|
+
type: mergePropertyType(prev.type, prop.type),
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return { kind: "object", props, open };
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function mergePropertyType(left, right) {
|
|
2147
|
+
if (!left || left.kind === "any") {
|
|
2148
|
+
return right;
|
|
2149
|
+
}
|
|
2150
|
+
if (!right || right.kind === "any") {
|
|
2151
|
+
return left;
|
|
2152
|
+
}
|
|
2153
|
+
if (left.kind === "object" && right.kind === "object") {
|
|
2154
|
+
return mergeObjectTypes([left, right]);
|
|
2155
|
+
}
|
|
2156
|
+
return right;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2047
2159
|
function lookupType(mod, name, index, stack = new Set()) {
|
|
2048
2160
|
if (!mod || stack.has(mod.file)) {
|
|
2049
2161
|
return null;
|