@ascribed/astro 0.2.1-next.12 → 0.2.1-next.14

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The [Astro](https://astro.build) integration for [Ascribe](https://github.com/ascribed-dev/ascribe), for Astro 7.3.5 and later 7.x releases.
4
4
 
5
- It runs `ascribe build --emit site` before Astro loads content, gives you a content collection over the output with a schema generated from `ascribe.toml`, applies heading ids and image attributes in Astro's own Markdown pipeline (so Astro keeps its heading, table-of-contents, and image handling), loads the element library, and serves the files pages link to. In `astro dev`, it rebuilds as you edit.
5
+ It runs `ascribe build --emit site` before Astro loads content, gives you a content collection over the output with a schema generated from `ascribe.toml`, applies heading ids and image attributes in Astro's own Markdown pipeline (so Astro keeps its heading, table-of-contents, and image handling), loads the element library, serves the files pages link to, and gives each permalink an address that forwards to its heading. In `astro dev`, it rebuilds as you edit.
6
6
 
7
7
  ```sh
8
8
  npm install @ascribed/astro
@@ -35,6 +35,18 @@ export const collections = { docs: defineCollection(ascribeCollection({ schema }
35
35
 
36
36
  If the Ascribe project is outside the Astro root (`project: "../docs"`), import `../.astro/integrations/_ascribed_astro/schema.ts` instead: the integration copies the schema there after each build, so TypeScript finds `astro/zod` from it.
37
37
 
38
+ `[consumer] base-path` can be Astro's `base` or a path under it, so a home page can sit at `/` in front of docs at `/docs/`. Several projects can share one site, each under its own base path:
39
+
40
+ ```js
41
+ ascribe({
42
+ projects: [
43
+ { name: "docs", project: "../docs", build: "site" },
44
+ { name: "home", project: "home", build: "site" },
45
+ ],
46
+ review: "docs",
47
+ });
48
+ ```
49
+
38
50
  ```astro
39
51
  ---
40
52
  // A layout
@@ -48,17 +60,18 @@ const { entry } = Astro.props;
48
60
  <h1 set:html={entry.data.formatted.title} />
49
61
  ```
50
62
 
51
- The [Astro guide](https://ascribed-dev.com/guides/astro/) walks through a site: `ascribe.toml`, the collection, the route, the layout, the options, what `astro dev` does, and [review in the site preview](https://ascribed-dev.com/guides/astro/#review-in-the-site-preview). [Review](https://ascribed-dev.com/guides/review/) walks through reviewing a pull request on your site.
63
+ The [Astro guide](https://ascribed-dev.com/guides/astro/) walks through a site: `ascribe.toml`, the collection, the route, the layout, the options, [a home page in front of the docs](https://ascribed-dev.com/guides/astro/#a-home-page-in-front-of-the-docs), [several projects in one site](https://ascribed-dev.com/guides/astro/#several-projects-in-one-site), what `astro dev` does, and [review in the site preview](https://ascribed-dev.com/guides/astro/#review-in-the-site-preview). [Review](https://ascribed-dev.com/guides/review/) walks through reviewing a pull request on your site.
52
64
 
53
65
  ## Options
54
66
 
55
67
  | Option | Meaning |
56
68
  |---|---|
57
- | `build` | The build whose site output is the collection: a build name in `ascribe.toml`. Required. |
69
+ | `build` | The build whose site output is the collection: a build name in `ascribe.toml`. Required, unless `projects` is given. |
58
70
  | `project` | The directory holding `ascribe.toml`, relative to the Astro root. By default, the root. |
71
+ | `projects` | Several projects in one site, in place of `build` and `project`: a list of entries, each `{ name, build, project }`. Each collection names its entry, `ascribeCollection({ schema, name })`, and each entry's schema is copied to `.astro/integrations/_ascribed_astro/<name>/schema.ts`. |
59
72
  | `binary` | The `ascribe` binary, relative to the Astro root. By default, `ASCRIBE_BIN`, then the binary `@ascribed/cli` installed. |
60
73
  | `anchors` | Source anchors (`ascribe build --anchors`), for review: `"dev"` in `astro dev` only, `true` always. By default, `false`, though `review` turns them on in `astro dev`. |
61
- | `review` | The **Ascribe review** app in `astro dev`'s toolbar: a pull request's changes and comments on the real page. `false` leaves it out. By default, `true`. Never in `astro build`. |
74
+ | `review` | The **Ascribe review** app in `astro dev`'s toolbar: a pull request's changes and comments on the real page. `false` leaves it out. By default, `true`. Never in `astro build`. With `projects`, an entry's name reviews that entry, and `true` the first. |
62
75
  | `codeTitles` | A code block's title (`title="…"` in its info string, as `@snippet` writes it) above it: a Shiki transformer puts the block in a `<figure class="code-title">` with a `<figcaption>`. `false` leaves it out. By default, `true`. |
63
76
 
64
77
  ## Development
@@ -70,10 +83,13 @@ The [Astro guide](https://ascribed-dev.com/guides/astro/) walks through a site:
70
83
  | `src/Elements.astro` | Loads `@ascribed/elements` (stylesheet and script) |
71
84
  | `src/Availability.astro` | A page's availability as the element library's badge |
72
85
  | `src/code-titles.ts` | The Shiki transformer that shows code block titles |
86
+ | `src/Permalink.astro`, `src/permalinks.ts` | The route at each permalink's address, and its pages, from the site output's `_ascribe/permalinks.json` |
87
+ | `src/MissingPermalink.astro` | The explanation on a site's 404 page for an address under the permalinks' path that matches nothing |
73
88
  | `src/schema.ts` | Copies the generated schema inside the Astro root |
74
89
  | `src/attributes.ts` | The attribute-marker and source-anchor rules, on a hast tree |
75
90
  | `src/rehype.ts`, `src/satteri.ts` | The rules applied in each of Astro's Markdown processors |
76
91
  | `src/project.ts`, `src/binary.ts`, `src/run.ts`, `src/files.ts`, `src/dev.ts` | `ascribe.toml`, the binary, running it, serving files, dev rebuilds |
92
+ | `src/projects.ts` | Several projects in one site: the entries' names, and what the projects can't share |
77
93
  | `src/review/` | Review in the site preview, on the dev server's side: `ascribe diff`, the pull request (through `@ascribed/review/github` and `gh`), the routes, `.ascribe/dev.json`, and the toolbar channel's requests (`server.ts`, `protocol.ts`) |
78
94
  | `src/toolbar/` | The **Ascribe review** toolbar app, in the page: the panel, and the `@ascribed/review` overlay on the page's blocks |
79
95
 
package/dist/content.d.ts CHANGED
@@ -2,6 +2,8 @@ import { glob } from "astro/loaders";
2
2
  /**
3
3
  * The collection's loader and schema: the `glob` loader on the site output's
4
4
  * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).
5
+ * With several projects (`ascribe({ projects })`), `name` says which entry's
6
+ * site output; it can be left out when there's one entry.
5
7
  *
6
8
  * Entry ids are Astro's own (`guides/my-setup` for `Guides/My Setup.md`, `index`
7
9
  * for the root page), which is what `AstroRouter` in `ascribe-resolve`
@@ -10,6 +12,7 @@ import { glob } from "astro/loaders";
10
12
  */
11
13
  export declare function ascribeCollection<S>(options: {
12
14
  schema: S;
15
+ name?: string;
13
16
  }): {
14
17
  loader: ReturnType<typeof glob>;
15
18
  schema: S;
package/dist/content.js CHANGED
@@ -6,13 +6,22 @@
6
6
  // import { ascribeCollection } from "@ascribed/astro/content";
7
7
  // import { schema } from "../.ascribe/build/site/site/_ascribe/schema.ts";
8
8
  // export const collections = { docs: defineCollection(ascribeCollection({ schema })) };
9
+ //
10
+ // With several projects, each collection names its entry:
11
+ //
12
+ // import { schema as docs } from "../.astro/integrations/_ascribed_astro/docs/schema.ts";
13
+ // export const collections = {
14
+ // docs: defineCollection(ascribeCollection({ schema: docs, name: "docs" })),
15
+ // };
9
16
  import { pathToFileURL } from "node:url";
10
17
  import path from "node:path";
11
18
  import { glob } from "astro/loaders";
12
- import { siteRoot } from "virtual:ascribe/site";
19
+ import { collections, siteRoot } from "virtual:ascribe/site";
13
20
  /**
14
21
  * The collection's loader and schema: the `glob` loader on the site output's
15
22
  * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).
23
+ * With several projects (`ascribe({ projects })`), `name` says which entry's
24
+ * site output; it can be left out when there's one entry.
16
25
  *
17
26
  * Entry ids are Astro's own (`guides/my-setup` for `Guides/My Setup.md`, `index`
18
27
  * for the root page), which is what `AstroRouter` in `ascribe-resolve`
@@ -21,7 +30,27 @@ import { siteRoot } from "virtual:ascribe/site";
21
30
  */
22
31
  export function ascribeCollection(options) {
23
32
  // `_ascribe/` holds the schema and published files, never pages.
24
- const base = pathToFileURL(siteRoot + path.sep);
33
+ const base = pathToFileURL(rootFor(options.name) + path.sep);
25
34
  return { loader: glob({ pattern: ["**/*.md", "!_ascribe/**"], base }), schema: options.schema };
26
35
  }
36
+ /** The site output root of the entry `name` names. */
37
+ function rootFor(name) {
38
+ const names = Object.keys(collections);
39
+ if (names.length === 0) {
40
+ if (name !== undefined) {
41
+ throw new Error(`@ascribed/astro: ascribeCollection was given the name "${name}", but ascribe() was given one project, which has no name. Leave out \`name\`, or list the project in \`projects\`.`);
42
+ }
43
+ return siteRoot;
44
+ }
45
+ if (name === undefined) {
46
+ if (names.length === 1)
47
+ return collections[names[0]];
48
+ throw new Error(`@ascribed/astro: ascribeCollection needs a \`name\` when ascribe() has several projects: one of ${names.map((n) => `"${n}"`).join(", ")}.`);
49
+ }
50
+ const root = collections[name];
51
+ if (root === undefined) {
52
+ throw new Error(`@ascribed/astro: ascribeCollection was given the name "${name}", and ascribe()'s projects are ${names.map((n) => `"${n}"`).join(", ")}.`);
53
+ }
54
+ return root;
55
+ }
27
56
  //# sourceMappingURL=content.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,6EAA6E;AAC7E,2EAA2E;AAC3E,EAAE;AACF,sDAAsD;AACtD,iEAAiE;AACjE,6EAA6E;AAC7E,0FAA0F;AAC1F,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEhD;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAI,OAAsB;IAIzD,iEAAiE;IACjE,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IAChD,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,SAAS,EAAE,cAAc,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;AAClG,CAAC","sourcesContent":["// The content collection over a build's site output. Import it from\n// `content.config.ts`, which Vite loads, so it can read what the integration\n// resolved (`virtual:ascribe/site`) instead of finding the project itself.\n//\n// import { defineCollection } from \"astro:content\";\n// import { ascribeCollection } from \"@ascribed/astro/content\";\n// import { schema } from \"../.ascribe/build/site/site/_ascribe/schema.ts\";\n// export const collections = { docs: defineCollection(ascribeCollection({ schema })) };\nimport { pathToFileURL } from \"node:url\";\nimport path from \"node:path\";\nimport { glob } from \"astro/loaders\";\nimport { siteRoot } from \"virtual:ascribe/site\";\n\n/**\n * The collection's loader and schema: the `glob` loader on the site output's\n * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).\n *\n * Entry ids are Astro's own (`guides/my-setup` for `Guides/My Setup.md`, `index`\n * for the root page), which is what `AstroRouter` in `ascribe-resolve`\n * computes, so a page's route is the base path plus its id, and the root page is\n * at the base path itself.\n */\nexport function ascribeCollection<S>(options: { schema: S }): {\n loader: ReturnType<typeof glob>;\n schema: S;\n} {\n // `_ascribe/` holds the schema and published files, never pages.\n const base = pathToFileURL(siteRoot + path.sep);\n return { loader: glob({ pattern: [\"**/*.md\", \"!_ascribe/**\"], base }), schema: options.schema };\n}\n"]}
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,6EAA6E;AAC7E,2EAA2E;AAC3E,EAAE;AACF,sDAAsD;AACtD,iEAAiE;AACjE,6EAA6E;AAC7E,0FAA0F;AAC1F,EAAE;AACF,0DAA0D;AAC1D,EAAE;AACF,4FAA4F;AAC5F,iCAAiC;AACjC,iFAAiF;AACjF,OAAO;AACP,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAI,OAAqC;IAIxE,iEAAiE;IACjE,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7D,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,SAAS,EAAE,cAAc,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;AAClG,CAAC;AAED,sDAAsD;AACtD,SAAS,OAAO,CAAC,IAAwB;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACb,0DAA0D,IAAI,qHAAqH,CACpL,CAAC;QACJ,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAW,CAAW,CAAC;QACzE,MAAM,IAAI,KAAK,CACb,mGAAmG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5I,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,0DAA0D,IAAI,mCAAmC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC1I,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["// The content collection over a build's site output. Import it from\n// `content.config.ts`, which Vite loads, so it can read what the integration\n// resolved (`virtual:ascribe/site`) instead of finding the project itself.\n//\n// import { defineCollection } from \"astro:content\";\n// import { ascribeCollection } from \"@ascribed/astro/content\";\n// import { schema } from \"../.ascribe/build/site/site/_ascribe/schema.ts\";\n// export const collections = { docs: defineCollection(ascribeCollection({ schema })) };\n//\n// With several projects, each collection names its entry:\n//\n// import { schema as docs } from \"../.astro/integrations/_ascribed_astro/docs/schema.ts\";\n// export const collections = {\n// docs: defineCollection(ascribeCollection({ schema: docs, name: \"docs\" })),\n// };\nimport { pathToFileURL } from \"node:url\";\nimport path from \"node:path\";\nimport { glob } from \"astro/loaders\";\nimport { collections, siteRoot } from \"virtual:ascribe/site\";\n\n/**\n * The collection's loader and schema: the `glob` loader on the site output's\n * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).\n * With several projects (`ascribe({ projects })`), `name` says which entry's\n * site output; it can be left out when there's one entry.\n *\n * Entry ids are Astro's own (`guides/my-setup` for `Guides/My Setup.md`, `index`\n * for the root page), which is what `AstroRouter` in `ascribe-resolve`\n * computes, so a page's route is the base path plus its id, and the root page is\n * at the base path itself.\n */\nexport function ascribeCollection<S>(options: { schema: S; name?: string }): {\n loader: ReturnType<typeof glob>;\n schema: S;\n} {\n // `_ascribe/` holds the schema and published files, never pages.\n const base = pathToFileURL(rootFor(options.name) + path.sep);\n return { loader: glob({ pattern: [\"**/*.md\", \"!_ascribe/**\"], base }), schema: options.schema };\n}\n\n/** The site output root of the entry `name` names. */\nfunction rootFor(name: string | undefined): string {\n const names = Object.keys(collections);\n if (names.length === 0) {\n if (name !== undefined) {\n throw new Error(\n `@ascribed/astro: ascribeCollection was given the name \"${name}\", but ascribe() was given one project, which has no name. Leave out \\`name\\`, or list the project in \\`projects\\`.`,\n );\n }\n return siteRoot;\n }\n if (name === undefined) {\n if (names.length === 1) return collections[names[0] as string] as string;\n throw new Error(\n `@ascribed/astro: ascribeCollection needs a \\`name\\` when ascribe() has several projects: one of ${names.map((n) => `\"${n}\"`).join(\", \")}.`,\n );\n }\n const root = collections[name];\n if (root === undefined) {\n throw new Error(\n `@ascribed/astro: ascribeCollection was given the name \"${name}\", and ascribe()'s projects are ${names.map((n) => `\"${n}\"`).join(\", \")}.`,\n );\n }\n return root;\n}\n"]}
package/dist/dev.d.ts CHANGED
@@ -38,7 +38,8 @@ export declare function watchDev(options: {
38
38
  trailingSlash: string;
39
39
  site: string | undefined;
40
40
  };
41
- build: string;
41
+ /** The project's builds the site shows: one, or more when entries share the project. */
42
+ builds: readonly string[];
42
43
  rebuild: () => Promise<void>;
43
44
  /** Called after each successful rebuild, before Astro reloads the page. */
44
45
  onBuilt?: () => void;
package/dist/dev.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { permalinksText } from "./permalinks.js";
3
4
  import { consumerMismatches, readProject } from "./project.js";
4
5
  /**
5
6
  * The assets a build copied from outside the content root, as absolute paths,
@@ -88,7 +89,7 @@ export function watchDev(options) {
88
89
  let assets = new Set();
89
90
  const seen = options.seen ?? new SeenFiles();
90
91
  const watchAssets = () => {
91
- assets = new Set(outsideAssets(project, options.build));
92
+ assets = new Set(options.builds.flatMap((build) => outsideAssets(project, build)));
92
93
  if (assets.size > 0)
93
94
  server.watcher.add([...assets]);
94
95
  seen.prime([...assets]);
@@ -96,7 +97,9 @@ export function watchDev(options) {
96
97
  const drain = async () => {
97
98
  running = true;
98
99
  const previousProject = project;
99
- const previousSiteRoot = previousProject.siteRoot(options.build);
100
+ const previousSiteRoots = options.builds.map((build) => previousProject.siteRoot(build));
101
+ const permalinkTexts = () => previousSiteRoots.map((root) => permalinksText(root) ?? "");
102
+ const previousPermalinks = permalinkTexts().join("\0");
100
103
  while (dirty) {
101
104
  dirty = false;
102
105
  try {
@@ -112,7 +115,7 @@ export function watchDev(options) {
112
115
  options.onBuilt?.();
113
116
  try {
114
117
  const reloaded = readProject(project.dir);
115
- if (reloaded.siteRoot(options.build) !== previousSiteRoot) {
118
+ if (options.builds.some((build, i) => reloaded.siteRoot(build) !== previousSiteRoots[i])) {
116
119
  throw new Error("ascribe.toml changed output-dir; restart astro dev to reload the collection.");
117
120
  }
118
121
  const mismatches = consumerMismatches(reloaded, options.astro);
@@ -124,6 +127,13 @@ export function watchDev(options) {
124
127
  server.watcher.add(project.configPath);
125
128
  watchAssets();
126
129
  await refreshContent();
130
+ // Astro keeps each route's `getStaticPaths` until content changes, and adding or removing a
131
+ // permalink may change no page: clear it, as Astro does, so the addresses follow the list.
132
+ if (permalinkTexts().join("\0") !== previousPermalinks) {
133
+ for (const name of ["ssr", "prerender"]) {
134
+ server.environments[name]?.hot.send("astro:content-changed", {});
135
+ }
136
+ }
127
137
  server.ws.send({ type: "full-reload" });
128
138
  }
129
139
  catch (error) {
package/dist/dev.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"dev.js","sourceRoot":"","sources":["../src/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { AstroIntegration } from \"astro\";\nimport { consumerMismatches, readProject, type ProjectInfo } from \"./project.js\";\n\ntype ServerOptions = Parameters<NonNullable<AstroIntegration[\"hooks\"][\"astro:server:setup\"]>>[0];\n\n/**\n * The assets a build copied from outside the content root, as absolute paths,\n * read from the site output's manifest. Watching the content root doesn't\n * cover them. A manifest that's missing or unreadable gives none: the next\n * successful build writes one.\n */\nexport function outsideAssets(project: ProjectInfo, build: string): string[] {\n const manifest = path.join(path.dirname(project.siteRoot(build)), \"site.manifest.json\");\n let files: unknown;\n try {\n files = (JSON.parse(readFileSync(manifest, \"utf8\")) as { files?: unknown }).files;\n } catch {\n return [];\n }\n if (!Array.isArray(files)) return [];\n const assets = new Set<string>();\n for (const entry of files as { kind?: unknown; source?: unknown }[]) {\n if (entry.kind !== \"asset\" || typeof entry.source !== \"string\") continue;\n const absolute = path.resolve(project.contentRoot, entry.source);\n if (!absolute.startsWith(project.contentRoot + path.sep)) assets.add(absolute);\n }\n return [...assets].sort();\n}\n\n/**\n * Each watched file's size and modification time, as last seen. A watcher\n * can report an edit late, after a build already read it: macOS delivers\n * edits made just before the dev server started some seconds after it\n * started. An event that changes neither is no edit, and rebuilding for it\n * would reload the page under the reader.\n */\nexport class SeenFiles {\n private readonly stamps = new Map<string, string>();\n\n /** Records the files under `paths` (files or folders) as they are now. */\n prime(paths: readonly string[]): void {\n for (const at of paths) {\n let entries: string[];\n try {\n entries = statSync(at).isDirectory()\n ? readdirSync(at, { recursive: true, encoding: \"utf8\" }).map((f) => path.join(at, f))\n : [at];\n } catch {\n continue;\n }\n for (const file of entries) {\n const stamp = stampOf(file);\n if (stamp !== undefined && !this.stamps.has(file)) this.stamps.set(file, stamp);\n }\n }\n }\n\n /** Whether `file` differs from when it was last seen (or wasn't seen), and records it. */\n changed(file: string): boolean {\n const stamp = stampOf(file);\n const before = this.stamps.get(file);\n if (stamp === undefined) this.stamps.delete(file);\n else this.stamps.set(file, stamp);\n return stamp === undefined || stamp !== before;\n }\n}\n\nfunction stampOf(file: string): string | undefined {\n try {\n const stat = statSync(file);\n return stat.isFile() ? `${stat.size}:${stat.mtimeMs}` : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** What `watchDev` gives back. */\nexport interface DevBuilds {\n /** Runs `task` in turn with the compiler runs, never during one. */\n inTurn(task: () => Promise<void>): Promise<void>;\n}\n\n/** Watch source files, serialize compiler runs, and refresh Astro after successful output. */\nexport function watchDev(options: {\n server: ServerOptions[\"server\"];\n refreshContent: () => Promise<void>;\n logger: ServerOptions[\"logger\"];\n project: ProjectInfo;\n astro: { base: string; trailingSlash: string; site: string | undefined };\n build: string;\n rebuild: () => Promise<void>;\n /** Called after each successful rebuild, before Astro reloads the page. */\n onBuilt?: () => void;\n /** The sources as the first build read them. */\n seen?: SeenFiles;\n}): DevBuilds {\n const { server, refreshContent, logger, rebuild } = options;\n let project = options.project;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let pending: Promise<void> = Promise.resolve();\n let dirty = false;\n let running = false;\n let failed = false;\n let assets = new Set<string>();\n const seen = options.seen ?? new SeenFiles();\n const watchAssets = () => {\n assets = new Set(outsideAssets(project, options.build));\n if (assets.size > 0) server.watcher.add([...assets]);\n seen.prime([...assets]);\n };\n\n const drain = async () => {\n running = true;\n const previousProject = project;\n const previousSiteRoot = previousProject.siteRoot(options.build);\n while (dirty) {\n dirty = false;\n try {\n await rebuild();\n failed = false;\n } catch (error) {\n failed = true;\n logger.error(error instanceof Error ? error.message : String(error));\n }\n }\n if (!failed) {\n options.onBuilt?.();\n try {\n const reloaded = readProject(project.dir);\n if (reloaded.siteRoot(options.build) !== previousSiteRoot) {\n throw new Error(\n \"ascribe.toml changed output-dir; restart astro dev to reload the collection.\",\n );\n }\n const mismatches = consumerMismatches(reloaded, options.astro);\n if (mismatches.length > 0) {\n throw new Error(\n `@ascribed/astro: ascribe.toml and astro.config disagree:\\n- ${mismatches.join(\"\\n- \")}`,\n );\n }\n project = reloaded;\n server.watcher.add(project.contentRoot);\n server.watcher.add(project.configPath);\n watchAssets();\n await refreshContent();\n server.ws.send({ type: \"full-reload\" });\n } catch (error) {\n failed = true;\n logger.error(\n `could not refresh Ascribe content: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n running = false;\n };\n const schedule = () => {\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = undefined;\n pending = pending.then(drain);\n }, 50);\n };\n const onChange = (event: string, file: string) => {\n if (![\"add\", \"change\", \"unlink\"].includes(event)) return;\n const absolute = path.resolve(file);\n const source =\n absolute === project.configPath ||\n assets.has(absolute) ||\n (absolute.startsWith(project.contentRoot + path.sep) &&\n !absolute.startsWith(project.outputRoot + path.sep));\n if (!source) return;\n // A late report of an edit the last build already had.\n if (event !== \"unlink\" && !seen.changed(absolute)) return;\n dirty = true;\n schedule();\n };\n server.watcher.add(project.contentRoot);\n server.watcher.add(project.configPath);\n seen.prime([project.contentRoot, project.configPath]);\n watchAssets();\n server.watcher.on(\"all\", onChange);\n server.httpServer?.once(\"close\", () => {\n if (timer) clearTimeout(timer);\n server.watcher.off(\"all\", onChange);\n });\n server.middlewares.use((_req, res, next) => {\n if (!dirty && !running && !failed) return next();\n if (timer) {\n clearTimeout(timer);\n timer = undefined;\n pending = pending.then(drain);\n }\n void pending.then(() => {\n if (failed) {\n res.statusCode = 503;\n res.end(\"Ascribe build failed; fix the source and save to retry.\");\n } else next();\n });\n });\n return {\n inTurn: (task) => {\n const run = pending.then(task);\n pending = run.catch(() => undefined);\n return run;\n },\n };\n}\n"]}
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../src/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { AstroIntegration } from \"astro\";\nimport { permalinksText } from \"./permalinks.js\";\nimport { consumerMismatches, readProject, type ProjectInfo } from \"./project.js\";\n\ntype ServerOptions = Parameters<NonNullable<AstroIntegration[\"hooks\"][\"astro:server:setup\"]>>[0];\n\n/**\n * The assets a build copied from outside the content root, as absolute paths,\n * read from the site output's manifest. Watching the content root doesn't\n * cover them. A manifest that's missing or unreadable gives none: the next\n * successful build writes one.\n */\nexport function outsideAssets(project: ProjectInfo, build: string): string[] {\n const manifest = path.join(path.dirname(project.siteRoot(build)), \"site.manifest.json\");\n let files: unknown;\n try {\n files = (JSON.parse(readFileSync(manifest, \"utf8\")) as { files?: unknown }).files;\n } catch {\n return [];\n }\n if (!Array.isArray(files)) return [];\n const assets = new Set<string>();\n for (const entry of files as { kind?: unknown; source?: unknown }[]) {\n if (entry.kind !== \"asset\" || typeof entry.source !== \"string\") continue;\n const absolute = path.resolve(project.contentRoot, entry.source);\n if (!absolute.startsWith(project.contentRoot + path.sep)) assets.add(absolute);\n }\n return [...assets].sort();\n}\n\n/**\n * Each watched file's size and modification time, as last seen. A watcher\n * can report an edit late, after a build already read it: macOS delivers\n * edits made just before the dev server started some seconds after it\n * started. An event that changes neither is no edit, and rebuilding for it\n * would reload the page under the reader.\n */\nexport class SeenFiles {\n private readonly stamps = new Map<string, string>();\n\n /** Records the files under `paths` (files or folders) as they are now. */\n prime(paths: readonly string[]): void {\n for (const at of paths) {\n let entries: string[];\n try {\n entries = statSync(at).isDirectory()\n ? readdirSync(at, { recursive: true, encoding: \"utf8\" }).map((f) => path.join(at, f))\n : [at];\n } catch {\n continue;\n }\n for (const file of entries) {\n const stamp = stampOf(file);\n if (stamp !== undefined && !this.stamps.has(file)) this.stamps.set(file, stamp);\n }\n }\n }\n\n /** Whether `file` differs from when it was last seen (or wasn't seen), and records it. */\n changed(file: string): boolean {\n const stamp = stampOf(file);\n const before = this.stamps.get(file);\n if (stamp === undefined) this.stamps.delete(file);\n else this.stamps.set(file, stamp);\n return stamp === undefined || stamp !== before;\n }\n}\n\nfunction stampOf(file: string): string | undefined {\n try {\n const stat = statSync(file);\n return stat.isFile() ? `${stat.size}:${stat.mtimeMs}` : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** What `watchDev` gives back. */\nexport interface DevBuilds {\n /** Runs `task` in turn with the compiler runs, never during one. */\n inTurn(task: () => Promise<void>): Promise<void>;\n}\n\n/** Watch source files, serialize compiler runs, and refresh Astro after successful output. */\nexport function watchDev(options: {\n server: ServerOptions[\"server\"];\n refreshContent: () => Promise<void>;\n logger: ServerOptions[\"logger\"];\n project: ProjectInfo;\n astro: { base: string; trailingSlash: string; site: string | undefined };\n /** The project's builds the site shows: one, or more when entries share the project. */\n builds: readonly string[];\n rebuild: () => Promise<void>;\n /** Called after each successful rebuild, before Astro reloads the page. */\n onBuilt?: () => void;\n /** The sources as the first build read them. */\n seen?: SeenFiles;\n}): DevBuilds {\n const { server, refreshContent, logger, rebuild } = options;\n let project = options.project;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let pending: Promise<void> = Promise.resolve();\n let dirty = false;\n let running = false;\n let failed = false;\n let assets = new Set<string>();\n const seen = options.seen ?? new SeenFiles();\n const watchAssets = () => {\n assets = new Set(options.builds.flatMap((build) => outsideAssets(project, build)));\n if (assets.size > 0) server.watcher.add([...assets]);\n seen.prime([...assets]);\n };\n\n const drain = async () => {\n running = true;\n const previousProject = project;\n const previousSiteRoots = options.builds.map((build) => previousProject.siteRoot(build));\n const permalinkTexts = () => previousSiteRoots.map((root) => permalinksText(root) ?? \"\");\n const previousPermalinks = permalinkTexts().join(\"\\0\");\n while (dirty) {\n dirty = false;\n try {\n await rebuild();\n failed = false;\n } catch (error) {\n failed = true;\n logger.error(error instanceof Error ? error.message : String(error));\n }\n }\n if (!failed) {\n options.onBuilt?.();\n try {\n const reloaded = readProject(project.dir);\n if (options.builds.some((build, i) => reloaded.siteRoot(build) !== previousSiteRoots[i])) {\n throw new Error(\n \"ascribe.toml changed output-dir; restart astro dev to reload the collection.\",\n );\n }\n const mismatches = consumerMismatches(reloaded, options.astro);\n if (mismatches.length > 0) {\n throw new Error(\n `@ascribed/astro: ascribe.toml and astro.config disagree:\\n- ${mismatches.join(\"\\n- \")}`,\n );\n }\n project = reloaded;\n server.watcher.add(project.contentRoot);\n server.watcher.add(project.configPath);\n watchAssets();\n await refreshContent();\n // Astro keeps each route's `getStaticPaths` until content changes, and adding or removing a\n // permalink may change no page: clear it, as Astro does, so the addresses follow the list.\n if (permalinkTexts().join(\"\\0\") !== previousPermalinks) {\n for (const name of [\"ssr\", \"prerender\"]) {\n server.environments[name]?.hot.send(\"astro:content-changed\", {});\n }\n }\n server.ws.send({ type: \"full-reload\" });\n } catch (error) {\n failed = true;\n logger.error(\n `could not refresh Ascribe content: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n running = false;\n };\n const schedule = () => {\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = undefined;\n pending = pending.then(drain);\n }, 50);\n };\n const onChange = (event: string, file: string) => {\n if (![\"add\", \"change\", \"unlink\"].includes(event)) return;\n const absolute = path.resolve(file);\n const source =\n absolute === project.configPath ||\n assets.has(absolute) ||\n (absolute.startsWith(project.contentRoot + path.sep) &&\n !absolute.startsWith(project.outputRoot + path.sep));\n if (!source) return;\n // A late report of an edit the last build already had.\n if (event !== \"unlink\" && !seen.changed(absolute)) return;\n dirty = true;\n schedule();\n };\n server.watcher.add(project.contentRoot);\n server.watcher.add(project.configPath);\n seen.prime([project.contentRoot, project.configPath]);\n watchAssets();\n server.watcher.on(\"all\", onChange);\n server.httpServer?.once(\"close\", () => {\n if (timer) clearTimeout(timer);\n server.watcher.off(\"all\", onChange);\n });\n server.middlewares.use((_req, res, next) => {\n if (!dirty && !running && !failed) return next();\n if (timer) {\n clearTimeout(timer);\n timer = undefined;\n pending = pending.then(drain);\n }\n void pending.then(() => {\n if (failed) {\n res.statusCode = 503;\n res.end(\"Ascribe build failed; fix the source and save to retry.\");\n } else next();\n });\n });\n return {\n inTurn: (task) => {\n const run = pending.then(task);\n pending = run.catch(() => undefined);\n return run;\n },\n };\n}\n"]}
package/dist/files.d.ts CHANGED
@@ -1,10 +1,18 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- /** Copies the published files to `<outDir>/_ascribe/files/`. Returns whether there were any. */
3
- export declare function copyPublishedFiles(siteRoot: string, outDir: URL): Promise<boolean>;
4
- /** Copies the plain output for agents to the build output's root, the base path. Returns whether there was one. */
5
- export declare function copyAgentFiles(plainRoot: string, outDir: URL): Promise<boolean>;
2
+ /**
3
+ * The base path's folder in Astro's build output, which is at Astro's `base`:
4
+ * `docs/` for a base path of `/docs/` under a `base` of `/`, `""` when they're the same.
5
+ */
6
+ export declare function folderUnder(basePath: string, astroBase: string): string;
7
+ /**
8
+ * Copies the published files to `<outDir>/<folder>_ascribe/files/`, `folder` being the
9
+ * base path's (`folderUnder`). Returns whether there were any.
10
+ */
11
+ export declare function copyPublishedFiles(siteRoot: string, outDir: URL, folder?: string): Promise<boolean>;
12
+ /** Copies the plain output for agents to the base path's folder of the build output. Returns whether there was one. */
13
+ export declare function copyAgentFiles(plainRoot: string, outDir: URL, folder?: string): Promise<boolean>;
6
14
  type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
