@ascribed/astro 0.2.1-next.15 → 0.2.1-next.17

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.
Files changed (51) hide show
  1. package/README.md +21 -7
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +62 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/content.d.ts +27 -0
  6. package/dist/content.js +71 -1
  7. package/dist/content.js.map +1 -1
  8. package/dist/files.d.ts +5 -2
  9. package/dist/files.js +13 -5
  10. package/dist/files.js.map +1 -1
  11. package/dist/head-check.d.ts +6 -0
  12. package/dist/head-check.js +28 -0
  13. package/dist/head-check.js.map +1 -0
  14. package/dist/head.d.ts +54 -0
  15. package/dist/head.js +114 -0
  16. package/dist/head.js.map +1 -0
  17. package/dist/headers.d.ts +50 -0
  18. package/dist/headers.js +155 -0
  19. package/dist/headers.js.map +1 -0
  20. package/dist/index.d.ts +12 -1
  21. package/dist/index.js +124 -14
  22. package/dist/index.js.map +1 -1
  23. package/dist/init.d.ts +41 -0
  24. package/dist/init.js +130 -0
  25. package/dist/init.js.map +1 -0
  26. package/dist/nav.d.ts +51 -0
  27. package/dist/nav.js +97 -0
  28. package/dist/nav.js.map +1 -0
  29. package/dist/permalinks.d.ts +6 -0
  30. package/dist/permalinks.js +13 -0
  31. package/dist/permalinks.js.map +1 -1
  32. package/dist/project.d.ts +26 -0
  33. package/dist/project.js +32 -0
  34. package/dist/project.js.map +1 -1
  35. package/dist/projects.d.ts +0 -6
  36. package/dist/projects.js +1 -14
  37. package/dist/projects.js.map +1 -1
  38. package/dist/records.d.ts +136 -0
  39. package/dist/records.js +182 -0
  40. package/dist/records.js.map +1 -0
  41. package/dist/root-files.d.ts +68 -0
  42. package/dist/root-files.js +103 -0
  43. package/dist/root-files.js.map +1 -0
  44. package/dist/run.js +3 -2
  45. package/dist/run.js.map +1 -1
  46. package/package.json +12 -6
  47. package/src/Head.astro +58 -0
  48. package/starter/src/content.config.ts +8 -0
  49. package/starter/src/layouts/Docs.astro +87 -0
  50. package/starter/src/nav.ts +13 -0
  51. package/starter/src/pages/[...slug].astro +20 -0
