@ascribed/astro 0.2.1-next.15 → 0.2.1-next.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -7
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -0
- package/dist/content.d.ts +26 -0
- package/dist/content.js +70 -1
- package/dist/content.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +87 -11
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +41 -0
- package/dist/init.js +130 -0
- package/dist/init.js.map +1 -0
- package/dist/nav.d.ts +51 -0
- package/dist/nav.js +97 -0
- package/dist/nav.js.map +1 -0
- package/dist/permalinks.d.ts +6 -0
- package/dist/permalinks.js +13 -0
- package/dist/permalinks.js.map +1 -1
- package/dist/project.d.ts +24 -0
- package/dist/project.js +31 -0
- package/dist/project.js.map +1 -1
- package/dist/projects.d.ts +0 -6
- package/dist/projects.js +1 -14
- package/dist/projects.js.map +1 -1
- package/dist/records.d.ts +78 -0
- package/dist/records.js +153 -0
- package/dist/records.js.map +1 -0
- package/dist/root-files.d.ts +63 -0
- package/dist/root-files.js +100 -0
- package/dist/root-files.js.map +1 -0
- package/dist/run.js +3 -2
- package/dist/run.js.map +1 -1
- package/package.json +12 -6
- package/src/Head.astro +40 -0
- package/starter/src/content.config.ts +8 -0
- package/starter/src/layouts/Docs.astro +87 -0
- package/starter/src/nav.ts +13 -0
- package/starter/src/pages/[...slug].astro +20 -0
package/dist/permalinks.js.map
CHANGED
|
@@ -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"]}
|
|
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"]}
|
package/dist/project.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface ProjectInfo {
|
|
|
16
16
|
/** Whether builds publish what agents read: `llms.txt` and each page's Markdown, in the plain output. */
|
|
17
17
|
agents: boolean;
|
|
18
18
|
};
|
|
19
|
+
/** The names of `[builds]`'s tables, in order: empty when there are none, and the one build is `site`. */
|
|
20
|
+
builds: string[];
|
|
19
21
|
/** The site output's root for a build: `<output-dir>/<build>/site`. */
|
|
20
22
|
siteRoot(build: string): string;
|
|
21
23
|
/** The plain output's root for a build: `<output-dir>/<build>/plain`. */
|
|
@@ -32,6 +34,28 @@ export declare function normalizeBase(base: string): string;
|
|
|
32
34
|
* package's components.)
|
|
33
35
|
*/
|
|
34
36
|
export declare function homeRoute(base: string, trailingSlash: "always" | "never" | "ignore"): string;
|
|
37
|
+
/** Astro's routing settings, as the integration reads and supplies them. */
|
|
38
|
+
export interface AstroRouting {
|
|
39
|
+
base: string;
|
|
40
|
+
trailingSlash: "always" | "never" | "ignore";
|
|
41
|
+
site: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Astro's routing with what astro.config leaves unset taken from `[consumer]`,
|
|
45
|
+
* and the settings taken, for `updateConfig`. Astro gives an integration its
|
|
46
|
+
* configuration with its defaults filled in, so unset is the default: no
|
|
47
|
+
* `site`, `trailingSlash: "ignore"`, and `base: "/"`. `site` and
|
|
48
|
+
* `trailingSlash` come from the first project that has them, since every
|
|
49
|
+
* project must agree with them anyway. `base` comes from the first project's
|
|
50
|
+
* `base-path` only with `supplyBase`, as with one project; with several, it
|
|
51
|
+
* stays the site's, the root every base path is under. A base Astro has is
|
|
52
|
+
* rewritten to end the way a supplied `trailingSlash` says, as Astro writes
|
|
53
|
+
* it when the setting is its own.
|
|
54
|
+
*/
|
|
55
|
+
export declare function supplyRouting(projects: readonly ProjectInfo[], astro: AstroRouting, supplyBase: boolean): {
|
|
56
|
+
routing: AstroRouting;
|
|
57
|
+
supplied: Partial<AstroRouting>;
|
|
58
|
+
};
|
|
35
59
|
/**
|
|
36
60
|
* The ways `ascribe.toml`'s `[consumer]` disagrees with Astro's configuration,
|
|
37
61
|
* as sentences. Ascribe writes every link with its own settings, so a site
|
package/dist/project.js
CHANGED
|
@@ -31,6 +31,7 @@ export function readProject(dir) {
|
|
|
31
31
|
trailingSlash,
|
|
32
32
|
agents: consumer["agents"] === true,
|
|
33
33
|
},
|
|
34
|
+
builds: Object.keys(section(table, "builds")),
|
|
34
35
|
siteRoot: (build) => path.resolve(dir, outputDir, build, "site"),
|
|
35
36
|
plainRoot: (build) => path.resolve(dir, outputDir, build, "plain"),
|
|
36
37
|
};
|
|
@@ -60,6 +61,36 @@ export function homeRoute(base, trailingSlash) {
|
|
|
60
61
|
const root = normalizeBase(base);
|
|
61
62
|
return trailingSlash === "never" && root !== "/" ? root.slice(0, -1) : root;
|
|
62
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Astro's routing with what astro.config leaves unset taken from `[consumer]`,
|
|
66
|
+
* and the settings taken, for `updateConfig`. Astro gives an integration its
|
|
67
|
+
* configuration with its defaults filled in, so unset is the default: no
|
|
68
|
+
* `site`, `trailingSlash: "ignore"`, and `base: "/"`. `site` and
|
|
69
|
+
* `trailingSlash` come from the first project that has them, since every
|
|
70
|
+
* project must agree with them anyway. `base` comes from the first project's
|
|
71
|
+
* `base-path` only with `supplyBase`, as with one project; with several, it
|
|
72
|
+
* stays the site's, the root every base path is under. A base Astro has is
|
|
73
|
+
* rewritten to end the way a supplied `trailingSlash` says, as Astro writes
|
|
74
|
+
* it when the setting is its own.
|
|
75
|
+
*/
|
|
76
|
+
export function supplyRouting(projects, astro, supplyBase) {
|
|
77
|
+
const supplied = {};
|
|
78
|
+
if (astro.site === undefined) {
|
|
79
|
+
const site = projects.find((project) => project.consumer.site !== undefined)?.consumer.site;
|
|
80
|
+
if (site !== undefined)
|
|
81
|
+
supplied.site = site;
|
|
82
|
+
}
|
|
83
|
+
const first = projects[0];
|
|
84
|
+
if (first === undefined)
|
|
85
|
+
return { routing: astro, supplied };
|
|
86
|
+
if (astro.trailingSlash === "ignore")
|
|
87
|
+
supplied.trailingSlash = first.consumer.trailingSlash;
|
|
88
|
+
const trailingSlash = supplied.trailingSlash ?? astro.trailingSlash;
|
|
89
|
+
const base = homeRoute(supplyBase && normalizeBase(astro.base) === "/" ? first.consumer.basePath : astro.base, trailingSlash);
|
|
90
|
+
if (base !== astro.base)
|
|
91
|
+
supplied.base = base;
|
|
92
|
+
return { routing: { ...astro, ...supplied }, supplied };
|
|
93
|
+
}
|
|
63
94
|
/**
|
|
64
95
|
* The ways `ascribe.toml`'s `[consumer]` disagrees with Astro's configuration,
|
|
65
96
|
* as sentences. Ascribe writes every link with its own settings, so a site
|
package/dist/project.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["// What the integration needs from `ascribe.toml`: where a build's site output\n// goes, and the consumer settings that must agree with `astro.config`.\n// The compiler validates the whole file; this reads\n// only what it compiled.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { parse } from \"smol-toml\";\n\n/** The parts of an Ascribe project the integration reads. */\nexport interface ProjectInfo {\n /** The directory holding `ascribe.toml`. */\n dir: string;\n /** The path of `ascribe.toml`. */\n configPath: string;\n /** Source files and assets watched during development. */\n contentRoot: string;\n /** Generated output, never treated as a source change. */\n outputRoot: string;\n /** `[consumer]`, with the profile's defaults. */\n consumer: {\n site: string | undefined;\n basePath: string;\n trailingSlash: \"always\" | \"never\";\n /** Whether builds publish what agents read: `llms.txt` and each page's Markdown, in the plain output. */\n agents: boolean;\n };\n /** The site output's root for a build: `<output-dir>/<build>/site`. */\n siteRoot(build: string): string;\n /** The plain output's root for a build: `<output-dir>/<build>/plain`. */\n plainRoot(build: string): string;\n}\n\n/** Reads `ascribe.toml` in `dir`. Throws a readable error if it can't be read. */\nexport function readProject(dir: string): ProjectInfo {\n const configPath = path.join(dir, \"ascribe.toml\");\n let table: Record<string, unknown>;\n try {\n table = parse(readFileSync(configPath, \"utf8\"));\n } catch (error) {\n throw new Error(\n `@ascribed/astro can't read ${configPath}: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n const project = section(table, \"project\");\n const consumer = section(table, \"consumer\");\n const outputDir =\n typeof project[\"output-dir\"] === \"string\" ? project[\"output-dir\"] : \".ascribe/build\";\n const contentDir = typeof project[\"content-root\"] === \"string\" ? project[\"content-root\"] : \"docs\";\n const trailingSlash = consumer[\"trailing-slash\"] === \"never\" ? \"never\" : \"always\";\n return {\n dir,\n configPath,\n contentRoot: path.resolve(dir, contentDir),\n outputRoot: path.resolve(dir, outputDir),\n consumer: {\n site: typeof consumer[\"site\"] === \"string\" ? consumer[\"site\"] : undefined,\n basePath: normalizeBase(\n typeof consumer[\"base-path\"] === \"string\" ? consumer[\"base-path\"] : \"/\",\n ),\n trailingSlash,\n agents: consumer[\"agents\"] === true,\n },\n siteRoot: (build) => path.resolve(dir, outputDir, build, \"site\"),\n plainRoot: (build) => path.resolve(dir, outputDir, build, \"plain\"),\n };\n}\n\nfunction section(table: Record<string, unknown>, name: string): Record<string, unknown> {\n const value = table[name];\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\n/** A base path with a leading and a trailing `/`, as Ascribe's router has it. */\nexport function normalizeBase(base: string): string {\n let result = base.trim();\n if (!result.startsWith(\"/\")) result = `/${result}`;\n if (!result.endsWith(\"/\")) result += \"/\";\n return result;\n}\n\n/**\n * The route of a base path, ending the way `trailingSlash` says the site's\n * routes do: the site's root for Astro's `base`, a project's root for its\n * `[consumer] base-path`. (`import.meta.env.BASE_URL` isn't replaced in a\n * package's components.)\n */\nexport function homeRoute(base: string, trailingSlash: \"always\" | \"never\" | \"ignore\"): string {\n const root = normalizeBase(base);\n return trailingSlash === \"never\" && root !== \"/\" ? root.slice(0, -1) : root;\n}\n\n/**\n * The ways `ascribe.toml`'s `[consumer]` disagrees with Astro's configuration,\n * as sentences. Ascribe writes every link with its own settings, so a site\n * whose routes differ would have broken links.\n */\nexport function consumerMismatches(\n project: ProjectInfo,\n astro: { base: string; trailingSlash: string; site: string | undefined },\n): string[] {\n const problems: string[] = [];\n const { basePath, trailingSlash, site } = project.consumer;\n // A base path under Astro's `base` leaves the rest of the site to other pages, such as a home page.\n if (!basePath.startsWith(normalizeBase(astro.base))) {\n problems.push(\n `[consumer] base-path is \"${basePath}\", but Astro's \\`base\\` is \"${astro.base}\". Set base-path to that path or a path under it.`,\n );\n }\n // Astro's \"ignore\" serves both forms, so Ascribe's links work either way.\n if (astro.trailingSlash !== \"ignore\" && astro.trailingSlash !== trailingSlash) {\n problems.push(\n `[consumer] trailing-slash is \"${trailingSlash}\", but Astro's \\`trailingSlash\\` is \"${astro.trailingSlash}\".`,\n );\n }\n if (site !== undefined && astro.site !== undefined && origin(site) !== origin(astro.site)) {\n problems.push(`[consumer] site is \"${site}\", but Astro's \\`site\\` is \"${astro.site}\".`);\n }\n return problems;\n}\n\nfunction origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return url;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["// What the integration needs from `ascribe.toml`: where a build's site output\n// goes, and the consumer settings that must agree with `astro.config`.\n// The compiler validates the whole file; this reads\n// only what it compiled.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { parse } from \"smol-toml\";\n\n/** The parts of an Ascribe project the integration reads. */\nexport interface ProjectInfo {\n /** The directory holding `ascribe.toml`. */\n dir: string;\n /** The path of `ascribe.toml`. */\n configPath: string;\n /** Source files and assets watched during development. */\n contentRoot: string;\n /** Generated output, never treated as a source change. */\n outputRoot: string;\n /** `[consumer]`, with the profile's defaults. */\n consumer: {\n site: string | undefined;\n basePath: string;\n trailingSlash: \"always\" | \"never\";\n /** Whether builds publish what agents read: `llms.txt` and each page's Markdown, in the plain output. */\n agents: boolean;\n };\n /** The names of `[builds]`'s tables, in order: empty when there are none, and the one build is `site`. */\n builds: string[];\n /** The site output's root for a build: `<output-dir>/<build>/site`. */\n siteRoot(build: string): string;\n /** The plain output's root for a build: `<output-dir>/<build>/plain`. */\n plainRoot(build: string): string;\n}\n\n/** Reads `ascribe.toml` in `dir`. Throws a readable error if it can't be read. */\nexport function readProject(dir: string): ProjectInfo {\n const configPath = path.join(dir, \"ascribe.toml\");\n let table: Record<string, unknown>;\n try {\n table = parse(readFileSync(configPath, \"utf8\"));\n } catch (error) {\n throw new Error(\n `@ascribed/astro can't read ${configPath}: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n const project = section(table, \"project\");\n const consumer = section(table, \"consumer\");\n const outputDir =\n typeof project[\"output-dir\"] === \"string\" ? project[\"output-dir\"] : \".ascribe/build\";\n const contentDir = typeof project[\"content-root\"] === \"string\" ? project[\"content-root\"] : \"docs\";\n const trailingSlash = consumer[\"trailing-slash\"] === \"never\" ? \"never\" : \"always\";\n return {\n dir,\n configPath,\n contentRoot: path.resolve(dir, contentDir),\n outputRoot: path.resolve(dir, outputDir),\n consumer: {\n site: typeof consumer[\"site\"] === \"string\" ? consumer[\"site\"] : undefined,\n basePath: normalizeBase(\n typeof consumer[\"base-path\"] === \"string\" ? consumer[\"base-path\"] : \"/\",\n ),\n trailingSlash,\n agents: consumer[\"agents\"] === true,\n },\n builds: Object.keys(section(table, \"builds\")),\n siteRoot: (build) => path.resolve(dir, outputDir, build, \"site\"),\n plainRoot: (build) => path.resolve(dir, outputDir, build, \"plain\"),\n };\n}\n\nfunction section(table: Record<string, unknown>, name: string): Record<string, unknown> {\n const value = table[name];\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\n/** A base path with a leading and a trailing `/`, as Ascribe's router has it. */\nexport function normalizeBase(base: string): string {\n let result = base.trim();\n if (!result.startsWith(\"/\")) result = `/${result}`;\n if (!result.endsWith(\"/\")) result += \"/\";\n return result;\n}\n\n/**\n * The route of a base path, ending the way `trailingSlash` says the site's\n * routes do: the site's root for Astro's `base`, a project's root for its\n * `[consumer] base-path`. (`import.meta.env.BASE_URL` isn't replaced in a\n * package's components.)\n */\nexport function homeRoute(base: string, trailingSlash: \"always\" | \"never\" | \"ignore\"): string {\n const root = normalizeBase(base);\n return trailingSlash === \"never\" && root !== \"/\" ? root.slice(0, -1) : root;\n}\n\n/** Astro's routing settings, as the integration reads and supplies them. */\nexport interface AstroRouting {\n base: string;\n trailingSlash: \"always\" | \"never\" | \"ignore\";\n site: string | undefined;\n}\n\n/**\n * Astro's routing with what astro.config leaves unset taken from `[consumer]`,\n * and the settings taken, for `updateConfig`. Astro gives an integration its\n * configuration with its defaults filled in, so unset is the default: no\n * `site`, `trailingSlash: \"ignore\"`, and `base: \"/\"`. `site` and\n * `trailingSlash` come from the first project that has them, since every\n * project must agree with them anyway. `base` comes from the first project's\n * `base-path` only with `supplyBase`, as with one project; with several, it\n * stays the site's, the root every base path is under. A base Astro has is\n * rewritten to end the way a supplied `trailingSlash` says, as Astro writes\n * it when the setting is its own.\n */\nexport function supplyRouting(\n projects: readonly ProjectInfo[],\n astro: AstroRouting,\n supplyBase: boolean,\n): { routing: AstroRouting; supplied: Partial<AstroRouting> } {\n const supplied: Partial<AstroRouting> = {};\n if (astro.site === undefined) {\n const site = projects.find((project) => project.consumer.site !== undefined)?.consumer.site;\n if (site !== undefined) supplied.site = site;\n }\n const first = projects[0];\n if (first === undefined) return { routing: astro, supplied };\n if (astro.trailingSlash === \"ignore\") supplied.trailingSlash = first.consumer.trailingSlash;\n const trailingSlash = supplied.trailingSlash ?? astro.trailingSlash;\n const base = homeRoute(\n supplyBase && normalizeBase(astro.base) === \"/\" ? first.consumer.basePath : astro.base,\n trailingSlash,\n );\n if (base !== astro.base) supplied.base = base;\n return { routing: { ...astro, ...supplied }, supplied };\n}\n\n/**\n * The ways `ascribe.toml`'s `[consumer]` disagrees with Astro's configuration,\n * as sentences. Ascribe writes every link with its own settings, so a site\n * whose routes differ would have broken links.\n */\nexport function consumerMismatches(\n project: ProjectInfo,\n astro: { base: string; trailingSlash: string; site: string | undefined },\n): string[] {\n const problems: string[] = [];\n const { basePath, trailingSlash, site } = project.consumer;\n // A base path under Astro's `base` leaves the rest of the site to other pages, such as a home page.\n if (!basePath.startsWith(normalizeBase(astro.base))) {\n problems.push(\n `[consumer] base-path is \"${basePath}\", but Astro's \\`base\\` is \"${astro.base}\". Set base-path to that path or a path under it.`,\n );\n }\n // Astro's \"ignore\" serves both forms, so Ascribe's links work either way.\n if (astro.trailingSlash !== \"ignore\" && astro.trailingSlash !== trailingSlash) {\n problems.push(\n `[consumer] trailing-slash is \"${trailingSlash}\", but Astro's \\`trailingSlash\\` is \"${astro.trailingSlash}\".`,\n );\n }\n if (site !== undefined && astro.site !== undefined && origin(site) !== origin(astro.site)) {\n problems.push(`[consumer] site is \"${site}\", but Astro's \\`site\\` is \"${astro.site}\".`);\n }\n return problems;\n}\n\nfunction origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return url;\n }\n}\n"]}
|
package/dist/projects.d.ts
CHANGED
|
@@ -19,12 +19,6 @@ export interface ResolvedEntry {
|
|
|
19
19
|
}
|
|
20
20
|
/** Two projects publishing at one base path, as sentences. */
|
|
21
21
|
export declare function basePathClashes(entries: readonly ResolvedEntry[]): string[];
|
|
22
|
-
/**
|
|
23
|
-
* A page's entry id, as Astro's `glob` loader computes it from the page's
|
|
24
|
-
* path in the site output: each segment slugged, without the extension, and
|
|
25
|
-
* without a final `/index`.
|
|
26
|
-
*/
|
|
27
|
-
export declare function entryId(pagePath: string): string;
|
|
28
22
|
/** The pages of a build's site output, by their paths in it, from its manifest. */
|
|
29
23
|
export declare function sitePages(project: ProjectInfo, build: string): string[];
|
|
30
24
|
/**
|
package/dist/projects.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// one project to these rules, since they share its `ascribe.toml`.
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import {
|
|
8
|
+
import { entryId } from "./nav.js";
|
|
9
9
|
import { hasPermalinks } from "./permalinks.js";
|
|
10
10
|
import { normalizeBase } from "./project.js";
|
|
11
11
|
/** What a name may be: letters, digits, `-`, and `_`, starting with a letter. */
|
|
@@ -48,19 +48,6 @@ export function basePathClashes(entries) {
|
|
|
48
48
|
}
|
|
49
49
|
return problems;
|
|
50
50
|
}
|
|
51
|
-
/**
|
|
52
|
-
* A page's entry id, as Astro's `glob` loader computes it from the page's
|
|
53
|
-
* path in the site output: each segment slugged, without the extension, and
|
|
54
|
-
* without a final `/index`.
|
|
55
|
-
*/
|
|
56
|
-
export function entryId(pagePath) {
|
|
57
|
-
return pagePath
|
|
58
|
-
.replace(/\.md$/, "")
|
|
59
|
-
.split("/")
|
|
60
|
-
.map((segment) => slug(segment))
|
|
61
|
-
.join("/")
|
|
62
|
-
.replace(/\/index$/, "");
|
|
63
|
-
}
|
|
64
51
|
/** The pages of a build's site output, by their paths in it, from its manifest. */
|
|
65
52
|
export function sitePages(project, build) {
|
|
66
53
|
const manifest = path.join(path.dirname(project.siteRoot(build)), "site.manifest.json");
|
package/dist/projects.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../src/projects.ts"],"names":[],"mappings":"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","sourcesContent":["// Several Ascribe projects in one Astro site: the entries of `projects`, and\n// the rules for what they share. Each entry has a name, which is what a\n// collection, `review`, a schema's folder, and an error message say. Two\n// entries may share a project and differ by build; entries of one project are\n// one project to these rules, since they share its `ascribe.toml`.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { slug } from \"github-slugger\";\nimport { hasPermalinks, type PermalinkList } from \"./permalinks.js\";\nimport { normalizeBase, type ProjectInfo } from \"./project.js\";\n\n/** One entry of the integration's `projects` option. */\nexport interface ProjectEntry {\n /** The entry's name: its collection's, `review`'s, and its schema folder's. */\n name: string;\n /** The build whose site output is the collection (a name in `ascribe.toml`). */\n build: string;\n /** The directory holding `ascribe.toml`, relative to the Astro root. Default: the root. */\n project?: string;\n}\n\n/** What a name may be: letters, digits, `-`, and `_`, starting with a letter. */\nconst NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;\n\n/** The ways the entries' names are wrong, as sentences. */\nexport function nameProblems(entries: readonly ProjectEntry[]): string[] {\n const problems: string[] = [];\n if (entries.length === 0)\n problems.push(\"`projects` is empty. Give it an entry for each project.\");\n const seen = new Set<string>();\n for (const entry of entries) {\n if (typeof entry.name !== \"string\" || !NAME.test(entry.name)) {\n problems.push(\n `an entry's name is ${JSON.stringify(entry.name)}. A name has letters, digits, \"-\", and \"_\", and starts with a letter.`,\n );\n } else if (seen.has(entry.name)) {\n problems.push(`two entries are named \"${entry.name}\". Each entry's name must be its own.`);\n }\n seen.add(entry.name);\n }\n return problems;\n}\n\n/** An entry with its project read. */\nexport interface ResolvedEntry {\n name: string;\n build: string;\n project: ProjectInfo;\n}\n\n/** The entries of distinct projects, each project once with its first entry's name. */\nfunction distinctProjects(entries: readonly ResolvedEntry[]): ResolvedEntry[] {\n const byDir = new Map<string, ResolvedEntry>();\n for (const entry of entries)\n if (!byDir.has(entry.project.dir)) byDir.set(entry.project.dir, entry);\n return [...byDir.values()];\n}\n\n/** Two projects publishing at one base path, as sentences. */\nexport function basePathClashes(entries: readonly ResolvedEntry[]): string[] {\n const problems: string[] = [];\n const projects = distinctProjects(entries);\n for (const [i, a] of projects.entries()) {\n for (const b of projects.slice(i + 1)) {\n if (a.project.consumer.basePath === b.project.consumer.basePath) {\n problems.push(\n `\"${a.name}\" and \"${b.name}\" both have [consumer] base-path \"${a.project.consumer.basePath}\". Give each project its own.`,\n );\n }\n }\n }\n return problems;\n}\n\n/**\n * A page's entry id, as Astro's `glob` loader computes it from the page's\n * path in the site output: each segment slugged, without the extension, and\n * without a final `/index`.\n */\nexport function entryId(pagePath: string): string {\n return pagePath\n .replace(/\\.md$/, \"\")\n .split(\"/\")\n .map((segment) => slug(segment))\n .join(\"/\")\n .replace(/\\/index$/, \"\");\n}\n\n/** The pages of a build's site output, by their paths in it, from its manifest. */\nexport function sitePages(project: ProjectInfo, build: string): string[] {\n const manifest = path.join(path.dirname(project.siteRoot(build)), \"site.manifest.json\");\n let files: unknown;\n try {\n files = (JSON.parse(readFileSync(manifest, \"utf8\")) as { files?: unknown }).files;\n } catch {\n return [];\n }\n if (!Array.isArray(files)) return [];\n return (files as { kind?: unknown; path?: unknown }[])\n .filter((file) => file.kind === \"page\" && typeof file.path === \"string\")\n .map((file) => file.path as string);\n}\n\n/**\n * A page of one project whose route falls under another project's base path,\n * as sentences: one project at `/` with another at `/docs/` is allowed, and a\n * page of the first at `/docs/…` is not. `pages` gives an entry's pages.\n */\nexport function pagesUnderOtherPaths(\n entries: readonly ResolvedEntry[],\n pages: (entry: ResolvedEntry) => readonly string[] = (entry) =>\n sitePages(entry.project, entry.build),\n): string[] {\n const problems: string[] = [];\n const projects = distinctProjects(entries);\n for (const a of entries) {\n const base = a.project.consumer.basePath;\n const inner = projects.filter(\n (b) =>\n b.project.dir !== a.project.dir &&\n b.project.consumer.basePath !== base &&\n b.project.consumer.basePath.startsWith(base),\n );\n if (inner.length === 0) continue;\n for (const page of pages(a)) {\n const id = entryId(page);\n const route = id === \"index\" ? base : `${base}${id}/`;\n const under = inner.find((b) => route.startsWith(b.project.consumer.basePath));\n if (under !== undefined) {\n problems.push(\n `the page ${page} of \"${a.name}\" is at ${route}, under the base path of \"${under.name}\", ${under.project.consumer.basePath}. Move the page, or give \"${under.name}\" another base path.`,\n );\n }\n }\n }\n return problems;\n}\n\n/**\n * The problems with serving every entry's permalinks from one route, as\n * sentences: the lists that name anything must be published at one path (an\n * empty one, which every build writes, can be anywhere), and two projects may\n * not declare one name, since neither build can see the other. Entries of one\n * project may: the first entry's wins. `lists` holds each entry's list, or\n * `undefined` where its build wrote none.\n */\nexport function permalinkProblems(\n entries: readonly ResolvedEntry[],\n lists: readonly (PermalinkList | undefined)[],\n): string[] {\n const problems: string[] = [];\n let first: { entry: ResolvedEntry; path: string } | undefined;\n const owners = new Map<string, ResolvedEntry>();\n for (const [i, entry] of entries.entries()) {\n const list = lists[i];\n if (list === undefined || !hasPermalinks(list)) continue;\n const at = normalizeBase(list.path);\n if (first === undefined) first = { entry, path: at };\n else if (at !== first.path) {\n problems.push(\n `\"${first.entry.name}\" publishes its permalinks at ${first.path}, and \"${entry.name}\" at ${at}. One route serves them all: set [consumer] permalink-path to one path in both.`,\n );\n continue;\n }\n for (const name of [...Object.keys(list.permalinks), ...Object.keys(list.removed ?? {})]) {\n const owner = owners.get(name);\n if (owner === undefined) owners.set(name, entry);\n else if (owner.project.dir !== entry.project.dir) {\n problems.push(\n `\"${owner.name}\" and \"${entry.name}\" both declare the permalink \"${name}\". A name leads to one place: rename or retire it in one of them.`,\n );\n }\n }\n }\n return problems;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../src/projects.ts"],"names":[],"mappings":"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","sourcesContent":["// Several Ascribe projects in one Astro site: the entries of `projects`, and\n// the rules for what they share. Each entry has a name, which is what a\n// collection, `review`, a schema's folder, and an error message say. Two\n// entries may share a project and differ by build; entries of one project are\n// one project to these rules, since they share its `ascribe.toml`.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { entryId } from \"./nav.js\";\nimport { hasPermalinks, type PermalinkList } from \"./permalinks.js\";\nimport { normalizeBase, type ProjectInfo } from \"./project.js\";\n\n/** One entry of the integration's `projects` option. */\nexport interface ProjectEntry {\n /** The entry's name: its collection's, `review`'s, and its schema folder's. */\n name: string;\n /** The build whose site output is the collection (a name in `ascribe.toml`). */\n build: string;\n /** The directory holding `ascribe.toml`, relative to the Astro root. Default: the root. */\n project?: string;\n}\n\n/** What a name may be: letters, digits, `-`, and `_`, starting with a letter. */\nconst NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;\n\n/** The ways the entries' names are wrong, as sentences. */\nexport function nameProblems(entries: readonly ProjectEntry[]): string[] {\n const problems: string[] = [];\n if (entries.length === 0)\n problems.push(\"`projects` is empty. Give it an entry for each project.\");\n const seen = new Set<string>();\n for (const entry of entries) {\n if (typeof entry.name !== \"string\" || !NAME.test(entry.name)) {\n problems.push(\n `an entry's name is ${JSON.stringify(entry.name)}. A name has letters, digits, \"-\", and \"_\", and starts with a letter.`,\n );\n } else if (seen.has(entry.name)) {\n problems.push(`two entries are named \"${entry.name}\". Each entry's name must be its own.`);\n }\n seen.add(entry.name);\n }\n return problems;\n}\n\n/** An entry with its project read. */\nexport interface ResolvedEntry {\n name: string;\n build: string;\n project: ProjectInfo;\n}\n\n/** The entries of distinct projects, each project once with its first entry's name. */\nfunction distinctProjects(entries: readonly ResolvedEntry[]): ResolvedEntry[] {\n const byDir = new Map<string, ResolvedEntry>();\n for (const entry of entries)\n if (!byDir.has(entry.project.dir)) byDir.set(entry.project.dir, entry);\n return [...byDir.values()];\n}\n\n/** Two projects publishing at one base path, as sentences. */\nexport function basePathClashes(entries: readonly ResolvedEntry[]): string[] {\n const problems: string[] = [];\n const projects = distinctProjects(entries);\n for (const [i, a] of projects.entries()) {\n for (const b of projects.slice(i + 1)) {\n if (a.project.consumer.basePath === b.project.consumer.basePath) {\n problems.push(\n `\"${a.name}\" and \"${b.name}\" both have [consumer] base-path \"${a.project.consumer.basePath}\". Give each project its own.`,\n );\n }\n }\n }\n return problems;\n}\n\n/** The pages of a build's site output, by their paths in it, from its manifest. */\nexport function sitePages(project: ProjectInfo, build: string): string[] {\n const manifest = path.join(path.dirname(project.siteRoot(build)), \"site.manifest.json\");\n let files: unknown;\n try {\n files = (JSON.parse(readFileSync(manifest, \"utf8\")) as { files?: unknown }).files;\n } catch {\n return [];\n }\n if (!Array.isArray(files)) return [];\n return (files as { kind?: unknown; path?: unknown }[])\n .filter((file) => file.kind === \"page\" && typeof file.path === \"string\")\n .map((file) => file.path as string);\n}\n\n/**\n * A page of one project whose route falls under another project's base path,\n * as sentences: one project at `/` with another at `/docs/` is allowed, and a\n * page of the first at `/docs/…` is not. `pages` gives an entry's pages.\n */\nexport function pagesUnderOtherPaths(\n entries: readonly ResolvedEntry[],\n pages: (entry: ResolvedEntry) => readonly string[] = (entry) =>\n sitePages(entry.project, entry.build),\n): string[] {\n const problems: string[] = [];\n const projects = distinctProjects(entries);\n for (const a of entries) {\n const base = a.project.consumer.basePath;\n const inner = projects.filter(\n (b) =>\n b.project.dir !== a.project.dir &&\n b.project.consumer.basePath !== base &&\n b.project.consumer.basePath.startsWith(base),\n );\n if (inner.length === 0) continue;\n for (const page of pages(a)) {\n const id = entryId(page);\n const route = id === \"index\" ? base : `${base}${id}/`;\n const under = inner.find((b) => route.startsWith(b.project.consumer.basePath));\n if (under !== undefined) {\n problems.push(\n `the page ${page} of \"${a.name}\" is at ${route}, under the base path of \"${under.name}\", ${under.project.consumer.basePath}. Move the page, or give \"${under.name}\" another base path.`,\n );\n }\n }\n }\n return problems;\n}\n\n/**\n * The problems with serving every entry's permalinks from one route, as\n * sentences: the lists that name anything must be published at one path (an\n * empty one, which every build writes, can be anywhere), and two projects may\n * not declare one name, since neither build can see the other. Entries of one\n * project may: the first entry's wins. `lists` holds each entry's list, or\n * `undefined` where its build wrote none.\n */\nexport function permalinkProblems(\n entries: readonly ResolvedEntry[],\n lists: readonly (PermalinkList | undefined)[],\n): string[] {\n const problems: string[] = [];\n let first: { entry: ResolvedEntry; path: string } | undefined;\n const owners = new Map<string, ResolvedEntry>();\n for (const [i, entry] of entries.entries()) {\n const list = lists[i];\n if (list === undefined || !hasPermalinks(list)) continue;\n const at = normalizeBase(list.path);\n if (first === undefined) first = { entry, path: at };\n else if (at !== first.path) {\n problems.push(\n `\"${first.entry.name}\" publishes its permalinks at ${first.path}, and \"${entry.name}\" at ${at}. One route serves them all: set [consumer] permalink-path to one path in both.`,\n );\n continue;\n }\n for (const name of [...Object.keys(list.permalinks), ...Object.keys(list.removed ?? {})]) {\n const owner = owners.get(name);\n if (owner === undefined) owners.set(name, entry);\n else if (owner.project.dir !== entry.project.dir) {\n problems.push(\n `\"${owner.name}\" and \"${entry.name}\" both declare the permalink \"${name}\". A name leads to one place: rename or retire it in one of them.`,\n );\n }\n }\n }\n return problems;\n}\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** How a page is treated: listed, listed and not to be quoted, or not listed. */
|
|
2
|
+
export type Listing = "listed" | "unquoted" | "unlisted";
|
|
3
|
+
/** One page's record, with the route it's keyed by. */
|
|
4
|
+
export interface PageRecord {
|
|
5
|
+
/** The page's route, from the host's root. */
|
|
6
|
+
route: string;
|
|
7
|
+
/** The page's path from the content root, with `/`. */
|
|
8
|
+
source: string;
|
|
9
|
+
/** The page's title, as plain text. */
|
|
10
|
+
title?: string;
|
|
11
|
+
/** The page's description, as plain text on one line. */
|
|
12
|
+
description?: string;
|
|
13
|
+
/** The page's own address, absolute, when the project sets `[consumer] site`. */
|
|
14
|
+
canonical?: string;
|
|
15
|
+
listing: Listing;
|
|
16
|
+
/** The address of the page's Markdown version, from the host's root, when the build publishes one. */
|
|
17
|
+
markdown?: string;
|
|
18
|
+
}
|
|
19
|
+
/** `_ascribe/pages.json`. */
|
|
20
|
+
export interface PageRecords {
|
|
21
|
+
version: number;
|
|
22
|
+
site?: string;
|
|
23
|
+
pages: Record<string, Omit<PageRecord, "route">>;
|
|
24
|
+
robots: {
|
|
25
|
+
lines: string[];
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** The records in a build's site output, or `undefined` when there aren't any. */
|
|
29
|
+
export declare function readRecords(siteRoot: string): PageRecords | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* The record of the page whose source path is `source`, in the build whose
|
|
32
|
+
* site output is at `siteRoot`, or `undefined` when it has none. Read again
|
|
33
|
+
* when the file changes, as it does with each build in `astro dev`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function recordFor(siteRoot: string, source: string): PageRecord | undefined;
|
|
36
|
+
/** The records' pages, each with its route, in the file's order. */
|
|
37
|
+
export declare function pageList(records: PageRecords | undefined): PageRecord[];
|
|
38
|
+
/**
|
|
39
|
+
* A route as the sitemap compares it: with a leading `/` and no trailing one,
|
|
40
|
+
* except at the root, so `/docs/` and `/docs` are one page.
|
|
41
|
+
*/
|
|
42
|
+
export declare function routeKey(route: string): string;
|
|
43
|
+
/** A route ending the way Astro's `trailingSlash` says, as a built page's address. */
|
|
44
|
+
export declare function routeAs(route: string, trailingSlash: "always" | "never" | "ignore"): string;
|
|
45
|
+
/** What the sitemap is drawn from. */
|
|
46
|
+
export interface SitemapSources {
|
|
47
|
+
/** Every project's records. */
|
|
48
|
+
projects: readonly PageRecord[][];
|
|
49
|
+
/**
|
|
50
|
+
* Whether a project's page has a route in the site: was built, or, in
|
|
51
|
+
* `astro dev`, matches one of the site's routes.
|
|
52
|
+
*/
|
|
53
|
+
routed: (route: string) => boolean;
|
|
54
|
+
/** The site's own pages, each by its route from the host's root. */
|
|
55
|
+
sitePages: readonly string[];
|
|
56
|
+
/** Routes from the host's root to leave out: forwarding pages, the not-found page, and the site's list. */
|
|
57
|
+
excluded: readonly string[];
|
|
58
|
+
/** Astro's `site`. */
|
|
59
|
+
site: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The sitemap's addresses, absolute, sorted, each once: every listed page of
|
|
63
|
+
* every project that has a route in the site, and every page of the site's
|
|
64
|
+
* own, less the excluded routes. A project's page that isn't listed is left
|
|
65
|
+
* out, and so is a page of the site's own at the same route as one.
|
|
66
|
+
*/
|
|
67
|
+
export declare function sitemapAddresses(sources: SitemapSources): string[];
|
|
68
|
+
/** A sitemap of `addresses`, with no dates. */
|
|
69
|
+
export declare function sitemapXml(addresses: readonly string[]): string;
|
|
70
|
+
/**
|
|
71
|
+
* A site's `robots.txt` from each project's lines: a `User-agent: *` group
|
|
72
|
+
* with every project's lines before its first `User-agent` line, which are
|
|
73
|
+
* for every crawler, then the groups each project wrote whole, then the
|
|
74
|
+
* sitemap's address. `undefined` when it would say nothing.
|
|
75
|
+
*/
|
|
76
|
+
export declare function robotsText(projects: readonly (readonly string[])[], sitemap: string | undefined): string | undefined;
|
|
77
|
+
/** Where the sitemap is, from the host's root: at Astro's `base`. */
|
|
78
|
+
export declare function sitemapRoute(base: string): string;
|
package/dist/records.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// The page records: `_ascribe/pages.json` in a build's site output
|
|
2
|
+
// (output-layout contract §1.3), what each page tells every reader that isn't
|
|
3
|
+
// a person. The integration writes from them a page's head (`Head.astro`),
|
|
4
|
+
// the site's one sitemap, and its one `robots.txt`, from every project's
|
|
5
|
+
// records together, at the host's root.
|
|
6
|
+
import { readFileSync, statSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { normalizeBase } from "./project.js";
|
|
9
|
+
/** The records' format version this reads. */
|
|
10
|
+
const VERSION = 1;
|
|
11
|
+
function recordsFile(siteRoot) {
|
|
12
|
+
return path.join(siteRoot, "_ascribe", "pages.json");
|
|
13
|
+
}
|
|
14
|
+
/** The records in a build's site output, or `undefined` when there aren't any. */
|
|
15
|
+
export function readRecords(siteRoot) {
|
|
16
|
+
const file = recordsFile(siteRoot);
|
|
17
|
+
let text;
|
|
18
|
+
try {
|
|
19
|
+
text = readFileSync(file, "utf8");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const records = JSON.parse(text);
|
|
25
|
+
if (records.version !== VERSION) {
|
|
26
|
+
throw new Error(`@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.`);
|
|
27
|
+
}
|
|
28
|
+
return records;
|
|
29
|
+
}
|
|
30
|
+
/** A build's records by source path, as last read, and when its file was written. */
|
|
31
|
+
const bySource = new Map();
|
|
32
|
+
/**
|
|
33
|
+
* The record of the page whose source path is `source`, in the build whose
|
|
34
|
+
* site output is at `siteRoot`, or `undefined` when it has none. Read again
|
|
35
|
+
* when the file changes, as it does with each build in `astro dev`.
|
|
36
|
+
*/
|
|
37
|
+
export function recordFor(siteRoot, source) {
|
|
38
|
+
let written;
|
|
39
|
+
try {
|
|
40
|
+
written = statSync(recordsFile(siteRoot)).mtimeMs;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
let known = bySource.get(siteRoot);
|
|
46
|
+
if (known === undefined || known.written !== written) {
|
|
47
|
+
const pages = new Map();
|
|
48
|
+
for (const record of pageList(readRecords(siteRoot))) {
|
|
49
|
+
if (!pages.has(record.source))
|
|
50
|
+
pages.set(record.source, record);
|
|
51
|
+
}
|
|
52
|
+
known = { written, pages };
|
|
53
|
+
bySource.set(siteRoot, known);
|
|
54
|
+
}
|
|
55
|
+
return known.pages.get(source);
|
|
56
|
+
}
|
|
57
|
+
/** The records' pages, each with its route, in the file's order. */
|
|
58
|
+
export function pageList(records) {
|
|
59
|
+
return Object.entries(records?.pages ?? {}).map(([route, page]) => ({ route, ...page }));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A route as the sitemap compares it: with a leading `/` and no trailing one,
|
|
63
|
+
* except at the root, so `/docs/` and `/docs` are one page.
|
|
64
|
+
*/
|
|
65
|
+
export function routeKey(route) {
|
|
66
|
+
const withSlash = route.startsWith("/") ? route : `/${route}`;
|
|
67
|
+
return withSlash.length > 1 && withSlash.endsWith("/") ? withSlash.slice(0, -1) : withSlash;
|
|
68
|
+
}
|
|
69
|
+
/** A route ending the way Astro's `trailingSlash` says, as a built page's address. */
|
|
70
|
+
export function routeAs(route, trailingSlash) {
|
|
71
|
+
if (trailingSlash === "never")
|
|
72
|
+
return routeKey(route);
|
|
73
|
+
if (trailingSlash === "always" && !route.endsWith("/"))
|
|
74
|
+
return `${route}/`;
|
|
75
|
+
return route;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The sitemap's addresses, absolute, sorted, each once: every listed page of
|
|
79
|
+
* every project that has a route in the site, and every page of the site's
|
|
80
|
+
* own, less the excluded routes. A project's page that isn't listed is left
|
|
81
|
+
* out, and so is a page of the site's own at the same route as one.
|
|
82
|
+
*/
|
|
83
|
+
export function sitemapAddresses(sources) {
|
|
84
|
+
const excluded = new Set(sources.excluded.map(routeKey));
|
|
85
|
+
const records = new Set();
|
|
86
|
+
const routes = new Map();
|
|
87
|
+
for (const pages of sources.projects) {
|
|
88
|
+
for (const page of pages) {
|
|
89
|
+
const key = routeKey(page.route);
|
|
90
|
+
records.add(key);
|
|
91
|
+
if (page.listing === "unlisted" || excluded.has(key) || !sources.routed(page.route))
|
|
92
|
+
continue;
|
|
93
|
+
if (!routes.has(key))
|
|
94
|
+
routes.set(key, page.route);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const route of sources.sitePages) {
|
|
98
|
+
const key = routeKey(route);
|
|
99
|
+
if (records.has(key) || excluded.has(key) || routes.has(key))
|
|
100
|
+
continue;
|
|
101
|
+
routes.set(key, route);
|
|
102
|
+
}
|
|
103
|
+
return [
|
|
104
|
+
...new Set([...routes.values()].map((route) => new URL(route, sources.site).href)),
|
|
105
|
+
].sort();
|
|
106
|
+
}
|
|
107
|
+
function escapeXml(text) {
|
|
108
|
+
return text
|
|
109
|
+
.replaceAll("&", "&")
|
|
110
|
+
.replaceAll("<", "<")
|
|
111
|
+
.replaceAll(">", ">")
|
|
112
|
+
.replaceAll('"', """)
|
|
113
|
+
.replaceAll("'", "'");
|
|
114
|
+
}
|
|
115
|
+
/** A sitemap of `addresses`, with no dates. */
|
|
116
|
+
export function sitemapXml(addresses) {
|
|
117
|
+
const urls = addresses.map((address) => ` <url><loc>${escapeXml(address)}</loc></url>\n`);
|
|
118
|
+
return ('<?xml version="1.0" encoding="UTF-8"?>\n' +
|
|
119
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' +
|
|
120
|
+
urls.join("") +
|
|
121
|
+
"</urlset>\n");
|
|
122
|
+
}
|
|
123
|
+
const USER_AGENT = /^\s*user-agent\s*:/i;
|
|
124
|
+
/**
|
|
125
|
+
* A site's `robots.txt` from each project's lines: a `User-agent: *` group
|
|
126
|
+
* with every project's lines before its first `User-agent` line, which are
|
|
127
|
+
* for every crawler, then the groups each project wrote whole, then the
|
|
128
|
+
* sitemap's address. `undefined` when it would say nothing.
|
|
129
|
+
*/
|
|
130
|
+
export function robotsText(projects, sitemap) {
|
|
131
|
+
const everyone = [];
|
|
132
|
+
const groups = [];
|
|
133
|
+
for (const lines of projects) {
|
|
134
|
+
const first = lines.findIndex((line) => USER_AGENT.test(line));
|
|
135
|
+
everyone.push(...(first === -1 ? lines : lines.slice(0, first)));
|
|
136
|
+
if (first !== -1)
|
|
137
|
+
groups.push(lines.slice(first));
|
|
138
|
+
}
|
|
139
|
+
const blocks = [];
|
|
140
|
+
if (everyone.length > 0)
|
|
141
|
+
blocks.push(["User-agent: *", ...everyone]);
|
|
142
|
+
blocks.push(...groups);
|
|
143
|
+
if (sitemap !== undefined)
|
|
144
|
+
blocks.push([`Sitemap: ${sitemap}`]);
|
|
145
|
+
if (blocks.length === 0)
|
|
146
|
+
return undefined;
|
|
147
|
+
return `${blocks.map((block) => block.join("\n")).join("\n\n")}\n`;
|
|
148
|
+
}
|
|
149
|
+
/** Where the sitemap is, from the host's root: at Astro's `base`. */
|
|
150
|
+
export function sitemapRoute(base) {
|
|
151
|
+
return `${normalizeBase(base)}sitemap.xml`;
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=records.js.map
|
|
@@ -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/** 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 /** The address of the page's Markdown version, from the host's root, when the build publishes one. */\n markdown?: string;\n}\n\n/** `_ascribe/pages.json`. */\nexport interface PageRecords {\n version: number;\n site?: string;\n pages: Record<string, Omit<PageRecord, \"route\">>;\n robots: { lines: string[] };\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(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\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 each project's lines: a `User-agent: *` group\n * with every project's lines before its first `User-agent` line, which are\n * for every crawler, then the groups each project wrote whole, then the\n * sitemap's address. `undefined` when it would say nothing.\n */\nexport function robotsText(\n projects: readonly (readonly string[])[],\n sitemap: string | undefined,\n): string | undefined {\n const everyone: string[] = [];\n const groups: string[][] = [];\n for (const lines of projects) {\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) as string[]);\n }\n const blocks: string[][] = [];\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/** 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,63 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { type SitemapSources } from "./records.js";
|
|
3
|
+
/** What the files are drawn from. */
|
|
4
|
+
export interface RootFileSources extends Omit<SitemapSources, "site"> {
|
|
5
|
+
/** Each project's `robots.txt` lines, from the host's root, each project once. */
|
|
6
|
+
lines: readonly (readonly string[])[];
|
|
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
|
+
/** The files, each `undefined` when none is written, and what to say about them. */
|
|
22
|
+
export interface RootFiles {
|
|
23
|
+
sitemap: string | undefined;
|
|
24
|
+
robots: string | undefined;
|
|
25
|
+
notes: RootFileNote[];
|
|
26
|
+
}
|
|
27
|
+
/** The sitemap and `robots.txt` for a site, and what the build says about them. */
|
|
28
|
+
export declare function rootFiles(sources: RootFileSources): RootFiles;
|
|
29
|
+
type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
|
|
30
|
+
/**
|
|
31
|
+
* A dev-server middleware serving the sitemap at `<base>sitemap.xml` and
|
|
32
|
+
* `robots.txt` at `/`, each as `files` has it when it's asked for, so each
|
|
33
|
+
* rebuild's records are served.
|
|
34
|
+
*/
|
|
35
|
+
export declare function rootFilesMiddleware(base: string, files: () => RootFiles): Middleware;
|
|
36
|
+
/** A route as `astro:routes:resolved` gives it, as much as these read. */
|
|
37
|
+
export interface AstroRoute {
|
|
38
|
+
pattern: string;
|
|
39
|
+
patternRegex: RegExp;
|
|
40
|
+
params: string[];
|
|
41
|
+
type: string;
|
|
42
|
+
origin: string;
|
|
43
|
+
}
|
|
44
|
+
/** Astro's routing, as much as these read. */
|
|
45
|
+
interface Routing {
|
|
46
|
+
base: string;
|
|
47
|
+
trailingSlash: "always" | "never" | "ignore";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The site's own pages `astro dev` can name: each route of the site's own
|
|
51
|
+
* with no parameters, other than the not-found and error pages, by its route
|
|
52
|
+
* from the host's root. A page a route's `getStaticPaths` lists is known only
|
|
53
|
+
* to a build.
|
|
54
|
+
*/
|
|
55
|
+
export declare function devSitePages(routes: readonly AstroRoute[], astro: Routing): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Whether a project's page, by its route from the host's root, has a route
|
|
58
|
+
* in the site in `astro dev`: whether one of the site's own routes matches
|
|
59
|
+
* it. A project no route shows, such as one whose collection a page only
|
|
60
|
+
* quotes from, has no pages in the sitemap.
|
|
61
|
+
*/
|
|
62
|
+
export declare function devRouted(routes: readonly AstroRoute[], astro: Routing): (route: string) => boolean;
|
|
63
|
+
export {};
|