gscdump 1.4.11 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -18,7 +18,7 @@ import { hasGscReadScope, hasGscWriteScope, hasIndexingScope } from "./core/scop
18
18
  import { ParsedGscSiteUrl, normalizeGscSiteUrl, normalizeRegistrationTarget, parseGscSiteUrl } from "./core/site-url.mjs";
19
19
  import { normalizeUrl } from "./normalize.mjs";
20
20
  import { AccountNextAction, AccountStatus, AnalyticsNextAction, AnalyticsStatus, GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSCDUMP_WRITE_ANALYTICS_SCOPE, IndexingNextAction, IndexingStatus, LifecycleError, LifecycleErrorCode, LifecycleProgress, LifecycleWebhookEnvelope, LifecycleWebhookEvent, PartnerLifecycleAccount, PartnerLifecycleResponse, PartnerLifecycleSite, PropertyNextAction, PropertyStatus, QuerySourceMode, SitemapNextAction, SitemapStatus, accountNextActions, accountStatuses, analyticsNextActions, analyticsStatuses, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, lifecycleErrorCodes, parseGrantedScopes, propertyNextActions, propertyStatuses, querySourceModes, sitemapNextActions, sitemapStatuses } from "./onboarding.mjs";
21
- import { DiscoverSitemapOptions, FetchSitemapUrlsOptions, discoverSitemap, fetchSitemapUrls } from "./sitemap.mjs";
21
+ import { discoverSitemap, fetchSitemapUrls } from "./sitemap-compat.mjs";
22
22
  import { decodeSiteId, encodeSiteId, normalizeSiteUrl } from "./tenant.mjs";
23
23
  import { urlMatchKey } from "./url.mjs";
24
- export { type AccountNextAction, type AccountStatus, AmpInspectionResult, AmpIssue, type AnalyticsNextAction, type AnalyticsStatus, ApiSite, ApiSitemap, ApiSitemapContent, Auth, AuthClient, AuthOptions, type BackfillProgress, BatchInspectUrlsFlatSettledOptions, CallOptions, CanonicalDifferenceKind, DAYS_PER_RANGE, DEFAULT_GSC_REQUEST_TIMEOUT_MS, DataRow, DimensionFilter, DimensionFilterGroup, DiscoverSitemapOptions, Err, FetchSitemapUrlsOptions, FetchSitesWithSitemapsOptions, GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSCDUMP_WRITE_ANALYTICS_SCOPE, GSC_FINALIZED_LAG_DAYS, GSC_FRESHEST_LAG_DAYS, GSC_INDEXING_SCOPE, GSC_READ_SCOPE, GSC_RETENTION_MONTHS, GSC_SITE_VERIFICATION_SCOPE, GSC_WRITE_SCOPE, GoogleSearchConsoleClient, GoogleSearchConsoleClientOptions, GscApiError, type GscApiErrorInfo, type GscError, type GscErrorKind, GscPropertyCandidate, INDEXING_ISSUE_FILTERS, INDEXING_ISSUE_LABELS, INDEXING_ISSUE_SEVERITY, IndexStatusResult, IndexingEligibility, IndexingIneligibleReason, IndexingIssueType, IndexingMetadata, type IndexingNextAction, IndexingNotificationType, IndexingResult, type IndexingStatus, InspectUrlFlatSettledResult, InspectUrlIndexResponse, InspectUrlResult, InspectionPriority, KNOWN_COVERAGE_STATES, KNOWN_PAGE_FETCH_STATES, LegacyInspectionPriority, type LifecycleError, type LifecycleErrorCode, type LifecycleProgress, type LifecycleWebhookEnvelope, type LifecycleWebhookEvent, MAX_FLAT_INSPECTION_BATCH_CONCURRENCY, MS_PER_DAY, MobileUsabilityIssue, MobileUsabilityResult, OAuthTokenInfo, OAuthTokens, Ok, ParsedGscSiteUrl, ParsedIndexingResult, type PartnerLifecycleAccount, type PartnerLifecycleResponse, type PartnerLifecycleSite, Period, type PropertyNextAction, type PropertyStatus, PublishUrlNotificationResponse, QueryReturn, type QuerySourceMode, RequiredNonNullable, ResolvedAnalyticsRange, Result, RichResultsDetectedItem, RichResultsIssue, RichResultsItem, RichResultsResult, SearchAnalyticsMetadata, SearchAnalyticsQuery, SearchAnalyticsResponse, Site, SiteAnalytics, type SitemapNextAction, type SitemapStatus, URL_INSPECTION_EFFECTIVE_LIMIT, UnmappedInspectionReasons, UrlInspectionResult, UrlNotification, UrlNotificationMetadata, ValueWeightedInspectionPriority, VerificationMethod, VerificationSite, VerificationSiteType, VerificationToken, VerificationWebResource, accountNextActions, accountStatuses, addDays, addSite, analyticsNextActions, analyticsStatuses, batchInspectUrls, batchInspectUrlsFlatSettled, batchRequestIndexing, canUseUrlInspection, classifyCanonicalDifference, classifyError, countDays, createAuth, createFetch, decodeSiteId, deleteSite, deleteSitemap, discoverSitemap, encodeSiteId, err, exchangeAuthCodeResult, fetchSitemap, fetchSitemapUrls, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, findBestGscProperty, findExactGscProperty, formatGscPropertyCandidates, generateGscDateRange, getBackfillProgress, getDateRange, getFreshestGscDate, getIndexingEligibility, getIndexingMetadata, getLatestGscDate, getNextCheckAfter, getNextCheckPriority, getNextDate, getOldestGscDate, getPendingDates, getPreviousDate, getPstDate, getVerificationToken, getVerifiedSite, googleSearchConsole, groupIntoRanges, gscErrorToException, gscPropertyMatchesTarget, hasGoogleScope, hasGscReadScope, hasGscWriteScope, hasIndexingScope, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, inspectUrl, inspectUrlFlat, introspectAccessToken, introspectAccessTokenResult, isCanonicalMismatch, isErr, isFragmentUrl, isOk, isPermissionDeniedError, isValidGscDate, isVerifiedGscPermission, isVerifiedGscProperty, lifecycleErrorCodes, listVerifiedSites, matchGscSite, normalizeGscSiteUrl, normalizeRegistrationTarget, normalizeSiteUrl, normalizeUrl, ok, parseGoogleError, parseGrantedScopes, parseGscSiteUrl, pickBestGscProperty, propertyNextActions, propertyStatuses, querySourceModes, refreshAccessToken, requestIndexing, resolveVerificationTarget, rethrowAsGscApiError, revokeOAuthToken, revokeOAuthTokenResult, runSequentialBatch, siteUrlToVerificationSite, sitemapNextActions, sitemapStatuses, submitSitemap, toIsoDate, unmappedInspectionReasons, unverifySite, unwrapResult, urlMatchKey, verificationMethodsFor, verifySite };
24
+ export { type AccountNextAction, type AccountStatus, AmpInspectionResult, AmpIssue, type AnalyticsNextAction, type AnalyticsStatus, ApiSite, ApiSitemap, ApiSitemapContent, Auth, AuthClient, AuthOptions, type BackfillProgress, BatchInspectUrlsFlatSettledOptions, CallOptions, CanonicalDifferenceKind, DAYS_PER_RANGE, DEFAULT_GSC_REQUEST_TIMEOUT_MS, DataRow, DimensionFilter, DimensionFilterGroup, Err, FetchSitesWithSitemapsOptions, GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSCDUMP_WRITE_ANALYTICS_SCOPE, GSC_FINALIZED_LAG_DAYS, GSC_FRESHEST_LAG_DAYS, GSC_INDEXING_SCOPE, GSC_READ_SCOPE, GSC_RETENTION_MONTHS, GSC_SITE_VERIFICATION_SCOPE, GSC_WRITE_SCOPE, GoogleSearchConsoleClient, GoogleSearchConsoleClientOptions, GscApiError, type GscApiErrorInfo, type GscError, type GscErrorKind, GscPropertyCandidate, INDEXING_ISSUE_FILTERS, INDEXING_ISSUE_LABELS, INDEXING_ISSUE_SEVERITY, IndexStatusResult, IndexingEligibility, IndexingIneligibleReason, IndexingIssueType, IndexingMetadata, type IndexingNextAction, IndexingNotificationType, IndexingResult, type IndexingStatus, InspectUrlFlatSettledResult, InspectUrlIndexResponse, InspectUrlResult, InspectionPriority, KNOWN_COVERAGE_STATES, KNOWN_PAGE_FETCH_STATES, LegacyInspectionPriority, type LifecycleError, type LifecycleErrorCode, type LifecycleProgress, type LifecycleWebhookEnvelope, type LifecycleWebhookEvent, MAX_FLAT_INSPECTION_BATCH_CONCURRENCY, MS_PER_DAY, MobileUsabilityIssue, MobileUsabilityResult, OAuthTokenInfo, OAuthTokens, Ok, ParsedGscSiteUrl, ParsedIndexingResult, type PartnerLifecycleAccount, type PartnerLifecycleResponse, type PartnerLifecycleSite, Period, type PropertyNextAction, type PropertyStatus, PublishUrlNotificationResponse, QueryReturn, type QuerySourceMode, RequiredNonNullable, ResolvedAnalyticsRange, Result, RichResultsDetectedItem, RichResultsIssue, RichResultsItem, RichResultsResult, SearchAnalyticsMetadata, SearchAnalyticsQuery, SearchAnalyticsResponse, Site, SiteAnalytics, type SitemapNextAction, type SitemapStatus, URL_INSPECTION_EFFECTIVE_LIMIT, UnmappedInspectionReasons, UrlInspectionResult, UrlNotification, UrlNotificationMetadata, ValueWeightedInspectionPriority, VerificationMethod, VerificationSite, VerificationSiteType, VerificationToken, VerificationWebResource, accountNextActions, accountStatuses, addDays, addSite, analyticsNextActions, analyticsStatuses, batchInspectUrls, batchInspectUrlsFlatSettled, batchRequestIndexing, canUseUrlInspection, classifyCanonicalDifference, classifyError, countDays, createAuth, createFetch, decodeSiteId, deleteSite, deleteSitemap, discoverSitemap, encodeSiteId, err, exchangeAuthCodeResult, fetchSitemap, fetchSitemapUrls, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, findBestGscProperty, findExactGscProperty, formatGscPropertyCandidates, generateGscDateRange, getBackfillProgress, getDateRange, getFreshestGscDate, getIndexingEligibility, getIndexingMetadata, getLatestGscDate, getNextCheckAfter, getNextCheckPriority, getNextDate, getOldestGscDate, getPendingDates, getPreviousDate, getPstDate, getVerificationToken, getVerifiedSite, googleSearchConsole, groupIntoRanges, gscErrorToException, gscPropertyMatchesTarget, hasGoogleScope, hasGscReadScope, hasGscWriteScope, hasIndexingScope, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, inspectUrl, inspectUrlFlat, introspectAccessToken, introspectAccessTokenResult, isCanonicalMismatch, isErr, isFragmentUrl, isOk, isPermissionDeniedError, isValidGscDate, isVerifiedGscPermission, isVerifiedGscProperty, lifecycleErrorCodes, listVerifiedSites, matchGscSite, normalizeGscSiteUrl, normalizeRegistrationTarget, normalizeSiteUrl, normalizeUrl, ok, parseGoogleError, parseGrantedScopes, parseGscSiteUrl, pickBestGscProperty, propertyNextActions, propertyStatuses, querySourceModes, refreshAccessToken, requestIndexing, resolveVerificationTarget, rethrowAsGscApiError, revokeOAuthToken, revokeOAuthTokenResult, runSequentialBatch, siteUrlToVerificationSite, sitemapNextActions, sitemapStatuses, submitSitemap, toIsoDate, unmappedInspectionReasons, unverifySite, unwrapResult, urlMatchKey, verificationMethodsFor, verifySite };
package/dist/index.mjs CHANGED
@@ -18,7 +18,7 @@ import { URL_INSPECTION_EFFECTIVE_LIMIT } from "./core/quota.mjs";
18
18
  import "./core/types.mjs";