@@ -0,0 +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"]}
@@ -0,0 +1,68 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type ProjectRobots, type SitemapSources } from "./records.js";
3
+ /** What the files are drawn from. */
4
+ export interface RootFileSources extends Omit<SitemapSources, "site"> {
5
+ /** What each project says to crawlers, each project once. */
6
+ robots: readonly ProjectRobots[];
7
+ /** Astro's `site`, when it's set. */
8
+ site: string | undefined;
9
+ /** Astro's `base`. */
10
+ base: string;
11
+ /** Whether the site has its own `robots.txt`, in `public/`. */
12
+ ownRobots: boolean;
13
+ /** Whether the site has its own `sitemap.xml`, in `public/`. */
14
+ ownSitemap: boolean;
15
+ }
16
+ /** A thing the build says about the files, and how loudly. */
17
+ interface RootFileNote {
18
+ level: "info" | "warn";
19
+ text: string;
20
+ }
21
+ /**
22
+ * The files, each `undefined` when none is written, what to say about them,
23
+ * and the problems that stop `robots.txt` from being written, which fail a
24
+ * build.
25
+ */
26
+ export interface RootFiles {
27
+ sitemap: string | undefined;
28
+ robots: string | undefined;
29
+ notes: RootFileNote[];
30
+ problems: string[];
31
+ }
32
+ /** The sitemap and `robots.txt` for a site, and what the build says about them. */
33
+ export declare function rootFiles(sources: RootFileSources): RootFiles;
34
+ type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
35
+ /**
36
+ * A dev-server middleware serving the sitemap at `<base>sitemap.xml` and
37
+ * `robots.txt` at `/`, each as `files` has it when it's asked for, so each
38
+ * rebuild's records are served.
39
+ */
40
+ export declare function rootFilesMiddleware(base: string, files: () => RootFiles): Middleware;
41
+ /** A route as `astro:routes:resolved` gives it, as much as these read. */
42
+ export interface AstroRoute {
43
+ pattern: string;
44
+ patternRegex: RegExp;
45
+ params: string[];
46
+ type: string;
47
+ origin: string;
48
+ }
49
+ /** Astro's routing, as much as these read. */
50
+ interface Routing {
51
+ base: string;
52
+ trailingSlash: "always" | "never" | "ignore";
53
+ }
54
+ /**
55
+ * The site's own pages `astro dev` can name: each route of the site's own
56
+ * with no parameters, other than the not-found and error pages, by its route
57
+ * from the host's root. A page a route's `getStaticPaths` lists is known only
58
+ * to a build.
59
+ */
60
+ export declare function devSitePages(routes: readonly AstroRoute[], astro: Routing): string[];
61
+ /**
62
+ * Whether a project's page, by its route from the host's root, has a route
63
+ * in the site in `astro dev`: whether one of the site's own routes matches
64
+ * it. A project no route shows, such as one whose collection a page only
65
+ * quotes from, has no pages in the sitemap.
66
+ */
67
+ export declare function devRouted(routes: readonly AstroRoute[], astro: Routing): (route: string) => boolean;
68
+ export {};
@@ -0,0 +1,103 @@
1
+ import { normalizeBase } from "./project.js";
2
+ import { purposeConflicts, robotsText, routeAs, routeKey, sitemapAddresses, sitemapRoute, sitemapXml, } from "./records.js";
3
+ /** The sitemap and `robots.txt` for a site, and what the build says about them. */
4
+ export function rootFiles(sources) {
5
+ const notes = [];
6
+ const base = normalizeBase(sources.base);
7
+ let sitemap;
8
+ let sitemapAddress;
9
+ if (sources.ownSitemap) {
10
+ // The site's own, at the same address, which robots.txt points to when there's an address.
11
+ notes.push({ level: "info", text: "no sitemap.xml: public/sitemap.xml is the site's own." });
12
+ if (sources.site !== undefined)
13
+ sitemapAddress = new URL(sitemapRoute(base), sources.site).href;
14
+ }
15
+ else if (sources.site === undefined) {
16
+ notes.push({
17
+ level: "warn",
18
+ text: "no sitemap.xml: a sitemap lists pages by their absolute addresses, and the site has no address. Set [consumer] site in ascribe.toml, or Astro's `site`.",
19
+ });
20
+ }
21
+ else {
22
+ sitemap = sitemapXml(sitemapAddresses({ ...sources, site: sources.site }));
23
+ sitemapAddress = new URL(sitemapRoute(base), sources.site).href;
24
+ }
25
+ const problems = purposeConflicts(sources.robots);
26
+ let robots = problems.length > 0
27
+ ? undefined
28
+ : robotsText(sources.robots.map((project) => project.robots), sitemapAddress);
29
+ if (robots !== undefined && base !== "/") {
30
+ notes.push({
31
+ level: "info",
32
+ text: `no robots.txt: Astro's \`base\` is "${base}", and a crawler reads robots.txt only at the host's root. Put these lines in the host's own:\n${robots}`,
33
+ });
34
+ robots = undefined;
35
+ }
36
+ else if (robots !== undefined && sources.ownRobots) {
37
+ notes.push({
38
+ level: "info",
39
+ text: `no robots.txt: public/robots.txt is the site's own. Ascribe's lines for it:\n${robots}`,
40
+ });
41
+ robots = undefined;
42
+ }
43
+ return { sitemap, robots, notes, problems };
44
+ }
45
+ /**
46
+ * A dev-server middleware serving the sitemap at `<base>sitemap.xml` and
47
+ * `robots.txt` at `/`, each as `files` has it when it's asked for, so each
48
+ * rebuild's records are served.
49
+ */
50
+ export function rootFilesMiddleware(base, files) {
51
+ const sitemapPath = sitemapRoute(base);
52
+ return (req, res, next) => {
53
+ if (req.method !== "GET" && req.method !== "HEAD")
54
+ return next();
55
+ // Vite strips the base from `req.url`; connect keeps the original in `originalUrl`.
56
+ const pathname = (req.originalUrl ?? req.url ?? "").split(/[?#]/, 1)[0] ?? "";
57
+ if (pathname !== sitemapPath && pathname !== "/robots.txt")
58
+ return next();
59
+ const current = files();
60
+ const [text, type] = pathname === sitemapPath
61
+ ? [current.sitemap, "application/xml; charset=utf-8"]
62
+ : [current.robots, "text/plain; charset=utf-8"];
63
+ if (text === undefined)
64
+ return next();
65
+ res.setHeader("Content-Type", type);
66
+ res.setHeader("Content-Length", Buffer.byteLength(text));
67
+ res.end(req.method === "HEAD" ? undefined : text);
68
+ };
69
+ }
70
+ /**
71
+ * The site's own pages `astro dev` can name: each route of the site's own
72
+ * with no parameters, other than the not-found and error pages, by its route
73
+ * from the host's root. A page a route's `getStaticPaths` lists is known only
74
+ * to a build.
75
+ */
76
+ export function devSitePages(routes, astro) {
77
+ const root = normalizeBase(astro.base);
78
+ return routes
79
+ .filter((route) => route.type === "page" &&
80
+ route.origin === "project" &&
81
+ route.params.length === 0 &&
82
+ route.pattern !== "/404" &&
83
+ route.pattern !== "/500")
84
+ .map((route) => routeAs(`${root}${route.pattern.replace(/^\//, "")}`, astro.trailingSlash));
85
+ }
86
+ /**
87
+ * Whether a project's page, by its route from the host's root, has a route
88
+ * in the site in `astro dev`: whether one of the site's own routes matches
89
+ * it. A project no route shows, such as one whose collection a page only
90
+ * quotes from, has no pages in the sitemap.
91
+ */
92
+ export function devRouted(routes, astro) {
93
+ const root = normalizeBase(astro.base);
94
+ const pages = routes.filter((route) => route.type === "page" && route.origin === "project");
95
+ return (route) => {
96
+ if (!`${route}/`.startsWith(root))
97
+ return false;
98
+ const under = `/${route.slice(root.length)}`;
99
+ const forms = new Set([under, routeKey(under), `${routeKey(under)}/`]);
100
+ return pages.some((page) => [...forms].some((form) => page.patternRegex.test(form)));
101
+ };
102
+ }
103
+ //# sourceMappingURL=root-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-files.js","sourceRoot":"","sources":["../src/root-files.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,OAAO,EACP,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,UAAU,GAGX,MAAM,cAAc,CAAC;AAkCtB,mFAAmF;AACnF,MAAM,UAAU,SAAS,CAAC,OAAwB;IAChD,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,OAA2B,CAAC;IAChC,IAAI,cAAkC,CAAC;IACvC,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACvB,2FAA2F;QAC3F,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,uDAAuD,EAAE,CAAC,CAAC;QAC7F,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS;YAAE,cAAc,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;IAClG,CAAC;SAAM,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC;YACT,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,yJAAyJ;SAChK,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,UAAU,CAAC,gBAAgB,CAAC,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC3E,cAAc,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;IAClE,CAAC;IACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,IAAI,MAAM,GACR,QAAQ,CAAC,MAAM,GAAG,CAAC;QACjB,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,UAAU,CACR,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAC/C,cAAc,CACf,CAAC;IACR,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;QACzC,KAAK,CAAC,IAAI,CAAC;YACT,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,uCAAuC,IAAI,kGAAkG,MAAM,EAAE;SAC5J,CAAC,CAAC;QACH,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;SAAM,IAAI,MAAM,KAAK,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACrD,KAAK,CAAC,IAAI,CAAC;YACT,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,gFAAgF,MAAM,EAAE;SAC/F,CAAC,CAAC;QACH,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;AAC9C,CAAC;AAID;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,KAAsB;IACtE,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;YAAE,OAAO,IAAI,EAAE,CAAC;QACjE,oFAAoF;QACpF,MAAM,QAAQ,GACZ,CAAE,GAAgC,CAAC,WAAW,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7F,IAAI,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,aAAa;YAAE,OAAO,IAAI,EAAE,CAAC;QAC1E,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;QACxB,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAChB,QAAQ,KAAK,WAAW;YACtB,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,gCAAgC,CAAC;YACrD,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;QACpD,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,EAAE,CAAC;QACtC,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;QACpC,GAAG,CAAC,SAAS,CAAC,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QACzD,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC,CAAC;AACJ,CAAC;AAiBD;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAA6B,EAAE,KAAc;IACxE,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,MAAM;SACV,MAAM,CACL,CAAC,KAAK,EAAE,EAAE,CACR,KAAK,CAAC,IAAI,KAAK,MAAM;QACrB,KAAK,CAAC,MAAM,KAAK,SAAS;QAC1B,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;QACzB,KAAK,CAAC,OAAO,KAAK,MAAM;QACxB,KAAK,CAAC,OAAO,KAAK,MAAM,CAC3B;SACA,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC;AAChG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CACvB,MAA6B,EAC7B,KAAc;IAEd,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC;IAC5F,OAAO,CAAC,KAAK,EAAE,EAAE;QACf,IAAI,CAAC,GAAG,KAAK,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QAChD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QACvE,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvF,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// The site's sitemap and `robots.txt`, from every project's page records\n// together: one of each for the site, at the host's root, which is above the\n// base path of every project that isn't mounted there. `astro build` writes\n// them into its output, and `astro dev` serves them.\n//\n// A site whose Astro `base` isn't `/` has no host root: its sitemap goes\n// under the base, and no `robots.txt` is written, since one under a path does\n// nothing. The build says what the host's own needs instead.\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { normalizeBase } from \"./project.js\";\nimport {\n purposeConflicts,\n robotsText,\n routeAs,\n routeKey,\n sitemapAddresses,\n sitemapRoute,\n sitemapXml,\n type ProjectRobots,\n type SitemapSources,\n} from \"./records.js\";\n\n/** What the files are drawn from. */\nexport interface RootFileSources extends Omit<SitemapSources, \"site\"> {\n /** What each project says to crawlers, each project once. */\n robots: readonly ProjectRobots[];\n /** Astro's `site`, when it's set. */\n site: string | undefined;\n /** Astro's `base`. */\n base: string;\n /** Whether the site has its own `robots.txt`, in `public/`. */\n ownRobots: boolean;\n /** Whether the site has its own `sitemap.xml`, in `public/`. */\n ownSitemap: boolean;\n}\n\n/** A thing the build says about the files, and how loudly. */\ninterface RootFileNote {\n level: \"info\" | \"warn\";\n text: string;\n}\n\n/**\n * The files, each `undefined` when none is written, what to say about them,\n * and the problems that stop `robots.txt` from being written, which fail a\n * build.\n */\nexport interface RootFiles {\n sitemap: string | undefined;\n robots: string | undefined;\n notes: RootFileNote[];\n problems: string[];\n}\n\n/** The sitemap and `robots.txt` for a site, and what the build says about them. */\nexport function rootFiles(sources: RootFileSources): RootFiles {\n const notes: RootFileNote[] = [];\n const base = normalizeBase(sources.base);\n let sitemap: string | undefined;\n let sitemapAddress: string | undefined;\n if (sources.ownSitemap) {\n // The site's own, at the same address, which robots.txt points to when there's an address.\n notes.push({ level: \"info\", text: \"no sitemap.xml: public/sitemap.xml is the site's own.\" });\n if (sources.site !== undefined) sitemapAddress = new URL(sitemapRoute(base), sources.site).href;\n } else if (sources.site === undefined) {\n notes.push({\n level: \"warn\",\n text: \"no sitemap.xml: a sitemap lists pages by their absolute addresses, and the site has no address. Set [consumer] site in ascribe.toml, or Astro's `site`.\",\n });\n } else {\n sitemap = sitemapXml(sitemapAddresses({ ...sources, site: sources.site }));\n sitemapAddress = new URL(sitemapRoute(base), sources.site).href;\n }\n const problems = purposeConflicts(sources.robots);\n let robots =\n problems.length > 0\n ? undefined\n : robotsText(\n sources.robots.map((project) => project.robots),\n sitemapAddress,\n );\n if (robots !== undefined && base !== \"/\") {\n notes.push({\n level: \"info\",\n text: `no robots.txt: Astro's \\`base\\` is \"${base}\", and a crawler reads robots.txt only at the host's root. Put these lines in the host's own:\\n${robots}`,\n });\n robots = undefined;\n } else if (robots !== undefined && sources.ownRobots) {\n notes.push({\n level: \"info\",\n text: `no robots.txt: public/robots.txt is the site's own. Ascribe's lines for it:\\n${robots}`,\n });\n robots = undefined;\n }\n return { sitemap, robots, notes, problems };\n}\n\ntype Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;\n\n/**\n * A dev-server middleware serving the sitemap at `<base>sitemap.xml` and\n * `robots.txt` at `/`, each as `files` has it when it's asked for, so each\n * rebuild's records are served.\n */\nexport function rootFilesMiddleware(base: string, files: () => RootFiles): Middleware {\n const sitemapPath = sitemapRoute(base);\n return (req, res, next) => {\n if (req.method !== \"GET\" && req.method !== \"HEAD\") return next();\n // Vite strips the base from `req.url`; connect keeps the original in `originalUrl`.\n const pathname =\n ((req as { originalUrl?: string }).originalUrl ?? req.url ?? \"\").split(/[?#]/, 1)[0] ?? \"\";\n if (pathname !== sitemapPath && pathname !== \"/robots.txt\") return next();\n const current = files();\n const [text, type] =\n pathname === sitemapPath\n ? [current.sitemap, \"application/xml; charset=utf-8\"]\n : [current.robots, \"text/plain; charset=utf-8\"];\n if (text === undefined) return next();\n res.setHeader(\"Content-Type\", type);\n res.setHeader(\"Content-Length\", Buffer.byteLength(text));\n res.end(req.method === \"HEAD\" ? undefined : text);\n };\n}\n\n/** A route as `astro:routes:resolved` gives it, as much as these read. */\nexport interface AstroRoute {\n pattern: string;\n patternRegex: RegExp;\n params: string[];\n type: string;\n origin: string;\n}\n\n/** Astro's routing, as much as these read. */\ninterface Routing {\n base: string;\n trailingSlash: \"always\" | \"never\" | \"ignore\";\n}\n\n/**\n * The site's own pages `astro dev` can name: each route of the site's own\n * with no parameters, other than the not-found and error pages, by its route\n * from the host's root. A page a route's `getStaticPaths` lists is known only\n * to a build.\n */\nexport function devSitePages(routes: readonly AstroRoute[], astro: Routing): string[] {\n const root = normalizeBase(astro.base);\n return routes\n .filter(\n (route) =>\n route.type === \"page\" &&\n route.origin === \"project\" &&\n route.params.length === 0 &&\n route.pattern !== \"/404\" &&\n route.pattern !== \"/500\",\n )\n .map((route) => routeAs(`${root}${route.pattern.replace(/^\\//, \"\")}`, astro.trailingSlash));\n}\n\n/**\n * Whether a project's page, by its route from the host's root, has a route\n * in the site in `astro dev`: whether one of the site's own routes matches\n * it. A project no route shows, such as one whose collection a page only\n * quotes from, has no pages in the sitemap.\n */\nexport function devRouted(\n routes: readonly AstroRoute[],\n astro: Routing,\n): (route: string) => boolean {\n const root = normalizeBase(astro.base);\n const pages = routes.filter((route) => route.type === \"page\" && route.origin === \"project\");\n return (route) => {\n if (!`${route}/`.startsWith(root)) return false;\n const under = `/${route.slice(root.length)}`;\n const forms = new Set([under, routeKey(under), `${routeKey(under)}/`]);\n return pages.some((page) => [...forms].some((form) => page.patternRegex.test(form)));\n };\n}\n"]}
package/dist/run.js CHANGED
@@ -2,8 +2,9 @@
2
2
  // its JSON output while review is on.
3
3
  import { execFile } from "node:child_process";
4
4
  /**
5
- * A check's summary line, the report's last: `checked 14 files: 0 errors, 0 warnings`,
6
- * with `, 3 advice` after when there is advice.
5
+ * A check's summary line, the report's last: `checked 14 files for build \`site\`, reported on 14:
6
+ * 0 errors, 0 warnings, 0 advice`. An older binary wrote `checked 14 files: 0 errors,
7
+ * 0 warnings`, with `, 3 advice` after only when there was advice.
7
8
  */
8
9
  const SUMMARY = /^checked [^:\n]*: (\d+) errors?, (\d+) warnings?(?:, \d+ advice)?$/;
9
10
  /**
package/dist/run.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,sCAAsC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAU9C;;;GAGG;AACH,MAAM,OAAO,GAAG,oEAAoE,CAAC;AAErF;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,WAAmB;IAC7C,IAAI,WAAW,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IACrC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7E,OAAO,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;AACtE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,OAAoC,EACpC,OAAe,EACf,MAAM,GAAG,KAAK;IAEd,OAAO,OAAO,KAAK,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,IAAI,CAAC,OAAO,KAAK,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC;AAClF,CAAC;AAED,kHAAkH;AAClH,MAAM,UAAU,SAAS,CAAC,MAAoC,EAAE,OAAe;IAC7E,OAAO,OAAO,KAAK,KAAK,IAAI,MAAM,KAAK,KAAK,CAAC;AAC/C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,OASxB;IACC,MAAM,IAAI,GAAG;QACX,OAAO;QACP,QAAQ;QACR,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QACvC,SAAS;QACT,OAAO,CAAC,KAAK;QACb,UAAU;QACV,OAAO,CAAC,UAAU;QAClB,SAAS;QACT,OAAO;KACR,CAAC;IACF,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI;QAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACrD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CACN,OAAO,CAAC,MAAM,EACd,IAAI,EACJ,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,EACjD,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxB,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,MAAM,MAAM,GAAG,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC;gBAC3C,MAAM,CACJ,IAAI,KAAK,CACP,2BAA2B,OAAO,CAAC,KAAK,YAAY,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,EAAE,EAC3G,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YACD,OAAO,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAClE,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// Running `ascribe build` for the configured build: its site output, and\n// its JSON output while review is on.\nimport { execFile } from \"node:child_process\";\n\n/** What a successful build printed. */\nexport interface BuildResult {\n /** The checks' report, which is more than a summary line when there is anything to report. */\n diagnostics: string;\n /** The `built <build>/site: …` lines. */\n summary: string;\n}\n\n/**\n * A check's summary line, the report's last: `checked 14 files: 0 errors, 0 warnings`,\n * with `, 3 advice` after when there is advice.\n */\nconst SUMMARY = /^checked [^:\\n]*: (\\d+) errors?, (\\d+) warnings?(?:, \\d+ advice)?$/;\n\n/**\n * Whether a build's report is worth a warning: anything but a report whose\n * summary line counts no errors and no warnings, which is logged at info\n * level, advice and all.\n */\nexport function hasWarnings(diagnostics: string): boolean {\n if (diagnostics === \"\") return false;\n const summary = SUMMARY.exec(diagnostics.trimEnd().split(\"\\n\").at(-1) ?? \"\");\n return summary === null || summary[1] !== \"0\" || summary[2] !== \"0\";\n}\n\n/**\n * Whether an Astro command builds with source anchors, given the integration's\n * `anchors` option: `\"dev\"` means in `astro dev` only, so `astro build` has\n * them only when it is `true`. Review in the site preview (`review`) needs\n * them in `astro dev`.\n */\nexport function anchorsFor(\n anchors: boolean | \"dev\" | undefined,\n command: string,\n review = false,\n): boolean {\n return anchors === true || (command === \"dev\" && (anchors === \"dev\" || review));\n}\n\n/** Whether an Astro command has review in the site preview, given the `review` option (an entry's name is on). */\nexport function reviewFor(review: boolean | string | undefined, command: string): boolean {\n return command === \"dev\" && review !== false;\n}\n\n/**\n * Builds one build's outputs, the site output by default. Rejects, with the compiler's report as the\n * message, if the build has errors: the Astro build fails with them.\n */\nexport function runBuild(options: {\n binary: string;\n configPath: string;\n build: string;\n cwd: string;\n /** Write source anchors (`--anchors`). */\n anchors?: boolean;\n /** The outputs to write. Default: `[\"site\"]`. */\n outputs?: readonly (\"site\" | \"plain\" | \"json\")[];\n}): Promise<BuildResult> {\n const args = [\n \"build\",\n \"--emit\",\n (options.outputs ?? [\"site\"]).join(\",\"),\n \"--build\",\n options.build,\n \"--config\",\n options.configPath,\n \"--color\",\n \"never\",\n ];\n if (options.anchors === true) args.push(\"--anchors\");\n return new Promise((resolve, reject) => {\n execFile(\n options.binary,\n args,\n { cwd: options.cwd, maxBuffer: 64 * 1024 * 1024 },\n (error, stdout, stderr) => {\n if (error !== null) {\n const report = `${stdout}${stderr}`.trim();\n reject(\n new Error(\n `\\`ascribe build --build ${options.build}\\` failed${report === \"\" ? `: ${error.message}` : `:\\n${report}`}`,\n { cause: error },\n ),\n );\n return;\n }\n resolve({ diagnostics: stdout.trim(), summary: stderr.trim() });\n },\n );\n });\n}\n"]}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,sCAAsC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAU9C;;;;GAIG;AACH,MAAM,OAAO,GAAG,oEAAoE,CAAC;AAErF;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,WAAmB;IAC7C,IAAI,WAAW,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IACrC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7E,OAAO,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;AACtE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,OAAoC,EACpC,OAAe,EACf,MAAM,GAAG,KAAK;IAEd,OAAO,OAAO,KAAK,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,IAAI,CAAC,OAAO,KAAK,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC;AAClF,CAAC;AAED,kHAAkH;AAClH,MAAM,UAAU,SAAS,CAAC,MAAoC,EAAE,OAAe;IAC7E,OAAO,OAAO,KAAK,KAAK,IAAI,MAAM,KAAK,KAAK,CAAC;AAC/C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,OASxB;IACC,MAAM,IAAI,GAAG;QACX,OAAO;QACP,QAAQ;QACR,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QACvC,SAAS;QACT,OAAO,CAAC,KAAK;QACb,UAAU;QACV,OAAO,CAAC,UAAU;QAClB,SAAS;QACT,OAAO;KACR,CAAC;IACF,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI;QAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACrD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CACN,OAAO,CAAC,MAAM,EACd,IAAI,EACJ,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,EACjD,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxB,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,MAAM,MAAM,GAAG,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC;gBAC3C,MAAM,CACJ,IAAI,KAAK,CACP,2BAA2B,OAAO,CAAC,KAAK,YAAY,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,EAAE,EAC3G,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YACD,OAAO,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAClE,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// Running `ascribe build` for the configured build: its site output, and\n// its JSON output while review is on.\nimport { execFile } from \"node:child_process\";\n\n/** What a successful build printed. */\nexport interface BuildResult {\n /** The checks' report, which is more than a summary line when there is anything to report. */\n diagnostics: string;\n /** The `built <build>/site: …` lines. */\n summary: string;\n}\n\n/**\n * A check's summary line, the report's last: `checked 14 files for build \\`site\\`, reported on 14:\n * 0 errors, 0 warnings, 0 advice`. An older binary wrote `checked 14 files: 0 errors,\n * 0 warnings`, with `, 3 advice` after only when there was advice.\n */\nconst SUMMARY = /^checked [^:\\n]*: (\\d+) errors?, (\\d+) warnings?(?:, \\d+ advice)?$/;\n\n/**\n * Whether a build's report is worth a warning: anything but a report whose\n * summary line counts no errors and no warnings, which is logged at info\n * level, advice and all.\n */\nexport function hasWarnings(diagnostics: string): boolean {\n if (diagnostics === \"\") return false;\n const summary = SUMMARY.exec(diagnostics.trimEnd().split(\"\\n\").at(-1) ?? \"\");\n return summary === null || summary[1] !== \"0\" || summary[2] !== \"0\";\n}\n\n/**\n * Whether an Astro command builds with source anchors, given the integration's\n * `anchors` option: `\"dev\"` means in `astro dev` only, so `astro build` has\n * them only when it is `true`. Review in the site preview (`review`) needs\n * them in `astro dev`.\n */\nexport function anchorsFor(\n anchors: boolean | \"dev\" | undefined,\n command: string,\n review = false,\n): boolean {\n return anchors === true || (command === \"dev\" && (anchors === \"dev\" || review));\n}\n\n/** Whether an Astro command has review in the site preview, given the `review` option (an entry's name is on). */\nexport function reviewFor(review: boolean | string | undefined, command: string): boolean {\n return command === \"dev\" && review !== false;\n}\n\n/**\n * Builds one build's outputs, the site output by default. Rejects, with the compiler's report as the\n * message, if the build has errors: the Astro build fails with them.\n */\nexport function runBuild(options: {\n binary: string;\n configPath: string;\n build: string;\n cwd: string;\n /** Write source anchors (`--anchors`). */\n anchors?: boolean;\n /** The outputs to write. Default: `[\"site\"]`. */\n outputs?: readonly (\"site\" | \"plain\" | \"json\")[];\n}): Promise<BuildResult> {\n const args = [\n \"build\",\n \"--emit\",\n (options.outputs ?? [\"site\"]).join(\",\"),\n \"--build\",\n options.build,\n \"--config\",\n options.configPath,\n \"--color\",\n \"never\",\n ];\n if (options.anchors === true) args.push(\"--anchors\");\n return new Promise((resolve, reject) => {\n execFile(\n options.binary,\n args,\n { cwd: options.cwd, maxBuffer: 64 * 1024 * 1024 },\n (error, stdout, stderr) => {\n if (error !== null) {\n const report = `${stdout}${stderr}`.trim();\n reject(\n new Error(\n `\\`ascribe build --build ${options.build}\\` failed${report === \"\" ? `: ${error.message}` : `:\\n${report}`}`,\n { cause: error },\n ),\n );\n return;\n }\n resolve({ diagnostics: stdout.trim(), summary: stderr.trim() });\n },\n );\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascribed/astro",
3
- "version": "0.2.1-next.15",
3
+ "version": "0.2.1-next.17",
4
4
  "description": "The Astro integration for Ascribe.",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
@@ -12,12 +12,17 @@
12
12
  "homepage": "https://github.com/ascribed-dev/ascribe/tree/main/packages/astro#readme",
13
13
  "bugs": "https://github.com/ascribed-dev/ascribe/issues",
14
14
  "sideEffects": false,
15
+ "bin": {
16
+ "ascribe-astro": "./dist/cli.js"
17
+ },
15
18
  "files": [
16
19
  "dist",
20
+ "starter",
17
21
  "src/Elements.astro",
18
22
  "src/Availability.astro",
19
23
  "src/Permalink.astro",
20
- "src/MissingPermalink.astro"
24
+ "src/MissingPermalink.astro",
25
+ "src/Head.astro"
21
26
  ],
22
27
  "exports": {
23
28
  ".": {
@@ -35,6 +40,7 @@
35
40
  "./Elements.astro": "./src/Elements.astro",
36
41
  "./Availability.astro": "./src/Availability.astro",
37
42
  "./MissingPermalink.astro": "./src/MissingPermalink.astro",
43
+ "./Head.astro": "./src/Head.astro",
38
44
  "./content": {
39
45
  "types": "./dist/content.d.ts",
40
46
  "default": "./dist/content.js"
@@ -48,9 +54,9 @@
48
54
  "node": ">=24"
49
55
  },
50
56
  "dependencies": {
51
- "@ascribed/cli": "0.2.1-next.15",
52
- "@ascribed/elements": "0.2.1-next.15",
53
- "@ascribed/review": "0.2.1-next.15",
57
+ "@ascribed/cli": "0.2.1-next.17",
58
+ "@ascribed/elements": "0.2.1-next.17",
59
+ "@ascribed/review": "0.2.1-next.17",
54
60
  "github-slugger": "^2.0.0",
55
61
  "property-information": "^7.2.0",
56
62
  "smol-toml": "^1.9.0"
@@ -86,7 +92,7 @@
86
92
  "publishConfig": {
87
93
  "access": "public"
88
94
  },
89
- "gitHead": "b4d4611f06c75553d379e3ad09a93b809071228f",
95
+ "gitHead": "605791982664a254a310026f05ebe69184943131",
90
96
  "scripts": {
91
97
  "build": "tsc -p tsconfig.build.json",
92
98
  "typecheck": "tsc --noEmit",
package/src/Head.astro ADDED
@@ -0,0 +1,58 @@
1
+ ---
2
+ // Everything a page's head says about the page, from its record in the
3
+ // build's `_ascribe/pages.json`: its `<title>` and description, its canonical
4
+ // link, `<meta name="robots">` (`noindex` when it isn't listed, `nosnippet`
5
+ // when it's listed and not to be quoted), a link to its Markdown version when
6
+ // the build publishes one and the page is offered, the Open Graph tags a link
7
+ // preview reads, and its breadcrumbs as structured data. Each comes from the
8
+ // record, as the sitemap and `llms.txt` do, so none can disagree with them. A
9
+ // layout includes it in `<head>`, given the page's collection entry:
10
+ //
11
+ // <Head
12
+ // entry={entry}
13
+ // title={(page, record) => (record.route === "/" ? "Loom" : `${page} · Loom`)}
14
+ // name="Loom"
15
+ // card={{ src: "/social-card.png", alt: "Loom's docs", width: 1200, height: 630 }}
16
+ // />
17
+ //
18
+ // What the site knows and the record doesn't is given by property: `title`,
19
+ // a function from the page's title and its record, which has its route, to
20
+ // the whole title; `name`, the site's name, for `og:site_name` and, on the
21
+ // page at the host's root, the site-name structured data; and `card`, the
22
+ // page's social card. The layout keeps the character set, the viewport,
23
+ // icons, theme colors, and styles. A page with no record, such as an Astro
24
+ // page of the site's own, gets nothing: it writes its own head.
25
+ import { headTags, pageRecord, scriptJson, type Card, type PageRecord } from "@ascribed/astro/content";
26
+
27
+ interface Props {
28
+ /** The page's collection entry, or `undefined` for a page that has none. */
29
+ entry?: { filePath?: string | undefined; data?: unknown } | undefined;
30
+ /** The whole title, from the page's title and its record. Default: the page's title. */
31
+ title?: (title: string, record: PageRecord) => string;
32
+ /** The site's name. */
33
+ name?: string;
34
+ /** The page's social card. Without one, no image tags. */
35
+ card?: Card;
36
+ }
37
+
38
+ const { entry, title, name, card } = Astro.props;
39
+ const record = entry === undefined ? undefined : pageRecord(entry);
40
+ const tags =
41
+ record === undefined
42
+ ? []
43
+ : headTags(record, { title, name, card, address: Astro.site ?? Astro.url });
44
+ ---
45
+
46
+ {
47
+ tags.map((tag) =>
48
+ tag.tag === "title" ? (
49
+ <title>{tag.text}</title>
50
+ ) : tag.tag === "meta" ? (
51
+ <meta {...tag.attributes} />
52
+ ) : tag.tag === "link" ? (
53
+ <link {...tag.attributes} />
54
+ ) : (
55
+ <script is:inline type="application/ld+json" set:html={scriptJson(tag.json)} />
56
+ ),
57
+ )
58
+ }
@@ -0,0 +1,8 @@
1
+ import { defineCollection } from "astro:content";
2
+ import { ascribeCollection } from "@ascribed/astro/content";
3
+ // Generated by `ascribe build --emit site`; the integration runs it first.
4
+ import { schema } from "../.ascribe/build/site/site/_ascribe/schema.ts";
5
+
6
+ export const collections = {
7
+ docs: defineCollection(ascribeCollection({ schema })),
8
+ };
@@ -0,0 +1,87 @@
1
+ ---
2
+ import { getCollection, type CollectionEntry } from "astro:content";
3
+ import type { ComponentProps } from "astro/types";
4
+ import Availability from "@ascribed/astro/Availability.astro";
5
+ import Elements from "@ascribed/astro/Elements.astro";
6
+ // Everything the head says about the page, from its record.
7
+ import Head from "@ascribed/astro/Head.astro";
8
+ import { sidebar } from "@ascribed/astro/content";
9
+ import { nav } from "../nav.ts";
10
+
11
+ interface Props {
12
+ entry: CollectionEntry<"docs">;
13
+ headings: { depth: number; slug: string; text: string }[];
14
+ }
15
+
16
+ /**
17
+ * What the layout reads of a page's frontmatter: `title`, which every content
18
+ * type has, and what a type may have. `title` is plain text; `formatted.title`
19
+ * is the title as HTML, with its code spans as <code>, when the content model
20
+ * sets `inline = "code"` on `title`.
21
+ */
22
+ interface Page {
23
+ title: string;
24
+ available?: ComponentProps<typeof Availability>["available"];
25
+ formatted?: { title?: string };
26
+ }
27
+
28
+ const { entry, headings } = Astro.props;
29
+ const page = (of: CollectionEntry<"docs">) => of.data as Page;
30
+ /** A page's title as HTML: its formatted form, or its plain text escaped. */
31
+ const titleHtml = (of: CollectionEntry<"docs">) =>
32
+ page(of).formatted?.title ??
33
+ page(of).title.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
34
+ const { available } = page(entry);
35
+ // The sidebar: src/nav.ts's groups, each page with its address. The build
36
+ // fails when the list leaves out a page, or names one that doesn't exist.
37
+ const groups = sidebar(nav, await getCollection("docs"));
38
+ ---
39
+
40
+ <!doctype html>
41
+ <html lang="en">
42
+ <head>
43
+ <meta charset="utf-8" />
44
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
45
+ <Head entry={entry} />
46
+ <Elements />
47
+ </head>
48
+ <body>
49
+ <nav aria-label="Docs">
50
+ {
51
+ groups.map((group) => (
52
+ <section>
53
+ <h2>{group.label}</h2>
54
+ <ul>
55
+ {group.pages.map((item) => (
56
+ <li>
57
+ <a
58
+ href={item.href}
59
+ aria-current={item.entry.id === entry.id ? "page" : undefined}
60
+ >
61
+ <Fragment set:html={titleHtml(item.entry)} />
62
+ </a>
63
+ </li>
64
+ ))}
65
+ </ul>
66
+ </section>
67
+ ))
68
+ }
69
+ </nav>
70
+ <main>
71
+ <h1 set:html={titleHtml(entry)} />
72
+ <Availability available={available} />
73
+ <nav aria-label="On this page">
74
+ <ul>
75
+ {
76
+ headings.map((heading) => (
77
+ <li data-depth={heading.depth}>
78
+ <a href={`#${heading.slug}`}>{heading.text}</a>
79
+ </li>
80
+ ))
81
+ }
82
+ </ul>
83
+ </nav>
84
+ <article><slot /></article>
85
+ </main>
86
+ </body>
87
+ </html>
@@ -0,0 +1,13 @@
1
+ // The sidebar's list: its groups, in order, each with its pages' source files,
2
+ // relative to the content root. `npx @ascribed/astro init` wrote it from the
3
+ // pages there were then; it's yours to reorder, relabel, and add to. Ascribe's
4
+ // content model has no page order or grouping yet, so the site keeps the list,
5
+ // and the layout's `sidebar()` keeps it complete: the build fails on a page the
6
+ // list leaves out, and on a file it names that isn't a page.
7
+ import type { NavGroup } from "@ascribed/astro/content";
8
+
9
+ export const nav: NavGroup[] = [
10
+ { label: "Start", pages: ["index.md"] },
11
+ { label: "Guides", pages: ["Guides/My Setup.md"] },
12
+ { label: "Reference", pages: ["Reference/Options.md"] },
13
+ ];
@@ -0,0 +1,20 @@
1
+ ---
2
+ import { getCollection, render } from "astro:content";
3
+ import Docs from "../layouts/Docs.astro";
4
+
5
+ // A page's route is the base path plus its entry id, and the root page (id
6
+ // `index`) is at the base path itself: what Ascribe's router computes for
7
+ // every link it writes.
8
+ export async function getStaticPaths() {
9
+ const entries = await getCollection("docs");
10
+ return entries.map((entry) => ({
11
+ params: { slug: entry.id === "index" ? undefined : entry.id },
12
+ props: { entry },
13
+ }));
14
+ }
15
+
16
+ const { entry } = Astro.props;
17
+ const { Content, headings } = await render(entry);
18
+ ---
19
+
20
+ <Docs entry={entry} headings={headings}><Content /></Docs>