@nocobase/shared 3.0.0-alpha.1 → 3.0.0-alpha.12
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/lib/index.d.ts +1 -0
- package/lib/index.js +2 -0
- package/lib/ui-operation.d.ts +25 -0
- package/lib/ui-operation.js +114 -0
- package/lib/variable-usage.d.ts +8 -0
- package/lib/variable-usage.js +265 -87
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/ui-operation.ts +99 -0
- package/src/variable-usage.ts +299 -98
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -24,9 +24,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
24
24
|
var src_exports = {};
|
|
25
25
|
module.exports = __toCommonJS(src_exports);
|
|
26
26
|
__reExport(src_exports, require("./getValuesByPath"), module.exports);
|
|
27
|
+
__reExport(src_exports, require("./ui-operation"), module.exports);
|
|
27
28
|
__reExport(src_exports, require("./variable-usage"), module.exports);
|
|
28
29
|
// Annotate the CommonJS export names for ESM import in node:
|
|
29
30
|
0 && (module.exports = {
|
|
30
31
|
...require("./getValuesByPath"),
|
|
32
|
+
...require("./ui-operation"),
|
|
31
33
|
...require("./variable-usage")
|
|
32
34
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
export declare const UI_OPERATION_QUERY_KEY = "_operation_";
|
|
10
|
+
export declare const UI_OPERATION_VERSION = 1;
|
|
11
|
+
export declare const MAX_UI_OPERATION_ENCODED_LENGTH: number;
|
|
12
|
+
export interface UIOperation<TOperationId extends string = string, TParams extends Record<string, unknown> = Record<string, unknown>> {
|
|
13
|
+
v: typeof UI_OPERATION_VERSION;
|
|
14
|
+
operationId: TOperationId;
|
|
15
|
+
params?: TParams;
|
|
16
|
+
}
|
|
17
|
+
export interface UIOperationCodec {
|
|
18
|
+
encode(value: string): string;
|
|
19
|
+
decode(value: string): string | undefined;
|
|
20
|
+
}
|
|
21
|
+
export declare function isUIOperation(value: unknown): value is UIOperation;
|
|
22
|
+
export declare function encodeUIOperation(operation: UIOperation, codec: UIOperationCodec): string;
|
|
23
|
+
export declare function decodeUIOperation(encoded: string, codec: UIOperationCodec): UIOperation | undefined;
|
|
24
|
+
export declare function parseUIOperation(search: string, codec: UIOperationCodec): UIOperation | undefined;
|
|
25
|
+
export declare function removeUIOperation(search: string): string;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
15
|
+
var __export = (target, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
19
|
+
var __copyProps = (to, from, except, desc) => {
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
+
for (let key of __getOwnPropNames(from))
|
|
22
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
23
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
24
|
+
}
|
|
25
|
+
return to;
|
|
26
|
+
};
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var ui_operation_exports = {};
|
|
29
|
+
__export(ui_operation_exports, {
|
|
30
|
+
MAX_UI_OPERATION_ENCODED_LENGTH: () => MAX_UI_OPERATION_ENCODED_LENGTH,
|
|
31
|
+
UI_OPERATION_QUERY_KEY: () => UI_OPERATION_QUERY_KEY,
|
|
32
|
+
UI_OPERATION_VERSION: () => UI_OPERATION_VERSION,
|
|
33
|
+
decodeUIOperation: () => decodeUIOperation,
|
|
34
|
+
encodeUIOperation: () => encodeUIOperation,
|
|
35
|
+
isUIOperation: () => isUIOperation,
|
|
36
|
+
parseUIOperation: () => parseUIOperation,
|
|
37
|
+
removeUIOperation: () => removeUIOperation
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(ui_operation_exports);
|
|
40
|
+
const UI_OPERATION_QUERY_KEY = "_operation_";
|
|
41
|
+
const UI_OPERATION_VERSION = 1;
|
|
42
|
+
const MAX_UI_OPERATION_ENCODED_LENGTH = 8 * 1024;
|
|
43
|
+
function isPlainObject(value) {
|
|
44
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const prototype = Object.getPrototypeOf(value);
|
|
48
|
+
return prototype === Object.prototype || prototype === null;
|
|
49
|
+
}
|
|
50
|
+
__name(isPlainObject, "isPlainObject");
|
|
51
|
+
function isUIOperation(value) {
|
|
52
|
+
if (!isPlainObject(value) || value.v !== UI_OPERATION_VERSION || typeof value.operationId !== "string" || !value.operationId) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return value.params === void 0 || isPlainObject(value.params);
|
|
56
|
+
}
|
|
57
|
+
__name(isUIOperation, "isUIOperation");
|
|
58
|
+
function encodeUIOperation(operation, codec) {
|
|
59
|
+
if (!isUIOperation(operation)) {
|
|
60
|
+
throw new TypeError("Invalid UI operation.");
|
|
61
|
+
}
|
|
62
|
+
const encoded = codec.encode(JSON.stringify(operation));
|
|
63
|
+
if (encoded.length > MAX_UI_OPERATION_ENCODED_LENGTH) {
|
|
64
|
+
throw new RangeError(`UI operation exceeds the ${MAX_UI_OPERATION_ENCODED_LENGTH}-byte encoded length limit.`);
|
|
65
|
+
}
|
|
66
|
+
return encoded;
|
|
67
|
+
}
|
|
68
|
+
__name(encodeUIOperation, "encodeUIOperation");
|
|
69
|
+
function decodeUIOperation(encoded, codec) {
|
|
70
|
+
if (encoded.length > MAX_UI_OPERATION_ENCODED_LENGTH) {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const decoded = codec.decode(encoded);
|
|
75
|
+
if (decoded === void 0) {
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
const parsed = JSON.parse(decoded);
|
|
79
|
+
return isUIOperation(parsed) ? parsed : void 0;
|
|
80
|
+
} catch {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
__name(decodeUIOperation, "decodeUIOperation");
|
|
85
|
+
function parseUIOperation(search, codec) {
|
|
86
|
+
try {
|
|
87
|
+
const values = new URLSearchParams(search).getAll(UI_OPERATION_QUERY_KEY);
|
|
88
|
+
if (values.length !== 1) {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
return decodeUIOperation(values[0], codec);
|
|
92
|
+
} catch {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
__name(parseUIOperation, "parseUIOperation");
|
|
97
|
+
function removeUIOperation(search) {
|
|
98
|
+
const params = new URLSearchParams(search);
|
|
99
|
+
params.delete(UI_OPERATION_QUERY_KEY);
|
|
100
|
+
const remaining = params.toString();
|
|
101
|
+
return remaining ? `?${remaining}` : "";
|
|
102
|
+
}
|
|
103
|
+
__name(removeUIOperation, "removeUIOperation");
|
|
104
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
105
|
+
0 && (module.exports = {
|
|
106
|
+
MAX_UI_OPERATION_ENCODED_LENGTH,
|
|
107
|
+
UI_OPERATION_QUERY_KEY,
|
|
108
|
+
UI_OPERATION_VERSION,
|
|
109
|
+
decodeUIOperation,
|
|
110
|
+
encodeUIOperation,
|
|
111
|
+
isUIOperation,
|
|
112
|
+
parseUIOperation,
|
|
113
|
+
removeUIOperation
|
|
114
|
+
});
|
package/lib/variable-usage.d.ts
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
export type JSONValue = string | {
|
|
10
10
|
[key: string]: JSONValue;
|
|
11
11
|
} | JSONValue[];
|
|
12
|
+
export type VariableUsageExtractionResult = {
|
|
13
|
+
unsupportedDynamicPath: boolean;
|
|
14
|
+
usage: Record<string, string[]>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* 提取模板中的 ctx 使用情况。该函数只做静态扫描,不执行表达式。
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractVariableUsage(template: JSONValue): VariableUsageExtractionResult;
|
|
12
20
|
/**
|
|
13
21
|
* 提取模板中使用到的 ctx 顶层变量名集合。
|
|
14
22
|
* - 支持点语法与顶层括号变量:ctx.user / ctx["user"]
|
package/lib/variable-usage.js
CHANGED
|
@@ -28,111 +28,289 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
28
28
|
var variable_usage_exports = {};
|
|
29
29
|
__export(variable_usage_exports, {
|
|
30
30
|
extractUsedVariableNames: () => extractUsedVariableNames,
|
|
31
|
-
extractUsedVariablePaths: () => extractUsedVariablePaths
|
|
31
|
+
extractUsedVariablePaths: () => extractUsedVariablePaths,
|
|
32
|
+
extractVariableUsage: () => extractVariableUsage
|
|
32
33
|
});
|
|
33
34
|
module.exports = __toCommonJS(variable_usage_exports);
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
function isObject(value) {
|
|
36
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
__name(isObject, "isObject");
|
|
39
|
+
function isIdentifierStart(char) {
|
|
40
|
+
return !!char && /[a-zA-Z_$]/.test(char);
|
|
41
|
+
}
|
|
42
|
+
__name(isIdentifierStart, "isIdentifierStart");
|
|
43
|
+
function isIdentifierPart(char) {
|
|
44
|
+
return !!char && /[a-zA-Z0-9_$]/.test(char);
|
|
45
|
+
}
|
|
46
|
+
__name(isIdentifierPart, "isIdentifierPart");
|
|
47
|
+
function skipSpaces(input, index) {
|
|
48
|
+
let next = index;
|
|
49
|
+
while (/\s/.test(input[next] || "")) next += 1;
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
52
|
+
__name(skipSpaces, "skipSpaces");
|
|
53
|
+
function skipPostfix(input, index) {
|
|
54
|
+
let next = skipSpaces(input, index);
|
|
55
|
+
while (input[next] === "!") {
|
|
56
|
+
next = skipSpaces(input, next + 1);
|
|
57
|
+
}
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
__name(skipPostfix, "skipPostfix");
|
|
61
|
+
function previousNonSpaceIndex(input, index) {
|
|
62
|
+
let next = index;
|
|
63
|
+
while (next >= 0 && /\s/.test(input[next] || "")) next -= 1;
|
|
64
|
+
return next;
|
|
65
|
+
}
|
|
66
|
+
__name(previousNonSpaceIndex, "previousNonSpaceIndex");
|
|
67
|
+
function isGroupedCtxReference(input, start) {
|
|
68
|
+
const openParenIndex = previousNonSpaceIndex(input, start - 1);
|
|
69
|
+
if (input[openParenIndex] !== "(") return false;
|
|
70
|
+
const beforeOpenParenIndex = previousNonSpaceIndex(input, openParenIndex - 1);
|
|
71
|
+
if (beforeOpenParenIndex < 0) return true;
|
|
72
|
+
const beforeOpenParen = input[beforeOpenParenIndex];
|
|
73
|
+
return !isIdentifierPart(beforeOpenParen) && beforeOpenParen !== ")" && beforeOpenParen !== "]";
|
|
74
|
+
}
|
|
75
|
+
__name(isGroupedCtxReference, "isGroupedCtxReference");
|
|
76
|
+
function hasGroupedContinuation(input, index) {
|
|
77
|
+
const closeParenIndex = skipSpaces(input, index);
|
|
78
|
+
if (input[closeParenIndex] !== ")") return false;
|
|
79
|
+
const continuationIndex = skipSpaces(input, closeParenIndex + 1);
|
|
80
|
+
return input[continuationIndex] === "." || input[continuationIndex] === "[" || input.startsWith("?.", continuationIndex);
|
|
81
|
+
}
|
|
82
|
+
__name(hasGroupedContinuation, "hasGroupedContinuation");
|
|
83
|
+
function readIdentifier(input, index) {
|
|
84
|
+
if (!isIdentifierStart(input[index])) return null;
|
|
85
|
+
let next = index + 1;
|
|
86
|
+
while (isIdentifierPart(input[next])) next += 1;
|
|
87
|
+
return { value: input.slice(index, next), next };
|
|
88
|
+
}
|
|
89
|
+
__name(readIdentifier, "readIdentifier");
|
|
90
|
+
function readNumber(input, index) {
|
|
91
|
+
const match = input.slice(index).match(/^\d+/);
|
|
92
|
+
return match ? { value: match[0], next: index + match[0].length } : null;
|
|
93
|
+
}
|
|
94
|
+
__name(readNumber, "readNumber");
|
|
95
|
+
function readQuotedProperty(input, index) {
|
|
96
|
+
const quote = input[index];
|
|
97
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
98
|
+
let next = index + 1;
|
|
99
|
+
let value = "";
|
|
100
|
+
while (next < input.length) {
|
|
101
|
+
const char = input[next];
|
|
102
|
+
if (char === "\\") {
|
|
103
|
+
value += input.slice(next, next + 2);
|
|
104
|
+
next += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (char === quote) {
|
|
108
|
+
return { value, next: next + 1 };
|
|
109
|
+
}
|
|
110
|
+
value += char;
|
|
111
|
+
next += 1;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
__name(readQuotedProperty, "readQuotedProperty");
|
|
116
|
+
function readBracketAccess(input, index) {
|
|
117
|
+
let next = skipSpaces(input, index + 1);
|
|
118
|
+
const quoted = readQuotedProperty(input, next);
|
|
119
|
+
if (quoted) {
|
|
120
|
+
next = skipSpaces(input, quoted.next);
|
|
121
|
+
if (input[next] !== "]") return { dynamic: true };
|
|
122
|
+
return { dynamic: false, kind: "property", next: next + 1, value: quoted.value };
|
|
123
|
+
}
|
|
124
|
+
const number = readNumber(input, next);
|
|
125
|
+
if (number) {
|
|
126
|
+
next = skipSpaces(input, number.next);
|
|
127
|
+
if (input[next] !== "]") return { dynamic: true };
|
|
128
|
+
return { dynamic: false, kind: "index", next: next + 1, value: number.value };
|
|
129
|
+
}
|
|
130
|
+
return { dynamic: true };
|
|
131
|
+
}
|
|
132
|
+
__name(readBracketAccess, "readBracketAccess");
|
|
133
|
+
function appendPathSegment(segments, kind, value) {
|
|
134
|
+
if (kind === "property") {
|
|
135
|
+
segments.push(value);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (segments.length) {
|
|
139
|
+
segments[segments.length - 1] = `${segments[segments.length - 1]}[${value}]`;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
segments.push(`[${value}]`);
|
|
143
|
+
}
|
|
144
|
+
__name(appendPathSegment, "appendPathSegment");
|
|
145
|
+
function readAccess(input, index) {
|
|
146
|
+
const next = skipPostfix(input, index);
|
|
147
|
+
if (input[next] === "[") {
|
|
148
|
+
return readBracketAccess(input, next);
|
|
149
|
+
}
|
|
150
|
+
if (input.startsWith("?.", next)) {
|
|
151
|
+
const propertyStart = skipSpaces(input, next + 2);
|
|
152
|
+
if (input[propertyStart] === "[") {
|
|
153
|
+
return readBracketAccess(input, propertyStart);
|
|
154
|
+
}
|
|
155
|
+
const identifier = readIdentifier(input, propertyStart) || readNumber(input, propertyStart);
|
|
156
|
+
return identifier ? { dynamic: false, kind: "property", next: identifier.next, value: identifier.value } : null;
|
|
157
|
+
}
|
|
158
|
+
if (input[next] === ".") {
|
|
159
|
+
const propertyStart = skipSpaces(input, next + 1);
|
|
160
|
+
const identifier = readIdentifier(input, propertyStart) || readNumber(input, propertyStart);
|
|
161
|
+
return identifier ? { dynamic: false, kind: "property", next: identifier.next, value: identifier.value } : null;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
__name(readAccess, "readAccess");
|
|
166
|
+
function parseDotOnlyCtxReference(expr) {
|
|
167
|
+
const trimmed = expr.trim();
|
|
168
|
+
const segment = "(?:[a-zA-Z_$][a-zA-Z0-9_$-]*|\\d+)";
|
|
169
|
+
const match = trimmed.match(new RegExp(`^ctx\\.([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\\.(${segment}(?:\\.${segment})*))?$`));
|
|
170
|
+
if (!match) return null;
|
|
171
|
+
return {
|
|
172
|
+
methodCall: false,
|
|
173
|
+
path: match[2] || void 0,
|
|
174
|
+
unsupportedDynamicPath: false,
|
|
175
|
+
varName: match[1]
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
__name(parseDotOnlyCtxReference, "parseDotOnlyCtxReference");
|
|
179
|
+
function parseCtxReference(input, start) {
|
|
180
|
+
if (input.slice(start, start + 3) !== "ctx") return null;
|
|
181
|
+
if (isIdentifierPart(input[start - 1]) || isIdentifierPart(input[start + 3])) return null;
|
|
182
|
+
if (isGroupedCtxReference(input, start)) {
|
|
183
|
+
return {
|
|
184
|
+
next: start + 3,
|
|
185
|
+
reference: {
|
|
186
|
+
methodCall: false,
|
|
187
|
+
unsupportedDynamicPath: true,
|
|
188
|
+
varName: ""
|
|
52
189
|
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const rootAccess = readAccess(input, start + 3);
|
|
193
|
+
if (!rootAccess) return null;
|
|
194
|
+
if (rootAccess.dynamic) {
|
|
195
|
+
return {
|
|
196
|
+
next: start + 3,
|
|
197
|
+
reference: {
|
|
198
|
+
methodCall: false,
|
|
199
|
+
unsupportedDynamicPath: true,
|
|
200
|
+
varName: ""
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (rootAccess.kind !== "property") {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const segments = [];
|
|
208
|
+
let next = rootAccess.next;
|
|
209
|
+
let methodCall = false;
|
|
210
|
+
let unsupportedDynamicPath = false;
|
|
211
|
+
while (next < input.length) {
|
|
212
|
+
const accessStart = skipPostfix(input, next);
|
|
213
|
+
if ((input[accessStart] === "(" || input.startsWith("?.(", accessStart)) && !segments.length) {
|
|
214
|
+
methodCall = true;
|
|
215
|
+
next = accessStart;
|
|
216
|
+
break;
|
|
57
217
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
218
|
+
const access = readAccess(input, next);
|
|
219
|
+
if (!access) {
|
|
220
|
+
next = accessStart;
|
|
221
|
+
if (input[accessStart] === "[" || input[accessStart] === "." || input.startsWith("?.", accessStart)) {
|
|
222
|
+
unsupportedDynamicPath = true;
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
if (access.dynamic) {
|
|
227
|
+
unsupportedDynamicPath = true;
|
|
228
|
+
next = accessStart;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
appendPathSegment(segments, access.kind, access.value);
|
|
232
|
+
next = access.next;
|
|
233
|
+
}
|
|
234
|
+
const path = segments.join(".");
|
|
235
|
+
return {
|
|
236
|
+
next: Math.max(next, rootAccess.next),
|
|
237
|
+
reference: {
|
|
238
|
+
methodCall,
|
|
239
|
+
path: path || void 0,
|
|
240
|
+
unsupportedDynamicPath: unsupportedDynamicPath || hasGroupedContinuation(input, next),
|
|
241
|
+
varName: rootAccess.value
|
|
242
|
+
}
|
|
243
|
+
};
|
|
61
244
|
}
|
|
62
|
-
__name(
|
|
63
|
-
function
|
|
245
|
+
__name(parseCtxReference, "parseCtxReference");
|
|
246
|
+
function collectCtxReferences(expr) {
|
|
247
|
+
const dotOnly = parseDotOnlyCtxReference(expr);
|
|
248
|
+
if (dotOnly) return [dotOnly];
|
|
249
|
+
const references = [];
|
|
250
|
+
for (let index = 0; index < expr.length; index += 1) {
|
|
251
|
+
if (expr.slice(index, index + 3) !== "ctx") continue;
|
|
252
|
+
const parsed = parseCtxReference(expr, index);
|
|
253
|
+
if (!parsed) continue;
|
|
254
|
+
references.push(parsed.reference);
|
|
255
|
+
index = Math.max(index, parsed.next - 1);
|
|
256
|
+
}
|
|
257
|
+
return references;
|
|
258
|
+
}
|
|
259
|
+
__name(collectCtxReferences, "collectCtxReferences");
|
|
260
|
+
function addUsagePath(usage, varName, path, methodCall) {
|
|
261
|
+
usage[varName] = usage[varName] || [];
|
|
262
|
+
if (path) {
|
|
263
|
+
usage[varName].push(path);
|
|
264
|
+
} else if (methodCall && !usage[varName].includes("")) {
|
|
265
|
+
usage[varName].push("");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
__name(addUsagePath, "addUsagePath");
|
|
269
|
+
function extractVariableUsage(template) {
|
|
64
270
|
const usage = {};
|
|
271
|
+
let unsupportedDynamicPath = false;
|
|
272
|
+
const visitExpression = /* @__PURE__ */ __name((expr) => {
|
|
273
|
+
for (const reference of collectCtxReferences(expr)) {
|
|
274
|
+
if (reference.unsupportedDynamicPath) {
|
|
275
|
+
unsupportedDynamicPath = true;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!reference.varName) continue;
|
|
279
|
+
addUsagePath(usage, reference.varName, reference.path, reference.methodCall);
|
|
280
|
+
}
|
|
281
|
+
}, "visitExpression");
|
|
65
282
|
const visit = /* @__PURE__ */ __name((src) => {
|
|
66
283
|
if (typeof src === "string") {
|
|
67
284
|
const regex = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
68
|
-
let
|
|
69
|
-
while ((
|
|
70
|
-
|
|
71
|
-
const pathRegex = /ctx\.([a-zA-Z_$][a-zA-Z0-9_$]*)([^\s)]*)/g;
|
|
72
|
-
let pm;
|
|
73
|
-
while ((pm = pathRegex.exec(expr)) !== null) {
|
|
74
|
-
const varName = pm[1];
|
|
75
|
-
const after = pm[2] || "";
|
|
76
|
-
usage[varName] = usage[varName] || [];
|
|
77
|
-
if (after.startsWith(".")) {
|
|
78
|
-
usage[varName].push(after.slice(1));
|
|
79
|
-
} else if (after.startsWith("[")) {
|
|
80
|
-
const mm = after.match(/^\[\s*(["'])\s*([^'"\]]+)\s*\1\s*\](.*)$/);
|
|
81
|
-
if (mm) {
|
|
82
|
-
const first = mm[2];
|
|
83
|
-
const rest = mm[3] || "";
|
|
84
|
-
usage[varName].push(`${first}${rest}`);
|
|
85
|
-
} else {
|
|
86
|
-
const mn = after.match(/^\[(\d+)\](.*)$/);
|
|
87
|
-
if (mn) {
|
|
88
|
-
const idx = mn[1];
|
|
89
|
-
const rest = mn[2] || "";
|
|
90
|
-
usage[varName].push(`[${idx}]${rest}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
} else if (after.startsWith("(")) {
|
|
94
|
-
if (!usage[varName].length) usage[varName].push("");
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const bracketVarRegex = /ctx\[\s*(["'])\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\1\s*\]([^\s)]*)/g;
|
|
98
|
-
let bm;
|
|
99
|
-
while ((bm = bracketVarRegex.exec(expr)) !== null) {
|
|
100
|
-
const varName = bm[2];
|
|
101
|
-
const after = bm[3] || "";
|
|
102
|
-
usage[varName] = usage[varName] || [];
|
|
103
|
-
if (after.startsWith(".")) {
|
|
104
|
-
usage[varName].push(after.slice(1));
|
|
105
|
-
} else if (after.startsWith("[")) {
|
|
106
|
-
const mm = after.match(/^\[\s*(["'])\s*([^'"\]]+)\s*\1\s*\](.*)$/);
|
|
107
|
-
if (mm) {
|
|
108
|
-
const first = mm[2];
|
|
109
|
-
const rest = mm[3] || "";
|
|
110
|
-
usage[varName].push(`${first}${rest}`);
|
|
111
|
-
} else {
|
|
112
|
-
const mn = after.match(/^\[(\d+)\](.*)$/);
|
|
113
|
-
if (mn) {
|
|
114
|
-
const idx = mn[1];
|
|
115
|
-
const rest = mn[2] || "";
|
|
116
|
-
usage[varName].push(`[${idx}]${rest}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
} else if (after.startsWith("(")) {
|
|
120
|
-
if (!usage[varName].length) usage[varName].push("");
|
|
121
|
-
}
|
|
122
|
-
}
|
|
285
|
+
let match;
|
|
286
|
+
while ((match = regex.exec(src)) !== null) {
|
|
287
|
+
visitExpression(match[1]);
|
|
123
288
|
}
|
|
124
|
-
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (Array.isArray(src)) {
|
|
125
292
|
src.forEach(visit);
|
|
126
|
-
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (isObject(src)) {
|
|
127
296
|
Object.values(src).forEach(visit);
|
|
128
297
|
}
|
|
129
298
|
}, "visit");
|
|
130
299
|
visit(template);
|
|
131
|
-
return usage;
|
|
300
|
+
return { unsupportedDynamicPath, usage };
|
|
301
|
+
}
|
|
302
|
+
__name(extractVariableUsage, "extractVariableUsage");
|
|
303
|
+
function extractUsedVariableNames(template) {
|
|
304
|
+
return new Set(Object.keys(extractVariableUsage(template).usage));
|
|
305
|
+
}
|
|
306
|
+
__name(extractUsedVariableNames, "extractUsedVariableNames");
|
|
307
|
+
function extractUsedVariablePaths(template) {
|
|
308
|
+
return extractVariableUsage(template).usage;
|
|
132
309
|
}
|
|
133
310
|
__name(extractUsedVariablePaths, "extractUsedVariablePaths");
|
|
134
311
|
// Annotate the CommonJS export names for ESM import in node:
|
|
135
312
|
0 && (module.exports = {
|
|
136
313
|
extractUsedVariableNames,
|
|
137
|
-
extractUsedVariablePaths
|
|
314
|
+
extractUsedVariablePaths,
|
|
315
|
+
extractVariableUsage
|
|
138
316
|
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/shared",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.12",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"types": "./lib/index.d.ts",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
-
"gitHead": "
|
|
7
|
+
"gitHead": "2ba035885741a9977d76bbabf8f2caf44b199139"
|
|
8
8
|
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const UI_OPERATION_QUERY_KEY = '_operation_';
|
|
11
|
+
export const UI_OPERATION_VERSION = 1;
|
|
12
|
+
export const MAX_UI_OPERATION_ENCODED_LENGTH = 8 * 1024;
|
|
13
|
+
|
|
14
|
+
export interface UIOperation<
|
|
15
|
+
TOperationId extends string = string,
|
|
16
|
+
TParams extends Record<string, unknown> = Record<string, unknown>,
|
|
17
|
+
> {
|
|
18
|
+
v: typeof UI_OPERATION_VERSION;
|
|
19
|
+
operationId: TOperationId;
|
|
20
|
+
params?: TParams;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UIOperationCodec {
|
|
24
|
+
encode(value: string): string;
|
|
25
|
+
decode(value: string): string | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
29
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const prototype = Object.getPrototypeOf(value);
|
|
34
|
+
return prototype === Object.prototype || prototype === null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isUIOperation(value: unknown): value is UIOperation {
|
|
38
|
+
if (
|
|
39
|
+
!isPlainObject(value) ||
|
|
40
|
+
value.v !== UI_OPERATION_VERSION ||
|
|
41
|
+
typeof value.operationId !== 'string' ||
|
|
42
|
+
!value.operationId
|
|
43
|
+
) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return value.params === undefined || isPlainObject(value.params);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function encodeUIOperation(operation: UIOperation, codec: UIOperationCodec): string {
|
|
51
|
+
if (!isUIOperation(operation)) {
|
|
52
|
+
throw new TypeError('Invalid UI operation.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const encoded = codec.encode(JSON.stringify(operation));
|
|
56
|
+
if (encoded.length > MAX_UI_OPERATION_ENCODED_LENGTH) {
|
|
57
|
+
throw new RangeError(`UI operation exceeds the ${MAX_UI_OPERATION_ENCODED_LENGTH}-byte encoded length limit.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return encoded;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function decodeUIOperation(encoded: string, codec: UIOperationCodec): UIOperation | undefined {
|
|
64
|
+
if (encoded.length > MAX_UI_OPERATION_ENCODED_LENGTH) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const decoded = codec.decode(encoded);
|
|
70
|
+
if (decoded === undefined) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const parsed: unknown = JSON.parse(decoded);
|
|
75
|
+
return isUIOperation(parsed) ? parsed : undefined;
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function parseUIOperation(search: string, codec: UIOperationCodec): UIOperation | undefined {
|
|
82
|
+
try {
|
|
83
|
+
const values = new URLSearchParams(search).getAll(UI_OPERATION_QUERY_KEY);
|
|
84
|
+
if (values.length !== 1) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return decodeUIOperation(values[0], codec);
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function removeUIOperation(search: string): string {
|
|
95
|
+
const params = new URLSearchParams(search);
|
|
96
|
+
params.delete(UI_OPERATION_QUERY_KEY);
|
|
97
|
+
const remaining = params.toString();
|
|
98
|
+
return remaining ? `?${remaining}` : '';
|
|
99
|
+
}
|
package/src/variable-usage.ts
CHANGED
|
@@ -10,41 +10,316 @@
|
|
|
10
10
|
// 轻量模板类型定义:与前后端均可复用
|
|
11
11
|
export type JSONValue = string | { [key: string]: JSONValue } | JSONValue[];
|
|
12
12
|
|
|
13
|
+
export type VariableUsageExtractionResult = {
|
|
14
|
+
unsupportedDynamicPath: boolean;
|
|
15
|
+
usage: Record<string, string[]>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type CtxReference = {
|
|
19
|
+
methodCall: boolean;
|
|
20
|
+
path?: string;
|
|
21
|
+
unsupportedDynamicPath: boolean;
|
|
22
|
+
varName: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function isObject(value: unknown): value is Record<string, JSONValue> {
|
|
26
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isIdentifierStart(char: string | undefined) {
|
|
30
|
+
return !!char && /[a-zA-Z_$]/.test(char);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isIdentifierPart(char: string | undefined) {
|
|
34
|
+
return !!char && /[a-zA-Z0-9_$]/.test(char);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function skipSpaces(input: string, index: number) {
|
|
38
|
+
let next = index;
|
|
39
|
+
while (/\s/.test(input[next] || '')) next += 1;
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function skipPostfix(input: string, index: number) {
|
|
44
|
+
let next = skipSpaces(input, index);
|
|
45
|
+
while (input[next] === '!') {
|
|
46
|
+
next = skipSpaces(input, next + 1);
|
|
47
|
+
}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function previousNonSpaceIndex(input: string, index: number) {
|
|
52
|
+
let next = index;
|
|
53
|
+
while (next >= 0 && /\s/.test(input[next] || '')) next -= 1;
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isGroupedCtxReference(input: string, start: number) {
|
|
58
|
+
const openParenIndex = previousNonSpaceIndex(input, start - 1);
|
|
59
|
+
if (input[openParenIndex] !== '(') return false;
|
|
60
|
+
|
|
61
|
+
const beforeOpenParenIndex = previousNonSpaceIndex(input, openParenIndex - 1);
|
|
62
|
+
if (beforeOpenParenIndex < 0) return true;
|
|
63
|
+
|
|
64
|
+
const beforeOpenParen = input[beforeOpenParenIndex];
|
|
65
|
+
return !isIdentifierPart(beforeOpenParen) && beforeOpenParen !== ')' && beforeOpenParen !== ']';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hasGroupedContinuation(input: string, index: number) {
|
|
69
|
+
const closeParenIndex = skipSpaces(input, index);
|
|
70
|
+
if (input[closeParenIndex] !== ')') return false;
|
|
71
|
+
|
|
72
|
+
const continuationIndex = skipSpaces(input, closeParenIndex + 1);
|
|
73
|
+
return (
|
|
74
|
+
input[continuationIndex] === '.' || input[continuationIndex] === '[' || input.startsWith('?.', continuationIndex)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readIdentifier(input: string, index: number): { next: number; value: string } | null {
|
|
79
|
+
if (!isIdentifierStart(input[index])) return null;
|
|
80
|
+
let next = index + 1;
|
|
81
|
+
while (isIdentifierPart(input[next])) next += 1;
|
|
82
|
+
return { value: input.slice(index, next), next };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readNumber(input: string, index: number): { next: number; value: string } | null {
|
|
86
|
+
const match = input.slice(index).match(/^\d+/);
|
|
87
|
+
return match ? { value: match[0], next: index + match[0].length } : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readQuotedProperty(input: string, index: number): { next: number; value: string } | null {
|
|
91
|
+
const quote = input[index];
|
|
92
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
93
|
+
let next = index + 1;
|
|
94
|
+
let value = '';
|
|
95
|
+
while (next < input.length) {
|
|
96
|
+
const char = input[next];
|
|
97
|
+
if (char === '\\') {
|
|
98
|
+
value += input.slice(next, next + 2);
|
|
99
|
+
next += 2;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (char === quote) {
|
|
103
|
+
return { value, next: next + 1 };
|
|
104
|
+
}
|
|
105
|
+
value += char;
|
|
106
|
+
next += 1;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readBracketAccess(input: string, index: number) {
|
|
112
|
+
let next = skipSpaces(input, index + 1);
|
|
113
|
+
const quoted = readQuotedProperty(input, next);
|
|
114
|
+
if (quoted) {
|
|
115
|
+
next = skipSpaces(input, quoted.next);
|
|
116
|
+
if (input[next] !== ']') return { dynamic: true as const };
|
|
117
|
+
return { dynamic: false as const, kind: 'property' as const, next: next + 1, value: quoted.value };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const number = readNumber(input, next);
|
|
121
|
+
if (number) {
|
|
122
|
+
next = skipSpaces(input, number.next);
|
|
123
|
+
if (input[next] !== ']') return { dynamic: true as const };
|
|
124
|
+
return { dynamic: false as const, kind: 'index' as const, next: next + 1, value: number.value };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { dynamic: true as const };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function appendPathSegment(segments: string[], kind: 'property' | 'index', value: string) {
|
|
131
|
+
if (kind === 'property') {
|
|
132
|
+
segments.push(value);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (segments.length) {
|
|
136
|
+
segments[segments.length - 1] = `${segments[segments.length - 1]}[${value}]`;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
segments.push(`[${value}]`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readAccess(input: string, index: number) {
|
|
143
|
+
const next = skipPostfix(input, index);
|
|
144
|
+
if (input[next] === '[') {
|
|
145
|
+
return readBracketAccess(input, next);
|
|
146
|
+
}
|
|
147
|
+
if (input.startsWith('?.', next)) {
|
|
148
|
+
const propertyStart = skipSpaces(input, next + 2);
|
|
149
|
+
if (input[propertyStart] === '[') {
|
|
150
|
+
return readBracketAccess(input, propertyStart);
|
|
151
|
+
}
|
|
152
|
+
const identifier = readIdentifier(input, propertyStart) || readNumber(input, propertyStart);
|
|
153
|
+
return identifier
|
|
154
|
+
? { dynamic: false as const, kind: 'property' as const, next: identifier.next, value: identifier.value }
|
|
155
|
+
: null;
|
|
156
|
+
}
|
|
157
|
+
if (input[next] === '.') {
|
|
158
|
+
const propertyStart = skipSpaces(input, next + 1);
|
|
159
|
+
const identifier = readIdentifier(input, propertyStart) || readNumber(input, propertyStart);
|
|
160
|
+
return identifier
|
|
161
|
+
? { dynamic: false as const, kind: 'property' as const, next: identifier.next, value: identifier.value }
|
|
162
|
+
: null;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseDotOnlyCtxReference(expr: string): CtxReference | null {
|
|
168
|
+
const trimmed = expr.trim();
|
|
169
|
+
const segment = '(?:[a-zA-Z_$][a-zA-Z0-9_$-]*|\\d+)';
|
|
170
|
+
const match = trimmed.match(new RegExp(`^ctx\\.([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\\.(${segment}(?:\\.${segment})*))?$`));
|
|
171
|
+
if (!match) return null;
|
|
172
|
+
return {
|
|
173
|
+
methodCall: false,
|
|
174
|
+
path: match[2] || undefined,
|
|
175
|
+
unsupportedDynamicPath: false,
|
|
176
|
+
varName: match[1],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseCtxReference(input: string, start: number): { next: number; reference: CtxReference } | null {
|
|
181
|
+
if (input.slice(start, start + 3) !== 'ctx') return null;
|
|
182
|
+
if (isIdentifierPart(input[start - 1]) || isIdentifierPart(input[start + 3])) return null;
|
|
183
|
+
|
|
184
|
+
if (isGroupedCtxReference(input, start)) {
|
|
185
|
+
return {
|
|
186
|
+
next: start + 3,
|
|
187
|
+
reference: {
|
|
188
|
+
methodCall: false,
|
|
189
|
+
unsupportedDynamicPath: true,
|
|
190
|
+
varName: '',
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const rootAccess = readAccess(input, start + 3);
|
|
196
|
+
if (!rootAccess) return null;
|
|
197
|
+
if (rootAccess.dynamic) {
|
|
198
|
+
return {
|
|
199
|
+
next: start + 3,
|
|
200
|
+
reference: {
|
|
201
|
+
methodCall: false,
|
|
202
|
+
unsupportedDynamicPath: true,
|
|
203
|
+
varName: '',
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (rootAccess.kind !== 'property') {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const segments: string[] = [];
|
|
212
|
+
let next = rootAccess.next;
|
|
213
|
+
let methodCall = false;
|
|
214
|
+
let unsupportedDynamicPath = false;
|
|
215
|
+
|
|
216
|
+
while (next < input.length) {
|
|
217
|
+
const accessStart = skipPostfix(input, next);
|
|
218
|
+
if ((input[accessStart] === '(' || input.startsWith('?.(', accessStart)) && !segments.length) {
|
|
219
|
+
methodCall = true;
|
|
220
|
+
next = accessStart;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const access = readAccess(input, next);
|
|
225
|
+
if (!access) {
|
|
226
|
+
next = accessStart;
|
|
227
|
+
if (input[accessStart] === '[' || input[accessStart] === '.' || input.startsWith('?.', accessStart)) {
|
|
228
|
+
unsupportedDynamicPath = true;
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (access.dynamic) {
|
|
233
|
+
unsupportedDynamicPath = true;
|
|
234
|
+
next = accessStart;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
appendPathSegment(segments, access.kind, access.value);
|
|
238
|
+
next = access.next;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const path = segments.join('.');
|
|
242
|
+
return {
|
|
243
|
+
next: Math.max(next, rootAccess.next),
|
|
244
|
+
reference: {
|
|
245
|
+
methodCall,
|
|
246
|
+
path: path || undefined,
|
|
247
|
+
unsupportedDynamicPath: unsupportedDynamicPath || hasGroupedContinuation(input, next),
|
|
248
|
+
varName: rootAccess.value,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function collectCtxReferences(expr: string): CtxReference[] {
|
|
254
|
+
const dotOnly = parseDotOnlyCtxReference(expr);
|
|
255
|
+
if (dotOnly) return [dotOnly];
|
|
256
|
+
|
|
257
|
+
const references: CtxReference[] = [];
|
|
258
|
+
for (let index = 0; index < expr.length; index += 1) {
|
|
259
|
+
if (expr.slice(index, index + 3) !== 'ctx') continue;
|
|
260
|
+
const parsed = parseCtxReference(expr, index);
|
|
261
|
+
if (!parsed) continue;
|
|
262
|
+
references.push(parsed.reference);
|
|
263
|
+
index = Math.max(index, parsed.next - 1);
|
|
264
|
+
}
|
|
265
|
+
return references;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function addUsagePath(usage: Record<string, string[]>, varName: string, path?: string, methodCall?: boolean) {
|
|
269
|
+
usage[varName] = usage[varName] || [];
|
|
270
|
+
if (path) {
|
|
271
|
+
usage[varName].push(path);
|
|
272
|
+
} else if (methodCall && !usage[varName].includes('')) {
|
|
273
|
+
usage[varName].push('');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
13
277
|
/**
|
|
14
|
-
*
|
|
15
|
-
* - 支持点语法与顶层括号变量:ctx.user / ctx["user"]
|
|
278
|
+
* 提取模板中的 ctx 使用情况。该函数只做静态扫描,不执行表达式。
|
|
16
279
|
*/
|
|
17
|
-
export function
|
|
18
|
-
const
|
|
280
|
+
export function extractVariableUsage(template: JSONValue): VariableUsageExtractionResult {
|
|
281
|
+
const usage: Record<string, string[]> = {};
|
|
282
|
+
let unsupportedDynamicPath = false;
|
|
19
283
|
|
|
20
|
-
const
|
|
284
|
+
const visitExpression = (expr: string) => {
|
|
285
|
+
for (const reference of collectCtxReferences(expr)) {
|
|
286
|
+
if (reference.unsupportedDynamicPath) {
|
|
287
|
+
unsupportedDynamicPath = true;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (!reference.varName) continue;
|
|
291
|
+
addUsagePath(usage, reference.varName, reference.path, reference.methodCall);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const visit = (src: JSONValue) => {
|
|
21
296
|
if (typeof src === 'string') {
|
|
22
297
|
const regex = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
23
|
-
let
|
|
24
|
-
while ((
|
|
25
|
-
|
|
26
|
-
// ctx.<var>
|
|
27
|
-
const pathRegex = /ctx\.([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
|
28
|
-
let pm: RegExpExecArray | null;
|
|
29
|
-
while ((pm = pathRegex.exec(expr)) !== null) {
|
|
30
|
-
result.add(pm[1]);
|
|
31
|
-
}
|
|
32
|
-
// ctx["var"] or ctx['var']
|
|
33
|
-
const bracketVarRegex = /ctx\[\s*(["'])\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\1\s*\]/g;
|
|
34
|
-
let bm: RegExpExecArray | null;
|
|
35
|
-
while ((bm = bracketVarRegex.exec(expr)) !== null) {
|
|
36
|
-
result.add(bm[2]);
|
|
37
|
-
}
|
|
298
|
+
let match: RegExpExecArray | null;
|
|
299
|
+
while ((match = regex.exec(src)) !== null) {
|
|
300
|
+
visitExpression(match[1]);
|
|
38
301
|
}
|
|
39
|
-
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (Array.isArray(src)) {
|
|
40
305
|
src.forEach(visit);
|
|
41
|
-
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (isObject(src)) {
|
|
42
309
|
Object.values(src).forEach(visit);
|
|
43
310
|
}
|
|
44
311
|
};
|
|
45
312
|
|
|
46
313
|
visit(template);
|
|
47
|
-
return
|
|
314
|
+
return { unsupportedDynamicPath, usage };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 提取模板中使用到的 ctx 顶层变量名集合。
|
|
319
|
+
* - 支持点语法与顶层括号变量:ctx.user / ctx["user"]
|
|
320
|
+
*/
|
|
321
|
+
export function extractUsedVariableNames(template: JSONValue): Set<string> {
|
|
322
|
+
return new Set(Object.keys(extractVariableUsage(template).usage));
|
|
48
323
|
}
|
|
49
324
|
|
|
50
325
|
/**
|
|
@@ -54,79 +329,5 @@ export function extractUsedVariableNames(template: JSONValue): Set<string> {
|
|
|
54
329
|
* - 方法调用:记录空字符串以触发服务端 attach(例如 ctx.twice(21) => { twice: [''] })
|
|
55
330
|
*/
|
|
56
331
|
export function extractUsedVariablePaths(template: JSONValue): Record<string, string[]> {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const visit = (src: any) => {
|
|
60
|
-
if (typeof src === 'string') {
|
|
61
|
-
const regex = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
62
|
-
let m: RegExpExecArray | null;
|
|
63
|
-
while ((m = regex.exec(src)) !== null) {
|
|
64
|
-
const expr = m[1];
|
|
65
|
-
|
|
66
|
-
// 点语法:ctx.varName[...]
|
|
67
|
-
const pathRegex = /ctx\.([a-zA-Z_$][a-zA-Z0-9_$]*)([^\s)]*)/g;
|
|
68
|
-
let pm: RegExpExecArray | null;
|
|
69
|
-
while ((pm = pathRegex.exec(expr)) !== null) {
|
|
70
|
-
const varName = pm[1];
|
|
71
|
-
const after = pm[2] || '';
|
|
72
|
-
usage[varName] = usage[varName] || [];
|
|
73
|
-
if (after.startsWith('.')) {
|
|
74
|
-
usage[varName].push(after.slice(1));
|
|
75
|
-
} else if (after.startsWith('[')) {
|
|
76
|
-
// 首段括号键或数字索引
|
|
77
|
-
const mm = after.match(/^\[\s*(["'])\s*([^'"\]]+)\s*\1\s*\](.*)$/);
|
|
78
|
-
if (mm) {
|
|
79
|
-
const first = mm[2];
|
|
80
|
-
const rest = mm[3] || '';
|
|
81
|
-
usage[varName].push(`${first}${rest}`);
|
|
82
|
-
} else {
|
|
83
|
-
const mn = after.match(/^\[(\d+)\](.*)$/);
|
|
84
|
-
if (mn) {
|
|
85
|
-
const idx = mn[1];
|
|
86
|
-
const rest = mn[2] || '';
|
|
87
|
-
usage[varName].push(`[${idx}]${rest}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} else if (after.startsWith('(')) {
|
|
91
|
-
if (!usage[varName].length) usage[varName].push('');
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 顶层括号变量:ctx["varName"]...
|
|
96
|
-
const bracketVarRegex = /ctx\[\s*(["'])\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\1\s*\]([^\s)]*)/g;
|
|
97
|
-
let bm: RegExpExecArray | null;
|
|
98
|
-
while ((bm = bracketVarRegex.exec(expr)) !== null) {
|
|
99
|
-
const varName = bm[2];
|
|
100
|
-
const after = bm[3] || '';
|
|
101
|
-
usage[varName] = usage[varName] || [];
|
|
102
|
-
if (after.startsWith('.')) {
|
|
103
|
-
usage[varName].push(after.slice(1));
|
|
104
|
-
} else if (after.startsWith('[')) {
|
|
105
|
-
const mm = after.match(/^\[\s*(["'])\s*([^'"\]]+)\s*\1\s*\](.*)$/);
|
|
106
|
-
if (mm) {
|
|
107
|
-
const first = mm[2];
|
|
108
|
-
const rest = mm[3] || '';
|
|
109
|
-
usage[varName].push(`${first}${rest}`);
|
|
110
|
-
} else {
|
|
111
|
-
const mn = after.match(/^\[(\d+)\](.*)$/);
|
|
112
|
-
if (mn) {
|
|
113
|
-
const idx = mn[1];
|
|
114
|
-
const rest = mn[2] || '';
|
|
115
|
-
usage[varName].push(`[${idx}]${rest}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
} else if (after.startsWith('(')) {
|
|
119
|
-
if (!usage[varName].length) usage[varName].push('');
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
} else if (Array.isArray(src)) {
|
|
124
|
-
src.forEach(visit);
|
|
125
|
-
} else if (src && typeof src === 'object') {
|
|
126
|
-
Object.values(src).forEach(visit);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
visit(template);
|
|
131
|
-
return usage;
|
|
332
|
+
return extractVariableUsage(template).usage;
|
|
132
333
|
}
|