@flisk/analyze-tracking 0.8.8 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -19
- package/package.json +5 -3
- package/schema.json +10 -0
- package/src/analyze/index.js +20 -13
- package/src/analyze/javascript/detectors/analytics-source.js +38 -4
- package/src/analyze/javascript/extractors/event-extractor.js +29 -12
- package/src/analyze/javascript/parser.js +26 -8
- package/src/analyze/swift/constants.js +61 -0
- package/src/analyze/swift/custom.js +36 -0
- package/src/analyze/swift/index.js +708 -0
- package/src/analyze/swift/providers.js +51 -0
- package/src/analyze/swift/runtime.js +46 -0
- package/src/analyze/swift/utils.js +75 -0
- package/src/analyze/typescript/detectors/analytics-source.js +37 -3
- package/src/analyze/typescript/extractors/event-extractor.js +59 -31
- package/src/analyze/typescript/extractors/property-extractor.js +129 -91
- package/src/analyze/typescript/parser.js +27 -8
- package/src/analyze/typescript/utils/type-resolver.js +600 -21
- package/src/analyze/utils/customFunctionParser.js +29 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider detection for Swift analytics SDKs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { normalizeChainPart, sliceRange } = require('./utils');
|
|
6
|
+
|
|
7
|
+
function detectProvider(call, source) {
|
|
8
|
+
const method = call.name;
|
|
9
|
+
const chain = Array.isArray(call.calleeChain) ? call.calleeChain : [];
|
|
10
|
+
const recvText = call.receiver || null;
|
|
11
|
+
const recvBase = recvText ? recvText.split('.')[0] : null;
|
|
12
|
+
const base = call.baseIdentifier || recvBase || (chain.length ? normalizeChainPart(chain[0]).split('.')[0] : null);
|
|
13
|
+
const methodCand = method || (chain.length ? normalizeChainPart(chain[chain.length - 1]) : null);
|
|
14
|
+
|
|
15
|
+
if (base === 'dataLayer' && (methodCand === 'append' || methodCand === 'push')) return 'gtm';
|
|
16
|
+
if (base === 'Analytics' && methodCand === 'logEvent') return 'googleanalytics';
|
|
17
|
+
if (base === 'analytics' && methodCand === 'track') return 'segment';
|
|
18
|
+
if (base === 'Mixpanel' && methodCand === 'track') return 'mixpanel';
|
|
19
|
+
if (base === 'amplitude' && methodCand === 'track') return 'amplitude';
|
|
20
|
+
if (base === 'RSClient' && methodCand === 'track') return 'rudderstack';
|
|
21
|
+
if (base === 'MParticle' && methodCand === 'logEvent') return 'mparticle';
|
|
22
|
+
if (base === 'PostHogSDK' && methodCand === 'capture') return 'posthog';
|
|
23
|
+
if (base === 'PendoManager' && methodCand === 'track') return 'pendo';
|
|
24
|
+
if (base === 'Heap' && methodCand === 'track') return 'heap';
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const text = sliceRange(source, call.range || {});
|
|
28
|
+
const t = text.replace(/\s+/g, '');
|
|
29
|
+
if (/\bdataLayer\.(append|push)\(/.test(t)) return 'gtm';
|
|
30
|
+
if (/\bAnalytics\.logEvent\(/.test(t)) return 'googleanalytics';
|
|
31
|
+
if (/\banalytics\.track\(/.test(t)) return 'segment';
|
|
32
|
+
if (/\bMixpanel\.[A-Za-z0-9_]+\(\)\.track\(/.test(t) || /\bMixpanel\.track\(/.test(t)) return 'mixpanel';
|
|
33
|
+
if (/\bamplitude\.track\(/.test(t)) return 'amplitude';
|
|
34
|
+
if (/\bRSClient\.[A-Za-z0-9_?]+\(\)?(?:\?\.|\.)track\(/.test(t) || /\bRSClient\(\)\.track\(/.test(t)) return 'rudderstack';
|
|
35
|
+
if (/\bMParticle\.[A-Za-z0-9_]+\(\)\.logEvent\(/.test(t)) return 'mparticle';
|
|
36
|
+
if (/\bPostHogSDK\.[A-Za-z0-9_]+\.capture\(/.test(t)) return 'posthog';
|
|
37
|
+
if (/\bPendoManager\.[A-Za-z0-9_]+\(\)\.track\(/.test(t)) return 'pendo';
|
|
38
|
+
if (/\bHeap\.[A-Za-z0-9_]+\.track\(/.test(t)) return 'heap';
|
|
39
|
+
} catch (_) {}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (methodCand === 'append') {
|
|
43
|
+
const text = sliceRange(source, call.range || {});
|
|
44
|
+
if (text.includes('event:')) return 'gtm';
|
|
45
|
+
}
|
|
46
|
+
} catch (_) {}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { detectProvider };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swift runtime bridge for @flisk/swift-ast
|
|
3
|
+
* - Provides lazy ESM/CJS loading
|
|
4
|
+
* - Serializes WASI-backed calls to avoid double-start errors
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { pathToFileURL: pathToFileUrl } = require('url');
|
|
10
|
+
|
|
11
|
+
let __swiftAst = null;
|
|
12
|
+
async function getSwiftAst() {
|
|
13
|
+
if (__swiftAst) return __swiftAst;
|
|
14
|
+
try {
|
|
15
|
+
// Try CJS require first (when package exposes CJS entry)
|
|
16
|
+
// eslint-disable-next-line global-require
|
|
17
|
+
__swiftAst = require('@flisk/swift-ast');
|
|
18
|
+
return __swiftAst;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
// Fallback to ESM dynamic import
|
|
21
|
+
try {
|
|
22
|
+
const m = await import('@flisk/swift-ast');
|
|
23
|
+
__swiftAst = m;
|
|
24
|
+
return __swiftAst;
|
|
25
|
+
} catch (_) {
|
|
26
|
+
// Final fallback: local workspace copy of swift-ast (for dev)
|
|
27
|
+
const localDist = path.resolve('/Users/sameenkarim/flisk/dev/swift-ast/dist/index.js');
|
|
28
|
+
if (fs.existsSync(localDist)) {
|
|
29
|
+
const m2 = await import(pathToFileUrl(localDist).href);
|
|
30
|
+
__swiftAst = m2;
|
|
31
|
+
return __swiftAst;
|
|
32
|
+
}
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Serialize WASI-backed swift-ast operations
|
|
39
|
+
let __swiftLock = Promise.resolve();
|
|
40
|
+
function withSwift(callback) {
|
|
41
|
+
const p = __swiftLock.then(callback, callback);
|
|
42
|
+
__swiftLock = p.then(() => {}, () => {});
|
|
43
|
+
return p;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { getSwiftAst, withSwift };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Swift analyzer utilities
|
|
3
|
+
* - String inference, key resolution, text slicing, and traversal helpers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function normalizeChainPart(p) {
|
|
7
|
+
if (!p) return p;
|
|
8
|
+
return String(p).replace(/\s+/g, '').replace(/\(\)$/g, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function endsWithChain(chain, sigParts) {
|
|
12
|
+
if (sigParts.length > chain.length) return false;
|
|
13
|
+
for (let i = 1; i <= sigParts.length; i++) {
|
|
14
|
+
if (normalizeChainPart(chain[chain.length - i]) !== sigParts[sigParts.length - i]) return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function extractStringLiteral(text) {
|
|
20
|
+
const m = /^\s*"([\s\S]*?)"\s*$/.exec(text || '');
|
|
21
|
+
return m ? m[1] : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isIdentifier(text) {
|
|
25
|
+
return /^[_A-Za-z][_A-Za-z0-9\.]*$/.test(text || '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function inferValueTypeFromText(text) {
|
|
29
|
+
const t = (text || '').trim();
|
|
30
|
+
if (/^"/.test(t)) return { type: 'string' };
|
|
31
|
+
if (/^(true|false)$/i.test(t)) return { type: 'boolean' };
|
|
32
|
+
if (/^[0-9]+(\.[0-9]+)?$/.test(t)) return { type: 'number' };
|
|
33
|
+
if (/^\[/.test(t)) {
|
|
34
|
+
const inside = t.slice(1, -1).trim();
|
|
35
|
+
if (!inside) return { type: 'array', items: { type: 'any' } };
|
|
36
|
+
if (/^(\s*"[\s\S]*?"\s*,)*\s*"[\s\S]*?"\s*$/.test(inside)) return { type: 'array', items: { type: 'string' } };
|
|
37
|
+
if (/^(\s*[0-9]+(\.[0-9]+)?\s*,)*\s*[0-9]+(\.[0-9]+)?\s*$/.test(inside)) return { type: 'array', items: { type: 'number' } };
|
|
38
|
+
return { type: 'array', items: { type: 'any' } };
|
|
39
|
+
}
|
|
40
|
+
if (/^\{/.test(t) || /\)$/.test(t)) return { type: 'object' };
|
|
41
|
+
if (isIdentifier(t)) return { type: 'string' };
|
|
42
|
+
return { type: 'any' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function splitTopLevel(s) {
|
|
46
|
+
const items = [];
|
|
47
|
+
let depthBr = 0, depthPr = 0; let cur = '';
|
|
48
|
+
for (let i = 0; i < s.length; i++) {
|
|
49
|
+
const ch = s[i];
|
|
50
|
+
if (ch === '[') depthBr++; else if (ch === ']') depthBr--;
|
|
51
|
+
else if (ch === '(') depthPr++; else if (ch === ')') depthPr--;
|
|
52
|
+
if (ch === ',' && depthBr === 0 && depthPr === 0) { items.push(cur); cur = ''; continue; }
|
|
53
|
+
cur += ch;
|
|
54
|
+
}
|
|
55
|
+
if (cur.trim()) items.push(cur);
|
|
56
|
+
return items;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sliceRange(source, range) {
|
|
60
|
+
const s = range?.start?.offset || 0; const e = range?.end?.offset || s;
|
|
61
|
+
return source.slice(s, e);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function escapeRegExp(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
normalizeChainPart,
|
|
68
|
+
endsWithChain,
|
|
69
|
+
extractStringLiteral,
|
|
70
|
+
isIdentifier,
|
|
71
|
+
inferValueTypeFromText,
|
|
72
|
+
splitTopLevel,
|
|
73
|
+
sliceRange,
|
|
74
|
+
escapeRegExp,
|
|
75
|
+
};
|
|
@@ -9,16 +9,25 @@ const { ANALYTICS_PROVIDERS } = require('../constants');
|
|
|
9
9
|
/**
|
|
10
10
|
* Detects the analytics provider from a CallExpression node
|
|
11
11
|
* @param {Object} node - TypeScript CallExpression node
|
|
12
|
-
* @param {string} [
|
|
12
|
+
* @param {string|Object} [customFunctionOrConfig] - Custom function name string or custom config object
|
|
13
13
|
* @returns {string} The detected analytics source or 'unknown'
|
|
14
14
|
*/
|
|
15
|
-
function detectAnalyticsSource(node,
|
|
15
|
+
function detectAnalyticsSource(node, customFunctionOrConfig) {
|
|
16
16
|
if (!node.expression) {
|
|
17
17
|
return 'unknown';
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Check for custom function first
|
|
21
|
-
|
|
21
|
+
// Support both old string format and new config object format
|
|
22
|
+
const customConfig = typeof customFunctionOrConfig === 'object' ? customFunctionOrConfig : null;
|
|
23
|
+
const customFunction = typeof customFunctionOrConfig === 'string' ? customFunctionOrConfig : (customConfig?.functionName);
|
|
24
|
+
|
|
25
|
+
if (customConfig?.isMethodAsEvent) {
|
|
26
|
+
// Method-as-event pattern: match any method on the specified object
|
|
27
|
+
if (isMethodAsEventFunction(node, customConfig)) {
|
|
28
|
+
return 'custom';
|
|
29
|
+
}
|
|
30
|
+
} else if (customFunction && isCustomFunction(node, customFunction)) {
|
|
22
31
|
return 'custom';
|
|
23
32
|
}
|
|
24
33
|
|
|
@@ -37,6 +46,31 @@ function detectAnalyticsSource(node, customFunction) {
|
|
|
37
46
|
return 'unknown';
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Checks if the node matches a method-as-event custom function pattern
|
|
51
|
+
* @param {Object} node - TypeScript CallExpression node
|
|
52
|
+
* @param {Object} customConfig - Custom function configuration with isMethodAsEvent: true
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
function isMethodAsEventFunction(node, customConfig) {
|
|
56
|
+
if (!customConfig?.isMethodAsEvent || !customConfig?.objectName) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Must be a PropertyAccessExpression: objectName.methodName(...)
|
|
61
|
+
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The object part must match the configured objectName
|
|
66
|
+
const objectExpr = node.expression.expression;
|
|
67
|
+
if (!ts.isIdentifier(objectExpr)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return objectExpr.escapedText === customConfig.objectName;
|
|
72
|
+
}
|
|
73
|
+
|
|
40
74
|
/**
|
|
41
75
|
* Checks if the node is a custom function call
|
|
42
76
|
* @param {Object} node - TypeScript CallExpression node
|
|
@@ -76,10 +76,10 @@ function extractSnowplowEvent(node, checker, sourceFile) {
|
|
|
76
76
|
|
|
77
77
|
// tracker.track(buildStructEvent({ action: 'event_name', ... }))
|
|
78
78
|
const firstArg = node.arguments[0];
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
// Check if it's a direct buildStructEvent call
|
|
81
|
-
if (ts.isCallExpression(firstArg) &&
|
|
82
|
-
ts.isIdentifier(firstArg.expression) &&
|
|
81
|
+
if (ts.isCallExpression(firstArg) &&
|
|
82
|
+
ts.isIdentifier(firstArg.expression) &&
|
|
83
83
|
firstArg.expression.escapedText === 'buildStructEvent' &&
|
|
84
84
|
firstArg.arguments.length > 0) {
|
|
85
85
|
const structEventArg = firstArg.arguments[0];
|
|
@@ -141,7 +141,7 @@ function extractGTMEvent(node, checker, sourceFile) {
|
|
|
141
141
|
|
|
142
142
|
// dataLayer.push({ event: 'event_name', property1: 'value1', property2: 'value2' })
|
|
143
143
|
const firstArg = node.arguments[0];
|
|
144
|
-
|
|
144
|
+
|
|
145
145
|
if (!ts.isObjectLiteralExpression(firstArg)) {
|
|
146
146
|
return { eventName: null, propertiesNode: null };
|
|
147
147
|
}
|
|
@@ -153,7 +153,7 @@ function extractGTMEvent(node, checker, sourceFile) {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
const eventName = getStringValue(eventProperty.initializer, checker, sourceFile);
|
|
156
|
-
|
|
156
|
+
|
|
157
157
|
// Create a modified properties node without the 'event' property
|
|
158
158
|
const modifiedProperties = firstArg.properties.filter(prop => {
|
|
159
159
|
if (ts.isPropertyAssignment(prop) && prop.name) {
|
|
@@ -169,7 +169,7 @@ function extractGTMEvent(node, checker, sourceFile) {
|
|
|
169
169
|
|
|
170
170
|
// Create a synthetic object literal with the filtered properties
|
|
171
171
|
const modifiedPropertiesNode = ts.factory.createObjectLiteralExpression(modifiedProperties);
|
|
172
|
-
|
|
172
|
+
|
|
173
173
|
// Copy source positions for proper analysis
|
|
174
174
|
if (firstArg.pos !== undefined) {
|
|
175
175
|
modifiedPropertiesNode.pos = firstArg.pos;
|
|
@@ -192,10 +192,31 @@ function extractGTMEvent(node, checker, sourceFile) {
|
|
|
192
192
|
function extractCustomEvent(node, checker, sourceFile, customConfig) {
|
|
193
193
|
const args = node.arguments || [];
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
let eventName;
|
|
196
|
+
let propertiesArg;
|
|
197
|
+
|
|
198
|
+
if (customConfig?.isMethodAsEvent) {
|
|
199
|
+
// Method-as-event pattern: event name comes from the method name
|
|
200
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
201
|
+
const methodName = node.expression.name;
|
|
202
|
+
if (methodName && ts.isIdentifier(methodName)) {
|
|
203
|
+
eventName = methodName.escapedText || methodName.text;
|
|
204
|
+
} else {
|
|
205
|
+
// Fallback: could not extract method name
|
|
206
|
+
eventName = null;
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
eventName = null;
|
|
210
|
+
}
|
|
197
211
|
|
|
198
|
-
|
|
212
|
+
// Properties are at the configured index (default 0)
|
|
213
|
+
propertiesArg = args[customConfig?.propertiesIndex ?? 0];
|
|
214
|
+
} else {
|
|
215
|
+
// Standard custom function pattern: event name comes from argument
|
|
216
|
+
const eventArg = args[customConfig?.eventIndex ?? 0];
|
|
217
|
+
propertiesArg = args[customConfig?.propertiesIndex ?? 1];
|
|
218
|
+
eventName = getStringValue(eventArg, checker, sourceFile);
|
|
219
|
+
}
|
|
199
220
|
|
|
200
221
|
const extraArgs = {};
|
|
201
222
|
if (customConfig && customConfig.extraParams) {
|
|
@@ -320,22 +341,22 @@ function processEventData(eventData, source, filePath, line, functionName, check
|
|
|
320
341
|
*/
|
|
321
342
|
function getStringValue(node, checker, sourceFile) {
|
|
322
343
|
if (!node) return null;
|
|
323
|
-
|
|
344
|
+
|
|
324
345
|
// Handle string literals (existing behavior)
|
|
325
346
|
if (ts.isStringLiteral(node)) {
|
|
326
347
|
return node.text;
|
|
327
348
|
}
|
|
328
|
-
|
|
349
|
+
|
|
329
350
|
// Handle property access expressions like TRACKING_EVENTS.ECOMMERCE_PURCHASE
|
|
330
351
|
if (ts.isPropertyAccessExpression(node)) {
|
|
331
352
|
return resolvePropertyAccessToString(node, checker, sourceFile);
|
|
332
353
|
}
|
|
333
|
-
|
|
354
|
+
|
|
334
355
|
// Handle identifiers that might reference constants
|
|
335
356
|
if (ts.isIdentifier(node)) {
|
|
336
357
|
return resolveIdentifierToString(node, checker, sourceFile);
|
|
337
358
|
}
|
|
338
|
-
|
|
359
|
+
|
|
339
360
|
return null;
|
|
340
361
|
}
|
|
341
362
|
|
|
@@ -438,39 +459,39 @@ function resolveIdentifierToString(node, checker, sourceFile) {
|
|
|
438
459
|
if (!symbol) {
|
|
439
460
|
return null;
|
|
440
461
|
}
|
|
441
|
-
|
|
462
|
+
|
|
442
463
|
// First try to resolve through value declaration
|
|
443
464
|
if (symbol.valueDeclaration) {
|
|
444
465
|
const declaration = symbol.valueDeclaration;
|
|
445
|
-
|
|
466
|
+
|
|
446
467
|
// Handle variable declarations with string literal initializers
|
|
447
|
-
if (ts.isVariableDeclaration(declaration) &&
|
|
468
|
+
if (ts.isVariableDeclaration(declaration) &&
|
|
448
469
|
declaration.initializer &&
|
|
449
470
|
ts.isStringLiteral(declaration.initializer)) {
|
|
450
471
|
return declaration.initializer.text;
|
|
451
472
|
}
|
|
452
|
-
|
|
473
|
+
|
|
453
474
|
// Handle const declarations with object literals containing string properties
|
|
454
|
-
if (ts.isVariableDeclaration(declaration) &&
|
|
475
|
+
if (ts.isVariableDeclaration(declaration) &&
|
|
455
476
|
declaration.initializer &&
|
|
456
477
|
ts.isObjectLiteralExpression(declaration.initializer)) {
|
|
457
478
|
// This case is handled by property access resolution
|
|
458
479
|
return null;
|
|
459
480
|
}
|
|
460
481
|
}
|
|
461
|
-
|
|
482
|
+
|
|
462
483
|
// If value declaration doesn't exist or doesn't help, try type resolution
|
|
463
484
|
// This handles imported constants that are resolved through TypeScript's type system
|
|
464
485
|
const type = checker.getTypeOfSymbolAtLocation(symbol, node);
|
|
465
486
|
if (type && type.isStringLiteral && typeof type.isStringLiteral === 'function' && type.isStringLiteral()) {
|
|
466
487
|
return type.value;
|
|
467
488
|
}
|
|
468
|
-
|
|
489
|
+
|
|
469
490
|
// Alternative approach for string literal types (different TypeScript versions)
|
|
470
491
|
if (type && type.flags && (type.flags & ts.TypeFlags.StringLiteral)) {
|
|
471
492
|
return type.value;
|
|
472
493
|
}
|
|
473
|
-
|
|
494
|
+
|
|
474
495
|
return null;
|
|
475
496
|
} catch (error) {
|
|
476
497
|
return null;
|
|
@@ -485,7 +506,7 @@ function resolveIdentifierToString(node, checker, sourceFile) {
|
|
|
485
506
|
*/
|
|
486
507
|
function findPropertyByKey(objectNode, key) {
|
|
487
508
|
if (!objectNode.properties) return null;
|
|
488
|
-
|
|
509
|
+
|
|
489
510
|
return objectNode.properties.find(prop => {
|
|
490
511
|
if (prop.name) {
|
|
491
512
|
if (ts.isIdentifier(prop.name)) {
|
|
@@ -506,30 +527,37 @@ function findPropertyByKey(objectNode, key) {
|
|
|
506
527
|
*/
|
|
507
528
|
function cleanupProperties(properties) {
|
|
508
529
|
const cleaned = {};
|
|
509
|
-
|
|
530
|
+
|
|
510
531
|
for (const [key, value] of Object.entries(properties)) {
|
|
511
532
|
if (value && typeof value === 'object') {
|
|
512
|
-
// Remove __unresolved marker
|
|
533
|
+
// Remove __unresolved marker from the value itself
|
|
513
534
|
if (value.__unresolved) {
|
|
514
535
|
delete value.__unresolved;
|
|
515
536
|
}
|
|
516
|
-
|
|
537
|
+
|
|
517
538
|
// Recursively clean nested properties
|
|
518
539
|
if (value.properties) {
|
|
519
540
|
value.properties = cleanupProperties(value.properties);
|
|
520
541
|
}
|
|
521
|
-
|
|
522
|
-
// Clean array item properties
|
|
523
|
-
if (value.type === 'array' && value.items
|
|
524
|
-
|
|
542
|
+
|
|
543
|
+
// Clean array item properties and __unresolved markers
|
|
544
|
+
if (value.type === 'array' && value.items) {
|
|
545
|
+
// Remove __unresolved from items directly
|
|
546
|
+
if (value.items.__unresolved) {
|
|
547
|
+
delete value.items.__unresolved;
|
|
548
|
+
}
|
|
549
|
+
// Clean nested properties in items
|
|
550
|
+
if (value.items.properties) {
|
|
551
|
+
value.items.properties = cleanupProperties(value.items.properties);
|
|
552
|
+
}
|
|
525
553
|
}
|
|
526
|
-
|
|
554
|
+
|
|
527
555
|
cleaned[key] = value;
|
|
528
556
|
} else {
|
|
529
557
|
cleaned[key] = value;
|
|
530
558
|
}
|
|
531
559
|
}
|
|
532
|
-
|
|
560
|
+
|
|
533
561
|
return cleaned;
|
|
534
562
|
}
|
|
535
563
|
|