chore: code formatting

This commit is contained in:
github-actions[bot]
2022-04-23 23:18:43 +00:00
parent 73cd43c18f
commit 4c98629bbc
130 changed files with 1839 additions and 1788 deletions

View File

@@ -2,8 +2,8 @@
import core from "@actions/core"
import github from "@actions/github"
import octokit from "@octokit/graphql"
import fs from "fs/promises"
import processes from "child_process"
import fs from "fs/promises"
import paths from "path"
import sgit from "simple-git"
import mocks from "../../../tests/mocks/index.mjs"
@@ -22,7 +22,8 @@ const debugged = []
const preset = {}
//Info logger
const info = (left, right, {token = false} = {}) => console.log(`${`${left}`.padEnd(63 + 9 * (/0m$/.test(left)))}${
const info = (left, right, {token = false} = {}) =>
console.log(`${`${left}`.padEnd(63 + 9 * (/0m$/.test(left)))}${
Array.isArray(right)
? right.join(", ") || "(none)"
: right === undefined
@@ -37,7 +38,7 @@ info.section = (left = "", right = " ") => info(`\x1b[36m${left}\x1b[0m`, right)
info.group = ({metadata, name, inputs}) => {
info.section(metadata.plugins[name]?.name?.match(/(?<section>[\w\s]+)/i)?.groups?.section?.trim(), " ")
for (const [input, value] of Object.entries(inputs))
info(metadata.plugins[name]?.inputs[input]?.description?.split("\n")[0] ?? metadata.plugins[name]?.inputs[input]?.description ?? input, `${input in preset ? "*" : ""}${value}`, {token:metadata.plugins[name]?.inputs[input]?.type === "token"})
info(metadata.plugins[name]?.inputs[input]?.description?.split("\n")[0] ?? metadata.plugins[name]?.inputs[input]?.description ?? input, `${input in preset ? "*" : ""}${value}`, {token: metadata.plugins[name]?.inputs[input]?.type === "token"})
}
info.break = () => console.log("─".repeat(88))
@@ -70,14 +71,12 @@ async function retry(func, {retries = 1, delay = 0} = {}) {
//Process exit
function quit(reason) {
const code = {success:0, skipped:0, failed:1}[reason] ?? 0
const code = {success: 0, skipped: 0, failed: 1}[reason] ?? 0
process.exit(code)
}
//=====================================================================================================
} //=====================================================================================================
//Runner
(async function() {
;(async function() {
try {
//Initialization
info.break()
@@ -96,9 +95,9 @@ function quit(reason) {
}
//Load configuration
const {conf, Plugins, Templates} = await setup({log:false, community:{templates:core.getInput("setup_community_templates")}})
const {conf, Plugins, Templates} = await setup({log: false, community: {templates: core.getInput("setup_community_templates")}})
const {metadata} = conf
conf.settings.extras = {default:true}
conf.settings.extras = {default: true}
info("Setup", "complete")
info("Version", conf.package.version)
@@ -111,42 +110,42 @@ function quit(reason) {
}
//Core inputs
Object.assign(preset, await presets(core.getInput("config_presets"), {log:false, core}))
Object.assign(preset, await presets(core.getInput("config_presets"), {log: false, core}))
const {
user:_user,
repo:_repo,
user: _user,
repo: _repo,
token,
template,
query,
"setup.community.templates":_templates,
filename:_filename,
"setup.community.templates": _templates,
filename: _filename,
optimize,
verify,
"markdown.cache":_markdown_cache,
"markdown.cache": _markdown_cache,
debug,
"debug.flags":dflags,
"debug.print":dprint,
"use.mocked.data":mocked,
"debug.flags": dflags,
"debug.print": dprint,
"use.mocked.data": mocked,
dryrun,
"plugins.errors.fatal":die,
"committer.token":_token,
"committer.branch":_branch,
"committer.message":_message,
"committer.gist":_gist,
"use.prebuilt.image":_image,
"plugins.errors.fatal": die,
"committer.token": _token,
"committer.branch": _branch,
"committer.message": _message,
"committer.gist": _gist,
"use.prebuilt.image": _image,
retries,
"retries.delay":retries_delay,
"retries.output.action":retries_output_action,
"retries.delay.output.action":retries_delay_output_action,
"output.action":_action,
"output.condition":_output_condition,
"retries.delay": retries_delay,
"retries.output.action": retries_output_action,
"retries.delay.output.action": retries_delay_output_action,
"output.action": _action,
"output.condition": _output_condition,
delay,
"notice.release":_notice_releases,
"notice.release": _notice_releases,
...config
} = metadata.plugins.core.inputs.action({core, preset})
const q = {...query, ...(_repo ? {repo:_repo} : null), template}
const q = {...query, ...(_repo ? {repo: _repo} : null), template}
const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(config["config.output"]) ? config["config.output"] : metadata.templates[template]?.formats?.[0] ?? null
const filename = _filename.replace(/[*]/g, {jpeg:"jpg", markdown:"md", "markdown-pdf":"pdf", insights:"html"}[_output] ?? _output ?? "*")
const filename = _filename.replace(/[*]/g, {jpeg: "jpg", markdown: "md", "markdown-pdf": "pdf", insights: "html"}[_output] ?? _output ?? "*")
//Docker image
if (_image)
@@ -162,7 +161,7 @@ function quit(reason) {
q["debug.flags"] = dflags.join(" ")
//Token for data gathering
info("GitHub token", token, {token:true})
info("GitHub token", token, {token: true})
//A GitHub token should start with "gh" along an additional letter for type
//See https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats
info("GitHub token format", /^gh[pousr]_/.test(token) ? "correct" : "(old or invalid)")
@@ -171,7 +170,7 @@ function quit(reason) {
conf.settings.token = token
const api = {}
const resources = {}
api.graphql = octokit.graphql.defaults({headers:{authorization:`token ${token}`}})
api.graphql = octokit.graphql.defaults({headers: {authorization: `token ${token}`}})
info("Github GraphQL API", "ok")
const octoraw = github.getOctokit(token)
api.rest = octoraw.rest
@@ -185,12 +184,12 @@ function quit(reason) {
//Test token validity and requests count
else if (!/^NOT_NEEDED$/.test(token)) {
//Check rate limit
const {data} = await api.rest.rateLimit.get().catch(() => ({data:{resources:{}}}))
const {data} = await api.rest.rateLimit.get().catch(() => ({data: {resources: {}}}))
Object.assign(resources, data.resources)
info("API requests (REST)", resources.core ? `${resources.core.remaining}/${resources.core.limit}` : "(unknown)")
info("API requests (GraphQL)", resources.graphql ? `${resources.graphql.remaining}/${resources.graphql.limit}` : "(unknown)")
info("API requests (search)", resources.search ? `${resources.search.remaining}/${resources.search.limit}` : "(unknown)")
if ((!resources.core.remaining)||(!resources.graphql.remaining)) {
if ((!resources.core.remaining) || (!resources.graphql.remaining)) {
console.warn("::warning::It seems you have reached your API requests limit. Please retry later.")
info.break()
console.log("Nothing can be done currently, thanks for using metrics!")
@@ -217,7 +216,7 @@ function quit(reason) {
//Check for new versions
if (_notice_releases) {
const {data:[{tag_name:tag}]} = await rest.repos.listReleases({owner:"lowlighter", repo:"metrics"})
const {data: [{tag_name: tag}]} = await rest.repos.listReleases({owner: "lowlighter", repo: "metrics"})
const current = Number(conf.package.version.match(/(\d+\.\d+)/)?.[1] ?? 0)
const latest = Number(tag.match(/(\d+\.\d+)/)?.[1] ?? 0)
if (latest > current)
@@ -254,7 +253,7 @@ function quit(reason) {
committer.merge = _action.match(/^pull-request-(?<method>merge|squash|rebase)$/)?.groups?.method ?? null
committer.branch = _branch || github.context.ref.replace(/^refs[/]heads[/]/, "")
committer.head = committer.pr ? `metrics-run-${github.context.runId}` : committer.branch
info("Committer token", committer.token, {token:true})
info("Committer token", committer.token, {token: true})
if (!committer.token)
throw new Error("You must provide a valid GitHub token to commit your metrics")
info("Committer branch", committer.branch)
@@ -273,25 +272,25 @@ function quit(reason) {
}
//Create head branch if needed
try {
await committer.rest.git.getRef({...github.context.repo, ref:`heads/${committer.head}`})
await committer.rest.git.getRef({...github.context.repo, ref: `heads/${committer.head}`})
info("Committer head branch status", "ok")
}
catch (error) {
console.debug(error)
if (/not found/i.test(`${error}`)) {
const {data:{object:{sha}}} = await committer.rest.git.getRef({...github.context.repo, ref:`heads/${committer.branch}`})
const {data: {object: {sha}}} = await committer.rest.git.getRef({...github.context.repo, ref: `heads/${committer.branch}`})
info("Committer branch current sha", sha)
await committer.rest.git.createRef({...github.context.repo, ref:`refs/heads/${committer.head}`, sha})
await committer.rest.git.createRef({...github.context.repo, ref: `refs/heads/${committer.head}`, sha})
info("Committer head branch status", "(created)")
}
else
else {
throw error
}
}
//Retrieve previous render SHA to be able to update file content through API
committer.sha = null
try {
const {repository:{object:{oid}}} = await graphql(
const {repository: {object: {oid}}} = await graphql(
`
query Sha {
repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
@@ -299,7 +298,7 @@ function quit(reason) {
}
}
`,
{headers:{authorization:`token ${committer.token}`}},
{headers: {authorization: `token ${committer.token}`}},
)
committer.sha = oid
}
@@ -308,8 +307,9 @@ function quit(reason) {
}
info("Previous render sha", committer.sha ?? "(none)")
}
else if (dryrun)
else if (dryrun) {
info("Dry-run", true)
}
//SVG file
conf.settings.optimize = optimize
@@ -326,7 +326,7 @@ function quit(reason) {
//Core config
info.break()
info.group({metadata, name:"core", inputs:config})
info.group({metadata, name: "core", inputs: config})
info("Plugin errors", die ? "(exit with error)" : "(displayed in generated image)")
const convert = _output || null
Object.assign(q, config)
@@ -337,7 +337,7 @@ function quit(reason) {
await new Promise(async (solve, reject) => {
let stdout = ""
setTimeout(() => reject("Timeout while waiting for Insights webserver"), 5 * 60 * 1000)
const web = await processes.spawn("node", ["/metrics/source/app/web/index.mjs"], {env:{...process.env}})
const web = await processes.spawn("node", ["/metrics/source/app/web/index.mjs"], {env: {...process.env}})
web.stdout.on("data", data => (console.debug(`web > ${data}`), stdout += data, /Server ready !/.test(stdout) ? solve() : null))
web.stderr.on("data", data => console.debug(`web > ${data}`))
})
@@ -351,9 +351,9 @@ function quit(reason) {
//Base content
info.break()
const {base:parts, repositories:_repositories, ...base} = metadata.plugins.base.inputs.action({core, preset})
const {base: parts, repositories: _repositories, ...base} = metadata.plugins.base.inputs.action({core, preset})
conf.settings.repositories = _repositories
info.group({metadata, name:"base", inputs:{repositories:conf.settings.repositories, ...base}})
info.group({metadata, name: "base", inputs: {repositories: conf.settings.repositories, ...base}})
info("Base sections", parts)
base.base = false
for (const part of conf.settings.plugins.base.parts)
@@ -364,7 +364,7 @@ function quit(reason) {
const plugins = {}
for (const name of Object.keys(Plugins).filter(key => !["base", "core"].includes(key))) {
//Parse inputs
const {[name]:enabled, ...inputs} = metadata.plugins[name].inputs.action({core, preset})
const {[name]: enabled, ...inputs} = metadata.plugins[name].inputs.action({core, preset})
plugins[name] = {enabled}
//Register user inputs
if (enabled) {
@@ -386,9 +386,9 @@ function quit(reason) {
info.break()
info.section("Rendering")
let rendered = await retry(async () => {
const {rendered} = await metrics({login:user, q}, {graphql, rest, plugins, conf, die, verify, convert}, {Plugins, Templates})
const {rendered} = await metrics({login: user, q}, {graphql, rest, plugins, conf, die, verify, convert}, {Plugins, Templates})
return rendered
}, {retries, delay:retries_delay})
}, {retries, delay: retries_delay})
if (!rendered)
throw new Error("Could not render metrics")
info("Status", "complete")
@@ -409,13 +409,13 @@ function quit(reason) {
let data = ""
await retry(async () => {
try {
data = `${Buffer.from((await committer.rest.repos.getContent({...github.context.repo, ref:`heads/${committer.head}`, path:filename})).data.content, "base64")}`
data = `${Buffer.from((await committer.rest.repos.getContent({...github.context.repo, ref: `heads/${committer.head}`, path: filename})).data.content, "base64")}`
}
catch (error) {
if (error.response.status !== 404)
throw error
}
}, {retries:retries_output_action, delay:retries_delay_output_action})
}, {retries: retries_output_action, delay: retries_delay_output_action})
const previous = await svg.hash(data)
info("Previous hash", previous)
const current = await svg.hash(rendered)
@@ -430,7 +430,7 @@ function quit(reason) {
if (dryrun)
info("Actions to perform", "(none)")
else {
await fs.mkdir(paths.dirname(paths.join("/renders", filename)), {recursive:true})
await fs.mkdir(paths.dirname(paths.join("/renders", filename)), {recursive: true})
await fs.writeFile(paths.join("/renders", filename), Buffer.from(typeof rendered === "object" ? JSON.stringify(rendered) : `${rendered}`))
info(`Save to /metrics_renders/${filename}`, "ok")
info("Output action", _action)
@@ -454,7 +454,7 @@ function quit(reason) {
console.debug(`Processing ${path}`)
let sha = null
try {
const {repository:{object:{oid}}} = await graphql(
const {repository: {object: {oid}}} = await graphql(
`
query Sha {
repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
@@ -462,7 +462,7 @@ function quit(reason) {
}
}
`,
{headers:{authorization:`token ${committer.token}`}},
{headers: {authorization: `token ${committer.token}`}},
)
sha = oid
}
@@ -474,14 +474,14 @@ function quit(reason) {
...github.context.repo,
path,
content,
message:`${committer.message} (cache)`,
message: `${committer.message} (cache)`,
...(sha ? {sha} : {}),
branch:committer.pr ? committer.head : committer.branch,
branch: committer.pr ? committer.head : committer.branch,
})
rendered = rendered.replace(match, `<img src="https://github.com/${github.context.repo.owner}/${github.context.repo.repo}/blob/${committer.branch}/${path}">`)
info(`Saving ${path}`, "ok")
}
}, {retries:retries_output_action, delay:retries_delay_output_action})
}, {retries: retries_output_action, delay: retries_delay_output_action})
}
}
@@ -499,10 +499,10 @@ function quit(reason) {
//Upload to gist (this is done as user since committer_token may not have gist rights)
if (committer.gist) {
await retry(async () => {
await rest.gists.update({gist_id:committer.gist, files:{[filename]:{content:rendered}}})
await rest.gists.update({gist_id: committer.gist, files: {[filename]: {content: rendered}}})
info(`Upload to gist ${committer.gist}`, "ok")
committer.commit = false
}, {retries:retries_output_action, delay:retries_delay_output_action})
}, {retries: retries_output_action, delay: retries_delay_output_action})
}
//Commit metrics
@@ -510,14 +510,14 @@ function quit(reason) {
await retry(async () => {
await committer.rest.repos.createOrUpdateFileContents({
...github.context.repo,
path:filename,
message:committer.message,
content:Buffer.from(typeof rendered === "object" ? JSON.stringify(rendered) : `${rendered}`).toString("base64"),
branch:committer.pr ? committer.head : committer.branch,
...(committer.sha ? {sha:committer.sha} : {}),
path: filename,
message: committer.message,
content: Buffer.from(typeof rendered === "object" ? JSON.stringify(rendered) : `${rendered}`).toString("base64"),
branch: committer.pr ? committer.head : committer.branch,
...(committer.sha ? {sha: committer.sha} : {}),
})
info(`Commit to branch ${committer.branch}`, "ok")
}, {retries:retries_output_action, delay:retries_delay_output_action})
}, {retries: retries_output_action, delay: retries_delay_output_action})
}
//Pull request
@@ -526,7 +526,7 @@ function quit(reason) {
let number = null
await retry(async () => {
try {
({data:{number}} = await committer.rest.pulls.create({...github.context.repo, head:committer.head, base:committer.branch, title:`Auto-generated metrics for run #${github.context.runId}`, body:" ", maintainer_can_modify:true}))
;({data: {number}} = await committer.rest.pulls.create({...github.context.repo, head: committer.head, base: committer.branch, title: `Auto-generated metrics for run #${github.context.runId}`, body: " ", maintainer_can_modify: true}))
info(`Pull request from ${committer.head} to ${committer.branch}`, "(created)")
}
catch (error) {
@@ -535,7 +535,7 @@ function quit(reason) {
if (/A pull request already exists/.test(error)) {
info(`Pull request from ${committer.head} to ${committer.branch}`, "(already existing)")
const q = `repo:${github.context.repo.owner}/${github.context.repo.repo}+type:pr+state:open+Auto-generated metrics for run #${github.context.runId}+in:title`
const prs = (await committer.rest.search.issuesAndPullRequests({q})).data.items.filter(({user:{login}}) => login === "github-actions[bot]")
const prs = (await committer.rest.search.issuesAndPullRequests({q})).data.items.filter(({user: {login}}) => login === "github-actions[bot]")
if (prs.length < 1)
throw new Error("0 matching prs. Cannot proceed.")
if (prs.length > 1)
@@ -548,12 +548,12 @@ function quit(reason) {
committer.merge = false
number = "(none)"
}
else
else {
throw error
}
}
info("Pull request number", number)
}, {retries:retries_output_action, delay:retries_delay_output_action})
}, {retries: retries_output_action, delay: retries_delay_output_action})
//Merge pull request
if (committer.merge) {
info("Merge method", committer.merge)
@@ -561,7 +561,7 @@ function quit(reason) {
do {
const success = await retry(async () => {
//Check pull request mergeability (https://octokit.github.io/rest.js/v18#pulls-get)
const {data:{mergeable, mergeable_state:state}} = await committer.rest.pulls.get({...github.context.repo, pull_number:number})
const {data: {mergeable, mergeable_state: state}} = await committer.rest.pulls.get({...github.context.repo, pull_number: number})
console.debug(`Pull request #${number} mergeable state is "${state}"`)
if (mergeable === null) {
await wait(15)
@@ -570,17 +570,17 @@ function quit(reason) {
if (!mergeable)
throw new Error(`Pull request #${number} is not mergeable (state is "${state}")`)
//Merge pull request
await committer.rest.pulls.merge({...github.context.repo, pull_number:number, merge_method:committer.merge})
await committer.rest.pulls.merge({...github.context.repo, pull_number: number, merge_method: committer.merge})
info(`Merge #${number} to ${committer.branch}`, "ok")
return true
}, {retries:retries_output_action, delay:retries_delay_output_action})
}, {retries: retries_output_action, delay: retries_delay_output_action})
if (!success)
continue
//Delete head branch
await retry(async () => {
try {
await wait(15)
await committer.rest.git.deleteRef({...github.context.repo, ref:`heads/${committer.head}`})
await committer.rest.git.deleteRef({...github.context.repo, ref: `heads/${committer.head}`})
}
catch (error) {
console.debug(error)
@@ -588,7 +588,7 @@ function quit(reason) {
throw error
}
info(`Branch ${committer.head}`, "(deleted)")
}, {retries:retries_output_action, delay:retries_delay_output_action})
}, {retries: retries_output_action, delay: retries_delay_output_action})
break
} while (--attempts)
}
@@ -596,14 +596,14 @@ function quit(reason) {
}
//Consumed API requests
if ((!mocked)&&(!/^NOT_NEEDED$/.test(token))) {
if ((!mocked) && (!/^NOT_NEEDED$/.test(token))) {
info.break()
info.section("Consumed API requests")
info(" * provided that no other app used your quota during execution", "")
const {data:current} = await rest.rateLimit.get().catch(() => ({data:{resources:{}}}))
const {data: current} = await rest.rateLimit.get().catch(() => ({data: {resources: {}}}))
for (const type of ["core", "graphql", "search"]) {
const used = resources[type].remaining - current.resources[type].remaining
info({core:"REST API", graphql:"GraphQL API", search:"Search API"}[type], (Number.isFinite(used)&&(used >= 0)) ? used : "(unknown)")
info({core: "REST API", graphql: "GraphQL API", search: "Search API"}[type], (Number.isFinite(used) && (used >= 0)) ? used : "(unknown)")
}
}

View File

@@ -10,7 +10,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
//Debug
login = login.replace(/[\n\r]/g, "")
console.debug(`metrics/compute/${login} > start`)
console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256}))
console.debug(util.inspect(q, {depth: Infinity, maxStringLength: 256}))
//Load template
const template = q.template || conf.settings.templates.default
@@ -24,14 +24,14 @@ 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 data = {q, animated:true, large:false, base:{}, config:{}, errors:[], plugins:{}, computed:{}, extras, postscripts:[]}
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 data = {q, animated: true, large: false, base: {}, config: {}, errors: [], plugins: {}, computed: {}, extras, postscripts: []}
const imports = {
plugins:Plugins,
templates:Templates,
metadata:conf.metadata,
plugins: Plugins,
templates: Templates,
metadata: conf.metadata,
...utils,
...utils.formatters({timeZone:q["config.timezone"]}),
...utils.formatters({timeZone: q["config.timezone"]}),
...(/markdown/.test(convert)
? {
imgb64(url, options) {
@@ -60,7 +60,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
//Executing base plugin and compute metrics
console.debug(`metrics/compute/${login} > compute`)
await Plugins.base({login, q, data, rest, graphql, plugins, queries, pending, imports}, conf)
await computer({login, q}, {conf, data, rest, graphql, plugins, queries, account:data.account, convert, template}, {pending, imports})
await computer({login, q}, {conf, data, rest, graphql, plugins, queries, account: data.account, convert, template}, {pending, imports})
const promised = await Promise.all(pending)
//Check plugins errors
@@ -70,7 +70,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
if (die)
throw new Error("An error occured during rendering, dying")
else
console.debug(util.inspect(errors, {depth:Infinity, maxStringLength:256}))
console.debug(util.inspect(errors, {depth: Infinity, maxStringLength: 256}))
}
//JSON output
@@ -89,7 +89,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
}
return value
}))
return {rendered, mime:"application/json"}
return {rendered, mime: "application/json"}
}
//Markdown output
@@ -100,12 +100,12 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
try {
let template = `${q.markdown}`.replace(/\n/g, "")
if (!/^https:/.test(template)) {
const {data:{default_branch:branch, full_name:repo}} = await rest.repos.get({owner:login, repo:q.repo || login})
const {data: {default_branch: branch, full_name: repo}} = await rest.repos.get({owner: login, repo: q.repo || login})
console.debug(`metrics/compute/${login} > on ${repo} with default branch ${branch}`)
template = `https://raw.githubusercontent.com/${repo}/${branch}/${template}`
}
console.debug(`metrics/compute/${login} > fetching ${template}`)
;({data:source} = await imports.axios.get(template, {headers:{Accept:"text/plain"}}))
;({data: source} = await imports.axios.get(template, {headers: {Accept: "text/plain"}}))
}
catch (error) {
console.debug(error)
@@ -123,7 +123,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
console.debug(`metrics/compute/${login} > embed called with`)
console.debug(q)
let {base} = q
q = {..._q, ...Object.fromEntries(Object.keys(Plugins).map(key => [key, false])), ...Object.fromEntries(conf.settings.plugins.base.parts.map(part => [`base.${part}`, false])), template:q.repo ? "repository" : "classic", ...q}
q = {..._q, ...Object.fromEntries(Object.keys(Plugins).map(key => [key, false])), ...Object.fromEntries(conf.settings.plugins.base.parts.map(part => [`base.${part}`, false])), template: q.repo ? "repository" : "classic", ...q}
//Translate action syntax to web syntax
let parts = []
if (base === true)
@@ -140,33 +140,33 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
}
q = Object.fromEntries([...Object.entries(q).map(([key, value]) => [key.replace(/^plugin_/, "").replace(/_/g, "."), value]), ["base", false]])
//Compute rendering
const {rendered} = await metrics({login, q}, {...arguments[1], convert:["svg", "png", "jpeg"].includes(q["config.output"]) ? q["config.output"] : null}, arguments[2])
const {rendered} = await metrics({login, q}, {...arguments[1], convert: ["svg", "png", "jpeg"].includes(q["config.output"]) ? q["config.output"] : null}, arguments[2])
console.debug(`metrics/compute/${login}/embed > ${name} > success >>>>>>>>>>>>>>>>>>>>>>`)
return `<img class="metrics-cachable" data-name="${name}" src="data:image/${{png:"png", jpeg:"jpeg"}[q["config.output"]] ?? "svg+xml"};base64,${Buffer.from(rendered).toString("base64")}">`
return `<img class="metrics-cachable" data-name="${name}" src="data:image/${{png: "png", jpeg: "jpeg"}[q["config.output"]] ?? "svg+xml"};base64,${Buffer.from(rendered).toString("base64")}">`
}
//Rendering template source
let rendered = source.replace(/\{\{ (?<content>[\s\S]*?) \}\}/g, "{%= $<content> %}")
console.debug(rendered)
for (const delimiters of [{openDelimiter:"<", closeDelimiter:">"}, {openDelimiter:"{", closeDelimiter:"}"}])
rendered = await ejs.render(rendered, {...data, s:imports.s, f:imports.format, embed}, {views, async:true, ...delimiters})
for (const delimiters of [{openDelimiter: "<", closeDelimiter: ">"}, {openDelimiter: "{", closeDelimiter: "}"}])
rendered = await ejs.render(rendered, {...data, s: imports.s, f: imports.format, embed}, {views, async: true, ...delimiters})
console.debug(`metrics/compute/${login} > success`)
//Output
if (convert === "markdown-pdf") {
return imports.svg.pdf(rendered, {
paddings:q["config.padding"] || conf.settings.padding,
style:extras.css,
twemojis:q["config.twemoji"],
gemojis:q["config.gemoji"],
octicons:q["config.octicon"],
paddings: q["config.padding"] || conf.settings.padding,
style: extras.css,
twemojis: q["config.twemoji"],
gemojis: q["config.gemoji"],
octicons: q["config.octicon"],
rest,
})
}
return {rendered, mime:"text/html"}
return {rendered, mime: "text/html"}
}
//Rendering
console.debug(`metrics/compute/${login} > render`)
let rendered = await ejs.render(image, {...data, s:imports.s, f:imports.format, style, fonts}, {views, async:true})
let rendered = await ejs.render(image, {...data, s: imports.s, f: imports.format, style, fonts}, {views, async: true})
//Additional transformations
if (q["config.twemoji"])
@@ -192,7 +192,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
console.debug(`metrics/compute/${login} > verified SVG, no parsing errors found`)
}
//Resizing
const {resized, mime} = await imports.svg.resize(rendered, {paddings:q["config.padding"] || conf.settings.padding, convert:convert === "svg" ? null : convert, scripts:[...data.postscripts, extras.js || null].filter(x => x)})
const {resized, mime} = await imports.svg.resize(rendered, {paddings: q["config.padding"] || conf.settings.padding, convert: convert === "svg" ? null : convert, scripts: [...data.postscripts, extras.js || null].filter(x => x)})
rendered = resized
//Result
@@ -212,37 +212,37 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
//Metrics insights
metrics.insights = async function({login}, {graphql, rest, conf}, {Plugins, Templates}) {
const q = {
template:"classic",
achievements:true,
"achievements.threshold":"X",
isocalendar:true,
"isocalendar.duration":"full-year",
languages:true,
"languages.limit":0,
activity:true,
"activity.limit":100,
"activity.days":0,
notable:true,
followup:true,
"followup.sections":"repositories, user",
habits:true,
"habits.from":100,
"habits.days":7,
"habits.facts":false,
"habits.charts":true,
introduction:true,
template: "classic",
achievements: true,
"achievements.threshold": "X",
isocalendar: true,
"isocalendar.duration": "full-year",
languages: true,
"languages.limit": 0,
activity: true,
"activity.limit": 100,
"activity.days": 0,
notable: true,
followup: true,
"followup.sections": "repositories, user",
habits: true,
"habits.from": 100,
"habits.days": 7,
"habits.facts": false,
"habits.charts": true,
introduction: true,
}
const plugins = {
achievements:{enabled:true},
isocalendar:{enabled:true},
languages:{enabled:true, extras:false},
activity:{enabled:true, markdown:"extended"},
notable:{enabled:true},
followup:{enabled:true},
habits:{enabled:true, extras:false},
introduction:{enabled:true},
achievements: {enabled: true},
isocalendar: {enabled: true},
languages: {enabled: true, extras: false},
activity: {enabled: true, markdown: "extended"},
notable: {enabled: true},
followup: {enabled: true},
habits: {enabled: true, extras: false},
introduction: {enabled: true},
}
return metrics({login, q}, {graphql, rest, plugins, conf, convert:"json"}, {Plugins, Templates})
return metrics({login, q}, {graphql, rest, plugins, conf, convert: "json"}, {Plugins, Templates})
}
//Metrics insights static render
@@ -260,7 +260,7 @@ metrics.insights.output = async function({login, imports, conf}, {graphql, rest,
await page.goto(`${server}/about/${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.waitForSelector(".container .user", {timeout:10 * 60 * 1000})
await page.waitForSelector(".container .user", {timeout: 10 * 60 * 1000})
//Rendering
console.debug(`metrics/compute/${login} > insights > rendering data`)
@@ -273,9 +273,9 @@ 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", "about/.statics/style.css"].map(path => utils.axios.get(`${server}/${path}`)))).map(({data: style}) => `<style>${style}</style>`).join("\n")}
</body>
</html>`
await browser.close()
return {mime:"text/html", rendered}
return {mime: "text/html", rendered}
}

View File

@@ -1,7 +1,7 @@
//Imports
import fs from "fs"
import yaml from "js-yaml"
import {marked} from "marked"
import { marked } from "marked"
import fetch from "node-fetch"
import path from "path"
import url from "url"
@@ -13,7 +13,7 @@ const categories = ["core", "github", "social", "community"]
let previous = null
//Environment
const env = {ghactions:`${process.env.GITHUB_ACTIONS}` === "true"}
const env = {ghactions: `${process.env.GITHUB_ACTIONS}` === "true"}
/**Metadata descriptor parser */
export default async function metadata({log = true, diff = false} = {}) {
@@ -50,7 +50,7 @@ export default async function metadata({log = true, diff = false} = {}) {
if (!(await fs.promises.lstat(path.join(___plugins, name))).isDirectory())
continue
logger(`metrics/metadata > loading plugin metadata [community/${name}]`)
Plugins[name] = await metadata.plugin({__plugins:___plugins, __templates, name, logger})
Plugins[name] = await metadata.plugin({__plugins: ___plugins, __templates, name, logger})
Plugins[name].community = true
}
continue
@@ -84,7 +84,7 @@ export default async function metadata({log = true, diff = false} = {}) {
const descriptor = yaml.load(`${await fs.promises.readFile(__descriptor, "utf-8")}`)
//Metadata
return {plugins:Plugins, templates:Templates, packaged, descriptor, env}
return {plugins: Plugins, templates: Templates, packaged, descriptor, env}
}
/**Metadata extractor for inputs */
@@ -104,14 +104,14 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
//Inputs parser
{
meta.inputs = function({data:{user = null} = {}, q, account}, defaults = {}) {
meta.inputs = function({data: {user = null} = {}, q, account}, defaults = {}) {
//Support check
if (!account)
console.debug(`metrics/inputs > account type not set for plugin ${name}!`)
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: `Not supported for: ${context}`, instance: new Error()}}
}
//Special values replacer
const replacer = value => {
@@ -128,7 +128,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
}
//Inputs checks
const result = Object.fromEntries(
Object.entries(inputs).map(([key, {type, format, default:defaulted, min, max, values, inherits:_inherits}]) => [
Object.entries(inputs).map(([key, {type, format, default: defaulted, min, max, values, inherits: _inherits}]) => [
//Format key
metadata.to.query(key, {name}),
//Format value
@@ -165,7 +165,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
logger(`metrics/inputs > failed to decode uri : ${value}`)
value = defaulted
}
const separators = {"comma-separated":",", "space-separated":" "}
const separators = {"comma-separated": ",", "space-separated": " "}
const separator = separators[[format].flat().filter(s => s in separators)[0]] ?? ","
return value.split(separator).map(v => replacer(v).toLocaleLowerCase()).filter(v => Array.isArray(values) ? values.includes(v) : true).filter(v => v)
}
@@ -230,13 +230,14 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
Object.entries(inputs).map(([key, value]) => [
key,
{
comment:"",
descriptor:yaml.dump({
[key]:Object.fromEntries(
Object.entries(value).filter(([key]) => ["description", "default", "required"].includes(key)).map(([k, v]) => k === "description" ? [k, v.split("\n")[0]] : k === "default" ? [k, ((/^\$\{\{[\s\S]+\}\}$/.test(v)) || (["config_presets", "config_timezone", "use_prebuilt_image"].includes(key))) ? v : "<default-value>"] : [k, v]
comment: "",
descriptor: yaml.dump({
[key]: Object.fromEntries(
Object.entries(value).filter(([key]) => ["description", "default", "required"].includes(key)).map(([k, v]) =>
k === "description" ? [k, v.split("\n")[0]] : k === "default" ? [k, ((/^\$\{\{[\s\S]+\}\}$/.test(v)) || (["config_presets", "config_timezone", "use_prebuilt_image"].includes(key))) ? v : "<default-value>"] : [k, v]
),
),
}, {quotingType:'"', noCompatMode:true}),
}, {quotingType: '"', noCompatMode: true}),
},
]),
)
@@ -258,9 +259,9 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
value = "<default-value>"
}
}
else
else {
value = process.env[`INPUT_${key.toUpperCase()}`]?.trim() ?? "<default-value>"
}
const unspecified = value === "<default-value>"
//From presets
@@ -279,32 +280,32 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
q[key] = value
}
}
return meta.inputs({q, account:"bypass"})
return meta.inputs({q, account: "bypass"})
}
}
//Web metadata
{
meta.web = Object.fromEntries(
Object.entries(inputs).map(([key, {type, description:text, example, default:defaulted, min = 0, max = 9999, values}]) => [
Object.entries(inputs).map(([key, {type, description: text, example, default: defaulted, min = 0, max = 9999, values}]) => [
//Format key
metadata.to.query(key),
//Value descriptor
(() => {
switch (type) {
case "boolean":
return {text, type:"boolean", defaulted:/^(?:[Tt]rue|[Oo]n|[Yy]es|1)$/.test(defaulted) ? true : /^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(defaulted) ? false : defaulted}
return {text, type: "boolean", defaulted: /^(?:[Tt]rue|[Oo]n|[Yy]es|1)$/.test(defaulted) ? true : /^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(defaulted) ? false : defaulted}
case "number":
return {text, type:"number", min, max, defaulted}
return {text, type: "number", min, max, defaulted}
case "array":
return {text, type:"text", placeholder:example ?? defaulted, defaulted}
return {text, type: "text", placeholder: example ?? defaulted, defaulted}
case "string": {
if (Array.isArray(values))
return {text, type:"select", values, defaulted}
return {text, type:"text", placeholder:example ?? defaulted, defaulted}
return {text, type: "select", values, defaulted}
return {text, type: "text", placeholder: example ?? defaulted, defaulted}
}
case "json":
return {text, type:"text", placeholder:example ?? defaulted, defaulted}
return {text, type: "text", placeholder: example ?? defaulted, defaulted}
default:
return null
}
@@ -317,7 +318,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
{
//Extract demos
const raw = `${await fs.promises.readFile(path.join(__plugins, name, "README.md"), "utf-8")}`
const demo = meta.examples ? demos({examples:meta.examples}) : raw.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? "<td></td>"
const demo = meta.examples ? demos({examples: meta.examples}) : raw.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? "<td></td>"
//Compatibility
const templates = {}
@@ -337,7 +338,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
const header = [
"<table>",
` <tr><th colspan="2"><h3>${meta.name}</h3></th></tr>`,
` <tr><td colspan="2" align="center">${marked.parse(meta.description ?? "", {silent:true})}</td></tr>`,
` <tr><td colspan="2" align="center">${marked.parse(meta.description ?? "", {silent: true})}</td></tr>`,
meta.authors?.length ? `<tr><th>Authors</th><td>${[meta.authors].flat().map(author => `<a href="https://github.com/${author}">@${author}</a>`)}</td></tr>` : "",
" <tr>",
' <th rowspan="3">Supported features<br><sub><a href="metadata.yml">→ Full specification</a></sub></th>',
@@ -353,14 +354,16 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
}</td>`,
" </tr>",
" <tr>",
` <td>${[
...(meta.scopes ?? []).map(scope => `<code>🔑 ${{public_access:"(scopeless)"}[scope] ?? scope}</code>`),
...Object.entries(inputs).filter(([_, {type}]) => type === "token").map(([token]) => `<code>🗝 ${token}</code>`),
...(meta.scopes?.length ? ["read:org", "read:user", "read:packages", "repo"].map(scope => !meta.scopes.includes(scope) ? `<code>${scope} (optional)</code>` : null).filter(v => v) : []),
].filter(v => v).join(" ") || "<i>No tokens are required for this plugin</i>"}</td>`,
` <td>${
[
...(meta.scopes ?? []).map(scope => `<code>🔑 ${{public_access: "(scopeless)"}[scope] ?? scope}</code>`),
...Object.entries(inputs).filter(([_, {type}]) => type === "token").map(([token]) => `<code>🗝 ${token}</code>`),
...(meta.scopes?.length ? ["read:org", "read:user", "read:packages", "repo"].map(scope => !meta.scopes.includes(scope) ? `<code>${scope} (optional)</code>` : null).filter(v => v) : []),
].filter(v => v).join(" ") || "<i>No tokens are required for this plugin</i>"
}</td>`,
" </tr>",
" <tr>",
demos({colspan:2, wrap:name === "base", examples:meta.examples}),
demos({colspan: 2, wrap: name === "base", examples: meta.examples}),
" </tr>",
"</table>",
].filter(v => v).join("\n")
@@ -415,7 +418,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
cell.push(`<b>allowed values:</b><ul>${o.values.map(value => `<li>${value}</li>`).join("")}</ul>`)
return ` <tr>
<td nowrap="nowrap"><h4><code>${option}</code></h4></td>
<td rowspan="2">${marked.parse(description, {silent:true})}<img width="900" height="1" alt=""></td>
<td rowspan="2">${marked.parse(description, {silent: true})}<img width="900" height="1" alt=""></td>
</tr>
<tr>
<td nowrap="nowrap">${cell.join("\n")}</td>
@@ -462,7 +465,7 @@ metadata.template = async function({__templates, name, plugins}) {
const header = [
"<table>",
` <tr><th colspan="2"><h3>${meta.name ?? "(unnamed template)"}</h3></th></tr>`,
` <tr><td colspan="2" align="center">${marked.parse(meta.description ?? "", {silent:true})}</td></tr>`,
` <tr><td colspan="2" align="center">${marked.parse(meta.description ?? "", {silent: true})}</td></tr>`,
" <tr>",
' <th rowspan="3">Supported features<br><sub><a href="metadata.yml">→ Full specification</a></sub></th>',
` <td>${Object.entries(compatibility).filter(([_, value]) => value).map(([id]) => `<a href="/source/plugins/${id}/README.md" title="${plugins[id].name}">${plugins[id].icon}</a>`).join(" ")}${meta.formats?.includes("markdown") ? " <code>✓ embed()</code>" : ""}</td>`,
@@ -489,24 +492,24 @@ metadata.template = async function({__templates, name, plugins}) {
}</td>`,
" </tr>",
" <tr>",
demos({colspan:2, examples:meta.examples}),
demos({colspan: 2, examples: meta.examples}),
" </tr>",
"</table>",
].join("\n")
//Result
return {
name:meta.name ?? "(unnamed template)",
description:meta.description ?? "",
index:meta.index ?? null,
formats:meta.formats ?? null,
supports:meta.supports ?? null,
readme:{
demo:demos({examples:meta.examples}),
compatibility:{
name: meta.name ?? "(unnamed template)",
description: meta.description ?? "",
index: meta.index ?? null,
formats: meta.formats ?? null,
supports: meta.supports ?? null,
readme: {
demo: demos({examples: meta.examples}),
compatibility: {
...Object.fromEntries(Object.entries(compatibility).filter(([_, value]) => value)),
...Object.fromEntries(Object.entries(compatibility).filter(([_, value]) => !value).map(([key, value]) => [key, meta.formats?.includes("markdown") ? "embed" : value])),
base:true,
base: true,
},
header,
},

View File

@@ -7,8 +7,8 @@ import metadata from "./metadata.mjs"
/**Presets parser */
export default async function presets(list, {log = true, core = null} = {}) {
//Init
const {plugins} = await metadata({log:false})
const {"config.presets":files} = plugins.core.inputs({q:{"config.presets":list}, account:"bypass"})
const {plugins} = await metadata({log: false})
const {"config.presets": files} = plugins.core.inputs({q: {"config.presets": list}, account: "bypass"})
const logger = log ? console.debug : () => null
const allowed = Object.entries(metadata.inputs).filter(([_, {type, preset}]) => (type !== "token") && (!/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(preset))).map(([key]) => key)
const env = core ? "action" : "web"
@@ -36,7 +36,7 @@ export default async function presets(list, {log = true, core = null} = {}) {
logger(`metrics/presets > ${file} cannot be loaded in current environment ${env}, skipping`)
continue
}
const {schema, with:inputs} = yaml.load(text)
const {schema, with: inputs} = yaml.load(text)
logger(`metrics/presets > ${file} preset schema is ${schema}`)
//Evaluate preset

View File

@@ -27,15 +27,15 @@ export default async function({log = true, sandbox = false, community = {}} = {}
const logger = log ? console.debug : () => null
logger("metrics/setup > setup")
const conf = {
authenticated:null,
templates:{},
queries:{},
settings:{port:3000},
metadata:{},
paths:{
statics:__statics,
templates:__templates,
node_modules:__modules,
authenticated: null,
templates: {},
queries: {},
settings: {port: 3000},
metadata: {},
paths: {
statics: __statics,
templates: __templates,
node_modules: __modules,
},
}
@@ -64,13 +64,13 @@ export default async function({log = true, sandbox = false, community = {}} = {}
}
if (!conf.settings.templates)
conf.settings.templates = {default:"classic", enabled:[]}
conf.settings.templates = {default: "classic", enabled: []}
if (!conf.settings.plugins)
conf.settings.plugins = {}
conf.settings.community = {...conf.settings.community, ...community}
conf.settings.plugins.base = {parts:["header", "activity", "community", "repositories", "metadata"]}
conf.settings.plugins.base = {parts: ["header", "activity", "community", "repositories", "metadata"]}
if (conf.settings.debug)
logger(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256}))
logger(util.inspect(conf.settings, {depth: Infinity, maxStringLength: 256}))
//Load package settings
logger("metrics/setup > load package.json")
@@ -85,7 +85,7 @@ export default async function({log = true, sandbox = false, community = {}} = {}
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})
await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
//Download community templates
for (const template of conf.settings.community.templates) {
try {
@@ -95,10 +95,10 @@ export default async function({log = true, sandbox = false, community = {}} = {}
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"})
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.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)
@@ -113,18 +113,18 @@ export default async function({log = true, sandbox = false, community = {}} = {}
logger(`metrics/setup > @${name} extended from ${inherit}`)
await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs"))
}
else
else {
logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`)
}
}
}
else
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})
await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
logger(`metrics/setup > loaded community template ${name}`)
}
catch (error) {
@@ -133,9 +133,9 @@ export default async function({log = true, sandbox = false, community = {}} = {}
}
}
}
else
else {
logger("metrics/setup > no community templates to install")
}
//Load templates
for (const name of await fs.promises.readdir(__templates)) {
@@ -145,10 +145,10 @@ export default async function({log = true, sandbox = false, community = {}} = {}
continue
logger(`metrics/setup > load template [${name}]`)
//Cache templates files
const files = ["image.svg", "style.css", "fonts.css"].map(file => path.join(__templates, (fs.existsSync(path.join(directory, file)) ? name : "classic"), file))
const files = ["image.svg", "style.css", "fonts.css"].map(file => path.join(__templates, fs.existsSync(path.join(directory, file)) ? name : "classic", file))
const [image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(file)}`))
const partials = JSON.parse(`${await fs.promises.readFile(path.join(directory, "partials/_.json"))}`)
conf.templates[name] = {image, style, fonts, partials, views:[directory]}
conf.templates[name] = {image, style, fonts, partials, views: [directory]}
//Cache templates scripts
Templates[name] = await (async () => {
@@ -165,7 +165,7 @@ export default async function({log = true, sandbox = false, community = {}} = {}
const [image, style, fonts] = files.map(file => `${fs.readFileSync(file)}`)
const partials = JSON.parse(`${fs.readFileSync(path.join(directory, "partials/_.json"))}`)
logger(`metrics/setup > reload template [${name}] > success`)
return {image, style, fonts, partials, views:[directory]}
return {image, style, fonts, partials, views: [directory]}
},
})
}
@@ -177,7 +177,7 @@ export default async function({log = true, sandbox = false, community = {}} = {}
case "community": {
const ___plugins = path.join(__plugins, "community")
for (const name of await fs.promises.readdir(___plugins))
await load.plugin(name, {__plugins:___plugins, Plugins, conf, logger})
await load.plugin(name, {__plugins: ___plugins, Plugins, conf, logger})
continue
}
default:
@@ -191,7 +191,7 @@ export default async function({log = true, sandbox = false, community = {}} = {}
//Store authenticated user
if (conf.settings.token) {
try {
conf.authenticated = (await (new OctokitRest.Octokit({auth:conf.settings.token})).users.getAuthenticated()).data.login
conf.authenticated = (await (new OctokitRest.Octokit({auth: conf.settings.token})).users.getAuthenticated()).data.login
logger(`metrics/setup > setup > authenticated as ${conf.authenticated}`)
}
catch (error) {
@@ -251,7 +251,8 @@ const load = {
}
}
//Create queries formatters
Object.keys(queries).map(query => queries[query.substring(1)] = (vars = {}) => {
Object.keys(queries).map(query =>
queries[query.substring(1)] = (vars = {}) => {
let queried = queries[query]
for (const [key, value] of Object.entries(vars))
queried = queried.replace(new RegExp(`[$]${key}`, "g"), value)

View File

@@ -1,17 +1,16 @@
//Imports
import octicons from "@primer/octicons"
import fs from "fs/promises"
import prism_lang from "prismjs/components/index.js"
import axios from "axios"
import processes from "child_process"
import crypto from "crypto"
import {minify as csso} from "csso"
import { minify as csso } from "csso"
import emoji from "emoji-name-map"
import fss from "fs"
import fs from "fs/promises"
import GIFEncoder from "gifencoder"
import jimp from "jimp"
import linguist from "linguist-js"
import {marked} from "marked"
import { marked } from "marked"
import minimatch from "minimatch"
import nodechartist from "node-chartist"
import fetch from "node-fetch"
@@ -20,6 +19,7 @@ import os from "os"
import paths from "path"
import PNG from "png-js"
import prism from "prismjs"
import prism_lang from "prismjs/components/index.js"
import _puppeteer from "puppeteer"
import purgecss from "purgecss"
import readline from "readline"
@@ -34,7 +34,7 @@ import xmlformat from "xml-formatter"
prism_lang()
//Exports
export {axios, emoji, fetch, fs, git, jimp, minimatch, opengraph, os, paths, processes, rss, url, util}
export { axios, emoji, fetch, fs, git, jimp, minimatch, opengraph, os, paths, processes, rss, url, util }
/**Returns module __dirname */
export function __module(module) {
@@ -45,25 +45,25 @@ export function __module(module) {
export const puppeteer = {
async launch() {
return _puppeteer.launch({
headless:this.headless,
executablePath:process.env.PUPPETEER_BROWSER_PATH,
args:this.headless ? ["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] : [],
ignoreDefaultArgs:["--disable-extensions"],
headless: this.headless,
executablePath: process.env.PUPPETEER_BROWSER_PATH,
args: this.headless ? ["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] : [],
ignoreDefaultArgs: ["--disable-extensions"],
})
},
headless:true,
headless: true,
}
/**Plural formatter */
export function s(value, end = "") {
return value !== 1 ? {y:"ies", "":"s"}[end] : end
return value !== 1 ? {y: "ies", "": "s"}[end] : end
}
/**Formatters */
export function formatters({timeZone} = {}) {
//Check options
try {
new Date().toLocaleString("fr", {timeZoneName:"short", timeZone})
new Date().toLocaleString("fr", {timeZoneName: "short", timeZone})
}
catch {
timeZone = undefined
@@ -72,7 +72,7 @@ export function formatters({timeZone} = {}) {
/**Formatter */
const format = function(n, {sign = false, unit = true, fixed} = {}) {
if (unit) {
for (const {u, v} of [{u:"b", v:10 ** 9}, {u:"m", v:10 ** 6}, {u:"k", v:10 ** 3}]) {
for (const {u, v} of [{u: "b", v: 10 ** 9}, {u: "m", v: 10 ** 6}, {u: "k", v: 10 ** 3}]) {
if (n / v >= 1)
return `${(sign) && (n > 0) ? "+" : ""}${(n / v).toFixed(fixed ?? 2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
}
@@ -82,7 +82,7 @@ export function formatters({timeZone} = {}) {
/**Bytes formatter */
format.bytes = function(n) {
for (const {u, v} of [{u:"E", v:10 ** 18}, {u:"P", v:10 ** 15}, {u:"T", v:10 ** 12}, {u:"G", v:10 ** 9}, {u:"M", v:10 ** 6}, {u:"k", v:10 ** 3}]) {
for (const {u, v} of [{u: "E", v: 10 ** 18}, {u: "P", v: 10 ** 15}, {u: "T", v: 10 ** 12}, {u: "G", v: 10 ** 9}, {u: "M", v: 10 ** 6}, {u: "k", v: 10 ** 3}]) {
if (n / v >= 1)
return `${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B`
}
@@ -110,11 +110,11 @@ export function formatters({timeZone} = {}) {
format.date = function(string, options) {
if (options.date) {
delete options.date
Object.assign(options, {day:"numeric", month:"short", year:"numeric"})
Object.assign(options, {day: "numeric", month: "short", year: "numeric"})
}
if (options.time) {
delete options.time
Object.assign(options, {hour:"2-digit", minute:"2-digit", second:"2-digit"})
Object.assign(options, {hour: "2-digit", minute: "2-digit", second: "2-digit"})
}
return new Intl.DateTimeFormat("en-GB", {timeZone, ...options}).format(new Date(string))
}
@@ -139,7 +139,7 @@ export function shuffle(array) {
}
/**Escape html */
export function htmlescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) {
export function htmlescape(string, u = {"&": true, "<": true, ">": true, '"': true, "'": true}) {
return string
.replace(/&(?!(?:amp|lt|gt|quot|apos);)/g, u["&"] ? "&amp;" : "&")
.replace(/</g, u["<"] ? "&lt;" : "<")
@@ -149,7 +149,7 @@ export function htmlescape(string, u = {"&":true, "<":true, ">":true, '"':true,
}
/**Unescape html */
export function htmlunescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) {
export function htmlunescape(string, u = {"&": true, "<": true, ">": true, '"': true, "'": true}) {
return string
.replace(/&lt;/g, u["<"] ? "<" : "&lt;")
.replace(/&gt;/g, u[">"] ? ">" : "&gt;")
@@ -173,7 +173,7 @@ export async function chartist() {
/**Language analyzer (single file) */
export async function language({filename, patch}) {
console.debug(`metrics/language > ${filename}`)
const {files:{results}} = await linguist(filename, {fileContent:patch})
const {files: {results}} = await linguist(filename, {fileContent: patch})
const result = (results[filename] ?? "unknown").toLocaleLowerCase()
console.debug(`metrics/language > ${filename} > result: ${result}`)
return result
@@ -181,7 +181,7 @@ export async function language({filename, patch}) {
/**Run command (use this to execute commands and process whole output at once, may not be suitable for large outputs) */
export async function run(command, options, {prefixed = true, log = true} = {}) {
const prefix = {win32:"wsl"}[process.platform] ?? ""
const prefix = {win32: "wsl"}[process.platform] ?? ""
command = `${prefixed ? prefix : ""} ${command}`.trim()
return new Promise((solve, reject) => {
console.debug(`metrics/command/run > ${command}`)
@@ -202,7 +202,7 @@ export async function run(command, options, {prefixed = true, log = true} = {})
/**Spawn command (use this to execute commands and process output on the fly) */
export async function spawn(command, args = [], options = {}, {prefixed = true, timeout = 300 * 1000, stdout} = {}) { //eslint-disable-line max-params
const prefix = {win32:"wsl"}[process.platform] ?? ""
const prefix = {win32: "wsl"}[process.platform] ?? ""
if ((prefixed) && (prefix)) {
args.unshift(command)
command = prefix
@@ -211,8 +211,8 @@ export async function spawn(command, args = [], options = {}, {prefixed = true,
throw new Error("`stdout` argument was not provided, use run() instead of spawn() if processing output is not needed")
return new Promise((solve, reject) => {
console.debug(`metrics/command/spawn > ${command} with ${args.join(" ")}`)
const child = processes.spawn(command, args, {...options, shell:true, timeout})
const reader = readline.createInterface({input:child.stdout})
const child = processes.spawn(command, args, {...options, shell: true, timeout})
const reader = readline.createInterface({input: child.stdout})
reader.on("line", stdout)
const closed = new Promise(close => reader.on("close", close))
child.on("close", async code => {
@@ -245,18 +245,18 @@ export function highlight(code, lang) {
/**Markdown-html sanitizer-interpreter */
export async function markdown(text, {mode = "inline", codelines = Infinity} = {}) {
//Sanitize user input once to prevent injections and parse into markdown
let rendered = await marked.parse(htmlunescape(htmlsanitize(text)), {highlight, silent:true, xhtml:true})
let rendered = await marked.parse(htmlunescape(htmlsanitize(text)), {highlight, silent: true, xhtml: true})
//Markdown mode
switch (mode) {
case "inline": {
rendered = htmlsanitize(
htmlsanitize(rendered, {
allowedTags:["h1", "h2", "h3", "h4", "h5", "h6", "br", "blockquote", "code", "span"],
allowedAttributes:{code:["class"], span:["class"]},
allowedTags: ["h1", "h2", "h3", "h4", "h5", "h6", "br", "blockquote", "code", "span"],
allowedAttributes: {code: ["class"], span: ["class"]},
}),
{
allowedAttributes:{code:["class"], span:["class"]},
transformTags:{h1:"b", h2:"b", h3:"b", h4:"b", h5:"b", h6:"b", blockquote:"i"},
allowedAttributes: {code: ["class"], span: ["class"]},
transformTags: {h1: "b", h2: "b", h3: "b", h4: "b", h5: "b", h6: "b", blockquote: "i"},
},
)
break
@@ -349,7 +349,7 @@ export const svg = {
}
//Additional transformations
if (twemojis)
rendered = await svg.twemojis(rendered, {custom:false})
rendered = await svg.twemojis(rendered, {custom: false})
if ((gemojis) && (rest))
rendered = await svg.gemojis(rendered, {rest})
if (octicons)
@@ -358,13 +358,13 @@ export const svg = {
//Render through browser and print pdf
console.debug("metrics/svg/pdf > loading svg")
const page = await svg.resize.browser.newPage()
page.on("console", ({_text:text}) => console.debug(`metrics/svg/pdf > puppeteer > ${text}`))
await page.setContent(`<main class="markdown-body">${rendered}</main>`, {waitUntil:["load", "domcontentloaded", "networkidle2"]})
page.on("console", ({_text: text}) => console.debug(`metrics/svg/pdf > puppeteer > ${text}`))
await page.setContent(`<main class="markdown-body">${rendered}</main>`, {waitUntil: ["load", "domcontentloaded", "networkidle2"]})
console.debug("metrics/svg/pdf > loaded svg successfully")
const margins = (Array.isArray(paddings) ? paddings : paddings.split(",")).join(" ")
console.debug(`metrics/svg/pdf > margins set to ${margins}`)
await page.addStyleTag({
content:`
content: `
main { margin: ${margins}; }
main svg { height: 1em; width: 1em; }
${await fs.readFile(paths.join(__module(import.meta.url), "../../../node_modules", "@primer/css/dist/markdown.css")).catch(_ => "")}${style}
@@ -374,7 +374,7 @@ export const svg = {
//Result
await page.close()
console.debug("metrics/svg/pdf > rendering complete")
return {rendered, mime:"application/pdf"}
return {rendered, mime: "application/pdf"}
},
/**Render and resize svg */
async resize(rendered, {paddings, convert, scripts = []}) {
@@ -384,7 +384,7 @@ export const svg = {
console.debug(`metrics/svg/resize > started ${await svg.resize.browser.version()}`)
}
//Format padding
const padding = {width:1, height:1, absolute:{width:0, height:0}}
const padding = {width: 1, height: 1, absolute: {width: 0, height: 0}}
paddings = Array.isArray(paddings) ? paddings : `${paddings}`.split(",").map(x => x.trim())
for (const [i, dimension] of [[0, "width"], [1, "height"]]) {
let operands = (paddings?.[i] ?? paddings[0])
@@ -400,18 +400,18 @@ export const svg = {
//Render through browser and resize height
console.debug("metrics/svg/resize > loading svg")
const page = await svg.resize.browser.newPage()
page.setViewport({width:980, height:980})
page.setViewport({width: 980, height: 980})
page
.on("console", message => console.debug(`metrics/svg/resize > puppeteer > ${message.text()}`))
.on("pageerror", error => console.debug(`metrics/svg/resize > puppeteer > ${error.message}`))
await page.setContent(rendered, {waitUntil:["load", "domcontentloaded", "networkidle2"]})
await page.setContent(rendered, {waitUntil: ["load", "domcontentloaded", "networkidle2"]})
console.debug("metrics/svg/resize > loaded svg successfully")
await page.addStyleTag({content:"body { margin: 0; padding: 0; }"})
await page.addStyleTag({content: "body { margin: 0; padding: 0; }"})
let mime = "image/svg+xml"
console.debug("metrics/svg/resize > resizing svg")
let height, resized, width
try {
({resized, width, height} = await page.evaluate(
;({resized, width, height} = await page.evaluate(
async (padding, scripts) => {
//Execute additional JavaScript
for (const script of scripts) {
@@ -431,7 +431,7 @@ export const svg = {
console.debug(`animations are ${animated ? "enabled" : "disabled"}`)
await new Promise(solve => setTimeout(solve, 2400))
//Get bounds and resize
let {y:height, width} = document.querySelector("svg #metrics-end").getBoundingClientRect()
let {y: height, width} = document.querySelector("svg #metrics-end").getBoundingClientRect()
console.debug(`bounds width=${width}, height=${height}`)
height = Math.max(1, Math.ceil(height * padding.height + padding.absolute.height))
width = Math.max(1, Math.ceil(width * padding.width + padding.absolute.width))
@@ -445,7 +445,7 @@ export const svg = {
if (animated)
document.querySelector("svg").classList.remove("no-animations")
//Result
return {resized:new XMLSerializer().serializeToString(document.querySelector("svg")), height, width}
return {resized: new XMLSerializer().serializeToString(document.querySelector("svg")), height, width}
},
padding,
scripts,
@@ -458,7 +458,7 @@ export const svg = {
//Convert if required
if (convert) {
console.debug(`metrics/svg/resize > convert to ${convert}`)
resized = await page.screenshot({type:convert, clip:{x:0, y:0, width, height}, omitBackground:true})
resized = await page.screenshot({type: convert, clip: {x: 0, y: 0, width, height}, omitBackground: true})
mime = `image/${convert}`
}
//Result
@@ -478,7 +478,7 @@ export const svg = {
}
//Compute hash
const page = await svg.resize.browser.newPage()
await page.setContent(rendered, {waitUntil:["load", "domcontentloaded", "networkidle2"]})
await page.setContent(rendered, {waitUntil: ["load", "domcontentloaded", "networkidle2"]})
const data = await page.evaluate(async () => {
document.querySelector("footer")?.remove()
return document.querySelector("svg").outerHTML
@@ -494,7 +494,7 @@ export const svg = {
//Load emojis
console.debug("metrics/svg/twemojis > rendering twemojis")
const emojis = new Map()
for (const {text:emoji, url} of twemojis.parse(rendered)) {
for (const {text: emoji, url} of twemojis.parse(rendered)) {
if (!emojis.has(emoji))
emojis.set(emoji, (await axios.get(url)).data.replace(/^<svg /, '<svg class="twemoji" '))
}
@@ -513,7 +513,7 @@ export const svg = {
const emojis = new Map()
try {
for (const [emoji, url] of Object.entries((await rest.emojis.get()).data).map(([key, value]) => [`:${key}:`, value])) {
if (((!emojis.has(emoji))) && (new RegExp(emoji, "g").test(rendered)))
if ((!emojis.has(emoji)) && (new RegExp(emoji, "g").test(rendered)))
emojis.set(emoji, `<img class="gemoji" src="${await imgb64(url)}" height="16" width="16" alt="" />`)
}
}
@@ -535,9 +535,9 @@ export const svg = {
for (const size of Object.keys(heights)) {
const octicon = `:octicon-${name}-${size}:`
if (new RegExp(`:octicon-${name}(?:-[0-9]+)?:`, "g").test(rendered)) {
icons.set(octicon, toSVG({height:size, width:size}))
icons.set(octicon, toSVG({height: size, width: size}))
if (Number(size) === 16)
icons.set(`:octicon-${name}:`, toSVG({height:size, width:size}))
icons.set(`:octicon-${name}:`, toSVG({height: size, width: size}))
}
}
}
@@ -547,7 +547,7 @@ export const svg = {
return rendered
},
/**Optimizers */
optimize:{
optimize: {
/**CSS optimizer */
async css(rendered) {
//Extract styles
@@ -558,9 +558,9 @@ export const svg = {
while (regex.test(rendered)) {
const style = htmlunescape(rendered.match(regex)?.groups?.style ?? "")
rendered = rendered.replace(regex, cleaned)
css.push({raw:style})
css.push({raw: style})
}
const content = [{raw:rendered, extension:"html"}]
const content = [{raw: rendered, extension: "html"}]
//Purge CSS
const purged = await new purgecss.PurgeCSS().purge({content, css})
@@ -574,7 +574,7 @@ export const svg = {
console.debug("metrics/svg/optimize/xml > skipped as raw option is enabled")
return rendered
}
return xmlformat(rendered, {lineSeparator:"\n", collapseContent:true})
return xmlformat(rendered, {lineSeparator: "\n", collapseContent: true})
},
/**SVG optimizer */
async svg(rendered, {raw = false} = {}, experimental = new Set()) {
@@ -587,16 +587,16 @@ export const svg = {
console.debug("metrics/svg/optimize/svg > this feature require experimental feature flag --optimize-svg")
return rendered
}
const {error, data:optimized} = await SVGO.optimize(rendered, {
multipass:true,
plugins:SVGO.extendDefaultPlugins([
const {error, data: optimized} = await SVGO.optimize(rendered, {
multipass: true,
plugins: SVGO.extendDefaultPlugins([
//Additional cleanup
{name:"cleanupListOfValues"},
{name:"removeRasterImages"},
{name:"removeScriptElement"},
{name: "cleanupListOfValues"},
{name: "removeRasterImages"},
{name: "removeScriptElement"},
//Force CSS style consistency
{name:"inlineStyles", active:false},
{name:"removeViewBox", active:false},
{name: "inlineStyles", active: false},
{name: "removeViewBox", active: false},
]),
})
if (error)
@@ -616,7 +616,7 @@ export async function record({page, width, height, frames, scale = 1, quality =
//Register images frames
const images = []
for (let i = 0; i < frames; i++) {
images.push(await page.screenshot({type:"png", clip:{width, height, x, y}, omitBackground:background}))
images.push(await page.screenshot({type: "png", clip: {width, height, x, y}, omitBackground: background}))
await wait(delay / 1000)
if (i % 10 === 0)
console.debug(`metrics/record > processed ${i}/${frames} frames`)
@@ -643,7 +643,7 @@ export async function gif({page, width, height, frames, x = 0, y = 0, repeat = t
encoder.setQuality(quality)
//Register frames
for (let i = 0; i < frames; i++) {
const buffer = new PNG(await page.screenshot({clip:{width, height, x, y}}))
const buffer = new PNG(await page.screenshot({clip: {width, height, x, y}}))
encoder.addFrame(await new Promise(solve => buffer.decode(pixels => solve(pixels))))
if (frames % 10 === 0)
console.debug(`metrics/puppeteergif > processed ${i}/${frames} frames`)

View File

@@ -1,4 +1,4 @@
import app from "./instance.mjs"
;(async function() {
await app({sandbox:process.env.SANDBOX})
await app({sandbox: process.env.SANDBOX})
})()

View File

@@ -18,7 +18,7 @@ export default async function({sandbox = false} = {}) {
//Sandbox mode
if (sandbox) {
console.debug("metrics/app > sandbox mode is specified, enabling advanced features")
Object.assign(conf.settings, {sandbox:true, optimize:true, cached:0, "plugins.default":true, extras:{default:true}})
Object.assign(conf.settings, {sandbox: true, optimize: true, cached: 0, "plugins.default": true, extras: {default: true}})
}
const {token, maxusers = 0, restricted = [], debug = false, cached = 30 * 60 * 1000, port = 3000, ratelimiter = null, plugins = null} = conf.settings
const mock = sandbox || conf.settings.mocked
@@ -48,10 +48,10 @@ export default async function({sandbox = false} = {}) {
conf.settings.token = "MOCKED_TOKEN"
}
if (debug)
console.debug(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256}))
console.debug(util.inspect(conf.settings, {depth: Infinity, maxStringLength: 256}))
//Load octokits
const api = {graphql:octokit.graphql.defaults({headers:{authorization:`token ${token}`}}), rest:new OctokitRest.Octokit({auth:token})}
const api = {graphql: octokit.graphql.defaults({headers: {authorization: `token ${token}`}}), rest: new OctokitRest.Octokit({auth: token})}
//Apply mocking if needed
if (mock)
Object.assign(api, await mocks(api))
@@ -68,8 +68,8 @@ export default async function({sandbox = false} = {}) {
skip(req, _res) {
return !!cache.get(req.params.login)
},
message:"Too many requests: retry later",
headers:true,
message: "Too many requests: retry later",
headers: true,
...ratelimiter,
}))
}
@@ -84,24 +84,24 @@ export default async function({sandbox = false} = {}) {
})
//Base routes
const limiter = ratelimit({max:debug ? Number.MAX_SAFE_INTEGER : 60, windowMs:60 * 1000, headers:false})
const limiter = ratelimit({max: debug ? Number.MAX_SAFE_INTEGER : 60, windowMs: 60 * 1000, headers: false})
const metadata = Object.fromEntries(
Object.entries(conf.metadata.plugins)
.map(([key, value]) => [key, Object.fromEntries(Object.entries(value).filter(([key]) => ["name", "icon", "category", "web", "supports", "scopes"].includes(key)))])
.map(([key, value]) => [key, key === "core" ? {...value, web:Object.fromEntries(Object.entries(value.web).filter(([key]) => /^config[.]/.test(key)).map(([key, value]) => [key.replace(/^config[.]/, ""), value]))} : value]),
.map(([key, value]) => [key, key === "core" ? {...value, web: Object.fromEntries(Object.entries(value.web).filter(([key]) => /^config[.]/.test(key)).map(([key, value]) => [key.replace(/^config[.]/, ""), value]))} : value]),
)
const enabled = Object.entries(metadata).filter(([_name, {category}]) => category !== "core").map(([name]) => ({name, category:metadata[name]?.category ?? "community", enabled:plugins[name]?.enabled ?? false}))
const templates = Object.entries(Templates).map(([name]) => ({name, enabled:(conf.settings.templates.enabled.length ? conf.settings.templates.enabled.includes(name) : true) ?? false}))
const actions = {flush:new Map()}
const requests = {rest:{limit:0, used:0, remaining:0, reset:NaN}, graphql:{limit:0, used:0, remaining:0, reset:NaN}}
const enabled = Object.entries(metadata).filter(([_name, {category}]) => category !== "core").map(([name]) => ({name, category: metadata[name]?.category ?? "community", enabled: plugins[name]?.enabled ?? false}))
const templates = Object.entries(Templates).map(([name]) => ({name, enabled: (conf.settings.templates.enabled.length ? conf.settings.templates.enabled.includes(name) : true) ?? false}))
const actions = {flush: new Map()}
const requests = {rest: {limit: 0, used: 0, remaining: 0, reset: NaN}, graphql: {limit: 0, used: 0, remaining: 0, reset: NaN}}
let _requests_refresh = false
if (!conf.settings.notoken) {
const refresh = async () => {
try {
const {limit} = await graphql("{ limit:rateLimit {limit remaining reset:resetAt used} }")
Object.assign(requests, {
rest:(await rest.rateLimit.get()).data.rate,
graphql:{...limit, reset:new Date(limit.reset).getTime()},
rest: (await rest.rateLimit.get()).data.rate,
graphql: {...limit, reset: new Date(limit.reset).getTime()},
})
}
catch {
@@ -245,9 +245,9 @@ export default async function({sandbox = false} = {}) {
console.debug(`metrics/app/${login} > awaiting pending request`)
await pending.get(login)
}
else
else {
pending.set(login, new Promise(_solve => solve = _solve))
}
//Read cached data if possible
if ((!debug) && (cached) && (cache.get(login))) {
@@ -273,7 +273,7 @@ export default async function({sandbox = false} = {}) {
try {
//Render
const q = req.query
console.debug(`metrics/app/${login} > ${util.inspect(q, {depth:Infinity, maxStringLength:256})}`)
console.debug(`metrics/app/${login} > ${util.inspect(q, {depth: Infinity, maxStringLength: 256})}`)
if ((q["config.presets"]) && (conf.settings.extras?.presets ?? conf.settings.extras?.default ?? false)) {
console.debug(`metrics/app/${login} > presets have been specified, loading them`)
Object.assign(q, await presets(q["config.presets"]))
@@ -283,9 +283,9 @@ export default async function({sandbox = false} = {}) {
rest,
plugins,
conf,
die:q["plugins.errors.fatal"] ?? false,
verify:q.verify ?? false,
convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(q["config.output"]) ? q["config.output"] : null,
die: q["plugins.errors.fatal"] ?? false,
verify: q.verify ?? false,
convert: ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(q["config.output"]) ? q["config.output"] : null,
}, {Plugins, Templates})
//Cache
if ((!debug) && (cached)) {
@@ -331,13 +331,14 @@ export default async function({sandbox = false} = {}) {
})
//Listen
app.listen(port, () => console.log([
app.listen(port, () =>
console.log([
`Listening on port │ ${port}`,
`Debug mode │ ${debug}`,
`Mocked data │ ${conf.settings.mocked ?? false}`,
`Restricted to users │ ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
`Cached time │ ${cached} seconds`,
`Rate limiter │ ${ratelimiter ? util.inspect(ratelimiter, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`,
`Rate limiter │ ${ratelimiter ? util.inspect(ratelimiter, {depth: Infinity, maxStringLength: 256}) : "(enabled)"}`,
`Max simultaneous users │ ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
`Plugins enabled │ ${enabled.map(({name}) => name).join(", ")}`,
`SVG optimization │ ${conf.settings.optimize ?? false}`,

View File

@@ -6,7 +6,7 @@
async mounted() {
//Palette
try {
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
this.palette = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
}
catch (error) {}
//Embed
@@ -26,17 +26,17 @@
await Promise.all([
//GitHub limit tracker
(async () => {
const { data: requests } = await axios.get("/.requests")
const {data: requests} = await axios.get("/.requests")
this.requests = requests
})(),
//Version
(async () => {
const { data: version } = await axios.get("/.version")
const {data: version} = await axios.get("/.version")
this.version = `v${version}`
})(),
//Hosted
(async () => {
const { data: hosted } = await axios.get("/.hosted")
const {data: hosted} = await axios.get("/.hosted")
this.hosted = hosted
})(),
])
@@ -89,12 +89,12 @@
this.metrics = (await axios.get(`/about/query/${this.user}`)).data
}
catch (error) {
this.error = { code: error.response.status, message: error.response.data }
this.error = {code: error.response.status, message: error.response.data}
}
finally {
this.pending = false
try {
const { data: requests } = await axios.get("/.requests")
const {data: requests} = await axios.get("/.requests")
this.requests = requests
}
catch {}
@@ -104,10 +104,10 @@
//Computed properties
computed: {
ranked() {
return this.metrics?.rendered.plugins.achievements.list?.filter(({ leaderboard }) => leaderboard).sort((a, b) => a.leaderboard.type.localeCompare(b.leaderboard.type)) ?? []
return this.metrics?.rendered.plugins.achievements.list?.filter(({leaderboard}) => leaderboard).sort((a, b) => a.leaderboard.type.localeCompare(b.leaderboard.type)) ?? []
},
achievements() {
return this.metrics?.rendered.plugins.achievements.list?.filter(({ leaderboard }) => !leaderboard).filter(({ title }) => !/(?:automator|octonaut|infographile)/i.test(title)) ?? []
return this.metrics?.rendered.plugins.achievements.list?.filter(({leaderboard}) => !leaderboard).filter(({title}) => !/(?:automator|octonaut|infographile)/i.test(title)) ?? []
},
introduction() {
return this.metrics?.rendered.plugins.introduction?.text ?? ""
@@ -138,8 +138,8 @@
account() {
if (!this.metrics)
return null
const { login, name } = this.metrics.rendered.user
return { login, name, avatar: this.metrics.rendered.computed.avatar, type: this.metrics?.rendered.account }
const {login, name} = this.metrics.rendered.user
return {login, name, avatar: this.metrics.rendered.computed.avatar, type: this.metrics?.rendered.account}
},
url() {
return `${window.location.protocol}//${window.location.host}/about/${this.user}`
@@ -160,7 +160,7 @@
embed: false,
localstorage: false,
searchable: false,
requests: { rest: { limit: 0, used: 0, remaining: 0, reset: NaN }, graphql: { limit: 0, used: 0, remaining: 0, reset: NaN } },
requests: {rest: {limit: 0, used: 0, remaining: 0, reset: NaN}, graphql: {limit: 0, used: 0, remaining: 0, reset: NaN}},
palette: "light",
metrics: null,
pending: false,

View File

@@ -1,6 +1,6 @@
;(async function() {
//Init
const { data: metadata } = await axios.get("/.plugins.metadata")
const {data: metadata} = await axios.get("/.plugins.metadata")
delete metadata.core.web.output
delete metadata.core.web.twemojis
//App
@@ -11,49 +11,49 @@
//Interpolate config from browser
try {
this.config.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
this.palette = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
}
catch (error) {}
//Init
await Promise.all([
//GitHub limit tracker
(async () => {
const { data: requests } = await axios.get("/.requests")
const {data: requests} = await axios.get("/.requests")
this.requests = requests
})(),
//Templates
(async () => {
const { data: templates } = await axios.get("/.templates")
const {data: templates} = await axios.get("/.templates")
templates.sort((a, b) => (a.name.startsWith("@") ^ b.name.startsWith("@")) ? (a.name.startsWith("@") ? 1 : -1) : a.name.localeCompare(b.name))
this.templates.list = templates
this.templates.selected = templates[0]?.name || "classic"
})(),
//Plugins
(async () => {
const { data: plugins } = await axios.get("/.plugins")
this.plugins.list = plugins.filter(({ name }) => metadata[name]?.supports.includes("user") || metadata[name]?.supports.includes("organization"))
const categories = [...new Set(this.plugins.list.map(({ category }) => category))]
const {data: plugins} = await axios.get("/.plugins")
this.plugins.list = plugins.filter(({name}) => metadata[name]?.supports.includes("user") || metadata[name]?.supports.includes("organization"))
const categories = [...new Set(this.plugins.list.map(({category}) => category))]
this.plugins.categories = Object.fromEntries(categories.map(category => [category, this.plugins.list.filter(value => category === value.category)]))
})(),
//Base
(async () => {
const { data: base } = await axios.get("/.plugins.base")
const {data: base} = await axios.get("/.plugins.base")
this.plugins.base = base
this.plugins.enabled.base = Object.fromEntries(base.map(key => [key, true]))
})(),
//Version
(async () => {
const { data: version } = await axios.get("/.version")
const {data: version} = await axios.get("/.version")
this.version = `v${version}`
})(),
//Hosted
(async () => {
const { data: hosted } = await axios.get("/.hosted")
const {data: hosted} = await axios.get("/.hosted")
this.hosted = hosted
})(),
])
//Generate placeholder
this.mock({ timeout: 200 })
this.mock({timeout: 200})
setInterval(() => {
const marker = document.querySelector("#metrics-end")
if (marker) {
@@ -62,7 +62,7 @@
}
}, 100)
},
components: { Prism: PrismComponent },
components: {Prism: PrismComponent},
//Watchers
watch: {
tab: {
@@ -90,10 +90,10 @@
tab: "overview",
palette: "light",
clipboard: null,
requests: { rest: { limit: 0, used: 0, remaining: 0, reset: NaN }, graphql: { limit: 0, used: 0, remaining: 0, reset: NaN } },
requests: {rest: {limit: 0, used: 0, remaining: 0, reset: NaN}, graphql: {limit: 0, used: 0, remaining: 0, reset: NaN}},
cached: new Map(),
config: Object.fromEntries(Object.entries(metadata.core.web).map(([key, { defaulted }]) => [key, defaulted])),
metadata: Object.fromEntries(Object.entries(metadata).map(([key, { web }]) => [key, web])),
config: Object.fromEntries(Object.entries(metadata.core.web).map(([key, {defaulted}]) => [key, defaulted])),
metadata: Object.fromEntries(Object.entries(metadata).map(([key, {web}]) => [key, web])),
hosted: null,
docs: {
overview: {
@@ -121,15 +121,15 @@
"base.community": "Community stats",
"base.repositories": "Repositories metrics",
"base.metadata": "Metadata",
...Object.fromEntries(Object.entries(metadata).map(([key, { name }]) => [key, name])),
...Object.fromEntries(Object.entries(metadata).map(([key, {name}]) => [key, name])),
},
options: {
descriptions: { ...(Object.assign({}, ...Object.entries(metadata).flatMap(([key, { web }]) => web))) },
descriptions: {...(Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)))},
...(Object.fromEntries(
Object.entries(
Object.assign({}, ...Object.entries(metadata).flatMap(([key, { web }]) => web)),
Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)),
)
.map(([key, { defaulted }]) => [key, defaulted]),
.map(([key, {defaulted}]) => [key, defaulted]),
)),
},
},
@@ -157,7 +157,7 @@
computed: {
//Unusable plugins
unusable() {
return this.plugins.list.filter(({ name }) => this.plugins.enabled[name]).filter(({ enabled }) => !enabled).map(({ name }) => name)
return this.plugins.list.filter(({name}) => this.plugins.enabled[name]).filter(({enabled}) => !enabled).map(({name}) => name)
},
//User's avatar
avatar() {
@@ -239,13 +239,13 @@
` base: ${Object.entries(this.plugins.enabled.base).filter(([key, value]) => value).map(([key]) => key).join(", ") || '""'}`,
...[
...Object.entries(this.plugins.options).filter(([key, value]) => (key in metadata.base.web) && (value !== metadata.base.web[key]?.defaulted)).map(([key, value]) =>
` ${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? { true: "yes", false: "no" }[value] : value}`
` ${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`
),
...Object.entries(this.plugins.enabled).filter(([key, value]) => (key !== "base") && (value)).map(([key]) => ` plugin_${key}: yes`),
...Object.entries(this.plugins.options).filter(([key, value]) => value).filter(([key, value]) => this.plugins.enabled[key.split(".")[0]]).map(([key, value]) =>
` plugin_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? { true: "yes", false: "no" }[value] : value}`
` plugin_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`
),
...Object.entries(this.config).filter(([key, value]) => (value) && (value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => ` config_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? { true: "yes", false: "no" }[value] : value}`),
...Object.entries(this.config).filter(([key, value]) => (value) && (value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => ` config_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`),
].sort(),
].join("\n")
},
@@ -276,7 +276,7 @@
methods: {
//Refresh computed properties
async refresh() {
const keys = { action: ["scopes", "action"], markdown: ["url", "embed"] }[this.tab]
const keys = {action: ["scopes", "action"], markdown: ["url", "embed"]}[this.tab]
if (keys) {
for (const key of keys)
this._computedWatchers[key]?.run()
@@ -284,7 +284,7 @@
}
},
//Load and render placeholder image
async mock({ timeout = 600 } = {}) {
async mock({timeout = 600} = {}) {
this.refresh()
clearTimeout(this.templates.placeholder.timeout)
this.templates.placeholder.timeout = setTimeout(async () => {
@@ -315,12 +315,12 @@
this.generated.error = null
}
catch (error) {
this.generated.error = { code: error.response.status, message: error.response.data }
this.generated.error = {code: error.response.status, message: error.response.data}
}
finally {
this.generated.pending = false
try {
const { data: requests } = await axios.get("/.requests")
const {data: requests} = await axios.get("/.requests")
this.requests = requests
}
catch {}

View File

@@ -1,4 +1,4 @@
;(function({ axios, faker, ejs } = { axios: globalThis.axios, faker: globalThis.faker, ejs: globalThis.ejs }) {
;(function({axios, faker, ejs} = {axios: globalThis.axios, faker: globalThis.faker, ejs: globalThis.ejs}) {
//Load assets
const cached = new Map()
async function load(url) {
@@ -31,7 +31,7 @@
//Placeholder function
globalThis.placeholder = async function(set) {
//Load templates informations
let { image, style, fonts, partials } = await load(`/.templates/${set.templates.selected}`)
let {image, style, fonts, partials} = await load(`/.templates/${set.templates.selected}`)
await Promise.all(partials.map(async partial => await load(`/.templates/${set.templates.selected}/partials/${escape(partial)}.ejs`)))
//Trap includes
image = image.replace(/<%-\s*await include[(](`.*?[.]ejs`)[)]\s*%>/g, (m, g) => `<%- await $include(${g}) %>`)
@@ -45,11 +45,11 @@
partials: new Set([...(set.config.order || "").split(",").map(x => x.trim()).filter(x => partials.includes(x)), ...partials]),
//Plural helper
s(value, end = "") {
return value !== 1 ? { y: "ies", "": "s" }[end] : end
return value !== 1 ? {y: "ies", "": "s"}[end] : end
},
//Formatter helper
f(n, { sign = false } = {}) {
for (const { u, v } of [{ u: "b", v: 10 ** 9 }, { u: "m", v: 10 ** 6 }, { u: "k", v: 10 ** 3 }]) {
f(n, {sign = false} = {}) {
for (const {u, v} of [{u: "b", v: 10 ** 9}, {u: "m", v: 10 ** 6}, {u: "k", v: 10 ** 3}]) {
if (n / v >= 1)
return `${(sign) && (n > 0) ? "+" : ""}${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
}
@@ -58,10 +58,10 @@
//Trap for includes
async $include(path) {
const partial = await load(`/.templates/${set.templates.selected}/${escape(path)}`)
return await ejs.render(partial, data, { async: true, rmWhitespace: true })
return await ejs.render(partial, data, {async: true, rmWhitespace: true})
},
//Meta-data
meta: { version: set.version, author: "lowlighter", generated: new Date().toGMTString().replace(/GMT$/g, "").trim() },
meta: {version: set.version, author: "lowlighter", generated: new Date().toGMTString().replace(/GMT$/g, "").trim()},
//Animated
animated: false,
//Display size
@@ -70,30 +70,30 @@
//Config
config: set.config,
//Extras
extras: { css: options["extras.css"] ?? "" },
extras: {css: options["extras.css"] ?? ""},
//Base elements
base: set.plugins.enabled.base,
//Computed elements
computed: {
commits: faker.datatype.number(10000),
sponsorships: faker.datatype.number(10),
licenses: { favorite: [""], used: { MIT: 1 }, about: {} },
token: { scopes: [] },
licenses: {favorite: [""], used: {MIT: 1}, about: {}},
token: {scopes: []},
repositories: {
watchers: faker.datatype.number(1000),
stargazers: faker.datatype.number(10000),
issues_open: faker.datatype.number(1000),
issues_closed: faker.datatype.number(1000),
pr_open: faker.datatype.number(1000),
pr_closed: { totalCount: faker.datatype.number(100) },
pr_closed: {totalCount: faker.datatype.number(100)},
pr_merged: faker.datatype.number(1000),
forks: faker.datatype.number(1000),
releases: faker.datatype.number(1000),
},
diskUsage: `${faker.datatype.float({ min: 1, max: 999 }).toFixed(1)}MB`,
registration: `${faker.datatype.number({ min: 2, max: 10 })} years ago`,
diskUsage: `${faker.datatype.float({min: 1, max: 999}).toFixed(1)}MB`,
registration: `${faker.datatype.number({min: 2, max: 10})} years ago`,
cakeday: false,
calendar: new Array(14).fill(null).map(_ => ({ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) })),
calendar: new Array(14).fill(null).map(_ => ({color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])})),
avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
},
//User data
@@ -107,12 +107,12 @@
websiteUrl: options["pagespeed.url"] || "(attached website)",
isHireable: false,
twitterUsername: options["tweets.user"] || "(attached Twitter account)",
repositories: { totalCount: faker.datatype.number(100), totalDiskUsage: faker.datatype.number(100000), nodes: [] },
packages: { totalCount: faker.datatype.number(10) },
starredRepositories: { totalCount: faker.datatype.number(1000) },
watching: { totalCount: faker.datatype.number(100) },
sponsorshipsAsSponsor: { totalCount: faker.datatype.number(10) },
sponsorshipsAsMaintainer: { totalCount: faker.datatype.number(10) },
repositories: {totalCount: faker.datatype.number(100), totalDiskUsage: faker.datatype.number(100000), nodes: []},
packages: {totalCount: faker.datatype.number(10)},
starredRepositories: {totalCount: faker.datatype.number(1000)},
watching: {totalCount: faker.datatype.number(100)},
sponsorshipsAsSponsor: {totalCount: faker.datatype.number(10)},
sponsorshipsAsMaintainer: {totalCount: faker.datatype.number(10)},
contributionsCollection: {
totalRepositoriesWithContributedCommits: faker.datatype.number(100),
totalCommitContributions: faker.datatype.number(10000),
@@ -121,12 +121,12 @@
totalPullRequestContributions: faker.datatype.number(1000),
totalPullRequestReviewContributions: faker.datatype.number(1000),
},
calendar: { contributionCalendar: { weeks: [] } },
repositoriesContributedTo: { totalCount: faker.datatype.number(100) },
followers: { totalCount: faker.datatype.number(1000) },
following: { totalCount: faker.datatype.number(1000) },
issueComments: { totalCount: faker.datatype.number(1000) },
organizations: { totalCount: faker.datatype.number(10) },
calendar: {contributionCalendar: {weeks: []}},
repositoriesContributedTo: {totalCount: faker.datatype.number(100)},
followers: {totalCount: faker.datatype.number(1000)},
following: {totalCount: faker.datatype.number(1000)},
issueComments: {totalCount: faker.datatype.number(1000)},
organizations: {totalCount: faker.datatype.number(10)},
},
//Plugins
plugins: {
@@ -148,7 +148,7 @@
id: faker.datatype.number(100000000000000).toString(),
created_at: faker.date.recent(),
entities: {
mentions: [{ start: 22, end: 33, username: "lowlighter" }],
mentions: [{start: 22, end: 33, username: "lowlighter"}],
},
text: 'Checkout metrics from <span class="mention">@lowlighter</span> ! <span class="hashtag">#GitHub</span> ',
mentions: ["lowlighter"],
@@ -177,7 +177,7 @@
? ({
traffic: {
views: {
count: `${faker.datatype.number({ min: 10, max: 100 })}.${faker.datatype.number(9)}k`,
count: `${faker.datatype.number({min: 10, max: 100})}.${faker.datatype.number(9)}k`,
uniques: `${faker.datatype.number(10)}.${faker.datatype.number(9)}k`,
},
},
@@ -264,7 +264,7 @@
? {
user: {
commits: faker.datatype.number(100),
percentage: faker.datatype.float({ max: 1 }),
percentage: faker.datatype.float({max: 1}),
maintainer: false,
stars: faker.datatype.number(100),
},
@@ -365,7 +365,7 @@
unlock: null,
text: faker.lorem.sentence(),
get icon() {
const colors = { S: ["#FF0000", "#FF8500"], A: ["#B59151", "#FFD576"], B: ["#7D6CFF", "#B2A8FF"], C: ["#2088FF", "#79B8FF"], $: ["#FF48BD", "#FF92D8"], X: ["#7A7A7A", "#B0B0B0"] }
const colors = {S: ["#FF0000", "#FF8500"], A: ["#B59151", "#FFD576"], B: ["#7D6CFF", "#B2A8FF"], C: ["#2088FF", "#79B8FF"], $: ["#FF48BD", "#FF92D8"], X: ["#7A7A7A", "#B0B0B0"]}
return `<g xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke-linejoin="round"><g stroke="#secondary"><path d="M8 43a3 3 0 100 6 3 3 0 000-6zm40 0a3.001 3.001 0 10.002 6.002A3.001 3.001 0 0048 43zm-18 3h-4.971m-11.045 0H11M45 46h-4"/></g><path stroke="#primary" d="M13 51h28M36.992 45.276l6.375-8.017c1.488.63 3.272.29 4.414-.977a3.883 3.883 0 00.658-4.193l-1.96 2.174-1.936-.151-.406-1.955 1.96-2.173a3.898 3.898 0 00-4.107 1.092 3.886 3.886 0 00-.512 4.485l-7.317 7.169c-1.32 1.314-.807 2.59-.236 3.105.67.601 1.888.845 3.067-.56z"/><g stroke="#primary"><path d="M12.652 31.063l9.442 12.578a.512.512 0 01-.087.716l-2.396 1.805a.512.512 0 01-.712-.114L9.46 33.47l-.176-3.557 3.37 1.15zM17.099 43.115l2.395-1.806"/></g></g><path d="M25.68 36.927v-2.54a2.227 2.227 0 01.37-1.265c-.526-.04-3.84-.371-3.84-4.302 0-1.013.305-1.839.915-2.477a4.989 4.989 0 01-.146-1.86c.087-.882.946-.823 2.577.178 1.277-.47 2.852-.47 4.725 0 .248-.303 2.434-1.704 2.658-.268.047.296.016.946-.093 1.95.516.524.776 1.358.78 2.501.007 2.261-1.26 3.687-3.8 4.278.24.436.355.857.346 1.264a117.57 117.57 0 000 2.614c2.43-.744 4.228-2.06 5.395-3.95.837-1.356 1.433-2.932 1.433-4.865 0-2.886-1.175-4.984-2.5-6.388C32.714 19.903 30.266 19 28 19a9.094 9.094 0 00-6.588 2.897C20.028 23.393 19 25.507 19 28.185c0 2.026.701 3.945 1.773 5.38 1.228 1.643 2.864 2.764 4.907 3.362zM52.98 25.002l-3.07 3.065-1.49-1.485M6.98 25.002l-3.07 3.065-1.49-1.485" stroke="#primary" stroke-linejoin="round"/><path d="M19.001 11V9a2 2 0 012-2h14a2 2 0 012 2v2m-21 12.028v-10.03a2 2 0 012-1.998h20a2 2 0 012 2v10.028" stroke="#secondary" stroke-linejoin="round"/><path stroke="#secondary" d="M28.001 7V3M15.039 7.797c-5.297 3.406-9.168 8.837-10.517 15.2m46.737-.936c-1.514-5.949-5.25-11.01-10.273-14.248"/></g>`
.replace(/#primary/g, colors[this.rank][0])
.replace(/#secondary/g, colors[this.rank][1])
@@ -374,9 +374,9 @@
progress: faker.datatype.number(100) / 100,
value: faker.datatype.number(1000),
}))
.filter(({ rank }) => options["achievements.secrets"] ? true : rank !== "$")
.filter(({ rank }) => ({ S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[rank] >= { S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[options["achievements.threshold"]]))
.sort((a, b) => ({ S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[b.rank] + b.progress * 0.99) - ({ S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[a.rank] + a.progress * 0.99))
.filter(({rank}) => options["achievements.secrets"] ? true : rank !== "$")
.filter(({rank}) => ({S: 5, A: 4, B: 3, C: 2, $: 1, X: 0}[rank] >= {S: 5, A: 4, B: 3, C: 2, $: 1, X: 0}[options["achievements.threshold"]]))
.sort((a, b) => ({S: 5, A: 4, B: 3, C: 2, $: 1, X: 0}[b.rank] + b.progress * 0.99) - ({S: 5, A: 4, B: 3, C: 2, $: 1, X: 0}[a.rank] + a.progress * 0.99))
.slice(0, options["achievements.limit"] || Infinity),
},
})
@@ -437,26 +437,26 @@
sections: options["languages.sections"].split(", ").map(x => x.trim()).filter(x => /^(most-used|recently-used)$/.test(x)),
details: options["languages.details"].split(",").map(x => x.trim()).filter(x => x),
get colors() {
return Object.fromEntries(Object.entries(this.favorites).map(([key, { color }]) => [key, color]))
return Object.fromEntries(Object.entries(this.favorites).map(([key, {color}]) => [key, color]))
},
total: faker.datatype.number(10000),
get stats() {
return Object.fromEntries(Object.entries(this.favorites).map(([key, { value }]) => [key, value]))
return Object.fromEntries(Object.entries(this.favorites).map(([key, {value}]) => [key, value]))
},
["stats.recent"]: {
total: faker.datatype.number(10000),
get lines() {
return Object.fromEntries(Object.entries(this.favorites).map(([key, { value }]) => [key, value]))
return Object.fromEntries(Object.entries(this.favorites).map(([key, {value}]) => [key, value]))
},
get stats() {
return Object.fromEntries(Object.entries(this.favorites).map(([key, { value }]) => [key, value]))
return Object.fromEntries(Object.entries(this.favorites).map(([key, {value}]) => [key, value]))
},
commits: faker.datatype.number(500),
files: faker.datatype.number(1000),
days: Number(options["languages.recent.days"]),
},
favorites: distribution(7).map((value, index, array) => ({ name: faker.lorem.word(), color: faker.internet.color(), value, size: faker.datatype.number(1000000), x: array.slice(0, index).reduce((a, b) => a + b, 0) })),
recent: distribution(7).map((value, index, array) => ({ name: faker.lorem.word(), color: faker.internet.color(), value, size: faker.datatype.number(1000000), x: array.slice(0, index).reduce((a, b) => a + b, 0) })),
favorites: distribution(7).map((value, index, array) => ({name: faker.lorem.word(), color: faker.internet.color(), value, size: faker.datatype.number(1000000), x: array.slice(0, index).reduce((a, b) => a + b, 0)})),
recent: distribution(7).map((value, index, array) => ({name: faker.lorem.word(), color: faker.internet.color(), value, size: faker.datatype.number(1000000), x: array.slice(0, index).reduce((a, b) => a + b, 0)})),
},
})
: null),
@@ -536,7 +536,7 @@
},
},
},
indents: { style: "spaces", spaces: 1, tabs: 0 },
indents: {style: "spaces", spaces: 1, tabs: 0},
linguist: {
available: true,
get ordered() {
@@ -554,7 +554,7 @@
? ({
get people() {
const types = options["people.types"].split(",").map(x => x.trim())
.map(x => ({ followed: "following", sponsors: "sponsorshipsAsMaintainer", sponsored: "sponsorshipsAsSponsor", sponsoring: "sponsorshipsAsSponsor" })[x] ?? x)
.map(x => ({followed: "following", sponsors: "sponsorshipsAsMaintainer", sponsored: "sponsorshipsAsSponsor", sponsoring: "sponsorshipsAsSponsor"})[x] ?? x)
.filter(x => ["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor"].includes(x))
return {
types,
@@ -598,8 +598,8 @@
data: new Array(12).fill(null).map(_ => ({
timeUTCHumanReadable: `${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`,
color: faker.random.arrayElement(["#9be9a8", "#40c463", "#30a14e", "#216e39"]),
sgv: faker.datatype.number({ min: 40, max: 400 }),
delta: faker.datatype.number({ min: -10, max: 10 }),
sgv: faker.datatype.number({min: 40, max: 400}),
delta: faker.datatype.number({min: -10, max: 10}),
direction: faker.random.arrayElement(["SingleUp", "DoubleUp", "FortyFiveUp", "Flat", "FortyFiveDown", "SingleDown", "DoubleDown"]),
alert: faker.random.arrayElement(["Normal", "Urgent High", "Urgent Low", "High", "Low"]),
arrowHumanReadable: faker.random.arrayElement(["↑↑", "↑", "↗", "→", "↘", "↓", "↓↓"]),
@@ -611,9 +611,9 @@
...(set.plugins.enabled.fortune
? ({
fortune: faker.random.arrayElement([
{ chance: .06, color: "#43FD3B", text: "Good news will come to you by mail" },
{ chance: .06, color: "#00CBB0", text: "キタ━━━━━━(゚∀゚)━━━━━━ !!!!" },
{ chance: 0.03, color: "#FD4D32", text: "Excellent Luck" },
{chance: .06, color: "#43FD3B", text: "Good news will come to you by mail"},
{chance: .06, color: "#00CBB0", text: "キタ━━━━━━(゚∀゚)━━━━━━ !!!!"},
{chance: 0.03, color: "#FD4D32", text: "Excellent Luck"},
]),
})
: null),
@@ -624,10 +624,10 @@
url: options["pagespeed.url"] || "(attached website url)",
detailed: options["pagespeed.detailed"] || false,
scores: [
{ score: faker.datatype.float({ max: 1 }), title: "Performance" },
{ score: faker.datatype.float({ max: 1 }), title: "Accessibility" },
{ score: faker.datatype.float({ max: 1 }), title: "Best Practices" },
{ score: faker.datatype.float({ max: 1 }), title: "SEO" },
{score: faker.datatype.float({max: 1}), title: "Performance"},
{score: faker.datatype.float({max: 1}), title: "Accessibility"},
{score: faker.datatype.float({max: 1}), title: "Best Practices"},
{score: faker.datatype.float({max: 1}), title: "SEO"},
],
metrics: {
observedFirstContentfulPaint: faker.datatype.number(500),
@@ -639,12 +639,12 @@
maxPotentialFID: faker.datatype.number(500),
observedLoad: faker.datatype.number(500),
firstMeaningfulPaint: faker.datatype.number(500),
observedCumulativeLayoutShift: faker.datatype.float({ max: 1 }),
observedCumulativeLayoutShift: faker.datatype.float({max: 1}),
observedSpeedIndex: faker.datatype.number(1000),
observedSpeedIndexTs: faker.time.recent(),
observedTimeOriginTs: faker.time.recent(),
observedLargestContentfulPaint: faker.datatype.number(1000),
cumulativeLayoutShift: faker.datatype.float({ max: 1 }),
cumulativeLayoutShift: faker.datatype.float({max: 1}),
observedFirstPaintTs: faker.time.recent(),
observedTraceEndTs: faker.time.recent(),
largestContentfulPaint: faker.datatype.number(2000),
@@ -698,14 +698,14 @@
? ({
discussions: {
categories: {
stats: { "🙏 Q&A": faker.datatype.number(100), "📣 Announcements": faker.datatype.number(100), "💡 Ideas": faker.datatype.number(100), "💬 General": faker.datatype.number(100) },
stats: {"🙏 Q&A": faker.datatype.number(100), "📣 Announcements": faker.datatype.number(100), "💡 Ideas": faker.datatype.number(100), "💬 General": faker.datatype.number(100)},
favorite: "📣 Announcements",
},
upvotes: { discussions: faker.datatype.number(1000), comments: faker.datatype.number(1000) },
upvotes: {discussions: faker.datatype.number(1000), comments: faker.datatype.number(1000)},
started: faker.datatype.number(1000),
comments: faker.datatype.number(1000),
answers: faker.datatype.number(1000),
display: { categories: options["discussions.categories"] ? { limit: options["discussions.categories.limit"] || Infinity } : null },
display: {categories: options["discussions.categories"] ? {limit: options["discussions.categories.limit"] || Infinity} : null},
},
})
: null),
@@ -730,7 +730,7 @@
? ({
topics: {
mode: options["topics.mode"],
type: { starred: "labels", labels: "labels", mastered: "icons", icons: "icons" }[options["topics.mode"]] || "labels",
type: {starred: "labels", labels: "labels", mastered: "icons", icons: "icons"}[options["topics.mode"]] || "labels",
list: new Array(Number(options["topics.limit"]) || 20).fill(null).map(_ => ({
name: faker.lorem.words(2),
description: faker.lorem.sentence(),
@@ -759,8 +759,8 @@
totalCount: faker.datatype.number(100),
},
stargazerCount: faker.datatype.number(10000),
licenseInfo: { nickname: null, name: "MIT License" },
primaryLanguage: { color: "#f1e05a", name: "JavaScript" },
licenseInfo: {nickname: null, name: "MIT License"},
primaryLanguage: {color: "#f1e05a", name: "JavaScript"},
},
starred: "1 day ago",
},
@@ -779,8 +779,8 @@
totalCount: faker.datatype.number(100),
},
stargazerCount: faker.datatype.number(10000),
licenseInfo: { nickname: null, name: "License" },
primaryLanguage: { color: faker.internet.color(), name: faker.lorem.word() },
licenseInfo: {nickname: null, name: "License"},
primaryLanguage: {color: faker.internet.color(), name: faker.lorem.word()},
},
starred: `${i + 2} days ago`,
})),
@@ -825,8 +825,8 @@
totalCount: faker.datatype.number(100),
},
stargazerCount: faker.datatype.number(10000),
licenseInfo: { nickname: null, name: "License" },
primaryLanguage: { color: faker.internet.color(), name: faker.lorem.word() },
licenseInfo: {nickname: null, name: "License"},
primaryLanguage: {color: faker.internet.color(), name: faker.lorem.word()},
})),
},
})
@@ -861,7 +861,7 @@
for (let d = -14; d <= 0; d++) {
const date = new Date(Date.now() - d * 24 * 60 * 60 * 1000).toISOString().substring(0, 10)
dates.push(date)
result.total.dates[date] = (total += (result.increments.dates[date] = faker.datatype.number(100)))
result.total.dates[date] = total += result.increments.dates[date] = faker.datatype.number(100)
}
return result
},
@@ -884,13 +884,13 @@
percents -= result.percent
result.percent /= 100
}
results.filter(({ name }) => elements.includes(name) ? false : (elements.push(name), true))
results.filter(({name}) => elements.includes(name) ? false : (elements.push(name), true))
return results.sort((a, b) => b.percent - a.percent)
}
return {
sections: options["wakatime.sections"].split(",").map(x => x.trim()).filter(x => x),
days: Number(options["wakatime.days"]) || 7,
time: { total: faker.datatype.number(100000), daily: faker.datatype.number(24) },
time: {total: faker.datatype.number(100000), daily: faker.datatype.number(24)},
editors: stats(["VS Code", "Chrome", "IntelliJ", "PhpStorm", "WebStorm", "Android Studio", "Visual Studio", "Sublime Text", "PyCharm", "Vim", "Atom", "Xcode"]),
languages: stats(["JavaScript", "TypeScript", "PHP", "Java", "Python", "Vue.js", "HTML", "C#", "JSON", "Dart", "SCSS", "Kotlin", "JSX", "Go", "Ruby", "YAML"]),
projects: stats(),
@@ -909,16 +909,16 @@
count: faker.datatype.number(1000),
minutesWatched: faker.datatype.number(100000),
episodesWatched: faker.datatype.number(10000),
genres: new Array(4).fill(null).map(_ => ({ genre: faker.lorem.word() })),
genres: new Array(4).fill(null).map(_ => ({genre: faker.lorem.word()})),
},
manga: {
count: faker.datatype.number(1000),
chaptersRead: faker.datatype.number(100000),
volumesRead: faker.datatype.number(10000),
genres: new Array(4).fill(null).map(_ => ({ genre: faker.lorem.word() })),
genres: new Array(4).fill(null).map(_ => ({genre: faker.lorem.word()})),
},
},
genres: new Array(4).fill(null).map(_ => ({ genre: faker.lorem.word() })),
genres: new Array(4).fill(null).map(_ => ({genre: faker.lorem.word()})),
},
get lists() {
const media = type => ({
@@ -929,7 +929,7 @@
genres: new Array(6).fill(null).map(_ => faker.lorem.word()),
progress: faker.datatype.number(100),
description: faker.lorem.paragraphs(),
scores: { user: faker.datatype.number(100), community: faker.datatype.number(100) },
scores: {user: faker.datatype.number(100), community: faker.datatype.number(100)},
released: 100 + faker.datatype.number(1000),
artwork: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
})
@@ -939,16 +939,16 @@
...(medias.includes("anime")
? {
anime: {
...(sections.includes("watching") ? { watching: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("ANIME")) } : {}),
...(sections.includes("favorites") ? { favorites: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("ANIME")) } : {}),
...(sections.includes("watching") ? {watching: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("ANIME"))} : {}),
...(sections.includes("favorites") ? {favorites: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("ANIME"))} : {}),
},
}
: {}),
...(medias.includes("manga")
? {
manga: {
...(sections.includes("reading") ? { reading: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("MANGA")) } : {}),
...(sections.includes("favorites") ? { favorites: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("MANGA")) } : {}),
...(sections.includes("reading") ? {reading: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("MANGA"))} : {}),
...(sections.includes("favorites") ? {favorites: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("MANGA"))} : {}),
},
}
: {}),
@@ -974,7 +974,7 @@
repo: `${faker.random.word()}/${faker.random.word()}`,
size: 1,
branch: "master",
commits: [{ sha: faker.git.shortSha(), message: faker.lorem.sentence() }],
commits: [{sha: faker.git.shortSha(), message: faker.lorem.sentence()}],
timestamp: faker.date.recent(),
},
{
@@ -1026,8 +1026,8 @@
user: set.user,
number: faker.datatype.number(100),
title: faker.lorem.sentence(),
lines: { added: faker.datatype.number(1000), deleted: faker.datatype.number(1000) },
files: { changed: faker.datatype.number(10) },
lines: {added: faker.datatype.number(1000), deleted: faker.datatype.number(1000)},
files: {changed: faker.datatype.number(10)},
timestamp: faker.date.recent(),
},
{
@@ -1061,13 +1061,13 @@
{
type: "ref/create",
repo: `${faker.random.word()}/${faker.random.word()}`,
ref: { name: faker.lorem.slug(), type: faker.random.arrayElement(["tag", "branch"]) },
ref: {name: faker.lorem.slug(), type: faker.random.arrayElement(["tag", "branch"])},
timestamp: faker.date.recent(),
},
{
type: "ref/delete",
repo: `${faker.random.word()}/${faker.random.word()}`,
ref: { name: faker.lorem.slug(), type: faker.random.arrayElement(["tag", "branch"]) },
ref: {name: faker.lorem.slug(), type: faker.random.arrayElement(["tag", "branch"])},
timestamp: faker.date.recent(),
},
{
@@ -1096,7 +1096,7 @@
...(set.plugins.enabled.isocalendar
? ({
isocalendar: {
streak: { max: 30 + faker.datatype.number(20), current: faker.datatype.number(30) },
streak: {max: 30 + faker.datatype.number(20), current: faker.datatype.number(30)},
max: 10 + faker.datatype.number(40),
average: faker.datatype.float(10),
svg: await staticPlaceholder(set.plugins.enabled.isocalendar, `isocalendar.${options["isocalendar.duration"]}.svg`),
@@ -1108,8 +1108,8 @@
...(set.plugins.enabled.support
? ({
support: {
stats: { solutions: faker.datatype.number(100), posts: faker.datatype.number(1000), topics: faker.datatype.number(1000), received: faker.datatype.number(1000), hearts: faker.datatype.number(1000) },
badges: { uniques: [], multiples: [], count: faker.datatype.number(1000) },
stats: {solutions: faker.datatype.number(100), posts: faker.datatype.number(1000), topics: faker.datatype.number(1000), received: faker.datatype.number(1000), hearts: faker.datatype.number(1000)},
badges: {uniques: [], multiples: [], count: faker.datatype.number(1000)},
},
})
: null),
@@ -1200,20 +1200,20 @@
}
//Formatters
data.f.bytes = function(n) {
for (const { u, v } of [{ u: "E", v: 10 ** 18 }, { u: "P", v: 10 ** 15 }, { u: "T", v: 10 ** 12 }, { u: "G", v: 10 ** 9 }, { u: "M", v: 10 ** 6 }, { u: "k", v: 10 ** 3 }]) {
for (const {u, v} of [{u: "E", v: 10 ** 18}, {u: "P", v: 10 ** 15}, {u: "T", v: 10 ** 12}, {u: "G", v: 10 ** 9}, {u: "M", v: 10 ** 6}, {u: "k", v: 10 ** 3}]) {
if (n / v >= 1)
return `${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B`
}
return `${n} byte${n > 1 ? "s" : ""}`
}
data.f.percentage = function(n, { rescale = true } = {}) {
data.f.percentage = function(n, {rescale = true} = {}) {
return `${
(n * (rescale ? 100 : 1)).toFixed(2)
.replace(/[.]([1-9]*)(0+)$/, (m, a, b) => `.${a}`)
.replace(/[.]$/, "")
}%`
}
data.f.ellipsis = function(text, { length = 20 } = {}) {
data.f.ellipsis = function(text, {length = 20} = {}) {
text = `${text}`
if (text.length < length)
return text
@@ -1222,11 +1222,11 @@
data.f.date = function(string, options) {
if (options.date) {
delete options.date
Object.assign(options, { day: "numeric", month: "short", year: "numeric" })
Object.assign(options, {day: "numeric", month: "short", year: "numeric"})
}
if (options.time) {
delete options.time
Object.assign(options, { hour: "2-digit", minute: "2-digit", second: "2-digit" })
Object.assign(options, {hour: "2-digit", minute: "2-digit", second: "2-digit"})
}
return new Intl.DateTimeFormat("en-GB", options).format(new Date(string))
}
@@ -1234,7 +1234,7 @@
return text?.name ?? text
}
//Render
return await ejs.render(image, data, { async: true, rmWhitespace: true })
return await ejs.render(image, data, {async: true, rmWhitespace: true})
}
//Reset globals contexts
globalThis.placeholder.init = function(globals) {