@ascribed/astro 0.4.4 → 0.4.5-next.34

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.
@@ -1 +1 @@
1
- {"version":3,"file":"permalinks.js","sourceRoot":"","sources":["../src/permalinks.ts"],"names":[],"mappings":"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","sourcesContent":["// Permalinks' addresses: a page at `<path><id>` for each permalink the build\n// publishes, which sends the reader on to its heading, and for each retired\n// one with nowhere to lead, which says why it was removed. Ascribe writes the list,\n// `_ascribe/permalinks.json` in the site output (output-layout contract §1.2);\n// the integration adds the route (`Permalink.astro`) and builds its pages\n// from it, in `astro build` and `astro dev`. With several projects, one route\n// serves every project's list.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The list's format version this reads. */\nconst VERSION = 1;\n\n/** `_ascribe/permalinks.json`. */\nexport interface PermalinkList {\n version: number;\n /** Where the addresses are published, from the site's root, ending in `/`. */\n path: string;\n /** The content model's dimensions, each with its values: the parameters an address takes. */\n dimensions?: Record<string, string[]>;\n /**\n * Each permalink's destination. An alias names the permalink it leads to,\n * and a retired permalink that leads somewhere its reason.\n */\n permalinks: Record<\n string,\n { route: string; fragment?: string; source: string; alias?: string; retired?: string }\n >;\n /** The retired permalinks with nowhere to lead in this build, each with why. */\n removed?: Record<string, { reason: string }>;\n}\n\n/**\n * What an address's page needs: where it sends the reader, and the\n * parameters it takes; or, for a retired permalink with nowhere to lead,\n * why it was removed.\n */\nexport type Forwarding =\n { to: string; dimensions: Record<string, string[]> } | { reason: string; home?: string };\n\n/** A build's site output whose list the route serves, and the route of its project's root. */\nexport interface PermalinkSource {\n siteRoot: string;\n home: string;\n}\n\n/** The list's text in a build's site output, or `undefined` when there isn't one. */\nexport function permalinksText(siteRoot: string): string | undefined {\n try {\n return readFileSync(listFile(siteRoot), \"utf8\");\n } catch {\n return undefined;\n }\n}\n\nfunction listFile(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"permalinks.json\");\n}\n\n/** The list in a build's site output, or `undefined` when there isn't one. */\nexport function readPermalinks(siteRoot: string): PermalinkList | undefined {\n const file = listFile(siteRoot);\n const text = permalinksText(siteRoot);\n if (text === undefined) return undefined;\n const list = JSON.parse(text) as PermalinkList;\n if (list.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(list.version)} of the permalinks list, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return list;\n}\n\n/**\n * The route pattern for the addresses, as Astro takes it: under `base`, which\n * Astro adds. Throws when the list's path isn't under `base`, since an Astro\n * site serves nothing outside it.\n */\nexport function permalinkPattern(listPath: string, base: string): string {\n const root = normalizeBase(base);\n const at = normalizeBase(listPath);\n if (!at.startsWith(root)) {\n throw new Error(\n `@ascribed/astro: permalinks are published at \"${at}\", which isn't under Astro's \\`base\\`, \"${base}\", and an Astro site serves nothing outside it. Set [consumer] permalink-path in ascribe.toml to a path under \"${root}\".`,\n );\n }\n return `/${at.slice(root.length)}[id]`;\n}\n\n/** Where a permalink leads: its page's route, and the heading's anchor. */\nfunction destination(entry: { route: string; fragment?: string }): string {\n return entry.fragment === undefined ? entry.route : `${entry.route}#${entry.fragment}`;\n}\n\n/**\n * `getStaticPaths` for the route: one page for each permalink in the site\n * outputs' lists, and one for each retired permalink they list as removed,\n * whose page links to its project's root. A name in more than one list gets\n * the first's page; the integration refuses one that two projects declare.\n */\nexport function forwardingPaths(\n sources: string | readonly PermalinkSource[],\n): { params: { id: string }; props: Forwarding }[] {\n const all = typeof sources === \"string\" ? [{ siteRoot: sources, home: undefined }] : sources;\n const pages = new Map<string, Forwarding>();\n for (const { siteRoot, home } of all) {\n const list = readPermalinks(siteRoot);\n if (list === undefined) continue;\n const dimensions = list.dimensions ?? {};\n for (const [id, entry] of Object.entries(list.permalinks)) {\n if (!pages.has(id)) pages.set(id, { to: destination(entry), dimensions });\n }\n for (const [id, entry] of Object.entries(list.removed ?? {})) {\n if (!pages.has(id))\n pages.set(\n id,\n home === undefined ? { reason: entry.reason } : { reason: entry.reason, home },\n );\n }\n }\n const forwarding = [...pages].filter(([, props]) => \"to\" in props);\n const removed = [...pages].filter(([, props]) => !(\"to\" in props));\n return [...forwarding, ...removed].map(([id, props]) => ({ params: { id }, props }));\n}\n\n/** Whether a list names anything: a permalink, or a retired one it lists as removed. */\nexport function hasPermalinks(list: PermalinkList): boolean {\n return Object.keys(list.permalinks).length + Object.keys(list.removed ?? {}).length > 0;\n}\n\n/**\n * The list a site's not-found page explains addresses from, and the route of\n * its project's root: the first that has a permalink, or else the first list.\n */\nexport function servedList(\n sources: readonly PermalinkSource[],\n): { path: string; home: string } | undefined {\n let first: { path: string; home: string } | undefined;\n for (const { siteRoot, home } of sources) {\n const list = readPermalinks(siteRoot);\n if (list === undefined) continue;\n first ??= { path: list.path, home };\n if (hasPermalinks(list)) return { path: list.path, home };\n }\n return first;\n}\n\n/**\n * The routes of the forwarding pages, from the host's root: one for each name\n * in `lists`, at the path of the list the route serves, as `hasPermalinks`\n * picks it. They aren't pages of a build, and no sitemap lists them.\n */\nexport function forwardingRoutes(lists: readonly (PermalinkList | undefined)[]): string[] {\n const known = lists.filter((list) => list !== undefined);\n const served = known.find(hasPermalinks) ?? known[0];\n if (served === undefined) return [];\n const at = normalizeBase(served.path);\n return known.flatMap((list) =>\n [...Object.keys(list.permalinks), ...Object.keys(list.removed ?? {})].map((id) => `${at}${id}`),\n );\n}\n"]}
1
+ {"version":3,"file":"permalinks.js","sourceRoot":"","sources":["../src/permalinks.ts"],"names":[],"mappings":"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","sourcesContent":["// Permalinks' addresses: a page at `<path><id>` for each permalink the build\n// publishes, which sends the reader on to its heading, and for each retired\n// one, which says why it was removed. Ascribe writes the list,\n// `_ascribe/permalinks.json` in the site output (output-layout contract §1.2);\n// the integration adds the route (`Permalink.astro`) and builds its pages\n// from it, in `astro build` and `astro dev`. With several projects, one route\n// serves every project's list.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { readHelp } from \"./help-file.js\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The list's format version this reads. */\nconst VERSION = 1;\n\n/** `_ascribe/permalinks.json`. */\nexport interface PermalinkList {\n version: number;\n /** Where the addresses are published, from the site's root, ending in `/`. */\n path: string;\n /** The content model's dimensions, each with its values: the parameters an address takes. */\n dimensions?: Record<string, string[]>;\n /**\n * Each permalink's destination. A name `ascribe.addresses` moves to\n * another permalink names that permalink, and one it moves to a page\n * where, as the file writes it.\n */\n permalinks: Record<\n string,\n { route: string; fragment?: string; source: string; alias?: string; moved?: string }\n >;\n /** The retired permalinks in this build, each with why. */\n removed?: Record<string, { reason: string }>;\n}\n\n/**\n * What an address's page needs: where it sends the reader, the parameters it\n * takes, and, for a name with a help entry, the addresses of its section as\n * Markdown and as JSON, which the page names as its alternates; or, for a\n * retired permalink, why it was removed.\n */\nexport type Forwarding =\n | {\n to: string;\n dimensions: Record<string, string[]>;\n alternates?: { markdown: string; json: string };\n }\n | { reason: string; home?: string };\n\n/** A build's site output whose list the route serves, and the route of its project's root. */\nexport interface PermalinkSource {\n siteRoot: string;\n home: string;\n}\n\n/** The list's text in a build's site output, or `undefined` when there isn't one. */\nexport function permalinksText(siteRoot: string): string | undefined {\n try {\n return readFileSync(listFile(siteRoot), \"utf8\");\n } catch {\n return undefined;\n }\n}\n\nfunction listFile(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"permalinks.json\");\n}\n\n/** The list in a build's site output, or `undefined` when there isn't one. */\nexport function readPermalinks(siteRoot: string): PermalinkList | undefined {\n const file = listFile(siteRoot);\n const text = permalinksText(siteRoot);\n if (text === undefined) return undefined;\n const list = JSON.parse(text) as PermalinkList;\n if (list.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(list.version)} of the permalinks list, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return list;\n}\n\n/**\n * The route pattern for the addresses, as Astro takes it: under `base`, which\n * Astro adds. Throws when the list's path isn't under `base`, since an Astro\n * site serves nothing outside it.\n */\nexport function permalinkPattern(listPath: string, base: string): string {\n const root = normalizeBase(base);\n const at = normalizeBase(listPath);\n if (!at.startsWith(root)) {\n throw new Error(\n `@ascribed/astro: permalinks are published at \"${at}\", which isn't under Astro's \\`base\\`, \"${base}\", and an Astro site serves nothing outside it. Set [consumer] permalink-path in ascribe.toml to a path under \"${root}\".`,\n );\n }\n return `/${at.slice(root.length)}[id]`;\n}\n\n/** Where a permalink leads: its page's route, and the heading's anchor. */\nfunction destination(entry: { route: string; fragment?: string }): string {\n return entry.fragment === undefined ? entry.route : `${entry.route}#${entry.fragment}`;\n}\n\n/**\n * `getStaticPaths` for the route: one page for each permalink in the site\n * outputs' lists, and one for each retired permalink they list as removed,\n * whose page links to its project's root. A name in more than one list gets\n * the first's page; the integration refuses one that two projects declare.\n */\nexport function forwardingPaths(\n sources: string | readonly PermalinkSource[],\n): { params: { id: string }; props: Forwarding }[] {\n const all = typeof sources === \"string\" ? [{ siteRoot: sources, home: undefined }] : sources;\n const pages = new Map<string, Forwarding>();\n for (const { siteRoot, home } of all) {\n const list = readPermalinks(siteRoot);\n if (list === undefined) continue;\n const dimensions = list.dimensions ?? {};\n // The names with a help entry, served beside their addresses (`help.ts`).\n const help = readHelp(siteRoot)?.help ?? {};\n const at = normalizeBase(list.path);\n for (const [id, entry] of Object.entries(list.permalinks)) {\n if (pages.has(id)) continue;\n const alternates = Object.hasOwn(help, id)\n ? { alternates: { markdown: `${at}${id}.md`, json: `${at}${id}.json` } }\n : {};\n pages.set(id, { to: destination(entry), dimensions, ...alternates });\n }\n for (const [id, entry] of Object.entries(list.removed ?? {})) {\n if (!pages.has(id))\n pages.set(\n id,\n home === undefined ? { reason: entry.reason } : { reason: entry.reason, home },\n );\n }\n }\n const forwarding = [...pages].filter(([, props]) => \"to\" in props);\n const removed = [...pages].filter(([, props]) => !(\"to\" in props));\n return [...forwarding, ...removed].map(([id, props]) => ({ params: { id }, props }));\n}\n\n/** Whether a list names anything: a permalink, or a retired one it lists as removed. */\nexport function hasPermalinks(list: PermalinkList): boolean {\n return Object.keys(list.permalinks).length + Object.keys(list.removed ?? {}).length > 0;\n}\n\n/**\n * The list a site's not-found page explains addresses from, and the route of\n * its project's root: the first that has a permalink, or else the first list.\n */\nexport function servedList(\n sources: readonly PermalinkSource[],\n): { path: string; home: string } | undefined {\n let first: { path: string; home: string } | undefined;\n for (const { siteRoot, home } of sources) {\n const list = readPermalinks(siteRoot);\n if (list === undefined) continue;\n first ??= { path: list.path, home };\n if (hasPermalinks(list)) return { path: list.path, home };\n }\n return first;\n}\n\n/**\n * The routes of the forwarding pages, from the host's root: one for each name\n * in `lists`, at the path of the list the route serves, as `hasPermalinks`\n * picks it. They aren't pages of a build, and no sitemap lists them.\n */\nexport function forwardingRoutes(lists: readonly (PermalinkList | undefined)[]): string[] {\n const known = lists.filter((list) => list !== undefined);\n const served = known.find(hasPermalinks) ?? known[0];\n if (served === undefined) return [];\n const at = normalizeBase(served.path);\n return known.flatMap((list) =>\n [...Object.keys(list.permalinks), ...Object.keys(list.removed ?? {})].map((id) => `${at}${id}`),\n );\n}\n"]}
@@ -1,8 +1,15 @@
1
1
  /** One redirect: an old address, from the host's root, to where it leads now. */
