@ascribed/astro 0.2.1-next.9 → 0.3.0
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 +41 -9
- 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 +30 -0
- package/dist/content.js +101 -2
- package/dist/content.js.map +1 -1
- package/dist/dev.d.ts +2 -1
- package/dist/dev.js +13 -3
- package/dist/dev.js.map +1 -1
- package/dist/files.d.ts +21 -4
- package/dist/files.js +46 -8
- package/dist/files.js.map +1 -1
- package/dist/head-check.d.ts +6 -0
- package/dist/head-check.js +28 -0
- package/dist/head-check.js.map +1 -0
- package/dist/head.d.ts +54 -0
- package/dist/head.js +114 -0
- package/dist/head.js.map +1 -0
- package/dist/headers.d.ts +50 -0
- package/dist/headers.js +155 -0
- package/dist/headers.js.map +1 -0
- package/dist/index.d.ts +40 -9
- package/dist/index.js +330 -95
- 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/names.d.ts +5 -0
- package/dist/names.js +5 -0
- package/dist/names.js.map +1 -1
- 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 +78 -0
- package/dist/permalinks.js +114 -0
- package/dist/permalinks.js.map +1 -0
- package/dist/project.d.ts +37 -0
- package/dist/project.js +47 -2
- package/dist/project.js.map +1 -1
- package/dist/projects.d.ts +38 -0
- package/dist/projects.js +127 -0
- package/dist/projects.js.map +1 -0
- package/dist/records.d.ts +136 -0
- package/dist/records.js +182 -0
- package/dist/records.js.map +1 -0
- package/dist/review/devfile.d.ts +6 -1
- package/dist/review/devfile.js +12 -2
- package/dist/review/devfile.js.map +1 -1
- package/dist/review/diff.d.ts +13 -0
- package/dist/review/diff.js +34 -1
- package/dist/review/diff.js.map +1 -1
- package/dist/review/protocol.d.ts +6 -1
- package/dist/review/protocol.js.map +1 -1
- package/dist/review/routes.d.ts +3 -0
- package/dist/review/routes.js +49 -35
- package/dist/review/routes.js.map +1 -1
- package/dist/review/server.d.ts +15 -0
- package/dist/review/server.js +58 -7
- package/dist/review/server.js.map +1 -1
- package/dist/root-files.d.ts +68 -0
- package/dist/root-files.js +103 -0
- package/dist/root-files.js.map +1 -0
- package/dist/run.d.ts +7 -6
- package/dist/run.js +14 -6
- package/dist/run.js.map +1 -1
- package/dist/shapes.d.ts +8 -1
- package/dist/shapes.js.map +1 -1
- package/dist/toolbar/app.js +98 -0
- package/dist/toolbar/app.js.map +1 -1
- package/dist/toolbar/style.d.ts +1 -1
- package/dist/toolbar/style.js +31 -23
- package/dist/toolbar/style.js.map +1 -1
- package/package.json +32 -9
- package/src/Head.astro +58 -0
- package/src/MissingPermalink.astro +59 -0
- package/src/Permalink.astro +78 -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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** `_ascribe/permalinks.json`. */
|
|
2
|
+
export interface PermalinkList {
|
|
3
|
+
version: number;
|
|
4
|
+
/** Where the addresses are published, from the site's root, ending in `/`. */
|
|
5
|
+
path: string;
|
|
6
|
+
/** The content model's dimensions, each with its values: the parameters an address takes. */
|
|
7
|
+
dimensions?: Record<string, string[]>;
|
|
8
|
+
/**
|
|
9
|
+
* Each permalink's destination. An alias names the permalink it leads to,
|
|
10
|
+
* and a retired permalink that leads somewhere its reason.
|
|
11
|
+
*/
|
|
12
|
+
permalinks: Record<string, {
|
|
13
|
+
route: string;
|
|
14
|
+
fragment?: string;
|
|
15
|
+
source: string;
|
|
16
|
+
alias?: string;
|
|
17
|
+
retired?: string;
|
|
18
|
+
}>;
|
|
19
|
+
/** The retired permalinks with nowhere to lead in this build, each with why. */
|
|
20
|
+
removed?: Record<string, {
|
|
21
|
+
reason: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What an address's page needs: where it sends the reader, and the
|
|
26
|
+
* parameters it takes; or, for a retired permalink with nowhere to lead,
|
|
27
|
+
* why it was removed.
|
|
28
|
+
*/
|
|
29
|
+
export type Forwarding = {
|
|
30
|
+
to: string;
|
|
31
|
+
dimensions: Record<string, string[]>;
|
|
32
|
+
} | {
|
|
33
|
+
reason: string;
|
|
34
|
+
home?: string;
|
|
35
|
+
};
|
|
36
|
+
/** A build's site output whose list the route serves, and the route of its project's root. */
|
|
37
|
+
export interface PermalinkSource {
|
|
38
|
+
siteRoot: string;
|
|
39
|
+
home: string;
|
|
40
|
+
}
|
|
41
|
+
/** The list's text in a build's site output, or `undefined` when there isn't one. */
|
|
42
|
+
export declare function permalinksText(siteRoot: string): string | undefined;
|
|
43
|
+
/** The list in a build's site output, or `undefined` when there isn't one. */
|
|
44
|
+
export declare function readPermalinks(siteRoot: string): PermalinkList | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The route pattern for the addresses, as Astro takes it: under `base`, which
|
|
47
|
+
* Astro adds. Throws when the list's path isn't under `base`, since an Astro
|
|
48
|
+
* site serves nothing outside it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function permalinkPattern(listPath: string, base: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* `getStaticPaths` for the route: one page for each permalink in the site
|
|
53
|
+
* outputs' lists, and one for each retired permalink they list as removed,
|
|
54
|
+
* whose page links to its project's root. A name in more than one list gets
|
|
55
|
+
* the first's page; the integration refuses one that two projects declare.
|
|
56
|
+
*/
|
|
57
|
+
export declare function forwardingPaths(sources: string | readonly PermalinkSource[]): {
|
|
58
|
+
params: {
|
|
59
|
+
id: string;
|
|
60
|
+
};
|
|
61
|
+
props: Forwarding;
|
|
62
|
+
}[];
|
|
63
|
+
/** Whether a list names anything: a permalink, or a retired one it lists as removed. */
|
|
64
|
+
export declare function hasPermalinks(list: PermalinkList): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The list a site's not-found page explains addresses from, and the route of
|
|
67
|
+
* its project's root: the first that has a permalink, or else the first list.
|
|
68
|
+
*/
|
|
69
|
+
export declare function servedList(sources: readonly PermalinkSource[]): {
|
|
70
|
+
path: string;
|
|
71
|
+
home: string;
|
|
72
|
+
} | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* The routes of the forwarding pages, from the host's root: one for each name
|
|
75
|
+
* in `lists`, at the path of the list the route serves, as `hasPermalinks`
|
|
76
|
+
* picks it. They aren't pages of a build, and no sitemap lists them.
|
|
77
|
+
*/
|
|
78
|
+
export declare function forwardingRoutes(lists: readonly (PermalinkList | undefined)[]): string[];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Permalinks' addresses: a page at `<path><id>` for each permalink the build
|
|
2
|
+
// publishes, which sends the reader on to its heading, and for each retired
|
|
3
|
+
// one with nowhere to lead, which says why it was removed. Ascribe writes the list,
|
|
4
|
+
// `_ascribe/permalinks.json` in the site output (output-layout contract §1.2);
|
|
5
|
+
// the integration adds the route (`Permalink.astro`) and builds its pages
|
|
6
|
+
// from it, in `astro build` and `astro dev`. With several projects, one route
|
|
7
|
+
// serves every project's list.
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { normalizeBase } from "./project.js";
|
|
11
|
+
/** The list's format version this reads. */
|
|
12
|
+
const VERSION = 1;
|
|
13
|
+
/** The list's text in a build's site output, or `undefined` when there isn't one. */
|
|
14
|
+
export function permalinksText(siteRoot) {
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(listFile(siteRoot), "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function listFile(siteRoot) {
|
|
23
|
+
return path.join(siteRoot, "_ascribe", "permalinks.json");
|
|
24
|
+
}
|
|
25
|
+
/** The list in a build's site output, or `undefined` when there isn't one. */
|
|
26
|
+
export function readPermalinks(siteRoot) {
|
|
27
|
+
const file = listFile(siteRoot);
|
|
28
|
+
const text = permalinksText(siteRoot);
|
|
29
|
+
if (text === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
const list = JSON.parse(text);
|
|
32
|
+
if (list.version !== VERSION) {
|
|
33
|
+
throw new Error(`@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.`);
|
|
34
|
+
}
|
|
35
|
+
return list;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The route pattern for the addresses, as Astro takes it: under `base`, which
|
|
39
|
+
* Astro adds. Throws when the list's path isn't under `base`, since an Astro
|
|
40
|
+
* site serves nothing outside it.
|
|
41
|
+
*/
|
|
42
|
+
export function permalinkPattern(listPath, base) {
|
|
43
|
+
const root = normalizeBase(base);
|
|
44
|
+
const at = normalizeBase(listPath);
|
|
45
|
+
if (!at.startsWith(root)) {
|
|
46
|
+
throw new Error(`@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}".`);
|
|
47
|
+
}
|
|
48
|
+
return `/${at.slice(root.length)}[id]`;
|
|
49
|
+
}
|
|
50
|
+
/** Where a permalink leads: its page's route, and the heading's anchor. */
|
|
51
|
+
function destination(entry) {
|
|
52
|
+
return entry.fragment === undefined ? entry.route : `${entry.route}#${entry.fragment}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `getStaticPaths` for the route: one page for each permalink in the site
|
|
56
|
+
* outputs' lists, and one for each retired permalink they list as removed,
|
|
57
|
+
* whose page links to its project's root. A name in more than one list gets
|
|
58
|
+
* the first's page; the integration refuses one that two projects declare.
|
|
59
|
+
*/
|
|
60
|
+
export function forwardingPaths(sources) {
|
|
61
|
+
const all = typeof sources === "string" ? [{ siteRoot: sources, home: undefined }] : sources;
|
|
62
|
+
const pages = new Map();
|
|
63
|
+
for (const { siteRoot, home } of all) {
|
|
64
|
+
const list = readPermalinks(siteRoot);
|
|
65
|
+
if (list === undefined)
|
|
66
|
+
continue;
|
|
67
|
+
const dimensions = list.dimensions ?? {};
|
|
68
|
+
for (const [id, entry] of Object.entries(list.permalinks)) {
|
|
69
|
+
if (!pages.has(id))
|
|
70
|
+
pages.set(id, { to: destination(entry), dimensions });
|
|
71
|
+
}
|
|
72
|
+
for (const [id, entry] of Object.entries(list.removed ?? {})) {
|
|
73
|
+
if (!pages.has(id))
|
|
74
|
+
pages.set(id, home === undefined ? { reason: entry.reason } : { reason: entry.reason, home });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const forwarding = [...pages].filter(([, props]) => "to" in props);
|
|
78
|
+
const removed = [...pages].filter(([, props]) => !("to" in props));
|
|
79
|
+
return [...forwarding, ...removed].map(([id, props]) => ({ params: { id }, props }));
|
|
80
|
+
}
|
|
81
|
+
/** Whether a list names anything: a permalink, or a retired one it lists as removed. */
|
|
82
|
+
export function hasPermalinks(list) {
|
|
83
|
+
return Object.keys(list.permalinks).length + Object.keys(list.removed ?? {}).length > 0;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The list a site's not-found page explains addresses from, and the route of
|
|
87
|
+
* its project's root: the first that has a permalink, or else the first list.
|
|
88
|
+
*/
|
|
89
|
+
export function servedList(sources) {
|
|
90
|
+
let first;
|
|
91
|
+
for (const { siteRoot, home } of sources) {
|
|
92
|
+
const list = readPermalinks(siteRoot);
|
|
93
|
+
if (list === undefined)
|
|
94
|
+
continue;
|
|
95
|
+
first ??= { path: list.path, home };
|
|
96
|
+
if (hasPermalinks(list))
|
|
97
|
+
return { path: list.path, home };
|
|
98
|
+
}
|
|
99
|
+
return first;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The routes of the forwarding pages, from the host's root: one for each name
|
|
103
|
+
* in `lists`, at the path of the list the route serves, as `hasPermalinks`
|
|
104
|
+
* picks it. They aren't pages of a build, and no sitemap lists them.
|
|
105
|
+
*/
|
|
106
|
+
export function forwardingRoutes(lists) {
|
|
107
|
+
const known = lists.filter((list) => list !== undefined);
|
|
108
|
+
const served = known.find(hasPermalinks) ?? known[0];
|
|
109
|
+
if (served === undefined)
|
|
110
|
+
return [];
|
|
111
|
+
const at = normalizeBase(served.path);
|
|
112
|
+
return known.flatMap((list) => [...Object.keys(list.permalinks), ...Object.keys(list.removed ?? {})].map((id) => `${at}${id}`));
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=permalinks.js.map
|
|
@@ -0,0 +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"]}
|
package/dist/project.d.ts
CHANGED
|
@@ -13,14 +13,51 @@ export interface ProjectInfo {
|
|
|
13
13
|
site: string | undefined;
|
|
14
14
|
basePath: string;
|
|
15
15
|
trailingSlash: "always" | "never";
|
|
16
|
+
/** Whether builds publish what agents read: `llms.txt` and each page's Markdown, in the plain output. */
|
|
17
|
+
agents: boolean;
|
|
18
|
+
/** The host whose format the site's response headers are written in: `"netlify"`, or none. */
|
|
19
|
+
headers: "netlify" | undefined;
|
|
16
20
|
};
|
|
21
|
+
/** The names of `[builds]`'s tables, in order: empty when there are none, and the one build is `site`. */
|
|
22
|
+
builds: string[];
|
|
17
23
|
/** The site output's root for a build: `<output-dir>/<build>/site`. */
|
|
18
24
|
siteRoot(build: string): string;
|
|
25
|
+
/** The plain output's root for a build: `<output-dir>/<build>/plain`. */
|
|
26
|
+
plainRoot(build: string): string;
|
|
19
27
|
}
|
|
20
28
|
/** Reads `ascribe.toml` in `dir`. Throws a readable error if it can't be read. */
|
|
21
29
|
export declare function readProject(dir: string): ProjectInfo;
|
|
22
30
|
/** A base path with a leading and a trailing `/`, as Ascribe's router has it. */
|
|
23
31
|
export declare function normalizeBase(base: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* The route of a base path, ending the way `trailingSlash` says the site's
|
|
34
|
+
* routes do: the site's root for Astro's `base`, a project's root for its
|
|
35
|
+
* `[consumer] base-path`. (`import.meta.env.BASE_URL` isn't replaced in a
|
|
36
|
+
* package's components.)
|
|
37
|
+
*/
|
|
38
|
+
export declare function homeRoute(base: string, trailingSlash: "always" | "never" | "ignore"): string;
|
|
39
|
+
/** Astro's routing settings, as the integration reads and supplies them. */
|
|
40
|
+
export interface AstroRouting {
|
|
41
|
+
base: string;
|
|
42
|
+
trailingSlash: "always" | "never" | "ignore";
|
|
43
|
+
site: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Astro's routing with what astro.config leaves unset taken from `[consumer]`,
|
|
47
|
+
* and the settings taken, for `updateConfig`. Astro gives an integration its
|
|
48
|
+
* configuration with its defaults filled in, so unset is the default: no
|
|
49
|
+
* `site`, `trailingSlash: "ignore"`, and `base: "/"`. `site` and
|
|
50
|
+
* `trailingSlash` come from the first project that has them, since every
|
|
51
|
+
* project must agree with them anyway. `base` comes from the first project's
|
|
52
|
+
* `base-path` only with `supplyBase`, as with one project; with several, it
|
|
53
|
+
* stays the site's, the root every base path is under. A base Astro has is
|
|
54
|
+
* rewritten to end the way a supplied `trailingSlash` says, as Astro writes
|
|
55
|
+
* it when the setting is its own.
|
|
56
|
+
*/
|
|
57
|
+
export declare function supplyRouting(projects: readonly ProjectInfo[], astro: AstroRouting, supplyBase: boolean): {
|
|
58
|
+
routing: AstroRouting;
|
|
59
|
+
supplied: Partial<AstroRouting>;
|
|
60
|
+
};
|
|
24
61
|
/**
|
|
25
62
|
* The ways `ascribe.toml`'s `[consumer]` disagrees with Astro's configuration,
|
|
26
63
|
* as sentences. Ascribe writes every link with its own settings, so a site
|
package/dist/project.js
CHANGED
|
@@ -29,8 +29,12 @@ export function readProject(dir) {
|
|
|
29
29
|
site: typeof consumer["site"] === "string" ? consumer["site"] : undefined,
|
|
30
30
|
basePath: normalizeBase(typeof consumer["base-path"] === "string" ? consumer["base-path"] : "/"),
|
|
31
31
|
trailingSlash,
|
|
32
|
+
agents: consumer["agents"] === true,
|
|
33
|
+
headers: consumer["headers"] === "netlify" ? "netlify" : undefined,
|
|
32
34
|
},
|
|
35
|
+
builds: Object.keys(section(table, "builds")),
|
|
33
36
|
siteRoot: (build) => path.resolve(dir, outputDir, build, "site"),
|
|
37
|
+
plainRoot: (build) => path.resolve(dir, outputDir, build, "plain"),
|
|
34
38
|
};
|
|
35
39
|
}
|
|
36
40
|
function section(table, name) {
|
|
@@ -48,6 +52,46 @@ export function normalizeBase(base) {
|
|
|
48
52
|
result += "/";
|
|
49
53
|
return result;
|
|
50
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The route of a base path, ending the way `trailingSlash` says the site's
|
|
57
|
+
* routes do: the site's root for Astro's `base`, a project's root for its
|
|
58
|
+
* `[consumer] base-path`. (`import.meta.env.BASE_URL` isn't replaced in a
|
|
59
|
+
* package's components.)
|
|
60
|
+
*/
|
|
61
|
+
export function homeRoute(base, trailingSlash) {
|
|
62
|
+
const root = normalizeBase(base);
|
|
63
|
+
return trailingSlash === "never" && root !== "/" ? root.slice(0, -1) : root;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Astro's routing with what astro.config leaves unset taken from `[consumer]`,
|
|
67
|
+
* and the settings taken, for `updateConfig`. Astro gives an integration its
|
|
68
|
+
* configuration with its defaults filled in, so unset is the default: no
|
|
69
|
+
* `site`, `trailingSlash: "ignore"`, and `base: "/"`. `site` and
|
|
70
|
+
* `trailingSlash` come from the first project that has them, since every
|
|
71
|
+
* project must agree with them anyway. `base` comes from the first project's
|
|
72
|
+
* `base-path` only with `supplyBase`, as with one project; with several, it
|
|
73
|
+
* stays the site's, the root every base path is under. A base Astro has is
|
|
74
|
+
* rewritten to end the way a supplied `trailingSlash` says, as Astro writes
|
|
75
|
+
* it when the setting is its own.
|
|
76
|
+
*/
|
|
77
|
+
export function supplyRouting(projects, astro, supplyBase) {
|
|
78
|
+
const supplied = {};
|
|
79
|
+
if (astro.site === undefined) {
|
|
80
|
+
const site = projects.find((project) => project.consumer.site !== undefined)?.consumer.site;
|
|
81
|
+
if (site !== undefined)
|
|
82
|
+
supplied.site = site;
|
|
83
|
+
}
|
|
84
|
+
const first = projects[0];
|
|
85
|
+
if (first === undefined)
|
|
86
|
+
return { routing: astro, supplied };
|
|
87
|
+
if (astro.trailingSlash === "ignore")
|
|
88
|
+
supplied.trailingSlash = first.consumer.trailingSlash;
|
|
89
|
+
const trailingSlash = supplied.trailingSlash ?? astro.trailingSlash;
|
|
90
|
+
const base = homeRoute(supplyBase && normalizeBase(astro.base) === "/" ? first.consumer.basePath : astro.base, trailingSlash);
|
|
91
|
+
if (base !== astro.base)
|
|
92
|
+
supplied.base = base;
|
|
93
|
+
return { routing: { ...astro, ...supplied }, supplied };
|
|
94
|
+
}
|
|
51
95
|
/**
|
|
52
96
|
* The ways `ascribe.toml`'s `[consumer]` disagrees with Astro's configuration,
|
|
53
97
|
* as sentences. Ascribe writes every link with its own settings, so a site
|
|
@@ -56,8 +100,9 @@ export function normalizeBase(base) {
|
|
|
56
100
|
export function consumerMismatches(project, astro) {
|
|
57
101
|
const problems = [];
|
|
58
102
|
const { basePath, trailingSlash, site } = project.consumer;
|
|
59
|
-
|
|
60
|
-
|
|
103
|
+
// A base path under Astro's `base` leaves the rest of the site to other pages, such as a home page.
|
|
104
|
+
if (!basePath.startsWith(normalizeBase(astro.base))) {
|
|
105
|
+
problems.push(`[consumer] base-path is "${basePath}", but Astro's \`base\` is "${astro.base}". Set base-path to that path or a path under it.`);
|
|
61
106
|
}
|
|
62
107
|
// Astro's "ignore" serves both forms, so Ascribe's links work either way.
|
|
63
108
|
if (astro.trailingSlash !== "ignore" && astro.trailingSlash !== trailingSlash) {
|
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: { site: string | undefined; basePath: string; trailingSlash: \"always\" | \"never\" };\n /** The site output's root for a build: `<output-dir>/<build>/site`. */\n siteRoot(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 },\n siteRoot: (build) => path.resolve(dir, outputDir, build, \"site\"),\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 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 if (normalizeBase(astro.base) !== basePath) {\n problems.push(\n `[consumer] base-path is \"${basePath}\", but Astro's \\`base\\` is \"${astro.base}\". Set them to the same path.`,\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 /** The host whose format the site's response headers are written in: `\"netlify\"`, or none. */\n headers: \"netlify\" | undefined;\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 headers: consumer[\"headers\"] === \"netlify\" ? \"netlify\" : undefined,\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"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type PermalinkList } from "./permalinks.js";
|
|
2
|
+
import { type ProjectInfo } from "./project.js";
|
|
3
|
+
/** One entry of the integration's `projects` option. */
|
|
4
|
+
export interface ProjectEntry {
|
|
5
|
+
/** The entry's name: its collection's, `review`'s, and its schema folder's. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** The build whose site output is the collection (a name in `ascribe.toml`). */
|
|
8
|
+
build: string;
|
|
9
|
+
/** The directory holding `ascribe.toml`, relative to the Astro root. Default: the root. */
|
|
10
|
+
project?: string;
|
|
11
|
+
}
|
|
12
|
+
/** The ways the entries' names are wrong, as sentences. */
|
|
13
|
+
export declare function nameProblems(entries: readonly ProjectEntry[]): string[];
|
|
14
|
+
/** An entry with its project read. */
|
|
15
|
+
export interface ResolvedEntry {
|
|
16
|
+
name: string;
|
|
17
|
+
build: string;
|
|
18
|
+
project: ProjectInfo;
|
|
19
|
+
}
|
|
20
|
+
/** Two projects publishing at one base path, as sentences. */
|
|
21
|
+
export declare function basePathClashes(entries: readonly ResolvedEntry[]): string[];
|
|
22
|
+
/** The pages of a build's site output, by their paths in it, from its manifest. */
|
|
23
|
+
export declare function sitePages(project: ProjectInfo, build: string): string[];
|
|
24
|
+
/**
|
|
25
|
+
* A page of one project whose route falls under another project's base path,
|
|
26
|
+
* as sentences: one project at `/` with another at `/docs/` is allowed, and a
|
|
27
|
+
* page of the first at `/docs/…` is not. `pages` gives an entry's pages.
|
|
28
|
+
*/
|
|
29
|
+
export declare function pagesUnderOtherPaths(entries: readonly ResolvedEntry[], pages?: (entry: ResolvedEntry) => readonly string[]): string[];
|
|
30
|
+
/**
|
|
31
|
+
* The problems with serving every entry's permalinks from one route, as
|
|
32
|
+
* sentences: the lists that name anything must be published at one path (an
|
|
33
|
+
* empty one, which every build writes, can be anywhere), and two projects may
|
|
34
|
+
* not declare one name, since neither build can see the other. Entries of one
|
|
35
|
+
* project may: the first entry's wins. `lists` holds each entry's list, or
|
|
36
|
+
* `undefined` where its build wrote none.
|
|
37
|
+
*/
|
|
38
|
+
export declare function permalinkProblems(entries: readonly ResolvedEntry[], lists: readonly (PermalinkList | undefined)[]): string[];
|
package/dist/projects.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Several Ascribe projects in one Astro site: the entries of `projects`, and
|
|
2
|
+
// the rules for what they share. Each entry has a name, which is what a
|
|
3
|
+
// collection, `review`, a schema's folder, and an error message say. Two
|
|
4
|
+
// entries may share a project and differ by build; entries of one project are
|
|
5
|
+
// one project to these rules, since they share its `ascribe.toml`.
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { entryId } from "./nav.js";
|
|
9
|
+
import { hasPermalinks } from "./permalinks.js";
|
|
10
|
+
import { normalizeBase } from "./project.js";
|
|
11
|
+
/** What a name may be: letters, digits, `-`, and `_`, starting with a letter. */
|
|
12
|
+
const NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
13
|
+
/** The ways the entries' names are wrong, as sentences. */
|
|
14
|
+
export function nameProblems(entries) {
|
|
15
|
+
const problems = [];
|
|
16
|
+
if (entries.length === 0)
|
|
17
|
+
problems.push("`projects` is empty. Give it an entry for each project.");
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (typeof entry.name !== "string" || !NAME.test(entry.name)) {
|
|
21
|
+
problems.push(`an entry's name is ${JSON.stringify(entry.name)}. A name has letters, digits, "-", and "_", and starts with a letter.`);
|
|
22
|
+
}
|
|
23
|
+
else if (seen.has(entry.name)) {
|
|
24
|
+
problems.push(`two entries are named "${entry.name}". Each entry's name must be its own.`);
|
|
25
|
+
}
|
|
26
|
+
seen.add(entry.name);
|
|
27
|
+
}
|
|
28
|
+
return problems;
|
|
29
|
+
}
|
|
30
|
+
/** The entries of distinct projects, each project once with its first entry's name. */
|
|
31
|
+
function distinctProjects(entries) {
|
|
32
|
+
const byDir = new Map();
|
|
33
|
+
for (const entry of entries)
|
|
34
|
+
if (!byDir.has(entry.project.dir))
|
|
35
|
+
byDir.set(entry.project.dir, entry);
|
|
36
|
+
return [...byDir.values()];
|
|
37
|
+
}
|
|
38
|
+
/** Two projects publishing at one base path, as sentences. */
|
|
39
|
+
export function basePathClashes(entries) {
|
|
40
|
+
const problems = [];
|
|
41
|
+
const projects = distinctProjects(entries);
|
|
42
|
+
for (const [i, a] of projects.entries()) {
|
|
43
|
+
for (const b of projects.slice(i + 1)) {
|
|
44
|
+
if (a.project.consumer.basePath === b.project.consumer.basePath) {
|
|
45
|
+
problems.push(`"${a.name}" and "${b.name}" both have [consumer] base-path "${a.project.consumer.basePath}". Give each project its own.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return problems;
|
|
50
|
+
}
|
|
51
|
+
/** The pages of a build's site output, by their paths in it, from its manifest. */
|
|
52
|
+
export function sitePages(project, build) {
|
|
53
|
+
const manifest = path.join(path.dirname(project.siteRoot(build)), "site.manifest.json");
|
|
54
|
+
let files;
|
|
55
|
+
try {
|
|
56
|
+
files = JSON.parse(readFileSync(manifest, "utf8")).files;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(files))
|
|
62
|
+
return [];
|
|
63
|
+
return files
|
|
64
|
+
.filter((file) => file.kind === "page" && typeof file.path === "string")
|
|
65
|
+
.map((file) => file.path);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A page of one project whose route falls under another project's base path,
|
|
69
|
+
* as sentences: one project at `/` with another at `/docs/` is allowed, and a
|
|
70
|
+
* page of the first at `/docs/…` is not. `pages` gives an entry's pages.
|
|
71
|
+
*/
|
|
72
|
+
export function pagesUnderOtherPaths(entries, pages = (entry) => sitePages(entry.project, entry.build)) {
|
|
73
|
+
const problems = [];
|
|
74
|
+
const projects = distinctProjects(entries);
|
|
75
|
+
for (const a of entries) {
|
|
76
|
+
const base = a.project.consumer.basePath;
|
|
77
|
+
const inner = projects.filter((b) => b.project.dir !== a.project.dir &&
|
|
78
|
+
b.project.consumer.basePath !== base &&
|
|
79
|
+
b.project.consumer.basePath.startsWith(base));
|
|
80
|
+
if (inner.length === 0)
|
|
81
|
+
continue;
|
|
82
|
+
for (const page of pages(a)) {
|
|
83
|
+
const id = entryId(page);
|
|
84
|
+
const route = id === "index" ? base : `${base}${id}/`;
|
|
85
|
+
const under = inner.find((b) => route.startsWith(b.project.consumer.basePath));
|
|
86
|
+
if (under !== undefined) {
|
|
87
|
+
problems.push(`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.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return problems;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The problems with serving every entry's permalinks from one route, as
|
|
95
|
+
* sentences: the lists that name anything must be published at one path (an
|
|
96
|
+
* empty one, which every build writes, can be anywhere), and two projects may
|
|
97
|
+
* not declare one name, since neither build can see the other. Entries of one
|
|
98
|
+
* project may: the first entry's wins. `lists` holds each entry's list, or
|
|
99
|
+
* `undefined` where its build wrote none.
|
|
100
|
+
*/
|
|
101
|
+
export function permalinkProblems(entries, lists) {
|
|
102
|
+
const problems = [];
|
|
103
|
+
let first;
|
|
104
|
+
const owners = new Map();
|
|
105
|
+
for (const [i, entry] of entries.entries()) {
|
|
106
|
+
const list = lists[i];
|
|
107
|
+
if (list === undefined || !hasPermalinks(list))
|
|
108
|
+
continue;
|
|
109
|
+
const at = normalizeBase(list.path);
|
|
110
|
+
if (first === undefined)
|
|
111
|
+
first = { entry, path: at };
|
|
112
|
+
else if (at !== first.path) {
|
|
113
|
+
problems.push(`"${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.`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
for (const name of [...Object.keys(list.permalinks), ...Object.keys(list.removed ?? {})]) {
|
|
117
|
+
const owner = owners.get(name);
|
|
118
|
+
if (owner === undefined)
|
|
119
|
+
owners.set(name, entry);
|
|
120
|
+
else if (owner.project.dir !== entry.project.dir) {
|
|
121
|
+
problems.push(`"${owner.name}" and "${entry.name}" both declare the permalink "${name}". A name leads to one place: rename or retire it in one of them.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return problems;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=projects.js.map
|
|
@@ -0,0 +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 { 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"]}
|