From d2923797e5688b2900fad8b7ab686859724177ba Mon Sep 17 00:00:00 2001
From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com>
Date: Tue, 19 Jan 2021 20:51:51 +0100
Subject: [PATCH] Feat community templates (#68)
---
.gitignore | 6 +++-
README.md | 35 ++++++++++++++++++++
action.yml | 7 ++++
package-lock.json | 66 +++++++++++++++++++------------------
package.json | 4 +--
settings.example.json | 3 ++
source/app/action/index.mjs | 8 ++++-
source/app/setup.mjs | 48 +++++++++++++++++++++++++--
tests/metrics.test.js | 15 ++++++++-
9 files changed, 153 insertions(+), 39 deletions(-)
diff --git a/.gitignore b/.gitignore
index e7b46764..d94d98b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -104,4 +104,8 @@ dist
.tern-port
# User settings
-settings.json
\ No newline at end of file
+settings.json
+
+# Community templates
+source/templates/.community
+source/templates/@*
\ No newline at end of file
diff --git a/README.md b/README.md
index 38998b71..d6144494 100644
--- a/README.md
+++ b/README.md
@@ -630,6 +630,41 @@ The default template is `classic`.
* **N**: Feature is already released, but new ones are available on `@master`
* **R**: Repository template (all plugins content will be restricted to related repository)
+
+💬 Using community templates
+
+ 🚧 This feature is available as pre-release on @master branch (unstable)
+
+It is possible to use official releases along with custom templates from forked repositories (not necessarily your own).
+This can be used to use different layouts, styles colors, etc.
+
+Use `setup_community_templates` option to specify additional external sources in the following format: `user/repo@branch:template`. Templates added this way will be downloaded through git and will be available with the same template name but prefixed with `@`.
+
+For example, to use the `super-metrics` template from `github-user`'s fork, add the following:
+```yaml
+- uses: lowlighter/metrics@master
+ with:
+ # ... other options
+ template: "@super-metrics"
+ setup_community_templates: github-user/metrics@master:classic
+```
+
+By default, community templates have their `template.mjs` removed and fallback to the one used by `classic` template.
+It means that they're restricted to common and plugins data, to prevent malicious code injection and token leaks.
+
+If you really trust a template, it is possible to bypass this behaviour by appending `+trust` at the end of their source like below:
+```yaml
+- uses: lowlighter/metrics@master
+ with:
+ # ... other options
+ setup_community_templates: github-user/metrics@master:classic+trust
+```
+
+To create a new community template, just fork this repository and create a folder in `/source/templates` with the same structure as current templates.
+Then, it's just as simple as HTML and CSS with a bit of JavaScript!
+
+
+
💬 Using repository template
diff --git a/action.yml b/action.yml
index 3e84f9bb..40274ff1 100644
--- a/action.yml
+++ b/action.yml
@@ -38,6 +38,13 @@ inputs:
description: SVG optimization
default: yes
+ # Setup additional templates from remote repositories (like forks)
+ # Format is : user/repo@branch:template
+ # To use a community template, set "template" option to "@template" (where template is the template name)
+ setup_community_templates:
+ description: Additional community templates to setup
+ default: ""
+
# Timezone used by metrics
# See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# Some plugins will use it to calibrate dates
diff --git a/package-lock.json b/package-lock.json
index 752443e8..6c3abe47 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -936,16 +936,16 @@
}
},
"@octokit/openapi-types": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.2.0.tgz",
- "integrity": "sha512-274lNUDonw10kT8wHg8fCcUc1ZjZHbWv0/TbAwb0ojhBQqZYc1cQ/4yqTVTtPMDeZ//g7xVEYe/s3vURkRghPg=="
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.3.1.tgz",
+ "integrity": "sha512-KTzpRDT07euvbBYbPs121YDqq5DT94nBDFIyogsDhOnWL8yDCHev6myeiPTgS+VLmyUbdNCYu6L/gVj+Bd1q8Q=="
},
"@octokit/plugin-paginate-rest": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.7.0.tgz",
- "integrity": "sha512-+zARyncLjt9b0FjqPAbJo4ss7HOlBi1nprq+cPlw5vu2+qjy7WvlXhtXFdRHQbSL1Pt+bfAKaLADEkkvg8sP8w==",
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.7.1.tgz",
+ "integrity": "sha512-dUsxsEIrBqhlQNfXRhMhXOTQi0SSG38+QWcPGO226HFPFJk44vWukegHfMG3496vLv9T2oT7IuAGssGpcUg5bQ==",
"requires": {
- "@octokit/types": "^6.0.1"
+ "@octokit/types": "^6.3.1"
}
},
"@octokit/plugin-request-log": {
@@ -954,11 +954,11 @@
"integrity": "sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg=="
},
"@octokit/plugin-rest-endpoint-methods": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.4.3.tgz",
- "integrity": "sha512-qzGV1D8m8pRc3BLcKQIGeCMO2VfzcG5s0l5aXnmguTg6I7/x9sCAUNzhpeIOnHGrDTpF2STqB7duYJm4CxUo3Q==",
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.5.2.tgz",
+ "integrity": "sha512-JXoDIh+QnzFb6C5ZqIcUzDkn1fLrxawi98ZbvYb9s7Z2CJLITUWpbTAxSgseczEho18pYhamEBRR/h3o3HIXJQ==",
"requires": {
- "@octokit/types": "^6.1.0",
+ "@octokit/types": "^6.3.2",
"deprecation": "^2.3.1"
}
},
@@ -1010,11 +1010,11 @@
}
},
"@octokit/types": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.2.1.tgz",
- "integrity": "sha512-jHs9OECOiZxuEzxMZcXmqrEO8GYraHF+UzNVH2ACYh8e/Y7YoT+hUf9ldvVd6zIvWv4p3NdxbQ0xx3ku5BnSiA==",
+ "version": "6.3.2",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.3.2.tgz",
+ "integrity": "sha512-H6cbnDumWOQJneyNKCBWgnktRqTWcEm6gq2cIS3frtVgpCqB8zguromnjIWJW375btjnxwmbYBTEAEouruZ2Yw==",
"requires": {
- "@octokit/openapi-types": "^2.2.0",
+ "@octokit/openapi-types": "^2.3.1",
"@types/node": ">= 8"
}
},
@@ -1111,9 +1111,9 @@
}
},
"@types/node": {
- "version": "14.14.20",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz",
- "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A=="
+ "version": "14.14.21",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz",
+ "integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
@@ -2343,22 +2343,24 @@
}
},
"es-abstract": {
- "version": "1.18.0-next.1",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
- "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+ "version": "1.18.0-next.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
+ "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==",
"requires": {
+ "call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
- "is-negative-zero": "^2.0.0",
+ "is-negative-zero": "^2.0.1",
"is-regex": "^1.1.1",
- "object-inspect": "^1.8.0",
+ "object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
- "object.assign": "^4.1.1",
- "string.prototype.trimend": "^1.0.1",
- "string.prototype.trimstart": "^1.0.1"
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.3",
+ "string.prototype.trimstart": "^1.0.3"
}
},
"es-to-primitive": {
@@ -5654,9 +5656,9 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"parse-json": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
- "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
@@ -7315,9 +7317,9 @@
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
},
"vue-prism-component": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/vue-prism-component/-/vue-prism-component-2.0.0.tgz",
- "integrity": "sha512-1ofrL+GCZOv4HqtX5W3EgkhSAgadSeuD8FDTXbwhLy8kS+28RCR8t2S5VTeM9U/peAaXLBpSgRt3J25ao8KTeg=="
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/vue-prism-component/-/vue-prism-component-1.2.0.tgz",
+ "integrity": "sha512-0N9CNuQu+36CJpdsZHrhdq7d18oBvjVMjawyKdIr8xuzFWLfdxECZQYbFaYoopPBg3SvkEEMtkhYqdgTQl5Y+A=="
},
"w3c-hr-time": {
"version": "1.0.2",
diff --git a/package.json b/package.json
index a12d77b9..49b0059b 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"scripts": {
"start": "node source/app/web/index.mjs",
"test": "npx jest",
- "upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest colors@latest compression@latest ejs@latest express@latest express-rate-limit@latest image-to-base64@latest memory-cache@latest prismjs@latest puppeteer@latest svgo@latest vue@latest vue-prism-component@latest faker@latest jest@latest js-yaml@latest libxmljs@latest"
+ "upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest colors@latest compression@latest ejs@latest express@latest express-rate-limit@latest image-to-base64@latest memory-cache@latest prismjs@latest puppeteer@latest svgo@latest vue@latest faker@latest jest@latest js-yaml@latest libxmljs@latest"
},
"repository": {
"type": "git",
@@ -35,7 +35,7 @@
"puppeteer": "^5.5.0",
"svgo": "^1.3.2",
"vue": "^2.6.12",
- "vue-prism-component": "^2.0.0"
+ "vue-prism-component": "^1.2.0"
},
"devDependencies": {
"faker": "^5.1.0",
diff --git a/settings.example.json b/settings.example.json
index c3ce40b2..405e8d95 100644
--- a/settings.example.json
+++ b/settings.example.json
@@ -12,6 +12,9 @@
"debug":false, "//":"Debug mode",
"mocked":false, "//":"Use mocked data",
"repositories":100, "//":"Number of repositories to use to compute metrics",
+ "community":{ "//":"Community settings",
+ "templates":[], "//":"Community templates"
+ },
"templates":{ "//":"Template configuration",
"default":"classic", "//":"Default template",
"enabled":[], "//":"Enabled templates, leave empty to enable all templates"
diff --git a/source/app/action/index.mjs b/source/app/action/index.mjs
index e4be393c..cf8b5e48 100644
--- a/source/app/action/index.mjs
+++ b/source/app/action/index.mjs
@@ -46,8 +46,14 @@
}
}
+ //Pre-Setup
+ const community = {
+ templates:input.array("setup_community_templates")
+ }
+ info("Setup - community templates", community.templates)
+
//Load configuration
- const {conf, Plugins, Templates} = await setup({log:false, nosettings:true})
+ const {conf, Plugins, Templates} = await setup({log:false, nosettings:true, community})
info("Setup", "complete")
info("Version", conf.package.version)
diff --git a/source/app/setup.mjs b/source/app/setup.mjs
index 74a43552..7dc53676 100644
--- a/source/app/setup.mjs
+++ b/source/app/setup.mjs
@@ -3,11 +3,13 @@
import path from "path"
import util from "util"
import url from "url"
+ import processes from "child_process"
+
const Templates = {}
const Plugins = {}
/** Setup */
- export default async function ({log = true, nosettings = false} = {}) {
+ export default async function ({log = true, nosettings = false, community = {}} = {}) {
//Paths
const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../..")
@@ -49,6 +51,7 @@
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}))
@@ -58,6 +61,47 @@
conf.package = JSON.parse(`${await fs.promises.readFile(__package)}`)
logger(`metrics/setup > load package.json > success`)
+ //Load community template
+ 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(/^(?[\s\S]+?)@(?[\s\S]+?):(?[\s\S]+?)(?[+]trust)?$/)?.groups
+ 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"))
+ }
+ 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 template
@@ -72,7 +116,7 @@
conf.templates[name] = {image, style, fonts, partials, views:[directory]}
//Cache templates scripts
- Templates[name] = (await import(url.pathToFileURL(path.join(directory, "template.mjs")).href)).default
+ Templates[name] = (await import(url.pathToFileURL(path.join(fs.existsSync(path.join(directory, "templates.mjs")) ? directory : path.join(__templates, "classic"), "template.mjs")).href)).default
logger(`metrics/setup > load template [${name}] > success`)
//Debug
if (conf.settings.debug) {
diff --git a/tests/metrics.test.js b/tests/metrics.test.js
index 80f7c6fd..bc16fe58 100644
--- a/tests/metrics.test.js
+++ b/tests/metrics.test.js
@@ -313,4 +313,17 @@
...input
})).toBe(true), 60*1e3)
})
- )
\ No newline at end of file
+ )
+
+ describe("Additional options", () => {
+ test("Community templates", async () => expect(await action.run({
+ token:"MOCKED_TOKEN",
+ plugin_pagespeed_token:"MOCKED_TOKEN",
+ plugin_tweets_token:"MOCKED_TOKEN",
+ plugin_music_token:"MOCKED_CLIENT_ID, MOCKED_CLIENT_SECRET, MOCKED_REFRESH_TOKEN",
+ template:"@classic", base:"",
+ config_timezone:"Europe/Paris",
+ plugins_errors_fatal:true, dryrun:true, use_mocked_data:true, verify:true,
+ setup_community_templates:"lowlighter/metrics@master:classic",
+ })).toBe(true), 60*1e3)
+ })