@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,138 @@
1
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolveProject } from './lib/project-config';
3
+ import { CloudflareDeploymentAPI } from './lib/deployment-upload';
4
+
5
+ const mode = process.argv[2];
6
+ const project = resolveProject();
7
+ const origin = project.site.url;
8
+ const worker = project.cloudflare?.name;
9
+ const requestedPaths = process.env.TWIKI_LIVE_PATHS;
10
+ const testPaths: string[] = requestedPaths ? JSON.parse(requestedPaths) : ['/'];
11
+ if (
12
+ !Array.isArray(testPaths) ||
13
+ testPaths.some(
14
+ (value) => typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//')
15
+ )
16
+ )
17
+ throw new Error('TWIKI_LIVE_PATHS must be a JSON array of local URL paths');
18
+ const directory = 'build/benchmarks/live';
19
+ mkdirSync(directory, { recursive: true });
20
+ if (mode === 'capture' || mode === 'verify-rollback') {
21
+ if (typeof worker !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(worker))
22
+ throw new Error('Configure the Worker name before live capture');
23
+ const account = process.env.CLOUDFLARE_ACCOUNT_ID;
24
+ if (!account || !/^[a-f0-9]{32}$/.test(account)) throw new Error('Missing account identity');
25
+ const api = new CloudflareDeploymentAPI(process.env.CLOUDFLARE_API_TOKEN ?? '');
26
+ const result = (await api.request(
27
+ `/accounts/${account}/workers/scripts/${worker}/deployments`
28
+ )) as {
29
+ deployments: { versions: { version_id: string; percentage: number }[] }[];
30
+ };
31
+ const versions = result.deployments[0]?.versions;
32
+ if (
33
+ versions?.length !== 1 ||
34
+ versions[0].percentage !== 100 ||
35
+ !/^[a-f0-9-]{36}$/.test(versions[0].version_id)
36
+ )
37
+ throw new Error('Expected one active version');
38
+ const version = versions[0].version_id;
39
+ if (mode === 'verify-rollback') {
40
+ if (version !== process.env.ROLLBACK_VERSION) throw new Error('Rollback version is not active');
41
+ const response = await fetch(origin, { signal: AbortSignal.timeout(10_000) });
42
+ if (response.status !== 200 || (await response.text()).length < 1000)
43
+ throw new Error('Rollback HTML unavailable');
44
+ console.log(`Verified rollback: ${version}`);
45
+ process.exit(0);
46
+ }
47
+ writeFileSync(`${directory}/rollback.json`, JSON.stringify({ version }, null, 2));
48
+ appendFileSync(process.env.GITHUB_OUTPUT!, `version=${version}\n`);
49
+ console.log(`Rollback version: ${version}`);
50
+ } else if (mode === 'baseline' || mode === 'measure') {
51
+ const paths = testPaths;
52
+ const reports = [];
53
+ for (const pathname of paths) {
54
+ const samples = [];
55
+ for (let index = 0; index < 9; index++) {
56
+ const started = performance.now();
57
+ const response = await fetch(origin + pathname, {
58
+ redirect: 'manual',
59
+ signal: AbortSignal.timeout(10_000)
60
+ });
61
+ const headersMs = performance.now() - started;
62
+ const body = await response.text();
63
+ if (
64
+ response.status !== 200 ||
65
+ !response.headers.get('content-type')?.includes('text/html') ||
66
+ body.length < 1000
67
+ )
68
+ throw new Error('Public HTML acceptance failed');
69
+ if (mode === 'measure' && !response.headers.get('x-wisconsin-application-version'))
70
+ throw new Error('SSR version header missing');
71
+ samples.push({
72
+ headersMs,
73
+ totalMs: performance.now() - started,
74
+ colo: response.headers.get('cf-ray')?.split('-').at(-1)
75
+ });
76
+ }
77
+ const warm = samples
78
+ .slice(1)
79
+ .map((value) => value.headersMs)
80
+ .sort((a, b) => a - b);
81
+ reports.push({
82
+ path: pathname,
83
+ firstRequestMs: samples[0].headersMs,
84
+ warmMedianMs: (warm[3] + warm[4]) / 2,
85
+ samples
86
+ });
87
+ }
88
+ writeFileSync(`${directory}/${mode}.json`, JSON.stringify(reports, null, 2));
89
+ console.log(JSON.stringify({ mode, reports }));
90
+ if (mode === 'measure') {
91
+ const descriptor = JSON.parse(
92
+ readFileSync('build/.svelte-kit/cloudflare/_content/current.json', 'utf8')
93
+ );
94
+ for (const pathname of [
95
+ '/_content/current.json',
96
+ `/_content/full/${descriptor.snapshots.full}/manifest.json`,
97
+ '/_published/index.html'
98
+ ]) {
99
+ const response = await fetch(origin + pathname, {
100
+ redirect: 'manual',
101
+ signal: AbortSignal.timeout(10_000)
102
+ });
103
+ await response.body?.cancel();
104
+ if (![401, 403, 404].includes(response.status))
105
+ throw new Error('Internal asset access was not denied');
106
+ }
107
+ for (const cookie of process.env.TWIKI_LIVE_PRIVATE_PATH
108
+ ? ['', 'better-auth.session_token=invalid']
109
+ : []) {
110
+ const privatePath = process.env.TWIKI_LIVE_PRIVATE_PATH!;
111
+ if (!privatePath.startsWith('/') || privatePath.startsWith('//'))
112
+ throw new Error('Invalid private test path');
113
+ const response = await fetch(origin + privatePath, {
114
+ headers: { cookie },
115
+ signal: AbortSignal.timeout(10_000)
116
+ });
117
+ if (response.status !== 200 || !(await response.text()).includes('Content locked'))
118
+ throw new Error('Private note did not render a locked preview');
119
+ }
120
+ const baseline = JSON.parse(
121
+ readFileSync(`${directory}/baseline.json`, 'utf8')
122
+ ) as typeof reports;
123
+ for (let i = 0; i < reports.length; i++) {
124
+ if (
125
+ reports[i].warmMedianMs > Math.max(500, baseline[i].warmMedianMs * 2) ||
126
+ reports[i].firstRequestMs > 3000
127
+ )
128
+ throw new Error('Live response regression exceeded acceptance bounds');
129
+ }
130
+ }
131
+ writeFileSync(`${directory}/${mode}.json`, JSON.stringify(reports, null, 2));
132
+ console.log(JSON.stringify({ mode, reports }));
133
+ if (process.env.GITHUB_STEP_SUMMARY)
134
+ appendFileSync(
135
+ process.env.GITHUB_STEP_SUMMARY,
136
+ `### ${mode}\n\n\`\`\`json\n${JSON.stringify(reports, null, 2)}\n\`\`\`\n`
137
+ );
138
+ } else throw new Error('Expected capture, baseline or measure');
@@ -0,0 +1,103 @@
1
+ import { existsSync, readFileSync, rmSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { stageFingerprint } from './fingerprint';
4
+ import { trackOutputs, writeChanged } from './lib/output';
5
+ import { contentInputFingerprint, contentOutputFingerprint } from './lib/dev-state';
6
+ import { rRendererIdentity } from './lib/r-runtime';
7
+ import { resolveProject, projectIdentity, writeProjectSiteConfig } from './lib/project-config';
8
+
9
+ const state = globalThis as typeof globalThis & {
10
+ wisconsinContentBuilds?: Map<string, Promise<Set<string>>>;
11
+ };
12
+
13
+ export async function prepareContent(
14
+ reuse = false,
15
+ changedInputs?: Set<string>,
16
+ development = false
17
+ ) {
18
+ const builds = (state.wisconsinContentBuilds ??= new Map());
19
+ const site = process.cwd();
20
+ const project = resolveProject(site);
21
+ const repo = project.root;
22
+ writeProjectSiteConfig(site, project);
23
+ const key = JSON.stringify([
24
+ process.cwd(),
25
+ projectIdentity(project),
26
+ process.env.VITE_PUBLIC_EDITION,
27
+ await stageFingerprint('pipeline'),
28
+ development,
29
+ development ? rRendererIdentity(repo) : undefined
30
+ ]);
31
+ const marker = path.resolve('build/generated/dev-state.json');
32
+ const started = performance.now();
33
+ let inputs = development ? contentInputFingerprint(repo, key) : '';
34
+ if (development && reuse && existsSync(marker)) {
35
+ try {
36
+ const saved = JSON.parse(readFileSync(marker, 'utf8'));
37
+ if (saved.inputs === inputs && saved.outputs === contentOutputFingerprint(site)) {
38
+ console.log(
39
+ `content startup: reused saved output in ${Math.round(performance.now() - started)}ms`
40
+ );
41
+ return new Set<string>();
42
+ }
43
+ } catch {
44
+ /* Reconcile incomplete or stale generated output. */
45
+ }
46
+ }
47
+ if (
48
+ !development &&
49
+ reuse &&
50
+ process.env.WISCONSIN_PREPARED_CONTENT === key &&
51
+ existsSync('build/generated/content-manifest.json')
52
+ )
53
+ return;
54
+ if (
55
+ !development &&
56
+ reuse &&
57
+ existsSync(path.resolve('build/generated/content-manifest.json')) &&
58
+ builds.has(key)
59
+ )
60
+ return builds.get(key);
61
+ rmSync(marker, { force: true });
62
+ // A changed renderer or configuration must reconcile all courses, including unchanged worksheets.
63
+ const build = runPipeline(
64
+ development && process.env.WISCONSIN_PREPARED_CONTENT !== key ? undefined : changedInputs,
65
+ development
66
+ );
67
+ builds.set(key, build);
68
+ try {
69
+ const changed = await build;
70
+ if (development) {
71
+ let current = contentInputFingerprint(repo, key);
72
+ while (current !== inputs) {
73
+ inputs = current;
74
+ for (const file of await runPipeline(undefined, true)) changed.add(file);
75
+ current = contentInputFingerprint(repo, key);
76
+ }
77
+ writeChanged(marker, JSON.stringify({ inputs, outputs: contentOutputFingerprint(site) }));
78
+ }
79
+ process.env.WISCONSIN_PREPARED_CONTENT = key;
80
+ return changed;
81
+ } catch (error) {
82
+ builds.delete(key);
83
+ delete process.env.WISCONSIN_PREPARED_CONTENT;
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ async function runPipeline(changedInputs?: Set<string>, development = false) {
89
+ const start = performance.now();
90
+ // A saved development snapshot does not need the parser or asset renderers.
91
+ const [{ compileContent }, { prepareAssets }] = await Promise.all([
92
+ import('./compiler'),
93
+ import('./assets')
94
+ ]);
95
+ const changed = await trackOutputs(async () => {
96
+ await compileContent();
97
+ await prepareAssets(changedInputs, development);
98
+ });
99
+ console.log(
100
+ `content pipeline: ${((performance.now() - start) / 1000).toFixed(2)}s, ${changed.size} changed outputs`
101
+ );
102
+ return changed;
103
+ }
@@ -0,0 +1,3 @@
1
+ import { prepareContent } from './pipeline';
2
+
3
+ await prepareContent();
@@ -0,0 +1,28 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { coursePolicies, publicationDecision } from './lib/publishing';
4
+
5
+ const repo = path.resolve(import.meta.dir, '../..');
6
+ const policies = coursePolicies(repo);
7
+ const files = execFileSync(
8
+ 'git',
9
+ ['-C', repo, 'ls-files', '-z', '--recurse-submodules', '--', 'content'],
10
+ { maxBuffer: 1 << 28 }
11
+ )
12
+ .toString()
13
+ .split('\0')
14
+ .filter(Boolean);
15
+ const requested = process.argv[2]?.replace(/^content\//, '');
16
+ let count = 0;
17
+ for (const tracked of files) {
18
+ const relative = tracked.slice(8);
19
+ if (requested && relative !== requested && !relative.startsWith(requested + '/')) continue;
20
+ const [course, ...segments] = relative.split('/');
21
+ const decision = publicationDecision(segments.join('/'), policies.get(course));
22
+ if (decision.public) count++;
23
+ if (requested || decision.public)
24
+ console.log(`${decision.public ? 'PUBLIC ' : 'PRIVATE'} ${relative} — ${decision.reason}`);
25
+ }
26
+ console.log(
27
+ `${count} files selected for publication; normal content and file safety filters still apply.`
28
+ );
@@ -0,0 +1,45 @@
1
+ import path from 'node:path';
2
+ import { appendFileSync, lstatSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
3
+ import { captureRRenderProfile } from './lib/r-runtime';
4
+ import { resolveProject } from './lib/project-config';
5
+
6
+ const command = process.argv[2];
7
+ const site = path.resolve(import.meta.dirname, '..');
8
+ const options = process.argv.slice(3);
9
+ const ownedOutput = (file: string) => {
10
+ const absolute = path.resolve(file);
11
+ mkdirSync(path.dirname(absolute), { recursive: true });
12
+ const info = lstatSync(absolute, { throwIfNoEntry: false });
13
+ if (
14
+ realpathSync(path.dirname(absolute)) !== path.dirname(absolute) ||
15
+ (info && (!info.isFile() || realpathSync(absolute) !== absolute))
16
+ )
17
+ throw new Error('R cache output must be an owned regular file');
18
+ return absolute;
19
+ };
20
+ if (
21
+ command === 'capture-profile' &&
22
+ (!options.length || (options.length === 2 && options[0] === '--output'))
23
+ ) {
24
+ const bytes = JSON.stringify(captureRRenderProfile(), null, 2) + '\n';
25
+ if (options.length) writeFileSync(ownedOutput(options[1]), bytes);
26
+ console.log(bytes.trimEnd());
27
+ } else if (
28
+ command === 'preflight' &&
29
+ (!options.length || (options.length === 1 && options[0] === '--github-output'))
30
+ ) {
31
+ const { preflightRmdPreviews } = await import('./lib/rmd-preflight');
32
+ const result = await preflightRmdPreviews(site, resolveProject(site).root);
33
+ if (options.length) {
34
+ if (!process.env.GITHUB_OUTPUT)
35
+ throw new Error('GITHUB_OUTPUT is required with --github-output');
36
+ appendFileSync(
37
+ ownedOutput(process.env.GITHUB_OUTPUT),
38
+ `requiresR=${result.requiresR}\nworksheets=${result.worksheets}\nverified=${result.verified}\n`
39
+ );
40
+ }
41
+ console.log(JSON.stringify(result));
42
+ } else
43
+ throw new Error(
44
+ 'Use bun tooling/r-cache.ts capture-profile [--output <file>] or preflight [--github-output]'
45
+ );
@@ -0,0 +1,7 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "platform": "linux",
4
+ "architecture": "x64",
5
+ "runtime": "R version 4.3.3 (2024-02-29)knitr 1.45evaluate 0.23highr 0.10xfun 0.41yaml 2.3.8\nstructure(list(platform = \"x86_64-pc-linux-gnu\", arch = \"x86_64\", \n os = \"linux-gnu\", system = \"x86_64, linux-gnu\", status = \"\", \n major = \"4\", minor = \"3.3\", year = \"2024\", month = \"02\", \n day = \"29\", `svn rev` = \"86002\", language = \"R\", version.string = \"R version 4.3.3 (2024-02-29)\", \n nickname = \"Angel Food Cake\"), class = \"simple.list\")\nc(jpeg = TRUE, png = TRUE, tiff = TRUE, tcltk = TRUE, X11 = FALSE, \naqua = FALSE, `http/ftp` = TRUE, sockets = TRUE, libxml = FALSE, \nfifo = TRUE, cledit = FALSE, iconv = TRUE, NLS = TRUE, Rprof = TRUE, \nprofmem = TRUE, cairo = TRUE, ICU = TRUE, long.double = TRUE, \nlibcurl = TRUE)\nc(zlib = \"1.3\", bzlib = \"1.0.8, 13-Jul-2019\", xz = \"5.4.5\", PCRE = \"10.42 2022-12-11\", \nICU = \"74.2\", TRE = \"TRE 0.8.0 R_fixes (BSD)\", iconv = \"glibc 2.39\", \nreadline = \"8.2\", BLAS = \"/usr/lib/x86_64-linux-gnu/blas/libblas.so.3.12.0\"\n)\nc(cairo = \"1.18.0\", cairoFT = \"\", pango = \"1.52.1\", libpng = \"1.6.43\", \njpeg = \"8.0\", libtiff = \"LIBTIFF, Version 4.5.1\")\n\"LC_CTYPE=C.UTF-8;LC_NUMERIC=C;LC_TIME=C.UTF-8;LC_COLLATE=C.UTF-8;LC_MONETARY=C.UTF-8;LC_MESSAGES=C.UTF-8;LC_PAPER=C.UTF-8;LC_NAME=C;LC_ADDRESS=C;LC_TELEPHONE=C;LC_MEASUREMENT=C.UTF-8;LC_IDENTIFICATION=C\"\n\"Etc/UTC\"\n\n\nabind 1.4-5 4.0.1\nbackports 1.4.1 4.1.2\nbase 4.3.3 4.3.3\nbit 4.0.5 4.2.2\nbit64 4.0.5 4.0.2\nboot 1.3-30 4.3.2\nbrio 1.1.4 4.3.2\nbroom 1.0.5 4.3.0\ncallr 3.7.5 4.3.3\ncar 3.1-2 4.3.1\ncarData 3.0-5 4.1.2\ncaret 6.0-94 4.3.0\ncellranger 1.1.0 4.0.1\nclass 7.3-22 4.3.3\ncli 3.6.2 4.3.2\nclipr 0.8.0 4.1.2\nclock 0.7.0 4.3.1\ncodetools 0.2-19 4.2.2\ncolorspace 2.1-0 4.2.2\ncompiler 4.3.3 4.3.3\nconquer 1.3.3 4.2.2\ncpp11 0.4.7 4.3.2\ncrayon 1.5.2 4.2.2\ncurl 5.2.0 4.3.3\ndata.table 1.14.10 4.3.2\ndatasets 4.3.3 4.3.3\ndesc 1.4.3 4.3.2\ndiagram 1.6.5 4.3.1\ndiffobj 0.3.5 4.1.1\ndigest 0.6.34 4.3.2\ndplyr 1.1.4 4.3.2\ne1071 1.7-14 4.3.2\nellipsis 0.3.2 4.1.1\nevaluate 0.23 4.3.2\nfansi 1.0.5 4.3.2\nfarver 2.1.1 4.2.1\nforcats 1.0.0 4.2.2\nforeach 1.5.2 4.1.2\nforeign 0.8-86 4.3.3\nfs 1.6.3 4.3.3\nfuture 1.33.1 4.3.2\nfuture.apply 1.11.1 4.3.2\ngenerics 0.1.3 4.2.1\nggplot2 3.4.4 4.3.1\nglobals 0.16.2 4.2.2\nglue 1.7.0 4.3.2\ngower 1.0.1 4.2.2\ngraphics 4.3.3 4.3.3\ngrDevices 4.3.3 4.3.3\ngrid 4.3.3 4.3.3\ngtable 0.3.4 4.3.1\nhardhat 1.3.1 4.3.2\nhaven 2.5.4 4.3.2\nhighr 0.10 4.2.2\nhms 1.1.3 4.3.1\nipred 0.9-14 4.3.1\nisoband 0.2.7 4.2.2\niterators 1.0.14 4.1.2\njsonlite 1.8.8 4.3.2\nKernSmooth 2.23-22 4.3.3\nknitr 1.45 4.3.2\nlabeling 0.4.3 4.3.1\nlattice 0.22-5 4.3.3\nlava 1.7.3 4.3.2\nlifecycle 1.0.4 4.3.2\nlistenv 0.9.1 4.3.2\nlittler 0.3.19 4.3.2\nlme4 1.1-35.1 4.3.2\nlubridate 1.9.3 4.3.1\nmagrittr 2.0.3 4.1.2\nmaptools 1.1-8 4.3.1\nMASS 7.3-60.0.1 4.3.2\nMatrix 1.6-5 4.3.2\nMatrixModels 0.5-3 4.3.2\nmatrixStats 1.2.0 4.3.2\nmethods 4.3.3 4.3.3\nmgcv 1.9-1 4.3.2\nminqa 1.2.6 4.3.1\nModelMetrics 1.2.2.2 4.0.1\nmunsell 0.5.0 4.0.1\nnlme 3.1-164 4.3.3\nnloptr 2.0.3 4.2.0\nnnet 7.3-19 4.3.3\nnumDeriv 2016.8-1.1 4.0.1\nopenxlsx 4.2.5.2 4.2.2\nparallel 4.3.3 4.3.3\nparallelly 1.37.1 4.3.3\npbkrtest 0.5.2 4.2.2\npillar 1.9.0 4.3.0\npkgbuild 1.4.3 4.3.2\npkgconfig 2.0.3 4.0.1\npkgKitten 0.2.3 4.2.2\npkgload 1.3.4 4.3.2\nplyr 1.8.9 4.3.1\npraise 1.0.0 4.0.1\nprettyunits 1.2.0 4.3.1\npROC 1.18.5 4.3.2\nprocessx 3.8.3 4.3.2\nprodlim 2023.08.28 4.3.1\nprogress 1.2.3 4.3.2\nprogressr 0.14.0 4.3.1\nproxy 0.4-27 4.2.0\nps 1.7.6 4.3.2\npurrr 1.0.2 4.3.1\nquantreg 5.97 4.3.1\nR.methodsS3 1.8.2 4.2.0\nR.oo 1.26.0 4.3.2\nR.utils 2.12.3 4.3.2\nR6 2.5.1 4.1.1\nRColorBrewer 1.1-3 4.2.0\nRcpp 1.0.12 4.3.2\nRcppArmadillo 0.12.8.1.0 4.3.3\nRcppEigen 0.3.4.0.0 4.3.3\nreadr 2.1.5 4.3.2\nreadxl 1.4.3 4.3.1\nrecipes 1.0.9 4.3.2\nrematch 2.0.0 4.3.1\nrematch2 2.1.2 4.0.1\nreshape2 1.4.4 4.0.1\nrio 1.0.1 4.3.1\nrlang 1.1.3 4.3.2\nrpart 4.1.23 4.3.3\nrprojroot 2.0.4 4.3.2\nscales 1.3.0 4.3.2\nshape 1.4.6 4.1.1\nsp 2.1-2 4.3.2\nSparseM 1.81 4.0.4\nsplines 4.3.3 4.3.3\nSQUAREM 2021.1 4.0.3\nstatmod 1.5.0 4.2.2\nstats 4.3.3 4.3.3\nstats4 4.3.3 4.3.3\nstringi 1.8.3 4.3.2\nstringr 1.5.1 4.3.2\nsurvival 3.5-8 4.3.2\ntcltk 4.3.3 4.3.3\ntestthat 3.2.1 4.3.2\ntibble 3.2.1 4.3.1\ntidyr 1.3.1 4.3.2\ntidyselect 1.2.0 4.2.2\ntimechange 0.3.0 4.3.2\ntimeDate 4032.109 4.3.2\ntools 4.3.3 4.3.3\ntzdb 0.4.0 4.3.1\nutf8 1.2.4 4.3.1\nutils 4.3.3 4.3.3\nvctrs 0.6.5 4.3.2\nviridisLite 0.4.2 4.3.1\nvroom 1.6.5 4.3.2\nwaldo 0.5.2 4.3.2\nwithr 2.5.0 4.1.2\nwritexl 1.5.0 4.3.2\nxfun 0.41 4.3.2\nyaml 2.3.8 4.3.2\nzip 2.3.1 4.3.2\n",
6
+ "runtimeSha256": "4941ee37cbb4f1b5ffd5ae184b19328a2c828530d7985cbf1e8ace72a4f3d202"
7
+ }
@@ -0,0 +1,94 @@
1
+ import * as pagefind from 'pagefind';
2
+ import path from 'node:path';
3
+ import { readFileSync, rmSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { fileSearchRecord } from './lib/file-search.js';
6
+ import { searchHtmlInputs } from './lib/search-inputs.js';
7
+ import {
8
+ restoreSearchCache,
9
+ saveSearchCache,
10
+ searchInputKey,
11
+ selectSearchCache
12
+ } from './lib/search-cache.js';
13
+
14
+ /** @param {string} directory */
15
+ export async function indexSearch(directory) {
16
+ const start = performance.now();
17
+ const manifest = JSON.parse(readFileSync('build/generated/content-manifest.json', 'utf8'));
18
+ const html = searchHtmlInputs(manifest, (slug) =>
19
+ JSON.parse(readFileSync(path.join('build/generated/pages', `${slug}.json`), 'utf8'))
20
+ );
21
+ const entries = JSON.parse(readFileSync('src/lib/generated/file-entries.json', 'utf8'));
22
+ const records = entries.map(fileSearchRecord);
23
+ const edition = process.env.VITE_PUBLIC_EDITION === 'true' ? 'public' : 'full';
24
+ const pagefindVersion = JSON.parse(
25
+ readFileSync(
26
+ path.resolve(path.dirname(fileURLToPath(import.meta.resolve('pagefind'))), '../package.json'),
27
+ 'utf8'
28
+ )
29
+ ).version;
30
+ const key = searchInputKey({ html, records, pagefindVersion, edition });
31
+ const cacheRoot = path.resolve('build/generated/cache');
32
+ const cache = path.join(cacheRoot, 'search', edition, key);
33
+ const output = path.join(directory, 'pagefind');
34
+ if (restoreSearchCache(cache, output, key, edition)) {
35
+ const saved = JSON.parse(readFileSync(path.join(cache, 'manifest.json'), 'utf8'));
36
+ selectSearchCache(cacheRoot, edition, [
37
+ `search/${edition}/${key}/manifest.json`,
38
+ ...saved.files.map(
39
+ /** @param {{path: string}} file */ (file) => `search/${edition}/${key}/files/${file.path}`
40
+ )
41
+ ]);
42
+ console.log(
43
+ `search: ${html.length} pages + ${entries.length} file records in ${((performance.now() - start) / 1000).toFixed(2)}s (complete index cached)`
44
+ );
45
+ return;
46
+ }
47
+ try {
48
+ const { index, errors } = await pagefind.createIndex();
49
+ if (!index) throw new Error(errors.join('\n'));
50
+ for (let offset = 0; offset < html.length; offset += 64) {
51
+ const results = await Promise.all(
52
+ html.slice(offset, offset + 64).map((input) => index.addHTMLFile(input))
53
+ );
54
+ for (const result of results)
55
+ if (result.errors.length) throw new Error(result.errors.join('\n'));
56
+ }
57
+ // Bound submissions rather than queueing the whole catalog at once.
58
+ for (let offset = 0; offset < records.length; offset += 64) {
59
+ const results = await Promise.all(
60
+ records
61
+ .slice(offset, offset + 64)
62
+ .map(
63
+ /** @param {import('pagefind').CustomRecord} record */ (record) =>
64
+ index.addCustomRecord(record)
65
+ )
66
+ );
67
+ for (const result of results)
68
+ if (result.errors.length) throw new Error(result.errors.join('\n'));
69
+ }
70
+ // The index owns this directory; remove old generations on a cache miss.
71
+ rmSync(output, { recursive: true, force: true });
72
+ const written = await index.writeFiles({ outputPath: output });
73
+ if (written.errors.length) throw new Error(written.errors.join('\n'));
74
+ try {
75
+ selectSearchCache(cacheRoot, edition, saveSearchCache(cacheRoot, output, key, edition));
76
+ } catch {
77
+ selectSearchCache(cacheRoot, edition, []);
78
+ console.log(
79
+ 'search: complete index cache could not be saved; generated index remains available'
80
+ );
81
+ }
82
+ console.log(
83
+ `search: ${html.length} pages + ${entries.length} file records in ${((performance.now() - start) / 1000).toFixed(2)}s`
84
+ );
85
+ } finally {
86
+ await pagefind.close();
87
+ }
88
+ }
89
+
90
+ // The build orchestrator runs this in the same Node runtime as Vite's adapter.
91
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
92
+ if (!process.argv[2]) throw new Error('Expected Pagefind output directory');
93
+ await indexSearch(process.argv[2]);
94
+ }
@@ -0,0 +1,109 @@
1
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { site } from '../src/lib/config';
4
+
5
+ interface Descriptor {
6
+ schemaVersion: number;
7
+ applicationVersion: string;
8
+ snapshots: { public: string; full: string };
9
+ }
10
+
11
+ export function matchesDeployment(response: Response, descriptor: Descriptor) {
12
+ return (
13
+ response.status === 200 &&
14
+ response.headers.get('content-type')?.includes('text/html') === true &&
15
+ response.headers.get('x-wisconsin-content-version') === descriptor.snapshots.public &&
16
+ response.headers.get('x-wisconsin-application-version') === descriptor.applicationVersion
17
+ );
18
+ }
19
+
20
+ if (import.meta.main) {
21
+ const descriptor = JSON.parse(
22
+ readFileSync('build/.svelte-kit/cloudflare/_content/current.json', 'utf8')
23
+ ) as Descriptor;
24
+ if (
25
+ descriptor.schemaVersion !== 1 ||
26
+ ![descriptor.applicationVersion, descriptor.snapshots.public, descriptor.snapshots.full].every(
27
+ (value) => /^[a-f0-9]{64}$/.test(value)
28
+ )
29
+ )
30
+ throw new Error('Invalid local deployment descriptor');
31
+ const origin = new URL(process.env.DEPLOYMENT_ORIGIN ?? site.url);
32
+ if (origin.protocol !== 'https:' && origin.hostname !== '127.0.0.1')
33
+ throw new Error('Deployment verification requires HTTPS');
34
+ const started = Date.now();
35
+ const deadline = started + 45_000;
36
+ let verified = false;
37
+ while (Date.now() < deadline) {
38
+ try {
39
+ const response = await fetch(origin, {
40
+ redirect: 'manual',
41
+ headers: { 'cache-control': 'no-cache' },
42
+ signal: AbortSignal.timeout(5_000)
43
+ });
44
+ const body = await response.text();
45
+ if (
46
+ matchesDeployment(response, descriptor) &&
47
+ (!process.env.DEPLOYMENT_EXPECTED_TEXT ||
48
+ body.includes(process.env.DEPLOYMENT_EXPECTED_TEXT))
49
+ ) {
50
+ verified = true;
51
+ break;
52
+ }
53
+ } catch {}
54
+ await Bun.sleep(500);
55
+ }
56
+ if (!verified) throw new Error('The expected application/content version was not verified live');
57
+ const acceptedAt = Date.now();
58
+ let deploymentStarted = Number(process.env.DEPLOYMENT_STARTED_AT);
59
+ let timingSource = 'first-workflow-step';
60
+ // Job started_at includes runner setup; workflow creation time also includes
61
+ // queueing, so it is unsuitable for the agreed deployment measurement.
62
+ if (process.env.GITHUB_TOKEN && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID) {
63
+ try {
64
+ const jobs = await fetch(
65
+ `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/attempts/${process.env.GITHUB_RUN_ATTEMPT ?? '1'}/jobs?per_page=100`,
66
+ {
67
+ headers: {
68
+ Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
69
+ Accept: 'application/vnd.github+json'
70
+ },
71
+ signal: AbortSignal.timeout(5_000)
72
+ }
73
+ );
74
+ if (jobs.ok) {
75
+ const value = (await jobs.json()) as { jobs?: { name: string; started_at: string }[] };
76
+ const selected = value.jobs?.find((job) => job.name === process.env.GITHUB_JOB);
77
+ const timestamp = selected && Date.parse(selected.started_at);
78
+ if (timestamp && Number.isFinite(timestamp)) {
79
+ deploymentStarted = timestamp;
80
+ timingSource = 'github-job-started-at';
81
+ }
82
+ }
83
+ } catch {
84
+ /* Preserve an explicitly labeled first-step measurement. */
85
+ }
86
+ }
87
+ const report = {
88
+ verified: true,
89
+ timingSource,
90
+ verificationSeconds: (acceptedAt - started) / 1_000,
91
+ deploymentSeconds:
92
+ Number.isFinite(deploymentStarted) && deploymentStarted > 0
93
+ ? (acceptedAt - deploymentStarted) / 1_000
94
+ : null,
95
+ applicationVersion: descriptor.applicationVersion,
96
+ publicSnapshot: descriptor.snapshots.public
97
+ };
98
+ mkdirSync('build/benchmarks', { recursive: true });
99
+ writeFileSync(
100
+ path.join('build/benchmarks', 'deployment.json'),
101
+ JSON.stringify(report, null, 2) + '\n'
102
+ );
103
+ console.log(JSON.stringify(report));
104
+ if (process.env.GITHUB_STEP_SUMMARY)
105
+ appendFileSync(
106
+ process.env.GITHUB_STEP_SUMMARY,
107
+ `### Verified deployment\n\n\`\`\`json\n${JSON.stringify(report, null, 2)}\n\`\`\`\n`
108
+ );
109
+ }