7
- /** A dev-server middleware serving the published files at `<base>_ascribe/files/`. */
15
+ /** A dev-server middleware serving the published files at `<base>_ascribe/files/`, `base` being the project's base path. */
8
16
  export declare function filesMiddleware(siteRoot: string, base: string): Middleware;
9
17
  /** A dev-server middleware serving the plain output for agents at `<base>`: `llms.txt`, each page's `.md`, and their files. */
10
18
  export declare function agentsMiddleware(plainRoot: string, base: string): Middleware;
package/dist/files.js CHANGED
@@ -1,26 +1,40 @@
1
1
  // Serving `_ascribe/files/`: the files pages link to that aren't pages or
2
2
  // images. Astro doesn't copy them, so the integration
3
3
  // does: into the build's output directory, and in dev through a middleware.
4
- // Both put them at `<base>_ascribe/files/`, the URL the site output links to.
4
+ // Both put them at `<base-path>_ascribe/files/`, the URL the site output links
5
+ // to, where `<base-path>` is the project's `[consumer] base-path`: Astro's
6
+ // `base` or a path under it.
5
7
  //
6
8
  // With `[consumer] agents = true`, the same for the plain output, which is
7
9
  // laid out by URL: `llms.txt`, each page's Markdown, and their files, all at
8
- // `<base>` and below.
10
+ // `<base-path>` and below.
9
11
  import { createReadStream } from "node:fs";
