From 3553c3401fefb22ea48116e5a364f3bc54a55dd4 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/39] 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 2bb92841..303c1a7d 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -43,5 +43,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 67880a8be6e6aecda5f3d1ca38132ebf2d6837f2 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/39] 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 303c1a7d..e28e281c 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -44,6 +44,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 5ef68858695352d8472d52276567cd5e7fe5c43f 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/39] 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 eb9a0654b49322a987a0d4874cffe0fd8c5c7d7f 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/39] 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 dc8e10d5c19d9a61edde796c7c316df96ae59d8c 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/39] 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 76c561294894fe729e5a9a1e1b9e4aa856a47ae0 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/39] 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 9f3fd300ba8b77e7105577bc4ba505ea0a914333 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/39] 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 a01e3d21f5917908ea7cc7afa96d90f0a72e0eb5 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/39] 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 49b5e2525be90a0a5043cefef865347f77a1113e 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/39] refactor: transform into hook --- package-lock.json | 443 ++++++++++++------ 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, 458 insertions(+), 231 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 fffa097b..be4da23f 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": { @@ -2719,64 +2761,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", @@ -2828,6 +2812,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", @@ -2838,6 +2870,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", @@ -2939,14 +2978,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": { @@ -2957,18 +2996,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" @@ -2979,9 +3018,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": { @@ -2992,21 +3031,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" @@ -3017,13 +3056,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": { @@ -3035,21 +3074,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" @@ -3059,20 +3098,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" @@ -3083,17 +3122,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": { @@ -3130,6 +3169,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -3143,6 +3183,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -3169,6 +3210,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3182,6 +3224,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -3195,6 +3238,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3208,6 +3252,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3221,6 +3266,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3234,6 +3280,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3247,6 +3294,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3260,6 +3308,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3273,6 +3322,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3286,6 +3336,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3299,6 +3350,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3312,6 +3364,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3325,6 +3378,7 @@ "wasm32" ], "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" @@ -3333,6 +3387,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", @@ -3341,6 +3408,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3354,6 +3422,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3367,6 +3436,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3569,6 +3639,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", @@ -3617,9 +3697,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": { @@ -4111,6 +4191,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", @@ -4648,9 +4735,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" }, @@ -5307,6 +5394,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", @@ -5873,15 +5967,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", @@ -6781,13 +6885,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" @@ -7067,9 +7171,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" @@ -7300,6 +7404,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", @@ -7339,27 +7481,34 @@ } }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", "engines": { "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==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", "peer": true, "dependencies": { - "scheduler": "^0.26.0" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19.2.5" } }, + "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", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", @@ -7779,9 +7928,9 @@ } }, "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "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 }, @@ -8213,9 +8362,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" @@ -8274,9 +8423,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": { @@ -8353,9 +8502,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 5d26f94c..02e5583b 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 e3c3aac0ca1cc1e7ab5c6baf4ba4ee8b1e43972f 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/39] 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 f8fd4941a6372d499333e8b83e47c5e0ff3de680 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/39] 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 f5e2a5424d32cee865ddd70cd508eb037f655c55 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/39] 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 03aa57ed68587c6abfbe426ee3c28fb17e7b4462 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/39] 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 ebc5431f8e47226fabc687e32b5dcfb8133863dc 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/39] 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 3a578c025771ab224002921c166e52108574f345 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/39] 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 85360ab806eb72f6c693383a24467bca92e3e0d2 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/39] 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 1c024c109c867bbb70addc02b6a01013ea369081 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/39] chore: remove testing library --- package-lock.json | 195 +--------------------------------------------- package.json | 4 +- 2 files changed, 4 insertions(+), 195 deletions(-) diff --git a/package-lock.json b/package-lock.json index be4da23f..8e265cf8 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", @@ -918,9 +881,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", "license": "MIT", "optional": true, "dependencies": { @@ -2312,24 +2275,6 @@ "node": ">=14.0.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "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": { - "@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/@rolldown/binding-win32-arm64-msvc": { "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", @@ -2812,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", @@ -2870,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", @@ -3639,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", @@ -4191,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", @@ -5394,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", @@ -5976,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", @@ -7404,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", @@ -7502,13 +7320,6 @@ "react": "^19.2.5" } }, - "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", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", diff --git a/package.json b/package.json index 02e5583b..5d26f94c 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 3a35079ad8c7ca95f1a820254cb1cf55ce2e9655 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/39] 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 a4fe2e15b7bf6b718df5fa3c703a6bed8188e389 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/39] 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 3315e5a42881dbdf93a656f888d3ea1dc4a64b32 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/39] 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 fd945cc1b4621dcba448e44aa340ef3f6c4d1424 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/39] 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 e28e281c..348f92db 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -34,6 +34,7 @@ export default createLazyGenerator({ useAbsoluteURLs: false, 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 081061db..e200135a 100644 --- a/src/generators/web/ui/types.d.ts +++ b/src/generators/web/ui/types.d.ts @@ -24,6 +24,7 @@ declare module '#theme/config' { // From config generation export const version: string; + export const versionMajor: number; export const versions: Array<{ url: string; label: string; @@ -32,6 +33,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 e4a66f23..184163ea 100644 --- a/src/generators/web/utils/processing.mjs +++ b/src/generators/web/utils/processing.mjs @@ -84,7 +84,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 753aac152088d7840928f5cc56992b15d3e7d1bc 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/39] 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 93eaac81970f53ced66a0f67fd5f3d1c1b2c9ca8 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/39] 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 8d80dc19304aa23d050fe30fdd9ff2c5b5c762ab 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/39] 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 348f92db..ca0bb6a9 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -44,7 +44,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 7a9165386bc067cabf672b2ca7e56896b576f14b 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/39] 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) { From b5d8230e633d62ddcfdd89295d739bec4c6a29c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 9 Apr 2026 19:55:07 -0300 Subject: [PATCH 26/39] refactor: remove unused type --- .../web/ui/components/AnnouncementBanner/types.d.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/types.d.ts b/src/generators/web/ui/components/AnnouncementBanner/types.d.ts index 5423b6df..8ea873de 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/types.d.ts +++ b/src/generators/web/ui/components/AnnouncementBanner/types.d.ts @@ -15,8 +15,3 @@ export type RemoteConfig = { export type AnnouncementBannerProps = { banners: BannerEntry[]; }; - -export type RemoteLoadableBannerProps = { - remoteConfig: string; - versionMajor: number | null; -}; From d523a9aadc63f22369debb5eed18be4d312a299e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 9 Apr 2026 21:30:53 -0300 Subject: [PATCH 27/39] refactor: assign promise --- src/generators/web/utils/processing.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/generators/web/utils/processing.mjs b/src/generators/web/utils/processing.mjs index 184163ea..7228ad6f 100644 --- a/src/generators/web/utils/processing.mjs +++ b/src/generators/web/utils/processing.mjs @@ -84,7 +84,8 @@ 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, await executedFunction(enhancedRequire)); + const dehydratedHtml = await executedFunction(enhancedRequire); + pages.set(chunk.fileName, dehydratedHtml); } return { pages, css }; From bf24092affb7d042d546a3eb68adac1b7e2935a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 05:53:49 -0300 Subject: [PATCH 28/39] chore: revert package-lock.json --- package-lock.json | 459 +++++++++++++++++++++------------------------- package.json | 2 +- 2 files changed, 212 insertions(+), 249 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e265cf8..8fcfc335 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@node-core/doc-kit", - "version": "1.3.4", + "version": "1.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@node-core/doc-kit", - "version": "1.3.4", + "version": "1.3.5", "dependencies": { "@actions/core": "^3.0.0", "@heroicons/react": "^2.2.0", @@ -132,20 +132,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", - "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "license": "MIT", "optional": true, "dependencies": { @@ -153,9 +153,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "license": "MIT", "optional": true, "dependencies": { @@ -163,17 +163,17 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.84.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.84.0.tgz", - "integrity": "sha512-0xew1CxOam0gV5OMjh2KjFQZsKL2bByX1+q4j3E73MpYIdyUxcZb/xQct9ccUb+ve5KGUYbCUxyPnYB7RbuP+w==", + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.86.0.tgz", + "integrity": "sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==", "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.8", - "@typescript-eslint/types": "^8.54.0", - "comment-parser": "1.4.5", + "@typescript-eslint/types": "^8.58.0", + "comment-parser": "1.4.6", "esquery": "^1.7.0", - "jsdoc-type-pratt-parser": "~7.1.1" + "jsdoc-type-pratt-parser": "~7.2.0" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -316,13 +316,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.3", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" }, @@ -331,22 +331,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^1.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -378,9 +378,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -388,13 +388,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { @@ -881,21 +881,15 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", - "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", "license": "MIT", "optional": true, "dependencies": { - "@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" + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" } }, "node_modules/@noble/hashes": { @@ -1053,9 +1047,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -2068,9 +2062,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", "cpu": [ "arm64" ], @@ -2084,9 +2078,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", "cpu": [ "arm64" ], @@ -2100,9 +2094,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", "cpu": [ "x64" ], @@ -2116,9 +2110,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", "cpu": [ "x64" ], @@ -2132,9 +2126,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", "cpu": [ "arm" ], @@ -2148,9 +2142,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", "cpu": [ "arm64" ], @@ -2164,9 +2158,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", "cpu": [ "arm64" ], @@ -2180,9 +2174,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", "cpu": [ "ppc64" ], @@ -2196,9 +2190,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", "cpu": [ "s390x" ], @@ -2212,9 +2206,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", "cpu": [ "x64" ], @@ -2228,9 +2222,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", "cpu": [ "x64" ], @@ -2244,9 +2238,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", "cpu": [ "arm64" ], @@ -2260,25 +2254,45 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", "cpu": [ "wasm32" ], "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" }, "engines": { "node": ">=14.0.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@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/@rolldown/binding-win32-arm64-msvc": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", "cpu": [ "arm64" ], @@ -2292,9 +2306,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", "cpu": [ "x64" ], @@ -2308,9 +2322,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", "license": "MIT" }, "node_modules/@rollup/plugin-virtual": { @@ -2484,9 +2498,9 @@ } }, "node_modules/@swc/html-wasm": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/html-wasm/-/html-wasm-1.15.21.tgz", - "integrity": "sha512-2Wo2S5DbfAswldF/X5gSmqgTy4uMBEUix7zYcIFsXgZyVJ8PDPE19cgxSBkauJxxGXfvo5rVCcEjx0XrJHpDEA==", + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/html-wasm/-/html-wasm-1.15.24.tgz", + "integrity": "sha512-UB/qSPutPK24d3gyMkcRN4nFhOxOjZsKfQ2qT09GlRMOgO5piebUqLJpJVlieFORVb5hAkolKQvsFAVnOUzsAA==", "license": "Apache-2.0" }, "node_modules/@tailwindcss/node": { @@ -2868,14 +2882,14 @@ "license": "MIT" }, "node_modules/@typescript-eslint/project-service": { - "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==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", + "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.0", - "@typescript-eslint/types": "^8.58.0", + "@typescript-eslint/tsconfig-utils": "^8.58.2", + "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "engines": { @@ -2890,14 +2904,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "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==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", + "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0" + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2908,9 +2922,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "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==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", + "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", "dev": true, "license": "MIT", "engines": { @@ -2925,15 +2939,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "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==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", + "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0", - "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2950,9 +2964,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", - "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", + "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", "dev": true, "license": "MIT", "engines": { @@ -2964,16 +2978,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "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==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", + "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", "dev": true, "license": "MIT", "dependencies": { - "@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", + "@typescript-eslint/project-service": "8.58.2", + "@typescript-eslint/tsconfig-utils": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2992,16 +3006,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", - "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", + "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0" + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3016,13 +3030,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "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==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", + "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3059,7 +3073,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -3073,7 +3086,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -3100,7 +3112,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -3114,7 +3125,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -3128,7 +3138,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3142,7 +3151,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3156,7 +3164,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3170,7 +3177,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3184,7 +3190,6 @@ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3198,7 +3203,6 @@ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3212,7 +3216,6 @@ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3226,7 +3229,6 @@ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3240,7 +3242,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3254,7 +3255,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -3268,7 +3268,6 @@ "wasm32" ], "dev": true, - "license": "MIT", "optional": true, "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" @@ -3277,19 +3276,6 @@ "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", @@ -3298,7 +3284,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -3312,7 +3297,6 @@ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -3326,7 +3310,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -3903,9 +3886,9 @@ } }, "node_modules/comment-parser": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.5.tgz", - "integrity": "sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz", + "integrity": "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==", "dev": true, "license": "MIT", "engines": { @@ -4164,18 +4147,18 @@ } }, "node_modules/eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", - "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", + "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.3", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", + "@eslint/config-array": "^0.23.4", + "@eslint/config-helpers": "^0.5.4", + "@eslint/core": "^1.2.0", + "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4304,19 +4287,19 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "62.8.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.8.1.tgz", - "integrity": "sha512-e9358PdHgvcMF98foNd3L7hVCw70Lt+YcSL7JzlJebB8eT5oRJtW6bHMQKoAwJtw6q0q0w/fRIr2kwnHdFDI6A==", + "version": "62.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.9.0.tgz", + "integrity": "sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@es-joy/jsdoccomment": "~0.84.0", + "@es-joy/jsdoccomment": "~0.86.0", "@es-joy/resolve.exports": "1.2.0", "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.5", + "comment-parser": "1.4.6", "debug": "^4.4.3", "escape-string-regexp": "^4.0.0", - "espree": "^11.1.0", + "espree": "^11.2.0", "esquery": "^1.7.0", "html-entities": "^2.6.0", "object-deep-merge": "^2.0.0", @@ -5268,9 +5251,9 @@ } }, "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", - "integrity": "sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.2.0.tgz", + "integrity": "sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==", "dev": true, "license": "MIT", "engines": { @@ -5833,9 +5816,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -7060,9 +7043,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", "funding": [ { "type": "opencollective", @@ -7216,9 +7199,9 @@ "license": "MIT" }, "node_modules/preact": { - "version": "10.29.0", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz", - "integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==", + "version": "10.29.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", "license": "MIT", "funding": { "type": "opencollective", @@ -7299,27 +7282,14 @@ } }, "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/react-dom": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", - "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.5" - } - }, "node_modules/react-markdown": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", @@ -7706,13 +7676,13 @@ "license": "MIT" }, "node_modules/rolldown": { - "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==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" @@ -7721,29 +7691,22 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@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/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 + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + } }, "node_modules/semver": { "version": "7.7.4", @@ -8130,9 +8093,9 @@ } }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 5d26f94c..61bdca30 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@node-core/doc-kit", "type": "module", - "version": "1.3.4", + "version": "1.3.5", "repository": { "type": "git", "url": "git+https://github.com/nodejs/doc-kit.git" From bbf5c116567266f98981b50461520c393343c1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 05:57:55 -0300 Subject: [PATCH 29/39] chore: revert package-lock.json --- package-lock.json | 93 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8fcfc335..cc5670f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -881,15 +881,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==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", "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": { @@ -2271,24 +2277,6 @@ "node": ">=14.0.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", - "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@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/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.0-rc.15", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", @@ -3073,6 +3061,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -3086,6 +3075,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -3112,6 +3102,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3125,6 +3116,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -3138,6 +3130,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3151,6 +3144,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3164,6 +3158,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3177,6 +3172,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3190,6 +3186,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3203,6 +3200,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3216,6 +3214,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3229,6 +3228,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3242,6 +3242,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3255,6 +3256,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -3268,6 +3270,7 @@ "wasm32" ], "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" @@ -3276,6 +3279,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", @@ -3284,6 +3300,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3297,6 +3314,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3310,6 +3328,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -7282,14 +7301,27 @@ } }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, "node_modules/react-markdown": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", @@ -7708,6 +7740,13 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, + "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", From f799ce987bc79f52b7a9fe6ddbe07ea682d75e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 06:18:25 -0300 Subject: [PATCH 30/39] test: set up e2e test --- .gitignore | 4 ++++ package.json | 2 ++ playwright.config.js | 9 +++++++++ 3 files changed, 15 insertions(+) create mode 100644 playwright.config.js diff --git a/.gitignore b/.gitignore index c86ab85e..ac08a4ba 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ isolate-* # Node's Source Folder node + +# Playwright +playwright-report/ +test-results/ diff --git a/package.json b/package.json index 61bdca30..42d6e8b3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "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\"", "test:watch": "node --test --experimental-test-module-mocks --watch \"src/**/*.test.mjs\"", + "test:e2e": "playwright test", "prepare": "husky || exit 0", "run": "node bin/cli.mjs", "watch": "node --watch bin/cli.mjs" @@ -40,6 +41,7 @@ "globals": "~17.4.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", + "@playwright/test": "^1.59.1", "prettier": "3.8.1" }, "dependencies": { diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 00000000..453a541b --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,9 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + use: { + baseURL: 'http://localhost:3000', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], +}); From 0b68e7d9a82fa8423796f10280d4f471837c7e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 06:18:54 -0300 Subject: [PATCH 31/39] test: create banner test --- e2e/announcement-banner.spec.js | 66 +++++++++++++++++++++++++++++++++ package-lock.json | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 e2e/announcement-banner.spec.js diff --git a/e2e/announcement-banner.spec.js b/e2e/announcement-banner.spec.js new file mode 100644 index 00000000..2fecf4b3 --- /dev/null +++ b/e2e/announcement-banner.spec.js @@ -0,0 +1,66 @@ +import { expect, test } from '@playwright/test'; + +const REMOTE_CONFIG_URL = 'https://nodejs.org/site.json'; + +test.describe('Announcement Banner', () => { + test('renders a text-only banner', async ({ page }) => { + await page.route(REMOTE_CONFIG_URL, route => + route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + websiteBanners: { + index: { text: 'Important announcement for all users' }, + }, + }), + }) + ); + + await page.goto('/assert.html'); + + const banner = page.getByRole('region', { name: 'Announcements' }); + await expect(banner).toBeVisible(); + await expect(banner).toContainText('Important announcement for all users'); + }); + + test('renders a banner with a link', async ({ page }) => { + await page.route(REMOTE_CONFIG_URL, route => + route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + websiteBanners: { + index: { + text: 'Read the release notes', + link: 'https://nodejs.org/releases', + }, + }, + }), + }) + ); + + await page.goto('/assert.html'); + + const banner = page.getByRole('region', { name: 'Announcements' }); + await expect(banner).toBeVisible(); + + const link = banner.getByRole('link', { name: 'Read the release notes' }); + await expect(link).toBeVisible(); + await expect(link).toHaveAttribute('href', 'https://nodejs.org/releases'); + await expect(link).toHaveAttribute('target', '_blank'); + }); + + test('does not render when there are no active banners', async ({ page }) => { + await page.route(REMOTE_CONFIG_URL, route => + route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ websiteBanners: {} }), + }) + ); + + await page.goto('/assert.html'); + await page.waitForLoadState('networkidle'); + + await expect( + page.getByRole('region', { name: 'Announcements' }) + ).not.toBeAttached(); + }); +}); diff --git a/package-lock.json b/package-lock.json index cc5670f1..2dfa2b9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@playwright/test": "^1.59.1", "@reporters/github": "^1.12.0", "@types/mdast": "^4.0.4", "@types/node": "^24.10.1", @@ -1068,6 +1069,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -7061,6 +7078,53 @@ "@napi-rs/nice": "^1.0.4" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.9", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", From fa73da18f909574ed38105192615c92e9ce30f63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 06:36:34 -0300 Subject: [PATCH 32/39] ci: add e2e job --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2899571c..83ba4ba4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,3 +78,31 @@ jobs: with: files: ./junit.xml report_type: test_results + + e2e: + name: E2E Tests + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps + + - name: Run E2E tests + run: node --run test:e2e From 8bab1b03598005fa2ca6c3ef2aedfdfd0935d80e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 06:45:01 -0300 Subject: [PATCH 33/39] ci: build and serve before testing --- .github/workflows/ci.yml | 10 ++++++++++ eslint.config.mjs | 2 +- playwright.config.js | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83ba4ba4..33068fd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,16 @@ jobs: - name: Install dependencies run: npm ci + - name: Checkout Node.js source + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: nodejs/node + sparse-checkout: doc/api/assert.md + path: node + + - name: Build docs + run: npx doc-kit generate -t web -i "./node/doc/api/assert.md" -o out + - name: Install Playwright browsers run: npx playwright install --with-deps diff --git a/eslint.config.mjs b/eslint.config.mjs index 102c43f2..67be361c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,7 +13,7 @@ export default defineConfig([ ignores: ['out/', 'src/generators/api-links/__tests__/fixtures/'], }, { - files: ['**/*.{mjs,jsx}'], + files: ['**/*.{mjs,jsx,js}'], plugins: { jsdoc }, languageOptions: { ecmaVersion: 'latest', diff --git a/playwright.config.js b/playwright.config.js index 453a541b..ddffdc95 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -5,5 +5,10 @@ export default defineConfig({ use: { baseURL: 'http://localhost:3000', }, + webServer: { + command: 'npx serve out', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + }, projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], }); From c78cedbfc922a8b2068c18ab652e6c6db9f98f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 06:47:26 -0300 Subject: [PATCH 34/39] refactor: add missing curly braces --- src/generators/legacy-html/assets/api.js | 36 ++++++++++++++++++------ 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/generators/legacy-html/assets/api.js b/src/generators/legacy-html/assets/api.js index 0678c52f..939169ba 100644 --- a/src/generators/legacy-html/assets/api.js +++ b/src/generators/legacy-html/assets/api.js @@ -32,7 +32,9 @@ const setupTheme = () => { document.documentElement.classList.add('dark-mode'); } - if (!themeToggleButton) return; + if (!themeToggleButton) { + return; + } themeToggleButton.hidden = false; @@ -68,7 +70,9 @@ const setupTheme = () => { */ const setupPickers = () => { const pickers = document.querySelectorAll('.picker-header > a'); - if (!pickers.length) return; + if (!pickers.length) { + return; + } const closeAllPickers = () => { pickers.forEach(picker => { @@ -81,7 +85,9 @@ const setupPickers = () => { }; const handleEscKey = e => { - if (e.key === 'Escape') closeAllPickers(); + if (e.key === 'Escape') { + closeAllPickers(); + } }; pickers.forEach(picker => { @@ -90,7 +96,9 @@ const setupPickers = () => { picker.addEventListener('click', e => { e.preventDefault(); - if (picker.ariaExpanded === 'true') return; + if (picker.ariaExpanded === 'true') { + return; + } requestAnimationFrame(() => { picker.ariaExpanded = true; @@ -108,7 +116,9 @@ const setupPickers = () => { */ const setupStickyHeaders = () => { const header = document.querySelector('.header'); - if (!header) return; + if (!header) { + return; + } let ignoreNextIntersection = false; @@ -117,7 +127,9 @@ const setupStickyHeaders = () => { const currentPinned = header.classList.contains('is-pinned'); const shouldPin = entries[0].intersectionRatio < 1; - if (currentPinned === shouldPin) return; + if (currentPinned === shouldPin) { + return; + } if (ignoreNextIntersection) { ignoreNextIntersection = false; return; @@ -136,7 +148,9 @@ const setupStickyHeaders = () => { */ const setupAltDocsLink = () => { const linkWrapper = document.getElementById('alt-docs'); - if (!linkWrapper) return; + if (!linkWrapper) { + return; + } const updateHashes = () => { linkWrapper @@ -153,7 +167,9 @@ const setupAltDocsLink = () => { */ const setupFlavorToggles = () => { const toggles = document.querySelectorAll('.js-flavor-toggle'); - if (!toggles.length) return; + if (!toggles.length) { + return; + } const isCustomFlavorEnabled = localStorage.getItem('customFlavor') === 'true'; @@ -220,4 +236,6 @@ function setupSidebarScroll() { // Initialize either on DOMContentLoaded or immediately if already loaded document.addEventListener('DOMContentLoaded', initFeatures); -if (document.readyState !== 'loading') initFeatures(); +if (document.readyState !== 'loading') { + initFeatures(); +} From 153779b2f622d8dcf0839054f178b318ac525db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 08:20:17 -0300 Subject: [PATCH 35/39] refactor: pass whole version object --- .../web/ui/components/AnnouncementBanner/index.jsx | 4 ++-- src/generators/web/ui/components/SideBar/index.jsx | 2 +- src/generators/web/ui/types.d.ts | 4 ++-- src/generators/web/utils/config.mjs | 10 ++++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index 932037e2..f4a0dde2 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -3,13 +3,13 @@ import { lazy, Suspense } from 'preact/compat'; import AnnouncementBanner from './AnnouncementBanner.jsx'; import { loadBanners } from './loadBanners.mjs'; -import { remoteConfig, versionMajor } from '#theme/config'; +import { remoteConfig, version } from '#theme/config'; // TODO: Revisit SERVER global usage. const LazyBanners = SERVER ? null : lazy(async () => { - const active = await loadBanners(remoteConfig, versionMajor); + const active = await loadBanners(remoteConfig, version.major); if (!active.length) { return { default: () => null }; diff --git a/src/generators/web/ui/components/SideBar/index.jsx b/src/generators/web/ui/components/SideBar/index.jsx index 40a987b7..2df4e200 100644 --- a/src/generators/web/ui/components/SideBar/index.jsx +++ b/src/generators/web/ui/components/SideBar/index.jsx @@ -58,7 +58,7 @@ export default ({ metadata }) => { values={compatibleVersions} inline={true} className={styles.select} - placeholder={version} + placeholder={`v${version.version}`} onChange={redirect} />
diff --git a/src/generators/web/ui/types.d.ts b/src/generators/web/ui/types.d.ts index e200135a..b872b09b 100644 --- a/src/generators/web/ui/types.d.ts +++ b/src/generators/web/ui/types.d.ts @@ -1,6 +1,7 @@ import { GlobalConfiguration } from '../../../utils/configuration/types'; import { MetadataEntry } from '../../metadata/types'; import { Configuration } from '../types'; +import { SemVer } from 'semver'; declare global { const SERVER: boolean; @@ -23,8 +24,7 @@ declare module '#theme/config' { export const useAbsoluteURLs: Configuration['useAbsoluteURLs']; // From config generation - export const version: string; - export const versionMajor: number; + export const version: SemVer; export const versions: Array<{ url: string; label: string; diff --git a/src/generators/web/utils/config.mjs b/src/generators/web/utils/config.mjs index 414e6f8f..f58f303f 100644 --- a/src/generators/web/utils/config.mjs +++ b/src/generators/web/utils/config.mjs @@ -75,8 +75,11 @@ export function buildLanguageDisplayNameMap() { export default function createConfigSource(input) { const { version: configVersion, ...config } = getConfig('web'); - const version = `v${configVersion.version}`; - const editURL = populate(config.editURL, { ...config, version }); + const versionLabel = `v${configVersion.version}`; + const editURL = populate(config.editURL, { + ...config, + version: versionLabel, + }); const pageURL = populate(config.pageURL, config); const exports = { @@ -85,8 +88,7 @@ export default function createConfigSource(input) { // These are keys that are large, and not needed by components, so we ignore them ['changelog', 'index', 'imports', 'virtualImports'] ), - version, - versionMajor: configVersion.major ?? null, + version: configVersion, versions: buildVersionEntries(config.changelog, pageURL), editURL, pages: buildPageList(input), From 0e58c964724160c47fe158cf51af83e3f72a7acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 08:34:30 -0300 Subject: [PATCH 36/39] chore: add todo issue link --- src/generators/web/ui/components/AnnouncementBanner/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index f4a0dde2..928a6100 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -5,7 +5,7 @@ import { loadBanners } from './loadBanners.mjs'; import { remoteConfig, version } from '#theme/config'; -// TODO: Revisit SERVER global usage. +// TODO: Revisit SERVER global usage (https://github.com/nodejs/doc-kit/issues/353) const LazyBanners = SERVER ? null : lazy(async () => { From da0639cd960198c8255ef9d7427626472a22a729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 15:42:04 +0100 Subject: [PATCH 37/39] refactor: inline version label --- src/generators/web/utils/config.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/generators/web/utils/config.mjs b/src/generators/web/utils/config.mjs index f58f303f..5712b6ff 100644 --- a/src/generators/web/utils/config.mjs +++ b/src/generators/web/utils/config.mjs @@ -75,10 +75,9 @@ export function buildLanguageDisplayNameMap() { export default function createConfigSource(input) { const { version: configVersion, ...config } = getConfig('web'); - const versionLabel = `v${configVersion.version}`; const editURL = populate(config.editURL, { ...config, - version: versionLabel, + version: `v${configVersion.version}`, }); const pageURL = populate(config.pageURL, config); From d10254760bb2aacf98a81bf295a0c79cdef8530f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 15:44:42 +0100 Subject: [PATCH 38/39] refactor: restructure component --- .../AnnouncementBanner/AnnouncementBanner.jsx | 26 ---------- .../RemoteLoadableBanner.jsx | 31 +++++++++++ .../components/AnnouncementBanner/index.jsx | 51 +++++++++---------- .../web/ui/components/Layout/index.jsx | 2 +- 4 files changed, 55 insertions(+), 55 deletions(-) delete mode 100644 src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx create mode 100644 src/generators/web/ui/components/AnnouncementBanner/RemoteLoadableBanner.jsx diff --git a/src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx b/src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx deleted file mode 100644 index e6be507a..00000000 --- a/src/generators/web/ui/components/AnnouncementBanner/AnnouncementBanner.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; -import Banner from '@node-core/ui-components/Common/Banner'; - -import styles from './index.module.css'; - -/** - * @param {import('./types.d.ts').AnnouncementBannerProps} 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/RemoteLoadableBanner.jsx b/src/generators/web/ui/components/AnnouncementBanner/RemoteLoadableBanner.jsx new file mode 100644 index 00000000..13627750 --- /dev/null +++ b/src/generators/web/ui/components/AnnouncementBanner/RemoteLoadableBanner.jsx @@ -0,0 +1,31 @@ +import { lazy, Suspense } from 'preact/compat'; + +import AnnouncementBanner from './index.jsx'; +import { loadBanners } from './loadBanners.mjs'; + +import { remoteConfig, version } from '#theme/config'; + +// TODO: Revisit SERVER global usage (https://github.com/nodejs/doc-kit/issues/353) +const LazyBanners = SERVER + ? null + : lazy(async () => { + const active = await loadBanners(remoteConfig, version.major); + + if (!active.length) { + return { default: () => null }; + } + + return { default: () => }; + }); + +const RemoteLoadableBanner = SERVER + ? () =>
+ : () => ( +
+ + + +
+ ); + +export default RemoteLoadableBanner; diff --git a/src/generators/web/ui/components/AnnouncementBanner/index.jsx b/src/generators/web/ui/components/AnnouncementBanner/index.jsx index 928a6100..e6be507a 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/index.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/index.jsx @@ -1,31 +1,26 @@ -import { lazy, Suspense } from 'preact/compat'; +import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; +import Banner from '@node-core/ui-components/Common/Banner'; -import AnnouncementBanner from './AnnouncementBanner.jsx'; -import { loadBanners } from './loadBanners.mjs'; +import styles from './index.module.css'; -import { remoteConfig, version } from '#theme/config'; +/** + * @param {import('./types.d.ts').AnnouncementBannerProps} props + */ +const AnnouncementBanner = ({ banners }) => ( +
+ {banners.map(banner => ( + + {banner.link ? ( + + {banner.text} + + ) : ( + banner.text + )} + {banner.link && } + + ))} +
+); -// TODO: Revisit SERVER global usage (https://github.com/nodejs/doc-kit/issues/353) -const LazyBanners = SERVER - ? null - : lazy(async () => { - const active = await loadBanners(remoteConfig, version.major); - - if (!active.length) { - return { default: () => null }; - } - - return { default: () => }; - }); - -const RemoteLoadableBanner = SERVER - ? () =>
- : () => ( -
- - - -
- ); - -export default RemoteLoadableBanner; +export default AnnouncementBanner; diff --git a/src/generators/web/ui/components/Layout/index.jsx b/src/generators/web/ui/components/Layout/index.jsx index b4416363..8b2992c8 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/RemoteLoadableBanner'; import Footer from '#theme/Footer'; import MetaBar from '#theme/Metabar'; From 5243f37bd81dc7a2bfded6e958a6adc359fe1499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 14 Apr 2026 15:47:28 +0100 Subject: [PATCH 39/39] refactor: rename to remoteConfigUrl --- src/generators/web/index.mjs | 2 +- .../AnnouncementBanner/RemoteLoadableBanner.jsx | 4 ++-- .../ui/components/AnnouncementBanner/loadBanners.mjs | 10 +++++----- src/generators/web/ui/types.d.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/generators/web/index.mjs b/src/generators/web/index.mjs index ca0bb6a9..7f86ba9b 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -34,7 +34,7 @@ export default createLazyGenerator({ useAbsoluteURLs: false, editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`, pageURL: '{baseURL}/latest-{version}/api{path}.html', - remoteConfig: 'https://nodejs.org/site.json', + remoteConfigUrl: '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/RemoteLoadableBanner.jsx b/src/generators/web/ui/components/AnnouncementBanner/RemoteLoadableBanner.jsx index 13627750..e6116383 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/RemoteLoadableBanner.jsx +++ b/src/generators/web/ui/components/AnnouncementBanner/RemoteLoadableBanner.jsx @@ -3,13 +3,13 @@ import { lazy, Suspense } from 'preact/compat'; import AnnouncementBanner from './index.jsx'; import { loadBanners } from './loadBanners.mjs'; -import { remoteConfig, version } from '#theme/config'; +import { remoteConfigUrl, version } from '#theme/config'; // TODO: Revisit SERVER global usage (https://github.com/nodejs/doc-kit/issues/353) const LazyBanners = SERVER ? null : lazy(async () => { - const active = await loadBanners(remoteConfig, version.major); + const active = await loadBanners(remoteConfigUrl, version.major); if (!active.length) { return { default: () => null }; diff --git a/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs index 818cab8f..8e1cd4d3 100644 --- a/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs +++ b/src/generators/web/ui/components/AnnouncementBanner/loadBanners.mjs @@ -2,20 +2,20 @@ import { isBannerActive } from '../../utils/banner.mjs'; /** * Fetches and returns active banners for the given version. - * Returns an empty array when remoteConfig is absent, the response is not ok, + * Returns an empty array when remoteConfigUrl is absent, the response is not ok, * or on any fetch/parse failure. * - * @param {string | undefined} remoteConfig + * @param {string | undefined} remoteConfigUrl * @param {number | null} versionMajor * @returns {Promise>} */ -export const loadBanners = async (remoteConfig, versionMajor) => { - if (!remoteConfig) { +export const loadBanners = async (remoteConfigUrl, versionMajor) => { + if (!remoteConfigUrl) { return []; } try { - const res = await fetch(remoteConfig); + const res = await fetch(remoteConfigUrl); /** @type {import('./types.d.ts').RemoteConfig} */ const { websiteBanners = {} } = await res.json(); diff --git a/src/generators/web/ui/types.d.ts b/src/generators/web/ui/types.d.ts index b872b09b..b1eb7d25 100644 --- a/src/generators/web/ui/types.d.ts +++ b/src/generators/web/ui/types.d.ts @@ -33,7 +33,7 @@ declare module '#theme/config' { export const editURL: string; export const pages: Array<[string, string]>; export const languageDisplayNameMap: Map; - export const remoteConfig: string; + export const remoteConfigUrl: string; } // Omit Primitives from Metadata