@discoveryjs/cli
Version:
CLI tools to serve & build projects based on Discovery.js
539 lines (450 loc) • 15.6 kB
JavaScript
const fs = require('fs');
const path = require('path');
const cron = require('cron-validator');
const { parseDuration } = require('./parse-duration');
function getDefaultFavicon(workingDir) {
try {
return require.resolve('@discoveryjs/discovery/src/favicon.png', {
paths: [workingDir, __dirname]
});
} catch {
return '';
}
}
function getDefaultIcon(workingDir) {
try {
return require.resolve('@discoveryjs/discovery/src/logo.svg', {
paths: [workingDir, __dirname]
});
} catch {
return '';
}
}
function warnOnDeprecatedOption(options, optionName, replaceName) {
if (options && optionName in options) {
console.warn(`Option "${optionName}" is deprecated${replaceName ? `, use "${replaceName}" instead` : ''}`);
}
}
function stripKeys(obj, stripKeys) {
const result = {};
for (const key of Object.keys(obj)) {
if (!stripKeys.includes(key)) {
result[key] = obj[key];
}
}
return result;
}
function unique(arr) {
return [...new Set(arr)];
}
function resolveFilename(filepath, basedir) {
return require.resolve(path.resolve(basedir, filepath));
}
function resolveConfigFilename(filename) {
const cwd = process.env.PWD || process.cwd();
if (filename) {
filename = path.resolve(cwd, filename);
} else {
const autoFilenames = [
path.join(cwd, '.discoveryrc.js'),
path.join(cwd, '.discoveryrc.mjs'),
path.join(cwd, '.discoveryrc.cjs'),
path.join(cwd, '.discoveryrc.json'),
path.join(cwd, '.discoveryrc'),
path.join(cwd, 'package.json')
];
for (let candidate of autoFilenames) {
if (fs.existsSync(candidate)) {
filename = candidate;
break;
}
}
if (filename && path.basename(filename) === 'package.json') {
try {
if ('discovery' in require(filename) === false) {
filename = undefined;
}
} catch (e) {
filename = undefined;
}
}
}
return filename;
}
function normalizeViewConfig(viewConfig, basedir) {
const warnings = [];
let {
basedir: viewBasedir,
noscript,
libs,
inspector,
router,
serveOnlyAssets,
assets,
bundles
} = viewConfig || {};
// basedir
viewBasedir = path.resolve(basedir, viewBasedir || '');
// inspector (enabled by default)
inspector = inspector === undefined || Boolean(inspector);
// default router (enabled by default)
router = router === undefined || Boolean(router);
// noscript content
noscript = typeof noscript === 'string'
? path.resolve(basedir, noscript || '')
: typeof noscript === 'function'
? noscript
: null;
// assets
serveOnlyAssets = (Array.isArray(serveOnlyAssets) ? serveOnlyAssets : [])
.map(filename => resolveFilename(filename, viewBasedir));
assets = (Array.isArray(assets) ? assets : [])
.map(filename => resolveFilename(filename, viewBasedir));
// bundles
bundles = bundles
? Object.fromEntries(Object.entries(bundles).map(([relpath, filename]) => [
path.posix.resolve('/', relpath).slice(1),
resolveFilename(filename, viewBasedir)
]))
: null;
// validation
if (libs) {
warnings.push('modelConfig.view.libs is not supported anymore, use require() or ES6 import expressions instead');
}
return {
...stripKeys(viewConfig || {}, ['basedir']),
inspector,
router,
noscript,
serveOnlyAssets,
assets,
bundles,
warnings
};
}
function normalizeScriptConfig(scriptConfig, basedir) {
if (!scriptConfig) {
return null;
}
const warnings = [];
let {
basedir: viewBasedir,
modules
} = scriptConfig;
// basedir
viewBasedir = path.resolve(basedir, viewBasedir || '');
// modules
modules = (Array.isArray(modules) ? modules : [])
.map(filename => resolveFilename(filename, viewBasedir));
return {
...stripKeys(scriptConfig, ['basedir']),
modules,
warnings
};
}
function normalizeModelConfig(modelConfig, basedir) {
const warnings = [];
let {
basedir: modelBasedir,
routers,
extendRouter,
data,
encodings,
setup,
prepare,
icon,
favicon,
colorScheme,
view,
script,
cache,
cacheTtl = 0, // 0 – TTL check is disabled
cacheBgUpdate = false
} = modelConfig || {};
// basedir
modelBasedir = path.resolve(basedir, modelBasedir || '');
// routers
routers = Array.isArray(routers)
? routers.map(filename => resolveFilename(filename, modelBasedir))
: [];
// data
if (typeof data === 'string') {
data = resolveFilename(data, modelBasedir);
}
// encodings
encodings = typeof encodings === 'string'
? resolveFilename(encodings, modelBasedir)
: null;
// setup
setup = typeof setup === 'string'
? resolveFilename(setup, modelBasedir)
: null;
// prepare
prepare = typeof prepare === 'string'
? resolveFilename(prepare, modelBasedir)
: null;
// icons
icon = typeof icon === 'string'
? resolveFilename(icon, modelBasedir)
: null;
favicon = typeof favicon === 'string'
? resolveFilename(favicon, modelBasedir)
: null;
// view
view = normalizeViewConfig(view, modelBasedir);
// script
script = normalizeScriptConfig(script, modelBasedir);
// cache
cache = Boolean(cache);
// cacheTtl
if (typeof cacheTtl === 'string') {
const duration = parseDuration(cacheTtl);
if (duration !== null) {
cacheTtl = duration;
} else if (!cron.isValidCron(cacheTtl)) {
warnings.push(`Bad cron expression in modelConfig.cacheTtl: "${cacheTtl}"`);
cacheTtl = 0;
}
} else if (!isFinite(cacheTtl) || !Number.isInteger(cacheTtl) || cacheTtl < 0) {
warnings.push(`Bad duration value in modelConfig.cacheTtl: "${cacheTtl}"`);
cacheTtl = 0;
}
// cacheBgUpdate
if (typeof cacheBgUpdate !== 'boolean' && cacheBgUpdate !== 'only') {
warnings.push(`Bad value for modelConfig.cacheBgUpdate (should be boolean or "only"): ${cacheBgUpdate}`);
cacheBgUpdate = false;
}
// validation
if (extendRouter) {
warnings.push('modelConfig.extendRouter is not supported anymore, use modelConfig.routes instead');
}
if (cacheBgUpdate && !cacheTtl) {
warnings.push('modelConfig.cacheBgUpdate is enabled, but modelConfig.cacheTtl is not set (cacheBgUpdate setting is ignored)');
cacheBgUpdate = false;
}
// color scheme
warnOnDeprecatedOption(modelConfig, 'darkmode', 'colorScheme');
if (modelConfig && modelConfig.colorScheme === undefined && modelConfig.darkmode) {
colorScheme = modelConfig.darkmode;
}
return {
name: 'Untitled model',
version: null,
description: null,
cache: undefined,
...stripKeys(modelConfig || {}, ['slug', 'basedir', 'darkmode']),
data,
encodings,
setup,
prepare,
icon,
favicon,
colorScheme,
view,
routers,
script,
cache,
cacheTtl,
cacheBgUpdate,
warnings
};
}
function resolveModelConfig(value, basedir) {
if (typeof value === 'string') {
const filepath = resolveFilename(value, basedir);
return [require(filepath), path.dirname(filepath)];
}
return [value, basedir];
}
function getDownloadUrl(download) {
return download ? 'build.zip' : false;
}
function normalizeConfig(config, model, basedir) {
let result;
config = config || {};
// if no models treat it as single model configuration
if (!config.models) {
model = 'default';
result = {
name: 'Implicit config',
version: null,
description: null,
mode: 'single',
models: {
default: config
}
};
} else {
result = {
name: 'Discovery',
version: null,
description: null,
mode: model ? 'single' : 'multi',
...stripKeys(config, ['mode', 'darkmode'])
};
warnOnDeprecatedOption(config, 'darkmode', 'colorScheme');
if ('colorScheme' in config === false && 'darkmode' in config) {
result.colorScheme = config.darkmode;
}
}
const cwd = process.env.PWD || process.cwd();
const configBasedir = result.basedir ? path.resolve(basedir || cwd, result.basedir) : basedir || cwd;
const modelBaseConfig = normalizeModelConfig(result.modelBaseConfig || {}, configBasedir);
const favicon = result.favicon
? resolveFilename(result.favicon, configBasedir)
: null;
result.colorScheme = result.colorScheme !== undefined ? result.colorScheme : 'auto';
result.download = result.download !== undefined ? Boolean(result.download) : true;
result.upload = result.upload !== undefined ? result.upload : false;
result.embed = result.embed !== undefined ? Boolean(result.embed) : false;
result.encodings = result.encodings !== undefined ? resolveFilename(result.encodings, configBasedir) : false;
result.view = normalizeViewConfig(result.view, configBasedir);
result.favicon = favicon || getDefaultFavicon(cwd);
result.icon = result.icon
? resolveFilename(result.icon, configBasedir)
: favicon || getDefaultIcon(cwd);
if (result.extendRouter) {
console.error('config.extendRouter is not supported anymore, use config.routes instead');
}
result.routers = Array.isArray(result.routers)
? result.routers.map(filename => path.resolve(configBasedir, filename))
: [];
result.models = Object.keys(result.models).reduce((res, slug) => {
if (!model || model === slug) {
const [resolvedConfig, modelBasedir] = resolveModelConfig(result.models[slug], configBasedir);
const modelConfig = {
slug,
...normalizeModelConfig({
...stripKeys(modelBaseConfig, 'prepare'),
...resolvedConfig
}, modelBasedir)
};
const favicon = modelConfig.favicon;
if (modelBaseConfig.prepare) {
modelConfig.commonPrepare = modelBaseConfig.prepare;
}
if (modelBaseConfig.encodings) {
modelConfig.commonEncodings = modelBaseConfig.encodings;
}
if (!favicon) {
modelConfig.favicon = modelBaseConfig.favicon || result.favicon;
}
if (!modelConfig.icon) {
modelConfig.icon = favicon || modelBaseConfig.icon || modelBaseConfig.favicon || result.icon;
}
modelConfig.colorScheme = modelConfig.colorScheme !== undefined
? modelConfig.colorScheme
: result.colorScheme;
modelConfig.download = getDownloadUrl(
modelConfig.download !== undefined
? Boolean(modelConfig.download)
: result.download
);
modelConfig.upload = modelConfig.upload !== undefined
? modelConfig.upload
: result.upload;
modelConfig.embed = modelConfig.embed !== undefined
? Boolean(modelConfig.embed)
: result.embed;
modelConfig.routers = unique([
...modelBaseConfig.routers,
...modelConfig.routers
]);
modelConfig.view.serveOnlyAssets = unique([
...modelBaseConfig.view.serveOnlyAssets,
...modelConfig.view.serveOnlyAssets
]);
modelConfig.view.assets = unique([
...modelBaseConfig.view.assets,
...modelConfig.view.assets
]);
if (modelConfig.script) {
modelConfig.script.modules = unique([
...modelBaseConfig?.script?.modules || [],
...modelConfig.script.modules
]);
}
res.push(modelConfig);
}
return res;
}, []);
return result;
}
function readJsonFromFile(filename) {
return JSON.parse(fs.readFileSync(filename, 'utf8'));
}
async function loadConfig(filename, model) {
let configFilename = resolveConfigFilename(filename);
let config;
if (!configFilename) {
return normalizeConfig({}, model);
}
if (!fs.existsSync(configFilename)) {
throw new Error('Config file is not found: ' + filename);
}
switch (path.basename(configFilename)) {
case '.discoveryrc':
config = readJsonFromFile(configFilename);
break;
case 'package.json':
const packageJson = readJsonFromFile(configFilename);
config = packageJson.discovery;
if (typeof packageJson.discovery === 'string') {
configFilename = path.resolve(path.dirname(configFilename), packageJson.discovery);
config = require(configFilename);
} else {
config = packageJson.discovery;
}
config = {
name: packageJson.name,
...config
};
break;
default:
// .discoveryrc.js
// .discoveryrc.cjs
// .discoveryrc.mjs
// .discoveryrc.json
// or any other
if (path.extname(configFilename) === '.json') {
config = readJsonFromFile(configFilename);
} else {
// doesn't work for now since we need a hot relead of the config
// const exports = await import(configFilename);
// config = exports.default;
config = require(configFilename);
}
}
return normalizeConfig(config, model, path.dirname(configFilename));
}
async function loadConfigWithFallback({ configFile, model } = {}) {
const resolvedConfigFile = resolveConfigFilename(configFile);
const config = resolvedConfigFile
? await loadConfig(resolvedConfigFile, model)
: {
...normalizeConfig({
upload: true,
meta: {
description: [
'Running in `model free mode` since no config or model is set. However, you can load the JSON file, analyse it, and create your own report.',
'',
'See [documention](https://github.com/discoveryjs/discovery/blob/master/README.md) for details.'
]
}
}),
name: 'Discovery',
mode: 'modelfree'
};
return {
configFile: resolvedConfigFile,
config
};
}
module.exports = {
resolveConfigFilename,
normalizeViewConfig,
normalizeModelConfig,
normalizeConfig,
loadConfig,
loadConfigWithFallback
};