19
19
  import { normalizeUrl } from "./normalize.mjs";
20
20
  import { GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSCDUMP_WRITE_ANALYTICS_SCOPE, accountNextActions, accountStatuses, analyticsNextActions, analyticsStatuses, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, lifecycleErrorCodes, parseGrantedScopes, propertyNextActions, propertyStatuses, querySourceModes, sitemapNextActions, sitemapStatuses } from "./onboarding.mjs";
21
- import { discoverSitemap, fetchSitemapUrls } from "./sitemap.mjs";
21
+ import { discoverSitemap, fetchSitemapUrls } from "./sitemap-compat.mjs";
22
22
  import { decodeSiteId, encodeSiteId, normalizeSiteUrl } from "./tenant.mjs";
23
23
  import { urlMatchKey } from "./url.mjs";
24
24
  export { DAYS_PER_RANGE, DEFAULT_GSC_REQUEST_TIMEOUT_MS, GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSCDUMP_WRITE_ANALYTICS_SCOPE, GSC_FINALIZED_LAG_DAYS, GSC_FRESHEST_LAG_DAYS, GSC_INDEXING_SCOPE, GSC_READ_SCOPE, GSC_RETENTION_MONTHS, GSC_SITE_VERIFICATION_SCOPE, GSC_WRITE_SCOPE, GscApiError, INDEXING_ISSUE_FILTERS, INDEXING_ISSUE_LABELS, INDEXING_ISSUE_SEVERITY, KNOWN_COVERAGE_STATES, KNOWN_PAGE_FETCH_STATES, MAX_FLAT_INSPECTION_BATCH_CONCURRENCY, MS_PER_DAY, URL_INSPECTION_EFFECTIVE_LIMIT, accountNextActions, accountStatuses, addDays, addSite, analyticsNextActions, analyticsStatuses, batchInspectUrls, batchInspectUrlsFlatSettled, batchRequestIndexing, canUseUrlInspection, classifyCanonicalDifference, classifyError, countDays, createAuth, createFetch, decodeSiteId, deleteSite, deleteSitemap, discoverSitemap, encodeSiteId, err, exchangeAuthCodeResult, fetchSitemap, fetchSitemapUrls, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, findBestGscProperty, findExactGscProperty, formatGscPropertyCandidates, generateGscDateRange, getBackfillProgress, getDateRange, getFreshestGscDate, getIndexingEligibility, getIndexingMetadata, getLatestGscDate, getNextCheckAfter, getNextCheckPriority, getNextDate, getOldestGscDate, getPendingDates, getPreviousDate, getPstDate, getVerificationToken, getVerifiedSite, googleSearchConsole, groupIntoRanges, gscErrorToException, gscPropertyMatchesTarget, hasGoogleScope, hasGscReadScope, hasGscWriteScope, hasIndexingScope, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, inspectUrl, inspectUrlFlat, introspectAccessToken, introspectAccessTokenResult, isCanonicalMismatch, isErr, isFragmentUrl, isOk, isPermissionDeniedError, isValidGscDate, isVerifiedGscPermission, isVerifiedGscProperty, lifecycleErrorCodes, listVerifiedSites, matchGscSite, normalizeGscSiteUrl, normalizeRegistrationTarget, normalizeSiteUrl, normalizeUrl, ok, parseGoogleError, parseGrantedScopes, parseGscSiteUrl, pickBestGscProperty, propertyNextActions, propertyStatuses, querySourceModes, refreshAccessToken, requestIndexing, resolveVerificationTarget, rethrowAsGscApiError, revokeOAuthToken, revokeOAuthTokenResult, runSequentialBatch, siteUrlToVerificationSite, sitemapNextActions, sitemapStatuses, submitSitemap, toIsoDate, unmappedInspectionReasons, unverifySite, unwrapResult, urlMatchKey, verificationMethodsFor, verifySite };
