From c9943ba1ccd30b9fdafd4aaa28fa7b7611222875 Mon Sep 17 00:00:00 2001 From: Antal Orcsik Date: Thu, 13 Jun 2024 18:15:27 +0200 Subject: [PATCH 1/2] implement stacks logic --- .eslintrc.js | 15 ++ index.js | 32 ++-- src/css/stacks.css | 101 +++++++++++++ src/js/changelog-topic.js | 17 +-- src/js/changelog/worker.js | 1 - src/js/integrations/worker.js | 1 - src/js/shared/common.js | 18 +++ src/js/stacks.js | 272 ++++++++++++++++++++++++++++++++++ src/js/stacks/worker.js | 15 ++ 9 files changed, 446 insertions(+), 26 deletions(-) create mode 100644 src/css/stacks.css create mode 100644 src/js/stacks.js create mode 100644 src/js/stacks/worker.js diff --git a/.eslintrc.js b/.eslintrc.js index fed582d..c944fc4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,4 +16,19 @@ module.exports = { 'no-param-reassign': ['error', { props: false }], 'class-methods-use-this': 'off', }, + overrides: [ + { + files: ['./src/**/worker.js'], + rules: { + 'no-return-await': 'off', + 'no-restricted-globals': 'off', + }, + }, + { + files: ['./index.js'], + rules: { + 'no-eval': 'off', + }, + }, + ], }; diff --git a/index.js b/index.js index 724c7df..050bd74 100644 --- a/index.js +++ b/index.js @@ -28,7 +28,6 @@ async function importWorker(workerPath) { if (!importedWorkers[workerName]) { if (workerContent.match(/addEventListener\('fetch',/)) { // Service Worker Syntax - // eslint-disable-next-line no-eval eval(`(() => { function addEventListener(_, cb) { importedWorkers['${workerName}'] = { type: "Service" }; @@ -39,7 +38,6 @@ async function importWorker(workerPath) { } if (workerContent.match(/export default {/)) { // ES6 Module Syntax - // eslint-disable-next-line no-eval eval(`(() => { ${workerContent.replace( /export default {/, @@ -49,10 +47,7 @@ async function importWorker(workerPath) { )} })();`); } - console.log( - `Registered new ${importedWorkers[workerName].type} Worker: ${workerName}`, - importedWorkers[workerName].handler, - ); + process.stdout.write(`[info] Registered new ${importedWorkers[workerName].type} Worker: ${workerName}\n`); } return importedWorkers[workerName]; } @@ -68,6 +63,9 @@ async function getWorker(urlObject) { if (urlObject.pathname.match(/^\/changelog/)) { return importWorker('./src/js/changelog/worker.js'); } + if (urlObject.pathname.match(/^\/stacks/)) { + return importWorker('./src/js/stacks/worker.js'); + } return { type: 'ES6 Module', handler: { @@ -92,16 +90,25 @@ if (webpackConfig.mode === 'development') app.use(hotMiddleware(compiler)); app.get(/\/.*/, async (req, res) => { const urlObject = new URL(`http://${req.hostname}${req.url}`); + process.stdout.write(`[info] Handling request to ${urlObject}\n`); + try { - const content = await fs.promises.readFile(`./dist${urlObject.pathname}`); + const filePath = `./dist${urlObject.pathname}`; + const content = await fs.promises.readFile(filePath); res.statusCode = 200; const extname = path.extname(urlObject.pathname); if (extname === '.js') res.setHeader('Content-Type', 'text/javascript'); if (extname === '.html') res.setHeader('Content-Type', 'text/html'); if (extname === '.json') res.setHeader('Content-Type', 'application/json'); + + process.stdout.write(`[info] Serving local file ${filePath}\n`); + res.end(content); } catch (error) { const requestHandler = await getWorker(urlObject); + + process.stdout.write(`[info] Using request handler ${requestHandler.type}\n`); + const fetchEvent = { request: { url: urlObject, @@ -111,7 +118,14 @@ app.get(/\/.*/, async (req, res) => { res.statusCode = response.status; res.setHeader('Content-Type', response.headers.get('Content-Type')); const text = await response.text(); - res.end(text.replace('https://webflow-scripts.bitrise.io/', '/')); + + process.stdout.write(`[info] Serving response with status ${res.statusCode}\n`); + + res.end( + text + .replace("document.location.host === 'test-e93bfd.webflow.io'", 'true') + .replace('https://webflow-scripts.bitrise.io/', '/'), + ); }, }; @@ -126,5 +140,5 @@ app.get(/\/.*/, async (req, res) => { }); app.listen(port, hostname, () => { - console.log(`Server running at http://${hostname}:${port}/`); + process.stdout.write(`Server running at http://${hostname}:${port}/\n`); }); diff --git a/src/css/stacks.css b/src/css/stacks.css new file mode 100644 index 0000000..9828d60 --- /dev/null +++ b/src/css/stacks.css @@ -0,0 +1,101 @@ +div#stacks-content { + --gray-100: #f8f9fa; + --gray-200: #e9ecef; +} + +div#stacks-content p { + margin: 1rem 0; +} + +div#stacks-content code { + padding: .125rem .25rem; + background: var(--gray-200); + border-radius: .25rem; + font-size: .875rem; +} + +div#stacks-content pre { + padding: 1rem; + background: var(--gray-200); + border-radius: .25rem; + font-size: .875rem; + overflow-x: auto; +} + +div#stacks-content pre code { + padding: 0; + background: none; +} + +div#stacks-content h2 { + font-size: 1.5rem; + line-height: 1.5em; + margin: 1.5rem 0 1rem 0; +} + +div#stacks-content h3 { + font-size: 1.17rem; + line-height: 1.5em; + margin: 1.5rem 0 1rem 0; +} + +div#stacks-content h2 a, +div#stacks-content h3 a { + display: none; +} +div#stacks-content h3:hover a, +div#stacks-content h2:hover a { + display: inline; +} + +div#stacks-content details { + padding: 1rem; + border: 1px solid var(--gray-200); + border-radius: .25rem; + margin-bottom: .5rem; +} +div#stacks-content details summary::-webkit-details-marker { + display:none; +} +div#stacks-content details summary svg { + content: ""; + float: right; + transition: transform .1s; +} +div#stacks-content details[open] summary svg { + transform: rotateZ(180deg); +} + +div#stacks-content .book-hint.info { + border-color: #6bf; + background-color: rgba(102, 187, 255, .1); +} + +div#stacks-content .book-hint.warning { + border-color: #fd6; + background-color: rgba(255, 221, 102, .1); +} + +div#stacks-content .book-hint.danger { + border-color: #f66; + background-color: rgba(255, 102, 102, .1); +} + +div#stacks-content blockquote { + margin: 1rem 0; + padding: .5rem 1rem .5rem .75rem; + border-inline-start: .25rem solid var(--gray-200); + border-radius: .25rem; + font-size: 1rem; + line-height: 1.5rem; +} + +div#stacks-content table tr th, +div#stacks-content table tr td { + padding: .5rem 1rem; + border: 1px solid var(--gray-200); +} + +div#stacks-content table tr:nth-child(2n) { + background: var(--gray-100); +} \ No newline at end of file diff --git a/src/js/changelog-topic.js b/src/js/changelog-topic.js index f0ee6c2..3fe94f6 100644 --- a/src/js/changelog-topic.js +++ b/src/js/changelog-topic.js @@ -1,22 +1,9 @@ import ChangelogService from './changelog/ChangelogService'; import ChangelogTagFactory from './changelog/ChangelogTagFactory'; -import { formatDate, setMetaContent } from './shared/common'; +import { detectTopicFromUrl, formatDate, setMetaContent } from './shared/common'; import '../css/changelog.css'; -/** - * @param {URL} url - * @returns {?string} - */ -function detectTopicFromUrl(url) { - const path = url.pathname; - const match = path.match(/changelog\/(.+)$/); - if (match) { - return match[1]; - } - return null; -} - const tagFactory = new ChangelogTagFactory(); /** @type {HTMLDivElement} */ @@ -24,7 +11,7 @@ const topicMetaSeparator = document.querySelector('#changelog-topic-meta-separat topicMetaSeparator.style.display = 'none'; const url = new URL(document.location.href); -const topicSlugId = detectTopicFromUrl(url); +const topicSlugId = detectTopicFromUrl(url, 'changelog'); /** @type {string} */ const apiBase = document.location.hostname.match(/(localhost|127\.0\.0\.1)/) ? '' : 'https://bitrise.io'; diff --git a/src/js/changelog/worker.js b/src/js/changelog/worker.js index e6c7a5b..610099a 100644 --- a/src/js/changelog/worker.js +++ b/src/js/changelog/worker.js @@ -18,7 +18,6 @@ async function addCorsHeaders(originalResponse) { return response; } -// eslint-disable-next-line no-restricted-globals addEventListener('fetch', (event) => { const urlObject = new URL(event.request.url); let useCors = true; diff --git a/src/js/integrations/worker.js b/src/js/integrations/worker.js index 9259d02..6da32b2 100644 --- a/src/js/integrations/worker.js +++ b/src/js/integrations/worker.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line no-restricted-globals addEventListener('fetch', (event) => { const urlObject = new URL(event.request.url); diff --git a/src/js/shared/common.js b/src/js/shared/common.js index 79236e0..4f6e2cc 100644 --- a/src/js/shared/common.js +++ b/src/js/shared/common.js @@ -93,6 +93,23 @@ function formatDate(date) { return `${month} ${day.replace(/^0/, '')}, ${year}`; } +/** + * @param {URL} url + * @param {string} prefix + * @returns {?string} + */ +function detectTopicFromUrl(url, prefix) { + const path = url.pathname; + if (path.match(new RegExp(`${prefix}/?$`))) { + return ''; + } + const match = path.match(new RegExp(`${prefix}/(.+)$`)); + if (match) { + return match[1]; + } + return null; +} + export { capitalize, fancyConsoleLog, @@ -103,4 +120,5 @@ export { icaseEqual, icaseIncludes, setMetaContent, + detectTopicFromUrl, }; diff --git a/src/js/stacks.js b/src/js/stacks.js new file mode 100644 index 0000000..d29ab1a --- /dev/null +++ b/src/js/stacks.js @@ -0,0 +1,272 @@ +import { detectTopicFromUrl, fancyConsoleLog } from './shared/common'; + +import '../css/stacks.css'; + +const stacksAPIBase = 'https://stacks.bitrise.io/'; + +/** + * @typedef {{ + * content_html: string; + * platform: string; + * stack_flavor: string; + * stack_id: string; + * stack_name: string; + * summary: string; + * updated_at: string; + * }} StackReportData + * @typedef {{ + * content_html: string; + * summary: string; + * title: string; + * updated_at: string; + * }} StacksPageData + */ + +const formatHtml = (html) => { + return html + .replaceAll( + '', + ` + + + + `, + ) + .replaceAll(/\s+<\/code>/g, '') + .replaceAll(/href="\//g, 'href="/stacks/') + .replaceAll('https://stacks.bitrise.io/', '/stacks/'); +}; + +(async () => { + const url = new URL(document.location.href); + const pagePath = detectTopicFromUrl(url, 'stacks').replace(/\/$/, ''); + const pageType = pagePath.split('/')[0]; + + if (pageType === '') { + const response = await fetch(`${stacksAPIBase}`); + /** @type {StacksPageData} */ + const html = await response.text(); + const match = html.match(//gms); + + const dataContainerId = 'stacks-data-container'; + let dataContainer = document.getElementById(dataContainerId); + if (!dataContainer) { + dataContainer = document.createElement('div'); + dataContainer.style.display = 'none'; + dataContainer.id = dataContainerId; + document.querySelector('body').append(dataContainer); + } + [dataContainer.innerHTML] = match; + + const stacksLinks = { + announcements: {}, + xcode: {}, + ubuntu: {}, + aws: {}, + tools: {}, + tips: {}, + }; + + const xcodeStackList = document.getElementById('xcode-stack-list'); + const xcodeStableOnlyStack = document.getElementById('xcode-stable-only').cloneNode(true); + xcodeStableOnlyStack.removeAttribute('id'); + const xcodeStableAndEdgeStack = document.getElementById('xcode-stable-and-edge').cloneNode(true); + xcodeStableAndEdgeStack.removeAttribute('id'); + const xcodeEdgeOnlyStack = document.getElementById('xcode-edge-only').cloneNode(true); + xcodeEdgeOnlyStack.removeAttribute('id'); + [...xcodeStackList.querySelectorAll('.stack-row')].forEach((row) => { + if (row.className.match(/stack-row-header/)) { + // leave header + } else if (row.id === 'xcode-stable-only' || row.id === 'xcode-stable-and-edge' || row.id === 'xcode-edge-only') { + row.style.display = 'none'; + } else { + row.remove(); + } + }); + + const ubuntuStackList = document.getElementById('ubuntu-stack-list'); + const ubuntuStack = ubuntuStackList.querySelectorAll('.stack-row')[1].cloneNode(true); + [...ubuntuStackList.querySelectorAll('.stack-row')].forEach((row, index) => { + if (row.className.match(/stack-row-header/)) { + // leave header + } else if (index === 1) { + row.style.display = 'none'; + } else { + row.remove(); + } + }); + + const awsStackList = document.getElementById('aws-stack-list'); + const awsStack = awsStackList.querySelectorAll('.stack-row')[1].cloneNode(true); + [...awsStackList.querySelectorAll('.stack-row')].forEach((row, index) => { + if (row.className.match(/stack-row-header/)) { + // leave header + } else if (index === 1) { + row.style.display = 'none'; + } else { + row.remove(); + } + }); + + [...dataContainer.querySelectorAll('a')] + .map((link) => { + return [new URL(link.href).pathname, link.innerHTML]; + }) + .forEach(([pathname, title]) => { + const announcementsMatch = pathname.match(/announcements\/([^/]+)/); + if (announcementsMatch) { + stacksLinks.announcements[announcementsMatch[1]] = [pathname, title]; + } + const toolsMatch = pathname.match(/tools\/([^/]+)/); + if (toolsMatch) { + stacksLinks.tools[toolsMatch[1]] = [pathname, title]; + } + const tipsMatch = pathname.match(/tips\/([^/]+)/); + if (tipsMatch) { + stacksLinks.tips[tipsMatch[1]] = [pathname, title]; + } + const awsMatch = pathname.match(/(stack_reports|changelogs)\/aws\/([^/]+)/); + if (awsMatch) { + const page = awsMatch[1]; + const version = awsMatch[2]; + if (!stacksLinks.aws[version]) stacksLinks.aws[version] = {}; + stacksLinks.aws[version].title = title.replace(/ changelog/, '').trim(); + stacksLinks.aws[version][page] = pathname; + } + const xcodeMatch = pathname.match(/(stack_reports|changelogs)\/([^/]+xcode[^/]+)/); + if (xcodeMatch) { + const page = xcodeMatch[1]; + const edge = xcodeMatch[2].match(/-edge/) ? 'edge' : 'stable'; + const version = xcodeMatch[2].replace(/-edge/, ''); + if (!stacksLinks.xcode[version]) stacksLinks.xcode[version] = {}; + if (!stacksLinks.xcode[version][edge]) stacksLinks.xcode[version][edge] = {}; + stacksLinks.xcode[version].title = title.replace(/ with edge updates| changelog/, '').trim(); + stacksLinks.xcode[version][edge][page] = pathname; + } + const ubuntuMatch = pathname.match(/(stack_reports|changelogs)\/(linux[^/]+)/); + if (ubuntuMatch) { + const page = ubuntuMatch[1]; + const version = ubuntuMatch[2]; + if (!stacksLinks.ubuntu[version]) stacksLinks.ubuntu[version] = {}; + stacksLinks.ubuntu[version].title = title.replace(/ changelog/, '').trim(); + stacksLinks.ubuntu[version][page] = pathname; + } + }); + + dataContainer.remove(); + + Object.keys(stacksLinks.xcode) + .sort() + .forEach((version, index) => { + if (!stacksLinks.xcode[version].edge) { + const row = xcodeStableOnlyStack.cloneNode(true); + row.style.removeProperty('display'); + row.querySelector('.stack-version-title').innerHTML = stacksLinks.xcode[version].title; + row.querySelectorAll('.stack-links')[0].querySelector('.stack-link-reports').href = + `/stacks${stacksLinks.xcode[version].stable.stack_reports}`; + row.querySelectorAll('.stack-links')[0].querySelector('.stack-link-changelogs').href = + `/stacks${stacksLinks.xcode[version].stable.changelogs}`; + row.className = row.className.replace('stack-row-odd', ''); + if (index % 2) row.className += ' stack-row-odd'; + xcodeStackList.appendChild(row); + } else if (!stacksLinks.xcode[version].stable) { + const row = xcodeEdgeOnlyStack.cloneNode(true); + row.style.removeProperty('display'); + row.querySelector('.stack-version-title').innerHTML = stacksLinks.xcode[version].title; + row.querySelectorAll('.stack-links')[1].querySelector('.stack-link-reports').href = + `/stacks${stacksLinks.xcode[version].edge.stack_reports}`; + row.querySelectorAll('.stack-links')[1].querySelector('.stack-link-changelogs').href = + `/stacks${stacksLinks.xcode[version].edge.changelogs}`; + row.className = row.className.replace('stack-row-odd', ''); + if (index % 2) row.className += ' stack-row-odd'; + xcodeStackList.appendChild(row); + } else { + const row = xcodeStableAndEdgeStack.cloneNode(true); + row.style.removeProperty('display'); + row.querySelector('.stack-version-title').innerHTML = stacksLinks.xcode[version].title; + row.querySelectorAll('.stack-links')[0].querySelector('.stack-link-reports').href = + `/stacks${stacksLinks.xcode[version].stable.stack_reports}`; + row.querySelectorAll('.stack-links')[0].querySelector('.stack-link-changelogs').href = + stacksLinks.xcode[version].stable.changelogs; + row.querySelectorAll('.stack-links')[1].querySelector('.stack-link-reports').href = + `/stacks${stacksLinks.xcode[version].edge.stack_reports}`; + row.querySelectorAll('.stack-links')[1].querySelector('.stack-link-changelogs').href = + `/stacks${stacksLinks.xcode[version].edge.changelogs}`; + row.className = row.className.replace('stack-row-odd', ''); + if (index % 2) row.className += ' stack-row-odd'; + xcodeStackList.appendChild(row); + } + }); + + Object.keys(stacksLinks.ubuntu) + .sort() + .forEach((version, index) => { + const row = ubuntuStack.cloneNode(true); + row.style.removeProperty('display'); + row.querySelector('.stack-version-title').innerHTML = stacksLinks.ubuntu[version].title; + row.querySelectorAll('.stack-links')[0].querySelector('.stack-link-reports').href = + `/stacks${stacksLinks.ubuntu[version].stack_reports}`; + row.querySelectorAll('.stack-links')[0].querySelector('.stack-link-changelogs').href = + `/stacks${stacksLinks.ubuntu[version].changelogs}`; + row.className = row.className.replace('stack-row-odd', ''); + if (index % 2) row.className += ' stack-row-odd'; + ubuntuStackList.appendChild(row); + }); + + Object.keys(stacksLinks.aws) + .sort() + .forEach((version, index) => { + const row = awsStack.cloneNode(true); + row.style.removeProperty('display'); + row.querySelector('.stack-version-title').innerHTML = stacksLinks.aws[version].title; + row.querySelectorAll('.stack-links')[0].querySelector('.stack-link-reports').href = + `/stacks${stacksLinks.aws[version].stack_reports}`; + row.querySelectorAll('.stack-links')[0].querySelector('.stack-link-changelogs').href = + `/stacks${stacksLinks.aws[version].changelogs}`; + row.className = row.className.replace('stack-row-odd', ''); + if (index % 2) row.className += ' stack-row-odd'; + awsStackList.appendChild(row); + }); + + console.log(stacksLinks); + } else if (pageType === 'changelogs' || pageType === 'announcements' || pageType === 'tools' || pageType === 'tips') { + const response = await fetch(`${stacksAPIBase}${pagePath}/index.json`); + /** @type {StacksPageData} */ + const data = await response.json(); + + document.getElementById('stacks-title').innerHTML = data.title; + document.getElementById('stacks-content').innerHTML = ` +
+

${new Date(data.updated_at).toLocaleDateString()}

+
+ + ${formatHtml(data.content_html)} + `; + } else if (pageType === 'stack_reports') { + const response = await fetch(`${stacksAPIBase}${pagePath}/index.json`); + /** @type {StacksPageData} */ + const data = await response.json(); + + document.getElementById('stacks-title').innerHTML = data.stack_name; + document.getElementById('stacks-content').innerHTML = ` +
+

Stack ID: ${data.stack_id}

+

Current stack revision: ${data.stack_revision}

+ +

This Bitrise stack contains the following software:

+
+ + ${formatHtml(data.content_html)} + `; + } else { + console.log(pageType); + } + + fancyConsoleLog('Bitrise.io Stacks'); +})(); + +if (import.meta.webpackHot) import.meta.webpackHot.accept(); diff --git a/src/js/stacks/worker.js b/src/js/stacks/worker.js new file mode 100644 index 0000000..5128657 --- /dev/null +++ b/src/js/stacks/worker.js @@ -0,0 +1,15 @@ +export default { + async fetch(request) { + const urlObject = new URL(request.url); + + urlObject.hostname = 'webflow.bitrise.io'; + + if (urlObject.pathname.match(/^\/stacks\/.+\/.+/)) { + urlObject.pathname = '/stacks/subpage'; + } else if (urlObject.pathname.match(/\/stacks$|stacks\/(.*)/)) { + urlObject.pathname = '/stacks'; + } + + return await fetch(urlObject, request); + }, +}; From 5fd4fe097ff2144ebbeb9e98d8b0082051ebff92 Mon Sep 17 00:00:00 2001 From: Antal Orcsik Date: Fri, 14 Jun 2024 09:14:39 +0200 Subject: [PATCH 2/2] fix links --- src/js/stacks.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js/stacks.js b/src/js/stacks.js index d29ab1a..73344ce 100644 --- a/src/js/stacks.js +++ b/src/js/stacks.js @@ -110,7 +110,7 @@ const formatHtml = (html) => { [...dataContainer.querySelectorAll('a')] .map((link) => { - return [new URL(link.href).pathname, link.innerHTML]; + return [new URL(link.href).pathname.replace(/\/$/, ''), link.innerHTML]; }) .forEach(([pathname, title]) => { const announcementsMatch = pathname.match(/announcements\/([^/]+)/); @@ -130,7 +130,7 @@ const formatHtml = (html) => { const page = awsMatch[1]; const version = awsMatch[2]; if (!stacksLinks.aws[version]) stacksLinks.aws[version] = {}; - stacksLinks.aws[version].title = title.replace(/ changelog/, '').trim(); + stacksLinks.aws[version].title = title.replace(/ changelogs?/, '').trim(); stacksLinks.aws[version][page] = pathname; } const xcodeMatch = pathname.match(/(stack_reports|changelogs)\/([^/]+xcode[^/]+)/); @@ -140,7 +140,7 @@ const formatHtml = (html) => { const version = xcodeMatch[2].replace(/-edge/, ''); if (!stacksLinks.xcode[version]) stacksLinks.xcode[version] = {}; if (!stacksLinks.xcode[version][edge]) stacksLinks.xcode[version][edge] = {}; - stacksLinks.xcode[version].title = title.replace(/ with edge updates| changelog/, '').trim(); + stacksLinks.xcode[version].title = title.replace(/ with edge updates| changelogs?/, '').trim(); stacksLinks.xcode[version][edge][page] = pathname; } const ubuntuMatch = pathname.match(/(stack_reports|changelogs)\/(linux[^/]+)/); @@ -148,7 +148,7 @@ const formatHtml = (html) => { const page = ubuntuMatch[1]; const version = ubuntuMatch[2]; if (!stacksLinks.ubuntu[version]) stacksLinks.ubuntu[version] = {}; - stacksLinks.ubuntu[version].title = title.replace(/ changelog/, '').trim(); + stacksLinks.ubuntu[version].title = title.replace(/ changelogs?/, '').trim(); stacksLinks.ubuntu[version][page] = pathname; } });