Code formatting (#280)

This commit is contained in:
Simon Lecoq
2021-04-29 14:32:34 +02:00
committed by GitHub
parent 92090b60b5
commit ce18af8573
98 changed files with 10278 additions and 9807 deletions

View File

@@ -1,433 +1,468 @@
//Imports
import core from "@actions/core"
import github from "@actions/github"
import octokit from "@octokit/graphql"
import setup from "../metrics/setup.mjs"
import mocks from "../mocks/index.mjs"
import metrics from "../metrics/index.mjs"
import fs from "fs/promises"
import paths from "path"
import sgit from "simple-git"
process.on("unhandledRejection", error => {
throw error
})
import core from "@actions/core"
import github from "@actions/github"
import octokit from "@octokit/graphql"
import fs from "fs/promises"
import paths from "path"
import sgit from "simple-git"
import metrics from "../metrics/index.mjs"
import setup from "../metrics/setup.mjs"
import mocks from "../mocks/index.mjs"
process.on("unhandledRejection", error => {
throw error
})
//Debug message buffer
let DEBUG = true
const debugged = []
let DEBUG = true
const debugged = []
//Info logger
const info = (left, right, {token = false} = {}) => console.log(`${`${left}`.padEnd(56 + 9*(/0m$/.test(left)))}${
Array.isArray(right) ? right.join(", ") || "(none)" :
right === undefined ? "(default)" :
token ? /^MOCKED/.test(right) ? "(MOCKED TOKEN)" : /^NOT_NEEDED$/.test(right) ? "(NOT NEEDED)" : (right ? "(provided)" : "(missing)") :
typeof right === "object" ? JSON.stringify(right) :
right
const info = (left, right, {token = false} = {}) => console.log(`${`${left}`.padEnd(56 + 9 * (/0m$/.test(left)))}${
Array.isArray(right)
? right.join(", ") || "(none)"
: right === undefined
? "(default)"
: token
? /^MOCKED/.test(right) ? "(MOCKED TOKEN)" : /^NOT_NEEDED$/.test(right) ? "(NOT NEEDED)" : (right ? "(provided)" : "(missing)")
: typeof right === "object"
? JSON.stringify(right)
: right
}`)
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 ?? input, value, {token:metadata.plugins[name]?.inputs[input]?.type === "token"})
}
info.break = () => console.log("─".repeat(88))
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 ?? input, value, {token:metadata.plugins[name]?.inputs[input]?.type === "token"})
}
info.break = () => console.log("─".repeat(88))
//Waiter
async function wait(seconds) {
await new Promise(solve => setTimeout(solve, seconds*1000))
}
async function wait(seconds) {
await new Promise(solve => setTimeout(solve, seconds * 1000))
}
//Runner
(async function() {
(async function() {
try {
//Initialization
info.break()
info.section("Metrics")
//Skip process if needed
if ((github.context.eventName === "push") && (github.context.payload?.head_commit)) {
if (/\[Skip GitHub Action\]/.test(github.context.payload.head_commit.message)) {
console.log("Skipped because [Skip GitHub Action] is in commit message")
process.exit(0)
}
if (/Auto-generated metrics for run #\d+/.test(github.context.payload.head_commit.message)) {
console.log("Skipped because this seems to be an automated pull request merge")
process.exit(0)
}
}
//Load configuration
const {conf, Plugins, Templates} = await setup({log:false, nosettings:true, community:{templates:core.getInput("setup_community_templates")}})
const {metadata} = conf
conf.settings.extras = {default:true}
info("Setup", "complete")
info("Version", conf.package.version)
//Core inputs
const {
user:_user,
repo:_repo,
token,
template,
query,
"setup.community.templates":_templates,
filename:_filename,
optimize,
verify,
"markdown.cache":_markdown_cache,
debug,
"debug.flags":dflags,
"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,
retries,
"retries.delay":retries_delay,
"output.action":_action,
...config
} = metadata.plugins.core.inputs.action({core})
const q = {...query, ...(_repo ? {repo:_repo} : null), template}
const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].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"}[_output] ?? _output)
//Docker image
if (_image)
info("Using prebuilt image", _image)
//Debug mode and flags
info("Debug mode", debug)
if (!debug) {
console.debug = message => debugged.push(message)
DEBUG = false
}
info("Debug flags", dflags)
q["debug.flags"] = dflags.join(" ")
//Token for data gathering
info("GitHub token", token, {token:true})
if (!token)
throw new Error("You must provide a valid GitHub personal token to gather your metrics (see https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/setup/action/setup.md for more informations)")
conf.settings.token = token
const api = {}
api.graphql = octokit.graphql.defaults({headers:{authorization:`token ${token}`}})
info("Github GraphQL API", "ok")
api.rest = github.getOctokit(token)
info("Github REST API", "ok")
//Apply mocking if needed
if (mocked) {
Object.assign(api, await mocks(api))
info("Use mocked API", true)
}
//Test token validity
else if (!/^NOT_NEEDED$/.test(token)) {
const {headers} = await api.rest.request("HEAD /")
if (!("x-oauth-scopes" in headers)) {
throw new Error(
'GitHub API did not send any "x-oauth-scopes" header back from provided "token". It means that your token may not be valid or you\'re using GITHUB_TOKEN which cannot be used since metrics will fetch data outside of this repository scope. Use a personal access token instead (see https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/setup/action/setup.md for more informations).',
)
}
info("Token validity", "seems ok")
}
//Extract octokits
const {graphql, rest} = api
//GitHub user
let authenticated
try {
authenticated = (await rest.users.getAuthenticated()).data.login
}
catch {
authenticated = github.context.repo.owner
}
const user = _user || authenticated
conf.authenticated = user
info("GitHub account", user)
if (q.repo)
info("GitHub repository", `${user}/${q.repo}`)
//Current repository
info("Current repository", `${github.context.repo.owner}/${github.context.repo.repo}`)
//Committer
const committer = {}
if (!dryrun) {
//Compute committer informations
committer.token = _token || token
committer.gist = _action === "gist" ? _gist : null
committer.commit = true
committer.message = _message.replace(/[$][{]filename[}]/g, filename)
committer.pr = /^pull-request/.test(_action)
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})
if (!committer.token)
throw new Error("You must provide a valid GitHub token to commit your metrics")
info("Committer branch", committer.branch)
info("Committer head branch", committer.head)
//Gist
if (committer.gist)
info("Committer Gist id", committer.gist)
//Instantiate API for committer
committer.rest = github.getOctokit(committer.token)
info("Committer REST API", "ok")
try {
//Initialization
info.break()
info.section("Metrics")
info("Committer account", (await committer.rest.users.getAuthenticated()).data.login)
}
catch {
info("Committer account", "(github-actions)")
}
//Create head branch if needed
try {
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}`})
info("Committer branch current sha", sha)
await committer.rest.git.createRef({...github.context.repo, ref:`refs/heads/${committer.head}`, sha})
info("Committer head branch status", "(created)")
}
else
throw error
//Skip process if needed
if ((github.context.eventName === "push")&&(github.context.payload?.head_commit)) {
if (/\[Skip GitHub Action\]/.test(github.context.payload.head_commit.message)) {
console.log("Skipped because [Skip GitHub Action] is in commit message")
process.exit(0)
}
if (/Auto-generated metrics for run #\d+/.test(github.context.payload.head_commit.message)) {
console.log("Skipped because this seems to be an automated pull request merge")
process.exit(0)
}
}
//Load configuration
const {conf, Plugins, Templates} = await setup({log:false, nosettings:true, community:{templates:core.getInput("setup_community_templates")}})
const {metadata} = conf
conf.settings.extras = {default:true}
info("Setup", "complete")
info("Version", conf.package.version)
//Core inputs
const {
user:_user, repo:_repo, token,
template, query, "setup.community.templates":_templates,
filename:_filename, optimize, verify, "markdown.cache":_markdown_cache,
debug, "debug.flags":dflags, "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,
retries, "retries.delay":retries_delay,
"output.action":_action,
...config
} = metadata.plugins.core.inputs.action({core})
const q = {...query, ...(_repo ? {repo:_repo} : null), template}
const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].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"}[_output] ?? _output)
//Docker image
if (_image)
info("Using prebuilt image", _image)
//Debug mode and flags
info("Debug mode", debug)
if (!debug) {
console.debug = message => debugged.push(message)
DEBUG = false
}
info("Debug flags", dflags)
q["debug.flags"] = dflags.join(" ")
//Token for data gathering
info("GitHub token", token, {token:true})
if (!token)
throw new Error("You must provide a valid GitHub personal token to gather your metrics (see https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/setup/action/setup.md for more informations)")
conf.settings.token = token
const api = {}
api.graphql = octokit.graphql.defaults({headers:{authorization:`token ${token}`}})
info("Github GraphQL API", "ok")
api.rest = github.getOctokit(token)
info("Github REST API", "ok")
//Apply mocking if needed
if (mocked) {
Object.assign(api, await mocks(api))
info("Use mocked API", true)
}
//Test token validity
else if (!/^NOT_NEEDED$/.test(token)) {
const {headers} = await api.rest.request("HEAD /")
if (!("x-oauth-scopes" in headers))
throw new Error("GitHub API did not send any \"x-oauth-scopes\" header back from provided \"token\". It means that your token may not be valid or you're using GITHUB_TOKEN which cannot be used since metrics will fetch data outside of this repository scope. Use a personal access token instead (see https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/setup/action/setup.md for more informations).")
info("Token validity", "seems ok")
}
//Extract octokits
const {graphql, rest} = api
//GitHub user
let authenticated
try {
authenticated = (await rest.users.getAuthenticated()).data.login
}
catch {
authenticated = github.context.repo.owner
}
const user = _user || authenticated
conf.authenticated = user
info("GitHub account", user)
if (q.repo)
info("GitHub repository", `${user}/${q.repo}`)
//Current repository
info("Current repository", `${github.context.repo.owner}/${github.context.repo.repo}`)
//Committer
const committer = {}
if (!dryrun) {
//Compute committer informations
committer.token = _token || token
committer.gist = _action === "gist" ? _gist : null
committer.commit = true
committer.message = _message.replace(/[$][{]filename[}]/g, filename)
committer.pr = /^pull-request/.test(_action)
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})
if (!committer.token)
throw new Error("You must provide a valid GitHub token to commit your metrics")
info("Committer branch", committer.branch)
info("Committer head branch", committer.head)
//Gist
if (committer.gist)
info("Committer Gist id", committer.gist)
//Instantiate API for committer
committer.rest = github.getOctokit(committer.token)
info("Committer REST API", "ok")
try {
info("Committer account", (await committer.rest.users.getAuthenticated()).data.login)
}
catch {
info("Committer account", "(github-actions)")
}
//Create head branch if needed
try {
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}`})
info("Committer branch current sha", sha)
await committer.rest.git.createRef({...github.context.repo, ref:`refs/heads/${committer.head}`, sha})
info("Committer head branch status", "(created)")
}
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(`
}
//Retrieve previous render SHA to be able to update file content through API
committer.sha = null
try {
const {repository:{object:{oid}}} = await graphql(
`
query Sha {
repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
object(expression: "${committer.head}:${filename}") { ... on Blob { oid } }
}
}
`, {headers:{authorization:`token ${committer.token}`}})
committer.sha = oid
}
catch (error) {
console.debug(error)
}
info("Previous render sha", committer.sha ?? "(none)")
}
`,
{headers:{authorization:`token ${committer.token}`}},
)
committer.sha = oid
}
catch (error) {
console.debug(error)
}
info("Previous render sha", committer.sha ?? "(none)")
}
else
info("Dry-run", true)
//SVG file
conf.settings.optimize = optimize
info("SVG output", filename)
info("SVG optimization", optimize)
info("SVG verification after generation", verify)
//Template
info.break()
info.section("Templates")
info("Community templates", _templates)
info("Template used", template)
info("Query additional params", query)
//Core config
info.break()
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)
if (/markdown/.test(convert))
info("Markdown cache", _markdown_cache)
//Base content
info.break()
const {base:parts, ...base} = metadata.plugins.base.inputs.action({core})
info.group({metadata, name:"base", inputs:base})
info("Base sections", parts)
base.base = false
for (const part of conf.settings.plugins.base.parts)
base[`base.${part}`] = parts.includes(part)
Object.assign(q, base)
//Additional plugins
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})
plugins[name] = {enabled}
//Register user inputs
if (enabled) {
info.break()
info.group({metadata, name, inputs})
q[name] = true
for (const [key, value] of Object.entries(inputs)) {
//Store token in plugin configuration
if (metadata.plugins[name].inputs[key].type === "token")
plugins[name][key] = value
//Store value in query
else
info("Dry-run", true)
q[`${name}.${key}`] = value
}
}
}
//SVG file
conf.settings.optimize = optimize
info("SVG output", filename)
info("SVG optimization", optimize)
info("SVG verification after generation", verify)
//Render metrics
info.break()
info.section("Rendering")
let error = null, rendered = null
for (let attempt = 1; attempt <= retries; attempt++) {
try {
console.debug(`::group::Attempt ${attempt}/${retries}`)
;({rendered} = await metrics({login:user, q}, {graphql, rest, plugins, conf, die, verify, convert}, {Plugins, Templates}))
console.debug("::endgroup::")
break
}
catch (_error) {
error = _error
console.debug("::endgroup::")
console.debug(`::warning::rendering failed (${error.message})`)
await wait(retries_delay)
}
}
if (!rendered)
throw error ?? new Error("Could not render metrics")
info("Status", "complete")
//Template
info.break()
info.section("Templates")
info("Community templates", _templates)
info("Template used", template)
info("Query additional params", query)
//Save output to renders output folder
info.break()
info.section("Saving")
if (dryrun)
info("Actions to perform", "(none)")
else {
await fs.mkdir(paths.dirname(paths.join("/renders", filename)), {recursive:true})
await fs.writeFile(paths.join("/renders", filename), Buffer.from(rendered))
info(`Save to /metrics_renders/${filename}`, "ok")
}
//Core config
info.break()
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)
if (/markdown/.test(convert))
info("Markdown cache", _markdown_cache)
//Base content
info.break()
const {base:parts, ...base} = metadata.plugins.base.inputs.action({core})
info.group({metadata, name:"base", inputs:base})
info("Base sections", parts)
base.base = false
for (const part of conf.settings.plugins.base.parts)
base[`base.${part}`] = parts.includes(part)
Object.assign(q, base)
//Additional plugins
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})
plugins[name] = {enabled}
//Register user inputs
if (enabled) {
info.break()
info.group({metadata, name, inputs})
q[name] = true
for (const [key, value] of Object.entries(inputs)) {
//Store token in plugin configuration
if (metadata.plugins[name].inputs[key].type === "token")
plugins[name][key] = value
//Store value in query
else
q[`${name}.${key}`] = value
}
}
}
//Render metrics
info.break()
info.section("Rendering")
let error = null, rendered = null
for (let attempt = 1; attempt <= retries; attempt++) {
try {
console.debug(`::group::Attempt ${attempt}/${retries}`)
;({rendered} = await metrics({login:user, q}, {graphql, rest, plugins, conf, die, verify, convert}, {Plugins, Templates}))
console.debug("::endgroup::")
break
}
catch (_error) {
error = _error
console.debug("::endgroup::")
console.debug(`::warning::rendering failed (${error.message})`)
await wait(retries_delay)
}
}
if (!rendered)
throw error ?? new Error("Could not render metrics")
info("Status", "complete")
//Save output to renders output folder
info.break()
info.section("Saving")
if (dryrun)
info("Actions to perform", "(none)")
else {
await fs.mkdir(paths.dirname(paths.join("/renders", filename)), {recursive:true})
await fs.writeFile(paths.join("/renders", filename), Buffer.from(rendered))
info(`Save to /metrics_renders/${filename}`, "ok")
}
//Cache
if (/markdown/.test(convert)) {
const regex = /(?<match><img class="metrics-cachable" data-name="(?<name>[\s\S]+?)" src="data:image[/]svg[+]xml;base64,(?<content>[/+=\w]+)">)/g
let matched = null
while (matched = regex.exec(rendered)?.groups) { //eslint-disable-line no-cond-assign
const {match, name, content} = matched
let path = `${_markdown_cache}/${name}.svg`
console.debug(`Processing ${path}`)
let sha = null
try {
const {repository:{object:{oid}}} = await graphql(`
//Cache
if (/markdown/.test(convert)) {
const regex = /(?<match><img class="metrics-cachable" data-name="(?<name>[\s\S]+?)" src="data:image[/]svg[+]xml;base64,(?<content>[/+=\w]+)">)/g
let matched = null
while (matched = regex.exec(rendered)?.groups) { //eslint-disable-line no-cond-assign
const {match, name, content} = matched
let path = `${_markdown_cache}/${name}.svg`
console.debug(`Processing ${path}`)
let sha = null
try {
const {repository:{object:{oid}}} = await graphql(
`
query Sha {
repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
object(expression: "${committer.head}:${path}") { ... on Blob { oid } }
}
}
`, {headers:{authorization:`token ${committer.token}`}})
sha = oid
}
catch (error) {
console.debug(error)
}
finally {
await committer.rest.repos.createOrUpdateFileContents({
...github.context.repo, path, content,
message:`${committer.message} (cache)`, ...(sha ? {sha} : {}),
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")
}
}
}
//Check editions
if ((committer.commit)||(committer.pr)) {
const git = sgit()
const sha = await git.hashObject(paths.join("/renders", filename))
info("Current render sha", sha)
if (committer.sha === sha) {
info(`Commit to branch ${committer.branch}`, "(no changes)")
committer.commit = false
}
}
//Upload to gist (this is done as user since committer_token may not have gist rights)
if (committer.gist) {
await rest.gists.update({gist_id:committer.gist, files:{[filename]:{content:rendered}}})
info(`Upload to gist ${committer.gist}`, "ok")
committer.commit = false
}
//Commit metrics
if (committer.commit) {
await committer.rest.repos.createOrUpdateFileContents({
...github.context.repo, path:filename, message:committer.message,
content:Buffer.from(rendered).toString("base64"),
branch:committer.pr ? committer.head : committer.branch,
...(committer.sha ? {sha:committer.sha} : {}),
})
info(`Commit to branch ${committer.branch}`, "ok")
}
//Pull request
if (committer.pr) {
//Create pull request
let number = null
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}))
info(`Pull request from ${committer.head} to ${committer.branch}`, "(created)")
}
catch (error) {
console.debug(error)
//Check if pull request has already been created previously
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]")
if (prs.length < 1)
throw new Error("0 matching prs. Cannot proceed.")
if (prs.length > 1)
throw new Error(`Found more than one matching prs: ${prs.map(({number}) => `#${number}`).join(", ")}. Cannot proceed.`)
;({number} = prs.shift())
}
//Check if pull request could not been created because there are no diff between head and base
else if (/No commits between/.test(error)) {
info(`Pull request from ${committer.head} to ${committer.branch}`, "(no diff)")
committer.merge = false
number = "(none)"
}
else
throw error
}
info("Pull request number", number)
//Merge pull request
if (committer.merge) {
info("Merge method", committer.merge)
let attempts = 240
do {
//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})
console.debug(`Pull request #${number} mergeable state is "${state}"`)
if (mergeable === null) {
await wait(15)
continue
}
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})
info(`Merge #${number} to ${committer.branch}`, "ok")
//Delete head branch
try {
await wait(15)
await committer.rest.git.deleteRef({...github.context.repo, ref:`heads/${committer.head}`})
}
catch (error) {
console.debug(error)
if (!/reference does not exist/i.test(`${error}`))
throw error
}
info(`Branch ${committer.head}`, "(deleted)")
break
} while (--attempts)
}
}
//Success
info.break()
console.log("Success, thanks for using metrics!")
process.exit(0)
`,
{headers:{authorization:`token ${committer.token}`}},
)
sha = oid
}
catch (error) {
console.debug(error)
}
finally {
await committer.rest.repos.createOrUpdateFileContents({
...github.context.repo,
path,
content,
message:`${committer.message} (cache)`,
...(sha ? {sha} : {}),
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")
}
}
}
//Check editions
if ((committer.commit) || (committer.pr)) {
const git = sgit()
const sha = await git.hashObject(paths.join("/renders", filename))
info("Current render sha", sha)
if (committer.sha === sha) {
info(`Commit to branch ${committer.branch}`, "(no changes)")
committer.commit = false
}
}
//Upload to gist (this is done as user since committer_token may not have gist rights)
if (committer.gist) {
await rest.gists.update({gist_id:committer.gist, files:{[filename]:{content:rendered}}})
info(`Upload to gist ${committer.gist}`, "ok")
committer.commit = false
}
//Commit metrics
if (committer.commit) {
await committer.rest.repos.createOrUpdateFileContents({
...github.context.repo,
path:filename,
message:committer.message,
content:Buffer.from(rendered).toString("base64"),
branch:committer.pr ? committer.head : committer.branch,
...(committer.sha ? {sha:committer.sha} : {}),
})
info(`Commit to branch ${committer.branch}`, "ok")
}
//Pull request
if (committer.pr) {
//Create pull request
let number = null
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}))
info(`Pull request from ${committer.head} to ${committer.branch}`, "(created)")
}
//Errors
catch (error) {
console.error(error)
//Print debug buffer if debug was not enabled (if it is, it's already logged on the fly)
if (!DEBUG) {
for (const log of [info.break(), "An error occured, logging debug message :", ...debugged])
console.log(log)
}
core.setFailed(error.message)
process.exit(1)
console.debug(error)
//Check if pull request has already been created previously
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]")
if (prs.length < 1)
throw new Error("0 matching prs. Cannot proceed.")
if (prs.length > 1)
throw new Error(`Found more than one matching prs: ${prs.map(({number}) => `#${number}`).join(", ")}. Cannot proceed.`)
;({number} = prs.shift())
}
//Check if pull request could not been created because there are no diff between head and base
else if (/No commits between/.test(error)) {
info(`Pull request from ${committer.head} to ${committer.branch}`, "(no diff)")
committer.merge = false
number = "(none)"
}
else
throw error
}
})()
info("Pull request number", number)
//Merge pull request
if (committer.merge) {
info("Merge method", committer.merge)
let attempts = 240
do {
//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})
console.debug(`Pull request #${number} mergeable state is "${state}"`)
if (mergeable === null) {
await wait(15)
continue
}
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})
info(`Merge #${number} to ${committer.branch}`, "ok")
//Delete head branch
try {
await wait(15)
await committer.rest.git.deleteRef({...github.context.repo, ref:`heads/${committer.head}`})
}
catch (error) {
console.debug(error)
if (!/reference does not exist/i.test(`${error}`))
throw error
}
info(`Branch ${committer.head}`, "(deleted)")
break
} while (--attempts)
}
}
//Success
info.break()
console.log("Success, thanks for using metrics!")
process.exit(0)
}
//Errors
catch (error) {
console.error(error)
//Print debug buffer if debug was not enabled (if it is, it's already logged on the fly)
if (!DEBUG) {
for (const log of [info.break(), "An error occured, logging debug message :", ...debugged])
console.log(log)
}
core.setFailed(error.message)
process.exit(1)
}
})()

View File

