@gscdump/devframe 0.0.0-stage → 5.7.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.
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ const DEFAULT_API_ROOT = "https://gscdump.com/api";
3
+ const cliAuthenticationSchema = z.union([
4
+ z.object({
5
+ _tag: z.literal("Hosted"),
6
+ apiRoot: z.string().url(),
7
+ sessionId: z.string().min(1)
8
+ }),
9
+ z.object({
10
+ _tag: z.literal("Hosted"),
11
+ apiRoot: z.string().url(),
12
+ apiKey: z.string().min(1)
13
+ }),
14
+ z.object({ _tag: z.literal("Local") })
15
+ ]);
16
+ function trimRoot(root) {
17
+ return root.replace(/\/+$/, "");
18
+ }
19
+ async function resolveCredential(input) {
20
+ const apiRoot = trimRoot(input.apiRoot ?? input.env.GSCDUMP_API_ROOT ?? DEFAULT_API_ROOT);
21
+ if (input.apiKey) return {
22
+ _tag: "ApiKey",
23
+ apiKey: input.apiKey,
24
+ apiRoot,
25
+ source: "option"
26
+ };
27
+ if (input.env.GSCDUMP_API_KEY) return {
28
+ _tag: "ApiKey",
29
+ apiKey: input.env.GSCDUMP_API_KEY,
30
+ apiRoot,
31
+ source: "env"
32
+ };
33
+ const saved = await input.readCliAuthentication();
34
+ if (saved == null) return null;
35
+ const parsed = cliAuthenticationSchema.safeParse(saved);
36
+ if (!parsed.success || parsed.data._tag === "Local") return null;
37
+ const root = trimRoot(parsed.data.apiRoot);
38
+ return "sessionId" in parsed.data ? {
39
+ _tag: "CliSession",
40
+ sessionId: parsed.data.sessionId,
41
+ apiRoot: root,
42
+ source: "cli-session"
43
+ } : {
44
+ _tag: "ApiKey",
45
+ apiKey: parsed.data.apiKey,
46
+ apiRoot: root,
47
+ source: "cli-session"
48
+ };
49
+ }
50
+ function bearerOf(credential) {
51
+ return credential._tag === "CliSession" ? credential.sessionId : credential.apiKey;
52
+ }
53
+ function cliHeadersOf(credential) {
54
+ return credential._tag === "CliSession" ? { "x-cli-session": credential.sessionId } : { "x-api-key": credential.apiKey };
55
+ }
56
+ function dashboardOrigin(apiRoot) {
57
+ return URL.parse(apiRoot)?.origin ?? "https://gscdump.com";
58
+ }
59
+ export { bearerOf, cliHeadersOf, dashboardOrigin, resolveCredential };
@@ -0,0 +1,18 @@
1
+ import { DailyPoint, DateWindow, GscdumpContext, MetricTotals, PageStats, PageStatsInput, Period, QueryRow, SiteSummary } from "./shared/protocol.mjs";
2
+ import { HostedSourceOptions } from "./sources/hosted.mjs";
3
+ import { DevframeDefinition } from "devframe";
4
+ export interface GscdumpDevframeOptions extends HostedSourceOptions {
5
+ /** The Site to read: a Site ID, a Site URL, or its host. Optional when the credential holds one Site. */
6
+ site?: string;
7
+ /** Override the request implementation, for proxies and tests. */
8
+ fetch?: typeof fetch;
9
+ }
10
+ /**
11
+ * The gscdump devframe: Search Console clicks, impressions, CTR, position, and
12
+ * top queries for the page in view, read from the gscdump Hosted record.
13
+ *
14
+ * Mount it in Vite DevTools with `@gscdump/devframe/vite`, or pass it to any
15
+ * devframe hub.
16
+ */
17
+ export declare function createGscdumpDevframe(options?: GscdumpDevframeOptions): DevframeDefinition;
18
+ export { type DailyPoint, type DateWindow, type GscdumpContext, type HostedSourceOptions, type MetricTotals, type PageStats, type PageStatsInput, type Period, type QueryRow, type SiteSummary, createGscdumpDevframe as default };
package/dist/index.mjs ADDED
@@ -0,0 +1,53 @@
1
+ import { description, homepage, name, version } from "./package.mjs";
2
+ import { DEVFRAME_ID } from "./shared/protocol.mjs";
3
+ import { createPageStatsReader } from "./reader.mjs";
4
+ import { createRpcFunctions } from "./rpc.mjs";
5
+ import { createHostedSource } from "./sources/hosted.mjs";
6
+ import { createRequire } from "node:module";
7
+ import { readFile } from "node:fs/promises";
8
+ import { homedir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+ import process from "node:process";
11
+ import { defineDevframe } from "devframe";
12
+ function packageDir() {
13
+ return dirname(createRequire(import.meta.url).resolve(`${name}/package.json`));
14
+ }
15
+ async function readCliAuthentication(env) {
16
+ const dir = env.GSCDUMP_CONFIG_DIR ?? join(homedir(), ".config", "gscdump");
17
+ const body = await readFile(join(dir, "authentication.json"), "utf8").catch((error) => {
18
+ if (error.code === "ENOENT") return null;
19
+ throw error;
20
+ });
21
+ return body === null ? null : JSON.parse(body);
22
+ }
23
+ function createGscdumpDevframe(options = {}) {
24
+ const dir = packageDir();
25
+ return defineDevframe({
26
+ id: DEVFRAME_ID,
27
+ name: "Search Console",
28
+ version,
29
+ packageName: name,
30
+ importMetaUrl: import.meta.url,
31
+ homepage,
32
+ description,
33
+ icon: "ph:chart-line-up-duotone",
34
+ clientAssets: join(dir, "dist/client"),
35
+ dock: {
36
+ category: "web",
37
+ clientScript: { importFrom: join(dir, "dist/client-script/index.mjs") }
38
+ },
39
+ capabilities: { build: false },
40
+ setup(ctx) {
41
+ const env = process.env;
42
+ const source = createHostedSource(options, {
43
+ fetch: options.fetch ?? globalThis.fetch,
44
+ env,
45
+ readCliAuthentication: () => readCliAuthentication(env)
46
+ });
47
+ const reader = createPageStatsReader({ site: options.site }, source, Date.now);
48
+ const scoped = ctx.scope(DEVFRAME_ID);
49
+ for (const fn of createRpcFunctions(reader)) scoped.rpc.register(fn);
50
+ }
51
+ });
52
+ }
53
+ export { createGscdumpDevframe, createGscdumpDevframe as default };
@@ -0,0 +1,5 @@
1
+ var name = "@gscdump/devframe";
2
+ var version = "5.7.1";
3
+ var description = "Search Console clicks, impressions, and queries for the page you are developing, in Vite DevTools and any devframe hub.";
4
+ var homepage = "https://github.com/harlan-zw/gscdump/tree/main/packages/devframe#readme";
5
+ export { description, homepage, name, version };
@@ -0,0 +1,114 @@
1
+ const DAY_MS = 864e5;
2
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
3
+ function addDays(date, days) {
4
+ return new Date(Date.parse(`${date}T00:00:00Z`) + days * DAY_MS).toISOString().slice(0, 10);
5
+ }
6
+ function pagePathOf(input) {
7
+ const text = input.trim();
8
+ if (!text) return null;
9
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(text)) {
10
+ const url = URL.parse(text);
11
+ return url ? `${url.pathname}${url.search}` : null;
12
+ }
13
+ const path = text.split("#")[0];
14
+ return path.startsWith("/") ? path : `/${path}`;
15
+ }
16
+ function periodWindows(days, site) {
17
+ const { oldestDate, newestDate } = site;
18
+ if (!newestDate || !ISO_DATE.test(newestDate)) return null;
19
+ const oldest = oldestDate && ISO_DATE.test(oldestDate) ? oldestDate : null;
20
+ const start = addDays(newestDate, -(days - 1));
21
+ const current = {
22
+ start: oldest && start < oldest ? oldest : start,
23
+ end: newestDate
24
+ };
25
+ const previousEnd = addDays(start, -1);
26
+ const previousStart = addDays(previousEnd, -(days - 1));
27
+ return {
28
+ current,
29
+ previous: oldest && previousStart < oldest ? null : {
30
+ start: previousStart,
31
+ end: previousEnd
32
+ }
33
+ };
34
+ }
35
+ function num(value) {
36
+ const n = typeof value === "number" ? value : Number(value);
37
+ return Number.isFinite(n) ? n : 0;
38
+ }
39
+ function finish(sum) {
40
+ return {
41
+ clicks: sum.clicks,
42
+ impressions: sum.impressions,
43
+ ctr: sum.impressions > 0 ? sum.clicks / sum.impressions : 0,
44
+ position: sum.impressions > 0 ? sum.positionWeight / sum.impressions : null
45
+ };
46
+ }
47
+ function accumulate(sum, row) {
48
+ const impressions = num(row.impressions);
49
+ sum.clicks += num(row.clicks);
50
+ sum.impressions += impressions;
51
+ sum.positionWeight += num(row.position) * impressions;
52
+ }
53
+ function inWindow(date, window) {
54
+ return date >= window.start && date <= window.end;
55
+ }
56
+ function fillDays(window, byDate) {
57
+ const days = [];
58
+ for (let date = window.start; date <= window.end; date = addDays(date, 1)) days.push(byDate.get(date) ?? {
59
+ date,
60
+ clicks: 0,
61
+ impressions: 0,
62
+ ctr: 0,
63
+ position: null
64
+ });
65
+ return days;
66
+ }
67
+ function summariseDaily(rows, windows) {
68
+ const current = {
69
+ clicks: 0,
70
+ impressions: 0,
71
+ positionWeight: 0
72
+ };
73
+ const previous = {
74
+ clicks: 0,
75
+ impressions: 0,
76
+ positionWeight: 0
77
+ };
78
+ const byDate = /* @__PURE__ */ new Map();
79
+ for (const row of rows) {
80
+ const date = String(row.date ?? "");
81
+ const target = inWindow(date, windows.current) ? current : windows.previous && inWindow(date, windows.previous) ? previous : null;
82
+ if (!target) continue;
83
+ accumulate(target, row);
84
+ const day = {
85
+ clicks: 0,
86
+ impressions: 0,
87
+ positionWeight: 0
88
+ };
89
+ accumulate(day, row);
90
+ byDate.set(date, {
91
+ date,
92
+ ...finish(day)
93
+ });
94
+ }
95
+ return {
96
+ totals: finish(current),
97
+ previousTotals: windows.previous ? finish(previous) : null,
98
+ daily: fillDays(windows.current, byDate),
99
+ previousDaily: windows.previous ? fillDays(windows.previous, byDate) : null
100
+ };
101
+ }
102
+ function parseQueryRows(rows) {
103
+ return rows.filter((row) => typeof row.query === "string" && row.query.length > 0).map((row) => ({
104
+ query: String(row.query),
105
+ clicks: num(row.clicks),
106
+ impressions: num(row.impressions),
107
+ ctr: num(row.ctr),
108
+ position: row.position == null ? null : num(row.position)
109
+ })).sort((a, b) => b.clicks - a.clicks || b.impressions - a.impressions);
110
+ }
111
+ function dashboardPageUrl(origin, site, path) {
112
+ return `${origin}/app/sites/${encodeURIComponent(site.siteUrl)}/search-console/pages/${encodeURIComponent(path)}?siteId=${encodeURIComponent(site.siteId)}`;
113
+ }
114
+ export { addDays, dashboardPageUrl, pagePathOf, parseQueryRows, periodWindows, summariseDaily };
@@ -0,0 +1,215 @@
1
+ import { hostOf } from "./source.mjs";
2
+ import { matchSite } from "./account.mjs";
3
+ import { addDays, dashboardPageUrl, pagePathOf, parseQueryRows, periodWindows, summariseDaily } from "./page-stats.mjs";
4
+ import { PERIODS } from "./shared/protocol.mjs";
5
+ import { periodToDateRange } from "@gscdump/sdk/period";
6
+ const SITES_TTL_MS = 3e5;
7
+ const STATS_TTL_MS = 3e5;
8
+ const TOP_QUERIES = 25;
9
+ function periodDays(period) {
10
+ return PERIODS.find((option) => option.value === period)?.days ?? 28;
11
+ }
12
+ function contextForPage(sites, pageUrl, preferredSiteId) {
13
+ const host = hostOf(pageUrl) ?? pageUrl;
14
+ const matches = sites.filter((site) => site.host === host);
15
+ if (matches.length === 0) return {
16
+ _tag: "NoSiteForPage",
17
+ host
18
+ };
19
+ const pick = matches.find((site) => site.siteId === preferredSiteId) ?? (matches.length === 1 ? matches[0] : void 0);
20
+ return pick ? {
21
+ _tag: "Ready",
22
+ site: pick,
23
+ sites: matches,
24
+ configured: false
25
+ } : {
26
+ _tag: "SiteRequired",
27
+ sites: matches,
28
+ target: host
29
+ };
30
+ }
31
+ function createPageStatsReader(options, source, now) {
32
+ let sitesCache = null;
33
+ const stats = /* @__PURE__ */ new Map();
34
+ function readSites() {
35
+ if (sitesCache && now() - sitesCache.at < SITES_TTL_MS) return sitesCache.value;
36
+ const value = source.sites();
37
+ sitesCache = {
38
+ at: now(),
39
+ value
40
+ };
41
+ value.then((result) => {
42
+ if (result._tag === "Blocked" && sitesCache?.value === value) sitesCache = null;
43
+ }, () => {
44
+ if (sitesCache?.value === value) sitesCache = null;
45
+ });
46
+ return value;
47
+ }
48
+ async function context(preferredSiteId, pageUrl) {
49
+ const read = await readSites();
50
+ if (read._tag === "Blocked") return read.context;
51
+ const sites = read.sites;
52
+ if (sites.length === 0) return { _tag: "NoSites" };
53
+ if (options.site) {
54
+ const match = matchSite(sites, options.site);
55
+ if (match._tag === "Found") return {
56
+ _tag: "Ready",
57
+ site: match.site,
58
+ sites,
59
+ configured: true
60
+ };
61
+ const picked = preferredSiteId ? sites.find((site) => site.siteId === preferredSiteId) : void 0;
62
+ return picked ? {
63
+ _tag: "Ready",
64
+ site: picked,
65
+ sites,
66
+ configured: false
67
+ } : {
68
+ _tag: "SiteRequired",
69
+ sites,
70
+ target: options.site
71
+ };
72
+ }
73
+ if (options.siteFromPage && pageUrl) return contextForPage(sites, pageUrl, preferredSiteId ?? null);
74
+ const preferred = preferredSiteId ? sites.find((site) => site.siteId === preferredSiteId) : void 0;
75
+ if (preferred) return {
76
+ _tag: "Ready",
77
+ site: preferred,
78
+ sites,
79
+ configured: false
80
+ };
81
+ const match = matchSite(sites, null);
82
+ return match._tag === "Found" ? {
83
+ _tag: "Ready",
84
+ site: match.site,
85
+ sites,
86
+ configured: false
87
+ } : {
88
+ _tag: "SiteRequired",
89
+ sites,
90
+ target: null
91
+ };
92
+ }
93
+ async function siteForPage(input) {
94
+ if (!options.siteFromPage) return context(input.siteId);
95
+ const read = await readSites();
96
+ if (read._tag === "Blocked") return read.context;
97
+ const chosen = input.siteId ? read.sites.find((site) => site.siteId === input.siteId) : void 0;
98
+ if (chosen) return {
99
+ _tag: "Ready",
100
+ site: chosen,
101
+ sites: read.sites,
102
+ configured: false
103
+ };
104
+ return /^https?:\/\//i.test(input.page) ? contextForPage(read.sites, input.page, null) : {
105
+ _tag: "SiteRequired",
106
+ sites: read.sites,
107
+ target: null
108
+ };
109
+ }
110
+ function firstFailure(daily, queries) {
111
+ if (daily._tag !== "Ok") return daily;
112
+ return queries._tag !== "Ok" ? queries : null;
113
+ }
114
+ async function readPage(site, path, period, windows, retried) {
115
+ const span = {
116
+ start: windows.previous?.start ?? windows.current.start,
117
+ end: windows.current.end
118
+ };
119
+ const [daily, queries] = await Promise.all([source.dailyRows(site.siteId, path, span), source.queryRows(site.siteId, path, windows.current, TOP_QUERIES)]);
120
+ const failure = firstFailure(daily, queries);
121
+ if (!failure && daily._tag === "Ok" && queries._tag === "Ok") return {
122
+ _tag: "Ok",
123
+ siteId: site.siteId,
124
+ path,
125
+ period,
126
+ window: windows.current,
127
+ previousWindow: windows.previous,
128
+ ...summariseDaily(daily.rows, windows),
129
+ queries: parseQueryRows(queries.rows),
130
+ dashboardUrl: dashboardPageUrl(source.dashboardOrigin, site, path)
131
+ };
132
+ switch (failure?._tag) {
133
+ case "Refused": {
134
+ const refusal = failure.refusal;
135
+ if (refusal.reason === "range_not_synced" && !retried && refusal.missingStart > windows.current.start) {
136
+ const shifted = periodWindows(periodDays(period), {
137
+ oldestDate: site.oldestDate,
138
+ newestDate: addDays(refusal.missingStart, -1)
139
+ });
140
+ if (shifted) return readPage(site, path, period, shifted, true);
141
+ }
142
+ return refusal.reason === "record_not_ready" ? {
143
+ _tag: "NoData",
144
+ siteId: site.siteId,
145
+ message: "gscdump has not prepared this Site's record for reads yet. Try again in a few minutes."
146
+ } : {
147
+ _tag: "NoData",
148
+ siteId: site.siteId,
149
+ message: `The Site's record does not hold ${refusal.missingStart} to ${refusal.missingEnd} yet.`
150
+ };
151
+ }
152
+ case "RateLimited": return { _tag: "RateLimited" };
153
+ case "Unauthorized":
154
+ sitesCache = null;
155
+ return {
156
+ _tag: "SiteUnavailable",
157
+ context: failure.context
158
+ };
159
+ case "Failed": return {
160
+ _tag: "Failed",
161
+ message: failure.message,
162
+ requestId: failure.requestId
163
+ };
164
+ default: return {
165
+ _tag: "Failed",
166
+ message: "The read returned no rows and no error.",
167
+ requestId: null
168
+ };
169
+ }
170
+ }
171
+ async function pageStats(input) {
172
+ const path = pagePathOf(input.page);
173
+ if (!path) return {
174
+ _tag: "InvalidPage",
175
+ page: input.page
176
+ };
177
+ const period = input.period ?? "28d";
178
+ const resolved = await siteForPage(input);
179
+ if (resolved._tag !== "Ready") return {
180
+ _tag: "SiteUnavailable",
181
+ context: resolved
182
+ };
183
+ const site = resolved.site;
184
+ const key = `${site.siteId}|${period}|${path}`;
185
+ const hit = stats.get(key);
186
+ if (hit && now() - hit.at < STATS_TTL_MS) return hit.value;
187
+ const stableEnd = periodToDateRange(period, { now: new Date(now()) }).end;
188
+ const newestDate = site.newestDate && site.newestDate > stableEnd ? stableEnd : site.newestDate;
189
+ const windows = periodWindows(periodDays(period), {
190
+ oldestDate: site.oldestDate,
191
+ newestDate
192
+ });
193
+ if (!windows) return {
194
+ _tag: "NoData",
195
+ siteId: site.siteId,
196
+ message: "The Site's record holds no days yet. gscdump is still syncing it."
197
+ };
198
+ const value = readPage(site, path, period, windows, false);
199
+ stats.set(key, {
200
+ at: now(),
201
+ value
202
+ });
203
+ value.then((result) => {
204
+ if (result._tag !== "Ok" && stats.get(key)?.value === value) stats.delete(key);
205
+ }, () => {
206
+ if (stats.get(key)?.value === value) stats.delete(key);
207
+ });
208
+ return value;
209
+ }
210
+ return {
211
+ context,
212
+ pageStats
213
+ };
214
+ }
215
+ export { createPageStatsReader };
package/dist/rpc.mjs ADDED
@@ -0,0 +1,31 @@
1
+ import { defineRpcFunction } from "devframe";
2
+ import { z } from "zod";
3
+ const pageStatsArgs = z.object({
4
+ page: z.string().min(1).describe("A path such as `/blog/post`, or a full URL of a page on the Site."),
5
+ period: z.enum([
6
+ "7d",
7
+ "28d",
8
+ "3m"
9
+ ]).optional().describe("The window to read. Defaults to `28d`."),
10
+ siteId: z.string().optional().describe("The Site ID. Defaults to the Site the devtool reads.")
11
+ });
12
+ const pageStatsResult = z.custom();
13
+ function createRpcFunctions(reader) {
14
+ return [defineRpcFunction({
15
+ name: "get-context",
16
+ type: "query",
17
+ jsonSerializable: true,
18
+ handler: (preferredSiteId, pageUrl) => reader.context(preferredSiteId ?? null, pageUrl ?? null)
19
+ }), defineRpcFunction({
20
+ name: "get-page-stats",
21
+ type: "query",
22
+ args: [pageStatsArgs],
23
+ returns: pageStatsResult,
24
+ agent: {
25
+ title: "Search Console page stats",
26
+ description: "Read Google Search Console clicks, impressions, CTR, average position, the daily series, and the top queries for one page of the Site, from the gscdump Hosted record. Call it to check how a page performs in Google search before or after you change it."
27
+ },
28
+ handler: (input) => reader.pageStats(input)
29
+ })];
30
+ }
31
+ export { createRpcFunctions };
@@ -0,0 +1,106 @@
1
+ export type Period = '7d' | '28d' | '3m';
2
+ export interface SiteSummary {
3
+ siteId: string;
4
+ /** The Site as gscdump registered it, for example `example.com`. */
5
+ siteUrl: string;
6
+ /** The exact host the Site covers, for example `example.com`. A page on another host is not on this Site. */
7
+ host: string;
8
+ /** The first day the Hosted record holds, `YYYY-MM-DD`. */
9
+ oldestDate: string | null;
10
+ /** The last day the Hosted record holds, `YYYY-MM-DD`. */
11
+ newestDate: string | null;
12
+ }
13
+ /**
14
+ * What the panel can read, decided once per panel boot and again when the
15
+ * page's host changes. Each tag names the next step the panel shows.
16
+ */
17
+ export type GscdumpContext = {
18
+ _tag: 'Ready';
19
+ site: SiteSummary;
20
+ sites: SiteSummary[];
21
+ configured: boolean;
22
+ } | {
23
+ _tag: 'SiteRequired';
24
+ sites: SiteSummary[];
25
+ target: string | null;
26
+ } | {
27
+ _tag: 'NoSiteForPage';
28
+ host: string;
29
+ } | BlockedContext;
30
+ /** A context that stops before any Site is chosen: no credential, no Sites, or no service. */
31
+ export type BlockedContext = {
32
+ _tag: 'NoSites';
33
+ } | {
34
+ _tag: 'CredentialMissing';
35
+ } | {
36
+ _tag: 'CredentialRejected';
37
+ source: CredentialSource;
38
+ } | {
39
+ _tag: 'SignedOut';
40
+ signInUrl: string;
41
+ } | {
42
+ _tag: 'Unavailable';
43
+ message: string;
44
+ };
45
+ export type CredentialSource = 'option' | 'env' | 'cli-session';
46
+ export interface MetricTotals {
47
+ clicks: number;
48
+ impressions: number;
49
+ ctr: number;
50
+ /** Impression-weighted average position. `null` when the page had no impressions. */
51
+ position: number | null;
52
+ }
53
+ export interface DailyPoint extends MetricTotals {
54
+ date: string;
55
+ }
56
+ export interface QueryRow extends MetricTotals {
57
+ query: string;
58
+ }
59
+ export interface DateWindow {
60
+ start: string;
61
+ end: string;
62
+ }
63
+ export interface PageStatsInput {
64
+ /** A path such as `/blog/post`, or a full URL. Search Console stores pages by path. */
65
+ page: string;
66
+ period?: Period;
67
+ /** Defaults to the Site the context resolved. */
68
+ siteId?: string;
69
+ }
70
+ export type PageStats = {
71
+ _tag: 'Ok';
72
+ siteId: string;
73
+ path: string;
74
+ period: Period;
75
+ window: DateWindow;
76
+ /** `null` when the Hosted record does not hold the preceding window. */
77
+ previousWindow: DateWindow | null;
78
+ totals: MetricTotals;
79
+ previousTotals: MetricTotals | null;
80
+ /** One point per day of `window`, ascending. Days without data are zero. */
81
+ daily: DailyPoint[];
82
+ /** One point per day of `previousWindow`, ascending. */
83
+ previousDaily: DailyPoint[] | null;
84
+ /** The queries that earned the page impressions, most clicks first. */
85
+ queries: QueryRow[];
86
+ /** The page in the gscdump.com dashboard. */
87
+ dashboardUrl: string;
88
+ } | {
89
+ _tag: 'InvalidPage';
90
+ page: string;
91
+ } | {
92
+ _tag: 'SiteUnavailable';
93
+ context: Exclude<GscdumpContext, {
94
+ _tag: 'Ready';
95
+ }>;
96
+ } | {
97
+ _tag: 'NoData';
98
+ siteId: string;
99
+ message: string;
100
+ } | {
101
+ _tag: 'RateLimited';
102
+ } | {
103
+ _tag: 'Failed';
104
+ message: string;
105
+ requestId: string | null;
106
+ };
@@ -0,0 +1,19 @@
1
+ const DEVFRAME_ID = "gscdump";
2
+ const PERIODS = [
3
+ {
4
+ value: "7d",
5
+ label: "Last 7 days",
6
+ days: 7
7
+ },
8
+ {
9
+ value: "28d",
10
+ label: "Last 28 days",
11
+ days: 28
12
+ },
13
+ {
14
+ value: "3m",
15
+ label: "Last 3 months",
16
+ days: 90
17
+ }
18
+ ];
19
+ export { DEVFRAME_ID, PERIODS };
@@ -0,0 +1 @@
1
+ import "./shared/protocol.mjs";
@@ -0,0 +1,7 @@
1
+ function hostOf(value) {
2
+ const text = value.trim();
3
+ if (text.startsWith("sc-domain:")) return text.slice(10).toLowerCase().replace(/\.$/, "") || null;
4
+ const url = URL.parse(/^[a-z][a-z\d+.-]*:\/\//i.test(text) ? text : `https://${text}`);
5
+ return url?.hostname ? url.hostname.toLowerCase() : null;
6
+ }
7
+ export { hostOf };
@@ -0,0 +1,7 @@
1
+ import "../source.mjs";
2
+ export interface HostedSourceOptions {
3
+ /** A gscdump user API key. Defaults to `GSCDUMP_API_KEY`, then the CLI's Hosted login. */
4
+ apiKey?: string;
5
+ /** Defaults to `GSCDUMP_API_ROOT`, then `https://gscdump.com/api`. */
6
+ apiRoot?: string;
7
+ }