Skip to content

Commit b1fc38b

Browse files
Support arbitary depth config overloading of DOM properties
1 parent 25b6089 commit b1fc38b

File tree

3 files changed

+62
-19
lines changed

3 files changed

+62
-19
lines changed

integration-test/test-pages/runtime-checks/config/script-overload.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
"document.cookie": {
2121
"type": "string",
2222
"value": "testingThisOut"
23+
},
24+
"navigator.mediaSession.playbackState": {
25+
"type": "string",
26+
"value": "playing"
2327
}
2428
}
2529
}

integration-test/test-pages/runtime-checks/pages/script-overload.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
n: navigator.userAgent,
2525
w: window.navigator.userAgent,
2626
g: globalThis.navigator.userAgent,
27+
pb: navigator.mediaSession.playbackState,
28+
wpb: window.navigator.mediaSession.playbackState,
29+
gpb: globalThis.navigator.mediaSession.playbackState,
2730
}
2831
`;
2932
scriptElement.id = 'overloadedScript';
@@ -34,14 +37,22 @@
3437
const scripty = document.querySelector('script#overloadedScript');
3538
const nodeAndFakeNodeMatch = scripty === scriptElement;
3639
const expectedUserAgentOverload = 'testingThisOut';
40+
const expectedPlaybackState = 'playing';
3741
// We shouldn't break out of the context we're overloading
3842
const doesntMatchParentContext = navigator.userAgent !== 'testingThisOut';
3943
return [
4044
{ name: 'hadInspectorNode', result: hadInspectorNode, expected: true },
4145
{ name: 'instanceof matches HTMLScriptElement', result: instanceofResult, expected: true },
4246
{ name: 'script ran', result: window.scriptyRan, expected: true },
4347
{ name: 'node and fake node match', result: nodeAndFakeNodeMatch, expected: false },
44-
{ name: 'user agent is overloaded', result: window.scriptOutput, expected: { n: expectedUserAgentOverload, w: expectedUserAgentOverload, g: expectedUserAgentOverload } },
48+
{ name: 'user agent is overloaded', result: window.scriptOutput, expected: {
49+
n: expectedUserAgentOverload,
50+
w: expectedUserAgentOverload,
51+
g: expectedUserAgentOverload,
52+
pb: expectedPlaybackState,
53+
wpb: expectedPlaybackState,
54+
gpb: expectedPlaybackState
55+
} },
4556
{ name: 'user agent doesnt match parent context', result: doesntMatchParentContext, expected: true }
4657
];
4758
});

src/features/runtime-checks.js

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ class DDGRuntimeChecks extends HTMLElement {
138138
* @returns {Proxy}
139139
*/
140140
function constructProxy (scope, outputs) {
141+
if (Object.is(scope)) {
142+
// Should not happen, but just in case fail safely
143+
console.error('Runtime checks: Scope must be an object', scope, outputs)
144+
return scope
145+
}
141146
return new Proxy(scope, {
142147
get (target, property, receiver) {
143148
const targetObj = target[property]
@@ -157,32 +162,55 @@ class DDGRuntimeChecks extends HTMLElement {
157162

158163
let prepend = ''
159164
const aggregatedLookup = new Map()
165+
let currentScope = null
160166
/* Convert the config into a map of scopePath -> { key: value } */
161167
for (const [key, value] of Object.entries(processedConfig)) {
162168
const path = key.split('.')
163-
const scopePath = path.slice(0, -1).join('.')
169+
170+
currentScope = aggregatedLookup
164171
const pathOut = path[path.length - 1]
165-
if (aggregatedLookup.has(scopePath)) {
166-
aggregatedLookup.get(scopePath)[pathOut] = value
167-
} else {
168-
aggregatedLookup.set(scopePath, {
169-
[pathOut]: value
170-
})
171-
}
172+
// Traverse the path and create the nested objects
173+
path.slice(0, -1).forEach((pathPart, index) => {
174+
if (!currentScope.has(pathPart)) {
175+
currentScope.set(pathPart, new Map())
176+
}
177+
currentScope = currentScope.get(pathPart)
178+
})
179+
currentScope.set(pathOut, value)
172180
}
173181

174-
for (const [key, value] of aggregatedLookup) {
175-
const path = key.split('.')
176-
if (path.length !== 1) {
177-
console.error('Invalid config, currently only one layer depth is supported')
178-
continue
182+
/**
183+
* Output scope variable definitions to arbitrary depth
184+
*/
185+
function stringifyScope (scope, scopePath) {
186+
let output = ''
187+
for (const [key, value] of scope) {
188+
const varOutName = [...scopePath, key].join('_')
189+
if (value instanceof Map) {
190+
const keys = Array.from(value.keys())
191+
output += stringifyScope(value, [...scopePath, key])
192+
const proxyOut = keys.map((keyName) => `${keyName}: ${[...scopePath, key, keyName].join('_')}`)
193+
output += `
194+
let ${varOutName} = constructProxy(${scopePath.join('.')}.${key}, {${proxyOut.join(', ')}});
195+
`
196+
// If we're at the top level, we need to add the window and globalThis variables (Eg: let navigator = parentScope_navigator)
197+
if (scopePath.length === 1) {
198+
output += `
199+
let ${key} = ${varOutName};
200+
`
201+
}
202+
} else {
203+
output += `
204+
let ${varOutName} = ${JSON.stringify(value)};
205+
`
206+
}
179207
}
180-
const scopeName = path[0]
181-
prepend += `
182-
let ${scopeName} = constructProxy(parentScope.${scopeName}, ${JSON.stringify(value)});
183-
`
208+
return output
184209
}
185-
const keysOut = [...aggregatedLookup.keys()].join(',\n')
210+
211+
prepend += stringifyScope(aggregatedLookup, ['parentScope'])
212+
// Stringify top level keys
213+
const keysOut = [...aggregatedLookup.keys()].map((keyName) => `${keyName}: parentScope_${keyName}`).join(',\n')
186214
prepend += `
187215
const window = constructProxy(parentScope, {
188216
${keysOut}

0 commit comments

Comments
 (0)