mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
7.2 KiB
225 lines
7.2 KiB
/**
|
|
* @license Angular v21.1.1
|
|
* (c) 2010-2026 Google LLC. https://angular.dev/
|
|
* License: MIT
|
|
*/
|
|
|
|
const PARSE_TO_PAIRS = /([0-9]+[^0-9]+)/g;
|
|
const PAIR_SPLIT = /^([0-9]+)([dhmsu]+)$/;
|
|
function parseDurationToMs(duration) {
|
|
const matches = [];
|
|
let array;
|
|
while ((array = PARSE_TO_PAIRS.exec(duration)) !== null) {
|
|
matches.push(array[0]);
|
|
}
|
|
return matches.map(match => {
|
|
const res = PAIR_SPLIT.exec(match);
|
|
if (res === null) {
|
|
throw new Error(`Not a valid duration: ${match}`);
|
|
}
|
|
let factor = 0;
|
|
switch (res[2]) {
|
|
case 'd':
|
|
factor = 86400000;
|
|
break;
|
|
case 'h':
|
|
factor = 3600000;
|
|
break;
|
|
case 'm':
|
|
factor = 60000;
|
|
break;
|
|
case 's':
|
|
factor = 1000;
|
|
break;
|
|
case 'u':
|
|
factor = 1;
|
|
break;
|
|
default:
|
|
throw new Error(`Not a valid duration unit: ${res[2]}`);
|
|
}
|
|
return parseInt(res[1]) * factor;
|
|
}).reduce((total, value) => total + value, 0);
|
|
}
|
|
|
|
const QUESTION_MARK = '[^/]';
|
|
const WILD_SINGLE = '[^/]*';
|
|
const WILD_OPEN = '(?:.+\\/)?';
|
|
const TO_ESCAPE_BASE = [{
|
|
replace: /\./g,
|
|
with: '\\.'
|
|
}, {
|
|
replace: /\+/g,
|
|
with: '\\+'
|
|
}, {
|
|
replace: /\*/g,
|
|
with: WILD_SINGLE
|
|
}];
|
|
const TO_ESCAPE_WILDCARD_QM = [...TO_ESCAPE_BASE, {
|
|
replace: /\?/g,
|
|
with: QUESTION_MARK
|
|
}];
|
|
const TO_ESCAPE_LITERAL_QM = [...TO_ESCAPE_BASE, {
|
|
replace: /\?/g,
|
|
with: '\\?'
|
|
}];
|
|
function globToRegex(glob, literalQuestionMark = false) {
|
|
const toEscape = literalQuestionMark ? TO_ESCAPE_LITERAL_QM : TO_ESCAPE_WILDCARD_QM;
|
|
const segments = glob.split('/').reverse();
|
|
let regex = '';
|
|
while (segments.length > 0) {
|
|
const segment = segments.pop();
|
|
if (segment === '**') {
|
|
if (segments.length > 0) {
|
|
regex += WILD_OPEN;
|
|
} else {
|
|
regex += '.*';
|
|
}
|
|
} else {
|
|
const processed = toEscape.reduce((segment, escape) => segment.replace(escape.replace, escape.with), segment);
|
|
regex += processed;
|
|
if (segments.length > 0) {
|
|
regex += '\\/';
|
|
}
|
|
}
|
|
}
|
|
return regex;
|
|
}
|
|
|
|
const DEFAULT_NAVIGATION_URLS = ['/**', '!/**/*.*', '!/**/*__*', '!/**/*__*/**'];
|
|
class Generator {
|
|
fs;
|
|
baseHref;
|
|
constructor(fs, baseHref) {
|
|
this.fs = fs;
|
|
this.baseHref = baseHref;
|
|
}
|
|
async process(config) {
|
|
const unorderedHashTable = {};
|
|
const assetGroups = await this.processAssetGroups(config, unorderedHashTable);
|
|
return {
|
|
configVersion: 1,
|
|
timestamp: Date.now(),
|
|
appData: config.appData,
|
|
index: joinUrls(this.baseHref, config.index),
|
|
assetGroups,
|
|
dataGroups: this.processDataGroups(config),
|
|
hashTable: withOrderedKeys(unorderedHashTable),
|
|
navigationUrls: processNavigationUrls(this.baseHref, config.navigationUrls),
|
|
navigationRequestStrategy: config.navigationRequestStrategy ?? 'performance',
|
|
applicationMaxAge: config.applicationMaxAge ? parseDurationToMs(config.applicationMaxAge) : undefined
|
|
};
|
|
}
|
|
async processAssetGroups(config, hashTable) {
|
|
const allFiles = await this.fs.list('/');
|
|
const seenMap = new Set();
|
|
const filesPerGroup = new Map();
|
|
for (const group of config.assetGroups || []) {
|
|
if (group.resources.versionedFiles) {
|
|
throw new Error(`Asset-group '${group.name}' in 'ngsw-config.json' uses the 'versionedFiles' option, ` + "which is no longer supported. Use 'files' instead.");
|
|
}
|
|
const fileMatcher = globListToMatcher(group.resources.files || []);
|
|
const matchedFiles = allFiles.filter(fileMatcher).filter(file => !seenMap.has(file)).sort();
|
|
matchedFiles.forEach(file => seenMap.add(file));
|
|
filesPerGroup.set(group, matchedFiles);
|
|
}
|
|
const allMatchedFiles = [].concat(...Array.from(filesPerGroup.values())).sort();
|
|
const allMatchedHashes = await processInBatches(allMatchedFiles, 500, file => this.fs.hash(file));
|
|
allMatchedFiles.forEach((file, idx) => {
|
|
hashTable[joinUrls(this.baseHref, file)] = allMatchedHashes[idx];
|
|
});
|
|
return Array.from(filesPerGroup.entries()).map(([group, matchedFiles]) => ({
|
|
name: group.name,
|
|
installMode: group.installMode || 'prefetch',
|
|
updateMode: group.updateMode || group.installMode || 'prefetch',
|
|
cacheQueryOptions: buildCacheQueryOptions(group.cacheQueryOptions),
|
|
urls: matchedFiles.map(url => joinUrls(this.baseHref, url)),
|
|
patterns: (group.resources.urls || []).map(url => urlToRegex(url, this.baseHref, true))
|
|
}));
|
|
}
|
|
processDataGroups(config) {
|
|
return (config.dataGroups || []).map(group => {
|
|
return {
|
|
name: group.name,
|
|
patterns: group.urls.map(url => urlToRegex(url, this.baseHref, true)),
|
|
strategy: group.cacheConfig.strategy || 'performance',
|
|
maxSize: group.cacheConfig.maxSize,
|
|
maxAge: parseDurationToMs(group.cacheConfig.maxAge),
|
|
timeoutMs: group.cacheConfig.timeout && parseDurationToMs(group.cacheConfig.timeout),
|
|
refreshAheadMs: group.cacheConfig.refreshAhead && parseDurationToMs(group.cacheConfig.refreshAhead),
|
|
cacheOpaqueResponses: group.cacheConfig.cacheOpaqueResponses,
|
|
cacheQueryOptions: buildCacheQueryOptions(group.cacheQueryOptions),
|
|
version: group.version !== undefined ? group.version : 1
|
|
};
|
|
});
|
|
}
|
|
}
|
|
function processNavigationUrls(baseHref, urls = DEFAULT_NAVIGATION_URLS) {
|
|
return urls.map(url => {
|
|
const positive = !url.startsWith('!');
|
|
url = positive ? url : url.slice(1);
|
|
return {
|
|
positive,
|
|
regex: `^${urlToRegex(url, baseHref)}$`
|
|
};
|
|
});
|
|
}
|
|
async function processInBatches(items, batchSize, processFn) {
|
|
const batches = [];
|
|
for (let i = 0; i < items.length; i += batchSize) {
|
|
batches.push(items.slice(i, i + batchSize));
|
|
}
|
|
return batches.reduce(async (prev, batch) => (await prev).concat(await Promise.all(batch.map(item => processFn(item)))), Promise.resolve([]));
|
|
}
|
|
function globListToMatcher(globs) {
|
|
const patterns = globs.map(pattern => {
|
|
if (pattern.startsWith('!')) {
|
|
return {
|
|
positive: false,
|
|
regex: new RegExp('^' + globToRegex(pattern.slice(1)) + '$')
|
|
};
|
|
} else {
|
|
return {
|
|
positive: true,
|
|
regex: new RegExp('^' + globToRegex(pattern) + '$')
|
|
};
|
|
}
|
|
});
|
|
return file => matches(file, patterns);
|
|
}
|
|
function matches(file, patterns) {
|
|
return patterns.reduce((isMatch, pattern) => {
|
|
if (pattern.positive) {
|
|
return isMatch || pattern.regex.test(file);
|
|
} else {
|
|
return isMatch && !pattern.regex.test(file);
|
|
}
|
|
}, false);
|
|
}
|
|
function urlToRegex(url, baseHref, literalQuestionMark) {
|
|
if (!url.startsWith('/') && url.indexOf('://') === -1) {
|
|
url = joinUrls(baseHref.replace(/^\.(?=\/)/, ''), url);
|
|
}
|
|
return globToRegex(url, literalQuestionMark);
|
|
}
|
|
function joinUrls(a, b) {
|
|
if (a.endsWith('/') && b.startsWith('/')) {
|
|
return a + b.slice(1);
|
|
} else if (!a.endsWith('/') && !b.startsWith('/')) {
|
|
return a + '/' + b;
|
|
}
|
|
return a + b;
|
|
}
|
|
function withOrderedKeys(unorderedObj) {
|
|
const orderedObj = {};
|
|
Object.keys(unorderedObj).sort().forEach(key => orderedObj[key] = unorderedObj[key]);
|
|
return orderedObj;
|
|
}
|
|
function buildCacheQueryOptions(inOptions) {
|
|
return {
|
|
ignoreVary: true,
|
|
...inOptions
|
|
};
|
|
}
|
|
|
|
export { Generator };
|
|
//# sourceMappingURL=config.mjs.map
|
|
|