1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
import React from 'dom-chef';
import {CachedFunction} from 'webext-storage-cache';
import {isEnterprise} from 'github-url-detection';
import compareVersions from 'tiny-version-compare';
import {any as concatenateTemplateLiteralTag} from 'code-tag';
import {RGHOptions} from '../options-storage.js';
import isDevelopmentVersion from './is-development-version.js';
const {version: currentVersion} = browser.runtime.getManifest();
function parseCsv(content: string): string[][] {
const lines = [];
const [_header, ...rawLines] = content.trim().split('\n');
for (const line of rawLines) {
if (line.trim()) {
lines.push(line.split(',').map(cell => cell.trim()));
}
}
return lines;
}
async function fetchHotfix(path: string): Promise<string> {
// The explicit endpoint is necessary because it shouldn't change on GHE
// We can't use `https://raw.githubusercontent.com` because of permission issues https://github.com/refined-github/refined-github/pull/3530#issuecomment-691595925
const request = await fetch(`https://api.github.com/repos/refined-github/yolo/contents/${path}`);
const {content} = await request.json();
// Rate-limit check
if (content) {
return atob(content).trim();
}
return '';
}
type HotfixStorage = Array<[FeatureID, string, string]>;
export const brokenFeatures = new CachedFunction('broken-features', {
async updater(): Promise<HotfixStorage> {
const content = await fetchHotfix('broken-features.csv');
if (!content) {
return [];
}
const storage: HotfixStorage = [];
for (const [featureID, relatedIssue, unaffectedVersion] of parseCsv(content)) {
if (featureID && relatedIssue && (!unaffectedVersion || compareVersions(unaffectedVersion, currentVersion) > 0)) {
storage.push([featureID as FeatureID, relatedIssue, unaffectedVersion]);
}
}
return storage;
},
maxAge: {hours: 6},
staleWhileRevalidate: {days: 30},
});
export const styleHotfixes = new CachedFunction('style-hotfixes', {
updater: async (version: string): Promise<string> => fetchHotfix(`style/${version}.css`),
maxAge: {hours: 6},
staleWhileRevalidate: {days: 300},
cacheKey: () => '',
},
);
export async function getLocalHotfixes(): Promise<HotfixStorage> {
// To facilitate debugging, ignore hotfixes during development.
// Change the version in manifest.json to test hotfixes
if (isDevelopmentVersion()) {
return [];
}
return await brokenFeatures.get() ?? [];
}
export async function getLocalHotfixesAsOptions(): Promise<Partial<RGHOptions>> {
const options: Partial<RGHOptions> = {};
for (const [feature] of await getLocalHotfixes()) {
options[`feature:${feature}`] = false;
}
return options;
}
export async function applyStyleHotfixes(style: string): Promise<void> {
if (isDevelopmentVersion() || isEnterprise() || !style) {
return;
}
// Prepend to body because that's the only way to guarantee they come after the static file
document.body.prepend(<style>{style}</style>);
}
let localStrings: Record<string, string> = {};
export function _(...arguments_: Parameters<typeof concatenateTemplateLiteralTag>): string {
const original = concatenateTemplateLiteralTag(...arguments_);
return localStrings[original] ?? original;
}
// Updates the local object from the storage to enable synchronous access
export async function preloadSyncLocalStrings(): Promise<void> {
if (isDevelopmentVersion() || isEnterprise()) {
return;
}
localStrings = await localStringsHotfix.get() ?? {};
}
export const localStringsHotfix = new CachedFunction('strings-hotfixes', {
async updater(): Promise<Record<string, string>> {
const json = await fetchHotfix('strings.json');
return json ? JSON.parse(json) : {};
},
maxAge: {hours: 6},
staleWhileRevalidate: {days: 30},
});
|