2
2
  export interface Redirect {
3
3
  from: string;
4
+ /** The query parameters the rule matches, as `name=:name`: a permalink's dimensions. */
5
+ query?: string[];
4
6
  to: string;
5
7
  status: number;
8
+ /**
9
+ * Whether the rule applies even with a page at `from`, Netlify's `!`: a
10
+ * permalink's, whose forwarding page is there for hosts without redirects.
11
+ */
12
+ force?: boolean;
6
13
  }
7
14
  /**
8
15
  * The redirects in the host's `_redirects` the compiler wrote in a build's
@@ -11,11 +18,15 @@ export interface Redirect {
11
18
  */
12
19
  export declare function readHostRedirects(siteRoot: string): Redirect[];
13
20
  /**
14
- * Every project's redirects as one list, in the order given: an address two
15
- * projects both redirect keeps the first's, as the first entry's files win.
21
+ * Every project's redirects as one list, in the order given: a rule two
22
+ * projects both write for an address, with the same query parameters, keeps
23
+ * the first's, as the first entry's files win.
16
24
  */
17
25
  export declare function mergeRedirects(projects: readonly (readonly Redirect[])[]): Redirect[];
18
- /** The redirects in Netlify's `_redirects` format: each on a line, `from`, `to`, and the status. */
26
+ /**
27
+ * The redirects in Netlify's `_redirects` format: each on a line, `from`, its
28
+ * query parameters, `to`, and the status, with `!` when it's forced.
29
+ */
19
30
  export declare function netlifyRedirects(redirects: readonly Redirect[]): string;
20
31
  /** What the build does about the host's redirects. */