10
12
  import { cp, stat } from "node:fs/promises";
11
13
  import path from "node:path";
12
14
  import { fileURLToPath } from "node:url";
15
+ import { normalizeBase } from "./project.js";
13
16
  /** The directory of published files inside a site output root. */
14
17
  function publishedDir(siteRoot) {
15
18
  return path.join(siteRoot, "_ascribe", "files");
16
19
  }
17
- /** Copies the published files to `<outDir>/_ascribe/files/`. Returns whether there were any. */
18
- export async function copyPublishedFiles(siteRoot, outDir) {
19
- return copyDirectory(publishedDir(siteRoot), path.join(fileURLToPath(outDir), "_ascribe", "files"));
20
+ /**
21
+ * The base path's folder in Astro's build output, which is at Astro's `base`:
22
+ * `docs/` for a base path of `/docs/` under a `base` of `/`, `""` when they're the same.
23
+ */
24
+ export function folderUnder(basePath, astroBase) {
25
+ const root = normalizeBase(astroBase);
26
+ return basePath.startsWith(root) ? basePath.slice(root.length) : "";
20
27
  }
21
- /** Copies the plain output for agents to the build output's root, the base path. Returns whether there was one. */
22
- export async function copyAgentFiles(plainRoot, outDir) {
23
- return copyDirectory(plainRoot, fileURLToPath(outDir));
28
+ /**
29
+ * Copies the published files to `<outDir>/<folder>_ascribe/files/`, `folder` being the
30
+ * base path's (`folderUnder`). Returns whether there were any.
31
+ */
32
+ export async function copyPublishedFiles(siteRoot, outDir, folder = "") {
33
+ return copyDirectory(publishedDir(siteRoot), path.join(fileURLToPath(outDir), folder, "_ascribe", "files"));
34
+ }
35
+ /** Copies the plain output for agents to the base path's folder of the build output. Returns whether there was one. */
36
+ export async function copyAgentFiles(plainRoot, outDir, folder = "") {
37
+ return copyDirectory(plainRoot, path.join(fileURLToPath(outDir), folder));
24
38
  }
25
39
  async function copyDirectory(from, to) {
26
40
  if (!(await stat(from).then((s) => s.isDirectory(), () => false)))
@@ -47,7 +61,7 @@ const TYPES = {
47
61
  ".webp": "image/webp",
48
62
  ".svg": "image/svg+xml",
49
63
  };
50
- /** A dev-server middleware serving the published files at `<base>_ascribe/files/`. */
64
+ /** A dev-server middleware serving the published files at `<base>_ascribe/files/`, `base` being the project's base path. */
51
65
  export function filesMiddleware(siteRoot, base) {
52
66
  return directoryMiddleware(publishedDir(siteRoot), `${base}_ascribe/files/`);
53
67
  }
package/dist/files.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,sDAAsD;AACtD,4EAA4E;AAC5E,8EAA8E;AAC9E,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,sBAAsB;AACtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,kEAAkE;AAClE,SAAS,YAAY,CAAC,QAAgB;IACpC,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;AAClD,CAAC;AAED,gGAAgG;AAChG,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,QAAgB,EAAE,MAAW;IACpE,OAAO,aAAa,CAClB,YAAY,CAAC,QAAQ,CAAC,EACtB,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,CACtD,CAAC;AACJ,CAAC;AAED,mHAAmH;AACnH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAiB,EAAE,MAAW;IACjE,OAAO,aAAa,CAAC,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAAY,EAAE,EAAU;IACnD,IACE,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CACrB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,EACtB,GAAG,EAAE,CAAC,KAAK,CACZ,CAAC;QAEF,OAAO,KAAK,CAAC;IACf,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,KAAK,GAA2B;IACpC,MAAM,EAAE,iBAAiB;IACzB,MAAM,EAAE,iBAAiB;IACzB,OAAO,EAAE,kBAAkB;IAC3B,OAAO,EAAE,0BAA0B;IACnC,MAAM,EAAE,0BAA0B;IAClC,MAAM,EAAE,2BAA2B;IACnC,MAAM,EAAE,yBAAyB;IACjC,KAAK,EAAE,8BAA8B;IACrC,OAAO,EAAE,0BAA0B;IACnC,MAAM,EAAE,yBAAyB;IACjC,KAAK,EAAE,gCAAgC;IACvC,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,WAAW;IACnB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,eAAe;CACxB,CAAC;AAIF,sFAAsF;AACtF,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,IAAY;IAC5D,OAAO,mBAAmB,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,GAAG,IAAI,iBAAiB,CAAC,CAAC;AAC/E,CAAC;AAED,+HAA+H;AAC/H,MAAM,UAAU,gBAAgB,CAAC,SAAiB,EAAE,IAAY;IAC9D,OAAO,mBAAmB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AAC9C,CAAC;AAED,4FAA4F;AAC5F,SAAS,mBAAmB,CAAC,IAAY,EAAE,MAAc;IACvD,uEAAuE;IACvE,gGAAgG;IAChG,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,MAAM,QAAQ,GACZ,CAAE,GAAgC,CAAC,WAAW,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7F,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC;YACjF,OAAO,IAAI,EAAE,CAAC;QAChB,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC1C,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,EAAE,CAAC;QACtE,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CACb,CAAC,IAAI,EAAE,EAAE;YACP,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;gBAAE,OAAO,IAAI,EAAE,CAAC;YAClC,GAAG,CAAC,SAAS,CACX,cAAc,EACd,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,0BAA0B,CACtE,CAAC;YACF,GAAG,CAAC,SAAS,CAAC,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;gBAAE,OAAO,KAAK,GAAG,CAAC,GAAG,EAAE,CAAC;YACjD,gBAAgB,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnC,CAAC,EACD,GAAG,EAAE,CAAC,IAAI,EAAE,CACb,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Serving `_ascribe/files/`: the files pages link to that aren't pages or\n// images. Astro doesn't copy them, so the integration\n// does: into the build's output directory, and in dev through a middleware.\n// Both put them at `<base>_ascribe/files/`, the URL the site output links to.\n//\n// With `[consumer] agents = true`, the same for the plain output, which is\n// laid out by URL: `llms.txt`, each page's Markdown, and their files, all at\n// `<base>` and below.\nimport { createReadStream } from \"node:fs\";\nimport { cp, stat } from \"node:fs/promises\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/** The directory of published files inside a site output root. */\nfunction publishedDir(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"files\");\n}\n\n/** Copies the published files to `<outDir>/_ascribe/files/`. Returns whether there were any. */\nexport async function copyPublishedFiles(siteRoot: string, outDir: URL): Promise<boolean> {\n return copyDirectory(\n publishedDir(siteRoot),\n path.join(fileURLToPath(outDir), \"_ascribe\", \"files\"),\n );\n}\n\n/** Copies the plain output for agents to the build output's root, the base path. Returns whether there was one. */\nexport async function copyAgentFiles(plainRoot: string, outDir: URL): Promise<boolean> {\n return copyDirectory(plainRoot, fileURLToPath(outDir));\n}\n\nasync function copyDirectory(from: string, to: string): Promise<boolean> {\n if (\n !(await stat(from).then(\n (s) => s.isDirectory(),\n () => false,\n ))\n )\n return false;\n await cp(from, to, { recursive: true });\n return true;\n}\n\nconst TYPES: Record<string, string> = {\n \".pdf\": \"application/pdf\",\n \".zip\": \"application/zip\",\n \".json\": \"application/json\",\n \".yaml\": \"text/yaml; charset=utf-8\",\n \".yml\": \"text/yaml; charset=utf-8\",\n \".txt\": \"text/plain; charset=utf-8\",\n \".csv\": \"text/csv; charset=utf-8\",\n \".md\": \"text/markdown; charset=utf-8\",\n \".html\": \"text/html; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".svg\": \"image/svg+xml\",\n};\n\ntype Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;\n\n/** A dev-server middleware serving the published files at `<base>_ascribe/files/`. */\nexport function filesMiddleware(siteRoot: string, base: string): Middleware {\n return directoryMiddleware(publishedDir(siteRoot), `${base}_ascribe/files/`);\n}\n\n/** A dev-server middleware serving the plain output for agents at `<base>`: `llms.txt`, each page's `.md`, and their files. */\nexport function agentsMiddleware(plainRoot: string, base: string): Middleware {\n return directoryMiddleware(plainRoot, base);\n}\n\n/** A middleware serving the files in `root` at the URL path `prefix`, which ends in `/`. */\nfunction directoryMiddleware(root: string, prefix: string): Middleware {\n // Only under the base path, as in the build. Vite strips the base from\n // `req.url` before a plugin's middleware runs, but connect keeps the original in `originalUrl`.\n return (req, res, next) => {\n const pathname =\n ((req as { originalUrl?: string }).originalUrl ?? req.url ?? \"\").split(/[?#]/, 1)[0] ?? \"\";\n if (!pathname.startsWith(prefix) || (req.method !== \"GET\" && req.method !== \"HEAD\"))\n return next();\n let relative: string;\n try {\n relative = decodeURIComponent(pathname.slice(prefix.length));\n } catch {\n return next();\n }\n const file = path.resolve(root, relative);\n if (file !== root && !file.startsWith(root + path.sep)) return next();\n stat(file).then(\n (info) => {\n if (!info.isFile()) return next();\n res.setHeader(\n \"Content-Type\",\n TYPES[path.extname(file).toLowerCase()] ?? \"application/octet-stream\",\n );\n res.setHeader(\"Content-Length\", info.size);\n if (req.method === \"HEAD\") return void res.end();\n createReadStream(file).pipe(res);\n },\n () => next(),\n );\n };\n}\n"]}
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"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","sourcesContent":["// Serving `_ascribe/files/`: the files pages link to that aren't pages or\n// images. Astro doesn't copy them, so the integration\n// does: into the build's output directory, and in dev through a middleware.\n// Both put them at `<base-path>_ascribe/files/`, the URL the site output links\n// to, where `<base-path>` is the project's `[consumer] base-path`: Astro's\n// `base` or a path under it.\n//\n// With `[consumer] agents = true`, the same for the plain output, which is\n// laid out by URL: `llms.txt`, each page's Markdown, and their files, all at\n// `<base-path>` and below.\nimport { createReadStream } from \"node:fs\";\nimport { cp, stat } from \"node:fs/promises\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The directory of published files inside a site output root. */\nfunction publishedDir(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"files\");\n}\n\n/**\n * The base path's folder in Astro's build output, which is at Astro's `base`:\n * `docs/` for a base path of `/docs/` under a `base` of `/`, `\"\"` when they're the same.\n */\nexport function folderUnder(basePath: string, astroBase: string): string {\n const root = normalizeBase(astroBase);\n return basePath.startsWith(root) ? basePath.slice(root.length) : \"\";\n}\n\n/**\n * Copies the published files to `<outDir>/<folder>_ascribe/files/`, `folder` being the\n * base path's (`folderUnder`). Returns whether there were any.\n */\nexport async function copyPublishedFiles(\n siteRoot: string,\n outDir: URL,\n folder = \"\",\n): Promise<boolean> {\n return copyDirectory(\n publishedDir(siteRoot),\n path.join(fileURLToPath(outDir), folder, \"_ascribe\", \"files\"),\n );\n}\n\n/** Copies the plain output for agents to the base path's folder of the build output. Returns whether there was one. */\nexport async function copyAgentFiles(\n plainRoot: string,\n outDir: URL,\n folder = \"\",\n): Promise<boolean> {\n return copyDirectory(plainRoot, path.join(fileURLToPath(outDir), folder));\n}\n\nasync function copyDirectory(from: string, to: string): Promise<boolean> {\n if (\n !(await stat(from).then(\n (s) => s.isDirectory(),\n () => false,\n ))\n )\n return false;\n await cp(from, to, { recursive: true });\n return true;\n}\n\nconst TYPES: Record<string, string> = {\n \".pdf\": \"application/pdf\",\n \".zip\": \"application/zip\",\n \".json\": \"application/json\",\n \".yaml\": \"text/yaml; charset=utf-8\",\n \".yml\": \"text/yaml; charset=utf-8\",\n \".txt\": \"text/plain; charset=utf-8\",\n \".csv\": \"text/csv; charset=utf-8\",\n \".md\": \"text/markdown; charset=utf-8\",\n \".html\": \"text/html; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".svg\": \"image/svg+xml\",\n};\n\ntype Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;\n\n/** A dev-server middleware serving the published files at `<base>_ascribe/files/`, `base` being the project's base path. */\nexport function filesMiddleware(siteRoot: string, base: string): Middleware {\n return directoryMiddleware(publishedDir(siteRoot), `${base}_ascribe/files/`);\n}\n\n/** A dev-server middleware serving the plain output for agents at `<base>`: `llms.txt`, each page's `.md`, and their files. */\nexport function agentsMiddleware(plainRoot: string, base: string): Middleware {\n return directoryMiddleware(plainRoot, base);\n}\n\n/** A middleware serving the files in `root` at the URL path `prefix`, which ends in `/`. */\nfunction directoryMiddleware(root: string, prefix: string): Middleware {\n // Only under the base path, as in the build. Vite strips the base from\n // `req.url` before a plugin's middleware runs, but connect keeps the original in `originalUrl`.\n return (req, res, next) => {\n const pathname =\n ((req as { originalUrl?: string }).originalUrl ?? req.url ?? \"\").split(/[?#]/, 1)[0] ?? \"\";\n if (!pathname.startsWith(prefix) || (req.method !== \"GET\" && req.method !== \"HEAD\"))\n return next();\n let relative: string;\n try {\n relative = decodeURIComponent(pathname.slice(prefix.length));\n } catch {\n return next();\n }\n const file = path.resolve(root, relative);\n if (file !== root && !file.startsWith(root + path.sep)) return next();\n stat(file).then(\n (info) => {\n if (!info.isFile()) return next();\n res.setHeader(\n \"Content-Type\",\n TYPES[path.extname(file).toLowerCase()] ?? \"application/octet-stream\",\n );\n res.setHeader(\"Content-Length\", info.size);\n if (req.method === \"HEAD\") return void res.end();\n createReadStream(file).pipe(res);\n },\n () => next(),\n );\n };\n}\n"]}
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
- /** Options of the integration. */
5
- export interface AscribeOptions {
6
- /** The build whose site output is the collection (a name in `ascribe.toml`). */
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,9 +20,10 @@ 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
@@ -32,8 +31,26 @@ export interface AscribeOptions {
32
31
  */
33
32
  codeTitles?: boolean;
34
33
  }
34
+ /** Options of the integration with one project. */
35
+ export interface OneProjectOptions extends SharedOptions {
36
+ /** The build whose site output is the collection (a name in `ascribe.toml`). */
37
+ build: string;
38
+ /** The directory holding `ascribe.toml`, relative to the Astro root. Default: the root. */
39
+ project?: string;
40
+ projects?: never;
41
+ }
42
+ /** Options of the integration with several projects, each entry named. */
43
+ export interface ProjectsOptions extends SharedOptions {
44
+ /** The projects, each a named build of a project; two entries may share a project. */
45
+ projects: ProjectEntry[];
46
+ build?: never;
47
+ project?: never;
48
+ }
49
+ /** Options of the integration. */
50
+ export type AscribeOptions = OneProjectOptions | ProjectsOptions;
35
51
  /**
36
- * Runs `ascribe build`, checks the site's routing, adds the markdown plugin, and serves published
37
- * files, and, with `[consumer] agents = true`, `llms.txt` and each page's Markdown.
52
+ * Runs `ascribe build` for each project, checks the site's routing, adds the markdown plugin, adds a
53
+ * page for each permalink's address, and serves published files, and, with `[consumer] agents = true`,
54
+ * `llms.txt` and each page's Markdown, each at its project's base path.
38
55
  */
39
56
  export default function ascribe(options: AscribeOptions): AstroIntegration;