@ascribed/astro 0.4.2 → 0.4.4-next.32
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 +2 -2
- package/dist/headers.d.ts +7 -1
- package/dist/headers.js +36 -4
- package/dist/headers.js.map +1 -1
- package/dist/help-route.d.ts +10 -0
- package/dist/help-route.js +10 -0
- package/dist/help-route.js.map +1 -0
- package/dist/help.d.ts +45 -0
- package/dist/help.js +101 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +56 -9
- package/dist/index.js.map +1 -1
- package/dist/project.d.ts +18 -2
- package/dist/project.js +25 -1
- package/dist/project.js.map +1 -1
- package/dist/redirects.d.ts +69 -0
- package/dist/redirects.js +165 -0
- package/dist/redirects.js.map +1 -0
- package/package.json +10 -4
- package/src/Gone.astro +37 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ export default defineConfig({
|
|
|
21
21
|
});
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Astro's `site`, `base`, and `trailingSlash` come from `ascribe.toml`'s `[consumer]` when the Astro config leaves them out. With `projects`, `site` and `trailingSlash` come from the first entry whose project sets them, and `base` stays the site's. Set in the Astro config, each must agree with `[consumer]`, or the build fails.
|
|
24
|
+
Astro's `site`, `base`, and `trailingSlash` come from `ascribe.toml`'s `[consumer]` when the Astro config leaves them out. With `projects`, `site` and `trailingSlash` come from the first entry whose project sets them, and `base` stays the site's. Set in the Astro config, each must agree with `[consumer]`, or the build fails. With `trailing-slash = "never"`, Astro's `build.format` becomes `"file"`, so each page is `x.html`, which a static host serves at `x`; a `build.format` that disagrees with `trailing-slash` fails the build.
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
27
|
// src/content.config.ts
|
|
@@ -69,7 +69,7 @@ Each build writes a record of every page, `_ascribe/pages.json`, and the integra
|
|
|
69
69
|
|
|
70
70
|
A versioned build (`versions` in `ascribe.toml`) is one collection with every live line's pages: the current line's at the base path, and an older line's under its number, `/docs/3.4/`, with the line in front of their entry ids. Each entry's data has `line` and `current`. `Versions.astro`, given a page's entry, lists the lines, each that publishes the page a link, with the page's own marked; `VersionBanner.astro` says on an older line's page which version it is and links to the current line's; `sidebar(entries, { entry })` shows the pages of the entry's line. `astro dev` rebuilds every line. A permalink forwards to the current line, and each line's `llms.txt` and Markdown are published at its base path.
|
|
71
71
|
|
|
72
|
-
A page's Markdown version has no head, so its rules are response headers, which each build lists in `_ascribe/headers.json`: `X-Robots-Tag` when the page isn't listed, and a canonical `Link` to the page. `astro dev` sends them. With `[consumer]
|
|
72
|
+
A page's Markdown version has no head, so its rules are response headers, which each build lists in `_ascribe/headers.json`: `X-Robots-Tag` when the page isn't listed, and a canonical `Link` to the page. `astro dev` sends them. With `[consumer] host = "netlify"`, each build writes them in Netlify's format too, at `_ascribe/host/_headers`, and `astro build` merges every project's into the site's `_headers`; a site with its own `public/_headers` keeps it, and the build fails with the lines it lacks. The same setting has `astro build` write the site's `_redirects`, from the moves each project's `ascribe.addresses` records, ahead of a site's own `public/_redirects`.
|
|
73
73
|
|
|
74
74
|
`npx @ascribed/astro init` writes those files into an Astro project once, as yours from then on: the collection (`src/content.config.ts`), the route (`src/pages/[...slug].astro`), and a layout that loads the elements and draws the sidebar (`src/layouts/Docs.astro`). It never writes over a file. `--project <dir>` names the folder holding `ascribe.toml`, and `--build <name>` the build, when there are several. The layout's sidebar comes from `sidebar(entries, { entry })` in `@ascribed/astro/content`: the project's navigation, in `ascribe.nav`'s order or the default one, from the build's `_ascribe/nav.json`, with no list to keep. `neighbors(entry, entries)` gives a page's previous and next in the same order, for links at its foot. A site's own list, `sidebar(entries, { nav })`, overrides the navigation: its groups, in its order, and the build fails when the list leaves out a page or names one that isn't there; `neighbors` takes it too.
|
|
75
75
|
|
package/dist/headers.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface HeaderRule {
|
|
|
7
7
|
}
|
|
8
8
|
/** The rules in a build's site output, or `undefined` when it has no list. */
|
|
9
9
|
export declare function readHeaders(siteRoot: string): HeaderRule[] | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The rules in the host's `_headers` the compiler wrote in a build's site
|
|
12
|
+
* output, `_ascribe/host/_headers`, in its order: `undefined` when it has
|
|
13
|
+
* none, as without `[consumer] host`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readHostHeaders(siteRoot: string): HeaderRule[] | undefined;
|
|
10
16
|
/**
|
|
11
17
|
* The headers the rules in the build whose site output is at `siteRoot` give
|
|
12
18
|
* `pathname`, a path from the host's root: none when no rule names it. Read
|
|
@@ -41,7 +47,7 @@ export type HeadersOutcome = {
|
|
|
41
47
|
};
|
|
42
48
|
/** What the build does about `_headers`, given every project's rules and the site. */
|
|
43
49
|
export declare function headersOutcome(sources: {
|
|
44
|
-
/** The rules of every project that sets `[consumer]
|
|
50
|
+
/** The rules of every project that sets `[consumer] host = "netlify"`. */
|
|
45
51
|
rules: readonly HeaderRule[];
|
|
46
52
|
/** Astro's `base`. */
|
|
47
53
|
base: string;
|
package/dist/headers.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// The response headers a build's files need from the host:
|
|
2
2
|
// `_ascribe/headers.json` in its site output (output-layout contract §1.4).
|
|
3
3
|
// A Markdown version has no head, so its page's rules are headers, which only
|
|
4
|
-
// the host sends. With `[consumer]
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// with
|
|
4
|
+
// the host sends. With `[consumer] host = "netlify"`, the compiler writes them
|
|
5
|
+
// in Netlify's format too, at `_ascribe/host/_headers` (§1.9), and `astro
|
|
6
|
+
// build` merges every project's into the site's `_headers`; `astro dev` sends
|
|
7
|
+
// them with each Markdown version whatever the setting, so a site can be
|
|
8
|
+
// tried with no host.
|
|
8
9
|
//
|
|
9
10
|
// A site's own `public/_headers` is never rewritten: the build checks it has
|
|
10
11
|
// the lines, and fails with the lines to add when it doesn't.
|
|
@@ -32,6 +33,37 @@ export function readHeaders(siteRoot) {
|
|
|
32
33
|
}
|
|
33
34
|
return list.headers;
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The rules in the host's `_headers` the compiler wrote in a build's site
|
|
38
|
+
* output, `_ascribe/host/_headers`, in its order: `undefined` when it has
|
|
39
|
+
* none, as without `[consumer] host`.
|
|
40
|
+
*/
|
|
41
|
+
export function readHostHeaders(siteRoot) {
|
|
42
|
+
let text;
|
|
43
|
+
try {
|
|
44
|
+
text = readFileSync(path.join(siteRoot, "_ascribe", "host", "_headers"), "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const rules = [];
|
|
50
|
+
let current;
|
|
51
|
+
for (const line of text.split(/\r?\n/)) {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
54
|
+
continue;
|
|
55
|
+
if (!/^\s/.test(line)) {
|
|
56
|
+
current = { path: trimmed, headers: {} };
|
|
57
|
+
rules.push(current);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const colon = trimmed.indexOf(":");
|
|
61
|
+
if (current === undefined || colon === -1)
|
|
62
|
+
continue;
|
|
63
|
+
current.headers[trimmed.slice(0, colon).trim()] = trimmed.slice(colon + 1).trim();
|
|
64
|
+
}
|
|
65
|
+
return rules;
|
|
66
|
+
}
|
|
35
67
|
/** A build's rules by path, as last read, and when its file was written. */
|
|
36
68
|
const byPath = new Map();
|
|
37
69
|
/**
|
package/dist/headers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"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","sourcesContent":["// The response headers a build's files need from the host:\n// `_ascribe/headers.json` in its site output (output-layout contract §1.4).\n// A Markdown version has no head, so its page's rules are headers, which only\n// the host sends. With `[consumer] headers = \"netlify\"`, `astro build` writes\n// them into the site's `_headers`, Netlify's format; `astro dev` sends them\n// with each Markdown version whatever the setting, so a site can be tried\n// with no host.\n//\n// A site's own `public/_headers` is never rewritten: the build checks it has\n// the lines, and fails with the lines to add when it doesn't.\nimport { readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The format version of `_ascribe/headers.json` this reads. */\nconst VERSION = 1;\n\n/** The headers a host sends with one path, matched exactly. */\nexport interface HeaderRule {\n /** The path, from the host's root. */\n path: string;\n /** Each header's value, by its name. */\n headers: Record<string, string>;\n}\n\n/** `_ascribe/headers.json`. */\ninterface HeaderList {\n version: number;\n headers: HeaderRule[];\n}\n\nfunction headersFile(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"headers.json\");\n}\n\n/** The rules in a build's site output, or `undefined` when it has no list. */\nexport function readHeaders(siteRoot: string): HeaderRule[] | undefined {\n const file = headersFile(siteRoot);\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return undefined;\n }\n const list = JSON.parse(text) as HeaderList;\n if (list.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(list.version)} of the headers list, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return list.headers;\n}\n\n/** A build's rules by path, as last read, and when its file was written. */\nconst byPath = new Map<string, { written: number; rules: Map<string, Record<string, string>> }>();\n\n/**\n * The headers the rules in the build whose site output is at `siteRoot` give\n * `pathname`, a path from the host's root: none when no rule names it. Read\n * again when the file changes, as it does with each build in `astro dev`.\n */\nexport function headersFor(siteRoot: string, pathname: string): Record<string, string> {\n let written: number;\n try {\n written = statSync(headersFile(siteRoot)).mtimeMs;\n } catch {\n return {};\n }\n let known = byPath.get(siteRoot);\n if (known === undefined || known.written !== written) {\n const rules = new Map<string, Record<string, string>>();\n for (const rule of readHeaders(siteRoot) ?? []) {\n if (!rules.has(rule.path)) rules.set(rule.path, rule.headers);\n }\n known = { written, rules };\n byPath.set(siteRoot, known);\n }\n return known.rules.get(pathname) ?? {};\n}\n\n/**\n * Every project's rules as one list, sorted by path: a path two projects\n * both have keeps the first's rule, as the first entry's files win.\n */\nexport function mergeRules(projects: readonly (readonly HeaderRule[])[]): HeaderRule[] {\n const rules = new Map<string, HeaderRule>();\n for (const list of projects) {\n for (const rule of list) if (!rules.has(rule.path)) rules.set(rule.path, rule);\n }\n return [...rules.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n\n/** The rules in Netlify's `_headers` format: each path, then its headers indented below it. */\nexport function netlifyHeaders(rules: readonly HeaderRule[]): string {\n return rules\n .map(\n (rule) =>\n `${rule.path}\\n${Object.entries(rule.headers)\n .map(([name, value]) => ` ${name}: ${value}\\n`)\n .join(\"\")}`,\n )\n .join(\"\");\n}\n\n/**\n * The headers `_headers` gives each path exactly, as Netlify reads the file:\n * a line that isn't indented is a path, an indented `Name: value` line is a\n * header of the path above it, and a line starting with `#` is a comment.\n * Names are compared in lower case, as Netlify compares them. A path with a\n * wildcard or a placeholder is kept as written, and matches only itself here.\n */\nfunction parseHeaders(text: string): Map<string, Set<string>> {\n const paths = new Map<string, Set<string>>();\n let current: Set<string> | undefined;\n for (const line of text.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (trimmed === \"\" || trimmed.startsWith(\"#\")) continue;\n if (!/^\\s/.test(line)) {\n current = paths.get(trimmed) ?? new Set();\n paths.set(trimmed, current);\n continue;\n }\n const colon = trimmed.indexOf(\":\");\n if (current === undefined || colon === -1) continue;\n current.add(headerKey(trimmed.slice(0, colon), trimmed.slice(colon + 1)));\n }\n return paths;\n}\n\nfunction headerKey(name: string, value: string): string {\n return `${name.trim().toLowerCase()}: ${value.trim()}`;\n}\n\n/**\n * The rules, each with only the headers the site's own `_headers` doesn't\n * give its path, leaving out a rule it has whole. Empty when it has every\n * line.\n */\nexport function missingRules(own: string, rules: readonly HeaderRule[]): HeaderRule[] {\n const has = parseHeaders(own);\n const missing: HeaderRule[] = [];\n for (const rule of rules) {\n const given = has.get(rule.path);\n const headers = Object.fromEntries(\n Object.entries(rule.headers).filter(([name, value]) => !given?.has(headerKey(name, value))),\n );\n if (Object.keys(headers).length > 0) missing.push({ path: rule.path, headers });\n }\n return missing;\n}\n\n/** What the build does about the host's headers. */\nexport type HeadersOutcome =\n | { kind: \"none\" }\n | { kind: \"write\"; text: string }\n | { kind: \"note\"; text: string }\n | { kind: \"fail\"; text: string };\n\n/** What the build does about `_headers`, given every project's rules and the site. */\nexport function headersOutcome(sources: {\n /** The rules of every project that sets `[consumer] headers = \"netlify\"`. */\n rules: readonly HeaderRule[];\n /** Astro's `base`. */\n base: string;\n /** The site's own `public/_headers`, when it has one. */\n own: string | undefined;\n}): HeadersOutcome {\n if (sources.rules.length === 0) return { kind: \"none\" };\n const text = netlifyHeaders(sources.rules);\n const base = normalizeBase(sources.base);\n if (base !== \"/\") {\n return {\n kind: \"note\",\n text: `no _headers: Astro's \\`base\\` is \"${base}\", and Netlify reads _headers only at the root of what's deployed. Put these lines in the host's own:\\n${text}`,\n };\n }\n if (sources.own !== undefined) {\n const missing = missingRules(sources.own, sources.rules);\n if (missing.length === 0) {\n return {\n kind: \"note\",\n text: \"no _headers: public/_headers is the site's own, and has Ascribe's lines.\",\n };\n }\n return {\n kind: \"fail\",\n text: `public/_headers is the site's own, and lacks the headers Ascribe's pages need. Add these lines to it:\\n${netlifyHeaders(missing)}`,\n };\n }\n return { kind: \"write\", text };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"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","sourcesContent":["// The response headers a build's files need from the host:\n// `_ascribe/headers.json` in its site output (output-layout contract §1.4).\n// A Markdown version has no head, so its page's rules are headers, which only\n// the host sends. With `[consumer] host = \"netlify\"`, the compiler writes them\n// in Netlify's format too, at `_ascribe/host/_headers` (§1.9), and `astro\n// build` merges every project's into the site's `_headers`; `astro dev` sends\n// them with each Markdown version whatever the setting, so a site can be\n// tried with no host.\n//\n// A site's own `public/_headers` is never rewritten: the build checks it has\n// the lines, and fails with the lines to add when it doesn't.\nimport { readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The format version of `_ascribe/headers.json` this reads. */\nconst VERSION = 1;\n\n/** The headers a host sends with one path, matched exactly. */\nexport interface HeaderRule {\n /** The path, from the host's root. */\n path: string;\n /** Each header's value, by its name. */\n headers: Record<string, string>;\n}\n\n/** `_ascribe/headers.json`. */\ninterface HeaderList {\n version: number;\n headers: HeaderRule[];\n}\n\nfunction headersFile(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"headers.json\");\n}\n\n/** The rules in a build's site output, or `undefined` when it has no list. */\nexport function readHeaders(siteRoot: string): HeaderRule[] | undefined {\n const file = headersFile(siteRoot);\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return undefined;\n }\n const list = JSON.parse(text) as HeaderList;\n if (list.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(list.version)} of the headers list, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return list.headers;\n}\n\n/**\n * The rules in the host's `_headers` the compiler wrote in a build's site\n * output, `_ascribe/host/_headers`, in its order: `undefined` when it has\n * none, as without `[consumer] host`.\n */\nexport function readHostHeaders(siteRoot: string): HeaderRule[] | undefined {\n let text: string;\n try {\n text = readFileSync(path.join(siteRoot, \"_ascribe\", \"host\", \"_headers\"), \"utf8\");\n } catch {\n return undefined;\n }\n const rules: HeaderRule[] = [];\n let current: HeaderRule | undefined;\n for (const line of text.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (trimmed === \"\" || trimmed.startsWith(\"#\")) continue;\n if (!/^\\s/.test(line)) {\n current = { path: trimmed, headers: {} };\n rules.push(current);\n continue;\n }\n const colon = trimmed.indexOf(\":\");\n if (current === undefined || colon === -1) continue;\n current.headers[trimmed.slice(0, colon).trim()] = trimmed.slice(colon + 1).trim();\n }\n return rules;\n}\n\n/** A build's rules by path, as last read, and when its file was written. */\nconst byPath = new Map<string, { written: number; rules: Map<string, Record<string, string>> }>();\n\n/**\n * The headers the rules in the build whose site output is at `siteRoot` give\n * `pathname`, a path from the host's root: none when no rule names it. Read\n * again when the file changes, as it does with each build in `astro dev`.\n */\nexport function headersFor(siteRoot: string, pathname: string): Record<string, string> {\n let written: number;\n try {\n written = statSync(headersFile(siteRoot)).mtimeMs;\n } catch {\n return {};\n }\n let known = byPath.get(siteRoot);\n if (known === undefined || known.written !== written) {\n const rules = new Map<string, Record<string, string>>();\n for (const rule of readHeaders(siteRoot) ?? []) {\n if (!rules.has(rule.path)) rules.set(rule.path, rule.headers);\n }\n known = { written, rules };\n byPath.set(siteRoot, known);\n }\n return known.rules.get(pathname) ?? {};\n}\n\n/**\n * Every project's rules as one list, sorted by path: a path two projects\n * both have keeps the first's rule, as the first entry's files win.\n */\nexport function mergeRules(projects: readonly (readonly HeaderRule[])[]): HeaderRule[] {\n const rules = new Map<string, HeaderRule>();\n for (const list of projects) {\n for (const rule of list) if (!rules.has(rule.path)) rules.set(rule.path, rule);\n }\n return [...rules.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n\n/** The rules in Netlify's `_headers` format: each path, then its headers indented below it. */\nexport function netlifyHeaders(rules: readonly HeaderRule[]): string {\n return rules\n .map(\n (rule) =>\n `${rule.path}\\n${Object.entries(rule.headers)\n .map(([name, value]) => ` ${name}: ${value}\\n`)\n .join(\"\")}`,\n )\n .join(\"\");\n}\n\n/**\n * The headers `_headers` gives each path exactly, as Netlify reads the file:\n * a line that isn't indented is a path, an indented `Name: value` line is a\n * header of the path above it, and a line starting with `#` is a comment.\n * Names are compared in lower case, as Netlify compares them. A path with a\n * wildcard or a placeholder is kept as written, and matches only itself here.\n */\nfunction parseHeaders(text: string): Map<string, Set<string>> {\n const paths = new Map<string, Set<string>>();\n let current: Set<string> | undefined;\n for (const line of text.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (trimmed === \"\" || trimmed.startsWith(\"#\")) continue;\n if (!/^\\s/.test(line)) {\n current = paths.get(trimmed) ?? new Set();\n paths.set(trimmed, current);\n continue;\n }\n const colon = trimmed.indexOf(\":\");\n if (current === undefined || colon === -1) continue;\n current.add(headerKey(trimmed.slice(0, colon), trimmed.slice(colon + 1)));\n }\n return paths;\n}\n\nfunction headerKey(name: string, value: string): string {\n return `${name.trim().toLowerCase()}: ${value.trim()}`;\n}\n\n/**\n * The rules, each with only the headers the site's own `_headers` doesn't\n * give its path, leaving out a rule it has whole. Empty when it has every\n * line.\n */\nexport function missingRules(own: string, rules: readonly HeaderRule[]): HeaderRule[] {\n const has = parseHeaders(own);\n const missing: HeaderRule[] = [];\n for (const rule of rules) {\n const given = has.get(rule.path);\n const headers = Object.fromEntries(\n Object.entries(rule.headers).filter(([name, value]) => !given?.has(headerKey(name, value))),\n );\n if (Object.keys(headers).length > 0) missing.push({ path: rule.path, headers });\n }\n return missing;\n}\n\n/** What the build does about the host's headers. */\nexport type HeadersOutcome =\n | { kind: \"none\" }\n | { kind: \"write\"; text: string }\n | { kind: \"note\"; text: string }\n | { kind: \"fail\"; text: string };\n\n/** What the build does about `_headers`, given every project's rules and the site. */\nexport function headersOutcome(sources: {\n /** The rules of every project that sets `[consumer] host = \"netlify\"`. */\n rules: readonly HeaderRule[];\n /** Astro's `base`. */\n base: string;\n /** The site's own `public/_headers`, when it has one. */\n own: string | undefined;\n}): HeadersOutcome {\n if (sources.rules.length === 0) return { kind: \"none\" };\n const text = netlifyHeaders(sources.rules);\n const base = normalizeBase(sources.base);\n if (base !== \"/\") {\n return {\n kind: \"note\",\n text: `no _headers: Astro's \\`base\\` is \"${base}\", and Netlify reads _headers only at the root of what's deployed. Put these lines in the host's own:\\n${text}`,\n };\n }\n if (sources.own !== undefined) {\n const missing = missingRules(sources.own, sources.rules);\n if (missing.length === 0) {\n return {\n kind: \"note\",\n text: \"no _headers: public/_headers is the site's own, and has Ascribe's lines.\",\n };\n }\n return {\n kind: \"fail\",\n text: `public/_headers is the site's own, and lacks the headers Ascribe's pages need. Add these lines to it:\\n${netlifyHeaders(missing)}`,\n };\n }\n return { kind: \"write\", text };\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { base, permalinks } from "virtual:ascribe/site";
|
|
2
|
+
import { helpPaths } from "./help.js";
|
|
3
|
+
export const getStaticPaths = (({ routePattern }) => helpPaths(permalinks.map((source) => source.siteRoot), base, routePattern));
|
|
4
|
+
export const GET = ({ props }) => new Response(props.body, {
|
|
5
|
+
headers: {
|
|
6
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
7
|
+
"Access-Control-Allow-Origin": "*",
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
//# sourceMappingURL=help-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"help-route.js","sourceRoot":"","sources":["../src/help-route.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,CAClD,SAAS,CACP,UAAU,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAC3C,IAAI,EACJ,YAAY,CACb,CAA0B,CAAC;AAE9B,MAAM,CAAC,MAAM,GAAG,GAAa,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CACzC,IAAI,QAAQ,CAAE,KAA0B,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE;QACP,cAAc,EAAE,iCAAiC;QACjD,6BAA6B,EAAE,GAAG;KACnC;CACF,CAAC,CAAC","sourcesContent":["// A help entry's own address: one entry of a line's `_ascribe/help.json`, as\n// JSON (`help.ts`). The integration adds this route once for each folder of\n// entries, `<path>[id].json` for the current line and `<line>/go/[id].json`\n// under the base path for each older one; which folder this is comes from\n// the route's pattern. Any origin may read it, as the build's headers say\n// for a host.\nimport type { APIRoute, GetStaticPaths } from \"astro\";\nimport { base, permalinks } from \"virtual:ascribe/site\";\nimport { helpPaths } from \"./help.js\";\n\nexport const getStaticPaths = (({ routePattern }) =>\n helpPaths(\n permalinks.map((source) => source.siteRoot),\n base,\n routePattern,\n )) satisfies GetStaticPaths;\n\nexport const GET: APIRoute = ({ props }) =>\n new Response((props as { body: string }).body, {\n headers: {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Access-Control-Allow-Origin\": \"*\",\n },\n });\n"]}
|
package/dist/help.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** A permalink's section, as `_ascribe/help.json` has it. */
|
|
2
|
+
interface HelpEntry {
|
|
3
|
+
title: string;
|
|
4
|
+
summary?: string;
|
|
5
|
+
text: string;
|
|
6
|
+
address?: string;
|
|
7
|
+
source: string;
|
|
8
|
+
}
|
|
9
|
+
/** `_ascribe/help.json`. */
|
|
10
|
+
export interface HelpFile {
|
|
11
|
+
version: number;
|
|
12
|
+
/** The build's name, as `ascribe.toml` declares it. */
|
|
13
|
+
build: string;
|
|
14
|
+
/** For a line of a versioned build, the line. */
|
|
15
|
+
line?: string;
|
|
16
|
+
help: Record<string, HelpEntry>;
|
|
17
|
+
}
|
|
18
|
+
/** The file in a build's site output, or `undefined` when there isn't one. */
|
|
19
|
+
export declare function readHelp(siteRoot: string): HelpFile | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* What each entry's address answers, by the address from the host's root:
|
|
22
|
+
* the file it's in with that entry alone, its `version`, `build`, and
|
|
23
|
+
* `line` as the file has them, so a product reads a fetched entry and its
|
|
24
|
+
* bundled copy alike. An address two files give keeps the first's.
|
|
25
|
+
*/
|
|
26
|
+
export declare function helpAddresses(siteRoots: readonly string[]): Map<string, HelpFile>;
|
|
27
|
+
/**
|
|
28
|
+
* The route patterns the entries need, one for each folder that has any:
|
|
29
|
+
* the current line's, then each older line's. Throws when a folder isn't
|
|
30
|
+
* under `base`, since an Astro site serves nothing outside it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function helpPatterns(siteRoots: readonly string[], base: string): string[];
|
|
33
|
+
/**
|
|
34
|
+
* `getStaticPaths` for the route at `routePattern`, one of `helpPatterns`:
|
|
35
|
+
* one answer for each entry whose address it matches, the entry as JSON.
|
|
36
|
+
*/
|
|
37
|
+
export declare function helpPaths(siteRoots: readonly string[], base: string, routePattern: string): {
|
|
38
|
+
params: {
|
|
39
|
+
id: string;
|
|
40
|
+
};
|
|
41
|
+
props: {
|
|
42
|
+
body: string;
|
|
43
|
+
};
|
|
44
|
+
}[];
|
|
45
|
+
export {};
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Each permalink's help at an address of its own, for a product to fetch its
|
|
2
|
+
// line's: one entry of a build's `_ascribe/help.json` (output-layout contract
|
|
3
|
+
// §1.7), as JSON. The current line's entries are beside its permalinks'
|
|
4
|
+
// addresses, at `<path><name>.json`; an older line's are at `go/<name>.json`
|
|
5
|
+
// under the line's own base path, `/docs/3.4/go/connect.json`, since the
|
|
6
|
+
// permalinks' addresses are the current line's. The integration adds a route
|
|
7
|
+
// (`help-route.ts`) for each of those folders, in `astro build` and `astro dev`.
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { permalinkPattern, readPermalinks } from "./permalinks.js";
|
|
11
|
+
import { normalizeBase } from "./project.js";
|
|
12
|
+
import { olderLines } from "./versions.js";
|
|
13
|
+
/** The file's format version this reads. */
|
|
14
|
+
const VERSION = 1;
|
|
15
|
+
/** The file in a build's site output, or `undefined` when there isn't one. */
|
|
16
|
+
export function readHelp(siteRoot) {
|
|
17
|
+
const file = path.join(siteRoot, "_ascribe", "help.json");
|
|
18
|
+
let text;
|
|
19
|
+
try {
|
|
20
|
+
text = readFileSync(file, "utf8");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const help = JSON.parse(text);
|
|
26
|
+
if (help.version !== VERSION) {
|
|
27
|
+
throw new Error(`@ascribed/astro: ${file} is version ${String(help.version)} of the help file, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`);
|
|
28
|
+
}
|
|
29
|
+
return help;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Each line's file, with the folder its entries are served under: the
|
|
33
|
+
* current line's at the permalinks' path, each older line's at `go/` under
|
|
34
|
+
* its base path. A site output with no file, or no permalinks list, has none.
|
|
35
|
+
*/
|
|
36
|
+
function helpFolders(siteRoots) {
|
|
37
|
+
return siteRoots.flatMap((siteRoot) => {
|
|
38
|
+
const list = readPermalinks(siteRoot);
|
|
39
|
+
const help = readHelp(siteRoot);
|
|
40
|
+
const current = list === undefined || help === undefined ? [] : [{ folder: normalizeBase(list.path), help }];
|
|
41
|
+
const older = olderLines(siteRoot).flatMap((line) => {
|
|
42
|
+
const lineHelp = readHelp(line.siteRoot);
|
|
43
|
+
return lineHelp === undefined
|
|
44
|
+
? []
|
|
45
|
+
: [{ folder: `${normalizeBase(line.base)}go/`, help: lineHelp }];
|
|
46
|
+
});
|
|
47
|
+
return [...current, ...older];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What each entry's address answers, by the address from the host's root:
|
|
52
|
+
* the file it's in with that entry alone, its `version`, `build`, and
|
|
53
|
+
* `line` as the file has them, so a product reads a fetched entry and its
|
|
54
|
+
* bundled copy alike. An address two files give keeps the first's.
|
|
55
|
+
*/
|
|
56
|
+
export function helpAddresses(siteRoots) {
|
|
57
|
+
const addresses = new Map();
|
|
58
|
+
for (const { folder, help } of helpFolders(siteRoots)) {
|
|
59
|
+
for (const [name, entry] of Object.entries(help.help)) {
|
|
60
|
+
const address = `${folder}${name}.json`;
|
|
61
|
+
if (addresses.has(address))
|
|
62
|
+
continue;
|
|
63
|
+
addresses.set(address, {
|
|
64
|
+
version: help.version,
|
|
65
|
+
build: help.build,
|
|
66
|
+
...(help.line === undefined ? {} : { line: help.line }),
|
|
67
|
+
help: { [name]: entry },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return addresses;
|
|
72
|
+
}
|
|
73
|
+
/** The route pattern for a folder's entries, as Astro takes it: under `base`, which Astro adds. */
|
|
74
|
+
function helpPattern(folder, base) {
|
|
75
|
+
return `${permalinkPattern(folder, base)}.json`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The route patterns the entries need, one for each folder that has any:
|
|
79
|
+
* the current line's, then each older line's. Throws when a folder isn't
|
|
80
|
+
* under `base`, since an Astro site serves nothing outside it.
|
|
81
|
+
*/
|
|
82
|
+
export function helpPatterns(siteRoots, base) {
|
|
83
|
+
const patterns = helpFolders(siteRoots)
|
|
84
|
+
.filter(({ help }) => Object.keys(help.help).length > 0)
|
|
85
|
+
.map(({ folder }) => helpPattern(folder, base));
|
|
86
|
+
return [...new Set(patterns)];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* `getStaticPaths` for the route at `routePattern`, one of `helpPatterns`:
|
|
90
|
+
* one answer for each entry whose address it matches, the entry as JSON.
|
|
91
|
+
*/
|
|
92
|
+
export function helpPaths(siteRoots, base, routePattern) {
|
|
93
|
+
return [...helpAddresses(siteRoots)].flatMap(([address, help]) => {
|
|
94
|
+
const name = path.posix.basename(address, ".json");
|
|
95
|
+
const folder = address.slice(0, address.length - `${name}.json`.length);
|
|
96
|
+
return helpPattern(folder, base) === routePattern
|
|
97
|
+
? [{ params: { id: name }, props: { body: `${JSON.stringify(help, null, 2)}\n` } }]
|
|
98
|
+
: [];
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=help.js.map
|
package/dist/help.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"help.js","sourceRoot":"","sources":["../src/help.ts"],"names":[],"mappings":"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","sourcesContent":["// Each permalink's help at an address of its own, for a product to fetch its\n// line's: one entry of a build's `_ascribe/help.json` (output-layout contract\n// §1.7), as JSON. The current line's entries are beside its permalinks'\n// addresses, at `<path><name>.json`; an older line's are at `go/<name>.json`\n// under the line's own base path, `/docs/3.4/go/connect.json`, since the\n// permalinks' addresses are the current line's. The integration adds a route\n// (`help-route.ts`) for each of those folders, in `astro build` and `astro dev`.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { permalinkPattern, readPermalinks } from \"./permalinks.js\";\nimport { normalizeBase } from \"./project.js\";\nimport { olderLines } from \"./versions.js\";\n\n/** The file's format version this reads. */\nconst VERSION = 1;\n\n/** A permalink's section, as `_ascribe/help.json` has it. */\ninterface HelpEntry {\n title: string;\n summary?: string;\n text: string;\n address?: string;\n source: string;\n}\n\n/** `_ascribe/help.json`. */\nexport interface HelpFile {\n version: number;\n /** The build's name, as `ascribe.toml` declares it. */\n build: string;\n /** For a line of a versioned build, the line. */\n line?: string;\n help: Record<string, HelpEntry>;\n}\n\n/** The file in a build's site output, or `undefined` when there isn't one. */\nexport function readHelp(siteRoot: string): HelpFile | undefined {\n const file = path.join(siteRoot, \"_ascribe\", \"help.json\");\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return undefined;\n }\n const help = JSON.parse(text) as HelpFile;\n if (help.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(help.version)} of the help file, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return help;\n}\n\n/** A folder of entries' addresses, from the host's root, and the file they're served from. */\ninterface HelpFolder {\n folder: string;\n help: HelpFile;\n}\n\n/**\n * Each line's file, with the folder its entries are served under: the\n * current line's at the permalinks' path, each older line's at `go/` under\n * its base path. A site output with no file, or no permalinks list, has none.\n */\nfunction helpFolders(siteRoots: readonly string[]): HelpFolder[] {\n return siteRoots.flatMap((siteRoot) => {\n const list = readPermalinks(siteRoot);\n const help = readHelp(siteRoot);\n const current =\n list === undefined || help === undefined ? [] : [{ folder: normalizeBase(list.path), help }];\n const older = olderLines(siteRoot).flatMap((line) => {\n const lineHelp = readHelp(line.siteRoot);\n return lineHelp === undefined\n ? []\n : [{ folder: `${normalizeBase(line.base)}go/`, help: lineHelp }];\n });\n return [...current, ...older];\n });\n}\n\n/**\n * What each entry's address answers, by the address from the host's root:\n * the file it's in with that entry alone, its `version`, `build`, and\n * `line` as the file has them, so a product reads a fetched entry and its\n * bundled copy alike. An address two files give keeps the first's.\n */\nexport function helpAddresses(siteRoots: readonly string[]): Map<string, HelpFile> {\n const addresses = new Map<string, HelpFile>();\n for (const { folder, help } of helpFolders(siteRoots)) {\n for (const [name, entry] of Object.entries(help.help)) {\n const address = `${folder}${name}.json`;\n if (addresses.has(address)) continue;\n addresses.set(address, {\n version: help.version,\n build: help.build,\n ...(help.line === undefined ? {} : { line: help.line }),\n help: { [name]: entry },\n });\n }\n }\n return addresses;\n}\n\n/** The route pattern for a folder's entries, as Astro takes it: under `base`, which Astro adds. */\nfunction helpPattern(folder: string, base: string): string {\n return `${permalinkPattern(folder, base)}.json`;\n}\n\n/**\n * The route patterns the entries need, one for each folder that has any:\n * the current line's, then each older line's. Throws when a folder isn't\n * under `base`, since an Astro site serves nothing outside it.\n */\nexport function helpPatterns(siteRoots: readonly string[], base: string): string[] {\n const patterns = helpFolders(siteRoots)\n .filter(({ help }) => Object.keys(help.help).length > 0)\n .map(({ folder }) => helpPattern(folder, base));\n return [...new Set(patterns)];\n}\n\n/**\n * `getStaticPaths` for the route at `routePattern`, one of `helpPatterns`:\n * one answer for each entry whose address it matches, the entry as JSON.\n */\nexport function helpPaths(\n siteRoots: readonly string[],\n base: string,\n routePattern: string,\n): { params: { id: string }; props: { body: string } }[] {\n return [...helpAddresses(siteRoots)].flatMap(([address, help]) => {\n const name = path.posix.basename(address, \".json\");\n const folder = address.slice(0, address.length - `${name}.json`.length);\n return helpPattern(folder, base) === routePattern\n ? [{ params: { id: name }, props: { body: `${JSON.stringify(help, null, 2)}\\n` } }]\n : [];\n });\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -60,9 +60,9 @@ export interface ProjectsOptions extends SharedOptions {
|
|
|
60
60
|
export type AscribeOptions = OneProjectOptions | ProjectsOptions;
|
|
61
61
|
/**
|
|
62
62
|
* Runs `ascribe build` for each project, checks the site's routing, adds the markdown plugin, adds a
|
|
63
|
-
* page for each permalink's address, and serves published files, and, with `[consumer] agents = true`,
|
|
63
|
+
* page for each permalink's address and an answer for each line's help entries, and serves published files, and, with `[consumer] agents = true`,
|
|
64
64
|
* `llms.txt` and each page's Markdown, each at its project's base path; and writes the site's one
|
|
65
|
-
* sitemap and `robots.txt` from every project's page records, and, with `[consumer]
|
|
66
|
-
* "netlify"`, its `_headers` from
|
|
65
|
+
* sitemap and `robots.txt` from every project's page records, and, with `[consumer] host =
|
|
66
|
+
* "netlify"`, its `_headers` and `_redirects` from the host's files each build writes.
|
|
67
67
|
*/
|
|
68
68
|
export default function ascribe(options: AscribeOptions): AstroIntegration;
|
package/dist/index.js
CHANGED
|
@@ -49,9 +49,11 @@ import { codeTitles } from "./code-titles.js";
|
|
|
49
49
|
import { SeenFiles, watchDev } from "./dev.js";
|
|
50
50
|
import { agentsMiddleware, copyAgentFiles, copyPublishedFiles, filesMiddleware, folderUnder, inOrder, } from "./files.js";
|
|
51
51
|
import { WARN as HEAD_WARN } from "./head-check.js";
|
|
52
|
-
import { headersFor, headersOutcome, mergeRules,
|
|
52
|
+
import { headersFor, headersOutcome, mergeRules, readHostHeaders } from "./headers.js";
|
|
53
|
+
import { gonePatterns, goneRoutes, mergeRedirects, readHostRedirects, redirectsOutcome, } from "./redirects.js";
|
|
54
|
+
import { helpPatterns } from "./help.js";
|
|
53
55
|
import { forwardingRoutes, hasPermalinks, permalinkPattern, readPermalinks } from "./permalinks.js";
|
|
54
|
-
import { consumerMismatches, homeRoute, normalizeBase, readProject, supplyRouting, } from "./project.js";
|
|
56
|
+
import { consumerMismatches, homeRoute, normalizeBase, readProject, supplyBuildFormat, supplyRouting, } from "./project.js";
|
|
55
57
|
import { basePathClashes, nameProblems, pagesUnderOtherPaths, permalinkProblems, } from "./projects.js";
|
|
56
58
|
import { pageList, readRecords, routeAs, routeKey, sitemapRoute, } from "./records.js";
|
|
57
59
|
import rehypeAscribeAttributes from "./rehype.js";
|
|
@@ -77,10 +79,10 @@ function fail(problems, what) {
|
|
|
77
79
|
}
|
|
78
80
|
/**
|
|
79
81
|
* Runs `ascribe build` for each project, checks the site's routing, adds the markdown plugin, adds a
|
|
80
|
-
* page for each permalink's address, and serves published files, and, with `[consumer] agents = true`,
|
|
82
|
+
* page for each permalink's address and an answer for each line's help entries, and serves published files, and, with `[consumer] agents = true`,
|
|
81
83
|
* `llms.txt` and each page's Markdown, each at its project's base path; and writes the site's one
|
|
82
|
-
* sitemap and `robots.txt` from every project's page records, and, with `[consumer]
|
|
83
|
-
* "netlify"`, its `_headers` from
|
|
84
|
+
* sitemap and `robots.txt` from every project's page records, and, with `[consumer] host =
|
|
85
|
+
* "netlify"`, its `_headers` and `_redirects` from the host's files each build writes.
|
|
84
86
|
*/
|
|
85
87
|
export default function ascribe(options) {
|
|
86
88
|
/** With `projects`; otherwise the one entry has no name a collection gives. */
|
|
@@ -123,6 +125,7 @@ export default function ascribe(options) {
|
|
|
123
125
|
sitePages,
|
|
124
126
|
excluded: [
|
|
125
127
|
...forwardingRoutes(entries.map((entry) => readPermalinks(entry.siteRoot))),
|
|
128
|
+
...goneRoutes(entries.map((entry) => entry.siteRoot)),
|
|
126
129
|
`${root}404`,
|
|
127
130
|
`${root}500`,
|
|
128
131
|
...(options.sitemap?.exclude ?? []),
|
|
@@ -175,6 +178,9 @@ export default function ascribe(options) {
|
|
|
175
178
|
? `"${entry.name}"'s ascribe.toml and astro.config disagree`
|
|
176
179
|
: "ascribe.toml and astro.config disagree");
|
|
177
180
|
}
|
|
181
|
+
// Each page's file must be served at the route Ascribe publishes for it, not behind a redirect.
|
|
182
|
+
const format = supplyBuildFormat(astro.trailingSlash, config.build.format);
|
|
183
|
+
fail(format.problems, "ascribe.toml and astro.config disagree");
|
|
178
184
|
fail(basePathClashes(entries), "two projects share a base path");
|
|
179
185
|
if (typeof options.review === "string" && !entries.some((e) => e.name === options.review)) {
|
|
180
186
|
throw new Error(`@ascribed/astro: \`review\` names "${options.review}", and no entry of \`projects\` has that name.`);
|
|
@@ -311,11 +317,31 @@ export default function ascribe(options) {
|
|
|
311
317
|
prerender: true,
|
|
312
318
|
});
|
|
313
319
|
}
|
|
320
|
+
// Each retired address's page, which the host's `_redirects` serves at the address with
|
|
321
|
+
// a 404. One route for each folder: each build's base path's.
|
|
322
|
+
for (const pattern of gonePatterns(entries.map((entry) => entry.siteRoot), astro.base)) {
|
|
323
|
+
injectRoute({
|
|
324
|
+
pattern,
|
|
325
|
+
entrypoint: new URL("../src/Gone.astro", import.meta.url),
|
|
326
|
+
prerender: true,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
// Each permalink's help, at `<name>.json` beside its address, and each older line's
|
|
330
|
+
// under the line's base path. One route for each folder: the current line's, then each
|
|
331
|
+
// older line's.
|
|
332
|
+
for (const pattern of helpPatterns(entries.map((entry) => entry.siteRoot), astro.base)) {
|
|
333
|
+
injectRoute({
|
|
334
|
+
pattern,
|
|
335
|
+
entrypoint: new URL("./help-route.js", import.meta.url),
|
|
336
|
+
prerender: true,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
314
339
|
}
|
|
315
340
|
const projectHome = (entry) => homeRoute(entry.project.consumer.basePath, astro.trailingSlash);
|
|
316
341
|
// Dev serving of `_ascribe/files/`, and of the plain output for agents; the build copies them in `astro:build:done`.
|
|
317
342
|
updateConfig({
|
|
318
343
|
...supplied,
|
|
344
|
+
...(format.supplied === undefined ? {} : { build: { format: format.supplied } }),
|
|
319
345
|
...(options.codeTitles === false
|
|
320
346
|
? {}
|
|
321
347
|
: { markdown: { shikiConfig: { transformers: [codeTitles] } } }),
|
|
@@ -330,6 +356,7 @@ export default function ascribe(options) {
|
|
|
330
356
|
? `export const siteRoot = ${JSON.stringify(entries[0]?.siteRoot ?? "")};\n` +
|
|
331
357
|
`export const astroRoot = ${JSON.stringify(root)};\n` +
|
|
332
358
|
`export const home = ${JSON.stringify(homeRoute(astro.base, astro.trailingSlash))};\n` +
|
|
359
|
+
`export const base = ${JSON.stringify(astro.base)};\n` +
|
|
333
360
|
`export const collections = ${JSON.stringify(named ? Object.fromEntries(entries.map((e) => [e.name, e.siteRoot])) : {})};\n` +
|
|
334
361
|
`export const permalinks = ${JSON.stringify(entries.map((e) => ({ siteRoot: e.siteRoot, home: projectHome(e) })))};\n` +
|
|
335
362
|
`export const routes = ${JSON.stringify(entries.map((e) => ({
|
|
@@ -478,12 +505,17 @@ export default function ascribe(options) {
|
|
|
478
505
|
await writeFile(path.join(out, "robots.txt"), files.robots);
|
|
479
506
|
logger.info("wrote /robots.txt");
|
|
480
507
|
}
|
|
481
|
-
// The host's
|
|
508
|
+
// The host's files, from the projects that name it: each build's,
|
|
509
|
+
// and each of its older lines'.
|
|
510
|
+
const hostRoots = entries
|
|
511
|
+
.filter((entry) => entry.project.consumer.host === "netlify")
|
|
512
|
+
.flatMap((entry) => [
|
|
513
|
+
entry.siteRoot,
|
|
514
|
+
...olderLines(entry.siteRoot).map((line) => line.siteRoot),
|
|
515
|
+
]);
|
|
482
516
|
const ownHeaders = path.join(publicDir, "_headers");
|
|
483
517
|
const headers = headersOutcome({
|
|
484
|
-
rules: mergeRules(
|
|
485
|
-
.filter((entry) => entry.project.consumer.headers === "netlify")
|
|
486
|
-
.flatMap((entry) => [entry.siteRoot, ...olderLines(entry.siteRoot).map((line) => line.siteRoot)].map((siteRoot) => readHeaders(siteRoot) ?? []))),
|
|
518
|
+
rules: mergeRules(hostRoots.map((siteRoot) => readHostHeaders(siteRoot) ?? [])),
|
|
487
519
|
base,
|
|
488
520
|
own: existsSync(ownHeaders) ? readFileSync(ownHeaders, "utf8") : undefined,
|
|
489
521
|
});
|
|
@@ -495,6 +527,21 @@ export default function ascribe(options) {
|
|
|
495
527
|
await writeFile(path.join(out, "_headers"), headers.text);
|
|
496
528
|
logger.info("wrote /_headers");
|
|
497
529
|
}
|
|
530
|
+
const ownRedirects = path.join(publicDir, "_redirects");
|
|
531
|
+
const redirects = redirectsOutcome({
|
|
532
|
+
redirects: mergeRedirects(hostRoots.map(readHostRedirects)),
|
|
533
|
+
base,
|
|
534
|
+
published: (route) => builtKeys.has(routeKey(route)),
|
|
535
|
+
own: existsSync(ownRedirects) ? readFileSync(ownRedirects, "utf8") : undefined,
|
|
536
|
+
});
|
|
537
|
+
if (redirects.kind === "fail")
|
|
538
|
+
throw new Error(`@ascribed/astro: ${redirects.text}`);
|
|
539
|
+
if (redirects.kind === "note")
|
|
540
|
+
logger.info(redirects.text);
|
|
541
|
+
if (redirects.kind === "write") {
|
|
542
|
+
await writeFile(path.join(out, "_redirects"), redirects.text);
|
|
543
|
+
logger.info("wrote /_redirects");
|
|
544
|
+
}
|
|
498
545
|
// Entries of one project publish at one base path: the first entry's copies go last, so they win.
|
|
499
546
|
for (const entry of [...entries].reverse()) {
|
|
500
547
|
const folder = folderUnder(entry.project.consumer.basePath, base);
|