21
32
  export type RedirectsOutcome = {
package/dist/redirects.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // The redirects a build writes for the host: with `[consumer] host =
2
2
  // "netlify"`, the compiler writes Netlify's `_redirects` in each build's site
3
3
  // output, at `_ascribe/host/_redirects` (output-layout contract §1.9), from
4
- // the moves `ascribe.addresses` records. `astro build` merges every
4
+ // the moves `ascribe.addresses` records and the permalinks' addresses. `astro build` merges every
5
5
  // project's into the site's `_redirects`, at the root of what's deployed.
6
6
  //
7
7
  // A site's own `public/_redirects` is kept, after Ascribe's rules: Netlify
@@ -29,33 +29,53 @@ export function readHostRedirects(siteRoot) {
29
29
  const trimmed = line.trim();
30
30
  if (trimmed === "" || trimmed.startsWith("#"))
31
31
  continue;
32
- const [from, to, status] = trimmed.split(/\s+/);
32
+ const [from, ...rest] = trimmed.split(/\s+/);
33
+ // A query parameter is `name=:name`; the target starts with `/`.
34
+ const query = rest.filter((part) => !part.startsWith("/") && part.includes("="));
35
+ const [to, status] = rest.slice(query.length);
33
36
  if (from === undefined || to === undefined)
34
37
  continue;
35
- out.push({ from, to, status: status === undefined ? 301 : Number.parseInt(status, 10) });
38
+ const force = status?.endsWith("!") ?? false;
39
+ out.push({
40
+ from,
41
+ ...(query.length > 0 ? { query } : {}),
42
+ to,
43
+ status: status === undefined ? 301 : Number.parseInt(status, 10),
44
+ ...(force ? { force } : {}),
45
+ });
36
46
  }
37
47
  return out;
38
48
  }
49
+ /** What a rule matches: its address, and its query parameters. */
50
+ function matches(redirect) {
51
+ return [redirect.from, ...(redirect.query ?? [])].join(" ");
52
+ }
39
53
  /**
40
- * Every project's redirects as one list, in the order given: an address two
41
- * projects both redirect keeps the first's, as the first entry's files win.
54
+ * Every project's redirects as one list, in the order given: a rule two
55
+ * projects both write for an address, with the same query parameters, keeps
56
+ * the first's, as the first entry's files win.
42
57
  */
43
58
  export function mergeRedirects(projects) {
44
59
  const seen = new Set();
45
60
  const out = [];
46
61
  for (const list of projects) {
47
62
  for (const redirect of list) {
48
- if (seen.has(redirect.from))
63
+ if (seen.has(matches(redirect)))
49
64
  continue;
50
- seen.add(redirect.from);
65
+ seen.add(matches(redirect));
51
66
  out.push(redirect);
52
67
  }
53
68
  }
54
69
  return out;
55
70
  }
56
- /** The redirects in Netlify's `_redirects` format: each on a line, `from`, `to`, and the status. */
71
+ /**
72
+ * The redirects in Netlify's `_redirects` format: each on a line, `from`, its
73
+ * query parameters, `to`, and the status, with `!` when it's forced.
74
+ */
57
75
  export function netlifyRedirects(redirects) {
58
- return redirects.map((r) => `${r.from} ${r.to} ${String(r.status)}\n`).join("");
76
+ return redirects
77
+ .map((r) => `${matches(r)} ${r.to} ${String(r.status)}${r.force === true ? "!" : ""}\n`)
78
+ .join("");
59
79
  }
60
80
  /** What the build does about `_redirects`, given every project's redirects and the site. */
61
81
  export function redirectsOutcome(sources) {
@@ -69,7 +89,8 @@ export function redirectsOutcome(sources) {
69
89
  text: `no _redirects: Astro's \`base\` is "${base}", and Netlify reads _redirects only at the root of what's deployed. Put these lines in the host's own:\n${text}`,
70
90
  };
71
91
  }
