form0-core 0.1.7 → 0.1.9
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/package.json +2 -2
- package/src/builtins/control/setresult.js +1 -1
- package/src/builtins/event/control/off.js +11 -0
- package/src/builtins/event/control/on.js +16 -0
- package/src/builtins/registry.js +19 -5
- package/src/engine/calculation.js +276 -152
- package/src/engine/form-engine.js +7 -4
- package/src/index.js +5 -0
- package/src/security/validation.js +6 -7
- package/src/utilities/calculation-dependencies.js +311 -0
- package/src/utilities/calculation-editor.js +60 -97
- package/src/utilities/form-event-editor.js +488 -0
- package/src/utilities/value-equality.js +54 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { BUILTIN_CONTEXTS } from '../builtins/builtin-metadata.js';
|
|
2
|
+
import { BUILTIN_DEFINITION_BY_NAME, EVENT_BUILTIN_DEFINITIONS } from '../builtins/registry.js';
|
|
3
|
+
import { ContextResolver } from '../engine/context-resolver.js';
|
|
4
|
+
import { getAllEventTypes } from '../engine/event-registry.js';
|
|
5
|
+
import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
|
|
6
|
+
import { validateExpression } from '../security/validation.js';
|
|
7
|
+
import { flattenFields } from './field-helpers.js';
|
|
8
|
+
|
|
9
|
+
const COMMON_GLOBAL_FUNCTIONS = new Set([
|
|
10
|
+
'Math',
|
|
11
|
+
'Date',
|
|
12
|
+
'JSON',
|
|
13
|
+
'Number',
|
|
14
|
+
'String',
|
|
15
|
+
'Array',
|
|
16
|
+
'Object',
|
|
17
|
+
'Boolean',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const STRUCTURAL_FIELD_TYPES = new Set([
|
|
21
|
+
'Section',
|
|
22
|
+
'RepeatableSection',
|
|
23
|
+
'BuildingPlanSection',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const FORM_EVENT_BUILTIN_STATUS_BY_NAME = Object.freeze({
|
|
27
|
+
EVAL: 'advanced',
|
|
28
|
+
FORM: 'unavailable',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function getFormEventBuiltinStatus(name) {
|
|
32
|
+
return FORM_EVENT_BUILTIN_STATUS_BY_NAME[name] ?? 'stable';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toFormEventBuiltinDefinition(definition) {
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
name: definition.name,
|
|
38
|
+
category: definition.category,
|
|
39
|
+
signature: definition.signature,
|
|
40
|
+
description: definition.description,
|
|
41
|
+
examples: Object.freeze([...definition.examples]),
|
|
42
|
+
status: getFormEventBuiltinStatus(definition.name),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const FORM_EVENT_BUILTIN_CATALOG = Object.freeze(
|
|
47
|
+
EVENT_BUILTIN_DEFINITIONS.map(toFormEventBuiltinDefinition)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const FORM_EVENT_BUILTIN_METADATA_BY_NAME = new Map(
|
|
51
|
+
FORM_EVENT_BUILTIN_CATALOG.map((definition) => [definition.name, definition])
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
function normalizeFormSchema(schema) {
|
|
55
|
+
if (schema && Array.isArray(schema.elements)) {
|
|
56
|
+
return schema;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (schema && schema.form && Array.isArray(schema.form.elements)) {
|
|
60
|
+
return schema.form;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new Error('Expected a form schema or a root schema containing form.elements');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isBuiltinCallMatchAllowed(functionName) {
|
|
67
|
+
return COMMON_GLOBAL_FUNCTIONS.has(functionName);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function removeEvalContents(code) {
|
|
71
|
+
let result = '';
|
|
72
|
+
let i = 0;
|
|
73
|
+
|
|
74
|
+
while (i < code.length) {
|
|
75
|
+
const evalMatch = code.substring(i).match(/^EVAL\s*\(/);
|
|
76
|
+
if (evalMatch) {
|
|
77
|
+
result += 'EVAL()';
|
|
78
|
+
i += evalMatch[0].length;
|
|
79
|
+
|
|
80
|
+
let parenCount = 1;
|
|
81
|
+
let inSingleQuote = false;
|
|
82
|
+
let inDoubleQuote = false;
|
|
83
|
+
let inBacktick = false;
|
|
84
|
+
|
|
85
|
+
while (i < code.length && parenCount > 0) {
|
|
86
|
+
const char = code[i];
|
|
87
|
+
|
|
88
|
+
if (char === "'" && !inDoubleQuote && !inBacktick) {
|
|
89
|
+
inSingleQuote = !inSingleQuote;
|
|
90
|
+
} else if (char === '"' && !inSingleQuote && !inBacktick) {
|
|
91
|
+
inDoubleQuote = !inDoubleQuote;
|
|
92
|
+
} else if (char === '`' && !inSingleQuote && !inDoubleQuote) {
|
|
93
|
+
inBacktick = !inBacktick;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
|
|
97
|
+
if (char === '(') {
|
|
98
|
+
parenCount++;
|
|
99
|
+
} else if (char === ')') {
|
|
100
|
+
parenCount--;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
i++;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
result += code[i];
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractFunctionCalls(code) {
|
|
116
|
+
const functionCallPattern = /\b([A-Z_][A-Z0-9_]*)\s*\(/g;
|
|
117
|
+
const matches = [];
|
|
118
|
+
let match;
|
|
119
|
+
|
|
120
|
+
while ((match = functionCallPattern.exec(code)) !== null) {
|
|
121
|
+
matches.push({
|
|
122
|
+
name: match[1],
|
|
123
|
+
index: match.index,
|
|
124
|
+
length: match[1].length,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return matches;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractFieldReferenceMatches(code) {
|
|
132
|
+
const cleanedCode = removeEvalContents(code);
|
|
133
|
+
const fieldRegex = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
|
|
134
|
+
const matches = [];
|
|
135
|
+
let match;
|
|
136
|
+
|
|
137
|
+
while ((match = fieldRegex.exec(cleanedCode)) !== null) {
|
|
138
|
+
matches.push({
|
|
139
|
+
fieldName: match[1],
|
|
140
|
+
reference: `$${match[1]}`,
|
|
141
|
+
index: match.index,
|
|
142
|
+
length: match[0].length,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return matches;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function extractLiteralEventTypeMatches(code) {
|
|
150
|
+
const matches = [];
|
|
151
|
+
const eventTypeCallPattern = /\b(ON|OFF)\s*\(\s*(['"])([^'"\\]*(?:\\.[^'"\\]*)*)\2/g;
|
|
152
|
+
let match;
|
|
153
|
+
|
|
154
|
+
while ((match = eventTypeCallPattern.exec(code)) !== null) {
|
|
155
|
+
const eventType = match[3];
|
|
156
|
+
const eventTypeIndex = match.index + match[0].indexOf(eventType);
|
|
157
|
+
matches.push({
|
|
158
|
+
builtinName: match[1],
|
|
159
|
+
eventType,
|
|
160
|
+
index: eventTypeIndex,
|
|
161
|
+
length: eventType.length,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return matches;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function cloneCatalogDefinition(definition) {
|
|
169
|
+
return {
|
|
170
|
+
...definition,
|
|
171
|
+
examples: [...definition.examples],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toFormEventReferenceDefinition(field, resolver) {
|
|
176
|
+
return {
|
|
177
|
+
key: typeof field.key === 'string' ? field.key : field.data_name,
|
|
178
|
+
reference: `$${field.data_name}`,
|
|
179
|
+
dataName: field.data_name,
|
|
180
|
+
label: typeof field.label === 'string' ? field.label : field.data_name,
|
|
181
|
+
type: typeof field.type === 'string' ? field.type : 'UnknownField',
|
|
182
|
+
description: typeof field.description === 'string' ? field.description : null,
|
|
183
|
+
displayStyle:
|
|
184
|
+
field.display && typeof field.display === 'object' && typeof field.display.style === 'string'
|
|
185
|
+
? field.display.style
|
|
186
|
+
: typeof field.display === 'string'
|
|
187
|
+
? field.display
|
|
188
|
+
: null,
|
|
189
|
+
parentPath: resolver.getFieldInfo(field.data_name)?.parentPath || [],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function createIssue({ code, severity, message, symbol = null, index = null, length = null }) {
|
|
194
|
+
return {
|
|
195
|
+
code,
|
|
196
|
+
severity,
|
|
197
|
+
message,
|
|
198
|
+
symbol,
|
|
199
|
+
startIndex: typeof index === 'number' ? index : null,
|
|
200
|
+
endIndex: typeof index === 'number' && typeof length === 'number' ? index + length : null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function addIssue(issues, issueKeys, issue) {
|
|
205
|
+
const key = `${issue.code}:${issue.symbol || issue.message}`;
|
|
206
|
+
if (issueKeys.has(key)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
issueKeys.add(key);
|
|
210
|
+
issues.push(issue);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function validateSyntax(code) {
|
|
214
|
+
new Function(code);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function createSchemaFilteredSecurityConfig(securityConfig) {
|
|
218
|
+
if (!securityConfig || typeof securityConfig !== 'object') {
|
|
219
|
+
return DEFAULT_SECURITY_CONFIG;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
...securityConfig,
|
|
224
|
+
validateBuiltins: false,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @typedef {'stable' | 'advanced' | 'unavailable'} FormEventBuiltinStatus
|
|
230
|
+
*
|
|
231
|
+
* @typedef {Object} FormEventBuiltinDefinition
|
|
232
|
+
* @property {string} name
|
|
233
|
+
* @property {string} category
|
|
234
|
+
* @property {string} signature
|
|
235
|
+
* @property {string} description
|
|
236
|
+
* @property {string[]} examples
|
|
237
|
+
* @property {FormEventBuiltinStatus} status
|
|
238
|
+
*
|
|
239
|
+
* @typedef {Object} FormEventReferenceDefinition
|
|
240
|
+
* @property {string} key
|
|
241
|
+
* @property {string} reference
|
|
242
|
+
* @property {string} dataName
|
|
243
|
+
* @property {string} label
|
|
244
|
+
* @property {string} type
|
|
245
|
+
* @property {string | null} description
|
|
246
|
+
* @property {string | null} displayStyle
|
|
247
|
+
* @property {string[]} parentPath
|
|
248
|
+
*
|
|
249
|
+
* @typedef {Object} FormEventCodeIssue
|
|
250
|
+
* @property {string} code
|
|
251
|
+
* @property {'error' | 'warning'} severity
|
|
252
|
+
* @property {string} message
|
|
253
|
+
* @property {string | null} symbol
|
|
254
|
+
* @property {number | null} startIndex
|
|
255
|
+
* @property {number | null} endIndex
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Return the canonical builtin catalog for form event authoring.
|
|
260
|
+
*
|
|
261
|
+
* @returns {FormEventBuiltinDefinition[]}
|
|
262
|
+
*/
|
|
263
|
+
export function getFormEventBuiltinCatalog() {
|
|
264
|
+
return FORM_EVENT_BUILTIN_CATALOG.map(cloneCatalogDefinition);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Return the list of field references available for form event authoring.
|
|
269
|
+
*
|
|
270
|
+
* @param {Object} options
|
|
271
|
+
* @param {Object} options.schema
|
|
272
|
+
* @returns {FormEventReferenceDefinition[]}
|
|
273
|
+
*/
|
|
274
|
+
export function getFormEventReferenceCatalog({ schema }) {
|
|
275
|
+
const form = normalizeFormSchema(schema);
|
|
276
|
+
const resolver = new ContextResolver(form);
|
|
277
|
+
|
|
278
|
+
return flattenFields(form.elements)
|
|
279
|
+
.filter((field) => field && typeof field === 'object')
|
|
280
|
+
.filter((field) => field.data_name)
|
|
281
|
+
.filter((field) => !STRUCTURAL_FIELD_TYPES.has(field.type))
|
|
282
|
+
.map((field) => toFormEventReferenceDefinition(field, resolver));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Statistically analyze form event code without executing it.
|
|
287
|
+
*
|
|
288
|
+
* @param {Object} options
|
|
289
|
+
* @param {string} options.code
|
|
290
|
+
* @param {Object} options.schema
|
|
291
|
+
* @param {Object} [options.securityConfig]
|
|
292
|
+
* @returns {{
|
|
293
|
+
* valid: boolean,
|
|
294
|
+
* issues: FormEventCodeIssue[],
|
|
295
|
+
* usedBuiltins: string[],
|
|
296
|
+
* referencedFields: string[],
|
|
297
|
+
* }}
|
|
298
|
+
*/
|
|
299
|
+
export function analyzeFormEventCode({
|
|
300
|
+
code,
|
|
301
|
+
schema,
|
|
302
|
+
securityConfig = DEFAULT_SECURITY_CONFIG,
|
|
303
|
+
}) {
|
|
304
|
+
const issues = [];
|
|
305
|
+
const issueKeys = new Set();
|
|
306
|
+
const usedBuiltins = [];
|
|
307
|
+
const usedBuiltinNames = new Set();
|
|
308
|
+
const referencedFields = [];
|
|
309
|
+
const referencedFieldNames = new Set();
|
|
310
|
+
const validEventTypes = new Set(getAllEventTypes());
|
|
311
|
+
const normalizedCode = typeof code === 'string' ? code : '';
|
|
312
|
+
const form = normalizeFormSchema(schema);
|
|
313
|
+
const knownFieldNames = new Set(
|
|
314
|
+
flattenFields(form.elements)
|
|
315
|
+
.filter((field) => field && typeof field === 'object')
|
|
316
|
+
.map((field) => field.data_name)
|
|
317
|
+
.filter((fieldName) => typeof fieldName === 'string' && fieldName.length > 0)
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
if (normalizedCode.trim().length > 0) {
|
|
321
|
+
try {
|
|
322
|
+
validateSyntax(normalizedCode);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
addIssue(
|
|
325
|
+
issues,
|
|
326
|
+
issueKeys,
|
|
327
|
+
createIssue({
|
|
328
|
+
code: 'invalid_syntax',
|
|
329
|
+
severity: 'error',
|
|
330
|
+
message:
|
|
331
|
+
error instanceof Error && error.message
|
|
332
|
+
? `Invalid form event syntax: ${error.message}`
|
|
333
|
+
: 'Invalid form event syntax.',
|
|
334
|
+
index: 0,
|
|
335
|
+
length: 1,
|
|
336
|
+
})
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const securityValidation = validateExpression(
|
|
342
|
+
normalizedCode,
|
|
343
|
+
createSchemaFilteredSecurityConfig(securityConfig),
|
|
344
|
+
true
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
if (!securityValidation.valid) {
|
|
348
|
+
addIssue(
|
|
349
|
+
issues,
|
|
350
|
+
issueKeys,
|
|
351
|
+
createIssue({
|
|
352
|
+
code: 'security_validation_failed',
|
|
353
|
+
severity: 'error',
|
|
354
|
+
message: securityValidation.reason,
|
|
355
|
+
index: 0,
|
|
356
|
+
length: 1,
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const call of extractFunctionCalls(normalizedCode)) {
|
|
362
|
+
if (isBuiltinCallMatchAllowed(call.name)) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const builtinDefinition = BUILTIN_DEFINITION_BY_NAME.get(call.name);
|
|
367
|
+
if (builtinDefinition) {
|
|
368
|
+
const isEventBuiltin = builtinDefinition.contexts.includes(BUILTIN_CONTEXTS.EVENT);
|
|
369
|
+
|
|
370
|
+
if (!isEventBuiltin && builtinDefinition.contexts.includes(BUILTIN_CONTEXTS.CALCULATION)) {
|
|
371
|
+
addIssue(
|
|
372
|
+
issues,
|
|
373
|
+
issueKeys,
|
|
374
|
+
createIssue({
|
|
375
|
+
code: 'calculation_builtin_not_allowed',
|
|
376
|
+
severity: 'error',
|
|
377
|
+
message: `${call.name}() is only available in CalculatedField expressions, not in form event code.`,
|
|
378
|
+
symbol: call.name,
|
|
379
|
+
index: call.index,
|
|
380
|
+
length: call.length,
|
|
381
|
+
})
|
|
382
|
+
);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const builtinMetadata = FORM_EVENT_BUILTIN_METADATA_BY_NAME.get(call.name);
|
|
387
|
+
if (!builtinMetadata) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!usedBuiltinNames.has(call.name)) {
|
|
392
|
+
usedBuiltinNames.add(call.name);
|
|
393
|
+
usedBuiltins.push(call.name);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (builtinMetadata.status === 'unavailable') {
|
|
397
|
+
addIssue(
|
|
398
|
+
issues,
|
|
399
|
+
issueKeys,
|
|
400
|
+
createIssue({
|
|
401
|
+
code: 'unavailable_builtin',
|
|
402
|
+
severity: 'error',
|
|
403
|
+
message: `${call.name}() is not available in form event code.`,
|
|
404
|
+
symbol: call.name,
|
|
405
|
+
index: call.index,
|
|
406
|
+
length: call.length,
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
} else if (builtinMetadata.status === 'advanced') {
|
|
410
|
+
addIssue(
|
|
411
|
+
issues,
|
|
412
|
+
issueKeys,
|
|
413
|
+
createIssue({
|
|
414
|
+
code: 'advanced_builtin',
|
|
415
|
+
severity: 'warning',
|
|
416
|
+
message: `${call.name}() is an advanced builtin. Prefer direct $field references when possible.`,
|
|
417
|
+
symbol: call.name,
|
|
418
|
+
index: call.index,
|
|
419
|
+
length: call.length,
|
|
420
|
+
})
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
addIssue(
|
|
428
|
+
issues,
|
|
429
|
+
issueKeys,
|
|
430
|
+
createIssue({
|
|
431
|
+
code: 'unknown_builtin',
|
|
432
|
+
severity: 'error',
|
|
433
|
+
message: `Unknown builtin function: ${call.name}.`,
|
|
434
|
+
symbol: call.name,
|
|
435
|
+
index: call.index,
|
|
436
|
+
length: call.length,
|
|
437
|
+
})
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for (const reference of extractFieldReferenceMatches(normalizedCode)) {
|
|
442
|
+
if (!referencedFieldNames.has(reference.fieldName)) {
|
|
443
|
+
referencedFieldNames.add(reference.fieldName);
|
|
444
|
+
referencedFields.push(reference.fieldName);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!knownFieldNames.has(reference.fieldName)) {
|
|
448
|
+
addIssue(
|
|
449
|
+
issues,
|
|
450
|
+
issueKeys,
|
|
451
|
+
createIssue({
|
|
452
|
+
code: 'unknown_field_reference',
|
|
453
|
+
severity: 'error',
|
|
454
|
+
message: `Unknown field reference: ${reference.reference}.`,
|
|
455
|
+
symbol: reference.reference,
|
|
456
|
+
index: reference.index,
|
|
457
|
+
length: reference.length,
|
|
458
|
+
})
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
for (const eventTypeReference of extractLiteralEventTypeMatches(normalizedCode)) {
|
|
464
|
+
if (validEventTypes.has(eventTypeReference.eventType)) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
addIssue(
|
|
469
|
+
issues,
|
|
470
|
+
issueKeys,
|
|
471
|
+
createIssue({
|
|
472
|
+
code: 'unknown_event_type',
|
|
473
|
+
severity: 'error',
|
|
474
|
+
message: `Unknown event type: ${eventTypeReference.eventType}.`,
|
|
475
|
+
symbol: eventTypeReference.eventType,
|
|
476
|
+
index: eventTypeReference.index,
|
|
477
|
+
length: eventTypeReference.length,
|
|
478
|
+
})
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
valid: issues.every((issue) => issue.severity !== 'error'),
|
|
484
|
+
issues,
|
|
485
|
+
usedBuiltins,
|
|
486
|
+
referencedFields,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
if (!value || typeof value !== 'object') {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function areStructuredValuesEqual(left, right) {
|
|
10
|
+
if (Object.is(left, right)) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (left instanceof Date && right instanceof Date) {
|
|
15
|
+
return left.getTime() === right.getTime();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
19
|
+
if (left.length !== right.length) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
24
|
+
if (!areStructuredValuesEqual(left[index], right[index])) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
33
|
+
const leftKeys = Object.keys(left);
|
|
34
|
+
const rightKeys = Object.keys(right);
|
|
35
|
+
|
|
36
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const key of leftKeys) {
|
|
41
|
+
if (!Object.prototype.hasOwnProperty.call(right, key)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!areStructuredValuesEqual(left[key], right[key])) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false;
|
|
54
|
+
}
|