@oxymoron.13/tizentube-s95b 0.2.2 → 0.2.3

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.
Files changed (84) hide show
  1. package/README.md +49 -42
  2. package/S95B-CHANGES.txt +10 -4
  3. package/UPSTREAM-README.md +42 -0
  4. package/dist/userScript.js +2 -2
  5. package/mods/config.js +117 -0
  6. package/mods/domrect-polyfill.js +104 -0
  7. package/mods/features/adblock.js +584 -0
  8. package/mods/features/autoFrameRate.js +42 -0
  9. package/mods/features/enableFeatures.js +19 -0
  10. package/mods/features/moreSubtitles.js +378 -0
  11. package/mods/features/pictureInPicture.js +198 -0
  12. package/mods/features/preferredVideoQuality.js +101 -0
  13. package/mods/features/s95bCodec.js +45 -0
  14. package/mods/features/s95bI18n.js +28 -0
  15. package/mods/features/s95bMenu.js +142 -0
  16. package/mods/features/sponsorblock.js +437 -0
  17. package/mods/features/standaloneUserscript.js +107 -0
  18. package/mods/features/updater.js +119 -0
  19. package/mods/features/userAgentSpoofing.js +73 -0
  20. package/mods/features/videoQueuing.js +64 -0
  21. package/mods/package-lock.json +2321 -0
  22. package/mods/package.json +33 -0
  23. package/mods/resolveCommand.js +376 -0
  24. package/mods/rollup.config.js +41 -0
  25. package/mods/spatial-navigation-polyfill.js +1756 -0
  26. package/mods/tiny-sha256.js +99 -0
  27. package/mods/translations/i18nResources.js +11 -0
  28. package/mods/translations/index.js +18 -0
  29. package/mods/translations/language-names.js +981 -0
  30. package/mods/translations/resources/ar.json +308 -0
  31. package/mods/translations/resources/az.json +308 -0
  32. package/mods/translations/resources/bs.json +308 -0
  33. package/mods/translations/resources/cs.json +308 -0
  34. package/mods/translations/resources/de.json +308 -0
  35. package/mods/translations/resources/dk.json +308 -0
  36. package/mods/translations/resources/el.json +308 -0
  37. package/mods/translations/resources/en.json +308 -0
  38. package/mods/translations/resources/es-419.json +308 -0
  39. package/mods/translations/resources/es.json +308 -0
  40. package/mods/translations/resources/fi-FI.json +308 -0
  41. package/mods/translations/resources/fr.json +311 -0
  42. package/mods/translations/resources/hr.json +308 -0
  43. package/mods/translations/resources/hu.json +308 -0
  44. package/mods/translations/resources/id.json +308 -0
  45. package/mods/translations/resources/it.json +308 -0
  46. package/mods/translations/resources/ja.json +308 -0
  47. package/mods/translations/resources/lt.json +308 -0
  48. package/mods/translations/resources/lv.json +308 -0
  49. package/mods/translations/resources/pl.json +308 -0
  50. package/mods/translations/resources/pt-BR.json +308 -0
  51. package/mods/translations/resources/pt-PT.json +308 -0
  52. package/mods/translations/resources/ro.json +310 -0
  53. package/mods/translations/resources/ru.json +308 -0
  54. package/mods/translations/resources/sr-Cyrl.json +308 -0
  55. package/mods/translations/resources/sr-Latn.json +308 -0
  56. package/mods/translations/resources/tlh.json +308 -0
  57. package/mods/translations/resources/tr.json +308 -0
  58. package/mods/translations/resources/uk.json +308 -0
  59. package/mods/translations/resources/vi.json +309 -0
  60. package/mods/translations/resources/zh-CN.json +308 -0
  61. package/mods/translations/resources/zh-TW.json +308 -0
  62. package/mods/translations/update-translations.js +55 -0
  63. package/mods/ui/chapters.js +137 -0
  64. package/mods/ui/clock.js +71 -0
  65. package/mods/ui/customCommandExecution.js +49 -0
  66. package/mods/ui/customGuideAction.js +119 -0
  67. package/mods/ui/customUI.js +192 -0
  68. package/mods/ui/customYTSettings.js +29 -0
  69. package/mods/ui/disableWhosWatching.js +48 -0
  70. package/mods/ui/settings.js +1024 -0
  71. package/mods/ui/sidebarModification.js +179 -0
  72. package/mods/ui/speedUI.js +119 -0
  73. package/mods/ui/theme.js +26 -0
  74. package/mods/ui/ui.css +157 -0
  75. package/mods/ui/ui.js +261 -0
  76. package/mods/ui/ytUI.js +510 -0
  77. package/mods/userScript.js +28 -0
  78. package/mods/utils/ASTParser.js +67 -0
  79. package/mods/utils/innerTubeCalls.js +138 -0
  80. package/package.json +11 -5
  81. package/service/package-lock.json +4193 -0
  82. package/service/package.json +29 -0
  83. package/service/rollup.config.js +47 -0
  84. package/service/service.js +118 -0
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@tizentube/mods",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "rollup -c rollup.config.js"
9
+ },
10
+ "keywords": [],
11
+ "author": "Reis Can, YouTube WebOs contributors",
12
+ "license": "GPL-3.0-only",
13
+ "dependencies": {
14
+ "@babel/preset-env": "^7.22.9",
15
+ "@rollup/plugin-babel": "^6.0.3",
16
+ "@rollup/plugin-terser": "^0.4.3",
17
+ "core-js": "^3.46.0",
18
+ "esprima": "^4.0.1",
19
+ "estraverse": "^5.3.0",
20
+ "i18next": "^25.8.18",
21
+ "qrcode-npm": "^0.0.3",
22
+ "rollup": "^3.26.2",
23
+ "rollup-plugin-string": "^3.0.0",
24
+ "tiny-sha256": "^1.0.2",
25
+ "whatwg-fetch": "^3.6.2"
26
+ },
27
+ "devDependencies": {
28
+ "@rollup/plugin-commonjs": "^29.0.0",
29
+ "@rollup/plugin-json": "^6.1.0",
30
+ "@rollup/plugin-node-resolve": "^16.0.3",
31
+ "@rollup/plugin-replace": "^6.0.3"
32
+ }
33
+ }
@@ -0,0 +1,376 @@
1
+ import { configWrite, configRead } from './config.js';
2
+ import { enablePip } from './features/pictureInPicture.js';
3
+ import modernUI, { optionShow } from './ui/settings.js';
4
+ import { speedSettings } from './ui/speedUI.js';
5
+ import { showToast, buttonItem, showModal, QrCodeRenderer, overlayPanelItemListRenderer, overlayMessageRenderer } from './ui/ytUI.js';
6
+ import checkForUpdates from './features/updater.js';
7
+ import { t } from 'i18next';
8
+ import { requestNextAndNavigateChannel, getFeedbackPanelTokens, sendFeedbackToken } from './utils/innerTubeCalls.js';
9
+ import qrcode from 'qrcode-npm';
10
+ import showGuideSettings from './ui/sidebarModification.js';
11
+
12
+ export default function resolveCommand(cmd, _) {
13
+ // resolveCommand function is pretty OP, it can do from opening modals, changing client settings and way more.
14
+ // Because the client might change, we should find it first.
15
+
16
+ for (const key in window._yttv) {
17
+ if (window._yttv[key] && window._yttv[key].instance && window._yttv[key].instance.resolveCommand) {
18
+ return window._yttv[key].instance.resolveCommand(cmd, _);
19
+ }
20
+ }
21
+ }
22
+
23
+ export function findFunction(funcName) {
24
+ for (const key in window._yttv) {
25
+ if (window._yttv[key] && window._yttv[key][funcName] && typeof window._yttv[key][funcName] === 'function') {
26
+ return window._yttv[key][funcName];
27
+ }
28
+ }
29
+ }
30
+
31
+ // Patch resolveCommand to be able to change TizenTube settings
32
+
33
+ export function patchResolveCommand() {
34
+ for (const key in window._yttv) {
35
+ if (window._yttv[key] && window._yttv[key].instance && window._yttv[key].instance.resolveCommand) {
36
+
37
+ const ogResolve = window._yttv[key].instance.resolveCommand;
38
+ window._yttv[key].instance.resolveCommand = function (cmd, _) {
39
+ if (cmd.setClientSettingEndpoint) {
40
+ // Command to change client settings. Use TizenTube configuration to change settings.
41
+ for (const settings of cmd.setClientSettingEndpoint.settingDatas) {
42
+ if (!settings.clientSettingEnum.item.includes('_')) {
43
+ for (const setting of cmd.setClientSettingEndpoint.settingDatas) {
44
+ const valName = Object.keys(setting).find(key => key.includes('Value'));
45
+ const value = valName === 'intValue' ? Number(setting[valName]) : setting[valName];
46
+ if (valName === 'arrayValue') {
47
+ const arr = configRead(setting.clientSettingEnum.item);
48
+ if (arr.includes(value)) {
49
+ arr.splice(arr.indexOf(value), 1);
50
+ } else {
51
+ arr.push(value);
52
+ }
53
+ configWrite(setting.clientSettingEnum.item, arr);
54
+ } else configWrite(setting.clientSettingEnum.item, value);
55
+ }
56
+ } else if (settings.clientSettingEnum.item === 'I18N_LANGUAGE') {
57
+ const lang = settings.stringValue;
58
+ const date = new Date();
59
+ date.setFullYear(date.getFullYear() + 10);
60
+ document.cookie = `PREF=hl=${lang}; expires=${date.toUTCString()};`;
61
+ resolveCommand({
62
+ signalAction: {
63
+ signal: 'RELOAD_PAGE'
64
+ }
65
+ });
66
+ return true;
67
+ }
68
+ }
69
+ } else if (cmd.customAction) {
70
+ customAction(cmd.customAction.action, cmd.customAction.parameters);
71
+ return true;
72
+ } else if (cmd?.signalAction?.customAction) {
73
+ customAction(cmd.signalAction.customAction.action, cmd.signalAction.customAction.parameters);
74
+ return true;
75
+ } else if (cmd?.showEngagementPanelEndpoint?.customAction) {
76
+ customAction(cmd.showEngagementPanelEndpoint.customAction.action, cmd.showEngagementPanelEndpoint.customAction.parameters);
77
+ return true;
78
+ } else if (cmd?.playlistEditEndpoint?.customAction) {
79
+ customAction(cmd.playlistEditEndpoint.customAction.action, cmd.playlistEditEndpoint.customAction.parameters);
80
+ return true;
81
+ } else if (cmd?.openPopupAction?.uniqueId === 'playback-settings') {
82
+ // Patch the playback settings popup to use TizenTube speed settings
83
+ const items = cmd.openPopupAction.popup.overlaySectionRenderer.overlay.overlayTwoPanelRenderer.actionPanel.overlayPanelRenderer.content.overlayPanelItemListRenderer.items;
84
+ for (const item of items) {
85
+ if (item?.compactLinkRenderer?.icon?.iconType === 'SLOW_MOTION_VIDEO') {
86
+ item.compactLinkRenderer.subtitle && (item.compactLinkRenderer.subtitle.simpleText = t('player.withTizenTube'));
87
+ item.compactLinkRenderer.serviceEndpoint = {
88
+ clickTrackingParams: "null",
89
+ signalAction: {
90
+ customAction: {
91
+ action: 'TT_SPEED_SETTINGS_SHOW',
92
+ parameters: []
93
+ }
94
+ }
95
+ };
96
+ }
97
+ }
98
+
99
+ cmd.openPopupAction.popup.overlaySectionRenderer.overlay.overlayTwoPanelRenderer.actionPanel.overlayPanelRenderer.content.overlayPanelItemListRenderer.items.splice(2, 0,
100
+ buttonItem(
101
+ { title: t('player.miniPlayer') },
102
+ { icon: 'CLEAR_COOKIES' }, [
103
+ {
104
+ customAction: {
105
+ action: 'ENTER_MP'
106
+ }
107
+ }
108
+ ])
109
+ );
110
+
111
+ cmd.openPopupAction.popup.overlaySectionRenderer.overlay.overlayTwoPanelRenderer.actionPanel.overlayPanelRenderer.content.overlayPanelItemListRenderer.items.splice(3, 0,
112
+ buttonItem(
113
+ { title: t('player.share.button') },
114
+ { icon: 'OPEN_IN_NEW' }, [
115
+ {
116
+ customAction: {
117
+ action: 'SHARE'
118
+ }
119
+ }
120
+ ])
121
+ );
122
+
123
+ cmd.openPopupAction.popup.overlaySectionRenderer.overlay.overlayTwoPanelRenderer.actionPanel.overlayPanelRenderer.content.overlayPanelItemListRenderer.items.splice(3, 0,
124
+ buttonItem(
125
+ { title: t('player.screenOff') },
126
+ { icon: 'EYE_OFF' }, [
127
+ {
128
+ customAction: {
129
+ action: 'SCREEN_OFF'
130
+ }
131
+ }
132
+ ])
133
+ );
134
+
135
+ if (window.h5vcc && window.h5vcc.tizentube && window.h5vcc.tizentube.HasSystemFeature &&
136
+ window.h5vcc.tizentube.HasSystemFeature('android.software.picture_in_picture')) {
137
+ cmd.openPopupAction.popup.overlaySectionRenderer.overlay.overlayTwoPanelRenderer.actionPanel.overlayPanelRenderer.content.overlayPanelItemListRenderer.items.splice(3, 0,
138
+ buttonItem(
139
+ { title: t('player.pictureInPicture') },
140
+ { icon: 'TV' }, [
141
+ {
142
+ customAction: {
143
+ action: 'ENTER_PIP'
144
+ }
145
+ },
146
+ {
147
+ signalAction: {
148
+ signal: 'POPUP_BACK'
149
+ }
150
+ }
151
+ ])
152
+ );
153
+ }
154
+ } else if (cmd?.watchEndpoint?.videoId) {
155
+ window.isPipPlaying = false;
156
+ const ytlrPlayerContainer = document.querySelector('ytlr-player-container');
157
+ ytlrPlayerContainer.style.removeProperty('z-index');
158
+ }
159
+
160
+ if (cmd.customAction) return window._yttv[key].instance.resolveCommand(cmd, _);
161
+
162
+ if (cmd.commandExecutorCommand && cmd.commandExecutorCommand.commands) {
163
+ for (const command of cmd.commandExecutorCommand.commands) {
164
+ if (command.customAction) {
165
+ customAction(command.customAction.action, command.customAction.parameters);
166
+ } else if (command.signalAction?.customAction) {
167
+ customAction(command.signalAction.customAction.action, command.signalAction.customAction.parameters);
168
+ } else if (command.showEngagementPanelEndpoint?.customAction) {
169
+ customAction(command.showEngagementPanelEndpoint.customAction.action, command.showEngagementPanelEndpoint.customAction.parameters);
170
+ } else if (command.playlistEditEndpoint?.customAction) {
171
+ customAction(command.playlistEditEndpoint.customAction.action, command.playlistEditEndpoint.customAction.parameters);
172
+ } else {
173
+ window._yttv[key].instance.resolveCommand(command, _);
174
+ }
175
+ }
176
+ return true;
177
+ }
178
+
179
+ if (cmd?.requestAccountSelectorCommand
180
+ && cmd.requestAccountSelectorCommand?.identityActionContext?.eventTrigger === 'ACCOUNT_EVENT_TRIGGER_ON_EXIT') {
181
+ if (!configRead('enableWhosWatchingMenuOnAppExit')) {
182
+ ogResolve.call(this, {
183
+ signalAction: {
184
+ signal: 'EXIT_APP'
185
+ }
186
+ });
187
+ return false;
188
+ }
189
+ }
190
+
191
+ return ogResolve.call(this, cmd, _);
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ function customAction(action, parameters) {
198
+ switch (action) {
199
+ case 'SETTINGS_UPDATE':
200
+ modernUI(true, parameters);
201
+ break;
202
+ case 'OPTIONS_SHOW':
203
+ optionShow(parameters, parameters.update);
204
+ break;
205
+ case 'SKIP':
206
+ const kE = document.createEvent('Event');
207
+ kE.initEvent('keydown', true, true);
208
+ kE.keyCode = 27;
209
+ kE.which = 27;
210
+ document.dispatchEvent(kE);
211
+
212
+ document.querySelector('video').currentTime = parameters.time;
213
+ break;
214
+ case 'TT_SETTINGS_SHOW':
215
+ modernUI();
216
+ break;
217
+ case 'TT_SPEED_SETTINGS_SHOW':
218
+ speedSettings();
219
+ break;
220
+ case 'UPDATE_REMIND_LATER':
221
+ configWrite('dontCheckUpdateUntil', parameters);
222
+ break;
223
+ case 'UPDATE_DOWNLOAD':
224
+ window.h5vcc.tizentube.InstallAppFromURL(parameters);
225
+ showToast(t('settings.options.updater.downloading.title'), t('settings.options.updater.downloading.subtitle'));
226
+ break;
227
+ case 'SET_PLAYER_SPEED':
228
+ const speed = Number(parameters);
229
+ document.querySelector('video').playbackRate = speed;
230
+ break;
231
+ case 'ENTER_MP':
232
+ enablePip();
233
+ break;
234
+ case 'ENTER_PIP':
235
+ window.h5vcc.tizentube.EnterPIP();
236
+ break;
237
+ case 'SHOW_TOAST':
238
+ showToast('TizenTube', parameters);
239
+ break;
240
+ case 'ADD_TO_QUEUE':
241
+ window.queuedVideos.videos.push(parameters);
242
+ showToast('TizenTube', t('toasts.videoAddedToQueue'));
243
+ break;
244
+ case 'CLEAR_QUEUE':
245
+ window.queuedVideos.videos = [];
246
+ showToast('TizenTube', t('toasts.videoQueueCleared'));
247
+ break;
248
+ case 'CHECK_FOR_UPDATES':
249
+ checkForUpdates(true);
250
+ break;
251
+ case 'GO_TO_CHANNEL':
252
+ requestNextAndNavigateChannel(parameters);
253
+ break;
254
+ case 'NOT_INTERESTED':
255
+ markFeedback(parameters, 0, true);
256
+ break;
257
+ case 'DONT_RECOMMEND_CHANNEL':
258
+ markFeedback(parameters, 1, false);
259
+ break;
260
+ case 'FEEDBACK_REASON':
261
+ sendFeedbackToken(parameters.token)
262
+ .then(() => {
263
+ resolveCommand({
264
+ signalAction: {
265
+ signal: 'POPUP_BACK'
266
+ }
267
+ });
268
+ showToast('TizenTube', t('toasts.feedbackSent'));
269
+ })
270
+ .catch(err => console.error('TizenTube: failed to send feedback:', err));
271
+ break;
272
+ case 'SHARE':
273
+ const videoPlayer = document.querySelector('.html5-video-player');
274
+ const videoData = videoPlayer.getVideoData();
275
+ const videoId = videoData.video_id;
276
+ const shareUrl = `https://www.youtube.com/watch?v=${videoId}`;
277
+
278
+ const qr = qrcode.qrcode(6, 'H');
279
+ qr.addData(shareUrl);
280
+ qr.make();
281
+
282
+ const qrDataImgTag = qr.createImgTag(8, 8);
283
+ const qrDataUrl = qrDataImgTag.match(/src="([^"]+)"/)[1];
284
+
285
+ showModal({
286
+ title: t('player.share.title'),
287
+ }, overlayPanelItemListRenderer([
288
+ overlayMessageRenderer(t('player.share.qrCodeScanMessage')),
289
+ QrCodeRenderer(qrDataUrl)
290
+ ]), 'tt-share-modal');
291
+ break;
292
+ case 'SHOW_GUIDE_BUTTONS':
293
+ showGuideSettings('SHOW_GUIDE_BUTTONS', parameters);
294
+ break;
295
+ case 'RELOAD_GUIDE_OPTIONS':
296
+ showGuideSettings(parameters.settingType, true);
297
+ break;
298
+ case 'MOVE_GUIDE_BUTTON':
299
+ showGuideSettings('MOVE_GUIDE_BUTTON', parameters);
300
+ break;
301
+ case 'SHOW_GUIDE_SETTINGS':
302
+ showGuideSettings(parameters);
303
+ break;
304
+ case 'ADD_OR_REMOVE_CHANNEL_TO_SIDEBAR':
305
+ const sortedSidebarContents = configRead('sidebarContentsOrder');
306
+
307
+ if (sortedSidebarContents.find(item => item.browseId === parameters.browseId)) {
308
+ sortedSidebarContents.splice(sortedSidebarContents.findIndex(item => item.browseId === parameters.browseId), 1);
309
+ } else {
310
+ sortedSidebarContents.push({
311
+ browseId: parameters.browseId,
312
+ title: parameters.title,
313
+ });
314
+ }
315
+ configWrite('sidebarContentsOrder', sortedSidebarContents);
316
+ showToast(t('toasts.sidebarContentsUpdated.title'), t('toasts.sidebarContentsUpdated.subtitle'));
317
+ break;
318
+ case 'SCREEN_OFF':
319
+ for (const child of document.body.children) {
320
+ if (child.tagName.toLowerCase() === 'script' || child.tagName.toLowerCase() === 'svg') continue;
321
+ child.style.setProperty('display', 'none', 'important');
322
+ }
323
+ window.screenTurnedOffAt = Date.now();
324
+ break;
325
+ }
326
+ }
327
+
328
+ // YouTube moved the "Not interested" / "Don't recommend channel" feedback tokens
329
+ // out of the long press menu into an engagement panel (see getFeedbackPanel in
330
+ // adblock.js). Fetch the panel first, then send the feedback with the token it
331
+ // returns (token 0 = "Not interested", token 1 = "Don't recommend channel").
332
+ // If YouTube asks for a dismissal reason, present the reasons in a modal and send
333
+ // the selected one (see FEEDBACK_REASON).
334
+ async function markFeedback(parameters, tokenIndex, showReasons) {
335
+ try {
336
+ const tokens = await getFeedbackPanelTokens(parameters.panelId, parameters.params);
337
+ const token = tokens[tokenIndex];
338
+ if (!token) return;
339
+
340
+ const response = await sendFeedbackToken(token);
341
+ if (!showReasons) return;
342
+
343
+ const dismissal = response?.feedbackResponses?.[0]?.followUpDialog?.dismissalFollowUpRenderer;
344
+ const reasons = dismissal?.reasons;
345
+ if (!Array.isArray(reasons) || !reasons.length) return;
346
+
347
+ const buttons = reasons.map((reason) => buttonItem(
348
+ { title: extractText(reason.title) },
349
+ null,
350
+ [
351
+ {
352
+ signalAction: {
353
+ signal: 'POPUP_BACK'
354
+ }
355
+ },
356
+ {
357
+ customAction: {
358
+ action: 'FEEDBACK_REASON',
359
+ parameters: { token: reason.token }
360
+ }
361
+ }
362
+ ]
363
+ ));
364
+ showModal(extractText(dismissal.dismissalReasonsPrompt), overlayPanelItemListRenderer(buttons), 'tt-feedback-reasons');
365
+ } catch (err) {
366
+ console.error('TizenTube: failed to mark feedback:', err);
367
+ }
368
+ }
369
+
370
+ function extractText(text) {
371
+ if (!text) return '';
372
+ if (text.simpleText) return text.simpleText;
373
+ if (text.content) return text.content;
374
+ if (Array.isArray(text.runs)) return text.runs.map((run) => run?.text ?? '').join('');
375
+ return String(text);
376
+ }
@@ -0,0 +1,41 @@
1
+ import { string } from 'rollup-plugin-string';
2
+ import terser from '@rollup/plugin-terser';
3
+ import getBabelOutputPlugin from '@rollup/plugin-babel';
4
+ import { nodeResolve } from '@rollup/plugin-node-resolve';
5
+ import commonjs from '@rollup/plugin-commonjs';
6
+ import replace from '@rollup/plugin-replace';
7
+ import json from '@rollup/plugin-json';
8
+
9
+ export default {
10
+ input: "userScript.js",
11
+ output: { file: "../dist/userScript.js", format: "iife" },
12
+ plugins: [
13
+ json(),
14
+ string({
15
+ include: "**/*.css",
16
+ }),
17
+ nodeResolve({
18
+ browser: true,
19
+ preferBuiltins: false,
20
+ }),
21
+ commonjs({
22
+ include: [/node_modules/, /mods/],
23
+ transformMixedEsModules: true,
24
+ }),
25
+ getBabelOutputPlugin({
26
+ babelHelpers: 'bundled',
27
+ presets: [
28
+ ['@babel/preset-env', {
29
+ targets: 'Chrome 47',
30
+ }],
31
+ ],
32
+ }),
33
+ terser({
34
+ ecma: '5',
35
+ mangle: true,
36
+ }),
37
+ replace({
38
+ '\uFFFF': '\u0000',
39
+ })
40
+ ]
41
+ };