72
- const taken = sources.redirects.filter((r) => sources.published(routeKey(r.from)));
92
+ // A forced rule is meant to pass the page at its address: a permalink's forwarding page.
93
+ const taken = sources.redirects.filter((r) => r.force !== true && sources.published(routeKey(r.from)));
73
94
  if (taken.length > 0) {
74
95
  return {
75
96
  kind: "fail",
@@ -1 +1 @@
1
- {"version":3,"file":"redirects.js","sourceRoot":"","sources":["../src/redirects.ts"],"names":[],"mappings":"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","sourcesContent":["// The redirects a build writes for the host: with `[consumer] host =\n// \"netlify\"`, the compiler writes Netlify's `_redirects` in each build's site\n// output, at `_ascribe/host/_redirects` (output-layout contract §1.9), from\n// the moves `ascribe.addresses` records. `astro build` merges every\n// project's into the site's `_redirects`, at the root of what's deployed.\n//\n// A site's own `public/_redirects` is kept, after Ascribe's rules: Netlify\n// uses the first rule that matches, so a page's recorded move wins over a\n// pattern of the site's.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { normalizeBase } from \"./project.js\";\nimport { routeKey } from \"./records.js\";\n\n/** One redirect: an old address, from the host's root, to where it leads now. */\nexport interface Redirect {\n from: string;\n to: string;\n status: number;\n}\n\n/**\n * The redirects in the host's `_redirects` the compiler wrote in a build's\n * site output, in its order: none when it has none, as without `[consumer]\n * host`.\n */\nexport function readHostRedirects(siteRoot: string): Redirect[] {\n let text: string;\n try {\n text = readFileSync(path.join(siteRoot, \"_ascribe\", \"host\", \"_redirects\"), \"utf8\");\n } catch {\n return [];\n }\n const out: Redirect[] = [];\n for (const line of text.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (trimmed === \"\" || trimmed.startsWith(\"#\")) continue;\n const [from, to, status] = trimmed.split(/\\s+/);\n if (from === undefined || to === undefined) continue;\n out.push({ from, to, status: status === undefined ? 301 : Number.parseInt(status, 10) });\n }\n return out;\n}\n\n/**\n * Every project's redirects as one list, in the order given: an address two\n * projects both redirect keeps the first's, as the first entry's files win.\n */\nexport function mergeRedirects(projects: readonly (readonly Redirect[])[]): Redirect[] {\n const seen = new Set<string>();\n const out: Redirect[] = [];\n for (const list of projects) {\n for (const redirect of list) {\n if (seen.has(redirect.from)) continue;\n seen.add(redirect.from);\n out.push(redirect);\n }\n }\n return out;\n}\n\n/** The redirects in Netlify's `_redirects` format: each on a line, `from`, `to`, and the status. */\nexport function netlifyRedirects(redirects: readonly Redirect[]): string {\n return redirects.map((r) => `${r.from} ${r.to} ${String(r.status)}\\n`).join(\"\");\n}\n\n/** What the build does about the host's redirects. */\nexport type RedirectsOutcome =\n | { kind: \"none\" }\n | { kind: \"write\"; text: string }\n | { kind: \"note\"; text: string }\n | { kind: \"fail\"; text: string };\n\n/** What the build does about `_redirects`, given every project's redirects and the site. */\nexport function redirectsOutcome(sources: {\n /** The redirects of every project that sets `[consumer] host = \"netlify\"`, merged. */\n redirects: readonly Redirect[];\n /** Astro's `base`. */\n base: string;\n /** Whether a route is a page the site publishes, which a redirect mustn't hide. */\n published: (route: string) => boolean;\n /** The site's own `public/_redirects`, when it has one. */\n own: string | undefined;\n}): RedirectsOutcome {\n if (sources.redirects.length === 0) return { kind: \"none\" };\n const text = netlifyRedirects(sources.redirects);\n const base = normalizeBase(sources.base);\n if (base !== \"/\") {\n return {\n kind: \"note\",\n text: `no _redirects: Astro's \\`base\\` is \"${base}\", and Netlify reads _redirects only at the root of what's deployed. Put these lines in the host's own:\\n${text}`,\n };\n }\n const taken = sources.redirects.filter((r) => sources.published(routeKey(r.from)));\n if (taken.length > 0) {\n return {\n kind: \"fail\",\n text: `a page the site publishes is at an address ascribe.addresses forwards somewhere else, so its redirect would hide the page or never be used. Remove the entry, or move the page:\\n${netlifyRedirects(taken)}`,\n };\n }\n if (sources.own === undefined) return { kind: \"write\", text };\n const own = sources.own.endsWith(\"\\n\") || sources.own === \"\" ? sources.own : `${sources.own}\\n`;\n return {\n kind: \"write\",\n text: `${text}\\n# The site's own, from public/_redirects: after Ascribe's, since the first rule that matches is used.\\n${own}`,\n };\n}\n\n/** The folder a retired address's page is in, under its build's base path. */\nconst GONE_FOLDER = \"/_ascribe/gone/\";\n\n/** The format version of `_ascribe/redirects.json` this reads. */\nconst REDIRECTS_VERSION = 1;\n\n/** A retired address, as a build's `_ascribe/redirects.json` lists it (output-layout contract §1.8). */\ninterface Gone {\n /** The address, from the host's root. */\n path: string;\n /** Why it was retired, as its authors wrote it. */\n reason: string;\n /** The `ascribe.addresses` key it comes from. */\n entry: string;\n /** Where its page is, from the host's root: the base path, `_ascribe/gone/`, and the address from there. */\n page: string;\n}\n\n/** What a retired address's page says: why, and where the docs' home is. */\nexport interface GonePage {\n reason: string;\n home: string;\n}\n\n/** The retired addresses a build's site output lists: none when it has no `_ascribe/redirects.json`. */\nfunction readGone(siteRoot: string): Gone[] {\n const file = path.join(siteRoot, \"_ascribe\", \"redirects.json\");\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return [];\n }\n const list = JSON.parse(text) as { version: number; gone?: Gone[] };\n if (list.version !== REDIRECTS_VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(list.version)} of the redirects list, and this version of the integration reads version ${String(REDIRECTS_VERSION)}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return list.gone ?? [];\n}\n\n/**\n * Each retired address's page the site outputs list, by its route from the\n * host's root, with its folder (its build's base path and `_ascribe/gone/`),\n * its address from there, and what it says. A page two projects both list\n * keeps the first's.\n */\nfunction gonePages(\n sources: readonly { siteRoot: string; home: string }[],\n): Map<string, { folder: string; address: string; props: GonePage }> {\n const pages = new Map<string, { folder: string; address: string; props: GonePage }>();\n for (const { siteRoot, home } of sources) {\n for (const gone of readGone(siteRoot)) {\n const at = gone.page.indexOf(GONE_FOLDER);\n if (at < 0) continue;\n const folder = gone.page.slice(0, at + GONE_FOLDER.length);\n const address = gone.page.slice(folder.length).replace(/\\/+$/, \"\");\n if (address === \"\") continue;\n const route = `${folder}${address}`;\n if (!pages.has(route))\n pages.set(route, { folder, address, props: { reason: gone.reason, home } });\n }\n }\n return pages;\n}\n\n/**\n * The route pattern for the pages in `folder`, as Astro takes it: under\n * `base`, which Astro adds. Throws when the folder isn't under `base`, since\n * an Astro site serves nothing outside it.\n */\nfunction gonePattern(folder: string, base: string): string {\n const root = normalizeBase(base);\n if (!folder.startsWith(root)) {\n throw new Error(\n `@ascribed/astro: retired addresses' pages are published at \"${folder}\", which isn't under Astro's \\`base\\`, \"${base}\", and an Astro site serves nothing outside it. Set [consumer] base-path in ascribe.toml to a path under \"${root}\".`,\n );\n }\n return `/${folder.slice(root.length)}[...address]`;\n}\n\n/**\n * The route patterns the retired addresses' pages need, one for each\n * folder that has any, under `base`.\n */\nexport function gonePatterns(siteRoots: readonly string[], base: string): string[] {\n const pages = gonePages(siteRoots.map((siteRoot) => ({ siteRoot, home: \"/\" })));\n return [...new Set([...pages.values()].map(({ folder }) => gonePattern(folder, base)))];\n}\n\n/**\n * `getStaticPaths` for the route at `routePattern`, one of `gonePatterns`:\n * one page for each retired address's page in its folder, with the reason\n * and the route of its project's root.\n */\nexport function gonePaths(\n sources: readonly { siteRoot: string; home: string }[],\n base: string,\n routePattern: string,\n): { params: { address: string }; props: GonePage }[] {\n return [...gonePages(sources).values()]\n .filter(({ folder }) => gonePattern(folder, base) === routePattern)\n .map(({ address, props }) => ({ params: { address }, props }));\n}\n\n/** The routes of the retired addresses' pages the site outputs at `siteRoots` list, from the host's root. */\nexport function goneRoutes(siteRoots: readonly string[]): string[] {\n return [...gonePages(siteRoots.map((siteRoot) => ({ siteRoot, home: \"/\" }))).keys()];\n}\n"]}
1
+ {"version":3,"file":"redirects.js","sourceRoot":"","sources":["../src/redirects.ts"],"names":[],"mappings":"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","sourcesContent":["// The redirects a build writes for the host: with `[consumer] host =\n// \"netlify\"`, the compiler writes Netlify's `_redirects` in each build's site\n// output, at `_ascribe/host/_redirects` (output-layout contract §1.9), from\n// the moves `ascribe.addresses` records and the permalinks' addresses. `astro build` merges every\n// project's into the site's `_redirects`, at the root of what's deployed.\n//\n// A site's own `public/_redirects` is kept, after Ascribe's rules: Netlify\n// uses the first rule that matches, so a page's recorded move wins over a\n// pattern of the site's.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { normalizeBase } from \"./project.js\";\nimport { routeKey } from \"./records.js\";\n\n/** One redirect: an old address, from the host's root, to where it leads now. */\nexport interface Redirect {\n from: string;\n /** The query parameters the rule matches, as `name=:name`: a permalink's dimensions. */\n query?: string[];\n to: string;\n status: number;\n /**\n * Whether the rule applies even with a page at `from`, Netlify's `!`: a\n * permalink's, whose forwarding page is there for hosts without redirects.\n */\n force?: boolean;\n}\n\n/**\n * The redirects in the host's `_redirects` the compiler wrote in a build's\n * site output, in its order: none when it has none, as without `[consumer]\n * host`.\n */\nexport function readHostRedirects(siteRoot: string): Redirect[] {\n let text: string;\n try {\n text = readFileSync(path.join(siteRoot, \"_ascribe\", \"host\", \"_redirects\"), \"utf8\");\n } catch {\n return [];\n }\n const out: Redirect[] = [];\n for (const line of text.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (trimmed === \"\" || trimmed.startsWith(\"#\")) continue;\n const [from, ...rest] = trimmed.split(/\\s+/);\n // A query parameter is `name=:name`; the target starts with `/`.\n const query = rest.filter((part) => !part.startsWith(\"/\") && part.includes(\"=\"));\n const [to, status] = rest.slice(query.length);\n if (from === undefined || to === undefined) continue;\n const force = status?.endsWith(\"!\") ?? false;\n out.push({\n from,\n ...(query.length > 0 ? { query } : {}),\n to,\n status: status === undefined ? 301 : Number.parseInt(status, 10),\n ...(force ? { force } : {}),\n });\n }\n return out;\n}\n\n/** What a rule matches: its address, and its query parameters. */\nfunction matches(redirect: Redirect): string {\n return [redirect.from, ...(redirect.query ?? [])].join(\" \");\n}\n\n/**\n * Every project's redirects as one list, in the order given: a rule two\n * projects both write for an address, with the same query parameters, keeps\n * the first's, as the first entry's files win.\n */\nexport function mergeRedirects(projects: readonly (readonly Redirect[])[]): Redirect[] {\n const seen = new Set<string>();\n const out: Redirect[] = [];\n for (const list of projects) {\n for (const redirect of list) {\n if (seen.has(matches(redirect))) continue;\n seen.add(matches(redirect));\n out.push(redirect);\n }\n }\n return out;\n}\n\n/**\n * The redirects in Netlify's `_redirects` format: each on a line, `from`, its\n * query parameters, `to`, and the status, with `!` when it's forced.\n */\nexport function netlifyRedirects(redirects: readonly Redirect[]): string {\n return redirects\n .map((r) => `${matches(r)} ${r.to} ${String(r.status)}${r.force === true ? \"!\" : \"\"}\\n`)\n .join(\"\");\n}\n\n/** What the build does about the host's redirects. */\nexport type RedirectsOutcome =\n | { kind: \"none\" }\n | { kind: \"write\"; text: string }\n | { kind: \"note\"; text: string }\n | { kind: \"fail\"; text: string };\n\n/** What the build does about `_redirects`, given every project's redirects and the site. */\nexport function redirectsOutcome(sources: {\n /** The redirects of every project that sets `[consumer] host = \"netlify\"`, merged. */\n redirects: readonly Redirect[];\n /** Astro's `base`. */\n base: string;\n /** Whether a route is a page the site publishes, which a redirect mustn't hide. */\n published: (route: string) => boolean;\n /** The site's own `public/_redirects`, when it has one. */\n own: string | undefined;\n}): RedirectsOutcome {\n if (sources.redirects.length === 0) return { kind: \"none\" };\n const text = netlifyRedirects(sources.redirects);\n const base = normalizeBase(sources.base);\n if (base !== \"/\") {\n return {\n kind: \"note\",\n text: `no _redirects: Astro's \\`base\\` is \"${base}\", and Netlify reads _redirects only at the root of what's deployed. Put these lines in the host's own:\\n${text}`,\n };\n }\n // A forced rule is meant to pass the page at its address: a permalink's forwarding page.\n const taken = sources.redirects.filter(\n (r) => r.force !== true && sources.published(routeKey(r.from)),\n );\n if (taken.length > 0) {\n return {\n kind: \"fail\",\n text: `a page the site publishes is at an address ascribe.addresses forwards somewhere else, so its redirect would hide the page or never be used. Remove the entry, or move the page:\\n${netlifyRedirects(taken)}`,\n };\n }\n if (sources.own === undefined) return { kind: \"write\", text };\n const own = sources.own.endsWith(\"\\n\") || sources.own === \"\" ? sources.own : `${sources.own}\\n`;\n return {\n kind: \"write\",\n text: `${text}\\n# The site's own, from public/_redirects: after Ascribe's, since the first rule that matches is used.\\n${own}`,\n };\n}\n\n/** The folder a retired address's page is in, under its build's base path. */\nconst GONE_FOLDER = \"/_ascribe/gone/\";\n\n/** The format version of `_ascribe/redirects.json` this reads. */\nconst REDIRECTS_VERSION = 1;\n\n/** A retired address, as a build's `_ascribe/redirects.json` lists it (output-layout contract §1.8). */\ninterface Gone {\n /** The address, from the host's root. */\n path: string;\n /** Why it was retired, as its authors wrote it. */\n reason: string;\n /** The `ascribe.addresses` key it comes from. */\n entry: string;\n /** Where its page is, from the host's root: the base path, `_ascribe/gone/`, and the address from there. */\n page: string;\n}\n\n/** What a retired address's page says: why, and where the docs' home is. */\nexport interface GonePage {\n reason: string;\n home: string;\n}\n\n/** The retired addresses a build's site output lists: none when it has no `_ascribe/redirects.json`. */\nfunction readGone(siteRoot: string): Gone[] {\n const file = path.join(siteRoot, \"_ascribe\", \"redirects.json\");\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return [];\n }\n const list = JSON.parse(text) as { version: number; gone?: Gone[] };\n if (list.version !== REDIRECTS_VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(list.version)} of the redirects list, and this version of the integration reads version ${String(REDIRECTS_VERSION)}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return list.gone ?? [];\n}\n\n/**\n * Each retired address's page the site outputs list, by its route from the\n * host's root, with its folder (its build's base path and `_ascribe/gone/`),\n * its address from there, and what it says. A page two projects both list\n * keeps the first's.\n */\nfunction gonePages(\n sources: readonly { siteRoot: string; home: string }[],\n): Map<string, { folder: string; address: string; props: GonePage }> {\n const pages = new Map<string, { folder: string; address: string; props: GonePage }>();\n for (const { siteRoot, home } of sources) {\n for (const gone of readGone(siteRoot)) {\n const at = gone.page.indexOf(GONE_FOLDER);\n if (at < 0) continue;\n const folder = gone.page.slice(0, at + GONE_FOLDER.length);\n const address = gone.page.slice(folder.length).replace(/\\/+$/, \"\");\n if (address === \"\") continue;\n const route = `${folder}${address}`;\n if (!pages.has(route))\n pages.set(route, { folder, address, props: { reason: gone.reason, home } });\n }\n }\n return pages;\n}\n\n/**\n * The route pattern for the pages in `folder`, as Astro takes it: under\n * `base`, which Astro adds. Throws when the folder isn't under `base`, since\n * an Astro site serves nothing outside it.\n */\nfunction gonePattern(folder: string, base: string): string {\n const root = normalizeBase(base);\n if (!folder.startsWith(root)) {\n throw new Error(\n `@ascribed/astro: retired addresses' pages are published at \"${folder}\", which isn't under Astro's \\`base\\`, \"${base}\", and an Astro site serves nothing outside it. Set [consumer] base-path in ascribe.toml to a path under \"${root}\".`,\n );\n }\n return `/${folder.slice(root.length)}[...address]`;\n}\n\n/**\n * The route patterns the retired addresses' pages need, one for each\n * folder that has any, under `base`.\n */\nexport function gonePatterns(siteRoots: readonly string[], base: string): string[] {\n const pages = gonePages(siteRoots.map((siteRoot) => ({ siteRoot, home: \"/\" })));\n return [...new Set([...pages.values()].map(({ folder }) => gonePattern(folder, base)))];\n}\n\n/**\n * `getStaticPaths` for the route at `routePattern`, one of `gonePatterns`:\n * one page for each retired address's page in its folder, with the reason\n * and the route of its project's root.\n */\nexport function gonePaths(\n sources: readonly { siteRoot: string; home: string }[],\n base: string,\n routePattern: string,\n): { params: { address: string }; props: GonePage }[] {\n return [...gonePages(sources).values()]\n .filter(({ folder }) => gonePattern(folder, base) === routePattern)\n .map(({ address, props }) => ({ params: { address }, props }));\n}\n\n/** The routes of the retired addresses' pages the site outputs at `siteRoots` list, from the host's root. */\nexport function goneRoutes(siteRoots: readonly string[]): string[] {\n return [...gonePages(siteRoots.map((siteRoot) => ({ siteRoot, home: \"/\" }))).keys()];\n}\n"]}
package/dist/run.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  /** What a successful build printed. */
2
2
  export interface BuildResult {
3
- /** The checks' report, which is more than a summary line when there is anything to report. */
3
+ /** The checks' summary line, after each error in full when there are any. */
4
4
  diagnostics: string;
5
- /** The `built <build>/site: …` lines. */
5
+ /** The `built <build>: …` lines, one per build. */
6
6
  summary: string;
7
7
  }
8
8
  /**
9
- * Whether a build's report is worth a warning: anything but a report whose
10
- * summary line counts no errors and no warnings, which is logged at info
11
- * level, advice and all.
9
+ * Whether a build's report is worth a warning: anything but a summary line
10
+ * that counts no errors and no warnings, which is logged at info level,
11
+ * advice and all.
12
12
  */
13
13
  export declare function hasWarnings(diagnostics: string): boolean;
14
14
  /**
package/dist/run.js CHANGED
@@ -2,20 +2,24 @@
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 for build \`site\`, reported on 14:
6
- * 0 errors, 0 warnings, 0 advice`.
5
+ * The checks' summary line, the report's last: `checked 14 files in build \`site\`: no problems`,
6
+ * or the counts, with the command that lists the warnings and advice when there are any:
7
+ * `checked 14 files in build \`site\`: 0 errors, 1 warning, 3 advice: ascribe check --build site
8
+ * for each`.
7
9
  */
8
- const SUMMARY = /^checked [^:\n]*: (\d+) errors?, (\d+) warnings?, \d+ advice$/;
10
+ const SUMMARY = /^checked [^:\n]*: (?:no problems|(\d+) errors?, (\d+) warnings?, \d+ advice)(?:, \d+ acknowledged)?(?:: .* for each)?$/;
9
11
  /**
10
- * Whether a build's report is worth a warning: anything but a report whose
11
- * summary line counts no errors and no warnings, which is logged at info
12
- * level, advice and all.
12
+ * Whether a build's report is worth a warning: anything but a summary line
13
+ * that counts no errors and no warnings, which is logged at info level,
14
+ * advice and all.
13
15
  */
14
16
  export function hasWarnings(diagnostics) {
15
17
  if (diagnostics === "")
16
18
  return false;
17
19
  const summary = SUMMARY.exec(diagnostics.trimEnd().split("\n").at(-1) ?? "");
18
- return summary === null || summary[1] !== "0" || summary[2] !== "0";
20
+ if (summary === null)
21
+ return true;
22
+ return (summary[1] ?? "0") !== "0" || (summary[2] ?? "0") !== "0";
19
23
  }
20
24
  /**
21
25
  * Whether an Astro command builds with source anchors, given the integration's
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,+DAA+D,CAAC;AAEhF;;;;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,OAUxB;IACC,MAAM,MAAM,GAAG,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IACnF,MAAM,IAAI,GAAG;QACX,OAAO;QACP,QAAQ;QACR,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QACvC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QAChD,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,mBAAmB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,EAAE,EACzI,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`.\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, or several builds', 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 /** The build, or several, each a `--build`. */\n build: string | readonly 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 builds = typeof options.build === \"string\" ? [options.build] : options.build;\n const args = [\n \"build\",\n \"--emit\",\n (options.outputs ?? [\"site\"]).join(\",\"),\n ...builds.flatMap((build) => [\"--build\", 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 ${builds.map((build) => `--build ${build}`).join(\" \")}\\` 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":"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","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' summary line, after each error in full when there are any. */\n diagnostics: string;\n /** The `built <build>: …` lines, one per build. */\n summary: string;\n}\n\n/**\n * The checks' summary line, the report's last: `checked 14 files in build \\`site\\`: no problems`,\n * or the counts, with the command that lists the warnings and advice when there are any:\n * `checked 14 files in build \\`site\\`: 0 errors, 1 warning, 3 advice: ascribe check --build site\n * for each`.\n */\nconst SUMMARY =\n /^checked [^:\\n]*: (?:no problems|(\\d+) errors?, (\\d+) warnings?, \\d+ advice)(?:, \\d+ acknowledged)?(?:: .* for each)?$/;\n\n/**\n * Whether a build's report is worth a warning: anything but a summary line\n * that counts no errors and no warnings, which is logged at info level,\n * 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 if (summary === null) return true;\n return (summary[1] ?? \"0\") !== \"0\" || (summary[2] ?? \"0\") !== \"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, or several builds', 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 /** The build, or several, each a `--build`. */\n build: string | readonly 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 builds = typeof options.build === \"string\" ? [options.build] : options.build;\n const args = [\n \"build\",\n \"--emit\",\n (options.outputs ?? [\"site\"]).join(\",\"),\n ...builds.flatMap((build) => [\"--build\", 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 ${builds.map((build) => `--build ${build}`).join(\" \")}\\` 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/dist/shapes.d.ts CHANGED
@@ -100,6 +100,13 @@ export interface DiffReport {
100
100
  */
101
101
  blocks_omitted?: boolean;
102
102
  }
103
+ /** A page that moved: where it was, and where it is, by content path. */
104
+ export interface MovedPage {
105
+ /** Where it was, in the base. */
106
+ from: string;
107
+ /** Where it is now. */
108
+ to: string;
109
+ }
103
110
  /** What changed on one page of a build. */
104
111
  export interface PageDiff {
105
112
  /** The page's content path. */
@@ -117,14 +124,22 @@ export interface PageDiff {
117
124
  * The other changed files the page's change can come from: fragments it
118
125
  * includes and pages its links take a title or a heading from, in path
119
126
  * order; then the snippets whose code changed, by address
120
- * (`code:app.py#main`); and `ascribe.toml` (last) when the content model
121
- * is a cause.
127
+ * (`code:app.py#main`); then the operations of a declared API whose spec
128
+ * changed what the page writes for them, an `@operation`'s method, path,
129
+ * summary, or deprecation, or a link's address, each by its spec's path
130
+ * from the project root, `#`, and the fragment the page names it by
131
+ * (`api/openapi.yaml#createWidget`); and `ascribe.toml` (last) when the
132
+ * content model is a cause. The kinds are told apart by their form: a
133
+ * content file's path ends in `.md`, a snippet's address has its
134
+ * source's name and `:` before its path, and an operation's has a `#`
135
+ * and neither.
122
136
  */
123
137
  because: string[];
124
138
  /**
125
139
  * What changed about the page itself besides its blocks, in this
126
140
  * order: `title`, `frontmatter`, `availability` (the page-level one),
127
- * and `route`. Empty for an added or removed page.
141
+ * and `route`. Empty for an added or removed page. A moved page's
142
+ * route moves with it, so it isn't listed for one.
128
143
  */
129
144
  page_changed: string[];
130
145
  /** How many changes of each kind. */
@@ -135,6 +150,21 @@ export interface PageDiff {
135
150
  * the report's `blocks_omitted` is `true`.
136
151
  */
137
152
  changes: Change[];
153
+ /**
154
+ * For a page that moved, the content path it was at: git sees its
155
+ * file renamed, or `ascribe.addresses` records the move. The page is
156
+ * listed once, at `path`, compared with the page as it was at
157
+ * `moved_from`; `status` is `changed`, and its `counts` and `changes`
158
+ * are what differs besides the move, none for a page that only moved.
159
+ * Absent for a page that didn't move.
160
+ */
161
+ moved_from?: string;
162
+ /**
163
+ * The pages among `because` whose only change is that they moved, each
164
+ * by where it was and where it is: a link on this page leads to its new
165
+ * address. Absent when there's none.
166
+ */
167
+ moved_links?: MovedPage[];
138
168
  }
139
169
  /** Whether a page is new, gone, or different. */
140
170
  export type PageStatus = "added" | "removed" | "changed";
@@ -1 +1 @@
1
- {"version":3,"file":"shapes.js","sourceRoot":"","sources":["../src/shapes.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,2EAA2E;AAC3E,6EAA6E;AAC7E,yEAAyE;AACzE,EAAE;AACF,8CAA8C","sourcesContent":["// The JSON Ascribe writes that this package reads, as TypeScript types.\n// Generated from the Rust types that write it, through the JSON Schemas in\n// schemas/, by crates/ascribe-cli/src/shapes.rs. Change the Rust types, then\n// run `ASCRIBE_BLESS=1 cargo test -p ascribe-cli shapes`. Don't edit it.\n//\n// - DiffReport (`ascribe diff --format json`)\n\n/**\n * Where a block's text is written: the README's anchor grammar, the same\n * string the rendered page carries in `data-ascribe-source` and\n * `data-ascribe-via`.\n */\nexport interface Anchor {\n /**\n * `<path>:<first>-<last>`: the file's content path, percent-encoded by\n * segment, and the block's first and last lines, from 1.\n */\n source: string;\n /**\n * The includes the block came through, outermost first, each\n * `<path>:<line>`. Empty for a block written in the page itself.\n */\n via: string[];\n}\n\n/** The base of a comparison, in the report. */\nexport interface BaseInfo {\n /** The revision asked for, or the default branch used. */\n requested: string;\n /** The commit it names. */\n commit: string;\n /**\n * The merge base of that commit and `HEAD`, which the comparison reads;\n * `null` with `--base-exact`, which reads `commit`.\n */\n merge_base: string | null;\n}\n\n/** What changed in one build. */\nexport interface BuildDiff {\n /** The build's name. */\n build: string;\n /** The pages that changed, in path order. */\n pages: PageDiff[];\n}\n\n/** One block's change. */\nexport interface Change {\n /** What happened to it. */\n kind: ChangeKind;\n /**\n * For a removed block, and a moved block's old place: the block of the\n * new version it came after, among its siblings. Absent when it was\n * first.\n */\n after?: Anchor;\n /** Where it's written now: every kind but `removed`. */\n now?: Anchor;\n /**\n * For a removed block, and a moved block's old place: the block of the\n * new version it was inside. Absent at the top of the page.\n */\n parent?: Anchor;\n /**\n * For a removed block, its text, whitespace collapsed, so it can be\n * shown where it was.\n */\n text?: string;\n /** Where it was written: every kind but `added`. */\n was?: Anchor;\n /** For changed prose, the words that differ. */\n words?: Words;\n}\n\n/** The kinds of block change. */\nexport type ChangeKind = \"changed\" | \"added\" | \"removed\" | \"moved\";\n\n/** How many changes of each kind a page has. */\nexport interface Counts {\n /** Blocks whose content changed. */\n changed: number;\n /** Blocks only in the new version. */\n added: number;\n /** Blocks only in the old version. */\n removed: number;\n /** Blocks in both, somewhere else. */\n moved: number;\n}\n\n/** The whole report, as `ascribe diff --format json` writes it. */\nexport interface DiffReport {\n /** `SCHEMA_VERSION`. */\n schema_version: number;\n /** The version of Ascribe that wrote it. */\n ascribe_version: string;\n /** What was compared with. */\n base: BaseInfo;\n /** Where the project is. */\n repository: RepositoryInfo;\n /**\n * How many errors `ascribe check` finds in the working tree, for the\n * builds compared. The comparison runs regardless, but a page with an\n * error may not render as it will once it's fixed, so a reviewer should\n * know. `diff_project` sets it; `Report::new` leaves it zero.\n */\n working_tree_errors: number;\n /** What changed, per build, in the order asked for. */\n builds: BuildDiff[];\n /**\n * Whether every page's `changes` was left empty, as `ascribe diff\n * --pages-only` leaves them: its `counts` still count them. Only there\n * when it's `true`.\n */\n blocks_omitted?: boolean;\n}\n\n/** What changed on one page of a build. */\nexport interface PageDiff {\n /** The page's content path. */\n path: string;\n /** Its route: the new one, or for a removed page the old one. */\n route: string;\n /**\n * Whether the build publishes it only now, only before, or in both\n * with a difference.\n */\n status: PageStatus;\n /** Whether the page's own file changed (or exists on one side only). */\n own_file_changed: boolean;\n /**\n * The other changed files the page's change can come from: fragments it\n * includes and pages its links take a title or a heading from, in path\n * order; then the snippets whose code changed, by address\n * (`code:app.py#main`); and `ascribe.toml` (last) when the content model\n * is a cause.\n */\n because: string[];\n /**\n * What changed about the page itself besides its blocks, in this\n * order: `title`, `frontmatter`, `availability` (the page-level one),\n * and `route`. Empty for an added or removed page.\n */\n page_changed: string[];\n /** How many changes of each kind. */\n counts: Counts;\n /**\n * The block-level changes, in the page's order, a removed block where\n * it was. Empty for an added or removed page, and for every page when\n * the report's `blocks_omitted` is `true`.\n */\n changes: Change[];\n}\n\n/** Whether a page is new, gone, or different. */\nexport type PageStatus = \"added\" | \"removed\" | \"changed\";\n\n/** The repository, in the report. */\nexport interface RepositoryInfo {\n /** The repository's top-level directory, as git prints it. */\n root: string;\n /**\n * The project's folder inside it, with a trailing `/`, or empty when\n * the project is at the repository's root.\n */\n project_prefix: string;\n}\n\n/**\n * The words that differ inside a changed block of prose. Ranges are\n * `[start, end)` in characters (Unicode scalar values) of each side's\n * `text`, which is the block's text with whitespace collapsed.\n */\nexport interface Words {\n /** Ranges in `now_text`: words added or replacing others. */\n now: [number, number][];\n /** Ranges in `was_text`: words removed or replaced. */\n was: [number, number][];\n /** The block's text now. */\n now_text: string;\n /** The block's text before. */\n was_text: string;\n}\n"]}
1
+ {"version":3,"file":"shapes.js","sourceRoot":"","sources":["../src/shapes.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,2EAA2E;AAC3E,6EAA6E;AAC7E,yEAAyE;AACzE,EAAE;AACF,8CAA8C","sourcesContent":["// The JSON Ascribe writes that this package reads, as TypeScript types.\n// Generated from the Rust types that write it, through the JSON Schemas in\n// schemas/, by crates/ascribe-cli/src/shapes.rs. Change the Rust types, then\n// run `ASCRIBE_BLESS=1 cargo test -p ascribe-cli shapes`. Don't edit it.\n//\n// - DiffReport (`ascribe diff --format json`)\n\n/**\n * Where a block's text is written: the README's anchor grammar, the same\n * string the rendered page carries in `data-ascribe-source` and\n * `data-ascribe-via`.\n */\nexport interface Anchor {\n /**\n * `<path>:<first>-<last>`: the file's content path, percent-encoded by\n * segment, and the block's first and last lines, from 1.\n */\n source: string;\n /**\n * The includes the block came through, outermost first, each\n * `<path>:<line>`. Empty for a block written in the page itself.\n */\n via: string[];\n}\n\n/** The base of a comparison, in the report. */\nexport interface BaseInfo {\n /** The revision asked for, or the default branch used. */\n requested: string;\n /** The commit it names. */\n commit: string;\n /**\n * The merge base of that commit and `HEAD`, which the comparison reads;\n * `null` with `--base-exact`, which reads `commit`.\n */\n merge_base: string | null;\n}\n\n/** What changed in one build. */\nexport interface BuildDiff {\n /** The build's name. */\n build: string;\n /** The pages that changed, in path order. */\n pages: PageDiff[];\n}\n\n/** One block's change. */\nexport interface Change {\n /** What happened to it. */\n kind: ChangeKind;\n /**\n * For a removed block, and a moved block's old place: the block of the\n * new version it came after, among its siblings. Absent when it was\n * first.\n */\n after?: Anchor;\n /** Where it's written now: every kind but `removed`. */\n now?: Anchor;\n /**\n * For a removed block, and a moved block's old place: the block of the\n * new version it was inside. Absent at the top of the page.\n */\n parent?: Anchor;\n /**\n * For a removed block, its text, whitespace collapsed, so it can be\n * shown where it was.\n */\n text?: string;\n /** Where it was written: every kind but `added`. */\n was?: Anchor;\n /** For changed prose, the words that differ. */\n words?: Words;\n}\n\n/** The kinds of block change. */\nexport type ChangeKind = \"changed\" | \"added\" | \"removed\" | \"moved\";\n\n/** How many changes of each kind a page has. */\nexport interface Counts {\n /** Blocks whose content changed. */\n changed: number;\n /** Blocks only in the new version. */\n added: number;\n /** Blocks only in the old version. */\n removed: number;\n /** Blocks in both, somewhere else. */\n moved: number;\n}\n\n/** The whole report, as `ascribe diff --format json` writes it. */\nexport interface DiffReport {\n /** `SCHEMA_VERSION`. */\n schema_version: number;\n /** The version of Ascribe that wrote it. */\n ascribe_version: string;\n /** What was compared with. */\n base: BaseInfo;\n /** Where the project is. */\n repository: RepositoryInfo;\n /**\n * How many errors `ascribe check` finds in the working tree, for the\n * builds compared. The comparison runs regardless, but a page with an\n * error may not render as it will once it's fixed, so a reviewer should\n * know. `diff_project` sets it; `Report::new` leaves it zero.\n */\n working_tree_errors: number;\n /** What changed, per build, in the order asked for. */\n builds: BuildDiff[];\n /**\n * Whether every page's `changes` was left empty, as `ascribe diff\n * --pages-only` leaves them: its `counts` still count them. Only there\n * when it's `true`.\n */\n blocks_omitted?: boolean;\n}\n\n/** A page that moved: where it was, and where it is, by content path. */\nexport interface MovedPage {\n /** Where it was, in the base. */\n from: string;\n /** Where it is now. */\n to: string;\n}\n\n/** What changed on one page of a build. */\nexport interface PageDiff {\n /** The page's content path. */\n path: string;\n /** Its route: the new one, or for a removed page the old one. */\n route: string;\n /**\n * Whether the build publishes it only now, only before, or in both\n * with a difference.\n */\n status: PageStatus;\n /** Whether the page's own file changed (or exists on one side only). */\n own_file_changed: boolean;\n /**\n * The other changed files the page's change can come from: fragments it\n * includes and pages its links take a title or a heading from, in path\n * order; then the snippets whose code changed, by address\n * (`code:app.py#main`); then the operations of a declared API whose spec\n * changed what the page writes for them, an `@operation`'s method, path,\n * summary, or deprecation, or a link's address, each by its spec's path\n * from the project root, `#`, and the fragment the page names it by\n * (`api/openapi.yaml#createWidget`); and `ascribe.toml` (last) when the\n * content model is a cause. The kinds are told apart by their form: a\n * content file's path ends in `.md`, a snippet's address has its\n * source's name and `:` before its path, and an operation's has a `#`\n * and neither.\n */\n because: string[];\n /**\n * What changed about the page itself besides its blocks, in this\n * order: `title`, `frontmatter`, `availability` (the page-level one),\n * and `route`. Empty for an added or removed page. A moved page's\n * route moves with it, so it isn't listed for one.\n */\n page_changed: string[];\n /** How many changes of each kind. */\n counts: Counts;\n /**\n * The block-level changes, in the page's order, a removed block where\n * it was. Empty for an added or removed page, and for every page when\n * the report's `blocks_omitted` is `true`.\n */\n changes: Change[];\n /**\n * For a page that moved, the content path it was at: git sees its\n * file renamed, or `ascribe.addresses` records the move. The page is\n * listed once, at `path`, compared with the page as it was at\n * `moved_from`; `status` is `changed`, and its `counts` and `changes`\n * are what differs besides the move, none for a page that only moved.\n * Absent for a page that didn't move.\n */\n moved_from?: string;\n /**\n * The pages among `because` whose only change is that they moved, each\n * by where it was and where it is: a link on this page leads to its new\n * address. Absent when there's none.\n */\n moved_links?: MovedPage[];\n}\n\n/** Whether a page is new, gone, or different. */\nexport type PageStatus = \"added\" | \"removed\" | \"changed\";\n\n/** The repository, in the report. */\nexport interface RepositoryInfo {\n /** The repository's top-level directory, as git prints it. */\n root: string;\n /**\n * The project's folder inside it, with a trailing `/`, or empty when\n * the project is at the repository's root.\n */\n project_prefix: string;\n}\n\n/**\n * The words that differ inside a changed block of prose. Ranges are\n * `[start, end)` in characters (Unicode scalar values) of each side's\n * `text`, which is the block's text with whitespace collapsed.\n */\nexport interface Words {\n /** Ranges in `now_text`: words added or replacing others. */\n now: [number, number][];\n /** Ranges in `was_text`: words removed or replaced. */\n was: [number, number][];\n /** The block's text now. */\n now_text: string;\n /** The block's text before. */\n was_text: string;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascribed/astro",
3
- "version": "0.4.4",
3
+ "version": "0.4.5-next.34",
4
4
  "description": "A toolchain for documentation written as code. This package is its Astro integration, which publishes a project's pages as a site.",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
@@ -72,9 +72,9 @@
72
72
  "node": ">=24"
73
73
  },
74
74
  "dependencies": {
75
- "@ascribed/cli": "0.4.4",
76
- "@ascribed/elements": "0.4.4",
77
- "@ascribed/review": "0.4.4",
75
+ "@ascribed/cli": "0.4.5-next.34",
76
+ "@ascribed/elements": "0.4.5-next.34",
77
+ "@ascribed/review": "0.4.5-next.34",
78
78
  "github-slugger": "^2.0.0",
79
79
  "property-information": "^7.2.0",
80
80
  "smol-toml": "^1.9.1"
@@ -110,6 +110,7 @@
110
110
  "publishConfig": {
111
111
  "access": "public"
112
112
  },
113
+ "gitHead": "570d8f155ffbc71857ce05a47a70840f1bbab67e",
113
114
  "scripts": {
114
115
  "build": "tsc -p tsconfig.build.json",
115
116
  "typecheck": "tsc --noEmit",
@@ -10,8 +10,8 @@
10
10
  //
11
11
  // `home` is where it sends the reader: by default the root of the project whose
12
12
  // permalinks it explains, which with several projects is the first that has
13
- // any. Children replace its last paragraph, which offers the home page and
14
- // searching.
13
+ // any. Children replace its last paragraph, which offers the home page: a site
14
+ // with search can offer that too.
15
15
  import { home as root, permalinks } from "virtual:ascribe/site";
16
16
  import { servedList } from "@ascribed/astro/permalinks";
17
17
 
@@ -33,7 +33,7 @@ const path = served?.path;
33
33
  older version of the product than they describe.
34
34
  </p>
35
35
  <slot>
36
- <p>Start from the <a href={home}>home page</a>, or search the docs for what you need.</p>
36
+ <p>Start from the <a href={home}>home page</a>.</p>
37
37
  </slot>
38
38
  </section>
39
39
  )
@@ -5,9 +5,12 @@
5
5
  // `_ascribe/permalinks.json`. It forwards without a script, links to the same
6
6
  // place for a browser that doesn't follow, and isn't indexed. Its script
7
7
  // first takes the reader's choices from the address's parameters
8
- // (`?platform=macos`), as if they'd picked those tabs, then forwards.
8
+ // (`?platform=macos`), as if they'd picked those tabs, then forwards. A host
9
+ // that takes redirects forwards with a 301 instead, and serves this page only
10
+ // for an address with parameters. The page names its section's Markdown and
11
+ // JSON as its alternates, for an agent that doesn't follow it.
9
12
  //
10
- // A retired permalink with nowhere to lead gets a page that says what was
13
+ // A retired permalink gets a page that says what was
11
14
  // here was removed, gives the reason as its authors wrote it, as text, and
12
15
  // links to the docs' home: the root of the project whose list named it.
13
16
  import { home as root, permalinks } from "virtual:ascribe/site";
@@ -20,6 +23,7 @@ export function getStaticPaths() {
20
23
  const props = Astro.props as Forwarding;
21
24
  const to = "to" in props ? props.to : undefined;
22
25
  const dimensions = "to" in props ? props.dimensions : {};
26
+ const alternates = "to" in props ? props.alternates : undefined;
23
27
  const reason = "reason" in props ? props.reason : undefined;
24
28
  const home = ("reason" in props ? props.home : undefined) ?? root;
25
29
  const canonical =
@@ -35,6 +39,14 @@ const canonical =
35
39
  <meta name="robots" content="noindex" />
36
40
  {to !== undefined && <meta http-equiv="refresh" content={`0;url=${to}`} />}
37
41
  {canonical !== undefined && <link rel="canonical" href={canonical} />}
42
+ {
43
+ alternates !== undefined && (
44
+ <>
45
+ <link rel="alternate" type="text/markdown" href={alternates.markdown} />
46
+ <link rel="alternate" type="application/json" href={alternates.json} />
47
+ </>
48
+ )
49
+ }
38
50
  </head>
39
51
  <body>
40
52
  {