hexo-theme-icarus/includes/tasks/check_config.js

261 lines
9.1 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const logger = require('hexo-log')();
const Schema = require('js-yaml/lib/js-yaml/schema');
const Type = require('js-yaml/lib/js-yaml/type');
const rootSpec = require('../specs/_config.yml');
const { projectName, is } = require('../specs/common');
const { type, required, condition, defaultValue, description } = require('../specs/common').descriptor;
const UNDEFINED = Symbol('undefined');
const CONFIG_PATH = path.join(__dirname, '../..', '_config.yml');
const YAML_SCHEMA = new Schema({
include: [
require('js-yaml/lib/js-yaml/schema/default_full')
],
implicit: [
new Type('tag:yaml.org,2002:null', {
kind: 'scalar',
resolve(data) {
if (data === null) {
return true;
}
const max = data.length;
return (max === 1 && data === '~') ||
(max === 4 && (data === 'null' || data === 'Null' || data === 'NULL'));
},
construct: () => null,
predicate: object => object === null,
represent: {
empty: function () { return ''; }
},
defaultStyle: 'empty'
})
]
});
function toPlainObject(object, depth) {
if (object === null || (!is.object(object) && !is.array(object))) {
return object;
}
if (depth <= 0) {
return is.array(object) ? '[Array]' : '[Object]';
}
if (is.array(object)) {
const result = [];
for (let child of object) {
result.push(toPlainObject(child, depth - 1));
}
return result;
}
const result = {};
for (let key in object) {
result[key] = toPlainObject(object[key], depth - 1);
}
for (let key of Object.getOwnPropertySymbols(object)) {
result[key.toString()] = toPlainObject(object[key], depth - 1);
}
return result;
}
function isValidSpec(spec) {
if (!spec.hasOwnProperty(type)) {
return false;
}
return true;
}
function checkPrecondition(spec, parameter) {
if (!spec.hasOwnProperty(condition)) {
return true;
}
try {
if (spec[condition](parameter) === true) {
return true;
}
} catch (e) { }
return false;
}
function createDefaultConfig(spec, parentConfig = null) {
if (!isValidSpec(spec)) {
return UNDEFINED;
}
if (!checkPrecondition(spec, parentConfig)) {
return UNDEFINED;
}
if (spec.hasOwnProperty(defaultValue)) {
return spec[defaultValue];
}
const types = is.array(spec[type]) ? spec[type] : [spec[type]];
if (types.includes('object')) {
let defaults = UNDEFINED;
for (let key in spec) {
if (typeof (key) === 'symbol' || key === '*') {
continue;
}
const value = createDefaultConfig(spec[key], defaults);
if (value !== UNDEFINED) {
if (defaults === UNDEFINED) {
defaults = {};
}
if (spec[key].hasOwnProperty(description)) {
defaults['#' + key] = spec[key][description];
}
defaults[key] = value;
}
}
return defaults;
} else if (types.includes('array') && spec.hasOwnProperty('*')) {
return [createDefaultConfig(spec['*'], {})];
}
return UNDEFINED;
}
function dumpConfig(config, path) {
const configYaml = yaml.safeDump(config, {
indent: 4,
lineWidth: 1024,
schema: YAML_SCHEMA
}).replace(/^(\s*)\'#.*?\':\s*\'*(.*?)\'*$/mg, '$1# $2');
fs.writeFileSync(path, configYaml);
}
function validateConfigVersion(config, spec) {
function getMajorVersion(version) {
try {
return parseInt(version.split('.')[0]);
} catch (e) {
logger.error(`Configuration version number ${version} is malformed.`);
}
return null;
}
if (!config.hasOwnProperty('version')) {
logger.error('Failed to get the version number of the configuration file.');
logger.warn('You are probably using a previous version of confiugration.');
logger.warn(`Please be noted that it may not work in the newer versions of ${projectName}.`);
return false;
}
const specMajorVersion = getMajorVersion(spec.version[defaultValue]);
const configMajorVersion = getMajorVersion(config.version);
if (configMajorVersion === null || specMajorVersion === null) {
return false;
}
if (configMajorVersion < specMajorVersion) {
logger.warn('You are using a previous version of confiugration.');
logger.warn(`Please be noted that it may not work in the newer versions of ${projectName}.`);
return false;
}
if (configMajorVersion > specMajorVersion) {
logger.warn('You are probably using a more recent version of confiugration.');
logger.warn(`Please be noted that it may not work in the previous versions of ${projectName}.`);
return false;
}
return true;
}
function validateConfigType(config, specTypes) {
specTypes = is.array(specTypes) ? specTypes : [specTypes];
for (let specType of specTypes) {
if (is[specType](config)) {
return specType;
}
}
logger.error(`Config ${toPlainObject(config, 2)} do not match types ${specTypes}`);
return null;
}
const INVALID_SPEC = Symbol();
const MISSING_REQUIRED = Symbol();
const INVALID_TYPE = Symbol();
function validateConfigAndWarn(config, spec, parentConfig, configPath = []) {
const result = validateConfig(config, spec, parentConfig, configPath);
if (result !== true) {
const pathString = configPath.join('.');
const specTypes = is.array(spec[type]) ? spec[type] : [spec[type]];
switch(result) {
case INVALID_SPEC:
logger.error(`Invalid specification! The specification '${pathString}' does not have a [type] field:`);
logger.error('The specification of this configuration is:');
logger.error(JSON.stringify(toPlainObject(spec, 2), null, 4));
break;
case MISSING_REQUIRED:
logger.error(`Configuration '${pathString}' in required by the specification but is missing from the configuration!`);
logger.error('The specification of this configuration is:');
logger.error(JSON.stringify(toPlainObject(spec, 2), null, 4));
break;
case INVALID_TYPE:
logger.error(`Type mismatch! Configuration '${pathString}' is not the '${specTypes.join(' or ')}' type.`);
logger.error('The configuration value is:');
logger.error(JSON.stringify(toPlainObject(config, 2), null, 4));
logger.error('The specification of this configuration is:');
logger.error(JSON.stringify(toPlainObject(spec, 2), null, 4));
break;
}
}
return result;
}
function validateConfig(config, spec, parentConfig = null, configPath = []) {
if (!isValidSpec(spec)) {
return INVALID_SPEC;
}
if (!checkPrecondition(spec, parentConfig)) {
return true;
}
if (typeof(config) === 'undefined' || config === null) {
if (spec[required] === true) {
return MISSING_REQUIRED;
}
return true;
}
const configType = validateConfigType(config, spec[type]);
if (configType === null) {
return INVALID_TYPE;
}
if (configType === 'array' && spec.hasOwnProperty('*')) {
for (let i = 0; i < config.length; i++) {
if (!validateConfigAndWarn(config[i], spec['*'], config, configPath.concat(`[${i}]`))) {
return false;
}
}
} else if (configType === 'object') {
for (let key in spec) {
if (key === '*') {
for (let configKey in config) {
if (!validateConfigAndWarn(config[configKey], spec['*'], config, configPath.concat(configKey))) {
return false;
}
}
} else {
if (!validateConfigAndWarn(config[key], spec[key], config, configPath.concat(key))) {
return false;
}
}
}
}
return true;
}
logger.info('Checking if the configuration file exists...');
if (!fs.existsSync(CONFIG_PATH)) {
const relativePath = path.relative(process.cwd(), CONFIG_PATH);
logger.warn(`${relativePath} is not found. We are creating one for you...`);
dumpConfig(createDefaultConfig(rootSpec), CONFIG_PATH);
logger.info(`${relativePath} is created. Please restart Hexo to apply changes.`);
process.exit(0);
}
logger.info('Validating the configuration file...');
const config = yaml.safeLoad(fs.readFileSync(CONFIG_PATH));
if (!validateConfigVersion(config, rootSpec)) {
logger.info(`To let ${projectName} create a fresh configuration file for you, please delete or rename the following file:`);
logger.info(CONFIG_PATH);
} else {
validateConfigAndWarn(config, rootSpec);
}