diff --git a/.coderrabbit.yml b/.coderrabbit.yml new file mode 100644 index 000000000000..e673f66e890d --- /dev/null +++ b/.coderrabbit.yml @@ -0,0 +1,45 @@ +language: "en-US" +reviews: + profile: "assertive" + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + auto_review: + enabled: true +tools: + spellcheck: + enabled: true + markdownlint: + enabled: true + biome: + enabled: true + github-checks: + enabled: true + timeout_ms: 180000 + languagetool: + enabled: true + enabled_only: false + level: default + hadolint: + enabled: true + yamllint: + enabled: true + gitleaks: + enabled: true + eslint: + enabled: true + actionlint: + enabled: true + semgrep: + enabled: true +chat: + auto_reply: true +knowledge_base: + opt_out: false + learnings: + scope: "local" + issues: + scope: "local" + pull_requests: + scope: "local" diff --git a/.eslintrc b/.eslintrc index ddab7e016ee3..f77da8144e9c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,7 +9,8 @@ "env": { "browser": true, "es2021": true, - "node": true + "node": true, + "jest": true }, "plugins": [ "react", @@ -313,4 +314,4 @@ } } ] -} \ No newline at end of file +} diff --git a/.github/workflows/update-maintainers-trigger.yaml b/.github/workflows/update-maintainers-trigger.yaml new file mode 100644 index 000000000000..12fc4abe4f06 --- /dev/null +++ b/.github/workflows/update-maintainers-trigger.yaml @@ -0,0 +1,28 @@ +# This action is centrally managed in https://github.com/asyncapi/.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo + +name: Trigger MAINTAINERS.yaml file update + +on: + push: + branches: [ master ] + paths: + # Check all valid CODEOWNERS locations: + # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location + - 'CODEOWNERS' + - '.github/CODEOWNERS' + - '.docs/CODEOWNERS' + +jobs: + trigger-maintainers-update: + name: Trigger updating MAINTAINERS.yaml because of CODEOWNERS change + runs-on: ubuntu-latest + + steps: + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # https://github.com/peter-evans/repository-dispatch/releases/tag/v3.0.0 + with: + # The PAT with the 'public_repo' scope is required + token: ${{ secrets.GH_TOKEN }} + repository: ${{ github.repository_owner }}/community + event-type: trigger-maintainers-update diff --git a/components/campaigns/banners.ts b/components/campaigns/banners.ts index 0620e6bdde9a..7aad4527c830 100644 --- a/components/campaigns/banners.ts +++ b/components/campaigns/banners.ts @@ -17,12 +17,12 @@ export function shouldShowBanner(cfpDeadline: string) { export const banners = [ { - title: "AsyncAPI Online Conference'24", - city: 'YouTube', - dateLocation: '30th of October, 2024 | YouTube & LinkedIn', - cfaText: 'Join us Live', - eventName: 'the AsyncAPI Online Conference', - cfpDeadline: '2024-10-30T06:00:00Z', - link: 'https://www.youtube.com/live/F9wHxd-v2f0?si=kPCqgUzqAKC0FaqJ' + title: 'AsyncAPI Conference', + city: 'Paris Edition', + dateLocation: '5th of December, 2024 | France, Paris', + cfaText: 'Get Your Tickets', + eventName: 'the AsyncAPI Conf in Paris', + cfpDeadline: '2024-12-01T06:00:00Z', + link: 'https://conference.asyncapi.com/#tickets' } ]; diff --git a/config/MAINTAINERS.json b/config/MAINTAINERS.json index 60932854b74b..d94f257ad868 100644 --- a/config/MAINTAINERS.json +++ b/config/MAINTAINERS.json @@ -181,8 +181,8 @@ "github": "jonaslagoni", "linkedin": "jonaslagoni", "slack": "UQ2ANBG1E", + "company": "EventStack", "availableForHire": false, - "company": "Postman", "isTscMember": true, "repos": [ "spec-json-schemas", @@ -250,8 +250,7 @@ "linkedin": "lukasz-gornicki-a621914", "slack": "UD698Q5LM", "twitter": "derberq", - "availableForHire": false, - "company": "Postman", + "availableForHire": true, "isTscMember": true, "repos": [ "spec", @@ -315,7 +314,6 @@ "github": "AceTheCreator", "twitter": "_acebuild", "slack": "U01RWDD69PZ", - "company": "Postman", "availableForHire": false, "isTscMember": true, "repos": [ @@ -411,8 +409,8 @@ "slack": "U01N6AW5V5G", "twitter": "amzani", "linkedin": "amzani", + "company": "Apideck", "availableForHire": false, - "company": "Postman", "isTscMember": true, "repos": [ "studio", @@ -426,8 +424,8 @@ "linkedin": "smoya", "slack": "UN22ZTLHG", "twitter": "smoyac", + "company": "Timescale", "availableForHire": false, - "company": "Postman", "isTscMember": true, "repos": [ "spec", @@ -452,8 +450,7 @@ "slack": "U01SGCZMJKW", "twitter": "souvik_ns", "linkedin": "souvik-de-a2b941169", - "availableForHire": false, - "company": "Postman", + "availableForHire": true, "isTscMember": true, "repos": [ "cli", @@ -468,8 +465,7 @@ "twitter": "QuetzalliWrites", "slack": "U02AKC14WAJ", "linkedin": "quetzalli-writes", - "availableForHire": false, - "company": "Postman", + "availableForHire": true, "isTscMember": true, "repos": [ "website" @@ -987,5 +983,18 @@ "kotlin-asyncapi" ], "githubID": 758568 + }, + { + "name": "Ashmit Jagtap", + "github": "ashmit-coder", + "linkedin": "ashmit-jagtap", + "twitter": "AshmitJagtap", + "slack": "U06RA7GDHU1", + "availableForHire": true, + "isTscMember": true, + "repos": [ + "conference-website" + ], + "githubID": 69006513 } ] \ No newline at end of file diff --git a/dashboard.json b/dashboard.json index d4ecb4b72ce9..a0f838482148 100644 --- a/dashboard.json +++ b/dashboard.json @@ -17,26 +17,109 @@ "score": 34.46095064991105 }, { - "id": "PR_kwDOFLhIt855u7Eb", + "id": "I_kwDODou01c5BZZv-", + "isPR": false, + "isAssigned": false, + "title": "Open Graph link preview image according to the document to open", + "author": "smoya", + "resourcePath": "/asyncapi/studio/issues/224", + "repo": "asyncapi/studio", + "labels": [ + { + "name": "enhancement", + "color": "a2eeef" + }, + { + "name": "keep-open", + "color": "f9dd4b" + } + ], + "score": 31.302030173669205 + }, + { + "id": "I_kwDOGQYLdM5AX1lK", + "isPR": false, + "isAssigned": true, + "title": "Brand Refresh: Mascot", + "author": "mcturco", + "resourcePath": "/asyncapi/brand/issues/12", + "repo": "asyncapi/brand", + "labels": [ + { + "name": ":art: design", + "color": "0D67D3" + }, + { + "name": "bounty", + "color": "0E8A16" + } + ], + "score": 22.112443333692923 + }, + { + "id": "PR_kwDOFLhIt85bqKL8", "isPR": true, "isAssigned": false, - "title": "docs: added community marketing strategy doc", - "author": "iambami", - "resourcePath": "/asyncapi/community/pull/1358", + "title": "docs: add Bounty Program Rules", + "author": "aeworxet", + "resourcePath": "/asyncapi/community/pull/897", "repo": "asyncapi/community", "labels": [], - "score": 19.527872034949596 + "score": 22.112443333692923 + }, + { + "id": "I_kwDOBW5R_c5BIl5P", + "isPR": false, + "isAssigned": false, + "title": "Add new page for collecting user testing participants", + "author": "mcturco", + "resourcePath": "/asyncapi/website/issues/529", + "repo": "asyncapi/website", + "labels": [ + { + "name": "enhancement", + "color": "84b6eb" + }, + { + "name": "Epic", + "color": "3E4B9E" + }, + { + "name": "keep-open", + "color": "ffee84" + }, + { + "name": "area/design", + "color": "0d67d3" + }, + { + "name": "area/javascript", + "color": "ededed" + } + ], + "score": 21.538094156194408 }, { - "id": "PR_kwDOBW5R_c52BRgf", + "id": "PR_kwDOBW5R_c5-T7mG", "isPR": true, "isAssigned": false, - "title": "feat: added test for build-rss.js", + "title": "feat: add tests for build post list script", "author": "vishvamsinh28", - "resourcePath": "/asyncapi/website/pull/3101", + "resourcePath": "/asyncapi/website/pull/3284", "repo": "asyncapi/website", "labels": [], - "score": 19.527872034949596 + "score": 20.38939580119737 + }, + { + "id": "PR_kwDOFLhIt855u7Eb", + "isPR": true, + "isAssigned": false, + "title": "docs: added marketing strategy doc", + "author": "iambami", + "resourcePath": "/asyncapi/community/pull/1358", + "repo": "asyncapi/community", + "labels": [], + "score": 20.102221212448114 }, { "id": "PR_kwDOFLhIt85oVQqh", @@ -50,24 +133,20 @@ "score": 18.666348268701817 }, { - "id": "I_kwDOGQYLdM5AX1lK", - "isPR": false, - "isAssigned": true, - "title": "Brand Refresh: Mascot", - "author": "mcturco", - "resourcePath": "/asyncapi/brand/issues/12", - "repo": "asyncapi/brand", + "id": "PR_kwDOBW5R_c535wDj", + "isPR": true, + "isAssigned": false, + "title": "feat: add test for combine tools script", + "author": "vishvamsinh28", + "resourcePath": "/asyncapi/website/pull/3136", + "repo": "asyncapi/website", "labels": [ { - "name": ":art: design", - "color": "0D67D3" - }, - { - "name": "bounty", - "color": "0E8A16" + "name": "gsoc", + "color": "F4D03F" } ], - "score": 17.230475324955524 + "score": 16.65612614745701 }, { "id": "PR_kwDOFLhIt853IEwA", @@ -91,44 +170,6 @@ "labels": [], "score": 15.220253203710714 }, - { - "id": "PR_kwDOCHlHJM54CmhW", - "isPR": true, - "isAssigned": false, - "title": "fix: add the migration guide and nunjucks depreciation notes", - "author": "Gmin2", - "resourcePath": "/asyncapi/generator/pull/1253", - "repo": "asyncapi/generator", - "labels": [], - "score": 13.78438025996442 - }, - { - "id": "I_kwDODou01c5o2x-Z", - "isPR": false, - "isAssigned": false, - "title": "Start using a React framework", - "author": "fmvilas", - "resourcePath": "/asyncapi/studio/issues/661", - "repo": "asyncapi/studio", - "labels": [], - "score": 13.210031082465903 - }, - { - "id": "I_kwDOFLhIt85xI2wH", - "isPR": false, - "isAssigned": false, - "title": "Measure AsyncAPI Adoption", - "author": "fmvilas", - "resourcePath": "/asyncapi/community/issues/879", - "repo": "asyncapi/community", - "labels": [ - { - "name": "stale", - "color": "ededed" - } - ], - "score": 12.922856493716644 - }, { "id": "PR_kwDOBW5R_c59wJxU", "isPR": true, @@ -138,69 +179,79 @@ "resourcePath": "/asyncapi/website/pull/3276", "repo": "asyncapi/website", "labels": [], - "score": 11.199808961221091 - }, + "score": 14.07155484871368 + } + ], + "goodFirstIssues": [ { - "id": "I_kwDODwv8N86BkfYV", - "isPR": false, + "id": "I_kwDOFLhIt86dkhlL", + "title": "Design for mentors for promotion", "isAssigned": false, - "title": "Add Gallery Section to AACoT'24 Conference Website", - "author": "Mayaleeeee", - "resourcePath": "/asyncapi/conference-website/issues/264", - "repo": "asyncapi/conference-website", + "resourcePath": "/asyncapi/community/issues/1582", + "repo": "asyncapi/community", + "author": "iambami", + "area": "design", "labels": [ { - "name": "Hacktoberfest", - "color": "FF8AE2" + "name": ":loudspeaker: marketing", + "color": "a829e2" } - ], - "score": 10.912634372471834 - } - ], - "goodFirstIssues": [ + ] + }, { - "id": "I_kwDODwv8N86bdV6Z", - "title": "Links in the Resources Hub Should Open in a New Window", + "id": "I_kwDOFLhIt86dker5", + "title": "Designs for the mentees selected for the mentorship program for promotion", "isAssigned": false, - "resourcePath": "/asyncapi/conference-website/issues/434", - "repo": "asyncapi/conference-website", - "author": "AceTheCreator", - "area": "Unknown", + "resourcePath": "/asyncapi/community/issues/1581", + "repo": "asyncapi/community", + "author": "iambami", + "area": "design", "labels": [ { - "name": "Hacktoberfest", - "color": "FF8AE2" + "name": ":loudspeaker: marketing", + "color": "a829e2" } ] }, { - "id": "I_kwDOFLhIt86bdQd-", - "title": "Add Proposed Project Ideas to Mentorship Directory", + "id": "I_kwDOBW5R_c6ddpHW", + "title": "[BUG] algolia search icon not visible on website navbar", "isAssigned": false, - "resourcePath": "/asyncapi/community/issues/1564", - "repo": "asyncapi/community", - "author": "AceTheCreator", + "resourcePath": "/asyncapi/website/issues/3371", + "repo": "asyncapi/website", + "author": "anshgoyalevil", "area": "Unknown", "labels": [ { - "name": "Hacktoberfest", - "color": "FF8AE2" + "name": "bug", + "color": "ee0701" } ] }, { - "id": "I_kwDOBW5R_c6aKzLD", - "title": "[FEATURE] Add coderabbit configuration to the repo", + "id": "I_kwDOBW5R_c6crqQz", + "title": "Improve image type detection in build-rss.js", "isAssigned": true, - "resourcePath": "/asyncapi/website/issues/3293", + "resourcePath": "/asyncapi/website/issues/3357", "repo": "asyncapi/website", - "author": "akshatnema", + "author": "coderabbitai", "area": "Unknown", "labels": [ { "name": "enhancement", "color": "84b6eb" - }, + } + ] + }, + { + "id": "I_kwDOFLhIt86bdQd-", + "title": "Add Proposed Project Ideas to Mentorship Directory", + "isAssigned": false, + "resourcePath": "/asyncapi/community/issues/1564", + "repo": "asyncapi/community", + "author": "AceTheCreator", + "area": "Unknown", + "labels": [ { "name": "Hacktoberfest", "color": "FF8AE2" @@ -241,21 +292,6 @@ } ] }, - { - "id": "I_kwDOCVQpZM6YZc4E", - "title": "Remove this redundant \"undefined\"", - "isAssigned": false, - "resourcePath": "/asyncapi/asyncapi-react/issues/1053", - "repo": "asyncapi/asyncapi-react", - "author": "AceTheCreator", - "area": "Unknown", - "labels": [ - { - "name": "Hacktoberfest", - "color": "FF8AE2" - } - ] - }, { "id": "I_kwDOCVQpZM6YZbiE", "title": "Remove this redundant \"undefined\"", @@ -290,51 +326,6 @@ } ] }, - { - "id": "I_kwDODwv8N86Vde8t", - "title": "Sponsor announcement design for social media", - "isAssigned": false, - "resourcePath": "/asyncapi/conference-website/issues/380", - "repo": "asyncapi/conference-website", - "author": "thulieblack", - "area": "Unknown", - "labels": [ - { - "name": "design", - "color": "5D0F46" - } - ] - }, - { - "id": "I_kwDODwv8N86VdePb", - "title": "The conference countdown banner", - "isAssigned": false, - "resourcePath": "/asyncapi/conference-website/issues/378", - "repo": "asyncapi/conference-website", - "author": "thulieblack", - "area": "Unknown", - "labels": [ - { - "name": "design", - "color": "5D0F46" - } - ] - }, - { - "id": "I_kwDOBW5R_c6VIyCf", - "title": "[Docs Bug 🐞 report]: Broken link to Generator Github Actions", - "isAssigned": false, - "resourcePath": "/asyncapi/website/issues/3190", - "repo": "asyncapi/website", - "author": "chinma-yyy", - "area": "Unknown", - "labels": [ - { - "name": "🐞 docs bug", - "color": "FFD23F" - } - ] - }, { "id": "I_kwDOGQYLdM6VGsJA", "title": "Design a Graphic for the Member Spotlight Post", @@ -453,7 +444,12 @@ "repo": "asyncapi/cli", "author": "Amzani", "area": "typescript", - "labels": [] + "labels": [ + { + "name": "stale", + "color": "ededed" + } + ] }, { "id": "I_kwDOFDnrNc6Gp8Qd", @@ -502,7 +498,7 @@ { "id": "I_kwDOE8Qh3857Kllp", "title": "Add loading animation for when playground generate models ", - "isAssigned": true, + "isAssigned": false, "resourcePath": "/asyncapi/modelina/issues/1725", "repo": "asyncapi/modelina", "author": "jonaslagoni", @@ -632,6 +628,10 @@ { "name": "bug", "color": "d73a4a" + }, + { + "name": "stale", + "color": "ededed" } ] }, diff --git a/package-lock.json b/package-lock.json index 837177c7acf7..0749d409a5fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,6 +115,7 @@ "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-tailwindcss": "^3.14.2", "eslint-plugin-unused-imports": "^3.1.0", + "fast-xml-parser": "^4.5.0", "inquirer": "^9.2.14", "jest": "^29.7.0", "postcss-import": "^16.0.1", @@ -13859,6 +13860,29 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -27471,6 +27495,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true, + "license": "MIT" + }, "node_modules/style-loader": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", diff --git a/package.json b/package.json index 3885874ae365..84f538697d4d 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "remark-cli": "^12.0.1", "remark-lint": "^10.0.0", "remark-mdx": "^3.0.1", - "storybook": "^8.2.4" + "storybook": "^8.2.4", + "fast-xml-parser": "^4.5.0" } } diff --git a/scripts/build-rss.js b/scripts/build-rss.js index a5461f5e0baf..673da1398fe0 100644 --- a/scripts/build-rss.js +++ b/scripts/build-rss.js @@ -1,8 +1,8 @@ -const fs = require('fs') +const fs = require('fs').promises const json2xml = require('jgexml/json2xml') function getAllPosts() { - return require('../config/posts.json') + return require('../config/posts.json'); } function clean(s) { @@ -15,61 +15,89 @@ function clean(s) { return s } -module.exports = function rssFeed(type, title, desc, outputPath) { +module.exports = async function rssFeed(type, title, desc, outputPath) { + try { - const posts = getAllPosts()[`${type}`] - .sort((i1, i2) => { - const i1Date = new Date(i1.date) - const i2Date = new Date(i2.date) + let posts = getAllPosts()[`${type}`] + const missingDatePosts = posts.filter(post => !post.date); + posts = posts.filter(post => post.date); + posts.sort((i1, i2) => { + const i1Date = new Date(i1.date); + const i2Date = new Date(i2.date); + if (i1.featured && !i2.featured) return -1; + if (!i1.featured && i2.featured) return 1; + return i2Date - i1Date; + }); - if (i1.featured && !i2.featured) return -1 - if (!i1.featured && i2.featured) return 1 - return i2Date - i1Date - }) + if (missingDatePosts.length > 0) { + throw new Error(`Missing date in posts: ${missingDatePosts.map(p => p.title || p.slug).join(', ')}`); + } + + const base = 'https://www.asyncapi.com' + const tracking = '?utm_source=rss'; + + const feed = {} + const rss = {} + rss['@version'] = '2.0' + rss["@xmlns:atom"] = 'http://www.w3.org/2005/Atom' + rss.channel = {} + rss.channel.title = title + rss.channel.link = `${base}/${outputPath}` + rss.channel["atom:link"] = {} + rss.channel["atom:link"]["@rel"] = 'self' + rss.channel["atom:link"]["@href"] = rss.channel.link + rss.channel["atom:link"]["@type"] = 'application/rss+xml' + rss.channel.description = desc + rss.channel.language = 'en-gb'; + rss.channel.copyright = 'Made with :love: by the AsyncAPI Initiative.'; + rss.channel.webMaster = 'info@asyncapi.io (AsyncAPI Initiative)' + rss.channel.pubDate = new Date().toUTCString() + rss.channel.generator = 'next.js' + rss.channel.item = [] - const base = 'https://www.asyncapi.com' - const tracking = '?utm_source=rss'; + const invalidPosts = posts.filter(post => + !post.title || !post.slug || !post.excerpt || !post.date + ); - const feed = {} - const rss = {} - rss['@version'] = '2.0' - rss["@xmlns:atom"] = 'http://www.w3.org/2005/Atom' - rss.channel = {} - rss.channel.title = title - rss.channel.link = `${base}/${outputPath}` - rss.channel["atom:link"] = {} - rss.channel["atom:link"]["@rel"] = 'self' - rss.channel["atom:link"]["@href"] = rss.channel.link - rss.channel["atom:link"]["@type"] = 'application/rss+xml' - rss.channel.description = desc - rss.channel.language = 'en-gb'; - rss.channel.copyright = 'Made with :love: by the AsyncAPI Initiative.'; - rss.channel.webMaster = 'info@asyncapi.io (AsyncAPI Initiative)' - rss.channel.pubDate = new Date().toUTCString() - rss.channel.generator = 'next.js' - rss.channel.item = [] + if (invalidPosts.length > 0) { + throw new Error(`Missing required fields in posts: ${invalidPosts.map(p => p.title || p.slug).join(', ')}`); + } - for (let post of posts) { - const link = `${base}${post.slug}${tracking}`; - const item = { title: post.title, description: clean(post.excerpt), link, category: type, guid: { '@isPermaLink': true, '': link }, pubDate: new Date(post.date).toUTCString() } - if (post.cover) { - const enclosure = {}; - enclosure["@url"] = base+post.cover; - enclosure["@length"] = 15026; // dummy value, anything works - enclosure["@type"] = 'image/jpeg'; - if (typeof enclosure["@url"] === 'string') { - let tmp = enclosure["@url"].toLowerCase(); - if (tmp.indexOf('.png')>=0) enclosure["@type"] = 'image/png'; - if (tmp.indexOf('.svg')>=0) enclosure["@type"] = 'image/svg+xml'; - if (tmp.indexOf('.webp')>=0) enclosure["@type"] = 'image/webp'; + for (let post of posts) { + const link = `${base}${post.slug}${tracking}`; + const { title, excerpt, date } = post; + const pubDate = new Date(date).toUTCString(); + const description = clean(excerpt); + const guid = { '@isPermaLink': true, '': link }; + const item = { + title, + description, + link, + category: type, + guid, + pubDate + }; + if (post.cover) { + const enclosure = {}; + enclosure["@url"] = base + post.cover; + enclosure["@length"] = 15026; // dummy value, anything works + enclosure["@type"] = 'image/jpeg'; + if (typeof enclosure["@url"] === 'string') { + let tmp = enclosure["@url"].toLowerCase(); + if (tmp.indexOf('.png') >= 0) enclosure["@type"] = 'image/png'; + if (tmp.indexOf('.svg') >= 0) enclosure["@type"] = 'image/svg+xml'; + if (tmp.indexOf('.webp') >= 0) enclosure["@type"] = 'image/webp'; + } + item.enclosure = enclosure; } - item.enclosure = enclosure; + rss.channel.item.push(item) } - rss.channel.item.push(item) - } - feed.rss = rss + feed.rss = rss - const xml = json2xml.getXml(feed,'@','',2) - fs.writeFileSync(`./public/${outputPath}`, xml, 'utf8') + const xml = json2xml.getXml(feed, '@', '', 2); + await fs.writeFile(`./public/${outputPath}`, xml, 'utf8'); + } catch (err) { + throw new Error(`Failed to generate RSS feed: ${err.message}`); + } }; diff --git a/tests/build-rss.test.js b/tests/build-rss.test.js new file mode 100644 index 000000000000..7961740fe5c6 --- /dev/null +++ b/tests/build-rss.test.js @@ -0,0 +1,147 @@ +const fs = require('fs'); +const path = require('path'); +const rssFeed = require('../scripts/build-rss'); +const { XMLParser } = require('fast-xml-parser'); +const parser = new XMLParser({ ignoreAttributes: false }); +const { mockRssData, title, type, desc, missingDateMockData, incompletePostMockData } = require('./fixtures/rssData'); + +describe('rssFeed', () => { + const testOutputDir = path.join(__dirname, '..', 'public', 'test-output'); + const outputPath = 'test-output/rss.xml'; + + beforeAll(async () => { + try { + await fs.promises.mkdir(testOutputDir, { recursive: true }); + } catch (err) { + throw new Error(`Error while creating temp dir: ${err.message}`); + } + }); + + afterAll(async () => { + try { + const files = await fs.promises.readdir(testOutputDir); + await Promise.all(files.map(file => fs.promises.unlink(path.join(testOutputDir, file)))); + await fs.promises.rmdir(testOutputDir); + } catch (err) { + throw new Error(`Error while deleting temp dir: ${err.message}`); + } + }); + + afterEach(() => { + jest.resetModules(); + }); + + it('should generate RSS feed and write to file', async () => { + + jest.doMock('../config/posts.json', () => mockRssData, { virtual: true }); + + await expect(rssFeed(type, title, desc, outputPath)).resolves.toBeUndefined() + + const filePath = path.join(__dirname, '..', 'public', outputPath); + expect(fs.existsSync(filePath)).toBe(true); + const fileContent = fs.readFileSync(filePath, 'utf8'); + expect(fileContent).toContain(' { + jest.doMock('../config/posts.json', () => mockRssData, { virtual: true }); + + await expect(rssFeed(type, title, desc, outputPath)).resolves.toBeUndefined(); + + const filePath = path.join(__dirname, '..', 'public', outputPath); + const fileContent = fs.readFileSync(filePath, 'utf8'); + + const parsedContent = parser.parse(fileContent); + const itemTitles = parsedContent.rss.channel.item.map(item => item.title); + + expect(itemTitles[0]).toBe('Test Post 1'); + expect(itemTitles[1]).toBe('Another Featured Post'); + + expect(itemTitles[2]).toBe('Post with Special Characters: & < > "'); + expect(itemTitles[3]).toBe('Post with UTC Date Format'); + expect(itemTitles[4]).toBe('Non-Featured Post 1'); + expect(itemTitles[5]).toBe('Non-Featured Post 3'); + }); + + it('should sort posts by date in descending order', async () => { + jest.doMock('../config/posts.json', () => mockRssData, { virtual: true }); + + await expect(rssFeed(type, title, desc, outputPath)).resolves.toBeUndefined(); + + const filePath = path.join(__dirname, '..', 'public', outputPath); + const fileContent = fs.readFileSync(filePath, 'utf8'); + + const parsedContent = parser.parse(fileContent); + const itemTitles = parsedContent.rss.channel.item.map(item => item.title); + + expect(itemTitles[0]).toBe('Test Post 1'); + expect(itemTitles[1]).toBe('Another Featured Post') + expect(itemTitles[2]).toBe('Post with Special Characters: & < > "'); + expect(itemTitles[3]).toBe('Post with UTC Date Format'); + expect(itemTitles[4]).toBe('Non-Featured Post 1'); + expect(itemTitles[5]).toBe('Non-Featured Post 3'); + }); + + it('should set correct enclosure type based on image extension', async () => { + jest.doMock('../config/posts.json', () => mockRssData, { virtual: true }); + + await expect(rssFeed(type, title, desc, outputPath)).resolves.toBeUndefined() + + const filePath = path.join(__dirname, '..', 'public', outputPath); + const fileContent = fs.readFileSync(filePath, 'utf8'); + + expect(fileContent).toContain(' { + jest.doMock('../config/posts.json', () => mockRssData, { virtual: true }); + + const invalidOutputPath = "invalid/path"; + + await expect(rssFeed(type, title, desc, invalidOutputPath)).rejects.toThrow(/ENOENT|EACCES/); + + }); + + it('should throw an error when posts.json is malformed', async () => { + jest.doMock('../config/posts.json', () => { + return { invalidKey: [] }; + }, { virtual: true }); + + await expect(rssFeed(type, title, desc, outputPath)).rejects.toThrow('Failed to generate RSS feed'); + + }); + + it('should handle empty posts array', async () => { + const emptyMockData = { blog: [] }; + jest.doMock('../config/posts.json', () => emptyMockData, { virtual: true }); + + await expect(rssFeed(type, title, desc, outputPath)).resolves.toBeUndefined() + + const filePath = path.join(__dirname, '..', 'public', outputPath); + const fileContent = fs.readFileSync(filePath, 'utf8'); + expect(fileContent).toContain(''); + }); + + it('should throw an error when post is missing required fields', async () => { + + jest.doMock('../config/posts.json', () => incompletePostMockData, { virtual: true }); + + await expect(rssFeed(type, title, desc, outputPath)).rejects.toThrow('Missing required fields'); + + }); + + it('should throw an error when a post is missing a date field during sorting', async () => { + + jest.doMock('../config/posts.json', () => missingDateMockData, { virtual: true }); + + await expect(rssFeed(type, title, desc, outputPath)).rejects.toThrow('Failed to generate RSS feed: Missing date in posts: Post without Date'); + + }); + +}); diff --git a/tests/fixtures/rssData.js b/tests/fixtures/rssData.js new file mode 100644 index 000000000000..89717784e51b --- /dev/null +++ b/tests/fixtures/rssData.js @@ -0,0 +1,93 @@ +const mockRssData = { + blog: [ + { + title: 'Non-Featured Post 1', + slug: '/blog/non-featured-post-1', + excerpt: 'This is a non-featured post', + date: '2023-07-05', + featured: false, + }, + { + title: 'Test Post 1', + slug: '/blog/test-post-1', + excerpt: 'This is a featured test post', + date: '2023-07-07', + featured: true, + cover: '/img/test-cover.jpg', + }, + { + title: 'Another Featured Post', + slug: '/blog/another-featured-post', + excerpt: 'This is another featured post', + date: '2023-07-06', + featured: true, + cover: '/img/test-cover.svg', + }, + { + title: 'Non-Featured Post 2', + slug: '/blog/non-featured-post-2', + excerpt: 'This is another non-featured post', + date: '2023-07-03', + featured: false, + cover: '/img/test-cover.webp', + }, + { + title: 'Non-Featured Post 3', + slug: '/blog/non-featured-post-3', + excerpt: 'This is yet another non-featured post', + date: '2023-07-04', + featured: false, + cover: '/img/test-cover.png', + }, + { + title: 'Post with Special Characters: & < > "', + slug: '/blog/special-chars', + excerpt: 'Testing HTML entities & encoding', + date: '2023-07-06T12:00:00Z', + featured: false, + }, + { + title: 'Post with UTC Date Format', + slug: '/blog/utc-date-format', + excerpt: 'This post uses a UTC date format', + date: 'Wed, 05 Jul 2023 12:00:00 GMT', + featured: false, + }, + ], +}; + +const missingDateMockData = { + blog: [ + { + title: 'Post without Date', + slug: '/blog/no-date-post', + excerpt: 'This post is missing a date', + featured: false, + }, + { + title: 'Valid Post', + slug: '/blog/valid-post', + excerpt: 'This post has a valid date', + date: '2024-07-05', + featured: true, + }, + ], +}; + +const incompletePostMockData = { + blog: [ + { + slug: '/blog/incomplete-post', + excerpt: 'This post is incomplete', + date: '2024-07-05', + featured: false, + }, + ], +}; + +const type = 'blog'; +const title = 'Test Blog RSS'; +const desc = 'Test blog RSS feed'; +const outputPath = 'test-output/blog.xml'; + +module.exports = { mockRssData, title, type, desc, outputPath, missingDateMockData, incompletePostMockData };