@twango/twiki 0.0.0-stage → 0.1.0-rc.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.
Files changed (225) hide show
  1. package/.github/actions/product-cache/action.yml +32 -0
  2. package/.github/actions/product-cache/index.mjs +450 -0
  3. package/LICENSE +21 -0
  4. package/README.md +146 -2
  5. package/bun.lock +1701 -0
  6. package/cli/runtime.test.ts +420 -0
  7. package/cli/runtime.ts +385 -0
  8. package/cli/twiki.ts +305 -0
  9. package/package.json +138 -4
  10. package/site/_headers +7 -0
  11. package/site/assets/fonts/OFL.txt +93 -0
  12. package/site/assets/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
  13. package/site/assets/social-background.svg +38 -0
  14. package/site/bun.lock +1701 -0
  15. package/site/database/index.ts +7 -0
  16. package/site/database/schema.ts +95 -0
  17. package/site/drizzle.config.ts +7 -0
  18. package/site/migrations/0001_auth.sql +14 -0
  19. package/site/migrations/0002_site_access.sql +5 -0
  20. package/site/migrations/0003_github_username.sql +1 -0
  21. package/site/migrations/meta/0002_snapshot.json +432 -0
  22. package/site/migrations/meta/0003_snapshot.json +439 -0
  23. package/site/migrations/meta/_journal.json +27 -0
  24. package/site/package.json +120 -0
  25. package/site/src/app.d.ts +14 -0
  26. package/site/src/app.html +11 -0
  27. package/site/src/hooks.server.ts +12 -0
  28. package/site/src/lib/analytics-config.json +1 -0
  29. package/site/src/lib/analytics.ts +40 -0
  30. package/site/src/lib/auth-config.json +3 -0
  31. package/site/src/lib/components/SEO.svelte +113 -0
  32. package/site/src/lib/components/doc/AccessLink.svelte +27 -0
  33. package/site/src/lib/components/doc/Backlinks.svelte +59 -0
  34. package/site/src/lib/components/doc/Breadcrumbs.svelte +29 -0
  35. package/site/src/lib/components/doc/ContentMeta.svelte +61 -0
  36. package/site/src/lib/components/doc/DocPager.svelte +91 -0
  37. package/site/src/lib/components/doc/DocShell.svelte +115 -0
  38. package/site/src/lib/components/doc/LicenseNotice.svelte +26 -0
  39. package/site/src/lib/components/doc/LockedContent.svelte +29 -0
  40. package/site/src/lib/components/doc/NavTree.svelte +158 -0
  41. package/site/src/lib/components/doc/NoteActions.svelte +139 -0
  42. package/site/src/lib/components/doc/PublicationBadge.svelte +49 -0
  43. package/site/src/lib/components/doc/ReaderModeToggle.svelte +19 -0
  44. package/site/src/lib/components/doc/Sidebar.svelte +351 -0
  45. package/site/src/lib/components/doc/Toc.svelte +339 -0
  46. package/site/src/lib/components/doc/enhancements.ts +146 -0
  47. package/site/src/lib/components/doc/heading-tracker.svelte.ts +39 -0
  48. package/site/src/lib/components/doc/image-preview.ts +114 -0
  49. package/site/src/lib/components/doc/reader-mode.svelte.ts +40 -0
  50. package/site/src/lib/components/embeds/mermaid.ts +353 -0
  51. package/site/src/lib/components/files/FileCode.svelte +81 -0
  52. package/site/src/lib/components/files/FileExplorer.svelte +74 -0
  53. package/site/src/lib/components/files/FileHistory.svelte +170 -0
  54. package/site/src/lib/components/files/FileIcon.svelte +28 -0
  55. package/site/src/lib/components/files/FileRmd.svelte +209 -0
  56. package/site/src/lib/components/files/FileTabs.svelte +180 -0
  57. package/site/src/lib/components/files/FileTree.svelte +67 -0
  58. package/site/src/lib/components/files/blame-gutter.ts +45 -0
  59. package/site/src/lib/components/files/code-view.ts +98 -0
  60. package/site/src/lib/components/files/file-workspace.svelte.ts +100 -0
  61. package/site/src/lib/components/graph/GraphPanel.svelte +275 -0
  62. package/site/src/lib/components/graph/GraphView.svelte +100 -0
  63. package/site/src/lib/components/graph/graph-data.ts +137 -0
  64. package/site/src/lib/components/graph/graph-model.ts +103 -0
  65. package/site/src/lib/components/graph/index.ts +3 -0
  66. package/site/src/lib/components/graph/render-graph.ts +626 -0
  67. package/site/src/lib/components/popover/index.ts +1 -0
  68. package/site/src/lib/components/popover/link-popovers.ts +233 -0
  69. package/site/src/lib/components/popover/popover.css +82 -0
  70. package/site/src/lib/components/search/SearchButton.svelte +57 -0
  71. package/site/src/lib/components/search/SearchPalette.svelte +189 -0
  72. package/site/src/lib/components/search/index.ts +3 -0
  73. package/site/src/lib/components/search/pagefind-client.svelte.ts +129 -0
  74. package/site/src/lib/components/search/pagefind-query.ts +22 -0
  75. package/site/src/lib/components/search/pagefind-url.ts +12 -0
  76. package/site/src/lib/components/search/search-state.svelte.ts +22 -0
  77. package/site/src/lib/components/ui/IconButton.svelte +30 -0
  78. package/site/src/lib/components/ui/ThemeToggle.svelte +22 -0
  79. package/site/src/lib/components/ui/index.ts +1 -0
  80. package/site/src/lib/config.ts +9 -0
  81. package/site/src/lib/content-license.ts +7 -0
  82. package/site/src/lib/file-history.ts +37 -0
  83. package/site/src/lib/file-icons.ts +27 -0
  84. package/site/src/lib/file-tabs.ts +72 -0
  85. package/site/src/lib/files.ts +73 -0
  86. package/site/src/lib/github-user.ts +70 -0
  87. package/site/src/lib/metadata.ts +166 -0
  88. package/site/src/lib/notebook-nav.ts +63 -0
  89. package/site/src/lib/publication.ts +1 -0
  90. package/site/src/lib/rmd.ts +156 -0
  91. package/site/src/lib/server/access-admin.ts +33 -0
  92. package/site/src/lib/server/content-model.ts +253 -0
  93. package/site/src/lib/server/content-provider.ts +266 -0
  94. package/site/src/lib/server/content.ts +277 -0
  95. package/site/src/lib/server/runtime-content.ts +34 -0
  96. package/site/src/lib/site-config.json +9 -0
  97. package/site/src/lib/social-image.ts +13 -0
  98. package/site/src/lib/styles/article.css +607 -0
  99. package/site/src/lib/styles/foundation.css +138 -0
  100. package/site/src/lib/styles/print.css +115 -0
  101. package/site/src/lib/styles/shell.css +182 -0
  102. package/site/src/lib/text-exports.ts +5 -0
  103. package/site/src/lib/types.ts +140 -0
  104. package/site/src/lib/utils.ts +6 -0
  105. package/site/src/routes/+error.svelte +16 -0
  106. package/site/src/routes/+layout.server.ts +10 -0
  107. package/site/src/routes/+layout.svelte +72 -0
  108. package/site/src/routes/+layout.ts +3 -0
  109. package/site/src/routes/404/+page.svelte +13 -0
  110. package/site/src/routes/[...slug]/+page.server.ts +24 -0
  111. package/site/src/routes/[...slug]/+page.svelte +180 -0
  112. package/site/src/routes/[course]/files/[...file]/+page.svelte +373 -0
  113. package/site/src/routes/[course]/files/[...file]/+page.ts +38 -0
  114. package/site/src/routes/admin/access/+page.server.ts +53 -0
  115. package/site/src/routes/admin/access/+page.svelte +118 -0
  116. package/site/src/routes/graph.json/+server.ts +7 -0
  117. package/site/src/routes/index.xml/+server.ts +7 -0
  118. package/site/src/routes/layout.css +10 -0
  119. package/site/src/routes/robots.txt/+server.ts +9 -0
  120. package/site/src/routes/sitemap.xml/+server.ts +7 -0
  121. package/site/src/routes/tags/+page.server.ts +5 -0
  122. package/site/src/routes/tags/+page.svelte +38 -0
  123. package/site/src/routes/tags/[...tag]/+page.server.ts +22 -0
  124. package/site/src/routes/tags/[...tag]/+page.svelte +52 -0
  125. package/site/static/favicon.png +0 -0
  126. package/site/static/fonts/JetBrainsMono-Italic-VF.woff2 +0 -0
  127. package/site/static/fonts/JetBrainsMono-VF.woff2 +0 -0
  128. package/site/static/fonts/OverusedGrotesk-VF.woff2 +0 -0
  129. package/site/static/fonts/OverusedGroteskItalic-VF.woff2 +0 -0
  130. package/site/static/fonts/Virgil.woff2 +0 -0
  131. package/site/svelte.config.js +71 -0
  132. package/site/tooling/LICENSE-quartz +25 -0
  133. package/site/tooling/adapter.js +55 -0
  134. package/site/tooling/assets.ts +323 -0
  135. package/site/tooling/benchmark.ts +191 -0
  136. package/site/tooling/build.ts +407 -0
  137. package/site/tooling/cache.ts +133 -0
  138. package/site/tooling/check-diagrams.ts +164 -0
  139. package/site/tooling/check-links.ts +86 -0
  140. package/site/tooling/check-targets.ts +139 -0
  141. package/site/tooling/compiler.ts +1386 -0
  142. package/site/tooling/deploy.ts +112 -0
  143. package/site/tooling/fingerprint.ts +81 -0
  144. package/site/tooling/lib/app-version.js +92 -0
  145. package/site/tooling/lib/application-asset-urls.ts +27 -0
  146. package/site/tooling/lib/application-cache.ts +269 -0
  147. package/site/tooling/lib/asset-hash-cache.ts +116 -0
  148. package/site/tooling/lib/autotag.ts +99 -0
  149. package/site/tooling/lib/build-assembly.ts +148 -0
  150. package/site/tooling/lib/build-workspace.ts +386 -0
  151. package/site/tooling/lib/cache-selection.ts +70 -0
  152. package/site/tooling/lib/cloudflare-prewarm.js +40 -0
  153. package/site/tooling/lib/comments.ts +32 -0
  154. package/site/tooling/lib/content-cache.ts +244 -0
  155. package/site/tooling/lib/content-urls.ts +50 -0
  156. package/site/tooling/lib/course-cache.ts +298 -0
  157. package/site/tooling/lib/course-files.ts +281 -0
  158. package/site/tooling/lib/currency.ts +33 -0
  159. package/site/tooling/lib/deployment-config.ts +222 -0
  160. package/site/tooling/lib/deployment-manifest.ts +274 -0
  161. package/site/tooling/lib/deployment-prepare.ts +325 -0
  162. package/site/tooling/lib/deployment-upload.ts +293 -0
  163. package/site/tooling/lib/dev-git-state.ts +130 -0
  164. package/site/tooling/lib/dev-history.ts +86 -0
  165. package/site/tooling/lib/dev-social-images.ts +38 -0
  166. package/site/tooling/lib/dev-state.ts +107 -0
  167. package/site/tooling/lib/edition-builds.ts +184 -0
  168. package/site/tooling/lib/edition-paths.js +29 -0
  169. package/site/tooling/lib/empty-tag-route.js +23 -0
  170. package/site/tooling/lib/file-history.ts +201 -0
  171. package/site/tooling/lib/file-icons.ts +93 -0
  172. package/site/tooling/lib/file-policy.ts +38 -0
  173. package/site/tooling/lib/file-search.js +13 -0
  174. package/site/tooling/lib/git-revisions.ts +41 -0
  175. package/site/tooling/lib/history-pool.ts +74 -0
  176. package/site/tooling/lib/history-worker.ts +22 -0
  177. package/site/tooling/lib/knit-rmd.R +38 -0
  178. package/site/tooling/lib/lastmod.ts +201 -0
  179. package/site/tooling/lib/markdown-files.ts +51 -0
  180. package/site/tooling/lib/ofm.ts +634 -0
  181. package/site/tooling/lib/output.ts +60 -0
  182. package/site/tooling/lib/product-cache.ts +1006 -0
  183. package/site/tooling/lib/project-config.ts +288 -0
  184. package/site/tooling/lib/public-assets.ts +38 -0
  185. package/site/tooling/lib/public-links.ts +56 -0
  186. package/site/tooling/lib/publishing.ts +156 -0
  187. package/site/tooling/lib/r-runtime.ts +257 -0
  188. package/site/tooling/lib/rebuild-queue.ts +25 -0
  189. package/site/tooling/lib/rmd-preflight.ts +166 -0
  190. package/site/tooling/lib/rmd-preview-cache.ts +93 -0
  191. package/site/tooling/lib/rmd-previews.ts +129 -0
  192. package/site/tooling/lib/runtime-data-modules.ts +30 -0
  193. package/site/tooling/lib/runtime-packaging.ts +321 -0
  194. package/site/tooling/lib/search-cache.js +165 -0
  195. package/site/tooling/lib/search-inputs.js +129 -0
  196. package/site/tooling/lib/slug.ts +377 -0
  197. package/site/tooling/lib/social-images.ts +210 -0
  198. package/site/tooling/lib/text-exports.ts +51 -0
  199. package/site/tooling/lib/visit-elements.ts +17 -0
  200. package/site/tooling/live-test.ts +138 -0
  201. package/site/tooling/pipeline.ts +103 -0
  202. package/site/tooling/prepare.ts +3 -0
  203. package/site/tooling/publish.ts +28 -0
  204. package/site/tooling/r-cache.ts +45 -0
  205. package/site/tooling/r-render-profile.json +7 -0
  206. package/site/tooling/search.js +94 -0
  207. package/site/tooling/verify-deployment.ts +109 -0
  208. package/site/tooling/vite-content.ts +240 -0
  209. package/site/tooling/wrangler-paths.js +25 -0
  210. package/site/tooling/wrangler.ts +19 -0
  211. package/site/tsconfig.json +16 -0
  212. package/site/vite.config.ts +40 -0
  213. package/site/worker/access.ts +57 -0
  214. package/site/worker/analytics.ts +100 -0
  215. package/site/worker/auth-routes.ts +78 -0
  216. package/site/worker/auth.ts +78 -0
  217. package/site/worker/content-runtime.ts +226 -0
  218. package/site/worker/framing.ts +4 -0
  219. package/site/worker/gate.ts +147 -0
  220. package/site/worker/index.ts +9 -0
  221. package/site/worker/login.ts +62 -0
  222. package/site/worker/publication.ts +44 -0
  223. package/site/worker/runtime.ts +104 -0
  224. package/site/worker/tsconfig.json +13 -0
  225. package/site/wrangler.jsonc +37 -0
