diff --git a/.changeset/popular-cherries-float.md b/.changeset/popular-cherries-float.md new file mode 100644 index 000000000000..f04ab3f3ce54 --- /dev/null +++ b/.changeset/popular-cherries-float.md @@ -0,0 +1,18 @@ +--- +'@astrojs/sitemap': minor +--- + +# Key features + +- Split up your large sitemap into multiple sitemaps by custom limit. +- Ability to add sitemap specific attributes such as `lastmod` etc. +- Final output customization via JS function. +- Localization support. +- Reliability: all config options are validated. + +## Important changes + +The integration always generates at least two files instead of one: + +- `sitemap-index.xml` - index file; +- `sitemap-{i}.xml` - actual sitemap. diff --git a/examples/integrations-playground/astro.config.mjs b/examples/integrations-playground/astro.config.mjs index a7c3d4a0b7a2..939d228669c3 100644 --- a/examples/integrations-playground/astro.config.mjs +++ b/examples/integrations-playground/astro.config.mjs @@ -9,5 +9,6 @@ import solid from '@astrojs/solid-js'; // https://astro.build/config export default defineConfig({ + site: 'https://example.com', integrations: [lit(), react(), tailwind(), turbolinks(), partytown(), sitemap(), solid()], }); diff --git a/packages/integrations/sitemap/README.md b/packages/integrations/sitemap/README.md index 9d84cae195d2..ab880a075f0a 100644 --- a/packages/integrations/sitemap/README.md +++ b/packages/integrations/sitemap/README.md @@ -64,7 +64,35 @@ export default { } ``` -Now, [build your site for production](https://docs.astro.build/en/reference/cli-reference/#astro-build) via the `astro build` command. You should find your sitemap under `dist/sitemap.xml`! +Now, [build your site for production](https://docs.astro.build/en/reference/cli-reference/#astro-build) via the `astro build` command. You should find your _sitemap_ under `dist/sitemap-index.xml` and `dist/sitemap-0.xml`! + +Generated sitemap content for two pages website: + +**sitemap-index.xml** + +```xml + + + + https://stargazers.club/sitemap-0.xml + + +``` + +**sitemap-0.xml** + + +```xml + + + + https://stargazers.club/ + + + https://stargazers.club/second-page/ + + +``` You can also check our [Astro Integration Documentation][astro-integration] for more on integrations. @@ -111,5 +139,158 @@ export default { } ``` +### entryLimit + +Non-negative `Number` of entries per sitemap file. Default value is 45000. A sitemap index and multiple sitemaps are created if you have more entries. See explanation on [Google](https://developers.google.com/search/docs/advanced/sitemaps/large-sitemaps). + +__astro.config.mjs__ + +```js +import sitemap from '@astrojs/sitemap'; + +export default { + site: 'https://stargazers.club', + integrations: [ + sitemap({ + entryLimit: 10000, + }), + ], +} +``` + +### changefreq, lastmod, priority + +`changefreq` - How frequently the page is likely to change. Available values: `always` \| `hourly` \| `daily` \| `weekly` \| `monthly` \| `yearly` \| `never`. + +`priority` - The priority of this URL relative to other URLs on your site. Valid values range from 0.0 to 1.0. + +`lastmod` - The date of page last modification. + +`changefreq` and `priority` are ignored by Google. + +See detailed explanation of sitemap specific options on [sitemap.org](https://www.sitemaps.org/protocol.html). + + +:exclamation: This integration uses 'astro:build:done' hook. The hook exposes generated page paths only. So with present version of Astro the integration has no abilities to analyze a page source, frontmatter etc. The integration can add `changefreq`, `lastmod` and `priority` attributes only in a batch or nothing. + +__astro.config.mjs__ + +```js +import sitemap from '@astrojs/sitemap'; + +export default { + site: 'https://stargazers.club', + integrations: [ + sitemap({ + changefreq: 'weekly', + priority: 0.7, + lastmod: new Date('2022-02-24'), + }), + ], +} +``` + +### serialize + +Async or sync function called for each sitemap entry just before writing to a disk. + +It receives as parameter `SitemapItem` object which consists of `url` (required, absolute page URL) and optional `changefreq`, `lastmod`, `priority` and `links` properties. + +Optional `links` property contains a `LinkItem` list of alternate pages including a parent page. +`LinkItem` type has two required fields: `url` (the fully-qualified URL for the version of this page for the specified language) and `hreflang` (a supported language code targeted by this version of the page). + +`serialize` function should return `SitemapItem`, touched or not. + +The example below shows the ability to add the sitemap specific properties individually. + +__astro.config.mjs__ + +```js +import sitemap from '@astrojs/sitemap'; + +export default { + site: 'https://stargazers.club', + integrations: [ + sitemap({ + serialize(item) { + if (/your-special-page/.test(item.url)) { + item.changefreq = 'daily'; + item.lastmod = new Date(); + item.priority = 0.9; + } + return item; + }, + }), + ], +} +``` + +### i18n + +To localize a sitemap you should supply the integration config with the `i18n` option. The integration will check generated page paths on presence of locale keys in paths. + +`i18n` object has two required properties: + +- `defaultLocale`: `String`. Its value must exist as one of `locales` keys. +- `locales`: `Record`, key/value - pairs. The key is used to look for a locale part in a page path. The value is a language attribute, only English alphabet and hyphen allowed. See more about language attribute on [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang). + + +Read more about localization on Google in [Advanced SEO](https://developers.google.com/search/docs/advanced/crawling/localized-versions#all-method-guidelines). + +__astro.config.mjs__ + +```js +import sitemap from '@astrojs/sitemap'; + +export default { + site: 'https://stargazers.club', + integrations: [ + sitemap({ + i18n: { + defaultLocale: 'en', // All urls that don't contain `es` or `fr` after `https://stargazers.club/` will be treated as default locale, i.e. `en` + locales: { + en: 'en-US', // The `defaultLocale` value must present in `locales` keys + es: 'es-ES', + fr: 'fr-CA', + }, + }, + }), + ], +}; +... + +``` + +The sitemap content will be: + +```xml +... + + https://stargazers.club/ + + + + + + https://stargazers.club/es/ + + + + + + https://stargazers.club/fr/ + + + + + + https://stargazers.club/es/second-page/ + + + + +... +``` + [astro-integration]: https://docs.astro.build/en/guides/integrations-guide/ [astro-ui-frameworks]: https://docs.astro.build/en/core-concepts/framework-components/#using-framework-components diff --git a/packages/integrations/sitemap/package.json b/packages/integrations/sitemap/package.json index 3fbfc2b94b5e..d61c608b3b54 100644 --- a/packages/integrations/sitemap/package.json +++ b/packages/integrations/sitemap/package.json @@ -13,7 +13,8 @@ }, "keywords": [ "astro-component", - "seo" + "seo", + "sitemap" ], "bugs": "https://github.com/withastro/astro/issues", "homepage": "https://astro.build", @@ -21,12 +22,18 @@ ".": "./dist/index.js", "./package.json": "./package.json" }, + "files": [ + "dist" + ], "scripts": { "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"" }, - "dependencies": {}, + "dependencies": { + "sitemap": "^7.1.1", + "zod": "^3.17.3" + }, "devDependencies": { "astro": "workspace:*", "astro-scripts": "workspace:*" diff --git a/packages/integrations/sitemap/src/config-defaults.ts b/packages/integrations/sitemap/src/config-defaults.ts new file mode 100644 index 000000000000..22288fc119eb --- /dev/null +++ b/packages/integrations/sitemap/src/config-defaults.ts @@ -0,0 +1,5 @@ +import type { SitemapOptions } from './index'; + +export const SITEMAP_CONFIG_DEFAULTS: SitemapOptions & any = { + entryLimit: 45000, +}; diff --git a/packages/integrations/sitemap/src/constants.ts b/packages/integrations/sitemap/src/constants.ts new file mode 100644 index 000000000000..431cc5954e13 --- /dev/null +++ b/packages/integrations/sitemap/src/constants.ts @@ -0,0 +1,9 @@ +export const changefreqValues = [ + 'always', + 'hourly', + 'daily', + 'weekly', + 'monthly', + 'yearly', + 'never', +] as const; diff --git a/packages/integrations/sitemap/src/generate-sitemap.ts b/packages/integrations/sitemap/src/generate-sitemap.ts new file mode 100644 index 000000000000..3c39e1f7ec45 --- /dev/null +++ b/packages/integrations/sitemap/src/generate-sitemap.ts @@ -0,0 +1,55 @@ +import { SitemapItemLoose } from 'sitemap'; + +import type { SitemapOptions } from './index'; +import { parseUrl } from './utils/parse-url'; + +const STATUS_CODE_PAGE_REGEXP = /\/[0-9]{3}\/?$/; + +/** Construct sitemap.xml given a set of URLs */ +export function generateSitemap(pages: string[], finalSiteUrl: string, opts: SitemapOptions) { + const { changefreq, priority: prioritySrc, lastmod: lastmodSrc, i18n } = opts || {}; + // TODO: find way to respect URLs here + const urls = [...pages].filter((url) => !STATUS_CODE_PAGE_REGEXP.test(url)); + urls.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); // sort alphabetically so sitemap is same each time + + const lastmod = lastmodSrc?.toISOString(); + const priority = typeof prioritySrc === 'number' ? prioritySrc : undefined; + + const { locales, defaultLocale } = i18n || {}; + const localeCodes = Object.keys(locales || {}); + + const getPath = (url: string) => { + const result = parseUrl(url, i18n?.defaultLocale || '', localeCodes, finalSiteUrl); + return result?.path; + }; + const getLocale = (url: string) => { + const result = parseUrl(url, i18n?.defaultLocale || '', localeCodes, finalSiteUrl); + return result?.locale; + }; + + const urlData = urls.map((url) => { + let links; + if (defaultLocale && locales) { + const currentPath = getPath(url); + if (currentPath) { + const filtered = urls.filter((subUrl) => getPath(subUrl) === currentPath); + if (filtered.length > 1) { + links = filtered.map((subUrl) => ({ + url: subUrl, + lang: locales[getLocale(subUrl)!], + })); + } + } + } + + return { + url, + links, + lastmod, + priority, + changefreq, // : changefreq as EnumChangefreq, + } as SitemapItemLoose; + }); + + return urlData; +} diff --git a/packages/integrations/sitemap/src/index.ts b/packages/integrations/sitemap/src/index.ts index 169eeb788701..700f68876926 100644 --- a/packages/integrations/sitemap/src/index.ts +++ b/packages/integrations/sitemap/src/index.ts @@ -1,91 +1,138 @@ +import { fileURLToPath } from 'url'; import type { AstroConfig, AstroIntegration } from 'astro'; -import fs from 'node:fs'; -const STATUS_CODE_PAGE_REGEXP = /\/[0-9]{3}\/?$/; +import { ZodError } from 'zod'; +import { LinkItem as LinkItemBase, SitemapItemLoose, simpleSitemapAndIndex } from 'sitemap'; -type SitemapOptions = +import { Logger } from './utils/logger'; +import { changefreqValues } from './constants'; +import { validateOptions } from './validate-options'; +import { generateSitemap } from './generate-sitemap'; + +export type ChangeFreq = typeof changefreqValues[number]; +export type SitemapItem = Pick< + SitemapItemLoose, + 'url' | 'lastmod' | 'changefreq' | 'priority' | 'links' +>; +export type LinkItem = LinkItemBase; + +export type SitemapOptions = | { - /** - * All pages are included in your sitemap by default. - * With this config option, you can filter included pages by URL. - * - * The `page` function parameter is the full URL of your rendered page, including your `site` domain. - * Return `true` to include a page in your sitemap, and `false` to remove it. - * - * ```js - * filter: (page) => page !== 'http://example.com/secret-page' - * ``` - */ filter?(page: string): boolean; - - /** - * If you have any URL, not rendered by Astro, that you want to include in your sitemap, - * this config option will help you to include your array of custom pages in your sitemap. - * - * ```js - * customPages: ['http://example.com/custom-page', 'http://example.com/custom-page2'] - * ``` - */ - customPages?: Array; - - /** - * If present, we use the `site` config option as the base for all sitemap URLs - * Use `canonicalURL` to override this - */ + customPages?: string[]; canonicalURL?: string; + + i18n?: { + defaultLocale: string; + locales: Record; + }; + // number of entries per sitemap file + entryLimit?: number; + + // sitemap specific + changefreq?: ChangeFreq; + lastmod?: Date; + priority?: number; + + // called for each sitemap item just before to save them on disk, sync or async + serialize?(item: SitemapItemLoose): SitemapItemLoose; } | undefined; -/** Construct sitemap.xml given a set of URLs */ -function generateSitemap(pages: string[]) { - // TODO: find way to respect URLs here - const urls = [...pages].filter((url) => !STATUS_CODE_PAGE_REGEXP.test(url)); - urls.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); // sort alphabetically so sitemap is same each time - let sitemap = ``; - for (const url of urls) { - sitemap += `${url}`; - } - sitemap += `\n`; - return sitemap; +function formatConfigErrorMessage(err: ZodError) { + const errorList = err.issues.map((issue) => ` ${issue.path.join('.')} ${issue.message + '.'}`); + return errorList.join('\n'); } -export default function createPlugin({ - filter, - customPages, - canonicalURL, -}: SitemapOptions = {}): AstroIntegration { +const PKG_NAME = '@astrojs/sitemap'; +const OUTFILE = 'sitemap-index.xml'; + +const createPlugin = (options?: SitemapOptions): AstroIntegration => { let config: AstroConfig; return { - name: '@astrojs/sitemap', + name: PKG_NAME, + hooks: { - 'astro:config:done': async ({ config: _config }) => { - config = _config; + 'astro:config:done': async ({ config: cfg }) => { + config = cfg; }, - 'astro:build:done': async ({ pages, dir }) => { - let finalSiteUrl: URL; - if (canonicalURL) { - finalSiteUrl = new URL(canonicalURL); - finalSiteUrl.pathname += finalSiteUrl.pathname.endsWith('/') ? '' : '/'; // normalizes the final url since it's provided by user - } else if (config.site) { - finalSiteUrl = new URL(config.base, config.site); - } else { - console.warn( - 'The Sitemap integration requires either the `site` astro.config option or `canonicalURL` integration option. Skipping.' - ); - return; - } - let pageUrls = pages.map((p) => { - const path = finalSiteUrl.pathname + p.pathname; - return new URL(path, finalSiteUrl).href; - }); - if (filter) { - pageUrls = pageUrls.filter((page: string) => filter(page)); - } - if (customPages) { - pageUrls = [...pageUrls, ...customPages]; + + 'astro:build:done': async ({ dir, pages }) => { + const logger = new Logger(PKG_NAME); + + try { + const opts = validateOptions(config.site, options); + + const { filter, customPages, canonicalURL, serialize, entryLimit } = opts; + + let finalSiteUrl: URL; + if (canonicalURL) { + finalSiteUrl = new URL(canonicalURL); + if (!finalSiteUrl.pathname.endsWith('/')) { + finalSiteUrl.pathname += '/'; // normalizes the final url since it's provided by user + } + } else { + // `validateOptions` forces to provide `canonicalURL` or `config.site` at least. + // So step to check on empty values of `canonicalURL` and `config.site` is dropped. + finalSiteUrl = new URL(config.base, config.site); + } + + let pageUrls = pages.map((p) => { + const path = finalSiteUrl.pathname + p.pathname; + return new URL(path, finalSiteUrl).href; + }); + + try { + if (filter) { + pageUrls = pageUrls.filter(filter); + } + } catch (err) { + logger.error(`Error filtering pages\n${(err as any).toString()}`); + return; + } + + if (customPages) { + pageUrls = [...pageUrls, ...customPages]; + } + + if (pageUrls.length === 0) { + logger.warn(`No data for sitemap.\n\`${OUTFILE}\` is not created.`); + return; + } + + let urlData = generateSitemap(pageUrls, finalSiteUrl.href, opts); + + if (serialize) { + try { + const serializedUrls: SitemapItemLoose[] = []; + for (const item of urlData) { + const serialized = await Promise.resolve(serialize(item)); + serializedUrls.push(serialized); + } + urlData = serializedUrls; + } catch (err) { + logger.error(`Error serializing pages\n${(err as any).toString()}`); + return; + } + } + + await simpleSitemapAndIndex({ + hostname: finalSiteUrl.href, + destinationDir: fileURLToPath(dir), + sourceData: urlData, + limit: entryLimit, + gzip: false, + }); + logger.success(`\`${OUTFILE}\` is created.`); + } catch (err) { + if (err instanceof ZodError) { + logger.warn(formatConfigErrorMessage(err)); + } else { + throw err; + } } - const sitemapContent = generateSitemap(pageUrls); - fs.writeFileSync(new URL('sitemap.xml', dir), sitemapContent); }, }, }; -} +}; + +export default createPlugin; diff --git a/packages/integrations/sitemap/src/schema.ts b/packages/integrations/sitemap/src/schema.ts new file mode 100644 index 000000000000..723f9ac58494 --- /dev/null +++ b/packages/integrations/sitemap/src/schema.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; +import { changefreqValues } from './constants'; +import { SITEMAP_CONFIG_DEFAULTS } from './config-defaults'; + +const localeKeySchema = () => z.string().min(1); + +const isFunction = (fn: any) => fn instanceof Function; + +const fnSchema = () => + z + .any() + .refine((val) => !val || isFunction(val), { message: 'Not a function' }) + .optional(); + +export const SitemapOptionsSchema = z + .object({ + filter: fnSchema(), + customPages: z.string().url().array().optional(), + canonicalURL: z.string().url().optional(), + + i18n: z + .object({ + defaultLocale: localeKeySchema(), + locales: z.record( + localeKeySchema(), + z + .string() + .min(2) + .regex(/^[a-zA-Z\-]+$/gm, { + message: 'Only English alphabet symbols and hyphen allowed', + }) + ), + }) + .refine((val) => !val || val.locales[val.defaultLocale], { + message: '`defaultLocale` must exists in `locales` keys', + }) + .optional(), + + entryLimit: z.number().nonnegative().default(SITEMAP_CONFIG_DEFAULTS.entryLimit), + serialize: fnSchema(), + + changefreq: z.enum(changefreqValues).optional(), + lastmod: z.date().optional(), + priority: z.number().min(0).max(1).optional(), + }) + .strict() + .default(SITEMAP_CONFIG_DEFAULTS); diff --git a/packages/integrations/sitemap/src/utils/is-object-empty.ts b/packages/integrations/sitemap/src/utils/is-object-empty.ts new file mode 100644 index 000000000000..0d61810695a1 --- /dev/null +++ b/packages/integrations/sitemap/src/utils/is-object-empty.ts @@ -0,0 +1,10 @@ +// @internal +export const isObjectEmpty = (o: any) => { + if (!o) { + return true; + } + if (Array.isArray(o)) { + return o.length === 0; + } + return Object.keys(o).length === 0 && Object.getPrototypeOf(o) === Object.prototype; +}; diff --git a/packages/integrations/sitemap/src/utils/is-valid-url.ts b/packages/integrations/sitemap/src/utils/is-valid-url.ts new file mode 100644 index 000000000000..b140623b090f --- /dev/null +++ b/packages/integrations/sitemap/src/utils/is-valid-url.ts @@ -0,0 +1,13 @@ +// @internal +export const isValidUrl = (s: any) => { + if (typeof s !== 'string' || !s) { + return false; + } + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const dummy = new URL(s); + return true; + } catch { + return false; + } +}; diff --git a/packages/integrations/sitemap/src/utils/logger.ts b/packages/integrations/sitemap/src/utils/logger.ts new file mode 100644 index 000000000000..203baeaa7a57 --- /dev/null +++ b/packages/integrations/sitemap/src/utils/logger.ts @@ -0,0 +1,46 @@ +// @internal +export interface ILogger { + info(msg: string): void; + success(msg: string): void; + warn(msg: string): void; + error(msg: string): void; +} + +// @internal +export class Logger implements ILogger { + private colors = { + reset: '\x1b[0m', + fg: { + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + }, + } as const; + + private packageName: string; + + constructor(packageName: string) { + this.packageName = packageName; + } + + private log(msg: string, prefix: string = '') { + // eslint-disable-next-line no-console + console.log(`%s${this.packageName}:%s ${msg}\n`, prefix, prefix ? this.colors.reset : ''); + } + + info(msg: string) { + this.log(msg); + } + + success(msg: string) { + this.log(msg, this.colors.fg.green); + } + + warn(msg: string) { + this.log(`Skipped!\n${msg}`, this.colors.fg.yellow); + } + + error(msg: string) { + this.log(`Failed!\n${msg}`, this.colors.fg.red); + } +} diff --git a/packages/integrations/sitemap/src/utils/parse-url.ts b/packages/integrations/sitemap/src/utils/parse-url.ts new file mode 100644 index 000000000000..f9189cf7d60d --- /dev/null +++ b/packages/integrations/sitemap/src/utils/parse-url.ts @@ -0,0 +1,39 @@ +export const parseUrl = ( + url: string, + defaultLocale: string, + localeCodes: string[], + base: string +) => { + if ( + !url || + !defaultLocale || + localeCodes.length === 0 || + localeCodes.some((key) => !key) || + !base + ) { + throw new Error('parseUrl: some parameters are empty'); + } + if (url.indexOf(base) !== 0) { + return undefined; + } + let s = url.replace(base, ''); + if (!s || s === '/') { + return { locale: defaultLocale, path: '/' }; + } + if (!s.startsWith('/')) { + s = '/' + s; + } + const a = s.split('/'); + const locale = a[1]; + if (localeCodes.some((key) => key === locale)) { + let path = a.slice(2).join('/'); + if (path === '//') { + path = '/'; + } + if (path !== '/' && !path.startsWith('/')) { + path = '/' + path; + } + return { locale, path }; + } + return { locale: defaultLocale, path: s }; +}; diff --git a/packages/integrations/sitemap/src/validate-options.ts b/packages/integrations/sitemap/src/validate-options.ts new file mode 100644 index 000000000000..f89582d82747 --- /dev/null +++ b/packages/integrations/sitemap/src/validate-options.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; +import type { SitemapOptions } from './index'; +import { SitemapOptionsSchema } from './schema'; + +// @internal +export const validateOptions = (site: string | undefined, opts: SitemapOptions) => { + const result = SitemapOptionsSchema.parse(opts); + + z.object({ + site: z.string().optional(), // Astro takes care of `site`: how to validate, transform and refine + canonicalURL: z.string().optional(), // `canonicalURL` is already validated in prev step + }) + .refine(({ site, canonicalURL }) => site || canonicalURL, { + message: 'Required `site` astro.config option or `canonicalURL` integration option', + }) + .parse({ + site, + canonicalURL: result.canonicalURL, + }); + + return result; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abb1384cfb26..7e862156e40b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1824,6 +1824,11 @@ importers: specifiers: astro: workspace:* astro-scripts: workspace:* + sitemap: ^7.1.1 + zod: ^3.17.3 + dependencies: + sitemap: 7.1.1 + zod: 3.17.3 devDependencies: astro: link:../../astro astro-scripts: link:../../../scripts @@ -6852,6 +6857,12 @@ packages: '@types/node': 17.0.41 dev: false + /@types/sax/1.2.4: + resolution: {integrity: sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==} + dependencies: + '@types/node': 17.0.41 + dev: false + /@types/scheduler/0.16.2: resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} @@ -8164,6 +8175,11 @@ packages: /debug/3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: ms: 2.1.3 dev: false @@ -11080,6 +11096,8 @@ packages: debug: 3.2.7 iconv-lite: 0.4.24 sax: 1.2.4 + transitivePeerDependencies: + - supports-color dev: false /netmask/2.0.2: @@ -11163,6 +11181,8 @@ packages: rimraf: 2.7.1 semver: 5.7.1 tar: 4.4.19 + transitivePeerDependencies: + - supports-color dev: false /node-releases/2.0.5: @@ -12535,6 +12555,17 @@ packages: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: false + /sitemap/7.1.1: + resolution: {integrity: sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==} + engines: {node: '>=12.0.0', npm: '>=5.6.0'} + hasBin: true + dependencies: + '@types/node': 17.0.41 + '@types/sax': 1.2.4 + arg: 5.0.2 + sax: 1.2.4 + dev: false + /slash/2.0.0: resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} engines: {node: '>=6'}