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) + })