@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/review/routes.js
CHANGED
|
@@ -15,8 +15,14 @@ export function normalizeRoute(route) {
|
|
|
15
15
|
const trimmed = (decoded.split(/[?#]/)[0] ?? "").replace(/\/+$/, "");
|
|
16
16
|
return trimmed === "" ? "/" : trimmed;
|
|
17
17
|
}
|
|
18
|
-
/** How much of a page's JSON to read
|
|
19
|
-
const
|
|
18
|
+
/** How much of a page's JSON to read at a time. */
|
|
19
|
+
const CHUNK_BYTES = 4096;
|
|
20
|
+
/**
|
|
21
|
+
* Where the page's own keys end in the JSON output, which writes it
|
|
22
|
+
* pretty-printed: `availability` comes after `path`, `route`, `title`,
|
|
23
|
+
* `frontmatter`, and `formatted`, and before the page's blocks.
|
|
24
|
+
*/
|
|
25
|
+
const AFTER_HEAD = Buffer.from('\n "availability":');
|
|
20
26
|
/**
|
|
21
27
|
* Every page of the build's JSON output (`<output-dir>/<build>/json/`), by
|
|
22
28
|
* normalized route. Empty when there's no JSON output.
|
|
@@ -35,20 +41,43 @@ export async function readRoutes(jsonRoot) {
|
|
|
35
41
|
if (entry.kind !== "page" || typeof entry.path !== "string")
|
|
36
42
|
return;
|
|
37
43
|
const page = await readHead(path.join(jsonRoot, ...entry.path.split("/")));
|
|
38
|
-
if (page)
|
|
39
|
-
|
|
44
|
+
if (page) {
|
|
45
|
+
const { route, ...routed } = page;
|
|
46
|
+
routes.set(normalizeRoute(route), routed);
|
|
47
|
+
}
|
|
40
48
|
}));
|
|
41
49
|
return routes;
|
|
42
50
|
}
|
|
43
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* A page's `path`, `route`, `title`, and formatted title, from the start of
|
|
53
|
+
* its JSON up to `availability`, or the whole of it if need be.
|
|
54
|
+
*/
|
|
44
55
|
async function readHead(file) {
|
|
45
|
-
let
|
|
56
|
+
let text;
|
|
46
57
|
try {
|
|
47
58
|
const handle = await open(file, "r");
|
|
48
59
|
try {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let read = 0;
|
|
62
|
+
let end = -1;
|
|
63
|
+
for (;;) {
|
|
64
|
+
const buffer = Buffer.alloc(CHUNK_BYTES);
|
|
65
|
+
const { bytesRead } = await handle.read(buffer, 0, CHUNK_BYTES, read);
|
|
66
|
+
if (bytesRead === 0)
|
|
67
|
+
break;
|
|
68
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
69
|
+
read += bytesRead;
|
|
70
|
+
// The marker may straddle two chunks, so look from just before this one.
|
|
71
|
+
end = Buffer.concat(chunks).indexOf(AFTER_HEAD, Math.max(0, read - bytesRead - AFTER_HEAD.length));
|
|
72
|
+
if (end >= 0)
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const all = Buffer.concat(chunks);
|
|
76
|
+
// The keys before `availability`, closed as an object of their own.
|
|
77
|
+
text =
|
|
78
|
+
end >= 0
|
|
79
|
+
? `${all.subarray(0, end).toString("utf8").replace(/,\s*$/, "")}}`
|
|
80
|
+
: all.toString("utf8");
|
|
52
81
|
}
|
|
53
82
|
finally {
|
|
54
83
|
await handle.close();
|
|
@@ -57,36 +86,21 @@ async function readHead(file) {
|
|
|
57
86
|
catch {
|
|
58
87
|
return undefined;
|
|
59
88
|
}
|
|
60
|
-
|
|
61
|
-
const match = new RegExp(`"${name}"\\s*:\\s*("(?:[^"\\\\]|\\\\.)*")`).exec(head);
|
|
62
|
-
if (!match?.[1])
|
|
63
|
-
return undefined;
|
|
64
|
-
try {
|
|
65
|
-
return JSON.parse(match[1]);
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
// The page's own keys come before `frontmatter`, so the first of each is the page's.
|
|
72
|
-
const pagePath = field("path");
|
|
73
|
-
const route = field("route");
|
|
74
|
-
const title = field("title");
|
|
75
|
-
if (pagePath !== undefined && route !== undefined && title !== undefined) {
|
|
76
|
-
return { path: pagePath, route, title };
|
|
77
|
-
}
|
|
89
|
+
let page;
|
|
78
90
|
try {
|
|
79
|
-
|
|
80
|
-
return typeof page.path === "string" && typeof page.route === "string"
|
|
81
|
-
? {
|
|
82
|
-
path: page.path,
|
|
83
|
-
route: page.route,
|
|
84
|
-
title: typeof page.title === "string" ? page.title : null,
|
|
85
|
-
}
|
|
86
|
-
: undefined;
|
|
91
|
+
page = JSON.parse(text);
|
|
87
92
|
}
|
|
88
93
|
catch {
|
|
89
94
|
return undefined;
|
|
90
95
|
}
|
|
96
|
+
if (typeof page.path !== "string" || typeof page.route !== "string")
|
|
97
|
+
return undefined;
|
|
98
|
+
const formatted = page.formatted?.title;
|
|
99
|
+
return {
|
|
100
|
+
path: page.path,
|
|
101
|
+
route: page.route,
|
|
102
|
+
title: typeof page.title === "string" ? page.title : null,
|
|
103
|
+
formatted_title: Array.isArray(formatted) ? formatted : null,
|
|
104
|
+
};
|
|
91
105
|
}
|
|
92
106
|
//# sourceMappingURL=routes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/review/routes.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,yEAAyE;AACzE,sBAAsB;AACtB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/review/routes.ts"],"names":[],"mappings":"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","sourcesContent":["// Which page of the build is at a route, and its title: read from the JSON\n// output, which the dev server writes while review is on, since the site\n// output doesn't say.\nimport { open, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { FormattedPiece } from \"@ascribed/review/overlay\";\n\n/** A route as compared: decoded, without a trailing `/` (except the root's). */\nexport function normalizeRoute(route: string): string {\n let decoded: string;\n try {\n decoded = decodeURI(route);\n } catch {\n decoded = route;\n }\n const trimmed = (decoded.split(/[?#]/)[0] ?? \"\").replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n}\n\n/** A page of the build, as its JSON output names it. */\nexport interface RoutedPage {\n /** The content path. */\n path: string;\n title: string | null;\n /** The title formatted, when its field sets `inline = \"code\"`. */\n formatted_title: FormattedPiece[] | null;\n}\n\n/** How much of a page's JSON to read at a time. */\nconst CHUNK_BYTES = 4096;\n\n/**\n * Where the page's own keys end in the JSON output, which writes it\n * pretty-printed: `availability` comes after `path`, `route`, `title`,\n * `frontmatter`, and `formatted`, and before the page's blocks.\n */\nconst AFTER_HEAD = Buffer.from('\\n \"availability\":');\n\n/**\n * Every page of the build's JSON output (`<output-dir>/<build>/json/`), by\n * normalized route. Empty when there's no JSON output.\n */\nexport async function readRoutes(jsonRoot: string): Promise<Map<string, RoutedPage>> {\n const routes = new Map<string, RoutedPage>();\n let files: { path?: unknown; kind?: unknown }[];\n try {\n const manifest = JSON.parse(\n await readFile(path.join(path.dirname(jsonRoot), \"json.manifest.json\"), \"utf8\"),\n ) as { files?: unknown };\n files = Array.isArray(manifest.files) ? (manifest.files as typeof files) : [];\n } catch {\n return routes;\n }\n await Promise.all(\n files.map(async (entry) => {\n if (entry.kind !== \"page\" || typeof entry.path !== \"string\") return;\n const page = await readHead(path.join(jsonRoot, ...entry.path.split(\"/\")));\n if (page) {\n const { route, ...routed } = page;\n routes.set(normalizeRoute(route), routed);\n }\n }),\n );\n return routes;\n}\n\n/**\n * A page's `path`, `route`, `title`, and formatted title, from the start of\n * its JSON up to `availability`, or the whole of it if need be.\n */\nasync function readHead(file: string): Promise<(RoutedPage & { route: string }) | undefined> {\n let text: string;\n try {\n const handle = await open(file, \"r\");\n try {\n const chunks: Buffer[] = [];\n let read = 0;\n let end = -1;\n for (;;) {\n const buffer = Buffer.alloc(CHUNK_BYTES);\n const { bytesRead } = await handle.read(buffer, 0, CHUNK_BYTES, read);\n if (bytesRead === 0) break;\n chunks.push(buffer.subarray(0, bytesRead));\n read += bytesRead;\n // The marker may straddle two chunks, so look from just before this one.\n end = Buffer.concat(chunks).indexOf(\n AFTER_HEAD,\n Math.max(0, read - bytesRead - AFTER_HEAD.length),\n );\n if (end >= 0) break;\n }\n const all = Buffer.concat(chunks);\n // The keys before `availability`, closed as an object of their own.\n text =\n end >= 0\n ? `${all.subarray(0, end).toString(\"utf8\").replace(/,\\s*$/, \"\")}}`\n : all.toString(\"utf8\");\n } finally {\n await handle.close();\n }\n } catch {\n return undefined;\n }\n let page: { path?: unknown; route?: unknown; title?: unknown; formatted?: { title?: unknown } };\n try {\n page = JSON.parse(text) as typeof page;\n } catch {\n return undefined;\n }\n if (typeof page.path !== \"string\" || typeof page.route !== \"string\") return undefined;\n const formatted = page.formatted?.title;\n return {\n path: page.path,\n route: page.route,\n title: typeof page.title === \"string\" ? page.title : null,\n formatted_title: Array.isArray(formatted) ? (formatted as FormattedPiece[]) : null,\n };\n}\n"]}
|
package/dist/review/server.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export interface ReviewServerOptions {
|
|
|
27
27
|
channelProblem: string | undefined;
|
|
28
28
|
/** Compares the build with `base` (the default branch when `undefined`). */
|
|
29
29
|
diff(base: string | undefined): Promise<DiffResult>;
|
|
30
|
+
/**
|
|
31
|
+
* `ascribe diff`'s agent prompt about the page or fragment at a content
|
|
32
|
+
* path, against `base`; `undefined` when it didn't change.
|
|
33
|
+
*/
|
|
34
|
+
prompt(path: string, base: string | undefined): Promise<string | undefined>;
|
|
35
|
+
/** The project's folder: where its `ascribe.toml` is. */
|
|
36
|
+
projectDir: string;
|
|
30
37
|
/** Opens the pull request's review. */
|
|
31
38
|
connect(): Promise<Connection>;
|
|
32
39
|
/** Writes the build's JSON output, in turn with the dev server's builds: it has the routes. */
|
|
@@ -66,6 +73,12 @@ export declare class ReviewServer {
|
|
|
66
73
|
private receive;
|
|
67
74
|
/** Answers one request. Rejects with a sentence to show. */
|
|
68
75
|
handle(method: string, params: Record<string, unknown>, tab?: string): Promise<unknown>;
|
|
76
|
+
/**
|
|
77
|
+
* The agent prompt the page asked for: a thread's, or every open one's,
|
|
78
|
+
* from the pull request; the page's changes, or a fragment's reach, from
|
|
79
|
+
* `ascribe diff`. `undefined` when there's nothing to prompt about.
|
|
80
|
+
*/
|
|
81
|
+
private prompt;
|
|
69
82
|
private start;
|
|
70
83
|
private stop;
|
|
71
84
|
private refresh;
|
|
@@ -75,6 +88,8 @@ export declare class ReviewServer {
|
|
|
75
88
|
private changes;
|
|
76
89
|
private compare;
|
|
77
90
|
private pageView;
|
|
91
|
+
/** The build's pages by route, read once until the next rebuild. */
|
|
92
|
+
private readRoutes;
|
|
78
93
|
/** Tells every page the threads or the changes changed; `from` is the page that did it. */
|
|
79
94
|
private changed;
|
|
80
95
|
}
|
package/dist/review/server.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// build with the pull request's base (or the default branch), and compares
|
|
11
11
|
// again after each rebuild.
|
|
12
12
|
import path from "node:path";
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
13
14
|
import { messageOf } from "./github.js";
|
|
14
15
|
import { CHANGED_EVENT, REQUEST_EVENT, RESULT_EVENT, } from "./protocol.js";
|
|
15
16
|
import { normalizeRoute } from "./routes.js";
|
|
@@ -120,6 +121,8 @@ export class ReviewServer {
|
|
|
120
121
|
await this.refresh();
|
|
121
122
|
this.changed(tab);
|
|
122
123
|
return null;
|
|
124
|
+
case "prompt":
|
|
125
|
+
return (await this.prompt(params)) ?? null;
|
|
123
126
|
}
|
|
124
127
|
if (!OVERLAY.has(method))
|
|
125
128
|
throw new Error(`Unknown request: ${method}.`);
|
|
@@ -133,8 +136,10 @@ export class ReviewServer {
|
|
|
133
136
|
page: pagePath === undefined ? undefined : { build: this.options.build, path: pagePath },
|
|
134
137
|
lastPage: pagePath === undefined ? undefined : this.pages.get(pagePath),
|
|
135
138
|
changedPages: async () => {
|
|
136
|
-
const changes = await this.changes();
|
|
137
|
-
|
|
139
|
+
const [changes, routes] = await Promise.all([this.changes(), this.readRoutes()]);
|
|
140
|
+
if (!("result" in changes))
|
|
141
|
+
return [];
|
|
142
|
+
return changes.result.pages.map((p) => listed(p, routes.get(normalizeRoute(p.route))));
|
|
138
143
|
},
|
|
139
144
|
}, method, params);
|
|
140
145
|
if (answer.page)
|
|
@@ -143,6 +148,47 @@ export class ReviewServer {
|
|
|
143
148
|
this.changed(tab);
|
|
144
149
|
return answer.result;
|
|
145
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* The agent prompt the page asked for: a thread's, or every open one's,
|
|
153
|
+
* from the pull request; the page's changes, or a fragment's reach, from
|
|
154
|
+
* `ascribe diff`. `undefined` when there's nothing to prompt about.
|
|
155
|
+
*/
|
|
156
|
+
async prompt(params) {
|
|
157
|
+
if (!this.on)
|
|
158
|
+
throw new Error("Review is off.");
|
|
159
|
+
const request = params["request"];
|
|
160
|
+
const pagePath = typeof params["path"] === "string" ? params["path"] : undefined;
|
|
161
|
+
const connection = await this.connected();
|
|
162
|
+
switch (request?.kind) {
|
|
163
|
+
case "thread":
|
|
164
|
+
case "open-threads": {
|
|
165
|
+
if (connection.state !== "on")
|
|
166
|
+
throw new Error("Review comments aren't on.");
|
|
167
|
+
const github = await import("@ascribed/review/github");
|
|
168
|
+
const contentRoot = this.options.contentRoot;
|
|
169
|
+
return github.buildThreadsPrompt({
|
|
170
|
+
session: connection.session,
|
|
171
|
+
project: github.promptProject(this.options.projectDir),
|
|
172
|
+
changedPages: async () => {
|
|
173
|
+
const changes = await this.changes();
|
|
174
|
+
return "result" in changes ? changes.result.pages : [];
|
|
175
|
+
},
|
|
176
|
+
// The agent reads the files on disk, as the dev server does: no editor here.
|
|
177
|
+
readSource: (file) => readFile(path.join(contentRoot, ...file.split("/")), "utf8").then((text) => ({ text, unsaved: false }), () => undefined),
|
|
178
|
+
}, request);
|
|
179
|
+
}
|
|
180
|
+
case "page-changes":
|
|
181
|
+
case "fragment-reach": {
|
|
182
|
+
const target = request.kind === "page-changes" ? pagePath : request.fragment;
|
|
183
|
+
if (target === undefined)
|
|
184
|
+
throw new Error("That isn't a page of the project.");
|
|
185
|
+
const base = connection.state === "on" ? connection.base : undefined;
|
|
186
|
+
return this.options.prompt(target, base);
|
|
187
|
+
}
|
|
188
|
+
default:
|
|
189
|
+
throw new Error("Unknown prompt.");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
146
192
|
async start() {
|
|
147
193
|
if (this.on)
|
|
148
194
|
return;
|
|
@@ -245,11 +291,10 @@ export class ReviewServer {
|
|
|
245
291
|
throw new Error("Review is off.");
|
|
246
292
|
const route = normalizeRoute(typeof params["route"] === "string" ? params["route"] : "/");
|
|
247
293
|
const fromPage = typeof params["path"] === "string" ? params["path"] : null;
|
|
248
|
-
this.routes ??= this.options.readRoutes().catch(() => new Map());
|
|
249
294
|
const [changes, connection, routes] = await Promise.all([
|
|
250
295
|
this.changes(),
|
|
251
296
|
this.connected(),
|
|
252
|
-
this.
|
|
297
|
+
this.readRoutes(),
|
|
253
298
|
]);
|
|
254
299
|
const pages = "result" in changes ? changes.result.pages : [];
|
|
255
300
|
const shown = pages.filter((p) => p.status !== "removed");
|
|
@@ -265,20 +310,26 @@ export class ReviewServer {
|
|
|
265
310
|
problem: "error" in changes ? changes.error : null,
|
|
266
311
|
errors: "result" in changes ? changes.result.errors : 0,
|
|
267
312
|
threads: threadsState(connection),
|
|
268
|
-
changedPages: pages.map((p) => listed(p, routes.get(normalizeRoute(p.route))
|
|
313
|
+
changedPages: pages.map((p) => listed(p, routes.get(normalizeRoute(p.route)))),
|
|
269
314
|
contentRoot: this.options.contentRoot,
|
|
270
315
|
separator: path.sep,
|
|
271
316
|
};
|
|
272
317
|
}
|
|
318
|
+
/** The build's pages by route, read once until the next rebuild. */
|
|
319
|
+
readRoutes() {
|
|
320
|
+
this.routes ??= this.options.readRoutes().catch(() => new Map());
|
|
321
|
+
return this.routes;
|
|
322
|
+
}
|
|
273
323
|
/** Tells every page the threads or the changes changed; `from` is the page that did it. */
|
|
274
324
|
changed(from) {
|
|
275
325
|
this.options.channel.send(CHANGED_EVENT, { from });
|
|
276
326
|
}
|
|
277
327
|
}
|
|
278
328
|
/** A changed page as the list shows it: without its changes, with its title. */
|
|
279
|
-
function listed(page,
|
|
329
|
+
function listed(page, routed) {
|
|
280
330
|
return {
|
|
281
|
-
title,
|
|
331
|
+
title: routed?.title ?? null,
|
|
332
|
+
formatted_title: routed?.formatted_title ?? null,
|
|
282
333
|
path: page.path,
|
|
283
334
|
route: page.route,
|
|
284
335
|
status: page.status,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/review/server.ts"],"names":[],"mappings":"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","sourcesContent":["// Review in the site preview, on the dev server's side: everything the\n// toolbar app asks for, answered over Astro's dev toolbar channel. Every\n// `git`, `ascribe`, and `gh` call happens here; the page gets the changes and\n// the threads, never a token. There are no HTTP routes: any page open in the\n// same browser could call a route on localhost, while the toolbar's channel\n// takes only clients that have the dev server's token (`channelProblem`).\n//\n// Nothing runs until the reviewer turns the app on. Then the server opens\n// the pull request's review (when the GitHub CLI finds one), compares the\n// build with the pull request's base (or the default branch), and compares\n// again after each rebuild.\nimport path from \"node:path\";\nimport type { OverlayMethod, ReviewSession } from \"@ascribed/review/github\";\nimport type { PageRef } from \"@ascribed/review/place\";\nimport type { DiffResult } from \"./diff.js\";\nimport { messageOf, type Connection } from \"./github.js\";\nimport {\n CHANGED_EVENT,\n REQUEST_EVENT,\n RESULT_EVENT,\n type ChangedPage,\n type PageDiff,\n type PageView,\n type Request,\n type Result,\n type Status,\n type ThreadsState,\n} from \"./protocol.js\";\nimport { normalizeRoute, type RoutedPage } from \"./routes.js\";\n\n/** A page's end of the channel: the server answers it alone. */\nexport interface ChannelClient {\n send(event: string, payload?: unknown): void;\n}\n\n/**\n * The toolbar's channel, as `astro:server:setup`'s `toolbar` has it. Its\n * listeners get the sending page's client too (it is Vite's `server.hot`);\n * without one, an answer goes to every page, which tell theirs by `tab`.\n */\nexport interface ToolbarChannel {\n on(event: string, callback: (data: unknown, client?: ChannelClient) => void): void;\n send(event: string, payload: unknown): void;\n}\n\nexport interface ReviewServerOptions {\n channel: ToolbarChannel;\n logger: { info(message: string): void; warn(message: string): void };\n build: string;\n /** The content root, absolute. */\n contentRoot: string;\n /** Why someone else could use the channel, if they could: then GitHub stays off. */\n channelProblem: string | undefined;\n /** Compares the build with `base` (the default branch when `undefined`). */\n diff(base: string | undefined): Promise<DiffResult>;\n /** Opens the pull request's review. */\n connect(): Promise<Connection>;\n /** Writes the build's JSON output, in turn with the dev server's builds: it has the routes. */\n writeRoutes(): Promise<void>;\n /** Every page of the build, by normalized route. */\n readRoutes(): Promise<Map<string, RoutedPage>>;\n /** How long to wait after a rebuild before comparing again. Default 150 ms. */\n debounceMs?: number;\n}\n\n/** The overlay's requests, which go to the session. */\nconst OVERLAY: ReadonlySet<string> = new Set<OverlayMethod>([\n \"load\",\n \"commentTarget\",\n \"comment\",\n \"reply\",\n \"allThreads\",\n \"resolve\",\n \"submit\",\n \"discard\",\n]);\n\nexport class ReviewServer {\n private on = false;\n private problem: string | undefined;\n private connection: Connection | undefined;\n private connecting: Promise<Connection> | undefined;\n private diffed: { result: DiffResult } | { error: string } | undefined;\n private diffing: Promise<void> | undefined;\n private stale = false;\n private timer: ReturnType<typeof setTimeout> | undefined;\n private routes: Promise<Map<string, RoutedPage>> | undefined;\n /** The page each overlay last read, by content path: comments are made on it. */\n private readonly pages = new Map<string, PageRef>();\n /** How long the last comparison took, in milliseconds. */\n lastDiffMs: number | undefined;\n\n constructor(private readonly options: ReviewServerOptions) {\n this.problem = options.channelProblem;\n options.channel.on(REQUEST_EVENT, (data, client) => {\n void this.receive(data, client);\n });\n }\n\n /**\n * The address the dev server listens on, once it does: one other machines\n * can reach turns comments off, as `server.host` does.\n */\n listening(address: string): void {\n if (this.problem !== undefined || isLoopback(address)) return;\n this.problem = NETWORK_PROBLEM;\n this.connection = undefined;\n this.connecting = undefined;\n }\n\n /** Whether review is on: then the dev server's builds write JSON output too. */\n get active(): boolean {\n return this.on;\n }\n\n /** The dev server rebuilt: compare again soon, and read the routes again when asked. */\n rebuilt(): void {\n if (!this.on) return;\n this.stale = true;\n this.routes = undefined;\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => {\n this.timer = undefined;\n void this.changes();\n }, this.options.debounceMs ?? 150);\n }\n\n /** The dev server stopped. */\n dispose(): void {\n if (this.timer) clearTimeout(this.timer);\n this.on = false;\n }\n\n private async receive(data: unknown, client: ChannelClient | undefined): Promise<void> {\n const request = data as Partial<Request> | null;\n if (typeof request?.id !== \"number\" || typeof request.method !== \"string\") return;\n const params =\n typeof request.params === \"object\" && request.params !== null ? request.params : {};\n const tab = typeof request.tab === \"string\" ? request.tab : \"\";\n const answer: Result = { tab, id: request.id };\n try {\n answer.result = await this.handle(request.method, params, tab);\n } catch (error) {\n const code = (error as { code?: unknown }).code;\n answer.error =\n typeof code === \"string\"\n ? { message: messageOf(error), code }\n : { message: messageOf(error) };\n }\n if (typeof client?.send === \"function\") client.send(RESULT_EVENT, answer);\n else this.options.channel.send(RESULT_EVENT, answer);\n }\n\n /** Answers one request. Rejects with a sentence to show. */\n async handle(method: string, params: Record<string, unknown>, tab = \"\"): Promise<unknown> {\n switch (method) {\n case \"status\":\n return { on: this.on } satisfies Status;\n case \"start\":\n await this.start();\n return { on: true } satisfies Status;\n case \"stop\":\n this.stop();\n this.changed(tab);\n return { on: false } satisfies Status;\n case \"page\":\n return this.pageView(params);\n case \"refresh\":\n await this.refresh();\n this.changed(tab);\n return null;\n }\n if (!OVERLAY.has(method)) throw new Error(`Unknown request: ${method}.`);\n const connection = this.on ? await this.connected() : undefined;\n if (connection?.state !== \"on\") throw new Error(\"Review comments aren't on.\");\n const github = await import(\"@ascribed/review/github\");\n const pagePath = typeof params[\"path\"] === \"string\" ? params[\"path\"] : undefined;\n const answer = await github.answerRequest(\n {\n session: connection.session,\n page: pagePath === undefined ? undefined : { build: this.options.build, path: pagePath },\n lastPage: pagePath === undefined ? undefined : this.pages.get(pagePath),\n changedPages: async () => {\n const changes = await this.changes();\n return \"result\" in changes ? changes.result.pages : [];\n },\n },\n method as OverlayMethod,\n params,\n );\n if (answer.page) this.pages.set(answer.page.path, answer.page);\n if (answer.changed) this.changed(tab);\n return answer.result;\n }\n\n private async start(): Promise<void> {\n if (this.on) return;\n this.on = true;\n this.stale = true;\n this.routes = undefined;\n this.connection = undefined;\n const started = Date.now();\n // The routes come from the JSON output, which the next builds write too.\n const routes = this.options.writeRoutes().catch((error: unknown) => {\n this.options.logger.warn(`couldn't write the routes for review: ${messageOf(error)}`);\n });\n await this.connected();\n const changes = await this.changes();\n await routes;\n if (\"result\" in changes) {\n const base = changes.result.base;\n this.options.logger.info(\n `review: compared \"${this.options.build}\" with ${base.requested} in ${this.lastDiffMs ?? 0} ms (started in ${Date.now() - started} ms)`,\n );\n } else {\n this.options.logger.warn(`review: ${changes.error}`);\n }\n }\n\n private stop(): void {\n this.on = false;\n this.connection = undefined;\n this.connecting = undefined;\n this.diffed = undefined;\n this.routes = undefined;\n this.pages.clear();\n if (this.timer) clearTimeout(this.timer);\n this.timer = undefined;\n }\n\n private async refresh(): Promise<void> {\n if (!this.on) return;\n const connection = await this.connected();\n if (connection.state === \"on\") await connection.session.refresh();\n else this.connection = undefined;\n // A pull request found now changes the base.\n await this.connected();\n this.stale = true;\n await this.changes();\n }\n\n /** The pull request's review, opened once while review is on. */\n private connected(): Promise<Connection> {\n if (this.connection) return Promise.resolve(this.connection);\n if (this.problem !== undefined) {\n this.connection = {\n state: \"unprotected\",\n message: `Showing changes only. Comments are off because ${this.problem}.`,\n };\n return Promise.resolve(this.connection);\n }\n this.connecting ??= this.options\n .connect()\n .catch((error: unknown): Connection => ({ state: \"error\", message: messageOf(error) }))\n .then((connection) => {\n this.connecting = undefined;\n // The server turned out to listen on the network meanwhile.\n if (this.problem !== undefined) return this.connected();\n if (this.on) this.connection = connection;\n return connection;\n });\n return this.connecting;\n }\n\n /** The build's changes, compared again first if a rebuild made them stale. */\n private async changes(): Promise<{ result: DiffResult } | { error: string }> {\n while (this.diffing) await this.diffing;\n if (this.stale || this.diffed === undefined) {\n this.stale = false;\n this.diffing = this.compare().finally(() => {\n this.diffing = undefined;\n });\n await this.diffing;\n }\n return this.diffed ?? { error: \"Review is off.\" };\n }\n\n private async compare(): Promise<void> {\n const connection = await this.connected();\n const base = connection.state === \"on\" ? connection.base : undefined;\n const started = Date.now();\n try {\n this.diffed = { result: await this.options.diff(base) };\n } catch (error) {\n this.diffed = { error: messageOf(error) };\n }\n this.lastDiffMs = Date.now() - started;\n }\n\n private async pageView(params: Record<string, unknown>): Promise<PageView> {\n if (!this.on) throw new Error(\"Review is off.\");\n const route = normalizeRoute(typeof params[\"route\"] === \"string\" ? params[\"route\"] : \"/\");\n const fromPage = typeof params[\"path\"] === \"string\" ? params[\"path\"] : null;\n this.routes ??= this.options.readRoutes().catch(() => new Map<string, RoutedPage>());\n const [changes, connection, routes] = await Promise.all([\n this.changes(),\n this.connected(),\n this.routes,\n ]);\n const pages = \"result\" in changes ? changes.result.pages : [];\n const shown = pages.filter((p) => p.status !== \"removed\");\n let page = shown.find((p) => normalizeRoute(p.route) === route) ?? null;\n const pagePath = page?.path ?? fromPage ?? routes.get(route)?.path ?? null;\n if (page === null && pagePath !== null) page = shown.find((p) => p.path === pagePath) ?? null;\n return {\n kind: pagePath === null ? \"not-page\" : \"page\",\n path: pagePath,\n page,\n base: \"result\" in changes ? changes.result.base : null,\n problem: \"error\" in changes ? changes.error : null,\n errors: \"result\" in changes ? changes.result.errors : 0,\n threads: threadsState(connection),\n changedPages: pages.map((p) => listed(p, routes.get(normalizeRoute(p.route))?.title ?? null)),\n contentRoot: this.options.contentRoot,\n separator: path.sep,\n };\n }\n\n /** Tells every page the threads or the changes changed; `from` is the page that did it. */\n private changed(from: string): void {\n this.options.channel.send(CHANGED_EVENT, { from });\n }\n}\n\n/** A changed page as the list shows it: without its changes, with its title. */\nfunction listed(page: PageDiff, title: string | null): ChangedPage {\n return {\n title,\n path: page.path,\n route: page.route,\n status: page.status,\n own_file_changed: page.own_file_changed,\n because: page.because,\n page_changed: page.page_changed,\n counts: page.counts,\n };\n}\n\n/** What the page shows about the threads. */\nfunction threadsState(connection: Connection): ThreadsState {\n if (connection.state !== \"on\") return connection;\n const pr: ReviewSession[\"pullRequest\"] = connection.session.pullRequest;\n return {\n state: \"on\",\n pullRequest: { number: pr.number, url: pr.url, baseRefName: pr.baseRefName },\n local: connection.local,\n };\n}\n\n/**\n * Why someone other than the developer could use the dev server's toolbar\n * channel, or `undefined` when they can't. Vite takes a browser's connection\n * only with the token in its client script, which the page's own origin can\n * read and, by default, only localhost origins can fetch. These settings undo\n * that, and a server listening on the network gives its pages, token and all,\n * to anyone who can reach it.\n */\nexport function channelProblem(config: {\n server: { cors?: unknown; allowedHosts?: unknown; host?: unknown };\n legacy?: { skipWebSocketTokenCheck?: unknown } | undefined;\n}): string | undefined {\n const pages = \"other web pages could talk to this dev server\";\n if (config.legacy?.skipWebSocketTokenCheck === true) {\n return `${pages}: \\`legacy.skipWebSocketTokenCheck\\` is on`;\n }\n if (config.server.allowedHosts === true) return `${pages}: \\`server.allowedHosts\\` is \\`true\\``;\n const cors = config.server.cors;\n const origin =\n typeof cors === \"object\" && cors !== null ? (cors as { origin?: unknown }).origin : undefined;\n if (cors === true || origin === true || origin === \"*\") {\n return `${pages}: \\`server.cors\\` lets any origin read its pages`;\n }\n if (!isLoopback(config.server.host)) return NETWORK_PROBLEM;\n return undefined;\n}\n\nconst NETWORK_PROBLEM =\n \"the dev server is listening on the network (`--host`), so anyone who can reach it could comment as you\";\n\n/** Whether Vite's `server.host`, or the address the server listens on, keeps the server on this machine: unset, `false`, or a loopback name. */\nfunction isLoopback(host: unknown): boolean {\n if (host === undefined || host === false) return true;\n if (typeof host !== \"string\") return false;\n const name = host.toLowerCase().replace(/^\\[(.*)\\]$/, \"$1\");\n return name === \"localhost\" || name === \"::1\" || /^127(\\.\\d{1,3}){3}$/.test(name);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/review/server.ts"],"names":[],"mappings":"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","sourcesContent":["// Review in the site preview, on the dev server's side: everything the\n// toolbar app asks for, answered over Astro's dev toolbar channel. Every\n// `git`, `ascribe`, and `gh` call happens here; the page gets the changes and\n// the threads, never a token. There are no HTTP routes: any page open in the\n// same browser could call a route on localhost, while the toolbar's channel\n// takes only clients that have the dev server's token (`channelProblem`).\n//\n// Nothing runs until the reviewer turns the app on. Then the server opens\n// the pull request's review (when the GitHub CLI finds one), compares the\n// build with the pull request's base (or the default branch), and compares\n// again after each rebuild.\nimport path from \"node:path\";\nimport { readFile } from \"node:fs/promises\";\nimport type { OverlayMethod, PromptRequest, ReviewSession } from \"@ascribed/review/github\";\nimport type { PageRef } from \"@ascribed/review/place\";\nimport type { DiffResult } from \"./diff.js\";\nimport { messageOf, type Connection } from \"./github.js\";\nimport {\n CHANGED_EVENT,\n REQUEST_EVENT,\n RESULT_EVENT,\n type ChangedPage,\n type PageDiff,\n type PageView,\n type Request,\n type Result,\n type Status,\n type ThreadsState,\n} from \"./protocol.js\";\nimport { normalizeRoute, type RoutedPage } from \"./routes.js\";\n\n/** A page's end of the channel: the server answers it alone. */\nexport interface ChannelClient {\n send(event: string, payload?: unknown): void;\n}\n\n/**\n * The toolbar's channel, as `astro:server:setup`'s `toolbar` has it. Its\n * listeners get the sending page's client too (it is Vite's `server.hot`);\n * without one, an answer goes to every page, which tell theirs by `tab`.\n */\nexport interface ToolbarChannel {\n on(event: string, callback: (data: unknown, client?: ChannelClient) => void): void;\n send(event: string, payload: unknown): void;\n}\n\nexport interface ReviewServerOptions {\n channel: ToolbarChannel;\n logger: { info(message: string): void; warn(message: string): void };\n build: string;\n /** The content root, absolute. */\n contentRoot: string;\n /** Why someone else could use the channel, if they could: then GitHub stays off. */\n channelProblem: string | undefined;\n /** Compares the build with `base` (the default branch when `undefined`). */\n diff(base: string | undefined): Promise<DiffResult>;\n /**\n * `ascribe diff`'s agent prompt about the page or fragment at a content\n * path, against `base`; `undefined` when it didn't change.\n */\n prompt(path: string, base: string | undefined): Promise<string | undefined>;\n /** The project's folder: where its `ascribe.toml` is. */\n projectDir: string;\n /** Opens the pull request's review. */\n connect(): Promise<Connection>;\n /** Writes the build's JSON output, in turn with the dev server's builds: it has the routes. */\n writeRoutes(): Promise<void>;\n /** Every page of the build, by normalized route. */\n readRoutes(): Promise<Map<string, RoutedPage>>;\n /** How long to wait after a rebuild before comparing again. Default 150 ms. */\n debounceMs?: number;\n}\n\n/** The overlay's requests, which go to the session. */\nconst OVERLAY: ReadonlySet<string> = new Set<OverlayMethod>([\n \"load\",\n \"commentTarget\",\n \"comment\",\n \"reply\",\n \"allThreads\",\n \"resolve\",\n \"submit\",\n \"discard\",\n]);\n\nexport class ReviewServer {\n private on = false;\n private problem: string | undefined;\n private connection: Connection | undefined;\n private connecting: Promise<Connection> | undefined;\n private diffed: { result: DiffResult } | { error: string } | undefined;\n private diffing: Promise<void> | undefined;\n private stale = false;\n private timer: ReturnType<typeof setTimeout> | undefined;\n private routes: Promise<Map<string, RoutedPage>> | undefined;\n /** The page each overlay last read, by content path: comments are made on it. */\n private readonly pages = new Map<string, PageRef>();\n /** How long the last comparison took, in milliseconds. */\n lastDiffMs: number | undefined;\n\n constructor(private readonly options: ReviewServerOptions) {\n this.problem = options.channelProblem;\n options.channel.on(REQUEST_EVENT, (data, client) => {\n void this.receive(data, client);\n });\n }\n\n /**\n * The address the dev server listens on, once it does: one other machines\n * can reach turns comments off, as `server.host` does.\n */\n listening(address: string): void {\n if (this.problem !== undefined || isLoopback(address)) return;\n this.problem = NETWORK_PROBLEM;\n this.connection = undefined;\n this.connecting = undefined;\n }\n\n /** Whether review is on: then the dev server's builds write JSON output too. */\n get active(): boolean {\n return this.on;\n }\n\n /** The dev server rebuilt: compare again soon, and read the routes again when asked. */\n rebuilt(): void {\n if (!this.on) return;\n this.stale = true;\n this.routes = undefined;\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => {\n this.timer = undefined;\n void this.changes();\n }, this.options.debounceMs ?? 150);\n }\n\n /** The dev server stopped. */\n dispose(): void {\n if (this.timer) clearTimeout(this.timer);\n this.on = false;\n }\n\n private async receive(data: unknown, client: ChannelClient | undefined): Promise<void> {\n const request = data as Partial<Request> | null;\n if (typeof request?.id !== \"number\" || typeof request.method !== \"string\") return;\n const params =\n typeof request.params === \"object\" && request.params !== null ? request.params : {};\n const tab = typeof request.tab === \"string\" ? request.tab : \"\";\n const answer: Result = { tab, id: request.id };\n try {\n answer.result = await this.handle(request.method, params, tab);\n } catch (error) {\n const code = (error as { code?: unknown }).code;\n answer.error =\n typeof code === \"string\"\n ? { message: messageOf(error), code }\n : { message: messageOf(error) };\n }\n if (typeof client?.send === \"function\") client.send(RESULT_EVENT, answer);\n else this.options.channel.send(RESULT_EVENT, answer);\n }\n\n /** Answers one request. Rejects with a sentence to show. */\n async handle(method: string, params: Record<string, unknown>, tab = \"\"): Promise<unknown> {\n switch (method) {\n case \"status\":\n return { on: this.on } satisfies Status;\n case \"start\":\n await this.start();\n return { on: true } satisfies Status;\n case \"stop\":\n this.stop();\n this.changed(tab);\n return { on: false } satisfies Status;\n case \"page\":\n return this.pageView(params);\n case \"refresh\":\n await this.refresh();\n this.changed(tab);\n return null;\n case \"prompt\":\n return (await this.prompt(params)) ?? null;\n }\n if (!OVERLAY.has(method)) throw new Error(`Unknown request: ${method}.`);\n const connection = this.on ? await this.connected() : undefined;\n if (connection?.state !== \"on\") throw new Error(\"Review comments aren't on.\");\n const github = await import(\"@ascribed/review/github\");\n const pagePath = typeof params[\"path\"] === \"string\" ? params[\"path\"] : undefined;\n const answer = await github.answerRequest(\n {\n session: connection.session,\n page: pagePath === undefined ? undefined : { build: this.options.build, path: pagePath },\n lastPage: pagePath === undefined ? undefined : this.pages.get(pagePath),\n changedPages: async () => {\n const [changes, routes] = await Promise.all([this.changes(), this.readRoutes()]);\n if (!(\"result\" in changes)) return [];\n return changes.result.pages.map((p) => listed(p, routes.get(normalizeRoute(p.route))));\n },\n },\n method as OverlayMethod,\n params,\n );\n if (answer.page) this.pages.set(answer.page.path, answer.page);\n if (answer.changed) this.changed(tab);\n return answer.result;\n }\n\n /**\n * The agent prompt the page asked for: a thread's, or every open one's,\n * from the pull request; the page's changes, or a fragment's reach, from\n * `ascribe diff`. `undefined` when there's nothing to prompt about.\n */\n private async prompt(params: Record<string, unknown>): Promise<string | undefined> {\n if (!this.on) throw new Error(\"Review is off.\");\n const request = params[\"request\"] as PromptRequest | undefined;\n const pagePath = typeof params[\"path\"] === \"string\" ? params[\"path\"] : undefined;\n const connection = await this.connected();\n switch (request?.kind) {\n case \"thread\":\n case \"open-threads\": {\n if (connection.state !== \"on\") throw new Error(\"Review comments aren't on.\");\n const github = await import(\"@ascribed/review/github\");\n const contentRoot = this.options.contentRoot;\n return github.buildThreadsPrompt(\n {\n session: connection.session,\n project: github.promptProject(this.options.projectDir),\n changedPages: async () => {\n const changes = await this.changes();\n return \"result\" in changes ? changes.result.pages : [];\n },\n // The agent reads the files on disk, as the dev server does: no editor here.\n readSource: (file) =>\n readFile(path.join(contentRoot, ...file.split(\"/\")), \"utf8\").then(\n (text) => ({ text, unsaved: false }),\n () => undefined,\n ),\n },\n request,\n );\n }\n case \"page-changes\":\n case \"fragment-reach\": {\n const target = request.kind === \"page-changes\" ? pagePath : request.fragment;\n if (target === undefined) throw new Error(\"That isn't a page of the project.\");\n const base = connection.state === \"on\" ? connection.base : undefined;\n return this.options.prompt(target, base);\n }\n default:\n throw new Error(\"Unknown prompt.\");\n }\n }\n\n private async start(): Promise<void> {\n if (this.on) return;\n this.on = true;\n this.stale = true;\n this.routes = undefined;\n this.connection = undefined;\n const started = Date.now();\n // The routes come from the JSON output, which the next builds write too.\n const routes = this.options.writeRoutes().catch((error: unknown) => {\n this.options.logger.warn(`couldn't write the routes for review: ${messageOf(error)}`);\n });\n await this.connected();\n const changes = await this.changes();\n await routes;\n if (\"result\" in changes) {\n const base = changes.result.base;\n this.options.logger.info(\n `review: compared \"${this.options.build}\" with ${base.requested} in ${this.lastDiffMs ?? 0} ms (started in ${Date.now() - started} ms)`,\n );\n } else {\n this.options.logger.warn(`review: ${changes.error}`);\n }\n }\n\n private stop(): void {\n this.on = false;\n this.connection = undefined;\n this.connecting = undefined;\n this.diffed = undefined;\n this.routes = undefined;\n this.pages.clear();\n if (this.timer) clearTimeout(this.timer);\n this.timer = undefined;\n }\n\n private async refresh(): Promise<void> {\n if (!this.on) return;\n const connection = await this.connected();\n if (connection.state === \"on\") await connection.session.refresh();\n else this.connection = undefined;\n // A pull request found now changes the base.\n await this.connected();\n this.stale = true;\n await this.changes();\n }\n\n /** The pull request's review, opened once while review is on. */\n private connected(): Promise<Connection> {\n if (this.connection) return Promise.resolve(this.connection);\n if (this.problem !== undefined) {\n this.connection = {\n state: \"unprotected\",\n message: `Showing changes only. Comments are off because ${this.problem}.`,\n };\n return Promise.resolve(this.connection);\n }\n this.connecting ??= this.options\n .connect()\n .catch((error: unknown): Connection => ({ state: \"error\", message: messageOf(error) }))\n .then((connection) => {\n this.connecting = undefined;\n // The server turned out to listen on the network meanwhile.\n if (this.problem !== undefined) return this.connected();\n if (this.on) this.connection = connection;\n return connection;\n });\n return this.connecting;\n }\n\n /** The build's changes, compared again first if a rebuild made them stale. */\n private async changes(): Promise<{ result: DiffResult } | { error: string }> {\n while (this.diffing) await this.diffing;\n if (this.stale || this.diffed === undefined) {\n this.stale = false;\n this.diffing = this.compare().finally(() => {\n this.diffing = undefined;\n });\n await this.diffing;\n }\n return this.diffed ?? { error: \"Review is off.\" };\n }\n\n private async compare(): Promise<void> {\n const connection = await this.connected();\n const base = connection.state === \"on\" ? connection.base : undefined;\n const started = Date.now();\n try {\n this.diffed = { result: await this.options.diff(base) };\n } catch (error) {\n this.diffed = { error: messageOf(error) };\n }\n this.lastDiffMs = Date.now() - started;\n }\n\n private async pageView(params: Record<string, unknown>): Promise<PageView> {\n if (!this.on) throw new Error(\"Review is off.\");\n const route = normalizeRoute(typeof params[\"route\"] === \"string\" ? params[\"route\"] : \"/\");\n const fromPage = typeof params[\"path\"] === \"string\" ? params[\"path\"] : null;\n const [changes, connection, routes] = await Promise.all([\n this.changes(),\n this.connected(),\n this.readRoutes(),\n ]);\n const pages = \"result\" in changes ? changes.result.pages : [];\n const shown = pages.filter((p) => p.status !== \"removed\");\n let page = shown.find((p) => normalizeRoute(p.route) === route) ?? null;\n const pagePath = page?.path ?? fromPage ?? routes.get(route)?.path ?? null;\n if (page === null && pagePath !== null) page = shown.find((p) => p.path === pagePath) ?? null;\n return {\n kind: pagePath === null ? \"not-page\" : \"page\",\n path: pagePath,\n page,\n base: \"result\" in changes ? changes.result.base : null,\n problem: \"error\" in changes ? changes.error : null,\n errors: \"result\" in changes ? changes.result.errors : 0,\n threads: threadsState(connection),\n changedPages: pages.map((p) => listed(p, routes.get(normalizeRoute(p.route)))),\n contentRoot: this.options.contentRoot,\n separator: path.sep,\n };\n }\n\n /** The build's pages by route, read once until the next rebuild. */\n private readRoutes(): Promise<Map<string, RoutedPage>> {\n this.routes ??= this.options.readRoutes().catch(() => new Map<string, RoutedPage>());\n return this.routes;\n }\n\n /** Tells every page the threads or the changes changed; `from` is the page that did it. */\n private changed(from: string): void {\n this.options.channel.send(CHANGED_EVENT, { from });\n }\n}\n\n/** A changed page as the list shows it: without its changes, with its title. */\nfunction listed(page: PageDiff, routed: RoutedPage | undefined): ChangedPage {\n return {\n title: routed?.title ?? null,\n formatted_title: routed?.formatted_title ?? null,\n path: page.path,\n route: page.route,\n status: page.status,\n own_file_changed: page.own_file_changed,\n because: page.because,\n page_changed: page.page_changed,\n counts: page.counts,\n };\n}\n\n/** What the page shows about the threads. */\nfunction threadsState(connection: Connection): ThreadsState {\n if (connection.state !== \"on\") return connection;\n const pr: ReviewSession[\"pullRequest\"] = connection.session.pullRequest;\n return {\n state: \"on\",\n pullRequest: { number: pr.number, url: pr.url, baseRefName: pr.baseRefName },\n local: connection.local,\n };\n}\n\n/**\n * Why someone other than the developer could use the dev server's toolbar\n * channel, or `undefined` when they can't. Vite takes a browser's connection\n * only with the token in its client script, which the page's own origin can\n * read and, by default, only localhost origins can fetch. These settings undo\n * that, and a server listening on the network gives its pages, token and all,\n * to anyone who can reach it.\n */\nexport function channelProblem(config: {\n server: { cors?: unknown; allowedHosts?: unknown; host?: unknown };\n legacy?: { skipWebSocketTokenCheck?: unknown } | undefined;\n}): string | undefined {\n const pages = \"other web pages could talk to this dev server\";\n if (config.legacy?.skipWebSocketTokenCheck === true) {\n return `${pages}: \\`legacy.skipWebSocketTokenCheck\\` is on`;\n }\n if (config.server.allowedHosts === true) return `${pages}: \\`server.allowedHosts\\` is \\`true\\``;\n const cors = config.server.cors;\n const origin =\n typeof cors === \"object\" && cors !== null ? (cors as { origin?: unknown }).origin : undefined;\n if (cors === true || origin === true || origin === \"*\") {\n return `${pages}: \\`server.cors\\` lets any origin read its pages`;\n }\n if (!isLoopback(config.server.host)) return NETWORK_PROBLEM;\n return undefined;\n}\n\nconst NETWORK_PROBLEM =\n \"the dev server is listening on the network (`--host`), so anyone who can reach it could comment as you\";\n\n/** Whether Vite's `server.host`, or the address the server listens on, keeps the server on this machine: unset, `false`, or a loopback name. */\nfunction isLoopback(host: unknown): boolean {\n if (host === undefined || host === false) return true;\n if (typeof host !== \"string\") return false;\n const name = host.toLowerCase().replace(/^\\[(.*)\\]$/, \"$1\");\n return name === \"localhost\" || name === \"::1\" || /^127(\\.\\d{1,3}){3}$/.test(name);\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { type ProjectRobots, type SitemapSources } from "./records.js";
|
|
3
|
+
/** What the files are drawn from. */
|
|
4
|
+
export interface RootFileSources extends Omit<SitemapSources, "site"> {
|
|
5
|
+
/** What each project says to crawlers, each project once. */
|
|
6
|
+
robots: readonly ProjectRobots[];
|
|
7
|
+
/** Astro's `site`, when it's set. */
|
|
8
|
+
site: string | undefined;
|
|
9
|
+
/** Astro's `base`. */
|
|
10
|
+
base: string;
|
|
11
|
+
/** Whether the site has its own `robots.txt`, in `public/`. */
|
|
12
|
+
ownRobots: boolean;
|
|
13
|
+
/** Whether the site has its own `sitemap.xml`, in `public/`. */
|
|
14
|
+
ownSitemap: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** A thing the build says about the files, and how loudly. */
|
|
17
|
+
interface RootFileNote {
|
|
18
|
+
level: "info" | "warn";
|
|
19
|
+
text: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The files, each `undefined` when none is written, what to say about them,
|
|
23
|
+
* and the problems that stop `robots.txt` from being written, which fail a
|
|
24
|
+
* build.
|
|
25
|
+
*/
|
|
26
|
+
export interface RootFiles {
|
|
27
|
+
sitemap: string | undefined;
|
|
28
|
+
robots: string | undefined;
|
|
29
|
+
notes: RootFileNote[];
|
|
30
|
+
problems: string[];
|
|
31
|
+
}
|
|
32
|
+
/** The sitemap and `robots.txt` for a site, and what the build says about them. */
|
|
33
|
+
export declare function rootFiles(sources: RootFileSources): RootFiles;
|
|
34
|
+
type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
|
|
35
|
+
/**
|
|
36
|
+
* A dev-server middleware serving the sitemap at `<base>sitemap.xml` and
|
|
37
|
+
* `robots.txt` at `/`, each as `files` has it when it's asked for, so each
|
|
38
|
+
* rebuild's records are served.
|
|
39
|
+
*/
|
|
40
|
+
export declare function rootFilesMiddleware(base: string, files: () => RootFiles): Middleware;
|
|
41
|
+
/** A route as `astro:routes:resolved` gives it, as much as these read. */
|
|
42
|
+
export interface AstroRoute {
|
|
43
|
+
pattern: string;
|
|
44
|
+
patternRegex: RegExp;
|
|
45
|
+
params: string[];
|
|
46
|
+
type: string;
|
|
47
|
+
origin: string;
|
|
48
|
+
}
|
|
49
|
+
/** Astro's routing, as much as these read. */
|
|
50
|
+
interface Routing {
|
|
51
|
+
base: string;
|
|
52
|
+
trailingSlash: "always" | "never" | "ignore";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The site's own pages `astro dev` can name: each route of the site's own
|
|
56
|
+
* with no parameters, other than the not-found and error pages, by its route
|
|
57
|
+
* from the host's root. A page a route's `getStaticPaths` lists is known only
|
|
58
|
+
* to a build.
|
|
59
|
+
*/
|
|
60
|
+
export declare function devSitePages(routes: readonly AstroRoute[], astro: Routing): string[];
|
|
61
|
+
/**
|
|
62
|
+
* Whether a project's page, by its route from the host's root, has a route
|
|
63
|
+
* in the site in `astro dev`: whether one of the site's own routes matches
|
|
64
|
+
* it. A project no route shows, such as one whose collection a page only
|
|
65
|
+
* quotes from, has no pages in the sitemap.
|
|
66
|
+
*/
|
|
67
|
+
export declare function devRouted(routes: readonly AstroRoute[], astro: Routing): (route: string) => boolean;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { normalizeBase } from "./project.js";
|
|
2
|
+
import { purposeConflicts, robotsText, routeAs, routeKey, sitemapAddresses, sitemapRoute, sitemapXml, } from "./records.js";
|
|
3
|
+
/** The sitemap and `robots.txt` for a site, and what the build says about them. */
|
|
4
|
+
export function rootFiles(sources) {
|
|
5
|
+
const notes = [];
|
|
6
|
+
const base = normalizeBase(sources.base);
|
|
7
|
+
let sitemap;
|
|
8
|
+
let sitemapAddress;
|
|
9
|
+
if (sources.ownSitemap) {
|
|
10
|
+
// The site's own, at the same address, which robots.txt points to when there's an address.
|
|
11
|
+
notes.push({ level: "info", text: "no sitemap.xml: public/sitemap.xml is the site's own." });
|
|
12
|
+
if (sources.site !== undefined)
|
|
13
|
+
sitemapAddress = new URL(sitemapRoute(base), sources.site).href;
|
|
14
|
+
}
|
|
15
|
+
else if (sources.site === undefined) {
|
|
16
|
+
notes.push({
|
|
17
|
+
level: "warn",
|
|
18
|
+
text: "no sitemap.xml: a sitemap lists pages by their absolute addresses, and the site has no address. Set [consumer] site in ascribe.toml, or Astro's `site`.",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
sitemap = sitemapXml(sitemapAddresses({ ...sources, site: sources.site }));
|
|
23
|
+
sitemapAddress = new URL(sitemapRoute(base), sources.site).href;
|
|
24
|
+
}
|
|
25
|
+
const problems = purposeConflicts(sources.robots);
|
|
26
|
+
let robots = problems.length > 0
|
|
27
|
+
? undefined
|
|
28
|
+
: robotsText(sources.robots.map((project) => project.robots), sitemapAddress);
|
|
29
|
+
if (robots !== undefined && base !== "/") {
|
|
30
|
+
notes.push({
|
|
31
|
+
level: "info",
|
|
32
|
+
text: `no robots.txt: Astro's \`base\` is "${base}", and a crawler reads robots.txt only at the host's root. Put these lines in the host's own:\n${robots}`,
|
|
33
|
+
});
|
|
34
|
+
robots = undefined;
|
|
35
|
+
}
|
|
36
|
+
else if (robots !== undefined && sources.ownRobots) {
|
|
37
|
+
notes.push({
|
|
38
|
+
level: "info",
|
|
39
|
+
text: `no robots.txt: public/robots.txt is the site's own. Ascribe's lines for it:\n${robots}`,
|
|
40
|
+
});
|
|
41
|
+
robots = undefined;
|
|
42
|
+
}
|
|
43
|
+
return { sitemap, robots, notes, problems };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A dev-server middleware serving the sitemap at `<base>sitemap.xml` and
|
|
47
|
+
* `robots.txt` at `/`, each as `files` has it when it's asked for, so each
|
|
48
|
+
* rebuild's records are served.
|
|
49
|
+
*/
|
|
50
|
+
export function rootFilesMiddleware(base, files) {
|
|
51
|
+
const sitemapPath = sitemapRoute(base);
|
|
52
|
+
return (req, res, next) => {
|
|
53
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
54
|
+
return next();
|
|
55
|
+
// Vite strips the base from `req.url`; connect keeps the original in `originalUrl`.
|
|
56
|
+
const pathname = (req.originalUrl ?? req.url ?? "").split(/[?#]/, 1)[0] ?? "";
|
|
57
|
+
if (pathname !== sitemapPath && pathname !== "/robots.txt")
|
|
58
|
+
return next();
|
|
59
|
+
const current = files();
|
|
60
|
+
const [text, type] = pathname === sitemapPath
|
|
61
|
+
? [current.sitemap, "application/xml; charset=utf-8"]
|
|
62
|
+
: [current.robots, "text/plain; charset=utf-8"];
|
|
63
|
+
if (text === undefined)
|
|
64
|
+
return next();
|
|
65
|
+
res.setHeader("Content-Type", type);
|
|
66
|
+
res.setHeader("Content-Length", Buffer.byteLength(text));
|
|
67
|
+
res.end(req.method === "HEAD" ? undefined : text);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The site's own pages `astro dev` can name: each route of the site's own
|
|
72
|
+
* with no parameters, other than the not-found and error pages, by its route
|
|
73
|
+
* from the host's root. A page a route's `getStaticPaths` lists is known only
|
|
74
|
+
* to a build.
|
|
75
|
+
*/
|
|
76
|
+
export function devSitePages(routes, astro) {
|
|
77
|
+
const root = normalizeBase(astro.base);
|
|
78
|
+
return routes
|
|
79
|
+
.filter((route) => route.type === "page" &&
|
|
80
|
+
route.origin === "project" &&
|
|
81
|
+
route.params.length === 0 &&
|
|
82
|
+
route.pattern !== "/404" &&
|
|
83
|
+
route.pattern !== "/500")
|
|
84
|
+
.map((route) => routeAs(`${root}${route.pattern.replace(/^\//, "")}`, astro.trailingSlash));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether a project's page, by its route from the host's root, has a route
|
|
88
|
+
* in the site in `astro dev`: whether one of the site's own routes matches
|
|
89
|
+
* it. A project no route shows, such as one whose collection a page only
|
|
90
|
+
* quotes from, has no pages in the sitemap.
|
|
91
|
+
*/
|
|
92
|
+
export function devRouted(routes, astro) {
|
|
93
|
+
const root = normalizeBase(astro.base);
|
|
94
|
+
const pages = routes.filter((route) => route.type === "page" && route.origin === "project");
|
|
95
|
+
return (route) => {
|
|
96
|
+
if (!`${route}/`.startsWith(root))
|
|
97
|
+
return false;
|
|
98
|
+
const under = `/${route.slice(root.length)}`;
|
|
99
|
+
const forms = new Set([under, routeKey(under), `${routeKey(under)}/`]);
|
|
100
|
+
return pages.some((page) => [...forms].some((form) => page.patternRegex.test(form)));
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=root-files.js.map
|