From 51460253696bca8bd86f0e837a1c0a9eab40e84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Fri, 20 Feb 2026 16:55:14 -0300 Subject: [PATCH 01/25] feat: create announcements banner --- src/generators/jsx-ast/utils/buildContent.mjs | 1 + src/generators/web/constants.mjs | 4 ++ src/generators/web/index.mjs | 2 + .../components/AnnouncementBanner/index.jsx | 69 +++++++++++++++++++ .../components/AnnouncementBanner/types.d.ts | 11 +++ .../web/ui/utils/__tests__/banner.test.mjs | 63 +++++++++++++++++ src/generators/web/ui/utils/banner.mjs | 20 ++++++ 7 files changed, 170 insertions(+) create mode 100644 src/generators/web/ui/components/AnnouncementBanner/index.jsx create mode 100644 src/generators/web/ui/components/AnnouncementBanner/types.d.ts create mode 100644 src/generators/web/ui/utils/__tests__/banner.test.mjs create mode 100644 src/generators/web/ui/utils/banner.mjs diff --git a/src/generators/jsx-ast/utils/buildContent.mjs b/src/generators/jsx-ast/utils/buildContent.mjs index 12f54825..4650061c 100644 --- a/src/generators/jsx-ast/utils/buildContent.mjs +++ b/src/generators/jsx-ast/utils/buildContent.mjs @@ -279,6 +279,7 @@ export const processEntry = entry => { */ export const createDocumentLayout = (entries, metadata) => createTree('root', [ + createJSXElement(JSX_IMPORTS.AnnouncementBanner.name), createJSXElement(JSX_IMPORTS.Layout.name, { metadata, headings: extractHeadings(entries), diff --git a/src/generators/web/constants.mjs b/src/generators/web/constants.mjs index 5cbfe760..4fed5b54 100644 --- a/src/generators/web/constants.mjs +++ b/src/generators/web/constants.mjs @@ -14,6 +14,10 @@ export const ROOT = dirname(fileURLToPath(import.meta.url)); * An object containing mappings for various JSX components to their import paths. */ export const JSX_IMPORTS = { + AnnouncementBanner: { + name: 'AnnouncementBanner', + source: resolve(ROOT, './ui/components/AnnouncementBanner'), + }, Layout: { name: 'Layout', source: '#theme/Layout', diff --git a/src/generators/web/index.mjs b/src/generators/web/index.mjs index e41b76aa..84644a95 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -42,5 +42,7 @@ export default createLazyGenerator({ '#theme/Layout': join(import.meta.dirname, './ui/components/Layout'), }, virtualImports: {}, + remoteConfig: + 'https://gist.githubusercontent.com/araujogui/8ea72ffaf574f58fca1482e764e8b5c8/raw/16af51e4efbf37da7b6aff9b7e5dd967d955aacf/api-docs.config.json', }, }); diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx new file mode 100644 index 00000000..e526a41c --- /dev/null +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -0,0 +1,69 @@ +import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; +import Banner from '@node-core/ui-components/Common/Banner'; +import { useEffect, useState } from 'preact/hooks'; + +import { STATIC_DATA } from '../../constants.mjs'; +import { isBannerActive } from '../../utils/banner.mjs'; + +/** @import { BannerEntry, RemoteConfig } from './types.d.ts' */ + +/** + * Asynchronously fetches and displays announcement banners from the remote config. + * Global banners are rendered above version-specific ones. + * Non-blocking: silently ignores fetch/parse failures. + */ +export default () => { + const [banners, setBanners] = useState(/** @type {BannerEntry[]} */ ([])); + + useEffect(() => { + const { remoteConfig, versionMajor } = STATIC_DATA; + + if (!remoteConfig) { + return; + } + + fetch(remoteConfig, { + signal: AbortSignal.timeout(2500), + }) + .then(async res => { + if (!res.ok) { + return; + } + + /** @type {RemoteConfig} */ + const config = await res.json(); + + const active = []; + + const globalBanner = config.global?.banner; + if (globalBanner && isBannerActive(globalBanner)) { + active.push(globalBanner); + } + + const versionBanner = config[`v${versionMajor}`]?.banner; + if (versionBanner && isBannerActive(versionBanner)) { + active.push(versionBanner); + } + + setBanners(active); + }) + .catch(error => { + console.error(error); + }); + }, []); + + if (!banners.length) { + return null; + } + + return ( +
+ {banners.map(banner => ( + + {banner.link ? {banner.text} : banner.text} + {banner.link && } + + ))} +
+ ); +}; diff --git a/src/generators/web/ui/components/AnnouncementBanner/types.d.ts b/src/generators/web/ui/components/AnnouncementBanner/types.d.ts new file mode 100644 index 00000000..1c0a152d --- /dev/null +++ b/src/generators/web/ui/components/AnnouncementBanner/types.d.ts @@ -0,0 +1,11 @@ +import type { BannerProps } from '@node-core/ui-components/Common/Banner'; + +export type BannerEntry = { + startDate?: string; + endDate?: string; + text: string; + link?: string; + type?: BannerProps['type']; +}; + +export type RemoteConfig = Record; diff --git a/src/generators/web/ui/utils/__tests__/banner.test.mjs b/src/generators/web/ui/utils/__tests__/banner.test.mjs new file mode 100644 index 00000000..262d2c9d --- /dev/null +++ b/src/generators/web/ui/utils/__tests__/banner.test.mjs @@ -0,0 +1,63 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { isBannerActive } from '../banner.mjs'; + +const PAST = new Date(Date.now() - 86_400_000).toISOString(); // yesterday +const FUTURE = new Date(Date.now() + 86_400_000).toISOString(); // tomorrow + +const banner = (overrides = {}) => ({ + text: 'Test banner', + ...overrides, +}); + +describe('isBannerActive', () => { + describe('no startDate, no endDate', () => { + it('is always active', () => { + assert.equal(isBannerActive(banner()), true); + }); + }); + + describe('startDate only', () => { + it('is active when startDate is in the past', () => { + assert.equal(isBannerActive(banner({ startDate: PAST })), true); + }); + + it('is not active when startDate is in the future', () => { + assert.equal(isBannerActive(banner({ startDate: FUTURE })), false); + }); + }); + + describe('endDate only', () => { + it('is active when endDate is in the future', () => { + assert.equal(isBannerActive(banner({ endDate: FUTURE })), true); + }); + + it('is not active when endDate is in the past', () => { + assert.equal(isBannerActive(banner({ endDate: PAST })), false); + }); + }); + + describe('startDate and endDate', () => { + it('is active when now is within the range', () => { + assert.equal( + isBannerActive(banner({ startDate: PAST, endDate: FUTURE })), + true + ); + }); + + it('is not active when now is before the range', () => { + assert.equal( + isBannerActive(banner({ startDate: FUTURE, endDate: FUTURE })), + false + ); + }); + + it('is not active when now is after the range', () => { + assert.equal( + isBannerActive(banner({ startDate: PAST, endDate: PAST })), + false + ); + }); + }); +}); diff --git a/src/generators/web/ui/utils/banner.mjs b/src/generators/web/ui/utils/banner.mjs new file mode 100644 index 00000000..a3af015c --- /dev/null +++ b/src/generators/web/ui/utils/banner.mjs @@ -0,0 +1,20 @@ +/** @import { BannerEntry } from '../components/AnnouncementBanner/types' */ + +/** + * Checks whether a banner should be displayed based on its date range. + * Both `startDate` and `endDate` are optional; if omitted the banner is + * considered open-ended in that direction. + * + * @param {BannerEntry} banner + * @returns {boolean} + */ +export const isBannerActive = banner => { + const now = Date.now(); + if (banner.startDate && now < new Date(banner.startDate).getTime()) { + return false; + } + if (banner.endDate && now > new Date(banner.endDate).getTime()) { + return false; + } + return true; +}; From d6761cfff2a0e3b40820945b56e16a963b24a773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Sat, 21 Feb 2026 10:26:10 -0300 Subject: [PATCH 02/25] refactor: review --- src/generators/web/constants.mjs | 20 +++++++++++++ src/generators/web/index.mjs | 2 +- .../components/AnnouncementBanner/index.jsx | 29 ++++++++++++------- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/generators/web/constants.mjs b/src/generators/web/constants.mjs index 4fed5b54..3171bd57 100644 --- a/src/generators/web/constants.mjs +++ b/src/generators/web/constants.mjs @@ -22,6 +22,26 @@ export const JSX_IMPORTS = { name: 'Layout', source: '#theme/Layout', }, + NavBar: { + name: 'NavBar', + source: resolve(ROOT, './ui/components/NavBar'), + }, + Article: { + name: 'Article', + source: '@node-core/ui-components/Containers/Article', + }, + SideBar: { + name: 'SideBar', + source: resolve(ROOT, './ui/components/SideBar'), + }, + TableOfContents: { + name: 'TableOfContents', + source: '@node-core/ui-components/Common/TableOfContents', + }, + MetaBar: { + name: 'MetaBar', + source: resolve(ROOT, './ui/components/MetaBar'), + }, CodeBox: { name: 'CodeBox', source: resolve(ROOT, './ui/components/CodeBox'), diff --git a/src/generators/web/index.mjs b/src/generators/web/index.mjs index 84644a95..456cb9db 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -43,6 +43,6 @@ export default createLazyGenerator({ }, virtualImports: {}, remoteConfig: - 'https://gist.githubusercontent.com/araujogui/8ea72ffaf574f58fca1482e764e8b5c8/raw/16af51e4efbf37da7b6aff9b7e5dd967d955aacf/api-docs.config.json', + 'https://raw.githubusercontent.com/nodejs/nodejs.org/main/apps/site/site.json', }, }); diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index e526a41c..ebd83ef2 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -2,7 +2,6 @@ import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; import Banner from '@node-core/ui-components/Common/Banner'; import { useEffect, useState } from 'preact/hooks'; -import { STATIC_DATA } from '../../constants.mjs'; import { isBannerActive } from '../../utils/banner.mjs'; /** @import { BannerEntry, RemoteConfig } from './types.d.ts' */ @@ -11,13 +10,13 @@ import { isBannerActive } from '../../utils/banner.mjs'; * Asynchronously fetches and displays announcement banners from the remote config. * Global banners are rendered above version-specific ones. * Non-blocking: silently ignores fetch/parse failures. + * + * @param {{ remoteConfig: string, versionMajor: number | null }} props */ -export default () => { +export default ({ remoteConfig, versionMajor }) => { const [banners, setBanners] = useState(/** @type {BannerEntry[]} */ ([])); useEffect(() => { - const { remoteConfig, versionMajor } = STATIC_DATA; - if (!remoteConfig) { return; } @@ -40,9 +39,11 @@ export default () => { active.push(globalBanner); } - const versionBanner = config[`v${versionMajor}`]?.banner; - if (versionBanner && isBannerActive(versionBanner)) { - active.push(versionBanner); + if (versionMajor != null) { + const versionBanner = config[`v${versionMajor}`]?.banner; + if (versionBanner && isBannerActive(versionBanner)) { + active.push(versionBanner); + } } setBanners(active); @@ -57,11 +58,17 @@ export default () => { } return ( -
+
{banners.map(banner => ( - - {banner.link ? {banner.text} : banner.text} - {banner.link && } + + {banner.link ? ( + + {banner.text} + + + ) : ( + banner.text + )} ))}
From bd97266ace1f20de332eff32b4d350e823269eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Sat, 21 Feb 2026 11:25:06 -0300 Subject: [PATCH 03/25] refactor: review --- .../web/ui/components/AnnouncementBanner/index.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index ebd83ef2..b8bde491 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -34,13 +34,13 @@ export default ({ remoteConfig, versionMajor }) => { const active = []; - const globalBanner = config.global?.banner; + const globalBanner = config.websiteBanners?.index; if (globalBanner && isBannerActive(globalBanner)) { active.push(globalBanner); } if (versionMajor != null) { - const versionBanner = config[`v${versionMajor}`]?.banner; + const versionBanner = config.websiteBanners[`v${versionMajor}`]; if (versionBanner && isBannerActive(versionBanner)) { active.push(versionBanner); } @@ -64,11 +64,11 @@ export default ({ remoteConfig, versionMajor }) => { {banner.link ? ( {banner.text} - ) : ( banner.text )} + {banner.link && } ))}
From ff7e2a26348eb147f07ecd60a17291afec57a4d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 12 Mar 2026 19:28:44 -0300 Subject: [PATCH 04/25] feat: update remote config url --- src/generators/jsx-ast/index.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/generators/jsx-ast/index.mjs b/src/generators/jsx-ast/index.mjs index da61cb56..b90c3f06 100644 --- a/src/generators/jsx-ast/index.mjs +++ b/src/generators/jsx-ast/index.mjs @@ -18,6 +18,7 @@ export default createLazyGenerator({ defaultConfiguration: { ref: 'main', + remoteConfig: 'https://nodejs.org/site.json', }, hasParallelProcessor: true, From 04a975d87df1d6b86cd7f8ecd04f979342e5522d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 12 Mar 2026 19:29:09 -0300 Subject: [PATCH 05/25] refactor: split fetch util --- .../__tests__/fetchBanners.test.mjs | 166 ++++++++++++++++++ .../AnnouncementBanner/fetchBanners.mjs | 38 ++++ .../components/AnnouncementBanner/index.jsx | 37 +--- .../components/AnnouncementBanner/types.d.ts | 4 +- 4 files changed, 212 insertions(+), 33 deletions(-) create mode 100644 src/generators/web/ui/components/AnnouncementBanner/__tests__/fetchBanners.test.mjs create mode 100644 src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs diff --git a/src/generators/web/ui/components/AnnouncementBanner/__tests__/fetchBanners.test.mjs b/src/generators/web/ui/components/AnnouncementBanner/__tests__/fetchBanners.test.mjs new file mode 100644 index 00000000..89dbbe94 --- /dev/null +++ b/src/generators/web/ui/components/AnnouncementBanner/__tests__/fetchBanners.test.mjs @@ -0,0 +1,166 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { fetchBanners } from '../fetchBanners.mjs'; + +const PAST = new Date(Date.now() - 86_400_000).toISOString(); // yesterday +const FUTURE = new Date(Date.now() + 86_400_000).toISOString(); // tomorrow + +const makeResponse = (banners, ok = true) => ({ + ok, + json: async () => ({ websiteBanners: banners }), +}); + +describe('fetchBanners', () => { + describe('fetch behavior', () => { + it('fetches from the given URL', async t => { + t.mock.method(global, 'fetch', () => Promise.resolve(makeResponse({}))); + + await fetchBanners('https://example.com/site.json', null); + + assert.equal(global.fetch.mock.calls.length, 1); + assert.equal( + global.fetch.mock.calls[0].arguments[0], + 'https://example.com/site.json' + ); + }); + + it('returns an empty array on non-ok response', async t => { + t.mock.method(global, 'fetch', () => + Promise.resolve(makeResponse({}, false)) + ); + + const result = await fetchBanners('https://example.com/site.json', null); + + assert.deepEqual(result, []); + }); + + it('propagates fetch errors to the caller', async t => { + t.mock.method(global, 'fetch', () => + Promise.reject(new Error('Network error')) + ); + + await assert.rejects( + () => fetchBanners('https://example.com/site.json', null), + { message: 'Network error' } + ); + }); + }); + + describe('banner selection', () => { + it('returns the active global (index) banner', async t => { + const banner = { text: 'Global banner', type: 'warning' }; + t.mock.method(global, 'fetch', () => + Promise.resolve(makeResponse({ index: banner })) + ); + + const result = await fetchBanners('https://example.com/site.json', null); + + assert.deepEqual(result, [banner]); + }); + + it('returns the active version-specific banner', async t => { + const banner = { text: 'v20 banner', type: 'warning' }; + t.mock.method(global, 'fetch', () => + Promise.resolve(makeResponse({ v20: banner })) + ); + + const result = await fetchBanners('https://example.com/site.json', 20); + + assert.deepEqual(result, [banner]); + }); + + it('returns both global and version banners when both are active', async t => { + const globalBanner = { text: 'Global banner', type: 'warning' }; + const versionBanner = { text: 'v20 banner', type: 'error' }; + t.mock.method(global, 'fetch', () => + Promise.resolve( + makeResponse({ index: globalBanner, v20: versionBanner }) + ) + ); + + const result = await fetchBanners('https://example.com/site.json', 20); + + assert.deepEqual(result, [globalBanner, versionBanner]); + }); + + it('returns global banner first, version banner second', async t => { + const globalBanner = { text: 'Global', type: 'warning' }; + const versionBanner = { text: 'v22', type: 'error' }; + t.mock.method(global, 'fetch', () => + Promise.resolve( + makeResponse({ index: globalBanner, v22: versionBanner }) + ) + ); + + const result = await fetchBanners('https://example.com/site.json', 22); + + assert.equal(result[0], globalBanner); + assert.equal(result[1], versionBanner); + }); + + it('does not include the version banner when versionMajor is null', async t => { + const globalBanner = { text: 'Global banner', type: 'warning' }; + const versionBanner = { text: 'v20 banner', type: 'error' }; + t.mock.method(global, 'fetch', () => + Promise.resolve( + makeResponse({ index: globalBanner, v20: versionBanner }) + ) + ); + + const result = await fetchBanners('https://example.com/site.json', null); + + assert.deepEqual(result, [globalBanner]); + }); + + it('returns an empty array when websiteBanners is absent', async t => { + t.mock.method(global, 'fetch', () => + Promise.resolve({ ok: true, json: async () => ({}) }) + ); + + const result = await fetchBanners('https://example.com/site.json', null); + + assert.deepEqual(result, []); + }); + }); + + describe('date filtering', () => { + it('excludes a banner whose endDate has passed', async t => { + const banner = { text: 'Expired', type: 'warning', endDate: PAST }; + t.mock.method(global, 'fetch', () => + Promise.resolve(makeResponse({ index: banner })) + ); + + const result = await fetchBanners('https://example.com/site.json', null); + + assert.deepEqual(result, []); + }); + + it('excludes a banner whose startDate is in the future', async t => { + const banner = { text: 'Upcoming', type: 'warning', startDate: FUTURE }; + t.mock.method(global, 'fetch', () => + Promise.resolve(makeResponse({ index: banner })) + ); + + const result = await fetchBanners('https://example.com/site.json', null); + + assert.deepEqual(result, []); + }); + + it('includes a banner within its active date range', async t => { + const banner = { + text: 'Active', + type: 'warning', + startDate: PAST, + endDate: FUTURE, + }; + t.mock.method(global, 'fetch', () => + Promise.resolve(makeResponse({ index: banner })) + ); + + const result = await fetchBanners('https://example.com/site.json', null); + + assert.deepEqual(result, [banner]); + }); + }); +}); diff --git a/src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs b/src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs new file mode 100644 index 00000000..48d1ff7e --- /dev/null +++ b/src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs @@ -0,0 +1,38 @@ +/** @import { BannerEntry, RemoteConfig } from './types.d.ts' */ + +import { isBannerActive } from '../../utils/banner.mjs'; + +/** + * Fetches and returns active banners for the given version from the remote config. + * Returns an empty array on any fetch or parse failure. + * + * @param {string} remoteConfig + * @param {number | null} versionMajor + * @returns {Promise} + */ +export const fetchBanners = async (remoteConfig, versionMajor) => { + const res = await fetch(remoteConfig, { signal: AbortSignal.timeout(2500) }); + + if (!res.ok) { + return []; + } + + /** @type {RemoteConfig} */ + const config = await res.json(); + + const active = []; + + const globalBanner = config.websiteBanners?.index; + if (globalBanner && isBannerActive(globalBanner)) { + active.push(globalBanner); + } + + if (versionMajor != null) { + const versionBanner = config.websiteBanners?.[`v${versionMajor}`]; + if (versionBanner && isBannerActive(versionBanner)) { + active.push(versionBanner); + } + } + + return active; +}; diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index b8bde491..0561730c 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -2,9 +2,9 @@ import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; import Banner from '@node-core/ui-components/Common/Banner'; import { useEffect, useState } from 'preact/hooks'; -import { isBannerActive } from '../../utils/banner.mjs'; +import { fetchBanners } from './fetchBanners.mjs'; -/** @import { BannerEntry, RemoteConfig } from './types.d.ts' */ +/** @import { BannerEntry } from './types.d.ts' */ /** * Asynchronously fetches and displays announcement banners from the remote config. @@ -21,36 +21,9 @@ export default ({ remoteConfig, versionMajor }) => { return; } - fetch(remoteConfig, { - signal: AbortSignal.timeout(2500), - }) - .then(async res => { - if (!res.ok) { - return; - } - - /** @type {RemoteConfig} */ - const config = await res.json(); - - const active = []; - - const globalBanner = config.websiteBanners?.index; - if (globalBanner && isBannerActive(globalBanner)) { - active.push(globalBanner); - } - - if (versionMajor != null) { - const versionBanner = config.websiteBanners[`v${versionMajor}`]; - if (versionBanner && isBannerActive(versionBanner)) { - active.push(versionBanner); - } - } - - setBanners(active); - }) - .catch(error => { - console.error(error); - }); + fetchBanners(remoteConfig, versionMajor) + .then(setBanners) + .catch(console.error); }, []); if (!banners.length) { diff --git a/src/generators/web/ui/components/AnnouncementBanner/types.d.ts b/src/generators/web/ui/components/AnnouncementBanner/types.d.ts index 1c0a152d..bdd3b605 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/types.d.ts +++ b/src/generators/web/ui/components/AnnouncementBanner/types.d.ts @@ -8,4 +8,6 @@ export type BannerEntry = { type?: BannerProps['type']; }; -export type RemoteConfig = Record; +export type RemoteConfig = { + websiteBanners?: Record; +}; From f2ca2f5a9e917788c2e6067531ecbc1a9a80db8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Fri, 20 Mar 2026 21:25:55 -0300 Subject: [PATCH 06/25] refactor: simplify --- src/generators/web/ui/utils/banner.mjs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/generators/web/ui/utils/banner.mjs b/src/generators/web/ui/utils/banner.mjs index a3af015c..b8721152 100644 --- a/src/generators/web/ui/utils/banner.mjs +++ b/src/generators/web/ui/utils/banner.mjs @@ -8,13 +8,10 @@ * @param {BannerEntry} banner * @returns {boolean} */ -export const isBannerActive = banner => { +export const isBannerActive = ({ startDate, endDate }) => { const now = Date.now(); - if (banner.startDate && now < new Date(banner.startDate).getTime()) { - return false; - } - if (banner.endDate && now > new Date(banner.endDate).getTime()) { - return false; - } - return true; + return ( + (!startDate || now >= new Date(startDate)) && + (!endDate || now <= new Date(endDate)) + ); }; From 9546fc8123773499436988604b5c74581151d6cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Fri, 20 Mar 2026 21:30:06 -0300 Subject: [PATCH 07/25] refactor: add version skip comment --- .../web/ui/components/AnnouncementBanner/fetchBanners.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs b/src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs index 48d1ff7e..8b34cfb6 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs +++ b/src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs @@ -27,6 +27,7 @@ export const fetchBanners = async (remoteConfig, versionMajor) => { active.push(globalBanner); } + // no version info available, skip version-specific banner if (versionMajor != null) { const versionBanner = config.websiteBanners?.[`v${versionMajor}`]; if (versionBanner && isBannerActive(versionBanner)) { From 6eca586c34f034355a69cf8f4062ed8ca304268b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 23 Mar 2026 19:26:48 -0300 Subject: [PATCH 08/25] fix: cls --- .../web/ui/components/AnnouncementBanner/index.jsx | 3 ++- .../components/AnnouncementBanner/index.module.css | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 src/generators/web/ui/components/AnnouncementBanner/index.module.css diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index 0561730c..2c704c98 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -3,6 +3,7 @@ import Banner from '@node-core/ui-components/Common/Banner'; import { useEffect, useState } from 'preact/hooks'; import { fetchBanners } from './fetchBanners.mjs'; +import styles from './index.module.css'; /** @import { BannerEntry } from './types.d.ts' */ @@ -31,7 +32,7 @@ export default ({ remoteConfig, versionMajor }) => { } return ( -
+
{banners.map(banner => ( {banner.link ? ( diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.module.css b/src/generators/web/ui/components/AnnouncementBanner/index.module.css new file mode 100644 index 00000000..d8a38d10 --- /dev/null +++ b/src/generators/web/ui/components/AnnouncementBanner/index.module.css @@ -0,0 +1,14 @@ +.banners { + animation: slideDown 300ms ease-out 400ms backwards; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-0.5rem); + } + to { + opacity: 1; + transform: translateY(0); + } +} From 5fee0176b39ec3f7b1ec629ff591ea2b65129b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 23 Mar 2026 19:36:20 -0300 Subject: [PATCH 09/25] refactor: transform into hook --- package-lock.json | 617 +++++++++++------- package.json | 4 +- ...chBanners.test.mjs => useBanners.test.mjs} | 130 +++- .../AnnouncementBanner/fetchBanners.mjs | 39 -- .../components/AnnouncementBanner/index.jsx | 17 +- .../AnnouncementBanner/useBanners.mjs | 56 ++ 6 files changed, 527 insertions(+), 336 deletions(-) rename src/generators/web/ui/components/AnnouncementBanner/__tests__/{fetchBanners.test.mjs => useBanners.test.mjs} (51%) delete mode 100644 src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs create mode 100644 src/generators/web/ui/components/AnnouncementBanner/useBanners.mjs diff --git a/package-lock.json b/package-lock.json index 3f72098b..acca745a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,8 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@reporters/github": "^1.12.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", "@types/mdast": "^4.0.4", "@types/node": "^24.10.1", "@types/semver": "^7.7.1", @@ -121,6 +123,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -881,16 +918,21 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@noble/hashes": { @@ -1048,9 +1090,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.120.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", - "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -2063,9 +2105,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", "cpu": [ "arm64" ], @@ -2079,9 +2121,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", "cpu": [ "arm64" ], @@ -2095,9 +2137,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", "cpu": [ "x64" ], @@ -2111,9 +2153,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", "cpu": [ "x64" ], @@ -2127,9 +2169,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", - "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", "cpu": [ "arm" ], @@ -2143,9 +2185,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", "cpu": [ "arm64" ], @@ -2159,9 +2201,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", "cpu": [ "arm64" ], @@ -2175,9 +2217,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", "cpu": [ "ppc64" ], @@ -2191,9 +2233,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", "cpu": [ "s390x" ], @@ -2207,9 +2249,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", "cpu": [ "x64" ], @@ -2223,9 +2265,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", "cpu": [ "x64" ], @@ -2239,9 +2281,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", "cpu": [ "arm64" ], @@ -2255,9 +2297,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", - "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", "cpu": [ "wasm32" ], @@ -2270,26 +2312,10 @@ "node": ">=14.0.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", "cpu": [ "arm64" ], @@ -2303,9 +2329,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", "cpu": [ "x64" ], @@ -2319,9 +2345,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", - "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", "license": "MIT" }, "node_modules/@rollup/plugin-virtual": { @@ -2495,9 +2521,9 @@ } }, "node_modules/@swc/html-wasm": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/html-wasm/-/html-wasm-1.15.18.tgz", - "integrity": "sha512-nABVlYRZjfTJA3bTEf7w6Gu8GgRfFJZqTAJ+ehJzwKtCreMy4QFBGiv3KkCjIjxXg+U8qrpnqgo9SjVOq3lPEw==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/html-wasm/-/html-wasm-1.15.21.tgz", + "integrity": "sha512-2Wo2S5DbfAswldF/X5gSmqgTy4uMBEUix7zYcIFsXgZyVJ8PDPE19cgxSBkauJxxGXfvo5rVCcEjx0XrJHpDEA==", "license": "Apache-2.0" }, "node_modules/@tailwindcss/node": { @@ -2717,64 +2743,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.8.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.8.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", @@ -2826,6 +2794,54 @@ "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", "license": "MIT" }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2836,6 +2852,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2937,14 +2960,14 @@ "license": "MIT" }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", - "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.2", - "@typescript-eslint/types": "^8.57.2", + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "engines": { @@ -2955,18 +2978,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", - "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2" + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2977,9 +3000,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", - "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", "dev": true, "license": "MIT", "engines": { @@ -2990,21 +3013,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", - "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3015,13 +3038,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", - "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", "dev": true, "license": "MIT", "engines": { @@ -3033,21 +3056,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.2", - "@typescript-eslint/tsconfig-utils": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3057,20 +3080,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", - "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2" + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3081,17 +3104,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", - "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3128,6 +3151,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -3141,6 +3165,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -3167,6 +3192,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3180,6 +3206,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -3193,6 +3220,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3206,6 +3234,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3219,6 +3248,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3232,6 +3262,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3245,6 +3276,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3258,6 +3290,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3271,6 +3304,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3284,6 +3318,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3297,6 +3332,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3310,6 +3346,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3323,6 +3360,7 @@ "wasm32" ], "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" @@ -3331,6 +3369,19 @@ "node": ">=14.0.0" } }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.2.tgz", @@ -3339,6 +3390,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3352,6 +3404,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3365,6 +3418,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3567,6 +3621,16 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", @@ -3615,9 +3679,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4109,6 +4173,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -4342,9 +4413,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "62.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.8.0.tgz", - "integrity": "sha512-hu3r9/6JBmPG6wTcqtYzgZAnjEG2eqRUATfkFscokESg1VDxZM21ZaMire0KjeMwfj+SXvgB4Rvh5LBuesj92w==", + "version": "62.8.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.8.1.tgz", + "integrity": "sha512-e9358PdHgvcMF98foNd3L7hVCw70Lt+YcSL7JzlJebB8eT5oRJtW6bHMQKoAwJtw6q0q0w/fRIr2kwnHdFDI6A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4646,9 +4717,9 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true, "license": "ISC" }, @@ -5305,6 +5376,13 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jsdoc-type-pratt-parser": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.1.1.tgz", @@ -5871,15 +5949,25 @@ } }, "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -6779,13 +6867,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -7065,9 +7153,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -7264,9 +7352,9 @@ } }, "node_modules/preact-render-to-string": { - "version": "6.6.6", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.6.6.tgz", - "integrity": "sha512-EfqZJytnjJldV+YaaqhthU2oXsEf5e+6rDv957p+zxAvNfFLQOPfvBOTncscQ+akzu6Wrl7s3Pa0LjUQmWJsGQ==", + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.6.7.tgz", + "integrity": "sha512-3XdbsX3+vn9dQW+jJI/FsI9rlkgl6dbeUpqLsChak6jp3j3auFqBCkno7VChbMFs5Q8ylBj6DrUkKRwtVN3nvw==", "license": "MIT", "peerDependencies": { "preact": ">=10 || >= 11.0.0-0" @@ -7298,6 +7386,44 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", @@ -7345,18 +7471,12 @@ "node": ">=0.10.0" } }, - "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.0" - } + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" }, "node_modules/react-markdown": { "version": "10.1.0", @@ -7744,13 +7864,13 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", - "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.120.0", - "@rolldown/pluginutils": "1.0.0-rc.10" + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" @@ -7759,29 +7879,22 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-x64": "1.0.0-rc.10", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT", - "peer": true + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } }, "node_modules/semver": { "version": "7.7.4", @@ -8211,9 +8324,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -8272,9 +8385,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -8351,9 +8464,9 @@ } }, "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "license": "MIT", "engines": { "node": ">=18.17" diff --git a/package.json b/package.json index 885c7512..00b61e0f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "format": "prettier .", "format:write": "prettier --write .", "format:check": "prettier --check .", - "test": "node --test --experimental-test-module-mocks \"src/**/*.test.mjs\"", + "test": "node --test --experimental-test-module-mocks --import=global-jsdom/register \"src/**/*.test.mjs\"", "test:coverage": "c8 node --test --experimental-test-module-mocks \"src/**/*.test.mjs\"", "test:ci": "c8 --reporter=lcov node --test --experimental-test-module-mocks \"src/**/*.test.mjs\" --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=junit.xml --test-reporter=spec --test-reporter-destination=stdout", "test:update-snapshots": "node --test --experimental-test-module-mocks --test-update-snapshots \"src/**/*.test.mjs\"", @@ -28,6 +28,8 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@reporters/github": "^1.12.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", "@types/mdast": "^4.0.4", "@types/node": "^24.10.1", "@types/semver": "^7.7.1", diff --git a/src/generators/web/ui/components/AnnouncementBanner/__tests__/fetchBanners.test.mjs b/src/generators/web/ui/components/AnnouncementBanner/__tests__/useBanners.test.mjs similarity index 51% rename from src/generators/web/ui/components/AnnouncementBanner/__tests__/fetchBanners.test.mjs rename to src/generators/web/ui/components/AnnouncementBanner/__tests__/useBanners.test.mjs index 89dbbe94..552dc87d 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/__tests__/fetchBanners.test.mjs +++ b/src/generators/web/ui/components/AnnouncementBanner/__tests__/useBanners.test.mjs @@ -1,7 +1,9 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { fetchBanners } from '../fetchBanners.mjs'; +import { renderHook, waitFor } from '@testing-library/react'; + +import { useBanners } from '../useBanners.mjs'; const PAST = new Date(Date.now() - 86_400_000).toISOString(); // yesterday const FUTURE = new Date(Date.now() + 86_400_000).toISOString(); // tomorrow @@ -11,14 +13,19 @@ const makeResponse = (banners, ok = true) => ({ json: async () => ({ websiteBanners: banners }), }); -describe('fetchBanners', () => { +describe('useBanners', () => { describe('fetch behavior', () => { it('fetches from the given URL', async t => { t.mock.method(global, 'fetch', () => Promise.resolve(makeResponse({}))); - await fetchBanners('https://example.com/site.json', null); + renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: null, + }) + ); - assert.equal(global.fetch.mock.calls.length, 1); + await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); assert.equal( global.fetch.mock.calls[0].arguments[0], 'https://example.com/site.json' @@ -30,20 +37,31 @@ describe('fetchBanners', () => { Promise.resolve(makeResponse({}, false)) ); - const result = await fetchBanners('https://example.com/site.json', null); + const { result } = renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: null, + }) + ); - assert.deepEqual(result, []); + await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); + assert.deepEqual(result.current.banners, []); }); - it('propagates fetch errors to the caller', async t => { + it('handles fetch errors silently', async t => { t.mock.method(global, 'fetch', () => Promise.reject(new Error('Network error')) ); - await assert.rejects( - () => fetchBanners('https://example.com/site.json', null), - { message: 'Network error' } + const { result } = renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: null, + }) ); + + await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); + assert.deepEqual(result.current.banners, []); }); }); @@ -54,9 +72,14 @@ describe('fetchBanners', () => { Promise.resolve(makeResponse({ index: banner })) ); - const result = await fetchBanners('https://example.com/site.json', null); + const { result } = renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: null, + }) + ); - assert.deepEqual(result, [banner]); + await waitFor(() => assert.deepEqual(result.current.banners, [banner])); }); it('returns the active version-specific banner', async t => { @@ -65,9 +88,14 @@ describe('fetchBanners', () => { Promise.resolve(makeResponse({ v20: banner })) ); - const result = await fetchBanners('https://example.com/site.json', 20); + const { result } = renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: 20, + }) + ); - assert.deepEqual(result, [banner]); + await waitFor(() => assert.deepEqual(result.current.banners, [banner])); }); it('returns both global and version banners when both are active', async t => { @@ -79,9 +107,16 @@ describe('fetchBanners', () => { ) ); - const result = await fetchBanners('https://example.com/site.json', 20); + const { result } = renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: 20, + }) + ); - assert.deepEqual(result, [globalBanner, versionBanner]); + await waitFor(() => + assert.deepEqual(result.current.banners, [globalBanner, versionBanner]) + ); }); it('returns global banner first, version banner second', async t => { @@ -93,10 +128,17 @@ describe('fetchBanners', () => { ) ); - const result = await fetchBanners('https://example.com/site.json', 22); + const { result } = renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: 22, + }) + ); - assert.equal(result[0], globalBanner); - assert.equal(result[1], versionBanner); + await waitFor(() => { + assert.equal(result.current.banners[0], globalBanner); + assert.equal(result.current.banners[1], versionBanner); + }); }); it('does not include the version banner when versionMajor is null', async t => { @@ -108,9 +150,16 @@ describe('fetchBanners', () => { ) ); - const result = await fetchBanners('https://example.com/site.json', null); + const { result } = renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: null, + }) + ); - assert.deepEqual(result, [globalBanner]); + await waitFor(() => + assert.deepEqual(result.current.banners, [globalBanner]) + ); }); it('returns an empty array when websiteBanners is absent', async t => { @@ -118,9 +167,15 @@ describe('fetchBanners', () => { Promise.resolve({ ok: true, json: async () => ({}) }) ); - const result = await fetchBanners('https://example.com/site.json', null); + const { result } = renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: null, + }) + ); - assert.deepEqual(result, []); + await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); + assert.deepEqual(result.current.banners, []); }); }); @@ -131,9 +186,15 @@ describe('fetchBanners', () => { Promise.resolve(makeResponse({ index: banner })) ); - const result = await fetchBanners('https://example.com/site.json', null); + const { result } = renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: null, + }) + ); - assert.deepEqual(result, []); + await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); + assert.deepEqual(result.current.banners, []); }); it('excludes a banner whose startDate is in the future', async t => { @@ -142,9 +203,15 @@ describe('fetchBanners', () => { Promise.resolve(makeResponse({ index: banner })) ); - const result = await fetchBanners('https://example.com/site.json', null); + const { result } = renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: null, + }) + ); - assert.deepEqual(result, []); + await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); + assert.deepEqual(result.current.banners, []); }); it('includes a banner within its active date range', async t => { @@ -158,9 +225,14 @@ describe('fetchBanners', () => { Promise.resolve(makeResponse({ index: banner })) ); - const result = await fetchBanners('https://example.com/site.json', null); + const { result } = renderHook(() => + useBanners({ + remoteConfig: 'https://example.com/site.json', + versionMajor: null, + }) + ); - assert.deepEqual(result, [banner]); + await waitFor(() => assert.deepEqual(result.current.banners, [banner])); }); }); }); diff --git a/src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs b/src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs deleted file mode 100644 index 8b34cfb6..00000000 --- a/src/generators/web/ui/components/AnnouncementBanner/fetchBanners.mjs +++ /dev/null @@ -1,39 +0,0 @@ -/** @import { BannerEntry, RemoteConfig } from './types.d.ts' */ - -import { isBannerActive } from '../../utils/banner.mjs'; - -/** - * Fetches and returns active banners for the given version from the remote config. - * Returns an empty array on any fetch or parse failure. - * - * @param {string} remoteConfig - * @param {number | null} versionMajor - * @returns {Promise} - */ -export const fetchBanners = async (remoteConfig, versionMajor) => { - const res = await fetch(remoteConfig, { signal: AbortSignal.timeout(2500) }); - - if (!res.ok) { - return []; - } - - /** @type {RemoteConfig} */ - const config = await res.json(); - - const active = []; - - const globalBanner = config.websiteBanners?.index; - if (globalBanner && isBannerActive(globalBanner)) { - active.push(globalBanner); - } - - // no version info available, skip version-specific banner - if (versionMajor != null) { - const versionBanner = config.websiteBanners?.[`v${versionMajor}`]; - if (versionBanner && isBannerActive(versionBanner)) { - active.push(versionBanner); - } - } - - return active; -}; diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index 2c704c98..fc3481cc 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -1,11 +1,8 @@ import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; import Banner from '@node-core/ui-components/Common/Banner'; -import { useEffect, useState } from 'preact/hooks'; -import { fetchBanners } from './fetchBanners.mjs'; import styles from './index.module.css'; - -/** @import { BannerEntry } from './types.d.ts' */ +import { useBanners } from './useBanners.mjs'; /** * Asynchronously fetches and displays announcement banners from the remote config. @@ -15,17 +12,7 @@ import styles from './index.module.css'; * @param {{ remoteConfig: string, versionMajor: number | null }} props */ export default ({ remoteConfig, versionMajor }) => { - const [banners, setBanners] = useState(/** @type {BannerEntry[]} */ ([])); - - useEffect(() => { - if (!remoteConfig) { - return; - } - - fetchBanners(remoteConfig, versionMajor) - .then(setBanners) - .catch(console.error); - }, []); + const { banners } = useBanners({ remoteConfig, versionMajor }); if (!banners.length) { return null; diff --git a/src/generators/web/ui/components/AnnouncementBanner/useBanners.mjs b/src/generators/web/ui/components/AnnouncementBanner/useBanners.mjs new file mode 100644 index 00000000..f6c957c7 --- /dev/null +++ b/src/generators/web/ui/components/AnnouncementBanner/useBanners.mjs @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; + +import { isBannerActive } from '../../utils/banner.mjs'; + +/** @import { BannerEntry, RemoteConfig } from './types.d.ts' */ + +/** + * Fetches and returns active banners for the given version. + * Returns an empty array until loaded or on any failure. + * + * @param {{ remoteConfig: string, versionMajor: number | null }} props + * @returns {{ banners: BannerEntry[] }} + */ +export const useBanners = ({ remoteConfig, versionMajor }) => { + const [banners, setBanners] = useState(/** @type {BannerEntry[]} */ ([])); + + useEffect(() => { + if (!remoteConfig) { + return; + } + + /** @returns {Promise} */ + const load = async () => { + const res = await fetch(remoteConfig, { + signal: AbortSignal.timeout(2500), + }); + + if (!res.ok) { + return; + } + + /** @type {RemoteConfig} */ + const config = await res.json(); + const active = []; + + const globalBanner = config.websiteBanners?.index; + if (globalBanner && isBannerActive(globalBanner)) { + active.push(globalBanner); + } + + // no version info available, skip version-specific banner + if (versionMajor != null) { + const versionBanner = config.websiteBanners?.[`v${versionMajor}`]; + if (versionBanner && isBannerActive(versionBanner)) { + active.push(versionBanner); + } + } + + setBanners(active); + }; + + load().catch(console.error); + }, []); + + return { banners }; +}; From ddd8771fe907106e0a139c8dec496c2cf5747d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 23 Mar 2026 20:12:18 -0300 Subject: [PATCH 10/25] feat: set lazy loading --- .../__tests__/useBanners.test.mjs | 136 +++++------------- .../components/AnnouncementBanner/index.jsx | 71 +++++---- .../AnnouncementBanner/loadBanners.mjs | 47 ++++++ .../AnnouncementBanner/useBanners.mjs | 56 -------- src/generators/web/utils/generate.mjs | 10 +- 5 files changed, 135 insertions(+), 185 deletions(-) create mode 100644 src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs delete mode 100644 src/generators/web/ui/components/AnnouncementBanner/useBanners.mjs diff --git a/src/generators/web/ui/components/AnnouncementBanner/__tests__/useBanners.test.mjs b/src/generators/web/ui/components/AnnouncementBanner/__tests__/useBanners.test.mjs index 552dc87d..cb0560c4 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/__tests__/useBanners.test.mjs +++ b/src/generators/web/ui/components/AnnouncementBanner/__tests__/useBanners.test.mjs @@ -1,9 +1,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { renderHook, waitFor } from '@testing-library/react'; - -import { useBanners } from '../useBanners.mjs'; +import { loadBanners } from '../loadBanners.mjs'; const PAST = new Date(Date.now() - 86_400_000).toISOString(); // yesterday const FUTURE = new Date(Date.now() + 86_400_000).toISOString(); // tomorrow @@ -13,19 +11,14 @@ const makeResponse = (banners, ok = true) => ({ json: async () => ({ websiteBanners: banners }), }); -describe('useBanners', () => { +describe('loadBanners', () => { describe('fetch behavior', () => { it('fetches from the given URL', async t => { t.mock.method(global, 'fetch', () => Promise.resolve(makeResponse({}))); - renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: null, - }) - ); + await loadBanners('https://example.com/site.json', null); - await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); + assert.equal(global.fetch.mock.calls.length, 1); assert.equal( global.fetch.mock.calls[0].arguments[0], 'https://example.com/site.json' @@ -37,15 +30,9 @@ describe('useBanners', () => { Promise.resolve(makeResponse({}, false)) ); - const { result } = renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: null, - }) - ); + const result = await loadBanners('https://example.com/site.json', null); - await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); - assert.deepEqual(result.current.banners, []); + assert.deepEqual(result, []); }); it('handles fetch errors silently', async t => { @@ -53,15 +40,18 @@ describe('useBanners', () => { Promise.reject(new Error('Network error')) ); - const { result } = renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: null, - }) - ); + const result = await loadBanners('https://example.com/site.json', null); + + assert.deepEqual(result, []); + }); + + it('returns an empty array when remoteConfig is absent', async t => { + t.mock.method(global, 'fetch', () => Promise.resolve(makeResponse({}))); - await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); - assert.deepEqual(result.current.banners, []); + const result = await loadBanners(undefined, null); + + assert.deepEqual(result, []); + assert.equal(global.fetch.mock.calls.length, 0); }); }); @@ -72,14 +62,9 @@ describe('useBanners', () => { Promise.resolve(makeResponse({ index: banner })) ); - const { result } = renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: null, - }) - ); + const result = await loadBanners('https://example.com/site.json', null); - await waitFor(() => assert.deepEqual(result.current.banners, [banner])); + assert.deepEqual(result, [banner]); }); it('returns the active version-specific banner', async t => { @@ -88,14 +73,9 @@ describe('useBanners', () => { Promise.resolve(makeResponse({ v20: banner })) ); - const { result } = renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: 20, - }) - ); + const result = await loadBanners('https://example.com/site.json', 20); - await waitFor(() => assert.deepEqual(result.current.banners, [banner])); + assert.deepEqual(result, [banner]); }); it('returns both global and version banners when both are active', async t => { @@ -107,16 +87,9 @@ describe('useBanners', () => { ) ); - const { result } = renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: 20, - }) - ); + const result = await loadBanners('https://example.com/site.json', 20); - await waitFor(() => - assert.deepEqual(result.current.banners, [globalBanner, versionBanner]) - ); + assert.deepEqual(result, [globalBanner, versionBanner]); }); it('returns global banner first, version banner second', async t => { @@ -128,17 +101,10 @@ describe('useBanners', () => { ) ); - const { result } = renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: 22, - }) - ); + const result = await loadBanners('https://example.com/site.json', 22); - await waitFor(() => { - assert.equal(result.current.banners[0], globalBanner); - assert.equal(result.current.banners[1], versionBanner); - }); + assert.equal(result[0], globalBanner); + assert.equal(result[1], versionBanner); }); it('does not include the version banner when versionMajor is null', async t => { @@ -150,16 +116,9 @@ describe('useBanners', () => { ) ); - const { result } = renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: null, - }) - ); + const result = await loadBanners('https://example.com/site.json', null); - await waitFor(() => - assert.deepEqual(result.current.banners, [globalBanner]) - ); + assert.deepEqual(result, [globalBanner]); }); it('returns an empty array when websiteBanners is absent', async t => { @@ -167,15 +126,9 @@ describe('useBanners', () => { Promise.resolve({ ok: true, json: async () => ({}) }) ); - const { result } = renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: null, - }) - ); + const result = await loadBanners('https://example.com/site.json', null); - await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); - assert.deepEqual(result.current.banners, []); + assert.deepEqual(result, []); }); }); @@ -186,15 +139,9 @@ describe('useBanners', () => { Promise.resolve(makeResponse({ index: banner })) ); - const { result } = renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: null, - }) - ); + const result = await loadBanners('https://example.com/site.json', null); - await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); - assert.deepEqual(result.current.banners, []); + assert.deepEqual(result, []); }); it('excludes a banner whose startDate is in the future', async t => { @@ -203,15 +150,9 @@ describe('useBanners', () => { Promise.resolve(makeResponse({ index: banner })) ); - const { result } = renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: null, - }) - ); + const result = await loadBanners('https://example.com/site.json', null); - await waitFor(() => assert.equal(global.fetch.mock.calls.length, 1)); - assert.deepEqual(result.current.banners, []); + assert.deepEqual(result, []); }); it('includes a banner within its active date range', async t => { @@ -225,14 +166,9 @@ describe('useBanners', () => { Promise.resolve(makeResponse({ index: banner })) ); - const { result } = renderHook(() => - useBanners({ - remoteConfig: 'https://example.com/site.json', - versionMajor: null, - }) - ); + const result = await loadBanners('https://example.com/site.json', null); - await waitFor(() => assert.deepEqual(result.current.banners, [banner])); + assert.deepEqual(result, [banner]); }); }); }); diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index fc3481cc..83eca72f 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -1,37 +1,56 @@ import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; import Banner from '@node-core/ui-components/Common/Banner'; +import { lazy, Suspense } from 'preact/compat'; +import { useMemo } from 'preact/hooks'; import styles from './index.module.css'; -import { useBanners } from './useBanners.mjs'; +import { loadBanners } from './loadBanners.mjs'; + +/** @import { BannerEntry } from './types.d.ts' */ -/** - * Asynchronously fetches and displays announcement banners from the remote config. - * Global banners are rendered above version-specific ones. - * Non-blocking: silently ignores fetch/parse failures. - * - * @param {{ remoteConfig: string, versionMajor: number | null }} props - */ export default ({ remoteConfig, versionMajor }) => { - const { banners } = useBanners({ remoteConfig, versionMajor }); + const LazyBanners = useMemo( + () => + lazy(async () => { + const active = await loadBanners(remoteConfig, versionMajor); + + if (!active.length) { + return { default: () => null }; + } - if (!banners.length) { - return null; - } + return { + default: () => ( +
+ {active.map(banner => ( + + {banner.link ? ( + + {banner.text} + + ) : ( + banner.text + )} + {banner.link && } + + ))} +
+ ), + }; + }), + [] + ); return ( -
- {banners.map(banner => ( - - {banner.link ? ( - - {banner.text} - - ) : ( - banner.text - )} - {banner.link && } - - ))} -
+ + + ); }; diff --git a/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs new file mode 100644 index 00000000..234c0adb --- /dev/null +++ b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs @@ -0,0 +1,47 @@ +import { isBannerActive } from '../../utils/banner.mjs'; + +/** @import { BannerEntry, RemoteConfig } from './types.d.ts' */ + +/** + * Fetches and returns active banners for the given version. + * Returns an empty array when remoteConfig is absent, the response is not ok, + * or on any fetch/parse failure. + * + * @param {string | undefined} remoteConfig + * @param {number | null} versionMajor + * @returns {Promise} + */ +export const loadBanners = async (remoteConfig, versionMajor) => { + try { + if (!remoteConfig) { + return []; + } + + const res = await fetch(remoteConfig, { + signal: AbortSignal.timeout(2500), + }); + if (!res.ok) { + return []; + } + + /** @type {RemoteConfig} */ + const config = await res.json(); + const active = []; + + const globalBanner = config.websiteBanners?.index; + if (globalBanner && isBannerActive(globalBanner)) { + active.push(globalBanner); + } + + if (versionMajor != null) { + const versionBanner = config.websiteBanners?.[`v${versionMajor}`]; + if (versionBanner && isBannerActive(versionBanner)) { + active.push(versionBanner); + } + } + + return active; + } catch { + return []; + } +}; diff --git a/src/generators/web/ui/components/AnnouncementBanner/useBanners.mjs b/src/generators/web/ui/components/AnnouncementBanner/useBanners.mjs deleted file mode 100644 index f6c957c7..00000000 --- a/src/generators/web/ui/components/AnnouncementBanner/useBanners.mjs +++ /dev/null @@ -1,56 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { isBannerActive } from '../../utils/banner.mjs'; - -/** @import { BannerEntry, RemoteConfig } from './types.d.ts' */ - -/** - * Fetches and returns active banners for the given version. - * Returns an empty array until loaded or on any failure. - * - * @param {{ remoteConfig: string, versionMajor: number | null }} props - * @returns {{ banners: BannerEntry[] }} - */ -export const useBanners = ({ remoteConfig, versionMajor }) => { - const [banners, setBanners] = useState(/** @type {BannerEntry[]} */ ([])); - - useEffect(() => { - if (!remoteConfig) { - return; - } - - /** @returns {Promise} */ - const load = async () => { - const res = await fetch(remoteConfig, { - signal: AbortSignal.timeout(2500), - }); - - if (!res.ok) { - return; - } - - /** @type {RemoteConfig} */ - const config = await res.json(); - const active = []; - - const globalBanner = config.websiteBanners?.index; - if (globalBanner && isBannerActive(globalBanner)) { - active.push(globalBanner); - } - - // no version info available, skip version-specific banner - if (versionMajor != null) { - const versionBanner = config.websiteBanners?.[`v${versionMajor}`]; - if (versionBanner && isBannerActive(versionBanner)) { - active.push(versionBanner); - } - } - - setBanners(active); - }; - - load().catch(console.error); - }, []); - - return { banners }; -}; diff --git a/src/generators/web/utils/generate.mjs b/src/generators/web/utils/generate.mjs index ee10ec3d..3c4069f6 100644 --- a/src/generators/web/utils/generate.mjs +++ b/src/generators/web/utils/generate.mjs @@ -80,11 +80,15 @@ export default () => { // Import all JSX components ...baseImports, - // Import Preact's SSR render function (named import) - createImportDeclaration('render', 'preact-render-to-string', false), + // Import Preact's async SSR render function (named import) + createImportDeclaration( + 'renderToStringAsync', + 'preact-render-to-string', + false + ), // Render component to HTML string and return it - `return render(${componentCode});`, + `return renderToStringAsync(${componentCode});`, ].join('\n'); }; From 28f14fcedcad8f16c995cd7ba9c6f94cddfe9a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 25 Mar 2026 19:54:11 -0300 Subject: [PATCH 11/25] refactor: use layout component --- src/generators/web/constants.mjs | 24 ------------------- .../web/ui/components/Layout/index.jsx | 19 +++++++++++++-- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/generators/web/constants.mjs b/src/generators/web/constants.mjs index 3171bd57..5cbfe760 100644 --- a/src/generators/web/constants.mjs +++ b/src/generators/web/constants.mjs @@ -14,34 +14,10 @@ export const ROOT = dirname(fileURLToPath(import.meta.url)); * An object containing mappings for various JSX components to their import paths. */ export const JSX_IMPORTS = { - AnnouncementBanner: { - name: 'AnnouncementBanner', - source: resolve(ROOT, './ui/components/AnnouncementBanner'), - }, Layout: { name: 'Layout', source: '#theme/Layout', }, - NavBar: { - name: 'NavBar', - source: resolve(ROOT, './ui/components/NavBar'), - }, - Article: { - name: 'Article', - source: '@node-core/ui-components/Containers/Article', - }, - SideBar: { - name: 'SideBar', - source: resolve(ROOT, './ui/components/SideBar'), - }, - TableOfContents: { - name: 'TableOfContents', - source: '@node-core/ui-components/Common/TableOfContents', - }, - MetaBar: { - name: 'MetaBar', - source: resolve(ROOT, './ui/components/MetaBar'), - }, CodeBox: { name: 'CodeBox', source: resolve(ROOT, './ui/components/CodeBox'), diff --git a/src/generators/web/ui/components/Layout/index.jsx b/src/generators/web/ui/components/Layout/index.jsx index 9978fc0c..87973dfc 100644 --- a/src/generators/web/ui/components/Layout/index.jsx +++ b/src/generators/web/ui/components/Layout/index.jsx @@ -1,6 +1,8 @@ import TableOfContents from '@node-core/ui-components/Common/TableOfContents'; import Article from '@node-core/ui-components/Containers/Article'; +import AnnouncementBanner from '../AnnouncementBanner'; + import Footer from '#theme/Footer'; import MetaBar from '#theme/Metabar'; import NavBar from '#theme/Navigation'; @@ -13,10 +15,23 @@ import SideBar from '#theme/Sidebar'; * main content, meta bar, and footer. Override via `#theme/Layout` in your * configuration's `imports` to customize the entire page structure. * - * @param {{ metadata: import('../../types').SerializedMetadata, headings: Array, readingTime: string, children: import('preact').ComponentChildren }} props + * @param {{ + * metadata: import('../../types').SerializedMetadata, + * headings: Array, + * readingTime: string, + * children: import('preact').ComponentChildren, + * announcementBannerProps: object + * }} props */ -export default ({ metadata, headings, readingTime, children }) => ( +export default ({ + metadata, + headings, + readingTime, + announcementBannerProps, + children, +}) => ( <> +
From 40a8a2c832ecbc038c155ad43ff8e2b99ac631e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 25 Mar 2026 19:59:02 -0300 Subject: [PATCH 12/25] feat: separate components --- .../components/AnnouncementBanner/index.jsx | 54 +++++++++---------- .../components/AnnouncementBanner/types.d.ts | 9 ++++ .../web/ui/components/Layout/index.jsx | 4 +- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index 83eca72f..51d89729 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -8,7 +8,32 @@ import { loadBanners } from './loadBanners.mjs'; /** @import { BannerEntry } from './types.d.ts' */ -export default ({ remoteConfig, versionMajor }) => { +/** + * @param {{ banners: BannerEntry[] }} props + */ +const AnnouncementBanner = ({ banners }) => ( +
+ {banners.map(banner => ( + + {banner.link ? ( + + {banner.text} + + ) : ( + banner.text + )} + {banner.link && } + + ))} +
+); + +export default AnnouncementBanner; + +/** + * @param {{ remoteConfig: string, versionMajor: number | null }} props + */ +export const RemoteLoadableBanner = ({ remoteConfig, versionMajor }) => { const LazyBanners = useMemo( () => lazy(async () => { @@ -18,32 +43,7 @@ export default ({ remoteConfig, versionMajor }) => { return { default: () => null }; } - return { - default: () => ( -
- {active.map(banner => ( - - {banner.link ? ( - - {banner.text} - - ) : ( - banner.text - )} - {banner.link && } - - ))} -
- ), - }; + return { default: () => }; }), [] ); diff --git a/src/generators/web/ui/components/AnnouncementBanner/types.d.ts b/src/generators/web/ui/components/AnnouncementBanner/types.d.ts index bdd3b605..5423b6df 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/types.d.ts +++ b/src/generators/web/ui/components/AnnouncementBanner/types.d.ts @@ -11,3 +11,12 @@ export type BannerEntry = { export type RemoteConfig = { websiteBanners?: Record; }; + +export type AnnouncementBannerProps = { + banners: BannerEntry[]; +}; + +export type RemoteLoadableBannerProps = { + remoteConfig: string; + versionMajor: number | null; +}; diff --git a/src/generators/web/ui/components/Layout/index.jsx b/src/generators/web/ui/components/Layout/index.jsx index 87973dfc..b9e14613 100644 --- a/src/generators/web/ui/components/Layout/index.jsx +++ b/src/generators/web/ui/components/Layout/index.jsx @@ -1,7 +1,7 @@ import TableOfContents from '@node-core/ui-components/Common/TableOfContents'; import Article from '@node-core/ui-components/Containers/Article'; -import AnnouncementBanner from '../AnnouncementBanner'; +import { RemoteLoadableBanner } from '../AnnouncementBanner'; import Footer from '#theme/Footer'; import MetaBar from '#theme/Metabar'; @@ -31,7 +31,7 @@ export default ({ children, }) => ( <> - +
From 06305e7c276ece17b010beed8ee1badc80d7bd23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 25 Mar 2026 20:02:52 -0300 Subject: [PATCH 13/25] refactor: clean loadBanners --- .../AnnouncementBanner/loadBanners.mjs | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs index 234c0adb..567537bb 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs +++ b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs @@ -12,11 +12,11 @@ import { isBannerActive } from '../../utils/banner.mjs'; * @returns {Promise} */ export const loadBanners = async (remoteConfig, versionMajor) => { - try { - if (!remoteConfig) { - return []; - } + if (!remoteConfig) { + return []; + } + try { const res = await fetch(remoteConfig, { signal: AbortSignal.timeout(2500), }); @@ -25,22 +25,15 @@ export const loadBanners = async (remoteConfig, versionMajor) => { } /** @type {RemoteConfig} */ - const config = await res.json(); - const active = []; - - const globalBanner = config.websiteBanners?.index; - if (globalBanner && isBannerActive(globalBanner)) { - active.push(globalBanner); - } + const { websiteBanners = {} } = await res.json(); - if (versionMajor != null) { - const versionBanner = config.websiteBanners?.[`v${versionMajor}`]; - if (versionBanner && isBannerActive(versionBanner)) { - active.push(versionBanner); - } - } + const keys = ['index', versionMajor != null && `v${versionMajor}`].filter( + Boolean + ); - return active; + return keys + .map(key => websiteBanners[key]) + .filter(banner => banner && isBannerActive(banner)); } catch { return []; } From 84fbe495edd5bce80a414d9309323076aa7f5e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 30 Mar 2026 16:32:37 -0300 Subject: [PATCH 14/25] refactor: split component files --- .../AnnouncementBanner/AnnouncementBanner.jsx | 28 ++++++++++++++++ .../components/AnnouncementBanner/index.jsx | 32 +++---------------- .../web/ui/components/Layout/index.jsx | 2 +- 3 files changed, 33 insertions(+), 29 deletions(-) create mode 100644 src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx diff --git a/src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx b/src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx new file mode 100644 index 00000000..645e9ea1 --- /dev/null +++ b/src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx @@ -0,0 +1,28 @@ +import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; +import Banner from '@node-core/ui-components/Common/Banner'; + +import styles from './index.module.css'; + +/** @import { BannerEntry } from './types.d.ts' */ + +/** + * @param {{ banners: BannerEntry[] }} props + */ +const AnnouncementBanner = ({ banners }) => ( +
+ {banners.map(banner => ( + + {banner.link ? ( + + {banner.text} + + ) : ( + banner.text + )} + {banner.link && } + + ))} +
+); + +export default AnnouncementBanner; diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index 51d89729..d9d34338 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -1,39 +1,13 @@ -import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; -import Banner from '@node-core/ui-components/Common/Banner'; import { lazy, Suspense } from 'preact/compat'; import { useMemo } from 'preact/hooks'; -import styles from './index.module.css'; +import AnnouncementBanner from './AnnouncementBanner.jsx'; import { loadBanners } from './loadBanners.mjs'; -/** @import { BannerEntry } from './types.d.ts' */ - -/** - * @param {{ banners: BannerEntry[] }} props - */ -const AnnouncementBanner = ({ banners }) => ( -
- {banners.map(banner => ( - - {banner.link ? ( - - {banner.text} - - ) : ( - banner.text - )} - {banner.link && } - - ))} -
-); - -export default AnnouncementBanner; - /** * @param {{ remoteConfig: string, versionMajor: number | null }} props */ -export const RemoteLoadableBanner = ({ remoteConfig, versionMajor }) => { +const RemoteLoadableBanner = ({ remoteConfig, versionMajor }) => { const LazyBanners = useMemo( () => lazy(async () => { @@ -54,3 +28,5 @@ export const RemoteLoadableBanner = ({ remoteConfig, versionMajor }) => { ); }; + +export default RemoteLoadableBanner; diff --git a/src/generators/web/ui/components/Layout/index.jsx b/src/generators/web/ui/components/Layout/index.jsx index b9e14613..83904378 100644 --- a/src/generators/web/ui/components/Layout/index.jsx +++ b/src/generators/web/ui/components/Layout/index.jsx @@ -1,7 +1,7 @@ import TableOfContents from '@node-core/ui-components/Common/TableOfContents'; import Article from '@node-core/ui-components/Containers/Article'; -import { RemoteLoadableBanner } from '../AnnouncementBanner'; +import RemoteLoadableBanner from '../AnnouncementBanner'; import Footer from '#theme/Footer'; import MetaBar from '#theme/Metabar'; From 05e62968600370ad15a8a2b1335c99965b9d7a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 30 Mar 2026 16:36:16 -0300 Subject: [PATCH 15/25] test: fix test name --- .../__tests__/{useBanners.test.mjs => loadBanners.test.mjs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/generators/web/ui/components/AnnouncementBanner/__tests__/{useBanners.test.mjs => loadBanners.test.mjs} (100%) diff --git a/src/generators/web/ui/components/AnnouncementBanner/__tests__/useBanners.test.mjs b/src/generators/web/ui/components/AnnouncementBanner/__tests__/loadBanners.test.mjs similarity index 100% rename from src/generators/web/ui/components/AnnouncementBanner/__tests__/useBanners.test.mjs rename to src/generators/web/ui/components/AnnouncementBanner/__tests__/loadBanners.test.mjs From 6d139f2f7c9dbb16d940df72f64a4f6f3a293a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 30 Mar 2026 18:10:56 -0300 Subject: [PATCH 16/25] fix: review --- .../web/ui/components/AnnouncementBanner/loadBanners.mjs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs index 567537bb..be868024 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs +++ b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs @@ -17,12 +17,7 @@ export const loadBanners = async (remoteConfig, versionMajor) => { } try { - const res = await fetch(remoteConfig, { - signal: AbortSignal.timeout(2500), - }); - if (!res.ok) { - return []; - } + const res = await fetch(remoteConfig); /** @type {RemoteConfig} */ const { websiteBanners = {} } = await res.json(); From bb0ac9848fe8498319e48abb0220ce7fe036be90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 30 Mar 2026 18:11:56 -0300 Subject: [PATCH 17/25] chore: remove testing library --- package-lock.json | 189 +++++----------------------------------------- package.json | 4 +- 2 files changed, 20 insertions(+), 173 deletions(-) diff --git a/package-lock.json b/package-lock.json index acca745a..ecb5b74b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,8 +59,6 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@reporters/github": "^1.12.0", - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.2", "@types/mdast": "^4.0.4", "@types/node": "^24.10.1", "@types/semver": "^7.7.1", @@ -123,41 +121,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -2794,54 +2757,6 @@ "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", "license": "MIT" }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2852,13 +2767,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -3621,16 +3529,6 @@ "node": ">=10" } }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", @@ -4173,13 +4071,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -5376,13 +5267,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/jsdoc-type-pratt-parser": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.1.1.tgz", @@ -5958,16 +5842,6 @@ "node": "20 || >=22" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -7386,44 +7260,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", @@ -7471,12 +7307,18 @@ "node": ">=0.10.0" } }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } }, "node_modules/react-markdown": { "version": "10.1.0", @@ -7896,6 +7738,13 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", diff --git a/package.json b/package.json index 00b61e0f..885c7512 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "format": "prettier .", "format:write": "prettier --write .", "format:check": "prettier --check .", - "test": "node --test --experimental-test-module-mocks --import=global-jsdom/register \"src/**/*.test.mjs\"", + "test": "node --test --experimental-test-module-mocks \"src/**/*.test.mjs\"", "test:coverage": "c8 node --test --experimental-test-module-mocks \"src/**/*.test.mjs\"", "test:ci": "c8 --reporter=lcov node --test --experimental-test-module-mocks \"src/**/*.test.mjs\" --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=junit.xml --test-reporter=spec --test-reporter-destination=stdout", "test:update-snapshots": "node --test --experimental-test-module-mocks --test-update-snapshots \"src/**/*.test.mjs\"", @@ -28,8 +28,6 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@reporters/github": "^1.12.0", - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.2", "@types/mdast": "^4.0.4", "@types/node": "^24.10.1", "@types/semver": "^7.7.1", From 61037f40aa25af4420399d575028e33427eaed50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 31 Mar 2026 20:23:10 -0300 Subject: [PATCH 18/25] refactor: remove type import --- .../ui/components/AnnouncementBanner/AnnouncementBanner.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx b/src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx index 645e9ea1..e6be507a 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx @@ -3,10 +3,8 @@ import Banner from '@node-core/ui-components/Common/Banner'; import styles from './index.module.css'; -/** @import { BannerEntry } from './types.d.ts' */ - /** - * @param {{ banners: BannerEntry[] }} props + * @param {import('./types.d.ts').AnnouncementBannerProps} props */ const AnnouncementBanner = ({ banners }) => (
From 2974759bf51586d5663b609f4c933bd695daa0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 1 Apr 2026 19:01:17 -0300 Subject: [PATCH 19/25] refactor: inline type import --- .../web/ui/components/AnnouncementBanner/loadBanners.mjs | 6 ++---- src/generators/web/ui/utils/banner.mjs | 4 +--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs index be868024..13043c78 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs +++ b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs @@ -1,7 +1,5 @@ import { isBannerActive } from '../../utils/banner.mjs'; -/** @import { BannerEntry, RemoteConfig } from './types.d.ts' */ - /** * Fetches and returns active banners for the given version. * Returns an empty array when remoteConfig is absent, the response is not ok, @@ -9,7 +7,7 @@ import { isBannerActive } from '../../utils/banner.mjs'; * * @param {string | undefined} remoteConfig * @param {number | null} versionMajor - * @returns {Promise} + * @returns {Promise} */ export const loadBanners = async (remoteConfig, versionMajor) => { if (!remoteConfig) { @@ -19,7 +17,7 @@ export const loadBanners = async (remoteConfig, versionMajor) => { try { const res = await fetch(remoteConfig); - /** @type {RemoteConfig} */ + /** @type {import('./types.d.ts').RemoteConfig} */ const { websiteBanners = {} } = await res.json(); const keys = ['index', versionMajor != null && `v${versionMajor}`].filter( diff --git a/src/generators/web/ui/utils/banner.mjs b/src/generators/web/ui/utils/banner.mjs index b8721152..4afa820a 100644 --- a/src/generators/web/ui/utils/banner.mjs +++ b/src/generators/web/ui/utils/banner.mjs @@ -1,11 +1,9 @@ -/** @import { BannerEntry } from '../components/AnnouncementBanner/types' */ - /** * Checks whether a banner should be displayed based on its date range. * Both `startDate` and `endDate` are optional; if omitted the banner is * considered open-ended in that direction. * - * @param {BannerEntry} banner + * @param {import('../components/AnnouncementBanner/types').BannerEntry} banner * @returns {boolean} */ export const isBannerActive = ({ startDate, endDate }) => { From 4974a457823fac7f9c681ec12767e36a705e9452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 2 Apr 2026 12:13:31 -0300 Subject: [PATCH 20/25] fix: remove duplicated banner --- src/generators/jsx-ast/utils/buildContent.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/generators/jsx-ast/utils/buildContent.mjs b/src/generators/jsx-ast/utils/buildContent.mjs index 4650061c..12f54825 100644 --- a/src/generators/jsx-ast/utils/buildContent.mjs +++ b/src/generators/jsx-ast/utils/buildContent.mjs @@ -279,7 +279,6 @@ export const processEntry = entry => { */ export const createDocumentLayout = (entries, metadata) => createTree('root', [ - createJSXElement(JSX_IMPORTS.AnnouncementBanner.name), createJSXElement(JSX_IMPORTS.Layout.name, { metadata, headings: extractHeadings(entries), From 621ccae13475210c653819c489a2226b871de2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 2 Apr 2026 14:41:20 -0300 Subject: [PATCH 21/25] fix: web config --- src/generators/jsx-ast/index.mjs | 1 - src/generators/web/index.mjs | 1 + .../components/AnnouncementBanner/index.jsx | 1 + .../web/ui/components/Layout/index.jsx | 22 ++++++------------- src/generators/web/ui/types.d.ts | 2 ++ src/generators/web/utils/processing.mjs | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/generators/jsx-ast/index.mjs b/src/generators/jsx-ast/index.mjs index b90c3f06..da61cb56 100644 --- a/src/generators/jsx-ast/index.mjs +++ b/src/generators/jsx-ast/index.mjs @@ -18,7 +18,6 @@ export default createLazyGenerator({ defaultConfiguration: { ref: 'main', - remoteConfig: 'https://nodejs.org/site.json', }, hasParallelProcessor: true, diff --git a/src/generators/web/index.mjs b/src/generators/web/index.mjs index 456cb9db..9bd7225f 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -33,6 +33,7 @@ export default createLazyGenerator({ title: '{project} v{version} Documentation', editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`, pageURL: '{baseURL}/latest-{version}/api{path}.html', + remoteConfig: 'https://nodejs.org/site.json', imports: { '#theme/Logo': '@node-core/ui-components/Common/NodejsLogo', '#theme/Navigation': join(import.meta.dirname, './ui/components/NavBar'), diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index d9d34338..93c6f6a4 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -19,6 +19,7 @@ const RemoteLoadableBanner = ({ remoteConfig, versionMajor }) => { return { default: () => }; }), + // eslint-disable-next-line react-x/exhaustive-deps [] ); diff --git a/src/generators/web/ui/components/Layout/index.jsx b/src/generators/web/ui/components/Layout/index.jsx index 83904378..5444aefa 100644 --- a/src/generators/web/ui/components/Layout/index.jsx +++ b/src/generators/web/ui/components/Layout/index.jsx @@ -3,6 +3,7 @@ import Article from '@node-core/ui-components/Containers/Article'; import RemoteLoadableBanner from '../AnnouncementBanner'; +import { remoteConfig, versionMajor } from '#theme/config'; import Footer from '#theme/Footer'; import MetaBar from '#theme/Metabar'; import NavBar from '#theme/Navigation'; @@ -15,23 +16,14 @@ import SideBar from '#theme/Sidebar'; * main content, meta bar, and footer. Override via `#theme/Layout` in your * configuration's `imports` to customize the entire page structure. * - * @param {{ - * metadata: import('../../types').SerializedMetadata, - * headings: Array, - * readingTime: string, - * children: import('preact').ComponentChildren, - * announcementBannerProps: object - * }} props + * @param {{ metadata: import('../../types').SerializedMetadata, headings: Array, readingTime: string, children: import('preact').ComponentChildren }} props */ -export default ({ - metadata, - headings, - readingTime, - announcementBannerProps, - children, -}) => ( +export default ({ metadata, headings, readingTime, children }) => ( <> - +
diff --git a/src/generators/web/ui/types.d.ts b/src/generators/web/ui/types.d.ts index 8ecd5574..4b3af4a9 100644 --- a/src/generators/web/ui/types.d.ts +++ b/src/generators/web/ui/types.d.ts @@ -23,6 +23,7 @@ declare module '#theme/config' { // From config generation export const version: string; + export const versionMajor: number; export const versions: Array<{ url: string; label: string; @@ -31,6 +32,7 @@ declare module '#theme/config' { export const editURL: string; export const pages: Array<[string, string]>; export const languageDisplayNameMap: Map; + export const remoteConfig: string; } // Omit Primitives from Metadata diff --git a/src/generators/web/utils/processing.mjs b/src/generators/web/utils/processing.mjs index 4121ae15..3c5f8855 100644 --- a/src/generators/web/utils/processing.mjs +++ b/src/generators/web/utils/processing.mjs @@ -67,7 +67,7 @@ async function executeServerCode(serverCodeMap, requireFn, virtualImports) { // Execute each bundled entry and collect dehydrated HTML results for (const chunk of entryChunks) { const executedFunction = new Function('require', chunk.code); - pages.set(chunk.fileName, executedFunction(enhancedRequire)); + pages.set(chunk.fileName, await executedFunction(enhancedRequire)); } return { pages, css }; From 11418dc97a298c43f2fa6ca6fc1ca8b1548d2b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 2 Apr 2026 15:34:26 -0300 Subject: [PATCH 22/25] fix: simplify lazy --- .../components/AnnouncementBanner/index.jsx | 35 +++++++------------ .../web/ui/components/Layout/index.jsx | 6 +--- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index 93c6f6a4..5a8d9ed7 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -1,33 +1,22 @@ -import { lazy, Suspense } from 'preact/compat'; -import { useMemo } from 'preact/hooks'; +import { useState, useEffect } from 'preact/hooks'; import AnnouncementBanner from './AnnouncementBanner.jsx'; import { loadBanners } from './loadBanners.mjs'; -/** - * @param {{ remoteConfig: string, versionMajor: number | null }} props - */ -const RemoteLoadableBanner = ({ remoteConfig, versionMajor }) => { - const LazyBanners = useMemo( - () => - lazy(async () => { - const active = await loadBanners(remoteConfig, versionMajor); +import { remoteConfig, versionMajor } from '#theme/config'; - if (!active.length) { - return { default: () => null }; - } +const RemoteLoadableBanner = () => { + const [banners, setBanners] = useState([]); - return { default: () => }; - }), - // eslint-disable-next-line react-x/exhaustive-deps - [] - ); + useEffect(() => { + loadBanners(remoteConfig, versionMajor).then(active => { + if (active.length) { + setBanners(active); + } + }); + }, []); - return ( - - - - ); + return banners.length ? : null; }; export default RemoteLoadableBanner; diff --git a/src/generators/web/ui/components/Layout/index.jsx b/src/generators/web/ui/components/Layout/index.jsx index 5444aefa..b4416363 100644 --- a/src/generators/web/ui/components/Layout/index.jsx +++ b/src/generators/web/ui/components/Layout/index.jsx @@ -3,7 +3,6 @@ import Article from '@node-core/ui-components/Containers/Article'; import RemoteLoadableBanner from '../AnnouncementBanner'; -import { remoteConfig, versionMajor } from '#theme/config'; import Footer from '#theme/Footer'; import MetaBar from '#theme/Metabar'; import NavBar from '#theme/Navigation'; @@ -20,10 +19,7 @@ import SideBar from '#theme/Sidebar'; */ export default ({ metadata, headings, readingTime, children }) => ( <> - +
From 0bcc226dbcc99eb03e19c16f393aed81fd6c799f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 2 Apr 2026 15:42:47 -0300 Subject: [PATCH 23/25] fix: lazy loading --- .../components/AnnouncementBanner/index.jsx | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index 5a8d9ed7..eaf82d55 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -1,22 +1,30 @@ -import { useState, useEffect } from 'preact/hooks'; +import { lazy, Suspense } from 'preact/compat'; import AnnouncementBanner from './AnnouncementBanner.jsx'; import { loadBanners } from './loadBanners.mjs'; import { remoteConfig, versionMajor } from '#theme/config'; -const RemoteLoadableBanner = () => { - const [banners, setBanners] = useState([]); +const LazyBanners = SERVER + ? null + : lazy(async () => { + const active = await loadBanners(remoteConfig, versionMajor); - useEffect(() => { - loadBanners(remoteConfig, versionMajor).then(active => { - if (active.length) { - setBanners(active); + if (!active.length) { + return { default: () => null }; } + + return { default: () => }; }); - }, []); - return banners.length ? : null; -}; +const RemoteLoadableBanner = SERVER + ? () =>
+ : () => ( +
+ + + +
+ ); export default RemoteLoadableBanner; From f97c27d95bbd2f4917949cd23671f9445deaa549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Sun, 5 Apr 2026 23:19:24 -0300 Subject: [PATCH 24/25] fix: add major version config --- src/generators/web/index.mjs | 2 -- src/generators/web/ui/components/AnnouncementBanner/index.jsx | 1 + src/generators/web/utils/config.mjs | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/generators/web/index.mjs b/src/generators/web/index.mjs index 9bd7225f..ab981b91 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -43,7 +43,5 @@ export default createLazyGenerator({ '#theme/Layout': join(import.meta.dirname, './ui/components/Layout'), }, virtualImports: {}, - remoteConfig: - 'https://raw.githubusercontent.com/nodejs/nodejs.org/main/apps/site/site.json', }, }); diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index eaf82d55..932037e2 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -5,6 +5,7 @@ import { loadBanners } from './loadBanners.mjs'; import { remoteConfig, versionMajor } from '#theme/config'; +// TODO: Revisit SERVER global usage. const LazyBanners = SERVER ? null : lazy(async () => { diff --git a/src/generators/web/utils/config.mjs b/src/generators/web/utils/config.mjs index 00d7ae15..414e6f8f 100644 --- a/src/generators/web/utils/config.mjs +++ b/src/generators/web/utils/config.mjs @@ -86,6 +86,7 @@ export default function createConfigSource(input) { ['changelog', 'index', 'imports', 'virtualImports'] ), version, + versionMajor: configVersion.major ?? null, versions: buildVersionEntries(config.changelog, pageURL), editURL, pages: buildPageList(input), From 7e2aeca4cd15e5d45e246b4aeb50c22bc49493c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Sun, 5 Apr 2026 23:33:06 -0300 Subject: [PATCH 25/25] refactor: nit --- .../web/ui/components/AnnouncementBanner/loadBanners.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs index 13043c78..818cab8f 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs +++ b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs @@ -7,7 +7,7 @@ import { isBannerActive } from '../../utils/banner.mjs'; * * @param {string | undefined} remoteConfig * @param {number | null} versionMajor - * @returns {Promise} + * @returns {Promise>} */ export const loadBanners = async (remoteConfig, versionMajor) => { if (!remoteConfig) {