@@ -1,193 +1,205 @@
//Imports
import * as utils from "./utils.mjs"
import ejs from "ejs"
import util from "util"
import SVGO from "svgo"
import xmlformat from "xml-formatter"
import ejs from "ejs"
import SVGO from "svgo"
import util from "util"
import xmlformat from "xml-formatter"
import * as utils from "./utils.mjs"
//Setup
export default async function metrics({login, q}, {graphql, rest, plugins, conf, die = false, verify = false, convert = null}, {Plugins, Templates}) {
//Compute rendering
try {
export default async function metrics({login, q}, {graphql, rest, plugins, conf, die = false, verify = false, convert = null}, {Plugins, Templates}) {
//Compute rendering
try {
//Debug
login = login.replace(/[\n\r]/g, "")
console.debug(`metrics/compute/${login} > start`)
console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256}))
//Debug
login = login.replace(/[\n\r]/g, "")
console.debug(`metrics/compute/${login} > start`)
console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256}))
//Load template
const template = q.template || conf.settings.templates.default
if ((!(template in Templates)) || (!(template in conf.templates)) || ((conf.settings.templates.enabled.length) && (!conf.settings.templates.enabled.includes(template))))
throw new Error("unsupported template")
const {image, style, fonts, views, partials} = conf.templates[template]
const computer = Templates[template].default || Templates[template]
convert = convert ?? conf.metadata.templates[template].formats[0] ?? null
console.debug(`metrics/compute/${login} > output format set to ${convert}`)
//Load template
const template = q.template || conf.settings.templates.default
if ((!(template in Templates))||(!(template in conf.templates))||((conf.settings.templates.enabled.length)&&(!conf.settings.templates.enabled.includes(template))))
throw new Error("unsupported template")
const {image, style, fonts, views, partials} = conf.templates[template]
const computer = Templates[template].default || Templates[template]
convert = convert ?? conf.metadata.templates[template].formats[0] ?? null
console.debug(`metrics/compute/${login} > output format set to ${convert}`)
//Initialization
const pending = []
const {queries} = conf
const data = {animated:true, base:{}, config:{}, errors:[], plugins:{}, computed:{}}
const imports = {plugins:Plugins, templates:Templates, metadata:conf.metadata, ...utils, ...(/markdown/.test(convert) ? {imgb64(url, options) {
//Initialization
const pending = []
const {queries} = conf
const data = {animated:true, base:{}, config:{}, errors:[], plugins:{}, computed:{}}
const imports = {
plugins:Plugins,
templates:Templates,
metadata:conf.metadata,
...utils,
...(/markdown/.test(convert)
? {
imgb64(url, options) {
return options?.force ? utils.imgb64(...arguments) : url
}} : null)}
const experimental = new Set(decodeURIComponent(q["experimental.features"] ?? "").split(" ").map(x => x.trim().toLocaleLowerCase()).filter(x => x))
if (conf.settings["debug.headless"])
imports.puppeteer.headless = false
},
}
: null),
}
const experimental = new Set(decodeURIComponent(q["experimental.features"] ?? "").split(" ").map(x => x.trim().toLocaleLowerCase()).filter(x => x))
if (conf.settings["debug.headless"])
imports.puppeteer.headless = false
//Partial parts
{
data.partials = new Set([
...decodeURIComponent(q["config.order"] ?? "").split(",").map(x => x.trim().toLocaleLowerCase()).filter(partial => partials.includes(partial)),
...partials,
])
console.debug(`metrics/compute/${login} > content order : ${[...data.partials]}`)
}
//Partial parts
{
data.partials = new Set([
...decodeURIComponent(q["config.order"] ?? "").split(",").map(x => x.trim().toLocaleLowerCase()).filter(partial => partials.includes(partial)),
...partials,
])
console.debug(`metrics/compute/${login} > content order : ${[...data.partials]}`)
}
//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})
const promised = await Promise.all(pending)
//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})
const promised = await Promise.all(pending)
//Check plugins errors
const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors]
if (errors.length) {
console.warn(`metrics/compute/${login} > ${errors.length} errors !`)
if (die)
throw new Error("An error occured during rendering, dying")
else
console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256}))
}
//Check plugins errors
const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors]
if (errors.length) {
console.warn(`metrics/compute/${login} > ${errors.length} errors !`)
if (die)
throw new Error("An error occured during rendering, dying")
else
console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256}))
}
//JSON output
if (convert === "json") {
console.debug(`metrics/compute/${login} > json output`)
return {rendered:data, mime:"application/json"}
}
//JSON output
if (convert === "json") {
console.debug(`metrics/compute/${login} > json output`)
return {rendered:data, mime:"application/json"}
}
//Markdown output
if (/markdown/.test(convert)) {
//Retrieving template source
console.debug(`metrics/compute/${login} > markdown render`)
let source = image
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})
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"}}))
}
catch (error) {
console.debug(error)
}
//Embed method
const embed = async(name, q = {}) => {
//Check arguments
if ((!name)||(typeof q !== "object")||(q === null)) {
if (die)
throw new Error("An error occured during embed rendering, dying")
return "<p>⚠️ Failed to execute embed function: invalid arguments</p>"
}
//Translate action syntax to web syntax
let parts = []
if (q.base === true)
({parts} = conf.settings.plugins.base)
if (typeof q.base === "string")
parts = q.base.split(",").map(x => x.trim())
if (Array.isArray(q.base))
parts = q.base
for (const part of conf.settings.plugins.base.parts)
q[`base.${part}`] = q[`base.${part}`] ?? parts.includes(part)
if (convert === "markdown-pdf") {
q["config.animations"] = false
q.config_animations = false
}
q = Object.fromEntries([...Object.entries(q).map(([key, value]) => [key.replace(/^plugin_/, "").replace(/_/g, "."), value]), ["base", false]])
//Enable required plugins
const plugins = Object.fromEntries(Object.entries(arguments[1].plugins).map(([key, value]) => [key, {...value, enabled:true}]))
//Compute rendering
const {rendered} = await metrics({login, q}, {...arguments[1], plugins, convert:null}, arguments[2])
return `<img class="metrics-cachable" data-name="${name}" src="data:image/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})
console.debug(`metrics/compute/${login} > success`)
//Output
if (convert === "markdown-pdf") {
return imports.svg.pdf(rendered, {
paddings:q["config.padding"] || conf.settings.padding,
style:(conf.settings.extras?.css ?? conf.settings.extras?.default ? q["extras.css"] ?? "" : ""),
twemojis:q["config.twemoji"],
gemojis:q["config.gemoji"],
rest,
})
}
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:style+(conf.settings.extras?.css ?? conf.settings.extras?.default ? q["extras.css"] ?? "" : ""), fonts}, {views, async:true})
//Additional transformations
if (q["config.twemoji"])
rendered = await imports.svg.twemojis(rendered)
if (q["config.gemoji"])
rendered = await imports.svg.gemojis(rendered, {rest})
//Optimize rendering
if (!q.raw)
rendered = xmlformat(rendered, {lineSeparator:"\n", collapseContent:true})
if ((conf.settings?.optimize)&&(!q.raw)) {
console.debug(`metrics/compute/${login} > optimize`)
if (experimental.has("--optimize")) {
const {error, data:optimized} = await SVGO.optimize(rendered, {multipass:true, plugins:SVGO.extendDefaultPlugins([
//Additional cleanup
{name:"cleanupListOfValues"},
{name:"removeRasterImages"},
{name:"removeScriptElement"},
//Force CSS style consistency
{name:"inlineStyles", active:false},
{name:"removeViewBox", active:false},
])})
if (error)
throw new Error(`Could not optimize SVG: \n${error}`)
rendered = optimized
console.debug(`metrics/compute/${login} > optimize > success`)
}
else
console.debug(`metrics/compute/${login} > optimize > this feature is currently disabled due to display issues (use --optimize flag in experimental features to force enable it)`)
}
//Verify svg
if (verify) {
console.debug(`metrics/compute/${login} > verify SVG`)
const libxmljs = (await import("libxmljs2")).default
const parsed = libxmljs.parseXml(rendered)
if (parsed.errors.length)
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
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})
rendered = resized
//Result
console.debug(`metrics/compute/${login} > success`)
return {rendered, mime}
//Markdown output
if (/markdown/.test(convert)) {
//Retrieving template source
console.debug(`metrics/compute/${login} > markdown render`)
let source = image
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})
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"}}))
}
//Internal error
catch (error) {
//User not found
if (((Array.isArray(error.errors))&&(error.errors[0].type === "NOT_FOUND")))
throw new Error("user not found")
//Generic error
throw error
console.debug(error)
}
}
//Embed method
const embed = async (name, q = {}) => {
//Check arguments
if ((!name) || (typeof q !== "object") || (q === null)) {
if (die)
throw new Error("An error occured during embed rendering, dying")
return "<p>⚠️ Failed to execute embed function: invalid arguments</p>"
}
//Translate action syntax to web syntax
let parts = []
if (q.base === true);
({parts} = conf.settings.plugins.base)
if (typeof q.base === "string")
parts = q.base.split(",").map(x => x.trim())
if (Array.isArray(q.base))
parts = q.base
for (const part of conf.settings.plugins.base.parts)
q[`base.${part}`] = q[`base.${part}`] ?? parts.includes(part)
if (convert === "markdown-pdf") {
q["config.animations"] = false
q.config_animations = false
}
q = Object.fromEntries([...Object.entries(q).map(([key, value]) => [key.replace(/^plugin_/, "").replace(/_/g, "."), value]), ["base", false]])
//Enable required plugins
const plugins = Object.fromEntries(Object.entries(arguments[1].plugins).map(([key, value]) => [key, {...value, enabled:true}]))
//Compute rendering
const {rendered} = await metrics({login, q}, {...arguments[1], plugins, convert:null}, arguments[2])
return `<img class="metrics-cachable" data-name="${name}" src="data:image/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})
console.debug(`metrics/compute/${login} > success`)
//Output
if (convert === "markdown-pdf") {
return imports.svg.pdf(rendered, {
paddings:q["config.padding"] || conf.settings.padding,
style:(conf.settings.extras?.css ?? conf.settings.extras?.default ? q["extras.css"] ?? "" : ""),
twemojis:q["config.twemoji"],
gemojis:q["config.gemoji"],
rest,
})
}
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:style + (conf.settings.extras?.css ?? conf.settings.extras?.default ? q["extras.css"] ?? "" : ""), fonts}, {views, async:true})
//Additional transformations
if (q["config.twemoji"])
rendered = await imports.svg.twemojis(rendered)
if (q["config.gemoji"])
rendered = await imports.svg.gemojis(rendered, {rest})
//Optimize rendering
if (!q.raw)
rendered = xmlformat(rendered, {lineSeparator:"\n", collapseContent:true})
if ((conf.settings?.optimize) && (!q.raw)) {
console.debug(`metrics/compute/${login} > optimize`)
if (experimental.has("--optimize")) {
const {error, data:optimized} = await SVGO.optimize(rendered, {
multipass:true,
plugins:SVGO.extendDefaultPlugins([
//Additional cleanup
{name:"cleanupListOfValues"},
{name:"removeRasterImages"},
{name:"removeScriptElement"},
//Force CSS style consistency
{name:"inlineStyles", active:false},
{name:"removeViewBox", active:false},
]),
})
if (error)
throw new Error(`Could not optimize SVG: \n${error}`)
rendered = optimized
console.debug(`metrics/compute/${login} > optimize > success`)
}
else
console.debug(`metrics/compute/${login} > optimize > this feature is currently disabled due to display issues (use --optimize flag in experimental features to force enable it)`)
}
//Verify svg
if (verify) {
console.debug(`metrics/compute/${login} > verify SVG`)
const libxmljs = (await import("libxmljs2")).default
const parsed = libxmljs.parseXml(rendered)
if (parsed.errors.length)
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
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})
rendered = resized
//Result
console.debug(`metrics/compute/${login} > success`)
return {rendered, mime}
}
//Internal error
catch (error) {
//User not found
if (((Array.isArray(error.errors)) && (error.errors[0].type === "NOT_FOUND")))
throw new Error("user not found")
//Generic error
throw error
}
}

View File

@@ -1,307 +1,313 @@
//Imports
import fs from "fs"
import path from "path"
import url from "url"
import yaml from "js-yaml"
import fs from "fs"
import yaml from "js-yaml"
import path from "path"
import url from "url"
//Defined categories
const categories = ["core", "github", "social", "other"]
const categories = ["core", "github", "social", "other"]
/**Metadata descriptor parser */
export default async function metadata({log = true} = {}) {
//Paths
const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../../..")
const __templates = path.join(__metrics, "source/templates")
const __plugins = path.join(__metrics, "source/plugins")
const __package = path.join(__metrics, "package.json")
export default async function metadata({log = true} = {}) {
//Paths
const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../../..")
const __templates = path.join(__metrics, "source/templates")
const __plugins = path.join(__metrics, "source/plugins")
const __package = path.join(__metrics, "package.json")
//Init
const logger = log ? console.debug : () => null
//Init
const logger = log ? console.debug : () => null
//Load plugins metadata
let Plugins = {}
logger("metrics/metadata > loading plugins metadata")
for (const name of await fs.promises.readdir(__plugins)) {
if (!(await fs.promises.lstat(path.join(__plugins, name))).isDirectory())
continue
logger(`metrics/metadata > loading plugin metadata [${name}]`)
Plugins[name] = await metadata.plugin({__plugins, name, logger})
}
//Reorder keys
const {base, core, ...plugins} = Plugins //eslint-disable-line no-unused-vars
Plugins = Object.fromEntries(Object.entries(Plugins).sort(([_an, a], [_bn, b]) => a.categorie === b.categorie ? (a.index ?? Infinity) - (b.index ?? Infinity) : categories.indexOf(a.categorie) - categories.indexOf(b.categorie)))
logger(`metrics/metadata > loaded [${Object.keys(Plugins).join(", ")}]`)
//Load templates metadata
let Templates = {}
logger("metrics/metadata > loading templates metadata")
for (const name of await fs.promises.readdir(__templates)) {
if (!(await fs.promises.lstat(path.join(__templates, name))).isDirectory())
continue
if (/^@/.test(name))
continue
logger(`metrics/metadata > loading template metadata [${name}]`)
Templates[name] = await metadata.template({__templates, name, plugins, logger})
}
//Reorder keys
const {community, ...templates} = Templates
Templates = {...Object.fromEntries(Object.entries(templates).sort(([_an, a], [_bn, b]) => (a.index ?? Infinity) - (b.index ?? Infinity))), community}
//Packaged metadata
const packaged = JSON.parse(`${await fs.promises.readFile(__package)}`)
//Metadata
return {plugins:Plugins, templates:Templates, packaged}
//Load plugins metadata
let Plugins = {}
logger("metrics/metadata > loading plugins metadata")
for (const name of await fs.promises.readdir(__plugins)) {
if (!(await fs.promises.lstat(path.join(__plugins, name))).isDirectory())
continue
logger(`metrics/metadata > loading plugin metadata [${name}]`)
Plugins[name] = await metadata.plugin({__plugins, name, logger})
}
//Reorder keys
const {base, core, ...plugins} = Plugins //eslint-disable-line no-unused-vars
Plugins = Object.fromEntries(Object.entries(Plugins).sort(([_an, a], [_bn, b]) => a.categorie === b.categorie ? (a.index ?? Infinity) - (b.index ?? Infinity) : categories.indexOf(a.categorie) - categories.indexOf(b.categorie)))
logger(`metrics/metadata > loaded [${Object.keys(Plugins).join(", ")}]`)
//Load templates metadata
let Templates = {}
logger("metrics/metadata > loading templates metadata")
for (const name of await fs.promises.readdir(__templates)) {
if (!(await fs.promises.lstat(path.join(__templates, name))).isDirectory())
continue
if (/^@/.test(name))
continue
logger(`metrics/metadata > loading template metadata [${name}]`)
Templates[name] = await metadata.template({__templates, name, plugins, logger})
}
//Reorder keys
const {community, ...templates} = Templates
Templates = {...Object.fromEntries(Object.entries(templates).sort(([_an, a], [_bn, b]) => (a.index ?? Infinity) - (b.index ?? Infinity))), community}
//Packaged metadata
const packaged = JSON.parse(`${await fs.promises.readFile(__package)}`)
//Metadata
return {plugins:Plugins, templates:Templates, packaged}
}
/**Metadata extractor for templates */
metadata.plugin = async function({__plugins, name, logger}) {
try {
//Load meta descriptor
const raw = `${await fs.promises.readFile(path.join(__plugins, name, "metadata.yml"), "utf-8")}`
const {inputs, ...meta} = yaml.load(raw)
metadata.plugin = async function({__plugins, name, logger}) {
try {
//Load meta descriptor
const raw = `${await fs.promises.readFile(path.join(__plugins, name, "metadata.yml"), "utf-8")}`
const {inputs, ...meta} = yaml.load(raw)
//Categorie
if (!categories.includes(meta.categorie))
meta.categorie = "other"
//Categorie
if (!categories.includes(meta.categorie))
meta.categorie = "other"
//Inputs parser
{
meta.inputs = function({data:{user = null} = {}, q, account}, defaults = {}) {
//Support check
if (!account)
logger(`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()}}
}
//Inputs checks
const result = Object.fromEntries(Object.entries(inputs).map(([key, {type, format, default:defaulted, min, max, values}]) => [
//Format key
metadata.to.query(key, {name}),
//Format value
(defaulted => {
//Default value
let value = q[metadata.to.query(key)] ?? q[key] ?? defaulted
//Apply type conversion
switch (type) {
//Booleans
case "boolean":{
if (/^(?:[Tt]rue|[Oo]n|[Yy]es|1)$/.test(value))
return true
if (/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(value))
return false
return defaulted
}
//Numbers
case "number":{
value = Number(value)
if (!Number.isFinite(value))
value = defaulted
if (Number.isFinite(min))
value = Math.max(min, value)
if (Number.isFinite(max))
value = Math.min(value, max)
return value
}
//Array
case "array":{
try {
value = decodeURIComponent(value)
}
catch {
logger(`metrics/inputs > failed to decode uri : ${value}`)
value = defaulted
}
const separators = {"comma-separated":",", "space-separated":" "}
const separator = separators[[format].flat().filter(s => s in separators)[0]] ?? ","
return value.split(separator).map(v => v.trim().toLocaleLowerCase()).filter(v => Array.isArray(values) ? values.includes(v) : true).filter(v => v)
}
//String
case "string":{
value = `${value}`.trim()
if (user) {
if (value === ".user.login")
return user.login
if (value === ".user.twitter")
return user.twitterUsername
if (value === ".user.website")
return user.websiteUrl
}
if ((Array.isArray(values))&&(!values.includes(value)))
return defaulted
return value
}
//JSON
case "json":{
try {
value = JSON.parse(value)
}
catch {
logger(`metrics/inputs > failed to parse json : ${value}`)
value = JSON.parse(defaulted)
}
return value
}
//Token
case "token":{
return value
}
//Default
default:{
return value
}
}
})(defaults[key] ?? defaulted),
]))
logger(`metrics/inputs > ${name} > ${JSON.stringify(result)}`)
return result
}
Object.assign(meta.inputs, inputs, Object.fromEntries(Object.entries(inputs).map(([key, value]) => [metadata.to.query(key, {name}), value])))
//Inputs parser
{
meta.inputs = function({data:{user = null} = {}, q, account}, defaults = {}) {
//Support check
if (!account)
logger(`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()}}
}
//Action metadata
{
//Extract comments
const comments = {}
raw.split(/(?:\r?\n){2,}/m)
.map(x => x.trim()).filter(x => x)
.map(x => x.split("\n").map(y => y.trim()).join("\n"))
.map(x => {
const input = x.match(new RegExp(`^\\s*(?<input>${Object.keys(inputs).join("|")}):`, "m"))?.groups?.input ?? null
if (input)
comments[input] = x.match(new RegExp(`(?<comment>[\\s\\S]*?)(?=(?:${Object.keys(inputs).sort((a, b) => b.length - a.length).join("|")}):)`))?.groups?.comment
})
//Action descriptor
meta.action = Object.fromEntries(Object.entries(inputs).map(([key, value]) => [
key,
{
comment:comments[key] ?? "",
descriptor:yaml.dump({[key]:Object.fromEntries(Object.entries(value).filter(([key]) => ["description", "default", "required"].includes(key)))}, {quotingType:'"', noCompatMode:true}),
},
]))
//Action inputs
meta.inputs.action = function({core}) {
//Build query object from inputs
const q = {}
for (const key of Object.keys(inputs)) {
const value = `${core.getInput(key)}`.trim()
//Inputs checks
const result = Object.fromEntries(
Object.entries(inputs).map(([key, {type, format, default:defaulted, min, max, values}]) => [
//Format key
metadata.to.query(key, {name}),
//Format value
(defaulted => {
//Default value
let value = q[metadata.to.query(key)] ?? q[key] ?? defaulted
//Apply type conversion
switch (type) {
//Booleans
case "boolean": {
if (/^(?:[Tt]rue|[Oo]n|[Yy]es|1)$/.test(value))
return true
if (/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(value))
return false
return defaulted
}
//Numbers
case "number": {
value = Number(value)
if (!Number.isFinite(value))
value = defaulted
if (Number.isFinite(min))
value = Math.max(min, value)
if (Number.isFinite(max))
value = Math.min(value, max)
return value
}
//Array
case "array": {
try {
q[key] = decodeURIComponent(value)
value = decodeURIComponent(value)
}
catch {
logger(`metrics/inputs > failed to decode uri : ${value}`)
q[key] = value
value = defaulted
}
const separators = {"comma-separated":",", "space-separated":" "}
const separator = separators[[format].flat().filter(s => s in separators)[0]] ?? ","
return value.split(separator).map(v => v.trim().toLocaleLowerCase()).filter(v => Array.isArray(values) ? values.includes(v) : true).filter(v => v)
}
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}]) => [
//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}
case "number":
return {text, type:"number", min, max, defaulted}
case "array":
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}
//String
case "string": {
value = `${value}`.trim()
if (user) {
if (value === ".user.login")
return user.login
if (value === ".user.twitter")
return user.twitterUsername
if (value === ".user.website")
return user.websiteUrl
}
case "json":
return {text, type:"text", placeholder:example ?? defaulted, defaulted}
default:
return null
if ((Array.isArray(values)) && (!values.includes(value)))
return defaulted
return value
}
})(),
]).filter(([key, value]) => (value)&&(key !== name)))
}
//Readme metadata
{
//Extract demos
const raw = `${await fs.promises.readFile(path.join(__plugins, name, "README.md"), "utf-8")}`
const demo = raw.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? "<td></td>"
//Readme descriptor
meta.readme = {demo}
}
//Icon
meta.icon = meta.name.split(" ")[0] ?? null
//Result
return meta
//JSON
case "json": {
try {
value = JSON.parse(value)
}
catch {
logger(`metrics/inputs > failed to parse json : ${value}`)
value = JSON.parse(defaulted)
}
return value
}
//Token
case "token": {
return value
}
//Default
default: {
return value
}
}
})(defaults[key] ?? defaulted),
]),
)
logger(`metrics/inputs > ${name} > ${JSON.stringify(result)}`)
return result
}
Object.assign(meta.inputs, inputs, Object.fromEntries(Object.entries(inputs).map(([key, value]) => [metadata.to.query(key, {name}), value])))
}
catch (error) {
logger(`metrics/metadata > failed to load plugin ${name}: ${error}`)
return null
}
}
/**Metadata extractor for templates */
metadata.template = async function({__templates, name, plugins, logger}) {
try {
//Load meta descriptor
const raw = fs.existsSync(path.join(__templates, name, "metadata.yml")) ? `${await fs.promises.readFile(path.join(__templates, name, "metadata.yml"), "utf-8")}` : ""
const readme = `${await fs.promises.readFile(path.join(__templates, name, "README.md"), "utf-8")}`
const meta = yaml.load(raw) ?? {}
//Action metadata
{
//Extract comments
const comments = {}
raw.split(/(?:\r?\n){2,}/m)
.map(x => x.trim()).filter(x => x)
.map(x => x.split("\n").map(y => y.trim()).join("\n"))
.map(x => {
const input = x.match(new RegExp(`^\\s*(?<input>${Object.keys(inputs).join("|")}):`, "m"))?.groups?.input ?? null
if (input)
comments[input] = x.match(new RegExp(`(?<comment>[\\s\\S]*?)(?=(?:${Object.keys(inputs).sort((a, b) => b.length - a.length).join("|")}):)`))?.groups?.comment
})
//Compatibility
const partials = path.join(__templates, name, "partials")
const compatibility = Object.fromEntries(Object.entries(plugins).map(([key]) => [key, false]))
if ((fs.existsSync(partials))&&((await fs.promises.lstat(partials)).isDirectory())) {
for (let plugin of await fs.promises.readdir(partials)) {
plugin = plugin.match(/(?<plugin>^[\s\S]+(?=[.]ejs$))/)?.groups?.plugin ?? null
if (plugin in compatibility)
compatibility[plugin] = true
//Action descriptor
meta.action = Object.fromEntries(
Object.entries(inputs).map(([key, value]) => [
key,
{
comment:comments[key] ?? "",
descriptor:yaml.dump({[key]:Object.fromEntries(Object.entries(value).filter(([key]) => ["description", "default", "required"].includes(key)))}, {quotingType:'"', noCompatMode:true}),
},
]),
)
//Action inputs
meta.inputs.action = function({core}) {
//Build query object from inputs
const q = {}
for (const key of Object.keys(inputs)) {
const value = `${core.getInput(key)}`.trim()
try {
q[key] = decodeURIComponent(value)
}
catch {
logger(`metrics/inputs > failed to decode uri : ${value}`)
q[key] = value
}
}
//Result
return {
name:meta.name ?? readme.match(/^### (?<name>[\s\S]+?)\n/)?.groups?.name?.trim(),
index:meta.index ?? null,
formats:meta.formats ?? null,
supports:meta.supports ?? null,
readme:{
demo:readme.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? '<td align="center" colspan="2">See <a href="/source/templates/community/README.md">documentation</a> 🌍</td>' : "<td></td>"),
compatibility:{...compatibility, base:true},
},
check({q, account = "bypass", format = null}) {
//Support check
if (account !== "bypass") {
const context = q.repo ? "repository" : account
if ((Array.isArray(this.supports))&&(!this.supports.includes(context)))
throw new Error(`not supported for: ${context}`)
}
//Format check
if ((format)&&(Array.isArray(this.formats))&&(!this.formats.includes(format)))
throw new Error(`not supported for: ${format}`)
},
}
return meta.inputs({q, account:"bypass"})
}
}
catch (error) {
logger(`metrics/metadata > failed to load template ${name}: ${error}`)
return null
//Web metadata
{
meta.web = Object.fromEntries(
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}
case "number":
return {text, type:"number", min, max, defaulted}
case "array":
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}
}
case "json":
return {text, type:"text", placeholder:example ?? defaulted, defaulted}
default:
return null
}
})(),
]).filter(([key, value]) => (value) && (key !== name)),
)
}
//Readme metadata
{
//Extract demos
const raw = `${await fs.promises.readFile(path.join(__plugins, name, "README.md"), "utf-8")}`
const demo = raw.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? "<td></td>"
//Readme descriptor
meta.readme = {demo}
}
//Icon
meta.icon = meta.name.split(" ")[0] ?? null
//Result
return meta
}
catch (error) {
logger(`metrics/metadata > failed to load plugin ${name}: ${error}`)
return null
}
}
/**Metadata extractor for templates */
metadata.template = async function({__templates, name, plugins, logger}) {
try {
//Load meta descriptor
const raw = fs.existsSync(path.join(__templates, name, "metadata.yml")) ? `${await fs.promises.readFile(path.join(__templates, name, "metadata.yml"), "utf-8")}` : ""
const readme = `${await fs.promises.readFile(path.join(__templates, name, "README.md"), "utf-8")}`
const meta = yaml.load(raw) ?? {}
//Compatibility
const partials = path.join(__templates, name, "partials")
const compatibility = Object.fromEntries(Object.entries(plugins).map(([key]) => [key, false]))
if ((fs.existsSync(partials)) && ((await fs.promises.lstat(partials)).isDirectory())) {
for (let plugin of await fs.promises.readdir(partials)) {
plugin = plugin.match(/(?<plugin>^[\s\S]+(?=[.]ejs$))/)?.groups?.plugin ?? null
if (plugin in compatibility)
compatibility[plugin] = true
}
}
//Result
return {
name:meta.name ?? readme.match(/^### (?<name>[\s\S]+?)\n/)?.groups?.name?.trim(),
index:meta.index ?? null,
formats:meta.formats ?? null,
supports:meta.supports ?? null,
readme:{
demo:readme.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? '<td align="center" colspan="2">See <a href="/source/templates/community/README.md">documentation</a> 🌍</td>' : "<td></td>"),
compatibility:{...compatibility, base:true},
},
check({q, account = "bypass", format = null}) {
//Support check
if (account !== "bypass") {
const context = q.repo ? "repository" : account
if ((Array.isArray(this.supports)) && (!this.supports.includes(context)))
throw new Error(`not supported for: ${context}`)
}
//Format check
if ((format) && (Array.isArray(this.formats)) && (!this.formats.includes(format)))
throw new Error(`not supported for: ${format}`)
},
}
}
catch (error) {
logger(`metrics/metadata > failed to load template ${name}: ${error}`)
return null
}
}
/**Metadata converters */
metadata.to = {
query(key, {name = null} = {}) {
key = key.replace(/^plugin_/, "").replace(/_/g, ".")
return name ? key.replace(new RegExp(`^(${name}.)`, "g"), "") : key
},
}
metadata.to = {
query(key, {name = null} = {}) {
key = key.replace(/^plugin_/, "").replace(/_/g, ".")
return name ? key.replace(new RegExp(`^(${name}.)`, "g"), "") : key
},
}

View File

