refactor(app/web): new features (#1124) [skip ci]
This commit is contained in:
@@ -24,7 +24,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
|
||||
//Initialization
|
||||
const pending = []
|
||||
const {queries} = conf
|
||||
const extras = {css: (conf.settings.extras?.css ?? conf.settings.extras?.default) ? q["extras.css"] ?? "" : "", js: (conf.settings.extras?.js ?? conf.settings.extras?.default) ? q["extras.js"] ?? "" : ""}
|
||||
const extras = {css: imports.metadata.plugins.core.extras("extras_css", {...conf.settings, error:false}) ? q["extras.css"] ?? "" : "", js: imports.metadata.plugins.core.extras("extras_js", {...conf.settings, error:false}) ? q["extras.js"] ?? "" : ""}
|
||||
const data = {q, animated: true, large: false, base: {}, config: {}, errors: [], plugins: {}, computed: {}, extras, postscripts: []}
|
||||
const imports = {
|
||||
plugins: Plugins,
|
||||
@@ -184,7 +184,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
|
||||
if ((conf.settings?.optimize === true) || (conf.settings?.optimize?.includes?.("svg")))
|
||||
rendered = await imports.svg.optimize.svg(rendered, q, experimental)
|
||||
//Verify svg
|
||||
if (verify) {
|
||||
if ((verify)&&(imports.metadata.plugins.core.extras("verify", {...conf.settings, error:false}))) {
|
||||
console.debug(`metrics/compute/${login} > verify SVG`)
|
||||
let libxmljs = null
|
||||
try {
|
||||
@@ -281,9 +281,9 @@ metrics.insights.output = async function({login, imports, conf}, {graphql, rest,
|
||||
console.debug(`metrics/compute/${login} > insights > generating data`)
|
||||
const result = await metrics.insights({login}, {graphql, rest, conf}, {Plugins, Templates})
|
||||
const json = JSON.stringify(result)
|
||||
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
|
||||
await page.goto(`${server}/insights/${login}?embed=1&localstorage=1`)
|
||||
await page.evaluate(async json => localStorage.setItem("local.metrics", json), json) //eslint-disable-line no-undef
|
||||
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
|
||||
await page.goto(`${server}/insights/${login}?embed=1&localstorage=1`)
|
||||
await page.waitForSelector(".container .user", {timeout: 10 * 60 * 1000})
|
||||
|
||||
//Rendering
|
||||
@@ -297,7 +297,7 @@ metrics.insights.output = async function({login, imports, conf}, {graphql, rest,
|
||||
</head>
|
||||
<body>
|
||||
${await page.evaluate(() => document.querySelector("main").outerHTML)}
|
||||
${(await Promise.all([".css/style.vars.css", ".css/style.css", "about/.statics/style.css"].map(path => utils.axios.get(`${server}/${path}`)))).map(({data: style}) => `<style>${style}</style>`).join("\n")}
|
||||
${(await Promise.all([".css/style.vars.css", ".css/style.css", "insights/.statics/style.css"].map(path => utils.axios.get(`${server}/${path}`)))).map(({data: style}) => `<style>${style}</style>`).join("\n")}
|
||||
</body>
|
||||
</html>`
|
||||
await browser.close()
|
||||
|
||||
@@ -111,7 +111,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
|
||||
if (account !== "bypass") {
|
||||
const context = q.repo ? "repository" : account
|
||||
if (!meta.supports?.includes(context))
|
||||
throw {error: {message: `Not supported for: ${context}`, instance: new Error()}}
|
||||
throw {error: {message: `Unsupported context ${context}`, instance: new Error()}}
|
||||
}
|
||||
//Special values replacer
|
||||
const replacer = value => {
|
||||
@@ -214,33 +214,59 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
|
||||
|
||||
//Extra features parser
|
||||
{
|
||||
meta.extras = function(input, {extras = {}}) {
|
||||
//Required permissions
|
||||
const required = inputs[metadata.to.yaml(input, {name})]?.extras ?? null
|
||||
if (!required)
|
||||
meta.extras = function(input, {extras = {}, error = true}) {
|
||||
const key = metadata.to.yaml(input, {name})
|
||||
try {
|
||||
//Required permissions
|
||||
const required = inputs[key]?.extras ?? null
|
||||
if (!required)
|
||||
return true
|
||||
console.debug(`metrics/extras > ${name} > ${key} > require [${required}]`)
|
||||
|
||||
//Legacy handling
|
||||
const enabled = extras?.features ?? extras?.default ?? false
|
||||
if (typeof enabled === "boolean") {
|
||||
console.debug(`metrics/extras > ${name} > ${key} > extras features is set to ${enabled}`)
|
||||
if (!enabled)
|
||||
throw new Error()
|
||||
return enabled
|
||||
}
|
||||
if (!Array.isArray(required)) {
|
||||
console.debug(`metrics/extras > ${name} > ${key} > extras is not a permission array, skipping`)
|
||||
return false
|
||||
}
|
||||
|
||||
//Legacy options handling
|
||||
if (!Array.isArray(extras.features))
|
||||
throw new Error(`metrics/extras > ${name} > ${key} > extras.features is not an array`)
|
||||
if (extras.css) {
|
||||
console.warn(`metrics/extras > ${name} > ${key} > extras.css is deprecated, use extras.features with "metrics.run.puppeteer.user.css" instead`)
|
||||
extras.features.push("metrics.run.puppeteer.user.css")
|
||||
}
|
||||
if (extras.js) {
|
||||
console.warn(`metrics/extras > ${name} > ${key} > extras.js is deprecated, use extras.features with "metrics.run.puppeteer.user.js" instead`)
|
||||
extras.features.push("metrics.run.puppeteer.user.js")
|
||||
}
|
||||
if (extras.presets) {
|
||||
console.warn(`metrics/extras > ${name} > ${key} > extras.presets is deprecated, use extras.features with "metrics.setup.community.presets" instead`)
|
||||
extras.features.push("metrics.setup.community.presets")
|
||||
}
|
||||
|
||||
//Check permissions
|
||||
const missing = required.filter(permission => !extras.features.includes(permission))
|
||||
if (missing.length > 0) {
|
||||
console.debug(`metrics/extras > ${name} > ${key} > missing permissions [${missing}]`)
|
||||
throw new Error()
|
||||
}
|
||||
return true
|
||||
console.debug(`metrics/extras > ${name} > ${input} > require [${required}]`)
|
||||
|
||||
//Legacy handling
|
||||
const enabled = extras?.features ?? extras?.default ?? false
|
||||
if (typeof enabled === "boolean") {
|
||||
console.debug(`metrics/extras > ${name} > ${input} > extras features is set to ${enabled}`)
|
||||
return enabled
|
||||
}
|
||||
if (!Array.isArray(required)) {
|
||||
console.debug(`metrics/extras > ${name} > ${input} > extras is not a permission array, skipping`)
|
||||
return false
|
||||
catch {
|
||||
if (!error) {
|
||||
console.debug(`metrics/extras > ${name} > ${key} > skipping (no error mode)`)
|
||||
return false
|
||||
}
|
||||
throw Object.assign(new Error(`Unsupported option "${key}"`), {extras:true})
|
||||
}
|
||||
|
||||
//Check permissions
|
||||
if (!Array.isArray(extras.features))
|
||||
throw new Error(`metrics/extras > ${name} > ${input} > extras.features is not an array`)
|
||||
const missing = required.filter(permission => !extras.features.includes(permission))
|
||||
if (missing.length > 0) {
|
||||
console.debug(`metrics/extras > ${name} > ${input} > missing permissions [${missing}], skipping`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +602,9 @@ metadata.to = {
|
||||
return name ? key.replace(new RegExp(`^(${name}.)`, "g"), "") : key
|
||||
},
|
||||
yaml(key, {name = ""} = {}) {
|
||||
const parts = [key.replaceAll(".", "_")]
|
||||
const parts = []
|
||||
if (key !== "enabled")
|
||||
parts.unshift(key.replaceAll(".", "_"))
|
||||
if (name)
|
||||
parts.unshift((name === "base") ? name : `plugin_${name}`)
|
||||
return parts.join("_")
|
||||
|
||||
@@ -78,63 +78,68 @@ export default async function({log = true, sandbox = false, community = {}} = {}
|
||||
logger("metrics/setup > load package.json > success")
|
||||
|
||||
//Load community templates
|
||||
if ((typeof conf.settings.community.templates === "string") && (conf.settings.community.templates.length)) {
|
||||
logger("metrics/setup > parsing community templates list")
|
||||
conf.settings.community.templates = [...new Set([...decodeURIComponent(conf.settings.community.templates).split(",").map(v => v.trim().toLocaleLowerCase()).filter(v => v)])]
|
||||
}
|
||||
if ((Array.isArray(conf.settings.community.templates)) && (conf.settings.community.templates.length)) {
|
||||
//Clean remote repository
|
||||
logger(`metrics/setup > ${conf.settings.community.templates.length} community templates to install`)
|
||||
await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
|
||||
//Download community templates
|
||||
for (const template of conf.settings.community.templates) {
|
||||
try {
|
||||
//Parse community template
|
||||
logger(`metrics/setup > load community template ${template}`)
|
||||
const {repo, branch, name, trust = false} = template.match(/^(?<repo>[\s\S]+?)@(?<branch>[\s\S]+?):(?<name>[\s\S]+?)(?<trust>[+]trust)?$/)?.groups ?? null
|
||||
const command = `git clone --single-branch --branch ${branch} https://github.com/${repo}.git ${path.join(__templates, ".community")}`
|
||||
logger(`metrics/setup > run ${command}`)
|
||||
//Clone remote repository
|
||||
processes.execSync(command, {stdio: "ignore"})
|
||||
//Extract template
|
||||
logger(`metrics/setup > extract ${name} from ${repo}@${branch}`)
|
||||
await fs.promises.rm(path.join(__templates, `@${name}`), {recursive: true, force: true})
|
||||
await fs.promises.rename(path.join(__templates, ".community/source/templates", name), path.join(__templates, `@${name}`))
|
||||
//JavaScript file
|
||||
if (trust)
|
||||
logger(`metrics/setup > keeping @${name}/template.mjs (unsafe mode is enabled)`)
|
||||
else if (fs.existsSync(path.join(__templates, `@${name}`, "template.mjs"))) {
|
||||
logger(`metrics/setup > removing @${name}/template.mjs`)
|
||||
await fs.promises.unlink(path.join(__templates, `@${name}`, "template.mjs"))
|
||||
const inherit = yaml.load(`${fs.promises.readFile(path.join(__templates, `@${name}`, "metadata.yml"))}`).extends ?? null
|
||||
if (inherit) {
|
||||
logger(`metrics/setup > @${name} extends from ${inherit}`)
|
||||
if (fs.existsSync(path.join(__templates, inherit, "template.mjs"))) {
|
||||
logger(`metrics/setup > @${name} extended from ${inherit}`)
|
||||
await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs"))
|
||||
}
|
||||
else {
|
||||
logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`)
|
||||
if ((conf.settings.extras?.features?.includes("metrics.setup.community.templates"))||(conf.settings.extras?.features === true)||(conf.settings.extras?.default)) {
|
||||
if ((typeof conf.settings.community.templates === "string") && (conf.settings.community.templates.length)) {
|
||||
logger("metrics/setup > parsing community templates list")
|
||||
conf.settings.community.templates = [...new Set([...decodeURIComponent(conf.settings.community.templates).split(",").map(v => v.trim().toLocaleLowerCase()).filter(v => v)])]
|
||||
}
|
||||
if ((Array.isArray(conf.settings.community.templates)) && (conf.settings.community.templates.length)) {
|
||||
//Clean remote repository
|
||||
logger(`metrics/setup > ${conf.settings.community.templates.length} community templates to install`)
|
||||
await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
|
||||
//Download community templates
|
||||
for (const template of conf.settings.community.templates) {
|
||||
try {
|
||||
//Parse community template
|
||||
logger(`metrics/setup > load community template ${template}`)
|
||||
const {repo, branch, name, trust = false} = template.match(/^(?<repo>[\s\S]+?)@(?<branch>[\s\S]+?):(?<name>[\s\S]+?)(?<trust>[+]trust)?$/)?.groups ?? null
|
||||
const command = `git clone --single-branch --branch ${branch} https://github.com/${repo}.git ${path.join(__templates, ".community")}`
|
||||
logger(`metrics/setup > run ${command}`)
|
||||
//Clone remote repository
|
||||
processes.execSync(command, {stdio: "ignore"})
|
||||
//Extract template
|
||||
logger(`metrics/setup > extract ${name} from ${repo}@${branch}`)
|
||||
await fs.promises.rm(path.join(__templates, `@${name}`), {recursive: true, force: true})
|
||||
await fs.promises.rename(path.join(__templates, ".community/source/templates", name), path.join(__templates, `@${name}`))
|
||||
//JavaScript file
|
||||
if (trust)
|
||||
logger(`metrics/setup > keeping @${name}/template.mjs (unsafe mode is enabled)`)
|
||||
else if (fs.existsSync(path.join(__templates, `@${name}`, "template.mjs"))) {
|
||||
logger(`metrics/setup > removing @${name}/template.mjs`)
|
||||
await fs.promises.unlink(path.join(__templates, `@${name}`, "template.mjs"))
|
||||
const inherit = yaml.load(`${fs.promises.readFile(path.join(__templates, `@${name}`, "metadata.yml"))}`).extends ?? null
|
||||
if (inherit) {
|
||||
logger(`metrics/setup > @${name} extends from ${inherit}`)
|
||||
if (fs.existsSync(path.join(__templates, inherit, "template.mjs"))) {
|
||||
logger(`metrics/setup > @${name} extended from ${inherit}`)
|
||||
await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs"))
|
||||
}
|
||||
else {
|
||||
logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger(`metrics/setup > @${name}/template.mjs does not exist`)
|
||||
}
|
||||
else {
|
||||
logger(`metrics/setup > @${name}/template.mjs does not exist`)
|
||||
}
|
||||
|
||||
//Clean remote repository
|
||||
logger(`metrics/setup > clean ${repo}@${branch}`)
|
||||
await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
|
||||
logger(`metrics/setup > loaded community template ${name}`)
|
||||
}
|
||||
catch (error) {
|
||||
logger(`metrics/setup > failed to load community template ${template}`)
|
||||
logger(error)
|
||||
//Clean remote repository
|
||||
logger(`metrics/setup > clean ${repo}@${branch}`)
|
||||
await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
|
||||
logger(`metrics/setup > loaded community template ${name}`)
|
||||
}
|
||||
catch (error) {
|
||||
logger(`metrics/setup > failed to load community template ${template}`)
|
||||
logger(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger("metrics/setup > no community templates to install")
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger("metrics/setup > no community templates to install")
|
||||
logger("metrics/setup > community templates are disabled")
|
||||
}
|
||||
|
||||
//Load templates
|
||||
@@ -188,6 +193,18 @@ export default async function({log = true, sandbox = false, community = {}} = {}
|
||||
//Load metadata
|
||||
conf.metadata = await metadata({log})
|
||||
|
||||
//Modes
|
||||
if ((!conf.settings.modes)||(!conf.settings.modes.length))
|
||||
conf.settings.modes = ["embed", "insights"]
|
||||
logger(`metrics/setup > setup > enabled modes ${JSON.stringify(conf.settings.modes)}`)
|
||||
|
||||
//Allowed outputs formats
|
||||
if ((!conf.settings.outputs)||(!conf.settings.outputs.length))
|
||||
conf.settings.outputs = metadata.inputs.config_output.values
|
||||
else
|
||||
conf.settings.outputs = conf.settings.outputs.filter(format => metadata.inputs.config_output.values.includes(format))
|
||||
logger(`metrics/setup > setup > allowed outputs ${JSON.stringify(conf.settings.outputs)}`)
|
||||
|
||||
//Store authenticated user
|
||||
if (conf.settings.token) {
|
||||
try {
|
||||
|
||||
@@ -125,6 +125,49 @@ export function formatters({timeZone} = {}) {
|
||||
return license.nickname ?? license.spdxId ?? license.name
|
||||
}
|
||||
|
||||
/**Error formatter */
|
||||
format.error = function(error, {descriptions = {}, ...attributes} = {}) {
|
||||
try {
|
||||
//Extras features error
|
||||
if (error.extras)
|
||||
throw {error: {message: error.message, instance: error}}
|
||||
//Already formatted error
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
//Custom description
|
||||
let message = "Unexpected error"
|
||||
if (descriptions.custom) {
|
||||
const description = descriptions.custom(error)
|
||||
if (description)
|
||||
message += ` (${description})`
|
||||
}
|
||||
//Axios error
|
||||
if (error.isAxiosError) {
|
||||
//Error code
|
||||
const status = error.response?.status
|
||||
message = `API error: ${status}`
|
||||
|
||||
//Error description (optional)
|
||||
if ((descriptions)&&(descriptions[status]))
|
||||
message += ` (${descriptions[status]})`
|
||||
else {
|
||||
const description = error.response?.data?.errors?.[0]?.message ?? error.response.data?.error_description ?? error.response?.data?.message ?? null
|
||||
if (description)
|
||||
message += ` (${description})`
|
||||
}
|
||||
|
||||
//Error data
|
||||
console.debug(error.response.data)
|
||||
error = error.response?.data ?? null
|
||||
throw {error: {message, instance: error}}
|
||||
}
|
||||
throw {error: {message, instance: error}}
|
||||
}
|
||||
catch (error) {
|
||||
return Object.assign(error, attributes)
|
||||
}
|
||||
}
|
||||
|
||||
return {format}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user