@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
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import type { AstroIntegration } from "astro";
|
|
2
|
+
import { type ProjectEntry } from "./projects.js";
|
|
2
3
|
export { default as rehypeAscribeAttributes } from "./rehype.js";
|
|
3
4
|
export { satteriAscribeAttributes } from "./satteri.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
build: string;
|
|
8
|
-
/** The directory holding `ascribe.toml`, relative to the Astro root. Default: the root. */
|
|
9
|
-
project?: string;
|
|
5
|
+
export type { ProjectEntry } from "./projects.js";
|
|
6
|
+
/** Options every form of the integration takes. */
|
|
7
|
+
interface SharedOptions {
|
|
10
8
|
/**
|
|
11
9
|
* The `ascribe` binary, relative to the Astro root. Default: `ASCRIBE_BIN`, then
|
|
12
10
|
* the platform binary installed with `@ascribed/cli`.
|
|
@@ -22,15 +20,48 @@ export interface AscribeOptions {
|
|
|
22
20
|
* Review in the site preview: an **Ascribe review** app in `astro dev`'s
|
|
23
21
|
* toolbar, which marks the page's changes and shows the pull request's
|
|
24
22
|
* comments, with source anchors in `astro dev`. Default `true`; `false`
|
|
25
|
-
* removes the app. `astro build` never has it.
|
|
23
|
+
* removes the app. `astro build` never has it. With several projects, it
|
|
24
|
+
* reviews one: the entry a name names, or the first for `true`.
|
|
26
25
|
*/
|
|
27
|
-
review?: boolean;
|
|
26
|
+
review?: boolean | string;
|
|
28
27
|
/**
|
|
29
28
|
* Show a code block's title (`title="…"` in its info string, as `@snippet`
|
|
30
29
|
* writes it) above it, with a Shiki transformer that puts the block in a
|
|
31
30
|
* `<figure class="code-title">` with a `<figcaption>`. Default `true`.
|
|
32
31
|
*/
|
|
33
32
|
codeTitles?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The site's one sitemap, at `<base>sitemap.xml`, of every listed page of
|
|
35
|
+
* every project and every page of the site's own. `exclude` lists routes,
|
|
36
|
+
* from the host's root, of the site's own pages to leave out of it, such
|
|
37
|
+
* as one that carries `noindex`.
|
|
38
|
+
*/
|
|
39
|
+
sitemap?: {
|
|
40
|
+
exclude?: string[];
|
|
41
|
+
};
|
|
34
42
|
}
|
|
35
|
-
/**
|
|
43
|
+
/** Options of the integration with one project. */
|
|
44
|
+
export interface OneProjectOptions extends SharedOptions {
|
|
45
|
+
/** The build whose site output is the collection (a name in `ascribe.toml`). */
|
|
46
|
+
build: string;
|
|
47
|
+
/** The directory holding `ascribe.toml`, relative to the Astro root. Default: the root. */
|
|
48
|
+
project?: string;
|
|
49
|
+
projects?: never;
|
|
50
|
+
}
|
|
51
|
+
/** Options of the integration with several projects, each entry named. */
|
|
52
|
+
export interface ProjectsOptions extends SharedOptions {
|
|
53
|
+
/** The projects, each a named build of a project; two entries may share a project. */
|
|
54
|
+
projects: ProjectEntry[];
|
|
55
|
+
build?: never;
|
|
56
|
+
project?: never;
|
|
57
|
+
}
|
|
58
|
+
/** Options of the integration. */
|
|
59
|
+
export type AscribeOptions = OneProjectOptions | ProjectsOptions;
|
|
60
|
+
/**
|
|
61
|
+
* Runs `ascribe build` for each project, checks the site's routing, adds the markdown plugin, adds a
|
|
62
|
+
* page for each permalink's address, and serves published files, and, with `[consumer] agents = true`,
|
|
63
|
+
* `llms.txt` and each page's Markdown, each at its project's base path; and writes the site's one
|
|
64
|
+
* sitemap and `robots.txt` from every project's page records, and, with `[consumer] headers =
|
|
65
|
+
* "netlify"`, its `_headers` from their headers lists.
|
|
66
|
+
*/
|
|
36
67
|
export default function ascribe(options: AscribeOptions): AstroIntegration;
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// // astro.config.mjs
|
|
5
5
|
// import ascribe from "@ascribed/astro";
|
|
6
6
|
// export default defineConfig({
|
|
7
|
-
// base
|
|
7
|
+
// // `site`, `base`, and `trailingSlash` come from ascribe.toml's
|
|
8
|
+
// // [consumer] when they're left out here; set here, they must agree.
|
|
8
9
|
// integrations: [ascribe({ build: "site" })],
|
|
9
10
|
// });
|
|
10
11
|
//
|
|
@@ -19,56 +20,161 @@
|
|
|
19
20
|
// // a layout
|
|
20
21
|
// import Elements from "@ascribed/astro/Elements.astro"; // <Elements /> in <head>
|
|
21
22
|
// import Availability from "@ascribed/astro/Availability.astro"; // the page's availability
|
|
23
|
+
// import Head from "@ascribed/astro/Head.astro"; // <Head entry={entry} /> in <head>
|
|
24
|
+
//
|
|
25
|
+
// // the sidebar: src/nav.ts's list, checked against the collection
|
|
26
|
+
// import { sidebar } from "@ascribed/astro/content";
|
|
27
|
+
// const groups = sidebar(nav, await getCollection("docs"));
|
|
28
|
+
//
|
|
29
|
+
// // src/pages/404.astro
|
|
30
|
+
// import MissingPermalink from "@ascribed/astro/MissingPermalink.astro"; // a permalink that isn't here
|
|
31
|
+
//
|
|
32
|
+
// Several projects in one site, each entry named, and each collection naming its entry:
|
|
33
|
+
//
|
|
34
|
+
// ascribe({
|
|
35
|
+
// projects: [
|
|
36
|
+
// { name: "docs", project: "../docs", build: "site" },
|
|
37
|
+
// { name: "home", project: "home", build: "site" },
|
|
38
|
+
// ],
|
|
39
|
+
// review: "docs",
|
|
40
|
+
// })
|
|
41
|
+
// ascribeCollection({ schema, name: "docs" })
|
|
42
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
43
|
+
import { writeFile } from "node:fs/promises";
|
|
22
44
|
import path from "node:path";
|
|
23
45
|
import { fileURLToPath } from "node:url";
|
|
24
46
|
import { findBinary } from "./binary.js";
|
|
25
47
|
import { codeTitles } from "./code-titles.js";
|
|
26
48
|
import { SeenFiles, watchDev } from "./dev.js";
|
|
27
|
-
import { copyPublishedFiles, filesMiddleware } from "./files.js";
|
|
28
|
-
import {
|
|
49
|
+
import { agentsMiddleware, copyAgentFiles, copyPublishedFiles, filesMiddleware, folderUnder, } from "./files.js";
|
|
50
|
+
import { WARN as HEAD_WARN } from "./head-check.js";
|
|
51
|
+
import { headersFor, headersOutcome, mergeRules, readHeaders } from "./headers.js";
|
|
52
|
+
import { forwardingRoutes, hasPermalinks, permalinkPattern, readPermalinks } from "./permalinks.js";
|
|
53
|
+
import { consumerMismatches, homeRoute, normalizeBase, readProject, supplyRouting, } from "./project.js";
|
|
54
|
+
import { basePathClashes, nameProblems, pagesUnderOtherPaths, permalinkProblems, } from "./projects.js";
|
|
55
|
+
import { pageList, readRecords, routeAs, routeKey, sitemapRoute, } from "./records.js";
|
|
29
56
|
import rehypeAscribeAttributes from "./rehype.js";
|
|
30
57
|
import { removeDevFile, siteUrl, writeDevFile } from "./review/devfile.js";
|
|
31
|
-
import { runDiff } from "./review/diff.js";
|
|
58
|
+
import { runDiff, runPrompt } from "./review/diff.js";
|
|
32
59
|
import { APP_ID } from "./review/protocol.js";
|
|
33
60
|
import { readRoutes } from "./review/routes.js";
|
|
34
61
|
import { channelProblem, ReviewServer } from "./review/server.js";
|
|
62
|
+
import { devRouted, devSitePages, rootFiles, rootFilesMiddleware, } from "./root-files.js";
|
|
35
63
|
import { anchorsFor, hasWarnings, reviewFor, runBuild } from "./run.js";
|
|
36
64
|
import { satteriAscribeAttributes } from "./satteri.js";
|
|
37
65
|
import { copySchema } from "./schema.js";
|
|
38
66
|
export { default as rehypeAscribeAttributes } from "./rehype.js";
|
|
39
67
|
export { satteriAscribeAttributes } from "./satteri.js";
|
|
40
|
-
/** The virtual module `content.ts` reads the resolved site output
|
|
68
|
+
/** The virtual module `content.ts` reads the resolved site output roots from. */
|
|
41
69
|
const SITE_MODULE = "virtual:ascribe/site";
|
|
42
70
|
/** The toolbar app's icon: a speech bubble over a page. */
|
|
43
71
|
const ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h8l4 4v6"/><path d="M14 3v4h4"/><path d="M6 3v15"/><path d="M11 14h10v6h-6l-3 2v-2h-1z"/></svg>';
|
|
44
|
-
|
|
72
|
+
function fail(problems, what) {
|
|
73
|
+
if (problems.length > 0)
|
|
74
|
+
throw new Error(`@ascribed/astro: ${what}:\n- ${problems.join("\n- ")}`);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Runs `ascribe build` for each project, checks the site's routing, adds the markdown plugin, adds a
|
|
78
|
+
* page for each permalink's address, and serves published files, and, with `[consumer] agents = true`,
|
|
79
|
+
* `llms.txt` and each page's Markdown, each at its project's base path; and writes the site's one
|
|
80
|
+
* sitemap and `robots.txt` from every project's page records, and, with `[consumer] headers =
|
|
81
|
+
* "netlify"`, its `_headers` from their headers lists.
|
|
82
|
+
*/
|
|
45
83
|
export default function ascribe(options) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let
|
|
49
|
-
let
|
|
84
|
+
/** With `projects`; otherwise the one entry has no name a collection gives. */
|
|
85
|
+
const named = options.projects !== undefined;
|
|
86
|
+
let entries = [];
|
|
87
|
+
let groups = [];
|
|
88
|
+
let routing;
|
|
50
89
|
/** The dev server's sources as its first build read them. */
|
|
51
90
|
const seen = new SeenFiles();
|
|
52
91
|
let binary;
|
|
53
|
-
let
|
|
92
|
+
let reviewed;
|
|
93
|
+
let devCommand = false;
|
|
54
94
|
let devHttps = false;
|
|
55
95
|
let review;
|
|
96
|
+
/** The site's routes, as Astro resolved them, for what `astro dev` serves of the site's own. */
|
|
97
|
+
let astroRoutes = [];
|
|
98
|
+
/** The site's `public/` folder, where its own `robots.txt` would be. */
|
|
99
|
+
let publicDir = "";
|
|
100
|
+
/** What the sitemap and `robots.txt` are drawn from, given the site's pages and its routes. */
|
|
101
|
+
const rootSources = (sitePages, routed) => {
|
|
102
|
+
const base = routing?.base ?? "/";
|
|
103
|
+
const records = entries.map((entry) => readRecords(entry.siteRoot));
|
|
104
|
+
// Entries of one project share its `ascribe.toml`, and so what it says to crawlers.
|
|
105
|
+
const robots = new Map();
|
|
106
|
+
for (const [i, entry] of entries.entries()) {
|
|
107
|
+
if (!robots.has(entry.project.dir))
|
|
108
|
+
robots.set(entry.project.dir, {
|
|
109
|
+
name: entry.name,
|
|
110
|
+
robots: records[i]?.robots ?? { lines: [] },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const root = normalizeBase(base);
|
|
114
|
+
return {
|
|
115
|
+
projects: records.map(pageList),
|
|
116
|
+
routed,
|
|
117
|
+
sitePages,
|
|
118
|
+
excluded: [
|
|
119
|
+
...forwardingRoutes(entries.map((entry) => readPermalinks(entry.siteRoot))),
|
|
120
|
+
`${root}404`,
|
|
121
|
+
`${root}500`,
|
|
122
|
+
...(options.sitemap?.exclude ?? []),
|
|
123
|
+
],
|
|
124
|
+
robots: [...robots.values()],
|
|
125
|
+
site: routing?.site,
|
|
126
|
+
base,
|
|
127
|
+
ownRobots: existsSync(path.join(publicDir, "robots.txt")),
|
|
128
|
+
ownSitemap: existsSync(path.join(publicDir, "sitemap.xml")),
|
|
129
|
+
};
|
|
130
|
+
};
|
|
56
131
|
return {
|
|
57
132
|
name: "@ascribed/astro",
|
|
58
133
|
hooks: {
|
|
59
|
-
"astro:config:setup": async ({ config, command, logger, updateConfig, addDevToolbarApp, createCodegenDir, }) => {
|
|
134
|
+
"astro:config:setup": async ({ config, command, logger, updateConfig, addDevToolbarApp, createCodegenDir, injectRoute, addMiddleware, }) => {
|
|
60
135
|
const root = fileURLToPath(config.root);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
136
|
+
publicDir = fileURLToPath(config.publicDir);
|
|
137
|
+
const list = options.projects ?? [
|
|
138
|
+
{
|
|
139
|
+
name: "",
|
|
140
|
+
build: options.build,
|
|
141
|
+
...(options.project === undefined ? {} : { project: options.project }),
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
if (named)
|
|
145
|
+
fail(nameProblems(list), "the `projects` option is wrong");
|
|
146
|
+
const codegen = command === "preview" ? "" : fileURLToPath(createCodegenDir());
|
|
147
|
+
entries = list.map((entry) => {
|
|
148
|
+
const project = readProject(path.resolve(root, entry.project ?? "."));
|
|
149
|
+
return {
|
|
150
|
+
name: entry.name,
|
|
151
|
+
build: entry.build,
|
|
152
|
+
project,
|
|
153
|
+
// An unknown build is `ascribe build`'s to report: it knows the implicit `site` build.
|
|
154
|
+
siteRoot: project.siteRoot(entry.build),
|
|
155
|
+
agentsRoot: project.consumer.agents ? project.plainRoot(entry.build) : undefined,
|
|
156
|
+
schemaDir: named ? path.join(codegen, entry.name) : codegen,
|
|
157
|
+
};
|
|
69
158
|
});
|
|
70
|
-
|
|
71
|
-
|
|
159
|
+
// What astro.config leaves unset comes from [consumer]: `site` and `trailingSlash` from the
|
|
160
|
+
// first project that has them, and with one project, `base` from its base path. With
|
|
161
|
+
// `projects`, `base` stays the site's, the root every base path is under.
|
|
162
|
+
const { routing: astro, supplied } = supplyRouting(entries.map((entry) => entry.project), { base: config.base, trailingSlash: config.trailingSlash, site: config.site }, !named);
|
|
163
|
+
// `ascribe.toml`'s routing must be Astro's, or every link Ascribe writes is wrong.
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
fail(consumerMismatches(entry.project, astro), named
|
|
166
|
+
? `"${entry.name}"'s ascribe.toml and astro.config disagree`
|
|
167
|
+
: "ascribe.toml and astro.config disagree");
|
|
168
|
+
}
|
|
169
|
+
fail(basePathClashes(entries), "two projects share a base path");
|
|
170
|
+
if (typeof options.review === "string" && !entries.some((e) => e.name === options.review)) {
|
|
171
|
+
throw new Error(`@ascribed/astro: \`review\` names "${options.review}", and no entry of \`projects\` has that name.`);
|
|
172
|
+
}
|
|
173
|
+
if (reviewFor(options.review, command)) {
|
|
174
|
+
reviewed =
|
|
175
|
+
typeof options.review === "string"
|
|
176
|
+
? entries.find((e) => e.name === options.review)
|
|
177
|
+
: entries[0];
|
|
72
178
|
}
|
|
73
179
|
// Where a markdown plugin goes depends on the processor Astro 7.3 runs.
|
|
74
180
|
const processor = config.markdown.processor;
|
|
@@ -83,13 +189,8 @@ export default function ascribe(options) {
|
|
|
83
189
|
throw new Error(`@ascribed/astro: the markdown processor "${processor.name}" is not one it can add its plugin to. ` +
|
|
84
190
|
"Use Astro's default (satteri) or unified(), or add `rehypeAscribeAttributes` to your own.");
|
|
85
191
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
trailingSlash: config.trailingSlash,
|
|
89
|
-
site: config.site,
|
|
90
|
-
};
|
|
91
|
-
reviewing = reviewFor(options.review, command);
|
|
92
|
-
if (reviewing) {
|
|
192
|
+
routing = astro;
|
|
193
|
+
if (reviewed !== undefined) {
|
|
93
194
|
addDevToolbarApp({
|
|
94
195
|
id: APP_ID,
|
|
95
196
|
name: "Ascribe review",
|
|
@@ -100,18 +201,21 @@ export default function ascribe(options) {
|
|
|
100
201
|
if (command !== "preview") {
|
|
101
202
|
const found = findBinary({ binary: options.binary, root });
|
|
102
203
|
binary = found;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
rebuild = async () => {
|
|
106
|
-
logger.info(`running ${path.basename(found)} build for "${options.build}"`);
|
|
204
|
+
const buildEntry = async (entry) => {
|
|
205
|
+
logger.info(`running ${path.basename(found)} build for "${entry.build}"${named ? ` (${entry.name})` : ""}`);
|
|
107
206
|
const result = await runBuild({
|
|
108
207
|
binary: found,
|
|
109
|
-
configPath: project.configPath,
|
|
110
|
-
build:
|
|
111
|
-
cwd: project.dir,
|
|
112
|
-
anchors: anchorsFor(options.anchors, command,
|
|
113
|
-
|
|
114
|
-
|
|
208
|
+
configPath: entry.project.configPath,
|
|
209
|
+
build: entry.build,
|
|
210
|
+
cwd: entry.project.dir,
|
|
211
|
+
anchors: anchorsFor(options.anchors, command, entry === reviewed),
|
|
212
|
+
outputs: [
|
|
213
|
+
"site",
|
|
214
|
+
// `llms.txt` and each page's Markdown, which agents read.
|
|
215
|
+
...(entry.agentsRoot === undefined ? [] : ["plain"]),
|
|
216
|
+
// While review is on, the JSON output says which page is at each route.
|
|
217
|
+
...(entry === reviewed && review?.active ? ["json"] : []),
|
|
218
|
+
],
|
|
115
219
|
});
|
|
116
220
|
if (hasWarnings(result.diagnostics))
|
|
117
221
|
logger.warn(result.diagnostics);
|
|
@@ -119,17 +223,69 @@ export default function ascribe(options) {
|
|
|
119
223
|
logger.info(result.diagnostics);
|
|
120
224
|
if (result.summary !== "")
|
|
121
225
|
logger.info(result.summary);
|
|
122
|
-
|
|
226
|
+
// The schema's copy inside the Astro root, for content.config.ts to import.
|
|
227
|
+
mkdirSync(entry.schemaDir, { recursive: true });
|
|
228
|
+
copySchema(entry.siteRoot, entry.schemaDir);
|
|
229
|
+
};
|
|
230
|
+
// What the projects share, which no one project's build can see.
|
|
231
|
+
const checkShared = () => {
|
|
232
|
+
if (entries.length < 2)
|
|
233
|
+
return;
|
|
234
|
+
const current = entries.map((entry) => ({
|
|
235
|
+
...entry,
|
|
236
|
+
project: readProject(entry.project.dir),
|
|
237
|
+
}));
|
|
238
|
+
fail(basePathClashes(current), "two projects share a base path");
|
|
239
|
+
fail(pagesUnderOtherPaths(current), "a page is under another project's base path");
|
|
240
|
+
fail(permalinkProblems(current, current.map((entry) => readPermalinks(entry.siteRoot))), "the projects' permalinks can't share one route");
|
|
123
241
|
};
|
|
242
|
+
// One group for each project: a change rebuilds the project it's in.
|
|
243
|
+
const byDir = new Map();
|
|
244
|
+
for (const entry of entries) {
|
|
245
|
+
byDir.set(entry.project.dir, [...(byDir.get(entry.project.dir) ?? []), entry]);
|
|
246
|
+
}
|
|
247
|
+
groups = [...byDir.values()].map((group) => ({
|
|
248
|
+
project: group[0].project,
|
|
249
|
+
entries: group,
|
|
250
|
+
rebuild: async () => {
|
|
251
|
+
for (const entry of group)
|
|
252
|
+
await buildEntry(entry);
|
|
253
|
+
checkShared();
|
|
254
|
+
},
|
|
255
|
+
}));
|
|
124
256
|
// The sources as the first build reads them: the watcher may report their edits late.
|
|
125
|
-
if (command === "dev")
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
257
|
+
if (command === "dev") {
|
|
258
|
+
for (const group of groups)
|
|
259
|
+
seen.prime([group.project.contentRoot, group.project.configPath]);
|
|
260
|
+
}
|
|
261
|
+
for (const entry of entries)
|
|
262
|
+
await buildEntry(entry);
|
|
263
|
+
checkShared();
|
|
264
|
+
devCommand = command === "dev";
|
|
265
|
+
if (devCommand) {
|
|
266
|
+
// A tag of the head written twice, by Head.astro and the layout, is warned about.
|
|
267
|
+
globalThis[HEAD_WARN] = (message) => logger.warn(message);
|
|
268
|
+
addMiddleware({
|
|
269
|
+
entrypoint: new URL("./head-check.js", import.meta.url),
|
|
270
|
+
order: "post",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
// Each permalink's address, a page that forwards to its heading. One route serves every
|
|
274
|
+
// list, at the path of the first that names anything.
|
|
275
|
+
const lists = entries.flatMap((entry) => readPermalinks(entry.siteRoot) ?? []);
|
|
276
|
+
const served = lists.find(hasPermalinks) ?? lists[0];
|
|
277
|
+
if (served !== undefined) {
|
|
278
|
+
injectRoute({
|
|
279
|
+
pattern: permalinkPattern(served.path, astro.base),
|
|
280
|
+
entrypoint: new URL("../src/Permalink.astro", import.meta.url),
|
|
281
|
+
prerender: true,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
130
284
|
}
|
|
131
|
-
|
|
285
|
+
const projectHome = (entry) => homeRoute(entry.project.consumer.basePath, astro.trailingSlash);
|
|
286
|
+
// Dev serving of `_ascribe/files/`, and of the plain output for agents; the build copies them in `astro:build:done`.
|
|
132
287
|
updateConfig({
|
|
288
|
+
...supplied,
|
|
133
289
|
...(options.codeTitles === false
|
|
134
290
|
? {}
|
|
135
291
|
: { markdown: { shikiConfig: { transformers: [codeTitles] } } }),
|
|
@@ -141,83 +297,162 @@ export default function ascribe(options) {
|
|
|
141
297
|
name: "@ascribed/astro:site",
|
|
142
298
|
resolveId: (id) => (id === SITE_MODULE ? `\0${SITE_MODULE}` : undefined),
|
|
143
299
|
load: (id) => id === `\0${SITE_MODULE}`
|
|
144
|
-
? `export const siteRoot = ${JSON.stringify(siteRoot)}
|
|
300
|
+
? `export const siteRoot = ${JSON.stringify(entries[0]?.siteRoot ?? "")};\n` +
|
|
301
|
+
`export const astroRoot = ${JSON.stringify(root)};\n` +
|
|
302
|
+
`export const home = ${JSON.stringify(homeRoute(astro.base, astro.trailingSlash))};\n` +
|
|
303
|
+
`export const collections = ${JSON.stringify(named ? Object.fromEntries(entries.map((e) => [e.name, e.siteRoot])) : {})};\n` +
|
|
304
|
+
`export const permalinks = ${JSON.stringify(entries.map((e) => ({ siteRoot: e.siteRoot, home: projectHome(e) })))};\n` +
|
|
305
|
+
`export const routes = ${JSON.stringify(entries.map((e) => ({
|
|
306
|
+
siteRoot: e.siteRoot,
|
|
307
|
+
basePath: e.project.consumer.basePath,
|
|
308
|
+
trailingSlash: e.project.consumer.trailingSlash,
|
|
309
|
+
})))};`
|
|
145
310
|
: undefined,
|
|
146
311
|
},
|
|
147
312
|
{
|
|
148
313
|
name: "@ascribed/astro:files",
|
|
149
314
|
configureServer(server) {
|
|
150
|
-
|
|
315
|
+
for (const entry of entries) {
|
|
316
|
+
const base = entry.project.consumer.basePath;
|
|
317
|
+
server.middlewares.use(filesMiddleware(entry.siteRoot, base));
|
|
318
|
+
if (entry.agentsRoot !== undefined) {
|
|
319
|
+
const siteRoot = entry.siteRoot;
|
|
320
|
+
server.middlewares.use(agentsMiddleware(entry.agentsRoot, base, (pathname) => headersFor(siteRoot, pathname)));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// The sitemap and robots.txt, as the build would write them now.
|
|
324
|
+
server.middlewares.use(rootFilesMiddleware(astro.base, () => rootFiles(rootSources(devSitePages(astroRoutes, astro), devRouted(astroRoutes, astro)))));
|
|
151
325
|
},
|
|
152
326
|
},
|
|
153
327
|
],
|
|
154
328
|
},
|
|
155
329
|
});
|
|
156
330
|
},
|
|
331
|
+
"astro:routes:resolved": ({ routes }) => {
|
|
332
|
+
astroRoutes = routes;
|
|
333
|
+
},
|
|
157
334
|
"astro:server:setup": ({ server, refreshContent, logger, toolbar }) => {
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
335
|
+
if (!devCommand || !routing)
|
|
336
|
+
return;
|
|
337
|
+
if (!refreshContent)
|
|
338
|
+
throw new Error("@ascribed/astro: this Astro version does not support refreshContent.");
|
|
339
|
+
devHttps = Boolean(server.config.server.https);
|
|
340
|
+
for (const group of groups) {
|
|
341
|
+
group.builds = watchDev({
|
|
164
342
|
server,
|
|
165
343
|
seen,
|
|
166
344
|
refreshContent: () => refreshContent({}),
|
|
167
345
|
logger,
|
|
168
|
-
project,
|
|
169
|
-
astro:
|
|
170
|
-
|
|
171
|
-
rebuild,
|
|
172
|
-
onBuilt: () =>
|
|
346
|
+
project: group.project,
|
|
347
|
+
astro: routing,
|
|
348
|
+
builds: group.entries.map((entry) => entry.build),
|
|
349
|
+
rebuild: group.rebuild,
|
|
350
|
+
onBuilt: () => {
|
|
351
|
+
if (reviewed !== undefined && group.entries.includes(reviewed))
|
|
352
|
+
review?.rebuilt();
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const group = groups.find((g) => reviewed !== undefined && g.entries.includes(reviewed));
|
|
357
|
+
if (reviewed !== undefined && group?.builds !== undefined && binary !== undefined) {
|
|
358
|
+
const entry = reviewed;
|
|
359
|
+
const builds = group.builds;
|
|
360
|
+
const run = { binary, configPath: entry.project.configPath, cwd: entry.project.dir };
|
|
361
|
+
const jsonRoot = path.join(path.dirname(entry.siteRoot), "json");
|
|
362
|
+
review = new ReviewServer({
|
|
363
|
+
channel: toolbar,
|
|
364
|
+
logger,
|
|
365
|
+
build: entry.build,
|
|
366
|
+
contentRoot: entry.project.contentRoot,
|
|
367
|
+
channelProblem: channelProblem(server.config),
|
|
368
|
+
diff: (base) => runDiff({ ...run, build: entry.build, base }),
|
|
369
|
+
prompt: (page, base) => runPrompt({ ...run, build: entry.build, base, path: page }),
|
|
370
|
+
projectDir: entry.project.dir,
|
|
371
|
+
connect: async () => (await import("./review/github.js")).connect(entry.project.dir),
|
|
372
|
+
writeRoutes: () => builds.inTurn(async () => {
|
|
373
|
+
await runBuild({ ...run, build: entry.build, outputs: ["json"] });
|
|
374
|
+
}),
|
|
375
|
+
readRoutes: () => readRoutes(jsonRoot),
|
|
173
376
|
});
|
|
174
|
-
if (reviewing && binary !== undefined) {
|
|
175
|
-
const found = binary;
|
|
176
|
-
const run = { binary: found, configPath: project.configPath, cwd: project.dir };
|
|
177
|
-
const jsonRoot = path.join(path.dirname(siteRoot), "json");
|
|
178
|
-
review = new ReviewServer({
|
|
179
|
-
channel: toolbar,
|
|
180
|
-
logger,
|
|
181
|
-
build: options.build,
|
|
182
|
-
contentRoot: project.contentRoot,
|
|
183
|
-
channelProblem: channelProblem(server.config),
|
|
184
|
-
diff: (base) => runDiff({ ...run, build: options.build, base }),
|
|
185
|
-
connect: async () => (await import("./review/github.js")).connect(project.dir),
|
|
186
|
-
writeRoutes: () => builds.inTurn(async () => {
|
|
187
|
-
await runBuild({ ...run, build: options.build, outputs: ["json"] });
|
|
188
|
-
}),
|
|
189
|
-
readRoutes: () => readRoutes(jsonRoot),
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
377
|
}
|
|
193
378
|
},
|
|
194
379
|
"astro:server:start": ({ address, logger }) => {
|
|
195
380
|
review?.listening(address.address);
|
|
196
|
-
if (!
|
|
381
|
+
if (!devCommand)
|
|
197
382
|
return;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
383
|
+
// Each project's folder gets its own file, with its own address: its base path.
|
|
384
|
+
for (const group of groups) {
|
|
385
|
+
const entry = group.entries[0];
|
|
386
|
+
const dir = group.project.dir;
|
|
387
|
+
try {
|
|
388
|
+
writeDevFile(dir, {
|
|
389
|
+
url: siteUrl(address, { https: devHttps, base: entry.project.consumer.basePath }),
|
|
390
|
+
build: entry.build,
|
|
391
|
+
pid: process.pid,
|
|
392
|
+
});
|
|
393
|
+
// Vite exits on a signal before `astro:server:done` runs.
|
|
394
|
+
process.once("exit", () => removeDevFile(dir, process.pid));
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
logger.warn(`couldn't write ${groups.length > 1 ? path.join(dir, ".ascribe", "dev.json") : ".ascribe/dev.json"}, which the editor's Open Site Preview reads: ${error instanceof Error ? error.message : String(error)}`);
|
|
398
|
+
}
|
|
210
399
|
}
|
|
211
400
|
},
|
|
212
401
|
"astro:server:done": () => {
|
|
213
402
|
review?.dispose();
|
|
214
403
|
review = undefined;
|
|
215
|
-
if (
|
|
216
|
-
|
|
404
|
+
if (devCommand)
|
|
405
|
+
for (const group of groups)
|
|
406
|
+
removeDevFile(group.project.dir, process.pid);
|
|
217
407
|
},
|
|
218
|
-
"astro:build:done": async ({ dir, logger }) => {
|
|
219
|
-
|
|
220
|
-
|
|
408
|
+
"astro:build:done": async ({ dir, pages, logger }) => {
|
|
409
|
+
const base = routing?.base ?? "/";
|
|
410
|
+
const trailingSlash = routing?.trailingSlash ?? "ignore";
|
|
411
|
+
// Every page Astro built, by its route from the host's root. Astro lists
|
|
412
|
+
// pages only, not endpoints such as a feed, so a dot in a page's last
|
|
413
|
+
// segment, as in `/v1.2`, is still a page.
|
|
414
|
+
const built = pages.map((page) => routeAs(`${normalizeBase(base)}${page.pathname.replace(/^\//, "")}`, trailingSlash));
|
|
415
|
+
const builtKeys = new Set(built.map(routeKey));
|
|
416
|
+
const files = rootFiles(rootSources(built, (route) => builtKeys.has(routeKey(route))));
|
|
417
|
+
fail(files.problems, "the site's robots.txt");
|
|
418
|
+
for (const note of files.notes)
|
|
419
|
+
logger[note.level](note.text);
|
|
420
|
+
const out = fileURLToPath(dir);
|
|
421
|
+
if (files.sitemap !== undefined) {
|
|
422
|
+
await writeFile(path.join(out, "sitemap.xml"), files.sitemap);
|
|
423
|
+
logger.info(`wrote ${sitemapRoute(base)}`);
|
|
424
|
+
}
|
|
425
|
+
if (files.robots !== undefined) {
|
|
426
|
+
await writeFile(path.join(out, "robots.txt"), files.robots);
|
|
427
|
+
logger.info("wrote /robots.txt");
|
|
428
|
+
}
|
|
429
|
+
// The host's headers, from the projects that name its format.
|
|
430
|
+
const ownHeaders = path.join(publicDir, "_headers");
|
|
431
|
+
const headers = headersOutcome({
|
|
432
|
+
rules: mergeRules(entries
|
|
433
|
+
.filter((entry) => entry.project.consumer.headers === "netlify")
|
|
434
|
+
.map((entry) => readHeaders(entry.siteRoot) ?? [])),
|
|
435
|
+
base,
|
|
436
|
+
own: existsSync(ownHeaders) ? readFileSync(ownHeaders, "utf8") : undefined,
|
|
437
|
+
});
|
|
438
|
+
if (headers.kind === "fail")
|
|
439
|
+
throw new Error(`@ascribed/astro: ${headers.text}`);
|
|
440
|
+
if (headers.kind === "note")
|
|
441
|
+
logger.info(headers.text);
|
|
442
|
+
if (headers.kind === "write") {
|
|
443
|
+
await writeFile(path.join(out, "_headers"), headers.text);
|
|
444
|
+
logger.info("wrote /_headers");
|
|
445
|
+
}
|
|
446
|
+
// Entries of one project publish at one base path: the first entry's copies go last, so they win.
|
|
447
|
+
for (const entry of [...entries].reverse()) {
|
|
448
|
+
const folder = folderUnder(entry.project.consumer.basePath, base);
|
|
449
|
+
const where = folder === "" ? "" : ` to ${folder}`;
|
|
450
|
+
if (await copyPublishedFiles(entry.siteRoot, dir, folder))
|
|
451
|
+
logger.info(`copied _ascribe/files/${where}`);
|
|
452
|
+
if (entry.agentsRoot !== undefined &&
|
|
453
|
+
(await copyAgentFiles(entry.agentsRoot, dir, folder)))
|
|
454
|
+
logger.info(`copied llms.txt and each page's Markdown${where}`);
|
|
455
|
+
}
|
|
221
456
|
},
|
|
222
457
|
},
|
|
223
458
|
};
|