Skip to content

Commit d309b19

Browse files
committed
fix: add content feature type
1 parent 6476ae4 commit d309b19

File tree

7 files changed

+188
-198
lines changed

7 files changed

+188
-198
lines changed

injected/src/config-feature.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { immutableJSONPatch } from 'immutable-json-patch';
2+
import { camelcase, computeEnabledFeatures, matchHostname, parseFeatureSettings } from './utils.js';
3+
4+
export default class ConfigFeature {
5+
/** @type {import('./utils.js').RemoteConfig | undefined} */
6+
#bundledConfig;
7+
8+
/** @type {any} */
9+
name;
10+
11+
/** @type {{ debug?: boolean, desktopModeEnabled?: boolean, forcedZoomEnabled?: boolean, featureSettings?: Record<string, unknown>, assets?: import('./content-feature.js').AssetConfig | undefined, site: Site, messagingConfig?: import('@duckduckgo/messaging').MessagingConfig } | null} */
12+
#args;
13+
14+
/**
15+
* @param {any} name
16+
*/
17+
constructor(name) {
18+
this.name = name;
19+
}
20+
21+
get args() {
22+
return this.#args;
23+
}
24+
25+
set args(args) {
26+
this.#args = args;
27+
}
28+
29+
get featureSettings() {
30+
return this.#args?.featureSettings;
31+
}
32+
33+
/**
34+
* @param {import('./content-scope-features.js').LoadArgs} loadArgs
35+
*/
36+
initLoadArgs(loadArgs) {
37+
const { bundledConfig, site, platform } = loadArgs;
38+
this.#bundledConfig = bundledConfig;
39+
this.#args = loadArgs;
40+
// If we have a bundled config, treat it as a regular config
41+
// This will be overriden by the remote config if it is available
42+
if (this.#bundledConfig && this.#args) {
43+
const enabledFeatures = computeEnabledFeatures(bundledConfig, site.domain, platform.version);
44+
this.#args.featureSettings = parseFeatureSettings(bundledConfig, enabledFeatures);
45+
}
46+
}
47+
48+
/**
49+
* Given a config key, interpret the value as a list of domain overrides, and return the elements that match the current page
50+
* Consider using patchSettings instead as per `getFeatureSetting`.
51+
* @param {string} featureKeyName
52+
* @return {any[]}
53+
* @protected
54+
*/
55+
matchDomainFeatureSetting(featureKeyName) {
56+
const domain = this.args?.site.domain;
57+
if (!domain) return [];
58+
const domains = this._getFeatureSettings()?.[featureKeyName] || [];
59+
return domains.filter((rule) => {
60+
if (Array.isArray(rule.domain)) {
61+
return rule.domain.some((domainRule) => {
62+
return matchHostname(domain, domainRule);
63+
});
64+
}
65+
return matchHostname(domain, rule.domain);
66+
});
67+
}
68+
69+
/**
70+
* Return the settings object for a feature
71+
* @param {string} [featureName] - The name of the feature to get the settings for; defaults to the name of the feature
72+
* @returns {any}
73+
*/
74+
_getFeatureSettings(featureName) {
75+
const camelFeatureName = featureName || camelcase(this.name);
76+
return this.featureSettings?.[camelFeatureName];
77+
}
78+
79+
/**
80+
* For simple boolean settings, return true if the setting is 'enabled'
81+
* For objects, verify the 'state' field is 'enabled'.
82+
* This allows for future forwards compatibility with more complex settings if required.
83+
* For example:
84+
* ```json
85+
* {
86+
* "toggle": "enabled"
87+
* }
88+
* ```
89+
* Could become later (without breaking changes):
90+
* ```json
91+
* {
92+
* "toggle": {
93+
* "state": "enabled",
94+
* "someOtherKey": 1
95+
* }
96+
* }
97+
* ```
98+
* This also supports domain overrides as per `getFeatureSetting`.
99+
* @param {string} featureKeyName
100+
* @param {string} [featureName]
101+
* @returns {boolean}
102+
*/
103+
getFeatureSettingEnabled(featureKeyName, featureName) {
104+
const result = this.getFeatureSetting(featureKeyName, featureName);
105+
if (typeof result === 'object') {
106+
return result.state === 'enabled';
107+
}
108+
return result === 'enabled';
109+
}
110+
111+
/**
112+
* Return a specific setting from the feature settings
113+
* If the "settings" key within the config has a "domains" key, it will be used to override the settings.
114+
* This uses JSONPatch to apply the patches to settings before getting the setting value.
115+
* For example.com getFeatureSettings('val') will return 1:
116+
* ```json
117+
* {
118+
* "settings": {
119+
* "domains": [
120+
* {
121+
* "domain": "example.com",
122+
* "patchSettings": [
123+
* { "op": "replace", "path": "/val", "value": 1 }
124+
* ]
125+
* }
126+
* ]
127+
* }
128+
* }
129+
* ```
130+
* "domain" can either be a string or an array of strings.
131+
132+
* For boolean states you should consider using getFeatureSettingEnabled.
133+
* @param {string} featureKeyName
134+
* @param {string} [featureName]
135+
* @returns {any}
136+
*/
137+
getFeatureSetting(featureKeyName, featureName) {
138+
let result = this._getFeatureSettings(featureName);
139+
if (featureKeyName === 'domains') {
140+
throw new Error('domains is a reserved feature setting key name');
141+
}
142+
const domainMatch = [...this.matchDomainFeatureSetting('domains')].sort((a, b) => {
143+
return a.domain.length - b.domain.length;
144+
});
145+
for (const match of domainMatch) {
146+
if (match.patchSettings === undefined) {
147+
continue;
148+
}
149+
try {
150+
result = immutableJSONPatch(result, match.patchSettings);
151+
} catch (e) {
152+
console.error('Error applying patch settings', e);
153+
}
154+
}
155+
return result?.[featureKeyName];
156+
}
157+
158+
/**
159+
* @returns {import('./utils.js').RemoteConfig | undefined}
160+
**/
161+
get bundledConfig() {
162+
return this.#bundledConfig;
163+
}
164+
}

0 commit comments

Comments
 (0)