@minidocs/astro 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +40 -2
- package/dist/behaviors.d.mts +114 -0
- package/dist/behaviors.mjs +635 -0
- package/dist/index.d.mts +23 -0
- package/dist/index.mjs +275 -0
- package/dist/links-Dp7d5R-O.mjs +242 -0
- package/dist/loaders.d.mts +25 -0
- package/dist/loaders.mjs +88 -0
- package/dist/routing-D28b92w8.mjs +60 -0
- package/dist/runtime.d.mts +202 -0
- package/dist/runtime.mjs +327 -0
- package/package.json +80 -3
- package/src/components/Prose.astro +9 -0
- package/src/components/callout/Root.astro +14 -0
- package/src/components/callout/Title.astro +9 -0
- package/src/components/callout/index.ts +2 -0
- package/src/components/index.ts +7 -0
- package/src/components/layout/Aside.astro +29 -0
- package/src/components/layout/Header.astro +18 -0
- package/src/components/layout/Main.astro +9 -0
- package/src/components/layout/Root.astro +15 -0
- package/src/components/layout/Sidebar.astro +9 -0
- package/src/components/layout/Title.astro +10 -0
- package/src/components/layout/index.ts +6 -0
- package/src/components/pager/Label.astro +9 -0
- package/src/components/pager/Link.astro +12 -0
- package/src/components/pager/Root.astro +12 -0
- package/src/components/pager/Title.astro +9 -0
- package/src/components/pager/index.ts +4 -0
- package/src/components/search/Count.astro +10 -0
- package/src/components/search/Dialog.astro +14 -0
- package/src/components/search/Empty.astro +10 -0
- package/src/components/search/Error.astro +10 -0
- package/src/components/search/Found.astro +10 -0
- package/src/components/search/Idle.astro +10 -0
- package/src/components/search/Input.astro +26 -0
- package/src/components/search/Loading.astro +10 -0
- package/src/components/search/Query.astro +10 -0
- package/src/components/search/Results.astro +20 -0
- package/src/components/search/Root.astro +15 -0
- package/src/components/search/Shortcut.astro +14 -0
- package/src/components/search/Trigger.astro +28 -0
- package/src/components/search/index.ts +13 -0
- package/src/components/sidebar/Group.astro +45 -0
- package/src/components/sidebar/Item.astro +9 -0
- package/src/components/sidebar/Label.astro +9 -0
- package/src/components/sidebar/Link.astro +23 -0
- package/src/components/sidebar/List.astro +11 -0
- package/src/components/sidebar/Root.astro +24 -0
- package/src/components/sidebar/Trigger.astro +29 -0
- package/src/components/sidebar/index.ts +7 -0
- package/src/components/toc/Item.astro +9 -0
- package/src/components/toc/Link.astro +12 -0
- package/src/components/toc/List.astro +11 -0
- package/src/components/toc/Root.astro +20 -0
- package/src/components/toc/index.ts +4 -0
- package/src/styles/base.css +42 -0
- package/src/styles/callout.css +58 -0
- package/src/styles/layout.css +406 -0
- package/src/styles/pager.css +56 -0
- package/src/styles/prose.css +218 -0
- package/src/styles/search.css +207 -0
- package/src/styles/sidebar.css +162 -0
- package/src/styles/toc.css +79 -0
- package/src/styles.css +9 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { t as contentOptions } from "./routing-D28b92w8.mjs";
|
|
2
|
+
import { a as registry, c as renderConfigTypes, i as namedCollections, n as remarkContentLinks, o as findUnbuiltPages, r as satteriContentLinks } from "./links-Dp7d5R-O.mjs";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseFilePath } from "@minidocs/core";
|
|
7
|
+
import remarkDirective from "remark-directive";
|
|
8
|
+
//#region src/callouts.ts
|
|
9
|
+
/** Callout types, shared by `:::type` directives, `> [!TYPE]` alerts and the `Callout` component. */
|
|
10
|
+
const CALLOUT_TYPES = [
|
|
11
|
+
"note",
|
|
12
|
+
"tip",
|
|
13
|
+
"important",
|
|
14
|
+
"warning",
|
|
15
|
+
"caution"
|
|
16
|
+
];
|
|
17
|
+
const isType = (name) => CALLOUT_TYPES.includes(name);
|
|
18
|
+
/** Same markup as the Callout component renders, as mdast data for the HTML conversion. */
|
|
19
|
+
const calloutData = (type) => ({
|
|
20
|
+
hName: "div",
|
|
21
|
+
hProperties: {
|
|
22
|
+
className: ["md-callout"],
|
|
23
|
+
dataType: type,
|
|
24
|
+
role: "note"
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
const titleData = {
|
|
28
|
+
hName: "p",
|
|
29
|
+
hProperties: { className: ["md-callout-title"] }
|
|
30
|
+
};
|
|
31
|
+
const ALERT = /^\[!(\w+)\][ \t]*(?:\n|$)/;
|
|
32
|
+
function unknownDirective(name) {
|
|
33
|
+
return /* @__PURE__ */ new Error(`[minidocs] Unknown callout type ":::${name}". Use one of ${CALLOUT_TYPES.join(", ")}, or handle it with your own plugin.`);
|
|
34
|
+
}
|
|
35
|
+
/** Reads the type of a `> [!TYPE]` alert and what is left of its first line, or `undefined` for plain blockquotes. */
|
|
36
|
+
function parseAlert(blockquote) {
|
|
37
|
+
const paragraph = blockquote.children?.[0];
|
|
38
|
+
const text = paragraph?.type === "paragraph" ? paragraph.children?.[0] : void 0;
|
|
39
|
+
const match = text?.type === "text" ? ALERT.exec(text.value ?? "") : null;
|
|
40
|
+
const type = match?.[1]?.toLowerCase();
|
|
41
|
+
if (!match || !type || !isType(type)) return void 0;
|
|
42
|
+
const rest = text.value.slice(match[0].length);
|
|
43
|
+
return {
|
|
44
|
+
type,
|
|
45
|
+
paragraph,
|
|
46
|
+
text,
|
|
47
|
+
rest,
|
|
48
|
+
empty: !rest && paragraph.children.length === 1
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Turning on directives also parses `:name` in text and `::name` lines, which would silently drop `10:30` or `a:hover`.
|
|
53
|
+
* Only `:::` blocks are callouts, so the others go back to their source text.
|
|
54
|
+
*/
|
|
55
|
+
function restoreSatteri(block) {
|
|
56
|
+
return (node, context) => {
|
|
57
|
+
const text = {
|
|
58
|
+
type: "text",
|
|
59
|
+
value: context.source.slice(node.position.start.offset, node.position.end.offset)
|
|
60
|
+
};
|
|
61
|
+
context.replaceNode(node, block ? {
|
|
62
|
+
type: "paragraph",
|
|
63
|
+
children: [text]
|
|
64
|
+
} : text);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Plugin for Sätteri, Astro's default Markdown processor. */
|
|
68
|
+
function satteriCallouts(options) {
|
|
69
|
+
return {
|
|
70
|
+
name: "minidocs-callouts",
|
|
71
|
+
options: { position: true },
|
|
72
|
+
...options.directives && {
|
|
73
|
+
containerDirective(node, context) {
|
|
74
|
+
if (node.data?.hName) return;
|
|
75
|
+
const name = node.name ?? "";
|
|
76
|
+
if (!isType(name)) throw unknownDirective(name);
|
|
77
|
+
const label = node.children?.[0];
|
|
78
|
+
if (label?.type === "paragraph" && label.data?.directiveLabel) context.setProperty(label, "data", titleData);
|
|
79
|
+
context.setProperty(node, "data", calloutData(name));
|
|
80
|
+
},
|
|
81
|
+
leafDirective: restoreSatteri(true),
|
|
82
|
+
textDirective: restoreSatteri(false)
|
|
83
|
+
},
|
|
84
|
+
...options.alerts && { blockquote(node, context) {
|
|
85
|
+
const alert = parseAlert(node);
|
|
86
|
+
if (!alert) return;
|
|
87
|
+
if (alert.empty) context.removeNode(alert.paragraph);
|
|
88
|
+
else context.setProperty(alert.text, "value", alert.rest);
|
|
89
|
+
context.setProperty(node, "data", calloutData(alert.type));
|
|
90
|
+
} }
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Remark plugin for the unified processor. Directive parsing comes from `remark-directive`, added before it. */
|
|
94
|
+
function remarkCallouts(options) {
|
|
95
|
+
return () => (tree, file) => {
|
|
96
|
+
const source = String(file.value ?? "");
|
|
97
|
+
const visit = (node) => {
|
|
98
|
+
const children = node.children ?? [];
|
|
99
|
+
for (let index = 0; index < children.length; index++) {
|
|
100
|
+
const child = children[index];
|
|
101
|
+
if (options.directives && (child.type === "textDirective" || child.type === "leafDirective")) {
|
|
102
|
+
const text = {
|
|
103
|
+
type: "text",
|
|
104
|
+
value: source.slice(child.position?.start.offset, child.position?.end.offset)
|
|
105
|
+
};
|
|
106
|
+
children[index] = child.type === "leafDirective" ? {
|
|
107
|
+
type: "paragraph",
|
|
108
|
+
children: [text]
|
|
109
|
+
} : text;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (options.directives && child.type === "containerDirective" && !child.data?.hName) {
|
|
113
|
+
const name = child.name ?? "";
|
|
114
|
+
if (!isType(name)) throw unknownDirective(name);
|
|
115
|
+
const label = child.children?.[0];
|
|
116
|
+
if (label?.type === "paragraph" && label.data?.directiveLabel) label.data = {
|
|
117
|
+
...label.data,
|
|
118
|
+
...titleData
|
|
119
|
+
};
|
|
120
|
+
child.data = {
|
|
121
|
+
...child.data,
|
|
122
|
+
...calloutData(name)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (options.alerts && child.type === "blockquote") {
|
|
126
|
+
const alert = parseAlert(child);
|
|
127
|
+
if (alert) {
|
|
128
|
+
if (alert.empty) child.children.shift();
|
|
129
|
+
else alert.text.value = alert.rest;
|
|
130
|
+
child.data = {
|
|
131
|
+
...child.data,
|
|
132
|
+
...calloutData(alert.type)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
visit(child);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
visit(tree);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/markdown.ts
|
|
144
|
+
/**
|
|
145
|
+
* Adds minidocs' Markdown features to the configured processor, which MDX uses too unless given its own.
|
|
146
|
+
* Processors are meant to be extended in place. Returns `false` for processors minidocs does not know.
|
|
147
|
+
*/
|
|
148
|
+
function extendProcessor(processor, callouts) {
|
|
149
|
+
const options = processor.options;
|
|
150
|
+
if (processor.name === "satteri") {
|
|
151
|
+
if (callouts.directives) options.features = {
|
|
152
|
+
...options.features,
|
|
153
|
+
directive: true
|
|
154
|
+
};
|
|
155
|
+
options.mdastPlugins = [
|
|
156
|
+
...options.mdastPlugins ?? [],
|
|
157
|
+
satteriContentLinks,
|
|
158
|
+
satteriCallouts(callouts)
|
|
159
|
+
];
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (processor.name === "unified") {
|
|
163
|
+
options.remarkPlugins = [
|
|
164
|
+
...options.remarkPlugins ?? [],
|
|
165
|
+
...callouts.directives ? [remarkDirective] : [],
|
|
166
|
+
remarkContentLinks,
|
|
167
|
+
remarkCallouts(callouts)
|
|
168
|
+
];
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/settings.ts
|
|
175
|
+
const trim = (path) => path.replace(/^\/+|\/+$/g, "");
|
|
176
|
+
function resolveSettings(config, options) {
|
|
177
|
+
return {
|
|
178
|
+
base: trim(config.base),
|
|
179
|
+
trailingSlash: config.trailingSlash === "always" || config.trailingSlash === "ignore" && config.build.format === "directory",
|
|
180
|
+
i18n: resolveI18n(config.i18n, options),
|
|
181
|
+
site: config.site ? config.site.replace(/\/+$/, "") : null
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function resolveI18n(i18n, options) {
|
|
185
|
+
if (!i18n) {
|
|
186
|
+
if (options.fallback) throw new Error("[minidocs] `fallback` requires `i18n` in your Astro config.");
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
if (i18n.fallback && Object.keys(i18n.fallback).length > 0) throw new Error("[minidocs] Astro's `i18n.fallback` works per route file and produces wrong pages for content-driven docs routes in static builds. Remove it and declare fallback with `minidocs({ fallback })` instead.");
|
|
190
|
+
if (i18n.routing === "manual") throw new Error("[minidocs] `i18n.routing: \"manual\"` is not supported yet.");
|
|
191
|
+
const locales = i18n.locales.map((locale) => {
|
|
192
|
+
if (typeof locale !== "string") throw new Error("[minidocs] Locales with custom paths are not supported yet. Use plain locale strings.");
|
|
193
|
+
return locale;
|
|
194
|
+
});
|
|
195
|
+
const fallback = options.fallback ?? {};
|
|
196
|
+
for (const [locale, targets] of Object.entries(fallback)) for (const target of [locale, ...[targets].flat()]) if (!locales.includes(target)) throw new Error(`[minidocs] Fallback refers to "${target}", which is not one of i18n.locales (${locales.join(", ")}).`);
|
|
197
|
+
return {
|
|
198
|
+
locales,
|
|
199
|
+
defaultLocale: i18n.defaultLocale,
|
|
200
|
+
prefixDefaultLocale: i18n.routing?.prefixDefaultLocale ?? false,
|
|
201
|
+
fallback
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/index.ts
|
|
206
|
+
const CONFIG_ID = "virtual:minidocs/config";
|
|
207
|
+
const RENDERERS_ID = "virtual:minidocs/renderers";
|
|
208
|
+
function minidocs(options = {}) {
|
|
209
|
+
let hasMdx = false;
|
|
210
|
+
let isStatic = false;
|
|
211
|
+
return {
|
|
212
|
+
name: "minidocs",
|
|
213
|
+
hooks: {
|
|
214
|
+
"astro:config:setup"({ config, updateConfig, logger }) {
|
|
215
|
+
const callouts = {
|
|
216
|
+
directives: options.markdown?.directives ?? true,
|
|
217
|
+
alerts: options.markdown?.alerts ?? true
|
|
218
|
+
};
|
|
219
|
+
if (!extendProcessor(config.markdown.processor, callouts)) logger.warn(`Content links and callouts are not supported with the "${config.markdown.processor.name}" Markdown processor.`);
|
|
220
|
+
updateConfig({ vite: {
|
|
221
|
+
plugins: [{
|
|
222
|
+
name: "minidocs:config",
|
|
223
|
+
resolveId: (id) => id === CONFIG_ID ? `\0${CONFIG_ID}` : void 0,
|
|
224
|
+
load: (id) => id === `\0${CONFIG_ID}` ? `export default ${JSON.stringify({
|
|
225
|
+
...registry.settings,
|
|
226
|
+
collections: Object.fromEntries(Object.entries(namedCollections()).map(([name, collection]) => [name, {
|
|
227
|
+
base: collection.base,
|
|
228
|
+
prefix: collection.prefix,
|
|
229
|
+
pageIds: pageIds(collection)
|
|
230
|
+
}]))
|
|
231
|
+
})};` : void 0
|
|
232
|
+
}, {
|
|
233
|
+
name: "minidocs:renderers",
|
|
234
|
+
resolveId: (id) => id === RENDERERS_ID ? `\0${RENDERERS_ID}` : void 0,
|
|
235
|
+
load: (id) => id === `\0${RENDERERS_ID}` ? hasMdx ? `import { getContainerRenderer } from '@astrojs/mdx/container-renderer';\nexport default [getContainerRenderer()];` : "export default [];" : void 0
|
|
236
|
+
}],
|
|
237
|
+
ssr: { noExternal: ["@minidocs/astro", "@minidocs/core"] }
|
|
238
|
+
} });
|
|
239
|
+
},
|
|
240
|
+
"astro:config:done"({ config, injectTypes }) {
|
|
241
|
+
registry.settings = resolveSettings(config, options);
|
|
242
|
+
registry.root = fileURLToPath(config.root);
|
|
243
|
+
hasMdx = config.integrations.some(({ name }) => name === "@astrojs/mdx");
|
|
244
|
+
isStatic = config.output === "static" && !config.adapter;
|
|
245
|
+
const injected = injectTypes({
|
|
246
|
+
filename: "types.d.ts",
|
|
247
|
+
content: renderConfigTypes(registry.settings)
|
|
248
|
+
});
|
|
249
|
+
registry.typesFile = new URL("./collections.d.ts", injected);
|
|
250
|
+
const path = fileURLToPath(registry.typesFile);
|
|
251
|
+
if (!existsSync(path)) {
|
|
252
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
253
|
+
writeFileSync(path, "export {};\n");
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
"astro:build:done"({ pages }) {
|
|
257
|
+
if (!isStatic || !registry.settings) return;
|
|
258
|
+
const built = new Set(pages.map(({ pathname }) => pathname.replace(/^\/+|\/+$/g, "")));
|
|
259
|
+
const missing = findUnbuiltPages(registry.settings, namedCollections(), built);
|
|
260
|
+
if (missing.length === 0) return;
|
|
261
|
+
const shown = missing.slice(0, 10).map((page) => ` ${page}`);
|
|
262
|
+
if (missing.length > shown.length) shown.push(` and ${missing.length - shown.length} more`);
|
|
263
|
+
throw new Error(`[minidocs] No route built these pages, so links to them are broken. Check that each collection's route file sits in the folder its prefix names.\n${shown.join("\n")}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** Ids of every page in a collection, in any locale. */
|
|
269
|
+
function pageIds(collection) {
|
|
270
|
+
if (!registry.settings) return [];
|
|
271
|
+
const { locales, localeDirs } = contentOptions(registry.settings, collection.base);
|
|
272
|
+
return [...new Set(collection.filePaths.map((filePath) => parseFilePath(filePath, collection.base, locales, localeDirs).id))];
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
export { minidocs as default };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { i as markdownHref, n as href, o as routePath, t as contentOptions } from "./routing-D28b92w8.mjs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, posix, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { listPages, parseFilePath } from "@minidocs/core";
|
|
6
|
+
//#region src/codegen.ts
|
|
7
|
+
const union = (values) => {
|
|
8
|
+
const list = [...new Set(values)].sort().map((value) => JSON.stringify(value));
|
|
9
|
+
return list.length > 0 ? list.join(" | ") : "never";
|
|
10
|
+
};
|
|
11
|
+
const byName = (collections) => Object.entries(collections).sort(([a], [b]) => a < b ? -1 : 1);
|
|
12
|
+
/** Every page a collection needs a route for, with the locale its URL is built with. */
|
|
13
|
+
function pagesOf(settings, collection) {
|
|
14
|
+
return listPages(collection.filePaths.map((filePath) => ({ filePath })), contentOptions(settings, collection.base)).map((page) => ({
|
|
15
|
+
...page,
|
|
16
|
+
urlLocale: settings.i18n ? page.locale : void 0
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
/** Renders the site-wide types, known as soon as the config is resolved. */
|
|
20
|
+
function renderConfigTypes(settings) {
|
|
21
|
+
return [
|
|
22
|
+
"// Generated by minidocs. Do not edit.",
|
|
23
|
+
"/// <reference path=\"./collections.d.ts\" />",
|
|
24
|
+
"declare module '@minidocs/astro/runtime' {",
|
|
25
|
+
`\tinterface Config { ${settings.i18n ? `i18n: true; locale: ${union(settings.i18n.locales)};` : "i18n: false;"} }`,
|
|
26
|
+
"}",
|
|
27
|
+
"export {};",
|
|
28
|
+
""
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
/** Renders the declaration file that types page ids and directories per collection, and per locale for multilingual sites. */
|
|
32
|
+
function renderTypes(settings, collections) {
|
|
33
|
+
const lines = [
|
|
34
|
+
"// Generated by minidocs. Do not edit.",
|
|
35
|
+
"declare module '@minidocs/astro/runtime' {",
|
|
36
|
+
" interface Collections {"
|
|
37
|
+
];
|
|
38
|
+
for (const [name, collection] of byName(collections)) {
|
|
39
|
+
const options = contentOptions(settings, collection.base);
|
|
40
|
+
const pages = pagesOf(settings, collection);
|
|
41
|
+
const idsAndDirs = (locale) => {
|
|
42
|
+
const inLocale = pages.filter((page) => page.locale === locale);
|
|
43
|
+
const dirs = /* @__PURE__ */ new Set([""]);
|
|
44
|
+
for (const page of inLocale) {
|
|
45
|
+
const { dir } = parseFilePath(page.entry.filePath, collection.base, options.locales, options.localeDirs);
|
|
46
|
+
const segments = dir.split("/").filter(Boolean);
|
|
47
|
+
for (let i = 1; i <= segments.length; i++) dirs.add(segments.slice(0, i).join("/"));
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
ids: union(inLocale.map((page) => page.id)),
|
|
51
|
+
dirs: union(dirs)
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
lines.push(`\t\t${JSON.stringify(name)}: {`);
|
|
55
|
+
if (settings.i18n) {
|
|
56
|
+
const perLocale = options.locales.map((locale) => [JSON.stringify(locale), idsAndDirs(locale)]);
|
|
57
|
+
lines.push(" pages: {", ...perLocale.map(([locale, { ids }]) => `\t\t\t\t${locale}: ${ids};`), " };");
|
|
58
|
+
lines.push(" dirs: {", ...perLocale.map(([locale, { dirs }]) => `\t\t\t\t${locale}: ${dirs};`), " };");
|
|
59
|
+
} else {
|
|
60
|
+
const { ids, dirs } = idsAndDirs(options.locales[0]);
|
|
61
|
+
lines.push(`\t\t\tpages: ${ids};`, `\t\t\tdirs: ${dirs};`);
|
|
62
|
+
}
|
|
63
|
+
lines.push(" };");
|
|
64
|
+
}
|
|
65
|
+
lines.push(" }", "}", "export {};", "");
|
|
66
|
+
return lines.join("\n");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Finds pages that would be served at the same URL, across all collections, for HTML pages and Markdown versions alike.
|
|
70
|
+
* Returns one message per pair of files.
|
|
71
|
+
*/
|
|
72
|
+
function findUrlConflicts(settings, collections) {
|
|
73
|
+
const owners = /* @__PURE__ */ new Map();
|
|
74
|
+
const conflicts = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const [name, collection] of byName(collections)) for (const page of pagesOf(settings, collection)) {
|
|
76
|
+
const owner = `${page.entry.filePath} (${name})`;
|
|
77
|
+
const urls = [href(settings, collection, page.id, page.urlLocale), markdownHref(settings, collection, page.id, page.urlLocale)];
|
|
78
|
+
for (const url of urls) {
|
|
79
|
+
const other = owners.get(url);
|
|
80
|
+
if (other === void 0) owners.set(url, owner);
|
|
81
|
+
else if (!conflicts.has(`${other}\n${owner}`)) conflicts.set(`${other}\n${owner}`, `"${url}" is the URL of two pages:\n ${other}\n ${owner}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...conflicts.values()];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Finds pages that no route generated, given the paths of built pages below `base`.
|
|
88
|
+
* A route file outside the folder that `prefix` names builds its pages elsewhere, and every link to them breaks.
|
|
89
|
+
*/
|
|
90
|
+
function findUnbuiltPages(settings, collections, built) {
|
|
91
|
+
const missing = [];
|
|
92
|
+
for (const [name, collection] of byName(collections)) for (const page of pagesOf(settings, collection)) {
|
|
93
|
+
const path = routePath(settings, collection, page.id, page.urlLocale) ?? "";
|
|
94
|
+
if (!built.has(path)) missing.push(`/${path} for ${page.entry.filePath} (${name}, prefix "${collection.prefix}")`);
|
|
95
|
+
}
|
|
96
|
+
return missing;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/registry.ts
|
|
100
|
+
const key = Symbol.for("minidocs.registry");
|
|
101
|
+
const scope = globalThis;
|
|
102
|
+
scope[key] ??= {
|
|
103
|
+
collections: {},
|
|
104
|
+
pendingLinks: []
|
|
105
|
+
};
|
|
106
|
+
const registry = scope[key];
|
|
107
|
+
/** Collections whose loader has run, keyed by collection name. */
|
|
108
|
+
function namedCollections() {
|
|
109
|
+
return Object.fromEntries(Object.values(registry.collections).filter((state) => state.name !== void 0).map((state) => [state.name, state]));
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/links.ts
|
|
113
|
+
/** Link to another page from a page in the same locale, relative so it stays in whatever locale the page is shown in. */
|
|
114
|
+
function relativeHref(settings, from, to, locale) {
|
|
115
|
+
const fromHref = href(settings, from.collection, from.id, locale);
|
|
116
|
+
const toHref = href(settings, to.collection, to.id, locale);
|
|
117
|
+
const fromDir = fromHref.slice(0, fromHref.lastIndexOf("/") + 1);
|
|
118
|
+
if (toHref.endsWith("/")) return `${posix.relative(fromDir, toHref) || "."}/`;
|
|
119
|
+
return posix.join(posix.relative(fromDir, posix.dirname(toHref)), posix.basename(toHref));
|
|
120
|
+
}
|
|
121
|
+
/** The collection whose root holds a project-relative file, the innermost one if roots are nested. */
|
|
122
|
+
function collectionOf(collections, file) {
|
|
123
|
+
return collections.filter(({ base }) => file.startsWith(`${base}/`)).reduce((best, collection) => best && best.base.length > collection.base.length ? best : collection, void 0);
|
|
124
|
+
}
|
|
125
|
+
const EXCLUDED = "points to a page that the collection's filter excludes.";
|
|
126
|
+
/**
|
|
127
|
+
* Rewrites a relative link to a content file, in any collection, into the URL of its page.
|
|
128
|
+
* Links within one locale become relative URLs, so pages shown through fallback keep linking within the shown locale.
|
|
129
|
+
* Returns `undefined` for links that are not to `.md` or `.mdx` files. Throws when the target does not exist.
|
|
130
|
+
*/
|
|
131
|
+
function resolveContentLink(settings, source, url, root) {
|
|
132
|
+
const match = /^([^#?]+\.mdx?)(#.*)?$/i.exec(url);
|
|
133
|
+
if (!match || /^([a-z][a-z\d+.-]*:|\/)/i.test(url)) return void 0;
|
|
134
|
+
const [, path, hash = ""] = match;
|
|
135
|
+
const absolute = resolve(root, dirname(source.file), decodeURI(path));
|
|
136
|
+
const target = relative(root, absolute).split(sep).join("/");
|
|
137
|
+
const collection = collectionOf(source.collections, target);
|
|
138
|
+
if (!collection) throw new Error(`"${url}" points outside every collection loaded with minidocs' glob, so it has no page.`);
|
|
139
|
+
if (!existsSync(absolute)) throw new Error(`"${url}" points to a file that does not exist.`);
|
|
140
|
+
if (source.isExcluded?.(target, collection)) throw new Error(`"${url}" ${EXCLUDED}`);
|
|
141
|
+
const parse = (file, of) => {
|
|
142
|
+
const { locales, localeDirs } = contentOptions(settings, of.base);
|
|
143
|
+
return parseFilePath(file, of.base, locales, localeDirs);
|
|
144
|
+
};
|
|
145
|
+
const from = parse(source.file, source.collection);
|
|
146
|
+
const to = parse(target, collection);
|
|
147
|
+
return (from.locale === to.locale ? relativeHref(settings, {
|
|
148
|
+
collection: source.collection,
|
|
149
|
+
id: from.id
|
|
150
|
+
}, {
|
|
151
|
+
collection,
|
|
152
|
+
id: to.id
|
|
153
|
+
}, settings.i18n ? from.locale : void 0) : href(settings, collection, to.id, to.locale)) + hash;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolves a link found in a Markdown file, or returns `undefined` to leave it alone.
|
|
157
|
+
* Only files inside a collection loaded with minidocs' glob are rewritten.
|
|
158
|
+
*/
|
|
159
|
+
function rewrite(filePath, url) {
|
|
160
|
+
const { settings, root } = registry;
|
|
161
|
+
if (!settings || !root) return void 0;
|
|
162
|
+
const file = relative(root, filePath).split(sep).join("/");
|
|
163
|
+
const collections = Object.values(registry.collections);
|
|
164
|
+
const collection = collectionOf(collections, file);
|
|
165
|
+
if (!collection) return void 0;
|
|
166
|
+
const ready = (state) => state.loaded && !state.loading;
|
|
167
|
+
const isExcluded = (target, to) => {
|
|
168
|
+
if (!ready(collection) || !ready(to)) {
|
|
169
|
+
registry.pendingLinks.push({
|
|
170
|
+
file,
|
|
171
|
+
url,
|
|
172
|
+
target,
|
|
173
|
+
source: collection,
|
|
174
|
+
to
|
|
175
|
+
});
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return to.excluded.has(target);
|
|
179
|
+
};
|
|
180
|
+
return resolveContentLink(settings, {
|
|
181
|
+
file,
|
|
182
|
+
collection,
|
|
183
|
+
collections,
|
|
184
|
+
isExcluded
|
|
185
|
+
}, url, root);
|
|
186
|
+
}
|
|
187
|
+
/** Checks links rendered while a collection involved was loading, once both collections have loaded. */
|
|
188
|
+
function checkPendingLinks() {
|
|
189
|
+
const ready = (state) => state.loaded && !state.loading;
|
|
190
|
+
const due = registry.pendingLinks.filter(({ source, to }) => ready(source) && ready(to));
|
|
191
|
+
registry.pendingLinks = registry.pendingLinks.filter((link) => !due.includes(link));
|
|
192
|
+
const broken = due.filter(({ file, target, source, to }) => !source.excluded.has(file) && to.excluded.has(target));
|
|
193
|
+
if (broken.length === 0) return;
|
|
194
|
+
const root = registry.root ?? "";
|
|
195
|
+
throw new Error(broken.map(({ file, url }) => failure(resolve(root, file), url, /* @__PURE__ */ new Error(`"${url}" ${EXCLUDED}`)).message).join("\n"));
|
|
196
|
+
}
|
|
197
|
+
/** Points at the line of the source file with the link. Processors may count lines from after the frontmatter. */
|
|
198
|
+
function failure(filePath, url, error) {
|
|
199
|
+
let line;
|
|
200
|
+
try {
|
|
201
|
+
const index = readFileSync(filePath, "utf8").split("\n").findIndex((text) => text.includes(url));
|
|
202
|
+
if (index !== -1) line = index + 1;
|
|
203
|
+
} catch {}
|
|
204
|
+
const location = `${relative(registry.root ?? "", filePath)}${line ? `:${line}` : ""}`;
|
|
205
|
+
return /* @__PURE__ */ new Error(`[minidocs] ${location}: ${error.message}`);
|
|
206
|
+
}
|
|
207
|
+
/** Plugin for Sätteri, Astro's default Markdown processor. */
|
|
208
|
+
const satteriContentLinks = (() => {
|
|
209
|
+
const visit = (node, context) => {
|
|
210
|
+
if (!context.fileURL) return;
|
|
211
|
+
const filePath = fileURLToPath(context.fileURL);
|
|
212
|
+
try {
|
|
213
|
+
const url = rewrite(filePath, node.url);
|
|
214
|
+
if (url !== void 0) context.setProperty(node, "url", url);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw failure(filePath, node.url, error);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
return {
|
|
220
|
+
name: "minidocs-content-links",
|
|
221
|
+
link: visit,
|
|
222
|
+
definition: visit
|
|
223
|
+
};
|
|
224
|
+
})();
|
|
225
|
+
/** Remark plugin for the unified processor from `@astrojs/markdown-remark`. */
|
|
226
|
+
function remarkContentLinks() {
|
|
227
|
+
return (tree, file) => {
|
|
228
|
+
const filePath = file.path;
|
|
229
|
+
if (!filePath) return;
|
|
230
|
+
const visit = (node) => {
|
|
231
|
+
if ((node.type === "link" || node.type === "definition") && node.url) try {
|
|
232
|
+
node.url = rewrite(filePath, node.url) ?? node.url;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw failure(filePath, node.url, error);
|
|
235
|
+
}
|
|
236
|
+
for (const child of node.children ?? []) visit(child);
|
|
237
|
+
};
|
|
238
|
+
visit(tree);
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
export { registry as a, renderConfigTypes as c, namedCollections as i, renderTypes as l, remarkContentLinks as n, findUnbuiltPages as o, satteriContentLinks as r, findUrlConflicts as s, checkPendingLinks as t };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Loader, glob as glob$1 } from "astro/loaders";
|
|
2
|
+
//#region src/loaders.d.ts
|
|
3
|
+
type GlobOptions = Parameters<typeof glob$1>[0];
|
|
4
|
+
/** An entry as the filter sees it, after the collection schema has parsed its data. */
|
|
5
|
+
export interface FilterEntry {
|
|
6
|
+
id: string;
|
|
7
|
+
data: Record<string, unknown>;
|
|
8
|
+
filePath?: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
export interface MinidocsGlobOptions {
|
|
11
|
+
/** Path segment the pages live under, e.g. `docs` for `/docs/...`. Defaults to the site root. */
|
|
12
|
+
prefix?: string;
|
|
13
|
+
/** Entries for which it returns `false` never reach the collection. */
|
|
14
|
+
filter?: (entry: FilterEntry) => boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Astro's `glob()` loader, plus recording of the collection root and URL prefix so minidocs can
|
|
18
|
+
* derive locales, page ids and URLs, and generate types for them.
|
|
19
|
+
* Entries rejected by `filter` never reach the collection, so types, links and pages all agree on what exists.
|
|
20
|
+
*/
|
|
21
|
+
export declare function glob({ prefix, filter, ...options }: GlobOptions & {
|
|
22
|
+
base: string | URL;
|
|
23
|
+
} & MinidocsGlobOptions): Loader;
|
|
24
|
+
//#endregion
|
|
25
|
+
export {};
|
package/dist/loaders.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { a as registry, i as namedCollections, l as renderTypes, s as findUrlConflicts, t as checkPendingLinks } from "./links-Dp7d5R-O.mjs";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { glob as glob$1 } from "astro/loaders";
|
|
6
|
+
//#region src/loaders.ts
|
|
7
|
+
/** Collection root relative to the project, as Astro's glob resolves `base`. */
|
|
8
|
+
const relativeBase = (base, root) => relative(root, base instanceof URL ? fileURLToPath(base) : resolve(root, base)).replaceAll("\\", "/");
|
|
9
|
+
function register(base, declared) {
|
|
10
|
+
registry.collections[base] ??= {
|
|
11
|
+
base,
|
|
12
|
+
...declared,
|
|
13
|
+
filePaths: [],
|
|
14
|
+
excluded: /* @__PURE__ */ new Set(),
|
|
15
|
+
loading: false,
|
|
16
|
+
loaded: false
|
|
17
|
+
};
|
|
18
|
+
return Object.assign(registry.collections[base], declared);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Astro's `glob()` loader, plus recording of the collection root and URL prefix so minidocs can
|
|
22
|
+
* derive locales, page ids and URLs, and generate types for them.
|
|
23
|
+
* Entries rejected by `filter` never reach the collection, so types, links and pages all agree on what exists.
|
|
24
|
+
*/
|
|
25
|
+
function glob({ prefix = "", filter, ...options }) {
|
|
26
|
+
const inner = glob$1(options);
|
|
27
|
+
const declared = { prefix: prefix.replace(/^\/+|\/+$/g, "") };
|
|
28
|
+
if (registry.root) register(relativeBase(options.base, registry.root), declared);
|
|
29
|
+
return {
|
|
30
|
+
...inner,
|
|
31
|
+
name: "minidocs-glob",
|
|
32
|
+
async load(context) {
|
|
33
|
+
const state = register(relativeBase(options.base, fileURLToPath(context.config.root)), declared);
|
|
34
|
+
if (state.name !== void 0 && state.name !== context.collection) throw new Error(`[minidocs] Collections "${state.name}" and "${context.collection}" are both loaded from ${state.base}.`);
|
|
35
|
+
state.name = context.collection;
|
|
36
|
+
let scheduled;
|
|
37
|
+
const update = () => {
|
|
38
|
+
clearTimeout(scheduled);
|
|
39
|
+
scheduled = setTimeout(() => writeTypes(context, state, (message) => context.logger.error(message)), 50);
|
|
40
|
+
};
|
|
41
|
+
const store = new Proxy(context.store, { get(target, property, receiver) {
|
|
42
|
+
const value = Reflect.get(target, property, receiver);
|
|
43
|
+
if (property !== "set" && property !== "delete" && property !== "clear") return value;
|
|
44
|
+
return (...args) => {
|
|
45
|
+
update();
|
|
46
|
+
if (property === "set" && filter) {
|
|
47
|
+
const entry = args[0];
|
|
48
|
+
if (!filter(entry)) {
|
|
49
|
+
if (entry.filePath) state.excluded.add(entry.filePath);
|
|
50
|
+
if (target.has(entry.id)) target.delete(entry.id);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (entry.filePath) state.excluded.delete(entry.filePath);
|
|
54
|
+
}
|
|
55
|
+
return value.apply(target, args);
|
|
56
|
+
};
|
|
57
|
+
} });
|
|
58
|
+
state.loading = true;
|
|
59
|
+
try {
|
|
60
|
+
await inner.load({
|
|
61
|
+
...context,
|
|
62
|
+
store
|
|
63
|
+
});
|
|
64
|
+
} finally {
|
|
65
|
+
state.loading = false;
|
|
66
|
+
}
|
|
67
|
+
state.loaded = true;
|
|
68
|
+
clearTimeout(scheduled);
|
|
69
|
+
writeTypes(context, state, (message) => {
|
|
70
|
+
throw new Error(message);
|
|
71
|
+
});
|
|
72
|
+
checkPendingLinks();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function writeTypes(context, state, report) {
|
|
77
|
+
const { settings, typesFile } = registry;
|
|
78
|
+
if (!settings || !typesFile) throw new Error("[minidocs] Add the minidocs() integration to your Astro config to use glob from '@minidocs/astro/loaders'.");
|
|
79
|
+
state.filePaths = context.store.values().map((entry) => entry.filePath).filter((filePath) => Boolean(filePath));
|
|
80
|
+
const collections = namedCollections();
|
|
81
|
+
const path = fileURLToPath(typesFile);
|
|
82
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
83
|
+
writeFileSync(path, renderTypes(settings, collections));
|
|
84
|
+
const conflicts = findUrlConflicts(settings, collections);
|
|
85
|
+
if (conflicts.length > 0) report(conflicts.map((conflict) => `[minidocs] ${conflict}`).join("\n"));
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { glob };
|