#407 - support for external config files

This commit is contained in:
Elio Struyf
2022-12-23 10:26:20 +01:00
parent 86a8ef68b6
commit cfa805add9
5 changed files with 157 additions and 82 deletions
+11
View File
@@ -13,11 +13,22 @@ export class Cache {
);
}
public static async get<T>(key: string, type: "workspace" | "global"): Promise<T | undefined> {
const ext = Extension.getInstance();
const cache = await ext.getState<T>(key, type);
return cache || undefined;
}
public static async set(key: string, data: any, type: "workspace" | "global") {
await Extension.getInstance().setState(key, data, "workspace");
}
private static async clear() {
const ext = Extension.getInstance();
await ext.setState(ExtensionState.Dashboard.Pages.Cache, undefined, "workspace");
await ext.setState(ExtensionState.Dashboard.Pages.Index, undefined, "workspace");
await ext.setState(ExtensionState.Settings.Extends, undefined, "workspace");
Notifications.info("Cache cleared");
}
+4
View File
@@ -19,6 +19,10 @@ export const ExtensionState = {
}
},
Settings: {
Extends: `frontMatter:Settings:Extends`,
},
Updates: {
v7_0_0: {
dateFields: `frontMatter:Updates:v7.0.0:dateFields`
+128 -82
View File
@@ -12,8 +12,8 @@ import { Extension } from './Extension';
import { debounceCallback } from './DebounceCallback';
import { Logger } from './Logger';
import * as jsoncParser from 'jsonc-parser';
import { existsAsync, readFileAsync, writeFileAsync } from '../utils';
import fetch from 'node-fetch';
import { existsAsync, fetchWithTimeout, readFileAsync, writeFileAsync } from '../utils';
import { Cache } from '../commands';
export class Settings {
public static globalFile = "frontmatter.json";
@@ -469,83 +469,8 @@ export class Settings {
const extendsConfig: string[] = Settings.globalConfig[extendsConfigName];
for (const externalConfig of extendsConfig) {
if (externalConfig.endsWith(`.json`)) {
let config: any = undefined;
if (externalConfig.startsWith('https://')) {
try {
const response = await fetch(externalConfig);
if (response.ok) {
config = await response.json();
}
} catch (e) {
Logger.error(`Error fetching external config "${externalConfig}".`);
}
} else {
const configPath = join(Folders.getWorkspaceFolder()?.fsPath || '', externalConfig);
if (await existsAsync(configPath)) {
const configTxt = await readFileAsync(configPath, 'utf8');
config = jsoncParser.parse(configTxt);
} else {
Logger.error(`External config "${externalConfig}" not found.`);
}
}
// Check if the config contains data and loop through it
if (config) {
// We need to loop through the config to make sure the objects and arrays are merged
for (const key in config) {
if (config.hasOwnProperty(key)) {
const value = config[key];
const settingName = key.replace(`${CONFIG_KEY}.`, '');
if (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean') {
if (typeof originalConfig[key] === 'undefined') {
Settings.globalConfig[key] = value;
}
}
// Objects and arrays to override
else if (settingName === SETTING_CONTENT_DRAFT_FIELD ||
settingName === SETTING_CONTENT_SUPPORTED_FILETYPES ||
settingName === SETTING_GLOBAL_NOTIFICATIONS ||
settingName === SETTING_GLOBAL_NOTIFICATIONS_DISABLED ||
settingName === SETTING_MEDIA_SUPPORTED_MIMETYPES ||
settingName === SETTING_COMMA_SEPARATED_FIELDS) {
if (typeof originalConfig[key] === 'undefined') {
Settings.globalConfig[key] = value;
}
}
else if (typeof value === 'object' && value !== null) {
// Check if array
if (Array.isArray(value)) {
if (settingName === SETTING_TAXONOMY_CATEGORIES ||
settingName === SETTING_TAXONOMY_TAGS ||
settingName === SETTING_REMOVE_QUOTES) {
// Merge the arrays
Settings.globalConfig[key] = [...(Settings.globalConfig[key] || []), ...(originalConfig[key] || []), ...value];
// Filter out the doubles
Settings.globalConfig[key] = Settings.globalConfig[key].filter((item: any, index: number) => {
return Settings.globalConfig[key].indexOf(item) === index;
}, Settings.globalConfig[key]);
} else {
for (const item of value) {
Settings.updateGlobalConfigSetting(settingName, item);
}
}
} else if (settingName === SETTING_CONTENT_SNIPPETS) {
for (const itemKey in value) {
const crntValue = Settings.globalConfig[key] || {};
if (!crntValue[itemKey]) {
Settings.globalConfig[key] = { ...crntValue, ...{ [itemKey]: value[itemKey] } };
}
}
}
}
}
}
}
const config = await Settings.getExternalConfig(externalConfig);
await Settings.extendConfig(config, originalConfig);
}
}
}
@@ -586,6 +511,72 @@ export class Settings {
}
}
/**
* Extend the config with external config data
* @param config
* @param originalConfig The original config data is used to make sure we don't override settings coming from the fontmatter.json file.
* @returns
*/
private static async extendConfig(config: any, originalConfig: any) {
if (!config) {
return;
}
// We need to loop through the config to make sure the objects and arrays are merged
for (const key in config) {
if (config.hasOwnProperty(key)) {
const value = config[key];
const settingName = key.replace(`${CONFIG_KEY}.`, '');
if (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean') {
if (typeof originalConfig[key] === 'undefined') {
Settings.globalConfig[key] = value;
}
}
// Objects and arrays to override
else if (settingName === SETTING_CONTENT_DRAFT_FIELD ||
settingName === SETTING_CONTENT_SUPPORTED_FILETYPES ||
settingName === SETTING_GLOBAL_NOTIFICATIONS ||
settingName === SETTING_GLOBAL_NOTIFICATIONS_DISABLED ||
settingName === SETTING_MEDIA_SUPPORTED_MIMETYPES ||
settingName === SETTING_COMMA_SEPARATED_FIELDS) {
if (typeof originalConfig[key] === 'undefined') {
Settings.globalConfig[key] = value;
}
}
else if (typeof value === 'object' && value !== null) {
// Check if array
if (Array.isArray(value)) {
if (settingName === SETTING_TAXONOMY_CATEGORIES ||
settingName === SETTING_TAXONOMY_TAGS ||
settingName === SETTING_REMOVE_QUOTES) {
// Merge the arrays
Settings.globalConfig[key] = [...(Settings.globalConfig[key] || []), ...(originalConfig[key] || []), ...value];
// Filter out the doubles
Settings.globalConfig[key] = Settings.globalConfig[key].filter((item: any, index: number) => {
return Settings.globalConfig[key].indexOf(item) === index;
}, Settings.globalConfig[key]);
} else {
for (const item of value) {
Settings.updateGlobalConfigSetting(settingName, item);
}
}
} else if (settingName === SETTING_CONTENT_SNIPPETS) {
for (const itemKey in value) {
const crntValue = Settings.globalConfig[key] || {};
if (!crntValue[itemKey]) {
Settings.globalConfig[key] = { ...crntValue, ...{ [itemKey]: value[itemKey] } };
}
}
}
}
}
}
}
/**
* Update the global config array/object settings
* @param relSettingName
@@ -596,7 +587,7 @@ export class Settings {
if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CUSTOM_SCRIPTS)) {
// const crntValue = Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] || [];
// Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] = [...crntValue, configJson];
Settings.updateGlobalConfigArraySetting(SETTING_CUSTOM_SCRIPTS, "id", configJson);
Settings.updateGlobalConfigArraySetting(SETTING_CUSTOM_SCRIPTS, "id", configJson, "script");
}
// Content types
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_CONTENT_TYPES)) {
@@ -663,10 +654,18 @@ export class Settings {
* @param fieldName
* @param configJson
*/
private static updateGlobalConfigArraySetting<T>(settingName: string, fieldName: string, configJson: any): void {
private static updateGlobalConfigArraySetting<T>(settingName: string, fieldName: string, configJson: any, fallbackFieldName?: string): void {
const crntValue: T[] = Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] || [];
const itemIdx = crntValue.findIndex((item: any) => item[fieldName] === configJson[fieldName]);
const itemIdx = crntValue.findIndex((item: any) => {
if (typeof item[fieldName] !== "undefined") {
return item[fieldName] === configJson[fieldName];
} else if (fallbackFieldName && typeof item[fallbackFieldName] !== "undefined") {
return item[fallbackFieldName] === configJson[fallbackFieldName];
} else {
return false;
}
});
if (itemIdx === -1) {
crntValue.push(configJson);
}
@@ -728,4 +727,51 @@ export class Settings {
l();
});
}
/**
* Retrieve the external configuration
* @param configPath
* @returns
*/
private static async getExternalConfig(configPath: string): Promise<any> {
let config: any = undefined;
if (configPath.startsWith('https://')) {
try {
let cachedResponse = await Cache.get<{[config: string]: { expires: number, data: any }}>(ExtensionState.Settings.Extends, "workspace");
if (cachedResponse && cachedResponse[configPath] && cachedResponse[configPath].expires > new Date().getTime()) {
config = cachedResponse[configPath].data;
} else {
const response = await fetchWithTimeout(configPath, { method: 'GET' });
if (response.ok) {
config = await response.json();
if (!cachedResponse) {
cachedResponse = {};
}
cachedResponse[configPath] = {
expires: (new Date(new Date().getTime() + (1000 * 60 * 10))).getTime(),
data: config
};
await Cache.set(ExtensionState.Settings.Extends, cachedResponse, "workspace");
}
}
} catch (e) {
Logger.error(`Error fetching external config "${configPath}".`);
}
} else {
const absConfigPath = join(Folders.getWorkspaceFolder()?.fsPath || '', configPath);
if (await existsAsync(absConfigPath)) {
const configTxt = await readFileAsync(absConfigPath, 'utf8');
config = jsoncParser.parse(configTxt);
} else {
Logger.error(`External config "${configPath}" not found.`);
}
}
return config;
}
}
+13
View File
@@ -0,0 +1,13 @@
import fetch from 'node-fetch';
export const fetchWithTimeout = async (url: string, options: any, timeout = 5000) => {
try {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, { ...options, signal: controller.signal });
clearTimeout(id);
return response;
} catch (error) {
throw new Error(`Request timed out: ${url}`);
}
}
+1
View File
@@ -1,5 +1,6 @@
export * from './copyFileAsync';
export * from './existsAsync';
export * from './fetchWithTimeout';
export * from './fieldWhenClause';
export * from './mkdirAsync';
export * from './readFileAsync';