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