@ascribed/astro 0.3.2-next.22 → 0.3.2

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/records.d.ts CHANGED
@@ -3,11 +3,11 @@ export type Listing = "listed" | "unquoted" | "unlisted";
3
3
  /**
4
4
  * Where a page's listing comes from: `default` when nothing sets it, `page`
5
5
  * for the page's own, and for a rule of `[listing]` its level (`site`,
6
- * `folder`, or `type`), its folder or type, the rule as written, and its line
6
+ * `older-lines`, `folder`, or `type`), its folder or type, the rule as written, and its line
7
7
  * in `ascribe.toml`.
8
8
  */
9
9
  export interface ListingFrom {
10
- level: "default" | "page" | "site" | "folder" | "type";
10
+ level: "default" | "page" | "site" | "older-lines" | "folder" | "type";
11
11
  folder?: string;
12
12
  type?: string;
13
13
  rule?: string;
@@ -92,7 +92,7 @@ export declare function routeKey(route: string): string;
92
92
  export declare function routeAs(route: string, trailingSlash: "always" | "never" | "ignore"): string;
93
93
  /** What the sitemap is drawn from. */
94
94
  export interface SitemapSources {
95
- /** Every project's records. */
95
+ /** Every project's records, and each older line's of a versioned build. */
96
96
  projects: readonly PageRecord[][];
97
97
  /**
98
98
  * Whether a project's page has a route in the site: was built, or, in
@@ -108,9 +108,11 @@ export interface SitemapSources {
108
108
  }
109
109
  /**
110
110
  * The sitemap's addresses, absolute, sorted, each once: every listed page of
111
- * every project that has a route in the site, and every page of the site's
112
- * own, less the excluded routes. A project's page that isn't listed is left
113
- * out, and so is a page of the site's own at the same route as one.
111
+ * every project that has a route in the site and whose canonical address is
112
+ * its own, and every page of the site's own, less the excluded routes. A
113
+ * project's page that isn't listed is left out, and so is an older line's
114
+ * page that the current line has, and a page of the site's own at the same
115
+ * route as one.
114
116
  */
115
117
  export declare function sitemapAddresses(sources: SitemapSources): string[];
116
118
  /** A sitemap of `addresses`, with no dates. */
package/dist/records.js CHANGED
@@ -74,11 +74,23 @@ export function routeAs(route, trailingSlash) {
74
74
  return `${route}/`;
75
75
  return route;
76
76
  }
77
+ /**
78
+ * Whether a page's canonical address is its own: it has none, or it's at the
79
+ * page's route. An older line's page whose canonical address is the same
80
+ * page in the current line isn't.
81
+ */
82
+ function canonicalIsOwn(page) {
83
+ if (page.canonical === undefined)
84
+ return true;
85
+ return routeKey(new URL(page.canonical).pathname) === routeKey(page.route);
86
+ }
77
87
  /**
78
88
  * The sitemap's addresses, absolute, sorted, each once: every listed page of
79
- * every project that has a route in the site, and every page of the site's
80
- * own, less the excluded routes. A project's page that isn't listed is left
81
- * out, and so is a page of the site's own at the same route as one.
89
+ * every project that has a route in the site and whose canonical address is
90
+ * its own, and every page of the site's own, less the excluded routes. A
91
+ * project's page that isn't listed is left out, and so is an older line's
92
+ * page that the current line has, and a page of the site's own at the same
93
+ * route as one.
82
94
  */
83
95
  export function sitemapAddresses(sources) {
84
96
  const excluded = new Set(sources.excluded.map(routeKey));
@@ -88,7 +100,10 @@ export function sitemapAddresses(sources) {
88
100
  for (const page of pages) {
89
101
  const key = routeKey(page.route);
90
102
  records.add(key);
91
- if (page.listing === "unlisted" || excluded.has(key) || !sources.routed(page.route))
103
+ if (page.listing === "unlisted" ||
104
+ !canonicalIsOwn(page) ||
105
+ excluded.has(key) ||
106
+ !sources.routed(page.route))
92
107
  continue;
93
108
  if (!routes.has(key))
94
109
  routes.set(key, page.route);
@@ -1 +1 @@
1
- {"version":3,"file":"records.js","sourceRoot":"","sources":["../src/records.ts"],"names":[],"mappings":"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","sourcesContent":["// The page records: `_ascribe/pages.json` in a build's site output\n// (output-layout contract §1.3), what each page tells every reader that isn't\n// a person. The integration writes from them a page's head (`Head.astro`),\n// the site's one sitemap, and its one `robots.txt`, from every project's\n// records together, at the host's root.\nimport { readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The records' format version this reads. */\nconst VERSION = 1;\n\n/** How a page is treated: listed, listed and not to be quoted, or not listed. */\nexport type Listing = \"listed\" | \"unquoted\" | \"unlisted\";\n\n/**\n * Where a page's listing comes from: `default` when nothing sets it, `page`\n * for the page's own, and for a rule of `[listing]` its level (`site`,\n * `folder`, or `type`), its folder or type, the rule as written, and its line\n * in `ascribe.toml`.\n */\nexport interface ListingFrom {\n level: \"default\" | \"page\" | \"site\" | \"folder\" | \"type\";\n folder?: string;\n type?: string;\n rule?: string;\n line?: number;\n}\n\n/** One page's record, with the route it's keyed by. */\nexport interface PageRecord {\n /** The page's route, from the host's root. */\n route: string;\n /** The page's path from the content root, with `/`. */\n source: string;\n /** The page's title, as plain text. */\n title?: string;\n /** The page's description, as plain text on one line. */\n description?: string;\n /** The page's own address, absolute, when the project sets `[consumer] site`. */\n canonical?: string;\n listing: Listing;\n /** Where `listing` comes from: the page, a rule of `[listing]`, or neither. */\n listing_from: ListingFrom;\n /** The address of the page's Markdown version, from the host's root, when the build publishes one. */\n markdown?: string;\n /**\n * The page's place in the site, from the top down to the page itself: the\n * page at each of its route's folders that has one, then the page. Absent\n * in records an earlier Ascribe wrote.\n */\n breadcrumbs?: Step[];\n}\n\n/** One step of a page's breadcrumbs: a page's route, from the host's root, and its title. */\nexport interface Step {\n route: string;\n title: string;\n}\n\n/** What a kind of crawler is for: gathering training data, building an AI search index, or fetching at a person's request. */\ntype Purpose = \"training\" | \"search\" | \"on-request\";\n\n/** The group of `robots.txt` a disallowed purpose writes. */\nexport interface RobotsGroup {\n purpose: Purpose;\n /** Every token of the purpose the binary knows, each a `User-agent` line. */\n user_agents: string[];\n /** The group's rules: `Disallow: /`. */\n lines: string[];\n}\n\n/** What a project says to crawlers. */\nexport interface Robots {\n /** The project's own lines, with paths from the host's root. */\n lines: string[];\n /** What the project says of each purpose it sets. */\n purposes?: Partial<Record<Purpose, \"allow\" | \"disallow\">>;\n /** A group for each purpose the project disallows. */\n groups?: RobotsGroup[];\n}\n\n/** `_ascribe/pages.json`. */\nexport interface PageRecords {\n version: number;\n site?: string;\n pages: Record<string, Omit<PageRecord, \"route\">>;\n robots: Robots;\n}\n\n/** What one project says to crawlers, by the name of its first entry. */\nexport interface ProjectRobots {\n name: string;\n robots: Robots;\n}\n\nfunction recordsFile(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"pages.json\");\n}\n\n/** The records in a build's site output, or `undefined` when there aren't any. */\nexport function readRecords(siteRoot: string): PageRecords | undefined {\n const file = recordsFile(siteRoot);\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return undefined;\n }\n const records = JSON.parse(text) as PageRecords;\n if (records.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(records.version)} of the page records, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return records;\n}\n\n/** A build's records by source path, as last read, and when its file was written. */\nconst bySource = new Map<string, { written: number; pages: Map<string, PageRecord> }>();\n\n/**\n * The record of the page whose source path is `source`, in the build whose\n * site output is at `siteRoot`, or `undefined` when it has none. Read again\n * when the file changes, as it does with each build in `astro dev`.\n */\nexport function recordFor(siteRoot: string, source: string): PageRecord | undefined {\n let written: number;\n try {\n written = statSync(recordsFile(siteRoot)).mtimeMs;\n } catch {\n return undefined;\n }\n let known = bySource.get(siteRoot);\n if (known === undefined || known.written !== written) {\n const pages = new Map<string, PageRecord>();\n for (const record of pageList(readRecords(siteRoot))) {\n if (!pages.has(record.source)) pages.set(record.source, record);\n }\n known = { written, pages };\n bySource.set(siteRoot, known);\n }\n return known.pages.get(source);\n}\n\n/** The records' pages, each with its route, in the file's order. */\nexport function pageList(records: PageRecords | undefined): PageRecord[] {\n return Object.entries(records?.pages ?? {}).map(([route, page]) => ({ route, ...page }));\n}\n\n/**\n * A route as the sitemap compares it: with a leading `/` and no trailing one,\n * except at the root, so `/docs/` and `/docs` are one page.\n */\nexport function routeKey(route: string): string {\n const withSlash = route.startsWith(\"/\") ? route : `/${route}`;\n return withSlash.length > 1 && withSlash.endsWith(\"/\") ? withSlash.slice(0, -1) : withSlash;\n}\n\n/** A route ending the way Astro's `trailingSlash` says, as a built page's address. */\nexport function routeAs(route: string, trailingSlash: \"always\" | \"never\" | \"ignore\"): string {\n if (trailingSlash === \"never\") return routeKey(route);\n if (trailingSlash === \"always\" && !route.endsWith(\"/\")) return `${route}/`;\n return route;\n}\n\n/** What the sitemap is drawn from. */\nexport interface SitemapSources {\n /** Every project's records. */\n projects: readonly PageRecord[][];\n /**\n * Whether a project's page has a route in the site: was built, or, in\n * `astro dev`, matches one of the site's routes.\n */\n routed: (route: string) => boolean;\n /** The site's own pages, each by its route from the host's root. */\n sitePages: readonly string[];\n /** Routes from the host's root to leave out: forwarding pages, the not-found page, and the site's list. */\n excluded: readonly string[];\n /** Astro's `site`. */\n site: string;\n}\n\n/**\n * The sitemap's addresses, absolute, sorted, each once: every listed page of\n * every project that has a route in the site, and every page of the site's\n * own, less the excluded routes. A project's page that isn't listed is left\n * out, and so is a page of the site's own at the same route as one.\n */\nexport function sitemapAddresses(sources: SitemapSources): string[] {\n const excluded = new Set(sources.excluded.map(routeKey));\n const records = new Set<string>();\n const routes = new Map<string, string>();\n for (const pages of sources.projects) {\n for (const page of pages) {\n const key = routeKey(page.route);\n records.add(key);\n if (page.listing === \"unlisted\" || excluded.has(key) || !sources.routed(page.route)) continue;\n if (!routes.has(key)) routes.set(key, page.route);\n }\n }\n for (const route of sources.sitePages) {\n const key = routeKey(route);\n if (records.has(key) || excluded.has(key) || routes.has(key)) continue;\n routes.set(key, route);\n }\n return [\n ...new Set([...routes.values()].map((route) => new URL(route, sources.site).href)),\n ].sort();\n}\n\nfunction escapeXml(text: string): string {\n return text\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"'\", \"&apos;\");\n}\n\n/** A sitemap of `addresses`, with no dates. */\nexport function sitemapXml(addresses: readonly string[]): string {\n const urls = addresses.map((address) => ` <url><loc>${escapeXml(address)}</loc></url>\\n`);\n return (\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n' +\n urls.join(\"\") +\n \"</urlset>\\n\"\n );\n}\n\nconst USER_AGENT = /^\\s*user-agent\\s*:/i;\n\n/**\n * A site's `robots.txt` from what each project says to crawlers: a group for\n * each purpose a project disallows, once for the site, then a\n * `User-agent: *` group with every project's lines before its first\n * `User-agent` line, which are for every crawler, then the groups each\n * project wrote whole, then the sitemap's address. `undefined` when it would\n * say nothing.\n */\nexport function robotsText(\n projects: readonly Robots[],\n sitemap: string | undefined,\n): string | undefined {\n const purposes = new Map<Purpose, string[]>();\n const everyone: string[] = [];\n const groups: string[][] = [];\n for (const robots of projects) {\n for (const group of robots.groups ?? []) {\n if (purposes.has(group.purpose)) continue;\n purposes.set(group.purpose, [\n ...group.user_agents.map((agent) => `User-agent: ${agent}`),\n ...group.lines,\n ]);\n }\n const lines = robots.lines;\n const first = lines.findIndex((line) => USER_AGENT.test(line));\n everyone.push(...(first === -1 ? lines : lines.slice(0, first)));\n if (first !== -1) groups.push(lines.slice(first));\n }\n const blocks: string[][] = [...purposes.values()];\n if (everyone.length > 0) blocks.push([\"User-agent: *\", ...everyone]);\n blocks.push(...groups);\n if (sitemap !== undefined) blocks.push([`Sitemap: ${sitemap}`]);\n if (blocks.length === 0) return undefined;\n return `${blocks.map((block) => block.join(\"\\n\")).join(\"\\n\\n\")}\\n`;\n}\n\n/**\n * Two projects that say contradictory things about one purpose, as\n * sentences: one allows it and the other disallows it. A site has one\n * `robots.txt`, so they must agree. A project that says nothing of a purpose\n * agrees with either.\n */\nexport function purposeConflicts(projects: readonly ProjectRobots[]): string[] {\n const problems: string[] = [];\n for (const purpose of [\"training\", \"search\", \"on-request\"] as const) {\n const allow = projects.find((p) => p.robots.purposes?.[purpose] === \"allow\");\n const disallow = projects.find((p) => p.robots.purposes?.[purpose] === \"disallow\");\n if (allow === undefined || disallow === undefined) continue;\n problems.push(\n `\"${disallow.name}\" has [crawlers] ${purpose} = \"disallow\" and \"${allow.name}\" has ${purpose} = \"allow\". The site has one robots.txt, so its projects must agree: make them the same, or remove one.`,\n );\n }\n return problems;\n}\n\n/** Where the sitemap is, from the host's root: at Astro's `base`. */\nexport function sitemapRoute(base: string): string {\n return `${normalizeBase(base)}sitemap.xml`;\n}\n"]}
1
+ {"version":3,"file":"records.js","sourceRoot":"","sources":["../src/records.ts"],"names":[],"mappings":"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","sourcesContent":["// The page records: `_ascribe/pages.json` in a build's site output\n// (output-layout contract §1.3), what each page tells every reader that isn't\n// a person. The integration writes from them a page's head (`Head.astro`),\n// the site's one sitemap, and its one `robots.txt`, from every project's\n// records together, at the host's root.\nimport { readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The records' format version this reads. */\nconst VERSION = 1;\n\n/** How a page is treated: listed, listed and not to be quoted, or not listed. */\nexport type Listing = \"listed\" | \"unquoted\" | \"unlisted\";\n\n/**\n * Where a page's listing comes from: `default` when nothing sets it, `page`\n * for the page's own, and for a rule of `[listing]` its level (`site`,\n * `older-lines`, `folder`, or `type`), its folder or type, the rule as written, and its line\n * in `ascribe.toml`.\n */\nexport interface ListingFrom {\n level: \"default\" | \"page\" | \"site\" | \"older-lines\" | \"folder\" | \"type\";\n folder?: string;\n type?: string;\n rule?: string;\n line?: number;\n}\n\n/** One page's record, with the route it's keyed by. */\nexport interface PageRecord {\n /** The page's route, from the host's root. */\n route: string;\n /** The page's path from the content root, with `/`. */\n source: string;\n /** The page's title, as plain text. */\n title?: string;\n /** The page's description, as plain text on one line. */\n description?: string;\n /** The page's own address, absolute, when the project sets `[consumer] site`. */\n canonical?: string;\n listing: Listing;\n /** Where `listing` comes from: the page, a rule of `[listing]`, or neither. */\n listing_from: ListingFrom;\n /** The address of the page's Markdown version, from the host's root, when the build publishes one. */\n markdown?: string;\n /**\n * The page's place in the site, from the top down to the page itself: the\n * page at each of its route's folders that has one, then the page. Absent\n * in records an earlier Ascribe wrote.\n */\n breadcrumbs?: Step[];\n}\n\n/** One step of a page's breadcrumbs: a page's route, from the host's root, and its title. */\nexport interface Step {\n route: string;\n title: string;\n}\n\n/** What a kind of crawler is for: gathering training data, building an AI search index, or fetching at a person's request. */\ntype Purpose = \"training\" | \"search\" | \"on-request\";\n\n/** The group of `robots.txt` a disallowed purpose writes. */\nexport interface RobotsGroup {\n purpose: Purpose;\n /** Every token of the purpose the binary knows, each a `User-agent` line. */\n user_agents: string[];\n /** The group's rules: `Disallow: /`. */\n lines: string[];\n}\n\n/** What a project says to crawlers. */\nexport interface Robots {\n /** The project's own lines, with paths from the host's root. */\n lines: string[];\n /** What the project says of each purpose it sets. */\n purposes?: Partial<Record<Purpose, \"allow\" | \"disallow\">>;\n /** A group for each purpose the project disallows. */\n groups?: RobotsGroup[];\n}\n\n/** `_ascribe/pages.json`. */\nexport interface PageRecords {\n version: number;\n site?: string;\n pages: Record<string, Omit<PageRecord, \"route\">>;\n robots: Robots;\n}\n\n/** What one project says to crawlers, by the name of its first entry. */\nexport interface ProjectRobots {\n name: string;\n robots: Robots;\n}\n\nfunction recordsFile(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"pages.json\");\n}\n\n/** The records in a build's site output, or `undefined` when there aren't any. */\nexport function readRecords(siteRoot: string): PageRecords | undefined {\n const file = recordsFile(siteRoot);\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return undefined;\n }\n const records = JSON.parse(text) as PageRecords;\n if (records.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(records.version)} of the page records, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return records;\n}\n\n/** A build's records by source path, as last read, and when its file was written. */\nconst bySource = new Map<string, { written: number; pages: Map<string, PageRecord> }>();\n\n/**\n * The record of the page whose source path is `source`, in the build whose\n * site output is at `siteRoot`, or `undefined` when it has none. Read again\n * when the file changes, as it does with each build in `astro dev`.\n */\nexport function recordFor(siteRoot: string, source: string): PageRecord | undefined {\n let written: number;\n try {\n written = statSync(recordsFile(siteRoot)).mtimeMs;\n } catch {\n return undefined;\n }\n let known = bySource.get(siteRoot);\n if (known === undefined || known.written !== written) {\n const pages = new Map<string, PageRecord>();\n for (const record of pageList(readRecords(siteRoot))) {\n if (!pages.has(record.source)) pages.set(record.source, record);\n }\n known = { written, pages };\n bySource.set(siteRoot, known);\n }\n return known.pages.get(source);\n}\n\n/** The records' pages, each with its route, in the file's order. */\nexport function pageList(records: PageRecords | undefined): PageRecord[] {\n return Object.entries(records?.pages ?? {}).map(([route, page]) => ({ route, ...page }));\n}\n\n/**\n * A route as the sitemap compares it: with a leading `/` and no trailing one,\n * except at the root, so `/docs/` and `/docs` are one page.\n */\nexport function routeKey(route: string): string {\n const withSlash = route.startsWith(\"/\") ? route : `/${route}`;\n return withSlash.length > 1 && withSlash.endsWith(\"/\") ? withSlash.slice(0, -1) : withSlash;\n}\n\n/** A route ending the way Astro's `trailingSlash` says, as a built page's address. */\nexport function routeAs(route: string, trailingSlash: \"always\" | \"never\" | \"ignore\"): string {\n if (trailingSlash === \"never\") return routeKey(route);\n if (trailingSlash === \"always\" && !route.endsWith(\"/\")) return `${route}/`;\n return route;\n}\n\n/** What the sitemap is drawn from. */\nexport interface SitemapSources {\n /** Every project's records, and each older line's of a versioned build. */\n projects: readonly PageRecord[][];\n /**\n * Whether a project's page has a route in the site: was built, or, in\n * `astro dev`, matches one of the site's routes.\n */\n routed: (route: string) => boolean;\n /** The site's own pages, each by its route from the host's root. */\n sitePages: readonly string[];\n /** Routes from the host's root to leave out: forwarding pages, the not-found page, and the site's list. */\n excluded: readonly string[];\n /** Astro's `site`. */\n site: string;\n}\n\n/**\n * Whether a page's canonical address is its own: it has none, or it's at the\n * page's route. An older line's page whose canonical address is the same\n * page in the current line isn't.\n */\nfunction canonicalIsOwn(page: PageRecord): boolean {\n if (page.canonical === undefined) return true;\n return routeKey(new URL(page.canonical).pathname) === routeKey(page.route);\n}\n\n/**\n * The sitemap's addresses, absolute, sorted, each once: every listed page of\n * every project that has a route in the site and whose canonical address is\n * its own, and every page of the site's own, less the excluded routes. A\n * project's page that isn't listed is left out, and so is an older line's\n * page that the current line has, and a page of the site's own at the same\n * route as one.\n */\nexport function sitemapAddresses(sources: SitemapSources): string[] {\n const excluded = new Set(sources.excluded.map(routeKey));\n const records = new Set<string>();\n const routes = new Map<string, string>();\n for (const pages of sources.projects) {\n for (const page of pages) {\n const key = routeKey(page.route);\n records.add(key);\n if (\n page.listing === \"unlisted\" ||\n !canonicalIsOwn(page) ||\n excluded.has(key) ||\n !sources.routed(page.route)\n )\n continue;\n if (!routes.has(key)) routes.set(key, page.route);\n }\n }\n for (const route of sources.sitePages) {\n const key = routeKey(route);\n if (records.has(key) || excluded.has(key) || routes.has(key)) continue;\n routes.set(key, route);\n }\n return [\n ...new Set([...routes.values()].map((route) => new URL(route, sources.site).href)),\n ].sort();\n}\n\nfunction escapeXml(text: string): string {\n return text\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"'\", \"&apos;\");\n}\n\n/** A sitemap of `addresses`, with no dates. */\nexport function sitemapXml(addresses: readonly string[]): string {\n const urls = addresses.map((address) => ` <url><loc>${escapeXml(address)}</loc></url>\\n`);\n return (\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n' +\n urls.join(\"\") +\n \"</urlset>\\n\"\n );\n}\n\nconst USER_AGENT = /^\\s*user-agent\\s*:/i;\n\n/**\n * A site's `robots.txt` from what each project says to crawlers: a group for\n * each purpose a project disallows, once for the site, then a\n * `User-agent: *` group with every project's lines before its first\n * `User-agent` line, which are for every crawler, then the groups each\n * project wrote whole, then the sitemap's address. `undefined` when it would\n * say nothing.\n */\nexport function robotsText(\n projects: readonly Robots[],\n sitemap: string | undefined,\n): string | undefined {\n const purposes = new Map<Purpose, string[]>();\n const everyone: string[] = [];\n const groups: string[][] = [];\n for (const robots of projects) {\n for (const group of robots.groups ?? []) {\n if (purposes.has(group.purpose)) continue;\n purposes.set(group.purpose, [\n ...group.user_agents.map((agent) => `User-agent: ${agent}`),\n ...group.lines,\n ]);\n }\n const lines = robots.lines;\n const first = lines.findIndex((line) => USER_AGENT.test(line));\n everyone.push(...(first === -1 ? lines : lines.slice(0, first)));\n if (first !== -1) groups.push(lines.slice(first));\n }\n const blocks: string[][] = [...purposes.values()];\n if (everyone.length > 0) blocks.push([\"User-agent: *\", ...everyone]);\n blocks.push(...groups);\n if (sitemap !== undefined) blocks.push([`Sitemap: ${sitemap}`]);\n if (blocks.length === 0) return undefined;\n return `${blocks.map((block) => block.join(\"\\n\")).join(\"\\n\\n\")}\\n`;\n}\n\n/**\n * Two projects that say contradictory things about one purpose, as\n * sentences: one allows it and the other disallows it. A site has one\n * `robots.txt`, so they must agree. A project that says nothing of a purpose\n * agrees with either.\n */\nexport function purposeConflicts(projects: readonly ProjectRobots[]): string[] {\n const problems: string[] = [];\n for (const purpose of [\"training\", \"search\", \"on-request\"] as const) {\n const allow = projects.find((p) => p.robots.purposes?.[purpose] === \"allow\");\n const disallow = projects.find((p) => p.robots.purposes?.[purpose] === \"disallow\");\n if (allow === undefined || disallow === undefined) continue;\n problems.push(\n `\"${disallow.name}\" has [crawlers] ${purpose} = \"disallow\" and \"${allow.name}\" has ${purpose} = \"allow\". The site has one robots.txt, so its projects must agree: make them the same, or remove one.`,\n );\n }\n return problems;\n}\n\n/** Where the sitemap is, from the host's root: at Astro's `base`. */\nexport function sitemapRoute(base: string): string {\n return `${normalizeBase(base)}sitemap.xml`;\n}\n"]}
@@ -2,7 +2,16 @@ import type { BaseInfo, PageDiff } from "./protocol.js";
2
2
  /** One build's changes against a base. */
3
3
  export interface DiffResult {
4
4
  base: BaseInfo;
5
+ /** The build's changed pages: the first build compared, a versioned build's current line. */
5
6
  pages: PageDiff[];
7
+ /**
8
+ * Each build compared, in order, with its changed pages, when more than one
9
+ * was: a versioned build's lines, the current one first.
10
+ */
11
+ lines?: {
12
+ build: string;
13
+ pages: PageDiff[];
14
+ }[];
6
15
  /** How many errors `ascribe check` finds in the working tree for the build. */
7
16
  errors: number;
8
17
  }
@@ -13,12 +22,13 @@ export interface DiffResult {
13
22
  export declare function runDiff(options: {
14
23
  binary: string;
15
24
  configPath: string;
16
- build: string;
25
+ /** The build, or a versioned build's lines, the current one first. */
26
+ build: string | readonly string[];
17
27
  cwd: string;
18
28
  base: string | undefined;
19
29
  }): Promise<DiffResult>;
20
- /** The build's part of `ascribe diff --format json`'s output. */
21
- export declare function parseDiff(json: string, build: string): DiffResult;
30
+ /** The build's part of `ascribe diff --format json`'s output, or each of the builds' parts. */
31
+ export declare function parseDiff(json: string, build: string | readonly string[]): DiffResult;
22
32
  /**
23
33
  * `ascribe diff --format prompt`'s prompt about a page, or a fragment's reach,
24
34
  * at a content path; `undefined` when it didn't change. Rejects with
@@ -27,7 +37,8 @@ export declare function parseDiff(json: string, build: string): DiffResult;
27
37
  export declare function runPrompt(options: {
28
38
  binary: string;
29
39
  configPath: string;
30
- build: string;
40
+ /** The build, or a versioned build's lines: the prompt is about the first the page changed in. */
41
+ build: string | readonly string[];
31
42
  cwd: string;
32
43
  base: string | undefined;
33
44
  path: string;
@@ -10,8 +10,7 @@ export function runDiff(options) {
10
10
  "diff",
11
11
  "--format",
12
12
  "json",
13
- "--build",
14
- options.build,
13
+ ...buildArgs(options.build),
15
14
  "--config",
16
15
  options.configPath,
17
16
  "--color",
@@ -35,16 +34,26 @@ export function runDiff(options) {
35
34
  });
36
35
  });
37
36
  }
38
- /** The build's part of `ascribe diff --format json`'s output. */
37
+ /** `--build` for each build. */
38
+ function buildArgs(build) {
39
+ return (typeof build === "string" ? [build] : build).flatMap((b) => ["--build", b]);
40
+ }
41
+ /** The build's part of `ascribe diff --format json`'s output, or each of the builds' parts. */
39
42
  export function parseDiff(json, build) {
40
43
  // Checked as far as telling a diff report from other JSON.
41
44
  const report = JSON.parse(json);
42
45
  if (report.base === undefined || !Array.isArray(report.builds)) {
43
46
  throw new Error("not a diff report");
44
47
  }
45
- const pages = report.builds.find((b) => b.build === build)?.pages ?? [];
48
+ const builds = typeof build === "string" ? [build] : build;
49
+ const reported = report.builds;
50
+ const pagesOf = (name) => reported.find((b) => b.build === name)?.pages ?? [];
51
+ const pages = pagesOf(builds[0] ?? "");
46
52
  const errors = typeof report.working_tree_errors === "number" ? report.working_tree_errors : 0;
47
- return { base: report.base, pages, errors };
53
+ if (builds.length < 2)
54
+ return { base: report.base, pages, errors };
55
+ const lines = builds.map((name) => ({ build: name, pages: pagesOf(name) }));
56
+ return { base: report.base, pages, errors, lines };
48
57
  }
49
58
  /**
50
59
  * `ascribe diff --format prompt`'s prompt about a page, or a fragment's reach,
@@ -56,8 +65,7 @@ export function runPrompt(options) {
56
65
  "diff",
57
66
  "--format",
58
67
  "prompt",
59
- "--build",
60
- options.build,
68
+ ...buildArgs(options.build),
61
69
  "--config",
62
70
  options.configPath,
63
71
  "--color",
@@ -1 +1 @@
1
- {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/review/diff.ts"],"names":[],"mappings":"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","sourcesContent":["// Running `ascribe diff` for the integration's build: its changes as JSON, and\n// its agent prompts.\nimport { execFile } from \"node:child_process\";\nimport type { DiffReport } from \"../shapes.js\";\nimport type { BaseInfo, PageDiff } from \"./protocol.js\";\n\n/** One build's changes against a base. */\nexport interface DiffResult {\n base: BaseInfo;\n pages: PageDiff[];\n /** How many errors `ascribe check` finds in the working tree for the build. */\n errors: number;\n}\n\n/**\n * Compares the build's pages with `base` (the default branch when it's\n * `undefined`). Rejects with `ascribe diff`'s reason as the message.\n */\nexport function runDiff(options: {\n binary: string;\n configPath: string;\n build: string;\n cwd: string;\n base: string | undefined;\n}): Promise<DiffResult> {\n const args = [\n \"diff\",\n \"--format\",\n \"json\",\n \"--build\",\n options.build,\n \"--config\",\n options.configPath,\n \"--color\",\n \"never\",\n ];\n if (options.base !== undefined) args.push(\"--base\", options.base);\n return new Promise((resolve, reject) => {\n execFile(\n options.binary,\n args,\n { cwd: options.cwd, maxBuffer: 256 * 1024 * 1024, windowsHide: true },\n (error, stdout, stderr) => {\n if (error !== null) {\n const reason = stderr.trim();\n reject(new Error(reason === \"\" ? error.message : reason, { cause: error }));\n return;\n }\n try {\n resolve(parseDiff(stdout, options.build));\n } catch (cause) {\n reject(new Error(\"`ascribe diff` wrote output that isn't its JSON.\", { cause }));\n }\n },\n );\n });\n}\n\n/** The build's part of `ascribe diff --format json`'s output. */\nexport function parseDiff(json: string, build: string): DiffResult {\n // Checked as far as telling a diff report from other JSON.\n const report = JSON.parse(json) as Partial<DiffReport>;\n if (report.base === undefined || !Array.isArray(report.builds)) {\n throw new Error(\"not a diff report\");\n }\n const pages = report.builds.find((b) => b.build === build)?.pages ?? [];\n const errors = typeof report.working_tree_errors === \"number\" ? report.working_tree_errors : 0;\n return { base: report.base, pages, errors };\n}\n\n/**\n * `ascribe diff --format prompt`'s prompt about a page, or a fragment's reach,\n * at a content path; `undefined` when it didn't change. Rejects with\n * `ascribe diff`'s reason as the message.\n */\nexport function runPrompt(options: {\n binary: string;\n configPath: string;\n build: string;\n cwd: string;\n base: string | undefined;\n path: string;\n}): Promise<string | undefined> {\n const args = [\n \"diff\",\n \"--format\",\n \"prompt\",\n \"--build\",\n options.build,\n \"--config\",\n options.configPath,\n \"--color\",\n \"never\",\n ];\n if (options.base !== undefined) args.push(\"--base\", options.base);\n // After `--`: a content path that starts with `-` is still a path.\n args.push(\"--\", options.path);\n return new Promise((resolve, reject) => {\n execFile(\n options.binary,\n args,\n { cwd: options.cwd, maxBuffer: 16 * 1024 * 1024, windowsHide: true },\n (error, stdout, stderr) => {\n if (error !== null) {\n const reason = stderr.trim();\n reject(new Error(reason === \"\" ? error.message : reason, { cause: error }));\n return;\n }\n resolve(stdout === \"\" ? undefined : stdout);\n },\n );\n });\n}\n"]}
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/review/diff.ts"],"names":[],"mappings":"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","sourcesContent":["// Running `ascribe diff` for the integration's build: its changes as JSON, and\n// its agent prompts.\nimport { execFile } from \"node:child_process\";\nimport type { DiffReport } from \"../shapes.js\";\nimport type { BaseInfo, PageDiff } from \"./protocol.js\";\n\n/** One build's changes against a base. */\nexport interface DiffResult {\n base: BaseInfo;\n /** The build's changed pages: the first build compared, a versioned build's current line. */\n pages: PageDiff[];\n /**\n * Each build compared, in order, with its changed pages, when more than one\n * was: a versioned build's lines, the current one first.\n */\n lines?: { build: string; pages: PageDiff[] }[];\n /** How many errors `ascribe check` finds in the working tree for the build. */\n errors: number;\n}\n\n/**\n * Compares the build's pages with `base` (the default branch when it's\n * `undefined`). Rejects with `ascribe diff`'s reason as the message.\n */\nexport function runDiff(options: {\n binary: string;\n configPath: string;\n /** The build, or a versioned build's lines, the current one first. */\n build: string | readonly string[];\n cwd: string;\n base: string | undefined;\n}): Promise<DiffResult> {\n const args = [\n \"diff\",\n \"--format\",\n \"json\",\n ...buildArgs(options.build),\n \"--config\",\n options.configPath,\n \"--color\",\n \"never\",\n ];\n if (options.base !== undefined) args.push(\"--base\", options.base);\n return new Promise((resolve, reject) => {\n execFile(\n options.binary,\n args,\n { cwd: options.cwd, maxBuffer: 256 * 1024 * 1024, windowsHide: true },\n (error, stdout, stderr) => {\n if (error !== null) {\n const reason = stderr.trim();\n reject(new Error(reason === \"\" ? error.message : reason, { cause: error }));\n return;\n }\n try {\n resolve(parseDiff(stdout, options.build));\n } catch (cause) {\n reject(new Error(\"`ascribe diff` wrote output that isn't its JSON.\", { cause }));\n }\n },\n );\n });\n}\n\n/** `--build` for each build. */\nfunction buildArgs(build: string | readonly string[]): string[] {\n return (typeof build === \"string\" ? [build] : build).flatMap((b) => [\"--build\", b]);\n}\n\n/** The build's part of `ascribe diff --format json`'s output, or each of the builds' parts. */\nexport function parseDiff(json: string, build: string | readonly string[]): DiffResult {\n // Checked as far as telling a diff report from other JSON.\n const report = JSON.parse(json) as Partial<DiffReport>;\n if (report.base === undefined || !Array.isArray(report.builds)) {\n throw new Error(\"not a diff report\");\n }\n const builds = typeof build === \"string\" ? [build] : build;\n const reported = report.builds;\n const pagesOf = (name: string) => reported.find((b) => b.build === name)?.pages ?? [];\n const pages = pagesOf(builds[0] ?? \"\");\n const errors = typeof report.working_tree_errors === \"number\" ? report.working_tree_errors : 0;\n if (builds.length < 2) return { base: report.base, pages, errors };\n const lines = builds.map((name) => ({ build: name, pages: pagesOf(name) }));\n return { base: report.base, pages, errors, lines };\n}\n\n/**\n * `ascribe diff --format prompt`'s prompt about a page, or a fragment's reach,\n * at a content path; `undefined` when it didn't change. Rejects with\n * `ascribe diff`'s reason as the message.\n */\nexport function runPrompt(options: {\n binary: string;\n configPath: string;\n /** The build, or a versioned build's lines: the prompt is about the first the page changed in. */\n build: string | readonly string[];\n cwd: string;\n base: string | undefined;\n path: string;\n}): Promise<string | undefined> {\n const args = [\n \"diff\",\n \"--format\",\n \"prompt\",\n ...buildArgs(options.build),\n \"--config\",\n options.configPath,\n \"--color\",\n \"never\",\n ];\n if (options.base !== undefined) args.push(\"--base\", options.base);\n // After `--`: a content path that starts with `-` is still a path.\n args.push(\"--\", options.path);\n return new Promise((resolve, reject) => {\n execFile(\n options.binary,\n args,\n { cwd: options.cwd, maxBuffer: 16 * 1024 * 1024, windowsHide: true },\n (error, stdout, stderr) => {\n if (error !== null) {\n const reason = stderr.trim();\n reject(new Error(reason === \"\" ? error.message : reason, { cause: error }));\n return;\n }\n resolve(stdout === \"\" ? undefined : stdout);\n },\n );\n });\n}\n"]}
@@ -8,6 +8,8 @@ export interface RoutedPage {
8
8
  title: string | null;
9
9
  /** The title formatted, when its field sets `inline = "code"`. */
10
10
  formatted_title: FormattedPiece[] | null;
11
+ /** For a line of a versioned build, the build that is the line: `site@3.4`. */
12
+ build?: string;
11
13
  }
12
14
  /**
13
15
  * Every page of the build's JSON output (`<output-dir>/<build>/json/`), by
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/review/routes.ts"],"names":[],"mappings":"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","sourcesContent":["// Which page of the build is at a route, and its title: read from the JSON\n// output, which the dev server writes while review is on, since the site\n// output doesn't say.\nimport { open, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { FormattedPiece } from \"@ascribed/review/overlay\";\n\n/** A route as compared: decoded, without a trailing `/` (except the root's). */\nexport function normalizeRoute(route: string): string {\n let decoded: string;\n try {\n decoded = decodeURI(route);\n } catch {\n decoded = route;\n }\n const trimmed = (decoded.split(/[?#]/)[0] ?? \"\").replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n}\n\n/** A page of the build, as its JSON output names it. */\nexport interface RoutedPage {\n /** The content path. */\n path: string;\n title: string | null;\n /** The title formatted, when its field sets `inline = \"code\"`. */\n formatted_title: FormattedPiece[] | null;\n}\n\n/** How much of a page's JSON to read at a time. */\nconst CHUNK_BYTES = 4096;\n\n/**\n * Where the page's own keys end in the JSON output, which writes it\n * pretty-printed: `availability` comes after `path`, `route`, `title`,\n * `frontmatter`, and `formatted`, and before the page's blocks.\n */\nconst AFTER_HEAD = Buffer.from('\\n \"availability\":');\n\n/**\n * Every page of the build's JSON output (`<output-dir>/<build>/json/`), by\n * normalized route. Empty when there's no JSON output.\n */\nexport async function readRoutes(jsonRoot: string): Promise<Map<string, RoutedPage>> {\n const routes = new Map<string, RoutedPage>();\n let files: { path?: unknown; kind?: unknown }[];\n try {\n const manifest = JSON.parse(\n await readFile(path.join(path.dirname(jsonRoot), \"json.manifest.json\"), \"utf8\"),\n ) as { files?: unknown };\n files = Array.isArray(manifest.files) ? (manifest.files as typeof files) : [];\n } catch {\n return routes;\n }\n await Promise.all(\n files.map(async (entry) => {\n if (entry.kind !== \"page\" || typeof entry.path !== \"string\") return;\n const page = await readHead(path.join(jsonRoot, ...entry.path.split(\"/\")));\n if (page) {\n const { route, ...routed } = page;\n routes.set(normalizeRoute(route), routed);\n }\n }),\n );\n return routes;\n}\n\n/**\n * A page's `path`, `route`, `title`, and formatted title, from the start of\n * its JSON up to `availability`, or the whole of it if need be.\n */\nasync function readHead(file: string): Promise<(RoutedPage & { route: string }) | undefined> {\n let text: string;\n try {\n const handle = await open(file, \"r\");\n try {\n const chunks: Buffer[] = [];\n let read = 0;\n let end = -1;\n for (;;) {\n const buffer = Buffer.alloc(CHUNK_BYTES);\n const { bytesRead } = await handle.read(buffer, 0, CHUNK_BYTES, read);\n if (bytesRead === 0) break;\n chunks.push(buffer.subarray(0, bytesRead));\n read += bytesRead;\n // The marker may straddle two chunks, so look from just before this one.\n end = Buffer.concat(chunks).indexOf(\n AFTER_HEAD,\n Math.max(0, read - bytesRead - AFTER_HEAD.length),\n );\n if (end >= 0) break;\n }\n const all = Buffer.concat(chunks);\n // The keys before `availability`, closed as an object of their own.\n text =\n end >= 0\n ? `${all.subarray(0, end).toString(\"utf8\").replace(/,\\s*$/, \"\")}}`\n : all.toString(\"utf8\");\n } finally {\n await handle.close();\n }\n } catch {\n return undefined;\n }\n let page: { path?: unknown; route?: unknown; title?: unknown; formatted?: { title?: unknown } };\n try {\n page = JSON.parse(text) as typeof page;\n } catch {\n return undefined;\n }\n if (typeof page.path !== \"string\" || typeof page.route !== \"string\") return undefined;\n const formatted = page.formatted?.title;\n return {\n path: page.path,\n route: page.route,\n title: typeof page.title === \"string\" ? page.title : null,\n formatted_title: Array.isArray(formatted) ? (formatted as FormattedPiece[]) : null,\n };\n}\n"]}
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/review/routes.ts"],"names":[],"mappings":"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","sourcesContent":["// Which page of the build is at a route, and its title: read from the JSON\n// output, which the dev server writes while review is on, since the site\n// output doesn't say.\nimport { open, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { FormattedPiece } from \"@ascribed/review/overlay\";\n\n/** A route as compared: decoded, without a trailing `/` (except the root's). */\nexport function normalizeRoute(route: string): string {\n let decoded: string;\n try {\n decoded = decodeURI(route);\n } catch {\n decoded = route;\n }\n const trimmed = (decoded.split(/[?#]/)[0] ?? \"\").replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n}\n\n/** A page of the build, as its JSON output names it. */\nexport interface RoutedPage {\n /** The content path. */\n path: string;\n title: string | null;\n /** The title formatted, when its field sets `inline = \"code\"`. */\n formatted_title: FormattedPiece[] | null;\n /** For a line of a versioned build, the build that is the line: `site@3.4`. */\n build?: string;\n}\n\n/** How much of a page's JSON to read at a time. */\nconst CHUNK_BYTES = 4096;\n\n/**\n * Where the page's own keys end in the JSON output, which writes it\n * pretty-printed: `availability` comes after `path`, `route`, `title`,\n * `frontmatter`, and `formatted`, and before the page's blocks.\n */\nconst AFTER_HEAD = Buffer.from('\\n \"availability\":');\n\n/**\n * Every page of the build's JSON output (`<output-dir>/<build>/json/`), by\n * normalized route. Empty when there's no JSON output.\n */\nexport async function readRoutes(jsonRoot: string): Promise<Map<string, RoutedPage>> {\n const routes = new Map<string, RoutedPage>();\n let files: { path?: unknown; kind?: unknown }[];\n try {\n const manifest = JSON.parse(\n await readFile(path.join(path.dirname(jsonRoot), \"json.manifest.json\"), \"utf8\"),\n ) as { files?: unknown };\n files = Array.isArray(manifest.files) ? (manifest.files as typeof files) : [];\n } catch {\n return routes;\n }\n await Promise.all(\n files.map(async (entry) => {\n if (entry.kind !== \"page\" || typeof entry.path !== \"string\") return;\n const page = await readHead(path.join(jsonRoot, ...entry.path.split(\"/\")));\n if (page) {\n const { route, ...routed } = page;\n routes.set(normalizeRoute(route), routed);\n }\n }),\n );\n return routes;\n}\n\n/**\n * A page's `path`, `route`, `title`, and formatted title, from the start of\n * its JSON up to `availability`, or the whole of it if need be.\n */\nasync function readHead(file: string): Promise<(RoutedPage & { route: string }) | undefined> {\n let text: string;\n try {\n const handle = await open(file, \"r\");\n try {\n const chunks: Buffer[] = [];\n let read = 0;\n let end = -1;\n for (;;) {\n const buffer = Buffer.alloc(CHUNK_BYTES);\n const { bytesRead } = await handle.read(buffer, 0, CHUNK_BYTES, read);\n if (bytesRead === 0) break;\n chunks.push(buffer.subarray(0, bytesRead));\n read += bytesRead;\n // The marker may straddle two chunks, so look from just before this one.\n end = Buffer.concat(chunks).indexOf(\n AFTER_HEAD,\n Math.max(0, read - bytesRead - AFTER_HEAD.length),\n );\n if (end >= 0) break;\n }\n const all = Buffer.concat(chunks);\n // The keys before `availability`, closed as an object of their own.\n text =\n end >= 0\n ? `${all.subarray(0, end).toString(\"utf8\").replace(/,\\s*$/, \"\")}}`\n : all.toString(\"utf8\");\n } finally {\n await handle.close();\n }\n } catch {\n return undefined;\n }\n let page: { path?: unknown; route?: unknown; title?: unknown; formatted?: { title?: unknown } };\n try {\n page = JSON.parse(text) as typeof page;\n } catch {\n return undefined;\n }\n if (typeof page.path !== \"string\" || typeof page.route !== \"string\") return undefined;\n const formatted = page.formatted?.title;\n return {\n path: page.path,\n route: page.route,\n title: typeof page.title === \"string\" ? page.title : null,\n formatted_title: Array.isArray(formatted) ? (formatted as FormattedPiece[]) : null,\n };\n}\n"]}
@@ -296,7 +296,8 @@ export class ReviewServer {
296
296
  this.connected(),
297
297
  this.readRoutes(),
298
298
  ]);
299
- const pages = "result" in changes ? changes.result.pages : [];
299
+ // The line the route is in, for a versioned build: its changes are the ones shown.
300
+ const pages = "result" in changes ? linePages(changes.result, route, routes) : [];
300
301
  const shown = pages.filter((p) => p.status !== "removed");
301
302
  let page = shown.find((p) => normalizeRoute(p.route) === route) ?? null;
302
303
  const pagePath = page?.path ?? fromPage ?? routes.get(route)?.path ?? null;
@@ -325,6 +326,21 @@ export class ReviewServer {
325
326
  this.options.channel.send(CHANGED_EVENT, { from });
326
327
  }
327
328
  }
329
+ /**
330
+ * The changed pages of the line `route` is in: of the line whose changed
331
+ * page or JSON output has the route, else the current line's. For a build
332
+ * that isn't versioned, its changed pages.
333
+ */
334
+ function linePages(result, route, routes) {
335
+ const lines = result.lines;
336
+ if (lines === undefined)
337
+ return result.pages;
338
+ const changed = lines.find((l) => l.pages.some((p) => normalizeRoute(p.route) === route));
339
+ if (changed)
340
+ return changed.pages;
341
+ const build = routes.get(route)?.build;
342
+ return lines.find((l) => l.build === build)?.pages ?? result.pages;
343
+ }
328
344
  /** A changed page as the list shows it: without its changes, with its title. */
329
345
  function listed(page, routed) {
330
346
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/review/server.ts"],"names":[],"mappings":"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","sourcesContent":["// Review in the site preview, on the dev server's side: everything the\n// toolbar app asks for, answered over Astro's dev toolbar channel. Every\n// `git`, `ascribe`, and `gh` call happens here; the page gets the changes and\n// the threads, never a token. There are no HTTP routes: any page open in the\n// same browser could call a route on localhost, while the toolbar's channel\n// takes only clients that have the dev server's token (`channelProblem`).\n//\n// Nothing runs until the reviewer turns the app on. Then the server opens\n// the pull request's review (when the GitHub CLI finds one), compares the\n// build with the pull request's base (or the default branch), and compares\n// again after each rebuild.\nimport path from \"node:path\";\nimport { readFile } from \"node:fs/promises\";\nimport type { OverlayMethod, PromptRequest, ReviewSession } from \"@ascribed/review/github\";\nimport type { PageRef } from \"@ascribed/review/place\";\nimport type { DiffResult } from \"./diff.js\";\nimport { messageOf, type Connection } from \"./github.js\";\nimport {\n CHANGED_EVENT,\n REQUEST_EVENT,\n RESULT_EVENT,\n type ChangedPage,\n type PageDiff,\n type PageView,\n type Request,\n type Result,\n type Status,\n type ThreadsState,\n} from \"./protocol.js\";\nimport { normalizeRoute, type RoutedPage } from \"./routes.js\";\n\n/** A page's end of the channel: the server answers it alone. */\nexport interface ChannelClient {\n send(event: string, payload?: unknown): void;\n}\n\n/**\n * The toolbar's channel, as `astro:server:setup`'s `toolbar` has it. Its\n * listeners get the sending page's client too (it is Vite's `server.hot`);\n * without one, an answer goes to every page, which tell theirs by `tab`.\n */\nexport interface ToolbarChannel {\n on(event: string, callback: (data: unknown, client?: ChannelClient) => void): void;\n send(event: string, payload: unknown): void;\n}\n\nexport interface ReviewServerOptions {\n channel: ToolbarChannel;\n logger: { info(message: string): void; warn(message: string): void };\n build: string;\n /** The content root, absolute. */\n contentRoot: string;\n /** Why someone else could use the channel, if they could: then GitHub stays off. */\n channelProblem: string | undefined;\n /** Compares the build with `base` (the default branch when `undefined`). */\n diff(base: string | undefined): Promise<DiffResult>;\n /**\n * `ascribe diff`'s agent prompt about the page or fragment at a content\n * path, against `base`; `undefined` when it didn't change.\n */\n prompt(path: string, base: string | undefined): Promise<string | undefined>;\n /** The project's folder: where its `ascribe.toml` is. */\n projectDir: string;\n /** Opens the pull request's review. */\n connect(): Promise<Connection>;\n /** Writes the build's JSON output, in turn with the dev server's builds: it has the routes. */\n writeRoutes(): Promise<void>;\n /** Every page of the build, by normalized route. */\n readRoutes(): Promise<Map<string, RoutedPage>>;\n /** How long to wait after a rebuild before comparing again. Default 150 ms. */\n debounceMs?: number;\n}\n\n/** The overlay's requests, which go to the session. */\nconst OVERLAY: ReadonlySet<string> = new Set<OverlayMethod>([\n \"load\",\n \"commentTarget\",\n \"comment\",\n \"reply\",\n \"allThreads\",\n \"resolve\",\n \"submit\",\n \"discard\",\n]);\n\nexport class ReviewServer {\n private on = false;\n private problem: string | undefined;\n private connection: Connection | undefined;\n private connecting: Promise<Connection> | undefined;\n private diffed: { result: DiffResult } | { error: string } | undefined;\n private diffing: Promise<void> | undefined;\n private stale = false;\n private timer: ReturnType<typeof setTimeout> | undefined;\n private routes: Promise<Map<string, RoutedPage>> | undefined;\n /** The page each overlay last read, by content path: comments are made on it. */\n private readonly pages = new Map<string, PageRef>();\n /** How long the last comparison took, in milliseconds. */\n lastDiffMs: number | undefined;\n\n constructor(private readonly options: ReviewServerOptions) {\n this.problem = options.channelProblem;\n options.channel.on(REQUEST_EVENT, (data, client) => {\n void this.receive(data, client);\n });\n }\n\n /**\n * The address the dev server listens on, once it does: one other machines\n * can reach turns comments off, as `server.host` does.\n */\n listening(address: string): void {\n if (this.problem !== undefined || isLoopback(address)) return;\n this.problem = NETWORK_PROBLEM;\n this.connection = undefined;\n this.connecting = undefined;\n }\n\n /** Whether review is on: then the dev server's builds write JSON output too. */\n get active(): boolean {\n return this.on;\n }\n\n /** The dev server rebuilt: compare again soon, and read the routes again when asked. */\n rebuilt(): void {\n if (!this.on) return;\n this.stale = true;\n this.routes = undefined;\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => {\n this.timer = undefined;\n void this.changes();\n }, this.options.debounceMs ?? 150);\n }\n\n /** The dev server stopped. */\n dispose(): void {\n if (this.timer) clearTimeout(this.timer);\n this.on = false;\n }\n\n private async receive(data: unknown, client: ChannelClient | undefined): Promise<void> {\n const request = data as Partial<Request> | null;\n if (typeof request?.id !== \"number\" || typeof request.method !== \"string\") return;\n const params =\n typeof request.params === \"object\" && request.params !== null ? request.params : {};\n const tab = typeof request.tab === \"string\" ? request.tab : \"\";\n const answer: Result = { tab, id: request.id };\n try {\n answer.result = await this.handle(request.method, params, tab);\n } catch (error) {\n const code = (error as { code?: unknown }).code;\n answer.error =\n typeof code === \"string\"\n ? { message: messageOf(error), code }\n : { message: messageOf(error) };\n }\n if (typeof client?.send === \"function\") client.send(RESULT_EVENT, answer);\n else this.options.channel.send(RESULT_EVENT, answer);\n }\n\n /** Answers one request. Rejects with a sentence to show. */\n async handle(method: string, params: Record<string, unknown>, tab = \"\"): Promise<unknown> {\n switch (method) {\n case \"status\":\n return { on: this.on } satisfies Status;\n case \"start\":\n await this.start();\n return { on: true } satisfies Status;\n case \"stop\":\n this.stop();\n this.changed(tab);\n return { on: false } satisfies Status;\n case \"page\":\n return this.pageView(params);\n case \"refresh\":\n await this.refresh();\n this.changed(tab);\n return null;\n case \"prompt\":\n return (await this.prompt(params)) ?? null;\n }\n if (!OVERLAY.has(method)) throw new Error(`Unknown request: ${method}.`);\n const connection = this.on ? await this.connected() : undefined;\n if (connection?.state !== \"on\") throw new Error(\"Review comments aren't on.\");\n const github = await import(\"@ascribed/review/github\");\n const pagePath = typeof params[\"path\"] === \"string\" ? params[\"path\"] : undefined;\n const answer = await github.answerRequest(\n {\n session: connection.session,\n page: pagePath === undefined ? undefined : { build: this.options.build, path: pagePath },\n lastPage: pagePath === undefined ? undefined : this.pages.get(pagePath),\n changedPages: async () => {\n const [changes, routes] = await Promise.all([this.changes(), this.readRoutes()]);\n if (!(\"result\" in changes)) return [];\n return changes.result.pages.map((p) => listed(p, routes.get(normalizeRoute(p.route))));\n },\n },\n method as OverlayMethod,\n params,\n );\n if (answer.page) this.pages.set(answer.page.path, answer.page);\n if (answer.changed) this.changed(tab);\n return answer.result;\n }\n\n /**\n * The agent prompt the page asked for: a thread's, or every open one's,\n * from the pull request; the page's changes, or a fragment's reach, from\n * `ascribe diff`. `undefined` when there's nothing to prompt about.\n */\n private async prompt(params: Record<string, unknown>): Promise<string | undefined> {\n if (!this.on) throw new Error(\"Review is off.\");\n const request = params[\"request\"] as PromptRequest | undefined;\n const pagePath = typeof params[\"path\"] === \"string\" ? params[\"path\"] : undefined;\n const connection = await this.connected();\n switch (request?.kind) {\n case \"thread\":\n case \"open-threads\": {\n if (connection.state !== \"on\") throw new Error(\"Review comments aren't on.\");\n const github = await import(\"@ascribed/review/github\");\n const contentRoot = this.options.contentRoot;\n return github.buildThreadsPrompt(\n {\n session: connection.session,\n project: github.promptProject(this.options.projectDir),\n changedPages: async () => {\n const changes = await this.changes();\n return \"result\" in changes ? changes.result.pages : [];\n },\n // The agent reads the files on disk, as the dev server does: no editor here.\n readSource: (file) =>\n readFile(path.join(contentRoot, ...file.split(\"/\")), \"utf8\").then(\n (text) => ({ text, unsaved: false }),\n () => undefined,\n ),\n },\n request,\n );\n }\n case \"page-changes\":\n case \"fragment-reach\": {\n const target = request.kind === \"page-changes\" ? pagePath : request.fragment;\n if (target === undefined) throw new Error(\"That isn't a page of the project.\");\n const base = connection.state === \"on\" ? connection.base : undefined;\n return this.options.prompt(target, base);\n }\n default:\n throw new Error(\"Unknown prompt.\");\n }\n }\n\n private async start(): Promise<void> {\n if (this.on) return;\n this.on = true;\n this.stale = true;\n this.routes = undefined;\n this.connection = undefined;\n const started = Date.now();\n // The routes come from the JSON output, which the next builds write too.\n const routes = this.options.writeRoutes().catch((error: unknown) => {\n this.options.logger.warn(`couldn't write the routes for review: ${messageOf(error)}`);\n });\n await this.connected();\n const changes = await this.changes();\n await routes;\n if (\"result\" in changes) {\n const base = changes.result.base;\n this.options.logger.info(\n `review: compared \"${this.options.build}\" with ${base.requested} in ${this.lastDiffMs ?? 0} ms (started in ${Date.now() - started} ms)`,\n );\n } else {\n this.options.logger.warn(`review: ${changes.error}`);\n }\n }\n\n private stop(): void {\n this.on = false;\n this.connection = undefined;\n this.connecting = undefined;\n this.diffed = undefined;\n this.routes = undefined;\n this.pages.clear();\n if (this.timer) clearTimeout(this.timer);\n this.timer = undefined;\n }\n\n private async refresh(): Promise<void> {\n if (!this.on) return;\n const connection = await this.connected();\n if (connection.state === \"on\") await connection.session.refresh();\n else this.connection = undefined;\n // A pull request found now changes the base.\n await this.connected();\n this.stale = true;\n await this.changes();\n }\n\n /** The pull request's review, opened once while review is on. */\n private connected(): Promise<Connection> {\n if (this.connection) return Promise.resolve(this.connection);\n if (this.problem !== undefined) {\n this.connection = {\n state: \"unprotected\",\n message: `Showing changes only. Comments are off because ${this.problem}.`,\n };\n return Promise.resolve(this.connection);\n }\n this.connecting ??= this.options\n .connect()\n .catch((error: unknown): Connection => ({ state: \"error\", message: messageOf(error) }))\n .then((connection) => {\n this.connecting = undefined;\n // The server turned out to listen on the network meanwhile.\n if (this.problem !== undefined) return this.connected();\n if (this.on) this.connection = connection;\n return connection;\n });\n return this.connecting;\n }\n\n /** The build's changes, compared again first if a rebuild made them stale. */\n private async changes(): Promise<{ result: DiffResult } | { error: string }> {\n while (this.diffing) await this.diffing;\n if (this.stale || this.diffed === undefined) {\n this.stale = false;\n this.diffing = this.compare().finally(() => {\n this.diffing = undefined;\n });\n await this.diffing;\n }\n return this.diffed ?? { error: \"Review is off.\" };\n }\n\n private async compare(): Promise<void> {\n const connection = await this.connected();\n const base = connection.state === \"on\" ? connection.base : undefined;\n const started = Date.now();\n try {\n this.diffed = { result: await this.options.diff(base) };\n } catch (error) {\n this.diffed = { error: messageOf(error) };\n }\n this.lastDiffMs = Date.now() - started;\n }\n\n private async pageView(params: Record<string, unknown>): Promise<PageView> {\n if (!this.on) throw new Error(\"Review is off.\");\n const route = normalizeRoute(typeof params[\"route\"] === \"string\" ? params[\"route\"] : \"/\");\n const fromPage = typeof params[\"path\"] === \"string\" ? params[\"path\"] : null;\n const [changes, connection, routes] = await Promise.all([\n this.changes(),\n this.connected(),\n this.readRoutes(),\n ]);\n const pages = \"result\" in changes ? changes.result.pages : [];\n const shown = pages.filter((p) => p.status !== \"removed\");\n let page = shown.find((p) => normalizeRoute(p.route) === route) ?? null;\n const pagePath = page?.path ?? fromPage ?? routes.get(route)?.path ?? null;\n if (page === null && pagePath !== null) page = shown.find((p) => p.path === pagePath) ?? null;\n return {\n kind: pagePath === null ? \"not-page\" : \"page\",\n path: pagePath,\n page,\n base: \"result\" in changes ? changes.result.base : null,\n problem: \"error\" in changes ? changes.error : null,\n errors: \"result\" in changes ? changes.result.errors : 0,\n threads: threadsState(connection),\n changedPages: pages.map((p) => listed(p, routes.get(normalizeRoute(p.route)))),\n contentRoot: this.options.contentRoot,\n separator: path.sep,\n };\n }\n\n /** The build's pages by route, read once until the next rebuild. */\n private readRoutes(): Promise<Map<string, RoutedPage>> {\n this.routes ??= this.options.readRoutes().catch(() => new Map<string, RoutedPage>());\n return this.routes;\n }\n\n /** Tells every page the threads or the changes changed; `from` is the page that did it. */\n private changed(from: string): void {\n this.options.channel.send(CHANGED_EVENT, { from });\n }\n}\n\n/** A changed page as the list shows it: without its changes, with its title. */\nfunction listed(page: PageDiff, routed: RoutedPage | undefined): ChangedPage {\n return {\n title: routed?.title ?? null,\n formatted_title: routed?.formatted_title ?? null,\n path: page.path,\n route: page.route,\n status: page.status,\n own_file_changed: page.own_file_changed,\n because: page.because,\n page_changed: page.page_changed,\n counts: page.counts,\n };\n}\n\n/** What the page shows about the threads. */\nfunction threadsState(connection: Connection): ThreadsState {\n if (connection.state !== \"on\") return connection;\n const pr: ReviewSession[\"pullRequest\"] = connection.session.pullRequest;\n return {\n state: \"on\",\n pullRequest: { number: pr.number, url: pr.url, baseRefName: pr.baseRefName },\n local: connection.local,\n };\n}\n\n/**\n * Why someone other than the developer could use the dev server's toolbar\n * channel, or `undefined` when they can't. Vite takes a browser's connection\n * only with the token in its client script, which the page's own origin can\n * read and, by default, only localhost origins can fetch. These settings undo\n * that, and a server listening on the network gives its pages, token and all,\n * to anyone who can reach it.\n */\nexport function channelProblem(config: {\n server: { cors?: unknown; allowedHosts?: unknown; host?: unknown };\n legacy?: { skipWebSocketTokenCheck?: unknown } | undefined;\n}): string | undefined {\n const pages = \"other web pages could talk to this dev server\";\n if (config.legacy?.skipWebSocketTokenCheck === true) {\n return `${pages}: \\`legacy.skipWebSocketTokenCheck\\` is on`;\n }\n if (config.server.allowedHosts === true) return `${pages}: \\`server.allowedHosts\\` is \\`true\\``;\n const cors = config.server.cors;\n const origin =\n typeof cors === \"object\" && cors !== null ? (cors as { origin?: unknown }).origin : undefined;\n if (cors === true || origin === true || origin === \"*\") {\n return `${pages}: \\`server.cors\\` lets any origin read its pages`;\n }\n if (!isLoopback(config.server.host)) return NETWORK_PROBLEM;\n return undefined;\n}\n\nconst NETWORK_PROBLEM =\n \"the dev server is listening on the network (`--host`), so anyone who can reach it could comment as you\";\n\n/** Whether Vite's `server.host`, or the address the server listens on, keeps the server on this machine: unset, `false`, or a loopback name. */\nfunction isLoopback(host: unknown): boolean {\n if (host === undefined || host === false) return true;\n if (typeof host !== \"string\") return false;\n const name = host.toLowerCase().replace(/^\\[(.*)\\]$/, \"$1\");\n return name === \"localhost\" || name === \"::1\" || /^127(\\.\\d{1,3}){3}$/.test(name);\n}\n"]}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/review/server.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,yEAAyE;AACzE,8EAA8E;AAC9E,6EAA6E;AAC7E,4EAA4E;AAC5E,0EAA0E;AAC1E,EAAE;AACF,0EAA0E;AAC1E,0EAA0E;AAC1E,2EAA2E;AAC3E,4BAA4B;AAC5B,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAI5C,OAAO,EAAE,SAAS,EAAmB,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,aAAa,EACb,aAAa,EACb,YAAY,GAQb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAmB,MAAM,aAAa,CAAC;AA4C9D,uDAAuD;AACvD,MAAM,OAAO,GAAwB,IAAI,GAAG,CAAgB;IAC1D,MAAM;IACN,eAAe;IACf,SAAS;IACT,OAAO;IACP,YAAY;IACZ,SAAS;IACT,QAAQ;IACR,SAAS;CACV,CAAC,CAAC;AAEH,MAAM,OAAO,YAAY;IAeM,OAAO;IAd5B,EAAE,GAAG,KAAK,CAAC;IACX,OAAO,CAAqB;IAC5B,UAAU,CAAyB;IACnC,UAAU,CAAkC;IAC5C,MAAM,CAAyD;IAC/D,OAAO,CAA4B;IACnC,KAAK,GAAG,KAAK,CAAC;IACd,KAAK,CAA4C;IACjD,MAAM,CAA+C;IAC7D,iFAAiF;IAChE,KAAK,GAAG,IAAI,GAAG,EAAmB,CAAC;IACpD,0DAA0D;IAC1D,UAAU,CAAqB;IAE/B,YAA6B,OAA4B;uBAA5B,OAAO;QAClC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC;QACtC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YACjD,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAClC,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,OAAe;QACvB,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO;QAC9D,IAAI,CAAC,OAAO,GAAG,eAAe,CAAC;QAC/B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED,gFAAgF;IAChF,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,EAAE,CAAC;IACjB,CAAC;IAED,wFAAwF;IACxF,OAAO;QACL,IAAI,CAAC,IAAI,CAAC,EAAE;YAAE,OAAO;QACrB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,IAAI,IAAI,CAAC,KAAK;YAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC3B,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;YACvB,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;QACtB,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC,CAAC;IACrC,CAAC;IAED,8BAA8B;IAC9B,OAAO;QACL,IAAI,IAAI,CAAC,KAAK;YAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,EAAE,GAAG,KAAK,CAAC;IAClB,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,IAAa,EAAE,MAAiC;QACpE,MAAM,OAAO,GAAG,IAA+B,CAAC;QAChD,IAAI,OAAO,OAAO,EAAE,EAAE,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO;QAClF,MAAM,MAAM,GACV,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACtF,MAAM,GAAG,GAAG,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/D,MAAM,MAAM,GAAW,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC;QAC/C,IAAI,CAAC;YACH,MAAM,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;QACjE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,GAAI,KAA4B,CAAC,IAAI,CAAC;YAChD,MAAM,CAAC,KAAK;gBACV,OAAO,IAAI,KAAK,QAAQ;oBACtB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE;oBACrC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QACtC,CAAC;QACD,IAAI,OAAO,MAAM,EAAE,IAAI,KAAK,UAAU;YAAE,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;;YACrE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACvD,CAAC;IAED,4DAA4D;IAC5D,KAAK,CAAC,MAAM,CAAC,MAAc,EAAE,MAA+B,EAAE,GAAG,GAAG,EAAE;QACpE,QAAQ,MAAM,EAAE,CAAC;YACf,KAAK,QAAQ;gBACX,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAmB,CAAC;YAC1C,KAAK,OAAO;gBACV,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;gBACnB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAmB,CAAC;YACvC,KAAK,MAAM;gBACT,IAAI,CAAC,IAAI,EAAE,CAAC;gBACZ,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBAClB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAmB,CAAC;YACxC,KAAK,MAAM;gBACT,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC/B,KAAK,SAAS;gBACZ,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;gBACrB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBAClB,OAAO,IAAI,CAAC;YACd,KAAK,QAAQ;gBACX,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,CAAC;QAC/C,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,MAAM,GAAG,CAAC,CAAC;QACzE,MAAM,UAAU,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QAChE,IAAI,UAAU,EAAE,KAAK,KAAK,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAC9E,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;QACvD,MAAM,QAAQ,GAAG,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACjF,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,CACvC;YACE,OAAO,EAAE,UAAU,CAAC,OAAO;YAC3B,IAAI,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE;YACxF,QAAQ,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC;YACvE,YAAY,EAAE,KAAK,IAAI,EAAE;gBACvB,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;gBACjF,IAAI,CAAC,CAAC,QAAQ,IAAI,OAAO,CAAC;oBAAE,OAAO,EAAE,CAAC;gBACtC,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACzF,CAAC;SACF,EACD,MAAuB,EACvB,MAAM,CACP,CAAC;QACF,IAAI,MAAM,CAAC,IAAI;YAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QAC/D,IAAI,MAAM,CAAC,OAAO;YAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACtC,OAAO,MAAM,CAAC,MAAM,CAAC;IACvB,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,MAAM,CAAC,MAA+B;QAClD,IAAI,CAAC,IAAI,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAChD,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAA8B,CAAC;QAC/D,MAAM,QAAQ,GAAG,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACjF,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QAC1C,QAAQ,OAAO,EAAE,IAAI,EAAE,CAAC;YACtB,KAAK,QAAQ,CAAC;YACd,KAAK,cAAc,EAAE,CAAC;gBACpB,IAAI,UAAU,CAAC,KAAK,KAAK,IAAI;oBAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;gBAC7E,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;gBACvD,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC;gBAC7C,OAAO,MAAM,CAAC,kBAAkB,CAC9B;oBACE,OAAO,EAAE,UAAU,CAAC,OAAO;oBAC3B,OAAO,EAAE,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;oBACtD,YAAY,EAAE,KAAK,IAAI,EAAE;wBACvB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;wBACrC,OAAO,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;oBACzD,CAAC;oBACD,6EAA6E;oBAC7E,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CACnB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,CAC/D,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EACpC,GAAG,EAAE,CAAC,SAAS,CAChB;iBACJ,EACD,OAAO,CACR,CAAC;YACJ,CAAC;YACD,KAAK,cAAc,CAAC;YACpB,KAAK,gBAAgB,EAAE,CAAC;gBACtB,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;gBAC7E,IAAI,MAAM,KAAK,SAAS;oBAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;gBAC/E,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;gBACrE,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAC3C,CAAC;YACD;gBACE,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,KAAK;QACjB,IAAI,IAAI,CAAC,EAAE;YAAE,OAAO;QACpB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;QACf,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,yEAAyE;QACzE,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACjE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,yCAAyC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACxF,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACrC,MAAM,MAAM,CAAC;QACb,IAAI,QAAQ,IAAI,OAAO,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC;YACjC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CACtB,qBAAqB,IAAI,CAAC,OAAO,CAAC,KAAK,UAAU,IAAI,CAAC,SAAS,OAAO,IAAI,CAAC,UAAU,IAAI,CAAC,mBAAmB,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,MAAM,CACxI,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAEO,IAAI;QACV,IAAI,CAAC,EAAE,GAAG,KAAK,CAAC;QAChB,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,IAAI,IAAI,CAAC,KAAK;YAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;IACzB,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,IAAI,CAAC,IAAI,CAAC,EAAE;YAAE,OAAO;QACrB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QAC1C,IAAI,UAAU,CAAC,KAAK,KAAK,IAAI;YAAE,MAAM,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;;YAC7D,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QACjC,6CAA6C;QAC7C,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACvB,CAAC;IAED,iEAAiE;IACzD,SAAS;QACf,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC7D,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAC/B,IAAI,CAAC,UAAU,GAAG;gBAChB,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,kDAAkD,IAAI,CAAC,OAAO,GAAG;aAC3E,CAAC;YACF,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,OAAO;aAC7B,OAAO,EAAE;aACT,KAAK,CAAC,CAAC,KAAc,EAAc,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;aACtF,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE;YACnB,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;YAC5B,4DAA4D;YAC5D,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC;YACxD,IAAI,IAAI,CAAC,EAAE;gBAAE,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;YAC1C,OAAO,UAAU,CAAC;QACpB,CAAC,CAAC,CAAC;QACL,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,8EAA8E;IACtE,KAAK,CAAC,OAAO;QACnB,OAAO,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,CAAC,OAAO,CAAC;QACxC,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC5C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;YACnB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;gBACzC,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;YAC3B,CAAC,CAAC,CAAC;YACH,MAAM,IAAI,CAAC,OAAO,CAAC;QACrB,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,IAAI,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IACpD,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QACrE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5C,CAAC;QACD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;IACzC,CAAC;IAEO,KAAK,CAAC,QAAQ,CAAC,MAA+B;QACpD,IAAI,CAAC,IAAI,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAChD,MAAM,KAAK,GAAG,cAAc,CAAC,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC1F,MAAM,QAAQ,GAAG,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC5E,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACtD,IAAI,CAAC,OAAO,EAAE;YACd,IAAI,CAAC,SAAS,EAAE;YAChB,IAAI,CAAC,UAAU,EAAE;SAClB,CAAC,CAAC;QACH,mFAAmF;QACnF,MAAM,KAAK,GAAG,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC;QAC1D,IAAI,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC;QACxE,MAAM,QAAQ,GAAG,IAAI,EAAE,IAAI,IAAI,QAAQ,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI,IAAI,CAAC;QAC3E,IAAI,IAAI,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI;YAAE,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI,IAAI,CAAC;QAC9F,OAAO;YACL,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM;YAC7C,IAAI,EAAE,QAAQ;YACd,IAAI;YACJ,IAAI,EAAE,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;YACtD,OAAO,EAAE,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI;YAClD,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YACvD,OAAO,EAAE,YAAY,CAAC,UAAU,CAAC;YACjC,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC9E,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW;YACrC,SAAS,EAAE,IAAI,CAAC,GAAG;SACpB,CAAC;IACJ,CAAC;IAED,oEAAoE;IAC5D,UAAU;QAChB,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,GAAG,EAAsB,CAAC,CAAC;QACrF,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,2FAA2F;IACnF,OAAO,CAAC,IAAY;QAC1B,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,CAAC;CACF;AAED;;;;GAIG;AACH,SAAS,SAAS,CAAC,MAAkB,EAAE,KAAa,EAAE,MAA+B;IACnF,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC;IAC7C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC;IAC1F,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC,KAAK,CAAC;IAClC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,EAAE,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC;AACrE,CAAC;AAED,gFAAgF;AAChF,SAAS,MAAM,CAAC,IAAc,EAAE,MAA8B;IAC5D,OAAO;QACL,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,IAAI;QAC5B,eAAe,EAAE,MAAM,EAAE,eAAe,IAAI,IAAI;QAChD,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;QACvC,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,MAAM,EAAE,IAAI,CAAC,MAAM;KACpB,CAAC;AACJ,CAAC;AAED,6CAA6C;AAC7C,SAAS,YAAY,CAAC,UAAsB;IAC1C,IAAI,UAAU,CAAC,KAAK,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC;IACjD,MAAM,EAAE,GAAiC,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC;IACxE,OAAO;QACL,KAAK,EAAE,IAAI;QACX,WAAW,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE,CAAC,WAAW,EAAE;QAC5E,KAAK,EAAE,UAAU,CAAC,KAAK;KACxB,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,MAG9B;IACC,MAAM,KAAK,GAAG,+CAA+C,CAAC;IAC9D,IAAI,MAAM,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,EAAE,CAAC;QACpD,OAAO,GAAG,KAAK,4CAA4C,CAAC;IAC9D,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,CAAC,YAAY,KAAK,IAAI;QAAE,OAAO,GAAG,KAAK,uCAAuC,CAAC;IAChG,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;IAChC,MAAM,MAAM,GACV,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,CAAE,IAA6B,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAChG,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACvD,OAAO,GAAG,KAAK,kDAAkD,CAAC;IACpE,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;QAAE,OAAO,eAAe,CAAC;IAC5D,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,eAAe,GACnB,wGAAwG,CAAC;AAE3G,gJAAgJ;AAChJ,SAAS,UAAU,CAAC,IAAa;IAC/B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACtD,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IAC5D,OAAO,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,KAAK,IAAI,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACpF,CAAC","sourcesContent":["// Review in the site preview, on the dev server's side: everything the\n// toolbar app asks for, answered over Astro's dev toolbar channel. Every\n// `git`, `ascribe`, and `gh` call happens here; the page gets the changes and\n// the threads, never a token. There are no HTTP routes: any page open in the\n// same browser could call a route on localhost, while the toolbar's channel\n// takes only clients that have the dev server's token (`channelProblem`).\n//\n// Nothing runs until the reviewer turns the app on. Then the server opens\n// the pull request's review (when the GitHub CLI finds one), compares the\n// build with the pull request's base (or the default branch), and compares\n// again after each rebuild.\nimport path from \"node:path\";\nimport { readFile } from \"node:fs/promises\";\nimport type { OverlayMethod, PromptRequest, ReviewSession } from \"@ascribed/review/github\";\nimport type { PageRef } from \"@ascribed/review/place\";\nimport type { DiffResult } from \"./diff.js\";\nimport { messageOf, type Connection } from \"./github.js\";\nimport {\n CHANGED_EVENT,\n REQUEST_EVENT,\n RESULT_EVENT,\n type ChangedPage,\n type PageDiff,\n type PageView,\n type Request,\n type Result,\n type Status,\n type ThreadsState,\n} from \"./protocol.js\";\nimport { normalizeRoute, type RoutedPage } from \"./routes.js\";\n\n/** A page's end of the channel: the server answers it alone. */\nexport interface ChannelClient {\n send(event: string, payload?: unknown): void;\n}\n\n/**\n * The toolbar's channel, as `astro:server:setup`'s `toolbar` has it. Its\n * listeners get the sending page's client too (it is Vite's `server.hot`);\n * without one, an answer goes to every page, which tell theirs by `tab`.\n */\nexport interface ToolbarChannel {\n on(event: string, callback: (data: unknown, client?: ChannelClient) => void): void;\n send(event: string, payload: unknown): void;\n}\n\nexport interface ReviewServerOptions {\n channel: ToolbarChannel;\n logger: { info(message: string): void; warn(message: string): void };\n build: string;\n /** The content root, absolute. */\n contentRoot: string;\n /** Why someone else could use the channel, if they could: then GitHub stays off. */\n channelProblem: string | undefined;\n /** Compares the build with `base` (the default branch when `undefined`). */\n diff(base: string | undefined): Promise<DiffResult>;\n /**\n * `ascribe diff`'s agent prompt about the page or fragment at a content\n * path, against `base`; `undefined` when it didn't change.\n */\n prompt(path: string, base: string | undefined): Promise<string | undefined>;\n /** The project's folder: where its `ascribe.toml` is. */\n projectDir: string;\n /** Opens the pull request's review. */\n connect(): Promise<Connection>;\n /** Writes the build's JSON output, in turn with the dev server's builds: it has the routes. */\n writeRoutes(): Promise<void>;\n /** Every page of the build, by normalized route. */\n readRoutes(): Promise<Map<string, RoutedPage>>;\n /** How long to wait after a rebuild before comparing again. Default 150 ms. */\n debounceMs?: number;\n}\n\n/** The overlay's requests, which go to the session. */\nconst OVERLAY: ReadonlySet<string> = new Set<OverlayMethod>([\n \"load\",\n \"commentTarget\",\n \"comment\",\n \"reply\",\n \"allThreads\",\n \"resolve\",\n \"submit\",\n \"discard\",\n]);\n\nexport class ReviewServer {\n private on = false;\n private problem: string | undefined;\n private connection: Connection | undefined;\n private connecting: Promise<Connection> | undefined;\n private diffed: { result: DiffResult } | { error: string } | undefined;\n private diffing: Promise<void> | undefined;\n private stale = false;\n private timer: ReturnType<typeof setTimeout> | undefined;\n private routes: Promise<Map<string, RoutedPage>> | undefined;\n /** The page each overlay last read, by content path: comments are made on it. */\n private readonly pages = new Map<string, PageRef>();\n /** How long the last comparison took, in milliseconds. */\n lastDiffMs: number | undefined;\n\n constructor(private readonly options: ReviewServerOptions) {\n this.problem = options.channelProblem;\n options.channel.on(REQUEST_EVENT, (data, client) => {\n void this.receive(data, client);\n });\n }\n\n /**\n * The address the dev server listens on, once it does: one other machines\n * can reach turns comments off, as `server.host` does.\n */\n listening(address: string): void {\n if (this.problem !== undefined || isLoopback(address)) return;\n this.problem = NETWORK_PROBLEM;\n this.connection = undefined;\n this.connecting = undefined;\n }\n\n /** Whether review is on: then the dev server's builds write JSON output too. */\n get active(): boolean {\n return this.on;\n }\n\n /** The dev server rebuilt: compare again soon, and read the routes again when asked. */\n rebuilt(): void {\n if (!this.on) return;\n this.stale = true;\n this.routes = undefined;\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => {\n this.timer = undefined;\n void this.changes();\n }, this.options.debounceMs ?? 150);\n }\n\n /** The dev server stopped. */\n dispose(): void {\n if (this.timer) clearTimeout(this.timer);\n this.on = false;\n }\n\n private async receive(data: unknown, client: ChannelClient | undefined): Promise<void> {\n const request = data as Partial<Request> | null;\n if (typeof request?.id !== \"number\" || typeof request.method !== \"string\") return;\n const params =\n typeof request.params === \"object\" && request.params !== null ? request.params : {};\n const tab = typeof request.tab === \"string\" ? request.tab : \"\";\n const answer: Result = { tab, id: request.id };\n try {\n answer.result = await this.handle(request.method, params, tab);\n } catch (error) {\n const code = (error as { code?: unknown }).code;\n answer.error =\n typeof code === \"string\"\n ? { message: messageOf(error), code }\n : { message: messageOf(error) };\n }\n if (typeof client?.send === \"function\") client.send(RESULT_EVENT, answer);\n else this.options.channel.send(RESULT_EVENT, answer);\n }\n\n /** Answers one request. Rejects with a sentence to show. */\n async handle(method: string, params: Record<string, unknown>, tab = \"\"): Promise<unknown> {\n switch (method) {\n case \"status\":\n return { on: this.on } satisfies Status;\n case \"start\":\n await this.start();\n return { on: true } satisfies Status;\n case \"stop\":\n this.stop();\n this.changed(tab);\n return { on: false } satisfies Status;\n case \"page\":\n return this.pageView(params);\n case \"refresh\":\n await this.refresh();\n this.changed(tab);\n return null;\n case \"prompt\":\n return (await this.prompt(params)) ?? null;\n }\n if (!OVERLAY.has(method)) throw new Error(`Unknown request: ${method}.`);\n const connection = this.on ? await this.connected() : undefined;\n if (connection?.state !== \"on\") throw new Error(\"Review comments aren't on.\");\n const github = await import(\"@ascribed/review/github\");\n const pagePath = typeof params[\"path\"] === \"string\" ? params[\"path\"] : undefined;\n const answer = await github.answerRequest(\n {\n session: connection.session,\n page: pagePath === undefined ? undefined : { build: this.options.build, path: pagePath },\n lastPage: pagePath === undefined ? undefined : this.pages.get(pagePath),\n changedPages: async () => {\n const [changes, routes] = await Promise.all([this.changes(), this.readRoutes()]);\n if (!(\"result\" in changes)) return [];\n return changes.result.pages.map((p) => listed(p, routes.get(normalizeRoute(p.route))));\n },\n },\n method as OverlayMethod,\n params,\n );\n if (answer.page) this.pages.set(answer.page.path, answer.page);\n if (answer.changed) this.changed(tab);\n return answer.result;\n }\n\n /**\n * The agent prompt the page asked for: a thread's, or every open one's,\n * from the pull request; the page's changes, or a fragment's reach, from\n * `ascribe diff`. `undefined` when there's nothing to prompt about.\n */\n private async prompt(params: Record<string, unknown>): Promise<string | undefined> {\n if (!this.on) throw new Error(\"Review is off.\");\n const request = params[\"request\"] as PromptRequest | undefined;\n const pagePath = typeof params[\"path\"] === \"string\" ? params[\"path\"] : undefined;\n const connection = await this.connected();\n switch (request?.kind) {\n case \"thread\":\n case \"open-threads\": {\n if (connection.state !== \"on\") throw new Error(\"Review comments aren't on.\");\n const github = await import(\"@ascribed/review/github\");\n const contentRoot = this.options.contentRoot;\n return github.buildThreadsPrompt(\n {\n session: connection.session,\n project: github.promptProject(this.options.projectDir),\n changedPages: async () => {\n const changes = await this.changes();\n return \"result\" in changes ? changes.result.pages : [];\n },\n // The agent reads the files on disk, as the dev server does: no editor here.\n readSource: (file) =>\n readFile(path.join(contentRoot, ...file.split(\"/\")), \"utf8\").then(\n (text) => ({ text, unsaved: false }),\n () => undefined,\n ),\n },\n request,\n );\n }\n case \"page-changes\":\n case \"fragment-reach\": {\n const target = request.kind === \"page-changes\" ? pagePath : request.fragment;\n if (target === undefined) throw new Error(\"That isn't a page of the project.\");\n const base = connection.state === \"on\" ? connection.base : undefined;\n return this.options.prompt(target, base);\n }\n default:\n throw new Error(\"Unknown prompt.\");\n }\n }\n\n private async start(): Promise<void> {\n if (this.on) return;\n this.on = true;\n this.stale = true;\n this.routes = undefined;\n this.connection = undefined;\n const started = Date.now();\n // The routes come from the JSON output, which the next builds write too.\n const routes = this.options.writeRoutes().catch((error: unknown) => {\n this.options.logger.warn(`couldn't write the routes for review: ${messageOf(error)}`);\n });\n await this.connected();\n const changes = await this.changes();\n await routes;\n if (\"result\" in changes) {\n const base = changes.result.base;\n this.options.logger.info(\n `review: compared \"${this.options.build}\" with ${base.requested} in ${this.lastDiffMs ?? 0} ms (started in ${Date.now() - started} ms)`,\n );\n } else {\n this.options.logger.warn(`review: ${changes.error}`);\n }\n }\n\n private stop(): void {\n this.on = false;\n this.connection = undefined;\n this.connecting = undefined;\n this.diffed = undefined;\n this.routes = undefined;\n this.pages.clear();\n if (this.timer) clearTimeout(this.timer);\n this.timer = undefined;\n }\n\n private async refresh(): Promise<void> {\n if (!this.on) return;\n const connection = await this.connected();\n if (connection.state === \"on\") await connection.session.refresh();\n else this.connection = undefined;\n // A pull request found now changes the base.\n await this.connected();\n this.stale = true;\n await this.changes();\n }\n\n /** The pull request's review, opened once while review is on. */\n private connected(): Promise<Connection> {\n if (this.connection) return Promise.resolve(this.connection);\n if (this.problem !== undefined) {\n this.connection = {\n state: \"unprotected\",\n message: `Showing changes only. Comments are off because ${this.problem}.`,\n };\n return Promise.resolve(this.connection);\n }\n this.connecting ??= this.options\n .connect()\n .catch((error: unknown): Connection => ({ state: \"error\", message: messageOf(error) }))\n .then((connection) => {\n this.connecting = undefined;\n // The server turned out to listen on the network meanwhile.\n if (this.problem !== undefined) return this.connected();\n if (this.on) this.connection = connection;\n return connection;\n });\n return this.connecting;\n }\n\n /** The build's changes, compared again first if a rebuild made them stale. */\n private async changes(): Promise<{ result: DiffResult } | { error: string }> {\n while (this.diffing) await this.diffing;\n if (this.stale || this.diffed === undefined) {\n this.stale = false;\n this.diffing = this.compare().finally(() => {\n this.diffing = undefined;\n });\n await this.diffing;\n }\n return this.diffed ?? { error: \"Review is off.\" };\n }\n\n private async compare(): Promise<void> {\n const connection = await this.connected();\n const base = connection.state === \"on\" ? connection.base : undefined;\n const started = Date.now();\n try {\n this.diffed = { result: await this.options.diff(base) };\n } catch (error) {\n this.diffed = { error: messageOf(error) };\n }\n this.lastDiffMs = Date.now() - started;\n }\n\n private async pageView(params: Record<string, unknown>): Promise<PageView> {\n if (!this.on) throw new Error(\"Review is off.\");\n const route = normalizeRoute(typeof params[\"route\"] === \"string\" ? params[\"route\"] : \"/\");\n const fromPage = typeof params[\"path\"] === \"string\" ? params[\"path\"] : null;\n const [changes, connection, routes] = await Promise.all([\n this.changes(),\n this.connected(),\n this.readRoutes(),\n ]);\n // The line the route is in, for a versioned build: its changes are the ones shown.\n const pages = \"result\" in changes ? linePages(changes.result, route, routes) : [];\n const shown = pages.filter((p) => p.status !== \"removed\");\n let page = shown.find((p) => normalizeRoute(p.route) === route) ?? null;\n const pagePath = page?.path ?? fromPage ?? routes.get(route)?.path ?? null;\n if (page === null && pagePath !== null) page = shown.find((p) => p.path === pagePath) ?? null;\n return {\n kind: pagePath === null ? \"not-page\" : \"page\",\n path: pagePath,\n page,\n base: \"result\" in changes ? changes.result.base : null,\n problem: \"error\" in changes ? changes.error : null,\n errors: \"result\" in changes ? changes.result.errors : 0,\n threads: threadsState(connection),\n changedPages: pages.map((p) => listed(p, routes.get(normalizeRoute(p.route)))),\n contentRoot: this.options.contentRoot,\n separator: path.sep,\n };\n }\n\n /** The build's pages by route, read once until the next rebuild. */\n private readRoutes(): Promise<Map<string, RoutedPage>> {\n this.routes ??= this.options.readRoutes().catch(() => new Map<string, RoutedPage>());\n return this.routes;\n }\n\n /** Tells every page the threads or the changes changed; `from` is the page that did it. */\n private changed(from: string): void {\n this.options.channel.send(CHANGED_EVENT, { from });\n }\n}\n\n/**\n * The changed pages of the line `route` is in: of the line whose changed\n * page or JSON output has the route, else the current line's. For a build\n * that isn't versioned, its changed pages.\n */\nfunction linePages(result: DiffResult, route: string, routes: Map<string, RoutedPage>): PageDiff[] {\n const lines = result.lines;\n if (lines === undefined) return result.pages;\n const changed = lines.find((l) => l.pages.some((p) => normalizeRoute(p.route) === route));\n if (changed) return changed.pages;\n const build = routes.get(route)?.build;\n return lines.find((l) => l.build === build)?.pages ?? result.pages;\n}\n\n/** A changed page as the list shows it: without its changes, with its title. */\nfunction listed(page: PageDiff, routed: RoutedPage | undefined): ChangedPage {\n return {\n title: routed?.title ?? null,\n formatted_title: routed?.formatted_title ?? null,\n path: page.path,\n route: page.route,\n status: page.status,\n own_file_changed: page.own_file_changed,\n because: page.because,\n page_changed: page.page_changed,\n counts: page.counts,\n };\n}\n\n/** What the page shows about the threads. */\nfunction threadsState(connection: Connection): ThreadsState {\n if (connection.state !== \"on\") return connection;\n const pr: ReviewSession[\"pullRequest\"] = connection.session.pullRequest;\n return {\n state: \"on\",\n pullRequest: { number: pr.number, url: pr.url, baseRefName: pr.baseRefName },\n local: connection.local,\n };\n}\n\n/**\n * Why someone other than the developer could use the dev server's toolbar\n * channel, or `undefined` when they can't. Vite takes a browser's connection\n * only with the token in its client script, which the page's own origin can\n * read and, by default, only localhost origins can fetch. These settings undo\n * that, and a server listening on the network gives its pages, token and all,\n * to anyone who can reach it.\n */\nexport function channelProblem(config: {\n server: { cors?: unknown; allowedHosts?: unknown; host?: unknown };\n legacy?: { skipWebSocketTokenCheck?: unknown } | undefined;\n}): string | undefined {\n const pages = \"other web pages could talk to this dev server\";\n if (config.legacy?.skipWebSocketTokenCheck === true) {\n return `${pages}: \\`legacy.skipWebSocketTokenCheck\\` is on`;\n }\n if (config.server.allowedHosts === true) return `${pages}: \\`server.allowedHosts\\` is \\`true\\``;\n const cors = config.server.cors;\n const origin =\n typeof cors === \"object\" && cors !== null ? (cors as { origin?: unknown }).origin : undefined;\n if (cors === true || origin === true || origin === \"*\") {\n return `${pages}: \\`server.cors\\` lets any origin read its pages`;\n }\n if (!isLoopback(config.server.host)) return NETWORK_PROBLEM;\n return undefined;\n}\n\nconst NETWORK_PROBLEM =\n \"the dev server is listening on the network (`--host`), so anyone who can reach it could comment as you\";\n\n/** Whether Vite's `server.host`, or the address the server listens on, keeps the server on this machine: unset, `false`, or a loopback name. */\nfunction isLoopback(host: unknown): boolean {\n if (host === undefined || host === false) return true;\n if (typeof host !== \"string\") return false;\n const name = host.toLowerCase().replace(/^\\[(.*)\\]$/, \"$1\");\n return name === \"localhost\" || name === \"::1\" || /^127(\\.\\d{1,3}){3}$/.test(name);\n}\n"]}
package/dist/run.d.ts CHANGED
@@ -21,13 +21,14 @@ export declare function anchorsFor(anchors: boolean | "dev" | undefined, command
21
21
  /** Whether an Astro command has review in the site preview, given the `review` option (an entry's name is on). */
22
22
  export declare function reviewFor(review: boolean | string | undefined, command: string): boolean;
23
23
  /**
24
- * Builds one build's outputs, the site output by default. Rejects, with the compiler's report as the
24
+ * Builds one build's outputs, or several builds', the site output by default. Rejects, with the compiler's report as the
25
25
  * message, if the build has errors: the Astro build fails with them.
26
26
  */
27
27
  export declare function runBuild(options: {
28
28
  binary: string;
29
29
  configPath: string;
30
- build: string;
30
+ /** The build, or several, each a `--build`. */
31
+ build: string | readonly string[];
31
32
  cwd: string;
32
33
  /** Write source anchors (`--anchors`). */
33
34
  anchors?: boolean;
package/dist/run.js CHANGED
@@ -31,16 +31,16 @@ export function reviewFor(review, command) {
31
31
  return command === "dev" && review !== false;
32
32
  }
33
33
  /**
34
- * Builds one build's outputs, the site output by default. Rejects, with the compiler's report as the
34
+ * Builds one build's outputs, or several builds', the site output by default. Rejects, with the compiler's report as the
35
35
  * message, if the build has errors: the Astro build fails with them.
36
36
  */
37
37
  export function runBuild(options) {
38
+ const builds = typeof options.build === "string" ? [options.build] : options.build;
38
39
  const args = [
39
40
  "build",
40
41
  "--emit",
41
42
  (options.outputs ?? ["site"]).join(","),
42
- "--build",
43
- options.build,
43
+ ...builds.flatMap((build) => ["--build", build]),
44
44
  "--config",
45
45
  options.configPath,
46
46
  "--color",
@@ -52,7 +52,7 @@ export function runBuild(options) {
52
52
  execFile(options.binary, args, { cwd: options.cwd, maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
53
53
  if (error !== null) {
54
54
  const report = `${stdout}${stderr}`.trim();
55
- reject(new Error(`\`ascribe build --build ${options.build}\` failed${report === "" ? `: ${error.message}` : `:\n${report}`}`, { cause: error }));
55
+ reject(new Error(`\`ascribe build ${builds.map((build) => `--build ${build}`).join(" ")}\` failed${report === "" ? `: ${error.message}` : `:\n${report}`}`, { cause: error }));
56
56
  return;
57
57
  }
58
58
  resolve({ diagnostics: stdout.trim(), summary: stderr.trim() });