@djvlc/runtime-core 1.0.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/index.cjs +3368 -0
- package/dist/index.d.cts +1228 -0
- package/dist/index.d.ts +1228 -0
- package/dist/index.js +3314 -0
- package/package.json +56 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ActionBridge: () => ActionBridge,
|
|
24
|
+
ActionError: () => ActionError,
|
|
25
|
+
AssetLoader: () => AssetLoader,
|
|
26
|
+
BaseRenderer: () => BaseRenderer,
|
|
27
|
+
ComponentBlockedError: () => ComponentBlockedError,
|
|
28
|
+
ComponentLoadError: () => ComponentLoadError,
|
|
29
|
+
ComponentLoader: () => ComponentLoader,
|
|
30
|
+
DjvlcRuntime: () => DjvlcRuntime,
|
|
31
|
+
DjvlcRuntimeError: () => DjvlcRuntimeError,
|
|
32
|
+
Evaluator: () => Evaluator,
|
|
33
|
+
EventBus: () => EventBus,
|
|
34
|
+
ExpressionEngine: () => ExpressionEngine,
|
|
35
|
+
ExpressionError: () => ExpressionError,
|
|
36
|
+
HostAPIImpl: () => HostAPIImpl,
|
|
37
|
+
IntegrityError: () => IntegrityError,
|
|
38
|
+
Lexer: () => Lexer,
|
|
39
|
+
PageLoadError: () => PageLoadError,
|
|
40
|
+
PageLoader: () => PageLoader,
|
|
41
|
+
Parser: () => Parser,
|
|
42
|
+
QueryError: () => QueryError,
|
|
43
|
+
RenderError: () => RenderError,
|
|
44
|
+
SecurityManager: () => SecurityManager,
|
|
45
|
+
StateManager: () => StateManager,
|
|
46
|
+
TelemetryManager: () => TelemetryManager,
|
|
47
|
+
builtinFunctions: () => builtinFunctions,
|
|
48
|
+
createFallbackElement: () => createFallbackElement,
|
|
49
|
+
createRuntime: () => createRuntime,
|
|
50
|
+
registerFallbackComponents: () => registerFallbackComponents
|
|
51
|
+
});
|
|
52
|
+
module.exports = __toCommonJS(index_exports);
|
|
53
|
+
|
|
54
|
+
// src/types/errors.ts
|
|
55
|
+
var import_contracts_types = require("@djvlc/contracts-types");
|
|
56
|
+
var DjvlcRuntimeError = class extends Error {
|
|
57
|
+
constructor(code, message, details, traceId) {
|
|
58
|
+
super(message || import_contracts_types.ErrorMessages[code] || "Unknown error");
|
|
59
|
+
this.name = "DjvlcRuntimeError";
|
|
60
|
+
this.code = code;
|
|
61
|
+
this.details = details;
|
|
62
|
+
this.traceId = traceId;
|
|
63
|
+
this.timestamp = Date.now();
|
|
64
|
+
}
|
|
65
|
+
toJSON() {
|
|
66
|
+
return {
|
|
67
|
+
name: this.name,
|
|
68
|
+
code: this.code,
|
|
69
|
+
message: this.message,
|
|
70
|
+
details: this.details,
|
|
71
|
+
traceId: this.traceId,
|
|
72
|
+
timestamp: this.timestamp
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var PageLoadError = class extends DjvlcRuntimeError {
|
|
77
|
+
constructor(message, details, traceId) {
|
|
78
|
+
super(import_contracts_types.ErrorCode.PAGE_NOT_FOUND, message, details, traceId);
|
|
79
|
+
this.name = "PageLoadError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var ComponentLoadError = class extends DjvlcRuntimeError {
|
|
83
|
+
constructor(componentName, componentVersion, message, code = import_contracts_types.ErrorCode.COMPONENT_NOT_FOUND, details) {
|
|
84
|
+
super(code, message, { ...details, componentName, componentVersion });
|
|
85
|
+
this.name = "ComponentLoadError";
|
|
86
|
+
this.componentName = componentName;
|
|
87
|
+
this.componentVersion = componentVersion;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var IntegrityError = class extends DjvlcRuntimeError {
|
|
91
|
+
constructor(componentName, componentVersion, expectedHash, actualHash) {
|
|
92
|
+
super(
|
|
93
|
+
import_contracts_types.ErrorCode.COMPONENT_INTEGRITY_MISMATCH,
|
|
94
|
+
`Integrity check failed for ${componentName}@${componentVersion}`,
|
|
95
|
+
{ expectedHash, actualHash }
|
|
96
|
+
);
|
|
97
|
+
this.name = "IntegrityError";
|
|
98
|
+
this.componentName = componentName;
|
|
99
|
+
this.componentVersion = componentVersion;
|
|
100
|
+
this.expectedHash = expectedHash;
|
|
101
|
+
this.actualHash = actualHash;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var ComponentBlockedError = class extends DjvlcRuntimeError {
|
|
105
|
+
constructor(componentName, componentVersion, reason) {
|
|
106
|
+
super(import_contracts_types.ErrorCode.COMPONENT_BLOCKED, `Component ${componentName}@${componentVersion} is blocked`, {
|
|
107
|
+
componentName,
|
|
108
|
+
componentVersion,
|
|
109
|
+
reason
|
|
110
|
+
});
|
|
111
|
+
this.name = "ComponentBlockedError";
|
|
112
|
+
this.componentName = componentName;
|
|
113
|
+
this.componentVersion = componentVersion;
|
|
114
|
+
this.reason = reason;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var ExpressionError = class extends DjvlcRuntimeError {
|
|
118
|
+
constructor(expression, message, position, details) {
|
|
119
|
+
super(import_contracts_types.ErrorCode.UNKNOWN, message, { ...details, expression, position });
|
|
120
|
+
this.name = "ExpressionError";
|
|
121
|
+
this.expression = expression;
|
|
122
|
+
this.position = position;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var ActionError = class extends DjvlcRuntimeError {
|
|
126
|
+
constructor(actionType, message, code = import_contracts_types.ErrorCode.ACTION_EXECUTION_FAILED, actionId, details) {
|
|
127
|
+
super(code, message, { ...details, actionType, actionId });
|
|
128
|
+
this.name = "ActionError";
|
|
129
|
+
this.actionType = actionType;
|
|
130
|
+
this.actionId = actionId;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var QueryError = class extends DjvlcRuntimeError {
|
|
134
|
+
constructor(queryId, message, code = import_contracts_types.ErrorCode.QUERY_EXECUTION_FAILED, details) {
|
|
135
|
+
super(code, message, { ...details, queryId });
|
|
136
|
+
this.name = "QueryError";
|
|
137
|
+
this.queryId = queryId;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var RenderError = class extends DjvlcRuntimeError {
|
|
141
|
+
constructor(componentId, componentType, message, details) {
|
|
142
|
+
super(import_contracts_types.ErrorCode.UNKNOWN, message, { ...details, componentId, componentType });
|
|
143
|
+
this.name = "RenderError";
|
|
144
|
+
this.componentId = componentId;
|
|
145
|
+
this.componentType = componentType;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/loader/page-loader.ts
|
|
150
|
+
var PageLoader = class {
|
|
151
|
+
constructor(options) {
|
|
152
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
153
|
+
this.options = {
|
|
154
|
+
channel: "prod",
|
|
155
|
+
cache: { enabled: true, maxAge: 300 },
|
|
156
|
+
...options
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 解析页面
|
|
161
|
+
* @param pageUid 页面 UID
|
|
162
|
+
* @param params 额外参数
|
|
163
|
+
*/
|
|
164
|
+
async resolve(pageUid, params) {
|
|
165
|
+
const cacheKey = this.getCacheKey(pageUid, params);
|
|
166
|
+
if (this.options.cache?.enabled) {
|
|
167
|
+
const cached = this.cache.get(cacheKey);
|
|
168
|
+
if (cached && this.isCacheValid(cached.timestamp)) {
|
|
169
|
+
this.log("debug", `Page ${pageUid} loaded from cache`);
|
|
170
|
+
return cached.data;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const startTime = performance.now();
|
|
174
|
+
try {
|
|
175
|
+
const url = this.buildResolveUrl(pageUid, params);
|
|
176
|
+
const response = await fetch(url, {
|
|
177
|
+
method: "GET",
|
|
178
|
+
headers: this.buildHeaders()
|
|
179
|
+
});
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
throw new PageLoadError(
|
|
182
|
+
`Failed to resolve page: ${response.status} ${response.statusText}`,
|
|
183
|
+
{ pageUid, status: response.status }
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const result = await response.json();
|
|
187
|
+
if (!this.isValidPageResolveResult(result)) {
|
|
188
|
+
throw new PageLoadError("Invalid page resolve response", { pageUid });
|
|
189
|
+
}
|
|
190
|
+
const data = result.data;
|
|
191
|
+
if (this.options.cache?.enabled) {
|
|
192
|
+
this.cache.set(cacheKey, { data, timestamp: Date.now() });
|
|
193
|
+
}
|
|
194
|
+
const loadTime = performance.now() - startTime;
|
|
195
|
+
this.log("info", `Page ${pageUid} resolved in ${loadTime.toFixed(2)}ms`);
|
|
196
|
+
return data;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (error instanceof PageLoadError) {
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
throw new PageLoadError(
|
|
202
|
+
`Failed to resolve page: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
203
|
+
{ pageUid }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* 预连接 API 服务器
|
|
209
|
+
*/
|
|
210
|
+
preconnect() {
|
|
211
|
+
const link = document.createElement("link");
|
|
212
|
+
link.rel = "preconnect";
|
|
213
|
+
link.href = this.options.apiBaseUrl;
|
|
214
|
+
document.head.appendChild(link);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 清除缓存
|
|
218
|
+
*/
|
|
219
|
+
clearCache(pageUid) {
|
|
220
|
+
if (pageUid) {
|
|
221
|
+
for (const key of this.cache.keys()) {
|
|
222
|
+
if (key.startsWith(pageUid)) {
|
|
223
|
+
this.cache.delete(key);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
this.cache.clear();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
buildResolveUrl(pageUid, params) {
|
|
231
|
+
const url = new URL(`${this.options.apiBaseUrl}/page/resolve`);
|
|
232
|
+
url.searchParams.set("pageUid", pageUid);
|
|
233
|
+
if (this.options.channel) {
|
|
234
|
+
url.searchParams.set("channel", this.options.channel);
|
|
235
|
+
}
|
|
236
|
+
if (this.options.previewToken) {
|
|
237
|
+
url.searchParams.set("previewToken", this.options.previewToken);
|
|
238
|
+
}
|
|
239
|
+
if (params?.uid) {
|
|
240
|
+
url.searchParams.set("uid", params.uid);
|
|
241
|
+
}
|
|
242
|
+
if (params?.deviceId) {
|
|
243
|
+
url.searchParams.set("deviceId", params.deviceId);
|
|
244
|
+
}
|
|
245
|
+
return url.toString();
|
|
246
|
+
}
|
|
247
|
+
buildHeaders() {
|
|
248
|
+
const headers = {
|
|
249
|
+
"Content-Type": "application/json",
|
|
250
|
+
...this.options.headers
|
|
251
|
+
};
|
|
252
|
+
if (this.options.authToken) {
|
|
253
|
+
headers["Authorization"] = `Bearer ${this.options.authToken}`;
|
|
254
|
+
}
|
|
255
|
+
return headers;
|
|
256
|
+
}
|
|
257
|
+
getCacheKey(pageUid, params) {
|
|
258
|
+
const parts = [pageUid, this.options.channel];
|
|
259
|
+
if (params?.uid) parts.push(params.uid);
|
|
260
|
+
if (params?.deviceId) parts.push(params.deviceId);
|
|
261
|
+
return parts.join(":");
|
|
262
|
+
}
|
|
263
|
+
isCacheValid(timestamp) {
|
|
264
|
+
const maxAge = (this.options.cache?.maxAge ?? 300) * 1e3;
|
|
265
|
+
return Date.now() - timestamp < maxAge;
|
|
266
|
+
}
|
|
267
|
+
isValidPageResolveResult(result) {
|
|
268
|
+
if (!result || typeof result !== "object") return false;
|
|
269
|
+
const r = result;
|
|
270
|
+
if (!r.data || typeof r.data !== "object") return false;
|
|
271
|
+
const data = r.data;
|
|
272
|
+
return typeof data.pageUid === "string" && typeof data.pageVersionId === "string" && data.pageJson !== void 0 && data.manifest !== void 0;
|
|
273
|
+
}
|
|
274
|
+
log(level, message) {
|
|
275
|
+
if (this.options.logger) {
|
|
276
|
+
this.options.logger[level](message);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// src/loader/component-loader.ts
|
|
282
|
+
var ComponentLoader = class {
|
|
283
|
+
constructor(options) {
|
|
284
|
+
this.loadedComponents = /* @__PURE__ */ new Map();
|
|
285
|
+
this.loadingPromises = /* @__PURE__ */ new Map();
|
|
286
|
+
this.options = {
|
|
287
|
+
enableSRI: true,
|
|
288
|
+
concurrency: 4,
|
|
289
|
+
timeout: 3e4,
|
|
290
|
+
blockedComponents: [],
|
|
291
|
+
...options
|
|
292
|
+
};
|
|
293
|
+
this.blockedSet = new Set(this.options.blockedComponents);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* 加载单个组件
|
|
297
|
+
*/
|
|
298
|
+
async load(item) {
|
|
299
|
+
const key = this.getComponentKey(item.name, item.version);
|
|
300
|
+
if (this.isBlocked(item.name, item.version)) {
|
|
301
|
+
throw new ComponentBlockedError(item.name, item.version, "Component is blocked");
|
|
302
|
+
}
|
|
303
|
+
const loaded = this.loadedComponents.get(key);
|
|
304
|
+
if (loaded) {
|
|
305
|
+
return loaded;
|
|
306
|
+
}
|
|
307
|
+
const loading = this.loadingPromises.get(key);
|
|
308
|
+
if (loading) {
|
|
309
|
+
return loading;
|
|
310
|
+
}
|
|
311
|
+
const loadPromise = this.loadComponent(item);
|
|
312
|
+
this.loadingPromises.set(key, loadPromise);
|
|
313
|
+
try {
|
|
314
|
+
const component = await loadPromise;
|
|
315
|
+
this.loadedComponents.set(key, component);
|
|
316
|
+
return component;
|
|
317
|
+
} finally {
|
|
318
|
+
this.loadingPromises.delete(key);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* 加载 Manifest 中的所有组件
|
|
323
|
+
*/
|
|
324
|
+
async loadAll(manifest) {
|
|
325
|
+
const results = /* @__PURE__ */ new Map();
|
|
326
|
+
const { concurrency = 4 } = this.options;
|
|
327
|
+
const components = manifest.components;
|
|
328
|
+
for (let i = 0; i < components.length; i += concurrency) {
|
|
329
|
+
const batch = components.slice(i, i + concurrency);
|
|
330
|
+
const batchPromises = batch.map(async (item) => {
|
|
331
|
+
const key = this.getComponentKey(item.name, item.version);
|
|
332
|
+
const startTime = performance.now();
|
|
333
|
+
try {
|
|
334
|
+
const loaded = await this.load(item);
|
|
335
|
+
results.set(key, {
|
|
336
|
+
name: item.name,
|
|
337
|
+
version: item.version,
|
|
338
|
+
status: "loaded",
|
|
339
|
+
component: loaded.Component,
|
|
340
|
+
loadTime: performance.now() - startTime
|
|
341
|
+
});
|
|
342
|
+
} catch (error) {
|
|
343
|
+
const status = error instanceof ComponentBlockedError ? "blocked" : "failed";
|
|
344
|
+
results.set(key, {
|
|
345
|
+
name: item.name,
|
|
346
|
+
version: item.version,
|
|
347
|
+
status,
|
|
348
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
349
|
+
loadTime: performance.now() - startTime
|
|
350
|
+
});
|
|
351
|
+
if (item.critical) {
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
await Promise.all(batchPromises);
|
|
357
|
+
}
|
|
358
|
+
return results;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* 预加载组件
|
|
362
|
+
*/
|
|
363
|
+
preload(items) {
|
|
364
|
+
items.forEach((item) => {
|
|
365
|
+
const link = document.createElement("link");
|
|
366
|
+
link.rel = "preload";
|
|
367
|
+
link.as = "script";
|
|
368
|
+
link.href = this.resolveUrl(item.entryUrl);
|
|
369
|
+
if (this.options.enableSRI && item.integrity) {
|
|
370
|
+
link.integrity = item.integrity;
|
|
371
|
+
link.crossOrigin = "anonymous";
|
|
372
|
+
}
|
|
373
|
+
document.head.appendChild(link);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* 检查组件是否已加载
|
|
378
|
+
*/
|
|
379
|
+
isLoaded(name, version) {
|
|
380
|
+
return this.loadedComponents.has(this.getComponentKey(name, version));
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 获取已加载的组件
|
|
384
|
+
*/
|
|
385
|
+
get(name, version) {
|
|
386
|
+
return this.loadedComponents.get(this.getComponentKey(name, version));
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* 检查组件是否被阻断
|
|
390
|
+
*/
|
|
391
|
+
isBlocked(name, version) {
|
|
392
|
+
return this.blockedSet.has(`${name}@${version}`) || this.blockedSet.has(name);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* 更新阻断列表
|
|
396
|
+
*/
|
|
397
|
+
updateBlockedList(blocked) {
|
|
398
|
+
this.blockedSet = new Set(blocked);
|
|
399
|
+
}
|
|
400
|
+
async loadComponent(item) {
|
|
401
|
+
const startTime = performance.now();
|
|
402
|
+
const url = this.resolveUrl(item.entryUrl);
|
|
403
|
+
this.log("debug", `Loading component ${item.name}@${item.version}`);
|
|
404
|
+
try {
|
|
405
|
+
const response = await this.fetchWithTimeout(url);
|
|
406
|
+
if (!response.ok) {
|
|
407
|
+
throw new ComponentLoadError(
|
|
408
|
+
item.name,
|
|
409
|
+
item.version,
|
|
410
|
+
`Failed to fetch component: ${response.status} ${response.statusText}`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const content = await response.text();
|
|
414
|
+
if (this.options.enableSRI && item.integrity) {
|
|
415
|
+
await this.validateIntegrity(item, content);
|
|
416
|
+
}
|
|
417
|
+
const Component = await this.executeScript(content, item);
|
|
418
|
+
const loadTime = performance.now() - startTime;
|
|
419
|
+
this.log("info", `Component ${item.name}@${item.version} loaded in ${loadTime.toFixed(2)}ms`);
|
|
420
|
+
return {
|
|
421
|
+
name: item.name,
|
|
422
|
+
version: item.version,
|
|
423
|
+
Component,
|
|
424
|
+
loadTime
|
|
425
|
+
};
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (error instanceof ComponentLoadError || error instanceof IntegrityError || error instanceof ComponentBlockedError) {
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
throw new ComponentLoadError(
|
|
431
|
+
item.name,
|
|
432
|
+
item.version,
|
|
433
|
+
`Failed to load component: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async fetchWithTimeout(url) {
|
|
438
|
+
const controller = new AbortController();
|
|
439
|
+
const timeout = setTimeout(() => controller.abort(), this.options.timeout);
|
|
440
|
+
try {
|
|
441
|
+
return await fetch(url, {
|
|
442
|
+
signal: controller.signal,
|
|
443
|
+
credentials: "omit"
|
|
444
|
+
});
|
|
445
|
+
} finally {
|
|
446
|
+
clearTimeout(timeout);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async validateIntegrity(item, content) {
|
|
450
|
+
if (!item.integrity) return;
|
|
451
|
+
const [algorithm, expectedHash] = item.integrity.split("-");
|
|
452
|
+
if (!algorithm || !expectedHash) {
|
|
453
|
+
throw new IntegrityError(item.name, item.version, item.integrity, "Invalid format");
|
|
454
|
+
}
|
|
455
|
+
const hashBuffer = await crypto.subtle.digest(
|
|
456
|
+
algorithm.toUpperCase(),
|
|
457
|
+
new TextEncoder().encode(content)
|
|
458
|
+
);
|
|
459
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
460
|
+
const actualHash = btoa(String.fromCharCode(...hashArray));
|
|
461
|
+
if (actualHash !== expectedHash) {
|
|
462
|
+
throw new IntegrityError(item.name, item.version, expectedHash, actualHash);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async executeScript(content, item) {
|
|
466
|
+
const blob = new Blob([content], { type: "application/javascript" });
|
|
467
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
468
|
+
try {
|
|
469
|
+
const module2 = await import(
|
|
470
|
+
/* @vite-ignore */
|
|
471
|
+
blobUrl
|
|
472
|
+
);
|
|
473
|
+
const Component = module2.default || module2[item.name] || module2.Component;
|
|
474
|
+
if (!Component) {
|
|
475
|
+
throw new ComponentLoadError(
|
|
476
|
+
item.name,
|
|
477
|
+
item.version,
|
|
478
|
+
"Component module does not export a valid component"
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return Component;
|
|
482
|
+
} finally {
|
|
483
|
+
URL.revokeObjectURL(blobUrl);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
resolveUrl(entryUrl) {
|
|
487
|
+
if (entryUrl.startsWith("http://") || entryUrl.startsWith("https://")) {
|
|
488
|
+
return entryUrl;
|
|
489
|
+
}
|
|
490
|
+
return `${this.options.cdnBaseUrl}/${entryUrl.replace(/^\//, "")}`;
|
|
491
|
+
}
|
|
492
|
+
getComponentKey(name, version) {
|
|
493
|
+
return `${name}@${version}`;
|
|
494
|
+
}
|
|
495
|
+
log(level, message) {
|
|
496
|
+
if (this.options.logger) {
|
|
497
|
+
this.options.logger[level](message);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// src/loader/asset-loader.ts
|
|
503
|
+
var AssetLoader = class {
|
|
504
|
+
constructor(options) {
|
|
505
|
+
this.preconnectedHosts = /* @__PURE__ */ new Set();
|
|
506
|
+
this.preloadedAssets = /* @__PURE__ */ new Set();
|
|
507
|
+
this.options = options;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* 预连接所有域名
|
|
511
|
+
*/
|
|
512
|
+
preconnectAll() {
|
|
513
|
+
const allHosts = [...this.options.cdnHosts, ...this.options.apiHosts];
|
|
514
|
+
allHosts.forEach((host) => this.preconnect(host));
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* 预连接单个域名
|
|
518
|
+
*/
|
|
519
|
+
preconnect(host) {
|
|
520
|
+
if (this.preconnectedHosts.has(host)) return;
|
|
521
|
+
const link = document.createElement("link");
|
|
522
|
+
link.rel = "preconnect";
|
|
523
|
+
link.href = host.startsWith("http") ? host : `https://${host}`;
|
|
524
|
+
link.crossOrigin = "anonymous";
|
|
525
|
+
document.head.appendChild(link);
|
|
526
|
+
this.preconnectedHosts.add(host);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* DNS 预解析
|
|
530
|
+
*/
|
|
531
|
+
dnsPrefetch(host) {
|
|
532
|
+
const link = document.createElement("link");
|
|
533
|
+
link.rel = "dns-prefetch";
|
|
534
|
+
link.href = host.startsWith("http") ? host : `https://${host}`;
|
|
535
|
+
document.head.appendChild(link);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* 预加载脚本
|
|
539
|
+
*/
|
|
540
|
+
preloadScript(url, integrity) {
|
|
541
|
+
if (this.preloadedAssets.has(url)) return;
|
|
542
|
+
const link = document.createElement("link");
|
|
543
|
+
link.rel = "preload";
|
|
544
|
+
link.as = "script";
|
|
545
|
+
link.href = url;
|
|
546
|
+
if (integrity) {
|
|
547
|
+
link.integrity = integrity;
|
|
548
|
+
link.crossOrigin = "anonymous";
|
|
549
|
+
}
|
|
550
|
+
document.head.appendChild(link);
|
|
551
|
+
this.preloadedAssets.add(url);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* 预加载样式
|
|
555
|
+
*/
|
|
556
|
+
preloadStyle(url, integrity) {
|
|
557
|
+
if (this.preloadedAssets.has(url)) return;
|
|
558
|
+
const link = document.createElement("link");
|
|
559
|
+
link.rel = "preload";
|
|
560
|
+
link.as = "style";
|
|
561
|
+
link.href = url;
|
|
562
|
+
if (integrity) {
|
|
563
|
+
link.integrity = integrity;
|
|
564
|
+
link.crossOrigin = "anonymous";
|
|
565
|
+
}
|
|
566
|
+
document.head.appendChild(link);
|
|
567
|
+
this.preloadedAssets.add(url);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* 预加载图片
|
|
571
|
+
*/
|
|
572
|
+
preloadImage(url) {
|
|
573
|
+
if (this.preloadedAssets.has(url)) return;
|
|
574
|
+
const link = document.createElement("link");
|
|
575
|
+
link.rel = "preload";
|
|
576
|
+
link.as = "image";
|
|
577
|
+
link.href = url;
|
|
578
|
+
document.head.appendChild(link);
|
|
579
|
+
this.preloadedAssets.add(url);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* 预获取资源(低优先级)
|
|
583
|
+
*/
|
|
584
|
+
prefetch(url, as) {
|
|
585
|
+
const link = document.createElement("link");
|
|
586
|
+
link.rel = "prefetch";
|
|
587
|
+
link.href = url;
|
|
588
|
+
if (as) link.as = as;
|
|
589
|
+
document.head.appendChild(link);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* 加载样式表
|
|
593
|
+
*/
|
|
594
|
+
loadStylesheet(url, integrity) {
|
|
595
|
+
return new Promise((resolve, reject) => {
|
|
596
|
+
const link = document.createElement("link");
|
|
597
|
+
link.rel = "stylesheet";
|
|
598
|
+
link.href = url;
|
|
599
|
+
if (integrity) {
|
|
600
|
+
link.integrity = integrity;
|
|
601
|
+
link.crossOrigin = "anonymous";
|
|
602
|
+
}
|
|
603
|
+
link.onload = () => resolve();
|
|
604
|
+
link.onerror = () => reject(new Error(`Failed to load stylesheet: ${url}`));
|
|
605
|
+
document.head.appendChild(link);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* 加载脚本
|
|
610
|
+
*/
|
|
611
|
+
loadScript(url, integrity) {
|
|
612
|
+
return new Promise((resolve, reject) => {
|
|
613
|
+
const script = document.createElement("script");
|
|
614
|
+
script.src = url;
|
|
615
|
+
script.async = true;
|
|
616
|
+
if (integrity) {
|
|
617
|
+
script.integrity = integrity;
|
|
618
|
+
script.crossOrigin = "anonymous";
|
|
619
|
+
}
|
|
620
|
+
script.onload = () => resolve();
|
|
621
|
+
script.onerror = () => reject(new Error(`Failed to load script: ${url}`));
|
|
622
|
+
document.body.appendChild(script);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// src/state/state-manager.ts
|
|
628
|
+
var StateManager = class {
|
|
629
|
+
constructor() {
|
|
630
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
631
|
+
this.state = this.createInitialState();
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* 获取当前状态
|
|
635
|
+
*/
|
|
636
|
+
getState() {
|
|
637
|
+
return this.state;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* 获取当前阶段
|
|
641
|
+
*/
|
|
642
|
+
getPhase() {
|
|
643
|
+
return this.state.phase;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* 设置阶段
|
|
647
|
+
*/
|
|
648
|
+
setPhase(phase) {
|
|
649
|
+
this.setState({ phase });
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* 设置页面数据
|
|
653
|
+
*/
|
|
654
|
+
setPage(page) {
|
|
655
|
+
this.setState({
|
|
656
|
+
page,
|
|
657
|
+
variables: page.pageJson.page.variables || {}
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* 设置错误
|
|
662
|
+
*/
|
|
663
|
+
setError(error) {
|
|
664
|
+
this.setState({
|
|
665
|
+
phase: "error",
|
|
666
|
+
error
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* 获取变量值
|
|
671
|
+
*/
|
|
672
|
+
getVariable(key) {
|
|
673
|
+
return this.state.variables[key];
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* 设置变量值
|
|
677
|
+
*/
|
|
678
|
+
setVariable(key, value) {
|
|
679
|
+
this.setState({
|
|
680
|
+
variables: {
|
|
681
|
+
...this.state.variables,
|
|
682
|
+
[key]: value
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* 批量设置变量
|
|
688
|
+
*/
|
|
689
|
+
setVariables(variables) {
|
|
690
|
+
this.setState({
|
|
691
|
+
variables: {
|
|
692
|
+
...this.state.variables,
|
|
693
|
+
...variables
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* 获取查询结果
|
|
699
|
+
*/
|
|
700
|
+
getQuery(queryId) {
|
|
701
|
+
return this.state.queries[queryId];
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* 设置查询结果
|
|
705
|
+
*/
|
|
706
|
+
setQuery(queryId, data) {
|
|
707
|
+
this.setState({
|
|
708
|
+
queries: {
|
|
709
|
+
...this.state.queries,
|
|
710
|
+
[queryId]: data
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* 更新组件加载状态
|
|
716
|
+
*/
|
|
717
|
+
setComponentStatus(key, result) {
|
|
718
|
+
const components = new Map(this.state.components);
|
|
719
|
+
components.set(key, result);
|
|
720
|
+
this.setState({ components });
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* 标记为已销毁
|
|
724
|
+
*/
|
|
725
|
+
setDestroyed() {
|
|
726
|
+
this.setState({ destroyed: true });
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* 订阅状态变更
|
|
730
|
+
*/
|
|
731
|
+
subscribe(listener) {
|
|
732
|
+
this.listeners.add(listener);
|
|
733
|
+
return () => {
|
|
734
|
+
this.listeners.delete(listener);
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* 重置状态
|
|
739
|
+
*/
|
|
740
|
+
reset() {
|
|
741
|
+
this.state = this.createInitialState();
|
|
742
|
+
this.notifyListeners();
|
|
743
|
+
}
|
|
744
|
+
setState(partial) {
|
|
745
|
+
this.state = { ...this.state, ...partial };
|
|
746
|
+
this.notifyListeners();
|
|
747
|
+
}
|
|
748
|
+
notifyListeners() {
|
|
749
|
+
this.listeners.forEach((listener) => {
|
|
750
|
+
try {
|
|
751
|
+
listener(this.state);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
console.error("State listener error:", error);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
createInitialState() {
|
|
758
|
+
return {
|
|
759
|
+
phase: "idle",
|
|
760
|
+
page: null,
|
|
761
|
+
variables: {},
|
|
762
|
+
queries: {},
|
|
763
|
+
components: /* @__PURE__ */ new Map(),
|
|
764
|
+
error: null,
|
|
765
|
+
destroyed: false
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
// src/events/event-bus.ts
|
|
771
|
+
var EventBus = class {
|
|
772
|
+
constructor(options = {}) {
|
|
773
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
774
|
+
this.options = {
|
|
775
|
+
debug: false,
|
|
776
|
+
maxListeners: 100,
|
|
777
|
+
...options
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* 发送事件
|
|
782
|
+
*/
|
|
783
|
+
emit(event) {
|
|
784
|
+
if (this.options.debug) {
|
|
785
|
+
this.log("debug", `Event emitted: ${event.type}`, event);
|
|
786
|
+
}
|
|
787
|
+
const handlers = this.handlers.get(event.type);
|
|
788
|
+
if (!handlers) return;
|
|
789
|
+
handlers.forEach((handler) => {
|
|
790
|
+
try {
|
|
791
|
+
handler(event);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
this.log("error", `Error in event handler for ${event.type}:`, error);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* 订阅事件
|
|
799
|
+
*/
|
|
800
|
+
on(type, handler) {
|
|
801
|
+
let handlers = this.handlers.get(type);
|
|
802
|
+
if (!handlers) {
|
|
803
|
+
handlers = /* @__PURE__ */ new Set();
|
|
804
|
+
this.handlers.set(type, handlers);
|
|
805
|
+
}
|
|
806
|
+
if (handlers.size >= (this.options.maxListeners ?? 100)) {
|
|
807
|
+
this.log("warn", `Max listeners (${this.options.maxListeners}) reached for event: ${type}`);
|
|
808
|
+
}
|
|
809
|
+
handlers.add(handler);
|
|
810
|
+
return () => {
|
|
811
|
+
handlers?.delete(handler);
|
|
812
|
+
if (handlers?.size === 0) {
|
|
813
|
+
this.handlers.delete(type);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* 取消订阅
|
|
819
|
+
*/
|
|
820
|
+
off(type, handler) {
|
|
821
|
+
const handlers = this.handlers.get(type);
|
|
822
|
+
if (handlers) {
|
|
823
|
+
handlers.delete(handler);
|
|
824
|
+
if (handlers.size === 0) {
|
|
825
|
+
this.handlers.delete(type);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* 一次性订阅
|
|
831
|
+
*/
|
|
832
|
+
once(type, handler) {
|
|
833
|
+
const wrappedHandler = (event) => {
|
|
834
|
+
unsubscribe();
|
|
835
|
+
handler(event);
|
|
836
|
+
};
|
|
837
|
+
const unsubscribe = this.on(type, wrappedHandler);
|
|
838
|
+
return unsubscribe;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* 清除所有监听器
|
|
842
|
+
*/
|
|
843
|
+
clear(type) {
|
|
844
|
+
if (type) {
|
|
845
|
+
this.handlers.delete(type);
|
|
846
|
+
} else {
|
|
847
|
+
this.handlers.clear();
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* 获取监听器数量
|
|
852
|
+
*/
|
|
853
|
+
listenerCount(type) {
|
|
854
|
+
return this.handlers.get(type)?.size ?? 0;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* 创建事件
|
|
858
|
+
*/
|
|
859
|
+
static createEvent(type, data, traceId) {
|
|
860
|
+
return {
|
|
861
|
+
type,
|
|
862
|
+
data,
|
|
863
|
+
timestamp: Date.now(),
|
|
864
|
+
traceId
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
log(level, message, ...args) {
|
|
868
|
+
if (this.options.logger) {
|
|
869
|
+
this.options.logger[level](message, ...args);
|
|
870
|
+
} else if (this.options.debug) {
|
|
871
|
+
console[level](`[EventBus] ${message}`, ...args);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
// src/events/action-bridge.ts
|
|
877
|
+
var ActionBridge = class {
|
|
878
|
+
constructor(options) {
|
|
879
|
+
this.debounceTimers = /* @__PURE__ */ new Map();
|
|
880
|
+
this.throttleTimers = /* @__PURE__ */ new Map();
|
|
881
|
+
this.options = options;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* 处理组件事件
|
|
885
|
+
*/
|
|
886
|
+
async handleEvent(event, eventData, context) {
|
|
887
|
+
if (event.enabled === false) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (event.condition) {
|
|
891
|
+
this.log("debug", `Event condition: ${event.condition}`);
|
|
892
|
+
}
|
|
893
|
+
if (event.debounce && event.debounce > 0) {
|
|
894
|
+
await this.debounce(event.id, event.debounce);
|
|
895
|
+
}
|
|
896
|
+
if (event.throttle && event.throttle > 0) {
|
|
897
|
+
if (!this.throttle(event.id, event.throttle)) {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
for (const action of event.actions) {
|
|
902
|
+
if (action.enabled === false) continue;
|
|
903
|
+
try {
|
|
904
|
+
if (action.delay && action.delay > 0) {
|
|
905
|
+
await this.delay(action.delay);
|
|
906
|
+
}
|
|
907
|
+
const params = this.resolveParams(action.params, eventData, context);
|
|
908
|
+
await this.executeAction(action, params);
|
|
909
|
+
} catch (error) {
|
|
910
|
+
this.log("error", `Action ${action.type} failed:`, error);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* 销毁
|
|
916
|
+
*/
|
|
917
|
+
destroy() {
|
|
918
|
+
this.debounceTimers.forEach((timer) => clearTimeout(timer));
|
|
919
|
+
this.debounceTimers.clear();
|
|
920
|
+
this.throttleTimers.clear();
|
|
921
|
+
}
|
|
922
|
+
async executeAction(action, params) {
|
|
923
|
+
this.log("debug", `Executing action: ${action.type}`, params);
|
|
924
|
+
const result = await this.options.executor.execute(action.type, params);
|
|
925
|
+
this.log("debug", `Action ${action.type} completed:`, result);
|
|
926
|
+
}
|
|
927
|
+
resolveParams(params, eventData, context) {
|
|
928
|
+
return {
|
|
929
|
+
...params,
|
|
930
|
+
__eventData: eventData,
|
|
931
|
+
__context: context
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
debounce(id, delay) {
|
|
935
|
+
return new Promise((resolve) => {
|
|
936
|
+
const existing = this.debounceTimers.get(id);
|
|
937
|
+
if (existing) {
|
|
938
|
+
clearTimeout(existing);
|
|
939
|
+
}
|
|
940
|
+
const timer = setTimeout(() => {
|
|
941
|
+
this.debounceTimers.delete(id);
|
|
942
|
+
resolve();
|
|
943
|
+
}, delay);
|
|
944
|
+
this.debounceTimers.set(id, timer);
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
throttle(id, interval) {
|
|
948
|
+
const now = Date.now();
|
|
949
|
+
const lastTime = this.throttleTimers.get(id) ?? 0;
|
|
950
|
+
if (now - lastTime < interval) {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
this.throttleTimers.set(id, now);
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
delay(ms) {
|
|
957
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
958
|
+
}
|
|
959
|
+
log(level, message, ...args) {
|
|
960
|
+
if (this.options.logger) {
|
|
961
|
+
this.options.logger[level](message, ...args);
|
|
962
|
+
} else if (this.options.debug) {
|
|
963
|
+
console[level](`[ActionBridge] ${message}`, ...args);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// src/expression/lexer.ts
|
|
969
|
+
var Lexer = class {
|
|
970
|
+
constructor(input) {
|
|
971
|
+
this.pos = 0;
|
|
972
|
+
this.tokens = [];
|
|
973
|
+
this.input = input;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* 分析表达式,返回 Token 列表
|
|
977
|
+
*/
|
|
978
|
+
tokenize() {
|
|
979
|
+
this.pos = 0;
|
|
980
|
+
this.tokens = [];
|
|
981
|
+
while (this.pos < this.input.length) {
|
|
982
|
+
this.skipWhitespace();
|
|
983
|
+
if (this.pos >= this.input.length) break;
|
|
984
|
+
const token = this.readToken();
|
|
985
|
+
if (token) {
|
|
986
|
+
this.tokens.push(token);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
this.tokens.push({
|
|
990
|
+
type: "EOF",
|
|
991
|
+
value: null,
|
|
992
|
+
start: this.input.length,
|
|
993
|
+
end: this.input.length
|
|
994
|
+
});
|
|
995
|
+
return this.tokens;
|
|
996
|
+
}
|
|
997
|
+
readToken() {
|
|
998
|
+
const char = this.input[this.pos];
|
|
999
|
+
const start = this.pos;
|
|
1000
|
+
if (this.isDigit(char) || char === "-" && this.isDigit(this.peek(1))) {
|
|
1001
|
+
return this.readNumber();
|
|
1002
|
+
}
|
|
1003
|
+
if (char === '"' || char === "'") {
|
|
1004
|
+
return this.readString(char);
|
|
1005
|
+
}
|
|
1006
|
+
if (this.isIdentifierStart(char)) {
|
|
1007
|
+
return this.readIdentifier();
|
|
1008
|
+
}
|
|
1009
|
+
const operator = this.readOperator();
|
|
1010
|
+
if (operator) {
|
|
1011
|
+
return operator;
|
|
1012
|
+
}
|
|
1013
|
+
switch (char) {
|
|
1014
|
+
case ".":
|
|
1015
|
+
this.pos++;
|
|
1016
|
+
return { type: "DOT", value: ".", start, end: this.pos };
|
|
1017
|
+
case "[":
|
|
1018
|
+
this.pos++;
|
|
1019
|
+
return { type: "LBRACKET", value: "[", start, end: this.pos };
|
|
1020
|
+
case "]":
|
|
1021
|
+
this.pos++;
|
|
1022
|
+
return { type: "RBRACKET", value: "]", start, end: this.pos };
|
|
1023
|
+
case "(":
|
|
1024
|
+
this.pos++;
|
|
1025
|
+
return { type: "LPAREN", value: "(", start, end: this.pos };
|
|
1026
|
+
case ")":
|
|
1027
|
+
this.pos++;
|
|
1028
|
+
return { type: "RPAREN", value: ")", start, end: this.pos };
|
|
1029
|
+
case ",":
|
|
1030
|
+
this.pos++;
|
|
1031
|
+
return { type: "COMMA", value: ",", start, end: this.pos };
|
|
1032
|
+
case "?":
|
|
1033
|
+
this.pos++;
|
|
1034
|
+
return { type: "QUESTION", value: "?", start, end: this.pos };
|
|
1035
|
+
case ":":
|
|
1036
|
+
this.pos++;
|
|
1037
|
+
return { type: "COLON", value: ":", start, end: this.pos };
|
|
1038
|
+
}
|
|
1039
|
+
throw new Error(`Unexpected character '${char}' at position ${this.pos}`);
|
|
1040
|
+
}
|
|
1041
|
+
readNumber() {
|
|
1042
|
+
const start = this.pos;
|
|
1043
|
+
let value = "";
|
|
1044
|
+
if (this.input[this.pos] === "-") {
|
|
1045
|
+
value += "-";
|
|
1046
|
+
this.pos++;
|
|
1047
|
+
}
|
|
1048
|
+
while (this.isDigit(this.input[this.pos])) {
|
|
1049
|
+
value += this.input[this.pos];
|
|
1050
|
+
this.pos++;
|
|
1051
|
+
}
|
|
1052
|
+
if (this.input[this.pos] === "." && this.isDigit(this.peek(1))) {
|
|
1053
|
+
value += ".";
|
|
1054
|
+
this.pos++;
|
|
1055
|
+
while (this.isDigit(this.input[this.pos])) {
|
|
1056
|
+
value += this.input[this.pos];
|
|
1057
|
+
this.pos++;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
type: "NUMBER",
|
|
1062
|
+
value: parseFloat(value),
|
|
1063
|
+
start,
|
|
1064
|
+
end: this.pos
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
readString(quote) {
|
|
1068
|
+
const start = this.pos;
|
|
1069
|
+
this.pos++;
|
|
1070
|
+
let value = "";
|
|
1071
|
+
while (this.pos < this.input.length && this.input[this.pos] !== quote) {
|
|
1072
|
+
if (this.input[this.pos] === "\\") {
|
|
1073
|
+
this.pos++;
|
|
1074
|
+
const escaped = this.input[this.pos];
|
|
1075
|
+
switch (escaped) {
|
|
1076
|
+
case "n":
|
|
1077
|
+
value += "\n";
|
|
1078
|
+
break;
|
|
1079
|
+
case "t":
|
|
1080
|
+
value += " ";
|
|
1081
|
+
break;
|
|
1082
|
+
case "r":
|
|
1083
|
+
value += "\r";
|
|
1084
|
+
break;
|
|
1085
|
+
case "\\":
|
|
1086
|
+
value += "\\";
|
|
1087
|
+
break;
|
|
1088
|
+
case '"':
|
|
1089
|
+
value += '"';
|
|
1090
|
+
break;
|
|
1091
|
+
case "'":
|
|
1092
|
+
value += "'";
|
|
1093
|
+
break;
|
|
1094
|
+
default:
|
|
1095
|
+
value += escaped;
|
|
1096
|
+
}
|
|
1097
|
+
} else {
|
|
1098
|
+
value += this.input[this.pos];
|
|
1099
|
+
}
|
|
1100
|
+
this.pos++;
|
|
1101
|
+
}
|
|
1102
|
+
if (this.input[this.pos] !== quote) {
|
|
1103
|
+
throw new Error(`Unterminated string at position ${start}`);
|
|
1104
|
+
}
|
|
1105
|
+
this.pos++;
|
|
1106
|
+
return {
|
|
1107
|
+
type: "STRING",
|
|
1108
|
+
value,
|
|
1109
|
+
start,
|
|
1110
|
+
end: this.pos
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
readIdentifier() {
|
|
1114
|
+
const start = this.pos;
|
|
1115
|
+
let value = "";
|
|
1116
|
+
while (this.pos < this.input.length && this.isIdentifierChar(this.input[this.pos])) {
|
|
1117
|
+
value += this.input[this.pos];
|
|
1118
|
+
this.pos++;
|
|
1119
|
+
}
|
|
1120
|
+
if (value === "true") {
|
|
1121
|
+
return { type: "BOOLEAN", value: true, start, end: this.pos };
|
|
1122
|
+
}
|
|
1123
|
+
if (value === "false") {
|
|
1124
|
+
return { type: "BOOLEAN", value: false, start, end: this.pos };
|
|
1125
|
+
}
|
|
1126
|
+
if (value === "null") {
|
|
1127
|
+
return { type: "NULL", value: null, start, end: this.pos };
|
|
1128
|
+
}
|
|
1129
|
+
return { type: "IDENTIFIER", value, start, end: this.pos };
|
|
1130
|
+
}
|
|
1131
|
+
readOperator() {
|
|
1132
|
+
const start = this.pos;
|
|
1133
|
+
const twoChar = this.input.slice(this.pos, this.pos + 2);
|
|
1134
|
+
const oneChar = this.input[this.pos];
|
|
1135
|
+
const twoCharOps = ["==", "!=", ">=", "<=", "&&", "||", "??"];
|
|
1136
|
+
if (twoCharOps.includes(twoChar)) {
|
|
1137
|
+
this.pos += 2;
|
|
1138
|
+
return { type: "OPERATOR", value: twoChar, start, end: this.pos };
|
|
1139
|
+
}
|
|
1140
|
+
const oneCharOps = ["+", "-", "*", "/", "%", ">", "<", "!"];
|
|
1141
|
+
if (oneCharOps.includes(oneChar)) {
|
|
1142
|
+
this.pos++;
|
|
1143
|
+
return { type: "OPERATOR", value: oneChar, start, end: this.pos };
|
|
1144
|
+
}
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
skipWhitespace() {
|
|
1148
|
+
while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) {
|
|
1149
|
+
this.pos++;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
isDigit(char) {
|
|
1153
|
+
return /[0-9]/.test(char);
|
|
1154
|
+
}
|
|
1155
|
+
isIdentifierStart(char) {
|
|
1156
|
+
return /[a-zA-Z_$]/.test(char);
|
|
1157
|
+
}
|
|
1158
|
+
isIdentifierChar(char) {
|
|
1159
|
+
return /[a-zA-Z0-9_$]/.test(char);
|
|
1160
|
+
}
|
|
1161
|
+
peek(offset = 1) {
|
|
1162
|
+
return this.input[this.pos + offset] || "";
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
// src/expression/parser.ts
|
|
1167
|
+
var import_contracts_types2 = require("@djvlc/contracts-types");
|
|
1168
|
+
var PRECEDENCE = {
|
|
1169
|
+
"||": 1,
|
|
1170
|
+
"??": 1,
|
|
1171
|
+
"&&": 2,
|
|
1172
|
+
"==": 3,
|
|
1173
|
+
"!=": 3,
|
|
1174
|
+
"<": 4,
|
|
1175
|
+
">": 4,
|
|
1176
|
+
"<=": 4,
|
|
1177
|
+
">=": 4,
|
|
1178
|
+
"+": 5,
|
|
1179
|
+
"-": 5,
|
|
1180
|
+
"*": 6,
|
|
1181
|
+
"/": 6,
|
|
1182
|
+
"%": 6
|
|
1183
|
+
};
|
|
1184
|
+
var Parser = class {
|
|
1185
|
+
constructor(tokens) {
|
|
1186
|
+
this.pos = 0;
|
|
1187
|
+
this.tokens = tokens;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* 解析表达式
|
|
1191
|
+
*/
|
|
1192
|
+
parse() {
|
|
1193
|
+
const result = this.parseExpression();
|
|
1194
|
+
if (this.current().type !== "EOF") {
|
|
1195
|
+
throw new Error(`Unexpected token '${this.current().value}' at position ${this.current().start}`);
|
|
1196
|
+
}
|
|
1197
|
+
return result;
|
|
1198
|
+
}
|
|
1199
|
+
parseExpression() {
|
|
1200
|
+
return this.parseTernary();
|
|
1201
|
+
}
|
|
1202
|
+
parseTernary() {
|
|
1203
|
+
const test = this.parseBinary(0);
|
|
1204
|
+
if (this.current().type === "QUESTION") {
|
|
1205
|
+
this.advance();
|
|
1206
|
+
const consequent = this.parseExpression();
|
|
1207
|
+
this.expect("COLON");
|
|
1208
|
+
const alternate = this.parseExpression();
|
|
1209
|
+
return {
|
|
1210
|
+
type: "conditional",
|
|
1211
|
+
test,
|
|
1212
|
+
consequent,
|
|
1213
|
+
alternate,
|
|
1214
|
+
raw: void 0
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
return test;
|
|
1218
|
+
}
|
|
1219
|
+
parseBinary(minPrecedence) {
|
|
1220
|
+
let left = this.parseUnary();
|
|
1221
|
+
while (true) {
|
|
1222
|
+
const token = this.current();
|
|
1223
|
+
if (token.type !== "OPERATOR") break;
|
|
1224
|
+
const precedence = PRECEDENCE[token.value];
|
|
1225
|
+
if (precedence === void 0 || precedence < minPrecedence) break;
|
|
1226
|
+
this.advance();
|
|
1227
|
+
const right = this.parseBinary(precedence + 1);
|
|
1228
|
+
left = {
|
|
1229
|
+
type: "binary",
|
|
1230
|
+
operator: token.value,
|
|
1231
|
+
left,
|
|
1232
|
+
right,
|
|
1233
|
+
raw: void 0
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
return left;
|
|
1237
|
+
}
|
|
1238
|
+
parseUnary() {
|
|
1239
|
+
const token = this.current();
|
|
1240
|
+
if (token.type === "OPERATOR" && (token.value === "!" || token.value === "-")) {
|
|
1241
|
+
this.advance();
|
|
1242
|
+
const argument = this.parseUnary();
|
|
1243
|
+
return {
|
|
1244
|
+
type: "unary",
|
|
1245
|
+
operator: token.value,
|
|
1246
|
+
argument,
|
|
1247
|
+
raw: void 0
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
return this.parsePostfix();
|
|
1251
|
+
}
|
|
1252
|
+
parsePostfix() {
|
|
1253
|
+
let node = this.parsePrimary();
|
|
1254
|
+
while (true) {
|
|
1255
|
+
const token = this.current();
|
|
1256
|
+
if (token.type === "DOT") {
|
|
1257
|
+
this.advance();
|
|
1258
|
+
const propToken = this.expect("IDENTIFIER");
|
|
1259
|
+
node = {
|
|
1260
|
+
type: "member",
|
|
1261
|
+
object: node,
|
|
1262
|
+
property: propToken.value,
|
|
1263
|
+
computed: false,
|
|
1264
|
+
raw: void 0
|
|
1265
|
+
};
|
|
1266
|
+
} else if (token.type === "LBRACKET") {
|
|
1267
|
+
this.advance();
|
|
1268
|
+
const index = this.parseExpression();
|
|
1269
|
+
this.expect("RBRACKET");
|
|
1270
|
+
node = {
|
|
1271
|
+
type: "member",
|
|
1272
|
+
object: node,
|
|
1273
|
+
property: index,
|
|
1274
|
+
computed: true,
|
|
1275
|
+
raw: void 0
|
|
1276
|
+
};
|
|
1277
|
+
} else if (token.type === "LPAREN" && node.type === "identifier") {
|
|
1278
|
+
const callee = node.name;
|
|
1279
|
+
if (!import_contracts_types2.BUILTIN_FUNCTION_NAMES.has(callee)) {
|
|
1280
|
+
throw new Error(`Unknown function '${callee}' at position ${token.start}`);
|
|
1281
|
+
}
|
|
1282
|
+
this.advance();
|
|
1283
|
+
const args = this.parseArguments();
|
|
1284
|
+
this.expect("RPAREN");
|
|
1285
|
+
node = {
|
|
1286
|
+
type: "call",
|
|
1287
|
+
callee,
|
|
1288
|
+
arguments: args,
|
|
1289
|
+
raw: void 0
|
|
1290
|
+
};
|
|
1291
|
+
} else {
|
|
1292
|
+
break;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return node;
|
|
1296
|
+
}
|
|
1297
|
+
parsePrimary() {
|
|
1298
|
+
const token = this.current();
|
|
1299
|
+
if (token.type === "NUMBER" || token.type === "STRING" || token.type === "BOOLEAN" || token.type === "NULL") {
|
|
1300
|
+
this.advance();
|
|
1301
|
+
return {
|
|
1302
|
+
type: "literal",
|
|
1303
|
+
value: token.value,
|
|
1304
|
+
start: token.start,
|
|
1305
|
+
end: token.end,
|
|
1306
|
+
raw: void 0
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
if (token.type === "IDENTIFIER") {
|
|
1310
|
+
this.advance();
|
|
1311
|
+
return {
|
|
1312
|
+
type: "identifier",
|
|
1313
|
+
name: token.value,
|
|
1314
|
+
start: token.start,
|
|
1315
|
+
end: token.end,
|
|
1316
|
+
raw: void 0
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
if (token.type === "LBRACKET") {
|
|
1320
|
+
return this.parseArray();
|
|
1321
|
+
}
|
|
1322
|
+
if (token.type === "LPAREN") {
|
|
1323
|
+
this.advance();
|
|
1324
|
+
const expr = this.parseExpression();
|
|
1325
|
+
this.expect("RPAREN");
|
|
1326
|
+
return expr;
|
|
1327
|
+
}
|
|
1328
|
+
throw new Error(`Unexpected token '${token.value}' at position ${token.start}`);
|
|
1329
|
+
}
|
|
1330
|
+
parseArray() {
|
|
1331
|
+
const start = this.current().start;
|
|
1332
|
+
this.advance();
|
|
1333
|
+
const elements = [];
|
|
1334
|
+
while (this.current().type !== "RBRACKET") {
|
|
1335
|
+
elements.push(this.parseExpression());
|
|
1336
|
+
if (this.current().type === "COMMA") {
|
|
1337
|
+
this.advance();
|
|
1338
|
+
} else {
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
const end = this.current().end;
|
|
1343
|
+
this.expect("RBRACKET");
|
|
1344
|
+
return {
|
|
1345
|
+
type: "array",
|
|
1346
|
+
elements,
|
|
1347
|
+
start,
|
|
1348
|
+
end,
|
|
1349
|
+
raw: void 0
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
parseArguments() {
|
|
1353
|
+
const args = [];
|
|
1354
|
+
if (this.current().type !== "RPAREN") {
|
|
1355
|
+
args.push(this.parseExpression());
|
|
1356
|
+
while (this.current().type === "COMMA") {
|
|
1357
|
+
this.advance();
|
|
1358
|
+
args.push(this.parseExpression());
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return args;
|
|
1362
|
+
}
|
|
1363
|
+
current() {
|
|
1364
|
+
return this.tokens[this.pos];
|
|
1365
|
+
}
|
|
1366
|
+
advance() {
|
|
1367
|
+
return this.tokens[this.pos++];
|
|
1368
|
+
}
|
|
1369
|
+
expect(type) {
|
|
1370
|
+
const token = this.current();
|
|
1371
|
+
if (token.type !== type) {
|
|
1372
|
+
throw new Error(`Expected '${type}' but got '${token.type}' at position ${token.start}`);
|
|
1373
|
+
}
|
|
1374
|
+
return this.advance();
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
// src/expression/functions.ts
|
|
1379
|
+
var builtinFunctions = {
|
|
1380
|
+
// ==================== 字符串函数 ====================
|
|
1381
|
+
len: (value) => {
|
|
1382
|
+
if (typeof value === "string" || Array.isArray(value)) {
|
|
1383
|
+
return value.length;
|
|
1384
|
+
}
|
|
1385
|
+
return 0;
|
|
1386
|
+
},
|
|
1387
|
+
trim: (str) => {
|
|
1388
|
+
return String(str ?? "").trim();
|
|
1389
|
+
},
|
|
1390
|
+
upper: (str) => {
|
|
1391
|
+
return String(str ?? "").toUpperCase();
|
|
1392
|
+
},
|
|
1393
|
+
lower: (str) => {
|
|
1394
|
+
return String(str ?? "").toLowerCase();
|
|
1395
|
+
},
|
|
1396
|
+
substr: (str, start, length) => {
|
|
1397
|
+
const s = String(str ?? "");
|
|
1398
|
+
const startNum = Number(start) || 0;
|
|
1399
|
+
const lengthNum = length !== void 0 ? Number(length) : void 0;
|
|
1400
|
+
return s.substring(startNum, lengthNum !== void 0 ? startNum + lengthNum : void 0);
|
|
1401
|
+
},
|
|
1402
|
+
concat: (...args) => {
|
|
1403
|
+
return args.map((a) => String(a ?? "")).join("");
|
|
1404
|
+
},
|
|
1405
|
+
replace: (str, search, replacement) => {
|
|
1406
|
+
return String(str ?? "").split(String(search)).join(String(replacement));
|
|
1407
|
+
},
|
|
1408
|
+
split: (str, separator) => {
|
|
1409
|
+
return String(str ?? "").split(String(separator));
|
|
1410
|
+
},
|
|
1411
|
+
join: (arr, separator) => {
|
|
1412
|
+
if (!Array.isArray(arr)) return "";
|
|
1413
|
+
return arr.join(separator !== void 0 ? String(separator) : ",");
|
|
1414
|
+
},
|
|
1415
|
+
startsWith: (str, search) => {
|
|
1416
|
+
return String(str ?? "").startsWith(String(search));
|
|
1417
|
+
},
|
|
1418
|
+
endsWith: (str, search) => {
|
|
1419
|
+
return String(str ?? "").endsWith(String(search));
|
|
1420
|
+
},
|
|
1421
|
+
contains: (str, search) => {
|
|
1422
|
+
return String(str ?? "").includes(String(search));
|
|
1423
|
+
},
|
|
1424
|
+
// ==================== 数字函数 ====================
|
|
1425
|
+
toNumber: (value) => {
|
|
1426
|
+
const num = Number(value);
|
|
1427
|
+
return isNaN(num) ? 0 : num;
|
|
1428
|
+
},
|
|
1429
|
+
toString: (value) => {
|
|
1430
|
+
return String(value ?? "");
|
|
1431
|
+
},
|
|
1432
|
+
toInt: (value) => {
|
|
1433
|
+
return Math.trunc(Number(value) || 0);
|
|
1434
|
+
},
|
|
1435
|
+
toFloat: (value) => {
|
|
1436
|
+
return parseFloat(String(value)) || 0;
|
|
1437
|
+
},
|
|
1438
|
+
round: (value, decimals) => {
|
|
1439
|
+
const num = Number(value) || 0;
|
|
1440
|
+
const dec = Number(decimals) || 0;
|
|
1441
|
+
const factor = Math.pow(10, dec);
|
|
1442
|
+
return Math.round(num * factor) / factor;
|
|
1443
|
+
},
|
|
1444
|
+
floor: (value) => {
|
|
1445
|
+
return Math.floor(Number(value) || 0);
|
|
1446
|
+
},
|
|
1447
|
+
ceil: (value) => {
|
|
1448
|
+
return Math.ceil(Number(value) || 0);
|
|
1449
|
+
},
|
|
1450
|
+
abs: (value) => {
|
|
1451
|
+
return Math.abs(Number(value) || 0);
|
|
1452
|
+
},
|
|
1453
|
+
min: (...args) => {
|
|
1454
|
+
const nums = args.map((a) => Number(a)).filter((n) => !isNaN(n));
|
|
1455
|
+
return nums.length > 0 ? Math.min(...nums) : 0;
|
|
1456
|
+
},
|
|
1457
|
+
max: (...args) => {
|
|
1458
|
+
const nums = args.map((a) => Number(a)).filter((n) => !isNaN(n));
|
|
1459
|
+
return nums.length > 0 ? Math.max(...nums) : 0;
|
|
1460
|
+
},
|
|
1461
|
+
sum: (arr) => {
|
|
1462
|
+
if (!Array.isArray(arr)) return 0;
|
|
1463
|
+
return arr.reduce((acc, val) => acc + (Number(val) || 0), 0);
|
|
1464
|
+
},
|
|
1465
|
+
avg: (arr) => {
|
|
1466
|
+
if (!Array.isArray(arr) || arr.length === 0) return 0;
|
|
1467
|
+
const sum = arr.reduce((acc, val) => acc + (Number(val) || 0), 0);
|
|
1468
|
+
return sum / arr.length;
|
|
1469
|
+
},
|
|
1470
|
+
random: () => {
|
|
1471
|
+
return Math.random();
|
|
1472
|
+
},
|
|
1473
|
+
randomInt: (min, max) => {
|
|
1474
|
+
const minNum = Math.ceil(Number(min) || 0);
|
|
1475
|
+
const maxNum = Math.floor(Number(max) || 100);
|
|
1476
|
+
return Math.floor(Math.random() * (maxNum - minNum + 1)) + minNum;
|
|
1477
|
+
},
|
|
1478
|
+
// ==================== 日期函数 ====================
|
|
1479
|
+
now: () => {
|
|
1480
|
+
return Date.now();
|
|
1481
|
+
},
|
|
1482
|
+
today: () => {
|
|
1483
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1484
|
+
},
|
|
1485
|
+
dateFormat: (timestamp, format) => {
|
|
1486
|
+
const date = new Date(Number(timestamp) || Date.now());
|
|
1487
|
+
const fmt = String(format || "YYYY-MM-DD");
|
|
1488
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
1489
|
+
return fmt.replace("YYYY", date.getFullYear().toString()).replace("MM", pad(date.getMonth() + 1)).replace("DD", pad(date.getDate())).replace("HH", pad(date.getHours())).replace("mm", pad(date.getMinutes())).replace("ss", pad(date.getSeconds()));
|
|
1490
|
+
},
|
|
1491
|
+
dateParse: (dateStr) => {
|
|
1492
|
+
const date = new Date(String(dateStr));
|
|
1493
|
+
return date.getTime();
|
|
1494
|
+
},
|
|
1495
|
+
year: (timestamp) => {
|
|
1496
|
+
return new Date(Number(timestamp) || Date.now()).getFullYear();
|
|
1497
|
+
},
|
|
1498
|
+
month: (timestamp) => {
|
|
1499
|
+
return new Date(Number(timestamp) || Date.now()).getMonth() + 1;
|
|
1500
|
+
},
|
|
1501
|
+
day: (timestamp) => {
|
|
1502
|
+
return new Date(Number(timestamp) || Date.now()).getDate();
|
|
1503
|
+
},
|
|
1504
|
+
addDays: (timestamp, days) => {
|
|
1505
|
+
const date = new Date(Number(timestamp) || Date.now());
|
|
1506
|
+
date.setDate(date.getDate() + (Number(days) || 0));
|
|
1507
|
+
return date.getTime();
|
|
1508
|
+
},
|
|
1509
|
+
diffDays: (timestamp1, timestamp2) => {
|
|
1510
|
+
const date1 = new Date(Number(timestamp1) || Date.now());
|
|
1511
|
+
const date2 = new Date(Number(timestamp2) || Date.now());
|
|
1512
|
+
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
|
1513
|
+
return Math.floor(diffTime / (1e3 * 60 * 60 * 24));
|
|
1514
|
+
},
|
|
1515
|
+
// ==================== 类型检查函数 ====================
|
|
1516
|
+
isNull: (value) => {
|
|
1517
|
+
return value === null || value === void 0;
|
|
1518
|
+
},
|
|
1519
|
+
isUndefined: (value) => {
|
|
1520
|
+
return value === void 0;
|
|
1521
|
+
},
|
|
1522
|
+
isEmpty: (value) => {
|
|
1523
|
+
if (value === null || value === void 0) return true;
|
|
1524
|
+
if (typeof value === "string") return value.length === 0;
|
|
1525
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
1526
|
+
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
1527
|
+
return false;
|
|
1528
|
+
},
|
|
1529
|
+
isArray: (value) => {
|
|
1530
|
+
return Array.isArray(value);
|
|
1531
|
+
},
|
|
1532
|
+
isObject: (value) => {
|
|
1533
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1534
|
+
},
|
|
1535
|
+
isString: (value) => {
|
|
1536
|
+
return typeof value === "string";
|
|
1537
|
+
},
|
|
1538
|
+
isNumber: (value) => {
|
|
1539
|
+
return typeof value === "number" && !isNaN(value);
|
|
1540
|
+
},
|
|
1541
|
+
isBoolean: (value) => {
|
|
1542
|
+
return typeof value === "boolean";
|
|
1543
|
+
},
|
|
1544
|
+
typeOf: (value) => {
|
|
1545
|
+
if (value === null) return "null";
|
|
1546
|
+
if (Array.isArray(value)) return "array";
|
|
1547
|
+
return typeof value;
|
|
1548
|
+
},
|
|
1549
|
+
// ==================== 默认值函数 ====================
|
|
1550
|
+
default: (value, defaultValue) => {
|
|
1551
|
+
return value ?? defaultValue;
|
|
1552
|
+
},
|
|
1553
|
+
coalesce: (...args) => {
|
|
1554
|
+
for (const arg of args) {
|
|
1555
|
+
if (arg !== null && arg !== void 0) return arg;
|
|
1556
|
+
}
|
|
1557
|
+
return null;
|
|
1558
|
+
},
|
|
1559
|
+
ifElse: (condition, trueValue, falseValue) => {
|
|
1560
|
+
return condition ? trueValue : falseValue;
|
|
1561
|
+
},
|
|
1562
|
+
// ==================== 数组函数 ====================
|
|
1563
|
+
first: (arr) => {
|
|
1564
|
+
if (!Array.isArray(arr)) return void 0;
|
|
1565
|
+
return arr[0];
|
|
1566
|
+
},
|
|
1567
|
+
last: (arr) => {
|
|
1568
|
+
if (!Array.isArray(arr)) return void 0;
|
|
1569
|
+
return arr[arr.length - 1];
|
|
1570
|
+
},
|
|
1571
|
+
at: (arr, index) => {
|
|
1572
|
+
if (!Array.isArray(arr)) return void 0;
|
|
1573
|
+
return arr[Number(index) || 0];
|
|
1574
|
+
},
|
|
1575
|
+
slice: (arr, start, end) => {
|
|
1576
|
+
if (!Array.isArray(arr)) return [];
|
|
1577
|
+
return arr.slice(Number(start) || 0, end !== void 0 ? Number(end) : void 0);
|
|
1578
|
+
},
|
|
1579
|
+
includes: (arr, value) => {
|
|
1580
|
+
if (!Array.isArray(arr)) return false;
|
|
1581
|
+
return arr.includes(value);
|
|
1582
|
+
},
|
|
1583
|
+
indexOf: (arr, value) => {
|
|
1584
|
+
if (!Array.isArray(arr)) return -1;
|
|
1585
|
+
return arr.indexOf(value);
|
|
1586
|
+
},
|
|
1587
|
+
reverse: (arr) => {
|
|
1588
|
+
if (!Array.isArray(arr)) return [];
|
|
1589
|
+
return [...arr].reverse();
|
|
1590
|
+
},
|
|
1591
|
+
sort: (arr) => {
|
|
1592
|
+
if (!Array.isArray(arr)) return [];
|
|
1593
|
+
return [...arr].sort();
|
|
1594
|
+
},
|
|
1595
|
+
unique: (arr) => {
|
|
1596
|
+
if (!Array.isArray(arr)) return [];
|
|
1597
|
+
return [...new Set(arr)];
|
|
1598
|
+
},
|
|
1599
|
+
flatten: (arr) => {
|
|
1600
|
+
if (!Array.isArray(arr)) return [];
|
|
1601
|
+
return arr.flat();
|
|
1602
|
+
},
|
|
1603
|
+
count: (arr) => {
|
|
1604
|
+
if (!Array.isArray(arr)) return 0;
|
|
1605
|
+
return arr.length;
|
|
1606
|
+
},
|
|
1607
|
+
// ==================== 对象函数 ====================
|
|
1608
|
+
get: (obj, path, defaultValue) => {
|
|
1609
|
+
if (obj === null || obj === void 0) return defaultValue;
|
|
1610
|
+
const pathStr = String(path);
|
|
1611
|
+
const parts = pathStr.split(".");
|
|
1612
|
+
let current = obj;
|
|
1613
|
+
for (const part of parts) {
|
|
1614
|
+
if (current === null || current === void 0) return defaultValue;
|
|
1615
|
+
current = current[part];
|
|
1616
|
+
}
|
|
1617
|
+
return current ?? defaultValue;
|
|
1618
|
+
},
|
|
1619
|
+
keys: (obj) => {
|
|
1620
|
+
if (typeof obj !== "object" || obj === null) return [];
|
|
1621
|
+
return Object.keys(obj);
|
|
1622
|
+
},
|
|
1623
|
+
values: (obj) => {
|
|
1624
|
+
if (typeof obj !== "object" || obj === null) return [];
|
|
1625
|
+
return Object.values(obj);
|
|
1626
|
+
},
|
|
1627
|
+
entries: (obj) => {
|
|
1628
|
+
if (typeof obj !== "object" || obj === null) return [];
|
|
1629
|
+
return Object.entries(obj);
|
|
1630
|
+
},
|
|
1631
|
+
has: (obj, key) => {
|
|
1632
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
1633
|
+
return String(key) in obj;
|
|
1634
|
+
},
|
|
1635
|
+
merge: (...objs) => {
|
|
1636
|
+
const result = {};
|
|
1637
|
+
for (const obj of objs) {
|
|
1638
|
+
if (typeof obj === "object" && obj !== null) {
|
|
1639
|
+
Object.assign(result, obj);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return result;
|
|
1643
|
+
},
|
|
1644
|
+
// ==================== 逻辑函数 ====================
|
|
1645
|
+
and: (...args) => {
|
|
1646
|
+
return args.every((a) => Boolean(a));
|
|
1647
|
+
},
|
|
1648
|
+
or: (...args) => {
|
|
1649
|
+
return args.some((a) => Boolean(a));
|
|
1650
|
+
},
|
|
1651
|
+
not: (value) => {
|
|
1652
|
+
return !value;
|
|
1653
|
+
},
|
|
1654
|
+
eq: (a, b) => {
|
|
1655
|
+
return a === b;
|
|
1656
|
+
},
|
|
1657
|
+
ne: (a, b) => {
|
|
1658
|
+
return a !== b;
|
|
1659
|
+
},
|
|
1660
|
+
gt: (a, b) => {
|
|
1661
|
+
return Number(a) > Number(b);
|
|
1662
|
+
},
|
|
1663
|
+
gte: (a, b) => {
|
|
1664
|
+
return Number(a) >= Number(b);
|
|
1665
|
+
},
|
|
1666
|
+
lt: (a, b) => {
|
|
1667
|
+
return Number(a) < Number(b);
|
|
1668
|
+
},
|
|
1669
|
+
lte: (a, b) => {
|
|
1670
|
+
return Number(a) <= Number(b);
|
|
1671
|
+
},
|
|
1672
|
+
between: (value, min, max) => {
|
|
1673
|
+
const num = Number(value);
|
|
1674
|
+
return num >= Number(min) && num <= Number(max);
|
|
1675
|
+
},
|
|
1676
|
+
// ==================== 格式化函数 ====================
|
|
1677
|
+
formatNumber: (value, decimals) => {
|
|
1678
|
+
const num = Number(value) || 0;
|
|
1679
|
+
const dec = Number(decimals) ?? 0;
|
|
1680
|
+
return num.toLocaleString(void 0, {
|
|
1681
|
+
minimumFractionDigits: dec,
|
|
1682
|
+
maximumFractionDigits: dec
|
|
1683
|
+
});
|
|
1684
|
+
},
|
|
1685
|
+
formatCurrency: (value, currency) => {
|
|
1686
|
+
const num = Number(value) || 0;
|
|
1687
|
+
const cur = String(currency || "CNY");
|
|
1688
|
+
return num.toLocaleString("zh-CN", { style: "currency", currency: cur });
|
|
1689
|
+
},
|
|
1690
|
+
formatPercent: (value, decimals) => {
|
|
1691
|
+
const num = Number(value) || 0;
|
|
1692
|
+
const dec = Number(decimals) ?? 0;
|
|
1693
|
+
return (num * 100).toFixed(dec) + "%";
|
|
1694
|
+
},
|
|
1695
|
+
// ==================== JSON 函数 ====================
|
|
1696
|
+
jsonParse: (str) => {
|
|
1697
|
+
try {
|
|
1698
|
+
return JSON.parse(String(str));
|
|
1699
|
+
} catch {
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
},
|
|
1703
|
+
jsonStringify: (value) => {
|
|
1704
|
+
return JSON.stringify(value);
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
// src/expression/evaluator.ts
|
|
1709
|
+
var Evaluator = class {
|
|
1710
|
+
constructor(options = {}) {
|
|
1711
|
+
this.depth = 0;
|
|
1712
|
+
this.startTime = 0;
|
|
1713
|
+
this.options = {
|
|
1714
|
+
maxDepth: 100,
|
|
1715
|
+
timeout: 1e3,
|
|
1716
|
+
debug: false,
|
|
1717
|
+
...options
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* 求值表达式
|
|
1722
|
+
*/
|
|
1723
|
+
evaluate(ast, context) {
|
|
1724
|
+
this.depth = 0;
|
|
1725
|
+
this.startTime = Date.now();
|
|
1726
|
+
try {
|
|
1727
|
+
const value = this.evaluateNode(ast, context);
|
|
1728
|
+
return { value };
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
return {
|
|
1731
|
+
value: void 0,
|
|
1732
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
evaluateNode(node, context) {
|
|
1737
|
+
this.checkLimits();
|
|
1738
|
+
switch (node.type) {
|
|
1739
|
+
case "literal":
|
|
1740
|
+
return this.evaluateLiteral(node);
|
|
1741
|
+
case "identifier":
|
|
1742
|
+
return this.evaluateIdentifier(node, context);
|
|
1743
|
+
case "member":
|
|
1744
|
+
return this.evaluateMember(node, context);
|
|
1745
|
+
case "call":
|
|
1746
|
+
return this.evaluateCall(node, context);
|
|
1747
|
+
case "binary":
|
|
1748
|
+
return this.evaluateBinary(node, context);
|
|
1749
|
+
case "unary":
|
|
1750
|
+
return this.evaluateUnary(node, context);
|
|
1751
|
+
case "conditional":
|
|
1752
|
+
return this.evaluateConditional(node, context);
|
|
1753
|
+
case "array":
|
|
1754
|
+
return this.evaluateArray(node, context);
|
|
1755
|
+
default:
|
|
1756
|
+
throw new ExpressionError("", `Unknown node type: ${node.type}`);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
evaluateLiteral(node) {
|
|
1760
|
+
return node.value;
|
|
1761
|
+
}
|
|
1762
|
+
evaluateIdentifier(node, context) {
|
|
1763
|
+
const name = node.name;
|
|
1764
|
+
switch (name) {
|
|
1765
|
+
case "state":
|
|
1766
|
+
return context.state;
|
|
1767
|
+
case "query":
|
|
1768
|
+
return context.query;
|
|
1769
|
+
case "context":
|
|
1770
|
+
return context.context;
|
|
1771
|
+
case "event":
|
|
1772
|
+
return context.event;
|
|
1773
|
+
case "item":
|
|
1774
|
+
return context.item;
|
|
1775
|
+
case "index":
|
|
1776
|
+
return context.index;
|
|
1777
|
+
default:
|
|
1778
|
+
throw new ExpressionError(
|
|
1779
|
+
"",
|
|
1780
|
+
`Unknown variable '${name}'`,
|
|
1781
|
+
node.start !== void 0 && node.end !== void 0 ? { start: node.start, end: node.end } : void 0
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
evaluateMember(node, context) {
|
|
1786
|
+
const object = this.evaluateNode(node.object, context);
|
|
1787
|
+
if (object === null || object === void 0) {
|
|
1788
|
+
return void 0;
|
|
1789
|
+
}
|
|
1790
|
+
let property;
|
|
1791
|
+
if (node.computed) {
|
|
1792
|
+
const propValue = this.evaluateNode(node.property, context);
|
|
1793
|
+
property = propValue;
|
|
1794
|
+
} else {
|
|
1795
|
+
property = node.property;
|
|
1796
|
+
}
|
|
1797
|
+
if (typeof object === "object" && object !== null) {
|
|
1798
|
+
return object[property];
|
|
1799
|
+
}
|
|
1800
|
+
return void 0;
|
|
1801
|
+
}
|
|
1802
|
+
evaluateCall(node, context) {
|
|
1803
|
+
const fn = builtinFunctions[node.callee];
|
|
1804
|
+
if (!fn) {
|
|
1805
|
+
throw new ExpressionError("", `Unknown function '${node.callee}'`);
|
|
1806
|
+
}
|
|
1807
|
+
const args = node.arguments.map((arg) => this.evaluateNode(arg, context));
|
|
1808
|
+
return fn(...args);
|
|
1809
|
+
}
|
|
1810
|
+
evaluateBinary(node, context) {
|
|
1811
|
+
const op = node.operator;
|
|
1812
|
+
if (op === "&&") {
|
|
1813
|
+
const left2 = this.evaluateNode(node.left, context);
|
|
1814
|
+
if (!left2) return left2;
|
|
1815
|
+
return this.evaluateNode(node.right, context);
|
|
1816
|
+
}
|
|
1817
|
+
if (op === "||") {
|
|
1818
|
+
const left2 = this.evaluateNode(node.left, context);
|
|
1819
|
+
if (left2) return left2;
|
|
1820
|
+
return this.evaluateNode(node.right, context);
|
|
1821
|
+
}
|
|
1822
|
+
if (op === "??") {
|
|
1823
|
+
const left2 = this.evaluateNode(node.left, context);
|
|
1824
|
+
if (left2 !== null && left2 !== void 0) return left2;
|
|
1825
|
+
return this.evaluateNode(node.right, context);
|
|
1826
|
+
}
|
|
1827
|
+
const left = this.evaluateNode(node.left, context);
|
|
1828
|
+
const right = this.evaluateNode(node.right, context);
|
|
1829
|
+
switch (op) {
|
|
1830
|
+
case "+":
|
|
1831
|
+
if (typeof left === "string" || typeof right === "string") {
|
|
1832
|
+
return String(left) + String(right);
|
|
1833
|
+
}
|
|
1834
|
+
return left + right;
|
|
1835
|
+
case "-":
|
|
1836
|
+
return left - right;
|
|
1837
|
+
case "*":
|
|
1838
|
+
return left * right;
|
|
1839
|
+
case "/":
|
|
1840
|
+
return left / right;
|
|
1841
|
+
case "%":
|
|
1842
|
+
return left % right;
|
|
1843
|
+
case "==":
|
|
1844
|
+
return left === right;
|
|
1845
|
+
case "!=":
|
|
1846
|
+
return left !== right;
|
|
1847
|
+
case "<":
|
|
1848
|
+
return left < right;
|
|
1849
|
+
case ">":
|
|
1850
|
+
return left > right;
|
|
1851
|
+
case "<=":
|
|
1852
|
+
return left <= right;
|
|
1853
|
+
case ">=":
|
|
1854
|
+
return left >= right;
|
|
1855
|
+
default:
|
|
1856
|
+
throw new ExpressionError("", `Unknown operator '${op}'`);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
evaluateUnary(node, context) {
|
|
1860
|
+
const argument = this.evaluateNode(node.argument, context);
|
|
1861
|
+
switch (node.operator) {
|
|
1862
|
+
case "!":
|
|
1863
|
+
return !argument;
|
|
1864
|
+
case "-":
|
|
1865
|
+
return -argument;
|
|
1866
|
+
default:
|
|
1867
|
+
throw new ExpressionError("", `Unknown unary operator '${node.operator}'`);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
evaluateConditional(node, context) {
|
|
1871
|
+
const test = this.evaluateNode(node.test, context);
|
|
1872
|
+
if (test) {
|
|
1873
|
+
return this.evaluateNode(node.consequent, context);
|
|
1874
|
+
}
|
|
1875
|
+
return this.evaluateNode(node.alternate, context);
|
|
1876
|
+
}
|
|
1877
|
+
evaluateArray(node, context) {
|
|
1878
|
+
return node.elements.map((el) => this.evaluateNode(el, context));
|
|
1879
|
+
}
|
|
1880
|
+
checkLimits() {
|
|
1881
|
+
this.depth++;
|
|
1882
|
+
if (this.depth > (this.options.maxDepth ?? 100)) {
|
|
1883
|
+
throw new ExpressionError("", "Maximum recursion depth exceeded");
|
|
1884
|
+
}
|
|
1885
|
+
const elapsed = Date.now() - this.startTime;
|
|
1886
|
+
if (elapsed > (this.options.timeout ?? 1e3)) {
|
|
1887
|
+
throw new ExpressionError("", "Expression evaluation timeout");
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
// src/expression/engine.ts
|
|
1893
|
+
var ExpressionEngine = class {
|
|
1894
|
+
constructor(options = {}) {
|
|
1895
|
+
this.astCache = /* @__PURE__ */ new Map();
|
|
1896
|
+
this.options = {
|
|
1897
|
+
cacheAST: true,
|
|
1898
|
+
maxCacheSize: 1e3,
|
|
1899
|
+
...options
|
|
1900
|
+
};
|
|
1901
|
+
this.evaluator = new Evaluator(options);
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* 求值表达式
|
|
1905
|
+
*/
|
|
1906
|
+
evaluate(expression, context) {
|
|
1907
|
+
try {
|
|
1908
|
+
const ast = this.parse(expression);
|
|
1909
|
+
return this.evaluator.evaluate(ast, context);
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
return {
|
|
1912
|
+
value: void 0,
|
|
1913
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* 求值表达式并返回值
|
|
1919
|
+
* 出错时返回 fallback 值
|
|
1920
|
+
*/
|
|
1921
|
+
evaluateWithFallback(expression, context, fallback) {
|
|
1922
|
+
const result = this.evaluate(expression, context);
|
|
1923
|
+
if (result.error) {
|
|
1924
|
+
this.log("warn", `Expression evaluation failed: ${result.error.message}`, expression);
|
|
1925
|
+
return fallback;
|
|
1926
|
+
}
|
|
1927
|
+
return result.value;
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* 求值模板字符串
|
|
1931
|
+
* 支持 ${expression} 语法
|
|
1932
|
+
*/
|
|
1933
|
+
evaluateTemplate(template, context) {
|
|
1934
|
+
return template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
1935
|
+
const result = this.evaluate(expr.trim(), context);
|
|
1936
|
+
if (result.error) {
|
|
1937
|
+
this.log("warn", `Template expression failed: ${result.error.message}`, expr);
|
|
1938
|
+
return "";
|
|
1939
|
+
}
|
|
1940
|
+
return String(result.value ?? "");
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* 解析表达式为 AST
|
|
1945
|
+
*/
|
|
1946
|
+
parse(expression) {
|
|
1947
|
+
if (this.options.cacheAST) {
|
|
1948
|
+
const cached = this.astCache.get(expression);
|
|
1949
|
+
if (cached) {
|
|
1950
|
+
return cached;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
try {
|
|
1954
|
+
const lexer = new Lexer(expression);
|
|
1955
|
+
const tokens = lexer.tokenize();
|
|
1956
|
+
const parser = new Parser(tokens);
|
|
1957
|
+
const ast = parser.parse();
|
|
1958
|
+
if (this.options.cacheAST) {
|
|
1959
|
+
if (this.astCache.size >= (this.options.maxCacheSize ?? 1e3)) {
|
|
1960
|
+
const keysToDelete = Array.from(this.astCache.keys()).slice(
|
|
1961
|
+
0,
|
|
1962
|
+
Math.floor(this.astCache.size / 2)
|
|
1963
|
+
);
|
|
1964
|
+
keysToDelete.forEach((key) => this.astCache.delete(key));
|
|
1965
|
+
}
|
|
1966
|
+
this.astCache.set(expression, ast);
|
|
1967
|
+
}
|
|
1968
|
+
return ast;
|
|
1969
|
+
} catch (error) {
|
|
1970
|
+
throw new ExpressionError(
|
|
1971
|
+
expression,
|
|
1972
|
+
error instanceof Error ? error.message : "Parse error"
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* 校验表达式
|
|
1978
|
+
*/
|
|
1979
|
+
validate(expression) {
|
|
1980
|
+
const errors = [];
|
|
1981
|
+
const warnings = [];
|
|
1982
|
+
const referencedPaths = [];
|
|
1983
|
+
const calledFunctions = [];
|
|
1984
|
+
try {
|
|
1985
|
+
const ast = this.parse(expression);
|
|
1986
|
+
this.collectReferences(ast, referencedPaths, calledFunctions);
|
|
1987
|
+
return {
|
|
1988
|
+
valid: true,
|
|
1989
|
+
errors: [],
|
|
1990
|
+
warnings,
|
|
1991
|
+
referencedPaths,
|
|
1992
|
+
calledFunctions
|
|
1993
|
+
};
|
|
1994
|
+
} catch (error) {
|
|
1995
|
+
errors.push({
|
|
1996
|
+
type: "SYNTAX_ERROR",
|
|
1997
|
+
message: error instanceof Error ? error.message : "Parse error"
|
|
1998
|
+
});
|
|
1999
|
+
return {
|
|
2000
|
+
valid: false,
|
|
2001
|
+
errors,
|
|
2002
|
+
warnings,
|
|
2003
|
+
referencedPaths,
|
|
2004
|
+
calledFunctions
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* 清除 AST 缓存
|
|
2010
|
+
*/
|
|
2011
|
+
clearCache() {
|
|
2012
|
+
this.astCache.clear();
|
|
2013
|
+
}
|
|
2014
|
+
collectReferences(node, paths, functions) {
|
|
2015
|
+
switch (node.type) {
|
|
2016
|
+
case "identifier":
|
|
2017
|
+
paths.push(node.name);
|
|
2018
|
+
break;
|
|
2019
|
+
case "member": {
|
|
2020
|
+
const path = this.buildMemberPath(node);
|
|
2021
|
+
if (path) paths.push(path);
|
|
2022
|
+
break;
|
|
2023
|
+
}
|
|
2024
|
+
case "call":
|
|
2025
|
+
functions.push(node.callee);
|
|
2026
|
+
node.arguments.forEach((arg) => this.collectReferences(arg, paths, functions));
|
|
2027
|
+
break;
|
|
2028
|
+
case "binary":
|
|
2029
|
+
this.collectReferences(node.left, paths, functions);
|
|
2030
|
+
this.collectReferences(node.right, paths, functions);
|
|
2031
|
+
break;
|
|
2032
|
+
case "unary":
|
|
2033
|
+
this.collectReferences(node.argument, paths, functions);
|
|
2034
|
+
break;
|
|
2035
|
+
case "conditional":
|
|
2036
|
+
this.collectReferences(node.test, paths, functions);
|
|
2037
|
+
this.collectReferences(node.consequent, paths, functions);
|
|
2038
|
+
this.collectReferences(node.alternate, paths, functions);
|
|
2039
|
+
break;
|
|
2040
|
+
case "array":
|
|
2041
|
+
node.elements.forEach((el) => this.collectReferences(el, paths, functions));
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
buildMemberPath(node) {
|
|
2046
|
+
if (node.type === "identifier") {
|
|
2047
|
+
return node.name;
|
|
2048
|
+
}
|
|
2049
|
+
if (node.type === "member" && !node.computed) {
|
|
2050
|
+
const objectPath = this.buildMemberPath(node.object);
|
|
2051
|
+
if (objectPath) {
|
|
2052
|
+
return `${objectPath}.${node.property}`;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
log(level, message, ...args) {
|
|
2058
|
+
if (this.options.logger) {
|
|
2059
|
+
this.options.logger[level](message, ...args);
|
|
2060
|
+
} else if (this.options.debug) {
|
|
2061
|
+
console[level](`[ExpressionEngine] ${message}`, ...args);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
// src/host-api/host-api-impl.ts
|
|
2067
|
+
var HostAPIImpl = class {
|
|
2068
|
+
constructor(options) {
|
|
2069
|
+
// ==================== 存储 ====================
|
|
2070
|
+
this.storage = {
|
|
2071
|
+
get: (key, options) => {
|
|
2072
|
+
const fullKey = this.getStorageKey(key, options?.namespace);
|
|
2073
|
+
const item = localStorage.getItem(fullKey);
|
|
2074
|
+
if (!item) return void 0;
|
|
2075
|
+
try {
|
|
2076
|
+
const parsed = JSON.parse(item);
|
|
2077
|
+
if (parsed.expires && Date.now() > parsed.expires) {
|
|
2078
|
+
localStorage.removeItem(fullKey);
|
|
2079
|
+
return void 0;
|
|
2080
|
+
}
|
|
2081
|
+
return parsed.value;
|
|
2082
|
+
} catch {
|
|
2083
|
+
return void 0;
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
set: (key, value, options) => {
|
|
2087
|
+
const fullKey = this.getStorageKey(key, options?.namespace);
|
|
2088
|
+
const item = {
|
|
2089
|
+
value,
|
|
2090
|
+
expires: options?.ttl ? Date.now() + options.ttl * 1e3 : void 0
|
|
2091
|
+
};
|
|
2092
|
+
localStorage.setItem(fullKey, JSON.stringify(item));
|
|
2093
|
+
},
|
|
2094
|
+
remove: (key, options) => {
|
|
2095
|
+
const fullKey = this.getStorageKey(key, options?.namespace);
|
|
2096
|
+
localStorage.removeItem(fullKey);
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
this.options = options;
|
|
2100
|
+
this.storageNamespace = `djvlc:${options.context.pageUid}`;
|
|
2101
|
+
}
|
|
2102
|
+
// ==================== 数据请求 ====================
|
|
2103
|
+
async requestData(queryId, params) {
|
|
2104
|
+
this.log("debug", `Requesting data: ${queryId}`, params);
|
|
2105
|
+
const startTime = performance.now();
|
|
2106
|
+
const context = this.options.context;
|
|
2107
|
+
try {
|
|
2108
|
+
const response = await fetch(`${this.options.apiBaseUrl}/data/query`, {
|
|
2109
|
+
method: "POST",
|
|
2110
|
+
headers: this.buildHeaders(),
|
|
2111
|
+
body: JSON.stringify({
|
|
2112
|
+
queryVersionId: queryId,
|
|
2113
|
+
params: params || {},
|
|
2114
|
+
context: {
|
|
2115
|
+
pageVersionId: context.pageVersionId,
|
|
2116
|
+
uid: context.userId
|
|
2117
|
+
}
|
|
2118
|
+
})
|
|
2119
|
+
});
|
|
2120
|
+
const result = await response.json();
|
|
2121
|
+
const duration = performance.now() - startTime;
|
|
2122
|
+
this.log("debug", `Data query completed in ${duration.toFixed(2)}ms`);
|
|
2123
|
+
if (result.success && result.data) {
|
|
2124
|
+
this.options.stateManager.setQuery(queryId, result.data);
|
|
2125
|
+
}
|
|
2126
|
+
return result;
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
this.log("error", `Data query failed: ${queryId}`, error);
|
|
2129
|
+
return {
|
|
2130
|
+
success: false,
|
|
2131
|
+
message: error instanceof Error ? error.message : "Query failed"
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
async executeAction(actionType, params) {
|
|
2136
|
+
this.log("debug", `Executing action: ${actionType}`, params);
|
|
2137
|
+
const startTime = performance.now();
|
|
2138
|
+
const context = this.options.context;
|
|
2139
|
+
try {
|
|
2140
|
+
const response = await fetch(`${this.options.apiBaseUrl}/actions/execute`, {
|
|
2141
|
+
method: "POST",
|
|
2142
|
+
headers: this.buildHeaders(),
|
|
2143
|
+
body: JSON.stringify({
|
|
2144
|
+
actionType,
|
|
2145
|
+
params: params || {},
|
|
2146
|
+
context: {
|
|
2147
|
+
pageVersionId: context.pageVersionId,
|
|
2148
|
+
uid: context.userId,
|
|
2149
|
+
deviceId: context.deviceId,
|
|
2150
|
+
channel: context.channel
|
|
2151
|
+
},
|
|
2152
|
+
idempotencyKey: this.generateIdempotencyKey(actionType, params)
|
|
2153
|
+
})
|
|
2154
|
+
});
|
|
2155
|
+
const result = await response.json();
|
|
2156
|
+
const duration = performance.now() - startTime;
|
|
2157
|
+
this.log("debug", `Action completed in ${duration.toFixed(2)}ms`);
|
|
2158
|
+
return result;
|
|
2159
|
+
} catch (error) {
|
|
2160
|
+
this.log("error", `Action failed: ${actionType}`, error);
|
|
2161
|
+
return {
|
|
2162
|
+
success: false,
|
|
2163
|
+
message: error instanceof Error ? error.message : "Action failed"
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
// ==================== 导航 ====================
|
|
2168
|
+
navigate(options) {
|
|
2169
|
+
this.log("debug", "Navigate:", options);
|
|
2170
|
+
this.track({
|
|
2171
|
+
eventName: "navigate",
|
|
2172
|
+
params: { url: options.url, newTab: options.newTab }
|
|
2173
|
+
});
|
|
2174
|
+
let url = options.url;
|
|
2175
|
+
if (options.params) {
|
|
2176
|
+
const searchParams = new URLSearchParams(options.params);
|
|
2177
|
+
url += (url.includes("?") ? "&" : "?") + searchParams.toString();
|
|
2178
|
+
}
|
|
2179
|
+
if (options.newTab) {
|
|
2180
|
+
window.open(url, "_blank");
|
|
2181
|
+
} else if (options.replace) {
|
|
2182
|
+
window.location.replace(url);
|
|
2183
|
+
} else {
|
|
2184
|
+
window.location.href = url;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
goBack() {
|
|
2188
|
+
this.log("debug", "Navigate back");
|
|
2189
|
+
window.history.back();
|
|
2190
|
+
}
|
|
2191
|
+
refresh() {
|
|
2192
|
+
this.log("debug", "Refresh page");
|
|
2193
|
+
window.location.reload();
|
|
2194
|
+
}
|
|
2195
|
+
// ==================== 对话框 ====================
|
|
2196
|
+
async openDialog(options) {
|
|
2197
|
+
this.log("debug", "Open dialog:", options);
|
|
2198
|
+
switch (options.type) {
|
|
2199
|
+
case "alert":
|
|
2200
|
+
window.alert(options.content || options.title);
|
|
2201
|
+
return true;
|
|
2202
|
+
case "confirm":
|
|
2203
|
+
return window.confirm(options.content || options.title);
|
|
2204
|
+
case "prompt":
|
|
2205
|
+
return window.prompt(options.content || options.title) || "";
|
|
2206
|
+
case "custom":
|
|
2207
|
+
this.log("warn", "Custom dialog not implemented");
|
|
2208
|
+
return false;
|
|
2209
|
+
default:
|
|
2210
|
+
return false;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
closeDialog() {
|
|
2214
|
+
this.log("debug", "Close dialog");
|
|
2215
|
+
}
|
|
2216
|
+
showToast(options) {
|
|
2217
|
+
this.log("debug", "Show toast:", options);
|
|
2218
|
+
const toast = document.createElement("div");
|
|
2219
|
+
toast.className = `djvlc-toast djvlc-toast-${options.type || "info"} djvlc-toast-${options.position || "top"}`;
|
|
2220
|
+
toast.textContent = options.message;
|
|
2221
|
+
Object.assign(toast.style, {
|
|
2222
|
+
position: "fixed",
|
|
2223
|
+
left: "50%",
|
|
2224
|
+
transform: "translateX(-50%)",
|
|
2225
|
+
padding: "12px 24px",
|
|
2226
|
+
borderRadius: "8px",
|
|
2227
|
+
color: "#fff",
|
|
2228
|
+
fontSize: "14px",
|
|
2229
|
+
zIndex: "10000",
|
|
2230
|
+
animation: "djvlc-toast-fade-in 0.3s ease"
|
|
2231
|
+
});
|
|
2232
|
+
const colors = {
|
|
2233
|
+
success: "#52c41a",
|
|
2234
|
+
error: "#ff4d4f",
|
|
2235
|
+
warning: "#faad14",
|
|
2236
|
+
info: "#1890ff"
|
|
2237
|
+
};
|
|
2238
|
+
toast.style.backgroundColor = colors[options.type || "info"];
|
|
2239
|
+
const positions = {
|
|
2240
|
+
top: { top: "20px" },
|
|
2241
|
+
center: { top: "50%", transform: "translate(-50%, -50%)" },
|
|
2242
|
+
bottom: { bottom: "20px" }
|
|
2243
|
+
};
|
|
2244
|
+
Object.assign(toast.style, positions[options.position || "top"]);
|
|
2245
|
+
document.body.appendChild(toast);
|
|
2246
|
+
setTimeout(() => {
|
|
2247
|
+
toast.style.animation = "djvlc-toast-fade-out 0.3s ease";
|
|
2248
|
+
setTimeout(() => toast.remove(), 300);
|
|
2249
|
+
}, options.duration || 3e3);
|
|
2250
|
+
}
|
|
2251
|
+
// ==================== 埋点 ====================
|
|
2252
|
+
track(event) {
|
|
2253
|
+
this.log("debug", "Track event:", event);
|
|
2254
|
+
const context = this.options.context;
|
|
2255
|
+
fetch(`${this.options.apiBaseUrl}/track`, {
|
|
2256
|
+
method: "POST",
|
|
2257
|
+
headers: this.buildHeaders(),
|
|
2258
|
+
body: JSON.stringify({
|
|
2259
|
+
eventName: event.eventName,
|
|
2260
|
+
params: event.params,
|
|
2261
|
+
timestamp: event.timestamp || Date.now(),
|
|
2262
|
+
context: {
|
|
2263
|
+
pageVersionId: context.pageVersionId,
|
|
2264
|
+
runtimeVersion: context.runtimeVersion,
|
|
2265
|
+
userId: context.userId,
|
|
2266
|
+
deviceId: context.deviceId,
|
|
2267
|
+
channel: context.channel
|
|
2268
|
+
}
|
|
2269
|
+
}),
|
|
2270
|
+
keepalive: true
|
|
2271
|
+
// 页面关闭时也能发送
|
|
2272
|
+
}).catch((error) => {
|
|
2273
|
+
this.log("warn", "Track failed:", error);
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
// ==================== 剪贴板 ====================
|
|
2277
|
+
async copyToClipboard(text) {
|
|
2278
|
+
try {
|
|
2279
|
+
await navigator.clipboard.writeText(text);
|
|
2280
|
+
return { success: true, content: text };
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
return {
|
|
2283
|
+
success: false,
|
|
2284
|
+
error: error instanceof Error ? error.message : "Copy failed"
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
async readFromClipboard() {
|
|
2289
|
+
try {
|
|
2290
|
+
const text = await navigator.clipboard.readText();
|
|
2291
|
+
return { success: true, content: text };
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
return {
|
|
2294
|
+
success: false,
|
|
2295
|
+
error: error instanceof Error ? error.message : "Read failed"
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
// ==================== 状态管理 ====================
|
|
2300
|
+
getState(key) {
|
|
2301
|
+
return this.options.stateManager.getVariable(key);
|
|
2302
|
+
}
|
|
2303
|
+
setState(key, value) {
|
|
2304
|
+
this.options.stateManager.setVariable(key, value);
|
|
2305
|
+
}
|
|
2306
|
+
getVariable(name) {
|
|
2307
|
+
return this.options.stateManager.getVariable(name);
|
|
2308
|
+
}
|
|
2309
|
+
// ==================== 组件通信 ====================
|
|
2310
|
+
postMessage(componentId, message) {
|
|
2311
|
+
this.log("debug", `Post message to ${componentId}:`, message);
|
|
2312
|
+
const event = new CustomEvent(`djvlc:message:${componentId}`, {
|
|
2313
|
+
detail: { message, from: this.options.context.pageUid }
|
|
2314
|
+
});
|
|
2315
|
+
document.dispatchEvent(event);
|
|
2316
|
+
}
|
|
2317
|
+
broadcast(channel, message) {
|
|
2318
|
+
this.log("debug", `Broadcast to ${channel}:`, message);
|
|
2319
|
+
const event = new CustomEvent(`djvlc:broadcast:${channel}`, {
|
|
2320
|
+
detail: { message, from: this.options.context.pageUid }
|
|
2321
|
+
});
|
|
2322
|
+
document.dispatchEvent(event);
|
|
2323
|
+
}
|
|
2324
|
+
// ==================== 上下文信息 ====================
|
|
2325
|
+
getContext() {
|
|
2326
|
+
return { ...this.options.context };
|
|
2327
|
+
}
|
|
2328
|
+
// ==================== 私有方法 ====================
|
|
2329
|
+
buildHeaders() {
|
|
2330
|
+
const headers = {
|
|
2331
|
+
"Content-Type": "application/json",
|
|
2332
|
+
...this.options.headers
|
|
2333
|
+
};
|
|
2334
|
+
if (this.options.authToken) {
|
|
2335
|
+
headers["Authorization"] = `Bearer ${this.options.authToken}`;
|
|
2336
|
+
}
|
|
2337
|
+
return headers;
|
|
2338
|
+
}
|
|
2339
|
+
getStorageKey(key, namespace) {
|
|
2340
|
+
return `${namespace || this.storageNamespace}:${key}`;
|
|
2341
|
+
}
|
|
2342
|
+
generateIdempotencyKey(actionType, params) {
|
|
2343
|
+
const timestamp = Date.now();
|
|
2344
|
+
const paramsStr = JSON.stringify(params || {});
|
|
2345
|
+
return `${actionType}:${timestamp}:${this.simpleHash(paramsStr)}`;
|
|
2346
|
+
}
|
|
2347
|
+
simpleHash(str) {
|
|
2348
|
+
let hash = 0;
|
|
2349
|
+
for (let i = 0; i < str.length; i++) {
|
|
2350
|
+
const char = str.charCodeAt(i);
|
|
2351
|
+
hash = (hash << 5) - hash + char;
|
|
2352
|
+
hash = hash & hash;
|
|
2353
|
+
}
|
|
2354
|
+
return Math.abs(hash).toString(36);
|
|
2355
|
+
}
|
|
2356
|
+
log(level, message, ...args) {
|
|
2357
|
+
if (this.options.logger) {
|
|
2358
|
+
this.options.logger[level](message, ...args);
|
|
2359
|
+
} else if (this.options.debug) {
|
|
2360
|
+
console[level](`[HostAPI] ${message}`, ...args);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
|
|
2365
|
+
// src/security/security-manager.ts
|
|
2366
|
+
var SecurityManager = class {
|
|
2367
|
+
constructor(options = {}) {
|
|
2368
|
+
this.blockedComponentsMap = /* @__PURE__ */ new Map();
|
|
2369
|
+
this.blockedActionsSet = /* @__PURE__ */ new Set();
|
|
2370
|
+
this.options = {
|
|
2371
|
+
enableSRI: true,
|
|
2372
|
+
cdnDomains: [],
|
|
2373
|
+
apiDomains: [],
|
|
2374
|
+
blockedComponents: [],
|
|
2375
|
+
blockedActions: [],
|
|
2376
|
+
...options
|
|
2377
|
+
};
|
|
2378
|
+
this.updateBlockedList(options.blockedComponents || [], options.blockedActions || []);
|
|
2379
|
+
}
|
|
2380
|
+
/**
|
|
2381
|
+
* 更新阻断列表
|
|
2382
|
+
*/
|
|
2383
|
+
updateBlockedList(components, actions) {
|
|
2384
|
+
this.blockedComponentsMap.clear();
|
|
2385
|
+
components.forEach((c) => {
|
|
2386
|
+
this.blockedComponentsMap.set(`${c.name}@${c.version}`, c);
|
|
2387
|
+
});
|
|
2388
|
+
this.blockedActionsSet = new Set(actions);
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* 检查组件是否被阻断
|
|
2392
|
+
*/
|
|
2393
|
+
isComponentBlocked(name, version) {
|
|
2394
|
+
return this.blockedComponentsMap.has(`${name}@${version}`);
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* 获取组件阻断信息
|
|
2398
|
+
*/
|
|
2399
|
+
getBlockedInfo(name, version) {
|
|
2400
|
+
return this.blockedComponentsMap.get(`${name}@${version}`);
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* 检查动作是否被阻断
|
|
2404
|
+
*/
|
|
2405
|
+
isActionBlocked(actionType) {
|
|
2406
|
+
return this.blockedActionsSet.has(actionType);
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* 验证组件完整性
|
|
2410
|
+
*/
|
|
2411
|
+
async validateComponent(name, version, content, expectedIntegrity) {
|
|
2412
|
+
if (!this.options.enableSRI) return;
|
|
2413
|
+
const [algorithm, expectedHash] = expectedIntegrity.split("-");
|
|
2414
|
+
if (!algorithm || !expectedHash) {
|
|
2415
|
+
throw new IntegrityError(name, version, expectedIntegrity, "Invalid format");
|
|
2416
|
+
}
|
|
2417
|
+
const actualHash = await this.computeHash(content, algorithm);
|
|
2418
|
+
if (actualHash !== expectedHash) {
|
|
2419
|
+
throw new IntegrityError(name, version, expectedHash, actualHash);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* 验证 URL 是否在白名单内
|
|
2424
|
+
*/
|
|
2425
|
+
isAllowedUrl(url, type) {
|
|
2426
|
+
const domains = type === "cdn" ? this.options.cdnDomains : this.options.apiDomains;
|
|
2427
|
+
if (!domains || domains.length === 0) return true;
|
|
2428
|
+
try {
|
|
2429
|
+
const parsedUrl = new URL(url);
|
|
2430
|
+
return domains.some((domain) => {
|
|
2431
|
+
if (domain.startsWith("*.")) {
|
|
2432
|
+
const suffix = domain.slice(2);
|
|
2433
|
+
return parsedUrl.hostname.endsWith(suffix);
|
|
2434
|
+
}
|
|
2435
|
+
return parsedUrl.hostname === domain;
|
|
2436
|
+
});
|
|
2437
|
+
} catch {
|
|
2438
|
+
return false;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* 生成 CSP 策略
|
|
2443
|
+
*/
|
|
2444
|
+
generateCSPPolicy() {
|
|
2445
|
+
const cdnDomains = this.options.cdnDomains || [];
|
|
2446
|
+
const apiDomains = this.options.apiDomains || [];
|
|
2447
|
+
const scriptSrc = ["'self'", ...cdnDomains].join(" ");
|
|
2448
|
+
const connectSrc = ["'self'", ...apiDomains].join(" ");
|
|
2449
|
+
const styleSrc = ["'self'", "'unsafe-inline'", ...cdnDomains].join(" ");
|
|
2450
|
+
const imgSrc = ["'self'", "data:", "blob:", ...cdnDomains].join(" ");
|
|
2451
|
+
return [
|
|
2452
|
+
`default-src 'self'`,
|
|
2453
|
+
`script-src ${scriptSrc}`,
|
|
2454
|
+
`style-src ${styleSrc}`,
|
|
2455
|
+
`img-src ${imgSrc}`,
|
|
2456
|
+
`connect-src ${connectSrc}`,
|
|
2457
|
+
`font-src 'self' data: ${cdnDomains.join(" ")}`,
|
|
2458
|
+
`frame-ancestors 'self'`,
|
|
2459
|
+
`base-uri 'self'`,
|
|
2460
|
+
`form-action 'self'`
|
|
2461
|
+
].join("; ");
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* 应用 CSP 策略
|
|
2465
|
+
*/
|
|
2466
|
+
applyCSP() {
|
|
2467
|
+
const meta = document.createElement("meta");
|
|
2468
|
+
meta.httpEquiv = "Content-Security-Policy";
|
|
2469
|
+
meta.content = this.generateCSPPolicy();
|
|
2470
|
+
document.head.appendChild(meta);
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* 确保组件未被阻断
|
|
2474
|
+
*/
|
|
2475
|
+
assertNotBlocked(name, version) {
|
|
2476
|
+
const blocked = this.getBlockedInfo(name, version);
|
|
2477
|
+
if (blocked) {
|
|
2478
|
+
throw new ComponentBlockedError(name, version, blocked.reason);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* 计算哈希值
|
|
2483
|
+
*/
|
|
2484
|
+
async computeHash(content, algorithm) {
|
|
2485
|
+
const encoder = new TextEncoder();
|
|
2486
|
+
const data = encoder.encode(content);
|
|
2487
|
+
const hashBuffer = await crypto.subtle.digest(
|
|
2488
|
+
algorithm.toUpperCase(),
|
|
2489
|
+
data
|
|
2490
|
+
);
|
|
2491
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
2492
|
+
return btoa(String.fromCharCode(...hashArray));
|
|
2493
|
+
}
|
|
2494
|
+
_log(level, message) {
|
|
2495
|
+
if (this.options.logger) {
|
|
2496
|
+
this.options.logger[level](message);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
// 使用 _log 避免 unused 错误,后续可按需启用日志
|
|
2500
|
+
get log() {
|
|
2501
|
+
return this._log.bind(this);
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
|
|
2505
|
+
// src/telemetry/telemetry-manager.ts
|
|
2506
|
+
var TelemetryManager = class {
|
|
2507
|
+
constructor(options) {
|
|
2508
|
+
this.spans = /* @__PURE__ */ new Map();
|
|
2509
|
+
this.metrics = [];
|
|
2510
|
+
this.errors = [];
|
|
2511
|
+
this.options = {
|
|
2512
|
+
enabled: true,
|
|
2513
|
+
sampleRate: 1,
|
|
2514
|
+
...options
|
|
2515
|
+
};
|
|
2516
|
+
this.traceId = this.generateId();
|
|
2517
|
+
this.shouldSample = Math.random() < (this.options.sampleRate ?? 1);
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* 获取 Trace ID
|
|
2521
|
+
*/
|
|
2522
|
+
getTraceId() {
|
|
2523
|
+
return this.traceId;
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* 获取 W3C Trace Context 格式的 traceparent
|
|
2527
|
+
*/
|
|
2528
|
+
getTraceparent() {
|
|
2529
|
+
return `00-${this.traceId}-${this.generateId().slice(0, 16)}-01`;
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* 开始一个 Span
|
|
2533
|
+
*/
|
|
2534
|
+
startSpan(name, parentSpanId, attributes) {
|
|
2535
|
+
const span = {
|
|
2536
|
+
spanId: this.generateId().slice(0, 16),
|
|
2537
|
+
traceId: this.traceId,
|
|
2538
|
+
parentSpanId,
|
|
2539
|
+
name,
|
|
2540
|
+
startTime: performance.now(),
|
|
2541
|
+
attributes
|
|
2542
|
+
};
|
|
2543
|
+
this.spans.set(span.spanId, span);
|
|
2544
|
+
this.log("debug", `Span started: ${name} (${span.spanId})`);
|
|
2545
|
+
return span;
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* 结束一个 Span
|
|
2549
|
+
*/
|
|
2550
|
+
endSpan(spanId, status = "ok") {
|
|
2551
|
+
const span = this.spans.get(spanId);
|
|
2552
|
+
if (span) {
|
|
2553
|
+
span.endTime = performance.now();
|
|
2554
|
+
span.status = status;
|
|
2555
|
+
this.log("debug", `Span ended: ${span.name} (${spanId}) - ${span.endTime - span.startTime}ms`);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* 记录性能指标
|
|
2560
|
+
*/
|
|
2561
|
+
recordMetric(type, name, duration, extra) {
|
|
2562
|
+
const metric = {
|
|
2563
|
+
type,
|
|
2564
|
+
name,
|
|
2565
|
+
duration,
|
|
2566
|
+
startTime: performance.now() - duration,
|
|
2567
|
+
extra
|
|
2568
|
+
};
|
|
2569
|
+
this.metrics.push(metric);
|
|
2570
|
+
this.log("debug", `Metric: ${type} - ${name} - ${duration}ms`);
|
|
2571
|
+
if (this.options.onMetric) {
|
|
2572
|
+
this.options.onMetric(metric);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* 记录错误
|
|
2577
|
+
*/
|
|
2578
|
+
recordError(error, context) {
|
|
2579
|
+
const runtimeError = {
|
|
2580
|
+
type: "LOAD_ERROR",
|
|
2581
|
+
message: error.message,
|
|
2582
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
2583
|
+
timestamp: Date.now(),
|
|
2584
|
+
traceId: this.traceId,
|
|
2585
|
+
...context
|
|
2586
|
+
};
|
|
2587
|
+
this.errors.push(runtimeError);
|
|
2588
|
+
this.log("error", `Error recorded: ${error.message}`);
|
|
2589
|
+
if (this.shouldSample && this.options.enabled) {
|
|
2590
|
+
this.reportError(runtimeError);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* 记录页面加载时间
|
|
2595
|
+
*/
|
|
2596
|
+
recordPageLoad(duration, extra) {
|
|
2597
|
+
this.recordMetric("page_resolve", "page_load", duration, {
|
|
2598
|
+
pageVersionId: this.options.pageVersionId,
|
|
2599
|
+
...extra
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
/**
|
|
2603
|
+
* 记录组件加载时间
|
|
2604
|
+
*/
|
|
2605
|
+
recordComponentLoad(name, version, duration, success) {
|
|
2606
|
+
this.recordMetric("component_load", `${name}@${version}`, duration, {
|
|
2607
|
+
success
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* 记录首次渲染时间
|
|
2612
|
+
*/
|
|
2613
|
+
recordFirstRender(duration) {
|
|
2614
|
+
this.recordMetric("first_render", "first_render", duration, {
|
|
2615
|
+
pageVersionId: this.options.pageVersionId
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* 记录动作执行时间
|
|
2620
|
+
*/
|
|
2621
|
+
recordActionExecute(actionType, duration, success) {
|
|
2622
|
+
this.recordMetric("action_execute", actionType, duration, { success });
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2625
|
+
* 记录查询执行时间
|
|
2626
|
+
*/
|
|
2627
|
+
recordQueryExecute(queryId, duration, success, fromCache) {
|
|
2628
|
+
this.recordMetric("query_execute", queryId, duration, {
|
|
2629
|
+
success,
|
|
2630
|
+
fromCache
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* 获取所有指标
|
|
2635
|
+
*/
|
|
2636
|
+
getMetrics() {
|
|
2637
|
+
return [...this.metrics];
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* 获取所有 Span
|
|
2641
|
+
*/
|
|
2642
|
+
getSpans() {
|
|
2643
|
+
return Array.from(this.spans.values());
|
|
2644
|
+
}
|
|
2645
|
+
/**
|
|
2646
|
+
* 清理数据
|
|
2647
|
+
*/
|
|
2648
|
+
clear() {
|
|
2649
|
+
this.spans.clear();
|
|
2650
|
+
this.metrics = [];
|
|
2651
|
+
this.errors = [];
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* 刷新上报
|
|
2655
|
+
*/
|
|
2656
|
+
async flush() {
|
|
2657
|
+
if (!this.shouldSample || !this.options.enabled || !this.options.endpoint) {
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
const payload = {
|
|
2661
|
+
traceId: this.traceId,
|
|
2662
|
+
pageVersionId: this.options.pageVersionId,
|
|
2663
|
+
appId: this.options.appId,
|
|
2664
|
+
spans: this.getSpans(),
|
|
2665
|
+
metrics: this.metrics,
|
|
2666
|
+
errors: this.errors,
|
|
2667
|
+
timestamp: Date.now()
|
|
2668
|
+
};
|
|
2669
|
+
try {
|
|
2670
|
+
await fetch(this.options.endpoint, {
|
|
2671
|
+
method: "POST",
|
|
2672
|
+
headers: { "Content-Type": "application/json" },
|
|
2673
|
+
body: JSON.stringify(payload),
|
|
2674
|
+
keepalive: true
|
|
2675
|
+
});
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
this.log("warn", "Failed to flush telemetry:", error);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
reportError(error) {
|
|
2681
|
+
if (this.options.endpoint) {
|
|
2682
|
+
fetch(`${this.options.endpoint}/errors`, {
|
|
2683
|
+
method: "POST",
|
|
2684
|
+
headers: { "Content-Type": "application/json" },
|
|
2685
|
+
body: JSON.stringify(error),
|
|
2686
|
+
keepalive: true
|
|
2687
|
+
}).catch(() => {
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
generateId() {
|
|
2692
|
+
const array = new Uint8Array(16);
|
|
2693
|
+
crypto.getRandomValues(array);
|
|
2694
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2695
|
+
}
|
|
2696
|
+
log(level, message, ...args) {
|
|
2697
|
+
if (this.options.logger) {
|
|
2698
|
+
this.options.logger[level](message, ...args);
|
|
2699
|
+
} else if (this.options.debug) {
|
|
2700
|
+
console[level](`[Telemetry] ${message}`, ...args);
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
};
|
|
2704
|
+
|
|
2705
|
+
// src/renderer/base-renderer.ts
|
|
2706
|
+
var import_contracts_types3 = require("@djvlc/contracts-types");
|
|
2707
|
+
var BaseRenderer = class {
|
|
2708
|
+
constructor(options) {
|
|
2709
|
+
this.container = null;
|
|
2710
|
+
this.renderedElements = /* @__PURE__ */ new Map();
|
|
2711
|
+
this.expressionContext = {
|
|
2712
|
+
state: {},
|
|
2713
|
+
query: {},
|
|
2714
|
+
context: {}
|
|
2715
|
+
};
|
|
2716
|
+
this.options = options;
|
|
2717
|
+
}
|
|
2718
|
+
/**
|
|
2719
|
+
* 初始化渲染器
|
|
2720
|
+
*/
|
|
2721
|
+
init() {
|
|
2722
|
+
this.log("debug", "Renderer initialized");
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* 渲染页面
|
|
2726
|
+
*/
|
|
2727
|
+
render(schema, container) {
|
|
2728
|
+
this.container = container;
|
|
2729
|
+
this.log("debug", "Rendering page", schema.page.id);
|
|
2730
|
+
container.innerHTML = "";
|
|
2731
|
+
this.applyPageStyles(schema.page.canvas);
|
|
2732
|
+
const fragment = document.createDocumentFragment();
|
|
2733
|
+
for (const component of schema.components) {
|
|
2734
|
+
const element = this.renderComponent(component);
|
|
2735
|
+
if (element) {
|
|
2736
|
+
fragment.appendChild(element);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
container.appendChild(fragment);
|
|
2740
|
+
this.log("info", `Rendered ${schema.components.length} components`);
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* 更新组件属性
|
|
2744
|
+
*/
|
|
2745
|
+
updateComponent(componentId, props) {
|
|
2746
|
+
const element = this.renderedElements.get(componentId);
|
|
2747
|
+
if (!element) {
|
|
2748
|
+
this.log("warn", `Component not found: ${componentId}`);
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
this.applyProps(element, props);
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* 更新表达式上下文
|
|
2755
|
+
*/
|
|
2756
|
+
updateContext(context) {
|
|
2757
|
+
this.expressionContext = {
|
|
2758
|
+
...this.expressionContext,
|
|
2759
|
+
...context
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* 销毁渲染器
|
|
2764
|
+
*/
|
|
2765
|
+
destroy() {
|
|
2766
|
+
this.renderedElements.forEach((element) => {
|
|
2767
|
+
element.remove();
|
|
2768
|
+
});
|
|
2769
|
+
this.renderedElements.clear();
|
|
2770
|
+
if (this.container) {
|
|
2771
|
+
this.container.innerHTML = "";
|
|
2772
|
+
}
|
|
2773
|
+
this.log("debug", "Renderer destroyed");
|
|
2774
|
+
}
|
|
2775
|
+
/**
|
|
2776
|
+
* 渲染单个组件
|
|
2777
|
+
*/
|
|
2778
|
+
renderComponent(instance) {
|
|
2779
|
+
const { id, type, props, style, children, visible } = instance;
|
|
2780
|
+
if (visible === false) {
|
|
2781
|
+
return null;
|
|
2782
|
+
}
|
|
2783
|
+
try {
|
|
2784
|
+
const loadedComponent = this.options.components.get(type);
|
|
2785
|
+
let element;
|
|
2786
|
+
if (loadedComponent && customElements.get(type)) {
|
|
2787
|
+
element = document.createElement(type);
|
|
2788
|
+
} else {
|
|
2789
|
+
element = document.createElement("div");
|
|
2790
|
+
element.className = `djvlc-component djvlc-${type}`;
|
|
2791
|
+
if (!loadedComponent) {
|
|
2792
|
+
this.log("warn", `Component not loaded: ${type}`);
|
|
2793
|
+
element.textContent = `[Component: ${type}]`;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
element.setAttribute("data-component-id", id);
|
|
2797
|
+
element.setAttribute("data-component-type", type);
|
|
2798
|
+
const resolvedProps = this.resolveProps(props);
|
|
2799
|
+
this.applyProps(element, resolvedProps);
|
|
2800
|
+
if (style) {
|
|
2801
|
+
this.applyStyles(element, style);
|
|
2802
|
+
}
|
|
2803
|
+
this.options.injectHostApi(element, id);
|
|
2804
|
+
if (children && children.length > 0) {
|
|
2805
|
+
for (const child of children) {
|
|
2806
|
+
const childElement = this.renderComponent(child);
|
|
2807
|
+
if (childElement) {
|
|
2808
|
+
element.appendChild(childElement);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
this.renderedElements.set(id, element);
|
|
2813
|
+
return element;
|
|
2814
|
+
} catch (error) {
|
|
2815
|
+
this.log("error", `Failed to render component: ${id}`, error);
|
|
2816
|
+
if (this.options.onRenderError) {
|
|
2817
|
+
return this.options.onRenderError(id, error);
|
|
2818
|
+
}
|
|
2819
|
+
const fallback = document.createElement("div");
|
|
2820
|
+
fallback.className = "djvlc-error-boundary";
|
|
2821
|
+
fallback.textContent = `Error rendering component: ${type}`;
|
|
2822
|
+
return fallback;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
/**
|
|
2826
|
+
* 解析 props 中的表达式
|
|
2827
|
+
*/
|
|
2828
|
+
resolveProps(props) {
|
|
2829
|
+
const resolved = {};
|
|
2830
|
+
for (const [key, value] of Object.entries(props)) {
|
|
2831
|
+
if ((0, import_contracts_types3.isExpressionBinding)(value)) {
|
|
2832
|
+
const result = this.options.expressionEngine.evaluateWithFallback(
|
|
2833
|
+
value.expression,
|
|
2834
|
+
this.expressionContext,
|
|
2835
|
+
value.fallback
|
|
2836
|
+
);
|
|
2837
|
+
resolved[key] = result;
|
|
2838
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
2839
|
+
resolved[key] = this.resolveProps(value);
|
|
2840
|
+
} else {
|
|
2841
|
+
resolved[key] = value;
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
return resolved;
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* 应用 props 到元素
|
|
2848
|
+
*/
|
|
2849
|
+
applyProps(element, props) {
|
|
2850
|
+
for (const [key, value] of Object.entries(props)) {
|
|
2851
|
+
if (value === null || value === void 0) continue;
|
|
2852
|
+
if (element.tagName.includes("-")) {
|
|
2853
|
+
element[key] = value;
|
|
2854
|
+
} else {
|
|
2855
|
+
if (typeof value === "boolean") {
|
|
2856
|
+
if (value) {
|
|
2857
|
+
element.setAttribute(key, "");
|
|
2858
|
+
} else {
|
|
2859
|
+
element.removeAttribute(key);
|
|
2860
|
+
}
|
|
2861
|
+
} else if (typeof value === "object") {
|
|
2862
|
+
element.setAttribute(key, JSON.stringify(value));
|
|
2863
|
+
} else {
|
|
2864
|
+
element.setAttribute(key, String(value));
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
/**
|
|
2870
|
+
* 应用样式到元素
|
|
2871
|
+
*/
|
|
2872
|
+
applyStyles(element, style) {
|
|
2873
|
+
for (const [key, value] of Object.entries(style)) {
|
|
2874
|
+
if (value === null || value === void 0) continue;
|
|
2875
|
+
let cssValue;
|
|
2876
|
+
if (typeof value === "number") {
|
|
2877
|
+
const unitless = ["zIndex", "opacity", "flex", "fontWeight"];
|
|
2878
|
+
cssValue = unitless.includes(key) ? String(value) : `${value}px`;
|
|
2879
|
+
} else {
|
|
2880
|
+
cssValue = String(value);
|
|
2881
|
+
}
|
|
2882
|
+
const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
2883
|
+
element.style.setProperty(cssKey, cssValue);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
2887
|
+
* 应用页面样式
|
|
2888
|
+
*/
|
|
2889
|
+
applyPageStyles(canvas) {
|
|
2890
|
+
if (!this.container) return;
|
|
2891
|
+
this.container.style.width = `${canvas.width}px`;
|
|
2892
|
+
if (canvas.height) {
|
|
2893
|
+
this.container.style.minHeight = `${canvas.height}px`;
|
|
2894
|
+
}
|
|
2895
|
+
if (canvas.background) {
|
|
2896
|
+
this.container.style.background = canvas.background;
|
|
2897
|
+
}
|
|
2898
|
+
this.container.classList.add("djvlc-page", `djvlc-canvas-${canvas.type}`);
|
|
2899
|
+
}
|
|
2900
|
+
log(level, message, ...args) {
|
|
2901
|
+
if (this.options.logger) {
|
|
2902
|
+
this.options.logger[level](message, ...args);
|
|
2903
|
+
} else if (this.options.debug) {
|
|
2904
|
+
console[level](`[Renderer] ${message}`, ...args);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
};
|
|
2908
|
+
|
|
2909
|
+
// src/renderer/fallback-component.ts
|
|
2910
|
+
function registerFallbackComponents() {
|
|
2911
|
+
if (!customElements.get("djvlc-fallback")) {
|
|
2912
|
+
customElements.define(
|
|
2913
|
+
"djvlc-fallback",
|
|
2914
|
+
class extends HTMLElement {
|
|
2915
|
+
constructor() {
|
|
2916
|
+
super();
|
|
2917
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
2918
|
+
shadow.innerHTML = `
|
|
2919
|
+
<style>
|
|
2920
|
+
:host {
|
|
2921
|
+
display: block;
|
|
2922
|
+
padding: 16px;
|
|
2923
|
+
background: #fff2f0;
|
|
2924
|
+
border: 1px solid #ffccc7;
|
|
2925
|
+
border-radius: 4px;
|
|
2926
|
+
color: #ff4d4f;
|
|
2927
|
+
font-size: 14px;
|
|
2928
|
+
}
|
|
2929
|
+
.title {
|
|
2930
|
+
font-weight: 600;
|
|
2931
|
+
margin-bottom: 8px;
|
|
2932
|
+
}
|
|
2933
|
+
.message {
|
|
2934
|
+
color: #666;
|
|
2935
|
+
}
|
|
2936
|
+
</style>
|
|
2937
|
+
<div class="title">\u7EC4\u4EF6\u52A0\u8F7D\u5931\u8D25</div>
|
|
2938
|
+
<div class="message"><slot>\u8BF7\u5237\u65B0\u9875\u9762\u91CD\u8BD5</slot></div>
|
|
2939
|
+
`;
|
|
2940
|
+
}
|
|
2941
|
+
static get observedAttributes() {
|
|
2942
|
+
return ["message", "component-name"];
|
|
2943
|
+
}
|
|
2944
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
2945
|
+
if (name === "message" && this.shadowRoot) {
|
|
2946
|
+
const message = this.shadowRoot.querySelector(".message");
|
|
2947
|
+
if (message) message.textContent = newValue;
|
|
2948
|
+
}
|
|
2949
|
+
if (name === "component-name" && this.shadowRoot) {
|
|
2950
|
+
const title = this.shadowRoot.querySelector(".title");
|
|
2951
|
+
if (title) title.textContent = `\u7EC4\u4EF6 ${newValue} \u52A0\u8F7D\u5931\u8D25`;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
if (!customElements.get("djvlc-blocked")) {
|
|
2958
|
+
customElements.define(
|
|
2959
|
+
"djvlc-blocked",
|
|
2960
|
+
class extends HTMLElement {
|
|
2961
|
+
constructor() {
|
|
2962
|
+
super();
|
|
2963
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
2964
|
+
shadow.innerHTML = `
|
|
2965
|
+
<style>
|
|
2966
|
+
:host {
|
|
2967
|
+
display: block;
|
|
2968
|
+
padding: 16px;
|
|
2969
|
+
background: #fffbe6;
|
|
2970
|
+
border: 1px solid #ffe58f;
|
|
2971
|
+
border-radius: 4px;
|
|
2972
|
+
color: #faad14;
|
|
2973
|
+
font-size: 14px;
|
|
2974
|
+
}
|
|
2975
|
+
.icon {
|
|
2976
|
+
margin-right: 8px;
|
|
2977
|
+
}
|
|
2978
|
+
</style>
|
|
2979
|
+
<span class="icon">\u26A0\uFE0F</span>
|
|
2980
|
+
<span>\u6B64\u7EC4\u4EF6\u5DF2\u88AB\u6682\u505C\u4F7F\u7528</span>
|
|
2981
|
+
`;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
if (!customElements.get("djvlc-error-boundary")) {
|
|
2987
|
+
customElements.define(
|
|
2988
|
+
"djvlc-error-boundary",
|
|
2989
|
+
class extends HTMLElement {
|
|
2990
|
+
constructor() {
|
|
2991
|
+
super();
|
|
2992
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
2993
|
+
shadow.innerHTML = `
|
|
2994
|
+
<style>
|
|
2995
|
+
:host {
|
|
2996
|
+
display: block;
|
|
2997
|
+
padding: 16px;
|
|
2998
|
+
background: #f5f5f5;
|
|
2999
|
+
border: 1px dashed #d9d9d9;
|
|
3000
|
+
border-radius: 4px;
|
|
3001
|
+
color: #999;
|
|
3002
|
+
font-size: 14px;
|
|
3003
|
+
text-align: center;
|
|
3004
|
+
}
|
|
3005
|
+
</style>
|
|
3006
|
+
<slot>\u6E32\u67D3\u51FA\u9519</slot>
|
|
3007
|
+
`;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
function createFallbackElement(type, message, componentName) {
|
|
3014
|
+
const tagName = `djvlc-${type === "error" ? "error-boundary" : type}`;
|
|
3015
|
+
const element = document.createElement(tagName);
|
|
3016
|
+
if (message) {
|
|
3017
|
+
element.setAttribute("message", message);
|
|
3018
|
+
}
|
|
3019
|
+
if (componentName) {
|
|
3020
|
+
element.setAttribute("component-name", componentName);
|
|
3021
|
+
}
|
|
3022
|
+
return element;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
// src/runtime.ts
|
|
3026
|
+
function createRuntime(options) {
|
|
3027
|
+
return new DjvlcRuntime(options);
|
|
3028
|
+
}
|
|
3029
|
+
var DjvlcRuntime = class {
|
|
3030
|
+
constructor(options) {
|
|
3031
|
+
this.container = null;
|
|
3032
|
+
this.options = {
|
|
3033
|
+
channel: "prod",
|
|
3034
|
+
debug: false,
|
|
3035
|
+
enableSRI: true,
|
|
3036
|
+
...options
|
|
3037
|
+
};
|
|
3038
|
+
this.logger = this.createLogger();
|
|
3039
|
+
this.stateManager = new StateManager();
|
|
3040
|
+
this.eventBus = new EventBus({ debug: options.debug, logger: this.logger });
|
|
3041
|
+
this.expressionEngine = new ExpressionEngine({ debug: options.debug, logger: this.logger });
|
|
3042
|
+
this.pageLoader = new PageLoader({
|
|
3043
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
3044
|
+
channel: options.channel,
|
|
3045
|
+
authToken: options.authToken,
|
|
3046
|
+
previewToken: options.previewToken,
|
|
3047
|
+
headers: options.headers,
|
|
3048
|
+
logger: this.logger
|
|
3049
|
+
});
|
|
3050
|
+
this.componentLoader = new ComponentLoader({
|
|
3051
|
+
cdnBaseUrl: options.cdnBaseUrl,
|
|
3052
|
+
enableSRI: options.enableSRI,
|
|
3053
|
+
logger: this.logger
|
|
3054
|
+
});
|
|
3055
|
+
this.assetLoader = new AssetLoader({
|
|
3056
|
+
cdnHosts: [new URL(options.cdnBaseUrl).host],
|
|
3057
|
+
apiHosts: [new URL(options.apiBaseUrl).host]
|
|
3058
|
+
});
|
|
3059
|
+
this.securityManager = new SecurityManager({
|
|
3060
|
+
enableSRI: options.enableSRI,
|
|
3061
|
+
cdnDomains: [new URL(options.cdnBaseUrl).host],
|
|
3062
|
+
apiDomains: [new URL(options.apiBaseUrl).host],
|
|
3063
|
+
logger: this.logger
|
|
3064
|
+
});
|
|
3065
|
+
this.log("info", "Runtime created");
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* 初始化运行时
|
|
3069
|
+
*/
|
|
3070
|
+
async init() {
|
|
3071
|
+
this.log("info", "Initializing runtime");
|
|
3072
|
+
const startTime = performance.now();
|
|
3073
|
+
try {
|
|
3074
|
+
this.container = this.resolveContainer();
|
|
3075
|
+
this.assetLoader.preconnectAll();
|
|
3076
|
+
this.pageLoader.preconnect();
|
|
3077
|
+
registerFallbackComponents();
|
|
3078
|
+
this.stateManager.setPhase("resolving");
|
|
3079
|
+
const initTime = performance.now() - startTime;
|
|
3080
|
+
this.log("info", `Runtime initialized in ${initTime.toFixed(2)}ms`);
|
|
3081
|
+
} catch (error) {
|
|
3082
|
+
this.handleError(error);
|
|
3083
|
+
throw error;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* 加载页面
|
|
3088
|
+
*/
|
|
3089
|
+
async load() {
|
|
3090
|
+
this.log("info", "Loading page:", this.options.pageUid);
|
|
3091
|
+
const startTime = performance.now();
|
|
3092
|
+
try {
|
|
3093
|
+
this.stateManager.setPhase("resolving");
|
|
3094
|
+
const page = await this.pageLoader.resolve(this.options.pageUid, {
|
|
3095
|
+
uid: this.options.userId,
|
|
3096
|
+
deviceId: this.options.deviceId
|
|
3097
|
+
});
|
|
3098
|
+
this.stateManager.setPage(page);
|
|
3099
|
+
this.telemetryManager = new TelemetryManager({
|
|
3100
|
+
pageVersionId: page.pageVersionId,
|
|
3101
|
+
debug: this.options.debug,
|
|
3102
|
+
logger: this.logger,
|
|
3103
|
+
onMetric: this.options.onMetric
|
|
3104
|
+
});
|
|
3105
|
+
if (page.runtimeConfig) {
|
|
3106
|
+
this.securityManager.updateBlockedList(
|
|
3107
|
+
page.runtimeConfig.blockedComponents || [],
|
|
3108
|
+
page.runtimeConfig.killSwitches || []
|
|
3109
|
+
);
|
|
3110
|
+
this.componentLoader.updateBlockedList(
|
|
3111
|
+
page.runtimeConfig.blockedComponents?.map((c) => `${c.name}@${c.version}`) || []
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
this.stateManager.setPhase("loading");
|
|
3115
|
+
this.componentLoader.preload(page.manifest.components);
|
|
3116
|
+
const componentResults = await this.componentLoader.loadAll(page.manifest);
|
|
3117
|
+
componentResults.forEach((result, key) => {
|
|
3118
|
+
this.stateManager.setComponentStatus(key, result);
|
|
3119
|
+
this.telemetryManager.recordComponentLoad(
|
|
3120
|
+
result.name,
|
|
3121
|
+
result.version,
|
|
3122
|
+
result.loadTime || 0,
|
|
3123
|
+
result.status === "loaded"
|
|
3124
|
+
);
|
|
3125
|
+
});
|
|
3126
|
+
this.initHostApi(page);
|
|
3127
|
+
this.initRenderer();
|
|
3128
|
+
const loadTime = performance.now() - startTime;
|
|
3129
|
+
this.telemetryManager.recordPageLoad(loadTime);
|
|
3130
|
+
this.log("info", `Page loaded in ${loadTime.toFixed(2)}ms`);
|
|
3131
|
+
this.emitEvent("page:loaded", { page, loadTime });
|
|
3132
|
+
return page;
|
|
3133
|
+
} catch (error) {
|
|
3134
|
+
this.stateManager.setPhase("error");
|
|
3135
|
+
this.handleError(error);
|
|
3136
|
+
throw error;
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
/**
|
|
3140
|
+
* 渲染页面
|
|
3141
|
+
*/
|
|
3142
|
+
async render() {
|
|
3143
|
+
const state = this.stateManager.getState();
|
|
3144
|
+
if (!state.page || !this.container) {
|
|
3145
|
+
throw new PageLoadError("Page not loaded");
|
|
3146
|
+
}
|
|
3147
|
+
this.log("info", "Rendering page");
|
|
3148
|
+
const startTime = performance.now();
|
|
3149
|
+
try {
|
|
3150
|
+
this.stateManager.setPhase("rendering");
|
|
3151
|
+
this.renderer.updateContext({
|
|
3152
|
+
state: state.variables,
|
|
3153
|
+
query: state.queries,
|
|
3154
|
+
context: {
|
|
3155
|
+
userId: this.options.userId,
|
|
3156
|
+
deviceId: this.options.deviceId,
|
|
3157
|
+
channel: this.options.channel,
|
|
3158
|
+
pageVersionId: state.page.pageVersionId
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
3161
|
+
this.renderer.render(state.page.pageJson, this.container);
|
|
3162
|
+
this.stateManager.setPhase("ready");
|
|
3163
|
+
const renderTime = performance.now() - startTime;
|
|
3164
|
+
this.telemetryManager.recordFirstRender(renderTime);
|
|
3165
|
+
this.log("info", `Page rendered in ${renderTime.toFixed(2)}ms`);
|
|
3166
|
+
} catch (error) {
|
|
3167
|
+
this.stateManager.setPhase("error");
|
|
3168
|
+
this.handleError(error);
|
|
3169
|
+
throw error;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
/**
|
|
3173
|
+
* 获取 Host API
|
|
3174
|
+
*/
|
|
3175
|
+
getHostApi() {
|
|
3176
|
+
return this.hostApi;
|
|
3177
|
+
}
|
|
3178
|
+
/**
|
|
3179
|
+
* 获取当前状态
|
|
3180
|
+
*/
|
|
3181
|
+
getState() {
|
|
3182
|
+
return this.stateManager.getState();
|
|
3183
|
+
}
|
|
3184
|
+
/**
|
|
3185
|
+
* 订阅状态变更
|
|
3186
|
+
*/
|
|
3187
|
+
onStateChange(listener) {
|
|
3188
|
+
return this.stateManager.subscribe(listener);
|
|
3189
|
+
}
|
|
3190
|
+
/**
|
|
3191
|
+
* 订阅事件
|
|
3192
|
+
*/
|
|
3193
|
+
on(type, handler) {
|
|
3194
|
+
return this.eventBus.on(type, handler);
|
|
3195
|
+
}
|
|
3196
|
+
/**
|
|
3197
|
+
* 更新组件
|
|
3198
|
+
*/
|
|
3199
|
+
updateComponent(componentId, props) {
|
|
3200
|
+
this.renderer.updateComponent(componentId, props);
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* 设置变量
|
|
3204
|
+
*/
|
|
3205
|
+
setVariable(key, value) {
|
|
3206
|
+
this.stateManager.setVariable(key, value);
|
|
3207
|
+
const state = this.stateManager.getState();
|
|
3208
|
+
if (state.page && this.container) {
|
|
3209
|
+
this.renderer.updateContext({
|
|
3210
|
+
state: state.variables
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* 刷新数据
|
|
3216
|
+
*/
|
|
3217
|
+
async refreshData(queryId) {
|
|
3218
|
+
const result = await this.hostApi.requestData(queryId);
|
|
3219
|
+
if (result.success) {
|
|
3220
|
+
this.stateManager.setQuery(queryId, result.data);
|
|
3221
|
+
const state = this.stateManager.getState();
|
|
3222
|
+
this.renderer.updateContext({
|
|
3223
|
+
query: state.queries
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* 销毁运行时
|
|
3229
|
+
*/
|
|
3230
|
+
destroy() {
|
|
3231
|
+
this.log("info", "Destroying runtime");
|
|
3232
|
+
this.telemetryManager?.flush();
|
|
3233
|
+
this.renderer?.destroy();
|
|
3234
|
+
this.eventBus.clear();
|
|
3235
|
+
this.stateManager.setDestroyed();
|
|
3236
|
+
if (this.container) {
|
|
3237
|
+
this.container.innerHTML = "";
|
|
3238
|
+
}
|
|
3239
|
+
this.log("info", "Runtime destroyed");
|
|
3240
|
+
}
|
|
3241
|
+
// ==================== 私有方法 ====================
|
|
3242
|
+
resolveContainer() {
|
|
3243
|
+
const { container } = this.options;
|
|
3244
|
+
if (typeof container === "string") {
|
|
3245
|
+
const element = document.querySelector(container);
|
|
3246
|
+
if (!element) {
|
|
3247
|
+
throw new Error(`Container not found: ${container}`);
|
|
3248
|
+
}
|
|
3249
|
+
return element;
|
|
3250
|
+
}
|
|
3251
|
+
return container;
|
|
3252
|
+
}
|
|
3253
|
+
initHostApi(page) {
|
|
3254
|
+
const context = {
|
|
3255
|
+
pageUid: page.pageUid,
|
|
3256
|
+
pageVersionId: page.pageVersionId,
|
|
3257
|
+
runtimeVersion: "0.1.0",
|
|
3258
|
+
userId: this.options.userId,
|
|
3259
|
+
deviceId: this.options.deviceId,
|
|
3260
|
+
channel: this.options.channel,
|
|
3261
|
+
isEditMode: false,
|
|
3262
|
+
isPreviewMode: page.isPreview || false
|
|
3263
|
+
};
|
|
3264
|
+
this.hostApi = new HostAPIImpl({
|
|
3265
|
+
apiBaseUrl: this.options.apiBaseUrl,
|
|
3266
|
+
authToken: this.options.authToken,
|
|
3267
|
+
headers: this.options.headers,
|
|
3268
|
+
stateManager: this.stateManager,
|
|
3269
|
+
eventBus: this.eventBus,
|
|
3270
|
+
expressionEngine: this.expressionEngine,
|
|
3271
|
+
context,
|
|
3272
|
+
debug: this.options.debug,
|
|
3273
|
+
logger: this.logger
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
initRenderer() {
|
|
3277
|
+
const components = /* @__PURE__ */ new Map();
|
|
3278
|
+
const state = this.stateManager.getState();
|
|
3279
|
+
state.components.forEach((result, key) => {
|
|
3280
|
+
if (result.status === "loaded" && result.component) {
|
|
3281
|
+
const [name, version] = key.split("@");
|
|
3282
|
+
components.set(name, {
|
|
3283
|
+
name,
|
|
3284
|
+
version,
|
|
3285
|
+
Component: result.component,
|
|
3286
|
+
loadTime: result.loadTime || 0
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
});
|
|
3290
|
+
this.renderer = new BaseRenderer({
|
|
3291
|
+
expressionEngine: this.expressionEngine,
|
|
3292
|
+
components,
|
|
3293
|
+
injectHostApi: (element, componentId) => {
|
|
3294
|
+
element.hostApi = this.hostApi;
|
|
3295
|
+
element.componentId = componentId;
|
|
3296
|
+
},
|
|
3297
|
+
debug: this.options.debug,
|
|
3298
|
+
logger: this.logger,
|
|
3299
|
+
onRenderError: (componentId, error) => {
|
|
3300
|
+
this.log("error", `Render error in ${componentId}:`, error);
|
|
3301
|
+
this.emitEvent("component:error", { componentId, error: error.message });
|
|
3302
|
+
return createFallbackElement("error", error.message);
|
|
3303
|
+
}
|
|
3304
|
+
});
|
|
3305
|
+
this.renderer.init();
|
|
3306
|
+
}
|
|
3307
|
+
handleError(error) {
|
|
3308
|
+
const runtimeError = error instanceof DjvlcRuntimeError ? error : { type: "LOAD_ERROR", message: error.message, timestamp: Date.now() };
|
|
3309
|
+
this.stateManager.setError(runtimeError);
|
|
3310
|
+
this.telemetryManager?.recordError(error);
|
|
3311
|
+
this.emitEvent("page:error", { error: error.message });
|
|
3312
|
+
if (this.options.onError) {
|
|
3313
|
+
this.options.onError(runtimeError);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
emitEvent(type, data) {
|
|
3317
|
+
const event = EventBus.createEvent(type, data, this.telemetryManager?.getTraceId());
|
|
3318
|
+
this.eventBus.emit(event);
|
|
3319
|
+
if (this.options.onEvent) {
|
|
3320
|
+
this.options.onEvent(event);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
createLogger() {
|
|
3324
|
+
const prefix = "[DJVLC]";
|
|
3325
|
+
return {
|
|
3326
|
+
debug: (...args) => {
|
|
3327
|
+
if (this.options.debug) console.debug(prefix, ...args);
|
|
3328
|
+
},
|
|
3329
|
+
info: (...args) => console.info(prefix, ...args),
|
|
3330
|
+
warn: (...args) => console.warn(prefix, ...args),
|
|
3331
|
+
error: (...args) => console.error(prefix, ...args)
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
log(level, message, ...args) {
|
|
3335
|
+
this.logger[level](message, ...args);
|
|
3336
|
+
}
|
|
3337
|
+
};
|
|
3338
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3339
|
+
0 && (module.exports = {
|
|
3340
|
+
ActionBridge,
|
|
3341
|
+
ActionError,
|
|
3342
|
+
AssetLoader,
|
|
3343
|
+
BaseRenderer,
|
|
3344
|
+
ComponentBlockedError,
|
|
3345
|
+
ComponentLoadError,
|
|
3346
|
+
ComponentLoader,
|
|
3347
|
+
DjvlcRuntime,
|
|
3348
|
+
DjvlcRuntimeError,
|
|
3349
|
+
Evaluator,
|
|
3350
|
+
EventBus,
|
|
3351
|
+
ExpressionEngine,
|
|
3352
|
+
ExpressionError,
|
|
3353
|
+
HostAPIImpl,
|
|
3354
|
+
IntegrityError,
|
|
3355
|
+
Lexer,
|
|
3356
|
+
PageLoadError,
|
|
3357
|
+
PageLoader,
|
|
3358
|
+
Parser,
|
|
3359
|
+
QueryError,
|
|
3360
|
+
RenderError,
|
|
3361
|
+
SecurityManager,
|
|
3362
|
+
StateManager,
|
|
3363
|
+
TelemetryManager,
|
|
3364
|
+
builtinFunctions,
|
|
3365
|
+
createFallbackElement,
|
|
3366
|
+
createRuntime,
|
|
3367
|
+
registerFallbackComponents
|
|
3368
|
+
});
|