@@ -1,231 +1,238 @@
//Imports
import fs from "fs"
import metadata from "./metadata.mjs"
import path from "path"
import processes from "child_process"
import util from "util"
import url from "url"
import yaml from "js-yaml"
import OctokitRest from "@octokit/rest"
import OctokitRest from "@octokit/rest"
import processes from "child_process"
import fs from "fs"
import yaml from "js-yaml"
import path from "path"
import url from "url"
import util from "util"
import metadata from "./metadata.mjs"
//Templates and plugins
const Templates = {}
const Plugins = {}
const Templates = {}
const Plugins = {}
/**Setup */
export default async function({log = true, nosettings = false, community = {}} = {}) {
//Paths
const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../../..")
const __statics = path.join(__metrics, "source/app/web/statics")
const __templates = path.join(__metrics, "source/templates")
const __plugins = path.join(__metrics, "source/plugins")
const __package = path.join(__metrics, "package.json")
const __settings = path.join(__metrics, "settings.json")
const __modules = path.join(__metrics, "node_modules")
//Init
const logger = log ? console.debug : () => null
logger("metrics/setup > setup")
const conf = {
authenticated:null,
templates:{},
queries:{},
settings:{},
metadata:{},
paths:{
statics:__statics,
templates:__templates,
node_modules:__modules,
},
}
//Load settings
logger("metrics/setup > load settings.json")
if (fs.existsSync(__settings)) {
if (nosettings)
logger("metrics/setup > load settings.json > skipped because no settings is enabled")
else {
conf.settings = JSON.parse(`${await fs.promises.readFile(__settings)}`)
logger("metrics/setup > load settings.json > success")
}
}
else
logger("metrics/setup > load settings.json > (missing)")
if (!conf.settings.templates)
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"]}
if (conf.settings.debug)
logger(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256}))
//Load package settings
logger("metrics/setup > load package.json")
conf.package = JSON.parse(`${await fs.promises.readFile(__package)}`)
logger("metrics/setup > load package.json > success")
//Load community templates
if ((typeof conf.settings.community.templates === "string")&&(conf.settings.community.templates.length)) {
logger("metrics/setup > parsing community templates list")
conf.settings.community.templates = [...new Set([...decodeURIComponent(conf.settings.community.templates).split(",").map(v => v.trim().toLocaleLowerCase()).filter(v => v)])]
}
if ((Array.isArray(conf.settings.community.templates))&&(conf.settings.community.templates.length)) {
//Clean remote repository
logger(`metrics/setup > ${conf.settings.community.templates.length} community templates to install`)
await fs.promises.rmdir(path.join(__templates, ".community"), {recursive:true})
//Download community templates
for (const template of conf.settings.community.templates) {
try {
//Parse community template
logger(`metrics/setup > load community template ${template}`)
const {repo, branch, name, trust = false} = template.match(/^(?<repo>[\s\S]+?)@(?<branch>[\s\S]+?):(?<name>[\s\S]+?)(?<trust>[+]trust)?$/)?.groups ?? null
const command = `git clone --single-branch --branch ${branch} https://github.com/${repo}.git ${path.join(__templates, ".community")}`
logger(`metrics/setup > run ${command}`)
//Clone remote repository
processes.execSync(command, {stdio:"ignore"})
//Extract template
logger(`metrics/setup > extract ${name} from ${repo}@${branch}`)
await fs.promises.rmdir(path.join(__templates, `@${name}`), {recursive:true})
await fs.promises.rename(path.join(__templates, ".community/source/templates", name), path.join(__templates, `@${name}`))
//JavaScript file
if (trust)
logger(`metrics/setup > keeping @${name}/template.mjs (unsafe mode is enabled)`)
else if (fs.existsSync(path.join(__templates, `@${name}`, "template.mjs"))) {
logger(`metrics/setup > removing @${name}/template.mjs`)
await fs.promises.unlink(path.join(__templates, `@${name}`, "template.mjs"))
const inherit = yaml.load(`${fs.promises.readFile(path.join(__templates, `@${name}`, "metadata.yml"))}`).extends ?? null
if (inherit) {
logger(`metrics/setup > @${name} extends from ${inherit}`)
if (fs.existsSync(path.join(__templates, inherit, "template.mjs"))) {
logger(`metrics/setup > @${name} extended from ${inherit}`)
await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs"))
}
else
logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`)
}
}
else
logger(`metrics/setup > @${name}/template.mjs does not exist`)
//Clean remote repository
logger(`metrics/setup > clean ${repo}@${branch}`)
await fs.promises.rmdir(path.join(__templates, ".community"), {recursive:true})
logger(`metrics/setup > loaded community template ${name}`)
}
catch (error) {
logger(`metrics/setup > failed to load community template ${template}`)
logger(error)
}
}
}
else
logger("metrics/setup > no community templates to install")
//Load templates
for (const name of await fs.promises.readdir(__templates)) {
//Search for templates
const directory = path.join(__templates, name)
if ((!(await fs.promises.lstat(directory)).isDirectory())||(!fs.existsSync(path.join(directory, "partials/_.json"))))
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 [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]}
//Cache templates scripts
Templates[name] = await (async() => {
const template = path.join(directory, "template.mjs")
const fallback = path.join(__templates, "classic", "template.mjs")
return (await import(url.pathToFileURL(fs.existsSync(template) ? template : fallback).href)).default
})()
logger(`metrics/setup > load template [${name}] > success`)
//Debug
if (conf.settings.debug) {
Object.defineProperty(conf.templates, name, {
get() {
logger(`metrics/setup > reload template [${name}]`)
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]}
},
})
}
}
//Load plugins
for (const name of await fs.promises.readdir(__plugins)) {
//Search for plugins
const directory = path.join(__plugins, name)
if (!(await fs.promises.lstat(directory)).isDirectory())
continue
//Cache plugins scripts
logger(`metrics/setup > load plugin [${name}]`)
Plugins[name] = (await import(url.pathToFileURL(path.join(directory, "index.mjs")).href)).default
logger(`metrics/setup > load plugin [${name}] > success`)
//Register queries
const __queries = path.join(directory, "queries")
if (fs.existsSync(__queries)) {
//Alias for default query
const queries = function() {
if (!queries[name])
throw new ReferenceError(`Default query for ${name} undefined`)
return queries[name](...arguments)
}
conf.queries[name] = queries
//Load queries
for (const file of await fs.promises.readdir(__queries)) {
//Cache queries
const query = file.replace(/[.]graphql$/, "")
logger(`metrics/setup > load query [${name}/${query}]`)
queries[`_${query}`] = `${await fs.promises.readFile(path.join(__queries, file))}`
logger(`metrics/setup > load query [${name}/${query}] > success`)
//Debug
if (conf.settings.debug) {
Object.defineProperty(queries, `_${query}`, {
get() {
logger(`metrics/setup > reload query [${name}/${query}]`)
const raw = `${fs.readFileSync(path.join(__queries, file))}`
logger(`metrics/setup > reload query [${name}/${query}] > success`)
return raw
},
})
}
}
//Create queries formatters
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)
return queried
})
}
}
//Load metadata
conf.metadata = await metadata({log})
//Store authenticated user
if (conf.settings.token) {
try {
conf.authenticated = (await (new OctokitRest.Octokit({auth:conf.settings.token})).users.getAuthenticated()).data.login
logger(`metrics/setup > setup > authenticated as ${conf.authenticated}`)
}
catch (error) {
logger(`metrics/setup > setup > could not verify authentication : ${error}`)
}
}
//Set no token property
Object.defineProperty(conf.settings, "notoken", {get() {
return conf.settings.token === "NOT_NEEDED"
}})
//Conf
logger("metrics/setup > setup > success")
return {Templates, Plugins, conf}
export default async function({log = true, nosettings = false, community = {}} = {}) {
//Paths
const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../../..")
const __statics = path.join(__metrics, "source/app/web/statics")
const __templates = path.join(__metrics, "source/templates")
const __plugins = path.join(__metrics, "source/plugins")
const __package = path.join(__metrics, "package.json")
const __settings = path.join(__metrics, "settings.json")
const __modules = path.join(__metrics, "node_modules")
//Init
const logger = log ? console.debug : () => null
logger("metrics/setup > setup")
const conf = {
authenticated:null,
templates:{},
queries:{},
settings:{},
metadata:{},
paths:{
statics:__statics,
templates:__templates,
node_modules:__modules,
},
}
//Load settings
logger("metrics/setup > load settings.json")
if (fs.existsSync(__settings)) {
if (nosettings)
logger("metrics/setup > load settings.json > skipped because no settings is enabled")
else {
conf.settings = JSON.parse(`${await fs.promises.readFile(__settings)}`)
logger("metrics/setup > load settings.json > success")
}
}
else
logger("metrics/setup > load settings.json > (missing)")
if (!conf.settings.templates)
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"]}
if (conf.settings.debug)
logger(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256}))
//Load package settings
logger("metrics/setup > load package.json")
conf.package = JSON.parse(`${await fs.promises.readFile(__package)}`)
logger("metrics/setup > load package.json > success")
//Load community templates
if ((typeof conf.settings.community.templates === "string") && (conf.settings.community.templates.length)) {
logger("metrics/setup > parsing community templates list")
conf.settings.community.templates = [...new Set([...decodeURIComponent(conf.settings.community.templates).split(",").map(v => v.trim().toLocaleLowerCase()).filter(v => v)])]
}
if ((Array.isArray(conf.settings.community.templates)) && (conf.settings.community.templates.length)) {
//Clean remote repository
logger(`metrics/setup > ${conf.settings.community.templates.length} community templates to install`)
await fs.promises.rmdir(path.join(__templates, ".community"), {recursive:true})
//Download community templates
for (const template of conf.settings.community.templates) {
try {
//Parse community template
logger(`metrics/setup > load community template ${template}`)
const {repo, branch, name, trust = false} = template.match(/^(?<repo>[\s\S]+?)@(?<branch>[\s\S]+?):(?<name>[\s\S]+?)(?<trust>[+]trust)?$/)?.groups ?? null
const command = `git clone --single-branch --branch ${branch} https://github.com/${repo}.git ${path.join(__templates, ".community")}`
logger(`metrics/setup > run ${command}`)
//Clone remote repository
processes.execSync(command, {stdio:"ignore"})
//Extract template
logger(`metrics/setup > extract ${name} from ${repo}@${branch}`)
await fs.promises.rmdir(path.join(__templates, `@${name}`), {recursive:true})
await fs.promises.rename(path.join(__templates, ".community/source/templates", name), path.join(__templates, `@${name}`))
//JavaScript file
if (trust)
logger(`metrics/setup > keeping @${name}/template.mjs (unsafe mode is enabled)`)
else if (fs.existsSync(path.join(__templates, `@${name}`, "template.mjs"))) {
logger(`metrics/setup > removing @${name}/template.mjs`)
await fs.promises.unlink(path.join(__templates, `@${name}`, "template.mjs"))
const inherit = yaml.load(`${fs.promises.readFile(path.join(__templates, `@${name}`, "metadata.yml"))}`).extends ?? null
if (inherit) {
logger(`metrics/setup > @${name} extends from ${inherit}`)
if (fs.existsSync(path.join(__templates, inherit, "template.mjs"))) {
logger(`metrics/setup > @${name} extended from ${inherit}`)
await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs"))
}
else
logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`)
}
}
else
logger(`metrics/setup > @${name}/template.mjs does not exist`)
//Clean remote repository
logger(`metrics/setup > clean ${repo}@${branch}`)
await fs.promises.rmdir(path.join(__templates, ".community"), {recursive:true})
logger(`metrics/setup > loaded community template ${name}`)
}
catch (error) {
logger(`metrics/setup > failed to load community template ${template}`)
logger(error)
}
}
}
else
logger("metrics/setup > no community templates to install")
//Load templates
for (const name of await fs.promises.readdir(__templates)) {
//Search for templates
const directory = path.join(__templates, name)
if ((!(await fs.promises.lstat(directory)).isDirectory()) || (!fs.existsSync(path.join(directory, "partials/_.json"))))
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 [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]}
//Cache templates scripts
Templates[name] = await (async () => {
const template = path.join(directory, "template.mjs")
const fallback = path.join(__templates, "classic", "template.mjs")
return (await import(url.pathToFileURL(fs.existsSync(template) ? template : fallback).href)).default
})()
logger(`metrics/setup > load template [${name}] > success`)
//Debug
if (conf.settings.debug) {
Object.defineProperty(conf.templates, name, {
get() {
logger(`metrics/setup > reload template [${name}]`)
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]}
},
})
}
}
//Load plugins
for (const name of await fs.promises.readdir(__plugins)) {
//Search for plugins
const directory = path.join(__plugins, name)
if (!(await fs.promises.lstat(directory)).isDirectory())
continue
//Cache plugins scripts
logger(`metrics/setup > load plugin [${name}]`)
Plugins[name] = (await import(url.pathToFileURL(path.join(directory, "index.mjs")).href)).default
logger(`metrics/setup > load plugin [${name}] > success`)
//Register queries
const __queries = path.join(directory, "queries")
if (fs.existsSync(__queries)) {
//Alias for default query
const queries = function() {
if (!queries[name])
throw new ReferenceError(`Default query for ${name} undefined`)
return queries[name](...arguments)
}
conf.queries[name] = queries
//Load queries
for (const file of await fs.promises.readdir(__queries)) {
//Cache queries
const query = file.replace(/[.]graphql$/, "")
logger(`metrics/setup > load query [${name}/${query}]`)
queries[`_${query}`] = `${await fs.promises.readFile(path.join(__queries, file))}`
logger(`metrics/setup > load query [${name}/${query}] > success`)
//Debug
if (conf.settings.debug) {
Object.defineProperty(queries, `_${query}`, {
get() {
logger(`metrics/setup > reload query [${name}/${query}]`)
const raw = `${fs.readFileSync(path.join(__queries, file))}`
logger(`metrics/setup > reload query [${name}/${query}] > success`)
return raw
},
})
}
}
//Create queries formatters
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)
return queried
}
)
}
}
//Load metadata
conf.metadata = await metadata({log})
//Store authenticated user
if (conf.settings.token) {
try {
conf.authenticated = (await (new OctokitRest.Octokit({auth:conf.settings.token})).users.getAuthenticated()).data.login
logger(`metrics/setup > setup > authenticated as ${conf.authenticated}`)
}
catch (error) {
logger(`metrics/setup > setup > could not verify authentication : ${error}`)
}
}
//Set no token property
Object.defineProperty(conf.settings, "notoken", {
get() {
return conf.settings.token === "NOT_NEEDED"
},
})
//Conf
logger("metrics/setup > setup > success")
return {Templates, Plugins, conf}
}

View File

@@ -1,422 +1,429 @@
//Imports
import fs from "fs/promises"
import fss from "fs"
import os from "os"
import paths from "path"
import url from "url"
import util from "util"
import processes from "child_process"
import axios from "axios"
import _puppeteer from "puppeteer"
import git from "simple-git"
import twemojis from "twemoji-parser"
import jimp from "jimp"
import opengraph from "open-graph-scraper"
import rss from "rss-parser"
import nodechartist from "node-chartist"
import GIFEncoder from "gifencoder"
import PNG from "png-js"
import marked from "marked"
import htmlsanitize from "sanitize-html"
import prism from "prismjs"
import prism_lang from "prismjs/components/index.js"
prism_lang()
import fs from "fs/promises"
import prism_lang from "prismjs/components/index.js"
import axios from "axios"
import processes from "child_process"
import fss from "fs"
import GIFEncoder from "gifencoder"
import jimp from "jimp"
import marked from "marked"
import nodechartist from "node-chartist"
import opengraph from "open-graph-scraper"
import os from "os"
import paths from "path"
import PNG from "png-js"
import prism from "prismjs"
import _puppeteer from "puppeteer"
import rss from "rss-parser"
import htmlsanitize from "sanitize-html"
import git from "simple-git"
import twemojis from "twemoji-parser"
import url from "url"
import util from "util"
prism_lang()
//Exports
export {fs, os, paths, url, util, processes, axios, git, opengraph, jimp, rss}
export {axios, fs, git, jimp, opengraph, os, paths, processes, rss, url, util}
/**Returns module __dirname */
export function __module(module) {
return paths.join(paths.dirname(url.fileURLToPath(module)))
}
export function __module(module) {
return paths.join(paths.dirname(url.fileURLToPath(module)))
}
/**Puppeteer instantier */
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:true,
}
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:true,
}
/**Plural formatter */
export function s(value, end = "") {
return value !== 1 ? {y:"ies", "":"s"}[end] : end
}
export function s(value, end = "") {
return value !== 1 ? {y:"ies", "":"s"}[end] : end
}
/**Formatter */
export function format(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}]) {
if (n/v >= 1)
return `${(sign)&&(n > 0) ? "+" : ""}${(n/v).toFixed(fixed ?? 2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
}
export function format(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}]) {
if (n / v >= 1)
return `${(sign) && (n > 0) ? "+" : ""}${(n / v).toFixed(fixed ?? 2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
}
return `${(sign)&&(n > 0) ? "+" : ""}${fixed ? n.toFixed(fixed) : n}`
}
return `${(sign) && (n > 0) ? "+" : ""}${fixed ? n.toFixed(fixed) : n}`
}
/**Bytes formatter */
export function bytes(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}]) {
if (n/v >= 1)
return `${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B`
}
return `${n} byte${n > 1 ? "s" : ""}`
export function bytes(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}]) {
if (n / v >= 1)
return `${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B`
}
format.bytes = bytes
return `${n} byte${n > 1 ? "s" : ""}`
}
format.bytes = bytes
/**Percentage formatter */
export function percentage(n, {rescale = true} = {}) {
return `${(n*(rescale ? 100 : 1)).toFixed(2)
export function percentage(n, {rescale = true} = {}) {
return `${
(n * (rescale ? 100 : 1)).toFixed(2)
.replace(/(?<=[.])(?<decimal>[1-9]*)0+$/, "$<decimal>")
.replace(/[.]$/, "")}%`
}
format.percentage = percentage
.replace(/[.]$/, "")
}%`
}
format.percentage = percentage
/**Text ellipsis formatter */
export function ellipsis(text, {length = 20} = {}) {
text = `${text}`
if (text.length < length)
return text
return `${text.substring(0, length)}`
}
format.ellipsis = ellipsis
export function ellipsis(text, {length = 20} = {}) {
text = `${text}`
if (text.length < length)
return text
return `${text.substring(0, length)}`
}
format.ellipsis = ellipsis
/**Date formatter */
export function date(string, options) {
return new Intl.DateTimeFormat("en-GB", options).format(new Date(string))
}
format.date = date
export function date(string, options) {
return new Intl.DateTimeFormat("en-GB", options).format(new Date(string))
}
format.date = date
/**Array shuffler */
export function shuffle(array) {
for (let i = array.length-1; i > 0; i--) {
const j = Math.floor(Math.random()*(i+1))
;[array[i], array[j]] = [array[j], array[i]]
}
return array
export function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[array[i], array[j]] = [array[j], array[i]]
}
return array
}
/**Escape html */
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;" : "<")
.replace(/>/g, u[">"] ? "&gt;" : ">")
.replace(/"/g, u['"'] ? "&quot;" : '"')
.replace(/'/g, u["'"] ? "&apos;" : "'")
}
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;" : "<")
.replace(/>/g, u[">"] ? "&gt;" : ">")
.replace(/"/g, u['"'] ? "&quot;" : '"')
.replace(/'/g, u["'"] ? "&apos;" : "'")
}
/**Unescape html */
export function htmlunescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) {
return string
.replace(/&lt;/g, u["<"] ? "<" : "&lt;")
.replace(/&gt;/g, u[">"] ? ">" : "&gt;")
.replace(/&quot;/g, u['"'] ? '"' : "&quot;")
.replace(/&(?:apos|#39);/g, u["'"] ? "'" : "&apos;")
.replace(/&amp;/g, u["&"] ? "&" : "&amp;")
}
export function htmlunescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) {
return string
.replace(/&lt;/g, u["<"] ? "<" : "&lt;")
.replace(/&gt;/g, u[">"] ? ">" : "&gt;")
.replace(/&quot;/g, u['"'] ? '"' : "&quot;")
.replace(/&(?:apos|#39);/g, u["'"] ? "'" : "&apos;")
.replace(/&amp;/g, u["&"] ? "&" : "&amp;")
}
/**Chartist */
export async function chartist() {
const css = `<style>${await fs.readFile(paths.join(__module(import.meta.url), "../../../node_modules", "node-chartist/dist/main.css")).catch(_ => "")}</style>`
return (await nodechartist(...arguments))
.replace(/class="ct-chart-line">/, `class="ct-chart-line">${css}`)
}
export async function chartist() {
const css = `<style>${await fs.readFile(paths.join(__module(import.meta.url), "../../../node_modules", "node-chartist/dist/main.css")).catch(_ => "")}</style>`
return (await nodechartist(...arguments))
.replace(/class="ct-chart-line">/, `class="ct-chart-line">${css}`)
}
/**Run command */
export async function run(command, options, {prefixed = true} = {}) {
const prefix = {win32:"wsl"}[process.platform] ?? ""
command = `${prefixed ? prefix : ""} ${command}`.trim()
return new Promise((solve, reject) => {
console.debug(`metrics/command > ${command}`)
const child = processes.exec(command, options)
let [stdout, stderr] = ["", ""]
child.stdout.on("data", data => stdout += data)
child.stderr.on("data", data => stderr += data)
child.on("close", code => {
console.debug(`metrics/command > ${command} > exited with code ${code}`)
console.debug(stdout)
console.debug(stderr)
return code === 0 ? solve(stdout) : reject(stderr)
})
export async function run(command, options, {prefixed = true} = {}) {
const prefix = {win32:"wsl"}[process.platform] ?? ""
command = `${prefixed ? prefix : ""} ${command}`.trim()
return new Promise((solve, reject) => {
console.debug(`metrics/command > ${command}`)
const child = processes.exec(command, options)
let [stdout, stderr] = ["", ""]
child.stdout.on("data", data => stdout += data)
child.stderr.on("data", data => stderr += data)
child.on("close", code => {
console.debug(`metrics/command > ${command} > exited with code ${code}`)
console.debug(stdout)
console.debug(stderr)
return code === 0 ? solve(stdout) : reject(stderr)
})
}
})
}
/**Check command existance */
export async function which(command) {
try {
console.debug(`metrics/command > checking existence of ${command}`)
await run(`which ${command}`)
return true
}
catch {
console.debug(`metrics/command > checking existence of ${command} > failed`)
}
return false
export async function which(command) {
try {
console.debug(`metrics/command > checking existence of ${command}`)
await run(`which ${command}`)
return true
}
catch {
console.debug(`metrics/command > checking existence of ${command} > failed`)
}
return false
}
/**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(htmlunescape(htmlsanitize(text)), {
highlight(code, lang) {
return lang in prism.languages ? prism.highlight(code, prism.languages[lang]) : code
export async function markdown(text, {mode = "inline", codelines = Infinity} = {}) {
//Sanitize user input once to prevent injections and parse into markdown
let rendered = await marked(htmlunescape(htmlsanitize(text)), {
highlight(code, lang) {
return lang in prism.languages ? prism.highlight(code, prism.languages[lang]) : code
},
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"]},
}),
{
allowedAttributes:{code:["class"], span:["class"]},
transformTags:{h1:"b", h2:"b", h3:"b", h4:"b", h5:"b", h6:"b", blockquote:"i"},
},
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"]},
}), {
allowedAttributes:{code:["class"], span:["class"]},
transformTags:{h1:"b", h2:"b", h3:"b", h4:"b", h5:"b", h6:"b", blockquote:"i"},
})
break
}
default:
break
}
//Trim code snippets
rendered = rendered.replace(/(?<open><code[\s\S]*?>)(?<code>[\s\S]*?)(?<close><\/code>)/g, (m, open, code, close) => { //eslint-disable-line max-params
const lines = code.trim().split("\n")
if ((lines.length > 1)&&(!/class="[\s\S]*"/.test(open)))
open = open.replace(/>/g, ' class="language-multiline">')
return `${open}${lines.slice(0, codelines).join("\n")}${lines.length > codelines ? `\n<span class="token trimmed">(${lines.length-codelines} more ${lines.length-codelines === 1 ? "line was" : "lines were"} trimmed)</span>` : ""}${close}`
})
return rendered
)
break
}
default:
break
}
//Trim code snippets
rendered = rendered.replace(/(?<open><code[\s\S]*?>)(?<code>[\s\S]*?)(?<close><\/code>)/g, (m, open, code, close) => { //eslint-disable-line max-params
const lines = code.trim().split("\n")
if ((lines.length > 1) && (!/class="[\s\S]*"/.test(open)))
open = open.replace(/>/g, ' class="language-multiline">')
return `${open}${lines.slice(0, codelines).join("\n")}${lines.length > codelines ? `\n<span class="token trimmed">(${lines.length - codelines} more ${lines.length - codelines === 1 ? "line was" : "lines were"} trimmed)</span>` : ""}${close}`
})
return rendered
}
/**Check GitHub filter against object */
export function ghfilter(text, object) {
console.debug(`metrics/svg/ghquery > checking ${text} against ${JSON.stringify(object)}`)
const result = text.split(" ").map(x => x.trim()).filter(x => x).map(criteria => {
const [key, filters] = criteria.split(":")
const value = object[key]
console.debug(`metrics/svg/ghquery > checking ${criteria} against ${value}`)
return filters.split(",").map(x => x.trim()).filter(x => x).map(filter => {
switch (true) {
case /^>\d+$/.test(filter):
return value > Number(filter.substring(1))
case /^<\d+$/.test(filter):
return value < Number(filter.substring(1))
case /^\d+$/.test(filter):
return value === Number(filter)
case /^\d+..\d+$/.test(filter):{
const [a, b] = filter.split("..").map(Number)
return (value >= a)&&(value <= b)
}
default:
return false
export function ghfilter(text, object) {
console.debug(`metrics/svg/ghquery > checking ${text} against ${JSON.stringify(object)}`)
const result = text.split(" ").map(x => x.trim()).filter(x => x).map(criteria => {
const [key, filters] = criteria.split(":")
const value = object[key]
console.debug(`metrics/svg/ghquery > checking ${criteria} against ${value}`)
return filters.split(",").map(x => x.trim()).filter(x => x).map(filter => {
switch (true) {
case /^>\d+$/.test(filter):
return value > Number(filter.substring(1))
case /^<\d+$/.test(filter):
return value < Number(filter.substring(1))
case /^\d+$/.test(filter):
return value === Number(filter)
case /^\d+..\d+$/.test(filter): {
const [a, b] = filter.split("..").map(Number)
return (value >= a) && (value <= b)
}
}).reduce((a, b) => a||b, false)
}).reduce((a, b) => a&&b, true)
console.debug(`metrics/svg/ghquery > ${result ? "matching" : "not matching"}`)
return result
}
default:
return false
}
}).reduce((a, b) => a || b, false)
}).reduce((a, b) => a && b, true)
console.debug(`metrics/svg/ghquery > ${result ? "matching" : "not matching"}`)
return result
}
/**Image to base64 */
export async function imgb64(image, {width, height, fallback = true} = {}) {
//Undefined image
if (!image)
return fallback ? "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" : null
//Load image
image = await jimp.read(image)
//Resize image
if ((width)&&(height))
image = image.resize(width, height)
return image.getBase64Async(jimp.AUTO)
}
export async function imgb64(image, {width, height, fallback = true} = {}) {
//Undefined image
if (!image)
return fallback ? "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" : null
//Load image
image = await jimp.read(image)
//Resize image
if ((width) && (height))
image = image.resize(width, height)
return image.getBase64Async(jimp.AUTO)
}
/**SVG utils */
export const svg = {
/**Render as pdf */
async pdf(rendered, {paddings = "", style = "", twemojis = false, gemojis = false, rest = null} = {}) {
//Instantiate browser if needed
if (!svg.resize.browser) {
svg.resize.browser = await puppeteer.launch()
console.debug(`metrics/svg/pdf > started ${await svg.resize.browser.version()}`)
}
//Additional transformations
if (twemojis)
rendered = await svg.twemojis(rendered, {custom:false})
if ((gemojis)&&(rest))
rendered = await svg.gemojis(rendered, {rest})
rendered = marked(rendered)
//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"]})
console.debug("metrics/svg/pdf > loaded svg successfully")
await page.addStyleTag({content:`
export const svg = {
/**Render as pdf */
async pdf(rendered, {paddings = "", style = "", twemojis = false, gemojis = false, rest = null} = {}) {
//Instantiate browser if needed
if (!svg.resize.browser) {
svg.resize.browser = await puppeteer.launch()
console.debug(`metrics/svg/pdf > started ${await svg.resize.browser.version()}`)
}
//Additional transformations
if (twemojis)
rendered = await svg.twemojis(rendered, {custom:false})
if ((gemojis) && (rest))
rendered = await svg.gemojis(rendered, {rest})
rendered = marked(rendered)
//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"]})
console.debug("metrics/svg/pdf > loaded svg successfully")
await page.addStyleTag({
content:`
main { margin: ${(Array.isArray(paddings) ? paddings : paddings.split(",")).join(" ")}; }
main svg { height: 1em; width: 1em; }
${await fs.readFile(paths.join(__module(import.meta.url), "../../../node_modules", "@primer/css/dist/markdown.css")).catch(_ => "")}${style}
`})
rendered = await page.pdf()
`,
})
rendered = await page.pdf()
//Result
await page.close()
console.debug("metrics/svg/pdf > rendering complete")
return {rendered, mime:"application/pdf"}
},
/**Render and resize svg */
async resize(rendered, {paddings, convert}) {
//Instantiate browser if needed
if (!svg.resize.browser) {
svg.resize.browser = await puppeteer.launch()
console.debug(`metrics/svg/resize > started ${await svg.resize.browser.version()}`)
}
//Format padding
const [pw = 1, ph] = (Array.isArray(paddings) ? paddings : `${paddings}`.split(",").map(x => x.trim())).map(padding => `${padding}`.substring(0, padding.length - 1)).map(value => 1 + Number(value) / 100)
const padding = {width:pw, height:(ph ?? pw)}
if (!Number.isFinite(padding.width))
padding.width = 1
if (!Number.isFinite(padding.height))
padding.height = 1
console.debug(`metrics/svg/resize > padding width*${padding.width}, height*${padding.height}`)
//Render through browser and resize height
console.debug("metrics/svg/resize > loading svg")
const page = await svg.resize.browser.newPage()
page.on("console", ({_text:text}) => console.debug(`metrics/svg/resize > puppeteer > ${text}`))
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; }"})
let mime = "image/svg+xml"
console.debug("metrics/svg/resize > resizing svg")
let height, resized, width
try {
({resized, width, height} = await page.evaluate(async padding => {
//Disable animations
const animated = !document.querySelector("svg").classList.contains("no-animations")
if (animated)
document.querySelector("svg").classList.add("no-animations")
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()
console.debug(`bounds width=${width}, height=${height}`)
height = Math.ceil(height * padding.height)
width = Math.ceil(width * padding.width)
console.debug(`bounds after applying padding width=${width} (*${padding.width}), height=${height} (*${padding.height})`)
//Resize svg
document.querySelector("svg").setAttribute("height", height)
//Enable animations
if (animated)
document.querySelector("svg").classList.remove("no-animations")
//Result
await page.close()
console.debug("metrics/svg/pdf > rendering complete")
return {rendered, mime:"application/pdf"}
},
/**Render and resize svg */
async resize(rendered, {paddings, convert}) {
//Instantiate browser if needed
if (!svg.resize.browser) {
svg.resize.browser = await puppeteer.launch()
console.debug(`metrics/svg/resize > started ${await svg.resize.browser.version()}`)
}
//Format padding
const [pw = 1, ph] = (Array.isArray(paddings) ? paddings : `${paddings}`.split(",").map(x => x.trim())).map(padding => `${padding}`.substring(0, padding.length-1)).map(value => 1+Number(value)/100)
const padding = {width:pw, height:(ph ?? pw)}
if (!Number.isFinite(padding.width))
padding.width = 1
if (!Number.isFinite(padding.height))
padding.height = 1
console.debug(`metrics/svg/resize > padding width*${padding.width}, height*${padding.height}`)
//Render through browser and resize height
console.debug("metrics/svg/resize > loading svg")
const page = await svg.resize.browser.newPage()
page.on("console", ({_text:text}) => console.debug(`metrics/svg/resize > puppeteer > ${text}`))
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; }"})
let mime = "image/svg+xml"
console.debug("metrics/svg/resize > resizing svg")
let height, resized, width
try {
({resized, width, height} = await page.evaluate(async padding => {
//Disable animations
const animated = !document.querySelector("svg").classList.contains("no-animations")
if (animated)
document.querySelector("svg").classList.add("no-animations")
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()
console.debug(`bounds width=${width}, height=${height}`)
height = Math.ceil(height*padding.height)
width = Math.ceil(width*padding.width)
console.debug(`bounds after applying padding width=${width} (*${padding.width}), height=${height} (*${padding.height})`)
//Resize svg
document.querySelector("svg").setAttribute("height", height)
//Enable animations
if (animated)
document.querySelector("svg").classList.remove("no-animations")
//Result
return {resized:new XMLSerializer().serializeToString(document.querySelector("svg")), height, width}
}, padding))
}
catch (error) {
console.error(error)
console.debug(`metrics/svg/resize > an error occured: ${error}`)
throw error
}
//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})
mime = `image/${convert}`
}
//Result
await page.close()
console.debug("metrics/svg/resize > rendering complete")
return {resized, mime}
},
/**Render twemojis */
async twemojis(rendered, {custom = true} = {}) {
//Load emojis
console.debug("metrics/svg/twemojis > rendering twemojis")
const emojis = new Map()
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" '))
}
//Apply replacements
for (const [emoji, twemoji] of emojis) {
if (custom)
rendered = rendered.replace(new RegExp(`<metrics[ ]*(?<attributes>[^>]*)>${emoji}</metrics>`, "g"), twemoji.replace(/(<svg class="twemoji" [\s\S]+?)(>)/, "$1 $<attributes> $2"))
rendered = rendered.replace(new RegExp(emoji, "g"), twemoji)
}
return rendered
},
/**Render github emojis */
async gemojis(rendered, {rest}) {
//Load gemojis
console.debug("metrics/svg/gemojis > rendering gemojis")
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)))
emojis.set(emoji, `<img class="gemoji" src="${await imgb64(url)}" height="16" width="16" alt="">`)
}
}
catch (error) {
console.debug("metrics/svg/gemojis > could not load gemojis")
console.debug(error)
}
//Apply replacements
for (const [emoji, gemoji] of emojis)
rendered = rendered.replace(new RegExp(emoji, "g"), gemoji)
return rendered
},
}
return {resized:new XMLSerializer().serializeToString(document.querySelector("svg")), height, width}
}, padding))
}
catch (error) {
console.error(error)
console.debug(`metrics/svg/resize > an error occured: ${error}`)
throw error
}
//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})
mime = `image/${convert}`
}
//Result
await page.close()
console.debug("metrics/svg/resize > rendering complete")
return {resized, mime}
},
/**Render twemojis */
async twemojis(rendered, {custom = true} = {}) {
//Load emojis
console.debug("metrics/svg/twemojis > rendering twemojis")
const emojis = new Map()
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" '))
}
//Apply replacements
for (const [emoji, twemoji] of emojis) {
if (custom)
rendered = rendered.replace(new RegExp(`<metrics[ ]*(?<attributes>[^>]*)>${emoji}</metrics>`, "g"), twemoji.replace(/(<svg class="twemoji" [\s\S]+?)(>)/, "$1 $<attributes> $2"))
rendered = rendered.replace(new RegExp(emoji, "g"), twemoji)
}
return rendered
},
/**Render github emojis */
async gemojis(rendered, {rest}) {
//Load gemojis
console.debug("metrics/svg/gemojis > rendering gemojis")
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)))
emojis.set(emoji, `<img class="gemoji" src="${await imgb64(url)}" height="16" width="16" alt="">`)
}
}
catch (error) {
console.debug("metrics/svg/gemojis > could not load gemojis")
console.debug(error)
}
//Apply replacements
for (const [emoji, gemoji] of emojis)
rendered = rendered.replace(new RegExp(emoji, "g"), gemoji)
return rendered
},
}
/**Wait */
export async function wait(seconds) {
await new Promise(solve => setTimeout(solve, seconds*1000))
}
export async function wait(seconds) {
await new Promise(solve => setTimeout(solve, seconds * 1000))
}
/**Create record from puppeteer browser */
export async function record({page, width, height, frames, scale = 1, quality = 80, x = 0, y = 0, delay = 150, background = true}) {
//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}))
await wait(delay/1000)
if (i%10 === 0)
console.debug(`metrics/record > processed ${i}/${frames} frames`)
}
console.debug(`metrics/record > processed ${frames}/${frames} frames`)
//Post-processing
console.debug("metrics/record > applying post-processing")
return Promise.all(images.map(async buffer => (await jimp.read(buffer)).scale(scale).quality(quality).getBase64Async("image/png")))
export async function record({page, width, height, frames, scale = 1, quality = 80, x = 0, y = 0, delay = 150, background = true}) {
//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}))
await wait(delay / 1000)
if (i % 10 === 0)
console.debug(`metrics/record > processed ${i}/${frames} frames`)
}
console.debug(`metrics/record > processed ${frames}/${frames} frames`)
//Post-processing
console.debug("metrics/record > applying post-processing")
return Promise.all(images.map(async buffer => (await jimp.read(buffer)).scale(scale).quality(quality).getBase64Async("image/png")))
}
/**Create gif from puppeteer browser*/
export async function gif({page, width, height, frames, x = 0, y = 0, repeat = true, delay = 150, quality = 10}) {
//Create temporary stream
const path = paths.join(os.tmpdir(), `${Math.round(Math.random()*1000000000)}.gif`)
console.debug(`metrics/puppeteergif > set write stream to "${path}"`)
if (fss.existsSync(path))
await fs.unlink(path)
//Create encoder
const encoder = new GIFEncoder(width, height)
encoder.createWriteStream().pipe(fss.createWriteStream(path))
encoder.start()
encoder.setRepeat(repeat ? 0 : -1)
encoder.setDelay(delay)
encoder.setQuality(quality)
//Register frames
for (let i = 0; i < frames; i++) {
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`)
}
console.debug(`metrics/puppeteergif > processed ${frames}/${frames} frames`)
//Close encoder and convert to base64
encoder.finish()
const result = await fs.readFile(path, "base64")
await fs.unlink(path)
return `data:image/gif;base64,${result}`
}
export async function gif({page, width, height, frames, x = 0, y = 0, repeat = true, delay = 150, quality = 10}) {
//Create temporary stream
const path = paths.join(os.tmpdir(), `${Math.round(Math.random() * 1000000000)}.gif`)
console.debug(`metrics/puppeteergif > set write stream to "${path}"`)
if (fss.existsSync(path))
await fs.unlink(path)
//Create encoder
const encoder = new GIFEncoder(width, height)
encoder.createWriteStream().pipe(fss.createWriteStream(path))
encoder.start()
encoder.setRepeat(repeat ? 0 : -1)
encoder.setDelay(delay)
encoder.setQuality(quality)
//Register frames
for (let i = 0; i < frames; i++) {
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`)
}
console.debug(`metrics/puppeteergif > processed ${frames}/${frames} frames`)
//Close encoder and convert to base64
encoder.finish()
const result = await fs.readFile(path, "base64")
await fs.unlink(path)
return `data:image/gif;base64,${result}`
}

View File

@@ -1,66 +1,66 @@
/**Mocked data */
export default function({faker, url, options, login = faker.internet.userName()}) {
//Last.fm api
if (/^https:..ws.audioscrobbler.com.*$/.test(url)) {
//Get recently played tracks
if (/user.getrecenttracks/.test(url)) {
console.debug(`metrics/compute/mocks > mocking lastfm api result > ${url}`)
const artist = faker.random.word()
const album = faker.random.words(3)
const track = faker.random.words(5)
const date = faker.date.recent()
return ({
status:200,
data:{
recenttracks:{
"@attr":{
page:"1",
perPage:"1",
user:"RJ",
total:"100",
pages:"100",
},
track:[
{
artist:{
mbid:"",
"#text":artist,
},
album:{
mbid:"",
"#text":album,
},
image:[
{
size:"small",
"#text":faker.image.abstract(),
},
{
size:"medium",
"#text":faker.image.abstract(),
},
{
size:"large",
"#text":faker.image.abstract(),
},
{
size:"extralarge",
"#text":faker.image.abstract(),
},
],
streamable:"0",
date:{
uts:Math.floor(date.getTime() / 1000),
"#text":date.toUTCString().slice(5, 22),
},
url:faker.internet.url(),
name:track,
mbid:"",
},
],
export default function({faker, url, options, login = faker.internet.userName()}) {
//Last.fm api
if (/^https:..ws.audioscrobbler.com.*$/.test(url)) {
//Get recently played tracks
if (/user.getrecenttracks/.test(url)) {
console.debug(`metrics/compute/mocks > mocking lastfm api result > ${url}`)
const artist = faker.random.word()
const album = faker.random.words(3)
const track = faker.random.words(5)
const date = faker.date.recent()
return ({
status:200,
data:{
recenttracks:{
"@attr":{
page:"1",
perPage:"1",
user:"RJ",
total:"100",
pages:"100",
},
track:[
{
artist:{
mbid:"",
"#text":artist,
},
album:{
mbid:"",
"#text":album,
},
image:[
{
size:"small",
"#text":faker.image.abstract(),
},
{
size:"medium",
"#text":faker.image.abstract(),
},
{
size:"large",
"#text":faker.image.abstract(),
},
{
size:"extralarge",
"#text":faker.image.abstract(),
},
],
streamable:"0",
date:{
uts:Math.floor(date.getTime() / 1000),
"#text":date.toUTCString().slice(5, 22),
},
url:faker.internet.url(),
name:track,
mbid:"",
},
})
}
}
],
},
},
})
}
}
}

View File

@@ -1,28 +1,28 @@
/**Mocked data */
export default function({faker, url}) {
//Last.fm api
if (/^https:..testapp.herokuapp.com.*$/.test(url)) {
//Get Nightscout Data
console.debug(`metrics/compute/mocks > mocking nightscout api result > ${url}`)
const lastInterval = Math.floor(new Date() / 300000) * 300000
return ({
status:200,
data:new Array(12).fill(null).map(_ => ({
_id:faker.git.commitSha().substring(0, 23),
device:"xDrip-DexcomG5",
date:lastInterval,
dateString:new Date(lastInterval).toISOString(),
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"]),
type:"sgv",
filtered:0,
unfiltered:0,
rssi:100,
noise:1,
sysTime:new Date(lastInterval).toISOString(),
utcOffset:faker.datatype.number({min:-12, max:14})*60,
})),
})
}
}
//Last.fm api
if (/^https:..testapp.herokuapp.com.*$/.test(url)) {
//Get Nightscout Data
console.debug(`metrics/compute/mocks > mocking nightscout api result > ${url}`)
const lastInterval = Math.floor(new Date() / 300000) * 300000
return ({
status:200,
data:new Array(12).fill(null).map(_ => ({
_id:faker.git.commitSha().substring(0, 23),
device:"xDrip-DexcomG5",
date:lastInterval,
dateString:new Date(lastInterval).toISOString(),
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"]),
type:"sgv",
filtered:0,
unfiltered:0,
rssi:100,
noise:1,
sysTime:new Date(lastInterval).toISOString(),
utcOffset:faker.datatype.number({min:-12, max:14}) * 60,
})),
})
}
}

View File

@@ -1,105 +1,105 @@
/**Mocked data */
export default function({faker, url, options, login = faker.internet.userName()}) {
//Tested url
const tested = url.match(/&url=(?<tested>.*?)(?:&|$)/)?.groups?.tested ?? faker.internet.url()
//Pagespeed api
if (/^https:..www.googleapis.com.pagespeedonline.v5.*$/.test(url)) {
//Pagespeed result
if (/v5.runPagespeed.*&key=MOCKED_TOKEN/.test(url)) {
console.debug(`metrics/compute/mocks > mocking pagespeed api result > ${url}`)
return ({
status:200,
data:{
captchaResult:"CAPTCHA_NOT_NEEDED",
id:tested,
lighthouseResult:{
requestedUrl:tested,
finalUrl:tested,
lighthouseVersion:"6.3.0",
audits:{
"final-screenshot":{
id:"final-screenshot",
title:"Final Screenshot",
score:null,
details:{
data:null,
type:"screenshot",
timestamp:Date.now(),
},
},
metrics:{
id:"metrics",
title:"Metrics",
score:null,
details:{
items:[
{
observedFirstContentfulPaint:faker.datatype.number(500),
observedFirstVisualChangeTs:faker.time.recent(),
observedFirstContentfulPaintTs:faker.time.recent(),
firstContentfulPaint:faker.datatype.number(500),
observedDomContentLoaded:faker.datatype.number(500),
observedFirstMeaningfulPaint:faker.datatype.number(1000),
maxPotentialFID:faker.datatype.number(500),
observedLoad:faker.datatype.number(500),
firstMeaningfulPaint:faker.datatype.number(500),
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}),
observedFirstPaintTs:faker.time.recent(),
observedTraceEndTs:faker.time.recent(),
largestContentfulPaint:faker.datatype.number(2000),
observedTimeOrigin:faker.datatype.number(10),
speedIndex:faker.datatype.number(1000),
observedTraceEnd:faker.datatype.number(2000),
observedDomContentLoadedTs:faker.time.recent(),
observedFirstPaint:faker.datatype.number(500),
totalBlockingTime:faker.datatype.number(500),
observedLastVisualChangeTs:faker.time.recent(),
observedFirstVisualChange:faker.datatype.number(500),
observedLargestContentfulPaintTs:faker.time.recent(),
estimatedInputLatency:faker.datatype.number(100),
observedLoadTs:faker.time.recent(),
observedLastVisualChange:faker.datatype.number(1000),
firstCPUIdle:faker.datatype.number(1000),
interactive:faker.datatype.number(1000),
observedNavigationStartTs:faker.time.recent(),
observedNavigationStart:faker.datatype.number(10),
observedFirstMeaningfulPaintTs:faker.time.recent(),
},
],
},
},
},
categories:{
"best-practices":{
id:"best-practices",
title:"Best Practices",
score:faker.datatype.float({max:1}),
},
seo:{
id:"seo",
title:"SEO",
score:faker.datatype.float({max:1}),
},
accessibility:{
id:"accessibility",
title:"Accessibility",
score:faker.datatype.float({max:1}),
},
performance:{
id:"performance",
title:"Performance",
score:faker.datatype.float({max:1}),
},
},
export default function({faker, url, options, login = faker.internet.userName()}) {
//Tested url
const tested = url.match(/&url=(?<tested>.*?)(?:&|$)/)?.groups?.tested ?? faker.internet.url()
//Pagespeed api
if (/^https:..www.googleapis.com.pagespeedonline.v5.*$/.test(url)) {
//Pagespeed result
if (/v5.runPagespeed.*&key=MOCKED_TOKEN/.test(url)) {
console.debug(`metrics/compute/mocks > mocking pagespeed api result > ${url}`)
return ({
status:200,
data:{
captchaResult:"CAPTCHA_NOT_NEEDED",
id:tested,
lighthouseResult:{
requestedUrl:tested,
finalUrl:tested,
lighthouseVersion:"6.3.0",
audits:{
"final-screenshot":{
id:"final-screenshot",
title:"Final Screenshot",
score:null,
details:{
data:null,
type:"screenshot",
timestamp:Date.now(),
},
analysisUTCTimestamp:`${faker.date.recent()}`,
},
})
}
}
metrics:{
id:"metrics",
title:"Metrics",
score:null,
details:{
items:[
{
observedFirstContentfulPaint:faker.datatype.number(500),
observedFirstVisualChangeTs:faker.time.recent(),
observedFirstContentfulPaintTs:faker.time.recent(),
firstContentfulPaint:faker.datatype.number(500),
observedDomContentLoaded:faker.datatype.number(500),
observedFirstMeaningfulPaint:faker.datatype.number(1000),
maxPotentialFID:faker.datatype.number(500),
observedLoad:faker.datatype.number(500),
firstMeaningfulPaint:faker.datatype.number(500),
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}),
observedFirstPaintTs:faker.time.recent(),
observedTraceEndTs:faker.time.recent(),
largestContentfulPaint:faker.datatype.number(2000),
observedTimeOrigin:faker.datatype.number(10),
speedIndex:faker.datatype.number(1000),
observedTraceEnd:faker.datatype.number(2000),
observedDomContentLoadedTs:faker.time.recent(),
observedFirstPaint:faker.datatype.number(500),
totalBlockingTime:faker.datatype.number(500),
observedLastVisualChangeTs:faker.time.recent(),
observedFirstVisualChange:faker.datatype.number(500),
observedLargestContentfulPaintTs:faker.time.recent(),
estimatedInputLatency:faker.datatype.number(100),
observedLoadTs:faker.time.recent(),
observedLastVisualChange:faker.datatype.number(1000),
firstCPUIdle:faker.datatype.number(1000),
interactive:faker.datatype.number(1000),
observedNavigationStartTs:faker.time.recent(),
observedNavigationStart:faker.datatype.number(10),
observedFirstMeaningfulPaintTs:faker.time.recent(),
},
],
},
},
},
categories:{
"best-practices":{
id:"best-practices",
title:"Best Practices",
score:faker.datatype.float({max:1}),
},
seo:{
id:"seo",
title:"SEO",
score:faker.datatype.float({max:1}),
},
accessibility:{
id:"accessibility",
title:"Accessibility",
score:faker.datatype.float({max:1}),
},
performance:{
id:"performance",
title:"Performance",
score:faker.datatype.float({max:1}),
},
},
},
analysisUTCTimestamp:`${faker.date.recent()}`,
},
})
}
}
}

View File

@@ -1,65 +1,65 @@
/**Mocked data */
export default function({faker, url, options, login = faker.internet.userName()}) {
//Spotify api
if (/^https:..api.spotify.com.*$/.test(url)) {
//Get recently played tracks
if (/me.player.recently-played/.test(url)&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN_ACCESS")) {
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
const artist = faker.random.words()
const track = faker.random.words(5)
return ({
status:200,
data:{
items:[
export default function({faker, url, options, login = faker.internet.userName()}) {
//Spotify api
if (/^https:..api.spotify.com.*$/.test(url)) {
//Get recently played tracks
if (/me.player.recently-played/.test(url) && (options?.headers?.Authorization === "Bearer MOCKED_TOKEN_ACCESS")) {
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
const artist = faker.random.words()
const track = faker.random.words(5)
return ({
status:200,
data:{
items:[
{
track:{
album:{
album_type:"single",
artists:[
{
track:{
album:{
album_type:"single",
artists:[
{
name:artist,
type:"artist",
},
],
images:[
{
height:640,
url:faker.image.abstract(),
width:640,
},
{
height:300,
url:faker.image.abstract(),
width:300,
},
{
height:64,
url:faker.image.abstract(),
width:64,
},
],
name:track,
release_date:`${faker.date.past()}`.substring(0, 10),
type:"album",
},
artists:[
{
name:artist,
type:"artist",
},
],
name:track,
preview_url:faker.internet.url(),
type:"track",
},
played_at:`${faker.date.recent()}`,
context:{
type:"album",
},
name:artist,
type:"artist",
},
],
images:[
{
height:640,
url:faker.image.abstract(),
width:640,
},
{
height:300,
url:faker.image.abstract(),
width:300,
},
{
height:64,
url:faker.image.abstract(),
width:64,
},
],
name:track,
release_date:`${faker.date.past()}`.substring(0, 10),
type:"album",
},
})
}
}
artists:[
{
name:artist,
type:"artist",
},
],
name:track,
preview_url:faker.internet.url(),
type:"track",
},
played_at:`${faker.date.recent()}`,
context:{
type:"album",
},
},
],
},
})
}
}
}

View File

@@ -1,99 +1,99 @@
/**Mocked data */
export default function({faker, url, options, login = faker.internet.userName()}) {
//Stackoverflow api
if (/^https:..api.stackexchange.com.2.2.*$/.test(url)) {
//Extract user id
const user_id = url.match(/[/]users[/](?<id>\d+)/)?.groups?.id ?? NaN
const pagesize = Number(url.match(/pagesize=(?<pagesize>\d+)/)?.groups?.pagesize) || 30
//User account
if (/users[/]\d+[/][?]site=stackoverflow$/.test(url)) {
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
return ({
status:200,
data:{
items:[
{
badge_counts:{bronze:faker.datatype.number(500), silver:faker.datatype.number(300), gold:faker.datatype.number(100)},
accept_rate:faker.datatype.number(100),
answer_count:faker.datatype.number(1000),
question_count:faker.datatype.number(1000),
view_count:faker.datatype.number(10000),
creation_date:faker.date.past(),
display_name:faker.internet.userName(),
user_id,
reputation:faker.datatype.number(100000),
},
],
has_more:false,
quota_max:300,
quota_remaining:faker.datatype.number(300),
},
})
}
//Total metrics
if (/[?]site=stackoverflow&filter=total$/.test(url)) {
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
return ({
status:200,
data:{
total:faker.datatype.number(10000),
},
})
}
//Questions
if ((/questions[?]site=stackoverflow/.test(url))||(/questions[/][\d;]+[?]site=stackoverflow/.test(url))) {
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
return ({
status:200,
data:{
items:new Array(pagesize).fill(null).map(_ => ({
tags:new Array(5).fill(null).map(_ => faker.lorem.slug()),
owner:{display_name:faker.internet.userName()},
is_answered:faker.datatype.boolean(),
view_count:faker.datatype.number(10000),
accepted_answer_id:faker.datatype.number(1000000),
answer_count:faker.datatype.number(100),
score:faker.datatype.number(1000),
creation_date:faker.time.recent(),
down_vote_count:faker.datatype.number(1000),
up_vote_count:faker.datatype.number(1000),
comment_count:faker.datatype.number(1000),
favorite_count:faker.datatype.number(1000),
title:faker.lorem.sentence(),
body_markdown:faker.lorem.paragraphs(),
link:faker.internet.url(),
question_id:faker.datatype.number(1000000),
})),
has_more:false,
quota_max:300,
quota_remaining:faker.datatype.number(300),
},
})
}
//Answers
if ((/answers[?]site=stackoverflow/.test(url))||(/answers[/][\d;]+[?]site=stackoverflow/.test(url))) {
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
return ({
status:200,
data:{
items:new Array(pagesize).fill(null).map(_ => ({
owner:{display_name:faker.internet.userName()},
link:faker.internet.url(),
is_accepted:faker.datatype.boolean(),
score:faker.datatype.number(1000),
down_vote_count:faker.datatype.number(1000),
up_vote_count:faker.datatype.number(1000),
comment_count:faker.datatype.number(1000),
creation_date:faker.time.recent(),
question_id:faker.datatype.number(1000000),
body_markdown:faker.lorem.paragraphs(),
answer_id:faker.datatype.number(1000000),
})),
has_more:false,
quota_max:300,
quota_remaining:faker.datatype.number(300),
},
})
}
}
export default function({faker, url, options, login = faker.internet.userName()}) {
//Stackoverflow api
if (/^https:..api.stackexchange.com.2.2.*$/.test(url)) {
//Extract user id
const user_id = url.match(/[/]users[/](?<id>\d+)/)?.groups?.id ?? NaN
const pagesize = Number(url.match(/pagesize=(?<pagesize>\d+)/)?.groups?.pagesize) || 30
//User account
if (/users[/]\d+[/][?]site=stackoverflow$/.test(url)) {
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
return ({
status:200,
data:{
items:[
{
badge_counts:{bronze:faker.datatype.number(500), silver:faker.datatype.number(300), gold:faker.datatype.number(100)},
accept_rate:faker.datatype.number(100),
answer_count:faker.datatype.number(1000),
question_count:faker.datatype.number(1000),
view_count:faker.datatype.number(10000),
creation_date:faker.date.past(),
display_name:faker.internet.userName(),
user_id,
reputation:faker.datatype.number(100000),
},
],
has_more:false,
quota_max:300,
quota_remaining:faker.datatype.number(300),
},
})
}
//Total metrics
if (/[?]site=stackoverflow&filter=total$/.test(url)) {
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
return ({
status:200,
data:{
total:faker.datatype.number(10000),
},
})
}
//Questions
if ((/questions[?]site=stackoverflow/.test(url)) || (/questions[/][\d;]+[?]site=stackoverflow/.test(url))) {
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
return ({
status:200,
data:{
items:new Array(pagesize).fill(null).map(_ => ({
tags:new Array(5).fill(null).map(_ => faker.lorem.slug()),
owner:{display_name:faker.internet.userName()},
is_answered:faker.datatype.boolean(),
view_count:faker.datatype.number(10000),
accepted_answer_id:faker.datatype.number(1000000),
answer_count:faker.datatype.number(100),
score:faker.datatype.number(1000),
creation_date:faker.time.recent(),
down_vote_count:faker.datatype.number(1000),
up_vote_count:faker.datatype.number(1000),
comment_count:faker.datatype.number(1000),
favorite_count:faker.datatype.number(1000),
title:faker.lorem.sentence(),
body_markdown:faker.lorem.paragraphs(),
link:faker.internet.url(),
question_id:faker.datatype.number(1000000),
})),
has_more:false,
quota_max:300,
quota_remaining:faker.datatype.number(300),
},
})
}
//Answers
if ((/answers[?]site=stackoverflow/.test(url)) || (/answers[/][\d;]+[?]site=stackoverflow/.test(url))) {
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
return ({
status:200,
data:{
items:new Array(pagesize).fill(null).map(_ => ({
owner:{display_name:faker.internet.userName()},
link:faker.internet.url(),
is_accepted:faker.datatype.boolean(),
score:faker.datatype.number(1000),
down_vote_count:faker.datatype.number(1000),
up_vote_count:faker.datatype.number(1000),
comment_count:faker.datatype.number(1000),
creation_date:faker.time.recent(),
question_id:faker.datatype.number(1000000),
body_markdown:faker.lorem.paragraphs(),
answer_id:faker.datatype.number(1000000),
})),
has_more:false,
quota_max:300,
quota_remaining:faker.datatype.number(300),
},
})
}
}
}

View File

@@ -1,64 +1,64 @@
/**Mocked data */
export default function({faker, url, options, login = faker.internet.userName()}) {
//Twitter api
if (/^https:..api.twitter.com.*$/.test(url)) {
//Get user profile
if ((/users.by.username/.test(url))&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
const username = url.match(/username[/](?<username>.*?)[?]/)?.groups?.username ?? faker.internet.userName()
return ({
status:200,
data:{
data:{
profile_image_url:faker.image.people(),
name:faker.name.findName(),
verified:faker.datatype.boolean(),
id:faker.datatype.number(1000000).toString(),
username,
},
},
})
}
//Get recent tweets
if ((/tweets.search.recent/.test(url))&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
return ({
status:200,
data:{
data:[
{
id:faker.datatype.number(100000000000000).toString(),
created_at:`${faker.date.recent()}`,
entities:{
mentions:[
{start:22, end:33, username:"lowlighter"},
],
},
text:"Checkout metrics from @lowlighter ! #GitHub",
},
{
id:faker.datatype.number(100000000000000).toString(),
created_at:`${faker.date.recent()}`,
text:faker.lorem.paragraph(),
},
export default function({faker, url, options, login = faker.internet.userName()}) {
//Twitter api
if (/^https:..api.twitter.com.*$/.test(url)) {
//Get user profile
if ((/users.by.username/.test(url)) && (options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
const username = url.match(/username[/](?<username>.*?)[?]/)?.groups?.username ?? faker.internet.userName()
return ({
status:200,
data:{
data:{
profile_image_url:faker.image.people(),
name:faker.name.findName(),
verified:faker.datatype.boolean(),
id:faker.datatype.number(1000000).toString(),
username,
},
},
})
}
//Get recent tweets
if ((/tweets.search.recent/.test(url)) && (options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
return ({
status:200,
data:{
data:[
{
id:faker.datatype.number(100000000000000).toString(),
created_at:`${faker.date.recent()}`,
entities:{
mentions:[
{start:22, end:33, username:"lowlighter"},
],
includes:{
users:[
{
id:faker.datatype.number(100000000000000).toString(),
name:"lowlighter",
username:"lowlighter",
},
],
},
meta:{
newest_id:faker.datatype.number(100000000000000).toString(),
oldest_id:faker.datatype.number(100000000000000).toString(),
result_count:2,
next_token:"MOCKED_CURSOR",
},
},
})
}
}
text:"Checkout metrics from @lowlighter ! #GitHub",
},
{
id:faker.datatype.number(100000000000000).toString(),
created_at:`${faker.date.recent()}`,
text:faker.lorem.paragraph(),
},
],
includes:{
users:[
{
id:faker.datatype.number(100000000000000).toString(),
name:"lowlighter",
username:"lowlighter",
},
],
},
meta:{
newest_id:faker.datatype.number(100000000000000).toString(),
oldest_id:faker.datatype.number(100000000000000).toString(),
result_count:2,
next_token:"MOCKED_CURSOR",
},
},
})
}
}
}

View File

@@ -1,52 +1,54 @@
/**Mocked data */
export default function({faker, url, options, login = faker.internet.userName()}) {
//Wakatime api
if (/^https:..wakatime.com.api.v1.users..*.stats.*$/.test(url)) {
//Get user profile
if (/api_key=MOCKED_TOKEN/.test(url)) {
console.debug(`metrics/compute/mocks > mocking wakatime api result > ${url}`)
const stats = array => {
const elements = []
let results = new Array(4+faker.datatype.number(2)).fill(null).map(_ => ({
get digital() {
return `${this.hours}:${this.minutes}`
},
hours:faker.datatype.number(1000), minutes:faker.datatype.number(1000),
name:array ? faker.random.arrayElement(array) : faker.random.words(2).replace(/ /g, "-").toLocaleLowerCase(),
percent:0, total_seconds:faker.datatype.number(1000000),
}))
results = results.filter(({name}) => elements.includes(name) ? false : (elements.push(name), true))
let percents = 100
for (const result of results) {
result.percent = 1+faker.datatype.number(percents-1)
percents -= result.percent
}
return results
}
return ({
status:200,
data:{
data:{
best_day:{
created_at:faker.date.recent(),
date:`${faker.date.recent()}`.substring(0, 10),
total_seconds:faker.datatype.number(1000000),
},
categories:stats(),
daily_average:faker.datatype.number(12*60*60),
daily_average_including_other_language:faker.datatype.number(12*60*60),
dependencies:stats(),
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"]),
machines:stats(),
operating_systems:stats(["Mac", "Windows", "Linux"]),
project:null,
projects:stats(),
total_seconds:faker.datatype.number(1000000000),
total_seconds_including_other_language:faker.datatype.number(1000000000),
},
},
})
}
export default function({faker, url, options, login = faker.internet.userName()}) {
//Wakatime api
if (/^https:..wakatime.com.api.v1.users..*.stats.*$/.test(url)) {
//Get user profile
if (/api_key=MOCKED_TOKEN/.test(url)) {
console.debug(`metrics/compute/mocks > mocking wakatime api result > ${url}`)
const stats = array => {
const elements = []
let results = new Array(4 + faker.datatype.number(2)).fill(null).map(_ => ({
get digital() {
return `${this.hours}:${this.minutes}`
},
hours:faker.datatype.number(1000),
minutes:faker.datatype.number(1000),
name:array ? faker.random.arrayElement(array) : faker.random.words(2).replace(/ /g, "-").toLocaleLowerCase(),
percent:0,
total_seconds:faker.datatype.number(1000000),
}))
results = results.filter(({name}) => elements.includes(name) ? false : (elements.push(name), true))
let percents = 100
for (const result of results) {
result.percent = 1 + faker.datatype.number(percents - 1)
percents -= result.percent
}
return results
}
return ({
status:200,
data:{
data:{
best_day:{
created_at:faker.date.recent(),
date:`${faker.date.recent()}`.substring(0, 10),
total_seconds:faker.datatype.number(1000000),
},
categories:stats(),
daily_average:faker.datatype.number(12 * 60 * 60),
daily_average_including_other_language:faker.datatype.number(12 * 60 * 60),
dependencies:stats(),
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"]),
machines:stats(),
operating_systems:stats(["Mac", "Windows", "Linux"]),
project:null,
projects:stats(),
total_seconds:faker.datatype.number(1000000000),
total_seconds_including_other_language:faker.datatype.number(1000000000),
},
},
})
}
}
}

View File

@@ -1,75 +1,75 @@
/**Mocked data */
export default function({faker, url, options, login = faker.internet.userName()}) {
//Wakatime api
if (/^https:..apidojo-yahoo-finance-v1.p.rapidapi.com.stock.v2.*$/.test(url)) {
//Get company profile
if (/get-profile/.test(url)) {
console.debug(`metrics/compute/mocks > mocking yahoo finance api result > ${url}`)
return ({
status:200,
data:{
price:{
marketCap:{
raw:faker.datatype.number(1000000000),
},
export default function({faker, url, options, login = faker.internet.userName()}) {
//Wakatime api
if (/^https:..apidojo-yahoo-finance-v1.p.rapidapi.com.stock.v2.*$/.test(url)) {
//Get company profile
if (/get-profile/.test(url)) {
console.debug(`metrics/compute/mocks > mocking yahoo finance api result > ${url}`)
return ({
status:200,
data:{
price:{
marketCap:{
raw:faker.datatype.number(1000000000),
},
symbol:"OCTO",
},
quoteType:{
shortName:faker.company.companyName(),
longName:faker.company.companyName(),
exchangeTimezoneName:faker.address.timeZone(),
symbol:"OCTO",
},
calendarEvents:{},
summaryDetail:{},
symbol:"OCTO",
assetProfile:{
fullTimeEmployees:faker.datatype.number(10000),
city:faker.address.city(),
country:faker.address.country(),
},
},
})
}
//Get stock chart
if (/get-chart/.test(url)) {
console.debug(`metrics/compute/mocks > mocking yahoo finance api result > ${url}`)
return ({
status:200,
data:{
chart:{
result:[
{
meta:{
currency:"USD",
symbol:"OCTO",
regularMarketPrice:faker.datatype.number(10000) / 100,
chartPreviousClose:faker.datatype.number(10000) / 100,
previousClose:faker.datatype.number(10000) / 100,
},
quoteType:{
shortName:faker.company.companyName(),
longName:faker.company.companyName(),
exchangeTimezoneName:faker.address.timeZone(),
symbol:"OCTO",
},
calendarEvents:{},
summaryDetail:{},
symbol:"OCTO",
assetProfile:{
fullTimeEmployees:faker.datatype.number(10000),
city:faker.address.city(),
country:faker.address.country(),
},
},
})
}
//Get stock chart
if (/get-chart/.test(url)) {
console.debug(`metrics/compute/mocks > mocking yahoo finance api result > ${url}`)
return ({
status:200,
data:{
chart:{
result:[
timestamp:new Array(1000).fill(Date.now()).map((x, i) => x + i * 60000),
indicators:{
quote:[
{
meta:{
currency:"USD",
symbol:"OCTO",
regularMarketPrice:faker.datatype.number(10000)/100,
chartPreviousClose:faker.datatype.number(10000)/100,
previousClose:faker.datatype.number(10000)/100,
close:new Array(1000).fill(null).map(_ => faker.datatype.number(10000) / 100),
get low() {
return this.close
},
timestamp:new Array(1000).fill(Date.now()).map((x, i) => x+i*60000),
indicators:{
quote:[
{
close:new Array(1000).fill(null).map(_ => faker.datatype.number(10000)/100),
get low() {
return this.close
},
get high() {
return this.close
},
get open() {
return this.close
},
volume:[],
},
],
get high() {
return this.close
},
get open() {
return this.close
},
volume:[],
},
],
},
},
})
}
}
}
],
},
},
})
}
}
}

View File

@@ -1,123 +1,122 @@
/**Mocked data */
export default function({faker, url, body, login = faker.internet.userName()}) {
if (/^https:..graphql.anilist.co.*$/.test(url)) {
//Initialization and media generator
const {query} = body
const media = ({type}) => ({
title:{romaji:faker.lorem.words(), english:faker.lorem.words(), native:faker.lorem.words()},
description:faker.lorem.paragraphs(),
type,
status:faker.random.arrayElement(["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]),
episodes:100+faker.datatype.number(100),
volumes:faker.datatype.number(100),
chapters:100+faker.datatype.number(1000),
averageScore:faker.datatype.number(100),
countryOfOrigin:"JP",
genres:new Array(6).fill(null).map(_ => faker.lorem.word()),
coverImage:{medium:null},
startDate:{year:faker.date.past(20).getFullYear()},
})
//User statistics query
if (/^query Statistics /.test(query)) {
console.debug("metrics/compute/mocks > mocking anilist api result > Statistics")
return ({
status:200,
data:{
data:{
User:{
id:faker.datatype.number(100000),
name:faker.internet.userName(),
about:null,
statistics:{
anime:{
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()})),
},
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()})),
},
},
export default function({faker, url, body, login = faker.internet.userName()}) {
if (/^https:..graphql.anilist.co.*$/.test(url)) {
//Initialization and media generator
const {query} = body
const media = ({type}) => ({
title:{romaji:faker.lorem.words(), english:faker.lorem.words(), native:faker.lorem.words()},
description:faker.lorem.paragraphs(),
type,
status:faker.random.arrayElement(["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]),
episodes:100 + faker.datatype.number(100),
volumes:faker.datatype.number(100),
chapters:100 + faker.datatype.number(1000),
averageScore:faker.datatype.number(100),
countryOfOrigin:"JP",
genres:new Array(6).fill(null).map(_ => faker.lorem.word()),
coverImage:{medium:null},
startDate:{year:faker.date.past(20).getFullYear()},
})
//User statistics query
if (/^query Statistics /.test(query)) {
console.debug("metrics/compute/mocks > mocking anilist api result > Statistics")
return ({
status:200,
data:{
data:{
User:{
id:faker.datatype.number(100000),
name:faker.internet.userName(),
about:null,
statistics:{
anime:{
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()})),
},
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()})),
},
},
},
})
}
//Favorites characters
if (/^query FavoritesCharacters /.test(query)) {
console.debug("metrics/compute/mocks > mocking anilist api result > Favorites characters")
return ({
status:200,
data:{
data:{
User:{
favourites:{
characters:{
nodes:new Array(2+faker.datatype.number(16)).fill(null).map(_ => ({
name:{full:faker.name.findName(), native:faker.name.findName()},
image:{medium:null},
})),
pageInfo:{currentPage:1, hasNextPage:false},
},
},
},
},
})
}
//Favorites characters
if (/^query FavoritesCharacters /.test(query)) {
console.debug("metrics/compute/mocks > mocking anilist api result > Favorites characters")
return ({
status:200,
data:{
data:{
User:{
favourites:{
characters:{
nodes:new Array(2 + faker.datatype.number(16)).fill(null).map(_ => ({
name:{full:faker.name.findName(), native:faker.name.findName()},
image:{medium:null},
})),
pageInfo:{currentPage:1, hasNextPage:false},
},
},
},
})
}
//Favorites anime/manga query
if (/^query Favorites /.test(query)) {
console.debug("metrics/compute/mocks > mocking anilist api result > Favorites")
const type = /anime[(]/.test(query) ? "ANIME" : /manga[(]/.test(query) ? "MANGA" : "OTHER"
return ({
status:200,
data:{
data:{
User:{
favourites:{
[type.toLocaleLowerCase()]:{
nodes:new Array(16).fill(null).map(_ => media({type})),
pageInfo:{currentPage:1, hasNextPage:false},
},
},
},
},
})
}
//Favorites anime/manga query
if (/^query Favorites /.test(query)) {
console.debug("metrics/compute/mocks > mocking anilist api result > Favorites")
const type = /anime[(]/.test(query) ? "ANIME" : /manga[(]/.test(query) ? "MANGA" : "OTHER"
return ({
status:200,
data:{
data:{
User:{
favourites:{
[type.toLocaleLowerCase()]:{
nodes:new Array(16).fill(null).map(_ => media({type})),
pageInfo:{currentPage:1, hasNextPage:false},
},
},
},
})
}
//Medias query
if (/^query Medias /.test(query)) {
console.debug("metrics/compute/mocks > mocking anilist api result > Medias")
const {type} = body.variables
return ({
status:200,
data:{
data:{
MediaListCollection:{
lists:[
{
name:{ANIME:"Watching", MANGA:"Reading", OTHER:"Completed"}[type],
isCustomList:false,
entries:new Array(16).fill(null).map(_ => ({
status:faker.random.arrayElement(["CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"]),
progress:faker.datatype.number(100),
progressVolumes:null,
score:0,
startedAt:{year:null, month:null, day:null},
completedAt:{year:null, month:null, day:null},
media:media({type}),
})),
},
],
},
},
})
}
//Medias query
if (/^query Medias /.test(query)) {
console.debug("metrics/compute/mocks > mocking anilist api result > Medias")
const {type} = body.variables
return ({
status:200,
data:{
data:{
MediaListCollection:{
lists:[
{
name:{ANIME:"Watching", MANGA:"Reading", OTHER:"Completed"}[type],
isCustomList:false,
entries:new Array(16).fill(null).map(_ => ({
status:faker.random.arrayElement(["CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"]),
progress:faker.datatype.number(100),
progressVolumes:null,
score:0,
startedAt:{year:null, month:null, day:null},
completedAt:{year:null, month:null, day:null},
media:media({type}),
})),
},
},
],
},
})
}
},
},
})
}
}
}

View File

@@ -1,23 +1,23 @@
/**Mocked data */
export default function({faker, url, body, login = faker.internet.userName()}) {
if (/^https:..api.hashnode.com.*$/.test(url)) {
console.debug(`metrics/compute/mocks > mocking hashnode result > ${url}`)
return ({
status:200,
data:{
data:{
user:{
publication:{
posts:new Array(30).fill(null).map(_ => ({
title:faker.lorem.sentence(),
brief:faker.lorem.paragraph(),
coverImage:null,
dateAdded:faker.date.recent(),
})),
},
},
export default function({faker, url, body, login = faker.internet.userName()}) {
if (/^https:..api.hashnode.com.*$/.test(url)) {
console.debug(`metrics/compute/mocks > mocking hashnode result > ${url}`)
return ({
status:200,
data:{
data:{
user:{
publication:{
posts:new Array(30).fill(null).map(_ => ({
title:faker.lorem.sentence(),
brief:faker.lorem.paragraph(),
coverImage:null,
dateAdded:faker.date.recent(),
})),
},
},
})
}
},
},
})
}
}

View File

@@ -1,22 +1,22 @@
//Imports
import urls from "url"
import urls from "url"
/**Mocked data */
export default function({faker, url, body, login = faker.internet.userName()}) {
if (/^https:..accounts.spotify.com.api.token.*$/.test(url)) {
//Access token generator
const params = new urls.URLSearchParams(body)
if ((params.get("grant_type") === "refresh_token")&&(params.get("client_id") === "MOCKED_CLIENT_ID")&&(params.get("client_secret") === "MOCKED_CLIENT_SECRET")&&(params.get("refresh_token") === "MOCKED_REFRESH_TOKEN")) {
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
return ({
status:200,
data:{
access_token:"MOCKED_TOKEN_ACCESS",
token_type:"Bearer",
expires_in:3600,
scope:"user-read-recently-played user-read-private",
},
})
}
export default function({faker, url, body, login = faker.internet.userName()}) {
if (/^https:..accounts.spotify.com.api.token.*$/.test(url)) {
//Access token generator
const params = new urls.URLSearchParams(body)
if ((params.get("grant_type") === "refresh_token") && (params.get("client_id") === "MOCKED_CLIENT_ID") && (params.get("client_secret") === "MOCKED_CLIENT_SECRET") && (params.get("refresh_token") === "MOCKED_REFRESH_TOKEN")) {
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
return ({
status:200,
data:{
access_token:"MOCKED_TOKEN_ACCESS",
token_type:"Bearer",
expires_in:3600,
scope:"user-read-recently-played user-read-private",
},
})
}
}
}

View File

@@ -1,67 +1,67 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics")
return ({
user:{
repositories:{
nodes:[
{
createdAt:faker.date.recent(),
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
},
],
totalCount:faker.datatype.number(100),
},
forks:{
nodes:[
{
createdAt:faker.date.recent(),
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
},
],
totalCount:faker.datatype.number(100),
},
popular:{
nodes:[{stargazers:{totalCount:faker.datatype.number(50000)}}],
},
pullRequests:{
nodes:[
{
createdAt:faker.date.recent(),
title:faker.lorem.sentence(),
repository:{nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`},
},
],
totalCount:faker.datatype.number(50000),
},
contributionsCollection:{
pullRequestReviewContributions:{
nodes:[
{
occurredAt:faker.date.recent(),
pullRequest:{
title:faker.lorem.sentence(),
number:faker.datatype.number(1000),
repository:{nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`},
},
},
],
totalCount:faker.datatype.number(1000),
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics")
return ({
user:{
repositories:{
nodes:[
{
createdAt:faker.date.recent(),
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
},
},
projects:{totalCount:faker.datatype.number(100)},
packages:{totalCount:faker.datatype.number(100)},
organizations:{nodes:[], totalCount:faker.datatype.number(5)},
gists:{
nodes:[{createdAt:faker.date.recent(), name:faker.lorem.slug()}],
],
totalCount:faker.datatype.number(100),
},
forks:{
nodes:[
{
createdAt:faker.date.recent(),
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
},
],
totalCount:faker.datatype.number(100),
},
popular:{
nodes:[{stargazers:{totalCount:faker.datatype.number(50000)}}],
},
pullRequests:{
nodes:[
{
createdAt:faker.date.recent(),
title:faker.lorem.sentence(),
repository:{nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`},
},
],
totalCount:faker.datatype.number(50000),
},
contributionsCollection:{
pullRequestReviewContributions:{
nodes:[
{
occurredAt:faker.date.recent(),
pullRequest:{
title:faker.lorem.sentence(),
number:faker.datatype.number(1000),
repository:{nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`},
},
},
],
totalCount:faker.datatype.number(1000),
},
starredRepositories:{totalCount:faker.datatype.number(1000)},
followers:{totalCount:faker.datatype.number(10000)},
following:{totalCount:faker.datatype.number(10000)},
bio:faker.lorem.sentence(),
status:{message:faker.lorem.paragraph()},
sponsorshipsAsSponsor:{totalCount:faker.datatype.number(100)},
},
})
}
projects:{totalCount:faker.datatype.number(100)},
packages:{totalCount:faker.datatype.number(100)},
organizations:{nodes:[], totalCount:faker.datatype.number(5)},
gists:{
nodes:[{createdAt:faker.date.recent(), name:faker.lorem.slug()}],
totalCount:faker.datatype.number(1000),
},
starredRepositories:{totalCount:faker.datatype.number(1000)},
followers:{totalCount:faker.datatype.number(10000)},
following:{totalCount:faker.datatype.number(10000)},
bio:faker.lorem.sentence(),
status:{message:faker.lorem.paragraph()},
sponsorshipsAsSponsor:{totalCount:faker.datatype.number(100)},
},
})
}

View File

@@ -1,8 +1,8 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics")
return ({
repository:{viewerHasStarred:faker.datatype.boolean()},
viewer:{login},
})
}
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics")
return ({
repository:{viewerHasStarred:faker.datatype.boolean()},
viewer:{login},
})
}

View File

@@ -1,8 +1,8 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/octocat")
return ({
user:{viewerIsFollowing:faker.datatype.boolean()},
viewer:{login},
})
}
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/octocat")
return ({
user:{viewerIsFollowing:faker.datatype.boolean()},
viewer:{login},
})
}

View File

@@ -1,33 +1,33 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/organizations")
return ({
organization:{
repositories:{
nodes:[
{
createdAt:faker.date.recent(),
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
},
],
totalCount:faker.datatype.number(100),
},
forks:{
nodes:[
{
createdAt:faker.date.recent(),
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
},
],
totalCount:faker.datatype.number(100),
},
popular:{
nodes:[{stargazers:{totalCount:faker.datatype.number(50000)}}],
},
projects:{totalCount:faker.datatype.number(100)},
packages:{totalCount:faker.datatype.number(100)},
membersWithRole:{totalCount:faker.datatype.number(100)},
sponsorshipsAsSponsor:{totalCount:faker.datatype.number(100)},
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/organizations")
return ({
organization:{
repositories:{
nodes:[
{
createdAt:faker.date.recent(),
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
},
],
totalCount:faker.datatype.number(100),
},
})
}
forks:{
nodes:[
{
createdAt:faker.date.recent(),
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
},
],
totalCount:faker.datatype.number(100),
},
popular:{
nodes:[{stargazers:{totalCount:faker.datatype.number(50000)}}],
},
projects:{totalCount:faker.datatype.number(100)},
packages:{totalCount:faker.datatype.number(100)},
membersWithRole:{totalCount:faker.datatype.number(100)},
sponsorshipsAsSponsor:{totalCount:faker.datatype.number(100)},
},
})
}

View File

@@ -1,12 +1,12 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/ranking")
return ({
repo_rank:{repositoryCount:faker.datatype.number(100000)},
forks_rank:{repositoryCount:faker.datatype.number(100000)},
created_rank:{userCount:faker.datatype.number(100000)},
user_rank:{userCount:faker.datatype.number(100000)},
repo_total:{repositoryCount:faker.datatype.number(100000)},
user_total:{userCount:faker.datatype.number(100000)},
})
}
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/ranking")
return ({
repo_rank:{repositoryCount:faker.datatype.number(100000)},
forks_rank:{repositoryCount:faker.datatype.number(100000)},
created_rank:{userCount:faker.datatype.number(100000)},
user_rank:{userCount:faker.datatype.number(100000)},
repo_total:{repositoryCount:faker.datatype.number(100000)},
user_total:{userCount:faker.datatype.number(100000)},
})
}

View File

@@ -1,14 +1,16 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/repositories")
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/repositories")
return /after: "MOCKED_CURSOR"/m.test(query)
? ({
user:{
repositories:{
edges:[],
nodes:[],
},
},
}) : ({
})
: ({
user:{
repositories:{
edges:[
@@ -46,4 +48,4 @@
},
},
})
}
}

View File

@@ -1,36 +1,36 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/repository")
return ({
user:{
repository:{
name:"metrics",
owner:{login},
createdAt:new Date().toISOString(),
diskUsage:Math.floor(Math.random()*10000),
homepageUrl:faker.internet.url(),
watchers:{totalCount:faker.datatype.number(1000)},
stargazers:{totalCount:faker.datatype.number(10000)},
languages:{
edges:[
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
],
},
issues_open:{totalCount:faker.datatype.number(100)},
issues_closed:{totalCount:faker.datatype.number(100)},
pr_open:{totalCount:faker.datatype.number(100)},
pr_closed:{totalCount:faker.datatype.number(100)},
pr_merged:{totalCount:faker.datatype.number(100)},
releases:{totalCount:faker.datatype.number(100)},
forkCount:faker.datatype.number(100),
licenseInfo:{spdxId:"MIT"},
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/repository")
return ({
user:{
repository:{
name:"metrics",
owner:{login},
createdAt:new Date().toISOString(),
diskUsage:Math.floor(Math.random() * 10000),
homepageUrl:faker.internet.url(),
watchers:{totalCount:faker.datatype.number(1000)},
stargazers:{totalCount:faker.datatype.number(10000)},
languages:{
edges:[
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
{size:faker.datatype.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}},
],
},
issues_open:{totalCount:faker.datatype.number(100)},
issues_closed:{totalCount:faker.datatype.number(100)},
pr_open:{totalCount:faker.datatype.number(100)},
pr_closed:{totalCount:faker.datatype.number(100)},
pr_merged:{totalCount:faker.datatype.number(100)},
releases:{totalCount:faker.datatype.number(100)},
forkCount:faker.datatype.number(100),
licenseInfo:{spdxId:"MIT"},
},
})
}
},
})
}

View File

@@ -1,68 +1,68 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/user")
return ({
user:{
databaseId:faker.datatype.number(10000000),
name:faker.name.findName(),
login,
createdAt:`${faker.date.past(10)}`,
avatarUrl:faker.image.people(),
websiteUrl:faker.internet.url(),
isHireable:faker.datatype.boolean(),
twitterUsername:login,
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),
restrictedContributionsCount:faker.datatype.number(10000),
totalIssueContributions:faker.datatype.number(100),
totalPullRequestContributions:faker.datatype.number(1000),
totalPullRequestReviewContributions:faker.datatype.number(1000),
},
calendar:{
contributionCalendar:{
weeks:[
{
contributionDays:[
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
],
},
{
contributionDays:[
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
],
},
{
contributionDays:[
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
],
},
],
},
},
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)},
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/user")
return ({
user:{
databaseId:faker.datatype.number(10000000),
name:faker.name.findName(),
login,
createdAt:`${faker.date.past(10)}`,
avatarUrl:faker.image.people(),
websiteUrl:faker.internet.url(),
isHireable:faker.datatype.boolean(),
twitterUsername:login,
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),
restrictedContributionsCount:faker.datatype.number(10000),
totalIssueContributions:faker.datatype.number(100),
totalPullRequestContributions:faker.datatype.number(1000),
totalPullRequestReviewContributions:faker.datatype.number(1000),
},
})
}
calendar:{
contributionCalendar:{
weeks:[
{
contributionDays:[
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
],
},
{
contributionDays:[
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
],
},
{
contributionDays:[
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
],
},
],
},
},
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)},
},
})
}

View File

@@ -1,14 +1,14 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > contributors/commit")
return ({
repository:{
object:{
oid:"MOCKED_SHA",
abbreviatedOid:"MOCKED_SHA",
messageHeadline:faker.lorem.sentence(),
committedDate:faker.date.recent(),
},
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > contributors/commit")
return ({
repository:{
object:{
oid:"MOCKED_SHA",
abbreviatedOid:"MOCKED_SHA",
messageHeadline:faker.lorem.sentence(),
committedDate:faker.date.recent(),
},
})
}
},
})
}

View File

@@ -1,13 +1,13 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > followup/user")
return ({
user:{
issues_open:{totalCount:faker.datatype.number(100)},
issues_closed:{totalCount:faker.datatype.number(100)},
pr_open:{totalCount:faker.datatype.number(100)},
pr_closed:{totalCount:faker.datatype.number(100)},
pr_merged:{totalCount:faker.datatype.number(100)},
},
})
}
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > followup/user")
return ({
user:{
issues_open:{totalCount:faker.datatype.number(100)},
issues_closed:{totalCount:faker.datatype.number(100)},
pr_open:{totalCount:faker.datatype.number(100)},
pr_closed:{totalCount:faker.datatype.number(100)},
pr_merged:{totalCount:faker.datatype.number(100)},
},
})
}

View File

@@ -1,14 +1,16 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > gists/default")
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > gists/default")
return /after: "MOCKED_CURSOR"/m.test(query)
? ({
user:{
gists:{
edges:[],
nodes:[],
},
},
}) : ({
})
: ({
user:{
gists:{
edges:[
@@ -36,4 +38,4 @@
},
},
})
}
}

View File

@@ -1,9 +1,9 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/organization")
return ({
organization:{
description:faker.lorem.sentences(),
},
})
}
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/organization")
return ({
organization:{
description:faker.lorem.sentences(),
},
})
}

View File

@@ -1,9 +1,9 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/repository")
return ({
repository:{
description:faker.lorem.sentences(),
},
})
}
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/repository")
return ({
repository:{
description:faker.lorem.sentences(),
},
})
}

View File

@@ -1,9 +1,9 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/user")
return ({
user:{
bio:faker.lorem.sentences(),
},
})
}
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/user")
return ({
user:{
bio:faker.lorem.sentences(),
},
})
}

View File

@@ -1,32 +1,32 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > isocalendar/calendar")
//Generate calendar
const date = new Date(query.match(/from: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
const to = new Date(query.match(/to: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
const weeks = []
let contributionDays = []
for (; date <= to; date.setDate(date.getDate()+1)) {
//Create new week on sunday
if (date.getDay() === 0) {
weeks.push({contributionDays})
contributionDays = []
}
//Random contributions
const contributionCount = Math.min(10, Math.max(0, faker.datatype.number(14)-4))
contributionDays.push({
contributionCount,
color:["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"][Math.ceil(contributionCount/10/0.25)],
date:date.toISOString().substring(0, 10),
})
}
return ({
user:{
calendar:{
contributionCalendar:{
weeks,
},
},
},
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > isocalendar/calendar")
//Generate calendar
const date = new Date(query.match(/from: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
const to = new Date(query.match(/to: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
const weeks = []
let contributionDays = []
for (; date <= to; date.setDate(date.getDate() + 1)) {
//Create new week on sunday
if (date.getDay() === 0) {
weeks.push({contributionDays})
contributionDays = []
}
//Random contributions
const contributionCount = Math.min(10, Math.max(0, faker.datatype.number(14) - 4))
contributionDays.push({
contributionCount,
color:["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"][Math.ceil(contributionCount / 10 / 0.25)],
date:date.toISOString().substring(0, 10),
})
}
return ({
user:{
calendar:{
contributionCalendar:{
weeks,
},
},
},
})
}

View File

@@ -1,278 +1,278 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > licenses/default")
return ({
licenses:[
{
spdxId:"AGPL-3.0",
name:"GNU Affero General Public License v3.0",
nickname:"GNU AGPLv3",
key:"agpl-3.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
{key:"document-changes", label:"State changes"},
{key:"disclose-source", label:"Disclose source"},
{key:"network-use-disclose", label:"Network use is distribution"},
{key:"same-license", label:"Same license"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"patent-use", label:"Patent use"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"Apache-2.0",
name:"Apache License 2.0",
nickname:null,
key:"apache-2.0",
limitations:[
{key:"trademark-use", label:"Trademark use"},
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
{key:"document-changes", label:"State changes"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"patent-use", label:"Patent use"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"BSD-2-Clause",
name:'BSD 2-Clause "Simplified" License',
nickname:null,
key:"bsd-2-clause",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"BSD-3-Clause",
name:'BSD 3-Clause "New" or "Revised" License',
nickname:null,
key:"bsd-3-clause",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"BSL-1.0",
name:"Boost Software License 1.0",
nickname:null,
key:"bsl-1.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright--source", label:"License and copyright notice for source"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"CC0-1.0",
name:"Creative Commons Zero v1.0 Universal",
nickname:null,
key:"cc0-1.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"trademark-use", label:"Trademark use"},
{key:"patent-use", label:"Patent use"},
{key:"warranty", label:"Warranty"},
],
conditions:[],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"EPL-2.0",
name:"Eclipse Public License 2.0",
nickname:null,
key:"epl-2.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"disclose-source", label:"Disclose source"},
{key:"include-copyright", label:"License and copyright notice"},
{key:"same-license", label:"Same license"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"distribution", label:"Distribution"},
{key:"modifications", label:"Modification"},
{key:"patent-use", label:"Patent use"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"GPL-2.0",
name:"GNU General Public License v2.0",
nickname:"GNU GPLv2",
key:"gpl-2.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
{key:"document-changes", label:"State changes"},
{key:"disclose-source", label:"Disclose source"},
{key:"same-license", label:"Same license"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"GPL-3.0",
name:"GNU General Public License v3.0",
nickname:"GNU GPLv3",
key:"gpl-3.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
{key:"document-changes", label:"State changes"},
{key:"disclose-source", label:"Disclose source"},
{key:"same-license", label:"Same license"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"patent-use", label:"Patent use"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"LGPL-2.1",
name:"GNU Lesser General Public License v2.1",
nickname:"GNU LGPLv2.1",
key:"lgpl-2.1",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
{key:"disclose-source", label:"Disclose source"},
{key:"document-changes", label:"State changes"},
{key:"same-license--library", label:"Same license (library)"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"MIT",
name:"MIT License",
nickname:null,
key:"mit",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"MPL-2.0",
name:"Mozilla Public License 2.0",
nickname:null,
key:"mpl-2.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"trademark-use", label:"Trademark use"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"disclose-source", label:"Disclose source"},
{key:"include-copyright", label:"License and copyright notice"},
{key:"same-license--file", label:"Same license (file)"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"patent-use", label:"Patent use"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"Unlicense",
name:"The Unlicense",
nickname:null,
key:"unlicense",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[],
permissions:[
{key:"private-use", label:"Private use"},
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
],
},
],
})
}
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > licenses/default")
return ({
licenses:[
{
spdxId:"AGPL-3.0",
name:"GNU Affero General Public License v3.0",
nickname:"GNU AGPLv3",
key:"agpl-3.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
{key:"document-changes", label:"State changes"},
{key:"disclose-source", label:"Disclose source"},
{key:"network-use-disclose", label:"Network use is distribution"},
{key:"same-license", label:"Same license"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"patent-use", label:"Patent use"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"Apache-2.0",
name:"Apache License 2.0",
nickname:null,
key:"apache-2.0",
limitations:[
{key:"trademark-use", label:"Trademark use"},
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
{key:"document-changes", label:"State changes"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"patent-use", label:"Patent use"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"BSD-2-Clause",
name:'BSD 2-Clause "Simplified" License',
nickname:null,
key:"bsd-2-clause",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"BSD-3-Clause",
name:'BSD 3-Clause "New" or "Revised" License',
nickname:null,
key:"bsd-3-clause",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"BSL-1.0",
name:"Boost Software License 1.0",
nickname:null,
key:"bsl-1.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright--source", label:"License and copyright notice for source"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"CC0-1.0",
name:"Creative Commons Zero v1.0 Universal",
nickname:null,
key:"cc0-1.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"trademark-use", label:"Trademark use"},
{key:"patent-use", label:"Patent use"},
{key:"warranty", label:"Warranty"},
],
conditions:[],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"EPL-2.0",
name:"Eclipse Public License 2.0",
nickname:null,
key:"epl-2.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"disclose-source", label:"Disclose source"},
{key:"include-copyright", label:"License and copyright notice"},
{key:"same-license", label:"Same license"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"distribution", label:"Distribution"},
{key:"modifications", label:"Modification"},
{key:"patent-use", label:"Patent use"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"GPL-2.0",
name:"GNU General Public License v2.0",
nickname:"GNU GPLv2",
key:"gpl-2.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
{key:"document-changes", label:"State changes"},
{key:"disclose-source", label:"Disclose source"},
{key:"same-license", label:"Same license"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"GPL-3.0",
name:"GNU General Public License v3.0",
nickname:"GNU GPLv3",
key:"gpl-3.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
{key:"document-changes", label:"State changes"},
{key:"disclose-source", label:"Disclose source"},
{key:"same-license", label:"Same license"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"patent-use", label:"Patent use"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"LGPL-2.1",
name:"GNU Lesser General Public License v2.1",
nickname:"GNU LGPLv2.1",
key:"lgpl-2.1",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
{key:"disclose-source", label:"Disclose source"},
{key:"document-changes", label:"State changes"},
{key:"same-license--library", label:"Same license (library)"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"MIT",
name:"MIT License",
nickname:null,
key:"mit",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"include-copyright", label:"License and copyright notice"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"MPL-2.0",
name:"Mozilla Public License 2.0",
nickname:null,
key:"mpl-2.0",
limitations:[
{key:"liability", label:"Liability"},
{key:"trademark-use", label:"Trademark use"},
{key:"warranty", label:"Warranty"},
],
conditions:[
{key:"disclose-source", label:"Disclose source"},
{key:"include-copyright", label:"License and copyright notice"},
{key:"same-license--file", label:"Same license (file)"},
],
permissions:[
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
{key:"patent-use", label:"Patent use"},
{key:"private-use", label:"Private use"},
],
},
{
spdxId:"Unlicense",
name:"The Unlicense",
nickname:null,
key:"unlicense",
limitations:[
{key:"liability", label:"Liability"},
{key:"warranty", label:"Warranty"},
],
conditions:[],
permissions:[
{key:"private-use", label:"Private use"},
{key:"commercial-use", label:"Commercial use"},
{key:"modifications", label:"Modification"},
{key:"distribution", label:"Distribution"},
],
},
],
})
}

View File

@@ -1,13 +1,13 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > licenses/repository")
return ({
user:{
repository:{
licenseInfo:{spdxId:"MIT", name:"MIT License", nickname:null, key:"mit"},
url:"https://github.com/lowlighter/metrics",
databaseId:293860197,
},
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > licenses/repository")
return ({
user:{
repository:{
licenseInfo:{spdxId:"MIT", name:"MIT License", nickname:null, key:"mit"},
url:"https://github.com/lowlighter/metrics",
databaseId:293860197,
},
})
}
},
})
}

View File

@@ -1,13 +1,15 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > notable/contributions")
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > notable/contributions")
return /after: "MOCKED_CURSOR"/m.test(query)
? ({
user:{
repositoriesContributedTo:{
edges:[],
},
},
}) : ({
})
: ({
user:{
repositoriesContributedTo:{
edges:[
@@ -29,4 +31,4 @@
},
},
})
}
}

View File

@@ -1,17 +1,19 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > people/default")
const type = query.match(/(?<type>followers|following)[(]/)?.groups?.type ?? "(unknown type)"
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > people/default")
const type = query.match(/(?<type>followers|following)[(]/)?.groups?.type ?? "(unknown type)"
return /after: "MOCKED_CURSOR"/m.test(query)
? ({
user:{
[type]:{
edges:[],
},
},
}) : ({
})
: ({
user:{
[type]:{
edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map((login = faker.internet.userName()) => ({
edges:new Array(Math.ceil(20 + 80 * Math.random())).fill(null).map((login = faker.internet.userName()) => ({
cursor:"MOCKED_CURSOR",
node:{
login,
@@ -21,4 +23,4 @@
},
},
})
}
}

View File

@@ -1,8 +1,9 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > people/repository")
const type = query.match(/(?<type>stargazers|watchers)[(]/)?.groups?.type ?? "(unknown type)"
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > people/repository")
const type = query.match(/(?<type>stargazers|watchers)[(]/)?.groups?.type ?? "(unknown type)"
return /after: "MOCKED_CURSOR"/m.test(query)
? ({
user:{
repository:{
[type]:{
@@ -10,11 +11,12 @@
},
},
},
}) : ({
})
: ({
user:{
repository:{
[type]:{
edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map((login = faker.internet.userName()) => ({
edges:new Array(Math.ceil(20 + 80 * Math.random())).fill(null).map((login = faker.internet.userName()) => ({
cursor:"MOCKED_CURSOR",
node:{
login,
@@ -25,4 +27,4 @@
},
},
})
}
}

View File

@@ -1,19 +1,21 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > people/sponsors")
const type = query.match(/(?<type>sponsorshipsAsSponsor|sponsorshipsAsMaintainer)[(]/)?.groups?.type ?? "(unknown type)"
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > people/sponsors")
const type = query.match(/(?<type>sponsorshipsAsSponsor|sponsorshipsAsMaintainer)[(]/)?.groups?.type ?? "(unknown type)"
return /after: "MOCKED_CURSOR"/m.test(query)
? ({
user:{
login,
[type]:{
edges:[],
},
},
}) : ({
})
: ({
user:{
login,
[type]:{
edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map((login = faker.internet.userName()) => ({
edges:new Array(Math.ceil(20 + 80 * Math.random())).fill(null).map((login = faker.internet.userName()) => ({
cursor:"MOCKED_CURSOR",
node:{
sponsorEntity:{
@@ -29,4 +31,4 @@
},
},
})
}
}

View File

@@ -1,21 +1,21 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > projects/repository")
return ({
user:{
repository:{
project:{
name:"Repository project example",
updatedAt:`${faker.date.recent()}`,
body:faker.lorem.paragraph(),
progress:{
doneCount:faker.datatype.number(10),
inProgressCount:faker.datatype.number(10),
todoCount:faker.datatype.number(10),
enabled:true,
},
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > projects/repository")
return ({
user:{
repository:{
project:{
name:"Repository project example",
updatedAt:`${faker.date.recent()}`,
body:faker.lorem.paragraph(),
progress:{
doneCount:faker.datatype.number(10),
inProgressCount:faker.datatype.number(10),
todoCount:faker.datatype.number(10),
enabled:true,
},
},
},
})
}
},
})
}

View File

@@ -1,24 +1,24 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > projects/user")
return ({
user:{
projects:{
totalCount:1,
nodes:[
{
name:"User-owned project",
updatedAt:`${faker.date.recent()}`,
body:faker.lorem.paragraph(),
progress:{
doneCount:faker.datatype.number(10),
inProgressCount:faker.datatype.number(10),
todoCount:faker.datatype.number(10),
enabled:true,
},
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > projects/user")
return ({
user:{
projects:{
totalCount:1,
nodes:[
{
name:"User-owned project",
updatedAt:`${faker.date.recent()}`,
body:faker.lorem.paragraph(),
progress:{
doneCount:faker.datatype.number(10),
inProgressCount:faker.datatype.number(10),
todoCount:faker.datatype.number(10),
enabled:true,
},
],
},
},
],
},
})
}
},
})
}

View File

@@ -1,15 +1,17 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > reactions/default")
const type = query.match(/(?<type>issues|issueComments)[(]/)?.groups?.type ?? "(unknown type)"
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > reactions/default")
const type = query.match(/(?<type>issues|issueComments)[(]/)?.groups?.type ?? "(unknown type)"
return /after: "MOCKED_CURSOR"/m.test(query)
? ({
user:{
[type]:{
edges:[],
nodes:[],
},
},
}) : ({
})
: ({
user:{
[type]:{
edges:new Array(100).fill(null).map(_ => ({
@@ -27,4 +29,4 @@
},
},
})
}
}

View File

@@ -1,13 +1,15 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > stargazers/default")
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > stargazers/default")
return /after: "MOCKED_CURSOR"/m.test(query)
? ({
repository:{
stargazers:{
edges:[],
},
},
}) : ({
})
: ({
repository:{
stargazers:{
edges:new Array(faker.datatype.number({min:50, max:100})).fill(null).map(() => ({
@@ -17,4 +19,4 @@
},
},
})
}
}

View File

@@ -1,37 +1,37 @@
/**Mocked data */
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > stars/default")
return ({
user:{
starredRepositories:{
edges:[
{
starredAt:`${faker.date.recent(14)}`,
node:{
description:"📊 An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !",
forkCount:faker.datatype.number(100),
isFork:false,
issues:{
totalCount:faker.datatype.number(100),
},
nameWithOwner:"lowlighter/metrics",
openGraphImageUrl:"https://repository-images.githubusercontent.com/293860197/7fd72080-496d-11eb-8fe0-238b38a0746a",
pullRequests:{
totalCount:faker.datatype.number(100),
},
stargazerCount:faker.datatype.number(10000),
licenseInfo:{
nickname:null,
name:"MIT License",
},
primaryLanguage:{
color:"#f1e05a",
name:"JavaScript",
},
export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > stars/default")
return ({
user:{
starredRepositories:{
edges:[
{
starredAt:`${faker.date.recent(14)}`,
node:{
description:"📊 An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !",
forkCount:faker.datatype.number(100),
isFork:false,
issues:{
totalCount:faker.datatype.number(100),
},
nameWithOwner:"lowlighter/metrics",
openGraphImageUrl:"https://repository-images.githubusercontent.com/293860197/7fd72080-496d-11eb-8fe0-238b38a0746a",
pullRequests:{
totalCount:faker.datatype.number(100),
},
stargazerCount:faker.datatype.number(10000),
licenseInfo:{
nickname:null,
name:"MIT License",
},
primaryLanguage:{
color:"#f1e05a",
name:"JavaScript",
},
},
],
},
},
],
},
})
}
},
})
}

View File

@@ -1,341 +1,341 @@
/**Mocked data */
export default function({faker}, target, that, [{username:login, page, per_page}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.activity.listEventsForAuthenticatedUser")
return ({
status:200,
url:`https://api.github.com/users/${login}/events?per_page=${per_page}&page=${page}`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
export default function({faker}, target, that, [{username:login, page, per_page}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.activity.listEventsForAuthenticatedUser")
return ({
status:200,
url:`https://api.github.com/users/${login}/events?per_page=${per_page}&page=${page}`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:page < 1 ? [] : [
{
id:"10000000000",
type:"CommitCommentEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
comment:{
user:{
login,
},
path:faker.system.fileName(),
commit_id:"MOCKED_SHA",
body:faker.lorem.sentence(),
},
},
created_at:faker.date.recent(7),
public:true,
},
data:page < 1 ? [] : [
{
id:"10000000000",
type:"CommitCommentEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
comment:{
user:{
login,
},
path:faker.system.fileName(),
commit_id:"MOCKED_SHA",
body:faker.lorem.sentence(),
{
id:"10000000001",
type:"PullRequestReviewCommentEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:"created",
comment:{
user:{
login,
},
body:faker.lorem.paragraph(),
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000001",
type:"PullRequestReviewCommentEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:"created",
comment:{
user:{
login,
},
body:faker.lorem.paragraph(),
},
pull_request:{
title:faker.lorem.sentence(),
number:1,
user:{
login:faker.internet.userName(),
},
body:"",
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000002",
type:"IssuesEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:faker.random.arrayElement(["opened", "closed", "reopened"]),
issue:{
number:2,
title:faker.lorem.sentence(),
user:{
login,
},
body:faker.lorem.paragraph(),
performed_via_github_app:null,
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000003",
type:"GollumEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
pages:[
{
page_name:faker.lorem.sentence(),
title:faker.lorem.sentence(),
summary:null,
action:"created",
sha:"MOCKED_SHA",
},
],
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000004",
type:"IssueCommentEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:"created",
issue:{
number:3,
title:faker.lorem.sentence(),
user:{
login,
},
labels:[
{
name:"lorem ipsum",
color:"d876e3",
},
],
state:"open",
},
comment:{
body:faker.lorem.paragraph(),
performed_via_github_app:null,
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000005",
type:"ForkEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
forkee:{
name:faker.random.word(),
full_name:`${faker.random.word()}/${faker.random.word()}`,
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000006",
type:"PullRequestReviewEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:"created",
review:{
user:{
login,
},
state:"approved",
},
pull_request:{
state:"open",
number:4,
locked:false,
title:faker.lorem.sentence(),
user:{
login:faker.internet.userName(),
},
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000007",
type:"ReleaseEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:"published",
release:{
tag_name:`v${faker.datatype.number()}.${faker.datatype.number()}`,
name:faker.random.words(4),
draft:faker.datatype.boolean(),
prerelease:faker.datatype.boolean(),
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000008",
type:"CreateEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
ref:faker.lorem.slug(),
ref_type:faker.random.arrayElement(["tag", "branch"]),
master_branch:"master",
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"100000000009",
type:"WatchEvent",
actor:{
login,
},
repo:{
name:"lowlighter/metrics",
},
payload:{action:"started"},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000010",
type:"DeleteEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
ref:faker.lorem.slug(),
ref_type:faker.random.arrayElement(["tag", "branch"]),
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000011",
type:"PushEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
size:1,
ref:"refs/heads/master",
commits:[
{
sha:"MOCKED_SHA",
message:faker.lorem.sentence(),
url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA",
},
],
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000012",
type:"PullRequestEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:faker.random.arrayElement(["opened", "closed"]),
number:5,
pull_request:{
user:{
login,
},
state:"open",
title:faker.lorem.sentence(),
additions:faker.datatype.number(1000),
deletions:faker.datatype.number(1000),
changed_files:faker.datatype.number(10),
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000013",
type:"MemberEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
member:{
pull_request:{
title:faker.lorem.sentence(),
number:1,
user:{
login:faker.internet.userName(),
},
action:"added",
body:"",
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000014",
type:"PublicEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{},
created_at:faker.date.recent(7),
public:true,
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000002",
type:"IssuesEvent",
actor:{
login,
},
],
})
}
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:faker.random.arrayElement(["opened", "closed", "reopened"]),
issue:{
number:2,
title:faker.lorem.sentence(),
user:{
login,
},
body:faker.lorem.paragraph(),
performed_via_github_app:null,
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000003",
type:"GollumEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
pages:[
{
page_name:faker.lorem.sentence(),
title:faker.lorem.sentence(),
summary:null,
action:"created",
sha:"MOCKED_SHA",
},
],
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000004",
type:"IssueCommentEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:"created",
issue:{
number:3,
title:faker.lorem.sentence(),
user:{
login,
},
labels:[
{
name:"lorem ipsum",
color:"d876e3",
},
],
state:"open",
},
comment:{
body:faker.lorem.paragraph(),
performed_via_github_app:null,
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000005",
type:"ForkEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
forkee:{
name:faker.random.word(),
full_name:`${faker.random.word()}/${faker.random.word()}`,
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000006",
type:"PullRequestReviewEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:"created",
review:{
user:{
login,
},
state:"approved",
},
pull_request:{
state:"open",
number:4,
locked:false,
title:faker.lorem.sentence(),
user:{
login:faker.internet.userName(),
},
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000007",
type:"ReleaseEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:"published",
release:{
tag_name:`v${faker.datatype.number()}.${faker.datatype.number()}`,
name:faker.random.words(4),
draft:faker.datatype.boolean(),
prerelease:faker.datatype.boolean(),
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000008",
type:"CreateEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
ref:faker.lorem.slug(),
ref_type:faker.random.arrayElement(["tag", "branch"]),
master_branch:"master",
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"100000000009",
type:"WatchEvent",
actor:{
login,
},
repo:{
name:"lowlighter/metrics",
},
payload:{action:"started"},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000010",
type:"DeleteEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
ref:faker.lorem.slug(),
ref_type:faker.random.arrayElement(["tag", "branch"]),
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000011",
type:"PushEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
size:1,
ref:"refs/heads/master",
commits:[
{
sha:"MOCKED_SHA",
message:faker.lorem.sentence(),
url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA",
},
],
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000012",
type:"PullRequestEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
action:faker.random.arrayElement(["opened", "closed"]),
number:5,
pull_request:{
user:{
login,
},
state:"open",
title:faker.lorem.sentence(),
additions:faker.datatype.number(1000),
deletions:faker.datatype.number(1000),
changed_files:faker.datatype.number(10),
},
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000013",
type:"MemberEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{
member:{
login:faker.internet.userName(),
},
action:"added",
},
created_at:faker.date.recent(7),
public:true,
},
{
id:"10000000014",
type:"PublicEvent",
actor:{
login,
},
repo:{
name:`${faker.random.word()}/${faker.random.word()}`,
},
payload:{},
created_at:faker.date.recent(7),
public:true,
},
],
})
}

View File

@@ -1,8 +1,8 @@
//Imports
import listEventsForAuthenticatedUser from "./listEventsForAuthenticatedUser.mjs"
import listEventsForAuthenticatedUser from "./listEventsForAuthenticatedUser.mjs"
/**Mocked data */
export default function({faker}, target, that, [{username:login, page, per_page}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.activity.listRepoEvents")
return listEventsForAuthenticatedUser(...arguments)
}
export default function({faker}, target, that, [{username:login, page, per_page}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.activity.listRepoEvents")
return listEventsForAuthenticatedUser(...arguments)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,23 @@
/**Mocked data */
export default function({faker}, target, that, args) {
return ({
status:200,
url:"https://api.github.com/rate_limit",
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
export default function({faker}, target, that, args) {
return ({
status:200,
url:"https://api.github.com/rate_limit",
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:{
resources:{
core:{limit:5000, used:0, remaining:5000, reset:0},
search:{limit:30, used:0, remaining:30, reset:0},
graphql:{limit:5000, used:0, remaining:5000, reset:0},
integration_manifest:{limit:5000, used:0, remaining:5000, reset:0},
source_import:{limit:100, used:0, remaining:100, reset:0},
code_scanning_upload:{limit:500, used:0, remaining:500, reset:0},
},
data:{
resources:{
core:{limit:5000, used:0, remaining:5000, reset:0},
search:{limit:30, used:0, remaining:30, reset:0},
graphql:{limit:5000, used:0, remaining:5000, reset:0},
integration_manifest:{limit:5000, used:0, remaining:5000, reset:0},
source_import:{limit:100, used:0, remaining:100, reset:0},
code_scanning_upload:{limit:500, used:0, remaining:500, reset:0},
},
rate:{limit:5000, used:0, remaining:"MOCKED", reset:0},
},
})
}
rate:{limit:5000, used:0, remaining:"MOCKED", reset:0},
},
})
}

View File

@@ -1,27 +1,27 @@
/**Mocked data */
export default function({faker}, target, that, [{owner, repo}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getContributorsStats")
return ({
status:200,
url:`https://api.github.com/repos/${owner}/${repo}/stats/contributors`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:[
{
total:faker.datatype.number(10000),
weeks:[
{w:1, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
{w:2, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
{w:3, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
{w:4, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
],
author:{
login:owner,
},
export default function({faker}, target, that, [{owner, repo}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getContributorsStats")
return ({
status:200,
url:`https://api.github.com/repos/${owner}/${repo}/stats/contributors`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:[
{
total:faker.datatype.number(10000),
weeks:[
{w:1, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
{w:2, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
{w:3, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
{w:4, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
],
author:{
login:owner,
},
],
})
}
},
],
})
}

View File

@@ -1,23 +1,23 @@
/**Mocked data */
export default function({faker}, target, that, [{owner, repo}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getViews")
const count = faker.datatype.number(10000)*2
const uniques = faker.datatype.number(count)*2
return ({
status:200,
url:`https://api.github.com/repos/${owner}/${repo}/traffic/views`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:{
count,
uniques,
views:[
{timestamp:`${faker.date.recent()}`, count:count/2, uniques:uniques/2},
{timestamp:`${faker.date.recent()}`, count:count/2, uniques:uniques/2},
],
},
})
}
export default function({faker}, target, that, [{owner, repo}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getViews")
const count = faker.datatype.number(10000) * 2
const uniques = faker.datatype.number(count) * 2
return ({
status:200,
url:`https://api.github.com/repos/${owner}/${repo}/traffic/views`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:{
count,
uniques,
views:[
{timestamp:`${faker.date.recent()}`, count:count / 2, uniques:uniques / 2},
{timestamp:`${faker.date.recent()}`, count:count / 2, uniques:uniques / 2},
],
},
})
}

View File

@@ -1,34 +1,36 @@
/**Mocked data */
export default function({faker}, target, that, [{page, per_page, owner, repo}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.listCommits")
return ({
status:200,
url:`https://api.github.com/repos/${owner}/${repo}/commits?per_page=${per_page}&page=${page}`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:page < 2 ? new Array(per_page).fill(null).map(() => ({
sha:"MOCKED_SHA",
get author() {
return this.commit.author
export default function({faker}, target, that, [{page, per_page, owner, repo}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.listCommits")
return ({
status:200,
url:`https://api.github.com/repos/${owner}/${repo}/commits?per_page=${per_page}&page=${page}`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:page < 2
? new Array(per_page).fill(null).map(() => ({
sha:"MOCKED_SHA",
get author() {
return this.commit.author
},
commit:{
message:faker.lorem.sentence(),
author:{
name:owner,
login:faker.internet.userName(),
avatar_url:null,
date:`${faker.date.recent(14)}`,
},
commit:{
message:faker.lorem.sentence(),
author:{
name:owner,
login:faker.internet.userName(),
avatar_url:null,
date:`${faker.date.recent(14)}`,
},
committer:{
name:owner,
login:faker.internet.userName(),
avatar_url:null,
date:`${faker.date.recent(14)}`,
},
committer:{
name:owner,
login:faker.internet.userName(),
avatar_url:null,
date:`${faker.date.recent(14)}`,
},
})) : [],
})
}
},
}))
: [],
})
}

View File

@@ -1,18 +1,18 @@
/**Mocked data */
export default function({faker}, target, that, [{owner, repo}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.listContributors")
return ({
status:200,
url:`https://api.github.com/repos/${owner}/${repo}/contributors`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:new Array(40+faker.datatype.number(60)).fill(null).map(() => ({
login:faker.internet.userName(),
avatar_url:null,
contributions:faker.datatype.number(1000),
})),
})
}
export default function({faker}, target, that, [{owner, repo}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.listContributors")
return ({
status:200,
url:`https://api.github.com/repos/${owner}/${repo}/contributors`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:new Array(40 + faker.datatype.number(60)).fill(null).map(() => ({
login:faker.internet.userName(),
avatar_url:null,
contributions:faker.datatype.number(1000),
})),
})
}

View File

@@ -1,59 +1,59 @@
/**Mocked data */
export default function({faker}, target, that, args) {
//Arguments
const [url] = args
//Head request
if (/^HEAD .$/.test(url)) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.request HEAD")
return ({
status:200,
url:"https://api.github.com/",
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:undefined,
})
}
//Commit content
if (/^https:..api.github.com.repos.lowlighter.metrics.commits.MOCKED_SHA/.test(url)) {
console.debug(`metrics/compute/mocks > mocking rest api result > rest.request ${url}`)
return ({
status:200,
url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA",
data:{
sha:"MOCKED_SHA",
commit:{
author:{
name:faker.internet.userName(),
email:faker.internet.email(),
date:`${faker.date.recent(7)}`,
},
committer:{
name:faker.internet.userName(),
email:faker.internet.email(),
date:`${faker.date.recent(7)}`,
},
},
author:{
login:faker.internet.userName(),
id:faker.datatype.number(100000000),
},
committer:{
login:faker.internet.userName(),
id:faker.datatype.number(100000000),
},
files:[
{
sha:"MOCKED_SHA",
filename:faker.system.fileName(),
patch:"@@ -0,0 +1,5 @@\n+//Imports\n+ import app from \"./src/app.mjs\"\n+\n+//Start app\n+ await app()\n\\ No newline at end of file",
},
],
},
})
}
return target(...args)
export default function({faker}, target, that, args) {
//Arguments
const [url] = args
//Head request
if (/^HEAD .$/.test(url)) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.request HEAD")
return ({
status:200,
url:"https://api.github.com/",
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:undefined,
})
}
//Commit content
if (/^https:..api.github.com.repos.lowlighter.metrics.commits.MOCKED_SHA/.test(url)) {
console.debug(`metrics/compute/mocks > mocking rest api result > rest.request ${url}`)
return ({
status:200,
url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA",
data:{
sha:"MOCKED_SHA",
commit:{
author:{
name:faker.internet.userName(),
email:faker.internet.email(),
date:`${faker.date.recent(7)}`,
},
committer:{
name:faker.internet.userName(),
email:faker.internet.email(),
date:`${faker.date.recent(7)}`,
},
},
author:{
login:faker.internet.userName(),
id:faker.datatype.number(100000000),
},
committer:{
login:faker.internet.userName(),
id:faker.datatype.number(100000000),
},
files:[
{
sha:"MOCKED_SHA",
filename:faker.system.fileName(),
patch:'@@ -0,0 +1,5 @@\n+//Imports\n+ import app from "./src/app.mjs"\n+\n+//Start app\n+ await app()\n\\ No newline at end of file',
},
],
},
})
}
return target(...args)
}

View File

@@ -1,18 +1,18 @@
/**Mocked data */
export default function({faker}, target, that, [{username}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getByUsername")
return ({
status:200,
url:`https://api.github.com/users/${username}/`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:{
login:faker.internet.userName(),
avatar_url:null,
contributions:faker.datatype.number(1000),
},
})
}
export default function({faker}, target, that, [{username}]) {
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getByUsername")
return ({
status:200,
url:`https://api.github.com/users/${username}/`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:{
login:faker.internet.userName(),
avatar_url:null,
contributions:faker.datatype.number(1000),
},
})
}

View File

@@ -1,150 +1,150 @@
//Imports
import axios from "axios"
import faker from "faker"
import paths from "path"
import urls from "url"
import rss from "rss-parser"
import fs from "fs/promises"
import fs from "fs/promises"
import axios from "axios"
import faker from "faker"
import paths from "path"
import rss from "rss-parser"
import urls from "url"
//Mocked state
let mocked = false
let mocked = false
//Mocking
export default async function({graphql, rest}) {
export default async function({graphql, rest}) {
//Check if already mocked
if (mocked)
return {graphql, rest}
mocked = true
console.debug("metrics/compute/mocks > mocking")
//Check if already mocked
if (mocked)
return {graphql, rest}
mocked = true
console.debug("metrics/compute/mocks > mocking")
//Load mocks
const __mocks = paths.join(paths.dirname(urls.fileURLToPath(import.meta.url)))
const mock = async ({directory, mocks}) => {
for (const entry of await fs.readdir(directory)) {
if ((await fs.lstat(paths.join(directory, entry))).isDirectory()) {
if (!mocks[entry])
mocks[entry] = {}
await mock({directory:paths.join(directory, entry), mocks:mocks[entry]})
}
else
mocks[entry.replace(/[.]mjs$/, "")] = (await import(urls.pathToFileURL(paths.join(directory, entry)).href)).default
//Load mocks
const __mocks = paths.join(paths.dirname(urls.fileURLToPath(import.meta.url)))
const mock = async({directory, mocks}) => {
for (const entry of await fs.readdir(directory)) {
if ((await fs.lstat(paths.join(directory, entry))).isDirectory()) {
if (!mocks[entry])
mocks[entry] = {}
await mock({directory:paths.join(directory, entry), mocks:mocks[entry]})
}
else
mocks[entry.replace(/[.]mjs$/, "")] = (await import(urls.pathToFileURL(paths.join(directory, entry)).href)).default
}
return mocks
}
const mocks = await mock({directory:paths.join(__mocks, "api"), mocks:{}})
//GraphQL API mocking
{
//Unmocked
console.debug("metrics/compute/mocks > mocking graphql api")
const unmocked = graphql
//Mocked
graphql = new Proxy(unmocked, {
apply(target, that, args) {
//Arguments
const [query] = args
const login = query.match(/login: "(?<login>.*?)"/)?.groups?.login ?? faker.internet.userName()
//Search for mocked query
for (const mocked of Object.keys(mocks.github.graphql)) {
if (new RegExp(`^query ${mocked.replace(/([.]\w)/g, (_, g) => g.toLocaleUpperCase().substring(1)).replace(/^(\w)/g, (_, g) => g.toLocaleUpperCase())} `).test(query))
return mocks.github.graphql[mocked]({faker, query, login})
}
return mocks
}
const mocks = await mock({directory:paths.join(__mocks, "api"), mocks:{}})
//GraphQL API mocking
{
//Unmocked
console.debug("metrics/compute/mocks > mocking graphql api")
const unmocked = graphql
//Mocked
graphql = new Proxy(unmocked, {
apply(target, that, args) {
//Arguments
const [query] = args
const login = query.match(/login: "(?<login>.*?)"/)?.groups?.login ?? faker.internet.userName()
//Search for mocked query
for (const mocked of Object.keys(mocks.github.graphql)) {
if (new RegExp(`^query ${mocked.replace(/([.]\w)/g, (_, g) => g.toLocaleUpperCase().substring(1)).replace(/^(\w)/g, (_, g) => g.toLocaleUpperCase())} `).test(query))
return mocks.github.graphql[mocked]({faker, query, login})
}
//Unmocked call
return target(...args)
},
})
}
//Rest API mocking
{
//Unmocked
console.debug("metrics/compute/mocks > mocking rest api")
const unmocked = {}
//Mocked
const mocker = ({path = "rest", mocks, mocked}) => {
for (const [key, value] of Object.entries(mocks)) {
console.debug(`metrics/compute/mocks > mocking rest api > mocking ${path}.${key}`)
if (typeof value === "function") {
unmocked[path] = value
mocked[key] = new Proxy(unmocked[path], {apply:value.bind(null, {faker})})
}
else
mocker({path:`${path}.${key}`, mocks:mocks[key], mocked:mocked[key]})
}
}
mocker({mocks:mocks.github.rest, mocked:rest})
}
//Axios mocking
{
//Unmocked
console.debug("metrics/compute/mocks > mocking axios")
const unmocked = {get:axios.get, post:axios.post}
//Mocked post requests
axios.post = new Proxy(unmocked.post, {
apply(target, that, args) {
//Arguments
const [url, body] = args
//Search for mocked request
for (const service of Object.keys(mocks.axios.post)) {
const mocked = mocks.axios.post[service]({faker, url, body})
if (mocked)
return mocked
}
//Unmocked call
return target(...args)
},
})
//Mocked get requests
axios.get = new Proxy(unmocked.get, {
apply(target, that, args) {
//Arguments
const [url, options] = args
//Search for mocked request
for (const service of Object.keys(mocks.axios.get)) {
const mocked = mocks.axios.get[service]({faker, url, options})
if (mocked)
return mocked
}
//Unmocked call
return target(...args)
},
})
}
//RSS mocking
{
//Unmocked
console.debug("metrics/compute/mocks > mocking rss-parser")
//Mock rss feed
rss.prototype.parseURL = function(url) {
console.debug(`metrics/compute/mocks > mocking rss feed result > ${url}`)
return ({
items:new Array(30).fill(null).map(_ => ({
title:faker.lorem.sentence(),
link:faker.internet.url(),
content:faker.lorem.paragraphs(),
contentSnippet:faker.lorem.paragraph(),
isoDate:faker.date.recent(),
})),
title:faker.lorem.words(),
description:faker.lorem.paragraph(),
link:url,
})
}
}
//Return mocked elements
return {graphql, rest}
//Unmocked call
return target(...args)
},
})
}
//Rest API mocking
{
//Unmocked
console.debug("metrics/compute/mocks > mocking rest api")
const unmocked = {}
//Mocked
const mocker = ({path = "rest", mocks, mocked}) => {
for (const [key, value] of Object.entries(mocks)) {
console.debug(`metrics/compute/mocks > mocking rest api > mocking ${path}.${key}`)
if (typeof value === "function") {
unmocked[path] = value
mocked[key] = new Proxy(unmocked[path], {apply:value.bind(null, {faker})})
}
else
mocker({path:`${path}.${key}`, mocks:mocks[key], mocked:mocked[key]})
}
}
mocker({mocks:mocks.github.rest, mocked:rest})
}
//Axios mocking
{
//Unmocked
console.debug("metrics/compute/mocks > mocking axios")
const unmocked = {get:axios.get, post:axios.post}
//Mocked post requests
axios.post = new Proxy(unmocked.post, {
apply(target, that, args) {
//Arguments
const [url, body] = args
//Search for mocked request
for (const service of Object.keys(mocks.axios.post)) {
const mocked = mocks.axios.post[service]({faker, url, body})
if (mocked)
return mocked
}
//Unmocked call
return target(...args)
},
})
//Mocked get requests
axios.get = new Proxy(unmocked.get, {
apply(target, that, args) {
//Arguments
const [url, options] = args
//Search for mocked request
for (const service of Object.keys(mocks.axios.get)) {
const mocked = mocks.axios.get[service]({faker, url, options})
if (mocked)
return mocked
}
//Unmocked call
return target(...args)
},
})
}
//RSS mocking
{
//Unmocked
console.debug("metrics/compute/mocks > mocking rss-parser")
//Mock rss feed
rss.prototype.parseURL = function(url) {
console.debug(`metrics/compute/mocks > mocking rss feed result > ${url}`)
return ({
items:new Array(30).fill(null).map(_ => ({
title:faker.lorem.sentence(),
link:faker.internet.url(),
content:faker.lorem.paragraphs(),
contentSnippet:faker.lorem.paragraph(),
isoDate:faker.date.recent(),
})),
title:faker.lorem.words(),
description:faker.lorem.paragraph(),
link:url,
})
}
}
//Return mocked elements
return {graphql, rest}
}

View File

@@ -1,321 +1,338 @@
//Imports
import octokit from "@octokit/graphql"
import OctokitRest from "@octokit/rest"
import express from "express"
import ratelimit from "express-rate-limit"
import compression from "compression"
import cache from "memory-cache"
import util from "util"
import setup from "../metrics/setup.mjs"
import mocks from "../mocks/index.mjs"
import metrics from "../metrics/index.mjs"
import octokit from "@octokit/graphql"
import OctokitRest from "@octokit/rest"
import compression from "compression"
import express from "express"
import ratelimit from "express-rate-limit"
import cache from "memory-cache"
import util from "util"
import metrics from "../metrics/index.mjs"
import setup from "../metrics/setup.mjs"
import mocks from "../mocks/index.mjs"
/**App */
export default async function({mock, nosettings} = {}) {
export default async function({mock, nosettings} = {}) {
//Load configuration settings
const {conf, Plugins, Templates} = await setup({nosettings})
const {token, maxusers = 0, restricted = [], debug = false, cached = 30 * 60 * 1000, port = 3000, ratelimiter = null, plugins = null} = conf.settings
mock = mock || conf.settings.mocked
//Load configuration settings
const {conf, Plugins, Templates} = await setup({nosettings})
const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null, plugins = null} = conf.settings
mock = mock || conf.settings.mocked
//Process mocking and default plugin state
for (const plugin of Object.keys(Plugins).filter(x => !["base", "core"].includes(x))) {
//Initialization
const {settings} = conf
if (!settings.plugins[plugin])
settings.plugins[plugin] = {}
//Auto-enable plugin if needed
if (conf.settings["plugins.default"])
settings.plugins[plugin].enabled = settings.plugins[plugin].enabled ?? (console.debug(`metrics/app > auto-enabling ${plugin}`), true)
//Mock plugins tokens if they're undefined
if (mock) {
const tokens = Object.entries(conf.metadata.plugins[plugin].inputs).filter(([key, value]) => (!/^plugin_/.test(key))&&(value.type === "token")).map(([key]) => key)
for (const token of tokens) {
if ((!settings.plugins[plugin][token])||(mock === "force")) {
console.debug(`metrics/app > using mocked token for ${plugin}.${token}`)
settings.plugins[plugin][token] = "MOCKED_TOKEN"
}
}
}
//Process mocking and default plugin state
for (const plugin of Object.keys(Plugins).filter(x => !["base", "core"].includes(x))) {
//Initialization
const {settings} = conf
if (!settings.plugins[plugin])
settings.plugins[plugin] = {}
//Auto-enable plugin if needed
if (conf.settings["plugins.default"])
settings.plugins[plugin].enabled = settings.plugins[plugin].enabled ?? (console.debug(`metrics/app > auto-enabling ${plugin}`), true)
//Mock plugins tokens if they're undefined
if (mock) {
const tokens = Object.entries(conf.metadata.plugins[plugin].inputs).filter(([key, value]) => (!/^plugin_/.test(key)) && (value.type === "token")).map(([key]) => key)
for (const token of tokens) {
if ((!settings.plugins[plugin][token]) || (mock === "force")) {
console.debug(`metrics/app > using mocked token for ${plugin}.${token}`)
settings.plugins[plugin][token] = "MOCKED_TOKEN"
}
}
if (((mock)&&(!conf.settings.token))||(mock === "force")) {
console.debug("metrics/app > using mocked token")
conf.settings.token = "MOCKED_TOKEN"
}
if (debug)
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})}
//Apply mocking if needed
if (mock)
Object.assign(api, await mocks(api))
const {graphql, rest} = api
//Setup server
const app = express()
app.use(compression())
const middlewares = []
//Rate limiter middleware
if (ratelimiter) {
app.set("trust proxy", 1)
middlewares.push(ratelimit({
skip(req, _res) {
return !!cache.get(req.params.login)
},
message:"Too many requests: retry later",
headers:true,
...ratelimiter,
}))
}
//Cache headers middleware
middlewares.push((req, res, next) => {
const maxage = Math.round(Number(req.query.cache))
if ((cached)||(maxage > 0))
res.header("Cache-Control", `public, max-age=${Math.round((maxage > 0 ? maxage : cached)/1000)}`)
else
res.header("Cache-Control", "no-store, no-cache")
next()
})
//Base routes
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", "categorie", "web", "supports"].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]))
const enabled = Object.entries(metadata).filter(([_name, {categorie}]) => categorie !== "core").map(([name]) => ({name, 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()}
let requests = {limit:0, used:0, remaining:0, reset:NaN}
if (!conf.settings.notoken) {
requests = (await rest.rateLimit.get()).data.rate
setInterval(async() => {
try {
requests = (await rest.rateLimit.get()).data.rate
}
catch {
console.debug("metrics/app > failed to update remaining requests")
}
}, 5*60*1000)
}
//Web
app.get("/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`))
app.get("/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`))
app.get("/favicon.ico", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/favicon.png`))
app.get("/.favicon.png", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/favicon.png`))
app.get("/.opengraph.png", limiter, (req, res) => conf.settings.web?.opengraph ? res.redirect(conf.settings.web?.opengraph) : res.sendFile(`${conf.paths.statics}/opengraph.png`))
//Plugins and templates
app.get("/.plugins", limiter, (req, res) => res.status(200).json(enabled))
app.get("/.plugins.base", limiter, (req, res) => res.status(200).json(conf.settings.plugins.base.parts))
app.get("/.plugins.metadata", limiter, (req, res) => res.status(200).json(metadata))
app.get("/.templates", limiter, (req, res) => res.status(200).json(templates))
app.get("/.templates/:template", limiter, (req, res) => req.params.template in conf.templates ? res.status(200).json(conf.templates[req.params.template]) : res.sendStatus(404))
for (const template in conf.templates)
app.use(`/.templates/${template}/partials`, express.static(`${conf.paths.templates}/${template}/partials`))
//Styles
app.get("/.css/style.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.css`))
app.get("/.css/style.vars.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.vars.css`))
app.get("/.css/style.prism.css", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/themes/prism-tomorrow.css`))
//Scripts
app.get("/.js/app.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.js`))
app.get("/.js/app.placeholder.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.placeholder.js`))
app.get("/.js/ejs.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/ejs/ejs.min.js`))
app.get("/.js/faker.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/faker/dist/faker.min.js`))
app.get("/.js/axios.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/axios/dist/axios.min.js`))
app.get("/.js/axios.min.map", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/axios/dist/axios.min.map`))
app.get("/.js/vue.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue/dist/vue.min.js`))
app.get("/.js/vue.prism.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue-prism-component/dist/vue-prism-component.min.js`))
app.get("/.js/vue-prism-component.min.js.map", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue-prism-component/dist/vue-prism-component.min.js.map`))
app.get("/.js/prism.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/prism.js`))
app.get("/.js/prism.yaml.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-yaml.min.js`))
app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-markdown.min.js`))
//Meta
app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version))
app.get("/.requests", limiter, (req, res) => res.status(200).json(requests))
app.get("/.hosted", limiter, (req, res) => res.status(200).json(conf.settings.hosted || null))
//Cache
app.get("/.uncache", limiter, (req, res) => {
const {token, user} = req.query
if (token) {
if (actions.flush.has(token)) {
console.debug(`metrics/app/${actions.flush.get(token)} > flushed cache`)
cache.del(actions.flush.get(token))
return res.sendStatus(200)
}
return res.sendStatus(400)
}
{
const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}`
actions.flush.set(token, user)
return res.json({token})
}
})
//About routes
app.use("/about/.statics/", express.static(`${conf.paths.statics}/about`))
app.get("/about/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
app.get("/about/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
app.get("/about/:login", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
app.get("/about/query/:login/", ...middlewares, async(req, res) => {
//Check username
const login = req.params.login?.replace(/[\n\r]/g, "")
if (!/^[-\w]+$/i.test(login)) {
console.debug(`metrics/app/${login}/insights > 400 (invalid username)`)
return res.status(400).send("Bad request: username seems invalid")
}
//Compute metrics
try {
//Read cached data if possible
if ((!debug)&&(cached)&&(cache.get(`about.${login}`))) {
console.debug(`metrics/app/${login}/insights > using cached results`)
return res.send(cache.get(`about.${login}`))
}
//Compute metrics
console.debug(`metrics/app/${login}/insights > compute insights`)
const json = await metrics({
login, 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,
},
}, {graphql, rest, plugins:{achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true}, activity:{enabled:true, markdown:"extended"}, notable:{enabled:true}}, conf, convert:"json"}, {Plugins, Templates})
//Cache
if ((!debug)&&(cached)) {
const maxage = Math.round(Number(req.query.cache))
cache.put(`about.${login}`, json, maxage > 0 ? maxage : cached)
}
return res.json(json)
}
//Internal error
catch (error) {
//Not found user
if ((error instanceof Error)&&(/^user not found$/.test(error.message))) {
console.debug(`metrics/app/${login} > 404 (user/organization not found)`)
return res.status(404).send("Not found: unknown user or organization")
}
//GitHub failed request
if ((error instanceof Error)&&(/this may be the result of a timeout, or it could be a GitHub bug/i.test(error.errors?.[0]?.message))) {
console.debug(`metrics/app/${login} > 502 (bad gateway from GitHub)`)
const request = encodeURIComponent(error.errors[0].message.match(/`(?<request>[\w:]+)`/)?.groups?.request ?? "").replace(/%3A/g, ":")
return res.status(500).send(`Internal Server Error: failed to execute request ${request} (this may be the result of a timeout, or it could be a GitHub bug)`)
}
//General error
console.error(error)
return res.status(500).send("Internal Server Error: failed to process metrics correctly")
}
})
//Metrics
const pending = new Map()
app.get("/:login/:repository?", ...middlewares, async(req, res) => {
//Request params
const login = req.params.login?.replace(/[\n\r]/g, "")
const repository = req.params.repository?.replace(/[\n\r]/g, "")
let solve = null
//Check username
if (!/^[-\w]+$/i.test(login)) {
console.debug(`metrics/app/${login} > 400 (invalid username)`)
return res.status(400).send("Bad request: username seems invalid")
}
//Allowed list check
if ((restricted.length)&&(!restricted.includes(login))) {
console.debug(`metrics/app/${login} > 403 (not in allowed users)`)
return res.status(403).send("Forbidden: username not in allowed list")
}
//Prevent multiples requests
if ((!debug)&&(!mock)&&(pending.has(login))) {
console.debug(`metrics/app/${login} > awaiting pending request`)
await pending.get(login)
}
else
pending.set(login, new Promise(_solve => solve = _solve))
//Read cached data if possible
if ((!debug)&&(cached)&&(cache.get(login))) {
console.debug(`metrics/app/${login} > using cached image`)
const {rendered, mime} = cache.get(login)
res.header("Content-Type", mime)
return res.send(rendered)
}
//Maximum simultaneous users
if ((maxusers)&&(cache.size()+1 > maxusers)) {
console.debug(`metrics/app/${login} > 503 (maximum users reached)`)
return res.status(503).send("Service Unavailable: maximum number of users reached, only cached metrics are available")
}
//Repository alias
if (repository) {
console.debug(`metrics/app/${login} > compute repository metrics`)
if (!req.query.template)
req.query.template = "repository"
req.query.repo = repository
}
//Compute rendering
try {
//Render
const q = req.query
console.debug(`metrics/app/${login} > ${util.inspect(q, {depth:Infinity, maxStringLength:256})}`)
const {rendered, mime} = await metrics({login, q}, {
graphql, rest, plugins, conf,
die:q["plugins.errors.fatal"] ?? false,
verify:q.verify ?? false,
convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(q["config.output"]) ? q["config.output"] : null,
}, {Plugins, Templates})
//Cache
if ((!debug)&&(cached)) {
const maxage = Math.round(Number(req.query.cache))
cache.put(login, {rendered, mime}, maxage > 0 ? maxage : cached)
}
//Send response
res.header("Content-Type", mime)
return res.send(rendered)
}
//Internal error
catch (error) {
//Not found user
if ((error instanceof Error)&&(/^user not found$/.test(error.message))) {
console.debug(`metrics/app/${login} > 404 (user/organization not found)`)
return res.status(404).send("Not found: unknown user or organization")
}
//Invalid template
if ((error instanceof Error)&&(/^unsupported template$/.test(error.message))) {
console.debug(`metrics/app/${login} > 400 (bad request)`)
return res.status(400).send("Bad request: unsupported template")
}
//Unsupported output format or account type
if ((error instanceof Error)&&(/^not supported for: [\s\S]*$/.test(error.message))) {
console.debug(`metrics/app/${login} > 406 (Not Acceptable)`)
return res.status(406).send("Not Acceptable: unsupported output format or account type for specified parameters")
}
//GitHub failed request
if ((error instanceof Error)&&(/this may be the result of a timeout, or it could be a GitHub bug/i.test(error.errors?.[0]?.message))) {
console.debug(`metrics/app/${login} > 502 (bad gateway from GitHub)`)
const request = encodeURIComponent(error.errors[0].message.match(/`(?<request>[\w:]+)`/)?.groups?.request ?? "").replace(/%3A/g, ":")
return res.status(500).send(`Internal Server Error: failed to execute request ${request} (this may be the result of a timeout, or it could be a GitHub bug)`)
}
//General error
console.error(error)
return res.status(500).send("Internal Server Error: failed to process metrics correctly")
}
//After rendering
finally {
solve?.()
}
})
//Listen
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)"}`,
`Max simultaneous users │ ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
`Plugins enabled │ ${enabled.map(({name}) => name).join(", ")}`,
`SVG optimization │ ${conf.settings.optimize ?? false}`,
"Server ready !",
].join("\n")))
}
}
if (((mock) && (!conf.settings.token)) || (mock === "force")) {
console.debug("metrics/app > using mocked token")
conf.settings.token = "MOCKED_TOKEN"
}
if (debug)
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})}
//Apply mocking if needed
if (mock)
Object.assign(api, await mocks(api))
const {graphql, rest} = api
//Setup server
const app = express()
app.use(compression())
const middlewares = []
//Rate limiter middleware
if (ratelimiter) {
app.set("trust proxy", 1)
middlewares.push(ratelimit({
skip(req, _res) {
return !!cache.get(req.params.login)
},
message:"Too many requests: retry later",
headers:true,
...ratelimiter,
}))
}
//Cache headers middleware
middlewares.push((req, res, next) => {
const maxage = Math.round(Number(req.query.cache))
if ((cached) || (maxage > 0))
res.header("Cache-Control", `public, max-age=${Math.round((maxage > 0 ? maxage : cached) / 1000)}`)
else
res.header("Cache-Control", "no-store, no-cache")
next()
})
//Base routes
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", "categorie", "web", "supports"].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]),
)
const enabled = Object.entries(metadata).filter(([_name, {categorie}]) => categorie !== "core").map(([name]) => ({name, 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()}
let requests = {limit:0, used:0, remaining:0, reset:NaN}
if (!conf.settings.notoken) {
requests = (await rest.rateLimit.get()).data.rate
setInterval(async () => {
try {
requests = (await rest.rateLimit.get()).data.rate
}
catch {
console.debug("metrics/app > failed to update remaining requests")
}
}, 5 * 60 * 1000)
}
//Web
app.get("/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`))
app.get("/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`))
app.get("/favicon.ico", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/favicon.png`))
app.get("/.favicon.png", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/favicon.png`))
app.get("/.opengraph.png", limiter, (req, res) => conf.settings.web?.opengraph ? res.redirect(conf.settings.web?.opengraph) : res.sendFile(`${conf.paths.statics}/opengraph.png`))
//Plugins and templates
app.get("/.plugins", limiter, (req, res) => res.status(200).json(enabled))
app.get("/.plugins.base", limiter, (req, res) => res.status(200).json(conf.settings.plugins.base.parts))
app.get("/.plugins.metadata", limiter, (req, res) => res.status(200).json(metadata))
app.get("/.templates", limiter, (req, res) => res.status(200).json(templates))
app.get("/.templates/:template", limiter, (req, res) => req.params.template in conf.templates ? res.status(200).json(conf.templates[req.params.template]) : res.sendStatus(404))
for (const template in conf.templates)
app.use(`/.templates/${template}/partials`, express.static(`${conf.paths.templates}/${template}/partials`))
//Styles
app.get("/.css/style.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.css`))
app.get("/.css/style.vars.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.vars.css`))
app.get("/.css/style.prism.css", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/themes/prism-tomorrow.css`))
//Scripts
app.get("/.js/app.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.js`))
app.get("/.js/app.placeholder.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.placeholder.js`))
app.get("/.js/ejs.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/ejs/ejs.min.js`))
app.get("/.js/faker.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/faker/dist/faker.min.js`))
app.get("/.js/axios.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/axios/dist/axios.min.js`))
app.get("/.js/axios.min.map", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/axios/dist/axios.min.map`))
app.get("/.js/vue.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue/dist/vue.min.js`))
app.get("/.js/vue.prism.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue-prism-component/dist/vue-prism-component.min.js`))
app.get("/.js/vue-prism-component.min.js.map", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue-prism-component/dist/vue-prism-component.min.js.map`))
app.get("/.js/prism.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/prism.js`))
app.get("/.js/prism.yaml.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-yaml.min.js`))
app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-markdown.min.js`))
//Meta
app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version))
app.get("/.requests", limiter, (req, res) => res.status(200).json(requests))
app.get("/.hosted", limiter, (req, res) => res.status(200).json(conf.settings.hosted || null))
//Cache
app.get("/.uncache", limiter, (req, res) => {
const {token, user} = req.query
if (token) {
if (actions.flush.has(token)) {
console.debug(`metrics/app/${actions.flush.get(token)} > flushed cache`)
cache.del(actions.flush.get(token))
return res.sendStatus(200)
}
return res.sendStatus(400)
}
{
const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}`
actions.flush.set(token, user)
return res.json({token})
}
})
//About routes
app.use("/about/.statics/", express.static(`${conf.paths.statics}/about`))
app.get("/about/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
app.get("/about/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
app.get("/about/:login", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
app.get("/about/query/:login/", ...middlewares, async (req, res) => {
//Check username
const login = req.params.login?.replace(/[\n\r]/g, "")
if (!/^[-\w]+$/i.test(login)) {
console.debug(`metrics/app/${login}/insights > 400 (invalid username)`)
return res.status(400).send("Bad request: username seems invalid")
}
//Compute metrics
try {
//Read cached data if possible
if ((!debug) && (cached) && (cache.get(`about.${login}`))) {
console.debug(`metrics/app/${login}/insights > using cached results`)
return res.send(cache.get(`about.${login}`))
}
//Compute metrics
console.debug(`metrics/app/${login}/insights > compute insights`)
const json = await metrics(
{
login,
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,
},
},
{graphql, rest, plugins:{achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true}, activity:{enabled:true, markdown:"extended"}, notable:{enabled:true}}, conf, convert:"json"},
{Plugins, Templates},
)
//Cache
if ((!debug) && (cached)) {
const maxage = Math.round(Number(req.query.cache))
cache.put(`about.${login}`, json, maxage > 0 ? maxage : cached)
}
return res.json(json)
}
//Internal error
catch (error) {
//Not found user
if ((error instanceof Error) && (/^user not found$/.test(error.message))) {
console.debug(`metrics/app/${login} > 404 (user/organization not found)`)
return res.status(404).send("Not found: unknown user or organization")
}
//GitHub failed request
if ((error instanceof Error) && (/this may be the result of a timeout, or it could be a GitHub bug/i.test(error.errors?.[0]?.message))) {
console.debug(`metrics/app/${login} > 502 (bad gateway from GitHub)`)
const request = encodeURIComponent(error.errors[0].message.match(/`(?<request>[\w:]+)`/)?.groups?.request ?? "").replace(/%3A/g, ":")
return res.status(500).send(`Internal Server Error: failed to execute request ${request} (this may be the result of a timeout, or it could be a GitHub bug)`)
}
//General error
console.error(error)
return res.status(500).send("Internal Server Error: failed to process metrics correctly")
}
})
//Metrics
const pending = new Map()
app.get("/:login/:repository?", ...middlewares, async (req, res) => {
//Request params
const login = req.params.login?.replace(/[\n\r]/g, "")
const repository = req.params.repository?.replace(/[\n\r]/g, "")
let solve = null
//Check username
if (!/^[-\w]+$/i.test(login)) {
console.debug(`metrics/app/${login} > 400 (invalid username)`)
return res.status(400).send("Bad request: username seems invalid")
}
//Allowed list check
if ((restricted.length) && (!restricted.includes(login))) {
console.debug(`metrics/app/${login} > 403 (not in allowed users)`)
return res.status(403).send("Forbidden: username not in allowed list")
}
//Prevent multiples requests
if ((!debug) && (!mock) && (pending.has(login))) {
console.debug(`metrics/app/${login} > awaiting pending request`)
await pending.get(login)
}
else
pending.set(login, new Promise(_solve => solve = _solve))
//Read cached data if possible
if ((!debug) && (cached) && (cache.get(login))) {
console.debug(`metrics/app/${login} > using cached image`)
const {rendered, mime} = cache.get(login)
res.header("Content-Type", mime)
return res.send(rendered)
}
//Maximum simultaneous users
if ((maxusers) && (cache.size() + 1 > maxusers)) {
console.debug(`metrics/app/${login} > 503 (maximum users reached)`)
return res.status(503).send("Service Unavailable: maximum number of users reached, only cached metrics are available")
}
//Repository alias
if (repository) {
console.debug(`metrics/app/${login} > compute repository metrics`)
if (!req.query.template)
req.query.template = "repository"
req.query.repo = repository
}
//Compute rendering
try {
//Render
const q = req.query
console.debug(`metrics/app/${login} > ${util.inspect(q, {depth:Infinity, maxStringLength:256})}`)
const {rendered, mime} = await metrics({login, q}, {
graphql,
rest,
plugins,
conf,
die:q["plugins.errors.fatal"] ?? false,
verify:q.verify ?? false,
convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(q["config.output"]) ? q["config.output"] : null,
}, {Plugins, Templates})
//Cache
if ((!debug) && (cached)) {
const maxage = Math.round(Number(req.query.cache))
cache.put(login, {rendered, mime}, maxage > 0 ? maxage : cached)
}
//Send response
res.header("Content-Type", mime)
return res.send(rendered)
}
//Internal error
catch (error) {
//Not found user
if ((error instanceof Error) && (/^user not found$/.test(error.message))) {
console.debug(`metrics/app/${login} > 404 (user/organization not found)`)
return res.status(404).send("Not found: unknown user or organization")
}
//Invalid template
if ((error instanceof Error) && (/^unsupported template$/.test(error.message))) {
console.debug(`metrics/app/${login} > 400 (bad request)`)
return res.status(400).send("Bad request: unsupported template")
}
//Unsupported output format or account type
if ((error instanceof Error) && (/^not supported for: [\s\S]*$/.test(error.message))) {
console.debug(`metrics/app/${login} > 406 (Not Acceptable)`)
return res.status(406).send("Not Acceptable: unsupported output format or account type for specified parameters")
}
//GitHub failed request
if ((error instanceof Error) && (/this may be the result of a timeout, or it could be a GitHub bug/i.test(error.errors?.[0]?.message))) {
console.debug(`metrics/app/${login} > 502 (bad gateway from GitHub)`)
const request = encodeURIComponent(error.errors[0].message.match(/`(?<request>[\w:]+)`/)?.groups?.request ?? "").replace(/%3A/g, ":")
return res.status(500).send(`Internal Server Error: failed to execute request ${request} (this may be the result of a timeout, or it could be a GitHub bug)`)
}
//General error
console.error(error)
return res.status(500).send("Internal Server Error: failed to process metrics correctly")
}
finally {
//After rendering
solve?.()
}
})
//Listen
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)"}`,
`Max simultaneous users │ ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
`Plugins enabled │ ${enabled.map(({name}) => name).join(", ")}`,
`SVG optimization │ ${conf.settings.optimize ?? false}`,
"Server ready !",
].join("\n")))
}

View File

@@ -1,143 +1,145 @@
;(async function() {
//App
return new Vue({
//Initialization
el:"main",
async mounted() {
//Palette
try {
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
} catch (error) {}
//User
const user = location.pathname.split("/").pop()
if ((user)&&(user !== "about")) {
this.user = user
await this.search()
}
else
this.searchable = true
//Embed
this.embed = !!(new URLSearchParams(location.search).get("embed"))
//Init
await Promise.all([
//GitHub limit tracker
(async () => {
const {data:requests} = await axios.get("/.requests")
this.requests = requests
})(),
//Version
(async () => {
const {data:version} = await axios.get("/.version")
this.version = `v${version}`
})(),
//Hosted
(async () => {
const {data:hosted} = await axios.get("/.hosted")
this.hosted = hosted
})(),
])
return new Vue({
//Initialization
el: "main",
async mounted() {
//Palette
try {
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
}
catch (error) {}
//User
const user = location.pathname.split("/").pop()
if ((user) && (user !== "about")) {
this.user = user
await this.search()
}
else {
this.searchable = true
}
//Embed
this.embed = !!(new URLSearchParams(location.search).get("embed"))
//Init
await Promise.all([
//GitHub limit tracker
(async () => {
const { data: requests } = await axios.get("/.requests")
this.requests = requests
})(),
//Version
(async () => {
const { data: version } = await axios.get("/.version")
this.version = `v${version}`
})(),
//Hosted
(async () => {
const { data: hosted } = await axios.get("/.hosted")
this.hosted = hosted
})(),
])
},
//Watchers
watch: {
palette: {
immediate: true,
handler(current, previous) {
document.querySelector("body").classList.remove(previous)
document.querySelector("body").classList.add(current)
},
//Watchers
watch:{
palette:{
immediate:true,
handler(current, previous) {
document.querySelector("body").classList.remove(previous)
document.querySelector("body").classList.add(current)
}
}
},
//Methods
methods:{
format(type, value, options) {
switch (type) {
case "number":
return new Intl.NumberFormat(navigator.lang, options).format(value)
case "date":
return new Intl.DateTimeFormat(navigator.lang, options).format(new Date(value))
case "comment":
const baseUrl = String.raw`https?:\/\/(?:www\.)?github.com\/([\w.-]+\/[\w.-]+)\/`
return value
.replace(
RegExp(baseUrl + String.raw`(?:issues|pull|discussions)\/(\d+)(?:\?\S+)?(#\S+)?`, 'g'),
(_, repo, id, comment) => (options?.repo === repo ? '' : repo) + `#${id}` + (comment ? ` (comment)` : '')
) // -> 'lowlighter/metrics#123'
.replace(
RegExp(baseUrl + String.raw`commit\/([\da-f]+)`, 'g'),
(_, repo, sha) => (options?.repo === repo ? '' : repo + '@') + sha
) // -> 'lowlighter/metrics@123abc'
.replace(
RegExp(baseUrl + String.raw`compare\/(\S+...\S+)`, 'g'),
(_, repo, tags) => (options?.repo === repo ? '' : repo + '@') + tags
) // -> 'lowlighter/metrics@1.0...1.1'
}
},
},
//Methods
methods: {
format(type, value, options) {
switch (type) {
case "number":
return new Intl.NumberFormat(navigator.lang, options).format(value)
case "date":
return new Intl.DateTimeFormat(navigator.lang, options).format(new Date(value))
case "comment":
const baseUrl = String.raw`https?:\/\/(?:www\.)?github.com\/([\w.-]+\/[\w.-]+)\/`
return value
},
async search() {
try {
this.error = null
this.metrics = null
this.pending = true
this.metrics = (await axios.get(`/about/query/${this.user}`)).data
}
catch (error) {
this.error = {code:error.response.status, message:error.response.data}
}
finally {
this.pending = false
}
}
},
//Computed properties
computed:{
ranked() {
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}) => !/(?:automater|octonaut|infographile)/i.test(title)) ?? []
},
isocalendar() {
return (this.metrics?.rendered.plugins.isocalendar.svg ?? "")
.replace(/#ebedf0/gi, "var(--color-calendar-graph-day-bg)")
.replace(/#9be9a8/gi, "var(--color-calendar-graph-day-L1-bg)")
.replace(/#40c463/gi, "var(--color-calendar-graph-day-L2-bg)")
.replace(/#30a14e/gi, "var(--color-calendar-graph-day-L3-bg)")
.replace(/#216e39/gi, "var(--color-calendar-graph-day-L4-bg)")
},
languages() {
return this.metrics?.rendered.plugins.languages.favorites ?? []
},
activity() {
return this.metrics?.rendered.plugins.activity.events ?? []
},
contributions() {
return this.metrics?.rendered.plugins.notable.contributions ?? []
},
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}
},
url() {
return `${window.location.protocol}//${window.location.host}/about/${this.user}`
},
preview() {
return /-preview$/.test(this.version)
}
},
//Data initialization
data:{
version:"",
hosted:null,
user:"",
embed:false,
searchable:false,
requests:{limit:0, used:0, remaining:0, reset:0},
palette:"light",
metrics:null,
pending:false,
error:null,
.replace(
RegExp(baseUrl + String.raw`(?:issues|pull|discussions)\/(\d+)(?:\?\S+)?(#\S+)?`, "g"),
(_, repo, id, comment) => (options?.repo === repo ? "" : repo) + `#${id}` + (comment ? ` (comment)` : ""),
) // -> 'lowlighter/metrics#123'
.replace(
RegExp(baseUrl + String.raw`commit\/([\da-f]+)`, "g"),
(_, repo, sha) => (options?.repo === repo ? "" : repo + "@") + sha,
) // -> 'lowlighter/metrics@123abc'
.replace(
RegExp(baseUrl + String.raw`compare\/(\S+...\S+)`, "g"),
(_, repo, tags) => (options?.repo === repo ? "" : repo + "@") + tags,
) // -> 'lowlighter/metrics@1.0...1.1'
}
})
return value
},
async search() {
try {
this.error = null
this.metrics = null
this.pending = true
this.metrics = (await axios.get(`/about/query/${this.user}`)).data
}
catch (error) {
this.error = { code: error.response.status, message: error.response.data }
}
finally {
this.pending = false
}
},
},
//Computed properties
computed: {
ranked() {
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 }) => !/(?:automater|octonaut|infographile)/i.test(title)) ?? []
},
isocalendar() {
return (this.metrics?.rendered.plugins.isocalendar.svg ?? "")
.replace(/#ebedf0/gi, "var(--color-calendar-graph-day-bg)")
.replace(/#9be9a8/gi, "var(--color-calendar-graph-day-L1-bg)")
.replace(/#40c463/gi, "var(--color-calendar-graph-day-L2-bg)")
.replace(/#30a14e/gi, "var(--color-calendar-graph-day-L3-bg)")
.replace(/#216e39/gi, "var(--color-calendar-graph-day-L4-bg)")
},
languages() {
return this.metrics?.rendered.plugins.languages.favorites ?? []
},
activity() {
return this.metrics?.rendered.plugins.activity.events ?? []
},
contributions() {
return this.metrics?.rendered.plugins.notable.contributions ?? []
},
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 }
},
url() {
return `${window.location.protocol}//${window.location.host}/about/${this.user}`
},
preview() {
return /-preview$/.test(this.version)
},
},
//Data initialization
data: {
version: "",
hosted: null,
user: "",
embed: false,
searchable: false,
requests: { limit: 0, used: 0, remaining: 0, reset: 0 },
palette: "light",
metrics: null,
pending: false,
error: null,
},
})
})()

View File

@@ -1,256 +1,264 @@
;(async function() {
//Init
const {data:metadata} = await axios.get("/.plugins.metadata")
delete metadata.core.web.output
delete metadata.core.web.twemojis
const { data: metadata } = await axios.get("/.plugins.metadata")
delete metadata.core.web.output
delete metadata.core.web.twemojis
//App
return new Vue({
//Initialization
el:"main",
async mounted() {
//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")
} catch (error) {}
//Init
await Promise.all([
//GitHub limit tracker
(async () => {
const {data:requests} = await axios.get("/.requests")
this.requests = requests
})(),
//Templates
(async () => {
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
})(),
//Base
(async () => {
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")
this.version = `v${version}`
})(),
//Hosted
(async () => {
const {data:hosted} = await axios.get("/.hosted")
this.hosted = hosted
})(),
])
//Generate placeholder
this.mock({timeout:200})
setInterval(() => {
const marker = document.querySelector("#metrics-end")
if (marker) {
this.mockresize()
marker.remove()
}
}, 100)
return new Vue({
//Initialization
el: "main",
async mounted() {
//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")
}
catch (error) {}
//Init
await Promise.all([
//GitHub limit tracker
(async () => {
const { data: requests } = await axios.get("/.requests")
this.requests = requests
})(),
//Templates
(async () => {
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
})(),
//Base
(async () => {
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")
this.version = `v${version}`
})(),
//Hosted
(async () => {
const { data: hosted } = await axios.get("/.hosted")
this.hosted = hosted
})(),
])
//Generate placeholder
this.mock({ timeout: 200 })
setInterval(() => {
const marker = document.querySelector("#metrics-end")
if (marker) {
this.mockresize()
marker.remove()
}
}, 100)
},
components: { Prism: PrismComponent },
//Watchers
watch: {
palette: {
immediate: true,
handler(current, previous) {
document.querySelector("body").classList.remove(previous)
document.querySelector("body").classList.add(current)
},
components:{Prism:PrismComponent},
//Watchers
watch:{
palette:{
immediate:true,
handler(current, previous) {
document.querySelector("body").classList.remove(previous)
document.querySelector("body").classList.add(current)
}
}
},
},
//Data initialization
data: {
version: "",
user: "",
mode: "metrics",
tab: "overview",
palette: "light",
requests: { limit: 0, used: 0, remaining: 0, reset: 0 },
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])),
hosted: null,
plugins: {
base: {},
list: [],
enabled: {},
descriptions: {
base: "🗃️ Base content",
"base.header": "Header",
"base.activity": "Account activity",
"base.community": "Community stats",
"base.repositories": "Repositories metrics",
"base.metadata": "Metadata",
...Object.fromEntries(Object.entries(metadata).map(([key, { name }]) => [key, name])),
},
//Data initialization
data:{
version:"",
user:"",
mode:"metrics",
tab:"overview",
palette:"light",
requests:{limit:0, used:0, remaining:0, reset:0},
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])),
hosted:null,
plugins:{
base:{},
list:[],
enabled:{},
descriptions:{
base:"🗃️ Base content",
"base.header":"Header",
"base.activity":"Account activity",
"base.community":"Community stats",
"base.repositories":"Repositories metrics",
"base.metadata":"Metadata",
...Object.fromEntries(Object.entries(metadata).map(([key, {name}]) => [key, name]))
},
options:{
descriptions:{...(Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)))},
...(Object.fromEntries(Object.entries(
Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)))
.map(([key, {defaulted}]) => [key, defaulted])
))
},
},
templates:{
list:[],
selected:"classic",
placeholder:{
timeout:null,
image:""
},
descriptions:{
classic:"Classic template",
terminal:"Terminal template",
markdown:"(hidden)",
repository:"(hidden)",
},
},
generated:{
pending:false,
content:"",
error:false,
},
options: {
descriptions: { ...(Object.assign({}, ...Object.entries(metadata).flatMap(([key, { web }]) => web))) },
...(Object.fromEntries(
Object.entries(
Object.assign({}, ...Object.entries(metadata).flatMap(([key, { web }]) => web)),
)
.map(([key, { defaulted }]) => [key, defaulted]),
)),
},
//Computed data
computed:{
//Unusable plugins
unusable() {
return this.plugins.list.filter(({name}) => this.plugins.enabled[name]).filter(({enabled}) => !enabled).map(({name}) => name)
},
//User's avatar
avatar() {
return this.generated.content ? `https://github.com/${this.user}.png` : null
},
//User's repository
repo() {
return `https://github.com/${this.user}/${this.user}`
},
//Endpoint to use for computed metrics
url() {
//Plugins enabled
const plugins = Object.entries(this.plugins.enabled)
.flatMap(([key, value]) => key === "base" ? Object.entries(value).map(([key, value]) => [`base.${key}`, value]) : [[key, value]])
.filter(([key, value]) => /^base[.]\w+$/.test(key) ? !value : value)
.map(([key, value]) => `${key}=${+value}`)
//Plugins options
const options = Object.entries(this.plugins.options)
.filter(([key, value]) => `${value}`.length)
.filter(([key, value]) => this.plugins.enabled[key.split(".")[0]])
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
//Base options
const base = Object.entries(this.plugins.options).filter(([key, value]) => (key in metadata.base.web)&&(value !== metadata.base.web[key]?.defaulted)).map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
//Config
const config = Object.entries(this.config).filter(([key, value]) => (value)&&(value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => `config.${key}=${encodeURIComponent(value)}`)
//Template
const template = (this.templates.selected !== this.templates.list[0]) ? [`template=${this.templates.selected}`] : []
//Generated url
const params = [...template, ...base, ...plugins, ...options, ...config].join("&")
return `${window.location.protocol}//${window.location.host}/${this.user}${params.length ? `?${params}` : ""}`
},
//Embedded generated code
embed() {
return `![Metrics](${this.url})`
},
//GitHub action auto-generated code
action() {
return [
`# Visit https://github.com/lowlighter/metrics/blob/master/action.yml for full reference`,
`name: Metrics`,
`on:`,
` # Schedule updates (each hour)`,
` schedule: [{cron: "0 * * * *"}]`,
` # Lines below let you run workflow manually and on each commit`,
` workflow_dispatch:`,
` push: {branches: ["master", "main"]}`,
`jobs:`,
` github-metrics:`,
` runs-on: ubuntu-latest`,
` steps:`,
` - uses: lowlighter/metrics@latest`,
` with:`,
` # Your GitHub token`,
` token: ${"$"}{{ secrets.METRICS_TOKEN }}`,
``,
` # Options`,
` user: ${this.user }`,
` template: ${this.templates.selected}`,
` 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(/[.]/, "_")}: ${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(/[.]/, "_")}: ${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(/[.]/, "_")}: ${typeof value === "boolean" ? {true:"yes", false:"no"}[value] : value}`),
].sort(),
].join("\n")
},
//Configurable plugins
configure() {
//Check enabled plugins
const enabled = Object.entries(this.plugins.enabled).filter(([key, value]) => (value)&&(key !== "base")).map(([key, value]) => key)
const filter = new RegExp(`^(?:${enabled.join("|")})[.]`)
//Search related options
const entries = Object.entries(this.plugins.options.descriptions).filter(([key, value]) => filter.test(key))
entries.push(...enabled.map(key => [key, this.plugins.descriptions[key]]))
entries.sort((a, b) => a[0].localeCompare(b[0]))
//Return object
const configure = Object.fromEntries(entries)
return Object.keys(configure).length ? configure : null
},
//Is in preview mode
preview() {
return /-preview$/.test(this.version)
}
},
templates: {
list: [],
selected: "classic",
placeholder: {
timeout: null,
image: "",
},
//Methods
methods:{
//Load and render placeholder image
async mock({timeout = 600} = {}) {
clearTimeout(this.templates.placeholder.timeout)
this.templates.placeholder.timeout = setTimeout(async () => {
this.templates.placeholder.image = await placeholder(this)
this.generated.content = ""
this.generated.error = null
}, timeout)
},
//Resize mock image
mockresize() {
const svg = document.querySelector(".preview .image svg")
if ((svg)&&(svg.getAttribute("height") == 99999)) {
const height = svg.querySelector("#metrics-end")?.getBoundingClientRect()?.y-svg.getBoundingClientRect()?.y
if (Number.isFinite(height))
svg.setAttribute("height", height)
}
},
//Generate metrics and flush cache
async generate() {
//Avoid requests spamming
if (this.generated.pending)
return
this.generated.pending = true
//Compute metrics
try {
await axios.get(`/.uncache?&token=${(await axios.get(`/.uncache?user=${this.user}`)).data.token}`)
this.generated.content = (await axios.get(this.url)).data
this.generated.error = null
} catch (error) {
this.generated.error = {code:error.response.status, message:error.response.data}
}
finally {
this.generated.pending = false
}
},
descriptions: {
classic: "Classic template",
terminal: "Terminal template",
markdown: "(hidden)",
repository: "(hidden)",
},
})
})()
},
generated: {
pending: false,
content: "",
error: false,
},
},
//Computed data
computed: {
//Unusable plugins
unusable() {
return this.plugins.list.filter(({ name }) => this.plugins.enabled[name]).filter(({ enabled }) => !enabled).map(({ name }) => name)
},
//User's avatar
avatar() {
return this.generated.content ? `https://github.com/${this.user}.png` : null
},
//User's repository
repo() {
return `https://github.com/${this.user}/${this.user}`
},
//Endpoint to use for computed metrics
url() {
//Plugins enabled
const plugins = Object.entries(this.plugins.enabled)
.flatMap(([key, value]) => key === "base" ? Object.entries(value).map(([key, value]) => [`base.${key}`, value]) : [[key, value]])
.filter(([key, value]) => /^base[.]\w+$/.test(key) ? !value : value)
.map(([key, value]) => `${key}=${+value}`)
//Plugins options
const options = Object.entries(this.plugins.options)
.filter(([key, value]) => `${value}`.length)
.filter(([key, value]) => this.plugins.enabled[key.split(".")[0]])
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
//Base options
const base = Object.entries(this.plugins.options).filter(([key, value]) => (key in metadata.base.web) && (value !== metadata.base.web[key]?.defaulted)).map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
//Config
const config = Object.entries(this.config).filter(([key, value]) => (value) && (value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => `config.${key}=${encodeURIComponent(value)}`)
//Template
const template = (this.templates.selected !== this.templates.list[0]) ? [`template=${this.templates.selected}`] : []
//Generated url
const params = [...template, ...base, ...plugins, ...options, ...config].join("&")
return `${window.location.protocol}//${window.location.host}/${this.user}${params.length ? `?${params}` : ""}`
},
//Embedded generated code
embed() {
return `![Metrics](${this.url})`
},
//GitHub action auto-generated code
action() {
return [
`# Visit https://github.com/lowlighter/metrics/blob/master/action.yml for full reference`,
`name: Metrics`,
`on:`,
` # Schedule updates (each hour)`,
` schedule: [{cron: "0 * * * *"}]`,
` # Lines below let you run workflow manually and on each commit`,
` workflow_dispatch:`,
` push: {branches: ["master", "main"]}`,
`jobs:`,
` github-metrics:`,
` runs-on: ubuntu-latest`,
` steps:`,
` - uses: lowlighter/metrics@latest`,
` with:`,
` # Your GitHub token`,
` token: ${"$"}{{ secrets.METRICS_TOKEN }}`,
``,
` # Options`,
` user: ${this.user}`,
` template: ${this.templates.selected}`,
` 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(/[.]/, "_")}: ${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(/[.]/, "_")}: ${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(/[.]/, "_")}: ${typeof value === "boolean" ? { true: "yes", false: "no" }[value] : value}`),
].sort(),
].join("\n")
},
//Configurable plugins
configure() {
//Check enabled plugins
const enabled = Object.entries(this.plugins.enabled).filter(([key, value]) => (value) && (key !== "base")).map(([key, value]) => key)
const filter = new RegExp(`^(?:${enabled.join("|")})[.]`)
//Search related options
const entries = Object.entries(this.plugins.options.descriptions).filter(([key, value]) => filter.test(key))
entries.push(...enabled.map(key => [key, this.plugins.descriptions[key]]))
entries.sort((a, b) => a[0].localeCompare(b[0]))
//Return object
const configure = Object.fromEntries(entries)
return Object.keys(configure).length ? configure : null
},
//Is in preview mode
preview() {
return /-preview$/.test(this.version)
},
},
//Methods
methods: {
//Load and render placeholder image
async mock({ timeout = 600 } = {}) {
clearTimeout(this.templates.placeholder.timeout)
this.templates.placeholder.timeout = setTimeout(async () => {
this.templates.placeholder.image = await placeholder(this)
this.generated.content = ""
this.generated.error = null
}, timeout)
},
//Resize mock image
mockresize() {
const svg = document.querySelector(".preview .image svg")
if ((svg) && (svg.getAttribute("height") == 99999)) {
const height = svg.querySelector("#metrics-end")?.getBoundingClientRect()?.y - svg.getBoundingClientRect()?.y
if (Number.isFinite(height))
svg.setAttribute("height", height)
}
},
//Generate metrics and flush cache
async generate() {
//Avoid requests spamming
if (this.generated.pending)
return
this.generated.pending = true
//Compute metrics
try {
await axios.get(`/.uncache?&token=${(await axios.get(`/.uncache?user=${this.user}`)).data.token}`)
this.generated.content = (await axios.get(this.url)).data
this.generated.error = null
}
catch (error) {
this.generated.error = { code: error.response.status, message: error.response.data }
}
finally {
this.generated.pending = false
}
},
},
})
})()

File diff suppressed because it is too large Load Diff