@@ -0,0 +1,8 @@
1
+ import { DiscoverSitemapOptions, FetchSitemapUrlsOptions } from "./sitemap.mjs";
2
+ /**
3
+ * Keep the v1 root exports without making every `gscdump` import initialize
4
+ * Nuxt Sitemap. The parser remains isolated behind the `./sitemap` entry.
5
+ */
6
+ declare function discoverSitemap(site: string, options?: DiscoverSitemapOptions): Promise<string | null>;
7
+ declare function fetchSitemapUrls(sitemapUrl: string, options?: FetchSitemapUrlsOptions): Promise<string[]>;
8
+ export { discoverSitemap, fetchSitemapUrls };
@@ -0,0 +1,7 @@
1
+ async function discoverSitemap(site, options = {}) {
2
+ return (await import("./sitemap.mjs")).discoverSitemap(site, options);
3
+ }
4
+ async function fetchSitemapUrls(sitemapUrl, options = {}) {
5
+ return (await import("./sitemap.mjs")).fetchSitemapUrls(sitemapUrl, options);
6
+ }
7
+ export { discoverSitemap, fetchSitemapUrls };
@@ -1,26 +1,179 @@
1
- interface DiscoverSitemapOptions {
2
- /** User-Agent sent on the discovery requests. */
3
- userAgent?: string;
4
- /** AbortSignal threaded through fetches; defaults to a 10s timeout per call. */
1
+ interface SitemapUrlEntry {
2
+ loc: string;
3
+ lastmod?: string;
4
+ changefreq?: string;
5
+ priority?: number;
6
+ }
7
+ interface SitemapIndexEntry {
8
+ loc: string;
9
+ lastmod?: string;
10
+ }
11
+ interface SitemapDocumentMeta {
12
+ bytesRead: number;
13
+ complete: boolean;
14
+ hasBom: boolean;
15
+ hasXmlDeclaration: boolean;
16
+ namespace: string | null;
17
+ }
18
+ type SitemapDocumentResult = {
19
+ _tag: 'urlset';
20
+ entries: SitemapUrlEntry[];
21
+ meta: SitemapDocumentMeta;
22
+ } | {
23
+ _tag: 'index';
24
+ entries: SitemapIndexEntry[];
25
+ meta: SitemapDocumentMeta;
26
+ } | {
27
+ _tag: 'empty_body';
28
+ } | {
29
+ _tag: 'html';
30
+ metaRefreshUrl?: string;
31
+ } | {
32
+ _tag: 'byte_limit';
33
+ bytesRead: number;
34
+ maxBytes: number;
35
+ } | {
36
+ _tag: 'parse_error';
37
+ error: string;
38
+ } | {
39
+ _tag: 'unsupported_document';
40
+ root: string | null;
41
+ };
42
+ interface ParseSitemapDocumentOptions {
43
+ maxBytes?: number;
44
+ maxEntries?: number;
45
+ acceptUrl?: (entry: SitemapUrlEntry) => boolean;
46
+ }
47
+ type SitemapBody = string | Uint8Array | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>;
48
+ interface SitemapFetchOptions extends ParseSitemapDocumentOptions {
49
+ fetcher?: typeof fetch;
50
+ headers?: HeadersInit;
5
51
  signal?: AbortSignal;
52
+ timeoutMs?: number;
53
+ }
54
+ type SitemapFetchResult = {
55
+ _tag: 'ok';
56
+ url: string;
57
+ document: Extract<SitemapDocumentResult, {
58
+ _tag: 'urlset' | 'index';
59
+ }>;
60
+ } | {
61
+ _tag: 'not_found';
62
+ url: string;
63
+ status: 404 | 410;
64
+ } | {
65
+ _tag: 'http_error';
66
+ url: string;
67
+ status: number;
68
+ statusText: string;
69
+ } | {
70
+ _tag: 'network_error';
71
+ url: string;
72
+ error: string;
73
+ } | {
74
+ _tag: 'document_error';
75
+ url: string;
76
+ error: Exclude<SitemapDocumentResult, {
77
+ _tag: 'urlset' | 'index';
78
+ }>;
79
+ };
80
+ interface DiscoverSitemapOptions extends SitemapFetchOptions {
81
+ userAgent?: string;
82
+ }
83
+ interface SitemapDiscoveryFailure {
84
+ url: string;
85
+ kind: 'http' | 'network' | 'document';
86
+ detail: string;
87
+ }
88
+ type SitemapDiscoveryResult = {
89
+ _tag: 'found';
90
+ url: string;
91
+ source: 'common_path' | 'robots';
92
+ } | {
93
+ _tag: 'not_found';
94
+ } | {
95
+ _tag: 'incomplete';
96
+ failures: SitemapDiscoveryFailure[];
97
+ };
98
+ interface WalkSitemapsOptions extends SitemapFetchOptions {
99
+ maxDepth?: number;
100
+ maxDocuments?: number;
101
+ maxUrls?: number;
6
102
  }
7
- /**
8
- * Try to discover a sitemap for `domain` by checking robots.txt for a
9
- * `Sitemap:` directive, then a small set of common paths. Returns the first
10
- * URL that responds with a 2xx, or `null`.
11
- */
12
- declare function discoverSitemap(domain: string, options?: DiscoverSitemapOptions): Promise<string | null>;
13
103
  interface FetchSitemapUrlsOptions extends DiscoverSitemapOptions {
14
- /** Maximum nested sitemap-index depth to follow. Default 3. */
15
104
  maxDepth?: number;
16
- /** Stop after this many URLs (across all nested sitemaps). Default unlimited. */
17
105
  limit?: number;
18
106
  }
107
+ interface SitemapWalkFailure {
108
+ url: string;
109
+ depth: number;
110
+ error: Exclude<SitemapFetchResult, {
111
+ _tag: 'ok';
112
+ }>;
113
+ }
114
+ type SitemapWalkResult = {
115
+ _tag: 'ok';
116
+ entries: SitemapUrlEntry[];
117
+ documentsRead: number;
118
+ complete: boolean;
119
+ failures: SitemapWalkFailure[];
120
+ } | {
121
+ _tag: 'not_found';
122
+ } | {
123
+ _tag: 'error';
124
+ failures: SitemapWalkFailure[];
125
+ };
126
+ type SitemapIdentityResult = {
127
+ _tag: 'ok';
128
+ url: string;
129
+ } | {
130
+ _tag: 'invalid';
131
+ reason: 'empty' | 'invalid_url' | 'off_origin';
132
+ };
133
+ interface SitemapEvidenceRecord {
134
+ path: string;
135
+ lastDownloaded?: string | null;
136
+ fetchedAt?: number | null;
137
+ }
138
+ interface ScopedSitemapRecords<T extends SitemapEvidenceRecord> {
139
+ sitemaps: Array<T & {
140
+ path: string;
141
+ }>;
142
+ excludedCount: number;
143
+ duplicateCount: number;
144
+ }
145
+ /**
146
+ * Streaming sitemap parser backed by the same utility Nuxt Sitemap consumers
147
+ * previously used. Expected failures are tagged values.
148
+ * Entry limits produce a useful partial document with `meta.complete = false`;
149
+ * byte limits fail closed because the document boundary was not observed.
150
+ */
151
+ declare function parseSitemapDocument(body: SitemapBody, options?: ParseSitemapDocumentOptions): Promise<SitemapDocumentResult>;
152
+ declare function fetchSitemapDocument(url: string, options?: SitemapFetchOptions): Promise<SitemapFetchResult>;
153
+ declare function parseRobotsSitemapUrls(robots: string, baseUrl?: string): string[];
154
+ declare function canonicalSitemapIdentity(path: string, site?: string): SitemapIdentityResult;
155
+ declare function sameSitemapIdentity(left: string, right: string, site?: string): boolean;
156
+ declare function scopeSitemapRecords<T extends SitemapEvidenceRecord>(records: readonly T[], site?: string): ScopedSitemapRecords<T>;
157
+ /**
158
+ * Discover a real sitemap document. A 200 HTML shell is rejected, and
159
+ * transport failures remain distinguishable from an authoritative miss.
160
+ */
161
+ declare function discoverSitemapResult(site: string, options?: DiscoverSitemapOptions): Promise<SitemapDiscoveryResult>;
162
+ /**
163
+ * Compatibility surface retained at the package root. New callers that need
164
+ * explicit incomplete-discovery evidence should use `discoverSitemapResult`.
165
+ */
166
+ declare function discoverSitemap(site: string, options?: DiscoverSitemapOptions): Promise<string | null>;
167
+ /**
168
+ * Bounded sitemap and sitemap-index walk. Partial results are explicit through
169
+ * `complete` and `failures`; callers must not infer removals from an incomplete
170
+ * walk.
171
+ */
172
+ declare function walkSitemaps(roots: string | readonly string[], options?: WalkSitemapsOptions): Promise<SitemapWalkResult>;
19
173
  /**
20
- * Fetch a sitemap (or sitemap index) and return the list of `<loc>` URLs.
21
- * Sitemap-index files are followed up to `maxDepth` levels. Duplicates are
22
- * de-duplicated. The XML parser is regex-based — it handles the common
23
- * `<loc>https://...</loc>` shape but doesn't validate the schema.
174
+ * Compatibility surface retained at the package root. The shared parser and
175
+ * traversal own the work while the original string-array contract remains.
24
176
  */
25
177
  declare function fetchSitemapUrls(sitemapUrl: string, options?: FetchSitemapUrlsOptions): Promise<string[]>;
26
- export { DiscoverSitemapOptions, FetchSitemapUrlsOptions, discoverSitemap, fetchSitemapUrls };
178
+ declare function sitemapContentHash(entries: readonly Pick<SitemapUrlEntry, 'loc'>[]): Promise<string>;
179
+ export { DiscoverSitemapOptions, FetchSitemapUrlsOptions, ParseSitemapDocumentOptions, ScopedSitemapRecords, SitemapBody, SitemapDiscoveryFailure, SitemapDiscoveryResult, SitemapDocumentMeta, SitemapDocumentResult, SitemapEvidenceRecord, SitemapFetchOptions, SitemapFetchResult, SitemapIdentityResult, SitemapIndexEntry, SitemapUrlEntry, SitemapWalkFailure, SitemapWalkResult, WalkSitemapsOptions, canonicalSitemapIdentity, discoverSitemap, discoverSitemapResult, fetchSitemapDocument, fetchSitemapUrls, parseRobotsSitemapUrls, parseSitemapDocument, sameSitemapIdentity, scopeSitemapRecords, sitemapContentHash, walkSitemaps };
package/dist/sitemap.mjs CHANGED
@@ -1,70 +1,670 @@
1
- const FETCH_TIMEOUT_MS = 1e4;
2
- const COMMON_PATHS = ["/sitemap.xml", "/sitemap_index.xml"];
3
- const SITEMAP_DIRECTIVE_RE = /^Sitemap:\s*(\S+)/im;
4
- async function discoverSitemap(domain, options = {}) {
5
- const userAgent = options.userAgent ?? "gscdump sitemap fetcher";
6
- const baseUrl = `https://${domain}`;
7
- const signalFor = () => options.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS);
8
- const robotsRes = await fetch(`${baseUrl}/robots.txt`, {
9
- headers: { "User-Agent": userAgent },
10
- signal: signalFor()
11
- }).catch(() => null);
12
- if (robotsRes?.ok) {
13
- const match = (await robotsRes.text()).match(SITEMAP_DIRECTIVE_RE);
14
- if (match?.[1]) {
15
- if ((await fetch(match[1], {
16
- method: "HEAD",
17
- signal: signalFor()
18
- }).catch(() => null))?.ok) return match[1];
19
- }
20
- }
21
- for (const path of COMMON_PATHS) {
22
- const url = `${baseUrl}${path}`;
23
- if ((await fetch(url, {
24
- method: "HEAD",
25
- headers: { "User-Agent": userAgent },
26
- signal: signalFor()
27
- }).catch(() => null))?.ok) return url;
28
- }
29
- return null;
30
- }
31
- const LOC_RE = /<loc>([^<]+)<\/loc>/gi;
32
- const SITEMAPINDEX_RE = /<sitemapindex\b/i;
33
- async function fetchSitemapUrls(sitemapUrl, options = {}) {
34
- const userAgent = options.userAgent ?? "gscdump sitemap fetcher";
35
- const maxDepth = options.maxDepth ?? 3;
36
- const limit = options.limit;
37
- const signalFor = () => options.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS);
1
+ import { urlMatchKey } from "./url.mjs";
2
+ import { parseSitemapStream } from "@nuxtjs/sitemap/utils";
3
+ const DEFAULT_FETCH_TIMEOUT_MS = 1e4;
4
+ const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
5
+ const DEFAULT_MAX_ENTRIES = 5e4;
6
+ const DEFAULT_MAX_DEPTH = 3;
7
+ const DEFAULT_MAX_DOCUMENTS = 100;
8
+ const SNIFF_BYTES = 65536;
9
+ const COMMON_SITEMAP_PATHS = [
10
+ "/sitemap.xml",
11
+ "/sitemap_index.xml",
12
+ "/sitemap-index.xml",
13
+ "/sitemaps.xml"
14
+ ];
15
+ function errorMessage(error) {
16
+ return error instanceof Error ? error.message : String(error);
17
+ }
18
+ function looksLikeHtml(prefix) {
19
+ const start = prefix.trimStart().replace(/^\uFEFF/, "").replace(/^<\?xml[\s\S]*?\?>\s*/i, "").replace(/^(?:<!--[\s\S]*?-->\s*)+/, "");
20
+ return /^<!doctype\s+html\b|^<html\b/i.test(start);
21
+ }
22
+ function extractMetaRefreshUrl(html) {
23
+ return /content=["']?\d+;\s*url=([^"'\s>]+)/i.exec(html)?.[1];
24
+ }
25
+ function isAsyncIterable(value) {
26
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
27
+ }
28
+ async function* readableStreamChunks(stream) {
29
+ const reader = stream.getReader();
30
+ let done = false;
31
+ try {
32
+ while (!done) {
33
+ const next = await reader.read();
34
+ done = next.done;
35
+ if (next.value) yield next.value;
36
+ }
37
+ } finally {
38
+ if (!done) await reader.cancel("sitemap reader stopped before the response body completed");
39
+ }
40
+ }
41
+ async function* bodyChunks(body) {
42
+ if (typeof body === "string") {
43
+ yield new TextEncoder().encode(body);
44
+ return;
45
+ }
46
+ if (body instanceof Uint8Array) {
47
+ yield body;
48
+ return;
49
+ }
50
+ if (isAsyncIterable(body)) {
51
+ yield* body;
52
+ return;
53
+ }
54
+ yield* readableStreamChunks(body);
55
+ }
56
+ function replayStream(reader, initial) {
57
+ let offset = 0;
58
+ return new ReadableStream({
59
+ async pull(controller) {
60
+ if (offset < initial.length) {
61
+ controller.enqueue(initial[offset++]);
62
+ return;
63
+ }
64
+ const next = await reader.read();
65
+ if (next.done) {
66
+ controller.close();
67
+ return;
68
+ }
69
+ if (next.value) controller.enqueue(next.value);
70
+ },
71
+ async cancel(reason) {
72
+ await reader.cancel(reason);
73
+ }
74
+ });
75
+ }
76
+ async function bodyWithDetectedCompression(body) {
77
+ const reader = body.getReader();
78
+ const initial = [];
79
+ let prefixLength = 0;
80
+ while (prefixLength < 2) {
81
+ const next = await reader.read();
82
+ if (next.done) break;
83
+ if (next.value) {
84
+ initial.push(next.value);
85
+ prefixLength += next.value.byteLength;
86
+ }
87
+ }
88
+ const prefix = initial.length === 1 ? initial[0] : Uint8Array.from(initial.flatMap((chunk) => [...chunk]));
89
+ const replay = replayStream(reader, initial);
90
+ if (!(prefix.length >= 2 && prefix[0] === 31 && prefix[1] === 139)) return replay;
91
+ if (typeof DecompressionStream === "undefined") {
92
+ await replay.cancel("gzip decompression is unavailable in this runtime");
93
+ throw new Error("gzip sitemap body requires DecompressionStream support");
94
+ }
95
+ return replay.pipeThrough(new DecompressionStream("gzip"));
96
+ }
97
+ function sitemapInputBoundaryError(bytesRead, maxBytes) {
98
+ return Object.assign(/* @__PURE__ */ new Error(`Sitemap exceeds ${maxBytes} bytes`), {
99
+ _tag: "byte_limit",
100
+ bytesRead,
101
+ maxBytes
102
+ });
103
+ }
104
+ function isSitemapInputBoundaryError(error) {
105
+ return error !== null && typeof error === "object" && "_tag" in error && error._tag === "byte_limit";
106
+ }
107
+ async function* replaySitemapInput(initial, iterator, stats, maxBytes) {
108
+ let done = false;
109
+ try {
110
+ yield* initial;
111
+ while (!done) {
112
+ const next = await iterator.next();
113
+ done = Boolean(next.done);
114
+ if (!next.value) continue;
115
+ const bytesRead = stats.bytesRead + next.value.byteLength;
116
+ if (bytesRead > maxBytes) throw sitemapInputBoundaryError(bytesRead, maxBytes);
117
+ stats.bytesRead = bytesRead;
118
+ yield next.value;
119
+ }
120
+ } finally {
121
+ if (!done) await iterator.return?.(void 0);
122
+ }
123
+ }
124
+ async function prepareSitemapInput(body, maxBytes) {
125
+ const iterator = bodyChunks(body)[Symbol.asyncIterator]();
126
+ const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
127
+ const initial = [];
128
+ const stats = {
129
+ bytesRead: 0,
130
+ prefix: "",
131
+ hasContent: false,
132
+ hasBom: false
133
+ };
134
+ let done = false;
135
+ while (stats.prefix.length < SNIFF_BYTES && !done) {
136
+ const next = await iterator.next();
137
+ done = Boolean(next.done);
138
+ if (!next.value) continue;
139
+ const bytesRead = stats.bytesRead + next.value.byteLength;
140
+ if (bytesRead > maxBytes) {
141
+ await iterator.return?.(void 0);
142
+ return {
143
+ _tag: "byte_limit",
144
+ bytesRead,
145
+ maxBytes
146
+ };
147
+ }
148
+ initial.push(next.value);
149
+ stats.bytesRead = bytesRead;
150
+ const text = decoder.decode(next.value, { stream: true });
151
+ stats.prefix += text.slice(0, SNIFF_BYTES - stats.prefix.length);
152
+ }
153
+ if (done && stats.prefix.length < SNIFF_BYTES) stats.prefix += decoder.decode();
154
+ stats.hasContent = Boolean(stats.prefix.trim());
155
+ stats.hasBom = stats.prefix.charCodeAt(0) === 65279;
156
+ if (!stats.hasContent) {
157
+ await iterator.return?.(void 0);
158
+ return { _tag: "empty_body" };
159
+ }
160
+ if (looksLikeHtml(stats.prefix)) {
161
+ await iterator.return?.(void 0);
162
+ const metaRefreshUrl = extractMetaRefreshUrl(stats.prefix);
163
+ return {
164
+ _tag: "html",
165
+ ...metaRefreshUrl ? { metaRefreshUrl } : {}
166
+ };
167
+ }
168
+ return {
169
+ _tag: "ready",
170
+ input: replaySitemapInput(initial, iterator, stats, maxBytes),
171
+ stats
172
+ };
173
+ }
174
+ function normalizeSitemapUrlInput(input) {
175
+ if (typeof input === "string") return { loc: input };
176
+ if (!input.loc) return null;
177
+ const lastmod = input.lastmod instanceof Date ? input.lastmod.toISOString() : input.lastmod;
178
+ return {
179
+ loc: input.loc,
180
+ ...lastmod ? { lastmod } : {},
181
+ ...input.changefreq ? { changefreq: input.changefreq } : {},
182
+ ...input.priority !== void 0 ? { priority: input.priority } : {}
183
+ };
184
+ }
185
+ function rootMetadata(prefix) {
186
+ const rootMatch = /<(?:[\w.-]+:)?(urlset|sitemapindex)\b([^>]*)>/i.exec(prefix);
187
+ if (!rootMatch) return {
188
+ root: /<(?:[\w.-]+:)?([\w.-]+)\b/.exec(prefix)?.[1]?.toLowerCase() ?? null,
189
+ namespace: null
190
+ };
191
+ const attributes = rootMatch[2] ?? "";
192
+ const namespace = /\sxmlns(?::[\w.-]+)?=(["'])(.*?)\1/i.exec(attributes)?.[2] ?? null;
193
+ return {
194
+ root: rootMatch[1].toLowerCase(),
195
+ namespace
196
+ };
197
+ }
198
+ async function parseSitemapDocument(body, options = {}) {
199
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
200
+ const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
201
+ const prepared = await prepareSitemapInput(body, maxBytes);
202
+ if (prepared._tag !== "ready") return prepared;
203
+ let root = null;
204
+ let complete = true;
205
+ const urls = [];
206
+ const sitemaps = [];
207
+ try {
208
+ for await (const event of parseSitemapStream(prepared.input)) {
209
+ if (event._tag === "kind") {
210
+ root = event.kind === "index" ? "sitemapindex" : "urlset";
211
+ continue;
212
+ }
213
+ if (event._tag === "warning") continue;
214
+ if (event._tag === "url") {
215
+ const entry = normalizeSitemapUrlInput(event.url);
216
+ if (!entry || options.acceptUrl && !options.acceptUrl(entry)) continue;
217
+ if (urls.length >= maxEntries) {
218
+ complete = false;
219
+ break;
220
+ }
221
+ urls.push(entry);
222
+ } else if (event._tag === "sitemap") {
223
+ if (sitemaps.length >= maxEntries) {
224
+ complete = false;
225
+ break;
226
+ }
227
+ sitemaps.push(event.sitemap);
228
+ }
229
+ if (!complete) break;
230
+ }
231
+ } catch (error) {
232
+ if (isSitemapInputBoundaryError(error)) return error;
233
+ const metadata = rootMetadata(prepared.stats.prefix);
234
+ if (metadata.root && metadata.root !== "urlset" && metadata.root !== "sitemapindex") return {
235
+ _tag: "unsupported_document",
236
+ root: metadata.root
237
+ };
238
+ return {
239
+ _tag: "parse_error",
240
+ error: `Not a valid sitemap: ${errorMessage(error)}`
241
+ };
242
+ }
243
+ const metadata = rootMetadata(prepared.stats.prefix);
244
+ const meta = {
245
+ bytesRead: prepared.stats.bytesRead,
246
+ complete,
247
+ hasBom: prepared.stats.hasBom,
248
+ hasXmlDeclaration: /^\s*<\?xml\b/i.test(prepared.stats.prefix),
249
+ namespace: metadata.namespace
250
+ };
251
+ if (root === "urlset") return {
252
+ _tag: "urlset",
253
+ entries: urls,
254
+ meta
255
+ };
256
+ if (root === "sitemapindex") return {
257
+ _tag: "index",
258
+ entries: sitemaps,
259
+ meta
260
+ };
261
+ return {
262
+ _tag: "unsupported_document",
263
+ root
264
+ };
265
+ }
266
+ function requestSignal(signal, timeoutMs) {
267
+ if (signal) return signal;
268
+ return AbortSignal.timeout(timeoutMs);
269
+ }
270
+ async function fetchSitemapDocument(url, options = {}) {
271
+ const fetcher = options.fetcher ?? globalThis.fetch;
272
+ let response;
273
+ try {
274
+ response = await fetcher(url, {
275
+ headers: {
276
+ accept: "application/xml,text/xml,application/rss+xml,*/*;q=0.1",
277
+ ...options.headers
278
+ },
279
+ redirect: "follow",
280
+ signal: requestSignal(options.signal, options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS)
281
+ });
282
+ } catch (error) {
283
+ return {
284
+ _tag: "network_error",
285
+ url,
286
+ error: errorMessage(error)
287
+ };
288
+ }
289
+ if (response.status === 404 || response.status === 410) return {
290
+ _tag: "not_found",
291
+ url,
292
+ status: response.status
293
+ };
294
+ if (!response.ok) return {
295
+ _tag: "http_error",
296
+ url,
297
+ status: response.status,
298
+ statusText: response.statusText
299
+ };
300
+ if (!response.body) return {
301
+ _tag: "document_error",
302
+ url,
303
+ error: { _tag: "empty_body" }
304
+ };
305
+ let body;
306
+ try {
307
+ body = await bodyWithDetectedCompression(response.body);
308
+ } catch (error) {
309
+ return {
310
+ _tag: "document_error",
311
+ url,
312
+ error: {
313
+ _tag: "parse_error",
314
+ error: errorMessage(error)
315
+ }
316
+ };
317
+ }
318
+ const document = await parseSitemapDocument(body, options);
319
+ if (document._tag === "urlset" || document._tag === "index") return {
320
+ _tag: "ok",
321
+ url: response.url || url,
322
+ document
323
+ };
324
+ return {
325
+ _tag: "document_error",
326
+ url,
327
+ error: document
328
+ };
329
+ }
330
+ function parseRobotsSitemapUrls(robots, baseUrl) {
331
+ const urls = [];
38
332
  const seen = /* @__PURE__ */ new Set();
39
- const out = [];
40
- const visit = async (url, depth) => {
41
- if (limit != null && out.length >= limit) return;
42
- if (depth > maxDepth) return;
43
- const res = await fetch(url, {
44
- headers: { "User-Agent": userAgent },
45
- signal: signalFor()
333
+ for (const match of robots.matchAll(/^\s*sitemap:\s*(\S+)/gim)) {
334
+ const raw = match[1];
335
+ if (!raw) continue;
336
+ let value = raw;
337
+ if (baseUrl) try {
338
+ value = new URL(raw, baseUrl).toString();
339
+ } catch {
340
+ continue;
341
+ }
342
+ if (!seen.has(value)) {
343
+ seen.add(value);
344
+ urls.push(value);
345
+ }
346
+ }
347
+ return urls;
348
+ }
349
+ function normalizeSiteOrigin(input) {
350
+ const value = input.trim().replace(/^sc-domain:/i, "");
351
+ try {
352
+ const url = new URL(/^https?:\/\//i.test(value) ? value : `https://${value}`);
353
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
354
+ return url.origin;
355
+ } catch {
356
+ return null;
357
+ }
358
+ }
359
+ function canonicalSitemapIdentity(path, site = "") {
360
+ const value = path.trim();
361
+ if (!value) return {
362
+ _tag: "invalid",
363
+ reason: "empty"
364
+ };
365
+ const siteOrigin = normalizeSiteOrigin(site);
366
+ let parsed;
367
+ try {
368
+ parsed = new URL(value, siteOrigin ? `${siteOrigin}/` : void 0);
369
+ } catch {
370
+ return {
371
+ _tag: "invalid",
372
+ reason: "invalid_url"
373
+ };
374
+ }
375
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return {
376
+ _tag: "invalid",
377
+ reason: "invalid_url"
378
+ };
379
+ if (siteOrigin && parsed.origin !== siteOrigin) return {
380
+ _tag: "invalid",
381
+ reason: "off_origin"
382
+ };
383
+ parsed.hash = "";
384
+ return {
385
+ _tag: "ok",
386
+ url: `${parsed.origin}${parsed.pathname}${parsed.search}`
387
+ };
388
+ }
389
+ function sameSitemapIdentity(left, right, site = "") {
390
+ const leftIdentity = canonicalSitemapIdentity(left, site);
391
+ if (leftIdentity._tag !== "ok") return false;
392
+ const rightIdentity = canonicalSitemapIdentity(right, site);
393
+ return rightIdentity._tag === "ok" && leftIdentity.url === rightIdentity.url;
394
+ }
395
+ function evidenceTime(record) {
396
+ const downloadedAt = record.lastDownloaded ? Date.parse(record.lastDownloaded) : NaN;
397
+ if (Number.isFinite(downloadedAt)) return downloadedAt;
398
+ return record.fetchedAt ?? 0;
399
+ }
400
+ function scopeSitemapRecords(records, site = "") {
401
+ const byIdentity = /* @__PURE__ */ new Map();
402
+ let excludedCount = 0;
403
+ let duplicateCount = 0;
404
+ for (const record of records) {
405
+ const identity = canonicalSitemapIdentity(record.path, site);
406
+ if (identity._tag !== "ok") {
407
+ excludedCount++;
408
+ continue;
409
+ }
410
+ const normalized = {
411
+ ...record,
412
+ path: identity.url
413
+ };
414
+ const existing = byIdentity.get(identity.url);
415
+ if (!existing) {
416
+ byIdentity.set(identity.url, normalized);
417
+ continue;
418
+ }
419
+ duplicateCount++;
420
+ if (evidenceTime(normalized) >= evidenceTime(existing)) byIdentity.set(identity.url, normalized);
421
+ }
422
+ return {
423
+ sitemaps: [...byIdentity.values()],
424
+ excludedCount,
425
+ duplicateCount
426
+ };
427
+ }
428
+ function discoveryFailure(result) {
429
+ if (result._tag === "network_error") return {
430
+ url: result.url,
431
+ kind: "network",
432
+ detail: result.error
433
+ };
434
+ if (result._tag === "http_error") return {
435
+ url: result.url,
436
+ kind: "http",
437
+ detail: `HTTP ${result.status}: ${result.statusText}`
438
+ };
439
+ return {
440
+ url: result.url,
441
+ kind: "document",
442
+ detail: result.error._tag
443
+ };
444
+ }
445
+ async function readRobots(origin, options) {
446
+ const url = `${origin}/robots.txt`;
447
+ const fetcher = options.fetcher ?? globalThis.fetch;
448
+ let response;
449
+ try {
450
+ response = await fetcher(url, {
451
+ headers: { "user-agent": options.userAgent ?? "gscdump sitemap fetcher" },
452
+ signal: requestSignal(options.signal, options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS)
453
+ });
454
+ } catch (error) {
455
+ return {
456
+ urls: [],
457
+ failure: {
458
+ url,
459
+ kind: "network",
460
+ detail: errorMessage(error)
461
+ }
462
+ };
463
+ }
464
+ if (response.status === 404 || response.status === 410) return { urls: [] };
465
+ if (!response.ok) return {
466
+ urls: [],
467
+ failure: {
468
+ url,
469
+ kind: "http",
470
+ detail: `HTTP ${response.status}: ${response.statusText}`
471
+ }
472
+ };
473
+ const maxBytes = Math.min(options.maxBytes ?? DEFAULT_MAX_BYTES, 1024 * 1024);
474
+ const body = response.body;
475
+ if (!body) return { urls: [] };
476
+ const decoder = new TextDecoder();
477
+ let bytes = 0;
478
+ let text = "";
479
+ try {
480
+ for await (const chunk of readableStreamChunks(body)) {
481
+ bytes += chunk.byteLength;
482
+ if (bytes > maxBytes) return {
483
+ urls: [],
484
+ failure: {
485
+ url,
486
+ kind: "document",
487
+ detail: `robots.txt exceeds ${maxBytes} bytes`
488
+ }
489
+ };
490
+ text += decoder.decode(chunk, { stream: true });
491
+ }
492
+ text += decoder.decode();
493
+ } catch (error) {
494
+ return {
495
+ urls: [],
496
+ failure: {
497
+ url,
498
+ kind: "network",
499
+ detail: errorMessage(error)
500
+ }
501
+ };
502
+ }
503
+ return { urls: parseRobotsSitemapUrls(text, origin) };
504
+ }
505
+ async function discoverSitemapResult(site, options = {}) {
506
+ const origin = normalizeSiteOrigin(site);
507
+ if (!origin) return {
508
+ _tag: "incomplete",
509
+ failures: [{
510
+ url: site,
511
+ kind: "document",
512
+ detail: "invalid site URL"
513
+ }]
514
+ };
515
+ const headers = {
516
+ "user-agent": options.userAgent ?? "gscdump sitemap fetcher",
517
+ ...options.headers
518
+ };
519
+ const failures = [];
520
+ for (const path of COMMON_SITEMAP_PATHS) {
521
+ const result = await fetchSitemapDocument(`${origin}${path}`, {
522
+ ...options,
523
+ headers
524
+ });
525
+ if (result._tag === "ok") return {
526
+ _tag: "found",
527
+ url: result.url,
528
+ source: "common_path"
529
+ };
530
+ if (result._tag !== "not_found" && !(result._tag === "document_error" && (result.error._tag === "html" || result.error._tag === "unsupported_document"))) failures.push(discoveryFailure(result));
531
+ }
532
+ const robots = await readRobots(origin, options);
533
+ if (robots.failure) failures.push(robots.failure);
534
+ for (const url of robots.urls) {
535
+ const result = await fetchSitemapDocument(url, {
536
+ ...options,
537
+ headers
46
538
  });
47
- if (!res.ok) throw new Error(`Fetch ${url} failed: ${res.status}`);
48
- const text = await res.text();
49
- if (SITEMAPINDEX_RE.test(text)) {
50
- for (const match of text.matchAll(LOC_RE)) {
51
- if (limit != null && out.length >= limit) return;
52
- const child = match[1].trim();
53
- if (!child) continue;
54
- await visit(child, depth + 1);
539
+ if (result._tag === "ok") return {
540
+ _tag: "found",
541
+ url: result.url,
542
+ source: "robots"
543
+ };
544
+ if (result._tag !== "not_found") failures.push(discoveryFailure(result));
545
+ }
546
+ return failures.length > 0 ? {
547
+ _tag: "incomplete",
548
+ failures
549
+ } : { _tag: "not_found" };
550
+ }
551
+ async function discoverSitemap(site, options = {}) {
552
+ const result = await discoverSitemapResult(site, options);
553
+ return result._tag === "found" ? result.url : null;
554
+ }
555
+ function walkFailure(url, depth, error) {
556
+ return {
557
+ url,
558
+ depth,
559
+ error
560
+ };
561
+ }
562
+ async function walkSitemaps(roots, options = {}) {
563
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
564
+ const maxDocuments = options.maxDocuments ?? DEFAULT_MAX_DOCUMENTS;
565
+ const maxUrls = options.maxUrls ?? DEFAULT_MAX_ENTRIES;
566
+ const queue = (typeof roots === "string" ? [roots] : [...roots]).map((url) => ({
567
+ url,
568
+ depth: 0
569
+ }));
570
+ const rootCount = queue.length;
571
+ const seenDocuments = /* @__PURE__ */ new Set();
572
+ const seenUrls = /* @__PURE__ */ new Set();
573
+ const entries = [];
574
+ const failures = [];
575
+ let documentsRead = 0;
576
+ let complete = true;
577
+ let loadedRoot = false;
578
+ let missingRoots = 0;
579
+ while (queue.length > 0) {
580
+ if (documentsRead >= maxDocuments) {
581
+ complete = false;
582
+ break;
583
+ }
584
+ const next = queue.shift();
585
+ if (seenDocuments.has(next.url)) continue;
586
+ seenDocuments.add(next.url);
587
+ const result = await fetchSitemapDocument(next.url, {
588
+ ...options,
589
+ maxEntries: Math.max(maxUrls - entries.length, 1)
590
+ });
591
+ if (result._tag !== "ok") {
592
+ if (next.depth === 0 && result._tag === "not_found") {
593
+ missingRoots++;
594
+ continue;
595
+ } else failures.push(walkFailure(next.url, next.depth, result));
596
+ complete = false;
597
+ continue;
598
+ }
599
+ if (next.depth === 0) loadedRoot = true;
600
+ documentsRead++;
601
+ complete &&= result.document.meta.complete;
602
+ if (result.document._tag === "index") {
603
+ if (next.depth >= maxDepth && result.document.entries.length > 0) {
604
+ complete = false;
605
+ continue;
606
+ }
607
+ for (const child of result.document.entries) queue.push({
608
+ url: child.loc,
609
+ depth: next.depth + 1
610
+ });
611
+ continue;
612
+ }
613
+ for (const entry of result.document.entries) {
614
+ if (seenUrls.has(entry.loc)) continue;
615
+ if (entries.length >= maxUrls) {
616
+ complete = false;
617
+ break;
55
618
  }
56
- return;
619
+ seenUrls.add(entry.loc);
620
+ entries.push(entry);
57
621
  }
58
- for (const match of text.matchAll(LOC_RE)) {
59
- const u = match[1].trim();
60
- if (!u) continue;
61
- if (seen.has(u)) continue;
62
- seen.add(u);
63
- out.push(u);
64
- if (limit != null && out.length >= limit) return;
622
+ if (entries.length >= maxUrls && queue.length > 0) {
623
+ complete = false;
624
+ break;
65
625
  }
626
+ }
627
+ if (!loadedRoot && missingRoots === rootCount && failures.length === 0) return { _tag: "not_found" };
628
+ if (!loadedRoot) return {
629
+ _tag: "error",
630
+ failures
631
+ };
632
+ return {
633
+ _tag: "ok",
634
+ entries,
635
+ documentsRead,
636
+ complete,
637
+ failures
66
638
  };
67
- await visit(sitemapUrl, 0);
68
- return out;
69
639
  }
70
- export { discoverSitemap, fetchSitemapUrls };
640
+ function legacyWalkFailureMessage(failure) {
641
+ const result = failure.error;
642
+ if (result._tag === "not_found" || result._tag === "http_error") return `Fetch ${failure.url} failed: ${result.status}`;
643
+ if (result._tag === "network_error") return result.error;
644
+ if (result.error._tag === "parse_error") return result.error.error;
645
+ return `Sitemap ${failure.url} failed: ${result.error._tag}`;
646
+ }
647
+ async function fetchSitemapUrls(sitemapUrl, options = {}) {
648
+ const { limit, maxDepth, userAgent, ...fetchOptions } = options;
649
+ const result = await walkSitemaps(sitemapUrl, {
650
+ ...fetchOptions,
651
+ headers: {
652
+ "user-agent": userAgent ?? "gscdump sitemap fetcher",
653
+ ...fetchOptions.headers
654
+ },
655
+ maxDepth,
656
+ maxDocuments: Number.MAX_SAFE_INTEGER,
657
+ maxUrls: limit ?? Number.MAX_SAFE_INTEGER
658
+ });
659
+ if (result._tag === "not_found") throw new Error(`Fetch ${sitemapUrl} failed: 404`);
660
+ if (result._tag === "error") throw new Error(result.failures[0] ? legacyWalkFailureMessage(result.failures[0]) : "Sitemap fetch failed");
661
+ if (result.failures[0]) throw new Error(legacyWalkFailureMessage(result.failures[0]));
662
+ return result.entries.map((entry) => entry.loc);
663
+ }
664
+ async function sitemapContentHash(entries) {
665
+ const normalized = entries.map((entry) => urlMatchKey(entry.loc)).filter((url) => Boolean(url)).sort();
666
+ const input = new TextEncoder().encode(normalized.join("\n"));
667
+ const hash = await crypto.subtle.digest("SHA-256", input);
668
+ return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("");
669
+ }
670
+ export { canonicalSitemapIdentity, discoverSitemap, discoverSitemapResult, fetchSitemapDocument, fetchSitemapUrls, parseRobotsSitemapUrls, parseSitemapDocument, sameSitemapIdentity, scopeSitemapRecords, sitemapContentHash, walkSitemaps };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gscdump",
3
3
  "type": "module",
4
- "version": "1.4.11",
4
+ "version": "1.5.1",
5
5
  "description": "Direct Google Search Console client with typed queries, streaming pagination, URL inspection, and indexing helpers",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -52,6 +52,11 @@
52
52
  "import": "./dist/core/result.mjs",
53
53
  "default": "./dist/core/result.mjs"
54
54
  },
55
+ "./sitemap": {
56
+ "types": "./dist/sitemap.d.mts",
57
+ "import": "./dist/sitemap.mjs",
58
+ "default": "./dist/sitemap.mjs"
59
+ },
55
60
  "./contracts": {
56
61
  "types": "./dist/contracts.d.mts",
57
62
  "import": "./dist/contracts.mjs",
@@ -82,8 +87,9 @@
82
87
  "node": ">=22"
83
88
  },
84
89
  "dependencies": {
90
+ "@nuxtjs/sitemap": "8.3.0",
85
91
  "ofetch": "^1.5.1",
86
- "@gscdump/contracts": "^1.4.11"
92
+ "@gscdump/contracts": "^1.5.1"
87
93
  },
88
94
  "scripts": {
89
95
  "dev": "obuild --stub",