@@ -0,0 +1,86 @@
1
+ /** Reject deployed-site URLs in notes: internal destinations belong in wikilinks. */
2
+ import { readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { unified } from 'unified';
6
+ import remarkParse from 'remark-parse';
7
+ import wikiLink from '@flowershow/remark-wiki-link';
8
+ import remarkGfm from 'remark-gfm';
9
+ import remarkRehype from 'remark-rehype';
10
+ import rehypeRaw from 'rehype-raw';
11
+ import { visit } from 'unist-util-visit';
12
+ import { stripObsidianComments } from './lib/comments';
13
+ import { site } from '../src/lib/config';
14
+ import { resolveProject } from './lib/project-config';
15
+ import { markdownFiles } from './lib/markdown-files';
16
+
17
+ const parser = unified()
18
+ .use(remarkParse)
19
+ .use(remarkGfm)
20
+ .use(wikiLink, {
21
+ urlResolver: ({ filePath, heading }) => filePath + (heading ? `#${heading}` : '')
22
+ })
23
+ .use(remarkRehype, { allowDangerousHtml: true })
24
+ .use(rehypeRaw);
25
+
26
+ export function directSiteLinks(markdown: string, origin = site.url) {
27
+ // Blank metadata and comments without shifting diagnostics in the original file.
28
+ const source = stripObsidianComments(
29
+ markdown.replace(/^---\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)(?:\r?\n|$)/, (s) =>
30
+ s.replace(/[^\r\n]/g, ' ')
31
+ ),
32
+ true
33
+ );
34
+ const tree = parser.runSync(parser.parse(source));
35
+ // An HTML comment can exempt the next source line, e.g. a README's site link.
36
+ const ignoredLines = new Set<number>();
37
+ visit(tree, 'comment', (node) => {
38
+ if (node.value.trim() === 'check-links-ignore-next-line' && node.position)
39
+ ignoredLines.add(node.position.end.line + 1);
40
+ });
41
+ const links: { url: string; line: number; column: number }[] = [];
42
+ visit(tree, 'element', (node) => {
43
+ if (node.position && ignoredLines.has(node.position.start.line)) return;
44
+ for (const property of ['href', 'src', 'data', 'poster']) {
45
+ const value = node.properties[property];
46
+ if (typeof value !== 'string' || !/^(?:https?:)?\/\//i.test(value)) continue;
47
+ let url: URL;
48
+ try {
49
+ url = new URL(value, origin);
50
+ } catch {
51
+ continue;
52
+ }
53
+ if (
54
+ url.hostname.toLowerCase().replace(/\.$/, '') !==
55
+ new URL(origin).hostname.toLowerCase().replace(/\.$/, '')
56
+ )
57
+ continue;
58
+ links.push({
59
+ url: value,
60
+ line: node.position?.start.line ?? 1,
61
+ column: node.position?.start.column ?? 1
62
+ });
63
+ }
64
+ });
65
+ return links;
66
+ }
67
+
68
+ if (import.meta.main) {
69
+ const inputs = process.argv.slice(2);
70
+ const project = resolveProject();
71
+ const content = project.contentDir;
72
+ const files = markdownFiles(inputs.length ? inputs : [content], /\.(?:md|rmd)$/i);
73
+ let failures = 0;
74
+ for (const file of files) {
75
+ for (const link of directSiteLinks(readFileSync(file, 'utf8'), project.site.url)) {
76
+ console.error(
77
+ `${path.relative(process.cwd(), file)}:${link.line}:${link.column}: Use an Obsidian wikilink instead of ${link.url}`
78
+ );
79
+ failures++;
80
+ }
81
+ }
82
+ console.log(
83
+ `Checked ${files.length} Markdown/R Markdown files; ${failures} direct same-site links.`
84
+ );
85
+ process.exitCode = failures ? 1 : 0;
86
+ }
@@ -0,0 +1,139 @@
1
+ /** Validate local link destinations with Lychee, using the site's wikilink resolution. */
2
+ import { spawnSync } from 'node:child_process';
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+ import { unified } from 'unified';
8
+ import remarkParse from 'remark-parse';
9
+ import remarkGfm from 'remark-gfm';
10
+ import remarkMath from 'remark-math';
11
+ import remarkRehype from 'remark-rehype';
12
+ import rehypeRaw from 'rehype-raw';
13
+ import wikiLink from '@flowershow/remark-wiki-link';
14
+ import { toHtml } from 'hast-util-to-html';
15
+ import { visit } from 'unist-util-visit';
16
+ import { resolveProject } from './lib/project-config';
17
+ import { markdownFiles } from './lib/markdown-files';
18
+ import { stripObsidianComments } from './lib/comments';
19
+ import {
20
+ slugifyFilePath,
21
+ simplifySlug,
22
+ stripSlashes,
23
+ transformLink,
24
+ type FilePath
25
+ } from './lib/slug';
26
+
27
+ export function targetRenderer(content: string, files: string[]) {
28
+ const destinations = new Map(
29
+ files.map((file) => [slugifyFilePath(path.relative(content, file) as FilePath), file])
30
+ );
31
+ const allSlugs = [...destinations.keys()];
32
+ return (sourceFile: string, markdown: string) => {
33
+ const sourceSlug = slugifyFilePath(path.relative(content, sourceFile) as FilePath);
34
+ const parser = unified()
35
+ .use(remarkParse)
36
+ .use(remarkGfm)
37
+ .use(remarkMath)
38
+ .use(wikiLink, {
39
+ urlResolver: ({ filePath, heading }) => {
40
+ // Flowershow 3.x includes the escaped table-alias separator's backslash.
41
+ filePath = filePath.replace(/\\$/, '').trim();
42
+ heading = heading.replace(/\\$/, '').trim();
43
+ if (!filePath) return pathToFileURL(sourceFile).href;
44
+ if (/^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(filePath)) return filePath;
45
+ const resolved = transformLink(sourceSlug, filePath, { strategy: 'shortest', allSlugs });
46
+ const url = new URL(
47
+ resolved,
48
+ `https://vault.invalid/${stripSlashes(simplifySlug(sourceSlug), true)}`
49
+ );
50
+ const key = decodeURIComponent(url.pathname.slice(1));
51
+ const target =
52
+ destinations.get(key as typeof sourceSlug) ??
53
+ destinations.get(`${key.replace(/\/$/, '')}/index` as typeof sourceSlug) ??
54
+ path.join(content, key);
55
+ return pathToFileURL(target).href + (heading ? `#${encodeURIComponent(heading)}` : '');
56
+ }
57
+ })
58
+ .use(remarkRehype, { allowDangerousHtml: true })
59
+ .use(rehypeRaw);
60
+ const source = stripObsidianComments(
61
+ markdown.replace(/^---\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)(?:\r?\n|$)/, (s) =>
62
+ s.replace(/[^\r\n]/g, ' ')
63
+ ),
64
+ true
65
+ );
66
+ const tree = parser.runSync(parser.parse(source));
67
+ const lines = source.split('\n').map(() => '');
68
+ visit(tree, 'element', (node) => {
69
+ for (const attribute of ['href', 'src', 'poster', 'data']) {
70
+ const value = node.properties[attribute];
71
+ if (typeof value !== 'string' || !value || value.startsWith('#')) continue;
72
+ // Normal Markdown links resolve relative to their source file.
73
+ const url = new URL(value, pathToFileURL(sourceFile));
74
+ if (url.protocol !== 'file:') continue; // deterministic, offline local audit
75
+ const line = (node.position?.start.line ?? 1) - 1;
76
+ lines[line] += toHtml({
77
+ type: 'element',
78
+ tagName: 'a',
79
+ properties: { href: url.href },
80
+ children: []
81
+ });
82
+ }
83
+ });
84
+ return lines.join('\n');
85
+ };
86
+ }
87
+
88
+ if (import.meta.main) {
89
+ const content = resolveProject().contentDir;
90
+ const inputs = process.argv.slice(2);
91
+ const files = markdownFiles([content], /./);
92
+ const sources = inputs.length
93
+ ? markdownFiles(inputs, /\.(?:md|rmd)$/i)
94
+ : files.filter((f) => /\.(?:md|rmd)$/i.test(f));
95
+ const render = targetRenderer(content, files);
96
+ const bundled = fileURLToPath(new URL('../build/tools/lychee', import.meta.url));
97
+ const lychee = process.env.LYCHEE ?? (existsSync(bundled) ? bundled : 'lychee');
98
+ const scratch = mkdtempSync(path.join(tmpdir(), 'wisconsin-link-targets-'));
99
+ try {
100
+ const originals = new Map<string, string>();
101
+ for (const [index, source] of sources.entries()) {
102
+ const html = path.join(scratch, `${index}.html`);
103
+ writeFileSync(html, render(source, readFileSync(source, 'utf8')));
104
+ originals.set(html, source);
105
+ }
106
+ const list = path.join(scratch, 'inputs.txt');
107
+ writeFileSync(list, [...originals.keys()].join('\n'));
108
+ const result = spawnSync(lychee, ['--offline', '--format', 'json', '--files-from', list], {
109
+ encoding: 'utf8',
110
+ maxBuffer: 32 * 1024 * 1024
111
+ });
112
+ if (result.error)
113
+ throw new Error(
114
+ 'Lychee is required. Install lychee 0.24.2 or set LYCHEE to its executable.',
115
+ { cause: result.error }
116
+ );
117
+ if (!result.stdout.trim()) throw new Error(result.stderr || `Lychee exited ${result.status}`);
118
+ const report = JSON.parse(result.stdout) as {
119
+ total: number;
120
+ errors: number;
121
+ error_map: Record<
122
+ string,
123
+ { url: string; span?: { line: number }; status: { text: string } }[]
124
+ >;
125
+ };
126
+ for (const [source, failures] of Object.entries(report.error_map)) {
127
+ for (const failure of failures)
128
+ console.error(
129
+ `${path.relative(process.cwd(), originals.get(source) ?? source)}:${failure.span?.line ?? 1}: ${failure.status.text} (${failure.url})`
130
+ );
131
+ }
132
+ console.log(
133
+ `Lychee checked ${report.total} local links in ${sources.length} Markdown/R Markdown files; ${report.errors} errors.`
134
+ );
135
+ process.exitCode = result.status ?? 1;
136
+ } finally {
137
+ rmSync(scratch, { recursive: true, force: true });
138
+ }
139
+ }